diff --git a/CHANGELOG.md b/CHANGELOG.md index c052023e27..4f8a732839 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +**0.12.0** +1. Sparkz dependency updated to 2.4.0 + * Updates in EON forger nodes connection policy (see release notes for further info) +2. New stake management support (see release notes for further info) +3. Reward from mainchain - new rules (see release notes for further info) +4. Added metrics endpoint +5. Minor fixes: + * [eth RPC endpoint] web3_clientVersion now returns also the EON version + * [eth RPC endpoint] Better error handling: in case of error the response status code will be 400 instead of 200 + * [eth RPC endpoint] In case of batch requests, the response is now always an array, even with a batch composed of only one element. + **0.11.0** 1. Sparkz dependency updated to 2.3.0 2. Updated third-party dependencies diff --git a/README.md b/README.md index 2af39ebe73..bc26d9a288 100644 --- a/README.md +++ b/README.md @@ -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.11.0.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.12.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/publish.sh b/ci/publish.sh index 45667d0d34..0ca8d3e852 100755 --- a/ci/publish.sh +++ b/ci/publish.sh @@ -15,10 +15,13 @@ function publish_project () { # Building and publishing cd "${project}" - if [[ "${TRAVIS_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-RC[0-9]+)?(-SNAPSHOT){1}[0-9]*$ ]]; then + if [[ "${TRAVIS_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-SNAPSHOT){1}[0-9]*$ ]]; then echo "" && echo "=== Publishing DEVELOPMENT release of '${project}' project on Sonatype Nexus repository. Timestamp is: $(date '+%a %b %d %H:%M:%S %Z %Y') ===" && echo "" mvn deploy -P sign,build-extras --settings "${workdir}"/ci/mvn_settings.xml -DskipTests=true -B || { retval="$?"; echo "Error: was not able to publish ${project} project version = ${TRAVIS_TAG} on public repository."; } - elif [[ "${TRAVIS_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-RC[0-9]+)?$ ]]; then + elif [[ "${TRAVIS_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-RC[0-9]+){1}$ ]]; then + echo "" && echo "=== Publishing RC release of '${project}' project on Maven repository. Timestamp is: $(date '+%Y-%m-%d %H:%M') ===" && echo "" + mvn deploy -P sign,build-extras --settings "${workdir}"/ci/mvn_settings.xml -DskipTests=true -B || { retval="$?"; echo "Error: was not able to publish ${project} project version = ${TRAVIS_TAG} on public repository."; } + elif [[ "${TRAVIS_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]$ ]]; then echo "" && echo "=== Publishing PRODUCTION release of '${project}' project on Maven repository. Timestamp is: $(date '+%Y-%m-%d %H:%M') ===" && echo "" mvn deploy -P sign,build-extras --settings "${workdir}"/ci/mvn_settings.xml -DskipTests=true -B || { retval="$?"; echo "Error: was not able to publish ${project} project version = ${TRAVIS_TAG} on public repository."; } else diff --git a/ci/run_sc.sh b/ci/run_sc.sh index cf53e91932..df2a03e4d0 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.11.0}" +SIMPLE_APP_VERSION="${SIMPLE_APP_VERSION:-0.12.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/ci/setup_env.sh b/ci/setup_env.sh index 0d7c7ab367..3177ae286d 100755 --- a/ci/setup_env.sh +++ b/ci/setup_env.sh @@ -126,8 +126,8 @@ if [ -n "${TRAVIS_TAG}" ]; then for release_branch in "${prod_release_br_list[@]}"; do if ( git branch -r --contains "${TRAVIS_TAG}" | grep -xqE ". origin\/${release_branch}$" ); then # Checking format of production release pom version - if ! [[ "${root_pom_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-RC[0-9]+)?$ ]]; then - echo "Warning: package(s) version is in the wrong format for PRODUCTION release. Expecting: d.d.d(-RC[0-9]+)?. The build is not going to be released !!!" + if ! [[ "${root_pom_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Warning: package(s) version is in the wrong format for PRODUCTION release. Expecting: d.d.d. The build is not going to be released !!!" export IS_A_RELEASE="false" fi @@ -151,19 +151,29 @@ if [ -n "${TRAVIS_TAG}" ]; then # DEV release if [ "${PROD_RELEASE}" = "false" ]; then # Checking if package version matches DEV release version - if ! [[ "${root_pom_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-RC[0-9]+)?(-SNAPSHOT){1}$ ]]; then - echo "Warning: package(s) version is in the wrong format for DEVELOPMENT release. Expecting: d.d.d(-RC[0-9]+)?(-SNAPSHOT){1}. The build is not going to be released !!!" - export IS_A_RELEASE="false" - fi - - # Checking Github tag format - if ! [[ "${TRAVIS_TAG}" =~ "${root_pom_version}"[0-9]*$ ]]; then - echo "" && echo "=== Warning: GIT tag format differs from the pom file version. ===" && echo "" - echo -e "Github tag name: ${TRAVIS_TAG}\nPom file version: ${root_pom_version}.\nThe build is not going to be released !!!" + if [[ "${root_pom_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-SNAPSHOT){1}$ ]]; then + if [[ "${TRAVIS_TAG}" =~ "${root_pom_version}"[0-9]*$ ]]; then + echo "" && echo "=== Development release ===" && echo "" + export IS_A_RELEASE="true" + else + echo "" && echo "=== Warning: GIT tag format differs from the pom file version. ===" && echo "" + echo -e "Github tag name: ${TRAVIS_TAG}\nPom file version: ${root_pom_version}.\nThe build is not going to be released !!!" + export IS_A_RELEASE="false" + fi + elif [[ "${root_pom_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-RC[0-9]+){1}$ ]]; then + if [[ "${TRAVIS_TAG}" == "${root_pom_version}" ]]; then + echo "" && echo "=== RC release ===" && echo "" + export IS_A_RELEASE="true" + else + echo "" && echo "=== Warning: GIT tag format differs from the pom file version. ===" && echo "" + echo -e "Github tag name: ${TRAVIS_TAG}\nPom file version: ${root_pom_version}.\nThe build is not going to be released !!!" + export IS_A_RELEASE="false" + fi + else + echo "Warning: package(s) version is in the wrong format for DEVELOPMENT or RC release. Expecting: d.d.d(-SNAPSHOT){1} or d.d.d(-RC[0-9]+){1}. The build is not going to be released !!!" export IS_A_RELEASE="false" fi - # Announcing DEV release if [ "${IS_A_RELEASE}" = "true" ]; then export PROD_RELEASE="false" export IS_A_GH_PRERELEASE="true" diff --git a/coverage-reports/generate_report.sh b/coverage-reports/generate_report.sh index 706d928234..e36829b1f8 100755 --- a/coverage-reports/generate_report.sh +++ b/coverage-reports/generate_report.sh @@ -6,7 +6,7 @@ # this script should be run in the root of coverage-reports folder # Specify snapshot version -SNAPSHOT_VERSION_TAG="0.11.0" +SNAPSHOT_VERSION_TAG="0.12.0" # Check if SIDECHAIN_SDK is set and not empty if [ -z "$SIDECHAIN_SDK" ]; then diff --git a/doc/index.md b/doc/index.md index 5e1bab5a8c..b912501f7d 100644 --- a/doc/index.md +++ b/doc/index.md @@ -1,5 +1,6 @@ # Horizen Sidechain SDK Release Notes +## Version [0.12.0](/doc/release/0.12.0.md) ## Version [0.11.0](/doc/release/0.11.0.md) ## Version [0.10.1](/doc/release/0.10.1.md) ## Version [0.10.0](/doc/release/0.10.0.md) diff --git a/doc/release/0.12.0.md b/doc/release/0.12.0.md new file mode 100644 index 0000000000..067d2eb67b --- /dev/null +++ b/doc/release/0.12.0.md @@ -0,0 +1,133 @@ +# Release notes - version 0.12.0 + +--- + +## Notes about new/updated Features + +### Stake V2 + +Support for new stake management, with two main targets: + +- Switch from a “UTXO based” to an “Account based” model (no more stakeId assigned to each delegation operation, but just a balance assigned to each pair [forger,delegator]) +- Allow the possibility to redirect earnings to a smart-contract, which will be responsible for rewards distribution among the delegators. + +Notable changes: +- The old stake native smart contract will be deprecated/deactivated. +A new native smart contract will be activated. +Methods exposed will allow to: + + - Introduce a preliminary mandatory registration step for forgers: will be performed by executing a transaction declaring the forger public keys (VRF key and block sign key), the percentage of rewards to be redirected to a smart contract responsible to manage the delegators’ rewards (named “reward share”), and the address of that smart contract. (The last two fields will be optional). + An additional signature will be required with the method to prove the sender is the effective owner of the forger keys: for this reason the preferred way is to use the new http endpoint /transaction/registerForger to invoke the tx based on the local wallet data (it will handle automatically the additional signature). A method is also available in the native smart contract, but currently there is no way to generate a vrf signature from outside the node (it requires the cryptolib, and we don’t have the layer to invoke it from javascript) + - A minimum amount of 10 ZEN will be required to be sent with the transaction: it will be converted automatically into the first stake assigned to the forger. + - The registration step will not be required for existing forgers owning a stake before the hardfork: they will be automatically added to the list of registered ones, with “reward share” = 0 and “smart contract address” = none. + - Introduce an updateForger() method to allow forgers with “reward share” = 0 and “smart contract address” = none to update the fields. The update will be allowed only one time: once set, the values will be immutable. This protects delegators from distribution mechanisms being changed without their knowledge. + - Modify the consensus lottery to consider only forgers owning an amount of stakes (directly or delegated) equals or over to 10 ZEN. + +The following changes will happen in the http endpoints: +- /transaction/allForgingStakes and /transaction/myForgingStakes +Same format as now, but the output field stakeId will no more be present +- /transaction/makeForgerStake
+**DEPRECATED** (creation of a new stake will be doable only by calling the native smart contract method delegate) +- /transaction/spendForgingStake
+**DEPRECATED** (withdraw of a stake will be doable only by calling the native smart contract method withdraw) + +The following addition will be included in:
+ +Endpoint: /block/getFeePayments
+Rpc endpoint: zen_getFeePayments
+ +Their result will keep the same format as now, but will also include the reward paid to the address of the smart contracts (if defined). + +We will also detail the amount coming from the mainchain redistribution: to be retrocompatible they will be into additional fields valueFromMainchain and valueFromFees: + +``` +{ + "result" : { + "feePayments" : [ + { + "address" : "c49dedc85a2c360fea781bcea2bc5d58fde19", + "value" : 2000000 -> total + "valueFromMainchain:": 500000 -> part from the mainchain + "valueFromFees": 1500000 -> part from the fees + } + ] + } +} +``` +### Reward from mainchain - new rules + +The maximum ZEN amount redistributed to forgers from the special address 0x000000000000000000003333333333333333333 in a single withdrawal epoch is now limited to a maximum value expressed by the following formula: + +- MAX_VALUE_REDISTRIBUTED = sum [10% of Mainchain’s block-reward Coinbase of each mainchain block reference included in the withdrawal epoch] + +- Funds over the limit will stay in the address balance and will be redistributed in the following epochs. + +For example:
+Current Mainchain block reward: 6.25 ZEN
+Number of mainchain block-reference in a withdrawal epoch: 100
+MAX_VALUE_REDISTRIBUTED = 10%(6.25) * 100 = 62.5 ZEN
+ +### Updates in RPC endpoints + +- web3_clientVersion now returns also the EON version, in the following format: +EON_VERSION/SDK_VERSION/ARCHITECTURE/JAVA_VERSION + +- Better error handling: in case of error the response status code will be 400 instead of 200. + +- In case of batch requests, the response is now always an array, even with a batch composed of only one element. Previously, if the batch request was composed by only one element, the response was an object. + +### New metrics endpoint +A new endpoint can be optionally exposed (on a different port from the serverAPI) to show some node metrics. +To configure it, you can add the following new fragment in the settings file (fragment is optional, the value displayed are the defaults): +``` +metricsApi { + enabled = false + bindAddress = "127.0.0.1:9088" + timeout = 5s + #apiKeyHash = "" +} +``` + +Format: + +The metrics will be exposed in a http endpoint /metrics, in Prometheus format, one line per metric, in this format: + +``` +metric_id value +``` + +Available metrics: + +Following metrics will be available (also listed in the endpoint /metrics/help): + +- **block_apply_time**
+Time to apply block to node wallet and state (milliseconds) +- **block_apply_time_fromslotstart**
+Delta between timestamp when block has been applied successfully on this node and start timestamp of the slot it belongs to (milliseconds) +- **block_applied_ok**
+Number of received blocks applied successfully (absolute value since start of the node) +- **block_applied_ko**
+Number of received blocks not applied (absolute value since start of the node) +- **mempool_size**
+Mempool size (number of transactions in this node mempool) +- **forge_block_count**
+Number of forged blocks by this node (absolute value since start of the node) +- **forge_lottery_time**
+Time to execute the lottery (milliseconds) +- **forge_blockcreation_time**
+Time to create a new forged block (calculated from the start timestamp of the slot it belongs to) (milliseconds) + +### Updates in EON forger nodes connection policy + +Default value for the property: + +maxForgerConnections + +Has been increased from 20 to 100. +(Remember this only applies if no value is set explicitly in conf / docker env). + +Furthermore, the dedicated connection pool for forgers nodes (governed by the above property) now only applies if the node is itself a forger. + +--- +Full [Changelog](/CHANGELOG.md) file here + diff --git a/examples/README.md b/examples/README.md index 3097531eef..0b17158b16 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.11.0.jar;./target/lib/* io.horizen.examples.SimpleApp + java -cp ./target/sidechains-sdk-simpleapp-0.12.0.jar;./target/lib/* io.horizen.examples.SimpleApp ``` * (Linux) ``` cd ./Sidechains-SDK/examples/utxo/simpleapp - java -cp ./target/sidechains-sdk-simpleapp-0.11.0.jar:./target/lib/\* io.horizen.examples.SimpleApp + java -cp ./target/sidechains-sdk-simpleapp-0.12.0.jar:./target/lib/\* io.horizen.examples.SimpleApp ``` **Model: Account** * (Windows) ``` cd Sidechains-SDK\examples\evmapp - java -cp ./target/sidechains-sdk-evmapp-0.11.0.jar;./target/lib/* io.horizen.examples.EvmApp + java -cp ./target/sidechains-sdk-evmapp-0.12.0.jar;./target/lib/* io.horizen.examples.EvmApp ``` * (Linux) ``` cd ./Sidechains-SDK/examples/account/evmapp - java -cp ./target/sidechains-evmapp-0.11.0.jar:./target/lib/\* io.horizen.examples.EvmApp + java -cp ./target/sidechains-evmapp-0.12.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.11.0.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.12.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 c7a6e0e430..7e32c62b65 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.11.0 + 0.12.0 2022 UTF-8 @@ -15,7 +15,7 @@ io.horizen sidechains-sdk - 0.11.0 + 0.12.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 73b2a3b173..bd99b89551 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 @@ -73,6 +73,10 @@ public List> getOptiona new Pair<>( new SidechainForkConsensusEpoch(70, 70, 70), new Version1_3_0Fork(true) + ), + new Pair<>( + new SidechainForkConsensusEpoch(80, 80, 80), + new Version1_4_0Fork(true) ) ); } diff --git a/examples/account/evmapp/src/main/java/io/horizen/examples/AppForkConfiguratorAllEnabledFromEpoch2.java b/examples/account/evmapp/src/main/java/io/horizen/examples/AppForkConfiguratorAllEnabledFromEpoch2.java index bd0279c8fe..6c659eb99b 100644 --- a/examples/account/evmapp/src/main/java/io/horizen/examples/AppForkConfiguratorAllEnabledFromEpoch2.java +++ b/examples/account/evmapp/src/main/java/io/horizen/examples/AppForkConfiguratorAllEnabledFromEpoch2.java @@ -55,6 +55,10 @@ public List> getOptiona new Pair<>( new SidechainForkConsensusEpoch(2, 2, 2), new Version1_3_0Fork(true) + ), + new Pair<>( + new SidechainForkConsensusEpoch(2, 2, 2), + new Version1_4_0Fork(true) ) ); } diff --git a/examples/account/evmapp/src/main/resources/sc_evm_settings.conf b/examples/account/evmapp/src/main/resources/sc_evm_settings.conf index 5ceebb773d..ffb05869f4 100644 --- a/examples/account/evmapp/src/main/resources/sc_evm_settings.conf +++ b/examples/account/evmapp/src/main/resources/sc_evm_settings.conf @@ -13,6 +13,12 @@ sparkz { bindAddress = "127.0.0.1:9085" timeout = 5s } + metricsApi { + enabled = true + bindAddress = "127.0.0.1:9089" + timeout = 5s + } + network { nodeName = "testNode1" diff --git a/examples/account/evmapp_sctool/pom.xml b/examples/account/evmapp_sctool/pom.xml index 2a14990cf5..29f4627f37 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.11.0 + 0.12.0 2022 11 @@ -14,13 +14,13 @@ io.horizen sidechains-sdk-scbootstrappingtools - 0.11.0 + 0.12.0 compile io.horizen sidechains-sdk-evmapp - 0.11.0 + 0.12.0 compile diff --git a/examples/mc_sc_workflow_example.md b/examples/mc_sc_workflow_example.md index 28eeaa9c4a..3c0da83f82 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.11.0.jar` -- utxo: `java -jar tools/sidechains-sdk-utxo_sctools/target/sidechains-sdk-utxo_sctools-0.11.0.jar` +- account: `java -jar tools/sidechains-sdk-account_sctools/target/sidechains-sdk-account_sctools-0.12.0.jar` +- utxo: `java -jar tools/sidechains-sdk-utxo_sctools/target/sidechains-sdk-utxo_sctools-0.12.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` @@ -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.11.0.jar;./examples/simpleapp/target/lib/* io.horizen.examples.SimpleApp ./examples/my_settings.conf + java -cp ./examples/utxo/simpleapp/target/sidechains-sdk-simpleapp-0.12.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.11.0.jar:./examples/simpleapp/target/lib/* io.horizen.examples.SimpleApp ./examples/my_settings.conf + java -cp ./examples/utxo/simpleapp/target/sidechains-sdk-simpleapp-0.12.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.11.0.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.12.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.11.0.jar;./examples/evmapp/target/lib/* io.horizen.examples.EvmApp ./examples/my_settings.conf + java -cp ./examples/account/evmapp/target/sidechains-sdk-evmapp-0.12.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.11.0.jar:./examples/evmapp/target/lib/* io.horizen.examples.EvmApp ./examples/my_settings.conf + java -cp ./examples/account/evmapp/target/sidechains-sdk-evmapp-0.12.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.11.0.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.12.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 629790b3cf..5cedc1b27d 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.11.0 + 0.12.0 2018 UTF-8 @@ -15,7 +15,7 @@ io.horizen sidechains-sdk - 0.11.0 + 0.12.0 junit diff --git a/examples/utxo/utxoapp_sctool/pom.xml b/examples/utxo/utxoapp_sctool/pom.xml index 7e7166b8fb..6b2a7dcdf8 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.11.0 + 0.12.0 2018 11 @@ -14,13 +14,13 @@ io.horizen sidechains-sdk-scbootstrappingtools - 0.11.0 + 0.12.0 compile io.horizen sidechains-sdk-simpleapp - 0.11.0 + 0.12.0 compile diff --git a/pom.xml b/pom.xml index ba99885542..58046d80f5 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.horizen Sidechains - 0.11.0 + 0.12.0 2018 UTF-8 diff --git a/qa/SidechainTestFramework/account/ac_utils.py b/qa/SidechainTestFramework/account/ac_utils.py index 2cfeca69a1..5482737a78 100644 --- a/qa/SidechainTestFramework/account/ac_utils.py +++ b/qa/SidechainTestFramework/account/ac_utils.py @@ -199,11 +199,12 @@ def eoa_transfer(node, sender, receiver, amount, call_method: CallMethod = CallM return res -def contract_function_static_call(node, smart_contract_type, smart_contract_address, from_address, method, *args, - tag = 'latest', eip1898 = False, isBlockHash = False): +def contract_function_static_call(node, smart_contract_type, smart_contract_address, from_address, method, *args, value = 0, + tag='latest', eip1898=False, isBlockHash=False): logging.info("Calling {}: using static call function".format(method)) res = smart_contract_type.static_call(node, method, *args, fromAddress=from_address, - toAddress=smart_contract_address, tag=tag, eip1898=eip1898, isBlockHash=isBlockHash) + toAddress=smart_contract_address, value=value, tag=tag, eip1898=eip1898, + isBlockHash=isBlockHash) return res @@ -270,17 +271,58 @@ def estimate_gas(node, from_address=None, to_address=None, data='0x', value='0x0 def ac_makeForgerStake(sc_node, owner_address, blockSignPubKey, vrf_public_key, amount, nonce=None): - forgerStakes = {"forgerStakeInfo": { - "ownerAddress": owner_address, - "blockSignPublicKey": blockSignPubKey, - "vrfPubKey": vrf_public_key, - "value": amount # in Satoshi - }, + forgerStakes = {"forgerStakeInfo": + { + "ownerAddress": owner_address, + "blockSignPublicKey": blockSignPubKey, + "vrfPubKey": vrf_public_key, + "value": amount # in Satoshi + }, "nonce": nonce } return sc_node.transaction_makeForgerStake(json.dumps(forgerStakes)) +def ac_registerForger(sc_node, block_sign_pub_key, vrf_public_key, staked_amount, reward_address=None, reward_share=None, + nonce=None): + parameters = { + "blockSignPubKey": block_sign_pub_key, + "vrfPubKey": vrf_public_key, + "stakedAmount": staked_amount, # in Satoshi + "rewardShare": reward_share, + "rewardAddress": reward_address, + "nonce": nonce + } + return sc_node.transaction_registerForger(json.dumps(parameters)) + +def ac_updateForger(sc_node, block_sign_pub_key, vrf_public_key, reward_address, reward_share, nonce=None): + parameters = { + "blockSignPubKey": block_sign_pub_key, + "vrfPubKey": vrf_public_key, + "rewardShare": reward_share, + "rewardAddress": reward_address, + "nonce": nonce + } + return sc_node.transaction_updateForger(json.dumps(parameters)) + +def ac_pagedForgersStakesByForger(sc_node, block_sign_pub_key, vrf_public_key, start_pos=0, size=10): + parameters = { + "blockSignPubKey": block_sign_pub_key, + "vrfPubKey": vrf_public_key, + "startPos": start_pos, + "size": size + } + return sc_node.transaction_pagedForgersStakesByForger(json.dumps(parameters)) + + +def ac_pagedForgersStakesByDelegator(sc_node, delegator_address, start_pos=0, size=10): + parameters = { + "delegatorAddress": delegator_address, + "startPos": start_pos, + "size": size + } + return sc_node.transaction_pagedForgersStakesByDelegator(json.dumps(parameters)) + def ac_invokeProxy(sc_node, contract_address, data, nonce=None, static=False): params = { @@ -296,4 +338,7 @@ def ac_invokeProxy(sc_node, contract_address, data, nonce=None, static=False): else: return sc_node.transaction_invokeProxyCall(json.dumps(params)) +def rpc_get_balance(sc_node, address): + return int( + sc_node.rpc_eth_getBalance(format_evm(address), 'latest')['result'], 16) diff --git a/qa/SidechainTestFramework/account/smart_contract_resources/contracts/ForgerStakes.sol b/qa/SidechainTestFramework/account/smart_contract_resources/contracts/ForgerStakes.sol index b0355152ea..d45a140256 100644 --- a/qa/SidechainTestFramework/account/smart_contract_resources/contracts/ForgerStakes.sol +++ b/qa/SidechainTestFramework/account/smart_contract_resources/contracts/ForgerStakes.sol @@ -22,6 +22,7 @@ interface ForgerStakes { event WithdrawForgerStake(address indexed owner, bytes32 stakeId); event StakeUpgrade(uint32 oldVersion, uint32 newVersion); event OpenForgerList(uint32 indexed forgerIndex, address sender, bytes32 blockSignProposition); + event DisableStakeV1(); function getAllForgersStakes() external view returns (StakeInfo[] memory); @@ -38,4 +39,7 @@ interface ForgerStakes { function upgrade() external returns (uint32); function getPagedForgersStakes(int32 startIndex, int32 pageSize) external view returns (int32, StakeInfo[] memory); + + // disableAndMigrate can be called only after fork point 1.4 and only by the ForgerStakesV2 smart contract + function disableAndMigrate() external returns (StakeInfo[] memory); } diff --git a/qa/SidechainTestFramework/account/smart_contract_resources/contracts/ForgerStakesV2.sol b/qa/SidechainTestFramework/account/smart_contract_resources/contracts/ForgerStakesV2.sol new file mode 100644 index 0000000000..1d85be067e --- /dev/null +++ b/qa/SidechainTestFramework/account/smart_contract_resources/contracts/ForgerStakesV2.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/* + Native Contract managing forgers stakes - Version 2 (activated from EON version 1.4) + contract address: 0x0000000000000000000022222222222222222333 +*/ +interface ForgerStakesV2 { + + // Event declaration + // Up to 3 parameters can be indexed. + // Indexed parameters help you filter the logs by the indexed parameter + event RegisterForger(address indexed sender, bytes32 signPubKey, bytes32 indexed vrf1, bytes1 indexed vrf2, uint256 value, uint32 rewardShare, address reward_address); + event UpdateForger(address indexed sender, bytes32 signPubKey, bytes32 indexed vrf1, bytes1 indexed vrf2, uint32 rewardShare, address reward_address); + event DelegateForgerStake(address indexed sender, bytes32 signPubKey, bytes32 indexed vrf1, bytes1 indexed vrf2, uint256 value); + event WithdrawForgerStake(address indexed sender, bytes32 signPubKey, bytes32 indexed vrf1, bytes1 indexed vrf2, uint256 value); + event ActivateStakeV2(); + + + //Data structures + struct ForgerInfo { + bytes32 signPubKey; + bytes32 vrf1; + bytes1 vrf2; + uint32 rewardShare; + address reward_address; + } + + struct StakeDataDelegator { + address delegator; + uint256 stakedAmount; + } + + struct StakeDataForger { + bytes32 signPubKey; + bytes32 vrf1; + bytes1 vrf2; + uint256 stakedAmount; + } + + //read-write methods + + /* + Register a new forger. + rewardShare can range in [0..1000] and can be 0 if and only if rewardAddress == 0x000..00. + Vrf key and signatures are split in two or more separate parameters, being longer than 32 bytes. + sign1_x are the 25519 signature chunks and sign2_x are the Vfr signature chunks. + The message to sign is the first 31 bytes of Keccak256 hash of a string formed by the concatenation + of signPubKey+vrfKey+rewardShare+rewardAddress. rewardAddress is represented in the Eip55 + checksum format and hex strings are lowercase with no prefix. + The method accepts WEI value: the sent value will be converted to the initial stake assigned to the forger. + The initial stake amount must be >= min threshold (10 Zen) + */ + function registerForger(bytes32 signPubKey, bytes32 vrfKey1, bytes1 vrfKey2, uint32 rewardShare, + address rewardAddress, bytes32 sign1_1, bytes32 sign1_2, + bytes32 sign2_1, bytes32 sign2_2, bytes32 sign2_3, bytes1 sign2_4) external payable; + + /* + Updates an existing forger. + A forger can be updated just once and only if rewardAddress == 0x000..00 and rewardShare == 0. + See above the registerForger command for the parameters meaning. + Note: 2 epochs should be gone by after the activation of the EON 1.4 fork + */ + function updateForger(bytes32 signPubKey, bytes32 vrf1, bytes1 vrf2, uint32 rewardShare, + address rewardAddress, bytes32 sign1_1, bytes32 sign1_2, + bytes32 sign2_1, bytes32 sign2_2, bytes32 sign2_3, bytes1 sign2_4) external; + + /* + Delegate a stake to a previously registered forger. + Vrf key is split in two separate parameters, being longer than 32 bytes. + */ + function delegate(bytes32 signPubKey, bytes32 vrf1, bytes1 vrf2) external payable; + + /* + Withdraw (unstake) a previously assigned stake. + Vrf key is split in two separate parameters, being longer than 32 bytes. + */ + function withdraw(bytes32 signPubKey, bytes32 vrf1, bytes1 vrf2, uint256 amount) external; + + //read only methods + + /* + Returns the total stake amount, at the end of one or more consensus epochs, assigned to a specific forger. + vrf, signKey and delegator are optional: if all are null, the total stake amount will be returned. If only + delegator is null, all the stakes assigned to the forger will be summed. + If vrf and signKey are null, but delegator is defined, the method will fail. + consensusEpochStart and maxNumOfEpoch are optional: if both null, the data at the current consensus epoch is returned. + Be aware that following convention apply when we talk about 'null' values: for bytes parameters, as addresses of key etc., a byte array of the expected length with all 0 values is interpreted as null, eg "0x0000000000000000000000000000000000000000" for addresses. + For consensusEpochStart and maxNumOfEpoch, it is 0. + Returned array contains also elements with 0 value. Returned values are ordered by epoch, and the array length may + be < maxNumOfEpoch if the current consensus epoch is < (consensusEpochStart + maxNumOfEpoch). + */ + function stakeTotal(bytes32 signPubKey, bytes32 vrf1, bytes1 vrf2, address delegator, uint32 consensusEpochStart, uint32 maxNumOfEpoch) external view returns (uint256[] memory listOfStakes); + + /* + Return total sum paid to the forger reward_address at the end of one or more consensus epochs. + Returned array contains also elements with 0 value. Returned values are ordered by epoch, and the array length may + be < maxNumOfEpoch if the current consensus epoch is < (consensusEpochStart + maxNumOfEpoch). + */ + function rewardsReceived(bytes32 signPubKey, bytes32 vrf1, bytes1 vrf2, uint32 consensusEpochStart, uint32 maxNumOfEpoch) external view returns (uint256[] memory listOfRewards); + + /* + Returns the first consensus epoch when a stake is present for a specific delegator. + signPubKey, vrf1, vrf2 and delegator parameters are mandatory. + If no stake has been found (the delegator never staked anything to this forger) the method returns -1 + */ + function stakeStart(bytes32 signPubKey, bytes32 vrf1, bytes1 vrf2, address delegator) external view returns (int32 consensusEpochStart); + + /* + Returns the info of a specific registered forger. + */ + function getForger(bytes32 signPubKey, bytes32 vrf1, bytes1 vrf2) external view returns (ForgerInfo memory forgerInfo); + + /* + Returns the paginated list of all the registered forgers. + Each element of the list is the detail of a specific forger. + nextIndex will contain the index of the next element not returned yet. If no element is still present, next will be -1. + */ + function getPagedForgers(int32 startIndex, int32 pageSize) external view returns (int32 nextIndex, ForgerInfo[] memory listOfForgerInfo); + + /* + Returns the paginated list of stakes delegated to a specific forger, grouped by delegator address. + Each element of the list is the total amount delegated by a specific address. + nextIndex will contain the index of the next element not returned yet. If no element is still present, next will be -1. + The returned array length may be less than pageSize even if there are still additional elements because stakes with 0 amount are filtered out. + */ + function getPagedForgersStakesByForger(bytes32 signPubKey, bytes32 vrf1, bytes1 vrf2, int32 startIndex, int32 pageSize) external view returns (int32 nextIndex, StakeDataDelegator[] memory listOfDelegatorStakes); + + /* + Returns the paginated list of stakes delegated by a specific address, grouped by forger. + Each element of the list is the total amount delegated to a specific forger. + nextIndex will contain the index of the next element not returned yet. If no element is still present, next will be -1. + The returned array length may be less than pageSize even if there are still additional elements because stakes with 0 amount are filtered out. + */ + function getPagedForgersStakesByDelegator(address delegator, int32 startIndex, int32 pageSize) external view returns (int32 nextIndex, StakeDataForger[] memory listOfForgerStakes); + + /* + / Returns the current consensus epoch. + */ + function getCurrentConsensusEpoch() external view returns (uint32 epoch); + + function activate() external; +} diff --git a/qa/SidechainTestFramework/account/utils.py b/qa/SidechainTestFramework/account/utils.py index bc4fe748de..d06a72468b 100644 --- a/qa/SidechainTestFramework/account/utils.py +++ b/qa/SidechainTestFramework/account/utils.py @@ -40,6 +40,7 @@ def convertZenniesToZen(valueInZennies): CERTIFICATE_KEY_ROTATION_SMART_CONTRACT_ADDRESS = "0000000000000000000044444444444444444444" MC_ADDR_OWNERSHIP_SMART_CONTRACT_ADDRESS = "0000000000000000000088888888888888888888" PROXY_SMART_CONTRACT_ADDRESS = "00000000000000000000AAAAAAAAAAAAAAAAAAAA" +FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS = "0000000000000000000022222222222222222333" # address used for burning coins NULL_ADDRESS = "0000000000000000000000000000000000000000" @@ -53,6 +54,8 @@ def convertZenniesToZen(valueInZennies): VERSION_1_2_FORK_EPOCH = 60 # The activation epoch for features released in v1.3 (e.g. SHANGHAI EVM), as coded in the sdk VERSION_1_3_FORK_EPOCH = 70 +# The activation epoch for features released in v1.4, as coded in the sdk +VERSION_1_4_FORK_EPOCH = 80 # Block gas limit BLOCK_GAS_LIMIT = 30000000 diff --git a/qa/SidechainTestFramework/scutil.py b/qa/SidechainTestFramework/scutil.py index f63b96c284..aa311e4d94 100755 --- a/qa/SidechainTestFramework/scutil.py +++ b/qa/SidechainTestFramework/scutil.py @@ -19,7 +19,7 @@ WAIT_CONST = 1 -SNAPSHOT_VERSION_TAG = "0.11.0-SNAPSHOT" +SNAPSHOT_VERSION_TAG = "0.12.0" # log levels of the log4j trace system used by java applications APP_LEVEL_OFF = "off" diff --git a/qa/run_sc_tests.sh b/qa/run_sc_tests.sh index 510cbb6008..1008526abd 100755 --- a/qa/run_sc_tests.sh +++ b/qa/run_sc_tests.sh @@ -150,6 +150,8 @@ testScriptsEvm=( 'sc_evm_shanghai.py' 'sc_evm_pause_forging.py' 'sc_evm_forger_reward_address.py' + 'sc_evm_native_forger_v2.py' + 'sc_evm_forger_v2_register.py' ); testScriptsUtxo=( diff --git a/qa/sc_evm_forger_and_delegator_rewards.py b/qa/sc_evm_forger_and_delegator_rewards.py new file mode 100755 index 0000000000..19ba35d5d7 --- /dev/null +++ b/qa/sc_evm_forger_and_delegator_rewards.py @@ -0,0 +1,501 @@ +#!/usr/bin/env python3 +import json +import logging +import time + +from SidechainTestFramework.account.ac_chain_setup import AccountChainSetup +from SidechainTestFramework.account.ac_use_smart_contract import SmartContract +from SidechainTestFramework.account.ac_utils import ac_makeForgerStake, format_eoa, contract_function_call, \ + ac_updateForger +from SidechainTestFramework.account.httpCalls.wallet.balance import http_wallet_balance +from SidechainTestFramework.account.utils import convertZenToZennies, convertZenniesToWei, convertZenToWei, \ + computeForgedTxFee, FORGER_POOL_RECIPIENT_ADDRESS, FORGER_STAKE_SMART_CONTRACT_ADDRESS, \ + FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS +from SidechainTestFramework.sc_boostrap_info import SCNodeConfiguration, MCConnectionInfo, SCNetworkConfiguration, \ + SCCreationInfo, SCForgerConfiguration +from SidechainTestFramework.sc_forging_util import check_mcreference_presence +from SidechainTestFramework.scutil import ( + connect_sc_nodes, generate_next_block, SLOTS_IN_EPOCH, EVM_APP_SLOT_TIME, + bootstrap_sidechain_nodes, AccountModel, generate_next_blocks, ) +from httpCalls.block.getFeePayments import http_block_getFeePayments +from test_framework.util import ( + assert_equal, fail, forward_transfer_to_sidechain, assert_false, websocket_port_by_mc_node_index, +) + +""" +Check Forger fee payments: +1. Forging using stakes of different SC nodes +Configuration: + - forger 1 - reward 1 - delegator 1 - share 500 + - forger 2 - reward 1 - delegator 2 - share 500 + - forger 3 - reward 3 - delegator 3 - share 500 + - forger 4 - reward 4 - delegator 3 - share 500 + - forger 5 - reward 5 - null - null - not upgraded + - forger 6 - reward 6 - delegator 6 - share 1000 + - forger 7 - reward 7 == delegator 7 - share 500 +Test: + - advance to epoch 67 + - send funds to new address at node 1, advance to 68 + - send some zen to 6 forgers, forge required blocks to create 6 new forger stakes + - reach fork 1.3, execute upgrade + - test block forging, reach epoch 79 + - send 1000 zen to mc_reward_pool + - distribute fees, ignore the results + - send 100 zen to mc_reward_pool + - forger stake is activated + - forgers are upgraded + - distribute fees + - forger rewards: + - forger 1 <1.4 - 2/16 of mc_reward_pool + 2/16 block_fees + 2 zennies remainder + - forger 1 >1.4 - 3/16 of mc_reward_pool + 3/16 block_fees + tips + 2 zennies remainder + - forger 2 - 3/16 of mc_reward_pool + 3/16 block_fees + 2 zennies remainder + - forger 3 - 2/16 of mc_reward_pool + 2/16 block_fees + - forger 4 - 2/16 of mc_reward_pool + 2/16 block_fees + - forger 5 - 2/16 of mc_reward_pool + 2/16 block_fees + - forger 6 - 1/16 of mc_reward_pool + 1/16 block_fees + - forger 7 - 1/16 of mc_reward_pool + 1/16 block_fees + + - reward 1 gets + - 100% from forger 1 before fork + - 50% from forger 1 after fork + - 50% from forger 2 + - reward 3 gets + - 50% from forger 3 + - reward 4 gets + - 50% from forger 4 + - reward 5 gets + - 100% from forger 5 + - reward 6 gets 0 + - reward 7 gets + - 100% from forger 7(as it is == to delegator 7) + + - delegator 1 gets + - 50% from forger 1 after fork + - delegator 2 gets + - 50% from forger 2 + - delegator 3 gets + - 50% from forger 3 + - 50% from forger 4 + - delegator 6 gets + - 100% from forger 6 + - delegator 7 gets + - 100% from forger 7(as it is == to reward 7) + + This test doesn't support --allforks. +""" + + +class ScEvmForgerAndDelegatorRewards(AccountChainSetup): + FORGER_REWARD_ADDRESS_1 = '0000000000000000000012341234123412341111' + FORGER_REWARD_ADDRESS_3 = '0000000000000000000012341234123412343333' + FORGER_REWARD_ADDRESS_4 = '0000000000000000000012341234123412344444' + FORGER_REWARD_ADDRESS_5 = '0000000000000000000012341234123412345555' + FORGER_REWARD_ADDRESS_6 = '0000000000000000000012341234123412346666' + FORGER_REWARD_ADDRESS_7 = '0000000000000000000012341234123412347777' + DELEGATOR_ADDRESS_1 = '0000000000000000000056785678567856781111' + DELEGATOR_ADDRESS_2 = '0000000000000000000056785678567856782222' + DELEGATOR_ADDRESS_3 = '0000000000000000000056785678567856783333' + DELEGATOR_ADDRESS_5 = '0000000000000000000056785678567856784444' + DELEGATOR_ADDRESS_6 = '0000000000000000000056785678567856785555' + DELEGATOR_ADDRESS_7 = FORGER_REWARD_ADDRESS_7 + + def __init__(self): + super().__init__(number_of_sidechain_nodes=7, withdrawalEpochLength=20, forward_amount=50, + block_timestamp_rewind=SLOTS_IN_EPOCH * EVM_APP_SLOT_TIME * 100) + + def run_test(self): + if self.options.all_forks: + logging.info("This test cannot be executed with --allforks") + exit() + + mc_node = self.nodes[0] + sc_node_1 = self.sc_nodes[0] + sc_node_2 = self.sc_nodes[1] + sc_node_3 = self.sc_nodes[2] + sc_node_4 = self.sc_nodes[3] + sc_node_5 = self.sc_nodes[4] + sc_node_6 = self.sc_nodes[5] + sc_node_7 = self.sc_nodes[6] + connect_sc_nodes(sc_node_1, 1) + self.sc_sync_all() + self.advance_to_epoch(67) + self.sync_all() + self.sc_sync_all() + + # transfer some fund from MC to SC1 at a new evm address, then mine mc block + evm_address_sc_node_1 = sc_node_1.wallet_createPrivateKeySecp256k1()["result"]["proposition"]["address"] + ft_amount_in_zen = 1000 + forward_transfer_to_sidechain(self.sc_nodes_bootstrap_info.sidechain_id, + mc_node, + evm_address_sc_node_1, + ft_amount_in_zen, + mc_return_address=mc_node.getnewaddress(), + generate_block=False) + + mc_node.generate(1) + self.sync_all() + generate_next_block(sc_node_1, "first", force_switch_to_next_epoch=True) # 68 + self.sc_sync_all() + + # Create forgers addresses. Send zen to forgers, Create forger stakes + forger_stake_list = sc_node_1.transaction_allForgingStakes()["result"]['stakes'] + forger_1_blockSignPubKey = forger_stake_list[0]['forgerStakeData']["forgerPublicKeys"]["blockSignPublicKey"]["publicKey"] + forger_1_vrfPubKey = forger_stake_list[0]['forgerStakeData']["forgerPublicKeys"]["vrfPublicKey"]["publicKey"] + forger_2_address, forger_2_blockSignPubKey, forger_2_vrfPubKey = self.create_forger_stake(mc_node, sc_node_2, 13, 11) + forger_3_address, forger_3_blockSignPubKey, forger_3_vrfPubKey = self.create_forger_stake(mc_node, sc_node_3, 13, 11) + forger_4_address, forger_4_blockSignPubKey, forger_4_vrfPubKey = self.create_forger_stake(mc_node, sc_node_4, 13, 11) + forger_5_address, forger_5_blockSignPubKey, forger_5_vrfPubKey = self.create_forger_stake(mc_node, sc_node_5, 13, 11) + forger_6_address, forger_6_blockSignPubKey, forger_6_vrfPubKey = self.create_forger_stake(mc_node, sc_node_6, 13, 11) + forger_7_address, forger_7_blockSignPubKey, forger_7_vrfPubKey = self.create_forger_stake(mc_node, sc_node_7, 13, 11) + self.sc_sync_all() + + # we now have 7 stakes, one from creation and 6 just added + stakeList = sc_node_1.transaction_allForgingStakes()["result"]['stakes'] + assert_equal(7, len(stakeList)) + + # reach fork point 1.3 and execute stake v1 upgrade + generate_next_block(sc_node_1, "first", force_switch_to_next_epoch=True) # 69 + generate_next_block(sc_node_1, "first", force_switch_to_next_epoch=True) # 70 + # Execute upgrade + old_forger_native_contract = SmartContract("ForgerStakes") + method = 'upgrade()' + contract_function_call(sc_node_1, old_forger_native_contract, FORGER_STAKE_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_1, method) + + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=True) # 71 + self.sc_sync_all() + + # Assert block creation by each node + generate_next_block(sc_node_2, "second") + generate_next_block(sc_node_3, "third") + generate_next_block(sc_node_4, "fourth") + generate_next_block(sc_node_5, "fifth") + generate_next_block(sc_node_6, "sixth") + generate_next_block(sc_node_7, "seventh") + + # Reach epoch 79 + self.advance_to_epoch(79) + # Send 100 zen to forger pool mc reward address + ft_pool_amount = 1000 + forward_transfer_to_sidechain(self.sc_nodes_bootstrap_info.sidechain_id, + mc_node, + format_eoa(FORGER_POOL_RECIPIENT_ADDRESS), + ft_pool_amount, + mc_return_address=mc_node.getnewaddress(), + generate_block=False) + mc_node.generate(self.withdrawalEpochLength - 7) + self.sync_all() + self.sc_sync_all() + sc_last_we_block_id = generate_next_block(sc_node_1, "first node") + self.sc_sync_all() + + # Check that the fee distribution took place, exact fees before 1.4 are not tested here + api_fee_payments = http_block_getFeePayments(sc_node_1, sc_last_we_block_id)['feePayments'] + # 7 because the genesis block is counted towards node 1 forger address, not reward address + assert_equal(7, len(api_fee_payments)) + + # trigger cert submission + # Generate 2 SC blocks on SC node and start them automatic cert creation. + generate_next_block(sc_node_1, "first node") + generate_next_block(sc_node_1, "first node") # 1 SC block to trigger Submitter logic + # Wait for Certificates appearance + time.sleep(10) + while mc_node.getmempoolinfo()["size"] < 1 and sc_node_1.submitter_isCertGenerationActive()["result"][ + "state"]: + logging.info("Wait for certificates in the MC mempool...") + if sc_node_1.submitter_isCertGenerationActive()["result"]["state"]: + logging.info("sc_node generating certificate now.") + time.sleep(2) + assert_equal(1, mc_node.getmempoolinfo()["size"], "Certificates was not added to MC node mempool.") + mc_node.generate(1) + self.sync_all() + self.sc_sync_all() + + # Generate some blocks for fee distribution calculation + generate_next_block(sc_node_1, "first", force_switch_to_next_epoch=True) # 80 + ft_pool_amount = 100 + forward_transfer_to_sidechain(self.sc_nodes_bootstrap_info.sidechain_id, + mc_node, + format_eoa(FORGER_POOL_RECIPIENT_ADDRESS), + ft_pool_amount, + mc_return_address=mc_node.getnewaddress(), + generate_block=False) + # Activate forger stake v2 + # Generates 2 blocks by node 1 + forger_pool_fee_1, node_1_tip_1 = self.activate_stake_v2(evm_address_sc_node_1, sc_node_1) + + # update forger + update_tx_1 = ac_updateForger(sc_node_1, forger_1_blockSignPubKey, forger_1_vrfPubKey, reward_address=self.DELEGATOR_ADDRESS_1, reward_share=500)['result']['transactionId'] + update_tx_2 = ac_updateForger(sc_node_2, forger_2_blockSignPubKey, forger_2_vrfPubKey, reward_address=self.DELEGATOR_ADDRESS_2, reward_share=500)['result']['transactionId'] + update_tx_3 = ac_updateForger(sc_node_3, forger_3_blockSignPubKey, forger_3_vrfPubKey, reward_address=self.DELEGATOR_ADDRESS_3, reward_share=500)['result']['transactionId'] + update_tx_4 = ac_updateForger(sc_node_4, forger_4_blockSignPubKey, forger_4_vrfPubKey, reward_address=self.DELEGATOR_ADDRESS_3, reward_share=500)['result']['transactionId'] + update_tx_6 = ac_updateForger(sc_node_6, forger_6_blockSignPubKey, forger_6_vrfPubKey, reward_address=self.DELEGATOR_ADDRESS_6, reward_share=1000)['result']['transactionId'] + update_tx_7 = ac_updateForger(sc_node_7, forger_7_blockSignPubKey, forger_7_vrfPubKey, reward_address=self.DELEGATOR_ADDRESS_7, reward_share=500)['result']['transactionId'] + + self.sc_sync_all() + generate_next_block(sc_node_2, "second") + _, forger_pool_fee_2, node_2_tip_2 = computeForgedTxFee(sc_node_1, update_tx_1) + _, forger_pool_fee_3, node_2_tip_3 = computeForgedTxFee(sc_node_1, update_tx_2) + _, forger_pool_fee_4, node_2_tip_4 = computeForgedTxFee(sc_node_1, update_tx_3) + _, forger_pool_fee_5, node_2_tip_5 = computeForgedTxFee(sc_node_1, update_tx_4) + _, forger_pool_fee_6, node_2_tip_6 = computeForgedTxFee(sc_node_1, update_tx_6) + _, forger_pool_fee_7, node_2_tip_7 = computeForgedTxFee(sc_node_1, update_tx_7) + generate_next_blocks(sc_node_2, "second", 1) + generate_next_blocks(sc_node_3, "third", 2) + generate_next_blocks(sc_node_4, "fourth", 2) + generate_next_blocks(sc_node_5, "fifth", 2) + generate_next_blocks(sc_node_6, "sixth", 1) + generate_next_blocks(sc_node_7, "seventh", 1) + + mc_node.generate(self.withdrawalEpochLength - 1) + + # Fee calculations + total_block_fee = forger_pool_fee_1 + forger_pool_fee_2 + forger_pool_fee_3 + forger_pool_fee_4 + forger_pool_fee_5 + forger_pool_fee_6 + forger_pool_fee_7 + per_block_fee = total_block_fee // 16 + block_fee_remainder = total_block_fee % 16 + node_1_before_fork_remainder, node_1_after_fork_remainder, node_2_remainder, \ + node_3_remainder, node_4_remainder, node_5_remainder, node_6_remainder, node_7_remainder = self.calculate_remainders(block_fee_remainder) + + total_node_1_tips = node_1_tip_1 + total_node_2_tips = node_2_tip_2 + node_2_tip_3 + node_2_tip_4 + node_2_tip_5 + node_2_tip_6 + node_2_tip_7 + per_block_mc_reward = convertZenniesToWei(2500000000) // 16 # 1,562,500,000,000,000,000 + + forger_1_before_fork_rewards_mc = per_block_mc_reward * 2 + forger_1_before_fork_rewards_fee = per_block_fee * 2 + node_1_before_fork_remainder + forger_1_before_fork_rewards = forger_1_before_fork_rewards_mc + forger_1_before_fork_rewards_fee + forger_1_after_fork_rewards_mc = per_block_mc_reward * 3 + forger_1_after_fork_rewards_fee = per_block_fee * 3 + total_node_1_tips + node_1_after_fork_remainder + forger_1_after_fork_rewards = forger_1_after_fork_rewards_mc + forger_1_after_fork_rewards_fee + forger_2_rewards_mc = per_block_mc_reward * 3 + forger_2_rewards_fee = per_block_fee * 3 + total_node_2_tips + node_2_remainder + forger_2_rewards = forger_2_rewards_mc + forger_2_rewards_fee + forger_3_rewards_mc = per_block_mc_reward * 2 + forger_3_rewards_fee = per_block_fee * 2 + node_3_remainder + forger_3_rewards = forger_3_rewards_mc + forger_3_rewards_fee + forger_4_rewards_mc = per_block_mc_reward * 2 + forger_4_rewards_fee = per_block_fee * 2 + node_4_remainder + forger_4_rewards = forger_4_rewards_mc + forger_4_rewards_fee + forger_5_rewards_mc = per_block_mc_reward * 2 + forger_5_rewards_fee = per_block_fee * 2 + node_5_remainder + forger_5_rewards = forger_5_rewards_mc + forger_5_rewards_fee + forger_6_rewards_mc = per_block_mc_reward * 1 + forger_6_rewards_fee = per_block_fee * 1 + node_6_remainder + forger_6_rewards = forger_6_rewards_mc + forger_6_rewards_fee + forger_7_rewards_mc = per_block_mc_reward * 1 + forger_7_rewards_fee = per_block_fee * 1 + node_7_remainder + forger_7_rewards = forger_7_rewards_mc + forger_7_rewards_fee + + reward_address_1_rewards = forger_1_before_fork_rewards + (forger_1_after_fork_rewards // 2) + (forger_2_rewards // 2) + (forger_1_after_fork_rewards % 2) + (forger_2_rewards % 2) + reward_address_1_rewards_mc = forger_1_before_fork_rewards_mc + (forger_1_after_fork_rewards_mc // 2) + (forger_2_rewards_mc // 2) + (forger_1_after_fork_rewards_mc % 2) + (forger_2_rewards_mc % 2) + reward_address_1_rewards_fee = forger_1_before_fork_rewards_fee + (forger_1_after_fork_rewards_fee // 2) + (forger_2_rewards_fee // 2) + (forger_1_after_fork_rewards_fee % 2) + (forger_2_rewards_fee % 2) + reward_address_3_rewards = forger_3_rewards // 2 + forger_3_rewards % 2 + reward_address_3_rewards_mc = forger_3_rewards_mc // 2 + forger_3_rewards_mc % 2 + reward_address_3_rewards_fee = forger_3_rewards_fee // 2 + forger_3_rewards_fee % 2 + reward_address_4_rewards = forger_4_rewards // 2 + forger_4_rewards % 2 + reward_address_4_rewards_mc = forger_4_rewards_mc // 2 + forger_4_rewards_mc % 2 + reward_address_4_rewards_fee = forger_4_rewards_fee // 2 + forger_4_rewards_fee % 2 + reward_address_5_rewards = forger_5_rewards + reward_address_7_rewards = forger_7_rewards + + delegator_address_1_rewards = forger_1_after_fork_rewards // 2 + delegator_address_2_rewards = forger_2_rewards // 2 + delegator_address_3_rewards = (forger_3_rewards // 2) + (forger_4_rewards // 2) + delegator_address_6_rewards = forger_6_rewards + + sc_last_we_block_id = generate_next_block(sc_node_2, "second") + api_fee_payments = http_block_getFeePayments(sc_node_1, sc_last_we_block_id)['feePayments'] + assert_equal(9, len(api_fee_payments)) + + api_fee_payments_reward_1 = [f for f in api_fee_payments if f['address']['address'] == self.FORGER_REWARD_ADDRESS_1][0] + assert_equal(reward_address_1_rewards, api_fee_payments_reward_1['value']) + assert_equal(reward_address_1_rewards_mc, api_fee_payments_reward_1['valueFromMainchain']) + assert_equal(reward_address_1_rewards_fee, api_fee_payments_reward_1['valueFromFees']) + + api_fee_payments_reward_3 = [f for f in api_fee_payments if f['address']['address'] == self.FORGER_REWARD_ADDRESS_3][0] + assert_equal(reward_address_3_rewards, api_fee_payments_reward_3['value']) + assert_equal(reward_address_3_rewards_mc, api_fee_payments_reward_3['valueFromMainchain']) + assert_equal(reward_address_3_rewards_fee, api_fee_payments_reward_3['valueFromFees']) + + api_fee_payments_reward_4 = [f for f in api_fee_payments if f['address']['address'] == self.FORGER_REWARD_ADDRESS_4][0] + assert_equal(reward_address_4_rewards, api_fee_payments_reward_4['value']) + assert_equal(reward_address_4_rewards_mc, api_fee_payments_reward_4['valueFromMainchain']) + assert_equal(reward_address_4_rewards_fee, api_fee_payments_reward_4['valueFromFees']) + + api_fee_payments_reward_5 = [f for f in api_fee_payments if f['address']['address'] == self.FORGER_REWARD_ADDRESS_5][0] + assert_equal(reward_address_5_rewards, api_fee_payments_reward_5['value']) + assert_equal(forger_5_rewards_mc, api_fee_payments_reward_5['valueFromMainchain']) + assert_equal(forger_5_rewards_fee, api_fee_payments_reward_5['valueFromFees']) + + api_fee_payments_reward_7 = [f for f in api_fee_payments if f['address']['address'] == self.FORGER_REWARD_ADDRESS_7][0] + assert_equal(reward_address_7_rewards, api_fee_payments_reward_7['value']) + assert_equal(forger_7_rewards_mc, api_fee_payments_reward_7['valueFromMainchain']) + assert_equal(forger_7_rewards_fee, api_fee_payments_reward_7['valueFromFees']) + + api_fee_payments_delegator_1 = [f for f in api_fee_payments if f['address']['address'] == self.DELEGATOR_ADDRESS_1][0] + assert_equal(delegator_address_1_rewards, api_fee_payments_delegator_1['value']) + assert_equal(forger_1_after_fork_rewards_mc // 2, api_fee_payments_delegator_1['valueFromMainchain']) + assert_equal(forger_1_after_fork_rewards_fee // 2, api_fee_payments_delegator_1['valueFromFees']) + + api_fee_payments_delegator_2 = [f for f in api_fee_payments if f['address']['address'] == self.DELEGATOR_ADDRESS_2][0] + assert_equal(delegator_address_2_rewards, api_fee_payments_delegator_2['value']) + assert_equal(forger_2_rewards_mc // 2, api_fee_payments_delegator_2['valueFromMainchain']) + assert_equal(forger_2_rewards_fee // 2, api_fee_payments_delegator_2['valueFromFees']) + + api_fee_payments_delegator_3 = [f for f in api_fee_payments if f['address']['address'] == self.DELEGATOR_ADDRESS_3][0] + assert_equal(delegator_address_3_rewards, api_fee_payments_delegator_3['value']) + assert_equal((forger_3_rewards_mc + forger_4_rewards_mc) // 2, api_fee_payments_delegator_3['valueFromMainchain']) + assert_equal((forger_3_rewards_fee + forger_4_rewards_fee) // 2, api_fee_payments_delegator_3['valueFromFees']) + + api_fee_payments_delegator_6 = [f for f in api_fee_payments if f['address']['address'] == self.DELEGATOR_ADDRESS_6][0] + assert_equal(delegator_address_6_rewards, api_fee_payments_delegator_6['value']) + assert_equal(forger_6_rewards_mc, api_fee_payments_delegator_6['valueFromMainchain']) + assert_equal(forger_6_rewards_fee, api_fee_payments_delegator_6['valueFromFees']) + + self.sc_sync_all() + + def activate_stake_v2(self, evm_address_sc_node_1, sc_node_1): + forger_v2_native_contract = SmartContract("ForgerStakesV2") + method = 'activate()' + tx_hash = contract_function_call(sc_node_1, forger_v2_native_contract, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_1, method) + generate_next_block(sc_node_1, "first node") + _, forgersPoolFee, forgerTip = computeForgedTxFee(sc_node_1, tx_hash) + self.sc_sync_all() + generate_next_block(sc_node_1, "first node") + tx_receipt = sc_node_1.rpc_eth_getTransactionReceipt(tx_hash)['result'] + assert_equal('0x1', tx_receipt['status'], 'Transaction failed') + intrinsic_gas = 21000 + 4 * 16 # activate signature are 4 non-zero bytes + assert_equal(intrinsic_gas, int(tx_receipt['gasUsed'], 16), "wrong used gas") + return forgersPoolFee, forgerTip + + def create_forger_stake( + self, + mc_node, + sc_node, + ft_amount_in_zen, + forger_stake_amount): + address = sc_node.wallet_createPrivateKeySecp256k1()["result"]["proposition"]["address"] + block_sign_pub_key = sc_node.wallet_createPrivateKey25519()["result"]["proposition"]["publicKey"] + vrf_pub_key = sc_node.wallet_createVrfSecret()["result"]["proposition"]["publicKey"] + ft_amount_in_zennies = convertZenToZennies(ft_amount_in_zen) + ft_amount_in_wei = convertZenniesToWei(ft_amount_in_zennies) + forward_transfer_to_sidechain(self.sc_nodes_bootstrap_info.sidechain_id, + mc_node, + address, + ft_amount_in_zen, + mc_return_address=mc_node.getnewaddress(), + generate_block=False) + self.sync_all() + assert_equal(1, mc_node.getmempoolinfo()["size"], "Forward Transfer expected to be added to mempool.") + # Generate MC block and SC block + mcblock_hash1 = mc_node.generate(1)[0] + scblock_id1 = generate_next_block(self.sc_nodes[0], "first node") + check_mcreference_presence(mcblock_hash1, scblock_id1, self.sc_nodes[0]) + self.sc_sync_all() + # balance is in wei + initial_balance_2 = http_wallet_balance(sc_node, address) + assert_equal(ft_amount_in_wei, initial_balance_2) + # Create forger stake with some Zen for SC node + forger_stake_amount_in_wei = convertZenToWei(forger_stake_amount) + makeForgerStakeJsonRes = ac_makeForgerStake(sc_node, address, block_sign_pub_key, + vrf_pub_key, + convertZenToZennies(forger_stake_amount)) + if "result" not in makeForgerStakeJsonRes: + fail("make forger stake failed: " + json.dumps(makeForgerStakeJsonRes)) + else: + logging.info("Forger stake created: " + json.dumps(makeForgerStakeJsonRes)) + self.sc_sync_all() + + tx_hash_0 = makeForgerStakeJsonRes['result']['transactionId'] + # Generate SC block + generate_next_block(self.sc_nodes[0], "first node") + self.sc_sync_all() + transactionFee_0, forgersPoolFee, forgerTip = computeForgedTxFee(self.sc_nodes[0], tx_hash_0) + # balance now is initial (ft) minus forgerStake and fee + assert_equal( + ft_amount_in_wei - + (forger_stake_amount_in_wei + transactionFee_0), + sc_node.wallet_getTotalBalance()['result']['balance'] + ) + return address, block_sign_pub_key, vrf_pub_key + + def advance_to_epoch(self, epoch_number: int): + sc_node = self.sc_nodes[0] + forging_info = sc_node.block_forgingInfo() + current_epoch = forging_info["result"]["bestBlockEpochNumber"] + # make sure we are not already passed the desired epoch + assert_false(current_epoch > epoch_number, "unexpected epoch number") + while current_epoch < epoch_number: + generate_next_block(sc_node, "first node", force_switch_to_next_epoch=True) + self.sc_sync_all() + forging_info = sc_node.block_forgingInfo() + current_epoch = forging_info["result"]["bestBlockEpochNumber"] + + def sc_setup_chain(self): + 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))), + forger_options=SCForgerConfiguration(forger_reward_address=self.FORGER_REWARD_ADDRESS_1), + api_key='Horizen', + cert_submitter_enabled=True), + SCNodeConfiguration( + MCConnectionInfo(address="ws://{0}:{1}".format(mc_node.hostname, websocket_port_by_mc_node_index(0))), + forger_options=SCForgerConfiguration(forger_reward_address=self.FORGER_REWARD_ADDRESS_1), + api_key='Horizen', + cert_submitter_enabled=False), + SCNodeConfiguration( + MCConnectionInfo(address="ws://{0}:{1}".format(mc_node.hostname, websocket_port_by_mc_node_index(0))), + forger_options=SCForgerConfiguration(forger_reward_address=self.FORGER_REWARD_ADDRESS_3), + api_key='Horizen', + cert_submitter_enabled=False), + SCNodeConfiguration( + MCConnectionInfo(address="ws://{0}:{1}".format(mc_node.hostname, websocket_port_by_mc_node_index(0))), + forger_options=SCForgerConfiguration(forger_reward_address=self.FORGER_REWARD_ADDRESS_4), + api_key='Horizen', + cert_submitter_enabled=False), + SCNodeConfiguration( + MCConnectionInfo(address="ws://{0}:{1}".format(mc_node.hostname, websocket_port_by_mc_node_index(0))), + forger_options=SCForgerConfiguration(forger_reward_address=self.FORGER_REWARD_ADDRESS_5), + api_key='Horizen', + cert_submitter_enabled=False), + SCNodeConfiguration( + MCConnectionInfo(address="ws://{0}:{1}".format(mc_node.hostname, websocket_port_by_mc_node_index(0))), + forger_options=SCForgerConfiguration(forger_reward_address=self.FORGER_REWARD_ADDRESS_6), + api_key='Horizen', + cert_submitter_enabled=False), + SCNodeConfiguration( + MCConnectionInfo(address="ws://{0}:{1}".format(mc_node.hostname, websocket_port_by_mc_node_index(0))), + forger_options=SCForgerConfiguration(forger_reward_address=self.FORGER_REWARD_ADDRESS_7), + api_key='Horizen', + cert_submitter_enabled=False), + ] + + network = SCNetworkConfiguration(SCCreationInfo(mc_node, 3, 20), *sc_node_configuration) + self.sc_nodes_bootstrap_info = \ + bootstrap_sidechain_nodes(self.options, network, + block_timestamp_rewind=SLOTS_IN_EPOCH * EVM_APP_SLOT_TIME * 100, + model=AccountModel) + + def calculate_remainders(self, block_fee_remainder): + # block order: 1^, 1^, 1, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 2 + if block_fee_remainder <= 2: + return block_fee_remainder, 0, 0, 0, 0, 0, 0, 0 + if block_fee_remainder <= 5: + return 2, block_fee_remainder - 2, 0, 0, 0, 0, 0, 0 + elif block_fee_remainder <= 7: + return 2, 3, block_fee_remainder - 5, 0, 0, 0, 0, 0 + elif block_fee_remainder <= 9: + return 2, 3, 2, block_fee_remainder - 7, 0, 0, 0, 0 + elif block_fee_remainder <= 11: + return 2, 3, 2, 2, block_fee_remainder - 9, 0, 0, 0 + elif block_fee_remainder <= 13: + return 2, 3, 2, 2, 2, block_fee_remainder - 11, 0, 0 + elif block_fee_remainder == 14: + return 2, 3, 2, 2, 2, 2, 1, 0 + elif block_fee_remainder == 15: + return 2, 3, 2, 2, 2, 2, 1, 1 + + +if __name__ == "__main__": + ScEvmForgerAndDelegatorRewards().main() diff --git a/qa/sc_evm_forger_v2_perf.py b/qa/sc_evm_forger_v2_perf.py new file mode 100755 index 0000000000..e044589048 --- /dev/null +++ b/qa/sc_evm_forger_v2_perf.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +import logging +import time +from decimal import Decimal + +from eth_utils import add_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 generate_block_and_get_tx_receipt, contract_function_call, \ + ac_registerForger, contract_function_static_call +from SidechainTestFramework.account.utils import convertZenToZennies, FORGER_STAKE_SMART_CONTRACT_ADDRESS, \ + VERSION_1_3_FORK_EPOCH, \ + VERSION_1_4_FORK_EPOCH, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, convertZenniesToWei +from SidechainTestFramework.scutil import generate_next_block, EVM_APP_SLOT_TIME +from sc_evm_forger import print_current_epoch_and_slot +from test_framework.util import ( + assert_equal, forward_transfer_to_sidechain, hex_str_to_bytes, ) + +""" +This is a script for testing performance of the Forgers Lottery and of stakeTotal method. +For the Lottery, it needs to enable sidechain logging level to debug and --nocleanup, for saving the logging files. +Then using grep command, retrieve all the lines with "Lottery times". The first +value is the epoch, the second one is the time taken to create the Merkle path, the third one is the lottery total time, + i.e. Merkle path + vrfProofCheckAgainstStake. Retrieve the data from both nodes, sc_node_1 has 99 forgers while + sc_node_2 has just one forger, so it can evaluate the time taken by vrfProofCheckAgainstStake that it is executed for + each forger. +For the stakeTotal, grep in the test log file (sc_test.log) "Checkpoint, time and gas". The first value is the epoch +number, the second one is the time taken for executing the stakeTotal and the third one is the used gas. + +Configuration: + - 2 SC nodes connected with each other + - 1 MC node + +Test: + - Reach fork point 1.3 and execute upgrade + - Reach fork point 1.4 and execute activate + - Create 100 forgers, skip to epochs and try to forge blocks on both nodes + - Creates 1000 stakes in 1000 different epochs (so creating 1000 checkpoints) just for genesis forger. Execute + stakeTotal with 100 epochs, using as a starting epoch each and every epoch where the stakes were created. + +""" + + +class SCEvmForgerV2Perf(AccountChainSetup): + def __init__(self): + super().__init__(number_of_sidechain_nodes=2, forward_amount=222, + block_timestamp_rewind=1500 * EVM_APP_SLOT_TIME * 1500) + + def run_test(self): + + mc_node = self.nodes[0] + sc_node_1 = self.sc_nodes[0] + sc_node_2 = self.sc_nodes[1] + + # transfer a small fund from MC to SC2 at a new evm address, do not mine mc block + # this is for enabling SC 2 gas fee payment when sending txes + evm_address_sc_node_2 = sc_node_2.wallet_createPrivateKeySecp256k1()["result"]["proposition"]["address"] + + ft_amount_in_zen_2 = Decimal('500.0') + + forward_transfer_to_sidechain(self.sc_nodes_bootstrap_info.sidechain_id, + mc_node, + evm_address_sc_node_2, + ft_amount_in_zen_2, + mc_return_address=mc_node.getnewaddress(), + generate_block=False) + + time.sleep(2) # MC needs this + + evm_address_sc_node_1 = sc_node_1.wallet_createPrivateKeySecp256k1()["result"]["proposition"]["address"] + + ft_amount_in_zen = Decimal('2000.0') + + forward_transfer_to_sidechain(self.sc_nodes_bootstrap_info.sidechain_id, + mc_node, + evm_address_sc_node_1, + ft_amount_in_zen, + mc_return_address=mc_node.getnewaddress(), + generate_block=True) + self.sync_all() + + # Generate SC block and check that FTs appears in SCs node wallet + generate_next_block(sc_node_1, "first node") + self.sc_sync_all() + print_current_epoch_and_slot(sc_node_1) + + # Get node 1 forger keys + forger_stake_list = sc_node_1.transaction_allForgingStakes()["result"]['stakes'] + block_sign_pub_key_genesis = forger_stake_list[0]['forgerStakeData']["forgerPublicKeys"]["blockSignPublicKey"][ + "publicKey"] + vrf_pub_key_genesis = forger_stake_list[0]['forgerStakeData']["forgerPublicKeys"]["vrfPublicKey"]["publicKey"] + delegator_address_genesis = forger_stake_list[0]['forgerStakeData']["ownerPublicKey"]["address"] + + current_best_epoch = sc_node_1.block_forgingInfo()["result"]["bestBlockEpochNumber"] + for i in range(0, VERSION_1_3_FORK_EPOCH - current_best_epoch): + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=True) + self.sc_sync_all() + + native_contract = SmartContract("ForgerStakes") + + # Execute upgrade + method = 'upgrade()' + tx_hash = contract_function_call(sc_node_1, native_contract, FORGER_STAKE_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_1, method) + + # Check the receipt + tx_receipt = generate_block_and_get_tx_receipt(sc_node_1, tx_hash)['result'] + assert_equal('0x1', tx_receipt['status'], 'Transaction failed') + + # Reach fork point 1.4 + current_best_epoch = sc_node_1.block_forgingInfo()["result"]["bestBlockEpochNumber"] + for i in range(0, VERSION_1_4_FORK_EPOCH - current_best_epoch): + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=True) + self.sc_sync_all() + + # Execute activate. + forger_v2_native_contract = SmartContract("ForgerStakesV2") + method = 'activate()' + tx_hash = contract_function_call(sc_node_1, forger_v2_native_contract, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_1, method) + + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=False) + self.sc_sync_all() + + # Check the receipt and the event log + tx_receipt = generate_block_and_get_tx_receipt(sc_node_1, tx_hash)['result'] + assert_equal('0x1', tx_receipt['status'], 'Transaction failed') + + # register 100 new forgers, 50 on node1 and 50 on node2 + + reward_share = 123 + reward_address = add_0x_prefix(evm_address_sc_node_2) + MIN_STAKED_AMOUNT_IN_ZEN = 10 + staked_amount = convertZenToZennies(MIN_STAKED_AMOUNT_IN_ZEN) + + mc_node.generate(1) + + num_of_forgers = 100 # This is the total number of forgers including the one in the genesis block + for i in range(0, num_of_forgers - 2): + # Create forger keys on node 1 + block_sign_pub_key_1 = sc_node_1.wallet_createPrivateKey25519()["result"]["proposition"]["publicKey"] + vrf_pub_key_1 = sc_node_1.wallet_createVrfSecret()["result"]["proposition"]["publicKey"] + ac_registerForger(sc_node_1, block_sign_pub_key_1, vrf_pub_key_1, staked_amount, + reward_share=reward_share, + reward_address=reward_address, nonce=None) + + self.sc_sync_all() + generate_next_block(sc_node_1, "first node") + mc_node.generate(1) + self.sc_sync_all() + + # Create the remaining forger on node 2 + block_sign_pub_key_2 = sc_node_2.wallet_createPrivateKey25519()["result"]["proposition"]["publicKey"] + vrf_pub_key_2 = sc_node_2.wallet_createVrfSecret()["result"]["proposition"]["publicKey"] + + ac_registerForger(sc_node_2, block_sign_pub_key_2, vrf_pub_key_2, convertZenToZennies(100), + reward_share=reward_share, + reward_address=reward_address, nonce=None) + self.sc_sync_all() + generate_next_block(sc_node_1, "first node") + self.sc_sync_all() + + forger_stake_list = sc_node_1.transaction_allForgingStakes()["result"]['stakes'] + assert_equal(num_of_forgers, len(forger_stake_list)) + + mc_node.generate(1) + + # Switch 2 epochs so the created forgers are selected for the Lottery + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=True) + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=True) + # Try to forge blocks on node 1 and node 2, to measure the lottery time on the 2 nodes + current_epoch = sc_node_1.block_forgingInfo()["result"]["bestBlockEpochNumber"] + logging.info("Check lottery time after 2 epochs after creating all the forgers: Current epoch is {}".format( + current_epoch)) + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=False) + generate_next_block(sc_node_2, "second node", force_switch_to_next_epoch=False) + mc_node.generate(1) + + # Create several checkpoints for genesis forger and check time/gas for stakeTotal + num_of_checkpoints = 1000 + logging.info("Testing stakeTotal with number of checkpoints {}".format(num_of_checkpoints)) + + delegate_method = 'delegate(bytes32,bytes32,bytes1)' + vrf_pub_key_to_bytes = hex_str_to_bytes(vrf_pub_key_genesis) + sign_key_to_bytes = hex_str_to_bytes(block_sign_pub_key_genesis) + staked_amount = convertZenniesToWei(1) + + starting_epoch = sc_node_1.block_forgingInfo()["result"]["bestBlockEpochNumber"] + 1 + + for i in range(0, num_of_checkpoints): + contract_function_call(sc_node_1, forger_v2_native_contract, + FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_1, delegate_method, sign_key_to_bytes, + vrf_pub_key_to_bytes[0:32], vrf_pub_key_to_bytes[32:], + value=staked_amount, overrideGas=200000) + + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=True) + mc_node.generate(1) + + method = "stakeTotal(bytes32,bytes32,bytes1,address,uint32,uint32)" + + num_of_epochs = 100 + for i in range(0, num_of_checkpoints): + from_epoch = starting_epoch + i + start = time.time() + contract_function_static_call(sc_node_1, forger_v2_native_contract, + FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_2, method, sign_key_to_bytes, + vrf_pub_key_to_bytes[0:32], vrf_pub_key_to_bytes[32:], + evm_address_sc_node_1, + from_epoch, num_of_epochs) + end = time.time() + estimated_gas = forger_v2_native_contract.estimate_gas(sc_node_1, method, sign_key_to_bytes, + vrf_pub_key_to_bytes[0:32], + vrf_pub_key_to_bytes[32:], evm_address_sc_node_1, + from_epoch, num_of_epochs, + fromAddress=evm_address_sc_node_2, + toAddress=FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + tag="latest") + logging.info("Checkpoint, time and gas: {0}, {1}, {2}".format(from_epoch, end - start, estimated_gas)) + + +if __name__ == "__main__": + SCEvmForgerV2Perf().main() diff --git a/qa/sc_evm_forger_v2_register.py b/qa/sc_evm_forger_v2_register.py new file mode 100755 index 0000000000..6286725ecf --- /dev/null +++ b/qa/sc_evm_forger_v2_register.py @@ -0,0 +1,598 @@ +#!/usr/bin/env python3 +import time +import logging +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, to_checksum_address + +from SidechainTestFramework.account.ac_chain_setup import AccountChainSetup +from SidechainTestFramework.account.ac_use_smart_contract import SmartContract +from SidechainTestFramework.account.ac_utils import generate_block_and_get_tx_receipt, contract_function_call, \ + ac_registerForger, ac_pagedForgersStakesByForger, ac_pagedForgersStakesByDelegator, rpc_get_balance, \ + ac_updateForger, format_evm +from SidechainTestFramework.account.utils import convertZenToZennies, FORGER_STAKE_SMART_CONTRACT_ADDRESS, \ + VERSION_1_3_FORK_EPOCH, \ + VERSION_1_4_FORK_EPOCH, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, convertZenniesToWei, convertZenToWei, \ + computeForgedTxFee +from SidechainTestFramework.scutil import generate_next_block, EVM_APP_SLOT_TIME +from sc_evm_forger import print_current_epoch_and_slot +from test_framework.util import ( + assert_equal, assert_true, fail, forward_transfer_to_sidechain, hex_str_to_bytes, bytes_to_hex_str, ) + +""" +Configuration: + - 2 SC nodes connected with each other + - 1 MC node + - SC node 1 owns a stakeAmount made out of cross chain creation output + +Test: + - Reach fork point 1.3 and execute upgrade + - Reach fork point 1.4 and execute activate + - Try registering a forger with the same keys as the genesis one (fails) + - Do some negative test with bad parameters + - register a new forger on node1 and a new forger on node2 + - Test get all stakes and get paginate by forger methods + + +""" + + +class SCEvmForgerV2register(AccountChainSetup): + def __init__(self): + super().__init__(number_of_sidechain_nodes=2, forward_amount=222, + block_timestamp_rewind=1500 * EVM_APP_SLOT_TIME * VERSION_1_3_FORK_EPOCH) + + def run_test(self): + if self.options.all_forks: + logging.info("This test cannot be executed with --allforks") + exit() + + mc_node = self.nodes[0] + sc_node_1 = self.sc_nodes[0] + sc_node_2 = self.sc_nodes[1] + + # transfer a small fund from MC to SC2 at a new evm address, do not mine mc block + # this is for enabling SC 2 gas fee payment when sending txes + evm_address_sc_node_2 = sc_node_2.wallet_createPrivateKeySecp256k1()["result"]["proposition"]["address"] + + ft_amount_in_zen_2 = Decimal('500.0') + + forward_transfer_to_sidechain(self.sc_nodes_bootstrap_info.sidechain_id, + mc_node, + evm_address_sc_node_2, + ft_amount_in_zen_2, + mc_return_address=mc_node.getnewaddress(), + generate_block=False) + + time.sleep(2) # MC needs this + + # transfer some fund from MC to SC1 at a new evm address, then mine mc block + evm_address_sc_node_1 = sc_node_1.wallet_createPrivateKeySecp256k1()["result"]["proposition"]["address"] + + ft_amount_in_zen = Decimal('1000.0') + + forward_transfer_to_sidechain(self.sc_nodes_bootstrap_info.sidechain_id, + mc_node, + evm_address_sc_node_1, + ft_amount_in_zen, + mc_return_address=mc_node.getnewaddress(), + generate_block=True) + self.sync_all() + + # Generate SC block and check that FTs appears in SCs node wallet + generate_next_block(sc_node_1, "first node") + self.sc_sync_all() + print_current_epoch_and_slot(sc_node_1) + + # Get node 1 forger keys + forger_stake_list = sc_node_1.transaction_allForgingStakes()["result"]['stakes'] + block_sign_pub_key_genesis = forger_stake_list[0]['forgerStakeData']["forgerPublicKeys"]["blockSignPublicKey"][ + "publicKey"] + vrf_pub_key_genesis = forger_stake_list[0]['forgerStakeData']["forgerPublicKeys"]["vrfPublicKey"]["publicKey"] + delegator_address_genesis = forger_stake_list[0]['forgerStakeData']["ownerPublicKey"]["address"] + + # Create forger keys on node 2 + block_sign_pub_key_2 = sc_node_2.wallet_createPrivateKey25519()["result"]["proposition"]["publicKey"] + vrf_pub_key_2 = sc_node_2.wallet_createVrfSecret()["result"]["proposition"]["publicKey"] + + # Reach fork point 1.3 + current_best_epoch = sc_node_1.block_forgingInfo()["result"]["bestBlockEpochNumber"] + for i in range(0, VERSION_1_3_FORK_EPOCH - current_best_epoch): + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=True) + self.sc_sync_all() + + native_contract = SmartContract("ForgerStakes") + + # Execute upgrade + method = 'upgrade()' + tx_hash = contract_function_call(sc_node_1, native_contract, FORGER_STAKE_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_1, method) + + # Check the receipt + tx_receipt = generate_block_and_get_tx_receipt(sc_node_1, tx_hash)['result'] + assert_equal('0x1', tx_receipt['status'], 'Transaction failed') + + # Create one more forger keys pair on node 1 + block_sign_pub_key_1_2 = sc_node_1.wallet_createPrivateKey25519()["result"]["proposition"]["publicKey"] + vrf_pub_key_1_2 = sc_node_1.wallet_createVrfSecret()["result"]["proposition"]["publicKey"] + self.sc_sync_all() + + MIN_STAKED_AMOUNT_IN_ZEN = 10 + staked_amount = convertZenToZennies(MIN_STAKED_AMOUNT_IN_ZEN) + + # Try registerForger before fork 1.4 + res = ac_registerForger(sc_node_1, block_sign_pub_key_1_2, vrf_pub_key_1_2, staked_amount, reward_address=None, + reward_share=0, nonce=None) + assert_true('error' in res) + assert_equal('0204', res['error']['code']) + assert_true('Fork 1.4 is not active, can not invoke this command' in res['error']['description']) + + # Reach fork point 1.4 + current_best_epoch = sc_node_1.block_forgingInfo()["result"]["bestBlockEpochNumber"] + for i in range(0, VERSION_1_4_FORK_EPOCH - current_best_epoch): + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=True) + self.sc_sync_all() + + # Try registerForger before storage activation + res = ac_registerForger(sc_node_1, block_sign_pub_key_1_2, vrf_pub_key_1_2, staked_amount, reward_address=None, + reward_share=0, nonce=None) + assert_true('error' in res) + assert_equal('0204', res['error']['code']) + assert_true('Forger Stake Storage V2 is not active' in res['error']['description']) + + # Execute activate. + forger_v2_native_contract = SmartContract("ForgerStakesV2") + method = 'activate()' + tx_hash = contract_function_call(sc_node_1, forger_v2_native_contract, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_1, method) + + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=False) + self.sc_sync_all() + + # Check the receipt and the event log + tx_receipt = generate_block_and_get_tx_receipt(sc_node_1, tx_hash)['result'] + assert_equal('0x1', tx_receipt['status'], 'Transaction failed') + intrinsic_gas = 21000 + 4 * 16 # activate signature are 4 non-zero bytes + assert_equal(intrinsic_gas, int(tx_receipt['gasUsed'], 16), "wrong used gas") + assert_equal(2, len(tx_receipt['logs']), 'Wrong number of logs') + + # negative tests + # ============================================================================================================== + # - try adding the same forger as the genesis one + result = ac_registerForger(sc_node_1, block_sign_pub_key_genesis, vrf_pub_key_genesis, staked_amount, + reward_address=None, reward_share=0, nonce=None) + self.sc_sync_all() + generate_next_block(sc_node_1, "first node") + self.sc_sync_all() + # Checking the receipt + tx_id = result['result']['transactionId'] + receipt = sc_node_2.rpc_eth_getTransactionReceipt(add_0x_prefix(tx_id)) + status = int(receipt['result']['status'], 16) + assert_equal(0, status, "adding an existing forger should result in a reverted tx") + + # - try staking an invalid amount (too low) + errored_res = ac_registerForger(sc_node_1, block_sign_pub_key_1_2, vrf_pub_key_1_2, staked_amount - 1, + reward_address=None, reward_share=0, nonce=None) + if 'error' not in errored_res: + fail("Should not be able to create a valid staking") + else: + assert_true("below the minimum stake amount threshold" in errored_res['error']['description']) + + # - try adding a forger with some illegal parameters + # . invalid signer key 25519 (key not in wallet) + errored_res = ac_registerForger(sc_node_1, block_sign_pub_key_2, vrf_pub_key_1_2, staked_amount, + reward_address=None, reward_share=0, nonce=None) + if 'error' not in errored_res: + fail("Should not be able to create a valid signature 25519") + else: + assert_true("blockSignPubKey" in errored_res['error']['detail']) + + # . invalid vrf key (key not in wallet) + errored_res = ac_registerForger(sc_node_1, block_sign_pub_key_1_2, vrf_pub_key_2, staked_amount, + reward_address=None, reward_share=0, nonce=None) + if 'error' not in errored_res: + fail("Should not be able to create a valid vrf signature") + else: + assert_true("vrfPublicKey" in errored_res['error']['detail']) + + # . invalid reward share (not in allowed range) + res = ac_registerForger(sc_node_1, block_sign_pub_key_1_2, vrf_pub_key_1_2, staked_amount, reward_address=None, + reward_share=1001, nonce=None) + assert_true('error' in res) + assert_equal('0211', res['error']['code']) + assert_true( + 'Reward share must be in the range [0, 1000]' in res['error']['description']) + + # . invalid reward share/reward address + res = ac_registerForger(sc_node_1, block_sign_pub_key_1_2, vrf_pub_key_1_2, staked_amount, reward_address=None, + reward_share=1000, nonce=None) + assert_true('error' in res) + assert_equal('0211', res['error']['code']) + assert_true( + 'Reward share cannot be different from 0 if reward address is null' in res['error']['description']) + + reward_address = add_0x_prefix(evm_address_sc_node_2) + res = ac_registerForger(sc_node_1, block_sign_pub_key_1_2, vrf_pub_key_1_2, staked_amount, + reward_address=reward_address, reward_share=0, nonce=None) + assert_true('error' in res) + assert_equal('0211', res['error']['code']) + assert_true('Reward share cannot be 0 if reward address is defined ' in res['error']['description']) + + + # . invalid reward address string (wrong length) + res = ac_registerForger(sc_node_1, block_sign_pub_key_1_2, vrf_pub_key_1_2, staked_amount, reward_address="0x111111112222222233333333444444445555555566", + reward_share=1000, nonce=None) + assert_true('error' in res) + assert_equal('0211', res['error']['code']) + assert_true( + 'Invalid address string length' in res['error']['description']) + + # . invalid reward address string (wrong hex string) + res = ac_registerForger(sc_node_1, block_sign_pub_key_1_2, vrf_pub_key_1_2, staked_amount, reward_address="0x111111112222222233333333444444445555555h", + reward_share=1000, nonce=None) + assert_true('error' in res) + assert_equal('0211', res['error']['code']) + assert_true( + 'Unrecognized character: h' in res['error']['description']) + + # register a new forger + reward_share = 0 + evm_address_sc_node_1_balance = rpc_get_balance(sc_node_1, evm_address_sc_node_1) + forger_contract_balance = rpc_get_balance(sc_node_1, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS) + + result = ac_registerForger(sc_node_1, block_sign_pub_key_1_2, vrf_pub_key_1_2, staked_amount, reward_share=reward_share) + + self.sc_sync_all() + generate_next_block(sc_node_1, "first node") + self.sc_sync_all() + + # Checking the receipt + tx_id = result['result']['transactionId'] + receipt = sc_node_2.rpc_eth_getTransactionReceipt(add_0x_prefix(tx_id)) + status = int(receipt['result']['status'], 16) + assert_equal(1, status, "Registering a forger should succeed") + assert_equal(1, len(receipt['result']['logs']), 'Wrong number of logs') + register_event = receipt['result']['logs'][0] + staked_amount_in_wei = convertZenniesToWei(staked_amount) + check_register_event(register_event, evm_address_sc_node_1, vrf_pub_key_1_2, block_sign_pub_key_1_2, + staked_amount_in_wei, reward_share, reward_address) + + gas_fee_paid, _, _ = computeForgedTxFee(sc_node_1, tx_id) + assert_equal(evm_address_sc_node_1_balance - staked_amount_in_wei - gas_fee_paid, + rpc_get_balance(sc_node_1, evm_address_sc_node_1)) + assert_equal(staked_amount_in_wei + forger_contract_balance, + rpc_get_balance(sc_node_1, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS)) + forger_contract_balance += staked_amount_in_wei + + # we have two forgers now, check legacy command + stake_list = sc_node_1.transaction_allForgingStakes()["result"]['stakes'] + assert_equal(2, len(stake_list)) + + # genesis forger + list1 = ac_pagedForgersStakesByForger(sc_node_1, block_sign_pub_key_genesis, vrf_pub_key_genesis) + assert_equal(1, len(list1)) + assert_equal(list1['result']['stakes'][0]['stakedAmount'], convertZenToWei(self.forward_amount)) + assert_equal(list1['result']['stakes'][0]['delegator']['address'], delegator_address_genesis) + + # second forger + evm_address_sc_node_2_balance = rpc_get_balance(sc_node_1, evm_address_sc_node_2) + list2 = ac_pagedForgersStakesByForger(sc_node_1, block_sign_pub_key_1_2, vrf_pub_key_1_2) + assert_equal(1, len(list2)) + assert_equal(list2['result']['stakes'][0]['stakedAmount'], staked_amount_in_wei) + + # register a third forger at node2 using native smart contract. This is for testing purposes, we are hard coding + # the correct signatures corresponding to pub keys and message to sign + reward_share = 1000 + reward_address = evm_address_sc_node_2 + reward_address_bytes = hex_str_to_bytes(reward_address) + + forger_sign_key_bytes = hex_str_to_bytes(block_sign_pub_key_2) + forger_vrf_key_bytes = hex_str_to_bytes(vrf_pub_key_2) + signature25519_bytes = hex_str_to_bytes( + "776c7362afed8799826d1c61a202c248d11c82866c804db3ed919ecef8581fc65db2a019a543197fa150a3b923aca950b377cbd12701afe4c53361f29f971709") + signature_vrf_bytes = hex_str_to_bytes( + "d710141f62b7f656aaa21ae6fba716774d38e39708374c8b6e6a059482204e2a00e4dd3aabc76d76744e39f2f3e35c26c4ac7837d6ebc757ba4b31fbf92a1b9e0ea337dbf3deecb39e9df9134fc79107469d79acf44ee215eb7e063ab083489725") + + staked_amount_2_in_wei = convertZenToWei(33) + register_forger_method = 'registerForger(bytes32,bytes32,bytes1,uint32,address,bytes32,bytes32,bytes32,bytes32,bytes32,bytes1)' + tx_id = contract_function_call(sc_node_2, forger_v2_native_contract, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_2, register_forger_method, + forger_sign_key_bytes, + forger_vrf_key_bytes[0:32], + forger_vrf_key_bytes[32:], + reward_share, + reward_address_bytes, + signature25519_bytes[0:32], + signature25519_bytes[32:], + signature_vrf_bytes[0:32], + signature_vrf_bytes[32:64], + signature_vrf_bytes[64:96], + signature_vrf_bytes[96:], + value=staked_amount_2_in_wei) + self.sc_sync_all() + + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=False) + self.sc_sync_all() + + # Checking the receipt + receipt = sc_node_2.rpc_eth_getTransactionReceipt(add_0x_prefix(tx_id)) + status = int(receipt['result']['status'], 16) + assert_equal(1, status, "Registering a forger should succeed") + register_event = receipt['result']['logs'][0] + check_register_event(register_event, evm_address_sc_node_2, vrf_pub_key_2, block_sign_pub_key_2, + staked_amount_2_in_wei, reward_share, reward_address) + + gas_fee_paid, _, _ = computeForgedTxFee(sc_node_2, tx_id) + assert_equal(evm_address_sc_node_2_balance - staked_amount_2_in_wei - gas_fee_paid, + rpc_get_balance(sc_node_2, evm_address_sc_node_2)) + assert_equal(staked_amount_2_in_wei + forger_contract_balance, + rpc_get_balance(sc_node_1, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS)) + forger_contract_balance += staked_amount_2_in_wei + + list3 = ac_pagedForgersStakesByForger(sc_node_2, block_sign_pub_key_2, vrf_pub_key_2) + assert_equal(1, len(list3)) + assert_equal(list3['result']['stakes'][0]['stakedAmount'], staked_amount_2_in_wei) + + list4 = ac_pagedForgersStakesByDelegator(sc_node_1, add_0x_prefix(evm_address_sc_node_1)) + assert_equal(1, len(list4)) + assert_equal(list4['result']['stakes'][0]['stakedAmount'], staked_amount_in_wei) + assert_equal(list4['result']['stakes'][0]['forgerPublicKeys']['blockSignPublicKey']['publicKey'], + block_sign_pub_key_1_2) + assert_equal(list4['result']['stakes'][0]['forgerPublicKeys']['vrfPublicKey']['publicKey'], vrf_pub_key_1_2) + + list5 = ac_pagedForgersStakesByDelegator(sc_node_1, add_0x_prefix(delegator_address_genesis)) + assert_equal(1, len(list5)) + assert_equal(list5['result']['stakes'][0]['stakedAmount'], convertZenToWei(self.forward_amount)) + assert_equal(list5['result']['stakes'][0]['forgerPublicKeys']['blockSignPublicKey']['publicKey'], + block_sign_pub_key_genesis) + assert_equal(list5['result']['stakes'][0]['forgerPublicKeys']['vrfPublicKey']['publicKey'], vrf_pub_key_genesis) + + + reward_share_updated = 1 + reward_address_updated = "0x1111111122222222333333334444444455555555" + + # switch to the next epoch + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=True) + self.sc_sync_all() + + # negative tests + # ============================================================================================================== + # - try updating a forger before 2 epochs pass by after the fork activation + res = ac_updateForger(sc_node_1, block_sign_pub_key_1_2, vrf_pub_key_1_2, + reward_address=reward_address_updated, reward_share=reward_share_updated) + self.sc_sync_all() + + assert_true('error' in res) + assert_equal('0204', res['error']['code']) + assert_true(' 2 epochs must go by before invoking this command' in res['error']['description']) + + # switch to the next epoch for the second time, now it is ok invoking update forger cmd + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=True) + self.sc_sync_all() + + # - try updating a forger that does not exist + result = ac_updateForger(sc_node_1, block_sign_pub_key_genesis, vrf_pub_key_1_2, + reward_address=reward_address_updated, reward_share=reward_share_updated) + self.sc_sync_all() + generate_next_block(sc_node_1, "first node") + self.sc_sync_all() + tx_id = result['result']['transactionId'] + receipt = sc_node_2.rpc_eth_getTransactionReceipt(add_0x_prefix(tx_id)) + status = int(receipt['result']['status'], 16) + assert_equal(0, status, "Upgrade forger should fail") + + + # - try updating a forger that currently has a reward share not null + result = ac_updateForger(sc_node_2, block_sign_pub_key_2, vrf_pub_key_2, + reward_address=reward_address_updated, reward_share=reward_share_updated) + self.sc_sync_all() + generate_next_block(sc_node_1, "first node") + self.sc_sync_all() + tx_id = result['result']['transactionId'] + receipt = sc_node_2.rpc_eth_getTransactionReceipt(add_0x_prefix(tx_id)) + status = int(receipt['result']['status'], 16) + assert_equal(0, status, "Upgrade forger should fail") + + # - try updating a forger specifying a null reward_share + res = ac_updateForger(sc_node_1, block_sign_pub_key_1_2, vrf_pub_key_1_2, + reward_address=reward_address_updated, reward_share=0) + self.sc_sync_all() + generate_next_block(sc_node_1, "first node") + self.sc_sync_all() + assert_true('error' in res) + assert_equal('0211', res['error']['code']) + assert_true('Reward share must be in the range (0, 1000]' in res['error']['description']) + + # - try updating a forger with the null reward address + res = ac_updateForger(sc_node_1, block_sign_pub_key_1_2, vrf_pub_key_1_2, + reward_address="0x0000000000000000000000000000000000000000", reward_share=reward_share_updated) + self.sc_sync_all() + generate_next_block(sc_node_1, "first node") + self.sc_sync_all() + assert_true('error' in res) + assert_equal('0211', res['error']['code']) + assert_true('Reward address can not be the null address' in res['error']['description']) + + + # - try updating a forger with an invalid reward address (wrong length) + res = ac_updateForger(sc_node_1, block_sign_pub_key_1_2, vrf_pub_key_1_2, + reward_address="0x0", reward_share=reward_share_updated) + self.sc_sync_all() + generate_next_block(sc_node_1, "first node") + self.sc_sync_all() + assert_true('error' in res) + assert_equal('0211', res['error']['code']) + assert_true('Invalid address string length' in res['error']['description']) + + # - try updating a forger with an invalid reward address (wrong hex string) + res = ac_updateForger(sc_node_1, block_sign_pub_key_1_2, vrf_pub_key_1_2, + reward_address="0h11111111222222223333333344444444555555", reward_share=reward_share_updated) + self.sc_sync_all() + generate_next_block(sc_node_1, "first node") + self.sc_sync_all() + assert_true('error' in res) + assert_equal('0211', res['error']['code']) + assert_true( + 'Unrecognized character: h' in res['error']['description']) + + # update first forger + result = ac_updateForger(sc_node_1, block_sign_pub_key_1_2, vrf_pub_key_1_2, + reward_address=reward_address_updated, reward_share=reward_share_updated) + + self.sc_sync_all() + generate_next_block(sc_node_1, "first node") + self.sc_sync_all() + + # Checking the receipt + tx_id = result['result']['transactionId'] + receipt = sc_node_2.rpc_eth_getTransactionReceipt(add_0x_prefix(tx_id)) + status = int(receipt['result']['status'], 16) + assert_equal(1, status, "Upgrade a forger should succeed") + assert_equal(1, len(receipt['result']['logs']), 'Wrong number of logs') + update_event = receipt['result']['logs'][0] + check_update_event(update_event, evm_address_sc_node_1, vrf_pub_key_1_2, block_sign_pub_key_1_2, + reward_share_updated, reward_address_updated) + + # Check getForger + method = 'getForger(bytes32,bytes32,bytes1)' + forger_sign_key_to_bytes = hex_str_to_bytes(block_sign_pub_key_1_2) + forger_vrf_pub_key_to_bytes = hex_str_to_bytes(vrf_pub_key_1_2) + forger_keys_to_bytes = (forger_sign_key_to_bytes, forger_vrf_pub_key_to_bytes[:32], + forger_vrf_pub_key_to_bytes[32:]) + data = forger_v2_native_contract.raw_encode_call(method, *forger_keys_to_bytes) + + result = sc_node_1.rpc_eth_call( + { + "to": format_evm(FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS), + "from": format_evm(evm_address_sc_node_1), + "input": data + }, "latest" + ) + + forger_info = decode_forger_info(hex_str_to_bytes(result['result'][2:])) + assert_equal(reward_share_updated, forger_info[2]) + assert_equal(add_0x_prefix(reward_address_updated), forger_info[3]) + + # update genesis forger via contract call + reward_share_gen_updated = 33 + reward_address_gen_updated = "3333333333333333333333333333333333333333" + + reward_address_bytes = hex_str_to_bytes(reward_address_gen_updated) + forger_sign_key_bytes = hex_str_to_bytes(block_sign_pub_key_genesis) + forger_vrf_key_bytes = hex_str_to_bytes(vrf_pub_key_genesis) + signature25519_bytes = hex_str_to_bytes("e50f654c4bff1e99c282b0eec21ced63fac7ed28d9a7d4f6529c2dd7a7bce93fd419f1288c8c865a325f9b2a4439aab696384159b736f5ae1562e72f0638a50e") + signature_vrf_bytes = hex_str_to_bytes("8f0964a947fe634832cfbef589ed5956792085cd462b5a44d64bd0d0bdc75a0c0062b9c5cc55680ebdec91917721668d725c3b5f4f5c8529f8ed4458c86fb831279e7c8abfefc30eca46c565928d9c89adf27e2359f3827fcc8b6f62f4a4b8ee36") + + update_forger_method = 'updateForger(bytes32,bytes32,bytes1,uint32,address,bytes32,bytes32,bytes32,bytes32,bytes32,bytes1)' + tx_id = contract_function_call(sc_node_1, forger_v2_native_contract, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_1, update_forger_method, + forger_sign_key_bytes, + forger_vrf_key_bytes[0:32], + forger_vrf_key_bytes[32:], + reward_share_gen_updated, + reward_address_bytes, + signature25519_bytes[0:32], + signature25519_bytes[32:], + signature_vrf_bytes[0:32], + signature_vrf_bytes[32:64], + signature_vrf_bytes[64:96], + signature_vrf_bytes[96:]) + self.sc_sync_all() + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=False) + self.sc_sync_all() + + # Checking the receipt + receipt = sc_node_2.rpc_eth_getTransactionReceipt(add_0x_prefix(tx_id)) + status = int(receipt['result']['status'], 16) + assert_equal(1, status, "Registering a forger should succeed") + update_event = receipt['result']['logs'][0] + check_update_event(update_event, evm_address_sc_node_1, vrf_pub_key_genesis, block_sign_pub_key_genesis, + reward_share_gen_updated, reward_address_gen_updated) + + + # Check getForger + method = 'getForger(bytes32,bytes32,bytes1)' + forger_sign_key_to_bytes = hex_str_to_bytes(block_sign_pub_key_genesis) + forger_vrf_pub_key_to_bytes = hex_str_to_bytes(vrf_pub_key_genesis) + forger_keys_to_bytes = (forger_sign_key_to_bytes, forger_vrf_pub_key_to_bytes[:32], + forger_vrf_pub_key_to_bytes[32:]) + data = forger_v2_native_contract.raw_encode_call(method, *forger_keys_to_bytes) + + result = sc_node_1.rpc_eth_call( + { + "to": format_evm(FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS), + "from": format_evm(evm_address_sc_node_1), + "input": data + }, "latest" + ) + + forger_info = decode_forger_info(hex_str_to_bytes(result['result'][2:])) + assert_equal(reward_share_gen_updated, forger_info[2]) + assert_equal(add_0x_prefix(reward_address_gen_updated), forger_info[3]) + + + + +def decode_forger_info(result): + raw_stake = decode(['(bytes32,bytes32,bytes1,uint32,address)'], result)[0] + forger_info = (bytes_to_hex_str(raw_stake[0]), + bytes_to_hex_str(raw_stake[1]) + bytes_to_hex_str(raw_stake[2]), + raw_stake[3], raw_stake[4]) + + return forger_info + +def check_register_event(register_forger_event, sender, vrf_pub_key, block_sign_pub_key, staked_amount, rewards_share, + reward_address): + assert_equal(4, len(register_forger_event['topics']), "Wrong number of topics in register_event") + event_id = remove_0x_prefix(register_forger_event['topics'][0]) + event_signature = remove_0x_prefix( + encode_hex( + event_signature_to_log_topic('RegisterForger(address,bytes32,bytes32,bytes1,uint256,uint32,address)'))) + assert_equal(event_signature, event_id, "Wrong event signature in topics") + + from_addr = decode(['address'], hex_str_to_bytes(register_forger_event['topics'][1][2:]))[0][2:] + assert_equal(sender.lower(), from_addr.lower(), "Wrong from address in topics") + + vrf1 = decode(['bytes32'], hex_str_to_bytes(register_forger_event['topics'][2][2:]))[0] + vrf2 = decode(['bytes1'], hex_str_to_bytes(register_forger_event['topics'][3][2:]))[0] + assert_equal(vrf_pub_key, + bytes_to_hex_str(vrf1) + bytes_to_hex_str(vrf2), "wrong vrfPublicKey") + + (pubKey25519, value, share, reward_contract_address) = decode(['bytes32', 'uint256', 'uint32', 'address'], + hex_str_to_bytes(register_forger_event['data'][2:])) + + assert_equal(block_sign_pub_key, bytes_to_hex_str(pubKey25519), "Wrong from address in topics") + assert_equal(staked_amount, value, "Wrong amount in event") + assert_equal(rewards_share, share, "Wrong rewards_share in event") + assert_equal(reward_address, reward_address, "Wrong reward_address in event") + + + +def check_update_event(update_forger_event, sender, vrf_pub_key, block_sign_pub_key, rewards_share, + reward_address): + assert_equal(4, len(update_forger_event['topics']), "Wrong number of topics in update forger event") + event_id = remove_0x_prefix(update_forger_event['topics'][0]) + event_signature = remove_0x_prefix( + encode_hex( + event_signature_to_log_topic('UpdateForger(address,bytes32,bytes32,bytes1,uint32,address)'))) + assert_equal(event_signature, event_id, "Wrong event signature in topics") + + from_addr = decode(['address'], hex_str_to_bytes(update_forger_event['topics'][1][2:]))[0][2:] + assert_equal(sender.lower(), from_addr.lower(), "Wrong from address in topics") + + vrf1 = decode(['bytes32'], hex_str_to_bytes(update_forger_event['topics'][2][2:]))[0] + vrf2 = decode(['bytes1'], hex_str_to_bytes(update_forger_event['topics'][3][2:]))[0] + assert_equal(vrf_pub_key, + bytes_to_hex_str(vrf1) + bytes_to_hex_str(vrf2), "wrong vrfPublicKey") + + (pubKey25519, share, reward_contract_address) = decode(['bytes32', 'uint32', 'address'], + hex_str_to_bytes(update_forger_event['data'][2:])) + + assert_equal(block_sign_pub_key, bytes_to_hex_str(pubKey25519), "Wrong from address in topics") + assert_equal(rewards_share, share, "Wrong rewards_share in event") + assert_equal(reward_address, reward_address, "Wrong reward_address in event") + + +if __name__ == "__main__": + SCEvmForgerV2register().main() diff --git a/qa/sc_evm_forging_fee_payments.py b/qa/sc_evm_forging_fee_payments.py index b0dec5f369..8ddb479dd9 100755 --- a/qa/sc_evm_forging_fee_payments.py +++ b/qa/sc_evm_forging_fee_payments.py @@ -15,7 +15,7 @@ from SidechainTestFramework.account.httpCalls.wallet.balance import http_wallet_balance from SidechainTestFramework.account.utils import convertZenToZennies, convertZenniesToWei, convertZenToWei, \ computeForgedTxFee, FORGER_POOL_RECIPIENT_ADDRESS, VERSION_1_2_FORK_EPOCH, FORGER_STAKE_SMART_CONTRACT_ADDRESS, \ - VERSION_1_3_FORK_EPOCH + VERSION_1_3_FORK_EPOCH, VERSION_1_4_FORK_EPOCH from SidechainTestFramework.sc_boostrap_info import SCNodeConfiguration, MCConnectionInfo, SCNetworkConfiguration, \ SCCreationInfo, SCForgerConfiguration from SidechainTestFramework.sc_forging_util import check_mcreference_presence @@ -117,7 +117,7 @@ def run_test(self): # Do FT of some Zen to SC Node 2 evm_address_sc_node_2 = sc_node_2.wallet_createPrivateKeySecp256k1()["result"]["proposition"]["address"] - ft_amount_in_zen = 2.0 + ft_amount_in_zen = 12.0 ft_amount_in_zennies = convertZenToZennies(ft_amount_in_zen) ft_amount_in_wei = convertZenniesToWei(ft_amount_in_zennies) @@ -149,7 +149,7 @@ def run_test(self): sc2_blockSignPubKey = sc_node_2.wallet_createPrivateKey25519()["result"]["proposition"]["publicKey"] sc2_vrfPubKey = sc_node_2.wallet_createVrfSecret()["result"]["proposition"]["publicKey"] - forger_stake_amount = 1 # Zen + forger_stake_amount = 11 # Zen forger_stake_amount_in_wei = convertZenToWei(forger_stake_amount) makeForgerStakeJsonRes = ac_makeForgerStake(sc_node_2, evm_address_sc_node_2, sc2_blockSignPubKey, @@ -238,7 +238,7 @@ def run_test(self): # let assume a portion of the MC coinbase is sent to the SC as a contribution to the forger pool # this funds should not be distributed until fork happens at epoch 60 - ft_pool_amount = 0.5 + ft_pool_amount = 100 ft_pool_amount_wei = convertZenToWei(ft_pool_amount) forward_transfer_to_sidechain(self.sc_nodes_bootstrap_info.sidechain_id, mc_node, @@ -368,18 +368,27 @@ def run_test(self): assert_equal(1, mc_node.getmempoolinfo()["size"], "Certificates was not added to MC node mempool.") - # Advance to epoch 60 to enable forger pool fork. First block will already be counted for the distribution - # Generate more blocks so that in total there were 5 blocks from node_1 and 3 blocks from node_2 - self.advance_to_epoch(VERSION_1_2_FORK_EPOCH) - generate_next_blocks(sc_node_1, "first node", 4) + # Advance to epoch 80 to enable forger pool fork + forger pool distribution cap fork. + # First block will already be counted for the distribution. + # Generate more blocks so that in total there were 22 blocks from node_1 and 3 blocks from node_2 + self.advance_to_epoch(VERSION_1_4_FORK_EPOCH) + generate_next_blocks(sc_node_1, "first node", 1) generate_next_blocks(sc_node_2, "second node", 2) mc_node.generate(self.withdrawalEpochLength) self.sc_sync_all() last_block_id = generate_next_block(sc_node_2, "second node") self.sc_sync_all() - per_block_fee = convertZenToWei(ft_pool_amount) // 8 - node_1_fees = per_block_fee * 5 + + # 2500000000 = 12.5 * 10^8 * 20 * 0.1, where + # 12.5 * 10^8 - base mainchain coinbase reward + # 20 - withdrawal epoch length + # 0.1 - divider/coefficient (10% of sum of mainchain coinbase reward for last epoch) + mc_withdrawal_epoch_distribution_cap = 2500000000 + distribution_cap = convertZenniesToWei(mc_withdrawal_epoch_distribution_cap) + per_block_fee = distribution_cap // 25 + ft_pool_remaining = ft_pool_amount_wei - distribution_cap + node_1_fees = per_block_fee * 22 node_2_fees = per_block_fee * 3 sc_node_1_balance_before_payments = sc_node_1_balance_after_payments @@ -396,19 +405,20 @@ def run_test(self): # assert forger pool balance is 0 now, as the fees are distributed forger_pool_balance = int(self.sc_nodes[0].rpc_eth_getBalance(format_evm(FORGER_POOL_RECIPIENT_ADDRESS), 'latest')['result'], 16) - assert_equal(0, forger_pool_balance) + assert_equal(ft_pool_remaining, forger_pool_balance) fee_payments_api_response = http_block_getFeePayments(sc_node_1, last_block_id)['feePayments'] - assert_equal(node_1_fees, fee_payments_api_response[0]['value']) - assert_equal(node_2_fees, fee_payments_api_response[1]['value']) - - # reach the VERSION_1_3_FORK_EPOCH fork and upgrade the forger stakes to the new format - current_best_epoch = sc_node_1.block_forgingInfo()["result"]["bestBlockEpochNumber"] - - for i in range(0, VERSION_1_3_FORK_EPOCH - current_best_epoch): - generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=True) - self.sc_sync_all() - + assert_equal(node_1_fees, [f for f in fee_payments_api_response if f['address']['address'] != self.FORGER_REWARD_ADDRESS][0]['value']) + assert_equal(node_2_fees, [f for f in fee_payments_api_response if f['address']['address'] == self.FORGER_REWARD_ADDRESS][0]['value']) + rpc_fee_payments_node1 = sc_node_1.rpc_zen_getFeePayments(add_0x_prefix(last_block_id))['result']['payments'] + rpc_fee_payments_node2 = sc_node_2.rpc_zen_getFeePayments(add_0x_prefix(last_block_id))['result']['payments'] + assert_equal(rpc_fee_payments_node1, rpc_fee_payments_node2) + assert_equal(rpc_fee_payments_node1[0]['value'], rpc_fee_payments_node1[0]['valueFromMainchain']) + assert_equal(rpc_fee_payments_node1[1]['value'], rpc_fee_payments_node1[1]['valueFromMainchain']) + assert_equal('0x0', rpc_fee_payments_node1[0]['valueFromFees']) + assert_equal('0x0', rpc_fee_payments_node1[1]['valueFromFees']) + assert_equal(node_1_fees, int([f for f in rpc_fee_payments_node1 if f['address'] != '0x' + self.FORGER_REWARD_ADDRESS][0]['value'], 16)) + assert_equal(node_2_fees, int([f for f in rpc_fee_payments_node1 if f['address'] == '0x' + self.FORGER_REWARD_ADDRESS][0]['value'], 16)) ''' ##################################################################################### ''' diff --git a/qa/sc_evm_native_forger_v2.py b/qa/sc_evm_native_forger_v2.py new file mode 100755 index 0000000000..5cee752c46 --- /dev/null +++ b/qa/sc_evm_native_forger_v2.py @@ -0,0 +1,1093 @@ +#!/usr/bin/env python3 +import json +import logging +import time +from decimal import Decimal + +from eth_abi import decode +from eth_utils import encode_hex, event_signature_to_log_topic, remove_0x_prefix, add_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 (ac_makeForgerStake, \ + format_evm, + generate_block_and_get_tx_receipt, contract_function_static_call, + contract_function_call, format_eoa, \ + rpc_get_balance) +from SidechainTestFramework.account.httpCalls.transaction.createEIP1559Transaction import createEIP1559Transaction +from SidechainTestFramework.account.simple_proxy_contract import SimpleProxyContract +from SidechainTestFramework.account.utils import convertZenToZennies, FORGER_STAKE_SMART_CONTRACT_ADDRESS, \ + VERSION_1_3_FORK_EPOCH, \ + VERSION_1_4_FORK_EPOCH, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, convertZenToWei, computeForgedTxFee, \ + FORGER_POOL_RECIPIENT_ADDRESS +from SidechainTestFramework.scutil import generate_next_block, EVM_APP_SLOT_TIME +from httpCalls.block.getFeePayments import http_block_getFeePayments +from sc_evm_forger import print_current_epoch_and_slot +from test_framework.util import ( + assert_equal, assert_true, assert_false, fail, forward_transfer_to_sidechain, bytes_to_hex_str, hex_str_to_bytes, ) + +NULL_ADDRESS = '0x0000000000000000000000000000000000000000' + +""" +Configuration: + - 2 SC nodes connected with each other + - 1 MC node + - SC node 1 owns a stakeAmount made out of cross chain creation output + +Test: + - Activate Fork 1.3 and execute upgrade, in order to use storage model v2 + - Create some stakes with different owners for node 1 forger using the old native smart contract + - Check that activate cannot be called on ForgerStake smart contract V2 before fork 1.4 + - Reach fork point 1.4. + - Try to execute disable on old Forger stake native contract and verify that it is not possible. + - Try executing methods of ForgerStake smart contract V2 before calling activate and verify that it is not possible. + - Execute activate on ForgerStake smart contract V2 and verify that the total amount of stakes are the same as before + - Try methods of the old Forger stake native contract and verify they cannot be executed anymore + - Try delegate and withdraw and verify they work as expected + - Try executing methods of ForgerStake smart contract V2 from a smart contract and verify they work as expected + + + +""" + + +class SCEvmNativeForgerV2(AccountChainSetup): + def __init__(self): + super().__init__(number_of_sidechain_nodes=2, forward_amount=100, withdrawalEpochLength=20, + block_timestamp_rewind=1500 * EVM_APP_SLOT_TIME * VERSION_1_4_FORK_EPOCH) + + def run_test(self): + if self.options.all_forks: + logging.info("This test cannot be executed with --allforks") + exit() + + mc_node = self.nodes[0] + sc_node_1 = self.sc_nodes[0] + sc_node_2 = self.sc_nodes[1] + + # transfer a small fund from MC to SC2 at a new evm address, do not mine mc block + # this is for enabling SC 2 gas fee payment when sending txes + evm_address_sc_node_2 = sc_node_2.wallet_createPrivateKeySecp256k1()["result"]["proposition"]["address"] + evm_address_sc_node_3 = sc_node_2.wallet_createPrivateKeySecp256k1()["result"]["proposition"]["address"] + + ft_amount_in_zen_2 = Decimal('500.0') + + forward_transfer_to_sidechain(self.sc_nodes_bootstrap_info.sidechain_id, + mc_node, + evm_address_sc_node_2, + ft_amount_in_zen_2, + mc_return_address=mc_node.getnewaddress(), + generate_block=False) + + time.sleep(2) # MC needs this + + # transfer some fund from MC to SC1 at a new evm address, then mine mc block + evm_address_sc_node_1 = sc_node_1.wallet_createPrivateKeySecp256k1()["result"]["proposition"]["address"] + + ft_amount_in_zen = Decimal('1000.0') + + forward_transfer_to_sidechain(self.sc_nodes_bootstrap_info.sidechain_id, + mc_node, + evm_address_sc_node_1, + ft_amount_in_zen, + mc_return_address=mc_node.getnewaddress(), + generate_block=True) + self.sync_all() + + # Generate SC block and check that FTs appears in SCs node wallet + generate_next_block(sc_node_1, "first node") + self.sc_sync_all() + print_current_epoch_and_slot(sc_node_1) + + # Get node 1 forger keys + forger_stake_list = sc_node_1.transaction_allForgingStakes()["result"]['stakes'] + block_sign_pub_key_1 = forger_stake_list[0]['forgerStakeData']["forgerPublicKeys"]["blockSignPublicKey"][ + "publicKey"] + vrf_pub_key_1 = forger_stake_list[0]['forgerStakeData']["forgerPublicKeys"]["vrfPublicKey"]["publicKey"] + + # Create forger keys on node 2 + block_sign_pub_key_2 = sc_node_2.wallet_createPrivateKey25519()["result"]["proposition"]["publicKey"] + vrf_pub_key_2 = sc_node_2.wallet_createVrfSecret()["result"]["proposition"]["publicKey"] + + # Create some additional addresses, don't care the node + evm_address_3 = sc_node_1.wallet_createPrivateKeySecp256k1()["result"]["proposition"]["address"] + evm_address_4 = sc_node_2.wallet_createPrivateKeySecp256k1()["result"]["proposition"]["address"] + evm_address_5 = sc_node_1.wallet_createPrivateKeySecp256k1()["result"]["proposition"]["address"] + + """Create some stakes for node 1 forger: + - 1 with evm_address_sc_node_1 as owner + - 3 with evm_address_sc_node_2 as owner + - 2 with evm_address_3 as owner + - 1 with evm_address_4 as owner + - 1 with evm_address_5 as owner + """ + ac_makeForgerStake(sc_node_1, evm_address_sc_node_1, block_sign_pub_key_1, + vrf_pub_key_1, convertZenToZennies(2), 0) + ac_makeForgerStake(sc_node_1, evm_address_sc_node_2, block_sign_pub_key_1, + vrf_pub_key_1, convertZenToZennies(1), 1) + ac_makeForgerStake(sc_node_1, evm_address_sc_node_2, block_sign_pub_key_1, + vrf_pub_key_1, convertZenToZennies(11), 2) + ac_makeForgerStake(sc_node_1, evm_address_3, block_sign_pub_key_1, + vrf_pub_key_1, convertZenToZennies(2), 3) + ac_makeForgerStake(sc_node_1, evm_address_sc_node_2, block_sign_pub_key_1, + vrf_pub_key_1, convertZenToZennies(1), 4) + ac_makeForgerStake(sc_node_1, evm_address_4, block_sign_pub_key_1, + vrf_pub_key_1, convertZenToZennies(3), 5) + ac_makeForgerStake(sc_node_1, evm_address_3, block_sign_pub_key_1, + vrf_pub_key_1, convertZenToZennies(3), 6) + ac_makeForgerStake(sc_node_1, evm_address_5, block_sign_pub_key_1, + vrf_pub_key_1, convertZenToZennies(1), 7) + self.sc_sync_all() + + # Generate SC block on SC node (keep epoch) + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=False) + self.sc_sync_all() + + orig_stake_list = sc_node_1.transaction_allForgingStakes()["result"]['stakes'] + assert_equal(9, len(orig_stake_list)) + + exp_stake_own_1 = 0 + exp_stake_own_2 = 0 + exp_stake_own_3 = 0 + exp_stake_own_4 = 0 + exp_stake_own_5 = 0 + genesis_stake = 0 + for stake in orig_stake_list: + if stake['forgerStakeData']['ownerPublicKey']['address'] == evm_address_sc_node_1: + exp_stake_own_1 += stake['forgerStakeData']['stakedAmount'] + elif stake['forgerStakeData']['ownerPublicKey']['address'] == evm_address_sc_node_2: + exp_stake_own_2 += stake['forgerStakeData']['stakedAmount'] + elif stake['forgerStakeData']['ownerPublicKey']['address'] == evm_address_3: + exp_stake_own_3 += stake['forgerStakeData']['stakedAmount'] + elif stake['forgerStakeData']['ownerPublicKey']['address'] == evm_address_4: + exp_stake_own_4 += stake['forgerStakeData']['stakedAmount'] + elif stake['forgerStakeData']['ownerPublicKey']['address'] == evm_address_5: + exp_stake_own_5 += stake['forgerStakeData']['stakedAmount'] + else: + genesis_stake += stake['forgerStakeData']['stakedAmount'] + + # Reach fork point 1.3 + current_best_epoch = sc_node_1.block_forgingInfo()["result"]["bestBlockEpochNumber"] + for i in range(0, VERSION_1_3_FORK_EPOCH - current_best_epoch): + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=True) + self.sc_sync_all() + + old_forger_native_contract = SmartContract("ForgerStakes") + method = 'upgrade()' + # Execute upgrade + contract_function_call(sc_node_1, old_forger_native_contract, FORGER_STAKE_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_1, method) + + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=True) + self.sc_sync_all() + + forger_stake_balance = rpc_get_balance(sc_node_1, FORGER_STAKE_SMART_CONTRACT_ADDRESS) + + # Check that disable on old smart contract cannot be called before fork 1.4 + method = 'disableAndMigrate()' + try: + contract_function_static_call(sc_node_1, old_forger_native_contract, FORGER_STAKE_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_1, method) + fail("disableAndMigrate call should fail before fork point") + except RuntimeError as err: + logging.info("Expected exception thrown: {}".format(err)) + assert_true("op code not supported" in str(err)) + + # Check that if activate is called before fork 1.4 it doesn't fail, but it is not executed. It is interpreted + # as an EOA-to-EOA with a data not null. + + forger_v2_native_contract = SmartContract("ForgerStakesV2") + method = 'activate()' + tx_hash = contract_function_call(sc_node_1, forger_v2_native_contract, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_1, method, value=convertZenToZennies(2)) + + self.sc_sync_all() + tx_receipt = generate_block_and_get_tx_receipt(sc_node_1, tx_hash)['result'] + assert_equal('0x1', tx_receipt['status'], 'Transaction failed') + + # Reach fork point 1.4 + current_best_epoch = sc_node_1.block_forgingInfo()["result"]["bestBlockEpochNumber"] + for i in range(0, VERSION_1_4_FORK_EPOCH - current_best_epoch): + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=True) + self.sc_sync_all() + + # Check that disable on old smart contract cannot be called from an account that is not + # FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS + method = 'disableAndMigrate()' + try: + contract_function_static_call(sc_node_1, old_forger_native_contract, FORGER_STAKE_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_1, method) + fail("disableAndMigrate call should fail") + except RuntimeError as err: + logging.info("Expected exception thrown: {}".format(err)) + assert_true("Authorization failed" in str(err)) + + # Check that delegate cannot be called before activate + delegate_method = 'delegate(bytes32,bytes32,bytes1)' + forger_1_vrf_pub_key_to_bytes = hex_str_to_bytes(vrf_pub_key_1) + forger_1_sign_key_to_bytes = hex_str_to_bytes(block_sign_pub_key_1) + + try: + contract_function_static_call(sc_node_1, forger_v2_native_contract, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_1, delegate_method, + forger_1_sign_key_to_bytes, forger_1_vrf_pub_key_to_bytes[0:32], + forger_1_vrf_pub_key_to_bytes[32:]) + fail("delegate call should fail") + except RuntimeError as err: + logging.info("Expected exception thrown: {}".format(err)) + assert_true("Forger stake V2 has not been activated yet" in str(err)) + + # Check that withdraw cannot be called before activate + method = 'withdraw(bytes32,bytes32,bytes1,uint256)' + forger_1_vrf_pub_key_to_bytes = hex_str_to_bytes(vrf_pub_key_1) + + try: + contract_function_static_call(sc_node_1, forger_v2_native_contract, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_1, method, forger_1_sign_key_to_bytes, + forger_1_vrf_pub_key_to_bytes[0:32], forger_1_vrf_pub_key_to_bytes[32:], + convertZenToWei(1)) + fail("withdraw call should fail") + except RuntimeError as err: + logging.info("Expected exception thrown: {}".format(err)) + assert_true("Forger stake V2 has not been activated yet" in str(err)) + + # Test that getForger and getPagedForgers fail before activate. + method = 'getForger(bytes32,bytes32,bytes1)' + forger_1_sign_key_to_bytes = hex_str_to_bytes(block_sign_pub_key_1) + forger_1_vrf_pub_key_to_bytes = hex_str_to_bytes(vrf_pub_key_1) + forger_1_keys_to_bytes = (forger_1_sign_key_to_bytes, forger_1_vrf_pub_key_to_bytes[:32], + forger_1_vrf_pub_key_to_bytes[32:]) + try: + contract_function_static_call(sc_node_1, forger_v2_native_contract, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_1, method, *forger_1_keys_to_bytes) + fail("getForger call should fail") + except RuntimeError as err: + logging.info("Expected exception thrown: {}".format(err)) + assert_true("Forger stake V2 has not been activated yet" in str(err)) + + method = 'getPagedForgers(int32,int32)' + get_paged_forgers_args = (0, 100) + try: + contract_function_static_call(sc_node_1, forger_v2_native_contract, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_1, method, *get_paged_forgers_args) + fail("getForger call should fail") + except RuntimeError as err: + logging.info("Expected exception thrown: {}".format(err)) + assert_true("Forger stake V2 has not been activated yet" in str(err)) + + # Test that getCurrentConsensusEpoch fails before activate. + + method = 'getCurrentConsensusEpoch()' + try: + contract_function_static_call(sc_node_1, forger_v2_native_contract, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_1, method) + fail("getCurrentConsensusEpoch call should fail") + except RuntimeError as err: + print("Expected exception thrown: {}".format(err)) + assert_true("Forger stake V2 has not been activated yet" in str(err)) + + # Check that stakeStart cannot be called before activate + method = 'stakeStart(bytes32,bytes32,bytes1,address)' + forger_1_vrf_pub_key_to_bytes = hex_str_to_bytes(vrf_pub_key_1) + + try: + contract_function_static_call(sc_node_1, forger_v2_native_contract, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_1, method, forger_1_sign_key_to_bytes, + forger_1_vrf_pub_key_to_bytes[0:32], forger_1_vrf_pub_key_to_bytes[32:], + "0x" + evm_address_sc_node_1) + fail("stakeStart call should fail") + except RuntimeError as err: + logging.info("Expected exception thrown: {}".format(err)) + assert_true("Forger stake V2 has not been activated yet" in str(err)) + + # Check that after fork 1.4 but before activate, it is still possible to call makeForgerStake and + # spendForgingStake + + make_forger_stake_json_res = ac_makeForgerStake(sc_node_1, evm_address_5, block_sign_pub_key_1, + vrf_pub_key_1, convertZenToZennies(4)) + + if "result" not in make_forger_stake_json_res: + fail("make forger stake with native smart contract v1 should work before activate") + else: + logging.info("Transaction created as expected") + generate_next_block(sc_node_1, "first node") + self.sc_sync_all() + + stake_list = sc_node_1.transaction_allForgingStakes()["result"]['stakes'] + assert_equal(len(orig_stake_list) + 1, len(stake_list)) + + stake_id = stake_list[-1]['stakeId'] + spend_forger_stake_json_res = sc_node_1.transaction_spendForgingStake( + json.dumps({"stakeId": stake_id})) + if "result" not in spend_forger_stake_json_res: + fail("spend forger stake failed: " + json.dumps(spend_forger_stake_json_res)) + else: + logging.info("Forger stake removed: " + json.dumps(spend_forger_stake_json_res)) + self.sc_sync_all() + + generate_next_block(sc_node_1, "first node") + self.sc_sync_all() + + # Execute activate. + forger_stake_v2_balance = rpc_get_balance(sc_node_1, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS) + method = 'activate()' + tx_hash = contract_function_call(sc_node_1, forger_v2_native_contract, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_1, method) + + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=False) + self.sc_sync_all() + + # Check the receipt and the event log + tx_receipt = generate_block_and_get_tx_receipt(sc_node_1, tx_hash)['result'] + assert_equal('0x1', tx_receipt['status'], 'Transaction failed') + intrinsic_gas = 21000 + 4 * 16 # activate signature are 4 non-zero bytes + assert_equal(intrinsic_gas, int(tx_receipt['gasUsed'], 16), "wrong used gas") + assert_equal(2, len(tx_receipt['logs']), 'Wrong number of logs') + + disable_event = tx_receipt['logs'][0] + assert_equal(1, len(disable_event['topics']), "Wrong number of topics in disable_event") + event_id = remove_0x_prefix(disable_event['topics'][0]) + event_signature = remove_0x_prefix( + encode_hex(event_signature_to_log_topic('DisableStakeV1()'))) + assert_equal(event_signature, event_id, "Wrong event signature in topics") + + activate_event = tx_receipt['logs'][1] + assert_equal(1, len(activate_event['topics']), "Wrong number of topics in activate_event") + event_id = remove_0x_prefix(activate_event['topics'][0]) + event_signature = remove_0x_prefix( + encode_hex(event_signature_to_log_topic('ActivateStakeV2()'))) + assert_equal(event_signature, event_id, "Wrong event signature in topics") + + # retrieve the stakes from the Forger Stake V2 + stake_list_node1 = sc_node_1.transaction_allForgingStakes()["result"]['stakes'] + stake_list_node2 = sc_node_2.transaction_allForgingStakes()["result"]['stakes'] + assert_equal(stake_list_node1, stake_list_node2, "Forging stakes are different on 2 nodes") + + for stake in stake_list_node1: + if stake['forgerStakeData']['ownerPublicKey']['address'] == evm_address_sc_node_1: + assert_equal(exp_stake_own_1, stake['forgerStakeData']['stakedAmount'], + "Forger stake is different after upgrade") + elif stake['forgerStakeData']['ownerPublicKey']['address'] == evm_address_sc_node_2: + assert_equal(exp_stake_own_2, stake['forgerStakeData']['stakedAmount'], + "Forger stake is different after upgrade") + elif stake['forgerStakeData']['ownerPublicKey']['address'] == evm_address_3: + assert_equal(exp_stake_own_3, stake['forgerStakeData']['stakedAmount'], + "Forger stake is different after upgrade") + elif stake['forgerStakeData']['ownerPublicKey']['address'] == evm_address_4: + assert_equal(exp_stake_own_4, stake['forgerStakeData']['stakedAmount'], + "Forger stake is different after upgrade") + elif stake['forgerStakeData']['ownerPublicKey']['address'] == evm_address_5: + assert_equal(exp_stake_own_5, stake['forgerStakeData']['stakedAmount'], + "Forger stake is different after upgrade") + else: + assert_equal(genesis_stake, stake['forgerStakeData']['stakedAmount'], + "Forger stake is different after upgrade") + assert_false('stakeId' in stake) + + # Test myForgingStakes(). On node 1 should return stakes belonging to evm_address_sc_node_1, evm_address_3, evm_address_5 and genesis. + # On node 2 evm_address_sc_node_2 and evm_address_4. + stake_list_node1 = sc_node_1.transaction_myForgingStakes()["result"]['stakes'] + for stake in stake_list_node1: + if stake['forgerStakeData']['ownerPublicKey']['address'] == evm_address_sc_node_1: + assert_equal(exp_stake_own_1, stake['forgerStakeData']['stakedAmount'], "Forger stake is different after upgrade") + elif stake['forgerStakeData']['ownerPublicKey']['address'] == evm_address_3: + assert_equal(exp_stake_own_3, stake['forgerStakeData']['stakedAmount'], "Forger stake is different after upgrade") + elif stake['forgerStakeData']['ownerPublicKey']['address'] == evm_address_5: + assert_equal(exp_stake_own_5, stake['forgerStakeData']['stakedAmount'], "Forger stake is different after upgrade") + elif stake['forgerStakeData']['ownerPublicKey']['address'] == evm_address_sc_node_2: + fail("returned stakes not belonging to an address of the node") + elif stake['forgerStakeData']['ownerPublicKey']['address'] == evm_address_4: + fail("returned stakes not belonging to an address of the node") + else: + assert_equal(genesis_stake, stake['forgerStakeData']['stakedAmount'], "Forger stake is different after upgrade") + assert_false('stakeId' in stake) + + stake_list_node2 = sc_node_2.transaction_myForgingStakes()["result"]['stakes'] + for stake in stake_list_node2: + if stake['forgerStakeData']['ownerPublicKey']['address'] == evm_address_sc_node_1: + fail("returned stakes not belonging to an address of the node") + elif stake['forgerStakeData']['ownerPublicKey']['address'] == evm_address_3: + fail("returned stakes not belonging to an address of the node") + elif stake['forgerStakeData']['ownerPublicKey']['address'] == evm_address_5: + fail("returned stakes not belonging to an address of the node") + elif stake['forgerStakeData']['ownerPublicKey']['address'] == evm_address_sc_node_2: + assert_equal(exp_stake_own_2, stake['forgerStakeData']['stakedAmount'], "Forger stake is different after upgrade") + elif stake['forgerStakeData']['ownerPublicKey']['address'] == evm_address_4: + assert_equal(exp_stake_own_4, stake['forgerStakeData']['stakedAmount'], "Forger stake is different after upgrade") + else: + fail("returned stakes not belonging to an address of the node") + assert_false('stakeId' in stake) + + # Check the balance of the 2 smart contracts + assert_equal(0, rpc_get_balance(sc_node_1, FORGER_STAKE_SMART_CONTRACT_ADDRESS)) + assert_equal(forger_stake_balance + forger_stake_v2_balance, + rpc_get_balance(sc_node_1, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS)) + forger_stake_v2_balance += forger_stake_balance + + # Check that activate cannot be called twice + + try: + contract_function_call(sc_node_1, forger_v2_native_contract, + FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_1, method) + fail("activate call should fail") + except RuntimeError as err: + pass + + # Check that old native smart contract is disabled + method = "getAllForgersStakes()" + try: + old_forger_native_contract.static_call(sc_node_1, method, fromAddress=evm_address_sc_node_1, + toAddress=FORGER_STAKE_SMART_CONTRACT_ADDRESS) + fail("call should fail after activate of Forger Stake v2") + except RuntimeError as err: + logging.info("Expected exception thrown: {}".format(err)) + # error is raised from API since the address has no balance + assert_true("Method is disabled" in str(err)) + + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=False) + + # Check that after activate, calling makeForgerStake, spendForgingStake and pagedForgingStakes HTTP APIs is + # not possible anymore + make_forger_stake_json_res = ac_makeForgerStake(sc_node_1, evm_address_5, block_sign_pub_key_1, + vrf_pub_key_1, convertZenToZennies(4)) + + if "error" not in make_forger_stake_json_res: + fail("make forger stake with native smart contract v1 should fail after activate") + + assert_equal("Method is disabled after Fork 1.4. Use Forger Stakes Native Smart Contract V2", + make_forger_stake_json_res['error']['description']) + generate_next_block(sc_node_1, "first node") + self.sc_sync_all() + + spend_forger_stake_json_res = sc_node_1.transaction_spendForgingStake( + json.dumps({"stakeId": stake_id})) + if "error" not in spend_forger_stake_json_res: + fail("spendForgingStake with native smart contract v1 should fail after activate") + assert_equal("Method is disabled after Fork 1.4. Use Forger Stakes Native Smart Contract V2", + spend_forger_stake_json_res['error']['description']) + + paged_stakes_res = sc_node_1.transaction_pagedForgingStakes(json.dumps({"size": 10, "startPos": 0})) + if "error" not in paged_stakes_res: + fail("pagedForgingStakes native smart contract v1 should fail after activate") + assert_equal("Method is disabled after Fork 1.4. Use Forger Stakes Native Smart Contract V2", + spend_forger_stake_json_res['error']['description']) + + + # Check getPagedForgers + method = 'getPagedForgers(int32,int32)' + data = forger_v2_native_contract.raw_encode_call(method, *get_paged_forgers_args) + + result = sc_node_1.rpc_eth_call( + { + "to": format_evm(FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS), + "from": format_evm(evm_address_sc_node_1), + "input": data + }, "latest" + ) + + (next_pos, list_of_forgers) = decode_paged_list_of_forgers(hex_str_to_bytes(result['result'][2:])) + assert_equal(-1, next_pos) + assert_equal(1, len(list_of_forgers)) + assert_equal(block_sign_pub_key_1, list_of_forgers[0][0]) + assert_equal(vrf_pub_key_1, list_of_forgers[0][1]) + assert_equal(0, list_of_forgers[0][2]) + assert_equal(NULL_ADDRESS, list_of_forgers[0][3]) + + # Check getForger + method = 'getForger(bytes32,bytes32,bytes1)' + data = forger_v2_native_contract.raw_encode_call(method, *forger_1_keys_to_bytes) + + result = sc_node_1.rpc_eth_call( + { + "to": format_evm(FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS), + "from": format_evm(evm_address_sc_node_1), + "input": data + }, "latest" + ) + + forger_info = decode_forger_info(hex_str_to_bytes(result['result'][2:])) + assert_equal(block_sign_pub_key_1, forger_info[0]) + assert_equal(vrf_pub_key_1, forger_info[1]) + assert_equal(0, forger_info[2]) + assert_equal(NULL_ADDRESS, forger_info[3]) + + # Try getForger on a non-registered forger + forger_2_sign_key_to_bytes = hex_str_to_bytes(block_sign_pub_key_2) + forger_2_vrf_pub_key_to_bytes = hex_str_to_bytes(vrf_pub_key_2) + forger_2_keys_to_bytes = (forger_2_sign_key_to_bytes, forger_2_vrf_pub_key_to_bytes[:32], + forger_2_vrf_pub_key_to_bytes[32:]) + + try: + contract_function_static_call(sc_node_1, forger_v2_native_contract, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_1, method, *forger_2_keys_to_bytes) + fail("getForger call should fail if forger is not registered yet") + except RuntimeError as err: + logging.info("Expected exception thrown: {}".format(err)) + assert_true("Forger doesn't exist." in str(err)) + + ################################ + # Delegate + ################################ + evm_address_sc_node_1_balance = rpc_get_balance(sc_node_1, evm_address_sc_node_1) + + staked_amount = convertZenToWei(1) + + delegate_method = 'delegate(bytes32,bytes32,bytes1)' + + tx_hash = contract_function_call(sc_node_1, forger_v2_native_contract, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_1, delegate_method, forger_1_sign_key_to_bytes, + forger_1_vrf_pub_key_to_bytes[0:32], forger_1_vrf_pub_key_to_bytes[32:], + value=staked_amount) + + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=False) + self.sc_sync_all() + + # Check the receipt and the event log + tx_receipt = generate_block_and_get_tx_receipt(sc_node_1, tx_hash)['result'] + assert_equal('0x1', tx_receipt['status'], 'Transaction failed') + assert_equal(41403, int(tx_receipt['gasUsed'], 16), "wrong used gas") + assert_equal(1, len(tx_receipt['logs']), 'Wrong number of logs') + delegate_event = tx_receipt['logs'][0] + check_delegate_event(delegate_event, evm_address_sc_node_1, forger_1_vrf_pub_key_to_bytes, block_sign_pub_key_1, + staked_amount) + + # Check the balance after delegate + gas_fee_paid, _, _ = computeForgedTxFee(sc_node_1, tx_hash) + assert_equal(evm_address_sc_node_1_balance - staked_amount - gas_fee_paid, + rpc_get_balance(sc_node_1, evm_address_sc_node_1)) + assert_equal(staked_amount + forger_stake_v2_balance, + rpc_get_balance(sc_node_1, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS)) + forger_stake_v2_balance += staked_amount + + # Check stakes by forger + method_paged_stakes_by_forger = "getPagedForgersStakesByForger(bytes32,bytes32,bytes1,int32,int32)" + + paged_stakes_by_forger_1_data_input = (forger_v2_native_contract. + raw_encode_call(method_paged_stakes_by_forger, + forger_1_sign_key_to_bytes, + forger_1_vrf_pub_key_to_bytes[0:32], + forger_1_vrf_pub_key_to_bytes[32:], 0, 100)) + result = sc_node_1.rpc_eth_call( + { + "to": "0x" + FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + "from": add_0x_prefix(evm_address_sc_node_1), + "input": paged_stakes_by_forger_1_data_input + }, "latest" + ) + + (next_pos, list_of_stakes) = decode_paged_list_of_forger_stakes(hex_str_to_bytes(result['result'][2:])) + assert_equal(-1, next_pos) + assert_equal(6, len(list_of_stakes)) + + exp_stake_own_1 += staked_amount + assert_equal(exp_stake_own_1, list_of_stakes["0x" + evm_address_sc_node_1]) + assert_equal(exp_stake_own_2, list_of_stakes["0x" + evm_address_sc_node_2]) + assert_equal(exp_stake_own_3, list_of_stakes["0x" + evm_address_3]) + assert_equal(exp_stake_own_4, list_of_stakes["0x" + evm_address_4]) + assert_equal(exp_stake_own_5, list_of_stakes["0x" + evm_address_5]) + + # Check stakes by delegator + method_paged_stakes_by_delegator = "getPagedForgersStakesByDelegator(address,int32,int32)" + + data_input = forger_v2_native_contract.raw_encode_call(method_paged_stakes_by_delegator, + "0x" + evm_address_sc_node_1, + 0, 100) + result = sc_node_1.rpc_eth_call( + { + "to": "0x" + FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + "from": add_0x_prefix(evm_address_sc_node_1), + "input": data_input + }, "latest" + ) + + (next_pos, list_of_stakes) = decode_paged_list_of_delegator_stakes(hex_str_to_bytes(result['result'][2:])) + assert_equal(-1, next_pos) + assert_equal(1, len(list_of_stakes)) + assert_equal(block_sign_pub_key_1, list_of_stakes[0][0]) + assert_equal(vrf_pub_key_1, list_of_stakes[0][1]) + assert_equal(exp_stake_own_1, list_of_stakes[0][2]) + + # Try delegate to a non-registered forger + + try: + contract_function_static_call(sc_node_1, forger_v2_native_contract, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_1, delegate_method, + hex_str_to_bytes(block_sign_pub_key_2), + forger_1_vrf_pub_key_to_bytes[0:32], forger_1_vrf_pub_key_to_bytes[32:], + value=convertZenToWei(1)) + fail("delegate call should fail") + except RuntimeError as err: + logging.info("Expected exception thrown: {}".format(err)) + assert_true("Forger doesn't exist" in str(err)) + + # Check stakeStart + method_stake_start = "stakeStart(bytes32,bytes32,bytes1,address)" + data_input = forger_v2_native_contract.raw_encode_call(method_stake_start, + forger_1_sign_key_to_bytes, + forger_1_vrf_pub_key_to_bytes[0:32], + forger_1_vrf_pub_key_to_bytes[32:], + "0x" + evm_address_sc_node_1) + result = sc_node_1.rpc_eth_call( + { + "to": "0x" + FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + "from": add_0x_prefix(evm_address_sc_node_1), + "input": data_input + }, "latest" + ) + assert_equal(VERSION_1_4_FORK_EPOCH, decode(['int32'], hex_str_to_bytes(result['result'][2:]))[0]) + # Check stakeStart value for address that did not delegated anything - should return -1 + data_input = forger_v2_native_contract.raw_encode_call(method_stake_start, + forger_1_sign_key_to_bytes, + forger_1_vrf_pub_key_to_bytes[0:32], + forger_1_vrf_pub_key_to_bytes[32:], + "0x" + evm_address_sc_node_3) + result = sc_node_1.rpc_eth_call( + { + "to": "0x" + FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + "from": add_0x_prefix(evm_address_sc_node_1), + "input": data_input + }, "latest" + ) + assert_equal(-1, decode(['int32'], hex_str_to_bytes(result['result'][2:]))[0]) + + ################################ + # Withdrawal + ################################ + evm_address_sc_node_2_balance = rpc_get_balance(sc_node_1, evm_address_sc_node_2) + + staked_amount_withdrawn = exp_stake_own_2 + withdraw_method = "withdraw(bytes32,bytes32,bytes1,uint256)" + + tx_hash = contract_function_call(sc_node_2, forger_v2_native_contract, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_2, withdraw_method, forger_1_sign_key_to_bytes, + forger_1_vrf_pub_key_to_bytes[0:32], forger_1_vrf_pub_key_to_bytes[32:], + staked_amount_withdrawn) + + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=False) + self.sc_sync_all() + + # Check the receipt and the event log + tx_receipt = generate_block_and_get_tx_receipt(sc_node_1, tx_hash)['result'] + assert_equal('0x1', tx_receipt['status'], 'Transaction failed') + assert_equal(41503, int(tx_receipt['gasUsed'], 16), "wrong used gas") + assert_equal(1, len(tx_receipt['logs']), 'Wrong number of logs') + withdraw_event = tx_receipt['logs'][0] + check_withdraw_event(withdraw_event, evm_address_sc_node_2, forger_1_vrf_pub_key_to_bytes, block_sign_pub_key_1, + staked_amount_withdrawn) + + # Check the balance after withdrawal + gas_fee_paid, _, _ = computeForgedTxFee(sc_node_1, tx_hash) + assert_equal(evm_address_sc_node_2_balance + staked_amount_withdrawn - gas_fee_paid, + rpc_get_balance(sc_node_1, evm_address_sc_node_2)) + assert_equal(forger_stake_v2_balance - staked_amount_withdrawn, + rpc_get_balance(sc_node_1, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS)) + forger_stake_v2_balance -= staked_amount_withdrawn + + # Check stakes by forger + result = sc_node_1.rpc_eth_call( + { + "to": "0x" + FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + "from": add_0x_prefix(evm_address_sc_node_1), + "input": paged_stakes_by_forger_1_data_input + }, "latest" + ) + + (next_pos, list_of_stakes) = decode_paged_list_of_forger_stakes(hex_str_to_bytes(result['result'][2:])) + assert_equal(-1, next_pos) + assert_equal(5, len(list_of_stakes)) + + assert_equal(exp_stake_own_1, list_of_stakes["0x" + evm_address_sc_node_1]) + assert_equal(exp_stake_own_3, list_of_stakes["0x" + evm_address_3]) + assert_equal(exp_stake_own_4, list_of_stakes["0x" + evm_address_4]) + assert_equal(exp_stake_own_5, list_of_stakes["0x" + evm_address_5]) + + assert_false(("0x" + evm_address_sc_node_2) in list_of_stakes) + + # Check stakes by delegator + method_paged_stakes_by_delegator = "getPagedForgersStakesByDelegator(address,int32,int32)" + + data_input = forger_v2_native_contract.raw_encode_call(method_paged_stakes_by_delegator, + "0x" + evm_address_sc_node_2, 0, 100) + result = sc_node_1.rpc_eth_call( + { + "to": "0x" + FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + "from": add_0x_prefix(evm_address_sc_node_1), + "input": data_input + }, "latest" + ) + + (next_pos, list_of_stakes) = decode_paged_list_of_delegator_stakes(hex_str_to_bytes(result['result'][2:])) + assert_equal(-1, next_pos) + assert_equal(0, len(list_of_stakes)) + + # Try withdrawal without enough funds + + try: + contract_function_static_call(sc_node_1, forger_v2_native_contract, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_2, withdraw_method, + forger_1_sign_key_to_bytes, + forger_1_vrf_pub_key_to_bytes[0:32], forger_1_vrf_pub_key_to_bytes[32:], + staked_amount_withdrawn) + fail("withdrawal call should fail") + except RuntimeError as err: + logging.info("Expected exception thrown: {}".format(err)) + assert_true("Not enough stake" in str(err)) + + # Try getCurrentConsensusEpoch + method = "getCurrentConsensusEpoch()" + epoch = \ + contract_function_static_call(sc_node_1, forger_v2_native_contract, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + evm_address_sc_node_2, method)[0] + + current_best_epoch = sc_node_1.block_forgingInfo()["result"]["bestBlockEpochNumber"] + assert_equal(current_best_epoch, epoch) + + # Get the block hash of the tip at the current epoch + block_id = sc_node_1.block_best()["result"]["block"]["id"] + + # Switch to a new epoch + generate_next_block(sc_node_1, "first node", force_switch_to_next_epoch=True) + current_best_epoch = sc_node_1.block_forgingInfo()["result"]["bestBlockEpochNumber"] + assert_false(epoch == current_best_epoch) + rpc_tag = { + "blockHash": "0x" + block_id + } + data_input = forger_v2_native_contract.raw_encode_call(method) + result = sc_node_1.rpc_eth_call( + { + "to": "0x" + FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + "from": add_0x_prefix(evm_address_sc_node_1), + "input": data_input + }, rpc_tag + ) + epoch_at_block = decode(['uint32'], hex_str_to_bytes(result['result'][2:]))[0] + assert_equal(epoch, epoch_at_block) + + ####################################################################################################### + # Interoperability test with an EVM smart contract calling forger stakes V2 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, self.options.all_forks) + + # 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") + + # Call delegate using the proxy + evm_address_interop_balance = rpc_get_balance(sc_node_1, evm_address_interop) + + proxy_contract_balance = rpc_get_balance(sc_node_1, proxy_contract.contract_address) + + native_input = format_eoa( + forger_v2_native_contract.raw_encode_call(delegate_method, forger_1_sign_key_to_bytes, + forger_1_vrf_pub_key_to_bytes[0:32], + forger_1_vrf_pub_key_to_bytes[32:])) + + tx_hash = proxy_contract.call_transaction(evm_address_interop, 2, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + staked_amount, native_input) + tx_receipt = generate_block_and_get_tx_receipt(sc_node_1, tx_hash)['result'] + assert_equal('0x1', tx_receipt['status'], 'Transaction failed') + assert_equal(183841, int(tx_receipt['gasUsed'], 16), "wrong used gas") + assert_equal(1, len(tx_receipt['logs']), 'Wrong number of logs') + delegate_event = tx_receipt['logs'][0] + check_delegate_event(delegate_event, format_eoa(proxy_contract.contract_address), forger_1_vrf_pub_key_to_bytes, + block_sign_pub_key_1, staked_amount) + + # Check the balance after delegate + gas_fee_paid, _, _ = computeForgedTxFee(sc_node_1, tx_hash) + assert_equal(evm_address_interop_balance - gas_fee_paid, rpc_get_balance(sc_node_1, evm_address_interop)) + evm_address_interop_balance -= gas_fee_paid + + assert_equal(proxy_contract_balance - staked_amount, + rpc_get_balance(sc_node_1, proxy_contract.contract_address)) + proxy_contract_balance -= staked_amount + + assert_equal(staked_amount + forger_stake_v2_balance, + rpc_get_balance(sc_node_1, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS)) + forger_stake_v2_balance += staked_amount + + # Check stakes by forger + result = sc_node_1.rpc_eth_call( + { + "to": "0x" + FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + "from": add_0x_prefix(evm_address_sc_node_1), + "input": paged_stakes_by_forger_1_data_input + }, "latest" + ) + + (next_pos, list_of_stakes) = decode_paged_list_of_forger_stakes(hex_str_to_bytes(result['result'][2:])) + assert_equal(-1, next_pos) + assert_equal(6, len(list_of_stakes)) + + assert_equal(exp_stake_own_1, list_of_stakes["0x" + evm_address_sc_node_1]) + assert_false(("0x" + evm_address_sc_node_2) in list_of_stakes) + assert_equal(exp_stake_own_3, list_of_stakes["0x" + evm_address_3]) + assert_equal(exp_stake_own_4, list_of_stakes["0x" + evm_address_4]) + assert_equal(exp_stake_own_5, list_of_stakes["0x" + evm_address_5]) + assert_equal(staked_amount, list_of_stakes[proxy_contract.contract_address.lower()]) + + # Check stakes by delegator + data_input = forger_v2_native_contract.raw_encode_call(method_paged_stakes_by_delegator, + proxy_contract.contract_address, + 0, 100) + result = sc_node_1.rpc_eth_call( + { + "to": "0x" + FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + "from": add_0x_prefix(evm_address_sc_node_1), + "input": data_input + }, "latest" + ) + + (next_pos, list_of_stakes) = decode_paged_list_of_delegator_stakes(hex_str_to_bytes(result['result'][2:])) + assert_equal(-1, next_pos) + assert_equal(1, len(list_of_stakes)) + assert_equal(block_sign_pub_key_1, list_of_stakes[0][0]) + assert_equal(vrf_pub_key_1, list_of_stakes[0][1]) + assert_equal(staked_amount, list_of_stakes[0][2]) + + # Call withdraw using the proxy + + native_input = format_eoa( + forger_v2_native_contract.raw_encode_call(withdraw_method, forger_1_sign_key_to_bytes, + forger_1_vrf_pub_key_to_bytes[0:32], + forger_1_vrf_pub_key_to_bytes[32:], staked_amount)) + + tx_hash = proxy_contract.call_transaction(evm_address_interop, 3, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + 0, native_input) + tx_receipt = generate_block_and_get_tx_receipt(sc_node_1, tx_hash)['result'] + + assert_equal('0x1', tx_receipt['status'], 'Transaction failed') + assert_equal(48675, int(tx_receipt['gasUsed'], 16), "wrong used gas") + assert_equal(1, len(tx_receipt['logs']), 'Wrong number of logs') + withdraw_event = tx_receipt['logs'][0] + check_withdraw_event(withdraw_event, format_eoa(proxy_contract.contract_address), forger_1_vrf_pub_key_to_bytes, + block_sign_pub_key_1, staked_amount) + + # Check the balance after withdrawal + gas_fee_paid, _, _ = computeForgedTxFee(sc_node_1, tx_hash) + assert_equal(evm_address_interop_balance - gas_fee_paid, rpc_get_balance(sc_node_1, evm_address_interop)) + evm_address_interop_balance -= gas_fee_paid + + assert_equal(proxy_contract_balance + staked_amount, + rpc_get_balance(sc_node_1, proxy_contract.contract_address)) + proxy_contract_balance += staked_amount + + assert_equal(forger_stake_v2_balance - staked_amount, + rpc_get_balance(sc_node_1, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS)) + forger_stake_v2_balance -= staked_amount_withdrawn + + # Check stakes by forger + result = sc_node_1.rpc_eth_call( + { + "to": "0x" + FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + "from": add_0x_prefix(evm_address_sc_node_1), + "input": paged_stakes_by_forger_1_data_input + }, "latest" + ) + + (next_pos, list_of_stakes) = decode_paged_list_of_forger_stakes(hex_str_to_bytes(result['result'][2:])) + assert_equal(-1, next_pos) + assert_equal(5, len(list_of_stakes)) + + assert_equal(exp_stake_own_1, list_of_stakes["0x" + evm_address_sc_node_1]) + assert_false(("0x" + evm_address_sc_node_2) in list_of_stakes) + assert_equal(exp_stake_own_3, list_of_stakes["0x" + evm_address_3]) + assert_equal(exp_stake_own_4, list_of_stakes["0x" + evm_address_4]) + assert_equal(exp_stake_own_5, list_of_stakes["0x" + evm_address_5]) + assert_false((proxy_contract.contract_address.lower()) in list_of_stakes) + + # Check stakes by delegator + data_input = forger_v2_native_contract.raw_encode_call(method_paged_stakes_by_delegator, + proxy_contract.contract_address, + 0, 100) + result = sc_node_1.rpc_eth_call( + { + "to": "0x" + FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + "from": add_0x_prefix(evm_address_sc_node_1), + "input": data_input + }, "latest" + ) + + (next_pos, list_of_stakes) = decode_paged_list_of_delegator_stakes(hex_str_to_bytes(result['result'][2:])) + assert_equal(-1, next_pos) + assert_equal(0, len(list_of_stakes)) + + # Check getCurrentConsensusEpoch using proxy + method = "getCurrentConsensusEpoch()" + native_input = format_eoa( + forger_v2_native_contract.raw_encode_call(method)) + + result = proxy_contract.do_static_call(evm_address_interop, 3, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, + native_input) + + epoch = decode(['uint32'], result)[0] + current_best_epoch = sc_node_1.block_forgingInfo()["result"]["bestBlockEpochNumber"] + assert_equal(current_best_epoch, epoch) + + ####################################################################################################### + # Reward workflow test + ####################################################################################################### + + ft_pool_amount = 0.5 + ft_pool_amount_wei = convertZenToWei(ft_pool_amount) + forward_transfer_to_sidechain(self.sc_nodes_bootstrap_info.sidechain_id, + mc_node, + format_eoa(FORGER_POOL_RECIPIENT_ADDRESS), + ft_pool_amount, + mc_return_address=mc_node.getnewaddress(), + generate_block=False) + mc_node.generate(1) + generate_next_block(sc_node_1, "second node") + # assert Forger Pool balance is updated + forger_pool_balance = int(self.sc_nodes[0].rpc_eth_getBalance(format_evm(FORGER_POOL_RECIPIENT_ADDRESS), 'latest')['result'], 16) + assert_equal(ft_pool_amount_wei, forger_pool_balance) + mc_node.generate(19) + + sc_last_we_block_id = generate_next_block(sc_node_1, "first node") + + self.sc_sync_all() + + # assert Forger Pool balance is distributed + forger_pool_balance = int( + self.sc_nodes[0].rpc_eth_getBalance(format_evm(FORGER_POOL_RECIPIENT_ADDRESS), 'latest')['result'], 16) + assert_equal(0, forger_pool_balance) + + payments = http_block_getFeePayments(sc_node_1, sc_last_we_block_id)['feePayments'] + assert_equal(forger_stake_list[0]['forgerStakeData']['ownerPublicKey']['address'], payments[0]['address']['address']) + assert_equal(ft_pool_amount_wei, payments[0]['valueFromMainchain']) + assert_equal(payments[0]['value'], payments[0]['valueFromMainchain'] + payments[0]['valueFromFees']) + + +def decode_paged_list_of_forgers(result): + next_pos = decode(['int32'], result[0:32])[0] + res = result[32:] + res = res[32:] # cut offset, don't care in this case + num_of_stakes = int(bytes_to_hex_str(res[0:32]), 16) + + res = res[32:] # cut the array length + + elem_size = 160 # 32 * 5 + list_of_elems = [res[i:i + elem_size] for i in range(0, num_of_stakes * elem_size, elem_size)] + + list_of_forgers = [] + for p in list_of_elems: + forger_info = decode_forger_info(p) + list_of_forgers.append(forger_info) + + return next_pos, list_of_forgers + + +def decode_forger_info(result): + raw_stake = decode(['(bytes32,bytes32,bytes1,uint32,address)'], result)[0] + forger_info = (bytes_to_hex_str(raw_stake[0]), + bytes_to_hex_str(raw_stake[1]) + bytes_to_hex_str(raw_stake[2]), + raw_stake[3], raw_stake[4]) + + return forger_info + + +def check_delegate_event(delegate_event, sender, vrf_pub_key, block_sign_pub_key, staked_amount): + assert_equal(4, len(delegate_event['topics']), "Wrong number of topics in delegate_event") + event_id = remove_0x_prefix(delegate_event['topics'][0]) + event_signature = remove_0x_prefix( + encode_hex(event_signature_to_log_topic('DelegateForgerStake(address,bytes32,bytes32,bytes1,uint256)'))) + assert_equal(event_signature, event_id, "Wrong event signature in topics") + + from_addr = decode(['address'], hex_str_to_bytes(delegate_event['topics'][1][2:]))[0][2:] + assert_equal(sender.lower(), from_addr.lower(), "Wrong from address in topics") + + vrf1 = decode(['bytes32'], hex_str_to_bytes(delegate_event['topics'][2][2:]))[0] + vrf2 = decode(['bytes1'], hex_str_to_bytes(delegate_event['topics'][3][2:]))[0] + + assert_equal(bytes_to_hex_str(vrf_pub_key), + bytes_to_hex_str(vrf1) + bytes_to_hex_str(vrf2), "wrong vrfPublicKey") + + (sign_pub_key, value) = decode(['bytes32', 'uint256'], hex_str_to_bytes(delegate_event['data'][2:])) + assert_equal(block_sign_pub_key, bytes_to_hex_str(sign_pub_key), "Wrong sign_pub_key in event") + assert_equal(staked_amount, value, "Wrong amount in event") + + +def check_withdraw_event(event, sender, vrf_pub_key, block_sign_pub_key, staked_amount): + assert_equal(4, len(event['topics']), "Wrong number of topics in withdraw_event") + event_id = remove_0x_prefix(event['topics'][0]) + event_signature = remove_0x_prefix( + encode_hex(event_signature_to_log_topic('WithdrawForgerStake(address,bytes32,bytes32,bytes1,uint256)'))) + 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(sender.lower(), from_addr.lower(), "Wrong from address in topics") + + vrf1 = decode(['bytes32'], hex_str_to_bytes(event['topics'][2][2:]))[0] + vrf2 = decode(['bytes1'], hex_str_to_bytes(event['topics'][3][2:]))[0] + + assert_equal(bytes_to_hex_str(vrf_pub_key), + bytes_to_hex_str(vrf1) + bytes_to_hex_str(vrf2), "wrong vrfPublicKey") + + (sign_pub_key, value) = decode(['bytes32', 'uint256'], hex_str_to_bytes(event['data'][2:])) + assert_equal(block_sign_pub_key, bytes_to_hex_str(sign_pub_key), "Wrong sign_pub_key in event") + assert_equal(staked_amount, value, "Wrong amount in event") + + +def decode_paged_list_of_forger_stakes(result): + next_pos = decode(['int32'], result[0:32])[0] + res = result[32:] + res = res[32:] # cut offset, don't care in this case + num_of_stakes = int(bytes_to_hex_str(res[0:32]), 16) + + res = res[32:] # cut the array length + + elem_size = 64 # 32 * 2 + list_of_elems = [res[i:i + elem_size] for i in range(0, num_of_stakes * elem_size, elem_size)] + + list_of_stakes = [] + for p in list_of_elems: + list_of_stakes.append(decode(['(address,uint256)'], p)[0]) + + return next_pos, dict(list_of_stakes) + + +def decode_paged_list_of_delegator_stakes(result): + next_pos = decode(['int32'], result[0:32])[0] + res = result[32:] + res = res[32:] # cut offset, don't care in this case + num_of_stakes = int(bytes_to_hex_str(res[0:32]), 16) + + res = res[32:] # cut the array length + + elem_size = 128 # 32 * 4 + list_of_elems = [res[i:i + elem_size] for i in range(0, num_of_stakes * elem_size, elem_size)] + + list_of_stakes = [] + for p in list_of_elems: + raw_stake = decode(['(bytes32,bytes32,bytes1,uint256)'], p)[0] + stake = (bytes_to_hex_str(raw_stake[0]), + bytes_to_hex_str(raw_stake[1]) + bytes_to_hex_str(raw_stake[2]), + raw_stake[3]) + list_of_stakes.append(stake) + + return next_pos, list_of_stakes + + +if __name__ == "__main__": + SCEvmNativeForgerV2().main() diff --git a/qa/sc_evm_rpc_eth.py b/qa/sc_evm_rpc_eth.py index 84c1657aef..52214943dd 100755 --- a/qa/sc_evm_rpc_eth.py +++ b/qa/sc_evm_rpc_eth.py @@ -1,4 +1,7 @@ #!/usr/bin/env python3 +import json +import urllib +import http.client from decimal import Decimal from SidechainTestFramework.account.ac_chain_setup import AccountChainSetup @@ -18,9 +21,44 @@ Test: - test eth_getTransactionByHash + - test some negative case for json rpc commands """ +REST_HEADERS = { + 'accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': 'Basic dXNlcjpIb3JpemVu' +} + +# SOME JSON RPC ERROR CODE +PARSE_ERROR_CODE = 32700 +INVALID_REQUEST_CODE = 32600 +METHOD_NOT_FOUND_CODE = 32601 +INVALID_PARAMS_CODE = 32602 + + +def do_rpc_call(node, payload, headers=None): + if headers is None: + headers = REST_HEADERS + + port = str(urllib.parse.urlparse(node.url).port) + conn = http.client.HTTPConnection("127.0.0.1:" + port) + conn.request("POST", "/ethv1", payload, headers) + res = conn.getresponse() + code = res.getcode() + data = res.read() + return code, json.loads(data.decode("utf-8")) + + +def check_result(code, result, expected_http_code, expected_string_in_result, expected_rpc_code=0): + print("RPC result = {}".format(result)) + assert_equal(expected_http_code, code) + assert_true('error' in result) + assert_true((result['error']['code'] == -expected_rpc_code)) + assert_true('message' in result['error']) + assert_true(expected_string_in_result in result['error']['message']) + class SCEvmRPCEth(AccountChainSetup): def __init__(self): @@ -90,6 +128,132 @@ def run_test(self): assert_equal(res_block['result']['height'], int(res["result"]['blockNumber'][2:], 16)) assert_equal("0x0", res["result"]['transactionIndex']) + # -------------------------------------------------- + + # missing request id + payload = json.dumps({ + "jsonrpc": "2.0", + "method": "eth_chainId", + "params": [] + }) + code, result = do_rpc_call(sc_node, payload) + check_result(code, result, expected_http_code=400, expected_string_in_result='missing field: id', + expected_rpc_code=INVALID_REQUEST_CODE) + + # missing params string (allowed) + payload = json.dumps({ + "jsonrpc": "2.0", + "method": "eth_chainId", + "id": 1 + }) + code, result = do_rpc_call(sc_node, payload) + assert_equal(200, code) + expected_result = {'jsonrpc': '2.0', 'id': 1, 'result': '0x3b9aca01'} + assert_equal(expected_result, result) + + # wrong params string (this command has no parameters) + payload = json.dumps({ + "jsonrpc": "2.0", + "method": "eth_chainId", + "id": 1, + "params": [{"qqq": 1}] + }) + code, result = do_rpc_call(sc_node, payload) + check_result(code, result, expected_http_code=200, expected_string_in_result='Invalid params', + expected_rpc_code=INVALID_PARAMS_CODE) + + # missing jsonrpc string + payload = json.dumps({ + "method": "eth_chainId", + "id": 1, + "params": [] + }) + code, result = do_rpc_call(sc_node, payload) + check_result(code, result, expected_http_code=400, expected_string_in_result='missing field: jsonrpc', + expected_rpc_code=INVALID_REQUEST_CODE) + + # wrong jsonrpc string version + payload = json.dumps({ + "method": "eth_chainId", + "id": 1, + "params": [], + "jsonrpc": "1.0", + }) + code, result = do_rpc_call(sc_node, payload) + check_result(code, result, expected_http_code=400, expected_string_in_result='jsonrpc value is not valid', + expected_rpc_code=INVALID_REQUEST_CODE) + + # wrong method specified (HTTP CODE 200) + payload = json.dumps({ + "method": "eth_chainId_", + "id": 1, + "params": [], + "jsonrpc": "2.0", + }) + code, result = do_rpc_call(sc_node, payload) + check_result(code, result, expected_http_code=200, expected_string_in_result='Method', + expected_rpc_code=METHOD_NOT_FOUND_CODE) + + # batch request with a failure, the expected output is an Array containing the corresponding Response objects + payload = json.dumps([ + { + "jsonrpc": "2.0", + "method": "net_version", + "params": [], + "id": 1 + }, + { + "jsonrpc": "2.0", + "method": "eth_chainId", + "params": [] + } + ]) + code, result = do_rpc_call(sc_node, payload) + assert_equal(400, code) + expected_result = [ + {'jsonrpc': '2.0', 'id': 1, 'result': '1000000001'}, + {'error': {'code': -32600, 'message': 'Invalid request: missing field: id', 'data': 'missing field: id'}, 'jsonrpc': '2.0', 'id': None} + ] + assert_equal(expected_result, result) + + # empty batch request + payload = json.dumps([]) + code, result = do_rpc_call(sc_node, payload) + check_result(code, result, expected_http_code=400, expected_string_in_result='Invalid request', + expected_rpc_code=INVALID_REQUEST_CODE) + + # batch request with just a failure, the expected output is a an array with a single error json + payload = json.dumps([1]) + code, result = do_rpc_call(sc_node, payload) + assert_equal(400, code) + expected_result = [ + {'error': {'code': -32600, 'message': 'Invalid request: missing field: id', 'data': 'missing field: id'}, 'jsonrpc': '2.0', 'id': None} + ] + assert_equal(expected_result, result) + + # batch with a single request, the response should be a json array with a single result (not just a result) + payload = json.dumps([ + { + "jsonrpc": "2.0", + "method": "net_version", + "params": [], + "id": 1 + }]) + + code, result = do_rpc_call(sc_node, payload) + assert_equal(200, code) + expected_result = [{'jsonrpc': '2.0', 'id': 1, 'result': '1000000001'}] + assert_equal(expected_result, result) + + # batch with an invalid json format + payload = '[{"jsonrpc": "2.0", "method": "net_version", "params": [], "id": 1}, {"jsonrpc": "2.0", "method"]' + + code, result = do_rpc_call(sc_node, payload) + check_result(code, result, expected_http_code=400, expected_string_in_result='Parse error', + expected_rpc_code=PARSE_ERROR_CODE) + expected_result = "{'error': {'code': -32700, 'message': \"Parse error" + assert_true(str(result).startswith(expected_result)) + if __name__ == "__main__": SCEvmRPCEth().main() diff --git a/qa/sc_evm_rpc_web3_methods.py b/qa/sc_evm_rpc_web3_methods.py index b9a31a4995..1223ce44e5 100755 --- a/qa/sc_evm_rpc_web3_methods.py +++ b/qa/sc_evm_rpc_web3_methods.py @@ -35,7 +35,7 @@ def run_test(self): # Check web3_clientVersion clientVersion = str(sc_node_1.rpc_web3_clientVersion()['result']) - assert_true(clientVersion.startswith('sidechains-sdk/')) + assert_true(clientVersion.startswith('dev/')) clientVersion = clientVersion.split("/") # Check that sdk version has at least three digits assert_true(re.search(r'\d.*\d.*\d', clientVersion[1])) diff --git a/qa/sc_evm_test_debug_methods.py b/qa/sc_evm_test_debug_methods.py index a0c7397704..8d56e5f5ad 100755 --- a/qa/sc_evm_test_debug_methods.py +++ b/qa/sc_evm_test_debug_methods.py @@ -1,12 +1,15 @@ #!/usr/bin/env python3 import logging +from eth_utils import 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 ( contract_function_call, deploy_smart_contract, generate_block_and_get_tx_receipt, ) +from SidechainTestFramework.account.httpCalls.transaction.createLegacyTransaction import createLegacyTransaction from test_framework.util import assert_equal, assert_true """ @@ -27,6 +30,7 @@ - Call traceCall method on current block - Generate a new block without transaction and call tracer methods - Call traceCall method on pending block + - Test for EON-1856 issue https://horizenlabs.atlassian.net/browse/EON-1856?atlOrigin=eyJpIjoiNDQzNDczZTkyMWVhNGExZTllMjlkNmVjNDkxMDgwNzkiLCJwIjoiaiJ9 """ @@ -273,7 +277,39 @@ def run_test(self): assert_true("output" not in trace_result) assert_equal("out of gas", trace_result["error"]) + # Test for EON-1856 issue. + # Create a transaction and forge a block. This block will have a lower base fee than its parent. The tx is + # created with a max gas fee slightly smaller than the base fee of the parent block but greater than the base + # fee of its block, so it is correctly forged. The test will fail if the debug_trace* methods use the block + # context of the parent block instead of the correct one. + + parent_block_base_fee = sc_node.block_best()["result"]['block']['header']['baseFee'] + + tx_hash = createLegacyTransaction(sc_node, + fromAddress=remove_0x_prefix(self.evm_address), + toAddress=remove_0x_prefix(self.evm_address), + value=1, + gasPrice=parent_block_base_fee - 1 + ) + + tx_status = generate_block_and_get_tx_receipt(sc_node, "0x" + tx_hash, True) + assert_equal(1, tx_status, "Error in tx - unrelated to debug methods") + + current_block_base_fee = sc_node.block_best()["result"]['block']['header']['baseFee'] + assert_true(current_block_base_fee < parent_block_base_fee, "Test for EON-1856 requires current block " + "base fee {0} is lower than parent block's one {1}" + .format(current_block_base_fee, parent_block_base_fee)) + + tx_trace = sc_node.rpc_debug_traceTransaction("0x" + tx_hash, {"tracer": "callTracer"})['result'] + assert_equal("CALL", tx_trace['type'], "callTracer type not CALL") + block_number = sc_node.rpc_eth_blockNumber()["result"] + tx_trace_block = sc_node.rpc_debug_traceBlockByNumber(block_number, {"tracer": "callTracer"})["result"][0] + assert_equal(tx_trace, tx_trace_block) + + block_hash = sc_node.rpc_eth_getBlockByNumber(block_number, False)['result']['hash'] + tx_trace_block = sc_node.rpc_debug_traceBlockByHash(block_hash, {"tracer": "callTracer"})["result"][0] + assert_equal(tx_trace, tx_trace_block) if __name__ == "__main__": diff --git a/sdk/.gitignore b/sdk/.gitignore index 4f34cfe7d1..1020d9a938 100644 --- a/sdk/.gitignore +++ b/sdk/.gitignore @@ -1,3 +1,4 @@ hs_err_* ${sys:logFilename} log +log/perfTest diff --git a/sdk/pom.xml b/sdk/pom.xml index f5a6dc67d1..04d0668ef2 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.horizen sidechains-sdk - 0.11.0 + 0.12.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.3.0 + 2.4.0 compile @@ -110,6 +110,21 @@ 5.1.0 compile + + io.prometheus + prometheus-metrics-core + 1.2.0 + + + io.prometheus + prometheus-metrics-instrumentation-jvm + 1.2.0 + + + io.prometheus + prometheus-metrics-exporter-common + 1.2.0 + com.google.inject.extensions @@ -189,6 +204,11 @@ jackson-databind 2.16.0 + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + 2.16.0 + org.mockito @@ -247,7 +267,7 @@ org.bouncycastle bcprov-jdk18on - 1.77 + 1.78 compile @@ -390,6 +410,7 @@ nonce_calculation_hex mcpublickeyhashproposition_hex boxmerklepathinfo_hex + block_fee_info_seq.dsv sidechaincoretransaction_hex ethereumtransaction_eoa2eoa_legacy_signed_hex ethereumtransaction_eoa2eoa_legacy_unsigned_hex diff --git a/sdk/src/main/java/io/horizen/account/api/rpc/handler/RpcResponseException.java b/sdk/src/main/java/io/horizen/account/api/rpc/handler/RpcResponseException.java new file mode 100644 index 0000000000..4fa6782153 --- /dev/null +++ b/sdk/src/main/java/io/horizen/account/api/rpc/handler/RpcResponseException.java @@ -0,0 +1,13 @@ +package io.horizen.account.api.rpc.handler; + +import io.horizen.account.api.rpc.request.RpcId; +import io.horizen.account.api.rpc.utils.RpcError; + +public class RpcResponseException extends RpcException { + public final RpcId id; + + public RpcResponseException(RpcError error, RpcId id) { + super(error); + this.id = id; + } +} diff --git a/sdk/src/main/java/io/horizen/account/api/rpc/request/RpcRequest.java b/sdk/src/main/java/io/horizen/account/api/rpc/request/RpcRequest.java index c513d6b420..f44de092c6 100644 --- a/sdk/src/main/java/io/horizen/account/api/rpc/request/RpcRequest.java +++ b/sdk/src/main/java/io/horizen/account/api/rpc/request/RpcRequest.java @@ -1,7 +1,7 @@ package io.horizen.account.api.rpc.request; import com.fasterxml.jackson.databind.JsonNode; -import io.horizen.account.api.rpc.handler.RpcException; +import io.horizen.account.api.rpc.handler.RpcResponseException; import io.horizen.account.api.rpc.utils.RpcCode; import io.horizen.account.api.rpc.utils.RpcError; @@ -16,34 +16,46 @@ public class RpcRequest { public final String method; public final JsonNode params; - private static final List mandatoryFields = List.of("jsonrpc", "id", "method"); + private static final String mandatoryIdField = "id"; + private static final List otherMandatoryFields = List.of("jsonrpc", "method"); private static final List stringFields = List.of("jsonrpc", "method"); private static final String JSON_RPC_VERSION = "2.0"; - public RpcRequest(JsonNode json) throws RpcException { - for (var field : mandatoryFields) { + public RpcRequest(JsonNode json) throws RpcResponseException { + + if (json.isArray() && json.isEmpty()) { + throw new RpcResponseException(RpcError.fromCode(RpcCode.InvalidRequest, "Empty array as input"), new RpcId()); + } + + // This should be the first to be checked, otherwise we fail to return the request id in other error conditions + if (!json.has(mandatoryIdField)) { + throw new RpcResponseException(RpcError.fromCode(RpcCode.InvalidRequest, String.format("missing field: %s", mandatoryIdField)), new RpcId()); + } + + try { + id = new RpcId(json.get(mandatoryIdField)); + } catch (IllegalArgumentException e) { + throw new RpcResponseException(RpcError.fromCode(RpcCode.InvalidRequest, e.getMessage()), new RpcId()); + } + + for (var field : otherMandatoryFields) { if (!json.has(field)) { - throw new RpcException( - RpcError.fromCode(RpcCode.InvalidRequest, String.format("missing field: %s", field))); + throw new RpcResponseException( + RpcError.fromCode(RpcCode.InvalidRequest, String.format("missing field: %s", field)), id); } } + for (var field : stringFields) { if (!json.get(field).isTextual()) { - throw new RpcException( - RpcError.fromCode(RpcCode.InvalidRequest, String.format("field must be string: %s", field))); + throw new RpcResponseException( + RpcError.fromCode(RpcCode.InvalidRequest, String.format("field must be string: %s", field)), id); } - - } - try { - id = new RpcId(json.get("id")); - } catch (IllegalArgumentException e) { - throw new RpcException(RpcError.fromCode(RpcCode.InvalidRequest, e.getMessage())); } jsonrpc = json.get("jsonrpc").asText(); // check if the jsonrpc value has the correct version if(!jsonrpc.equals(JSON_RPC_VERSION)) { - throw new RpcException(RpcError.fromCode(RpcCode.InvalidRequest, "jsonrpc value is not valid")); + throw new RpcResponseException(RpcError.fromCode(RpcCode.InvalidRequest, "jsonrpc value is not valid"), id); } method = json.get("method").asText(); diff --git a/sdk/src/main/java/io/horizen/account/api/rpc/types/FeePaymentsView.java b/sdk/src/main/java/io/horizen/account/api/rpc/types/FeePaymentsView.java index 68ef1d98b7..97c52b7117 100644 --- a/sdk/src/main/java/io/horizen/account/api/rpc/types/FeePaymentsView.java +++ b/sdk/src/main/java/io/horizen/account/api/rpc/types/FeePaymentsView.java @@ -1,11 +1,14 @@ package io.horizen.account.api.rpc.types; +import com.fasterxml.jackson.annotation.JsonInclude; import io.horizen.account.chain.AccountFeePaymentsInfo; +import io.horizen.account.utils.AccountPayment; import io.horizen.evm.Address; import scala.collection.JavaConverters; import java.math.BigInteger; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; public class FeePaymentsView { @@ -15,17 +18,31 @@ public FeePaymentsView(AccountFeePaymentsInfo info) { payments = JavaConverters .seqAsJavaList(info.payments()) .stream() - .map(payment -> new FeePaymentData(payment.address().address(), payment.value())) + .map(payment -> FeePaymentData.fromAccountFeePayment(payment)) .collect(Collectors.toList()); } + @JsonInclude(JsonInclude.Include.NON_ABSENT) private static class FeePaymentData { public final Address address; public final BigInteger value; + public final Optional valueFromMainchain; + public final Optional valueFromFees; - public FeePaymentData(Address address, BigInteger value) { + public FeePaymentData(Address address, BigInteger value, Optional valueFromMainchain, Optional valueFromFees) { this.address = address; this.value = value; + this.valueFromMainchain = valueFromMainchain; + this.valueFromFees = valueFromFees; + } + + public static FeePaymentData fromAccountFeePayment(AccountPayment payment) { + return new FeePaymentData( + payment.address().address(), + payment.value(), + Optional.ofNullable(payment.valueFromMainchain().getOrElse(() -> null)), + Optional.ofNullable(payment.valueFromFees().getOrElse(() -> null)) + ); } } } diff --git a/sdk/src/main/java/io/horizen/account/serialization/EthJsonMapper.java b/sdk/src/main/java/io/horizen/account/serialization/EthJsonMapper.java index f51eb0c560..bcadb0f67e 100644 --- a/sdk/src/main/java/io/horizen/account/serialization/EthJsonMapper.java +++ b/sdk/src/main/java/io/horizen/account/serialization/EthJsonMapper.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.fasterxml.jackson.module.scala.DefaultScalaModule; import io.horizen.evm.utils.BigIntegerDeserializer; import io.horizen.evm.utils.BigIntegerSerializer; @@ -21,6 +22,7 @@ public class EthJsonMapper { module.addDeserializer(byte[].class, new EthByteDeserializer()); mapper = new ObjectMapper(); mapper.registerModule(new DefaultScalaModule()); + mapper.registerModule(new Jdk8Module()); mapper.registerModule(module); mapper.enable(SerializationFeature.INDENT_OUTPUT); } diff --git a/sdk/src/test/java/io/horizen/account/state/AccountForgingStakeInfoABI.java b/sdk/src/main/java/io/horizen/account/state/AccountForgingStakeInfoABI.java similarity index 79% rename from sdk/src/test/java/io/horizen/account/state/AccountForgingStakeInfoABI.java rename to sdk/src/main/java/io/horizen/account/state/AccountForgingStakeInfoABI.java index 9f13857445..fb8fc38125 100644 --- a/sdk/src/test/java/io/horizen/account/state/AccountForgingStakeInfoABI.java +++ b/sdk/src/main/java/io/horizen/account/state/AccountForgingStakeInfoABI.java @@ -18,12 +18,12 @@ public class AccountForgingStakeInfoABI extends StaticStruct { public AccountForgingStakeInfoABI(byte[] stakeId, BigInteger amount, String owner, byte[] pubKey, byte[] vrf1, byte[] vrf2 ) { super( - new org.web3j.abi.datatypes.generated.Bytes32(stakeId), - new org.web3j.abi.datatypes.generated.Uint256(amount), + new Bytes32(stakeId), + new Uint256(amount), new org.web3j.abi.datatypes.Address(owner), - new org.web3j.abi.datatypes.generated.Bytes32(pubKey), - new org.web3j.abi.datatypes.generated.Bytes32(vrf1), - new org.web3j.abi.datatypes.generated.Bytes32(vrf2) + new Bytes32(pubKey), + new Bytes32(vrf1), + new Bytes1(vrf2) ); this.stakeId = stakeId; this.amount = amount; 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 a42ed09cb7..301c02e851 100644 --- a/sdk/src/main/java/io/horizen/account/state/EvmMessageProcessor.java +++ b/sdk/src/main/java/io/horizen/account/state/EvmMessageProcessor.java @@ -2,6 +2,7 @@ import io.horizen.account.fork.ContractInteroperabilityFork; import io.horizen.account.fork.Version1_3_0Fork; +import io.horizen.account.storage.MsgProcessorMetadataStorageReader; import io.horizen.evm.*; import io.horizen.evm.results.InvocationResult; import io.horizen.utils.BytesUtils; @@ -49,7 +50,7 @@ public boolean canProcess(Invocation invocation, BaseAccountStateView view, int } @Override - public byte[] process(Invocation invocation, BaseAccountStateView view, ExecutionContext context) + public byte[] process(Invocation invocation, BaseAccountStateView view, MsgProcessorMetadataStorageReader metadata, ExecutionContext context) throws ExecutionFailedException { // prepare context var block = context.blockContext(); 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 0c32c8bb38..64b7114372 100644 --- a/sdk/src/main/java/io/horizen/account/state/MessageProcessor.java +++ b/sdk/src/main/java/io/horizen/account/state/MessageProcessor.java @@ -1,6 +1,8 @@ package io.horizen.account.state; +import io.horizen.account.storage.MsgProcessorMetadataStorageReader; + // This interface models the entity which is responsible for handling the application of a transaction to a state view. // More in detail, a transaction is converted into a 'Message' object, which is processed // by a specific instance of MessageProcessor. @@ -32,12 +34,17 @@ public interface MessageProcessor { * * @param invocation invocation to execute * @param view state view + * @param metadata metadata view. IMPORTANT: always refers to the tip, not the current 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 considered as "invalid message" */ - byte[] process(Invocation invocation, BaseAccountStateView view, ExecutionContext context) - throws ExecutionFailedException; + byte[] process( + Invocation invocation, + BaseAccountStateView view, + MsgProcessorMetadataStorageReader metadata, + ExecutionContext context + ) throws ExecutionFailedException; } diff --git a/sdk/src/main/java/io/horizen/account/state/nativescdata/forgerstakev2/ForgerInfoABI.java b/sdk/src/main/java/io/horizen/account/state/nativescdata/forgerstakev2/ForgerInfoABI.java new file mode 100644 index 0000000000..07f4f0161e --- /dev/null +++ b/sdk/src/main/java/io/horizen/account/state/nativescdata/forgerstakev2/ForgerInfoABI.java @@ -0,0 +1,39 @@ +package io.horizen.account.state.nativescdata.forgerstakev2; + +import org.web3j.abi.datatypes.StaticStruct; +import org.web3j.abi.datatypes.generated.Bytes1; +import org.web3j.abi.datatypes.generated.Bytes32; +import org.web3j.abi.datatypes.generated.Uint32; + +public class ForgerInfoABI extends StaticStruct { + public byte[] pubKey; + public byte[] vrf1; + public byte[] vrf2; + public int rewardShare; + public String rewardAddress; + + public ForgerInfoABI(byte[] pubKey, byte[] vrf1, byte[] vrf2, int rewardShare, String rewardAddress) { + super( + new Bytes32(pubKey), + new Bytes32(vrf1), + new Bytes1(vrf2), + new Uint32(rewardShare), + new org.web3j.abi.datatypes.Address(rewardAddress) + ); + this.pubKey = pubKey; + this.vrf1 = vrf1; + this.vrf2 = vrf2; + this.rewardShare = rewardShare; + this.rewardAddress = rewardAddress; + } + + public ForgerInfoABI(Bytes32 pubKey, Bytes32 vrf1, Bytes1 vrf2, Uint32 rewardShare, org.web3j.abi.datatypes.Address rewardAddress) { + super(pubKey, vrf1, vrf2, rewardShare, rewardAddress); + this.pubKey = pubKey.getValue(); + this.vrf1 = vrf1.getValue(); + this.vrf2 = vrf2.getValue(); + this.rewardShare = rewardShare.getValue().intValueExact(); + this.rewardAddress = rewardAddress.getValue(); + } + +} diff --git a/sdk/src/main/java/io/horizen/metrics/MetricsHelp.java b/sdk/src/main/java/io/horizen/metrics/MetricsHelp.java new file mode 100644 index 0000000000..3f78530855 --- /dev/null +++ b/sdk/src/main/java/io/horizen/metrics/MetricsHelp.java @@ -0,0 +1,24 @@ +package io.horizen.metrics; + + +import com.fasterxml.jackson.annotation.JsonView; +import io.horizen.json.Views; + +@JsonView(Views.Default.class) +public class MetricsHelp { + + private String id; + private String description; + + public MetricsHelp(String id, String description){ + this.id = id; + this.description = description; + } + public String getId() { + return id; + } + + public String getDescription() { + return description; + } +} diff --git a/sdk/src/main/java/io/horizen/metrics/MetricsManager.java b/sdk/src/main/java/io/horizen/metrics/MetricsManager.java new file mode 100644 index 0000000000..9990a62fe8 --- /dev/null +++ b/sdk/src/main/java/io/horizen/metrics/MetricsManager.java @@ -0,0 +1,112 @@ +package io.horizen.metrics; + +import io.prometheus.metrics.core.metrics.Counter; +import io.prometheus.metrics.core.metrics.Gauge; +import io.prometheus.metrics.core.metrics.Info; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import sparkz.core.utils.TimeProvider; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + + +public class MetricsManager { + + protected static final Logger logger = LogManager.getLogger(); + + private TimeProvider timeProvider; + private static MetricsManager me; + + private Info nodeInfo; + private Counter blocksAppliedSuccessfully; + private Counter blocksNotApplied; + private Gauge blockApplyTime; + private Gauge blockApplyTimeAbsolute; + private Gauge mempoolSize; + private Gauge forgeBlockCount; + private Gauge forgeLotteryTime; + private Gauge forgeBlockCreationTime; + + private List helps; + + public static MetricsManager getInstance(){ + if (me == null){ + throw new RuntimeException("Metrics manager not initialized!"); + } + return me; + } + + public static void init(TimeProvider timeProvider) throws IOException { + if (me == null){ + me = new MetricsManager(timeProvider); + } + } + + private MetricsManager(TimeProvider timeProvider) throws IOException { + logger.debug("Initializing metrics engine"); + + this.timeProvider = timeProvider; + + //JvmMetrics.builder().register(); // initialize the out-of-the-box JVM metrics + helps = new ArrayList<>(); + + nodeInfo = Info.builder().name("node_info").labelNames("version", "sdkVersion", "architecture", "jdkVersion").register(); + helps.add(new MetricsHelp(nodeInfo.getPrometheusName(), "Node version")); + + blockApplyTime = Gauge.builder().name("block_apply_time").register(); + helps.add(new MetricsHelp(blockApplyTime.getPrometheusName(), "Time to apply block to node wallet and state (milliseconds)")); + + blockApplyTimeAbsolute = Gauge.builder().name("block_apply_time_fromslotstart").register(); + helps.add(new MetricsHelp(blockApplyTimeAbsolute.getPrometheusName(), "Delta between timestamp when block has been applied successfully on this node and start timestamp of the slot it belongs to (milliseconds)")); + + blocksAppliedSuccessfully = Counter.builder().name("block_applied_ok").register(); + helps.add(new MetricsHelp(blocksAppliedSuccessfully.getPrometheusName(),"Number of received blocks applied successfully (absolute value since start of the node)")); + + blocksNotApplied = Counter.builder().name("block_applied_ko").register(); + helps.add(new MetricsHelp(blocksNotApplied.getPrometheusName(), "Number of received blocks not applied (absolute value since start of the node)")); + + mempoolSize = Gauge.builder().name("mempool_size").register(); + helps.add(new MetricsHelp(mempoolSize.getPrometheusName(), "Mempool size (number of transactions in this node mempool)")); + + forgeBlockCount = Gauge.builder().name("forge_block_count").register(); + helps.add(new MetricsHelp(forgeBlockCount.getPrometheusName(), "Number of forged blocks by this node (absolute value since start of the node)")); + + forgeLotteryTime = Gauge.builder().name("forge_lottery_time").register(); + helps.add(new MetricsHelp(forgeLotteryTime.getPrometheusName(), "Time to execute the lottery (milliseconds)")); + + forgeBlockCreationTime = Gauge.builder().name("forge_blockcreation_time").register(); + helps.add(new MetricsHelp(forgeBlockCreationTime.getPrometheusName(), "Time to create a new forged block (calculated from the start timestamp of the slot it belongs to) (milliseconds)")); + } + + public long currentMillis(){ + return timeProvider.time(); + } + + public List getHelp(){ + return helps; + } + + public void appliedBlockOk(long millis, long millisFromBlockStamp){ + blockApplyTime.set(millis); + blockApplyTimeAbsolute.set(millisFromBlockStamp); + blocksAppliedSuccessfully.inc(); + } + + public void setVersion(String version){ nodeInfo.setLabelValues(version.split("/"));} + public void forgedBlock(long millis){ + forgeBlockCount.inc(); + forgeBlockCreationTime.set(millis); + } + public void appliedBlockKo(){ + blocksNotApplied.inc(); + } + public void mempoolSize(int size){ + mempoolSize.set(size); + } + public void lotteryDone(long millis){ + forgeLotteryTime.set(millis); + } + + +} diff --git a/sdk/src/main/java/io/horizen/utils/BytesUtils.java b/sdk/src/main/java/io/horizen/utils/BytesUtils.java index 026ef2cc73..ee5a932f1f 100644 --- a/sdk/src/main/java/io/horizen/utils/BytesUtils.java +++ b/sdk/src/main/java/io/horizen/utils/BytesUtils.java @@ -338,4 +338,14 @@ public static byte[] padWithZeroBytes(byte[] src, int destSize) { return src; } + // pad an array appending the necessary 0x00 bytes up to the wanted size + public static byte[] padRightWithZeroBytes(byte[] src, int destSize) { + if (src != null && src.length < destSize) { + byte[] padded_s = new byte[destSize]; + System.arraycopy(src, 0, padded_s, 0, src.length); + return padded_s; + } + return src; + } + } diff --git a/sdk/src/main/resources/account/api/accountApi.yaml b/sdk/src/main/resources/account/api/accountApi.yaml index c3520557ce..23a17c6a58 100644 --- a/sdk/src/main/resources/account/api/accountApi.yaml +++ b/sdk/src/main/resources/account/api/accountApi.yaml @@ -646,7 +646,7 @@ paths: tags: - transaction summary: creates transaction with forger stake - description: Creates transaction with forger stake. + description: Creates transaction with forger stake. This endpoint is disabled after Forger Stake V2 activation.. operationId: makeForgerStake security: - basicAuth: [] @@ -707,7 +707,7 @@ paths: tags: - transaction summary: creates and signs sidechain transaction for spending forging stake - description: Creates and signs sidechain transaction for spending forging stake. + description: Creates and signs sidechain transaction for spending forging stake. This endpoint is disabled after Forger Stake V2 activation.. operationId: spendForgingStake security: - basicAuth: [] @@ -1389,6 +1389,296 @@ paths: application/json: schema: $ref: '#/components/schemas/SidechainApiError' + + /transaction/registerForger: + post: + tags: + - transaction + summary: Register a new forger + description: Register a new forger. + operationId: registerForger + requestBody: + content: + application/json: + schema: + type: object + required: + - blockSignPubKey, vrfPubKey, rewardShare, rewardAddress, stakedAmount + properties: + blockSignPubKey: + type: string + description: Key of this forger (private key must be in the local wallet) + vrfPubKey: + type: string + description: Vrfkey of this forger(private key must be in the local wallet) + rewardShare: + type: integer + description: Reward to be redirected to a separate reward address (integer, range from 0 to 1000 - where 1000 represents 100%) + rewardAddress: + type: string + description: External reward address (may be a single EOA or (more likely) a smart contract handling rewards distribution to delegators) | + stakedAmount: + type: integer + description: Amount to be assigned as first stake to this forger. Specified in zennies, and must be >= 10 ZEN + gasInfo: + $ref: '#/components/schemas/EIP1559GasInfo' + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + properties: + result: + type: object + properties: + transactionId: + type: string + error: + $ref: '#/components/schemas/SidechainApiErrorResponse' + default: + description: any kind of http error + content: + application/json: + schema: + $ref: '#/components/schemas/SidechainApiError' + + /transaction/updateForger: + post: + tags: + - transaction + summary: Update an existing forger + description: Update an existing forger. This action can be performed only for forgers with rewardShare = 0 and rewardAddress not set, and only to assign them a value. Moreover, in order to execute this command, at least 2 epoch must be gone by after the EON 1.4 fork activation. + operationId: updateForger + requestBody: + content: + application/json: + schema: + type: object + required: + - blockSignPubKey, vrfPubKey, rewardShare, rewardAddress + properties: + blockSignPubKey: + type: string + description: Key of this forger (private key must be in the local wallet) + vrfPubKey: + type: string + description: Vrfkey of this forger(private key must be in the local wallet) + rewardShare: + type: integer + description: Reward share to be redirected to the specified reward address (integer, range from 0 to 1000 - where 1000 represents 100%) + rewardAddress: + type: string + description: External reward address (may be a single EOA or (more likely) a smart contract handling rewards distribution to delegators) | + gasInfo: + $ref: '#/components/schemas/EIP1559GasInfo' + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + properties: + result: + type: object + properties: + transactionId: + type: string + error: + $ref: '#/components/schemas/SidechainApiErrorResponse' + default: + description: any kind of http error + content: + application/json: + schema: + $ref: '#/components/schemas/SidechainApiError' + + /transaction/pagedForgingStakes: + post: + tags: + - transaction + summary: Returns the paginated list of forging stakes. + description: Returns the paginated list of forging stakes. This endpoint is disabled after Forger Stake V2 activation.. + operationId: pagedForgingStakes + responses: + '200': + description: successful operation. If nextPos is == -1 means no additional records, otherwise it is the starting index of the remaining elements. + content: + application/json: + schema: + type: object + properties: + result: + type: object + properties: + nextPos: + type: integer + stakes: + type: array + items: + type: object + properties: + forgerStakeData: + type: object + properties: + forgerPublicKeys: + type: object + properties: + blockSignPublicKey: + type: object + properties: + publicKey: + type: string + vrfPublicKey: + type: object + properties: + publicKey: + type: string + ownerPublicKey: + type: object + properties: + address: + type: string + stakedAmount: + type: integer + + default: + description: any kind of http error + content: + application/json: + schema: + $ref: '#/components/schemas/SidechainApiError' + + /transaction/pagedForgerStakesByDelegator: + post: + tags: + - transaction + summary: Returns the paginated list of forging stakes, filtered by a specific delegator. + description: Returns the paginated list of forging stakes, filtered by a specific delegator. + operationId: pagedForgerStakesByDelegator + requestBody: + content: + application/json: + schema: + type: object + required: + - delegatorAddress + properties: + delegatorAddress: + type: string + description: Delegator address + startPos: + type: integer + description: Start position in the paginated list + default: 0 + size: + type: integer + description: Number of records to return + default: 10 + + responses: + '200': + description: successful operation. If nextPos is == -1 means no additional records, otherwise it is the starting index of the remaining elements. + content: + application/json: + schema: + type: object + properties: + result: + type: object + properties: + nextPos: + type: integer + stakes: + type: array + items: + type: object + properties: + forgerPublicKeys: + type: object + properties: + blockSignPublicKey: + type: object + properties: + publicKey: + type: string + vrfPublicKey: + type: object + properties: + publicKey: + type: string + stakedAmount: + type: integer + default: + description: any kind of http error + content: + application/json: + schema: + $ref: '#/components/schemas/SidechainApiError' + + /transaction/pagedForgerStakesByForger: + post: + tags: + - transaction + summary: Returns the paginated list of forging stakes, filtered by a specific forger. + description: Returns the paginated list of forging stakes, filtered by a specific forger. + operationId: pagedForgerStakesByForger + requestBody: + content: + application/json: + schema: + type: object + required: + - blockSignPubKey, vrfPubKey + properties: + blockSignPubKey: + type: string + description: blockSignPubKey identifying the forger + vrfPubKey: + type: string + description: vrfPubKey identifying the forger + startPos: + type: integer + description: Start position in the paginated list + default: 0 + size: + type: integer + description: Number of records to return + default: 10 + + responses: + '200': + description: successful operation. If nextPos is == -1 means no additional records, otherwise it is the starting index of the remaining elements. + content: + application/json: + schema: + type: object + properties: + result: + type: object + properties: + nextPos: + type: integer + stakes: + type: array + items: + type: object + properties: + delegator: + type: object + properties: + address: + type: string + stakedAmount: + type: integer + default: + description: any kind of http error + content: + application/json: + schema: + $ref: '#/components/schemas/SidechainApiError' # Sidechain wallet operations /wallet/createPrivateKey25519: @@ -3618,6 +3908,7 @@ components: stakeId: type: string format: byte + nullable: true forgerStakeData: type: object properties: diff --git a/sdk/src/main/resources/account/rpc/api/eth_openrpc.json b/sdk/src/main/resources/account/rpc/api/eth_openrpc.json index fc1763cfd1..4f40262381 100644 --- a/sdk/src/main/resources/account/rpc/api/eth_openrpc.json +++ b/sdk/src/main/resources/account/rpc/api/eth_openrpc.json @@ -95,6 +95,16 @@ "title": "hex encoded value in wei", "type": "string", "pattern": "^0x[0-9,a-f,A-F]{40}$" + }, + "valueFromMainchain": { + "title": "After 1.4 - part of 'value' from mainchain reward. Hex encoded value in wei", + "type": "string", + "pattern": "^0x[0-9,a-f,A-F]{40}$" + }, + "valueFromFees": { + "title": "After 1.4 - part of 'value' from fees reward. Hex encoded value in wei", + "type": "string", + "pattern": "^0x[0-9,a-f,A-F]{40}$" } } } diff --git a/sdk/src/main/resources/sidechain-sdk-settings.conf b/sdk/src/main/resources/sidechain-sdk-settings.conf index 232487e8c4..5034710fd3 100644 --- a/sdk/src/main/resources/sidechain-sdk-settings.conf +++ b/sdk/src/main/resources/sidechain-sdk-settings.conf @@ -12,6 +12,16 @@ sparkz { restApi { bindAddress = "127.0.0.1:9085" timeout = 5s + # BCrypt hash of the password used in the request (for protected endpoints) + #apiKeyHash = "" + } + + metricsApi { + enabled = false + bindAddress = "127.0.0.1:9088" + timeout = 5s + # BCrypt hash of the password used in the request (to protect the endpoint) + #apiKeyHash = "" } network { diff --git a/sdk/src/main/scala/io/horizen/AbstractSidechainApp.scala b/sdk/src/main/scala/io/horizen/AbstractSidechainApp.scala index 5164e04883..ee293636fd 100644 --- a/sdk/src/main/scala/io/horizen/AbstractSidechainApp.scala +++ b/sdk/src/main/scala/io/horizen/AbstractSidechainApp.scala @@ -19,6 +19,7 @@ import io.horizen.forge.MainchainSynchronizer import io.horizen.fork.{ConsensusParamsFork, ConsensusParamsForkInfo, ForkConfigurator, ForkManager, OptionalSidechainFork, SidechainForkConsensusEpoch} import io.horizen.helper.{SecretSubmitProvider, SecretSubmitProviderImpl, TransactionSubmitProvider} import io.horizen.json.serializer.JsonHorizenPublicKeyHashSerializer +import io.horizen.metrics.MetricsManager import io.horizen.params._ import io.horizen.proposition._ import io.horizen.secret.SecretSerializer @@ -75,6 +76,8 @@ abstract class AbstractSidechainApp log.info(s"Starting application with settings \n$sidechainSettings") + protected val metricsManager = MetricsManager.init(timeProvider); + override implicit def exceptionHandler: ExceptionHandler = SidechainApiErrorHandler.exceptionHandler override implicit def rejectionHandler: RejectionHandler = SidechainApiRejectionHandler.rejectionHandler @@ -357,6 +360,8 @@ abstract class AbstractSidechainApp val coreApiRoutes: Seq[ApiRoute] + val metricsApiRoute: ApiRoute + // disabledApiRoutes is the list of endpoints from coreApiRoutes that may need to be disabled when certain criteria // are met (e.g. seeder node) lazy val disabledApiRoutes: Seq[SidechainRejectionApiRoute] = coreApiRoutes.flatMap{ @@ -398,6 +403,19 @@ abstract class AbstractSidechainApp connection.handleWithAsyncHandler(combinedRoute) }).run() + metricsApiRoute match { + case null => // do not expose metrics via http + case _ => + //Metrcis api are exposed on a separate port + val metricsBindAddress = sidechainSettings.metricsSettings.bindAddress + log.info("Exposing metric endpoint to %s".format(metricsBindAddress)) + Http().newServerAt(metricsBindAddress.getAddress.getHostAddress, metricsBindAddress.getPort).connectionSource().to(Sink.foreach { connection => + log.info("New REST metrics api connection from address :: %s".format(connection.remoteAddress.toString)) + connection.handleWithAsyncHandler(metricsApiRoute.route) + }).run() + } + + //Remove the Logger shutdown hook LogManager.getFactory match { case contextFactory: Log4jContextFactory => diff --git a/sdk/src/main/scala/io/horizen/AbstractSidechainNodeViewHolder.scala b/sdk/src/main/scala/io/horizen/AbstractSidechainNodeViewHolder.scala index 5dbe34ed01..adb8146197 100644 --- a/sdk/src/main/scala/io/horizen/AbstractSidechainNodeViewHolder.scala +++ b/sdk/src/main/scala/io/horizen/AbstractSidechainNodeViewHolder.scala @@ -5,6 +5,7 @@ import io.horizen.chain.AbstractFeePaymentsInfo import io.horizen.consensus.{FullConsensusEpochInfo, StakeConsensusEpochInfo, blockIdToEpochId} import io.horizen.history.AbstractHistory import io.horizen.history.validation._ +import io.horizen.metrics.MetricsManager import io.horizen.params.NetworkParams import io.horizen.secret.{Secret, SecretCreator} import io.horizen.storage.{AbstractHistoryStorage, SidechainStorageInfo} @@ -46,6 +47,7 @@ abstract class AbstractSidechainNodeViewHolder[ val maxTxFee: Long = sidechainSettings.wallet.maxTxFee val listOfStorageInfo: Seq[SidechainStorageInfo] + val metricsManager:MetricsManager = MetricsManager.getInstance() /** @@ -276,6 +278,7 @@ abstract class AbstractSidechainNodeViewHolder[ // This method is actually a copy-paste of parent NodeViewHolder.pmodModify method. // The difference is that modifiers are applied to the State and Wallet simultaneously. override protected def pmodModify(pmod: PMOD): Unit = { + val startTime = metricsManager.currentMillis() if (!history().contains(pmod.id)) { context.system.eventStream.publish(StartingPersistentModifierApplication(pmod)) @@ -298,14 +301,22 @@ abstract class AbstractSidechainNodeViewHolder[ updateNodeView(Some(newHistory), Some(newState), Some(newWallet), Some(newMemPool)) log.info(s"Persistent modifier ${pmod.encodedId} applied successfully and node view updated!") - log.debug(s"Current mempool size: ${newMemPool.size} transactions") + + val endTime = metricsManager.currentMillis() + metricsManager.mempoolSize(newMemPool.size) + metricsManager.appliedBlockOk( + endTime- startTime, + endTime - (pmod.timestamp * 1000) + ); + // TODO FOR MERGE: usedSizeKBytes()/usedPercentage() should be moved into sparkz.core.transaction.MemoryPool // or a new AbstractMemoryPool class should be created between MP and the concrete classes // - ${newMemPool.usedSizeKBytes}kb (${newMemPool.usedPercentage}%)") case Failure(e) => log.warn(s"Can`t apply persistent modifier (id: ${pmod.encodedId}, contents: $pmod) to minimal state", e) updateNodeView(updatedHistory = Some(newHistory)) + metricsManager.appliedBlockKo(); context.system.eventStream.publish(SemanticallyFailedModification(pmod, e)) } } else { @@ -313,6 +324,7 @@ abstract class AbstractSidechainNodeViewHolder[ } case Failure(e) => log.warn(s"Can`t apply persistent modifier (id: ${pmod.encodedId}, contents: $pmod) to history", e) + metricsManager.appliedBlockKo(); context.system.eventStream.publish(SyntacticallyFailedModification(pmod, e)) } } else { diff --git a/sdk/src/main/scala/io/horizen/SidechainSettings.scala b/sdk/src/main/scala/io/horizen/SidechainSettings.scala index a290c637fe..80ba224a9b 100644 --- a/sdk/src/main/scala/io/horizen/SidechainSettings.scala +++ b/sdk/src/main/scala/io/horizen/SidechainSettings.scala @@ -3,9 +3,9 @@ package io.horizen import io.horizen.account.mempool.MempoolMap import io.horizen.cryptolibprovider.CircuitTypes import io.horizen.cryptolibprovider.CircuitTypes.CircuitTypes -import sparkz.core.settings.SparkzSettings - +import sparkz.core.settings.{ApiSettings, SparkzSettings} import java.math.BigInteger +import java.net.InetSocketAddress import scala.annotation.meta.field import scala.concurrent.duration.{DurationInt, FiniteDuration} @@ -160,8 +160,15 @@ case class HistorySettings( resetModifiersStatus: Boolean = false, ) +case class MetricsApiSettings( + enabled: Boolean, + bindAddress: InetSocketAddress, + apiKeyHash: Option[String], + corsAllowedOrigin: Option[String], + timeout: FiniteDuration) extends ApiSettings case class SidechainSettings( sparkzSettings: SparkzSettings, + metricsSettings: MetricsApiSettings, genesisData: GenesisDataSettings, websocketClient: WebSocketClientSettings, websocketServer: WebSocketServerSettings, diff --git a/sdk/src/main/scala/io/horizen/SidechainSettingsReader.scala b/sdk/src/main/scala/io/horizen/SidechainSettingsReader.scala index 8834b41bbf..ebd4bf7e78 100644 --- a/sdk/src/main/scala/io/horizen/SidechainSettingsReader.scala +++ b/sdk/src/main/scala/io/horizen/SidechainSettingsReader.scala @@ -5,8 +5,8 @@ import com.typesafe.scalalogging.LazyLogging import net.ceedubs.ficus.Ficus._ import net.ceedubs.ficus.readers.ArbitraryTypeReader._ import net.ceedubs.ficus.readers.ValueReader -import net.ceedubs.ficus.readers.EnumerationReader._ //actually used -import sparkz.core.settings.{SettingsReaders, SparkzSettings} +import net.ceedubs.ficus.readers.EnumerationReader._ +import sparkz.core.settings.{RESTApiSettings, SettingsReaders, SparkzSettings} import java.io.File import java.math.BigInteger @@ -37,6 +37,7 @@ object SidechainSettingsReader val webSocketClientSettings = config.as[WebSocketClientSettings]("sparkz.websocketClient") val webSocketServerSettings = config.as[WebSocketServerSettings]("sparkz.websocketServer") val sparkzSettings = config.as[SparkzSettings]("sparkz") + val metricsSettings = config.as[MetricsApiSettings]("sparkz.metricsApi") val genesisSettings = config.as[GenesisDataSettings]("sparkz.genesis") val certificateSettings = config.as[WithdrawalEpochCertificateSettings]("sparkz.withdrawalEpochCertificate") val remoteKeysManagerSettings = config.as[RemoteKeysManagerSettings]("sparkz.remoteKeysManager") @@ -50,7 +51,7 @@ object SidechainSettingsReader val apiRateLimiterSettings = config.as[ApiRateLimiterSettings]("sparkz.apiRateLimiter") val historySettings = config.as[HistorySettings]("sparkz.history") - SidechainSettings(sparkzSettings, genesisSettings, webSocketClientSettings, webSocketServerSettings, certificateSettings, + SidechainSettings(sparkzSettings, metricsSettings, genesisSettings, webSocketClientSettings, webSocketServerSettings, certificateSettings, remoteKeysManagerSettings, mempoolSettings, walletSettings, forgerSettings, cswSettings, logInfoSettings, ethServiceSettings, accountMempoolSettings, apiRateLimiterSettings, historySettings) } diff --git a/sdk/src/main/scala/io/horizen/account/AccountSidechainApp.scala b/sdk/src/main/scala/io/horizen/account/AccountSidechainApp.scala index f8fa1f7d95..ef0fa0ceaf 100644 --- a/sdk/src/main/scala/io/horizen/account/AccountSidechainApp.scala +++ b/sdk/src/main/scala/io/horizen/account/AccountSidechainApp.scala @@ -27,6 +27,7 @@ import io.horizen.consensus.ConsensusDataStorage import io.horizen.evm.LevelDBDatabase import io.horizen.fork.ForkConfigurator import io.horizen.helper.{NodeViewProvider, NodeViewProviderImpl, TransactionSubmitProvider, TransactionSubmitProviderImpl} +import io.horizen.metrics.MetricsManager import io.horizen.network.SyncStatusActorRef import io.horizen.node.NodeWalletBase import io.horizen.secret.SecretSerializer @@ -170,7 +171,7 @@ class AccountSidechainApp @Inject() params, sidechainSettings.ethService, sidechainSettings.sparkzSettings.network.maxIncomingConnections, - RpcUtils.getClientVersion, + RpcUtils.getClientVersion(appVersion), sidechainTransactionActorRef, syncStatusActorRef, sidechainTransactionsCompanion @@ -194,6 +195,9 @@ class AccountSidechainApp @Inject() SidechainSubmitterApiRoute(settings.restApi, params, certificateSubmitterRef, nodeViewHolderRef, circuitType), route.AccountEthRpcRoute(settings.restApi, nodeViewHolderRef, rpcProcessor) ) + override lazy val metricsApiRoute: ApiRoute = if (sidechainSettings.metricsSettings.enabled) route.AccountMetricsRoute(sidechainSettings.metricsSettings, nodeViewHolderRef) else null + + MetricsManager.getInstance().setVersion(RpcUtils.getClientVersion(appVersion)) val nodeViewProvider: NodeViewProvider[ TX, diff --git a/sdk/src/main/scala/io/horizen/account/AccountSidechainNodeViewHolder.scala b/sdk/src/main/scala/io/horizen/account/AccountSidechainNodeViewHolder.scala index f53d7aa7de..2c7f641ae0 100644 --- a/sdk/src/main/scala/io/horizen/account/AccountSidechainNodeViewHolder.scala +++ b/sdk/src/main/scala/io/horizen/account/AccountSidechainNodeViewHolder.scala @@ -160,7 +160,7 @@ class AccountSidechainNodeViewHolder(sidechainSettings: SidechainSettings, override def getFeePaymentsInfo(state: MS, withdrawalEpochNumber: Int) : FPI = { val consensusEpochNumber: ConsensusEpochNumber = state.getCurrentConsensusEpochInfo._2.epoch - val feePayments = state.getFeePaymentsInfo(withdrawalEpochNumber, consensusEpochNumber) + val (feePayments, _) = state.getFeePaymentsInfo(withdrawalEpochNumber, consensusEpochNumber) AccountFeePaymentsInfo(feePayments) } diff --git a/sdk/src/main/scala/io/horizen/account/api/http/route/AccountEthRpcRejectionHandler.scala b/sdk/src/main/scala/io/horizen/account/api/http/route/AccountEthRpcRejectionHandler.scala index 7236561d58..a9bc9c94b5 100644 --- a/sdk/src/main/scala/io/horizen/account/api/http/route/AccountEthRpcRejectionHandler.scala +++ b/sdk/src/main/scala/io/horizen/account/api/http/route/AccountEthRpcRejectionHandler.scala @@ -19,7 +19,7 @@ object AccountEthRpcRejectionHandler { new RpcId(), RpcError.fromCode(RpcCode.ParseError, msg) ) - ) + ), hasError = true ) } .result() diff --git a/sdk/src/main/scala/io/horizen/account/api/http/route/AccountEthRpcRoute.scala b/sdk/src/main/scala/io/horizen/account/api/http/route/AccountEthRpcRoute.scala index 5ac0eb02ae..326350eb3a 100644 --- a/sdk/src/main/scala/io/horizen/account/api/http/route/AccountEthRpcRoute.scala +++ b/sdk/src/main/scala/io/horizen/account/api/http/route/AccountEthRpcRoute.scala @@ -56,7 +56,10 @@ case class AccountEthRpcRoute( _ => { entity(as[JsonNode]) { body => - SidechainApiResponse(rpcProcessor.processEthRpc(body)); + { + val (json_resp, hasError) = rpcProcessor.processEthRpc(body) + SidechainApiResponse(json_resp, hasError) + } } } } diff --git a/sdk/src/main/scala/io/horizen/account/api/http/route/AccountMetricsRoute.scala b/sdk/src/main/scala/io/horizen/account/api/http/route/AccountMetricsRoute.scala new file mode 100644 index 0000000000..d1f0fce1ea --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/api/http/route/AccountMetricsRoute.scala @@ -0,0 +1,82 @@ +package io.horizen.account.api.http.route + +import akka.actor.{ActorRef, ActorRefFactory} +import akka.http.scaladsl.server.Route +import com.fasterxml.jackson.annotation.JsonView +import com.fasterxml.jackson.databind.JsonNode +import io.horizen.SidechainTypes +import io.horizen.account.api.rpc.service.RpcProcessor +import io.horizen.account.block.{AccountBlock, AccountBlockHeader} +import io.horizen.account.chain.AccountFeePaymentsInfo +import io.horizen.account.node.{AccountNodeView, NodeAccountHistory, NodeAccountMemoryPool, NodeAccountState} +import io.horizen.api.http.JacksonSupport._ +import io.horizen.api.http.{ApiResponseUtil, SidechainApiResponse, SuccessResponse} +import io.horizen.api.http.route.SidechainApiRoute +import io.horizen.node.NodeWalletBase +import io.horizen.utils.ClosableResourceHandler +import io.prometheus.metrics.expositionformats.ExpositionFormats +import io.prometheus.metrics.model.registry.PrometheusRegistry +import sparkz.core.api.http.ApiDirectives +import io.horizen.MetricsApiSettings +import sparkz.util.SparkzLogging +import io.horizen.json.Views +import io.horizen.metrics.{MetricsHelp, MetricsManager} +import sparkz.core.settings.{ApiSettings, RESTApiSettings} + +import java.io.ByteArrayOutputStream +import scala.concurrent.ExecutionContext +import scala.reflect.ClassTag + +case class AccountMetricsRoute( + metricsSettings: MetricsApiSettings, + sidechainNodeViewHolderRef: ActorRef +)(implicit val context: ActorRefFactory, override val ec: ExecutionContext) + extends SidechainApiRoute[ + SidechainTypes#SCAT, + AccountBlockHeader, + AccountBlock, + AccountFeePaymentsInfo, + NodeAccountHistory, + NodeAccountState, + NodeWalletBase, + NodeAccountMemoryPool, + AccountNodeView + ] + with SidechainTypes + with ClosableResourceHandler + with SparkzLogging + with ApiDirectives { + + override val settings = metricsSettings.asInstanceOf[ApiSettings] + + override implicit val tag: ClassTag[AccountNodeView] = ClassTag[AccountNodeView](classOf[AccountNodeView]) + override val route: Route = pathPrefix("metrics") { + metricsHelp ~ metrics + } + + /** + * Returns registered metrics + */ + def metrics: Route = get { + entity(as[JsonNode]) { body => + { + val snapshots = PrometheusRegistry.defaultRegistry.scrape + val stream = new ByteArrayOutputStream + ExpositionFormats.init.getPrometheusTextFormatWriter.write(stream, snapshots) + SidechainApiResponse(stream.toString, false) + } + } + } + + def metricsHelp: Route = (get & path("help")) { + entity(as[JsonNode]) { body => + ApiResponseUtil.toResponse(MetricsHelpList(MetricsManager.getInstance().getHelp())) + } + } + + @JsonView(Array(classOf[Views.Default])) + private[horizen] case class MetricsHelpList(helps: java.util.List[MetricsHelp]) + extends SuccessResponse + + +} 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 86c324abdd..4f1747252c 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 @@ -11,15 +11,18 @@ import io.horizen.account.api.http.route.AccountTransactionRestScheme._ import io.horizen.account.block.{AccountBlock, AccountBlockHeader} import io.horizen.account.chain.AccountFeePaymentsInfo import io.horizen.account.companion.SidechainAccountTransactionsCompanion -import io.horizen.account.fork.Version1_3_0Fork +import io.horizen.account.fork.{Version1_3_0Fork, Version1_4_0Fork} import io.horizen.account.node.{AccountNodeView, NodeAccountHistory, NodeAccountMemoryPool, NodeAccountState} import io.horizen.account.proof.SignatureSecp256k1 import io.horizen.account.proposition.AddressProposition import io.horizen.account.secret.PrivateKeySecp256k1 +import io.horizen.account.state.ForgerStakeV2MsgProcessor.{MAX_REWARD_SHARE, MIN_REGISTER_FORGER_STAKED_AMOUNT_IN_WEI, NUM_OF_EPOCHS_AFTER_FORK_ACTIVATION_FOR_UPDATE_FORGER} import io.horizen.account.state.McAddrOwnershipMsgProcessor._ import io.horizen.account.state._ +import io.horizen.account.state.nativescdata.forgerstakev2.RegisterOrUpdateForgerCmdInputDecoder.NULL_ADDRESS_WITH_PREFIX_HEX_STRING +import io.horizen.account.state.nativescdata.forgerstakev2.{RegisterOrUpdateForgerCmdInput, StakeDataDelegator, StakeDataForger} import io.horizen.account.transaction.EthereumTransaction -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.WellKnownAddresses.{FORGER_STAKE_SMART_CONTRACT_ADDRESS, FORGER_STAKE_V2_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} @@ -33,17 +36,19 @@ import io.horizen.evm.Address import io.horizen.json.Views import io.horizen.node.NodeWalletBase import io.horizen.params.{NetworkParams, RegTestParams} -import io.horizen.proof.{SchnorrSignatureSerializer, Signature25519} -import io.horizen.proposition.{MCPublicKeyHashPropositionSerializer, PublicKey25519Proposition, SchnorrPropositionSerializer, VrfPublicKey} +import io.horizen.proof.{SchnorrSignatureSerializer, Signature25519, VrfProof} +import io.horizen.proposition._ import io.horizen.secret.PrivateKey25519 import io.horizen.utils.BytesUtils import org.web3j.crypto.Keys +import org.web3j.utils.Numeric.{cleanHexPrefix, hexStringToByteArray, prependHexPrefix} import sparkz.core.settings.RESTApiSettings import java.math.BigInteger import java.util.{Optional => JOptional} import scala.collection.convert.ImplicitConversions.`collection AsScalaIterable` import scala.concurrent.ExecutionContext +import scala.jdk.OptionConverters.RichOptional import scala.reflect.ClassTag import scala.util.{Failure, Success, Try} @@ -73,7 +78,8 @@ case class AccountTransactionApiRoute(override val settings: RESTApiSettings, signTransaction ~ makeForgerStake ~ withdrawCoins ~ spendForgingStake ~ createSmartContract ~ allWithdrawalRequests ~ allForgingStakes ~ myForgingStakes ~ decodeTransactionBytes ~ openForgerList ~ allowedForgerList ~ createKeyRotationTransaction ~ invokeProxyCall ~ invokeProxyStaticCall ~ sendKeysOwnership ~ getKeysOwnership ~ removeKeysOwnership ~ - getKeysOwnerScAddresses ~ sendMultisigKeysOwnership ~ pagedForgingStakes + getKeysOwnerScAddresses ~ sendMultisigKeysOwnership ~ pagedForgingStakes ~ pagedForgersStakesByForger ~ + pagedForgersStakesByDelegator ~ registerForger ~ updateForger } private def getFittingSecret(nodeView: AccountNodeView, fromAddress: Option[String], txValueInWei: BigInteger) @@ -102,6 +108,28 @@ case class AccountTransactionApiRoute(override val settings: RESTApiSettings, ) } + private def signMessageWithSecrets( + nodeView: AccountNodeView, + blockSignPubKey: PublicKey25519Proposition, vrfPublicKey: VrfPublicKey, + messageToSign: Array[Byte]): (Signature25519, VrfProof) = { + val wallet = nodeView.getNodeWallet + + val signature25519 = wallet.secretByPublicKey25519Proposition(blockSignPubKey).toScala match { + case None => throw new IllegalArgumentException("No matching secret for input blockSignPubKey") + case Some(secret) => secret.sign(messageToSign) + } + + val signatureVrf = wallet.secretByVrfPublicKey(vrfPublicKey).toScala match { + case None => throw new IllegalArgumentException("No matching secret for input vrfPublicKey") + case Some(secret) => secret.sign(messageToSign) + } + + log.debug(s"25519: key=$blockSignPubKey, signature=$signature25519") + log.debug(s"vrf: key=$vrfPublicKey, signature=$signatureVrf") + (signature25519, signatureVrf) + + } + /** * Create an unsigned legacy eth transaction, and then: * - if the optional input parameter 'outputRawBytes'=True is set in ReqLegacyTransaction just @@ -400,11 +428,67 @@ case class AccountTransactionApiRoute(override val settings: RESTApiSettings, applyOnNodeView { sidechainNodeView => val accountState = sidechainNodeView.getNodeState val epochNumber = accountState.getConsensusEpochNumber.getOrElse(0) - if (!sidechainNodeView.getNodeState.isForgerStakeAvailable(Version1_3_0Fork.get(epochNumber).active)) { - ApiResponseUtil.toResponse(GenericTransactionError("Unable to add", JOptional.empty())) - } else { - val valueInWei = ZenWeiConverter.convertZenniesToWei(body.forgerStakeInfo.value) + if (!accountState.isForgerStakeV1SmartContractDisabled(Version1_4_0Fork.get(epochNumber).active)) { + if (!sidechainNodeView.getNodeState.isForgerStakeAvailable(Version1_3_0Fork.get(epochNumber).active)) { + ApiResponseUtil.toResponse(GenericTransactionError("Unable to add", JOptional.empty())) + } else { + val valueInWei = ZenWeiConverter.convertZenniesToWei(body.forgerStakeInfo.value) + + // 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 = encodeAddNewStakeCmdRequest(body.forgerStakeInfo) + val tmpTx: EthereumTransaction = new EthereumTransaction( + params.chainId, + JOptional.of(new AddressProposition(FORGER_STAKE_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())) + } + } + } + else + ApiResponseUtil.toResponse(ErrorDisabledMethod()) + } + } + } + } + } + + def spendForgingStake: Route = (post & path("spendForgingStake")) { + withBasicAuth { + _ => { + entity(as[ReqSpendForgingStake]) { body => + // lock the view and try to create CoreTransaction + applyOnNodeView { sidechainNodeView => + val epochNumber = sidechainNodeView.getNodeState.getConsensusEpochNumber.getOrElse(0) + if (!sidechainNodeView.getNodeState.isForgerStakeV1SmartContractDisabled(Version1_4_0Fork.get(epochNumber).active)) { + val valueInWei = BigInteger.ZERO // default gas related params val baseFee = sidechainNodeView.getNodeState.getNextBaseFee var maxPriorityFeePerGas = BigInteger.valueOf(120) @@ -416,94 +500,259 @@ case class AccountTransactionApiRoute(override val settings: RESTApiSettings, maxPriorityFeePerGas = body.gasInfo.get.maxPriorityFeePerGas gasLimit = body.gasInfo.get.gasLimit } - + //getFittingSecret needs to take into account only gas 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 = encodeAddNewStakeCmdRequest(body.forgerStakeInfo) - val tmpTx: EthereumTransaction = new EthereumTransaction( - params.chainId, - JOptional.of(new AddressProposition(FORGER_STAKE_SMART_CONTRACT_ADDRESS)), - nonce, - gasLimit, - maxPriorityFeePerGas, - maxFeePerGas, - valueInWei, - dataBytes, - null - ) - validateAndSendTransaction(signTransactionWithSecret(secret, tmpTx)) + case Some(txCreatorSecret) => + val nonce = body.nonce.getOrElse(sidechainNodeView.getNodeState.getNonce(txCreatorSecret.publicImage.address)) + val stakeDataOpt = sidechainNodeView.getNodeState.getForgerStakeData(body.stakeId, Version1_3_0Fork.get(epochNumber).active) + stakeDataOpt match { + case Some(stakeData) => + val stakeOwnerSecretOpt = sidechainNodeView.getNodeWallet.secretByPublicKey(stakeData.ownerPublicKey) + if (stakeOwnerSecretOpt.isEmpty) { + ApiResponseUtil.toResponse(ErrorForgerStakeOwnerNotFound(s"Forger Stake Owner not found")) + } + else { + val stakeOwnerSecret = stakeOwnerSecretOpt.get().asInstanceOf[PrivateKeySecp256k1] + + val msgToSign = ForgerStakeMsgProcessor.getRemoveStakeCmdMessageToSign(BytesUtils.fromHexString(body.stakeId), txCreatorSecret.publicImage().address(), nonce.toByteArray) + val signature = stakeOwnerSecret.sign(msgToSign) + val dataBytes = encodeSpendStakeCmdRequest(signature, body.stakeId) + val tmpTx: EthereumTransaction = new EthereumTransaction( + params.chainId, + JOptional.of(new AddressProposition(FORGER_STAKE_SMART_CONTRACT_ADDRESS)), + nonce, + gasLimit, + maxPriorityFeePerGas, + maxFeePerGas, + valueInWei, + dataBytes, + null + ) + + validateAndSendTransaction(signTransactionWithSecret(txCreatorSecret, tmpTx)) + } + case None => ApiResponseUtil.toResponse(ErrorForgerStakeNotFound(s"No Forger Stake found with stake id ${body.stakeId}")) + } case None => ApiResponseUtil.toResponse(ErrorInsufficientBalance("No account with enough balance found", JOptional.empty())) } } + else + ApiResponseUtil.toResponse(ErrorDisabledMethod()) } } } } } - def spendForgingStake: Route = (post & path("spendForgingStake")) { + def pagedForgingStakes: Route = (post & path("pagedForgingStakes")) { withBasicAuth { _ => { - entity(as[ReqSpendForgingStake]) { body => + entity(as[ReqPagedForgingStakes]) { body => + withNodeView { sidechainNodeView => + val accountState = sidechainNodeView.getNodeState + val epochNumber = accountState.getConsensusEpochNumber.getOrElse(0) + if (!sidechainNodeView.getNodeState.isForgerStakeV1SmartContractDisabled(Version1_4_0Fork.get(epochNumber).active)) { + if (Version1_3_0Fork.get(epochNumber).active) { + Try { + accountState.getPagedListOfForgersStakes(body.startPos, body.size) + } match { + case Success((nextPos, listOfForgerStakes)) => + ApiResponseUtil.toResponse(RespPagedForgerStakes(nextPos, listOfForgerStakes.toList)) + case Failure(exception) => + ApiResponseUtil.toResponse(GenericTransactionError(s"Invalid input parameters", JOptional.of(exception))) + } + } else { + ApiResponseUtil.toResponse(GenericTransactionError(s"Fork 1.3 is not active, can not invoke this command", + JOptional.empty())) + } + } + else + ApiResponseUtil.toResponse(ErrorDisabledMethod()) + } + } + } + } + } + + private def addressIsNull(address: String) : Boolean = { + prependHexPrefix(address) == NULL_ADDRESS_WITH_PREFIX_HEX_STRING + } + + private def addressStringIsValid(address: String) : Try[Unit] = Try { + if (cleanHexPrefix(address).length == 2 * Address.LENGTH) { + val _ = BytesUtils.fromHexString(cleanHexPrefix(address)) + } else { + throw new IllegalArgumentException(s"Invalid address string length: ${cleanHexPrefix(address).length}, expected ${2 * Address.LENGTH}") + } + } + + def registerForger: Route = (post & path("registerForger")) { + withBasicAuth { + _ => { + entity(as[ReqRegisterForger]) { 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) + val rewardShare = body.rewardShare.getOrElse(0) - if (body.gasInfo.isDefined) { - maxFeePerGas = body.gasInfo.get.maxFeePerGas - maxPriorityFeePerGas = body.gasInfo.get.maxPriorityFeePerGas - gasLimit = body.gasInfo.get.gasLimit - } - //getFittingSecret needs to take into account only gas - val txCost = valueInWei.add(maxFeePerGas.multiply(gasLimit)) - val secret = getFittingSecret(sidechainNodeView, None, txCost) - secret match { - case Some(txCreatorSecret) => - val nonce = body.nonce.getOrElse(sidechainNodeView.getNodeState.getNonce(txCreatorSecret.publicImage.address)) - val epochNumber = sidechainNodeView.getNodeState.getConsensusEpochNumber.getOrElse(0) - val stakeDataOpt = sidechainNodeView.getNodeState.getForgerStakeData(body.stakeId, Version1_3_0Fork.get(epochNumber).active) - stakeDataOpt match { - case Some(stakeData) => - val stakeOwnerSecretOpt = sidechainNodeView.getNodeWallet.secretByPublicKey(stakeData.ownerPublicKey) - if (stakeOwnerSecretOpt.isEmpty) { - ApiResponseUtil.toResponse(ErrorForgerStakeOwnerNotFound(s"Forger Stake Owner not found")) + if (rewardShare < 0 || rewardShare > MAX_REWARD_SHARE) { + val msg = s"Reward share must be in the range [0, $MAX_REWARD_SHARE]" + ApiResponseUtil.toResponse(ErrorRegisterForgerInvalidRewardParams(msg)) + } + else { + val rewardAddress = body.rewardAddress.getOrElse(NULL_ADDRESS_WITH_PREFIX_HEX_STRING) + + addressStringIsValid(rewardAddress) match { + case Success(_) => + if (!addressIsNull(rewardAddress) && rewardShare == 0) { + val msg = s"Reward share cannot be 0 if reward address is defined - Reward share = ${body.rewardShare}, reward address = $rewardAddress" + ApiResponseUtil.toResponse(ErrorRegisterForgerInvalidRewardParams(msg)) + } + else if (addressIsNull(rewardAddress) && rewardShare != 0) { + val msg = s"Reward share cannot be different from 0 if reward address is null - Reward share = ${body.rewardShare}, reward address = $rewardAddress" + ApiResponseUtil.toResponse(ErrorRegisterForgerInvalidRewardParams(msg)) } else { - val stakeOwnerSecret = stakeOwnerSecretOpt.get().asInstanceOf[PrivateKeySecp256k1] + val valueInWei = ZenWeiConverter.convertZenniesToWei(body.stakedAmount) + if (valueInWei.compareTo(MIN_REGISTER_FORGER_STAKED_AMOUNT_IN_WEI) < 0) { + val msg = s"Value ${valueInWei.toString()} is below the minimum stake amount threshold: $MIN_REGISTER_FORGER_STAKED_AMOUNT_IN_WEI " + ApiResponseUtil.toResponse(ErrorRegisterForgerInvalidRewardParams(msg)) + } + else + doRegisterOrUpdateForger(ForgerStakeV2MsgProcessor.RegisterForgerCmd, sidechainNodeView, valueInWei, body, rewardShare, rewardAddress) + } + case Failure(ex) => + ApiResponseUtil.toResponse(ErrorRegisterForgerInvalidRewardParams(s"Invalid address: ${ex.getMessage}")) + } + } + } + } + } + } + } - val msgToSign = ForgerStakeMsgProcessor.getRemoveStakeCmdMessageToSign(BytesUtils.fromHexString(body.stakeId), txCreatorSecret.publicImage().address(), nonce.toByteArray) - val signature = stakeOwnerSecret.sign(msgToSign) - val dataBytes = encodeSpendStakeCmdRequest(signature, body.stakeId) - val tmpTx: EthereumTransaction = new EthereumTransaction( - params.chainId, - JOptional.of(new AddressProposition(FORGER_STAKE_SMART_CONTRACT_ADDRESS)), - nonce, - gasLimit, - maxPriorityFeePerGas, - maxFeePerGas, - valueInWei, - dataBytes, - null - ) + def updateForger: Route = (post & path("updateForger")) { + withBasicAuth { + _ => { + entity(as[ReqUpdateForger]) { body => + // lock the view and try to create CoreTransaction + applyOnNodeView { sidechainNodeView => + if (body.rewardShare <= 0 || body.rewardShare > MAX_REWARD_SHARE) { + val msg = s"Reward share must be in the range (0, $MAX_REWARD_SHARE]" + ApiResponseUtil.toResponse(ErrorRegisterForgerInvalidRewardParams(msg)) + } else { + addressStringIsValid(body.rewardAddress) match { + case Success(_) => + if (addressIsNull(body.rewardAddress)) { + val msg = s"Reward address can not be the null address" + ApiResponseUtil.toResponse(ErrorRegisterForgerInvalidRewardParams(msg)) + } else { + val valueInWei = BigInteger.ZERO + doRegisterOrUpdateForger(ForgerStakeV2MsgProcessor.UpdateForgerCmd, sidechainNodeView, valueInWei, body, body.rewardShare, body.rewardAddress) + } + case Failure(ex) => + ApiResponseUtil.toResponse(ErrorRegisterForgerInvalidRewardParams(s"Invalid address: ${ex.getMessage}")) + } + } + } + } + } + } + } + + private def doRegisterOrUpdateForger(operation: String, sidechainNodeView: AccountNodeView, valueInWei: BigInteger, body: ReqBaseForger, rewardShare: Int, rewardAddress: String) = { + + val accountState = sidechainNodeView.getNodeState + val epochNumber = accountState.getConsensusEpochNumber.getOrElse(0) + if (!Version1_4_0Fork.get(epochNumber).active) { + ApiResponseUtil.toResponse(GenericTransactionError(s"Fork 1.4 is not active, can not invoke this command", + JOptional.empty())) + } + else if (!accountState.forgerStakesV2IsActive) { + ApiResponseUtil.toResponse(GenericTransactionError(s"Forger Stake Storage V2 is not active, can not invoke this command", + JOptional.empty())) + } + else if (operation == ForgerStakeV2MsgProcessor.UpdateForgerCmd && + epochNumber < (Version1_4_0Fork.getActivationEpoch() + NUM_OF_EPOCHS_AFTER_FORK_ACTIVATION_FOR_UPDATE_FORGER) ) { + ApiResponseUtil.toResponse(GenericTransactionError(s"Fork 1.4 has been activated at epoch ${Version1_4_0Fork.getActivationEpoch()}, but $NUM_OF_EPOCHS_AFTER_FORK_ACTIVATION_FOR_UPDATE_FORGER epochs must go by before invoking this command (current epoch: $epochNumber)", + JOptional.empty())) + } else { + // 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 blockSignPubKey = PublicKey25519PropositionSerializer.getSerializer.parseBytesAndCheck(BytesUtils.fromHexString(body.blockSignPubKey)) + val vrfPubKey = VrfPublicKeySerializer.getSerializer.parseBytesAndCheck(BytesUtils.fromHexString(body.vrfPubKey)) + + Try { + val msg = ForgerStakeV2MsgProcessor.getHashedMessageToSign(body.blockSignPubKey, body.vrfPubKey, rewardShare, rewardAddress) + val signatures = signMessageWithSecrets(sidechainNodeView, blockSignPubKey, vrfPubKey, msg) + + encodeRegisterOrUpdateForgerCmdRequest(operation, blockSignPubKey, vrfPubKey, rewardShare, new AddressProposition(hexStringToByteArray(rewardAddress)), signatures._1, signatures._2) + } match { + case Success(dataBytes) => + + val tmpTx: EthereumTransaction = new EthereumTransaction( + params.chainId, + JOptional.of(new AddressProposition(FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS)), + nonce, + gasLimit, + maxPriorityFeePerGas, + maxFeePerGas, + valueInWei, + dataBytes, + null + ) + validateAndSendTransaction(signTransactionWithSecret(secret, tmpTx)) + + case Failure(exception) => + ApiResponseUtil.toResponse(GenericTransactionError(s"Command $operation failed: ", JOptional.of(exception))) - validateAndSendTransaction(signTransactionWithSecret(txCreatorSecret, tmpTx)) - } - case None => ApiResponseUtil.toResponse(ErrorForgerStakeNotFound(s"No Forger Stake found with stake id ${body.stakeId}")) - } - case None => - ApiResponseUtil.toResponse(ErrorInsufficientBalance("No account with enough balance found", JOptional.empty())) + } + + case None => + ApiResponseUtil.toResponse(ErrorInsufficientBalance("No account with enough balance found", JOptional.empty())) + + } + } + } + + + def pagedForgersStakesByForger: Route = (post & path("pagedForgersStakesByForger")) { + withBasicAuth { + _ => { + entity(as[ReqPagedForgerStakesByForger]) { body => + withNodeView { sidechainNodeView => + val accountState = sidechainNodeView.getNodeState + + Try { + val blockSignPubKey = PublicKey25519PropositionSerializer.getSerializer.parseBytesAndCheck(BytesUtils.fromHexString(body.blockSignPubKey)) + val vrfPubKey = VrfPublicKeySerializer.getSerializer.parseBytesAndCheck(BytesUtils.fromHexString(body.vrfPubKey)) + accountState.getPagedForgersStakesByForger( + ForgerPublicKeys(blockSignPubKey, vrfPubKey), body.startPos, body.size) + } match { + case Success(result) => + ApiResponseUtil.toResponse(RespPagedForgerStakesByForger(result.nextStartPos, result.stakesData.toList)) + case Failure(exception) => + ApiResponseUtil.toResponse(GenericTransactionError(s"Command failed: ", JOptional.of(exception))) } } } @@ -511,24 +760,24 @@ case class AccountTransactionApiRoute(override val settings: RESTApiSettings, } } - def pagedForgingStakes: Route = (post & path("pagedForgingStakes")) { + def pagedForgersStakesByDelegator: Route = (post & path("pagedForgersStakesByDelegator")) { withBasicAuth { _ => { - entity(as[ReqPagedForgingStakes]) { body => + entity(as[ReqPagedForgerStakesByDelegator]) { body => withNodeView { sidechainNodeView => val accountState = sidechainNodeView.getNodeState val epochNumber = accountState.getConsensusEpochNumber.getOrElse(0) - if (Version1_3_0Fork.get(epochNumber).active) { + if (Version1_4_0Fork.get(epochNumber).active) { Try { - accountState.getPagedListOfForgersStakes(body.startPos, body.size) + accountState.getPagedForgersStakesByDelegator(new Address(body.delegatorAddress), body.startPos, body.size) } match { - case Success((nextPos, listOfForgerStakes)) => - ApiResponseUtil.toResponse(RespPagedForgerStakes(nextPos, listOfForgerStakes.toList)) + case Success(result) => + ApiResponseUtil.toResponse(RespPagedForgerStakesByDelegator(result.nextStartPos, result.stakesData.toList)) case Failure(exception) => ApiResponseUtil.toResponse(GenericTransactionError(s"Invalid input parameters", JOptional.of(exception))) } } else { - ApiResponseUtil.toResponse(GenericTransactionError(s"Fork 1.3 is not active, can not invoke this command", + ApiResponseUtil.toResponse(GenericTransactionError(s"Fork 1.4 is not active, can not invoke this command", JOptional.empty())) } } @@ -541,7 +790,7 @@ case class AccountTransactionApiRoute(override val settings: RESTApiSettings, withNodeView { sidechainNodeView => val accountState = sidechainNodeView.getNodeState val epochNumber = accountState.getConsensusEpochNumber.getOrElse(0) - val listOfForgerStakes = accountState.getListOfForgersStakes(Version1_3_0Fork.get(epochNumber).active) + val listOfForgerStakes = accountState.getListOfForgersStakes(Version1_3_0Fork.get(epochNumber).active, Version1_4_0Fork.get(epochNumber).active) ApiResponseUtil.toResponse(RespForgerStakes(listOfForgerStakes.toList)) } } @@ -575,7 +824,7 @@ case class AccountTransactionApiRoute(override val settings: RESTApiSettings, withNodeView { sidechainNodeView => val accountState = sidechainNodeView.getNodeState val epochNumber = accountState.getConsensusEpochNumber.getOrElse(0) - val listOfForgerStakes = accountState.getListOfForgersStakes(Version1_3_0Fork.get(epochNumber).active) + val listOfForgerStakes = accountState.getListOfForgersStakes(Version1_3_0Fork.get(epochNumber).active, Version1_4_0Fork.get(epochNumber).active) if (listOfForgerStakes.nonEmpty) { val wallet = sidechainNodeView.getNodeWallet val walletPubKeys = wallet.allSecrets().map(_.publicImage).toSeq @@ -600,45 +849,44 @@ case class AccountTransactionApiRoute(override val settings: RESTApiSettings, val valueInWei = ZenWeiConverter.convertZenniesToWei(body.withdrawalRequest.value) val gasInfo = body.gasInfo - // 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 (gasInfo.isDefined) { - maxFeePerGas = gasInfo.get.maxFeePerGas - maxPriorityFeePerGas = gasInfo.get.maxPriorityFeePerGas - gasLimit = gasInfo.get.gasLimit - } + // 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) - val txCost = valueInWei.add(maxFeePerGas.multiply(gasLimit)) - val secret = getFittingSecret(sidechainNodeView, None, txCost) - secret match { - case Some(secret) => - val dataBytes = encodeAddNewWithdrawalRequestCmd(body.withdrawalRequest) - dataBytes match { - case Success(data) => - val nonce = body.nonce.getOrElse(sidechainNodeView.getNodeState.getNonce(secret.publicImage.address)) - val tmpTx: EthereumTransaction = new EthereumTransaction( - params.chainId, - JOptional.of(new AddressProposition(WithdrawalMsgProcessor.contractAddress)), - nonce, - gasLimit, - maxPriorityFeePerGas, - maxFeePerGas, - valueInWei, - data, - null - ) - validateAndSendTransaction(signTransactionWithSecret(secret, tmpTx)) - case Failure(exc) => - ApiResponseUtil.toResponse(ErrorInvalidMcAddress(s"Invalid Mc address ${body.withdrawalRequest.mainchainAddress}", JOptional.of(exc))) - } - case None => - ApiResponseUtil.toResponse(ErrorInsufficientBalance("No account with enough balance found", JOptional.empty())) - } + if (gasInfo.isDefined) { + maxFeePerGas = gasInfo.get.maxFeePerGas + maxPriorityFeePerGas = gasInfo.get.maxPriorityFeePerGas + gasLimit = gasInfo.get.gasLimit + } + val txCost = valueInWei.add(maxFeePerGas.multiply(gasLimit)) + val secret = getFittingSecret(sidechainNodeView, None, txCost) + secret match { + case Some(secret) => + val dataBytes = encodeAddNewWithdrawalRequestCmd(body.withdrawalRequest) + dataBytes match { + case Success(data) => + val nonce = body.nonce.getOrElse(sidechainNodeView.getNodeState.getNonce(secret.publicImage.address)) + val tmpTx: EthereumTransaction = new EthereumTransaction( + params.chainId, + JOptional.of(new AddressProposition(WithdrawalMsgProcessor.contractAddress)), + nonce, + gasLimit, + maxPriorityFeePerGas, + maxFeePerGas, + valueInWei, + data, + null + ) + validateAndSendTransaction(signTransactionWithSecret(secret, tmpTx)) + case Failure(exc) => + ApiResponseUtil.toResponse(ErrorInvalidMcAddress(s"Invalid Mc address ${body.withdrawalRequest.mainchainAddress}", JOptional.of(exc))) + } + case None => + ApiResponseUtil.toResponse(ErrorInsufficientBalance("No account with enough balance found", JOptional.empty())) + } } } } @@ -1109,6 +1357,10 @@ case class AccountTransactionApiRoute(override val settings: RESTApiSettings, } } + def encodeRegisterOrUpdateForgerCmdRequest(operation: String, blockSignPubKey: PublicKey25519Proposition, vrfPubKey: VrfPublicKey, rewardShare: Int, smartcontract_address: AddressProposition, sign1: Signature25519, sign2: VrfProof): Array[Byte] = { + val cmdInput = RegisterOrUpdateForgerCmdInput(ForgerPublicKeys(blockSignPubKey, vrfPubKey), rewardShare, smartcontract_address.address(), sign1, sign2) + Bytes.concat(BytesUtils.fromHexString(operation), cmdInput.encode()) + } def encodeAddNewStakeCmdRequest(forgerStakeInfo: TransactionForgerOutput): Array[Byte] = { @@ -1246,6 +1498,8 @@ case class AccountTransactionApiRoute(override val settings: RESTApiSettings, (transactionPathPrefix, "sendKeysOwnership", error), (transactionPathPrefix, "removeKeysOwnership", error), (transactionPathPrefix, "sendMultisigKeysOwnership", error), + (transactionPathPrefix, "registerForger", error), + (transactionPathPrefix, "updateForger", error), ) ++ proxyRoutes } else proxyRoutes @@ -1264,6 +1518,12 @@ object AccountTransactionRestScheme { @JsonView(Array(classOf[Views.Default])) private[horizen] case class RespPagedForgerStakes(nextPos: Int, stakes: List[AccountForgingStakeInfo]) extends SuccessResponse + @JsonView(Array(classOf[Views.Default])) + private[horizen] case class RespPagedForgerStakesByForger(nextPos: Int, stakes: List[StakeDataDelegator]) extends SuccessResponse + + @JsonView(Array(classOf[Views.Default])) + private[horizen] case class RespPagedForgerStakesByDelegator(nextPos: Int, stakes: List[StakeDataForger]) extends SuccessResponse + @JsonView(Array(classOf[Views.Default])) private[horizen] case class RespMcAddrOwnership(keysOwnership: Map[String, Seq[String]]) extends SuccessResponse @@ -1293,7 +1553,7 @@ object AccountTransactionRestScheme { mcMultisigAddress: String, mcSignatures: Array[String], redeemScript: String) - + private[horizen] case class TransactionRemoveMcAddrOwnershipInfo(var scAddress: String, mcTransparentAddress: Option[String]) @JsonView(Array(classOf[Views.Default])) @@ -1440,14 +1700,60 @@ object AccountTransactionRestScheme { @JsonView(Array(classOf[Views.Default])) - private[horizen] case class ReqPagedForgingStakes( - nonce: Option[BigInteger], - startPos: Int = 0, - size: Int = 10, - gasInfo: Option[EIP1559GasInfo]) { + private[horizen] case class ReqPagedForgingStakes(startPos: Int = 0, size: Int = 10) { require(size > 0 , "Size must be positive") } + + trait ReqBaseForger { + def nonce: Option[BigInteger] + def blockSignPubKey: String + def vrfPubKey: String + def gasInfo: Option[EIP1559GasInfo] + } + + + @JsonView(Array(classOf[Views.Default])) + private[horizen] case class ReqRegisterForger( + nonce: Option[BigInteger], + blockSignPubKey: String, + vrfPubKey: String, + rewardShare: Option[Int], + rewardAddress: Option[String], + stakedAmount: Long, // in zennies + gasInfo: Option[EIP1559GasInfo]) extends ReqBaseForger { + } + + + @JsonView(Array(classOf[Views.Default])) + private[horizen] case class ReqUpdateForger( + nonce: Option[BigInteger], + blockSignPubKey: String, + vrfPubKey: String, + rewardShare: Int, + rewardAddress: String, + gasInfo: Option[EIP1559GasInfo]) extends ReqBaseForger { + + } + + @JsonView(Array(classOf[Views.Default])) + private[horizen] case class ReqPagedForgerStakesByForger( + blockSignPubKey: String, + vrfPubKey: String, + startPos: Int = 0, + size: Int = 10) { + require(size > 0 , "Size must be positive") + } + + @JsonView(Array(classOf[Views.Default])) + private[horizen] case class ReqPagedForgerStakesByDelegator( + delegatorAddress: String, + startPos: Int = 0, + size: Int = 10) { + require(size > 0 , "Size must be positive") + } + + @JsonView(Array(classOf[Views.Default])) private[horizen] case class ReqEIP1559Transaction( to: Option[String], @@ -1547,4 +1853,15 @@ object AccountTransactionErrorResponse { override val code: String = "0210" } + case class ErrorRegisterForgerInvalidRewardParams(description: String) extends ErrorResponse { + override val code: String = "0211" + override val exception: JOptional[Throwable] = JOptional.empty() + } + + case class ErrorDisabledMethod() extends ErrorResponse { + override val code: String = "0212" + override val exception: JOptional[Throwable] = JOptional.empty() + override val description: String = "Method is disabled after Fork 1.4. Use Forger Stakes Native Smart Contract V2" + } + } 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 33a7483e30..0b36f3e011 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 @@ -4,7 +4,6 @@ import akka.actor.ActorRef import akka.pattern.ask import akka.util.Timeout import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.node.JsonNodeFactory import io.horizen.EthServiceSettings import io.horizen.account.api.rpc.handler.RpcException import io.horizen.account.api.rpc.types._ @@ -50,7 +49,6 @@ import sparkz.util.{ModifierId, SparkzLogging} import java.lang.reflect.Method import java.math.BigInteger import java.nio.charset.StandardCharsets -import java.util.Collections import scala.collection.JavaConverters.seqAsJavaListConverter import scala.collection.convert.ImplicitConversions.`collection AsScalaIterable` import scala.collection.mutable.ListBuffer @@ -59,7 +57,6 @@ import scala.concurrent.duration.FiniteDuration import scala.concurrent.{Await, Future, TimeoutException} import scala.language.postfixOps import scala.util.{Failure, Success, Try} -import scala.concurrent.ExecutionContext.Implicits.global class EthService( scNodeViewHolderRef: ActorRef, @@ -227,7 +224,7 @@ class EthService( private def doCall(nodeView: NV, params: TransactionArgs, tag: String): Array[Byte] = { getStateViewAtTag(nodeView, tag) { (tagStateView, blockContext) => val msg = params.toMessage(blockContext.baseFee, settings.globalRpcGasCap) - tagStateView.applyMessage(msg, new GasPool(msg.getGasLimit), blockContext) + tagStateView.applyMessage(msg, new GasPool(msg.getGasLimit), blockContext, nodeView.state.getView) } } @@ -722,7 +719,7 @@ class EthService( // apply transactions for ((tx, i) <- block.transactions.zipWithIndex) { - pendingStateView.applyTransaction(tx, i, gasPool, getBlockContext(block, blockInfo, nodeView.history)) match { + pendingStateView.applyTransaction(tx, i, gasPool, getBlockContext(block, blockInfo, nodeView.history), nodeView.state.getView) match { case Success(consensusDataReceipt) => val txGasUsed = consensusDataReceipt.cumulativeGasUsed.subtract(cumGasUsed) @@ -876,7 +873,10 @@ class EthService( val (block, blockInfo) = getBlockById(nodeView, blockId) // get state at previous block - getStateViewAtTag(nodeView, (blockInfo.height - 1).toString) { (tagStateView, blockContext) => + getStateViewAtTag(nodeView, (blockInfo.height - 1).toString) { (tagStateView, _) => + // We don't use the blockContext of the parent block, because it must be the one of block containing the transaction + val blockContext = getBlockContext(block, blockInfo, nodeView.history) + // apply mainchain references val epochNumber = TimeToEpochUtils.timeStampToEpochNumber(networkParams.sidechainGenesisBlockTimestamp, block.timestamp) val ftToSmartContractForkActive = Version1_2_0Fork.get(epochNumber).active @@ -890,7 +890,7 @@ class EthService( val traces = block.transactions.zipWithIndex.map({ case (tx, i) => using(new Tracer(config)) { tracer => blockContext.setTracer(tracer) - tagStateView.applyTransaction(tx, i, gasPool, blockContext) + tagStateView.applyTransaction(tx, i, gasPool, blockContext, nodeView.state.getView) tracer.getResult.result } }) @@ -936,8 +936,14 @@ class EthService( throw new RpcException(RpcError.fromCode(RpcCode.InvalidParams, s"transaction not found: $transactionHash")) ) + applyOnAccountView { nodeView => - getStateViewAtTag(nodeView, (blockNumber - 1).toString) { (tagStateView, blockContext) => + getStateViewAtTag(nodeView, (blockNumber - 1).toString) { (tagStateView, _) => + + // We don't use the blockContext of the parent block, because it must be the one of block containing the transaction + val blockInfo = getBlockInfoById(nodeView, block.id) + val blockContext = getBlockContext(block, blockInfo, nodeView.history) + // apply mainchain references val epochNumber = TimeToEpochUtils.timeStampToEpochNumber(networkParams.sidechainGenesisBlockTimestamp, block.timestamp) val ftToSmartContractForkActive = Version1_2_0Fork.get(epochNumber).active @@ -953,14 +959,14 @@ class EthService( // apply previous transactions without tracing for ((tx, i) <- previousTransactions.zipWithIndex) { - tagStateView.applyTransaction(tx, i, gasPool, blockContext) + tagStateView.applyTransaction(tx, i, gasPool, blockContext, nodeView.state.getView) } using(new Tracer(config)) { tracer => // enable tracing blockContext.setTracer(tracer) // apply requested transaction with tracing enabled - tagStateView.applyTransaction(requestedTx, previousTransactions.length, gasPool, blockContext) + tagStateView.applyTransaction(requestedTx, previousTransactions.length, gasPool, blockContext, nodeView.state.getView) // return the tracer result tracer.getResult.result } @@ -983,7 +989,7 @@ class EthService( 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 { + Try(tagStateView.applyMessage(msg, new GasPool(msg.getGasLimit), blockContext, nodeView.state.getView)) 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/api/rpc/service/RpcProcessor.scala b/sdk/src/main/scala/io/horizen/account/api/rpc/service/RpcProcessor.scala index 2970bcfb25..7b6b1c0d0a 100644 --- a/sdk/src/main/scala/io/horizen/account/api/rpc/service/RpcProcessor.scala +++ b/sdk/src/main/scala/io/horizen/account/api/rpc/service/RpcProcessor.scala @@ -1,7 +1,7 @@ package io.horizen.account.api.rpc.service import com.fasterxml.jackson.databind.JsonNode -import io.horizen.account.api.rpc.handler.{RpcException, RpcHandler} +import io.horizen.account.api.rpc.handler.{RpcHandler, RpcResponseException} import io.horizen.account.api.rpc.request.{RpcId, RpcRequest} import io.horizen.account.api.rpc.response.RpcResponseError import io.horizen.account.api.rpc.utils.{RpcCode, RpcError} @@ -12,35 +12,44 @@ import scala.jdk.CollectionConverters.asScalaIteratorConverter import scala.util.{Failure, Success, Try} -case class RpcProcessor(val rpcHandler: RpcHandler) extends SparkzLogging { +case class RpcProcessor(rpcHandler: RpcHandler) extends SparkzLogging { + + def processEthRpc(body: JsonNode): (String, Boolean) = { + + var jsonIsArray = false - def processEthRpc(body: JsonNode): String = { val requests = if (body.isArray && !body.isEmpty) { // if the input json is an array a batch rpc request will be handled // the single rpc request will retrieve from the input json and they will be processed by rpcHandler // the position of the elements in the output will reflect their position in the input request + jsonIsArray = true body.iterator().asScala.toArray } else { // if the input json is not an array a single rpc request will be handled Array(body) } + var hasError : Boolean = false + val responses = requests.map(json => Try.apply(new RpcRequest(json)).map(rpcHandler.apply) match { case Success(value) => value - case Failure(exception: RpcException) => new RpcResponseError(new RpcId(), exception.error); + case Failure(exception: RpcResponseException) => + hasError = true + new RpcResponseError(exception.id, exception.error); case Failure(exception) => log.trace(s"internal error on RPC call: $exception") + hasError = true new RpcResponseError(new RpcId(), RpcError.fromCode(RpcCode.InvalidRequest)); }) - val json = if (responses.length > 1) { + val json = if (jsonIsArray) { EthJsonMapper.serialize(responses) } else { EthJsonMapper.serialize(responses.head) } log.trace(s"RPC message response << $json") - json + (json, hasError) } } diff --git a/sdk/src/main/scala/io/horizen/account/api/rpc/service/RpcUtils.scala b/sdk/src/main/scala/io/horizen/account/api/rpc/service/RpcUtils.scala index cc82d265fd..4b82aa24f2 100644 --- a/sdk/src/main/scala/io/horizen/account/api/rpc/service/RpcUtils.scala +++ b/sdk/src/main/scala/io/horizen/account/api/rpc/service/RpcUtils.scala @@ -4,16 +4,13 @@ import scala.util.Try object RpcUtils { - def getClientVersion: String = { + def getClientVersion(appVersion: String): String = { val default = "dev" + val version = if (appVersion.isBlank) default else appVersion val architecture = Try(System.getProperty("os.arch")).getOrElse(default) val javaVersion = Try(System.getProperty("java.specification.version")).getOrElse(default) val sdkPackage = this.getClass.getPackage - val sdkTitle = sdkPackage.getImplementationTitle match { - case null => default - case title => Try(title.split(":")(1)).getOrElse(title) - } val sdkVersion = sdkPackage.getImplementationVersion - s"$sdkTitle/$sdkVersion/$architecture/jdk$javaVersion" + s"$version/$sdkVersion/$architecture/jdk$javaVersion" } } diff --git a/sdk/src/main/scala/io/horizen/account/forger/AccountForgeMessageBuilder.scala b/sdk/src/main/scala/io/horizen/account/forger/AccountForgeMessageBuilder.scala index 6bfd6bc9f2..64a8b15788 100644 --- a/sdk/src/main/scala/io/horizen/account/forger/AccountForgeMessageBuilder.scala +++ b/sdk/src/main/scala/io/horizen/account/forger/AccountForgeMessageBuilder.scala @@ -6,7 +6,7 @@ import io.horizen.account.block.AccountBlock.calculateReceiptRoot import io.horizen.account.block.{AccountBlock, AccountBlockHeader} import io.horizen.account.chain.AccountFeePaymentsInfo import io.horizen.account.companion.SidechainAccountTransactionsCompanion -import io.horizen.account.fork.{Version1_2_0Fork, GasFeeFork} +import io.horizen.account.fork.{GasFeeFork, Version1_2_0Fork, Version1_4_0Fork} import io.horizen.account.history.AccountHistory import io.horizen.account.mempool.{AccountMemoryPool, MempoolMap, TransactionsByPriceAndNonceIter} import io.horizen.account.proposition.AddressProposition @@ -16,6 +16,7 @@ import io.horizen.account.state.receipt.EthereumConsensusDataReceipt import io.horizen.account.storage.AccountHistoryStorage import io.horizen.account.transaction.EthereumTransaction import io.horizen.account.utils.FeeUtils.calculateBaseFee +import io.horizen.account.utils.ZenWeiConverter.MAX_MONEY_IN_WEI import io.horizen.account.utils._ import io.horizen.account.wallet.AccountWallet import io.horizen.block._ @@ -123,7 +124,7 @@ class AccountForgeMessageBuilder( priceAndNonceIter.removeAndSkipAccount() } else { - stateView.applyTransaction(tx, listOfTxsInBlock.size, blockGasPool, blockContext) match { + stateView.applyTransaction(tx, listOfTxsInBlock.size, blockGasPool, blockContext, stateView) match { case Success(consensusDataReceipt) => val ethTx = tx.asInstanceOf[EthereumTransaction] @@ -248,7 +249,18 @@ class AccountForgeMessageBuilder( val currentBlockPayments = resultTuple._3 val consensusEpochNumber: ConsensusEpochNumber = intToConsensusEpochNumber(blockContext.consensusEpochNumber) - dummyView.updateForgerBlockCounter(forgerAddress, consensusEpochNumber) + val fork_1_4_active = Version1_4_0Fork.get(consensusEpochNumber).active + if (fork_1_4_active) { + dummyView.updateForgerBlockCounter( + new ForgerIdentifier( + forgerAddress, + Some(ForgerPublicKeys(forgingStakeInfo.blockSignPublicKey, forgingStakeInfo.vrfPublicKey)) + ), + consensusEpochNumber + ) + } else { + dummyView.updateForgerBlockCounter(new ForgerIdentifier(forgerAddress), consensusEpochNumber) + } val feePayments = if (isWithdrawalEpochLastBlock) { // Current block is expected to be the continuation of the current tip, so there are no ommers. @@ -259,11 +271,19 @@ class AccountForgeMessageBuilder( require(ommers.isEmpty, "No Ommers allowed for the last block of the withdrawal epoch.") val withdrawalEpochNumber: Int = WithdrawalEpochUtils.getWithdrawalEpochInfo(mainchainBlockReferencesData.size, dummyView.getWithdrawalEpochInfo, params).epoch - + val distributionCap = if (fork_1_4_active) { + val mcLastBlockHeight = params.mainchainCreationBlockHeight + ((withdrawalEpochNumber + 1) * params.withdrawalEpochLength) - 1 + AccountFeePaymentsUtils.getMainchainWithdrawalEpochDistributionCap(mcLastBlockHeight, params) + } else MAX_MONEY_IN_WEI + val blockToAppendFeeInfo = if (fork_1_4_active) { + Some(currentBlockPayments.copy(forgerKeys = Some(ForgerPublicKeys(forgingStakeInfo.blockSignPublicKey, forgingStakeInfo.vrfPublicKey)))) + } else { + Some(currentBlockPayments) + } // get all previous payments for current ending epoch and append the one of the current block - val feePayments = dummyView.getFeePaymentsInfo(withdrawalEpochNumber, consensusEpochNumber, Some(currentBlockPayments)) + val (feePayments, poolBalanceDistributed) = dummyView.getFeePaymentsInfo(withdrawalEpochNumber, consensusEpochNumber, distributionCap, blockToAppendFeeInfo) - dummyView.resetForgerPoolAndBlockCounters(consensusEpochNumber) + dummyView.subtractForgerPoolBalanceAndResetBlockCounters(consensusEpochNumber, poolBalanceDistributed) // add rewards to forgers balance feePayments.foreach(payment => dummyView.addBalance(payment.address.address(), payment.value)) @@ -409,7 +429,7 @@ class AccountForgeMessageBuilder( // 2. get from stateDb using root above the collection of all forger stakes (ordered) val forgingStakeInfoSeq: Seq[ForgingStakeInfo] = using(state.getStateDbViewFromRoot(stateRoot)) { stateViewFromRoot => - stateViewFromRoot.getOrderedForgingStakesInfoSeq(nextConsensusEpochNumber) + stateViewFromRoot.getOrderedForgingStakesInfoSeq(nextConsensusEpochNumber - 2) } // 3. using wallet secrets, filter out the not-mine forging stakes diff --git a/sdk/src/main/scala/io/horizen/account/fork/Version1_4_0Fork.scala b/sdk/src/main/scala/io/horizen/account/fork/Version1_4_0Fork.scala new file mode 100644 index 0000000000..b158aff421 --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/fork/Version1_4_0Fork.scala @@ -0,0 +1,25 @@ +package io.horizen.account.fork + +import io.horizen.fork.{ForkManager, OptionalSidechainFork} + +case class Version1_4_0Fork(active: Boolean = false) extends OptionalSidechainFork + +/** + *

This fork introduces the following major changes:

+ *
    + *
  • 1. It enables new stake delegation management
  • + *
  • 2. It enables max cap for mainchain forger reward distribution based on mainchain coinbase.
  • + *
  • 2. It enables the minimum stake for forgers for participating in the Lottery.
  • + *
+ */ +object Version1_4_0Fork { + def get(epochNumber: Int): Version1_4_0Fork = { + ForkManager.getOptionalSidechainFork[Version1_4_0Fork](epochNumber).getOrElse(DefaultFork) + } + + def getActivationEpoch(): Int = { + ForkManager.getFirstActivationEpoch[Version1_4_0Fork]() + } + + private val DefaultFork: Version1_4_0Fork = Version1_4_0Fork() +} diff --git a/sdk/src/main/scala/io/horizen/account/history/AccountHistory.scala b/sdk/src/main/scala/io/horizen/account/history/AccountHistory.scala index 723c939a18..605f5565a2 100644 --- a/sdk/src/main/scala/io/horizen/account/history/AccountHistory.scala +++ b/sdk/src/main/scala/io/horizen/account/history/AccountHistory.scala @@ -48,6 +48,10 @@ extends AbstractHistory[ if (!Version1_3_0Fork.get(consensusEpochNumber).active) { // fork is not active for computing the number of sc blocks without mc refs false + } else if (parentBlockId.equals("0b4383a95047e218d93e84cdf98f8daf1adf0cfa145230e17bd9cc795aed39b3")) { + // Porkaround for EON Pregobi testnet block 1563033 + // it was accepted to the chain because this fix has been introduced after the 1.3 fork activation on Pregobi: https://github.com/HorizenOfficial/Sidechains-SDK/pull/996/files + false } else { storage.tooManyBlocksWithoutMcHeadersDataSince(parentBlockId) } diff --git a/sdk/src/main/scala/io/horizen/account/network/PagedForgersOutput.scala b/sdk/src/main/scala/io/horizen/account/network/PagedForgersOutput.scala new file mode 100644 index 0000000000..303e6a89e6 --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/network/PagedForgersOutput.scala @@ -0,0 +1,142 @@ +package io.horizen.account.network + +import com.fasterxml.jackson.annotation.JsonView +import io.horizen.account.abi.{ABIDecoder, ABIEncodable, MsgProcessorInputDecoder} +import io.horizen.account.proposition.{AddressProposition, AddressPropositionSerializer} +import io.horizen.account.state.nativescdata.forgerstakev2.{ForgerInfoABI, VRFDecoder} +import io.horizen.account.state.{ForgerPublicKeys, ForgerPublicKeysSerializer} +import io.horizen.evm.Address +import io.horizen.json.Views +import io.horizen.proposition.{PublicKey25519Proposition, VrfPublicKey} +import org.web3j.abi.TypeReference +import org.web3j.abi.datatypes.generated.{Bytes1, Bytes32, Int32, Uint32} +import org.web3j.abi.datatypes.{DynamicArray, DynamicStruct, StaticStruct, Type, Address => AbiAddress} +import sparkz.core.serialization.{BytesSerializable, SparkzSerializer} +import sparkz.util.serialization.{Reader, Writer} + +import java.util +import scala.collection.JavaConverters +import scala.collection.convert.ImplicitConversions.`collection AsScalaIterable` + + +case class PagedForgersOutput(nextStartPos: Int, listOfForgerInfo: Seq[ForgerInfo]) + extends ABIEncodable[DynamicStruct] { + + override def asABIType(): DynamicStruct = { + + val seqOfStruct = listOfForgerInfo.map(_.asABIType()) + val listOfStruct = JavaConverters.seqAsJavaList(seqOfStruct) + val theType = classOf[StaticStruct] + val listOfParams: util.List[Type[_]] = util.Arrays.asList( + new Int32(nextStartPos), + new DynamicArray(theType, listOfStruct) + ) + new DynamicStruct(listOfParams) + + } + + override def toString: String = "%s(startPos: %s, ForgerInfo: %s)" + .format( + this.getClass.toString, + nextStartPos, listOfForgerInfo) +} + + +object PagedForgersOutputDecoder + extends ABIDecoder[PagedForgersOutput] + with MsgProcessorInputDecoder[PagedForgersOutput]{ + + override val getListOfABIParamTypes: util.List[TypeReference[Type[_]]] = + org.web3j.abi.Utils.convert(util.Arrays.asList( + new TypeReference[Int32]() {}, + new TypeReference[DynamicArray[ForgerInfoABI]]() {} + )) + + override def createType(listOfParams: util.List[Type[_]]): PagedForgersOutput = { + val nextStartPos = listOfParams.get(0).asInstanceOf[Int32].getValue.intValueExact() + val listOfStaticStruct = listOfParams.get(1).asInstanceOf[DynamicArray[ForgerInfoABI]].getValue + val listOfForgerInfo = listOfStaticStruct.map(ForgerInfo(_)) + + PagedForgersOutput(nextStartPos, listOfForgerInfo.toSeq) + } + +} + +object GetForgerOutputDecoder + extends ABIDecoder[ForgerInfo] + with MsgProcessorInputDecoder[ForgerInfo] + with VRFDecoder{ + + override val getListOfABIParamTypes: util.List[TypeReference[Type[_]]] = + org.web3j.abi.Utils.convert(util.Arrays.asList( + new TypeReference[Bytes32]() {}, + new TypeReference[Bytes32]() {}, + new TypeReference[Bytes1]() {}, + new TypeReference[Uint32]() {}, + new TypeReference[AbiAddress]() {} + )) + + override def createType(listOfParams: util.List[Type[_]]): ForgerInfo = { + val forgerPublicKey = new PublicKey25519Proposition(listOfParams.get(0).asInstanceOf[Bytes32].getValue) + val vrfKey = decodeVrfKey(listOfParams.get(1).asInstanceOf[Bytes32], listOfParams.get(2).asInstanceOf[Bytes1]) + val forgerPublicKeys = ForgerPublicKeys(forgerPublicKey, vrfKey) + val rewardShare = listOfParams.get(3).asInstanceOf[Uint32].getValue.intValueExact() + val rewardAddress = new Address(listOfParams.get(4).asInstanceOf[AbiAddress].toString) + + ForgerInfo(forgerPublicKeys, rewardShare, new AddressProposition(rewardAddress)) + } + +} + + +@JsonView(Array(classOf[Views.Default])) +case class ForgerInfo(forgerPublicKeys: ForgerPublicKeys, + rewardShare:Int, + rewardAddress: AddressProposition) + extends BytesSerializable with ABIEncodable[StaticStruct] { + + require(rewardShare > -1, "rewardShare expected to be non negative.") + + override type M = ForgerInfo + + override def serializer: SparkzSerializer[ForgerInfo] = ForgerInfoSerializer + + override def toString: String = "%s(forgerPublicKeys: %s, rewardShare: %s, rewardAddress: %s)" + .format(this.getClass.toString, forgerPublicKeys, rewardShare, rewardAddress) + + + private[horizen] def asABIType(): StaticStruct = { + val forgerPublicKeysAbi = forgerPublicKeys.asABIType() + val listOfParams: util.List[Type[_]] = new util.ArrayList(forgerPublicKeysAbi.getValue.asInstanceOf[util.List[Type[_]]]) + listOfParams.add(new Uint32(rewardShare)) + listOfParams.add(new AbiAddress(rewardAddress.address().toString)) + new StaticStruct(listOfParams) + } +} + + +object ForgerInfo { + def apply(forgerInfoABI: ForgerInfoABI): ForgerInfo = + ForgerInfo( + ForgerPublicKeys(new PublicKey25519Proposition(forgerInfoABI.pubKey), + new VrfPublicKey(forgerInfoABI.vrf1 ++ forgerInfoABI.vrf2)), + forgerInfoABI.rewardShare, + new AddressProposition(new Address(forgerInfoABI.rewardAddress)) + ) + +} + +object ForgerInfoSerializer extends SparkzSerializer[ForgerInfo] { + override def serialize(s: ForgerInfo, w: Writer): Unit = { + ForgerPublicKeysSerializer.serialize(s.forgerPublicKeys, w) + w.putInt(s.rewardShare) + AddressPropositionSerializer.getSerializer.serialize(s.rewardAddress, w) + } + + override def parse(r: Reader): ForgerInfo = { + val forgerPublicKeys = ForgerPublicKeysSerializer.parse(r) + val rewardShare = r.getInt() + val rewardAddress = AddressPropositionSerializer.getSerializer.parse(r) + ForgerInfo(forgerPublicKeys, rewardShare, rewardAddress) + } +} 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 e65b35f928..dde54aa102 100644 --- a/sdk/src/main/scala/io/horizen/account/state/AccountState.scala +++ b/sdk/src/main/scala/io/horizen/account/state/AccountState.scala @@ -3,14 +3,17 @@ package io.horizen.account.state import com.horizen.certnative.BackwardTransfer import io.horizen.SidechainTypes import io.horizen.account.block.AccountBlock -import io.horizen.account.fork.{GasFeeFork, Version1_2_0Fork} +import io.horizen.account.fork.{GasFeeFork, Version1_2_0Fork, Version1_4_0Fork} import io.horizen.account.history.validation.InvalidTransactionChainIdException +import io.horizen.account.network.ForgerInfo import io.horizen.account.node.NodeAccountState +import io.horizen.account.state.nativescdata.forgerstakev2.{PagedStakesByDelegatorResponse, PagedStakesByForgerResponse} import io.horizen.account.state.receipt.{EthereumConsensusDataLog, EthereumReceipt} import io.horizen.account.storage.AccountStateMetadataStorage import io.horizen.account.transaction.EthereumTransaction import io.horizen.account.utils.Secp256k1.generateContractAddress -import io.horizen.account.utils.{AccountBlockFeeInfo, AccountFeePaymentsUtils, AccountPayment, FeeUtils} +import io.horizen.account.utils.ZenWeiConverter.MAX_MONEY_IN_WEI +import io.horizen.account.utils.{AccountBlockFeeInfo, AccountFeePaymentsUtils, AccountPayment, FeeUtils, ForgerIdentifier} import io.horizen.block.WithdrawalEpochCertificate import io.horizen.certificatesubmitter.keys.{CertifiersKeys, KeyRotationProof} import io.horizen.consensus.{ConsensusEpochInfo, ConsensusEpochNumber, ForgingStakeInfo, intToConsensusEpochNumber} @@ -19,7 +22,6 @@ import io.horizen.evm._ import io.horizen.params.NetworkParams import io.horizen.state.State import io.horizen.utils.{ByteArrayWrapper, BytesUtils, ClosableResourceHandler, MerkleTree, TimeToEpochUtils, WithdrawalEpochInfo, WithdrawalEpochUtils} -import io.horizen.transaction.exception.TransactionSemanticValidityException import sparkz.core._ import sparkz.core.transaction.state.TransactionValidation import sparkz.core.utils.NetworkTimeProvider @@ -155,7 +157,7 @@ class AccountState( ) for ((tx, txIndex) <- mod.sidechainTransactions.zipWithIndex) { - stateView.applyTransaction(tx, txIndex, blockGasPool, blockContext) match { + stateView.applyTransaction(tx, txIndex, blockGasPool, blockContext, stateView) match { case Success(consensusDataReceipt) => val txGasUsed = consensusDataReceipt.cumulativeGasUsed.subtract(cumGasUsed) // update cumulative gas used so far @@ -204,10 +206,27 @@ class AccountState( // - base -> forgers pool, weighted by number of blocks forged // - tip -> block forger // Note: store also entries with zero values, which can arise in sc blocks without any tx - stateView.updateFeePaymentInfo(AccountBlockFeeInfo(cumBaseFee, cumForgerTips, mod.header.forgerAddress)) + if (Version1_4_0Fork.get(consensusEpochNumber).active) { + stateView.updateFeePaymentInfo( + AccountBlockFeeInfo( + cumBaseFee, + cumForgerTips, + mod.header.forgerAddress, + Some(ForgerPublicKeys(mod.header.forgingStakeInfo.blockSignPublicKey, mod.header.forgingStakeInfo.vrfPublicKey)) + )) + + // update block counters for forger pool fee distribution + stateView.updateForgerBlockCounter( + new ForgerIdentifier(mod.forgerPublicKey, + Some(ForgerPublicKeys(mod.header.forgingStakeInfo.blockSignPublicKey, mod.header.forgingStakeInfo.vrfPublicKey))), + consensusEpochNumber + ) + } else { + stateView.updateFeePaymentInfo(AccountBlockFeeInfo(cumBaseFee, cumForgerTips, mod.header.forgerAddress)) - // update block counters for forger pool fee distribution - stateView.updateForgerBlockCounter(mod.forgerPublicKey, consensusEpochNumber) + // update block counters for forger pool fee distribution + stateView.updateForgerBlockCounter(new ForgerIdentifier(mod.forgerPublicKey), consensusEpochNumber) + } // If SC block has reached the end of the withdrawal epoch reward the forgers. evalForgersReward(mod, modWithdrawalEpochInfo, consensusEpochNumber, stateView) @@ -252,7 +271,11 @@ class AccountState( val isWithdrawalEpochFinished: Boolean = WithdrawalEpochUtils.isEpochLastIndex(modWithdrawalEpochInfo, params) if (isWithdrawalEpochFinished) { // current block fee info is already in the view therefore we pass None as third param - val feePayments = stateView.getFeePaymentsInfo(modWithdrawalEpochInfo.epoch, consensusEpochNumber, None) + val distributionCap = if (Version1_4_0Fork.get(consensusEpochNumber).active) { + val mcLastBlockHeight = params.mainchainCreationBlockHeight + ((modWithdrawalEpochInfo.epoch + 1) * params.withdrawalEpochLength) - 1 + AccountFeePaymentsUtils.getMainchainWithdrawalEpochDistributionCap(mcLastBlockHeight, params) + } else MAX_MONEY_IN_WEI + val (feePayments, poolBalanceDistributed) = stateView.getFeePaymentsInfo(modWithdrawalEpochInfo.epoch, consensusEpochNumber, distributionCap, None) log.info(s"End of Withdrawal Epoch ${modWithdrawalEpochInfo.epoch} reached, added ${feePayments.length} rewards with block ${mod.header.id}") @@ -266,7 +289,7 @@ class AccountState( } // reset forger pool balance and block counters - stateView.resetForgerPoolAndBlockCounters(consensusEpochNumber) + stateView.subtractForgerPoolBalanceAndResetBlockCounters(consensusEpochNumber, poolBalanceDistributed) // add rewards to forgers balance feePayments.foreach( @@ -351,7 +374,10 @@ class AccountState( // get a view over state db which is built with the given state root def getStateDbViewFromRoot(stateRoot: Array[Byte]): StateDbAccountStateView = - new StateDbAccountStateView(new StateDB(stateDbStorage, new Hash(stateRoot)), messageProcessors) + new StateDbAccountStateView( + new StateDB(stateDbStorage, new Hash(stateRoot)), + messageProcessors + ) // Base getters override def getWithdrawalRequests(withdrawalEpoch: Int): Seq[WithdrawalRequest] = @@ -386,11 +412,26 @@ class AccountState( override def hasCeased: Boolean = stateMetadataStorage.hasCeased - override def getFeePaymentsInfo(withdrawalEpoch: Int, consensusEpochNumber: ConsensusEpochNumber, blockToAppendFeeInfo: Option[AccountBlockFeeInfo] = None): Seq[AccountPayment] = { + override def getFeePaymentsInfo( + withdrawalEpoch: Int, + consensusEpochNumber: ConsensusEpochNumber, + distributionCap: BigInteger = MAX_MONEY_IN_WEI, + blockToAppendFeeInfo: Option[AccountBlockFeeInfo] = None): (Seq[AccountPayment], BigInteger) = + { val feePaymentInfoSeq = stateMetadataStorage.getFeePayments(withdrawalEpoch) val mcForgerPoolRewards = stateMetadataStorage.getMcForgerPoolRewards + val poolBalanceDistributed = mcForgerPoolRewards.values.foldLeft(BigInteger.ZERO)((a, b) => a.add(b)) + if (Version1_4_0Fork.get(consensusEpochNumber).active) { + val forgerRewards = AccountFeePaymentsUtils.getForgersRewards(feePaymentInfoSeq, mcForgerPoolRewards) + val (feePayments, delegatorPayments) = using(getView)(_.getForgersAndDelegatorsShares(forgerRewards)) + val allPayments = AccountFeePaymentsUtils.groupAllPaymentsByAddress(feePayments, delegatorPayments) - AccountFeePaymentsUtils.getForgersRewards(feePaymentInfoSeq, mcForgerPoolRewards) + (allPayments, poolBalanceDistributed) + } else { + val payments = AccountFeePaymentsUtils.getForgersRewards(feePaymentInfoSeq, mcForgerPoolRewards) + .map(fp => AccountPayment(fp.identifier.getAddress, fp.value)) + (payments, poolBalanceDistributed) + } } override def getWithdrawalEpochInfo: WithdrawalEpochInfo = stateMetadataStorage.getWithdrawalEpochInfo @@ -430,9 +471,17 @@ class AccountState( override def getNonce(address: Address): BigInteger = using(getView)(_.getNonce(address)) - override def getListOfForgersStakes(isForkV1_3Active: Boolean): Seq[AccountForgingStakeInfo] = using(getView)(_.getListOfForgersStakes(isForkV1_3Active)) + override def getListOfForgersStakes(isForkV1_3Active: Boolean, isForkV1_4Active: Boolean): Seq[AccountForgingStakeInfo] = using(getView)(_.getListOfForgersStakes(isForkV1_3Active, isForkV1_4Active)) + + override def getPagedListOfForgersStakes(startPos: Int, pageSize: Int): (Int, Seq[AccountForgingStakeInfo]) = using(getView)(_.getPagedListOfForgersStakes(startPos, pageSize)) + + override def getPagedForgersStakesByForger(forger: ForgerPublicKeys, startPos: Int, pageSize: Int): PagedStakesByForgerResponse = using(getView)(_.getPagedForgersStakesByForger(forger, startPos, pageSize)) + + override def getPagedForgersStakesByDelegator(delegator: Address, startPos: Int, pageSize: Int): PagedStakesByDelegatorResponse = using(getView)(_.getPagedForgersStakesByDelegator(delegator, startPos, pageSize)) - override def getPagedListOfForgersStakes(startPos: Int, pageSize: Int): (Int, Seq[AccountForgingStakeInfo]) = using(getView)(_.getPagedListOfForgersStakes(startPos, pageSize)) + override def getForgerInfo(forger: ForgerPublicKeys): Option[ForgerInfo] = using(getView)(_.getForgerInfo(forger)) + + override def isForgerStakeV1SmartContractDisabled(isForkV1_4Active: Boolean): Boolean = using(getView)(_.isForgerStakeV1SmartContractDisabled(isForkV1_4Active)) override def getAllowedForgerList: Seq[Int] = using(getView)(_.getAllowedForgerList) @@ -538,7 +587,7 @@ class AccountState( None } - + override def forgerStakesV2IsActive: Boolean = using(getView)(_.forgerStakesV2IsActive) } object AccountState extends SparkzLogging { diff --git a/sdk/src/main/scala/io/horizen/account/state/AccountStateReader.scala b/sdk/src/main/scala/io/horizen/account/state/AccountStateReader.scala index 2ed390fc38..86371e1cb7 100644 --- a/sdk/src/main/scala/io/horizen/account/state/AccountStateReader.scala +++ b/sdk/src/main/scala/io/horizen/account/state/AccountStateReader.scala @@ -1,5 +1,7 @@ package io.horizen.account.state +import io.horizen.account.network.ForgerInfo +import io.horizen.account.state.nativescdata.forgerstakev2.{PagedStakesByDelegatorResponse, PagedStakesByForgerResponse} import io.horizen.account.state.receipt.EthereumConsensusDataLog import io.horizen.certificatesubmitter.keys.{CertifiersKeys, KeyRotationProof} import io.horizen.evm.{Address, ResourceHandle} @@ -22,8 +24,15 @@ trait AccountStateReader { def getWithdrawalRequests(withdrawalEpoch: Int): Seq[WithdrawalRequest] - def getListOfForgersStakes(isForkV1_3Active: Boolean): Seq[AccountForgingStakeInfo] + def getListOfForgersStakes(isForkV1_3Active: Boolean, isForkV1_4Active: Boolean): Seq[AccountForgingStakeInfo] def getPagedListOfForgersStakes(startPos: Int, pageSize: Int): (Int, Seq[AccountForgingStakeInfo]) + def getPagedForgersStakesByForger(forger: ForgerPublicKeys, startPos: Int, pageSize: Int): PagedStakesByForgerResponse + def getPagedForgersStakesByDelegator(delegator: Address, startPos: Int, pageSize: Int): PagedStakesByDelegatorResponse + + def getForgerInfo(forger: ForgerPublicKeys): Option[ForgerInfo] + + def isForgerStakeV1SmartContractDisabled(isForkV1_4Active: Boolean): Boolean + def getForgerStakeData(stakeId: String, isForkV1_3Active: Boolean): Option[ForgerStakeData] def isForgingOpen: Boolean def isForgerStakeAvailable(isForkV1_3Active: Boolean): Boolean @@ -39,4 +48,6 @@ trait AccountStateReader { def certifiersKeys(withdrawalEpoch: Int): Option[CertifiersKeys] def keyRotationProof(withdrawalEpoch: Int, indexOfSigner: Int, keyType: Int): Option[KeyRotationProof] + + def forgerStakesV2IsActive: Boolean } 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 b9552b1ad2..a6f9497962 100644 --- a/sdk/src/main/scala/io/horizen/account/state/AccountStateView.scala +++ b/sdk/src/main/scala/io/horizen/account/state/AccountStateView.scala @@ -1,16 +1,16 @@ package io.horizen.account.state import io.horizen.SidechainTypes -import io.horizen.account.fork.Version1_2_0Fork -import io.horizen.account.proposition.AddressProposition +import io.horizen.account.fork.{Version1_2_0Fork, Version1_4_0Fork} import io.horizen.account.state.receipt.EthereumReceipt -import io.horizen.account.storage.AccountStateMetadataStorageView +import io.horizen.account.storage.{AccountStateMetadataStorageView, MsgProcessorMetadataStorageReader} +import io.horizen.account.utils.AccountFeePaymentsUtils.DelegatorFeePayment import io.horizen.account.utils._ import io.horizen.block.{MainchainBlockReferenceData, WithdrawalEpochCertificate} import io.horizen.consensus.ConsensusEpochNumber +import io.horizen.evm.StateDB import io.horizen.state.StateView import io.horizen.utils.WithdrawalEpochInfo -import io.horizen.evm.StateDB import sparkz.core.VersionTag import sparkz.util.{ModifierId, SparkzLogging} @@ -22,12 +22,13 @@ import java.math.BigInteger // - StateDbAccountStateView (concrete class) : evm stateDb read/write // Inherits its methods class AccountStateView( - metadataStorageView: AccountStateMetadataStorageView, - stateDb: StateDB, - messageProcessors: Seq[MessageProcessor] + metadataStorageView: AccountStateMetadataStorageView, + stateDb: StateDB, + messageProcessors: Seq[MessageProcessor], ) extends StateDbAccountStateView(stateDb, messageProcessors) - with StateView[SidechainTypes#SCAT] - with SparkzLogging { + with MsgProcessorMetadataStorageReader + with StateView[SidechainTypes#SCAT] + with SparkzLogging { def addTopQualityCertificates(refData: MainchainBlockReferenceData, blockId: ModifierId): Unit = { refData.topQualityCertificate.foreach(cert => { @@ -86,56 +87,112 @@ class AccountStateView( // after this we always reset the counters override def getFeePaymentsInfo( - withdrawalEpoch: Int, - consensusEpochNumber: ConsensusEpochNumber, - blockToAppendFeeInfo: Option[AccountBlockFeeInfo] = None - ): Seq[AccountPayment] = { + withdrawalEpoch: Int, + consensusEpochNumber: ConsensusEpochNumber, + distributionCap: BigInteger, + blockToAppendFeeInfo: Option[AccountBlockFeeInfo] = None, + ): (Seq[AccountPayment], BigInteger) = { var blockFeeInfoSeq = metadataStorageView.getFeePayments(withdrawalEpoch) blockToAppendFeeInfo.foreach(blockFeeInfo => blockFeeInfoSeq = blockFeeInfoSeq :+ blockFeeInfo) - val mcForgerPoolRewards = getMcForgerPoolRewards(consensusEpochNumber) + val mcForgerPoolRewards = getMcForgerPoolRewards(consensusEpochNumber, distributionCap) + val poolBalanceDistributed = mcForgerPoolRewards.values.foldLeft(BigInteger.ZERO)((a, b) => a.add(b)) metadataStorageView.updateMcForgerPoolRewards(mcForgerPoolRewards) - AccountFeePaymentsUtils.getForgersRewards(blockFeeInfoSeq, mcForgerPoolRewards) + if (Version1_4_0Fork.get(consensusEpochNumber).active) { + val forgerRewards = AccountFeePaymentsUtils.getForgersRewards(blockFeeInfoSeq, mcForgerPoolRewards) + val (forgerPayments, delegatorPayments) = getForgersAndDelegatorsShares(forgerRewards) + metadataStorageView.updateForgerDelegatorPayments(delegatorPayments, consensusEpochNumber) + + val allPayments = AccountFeePaymentsUtils.groupAllPaymentsByAddress(forgerPayments, delegatorPayments) + + (allPayments, poolBalanceDistributed) + } else { + val payments = AccountFeePaymentsUtils.getForgersRewards(blockFeeInfoSeq, mcForgerPoolRewards) + .map(fp => AccountPayment(fp.identifier.getAddress, fp.value)) + (payments, poolBalanceDistributed) + } + } + + private[horizen] def getForgersAndDelegatorsShares(feePayments: Seq[ForgerPayment]): (Seq[AccountPayment], Seq[DelegatorFeePayment]) = { + val allPayments = feePayments.map { feePayment => + Some(feePayment) + // after fork 1.4 blockSignPublicKey and vrfPublicKey are mandatory + .filter(_.identifier.getForgerKeys.isDefined) + // try get ForgerInfo from StakeStorageV2 + .flatMap(fp => getForgerInfo(fp.identifier.getForgerKeys.get)) + // split reward into forger and delegator shares + .map(info => AccountFeePaymentsUtils.getForgerAndDelegatorShares(feePayment, info)) + // for blocks <1.4 fork all reward goes to forger + .getOrElse { + val totalFeeReward = feePayment.value.subtract(feePayment.valueFromMainchain) + val forgerPayment = AccountPayment(feePayment.identifier.getAddress, feePayment.value, Some(feePayment.valueFromMainchain), Some(totalFeeReward)) + (forgerPayment, None) + } + } + // this is to collapse delegator payments into a flat list. Also null payments are filtered out. + (allPayments.withFilter(_._1.value.signum()==1).map(_._1), allPayments.flatMap(_._2)) } override def getAccountStateRoot: Array[Byte] = metadataStorageView.getAccountStateRoot - def getMcForgerPoolRewards(consensusEpochNumber: ConsensusEpochNumber): Map[AddressProposition, BigInteger] = { + override def getForgerRewards( + forgerPublicKeys: ForgerPublicKeys, + consensusEpochStart: Int, + maxNumOfEpochs: Int, + ): Seq[BigInteger] = { + metadataStorageView.getForgerRewards(forgerPublicKeys, consensusEpochStart, maxNumOfEpochs) + } + + def getMcForgerPoolRewards( + consensusEpochNumber: ConsensusEpochNumber, + distributionCap: BigInteger, + ): Map[ForgerIdentifier, BigInteger] = { if (Version1_2_0Fork.get(consensusEpochNumber).active) { val extraForgerReward = getBalance(WellKnownAddresses.FORGER_POOL_RECIPIENT_ADDRESS) if (extraForgerReward.signum() == 1) { - val counters: Map[AddressProposition, Long] = getForgerBlockCounters - val perBlockFee_remainder = extraForgerReward.divideAndRemainder(BigInteger.valueOf(counters.values.sum)) - val perBlockFee = perBlockFee_remainder(0) - var remainder = perBlockFee_remainder(1) - //sort and add remainder based by block count - val forgerPoolRewards = counters.toSeq.sortBy(_._2) - .map { address_blocks => - val blocks = BigInteger.valueOf(address_blocks._2) - val usedRemainder = remainder.min(blocks) - val reward = perBlockFee.multiply(blocks).add(usedRemainder) - remainder = remainder.subtract(usedRemainder) - (address_blocks._1, reward) - } - forgerPoolRewards.toMap + val availableReward = extraForgerReward.min(distributionCap) + val counters: Map[ForgerIdentifier, Long] = getForgerBlockCounters + val perBlockFee_remainder = availableReward.divideAndRemainder(BigInteger.valueOf(counters.values.sum)) + val perBlockFee = perBlockFee_remainder(0) + var remainder = perBlockFee_remainder(1) + //sort and add remainder based by block count + val forgerPoolRewards = counters.toSeq + .sortBy(_._2) + .map { address_blocks => + val blocks = BigInteger.valueOf(address_blocks._2) + val usedRemainder = remainder.min(blocks) + val reward = perBlockFee.multiply(blocks).add(usedRemainder) + remainder = remainder.subtract(usedRemainder) + (address_blocks._1, reward) + } + forgerPoolRewards.toMap } else Map.empty } else Map.empty } - def updateForgerBlockCounter(forgerPublicKey: AddressProposition, consensusEpochNumber: ConsensusEpochNumber): Unit = { + def updateForgerBlockCounter(forgerKey: ForgerIdentifier, consensusEpochNumber: ConsensusEpochNumber): Unit = { if (Version1_2_0Fork.get(consensusEpochNumber).active) { - metadataStorageView.updateForgerBlockCounter(forgerPublicKey) + metadataStorageView.updateForgerBlockCounter(forgerKey) } } - def getForgerBlockCounters: Map[AddressProposition, Long] = { + def getForgerBlockCounters: Map[ForgerIdentifier, Long] = { metadataStorageView.getForgerBlockCounters } - def resetForgerPoolAndBlockCounters(consensusEpochNumber: ConsensusEpochNumber): Unit = { + def subtractForgerPoolBalanceAndResetBlockCounters( + consensusEpochNumber: ConsensusEpochNumber, + poolBalanceDistributed: BigInteger, + ): Unit = { if (Version1_2_0Fork.get(consensusEpochNumber).active) { val forgerPoolBalance = getBalance(WellKnownAddresses.FORGER_POOL_RECIPIENT_ADDRESS) + if (poolBalanceDistributed.compareTo(forgerPoolBalance) > 0) { + val errMsg = + s"Trying to subtract more($poolBalanceDistributed) from the forger pool balance than available($forgerPoolBalance)" + log.error(errMsg) + throw new IllegalArgumentException(errMsg) + } if (forgerPoolBalance.signum() == 1) { - subBalance(WellKnownAddresses.FORGER_POOL_RECIPIENT_ADDRESS, forgerPoolBalance) + subBalance(WellKnownAddresses.FORGER_POOL_RECIPIENT_ADDRESS, poolBalanceDistributed) metadataStorageView.resetForgerBlockCounters() } } 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 cb25c72d18..8597b1b2cf 100644 --- a/sdk/src/main/scala/io/horizen/account/state/CertificateKeyRotationMsgProcessor.scala +++ b/sdk/src/main/scala/io/horizen/account/state/CertificateKeyRotationMsgProcessor.scala @@ -5,6 +5,7 @@ import io.horizen.account.abi.ABIUtil.{METHOD_ID_LENGTH, getABIMethodId, getArgu import io.horizen.account.abi.{ABIDecoder, ABIEncodable, MsgProcessorInputDecoder} import io.horizen.account.state.CertificateKeyRotationMsgProcessor.{CertificateKeyRotationContractAddress, CertificateKeyRotationContractCode, SubmitKeyRotationReqCmdSig} import io.horizen.account.state.events.SubmitKeyRotation +import io.horizen.account.storage.MsgProcessorMetadataStorageReader import io.horizen.account.utils.WellKnownAddresses.CERTIFICATE_KEY_ROTATION_SMART_CONTRACT_ADDRESS import io.horizen.certificatesubmitter.keys.KeyRotationProofTypes.{KeyRotationProofType, MasterKeyRotationProofType, SigningKeyRotationProofType} import io.horizen.certificatesubmitter.keys.{CertifiersKeys, KeyRotationProof, KeyRotationProofSerializer, KeyRotationProofTypes} @@ -37,7 +38,7 @@ case class CertificateKeyRotationMsgProcessor(params: NetworkParams) extends Nat override val contractCode: Array[Byte] = CertificateKeyRotationContractCode @throws(classOf[ExecutionFailedException]) - override def process(invocation: Invocation, view: BaseAccountStateView, context: ExecutionContext): Array[Byte] = { + override def process(invocation: Invocation, view: BaseAccountStateView, metadata: MsgProcessorMetadataStorageReader, context: ExecutionContext): Array[Byte] = { val gasView = view.getGasTrackedView(invocation.gasPool) getFunctionSignature(invocation.input) match { case SubmitKeyRotationReqCmdSig => 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 f5b4cf45db..7f815ca7aa 100644 --- a/sdk/src/main/scala/io/horizen/account/state/EoaMessageProcessor.scala +++ b/sdk/src/main/scala/io/horizen/account/state/EoaMessageProcessor.scala @@ -1,5 +1,6 @@ package io.horizen.account.state +import io.horizen.account.storage.MsgProcessorMetadataStorageReader import sparkz.util.SparkzLogging /* @@ -22,6 +23,7 @@ object EoaMessageProcessor extends MessageProcessor with SparkzLogging { override def process( invocation: Invocation, view: BaseAccountStateView, + metadata: MsgProcessorMetadataStorageReader, context: ExecutionContext ): Array[Byte] = { view.subBalance(invocation.caller, invocation.value) diff --git a/sdk/src/main/scala/io/horizen/account/state/ForgerBlockCountersSerializer.scala b/sdk/src/main/scala/io/horizen/account/state/ForgerBlockCountersSerializer.scala index d1d5a4893e..d4b4bbb905 100644 --- a/sdk/src/main/scala/io/horizen/account/state/ForgerBlockCountersSerializer.scala +++ b/sdk/src/main/scala/io/horizen/account/state/ForgerBlockCountersSerializer.scala @@ -1,27 +1,48 @@ package io.horizen.account.state -import io.horizen.account.proposition.{AddressProposition, AddressPropositionSerializer} +import io.horizen.account.proposition.AddressPropositionSerializer +import io.horizen.account.utils.ForgerIdentifier +import io.horizen.proposition.{PublicKey25519PropositionSerializer, VrfPublicKeySerializer} import sparkz.core.serialization.SparkzSerializer import sparkz.util.serialization.{Reader, Writer} -object ForgerBlockCountersSerializer extends SparkzSerializer[Map[AddressProposition, Long]] { +object ForgerBlockCountersSerializer extends SparkzSerializer[Map[ForgerIdentifier, Long]] { private val addressSerializer: AddressPropositionSerializer = AddressPropositionSerializer.getSerializer + private val signPubKeySerializer: PublicKey25519PropositionSerializer = + PublicKey25519PropositionSerializer.getSerializer + private val vrfPubKeySerializer: VrfPublicKeySerializer = VrfPublicKeySerializer.getSerializer + final val SERIALIZATION_FORMAT_1_4_FLAG = -1 - override def serialize(forgerBlockCounters: Map[AddressProposition, Long], w: Writer): Unit = { + override def serialize(forgerBlockCounters: Map[ForgerIdentifier, Long], w: Writer): Unit = { w.putInt(forgerBlockCounters.size) - forgerBlockCounters.foreach { case (address, counter) => - addressSerializer.serialize(address, w) - w.putLong(counter) + forgerBlockCounters.foreach { + case (forgerIdentifier, counter) => + addressSerializer.serialize(forgerIdentifier.getAddress, w) + if (forgerIdentifier.getForgerKeys.isDefined) { + w.putLong(SERIALIZATION_FORMAT_1_4_FLAG) //flag to indicate new serialization format is used + forgerIdentifier.getForgerKeys.foreach { keys => + signPubKeySerializer.serialize(keys.blockSignPublicKey, w) + vrfPubKeySerializer.serialize(keys.vrfPublicKey, w) + } + } + w.putLong(counter) } } - override def parse(r: Reader): Map[AddressProposition, Long] = { + override def parse(r: Reader): Map[ForgerIdentifier, Long] = { val length = r.getInt() (1 to length).map { _ => val address = addressSerializer.parse(r) val counter = r.getLong() - (address, counter) + if (counter == SERIALIZATION_FORMAT_1_4_FLAG) { + val blockSignPublicKey = signPubKeySerializer.parse(r) + val vrfPublicKey = vrfPubKeySerializer.parse(r) + val counter = r.getLong() + (new ForgerIdentifier(address, Some(ForgerPublicKeys(blockSignPublicKey, vrfPublicKey))), counter) + } else { + (new ForgerIdentifier(address), counter) + } }.toMap } 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 93946f7b86..cfe733d0c8 100644 --- a/sdk/src/main/scala/io/horizen/account/state/ForgerStakeMsgProcessor.scala +++ b/sdk/src/main/scala/io/horizen/account/state/ForgerStakeMsgProcessor.scala @@ -2,7 +2,7 @@ package io.horizen.account.state import com.google.common.primitives.{Bytes, Ints} import io.horizen.account.abi.ABIUtil.{METHOD_ID_LENGTH, getABIMethodId, getArgumentsFromData, getFunctionSignature} -import io.horizen.account.fork.{Version1_2_0Fork, Version1_3_0Fork} +import io.horizen.account.fork.{Version1_2_0Fork, Version1_3_0Fork, Version1_4_0Fork} import io.horizen.account.proof.SignatureSecp256k1 import io.horizen.account.proposition.AddressProposition import io.horizen.account.state.ForgerStakeLinkedList.{getStakeListItem, linkedListNodeRefIsNull, removeNode} @@ -11,7 +11,9 @@ import io.horizen.account.state.ForgerStakeStorage.getStorageVersionFromDb import io.horizen.account.state.ForgerStakeStorageV1.LinkedListTipKey import io.horizen.account.state.ForgerStakeStorageVersion.ForgerStakeStorageVersion import io.horizen.account.state.NativeSmartContractMsgProcessor.NULL_HEX_STRING_32 -import io.horizen.account.state.events.{DelegateForgerStake, OpenForgerList, StakeUpgrade, WithdrawForgerStake} +import io.horizen.account.state.events._ +import io.horizen.account.storage.MsgProcessorMetadataStorageReader +import io.horizen.account.utils.WellKnownAddresses import io.horizen.account.utils.WellKnownAddresses.FORGER_STAKE_SMART_CONTRACT_ADDRESS import io.horizen.account.utils.ZenWeiConverter.isValidZenAmount import io.horizen.evm.Address @@ -40,6 +42,8 @@ trait ForgerStakesProvider { private[horizen] def isForgerStakeAvailable(view: BaseAccountStateView, isForkV1_3Active: Boolean): Boolean private[horizen] def getAllowedForgerListIndexes(view: BaseAccountStateView): Seq[Int] + + private[horizen] def isForgerStakeV1SmartContractDisabled(view: BaseAccountStateView, isForkV1_4Active: Boolean): Boolean } case class ForgerStakeMsgProcessor(params: NetworkParams) extends NativeSmartContractMsgProcessor with ForgerStakesProvider { @@ -85,11 +89,11 @@ case class ForgerStakeMsgProcessor(params: NetworkParams) extends NativeSmartCon stakeStorage.findStakeData(view, stakeId) } - override private[horizen] def isForgerStakeAvailable(view: BaseAccountStateView, isForkV1_3Active: Boolean): Boolean = { + override private[horizen] def isForgerStakeAvailable(view: BaseAccountStateView, isForkV1_3Active: Boolean): Boolean = { if (!isForkV1_3Active){ true }else{ - val stakeStorage: ForgerStakeStorage = getForgerStakeStorage(view, true) + val stakeStorage: ForgerStakeStorage = getForgerStakeStorage(view, isForkV1_3Active = true) stakeStorage.isForgerStakeAvailable(view) } } @@ -445,29 +449,61 @@ case class ForgerStakeMsgProcessor(params: NetworkParams) extends NativeSmartCon restrictForgerList } - @throws(classOf[ExecutionFailedException]) - override def process(invocation: Invocation, view: BaseAccountStateView, context: ExecutionContext): Array[Byte] = { - val gasView = view.getGasTrackedView(invocation.gasPool) - getFunctionSignature(invocation.input) match { - case GetPagedListOfForgersCmd if Version1_3_0Fork.get(context.blockContext.consensusEpochNumber).active - => doGetPagedListOfForgersCmd(invocation, gasView) - case GetListOfForgersCmd => doGetListOfForgersCmd(invocation, gasView, Version1_3_0Fork.get(context.blockContext.consensusEpochNumber).active) - case AddNewStakeCmd => doAddNewStakeCmd(invocation, gasView, context.msg, - Version1_3_0Fork.get(context.blockContext.consensusEpochNumber).active) - case RemoveStakeCmd => doRemoveStakeCmd(invocation, gasView, context.msg, Version1_3_0Fork.get(context.blockContext.consensusEpochNumber).active) - case OpenStakeForgerListCmd => doOpenStakeForgerListCmd(invocation, gasView, context.msg) - case OpenStakeForgerListCmdCorrect if Version1_2_0Fork.get(context.blockContext.consensusEpochNumber).active - => doOpenStakeForgerListCmd(invocation, gasView, context.msg) - case UpgradeCmd if Version1_3_0Fork.get(context.blockContext.consensusEpochNumber).active - => doUpgradeCmd(invocation, view)// This doesn't consume gas, so it doesn't use GasTrackedView - case StakeOfCmd if Version1_3_0Fork.get(context.blockContext.consensusEpochNumber).active - => doStakeOfCmd(invocation, gasView) - case GetPagedForgersStakesOfUserCmd if Version1_3_0Fork.get(context.blockContext.consensusEpochNumber).active - => doGetPagedForgersStakesOfUserCmd(invocation, gasView) - case opCodeHex => throw new ExecutionRevertedException(s"op code not supported: $opCodeHex") - } + def doDisableAndMigrate(invocation: Invocation, view: BaseAccountStateView): Array[Byte] = { + requireIsNotPayable(invocation) + checkInputDoesntContainParams(invocation.input) + if (WellKnownAddresses.FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS != invocation.caller || + !view.getCodeHash(invocation.caller).sameElements(ForgerStakeV2MsgProcessor.contractCodeHash)) + throw new ExecutionRevertedException("Authorization failed") + + ForgerStakeStorage.setDisabled(view) + + val evmLog = getEthereumConsensusDataLog(new DisableStakeV1) + view.addLog(evmLog) + + doUncheckedGetListOfForgersStakesCmd(view, isForkV1_3Active = true) } + override def isForgerStakeV1SmartContractDisabled(view: BaseAccountStateView, isForkV1_4Active: Boolean): Boolean = { + isForkV1_4Active && ForgerStakeStorage.isDisabled(view) + } + + @throws(classOf[ExecutionFailedException]) + override def process(invocation: Invocation, view: BaseAccountStateView, metadata: MsgProcessorMetadataStorageReader, context: ExecutionContext): Array[Byte] = { + val gasView = view.getGasTrackedView(invocation.gasPool) + if (Version1_4_0Fork.get(context.blockContext.consensusEpochNumber).active && ForgerStakeStorage.isDisabled(gasView)) { + getFunctionSignature(invocation.input) match { + case OpenStakeForgerListCmd => doOpenStakeForgerListCmd(invocation, gasView, context.msg) + case OpenStakeForgerListCmdCorrect => doOpenStakeForgerListCmd(invocation, gasView, context.msg) + case GetPagedListOfForgersCmd | GetListOfForgersCmd | AddNewStakeCmd | RemoveStakeCmd | + UpgradeCmd | StakeOfCmd | GetPagedForgersStakesOfUserCmd | DisableAndMigrateCmd => throw new ExecutionRevertedException(s"Method is disabled - Please use the new ForgeStakeV2") + case opCodeHex => throw new ExecutionRevertedException(s"op code not supported: $opCodeHex") + } + } + else { + getFunctionSignature(invocation.input) match { + case GetPagedListOfForgersCmd if Version1_3_0Fork.get(context.blockContext.consensusEpochNumber).active + => doGetPagedListOfForgersCmd(invocation, gasView) + case GetListOfForgersCmd => doGetListOfForgersCmd(invocation, gasView, Version1_3_0Fork.get(context.blockContext.consensusEpochNumber).active) + case AddNewStakeCmd => doAddNewStakeCmd(invocation, gasView, context.msg, + Version1_3_0Fork.get(context.blockContext.consensusEpochNumber).active) + case RemoveStakeCmd => doRemoveStakeCmd(invocation, gasView, context.msg, Version1_3_0Fork.get(context.blockContext.consensusEpochNumber).active) + case OpenStakeForgerListCmd => doOpenStakeForgerListCmd(invocation, gasView, context.msg) + case OpenStakeForgerListCmdCorrect if Version1_2_0Fork.get(context.blockContext.consensusEpochNumber).active + => doOpenStakeForgerListCmd(invocation, gasView, context.msg) + case UpgradeCmd if Version1_3_0Fork.get(context.blockContext.consensusEpochNumber).active + => doUpgradeCmd(invocation, view) // This doesn't consume gas, so it doesn't use GasTrackedView + case StakeOfCmd if Version1_3_0Fork.get(context.blockContext.consensusEpochNumber).active + => doStakeOfCmd(invocation, gasView) + case GetPagedForgersStakesOfUserCmd if Version1_3_0Fork.get(context.blockContext.consensusEpochNumber).active + => doGetPagedForgersStakesOfUserCmd(invocation, gasView) + case DisableAndMigrateCmd if Version1_4_0Fork.get(context.blockContext.consensusEpochNumber).active + => doDisableAndMigrate(invocation, view) // This doesn't consume gas, so it doesn't use GasTrackedView + case opCodeHex => throw new ExecutionRevertedException(s"op code not supported: $opCodeHex") + } + } + } + override private[horizen] def isForgerListOpen(view: BaseAccountStateView): Boolean = { if (params.restrictForgers) { @@ -507,6 +543,8 @@ object ForgerStakeMsgProcessor { val UpgradeCmd: String = getABIMethodId("upgrade()") val StakeOfCmd: String = getABIMethodId("stakeOf(address)") val GetPagedForgersStakesOfUserCmd: String = getABIMethodId("getPagedForgersStakesByUser(address,int32,int32)") + // Methods added after Fork v. 1.4 + val DisableAndMigrateCmd: String = getABIMethodId("disableAndMigrate()") // ensure we have strings consistent with size of opcode require( @@ -518,7 +556,8 @@ object ForgerStakeMsgProcessor { OpenStakeForgerListCmdCorrect.length == 2 * METHOD_ID_LENGTH && UpgradeCmd.length == 2 * METHOD_ID_LENGTH && StakeOfCmd.length == 2 * METHOD_ID_LENGTH && - GetPagedForgersStakesOfUserCmd.length == 2 * METHOD_ID_LENGTH + GetPagedForgersStakesOfUserCmd.length == 2 * METHOD_ID_LENGTH && + DisableAndMigrateCmd.length == 2 * METHOD_ID_LENGTH ) def getRemoveStakeCmdMessageToSign(stakeId: Array[Byte], from: Address, nonce: Array[Byte]): Array[Byte] = { diff --git a/sdk/src/main/scala/io/horizen/account/state/ForgerStakeMsgProcessorData.scala b/sdk/src/main/scala/io/horizen/account/state/ForgerStakeMsgProcessorData.scala index 19994fdb62..66c988d76d 100644 --- a/sdk/src/main/scala/io/horizen/account/state/ForgerStakeMsgProcessorData.scala +++ b/sdk/src/main/scala/io/horizen/account/state/ForgerStakeMsgProcessorData.scala @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonView import io.horizen.account.abi.{ABIDecoder, ABIEncodable, ABIListEncoder, MsgProcessorInputDecoder} import io.horizen.account.proof.SignatureSecp256k1 import io.horizen.account.proposition.{AddressProposition, AddressPropositionSerializer} +import io.horizen.account.state.nativescdata.forgerstakev2.VRFDecoder import io.horizen.account.utils.BigIntegerUInt256.getUnsignedByteArray import io.horizen.account.utils.{BigIntegerUInt256, Secp256k1} import io.horizen.evm.Address @@ -21,6 +22,7 @@ import sparkz.util.serialization.{Reader, Writer} import java.math.BigInteger import java.util import scala.collection.JavaConverters +import scala.jdk.CollectionConverters.collectionAsScalaIterableConverter @JsonView(Array(classOf[Views.Default])) // used as element of the list to return when getting all forger stakes via msg processor @@ -33,15 +35,21 @@ case class AccountForgingStakeInfo( override def serializer: SparkzSerializer[AccountForgingStakeInfo] = AccountForgingStakeInfoSerializer - override def toString: String = "%s(stakeId: %s, forgerStakeData: %s)" - .format(this.getClass.toString, BytesUtils.toHexString(stakeId), forgerStakeData) + override def toString: String = { + val stakeIdStr = if (stakeId != null) BytesUtils.toHexString(stakeId) else "null" + "%s(stakeId: %s, forgerStakeData: %s)" + .format(this.getClass.toString, stakeIdStr, forgerStakeData) + } private[horizen] def asABIType(): StaticStruct = { val forgerPublicKeysParams = forgerStakeData.forgerPublicKeys.asABIType().getValue.asInstanceOf[util.Collection[_ <: Type[_]]] val listOfParams = new util.ArrayList[Type[_]]() - listOfParams.add(new Bytes32(stakeId)) + if (stakeId != null) + listOfParams.add(new Bytes32(stakeId)) + else + listOfParams.add(new Bytes32(new Array[Byte](32))) listOfParams.add(new Uint256(forgerStakeData.stakedAmount)) listOfParams.add(new AbiAddress(forgerStakeData.ownerPublicKey.address().toString)) @@ -74,6 +82,33 @@ object AccountForgingStakeInfoListEncoder extends ABIListEncoder[AccountForgingS override def getAbiClass: Class[StaticStruct] = classOf[StaticStruct] } +case class AccountForgingStakeInfoList(listOfStakes: Seq[AccountForgingStakeInfo]){ + override def toString: String = "%s(listOfStakes: %s)".format(this.getClass.toString, listOfStakes) +} + + +object AccountForgingStakeInfoListDecoder extends ABIDecoder[AccountForgingStakeInfoList] + with MsgProcessorInputDecoder[AccountForgingStakeInfoList] { + + override val getListOfABIParamTypes: util.List[TypeReference[Type[_]]] = + org.web3j.abi.Utils.convert(util.Arrays.asList( + new TypeReference[DynamicArray[AccountForgingStakeInfoABI]]() {} + )) + + override def createType(listOfParams: util.List[Type[_]]): AccountForgingStakeInfoList = { + val listOfStaticStruct = listOfParams.get(0).asInstanceOf[DynamicArray[AccountForgingStakeInfoABI]].getValue.asScala + val list = listOfStaticStruct.map(x => AccountForgingStakeInfo(x.stakeId, + ForgerStakeData( + ForgerPublicKeys(new PublicKey25519Proposition(x.pubKey), + new VrfPublicKey(x.vrf1 ++ x.vrf2)), + new AddressProposition(new Address(x.owner)), + x.amount))) + AccountForgingStakeInfoList(list.toSeq) + } +} + + + object AccountForgingStakeInfoSerializer extends SparkzSerializer[AccountForgingStakeInfo] { override def serialize(s: AccountForgingStakeInfo, w: Writer): Unit = { @@ -93,15 +128,10 @@ object AccountForgingStakeInfoSerializer extends SparkzSerializer[AccountForging case class ForgerPublicKeys( blockSignPublicKey: PublicKey25519Proposition, vrfPublicKey: VrfPublicKey) - extends BytesSerializable with ABIEncodable[StaticStruct] { + extends BytesSerializable with ABIEncodable[StaticStruct] + with VRFDecoder{ override type M = ForgerPublicKeys - private[horizen] def vrfPublicKeyToAbi(vrfPublicKey: Array[Byte]): (Bytes32, Bytes1) = { - val vrfPublicKeyFirst32Bytes = new Bytes32(util.Arrays.copyOfRange(vrfPublicKey, 0, 32)) - val vrfPublicKeyLastByte = new Bytes1(Array[Byte](vrfPublicKey(32))) - (vrfPublicKeyFirst32Bytes, vrfPublicKeyLastByte) - } - override def asABIType(): StaticStruct = { val vrfPublicKeyBytes = vrfPublicKeyToAbi(vrfPublicKey.pubKeyBytes()) @@ -113,10 +143,14 @@ case class ForgerPublicKeys( ) } + override def toString: String = "%s(blockSignPublicKey: %s, vrfPublicKey: %s)" + .format(this.getClass.toString, blockSignPublicKey, vrfPublicKey) + override def serializer: SparkzSerializer[ForgerPublicKeys] = ForgerPublicKeysSerializer } + object ForgerPublicKeysSerializer extends SparkzSerializer[ForgerPublicKeys] { override def serialize(s: ForgerPublicKeys, w: Writer): Unit = { diff --git a/sdk/src/main/scala/io/horizen/account/state/ForgerStakeStorage.scala b/sdk/src/main/scala/io/horizen/account/state/ForgerStakeStorage.scala index e04cffe5cf..69c193ea7e 100644 --- a/sdk/src/main/scala/io/horizen/account/state/ForgerStakeStorage.scala +++ b/sdk/src/main/scala/io/horizen/account/state/ForgerStakeStorage.scala @@ -348,6 +348,7 @@ object ForgerStakeStorageV2 extends ForgerStakeStorage { object ForgerStakeStorage { val ForgerStakeVersionKey: Array[Byte] = Blake2b256.hash("ForgerStakeVersion") + val DisabledKey: Array[Byte] = Blake2b256.hash("Disabled") def apply(storageVersion: ForgerStakeStorageVersion): ForgerStakeStorage = { storageVersion match { @@ -371,6 +372,19 @@ object ForgerStakeStorage { view.updateAccountStorage(FORGER_STAKE_SMART_CONTRACT_ADDRESS, ForgerStakeVersionKey, ver) ver } + + def isDisabled(view: BaseAccountStateView): Boolean = { + val disabled = view.getAccountStorage(FORGER_STAKE_SMART_CONTRACT_ADDRESS, DisabledKey) + new BigInteger(1, disabled) == BigInteger.ONE + } + + def setDisabled(view: BaseAccountStateView): Unit = { + val disabled = BigIntegerUtil.toUint256Bytes(BigInteger.ONE) + view.updateAccountStorage(FORGER_STAKE_SMART_CONTRACT_ADDRESS, DisabledKey, disabled) + } + + + } diff --git a/sdk/src/main/scala/io/horizen/account/state/ForgerStakeV2MsgProcessor.scala b/sdk/src/main/scala/io/horizen/account/state/ForgerStakeV2MsgProcessor.scala new file mode 100644 index 0000000000..a4bcc5c761 --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/ForgerStakeV2MsgProcessor.scala @@ -0,0 +1,626 @@ +package io.horizen.account.state + +import io.horizen.account.abi.ABIUtil.{METHOD_ID_LENGTH, getABIMethodId, getArgumentsFromData, getFunctionSignature} +import io.horizen.account.fork.Version1_4_0Fork +import io.horizen.account.network.{ForgerInfo, PagedForgersOutput} +import io.horizen.account.state.nativescdata.forgerstakev2.StakeStorage.{addForger, getForger, updateForger} +import io.horizen.account.state.nativescdata.forgerstakev2._ +import io.horizen.account.state.nativescdata.forgerstakev2.events._ +import io.horizen.account.storage.MsgProcessorMetadataStorageReader +import io.horizen.account.utils.WellKnownAddresses.{FORGER_STAKE_SMART_CONTRACT_ADDRESS, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS} +import io.horizen.account.utils.ZenWeiConverter.{convertZenniesToWei, isValidZenAmount} +import io.horizen.consensus.{ForgingStakeInfo, minForgerStake} +import io.horizen.evm.Address +import io.horizen.proof.{Signature25519, VrfProof} +import io.horizen.proposition.{PublicKey25519Proposition, VrfPublicKey} +import io.horizen.utils.BytesUtils +import org.web3j.crypto.Keys +import org.web3j.utils.Numeric.cleanHexPrefix +import sparkz.crypto.hash.Keccak256 + +import java.math.BigInteger +import java.nio.charset.StandardCharsets +import scala.util.{Failure, Success, Try} + +trait ForgerStakesV2Provider { + private[horizen] def getPagedForgersStakesByForger(view: BaseAccountStateView, forger: ForgerPublicKeys, startPos: Int, pageSize: Int): PagedStakesByForgerResponse + private[horizen] def getPagedForgersStakesByDelegator(view: BaseAccountStateView, delegator: Address, startPos: Int, pageSize: Int): PagedStakesByDelegatorResponse + private[horizen] def getPagedListOfForgersStakes(view: BaseAccountStateView, startPos: Int, pageSize: Int): PagedForgersListResponse + private[horizen] def getListOfForgersStakes(view: BaseAccountStateView): Seq[ForgerStakeData] + private[horizen] def getForgingStakes(view: BaseAccountStateView): Seq[ForgingStakeInfo] + private[horizen] def getForgerInfo(view: BaseAccountStateView, forger: ForgerPublicKeys): Option[ForgerInfo] + private[horizen] def isActive(view: BaseAccountStateView): Boolean +} + + +object ForgerStakeV2MsgProcessor extends NativeSmartContractWithFork with ForgerStakesV2Provider { + + val MAX_REWARD_SHARE = 1000 + val MIN_REGISTER_FORGER_STAKED_AMOUNT_IN_WEI: BigInteger = convertZenniesToWei(minForgerStake) // 10 Zen + val NUM_OF_EPOCHS_AFTER_FORK_ACTIVATION_FOR_UPDATE_FORGER: Int = 2 + + override val contractAddress: Address = FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS + override val contractCode: Array[Byte] = Keccak256.hash("ForgerStakeV2SmartContractCode") + + override def isForkActive(consensusEpochNumber: Int): Boolean = { + Version1_4_0Fork.get(consensusEpochNumber).active + } + + override def process(invocation: Invocation, view: BaseAccountStateView, metadata: MsgProcessorMetadataStorageReader, context: ExecutionContext): Array[Byte] = { + if (!isForkActive(context.blockContext.consensusEpochNumber)) + throw new ExecutionRevertedException(s"fork not active") + val gasView = view.getGasTrackedView(invocation.gasPool) + getFunctionSignature(invocation.input) match { + case RegisterForgerCmd => + doRegisterForger(invocation, gasView, context) + case UpdateForgerCmd => + doUpdateForger(invocation, gasView, context) + case DelegateCmd => + doDelegateCmd(invocation, gasView, context) + case WithdrawCmd => + doWithdrawCmd(invocation, gasView, context) + case StakeTotalCmd => + doStakeTotalCmd(invocation, gasView, context.blockContext.consensusEpochNumber) + case StakeStartCmd => + doStakeStartCmd(invocation, gasView) + case RewardsReceivedCmd => + doRewardsReceivedCmd(invocation, gasView, metadata, context.blockContext.consensusEpochNumber) + case GetPagedForgersStakesByForgerCmd => + doPagedForgersStakesByForgerCmd(invocation, gasView) + case GetPagedForgersStakesByDelegatorCmd => + doPagedForgersStakesByDelegatorCmd(invocation, gasView) + case GetCurrentConsensusEpochCmd => + doGetCurrentConsensusEpochCmd(invocation, gasView, context) + case ActivateCmd => + doActivateCmd(invocation, view, context) // That shouldn't consume gas, so it doesn't use gasView + case GetPagedForgersCmd => + doGetPagedForgersCmd(invocation, gasView) + case GetForgerCmd => + doGetForgerCmd(invocation, gasView) + case opCodeHex => throw new ExecutionRevertedException(s"op code not supported: $opCodeHex") + } + } + + + def verifySignatures(msgToSign: Array[Byte], blockSignPubKey: PublicKey25519Proposition, vrfPubKey: VrfPublicKey, sign25519: Signature25519, signVrf: VrfProof): Unit = { + if (!sign25519.isValid(blockSignPubKey, msgToSign)) { + val errMsg = s"Invalid signature, could not validate against blockSignerProposition=$blockSignPubKey (sign=$sign25519)" + log.debug(errMsg) + throw new ExecutionRevertedException(errMsg) + } + + if (!signVrf.isValid(vrfPubKey, msgToSign)) { + val errMsg = s"Invalid signature, could not validate against vrfKey=$vrfPubKey (sign=$signVrf)" + log.debug(errMsg) + throw new ExecutionRevertedException(errMsg) + } + } + + def doRegisterForger(invocation: Invocation, gasView: BaseAccountStateView, context: ExecutionContext): Array[Byte] = { + + checkForgerStakesV2IsActive(gasView) + + val stakedAmount = invocation.value + + // check that msg.value is a legal wei amount convertible to satoshis without any remainder and that + // it is over the minimum threshold + if (!isValidZenAmount(stakedAmount)) { + val errMsg = s"Value is not a legal wei amount: ${stakedAmount.toString()}, maximum 10 decimals accepted" + log.debug(errMsg) + throw new ExecutionRevertedException(errMsg) + } + if (stakedAmount.compareTo(MIN_REGISTER_FORGER_STAKED_AMOUNT_IN_WEI) < 0) { + val errMsg = s"Value ${stakedAmount.toString()} is below the minimum stake amount threshold: $MIN_REGISTER_FORGER_STAKED_AMOUNT_IN_WEI " + log.debug(errMsg) + throw new ExecutionRevertedException(errMsg) + } + + val inputParams = getArgumentsFromData(invocation.input) + + val cmdInput = RegisterOrUpdateForgerCmdInputDecoder.decode(inputParams) + + val blockSignPubKey = cmdInput.forgerPublicKeys.blockSignPublicKey + val vrfPubKey = cmdInput.forgerPublicKeys.vrfPublicKey + val rewardShare = cmdInput.rewardShare + val smartContractAddr = cmdInput.rewardAddress + val sign25519 = cmdInput.signature25519 + val signVrf = cmdInput.signatureVrf + + + if (gasView.getBalance(invocation.caller).subtract(stakedAmount).signum() < 0) { + val errMsg = s"Not enough balance: ${cmdInput.forgerPublicKeys.toString}" + log.debug(errMsg) + throw new ExecutionRevertedException(errMsg) + } + + // check that rewardShare is in legal range + if (rewardShare < 0 || rewardShare > MAX_REWARD_SHARE) { + val errMsg = s"Illegal reward share value: = $rewardShare" + log.debug(errMsg) + throw new ExecutionRevertedException(errMsg) + } + + if (rewardShare == 0 && smartContractAddr != Address.ZERO) { + val errMsg = s"Reward share cannot be 0 if reward address is defined - Reward share = $rewardShare, reward address = $smartContractAddr" + log.debug(errMsg) + throw new ExecutionRevertedException(errMsg) + } + else if (rewardShare != 0 && smartContractAddr == Address.ZERO) { + val errMsg = s"Reward share cannot be different from 0 if reward address is not defined - Reward share = $rewardShare, reward address = $smartContractAddr" + log.debug(errMsg) + throw new ExecutionRevertedException(errMsg) + } + + // we take for granted that forger list is open TODO comment also in fork list + + // check we do not have this forger yet. This is an early check, addForger will do it as well + if (getForger(gasView, blockSignPubKey, vrfPubKey).isDefined) { + val errMsg = s"Can not register an already existing forger: ${ForgerPublicKeys(blockSignPubKey, vrfPubKey).toString}" + log.debug(errMsg) + throw new ExecutionRevertedException(errMsg) + } + + val messageToSign = getHashedMessageToSign( + BytesUtils.toHexString(blockSignPubKey.pubKeyBytes()), + BytesUtils.toHexString(vrfPubKey.pubKeyBytes()), + rewardShare, + BytesUtils.toHexString(smartContractAddr.toBytes)) + + // verify the signatures (throws exceptions) + verifySignatures(messageToSign, blockSignPubKey, vrfPubKey, sign25519, signVrf) + + // add new forger to the db + val delegatorAddress = invocation.caller + addForger(gasView, blockSignPubKey, vrfPubKey, rewardShare, smartContractAddr, + context.blockContext.consensusEpochNumber, delegatorAddress, stakedAmount) + + gasView.subBalance(invocation.caller, stakedAmount) + // increase the balance of the "forger stake smart contract” account + gasView.addBalance(contractAddress, stakedAmount) + + val registerForgerEvent = RegisterForger(invocation.caller, blockSignPubKey, vrfPubKey, stakedAmount, rewardShare, smartContractAddr) + val evmLog = getEthereumConsensusDataLog(registerForgerEvent) + gasView.addLog(evmLog) + + log.debug(s"register forger exiting - ${cmdInput.toString}") + Array.emptyByteArray + } + + def doUpdateForger(invocation: Invocation, gasView: BaseAccountStateView, context: ExecutionContext): Array[Byte] = { + requireIsNotPayable(invocation) + checkForgerStakesV2IsActive(gasView) + + val inputParams = getArgumentsFromData(invocation.input) + + val cmdInput = RegisterOrUpdateForgerCmdInputDecoder.decode(inputParams) + val blockSignPubKey = cmdInput.forgerPublicKeys.blockSignPublicKey + val vrfPubKey = cmdInput.forgerPublicKeys.vrfPublicKey + val rewardShare = cmdInput.rewardShare + val rewardAddress = cmdInput.rewardAddress + val sign25519 = cmdInput.signature25519 + val signVrf = cmdInput.signatureVrf + + // check that rewardShare is in legal range (0, MAX] + if (rewardShare <= 0 || rewardShare > MAX_REWARD_SHARE) { + val errMsg = s"Illegal reward share value: = $rewardShare" + log.debug(errMsg) + throw new ExecutionRevertedException(errMsg) + } + + if (rewardAddress == Address.ZERO) { + val errMsg = s"Reward address cannot be the ZERO address" + log.debug(errMsg) + throw new ExecutionRevertedException(errMsg) + } + + // check we do have this forger and get it + val forger = getForger(gasView, blockSignPubKey, vrfPubKey) match { + case Some(obj) => obj + case None => + val errMsg = s"Forger does not exist: ${ForgerPublicKeys(blockSignPubKey, vrfPubKey).toString}" + log.debug(errMsg) + throw new ExecutionRevertedException(errMsg) + } + + if (forger.rewardShare != 0 || forger.rewardAddress.address() != Address.ZERO) { + val errMsg = s"Reward share or reward address are not null - Reward share = ${forger.rewardShare}, reward address = ${forger.rewardAddress}" + log.debug(errMsg) + throw new ExecutionRevertedException(errMsg) + } + + val messageToSign = getHashedMessageToSign( + BytesUtils.toHexString(blockSignPubKey.pubKeyBytes()), + BytesUtils.toHexString(vrfPubKey.pubKeyBytes()), + rewardShare, + BytesUtils.toHexString(rewardAddress.toBytes)) + + // verify the signatures (throws exceptions) + verifySignatures(messageToSign, blockSignPubKey, vrfPubKey, sign25519, signVrf) + + // update forger in the db + updateForger(gasView, blockSignPubKey, vrfPubKey, rewardShare, rewardAddress) + + val updateForgerEvent = UpdateForger(invocation.caller, blockSignPubKey, vrfPubKey, rewardShare, rewardAddress) + val evmLog = getEthereumConsensusDataLog(updateForgerEvent) + gasView.addLog(evmLog) + + log.debug(s"update forger exiting - ${cmdInput.toString}") + Array.emptyByteArray + } + + def doDelegateCmd(invocation: Invocation, view: BaseAccountStateView, context: ExecutionContext): Array[Byte] = { + + checkForgerStakesV2IsActive(view) + val inputParams = getArgumentsFromData(invocation.input) + val SelectByForgerCmdInput(forgerPublicKeys) = SelectByForgerCmdInputDecoder.decode(inputParams) + + log.debug(s"delegate called - $forgerPublicKeys") + val stakedAmount = invocation.value + + if (stakedAmount.signum() <= 0) { + val msg = "Value must not be zero" + log.debug(msg) + throw new ExecutionRevertedException(msg) + } + + if (!isValidZenAmount(stakedAmount)) { + val msg = s"Value is not a legal wei amount: $stakedAmount" + log.debug(msg) + throw new ExecutionRevertedException(msg) + } + + if (view.getBalance(invocation.caller).compareTo(stakedAmount) < 0){ + throw new ExecutionRevertedException(s"Insufficient funds. Required: $stakedAmount, available: ${view.getBalance(invocation.caller)}") + } + + val epochNumber = context.blockContext.consensusEpochNumber + + StakeStorage.addStake(view, forgerPublicKeys.blockSignPublicKey, forgerPublicKeys.vrfPublicKey, + epochNumber, invocation.caller, stakedAmount) + + val delegateStakeEvt = DelegateForgerStake(invocation.caller, forgerPublicKeys.blockSignPublicKey, forgerPublicKeys.vrfPublicKey, stakedAmount) + val evmLog = getEthereumConsensusDataLog(delegateStakeEvt) + view.addLog(evmLog) + + view.subBalance(invocation.caller, stakedAmount) + // increase the balance of the "forger stake smart contract” account + view.addBalance(contractAddress, stakedAmount) + + Array.emptyByteArray + } + + def doWithdrawCmd(invocation: Invocation, view: BaseAccountStateView, context: ExecutionContext): Array[Byte] = { + requireIsNotPayable(invocation) + checkForgerStakesV2IsActive(view) + + val inputParams = getArgumentsFromData(invocation.input) + val WithdrawCmdInput(forgerPublicKeys, stakedAmount) = WithdrawCmdInputDecoder.decode(inputParams) + + log.debug(s"withdraw called - $forgerPublicKeys $stakedAmount") + + if (stakedAmount.signum() != 1) { + val msg = s"Withdrawal amount must be greater than zero: $stakedAmount" + log.debug(msg) + throw new ExecutionRevertedException(msg) + } + + if (!isValidZenAmount(stakedAmount)) { + val msg = s"Value is not a legal wei amount: $stakedAmount" + log.debug(msg) + throw new ExecutionRevertedException(msg) + } + + val epochNumber = context.blockContext.consensusEpochNumber + + StakeStorage.removeStake(view, forgerPublicKeys.blockSignPublicKey, forgerPublicKeys.vrfPublicKey, + epochNumber, invocation.caller, stakedAmount) + + val withdrawStakeEvt = WithdrawForgerStake(invocation.caller, forgerPublicKeys.blockSignPublicKey, forgerPublicKeys.vrfPublicKey, stakedAmount) + val evmLog = getEthereumConsensusDataLog(withdrawStakeEvt) + view.addLog(evmLog) + + view.subBalance(contractAddress, stakedAmount) + view.addBalance(invocation.caller, stakedAmount) + + Array.emptyByteArray + } + + def checkForgerStakesV2IsActive(view: BaseAccountStateView): Unit = { + if (!StakeStorage.isActive(view)) { + val msg = "Forger stake V2 has not been activated yet" + log.debug(msg) + throw new ExecutionRevertedException("Forger stake V2 has not been activated yet") + } + } + + def doStakeTotalCmd(invocation: Invocation, view: BaseAccountStateView, currentEpoch: Int): Array[Byte] = { + requireIsNotPayable(invocation) + checkForgerStakesV2IsActive(view) + + val inputParams = getArgumentsFromData(invocation.input) + val cmdInput = StakeTotalCmdInputDecoder.decode(inputParams) + + if (cmdInput.consensusEpochStart.isDefined && (cmdInput.consensusEpochStart.get > currentEpoch)) { + val msgStr = s"Illegal argument - consensus epoch start ${cmdInput.consensusEpochStart.get} can not be greater than currentEpoch $currentEpoch" + throw new ExecutionRevertedException(msgStr) + } + + val forgerKeys = cmdInput.forgerPublicKeys + val delegator = cmdInput.delegator + val consensusEpochStart = if (cmdInput.consensusEpochStart.isEmpty) currentEpoch else cmdInput.consensusEpochStart.get + val maxNumOfEpoch = cmdInput.maxNumOfEpoch + log.info(s"stakeTotal called - $forgerKeys $delegator epochStart: $consensusEpochStart - maxNumOfEpoch: $maxNumOfEpoch") + + if (forgerKeys.isEmpty) { + if (delegator.isDefined) { + val msgStr = s"Illegal argument - delegator is defined while forger keys are not" + throw new ExecutionRevertedException(msgStr) + } + } else { + // check we do have this forger + if (getForger(view, forgerKeys.get.blockSignPublicKey, forgerKeys.get.vrfPublicKey).isEmpty) { + val errMsg = s"Forger does not exist: ${forgerKeys.toString}" + log.debug(errMsg) + throw new ExecutionRevertedException(errMsg) + } + } + + val consensusEpochEnd = + if (maxNumOfEpoch.isEmpty) consensusEpochStart + else if (consensusEpochStart + maxNumOfEpoch.get > currentEpoch) currentEpoch + else consensusEpochStart + maxNumOfEpoch.get - 1 + + val response: StakeTotalCmdOutput = StakeStorage.getStakeTotal(view, forgerKeys, delegator, consensusEpochStart, consensusEpochEnd) + + response.encode() + } + + def doRewardsReceivedCmd(invocation: Invocation, view: BaseAccountStateView, metadata: MsgProcessorMetadataStorageReader, currentEpoch: Int): Array[Byte] = { + requireIsNotPayable(invocation) + checkForgerStakesV2IsActive(view) + + val inputParams = getArgumentsFromData(invocation.input) + val cmdInput = RewardsReceivedCmdInputDecoder.decode(inputParams) + + val forgerKeys: ForgerPublicKeys = cmdInput.forgerPublicKeys + val consensusEpochStart: Int = cmdInput.consensusEpochStart + + if (consensusEpochStart >= currentEpoch) { + val msgStr = s"Illegal argument - consensus epoch start $consensusEpochStart can not be greater than currentEpoch $currentEpoch" + throw new ExecutionRevertedException(msgStr) + } + + val maxNumOfEpoch: Int = + if (consensusEpochStart + cmdInput.maxNumOfEpoch - 1 >= currentEpoch) + currentEpoch - consensusEpochStart + else + cmdInput.maxNumOfEpoch + + log.info(s"rewardsReceived called - $forgerKeys epochStart: $consensusEpochStart") + StakeStorage.getForger(view, forgerKeys.blockSignPublicKey, forgerKeys.vrfPublicKey) + .getOrElse(throw new ExecutionRevertedException("Forger doesn't exist.")) + + val rewards: Seq[BigInteger] = metadata.getForgerRewards(forgerKeys, consensusEpochStart, maxNumOfEpoch) + val response = RewardsReceivedCmdOutput(rewards) + + response.encode() + } + + def doStakeStartCmd(invocation: Invocation, view: BaseAccountStateView): Array[Byte] = { + requireIsNotPayable(invocation) + checkForgerStakesV2IsActive(view) + + val inputParams = getArgumentsFromData(invocation.input) + val cmdInput = StakeStartCmdInputDecoder.decode(inputParams) + + val forgerKeys = cmdInput.forgerPublicKeys + val delegator = cmdInput.delegator + log.info(s"stakeStart called - $forgerKeys $delegator") + + val response: StakeStartCmdOutput = StakeStorage.getStakeStart(view, forgerKeys, delegator) + response.encode() + } + + def doPagedForgersStakesByDelegatorCmd(invocation: Invocation, view: BaseAccountStateView): Array[Byte] = { + requireIsNotPayable(invocation) + checkForgerStakesV2IsActive(view) + + val inputParams = getArgumentsFromData(invocation.input) + val cmdInput = PagedForgersStakesByDelegatorCmdInputDecoder.decode(inputParams) + log.debug(s"getPagedForgersStakesByDelegator called - ${cmdInput.delegator} startIndex: ${cmdInput.startIndex} - pageSize: ${cmdInput.pageSize}") + + val response = Try { + // We must trap any exception arising and return the execution reverted exception + StakeStorage.getPagedForgersStakesByDelegator(view, cmdInput.delegator, cmdInput.startIndex, cmdInput.pageSize) + } match { + case Success(response) => response + case Failure(ex) => + throw new ExecutionRevertedException("Could not get paged result: " + ex.getMessage) + } + + PagedForgersStakesByDelegatorOutput(response.nextStartPos, response.stakesData).encode() + } + + def doPagedForgersStakesByForgerCmd(invocation: Invocation, view: BaseAccountStateView): Array[Byte] = { + requireIsNotPayable(invocation) + checkForgerStakesV2IsActive(view) + + val inputParams = getArgumentsFromData(invocation.input) + val cmdInput = PagedForgersStakesByForgerCmdInputDecoder.decode(inputParams) + log.debug(s"getPagedForgersStakesByForger called - ${cmdInput.forgerPublicKeys} startIndex: ${cmdInput.startIndex} - pageSize: ${cmdInput.pageSize}") + + val response = Try { + // We must trap any exception arising and return the execution reverted exception + StakeStorage.getPagedForgersStakesByForger(view, cmdInput.forgerPublicKeys, cmdInput.startIndex, cmdInput.pageSize) + } match { + case Success(response) => response + case Failure(ex) => + throw new ExecutionRevertedException("Could not get paged result: " + ex.getMessage) + } + PagedForgersStakesByForgerOutput(response.nextStartPos, response.stakesData).encode() + } + + def doGetForgerCmd(invocation: Invocation, view: BaseAccountStateView): Array[Byte] = { + requireIsNotPayable(invocation) + checkForgerStakesV2IsActive(view) + + val inputParams = getArgumentsFromData(invocation.input) + val cmdInput = SelectByForgerCmdInputDecoder.decode(inputParams) + + val forgerOpt = StakeStorage.getForger(view, cmdInput.forgerPublicKeys.blockSignPublicKey, cmdInput.forgerPublicKeys.vrfPublicKey) + if (forgerOpt.isEmpty) + throw new ExecutionRevertedException("Forger doesn't exist.") + + forgerOpt.get.encode() + } + + def doGetPagedForgersCmd(invocation: Invocation, view: BaseAccountStateView): Array[Byte] = { + requireIsNotPayable(invocation) + checkForgerStakesV2IsActive(view) + + val inputParams = getArgumentsFromData(invocation.input) + val PagedForgersCmdInput(startPos, pageSize) = PagedForgersCmdInputDecoder.decode(inputParams) + + val res = StakeStorage.getPagedListOfForgers(view, startPos, pageSize) + PagedForgersOutput(res.nextStartPos, res.forgers).encode() + } + + def doGetCurrentConsensusEpochCmd(invocation: Invocation, view: BaseAccountStateView, context: ExecutionContext): Array[Byte] = { + requireIsNotPayable(invocation) + checkForgerStakesV2IsActive(view) + checkInputDoesntContainParams(invocation) + + ConsensusEpochCmdOutput(context.blockContext.consensusEpochNumber).encode() + } + + def doActivateCmd(invocation: Invocation, view: BaseAccountStateView, context: ExecutionContext): Array[Byte] = { + + //Check is well formed + requireIsNotPayable(invocation) + checkInputDoesntContainParams(invocation) + + //Check it cannot be called twice + if (StakeStorage.isActive(view)) { + val msgStr = s"Forger stake V2 already activated" + log.debug(msgStr) + throw new ExecutionRevertedException(msgStr) + } + + val intrinsicGas = invocation.gasPool.getUsedGas + + //Call "disableAndMigrate" on old forger stake msg processor, so it won't be used anymore. + //It returns all existing stakes that will be recreated in the ForgerStakes V2 + val result = context.execute(invocation.call(FORGER_STAKE_SMART_CONTRACT_ADDRESS, BigInteger.ZERO, + BytesUtils.fromHexString(ForgerStakeMsgProcessor.DisableAndMigrateCmd), invocation.gasPool.getGas)) + val listOfExistingStakes = AccountForgingStakeInfoListDecoder.decode(result).listOfStakes + val stakesByForger = listOfExistingStakes.groupBy(_.forgerStakeData.forgerPublicKeys) + + val epochNumber = context.blockContext.consensusEpochNumber + + var totalMigratedStakeAmount = BigInteger.ZERO + stakesByForger.foreach { case (forgerKeys, stakesByForger) => + // Sum the stakes by delegator + val stakesByDelegator = stakesByForger.groupBy(_.forgerStakeData.ownerPublicKey) + val listOfTotalStakesByDelegator = stakesByDelegator.mapValues(_.foldLeft(BigInteger.ZERO) { + (sum, stake) => sum.add(stake.forgerStakeData.stakedAmount) + }) + //Take first delegator for registering the forger + val (firstDelegator, firstDelegatorStakeAmount) = listOfTotalStakesByDelegator.head + //in this case we don't have to check the 10 ZEN minimum threshold for adding a new forger + StakeStorage.addForger(view, forgerKeys.blockSignPublicKey, + forgerKeys.vrfPublicKey, 0, Address.ZERO, epochNumber, firstDelegator.address(), firstDelegatorStakeAmount) + totalMigratedStakeAmount = totalMigratedStakeAmount.add(firstDelegatorStakeAmount) + listOfTotalStakesByDelegator.tail.foreach { case (delegator, delegatorStakeAmount) => + StakeStorage.addStake(view, forgerKeys.blockSignPublicKey, forgerKeys.vrfPublicKey, + epochNumber, delegator.address(), delegatorStakeAmount) + totalMigratedStakeAmount = totalMigratedStakeAmount.add(delegatorStakeAmount) + } + } + + //Update the balance of both forger stake msg processors + view.subBalance(FORGER_STAKE_SMART_CONTRACT_ADDRESS, totalMigratedStakeAmount) + view.addBalance(FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS, totalMigratedStakeAmount) + + // Refund the used gas, because activate should be free, except for the intrinsic gas + invocation.gasPool.addGas(invocation.gasPool.getUsedGas.subtract(intrinsicGas)) + + StakeStorage.setActive(view) + + val activateEvent = ActivateStakeV2() + val evmLog = getEthereumConsensusDataLog(activateEvent) + view.addLog(evmLog) + + log.info(s"Forger stakes V2 activated successfully - ${listOfExistingStakes.size} items migrated, " + + s"total stake amount $totalMigratedStakeAmount") + Array.emptyByteArray + } + + val RegisterForgerCmd: String = getABIMethodId("registerForger(bytes32,bytes32,bytes1,uint32,address,bytes32,bytes32,bytes32,bytes32,bytes32,bytes1)") + val UpdateForgerCmd: String = getABIMethodId("updateForger(bytes32,bytes32,bytes1,uint32,address,bytes32,bytes32,bytes32,bytes32,bytes32,bytes1)") + val DelegateCmd: String = getABIMethodId("delegate(bytes32,bytes32,bytes1)") + val WithdrawCmd: String = getABIMethodId("withdraw(bytes32,bytes32,bytes1,uint256)") + val StakeTotalCmd: String = getABIMethodId("stakeTotal(bytes32,bytes32,bytes1,address,uint32,uint32)") + val StakeStartCmd: String = getABIMethodId("stakeStart(bytes32,bytes32,bytes1,address)") + val RewardsReceivedCmd: String = getABIMethodId("rewardsReceived(bytes32,bytes32,bytes1,uint32,uint32)") + val GetPagedForgersStakesByForgerCmd: String = getABIMethodId("getPagedForgersStakesByForger(bytes32,bytes32,bytes1,int32,int32)") + val GetPagedForgersStakesByDelegatorCmd: String = getABIMethodId("getPagedForgersStakesByDelegator(address,int32,int32)") + val ActivateCmd: String = getABIMethodId("activate()") + val GetForgerCmd: String = getABIMethodId("getForger(bytes32,bytes32,bytes1)") + val GetPagedForgersCmd: String = getABIMethodId("getPagedForgers(int32,int32)") + val GetCurrentConsensusEpochCmd: String = getABIMethodId("getCurrentConsensusEpoch()") + + // ensure we have strings consistent with size of opcode + require( + RegisterForgerCmd.length == 2 * METHOD_ID_LENGTH && + UpdateForgerCmd.length == 2 * METHOD_ID_LENGTH && + DelegateCmd.length == 2 * METHOD_ID_LENGTH && + WithdrawCmd.length == 2 * METHOD_ID_LENGTH && + StakeTotalCmd.length == 2 * METHOD_ID_LENGTH && + StakeStartCmd.length == 2 * METHOD_ID_LENGTH && + RewardsReceivedCmd.length == 2 * METHOD_ID_LENGTH && + ActivateCmd.length == 2 * METHOD_ID_LENGTH && + GetPagedForgersStakesByForgerCmd.length == 2 * METHOD_ID_LENGTH && + GetPagedForgersStakesByDelegatorCmd.length == 2 * METHOD_ID_LENGTH && + GetForgerCmd.length == 2 * METHOD_ID_LENGTH && + GetPagedForgersCmd.length == 2 * METHOD_ID_LENGTH && + GetCurrentConsensusEpochCmd.length == 2 * METHOD_ID_LENGTH + ) + + def getHashedMessageToSign(blockSignPubKeyStr: String, vrfPublicKeyStr: String, rewardShare: Int, rewardAddress: String): Array[Byte] = { + // we use lower case representation for hex strings and checksum format for address + val messageToSignString = cleanHexPrefix(blockSignPubKeyStr).toLowerCase + cleanHexPrefix(vrfPublicKeyStr).toLowerCase + rewardShare.toString + Keys.toChecksumAddress(rewardAddress) + // we take only first 31 bytes since vrf signature is involved, and we must be on the safe side using less than a field element's modulus bits size (253 bites) + Keccak256.hash(messageToSignString.getBytes(StandardCharsets.UTF_8)).take(31) + } + + override private[horizen] def getPagedListOfForgersStakes(view: BaseAccountStateView, startPos: Int, pageSize: Int): PagedForgersListResponse = { + StakeStorage.getPagedListOfForgers(view, startPos, pageSize) + } + + override private[horizen] def getListOfForgersStakes(view: BaseAccountStateView): Seq[ForgerStakeData] = { + StakeStorage.getAllForgerStakes(view) + } + + override private[horizen] def getPagedForgersStakesByForger(view: BaseAccountStateView, forger: ForgerPublicKeys, startPos: Int, pageSize: Int): PagedStakesByForgerResponse = { + checkForgerStakesV2IsActive(view) + StakeStorage.getPagedForgersStakesByForger(view, forger, startPos, pageSize) + } + + override private[horizen] def getPagedForgersStakesByDelegator(view: BaseAccountStateView, delegator: Address, startPos: Int, pageSize: Int): PagedStakesByDelegatorResponse = { + checkForgerStakesV2IsActive(view) + StakeStorage.getPagedForgersStakesByDelegator(view, delegator, startPos, pageSize) + } + + override private[horizen] def getForgingStakes(view: BaseAccountStateView): Seq[ForgingStakeInfo] = { + StakeStorage.getForgingStakes(view) + } + + override private[horizen] def getForgerInfo(view: BaseAccountStateView, forger: ForgerPublicKeys): Option[ForgerInfo] = { + StakeStorage.getForger(view, forger.blockSignPublicKey, forger.vrfPublicKey) + } + + override private[horizen] def isActive(view: BaseAccountStateView): Boolean = StakeStorage.isActive(view) + + + +} 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 5f975cc2cd..1d2c2b1751 100644 --- a/sdk/src/main/scala/io/horizen/account/state/McAddrOwnershipMsgProcessor.scala +++ b/sdk/src/main/scala/io/horizen/account/state/McAddrOwnershipMsgProcessor.scala @@ -7,6 +7,7 @@ import io.horizen.account.proof.SignatureSecp256k1 import io.horizen.account.state.McAddrOwnershipMsgProcessor.{AddNewMultisigOwnershipCmd, AddNewOwnershipCmd, GetListOfAllOwnershipsCmd, GetListOfOwnerScAddressesCmd, GetListOfOwnershipsCmd, OwnershipLinkedListNullValue, OwnershipsLinkedListTipKey, RemoveOwnershipCmd, ScAddressRefsLinkedListNullValue, ScAddressRefsLinkedListTipKey, checkMcRedeemScriptForMultisig, checkMultisigAddress, getMcSignature, getOwnershipId, isValidOwnershipSignature, verifySignaturesWithThreshold} import io.horizen.account.state.NativeSmartContractMsgProcessor.NULL_HEX_STRING_32 import io.horizen.account.state.events.{AddMcAddrOwnership, RemoveMcAddrOwnership} +import io.horizen.account.storage.MsgProcessorMetadataStorageReader import io.horizen.account.utils.BigIntegerUInt256.getUnsignedByteArray import io.horizen.account.utils.Secp256k1.{PUBLIC_KEY_SIZE, SIGNATURE_RS_SIZE} import io.horizen.account.utils.WellKnownAddresses.MC_ADDR_OWNERSHIP_SMART_CONTRACT_ADDRESS @@ -477,7 +478,7 @@ case class McAddrOwnershipMsgProcessor(networkParams: NetworkParams) extends Nat } @throws(classOf[ExecutionFailedException]) - override def process(invocation: Invocation, view: BaseAccountStateView, context: ExecutionContext): Array[Byte] = { + override def process(invocation: Invocation, view: BaseAccountStateView, metadata: MsgProcessorMetadataStorageReader, context: ExecutionContext): Array[Byte] = { if (!isForkActive(context.blockContext.consensusEpochNumber)) { throw new ExecutionRevertedException(s"zenDao fork not active") } diff --git a/sdk/src/main/scala/io/horizen/account/state/McForgerPoolRewardsSerializer.scala b/sdk/src/main/scala/io/horizen/account/state/McForgerPoolRewardsSerializer.scala index af20f6f18a..daedb187ac 100644 --- a/sdk/src/main/scala/io/horizen/account/state/McForgerPoolRewardsSerializer.scala +++ b/sdk/src/main/scala/io/horizen/account/state/McForgerPoolRewardsSerializer.scala @@ -1,34 +1,56 @@ package io.horizen.account.state -import io.horizen.account.proposition.{AddressProposition, AddressPropositionSerializer} -import io.horizen.evm.utils.BigIntegerSerializer +import io.horizen.account.proposition.AddressPropositionSerializer +import io.horizen.account.utils.ForgerIdentifier +import io.horizen.proposition.{PublicKey25519PropositionSerializer, VrfPublicKeySerializer} import sparkz.core.serialization.SparkzSerializer import sparkz.util.serialization.{Reader, Writer} import java.math.BigInteger -object McForgerPoolRewardsSerializer extends SparkzSerializer[Map[AddressProposition, BigInteger]] { - +object McForgerPoolRewardsSerializer extends SparkzSerializer[Map[ForgerIdentifier, BigInteger]] { + final val SERIALIZATION_FORMAT_1_4_FLAG = -1 private val addressSerializer: AddressPropositionSerializer = AddressPropositionSerializer.getSerializer - new BigIntegerSerializer() + private val signPubKeySerializer: PublicKey25519PropositionSerializer = + PublicKey25519PropositionSerializer.getSerializer + private val vrfPubKeySerializer: VrfPublicKeySerializer = VrfPublicKeySerializer.getSerializer - override def serialize(forgerPoolRewards: Map[AddressProposition, BigInteger], w: Writer): Unit = { + override def serialize(forgerPoolRewards: Map[ForgerIdentifier, BigInteger], w: Writer): Unit = { w.putInt(forgerPoolRewards.size) - forgerPoolRewards.foreach { case (address, reward) => - addressSerializer.serialize(address, w) - val rewardBytes: Array[Byte] = reward.toByteArray - w.putInt(rewardBytes.length) - w.putBytes(rewardBytes) + forgerPoolRewards.foreach { case (forgerIdentifier, reward) => + addressSerializer.serialize(forgerIdentifier.getAddress, w) + if (forgerIdentifier.getForgerKeys.isDefined) { + w.putInt(SERIALIZATION_FORMAT_1_4_FLAG) //flag to indicate new serialization format is used + forgerIdentifier.getForgerKeys.foreach { keys => + signPubKeySerializer.serialize(keys.blockSignPublicKey, w) + vrfPubKeySerializer.serialize(keys.vrfPublicKey, w) + } + w.putInt(reward.toByteArray.length) + w.putBytes(reward.toByteArray) + } + else { + w.putInt(reward.toByteArray.length) + w.putBytes(reward.toByteArray) + } } } - override def parse(r: Reader): Map[AddressProposition, BigInteger] = { + override def parse(r: Reader): Map[ForgerIdentifier, BigInteger] = { val length = r.getInt() (1 to length).map { _ => val address = addressSerializer.parse(r) - val rewardLength: Int = r.getInt - val reward: BigInteger = new BigInteger(r.getBytes(rewardLength)) - (address, reward) + val valueLength: Int = r.getInt + if (valueLength == SERIALIZATION_FORMAT_1_4_FLAG) { + val blockSignPublicKey = signPubKeySerializer.parse(r) + val vrfPublicKey = vrfPubKeySerializer.parse(r) + val rewardLength: Int = r.getInt + val reward: BigInteger = new BigInteger(r.getBytes(rewardLength)) + (new ForgerIdentifier(address, Some(ForgerPublicKeys(blockSignPublicKey, vrfPublicKey))), reward) + } + else { + val reward: BigInteger = new BigInteger(r.getBytes(valueLength)) + (new ForgerIdentifier(address), reward) + } }.toMap } 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 595c6d5bd9..3cff10fe5e 100644 --- a/sdk/src/main/scala/io/horizen/account/state/MessageProcessorUtil.scala +++ b/sdk/src/main/scala/io/horizen/account/state/MessageProcessorUtil.scala @@ -29,8 +29,9 @@ object MessageProcessorUtil { // the Eoa msg processor would preempt it Seq(McAddrOwnershipMsgProcessor(params)) ++ - maybeProxyMsgProcessor.toSeq ++ - Seq(EoaMessageProcessor, + maybeProxyMsgProcessor.toSeq ++ + Seq(ForgerStakeV2MsgProcessor) ++ + Seq(EoaMessageProcessor, WithdrawalMsgProcessor, ForgerStakeMsgProcessor(params), ) ++ 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 0c81409ca0..2e41a7fca5 100644 --- a/sdk/src/main/scala/io/horizen/account/state/NativeSmartContractMsgProcessor.scala +++ b/sdk/src/main/scala/io/horizen/account/state/NativeSmartContractMsgProcessor.scala @@ -1,5 +1,6 @@ package io.horizen.account.state +import io.horizen.account.abi.ABIUtil.{METHOD_ID_LENGTH, getArgumentsFromData} import io.horizen.account.state.events.EthereumEvent import io.horizen.account.state.receipt.EthereumConsensusDataLog import io.horizen.evm.Address @@ -35,9 +36,17 @@ abstract class NativeSmartContractMsgProcessor extends MessageProcessor with Spa EthereumEvent.getEthereumConsensusDataLog(contractAddress, event) } - def requireIsNotPayable(invocation: Invocation): Unit = if (invocation.value.signum() != 0) { - throw new ExecutionRevertedException("Call value must be zero") + def requireIsNotPayable(invocation: Invocation): Unit = if (invocation.value.signum() != 0) throw new ExecutionRevertedException("Call value must be zero") + + def checkInputDoesntContainParams(invocation: Invocation): Unit = { + // check we have no other bytes after the op code in the msg data + 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) + } } + } object NativeSmartContractMsgProcessor { diff --git a/sdk/src/main/scala/io/horizen/account/state/ProxyMsgProcessor.scala b/sdk/src/main/scala/io/horizen/account/state/ProxyMsgProcessor.scala index 1da08cda43..b136fe78f9 100644 --- a/sdk/src/main/scala/io/horizen/account/state/ProxyMsgProcessor.scala +++ b/sdk/src/main/scala/io/horizen/account/state/ProxyMsgProcessor.scala @@ -4,9 +4,10 @@ import io.horizen.account.abi.ABIUtil.{METHOD_ID_LENGTH, getABIMethodId, getArgu import io.horizen.account.fork.ContractInteroperabilityFork import io.horizen.account.state.ProxyMsgProcessor._ import io.horizen.account.state.events.ProxyInvocation +import io.horizen.account.storage.MsgProcessorMetadataStorageReader 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.params.{NetworkParams, RegTestParams} import io.horizen.utils.BytesUtils import org.web3j.utils.Numeric import sparkz.crypto.hash.Keccak256 @@ -93,7 +94,7 @@ case class ProxyMsgProcessor(params: NetworkParams) extends NativeSmartContractW @throws(classOf[ExecutionFailedException]) - override def process(invocation: Invocation, view: BaseAccountStateView, context: ExecutionContext): Array[Byte] = { + override def process(invocation: Invocation, view: BaseAccountStateView, metadata: MsgProcessorMetadataStorageReader, context: ExecutionContext): Array[Byte] = { log.debug(s"processing invocation: $invocation") val gasView = view.getGasTrackedView(invocation.gasPool) 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 ab8cbb2750..f668d4006f 100644 --- a/sdk/src/main/scala/io/horizen/account/state/StateDbAccountStateView.scala +++ b/sdk/src/main/scala/io/horizen/account/state/StateDbAccountStateView.scala @@ -1,17 +1,20 @@ package io.horizen.account.state import io.horizen.SidechainTypes -import io.horizen.account.fork.Version1_3_0Fork +import io.horizen.account.fork.{Version1_3_0Fork, Version1_4_0Fork} +import io.horizen.account.network.ForgerInfo import io.horizen.account.proposition.AddressProposition +import io.horizen.account.state.nativescdata.forgerstakev2.{PagedStakesByDelegatorResponse, PagedStakesByForgerResponse} import io.horizen.account.state.receipt.EthereumConsensusDataReceipt.ReceiptStatus import io.horizen.account.state.receipt.{EthereumConsensusDataLog, EthereumConsensusDataReceipt} +import io.horizen.account.storage.MsgProcessorMetadataStorageReader import io.horizen.account.transaction.EthereumTransaction import io.horizen.account.utils.{BigIntegerUtil, MainchainTxCrosschainOutputAddressUtil, 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.consensus.{ForgingStakeInfo, minForgerStake} import io.horizen.evm._ +import io.horizen.evm.results.{EvmLog, ProofAccountResult} import io.horizen.proposition.{PublicKey25519Proposition, VrfPublicKey} import io.horizen.transaction.mainchain.{ForwardTransfer, SidechainCreation} import io.horizen.utils.BytesUtils @@ -31,8 +34,12 @@ class StateDbAccountStateView( with SparkzLogging { lazy val withdrawalReqProvider: WithdrawalRequestProvider = messageProcessors.find(_.isInstanceOf[WithdrawalRequestProvider]).get.asInstanceOf[WithdrawalRequestProvider] + lazy val forgerStakesProvider: ForgerStakesProvider = messageProcessors.find(_.isInstanceOf[ForgerStakesProvider]).get.asInstanceOf[ForgerStakesProvider] + lazy val forgerStakesV2Provider: ForgerStakesV2Provider = + messageProcessors.find(_.isInstanceOf[ForgerStakesV2Provider]).get.asInstanceOf[ForgerStakesV2Provider] + // certificateKeysProvider is present only for NaiveThresholdSignatureCircuitWithKeyRotation lazy val certificateKeysProvider: CertificateKeysProvider = messageProcessors.find(_.isInstanceOf[CertificateKeysProvider]).get.asInstanceOf[CertificateKeysProvider] @@ -60,19 +67,39 @@ class StateDbAccountStateView( override def isForgingOpen: Boolean = forgerStakesProvider.isForgerListOpen(this) - override def isForgerStakeAvailable(isForkV1_3Active: Boolean): Boolean = + override def isForgerStakeAvailable(isForkV1_3Active: Boolean): Boolean = { forgerStakesProvider.isForgerStakeAvailable(this, isForkV1_3Active) - - override def getListOfForgersStakes(isForkV1_3Active: Boolean): Seq[AccountForgingStakeInfo] = - forgerStakesProvider.getListOfForgersStakes(this, isForkV1_3Active) + } - override def getPagedListOfForgersStakes(startPos: Int, pageSize: Int): (Int, Seq[AccountForgingStakeInfo]) = + override def getListOfForgersStakes(isForkV1_3Active: Boolean, isForkV1_4Active: Boolean): Seq[AccountForgingStakeInfo] = { + if (isForkV1_4Active && forgerStakesV2IsActive) { + forgerStakesV2Provider.getListOfForgersStakes(this) + .map(AccountForgingStakeInfo(null, _)) + } else { + forgerStakesProvider.getListOfForgersStakes(this, isForkV1_3Active) + } + } + + override def getPagedListOfForgersStakes(startPos: Int, pageSize: Int): (Int, Seq[AccountForgingStakeInfo]) = { forgerStakesProvider.getPagedListOfForgersStakes(this, startPos, pageSize) + } + + override def getPagedForgersStakesByForger(forger: ForgerPublicKeys, startPos: Int, pageSize: Int): PagedStakesByForgerResponse = + forgerStakesV2Provider.getPagedForgersStakesByForger(this, forger, startPos, pageSize) + + def isForgerStakeV1SmartContractDisabled(isForkV1_4Active: Boolean): Boolean = + forgerStakesProvider.isForgerStakeV1SmartContractDisabled(this, isForkV1_4Active) + + override def getPagedForgersStakesByDelegator(delegator: Address, startPos: Int, pageSize: Int): PagedStakesByDelegatorResponse = + forgerStakesV2Provider.getPagedForgersStakesByDelegator(this, delegator, startPos, pageSize) + + override def getForgerInfo(forger: ForgerPublicKeys): Option[ForgerInfo] = { + forgerStakesV2Provider.getForgerInfo(this, forger) + } override def getAllowedForgerList: Seq[Int] = forgerStakesProvider.getAllowedForgerListIndexes(this) - override def getListOfMcAddrOwnerships(scAddressOpt: Option[String]): Seq[McAddrOwnershipData] = mcAddrOwnershipProvider.getListOfMcAddrOwnerships(this, scAddressOpt) @@ -136,37 +163,54 @@ class StateDbAccountStateView( }) } - def getOrderedForgingStakesInfoSeq(epochNumber: Int): Seq[ForgingStakeInfo] = { - // get forger stakes list view (scala lazy collection) - getListOfForgersStakes(Version1_3_0Fork.get(epochNumber).active).view - - // group delegation stakes by blockSignPublicKey/vrfPublicKey pairs - .groupBy(stake => - (stake.forgerStakeData.forgerPublicKeys.blockSignPublicKey, stake.forgerStakeData.forgerPublicKeys.vrfPublicKey) - ) - - // create a seq of forging stake info for every group entry summing all the delegation amounts. - // Note: ForgingStakeInfo amount is a long and contains a Zennies amount converted from a BigInteger wei amount - // That is safe since the stakedAmount is checked in creation phase to be an exact zennies amount - .map { case ((blockSignKey, vrfKey), stakes) => - ForgingStakeInfo( - blockSignKey, - vrfKey, - stakes.map(stake => - ZenWeiConverter.convertWeiToZennies(stake.forgerStakeData.stakedAmount) - ).sum - ) - } - .toSeq + def getOrderedForgingStakesInfoSeq(stateEpochNumber: Int): Seq[ForgingStakeInfo] = { + val forkV1_4Active = Version1_4_0Fork.get(stateEpochNumber).active + val minStakeFilter: ForgingStakeInfo => Boolean = if (forkV1_4Active) { + fsi => fsi.stakeAmount >= minForgerStake + } else { + _ => true + } + if (forkV1_4Active && forgerStakesV2IsActive) { + // V2 Stake storage provides stakes per forger + forgerStakesV2Provider.getForgingStakes(this) + } else { + // get forger stakes list view (scala lazy collection) + val isForkV1_3Active = Version1_3_0Fork.get(stateEpochNumber).active + forgerStakesProvider.getListOfForgersStakes(this, isForkV1_3Active).view + // group delegation stakes by blockSignPublicKey/vrfPublicKey pairs + .groupBy(stake => + (stake.forgerStakeData.forgerPublicKeys.blockSignPublicKey, stake.forgerStakeData.forgerPublicKeys.vrfPublicKey) + ) + // create a seq of forging stake info for every group entry summing all the delegation amounts. + // Note: ForgingStakeInfo amount is a long and contains a Zennies amount converted from a BigInteger wei amount + // That is safe since the stakedAmount is checked in creation phase to be an exact zennies amount + .map { case ((blockSignKey, vrfKey), stakes) => + ForgingStakeInfo( + blockSignKey, + vrfKey, + stakes.map(stake => + ZenWeiConverter.convertWeiToZennies(stake.forgerStakeData.stakedAmount) + ).sum + ) + } + .toSeq + } + // if 1.4 fork applied, filter stakes of less than 10 zen + .filter(minStakeFilter) // sort the resulting sequence by decreasing stake amount .sorted(Ordering[ForgingStakeInfo].reverse) } @throws(classOf[InvalidMessageException]) @throws(classOf[ExecutionFailedException]) - def applyMessage(msg: Message, blockGasPool: GasPool, blockContext: BlockContext): Array[Byte] = { - new StateTransition(this, messageProcessors, blockGasPool, blockContext, msg).transition() + def applyMessage( + msg: Message, + blockGasPool: GasPool, + blockContext: BlockContext, + metadata: MsgProcessorMetadataStorageReader + ): Array[Byte] = { + new StateTransition(this, messageProcessors, blockGasPool, blockContext, msg, metadata).transition() } /** @@ -185,7 +229,8 @@ class StateDbAccountStateView( tx: SidechainTypes#SCAT, txIndex: Int, blockGasPool: GasPool, - blockContext: BlockContext + blockContext: BlockContext, + metadata: MsgProcessorMetadataStorageReader ): Try[EthereumConsensusDataReceipt] = Try { if (!tx.isInstanceOf[EthereumTransaction]) throw new IllegalArgumentException(s"Unsupported transaction type ${tx.getClass.getName}") @@ -209,7 +254,7 @@ class StateDbAccountStateView( // apply message to state val status = try { - applyMessage(msg, blockGasPool, blockContext) + applyMessage(msg, blockGasPool, blockContext, metadata) ReceiptStatus.SUCCESSFUL } catch { // any other exception will bubble up and invalidate the block @@ -391,4 +436,6 @@ class StateDbAccountStateView( def disableWriteProtection(): Unit = readOnly = false override def getNativeSmartContractAddressList(): Array[Address] = listOfNativeSmartContractAddresses + + override def forgerStakesV2IsActive: Boolean = forgerStakesV2Provider.isActive(this) } diff --git a/sdk/src/main/scala/io/horizen/account/state/StateDbArray.scala b/sdk/src/main/scala/io/horizen/account/state/StateDbArray.scala index f131d97f2c..e27202fe41 100644 --- a/sdk/src/main/scala/io/horizen/account/state/StateDbArray.scala +++ b/sdk/src/main/scala/io/horizen/account/state/StateDbArray.scala @@ -16,7 +16,7 @@ class StateDbArray(val account: Address, val keySeed: Array[Byte]) { size } - private def updateSize(view: BaseAccountStateView, newSize: Int): Unit = { + protected def updateSize(view: BaseAccountStateView, newSize: Int): Unit = { val paddedSize = BigIntegerUtil.toUint256Bytes(BigInteger.valueOf(newSize)) view.updateAccountStorage(account, baseArrayKey, paddedSize) } 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 fb02802f35..c380c53559 100644 --- a/sdk/src/main/scala/io/horizen/account/state/StateTransition.scala +++ b/sdk/src/main/scala/io/horizen/account/state/StateTransition.scala @@ -1,6 +1,7 @@ package io.horizen.account.state import io.horizen.account.fork.Version1_3_0Fork +import io.horizen.account.storage.MsgProcessorMetadataStorageReader import io.horizen.account.utils.BigIntegerUtil import io.horizen.evm.{EvmContext, ForkRules} import sparkz.util.SparkzLogging @@ -16,7 +17,8 @@ class StateTransition( blockGasPool: GasPool, val blockContext: BlockContext, val msg: Message, - ) extends SparkzLogging with ExecutionContext { + metadata: MsgProcessorMetadataStorageReader + ) extends SparkzLogging with ExecutionContext { // the current stack of invocations private val invocationStack = new ListBuffer[Invocation] @@ -241,7 +243,7 @@ class StateTransition( // 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)) + val result = Try.apply(processor.process(invocation, view, metadata, this)) // handle errors result match { // if the processor throws ExecutionRevertedException we revert changes 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 a44351452f..4991ee68bc 100644 --- a/sdk/src/main/scala/io/horizen/account/state/WithdrawalMsgProcessor.scala +++ b/sdk/src/main/scala/io/horizen/account/state/WithdrawalMsgProcessor.scala @@ -4,6 +4,7 @@ import com.google.common.primitives.{Bytes, Ints} import io.horizen.account.abi.ABIUtil.{METHOD_ID_LENGTH, getABIMethodId, getArgumentsFromData, getFunctionSignature} import io.horizen.account.abi.{ABIDecoder, ABIEncodable, ABIListEncoder, MsgProcessorInputDecoder} import io.horizen.account.state.events.AddWithdrawalRequest +import io.horizen.account.storage.MsgProcessorMetadataStorageReader import io.horizen.account.utils.WellKnownAddresses.WITHDRAWAL_REQ_SMART_CONTRACT_ADDRESS import io.horizen.account.utils.ZenWeiConverter import io.horizen.proposition.MCPublicKeyHashProposition @@ -35,7 +36,7 @@ object WithdrawalMsgProcessor extends NativeSmartContractMsgProcessor with Withd val DustThresholdInWei: BigInteger = ZenWeiConverter.convertZenniesToWei(ZenCoinsUtils.getMinDustThreshold(ZenCoinsUtils.MC_DEFAULT_FEE_RATE)) @throws(classOf[ExecutionFailedException]) - override def process(invocation: Invocation, view: BaseAccountStateView, context: ExecutionContext): Array[Byte] = { + override def process(invocation: Invocation, view: BaseAccountStateView, metadata: MsgProcessorMetadataStorageReader, context: ExecutionContext): Array[Byte] = { val gasView = view.getGasTrackedView(invocation.gasPool) getFunctionSignature(invocation.input) match { case GetListOfWithdrawalReqsCmdSig => diff --git a/sdk/src/main/scala/io/horizen/account/state/events/DisableStakeV1.scala b/sdk/src/main/scala/io/horizen/account/state/events/DisableStakeV1.scala new file mode 100644 index 0000000000..79a9053b52 --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/events/DisableStakeV1.scala @@ -0,0 +1,5 @@ +package io.horizen.account.state.events + +class DisableStakeV1( +) + diff --git a/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/ConsensusEpochCmdOutput.scala b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/ConsensusEpochCmdOutput.scala new file mode 100644 index 0000000000..5fb9afe1e0 --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/ConsensusEpochCmdOutput.scala @@ -0,0 +1,38 @@ +package io.horizen.account.state.nativescdata.forgerstakev2 + +import io.horizen.account.abi.{ABIDecoder, ABIEncodable, MsgProcessorInputDecoder} +import org.web3j.abi.TypeReference +import org.web3j.abi.datatypes.generated.Uint32 +import org.web3j.abi.datatypes.{StaticStruct, Type} + +import java.util + + +case class ConsensusEpochCmdOutput(epoch: Int) extends ABIEncodable[StaticStruct] { + + override def asABIType(): StaticStruct = { + + val listOfParams: util.List[Type[_]] = util.Arrays.asList( + new Uint32(epoch) + ) + new StaticStruct(listOfParams) + } + + override def toString: String = "%s(epoch: %s)" + .format(this.getClass.toString, epoch) +} + +object ConsensusEpochCmdOutputDecoder + extends ABIDecoder[ConsensusEpochCmdOutput] + with MsgProcessorInputDecoder[ConsensusEpochCmdOutput] { + + override val getListOfABIParamTypes: util.List[TypeReference[Type[_]]] = { + org.web3j.abi.Utils.convert(util.Arrays.asList( + new TypeReference[Uint32]() {})) + } + + override def createType(listOfParams: util.List[Type[_]]): ConsensusEpochCmdOutput = { + val epoch = listOfParams.get(0).asInstanceOf[Uint32].getValue + ConsensusEpochCmdOutput(epoch.intValueExact()) + } +} \ No newline at end of file diff --git a/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/PagedForgersCmdInputDecoder.scala b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/PagedForgersCmdInputDecoder.scala new file mode 100644 index 0000000000..13fcb3c7fb --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/PagedForgersCmdInputDecoder.scala @@ -0,0 +1,41 @@ +package io.horizen.account.state.nativescdata.forgerstakev2 + +import io.horizen.account.abi.{ABIDecoder, ABIEncodable, MsgProcessorInputDecoder} +import org.web3j.abi.TypeReference +import org.web3j.abi.datatypes.generated.Int32 +import org.web3j.abi.datatypes.{StaticStruct, Type} + +import java.util + +object PagedForgersCmdInputDecoder + extends ABIDecoder[PagedForgersCmdInput] + with MsgProcessorInputDecoder[PagedForgersCmdInput] { + + override val getListOfABIParamTypes: util.List[TypeReference[Type[_]]] = + org.web3j.abi.Utils.convert(util.Arrays.asList( + new TypeReference[Int32]() {}, + new TypeReference[Int32]() {} + )) + + override def createType(listOfParams: util.List[Type[_]]): PagedForgersCmdInput = { + val startIndex = listOfParams.get(0).asInstanceOf[Int32].getValue.intValueExact() + val pageSize = listOfParams.get(1).asInstanceOf[Int32].getValue.intValueExact() + PagedForgersCmdInput(startIndex, pageSize) + } + +} + +case class PagedForgersCmdInput(startIndex: Int, pageSize: Int) extends ABIEncodable[StaticStruct] { + + override def asABIType(): StaticStruct = { + + val listOfParams: util.List[Type[_]] = util.Arrays.asList( + new Int32(startIndex), + new Int32(pageSize)) + + new StaticStruct(listOfParams) + } + + override def toString: String = "%s(startIndex: %s, pageSize: %s)" + .format(this.getClass.toString, startIndex, pageSize) +} diff --git a/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/PagedForgersStakesByDelegatorCmdInputDecoder.scala b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/PagedForgersStakesByDelegatorCmdInputDecoder.scala new file mode 100644 index 0000000000..4d0eaa826b --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/PagedForgersStakesByDelegatorCmdInputDecoder.scala @@ -0,0 +1,42 @@ +package io.horizen.account.state.nativescdata.forgerstakev2 + +import io.horizen.account.abi.{ABIDecoder, ABIEncodable, MsgProcessorInputDecoder} +import io.horizen.evm.Address +import org.web3j.abi.TypeReference +import org.web3j.abi.datatypes.generated.Int32 +import org.web3j.abi.datatypes.{StaticStruct, Type, Address => AbiAddress} + +import java.util + +object PagedForgersStakesByDelegatorCmdInputDecoder + extends ABIDecoder[PagedForgersStakesByDelegatorCmdInput] + with MsgProcessorInputDecoder[PagedForgersStakesByDelegatorCmdInput] { + + override val getListOfABIParamTypes: util.List[TypeReference[Type[_]]] = + org.web3j.abi.Utils.convert(util.Arrays.asList( + new TypeReference[AbiAddress]() {}, + new TypeReference[Int32]() {}, + new TypeReference[Int32]() {} + )) + + override def createType(listOfParams: util.List[Type[_]]): PagedForgersStakesByDelegatorCmdInput = { + val delegator = new Address(listOfParams.get(0).asInstanceOf[AbiAddress].toString) + val startIndex = listOfParams.get(1).asInstanceOf[Int32].getValue.intValueExact() + val pageSize = listOfParams.get(2).asInstanceOf[Int32].getValue.intValueExact() + PagedForgersStakesByDelegatorCmdInput(delegator, startIndex, pageSize) + } +} + +case class PagedForgersStakesByDelegatorCmdInput(delegator: Address, startIndex: Int, pageSize: Int) extends ABIEncodable[StaticStruct] { + + override def asABIType(): StaticStruct = { + val listOfParams: util.List[Type[_]] = util.Arrays.asList( + new AbiAddress(delegator.toString), + new Int32(startIndex), + new Int32(pageSize)) + new StaticStruct(listOfParams) + } + + override def toString: String = "%s(delegator: %s, startIndex: %s, pageSize: %s)" + .format(this.getClass.toString, delegator, startIndex, pageSize) +} diff --git a/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/PagedForgersStakesByDelegatorOutput.scala b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/PagedForgersStakesByDelegatorOutput.scala new file mode 100644 index 0000000000..38c348345e --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/PagedForgersStakesByDelegatorOutput.scala @@ -0,0 +1,78 @@ +package io.horizen.account.state.nativescdata.forgerstakev2 + +import com.fasterxml.jackson.annotation.JsonView +import io.horizen.account.abi.ABIEncodable +import io.horizen.account.proposition.{AddressProposition, AddressPropositionSerializer} +import io.horizen.account.state.{ForgerPublicKeys, ForgerPublicKeysSerializer} +import io.horizen.account.utils.BigIntegerUInt256 +import io.horizen.json.Views +import org.web3j.abi.datatypes.{DynamicArray, DynamicStruct, StaticStruct, Type} +import org.web3j.abi.datatypes.generated.{Int32, Uint256} +import sparkz.core.serialization.{BytesSerializable, SparkzSerializer} +import sparkz.util.serialization.{Reader, Writer} +import org.web3j.abi.datatypes.{Address => AbiAddress} + +import java.math.BigInteger +import java.util +import scala.collection.JavaConverters + + +case class PagedForgersStakesByDelegatorOutput(nextStartPos: Int, listOfStakes: Seq[StakeDataForger]) + extends ABIEncodable[DynamicStruct] { + + override def asABIType(): DynamicStruct = { + + val seqOfStruct = listOfStakes.map(_.asABIType()) + val listOfStruct = JavaConverters.seqAsJavaList(seqOfStruct) + val theType = classOf[StaticStruct] + val listOfParams: util.List[Type[_]] = util.Arrays.asList( + new Int32(nextStartPos), + new DynamicArray(theType, listOfStruct) + ) + new DynamicStruct(listOfParams) + + } + + override def toString: String = "%s(startPos: %s, listOfStake: %s)" + .format( + this.getClass.toString, + nextStartPos, listOfStakes) +} + +@JsonView(Array(classOf[Views.Default])) +case class StakeDataForger( val forgerPublicKeys: ForgerPublicKeys, + val stakedAmount: BigInteger) + extends BytesSerializable with ABIEncodable[StaticStruct] { + + require(stakedAmount.signum() != -1, "stakeAmount expected to be non negative.") + + override type M = StakeDataForger + + override def serializer: SparkzSerializer[StakeDataForger] = StakeDataForgerSerializer + + override def toString: String = "%s(forger: %s, stakedAmount: %s)" + .format(this.getClass.toString, forgerPublicKeys, stakedAmount) + + + private[horizen] def asABIType(): StaticStruct = { + val forgerPublicKeysAbi = forgerPublicKeys.asABIType() + val listOfParams: util.List[Type[_]] = new util.ArrayList(forgerPublicKeysAbi.getValue.asInstanceOf[util.List[Type[_]]]) + listOfParams.add(new Uint256(stakedAmount)) + new StaticStruct(listOfParams) + } +} + +object StakeDataForgerSerializer extends SparkzSerializer[StakeDataForger] { + override def serialize(s: StakeDataForger, w: Writer): Unit = { + ForgerPublicKeysSerializer.serialize(s.forgerPublicKeys, w) + w.putInt(s.stakedAmount.toByteArray.length) + w.putBytes(s.stakedAmount.toByteArray) + } + + override def parse(r: Reader): StakeDataForger = { + val forgerPublicKeys = ForgerPublicKeysSerializer.parse(r) + val stakeAmountLength = r.getInt() + val stakeAmount = new BigIntegerUInt256(r.getBytes(stakeAmountLength)).getBigInt + StakeDataForger(forgerPublicKeys, stakeAmount) + } +} diff --git a/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/PagedForgersStakesByForgerCmdInputDecoder.scala b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/PagedForgersStakesByForgerCmdInputDecoder.scala new file mode 100644 index 0000000000..32adde8d8a --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/PagedForgersStakesByForgerCmdInputDecoder.scala @@ -0,0 +1,49 @@ +package io.horizen.account.state.nativescdata.forgerstakev2 + +import io.horizen.account.abi.{ABIDecoder, ABIEncodable, MsgProcessorInputDecoder} +import io.horizen.account.state.ForgerPublicKeys +import io.horizen.proposition.PublicKey25519Proposition +import org.web3j.abi.TypeReference +import org.web3j.abi.datatypes.{StaticStruct, Type} +import org.web3j.abi.datatypes.generated.{Bytes1, Bytes32, Int32} + +import java.util + +object PagedForgersStakesByForgerCmdInputDecoder + extends ABIDecoder[PagedForgersStakesByForgerCmdInput] + with MsgProcessorInputDecoder[PagedForgersStakesByForgerCmdInput] + with VRFDecoder{ + + override val getListOfABIParamTypes: util.List[TypeReference[Type[_]]] = + org.web3j.abi.Utils.convert(util.Arrays.asList( + new TypeReference[Bytes32]() {}, + new TypeReference[Bytes32]() {}, + new TypeReference[Bytes1]() {}, + new TypeReference[Int32]() {}, + new TypeReference[Int32]() {} + )) + + override def createType(listOfParams: util.List[Type[_]]): PagedForgersStakesByForgerCmdInput = { + val forgerPublicKey = new PublicKey25519Proposition(listOfParams.get(0).asInstanceOf[Bytes32].getValue) + val vrfKey = decodeVrfKey(listOfParams.get(1).asInstanceOf[Bytes32], listOfParams.get(2).asInstanceOf[Bytes1]) + val forgerPublicKeys = ForgerPublicKeys(forgerPublicKey, vrfKey) + val startIndex = listOfParams.get(3).asInstanceOf[Int32].getValue.intValueExact() + val pageSize = listOfParams.get(4).asInstanceOf[Int32].getValue.intValueExact() + PagedForgersStakesByForgerCmdInput(forgerPublicKeys, startIndex, pageSize) + } + +} + +case class PagedForgersStakesByForgerCmdInput(forgerPublicKeys: ForgerPublicKeys, startIndex: Int, pageSize: Int) extends ABIEncodable[StaticStruct] { + + override def asABIType(): StaticStruct = { + val forgerPublicKeysAbi = forgerPublicKeys.asABIType() + val listOfParams: util.List[Type[_]] = new util.ArrayList(forgerPublicKeysAbi.getValue.asInstanceOf[util.List[Type[_]]]) + listOfParams.add(new Int32(startIndex)) + listOfParams.add(new Int32(pageSize)) + new StaticStruct(listOfParams) + } + + override def toString: String = "%s(forgerPubKeys: %s, startIndex: %s, pageSize: %s)" + .format(this.getClass.toString, forgerPublicKeys, startIndex, pageSize) +} diff --git a/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/PagedForgersStakesByForgerOutput.scala b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/PagedForgersStakesByForgerOutput.scala new file mode 100644 index 0000000000..732ae272a0 --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/PagedForgersStakesByForgerOutput.scala @@ -0,0 +1,76 @@ +package io.horizen.account.state.nativescdata.forgerstakev2 + +import com.fasterxml.jackson.annotation.JsonView +import io.horizen.account.abi.ABIEncodable +import io.horizen.account.proposition.{AddressProposition, AddressPropositionSerializer} +import io.horizen.account.utils.BigIntegerUInt256 +import io.horizen.json.Views +import org.web3j.abi.datatypes.{DynamicArray, DynamicStruct, StaticStruct, Type} +import org.web3j.abi.datatypes.generated.{Int32, Uint256} +import sparkz.core.serialization.{BytesSerializable, SparkzSerializer} +import sparkz.util.serialization.{Reader, Writer} +import org.web3j.abi.datatypes.{Address => AbiAddress} +import java.math.BigInteger +import java.util +import scala.collection.JavaConverters + + +case class PagedForgersStakesByForgerOutput(nextStartPos: Int, listOfStakes: Seq[StakeDataDelegator]) + extends ABIEncodable[DynamicStruct] { + + override def asABIType(): DynamicStruct = { + + val seqOfStruct = listOfStakes.map(_.asABIType()) + val listOfStruct = JavaConverters.seqAsJavaList(seqOfStruct) + val theType = classOf[StaticStruct] + val listOfParams: util.List[Type[_]] = util.Arrays.asList( + new Int32(nextStartPos), + new DynamicArray(theType, listOfStruct) + ) + new DynamicStruct(listOfParams) + + } + + override def toString: String = "%s(startPos: %s, listOfStake: %s)" + .format( + this.getClass.toString, + nextStartPos, listOfStakes) +} + +@JsonView(Array(classOf[Views.Default])) +case class StakeDataDelegator(val delegator: AddressProposition, + val stakedAmount: BigInteger) + extends BytesSerializable with ABIEncodable[StaticStruct] { + + require(stakedAmount.signum() != -1, "stakeAmount expected to be non negative.") + + override type M = StakeDataDelegator + + override def serializer: SparkzSerializer[StakeDataDelegator] = StakeDataDelegatorSerializer + + override def toString: String = "%s(delegator: %s, stakedAmount: %s)" + .format(this.getClass.toString, delegator, stakedAmount) + + + private[horizen] def asABIType(): StaticStruct = { + val listOfParams = new util.ArrayList[Type[_]]() + listOfParams.add(new AbiAddress(delegator.address().toString)) + listOfParams.add(new Uint256(stakedAmount)) + new StaticStruct(listOfParams) + } +} + +object StakeDataDelegatorSerializer extends SparkzSerializer[StakeDataDelegator] { + override def serialize(s: StakeDataDelegator, w: Writer): Unit = { + AddressPropositionSerializer.getSerializer.serialize(s.delegator, w) + w.putInt(s.stakedAmount.toByteArray.length) + w.putBytes(s.stakedAmount.toByteArray) + } + + override def parse(r: Reader): StakeDataDelegator = { + val ownerPublicKey = AddressPropositionSerializer.getSerializer.parse(r) + val stakeAmountLength = r.getInt() + val stakeAmount = new BigIntegerUInt256(r.getBytes(stakeAmountLength)).getBigInt + StakeDataDelegator(ownerPublicKey, stakeAmount) + } +} diff --git a/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/RegisterOrUpdateForgerCmdInputDecoder.scala b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/RegisterOrUpdateForgerCmdInputDecoder.scala new file mode 100644 index 0000000000..6d231b3f0f --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/RegisterOrUpdateForgerCmdInputDecoder.scala @@ -0,0 +1,87 @@ +package io.horizen.account.state.nativescdata.forgerstakev2 + +import io.horizen.account.abi.{ABIDecoder, ABIEncodable, MsgProcessorInputDecoder} +import io.horizen.account.state.ForgerPublicKeys +import io.horizen.account.state.ForgerStakeV2MsgProcessor.MAX_REWARD_SHARE +import io.horizen.evm.Address +import io.horizen.proof.{Signature25519, VrfProof} +import io.horizen.proposition.PublicKey25519Proposition +import io.horizen.utils.Ed25519 +import org.web3j.abi.TypeReference +import org.web3j.abi.datatypes.generated.{Bytes1, Bytes32, Uint32} +import org.web3j.abi.datatypes.{StaticStruct, Type, Address => AbiAddress} + +import java.util + +object RegisterOrUpdateForgerCmdInputDecoder + extends ABIDecoder[RegisterOrUpdateForgerCmdInput] + with MsgProcessorInputDecoder[RegisterOrUpdateForgerCmdInput] + with VRFDecoder{ + + val NULL_ADDRESS_WITH_PREFIX_HEX_STRING : String = "0x0000000000000000000000000000000000000000" + + override val getListOfABIParamTypes: util.List[TypeReference[Type[_]]] = + org.web3j.abi.Utils.convert(util.Arrays.asList( + new TypeReference[Bytes32]() {}, // blockSignPublicKey + new TypeReference[Bytes32]() {}, // vrfKey + new TypeReference[Bytes1]() {}, + new TypeReference[Uint32]() {}, // rewardShare + new TypeReference[AbiAddress]() {}, // smart contract address + new TypeReference[Bytes32]() {}, // sign1 64 bytes + new TypeReference[Bytes32]() {}, + new TypeReference[Bytes32]() {}, // sign2 97 bytes + new TypeReference[Bytes32]() {}, + new TypeReference[Bytes32]() {}, + new TypeReference[Bytes1]() {} + )) + + override def createType(listOfParams: util.List[Type[_]]): RegisterOrUpdateForgerCmdInput = { + val blockSignPublicKey = new PublicKey25519Proposition(listOfParams.get(0).asInstanceOf[Bytes32].getValue) + val vrfKey = decodeVrfKey(listOfParams.get(1).asInstanceOf[Bytes32], listOfParams.get(2).asInstanceOf[Bytes1]) + val forgerPublicKeys = ForgerPublicKeys(blockSignPublicKey, vrfKey) + val rewardShare = listOfParams.get(3).asInstanceOf[Uint32].getValue.intValueExact() + val smartcontractAddress = new Address(listOfParams.get(4).asInstanceOf[AbiAddress].toString) + val sign1 = new Signature25519(listOfParams.get(5).asInstanceOf[Bytes32].getValue ++ listOfParams.get(6).asInstanceOf[Bytes32].getValue) + val sign2 = new VrfProof( + listOfParams.get(7).asInstanceOf[Bytes32].getValue ++ listOfParams.get(8).asInstanceOf[Bytes32].getValue ++ + listOfParams.get(9).asInstanceOf[Bytes32].getValue ++ listOfParams.get(10).asInstanceOf[Bytes1].getValue) + + RegisterOrUpdateForgerCmdInput(forgerPublicKeys, rewardShare, smartcontractAddress, sign1, sign2) + } + +} + +case class RegisterOrUpdateForgerCmdInput(forgerPublicKeys: ForgerPublicKeys, rewardShare: Int, + rewardAddress: Address, + signature25519: Signature25519, signatureVrf: VrfProof) + extends ABIEncodable[StaticStruct] + with VRFDecoder{ + require(rewardShare >= 0, "reward share expected to be non negative.") + require(rewardShare <= MAX_REWARD_SHARE, s"reward share expected to be $MAX_REWARD_SHARE at most") + + override def asABIType(): StaticStruct = { + + val listOfParams: util.List[Type[_]] = new util.ArrayList() + + val vrfPublicKeyBytes = vrfPublicKeyToAbi(forgerPublicKeys.vrfPublicKey.pubKeyBytes()) + val sign1Bytes = signature25519.bytes + val sign2Bytes = signatureVrf.bytes + + listOfParams.add(new Bytes32(forgerPublicKeys.blockSignPublicKey.bytes())) + listOfParams.add(vrfPublicKeyBytes._1) + listOfParams.add(vrfPublicKeyBytes._2) + listOfParams.add(new Uint32(rewardShare)) + listOfParams.add(new AbiAddress(rewardAddress.toString)) + listOfParams.add(new Bytes32(util.Arrays.copyOfRange(sign1Bytes, 0, 32))) + listOfParams.add(new Bytes32(util.Arrays.copyOfRange(sign1Bytes, 32, Ed25519.signatureLength()))) + listOfParams.add(new Bytes32(util.Arrays.copyOfRange(sign2Bytes, 0, 32))) + listOfParams.add(new Bytes32(util.Arrays.copyOfRange(sign2Bytes, 32, 64))) + listOfParams.add(new Bytes32(util.Arrays.copyOfRange(sign2Bytes, 64, 96))) + listOfParams.add(new Bytes1(util.Arrays.copyOfRange(sign2Bytes, 96, VrfProof.PROOF_LENGTH))) + + new StaticStruct(listOfParams) + } + + override def toString: String = "%s(forgerPubKeys: %s, rewardShare: %d, rewardsAddress: %s, sign1: %s, sign2: %s)" + .format(this.getClass.toString, forgerPublicKeys, rewardShare, rewardAddress, signature25519, signatureVrf) +} diff --git a/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/RewardsReceivedCmdInputDecoder.scala b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/RewardsReceivedCmdInputDecoder.scala new file mode 100644 index 0000000000..a81f08bcce --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/RewardsReceivedCmdInputDecoder.scala @@ -0,0 +1,60 @@ +package io.horizen.account.state.nativescdata.forgerstakev2 + +import io.horizen.account.abi.{ABIDecoder, ABIEncodable, MsgProcessorInputDecoder} +import io.horizen.account.state.ForgerPublicKeys +import io.horizen.proposition.PublicKey25519Proposition +import org.web3j.abi.TypeReference +import org.web3j.abi.datatypes.generated.{Bytes1, Bytes32, Uint32} +import org.web3j.abi.datatypes.{StaticStruct, Type} + +import java.util + +object RewardsReceivedCmdInputDecoder + extends ABIDecoder[RewardsReceivedCmdInput] + with MsgProcessorInputDecoder[RewardsReceivedCmdInput] + with VRFDecoder { + + override val getListOfABIParamTypes: util.List[TypeReference[Type[_]]] = + org.web3j.abi.Utils.convert( + util.Arrays.asList( + new TypeReference[Bytes32]() {}, + new TypeReference[Bytes32]() {}, + new TypeReference[Bytes1]() {}, + new TypeReference[Uint32]() {}, + new TypeReference[Uint32]() {}, + ), + ) + + override def createType(listOfParams: util.List[Type[_]]): RewardsReceivedCmdInput = { + val forgerPublicKey = new PublicKey25519Proposition(listOfParams.get(0).asInstanceOf[Bytes32].getValue) + val vrfKey = decodeVrfKey(listOfParams.get(1).asInstanceOf[Bytes32], listOfParams.get(2).asInstanceOf[Bytes1]) + val forgerPublicKeys = ForgerPublicKeys(forgerPublicKey, vrfKey) + val consensusEpochStart = listOfParams.get(3).asInstanceOf[Uint32].getValue.intValueExact() + val maxNumOfEpoch = listOfParams.get(4).asInstanceOf[Uint32].getValue.intValueExact() + RewardsReceivedCmdInput(forgerPublicKeys, consensusEpochStart, maxNumOfEpoch) + } + +} + +case class RewardsReceivedCmdInput( + forgerPublicKeys: ForgerPublicKeys, + consensusEpochStart: Int, + maxNumOfEpoch: Int, +) extends ABIEncodable[StaticStruct] { + + override def asABIType(): StaticStruct = { + val forgerPublicKeysAbi = forgerPublicKeys.asABIType() + val listOfParams: util.List[Type[_]] = + new util.ArrayList(forgerPublicKeysAbi.getValue.asInstanceOf[util.List[Type[_]]]) + listOfParams.add(new Uint32(consensusEpochStart)) + listOfParams.add(new Uint32(maxNumOfEpoch)) + new StaticStruct(listOfParams) + } + + override def toString: String = { + s"RewardsReceivedCmdInput(" + + s"forgerPublicKeys: $forgerPublicKeys, " + + s"consensusEpochStart: $consensusEpochStart, " + + s"maxNumOfEpoch: $maxNumOfEpoch)" + } +} diff --git a/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/RewardsReceivedCmdOutput.scala b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/RewardsReceivedCmdOutput.scala new file mode 100644 index 0000000000..ea7ee0b74d --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/RewardsReceivedCmdOutput.scala @@ -0,0 +1,42 @@ +package io.horizen.account.state.nativescdata.forgerstakev2 + +import io.horizen.account.abi.{ABIDecoder, ABIEncodable, MsgProcessorInputDecoder} +import org.web3j.abi.TypeReference +import org.web3j.abi.datatypes.generated.Uint256 +import org.web3j.abi.datatypes.{DynamicArray, DynamicStruct, Type} + +import java.math.BigInteger +import java.util +import scala.collection.JavaConverters +import scala.jdk.CollectionConverters.asScalaBufferConverter + +case class RewardsReceivedCmdOutput(listOfRewards: Seq[BigInteger]) extends ABIEncodable[DynamicStruct] { + + override def asABIType(): DynamicStruct = { + val seqOfStruct = listOfRewards.map(new Uint256(_)) + val listOfStruct = JavaConverters.seqAsJavaList(seqOfStruct) + val theType = classOf[Uint256] + val listOfParams: util.List[Type[_]] = util.Arrays.asList( + new DynamicArray(theType, listOfStruct), + ) + new DynamicStruct(listOfParams) + } + + override def toString: String = + "%s(listOfRewards: %s)" + .format(this.getClass.toString, listOfRewards) +} + +object RewardsReceivedCmdOutputDecoder + extends ABIDecoder[RewardsReceivedCmdOutput] + with MsgProcessorInputDecoder[RewardsReceivedCmdOutput] { + + override val getListOfABIParamTypes: util.List[TypeReference[Type[_]]] = { + org.web3j.abi.Utils.convert(util.Arrays.asList(new TypeReference[DynamicArray[Uint256]]() {})) + } + + override def createType(listOfParams: util.List[Type[_]]): RewardsReceivedCmdOutput = { + val listOfRewards = listOfParams.get(0).asInstanceOf[DynamicArray[Uint256]].getValue + RewardsReceivedCmdOutput(listOfRewards.asScala.map(_.getValue)) + } +} diff --git a/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/SelectByForgerCmdInputDecoder.scala b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/SelectByForgerCmdInputDecoder.scala new file mode 100644 index 0000000000..fe7d09ad19 --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/SelectByForgerCmdInputDecoder.scala @@ -0,0 +1,43 @@ +package io.horizen.account.state.nativescdata.forgerstakev2 + +import io.horizen.account.abi.{ABIDecoder, ABIEncodable, MsgProcessorInputDecoder} +import io.horizen.account.state.ForgerPublicKeys +import io.horizen.proposition.PublicKey25519Proposition +import org.web3j.abi.TypeReference +import org.web3j.abi.datatypes.generated.{Bytes1, Bytes32} +import org.web3j.abi.datatypes.{StaticStruct, Type} + +import java.util + +object SelectByForgerCmdInputDecoder + extends ABIDecoder[SelectByForgerCmdInput] + with MsgProcessorInputDecoder[SelectByForgerCmdInput] + with VRFDecoder{ + + override val getListOfABIParamTypes: util.List[TypeReference[Type[_]]] = + org.web3j.abi.Utils.convert(util.Arrays.asList( + new TypeReference[Bytes32]() {}, + new TypeReference[Bytes32]() {}, + new TypeReference[Bytes1]() {}, + )) + + override def createType(listOfParams: util.List[Type[_]]): SelectByForgerCmdInput = { + val forgerPublicKey = new PublicKey25519Proposition(listOfParams.get(0).asInstanceOf[Bytes32].getValue) + val vrfKey = decodeVrfKey(listOfParams.get(1).asInstanceOf[Bytes32], listOfParams.get(2).asInstanceOf[Bytes1]) + val forgerPublicKeys = ForgerPublicKeys(forgerPublicKey, vrfKey) + SelectByForgerCmdInput(forgerPublicKeys) + } + +} + +case class SelectByForgerCmdInput(forgerPublicKeys: ForgerPublicKeys) extends ABIEncodable[StaticStruct] { + + override def asABIType(): StaticStruct = { + val forgerPublicKeysAbi = forgerPublicKeys.asABIType() + val listOfParams: util.List[Type[_]] = new util.ArrayList(forgerPublicKeysAbi.getValue.asInstanceOf[util.List[Type[_]]]) + new StaticStruct(listOfParams) + } + + override def toString: String = "%s(forgerPubKeys: %s)" + .format(this.getClass.toString, forgerPublicKeys) +} diff --git a/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/StakeStartCmdInputDecoder.scala b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/StakeStartCmdInputDecoder.scala new file mode 100644 index 0000000000..54ea24489e --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/StakeStartCmdInputDecoder.scala @@ -0,0 +1,52 @@ +package io.horizen.account.state.nativescdata.forgerstakev2 + +import io.horizen.account.abi.{ABIDecoder, ABIEncodable, MsgProcessorInputDecoder} +import io.horizen.account.state.ForgerPublicKeys +import io.horizen.evm.Address +import io.horizen.proposition.PublicKey25519Proposition +import org.web3j.abi.TypeReference +import org.web3j.abi.datatypes.generated.{Bytes1, Bytes32} +import org.web3j.abi.datatypes.{StaticStruct, Type, Address => AbiAddress} + +import java.util + +object StakeStartCmdInputDecoder + extends ABIDecoder[StakeStartCmdInput] + with MsgProcessorInputDecoder[StakeStartCmdInput] + with VRFDecoder { + + override val getListOfABIParamTypes: util.List[TypeReference[Type[_]]] = + org.web3j.abi.Utils.convert( + util.Arrays.asList( + new TypeReference[Bytes32]() {}, + new TypeReference[Bytes32]() {}, + new TypeReference[Bytes1]() {}, + new TypeReference[AbiAddress]() {}, + ), + ) + + override def createType(listOfParams: util.List[Type[_]]): StakeStartCmdInput = { + val forgerPublicKey = new PublicKey25519Proposition(listOfParams.get(0).asInstanceOf[Bytes32].getValue) + val vrfKey = decodeVrfKey(listOfParams.get(1).asInstanceOf[Bytes32], listOfParams.get(2).asInstanceOf[Bytes1]) + val forgerPublicKeys = ForgerPublicKeys(forgerPublicKey, vrfKey) + val delegator = new Address(listOfParams.get(3).asInstanceOf[AbiAddress].toString) + StakeStartCmdInput(forgerPublicKeys, delegator) + } + +} + +case class StakeStartCmdInput(forgerPublicKeys: ForgerPublicKeys, delegator: Address) + extends ABIEncodable[StaticStruct] { + + override def asABIType(): StaticStruct = { + val forgerPublicKeysAbi = forgerPublicKeys.asABIType() + val listOfParams: util.List[Type[_]] = + new util.ArrayList(forgerPublicKeysAbi.getValue.asInstanceOf[util.List[Type[_]]]) + listOfParams.add(new AbiAddress(delegator.toString)) + new StaticStruct(listOfParams) + } + + override def toString: String = + "%s(forgerPubKeys: %s, delegator: %s)" + .format(this.getClass.toString, forgerPublicKeys, delegator) +} diff --git a/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/StakeStartCmdOutput.scala b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/StakeStartCmdOutput.scala new file mode 100644 index 0000000000..816ae9c574 --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/StakeStartCmdOutput.scala @@ -0,0 +1,40 @@ +package io.horizen.account.state.nativescdata.forgerstakev2 + +import io.horizen.account.abi.{ABIDecoder, ABIEncodable, MsgProcessorInputDecoder} +import org.web3j.abi.TypeReference +import org.web3j.abi.datatypes.generated.Int32 +import org.web3j.abi.datatypes.{StaticStruct, Type} + +import java.util + +case class StakeStartCmdOutput(epoch: Int) extends ABIEncodable[StaticStruct] { + + override def asABIType(): StaticStruct = { + val listOfParams: util.List[Type[_]] = new util.ArrayList() + // must be Int (with signum) because it carries the semantic 'not there' with the '-1' value + listOfParams.add(new Int32(epoch)) + new StaticStruct(listOfParams) + } + + override def toString: String = + "%s(epoch: %s)" + .format(this.getClass.toString, epoch) +} + +object StakeStartCmdOutputDecoder + extends ABIDecoder[StakeStartCmdOutput] + with MsgProcessorInputDecoder[StakeStartCmdOutput] { + + override val getListOfABIParamTypes: util.List[TypeReference[Type[_]]] = { + org.web3j.abi.Utils.convert( + util.Arrays.asList( + new TypeReference[Int32]() {}, + ), + ) + } + + override def createType(listOfParams: util.List[Type[_]]): StakeStartCmdOutput = { + val epoch = listOfParams.get(0).asInstanceOf[Int32].getValue.intValueExact() + StakeStartCmdOutput(epoch) + } +} diff --git a/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/StakeStorage.scala b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/StakeStorage.scala new file mode 100644 index 0000000000..41ebeddee4 --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/StakeStorage.scala @@ -0,0 +1,584 @@ +package io.horizen.account.state.nativescdata.forgerstakev2 + +import com.google.common.primitives.Bytes +import io.horizen.account.network.{ForgerInfo, ForgerInfoSerializer} +import io.horizen.account.proposition.{AddressProposition, AddressPropositionSerializer} +import io.horizen.account.state.NativeSmartContractMsgProcessor.NULL_HEX_STRING_32 +import io.horizen.account.state._ +import io.horizen.account.state.nativescdata.forgerstakev2.StakeStorage.{ACCOUNT, ForgerKey} +import io.horizen.account.utils.WellKnownAddresses.FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS +import io.horizen.account.utils.{BigIntegerUInt256, BigIntegerUtil, ZenWeiConverter} +import io.horizen.consensus.ForgingStakeInfo +import io.horizen.evm.Address +import io.horizen.proposition.{PublicKey25519Proposition, VrfPublicKey} +import io.horizen.utils.BytesUtils +import sparkz.core.serialization.{BytesSerializable, SparkzSerializer} +import sparkz.crypto.hash.Blake2b256 +import sparkz.util.serialization.{Reader, Writer} + +import java.math.BigInteger +import scala.collection.mutable.ListBuffer + + +object StakeStorage { + + val ACCOUNT: Address = FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS + + val ActivationKey: Array[Byte] = Blake2b256.hash("Activate") + + def isActive(view: BaseAccountStateView): Boolean = { + val activated = view.getAccountStorage(ACCOUNT, ActivationKey) + new BigInteger(1, activated) == BigInteger.ONE + } + + def setActive(view: BaseAccountStateView): Unit = { + val activated = BigIntegerUtil.toUint256Bytes(BigInteger.ONE) + view.updateAccountStorage(ACCOUNT, ActivationKey, activated) + } + + + def addForger(view: BaseAccountStateView, + blockSignProposition: PublicKey25519Proposition, + vrfPublicKey: VrfPublicKey, + rewardShare: Int, + rewardAddress: Address, + epochNumber: Int, + delegatorPublicKey: Address, + stakedAmount: BigInteger): Unit = { + + val forgerKey = ForgerKey(blockSignProposition, vrfPublicKey) + + if (ForgerMap.existsForger(view, forgerKey)) + throw new ExecutionRevertedException(s"Forger already registered.") + + ForgerMap.addForger(view, forgerKey, blockSignProposition, vrfPublicKey, rewardShare, rewardAddress) + + val forgerHistory = ForgerStakeHistory(forgerKey) + forgerHistory.addCheckpoint(view, epochNumber, stakedAmount) + + val delegatorChkSumAddress = DelegatorKey(delegatorPublicKey) + val stakeHistory = StakeHistory(forgerKey, delegatorChkSumAddress) + stakeHistory.addCheckpoint(view, epochNumber, stakedAmount) + + addNewDelegator(view, forgerKey, delegatorChkSumAddress) + + } + + def updateForger(view: BaseAccountStateView, blockSignProposition: PublicKey25519Proposition, vrfPublicKey: VrfPublicKey, rewardShare: Int, rewardAddress: Address): Unit = { + val forgerKey = ForgerKey(blockSignProposition, vrfPublicKey) + val forger = ForgerMap.getForgerOption(view, forgerKey).getOrElse(throw new ExecutionRevertedException("Forger doesn't exist.")) + if ((forger.rewardShare > 0) || (forger.rewardAddress.address() != Address.ZERO)) + throw new ExecutionRevertedException("Forger has already set reward share and reward address.") + ForgerMap.updateForger(view, forgerKey, blockSignProposition, vrfPublicKey, rewardShare, rewardAddress) + } + + private def addNewDelegator(view: BaseAccountStateView, forgerKey: ForgerKey, delegator: DelegatorKey): Unit = { + val listOfDelegators = DelegatorList(forgerKey) + listOfDelegators.addDelegator(view, delegator) + + val listOfForgers = DelegatorListOfForgerKeys(delegator) + listOfForgers.addForgerKey(view, forgerKey) + } + + def getPagedListOfForgers(view: BaseAccountStateView, startPos: Int, pageSize: Int): PagedForgersListResponse = + ForgerMap.getPagedListOfForgers(view, startPos, pageSize) + + def getForger(view: BaseAccountStateView, signKey: PublicKey25519Proposition, vrfKey: VrfPublicKey): Option[ForgerInfo] = { + val forgerKey = ForgerKey(signKey, vrfKey) + ForgerMap.getForgerOption(view, forgerKey) + } + + def addStake(view: BaseAccountStateView, + signKey: PublicKey25519Proposition, + vrfPublicKey: VrfPublicKey, + epochNumber: Int, + delegatorPublicKey: Address, + stakedAmount: BigInteger): Unit = { + + val forgerKey = ForgerKey(signKey, vrfPublicKey) + val forgerHistory = ForgerStakeHistory(forgerKey) + val forgerHistorySize = forgerHistory.getSize(view) + if (forgerHistorySize == 0) + throw new ExecutionRevertedException(s"Forger doesn't exist.") + + val addToStake = (latestStake: BigInteger) => latestStake.add(stakedAmount) + forgerHistory.updateOrAddCheckpoint(view, forgerHistorySize, epochNumber, addToStake) + + val delegatorChkSumAddress = DelegatorKey(delegatorPublicKey) + val stakeHistory = StakeHistory(forgerKey, delegatorChkSumAddress) + val stakeHistorySize = stakeHistory.getSize(view) + stakeHistory.updateOrAddCheckpoint(view, stakeHistorySize, epochNumber, addToStake) + if (stakeHistorySize == 0) + addNewDelegator(view, forgerKey, delegatorChkSumAddress) + } + + def removeStake(view: BaseAccountStateView, + blockSignProposition: PublicKey25519Proposition, + vrfPublicKey: VrfPublicKey, + epochNumber: Int, + delegatorPublicKey: Address, + stakedAmount: BigInteger): Unit = { + val forgerKey = ForgerKey(blockSignProposition, vrfPublicKey) + val forgerHistory = ForgerStakeHistory(forgerKey) + val forgerHistorySize = forgerHistory.getSize(view) + if (forgerHistorySize == 0) + throw new ExecutionRevertedException("Forger doesn't exist.") + val delegatorChkSumAddress = DelegatorKey(delegatorPublicKey) + val stakeHistory = StakeHistory(forgerKey, delegatorChkSumAddress) + val stakeHistorySize = stakeHistory.getSize(view) + if (stakeHistorySize == 0) + throw new ExecutionRevertedException(s"Delegator ${BytesUtils.toHexString(delegatorChkSumAddress.toBytes)} doesn't have stake with the forger.") + + + def subtractStake(latestStake: BigInteger): BigInteger = { + val newAmount = latestStake.subtract(stakedAmount) + if (newAmount.signum() == -1) + throw new ExecutionRevertedException(s"Not enough stake. Claimed $stakedAmount, available $latestStake") + newAmount + } + + stakeHistory.updateOrAddCheckpoint(view, stakeHistorySize, epochNumber, subtractStake) + forgerHistory.updateOrAddCheckpoint(view, forgerHistorySize, epochNumber, subtractStake) + } + + def getForgingStakes(view: BaseAccountStateView): Seq[ForgingStakeInfo] = { + val listOfForgerKeys = ForgerMap.getForgerKeys(view) + listOfForgerKeys.map { forgerKey => + val forger = ForgerMap.getForger(view, forgerKey) + val amount = ForgerStakeHistory(forgerKey).getLatestAmount(view) + ForgingStakeInfo(forger.forgerPublicKeys.blockSignPublicKey, forger.forgerPublicKeys.vrfPublicKey, ZenWeiConverter.convertWeiToZennies(amount)) + } + } + + def getAllForgerStakes(view: BaseAccountStateView): Seq[ForgerStakeData] = { + val listOfForgerKeys = ForgerMap.getForgerKeys(view) + listOfForgerKeys.flatMap { forgerKey => + val forger = ForgerMap.getForger(view, forgerKey) + val delegatorList = DelegatorList(forgerKey) + val delegatorSize = delegatorList.getSize(view) + val listOfStakes: ListBuffer[ForgerStakeData] = ListBuffer() + for (idx <- 0 until delegatorSize) { + val delegator = delegatorList.getDelegatorAt(view, idx) + val stakeHistory = StakeHistory(forgerKey, delegator) + val amount = stakeHistory.getLatestAmount(view) + if (amount.signum() == 1) { + listOfStakes.append(ForgerStakeData(forger.forgerPublicKeys, new AddressProposition(delegator), amount)) + } + } + listOfStakes + } + } + + def getStakeStart(view: BaseAccountStateView, forgerKeys: ForgerPublicKeys, delegator: Address): StakeStartCmdOutput = { + val forgerKey = ForgerKey(forgerKeys.blockSignPublicKey, forgerKeys.vrfPublicKey) + val delegatorKey = DelegatorKey(delegator) + val stakeHistory = StakeHistory(forgerKey, delegatorKey) + if (stakeHistory.getSize(view) == 0) + StakeStartCmdOutput(-1) + else + StakeStartCmdOutput(stakeHistory.getCheckpoint(view, 0).fromEpochNumber) + } + + def getStakeTotal(view: BaseAccountStateView, + forgerKeys: Option[ForgerPublicKeys], + delegator: Option[Address], + consensusEpochStart: Int, + consensusEpochEnd: Int): StakeTotalCmdOutput = { + forgerKeys match { + case Some(forgerKeys) => + val forgerKey = ForgerKey(forgerKeys.blockSignPublicKey, forgerKeys.vrfPublicKey) + val history: BaseStakeHistory = delegator match { + case Some(address) => StakeHistory(forgerKey, DelegatorKey(address)) + case None => ForgerStakeHistory(forgerKey) + } + StakeTotalCmdOutput(getForgerStakesPerEpoch(view, history, consensusEpochStart, consensusEpochEnd)) + + case None => + // sum all forging stakes per epochs + val totalStakesPerEpoch = ForgerMap.getForgerKeys(view).map(ForgerStakeHistory) + .map(history => getForgerStakesPerEpoch(view, history, consensusEpochStart, consensusEpochEnd)) + .transpose + .map(stakes => stakes.foldLeft(BigInteger.ZERO)((a, b) => a.add(b))) + + StakeTotalCmdOutput(totalStakesPerEpoch) + } + } + + private[forgerstakev2] def getForgerStakesPerEpoch(view: BaseAccountStateView, history: BaseStakeHistory, consensusEpochStart: Int, consensusEpochEnd: Int): Seq[BigInteger] = { + // if requested slice completely before the first checkpoint + if (history.getCheckpoint(view, 0).fromEpochNumber > consensusEpochEnd) + return List.fill[BigInteger](consensusEpochEnd - consensusEpochStart + 1)(BigInteger.ZERO) + + var currIndex = checkpointBSearch(view, history, consensusEpochEnd) + var currCheckpoint = history.getCheckpoint(view, currIndex) + val result = ListBuffer[BigInteger]() + for (currEpoch <- consensusEpochEnd to consensusEpochStart by -1) { + result.prepend(currCheckpoint.stakedAmount) + if (currCheckpoint.fromEpochNumber == currEpoch) { + currIndex = currIndex - 1 + if (currIndex == -1) + return List.fill[BigInteger](currEpoch - consensusEpochStart)(BigInteger.ZERO) ++ result + if (currEpoch != consensusEpochStart) + currCheckpoint = history.getCheckpoint(view, currIndex) + } + } + result + } + + /** + * Binary search with optimization for most recent elements - prioritizes top 2√n elements. + */ + private[forgerstakev2] def checkpointBSearch(view: BaseAccountStateView, history: BaseStakeHistory, epoch: Int): Int = { + val length = history.getSize(view) + var low = 0 + var high = length + var mid = 0 + + if (length > 5) { + mid = high - Math.sqrt(high).round.toInt + val checkpointEpochNumber = history.getCheckpoint(view, mid).fromEpochNumber + if (checkpointEpochNumber == epoch) return mid + else if (checkpointEpochNumber > epoch) high = mid + else low = mid + 1 + } + + while (low < high) { + mid = (high + low) / 2 + val checkpointEpochNumber = history.getCheckpoint(view, mid).fromEpochNumber + if (checkpointEpochNumber == epoch) return mid + else if (checkpointEpochNumber > epoch) high = mid + else low = mid + 1 + } + if (high == 0) 0 else high - 1 + } + + def getPagedForgersStakesByForger(view: BaseAccountStateView, forger: ForgerPublicKeys, startPos: Int, pageSize: Int): PagedStakesByForgerResponse = { + if (startPos < 0) + throw new IllegalArgumentException(s"Negative start position: $startPos can not be negative") + if (pageSize <= 0) + throw new IllegalArgumentException(s"Invalid page size $pageSize, must be positive") + + val forgerKey = ForgerKey(forger.blockSignPublicKey, forger.vrfPublicKey) + val listOfDelegators = DelegatorList(forgerKey) + val numOfDelegators = listOfDelegators.getSize(view) + if (startPos == 0 && numOfDelegators == 0) + return PagedStakesByForgerResponse(-1, Seq()) + + if (startPos > numOfDelegators - 1) + throw new IllegalArgumentException(s"Invalid start position reading list of delegators: $startPos, delegators array size: $numOfDelegators") + + var endPos = startPos + pageSize + if (endPos > numOfDelegators) + endPos = numOfDelegators + + val resultList = (startPos until endPos).view.map(index => { + val delegatorKey = listOfDelegators.getDelegatorAt(view, index) + val stakeHistory = StakeHistory(forgerKey, delegatorKey) + val amount = stakeHistory.getLatestAmount(view) + StakeDataDelegator(new AddressProposition(delegatorKey), amount) + }).filter(_.stakedAmount.signum() > 0).toList + + if (endPos == numOfDelegators) { + // tell the caller we are done with the array + endPos = -1 + } + + PagedStakesByForgerResponse(endPos, resultList) + } + + def getPagedForgersStakesByDelegator(view: BaseAccountStateView, delegator: Address, startPos: Int, pageSize: Int): PagedStakesByDelegatorResponse = { + if (startPos < 0) + throw new IllegalArgumentException(s"Negative start position: $startPos") + if (pageSize <= 0) + throw new IllegalArgumentException(s"Invalid page size $pageSize, must be positive") + + val delegatorKey = DelegatorKey(delegator) + val listOfForgers = DelegatorListOfForgerKeys(delegatorKey) + val numOfForgers = listOfForgers.getSize(view) + if (startPos == 0 && numOfForgers == 0) + return PagedStakesByDelegatorResponse(-1, Seq()) + + if (startPos > numOfForgers - 1) + throw new IllegalArgumentException(s"Invalid start position reading list of forgers: $startPos, forgers array size: $numOfForgers") + + var endPos = startPos + pageSize + if (endPos > numOfForgers) + endPos = numOfForgers + + val resultList = (startPos until endPos).view.map(index => { + val forgerKey = listOfForgers.getForgerKey(view, index) + val forger = ForgerMap.getForgerOption(view, forgerKey).getOrElse(throw new ExecutionRevertedException("Forger doesn't exist.")) + val stakeHistory = StakeHistory(forgerKey, delegatorKey) + val amount = stakeHistory.getLatestAmount(view) + StakeDataForger(forger.forgerPublicKeys, amount) + }).filter(_.stakedAmount.signum() > 0).toList + + if (endPos == numOfForgers) { + // tell the caller we are done with the array + endPos = -1 + } + PagedStakesByDelegatorResponse(endPos, resultList) + } + + + class BaseStakeHistory(uid: Array[Byte]) + extends StateDbArray(ACCOUNT, Blake2b256.hash(Bytes.concat(uid, "History".getBytes("UTF-8")))) { + + def addCheckpoint(view: BaseAccountStateView, epoch: Int, stakeAmount: BigInteger): Unit = { + val checkpoint = StakeCheckpoint(epoch, stakeAmount) + append(view, checkpointToPaddedBytes(checkpoint)) + } + + def updateOrAddCheckpoint(view: BaseAccountStateView, historySize: Int, epoch: Int, op: BigInteger => BigInteger): Unit = { + if (historySize > 0) { + val lastElemIndex = historySize - 1 + val lastCheckpoint = getCheckpoint(view, lastElemIndex) + val newAmount = op(lastCheckpoint.stakedAmount) + if (lastCheckpoint.fromEpochNumber == epoch) { + // Let's check if the newAmount is the same of the previous checkpoint. In that case, this last checkpoint + // is removed. + val secondLastCheckpointIdx = lastElemIndex - 1 + if (secondLastCheckpointIdx > -1 && getCheckpoint(view, secondLastCheckpointIdx).stakedAmount == newAmount) { + // Rollback to second last checkpoint + updateSize(view, historySize - 1) + // TODO It is not necessary to remove the last element in the history, because it will be overwritten sooner or later. + // However, it may save some gas. To be checked. + } + else { + val checkpoint = StakeCheckpoint(epoch, newAmount) + updateValue(view, lastElemIndex, checkpointToPaddedBytes(checkpoint)) + } + } + else if (lastCheckpoint.fromEpochNumber < epoch) + addCheckpoint(view, epoch, newAmount) + else + throw new ExecutionRevertedException(s"Epoch is in the past: epoch $epoch, last epoch: ${lastCheckpoint.fromEpochNumber}") + } + else if (historySize == 0) { + val newAmount = op(BigInteger.ZERO) + addCheckpoint(view, epoch, newAmount) + } + else + throw new IllegalArgumentException(s"Size cannot be negative: $historySize") + } + + def getCheckpoint(view: BaseAccountStateView, index: Int): StakeCheckpoint = { + val paddedValue = getValue(view, index) + StakeCheckpointSerializer.parseBytes(paddedValue) + } + + def getLatestAmount(view: BaseAccountStateView): BigInteger = { + val size = getSize(view) + if (size == 0) + BigInteger.ZERO + else { + val paddedValue = getValue(view, size - 1) + StakeCheckpointSerializer.parseBytes(paddedValue).stakedAmount + } + } + + private[horizen] def checkpointToPaddedBytes(checkpoint: StakeCheckpoint): Array[Byte] = { + BytesUtils.padRightWithZeroBytes(StakeCheckpointSerializer.toBytes(checkpoint), 32) + } + } + + case class StakeHistory(forgerKey: ForgerKey, delegator: DelegatorKey) + extends BaseStakeHistory(Blake2b256.hash(Bytes.concat(forgerKey.bytes, delegator.toBytes))) + + case class ForgerStakeHistory(forgerKey: ForgerKey) extends BaseStakeHistory(forgerKey.bytes) + + case class DelegatorList(forgerKey: ForgerKey) + extends StateDbArray(ACCOUNT, Blake2b256.hash(Bytes.concat(forgerKey.bytes, "Delegators".getBytes("UTF-8")))) { + + def addDelegator(view: BaseAccountStateView, delegator: DelegatorKey): Unit = { + append(view, BytesUtils.padRightWithZeroBytes(AddressPropositionSerializer.getSerializer.toBytes(new AddressProposition(delegator)), 32)) + } + + def getDelegatorAt(view: BaseAccountStateView, index: Int): DelegatorKey = { + DelegatorKey(AddressPropositionSerializer.getSerializer.parseBytes(getValue(view, index)).address()) + } + + } + + case class DelegatorListOfForgerKeys(delegator: DelegatorKey) + extends StateDbArray(ACCOUNT, Blake2b256.hash(Bytes.concat(delegator.toBytes, "Forgers".getBytes("UTF-8")))) { + + def addForgerKey(view: BaseAccountStateView, forgerKey: ForgerKey): Unit = { + append(view, forgerKey.bytes) + } + + def getForgerKey(view: BaseAccountStateView, idx: Int): ForgerKey = { + ForgerKey(getValue(view, idx)) + } + + } + + case class DelegatorKey(address: Address) + extends Address(new AddressProposition(address).checksumAddress()) + + case class ForgerKey(bytes: Array[Byte]) { + + override def hashCode(): Int = java.util.Arrays.hashCode(bytes) + + override def equals(obj: Any): Boolean = { + obj match { + case obj: ForgerKey => bytes.sameElements(obj.bytes) + case _ => false + } + } + + } + + object ForgerKey { + def apply(blockSignProposition: PublicKey25519Proposition, vrfPublicKey: VrfPublicKey): ForgerKey = + ForgerKey(Blake2b256.hash(Bytes.concat(blockSignProposition.pubKeyBytes(), vrfPublicKey.pubKeyBytes()))) + } +} + + +case class StakeCheckpoint( + fromEpochNumber: Int, + stakedAmount: BigInteger) + extends BytesSerializable { + + require(fromEpochNumber >= 0, s"Epoch cannot be a negative number. Passed value: $fromEpochNumber") + require(stakedAmount.signum() != -1, s"Stake cannot be a negative number. Passed value: $stakedAmount") + + override type M = StakeCheckpoint + + override def serializer: SparkzSerializer[StakeCheckpoint] = StakeCheckpointSerializer + + override def toString: String = "%s(fromEpochNumber: %s, stakedAmount: %s)" + .format(this.getClass.toString, fromEpochNumber, stakedAmount) + +} + +object StakeCheckpointSerializer extends SparkzSerializer[StakeCheckpoint] { + + override def serialize(s: StakeCheckpoint, w: Writer): Unit = { + w.putInt(s.fromEpochNumber) + w.putInt(s.stakedAmount.toByteArray.length) + w.putBytes(s.stakedAmount.toByteArray) + } + + override def parse(r: Reader): StakeCheckpoint = { + val fromEpochNumber = r.getInt() + val stakeAmountLength = r.getInt() + val stakeAmount = new BigIntegerUInt256(r.getBytes(stakeAmountLength)).getBigInt + StakeCheckpoint(fromEpochNumber, stakeAmount) + } +} + +object ForgerMap { + private object ListOfForgers extends StateDbArray(ACCOUNT, "Forgers".getBytes("UTF-8")) { + def getForgerKey(view: BaseAccountStateView, idx: Int): ForgerKey = { + ForgerKey(getValue(view, idx)) + } + + def addForgerKey(view: BaseAccountStateView, forgerKey: ForgerKey): Int = { + append(view, forgerKey.bytes) + } + + } + + def existsForger(view: BaseAccountStateView, forgerKey: ForgerKey): Boolean = { + val forgerData = view.getAccountStorage(ACCOUNT, forgerKey.bytes) + // getting a not existing key from state DB using RAW strategy + // gives an array of 32 bytes filled with 0, while using CHUNK strategy + // gives an empty array instead + !forgerData.sameElements(NULL_HEX_STRING_32) + } + + def addForger(view: BaseAccountStateView, + forgerKey: ForgerKey, + blockSignProposition: PublicKey25519Proposition, + vrfPublicKey: VrfPublicKey, + rewardShare: Int, + rewardAddress: Address, + ): Unit = { + ListOfForgers.addForgerKey(view, forgerKey) + + val forgerStakeData = ForgerInfo( + ForgerPublicKeys(blockSignProposition, vrfPublicKey), rewardShare, new AddressProposition(rewardAddress)) + + // store the forger stake data + view.updateAccountStorageBytes(ACCOUNT, forgerKey.bytes, + ForgerInfoSerializer.toBytes(forgerStakeData)) + } + + def updateForger(view: BaseAccountStateView, + forgerKey: ForgerKey, + blockSignProposition: PublicKey25519Proposition, + vrfPublicKey: VrfPublicKey, + rewardShare: Int, + rewardAddress: Address, + ): Unit = { + + val forgerStakeData = ForgerInfo( + ForgerPublicKeys(blockSignProposition, vrfPublicKey), rewardShare, new AddressProposition(rewardAddress)) + + // store the forger stake data + view.updateAccountStorageBytes(ACCOUNT, forgerKey.bytes, + ForgerInfoSerializer.toBytes(forgerStakeData)) + } + + + def getSize(view: BaseAccountStateView): Int = ListOfForgers.getSize(view) + + def getForgerKeys(view: BaseAccountStateView): Seq[ForgerKey] = { + val listSize = getSize(view) + (0 until listSize).map(index => { + ListOfForgers.getForgerKey(view, index) + }) + } + + def getForgerOption(view: BaseAccountStateView, forgerKey: ForgerKey): Option[ForgerInfo] = { + if (existsForger(view, forgerKey)) + Some(ForgerInfoSerializer.parseBytes(view.getAccountStorageBytes(ACCOUNT, forgerKey.bytes))) + else + None + } + + def getForger(view: BaseAccountStateView, forgerKey: ForgerKey): ForgerInfo = { + ForgerInfoSerializer.parseBytes(view.getAccountStorageBytes(ACCOUNT, forgerKey.bytes)) + } + + def getPagedListOfForgers(view: BaseAccountStateView, startPos: Int, pageSize: Int): PagedForgersListResponse = { + + if (startPos < 0) + throw new ExecutionRevertedException(s"Invalid startPos input: $startPos can not be negative") + if (pageSize <= 0) + throw new ExecutionRevertedException(s"Invalid page size $pageSize, must be positive") + + val listSize = getSize(view) + + if (startPos == 0 && listSize == 0) + return PagedForgersListResponse(-1, Seq.empty[ForgerInfo]) + + if (startPos > listSize - 1) + throw new ExecutionRevertedException(s"Invalid start position: $startPos, array size: $listSize") + + var endPos = startPos + pageSize + if (endPos > listSize) + endPos = listSize + + val listOfElems = (startPos until endPos).map(index => { + val currentKey = ListOfForgers.getForgerKey(view, index) + getForger(view, currentKey) + }) + + if (endPos == listSize) { + // tell the caller we are done with the array + endPos = -1 + } + + PagedForgersListResponse(endPos, listOfElems) + } + +} + +case class PagedForgersListResponse(nextStartPos: Int, forgers: Seq[ForgerInfo]) + +case class PagedStakesByForgerResponse(nextStartPos: Int, stakesData: Seq[StakeDataDelegator]) + +case class PagedStakesByDelegatorResponse(nextStartPos: Int, stakesData: Seq[StakeDataForger]) diff --git a/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/StakeTotalCmdInputDecoder.scala b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/StakeTotalCmdInputDecoder.scala new file mode 100644 index 0000000000..3144fd4a0d --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/StakeTotalCmdInputDecoder.scala @@ -0,0 +1,64 @@ +package io.horizen.account.state.nativescdata.forgerstakev2 + + +import io.horizen.account.abi.{ABIDecoder, ABIEncodable, MsgProcessorInputDecoder} +import io.horizen.account.state.ForgerPublicKeys +import io.horizen.account.state.nativescdata.forgerstakev2.StakeTotalCmdInputDecoder.{emptyAddressPlaceholder, emptyForgerPublicKeysPlaceholder, emptyIntPlaceholder} +import io.horizen.evm.Address +import io.horizen.proposition.{PublicKey25519Proposition, VrfPublicKey} +import org.web3j.abi.TypeReference +import org.web3j.abi.datatypes.generated.{Bytes1, Bytes32, Uint32} +import org.web3j.abi.datatypes.{StaticStruct, Type, Address => AbiAddress} + +import java.util + +object StakeTotalCmdInputDecoder + extends ABIDecoder[StakeTotalCmdInput] + with MsgProcessorInputDecoder[StakeTotalCmdInput] + with VRFDecoder{ + + val emptyForgerPublicKeysPlaceholder: ForgerPublicKeys = { + ForgerPublicKeys( + new PublicKey25519Proposition(new Array[Byte](PublicKey25519Proposition.KEY_LENGTH)), + new VrfPublicKey(new Array[Byte](VrfPublicKey.KEY_LENGTH)) + ) + } + val emptyAddressPlaceholder: Address = Address.ZERO + val emptyIntPlaceholder: Int = 0 + + override val getListOfABIParamTypes: util.List[TypeReference[Type[_]]] = + org.web3j.abi.Utils.convert(util.Arrays.asList( + new TypeReference[Bytes32]() {}, + new TypeReference[Bytes32]() {}, + new TypeReference[Bytes1]() {}, + new TypeReference[AbiAddress]() {}, + new TypeReference[Uint32]() {}, + new TypeReference[Uint32]() {} + )) + + override def createType(listOfParams: util.List[Type[_]]): StakeTotalCmdInput = { + val forgerPublicKey = new PublicKey25519Proposition(listOfParams.get(0).asInstanceOf[Bytes32].getValue) + val vrfKey = decodeVrfKey(listOfParams.get(1).asInstanceOf[Bytes32], listOfParams.get(2).asInstanceOf[Bytes1]) + val forgerPublicKeys = Some(ForgerPublicKeys(forgerPublicKey, vrfKey)).filter(_ != emptyForgerPublicKeysPlaceholder) + val delegator = Some(new Address(listOfParams.get(3).asInstanceOf[AbiAddress].toString)).filter(_ != emptyAddressPlaceholder) + val consensusEpochStart = Some(listOfParams.get(4).asInstanceOf[Uint32].getValue.intValueExact()).filter(_ != emptyIntPlaceholder) + val maxNumOfEpoch = Some(listOfParams.get(5).asInstanceOf[Uint32].getValue.intValueExact()).filter(_ != emptyIntPlaceholder) + StakeTotalCmdInput(forgerPublicKeys, delegator, consensusEpochStart, maxNumOfEpoch) + } + +} + +case class StakeTotalCmdInput(forgerPublicKeys: Option[ForgerPublicKeys], delegator: Option[Address], consensusEpochStart: Option[Int], maxNumOfEpoch: Option[Int]) extends ABIEncodable[StaticStruct] { + + override def asABIType(): StaticStruct = { + val forgerPublicKeysAbi = forgerPublicKeys.getOrElse(emptyForgerPublicKeysPlaceholder).asABIType() + val listOfParams: util.List[Type[_]] = new util.ArrayList(forgerPublicKeysAbi.getValue.asInstanceOf[util.List[Type[_]]]) + listOfParams.add(new AbiAddress(delegator.getOrElse(emptyAddressPlaceholder).toString)) + listOfParams.add(new Uint32(consensusEpochStart.getOrElse(emptyIntPlaceholder))) + listOfParams.add(new Uint32(maxNumOfEpoch.getOrElse(emptyIntPlaceholder))) + new StaticStruct(listOfParams) + } + + override def toString: String = "%s(forgerPubKeys: %s, delegator: %s, consensusEpochStart: %s, maxNumOfEpoch: %s)" + .format(this.getClass.toString, forgerPublicKeys, delegator, consensusEpochStart, maxNumOfEpoch) +} diff --git a/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/StakeTotalCmdOutput.scala b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/StakeTotalCmdOutput.scala new file mode 100644 index 0000000000..3cfdf12538 --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/StakeTotalCmdOutput.scala @@ -0,0 +1,43 @@ +package io.horizen.account.state.nativescdata.forgerstakev2 + +import io.horizen.account.abi.{ABIDecoder, ABIEncodable, MsgProcessorInputDecoder} +import org.web3j.abi.TypeReference +import org.web3j.abi.datatypes.generated.Uint256 +import org.web3j.abi.datatypes.{DynamicArray, DynamicStruct, Type} + +import java.math.BigInteger +import java.util +import scala.collection.JavaConverters +import scala.jdk.CollectionConverters.asScalaBufferConverter + + +case class StakeTotalCmdOutput(listOfStakes: Seq[BigInteger]) extends ABIEncodable[DynamicStruct] { + + override def asABIType(): DynamicStruct = { + val seqOfStruct = listOfStakes.map(new Uint256(_)) + val listOfStruct = JavaConverters.seqAsJavaList(seqOfStruct) + val theType = classOf[Uint256] + val listOfParams: util.List[Type[_]] = util.Arrays.asList( + new DynamicArray(theType, listOfStruct) + ) + new DynamicStruct(listOfParams) + } + + override def toString: String = "%s(listOfStakes: %s)" + .format(this.getClass.toString, listOfStakes) +} + +object StakeTotalCmdOutputDecoder + extends ABIDecoder[StakeTotalCmdOutput] + with MsgProcessorInputDecoder[StakeTotalCmdOutput] { + + override val getListOfABIParamTypes: util.List[TypeReference[Type[_]]] = { + org.web3j.abi.Utils.convert(util.Arrays.asList( + new TypeReference[DynamicArray[Uint256]]() {})) + } + + override def createType(listOfParams: util.List[Type[_]]): StakeTotalCmdOutput = { + val listOfStakes = listOfParams.get(0).asInstanceOf[DynamicArray[Uint256]].getValue + StakeTotalCmdOutput(listOfStakes.asScala.map(_.getValue)) + } +} \ No newline at end of file diff --git a/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/VRFDecoder.scala b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/VRFDecoder.scala new file mode 100644 index 0000000000..d2eeb148c9 --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/VRFDecoder.scala @@ -0,0 +1,20 @@ +package io.horizen.account.state.nativescdata.forgerstakev2 + +import io.horizen.proposition.VrfPublicKey +import org.web3j.abi.datatypes.generated.{Bytes1, Bytes32} + +import java.util + +trait VRFDecoder { + + protected[horizen] def decodeVrfKey(vrfFirst32Bytes: Bytes32, vrfLastByte: Bytes1): VrfPublicKey = { + val vrfinBytes = vrfFirst32Bytes.getValue ++ vrfLastByte.getValue + new VrfPublicKey(vrfinBytes) + } + + protected[horizen] def vrfPublicKeyToAbi(vrfPublicKey: Array[Byte]): (Bytes32, Bytes1) = { + val vrfPublicKeyFirst32Bytes = new Bytes32(util.Arrays.copyOfRange(vrfPublicKey, 0, 32)) + val vrfPublicKeyLastByte = new Bytes1(Array[Byte](vrfPublicKey(32))) + (vrfPublicKeyFirst32Bytes, vrfPublicKeyLastByte) + } +} diff --git a/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/WithdrawCmdInputDecoder.scala b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/WithdrawCmdInputDecoder.scala new file mode 100644 index 0000000000..b04d74ee50 --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/WithdrawCmdInputDecoder.scala @@ -0,0 +1,47 @@ +package io.horizen.account.state.nativescdata.forgerstakev2 + +import io.horizen.account.abi.{ABIDecoder, ABIEncodable, MsgProcessorInputDecoder} +import io.horizen.account.state.ForgerPublicKeys +import io.horizen.proposition.PublicKey25519Proposition +import org.web3j.abi.TypeReference +import org.web3j.abi.datatypes.generated.{Bytes1, Bytes32, Uint256} +import org.web3j.abi.datatypes.{StaticStruct, Type} + +import java.math.BigInteger +import java.util + +object WithdrawCmdInputDecoder + extends ABIDecoder[WithdrawCmdInput] + with MsgProcessorInputDecoder[WithdrawCmdInput] + with VRFDecoder{ + + override val getListOfABIParamTypes: util.List[TypeReference[Type[_]]] = + org.web3j.abi.Utils.convert(util.Arrays.asList( + new TypeReference[Bytes32]() {}, + new TypeReference[Bytes32]() {}, + new TypeReference[Bytes1]() {}, + new TypeReference[Uint256]() {} + )) + + override def createType(listOfParams: util.List[Type[_]]): WithdrawCmdInput = { + val forgerPublicKey = new PublicKey25519Proposition(listOfParams.get(0).asInstanceOf[Bytes32].getValue) + val vrfKey = decodeVrfKey(listOfParams.get(1).asInstanceOf[Bytes32], listOfParams.get(2).asInstanceOf[Bytes1]) + val forgerPublicKeys = ForgerPublicKeys(forgerPublicKey, vrfKey) + val value: BigInteger = listOfParams.get(3).asInstanceOf[Uint256].getValue + WithdrawCmdInput(forgerPublicKeys, value) + } + +} + +case class WithdrawCmdInput(forgerPublicKeys: ForgerPublicKeys, value: BigInteger) extends ABIEncodable[StaticStruct] { + + override def asABIType(): StaticStruct = { + val forgerPublicKeysAbi = forgerPublicKeys.asABIType() + val listOfParams: util.List[Type[_]] = new util.ArrayList(forgerPublicKeysAbi.getValue.asInstanceOf[util.List[Type[_]]]) + listOfParams.add(new Uint256(value)) + new StaticStruct(listOfParams) + } + + override def toString: String = "%s(forgerPubKeys: %s, value: %s)" + .format(this.getClass.toString, forgerPublicKeys, value.toString) +} diff --git a/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/events/ActivateStakeV2.scala b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/events/ActivateStakeV2.scala new file mode 100644 index 0000000000..69220de15c --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/events/ActivateStakeV2.scala @@ -0,0 +1,4 @@ +package io.horizen.account.state.nativescdata.forgerstakev2.events + +case class ActivateStakeV2( +) diff --git a/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/events/DelegateForgerStake.scala b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/events/DelegateForgerStake.scala new file mode 100644 index 0000000000..8f6f981c9a --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/events/DelegateForgerStake.scala @@ -0,0 +1,33 @@ +package io.horizen.account.state.nativescdata.forgerstakev2.events + +import io.horizen.account.state.events.annotation.{Indexed, Parameter} +import io.horizen.evm.Address +import io.horizen.proposition.{PublicKey25519Proposition, VrfPublicKey} +import org.web3j.abi.datatypes.generated.{Bytes1, Bytes32, Uint256} +import org.web3j.abi.datatypes.{Address => AbiAddress} + +import java.math.BigInteger +import scala.annotation.meta.getter + +case class DelegateForgerStake( + @(Parameter @getter)(1) @(Indexed @getter) sender: AbiAddress, + @(Parameter @getter)(2) signPubKey: Bytes32, + @(Parameter @getter)(3) @(Indexed @getter) vrf1: Bytes32, + @(Parameter @getter)(4) @(Indexed @getter) vrf2: Bytes1, + @(Parameter @getter)(5) value: Uint256 +) + +object DelegateForgerStake { + def apply( + sender: Address, + signPubKey: PublicKey25519Proposition, + vrfKey: VrfPublicKey, + value: BigInteger + ): DelegateForgerStake = DelegateForgerStake( + new AbiAddress(sender.toString), + new Bytes32(signPubKey.pubKeyBytes()), + new Bytes32(vrfKey.pubKeyBytes().slice(0, 32)), + new Bytes1(vrfKey.pubKeyBytes().slice(32, 33)), + new Uint256(value) + ) +} diff --git a/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/events/RegisterForger.scala b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/events/RegisterForger.scala new file mode 100644 index 0000000000..85d1a5d8d9 --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/events/RegisterForger.scala @@ -0,0 +1,39 @@ +package io.horizen.account.state.nativescdata.forgerstakev2.events + +import io.horizen.account.state.events.annotation.{Indexed, Parameter} +import io.horizen.evm.Address +import io.horizen.proposition.{PublicKey25519Proposition, VrfPublicKey} +import org.web3j.abi.datatypes.generated.{Bytes1, Bytes32, Uint256, Uint32} +import org.web3j.abi.datatypes.{Address => AbiAddress} + +import java.math.BigInteger +import scala.annotation.meta.getter + +case class RegisterForger( + @(Parameter @getter)(1) @(Indexed @getter) sender: AbiAddress, + @(Parameter @getter)(2) signPubKey: Bytes32, + @(Parameter @getter)(3) @(Indexed @getter) vrf1: Bytes32, + @(Parameter @getter)(4) @(Indexed @getter) vrf2: Bytes1, + @(Parameter @getter)(5) value: Uint256, + @(Parameter @getter)(6) rewardShare: Uint32, + @(Parameter @getter)(7) rewardAddress: AbiAddress +) + +object RegisterForger { + def apply( + sender: Address, + signPubKey: PublicKey25519Proposition, + vrfKey: VrfPublicKey, + value: BigInteger, + rewardShare: Int, + rewardAddress: Address + ): RegisterForger = RegisterForger( + new AbiAddress(sender.toString), + new Bytes32(signPubKey.pubKeyBytes()), + new Bytes32(vrfKey.pubKeyBytes().slice(0, 32)), + new Bytes1(vrfKey.pubKeyBytes().slice(32, 33)), + new Uint256(value), + new Uint32(rewardShare), + new AbiAddress(rewardAddress.toString) + ) +} diff --git a/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/events/UpdateForger.scala b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/events/UpdateForger.scala new file mode 100644 index 0000000000..85d2cfd837 --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/events/UpdateForger.scala @@ -0,0 +1,35 @@ +package io.horizen.account.state.nativescdata.forgerstakev2.events + +import io.horizen.account.state.events.annotation.{Indexed, Parameter} +import io.horizen.evm.Address +import io.horizen.proposition.{PublicKey25519Proposition, VrfPublicKey} +import org.web3j.abi.datatypes.generated.{Bytes1, Bytes32, Uint32} +import org.web3j.abi.datatypes.{Address => AbiAddress} + +import scala.annotation.meta.getter + +case class UpdateForger( + @(Parameter @getter)(1) @(Indexed @getter) sender: AbiAddress, + @(Parameter @getter)(2) signPubKey: Bytes32, + @(Parameter @getter)(3) @(Indexed @getter) vrf1: Bytes32, + @(Parameter @getter)(4) @(Indexed @getter) vrf2: Bytes1, + @(Parameter @getter)(5) rewardShare: Uint32, + @(Parameter @getter)(6) rewardAddress: AbiAddress +) + +object UpdateForger { + def apply( + sender: Address, + signPubKey: PublicKey25519Proposition, + vrfKey: VrfPublicKey, + rewardShare: Int, + rewardAddress: Address + ): UpdateForger = UpdateForger( + new AbiAddress(sender.toString), + new Bytes32(signPubKey.pubKeyBytes()), + new Bytes32(vrfKey.pubKeyBytes().slice(0, 32)), + new Bytes1(vrfKey.pubKeyBytes().slice(32, 33)), + new Uint32(rewardShare), + new AbiAddress(rewardAddress.toString) + ) +} diff --git a/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/events/WithdrawForgerStake.scala b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/events/WithdrawForgerStake.scala new file mode 100644 index 0000000000..1899b83489 --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/nativescdata/forgerstakev2/events/WithdrawForgerStake.scala @@ -0,0 +1,33 @@ +package io.horizen.account.state.nativescdata.forgerstakev2.events + +import io.horizen.account.state.events.annotation.{Indexed, Parameter} +import io.horizen.evm.Address +import io.horizen.proposition.{PublicKey25519Proposition, VrfPublicKey} +import org.web3j.abi.datatypes.generated.{Bytes1, Bytes32, Uint256} +import org.web3j.abi.datatypes.{Address => AbiAddress} + +import java.math.BigInteger +import scala.annotation.meta.getter + +case class WithdrawForgerStake( + @(Parameter @getter)(1) @(Indexed @getter) sender: AbiAddress, + @(Parameter @getter)(2) signPubKey: Bytes32, + @(Parameter @getter)(3) @(Indexed @getter) vrf1: Bytes32, + @(Parameter @getter)(4) @(Indexed @getter) vrf2: Bytes1, + @(Parameter @getter)(5) value: Uint256 +) + +object WithdrawForgerStake { + def apply( + sender: Address, + signPubKey: PublicKey25519Proposition, + vrfKey: VrfPublicKey, + value: BigInteger + ): WithdrawForgerStake = WithdrawForgerStake( + new AbiAddress(sender.toString), + new Bytes32(signPubKey.pubKeyBytes()), + new Bytes32(vrfKey.pubKeyBytes().slice(0, 32)), + new Bytes1(vrfKey.pubKeyBytes().slice(32, 33)), + new Uint256(value) + ) +} diff --git a/sdk/src/main/scala/io/horizen/account/storage/AccountStateMetadataStorage.scala b/sdk/src/main/scala/io/horizen/account/storage/AccountStateMetadataStorage.scala index 476a6c9225..2ab81d02cb 100644 --- a/sdk/src/main/scala/io/horizen/account/storage/AccountStateMetadataStorage.scala +++ b/sdk/src/main/scala/io/horizen/account/storage/AccountStateMetadataStorage.scala @@ -1,8 +1,8 @@ package io.horizen.account.storage -import io.horizen.account.proposition.AddressProposition +import io.horizen.account.state.ForgerPublicKeys import io.horizen.account.state.receipt.EthereumReceipt -import io.horizen.account.utils.AccountBlockFeeInfo +import io.horizen.account.utils.{AccountBlockFeeInfo, ForgerIdentifier} import io.horizen.block.WithdrawalEpochCertificate import io.horizen.consensus.ConsensusEpochNumber import io.horizen.storage.{SidechainStorageInfo, Storage} @@ -56,7 +56,13 @@ class AccountStateMetadataStorage(storage: Storage) override def getTransactionReceipt(txHash: Array[Byte]): Option[EthereumReceipt] = getView.getTransactionReceipt(txHash) - override def getForgerBlockCounters: Map[AddressProposition, Long] = getView.getForgerBlockCounters + override def getForgerBlockCounters: Map[ForgerIdentifier, Long] = getView.getForgerBlockCounters - override def getMcForgerPoolRewards: Map[AddressProposition, BigInteger] = getView.getMcForgerPoolRewards + override def getMcForgerPoolRewards: Map[ForgerIdentifier, BigInteger] = getView.getMcForgerPoolRewards + + override def getForgerRewards( + forgerPublicKeys: ForgerPublicKeys, + consensusEpochStart: Int, + maxNumOfEpochs: Int, + ): Seq[BigInteger] = getView.getForgerRewards(forgerPublicKeys, consensusEpochStart, maxNumOfEpochs) } diff --git a/sdk/src/main/scala/io/horizen/account/storage/AccountStateMetadataStorageReader.scala b/sdk/src/main/scala/io/horizen/account/storage/AccountStateMetadataStorageReader.scala index 73f5ccd864..5b990e81dd 100644 --- a/sdk/src/main/scala/io/horizen/account/storage/AccountStateMetadataStorageReader.scala +++ b/sdk/src/main/scala/io/horizen/account/storage/AccountStateMetadataStorageReader.scala @@ -1,8 +1,8 @@ package io.horizen.account.storage -import io.horizen.account.proposition.AddressProposition +import io.horizen.account.state.ForgerPublicKeys import io.horizen.account.state.receipt.EthereumReceipt -import io.horizen.account.utils.AccountBlockFeeInfo +import io.horizen.account.utils.{AccountBlockFeeInfo, ForgerIdentifier} import io.horizen.block.WithdrawalEpochCertificate import io.horizen.consensus.ConsensusEpochNumber import io.horizen.utils.WithdrawalEpochInfo @@ -33,9 +33,15 @@ trait AccountStateMetadataStorageReader { def getHeight: Int // zero bytes when storage is empty - def getAccountStateRoot: Array[Byte] // 32 bytes, kessack hash + def getAccountStateRoot: Array[Byte] // 32 bytes, keccak hash - def getForgerBlockCounters: Map[AddressProposition, Long] + def getForgerBlockCounters: Map[ForgerIdentifier, Long] - def getMcForgerPoolRewards: Map[AddressProposition, BigInteger] + def getMcForgerPoolRewards: Map[ForgerIdentifier, BigInteger] + + def getForgerRewards( + forgerPublicKeys: ForgerPublicKeys, + consensusEpochStart: Int, + maxNumOfEpochs: Int, + ): Seq[BigInteger] } 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 57106fdab1..fb3e5da63d 100644 --- a/sdk/src/main/scala/io/horizen/account/storage/AccountStateMetadataStorageView.scala +++ b/sdk/src/main/scala/io/horizen/account/storage/AccountStateMetadataStorageView.scala @@ -1,23 +1,23 @@ package io.horizen.account.storage import com.google.common.primitives.{Bytes, Ints} -import io.horizen.account.proposition.AddressProposition -import io.horizen.account.state.{ForgerBlockCountersSerializer, McForgerPoolRewardsSerializer} import io.horizen.account.state.receipt.{EthereumReceipt, EthereumReceiptSerializer} +import io.horizen.account.state.{ForgerBlockCountersSerializer, ForgerPublicKeys, McForgerPoolRewardsSerializer} import io.horizen.account.storage.AccountStateMetadataStorageView.DEFAULT_ACCOUNT_STATE_ROOT -import io.horizen.account.utils.{AccountBlockFeeInfo, AccountBlockFeeInfoSerializer, FeeUtils} +import io.horizen.account.utils.AccountFeePaymentsUtils.DelegatorFeePayment +import io.horizen.account.utils.{AccountBlockFeeInfo, AccountBlockFeeInfoSerializer, FeeUtils, ForgerIdentifier} import io.horizen.block.SidechainBlockBase.GENESIS_BLOCK_PARENT_ID import io.horizen.block.{WithdrawalEpochCertificate, WithdrawalEpochCertificateSerializer} import io.horizen.consensus.{ConsensusEpochNumber, intToConsensusEpochNumber} import io.horizen.storage.Storage import io.horizen.utils.{ByteArrayWrapper, WithdrawalEpochInfo, WithdrawalEpochInfoSerializer, Pair => JPair, _} -import sparkz.core.{VersionTag, bytesToVersion, versionToBytes} +import sparkz.core.{VersionTag, versionToBytes} import sparkz.crypto.hash.Blake2b256 import sparkz.util.{ModifierId, SparkzLogging, bytesToId, idToBytes} import java.math.BigInteger import java.nio.charset.StandardCharsets -import java.util.{UUID, ArrayList => JArrayList} +import java.util.{ArrayList => JArrayList} import scala.collection.mutable.ListBuffer import scala.compat.java8.OptionConverters._ import scala.util.{Failure, Success, Try} @@ -44,12 +44,13 @@ class AccountStateMetadataStorageView(storage: Storage) extends AccountStateMeta private[horizen] var lastCertificateSidechainBlockIdOpt: Option[ModifierId] = None private[horizen] var blockFeeInfoOpt: Option[AccountBlockFeeInfo] = None private[horizen] var consensusEpochOpt: Option[ConsensusEpochNumber] = None - private[horizen] var forgerBlockCountersOpt: Option[Map[AddressProposition, Long]] = None - private[horizen] var mcForgerPoolRewardsOpt: Option[Map[AddressProposition, BigInteger]] = None + private[horizen] var forgerBlockCountersOpt: Option[Map[ForgerIdentifier, Long]] = None + private[horizen] var mcForgerPoolRewardsOpt: Option[Map[ForgerIdentifier, BigInteger]] = None private[horizen] var accountStateRootOpt: Option[Array[Byte]] = None private[horizen] var receiptsOpt: Option[Seq[EthereumReceipt]] = None //Contains the base fee to be used when forging the next block private[horizen] var nextBaseFeeOpt: Option[BigInteger] = None + private[horizen] var delegatorPaymentsSeq: Seq[(ByteArrayWrapper, BigInteger)] = Seq.empty // all getters same as in StateMetadataStorage, but looking first in the cached/dirty entries in memory @@ -277,6 +278,7 @@ class AccountStateMetadataStorageView(storage: Storage) extends AccountStateMeta accountStateRootOpt = None receiptsOpt = None nextBaseFeeOpt = None + delegatorPaymentsSeq = Seq.empty } private[horizen] def saveToStorage(version: ByteArrayWrapper): Unit = { @@ -317,7 +319,7 @@ class AccountStateMetadataStorageView(storage: Storage) extends AccountStateMeta updateList.add(new JPair(getBlockFeeInfoKey(epochInfo.epoch, nextBlockFeeInfoCounter), new ByteArrayWrapper(AccountBlockFeeInfoSerializer.toBytes(feeInfo)))) - } + } ) // Update Consensus related data @@ -386,6 +388,11 @@ class AccountStateMetadataStorageView(storage: Storage) extends AccountStateMeta nextBaseFeeOpt.foreach(baseFee => updateList.add(new JPair(baseFeeKey, new ByteArrayWrapper(baseFee.toByteArray)))) + delegatorPaymentsSeq.foreach { + case (key, value) => + updateList.add(new JPair(key, new ByteArrayWrapper(getForgerReward(key).add(value).toByteArray))) + } + storage.update(version, updateList, removeList) } @@ -416,53 +423,95 @@ class AccountStateMetadataStorageView(storage: Storage) extends AccountStateMeta new ByteArrayWrapper(Blake2b256.hash(key)) } - def updateForgerBlockCounter(forgerPublicKey: AddressProposition): Unit = { - val counters: Map[AddressProposition, Long] = getForgerBlockCounters + def updateForgerBlockCounter(forgerPublicKey: ForgerIdentifier): Unit = { + val counters: Map[ForgerIdentifier, Long] = getForgerBlockCounters val existingCount: Long = counters.getOrElse(forgerPublicKey, 0) forgerBlockCountersOpt = Some(counters.updated(forgerPublicKey, existingCount + 1)) } - def updateMcForgerPoolRewards(forgerPoolRewards: Map[AddressProposition, BigInteger]): Unit = { + def updateMcForgerPoolRewards(forgerPoolRewards: Map[ForgerIdentifier, BigInteger]): Unit = { mcForgerPoolRewardsOpt = Some(forgerPoolRewards) } - override def getForgerBlockCounters: Map[AddressProposition, Long] = { + override def getForgerBlockCounters: Map[ForgerIdentifier, Long] = { forgerBlockCountersOpt.getOrElse(getForgerBlockCountersFromStorage) } - private[horizen] def getForgerBlockCountersFromStorage: Map[AddressProposition, Long] = { + private[horizen] def getForgerBlockCountersFromStorage: Map[ForgerIdentifier, Long] = { storage.get(getForgerBlockCountersKey).asScala match { case Some(baw) => ForgerBlockCountersSerializer.parseBytesTry(baw.data) match { case Success(counters) => counters case Failure(e) => log.error("Failed to parse forger block counters from storage", e) - Map.empty[AddressProposition, Long] + Map.empty[ForgerIdentifier, Long] } - case _ => Map.empty[AddressProposition, Long] + case _ => Map.empty[ForgerIdentifier, Long] } } - override def getMcForgerPoolRewards: Map[AddressProposition, BigInteger] = { + override def getMcForgerPoolRewards: Map[ForgerIdentifier, BigInteger] = { mcForgerPoolRewardsOpt.getOrElse(getMcForgerPoolRewardsFromStorage) } - private[horizen] def getMcForgerPoolRewardsFromStorage: Map[AddressProposition, BigInteger] = { + private[horizen] def getMcForgerPoolRewardsFromStorage: Map[ForgerIdentifier, BigInteger] = { storage.get(getMcForgerPoolRewardsKey).asScala match { case Some(baw) => McForgerPoolRewardsSerializer.parseBytesTry(baw.data) match { case Success(rewards) => rewards case Failure(exception) => log.error("Error while mc forger pool rewards parsing.", exception) - Map.empty[AddressProposition, BigInteger] + Map.empty[ForgerIdentifier, BigInteger] + } + case _ => Map.empty[ForgerIdentifier, BigInteger] + } + } + + override def getForgerRewards( + forgerPublicKeys: ForgerPublicKeys, + consensusEpochStart: Int, + maxNumOfEpochs: Int, + ): Seq[BigInteger] = { + (0 until maxNumOfEpochs).map { epoch => + getForgerReward(getForgerRewardsKey(forgerPublicKeys, consensusEpochStart + epoch)) + } + } + + private def getForgerReward(forgerRewardsKey: ByteArrayWrapper) = { + storage.get(forgerRewardsKey).asScala match { + case Some(baw) => + Try(new BigInteger(baw.data)) match { + case Success(rewards) => rewards + case Failure(exception) => + log.error("Error while forger rewards parsing.", exception) + BigInteger.ZERO } - case _ => Map.empty[AddressProposition, BigInteger] + case _ => BigInteger.ZERO } } + def updateForgerDelegatorPayments( + delegatorPayments: Seq[DelegatorFeePayment], + consensusEpochNumber: ConsensusEpochNumber + ): Unit = { + delegatorPaymentsSeq = delegatorPayments.map { delegatorPayment => + val forgerPublicKey = delegatorPayment.forgerKeys + val forgerRewardsKey = getForgerRewardsKey(forgerPublicKey, consensusEpochNumber) + (forgerRewardsKey, delegatorPayment.feePayment.value) + } + } def resetForgerBlockCounters(): Unit = { - forgerBlockCountersOpt = Some(Map.empty[AddressProposition, Long]) + forgerBlockCountersOpt = Some(Map.empty[ForgerIdentifier, Long]) + } + + private[horizen] def getForgerRewardsKey(forgerKeys: ForgerPublicKeys, consensusEpochNumber: Int): ByteArrayWrapper = { + calculateKey(Bytes.concat( + "forgerRewardsKey".getBytes(StandardCharsets.UTF_8), + forgerKeys.blockSignPublicKey.bytes(), + forgerKeys.vrfPublicKey.bytes(), + Ints.toByteArray(consensusEpochNumber) + )) } private[horizen] def getTopQualityCertificateKey(referencedWithdrawalEpoch: Int): ByteArrayWrapper = { diff --git a/sdk/src/main/scala/io/horizen/account/storage/MsgProcessorMetadataStorageReader.scala b/sdk/src/main/scala/io/horizen/account/storage/MsgProcessorMetadataStorageReader.scala new file mode 100644 index 0000000000..b2f7541b4b --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/storage/MsgProcessorMetadataStorageReader.scala @@ -0,0 +1,14 @@ +package io.horizen.account.storage + +import io.horizen.account.state.ForgerPublicKeys + +import java.math.BigInteger + +// minimal reader interface to access metadata storage from native smart contracts +trait MsgProcessorMetadataStorageReader { + def getForgerRewards( + forgerPublicKeys: ForgerPublicKeys, + consensusEpochStart: Int, + maxNumOfEpochs: Int, + ): Seq[BigInteger] +} diff --git a/sdk/src/main/scala/io/horizen/account/utils/AccountBlockFeeInfo.scala b/sdk/src/main/scala/io/horizen/account/utils/AccountBlockFeeInfo.scala index 1c5b87f151..8a4e96b927 100644 --- a/sdk/src/main/scala/io/horizen/account/utils/AccountBlockFeeInfo.scala +++ b/sdk/src/main/scala/io/horizen/account/utils/AccountBlockFeeInfo.scala @@ -2,19 +2,25 @@ package io.horizen.account.utils import com.fasterxml.jackson.annotation.JsonView import io.horizen.account.proposition.{AddressProposition, AddressPropositionSerializer} +import io.horizen.account.state.ForgerPublicKeys import io.horizen.json.Views +import io.horizen.proposition.{PublicKey25519PropositionSerializer, VrfPublicKeySerializer} import sparkz.core.serialization.{BytesSerializable, SparkzSerializer} import sparkz.util.serialization.{Reader, Writer} import java.math.BigInteger @JsonView(Array(classOf[Views.Default])) -case class AccountBlockFeeInfo(baseFee: BigInteger, forgerTips: BigInteger, forgerAddress: AddressProposition) extends BytesSerializable { +case class AccountBlockFeeInfo( + baseFee: BigInteger, + forgerTips: BigInteger, + forgerAddress: AddressProposition, + forgerKeys: Option[ForgerPublicKeys] = None, +) extends BytesSerializable { override type M = AccountBlockFeeInfo override def serializer: SparkzSerializer[AccountBlockFeeInfo] = AccountBlockFeeInfoSerializer } - object AccountBlockFeeInfoSerializer extends SparkzSerializer[AccountBlockFeeInfo] { override def serialize(obj: AccountBlockFeeInfo, w: Writer): Unit = { val baseFeeByteArray = obj.baseFee.toByteArray @@ -24,6 +30,10 @@ object AccountBlockFeeInfoSerializer extends SparkzSerializer[AccountBlockFeeInf w.putInt(forgerTipsByteArray.length) w.putBytes(forgerTipsByteArray) AddressPropositionSerializer.getSerializer.serialize(obj.forgerAddress, w) + obj.forgerKeys.foreach { keys => + PublicKey25519PropositionSerializer.getSerializer.serialize(keys.blockSignPublicKey, w) + VrfPublicKeySerializer.getSerializer.serialize(keys.vrfPublicKey, w) + } } override def parse(r: Reader): AccountBlockFeeInfo = { @@ -34,7 +44,12 @@ object AccountBlockFeeInfoSerializer extends SparkzSerializer[AccountBlockFeeInf val forgerTips = new BigIntegerUInt256(r.getBytes(forgerTipsLength)).getBigInt val forgerRewardKey: AddressProposition = AddressPropositionSerializer.getSerializer.parse(r) - - AccountBlockFeeInfo(baseFee, forgerTips, forgerRewardKey) + r.remaining match { + case 0 => AccountBlockFeeInfo(baseFee, forgerTips, forgerRewardKey) + case _ => + val blockSignPublicKey = PublicKey25519PropositionSerializer.getSerializer.parse(r) + val vrfPublicKey = VrfPublicKeySerializer.getSerializer.parse(r) + AccountBlockFeeInfo(baseFee, forgerTips, forgerRewardKey, Some(ForgerPublicKeys(blockSignPublicKey,vrfPublicKey))) + } } } diff --git a/sdk/src/main/scala/io/horizen/account/utils/AccountFeePaymentsUtils.scala b/sdk/src/main/scala/io/horizen/account/utils/AccountFeePaymentsUtils.scala index 8415549aa5..886785caf6 100644 --- a/sdk/src/main/scala/io/horizen/account/utils/AccountFeePaymentsUtils.scala +++ b/sdk/src/main/scala/io/horizen/account/utils/AccountFeePaymentsUtils.scala @@ -1,15 +1,19 @@ package io.horizen.account.utils -import io.horizen.account.proposition.AddressProposition +import io.horizen.account.network.ForgerInfo +import io.horizen.account.state.{ForgerPublicKeys, ForgerStakeV2MsgProcessor} import io.horizen.evm.{StateDB, TrieHasher} +import io.horizen.params.NetworkParams import java.math.BigInteger object AccountFeePaymentsUtils { val DEFAULT_ACCOUNT_FEE_PAYMENTS_HASH: Array[Byte] = StateDB.EMPTY_ROOT_HASH.toBytes + val MC_DISTRIBUTION_CAP_DIVIDER: BigInteger = BigInteger.valueOf(10) + val TOTAL_SHARE: BigInteger = BigInteger.valueOf(ForgerStakeV2MsgProcessor.MAX_REWARD_SHARE) def calculateFeePaymentsHash(feePayments: Seq[AccountPayment]): Array[Byte] = { - if(feePayments.isEmpty) { + if (feePayments.isEmpty) { // No fees for the whole epoch, so no fee payments for the Forgers. DEFAULT_ACCOUNT_FEE_PAYMENTS_HASH } else { @@ -18,14 +22,18 @@ object AccountFeePaymentsUtils { } } - def getForgersRewards(blockFeeInfoSeq : Seq[AccountBlockFeeInfo], mcForgerPoolRewards: Map[AddressProposition, BigInteger] = Map.empty): Seq[AccountPayment] = { + def getForgersRewards( + blockFeeInfoSeq: Seq[AccountBlockFeeInfo], + mcForgerPoolRewards: Map[ForgerIdentifier, BigInteger] = Map.empty, + ): Seq[ForgerPayment] = { if (blockFeeInfoSeq.isEmpty) - return mcForgerPoolRewards.map(reward => AccountPayment(reward._1, reward._2)).toSeq + return mcForgerPoolRewards.map(reward => ForgerPayment(reward._1, reward._2, reward._2)).toSeq var poolFee: BigInteger = BigInteger.ZERO - val forgersBlockRewards: Seq[AccountPayment] = blockFeeInfoSeq.map(feeInfo => { + val forgersBlockRewards: Seq[ForgerPayment] = blockFeeInfoSeq.map(feeInfo => { poolFee = poolFee.add(feeInfo.baseFee) - AccountPayment(feeInfo.forgerAddress, feeInfo.forgerTips) + val forgerIdentifier = new ForgerIdentifier(feeInfo.forgerAddress, feeInfo.forgerKeys) + ForgerPayment(forgerIdentifier, feeInfo.forgerTips, BigInteger.ZERO) }) // Split poolFee in equal parts to be paid to forgers. @@ -35,26 +43,97 @@ object AccountFeePaymentsUtils { val rest: Long = divAndRem(1).longValueExact() // Calculate final fee for forger considering forger fee, pool fee and the undistributed satoshis - val allForgersRewards : Seq[AccountPayment] = forgersBlockRewards.zipWithIndex.map { - case (forgerBlockReward: AccountPayment, index: Int) => - val finalForgerFee = forgerBlockReward.value.add(forgerPoolFee).add(if(index < rest) BigInteger.ONE else BigInteger.ZERO) - AccountPayment(forgerBlockReward.address, finalForgerFee) + val allForgersRewards: Seq[ForgerPayment] = forgersBlockRewards.zipWithIndex.map { + case (forgerBlockReward: ForgerPayment, index: Int) => + val finalForgerFee = + forgerBlockReward.value.add(forgerPoolFee).add(if (index < rest) BigInteger.ONE else BigInteger.ZERO) + ForgerPayment(forgerBlockReward.identifier, finalForgerFee, BigInteger.ZERO) } // Get all unique forger addresses - val forgerKeys = (allForgersRewards.map(_.address) ++ mcForgerPoolRewards.keys).distinct + val forgerKeys: Seq[ForgerIdentifier] = (allForgersRewards.map(_.identifier) ++ mcForgerPoolRewards.keys).distinct // sum all rewards for per forger address - forgerKeys.map { - forgerKey => { - val forgerTotalFee = allForgersRewards - .filter(info => forgerKey.equals(info.address)) - .foldLeft(BigInteger.ZERO)((sum, info) => sum.add(info.value)) - // add mcForgerPoolReward if exists - val mcForgerPoolReward = mcForgerPoolRewards.getOrElse(forgerKey, BigInteger.ZERO) - // return the resulting entry - AccountPayment(forgerKey, forgerTotalFee.add(mcForgerPoolReward)) + forgerKeys.map { forgerKey => + val forgerTotalFee = allForgersRewards + .filter(info => forgerKey.equals(info.identifier)) + .foldLeft(BigInteger.ZERO)((sum, info) => sum.add(info.value)) + // add mcForgerPoolReward if exists + val mcForgerPoolReward = mcForgerPoolRewards.getOrElse(forgerKey, BigInteger.ZERO) + // return the resulting entry + ForgerPayment(forgerKey, forgerTotalFee.add(mcForgerPoolReward), mcForgerPoolReward) + } + } + + def getForgerAndDelegatorShares( + feePayment: ForgerPayment, + forgerInfo: ForgerInfo, + ): (AccountPayment, Option[DelegatorFeePayment]) = { + val forgerRewardAddress = feePayment.identifier.getAddress + val totalMcReward = feePayment.valueFromMainchain + + if (forgerInfo.rewardShare == 0) { + // No delegator share, all reward goes to the forger + val totalFeeReward = feePayment.value.subtract(totalMcReward) + val forgerPayment = + AccountPayment(forgerRewardAddress, feePayment.value, Some(totalMcReward), Some(totalFeeReward)) + return (forgerPayment, None) + } + + val delegatorShare = BigInteger.valueOf(forgerInfo.rewardShare) + + val delegatorReward = feePayment.value.multiply(delegatorShare).divide(TOTAL_SHARE) + val delegatorMcReward = totalMcReward.multiply(delegatorShare).divide(TOTAL_SHARE) + val delegatorFeeReward = delegatorReward.subtract(delegatorMcReward) + + val forgerReward = feePayment.value.subtract(delegatorReward) + val forgerMcReward = totalMcReward.subtract(delegatorMcReward) + val forgerFeeReward = forgerReward.subtract(forgerMcReward) + + val forgerPayment = AccountPayment(forgerRewardAddress, forgerReward, Some(forgerMcReward), Some(forgerFeeReward)) + val delegatorPayment = DelegatorFeePayment( + AccountPayment(forgerInfo.rewardAddress, delegatorReward, Some(delegatorMcReward), Some(delegatorFeeReward)), + forgerInfo.forgerPublicKeys, + ) + (forgerPayment, Some(delegatorPayment)) + } + + def groupAllPaymentsByAddress( + feePayments: Seq[AccountPayment], + delegatorPayments: Seq[DelegatorFeePayment], + ): Seq[AccountPayment] = { + (feePayments ++ delegatorPayments.map(_.feePayment)) + .groupBy(_.address) + .map { case (address, payments) => + AccountPayment( + address, + payments.map(_.value).foldLeft(BigInteger.ZERO)((a, b) => a.add(b)), + Some(payments.map(_.valueFromMainchain).foldLeft(BigInteger.ZERO)((a, b) => a.add(b.getOrElse(BigInteger.ZERO)))), + Some(payments.map(_.valueFromFees).foldLeft(BigInteger.ZERO)((a, b) => a.add(b.getOrElse(BigInteger.ZERO)))), + ) + }.toSeq + } + + def getMainchainWithdrawalEpochDistributionCap(epochMaxHeight: Long, params: NetworkParams): BigInteger = { + val baseReward = 12.5 * 1e8 + val halvingInterval = params.mcHalvingInterval + val epochLength = params.withdrawalEpochLength + + var mcEpochRewardZennies = 0L + for (height <- epochMaxHeight - epochLength until epochMaxHeight) { + var reward = baseReward.longValue() + val halvings = height / halvingInterval + for (_ <- 1L to halvings) { + reward = reward >> 1 } + mcEpochRewardZennies = mcEpochRewardZennies + reward } + + val mcEpochRewardWei = ZenWeiConverter.convertZenniesToWei(mcEpochRewardZennies) + mcEpochRewardWei.divide(getMcDistributionCapDivider) } + + private def getMcDistributionCapDivider: BigInteger = MC_DISTRIBUTION_CAP_DIVIDER + + case class DelegatorFeePayment(feePayment: AccountPayment, forgerKeys: ForgerPublicKeys) } diff --git a/sdk/src/main/scala/io/horizen/account/utils/AccountPayment.scala b/sdk/src/main/scala/io/horizen/account/utils/AccountPayment.scala index b68eda19f3..2d8bdf9e5e 100644 --- a/sdk/src/main/scala/io/horizen/account/utils/AccountPayment.scala +++ b/sdk/src/main/scala/io/horizen/account/utils/AccountPayment.scala @@ -9,24 +9,50 @@ import sparkz.core.serialization.{BytesSerializable, SparkzSerializer} import java.math.BigInteger @JsonView(Array(classOf[Views.Default])) -case class AccountPayment(address: AddressProposition, value: BigInteger) extends BytesSerializable { +case class AccountPayment(address: AddressProposition, + value: BigInteger, + valueFromMainchain: Option[BigInteger] = None, + valueFromFees: Option[BigInteger] = None) extends BytesSerializable { override type M = AccountPayment + override def serializer: SparkzSerializer[AccountPayment] = AccountPaymentSerializer } object AccountPaymentSerializer extends SparkzSerializer[AccountPayment] { + final val SERIALIZATION_FORMAT_1_4_FLAG = -1 + override def serialize(obj: AccountPayment, w: Writer): Unit = { AddressPropositionSerializer.getSerializer.serialize(obj.address, w) - w.putInt(obj.value.toByteArray.length) - w.putBytes(obj.value.toByteArray) + // porkaround to support old and new serialization format in parallel + if (obj.valueFromMainchain.isDefined && obj.valueFromFees.isDefined) { + w.putInt(SERIALIZATION_FORMAT_1_4_FLAG) //flag to indicate new serialization format is used + w.putInt(obj.value.toByteArray.length) + w.putBytes(obj.value.toByteArray) + w.putInt(obj.valueFromMainchain.get.toByteArray.length) + w.putBytes(obj.valueFromMainchain.get.toByteArray) + w.putInt(obj.valueFromFees.get.toByteArray.length) + w.putBytes(obj.valueFromFees.get.toByteArray) + } else { + w.putInt(obj.value.toByteArray.length) + w.putBytes(obj.value.toByteArray) + } } override def parse(r: Reader): AccountPayment = { val address = AddressPropositionSerializer.getSerializer.parse(r) val valueLength = r.getInt - val value = new BigIntegerUInt256(r.getBytes(valueLength)).getBigInt - - AccountPayment(address, value) + if (valueLength == SERIALIZATION_FORMAT_1_4_FLAG) { + val valueLength = r.getInt + val value = new BigIntegerUInt256(r.getBytes(valueLength)).getBigInt + val valueFromMainchainLength = r.getInt + val valueFromMainchain = new BigIntegerUInt256(r.getBytes(valueFromMainchainLength)).getBigInt + val valueFromFeesLength = r.getInt + val valueFromFees = new BigIntegerUInt256(r.getBytes(valueFromFeesLength)).getBigInt + AccountPayment(address, value, Some(valueFromMainchain), Some(valueFromFees)) + } else { + val value = new BigIntegerUInt256(r.getBytes(valueLength)).getBigInt + AccountPayment(address, value, None, None) + } } } diff --git a/sdk/src/main/scala/io/horizen/account/utils/ForgerIdentifier.scala b/sdk/src/main/scala/io/horizen/account/utils/ForgerIdentifier.scala new file mode 100644 index 0000000000..9aea5f5027 --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/utils/ForgerIdentifier.scala @@ -0,0 +1,32 @@ +package io.horizen.account.utils + +import io.horizen.account.proposition.AddressProposition +import io.horizen.account.state.ForgerPublicKeys + +class ForgerIdentifier( + address: AddressProposition, + forgerKeys: Option[ForgerPublicKeys] = None, +) { + def getAddress: AddressProposition = address + def getForgerKeys: Option[ForgerPublicKeys] = forgerKeys + + override def equals(obj: Any): Boolean = + obj match { + case that: ForgerIdentifier => + if (forgerKeys.isDefined) { + address.equals(that.getAddress) && that.getForgerKeys.isDefined && forgerKeys.get.equals(that.getForgerKeys.get) + } else { + that.getForgerKeys.isEmpty && address.equals(that.getAddress) + } + case _ => false + } + + override def hashCode(): Int = { + if (forgerKeys.isDefined) { + val state = Seq(address, forgerKeys.get) + state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b) + } else { + address.hashCode() + } + } +} diff --git a/sdk/src/main/scala/io/horizen/account/utils/ForgerPayment.scala b/sdk/src/main/scala/io/horizen/account/utils/ForgerPayment.scala new file mode 100644 index 0000000000..ba07866ced --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/utils/ForgerPayment.scala @@ -0,0 +1,8 @@ +package io.horizen.account.utils +import java.math.BigInteger + +case class ForgerPayment( + identifier: ForgerIdentifier, + value: BigInteger, + valueFromMainchain: BigInteger +) 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 a164d21d37..9fadab004a 100644 --- a/sdk/src/main/scala/io/horizen/account/utils/WellKnownAddresses.scala +++ b/sdk/src/main/scala/io/horizen/account/utils/WellKnownAddresses.scala @@ -6,7 +6,8 @@ object WellKnownAddresses { // native smart contract address val WITHDRAWAL_REQ_SMART_CONTRACT_ADDRESS: Address = new Address("0x0000000000000000000011111111111111111111") - val FORGER_STAKE_SMART_CONTRACT_ADDRESS: Address = new Address("0x0000000000000000000022222222222222222222") + val FORGER_STAKE_SMART_CONTRACT_ADDRESS: Address = new Address("0x0000000000000000000022222222222222222222") + val FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS: Address = new Address("0x0000000000000000000022222222222222222333") 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/account/websocket/WebSocketAccountServerEndpoint.scala b/sdk/src/main/scala/io/horizen/account/websocket/WebSocketAccountServerEndpoint.scala index 8f9388f694..6ab9d753ed 100644 --- a/sdk/src/main/scala/io/horizen/account/websocket/WebSocketAccountServerEndpoint.scala +++ b/sdk/src/main/scala/io/horizen/account/websocket/WebSocketAccountServerEndpoint.scala @@ -68,7 +68,7 @@ class WebSocketAccountServerEndpoint() extends SparkzLogging { } } case _ => - val response = WebSocketAccountServerRef.rpcProcessor.processEthRpc(new ObjectMapper().readTree(message)) + val (response, _) = WebSocketAccountServerRef.rpcProcessor.processEthRpc(new ObjectMapper().readTree(message)) WebSocketAccountServerEndpoint.sendRpcResponse(response, session) } } catch { diff --git a/sdk/src/main/scala/io/horizen/api/http/SidechainApiResponse.scala b/sdk/src/main/scala/io/horizen/api/http/SidechainApiResponse.scala index 75f16621f1..b30079ab1a 100644 --- a/sdk/src/main/scala/io/horizen/api/http/SidechainApiResponse.scala +++ b/sdk/src/main/scala/io/horizen/api/http/SidechainApiResponse.scala @@ -31,10 +31,15 @@ object SidechainApiResponse { def apply(result: String): Route = OK(result) + def apply(result: String, hasError: Boolean): Route = + if (hasError) BAD_REQ(result) else OK(result) + def apply(result: Future[String]): Route = OK(result) def apply(result: Either[Throwable, String]): Route = result.fold(SidechainApiError.apply, OK.apply) object OK extends SidechainApiResponse(StatusCodes.OK) + object BAD_REQ extends SidechainApiResponse(StatusCodes.BadRequest) + } diff --git a/sdk/src/main/scala/io/horizen/api/http/route/SidechainNodeApiRoute.scala b/sdk/src/main/scala/io/horizen/api/http/route/SidechainNodeApiRoute.scala index 856c315e63..7a12a59acd 100644 --- a/sdk/src/main/scala/io/horizen/api/http/route/SidechainNodeApiRoute.scala +++ b/sdk/src/main/scala/io/horizen/api/http/route/SidechainNodeApiRoute.scala @@ -248,7 +248,7 @@ case class SidechainNodeApiRoute[ nodeType = nodeTypes, protocolVersion = protocolVersion, agentName = agentName, - sdkVersion = RpcUtils.getClientVersion, + sdkVersion = RpcUtils.getClientVersion(appVersion), nodeVersion = if (appVersion != "") Option(appVersion) else Option.empty, scId = sidechainId, scType = if (params.isNonCeasing) "non ceasing" else "ceasing", diff --git a/sdk/src/main/scala/io/horizen/consensus/package.scala b/sdk/src/main/scala/io/horizen/consensus/package.scala index 0772215e2e..b37058f4bf 100644 --- a/sdk/src/main/scala/io/horizen/consensus/package.scala +++ b/sdk/src/main/scala/io/horizen/consensus/package.scala @@ -4,6 +4,7 @@ import com.google.common.primitives.{Bytes, Ints} import io.horizen.cryptolibprovider.CryptoLibProvider import io.horizen.cryptolibprovider.utils.FieldElementUtils import com.horizen.poseidonnative.PoseidonHash +import io.horizen.utils.ZenCoinsUtils import io.horizen.vrf.VrfOutput import sparkz.util.ModifierId import supertagged.TaggedType @@ -20,6 +21,7 @@ package object consensus { val consensusPreForkLength: Int = 4 + 8 + consensusHardcodedSaltString.length val forgerStakePercentPrecision: BigDecimal = BigDecimal.valueOf(1000000) // where 1 / forgerStakePercentPrecision -- minimal possible forger stake percentage to be able to forge val stakeConsensusDivideMathContext: MathContext = MathContext.DECIMAL128 //shall be used during dividing, otherwise ArithmeticException is thrown in case of irrational number as division result + val minForgerStake: Long = 10 * ZenCoinsUtils.COIN // after fork v 1.4.0 - min amount of zennies required to be a forger val minSecondsInSlot: Int = 10 val maxSecondsInSlot:Int = 300 @@ -74,7 +76,7 @@ package object consensus { VrfMessage @@ resBytes } - private def generateHashAndCleanUp(elements: Array[Byte]*): Array[Byte] = { + def generateHashAndCleanUp(elements: Array[Byte]*): Array[Byte] = { val digest = PoseidonHash.getInstanceConstantLength(elements.length) elements.foreach { element => val fieldElement = FieldElementUtils.elementToFieldElement(element) diff --git a/sdk/src/main/scala/io/horizen/forge/AbstractForgeMessageBuilder.scala b/sdk/src/main/scala/io/horizen/forge/AbstractForgeMessageBuilder.scala index 94306c837c..c68c3b407b 100644 --- a/sdk/src/main/scala/io/horizen/forge/AbstractForgeMessageBuilder.scala +++ b/sdk/src/main/scala/io/horizen/forge/AbstractForgeMessageBuilder.scala @@ -6,6 +6,7 @@ import io.horizen.chain.{AbstractFeePaymentsInfo, MainchainHeaderHash, Sidechain import io.horizen.consensus._ import io.horizen.fork.{ActiveSlotCoefficientFork, ForkManager} import io.horizen.history.AbstractHistory +import io.horizen.metrics.MetricsManager import io.horizen.params.{NetworkParams, RegTestParams} import io.horizen.proof.{Signature25519, VrfProof} import io.horizen.secret.{PrivateKey25519, VrfSecretKey} @@ -46,6 +47,7 @@ abstract class AbstractForgeMessageBuilder[ type ForgeMessageType = GetDataFromCurrentView[ HIS, MS, VL, MP, ForgeResult] + val metricsManager:MetricsManager = MetricsManager.getInstance() def buildForgeMessageForEpochAndSlot(consensusEpochNumber: ConsensusEpochNumber, consensusSlotNumber: ConsensusSlotNumber, mcRefDataRetrievalTimeout: Timeout, forcedTx: Iterable[TX]): ForgeMessageType = { val forgingFunctionForEpochAndSlot: View => ForgeResult = tryToForgeNextBlock(consensusEpochNumber, consensusSlotNumber, mcRefDataRetrievalTimeout, forcedTx) @@ -75,6 +77,8 @@ abstract class AbstractForgeMessageBuilder[ case _ => // checks passed } + val lotteryStart = metricsManager.currentMillis(); + val nextBlockTimestamp = TimeToEpochUtils.getTimeStampForEpochAndSlot(params.sidechainGenesisBlockTimestamp, nextConsensusEpochNumber, nextConsensusSlotNumber) val consensusInfo: FullConsensusEpochInfo = nodeView.history.getFullConsensusEpochInfoForBlock(nextBlockTimestamp, parentBlockId) val totalStake = consensusInfo.stakeConsensusEpochInfo.totalStake @@ -85,6 +89,9 @@ abstract class AbstractForgeMessageBuilder[ nextConsensusEpochNumber, nodeView.vault, nodeView.history, nodeView.state, branchPointInfo, nextBlockTimestamp) .sortWith(_.forgingStakeInfo.stakeAmount > _.forgingStakeInfo.stakeAmount) + // TODO uncomment here and line 113 for tracing intermediate lottery time + //val merklePathTime = metricsManager.currentMillis() - lotteryStart + if (forgingStakeMerklePathInfoSeq.isEmpty) { NoOwnedForgingStake } else { @@ -101,6 +108,11 @@ abstract class AbstractForgeMessageBuilder[ val eligibleForgerOpt = eligibleForgingDataView.headOption //force all forging related calculations + val lotteryTime = metricsManager.currentMillis() - lotteryStart + metricsManager.lotteryDone(lotteryTime) + // log.debug(s"Lottery times - Epoch, Merkle path and total lottery time: $nextConsensusEpochNumber, $merklePathTime, $lotteryTime") + // log.debug(s"Lottery times - Epoch and total lottery time: $nextConsensusEpochNumber, $lotteryTime") + val forgingResult = eligibleForgerOpt .map { case (forgingStakeMerklePathInfo, privateKey25519, vrfProof, vrfOutput) => forgeBlock( diff --git a/sdk/src/main/scala/io/horizen/forge/AbstractForger.scala b/sdk/src/main/scala/io/horizen/forge/AbstractForger.scala index 9e4c451de1..277ec04358 100644 --- a/sdk/src/main/scala/io/horizen/forge/AbstractForger.scala +++ b/sdk/src/main/scala/io/horizen/forge/AbstractForger.scala @@ -9,6 +9,7 @@ import io.horizen.chain.AbstractFeePaymentsInfo import io.horizen.consensus.{ConsensusEpochAndSlot, ConsensusEpochNumber, ConsensusParamsUtil, ConsensusSlotNumber} import io.horizen.forge.AbstractForger.ReceivableMessages.{GetForgingInfo, StartForging, StopForging, TryForgeNextBlockForEpochAndSlot} import io.horizen.history.AbstractHistory +import io.horizen.metrics.MetricsManager import io.horizen.params.NetworkParams import io.horizen.storage.AbstractHistoryStorage import io.horizen.transaction.Transaction @@ -54,6 +55,8 @@ abstract class AbstractForger[ private def forgingInitiatorTimerTask: TimerTask = new TimerTask {override def run(): Unit = tryToCreateBlockNow()} private var timerOpt: Option[Timer] = None + private val metricsManager = MetricsManager.getInstance() + private def startTimer(): Unit = { this.timerOpt match { case Some(_) => log.info("Automatically forging already had been started") @@ -169,6 +172,7 @@ abstract class AbstractForger[ forgedBlockAsFuture.onComplete{ case Success(ForgeSuccess(block)) => { log.info(s"Got successfully forged block with id ${block.id}") + metricsManager.forgedBlock(metricsManager.currentMillis() - (block.timestamp * 1000)) viewHolderRef ! LocallyGeneratedModifier(block) respondsToOpt.map(respondsTo => respondsTo ! Success(block.id)) } diff --git a/sdk/src/main/scala/io/horizen/fork/ForkManager.scala b/sdk/src/main/scala/io/horizen/fork/ForkManager.scala index 8b04fbacf7..562c17d181 100644 --- a/sdk/src/main/scala/io/horizen/fork/ForkManager.scala +++ b/sdk/src/main/scala/io/horizen/fork/ForkManager.scala @@ -48,6 +48,14 @@ object ForkManager { findActiveFork(forksOfTypeT, consensusEpoch) } + def getFirstActivationEpoch[T <: OptionalSidechainFork : Manifest](): Int = { + assertInitialized() + val forksOfTypeT = optionalSidechainForks.collect({ case (i, fork: T) => (i, fork) }) + // head() throws an exception if the list is empty, which should not happen + val (activationEpoch, _) = forksOfTypeT.head + activationEpoch + } + def init(forkConfigurator: ForkConfigurator, networkName: String): Unit = { if (initialized) throw new IllegalStateException("ForkManager is already initialized.") 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 02610c5a38..7a6f37e8e5 100644 --- a/sdk/src/main/scala/io/horizen/history/validation/ConsensusValidator.scala +++ b/sdk/src/main/scala/io/horizen/history/validation/ConsensusValidator.scala @@ -178,7 +178,13 @@ 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 = { + private[horizen] def verifyForgingStakeInfo( + header: SidechainBlockHeaderBase, + stakeConsensusEpochInfo: StakeConsensusEpochInfo, + vrfOutput: VrfOutput, + percentageForkApplied: Boolean, + activeSlotCoefficient: Double + ): Unit = { log.whenDebugEnabled { s"Verify Forging stake info against root hash: ${BytesUtils.toHexString(stakeConsensusEpochInfo.rootHash)} by merkle path ${header.forgingStakeMerklePath.bytes().deep.mkString}" } @@ -196,7 +202,7 @@ class ConsensusValidator[ val stakeIsEnough = vrfProofCheckAgainstStake(vrfOutput, value, stakeConsensusEpochInfo.totalStake, percentageForkApplied, activeSlotCoefficient) if (!stakeIsEnough) { throw new IllegalArgumentException( - s"Stake value in forger box in block ${header.id} is not enough for to be forger.") + s"Forging stake value in block ${header.id} is not enough for to be forger.") } } } diff --git a/sdk/src/main/scala/io/horizen/params/MainNetParams.scala b/sdk/src/main/scala/io/horizen/params/MainNetParams.scala index c9da23a093..9a21fc7522 100644 --- a/sdk/src/main/scala/io/horizen/params/MainNetParams.scala +++ b/sdk/src/main/scala/io/horizen/params/MainNetParams.scala @@ -44,6 +44,7 @@ case class MainNetParams( override val isNonCeasing: Boolean = false, override val isHandlingTransactionsEnabled: Boolean = true, override val mcBlockRefDelay: Int = 0, + override val mcHalvingInterval: Int = 840000, override val resetModifiersStatus: Boolean = false, override val rewardAddress: Option[AddressProposition] = None, ) extends NetworkParams { diff --git a/sdk/src/main/scala/io/horizen/params/NetworkParams.scala b/sdk/src/main/scala/io/horizen/params/NetworkParams.scala index 20faa78b66..f0a3fc1d07 100644 --- a/sdk/src/main/scala/io/horizen/params/NetworkParams.scala +++ b/sdk/src/main/scala/io/horizen/params/NetworkParams.scala @@ -53,6 +53,7 @@ trait NetworkParams { val isCSWEnabled: Boolean val isHandlingTransactionsEnabled: Boolean = true val mcBlockRefDelay:Int + val mcHalvingInterval:Int // it is overridden only by regtest class for testing purposes val maxHistoryRewritingLength: Int = AbstractHistory.MAX_HISTORY_REWRITING_LENGTH diff --git a/sdk/src/main/scala/io/horizen/params/RegTestParams.scala b/sdk/src/main/scala/io/horizen/params/RegTestParams.scala index 66c1a45ca2..ceb5de727d 100644 --- a/sdk/src/main/scala/io/horizen/params/RegTestParams.scala +++ b/sdk/src/main/scala/io/horizen/params/RegTestParams.scala @@ -41,6 +41,7 @@ case class RegTestParams( override val isNonCeasing: Boolean = false, override val isHandlingTransactionsEnabled: Boolean = true, override val mcBlockRefDelay: Int = 0, + override val mcHalvingInterval: Int = 2000, override val resetModifiersStatus: Boolean = false, override val maxHistoryRewritingLength: Int = MAX_HISTORY_REWRITING_LENGTH, override val rewardAddress: Option[AddressProposition] = None, diff --git a/sdk/src/main/scala/io/horizen/params/TestNetParams.scala b/sdk/src/main/scala/io/horizen/params/TestNetParams.scala index ebe8b14d6a..cb1180ea9a 100644 --- a/sdk/src/main/scala/io/horizen/params/TestNetParams.scala +++ b/sdk/src/main/scala/io/horizen/params/TestNetParams.scala @@ -42,6 +42,7 @@ case class TestNetParams( override val isNonCeasing: Boolean = false, override val isHandlingTransactionsEnabled: Boolean = true, override val mcBlockRefDelay: Int = 0, + override val mcHalvingInterval: Int = 840000, override val resetModifiersStatus: Boolean = false, override val rewardAddress: Option[AddressProposition] = None, ) extends NetworkParams { diff --git a/sdk/src/main/scala/io/horizen/state/BaseStateReader.scala b/sdk/src/main/scala/io/horizen/state/BaseStateReader.scala index 4d58825fa1..4d9ad3ef3d 100644 --- a/sdk/src/main/scala/io/horizen/state/BaseStateReader.scala +++ b/sdk/src/main/scala/io/horizen/state/BaseStateReader.scala @@ -1,6 +1,7 @@ package io.horizen.state import io.horizen.account.state.receipt.EthereumReceipt +import io.horizen.account.utils.ZenWeiConverter.MAX_MONEY_IN_WEI import io.horizen.account.utils.{AccountBlockFeeInfo, AccountPayment} import io.horizen.block.WithdrawalEpochCertificate import io.horizen.consensus.ConsensusEpochNumber @@ -11,7 +12,7 @@ import java.math.BigInteger trait BaseStateReader { def getWithdrawalEpochInfo: WithdrawalEpochInfo def getTopQualityCertificate(referencedWithdrawalEpoch: Int): Option[WithdrawalEpochCertificate] - def getFeePaymentsInfo(withdrawalEpoch: Int, consensusEpochNumber: ConsensusEpochNumber, blockToAppendFeeInfo: Option[AccountBlockFeeInfo] = None): Seq[AccountPayment] + def getFeePaymentsInfo(withdrawalEpoch: Int, consensusEpochNumber: ConsensusEpochNumber, distributionCap: BigInteger = MAX_MONEY_IN_WEI, blockToAppendFeeInfo: Option[AccountBlockFeeInfo] = None): (Seq[AccountPayment], BigInteger) def getConsensusEpochNumber: Option[ConsensusEpochNumber] def getTransactionReceipt(txHash: Array[Byte]): Option[EthereumReceipt] def getNextBaseFee: BigInteger //Contains the base fee to be used when forging the next block diff --git a/sdk/src/main/scala/io/horizen/utxo/SidechainApp.scala b/sdk/src/main/scala/io/horizen/utxo/SidechainApp.scala index c7640d1112..4168b705f6 100644 --- a/sdk/src/main/scala/io/horizen/utxo/SidechainApp.scala +++ b/sdk/src/main/scala/io/horizen/utxo/SidechainApp.scala @@ -3,6 +3,7 @@ package io.horizen.utxo import akka.actor.ActorRef import com.google.inject.Inject import com.google.inject.name.Named +import io.horizen.account.api.http.route import io.horizen.api.http._ import io.horizen.api.http.route.{MainchainBlockApiRoute, SidechainNodeApiRoute, SidechainSubmitterApiRoute} import io.horizen.block.SidechainBlockBase @@ -240,6 +241,7 @@ class SidechainApp @Inject() SidechainCswApiRoute(settings.restApi, nodeViewHolderRef, cswManager, params), SidechainBackupApiRoute(settings.restApi, nodeViewHolderRef, boxIterator, params) ) + override lazy val metricsApiRoute: ApiRoute = null //TODO: add metrics also for UXO val nodeViewProvider: NodeViewProvider[ TX, diff --git a/sdk/src/test/java/io/horizen/account/secret/McSignatureTest.java b/sdk/src/test/java/io/horizen/account/secret/McSignatureTest.java index 7cef0c333d..1ae0ac73e3 100644 --- a/sdk/src/test/java/io/horizen/account/secret/McSignatureTest.java +++ b/sdk/src/test/java/io/horizen/account/secret/McSignatureTest.java @@ -124,7 +124,7 @@ public void mcPrivKeyToMcTaddr() { null, null, null, null, null, false, null, null, 0, false, false, - false, 0, false, 0, Option.empty())); + false, 0, 2000, false, 0, Option.empty())); System.out.println(computedTaddr); @@ -286,7 +286,7 @@ public void testMcSignature() throws SignatureException { null, null, null, null, null, false, null, null, 0,false, false, - false, 0, false, 0, Option.empty())); + false, 0, 2000, false, 0, Option.empty())); assertEquals(taddr, computedTaddr); diff --git a/sdk/src/test/java/io/horizen/utils/BytesUtilsTest.java b/sdk/src/test/java/io/horizen/utils/BytesUtilsTest.java index ec86f01f62..3c254e9495 100644 --- a/sdk/src/test/java/io/horizen/utils/BytesUtilsTest.java +++ b/sdk/src/test/java/io/horizen/utils/BytesUtilsTest.java @@ -247,7 +247,7 @@ public void BytesUtilsTest_toHexString() { @Test public void fromHorizenMcTransparentAddress() { // Test 1: valid MainNet addresses in MainNet network - NetworkParams mainNetParams = new MainNetParams(null, null, null, null, null, 1, 0,100, null, null, CircuitTypes.NaiveThresholdSignatureCircuit(),0, null, null, null, null, null, null, null, false, null, null, 11111111, true, false, true, 0, false, Option.empty()); + NetworkParams mainNetParams = new MainNetParams(null, null, null, null, null, 1, 0,100, null, null, CircuitTypes.NaiveThresholdSignatureCircuit(),0, null, null, null, null, null, null, null, false, null, null, 11111111, true, false, true, 0, 840000, false, Option.empty()); String pubKeyAddressMainNet = "znc3p7CFNTsz1s6CceskrTxKevQLPoDK4cK"; byte[] expectedPublicKeyHashBytesMainNet = BytesUtils.fromHexString("7843a3fcc6ab7d02d40946360c070b13cf7b9795"); @@ -297,7 +297,7 @@ public void fromHorizenMcTransparentAddress() { 0, null,null, null, null, null, null, null, false, null, null, - 11111111, true, false, true, 0, false, Option.empty()); + 11111111, true, false, true, 0, 840000, false, Option.empty()); String pubKeyAddressTestNet = "ztkxeiFhYTS5sueyWSMDa8UiNr5so6aDdYi"; byte[] expectedPublicKeyHashBytesTestNet = BytesUtils.fromHexString("c34e9f61c39bf4fa6225fcf715b59c195c12a6d7"); assertArrayEquals("Horizen base 58 check address expected to have different public key hash.", @@ -324,7 +324,7 @@ public void fromHorizenMcTransparentKeyAddress() { byte[] expectedPublicKeyHashBytes = BytesUtils.fromHexString("7843a3fcc6ab7d02d40946360c070b13cf7b9795"); // Test 1: valid MainNet addresses in MainNet network - NetworkParams mainNetParams = new MainNetParams(null, null, null, null, null, 1, 0,100, null, null, CircuitTypes.NaiveThresholdSignatureCircuit(),0, null, null, null, null, null, null, null, false, null, null, 11111111, true, false, true, 0, false, null); + NetworkParams mainNetParams = new MainNetParams(null, null, null, null, null, 1, 0,100, null, null, CircuitTypes.NaiveThresholdSignatureCircuit(),0, null, null, null, null, null, null, null, false, null, null, 11111111, true, false, true, 0, 840000, false, null); String pubKeyAddressMainNet = BytesUtils.toHorizenPublicKeyAddress(expectedPublicKeyHashBytes, mainNetParams); assertArrayEquals("Horizen base 58 check address expected to have different public key hash.", @@ -346,7 +346,7 @@ public void fromHorizenMcTransparentKeyAddress() { } // Test 3: valid TestNet addresses in TestNet network - NetworkParams testNetParams = new TestNetParams(null, null, null, null, null, 1, 0,100, null, null, CircuitTypes.NaiveThresholdSignatureCircuit(),0, null, null, null, null, null, null, null, false, null, null, 11111111, true, false, true, 0, false, null); + NetworkParams testNetParams = new TestNetParams(null, null, null, null, null, 1, 0,100, null, null, CircuitTypes.NaiveThresholdSignatureCircuit(),0, null, null, null, null, null, null, null, false, null, null, 11111111, true, false, true, 0, 840000, false, null); String pubKeyAddressTestNet = BytesUtils.toHorizenPublicKeyAddress(expectedPublicKeyHashBytes, testNetParams); assertArrayEquals("Horizen base 58 check address expected to have different public key hash.", expectedPublicKeyHashBytes, @@ -385,7 +385,7 @@ public void getPrefixDescription() { @Test public void toHorizenPublicKeyAddress() { // Test 1: valid MainNet addresses in MainNet network - NetworkParams mainNetParams = new MainNetParams(null, null, null, null, null, 1, 0,100, null, null, CircuitTypes.NaiveThresholdSignatureCircuit(),0, null, null, null, null, null, null, null, false, null, null, 11111111, true, false, true, 0, false, Option.empty()); + NetworkParams mainNetParams = new MainNetParams(null, null, null, null, null, 1, 0,100, null, null, CircuitTypes.NaiveThresholdSignatureCircuit(),0, null, null, null, null, null, null, null, false, null, null, 11111111, true, false, true, 0, 840000, false, Option.empty()); byte[] publicKeyHashBytesMainNet = BytesUtils.fromHexString("7843a3fcc6ab7d02d40946360c070b13cf7b9795"); String expectedPubKeyAddressMainNet = "znc3p7CFNTsz1s6CceskrTxKevQLPoDK4cK"; @@ -396,7 +396,7 @@ public void toHorizenPublicKeyAddress() { // Test 2: valid TestNet addresses in TestNet network - NetworkParams testNetParams = new TestNetParams(null, null, null, null, null, 1, 0,100, null, null, CircuitTypes.NaiveThresholdSignatureCircuit(),0, null, null, null, null, null, null, null, false, null, null, 11111111, true, false, true, 0, false, Option.empty()); + NetworkParams testNetParams = new TestNetParams(null, null, null, null, null, 1, 0,100, null, null, CircuitTypes.NaiveThresholdSignatureCircuit(),0, null, null, null, null, null, null, null, false, null, null, 11111111, true, false, true, 0, 840000, false, Option.empty()); byte[] publicKeyHashBytesTestNet = BytesUtils.fromHexString("c34e9f61c39bf4fa6225fcf715b59c195c12a6d7"); String expectedPubKeyAddressTestNet = "ztkxeiFhYTS5sueyWSMDa8UiNr5so6aDdYi"; diff --git a/sdk/src/test/resources/block_fee_info_seq.dsv b/sdk/src/test/resources/block_fee_info_seq.dsv new file mode 100644 index 0000000000..ad6e3a2326 --- /dev/null +++ b/sdk/src/test/resources/block_fee_info_seq.dsv @@ -0,0 +1,921 @@ +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1988560000000000 576682400000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +10894680000000000 2383664100000000 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +998740000000000 74905500000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +4412520000000000 882504000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +925440000000000 69408000000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +1270740000000000 95305500000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +1931740000000000 386348000000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +3039840000000000 607968000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +6787560000000000 3325442613451848 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +1546260000000000 448415400000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0x1448283357e8fb6ea763a78836ffd5517149bf70 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +3012820000000000 741705800000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +2166820000000000 257544000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +2898540000000000 579708000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +766900000000000 153380000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +1988560000000000 576682400000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +4900580000000000 980116000000000 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +6399720000000000 479979000000000 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +3826320000000000 904449000000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +1546020000000000 448345800000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1466800000000000 293360000000000 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +1466560000000000 293312000000000 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +788100000000000 157620000000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1980000000000000 396000000000000 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +4561080000000000 912216000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1546260000000000 448415400000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xfac3dc3dc9b2562d8de5d30c00ad265210cd3d7a +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +2493920000000000 370394000000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +7633000000000000 904972500000000 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +1931740000000000 386348000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1466800000000000 293360000000000 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +1988320000000000 576612800000000 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +4362320000000000 872464000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +3801060000000000 760212000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +1988080000000000 576543200000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +2603720000000000 520744000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +4458160000000000 557270000000000 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1987840000000000 576473600000000 0xfac3dc3dc9b2562d8de5d30c00ad265210cd3d7a +4458160000000000 557270000000000 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +9025640000000000 1805128000000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +1546500000000000 448485000000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +4458160000000000 557270000000000 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +1546500000000000 448485000000000 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +4458160000000000 557270000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +7471220000000000 1299045400000000 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +1466800000000000 293360000000000 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +11833100000000000 2356719400000000 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +2945480000000000 589096000000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +2946200000000000 589240000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1931740000000000 386348000000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +1431500000000000 286300000000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +3798420000000000 759684000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +1546500000000000 448485000000000 0xfac3dc3dc9b2562d8de5d30c00ad265210cd3d7a +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +3074340000000000 562969800000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +1466800000000000 293360000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +1466320000000000 293264000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +7398860000000000 796382000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +6517120000000000 1442587400000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x6b5b8861f260457bb91ba604e8856d6ad7eb17a0 +2408320000000000 702612800000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +5958780000000000 1330919400000000 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +1988560000000000 576682400000000 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +6328880000000000 1265776000000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +766900000000000 153380000000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +3039840000000000 607968000000000 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +1546500000000000 448485000000000 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1466800000000000 293360000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +2334600000000000 606105000000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +1466800000000000 293360000000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +3039840000000000 607968000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +1431740000000000 286348000000000 0xfac3dc3dc9b2562d8de5d30c00ad265210cd3d7a +1988320000000000 576612800000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +2946200000000000 589240000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +6803760000000000 7587825387535500 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +6026680000000000 1205336000000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +3092520000000000 896830800000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +2313400000000000 601843400000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +2933120000000000 586624000000000 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +5924960000000000 627722000000000 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +2746900000000000 549380000000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1431740000000000 286348000000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +5319900000000000 1063980000000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0x1448283357e8fb6ea763a78836ffd5517149bf70 +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +3092520000000000 896830800000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +6468420000000000 1611817800000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +3413840000000000 682768000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +3398300000000000 679660000000000 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +767140000000000 153428000000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x1448283357e8fb6ea763a78836ffd5517149bf70 +2313160000000000 601773800000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +12627080000000000 2982605200000000 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +2930340000000000 586068000000000 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +4865340000000000 973068000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +3976880000000000 1153295200000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +1988320000000000 576612800000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +12027680000000000 2584506400000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +2430820000000000 486164000000000 0xb545a82e49f9c595601c713765a05aea7590b2ed +1528560000000000 114642000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1988560000000000 576682400000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +5346120000000000 1208409000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +4072640000000000 814528000000000 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +1466800000000000 293360000000000 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1931500000000000 386300000000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0x1448283357e8fb6ea763a78836ffd5517149bf70 +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +931920000000000 232980000000000 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x1448283357e8fb6ea763a78836ffd5517149bf70 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x1448283357e8fb6ea763a78836ffd5517149bf70 +6802880000000000 937756400000000 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +12768040000000000 2448879000000000 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +15259360000000000 3272263700000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +7832740000000000 1745453600000000 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +9544480000000000 1265508500000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +1558760000000000 116907000000000 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +1980000000000000 396000000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +3092520000000000 896830800000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +1546500000000000 448485000000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +4479860000000000 1035157000000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1466800000000000 293360000000000 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +4878540000000000 975708000000000 0xb545a82e49f9c595601c713765a05aea7590b2ed +13274140000000000 3370688000000000 0xba2290aeaae3e1ea336431911c97a67ebff46528 +2313640000000000 601913000000000 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +1931740000000000 386348000000000 0x99d270f4a42b296fb888f168a5985e1d9839b064 +12176200000000000 2568940400000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1466800000000000 293360000000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +8830840000000000 2269560400000000 0xb545a82e49f9c595601c713765a05aea7590b2ed +2465540000000000 487115560000000 0xfac3dc3dc9b2562d8de5d30c00ad265210cd3d7a +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +2818300000000000 546750200000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1431980000000000 286396000000000 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +4445040000000000 1028193000000000 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +2254900000000000 450980000000000 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +2278860000000000 455772000000000 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +1931500000000000 386300000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1546500000000000 448485000000000 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +1466800000000000 293360000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +3534580000000000 1025028200000000 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +2603720000000000 520744000000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +1931500000000000 386300000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +1466800000000000 293360000000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +8386680000000000 1816477800000000 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +816860000000000 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1931500000000000 386300000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1466560000000000 293312000000000 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1988560000000000 576682400000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +3413840000000000 682768000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1988080000000000 576543200000000 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +3733580000000000 746716000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +8826820000000000 1944269600000000 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +8934760000000000 1368845400000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +4458160000000000 334362000000000 0x7aaac8a2be835d9b9261018c68dba7166e775096 +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +2995120000000000 407984000000000 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1546500000000000 448485000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +1931740000000000 386348000000000 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +5257620000000000 0 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +9093940000000000 1957973000000000 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +3398300000000000 679660000000000 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +1545540000000000 448206600000000 0xfac3dc3dc9b2562d8de5d30c00ad265210cd3d7a +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1546500000000000 448485000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +3478000000000000 834785000000000 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +1466320000000000 293264000000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +6735140000000000 1347028000000000 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +1931260000000000 386252000000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +2698880000000000 539776000000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +1988560000000000 576682400000000 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +2946200000000000 589240000000000 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +1545780000000000 448276200000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +2279100000000000 455820000000000 0xfac3dc3dc9b2562d8de5d30c00ad265210cd3d7a +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +1466800000000000 293360000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1546260000000000 448415400000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +3534580000000000 1025028200000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +4877700000000000 975540000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +3012820000000000 741705800000000 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +1988560000000000 576682400000000 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +5803260000000000 1149218160000000 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +9396920000000000 2034294040000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +4476840000000000 1034509800000000 0xba2290aeaae3e1ea336431911c97a67ebff46528 +4587960000000000 917592000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +3968080000000000 972564800000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +4880400000000000 976080000000000 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +1931500000000000 386300000000000 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +1546500000000000 448485000000000 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +1988560000000000 576682400000000 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +2930820000000000 586164000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +3455120000000000 869972800000000 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +5618900000000000 1262965000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +9146700000000000 1968525000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +1931980000000000 386396000000000 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +1466800000000000 293360000000000 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1466800000000000 293360000000000 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +2978240000000000 734811400000000 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1931740000000000 386348000000000 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +1988560000000000 576682400000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +4413000000000000 882600000000000 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +1546500000000000 448485000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +1431740000000000 286348000000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +3013060000000000 741775400000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +2279100000000000 455820000000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +2233940000000000 446788000000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1931500000000000 386300000000000 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +1988320000000000 576612800000000 0xba2290aeaae3e1ea336431911c97a67ebff46528 +2930820000000000 586164000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +1527840000000000 114588000000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +2279820000000000 455964000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +1988560000000000 576682400000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +4934520000000000 1165852800000000 0x99d270f4a42b296fb888f168a5985e1d9839b064 +2946200000000000 589240000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +1988320000000000 576612800000000 0xba2290aeaae3e1ea336431911c97a67ebff46528 +6453680000000000 1290736000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0x1448283357e8fb6ea763a78836ffd5517149bf70 +0 0 0xfac3dc3dc9b2562d8de5d30c00ad265210cd3d7a +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1546020000000000 448345800000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +1466560000000000 293312000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x4bae5d28b45b88e1901eb691b5f71f6eadcd8b9f +924960000000000 69372000000000 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +9673360000000000 2478190250000000 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +11654100000000000 1949411750000000 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +2082500000000000 3514218750000000 0xfac3dc3dc9b2562d8de5d30c00ad265210cd3d7a +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +1931500000000000 386300000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1546500000000000 448485000000000 0x90826921d1d4aee8e6b5ae296f80b4145eb434df +3965040000000000 787456800000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +2392000000000000 467464136000000 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +2473680000000000 451532189328000 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +767140000000000 153428000000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +6916180000000000 518713500000000 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +1988080000000000 576543200000000 0xba2290aeaae3e1ea336431911c97a67ebff46528 +2945720000000000 589144000000000 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +10625060000000000 1854754500000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xfac3dc3dc9b2562d8de5d30c00ad265210cd3d7a +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +1546500000000000 448485000000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +8731480000000000 838211000000000 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +767140000000000 153428000000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +1546020000000000 448345800000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +6154960000000000 1370155400000000 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +1546500000000000 448485000000000 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +1528320000000000 114624000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +5386380000000000 1256203200000000 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +2930340000000000 586068000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +1466800000000000 293360000000000 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +767140000000000 153428000000000 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +1546020000000000 448345800000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1931740000000000 386348000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1466560000000000 293312000000000 0xba2290aeaae3e1ea336431911c97a67ebff46528 +6106380000000000 1360439400000000 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0xfac3dc3dc9b2562d8de5d30c00ad265210cd3d7a +1988560000000000 576682400000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +6812380000000000 1362476000000000 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +5407900000000000 1439456000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +6359560000000000 1271912000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1988560000000000 576682400000000 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +2946200000000000 589240000000000 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +2280780000000000 456156000000000 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +3091560000000000 896552400000000 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +2933600000000000 586720000000000 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +6227760000000000 1132189500000000 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +3861980000000000 647553500000000 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0x85f79ba831a8b1716eff9726f2be54e079e75c62 +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0x1448283357e8fb6ea763a78836ffd5517149bf70 +0 0 0x85f79ba831a8b1716eff9726f2be54e079e75c62 +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +5320140000000000 1064028000000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x1448283357e8fb6ea763a78836ffd5517149bf70 +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +6802500000000000 448345800000000 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +4506640000000000 901328000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x85f79ba831a8b1716eff9726f2be54e079e75c62 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +1431740000000000 286348000000000 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +3039600000000000 607920000000000 0x85f79ba831a8b1716eff9726f2be54e079e75c62 +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0xfac3dc3dc9b2562d8de5d30c00ad265210cd3d7a +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +1546500000000000 448485000000000 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +3343380000000000 583251000000000 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0xfac3dc3dc9b2562d8de5d30c00ad265210cd3d7a +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +1466800000000000 293360000000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +1466800000000000 293360000000000 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +1528320000000000 114624000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +1431740000000000 286348000000000 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0xb545a82e49f9c595601c713765a05aea7590b2ed +788100000000000 157620000000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +1519800000000000 303960000000000 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +817100000000000 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x1448283357e8fb6ea763a78836ffd5517149bf70 +0 0 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +4458160000000000 26748960 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1988560000000000 576682400000000 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0xfac3dc3dc9b2562d8de5d30c00ad265210cd3d7a +2930820000000000 586164000000000 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +1546500000000000 448485000000000 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +1546500000000000 448485000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +3013060000000000 741775400000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +3976880000000000 1153295200000000 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +13739280000000000 2747856000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +4513520000000000 902704000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x1448283357e8fb6ea763a78836ffd5517149bf70 +1546500000000000 448485000000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +788100000000000 157620000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x9c98454c8f4d2d38ad824b407be5448cf0fe7b0a +3526260000000000 844415400000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +7913520000000000 870042426748960 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +2945960000000000 589192000000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +1545780000000000 448276200000000 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +1466800000000000 293360000000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +1431740000000000 286348000000000 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +3398300000000000 679660000000000 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1980000000000000 396000000000000 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +10272560000000000 1480992226748960 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +1546500000000000 448485000000000 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +4492460000000000 1037677000000000 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +2933360000000000 586672000000000 0xba2290aeaae3e1ea336431911c97a67ebff46528 +1466800000000000 293360000000000 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +2746900000000000 549380000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +2313640000000000 601913000000000 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +6026200000000000 1205240000000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +3092520000000000 896830800000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1546020000000000 448345800000000 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +2776180000000000 734184800000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +2430820000000000 486164000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +2933360000000000 586672000000000 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +5810840000000000 1136102320000000 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +4344280000000000 842790320000000 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +9022920000000000 1753234610400000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +4585860000000000 1056292200000000 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +1466560000000000 293312000000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +8688560000000000 651642000000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +4572760000000000 1192922000000000 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +4344280000000000 325821000000000 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +4344280000000000 325821000000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +2933600000000000 586720000000000 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +6332840000000000 902503400000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +2945720000000000 589144000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +2220080000000000 444016000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +14263060000000000 2300958320000000 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +4344280000000000 421395160000000 0xfac3dc3dc9b2562d8de5d30c00ad265210cd3d7a +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +5890300000000000 869740960000000 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +3092760000000000 896900400000000 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +5872360000000000 523359305200000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +3455120000000000 869994400000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +4900580000000000 980116000000000 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +9167980000000000 1808657520000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +9279860000000000 0 0x6b5b8861f260457bb91ba604e8856d6ad7eb17a0 +0 0 0xfac3dc3dc9b2562d8de5d30c00ad265210cd3d7a +1980000000000000 396000000000000 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +3837160000000000 737041692800000 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +3863000000000000 772600000000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +9664900000000000 1873539823328000 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +4344280000000000 809415823328000 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +4344280000000000 325821000000000 0x1448283357e8fb6ea763a78836ffd5517149bf70 +420000000000000 31500000000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +1546500000000000 448485000000000 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +5184280000000000 388821000000000 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +7779420000000000 914122000000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +3958280000000000 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +3933460000000000 0 0x1448283357e8fb6ea763a78836ffd5517149bf70 +2407840000000000 607973600000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +10695360000000000 2034072000000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +6580620000000000 629080140000000 0xfac3dc3dc9b2562d8de5d30c00ad265210cd3d7a +6161220000000000 597638340000000 0xfac3dc3dc9b2562d8de5d30c00ad265210cd3d7a +420000000000000 40740000000000 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +1965780000000000 487794000000000 0xba2290aeaae3e1ea336431911c97a67ebff46528 +9528560000000000 896542210400000 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +1546500000000000 448485000000000 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +5811080000000000 689850706044000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +5811080000000000 677955984862680 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +767140000000000 153428000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +2313640000000000 601913000000000 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +3039360000000000 607872000000000 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +4344280000000000 842790320000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1466800000000000 293360000000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +5111420000000000 479249000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +3001380000000000 557601000000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +3535060000000000 1025167400000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +6506600000000000 1579646800000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +2933600000000000 586720000000000 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +2933600000000000 586720000000000 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +2278860000000000 455772000000000 0x99d270f4a42b296fb888f168a5985e1d9839b064 +1534280000000000 306856000000000 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +2198880000000000 439776000000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +4457920000000000 864836480000000 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +4344280000000000 325821000000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +4344280000000000 325821000000000 0x85f79ba831a8b1716eff9726f2be54e079e75c62 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +4344280000000000 325821000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +4344280000000000 325821000000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +13778980000000000 1548125000000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +21234900000000000 2091458400000000 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +1988080000000000 576543200000000 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +4070280000000000 814056000000000 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0xfac3dc3dc9b2562d8de5d30c00ad265210cd3d7a +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +5889900000000000 620740000000000 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +3799140000000000 759828000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0xfac3dc3dc9b2562d8de5d30c00ad265210cd3d7a +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +1528080000000000 114606000000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0x85f79ba831a8b1716eff9726f2be54e079e75c62 +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +1546500000000000 448485000000000 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +9712300000000000 2200829900000000 0x99d270f4a42b296fb888f168a5985e1d9839b064 +8870260000000000 1091969500000000 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +1466800000000000 293360000000000 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +8938020000000000 1369519000000000 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +1931500000000000 386300000000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +7922800000000000 1862908400000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +6411600000000000 1421483400000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +5321100000000000 1064220000000000 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +3686400000000000 737280000000000 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +4838900000000000 483890000000000 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +1431500000000000 286300000000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +6234620000000000 623462000000000 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +12508540000000000 574690500000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +11874060000000000 1413869100000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1466800000000000 293360000000000 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +11123720000000000 963261500000000 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +5257740000000000 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +4569140000000000 854150500000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +3671300000000000 702887900000000 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +3414080000000000 682816000000000 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +0 0 0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c +1466800000000000 293360000000000 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0xfac3dc3dc9b2562d8de5d30c00ad265210cd3d7a +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +3522880000000000 505713500000000 0x6b5b8861f260457bb91ba604e8856d6ad7eb17a0 +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +1682740000000000 126205500000000 0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9 +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +0 0 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +1590900000000000 119317500000000 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0x0eef14a2db10cba19c3e13a4090f0dd3c669e459 +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1682740000000000 126205500000000 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +10793540000000000 1535246000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +11160160000000000 245523000000000 0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b +4417560000000000 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +3837160000000000 383716000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +0 0 0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28 +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc +0 0 0xba2290aeaae3e1ea336431911c97a67ebff46528 +0 0 0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74 +0 0 0x19f78fca9a4ee0dd795bf9a8277aee241bb972db +2664560000000000 226683500000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +1682740000000000 126205500000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x99d270f4a42b296fb888f168a5985e1d9839b064 +11912940000000000 1769579400262857 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +2165880000000000 162441000000000 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x28a48c183df1e30f64673cb4c84d7fd7df4ad506 +0 0 0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a +1682740000000000 126205500000000 0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c +0 0 0x62b1bc6fd237b775138d910274ff2911d7aea5cc \ No newline at end of file diff --git a/sdk/src/test/scala/io/horizen/account/AccountSidechainNodeViewHolderEventTest.scala b/sdk/src/test/scala/io/horizen/account/AccountSidechainNodeViewHolderEventTest.scala index 274e46fc81..74119a2eca 100644 --- a/sdk/src/test/scala/io/horizen/account/AccountSidechainNodeViewHolderEventTest.scala +++ b/sdk/src/test/scala/io/horizen/account/AccountSidechainNodeViewHolderEventTest.scala @@ -16,6 +16,7 @@ import io.horizen.cryptolibprovider.CircuitTypes.NaiveThresholdSignatureCircuit import io.horizen.evm.{Address, Database} import io.horizen.fixtures._ import io.horizen.fork.{ForkManagerUtil, SimpleForkConfigurator} +import io.horizen.metrics.MetricsManager import io.horizen.params.NetworkParams import io.horizen.storage.SidechainSecretStorage import io.horizen.utils.BytesUtils @@ -55,8 +56,10 @@ class AccountSidechainNodeViewHolderEventTest val mockStateDbNonces:TrieMap[Address, BigInteger] = TrieMap[Address, BigInteger]() + @Before def setUp(): Unit = { + MetricsManager.init(mock[NetworkTimeProvider]) ForkManagerUtil.initializeForkManager(new SimpleForkConfigurator(), "regtest") historyMock = mock[AccountHistory] @@ -75,6 +78,7 @@ class AccountSidechainNodeViewHolderEventTest wallet = mock[AccountWallet] Mockito.when(wallet.scanOffchain(ArgumentMatchers.any[SidechainTypes#SCAT])).thenReturn(wallet) + } @Test diff --git a/sdk/src/test/scala/io/horizen/account/AccountSidechainNodeViewHolderTest.scala b/sdk/src/test/scala/io/horizen/account/AccountSidechainNodeViewHolderTest.scala index 168958cb1a..842ab3e824 100644 --- a/sdk/src/test/scala/io/horizen/account/AccountSidechainNodeViewHolderTest.scala +++ b/sdk/src/test/scala/io/horizen/account/AccountSidechainNodeViewHolderTest.scala @@ -16,6 +16,7 @@ import io.horizen.account.wallet.AccountWallet import io.horizen.block.SidechainBlockBase import io.horizen.consensus.{ConsensusEpochInfo, FullConsensusEpochInfo, intToConsensusEpochNumber} import io.horizen.fixtures._ +import io.horizen.metrics.MetricsManager import io.horizen.params.{NetworkParams, RegTestParams} import io.horizen.utils.{CountDownLatchController, MerkleTree, WithdrawalEpochInfo} import io.horizen.{AccountMempoolSettings, SidechainSettings} @@ -28,10 +29,12 @@ import org.scalatestplus.junit.JUnitSuite import sparkz.core.NodeViewHolder.ReceivableMessages.{LocallyGeneratedModifier, LocallyGeneratedTransaction, ModifiersFromRemote} import sparkz.core.consensus.History.ProgressInfo import sparkz.core.network.NodeViewSynchronizer.ReceivableMessages.{FailedTransaction, ModifiersProcessingResult, SemanticallySuccessfulModifier} +import sparkz.core.utils.NetworkTimeProvider import sparkz.core.validation.RecoverableModifierError import sparkz.core.{VersionTag, idToVersion} import sparkz.util.{ModifierId, SparkzEncoding} +import java.math.BigInteger import java.nio.charset.StandardCharsets import java.time.Instant import java.util @@ -74,6 +77,7 @@ class AccountSidechainNodeViewHolderTest extends JUnitSuite baseStateReaderProvider = mock[BaseStateReaderProvider] mempool = AccountMemoryPool.createEmptyMempool(accountStateReaderProvider, baseStateReaderProvider, AccountMempoolSettings(), () => mock[AccountEventNotifier]) + MetricsManager.init(mock[NetworkTimeProvider]) mockedNodeViewHolderRef = getMockedAccountSidechainNodeViewHolderRef(history, state, wallet, mempool) } @@ -307,8 +311,8 @@ class AccountSidechainNodeViewHolderTest extends JUnitSuite Mockito.when(state.isWithdrawalEpochLastIndex).thenReturn(false) // Mock state fee payments with checks - Mockito.when(state.getFeePaymentsInfo(ArgumentMatchers.any[Int](), any(), ArgumentMatchers.any[Option[AccountBlockFeeInfo]])).thenAnswer(_ => { - Seq() + Mockito.when(state.getFeePaymentsInfo(ArgumentMatchers.any[Int](), any(), any(),ArgumentMatchers.any[Option[AccountBlockFeeInfo]])).thenAnswer(_ => { + (Seq(), BigInteger.valueOf(Long.MaxValue)) }) // Mock wallet scanPersistent with checks @@ -332,7 +336,7 @@ class AccountSidechainNodeViewHolderTest extends JUnitSuite Thread.sleep(100) // Verify that all the checks passed - Mockito.verify(state, times(0)).getFeePaymentsInfo(ArgumentMatchers.any[Int](), any(), ArgumentMatchers.any[Option[AccountBlockFeeInfo]]) + Mockito.verify(state, times(0)).getFeePaymentsInfo(ArgumentMatchers.any[Int](), any(), any(),ArgumentMatchers.any[Option[AccountBlockFeeInfo]]) } @Test @@ -360,10 +364,10 @@ class AccountSidechainNodeViewHolderTest extends JUnitSuite // Mock state fee payments with checks val expectedFeePayments: Seq[AccountPayment] = Seq(ForgerAccountFixture.getAccountPayment(0L), ForgerAccountFixture.getAccountPayment(1L)) - Mockito.when(state.getFeePaymentsInfo(ArgumentMatchers.any[Int](), any(), ArgumentMatchers.any[Option[AccountBlockFeeInfo]]())).thenAnswer(args => { + Mockito.when(state.getFeePaymentsInfo(ArgumentMatchers.any[Int](), any(), any(), ArgumentMatchers.any[Option[AccountBlockFeeInfo]]())).thenAnswer(args => { val epochNumber: Int = args.getArgument(0) assertEquals("Different withdrawal epoch number expected.", withdrawalEpochInfo.epoch, epochNumber) - expectedFeePayments + (expectedFeePayments, BigInteger.valueOf(Long.MaxValue)) }) // Mock wallet scanPersistent with checks @@ -385,7 +389,7 @@ class AccountSidechainNodeViewHolderTest extends JUnitSuite Thread.sleep(100) // Verify that all the checks passed - Mockito.verify(state, times(1)).getFeePaymentsInfo(ArgumentMatchers.any[Int](), any(), ArgumentMatchers.any[Option[AccountBlockFeeInfo]]) + Mockito.verify(state, times(1)).getFeePaymentsInfo(ArgumentMatchers.any[Int](), any(), any(), ArgumentMatchers.any[Option[AccountBlockFeeInfo]]) } @Test @@ -457,8 +461,8 @@ class AccountSidechainNodeViewHolderTest extends JUnitSuite val block5 = generateNextAccountBlock(block4, sidechainTransactionsCompanion, params) val block6 = generateNextAccountBlock(block5, sidechainTransactionsCompanion, params) - val firstRequestBlocks = Seq(block1, block2, block6) - val secondRequestBlocks = Seq(block3, block4, block5) + val firstRequestBlocks = Seq(block3, block2, block6) + val secondRequestBlocks = Seq(block1, block4, block5) val correctSequence = Array(block1, block2, block3, block4, block5, block6) var blockIndex = 0 @@ -490,13 +494,25 @@ class AccountSidechainNodeViewHolderTest extends JUnitSuite actorSystem.eventStream.subscribe(eventListener.ref, classOf[ModifiersProcessingResult[AccountBlock]]) mockedNodeViewHolderRef ! ModifiersFromRemote(firstRequestBlocks) + eventListener.fishForMessage(timeout.duration) { + case m => + m match { + case ModifiersProcessingResult(applied, cleared) => { + assertTrue("Applied block sequence should be empty.", applied.isEmpty) + assertTrue("Cleared block sequence is not empty.", cleared.isEmpty) + true + } + case _ => false // Log + } + } + mockedNodeViewHolderRef ! ModifiersFromRemote(secondRequestBlocks) eventListener.fishForMessage(timeout.duration) { case m => m match { case ModifiersProcessingResult(applied, cleared) => { - assertTrue("Applied block sequence is differ", applied.toSet.equals(correctSequence.toSet)) + assertEquals("Applied block sequence is differ", correctSequence.toSet, applied.toSet) assertTrue("Cleared block sequence is not empty.", cleared.isEmpty) true } @@ -665,6 +681,16 @@ class AccountSidechainNodeViewHolderTest extends JUnitSuite actorSystem.eventStream.subscribe(eventListener.ref, classOf[ModifiersProcessingResult[AccountBlock]]) mockedNodeViewHolderRef ! ModifiersFromRemote(halfFullCacheBlocks) + eventListener.fishForMessage(timeout.duration) { + case m => + m match { + case ModifiersProcessingResult(applied, cleared) => + assertEquals("Different number of applied blocks", 0, applied.length) + assertEquals("Different number of cleared blocks from cached", 0, cleared.length) + true + case _ => false + } + } mockedNodeViewHolderRef ! ModifiersFromRemote(twoHundredBlocks) eventListener.fishForMessage(timeout.duration) { diff --git a/sdk/src/test/scala/io/horizen/account/api/http/AccountNodeViewUtilMocks.scala b/sdk/src/test/scala/io/horizen/account/api/http/AccountNodeViewUtilMocks.scala index a9f3f8e603..8720e3d5f5 100644 --- a/sdk/src/test/scala/io/horizen/account/api/http/AccountNodeViewUtilMocks.scala +++ b/sdk/src/test/scala/io/horizen/account/api/http/AccountNodeViewUtilMocks.scala @@ -58,7 +58,7 @@ class AccountNodeViewUtilMocks extends MockitoSugar def getNodeStateMock(sidechainApiMockConfiguration: SidechainApiMockConfiguration): NodeAccountState = { val accountState = mock[NodeAccountState] Mockito.when(accountState.isForgerStakeAvailable(ArgumentMatchers.anyBoolean())).thenReturn(true) - Mockito.when(accountState.getListOfForgersStakes(ArgumentMatchers.anyBoolean())).thenReturn(listOfStakes) + Mockito.when(accountState.getListOfForgersStakes(ArgumentMatchers.anyBoolean(), ArgumentMatchers.anyBoolean())).thenReturn(listOfStakes) Mockito.when(accountState.getWithdrawalRequests(ArgumentMatchers.anyInt())).thenReturn(listOfWithdrawalRequests) Mockito .when(accountState.getBalance(ArgumentMatchers.any[Address])) diff --git a/sdk/src/test/scala/io/horizen/account/api/http/route/AccountEthRpcRouteTest.scala b/sdk/src/test/scala/io/horizen/account/api/http/route/AccountEthRpcRouteTest.scala index e28f3c6e06..b420fa10e6 100644 --- a/sdk/src/test/scala/io/horizen/account/api/http/route/AccountEthRpcRouteTest.scala +++ b/sdk/src/test/scala/io/horizen/account/api/http/route/AccountEthRpcRouteTest.scala @@ -10,11 +10,11 @@ import java.math.BigInteger class AccountEthRpcRouteTest extends AccountEthRpcRouteMock { - private def rpc(requestJson: String, expectedJson: String = null): JsonNode = { + private def rpc(requestJson: String, expectedJson: String = null, expectedHttpCode: Int = StatusCodes.OK.intValue) : JsonNode = { Post(basePath) .addCredentials(credentials) .withEntity(requestJson) ~> ethRpcRoute ~> check { - status.intValue() shouldBe StatusCodes.OK.intValue + status.intValue() shouldBe expectedHttpCode responseEntity.getContentType() shouldEqual ContentTypes.`application/json` val actual = mapper.readTree(entityAs[String]) if (expectedJson != null) { @@ -54,21 +54,24 @@ class AccountEthRpcRouteTest extends AccountEthRpcRouteMock { "reply at /ethv1 - single request - parse error no quotes" in { rpc( """{"jsonrpc":"2.0","id":"225","method":"eth_chainId_",params:[]}""", - """{"error":{"code":-32700,"message":"Parse error: Unexpected character ('p' (code 112)): was expecting double-quote to start field name\n at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 54]","data":"Unexpected character ('p' (code 112)): was expecting double-quote to start field name\n at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 54]"},"jsonrpc":"2.0","id":null}""" + """{"error":{"code":-32700,"message":"Parse error: Unexpected character ('p' (code 112)): was expecting double-quote to start field name\n at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 54]","data":"Unexpected character ('p' (code 112)): was expecting double-quote to start field name\n at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 54]"},"jsonrpc":"2.0","id":null}""", + expectedHttpCode = StatusCodes.BadRequest.intValue ) } "reply at /ethv1 - single request - parse error no braces" in { rpc( """ "jsonrpc":"2.0","id":"225","method":"eth_chainId_","params":[]""", - """{"error":{"code":-32600,"message":"Invalid request: missing field: jsonrpc","data":"missing field: jsonrpc"},"jsonrpc":"2.0","id":null}""" + """{"error":{"code":-32600,"message":"Invalid request: missing field: id","data":"missing field: id"},"jsonrpc":"2.0","id":null}""", + expectedHttpCode = StatusCodes.BadRequest.intValue ) } "reply at /ethv1 - single request - id not present" in { rpc( """{"jsonrpc":"2.0","method":"eth_chainId_","params":[]}""", - """{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Invalid request: missing field: id","data":"missing field: id"}}""" + """{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Invalid request: missing field: id","data":"missing field: id"}}""", + expectedHttpCode = StatusCodes.BadRequest.intValue ) } @@ -111,9 +114,10 @@ class AccountEthRpcRouteTest extends AccountEthRpcRouteMock { {"jsonrpc":"2.0","method":"eth_chainId","params":[]} ]""", s"""[ - {"jsonrpc":"2.0","id":8,"result":"$checkChainId"}, - {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Invalid request: missing field: id","data":"missing field: id"}} - ]""" + {"jsonrpc":"2.0","id":8,"result":"$checkChainId"}, + {"error":{"code":-32600,"message":"Invalid request: missing field: id","data":"missing field: id"},"jsonrpc":"2.0","id":null} + ]""".stripMargin, + expectedHttpCode = StatusCodes.BadRequest.intValue ) } @@ -125,10 +129,11 @@ class AccountEthRpcRouteTest extends AccountEthRpcRouteMock { {"jsonrpc":"2.0","id":16,"method":"eth_chainId","params":[]} ]""", s"""[ - {"jsonrpc":"2.0","id":8,"result":"$checkChainId"}, - {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Invalid request: missing field: jsonrpc","data":"missing field: jsonrpc"}}, - {"jsonrpc":"2.0","id":16,"result":"$checkChainId"} - ]""" + {"jsonrpc":"2.0","id":8,"result":"$checkChainId"}, + {"error":{"code":-32600,"message":"Invalid request: missing field: id","data":"missing field: id"},"jsonrpc":"2.0","id":null}, + {"jsonrpc":"2.0","id":16,"result":"0x1fca055"} + ]""".stripMargin, + expectedHttpCode = StatusCodes.BadRequest.intValue ) } @@ -140,24 +145,27 @@ class AccountEthRpcRouteTest extends AccountEthRpcRouteMock { 24 ]""", """[ - {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Invalid request: missing field: jsonrpc","data":"missing field: jsonrpc"}}, - {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Invalid request: missing field: jsonrpc","data":"missing field: jsonrpc"}}, - {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Invalid request: missing field: jsonrpc","data":"missing field: jsonrpc"}} - ]""" + {"error":{"code":-32600,"message":"Invalid request: missing field: id","data":"missing field: id"},"jsonrpc":"2.0","id":null}, + {"error":{"code":-32600,"message":"Invalid request: missing field: id","data":"missing field: id"},"jsonrpc":"2.0","id":null}, + {"error":{"code":-32600,"message":"Invalid request: missing field: id","data":"missing field: id"},"jsonrpc":"2.0","id":null} + ]""".stripMargin, + expectedHttpCode = StatusCodes.BadRequest.intValue ) } "reply at /ethv1 - batch request - invalid batch - empty array" in { rpc( """[]""", - """{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Invalid request: missing field: jsonrpc","data":"missing field: jsonrpc"}}""" + """{"error":{"code":-32600,"message":"Invalid request: Empty array as input","data":"Empty array as input"},"jsonrpc":"2.0","id":null}""", + expectedHttpCode = StatusCodes.BadRequest.intValue ) } "reply at /ethv1 - single request - invalid id" in { rpc( """{"jsonrpc":"2.0","id":65465817687165465465,"method":"eth_chainId","params":[]}""", - """{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Invalid request: Rpc Id value is greater than datatype max value","data":"Rpc Id value is greater than datatype max value"}}""" + """{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Invalid request: Rpc Id value is greater than datatype max value","data":"Rpc Id value is greater than datatype max value"}}""", + expectedHttpCode = StatusCodes.BadRequest.intValue ) } @@ -168,9 +176,10 @@ class AccountEthRpcRouteTest extends AccountEthRpcRouteMock { {"jsonrpc":"2.0","id":16,"method":"eth_chainId","params":[]} ]""", s"""[ - {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Invalid request: Rpc Id can't be a negative number","data":"Rpc Id can't be a negative number"}}, - {"jsonrpc":"2.0","id":16,"result":"$checkChainId"} - ]""" + {"error":{"code":-32600,"message":"Invalid request: Rpc Id can't be a negative number","data":"Rpc Id can't be a negative number"},"jsonrpc":"2.0","id":null}, + {"jsonrpc":"2.0","id":16,"result":"$checkChainId"} + ]""".stripMargin, + expectedHttpCode = StatusCodes.BadRequest.intValue ) } diff --git a/sdk/src/test/scala/io/horizen/account/api/rpc/service/EthServiceTest.scala b/sdk/src/test/scala/io/horizen/account/api/rpc/service/EthServiceTest.scala index 68f00fe7aa..c71d9f9593 100644 --- a/sdk/src/test/scala/io/horizen/account/api/rpc/service/EthServiceTest.scala +++ b/sdk/src/test/scala/io/horizen/account/api/rpc/service/EthServiceTest.scala @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import io.horizen.account.api.rpc.handler.RpcException import io.horizen.account.api.rpc.request.RpcRequest import io.horizen.account.block.AccountBlock +import io.horizen.account.chain.AccountFeePaymentsInfo import io.horizen.account.fork.GasFeeFork.DefaultGasFeeFork import io.horizen.account.history.AccountHistory import io.horizen.account.mempool.AccountMemoryPool @@ -17,7 +18,7 @@ import io.horizen.account.state.AccountState import io.horizen.account.state.receipt.{EthereumReceipt, ReceiptFixture} import io.horizen.account.transaction.EthereumTransaction import io.horizen.account.transaction.EthereumTransaction.EthereumTransactionType -import io.horizen.account.utils.{AccountMockDataHelper, EthereumTransactionEncoder, FeeUtils} +import io.horizen.account.utils.{AccountMockDataHelper, AccountPayment, EthereumTransactionEncoder, FeeUtils} import io.horizen.account.wallet.AccountWallet import io.horizen.api.http.{SidechainApiMockConfiguration, SidechainTransactionActorRef} import io.horizen.consensus.ConsensusParamsUtil @@ -31,7 +32,9 @@ import io.horizen.params.RegTestParams import io.horizen.utils.{BytesUtils, TimeToEpochUtils} import io.horizen.{EthServiceSettings, SidechainTypes} import org.junit.{Before, Test} +import org.mockito.ArgumentMatchers.{any, anyString} import org.mockito.Mockito +import org.mockito.Mockito.when import org.scalatest.prop.TableDrivenPropertyChecks import org.scalatestplus.junit.JUnitSuite import org.scalatestplus.mockito.MockitoSugar @@ -42,7 +45,7 @@ import sparkz.core.bytesToId import sparkz.core.network.NetworkController.ReceivableMessages.GetConnectedPeers import sparkz.core.network.NodeViewSynchronizer.ReceivableMessages.SuccessfulTransaction import sparkz.crypto.hash.Keccak256 -import sparkz.util.ByteArrayBuilder +import sparkz.util.{ByteArrayBuilder, ModifierId} import sparkz.util.serialization.VLQByteBufferWriter import java.math.BigInteger @@ -390,6 +393,15 @@ class EthServiceTest extends JUnitSuite with MockitoSugar with ReceiptFixture wi Some(genesisBlock), Some(genesisBlockId) ) + when(mockedHistory.feePaymentsInfo(any())).thenAnswer(_ => + Some(AccountFeePaymentsInfo( + Seq(AccountPayment( + new AddressProposition(new Address("0xd123b689dad8ed6b99f8bd55eed64ab357e6a8d1")), + BigInteger.valueOf(1), + Some(BigInteger.valueOf(0)), + None + ))))) + val mockedState: AccountState = mockHelper.getMockedState(receipt, Numeric.hexStringToByteArray(txHash)) val secret = PrivateKeySecp256k1Creator @@ -1144,8 +1156,8 @@ class EthServiceTest extends JUnitSuite with MockitoSugar with ReceiptFixture wi def zen_getFeePayments(): Unit = { val validCases = Table( ("Block id", "Expected output"), - ("0xdc7ac3d7de9d7fc524bbb95025a98c3e9290b041189ee73c638cf981e7f99bfc", """{"payments":[]}"""), - ("0x2", """{"payments":[]}"""), + ("0xdc7ac3d7de9d7fc524bbb95025a98c3e9290b041189ee73c638cf981e7f99bfc", """{"payments":[{"address":"0xd123b689dad8ed6b99f8bd55eed64ab357e6a8d1","value":"0x1","valueFromMainchain":"0x0"}]}"""), + ("0x2", """{"payments":[{"address":"0xd123b689dad8ed6b99f8bd55eed64ab357e6a8d1","value":"0x1","valueFromMainchain":"0x0"}]}"""), ) val invalidCases = diff --git a/sdk/src/test/scala/io/horizen/account/fixtures/MockedRpcProcessor.scala b/sdk/src/test/scala/io/horizen/account/fixtures/MockedRpcProcessor.scala index f8859063c4..6056e87ad5 100644 --- a/sdk/src/test/scala/io/horizen/account/fixtures/MockedRpcProcessor.scala +++ b/sdk/src/test/scala/io/horizen/account/fixtures/MockedRpcProcessor.scala @@ -29,7 +29,7 @@ case class MockedRpcProcessor(mockedSidechainNodeViewHolderRef: ActorRef, params, mockedSidechainSettings.ethService, mockedSidechainSettings.sparkzSettings.network.maxIncomingConnections, - RpcUtils.getClientVersion, + RpcUtils.getClientVersion("dev"), mockedSidechainTransactionActorRef, mockedSyncStatusActorRef, sidechainTransactionsCompanion 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 5830b85593..d51ed4d510 100644 --- a/sdk/src/test/scala/io/horizen/account/forger/AccountForgeMessageBuilderTest.scala +++ b/sdk/src/test/scala/io/horizen/account/forger/AccountForgeMessageBuilderTest.scala @@ -9,6 +9,7 @@ import io.horizen.account.proposition.AddressProposition import io.horizen.account.secret.{PrivateKeySecp256k1, PrivateKeySecp256k1Creator} import io.horizen.account.state._ import io.horizen.account.state.receipt.{EthereumReceipt, ReceiptFixture} +import io.horizen.account.storage.MsgProcessorMetadataStorageReader import io.horizen.account.transaction.EthereumTransaction import io.horizen.account.transaction.EthereumTransaction.EthereumTransactionType import io.horizen.account.utils.{AccountMockDataHelper, EthereumTransactionEncoder, FeeUtils, WellKnownAddresses, ZenWeiConverter} @@ -18,6 +19,7 @@ import io.horizen.consensus.{ConsensusParamsUtil, ForgingStakeInfo} import io.horizen.evm.{Address, Hash} import io.horizen.fixtures.{CompanionsFixture, SecretFixture, SidechainRelatedMainchainOutputFixture, VrfGenerator} import io.horizen.fork.{ConsensusParamsFork, ConsensusParamsForkInfo, CustomForkConfiguratorWithConsensusParamsFork, ForkManagerUtil} +import io.horizen.metrics.MetricsManager import io.horizen.params.{NetworkParams, TestNetParams} import io.horizen.proof.{Signature25519, VrfProof} import io.horizen.proposition.VrfPublicKey @@ -34,8 +36,10 @@ import org.mockito.Mockito.when import org.mockito.{ArgumentMatchers, Mockito} import org.scalatest.Assertions.assertThrows import org.scalatestplus.mockito.MockitoSugar +import org.scalatestplus.mockito.MockitoSugar.mock import org.web3j.utils.Numeric import sparkz.core.transaction.state.Secret +import sparkz.core.utils.NetworkTimeProvider import sparkz.crypto.hash.Keccak256 import sparkz.util.serialization.VLQByteBufferWriter import sparkz.util.{ByteArrayBuilder, bytesToId} @@ -61,6 +65,7 @@ class AccountForgeMessageBuilderTest @Before def init(): Unit = { + MetricsManager.init(mock[NetworkTimeProvider]) ForkManagerUtil.initializeForkManager(CustomForkConfiguratorWithConsensusParamsFork.getCustomForkConfiguratorWithConsensusParamsFork(Seq(), Seq(), Seq()), "regtest") } @@ -415,6 +420,7 @@ class AccountForgeMessageBuilderTest mockMsgProcessor.process( ArgumentMatchers.any[Invocation], ArgumentMatchers.any[BaseAccountStateView], + ArgumentMatchers.any[MsgProcessorMetadataStorageReader], ArgumentMatchers.any[ExecutionContext] ) ) diff --git a/sdk/src/test/scala/io/horizen/account/performance/AccountForgeMessageBuilderPerfTest.scala b/sdk/src/test/scala/io/horizen/account/performance/AccountForgeMessageBuilderPerfTest.scala index 6bd96a74f9..c946b19745 100644 --- a/sdk/src/test/scala/io/horizen/account/performance/AccountForgeMessageBuilderPerfTest.scala +++ b/sdk/src/test/scala/io/horizen/account/performance/AccountForgeMessageBuilderPerfTest.scala @@ -8,6 +8,7 @@ import io.horizen.account.mempool.AccountMemoryPool import io.horizen.account.state._ import io.horizen.account.state.receipt.EthereumConsensusDataReceipt import io.horizen.account.state.receipt.EthereumConsensusDataReceipt.ReceiptStatus +import io.horizen.account.storage.MsgProcessorMetadataStorageReader import io.horizen.account.utils.ZenWeiConverter import io.horizen.account.wallet.AccountWallet import io.horizen.block.MainchainBlockReferenceData @@ -34,7 +35,8 @@ class AccountForgeMessageBuilderPerfTest extends MockitoSugar with EthereumTrans ArgumentMatchers.any[SidechainTypes#SCAT], ArgumentMatchers.any[Int], ArgumentMatchers.any[GasPool], - ArgumentMatchers.any[BlockContext] + ArgumentMatchers.any[BlockContext], + ArgumentMatchers.any[MsgProcessorMetadataStorageReader] ) ) .thenAnswer(asw => { 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 d22334bb54..a00e5357b4 100644 --- a/sdk/src/test/scala/io/horizen/account/state/AccountStateTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/AccountStateTest.scala @@ -3,9 +3,9 @@ package io.horizen.account.state import io.horizen.account.fixtures.EthereumTransactionFixture import io.horizen.account.fork.GasFeeFork import io.horizen.account.fork.GasFeeFork.DefaultGasFeeFork -import io.horizen.account.storage.{AccountStateMetadataStorage, AccountStateMetadataStorageView} +import io.horizen.account.storage.AccountStateMetadataStorage import io.horizen.account.transaction.EthereumTransaction -import io.horizen.account.utils.{AccountBlockFeeInfo, AccountPayment} +import io.horizen.account.utils.{AccountBlockFeeInfo, AccountPayment, ForgerIdentifier} import io.horizen.consensus.{ConsensusEpochNumber, ConsensusParamsUtil, intToConsensusEpochNumber, intToConsensusSlotNumber} import io.horizen.evm._ import io.horizen.fixtures.{SecretFixture, SidechainTypesTestsExtension, StoreFixture} @@ -15,7 +15,6 @@ import io.horizen.utils import io.horizen.utils.{BytesUtils, ClosableResourceHandler, TimeToEpochUtils} import org.junit.Assert._ import org.junit._ -import org.mockito.ArgumentMatchers.any import org.mockito.{ArgumentMatchers, Mockito} import org.scalatestplus.junit.JUnitSuite import org.scalatestplus.mockito.MockitoSugar @@ -81,7 +80,7 @@ class AccountStateTest // Test 1: No block fee info record in the storage Mockito.when(metadataStorage.getFeePayments(ArgumentMatchers.any[Int]())).thenReturn(Seq()) Mockito.when(metadataStorage.getConsensusEpochNumber).thenReturn(Some(ConsensusEpochNumber @@ 1)) - var feePayments: Seq[AccountPayment] = state.getFeePaymentsInfo(0, intToConsensusEpochNumber(0)) + var feePayments: Seq[AccountPayment] = state.getFeePaymentsInfo(0, intToConsensusEpochNumber(0))._1 assertEquals(s"Fee payments size expected to be different.", 0, feePayments.size) // Test 2: with single block fee info record in the storage @@ -93,7 +92,7 @@ class AccountStateTest Mockito.when(metadataStorage.getFeePayments(ArgumentMatchers.any[Int]())).thenReturn(Seq(blockFeeInfo1)) Mockito.when(metadataStorage.getConsensusEpochNumber).thenReturn(Some(ConsensusEpochNumber @@ 1)) - feePayments = state.getFeePaymentsInfo(0, intToConsensusEpochNumber(0)) + feePayments = state.getFeePaymentsInfo(0, intToConsensusEpochNumber(0))._1 assertEquals(s"Fee payments size expected to be different.", 1, feePayments.size) assertEquals( s"Fee value for baseFee ${feePayments.head.value} is wrong", @@ -120,7 +119,7 @@ class AccountStateTest .thenReturn(Seq(blockFeeInfo1, blockFeeInfo2, blockFeeInfo3)) Mockito.when(metadataStorage.getConsensusEpochNumber).thenReturn(Some(ConsensusEpochNumber @@ 1)) - feePayments = state.getFeePaymentsInfo(0, intToConsensusEpochNumber(0)) + feePayments = state.getFeePaymentsInfo(0, intToConsensusEpochNumber(0))._1 assertEquals(s"Fee payments size expected to be different.", 3, feePayments.size) var forgerTotalFee = feePayments.foldLeft(BigInteger.ZERO)((sum, payment) => sum.add(payment.value)) @@ -147,7 +146,7 @@ class AccountStateTest .thenReturn(Seq(blockFeeInfo1, blockFeeInfo2, blockFeeInfo3, blockFeeInfo4)) Mockito.when(metadataStorage.getConsensusEpochNumber).thenReturn(Some(ConsensusEpochNumber @@ 1)) - feePayments = state.getFeePaymentsInfo(0, intToConsensusEpochNumber(0)) + feePayments = state.getFeePaymentsInfo(0, intToConsensusEpochNumber(0))._1 assertEquals(s"Fee payments size expected to be different.", 3, feePayments.size) forgerTotalFee = feePayments.foldLeft(BigInteger.ZERO)((sum, payment) => sum.add(payment.value)) @@ -172,7 +171,7 @@ class AccountStateTest totalFee = sumFeeInfos(bfi1, bfi2, bfi3, bfi4, bfi5) - feePayments = state.getFeePaymentsInfo(0, intToConsensusEpochNumber(0)) + feePayments = state.getFeePaymentsInfo(0, intToConsensusEpochNumber(0))._1 assertEquals(s"Fee payments size expected to be different.", 2, feePayments.size) forgerTotalFee = feePayments.foldLeft(BigInteger.ZERO)((sum, payment) => sum.add(payment.value)) @@ -202,9 +201,9 @@ class AccountStateTest Mockito.when(metadataStorage.getFeePayments(ArgumentMatchers.any[Int]())) .thenReturn(Seq(blockFeeInfo1, blockFeeInfo2, blockFeeInfo3, blockFeeInfo4)) - Mockito.when(metadataStorage.getMcForgerPoolRewards).thenAnswer(_ => Map(addr1 -> perBlockFee, addr2 -> perBlockFee, addr3 -> perBlockFee.add(perBlockFee))) + Mockito.when(metadataStorage.getMcForgerPoolRewards).thenAnswer(_ => Map(new ForgerIdentifier(addr1) -> perBlockFee, new ForgerIdentifier(addr2) -> perBlockFee, new ForgerIdentifier(addr3) -> perBlockFee.add(perBlockFee))) - val feePayments = state.getFeePaymentsInfo(0, intToConsensusEpochNumber(0)) + val feePayments = state.getFeePaymentsInfo(0, intToConsensusEpochNumber(0))._1 assertEquals(s"Fee payments size expected to be different.", 3, feePayments.size) val forgerTotalFee = feePayments.foldLeft(BigInteger.ZERO)((sum, payment) => sum.add(payment.value)) 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 c91b62b753..cabe32e8e3 100644 --- a/sdk/src/test/scala/io/horizen/account/state/AccountStateViewTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/AccountStateViewTest.scala @@ -5,16 +5,17 @@ import io.horizen.account.fixtures.ForgerAccountFixture.getPrivateKeySecp256k1 import io.horizen.account.fork.Version1_2_0Fork import io.horizen.account.storage.AccountStateMetadataStorageView import io.horizen.account.utils.WellKnownAddresses.FORGER_POOL_RECIPIENT_ADDRESS -import io.horizen.account.utils.{WellKnownAddresses, ZenWeiConverter} +import io.horizen.account.utils.ZenWeiConverter.MAX_MONEY_IN_WEI +import io.horizen.account.utils.{ForgerIdentifier, WellKnownAddresses, ZenWeiConverter} import io.horizen.consensus.intToConsensusEpochNumber +import io.horizen.evm.{Address, StateDB} import io.horizen.fixtures.StoreFixture +import io.horizen.fork.{ForkManagerUtil, OptionalSidechainFork, SidechainForkConsensusEpoch, SimpleForkConfigurator} import io.horizen.params.NetworkParams import io.horizen.proposition.MCPublicKeyHashProposition +import io.horizen.utils import io.horizen.utils.ByteArrayWrapper import io.horizen.utils.WithdrawalEpochUtils.MaxWithdrawalReqsNumPerEpoch -import io.horizen.evm.{Address, StateDB} -import io.horizen.fork.{ForkManagerUtil, OptionalSidechainFork, SidechainForkConsensusEpoch, SimpleForkConfigurator} -import io.horizen.utils import org.junit.Assert._ import org.junit._ import org.mockito.Mockito.when @@ -165,22 +166,22 @@ class AccountStateViewTest extends JUnitSuite with MockitoSugar with MessageProc val addr2 = getPrivateKeySecp256k1(1001).publicImage() val addr3 = getPrivateKeySecp256k1(1002).publicImage() val blockCounters = Map( - addr1 -> 2L, - addr2 -> 3L, - addr3 -> 5L, + new ForgerIdentifier(addr1) -> 2L, + new ForgerIdentifier(addr2) -> 3L, + new ForgerIdentifier(addr3) -> 5L, ) when(stateDb.getBalance(FORGER_POOL_RECIPIENT_ADDRESS)).thenReturn(BigInteger.valueOf(1000L)) when(metadataStorageView.getForgerBlockCounters).thenAnswer(_ => blockCounters) - val rewardsBeforeFork = stateView.getMcForgerPoolRewards(intToConsensusEpochNumber(0)) + val rewardsBeforeFork = stateView.getMcForgerPoolRewards(intToConsensusEpochNumber(0),MAX_MONEY_IN_WEI) assertTrue(rewardsBeforeFork.isEmpty) when(metadataStorageView.getConsensusEpochNumber).thenAnswer(_ => Some(36)) - val rewardsAfterFork = stateView.getMcForgerPoolRewards(intToConsensusEpochNumber(36)) + val rewardsAfterFork = stateView.getMcForgerPoolRewards(intToConsensusEpochNumber(36), MAX_MONEY_IN_WEI) assertEquals(3, rewardsAfterFork.size) - assertEquals(BigInteger.valueOf(200L), rewardsAfterFork(addr1)) - assertEquals(BigInteger.valueOf(300L), rewardsAfterFork(addr2)) - assertEquals(BigInteger.valueOf(500L), rewardsAfterFork(addr3)) + assertEquals(BigInteger.valueOf(200L), rewardsAfterFork(new ForgerIdentifier(addr1))) + assertEquals(BigInteger.valueOf(300L), rewardsAfterFork(new ForgerIdentifier(addr2))) + assertEquals(BigInteger.valueOf(500L), rewardsAfterFork(new ForgerIdentifier(addr3))) } 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 55aafdbe66..c7683e80e6 100644 --- a/sdk/src/test/scala/io/horizen/account/state/CertificateKeyRotationMsgProcessorTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/CertificateKeyRotationMsgProcessorTest.scala @@ -162,7 +162,7 @@ class CertificateKeyRotationMsgProcessorTest data = BytesUtils.fromHexString(SubmitKeyRotationReqCmdSig) ++ encodedInput, nonce = randomNonce ) - withGas(TestContext.process(certificateKeyRotationMsgProcessor, msg, view, blockContext, _)) + withGas(TestContext.process(certificateKeyRotationMsgProcessor, msg, view, blockContext, _, view)) } private def processBadKeyRotationMessage(newKey: SchnorrSecret, keyRotationProof: KeyRotationProof, view: AccountStateView, epoch: Int = 0, diff --git a/sdk/src/test/scala/io/horizen/account/state/ContractInteropCallTest.scala b/sdk/src/test/scala/io/horizen/account/state/ContractInteropCallTest.scala index 02092ee58f..920128d4d4 100644 --- a/sdk/src/test/scala/io/horizen/account/state/ContractInteropCallTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/ContractInteropCallTest.scala @@ -4,6 +4,7 @@ import com.google.common.primitives.Bytes import io.horizen.account.abi.ABIEncodable import io.horizen.account.abi.ABIUtil.{getArgumentsFromData, getFunctionSignature} import io.horizen.account.state.ContractInteropTestBase._ +import io.horizen.account.storage.MsgProcessorMetadataStorageReader import io.horizen.account.utils.BigIntegerUtil.toUint256Bytes import io.horizen.account.utils.{FeeUtils, Secp256k1} import io.horizen.evm._ @@ -50,6 +51,7 @@ class ContractInteropCallTest extends ContractInteropTestBase { override def process( invocation: Invocation, view: BaseAccountStateView, + metadata: MsgProcessorMetadataStorageReader, context: ExecutionContext ): Array[Byte] = { val gasView = view.getGasTrackedView(invocation.gasPool) diff --git a/sdk/src/test/scala/io/horizen/account/state/ContractInteropStackTest.scala b/sdk/src/test/scala/io/horizen/account/state/ContractInteropStackTest.scala index b6b59acd21..465c90aeea 100644 --- a/sdk/src/test/scala/io/horizen/account/state/ContractInteropStackTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/ContractInteropStackTest.scala @@ -1,5 +1,6 @@ package io.horizen.account.state +import io.horizen.account.storage.MsgProcessorMetadataStorageReader import io.horizen.evm.{Address, TraceOptions, Tracer} import io.horizen.utils.BytesUtils import org.junit.Assert.assertEquals @@ -40,6 +41,7 @@ class ContractInteropStackTest extends ContractInteropTestBase { override def process( invocation: Invocation, view: BaseAccountStateView, + metadata: MsgProcessorMetadataStorageReader, context: ExecutionContext ): Array[Byte] = { // parse input diff --git a/sdk/src/test/scala/io/horizen/account/state/ContractInteropTestBase.scala b/sdk/src/test/scala/io/horizen/account/state/ContractInteropTestBase.scala index f0dd790826..0f746a5f03 100644 --- a/sdk/src/test/scala/io/horizen/account/state/ContractInteropTestBase.scala +++ b/sdk/src/test/scala/io/horizen/account/state/ContractInteropTestBase.scala @@ -80,7 +80,7 @@ abstract class ContractInteropTestBase extends MessageProcessorFixture { } protected def transition(msg: Message, blckContext: BlockContext = blockContext, gasLimit: BigInteger = gasLimit): Array[Byte] = { - val transition = new StateTransition(stateView, processors, new GasPool(gasLimit), blckContext, msg) + val transition = new StateTransition(stateView, processors, new GasPool(gasLimit), blckContext, msg, stateView) transition.execute(Invocation.fromMessage(msg, new GasPool(gasLimit))) } diff --git a/sdk/src/test/scala/io/horizen/account/state/ContractInteropTransferTest.scala b/sdk/src/test/scala/io/horizen/account/state/ContractInteropTransferTest.scala index d031fb3b2f..7ce0e13983 100644 --- a/sdk/src/test/scala/io/horizen/account/state/ContractInteropTransferTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/ContractInteropTransferTest.scala @@ -1,5 +1,6 @@ package io.horizen.account.state +import io.horizen.account.storage.MsgProcessorMetadataStorageReader import io.horizen.evm._ import io.horizen.utils.BytesUtils import org.junit.Assert.assertEquals @@ -18,6 +19,7 @@ class ContractInteropTransferTest extends ContractInteropTestBase { override def process( invocation: Invocation, view: BaseAccountStateView, + metadata: MsgProcessorMetadataStorageReader, context: ExecutionContext ): Array[Byte] = { // accept incoming value transfers 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 3946f41dc7..1f237ef06f 100644 --- a/sdk/src/test/scala/io/horizen/account/state/EoaMessageProcessorTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/EoaMessageProcessorTest.scala @@ -111,7 +111,7 @@ class EoaMessageProcessorTest extends JUnitSuite with MockitoSugar with SecretFi .when(mockStateView.subBalance(ArgumentMatchers.any[Address], ArgumentMatchers.any[BigInteger])) .thenThrow(new ExecutionFailedException("something went error")) assertThrows[ExecutionFailedException]( - withGas(TestContext.process(EoaMessageProcessor, msg, mockStateView, defaultBlockContext, _)) + withGas(TestContext.process(EoaMessageProcessor, msg, mockStateView, defaultBlockContext, _, mockStateView)) ) // Test 3: Failure during addBalance @@ -120,7 +120,7 @@ class EoaMessageProcessorTest extends JUnitSuite with MockitoSugar with SecretFi .when(mockStateView.addBalance(ArgumentMatchers.any[Address], ArgumentMatchers.any[BigInteger])) .thenThrow(new ExecutionFailedException("something went error")) assertThrows[ExecutionFailedException]( - withGas(TestContext.process(EoaMessageProcessor, msg, mockStateView, defaultBlockContext, _)) + withGas(TestContext.process(EoaMessageProcessor, msg, mockStateView, defaultBlockContext, _, mockStateView)) ) } } 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 2c1b6166d9..1fa917c24e 100644 --- a/sdk/src/test/scala/io/horizen/account/state/EvmMessageProcessorIntegrationTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/EvmMessageProcessorIntegrationTest.scala @@ -92,7 +92,7 @@ class EvmMessageProcessorIntegrationTest extends EvmMessageProcessorTestBase { //Try with a smart contract compiled for Shanghai msg = getMessage(null, data = deployCodeShanghai ++ initialValue) val ex = intercept[ExecutionFailedException] { - withGas(TestContext.process(evmMessageProcessor, msg, stateView, defaultBlockContext, _)) + withGas(TestContext.process(evmMessageProcessor, msg, stateView, defaultBlockContext, _, stateView)) } assertTrue(ex.getMessage.contains("PUSH0")) diff --git a/sdk/src/test/scala/io/horizen/account/state/ForgerBlockCountersSerializerTest.scala b/sdk/src/test/scala/io/horizen/account/state/ForgerBlockCountersSerializerTest.scala index 7e3d4cad93..67b2f3f89b 100644 --- a/sdk/src/test/scala/io/horizen/account/state/ForgerBlockCountersSerializerTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/ForgerBlockCountersSerializerTest.scala @@ -1,10 +1,14 @@ package io.horizen.account.state import io.horizen.account.fixtures.ForgerAccountFixture.getPrivateKeySecp256k1 -import io.horizen.account.proposition.AddressProposition +import io.horizen.account.utils.ForgerIdentifier +import io.horizen.proposition.{PublicKey25519Proposition, VrfPublicKey} +import io.horizen.secret.PrivateKey25519Creator +import io.horizen.vrf.VrfGeneratedDataProvider import org.junit.Assert.{assertEquals, fail} import org.junit.Test +import java.nio.charset.StandardCharsets import scala.util.{Failure, Success} class ForgerBlockCountersSerializerTest { @@ -15,27 +19,55 @@ class ForgerBlockCountersSerializerTest { val addr2 = getPrivateKeySecp256k1(1001).publicImage() val addr3 = getPrivateKeySecp256k1(1002).publicImage() val forgerBlockCounters = Map( - addr1 -> 1L, - addr2 -> 100L, - addr3 -> 9999999L, + new ForgerIdentifier(addr1) -> 1L, + new ForgerIdentifier(addr2) -> 100L, + new ForgerIdentifier(addr3) -> 9999999L, ) val bytes = ForgerBlockCountersSerializer.toBytes(forgerBlockCounters) ForgerBlockCountersSerializer.parseBytesTry(bytes) match { - case Failure(_) => fail("Parsing failed in ForgerBlockCountersSerializer") + case Failure(_) => fail("Parsing failed in ForgerBlockCountersSerializer") case Success(value) => assertEquals("Parsed value different from serialized value", value, forgerBlockCounters) } } @Test def serializationRoundTripTest_Empty(): Unit = { - val forgerBlockCounters = Map.empty[AddressProposition, Long] + val forgerBlockCounters = Map.empty[ForgerIdentifier, Long] val bytes = ForgerBlockCountersSerializer.toBytes(forgerBlockCounters) ForgerBlockCountersSerializer.parseBytesTry(bytes) match { - case Failure(_) => fail("Parsing failed in ForgerBlockCountersSerializer") + case Failure(_) => fail("Parsing failed in ForgerBlockCountersSerializer") + case Success(value) => assertEquals("Parsed value different from serialized value", value, forgerBlockCounters) + } + } + + @Test + def serializationRoundTripTest_NewFormat(): Unit = { + val addr1 = getPrivateKeySecp256k1(1000).publicImage() + val proposition1: PublicKey25519Proposition = + PrivateKey25519Creator.getInstance().generateSecret("test1".getBytes(StandardCharsets.UTF_8)).publicImage() + val vrfPublicKey1: VrfPublicKey = VrfGeneratedDataProvider.getVrfSecretKey(1).publicImage() + val addr2 = getPrivateKeySecp256k1(1001).publicImage() + val proposition2: PublicKey25519Proposition = + PrivateKey25519Creator.getInstance().generateSecret("test2".getBytes(StandardCharsets.UTF_8)).publicImage() + val vrfPublicKey2: VrfPublicKey = VrfGeneratedDataProvider.getVrfSecretKey(2).publicImage() + val addr3 = getPrivateKeySecp256k1(1002).publicImage() + val proposition3: PublicKey25519Proposition = + PrivateKey25519Creator.getInstance().generateSecret("test3".getBytes(StandardCharsets.UTF_8)).publicImage() + val vrfPublicKey3: VrfPublicKey = VrfGeneratedDataProvider.getVrfSecretKey(3).publicImage() + val forgerBlockCounters = Map( + new ForgerIdentifier(addr1, Some(ForgerPublicKeys(proposition1, vrfPublicKey1))) -> 1L, + new ForgerIdentifier(addr2, Some(ForgerPublicKeys(proposition2, vrfPublicKey2))) -> 100L, + new ForgerIdentifier(addr3, Some(ForgerPublicKeys(proposition3, vrfPublicKey3))) -> 9999999L, + ) + + val bytes = ForgerBlockCountersSerializer.toBytes(forgerBlockCounters) + + ForgerBlockCountersSerializer.parseBytesTry(bytes) match { + case Failure(_) => fail("Parsing failed in ForgerBlockCountersSerializer") case Success(value) => assertEquals("Parsed value different from serialized value", value, forgerBlockCounters) } } 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 3585acb7b4..3c41fed4eb 100644 --- a/sdk/src/test/scala/io/horizen/account/state/ForgerStakeMsgProcessorTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/ForgerStakeMsgProcessorTest.scala @@ -4,7 +4,7 @@ package io.horizen.account.state import com.google.common.primitives.Bytes import io.horizen.account.abi.{ABIDecoder, MsgProcessorInputDecoder} import io.horizen.account.fork.GasFeeFork.DefaultGasFeeFork -import io.horizen.account.fork.{Version1_2_0Fork, Version1_3_0Fork} +import io.horizen.account.fork.{Version1_2_0Fork, Version1_3_0Fork, Version1_4_0Fork} import io.horizen.account.proposition.AddressProposition import io.horizen.account.secret.{PrivateKeySecp256k1, PrivateKeySecp256k1Creator} import io.horizen.account.state.ForgerStakeMsgProcessor._ @@ -12,7 +12,7 @@ import io.horizen.account.state.ForgerStakeStorage.getStorageVersionFromDb import io.horizen.account.state.NativeSmartContractMsgProcessor.NULL_HEX_STRING_32 import io.horizen.account.state.events.{DelegateForgerStake, OpenForgerList, StakeUpgrade, WithdrawForgerStake} import io.horizen.account.state.receipt.EthereumConsensusDataLog -import io.horizen.account.utils.{EthereumTransactionDecoder, ZenWeiConverter} +import io.horizen.account.utils.{EthereumTransactionDecoder, WellKnownAddresses, ZenWeiConverter} import io.horizen.evm.{Address, Hash} import io.horizen.fixtures.StoreFixture import io.horizen.fork.{ForkManagerUtil, OptionalSidechainFork, SidechainForkConsensusEpoch, SimpleForkConfigurator} @@ -68,9 +68,11 @@ class ForgerStakeMsgProcessorTest val OpenForgerStakeListEventSig: Array[Byte] = getEventSignature("OpenForgerList(uint32,address,bytes32)") val NumOfIndexedOpenForgerStakeListEvtParams = 1 val StakeUpgradeEventSig: Array[Byte] = getEventSignature("StakeUpgrade(uint32,uint32)") + val DisableEventSig: Array[Byte] = getEventSignature("DisableStakeV1()") val V1_2_MOCK_FORK_POINT: Int = 100 val V1_3_MOCK_FORK_POINT: Int = 200 + val V1_4_MOCK_FORK_POINT: Int = 300 val blockContextForkV1_3 = new BlockContext( Address.ZERO, @@ -85,6 +87,19 @@ class ForgerStakeMsgProcessorTest Hash.ZERO ) + val blockContextForkV1_4 = new BlockContext( + Address.ZERO, + 0, + 0, + DefaultGasFeeFork.blockGasLimit, + 0, + V1_4_MOCK_FORK_POINT, + 0, + 1, + MockedHistoryBlockHashProvider, + Hash.ZERO + ) + @Before def setUp(): Unit = { val forkConfigurator = new SimpleForkConfigurator() { @@ -96,6 +111,10 @@ class ForgerStakeMsgProcessorTest new Pair[SidechainForkConsensusEpoch, OptionalSidechainFork]( SidechainForkConsensusEpoch(V1_3_MOCK_FORK_POINT, V1_3_MOCK_FORK_POINT, V1_3_MOCK_FORK_POINT), new Version1_3_0Fork(true) + ), + new Pair[SidechainForkConsensusEpoch, OptionalSidechainFork]( + SidechainForkConsensusEpoch(V1_4_MOCK_FORK_POINT, V1_4_MOCK_FORK_POINT, V1_4_MOCK_FORK_POINT), + new Version1_4_0Fork(true) ) ).asJava } @@ -131,7 +150,7 @@ class ForgerStakeMsgProcessorTest val msg = getMessage(contractAddress, 0, BytesUtils.fromHexString(RemoveStakeCmd) ++ data, nonce) // try processing the removal of stake, should succeed - val returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msg, stateView, defaultBlockContext, _)) + val returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msg, stateView, defaultBlockContext, _, stateView)) assertNotNull(returnData) assertArrayEquals(stakeId, returnData) } @@ -139,7 +158,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 = TestContext.process(forgerStakeMessageProcessor, msg, stateView, defaultBlockContext, gas) + val result = TestContext.process(forgerStakeMessageProcessor, msg, stateView, defaultBlockContext, gas, stateView) (result, gas.getUsedGas) } // gas consumption depends on the number of items in the list @@ -164,6 +183,7 @@ class ForgerStakeMsgProcessorTest assertEquals("Wrong MethodId for GetPagedListOfForgersCmd", "af5f63ef", ForgerStakeMsgProcessor.GetPagedListOfForgersCmd) assertEquals("Wrong MethodId for GetPagedForgersStakesOfUserCmd", "5f6dfc1d", ForgerStakeMsgProcessor.GetPagedForgersStakesOfUserCmd) assertEquals("Wrong MethodId for UpgradeCmd", "d55ec697", ForgerStakeMsgProcessor.UpgradeCmd) + assertEquals("Wrong MethodId for DisableAndMigrateCmd", "ca6aaf28", ForgerStakeMsgProcessor.DisableAndMigrateCmd) } @Test @@ -603,7 +623,7 @@ class ForgerStakeMsgProcessorTest view.setupTxContext(txHash2, 10) // try processing a msg with the same stake (same msg), should fail assertThrows[ExecutionRevertedException]( - withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, _, view)) ) // Checking that log doesn't change @@ -616,7 +636,7 @@ class ForgerStakeMsgProcessorTest // should fail because input has a trailing byte val ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(forgerStakeMessageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Wrong message data field length")) @@ -732,7 +752,7 @@ class ForgerStakeMsgProcessorTest val upgradeMsg = getMessage( contractAddress, 0, BytesUtils.fromHexString(UpgradeCmd), randomNonce, ownerAddressProposition.address()) - withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _, view)) // should fail because forger is not in the allowed list ex = intercept[ExecutionRevertedException] { @@ -762,7 +782,7 @@ class ForgerStakeMsgProcessorTest // test with new storage model val upgradeMsg = getMessage( contractAddress, 0, BytesUtils.fromHexString(UpgradeCmd), randomNonce, ownerAddressProposition.address()) - withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _, view)) ex = intercept[ExecutionRevertedException] { assertGas(0, msg, view, forgerStakeMessageProcessor, blockContextForkV1_3) } @@ -789,7 +809,7 @@ class ForgerStakeMsgProcessorTest // Same test after Forger Stake Storage activation val upgradeMsg = getMessage( contractAddress, 0, BytesUtils.fromHexString(UpgradeCmd), randomNonce, ownerAddressProposition.address()) - withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _, view)) ex = intercept[ExecutionRevertedException] { assertGas(0, msg, view, forgerStakeMessageProcessor, blockContextForkV1_3) @@ -841,7 +861,7 @@ class ForgerStakeMsgProcessorTest // test with new storage model val upgradeMsg = getMessage( contractAddress, 0, BytesUtils.fromHexString(UpgradeCmd), randomNonce, ownerAddressProposition.address()) - withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _, view)) assertThrows[ExecutionRevertedException] { assertGas(0, msg, view, forgerStakeMessageProcessor, blockContextForkV1_3) @@ -891,7 +911,7 @@ class ForgerStakeMsgProcessorTest // test with new storage model val upgradeMsg = getMessage( contractAddress, 0, BytesUtils.fromHexString(UpgradeCmd), randomNonce, ownerAddressProposition.address()) - withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _, view)) assertThrows[ExecutionRevertedException] { assertGas(4400, msg, view, forgerStakeMessageProcessor, blockContextForkV1_3) } @@ -933,7 +953,7 @@ class ForgerStakeMsgProcessorTest // test with new storage model val upgradeMsg = getMessage( contractAddress, 0, BytesUtils.fromHexString(UpgradeCmd), randomNonce, ownerAddressProposition.address()) - withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _, view)) ex = intercept[ExecutionRevertedException] { assertGas(0, msg, view, forgerStakeMessageProcessor, blockContextForkV1_3) } @@ -981,12 +1001,12 @@ class ForgerStakeMsgProcessorTest ForgerStakeData(ForgerPublicKeys(blockSignerProposition, vrfPublicKey), ownerAddressProposition, stakeAmount))) - val returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, _)) + val returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, _, view)) assertNotNull(returnData) } //Check getListOfForgers - val forgerList = forgerStakeMessageProcessor.getListOfForgersStakes(view, false) + val forgerList = forgerStakeMessageProcessor.getListOfForgersStakes(view, isForkV1_3Active = false) assertEquals(listOfExpectedForgerStakes, forgerList.asJava) view.commit(bytesToVersion(getVersion.data())) @@ -1033,7 +1053,7 @@ class ForgerStakeMsgProcessorTest listOfExpectedForgerStakes.add(AccountForgingStakeInfo(expStakeId, ForgerStakeData(ForgerPublicKeys(blockSignerProposition, vrfPublicKey), ownerAddressProposition, stakeAmount))) - val returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, _)) + val returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, _, view)) assertNotNull(returnData) } @@ -1091,7 +1111,7 @@ class ForgerStakeMsgProcessorTest val expStakeId = forgerStakeMessageProcessor.getStakeId(addNewStakeMsg) val forgingStakeInfo = AccountForgingStakeInfo(expStakeId, ForgerStakeData(ForgerPublicKeys(blockSignerProposition, vrfPublicKey), ownerAddressProposition, stakeAmount)) val addNewStakeReturnData = withGas( - TestContext.process(forgerStakeMessageProcessor, addNewStakeMsg, view, defaultBlockContext, _) + TestContext.process(forgerStakeMessageProcessor, addNewStakeMsg, view, defaultBlockContext, _, view) ) assertNotNull(addNewStakeReturnData) @@ -1107,20 +1127,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(TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, _, view)) } // 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(TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, _, view)) } // 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(TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Wrong message data field length")) @@ -1146,7 +1166,7 @@ class ForgerStakeMsgProcessorTest msg = getMessage(contractAddress, BigInteger.ZERO, BytesUtils.fromHexString(RemoveStakeCmd) ++ badData2, nonce) val ex2 = intercept[ExecutionRevertedException] { - withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, _, view)) } assertTrue(ex2.getMessage.contains("ill-formed signature")) @@ -1186,13 +1206,13 @@ class ForgerStakeMsgProcessorTest var msg = getMessage(contractAddress, BigInteger.ONE, BytesUtils.fromHexString(GetListOfForgersCmd), randomNonce) assertThrows[ExecutionRevertedException] { - TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, gas) + TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, gas, view) } msg = getMessage(contractAddress, BigInteger.valueOf(-1), BytesUtils.fromHexString(GetListOfForgersCmd), randomNonce) assertThrows[ExecutionRevertedException] { - TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, gas) + TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, gas, view) } } view.commit(bytesToVersion(getVersion.data())) @@ -1244,7 +1264,7 @@ class ForgerStakeMsgProcessorTest val txHash1 = Keccak256.hash("first tx") view.setupTxContext(txHash1, 10) - val returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _)) + val returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _, view)) val version = new BigInteger(1, returnData).intValueExact() assertEquals(ForgerStakeStorageVersion.VERSION_2.id, version) assertEquals(ForgerStakeStorageVersion.VERSION_2, getStorageVersionFromDb(view)) @@ -1277,7 +1297,7 @@ class ForgerStakeMsgProcessorTest // should fail because input has a trailing byte exc = intercept[ExecutionRevertedException] { - withGas(TestContext.process(forgerStakeMessageProcessor, msgBad, view, blockContextForkV1_3, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, msgBad, view, blockContextForkV1_3, _, view)) } assertTrue(exc.getMessage.contains("invalid msg data length")) @@ -1307,7 +1327,7 @@ class ForgerStakeMsgProcessorTest val upgradeMsg = getMessage( contractAddress, 0, BytesUtils.fromHexString(UpgradeCmd), randomNonce, ownerAddressProposition.address()) - withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _, view)) // Check that at the beginning there is no stake val getAllStakesMsg = getMessage(contractAddress, 0, BytesUtils.fromHexString(GetListOfForgersCmd), randomNonce) @@ -1360,7 +1380,7 @@ class ForgerStakeMsgProcessorTest view.setupTxContext(txHash2, 10) assertThrows[ExecutionRevertedException]( - withGas(TestContext.process(forgerStakeMessageProcessor, addStakeMsg1, view, blockContextForkV1_3, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, addStakeMsg1, view, blockContextForkV1_3, _, view)) ) // Checking that log doesn't change @@ -1373,7 +1393,7 @@ class ForgerStakeMsgProcessorTest // should fail because input has a trailing byte val ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(forgerStakeMessageProcessor, msgBad, view, blockContextForkV1_3, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, msgBad, view, blockContextForkV1_3, _, view)) } assertTrue(ex.getMessage.contains("Wrong message data field length")) @@ -1707,7 +1727,7 @@ class ForgerStakeMsgProcessorTest val upgradeMsg = getMessage( contractAddress, 0, BytesUtils.fromHexString(UpgradeCmd), randomNonce, ownerAddressProposition.address()) - withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _, view)) val (list_of_stakes, _) = addStakes(view, blockSignerProposition, vrfPublicKey, ownerAddressProposition, 10000, blockContextForkV1_3) val msg4 = getMessage(contractAddress, 0, BytesUtils.fromHexString(GetListOfForgersCmd), randomNonce) @@ -1776,10 +1796,10 @@ class ForgerStakeMsgProcessorTest // Calling upgrade val upgradeMsg = getMessage( contractAddress, 0, BytesUtils.fromHexString(UpgradeCmd), randomNonce, ownerAddressProposition.address()) - withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _, view)) // Test without any stake - var returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _)) + var returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _, view)) val expectedListOfStakes = Seq.empty[AccountForgingStakeInfo] var res = PagedListOfStakesOutputDecoder.decode(returnData) assertEquals(-1, res.nextStartPos) @@ -1794,7 +1814,7 @@ class ForgerStakeMsgProcessorTest contractAddress, 0, BytesUtils.fromHexString(GetPagedForgersStakesOfUserCmd) ++ cmdInput.encode(), nonce, ownerAddressProposition.address()) var excIllegalArgumentException = intercept[IllegalArgumentException] { - withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _, view)) } assertTrue(excIllegalArgumentException.getMessage.startsWith("Invalid position where to start reading forger stakes")) @@ -1813,7 +1833,7 @@ class ForgerStakeMsgProcessorTest val badData = Bytes.concat(cmdInput.encode(), new Array[Byte](1)) msg = getMessage(contractAddress, BigInteger.ZERO, BytesUtils.fromHexString(GetPagedForgersStakesOfUserCmd) ++ badData, nonce, ownerAddressProposition.address()) val ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _, view)) } assertTrue(ex.getMessage.contains("Wrong message data field length")) @@ -1852,7 +1872,7 @@ class ForgerStakeMsgProcessorTest contractAddress, 0, BytesUtils.fromHexString(GetPagedForgersStakesOfUserCmd) ++ cmdInput.encode(), nonce, ownerAddressProposition.address()) excIllegalArgumentException = intercept[IllegalArgumentException] { - withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _, view)) } assertTrue(excIllegalArgumentException.getMessage.startsWith("Invalid position where to start reading forger stakes")) @@ -1867,7 +1887,7 @@ class ForgerStakeMsgProcessorTest ) val msg = getMessage( contractAddress, 0, BytesUtils.fromHexString(GetPagedForgersStakesOfUserCmd) ++ cmdInput.encode(), nonce, ownerAddressProposition.address()) - returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _)) + returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _, view)) //Check getListOfForgers val res = PagedListOfStakesOutputDecoder.decode(returnData) assertEquals(page, res.listOfStakes) @@ -1984,11 +2004,11 @@ class ForgerStakeMsgProcessorTest // Calling upgrade val upgradeMsg = getMessage( contractAddress, 0, BytesUtils.fromHexString(UpgradeCmd), randomNonce, ownerAddressProposition.address()) - withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _, view)) // Test without any stake - var returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _)) + var returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _, view)) assertEquals(BigInteger.ZERO, decodeStakeOfResult(returnData)) // Check that it is not payable @@ -2005,7 +2025,7 @@ class ForgerStakeMsgProcessorTest val badData = Bytes.concat(cmdInput.encode(), new Array[Byte](1)) msg = getMessage(contractAddress, BigInteger.ZERO, BytesUtils.fromHexString(StakeOfCmd) ++ badData, nonce, ownerAddressProposition.address()) val ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _, view)) } assertTrue(ex.getMessage.contains("Wrong message data field length")) @@ -2037,7 +2057,7 @@ class ForgerStakeMsgProcessorTest ) msg = getMessage( contractAddress, 0, BytesUtils.fromHexString(StakeOfCmd) ++ cmdInput.encode(), nonce, ownerAddressProposition.address()) - val returnData1 = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _)) + val returnData1 = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _, view)) assertEquals(expectedAmount1, decodeStakeOfResult(returnData1)) @@ -2046,7 +2066,7 @@ class ForgerStakeMsgProcessorTest ) msg = getMessage( contractAddress, 0, BytesUtils.fromHexString(StakeOfCmd) ++ cmdInput.encode(), nonce, ownerAddressProposition.address()) - val returnData2 = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _)) + val returnData2 = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _, view)) assertEquals(expectedAmount2, decodeStakeOfResult(returnData2)) cmdInput = StakeOfCmdInput( @@ -2054,7 +2074,7 @@ class ForgerStakeMsgProcessorTest ) msg = getMessage( contractAddress, 0, BytesUtils.fromHexString(StakeOfCmd) ++ cmdInput.encode(), nonce, ownerAddressProposition.address()) - val returnData3 = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _)) + val returnData3 = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _, view)) assertEquals(expectedAmount3, decodeStakeOfResult(returnData3)) @@ -2069,7 +2089,7 @@ class ForgerStakeMsgProcessorTest val msgRemove = getMessage(contractAddress, 0, BytesUtils.fromHexString(RemoveStakeCmd) ++ removeCmdInput.encode(), nonce2) - returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msgRemove, view, blockContextForkV1_3, _)) + returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msgRemove, view, blockContextForkV1_3, _, view)) assertNotNull(returnData) @@ -2078,7 +2098,7 @@ class ForgerStakeMsgProcessorTest ) msg = getMessage( contractAddress, 0, BytesUtils.fromHexString(StakeOfCmd) ++ cmdInput.encode(), nonce, ownerAddressProposition.address()) - returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _)) + returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _, view)) assertEquals(expectedAmount3.subtract(listOfExpectedStakes3(3).forgerStakeData.stakedAmount), decodeStakeOfResult(returnData)) view.commit(bytesToVersion(getVersion.data())) @@ -2103,7 +2123,7 @@ class ForgerStakeMsgProcessorTest forgerStakeMessageProcessor.init(view, view.getConsensusEpochNumberAsInt) val upgradeMsg = getMessage( contractAddress, 0, BytesUtils.fromHexString(UpgradeCmd), randomNonce, ownerAddressProposition.address()) - withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _, view)) // create sender account with some fund in it @@ -2122,7 +2142,7 @@ class ForgerStakeMsgProcessorTest val expStakeId = forgerStakeMessageProcessor.getStakeId(addNewStakeMsg) val forgingStakeInfo = AccountForgingStakeInfo(expStakeId, ForgerStakeData(ForgerPublicKeys(blockSignerProposition, vrfPublicKey), ownerAddressProposition, stakeAmount)) val addNewStakeReturnData = withGas( - TestContext.process(forgerStakeMessageProcessor, addNewStakeMsg, view, blockContextForkV1_3, _) + TestContext.process(forgerStakeMessageProcessor, addNewStakeMsg, view, blockContextForkV1_3, _, view) ) assertNotNull(addNewStakeReturnData) @@ -2138,20 +2158,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(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _, view)) } // 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(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _, view)) } // 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(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _, view)) } assertTrue(ex.getMessage.contains("Wrong message data field length")) @@ -2177,7 +2197,7 @@ class ForgerStakeMsgProcessorTest msg = getMessage(contractAddress, BigInteger.ZERO, BytesUtils.fromHexString(RemoveStakeCmd) ++ badData2, nonce) val ex2 = intercept[ExecutionRevertedException] { - withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, _, view)) } assertTrue(ex2.getMessage.contains("ill-formed signature")) @@ -2202,19 +2222,19 @@ class ForgerStakeMsgProcessorTest forgerStakeMessageProcessor.init(view, view.getConsensusEpochNumberAsInt) val upgradeMsg = getMessage( contractAddress, 0, BytesUtils.fromHexString(UpgradeCmd), randomNonce, ownerAddressProposition.address()) - withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _)) + withGas(TestContext.process(forgerStakeMessageProcessor, upgradeMsg, view, blockContextForkV1_3, _, view)) withGas { gas => var msg = getMessage(contractAddress, BigInteger.ONE, BytesUtils.fromHexString(GetListOfForgersCmd), randomNonce) assertThrows[ExecutionRevertedException] { - TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, gas) + TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, gas, view) } msg = getMessage(contractAddress, BigInteger.valueOf(-1), BytesUtils.fromHexString(GetListOfForgersCmd), randomNonce) assertThrows[ExecutionRevertedException] { - TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, gas) + TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_3, gas, view) } } view.commit(bytesToVersion(getVersion.data())) @@ -2260,6 +2280,137 @@ class ForgerStakeMsgProcessorTest } } + @Test + def testDisableAndMigrate(): Unit = { + + usingView(forgerStakeMessageProcessor) { view => + + forgerStakeMessageProcessor.init(view, view.getConsensusEpochNumberAsInt) + + // create sender account with some fund in it + val initialAmount = BigInteger.valueOf(10).multiply(ZenWeiConverter.MAX_MONEY_IN_WEI) + createSenderAccount(view, initialAmount) + + //Setting the context + val txHash1 = Keccak256.hash("first tx") + view.setupTxContext(txHash1, 10) + + var nonce = 0 + + // Test with the correct signature before fork. It should fail. + var msg = getMessage( + contractAddress, 0, BytesUtils.fromHexString(DisableAndMigrateCmd), nonce, ownerAddressProposition.address()) + + // should fail because, before Version 1.4 fork, DisableCmd is not a valid function signature + var exc = intercept[ExecutionRevertedException] { + assertGas(0, msg, view, forgerStakeMessageProcessor, blockContextForkV1_3) + } + assertEquals(s"op code not supported: $DisableAndMigrateCmd", exc.getMessage) + + // Test after fork. + // Check that it is not payable + val value = validWeiAmount + msg = getMessage( + contractAddress, value, BytesUtils.fromHexString(DisableAndMigrateCmd), nonce, ownerAddressProposition.address()) + + val excPayable = intercept[ExecutionRevertedException] { + assertGas(2100, msg, view, forgerStakeMessageProcessor, blockContextForkV1_4) + } + assertEquals(s"Call value must be zero", excPayable.getMessage) + + // Check for wrong input data + val badData = new Array[Byte](1) + msg = getMessage(contractAddress, BigInteger.ZERO, BytesUtils.fromHexString(DisableAndMigrateCmd) ++ badData, nonce, ownerAddressProposition.address()) + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + assertTrue(exc.getMessage.contains("invalid msg data length")) + + + // Test that disable can be called only by Forger stake contract V2 + msg = getMessage( + contractAddress, 0, BytesUtils.fromHexString(DisableAndMigrateCmd), nonce, ownerAddressProposition.address()) + exc = intercept[ExecutionRevertedException] { + assertGas(2100, msg, view, forgerStakeMessageProcessor, blockContextForkV1_4) + } + assertEquals(s"Authorization failed", exc.getMessage) + + // Test that disable can be called not only by Forger stake contract V2 address but it needs also the hashcode (i.e. + // that it is initialized) + msg = getMessage( + contractAddress, 0, BytesUtils.fromHexString(DisableAndMigrateCmd), nonce, WellKnownAddresses.FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS) + + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + assertEquals(s"Authorization failed", exc.getMessage) + + ForgerStakeV2MsgProcessor.init(view, blockContextForkV1_4.consensusEpochNumber) + + assertGas(2100, msg, view, forgerStakeMessageProcessor, blockContextForkV1_4) + + val listOfLogs = view.getLogs(txHash1) + assertEquals("Wrong number of logs", 1, listOfLogs.length) + assertEquals("Wrong address", contractAddress, listOfLogs.head.address) + assertEquals("Wrong number of topics", 1, listOfLogs.head.topics.length) //The first topic is the hash of the signature of the event + assertArrayEquals("Wrong event signature", DisableEventSig, listOfLogs.head.topics(0).toBytes) + + + // Check that the old stakes methods cannot be called anymore + // Don't care about actual data, because it should be stopped before unmarshaling + exc = intercept[ExecutionRevertedException] { + msg = getMessage(contractAddress, validWeiAmount, BytesUtils.fromHexString(AddNewStakeCmd), randomNonce) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + assertTrue(s"Wrong error message ${exc.getMessage}", exc.getMessage.contains("disabled")) + + exc = intercept[ExecutionRevertedException] { + val msg = getMessage(contractAddress, 0, BytesUtils.fromHexString(RemoveStakeCmd), nonce) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + assertTrue(s"Wrong error message ${exc.getMessage}", exc.getMessage.contains("disabled")) + + exc = intercept[ExecutionRevertedException] { + val msg = getMessage(contractAddress, 0, BytesUtils.fromHexString(GetPagedListOfForgersCmd), nonce) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + assertTrue(s"Wrong error message ${exc.getMessage}", exc.getMessage.contains("disabled")) + + exc = intercept[ExecutionRevertedException] { + val msg = getMessage(contractAddress, 0, BytesUtils.fromHexString(GetListOfForgersCmd), nonce) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + assertTrue(s"Wrong error message ${exc.getMessage}", exc.getMessage.contains("disabled")) + + exc = intercept[ExecutionRevertedException] { + val msg = getMessage(contractAddress, 0, BytesUtils.fromHexString(UpgradeCmd), nonce) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + assertTrue(s"Wrong error message ${exc.getMessage}", exc.getMessage.contains("disabled")) + + exc = intercept[ExecutionRevertedException] { + val msg = getMessage(contractAddress, 0, BytesUtils.fromHexString(StakeOfCmd), nonce) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + assertTrue(s"Wrong error message ${exc.getMessage}", exc.getMessage.contains("disabled")) + + exc = intercept[ExecutionRevertedException] { + val msg = getMessage(contractAddress, 0, BytesUtils.fromHexString(GetPagedForgersStakesOfUserCmd), nonce) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + assertTrue(s"Wrong error message ${exc.getMessage}", exc.getMessage.contains("disabled")) + + exc = intercept[ExecutionRevertedException] { + val msg = getMessage(contractAddress, 0, BytesUtils.fromHexString(DisableAndMigrateCmd), nonce) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + assertTrue(s"Wrong error message ${exc.getMessage}", exc.getMessage.contains("disabled")) + + } + } + + + private def addStakes(view: AccountStateView, blockSignerProposition: PublicKey25519Proposition, vrfPublicKey: VrfPublicKey, @@ -2284,7 +2435,7 @@ class ForgerStakeMsgProcessorTest listOfForgerStakes = listOfForgerStakes :+ AccountForgingStakeInfo(expStakeId, ForgerStakeData(ForgerPublicKeys(blockSignerProposition, vrfPublicKey), ownerAddressProposition1, stakeAmount)) - val returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContext, _)) + val returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContext, _, view)) assertNotNull(returnData) totalAmount = totalAmount.add(stakeAmount) } @@ -2322,8 +2473,8 @@ class ForgerStakeMsgProcessorTest TypeReference.makeTypeReference(expectedEvent.value.getTypeAsString)) .asInstanceOf[util.List[TypeReference[Type[_]]]] val listOfDecodedData = FunctionReturnDecoder.decode(BytesUtils.toHexString(actualEvent.data), listOfRefs) - assertEquals("Wrong amount in data", expectedEvent.stakeId, listOfDecodedData.get(0)) - assertEquals("Wrong stakeId in data", expectedEvent.value, listOfDecodedData.get(1)) + assertEquals("Wrong stakeId in data", expectedEvent.stakeId, listOfDecodedData.get(0)) + assertEquals("Wrong value in data", expectedEvent.value, listOfDecodedData.get(1)) } def checkRemoveForgerStakeEvent(expectedEvent: WithdrawForgerStake, actualEvent: EthereumConsensusDataLog): Unit = { diff --git a/sdk/src/test/scala/io/horizen/account/state/ForgerStakeV2MsgProcessorTest.scala b/sdk/src/test/scala/io/horizen/account/state/ForgerStakeV2MsgProcessorTest.scala new file mode 100644 index 0000000000..5722a4c27e --- /dev/null +++ b/sdk/src/test/scala/io/horizen/account/state/ForgerStakeV2MsgProcessorTest.scala @@ -0,0 +1,2236 @@ +package io.horizen.account.state + +import com.google.common.primitives.Bytes +import io.horizen.account.fork.GasFeeFork.DefaultGasFeeFork +import io.horizen.account.fork.{Version1_3_0Fork, Version1_4_0Fork} +import io.horizen.account.network.{ForgerInfo, GetForgerOutputDecoder, PagedForgersOutputDecoder} +import io.horizen.account.proposition.AddressProposition +import io.horizen.account.secret.{PrivateKeySecp256k1, PrivateKeySecp256k1Creator} +import io.horizen.account.state.ForgerStakeMsgProcessor.{AddNewStakeCmd => AddNewStakeCmdV1, GetListOfForgersCmd => GetListOfForgersCmdV1} +import io.horizen.account.state.ForgerStakeV2MsgProcessor._ +import io.horizen.account.state.nativescdata.forgerstakev2.RegisterOrUpdateForgerCmdInputDecoder.NULL_ADDRESS_WITH_PREFIX_HEX_STRING +import io.horizen.account.state.nativescdata.forgerstakev2.StakeStorage._ +import io.horizen.account.state.nativescdata.forgerstakev2._ +import io.horizen.account.state.nativescdata.forgerstakev2.events.{DelegateForgerStake, RegisterForger, UpdateForger, WithdrawForgerStake} +import io.horizen.account.state.receipt.EthereumConsensusDataLog +import io.horizen.account.utils.ZenWeiConverter +import io.horizen.consensus.intToConsensusEpochNumber +import io.horizen.evm.{Address, Hash} +import io.horizen.fixtures.StoreFixture +import io.horizen.fork.{ForkConfigurator, ForkManagerUtil, OptionalSidechainFork, SidechainForkConsensusEpoch} +import io.horizen.params.NetworkParams +import io.horizen.proof.{Signature25519, VrfProof} +import io.horizen.proposition.{PublicKey25519Proposition, VrfPublicKey} +import io.horizen.secret.{PrivateKey25519, PrivateKey25519Creator, VrfSecretKey} +import io.horizen.utils.{BytesUtils, Pair, ZenCoinsUtils} +import io.horizen.vrf.VrfGeneratedDataProvider +import org.junit.Assert._ +import org.junit._ +import org.mockito.Mockito.when +import org.mockito._ +import org.scalatestplus.junit.JUnitSuite +import org.scalatestplus.mockito._ +import org.web3j.abi.datatypes.Type +import org.web3j.abi.{FunctionReturnDecoder, TypeReference} +import org.web3j.crypto.Keys +import org.web3j.utils.Numeric.hexStringToByteArray +import sparkz.core.bytesToVersion +import sparkz.crypto.hash.Keccak256 + +import java.math.BigInteger +import java.nio.charset.StandardCharsets +import java.util +import java.util.Optional +import scala.annotation.tailrec +import scala.collection.JavaConverters.seqAsJavaListConverter +import scala.language.implicitConversions + +class ForgerStakeV2MsgProcessorTest + extends JUnitSuite + with MockitoSugar + with MessageProcessorFixture + with StoreFixture { + + val dummyBigInteger: BigInteger = BigInteger.ONE + val negativeAmount: BigInteger = BigInteger.valueOf(-1) + + val invalidWeiAmount: BigInteger = new BigInteger("10000000001") + val validWeiAmount: BigInteger = new BigInteger("10000000000") + val minimumStakeWeiAmount: BigInteger = new BigInteger("10000000000000000000") + val validStakeWeiAmount: BigInteger = minimumStakeWeiAmount.multiply(2) + + val mockNetworkParams: NetworkParams = mock[NetworkParams] + val forgerStakeV2MessageProcessor: ForgerStakeV2MsgProcessor.type = ForgerStakeV2MsgProcessor + val forgerStakeMessageProcessor: ForgerStakeMsgProcessor = ForgerStakeMsgProcessor(mockNetworkParams) + + /** short hand: forger state native contract address */ + val contractAddress: Address = forgerStakeV2MessageProcessor.contractAddress + + // create private/public key pair + val privateKey: PrivateKeySecp256k1 = PrivateKeySecp256k1Creator.getInstance().generateSecret("nativemsgprocessortest".getBytes(StandardCharsets.UTF_8)) + val ownerAddressProposition: AddressProposition = privateKey.publicImage() + + val RegisterForgerEventSig: Array[Byte] = getEventSignature("RegisterForger(address,bytes32,bytes32,bytes1,uint256,uint32,address)") + val UpdateForgerEventSig: Array[Byte] = getEventSignature("UpdateForger(address,bytes32,bytes32,bytes1,uint32,address)") + val DelegateForgerStakeEventSig: Array[Byte] = getEventSignature("DelegateForgerStake(address,bytes32,bytes32,bytes1,uint256)") + val NumOfIndexedRegisterForgerEvtParams = 3 + val NumOfIndexedDelegateStakeEvtParams = 3 + val WithdrawForgerStakeEventSig: Array[Byte] = getEventSignature("WithdrawForgerStake(address,bytes32,bytes32,bytes1,uint256)") + val NumOfIndexedRemoveForgerStakeEvtParams = 1 + val OpenForgerStakeListEventSig: Array[Byte] = getEventSignature("OpenForgerList(uint32,address,bytes32)") + val NumOfIndexedOpenForgerStakeListEvtParams = 1 + val ActivateStakeV2EventSig: Array[Byte] = getEventSignature("ActivateStakeV2()") + + val scAddrStr1: String = "00C8F107a09cd4f463AFc2f1E6E5bF6022Ad4600" + val scAddressObj1 = new Address("0x" + scAddrStr1) + + val V1_4_MOCK_FORK_POINT: Int = 300 + val V1_3_MOCK_FORK_POINT: Int = 200 + + val blockContextForkV1_4 = new BlockContext( + Address.ZERO, + 0, + 0, + DefaultGasFeeFork.blockGasLimit, + 0, + V1_4_MOCK_FORK_POINT, + 0, + 1, + MockedHistoryBlockHashProvider, + Hash.ZERO + ) + + val blockContextForkV1_4_plus10 = new BlockContext( + Address.ZERO, + 0, + 0, + DefaultGasFeeFork.blockGasLimit, + 0, + V1_4_MOCK_FORK_POINT + 10, + 0, + 1, + MockedHistoryBlockHashProvider, + Hash.ZERO + ) + + val blockContextForkV1_3 = new BlockContext( + Address.ZERO, + 0, + 0, + DefaultGasFeeFork.blockGasLimit, + 0, + V1_3_MOCK_FORK_POINT, + 0, + 1, + MockedHistoryBlockHashProvider, + Hash.ZERO + ) + + + 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(V1_3_MOCK_FORK_POINT, V1_3_MOCK_FORK_POINT, V1_3_MOCK_FORK_POINT), Version1_3_0Fork(true)), + new Pair(SidechainForkConsensusEpoch(V1_4_MOCK_FORK_POINT, V1_4_MOCK_FORK_POINT, V1_4_MOCK_FORK_POINT), Version1_4_0Fork(true)), + ).asJava + } + + + @Before + def init(): Unit = { + ForkManagerUtil.initializeForkManager(new TestOptionalForkConfigurator, "regtest") + // by default start with fork active + Mockito.when(metadataStorageView.getConsensusEpochNumber).thenReturn(Option(intToConsensusEpochNumber(V1_4_MOCK_FORK_POINT))) + } + + + def getDefaultMessage(opCode: Array[Byte], arguments: Array[Byte], nonce: BigInteger, value: BigInteger = negativeAmount): Message = { + val data = Bytes.concat(opCode, arguments) + new Message( + origin, + Optional.of(contractAddress), // to + dummyBigInteger, // gasPrice + dummyBigInteger, // gasFeeCap + dummyBigInteger, // gasTipCap + dummyBigInteger, // gasLimit + value, + nonce, + data, + false) + } + + def randomNonce: BigInteger = randomU256 + + + @Test + def testInit(): Unit = { + usingView(forgerStakeV2MessageProcessor) { view => + // we have to call init beforehand + assertTrue(forgerStakeV2MessageProcessor.isForkActive(view.getConsensusEpochNumberAsInt)) + assertFalse(view.accountExists(contractAddress)) + forgerStakeV2MessageProcessor.init(view, view.getConsensusEpochNumberAsInt) + assertTrue(view.accountExists(contractAddress)) + assertTrue(view.isSmartContractAccount(contractAddress)) + view.commit(bytesToVersion(getVersion.data())) + } + } + + @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 RegisterForgerCmd", "408abed9", ForgerStakeV2MsgProcessor.RegisterForgerCmd) + assertEquals("Wrong MethodId for UpdateForgerCmd", "baed8f01", ForgerStakeV2MsgProcessor.UpdateForgerCmd) + assertEquals("Wrong MethodId for Delegatemd", "431abc18", ForgerStakeV2MsgProcessor.DelegateCmd) + assertEquals("Wrong MethodId for WithdrawCmd", "5639b873", ForgerStakeV2MsgProcessor.WithdrawCmd) + assertEquals("Wrong MethodId for StakeTotalCmd", "895117b1", ForgerStakeV2MsgProcessor.StakeTotalCmd) + assertEquals("Wrong MethodId for GetPagedForgersStakesByForgerCmd", "23359a85", ForgerStakeV2MsgProcessor.GetPagedForgersStakesByForgerCmd) + assertEquals("Wrong MethodId for GetPagedForgersStakesByDelegatorCmd", "e99e75ac", ForgerStakeV2MsgProcessor.GetPagedForgersStakesByDelegatorCmd) + assertEquals("Wrong MethodId for ActivateCmd", "0f15f4c0", ForgerStakeV2MsgProcessor.ActivateCmd) + assertEquals("Wrong MethodId for GetForgerCmd", "7d8589fd", ForgerStakeV2MsgProcessor.GetForgerCmd) + assertEquals("Wrong MethodId for GetPagedForgersCmd", "c1bf3d56", ForgerStakeV2MsgProcessor.GetPagedForgersCmd) + assertEquals("Wrong MethodId for GetCurrentConsensusEpochCmd", "cf94a955", ForgerStakeV2MsgProcessor.GetCurrentConsensusEpochCmd) + } + + + @Test + def testInitBeforeFork(): Unit = { + + Mockito.when(metadataStorageView.getConsensusEpochNumber).thenReturn( + Option(intToConsensusEpochNumber(V1_4_MOCK_FORK_POINT-1))) + + usingView(forgerStakeV2MessageProcessor) { view => + + assertFalse(view.accountExists(contractAddress)) + assertFalse(forgerStakeV2MessageProcessor.initDone(view)) + + assertFalse(forgerStakeV2MessageProcessor.isForkActive(view.getConsensusEpochNumberAsInt)) + + forgerStakeV2MessageProcessor.init(view, view.getConsensusEpochNumberAsInt) + + // assert no initialization took place + assertFalse(view.accountExists(contractAddress)) + assertFalse(forgerStakeV2MessageProcessor.initDone(view)) + } + } + + + @Test + def testDoubleInit(): Unit = { + + usingView(forgerStakeV2MessageProcessor) { view => + + assertTrue(forgerStakeV2MessageProcessor.isForkActive(view.getConsensusEpochNumberAsInt)) + + assertFalse(view.accountExists(contractAddress)) + assertFalse(forgerStakeV2MessageProcessor.initDone(view)) + + forgerStakeV2MessageProcessor.init(view, view.getConsensusEpochNumberAsInt) + + assertTrue(view.accountExists(contractAddress)) + assertTrue(forgerStakeV2MessageProcessor.initDone(view)) + + view.commit(bytesToVersion(getVersion.data())) + + val ex = intercept[MessageProcessorInitializationException] { + forgerStakeV2MessageProcessor.init(view, view.getConsensusEpochNumberAsInt) + } + assertTrue(ex.getMessage.contains("already init")) + } + } + + + @Test + def testCanProcess(): Unit = { + usingView(forgerStakeV2MessageProcessor) { view => + + // assert no initialization took place yet + assertFalse(view.accountExists(contractAddress)) + assertFalse(forgerStakeV2MessageProcessor.initDone(view)) + + assertTrue(forgerStakeV2MessageProcessor.isForkActive(view.getConsensusEpochNumberAsInt)) + + // correct contract address + assertTrue(TestContext.canProcess(forgerStakeV2MessageProcessor, getMessage(forgerStakeV2MessageProcessor.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(forgerStakeV2MessageProcessor, getMessage(forgerStakeV2MessageProcessor.contractAddress), view, view.getConsensusEpochNumberAsInt)) + + // wrong address + assertFalse(TestContext.canProcess(forgerStakeV2MessageProcessor, getMessage(randomAddress), view, view.getConsensusEpochNumberAsInt)) + // contract deployment: to == null + assertFalse(TestContext.canProcess(forgerStakeV2MessageProcessor, getMessage(null), view, view.getConsensusEpochNumberAsInt)) + + view.commit(bytesToVersion(getVersion.data())) + } + } + + @Test + def testCanNotProcessBeforeFork(): Unit = { + + Mockito.when(metadataStorageView.getConsensusEpochNumber).thenReturn( + Option(intToConsensusEpochNumber(1))) + + usingView(forgerStakeV2MessageProcessor) { 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) + + + assertFalse(forgerStakeV2MessageProcessor.isForkActive(view.getConsensusEpochNumberAsInt)) + + // correct contract address and message but fork not yet reached + assertFalse(TestContext.canProcess(forgerStakeV2MessageProcessor, getMessage(forgerStakeV2MessageProcessor.contractAddress), view, view.getConsensusEpochNumberAsInt)) + + // the init did not take place + assertFalse(view.accountExists(contractAddress)) + assertFalse(forgerStakeV2MessageProcessor.initDone(view)) + + view.commit(bytesToVersion(getVersion.data())) + } + } + + @Test + def testActivateBase(): Unit = { + + val processors = Seq(forgerStakeV2MessageProcessor, forgerStakeMessageProcessor) + usingView(processors) { view => + // Initialize old forger stake directly in V2. The upgrade is made automatically in the init, in this case. + forgerStakeMessageProcessor.init(view, V1_3_MOCK_FORK_POINT) + assertEquals(ForgerStakeStorageVersion.VERSION_2, ForgerStakeStorage.getStorageVersionFromDb(view)) + + forgerStakeV2MessageProcessor.init(view, view.getConsensusEpochNumberAsInt) + + // create sender account with some fund in it + val initialAmount = BigInteger.valueOf(10).multiply(ZenWeiConverter.MAX_MONEY_IN_WEI) + createSenderAccount(view, initialAmount) + + val nonce = 0 + + // Test "activate" before reaching the fork point. It should fail. + + var msg = getMessage( + contractAddress, 0, BytesUtils.fromHexString(ActivateCmd), nonce, ownerAddressProposition.address()) + + // should fail because, before Version 1.4 fork, ActivateCmd is not a valid function signature + val blockContextBeforeFork = new BlockContext( + Address.ZERO, + 0, + 0, + DefaultGasFeeFork.blockGasLimit, + 0, + V1_4_MOCK_FORK_POINT - 1, + 0, + 1, + MockedHistoryBlockHashProvider, + Hash.ZERO + ) + + var exc = intercept[ExecutionRevertedException] { + assertGas(0, msg, view, forgerStakeV2MessageProcessor, blockContextBeforeFork) + } + assertTrue(exc.getMessage.contains("fork not active")) + assertEquals(ForgerStakeStorageVersion.VERSION_2, ForgerStakeStorage.getStorageVersionFromDb(view)) + + + // Test after fork. + + //Setting the context + val txHash1 = Keccak256.hash("first tx") + view.setupTxContext(txHash1, 10) + + assertGasInterop(0, msg, view, processors, blockContextForkV1_4) + + // Checking log + val listOfLogs = view.getLogs(txHash1) + checkActivateEvents(listOfLogs) + + // Check that old forger stake message processor cannot be used anymore + + msg = getMessage(forgerStakeMessageProcessor.contractAddress, 0, BytesUtils.fromHexString(GetListOfForgersCmdV1), randomNonce) + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + assertTrue(s"Wrong error message ${exc.getMessage}", exc.getMessage.contains("disabled")) + + + // Negative tests + msg = getMessage( + contractAddress, 0, BytesUtils.fromHexString(ActivateCmd), nonce, ownerAddressProposition.address()) + // Check that it cannot be called twice + exc = intercept[ExecutionRevertedException] { + assertGasInterop(0, msg, view, processors, blockContextForkV1_4) + } + assertEquals(s"Forger stake V2 already activated", exc.getMessage) + + // Check that it is not payable + val value = validWeiAmount + msg = getMessage( + contractAddress, value, BytesUtils.fromHexString(ActivateCmd), nonce, ownerAddressProposition.address()) + + val excPayable = intercept[ExecutionRevertedException] { + assertGasInterop(0, msg, view, processors, blockContextForkV1_4) + } + assertEquals("Call value must be zero", excPayable.getMessage) + + // try processing a msg with a trailing byte in the arguments + val badData = new Array[Byte](1) + val msgBad = getMessage(contractAddress, 0, BytesUtils.fromHexString(ActivateCmd) ++ badData, randomNonce) + + // should fail because input has a trailing byte + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msgBad, view, blockContextForkV1_4, _, view)) + } + assertTrue(s"Wrong exc message: ${exc.getMessage}, expected:invalid msg data length", exc.getMessage.contains("invalid msg data length")) + view.commit(bytesToVersion(getVersion.data())) + } + } + + @Test + def testActivate(): Unit = { + val processors = Seq(forgerStakeV2MessageProcessor, forgerStakeMessageProcessor) + usingView(processors) { view => + + forgerStakeMessageProcessor.init(view, V1_3_MOCK_FORK_POINT) + + // create sender account with some fund in it + val initialAmount = ZenWeiConverter.MAX_MONEY_IN_WEI + createSenderAccount(view, initialAmount) + + val listOfExpectedResults = (1 to 5).map {idx => + + val blockSignerProposition = new PublicKey25519Proposition(BytesUtils.fromHexString(s"112233445566778811223344556677881122334455667788112233445566778$idx")) // 32 bytes + val vrfPublicKey = new VrfPublicKey(BytesUtils.fromHexString(s"d6b775fd4cefc7446236683fdde9d0464bba43cc565fa066b0b3ed1b888b9d118$idx")) // 33 bytes + + Mockito.when(mockNetworkParams.restrictForgers).thenReturn(true) + Mockito.when(mockNetworkParams.allowedForgersList).thenReturn(Seq((blockSignerProposition, vrfPublicKey))) + + // Create some stakes with old storage model + val privateKey1: PrivateKeySecp256k1 = PrivateKeySecp256k1Creator.getInstance().generateSecret("nativemsgprocessortest1".getBytes(StandardCharsets.UTF_8)) + val owner1: AddressProposition = privateKey1.publicImage() + val amount1 = addStakesV2(view, blockSignerProposition, vrfPublicKey, owner1, 400, blockContextForkV1_3) + + val privateKey2: PrivateKeySecp256k1 = PrivateKeySecp256k1Creator.getInstance().generateSecret("nativemsgprocessortest2".getBytes(StandardCharsets.UTF_8)) + val owner2: AddressProposition = privateKey2.publicImage() + val amount2 = addStakesV2(view, blockSignerProposition, vrfPublicKey, owner2, 350, blockContextForkV1_3) + + val privateKey3: PrivateKeySecp256k1 = PrivateKeySecp256k1Creator.getInstance().generateSecret("nativemsgprocessortest3".getBytes(StandardCharsets.UTF_8)) + val owner3: AddressProposition = privateKey3.publicImage() + val amount3 = addStakesV2(view, blockSignerProposition, vrfPublicKey, owner3, 250, blockContextForkV1_3) + val listOfStakes = (owner3, amount3) :: (owner2, amount2) :: (owner1, amount1) :: Nil + (ForgerPublicKeys(blockSignerProposition, vrfPublicKey), listOfStakes) + } + + + // The balance of forgerStakeMessageProcessor corresponds to the total staked amount. Note that this is not always + // true, e.g. a forward transfer can increase the balance. + + val forgerStakeBalanceBeforeActivate = view.getBalance(forgerStakeMessageProcessor.contractAddress) + + // Check that before activate the balance of ForgerStakeV2 is zero + assertEquals(BigInteger.ZERO, view.getBalance(contractAddress)) + + //Setting the context + val txHash1 = Keccak256.hash("first tx") + view.setupTxContext(txHash1, 10) + + val activateMsg = getMessage( + contractAddress, 0, BytesUtils.fromHexString(ActivateCmd), randomNonce, ownerAddressProposition.address()) + assertGasInterop(0, activateMsg, view, processors, blockContextForkV1_4) + + val listOfStakes = StakeStorage.getAllForgerStakes(view) + val expNumOfStakes = listOfExpectedResults.foldLeft(0){(sum, res) => sum + res._2.size } + assertEquals(expNumOfStakes, listOfStakes.size) + + listOfExpectedResults.foreach{ case (forgerKeys, expListOfStakes) => + val forgerOpt = StakeStorage.getForger(view, forgerKeys.blockSignPublicKey, forgerKeys.vrfPublicKey) + assertFalse(forgerOpt.isEmpty) + assertEquals(forgerKeys.blockSignPublicKey, forgerOpt.get.forgerPublicKeys.blockSignPublicKey) + assertEquals(forgerKeys.vrfPublicKey, forgerOpt.get.forgerPublicKeys.vrfPublicKey) + assertEquals(0, forgerOpt.get.rewardShare) + assertEquals(Address.ZERO, forgerOpt.get.rewardAddress.address()) + + val forgerKey = ForgerKey(forgerKeys.blockSignPublicKey, forgerKeys.vrfPublicKey) + val forgerHistory = ForgerStakeHistory(forgerKey) + assertEquals(1, forgerHistory.getSize(view)) + assertEquals(blockContextForkV1_4.consensusEpochNumber, forgerHistory.getCheckpoint(view, 0).fromEpochNumber) + assertEquals(expListOfStakes.foldLeft(BigInteger.ZERO){(sum, pair) => sum.add(pair._2)}, forgerHistory.getCheckpoint(view, 0).stakedAmount) + + val listOfDelegators = DelegatorList(forgerKey) + assertEquals(expListOfStakes.size, listOfDelegators.getSize(view)) + + expListOfStakes.foreach{ case (expDelegator, expAmount) => + val stake1 = listOfStakes.find(stake => (stake.ownerPublicKey == expDelegator) && (stake.forgerPublicKeys == forgerKeys)) + assertTrue(stake1.isDefined) + assertEquals(expAmount, stake1.get.stakedAmount) + assertEquals(forgerOpt.get.forgerPublicKeys, stake1.get.forgerPublicKeys) + val stakeHistory = StakeHistory(forgerKey, DelegatorKey(expDelegator.address())) + assertEquals(1, stakeHistory.getSize(view)) + assertEquals(blockContextForkV1_4.consensusEpochNumber, stakeHistory.getCheckpoint(view, 0).fromEpochNumber) + assertEquals(expAmount, stakeHistory.getCheckpoint(view, 0).stakedAmount) + + } + + } + + // Checking log + val listOfLogs = view.getLogs(txHash1) + checkActivateEvents(listOfLogs) + + assertEquals(BigInteger.ZERO, view.getBalance(forgerStakeMessageProcessor.contractAddress)) + assertEquals(forgerStakeBalanceBeforeActivate, view.getBalance(contractAddress)) + + view.commit(bytesToVersion(getVersion.data())) + + } + } + + def getBlockContextForEpoch(epochNum: Int): BlockContext = new BlockContext( + Address.ZERO, + 0, + 0, + DefaultGasFeeFork.blockGasLimit, + 0, + epochNum, + 0, + 1, + MockedHistoryBlockHashProvider, + Hash.ZERO + ) + + + @Test + def testRegisterAndUpdateForger(): Unit = { + + val processors = Seq(forgerStakeV2MessageProcessor, forgerStakeMessageProcessor) + + usingView(processors) { view => + forgerStakeMessageProcessor.init(view, V1_3_MOCK_FORK_POINT) + forgerStakeV2MessageProcessor.init(view, view.getConsensusEpochNumberAsInt) + + // create sender account with some fund in it + val initialAmount = BigInteger.valueOf(100).multiply(validStakeWeiAmount) + val senderAddress = new Address("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + createSenderAccount(view, initialAmount, inAddress = senderAddress) + + //Setting the context for forger 1 + val pair25519_f1 = get25519KeyPair(1) + val pairVrf_f1 = getVrfKeyPair(1) + val blockSignerProposition_f1 = pair25519_f1._2 + val vrfPublicKey_f1 = pairVrf_f1._2 + val rewardShare_f1: Int = 0 + val rewardAddress_f1 = new AddressProposition(hexStringToByteArray(NULL_ADDRESS_WITH_PREFIX_HEX_STRING)) + val msg_f1 = ForgerStakeV2MsgProcessor.getHashedMessageToSign( + BytesUtils.toHexString(pair25519_f1._2.pubKeyBytes()), + BytesUtils.toHexString(pairVrf_f1._2.pubKeyBytes()), + rewardShare_f1, + Keys.toChecksumAddress(BytesUtils.toHexString(rewardAddress_f1.address().toBytes))) + val (signature25519, signatureVrf) = getSignatures(pair25519_f1._1, pairVrf_f1._1, msg_f1) + + ///////////////////////////////////////////////////////////////////////////////////////////// + // Before activate tests + ///////////////////////////////////////////////////////////////////////////////////////////// + + var regCmdInput = RegisterOrUpdateForgerCmdInput( + ForgerPublicKeys(blockSignerProposition_f1, vrfPublicKey_f1), rewardShare_f1, rewardAddress_f1.address(), signature25519, signatureVrf + ) + + var registerForgerData: Array[Byte] = BytesUtils.fromHexString(RegisterForgerCmd) ++ regCmdInput.encode() + var msg = getMessage(contractAddress, validStakeWeiAmount, registerForgerData, randomNonce, from = senderAddress) + + // Check that register forger cannot be called before activate + + var exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + var expectedErr = "Forger stake V2 has not been activated yet" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // Call activate + val initialOwnerBalance = BigInteger.valueOf(200).multiply(validStakeWeiAmount) + createSenderAccount(view, initialOwnerBalance, ownerAddressProposition.address()) + + val activateMsg = getMessage( + contractAddress, 0, BytesUtils.fromHexString(ActivateCmd), randomNonce, ownerAddressProposition.address()) + assertGasInterop(0, activateMsg, view, processors, blockContextForkV1_4) + + // first negative tests for testing the signature validity and stake amount + //------------------------------------------------------------------ + // Try register with an invalid signature 25519. It should fail. + val signature25519Bad: Signature25519 = new Signature25519(BytesUtils.fromHexString("074c8f9c17a54ffc661376b5cd8baf7fbcdfc009f5b8106c14bcf022214ad0db164e5fbbb6e1f6d5b44945c81ed6d113fcf58caec47adc7e4cf84a2070416c09")) + + regCmdInput = RegisterOrUpdateForgerCmdInput( + ForgerPublicKeys(blockSignerProposition_f1, vrfPublicKey_f1), rewardShare_f1, rewardAddress_f1.address(), signature25519Bad, signatureVrf + ) + registerForgerData = BytesUtils.fromHexString(RegisterForgerCmd) ++ regCmdInput.encode() + msg = getMessage(contractAddress, validStakeWeiAmount, registerForgerData, randomNonce, from = senderAddress) + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + expectedErr = "Invalid signature, could not validate against blockSignerProposition" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // Try register with an invalid signature vrf. It should fail. + val signatureVrfBad: VrfProof = new VrfProof(BytesUtils.fromHexString("03380183fea2c1d43a064cfeda6e4bc92ae5ab855a2388606b3b9d9f4dc9b90d8014eb09085d22f03c0c7fdd7b9864fcb5c3b31b187281a9eefccc98ce4b0c69008222de8501b929dc1d08f67c29033ac352671e11d4e8037cf192f05cbe584d24")) + + regCmdInput = RegisterOrUpdateForgerCmdInput( + ForgerPublicKeys(blockSignerProposition_f1, vrfPublicKey_f1), rewardShare_f1, rewardAddress_f1.address(), signature25519, signatureVrfBad + ) + registerForgerData = BytesUtils.fromHexString(RegisterForgerCmd) ++ regCmdInput.encode() + msg = getMessage(contractAddress, validStakeWeiAmount, registerForgerData, randomNonce, from = senderAddress) + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + expectedErr = "Invalid signature, could not validate against vrfKey" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // Try register with too low a stake amount. It should fail. + regCmdInput = RegisterOrUpdateForgerCmdInput( + ForgerPublicKeys(blockSignerProposition_f1, vrfPublicKey_f1), rewardShare_f1, rewardAddress_f1.address(), signature25519, signatureVrf + ) + registerForgerData = BytesUtils.fromHexString(RegisterForgerCmd) ++ regCmdInput.encode() + msg = getMessage(contractAddress, validWeiAmount, registerForgerData, randomNonce, from = senderAddress) + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + expectedErr = "is below the minimum stake amount threshold" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // Try register with an illegal stake amount. It should fail. + regCmdInput = RegisterOrUpdateForgerCmdInput( + ForgerPublicKeys(blockSignerProposition_f1, vrfPublicKey_f1), rewardShare_f1, rewardAddress_f1.address(), signature25519, signatureVrf + ) + registerForgerData = BytesUtils.fromHexString(RegisterForgerCmd) ++ regCmdInput.encode() + msg = getMessage(contractAddress, validStakeWeiAmount.add(BigInteger.ONE), registerForgerData, randomNonce, from = senderAddress) + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + expectedErr = "is not a legal wei amount" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + + // verify we can register a forger after the activation with the proper parameters + + val initialSenderBalance = view.getBalance(senderAddress) + val initialNscBalance = view.getBalance(contractAddress) + + val txHash_f1 = Keccak256.hash("first forger tx") + view.setupTxContext(txHash_f1, 10) + + regCmdInput = RegisterOrUpdateForgerCmdInput( + ForgerPublicKeys(blockSignerProposition_f1, vrfPublicKey_f1), rewardShare_f1, rewardAddress_f1.address(), signature25519, signatureVrf + ) + registerForgerData = BytesUtils.fromHexString(RegisterForgerCmd) ++ regCmdInput.encode() + msg = getMessage(contractAddress, validStakeWeiAmount, registerForgerData, randomNonce, from = senderAddress) + assertGas(294999, msg, view, forgerStakeV2MessageProcessor, blockContextForkV1_4) + + // Check log event + val listOfLogs_f1 = view.getLogs(txHash_f1) + assertEquals("Wrong number of logs", 1, listOfLogs_f1.length) + val expectedEvent_f1 = RegisterForger(msg.getFrom, regCmdInput.forgerPublicKeys.blockSignPublicKey, + regCmdInput.forgerPublicKeys.vrfPublicKey, validStakeWeiAmount, rewardShare_f1, rewardAddress_f1.address()) + + checkRegisterForgerEvent(expectedEvent_f1, listOfLogs_f1(0)) + + // check balances + val finaleSenderBalance = view.getBalance(senderAddress) + val finalNscBalance = view.getBalance(contractAddress) + + assertEquals(initialNscBalance.add(validStakeWeiAmount), finalNscBalance) + assertEquals(initialSenderBalance.subtract(validStakeWeiAmount), finaleSenderBalance) + + // Negative tests + // ------------------------------------------------------------------------------- + // Try register the same forger twice. It should fail. + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + expectedErr = "Can not register an already existing forger" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // Try register with an inconsistent reward share and reward address. It should fail. + var rewardShareTest = 1 + + regCmdInput = RegisterOrUpdateForgerCmdInput( + ForgerPublicKeys(blockSignerProposition_f1, vrfPublicKey_f1), rewardShareTest, rewardAddress_f1.address(), signature25519, signatureVrf + ) + registerForgerData = BytesUtils.fromHexString(RegisterForgerCmd) ++ regCmdInput.encode() + msg = getMessage(contractAddress, validStakeWeiAmount, registerForgerData, randomNonce, from = senderAddress) + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + expectedErr = "Reward share cannot be different from 0 if reward address is not defined" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // Try register with an inconsistent reward share and reward address. It should fail. + rewardShareTest = 0 + val smartContractAddressTest = new AddressProposition(hexStringToByteArray("0011223344556677889900112233445566778899")) + regCmdInput = RegisterOrUpdateForgerCmdInput( + ForgerPublicKeys(blockSignerProposition_f1, vrfPublicKey_f1), rewardShare_f1, smartContractAddressTest.address(), signature25519, signatureVrf + ) + registerForgerData = BytesUtils.fromHexString(RegisterForgerCmd) ++ regCmdInput.encode() + msg = getMessage(contractAddress, validStakeWeiAmount, registerForgerData, randomNonce, from = senderAddress) + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + expectedErr = "Reward share cannot be 0 if reward address is defined" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // we guard the validity range of reward share in the RegisterForgerCmdInput ctor + rewardShareTest = -1 + var exc2 = intercept[IllegalArgumentException] { + RegisterOrUpdateForgerCmdInput( + ForgerPublicKeys(blockSignerProposition_f1, vrfPublicKey_f1), rewardShareTest, rewardAddress_f1.address(), signature25519, signatureVrf + ) + } + expectedErr = "reward share expected to be non negative" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc2.getMessage}", exc2.getMessage.contains(expectedErr)) + + rewardShareTest = 1001 + exc2 = intercept[IllegalArgumentException] { + RegisterOrUpdateForgerCmdInput( + ForgerPublicKeys(blockSignerProposition_f1, vrfPublicKey_f1), rewardShareTest, rewardAddress_f1.address(), signature25519, signatureVrf + ) + } + expectedErr = "reward share expected to be 1000 at most" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc2.getMessage}", exc2.getMessage.contains(expectedErr)) + + // Try register from a sender with not enough funds. It should fail. + rewardShareTest = 0 + regCmdInput = RegisterOrUpdateForgerCmdInput( + ForgerPublicKeys(blockSignerProposition_f1, vrfPublicKey_f1), rewardShare_f1, rewardAddress_f1.address(), signature25519, signatureVrf + ) + registerForgerData = BytesUtils.fromHexString(RegisterForgerCmd) ++ regCmdInput.encode() + msg = getMessage(contractAddress, validStakeWeiAmount, registerForgerData, randomNonce, from = origin) + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + expectedErr = "Not enough balance" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + + //Setting the context for forger 2 + val pair25519_f2 = get25519KeyPair(2) + val pairVrf_f2 = getVrfKeyPair(2) + val blockSignerProposition_f2 = pair25519_f2._2 + val vrfPublicKey_f2 = pairVrf_f2._2 + val rewardShare_f2: Int = 123 + val rewardAddress_f2 = new AddressProposition(hexStringToByteArray("ca12fcb886cbf73a39d87aac9610f8a303536642")) + val msg_f2 = ForgerStakeV2MsgProcessor.getHashedMessageToSign( + BytesUtils.toHexString(pair25519_f2._2.pubKeyBytes()), + BytesUtils.toHexString(pairVrf_f2._2.pubKeyBytes()), + rewardShare_f2, + Keys.toChecksumAddress(BytesUtils.toHexString(rewardAddress_f2.address().toBytes))) + val (signature25519_f2, signatureVrf_f2) = getSignatures(pair25519_f2._1, pairVrf_f2._1, msg_f2) + + + val txHash_f2 = Keccak256.hash("second forger tx") + view.setupTxContext(txHash_f2, 11) + + // add one more forger 2 + regCmdInput = RegisterOrUpdateForgerCmdInput( + ForgerPublicKeys(blockSignerProposition_f2, vrfPublicKey_f2), rewardShare_f2, rewardAddress_f2.address(), signature25519_f2, signatureVrf_f2 + ) + registerForgerData = BytesUtils.fromHexString(RegisterForgerCmd) ++ regCmdInput.encode() + msg = getMessage(contractAddress, validStakeWeiAmount, registerForgerData, randomNonce, from = senderAddress) + assertGas(275099, msg, view, forgerStakeV2MessageProcessor, blockContextForkV1_4) + + // Check log event + val listOfLogs_f2 = view.getLogs(txHash_f2) + assertEquals("Wrong number of logs", 1, listOfLogs_f2.length) + val expectedEvent_f2 = RegisterForger(msg.getFrom, regCmdInput.forgerPublicKeys.blockSignPublicKey, + regCmdInput.forgerPublicKeys.vrfPublicKey, validStakeWeiAmount, rewardShare_f2, rewardAddress_f2.address()) + + checkRegisterForgerEvent(expectedEvent_f2, listOfLogs_f2(0)) + + // Try getForger, with first forger + var getForgerCmdInput = SelectByForgerCmdInput( + ForgerPublicKeys(blockSignerProposition_f1, vrfPublicKey_f1) + ) + + var getForgerData: Array[Byte] = BytesUtils.fromHexString(GetForgerCmd) ++ getForgerCmdInput.encode() + var msgGetForger = getMessage(contractAddress, BigInteger.ZERO, getForgerData, randomNonce) + val res1 = assertGas(10600, msgGetForger, view, forgerStakeV2MessageProcessor, blockContextForkV1_4) + + var getForgerOutput = GetForgerOutputDecoder.decode(res1) + assertEquals(getForgerCmdInput.forgerPublicKeys, getForgerOutput.forgerPublicKeys) + assertEquals(0, getForgerOutput.rewardShare) + assertEquals(Address.ZERO, getForgerOutput.rewardAddress.address()) + + // Try getForger, with second forger + getForgerCmdInput = SelectByForgerCmdInput( + ForgerPublicKeys(blockSignerProposition_f2, vrfPublicKey_f2) + ) + + getForgerData = BytesUtils.fromHexString(GetForgerCmd) ++ getForgerCmdInput.encode() + msgGetForger = getMessage(contractAddress, BigInteger.ZERO, getForgerData, randomNonce) + val res2 = assertGas(10600, msgGetForger, view, forgerStakeV2MessageProcessor, blockContextForkV1_4) + + getForgerOutput = GetForgerOutputDecoder.decode(res2) + assertEquals(getForgerCmdInput.forgerPublicKeys, getForgerOutput.forgerPublicKeys) + assertEquals(rewardShare_f2, getForgerOutput.rewardShare) + assertEquals(rewardAddress_f2.address(), getForgerOutput.rewardAddress.address()) + + // forger update + //--------------------------------------- + val rewardShare_update: Int = 33 + val reward_address_update = new AddressProposition(hexStringToByteArray("3333333333333333333333333333333333333333")) + + // update first forger + // - Try updating a forger setting an invalid reward share (null) + var rewardShare_update_bad: Int = 0 + var msg_u1_bad = ForgerStakeV2MsgProcessor.getHashedMessageToSign( + BytesUtils.toHexString(pair25519_f1._2.pubKeyBytes()), + BytesUtils.toHexString(pairVrf_f1._2.pubKeyBytes()), + rewardShare_update_bad, + Keys.toChecksumAddress(BytesUtils.toHexString(reward_address_update.address().toBytes))) + var (signature25519_u1_bad, signatureVrf_u1_bad) = getSignatures(pair25519_f1._1, pairVrf_f1._1, msg_u1_bad) + var regCmdInput_bad = RegisterOrUpdateForgerCmdInput( + ForgerPublicKeys(blockSignerProposition_f1, vrfPublicKey_f1), rewardShare_update_bad, reward_address_update.address(), signature25519_u1_bad, signatureVrf_u1_bad + ) + var updateForgerData_bad = BytesUtils.fromHexString(UpdateForgerCmd) ++ regCmdInput_bad.encode() + msg = getMessage(contractAddress, BigInteger.ZERO, updateForgerData_bad, randomNonce, from = senderAddress) + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + expectedErr = "Illegal reward share value" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // - Try updating a forger setting an invalid reward share (too big) + rewardShare_update_bad = 1001 + msg_u1_bad = ForgerStakeV2MsgProcessor.getHashedMessageToSign( + BytesUtils.toHexString(pair25519_f1._2.pubKeyBytes()), + BytesUtils.toHexString(pairVrf_f1._2.pubKeyBytes()), + rewardShare_update_bad, + Keys.toChecksumAddress(BytesUtils.toHexString(reward_address_update.address().toBytes))) + var sigs_tuple = getSignatures(pair25519_f1._1, pairVrf_f1._1, msg_u1_bad) + signature25519_u1_bad = sigs_tuple._1 + signatureVrf_u1_bad = sigs_tuple._2 + var exc_ill = intercept[IllegalArgumentException] { + RegisterOrUpdateForgerCmdInput( + ForgerPublicKeys(blockSignerProposition_f1, vrfPublicKey_f1), rewardShare_update_bad, reward_address_update.address(), signature25519_u1_bad, signatureVrf_u1_bad + ) + } + expectedErr = "reward share expected to be 1000 at most" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc_ill.getMessage.contains(expectedErr)) + + // - Try updating a forger setting an invalid reward share (negative) + rewardShare_update_bad = -10 + msg_u1_bad = ForgerStakeV2MsgProcessor.getHashedMessageToSign( + BytesUtils.toHexString(pair25519_f1._2.pubKeyBytes()), + BytesUtils.toHexString(pairVrf_f1._2.pubKeyBytes()), + rewardShare_update_bad, + Keys.toChecksumAddress(BytesUtils.toHexString(reward_address_update.address().toBytes))) + sigs_tuple = getSignatures(pair25519_f1._1, pairVrf_f1._1, msg_u1_bad) + signature25519_u1_bad = sigs_tuple._1 + signatureVrf_u1_bad = sigs_tuple._2 + exc_ill = intercept[IllegalArgumentException] { + RegisterOrUpdateForgerCmdInput( + ForgerPublicKeys(blockSignerProposition_f1, vrfPublicKey_f1), rewardShare_update_bad, reward_address_update.address(), signature25519_u1_bad, signatureVrf_u1_bad + ) + } + expectedErr = "reward share expected to be non negative" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc_ill.getMessage.contains(expectedErr)) + + // - Try updating a forger setting an invalid reward address (null) + val reward_address_update_bad = new AddressProposition(hexStringToByteArray("0000000000000000000000000000000000000000")) + msg_u1_bad = ForgerStakeV2MsgProcessor.getHashedMessageToSign( + BytesUtils.toHexString(pair25519_f1._2.pubKeyBytes()), + BytesUtils.toHexString(pairVrf_f1._2.pubKeyBytes()), + rewardShare_update, + Keys.toChecksumAddress(BytesUtils.toHexString(reward_address_update_bad.address().toBytes))) + sigs_tuple = getSignatures(pair25519_f1._1, pairVrf_f1._1, msg_u1_bad) + signature25519_u1_bad = sigs_tuple._1 + signatureVrf_u1_bad = sigs_tuple._2 + regCmdInput_bad = RegisterOrUpdateForgerCmdInput( + ForgerPublicKeys(blockSignerProposition_f1, vrfPublicKey_f1), rewardShare_update, reward_address_update_bad.address(), signature25519_u1_bad, signatureVrf_u1_bad + ) + updateForgerData_bad = BytesUtils.fromHexString(UpdateForgerCmd) ++ regCmdInput_bad.encode() + msg = getMessage(contractAddress, BigInteger.ZERO, updateForgerData_bad, randomNonce, from = senderAddress) + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + expectedErr = "Reward address cannot be the ZERO address" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // - Try updating a forger that does not exist + msg_u1_bad = ForgerStakeV2MsgProcessor.getHashedMessageToSign( + BytesUtils.toHexString(pair25519_f1._2.pubKeyBytes()), + BytesUtils.toHexString(pairVrf_f2._2.pubKeyBytes()), // mix of f1/f2 keys + rewardShare_update, + Keys.toChecksumAddress(BytesUtils.toHexString(reward_address_update.address().toBytes))) + sigs_tuple = getSignatures(pair25519_f1._1, pairVrf_f1._1, msg_u1_bad) + signature25519_u1_bad = sigs_tuple._1 + signatureVrf_u1_bad = sigs_tuple._2 + regCmdInput_bad = RegisterOrUpdateForgerCmdInput( + ForgerPublicKeys(blockSignerProposition_f1, vrfPublicKey_f2), rewardShare_update, reward_address_update.address(), signature25519_u1_bad, signatureVrf_u1_bad + ) + updateForgerData_bad = BytesUtils.fromHexString(UpdateForgerCmd) ++ regCmdInput_bad.encode() + msg = getMessage(contractAddress, BigInteger.ZERO, updateForgerData_bad, randomNonce, from = senderAddress) + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + expectedErr = "Forger does not exist" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + val msg_u1 = ForgerStakeV2MsgProcessor.getHashedMessageToSign( + BytesUtils.toHexString(pair25519_f1._2.pubKeyBytes()), + BytesUtils.toHexString(pairVrf_f1._2.pubKeyBytes()), + rewardShare_update, + Keys.toChecksumAddress(BytesUtils.toHexString(reward_address_update.address().toBytes))) + val (signature25519_u1, signatureVrf_u1) = getSignatures(pair25519_f1._1, pairVrf_f1._1, msg_u1) + + val txHash_u1 = Keccak256.hash("update1 tx") + view.setupTxContext(txHash_u1, 20) + + regCmdInput = RegisterOrUpdateForgerCmdInput( + ForgerPublicKeys(blockSignerProposition_f1, vrfPublicKey_f1), rewardShare_update, reward_address_update.address(), signature25519_u1, signatureVrf_u1 + ) + val updateForgerData = BytesUtils.fromHexString(UpdateForgerCmd) ++ regCmdInput.encode() + msg = getMessage(contractAddress, BigInteger.ZERO, updateForgerData, randomNonce, from = senderAddress) + assertGas(34143, msg, view, forgerStakeV2MessageProcessor, blockContextForkV1_4) + + // Check log event + val listOfLogs2 = view.getLogs(txHash_u1) + assertEquals("Wrong number of logs", 1, listOfLogs2.length) + val expectedEvent2 = UpdateForger(msg.getFrom, regCmdInput.forgerPublicKeys.blockSignPublicKey, + regCmdInput.forgerPublicKeys.vrfPublicKey, rewardShare_update, reward_address_update.address()) + + checkUpdateForgerEvent(expectedEvent2, listOfLogs2(0)) + + + // negative test + // - Try updating a forger which already has a reward share/address + val msg_u2_bad = ForgerStakeV2MsgProcessor.getHashedMessageToSign( + BytesUtils.toHexString(pair25519_f2._2.pubKeyBytes()), + BytesUtils.toHexString(pairVrf_f2._2.pubKeyBytes()), + rewardShare_update, + Keys.toChecksumAddress(BytesUtils.toHexString(reward_address_update.address().toBytes))) + sigs_tuple = getSignatures(pair25519_f2._1, pairVrf_f2._1, msg_u2_bad) + val signature25519_u2_bad = sigs_tuple._1 + val signatureVrf_u2_bad = sigs_tuple._2 + regCmdInput_bad = RegisterOrUpdateForgerCmdInput( + ForgerPublicKeys(blockSignerProposition_f2, vrfPublicKey_f2), rewardShare_update, reward_address_update.address(), signature25519_u2_bad, signatureVrf_u2_bad + ) + updateForgerData_bad = BytesUtils.fromHexString(UpdateForgerCmd) ++ regCmdInput_bad.encode() + msg = getMessage(contractAddress, BigInteger.ZERO, updateForgerData_bad, randomNonce, from = senderAddress) + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + expectedErr = "Reward share or reward address are not null" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + } + } + + def getVrfKeyPair(seed: Int): (VrfSecretKey, VrfPublicKey) = { + val secret: VrfSecretKey = VrfGeneratedDataProvider.getVrfSecretKey(seed) + val publicKey: VrfPublicKey = secret.publicImage() + (secret, publicKey) + } + + def get25519KeyPair(seed: Int): (PrivateKey25519, PublicKey25519Proposition) = { + val secret: PrivateKey25519 = PrivateKey25519Creator.getInstance.generateSecret(BigInteger.valueOf(seed).toByteArray) + val publicKey: PublicKey25519Proposition = secret.publicImage() + (secret, publicKey) + } + + def getSignatures(blockSignKey: PrivateKey25519, vrfKey: VrfSecretKey, msg: Array[Byte]): (Signature25519, VrfProof) = { + (blockSignKey.sign(msg), vrfKey.sign(msg)) + } + + @Test + def testSignatures() { + val generatedDataSeed = 908 + + val pairVrfKey: (VrfSecretKey, VrfPublicKey) = getVrfKeyPair(generatedDataSeed) + val pair25519: (PrivateKey25519, PublicKey25519Proposition) = get25519KeyPair(generatedDataSeed) + + val msg = ForgerStakeV2MsgProcessor.getHashedMessageToSign( + pair25519._2.pubKeyBytes().toString, pairVrfKey._2.pubKeyBytes().toString, 0, "ab") + + val (signature25519, signatureVrf) = getSignatures(pair25519._1, pairVrfKey._1, msg) + + assertTrue(signatureVrf.isValid(pairVrfKey._2, msg)) + assertTrue(signature25519.isValid(pair25519._2, msg)) + } + + @Test + def testAddAndRemoveStake(): Unit = { + + val processors = Seq(forgerStakeV2MessageProcessor, forgerStakeMessageProcessor) + + usingView(processors) { view => + forgerStakeMessageProcessor.init(view, V1_3_MOCK_FORK_POINT) + forgerStakeV2MessageProcessor.init(view, view.getConsensusEpochNumberAsInt) + + // create sender account with some fund in it + val initialAmount = BigInteger.valueOf(100).multiply(validWeiAmount) + createSenderAccount(view, initialAmount) + + //Setting the context + + val blockSignerProposition = new PublicKey25519Proposition(BytesUtils.fromHexString("1122334455667788112233445566778811223344556677881122334455667788")) // 32 bytes + val vrfPublicKey = new VrfPublicKey(BytesUtils.fromHexString("d6b775fd4cefc7446236683fdde9d0464bba43cc565fa066b0b3ed1b888b9d1180")) // 33 bytes + + ///////////////////////////////////////////////////////////////////////////////////////////// + // Before activate tests + ///////////////////////////////////////////////////////////////////////////////////////////// + + val delegateCmdInput = SelectByForgerCmdInput( + ForgerPublicKeys(blockSignerProposition, vrfPublicKey) + ) + + val delegateData: Array[Byte] = BytesUtils.fromHexString(DelegateCmd) ++ delegateCmdInput.encode() + var msg = getMessage(contractAddress, validWeiAmount, delegateData, randomNonce) + + // Check that delegate and withdraw cannot be called before activate + + var exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + var expectedErr = "Forger stake V2 has not been activated yet" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + val withdrawCmdInput = WithdrawCmdInput( + ForgerPublicKeys(blockSignerProposition, vrfPublicKey), + BigInteger.ONE + ) + + msg = getMessage(contractAddress, BigInteger.ZERO, BytesUtils.fromHexString(WithdrawCmd) ++ withdrawCmdInput.encode(), randomNonce) + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + assertTrue(exc.getMessage.contains("Forger stake V2 has not been activated yet")) + + // Call activate + + val initialOwnerBalance = BigInteger.valueOf(200).multiply(validWeiAmount) + createSenderAccount(view, initialOwnerBalance, ownerAddressProposition.address()) + + msg = getMessage( + contractAddress, 0, BytesUtils.fromHexString(ActivateCmd), randomNonce, ownerAddressProposition.address()) + + assertGasInterop(0, msg, view, processors, blockContextForkV1_4) + + ///////////////////////////////////////////////////////////////////////////////////////////// + // Delegate tests + ///////////////////////////////////////////////////////////////////////////////////////////// + + // Try delegate to a non-existing forger. It should fail. + msg = getMessage(contractAddress, validWeiAmount, delegateData, randomNonce) + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + expectedErr = "Forger doesn't exist." + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // Register the forger + val initialStake = new BigInteger("1000000000000") + + StakeStorage.addForger(view, delegateCmdInput.forgerPublicKeys.blockSignPublicKey, delegateCmdInput.forgerPublicKeys.vrfPublicKey, + 0, Address.ZERO, blockContextForkV1_4.consensusEpochNumber, ownerAddressProposition.address(), initialStake) + + //TODO we're using directly StakeStorage.addForger here because registerForger is not implemented yet. So we need + // to update the contract balance by hand with the initial stake + view.addBalance(contractAddress, initialStake) + + // Add the first stake by the same delegator + val txHash1 = Keccak256.hash("first tx") + view.setupTxContext(txHash1, 10) + + var stakeAmount = validWeiAmount + msg = getMessage(contractAddress, stakeAmount, delegateData, randomNonce, ownerAddressProposition.address()) + assertGas(13587, msg, view, forgerStakeV2MessageProcessor, blockContextForkV1_4) + + var listOfStakes = StakeStorage.getAllForgerStakes(view) + assertEquals(1, listOfStakes.size) + assertEquals(delegateCmdInput.forgerPublicKeys, listOfStakes.head.forgerPublicKeys) + assertEquals(ownerAddressProposition, listOfStakes.head.ownerPublicKey) + var expectedOwnerStakeAmount = initialStake.add(stakeAmount) + assertEquals(expectedOwnerStakeAmount, listOfStakes.head.stakedAmount) + + // Check that the balances of the delegator and of the forger smart contract have changed + var expectedOwnerBalance = initialOwnerBalance.subtract(stakeAmount) + assertEquals(expectedOwnerBalance, view.getBalance(ownerAddressProposition.address())) + var expectedForgerContractBalance = initialStake.add(stakeAmount) + assertEquals(expectedForgerContractBalance, view.getBalance(contractAddress)) + + // Check log event + var listOfLogs = view.getLogs(txHash1) + assertEquals("Wrong number of logs", 1, listOfLogs.length) + var expectedDelegateEvent = DelegateForgerStake(ownerAddressProposition.address(), delegateCmdInput.forgerPublicKeys.blockSignPublicKey, + delegateCmdInput.forgerPublicKeys.vrfPublicKey, stakeAmount) + + checkDelegateForgerStakeEvent(expectedDelegateEvent, listOfLogs(0)) + + // Add with same delegator but different epoch + val txHash2 = Keccak256.hash("tx2") + view.setupTxContext(txHash2, 10) + stakeAmount = validWeiAmount.multiply(2) + + var blockContext = getBlockContextForEpoch(V1_4_MOCK_FORK_POINT + 1) + + + msg = getMessage(contractAddress, stakeAmount, delegateData, randomNonce, ownerAddressProposition.address()) + assertGas(57787, msg, view, forgerStakeV2MessageProcessor, blockContext) + + listOfStakes = StakeStorage.getAllForgerStakes(view) + assertEquals(1, listOfStakes.size) + assertEquals(delegateCmdInput.forgerPublicKeys, listOfStakes.head.forgerPublicKeys) + assertEquals(ownerAddressProposition, listOfStakes.head.ownerPublicKey) + expectedOwnerStakeAmount = expectedOwnerStakeAmount.add(stakeAmount) + assertEquals(expectedOwnerStakeAmount, listOfStakes.head.stakedAmount) + + // Check that the balances of the delegator and of the forger smart contract have changed + expectedOwnerBalance = expectedOwnerBalance.subtract(stakeAmount) + assertEquals(expectedOwnerBalance, view.getBalance(ownerAddressProposition.address())) + + expectedForgerContractBalance = expectedForgerContractBalance.add(stakeAmount) + assertEquals(expectedForgerContractBalance, view.getBalance(contractAddress)) + + // Check log event + listOfLogs = view.getLogs(txHash2) + assertEquals("Wrong number of logs", 1, listOfLogs.length) + expectedDelegateEvent = DelegateForgerStake(ownerAddressProposition.address(), delegateCmdInput.forgerPublicKeys.blockSignPublicKey, + delegateCmdInput.forgerPublicKeys.vrfPublicKey, stakeAmount) + + checkDelegateForgerStakeEvent(expectedDelegateEvent, listOfLogs(0)) + + // Add with different delegator but same epoch + val privateKey1: PrivateKeySecp256k1 = PrivateKeySecp256k1Creator.getInstance().generateSecret("nativemsgprocessortest1".getBytes(StandardCharsets.UTF_8)) + val owner1: AddressProposition = privateKey1.publicImage() + + val initialOwner1Balance = BigInteger.valueOf(200).multiply(validWeiAmount) + createSenderAccount(view, initialOwner1Balance, owner1.address()) + + val txHash3 = Keccak256.hash("tx3") + view.setupTxContext(txHash3, 10) + stakeAmount = validWeiAmount.multiply(3) + + msg = getMessage(contractAddress, stakeAmount, delegateData, randomNonce, owner1.address()) + assertGas(124087, msg, view, forgerStakeV2MessageProcessor, blockContext) + + listOfStakes = StakeStorage.getAllForgerStakes(view) + assertEquals(2, listOfStakes.size) + assertEquals(delegateCmdInput.forgerPublicKeys, listOfStakes.head.forgerPublicKeys) + assertEquals(ownerAddressProposition, listOfStakes.head.ownerPublicKey) + assertEquals(expectedOwnerStakeAmount, listOfStakes.head.stakedAmount) + assertEquals(owner1, listOfStakes(1).ownerPublicKey) + val expectedOwner1StakeAmount = stakeAmount + assertEquals(expectedOwner1StakeAmount, listOfStakes(1).stakedAmount) + + // Check that the balances of the delegator and of the forger smart contract have changed + assertEquals(expectedOwnerBalance, view.getBalance(ownerAddressProposition.address())) + expectedForgerContractBalance = expectedForgerContractBalance.add(stakeAmount) + assertEquals(expectedForgerContractBalance, view.getBalance(contractAddress)) + + var expectedOwner1Balance = initialOwner1Balance.subtract(stakeAmount) + assertEquals(expectedOwner1Balance, view.getBalance(owner1.address())) + + // Check log event + listOfLogs = view.getLogs(txHash3) + assertEquals("Wrong number of logs", 1, listOfLogs.length) + expectedDelegateEvent = DelegateForgerStake(owner1.address(), delegateCmdInput.forgerPublicKeys.blockSignPublicKey, + delegateCmdInput.forgerPublicKeys.vrfPublicKey, stakeAmount) + checkDelegateForgerStakeEvent(expectedDelegateEvent, listOfLogs(0)) + + + // Add with different delegator and different epoch + val txHash4 = Keccak256.hash("tx4") + view.setupTxContext(txHash4, 10) + stakeAmount = validWeiAmount + + val privateKey2: PrivateKeySecp256k1 = PrivateKeySecp256k1Creator.getInstance().generateSecret("nativemsgprocessortest2".getBytes(StandardCharsets.UTF_8)) + val owner2: AddressProposition = privateKey2.publicImage() + + blockContext = getBlockContextForEpoch(blockContext.consensusEpochNumber + 1) + val initialOwner2Balance = BigInteger.valueOf(300).multiply(validWeiAmount) + createSenderAccount(view, initialOwner2Balance, owner2.address()) + + msg = getMessage(contractAddress, stakeAmount, delegateData, randomNonce, owner2.address()) + assertGas(144087, msg, view, forgerStakeV2MessageProcessor, blockContext) + + listOfStakes = StakeStorage.getAllForgerStakes(view) + assertEquals(3, listOfStakes.size) + assertEquals(delegateCmdInput.forgerPublicKeys, listOfStakes.head.forgerPublicKeys) + assertEquals(ownerAddressProposition, listOfStakes.head.ownerPublicKey) + assertEquals(expectedOwnerStakeAmount, listOfStakes.head.stakedAmount) + assertEquals(owner1, listOfStakes(1).ownerPublicKey) + assertEquals(expectedOwner1StakeAmount, listOfStakes(1).stakedAmount) + assertEquals(owner2, listOfStakes(2).ownerPublicKey) + val expectedOwner2StakeAmount = stakeAmount + assertEquals(expectedOwner2StakeAmount, listOfStakes(2).stakedAmount) + + // Check that the balances of the delegators and of the forger smart contract have changed + assertEquals(expectedOwnerBalance, view.getBalance(ownerAddressProposition.address())) + expectedForgerContractBalance = expectedForgerContractBalance.add(stakeAmount) + assertEquals(expectedForgerContractBalance, view.getBalance(contractAddress)) + assertEquals(expectedOwner1Balance, view.getBalance(owner1.address())) + + var expectedOwner2Balance = initialOwner2Balance.subtract(stakeAmount) + assertEquals(expectedOwner2Balance, view.getBalance(owner2.address())) + + // Check log event + listOfLogs = view.getLogs(txHash4) + assertEquals("Wrong number of logs", 1, listOfLogs.length) + expectedDelegateEvent = DelegateForgerStake(owner2.address(), delegateCmdInput.forgerPublicKeys.blockSignPublicKey, + delegateCmdInput.forgerPublicKeys.vrfPublicKey, stakeAmount) + checkDelegateForgerStakeEvent(expectedDelegateEvent, listOfLogs(0)) + + ////////////////////////////////////////////////////////// + // Negative tests + ////////////////////////////////////////////////////////// + + // Add stake without enough balance + + val privateKey3: PrivateKeySecp256k1 = PrivateKeySecp256k1Creator.getInstance().generateSecret("nativemsgprocessortest3".getBytes(StandardCharsets.UTF_8)) + val owner3: AddressProposition = privateKey3.publicImage() + assertEquals(BigInteger.ZERO, view.getBalance(owner3.address())) + + msg = getMessage(contractAddress, validWeiAmount, delegateData, randomNonce, owner3.address()) + exc = intercept[ExecutionRevertedException] { + assertGas(2300, msg, view, forgerStakeV2MessageProcessor, blockContext) + } + expectedErr = "Insufficient funds." + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // Add Stake with value 0. + + msg = getMessage(contractAddress, BigInteger.ZERO, delegateData, randomNonce, owner2.address()) + exc = intercept[ExecutionRevertedException] { + assertGas(2100, msg, view, forgerStakeV2MessageProcessor, blockContext) + } + expectedErr = "Value must not be zero" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // Add Stake with invalid zen amount. + + msg = getMessage(contractAddress, invalidWeiAmount, delegateData, randomNonce, owner2.address()) + exc = intercept[ExecutionRevertedException] { + assertGas(2100, msg, view, forgerStakeV2MessageProcessor, blockContext) + } + expectedErr = "Value is not a legal wei amount" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // Add Stake in a epoch in the past. + + blockContext = getBlockContextForEpoch(blockContext.consensusEpochNumber - 1) + msg = getMessage(contractAddress, validWeiAmount, delegateData, randomNonce, owner2.address()) + exc = intercept[ExecutionRevertedException] { + assertGas(6400, msg, view, forgerStakeV2MessageProcessor, blockContext) + } + expectedErr = "Epoch is in the past" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // try processing a msg with a trailing byte in the arguments + val badData = Bytes.concat(delegateData, new Array[Byte](1)) + val msgBad = getMessage(contractAddress, stakeAmount, badData, randomNonce) + + // should fail because input has a trailing byte + blockContext = getBlockContextForEpoch(blockContext.consensusEpochNumber + 10) + val ex = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msgBad, view, blockContext, _, view)) + } + assertTrue(ex.getMessage.contains("Wrong message data field length")) + + ////////////////////////////////////////////////////////// + // Withdrawal tests + ////////////////////////////////////////////////////////// + + // remove all the stakes for owner2 + stakeAmount = expectedOwner2StakeAmount + val withdrawCmdBytes: Array[Byte] = BytesUtils.fromHexString(WithdrawCmd) + var withdrawInput = WithdrawCmdInput( + ForgerPublicKeys(blockSignerProposition, vrfPublicKey), stakeAmount + ) + val txHash5 = Keccak256.hash("tx5") + view.setupTxContext(txHash5, 10) + + msg = getMessage(contractAddress, BigInteger.ZERO, withdrawCmdBytes ++ withdrawInput.encode(), randomNonce, owner2.address()) + assertGas(57687, msg, view, forgerStakeV2MessageProcessor, blockContext) + + listOfStakes = StakeStorage.getAllForgerStakes(view) + assertEquals(2, listOfStakes.size) + assertEquals(delegateCmdInput.forgerPublicKeys, listOfStakes.head.forgerPublicKeys) + assertEquals(ownerAddressProposition, listOfStakes.head.ownerPublicKey) + assertEquals(expectedOwnerStakeAmount, listOfStakes.head.stakedAmount) + assertEquals(owner1, listOfStakes(1).ownerPublicKey) + assertEquals(expectedOwner1StakeAmount, listOfStakes(1).stakedAmount) + + assertEquals(expectedOwnerBalance, view.getBalance(ownerAddressProposition.address())) + expectedForgerContractBalance = expectedForgerContractBalance.subtract(stakeAmount) + assertEquals(expectedForgerContractBalance, view.getBalance(contractAddress)) + assertEquals(expectedOwner1Balance, view.getBalance(owner1.address())) + + expectedOwner2Balance = expectedOwner2Balance.add(stakeAmount) + assertEquals(expectedOwner2Balance, view.getBalance(owner2.address())) + + // Check log event + listOfLogs = view.getLogs(txHash5) + assertEquals("Wrong number of logs", 1, listOfLogs.length) + var expectedWithdrawEvent = WithdrawForgerStake(owner2.address(), withdrawInput.forgerPublicKeys.blockSignPublicKey, + withdrawInput.forgerPublicKeys.vrfPublicKey, stakeAmount) + checkWithdrawForgerStakeEvent(expectedWithdrawEvent, listOfLogs(0)) + + // remove all the stakes for ownerAddressProposition in 2 different epochs + stakeAmount = initialStake + + withdrawInput = WithdrawCmdInput( + ForgerPublicKeys(blockSignerProposition, vrfPublicKey), stakeAmount + ) + val txHash6 = Keccak256.hash("tx6") + view.setupTxContext(txHash6, 10) + + msg = getMessage(contractAddress, BigInteger.ZERO, withdrawCmdBytes ++ withdrawInput.encode(), randomNonce, ownerAddressProposition.address()) + assertGas(37687, msg, view, forgerStakeV2MessageProcessor, blockContext) + + listOfStakes = StakeStorage.getAllForgerStakes(view) + assertEquals(2, listOfStakes.size) + assertEquals(delegateCmdInput.forgerPublicKeys, listOfStakes.head.forgerPublicKeys) + assertEquals(ownerAddressProposition, listOfStakes.head.ownerPublicKey) + expectedOwnerStakeAmount = expectedOwnerStakeAmount.subtract(stakeAmount) + assertEquals(expectedOwnerStakeAmount, listOfStakes.head.stakedAmount) + + assertEquals(owner1, listOfStakes(1).ownerPublicKey) + assertEquals(expectedOwner1StakeAmount, listOfStakes(1).stakedAmount) + + expectedOwnerBalance = expectedOwnerBalance.add(stakeAmount) + assertEquals(expectedOwnerBalance, view.getBalance(ownerAddressProposition.address())) + expectedForgerContractBalance = expectedForgerContractBalance.subtract(stakeAmount) + assertEquals(expectedForgerContractBalance, view.getBalance(contractAddress)) + assertEquals(expectedOwner1Balance, view.getBalance(owner1.address())) + + + // Check log event + listOfLogs = view.getLogs(txHash6) + assertEquals("Wrong number of logs", 1, listOfLogs.length) + expectedWithdrawEvent = WithdrawForgerStake(ownerAddressProposition.address(), withdrawInput.forgerPublicKeys.blockSignPublicKey, + withdrawInput.forgerPublicKeys.vrfPublicKey, stakeAmount) + checkWithdrawForgerStakeEvent(expectedWithdrawEvent, listOfLogs(0)) + + blockContext = getBlockContextForEpoch(blockContext.consensusEpochNumber + 1) + + stakeAmount = expectedOwnerStakeAmount + withdrawInput = WithdrawCmdInput( + ForgerPublicKeys(blockSignerProposition, vrfPublicKey), stakeAmount + ) + val txHash7 = Keccak256.hash("tx7") + view.setupTxContext(txHash7, 10) + + msg = getMessage(contractAddress, BigInteger.ZERO, withdrawCmdBytes ++ withdrawInput.encode(), randomNonce, ownerAddressProposition.address()) + assertGas(57687, msg, view, forgerStakeV2MessageProcessor, blockContext) + + listOfStakes = StakeStorage.getAllForgerStakes(view) + assertEquals(1, listOfStakes.size) + assertEquals(delegateCmdInput.forgerPublicKeys, listOfStakes.head.forgerPublicKeys) + + assertEquals(owner1, listOfStakes.head.ownerPublicKey) + assertEquals(expectedOwner1StakeAmount, listOfStakes.head.stakedAmount) + + expectedOwnerBalance = expectedOwnerBalance.add(stakeAmount) + assertEquals(expectedOwnerBalance, view.getBalance(ownerAddressProposition.address())) + expectedForgerContractBalance = expectedForgerContractBalance.subtract(stakeAmount) + assertEquals(expectedForgerContractBalance, view.getBalance(contractAddress)) + assertEquals(expectedOwner1Balance, view.getBalance(owner1.address())) + + + // Check log event + listOfLogs = view.getLogs(txHash7) + assertEquals("Wrong number of logs", 1, listOfLogs.length) + expectedWithdrawEvent = WithdrawForgerStake(ownerAddressProposition.address(), withdrawInput.forgerPublicKeys.blockSignPublicKey, + withdrawInput.forgerPublicKeys.vrfPublicKey, stakeAmount) + checkWithdrawForgerStakeEvent(expectedWithdrawEvent, listOfLogs(0)) + + + ////////////////////////////////////////////////////////// + // Negative tests + ////////////////////////////////////////////////////////// + + // Check that it is not payable + msg = getMessage(contractAddress, stakeAmount, withdrawCmdBytes ++ withdrawInput.encode(), randomNonce, owner1.address()) + exc = intercept[ExecutionRevertedException] { + assertGas(0, msg, view, forgerStakeV2MessageProcessor, blockContext) + } + expectedErr = "Call value must be zero" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // Invalid withdrawal amount: 0, invalid wei amount + withdrawInput = WithdrawCmdInput( + ForgerPublicKeys(blockSignerProposition, vrfPublicKey), BigInteger.ZERO + ) + msg = getMessage(contractAddress, BigInteger.ZERO, withdrawCmdBytes ++ withdrawInput.encode(), randomNonce, owner1.address()) + exc = intercept[ExecutionRevertedException] { + assertGas(2100, msg, view, forgerStakeV2MessageProcessor, blockContext) + } + expectedErr = "Withdrawal amount must be greater than zero" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + withdrawInput = WithdrawCmdInput( + ForgerPublicKeys(blockSignerProposition, vrfPublicKey), invalidWeiAmount + ) + msg = getMessage(contractAddress, BigInteger.ZERO, withdrawCmdBytes ++ withdrawInput.encode(), randomNonce, owner1.address()) + exc = intercept[ExecutionRevertedException] { + assertGas(2100, msg, view, forgerStakeV2MessageProcessor, blockContext) + } + expectedErr = "Value is not a legal wei amount" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // Wrong input: try processing a msg with a trailing byte in the arguments + withdrawInput = WithdrawCmdInput( + ForgerPublicKeys(blockSignerProposition, vrfPublicKey), stakeAmount + ) + msg = getMessage(contractAddress, BigInteger.ZERO, withdrawCmdBytes ++ withdrawInput.encode() ++ new Array[Byte](1), randomNonce, owner1.address()) + exc = intercept[ExecutionRevertedException] { + assertGas(2100, msg, view, forgerStakeV2MessageProcessor, blockContext) + } + expectedErr = "Wrong message data field length" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // Remove stake without any stake. 3 tests: delegator that hasn't ever delegate something to the forger, delegator + // who withdrew all its stakes and delegator who tries to withdraw an amount greater than its stakes. + withdrawInput = WithdrawCmdInput( + ForgerPublicKeys(blockSignerProposition, vrfPublicKey), stakeAmount + ) + + msg = getMessage(contractAddress, BigInteger.ZERO, withdrawCmdBytes ++ withdrawInput.encode(), randomNonce, owner3.address()) + exc = intercept[ExecutionRevertedException] { + assertGas(6300, msg, view, forgerStakeV2MessageProcessor, blockContext) + } + expectedErr = "doesn't have stake with the forger" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + msg = getMessage(contractAddress, BigInteger.ZERO, withdrawCmdBytes ++ withdrawInput.encode(), randomNonce, ownerAddressProposition.address()) + exc = intercept[ExecutionRevertedException] { + assertGas(8400, msg, view, forgerStakeV2MessageProcessor, blockContext) + } + expectedErr = "Not enough stake" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + withdrawInput = WithdrawCmdInput( + ForgerPublicKeys(blockSignerProposition, vrfPublicKey), expectedOwner1StakeAmount.add(stakeAmount) + ) + + msg = getMessage(contractAddress, BigInteger.ZERO, withdrawCmdBytes ++ withdrawInput.encode(), randomNonce, owner1.address()) + exc = intercept[ExecutionRevertedException] { + assertGas(8400, msg, view, forgerStakeV2MessageProcessor, blockContext) + } + expectedErr = "Not enough stake" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // Remove stake from a non-existing forger. + val vrfPublicKey2 = new VrfPublicKey(BytesUtils.fromHexString("22222222222222446236683fdde9d0464bba43cc565fa066b0b3ed1b888b9d1180")) // 33 bytes + + withdrawInput = WithdrawCmdInput( + ForgerPublicKeys(blockSignerProposition, vrfPublicKey2), stakeAmount + ) + + msg = getMessage(contractAddress, BigInteger.ZERO, withdrawCmdBytes ++ withdrawInput.encode(), randomNonce, owner1.address()) + exc = intercept[ExecutionRevertedException] { + assertGas(4200, msg, view, forgerStakeV2MessageProcessor, blockContext) + } + expectedErr = "Forger doesn't exist." + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // Remove stakes in the past + val revert = view.snapshot + withdrawInput = WithdrawCmdInput( + ForgerPublicKeys(blockSignerProposition, vrfPublicKey), expectedOwner1StakeAmount + ) + msg = getMessage(contractAddress, BigInteger.ZERO, withdrawCmdBytes ++ withdrawInput.encode(), randomNonce, owner1.address()) + blockContext = getBlockContextForEpoch(blockContext.consensusEpochNumber - 1) + + exc = intercept[ExecutionRevertedException] { + assertGas(32800, msg, view, forgerStakeV2MessageProcessor, blockContext) + } + expectedErr = "Epoch is in the past" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + view.revertToSnapshot(revert) + + /////////////////////////////////////////////////////////////////////////////////// + // Remove all the remaining stakes + /////////////////////////////////////////////////////////////////////////////////// + val txHash8 = Keccak256.hash("tx8") + view.setupTxContext(txHash8, 10) + blockContext = getBlockContextForEpoch(blockContext.consensusEpochNumber + 10) + assertGas(57687, msg, view, forgerStakeV2MessageProcessor, blockContext) + + listOfStakes = StakeStorage.getAllForgerStakes(view) + assertEquals(0, listOfStakes.size) + + assertEquals(BigInteger.ZERO, view.getBalance(contractAddress)) + expectedOwner1Balance = expectedOwner1Balance.add(withdrawInput.value) + assertEquals(expectedOwner1Balance, view.getBalance(owner1.address())) + + // Check log event + listOfLogs = view.getLogs(txHash8) + assertEquals("Wrong number of logs", 1, listOfLogs.length) + expectedWithdrawEvent = WithdrawForgerStake(owner1.address(), withdrawInput.forgerPublicKeys.blockSignPublicKey, + withdrawInput.forgerPublicKeys.vrfPublicKey, withdrawInput.value) + checkWithdrawForgerStakeEvent(expectedWithdrawEvent, listOfLogs(0)) + + /////////////////////////////////////////////////////////////////////////////////// + // Add again some stakes + /////////////////////////////////////////////////////////////////////////////////// + val txHash9 = Keccak256.hash("tx9") + view.setupTxContext(txHash9, 10) + stakeAmount = validWeiAmount + + msg = getMessage(contractAddress, stakeAmount, delegateData, randomNonce, owner1.address()) + assertGas(17787, msg, view, forgerStakeV2MessageProcessor, blockContext) + + listOfStakes = StakeStorage.getAllForgerStakes(view) + assertEquals(1, listOfStakes.size) + assertEquals(delegateCmdInput.forgerPublicKeys, listOfStakes.head.forgerPublicKeys) + assertEquals(owner1, listOfStakes.head.ownerPublicKey) + assertEquals(stakeAmount, listOfStakes.head.stakedAmount) + + // Check that the balances of the delegator and of the forger smart contract have changed + assertEquals(stakeAmount, view.getBalance(contractAddress)) + + expectedOwner1Balance = expectedOwner1Balance.subtract(stakeAmount) + assertEquals(expectedOwner1Balance, view.getBalance(owner1.address())) + + // Check log event + listOfLogs = view.getLogs(txHash9) + assertEquals("Wrong number of logs", 1, listOfLogs.length) + expectedDelegateEvent = DelegateForgerStake(owner1.address(), delegateCmdInput.forgerPublicKeys.blockSignPublicKey, + delegateCmdInput.forgerPublicKeys.vrfPublicKey, stakeAmount) + checkDelegateForgerStakeEvent(expectedDelegateEvent, listOfLogs(0)) + + + } + } + + @Test + def testGetStakeTotal(): Unit = { + val blockSignerProposition1 = new PublicKey25519Proposition(BytesUtils.fromHexString("1122334455667788112233445566778811223344556677881122334455667788")) // 32 bytes + val vrfPublicKey1 = new VrfPublicKey(BytesUtils.fromHexString("d6b775fd4cefc7446236683fdde9d0464bba43cc565fa066b0b3ed1b888b9d1180")) // 33 bytes + val blockSignerProposition2 = new PublicKey25519Proposition(BytesUtils.fromHexString("1122334455667788112233445566778811223344556677881122334455667799")) // 32 bytes + val vrfPublicKey2 = new VrfPublicKey(BytesUtils.fromHexString("d6b775fd4cefc7446236683fdde9d0464bba43cc565fa066b0b3ed1b888b9d1190")) // 33 bytes + val address1: Address = PrivateKeySecp256k1Creator.getInstance().generateSecret("nativemsgprocessortest1".getBytes(StandardCharsets.UTF_8)).publicImage().address() + val address2: Address = PrivateKeySecp256k1Creator.getInstance().generateSecret("nativemsgprocessortest2".getBytes(StandardCharsets.UTF_8)).publicImage().address() + val address3: Address = PrivateKeySecp256k1Creator.getInstance().generateSecret("nativemsgprocessortest3".getBytes(StandardCharsets.UTF_8)).publicImage().address() + val address4: Address = PrivateKeySecp256k1Creator.getInstance().generateSecret("nativemsgprocessortest4".getBytes(StandardCharsets.UTF_8)).publicImage().address() + + usingView(forgerStakeV2MessageProcessor) { view => + forgerStakeV2MessageProcessor.init(view, view.getConsensusEpochNumberAsInt) + // Setup + val initialAmount = BigInteger.valueOf(100).multiply(validWeiAmount) + createSenderAccount(view, initialAmount) + val txHash1 = Keccak256.hash("first tx") + view.setupTxContext(txHash1, 10) + + // assert invocation fails until stake v2 is active + val msg1 = getMessage(contractAddress, BigInteger.ZERO, BytesUtils.fromHexString(StakeTotalCmd) ++ Array.emptyByteArray, randomNonce) + val gas = new GasPool(1000000000) + assertThrows[ExecutionRevertedException](TestContext.process(forgerStakeV2MessageProcessor, msg1, view, blockContextForkV1_4_plus10, gas, view)) + + val BI_0 = BigInteger.ZERO + val BI_20 = BigInteger.valueOf(20 * ZenCoinsUtils.COIN) + val BI_40 = BigInteger.valueOf(40 * ZenCoinsUtils.COIN) + val BI_60 = BigInteger.valueOf(60 * ZenCoinsUtils.COIN) + val BI_80 = BigInteger.valueOf(80 * ZenCoinsUtils.COIN) + + StakeStorage.setActive(view) + StakeStorage.addForger(view, blockSignerProposition1, vrfPublicKey1, 100, Address.ZERO, V1_4_MOCK_FORK_POINT + 3, address1, BI_20) + StakeStorage.addForger(view, blockSignerProposition2, vrfPublicKey2, 100, Address.ZERO, V1_4_MOCK_FORK_POINT + 5, address2, BI_20) + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, V1_4_MOCK_FORK_POINT + 7, address3, BI_20) + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, V1_4_MOCK_FORK_POINT + 9, address4, BI_20) + /* + epoch forger1 forger2 delegator1 delegator2 total + 300 0 0 0 0 0 + 303 20 0 0 0 20 + 305 20 20 0 0 40 + 307 40 20 20 0 60 + 309 60 20 20 20 80 + + */ + + // get single delegation for current epoch + var stakeTotalCmdInput = StakeTotalCmdInput(Some(ForgerPublicKeys(blockSignerProposition1, vrfPublicKey1)), Some(address3), None, None) + var data: Array[Byte] = stakeTotalCmdInput.encode() + var msg = getMessage(contractAddress, BigInteger.ZERO, BytesUtils.fromHexString(StakeTotalCmd) ++ data, randomNonce) + var returnData = assertGas(15000, msg, view, forgerStakeV2MessageProcessor, blockContextForkV1_4_plus10) + assertNotNull(returnData) + var stakeTotalResponse = StakeTotalCmdOutputDecoder.decode(returnData) + assertEquals( + Seq(BI_20), + stakeTotalResponse.listOfStakes + ) + + // get single forger for current epoch + stakeTotalCmdInput = StakeTotalCmdInput(Some(ForgerPublicKeys(blockSignerProposition1, vrfPublicKey1)), None, None, None) + data= stakeTotalCmdInput.encode() + msg = getMessage(contractAddress, BigInteger.ZERO, BytesUtils.fromHexString(StakeTotalCmd) ++ data, randomNonce) + returnData = assertGas(19100, msg, view, forgerStakeV2MessageProcessor, blockContextForkV1_4_plus10) + assertNotNull(returnData) + stakeTotalResponse = StakeTotalCmdOutputDecoder.decode(returnData) + assertEquals( + Seq(BI_60), + stakeTotalResponse.listOfStakes + ) + + // get total stake for current epoch + stakeTotalCmdInput = StakeTotalCmdInput(None, None, None, None) + data= stakeTotalCmdInput.encode() + msg = getMessage(contractAddress, BigInteger.ZERO, BytesUtils.fromHexString(StakeTotalCmd) ++ data, randomNonce) + returnData = assertGas(21300, msg, view, forgerStakeV2MessageProcessor, blockContextForkV1_4_plus10) + assertNotNull(returnData) + stakeTotalResponse = StakeTotalCmdOutputDecoder.decode(returnData) + assertEquals( + Seq(BI_80), + stakeTotalResponse.listOfStakes + ) + + // get total stake for last 11 epochs + stakeTotalCmdInput = StakeTotalCmdInput(None, None, Some(V1_4_MOCK_FORK_POINT), Some(11)) + data= stakeTotalCmdInput.encode() + msg = getMessage(contractAddress, BigInteger.ZERO, BytesUtils.fromHexString(StakeTotalCmd) ++ data, randomNonce) + returnData = assertGas(21500, msg, view, forgerStakeV2MessageProcessor, blockContextForkV1_4_plus10) + assertNotNull(returnData) + stakeTotalResponse = StakeTotalCmdOutputDecoder.decode(returnData) + assertEquals( + Seq(BI_0, BI_0, BI_0, BI_20, BI_20, BI_40, BI_40, BI_60, BI_60, BI_80, BI_80), + stakeTotalResponse.listOfStakes + ) + + // negative - illegal input params combination + stakeTotalCmdInput = StakeTotalCmdInput(None, Some(address4), None, None) + data= stakeTotalCmdInput.encode() + msg = getMessage(contractAddress, BigInteger.ZERO, BytesUtils.fromHexString(StakeTotalCmd) ++ data, randomNonce) + assertThrows[ExecutionRevertedException](TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4_plus10, gas, view)) + + // negative - not existing forger + stakeTotalCmdInput = StakeTotalCmdInput(Some(ForgerPublicKeys(blockSignerProposition1, vrfPublicKey2)), None, None, None) + data= stakeTotalCmdInput.encode() + msg = getMessage(contractAddress, BigInteger.ZERO, BytesUtils.fromHexString(StakeTotalCmd) ++ data, randomNonce) + assertThrows[ExecutionRevertedException](TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4_plus10, gas, view)) + + + } + } + + @Test + def testGetForgerRewards(): Unit = { + val blockSignerProposition1 = new PublicKey25519Proposition(BytesUtils.fromHexString("1122334455667788112233445566778811223344556677881122334455667788")) // 32 bytes + val vrfPublicKey1 = new VrfPublicKey(BytesUtils.fromHexString("d6b775fd4cefc7446236683fdde9d0464bba43cc565fa066b0b3ed1b888b9d1180")) // 33 bytes + val forgerPublicKeys1 = ForgerPublicKeys(blockSignerProposition1, vrfPublicKey1) + val forgerRewards = Seq(BigInteger.valueOf(10), BigInteger.valueOf(20), BigInteger.valueOf(30), BigInteger.valueOf(40), BigInteger.valueOf(50)) + when(metadataStorageView.getForgerRewards(forgerPublicKeys1, 10, 5)) + .thenReturn(forgerRewards) + + usingView(forgerStakeV2MessageProcessor) { view => + forgerStakeV2MessageProcessor.init(view, view.getConsensusEpochNumberAsInt) + + // test getRewardsReceived fails until ForgerStakeV2 is active + val msg1 = getMessage(contractAddress, BigInteger.ZERO, BytesUtils.fromHexString(RewardsReceivedCmd) ++ Array.emptyByteArray, randomNonce) + val gas = new GasPool(1000000000) + assertThrows[ExecutionRevertedException](TestContext.process(forgerStakeV2MessageProcessor, msg1, view, blockContextForkV1_4_plus10, gas, view)) + + StakeStorage.setActive(view) + StakeStorage.addForger(view, blockSignerProposition1, vrfPublicKey1, 100, Address.ZERO, V1_4_MOCK_FORK_POINT + 3, Address.ZERO, BigInteger.ZERO) + + // test getRewardsReceived + val rewardsReceivedCmdInput = RewardsReceivedCmdInput(ForgerPublicKeys(blockSignerProposition1, vrfPublicKey1), 10, 5) + val msg = getMessage(contractAddress, BigInteger.ZERO, BytesUtils.fromHexString(RewardsReceivedCmd) ++ rewardsReceivedCmdInput.encode(), randomNonce) + val returnData = assertGas(10600, msg, view, forgerStakeV2MessageProcessor, blockContextForkV1_4_plus10) + assertNotNull(returnData) + val rewardsReceivedOutput = RewardsReceivedCmdOutputDecoder.decode(returnData) + assertEquals( + forgerRewards, + rewardsReceivedOutput.listOfRewards + ) + } + } + + @Test + def testGetForgers(): Unit = { + + val processors = Seq(forgerStakeV2MessageProcessor, forgerStakeMessageProcessor) + + usingView(processors) { view => + forgerStakeMessageProcessor.init(view, V1_3_MOCK_FORK_POINT) + forgerStakeV2MessageProcessor.init(view, view.getConsensusEpochNumberAsInt) + + // create sender account with some fund in it + val initialAmount = BigInteger.valueOf(100).multiply(validWeiAmount) + createSenderAccount(view, initialAmount) + + //Setting the context + + val blockSignerProposition = new PublicKey25519Proposition(BytesUtils.fromHexString("1122334455667788112233445566778811223344556677881122334455667788")) // 32 bytes + val vrfPublicKey = new VrfPublicKey(BytesUtils.fromHexString("d6b775fd4cefc7446236683fdde9d0464bba43cc565fa066b0b3ed1b888b9d1180")) // 33 bytes + + ///////////////////////////////////////////////////////////////////////////////////////////// + // Before activate tests + ///////////////////////////////////////////////////////////////////////////////////////////// + + // Check that getForger and getPagedForgers cannot be called before activate + + var getForgerCmdInput = SelectByForgerCmdInput( + ForgerPublicKeys(blockSignerProposition, vrfPublicKey) + ) + + var getForgerData: Array[Byte] = BytesUtils.fromHexString(GetForgerCmd) ++ getForgerCmdInput.encode() + var msg = getMessage(contractAddress, BigInteger.ZERO, getForgerData, randomNonce) + + // Try getForger before fork 1.4 + val blockContextBeforeFork = new BlockContext( + Address.ZERO, + 0, + 0, + DefaultGasFeeFork.blockGasLimit, + 0, + V1_4_MOCK_FORK_POINT - 1, + 0, + 1, + MockedHistoryBlockHashProvider, + Hash.ZERO + ) + + var exc = intercept[ExecutionRevertedException] { + assertGas(0, msg, view, forgerStakeV2MessageProcessor, blockContextBeforeFork) + } + var expectedErr = "fork not active" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + + // Try getForger after fork 1.4 but before activate + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + + expectedErr = "Forger stake V2 has not been activated yet" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // Try getPagedForgers before fork 1.4 + val getPagedForgersCmdInput = PagedForgersCmdInput( + 0, 100 + ) + + val getPagedForgersData: Array[Byte] = BytesUtils.fromHexString(GetPagedForgersCmd) ++ getPagedForgersCmdInput.encode() + msg = getMessage(contractAddress, BigInteger.ZERO, getPagedForgersData, randomNonce) + + exc = intercept[ExecutionRevertedException] { + assertGas(0, msg, view, forgerStakeV2MessageProcessor, blockContextBeforeFork) + } + + expectedErr = "fork not active" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // Try getPagedForgers after fork 1.4 but before activate + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + + expectedErr = "Forger stake V2 has not been activated yet" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + + // Call activate + + val initialOwnerBalance = BigInteger.valueOf(200).multiply(validWeiAmount) + createSenderAccount(view, initialOwnerBalance, ownerAddressProposition.address()) + + msg = getMessage( + contractAddress, 0, BytesUtils.fromHexString(ActivateCmd), randomNonce, ownerAddressProposition.address()) + + assertGasInterop(0, msg, view, processors, blockContextForkV1_4) + + ///////////////////////////////////////////////////////////////////////////////////////////// + // Tests + ///////////////////////////////////////////////////////////////////////////////////////////// + + // Try getForger for a non-existing forger. It should fail. + msg = getMessage(contractAddress, BigInteger.ZERO, getForgerData, randomNonce) + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + expectedErr = "Forger doesn't exist." + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // Try getPagedForgers without any forger. It should return an empty list + msg = getMessage(contractAddress, BigInteger.ZERO, getPagedForgersData, randomNonce) + var res = assertGas(4200, msg, view, forgerStakeV2MessageProcessor, blockContextForkV1_4) + + var getForgersOutput = PagedForgersOutputDecoder.decode(res) + assertEquals(-1, getForgersOutput.nextStartPos) + assertTrue(getForgersOutput.listOfForgerInfo.isEmpty) + + // Register a forger + val initialStake = new BigInteger("1000000000000") + + StakeStorage.addForger(view, blockSignerProposition, vrfPublicKey, + 0, Address.ZERO, blockContextForkV1_4.consensusEpochNumber, ownerAddressProposition.address(), initialStake) + + // Try getForger + msg = getMessage(contractAddress, BigInteger.ZERO, getForgerData, randomNonce) + res = assertGas(10600, msg, view, forgerStakeV2MessageProcessor, blockContextForkV1_4) + + var getForgerOutput = GetForgerOutputDecoder.decode(res) + assertEquals(getForgerCmdInput.forgerPublicKeys, getForgerOutput.forgerPublicKeys) + assertEquals(0, getForgerOutput.rewardShare) + assertEquals(Address.ZERO, getForgerOutput.rewardAddress.address()) + + // Try getPagedForgers + msg = getMessage(contractAddress, BigInteger.ZERO, getPagedForgersData, randomNonce) + res = assertGas(14700, msg, view, forgerStakeV2MessageProcessor, blockContextForkV1_4) + + getForgersOutput = PagedForgersOutputDecoder.decode(res) + assertEquals(-1, getForgersOutput.nextStartPos) + assertEquals(1, getForgersOutput.listOfForgerInfo.size) + assertEquals(getForgerCmdInput.forgerPublicKeys, getForgersOutput.listOfForgerInfo.head.forgerPublicKeys) + assertEquals(0, getForgersOutput.listOfForgerInfo.head.rewardShare) + assertEquals(Address.ZERO, getForgersOutput.listOfForgerInfo.head.rewardAddress.address()) + + // Register more forgers, with a reward address + + // add the initial forger to the expected forgers + val listOfExpectedForgers = getForgersOutput.listOfForgerInfo ++ (1 to 100).map {idx => + + val postfix = f"$idx%03d" + val blockSignerProposition = new PublicKey25519Proposition(BytesUtils.fromHexString(s"1122334455667788112233445566778811223344556677881122334455667$postfix")) // 32 bytes + val vrfPublicKey = new VrfPublicKey(BytesUtils.fromHexString(s"d6b775fd4cefc7446236683fdde9d0464bba43cc565fa066b0b3ed1b888b9d1$postfix")) // 33 bytes + + val privateKey1: PrivateKeySecp256k1 = PrivateKeySecp256k1Creator.getInstance().generateSecret(s"nativemsgprocessortest$postfix".getBytes(StandardCharsets.UTF_8)) + val rewardAddress: AddressProposition = privateKey1.publicImage() + val rewardShare = 1000 - idx + + StakeStorage.addForger(view, blockSignerProposition, vrfPublicKey, + rewardShare, rewardAddress.address(), blockContextForkV1_4.consensusEpochNumber, ownerAddressProposition.address(), initialStake) + ForgerInfo(ForgerPublicKeys(blockSignerProposition, vrfPublicKey), rewardShare, rewardAddress) + } + + // Try getForger + listOfExpectedForgers.foreach { expForgerInfo => + getForgerCmdInput = SelectByForgerCmdInput(expForgerInfo.forgerPublicKeys) + getForgerData = BytesUtils.fromHexString(GetForgerCmd) ++ getForgerCmdInput.encode() + msg = getMessage(contractAddress, BigInteger.ZERO, getForgerData, randomNonce) + res = assertGas(10600, msg, view, forgerStakeV2MessageProcessor, blockContextForkV1_4) + + getForgerOutput = GetForgerOutputDecoder.decode(res) + assertEquals(expForgerInfo, getForgerOutput) + } + + // Try getPagedForgers + + @tailrec + def checkPagedResult(listOfExpectedForgers: Seq[ForgerInfo], startPos: Int, pageSize: Int): Unit = { + val (currentPage, remaining) = listOfExpectedForgers.splitAt(pageSize) + if (listOfExpectedForgers.size < pageSize) + assertTrue(remaining.isEmpty) + else + assertEquals(listOfExpectedForgers.size - pageSize, remaining.size) + + val cmdInput = PagedForgersCmdInput( + startPos, + pageSize + ) + val msg = getMessage( + contractAddress, 0, BytesUtils.fromHexString(GetPagedForgersCmd) ++ cmdInput.encode(), randomNonce) + val returnData = withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + //Check getPagedForgers + val res = PagedForgersOutputDecoder.decode(returnData) + assertEquals(currentPage, res.listOfForgerInfo) + if (remaining.isEmpty) + assertEquals(-1, res.nextStartPos) + else { + checkPagedResult(remaining, res.nextStartPos, pageSize) + } + + } + + checkPagedResult(listOfExpectedForgers, 0, listOfExpectedForgers.size + 10) + checkPagedResult(listOfExpectedForgers, 0, listOfExpectedForgers.size) + checkPagedResult(listOfExpectedForgers, 0, listOfExpectedForgers.size - 1) + checkPagedResult(listOfExpectedForgers, 0, 13) + + var startPos = 3 + checkPagedResult(listOfExpectedForgers.drop(startPos), startPos, 5) + + startPos = listOfExpectedForgers.size - 1 + checkPagedResult(listOfExpectedForgers.drop(startPos), startPos, 1) + + + //////////////////////////////////////////////////////////// + // Negative tests + ////////////////////////////////////////////////////////// + + // Check that it is not payable + msg = getMessage(contractAddress, validWeiAmount, getForgerData, randomNonce) + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + expectedErr = "Call value must be zero" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + + msg = getMessage(contractAddress, validWeiAmount, getPagedForgersData, randomNonce) + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + expectedErr = "Call value must be zero" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + + // try processing a msg with a trailing byte in the arguments + var badData = Bytes.concat(getForgerData, new Array[Byte](1)) + var msgBad = getMessage(contractAddress, BigInteger.ZERO, badData, randomNonce) + + // should fail because input has a trailing byte + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msgBad, view, blockContextForkV1_4, _, view)) + } + expectedErr = "Wrong message data field length" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + badData = Bytes.concat(getPagedForgersData, new Array[Byte](1)) + msgBad = getMessage(contractAddress, BigInteger.ZERO, badData, randomNonce) + + // should fail because input has a trailing byte + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msgBad, view, blockContextForkV1_4, _, view)) + } + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // Try getPagedForgers with invalid input + + var cmdInput = PagedForgersCmdInput( + -1, + 10 + ) + msg = getMessage( + contractAddress, 0, BytesUtils.fromHexString(GetPagedForgersCmd) ++ cmdInput.encode(), randomNonce) + exc = intercept[ExecutionRevertedException] { + assertGasInterop(2100, msg, view, processors, blockContextForkV1_4) + } + expectedErr = "Invalid startPos input" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + + cmdInput = PagedForgersCmdInput( + listOfExpectedForgers.size, + 10 + ) + msg = getMessage( + contractAddress, 0, BytesUtils.fromHexString(GetPagedForgersCmd) ++ cmdInput.encode(), randomNonce) + exc = intercept[ExecutionRevertedException] { + assertGasInterop(4200, msg, view, processors, blockContextForkV1_4) + } + expectedErr = "Invalid start position" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + + cmdInput = PagedForgersCmdInput( + 0, + -1 + ) + msg = getMessage( + contractAddress, 0, BytesUtils.fromHexString(GetPagedForgersCmd) ++ cmdInput.encode(), randomNonce) + exc = intercept[ExecutionRevertedException] { + assertGasInterop(2100, msg, view, processors, blockContextForkV1_4) + } + expectedErr = "Invalid page size" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + cmdInput = PagedForgersCmdInput( + 0, + 0 + ) + msg = getMessage( + contractAddress, 0, BytesUtils.fromHexString(GetPagedForgersCmd) ++ cmdInput.encode(), randomNonce) + exc = intercept[ExecutionRevertedException] { + assertGasInterop(2100, msg, view, processors, blockContextForkV1_4) + } + expectedErr = "Invalid page size" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + } + + } + + + @Test + def testGetCurrentConsensusEpoch(): Unit = { + + val processors = Seq(forgerStakeV2MessageProcessor, forgerStakeMessageProcessor) + + usingView(processors) { view => + forgerStakeMessageProcessor.init(view, V1_3_MOCK_FORK_POINT) + forgerStakeV2MessageProcessor.init(view, view.getConsensusEpochNumberAsInt) + + // create sender account with some fund in it + val initialAmount = BigInteger.valueOf(100).multiply(validWeiAmount) + createSenderAccount(view, initialAmount) + + ///////////////////////////////////////////////////////////////////////////////////////////// + // Before activate tests + ///////////////////////////////////////////////////////////////////////////////////////////// + + // Check that getCurrentConsensusEpoch cannot be called before activate + + val getCurrentConsensusEpochData: Array[Byte] = BytesUtils.fromHexString(GetCurrentConsensusEpochCmd) + var msg = getMessage(contractAddress, BigInteger.ZERO, getCurrentConsensusEpochData, randomNonce) + + // Try GetCurrentConsensusEpochCmd before fork 1.4 + val blockContextBeforeFork = new BlockContext( + Address.ZERO, + 0, + 0, + DefaultGasFeeFork.blockGasLimit, + 0, + V1_4_MOCK_FORK_POINT - 1, + 0, + 1, + MockedHistoryBlockHashProvider, + Hash.ZERO + ) + + var exc = intercept[ExecutionRevertedException] { + assertGas(0, msg, view, forgerStakeV2MessageProcessor, blockContextBeforeFork) + } + var expectedErr = "fork not active" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + + // Try getForger after fork 1.4 but before activate + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + + expectedErr = "Forger stake V2 has not been activated yet" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // Call activate + + val initialOwnerBalance = BigInteger.valueOf(200).multiply(validWeiAmount) + createSenderAccount(view, initialOwnerBalance, ownerAddressProposition.address()) + + msg = getMessage( + contractAddress, 0, BytesUtils.fromHexString(ActivateCmd), randomNonce, ownerAddressProposition.address()) + + assertGasInterop(0, msg, view, processors, blockContextForkV1_4) + + ///////////////////////////////////////////////////////////////////////////////////////////// + // Tests + ///////////////////////////////////////////////////////////////////////////////////////////// + + msg = getMessage(contractAddress, BigInteger.ZERO, getCurrentConsensusEpochData, randomNonce) + var res = assertGas(2100, msg, view, forgerStakeV2MessageProcessor, blockContextForkV1_4) + + var epoch = ConsensusEpochCmdOutputDecoder.decode(res).epoch + assertEquals(blockContextForkV1_4.consensusEpochNumber, epoch) + + val expectedEpoch = blockContextForkV1_4.consensusEpochNumber + 345 + res = assertGas(2100, msg, view, forgerStakeV2MessageProcessor, getBlockContextForEpoch(expectedEpoch)) + epoch = ConsensusEpochCmdOutputDecoder.decode(res).epoch + assertEquals(expectedEpoch, epoch) + + //////////////////////////////////////////////////////////// + // Negative tests + ////////////////////////////////////////////////////////// + + // Check that it is not payable + msg = getMessage(contractAddress, validWeiAmount, getCurrentConsensusEpochData, randomNonce) + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msg, view, blockContextForkV1_4, _, view)) + } + expectedErr = "Call value must be zero" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + + // try processing a msg with a trailing byte in the arguments + val badData = Bytes.concat(getCurrentConsensusEpochData, new Array[Byte](1)) + val msgBad = getMessage(contractAddress, BigInteger.ZERO, badData, randomNonce) + + // should fail because input has a trailing byte + exc = intercept[ExecutionRevertedException] { + withGas(TestContext.process(forgerStakeV2MessageProcessor, msgBad, view, blockContextForkV1_4, _, view)) + } + expectedErr = "invalid msg data length" + assertTrue(s"Wrong error message, expected $expectedErr, got: ${exc.getMessage}", exc.getMessage.contains(expectedErr)) + } + + } + + + def checkActivateEvents(listOfLogs: Array[EthereumConsensusDataLog]): Unit = { + assertEquals("Wrong number of logs", 2, listOfLogs.length) + + assertEquals("Wrong address", forgerStakeMessageProcessor.contractAddress, listOfLogs.head.address) + assertArrayEquals("Wrong event signature", getEventSignature("DisableStakeV1()"), listOfLogs.head.topics(0).toBytes) + + assertEquals("Wrong address", contractAddress, listOfLogs(1).address) + assertEquals("Wrong number of topics", 1, listOfLogs(1).topics.length) //The first topic is the hash of the signature of the event + assertArrayEquals("Wrong event signature", ActivateStakeV2EventSig, listOfLogs(1).topics(0).toBytes) + + } + + + def checkRegisterForgerEvent(expectedEvent: RegisterForger, actualEvent: EthereumConsensusDataLog): Unit = { + assertEquals("Wrong address", contractAddress, actualEvent.address) + assertEquals("Wrong number of topics", NumOfIndexedRegisterForgerEvtParams + 1, actualEvent.topics.length) //The first topic is the hash of the signature of the event + assertArrayEquals("Wrong event signature", RegisterForgerEventSig, actualEvent.topics(0).toBytes) + assertEquals("Wrong sender address in topic", expectedEvent.sender, decodeEventTopic(actualEvent.topics(1), TypeReference.makeTypeReference(expectedEvent.sender.getTypeAsString))) + assertEquals("Wrong vrfKey1 in topic", expectedEvent.vrf1, decodeEventTopic(actualEvent.topics(2), TypeReference.makeTypeReference(expectedEvent.vrf1.getTypeAsString))) + assertEquals("Wrong vrfKey2 in topic", expectedEvent.vrf2, decodeEventTopic(actualEvent.topics(3), TypeReference.makeTypeReference(expectedEvent.vrf2.getTypeAsString))) + + val listOfRefs = util.Arrays.asList( + TypeReference.makeTypeReference(expectedEvent.signPubKey.getTypeAsString), + TypeReference.makeTypeReference(expectedEvent.value.getTypeAsString), + TypeReference.makeTypeReference(expectedEvent.rewardShare.getTypeAsString), + TypeReference.makeTypeReference(expectedEvent.rewardAddress.getTypeAsString)) + .asInstanceOf[util.List[TypeReference[Type[_]]]] + val listOfDecodedData = FunctionReturnDecoder.decode(BytesUtils.toHexString(actualEvent.data), listOfRefs) + assertEquals("Wrong signer key in data", expectedEvent.signPubKey, listOfDecodedData.get(0)) + assertEquals("Wrong amount in data", expectedEvent.value.getValue, listOfDecodedData.get(1).getValue) + assertEquals("Wrong reward share in data", expectedEvent.rewardShare.getValue, listOfDecodedData.get(2).getValue) + assertEquals("Wrong reward address in data", expectedEvent.rewardAddress, listOfDecodedData.get(3)) + } + + + def checkUpdateForgerEvent(expectedEvent: UpdateForger, actualEvent: EthereumConsensusDataLog): Unit = { + assertEquals("Wrong address", contractAddress, actualEvent.address) + assertEquals("Wrong number of topics", NumOfIndexedRegisterForgerEvtParams + 1, actualEvent.topics.length) //The first topic is the hash of the signature of the event + assertArrayEquals("Wrong event signature", UpdateForgerEventSig, actualEvent.topics(0).toBytes) + assertEquals("Wrong signer key address in topic", expectedEvent.sender, decodeEventTopic(actualEvent.topics(1), TypeReference.makeTypeReference(expectedEvent.sender.getTypeAsString))) + assertEquals("Wrong vrfKey1 in topic", expectedEvent.vrf1, decodeEventTopic(actualEvent.topics(2), TypeReference.makeTypeReference(expectedEvent.vrf1.getTypeAsString))) + assertEquals("Wrong vrfKey2 in topic", expectedEvent.vrf2, decodeEventTopic(actualEvent.topics(3), TypeReference.makeTypeReference(expectedEvent.vrf2.getTypeAsString))) + + val listOfRefs = util.Arrays.asList( + TypeReference.makeTypeReference(expectedEvent.signPubKey.getTypeAsString), + TypeReference.makeTypeReference(expectedEvent.rewardShare.getTypeAsString), + TypeReference.makeTypeReference(expectedEvent.rewardAddress.getTypeAsString)) + .asInstanceOf[util.List[TypeReference[Type[_]]]] + val listOfDecodedData = FunctionReturnDecoder.decode(BytesUtils.toHexString(actualEvent.data), listOfRefs) + assertEquals("Wrong signature in data", expectedEvent.signPubKey, listOfDecodedData.get(0)) + assertEquals("Wrong reward share in data", expectedEvent.rewardShare.getValue, listOfDecodedData.get(1).getValue) + assertEquals("Wrong reward address in data", expectedEvent.rewardAddress, listOfDecodedData.get(2)) + } + + def checkDelegateForgerStakeEvent(expectedEvent: DelegateForgerStake, actualEvent: EthereumConsensusDataLog): Unit = { + assertEquals("Wrong address", contractAddress, actualEvent.address) + assertEquals("Wrong number of topics", NumOfIndexedDelegateStakeEvtParams + 1, actualEvent.topics.length) //The first topic is the hash of the signature of the event + assertArrayEquals("Wrong event signature", DelegateForgerStakeEventSig, actualEvent.topics(0).toBytes) + assertEquals("Wrong sender address in topic", expectedEvent.sender, decodeEventTopic(actualEvent.topics(1), TypeReference.makeTypeReference(expectedEvent.sender.getTypeAsString))) + assertEquals("Wrong vrfKey1 in topic", expectedEvent.vrf1, decodeEventTopic(actualEvent.topics(2), TypeReference.makeTypeReference(expectedEvent.vrf1.getTypeAsString))) + assertEquals("Wrong vrfKey2 in topic", expectedEvent.vrf2, decodeEventTopic(actualEvent.topics(3), TypeReference.makeTypeReference(expectedEvent.vrf2.getTypeAsString))) + + val listOfRefs = util.Arrays.asList( + TypeReference.makeTypeReference(expectedEvent.signPubKey.getTypeAsString), + TypeReference.makeTypeReference(expectedEvent.value.getTypeAsString)) + .asInstanceOf[util.List[TypeReference[Type[_]]]] + val listOfDecodedData = FunctionReturnDecoder.decode(BytesUtils.toHexString(actualEvent.data), listOfRefs) + assertEquals("Wrong signPubKey in data", expectedEvent.signPubKey, listOfDecodedData.get(0)) + assertEquals("Wrong amount in data", expectedEvent.value.getValue, listOfDecodedData.get(1).getValue) + } + + def checkWithdrawForgerStakeEvent(expectedEvent: WithdrawForgerStake, actualEvent: EthereumConsensusDataLog): Unit = { + assertEquals("Wrong address", contractAddress, actualEvent.address) + assertEquals("Wrong number of topics", NumOfIndexedDelegateStakeEvtParams + 1, actualEvent.topics.length) //The first topic is the hash of the signature of the event + assertArrayEquals("Wrong event signature", WithdrawForgerStakeEventSig, actualEvent.topics(0).toBytes) + assertEquals("Wrong sender address in topic", expectedEvent.sender, decodeEventTopic(actualEvent.topics(1), TypeReference.makeTypeReference(expectedEvent.sender.getTypeAsString))) + assertEquals("Wrong vrfKey1 in topic", expectedEvent.vrf1, decodeEventTopic(actualEvent.topics(2), TypeReference.makeTypeReference(expectedEvent.vrf1.getTypeAsString))) + assertEquals("Wrong vrfKey2 in topic", expectedEvent.vrf2, decodeEventTopic(actualEvent.topics(3), TypeReference.makeTypeReference(expectedEvent.vrf2.getTypeAsString))) + + val listOfRefs = util.Arrays.asList( + TypeReference.makeTypeReference(expectedEvent.signPubKey.getTypeAsString), + TypeReference.makeTypeReference(expectedEvent.value.getTypeAsString)) + .asInstanceOf[util.List[TypeReference[Type[_]]]] + val listOfDecodedData = FunctionReturnDecoder.decode(BytesUtils.toHexString(actualEvent.data), listOfRefs) + assertEquals("Wrong signPubKey in data", expectedEvent.signPubKey, listOfDecodedData.get(0)) + assertEquals("Wrong amount in data", expectedEvent.value.getValue, listOfDecodedData.get(1).getValue) + } + + @Test + def getStakeStartTest() = { + val blockSignerProposition = new PublicKey25519Proposition(BytesUtils.fromHexString("1122334455667788112233445566778811223344556677881122334455667788")) // 32 bytes + val vrfPublicKey = new VrfPublicKey(BytesUtils.fromHexString("d6b775fd4cefc7446236683fdde9d0464bba43cc565fa066b0b3ed1b888b9d1180")) // 33 bytes + val address1: Address = PrivateKeySecp256k1Creator.getInstance().generateSecret("nativemsgprocessortest1".getBytes(StandardCharsets.UTF_8)).publicImage().address() + + usingView(forgerStakeV2MessageProcessor) { view => + forgerStakeV2MessageProcessor.init(view, view.getConsensusEpochNumberAsInt) + // Setup + val initialAmount = BigInteger.valueOf(100).multiply(validWeiAmount) + createSenderAccount(view, initialAmount) + val txHash1 = Keccak256.hash("first tx") + view.setupTxContext(txHash1, 10) + + // assert invocation fails until stake v2 is active + val msg1 = getMessage(contractAddress, BigInteger.ZERO, BytesUtils.fromHexString(StakeStartCmd) ++ Array.emptyByteArray, randomNonce) + val gas1 = new GasPool(1000000000) + assertThrows[ExecutionRevertedException](TestContext.process(forgerStakeV2MessageProcessor, msg1, view, blockContextForkV1_4_plus10, gas1, view)) + StakeStorage.setActive(view) + + // assert default value -1 is returned if delegation does not exist + var stakeStartCmdInput = StakeStartCmdInput(ForgerPublicKeys(blockSignerProposition, vrfPublicKey), address1) + var data: Array[Byte] = stakeStartCmdInput.encode() + var msg = getMessage(contractAddress, BigInteger.ZERO, BytesUtils.fromHexString(StakeStartCmd) ++ data, randomNonce) + var returnData = assertGas(4200, msg, view, forgerStakeV2MessageProcessor, blockContextForkV1_4_plus10) + assertNotNull(returnData) + var stakeStartResponse = StakeStartCmdOutputDecoder.decode(returnData) + assertEquals( + -1, + stakeStartResponse.epoch + ) + + val stakeStartEpoch = V1_4_MOCK_FORK_POINT + 3 + StakeStorage.addForger(view, blockSignerProposition, vrfPublicKey, 100, Address.ZERO, stakeStartEpoch, address1, BigInteger.TEN) + StakeStorage.addStake(view, blockSignerProposition, vrfPublicKey, stakeStartEpoch + 5, address1, BigInteger.TEN) + + // get valid stateStart + stakeStartCmdInput = StakeStartCmdInput(ForgerPublicKeys(blockSignerProposition, vrfPublicKey), address1) + data = stakeStartCmdInput.encode() + msg = getMessage(contractAddress, BigInteger.ZERO, BytesUtils.fromHexString(StakeStartCmd) ++ data, randomNonce) + returnData = assertGas(6300, msg, view, forgerStakeV2MessageProcessor, blockContextForkV1_4_plus10) + assertNotNull(returnData) + stakeStartResponse = StakeStartCmdOutputDecoder.decode(returnData) + assertEquals( + stakeStartEpoch, + stakeStartResponse.epoch + ) + } + } + + private def addStakesV2(view: AccountStateView, + blockSignerProposition: PublicKey25519Proposition, + vrfPublicKey: VrfPublicKey, + ownerAddressProposition1: AddressProposition, + numOfStakes: Int, + blockContext: BlockContext): BigInteger = { + val cmdInput1 = AddNewStakeCmdInput( + ForgerPublicKeys(blockSignerProposition, vrfPublicKey), + ownerAddressProposition1.address() + ) + val data: Array[Byte] = cmdInput1.encode() + + var listOfForgerStakes = Seq[AccountForgingStakeInfo]() + + var totalAmount = BigInteger.ZERO + for (i <- 1 to numOfStakes) { + val stakeAmount = validWeiAmount.multiply(BigInteger.valueOf(i)) + val nonce = randomNonce + val msg = getMessage(contractAddress, stakeAmount, + BytesUtils.fromHexString(AddNewStakeCmdV1) ++ data, nonce) + val expStakeId = forgerStakeMessageProcessor.getStakeId(msg) + listOfForgerStakes = listOfForgerStakes :+ AccountForgingStakeInfo(expStakeId, + ForgerStakeData(ForgerPublicKeys(blockSignerProposition, vrfPublicKey), + ownerAddressProposition1, stakeAmount)) + val returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, blockContext, _, view)) + assertNotNull(returnData) + totalAmount = totalAmount.add(stakeAmount) + } + totalAmount + } + +} + + diff --git a/sdk/src/test/scala/io/horizen/account/state/McAddrOwnershipMsgProcessorMultisigTest.scala b/sdk/src/test/scala/io/horizen/account/state/McAddrOwnershipMsgProcessorMultisigTest.scala index b1e6e4266f..abf510bf86 100644 --- a/sdk/src/test/scala/io/horizen/account/state/McAddrOwnershipMsgProcessorMultisigTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/McAddrOwnershipMsgProcessorMultisigTest.scala @@ -249,7 +249,7 @@ class McAddrOwnershipMsgProcessorMultisigTest 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(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _))) + assertThrows[ExecutionRevertedException](withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _, view))) // Checking that log doesn't change listOfLogs = view.getLogs(txHash2) @@ -480,7 +480,7 @@ class McAddrOwnershipMsgProcessorMultisigTest scAddressObj1 ) var ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Could not verify multisig address against redeemScript")) @@ -495,7 +495,7 @@ class McAddrOwnershipMsgProcessorMultisigTest scAddressObj1 ) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Could not verify multisig address against redeemScript")) @@ -511,7 +511,7 @@ class McAddrOwnershipMsgProcessorMultisigTest scAddressObj1 ) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Could not verify multisig address against redeemScript")) @@ -526,7 +526,7 @@ class McAddrOwnershipMsgProcessorMultisigTest scAddressObj1 ) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Signature 0 not valid")) @@ -542,7 +542,7 @@ class McAddrOwnershipMsgProcessorMultisigTest scAddressObj1 ) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Signatures are not enough. Input has 1, needs at least 2")) @@ -559,7 +559,7 @@ class McAddrOwnershipMsgProcessorMultisigTest scAddressObj1 ) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Signature 1 not valid")) @@ -576,7 +576,7 @@ class McAddrOwnershipMsgProcessorMultisigTest scAddressObj1 ) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } // in this case we are giving up checking signatures because after the first fails we can not // reach the threshold with the second one even if it is valid @@ -600,7 +600,7 @@ class McAddrOwnershipMsgProcessorMultisigTest scAddressObj1 ) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Unexpected format of redeemScript")) @@ -620,7 +620,7 @@ class McAddrOwnershipMsgProcessorMultisigTest scAddressObj1 ) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } // in this case we are giving up checking signatures after we have an invalid one assertTrue(ex.getMessage.contains("Signature 1 not valid")) @@ -654,7 +654,7 @@ class McAddrOwnershipMsgProcessorMultisigTest listOfAllExpectedData.add(McAddrOwnershipData(scAddrStr1.toLowerCase(), mcAddr)) listOfScAddress1ExpectedData.add(McAddrOwnershipData(scAddrStr1.toLowerCase(), mcAddr)) - val returnData = withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _)) + val returnData = withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _, view)) assertNotNull(returnData) } @@ -674,7 +674,7 @@ class McAddrOwnershipMsgProcessorMultisigTest listOfAllExpectedData.add(McAddrOwnershipData(scAddrStr2.toLowerCase(), mcAddr)) listOfScAddress2ExpectedData.add(McAddrOwnershipData(scAddrStr2.toLowerCase(), mcAddr)) - val returnData = withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _)) + val returnData = withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _, view)) assertNotNull(returnData) } @@ -735,7 +735,7 @@ class McAddrOwnershipMsgProcessorMultisigTest listOfExpectedData.add(McAddrOwnershipData(scAddrStr1, mcAddr)) - val returnData = withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _)) + val returnData = withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _, view)) assertNotNull(returnData) } @@ -801,7 +801,7 @@ class McAddrOwnershipMsgProcessorMultisigTest randomNonce, scAddressObj1 ) var ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Value must be zero")) @@ -815,7 +815,7 @@ class McAddrOwnershipMsgProcessorMultisigTest scAddressObj1) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Wrong message data field length")) @@ -828,7 +828,7 @@ class McAddrOwnershipMsgProcessorMultisigTest scAddressObj1) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("already associated")) @@ -841,7 +841,7 @@ class McAddrOwnershipMsgProcessorMultisigTest randomNonce, origin) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Invalid mc signature")) @@ -856,7 +856,7 @@ class McAddrOwnershipMsgProcessorMultisigTest randomNonce, scAddressObj1) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Invalid mc signature")) @@ -871,7 +871,7 @@ class McAddrOwnershipMsgProcessorMultisigTest randomNonce, scAddressObj1) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Invalid mc signature")) @@ -900,7 +900,7 @@ class McAddrOwnershipMsgProcessorMultisigTest ) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msg2, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msg2, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains(s"already associated to sc address ${scAddrStr1.toLowerCase()}")) } @@ -939,7 +939,7 @@ class McAddrOwnershipMsgProcessorMultisigTest randomNonce, scAddressObj1 ) var ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Value must be zero")) @@ -949,7 +949,7 @@ class McAddrOwnershipMsgProcessorMultisigTest randomNonce, scAddressObj1 ) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Value must be zero")) @@ -960,7 +960,7 @@ class McAddrOwnershipMsgProcessorMultisigTest randomNonce, scAddressObj1 ) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Wrong message data field length")) @@ -970,7 +970,7 @@ class McAddrOwnershipMsgProcessorMultisigTest randomNonce, origin ) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("account does not exist")) @@ -981,7 +981,7 @@ class McAddrOwnershipMsgProcessorMultisigTest randomNonce, origin ) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("is not the owner")) @@ -1063,7 +1063,7 @@ class McAddrOwnershipMsgProcessorMultisigTest ) // try processing the removal of ownership, should succeed - val returnData = withGas(TestContext.process(messageProcessor, msg, stateView, defaultBlockContext, _)) + val returnData = withGas(TestContext.process(messageProcessor, msg, stateView, defaultBlockContext, _, stateView)) assertNotNull(returnData) assertArrayEquals(getOwnershipId(mcTransparentAddress), returnData) } @@ -1072,7 +1072,7 @@ class McAddrOwnershipMsgProcessorMultisigTest val msg = getMessage(contractAddress, 0, BytesUtils.fromHexString(GetListOfAllOwnershipsCmd), randomNonce) val (returnData, usedGas) = withGas { gas => - val result = TestContext.process(messageProcessor, msg, stateView, defaultBlockContext, gas) + val result = TestContext.process(messageProcessor, msg, stateView, defaultBlockContext, gas, stateView) (result, gas.getUsedGas) } // gas consumption depends on the number of items in the list @@ -1091,7 +1091,7 @@ class McAddrOwnershipMsgProcessorMultisigTest contractAddress, 0, BytesUtils.fromHexString(GetListOfOwnershipsCmd) ++ data, randomNonce) val (returnData, usedGas) = withGas { gas => - val result = TestContext.process(messageProcessor, msg, stateView, defaultBlockContext, gas) + val result = TestContext.process(messageProcessor, msg, stateView, defaultBlockContext, gas, stateView) (result, gas.getUsedGas) } // gas consumption depends on the number of items in the list @@ -1134,7 +1134,7 @@ class McAddrOwnershipMsgProcessorMultisigTest scAddressObj1 ) val ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } // the signature has been applied on a different message assertTrue(ex.getMessage.contains("Invalid mc signature")) 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 b0812c2949..5b66a76138 100644 --- a/sdk/src/test/scala/io/horizen/account/state/McAddrOwnershipMsgProcessorTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/McAddrOwnershipMsgProcessorTest.scala @@ -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(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _))) + assertThrows[ExecutionRevertedException](withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _, view))) // 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(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _)) + val returnData = withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _, view)) assertNotNull(returnData) } @@ -502,7 +502,7 @@ class McAddrOwnershipMsgProcessorTest listOfAllExpectedData.add(McAddrOwnershipData(scAddrStr2.toLowerCase(), mcAddr)) listOfScAddress2ExpectedData.add(McAddrOwnershipData(scAddrStr2.toLowerCase(), mcAddr)) - val returnData = withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _)) + val returnData = withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _, view)) assertNotNull(returnData) } @@ -573,7 +573,7 @@ class McAddrOwnershipMsgProcessorTest listOfExpectedData.add(McAddrOwnershipData(scAddrStr1, mcAddr)) - val returnData = withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _)) + val returnData = withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _, view)) assertNotNull(returnData) } @@ -639,7 +639,7 @@ class McAddrOwnershipMsgProcessorTest randomNonce, scAddressObj1 ) var ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Value must be zero")) @@ -653,7 +653,7 @@ class McAddrOwnershipMsgProcessorTest scAddressObj1) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Wrong message data field length")) @@ -666,7 +666,7 @@ class McAddrOwnershipMsgProcessorTest scAddressObj1) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("already associated")) @@ -679,7 +679,7 @@ class McAddrOwnershipMsgProcessorTest randomNonce, origin) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Invalid mc signature")) @@ -694,7 +694,7 @@ class McAddrOwnershipMsgProcessorTest randomNonce, scAddressObj1) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Invalid mc signature")) @@ -709,7 +709,7 @@ class McAddrOwnershipMsgProcessorTest randomNonce, scAddressObj1) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Invalid mc signature")) @@ -738,7 +738,7 @@ class McAddrOwnershipMsgProcessorTest ) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msg2, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msg2, view, defaultBlockContext, _, view)) } 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(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Value must be zero")) @@ -787,7 +787,7 @@ class McAddrOwnershipMsgProcessorTest randomNonce, scAddressObj1 ) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Value must be zero")) @@ -798,7 +798,7 @@ class McAddrOwnershipMsgProcessorTest randomNonce, scAddressObj1 ) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("Wrong message data field length")) @@ -808,7 +808,7 @@ class McAddrOwnershipMsgProcessorTest randomNonce, origin ) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } assertTrue(ex.getMessage.contains("account does not exist")) @@ -819,7 +819,7 @@ class McAddrOwnershipMsgProcessorTest randomNonce, origin ) ex = intercept[ExecutionRevertedException] { - withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _, view)) } 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(TestContext.process(messageProcessor, msg, stateView, defaultBlockContext, _)) + val returnData = withGas(TestContext.process(messageProcessor, msg, stateView, defaultBlockContext, _, stateView)) assertNotNull(returnData) assertArrayEquals(getOwnershipId(mcTransparentAddress), returnData) } @@ -911,7 +911,7 @@ class McAddrOwnershipMsgProcessorTest val msg = getMessage(contractAddress, 0, BytesUtils.fromHexString(GetListOfAllOwnershipsCmd), randomNonce) val (returnData, usedGas) = withGas { gas => - val result = TestContext.process(messageProcessor, msg, stateView, defaultBlockContext, gas) + val result = TestContext.process(messageProcessor, msg, stateView, defaultBlockContext, gas, stateView) (result, gas.getUsedGas) } // gas consumption depends on the number of items in the list @@ -930,7 +930,7 @@ class McAddrOwnershipMsgProcessorTest contractAddress, 0, BytesUtils.fromHexString(GetListOfOwnershipsCmd) ++ data, randomNonce) val (returnData, usedGas) = withGas { gas => - val result = TestContext.process(messageProcessor, msg, stateView, defaultBlockContext, gas) + val result = TestContext.process(messageProcessor, msg, stateView, defaultBlockContext, gas, stateView) (result, gas.getUsedGas) } // gas consumption depends on the number of items in the list @@ -969,7 +969,7 @@ class McAddrOwnershipMsgProcessorTest listOfExpectedData.add(McAddrOwnershipData(scAddrStr1, mcAddr)) - val returnData = withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _)) + val returnData = withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _, view)) assertNotNull(returnData) @@ -991,7 +991,7 @@ class McAddrOwnershipMsgProcessorTest scAddressObj1 ) - val returnData2 = withGas(TestContext.process(messageProcessor, msg2, view, defaultBlockContext, _)) + val returnData2 = withGas(TestContext.process(messageProcessor, msg2, view, defaultBlockContext, _, view)) assertNotNull(returnData2) println("This is the returned value: " + BytesUtils.toHexString(returnData2)) @@ -1027,7 +1027,7 @@ class McAddrOwnershipMsgProcessorTest listOfScAddress2ExpectedData.add(McAddrOwnershipData(scAddrStr2.toLowerCase(), mcAddr)) - val returnData = withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _)) + val returnData = withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _, view)) assertNotNull(returnData) } @@ -1045,7 +1045,7 @@ class McAddrOwnershipMsgProcessorTest listOfExpectedData.add(McAddrOwnershipData(scAddrStr1, mcAddr)) - val returnData = withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _)) + val returnData = withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _, view)) assertNotNull(returnData) } diff --git a/sdk/src/test/scala/io/horizen/account/state/McForgerPoolRewardsSerializerTest.scala b/sdk/src/test/scala/io/horizen/account/state/McForgerPoolRewardsSerializerTest.scala index 61a67c6920..6130a8b47e 100644 --- a/sdk/src/test/scala/io/horizen/account/state/McForgerPoolRewardsSerializerTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/McForgerPoolRewardsSerializerTest.scala @@ -1,11 +1,15 @@ package io.horizen.account.state import io.horizen.account.fixtures.ForgerAccountFixture.getPrivateKeySecp256k1 -import io.horizen.account.proposition.AddressProposition +import io.horizen.account.utils.ForgerIdentifier +import io.horizen.proposition.{PublicKey25519Proposition, VrfPublicKey} +import io.horizen.secret.PrivateKey25519Creator +import io.horizen.vrf.VrfGeneratedDataProvider import org.junit.Assert.{assertEquals, fail} import org.junit.Test import java.math.BigInteger +import java.nio.charset.StandardCharsets import scala.util.{Failure, Success} class McForgerPoolRewardsSerializerTest { @@ -16,27 +20,55 @@ class McForgerPoolRewardsSerializerTest { val addr2 = getPrivateKeySecp256k1(1001).publicImage() val addr3 = getPrivateKeySecp256k1(1002).publicImage() val forgerBlockRewards = Map( - addr1 -> BigInteger.valueOf(1L), - addr2 -> BigInteger.valueOf(100L), - addr3 -> BigInteger.valueOf(9999999L), + new ForgerIdentifier(addr1) -> BigInteger.valueOf(1L), + new ForgerIdentifier(addr2) -> BigInteger.valueOf(100L), + new ForgerIdentifier(addr3) -> BigInteger.valueOf(9999999L), ) val bytes = McForgerPoolRewardsSerializer.toBytes(forgerBlockRewards) McForgerPoolRewardsSerializer.parseBytesTry(bytes) match { - case Failure(_) => fail("Parsing failed in McForgerPoolRewardsSerializer") + case Failure(_) => fail("Parsing failed in McForgerPoolRewardsSerializer") case Success(value) => assertEquals("Parsed value different from serialized value", value, forgerBlockRewards) } } @Test def serializationRoundTripTest_Empty(): Unit = { - val forgerBlockRewards = Map.empty[AddressProposition, BigInteger] + val forgerBlockRewards = Map.empty[ForgerIdentifier, BigInteger] val bytes = McForgerPoolRewardsSerializer.toBytes(forgerBlockRewards) McForgerPoolRewardsSerializer.parseBytesTry(bytes) match { - case Failure(_) => fail("Parsing failed in McForgerPoolRewardsSerializer") + case Failure(_) => fail("Parsing failed in McForgerPoolRewardsSerializer") + case Success(value) => assertEquals("Parsed value different from serialized value", value, forgerBlockRewards) + } + } + + @Test + def serializationRoundTripTest_NewFormat(): Unit = { + val addr1 = getPrivateKeySecp256k1(1000).publicImage() + val proposition1: PublicKey25519Proposition = + PrivateKey25519Creator.getInstance().generateSecret("test1".getBytes(StandardCharsets.UTF_8)).publicImage() + val vrfPublicKey1: VrfPublicKey = VrfGeneratedDataProvider.getVrfSecretKey(1).publicImage() + val addr2 = getPrivateKeySecp256k1(1001).publicImage() + val proposition2: PublicKey25519Proposition = + PrivateKey25519Creator.getInstance().generateSecret("test2".getBytes(StandardCharsets.UTF_8)).publicImage() + val vrfPublicKey2: VrfPublicKey = VrfGeneratedDataProvider.getVrfSecretKey(2).publicImage() + val addr3 = getPrivateKeySecp256k1(1002).publicImage() + val proposition3: PublicKey25519Proposition = + PrivateKey25519Creator.getInstance().generateSecret("test3".getBytes(StandardCharsets.UTF_8)).publicImage() + val vrfPublicKey3: VrfPublicKey = VrfGeneratedDataProvider.getVrfSecretKey(3).publicImage() + val forgerBlockRewards = Map( + new ForgerIdentifier(addr1, Some(ForgerPublicKeys(proposition1, vrfPublicKey1)))-> BigInteger.valueOf(1L), + new ForgerIdentifier(addr2, Some(ForgerPublicKeys(proposition2, vrfPublicKey2))) -> BigInteger.valueOf(100L), + new ForgerIdentifier(addr3, Some(ForgerPublicKeys(proposition3, vrfPublicKey3))) -> BigInteger.valueOf(9999999L), + ) + + val bytes = McForgerPoolRewardsSerializer.toBytes(forgerBlockRewards) + + McForgerPoolRewardsSerializer.parseBytesTry(bytes) match { + case Failure(_) => fail("Parsing failed in McForgerPoolRewardsSerializer") case Success(value) => assertEquals("Parsed value different from serialized value", value, forgerBlockRewards) } } 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 ef53df36e5..a6eb994ca2 100644 --- a/sdk/src/test/scala/io/horizen/account/state/MessageProcessorFixture.scala +++ b/sdk/src/test/scala/io/horizen/account/state/MessageProcessorFixture.scala @@ -3,8 +3,8 @@ package io.horizen.account.state import io.horizen.account.AccountFixture import io.horizen.account.fork.GasFeeFork.DefaultGasFeeFork import io.horizen.account.storage.AccountStateMetadataStorageView -import io.horizen.consensus.{ConsensusEpochInfo, intToConsensusEpochNumber} -import io.horizen.evm.{Address, ForkRules, Hash, MemoryDatabase, StateDB} +import io.horizen.consensus.intToConsensusEpochNumber +import io.horizen.evm._ import io.horizen.utils.{BytesUtils, ClosableResourceHandler} import org.junit.Assert.assertEquals import org.mockito.Mockito @@ -15,7 +15,7 @@ import org.web3j.abi.{EventEncoder, FunctionReturnDecoder, TypeReference} import java.math.BigInteger import java.util.Optional import scala.language.implicitConversions -import scala.util.{Failure, Success, Try} +import scala.util.Try trait MessageProcessorFixture extends AccountFixture with ClosableResourceHandler { val metadataStorageView: AccountStateMetadataStorageView = mock[AccountStateMetadataStorageView] @@ -85,12 +85,38 @@ trait MessageProcessorFixture extends AccountFixture with ClosableResourceHandle ): Array[Byte] = { view.setupAccessList(msg, ctx.forgerAddress, new ForkRules(true)) val gas = new GasPool(1000000000) - val result = Try.apply(TestContext.process(processor, msg, view, ctx, gas)) + val result = Try.apply(TestContext.process(processor, msg, view, ctx, gas, view)) + if (expectedGas != gas.getUsedGas){ + val msg = s"Unexpected gas consumption. Expected $expectedGas, actual: ${gas.getUsedGas}" + Console.err.println(msg) + throw new AssertionError(msg) + } +// assertEquals("Unexpected gas consumption", expectedGas, gas.getUsedGas) + // return result or rethrow any exception + result.get + } + + /** + * Creates a large temporary gas pool and verifies the amount of total gas consumed. + * It uses StateTransition instead of TestContext in order to allow calls between smart contracts. + */ + def assertGasInterop( + expectedGas: BigInteger, + msg: Message, + view: AccountStateView, + processors: Seq[MessageProcessor], + ctx: BlockContext, + ): Array[Byte] = { + view.setupAccessList(msg, ctx.forgerAddress, new ForkRules(true)) + val gas = new GasPool(1000000000) + val transition = new StateTransition(view, processors, gas, ctx, msg, view) + val result = Try.apply(transition.execute(Invocation.fromMessage(msg, gas))) assertEquals("Unexpected gas consumption", expectedGas, gas.getUsedGas) // return result or rethrow any exception result.get } + def getEventSignature(eventABISignature: String): Array[Byte] = org.web3j.utils.Numeric.hexStringToByteArray(EventEncoder.buildEventSignature(eventABISignature)) diff --git a/sdk/src/test/scala/io/horizen/account/state/TestContext.scala b/sdk/src/test/scala/io/horizen/account/state/TestContext.scala index da078872ea..5748478c7f 100644 --- a/sdk/src/test/scala/io/horizen/account/state/TestContext.scala +++ b/sdk/src/test/scala/io/horizen/account/state/TestContext.scala @@ -1,5 +1,7 @@ package io.horizen.account.state +import io.horizen.account.storage.MsgProcessorMetadataStorageReader + case class TestContext(msg: Message, blockContext: BlockContext) extends ExecutionContext { override var depth = 0 override def execute(invocation: Invocation): Array[Byte] = ??? @@ -22,8 +24,9 @@ object TestContext { msg: Message, view: BaseAccountStateView, blockContext: BlockContext, - gasPool: GasPool + gasPool: GasPool, + metadata: MsgProcessorMetadataStorageReader ): Array[Byte] = { - processor.process(Invocation.fromMessage(msg, gasPool), view, TestContext(msg, blockContext)) + processor.process(Invocation.fromMessage(msg, gasPool), view, metadata, 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 bf8322c2fe..2804357de8 100644 --- a/sdk/src/test/scala/io/horizen/account/state/WithdrawalMsgProcessorTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/WithdrawalMsgProcessorTest.scala @@ -107,7 +107,7 @@ class WithdrawalMsgProcessorTest extends JUnitSuite with MockitoSugar with Withd val msgWithWrongFunctionCall = getMessage(WithdrawalMsgProcessor.contractAddress, value, data) assertThrows[ExecutionRevertedException] { withGas( - TestContext.process(WithdrawalMsgProcessor, msgWithWrongFunctionCall, mockStateView, defaultBlockContext, _) + TestContext.process(WithdrawalMsgProcessor, msgWithWrongFunctionCall, mockStateView, defaultBlockContext, _, mockStateView) ) } } @@ -157,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(TestContext.process(WithdrawalMsgProcessor, msg, mockStateView, defaultBlockContext, _)) + withGas(TestContext.process(WithdrawalMsgProcessor, msg, mockStateView, defaultBlockContext, _, mockStateView)) ) // helper: mock balance call and assert that the withdrawal request throws @@ -165,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(TestContext.process(WithdrawalMsgProcessor, msg, mockStateView, blockContext, _)) + withGas(TestContext.process(WithdrawalMsgProcessor, msg, mockStateView, blockContext, _, mockStateView)) ) } @@ -211,7 +211,7 @@ class WithdrawalMsgProcessorTest extends JUnitSuite with MockitoSugar with Withd // Withdrawal request list with invalid data should throw ExecutionRevertedException assertThrows[ExecutionRevertedException]( - withGas(TestContext.process(WithdrawalMsgProcessor, msg, mockStateView, defaultBlockContext, _)) + withGas(TestContext.process(WithdrawalMsgProcessor, msg, mockStateView, defaultBlockContext, _, mockStateView)) ) // No withdrawal requests @@ -223,7 +223,7 @@ class WithdrawalMsgProcessorTest extends JUnitSuite with MockitoSugar with Withd .when(mockStateView.getAccountStorage(WithdrawalMsgProcessor.contractAddress, counterKey)) .thenReturn(numOfWithdrawalReqs) - var returnData = withGas(TestContext.process(WithdrawalMsgProcessor, msg, mockStateView, defaultBlockContext, _)) + var returnData = withGas(TestContext.process(WithdrawalMsgProcessor, msg, mockStateView, defaultBlockContext, _, mockStateView)) val expectedListOfWR = new util.ArrayList[WithdrawalRequest]() assertArrayEquals(WithdrawalRequestsListEncoder.encode(expectedListOfWR), returnData) @@ -253,7 +253,7 @@ class WithdrawalMsgProcessorTest extends JUnitSuite with MockitoSugar with Withd }) returnData = - withGas(TestContext.process(WithdrawalMsgProcessor, msg, mockStateView, defaultBlockContext, _), 10000000) + withGas(TestContext.process(WithdrawalMsgProcessor, msg, mockStateView, defaultBlockContext, _, mockStateView), 10000000) assertArrayEquals(WithdrawalRequestsListEncoder.encode(expectedListOfWR), returnData) } @@ -266,7 +266,7 @@ class WithdrawalMsgProcessorTest extends JUnitSuite with MockitoSugar with Withd ) assertThrows[ExecutionRevertedException] { - withGas(TestContext.process(WithdrawalMsgProcessor, msg, mockStateView, defaultBlockContext, _)) + withGas(TestContext.process(WithdrawalMsgProcessor, msg, mockStateView, defaultBlockContext, _, mockStateView)) } msg = getMessage( @@ -276,7 +276,7 @@ class WithdrawalMsgProcessorTest extends JUnitSuite with MockitoSugar with Withd ) assertThrows[ExecutionRevertedException] { - withGas(TestContext.process(WithdrawalMsgProcessor, msg, mockStateView, defaultBlockContext, _)) + withGas(TestContext.process(WithdrawalMsgProcessor, msg, mockStateView, defaultBlockContext, _, mockStateView)) } } } diff --git a/sdk/src/test/scala/io/horizen/account/state/nativescdata/forgerstakev2/StakeStoragePerfTest.scala b/sdk/src/test/scala/io/horizen/account/state/nativescdata/forgerstakev2/StakeStoragePerfTest.scala new file mode 100644 index 0000000000..99dfde9ebc --- /dev/null +++ b/sdk/src/test/scala/io/horizen/account/state/nativescdata/forgerstakev2/StakeStoragePerfTest.scala @@ -0,0 +1,390 @@ +package io.horizen.account.state.nativescdata.forgerstakev2 + +import io.horizen.account.state._ +import io.horizen.account.state.nativescdata.forgerstakev2.StakeStorage._ +import io.horizen.account.utils.WellKnownAddresses.FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS +import io.horizen.account.utils.ZenWeiConverter +import io.horizen.consensus.{ForgingStakeInfo, minForgerStake} +import io.horizen.evm.{Address, Hash, MemoryDatabase, StateDB} +import io.horizen.fixtures.StoreFixture +import io.horizen.proposition.{PublicKey25519Proposition, VrfPublicKey} +import io.horizen.utils.{ByteArrayWrapper, BytesUtils, ForgingStakeMerklePathInfo, MerkleTree} +import org.junit.{Ignore, Test} +import org.scalatestplus.junit.JUnitSuite + +import java.io.{BufferedWriter, FileWriter} +import java.math.BigInteger +import java.util.Calendar +import scala.collection.JavaConverters._ +import scala.language.implicitConversions + +class StakeStoragePerfTest + extends JUnitSuite + with MessageProcessorFixture + with StoreFixture { + + val blockSignerProposition1 = new PublicKey25519Proposition(BytesUtils.fromHexString("1122334455667788112233445566778811223344556677881122334455667788")) // 32 bytes + val vrfPublicKey1 = new VrfPublicKey(BytesUtils.fromHexString("d6b775fd4cefc7446236683fdde9d0464bba43cc565fa066b0b3ed1b888b9d1180")) // 33 bytes + val forger1Key: ForgerKey = ForgerKey(blockSignerProposition1, vrfPublicKey1) + + val delegator1 = new Address("0xaaa00001230000000000deadbeefaaaa2222de01") + + + @Ignore + @Test + def testMultipleForgers(): Unit = { + + val cal = Calendar.getInstance() + using(new MemoryDatabase()) { db => + + var stateDb = new StateDB(db, Hash.ZERO) + // Setup account + using(new AccountStateView(metadataStorageView, stateDb, Seq.empty)) { view => + createSenderAccount(view, BigInteger.TEN, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS) + val rootHash = stateDb.commit() + stateDb = new StateDB(db, rootHash) + } + + val epochNumber = 135869 + val rewardAddress = new Address("0xaaa0000123000000000011112222aaaa22222222") + val rewardShare = 93 + + val numOfSnapshots = 100 + + val numOfForgers = 1000 + val numOfForgersPerSnapshot = numOfForgers / numOfSnapshots + + val listOfForgerKeysWithDelegator = (1 to numOfForgers).map( + idx_forg => { + val postfix32 = f"$idx_forg%064X" + val blockSignerProposition = new PublicKey25519Proposition(BytesUtils.fromHexString(s"$postfix32")) // 32 bytes + val vrfPublicKey = new VrfPublicKey(BytesUtils.fromHexString(s"d2$postfix32")) // 33 bytes + val postfix20 = f"$idx_forg%040X" + val delegator = new Address(s"0x$postfix20") + (blockSignerProposition, vrfPublicKey, delegator) + }) + + + using(new BufferedWriter(new FileWriter(s"log/testAddForgers_${cal.getTimeInMillis}.csv", true))) { out => + // This tests if addForger method increases its time of execution increasing the number of forgers. + // It tests also: + // - the time of the operations performed by the lottery, i.e. getForgingStakes (including filtering), + // creation of the Merkle tree and creation of the Merkle Path. + // - The time and the gas consumed by stakeTotal method. + + out.write("#*********************************************************************\n\n") + out.write("#* Adding forger performance test \n\n") + out.write("#*********************************************************************\n\n") + + out.write(s"# Date and time of the test: ${cal.getTime}\n\n") + + out.write(s"#Total number of forgers: $numOfForgers\n") + out.write(s"#Number of snapshots: $numOfSnapshots\n") + out.write(s"#Number of forgers for each snapshot: $numOfForgersPerSnapshot\n") + + println(s"*************** Adding forgers performance test ***************") + println(s"Total number of forgers: $numOfForgers") + + val listOfSnapshotResults = new scala.collection.mutable.ListBuffer[(Float, Long, Long, Long, Long, BigInteger)]() + + listOfForgerKeysWithDelegator.grouped(numOfForgersPerSnapshot).foreach { listOfForgersPerSnapshot => + var startTime = System.currentTimeMillis() + listOfForgersPerSnapshot.foreach { case (pubSignKey, vrfKey, delegator) => + using(new AccountStateView(metadataStorageView, stateDb, Seq.empty)) { view => + StakeStorage.addForger(view, pubSignKey, vrfKey, rewardShare, rewardAddress, epochNumber, delegator, ZenWeiConverter.convertZenniesToWei(minForgerStake)) + val rootHash = stateDb.commit() + stateDb = new StateDB(db, rootHash) + } + } + val addForgerTime = System.currentTimeMillis() - startTime + using(new AccountStateView(metadataStorageView, stateDb, Seq.empty)) { view => + val signKeyRef = listOfForgersPerSnapshot(numOfForgersPerSnapshot / 2)._1 + val vrfKeyRef = listOfForgersPerSnapshot(numOfForgersPerSnapshot / 2)._2 + startTime = System.currentTimeMillis() + val forgingStakeInfoSeq = StakeStorage.getForgingStakes(view).filter(fsi => fsi.stakeAmount >= minForgerStake) + // sort the resulting sequence by decreasing stake amount + .sorted(Ordering[ForgingStakeInfo].reverse) + + val filteredForgingStakeInfoSeq = forgingStakeInfoSeq.filter(p => { + signKeyRef == (p.blockSignPublicKey) && + vrfKeyRef == p.vrfPublicKey + }) + val filterTime = System.currentTimeMillis() - startTime + + startTime = System.currentTimeMillis() + val forgingStakeInfoTree = MerkleTree.createMerkleTree(forgingStakeInfoSeq.map(info => info.hash).asJava) + val merkleTreeTime = System.currentTimeMillis() - startTime + + startTime = System.currentTimeMillis() + val merkleTreeLeaves = forgingStakeInfoTree.leaves().asScala.map(leaf => new ByteArrayWrapper(leaf)) + + val forgingStakeMerklePathInfoSeq: Seq[ForgingStakeMerklePathInfo] = + filteredForgingStakeInfoSeq.flatMap(forgingStakeInfo => { + merkleTreeLeaves.indexOf(new ByteArrayWrapper(forgingStakeInfo.hash)) match { + case -1 => + None + case index => + Some(ForgingStakeMerklePathInfo(forgingStakeInfo, forgingStakeInfoTree.getMerklePathForLeaf(index))) + } + }) + val merklePathTime = System.currentTimeMillis() - startTime + + val gas = new GasPool(1000000000) + val gasView = view.getGasTrackedView(gas) + val gasBefore = gas.getGas + startTime = System.currentTimeMillis() + StakeStorage.getStakeTotal(gasView, None, None, epochNumber, epochNumber) + val stakeTotalTime = System.currentTimeMillis() - startTime + val gasUsed = gasBefore.subtract(gas.getGas) + listOfSnapshotResults += Tuple6(addForgerTime.toFloat / numOfForgersPerSnapshot, filterTime, merkleTreeTime, merklePathTime, stakeTotalTime, gasUsed) + val rootHash = stateDb.commit() + stateDb = new StateDB(db, rootHash) + } + + } + + out.write(s"\n#********************* Test results *********************\n") + + val totalTime = listOfSnapshotResults.map(_._1).sum + + println(s"AddForger total time $totalTime ms") + val timePerForger: Float = totalTime / numOfSnapshots + println(s"Average time per forger $timePerForger ms") + println( + s"Average time per forger in Snapshots ${listOfSnapshotResults.map(_._1).mkString(", ")} " + ) + out.write(s"# AddForger total time: $totalTime ms\n") + out.write(s"# Average time per forger: $timePerForger ms\n\n") + + out.write(s"AVG addForger Time (ms), Forger Stakes Filter Time (ms), Merkle Tree Time (ms), Merkle Path Time (ms), stakeTotal Time (ms), stakeTotal Gas\n") + listOfSnapshotResults.foreach { res => + out.write(s"${res.productIterator.mkString(",")}\n") + } + + }// FileWriter + + // Testing stakeTotal and the lottery with multiple checkpoints/epochs + using(new BufferedWriter(new FileWriter(s"log/testMultipleEpochs_${cal.getTimeInMillis}.csv", true))) { out => + + // This tests how stakeTotal and Lottery methods increase their time/gas increasing the number of checkpoints. + // Checkpoints are added adding stakes to each forger in each epoch. + // stakeTotal is measured in 3 different epochs ranges: the first epochs, the last epochs and in the middle. + + + out.write("#*********************************************************************\n\n") + out.write("#* Adding checkpoints performance test \n\n") + out.write("#*********************************************************************\n\n") + out.write(s"# Date and time of the test: ${cal.getTime}\n\n") + + val numOfCheckpoints = 200 + val numOfCheckpointsPerSnapshot = numOfCheckpoints / numOfSnapshots + val numOfEpochs = numOfCheckpointsPerSnapshot + out.write(s"# Total number of checkpoints: $numOfCheckpoints\n") + out.write(s"# Number of snapshots: $numOfSnapshots\n") + out.write(s"# Number of checkpoints for each snapshot: $numOfCheckpointsPerSnapshot\n") + out.write(s"# Number of epochs for each stakeTotal call: $numOfEpochs\n") + + println(s"*************** Adding checkpoints performance test ***************") + println(s"Total number of checkpoints: $numOfCheckpoints") + + // In this case, we add a new delegator in any new epoch, so there is one checkpoint for delegator + + val listOfSnapshotResults = new scala.collection.mutable.ListBuffer[(Long, Long, BigInteger, Long, BigInteger, Long, BigInteger)]() + + (1 to numOfCheckpoints).foreach { idx => + + listOfForgerKeysWithDelegator.foreach { case (pubSignKey, vrfKey, _) => + + using(new AccountStateView(metadataStorageView, stateDb, Seq.empty)) { view => + StakeStorage.addStake(view, pubSignKey, vrfKey, epochNumber + idx, delegator1, ZenWeiConverter.convertZenniesToWei(minForgerStake + idx)) + + val rootHash = stateDb.commit() + stateDb = new StateDB(db, rootHash) + } + } + if ((idx + 1) % numOfCheckpointsPerSnapshot == 0) { + using(new AccountStateView(metadataStorageView, stateDb, Seq.empty)) { view => + var startTime = System.currentTimeMillis() + val forgingStakeInfoSeq = StakeStorage.getForgingStakes(view).filter(fsi => fsi.stakeAmount >= minForgerStake) + // sort the resulting sequence by decreasing stake amount + .sorted(Ordering[ForgingStakeInfo].reverse) + + val getForgingStakesTime = System.currentTimeMillis() - startTime + + val gas = new GasPool(1000000000) + val gasView = view.getGasTrackedView(gas) + var gasBefore = gas.getGas + var epochStart = epochNumber + var epochEnd = math.min(epochStart + numOfEpochs - 1, epochNumber + idx) + startTime = System.currentTimeMillis() + StakeStorage.getStakeTotal(gasView, None, None, epochStart, epochEnd) + val stakeTotalTimeFirst = System.currentTimeMillis() - startTime + val gasUsedFirst = gasBefore.subtract(gas.getGas) + + gasBefore = gas.getGas + epochEnd = epochNumber + idx + epochStart = math.max(epochNumber, epochEnd - numOfEpochs + 1) + startTime = System.currentTimeMillis() + StakeStorage.getStakeTotal(gasView, None, None, epochStart, epochEnd) + val stakeTotalTimeLast = System.currentTimeMillis() - startTime + val gasUsedLast = gasBefore.subtract(gas.getGas) + + gasBefore = gas.getGas + val middleEpoch = epochNumber + idx / 2 + val a = numOfEpochs/2 + epochStart = math.max(epochNumber, middleEpoch - a + 1) + epochEnd = math.min(epochStart + numOfEpochs - 1, epochNumber + idx) + startTime = System.currentTimeMillis() + StakeStorage.getStakeTotal(gasView, None, None, epochStart, epochEnd) + val stakeTotalTimeMiddle = System.currentTimeMillis() - startTime + val gasUsedMiddle = gasBefore.subtract(gas.getGas) + listOfSnapshotResults += Tuple7(getForgingStakesTime, stakeTotalTimeFirst, gasUsedFirst, stakeTotalTimeLast, gasUsedLast, stakeTotalTimeMiddle, gasUsedMiddle) + val rootHash = stateDb.commit() + stateDb = new StateDB(db, rootHash) + + } + } + + } + + out.write(s"getForgingStakes Time (ms), stakeTotal First Time (ms), stakeTotal First Gas, stakeTotal Last Time (ms), stakeTotal Last Gas, stakeTotal Middle Time (ms), stakeTotal Middle Gas\n") + + listOfSnapshotResults.foreach { res => + out.write(s"${res.productIterator.mkString(",")}\n") + } + } + + } + + } + + + + @Ignore + @Test + def testSingleForgerMultipleCheckpoints(): Unit = { + + val cal = Calendar.getInstance() + using(new BufferedWriter(new FileWriter(s"log/testSingleForgerMultipleCheckpoints_${cal.getTimeInMillis}.csv", true))) { out => + + // This tests how stakeTotal and addStake methods increase their time/gas increasing the number of checkpoints. + // Checkpoints are added always to the same forger in each epoch. + // stakeTotal is measured in 3 different epochs ranges: the first epochs, the last epochs and in the middle. + + out.write("#*********************************************************************\n\n") + out.write("#* Adding checkpoints performance test \n\n") + out.write("#*********************************************************************\n\n") + + out.write(s"# Date and time of the test: ${cal.getTime}\n\n") + + val numOfCheckpoints = 10000 + val numOfSnapshots = 100 + val numOfCheckpointsPerSnapshot = numOfCheckpoints / numOfSnapshots + val numOfEpochs = numOfCheckpointsPerSnapshot + out.write(s"# Total number of checkpoints: $numOfCheckpoints\n") + out.write(s"# Number of snapshots: $numOfSnapshots\n") + out.write(s"# Number of checkpoints for each snapshot: $numOfCheckpointsPerSnapshot\n") + out.write(s"# Number of epochs for each stakeTotal call: $numOfEpochs\n") + + using(new MemoryDatabase()) { db => + + val epochNumber = 135869 + var stateDb = new StateDB(db, Hash.ZERO) + // setup account and forger + using(new AccountStateView(metadataStorageView, stateDb, Seq.empty)) { view => + createSenderAccount(view, BigInteger.TEN, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS) + // Create a forger + val rewardAddress = new Address("0xaaa0000123000000000011112222aaaa22222222") + val rewardShare = 93 + StakeStorage.addForger(view, blockSignerProposition1, vrfPublicKey1, rewardShare, rewardAddress, epochNumber, delegator1, ZenWeiConverter.convertZenniesToWei(minForgerStake)) + val rootHash = stateDb.commit() + stateDb = new StateDB(db, rootHash) + } + + println(s"*************** Adding checkpoints performance test ***************") + println(s"Total number of checkpoints: $numOfCheckpoints") + + // In this case, we add a new delegator in any new epoch, so there is one checkpoint for delegator + val listOfDelegatorsWithIndex = (1 to numOfCheckpoints).map( + idx_forg => { + val postfix20 = f"$idx_forg%040X" + new Address(s"0x$postfix20") + }).zipWithIndex + + val listOfSnapshotResults = new scala.collection.mutable.ListBuffer[(Float, Long, BigInteger, Long, BigInteger, Long, BigInteger)]() + + val forgerPubKeys = Some(ForgerPublicKeys(blockSignerProposition1, vrfPublicKey1)) + + listOfDelegatorsWithIndex.grouped(numOfCheckpointsPerSnapshot).foreach { listOfDelegatorsWithIndexPerSnapshot => + var startTime = System.currentTimeMillis() + + listOfDelegatorsWithIndexPerSnapshot.foreach { case (delegator, idx) => + using(new AccountStateView(metadataStorageView, stateDb, Seq.empty)) { view => + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber + idx, delegator, ZenWeiConverter.convertZenniesToWei(minForgerStake + idx)) + val rootHash = stateDb.commit() + stateDb = new StateDB(db, rootHash) + } + } + val addStakeTime = System.currentTimeMillis() - startTime + + using(new AccountStateView(metadataStorageView, stateDb, Seq.empty)) { view => + val gas = new GasPool(1000000000) + val gasView = view.getGasTrackedView(gas) + var gasBefore = gas.getGas + val lastEpoch = epochNumber + listOfDelegatorsWithIndexPerSnapshot.last._2 + + var epochStart = epochNumber + var epochEnd = math.min(epochStart + numOfEpochs - 1, lastEpoch) + startTime = System.currentTimeMillis() + StakeStorage.getStakeTotal(gasView, forgerPubKeys, None, epochStart, epochEnd) + val stakeTotalTimeFirst = System.currentTimeMillis() - startTime + val gasUsedFirst = gasBefore.subtract(gas.getGas) + + gasBefore = gas.getGas + epochEnd = lastEpoch + epochStart = math.max(epochNumber, epochEnd - numOfEpochs + 1) + startTime = System.currentTimeMillis() + StakeStorage.getStakeTotal(gasView, forgerPubKeys, None, epochStart, epochEnd) + val stakeTotalTimeLast = System.currentTimeMillis() - startTime + val gasUsedLast = gasBefore.subtract(gas.getGas) + + gasBefore = gas.getGas + val middleEpoch = epochNumber + listOfDelegatorsWithIndexPerSnapshot.last._2 / 2 + val a = numOfEpochs / 2 + epochStart = math.max(epochNumber, middleEpoch - a + 1) + epochEnd = math.min(epochStart + numOfEpochs - 1, lastEpoch) + startTime = System.currentTimeMillis() + StakeStorage.getStakeTotal(gasView, forgerPubKeys, None, epochStart, epochEnd) + val stakeTotalTimeMiddle = System.currentTimeMillis() - startTime + val gasUsedMiddle = gasBefore.subtract(gas.getGas) + listOfSnapshotResults += Tuple7(addStakeTime.toFloat / numOfCheckpointsPerSnapshot, stakeTotalTimeFirst, gasUsedFirst, stakeTotalTimeLast, gasUsedLast, stakeTotalTimeMiddle, gasUsedMiddle) + val rootHash = stateDb.commit() + stateDb = new StateDB(db, rootHash) + } + } + + + out.write(s"\n# ********************* Test results *********************\n") + + val timePerStake: Float = listOfSnapshotResults.map(_._1).sum / numOfSnapshots + println(s"Average time per stake $timePerStake ms") + println( + s"Average time per stake in Snapshots ${listOfSnapshotResults.map(res => res._1).mkString(", ")} " + ) + out.write(s"# Average time per stake: $timePerStake ms\n") + out.write(s"addStake Time (ms), stakeTotal First Time (ms), stakeTotal First Gas, stakeTotal Last Time (ms), stakeTotal Last Gas, stakeTotal Middle Time (ms), stakeTotal Middle Gas\n") + + listOfSnapshotResults.foreach { res => + out.write(s"${res.productIterator.mkString(",")}\n") + } + + } + + } + + + } + + + +} diff --git a/sdk/src/test/scala/io/horizen/account/state/nativescdata/forgerstakev2/StakeStorageTest.scala b/sdk/src/test/scala/io/horizen/account/state/nativescdata/forgerstakev2/StakeStorageTest.scala new file mode 100644 index 0000000000..70624cd2bb --- /dev/null +++ b/sdk/src/test/scala/io/horizen/account/state/nativescdata/forgerstakev2/StakeStorageTest.scala @@ -0,0 +1,1309 @@ +package io.horizen.account.state.nativescdata.forgerstakev2 + +import io.horizen.account.network.ForgerInfo +import io.horizen.account.proposition.AddressProposition +import io.horizen.account.state._ +import io.horizen.account.state.nativescdata.forgerstakev2.StakeStorage._ +import io.horizen.account.utils.WellKnownAddresses.FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS +import io.horizen.account.utils.ZenWeiConverter +import io.horizen.evm.Address +import io.horizen.proposition.{PublicKey25519Proposition, VrfPublicKey} +import io.horizen.utils.BytesUtils +import org.junit.Assert._ +import org.junit.Test +import org.scalatestplus.junit.JUnitSuite + +import java.math.BigInteger +import scala.collection.mutable.ListBuffer +import scala.language.implicitConversions + +class StakeStorageTest + extends JUnitSuite + with MessageProcessorFixture { + + val blockSignerProposition1 = new PublicKey25519Proposition(BytesUtils.fromHexString("1122334455667788112233445566778811223344556677881122334455667788")) // 32 bytes + val vrfPublicKey1 = new VrfPublicKey(BytesUtils.fromHexString("d6b775fd4cefc7446236683fdde9d0464bba43cc565fa066b0b3ed1b888b9d1180")) // 33 bytes + val forger1Key: ForgerKey = ForgerKey(blockSignerProposition1, vrfPublicKey1) + + val blockSignerProposition2 = new PublicKey25519Proposition(BytesUtils.fromHexString("4455334455667788112233445566778811223344556677881122334455667788")) // 32 bytes + val vrfPublicKey2 = new VrfPublicKey(BytesUtils.fromHexString("445575fd4cefc7446236683fdde9d0464bba43cc565fa066b0b3ed1b888b9d1180")) // 33 bytes + val forger2Key: ForgerKey = ForgerKey(blockSignerProposition2, vrfPublicKey2) + + val blockSignerProposition3 = new PublicKey25519Proposition(BytesUtils.fromHexString("5555334455667788112233445566778811223344556677881122334455667788")) // 32 bytes + val vrfPublicKey3 = new VrfPublicKey(BytesUtils.fromHexString("555575fd4cefc7446236683fdde9d0464bba43cc565fa066b0b3ed1b888b9d1180")) // 33 bytes + val forger3Key: ForgerKey = ForgerKey(blockSignerProposition3, vrfPublicKey3) + + val delegator1 = new Address("0xaaa00001230000000000deadbeefaaaa2222de01") + val delegator2 = new Address("0xaaa00001230000000000aaaaaaabbbbb2222de02") + val delegator3 = new Address("0xaaabbbb1230000000000aaaaaaabbbbb2222de03") + + implicit def addressToChecksumAddress(t: Address): DelegatorKey = DelegatorKey(t) + + @Test + def testAddForger(): Unit = { + usingView { view => + + createSenderAccount(view, BigInteger.TEN, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS) + + var result = StakeStorage.getPagedListOfForgers(view, 0, 10) + + assertTrue(result.forgers.isEmpty) + assertEquals(-1, result.nextStartPos) + val epochNumber = 135869 + + val rewardAddress = new Address("0xaaa0000123000000000011112222aaaa22222222") + val rewardShare = 93 + val stakeAmount = BigInteger.TEN + + assertTrue(StakeStorage.getForger(view, blockSignerProposition1, vrfPublicKey1).isEmpty) + + StakeStorage.addForger(view, blockSignerProposition1, vrfPublicKey1, rewardShare, rewardAddress, epochNumber, delegator1, stakeAmount) + + result = StakeStorage.getPagedListOfForgers(view, 0, 10) + + var listOfForgers = result.forgers + assertEquals(1, listOfForgers.size) + assertEquals(blockSignerProposition1, listOfForgers.head.forgerPublicKeys.blockSignPublicKey) + assertEquals(vrfPublicKey1, listOfForgers.head.forgerPublicKeys.vrfPublicKey) + assertEquals(rewardAddress, listOfForgers.head.rewardAddress.address()) + assertEquals(rewardShare, listOfForgers.head.rewardShare) + assertEquals(-1, result.nextStartPos) + + assertEquals(listOfForgers.head, StakeStorage.getForger(view, blockSignerProposition1, vrfPublicKey1).get) + + val delegatorList = DelegatorList(forger1Key) + assertEquals(1, delegatorList.getSize(view)) + assertEquals(delegator1, delegatorList.getDelegatorAt(view, 0)) + + + val forger1History = ForgerStakeHistory(forger1Key) + assertEquals(1, forger1History.getSize(view)) + assertEquals(epochNumber, forger1History.getCheckpoint(view, 0).fromEpochNumber) + assertEquals(stakeAmount, forger1History.getCheckpoint(view, 0).stakedAmount) + assertEquals(stakeAmount, forger1History.getLatestAmount(view)) + + val stakeHistory = StakeHistory(forger1Key, delegator1) + assertEquals(1, stakeHistory.getSize(view)) + assertEquals(epochNumber, stakeHistory.getCheckpoint(view, 0).fromEpochNumber) + assertEquals(stakeAmount, stakeHistory.getCheckpoint(view, 0).stakedAmount) + assertEquals(stakeAmount, stakeHistory.getLatestAmount(view)) + + val forgerList = DelegatorListOfForgerKeys(delegator1) + assertEquals(1, forgerList.getSize(view)) + assertEquals(forger1Key, forgerList.getForgerKey(view, 0)) + + + // Try to register twice the same forger. It should fail + val ex = intercept[ExecutionRevertedException] { + StakeStorage.addForger(view, blockSignerProposition1, vrfPublicKey1, rewardShare, rewardAddress, epochNumber, delegator1, stakeAmount) + } + assertEquals("Forger already registered.", ex.getMessage) + + // Try to register another forger with the same delegator and the same rewardAddress + val rewardShare2 = 87 + val stakeAmount2 = ZenWeiConverter.MAX_MONEY_IN_WEI + val epochNumber2 = 444555444 + StakeStorage.addForger(view, blockSignerProposition2, vrfPublicKey2, rewardShare2, rewardAddress, epochNumber2, delegator1, stakeAmount2) + + result = StakeStorage.getPagedListOfForgers(view, 0, 10) + listOfForgers = result.forgers + assertEquals(2, listOfForgers.size) + assertEquals(blockSignerProposition1, listOfForgers.head.forgerPublicKeys.blockSignPublicKey) + assertEquals(vrfPublicKey1, listOfForgers.head.forgerPublicKeys.vrfPublicKey) + + assertEquals(blockSignerProposition2, listOfForgers(1).forgerPublicKeys.blockSignPublicKey) + assertEquals(vrfPublicKey2, listOfForgers(1).forgerPublicKeys.vrfPublicKey) + assertEquals(rewardAddress, listOfForgers(1).rewardAddress.address()) + assertEquals(rewardShare2, listOfForgers(1).rewardShare) + assertEquals(-1, result.nextStartPos) + + // Check that the first forger was not changed + assertEquals(1, delegatorList.getSize(view)) + assertEquals(delegator1, delegatorList.getDelegatorAt(view, 0)) + + assertEquals(1, forger1History.getSize(view)) + assertEquals(epochNumber, forger1History.getCheckpoint(view, 0).fromEpochNumber) + assertEquals(stakeAmount, forger1History.getCheckpoint(view, 0).stakedAmount) + assertEquals(stakeAmount, forger1History.getLatestAmount(view)) + + assertEquals(1, stakeHistory.getSize(view)) + assertEquals(epochNumber, stakeHistory.getCheckpoint(view, 0).fromEpochNumber) + assertEquals(stakeAmount, stakeHistory.getCheckpoint(view, 0).stakedAmount) + assertEquals(stakeAmount, stakeHistory.getLatestAmount(view)) + + // Check second forger + val delegatorList2 = DelegatorList(forger2Key) + assertEquals(1, delegatorList2.getSize(view)) + assertEquals(delegator1, delegatorList2.getDelegatorAt(view, 0)) + + val forgerHistory2 = ForgerStakeHistory(forger2Key) + assertEquals(1, forgerHistory2.getSize(view)) + assertEquals(epochNumber2, forgerHistory2.getCheckpoint(view, 0).fromEpochNumber) + assertEquals(stakeAmount2, forgerHistory2.getCheckpoint(view, 0).stakedAmount) + assertEquals(stakeAmount2, forgerHistory2.getLatestAmount(view)) + + val stakeHistory2 = StakeHistory(forger2Key, delegator1) + assertEquals(1, stakeHistory2.getSize(view)) + assertEquals(epochNumber2, stakeHistory2.getCheckpoint(view, 0).fromEpochNumber) + assertEquals(stakeAmount2, stakeHistory2.getCheckpoint(view, 0).stakedAmount) + assertEquals(stakeAmount2, stakeHistory2.getLatestAmount(view)) + + assertEquals(2, forgerList.getSize(view)) + assertEquals(forger1Key, forgerList.getForgerKey(view, 0)) + assertEquals(forger2Key, forgerList.getForgerKey(view, 1)) + + // Add a forger without reward address + val blockSignerProposition3 = new PublicKey25519Proposition(BytesUtils.fromHexString("3333334455667788112233445566778811223344556677881122334455667788")) // 32 bytes + val vrfPublicKey3 = new VrfPublicKey(BytesUtils.fromHexString("333375fd4cefc7446236683fdde9d0464bba43cc565fa066b0b3ed1b888b9d1180")) // 33 bytes + StakeStorage.addForger(view, blockSignerProposition3, vrfPublicKey3, 0, Address.ZERO, epochNumber2, delegator1, stakeAmount2) + + result = StakeStorage.getPagedListOfForgers(view, 0, 10) + listOfForgers = result.forgers + assertEquals(3, listOfForgers.size) + assertEquals(blockSignerProposition1, listOfForgers.head.forgerPublicKeys.blockSignPublicKey) + assertEquals(vrfPublicKey1, listOfForgers.head.forgerPublicKeys.vrfPublicKey) + + assertEquals(blockSignerProposition2, listOfForgers(1).forgerPublicKeys.blockSignPublicKey) + assertEquals(vrfPublicKey2, listOfForgers(1).forgerPublicKeys.vrfPublicKey) + assertEquals(rewardAddress, listOfForgers(1).rewardAddress.address()) + assertEquals(rewardShare2, listOfForgers(1).rewardShare) + + assertEquals(blockSignerProposition3, listOfForgers(2).forgerPublicKeys.blockSignPublicKey) + assertEquals(vrfPublicKey3, listOfForgers(2).forgerPublicKeys.vrfPublicKey) + assertEquals(Address.ZERO, listOfForgers(2).rewardAddress.address()) + assertEquals(0, listOfForgers(2).rewardShare) + + assertEquals(-1, result.nextStartPos) + } + } + + + @Test + def testGetPagedListOfForgers(): Unit = { + usingView { view => + + createSenderAccount(view, BigInteger.TEN, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS) + + val result = StakeStorage.getPagedListOfForgers(view, 0, 10) + assertTrue(result.forgers.isEmpty) + assertEquals(-1, result.nextStartPos) + + assertThrows[ExecutionRevertedException] { + StakeStorage.getPagedListOfForgers(view, 0, 0) + } + + assertThrows[ExecutionRevertedException] { + StakeStorage.getPagedListOfForgers(view, 1, 10) + } + + assertThrows[ExecutionRevertedException] { + StakeStorage.getPagedListOfForgers(view, 1, -10) + } + + assertThrows[ExecutionRevertedException] { + StakeStorage.getPagedListOfForgers(view, -1, 10) + } + + val numOfForgers = 100 + val listOfExpectedData = (0 until numOfForgers).map { idx => + val postfix = f"$idx%03d" + val blockSignerProposition = new PublicKey25519Proposition(BytesUtils.fromHexString(s"1122334455667788112233445566778811223344556677881122334455667$postfix")) // 32 bytes + val vrfPublicKey = new VrfPublicKey(BytesUtils.fromHexString(s"d6b775fd4cefc7446236683fdde9d0464bba43cc565fa066b0b3ed1b888b9d1$postfix")) // 33 bytes + + val delegator = new Address(s"0xaaa00001230000000000deadbeefaaaa22222$postfix") + val epochNumber = 135869 + idx + + val rewardAddress = new Address(s"0xaaa0000123000000000011112222aaaa22222$postfix") + val rewardShare = idx + 1 + val stakeAmount = ZenWeiConverter.convertZenniesToWei(idx + 1) + + StakeStorage.addForger(view, blockSignerProposition, vrfPublicKey, rewardShare, rewardAddress, epochNumber, delegator, stakeAmount) + + + val forgerKey = ForgerKey(blockSignerProposition, vrfPublicKey) + val delegatorList = DelegatorList(forgerKey) + assertEquals(1, delegatorList.getSize(view)) + assertEquals(delegator, delegatorList.getDelegatorAt(view, 0)) + + val forgerHistory = ForgerStakeHistory(forgerKey) + assertEquals(1, forgerHistory.getSize(view)) + assertEquals(epochNumber, forgerHistory.getCheckpoint(view, 0).fromEpochNumber) + assertEquals(stakeAmount, forgerHistory.getCheckpoint(view, 0).stakedAmount) + assertEquals(stakeAmount, forgerHistory.getLatestAmount(view)) + + val stakeHistory = StakeHistory(forgerKey, delegator) + assertEquals(1, stakeHistory.getSize(view)) + assertEquals(epochNumber, stakeHistory.getCheckpoint(view, 0).fromEpochNumber) + assertEquals(stakeAmount, stakeHistory.getCheckpoint(view, 0).stakedAmount) + assertEquals(stakeAmount, stakeHistory.getLatestAmount(view)) + + val forgerList = DelegatorListOfForgerKeys(delegator) + assertEquals(1, forgerList.getSize(view)) + assertEquals(forgerKey, forgerList.getForgerKey(view, 0)) + + (blockSignerProposition, vrfPublicKey, rewardAddress, rewardShare) + } + + val pageSize = 11 + var continue = true + var listOfResults = Seq.empty[ForgerInfo] + var startPos = 0 + + while (continue) { + val result = StakeStorage.getPagedListOfForgers(view, startPos, pageSize) + listOfResults = listOfResults ++ result.forgers + continue = if (result.nextStartPos != -1) { + assertEquals(pageSize, result.forgers.size) + true + } + else + false + startPos = result.nextStartPos + } + + assertEquals(listOfExpectedData.size, listOfResults.size) + (0 until numOfForgers).foreach { idx => + val (blockSignerProposition, vrfPublicKey, rewardAddress, rewardShare) = listOfExpectedData(idx) + val forgerInfo = listOfResults(idx) + assertEquals(blockSignerProposition, forgerInfo.forgerPublicKeys.blockSignPublicKey) + assertEquals(vrfPublicKey, forgerInfo.forgerPublicKeys.vrfPublicKey) + assertEquals(rewardAddress, forgerInfo.rewardAddress.address()) + assertEquals(rewardShare, forgerInfo.rewardShare) + } + + } + } + + @Test + def testAddStake(): Unit = { + usingView { view => + + createSenderAccount(view, BigInteger.TEN, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS) + + // Check that we don't have any forger yet + var result = StakeStorage.getPagedListOfForgers(view, 0, 10) + assertTrue(result.forgers.isEmpty) + + val epochNumber1 = 135869 + val stakeAmount1 = BigInteger.valueOf(300) + + // Add stake to a non-registered forger. it should fail + var ex = intercept[ExecutionRevertedException] { + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber1, delegator1, stakeAmount1) + } + assertEquals("Forger doesn't exist.", ex.getMessage) + + // Register the forger and try again adding stakes + val rewardAddress = new Address("0xaaa0000123000000000011112222aaaa22222222") + val rewardShare = 93 + val initialEpochNumber = 125869 + val initialStakeAmount = BigInteger.TEN + StakeStorage.addForger(view, blockSignerProposition1, vrfPublicKey1, rewardShare, rewardAddress, initialEpochNumber, delegator1, initialStakeAmount) + + result = StakeStorage.getPagedListOfForgers(view, 0, 10) + var listOfForgers = result.forgers + assertEquals(1, listOfForgers.size) + assertEquals(blockSignerProposition1, listOfForgers.head.forgerPublicKeys.blockSignPublicKey) + assertEquals(vrfPublicKey1, listOfForgers.head.forgerPublicKeys.vrfPublicKey) + assertEquals(rewardAddress, listOfForgers.head.rewardAddress.address()) + assertEquals(rewardShare, listOfForgers.head.rewardShare) + assertEquals(-1, result.nextStartPos) + + var listOfExpectedForger1Checkpoints = StakeCheckpoint(initialEpochNumber, initialStakeAmount) :: Nil + var listOfExpectedD1F1Checkpoints = StakeCheckpoint(initialEpochNumber, initialStakeAmount) :: Nil + + // Add stake using the same delegator + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber1, delegator1, stakeAmount1) + + val forger1DelegatorList = DelegatorList(forger1Key) + assertEquals(1, forger1DelegatorList.getSize(view)) + assertEquals(delegator1, forger1DelegatorList.getDelegatorAt(view, 0)) + + listOfExpectedForger1Checkpoints = listOfExpectedForger1Checkpoints :+ StakeCheckpoint(epochNumber1, listOfExpectedForger1Checkpoints.last.stakedAmount.add(stakeAmount1)) + listOfExpectedD1F1Checkpoints = listOfExpectedD1F1Checkpoints :+ StakeCheckpoint(epochNumber1, listOfExpectedD1F1Checkpoints.last.stakedAmount.add(stakeAmount1)) + + val forger1History = ForgerStakeHistory(forger1Key) + checkStakeHistory(view, forger1History, listOfExpectedForger1Checkpoints) + + val stakeHistory_d1_f1 = StakeHistory(forger1Key, delegator1) + checkStakeHistory(view, stakeHistory_d1_f1, listOfExpectedD1F1Checkpoints) + + val delegator1ForgerList = DelegatorListOfForgerKeys(delegator1) + assertEquals(1, delegator1ForgerList.getSize(view)) + assertEquals(forger1Key, delegator1ForgerList.getForgerKey(view, 0)) + + // Add another stake from the same delegator in the same consensus epoch + + val stakeAmount2 = BigInteger.valueOf(1000) + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber1, delegator1, stakeAmount2) + + // delegator list shouldn't change + assertEquals(1, forger1DelegatorList.getSize(view)) + + // ForgerHistory size should remain the same, but the value of the last checkpoint should change + listOfExpectedForger1Checkpoints = listOfExpectedForger1Checkpoints.updated(1, StakeCheckpoint(epochNumber1, + listOfExpectedForger1Checkpoints.last.stakedAmount.add(stakeAmount2))) + checkStakeHistory(view, forger1History, listOfExpectedForger1Checkpoints) + + // StakeHistory size should remain the same, but the value of the last checkpoint should change + listOfExpectedD1F1Checkpoints = listOfExpectedD1F1Checkpoints.updated(1, StakeCheckpoint(epochNumber1, + listOfExpectedD1F1Checkpoints.last.stakedAmount.add(stakeAmount2))) + checkStakeHistory(view, stakeHistory_d1_f1, listOfExpectedD1F1Checkpoints) + + // forger list of first delegator shouldn't change + assertEquals(1, delegator1ForgerList.getSize(view)) + assertEquals(forger1Key, delegator1ForgerList.getForgerKey(view, 0)) + + // Add another stake from the another delegator in the same consensus epoch + val stakeAmount_2_1 = BigInteger.valueOf(753536) + + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber1, delegator2, stakeAmount_2_1) + + //Check delegator list + assertEquals(2, forger1DelegatorList.getSize(view)) + assertEquals(delegator1, forger1DelegatorList.getDelegatorAt(view, 0)) + assertEquals(delegator2, forger1DelegatorList.getDelegatorAt(view, 1)) + + // ForgerHistory size should remain the same, but the value of the last checkpoint should change + listOfExpectedForger1Checkpoints = listOfExpectedForger1Checkpoints.updated(1, StakeCheckpoint(epochNumber1, listOfExpectedForger1Checkpoints.last.stakedAmount.add(stakeAmount_2_1))) + checkStakeHistory(view, forger1History, listOfExpectedForger1Checkpoints) + + val stakeHistory_d2_f1 = StakeHistory(forger1Key, delegator2) + var listOfExpectedD2F1Checkpoints = StakeCheckpoint(epochNumber1, stakeAmount_2_1) :: Nil + checkStakeHistory(view, stakeHistory_d2_f1, listOfExpectedD2F1Checkpoints) + + // Check delegator2 forger list + val delegator2ForgerList = DelegatorListOfForgerKeys(delegator2) + assertEquals(1, delegator2ForgerList.getSize(view)) + assertEquals(forger1Key, delegator1ForgerList.getForgerKey(view, 0)) + + // Add another stake from the second delegator in a different consensus epoch + val stakeAmount_2_2 = BigInteger.valueOf(22356) + val epochNumber2 = epochNumber1 + 10 + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber2, delegator2, stakeAmount_2_2) + + //Check delegator list, shouldn't change + assertEquals(2, forger1DelegatorList.getSize(view)) + assertEquals(delegator1, forger1DelegatorList.getDelegatorAt(view, 0)) + assertEquals(delegator2, forger1DelegatorList.getDelegatorAt(view, 1)) + + // Check ForgerHistory, we should have 3 checkpoints + listOfExpectedForger1Checkpoints = listOfExpectedForger1Checkpoints :+ StakeCheckpoint(epochNumber2, listOfExpectedForger1Checkpoints.last.stakedAmount.add(stakeAmount_2_2)) + checkStakeHistory(view, forger1History, listOfExpectedForger1Checkpoints) + + // Check delegator1 stake history, shouldn't change + checkStakeHistory(view, stakeHistory_d1_f1, listOfExpectedD1F1Checkpoints) + + // Check delegator2 stake history, we should have 2 checkpoints + listOfExpectedD2F1Checkpoints = listOfExpectedD2F1Checkpoints :+ StakeCheckpoint(epochNumber2, listOfExpectedD2F1Checkpoints.last.stakedAmount.add(stakeAmount_2_2)) + checkStakeHistory(view, stakeHistory_d2_f1, listOfExpectedD2F1Checkpoints) + + // Check delegator1 forger list + assertEquals(1, delegator1ForgerList.getSize(view)) + assertEquals(forger1Key, delegator1ForgerList.getForgerKey(view, 0)) + + // Check delegator2 forger list + assertEquals(1, delegator2ForgerList.getSize(view)) + assertEquals(forger1Key, delegator2ForgerList.getForgerKey(view, 0)) + + // Register another forger with delegator2 + val epochNumber3 = epochNumber2 + 65 + + StakeStorage.addForger(view, blockSignerProposition2, vrfPublicKey2, rewardShare, rewardAddress, epochNumber3, delegator2, initialStakeAmount) + + result = StakeStorage.getPagedListOfForgers(view, 0, 10) + + listOfForgers = result.forgers + assertEquals(2, listOfForgers.size) + assertEquals(blockSignerProposition1, listOfForgers.head.forgerPublicKeys.blockSignPublicKey) + assertEquals(vrfPublicKey1, listOfForgers.head.forgerPublicKeys.vrfPublicKey) + assertEquals(rewardAddress, listOfForgers.head.rewardAddress.address()) + assertEquals(rewardShare, listOfForgers.head.rewardShare) + + assertEquals(blockSignerProposition2, listOfForgers(1).forgerPublicKeys.blockSignPublicKey) + assertEquals(vrfPublicKey2, listOfForgers(1).forgerPublicKeys.vrfPublicKey) + assertEquals(rewardAddress, listOfForgers(1).rewardAddress.address()) + assertEquals(rewardShare, listOfForgers(1).rewardShare) + assertEquals(-1, result.nextStartPos) + + // Check delegator2 forger list + assertEquals(2, delegator2ForgerList.getSize(view)) + assertEquals(forger1Key, delegator2ForgerList.getForgerKey(view, 0)) + assertEquals(forger2Key, delegator2ForgerList.getForgerKey(view, 1)) + + // Check delegator2/forger1 stake history, shouldn't change + checkStakeHistory(view, stakeHistory_d2_f1, listOfExpectedD2F1Checkpoints) + + // Check delegator2/forger2 stake history + val stakeHistory_d2_f2 = StakeHistory(forger2Key, delegator2) + val listOfExpectedD2F2Checkpoints = StakeCheckpoint(epochNumber3, initialStakeAmount) :: Nil + checkStakeHistory(view, stakeHistory_d2_f2, listOfExpectedD2F2Checkpoints) + + // Add stake using a delegator address all upper case. It should be treated as the same delegator. + + val epochNumber4 = epochNumber3 + 10 + val DELEGATOR1 = new Address("0x" + delegator1.toStringNoPrefix.toUpperCase) + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber4, DELEGATOR1, stakeAmount1) + + assertEquals(2, forger1DelegatorList.getSize(view)) + assertEquals(delegator1, forger1DelegatorList.getDelegatorAt(view, 0)) + assertEquals(delegator2, forger1DelegatorList.getDelegatorAt(view, 1)) + + listOfExpectedForger1Checkpoints = listOfExpectedForger1Checkpoints :+ StakeCheckpoint(epochNumber4, listOfExpectedForger1Checkpoints.last.stakedAmount.add(stakeAmount1)) + listOfExpectedD1F1Checkpoints = listOfExpectedD1F1Checkpoints :+ StakeCheckpoint(epochNumber4, listOfExpectedD1F1Checkpoints.last.stakedAmount.add(stakeAmount1)) + + checkStakeHistory(view, forger1History, listOfExpectedForger1Checkpoints) + + checkStakeHistory(view, stakeHistory_d1_f1, listOfExpectedD1F1Checkpoints) + + assertArrayEquals(delegator1ForgerList.keySeed, DelegatorListOfForgerKeys(DELEGATOR1).keySeed) + + // Test with epoch before the last one. It should fail. + val badEpoch = epochNumber2 - 10 + ex = intercept[ExecutionRevertedException] { + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, badEpoch, delegator2, stakeAmount_2_2) + } + assertEquals(s"Epoch is in the past: epoch $badEpoch, last epoch: $epochNumber4", ex.getMessage) + + } + } + + @Test + def testRemoveStake(): Unit = { + usingView { view => + + createSenderAccount(view, BigInteger.TEN, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS) + + // Check that we don't have any forger yet + var result = StakeStorage.getPagedListOfForgers(view, 0, 10) + assertTrue(result.forgers.isEmpty) + + val epochNumber1 = 135869 + val stakeAmount1 = BigInteger.valueOf(5358869) + + // Remove stake from a non-registered forger, it should fail + var ex = intercept[ExecutionRevertedException] { + StakeStorage.removeStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber1, delegator1, stakeAmount1) + } + assertEquals("Forger doesn't exist.", ex.getMessage) + + // Register the forger and try again removing stakes + val rewardAddress = new Address("0xaaa0000123000000000011112222aaaa22222222") + val rewardShare = 93 + val initialEpochNumber = 125869 + val initialStakeAmount = ZenWeiConverter.MAX_MONEY_IN_WEI + + StakeStorage.addForger(view, blockSignerProposition1, vrfPublicKey1, rewardShare, rewardAddress, initialEpochNumber, delegator1, initialStakeAmount) + result = StakeStorage.getPagedListOfForgers(view, 0, 10) + val listOfForgers = result.forgers + assertEquals(1, listOfForgers.size) + assertEquals(blockSignerProposition1, listOfForgers.head.forgerPublicKeys.blockSignPublicKey) + assertEquals(vrfPublicKey1, listOfForgers.head.forgerPublicKeys.vrfPublicKey) + assertEquals(rewardAddress, listOfForgers.head.rewardAddress.address()) + assertEquals(rewardShare, listOfForgers.head.rewardShare) + assertEquals(-1, result.nextStartPos) + + var listOfExpectedForger1Checkpoints = StakeCheckpoint(initialEpochNumber, initialStakeAmount) :: Nil + var listOfExpectedD1F1Checkpoints = StakeCheckpoint(initialEpochNumber, initialStakeAmount) :: Nil + + // Remove stake using the same delegator + StakeStorage.removeStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber1, delegator1, stakeAmount1) + + listOfExpectedForger1Checkpoints = listOfExpectedForger1Checkpoints :+ StakeCheckpoint(epochNumber1, listOfExpectedForger1Checkpoints.last.stakedAmount.subtract(stakeAmount1)) + listOfExpectedD1F1Checkpoints = listOfExpectedD1F1Checkpoints :+ StakeCheckpoint(epochNumber1, listOfExpectedD1F1Checkpoints.last.stakedAmount.subtract(stakeAmount1)) + + val forger1DelegatorList = DelegatorList(forger1Key) + assertEquals(1, forger1DelegatorList.getSize(view)) + assertEquals(delegator1, forger1DelegatorList.getDelegatorAt(view, 0)) + + val forger1History = ForgerStakeHistory(forger1Key) + checkStakeHistory(view, forger1History, listOfExpectedForger1Checkpoints) + + val stakeHistory_d1_f1 = StakeHistory(forger1Key, delegator1) + checkStakeHistory(view, stakeHistory_d1_f1, listOfExpectedD1F1Checkpoints) + + val delegator1ForgerList = DelegatorListOfForgerKeys(delegator1) + assertEquals(1, delegator1ForgerList.getSize(view)) + assertEquals(forger1Key, delegator1ForgerList.getForgerKey(view, 0)) + + // Remove another stake from the same delegator in the same consensus epoch, using a delegator address all upper case + // It should be treated as the same delegator. + + val DELEGATOR1 = new Address("0x" + delegator1.toStringNoPrefix.toUpperCase) + val stakeAmount2 = BigInteger.valueOf(1000) + StakeStorage.removeStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber1, DELEGATOR1, stakeAmount2) + + // delegator list shouldn't change + assertEquals(1, forger1DelegatorList.getSize(view)) + + // ForgerHistory size should remain the same, but the value of the last checkpoint should change + listOfExpectedForger1Checkpoints = listOfExpectedForger1Checkpoints.updated(1, StakeCheckpoint(epochNumber1, listOfExpectedForger1Checkpoints.last.stakedAmount.subtract(stakeAmount2))) + checkStakeHistory(view, forger1History, listOfExpectedForger1Checkpoints) + + // StakeHistory size should remain the same, but the value of the last checkpoint should change + listOfExpectedD1F1Checkpoints = listOfExpectedD1F1Checkpoints.updated(1, StakeCheckpoint(epochNumber1, listOfExpectedD1F1Checkpoints.last.stakedAmount.subtract(stakeAmount2))) + checkStakeHistory(view, stakeHistory_d1_f1, listOfExpectedD1F1Checkpoints) + + // forger list of first delegator shouldn't change + assertEquals(1, delegator1ForgerList.getSize(view)) + assertEquals(forger1Key, delegator1ForgerList.getForgerKey(view, 0)) + + // Remove stake from the another delegator. It should fail + val stakeAmount_2_1 = BigInteger.valueOf(753536) + + assertThrows[ExecutionRevertedException] { + StakeStorage.removeStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber1, delegator2, stakeAmount_2_1) + } + + //Add some stake for delegator 2 + val epochNumber2 = epochNumber1 + 10 + val stakeAmount_2_2 = stakeAmount_2_1.multiply(BigInteger.TEN) + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber2, delegator2, stakeAmount_2_2) + + //Check delegator list + assertEquals(2, forger1DelegatorList.getSize(view)) + assertEquals(delegator1, forger1DelegatorList.getDelegatorAt(view, 0)) + assertEquals(delegator2, forger1DelegatorList.getDelegatorAt(view, 1)) + + // Check ForgerHistory + listOfExpectedForger1Checkpoints = listOfExpectedForger1Checkpoints :+ StakeCheckpoint(epochNumber2, listOfExpectedForger1Checkpoints.last.stakedAmount.add(stakeAmount_2_2)) + checkStakeHistory(view, forger1History, listOfExpectedForger1Checkpoints) + + val stakeHistory_d2_f1 = StakeHistory(forger1Key, delegator2) + var listOfExpectedD2F1Checkpoints = StakeCheckpoint(epochNumber2, stakeAmount_2_2) :: Nil + checkStakeHistory(view, stakeHistory_d2_f1, listOfExpectedD2F1Checkpoints) + + // Check delegator2 forger list + val delegator2ForgerList = DelegatorListOfForgerKeys(delegator2) + assertEquals(1, delegator2ForgerList.getSize(view)) + assertEquals(forger1Key, delegator1ForgerList.getForgerKey(view, 0)) + + // Check delegator1/forger1 stake, it shouldn't change + checkStakeHistory(view, stakeHistory_d1_f1, listOfExpectedD1F1Checkpoints) + + // Remove stake from delegator2 from another epoch + val epochNumber3 = epochNumber2 + 4756 + StakeStorage.removeStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber3, delegator2, stakeAmount_2_1) + + // Check ForgerHistory + listOfExpectedForger1Checkpoints = listOfExpectedForger1Checkpoints :+ StakeCheckpoint(epochNumber3, listOfExpectedForger1Checkpoints.last.stakedAmount.subtract(stakeAmount_2_1)) + checkStakeHistory(view, forger1History, listOfExpectedForger1Checkpoints) + + listOfExpectedD2F1Checkpoints = listOfExpectedD2F1Checkpoints :+ StakeCheckpoint(epochNumber3, listOfExpectedD2F1Checkpoints.last.stakedAmount.subtract(stakeAmount_2_1)) + checkStakeHistory(view, stakeHistory_d2_f1, listOfExpectedD2F1Checkpoints) + + // Remove stake from delegator1 from same epoch + val stakeAmount3 = BigInteger.valueOf(1000) + StakeStorage.removeStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber3, delegator1, stakeAmount3) + + // Check ForgerHistory + listOfExpectedForger1Checkpoints = listOfExpectedForger1Checkpoints.updated(listOfExpectedForger1Checkpoints.size - 1, + StakeCheckpoint(epochNumber3, listOfExpectedForger1Checkpoints.last.stakedAmount.subtract(stakeAmount3))) + checkStakeHistory(view, forger1History, listOfExpectedForger1Checkpoints) + + listOfExpectedD1F1Checkpoints = listOfExpectedD1F1Checkpoints :+ StakeCheckpoint(epochNumber3, listOfExpectedD1F1Checkpoints.last.stakedAmount.subtract(stakeAmount3)) + checkStakeHistory(view, stakeHistory_d1_f1, listOfExpectedD1F1Checkpoints) + + // Try to remove stake with epoch before the last one. It should fail. + val badEpoch = epochNumber3 - 10 + ex = intercept[ExecutionRevertedException] { + StakeStorage.removeStake(view, blockSignerProposition1, vrfPublicKey1, badEpoch, delegator2, stakeAmount_2_1) + } + assertEquals(s"Epoch is in the past: epoch $badEpoch, last epoch: $epochNumber3", ex.getMessage) + + // Try to remove more stake than available. It should fail + val epochNumber4 = epochNumber3 + 44 + assertThrows[ExecutionRevertedException] { + StakeStorage.removeStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber4, delegator1, listOfExpectedD1F1Checkpoints.last.stakedAmount.add(BigInteger.ONE)) + } + + // Try to remove all delegator1 stake. History should remain available + StakeStorage.removeStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber4, delegator1, listOfExpectedD1F1Checkpoints.last.stakedAmount) + // Check ForgerHistory + listOfExpectedForger1Checkpoints = listOfExpectedForger1Checkpoints :+ StakeCheckpoint(epochNumber4, listOfExpectedForger1Checkpoints.last.stakedAmount.subtract(listOfExpectedD1F1Checkpoints.last.stakedAmount)) + checkStakeHistory(view, forger1History, listOfExpectedForger1Checkpoints) + + listOfExpectedD1F1Checkpoints = listOfExpectedD1F1Checkpoints :+ StakeCheckpoint(epochNumber4, BigInteger.ZERO) + checkStakeHistory(view, stakeHistory_d1_f1, listOfExpectedD1F1Checkpoints) + + // Try to remove all delegator2 stake. History should remain available + val epochNumber5 = epochNumber4 + 12 + StakeStorage.removeStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber5, delegator2, listOfExpectedD2F1Checkpoints.last.stakedAmount) + // Check ForgerHistory + listOfExpectedForger1Checkpoints = listOfExpectedForger1Checkpoints :+ StakeCheckpoint(epochNumber5, BigInteger.ZERO) + checkStakeHistory(view, forger1History, listOfExpectedForger1Checkpoints) + + listOfExpectedD2F1Checkpoints = listOfExpectedD2F1Checkpoints :+ StakeCheckpoint(epochNumber5, BigInteger.ZERO) + checkStakeHistory(view, stakeHistory_d2_f1, listOfExpectedD2F1Checkpoints) + + } + } + + @Test + def testDuplicateCheckpoints(): Unit = { + usingView { view => + + createSenderAccount(view, BigInteger.TEN, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS) + + // Register the forger and try again removing stakes + val rewardAddress = new Address("0xaaa0000123000000000011112222aaaa22222222") + val rewardShare = 93 + val initialEpochNumber = 125869 + val initialStakeAmount = BigInteger.valueOf(5358869) + + StakeStorage.addForger(view, blockSignerProposition1, vrfPublicKey1, rewardShare, rewardAddress, initialEpochNumber, delegator1, initialStakeAmount) + + // Remove and then add again the same amount in the same epoch of the registration. Check everything works + + val stakeAmount1 = BigInteger.valueOf(5358869) + + StakeStorage.removeStake(view, blockSignerProposition1, vrfPublicKey1, initialEpochNumber, delegator1, stakeAmount1) + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, initialEpochNumber, delegator1, stakeAmount1) + + var listOfExpectedForger1Checkpoints = StakeCheckpoint(initialEpochNumber, initialStakeAmount) :: Nil + var listOfExpectedD1F1Checkpoints = StakeCheckpoint(initialEpochNumber, initialStakeAmount) :: Nil + + val forger1History = ForgerStakeHistory(forger1Key) + checkStakeHistory(view, forger1History, listOfExpectedForger1Checkpoints) + + val stakeHistory_d1_f1 = StakeHistory(forger1Key, delegator1) + checkStakeHistory(view, stakeHistory_d1_f1, listOfExpectedD1F1Checkpoints) + + // Let's do the same but in another epoch. The stake history should not becomes bigger + + val epochNumber1 = 135869 + StakeStorage.removeStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber1, delegator1, stakeAmount1) + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber1, delegator1, stakeAmount1) + + checkStakeHistory(view, forger1History, listOfExpectedForger1Checkpoints) + checkStakeHistory(view, stakeHistory_d1_f1, listOfExpectedD1F1Checkpoints) + + // Add again another stake in the same epoch. Check that we have a new checkpoint + val stakeAmount2 = BigInteger.valueOf(5555555) + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber1, delegator1, stakeAmount2) + + listOfExpectedForger1Checkpoints = listOfExpectedForger1Checkpoints :+ StakeCheckpoint(epochNumber1, initialStakeAmount.add(stakeAmount2)) + listOfExpectedD1F1Checkpoints = listOfExpectedD1F1Checkpoints :+ StakeCheckpoint(epochNumber1, initialStakeAmount.add(stakeAmount2)) + + checkStakeHistory(view, forger1History, listOfExpectedForger1Checkpoints) + checkStakeHistory(view, stakeHistory_d1_f1, listOfExpectedD1F1Checkpoints) + + } + } + + + @Test + def testGetAllForgerStakes(): Unit = { + usingView { view => + + createSenderAccount(view, BigInteger.TEN, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS) + + var listOfStakes = StakeStorage.getAllForgerStakes(view) + assertTrue(listOfStakes.isEmpty) + + val rewardAddress = new Address(s"0xaaa0000123000000000011112222aaaa22222111") + val rewardShare = 90 + var epochNumber = 135869 + val stakeAmount1 = BigInteger.valueOf(10000000000L) + StakeStorage.addForger(view, blockSignerProposition1, vrfPublicKey1, rewardShare, rewardAddress, epochNumber, delegator1, stakeAmount1) + var listOfExpectedData = ForgerStakeData(ForgerPublicKeys(blockSignerProposition1, vrfPublicKey1), new AddressProposition(delegator1), stakeAmount1) :: Nil + + listOfStakes = StakeStorage.getAllForgerStakes(view) + assertEquals(listOfExpectedData, listOfStakes) + + epochNumber += 10 + + val stakeAmount2 = BigInteger.valueOf(20000000000L) + StakeStorage.addForger(view, blockSignerProposition2, vrfPublicKey2, rewardShare, rewardAddress, epochNumber, delegator1, stakeAmount2) + listOfExpectedData = listOfExpectedData :+ ForgerStakeData(ForgerPublicKeys(blockSignerProposition2, vrfPublicKey2), new AddressProposition(delegator1), stakeAmount2) + + listOfStakes = StakeStorage.getAllForgerStakes(view) + assertEquals(listOfExpectedData, listOfStakes) + + epochNumber += 10 + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber, delegator1, stakeAmount1) + listOfExpectedData = listOfExpectedData.updated(0, ForgerStakeData(ForgerPublicKeys(blockSignerProposition1, vrfPublicKey1), new AddressProposition(delegator1), stakeAmount1.add(stakeAmount1))) + + listOfStakes = StakeStorage.getAllForgerStakes(view) + assertEquals(listOfExpectedData, listOfStakes) + + epochNumber += 10 + StakeStorage.addStake(view, blockSignerProposition2, vrfPublicKey2, epochNumber, delegator2, stakeAmount1) + listOfExpectedData = listOfExpectedData :+ ForgerStakeData(ForgerPublicKeys(blockSignerProposition2, vrfPublicKey2), new AddressProposition(delegator2), stakeAmount1) + StakeStorage.addStake(view, blockSignerProposition2, vrfPublicKey2, epochNumber, delegator3, stakeAmount2) + listOfExpectedData = listOfExpectedData :+ ForgerStakeData(ForgerPublicKeys(blockSignerProposition2, vrfPublicKey2), new AddressProposition(delegator3), stakeAmount2) + + listOfStakes = StakeStorage.getAllForgerStakes(view) + assertEquals(listOfExpectedData, listOfStakes) + + // Remove all forger2/delegator3 stakes. forger2/delegator3 stake shouldn't be in the resulting list + epochNumber += 10 + StakeStorage.removeStake(view, blockSignerProposition2, vrfPublicKey2, epochNumber, delegator3, stakeAmount2) + listOfExpectedData = listOfExpectedData.slice(0, listOfExpectedData.size - 1) + listOfStakes = StakeStorage.getAllForgerStakes(view) + assertEquals(listOfExpectedData, listOfStakes) + + + // Remove all forger1 stakes. forger1 shouldn't be in the resulting list + epochNumber += 10 + StakeStorage.removeStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber, delegator1, stakeAmount1.add(stakeAmount1)) + listOfExpectedData = listOfExpectedData.slice(1, listOfExpectedData.size) + listOfStakes = StakeStorage.getAllForgerStakes(view) + assertEquals(listOfExpectedData, listOfStakes) + + } + } + + @Test + def testGetPagedForgerStakes(): Unit = { + usingView { view => + + createSenderAccount(view, BigInteger.TEN, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS) + + // Activate Forger V2 + StakeStorage.setActive(view) + + // check that at the very beginning we have empty lists + val listOfStakesForgerEmpty = StakeStorage.getPagedForgersStakesByForger(view, ForgerPublicKeys(blockSignerProposition1, vrfPublicKey1), 0, 100) + assertTrue(listOfStakesForgerEmpty.stakesData.isEmpty) + assertTrue(listOfStakesForgerEmpty.nextStartPos == -1) + + val listOfStakesDelegatorEmpty = StakeStorage.getPagedForgersStakesByDelegator(view, delegator1, 0, 100) + assertTrue(listOfStakesDelegatorEmpty.stakesData.isEmpty) + assertTrue(listOfStakesDelegatorEmpty.nextStartPos == -1) + + val rewardAddress = new Address(s"0xaaa0000123000000000011112222aaaa22222111") + val rewardShare = 100 + val epochNumber = 1000 + + val stakeAmount1 = BigInteger.valueOf(10000000000L) + StakeStorage.addForger(view, blockSignerProposition1, vrfPublicKey1, rewardShare, rewardAddress, epochNumber, delegator1, stakeAmount1) + + val stakeAmount2 = BigInteger.valueOf(20000000000L) + StakeStorage.addForger(view, blockSignerProposition2, vrfPublicKey2, rewardShare, rewardAddress, epochNumber, delegator1, stakeAmount2) + + val stakeAmount3 = BigInteger.valueOf(40000000000L) + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber, delegator1, stakeAmount3) + + val stakeAmount4 = BigInteger.valueOf(80000000000L) + StakeStorage.addStake(view, blockSignerProposition2, vrfPublicKey2, epochNumber, delegator2, stakeAmount4) + + val stakeAmount5 = BigInteger.valueOf(160000000000L) + StakeStorage.addStake(view, blockSignerProposition2, vrfPublicKey2, epochNumber, delegator3, stakeAmount5) + + val listOfStakesForger1 = StakeStorage.getPagedForgersStakesByForger(view, ForgerPublicKeys(blockSignerProposition1, vrfPublicKey1), 0, 100) + // check forger1 has 1! delegator with staked amount as the sum of two contributions + assertTrue(listOfStakesForger1.stakesData.size == 1) + assertEquals(listOfStakesForger1.stakesData.head.delegator.address(), delegator1) + assertTrue(listOfStakesForger1.stakesData.head.stakedAmount.equals(stakeAmount1.add(stakeAmount3))) + + val listOfStakesForger2 = StakeStorage.getPagedForgersStakesByForger(view, ForgerPublicKeys(blockSignerProposition2, vrfPublicKey2), 0, 100) + // check forger2 has 3 delegators with expected stake amount + assertTrue(listOfStakesForger2.stakesData.size == 3) + var count = 0 + listOfStakesForger2.stakesData.foreach(entry => { + if (entry.delegator.address().equals(delegator1)) { + assertTrue(entry.stakedAmount.equals(stakeAmount2)) + count += 1 + } else if (entry.delegator.address().equals(delegator2)) { + assertTrue(entry.stakedAmount.equals(stakeAmount4)) + count += 1 + } else if (entry.delegator.address().equals(delegator3)) { + assertTrue(entry.stakedAmount.equals(stakeAmount5)) + count += 1 + } else { + fail("Unexpected entry") + } + }) + assertEquals(3, count) + + // get the result on two pages + val listOfStakesForger2_page1 = StakeStorage.getPagedForgersStakesByForger(view, ForgerPublicKeys(blockSignerProposition2, vrfPublicKey2), 0, 2) + assertTrue(listOfStakesForger2_page1.stakesData.size == 2) + assertTrue(listOfStakesForger2_page1.nextStartPos == 2) + + val listOfStakesForger2_page2 = StakeStorage.getPagedForgersStakesByForger(view, ForgerPublicKeys(blockSignerProposition2, vrfPublicKey2), 2, 1) + assertTrue(listOfStakesForger2_page2.stakesData.size == 1) + assertTrue(listOfStakesForger2_page2.nextStartPos == -1) + + // check the two pages joint together are the same as before + assertEquals(listOfStakesForger2_page1.stakesData ++ listOfStakesForger2_page2.stakesData, listOfStakesForger2.stakesData) + + // get stakes by delegator + val listOfStakesByDelegator1 = getPagedForgersStakesByDelegator(view, delegator1, 0, 5) + // check we have 2 records, one for each forger + count = 0 + assertTrue(listOfStakesByDelegator1.stakesData.size == 2) + listOfStakesByDelegator1.stakesData.foreach(entry => { + if (entry.forgerPublicKeys.toString.equals(ForgerPublicKeys(blockSignerProposition1, vrfPublicKey1).toString)) { + assertTrue(entry.stakedAmount.equals(stakeAmount1.add(stakeAmount3))) + count += 1 + } else if (entry.forgerPublicKeys.toString.equals(ForgerPublicKeys(blockSignerProposition2, vrfPublicKey2).toString)) { + assertTrue(entry.stakedAmount.equals(stakeAmount2)) + count += 1 + } else { + fail("Unexpected entry") + } + }) + assertEquals(2, count) + + // get the result on two pages + val listOfStakesDelegator1_page1 = getPagedForgersStakesByDelegator(view, delegator1, 0, 1) + assertTrue(listOfStakesDelegator1_page1.stakesData.size == 1) + assertTrue(listOfStakesDelegator1_page1.nextStartPos == 1) + + val listOfStakesDelegator1_page2 = getPagedForgersStakesByDelegator(view, delegator1, 1, 1) + assertTrue(listOfStakesDelegator1_page2.stakesData.size == 1) + assertTrue(listOfStakesDelegator1_page2.nextStartPos == -1) + + // check the two pages joint together are the same as before + assertEquals(listOfStakesDelegator1_page1.stakesData ++ listOfStakesDelegator1_page2.stakesData, listOfStakesByDelegator1.stakesData) + + // remove all the stakes of delegator 1 for forger 1, check we do not have it anymore in the list + StakeStorage.removeStake(view, blockSignerProposition1, vrfPublicKey1, epochNumber, delegator1, stakeAmount1.add(stakeAmount3)) + // get stakes by delegator + val listOfStakesByDelegator1_rem = getPagedForgersStakesByDelegator(view, delegator1, 0, 5) + // check we have 1 record, only forger2 + assertTrue(listOfStakesByDelegator1_rem.stakesData.size == 1) + count = 0 + listOfStakesByDelegator1_rem.stakesData.foreach(entry => { + if (entry.forgerPublicKeys.toString.equals(ForgerPublicKeys(blockSignerProposition2, vrfPublicKey2).toString)) { + assertTrue(entry.stakedAmount.equals(stakeAmount2)) + count += 1 + } else { + fail("Unexpected entry") + } + }) + assertEquals(1, count) + + // negative tests for 'by forger' + // - invalid start pos + var ex = intercept[IllegalArgumentException] { + getPagedForgersStakesByForger(view, ForgerPublicKeys(blockSignerProposition2, vrfPublicKey2), 4, 5) + } + assertTrue(ex.getMessage.contains("Invalid start position")) + + ex = intercept[IllegalArgumentException] { + getPagedForgersStakesByForger(view, ForgerPublicKeys(blockSignerProposition2, vrfPublicKey2), -1, 5) + } + assertTrue(ex.getMessage.contains("Negative start position")) + + // - invalid page size + ex = intercept[IllegalArgumentException] { + getPagedForgersStakesByForger(view, ForgerPublicKeys(blockSignerProposition2, vrfPublicKey2), 0, 0) + } + assertTrue(ex.getMessage.contains("Invalid page size")) + + ex = intercept[IllegalArgumentException] { + getPagedForgersStakesByForger(view, ForgerPublicKeys(blockSignerProposition2, vrfPublicKey2), 0, -1) + } + assertTrue(ex.getMessage.contains("Invalid page size")) + + // - null forger + assertThrows[NullPointerException] { + getPagedForgersStakesByForger(view, null, 0, 2) + } + // we throw an exception on an empty list if we specify bad start pos + ex = intercept[IllegalArgumentException] { + getPagedForgersStakesByForger(view, ForgerPublicKeys(blockSignerProposition2, vrfPublicKey1), 1, 100) + } + assertTrue(ex.getMessage.contains("Invalid start position")) + + // negative tests for 'by delegator' + // - invalid start pos + ex = intercept[IllegalArgumentException] { + getPagedForgersStakesByDelegator(view, delegator1, 4, 5) + } + assertTrue(ex.getMessage.contains("Invalid start position")) + + ex = intercept[IllegalArgumentException] { + getPagedForgersStakesByDelegator(view, delegator1, -1, 5) + } + assertTrue(ex.getMessage.contains("Negative start position")) + + // - invalid page size + ex = intercept[IllegalArgumentException] { + getPagedForgersStakesByDelegator(view, delegator1, 0, 0) + } + assertTrue(ex.getMessage.contains("Invalid page size")) + + ex = intercept[IllegalArgumentException] { + getPagedForgersStakesByDelegator(view, delegator1, 0, -1) + } + assertTrue(ex.getMessage.contains("Invalid page size")) + + // - null address + assertThrows[NullPointerException] { + getPagedForgersStakesByDelegator(view, null, 0, 2) + } + + // we throw an exception on an empty list if we specify bad start pos + ex = intercept[IllegalArgumentException] { + getPagedForgersStakesByDelegator(view, new Address("0x0000000000000000000000000000000000000000"), 1, 100) + } + assertTrue(ex.getMessage.contains("Invalid start position")) + } + } + + @Test + def testGetPagedForgerStakesLoad(): Unit = { + usingView { view => + + createSenderAccount(view, BigInteger.TEN, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS) + + val rewardAddress = new Address(s"0xaabbaabbaabbaabbaabbaabbaabbaabbaabbaabb") + val rewardShare = 0 + val epochNumber = 1000 + + val numOfForgers = 43 + val numOfDelegators = 71 + val stakeAmountBigInteger = BigInteger.valueOf(1234) + var delegator : Address = null + var forger: ForgerPublicKeys = null + val delegatorList : ListBuffer[Address] = ListBuffer() + val forgerList : ListBuffer[ForgerPublicKeys] = ListBuffer() + + (0 until numOfDelegators).foreach( + idx => { + val postfix = f"$idx%03d" + delegator = new Address(s"0xaaa00001230000000000deadbeefaaaa22222$postfix") + delegatorList.append(delegator) + } + ) + + // Activate Forger V2 + StakeStorage.setActive(view) + + (0 until numOfForgers).foreach( + idx_forg => { + val postfix = f"$idx_forg%03d" + + val blockSignerProposition = new PublicKey25519Proposition(BytesUtils.fromHexString(s"1122334455667788112233445566778811223344556677881122334455667$postfix")) // 32 bytes + val vrfPublicKey = new VrfPublicKey(BytesUtils.fromHexString(s"d6b775fd4cefc7446236683fdde9d0464bba43cc565fa066b0b3ed1b888b9d1$postfix")) // 33 bytes + forger = ForgerPublicKeys(blockSignerProposition, vrfPublicKey) + forgerList.append(forger) + + StakeStorage.addForger( + view, forger.blockSignPublicKey, forger.vrfPublicKey, rewardShare, + rewardAddress, epochNumber, delegator, stakeAmountBigInteger) + + + (0 until numOfDelegators).foreach( + idx_del => { + StakeStorage.addStake(view, forger.blockSignPublicKey, forger.vrfPublicKey, epochNumber, + delegatorList(idx_del), stakeAmountBigInteger) + }) + println(s"Added $numOfDelegators delegators to forger $idx_forg") + }) + + + + (0 until numOfForgers).foreach( + + idx => { + println(s"Getting stakes for forger $idx") + val pageSize = 7 + var continue = true + var listOfResultsByForger = Seq.empty[StakeDataDelegator] + var startPos = 0 + + while (continue) { + val result = StakeStorage.getPagedForgersStakesByForger(view, forgerList(idx), startPos, pageSize) + listOfResultsByForger = listOfResultsByForger ++ result.stakesData + continue = if (result.nextStartPos != -1) { + assertEquals(pageSize, result.stakesData.size) + true + } + else + false + startPos = result.nextStartPos + } + assertEquals(numOfDelegators, listOfResultsByForger.size) + }) + + (0 until numOfDelegators).foreach( + idx => { + println(s"Getting stakes by delegator $idx") + + val pageSize = 13 + var continue = true + var listOfResultsByDelegator = Seq.empty[StakeDataForger] + var startPos = 0 + + while (continue) { + val result = StakeStorage.getPagedForgersStakesByDelegator(view, delegatorList(idx), startPos, pageSize) + listOfResultsByDelegator = listOfResultsByDelegator ++ result.stakesData + continue = if (result.nextStartPos != -1) { + assertEquals(pageSize, result.stakesData.size) + true + } + else + false + startPos = result.nextStartPos + } + assertEquals(numOfForgers, listOfResultsByDelegator.size) + }) + + } + } + + @Test + def testUpdateForger(): Unit = { + usingView { view => + + createSenderAccount(view, BigInteger.TEN, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS) + val rewardAddress = new Address("0xaaa0000123000000000011112222aaaa22222222") + val rewardShare = 93 + + // Try to update a non existing forger. It should fail + var ex = intercept[ExecutionRevertedException] { + StakeStorage.updateForger(view, blockSignerProposition1, vrfPublicKey1, rewardShare, rewardAddress) + } + assertEquals("Forger doesn't exist.", ex.getMessage) + + // Try to update a forger that didn't specify rewardAddress and rewardShare during registration. It should work + val epochNumber = 135869 + val stakeAmount = BigInteger.valueOf(20000000000L) + StakeStorage.addForger(view, blockSignerProposition1, vrfPublicKey1, 0, Address.ZERO, epochNumber, delegator1, stakeAmount) + var result = StakeStorage.getPagedListOfForgers(view, 0, 10) + var listOfForgers = result.forgers + assertEquals(1, listOfForgers.size) + assertEquals(blockSignerProposition1, listOfForgers.head.forgerPublicKeys.blockSignPublicKey) + assertEquals(vrfPublicKey1, listOfForgers.head.forgerPublicKeys.vrfPublicKey) + assertEquals(Address.ZERO, listOfForgers.head.rewardAddress.address()) + assertEquals(0, listOfForgers.head.rewardShare) + + // Change the reward address and share + StakeStorage.updateForger(view, blockSignerProposition1, vrfPublicKey1, rewardShare, rewardAddress) + result = StakeStorage.getPagedListOfForgers(view, 0, 10) + listOfForgers = result.forgers + assertEquals(1, listOfForgers.size) + assertEquals(blockSignerProposition1, listOfForgers.head.forgerPublicKeys.blockSignPublicKey) + assertEquals(vrfPublicKey1, listOfForgers.head.forgerPublicKeys.vrfPublicKey) + assertEquals(rewardAddress, listOfForgers.head.rewardAddress.address()) + assertEquals(rewardShare, listOfForgers.head.rewardShare) + + // Try to change again rewardAddress and rewardShare. it should fail. + val rewardAddress2 = new Address("0xaaa0000123000000000011112222aaaa2222aaa2") + val rewardShare2 = 23 + + ex = intercept[ExecutionRevertedException] { + StakeStorage.updateForger(view, blockSignerProposition1, vrfPublicKey1, rewardShare2, rewardAddress2) + } + assertEquals("Forger has already set reward share and reward address.", ex.getMessage) + + // Try to update a forger that didn't specify rewardAddress and rewardShare during registration. It should work + StakeStorage.addForger(view, blockSignerProposition2, vrfPublicKey2, rewardShare2, rewardAddress2, epochNumber, delegator2, stakeAmount) + + // Change the reward address and share + ex = intercept[ExecutionRevertedException] { + StakeStorage.updateForger(view, blockSignerProposition2, vrfPublicKey2, rewardShare, rewardAddress) + } + assertEquals("Forger has already set reward share and reward address.", ex.getMessage) + + + } + } + + @Test + def binarySearchTest(): Unit = { + usingView { view => + createSenderAccount(view, BigInteger.TEN, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS) + val rewardAddress = new Address(s"0xaaa0000123000000000011112222aaaa22222111") + val stakeAmount1 = BigInteger.valueOf(10000000000L) + StakeStorage.addForger(view, blockSignerProposition1, vrfPublicKey1, 1, rewardAddress, 130, delegator1, stakeAmount1) + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, 160, delegator1, stakeAmount1) + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, 190, delegator1, stakeAmount1) + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, 220, delegator1, stakeAmount1) + val history = StakeHistory(ForgerKey(blockSignerProposition1, vrfPublicKey1), DelegatorKey(delegator1)) + /* + 0 -> 130 + 1 -> 160 + 2 -> 190 + 3 -> 220 + */ + assertEquals(0, StakeStorage.checkpointBSearch(view, history, -1)) + assertEquals(0, StakeStorage.checkpointBSearch(view, history, 0)) + assertEquals(0, StakeStorage.checkpointBSearch(view, history, 129)) + assertEquals(0, StakeStorage.checkpointBSearch(view, history, 130)) + assertEquals(0, StakeStorage.checkpointBSearch(view, history, 131)) + assertEquals(0, StakeStorage.checkpointBSearch(view, history, 150)) + assertEquals(0, StakeStorage.checkpointBSearch(view, history, 159)) + assertEquals(1, StakeStorage.checkpointBSearch(view, history, 160)) + assertEquals(1, StakeStorage.checkpointBSearch(view, history, 161)) + assertEquals(1, StakeStorage.checkpointBSearch(view, history, 189)) + assertEquals(2, StakeStorage.checkpointBSearch(view, history, 190)) + assertEquals(2, StakeStorage.checkpointBSearch(view, history, 191)) + assertEquals(2, StakeStorage.checkpointBSearch(view, history, 200)) + assertEquals(2, StakeStorage.checkpointBSearch(view, history, 219)) + assertEquals(3, StakeStorage.checkpointBSearch(view, history, 220)) + assertEquals(3, StakeStorage.checkpointBSearch(view, history, 221)) + assertEquals(3, StakeStorage.checkpointBSearch(view, history, Int.MaxValue)) + + } + } + + @Test + def getForgerStakesPerEpochTest(): Unit = { + usingView { view => + createSenderAccount(view, BigInteger.TEN, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS) + val rewardAddress = new Address(s"0xaaa0000123000000000011112222aaaa22222111") + val stakeAmount1 = BigInteger.valueOf(10000000000L) + StakeStorage.addForger(view, blockSignerProposition1, vrfPublicKey1, 1, rewardAddress, 130, delegator1, stakeAmount1) + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, 160, delegator1, stakeAmount1) + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, 190, delegator1, stakeAmount1) + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, 220, delegator1, stakeAmount1) + val history = StakeHistory(ForgerKey(blockSignerProposition1, vrfPublicKey1), DelegatorKey(delegator1)) + + /* + 0 -> 130 - 10000000000L + 1 -> 160 - 20000000000L + 2 -> 190 - 30000000000L + 3 -> 220 - 40000000000L + */ + + var stakesPerEpoch = StakeStorage.getForgerStakesPerEpoch(view, history, 125, 129) + assertEquals( + Array.fill[BigInteger](5)(BigInteger.ZERO).toSeq, + stakesPerEpoch + ) + + stakesPerEpoch = StakeStorage.getForgerStakesPerEpoch(view, history, 129, 129) + assertEquals( + Array.fill[BigInteger](1)(BigInteger.ZERO).toSeq, + stakesPerEpoch + ) + + stakesPerEpoch = StakeStorage.getForgerStakesPerEpoch(view, history, 130, 130) + assertEquals( + Array.fill[BigInteger](1)(10000000000L).toSeq, + stakesPerEpoch + ) + + stakesPerEpoch = StakeStorage.getForgerStakesPerEpoch(view, history, 300, 300) + assertEquals( + Array.fill[BigInteger](1)(40000000000L).toSeq, //220-300 + stakesPerEpoch + ) + + stakesPerEpoch = StakeStorage.getForgerStakesPerEpoch(view, history, 128, 132) + assertEquals( + Array.fill[BigInteger](2)(BigInteger.ZERO).toSeq ++ //128, 129 + Array.fill[BigInteger](3)(10000000000L).toSeq, //130, 131, 132 + stakesPerEpoch + ) + + stakesPerEpoch = StakeStorage.getForgerStakesPerEpoch(view, history, 100, 200) + assertEquals( + Array.fill[BigInteger](30)(BigInteger.ZERO).toSeq ++ //100-129 + Array.fill[BigInteger](30)(10000000000L).toSeq ++ //130-159 + Array.fill[BigInteger](30)(20000000000L).toSeq ++ //160-189 + Array.fill[BigInteger](11)(30000000000L).toSeq, //190-200 + stakesPerEpoch + ) + + stakesPerEpoch = StakeStorage.getForgerStakesPerEpoch(view, history, 100, 300) + assertEquals( + Array.fill[BigInteger](30)(BigInteger.ZERO).toSeq ++ //100-129 + Array.fill[BigInteger](30)(10000000000L).toSeq ++ //130-159 + Array.fill[BigInteger](30)(20000000000L).toSeq ++ //160-189 + Array.fill[BigInteger](30)(30000000000L).toSeq ++ //190-219 + Array.fill[BigInteger](81)(40000000000L).toSeq, //220-300 + stakesPerEpoch + ) + } + } + + @Test + def getStakeTotalTest(): Unit = { + usingView { view => + createSenderAccount(view, BigInteger.TEN, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS) + val rewardAddress = new Address(s"0xaaa0000123000000000011112222aaaa22222111") + val stakeAmount1 = BigInteger.valueOf(10000000000L) + StakeStorage.addForger(view, blockSignerProposition1, vrfPublicKey1, 1, rewardAddress, 5, delegator1, stakeAmount1) + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, 15, delegator1, stakeAmount1) + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, 25, delegator1, stakeAmount1) + + val rewardAddress2 = new Address(s"0xaaa0000123000000000011112222aaaa22222222") + val stakeAmount2 = BigInteger.valueOf(100000L) + StakeStorage.addForger(view, blockSignerProposition2, vrfPublicKey2, 1, rewardAddress2, 1, delegator2, stakeAmount2) + StakeStorage.addStake(view, blockSignerProposition2, vrfPublicKey2, 10, delegator2, stakeAmount2) + StakeStorage.addStake(view, blockSignerProposition2, vrfPublicKey2, 20, delegator2, stakeAmount2) + + val rewardAddress3 = new Address(s"0xaaa0000123000000000011112222aaaa22222333") + val stakeAmount3 = BigInteger.valueOf(100L) + StakeStorage.addForger(view, blockSignerProposition3, vrfPublicKey3, 1, rewardAddress3, 17, delegator3, stakeAmount3) + StakeStorage.addStake(view, blockSignerProposition3, vrfPublicKey3, 27, delegator3, stakeAmount3) + val history3 = StakeHistory(ForgerKey(blockSignerProposition3, vrfPublicKey3), DelegatorKey(delegator3)) + + /* + 1 -> 100000L + 5 -> 10000000000L + 100000L + 10-> 10000000000L + 200000L + 15-> 20000000000L + 200000L + 17-> 20000000000L + 200000L + 100L + 20-> 20000000000L + 300000L + 100L + 25-> 30000000000L + 300000L + 100L + 27-> 30000000000L + 300000L + 200L + */ + + var stakePerEpoch = StakeStorage.getStakeTotal(view, None, None, 1, 30).listOfStakes + assertEquals( + Array.fill[BigInteger](4)(100000L).toSeq ++ //1 + Array.fill[BigInteger](5)(10000000000L + 100000L).toSeq ++ //5 + Array.fill[BigInteger](5)(10000000000L + 200000L).toSeq ++ //10 + Array.fill[BigInteger](2)(20000000000L + 200000L).toSeq ++ //15 + Array.fill[BigInteger](3)(20000000000L + 200000L + 100L).toSeq ++ //17 + Array.fill[BigInteger](5)(20000000000L + 300000L + 100L).toSeq ++ //20 + Array.fill[BigInteger](2)(30000000000L + 300000L + 100L).toSeq ++ //25 + Array.fill[BigInteger](4)(30000000000L + 300000L + 200L).toSeq //27 + , + stakePerEpoch + ) + + stakePerEpoch = StakeStorage.getStakeTotal(view, Some(ForgerPublicKeys(blockSignerProposition3, vrfPublicKey3)), None, 1, 30).listOfStakes + assertEquals( + Array.fill[BigInteger](16)(0L).toSeq ++ //1 + Array.fill[BigInteger](10)(100L).toSeq ++ //17 + Array.fill[BigInteger](4)(200L).toSeq //27 + , + stakePerEpoch + ) + + stakePerEpoch = StakeStorage.getStakeTotal(view, Some(ForgerPublicKeys(blockSignerProposition3, vrfPublicKey3)), Some(delegator1), 1, 30).listOfStakes + assertEquals( + Array.fill[BigInteger](30)(0L).toSeq //1 + , + stakePerEpoch + ) + } + } + + @Test + def getStakeStartTest() = { + usingView { view => + createSenderAccount(view, BigInteger.TEN, FORGER_STAKE_V2_SMART_CONTRACT_ADDRESS) + val rewardAddress = new Address(s"0xaaa0000123000000000011112222aaaa22222111") + val stakeAmount1 = BigInteger.valueOf(10000000000L) + // should return -1 if delegation does not exist + val doesNotExistsResponse = StakeStorage.getStakeStart(view, ForgerPublicKeys(blockSignerProposition1, vrfPublicKey1), delegator1) + assertEquals(-1, doesNotExistsResponse.epoch) + + StakeStorage.addForger(view, blockSignerProposition1, vrfPublicKey1, 1, rewardAddress, 5, delegator1, stakeAmount1) + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, 15, delegator1, stakeAmount1) + StakeStorage.addStake(view, blockSignerProposition1, vrfPublicKey1, 25, delegator1, stakeAmount1) + val stakeStart = StakeStorage.getStakeStart(view, ForgerPublicKeys(blockSignerProposition1, vrfPublicKey1), delegator1) + assertEquals(5, stakeStart.epoch) + } + } + + def checkStakeHistory(view: BaseAccountStateView, history: BaseStakeHistory, expectedCheckpoints: Seq[StakeCheckpoint]): Unit = { + assertEquals(expectedCheckpoints.size, history.getSize(view)) + expectedCheckpoints.indices.foreach { idx => + assertEquals(expectedCheckpoints(idx), history.getCheckpoint(view, idx)) + } + expectedCheckpoints.lastOption.foreach(checkpoint => assertEquals(checkpoint.stakedAmount, history.getLatestAmount(view))) + } + + +} 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 f4c34b0391..9874d31b23 100644 --- a/sdk/src/test/scala/io/horizen/account/storage/AccountStateMetadataStorageViewTest.scala +++ b/sdk/src/test/scala/io/horizen/account/storage/AccountStateMetadataStorageViewTest.scala @@ -5,7 +5,7 @@ import io.horizen.SidechainTypes import io.horizen.account.proposition.AddressProposition import io.horizen.account.state.receipt.{EthereumReceipt, ReceiptFixture} import io.horizen.account.storage.AccountStateMetadataStorageView.DEFAULT_ACCOUNT_STATE_ROOT -import io.horizen.account.utils.AccountBlockFeeInfo +import io.horizen.account.utils.{AccountBlockFeeInfo, ForgerIdentifier} import io.horizen.block.{WithdrawalEpochCertificate, WithdrawalEpochCertificateFixture} import io.horizen.consensus.{ConsensusEpochNumber, intToConsensusEpochNumber} import io.horizen.fixtures.{SecretFixture, StoreFixture, TransactionFixture} @@ -19,7 +19,6 @@ 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 @@ -113,9 +112,9 @@ class AccountStateMetadataStorageViewTest assertTrue("receipts should not be in storage", stateMetadataStorage.getTransactionReceipt(receipt1.transactionHash).isEmpty) val addressProposition = new AddressProposition(BytesUtils.fromHexString("00000000000000000000000000000000000000aa")) - storageView.updateForgerBlockCounter(addressProposition) - assertTrue("Counter for forger does not exists",storageView.getForgerBlockCounters.contains(addressProposition)) - assertTrue("Counter for forger is incorrect",storageView.getForgerBlockCounters(addressProposition) == 1) + storageView.updateForgerBlockCounter(new ForgerIdentifier(addressProposition)) + assertTrue("Counter for forger does not exists",storageView.getForgerBlockCounters.contains(new ForgerIdentifier(addressProposition))) + assertTrue("Counter for forger is incorrect",storageView.getForgerBlockCounters(new ForgerIdentifier(addressProposition)) == 1) storageView.commit(bytesToVersion(getVersion.data())) diff --git a/sdk/src/test/scala/io/horizen/account/utils/AccountBlockFeeInfoSerializerTest.scala b/sdk/src/test/scala/io/horizen/account/utils/AccountBlockFeeInfoSerializerTest.scala new file mode 100644 index 0000000000..caa657660b --- /dev/null +++ b/sdk/src/test/scala/io/horizen/account/utils/AccountBlockFeeInfoSerializerTest.scala @@ -0,0 +1,45 @@ +package io.horizen.account.utils + +import io.horizen.account.proposition.AddressProposition +import io.horizen.account.state.ForgerPublicKeys +import io.horizen.fixtures.SecretFixture +import io.horizen.proposition.{PublicKey25519Proposition, VrfPublicKey} +import io.horizen.secret.PrivateKey25519Creator +import io.horizen.vrf.VrfGeneratedDataProvider +import org.junit.Assert.assertEquals +import org.junit.Test + +import java.math.BigInteger +import java.nio.charset.StandardCharsets + +class AccountBlockFeeInfoSerializerTest extends SecretFixture { + @Test + def serializeAccountBlockFeeInfo(): Unit = { + val address: AddressProposition = getAddressProposition(123) + val baseFee: BigInteger = BigInteger.valueOf(1234567890L) + val forgerTips: BigInteger = BigInteger.valueOf(1234567890L) + val feeInto: AccountBlockFeeInfo = AccountBlockFeeInfo(baseFee, forgerTips, address) + + val serializedBytes: Array[Byte] = AccountBlockFeeInfoSerializer.toBytes(feeInto) + + val deserializedFeeInto: AccountBlockFeeInfo = AccountBlockFeeInfoSerializer.parseBytes(serializedBytes) + + assertEquals(feeInto, deserializedFeeInto) + } + + @Test + def serializeAccountBlockFeeInfoNewFormat(): Unit = { + val address: AddressProposition = getAddressProposition(123) + val proposition: PublicKey25519Proposition = PrivateKey25519Creator.getInstance().generateSecret("test1".getBytes(StandardCharsets.UTF_8)).publicImage() + val vrfPublicKey: VrfPublicKey = VrfGeneratedDataProvider.getVrfSecretKey(1).publicImage() + val baseFee: BigInteger = BigInteger.valueOf(1234567890L) + val forgerTips: BigInteger = BigInteger.valueOf(1234567890L) + val feeInto: AccountBlockFeeInfo = AccountBlockFeeInfo(baseFee, forgerTips, address, Some(ForgerPublicKeys(proposition, vrfPublicKey))) + + val serializedBytes: Array[Byte] = AccountBlockFeeInfoSerializer.toBytes(feeInto) + + val deserializedFeeInto: AccountBlockFeeInfo = AccountBlockFeeInfoSerializer.parseBytes(serializedBytes) + + assertEquals(feeInto, deserializedFeeInto) + } +} diff --git a/sdk/src/test/scala/io/horizen/account/utils/AccountFeePaymentsUtilsTest.scala b/sdk/src/test/scala/io/horizen/account/utils/AccountFeePaymentsUtilsTest.scala index af53df76bd..468ce08878 100644 --- a/sdk/src/test/scala/io/horizen/account/utils/AccountFeePaymentsUtilsTest.scala +++ b/sdk/src/test/scala/io/horizen/account/utils/AccountFeePaymentsUtilsTest.scala @@ -1,21 +1,25 @@ package io.horizen.account.utils +import io.horizen.account.network.ForgerInfo import io.horizen.account.proposition.AddressProposition -import io.horizen.account.utils.AccountFeePaymentsUtils.getForgersRewards +import io.horizen.account.secret.PrivateKeySecp256k1Creator +import io.horizen.account.state.ForgerPublicKeys +import io.horizen.account.utils.AccountFeePaymentsUtils.{getForgerAndDelegatorShares, getForgersRewards, getMainchainWithdrawalEpochDistributionCap} +import io.horizen.evm.Address import io.horizen.fixtures._ +import io.horizen.params.MainNetParams +import io.horizen.proposition.{PublicKey25519Proposition, VrfPublicKey} import io.horizen.utils.BytesUtils import org.junit.Assert._ import org.junit._ import org.scalatestplus.junit.JUnitSuite import org.scalatestplus.mockito._ -import java.math.BigInteger +import java.math.BigInteger +import java.nio.charset.StandardCharsets +import scala.io.Source -class AccountFeePaymentsUtilsTest - extends JUnitSuite - with SidechainRelatedMainchainOutputFixture - with MockitoSugar -{ +class AccountFeePaymentsUtilsTest extends JUnitSuite with SidechainRelatedMainchainOutputFixture with MockitoSugar { val addr_a: Array[Byte] = BytesUtils.fromHexString("00000000000000000000000000000000000000aa") val addr_b: Array[Byte] = BytesUtils.fromHexString("00000000000000000000000000000000000000bb") val addr_c: Array[Byte] = BytesUtils.fromHexString("00000000000000000000000000000000000000cc") @@ -27,7 +31,7 @@ class AccountFeePaymentsUtilsTest @Test def testNullBlockFeeInfoSeq(): Unit = { - val blockFeeInfoSeq : Seq[AccountBlockFeeInfo] = Seq() + val blockFeeInfoSeq: Seq[AccountBlockFeeInfo] = Seq() val accountPaymentsList = getForgersRewards(blockFeeInfoSeq) assertTrue(accountPaymentsList.isEmpty) } @@ -35,20 +39,14 @@ class AccountFeePaymentsUtilsTest @Test def testHomogeneousBlockFeeInfoSeq(): Unit = { - var blockFeeInfoSeq : Seq[AccountBlockFeeInfo] = Seq() + var blockFeeInfoSeq: Seq[AccountBlockFeeInfo] = Seq() - val abfi_a = AccountBlockFeeInfo( - baseFee = BigInteger.valueOf(100), - forgerTips = BigInteger.valueOf(10), - forgerAddr_a) - val abfi_b = AccountBlockFeeInfo( - baseFee = BigInteger.valueOf(100), - forgerTips = BigInteger.valueOf(10), - forgerAddr_b) - val abfi_c = AccountBlockFeeInfo( - baseFee = BigInteger.valueOf(100), - forgerTips = BigInteger.valueOf(10), - forgerAddr_c) + val abfi_a = + AccountBlockFeeInfo(baseFee = BigInteger.valueOf(100), forgerTips = BigInteger.valueOf(10), forgerAddr_a) + val abfi_b = + AccountBlockFeeInfo(baseFee = BigInteger.valueOf(100), forgerTips = BigInteger.valueOf(10), forgerAddr_b) + val abfi_c = + AccountBlockFeeInfo(baseFee = BigInteger.valueOf(100), forgerTips = BigInteger.valueOf(10), forgerAddr_c) blockFeeInfoSeq = blockFeeInfoSeq :+ abfi_a blockFeeInfoSeq = blockFeeInfoSeq :+ abfi_b @@ -64,7 +62,7 @@ class AccountFeePaymentsUtilsTest @Test def testNotUniqueForgerAddresses(): Unit = { - var blockFeeInfoSeq : Seq[AccountBlockFeeInfo] = Seq() + var blockFeeInfoSeq: Seq[AccountBlockFeeInfo] = Seq() val abfi_a = AccountBlockFeeInfo( baseFee = BigInteger.valueOf(100), @@ -92,21 +90,18 @@ class AccountFeePaymentsUtilsTest val accountPaymentsList = getForgersRewards(blockFeeInfoSeq) assertEquals(accountPaymentsList.length, 3) - accountPaymentsList.foreach( - payment => { - if (payment.address.equals(forgerAddr_c)) - assertEquals(payment.value, BigInteger.valueOf(220)) - else - assertEquals(payment.value, BigInteger.valueOf(110)) - } - ) + accountPaymentsList.foreach(payment => { + if (payment.identifier.getAddress.equals(forgerAddr_c)) + assertEquals(payment.value, BigInteger.valueOf(220)) + else + assertEquals(payment.value, BigInteger.valueOf(110)) + }) } - @Test def testPoolWithRemainder(): Unit = { - var blockFeeInfoSeq : Seq[AccountBlockFeeInfo] = Seq() + var blockFeeInfoSeq: Seq[AccountBlockFeeInfo] = Seq() val abfi_a = AccountBlockFeeInfo( baseFee = BigInteger.valueOf(3), @@ -141,24 +136,22 @@ class AccountFeePaymentsUtilsTest val accountPaymentsList = getForgersRewards(blockFeeInfoSeq) assertEquals(accountPaymentsList.length, 3) - accountPaymentsList.foreach( - payment => { - if (payment.address.equals(forgerAddr_c)) - // last address is repeated, its reward are summed - // (forgerTip (4) + poolFee quota (3)) + (forgerTip (6) + poolFee quota (3)) - assertEquals(payment.value, BigInteger.valueOf((4 + 3) + (6 + 3))) - else { - // first 2 addresses have 1 satoshi more due to the remainder: - // forgerTip (10) + poolFee quota (3) + remainder quota (1) - assertEquals(payment.value, BigInteger.valueOf(10 + 3 + 1)) - } + accountPaymentsList.foreach(payment => { + if (payment.identifier.getAddress.equals(forgerAddr_c)) + // last address is repeated, its reward are summed + // (forgerTip (4) + poolFee quota (3)) + (forgerTip (6) + poolFee quota (3)) + assertEquals(payment.value, BigInteger.valueOf((4 + 3) + (6 + 3))) + else { + // first 2 addresses have 1 satoshi more due to the remainder: + // forgerTip (10) + poolFee quota (3) + remainder quota (1) + assertEquals(payment.value, BigInteger.valueOf(10 + 3 + 1)) } - ) + }) } @Test def testWithMcForgerPoolRewards(): Unit = { - var blockFeeInfoSeq : Seq[AccountBlockFeeInfo] = Seq() + var blockFeeInfoSeq: Seq[AccountBlockFeeInfo] = Seq() val abfi_a = AccountBlockFeeInfo( baseFee = BigInteger.valueOf(100), @@ -178,14 +171,12 @@ class AccountFeePaymentsUtilsTest forgerAddr_c) val mcForgerPoolRewards = Map( - forgerAddr_a -> BigInteger.valueOf(10), - forgerAddr_b -> BigInteger.valueOf(10), - forgerAddr_c -> BigInteger.valueOf(10), - forgerAddr_d -> BigInteger.valueOf(10), - + new ForgerIdentifier(forgerAddr_a) -> BigInteger.valueOf(10), + new ForgerIdentifier(forgerAddr_b) -> BigInteger.valueOf(10), + new ForgerIdentifier(forgerAddr_c) -> BigInteger.valueOf(10), + new ForgerIdentifier(forgerAddr_d) -> BigInteger.valueOf(10), ) - blockFeeInfoSeq = blockFeeInfoSeq :+ abfi_a blockFeeInfoSeq = blockFeeInfoSeq :+ abfi_b blockFeeInfoSeq = blockFeeInfoSeq :+ abfi_c1 @@ -194,15 +185,125 @@ class AccountFeePaymentsUtilsTest val accountPaymentsList = getForgersRewards(blockFeeInfoSeq, mcForgerPoolRewards) assertEquals(accountPaymentsList.length, 4) - accountPaymentsList.foreach( - payment => { - if (payment.address.equals(forgerAddr_c)) - assertEquals(BigInteger.valueOf(230), payment.value) - else if (payment.address.equals(forgerAddr_d)) - assertEquals(BigInteger.valueOf(10), payment.value) - else - assertEquals(BigInteger.valueOf(120), payment.value) + accountPaymentsList.foreach(payment => { + if (payment.identifier.getAddress.equals(forgerAddr_c)) + assertEquals(BigInteger.valueOf(230), payment.value) + else if (payment.identifier.getAddress.equals(forgerAddr_d)) + assertEquals(BigInteger.valueOf(10), payment.value) + else + assertEquals(BigInteger.valueOf(120), payment.value) + }) + } + + @Test + def getMainchainWithdrawalEpochDistributionCapTest(): Unit = { + val params = MainNetParams() + val baseReward = 1250000000L + val rewardAfterFirstHalving = baseReward / 2 + val rewardAfterSecondHalving = rewardAfterFirstHalving / 2 + val divider = 10 + + // test 1 - before first halving + var actual: BigInteger = getMainchainWithdrawalEpochDistributionCap(500, params) + var expected: BigInteger = ZenWeiConverter.convertZenniesToWei(baseReward * params.withdrawalEpochLength / divider) + assertEquals(expected, actual) + // test 2 - at first halving + actual = getMainchainWithdrawalEpochDistributionCap(840010, params) + expected = ZenWeiConverter.convertZenniesToWei((baseReward * (params.withdrawalEpochLength - 10) / divider) + (rewardAfterFirstHalving * 10 / divider)) + assertEquals(expected, actual) + // test 3 - after first halving + actual = getMainchainWithdrawalEpochDistributionCap(1000010, params) + expected = ZenWeiConverter.convertZenniesToWei(rewardAfterFirstHalving * params.withdrawalEpochLength / divider) + assertEquals(expected, actual) + // test 4 - at second halving + actual = getMainchainWithdrawalEpochDistributionCap(1680010, params) + expected = ZenWeiConverter.convertZenniesToWei((rewardAfterFirstHalving * (params.withdrawalEpochLength - 10) / divider) + (rewardAfterSecondHalving * 10 / divider)) + assertEquals(expected, actual) + } + + @Test + def getForgerAndDelegatorSharesTest(): Unit = { + val blockSignerProposition = new PublicKey25519Proposition(BytesUtils.fromHexString("1122334455667788112233445566778811223344556677881122334455667788")) // 32 bytes + val vrfPublicKey = new VrfPublicKey(BytesUtils.fromHexString("d6b775fd4cefc7446236683fdde9d0464bba43cc565fa066b0b3ed1b888b9d1180")) // 33 bytes + val forgerPublicKeys = ForgerPublicKeys(blockSignerProposition, vrfPublicKey) + val rewardAddress: AddressProposition = PrivateKeySecp256k1Creator.getInstance().generateSecret("nativemsgprocessortest1".getBytes(StandardCharsets.UTF_8)).publicImage() + + val feePayment = ForgerPayment(new ForgerIdentifier(forgerAddr_a), BigInteger.valueOf(100), BigInteger.valueOf(10)) + val forgerInfo = ForgerInfo(forgerPublicKeys, 100, rewardAddress) + + val (forgerPayment, Some(delegatorPayment)) = getForgerAndDelegatorShares(feePayment, forgerInfo) + + assertEquals(forgerPayment.address, forgerAddr_a) + assertEquals(forgerPayment.value, BigInteger.valueOf(90)) + assertEquals(forgerPayment.valueFromMainchain.get, BigInteger.valueOf(9)) + assertEquals(forgerPayment.valueFromFees.get, BigInteger.valueOf(81)) + + assertEquals(delegatorPayment.feePayment.address, rewardAddress) + assertEquals(delegatorPayment.feePayment.value, BigInteger.valueOf(10)) + assertEquals(delegatorPayment.feePayment.valueFromMainchain.get, BigInteger.valueOf(1)) + assertEquals(delegatorPayment.feePayment.valueFromFees.get, BigInteger.valueOf(9)) + assertEquals(delegatorPayment.forgerKeys, forgerPublicKeys) + } + + @Test + def testForgerRewardsOrdering_CornerCaseFromMainnet(): Unit = { + val feeInfo = getMainnetExampleBlockFeeInfo + val mcRewards = getMainnetExampleMcRewards + + val rewards = AccountFeePaymentsUtils.getForgersRewards(feeInfo, mcRewards) + .map(fp => AccountPayment(fp.identifier.getAddress, fp.value)) + + assertEquals( + "47f5f036f2b6ec8224508af3c1a33f36662ccf305c46104fb60c071ee3774dbd", + BytesUtils.toHexString(AccountFeePaymentsUtils.calculateFeePaymentsHash(rewards)) + ) + } + + private def getMainnetExampleMcRewards: Map[ForgerIdentifier, BigInteger] = { + Map( + new ForgerIdentifier(new AddressProposition(new Address("0x90826921d1d4aee8e6b5ae296f80b4145eb434df"))) -> new BigInteger("1294588159144"), + new ForgerIdentifier(new AddressProposition(new Address("0x4bae5d28b45b88e1901eb691b5f71f6eadcd8b9f"))) -> new BigInteger("2589176318288"), + new ForgerIdentifier(new AddressProposition(new Address("0x9c98454c8f4d2d38ad824b407be5448cf0fe7b0a"))) -> new BigInteger("3328940980656"), + new ForgerIdentifier(new AddressProposition(new Address("0x6f47d5bb9c4e1f2ed25d442c1a45e43e197e8fbe"))) -> new BigInteger("462352913980"), + new ForgerIdentifier(new AddressProposition(new Address("0x99d270f4a42b296fb888f168a5985e1d9839b064"))) -> new BigInteger("638925491828962"), + new ForgerIdentifier(new AddressProposition(new Address("0xfac3dc3dc9b2562d8de5d30c00ad265210cd3d7a"))) -> new BigInteger("15350116744136"), + new ForgerIdentifier(new AddressProposition(new Address("0xc9c8dd62a78c2cb9423d872d118b986c33ff7e3c"))) -> new BigInteger("629817139423556"), + new ForgerIdentifier(new AddressProposition(new Address("0x54ac4a5c11b7e6ddeabbb99f69bdb59820a3607a"))) -> new BigInteger("645814550247264"), + new ForgerIdentifier(new AddressProposition(new Address("0x1448283357e8fb6ea763a78836ffd5517149bf70"))) -> new BigInteger("6657881961312"), + new ForgerIdentifier(new AddressProposition(new Address("0x0eef14a2db10cba19c3e13a4090f0dd3c669e459"))) -> new BigInteger("638185727166594"), + new ForgerIdentifier(new AddressProposition(new Address("0x3f60469c1950a9b8b4f190a3168b59e354a2be6f"))) -> new BigInteger("3051529232268"), + new ForgerIdentifier(new AddressProposition(new Address("0x7aaac8a2be835d9b9261018c68dba7166e775096"))) -> new BigInteger("2820352775278"), + new ForgerIdentifier(new AddressProposition(new Address("0x0b3ee4a24ecf65bb4005219f873d915cbaac1b28"))) -> new BigInteger("593661141550320"), + new ForgerIdentifier(new AddressProposition(new Address("0x6b5b8861f260457bb91ba604e8856d6ad7eb17a0"))) -> new BigInteger("924705827960"), + new ForgerIdentifier(new AddressProposition(new Address("0x12ed8d94159083a64f97d382538b0881bd72429e"))) -> new BigInteger("92470582796"), + new ForgerIdentifier(new AddressProposition(new Address("0xb545a82e49f9c595601c713765a05aea7590b2ed"))) -> new BigInteger("277411748388"), + new ForgerIdentifier(new AddressProposition(new Address("0x8b4c5f6dfe440497fca3c13b8ab449b7d021682b"))) -> new BigInteger("648311255982756"), + new ForgerIdentifier(new AddressProposition(new Address("0x6aa2ee3a3fa290ef0dc4900f7e19f26bcadfed74"))) -> new BigInteger("584414083270720"), + new ForgerIdentifier(new AddressProposition(new Address("0xba2290aeaae3e1ea336431911c97a67ebff46528"))) -> new BigInteger("33011998058172"), + new ForgerIdentifier(new AddressProposition(new Address("0x28a48c183df1e30f64673cb4c84d7fd7df4ad506"))) -> new BigInteger("3077698407182838"), + new ForgerIdentifier(new AddressProposition(new Address("0x85f79ba831a8b1716eff9726f2be54e079e75c62"))) -> new BigInteger("1941882238716"), + new ForgerIdentifier(new AddressProposition(new Address("0x62b1bc6fd237b775138d910274ff2911d7aea5cc"))) -> new BigInteger("624453845621388"), + new ForgerIdentifier(new AddressProposition(new Address("0x19f78fca9a4ee0dd795bf9a8277aee241bb972db"))) -> new BigInteger("602584552790134"), + new ForgerIdentifier(new AddressProposition(new Address("0x8eb44f8b1c03d6d194f3ace68e1e0a4a696d44d9"))) -> new BigInteger("646276903161244"), + new ForgerIdentifier(new AddressProposition(new Address("0xac5722e85196c0ca9b7c0c00ec8f7ccf7b4d913c"))) -> new BigInteger("596157847285812"), + new ForgerIdentifier(new AddressProposition(new Address("0x0afbde33475321e870c55be95a3e6283b0385f80"))) -> new BigInteger("1895646947318"), + ) + } + + private def getMainnetExampleBlockFeeInfo: Seq[AccountBlockFeeInfo] = { + var blockFeeInfoSeq : Seq[AccountBlockFeeInfo] = Seq() + val source = Source.fromURL(getClass.getResource("/block_fee_info_seq.dsv")) + source.getLines().foreach( + line => { + val parts = line.split(" ") + val baseFee = new BigInteger(parts(0)) + val forgerTips = new BigInteger(parts(1)) + val forgerAddress = new AddressProposition(new Address(parts(2))) + blockFeeInfoSeq = blockFeeInfoSeq :+ AccountBlockFeeInfo(baseFee, forgerTips, forgerAddress) } ) + source.close() + blockFeeInfoSeq } + } 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 bda2ebde15..2642d4df11 100644 --- a/sdk/src/test/scala/io/horizen/account/utils/AccountMockDataHelper.scala +++ b/sdk/src/test/scala/io/horizen/account/utils/AccountMockDataHelper.scala @@ -11,7 +11,7 @@ import io.horizen.account.proposition.AddressProposition import io.horizen.account.secret.PrivateKeySecp256k1 import io.horizen.account.state._ import io.horizen.account.state.receipt.EthereumReceipt -import io.horizen.account.storage.AccountStateMetadataStorageView +import io.horizen.account.storage.{AccountStateMetadataStorageView, MsgProcessorMetadataStorageReader} import io.horizen.account.transaction.EthereumTransaction import io.horizen.account.wallet.AccountWallet import io.horizen.block.SidechainBlockBase.GENESIS_BLOCK_PARENT_ID @@ -380,6 +380,8 @@ case class AccountMockDataHelper(genesis: Boolean) msgProcessors.find(_.isInstanceOf[WithdrawalRequestProvider]).get.asInstanceOf[WithdrawalRequestProvider] override lazy val forgerStakesProvider: ForgerStakesProvider = msgProcessors.find(_.isInstanceOf[ForgerStakesProvider]).get.asInstanceOf[ForgerStakesProvider] + override lazy val forgerStakesV2Provider: ForgerStakesV2Provider = + msgProcessors.find(_.isInstanceOf[ForgerStakesV2Provider]).get.asInstanceOf[ForgerStakesV2Provider] override def getProof(address: Address, keys: Array[Array[Byte]], stateRoot: Hash): ProofAccountResult = { new ProofAccountResult( @@ -455,7 +457,7 @@ case class AccountMockDataHelper(genesis: Boolean) .when(mockMsgProcessor.canProcess(any[Invocation], any[BaseAccountStateView], any[Int])) .thenReturn(true) Mockito - .when(mockMsgProcessor.process(any[Invocation], any[BaseAccountStateView], any[ExecutionContext])) + .when(mockMsgProcessor.process(any[Invocation], any[BaseAccountStateView], any[MsgProcessorMetadataStorageReader], any[ExecutionContext])) .thenReturn(Array.empty[Byte]) mockMsgProcessor } diff --git a/sdk/src/test/scala/io/horizen/account/utils/AccountPaymentSerializerTest.scala b/sdk/src/test/scala/io/horizen/account/utils/AccountPaymentSerializerTest.scala index cd401c8083..de1ddcd6a3 100644 --- a/sdk/src/test/scala/io/horizen/account/utils/AccountPaymentSerializerTest.scala +++ b/sdk/src/test/scala/io/horizen/account/utils/AccountPaymentSerializerTest.scala @@ -20,4 +20,19 @@ class AccountPaymentSerializerTest extends SecretFixture { assertEquals(accountPayment, deserializedAccountPayment) } + + @Test + def serializeAccountPaymentNewFormat(): Unit = { + val address: AddressProposition = getAddressProposition(123) + val value: BigInteger = BigInteger.valueOf(1234567890L) + val valueFromMainchain: BigInteger = BigInteger.valueOf(2222222222L) + val valueFromFees: BigInteger = BigInteger.valueOf(5555555555L) + val accountPayment: AccountPayment = AccountPayment(address, value, Some(valueFromMainchain), Some(valueFromFees)) + + val serializedBytes: Array[Byte] = AccountPaymentSerializer.toBytes(accountPayment) + + val deserializedAccountPayment: AccountPayment = AccountPaymentSerializer.parseBytes(serializedBytes) + + assertEquals(accountPayment, deserializedAccountPayment) + } } diff --git a/sdk/src/test/scala/io/horizen/fixtures/sidechainblock/generation/SidechainBlocksGenerator.scala b/sdk/src/test/scala/io/horizen/fixtures/sidechainblock/generation/SidechainBlocksGenerator.scala index 065f4bf1d0..564c1ff3f9 100644 --- a/sdk/src/test/scala/io/horizen/fixtures/sidechainblock/generation/SidechainBlocksGenerator.scala +++ b/sdk/src/test/scala/io/horizen/fixtures/sidechainblock/generation/SidechainBlocksGenerator.scala @@ -551,6 +551,7 @@ object SidechainBlocksGenerator extends CompanionsFixture { override val isNonCeasing: Boolean = params.isNonCeasing override val minVirtualWithdrawalEpochLength: Int = 10 override val mcBlockRefDelay : Int = 0 + override val mcHalvingInterval: Int = 840000 } } diff --git a/sdk/src/test/scala/io/horizen/forger/ForgerGenerationRateTest.scala b/sdk/src/test/scala/io/horizen/forger/ForgerGenerationRateTest.scala index f0257d02d7..09a406fee2 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("Unexpected slot filled percentage ("+slotFilledPercentage+" is not between 3.7 and 6.3)", slotFilledPercentage <= 6.3 && slotFilledPercentage >= 3.7) + assertTrue("Unexpected slot filled percentage ("+slotFilledPercentage+" is not between 3.7 and 6.3)", slotFilledPercentage <= 6.3 && slotFilledPercentage >= 3.69) } @Ignore diff --git a/sdk/src/test/scala/io/horizen/history/validation/ConsensusValidatorTest.scala b/sdk/src/test/scala/io/horizen/history/validation/ConsensusValidatorTest.scala index 9274da8dd1..5b7ae2ef4c 100644 --- a/sdk/src/test/scala/io/horizen/history/validation/ConsensusValidatorTest.scala +++ b/sdk/src/test/scala/io/horizen/history/validation/ConsensusValidatorTest.scala @@ -188,7 +188,7 @@ class ConsensusValidatorTest extends JUnitSuite with HistoryConsensusChecker { println("Test blockWithNotEnoughStake") val blockWithNotEnoughStake = generateBlockWithNotEnoughStake(lastGenerator) history.append(blockWithNotEnoughStake).failed.get match { - case expected: IllegalArgumentException => assert(expected.getMessage == s"Stake value in forger box in block ${blockWithNotEnoughStake.id} is not enough for to be forger.") + case expected: IllegalArgumentException => assert(expected.getMessage == s"Forging stake value in block ${blockWithNotEnoughStake.id} is not enough for to be forger.") case nonExpected => assert(false, s"Got incorrect exception: ${nonExpected}") } } diff --git a/sdk/src/test/scala/io/horizen/utils/TimeToEpochUtilsTest.scala b/sdk/src/test/scala/io/horizen/utils/TimeToEpochUtilsTest.scala index dc8c23a4e5..7d20269555 100644 --- a/sdk/src/test/scala/io/horizen/utils/TimeToEpochUtilsTest.scala +++ b/sdk/src/test/scala/io/horizen/utils/TimeToEpochUtilsTest.scala @@ -52,6 +52,7 @@ class TimeToEpochUtilsTest extends JUnitSuite { override val isNonCeasing: Boolean = false override val minVirtualWithdrawalEpochLength: Int = 10 override val mcBlockRefDelay : Int = 0 + override val mcHalvingInterval: Int = 840000 } val defaultConsensusFork = ConsensusParamsFork.DefaultConsensusParamsFork diff --git a/sdk/src/test/scala/io/horizen/utxo/SidechainNodeViewHolderTest.scala b/sdk/src/test/scala/io/horizen/utxo/SidechainNodeViewHolderTest.scala index 042d9ce3ef..8e7d6940a8 100644 --- a/sdk/src/test/scala/io/horizen/utxo/SidechainNodeViewHolderTest.scala +++ b/sdk/src/test/scala/io/horizen/utxo/SidechainNodeViewHolderTest.scala @@ -9,6 +9,7 @@ import io.horizen.consensus.{ConsensusEpochInfo, FullConsensusEpochInfo, intToCo import io.horizen.fixtures._ import io.horizen.params.{NetworkParams, RegTestParams} import io.horizen.SidechainTypes +import io.horizen.metrics.MetricsManager import io.horizen.utils.{CountDownLatchController, MerkleTree, WithdrawalEpochInfo} import io.horizen.utxo.block.SidechainBlock import io.horizen.utxo.box.ZenBox @@ -24,9 +25,11 @@ import org.junit.{Before, Test} import org.mockito.Mockito.times import org.mockito.{ArgumentMatchers, Mockito} import org.scalatestplus.junit.JUnitSuite +import org.scalatestplus.mockito.MockitoSugar.mock import sparkz.core.NodeViewHolder.ReceivableMessages.{LocallyGeneratedModifier, ModifiersFromRemote} import sparkz.core.consensus.History.ProgressInfo import sparkz.core.network.NodeViewSynchronizer.ReceivableMessages.{ModifiersProcessingResult, SemanticallySuccessfulModifier} +import sparkz.core.utils.NetworkTimeProvider import sparkz.core.validation.RecoverableModifierError import sparkz.core.{VersionTag, idToVersion} import sparkz.util.{ModifierId, SparkzEncoding} @@ -62,6 +65,7 @@ class SidechainNodeViewHolderTest extends JUnitSuite @Before def setUp(): Unit = { + MetricsManager.init(mock[NetworkTimeProvider]) history = mock[SidechainHistory] state = mock[SidechainState] wallet = mock[SidechainWallet] @@ -485,8 +489,8 @@ class SidechainNodeViewHolderTest extends JUnitSuite val block5 = generateNextSidechainBlock(block4, sidechainTransactionsCompanion, params) val block6 = generateNextSidechainBlock(block5, sidechainTransactionsCompanion, params) - val firstRequestBlocks = Seq(block1, block2, block6) - val secondRequestBlocks = Seq(block3, block4, block5) + val firstRequestBlocks = Seq(block3, block2, block6) + val secondRequestBlocks = Seq(block1, block4, block5) val correctSequence = Array(block1, block2, block3, block4, block5, block6) var blockIndex = 0 @@ -520,7 +524,7 @@ class SidechainNodeViewHolderTest extends JUnitSuite case m => m match { case ModifiersProcessingResult(applied, cleared) => { - assertTrue("Applied block sequence is differ", applied.toSet.equals(correctSequence.toSet)) + assertEquals("Applied block sequence is differ", correctSequence.toSet, applied.toSet) assertTrue("Cleared block sequence is not empty.", cleared.isEmpty) true } diff --git a/sdk/src/test/scala/io/horizen/utxo/fixtures/SidechainNodeViewHolderFixture.scala b/sdk/src/test/scala/io/horizen/utxo/fixtures/SidechainNodeViewHolderFixture.scala index 9e35fb2f2d..49d95c9100 100644 --- a/sdk/src/test/scala/io/horizen/utxo/fixtures/SidechainNodeViewHolderFixture.scala +++ b/sdk/src/test/scala/io/horizen/utxo/fixtures/SidechainNodeViewHolderFixture.scala @@ -11,6 +11,7 @@ import io.horizen.cryptolibprovider.CircuitTypes import io.horizen.customconfig.CustomAkkaConfiguration import io.horizen.fixtures.{CompanionsFixture, StoreFixture} import io.horizen.fork.{ForkManagerUtil, SimpleForkConfigurator} +import io.horizen.metrics.MetricsManager import io.horizen.params.{MainNetParams, NetworkParams, RegTestParams, TestNetParams} import io.horizen.secret.SecretSerializer import io.horizen.storage.SidechainSecretStorage @@ -42,7 +43,6 @@ trait SidechainNodeViewHolderFixture val simpleForkConfigurator = new SimpleForkConfigurator ForkManagerUtil.initializeForkManager(simpleForkConfigurator, "regtest") - implicit def exceptionHandler: ExceptionHandler = SidechainApiErrorHandler.exceptionHandler implicit def rejectionHandler: RejectionHandler = ApiRejectionHandler.rejectionHandler @@ -52,6 +52,7 @@ trait SidechainNodeViewHolderFixture implicit val materializer: ActorMaterializer = ActorMaterializer() val timeProvider = new NetworkTimeProvider(sidechainSettings.sparkzSettings.ntp) + MetricsManager.init(timeProvider) val sidechainBoxesCompanion: SidechainBoxesCompanion = SidechainBoxesCompanion(new JHashMap[JByte, BoxSerializer[SidechainTypes#SCB]]()) val sidechainSecretsCompanion: SidechainSecretsCompanion = SidechainSecretsCompanion(new JHashMap[JByte, SecretSerializer[SidechainTypes#SCS]]()) diff --git a/sdk/src/test/scala/io/horizen/utxo/forge/ForgerTest.scala b/sdk/src/test/scala/io/horizen/utxo/forge/ForgerTest.scala index 6fb7c9c7bd..4276385f75 100644 --- a/sdk/src/test/scala/io/horizen/utxo/forge/ForgerTest.scala +++ b/sdk/src/test/scala/io/horizen/utxo/forge/ForgerTest.scala @@ -7,6 +7,7 @@ import io.horizen.utxo.companion.SidechainTransactionsCompanion import io.horizen.forge.AbstractForger.ReceivableMessages.StartForging import io.horizen.forge.MainchainSynchronizer import io.horizen.fork.{ConsensusParamsFork, ConsensusParamsForkInfo, CustomForkConfiguratorWithConsensusParamsFork, ForkManagerUtil} +import io.horizen.metrics.MetricsManager import io.horizen.params.NetworkParams import io.horizen.utils.TimeToEpochUtils import io.horizen.utxo.block.SidechainBlock @@ -25,7 +26,7 @@ import scala.concurrent.duration.DurationInt class ForgerTest extends JUnitSuite with Matchers { implicit val system: ActorSystem = ActorSystem() - + MetricsManager.init(mock[NetworkTimeProvider]) @Test def testForgingScheduleAtTheBeginningOfNewSlot(): Unit = { /* diff --git a/tools/dbtool/pom.xml b/tools/dbtool/pom.xml index 68f4851651..34ad305db3 100644 --- a/tools/dbtool/pom.xml +++ b/tools/dbtool/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.horizen sidechains-sdk-dbtools - 0.11.0 + 0.12.0 2022 UTF-8 @@ -15,7 +15,7 @@ io.horizen sidechains-sdk - 0.11.0 + 0.12.0 junit diff --git a/tools/sctool/pom.xml b/tools/sctool/pom.xml index efb0fd6087..35b13e8dc2 100644 --- a/tools/sctool/pom.xml +++ b/tools/sctool/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.horizen sidechains-sdk-scbootstrappingtools - 0.11.0 + 0.12.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.11.0 + 0.12.0 compile diff --git a/tools/sctool/src/main/java/io/horizen/ScBootstrappingToolCommandProcessor.java b/tools/sctool/src/main/java/io/horizen/ScBootstrappingToolCommandProcessor.java index d34e9f5d8e..cb6c592011 100644 --- a/tools/sctool/src/main/java/io/horizen/ScBootstrappingToolCommandProcessor.java +++ b/tools/sctool/src/main/java/io/horizen/ScBootstrappingToolCommandProcessor.java @@ -937,11 +937,11 @@ private NetworkParams getNetworkParams(byte network, byte[] scId, boolean isNewC switch(network) { case 0: // mainnet - return new MainNetParams(scId, null, null, null, null, 1, 0,100, null, null, circuitType,0, null, null, null, null, null, null, null, false, null, null, 11111111,true, false, true, 0, false, Option.empty()); + return new MainNetParams(scId, null, null, null, null, 1, 0,100, null, null, circuitType,0, null, null, null, null, null, null, null, false, null, null, 11111111,true, false, true, 0, 840000, false, Option.empty()); case 1: // testnet - return new TestNetParams(scId, null, null, null, null, 1, 0, 100, null, null, circuitType, 0, null, null, null, null, null, null, null, false, null, null, 11111111,true, false, true, 0, false, Option.empty()); + return new TestNetParams(scId, null, null, null, null, 1, 0, 100, null, null, circuitType, 0, null, null, null, null, null, null, null, false, null, null, 11111111,true, false, true, 0, 840000, false, Option.empty()); case 2: // regtest - return new RegTestParams(scId, null, null, null, null, 1, 0, 100, null, null, circuitType, 0, null, null, null, null, null, null, null, false, null, null, 11111111,true, false, true, 0, false, 0, Option.empty()); + return new RegTestParams(scId, null, null, null, null, 1, 0, 100, null, null, circuitType, 0, null, null, null, null, null, null, null, false, null, null, 11111111,true, false, true, 0, 2000, false, 0, Option.empty()); default: throw new IllegalStateException("Unexpected network type: " + network); diff --git a/tools/sidechains-sdk-account_sctools/pom.xml b/tools/sidechains-sdk-account_sctools/pom.xml index f675f43a20..9b133a6a1d 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.11.0 + 0.12.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.11.0 + 0.12.0 compile diff --git a/tools/sidechains-sdk-utxo_sctools/pom.xml b/tools/sidechains-sdk-utxo_sctools/pom.xml index 74d1495a95..38f311fe4a 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.11.0 + 0.12.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.11.0 + 0.12.0 compile diff --git a/tools/signingtool/pom.xml b/tools/signingtool/pom.xml index d29d994bf7..04b1cb0090 100644 --- a/tools/signingtool/pom.xml +++ b/tools/signingtool/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.horizen sidechains-sdk-signingtools - 0.11.0 + 0.12.0 2022 UTF-8 @@ -15,7 +15,7 @@ io.horizen sidechains-sdk - 0.11.0 + 0.12.0 junit