diff --git a/.circleci/config.yml b/.circleci/config.yml index 092a0a7f6b..398b83fed2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,79 +1,14 @@ version: 2.1 -orbs: - go: circleci/go@1.5.0 - -jobs: - # This job builds the hive executable and stores it in the workspace. - build: - docker: - - image: cimg/go:1.21 - steps: - # Build it. - - checkout - - go/load-cache - - go/mod-download - - go/save-cache - - run: {command: 'go build -ldflags="-s -extldflags=-static" -tags "osusergo netgo static_build" .'} - # Store the executable. - - persist_to_workspace: - root: . - paths: ["hive"] - # This job runs the smoke test simulations. This requires a virtual - # machine instead of the container-based build environment because - # hive needs to be able to talk to the docker containers it creates. - smoke-tests: - machine: - image: ubuntu-2004:202201-02 - steps: - - checkout - - attach_workspace: {at: "/tmp/build"} - - run: - command: "/tmp/build/hive --sim=smoke/genesis --client=go-ethereum" - - run: - command: "/tmp/build/hive --sim=smoke/network --client=go-ethereum" +setup: true - # This job also runs the smoke test simulations, but against a remote dockerd. - smoke-tests-remote-docker: - docker: - - image: cimg/base:2022.04 - steps: - - checkout - - attach_workspace: {at: "/tmp/build"} - - setup_remote_docker: {version: 20.10.14} - - run: - command: "/tmp/build/hive --sim=smoke/genesis --client=go-ethereum --loglevel 5" - - run: - command: "/tmp/build/hive --sim=smoke/network --client=go-ethereum --loglevel 5" - - # This job runs the go unit tests. - go-test: - docker: - - image: cimg/go:1.21 - steps: - # Get the source. - - checkout - - go/load-cache - - go/mod-download - - go/save-cache - # Run the tests. - - run: - name: "hive module tests" - command: "go test -cover ./..." - - run: - name: "hiveproxy module tests" - command: "go test -cover ./..." - working_directory: "./hiveproxy" - - run: - name: "Compile Go simulators" - command: ".circleci/compile-simulators.sh" +orbs: + path-filtering: circleci/path-filtering@0.0.1 workflows: - main: + setup-workflow: jobs: - - go-test - - build - - smoke-tests: - requires: ["build"] - - smoke-tests-remote-docker: - requires: ["build"] + - path-filtering/filter: + mapping: | + simulators/portal/.* rust-ci true + base-revision: origin/master diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml new file mode 100644 index 0000000000..992ff04cdd --- /dev/null +++ b/.circleci/continue_config.yml @@ -0,0 +1,106 @@ +version: 2.1 +orbs: + go: circleci/go@1.5.0 + +parameters: + rust-ci: + type: boolean + default: false + +jobs: + # This job builds the hive executable and stores it in the workspace. + build: + docker: + - image: cimg/go:1.21 + steps: + # Build it. + - checkout + - go/load-cache + - go/mod-download + - go/save-cache + - run: {command: 'go build -ldflags="-s -extldflags=-static" -tags "osusergo netgo static_build" .'} + # Store the executable. + - persist_to_workspace: + root: . + paths: ["hive"] + + # This job runs the smoke test simulations. This requires a virtual + # machine instead of the container-based build environment because + # hive needs to be able to talk to the docker containers it creates. + smoke-tests: + machine: + image: ubuntu-2004:202201-02 + steps: + - checkout + - attach_workspace: {at: "/tmp/build"} + - run: + command: "/tmp/build/hive --sim=smoke/genesis --client=go-ethereum" + - run: + command: "/tmp/build/hive --sim=smoke/network --client=go-ethereum" + + # This job also runs the smoke test simulations, but against a remote dockerd. + smoke-tests-remote-docker: + docker: + - image: cimg/base:2022.04 + steps: + - checkout + - attach_workspace: {at: "/tmp/build"} + - setup_remote_docker: {version: 20.10.14} + - run: + command: "/tmp/build/hive --sim=smoke/genesis --client=go-ethereum --loglevel 5" + - run: + command: "/tmp/build/hive --sim=smoke/network --client=go-ethereum --loglevel 5" + + # This job runs the go unit tests. + go-test: + docker: + - image: cimg/go:1.21 + steps: + # Get the source. + - checkout + - go/load-cache + - go/mod-download + - go/save-cache + # Run the tests. + - run: + name: "hive module tests" + command: "go test -cover ./..." + - run: + name: "hiveproxy module tests" + command: "go test -cover ./..." + working_directory: "./hiveproxy" + - run: + name: "Compile Go simulators" + command: ".circleci/compile-simulators.sh" + # this makes sure the rust code is good + rust-simulators: + docker: + - image: cimg/rust:1.75.0 + steps: + - checkout + - run: + name: Install rustfmt + command: rustup component add rustfmt + - run: + name: Install Clippy + command: rustup component add clippy + - run: + name: Install Clang + command: sudo apt update && sudo apt-get install clang -y + - run: + name: "Lint, build, test Rust simulators" + command: ".circleci/rust-simulators.sh" + +workflows: + main: + jobs: + - go-test + - build + - smoke-tests: + requires: ["build"] + - smoke-tests-remote-docker: + requires: ["build"] + rust-jobs: + when: << pipeline.parameters.rust-ci >> + jobs: + - rust-simulators diff --git a/.circleci/rust-simulators.sh b/.circleci/rust-simulators.sh new file mode 100755 index 0000000000..5bf64708ac --- /dev/null +++ b/.circleci/rust-simulators.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# This causes the bash script to exit immediately if any commands errors out +set -e + +failed="" +sims=$(find simulators -name Cargo.toml) +for d in $sims; do + d="$(dirname "$d")" + echo "Lint, build, test $d" + ( cd "$d" || exit 1; + cargo fmt --all -- --check; + cargo clippy --all --all-targets --all-features --no-deps -- --deny warnings; + cargo test --workspace -- --nocapture; + ) +done diff --git a/.gitignore b/.gitignore index 1f2ef94e1d..5c6532c305 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,9 @@ workspace .idea/ # build output /hive - -hiveview + +hiveview + +# build output for rust simulators files +simulators/**/Cargo.lock +simulators/**/target diff --git a/clients/besu/Dockerfile.git b/clients/besu/Dockerfile.git index 20d3d79011..7c9223c22c 100644 --- a/clients/besu/Dockerfile.git +++ b/clients/besu/Dockerfile.git @@ -8,7 +8,7 @@ ARG github=hyperledger/besu RUN echo "installing java on ubuntu base image" \ && apt-get update && apt-get install -y git libsodium-dev libnss3-dev \ - && apt-get install --no-install-recommends -q --assume-yes ca-certificates-java=20190909 \ + && apt-get install --no-install-recommends -q --assume-yes ca-certificates-java=20190909* \ && apt-get install --no-install-recommends -q --assume-yes openjdk-17-jre-headless=17* libjemalloc-dev=5.* \ && echo "Cloning: $github - $tag" \ && git clone --depth 1 --branch $tag https://github.com/$github \ diff --git a/clients/besu/besu.sh b/clients/besu/besu.sh index 428b93d3d1..54a6c7329e 100644 --- a/clients/besu/besu.sh +++ b/clients/besu/besu.sh @@ -156,7 +156,7 @@ RPCFLAGS="$RPCFLAGS --rpc-ws-enabled --rpc-ws-api=DEBUG,ETH,NET,WEB3,ADMIN --rpc # Enable merge support if needed if [ "$HIVE_TERMINAL_TOTAL_DIFFICULTY" != "" ]; then echo "0x7365637265747365637265747365637265747365637265747365637265747365" > /jwtsecret - RPCFLAGS="$RPCFLAGS --engine-host-allowlist=* --engine-jwt-enabled --engine-jwt-secret /jwtsecret" + RPCFLAGS="$RPCFLAGS --engine-host-allowlist=* --engine-jwt-secret /jwtsecret" fi # Start Besu. diff --git a/clients/besu/mapper.jq b/clients/besu/mapper.jq new file mode 100644 index 0000000000..e69de29bb2 diff --git a/clients/erigon/Dockerfile.git b/clients/erigon/Dockerfile.git index a5861a8a8b..a21616ca09 100644 --- a/clients/erigon/Dockerfile.git +++ b/clients/erigon/Dockerfile.git @@ -10,7 +10,7 @@ RUN echo "Cloning: $github - $tag" \ && apk add bash build-base ca-certificates git jq \ && git clone --depth 1 --branch $tag https://github.com/$github \ && cd erigon \ - && make erigon \ + && make BUILD_TAGS=nosqlite,noboltdb,nosilkworm erigon \ && cp build/bin/erigon /usr/local/bin/erigon ## Final stage: Sets up the environment for running erigon diff --git a/clients/erigon/erigon.sh b/clients/erigon/erigon.sh index 011a4a623c..89dee1dda1 100644 --- a/clients/erigon/erigon.sh +++ b/clients/erigon/erigon.sh @@ -139,6 +139,9 @@ if [ "$HIVE_TERMINAL_TOTAL_DIFFICULTY" != "" ]; then FLAGS="$FLAGS --authrpc.addr=0.0.0.0 --authrpc.jwtsecret=/jwt.secret" fi +# Configure snapshots. +FLAGS="$FLAGS --snapshots=false" + # Launch the main client. FLAGS="$FLAGS --nat=none" echo "Running erigon with flags $FLAGS" diff --git a/clients/erigon/mapper.jq b/clients/erigon/mapper.jq index 60499b9476..89eb423989 100644 --- a/clients/erigon/mapper.jq +++ b/clients/erigon/mapper.jq @@ -54,7 +54,7 @@ def to_bool: "grayGlacierBlock": env.HIVE_FORK_GRAY_GLACIER|to_int, "mergeNetsplitBlock": env.HIVE_MERGE_BLOCK_ID|to_int, "terminalTotalDifficulty": env.HIVE_TERMINAL_TOTAL_DIFFICULTY|to_int, - "terminalTotalDifficultyPassed": (if env.HIVE_TERMINAL_TOTAL_DIFFICULTY_PASSED == null then 1 else env.HIVE_TERMINAL_TOTAL_DIFFICULTY_PASSED|to_bool end), + "terminalTotalDifficultyPassed": (if env.HIVE_TERMINAL_TOTAL_DIFFICULTY_PASSED == null then true else env.HIVE_TERMINAL_TOTAL_DIFFICULTY_PASSED|to_bool end), "shanghaiTime": env.HIVE_SHANGHAI_TIMESTAMP|to_int, "cancunTime": env.HIVE_CANCUN_TIMESTAMP|to_int, }|remove_empty diff --git a/clients/fluffy/Dockerfile b/clients/fluffy/Dockerfile new file mode 100644 index 0000000000..b34610724b --- /dev/null +++ b/clients/fluffy/Dockerfile @@ -0,0 +1,12 @@ +ARG branch=amd64-master-latest + +FROM statusim/nimbus-fluffy:$branch + +ADD fluffy.sh /fluffy.sh +RUN chmod +x /fluffy.sh + +RUN echo "latest" > /version.txt + +EXPOSE 8545 9009/udp + +ENTRYPOINT ["/fluffy.sh"] diff --git a/clients/fluffy/fluffy.sh b/clients/fluffy/fluffy.sh new file mode 100755 index 0000000000..f9197904f8 --- /dev/null +++ b/clients/fluffy/fluffy.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Immediately abort the script on any error encountered +set -e + +IP_ADDR=$(hostname -i | awk '{print $1}') +FLAGS="" + +if [ "$HIVE_PORTAL_NETWORKS_SELECTED" != "" ]; then + if [[ $HIVE_PORTAL_NETWORKS_SELECTED =~ "beacon" ]]; then + # Providing atrusted block root is required currently to enable the beacon network. + # It can be a made up value for now as tests are not doing any sync. + FLAGS="$FLAGS --trusted-block-root:0x0000000000000000000000000000000000000000000000000000000000000000" + fi +fi + + +if [ "$HIVE_CLIENT_PRIVATE_KEY" != "" ]; then + FLAGS="$FLAGS --netkey-unsafe=0x$HIVE_CLIENT_PRIVATE_KEY" +fi + +# Fluffy runs all networks by default, so we can not configure to run networks individually +fluffy --rpc --rpc-address="0.0.0.0" --nat:extip:"$IP_ADDR" --network=none --log-level="debug" $FLAGS diff --git a/clients/fluffy/hive.yaml b/clients/fluffy/hive.yaml new file mode 100644 index 0000000000..a7a54e4224 --- /dev/null +++ b/clients/fluffy/hive.yaml @@ -0,0 +1,2 @@ +roles: + - portal diff --git a/clients/go-ethereum/geth.sh b/clients/go-ethereum/geth.sh index 9f05cbc34e..5c8d83f97a 100644 --- a/clients/go-ethereum/geth.sh +++ b/clients/go-ethereum/geth.sh @@ -140,7 +140,6 @@ fi if [ "$HIVE_MINER_EXTRA" != "" ]; then FLAGS="$FLAGS --miner.extradata $HIVE_MINER_EXTRA" fi -FLAGS="$FLAGS --miner.gasprice 16000000000" # Configure LES. if [ "$HIVE_LES_SERVER" == "1" ]; then diff --git a/clients/lighthouse-bn/lighthouse_bn.sh b/clients/lighthouse-bn/lighthouse_bn.sh index dea46b4ec3..398434b62c 100755 --- a/clients/lighthouse-bn/lighthouse_bn.sh +++ b/clients/lighthouse-bn/lighthouse_bn.sh @@ -51,6 +51,10 @@ if [[ "$HIVE_ETH2_BEACON_NODE_INDEX" != "" ]]; then fi fi +if [[ "$HIVE_ETH2_TRUSTED_PEER_IDS" != "" ]]; then + trustedpeers="$trustedpeers,$HIVE_ETH2_TRUSTED_PEER_IDS" +fi + LOG=info case "$HIVE_LOGLEVEL" in 0|1) LOG=error ;; diff --git a/clients/lodestar-bn/lodestar_bn.sh b/clients/lodestar-bn/lodestar_bn.sh index 3837b60ad4..a14bb4aeaa 100755 --- a/clients/lodestar-bn/lodestar_bn.sh +++ b/clients/lodestar-bn/lodestar_bn.sh @@ -38,6 +38,7 @@ bootnodes_option=$([[ "$HIVE_ETH2_BOOTNODE_ENRS" == "" ]] && echo "" || echo "-- metrics_option=$([[ "$HIVE_ETH2_METRICS_PORT" == "" ]] && echo "" || echo "--metrics --metrics.address=$CONTAINER_IP --metrics.port=$HIVE_ETH2_METRICS_PORT") builder_option=$([[ "$HIVE_ETH2_BUILDER_ENDPOINT" == "" ]] && echo "" || echo "--builder --builder.urls $HIVE_ETH2_BUILDER_ENDPOINT") echo BUILDER=$builder_option +peer_score_option=$([[ "$HIVE_ETH2_DISABLE_PEER_SCORING" == "" ]] && echo "" || echo "--disablePeerScoring") echo "bootnodes option : ${bootnodes_option}" @@ -64,6 +65,7 @@ node /usr/app/node_modules/.bin/lodestar \ $metrics_option \ $bootnodes_option \ $builder_option \ + $peer_score_option \ --enr.ip="${CONTAINER_IP}" \ --enr.tcp="${HIVE_ETH2_P2P_TCP_PORT:-9000}" \ --enr.udp="${HIVE_ETH2_P2P_UDP_PORT:-9000}" \ diff --git a/clients/nethermind/Dockerfile.git b/clients/nethermind/Dockerfile.git index 8c5e0a1d67..0a94381c00 100644 --- a/clients/nethermind/Dockerfile.git +++ b/clients/nethermind/Dockerfile.git @@ -1,7 +1,7 @@ ### Build Nethermind From Git: ## Builder stage: Compiles nethermind from a git repository -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0-jammy AS build ARG github=nethermindeth/nethermind ARG tag=master diff --git a/clients/nimbus-el/nimbus.sh b/clients/nimbus-el/nimbus.sh index 446fdf5691..2a29a1e818 100644 --- a/clients/nimbus-el/nimbus.sh +++ b/clients/nimbus-el/nimbus.sh @@ -115,14 +115,17 @@ fi set -e -# Configure RPC. +# Configure RPC +FLAGS="$FLAGS --http-address:0.0.0.0 --http-port:8545" +FLAGS="$FLAGS --rpc --rpc-api:eth,debug" +FLAGS="$FLAGS --ws --ws-api:eth,debug" + +# Configure graphql if [ "$HIVE_GRAPHQL_ENABLED" != "" ]; then - FLAGS="$FLAGS --graphql --graphql-address:0.0.0.0 --graphql-port:8545" -else - FLAGS="$FLAGS --rpc --rpc-api:eth,debug --rpc-address:0.0.0.0 --rpc-port:8545" - FLAGS="$FLAGS --ws --ws-api:eth,debug --ws-address:0.0.0.0 --ws-port:8546" + FLAGS="$FLAGS --graphql" fi +# Configure engine api if [ "$HIVE_TERMINAL_TOTAL_DIFFICULTY" != "" ]; then echo "0x7365637265747365637265747365637265747365637265747365637265747365" > /jwtsecret FLAGS="$FLAGS --engine-api:true --engine-api-address:0.0.0.0 --engine-api-port:8551 --jwt-secret:/jwtsecret" diff --git a/clients/prysm-vc/prysm_vc.sh b/clients/prysm-vc/prysm_vc.sh index ebffd9f089..c7fb6260ff 100755 --- a/clients/prysm-vc/prysm_vc.sh +++ b/clients/prysm-vc/prysm_vc.sh @@ -50,11 +50,10 @@ echo Starting Prysm Validator Client --verbosity="$LOG" \ --accept-terms-of-use=true \ --prater \ - --beacon-rpc-provider="$HIVE_ETH2_BN_API_IP:${HIVE_ETH2_BN_GRPC_PORT:-3500}" \ - --beacon-rpc-gateway-provider="$HIVE_ETH2_BN_API_IP:${HIVE_ETH2_BN_API_PORT:-4000}" \ + --enable-beacon-rest-api=true \ + --beacon-rest-api-provider="http://$HIVE_ETH2_BN_API_IP:${HIVE_ETH2_BN_API_PORT:-4000}" \ --datadir="/data/vc" \ --wallet-dir="/data/validators" \ --wallet-password-file="/wallet.pass" \ --chain-config-file="/hive/input/config.yaml" \ - $builder_option -# NOTE: gRPC/RPC ports are inverted to allow the simulator to access the REST API \ No newline at end of file + $builder_option \ No newline at end of file diff --git a/clients/reth/Dockerfile b/clients/reth/Dockerfile index 27c0789474..2f70b6aa5c 100644 --- a/clients/reth/Dockerfile +++ b/clients/reth/Dockerfile @@ -1,5 +1,5 @@ -ARG baseimage=paradigmxyz/reth -ARG tag=main +ARG baseimage=ghcr.io/paradigmxyz/reth +ARG tag=latest FROM $baseimage:$tag as builder diff --git a/clients/reth/mapper.jq b/clients/reth/mapper.jq index 1556c95356..d21b3355c8 100644 --- a/clients/reth/mapper.jq +++ b/clients/reth/mapper.jq @@ -52,7 +52,7 @@ def to_bool: "londonBlock": env.HIVE_FORK_LONDON|to_int, "arrowGlacierBlock": env.HIVE_FORK_ARROW_GLACIER|to_int, "grayGlacierBlock": env.HIVE_FORK_GRAY_GLACIER|to_int, - "parisBlock": env.HIVE_MERGE_BLOCK_ID|to_int, + "mergeNetsplitBlock": env.HIVE_MERGE_BLOCK_ID|to_int, "terminalTotalDifficulty": env.HIVE_TERMINAL_TOTAL_DIFFICULTY|to_int, "terminalTotalDifficultyPassed": env.HIVE_TERMINAL_TOTAL_DIFFICULTY_PASSED|to_bool, "shanghaiTime": env.HIVE_SHANGHAI_TIMESTAMP|to_int, diff --git a/clients/reth/reth.sh b/clients/reth/reth.sh index 0468bc96ba..a2b2a5d6a1 100644 --- a/clients/reth/reth.sh +++ b/clients/reth/reth.sh @@ -50,8 +50,9 @@ case "$HIVE_LOGLEVEL" in esac # Create the data directory. -mkdir /reth-hive-datadir -FLAGS="$FLAGS --datadir /reth-hive-datadir" +DATADIR="/reth-hive-datadir" +mkdir $DATADIR +FLAGS="$FLAGS --datadir $DATADIR" # TODO If a specific network ID is requested, use that #if [ "$HIVE_NETWORK_ID" != "" ]; then @@ -80,6 +81,9 @@ echo $FLAGS echo "Initializing database with genesis state..." $reth init $FLAGS --chain /genesis.json +# Make sure pruner doesn't start +echo -e "[prune]\\nblock_interval = 500_000" >> $DATADIR/reth.toml + # make sure we use the same genesis each time FLAGS="$FLAGS --chain /genesis.json" diff --git a/clients/trin-bridge/Dockerfile b/clients/trin-bridge/Dockerfile new file mode 100644 index 0000000000..aa7405a47d --- /dev/null +++ b/clients/trin-bridge/Dockerfile @@ -0,0 +1,19 @@ +FROM portalnetwork/trin:latest-bridge + +ADD https://raw.githubusercontent.com/ethereum/portal-spec-tests/master/tests/mainnet/history/hive/test_data_collection_of_forks_blocks.yaml /test_data_collection_of_forks_blocks.yaml +ADD trin_bridge.sh /trin_bridge.sh +RUN chmod +x /trin_bridge.sh + +ADD trin_bridge_version.sh /trin_bridge_version.sh +RUN chmod +x /trin_bridge_version.sh + +RUN /trin_bridge_version.sh > /version.txt + +# Export the usual networking ports to allow outside access to the node +EXPOSE 8545 9009/udp + +# add fake secrets for bridge activation +ENV PANDAOPS_CLIENT_ID=xxx +ENV PANDAOPS_CLIENT_SECRET=xxx + +ENTRYPOINT ["/trin_bridge.sh"] diff --git a/clients/trin-bridge/hive.yaml b/clients/trin-bridge/hive.yaml new file mode 100644 index 0000000000..a7a54e4224 --- /dev/null +++ b/clients/trin-bridge/hive.yaml @@ -0,0 +1,2 @@ +roles: + - portal diff --git a/clients/trin-bridge/trin_bridge.sh b/clients/trin-bridge/trin_bridge.sh new file mode 100644 index 0000000000..5828de0cb7 --- /dev/null +++ b/clients/trin-bridge/trin_bridge.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Immediately abort the script on any error encountered +set -e + +IP_ADDR=$(hostname -i | awk '{print $1}') +FLAGS="" + +if [ "$HIVE_BOOTNODES" != "" ]; then + FLAGS="$FLAGS --bootnodes=$HIVE_BOOTNODES" +else + echo "Warning: HIVE_BOOTNODES wasn't provided" + exit 1 +fi + +RUST_LOG=debug portal-bridge --node-count 1 $FLAGS --executable-path ./usr/bin/trin --mode test:/test_data_collection_of_forks_blocks.yaml --el-provider test --external-ip $IP_ADDR --epoch-accumulator-path . trin diff --git a/clients/trin-bridge/trin_bridge_version.sh b/clients/trin-bridge/trin_bridge_version.sh new file mode 100644 index 0000000000..b29ff5a658 --- /dev/null +++ b/clients/trin-bridge/trin_bridge_version.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Immediately abort the script on any error encountered +set -e + +#trin --version | tail -1 | sed "s/ /\//g" +echo "latest" diff --git a/clients/trin/Dockerfile b/clients/trin/Dockerfile new file mode 100644 index 0000000000..3f980aef80 --- /dev/null +++ b/clients/trin/Dockerfile @@ -0,0 +1,16 @@ +ARG branch=latest + +FROM portalnetwork/trin:$branch + +ADD trin.sh /trin.sh +RUN chmod +x /trin.sh + +ADD trin_version.sh /trin_version.sh +RUN chmod +x /trin_version.sh + +RUN /trin_version.sh > /version.txt + +# Export the usual networking ports to allow outside access to the node +EXPOSE 8545 9009/udp + +ENTRYPOINT ["/trin.sh"] diff --git a/clients/trin/hive.yaml b/clients/trin/hive.yaml new file mode 100644 index 0000000000..a7a54e4224 --- /dev/null +++ b/clients/trin/hive.yaml @@ -0,0 +1,2 @@ +roles: + - portal diff --git a/clients/trin/trin.sh b/clients/trin/trin.sh new file mode 100644 index 0000000000..2677d2966d --- /dev/null +++ b/clients/trin/trin.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Immediately abort the script on any error encountered +set -e + +IP_ADDR=$(hostname -i | awk '{print $1}') +FLAGS="" + +if [ "$HIVE_CLIENT_PRIVATE_KEY" != "" ]; then + FLAGS="$FLAGS --unsafe-private-key 0x$HIVE_CLIENT_PRIVATE_KEY" +fi + +if [ "$HIVE_PORTAL_NETWORKS_SELECTED" != "" ]; then + FLAGS="$FLAGS --networks $HIVE_PORTAL_NETWORKS_SELECTED" +else + FLAGS="$FLAGS --networks history" +fi + +RUST_LOG=debug trin --web3-transport http --web3-http-address http://0.0.0.0:8545 --external-address "$IP_ADDR":9009 --bootnodes none $FLAGS diff --git a/clients/trin/trin_version.sh b/clients/trin/trin_version.sh new file mode 100644 index 0000000000..b29ff5a658 --- /dev/null +++ b/clients/trin/trin_version.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Immediately abort the script on any error encountered +set -e + +#trin --version | tail -1 | sed "s/ /\//g" +echo "latest" diff --git a/clients/ultralight/Dockerfile b/clients/ultralight/Dockerfile new file mode 100644 index 0000000000..011fe582d5 --- /dev/null +++ b/clients/ultralight/Dockerfile @@ -0,0 +1,11 @@ +FROM ghcr.io/ethereumjs/ultralight:latest + +COPY ultralight.sh /ultralight.sh +RUN chmod +x /ultralight.sh + +RUN echo "latest" > /version.txt + +# Export the usual networking ports to allow outside access to the node +EXPOSE 8545 9000/udp + +ENTRYPOINT ["/ultralight.sh"] diff --git a/clients/ultralight/hive.yaml b/clients/ultralight/hive.yaml new file mode 100644 index 0000000000..a7a54e4224 --- /dev/null +++ b/clients/ultralight/hive.yaml @@ -0,0 +1,2 @@ +roles: + - portal diff --git a/clients/ultralight/ultralight.sh b/clients/ultralight/ultralight.sh new file mode 100644 index 0000000000..f619a18e0c --- /dev/null +++ b/clients/ultralight/ultralight.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Immediately abort the script on any error encountered +set -e + +IP_ADDR=$(hostname -i | awk '{print $1}') +FLAGS="" + +if [ "$HIVE_CLIENT_PRIVATE_KEY" != "" ]; then + FLAGS="$FLAGS --pk=0x1a2408021220$HIVE_CLIENT_PRIVATE_KEY" +fi + +if [ "$HIVE_PORTAL_NETWORKS_SELECTED" != "" ]; then + FLAGS="$FLAGS --networks=$HIVE_PORTAL_NETWORKS_SELECTED" +else + FLAGS="$FLAGS --networks=history" +fi + +DEBUG=* node /ultralight/packages/cli/dist/index.js --bindAddress="$IP_ADDR:9000" --dataDir="./data" --rpcPort=8545 $FLAGS diff --git a/cmd/hivechain/bytecode/callrevert.bin b/cmd/hivechain/bytecode/callrevert.bin index 5468f315f5..eb6915d94a 100644 Binary files a/cmd/hivechain/bytecode/callrevert.bin and b/cmd/hivechain/bytecode/callrevert.bin differ diff --git a/cmd/hivechain/bytecode/emit.bin b/cmd/hivechain/bytecode/emit.bin new file mode 100644 index 0000000000..3bb08f5cf8 Binary files /dev/null and b/cmd/hivechain/bytecode/emit.bin differ diff --git a/cmd/hivechain/bytecode/gencode.bin b/cmd/hivechain/bytecode/gencode.bin index bc95df8500..5f80e048ab 100644 Binary files a/cmd/hivechain/bytecode/gencode.bin and b/cmd/hivechain/bytecode/gencode.bin differ diff --git a/cmd/hivechain/compile.sh b/cmd/hivechain/compile.sh index 12a9efc464..554fa952df 100755 --- a/cmd/hivechain/compile.sh +++ b/cmd/hivechain/compile.sh @@ -6,6 +6,7 @@ geas -bin -no-push0 contracts/deployer.eas > bytecode/deployer.bin geas -bin -no-push0 contracts/callenv.eas > bytecode/callenv.bin geas -bin -no-push0 contracts/callme.eas > bytecode/callme.bin geas -bin -no-push0 contracts/callrevert.eas > bytecode/callrevert.bin +geas -bin -no-push0 contracts/emit.eas > bytecode/emit.bin geas -bin -no-push0 contracts/genlogs.eas > bytecode/genlogs.bin geas -bin -no-push0 contracts/gencode.eas > bytecode/gencode.bin geas -bin -no-push0 contracts/genstorage.eas > bytecode/genstorage.bin diff --git a/cmd/hivechain/contracts.go b/cmd/hivechain/contracts.go index 94ad377f3b..e171b9e4d0 100644 --- a/cmd/hivechain/contracts.go +++ b/cmd/hivechain/contracts.go @@ -20,7 +20,13 @@ var callmeCode []byte //go:embed bytecode/callenv.bin var callenvCode []byte +//go:embed bytecode/callrevert.bin +var callrevertCode []byte + +//go:embed bytecode/emit.bin +var emitCode []byte + // //go:embed bytecode/deposit.bin // var depositCode []byte -// +// // const depositContractAddr = "0x00000000219ab540356cBB839Cbe05303d7705Fa" diff --git a/cmd/hivechain/contracts/callrevert.eas b/cmd/hivechain/contracts/callrevert.eas index 94386d94ea..6f5d0b55fe 100644 --- a/cmd/hivechain/contracts/callrevert.eas +++ b/cmd/hivechain/contracts/callrevert.eas @@ -1,6 +1,6 @@ ;;; -*- mode: asm -*- ;;; This contract is for testing two common Solidity revert encodings: -;;; panic(uint) and error(string). +;;; Panic(uint) and Error(string). ;;; Dispatch .start: @@ -12,10 +12,11 @@ iszero ; [word!=0] jumpi @error ; [word] -#define s_panic .selector("panic(uint)") -#define panicv 17 -;;; Solidity ABI `panic(17)` +#define s_panic .selector("Panic(uint256)") +#define panicv 0x01 ;; == assert(false) + +;;; Solidity ABI `Panic(17)` ;;; Revert data layout: ;;; ;;; selector :: 4 || value :: 32 @@ -24,7 +25,7 @@ push s_panic << (28*8) ; [sel] push 0 ; [offset, sel] mstore ; [] - push 17 ; [panicv] + push panicv ; [panicv] push 4 ; [offset, panicv] mstore ; [] @@ -33,12 +34,11 @@ revert ; [] -#define s_error .selector("error(string)") +#define s_error .selector("Error(string)") #define errmsg "user error" #define errmsg_word errmsg << (255-.bitlen(errmsg)) -;;; Solidity ABI error -;;; +;;; Solidity ABI `Error(string)` ;;; Revert data layout: ;;; ;;; selector :: 4 || 0x20 :: 32 || len :: 32 || data :: len diff --git a/cmd/hivechain/contracts/emit.eas b/cmd/hivechain/contracts/emit.eas new file mode 100644 index 0000000000..a3da393480 --- /dev/null +++ b/cmd/hivechain/contracts/emit.eas @@ -0,0 +1,37 @@ +;;; -*- mode: asm -*- +;;; Example contract which emits storage and logs when invoked. +;;; The storage entry uses the hash of calldata as the value. +;;; In the log, the same hash is used as the second topic. + +#define s_counter 0 + + ;; hash calldata + calldatasize ; [size] + dup1 ; [size] + push 0 ; [offset, size, size] + dup1 ; [dest, offset, size, size] + calldatacopy ; [size] + push 0 ; [offset, size] + keccak256 ; [hash] + + ;; write a storage entry + push s_counter ; [slot] + sload ; [addr, counter, hash] + dup1 ; [counter, counter, hash] + dup3 ; [hash, counter, counter, hash] + sstore ; [counter, hash] + + ;; increment counter in storage + dup1 ; [counter, counter, hash] + push 1 ; [1, counter, counter, hash] + add ; [counter+1, counter, hash] + push s_counter ; [slot, counter+1, counter, hash] + sstore ; [counter, hash] + + ;; emit log + push 0 ; [offset, counter, hash] + mstore ; [hash] + push "emit" ; [topic1, hash=topic2] + push 32 ; [size, topic1, topic2] + push 0 ; [offset, size, topic1, topic2] + log2 ; [] diff --git a/cmd/hivechain/contracts/gencode.eas b/cmd/hivechain/contracts/gencode.eas index 290355511d..b323ff83f2 100644 --- a/cmd/hivechain/contracts/gencode.eas +++ b/cmd/hivechain/contracts/gencode.eas @@ -3,7 +3,7 @@ #include "rng.eas" -#define outputSize 512 +#define outputSize 256 #define p_rng 0 #define p_output p_rng + RngSize @@ -20,7 +20,7 @@ loop: dup2 ; [ptr, word, ptr] mstore ; [ptr] ;; increment output pointer - push 32 ; [1, ptr] + push 32 ; [32, ptr] add ; [newptr] push p_outputEnd ; [end, newptr] dup2 ; [newptr, end, newtpr] diff --git a/cmd/hivechain/generate.go b/cmd/hivechain/generate.go index 8322c9c93d..3c9b25df6a 100644 --- a/cmd/hivechain/generate.go +++ b/cmd/hivechain/generate.go @@ -64,7 +64,9 @@ type generator struct { accounts []genAccount rand *rand.Rand - modlist []*modifierInstance + // Modifier lists. + virgins []*modifierInstance + mods []*modifierInstance modOffset int // for write/export @@ -88,7 +90,7 @@ func newGenerator(cfg generatorConfig) *generator { genesis: genesis, rand: rand.New(rand.NewSource(10)), td: new(big.Int).Set(genesis.Difficulty), - modlist: cfg.createBlockModifiers(), + virgins: cfg.createBlockModifiers(), accounts: slices.Clone(knownAccounts), } } @@ -213,7 +215,8 @@ func (g *generator) setParentBeaconRoot(i int, gen *core.BlockGen) { // runModifiers executes the chain modifiers. func (g *generator) runModifiers(i int, gen *core.BlockGen) { - if len(g.modlist) == 0 || g.cfg.txInterval == 0 || i%g.cfg.txInterval != 0 { + totalMods := len(g.mods) + len(g.virgins) + if totalMods == 0 || g.cfg.txInterval == 0 || i%g.cfg.txInterval != 0 { return } @@ -224,9 +227,7 @@ func (g *generator) runModifiers(i int, gen *core.BlockGen) { // because this usually means there is no gas left. count := 0 refused := 0 // count of consecutive times apply() returned false - for ; count < g.cfg.txCount && refused < len(g.modlist); g.modOffset++ { - index := g.modOffset % len(g.modlist) - mod := g.modlist[index] + run := func(mod *modifierInstance) bool { ok := mod.apply(ctx) if ok { fmt.Println(" -", mod.name) @@ -235,6 +236,24 @@ func (g *generator) runModifiers(i int, gen *core.BlockGen) { } else { refused++ } + return ok + } + + // In order to avoid a pathological situation where a modifier never executes because + // of unfortunate scheduling, we first try modifiers from g.virgins. + for i := 0; i < len(g.virgins) && count < g.cfg.txCount; i++ { + mod := g.virgins[i] + if run(mod) { + g.mods = append(g.mods, mod) + g.virgins = append(g.virgins[:i], g.virgins[i+1:]...) + i-- + } + } + // If there is any space left, fill it using g.mods. + for len(g.mods) > 0 && count < g.cfg.txCount && refused < totalMods { + index := g.modOffset % len(g.mods) + run(g.mods[index]) + g.modOffset++ } } diff --git a/cmd/hivechain/generate_test.go b/cmd/hivechain/generate_test.go new file mode 100644 index 0000000000..3b32e49df1 --- /dev/null +++ b/cmd/hivechain/generate_test.go @@ -0,0 +1,29 @@ +package main + +import ( + "path/filepath" + "testing" +) + +func TestGenerate(t *testing.T) { + outdir := t.TempDir() + cfg := generatorConfig{ + txInterval: 1, + txCount: 10, + forkInterval: 2, + chainLength: 30, + outputDir: outdir, + outputs: outputFunctionNames(), + } + cfg, err := cfg.withDefaults() + if err != nil { + t.Fatal(err) + } + g := newGenerator(cfg) + if err := g.run(); err != nil { + t.Fatal(err) + } + + names, _ := filepath.Glob(filepath.Join(outdir, "*")) + t.Log("output files:", names) +} diff --git a/cmd/hivechain/genesis.go b/cmd/hivechain/genesis.go index a5a2497e06..363ee1f52c 100644 --- a/cmd/hivechain/genesis.go +++ b/cmd/hivechain/genesis.go @@ -160,6 +160,7 @@ func (cfg *generatorConfig) createGenesis() *core.Genesis { } add4788Contract(g.Alloc) addSnapTestContract(g.Alloc) + addEmitContract(g.Alloc) return &g } @@ -185,6 +186,16 @@ func addSnapTestContract(ga core.GenesisAlloc) { } } +const emitAddr = "0x7dcd17433742f4c0ca53122ab541d0ba67fc27df" + +func addEmitContract(ga core.GenesisAlloc) { + addr := common.HexToAddress(emitAddr) + ga[addr] = core.GenesisAccount{ + Balance: new(big.Int), + Code: emitCode, + } +} + // forkBlocks computes the block numbers where forks occur. Forks get enabled based on the // forkInterval. If the total number of requested blocks (chainLength) is lower than // necessary, the remaining forks activate on the last chain block. diff --git a/cmd/hivechain/mod.go b/cmd/hivechain/mod.go index 37b5f89acf..d39df135b2 100644 --- a/cmd/hivechain/mod.go +++ b/cmd/hivechain/mod.go @@ -1,6 +1,8 @@ package main import ( + "crypto/sha256" + "encoding/binary" "math/big" "github.com/ethereum/go-ethereum/common" @@ -22,9 +24,10 @@ func register(name string, new func() blockModifier) { } type genBlockContext struct { - index int - block *core.BlockGen - gen *generator + index int + block *core.BlockGen + gen *generator + txcount int } // Number returns the block number. @@ -53,7 +56,8 @@ func (ctx *genBlockContext) AddNewTx(sender *genAccount, data types.TxData) *typ if err != nil { panic(err) } - ctx.block.AddTx(tx) + ctx.block.AddTx(tx.WithoutBlobTxSidecar()) + ctx.txcount++ return tx } @@ -95,6 +99,11 @@ func (ctx *genBlockContext) Signer() types.Signer { return ctx.block.Signer() } +// TxCount returns the number of transactions added so far. +func (ctx *genBlockContext) TxCount() int { + return ctx.txcount +} + // ChainConfig returns the chain config. func (ctx *genBlockContext) ChainConfig() *params.ChainConfig { return ctx.gen.genesis.Config @@ -104,3 +113,13 @@ func (ctx *genBlockContext) ChainConfig() *params.ChainConfig { func (ctx *genBlockContext) ParentBlock() *types.Block { return ctx.block.PrevBlock(ctx.index - 1) } + +// TxRandomValue returns a random value that depends on the block number and current transaction index. +func (ctx *genBlockContext) TxRandomValue() uint64 { + var txindex [8]byte + binary.BigEndian.PutUint64(txindex[:], uint64(ctx.TxCount())) + h := sha256.New() + h.Write(ctx.Number().Bytes()) + h.Write(txindex[:]) + return binary.BigEndian.Uint64(h.Sum(nil)) +} diff --git a/cmd/hivechain/mod_randtx.go b/cmd/hivechain/mod_createspam.go similarity index 64% rename from cmd/hivechain/mod_randtx.go rename to cmd/hivechain/mod_createspam.go index 3e91942aff..dc8d8466d4 100644 --- a/cmd/hivechain/mod_randtx.go +++ b/cmd/hivechain/mod_createspam.go @@ -4,33 +4,36 @@ import ( "github.com/ethereum/go-ethereum/core/types" ) +// Here we create transactions that create spam contracts. These exist simply to fill up +// the state. We need a decent amount of state in the sync tests, for example. + func init() { register("randomlogs", func() blockModifier { - return &modCreateTx{ + return &modCreateSpam{ code: genlogsCode, gas: 20000, } }) register("randomcode", func() blockModifier { - return &modCreateTx{ + return &modCreateSpam{ code: gencodeCode, - gas: 30000, + gas: 60000, } }) register("randomstorage", func() blockModifier { - return &modCreateTx{ + return &modCreateSpam{ code: genstorageCode, gas: 80000, } }) } -type modCreateTx struct { +type modCreateSpam struct { code []byte gas uint64 } -func (m *modCreateTx) apply(ctx *genBlockContext) bool { +func (m *modCreateSpam) apply(ctx *genBlockContext) bool { gas := ctx.TxCreateIntrinsicGas(m.code) + m.gas if !ctx.HasGas(gas) { return false @@ -47,6 +50,6 @@ func (m *modCreateTx) apply(ctx *genBlockContext) bool { return true } -func (m *modCreateTx) txInfo() any { +func (m *modCreateSpam) txInfo() any { return nil } diff --git a/cmd/hivechain/mod_deploy.go b/cmd/hivechain/mod_deploy.go index b11b84951a..ecac168481 100644 --- a/cmd/hivechain/mod_deploy.go +++ b/cmd/hivechain/mod_deploy.go @@ -5,6 +5,7 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" ) func init() { @@ -14,6 +15,9 @@ func init() { register("deploy-callenv", func() blockModifier { return &modDeploy{code: callenvCode} }) + register("deploy-callrevert", func() blockModifier { + return &modDeploy{code: callrevertCode} + }) } type modDeploy struct { @@ -34,7 +38,9 @@ func (m *modDeploy) apply(ctx *genBlockContext) bool { var code []byte code = append(code, deployerCode...) code = append(code, m.code...) - gas := ctx.TxCreateIntrinsicGas(code) + 10000 + gas := ctx.TxCreateIntrinsicGas(code) + gas += uint64(len(m.code)) * params.CreateDataGas + gas += 15000 // extra gas for constructor execution if !ctx.HasGas(gas) { return false } diff --git a/cmd/hivechain/mod_txinvoke.go b/cmd/hivechain/mod_txinvoke.go new file mode 100644 index 0000000000..e313aa09bb --- /dev/null +++ b/cmd/hivechain/mod_txinvoke.go @@ -0,0 +1,180 @@ +package main + +import ( + "encoding/binary" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/kzg4844" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" +) + +func init() { + register("tx-emit-legacy", func() blockModifier { + return &modInvokeEmit{ + txType: types.LegacyTxType, + gasLimit: 100000, + } + }) + register("tx-emit-eip2930", func() blockModifier { + return &modInvokeEmit{ + txType: types.AccessListTxType, + gasLimit: 100000, + } + }) + register("tx-emit-eip1559", func() blockModifier { + return &modInvokeEmit{ + txType: types.DynamicFeeTxType, + gasLimit: 100000, + } + }) + register("tx-emit-eip4844", func() blockModifier { + return &modInvokeEmit{ + txType: types.BlobTxType, + gasLimit: 100000, + } + }) +} + +// modInvokeEmit creates transactions that invoke the 'emit' contract. +type modInvokeEmit struct { + txType byte + gasLimit uint64 + + txs []invokeEmitTxInfo +} + +type invokeEmitTxInfo struct { + TxHash common.Hash `json:"txhash"` + Sender common.Address `json:"sender"` + Block hexutil.Uint64 `json:"block"` + Index int `json:"indexInBlock"` + LogTopic0 common.Hash `json:"logtopic0"` + LogTopic1 common.Hash `json:"logtopic1"` +} + +func (m *modInvokeEmit) apply(ctx *genBlockContext) bool { + if !ctx.HasGas(m.gasLimit) { + return false + } + + sender := ctx.TxSenderAccount() + recipient := common.HexToAddress(emitAddr) + calldata := m.genCallData(ctx) + datahash := crypto.Keccak256Hash(calldata) + + var txdata types.TxData + switch m.txType { + case types.LegacyTxType: + txdata = &types.LegacyTx{ + Nonce: ctx.AccountNonce(sender.addr), + Gas: m.gasLimit, + GasPrice: ctx.TxGasFeeCap(), + To: &recipient, + Data: calldata, + Value: big.NewInt(2), + } + + case types.AccessListTxType: + if !ctx.ChainConfig().IsBerlin(ctx.Number()) { + return false + } + txdata = &types.AccessListTx{ + Nonce: ctx.AccountNonce(sender.addr), + Gas: m.gasLimit, + GasPrice: ctx.TxGasFeeCap(), + To: &recipient, + Value: big.NewInt(2), + Data: calldata, + AccessList: types.AccessList{ + { + Address: recipient, + StorageKeys: []common.Hash{{}, datahash}, + }, + }, + } + + case types.DynamicFeeTxType: + if !ctx.ChainConfig().IsLondon(ctx.Number()) { + return false + } + txdata = &types.DynamicFeeTx{ + Nonce: ctx.AccountNonce(sender.addr), + Gas: m.gasLimit, + GasFeeCap: ctx.TxGasFeeCap(), + GasTipCap: big.NewInt(1), + To: &recipient, + Value: big.NewInt(2), + Data: calldata, + AccessList: types.AccessList{ + { + Address: recipient, + StorageKeys: []common.Hash{{}, datahash}, + }, + }, + } + + case types.BlobTxType: + if !ctx.ChainConfig().IsCancun(ctx.Number(), ctx.Timestamp()) { + return false + } + var ( + blob1 = kzg4844.Blob{0x01} + blob1C, _ = kzg4844.BlobToCommitment(blob1) + blob1P, _ = kzg4844.ComputeBlobProof(blob1, blob1C) + ) + sidecar := &types.BlobTxSidecar{ + Blobs: []kzg4844.Blob{blob1}, + Commitments: []kzg4844.Commitment{blob1C}, + Proofs: []kzg4844.Proof{blob1P}, + } + txdata = &types.BlobTx{ + Nonce: ctx.AccountNonce(sender.addr), + GasTipCap: uint256.NewInt(1), + GasFeeCap: uint256.MustFromBig(ctx.TxGasFeeCap()), + Gas: m.gasLimit, + To: recipient, + Value: uint256.NewInt(3), + Data: calldata, + AccessList: types.AccessList{ + { + Address: recipient, + StorageKeys: []common.Hash{{}, datahash}, + }, + }, + BlobFeeCap: uint256.NewInt(params.BlobTxBlobGasPerBlob), + BlobHashes: sidecar.BlobHashes(), + Sidecar: sidecar, + } + + default: + panic(fmt.Errorf("unhandled tx type %d", m.txType)) + } + + txindex := ctx.TxCount() + tx := ctx.AddNewTx(sender, txdata) + m.txs = append(m.txs, invokeEmitTxInfo{ + Block: hexutil.Uint64(ctx.NumberU64()), + Sender: sender.addr, + TxHash: tx.Hash(), + Index: txindex, + LogTopic0: common.HexToHash("0x00000000000000000000000000000000000000000000000000000000656d6974"), + LogTopic1: datahash, + }) + return true +} + +func (m *modInvokeEmit) txInfo() any { + return m.txs +} + +func (m *modInvokeEmit) genCallData(ctx *genBlockContext) []byte { + d := make([]byte, 8) + binary.BigEndian.PutUint64(d, ctx.TxRandomValue()) + return append(d, "emit"...) +} diff --git a/cmd/hivechain/mod_txvaluetransfer.go b/cmd/hivechain/mod_txvaluetransfer.go new file mode 100644 index 0000000000..595c0c94b6 --- /dev/null +++ b/cmd/hivechain/mod_txvaluetransfer.go @@ -0,0 +1,115 @@ +package main + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" +) + +func init() { + register("tx-transfer-legacy", func() blockModifier { + return &modValueTransfer{ + txType: types.LegacyTxType, + gasLimit: params.TxGas, + } + }) + register("tx-transfer-eip2930", func() blockModifier { + return &modValueTransfer{ + txType: types.AccessListTxType, + gasLimit: params.TxGas, + } + }) + register("tx-transfer-eip1559", func() blockModifier { + return &modValueTransfer{ + txType: types.DynamicFeeTxType, + gasLimit: params.TxGas, + } + }) + +} + +type modValueTransfer struct { + txType byte + gasLimit uint64 + + txs []valueTransferInfo +} + +type valueTransferInfo struct { + TxHash common.Hash `json:"txhash"` + Sender common.Address `json:"sender"` + Block hexutil.Uint64 `json:"block"` + Index int `json:"indexInBlock"` +} + +func (m *modValueTransfer) apply(ctx *genBlockContext) bool { + if !ctx.HasGas(m.gasLimit) { + return false + } + + sender := ctx.TxSenderAccount() + recipient := pickRecipient(ctx) + + var txdata types.TxData + switch m.txType { + case types.LegacyTxType: + txdata = &types.LegacyTx{ + Nonce: ctx.AccountNonce(sender.addr), + Gas: m.gasLimit, + GasPrice: ctx.TxGasFeeCap(), + To: &recipient, + Value: big.NewInt(1), + } + + case types.AccessListTxType: + if !ctx.ChainConfig().IsBerlin(ctx.Number()) { + return false + } + txdata = &types.AccessListTx{ + Nonce: ctx.AccountNonce(sender.addr), + Gas: m.gasLimit, + GasPrice: ctx.TxGasFeeCap(), + To: &recipient, + Value: big.NewInt(1), + } + + case types.DynamicFeeTxType: + if !ctx.ChainConfig().IsLondon(ctx.Number()) { + return false + } + txdata = &types.DynamicFeeTx{ + Nonce: ctx.AccountNonce(sender.addr), + Gas: m.gasLimit, + GasFeeCap: ctx.TxGasFeeCap(), + GasTipCap: big.NewInt(1), + To: &recipient, + Value: big.NewInt(1), + } + + default: + panic(fmt.Errorf("unhandled tx type %d", m.txType)) + } + + txindex := ctx.TxCount() + tx := ctx.AddNewTx(sender, txdata) + m.txs = append(m.txs, valueTransferInfo{ + Block: hexutil.Uint64(ctx.NumberU64()), + Sender: sender.addr, + TxHash: tx.Hash(), + Index: txindex, + }) + return true +} + +func (m *modValueTransfer) txInfo() any { + return m.txs +} + +func pickRecipient(ctx *genBlockContext) common.Address { + i := ctx.TxRandomValue() % uint64(len(ctx.gen.accounts)) + return ctx.gen.accounts[i].addr +} diff --git a/cmd/hivechain/mod_valuetransfer.go b/cmd/hivechain/mod_valuetransfer.go deleted file mode 100644 index 0597ecbb49..0000000000 --- a/cmd/hivechain/mod_valuetransfer.go +++ /dev/null @@ -1,85 +0,0 @@ -package main - -import ( - "crypto/sha256" - "math/big" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/params" -) - -func init() { - register("valuetransfer", func() blockModifier { - return &modValueTransfer{} - }) -} - -type modValueTransfer struct { - counter int - txs []valueTransferInfo -} - -type valueTransferInfo struct { - Block hexutil.Uint64 `json:"block"` - Sender common.Address `json:"sender"` - Tx *types.Transaction `json:"tx"` -} - -func (m *modValueTransfer) apply(ctx *genBlockContext) bool { - if !ctx.HasGas(params.TxGas) { - return false - } - - sender := ctx.TxSenderAccount() - var txdata types.TxData - for txdata == nil { - switch m.counter % 2 { - case 0: - // legacy tx - r := randomRecipient(ctx.Number()) - txdata = &types.LegacyTx{ - Nonce: ctx.AccountNonce(sender.addr), - Gas: params.TxGas, - GasPrice: ctx.TxGasFeeCap(), - To: &r, - Value: big.NewInt(1), - } - case 1: - // EIP1559 tx - if !ctx.ChainConfig().IsLondon(ctx.Number()) { - m.counter++ - continue - } - r := randomRecipient(ctx.Number()) - txdata = &types.DynamicFeeTx{ - Nonce: ctx.AccountNonce(sender.addr), - Gas: params.TxGas, - GasFeeCap: ctx.TxGasFeeCap(), - GasTipCap: big.NewInt(1), - To: &r, - Value: big.NewInt(1), - } - } - } - - tx := ctx.AddNewTx(sender, txdata) - m.counter++ - m.txs = append(m.txs, valueTransferInfo{ - Block: hexutil.Uint64(ctx.NumberU64()), - Sender: sender.addr, - Tx: tx, - }) - return true -} - -func (m *modValueTransfer) txInfo() any { - return m.txs -} - -func randomRecipient(blocknum *big.Int) (a common.Address) { - h := sha256.Sum256(blocknum.Bytes()) - a.SetBytes(h[:20]) - return a -} diff --git a/cmd/hivechain/mod_withdrawals.go b/cmd/hivechain/mod_withdrawals.go index 1154543020..7f2eb77482 100644 --- a/cmd/hivechain/mod_withdrawals.go +++ b/cmd/hivechain/mod_withdrawals.go @@ -31,7 +31,7 @@ func (m *modWithdrawals) apply(ctx *genBlockContext) bool { w := types.Withdrawal{ Validator: 5, - Address: randomRecipient(ctx.Number()), + Address: pickRecipient(ctx), Amount: 100, } w.Index = ctx.block.AddWithdrawal(&w) diff --git a/cmd/hivechain/output.go b/cmd/hivechain/output.go index 724241bf2a..2e5929632a 100644 --- a/cmd/hivechain/output.go +++ b/cmd/hivechain/output.go @@ -158,8 +158,8 @@ func exportN(bc *core.BlockChain, w io.Writer, first uint64, last uint64) error // writeTxInfo writes information about the transactions that were added into the chain. func (g *generator) writeTxInfo() error { - m := make(map[string]any, len(g.modlist)) - for _, inst := range g.modlist { + m := make(map[string]any, len(g.mods)) + for _, inst := range g.mods { m[inst.name] = inst.txInfo() } return g.writeJSON("txinfo.json", &m) diff --git a/go.mod b/go.mod index 9ab9883430..304b608bf5 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,9 @@ require ( github.com/ethereum/hive/hiveproxy v0.0.0-20230919105823-37cbbe1ef86d github.com/evanw/esbuild v0.17.6 github.com/fsouza/go-dockerclient v1.9.8 + github.com/golang-jwt/jwt/v4 v4.5.0 github.com/gorilla/mux v1.8.0 + github.com/holiman/uint256 v1.2.3 github.com/lithammer/dedent v1.1.0 golang.org/x/exp v0.0.0-20230905200255-921286631fa9 golang.org/x/net v0.17.0 @@ -51,7 +53,6 @@ require ( github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect - github.com/holiman/uint256 v1.2.3 // indirect github.com/klauspost/compress v1.15.15 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect diff --git a/go.sum b/go.sum index 6213fc0cdb..c1a067b12c 100644 --- a/go.sum +++ b/go.sum @@ -139,6 +139,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= diff --git a/hivesim/engineapi.go b/hivesim/engineapi.go new file mode 100644 index 0000000000..a777ce332d --- /dev/null +++ b/hivesim/engineapi.go @@ -0,0 +1,27 @@ +package hivesim + +import ( + "fmt" + "net/http" + "time" + + "github.com/ethereum/go-ethereum/rpc" + "github.com/golang-jwt/jwt/v4" +) + +// This is the static secret configured for all execution-layer clients. +var ENGINEAPI_JWT_SECRET = [32]byte{0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x65} + +func jwtAuth(secret [32]byte) rpc.HTTPAuth { + return func(h http.Header) error { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "iat": &jwt.NumericDate{Time: time.Now()}, + }) + s, err := token.SignedString(secret[:]) + if err != nil { + return fmt.Errorf("failed to create JWT token: %w", err) + } + h.Set("Authorization", "Bearer "+s) + return nil + } +} diff --git a/hivesim/testapi.go b/hivesim/testapi.go index 27f97ad2c3..b9ac41de93 100644 --- a/hivesim/testapi.go +++ b/hivesim/testapi.go @@ -1,6 +1,7 @@ package hivesim import ( + "context" "fmt" "net" "os" @@ -159,9 +160,10 @@ type Client struct { Container string IP net.IP - mu sync.Mutex - rpc *rpc.Client - test *T + mu sync.Mutex + rpc *rpc.Client + enginerpc *rpc.Client + test *T } // EnodeURL returns the default peer-to-peer endpoint of the client. @@ -184,6 +186,20 @@ func (c *Client) RPC() *rpc.Client { return c.rpc } +// EngineAPI returns an RPC client connected to an execution-layer client's engine API server. +func (c *Client) EngineAPI() *rpc.Client { + c.mu.Lock() + defer c.mu.Unlock() + + if c.enginerpc != nil { + return c.enginerpc + } + auth := rpc.WithHTTPAuth(jwtAuth(ENGINEAPI_JWT_SECRET)) + url := fmt.Sprintf("http://%v:8551", c.IP) + c.enginerpc, _ = rpc.DialOptions(context.Background(), url, auth) + return c.enginerpc +} + // Exec runs a script in the client container. func (c *Client) Exec(command ...string) (*ExecInfo, error) { return c.test.Sim.ClientExec(c.test.SuiteID, c.test.TestID, c.Container, command) diff --git a/simulators/eth2/engine/scenarios.go b/simulators/eth2/engine/scenarios.go index a44307afc9..5d04331bd8 100644 --- a/simulators/eth2/engine/scenarios.go +++ b/simulators/eth2/engine/scenarios.go @@ -171,7 +171,7 @@ func TestRPCError(t *hivesim.T, env *tn.Environment, t.Fatalf("FAIL: %v", err) } - if err := testnet.VerifyELHeads(ctx); err == nil { + if err := testnet.VerifyELHeads(ctx); err != nil { t.Fatalf("FAIL: Expected different heads after spoof %v", err) } } @@ -1225,7 +1225,7 @@ func SyncingWithInvalidChain( // Block can't contain an executable payload t.Fatalf("FAIL: Head of the chain is not a bellatrix fork block") } - if payload, err := versionedBlock.ExecutionPayload(); err == nil { + if payload, err := versionedBlock.ExecutionPayload(); err != nil { t.Fatalf( "FAIL: error getting execution payload: %v", err, diff --git a/simulators/ethereum/consensus/forks.go b/simulators/ethereum/consensus/forks.go new file mode 100644 index 0000000000..ed74c718e3 --- /dev/null +++ b/simulators/ethereum/consensus/forks.go @@ -0,0 +1,339 @@ +package main + +var envForks = map[string]map[string]int{ + "Frontier": { + "HIVE_FORK_HOMESTEAD": 2000, + "HIVE_FORK_DAO_BLOCK": 2000, + "HIVE_FORK_TANGERINE": 2000, + "HIVE_FORK_SPURIOUS": 2000, + "HIVE_FORK_BYZANTIUM": 2000, + "HIVE_FORK_CONSTANTINOPLE": 2000, + "HIVE_FORK_PETERSBURG": 2000, + "HIVE_FORK_ISTANBUL": 2000, + "HIVE_FORK_BERLIN": 2000, + "HIVE_FORK_LONDON": 2000, + }, + "Homestead": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_DAO_BLOCK": 2000, + "HIVE_FORK_TANGERINE": 2000, + "HIVE_FORK_SPURIOUS": 2000, + "HIVE_FORK_BYZANTIUM": 2000, + "HIVE_FORK_CONSTANTINOPLE": 2000, + "HIVE_FORK_PETERSBURG": 2000, + "HIVE_FORK_ISTANBUL": 2000, + "HIVE_FORK_BERLIN": 2000, + "HIVE_FORK_LONDON": 2000, + }, + "EIP150": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 2000, + "HIVE_FORK_BYZANTIUM": 2000, + "HIVE_FORK_CONSTANTINOPLE": 2000, + "HIVE_FORK_PETERSBURG": 2000, + "HIVE_FORK_ISTANBUL": 2000, + "HIVE_FORK_BERLIN": 2000, + "HIVE_FORK_LONDON": 2000, + }, + "EIP158": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 2000, + "HIVE_FORK_CONSTANTINOPLE": 2000, + "HIVE_FORK_PETERSBURG": 2000, + "HIVE_FORK_ISTANBUL": 2000, + "HIVE_FORK_BERLIN": 2000, + "HIVE_FORK_LONDON": 2000, + }, + "Byzantium": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 0, + "HIVE_FORK_CONSTANTINOPLE": 2000, + "HIVE_FORK_PETERSBURG": 2000, + "HIVE_FORK_ISTANBUL": 2000, + "HIVE_FORK_BERLIN": 2000, + "HIVE_FORK_LONDON": 2000, + }, + "Constantinople": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 0, + "HIVE_FORK_CONSTANTINOPLE": 0, + "HIVE_FORK_PETERSBURG": 2000, + "HIVE_FORK_ISTANBUL": 2000, + "HIVE_FORK_BERLIN": 2000, + "HIVE_FORK_LONDON": 2000, + }, + "ConstantinopleFix": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 0, + "HIVE_FORK_CONSTANTINOPLE": 0, + "HIVE_FORK_PETERSBURG": 0, + "HIVE_FORK_ISTANBUL": 2000, + "HIVE_FORK_BERLIN": 2000, + "HIVE_FORK_LONDON": 2000, + }, + "Istanbul": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 0, + "HIVE_FORK_CONSTANTINOPLE": 0, + "HIVE_FORK_PETERSBURG": 0, + "HIVE_FORK_ISTANBUL": 0, + "HIVE_FORK_BERLIN": 2000, + "HIVE_FORK_LONDON": 2000, + }, + "Berlin": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 0, + "HIVE_FORK_CONSTANTINOPLE": 0, + "HIVE_FORK_PETERSBURG": 0, + "HIVE_FORK_ISTANBUL": 0, + "HIVE_FORK_BERLIN": 0, + "HIVE_FORK_LONDON": 2000, + }, + "FrontierToHomesteadAt5": { + "HIVE_FORK_HOMESTEAD": 5, + "HIVE_FORK_DAO_BLOCK": 2000, + "HIVE_FORK_TANGERINE": 2000, + "HIVE_FORK_SPURIOUS": 2000, + "HIVE_FORK_BYZANTIUM": 2000, + "HIVE_FORK_CONSTANTINOPLE": 2000, + "HIVE_FORK_PETERSBURG": 2000, + "HIVE_FORK_ISTANBUL": 2000, + "HIVE_FORK_BERLIN": 2000, + "HIVE_FORK_LONDON": 2000, + }, + "HomesteadToEIP150At5": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 5, + "HIVE_FORK_SPURIOUS": 2000, + "HIVE_FORK_BYZANTIUM": 2000, + "HIVE_FORK_CONSTANTINOPLE": 2000, + "HIVE_FORK_PETERSBURG": 2000, + "HIVE_FORK_ISTANBUL": 2000, + "HIVE_FORK_BERLIN": 2000, + "HIVE_FORK_LONDON": 2000, + }, + "HomesteadToDaoAt5": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_DAO_BLOCK": 5, + "HIVE_FORK_TANGERINE": 2000, + "HIVE_FORK_SPURIOUS": 2000, + "HIVE_FORK_BYZANTIUM": 2000, + "HIVE_FORK_CONSTANTINOPLE": 2000, + "HIVE_FORK_PETERSBURG": 2000, + "HIVE_FORK_ISTANBUL": 2000, + "HIVE_FORK_BERLIN": 2000, + "HIVE_FORK_LONDON": 2000, + }, + "EIP158ToByzantiumAt5": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 5, + "HIVE_FORK_CONSTANTINOPLE": 2000, + "HIVE_FORK_PETERSBURG": 2000, + "HIVE_FORK_ISTANBUL": 2000, + "HIVE_FORK_BERLIN": 2000, + "HIVE_FORK_LONDON": 2000, + }, + "ByzantiumToConstantinopleAt5": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 0, + "HIVE_FORK_CONSTANTINOPLE": 5, + "HIVE_FORK_PETERSBURG": 2000, + "HIVE_FORK_ISTANBUL": 2000, + "HIVE_FORK_BERLIN": 2000, + "HIVE_FORK_LONDON": 2000, + }, + "ByzantiumToConstantinopleFixAt5": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 0, + "HIVE_FORK_CONSTANTINOPLE": 5, + "HIVE_FORK_PETERSBURG": 5, + "HIVE_FORK_ISTANBUL": 2000, + "HIVE_FORK_BERLIN": 2000, + "HIVE_FORK_LONDON": 2000, + }, + "ConstantinopleFixToIstanbulAt5": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 0, + "HIVE_FORK_CONSTANTINOPLE": 0, + "HIVE_FORK_PETERSBURG": 0, + "HIVE_FORK_ISTANBUL": 5, + "HIVE_FORK_BERLIN": 2000, + "HIVE_FORK_LONDON": 2000, + }, + "IstanbulToBerlinAt5": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 0, + "HIVE_FORK_CONSTANTINOPLE": 0, + "HIVE_FORK_PETERSBURG": 0, + "HIVE_FORK_ISTANBUL": 0, + "HIVE_FORK_BERLIN": 5, + "HIVE_FORK_LONDON": 2000, + }, + "BerlinToLondonAt5": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 0, + "HIVE_FORK_CONSTANTINOPLE": 0, + "HIVE_FORK_PETERSBURG": 0, + "HIVE_FORK_ISTANBUL": 0, + "HIVE_FORK_BERLIN": 0, + "HIVE_FORK_LONDON": 5, + }, + "London": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 0, + "HIVE_FORK_CONSTANTINOPLE": 0, + "HIVE_FORK_PETERSBURG": 0, + "HIVE_FORK_ISTANBUL": 0, + "HIVE_FORK_BERLIN": 0, + "HIVE_FORK_LONDON": 0, + }, + "ArrowGlacierToMergeAtDiffC0000": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 0, + "HIVE_FORK_CONSTANTINOPLE": 0, + "HIVE_FORK_PETERSBURG": 0, + "HIVE_FORK_ISTANBUL": 0, + "HIVE_FORK_BERLIN": 0, + "HIVE_FORK_LONDON": 0, + "HIVE_TERMINAL_TOTAL_DIFFICULTY": 786432, + }, + "ArrowGlacierToParisAtDiffC0000": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 0, + "HIVE_FORK_CONSTANTINOPLE": 0, + "HIVE_FORK_PETERSBURG": 0, + "HIVE_FORK_ISTANBUL": 0, + "HIVE_FORK_BERLIN": 0, + "HIVE_FORK_LONDON": 0, + "HIVE_TERMINAL_TOTAL_DIFFICULTY": 786432, + }, + "Merge": { // Remove once Paris replaces Merge + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 0, + "HIVE_FORK_CONSTANTINOPLE": 0, + "HIVE_FORK_PETERSBURG": 0, + "HIVE_FORK_ISTANBUL": 0, + "HIVE_FORK_BERLIN": 0, + "HIVE_FORK_LONDON": 0, + "HIVE_FORK_MERGE": 0, + "HIVE_TERMINAL_TOTAL_DIFFICULTY": 0, + }, + "Paris": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 0, + "HIVE_FORK_CONSTANTINOPLE": 0, + "HIVE_FORK_PETERSBURG": 0, + "HIVE_FORK_ISTANBUL": 0, + "HIVE_FORK_BERLIN": 0, + "HIVE_FORK_LONDON": 0, + "HIVE_FORK_MERGE": 0, + "HIVE_TERMINAL_TOTAL_DIFFICULTY": 0, + }, + "Shanghai": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 0, + "HIVE_FORK_CONSTANTINOPLE": 0, + "HIVE_FORK_PETERSBURG": 0, + "HIVE_FORK_ISTANBUL": 0, + "HIVE_FORK_BERLIN": 0, + "HIVE_FORK_LONDON": 0, + "HIVE_FORK_MERGE": 0, + "HIVE_TERMINAL_TOTAL_DIFFICULTY": 0, + "HIVE_SHANGHAI_TIMESTAMP": 0, + }, + "MergeToShanghaiAtTime15k": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 0, + "HIVE_FORK_CONSTANTINOPLE": 0, + "HIVE_FORK_PETERSBURG": 0, + "HIVE_FORK_ISTANBUL": 0, + "HIVE_FORK_BERLIN": 0, + "HIVE_FORK_LONDON": 0, + "HIVE_FORK_MERGE": 0, + "HIVE_TERMINAL_TOTAL_DIFFICULTY": 0, + "HIVE_SHANGHAI_TIMESTAMP": 15000, + }, + "ParisToShanghaiAtTime15k": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 0, + "HIVE_FORK_CONSTANTINOPLE": 0, + "HIVE_FORK_PETERSBURG": 0, + "HIVE_FORK_ISTANBUL": 0, + "HIVE_FORK_BERLIN": 0, + "HIVE_FORK_LONDON": 0, + "HIVE_FORK_MERGE": 0, + "HIVE_TERMINAL_TOTAL_DIFFICULTY": 0, + "HIVE_SHANGHAI_TIMESTAMP": 15000, + }, + "Cancun": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 0, + "HIVE_FORK_CONSTANTINOPLE": 0, + "HIVE_FORK_PETERSBURG": 0, + "HIVE_FORK_ISTANBUL": 0, + "HIVE_FORK_BERLIN": 0, + "HIVE_FORK_LONDON": 0, + "HIVE_FORK_MERGE": 0, + "HIVE_TERMINAL_TOTAL_DIFFICULTY": 0, + "HIVE_SHANGHAI_TIMESTAMP": 0, + "HIVE_CANCUN_TIMESTAMP": 0, + }, + "ShanghaiToCancunAtTime15k": { + "HIVE_FORK_HOMESTEAD": 0, + "HIVE_FORK_TANGERINE": 0, + "HIVE_FORK_SPURIOUS": 0, + "HIVE_FORK_BYZANTIUM": 0, + "HIVE_FORK_CONSTANTINOPLE": 0, + "HIVE_FORK_PETERSBURG": 0, + "HIVE_FORK_ISTANBUL": 0, + "HIVE_FORK_BERLIN": 0, + "HIVE_FORK_LONDON": 0, + "HIVE_FORK_MERGE": 0, + "HIVE_TERMINAL_TOTAL_DIFFICULTY": 0, + "HIVE_SHANGHAI_TIMESTAMP": 0, + "HIVE_CANCUN_TIMESTAMP": 15000, + }, +} diff --git a/simulators/ethereum/consensus/main.go b/simulators/ethereum/consensus/main.go index b68ddda3fa..ba854dde64 100644 --- a/simulators/ethereum/consensus/main.go +++ b/simulators/ethereum/consensus/main.go @@ -19,314 +19,6 @@ import ( "github.com/ethereum/hive/hivesim" ) -type envvars map[string]int - -var ruleset = map[string]envvars{ - "Frontier": { - "HIVE_FORK_HOMESTEAD": 2000, - "HIVE_FORK_DAO_BLOCK": 2000, - "HIVE_FORK_TANGERINE": 2000, - "HIVE_FORK_SPURIOUS": 2000, - "HIVE_FORK_BYZANTIUM": 2000, - "HIVE_FORK_CONSTANTINOPLE": 2000, - "HIVE_FORK_PETERSBURG": 2000, - "HIVE_FORK_ISTANBUL": 2000, - "HIVE_FORK_BERLIN": 2000, - "HIVE_FORK_LONDON": 2000, - }, - "Homestead": { - "HIVE_FORK_HOMESTEAD": 0, - "HIVE_FORK_DAO_BLOCK": 2000, - "HIVE_FORK_TANGERINE": 2000, - "HIVE_FORK_SPURIOUS": 2000, - "HIVE_FORK_BYZANTIUM": 2000, - "HIVE_FORK_CONSTANTINOPLE": 2000, - "HIVE_FORK_PETERSBURG": 2000, - "HIVE_FORK_ISTANBUL": 2000, - "HIVE_FORK_BERLIN": 2000, - "HIVE_FORK_LONDON": 2000, - }, - "EIP150": { - "HIVE_FORK_HOMESTEAD": 0, - "HIVE_FORK_TANGERINE": 0, - "HIVE_FORK_SPURIOUS": 2000, - "HIVE_FORK_BYZANTIUM": 2000, - "HIVE_FORK_CONSTANTINOPLE": 2000, - "HIVE_FORK_PETERSBURG": 2000, - "HIVE_FORK_ISTANBUL": 2000, - "HIVE_FORK_BERLIN": 2000, - "HIVE_FORK_LONDON": 2000, - }, - "EIP158": { - "HIVE_FORK_HOMESTEAD": 0, - "HIVE_FORK_TANGERINE": 0, - "HIVE_FORK_SPURIOUS": 0, - "HIVE_FORK_BYZANTIUM": 2000, - "HIVE_FORK_CONSTANTINOPLE": 2000, - "HIVE_FORK_PETERSBURG": 2000, - "HIVE_FORK_ISTANBUL": 2000, - "HIVE_FORK_BERLIN": 2000, - "HIVE_FORK_LONDON": 2000, - }, - "Byzantium": { - "HIVE_FORK_HOMESTEAD": 0, - "HIVE_FORK_TANGERINE": 0, - "HIVE_FORK_SPURIOUS": 0, - "HIVE_FORK_BYZANTIUM": 0, - "HIVE_FORK_CONSTANTINOPLE": 2000, - "HIVE_FORK_PETERSBURG": 2000, - "HIVE_FORK_ISTANBUL": 2000, - "HIVE_FORK_BERLIN": 2000, - "HIVE_FORK_LONDON": 2000, - }, - "Constantinople": { - "HIVE_FORK_HOMESTEAD": 0, - "HIVE_FORK_TANGERINE": 0, - "HIVE_FORK_SPURIOUS": 0, - "HIVE_FORK_BYZANTIUM": 0, - "HIVE_FORK_CONSTANTINOPLE": 0, - "HIVE_FORK_PETERSBURG": 2000, - "HIVE_FORK_ISTANBUL": 2000, - "HIVE_FORK_BERLIN": 2000, - "HIVE_FORK_LONDON": 2000, - }, - "ConstantinopleFix": { - "HIVE_FORK_HOMESTEAD": 0, - "HIVE_FORK_TANGERINE": 0, - "HIVE_FORK_SPURIOUS": 0, - "HIVE_FORK_BYZANTIUM": 0, - "HIVE_FORK_CONSTANTINOPLE": 0, - "HIVE_FORK_PETERSBURG": 0, - "HIVE_FORK_ISTANBUL": 2000, - "HIVE_FORK_BERLIN": 2000, - "HIVE_FORK_LONDON": 2000, - }, - "Istanbul": { - "HIVE_FORK_HOMESTEAD": 0, - "HIVE_FORK_TANGERINE": 0, - "HIVE_FORK_SPURIOUS": 0, - "HIVE_FORK_BYZANTIUM": 0, - "HIVE_FORK_CONSTANTINOPLE": 0, - "HIVE_FORK_PETERSBURG": 0, - "HIVE_FORK_ISTANBUL": 0, - "HIVE_FORK_BERLIN": 2000, - "HIVE_FORK_LONDON": 2000, - }, - "Berlin": { - "HIVE_FORK_HOMESTEAD": 0, - "HIVE_FORK_TANGERINE": 0, - "HIVE_FORK_SPURIOUS": 0, - "HIVE_FORK_BYZANTIUM": 0, - "HIVE_FORK_CONSTANTINOPLE": 0, - "HIVE_FORK_PETERSBURG": 0, - "HIVE_FORK_ISTANBUL": 0, - "HIVE_FORK_BERLIN": 0, - "HIVE_FORK_LONDON": 2000, - }, - "FrontierToHomesteadAt5": { - "HIVE_FORK_HOMESTEAD": 5, - "HIVE_FORK_DAO_BLOCK": 2000, - "HIVE_FORK_TANGERINE": 2000, - "HIVE_FORK_SPURIOUS": 2000, - "HIVE_FORK_BYZANTIUM": 2000, - "HIVE_FORK_CONSTANTINOPLE": 2000, - "HIVE_FORK_PETERSBURG": 2000, - "HIVE_FORK_ISTANBUL": 2000, - "HIVE_FORK_BERLIN": 2000, - "HIVE_FORK_LONDON": 2000, - }, - "HomesteadToEIP150At5": { - "HIVE_FORK_HOMESTEAD": 0, - // "HIVE_FORK_DAO_BLOCK": 2000, - "HIVE_FORK_TANGERINE": 5, - "HIVE_FORK_SPURIOUS": 2000, - "HIVE_FORK_BYZANTIUM": 2000, - "HIVE_FORK_CONSTANTINOPLE": 2000, - "HIVE_FORK_PETERSBURG": 2000, - "HIVE_FORK_ISTANBUL": 2000, - "HIVE_FORK_BERLIN": 2000, - "HIVE_FORK_LONDON": 2000, - }, - "HomesteadToDaoAt5": { - "HIVE_FORK_HOMESTEAD": 0, - "HIVE_FORK_DAO_BLOCK": 5, - "HIVE_FORK_TANGERINE": 2000, - "HIVE_FORK_SPURIOUS": 2000, - "HIVE_FORK_BYZANTIUM": 2000, - "HIVE_FORK_CONSTANTINOPLE": 2000, - "HIVE_FORK_PETERSBURG": 2000, - "HIVE_FORK_ISTANBUL": 2000, - "HIVE_FORK_BERLIN": 2000, - "HIVE_FORK_LONDON": 2000, - }, - "EIP158ToByzantiumAt5": { - "HIVE_FORK_HOMESTEAD": 0, - //"HIVE_FORK_DAO_BLOCK": 2000, - "HIVE_FORK_TANGERINE": 0, - "HIVE_FORK_SPURIOUS": 0, - "HIVE_FORK_BYZANTIUM": 5, - "HIVE_FORK_CONSTANTINOPLE": 2000, - "HIVE_FORK_PETERSBURG": 2000, - "HIVE_FORK_ISTANBUL": 2000, - "HIVE_FORK_BERLIN": 2000, - "HIVE_FORK_LONDON": 2000, - }, - "ByzantiumToConstantinopleAt5": { - "HIVE_FORK_HOMESTEAD": 0, - //"HIVE_FORK_DAO_BLOCK": 2000, - "HIVE_FORK_TANGERINE": 0, - "HIVE_FORK_SPURIOUS": 0, - "HIVE_FORK_BYZANTIUM": 0, - "HIVE_FORK_CONSTANTINOPLE": 5, - "HIVE_FORK_PETERSBURG": 2000, - "HIVE_FORK_ISTANBUL": 2000, - "HIVE_FORK_BERLIN": 2000, - "HIVE_FORK_LONDON": 2000, - }, - "ByzantiumToConstantinopleFixAt5": { - "HIVE_FORK_HOMESTEAD": 0, - //"HIVE_FORK_DAO_BLOCK": 2000, - "HIVE_FORK_TANGERINE": 0, - "HIVE_FORK_SPURIOUS": 0, - "HIVE_FORK_BYZANTIUM": 0, - "HIVE_FORK_CONSTANTINOPLE": 5, - "HIVE_FORK_PETERSBURG": 5, - "HIVE_FORK_ISTANBUL": 2000, - "HIVE_FORK_BERLIN": 2000, - "HIVE_FORK_LONDON": 2000, - }, - "ConstantinopleFixToIstanbulAt5": { - "HIVE_FORK_HOMESTEAD": 0, - //"HIVE_FORK_DAO_BLOCK": 2000, - "HIVE_FORK_TANGERINE": 0, - "HIVE_FORK_SPURIOUS": 0, - "HIVE_FORK_BYZANTIUM": 0, - "HIVE_FORK_CONSTANTINOPLE": 0, - "HIVE_FORK_PETERSBURG": 0, - "HIVE_FORK_ISTANBUL": 5, - "HIVE_FORK_BERLIN": 2000, - "HIVE_FORK_LONDON": 2000, - }, - "IstanbulToBerlinAt5": { - "HIVE_FORK_HOMESTEAD": 0, - //"HIVE_FORK_DAO_BLOCK": 2000, - "HIVE_FORK_TANGERINE": 0, - "HIVE_FORK_SPURIOUS": 0, - "HIVE_FORK_BYZANTIUM": 0, - "HIVE_FORK_CONSTANTINOPLE": 0, - "HIVE_FORK_PETERSBURG": 0, - "HIVE_FORK_ISTANBUL": 0, - "HIVE_FORK_BERLIN": 5, - "HIVE_FORK_LONDON": 2000, - }, - "BerlinToLondonAt5": { - "HIVE_FORK_HOMESTEAD": 0, - //"HIVE_FORK_DAO_BLOCK": 2000, - "HIVE_FORK_TANGERINE": 0, - "HIVE_FORK_SPURIOUS": 0, - "HIVE_FORK_BYZANTIUM": 0, - "HIVE_FORK_CONSTANTINOPLE": 0, - "HIVE_FORK_PETERSBURG": 0, - "HIVE_FORK_ISTANBUL": 0, - "HIVE_FORK_BERLIN": 0, - "HIVE_FORK_LONDON": 5, - }, - "London": { - "HIVE_FORK_HOMESTEAD": 0, - "HIVE_FORK_TANGERINE": 0, - "HIVE_FORK_SPURIOUS": 0, - "HIVE_FORK_BYZANTIUM": 0, - "HIVE_FORK_CONSTANTINOPLE": 0, - "HIVE_FORK_PETERSBURG": 0, - "HIVE_FORK_ISTANBUL": 0, - "HIVE_FORK_BERLIN": 0, - "HIVE_FORK_LONDON": 0, - }, - "ArrowGlacierToMergeAtDiffC0000": { - "HIVE_FORK_HOMESTEAD": 0, - "HIVE_FORK_TANGERINE": 0, - "HIVE_FORK_SPURIOUS": 0, - "HIVE_FORK_BYZANTIUM": 0, - "HIVE_FORK_CONSTANTINOPLE": 0, - "HIVE_FORK_PETERSBURG": 0, - "HIVE_FORK_ISTANBUL": 0, - "HIVE_FORK_BERLIN": 0, - "HIVE_FORK_LONDON": 0, - "HIVE_TERMINAL_TOTAL_DIFFICULTY": 786432, - }, - "Merge": { - "HIVE_FORK_HOMESTEAD": 0, - "HIVE_FORK_TANGERINE": 0, - "HIVE_FORK_SPURIOUS": 0, - "HIVE_FORK_BYZANTIUM": 0, - "HIVE_FORK_CONSTANTINOPLE": 0, - "HIVE_FORK_PETERSBURG": 0, - "HIVE_FORK_ISTANBUL": 0, - "HIVE_FORK_BERLIN": 0, - "HIVE_FORK_LONDON": 0, - "HIVE_FORK_MERGE": 0, - "HIVE_TERMINAL_TOTAL_DIFFICULTY": 0, - }, - "Shanghai": { - "HIVE_FORK_HOMESTEAD": 0, - "HIVE_FORK_TANGERINE": 0, - "HIVE_FORK_SPURIOUS": 0, - "HIVE_FORK_BYZANTIUM": 0, - "HIVE_FORK_CONSTANTINOPLE": 0, - "HIVE_FORK_PETERSBURG": 0, - "HIVE_FORK_ISTANBUL": 0, - "HIVE_FORK_BERLIN": 0, - "HIVE_FORK_LONDON": 0, - "HIVE_FORK_MERGE": 0, - "HIVE_TERMINAL_TOTAL_DIFFICULTY": 0, - "HIVE_SHANGHAI_TIMESTAMP": 0, - }, - "MergeToShanghaiAtTime15k": { - "HIVE_FORK_HOMESTEAD": 0, - "HIVE_FORK_TANGERINE": 0, - "HIVE_FORK_SPURIOUS": 0, - "HIVE_FORK_BYZANTIUM": 0, - "HIVE_FORK_CONSTANTINOPLE": 0, - "HIVE_FORK_PETERSBURG": 0, - "HIVE_FORK_ISTANBUL": 0, - "HIVE_FORK_BERLIN": 0, - "HIVE_FORK_LONDON": 0, - "HIVE_FORK_MERGE": 0, - "HIVE_TERMINAL_TOTAL_DIFFICULTY": 0, - "HIVE_SHANGHAI_TIMESTAMP": 15000, - }, - "Cancun": { - "HIVE_FORK_HOMESTEAD": 0, - "HIVE_FORK_TANGERINE": 0, - "HIVE_FORK_SPURIOUS": 0, - "HIVE_FORK_BYZANTIUM": 0, - "HIVE_FORK_CONSTANTINOPLE": 0, - "HIVE_FORK_PETERSBURG": 0, - "HIVE_FORK_ISTANBUL": 0, - "HIVE_FORK_BERLIN": 0, - "HIVE_FORK_LONDON": 0, - "HIVE_FORK_MERGE": 0, - "HIVE_TERMINAL_TOTAL_DIFFICULTY": 0, - "HIVE_SHANGHAI_TIMESTAMP": 0, - "HIVE_CANCUN_TIMESTAMP": 0, - }, - "ShanghaiToCancunAtTime15k": { - "HIVE_FORK_HOMESTEAD": 0, - "HIVE_FORK_TANGERINE": 0, - "HIVE_FORK_SPURIOUS": 0, - "HIVE_FORK_BYZANTIUM": 0, - "HIVE_FORK_CONSTANTINOPLE": 0, - "HIVE_FORK_PETERSBURG": 0, - "HIVE_FORK_ISTANBUL": 0, - "HIVE_FORK_BERLIN": 0, - "HIVE_FORK_LONDON": 0, - "HIVE_FORK_MERGE": 0, - "HIVE_TERMINAL_TOTAL_DIFFICULTY": 0, - "HIVE_SHANGHAI_TIMESTAMP": 0, - "HIVE_CANCUN_TIMESTAMP": 15000, - }, -} - func main() { suite := hivesim.Suite{ Name: "consensus", @@ -470,7 +162,7 @@ type testcase struct { // validate returns error if the test's chain rules are not supported. func (tc *testcase) validate() error { net := tc.blockTest.json.Network - if _, exist := ruleset[net]; !exist { + if _, exist := envForks[net]; !exist { return fmt.Errorf("network `%v` not defined in ruleset", net) } return nil @@ -539,9 +231,9 @@ func (tc *testcase) run(t *hivesim.T) { // updateEnv sets environment variables from the test func (tc *testcase) updateEnv(env hivesim.Params) { - // Environment variables for rules. - rules := ruleset[tc.blockTest.json.Network] - for k, v := range rules { + // Environment variables for fork rules + forks := envForks[tc.blockTest.json.Network] + for k, v := range forks { env[k] = fmt.Sprintf("%d", v) } // Possibly disable POW. @@ -553,15 +245,16 @@ func (tc *testcase) updateEnv(env hivesim.Params) { // toGethGenesis creates the genesis specification from a test block. func toGethGenesis(test *btJSON) *core.Genesis { genesis := &core.Genesis{ - Nonce: test.Genesis.Nonce.Uint64(), - Timestamp: test.Genesis.Timestamp.Uint64(), - ExtraData: test.Genesis.ExtraData, - GasLimit: test.Genesis.GasLimit, - Difficulty: test.Genesis.Difficulty, - Mixhash: test.Genesis.MixHash, - Coinbase: test.Genesis.Coinbase, - Alloc: test.Pre, - BaseFee: test.Genesis.BaseFee, + Nonce: test.Genesis.Nonce.Uint64(), + Timestamp: test.Genesis.Timestamp.Uint64(), + ExtraData: test.Genesis.ExtraData, + GasLimit: test.Genesis.GasLimit, + Difficulty: test.Genesis.Difficulty, + Mixhash: test.Genesis.MixHash, + Coinbase: test.Genesis.Coinbase, + Alloc: test.Pre, + BaseFee: test.Genesis.BaseFee, + ExcessBlobGas: test.Genesis.ExcessBlobGas, } return genesis } @@ -651,7 +344,15 @@ func compareGenesis(have string, want btHeader) (string, error) { cmp(haveGenesis.GasUsed, want.GasUsed, "gasUsed") cmp(haveGenesis.Nonce, want.Nonce, "nonce") cmp(haveGenesis.BaseFee, want.BaseFee, "baseFeePerGas") - cmp(haveGenesis.ExcessBlobGas, want.ExcessBlobGas, "excessBlobGas") - cmp(haveGenesis.BlobGasUsed, want.BlobGasUsed, "blobGasUsed") + if haveGenesis.ExcessBlobGas != nil && want.ExcessBlobGas != nil { + cmp(*haveGenesis.ExcessBlobGas, *want.ExcessBlobGas, "excessBlobGas") + } else { + cmp(haveGenesis.ExcessBlobGas, want.ExcessBlobGas, "excessBlobGas") + } + if haveGenesis.BlobGasUsed != nil && want.BlobGasUsed != nil { + cmp(*haveGenesis.BlobGasUsed, *want.BlobGasUsed, "blobGasUsed") + } else { + cmp(haveGenesis.BlobGasUsed, want.BlobGasUsed, "blobGasUsed") + } return output, nil } diff --git a/simulators/ethereum/consensus/types.go b/simulators/ethereum/consensus/types.go index 4668f604e2..48c51ed594 100644 --- a/simulators/ethereum/consensus/types.go +++ b/simulators/ethereum/consensus/types.go @@ -55,8 +55,8 @@ type btHeader struct { GasUsed uint64 `json:"gasUsed"` Timestamp *big.Int `json:"timestamp"` BaseFee *big.Int `json:"baseFeePerGas"` // EIP-1559 - ExcessBlobGas uint64 `json:"excessBlobGas"` // EIP-4844 - BlobGasUsed uint64 `json:"blobGasUsed"` // EIP-4844 + ExcessBlobGas *uint64 `json:"excessBlobGas"` // EIP-4844 + BlobGasUsed *uint64 `json:"blobGasUsed"` // EIP-4844 } func (b *btHeader) UnmarshalJSON(input []byte) error { @@ -152,10 +152,12 @@ func (b *btHeader) UnmarshalJSON(input []byte) error { b.BaseFee = (*big.Int)(dec.BaseFee) } if dec.ExcessBlobGas != nil { - b.ExcessBlobGas = uint64(*dec.ExcessBlobGas) + ebg := uint64(*dec.ExcessBlobGas) + b.ExcessBlobGas = &ebg } if dec.BlobGasUsed != nil { - b.BlobGasUsed = uint64(*dec.BlobGasUsed) + bgu := uint64(*dec.BlobGasUsed) + b.BlobGasUsed = &bgu } return nil } diff --git a/simulators/ethereum/engine/suites/cancun/README.md b/simulators/ethereum/engine/suites/cancun/README.md index 9ce11310e3..bfe2147a67 100644 --- a/simulators/ethereum/engine/suites/cancun/README.md +++ b/simulators/ethereum/engine/suites/cancun/README.md @@ -1,5 +1,5 @@ # Cancun Engine API Testing -This test suite verifies behavior of the Engine API on the transtion to and after the Cancun fork: +This test suite verifies behavior of the Engine API on the transition to and after the Cancun fork: https://github.com/ethereum/execution-apis/blob/main/src/engine/cancun.md diff --git a/simulators/ethereum/engine/suites/cancun/tests.go b/simulators/ethereum/engine/suites/cancun/tests.go index eafc8a218a..cd3e00824f 100644 --- a/simulators/ethereum/engine/suites/cancun/tests.go +++ b/simulators/ethereum/engine/suites/cancun/tests.go @@ -541,7 +541,7 @@ var Tests = []test.Spec{ // - Payload Attributes uses Shanghai timestamp // - Payload Attributes Beacon Root is null // - // Verify that client returns INVALID_PARAMS_ERROR. + // Verify that client returns INVALID_PAYLOAD_ATTRIBUTES. // `, // MainFork: config.Cancun, // ForkHeight: 2, @@ -551,12 +551,12 @@ var Tests = []test.Spec{ // NewPayloads{ // FcUOnPayloadRequest: &helper.UpgradeForkchoiceUpdatedVersion{ // ForkchoiceUpdatedCustomizer: &helper.BaseForkchoiceUpdatedCustomizer{ - // ExpectedError: globals.INVALID_PARAMS_ERROR, + // ExpectedError: globals.INVALID_PAYLOAD_ATTRIBUTES, // }, // }, // ExpectationDescription: fmt.Sprintf(` - // ForkchoiceUpdatedV3 before Cancun with any null field must return INVALID_PARAMS_ERROR (code %d) - // `, *globals.INVALID_PARAMS_ERROR), + // ForkchoiceUpdatedV3 before Cancun with any null field must return INVALID_PAYLOAD_ATTRIBUTES (code %d) + // `, *globals.INVALID_PAYLOAD_ATTRIBUTES), // }, // }, //}, @@ -595,13 +595,13 @@ var Tests = []test.Spec{ //// ForkchoiceUpdatedV2 before cancun with beacon root //&CancunBaseSpec{ // BaseSpec: test.BaseSpec{ - // Name: "ForkchoiceUpdatedV2 To Request Shanghai Payload, Non-Null Beacon Root ", + // Name: "ForkchoiceUpdatedV2 To Request Shanghai Payload, Non-Null Beacon Root", // About: ` // Test sending ForkchoiceUpdatedV2 to request a Shanghai payload: // - Payload Attributes uses Shanghai timestamp // - Payload Attributes Beacon Root is non-null // - // Verify that client returns INVALID_PARAMS_ERROR. + // Verify that client returns INVALID_PAYLOAD_ATTRIBUTES. // `, // MainFork: config.Cancun, // ForkHeight: 2, @@ -613,11 +613,11 @@ var Tests = []test.Spec{ // PayloadAttributesCustomizer: &helper.BasePayloadAttributesCustomizer{ // BeaconRoot: &(common.Hash{}), // }, - // ExpectedError: globals.INVALID_PARAMS_ERROR, + // ExpectedError: globals.INVALID_PAYLOAD_ATTRIBUTES, // }, // ExpectationDescription: fmt.Sprintf(` - // ForkchoiceUpdatedV2 before Cancun with beacon root field must return INVALID_PARAMS_ERROR (code %d) - // `, *globals.INVALID_PARAMS_ERROR), + // ForkchoiceUpdatedV2 before Cancun with beacon root field must return INVALID_PAYLOAD_ATTRIBUTES (code %d) + // `, *globals.INVALID_PAYLOAD_ATTRIBUTES), // }, // }, //}, @@ -631,7 +631,7 @@ var Tests = []test.Spec{ // - Payload Attributes uses Cancun timestamp // - Payload Attributes Beacon Root is non-null // - // Verify that client returns INVALID_PARAMS_ERROR. + // Verify that client returns INVALID_PAYLOAD_ATTRIBUTES. // `, // MainFork: config.Cancun, // ForkHeight: 1, @@ -644,12 +644,12 @@ var Tests = []test.Spec{ // PayloadAttributesCustomizer: &helper.BasePayloadAttributesCustomizer{ // BeaconRoot: &(common.Hash{}), // }, - // ExpectedError: globals.INVALID_PARAMS_ERROR, + // ExpectedError: globals.INVALID_PAYLOAD_ATTRIBUTES, // }, // }, // ExpectationDescription: fmt.Sprintf(` - // ForkchoiceUpdatedV2 after Cancun with beacon root field must return INVALID_PARAMS_ERROR (code %d) - // `, *globals.INVALID_PARAMS_ERROR), + // ForkchoiceUpdatedV2 after Cancun with beacon root field must return INVALID_PAYLOAD_ATTRIBUTES (code %d) + // `, *globals.INVALID_PAYLOAD_ATTRIBUTES), // }, // }, //}, diff --git a/simulators/ethereum/engine/suites/engine/forkchoice.go b/simulators/ethereum/engine/suites/engine/forkchoice.go index 81ef794143..bc90020733 100644 --- a/simulators/ethereum/engine/suites/engine/forkchoice.go +++ b/simulators/ethereum/engine/suites/engine/forkchoice.go @@ -7,6 +7,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/hive/simulators/ethereum/engine/clmock" "github.com/ethereum/hive/simulators/ethereum/engine/config" + "github.com/ethereum/hive/simulators/ethereum/engine/globals" "github.com/ethereum/hive/simulators/ethereum/engine/helper" "github.com/ethereum/hive/simulators/ethereum/engine/test" typ "github.com/ethereum/hive/simulators/ethereum/engine/types" @@ -79,7 +80,7 @@ func (tc InconsistentForkchoiceTest) Execute(t *test.Env) { inconsistentFcU.FinalizedBlockHash = alternativePayloads[len(canonicalPayloads)-3].BlockHash } r := t.TestEngine.TestEngineForkchoiceUpdated(&inconsistentFcU, nil, t.CLMock.LatestPayloadBuilt.Timestamp) - r.ExpectError() + r.ExpectErrorCode(*globals.INVALID_FORKCHOICE_STATE) // Return to the canonical chain r = t.TestEngine.TestEngineForkchoiceUpdated(&t.CLMock.LatestForkchoice, nil, t.CLMock.LatestPayloadBuilt.Timestamp) diff --git a/simulators/ethereum/engine/suites/engine/payload_attributes.go b/simulators/ethereum/engine/suites/engine/payload_attributes.go index 0f246a50ce..540159b2a1 100644 --- a/simulators/ethereum/engine/suites/engine/payload_attributes.go +++ b/simulators/ethereum/engine/suites/engine/payload_attributes.go @@ -5,6 +5,7 @@ import ( "github.com/ethereum/hive/simulators/ethereum/engine/clmock" "github.com/ethereum/hive/simulators/ethereum/engine/config" + "github.com/ethereum/hive/simulators/ethereum/engine/globals" "github.com/ethereum/hive/simulators/ethereum/engine/helper" "github.com/ethereum/hive/simulators/ethereum/engine/test" ) @@ -14,7 +15,6 @@ type InvalidPayloadAttributesTest struct { Description string Customizer helper.PayloadAttributesCustomizer Syncing bool - ErrorOnSync bool } func (s InvalidPayloadAttributesTest) WithMainFork(fork config.Fork) test.Spec { @@ -69,15 +69,11 @@ func (tc InvalidPayloadAttributesTest) Execute(t *test.Env) { if tc.Syncing { // If we are SYNCING, the outcome should be SYNCING regardless of the validity of the payload atttributes r := t.TestEngine.TestEngineForkchoiceUpdated(&fcu, attr, t.CLMock.LatestPayloadBuilt.Timestamp) - if tc.ErrorOnSync { - r.ExpectError() - } else { - r.ExpectPayloadStatus(test.Syncing) - r.ExpectPayloadID(nil) - } + r.ExpectPayloadStatus(test.Syncing) + r.ExpectPayloadID(nil) } else { r := t.TestEngine.TestEngineForkchoiceUpdated(&fcu, attr, t.CLMock.LatestPayloadBuilt.Timestamp) - r.ExpectError() + r.ExpectErrorCode(*globals.INVALID_PAYLOAD_ATTRIBUTES) // Check that the forkchoice was applied, regardless of the error s := t.TestEngine.TestHeaderByNumber(Head) diff --git a/simulators/ethereum/engine/test/spec.go b/simulators/ethereum/engine/test/spec.go index e321fd54bc..caffdad900 100644 --- a/simulators/ethereum/engine/test/spec.go +++ b/simulators/ethereum/engine/test/spec.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/ethereum/hive/simulators/ethereum/engine/client" "math/big" + "time" "github.com/ethereum/hive/simulators/ethereum/engine/clmock" "github.com/ethereum/hive/simulators/ethereum/engine/config" @@ -64,6 +65,9 @@ type BaseSpec struct { // CL Mocker configuration for time increments BlockTimestampIncrement uint64 + // CL Mocker configuration for payload delay in seconds + GetPayloadDelay uint64 + // CL Mocker configuration for slots to `safe` and `finalized` respectively SlotsToSafe *big.Int SlotsToFinalized *big.Int @@ -118,6 +122,9 @@ func (s BaseSpec) ConfigureCLMock(cl *clmock.CLMocker) { cl.SafeSlotsToImportOptimistically = s.SafeSlotsToImportOptimistically } cl.BlockTimestampIncrement = new(big.Int).SetUint64(s.GetBlockTimeIncrements()) + if s.GetPayloadDelay != 0 { + cl.PayloadProductionClientDelay = time.Duration(s.GetPayloadDelay) * time.Second + } } func (s BaseSpec) GetAbout() string { diff --git a/simulators/ethereum/pyspec/Dockerfile b/simulators/ethereum/pyspec/Dockerfile index 2c5c5cedb1..f407236d8c 100644 --- a/simulators/ethereum/pyspec/Dockerfile +++ b/simulators/ethereum/pyspec/Dockerfile @@ -21,9 +21,9 @@ COPY --from=builder /source/pyspec/pyspec . # To run locally generated fixtures, comment the following RUN lines and # uncomment the ADD line. # Download the latest fixture release. -RUN wget https://github.com/ethereum/execution-spec-tests/releases/latest/download/fixtures_develop_hive.tar.gz -RUN tar -xzvf fixtures_develop_hive.tar.gz -RUN mv fixtures /fixtures +RUN wget https://github.com/ethereum/execution-spec-tests/releases/latest/download/fixtures_develop.tar.gz +RUN tar -xzvf fixtures_develop.tar.gz +RUN mv fixtures/blockchain_tests_hive /fixtures # ADD ./pyspec/fixtures /fixtures diff --git a/simulators/ethereum/pyspec/main.go b/simulators/ethereum/pyspec/main.go index e725efb987..72924f4e9c 100644 --- a/simulators/ethereum/pyspec/main.go +++ b/simulators/ethereum/pyspec/main.go @@ -68,21 +68,21 @@ func fixtureRunner(t *hivesim.T) { // spawn `parallelism` workers to run fixtures against clients var wg sync.WaitGroup - var testCh = make(chan *testcase) + var testCh = make(chan *TestCase) wg.Add(parallelism) for i := 0; i < parallelism; i++ { go func() { defer wg.Done() for test := range testCh { t.Run(hivesim.TestSpec{ - Name: test.name, + Name: test.Name, Description: ("Test Link: " + - repoLink(test.filepath)), + repoLink(test.FilePath)), Run: test.run, AlwaysRun: false, }) - if test.failedErr != nil { - failedTests[test.clientType+"/"+test.name] = test.failedErr + if test.FailedErr != nil { + failedTests[test.ClientType+"/"+test.Name] = test.FailedErr } } }() @@ -92,13 +92,13 @@ func fixtureRunner(t *hivesim.T) { re := regexp.MustCompile(testPattern) // deliver and run test cases against each client - loadFixtureTests(t, fileRoot, re, func(tc testcase) { + loadFixtureTests(t, fileRoot, re, func(tc TestCase) { for _, client := range clientTypes { if !client.HasRole("eth1") { continue } tc := tc // shallow copy - tc.clientType = client.Name + tc.ClientType = client.Name testCh <- &tc } }) diff --git a/simulators/ethereum/pyspec/runner.go b/simulators/ethereum/pyspec/runner.go index 275ff1312b..41e2946e63 100644 --- a/simulators/ethereum/pyspec/runner.go +++ b/simulators/ethereum/pyspec/runner.go @@ -2,7 +2,6 @@ package main import ( "context" - "errors" "fmt" "io/fs" "math/big" @@ -11,21 +10,19 @@ import ( "strings" "time" - api "github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/rpc" - "github.com/ethereum/go-ethereum/tests" "github.com/ethereum/hive/hivesim" "github.com/ethereum/hive/simulators/ethereum/engine/client/hive_rpc" "github.com/ethereum/hive/simulators/ethereum/engine/globals" +) - typ "github.com/ethereum/hive/simulators/ethereum/engine/types" +var ( + SyncTimeout = 10 * time.Second ) // loadFixtureTests extracts tests from fixture.json files in a given directory, // creates a testcase for each test, and passes the testcase struct to fn. -func loadFixtureTests(t *hivesim.T, root string, re *regexp.Regexp, fn func(testcase)) { +func loadFixtureTests(t *hivesim.T, root string, re *regexp.Regexp, fn func(TestCase)) { filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { // check file is actually a fixture if err != nil { @@ -40,8 +37,8 @@ func loadFixtureTests(t *hivesim.T, root string, re *regexp.Regexp, fn func(test return nil } - // extract fixture.json tests (multiple forks) into fixtureTest structs - var fixtureTests map[string]fixtureTest + // extract fixture.json tests (multiple forks) into fixture structs + var fixtureTests map[string]*Fixture if err := common.LoadJSON(path, &fixtureTests); err != nil { t.Logf("invalid test file: %v, unable to load json", err) return nil @@ -50,25 +47,20 @@ func loadFixtureTests(t *hivesim.T, root string, re *regexp.Regexp, fn func(test // create testcase structure from fixtureTests for name, fixture := range fixtureTests { // skip networks post merge or not supported - network := fixture.json.Fork + network := fixture.Fork if _, exist := envForks[network]; !exist { continue } // define testcase (tc) struct with initial fields - tc := testcase{ - fixture: fixture, - name: path[10:len(path)-5] + "/" + name, - filepath: path, + tc := TestCase{ + Name: path[10:len(path)-5] + "/" + name, + FilePath: path, + Fixture: fixture, } // match test case name against regex if provided - if !re.MatchString(tc.name) { + if !re.MatchString(tc.Name) { continue } - // extract genesis, payloads & post allocation field to tc - if err := tc.extractFixtureFields(fixture.json); err != nil { - t.Logf("test %v / %v: unable to extract fixture fields: %v", d.Name(), name, err) - tc.failedErr = fmt.Errorf("unable to extract fixture fields: %v", err) - } // feed tc to single worker within fixtureRunner() fn(tc) } @@ -80,12 +72,13 @@ func loadFixtureTests(t *hivesim.T, root string, re *regexp.Regexp, fn func(test // fixtureRunner, all testcase payloads are sent and executed using the EngineAPI. for // verification all fixture nonce, balance and storage values are checked against the // response received from the lastest block. -func (tc *testcase) run(t *hivesim.T) { +func (tc *TestCase) run(t *hivesim.T) { start := time.Now() + tc.FailCallback = t t.Log("setting variables required for starting client.") engineStarter := hive_rpc.HiveRPCEngineStarter{ - ClientType: tc.clientType, + ClientType: tc.ClientType, EnginePort: globals.EnginePortHTTP, EthPort: globals.EthPortHTTP, JWTSecret: globals.DefaultJwtTokenSecretBytes, @@ -99,123 +92,110 @@ func (tc *testcase) run(t *hivesim.T) { tc.updateEnv(env) t0 := time.Now() // If test is already failed, don't bother spinning up a client - if tc.failedErr != nil { - t.Errorf("test failed early: %v", tc.failedErr) - return + if tc.FailedErr != nil { + t.Fatalf("test failed early: %v", tc.FailedErr) } // start client (also creates an engine RPC client internally) t.Log("starting client with Engine API.") - engineClient, err := engineStarter.StartClient(t, ctx, tc.genesis, env, nil) + engineClient, err := engineStarter.StartClient(t, ctx, tc.Genesis(), env, nil) if err != nil { - tc.failedErr = err - t.Fatalf("can't start client with Engine API: %v", err) + tc.Fatalf("can't start client with Engine API: %v", err) } // verify genesis hash matches that of the fixture genesisBlock, err := engineClient.BlockByNumber(ctx, big.NewInt(0)) if err != nil { - tc.failedErr = err - t.Fatalf("unable to get genesis block: %v", err) + tc.Fatalf("unable to get genesis block: %v", err) } - if genesisBlock.Hash() != tc.fixture.json.Genesis.Hash { - tc.failedErr = errors.New("genesis hash mismatch") - t.Fatalf("genesis hash mismatch") + if genesisBlock.Hash() != tc.GenesisBlock.Hash { + tc.Fatalf("genesis hash mismatch") } t1 := time.Now() // send payloads and check response - latestValidHash := common.Hash{} - for _, engineNewPayload := range tc.engineNewPayloads { - plStatus, plErr := engineClient.NewPayload( - context.Background(), - int(engineNewPayload.Version), - engineNewPayload.HiveExecutionPayload, - ) - // check for rpc errors and compare error codes - errCode := int(engineNewPayload.ErrorCode) - if errCode != 0 { - checkRPCErrors(plErr, errCode, t, tc) - continue - } - // set expected payload return status - expectedStatus := "VALID" - if !engineNewPayload.Valid { - expectedStatus = "INVALID" - } - // check payload status matches expected - if plStatus.Status != expectedStatus { - tc.failedErr = fmt.Errorf("payload status mismatch: client returned %v and fixture expected %v", plStatus.Status, expectedStatus) - t.Fatalf("payload status mismatch: client returned %v fixture expected %v", plStatus.Status, expectedStatus) + var latestValidPayload *EngineNewPayload + for _, engineNewPayload := range tc.EngineNewPayloads { + engineNewPayload := engineNewPayload + if syncing, err := engineNewPayload.ExecuteValidate( + ctx, + engineClient, + ); err != nil { + tc.Fatalf("Payload validation error: %v", err) + } else if syncing { + tc.Fatalf("Payload validation failed (not synced)") } // update latest valid block hash if payload status is VALID - if plStatus.Status == "VALID" { - latestValidHash = *plStatus.LatestValidHash + if engineNewPayload.Valid() { + latestValidPayload = engineNewPayload } } t2 := time.Now() // only update head of beacon chain if valid response occurred - if latestValidHash != (common.Hash{}) { - // update with latest valid response - fcState := &api.ForkchoiceStateV1{HeadBlockHash: latestValidHash} - if _, fcErr := engineClient.ForkchoiceUpdated(ctx, int(tc.fixture.json.EngineFcuVersion), fcState, nil); fcErr != nil { - tc.failedErr = fcErr - t.Fatalf("unable to update head of beacon chain in test %s: %v ", tc.name, fcErr) + if latestValidPayload != nil { + if syncing, err := latestValidPayload.ForkchoiceValidate(ctx, engineClient, tc.EngineFcuVersion); err != nil { + tc.Fatalf("unable to update head of chain: %v", err) + } else if syncing { + tc.Fatalf("forkchoice update failed (not synced)") } } t3 := time.Now() - - // check nonce, balance & storage of accounts in final block against fixture values - for account, genesisAccount := range *tc.postAlloc { - // get nonce & balance from last block (end of test execution) - gotNonce, errN := engineClient.NonceAt(ctx, account, nil) - gotBalance, errB := engineClient.BalanceAt(ctx, account, nil) - if errN != nil { - tc.failedErr = errN - t.Errorf("unable to call nonce from account: %v, in test %s: %v", account, tc.name, errN) - } else if errB != nil { - tc.failedErr = errB - t.Errorf("unable to call balance from account: %v, in test %s: %v", account, tc.name, errB) - } - // check final nonce & balance matches expected in fixture - if genesisAccount.Nonce != gotNonce { - tc.failedErr = errors.New("nonce received doesn't match expected from fixture") - t.Errorf(`nonce received from account %v doesn't match expected from fixture in test %s: - received from block: %v - expected in fixture: %v`, account, tc.name, gotNonce, genesisAccount.Nonce) - } - if genesisAccount.Balance.Cmp(gotBalance) != 0 { - tc.failedErr = errors.New("balance received doesn't match expected from fixture") - t.Errorf(`balance received from account %v doesn't match expected from fixture in test %s: - received from block: %v - expected in fixture: %v`, account, tc.name, gotBalance, genesisAccount.Balance) - } - // check final storage - if len(genesisAccount.Storage) > 0 { - // extract fixture storage keys - keys := make([]common.Hash, 0, len(genesisAccount.Storage)) - for key := range genesisAccount.Storage { - keys = append(keys, key) - } - // get storage values for account with keys: keys - gotStorage, errS := engineClient.StorageAtKeys(ctx, account, keys, nil) - if errS != nil { - tc.failedErr = errS - t.Errorf("unable to get storage values from account: %v, in test %s: %v", account, tc.name, errS) + if err := tc.ValidatePost(ctx, engineClient); err != nil { + tc.Fatalf("unable to verify post allocation in test %s: %v", tc.Name, err) + } + + if tc.SyncPayload != nil { + // First send a new payload to the already running client + if syncing, err := tc.SyncPayload.ExecuteValidate( + ctx, + engineClient, + ); err != nil { + tc.Fatalf("unable to send sync payload: %v", err) + } else if syncing { + tc.Fatalf("sync payload failed (not synced)") + } + // Send a forkchoice update to the already running client to head to the sync payload + if syncing, err := tc.SyncPayload.ForkchoiceValidate(ctx, engineClient, tc.EngineFcuVersion); err != nil { + tc.Fatalf("unable to update head of chain: %v", err) + } else if syncing { + tc.Fatalf("forkchoice update failed (not synced)") + } + + // Spawn a second client connected to the already running client, + // send the forkchoice updated with the head hash and wait for sync. + // Then verify the post allocation. + // Add a timeout too. + secondEngineClient, err := engineStarter.StartClient(t, ctx, tc.Genesis(), env, nil, engineClient) + if err != nil { + tc.Fatalf("can't start client with Engine API: %v", err) + } + + if _, err := tc.SyncPayload.ExecuteValidate( + ctx, + secondEngineClient, + ); err != nil { + tc.Fatalf("unable to send sync payload: %v", err) + } // Don't check syncing here because some clients do sync immediately + + timeoutCtx, cancel := context.WithTimeout(ctx, SyncTimeout) + defer cancel() + for { + if syncing, err := tc.SyncPayload.ForkchoiceValidate(ctx, secondEngineClient, tc.EngineFcuVersion); err != nil { + tc.Fatalf("unable to update head of chain: %v", err) + } else if !syncing { + break } - // check values in storage match with fixture - for _, key := range keys { - if genesisAccount.Storage[key] != *gotStorage[key] { - tc.failedErr = errors.New("storage received doesn't match expected from fixture") - t.Errorf(`storage received from account %v doesn't match expected from fixture in test %s: from storage address: %v - received from block: %v - expected in fixture: %v`, account, tc.name, key, gotStorage[key], genesisAccount.Storage[key]) - } + select { + case <-timeoutCtx.Done(): + tc.Fatalf("timeout waiting for sync of secondary client") + default: } + time.Sleep(time.Second) } } + end := time.Now() - if tc.failedErr == nil { + if false { // TODO: Activate only on --sim.loglevel > 3 t.Logf(`test timing: setupClientEnv %v startClient %v @@ -229,74 +209,9 @@ func (tc *testcase) run(t *hivesim.T) { // updateEnv updates the environment variables against the fork rules // defined in envForks, for the network specified in the testcase fixture. -func (tc *testcase) updateEnv(env hivesim.Params) { - forkRules := envForks[tc.fixture.json.Fork] +func (tc *TestCase) updateEnv(env hivesim.Params) { + forkRules := envForks[tc.Fork] for k, v := range forkRules { env[k] = fmt.Sprintf("%d", v) } } - -// extractFixtureFields extracts the genesis, post allocation and payload -// fields from the given fixture test and stores them in the testcase struct. -func (tc *testcase) extractFixtureFields(fixture fixtureJSON) (err error) { - if tc.genesis, err = extractGenesis(fixture); err != nil { - return fmt.Errorf("failed to extract genesis: %w", err) - } - if tc.engineNewPayloads, err = extractEngineNewPayloads(fixture); err != nil { - return fmt.Errorf("failed to extract engineNewPayloads: %w", err) - } - tc.postAlloc = &fixture.Post - return nil -} - -// extracts the genesis block information from the given fixture. -func extractGenesis(fixture fixtureJSON) (*core.Genesis, error) { - genesis := &core.Genesis{ - Config: tests.Forks[fixture.Fork], - Coinbase: fixture.Genesis.Coinbase, - Difficulty: fixture.Genesis.Difficulty, - GasLimit: fixture.Genesis.GasLimit, - Timestamp: fixture.Genesis.Timestamp.Uint64(), - ExtraData: fixture.Genesis.ExtraData, - Mixhash: fixture.Genesis.MixHash, - Nonce: fixture.Genesis.Nonce.Uint64(), - BaseFee: fixture.Genesis.BaseFee, - BlobGasUsed: fixture.Genesis.BlobGasUsed, - ExcessBlobGas: fixture.Genesis.ExcessBlobGas, - Alloc: fixture.Pre, - } - return genesis, nil -} - -// extracts all the engineNewPayload information from the given fixture. -func extractEngineNewPayloads(fixture fixtureJSON) ([]engineNewPayload, error) { - var engineNewPayloads []engineNewPayload - for _, engineNewPayload := range fixture.EngineNewPayloads { - engineNewPayload := engineNewPayload - hiveExecutionPayload, err := typ.FromBeaconExecutableData(engineNewPayload.ExecutionPayload) - if err != nil { - return nil, errors.New("executionPayload param within engineNewPayload is invalid") - } - hiveExecutionPayload.VersionedHashes = &engineNewPayload.BlobVersionedHashes - hiveExecutionPayload.ParentBeaconBlockRoot = engineNewPayload.ParentBeaconBlockRoot - engineNewPayload.HiveExecutionPayload = &hiveExecutionPayload - engineNewPayloads = append(engineNewPayloads, engineNewPayload) - } - return engineNewPayloads, nil -} - -// checks for RPC errors and compares error codes if expected. -func checkRPCErrors(plErr error, fxErrCode int, t *hivesim.T, tc *testcase) { - rpcErr, isRpcErr := plErr.(rpc.Error) - if isRpcErr { - plErrCode := rpcErr.ErrorCode() - if plErrCode != fxErrCode { - tc.failedErr = fmt.Errorf("error code mismatch: client returned %v and fixture expected %v", plErrCode, fxErrCode) - t.Fatalf("error code mismatch\n client returned: %v\n fixture expected: %v\n in test %s", plErrCode, fxErrCode, tc.name) - } - t.Logf("expected error code caught by client: %v", plErrCode) - } else { - tc.failedErr = fmt.Errorf("fixture expected rpc error code: %v but none was returned from client", fxErrCode) - t.Fatalf("fixture expected rpc error code: %v but none was returned from client in test %s", fxErrCode, tc.name) - } -} diff --git a/simulators/ethereum/pyspec/types.go b/simulators/ethereum/pyspec/types.go index 63486948ea..1d763499c9 100644 --- a/simulators/ethereum/pyspec/types.go +++ b/simulators/ethereum/pyspec/types.go @@ -1,7 +1,9 @@ package main import ( - "encoding/json" + "context" + "errors" + "fmt" "math/big" api "github.com/ethereum/go-ethereum/beacon/engine" @@ -10,41 +12,105 @@ import ( "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/tests" + "github.com/ethereum/hive/simulators/ethereum/engine/client" typ "github.com/ethereum/hive/simulators/ethereum/engine/types" ) -type testcase struct { +type Fail interface { + Fatalf(format string, args ...interface{}) +} + +type TestCase struct { // test meta data - name string - filepath string - clientType string - failedErr error + Name string + FilePath string + ClientType string + FailedErr error // test fixture data - fixture fixtureTest - genesis *core.Genesis - postAlloc *core.GenesisAlloc - engineNewPayloads []engineNewPayload + *Fixture + FailCallback Fail +} + +func (tc *TestCase) Fatalf(format string, args ...interface{}) { + tc.FailedErr = fmt.Errorf(format, args...) + tc.FailCallback.Fatalf(format, args...) } -type fixtureTest struct { - json fixtureJSON +type Fixture struct { + Fork string `json:"network"` + GenesisBlock genesisBlock `json:"genesisBlockHeader"` + EngineNewPayloads []*EngineNewPayload `json:"engineNewPayloads"` + EngineFcuVersion int `json:"engineFcuVersion,string"` + Pre core.GenesisAlloc `json:"pre"` + PostAlloc core.GenesisAlloc `json:"postState"` + SyncPayload *EngineNewPayload `json:"syncPayload"` } -func (t *fixtureTest) UnmarshalJSON(in []byte) error { - if err := json.Unmarshal(in, &t.json); err != nil { - return err +func (f *Fixture) Genesis() *core.Genesis { + return &core.Genesis{ + Config: tests.Forks[f.Fork], + Coinbase: f.GenesisBlock.Coinbase, + Difficulty: f.GenesisBlock.Difficulty, + GasLimit: f.GenesisBlock.GasLimit, + Timestamp: f.GenesisBlock.Timestamp.Uint64(), + ExtraData: f.GenesisBlock.ExtraData, + Mixhash: f.GenesisBlock.MixHash, + Nonce: f.GenesisBlock.Nonce.Uint64(), + BaseFee: f.GenesisBlock.BaseFee, + BlobGasUsed: f.GenesisBlock.BlobGasUsed, + ExcessBlobGas: f.GenesisBlock.ExcessBlobGas, + Alloc: f.Pre, } - return nil } -type fixtureJSON struct { - Fork string `json:"network"` - Genesis genesisBlock `json:"genesisBlockHeader"` - EngineNewPayloads []engineNewPayload `json:"engineNewPayloads"` - EngineFcuVersion math.HexOrDecimal64 `json:"engineFcuVersion"` - Pre core.GenesisAlloc `json:"pre"` - Post core.GenesisAlloc `json:"postState"` +func (f *Fixture) ValidatePost(ctx context.Context, engineClient client.EngineClient) error { + // check nonce, balance & storage of accounts in final block against fixture values + for address, account := range f.PostAlloc { + // get nonce & balance from last block (end of test execution) + gotNonce, errN := engineClient.NonceAt(ctx, address, nil) + gotBalance, errB := engineClient.BalanceAt(ctx, address, nil) + if errN != nil { + return fmt.Errorf("unable to call nonce from account: %v: %v", address, errN) + } else if errB != nil { + return fmt.Errorf("unable to call balance from account: %v: %v", address, errB) + } + // check final nonce & balance matches expected in fixture + if account.Nonce != gotNonce { + return fmt.Errorf(`nonce received from account %v doesn't match expected from fixture: + received from block: %v + expected in fixture: %v`, address, gotNonce, account.Nonce) + } + if account.Balance.Cmp(gotBalance) != 0 { + return fmt.Errorf(`balance received from account %v doesn't match expected from fixture: + received from block: %v + expected in fixture: %v`, address, gotBalance, account.Balance) + } + // check final storage + if len(account.Storage) > 0 { + // extract fixture storage keys + keys := make([]common.Hash, 0, len(account.Storage)) + for key := range account.Storage { + keys = append(keys, key) + } + // get storage values for account with keys: keys + gotStorage, errS := engineClient.StorageAtKeys(ctx, address, keys, nil) + if errS != nil { + return fmt.Errorf("unable to get storage values from account: %v: %v", address, errS) + } + // check values in storage match with fixture + for _, key := range keys { + if account.Storage[key] != *gotStorage[key] { + return fmt.Errorf(`storage received from account %v doesn't match expected from fixture: + received from block: %v + expected in fixture: %v`, address, gotStorage[key], account.Storage[key]) + } + } + } + } + return nil } //go:generate go run github.com/fjl/gencodec -type genesisBlock -field-override genesisBlockUnmarshaling -out gen_gb.go @@ -73,13 +139,120 @@ type genesisBlockUnmarshaling struct { ExcessBlobGas *math.HexOrDecimal64 `json:"excessDataGas"` } -type engineNewPayload struct { +type EngineNewPayload struct { ExecutionPayload *api.ExecutableData `json:"executionPayload"` BlobVersionedHashes []common.Hash `json:"expectedBlobVersionedHashes"` ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot"` Version math.HexOrDecimal64 `json:"version"` - Valid bool `json:"valid"` - ErrorCode int64 `json:"errorCode,string"` + ValidationError *string `json:"validationError"` + ErrorCode int `json:"errorCode,string"` +} - HiveExecutionPayload *typ.ExecutableData +func (p *EngineNewPayload) ExecutableData() (*typ.ExecutableData, error) { + executableData, err := typ.FromBeaconExecutableData(p.ExecutionPayload) + if err != nil { + return nil, errors.New("executionPayload param within engineNewPayload is invalid") + } + executableData.VersionedHashes = &p.BlobVersionedHashes + executableData.ParentBeaconBlockRoot = p.ParentBeaconBlockRoot + return &executableData, nil +} + +func (p *EngineNewPayload) Valid() bool { + return p.ErrorCode == 0 && p.ValidationError == nil +} + +func (p *EngineNewPayload) ExpectedStatus() string { + if p.ValidationError != nil { + return "INVALID" + } + return "VALID" +} + +func (p *EngineNewPayload) Execute(ctx context.Context, engineClient client.EngineClient) (api.PayloadStatusV1, rpc.Error) { + executableData, err := p.ExecutableData() + if err != nil { + panic(err) + } + status, err := engineClient.NewPayload( + ctx, + int(p.Version), + executableData, + ) + return status, parseError(err) +} + +func (p *EngineNewPayload) ExecuteValidate(ctx context.Context, engineClient client.EngineClient) (bool, error) { + plStatus, plErr := p.Execute(ctx, engineClient) + if err := p.ValidateRPCError(plErr); err != nil { + return false, err + } else if plErr != nil { + // Got an expected error and is already validated in ValidateRPCError + return false, nil + } + if plStatus.Status == "SYNCING" { + return true, nil + } + // Check payload status matches expected + if plStatus.Status != p.ExpectedStatus() { + return false, fmt.Errorf("payload status mismatch: got %s, want %s", plStatus.Status, p.ExpectedStatus()) + } + return false, nil +} + +func (p *EngineNewPayload) ForkchoiceValidate(ctx context.Context, engineClient client.EngineClient, fcuVersion int) (bool, error) { + response, err := engineClient.ForkchoiceUpdated(ctx, fcuVersion, &api.ForkchoiceStateV1{HeadBlockHash: p.ExecutionPayload.BlockHash}, nil) + if err != nil { + return false, err + } + if response.PayloadStatus.Status == "SYNCING" { + return true, nil + } + if response.PayloadStatus.Status != p.ExpectedStatus() { + return false, fmt.Errorf("forkchoice update status mismatch: got %s, want %s", response.PayloadStatus.Status, p.ExpectedStatus()) + } + return false, nil +} + +type HTTPErrorWithCode struct { + rpc.HTTPError +} + +func (e HTTPErrorWithCode) ErrorCode() int { + return e.StatusCode +} + +func parseError(plErr interface{}) rpc.Error { + if plErr == nil { + return nil + } + rpcErr, isRpcErr := plErr.(rpc.Error) + if isRpcErr { + return rpcErr + } + httpErr, isHttpErr := plErr.(rpc.HTTPError) + if isHttpErr { + return HTTPErrorWithCode{httpErr} + } + panic("unable to parse") +} + +// checks for RPC errors and compares error codes if expected. +func (p *EngineNewPayload) ValidateRPCError(rpcErr rpc.Error) error { + if rpcErr == nil && p.ErrorCode == 0 { + return nil + } + if rpcErr == nil && p.ErrorCode != 0 { + return fmt.Errorf("expected error code %d but received no error", p.ErrorCode) + } + if rpcErr != nil && p.ErrorCode == 0 { + return fmt.Errorf("expected no error code but received %d", rpcErr.ErrorCode()) + } + if rpcErr != nil && p.ErrorCode != 0 { + plErrCode := rpcErr.ErrorCode() + if plErrCode != p.ErrorCode { + return fmt.Errorf("error code mismatch: got: %d, want: %d", plErrCode, p.ErrorCode) + } + } + return nil } diff --git a/simulators/ethereum/rpc-compat/go.mod b/simulators/ethereum/rpc-compat/go.mod index 46eb27822c..ecfe1d5a79 100644 --- a/simulators/ethereum/rpc-compat/go.mod +++ b/simulators/ethereum/rpc-compat/go.mod @@ -3,7 +3,8 @@ module github.com/ethereum/hive/simulators/ethereum/rpc-compat go 1.18 require ( - github.com/ethereum/hive v0.0.0-20231031133732-dcd7ddb75960 + github.com/ethereum/go-ethereum v1.13.5-0.20231031113925-bc42e88415d3 + github.com/ethereum/hive v0.0.0-20240131232337-d38a51d4e475 github.com/yudai/gojsondiff v1.0.0 ) @@ -12,15 +13,22 @@ require ( github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect github.com/deckarep/golang-set/v2 v2.1.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect - github.com/ethereum/go-ethereum v1.13.5-0.20231031113925-bc42e88415d3 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-stack/stack v1.8.1 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/holiman/uint256 v1.2.3 // indirect + github.com/lithammer/dedent v1.1.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect github.com/sergi/go-diff v1.2.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect + github.com/tidwall/gjson v1.17.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect @@ -31,4 +39,5 @@ require ( golang.org/x/mod v0.12.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/tools v0.13.0 // indirect + gopkg.in/inconshreveable/log15.v2 v2.0.0-20200109203555-b30bc20e4fd1 // indirect ) diff --git a/simulators/ethereum/rpc-compat/go.sum b/simulators/ethereum/rpc-compat/go.sum index 31c1b552ee..cd8f144914 100644 --- a/simulators/ethereum/rpc-compat/go.sum +++ b/simulators/ethereum/rpc-compat/go.sum @@ -16,8 +16,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= github.com/ethereum/go-ethereum v1.13.5-0.20231031113925-bc42e88415d3 h1:i4TE0DmzMYcr2IiCdlTGGk+7Q/EzXvBKbbG4uRxMjJ0= github.com/ethereum/go-ethereum v1.13.5-0.20231031113925-bc42e88415d3/go.mod h1:yMTu38GSuyxaYzQMViqNmQ1s3cE84abZexQmTgenWk0= -github.com/ethereum/hive v0.0.0-20231031133732-dcd7ddb75960 h1:7H9z2o/KImWQ/1PACU3ewsSFNxT/lzwYRnwit2YFqMg= -github.com/ethereum/hive v0.0.0-20231031133732-dcd7ddb75960/go.mod h1:sV7LrFBlEii71kI9udVbUVj7SXIifZ2VzFjQ8S6rZns= +github.com/ethereum/hive v0.0.0-20240131232337-d38a51d4e475 h1:IlQwBa8MmXq/pfdDbzCwYYqf3PVEXIvI6xkf3ZB8p4E= +github.com/ethereum/hive v0.0.0-20240131232337-d38a51d4e475/go.mod h1:D0RJuJaAolOejqq/n0YoX7VbPzkooLHwCSoaQn2z6xY= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= @@ -27,6 +27,8 @@ github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiU github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -54,8 +56,13 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1: github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= +github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -83,6 +90,16 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI= github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= +github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= +github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= @@ -136,6 +153,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= @@ -169,6 +187,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inconshreveable/log15.v2 v2.0.0-20200109203555-b30bc20e4fd1 h1:iiHuQZCNgYPmFQxd3BBN/Nc5+dAwzZuq5y40s20oQw0= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20200109203555-b30bc20e4fd1/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/simulators/ethereum/rpc-compat/main.go b/simulators/ethereum/rpc-compat/main.go index 08996f77dc..6a2d997980 100644 --- a/simulators/ethereum/rpc-compat/main.go +++ b/simulators/ethereum/rpc-compat/main.go @@ -7,48 +7,34 @@ import ( "io" "net" "net/http" - "os" - "path/filepath" "regexp" "strings" "time" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/hive/hivesim" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" diff "github.com/yudai/gojsondiff" "github.com/yudai/gojsondiff/formatter" ) var ( - clientEnv = hivesim.Params{ - "HIVE_NODETYPE": "full", - "HIVE_NETWORK_ID": "1337", - "HIVE_CHAIN_ID": "1337", - "HIVE_FORK_HOMESTEAD": "0", - //"HIVE_FORK_DAO_BLOCK": 2000, - "HIVE_FORK_TANGERINE": "0", - "HIVE_FORK_SPURIOUS": "0", - "HIVE_FORK_BYZANTIUM": "0", - "HIVE_FORK_CONSTANTINOPLE": "0", - "HIVE_FORK_PETERSBURG": "0", - "HIVE_FORK_ISTANBUL": "0", - "HIVE_FORK_BERLIN": "0", - "HIVE_FORK_LONDON": "0", - "HIVE_SHANGHAI_TIMESTAMP": "0", - "HIVE_TERMINAL_TOTAL_DIFFICULTY": "0", - "HIVE_TERMINAL_TOTAL_DIFFICULTY_PASSED": "1", - } files = map[string]string{ "genesis.json": "./tests/genesis.json", "chain.rlp": "./tests/chain.rlp", } ) -type test struct { - Name string - Data []byte -} - func main() { + // Load fork environment. + var clientEnv hivesim.Params + err := common.LoadJSON("tests/forkenv.json", &clientEnv) + if err != nil { + panic(err) + } + + // Run the test suite. suite := hivesim.Suite{ Name: "rpc-compat", Description: ` @@ -63,6 +49,7 @@ conformance with the execution API specification.`[1:], Parameters: clientEnv, Files: files, Run: func(t *hivesim.T, c *hivesim.Client) { + sendForkchoiceUpdated(t, c) runAllTests(t, c, c.Type) }, AlwaysRun: true, @@ -74,13 +61,14 @@ conformance with the execution API specification.`[1:], func runAllTests(t *hivesim.T, c *hivesim.Client, clientName string) { _, testPattern := t.Sim.TestPattern() re := regexp.MustCompile(testPattern) - tests := loadTests(t, "tests", re) for _, test := range tests { + test := test t.Run(hivesim.TestSpec{ - Name: fmt.Sprintf("%s (%s)", test.Name, clientName), + Name: fmt.Sprintf("%s (%s)", test.name, clientName), + Description: test.comment, Run: func(t *hivesim.T) { - if err := runTest(t, c, test.Data); err != nil { + if err := runTest(t, c, &test); err != nil { t.Fatal(err) } }, @@ -88,63 +76,69 @@ func runAllTests(t *hivesim.T, c *hivesim.Client, clientName string) { } } -func runTest(t *hivesim.T, c *hivesim.Client, data []byte) error { +func runTest(t *hivesim.T, c *hivesim.Client, test *rpcTest) error { var ( - client = &http.Client{ - Timeout: 5 * time.Second, - Transport: &loggingRoundTrip{ - t: t, - inner: http.DefaultTransport, - }, - } - url = fmt.Sprintf("http://%s", net.JoinHostPort(c.IP.String(), "8545")) - err error - resp []byte + client = &http.Client{Timeout: 5 * time.Second} + url = fmt.Sprintf("http://%s", net.JoinHostPort(c.IP.String(), "8545")) + err error + respBytes []byte ) - for _, line := range strings.Split(string(data), "\n") { - line = strings.TrimSpace(line) - switch { - case len(line) == 0 || strings.HasPrefix(line, "//"): - // Skip comments, blank lines. - continue - case strings.HasPrefix(line, ">> "): + for _, msg := range test.messages { + if msg.send { // Send request. - resp, err = postHttp(client, url, []byte(line[3:])) + t.Log(">> ", msg.data) + respBytes, err = postHttp(client, url, strings.NewReader(msg.data)) if err != nil { return err } - case strings.HasPrefix(line, "<< "): - // Read response. Unmarshal to interface{} to verify deep equality. Marshal - // again to remove padding differences and to print each field in the same - // order. This makes it easy to spot any discrepancies. - if resp == nil { + } else { + // Receive a response. + if respBytes == nil { return fmt.Errorf("invalid test, response before request") } - want := []byte(strings.TrimSpace(line)[3:]) // trim leading "<< " - // Now compare. - d, err := diff.New().Compare(resp, want) + expectedData := msg.data + resp := string(bytes.TrimSpace(respBytes)) + t.Log("<< ", resp) + if !gjson.Valid(resp) { + return fmt.Errorf("invalid JSON response") + } + + // Patch JSON to remove error messages. We only do this in the specific case + // where an error is expected AND returned by the client. + var errorRedacted bool + if gjson.Get(resp, "error").Exists() && gjson.Get(expectedData, "error").Exists() { + resp, _ = sjson.Delete(resp, "error.message") + expectedData, _ = sjson.Delete(expectedData, "error.message") + errorRedacted = true + } + + // Compare responses. + d, err := diff.New().Compare([]byte(resp), []byte(expectedData)) if err != nil { return fmt.Errorf("failed to unmarshal value: %s\n", err) } + // If there is a discrepancy, return error. if d.Modified() { - var got map[string]interface{} - json.Unmarshal(resp, &got) + if errorRedacted { + t.Log("note: error messages removed from comparison") + } + var got map[string]any + json.Unmarshal([]byte(resp), &got) config := formatter.AsciiFormatterConfig{ ShowArrayIndex: true, Coloring: false, } formatter := formatter.NewAsciiFormatter(got, config) diffString, _ := formatter.Format(d) - return fmt.Errorf("response differs from expected:\n%s", diffString) + return fmt.Errorf("response differs from expected (-- client, ++ test):\n%s", diffString) } - resp = nil - default: - t.Fatalf("invalid line in test script: %s", line) + respBytes = nil } } - if resp != nil { + + if respBytes != nil { t.Fatalf("unhandled response in test case") } return nil @@ -152,9 +146,8 @@ func runTest(t *hivesim.T, c *hivesim.Client, data []byte) error { // sendHttp sends an HTTP POST with the provided json data and reads the // response into a byte slice and returns it. -func postHttp(c *http.Client, url string, d []byte) ([]byte, error) { - data := bytes.NewBuffer(d) - req, err := http.NewRequest("POST", url, data) +func postHttp(c *http.Client, url string, d io.Reader) ([]byte, error) { + req, err := http.NewRequest("POST", url, d) if err != nil { return nil, fmt.Errorf("error building request: %v", err) } @@ -166,66 +159,20 @@ func postHttp(c *http.Client, url string, d []byte) ([]byte, error) { return io.ReadAll(resp.Body) } -// loggingRoundTrip writes requests and responses to the test log. -type loggingRoundTrip struct { - t *hivesim.T - inner http.RoundTripper -} - -func (rt *loggingRoundTrip) RoundTrip(req *http.Request) (*http.Response, error) { - // Read and log the request body. - reqBytes, err := io.ReadAll(req.Body) - req.Body.Close() - if err != nil { - return nil, err +// sendForkchoiceUpdated delivers the initial FcU request to the client. +func sendForkchoiceUpdated(t *hivesim.T, client *hivesim.Client) { + var request struct { + Method string + Params []any } - rt.t.Logf(">> %s", bytes.TrimSpace(reqBytes)) - reqCopy := *req - reqCopy.Body = io.NopCloser(bytes.NewReader(reqBytes)) - - // Do the round trip. - resp, err := rt.inner.RoundTrip(&reqCopy) - if err != nil { - return nil, err + if err := common.LoadJSON("tests/headfcu.json", &request); err != nil { + t.Fatal("error loading forkchoiceUpdated:", err) } - defer resp.Body.Close() - - // Read and log the response bytes. - respBytes, err := io.ReadAll(resp.Body) + t.Logf("sending %s: %v", request.Method, request.Params) + var resp any + err := client.EngineAPI().Call(&resp, request.Method, request.Params...) if err != nil { - return nil, err + t.Fatal("client rejected forkchoiceUpdated:", err) } - respCopy := *resp - respCopy.Body = io.NopCloser(bytes.NewReader(respBytes)) - rt.t.Logf("<< %s", bytes.TrimSpace(respBytes)) - return &respCopy, nil -} - -// loadTests walks the given directory looking for *.io files to load. -func loadTests(t *hivesim.T, root string, re *regexp.Regexp) []test { - tests := make([]test, 0) - filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - if err != nil { - t.Logf("unable to walk path: %s", err) - return err - } - if info.IsDir() { - return nil - } - if fname := info.Name(); !strings.HasSuffix(fname, ".io") { - return nil - } - pathname := strings.TrimSuffix(strings.TrimPrefix(path, root), ".io") - if !re.MatchString(pathname) { - fmt.Println("skip", pathname) - return nil // skip - } - data, err := os.ReadFile(path) - if err != nil { - return err - } - tests = append(tests, test{strings.TrimLeft(pathname, "/"), data}) - return nil - }) - return tests + t.Logf("response: %v", resp) } diff --git a/simulators/ethereum/rpc-compat/testload.go b/simulators/ethereum/rpc-compat/testload.go new file mode 100644 index 0000000000..57cd620a0c --- /dev/null +++ b/simulators/ethereum/rpc-compat/testload.go @@ -0,0 +1,102 @@ +package main + +import ( + "bufio" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/ethereum/hive/hivesim" + "github.com/tidwall/gjson" +) + +type rpcTest struct { + name string + comment string + speconly bool + messages []rpcTestMessage +} + +type rpcTestMessage struct { + data string + // if true, the message is a send (>>), otherwise it's a receive (<<) + send bool +} + +func loadTestFile(name string, r io.Reader) (rpcTest, error) { + var ( + rdr = bufio.NewReader(r) + scan = bufio.NewScanner(rdr) + inHeader = true + test = rpcTest{name: name} + ) + for scan.Scan() { + line := strings.TrimSpace(scan.Text()) + switch { + case len(line) == 0: + continue + + case strings.HasPrefix(line, "//"): + if !inHeader { + continue // ignore comments after requests + } + text := strings.TrimPrefix(strings.TrimPrefix(line, "//"), " ") + test.comment += text + "\n" + if strings.HasPrefix(text, "speconly:") { + test.speconly = true + } + + case strings.HasPrefix(line, ">>") || strings.HasPrefix(line, "<<"): + inHeader = false + data := strings.TrimSpace(line[2:]) + if !gjson.Valid(data) { + return test, fmt.Errorf("invalid JSON in line %q", line) + } + test.messages = append(test.messages, rpcTestMessage{ + data: data, + send: strings.HasPrefix(line, ">>"), + }) + + default: + return test, fmt.Errorf("invalid test line: %q", line) + } + } + return test, scan.Err() +} + +// loadTests walks the given directory looking for *.io files to load. +func loadTests(t *hivesim.T, root string, re *regexp.Regexp) []rpcTest { + var tests []rpcTest + filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + t.Logf("unable to walk path: %s", err) + return err + } + if info.IsDir() { + return nil + } + if fname := info.Name(); !strings.HasSuffix(fname, ".io") { + return nil + } + pathname := strings.TrimSuffix(strings.TrimPrefix(path, root+"/"), ".io") + if !re.MatchString(pathname) { + fmt.Println("skip", pathname) + return nil // skip + } + fd, err := os.Open(path) + if err != nil { + return err + } + defer fd.Close() + test, err := loadTestFile(pathname, fd) + if err != nil { + return fmt.Errorf("invalid test %s: %v", info.Name(), err) + } + tests = append(tests, test) + return nil + }) + return tests +} diff --git a/simulators/ethereum/rpc-compat/testload_test.go b/simulators/ethereum/rpc-compat/testload_test.go new file mode 100644 index 0000000000..9148d24c02 --- /dev/null +++ b/simulators/ethereum/rpc-compat/testload_test.go @@ -0,0 +1,48 @@ +package main + +import ( + "reflect" + "strings" + "testing" +) + +func TestLoad(t *testing.T) { + data := `// this is a test comment +// this is the second line +// speconly: lalalala +>> {"type":"send"} +<< {"type":"recv"} +` + + expectedComment := `this is a test comment +this is the second line +speconly: lalalala +` + expectedMessages := []rpcTestMessage{ + { + data: `{"type":"send"}`, + send: true, + }, + { + data: `{"type":"recv"}`, + send: false, + }, + } + + result, err := loadTestFile("the-test", strings.NewReader(data)) + if err != nil { + t.Fatal("error:", err) + } + if result.name != "the-test" { + t.Error("wrong test name:", result.comment) + } + if result.comment != expectedComment { + t.Errorf("wrong test comment %q", result.comment) + } + if !result.speconly { + t.Error("test is not marked speconly") + } + if !reflect.DeepEqual(result.messages, expectedMessages) { + t.Errorf("wrong test messages %+v", result.messages) + } +} diff --git a/simulators/ethereum/rpc/README.md b/simulators/ethereum/rpc/README.md index 4eaee839ad..28dbe5f52b 100644 --- a/simulators/ethereum/rpc/README.md +++ b/simulators/ethereum/rpc/README.md @@ -25,7 +25,7 @@ The genesis block also contains 2 contracts: Ethclient runs various tests that use the `ethclient.Client` API. Such as sending transactions, retrieving logs and balances. -ABI, interacts with the pre-deployed events contract. It send transactions, executs calls +ABI, interacts with the pre-deployed events contract. It send transactions, executes calls and examines generated logs. Each test is designed to run in parallel with other tests. In most cases the first step a diff --git a/simulators/ethereum/sync/go.mod b/simulators/ethereum/sync/go.mod index b6657206ff..c0bd451159 100644 --- a/simulators/ethereum/sync/go.mod +++ b/simulators/ethereum/sync/go.mod @@ -4,7 +4,7 @@ go 1.20 require ( github.com/ethereum/go-ethereum v1.13.5-0.20231031113925-bc42e88415d3 - github.com/ethereum/hive v0.0.0-20231031133732-dcd7ddb75960 + github.com/ethereum/hive v0.0.0-20240131232337-d38a51d4e475 ) require ( @@ -22,12 +22,10 @@ require ( github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect github.com/consensys/bavard v0.1.13 // indirect github.com/consensys/gnark-crypto v0.12.1 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect github.com/deckarep/golang-set/v2 v2.1.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect github.com/ethereum/c-kzg-4844 v0.4.0 // indirect - github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 // indirect github.com/getsentry/sentry-go v0.18.0 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-stack/stack v1.8.1 // indirect @@ -37,19 +35,15 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/gorilla/websocket v1.5.0 // indirect - github.com/hashicorp/go-bexpr v0.1.10 // indirect github.com/holiman/uint256 v1.2.3 // indirect - github.com/huin/goupnp v1.3.0 // indirect - github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/klauspost/compress v1.15.15 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect + github.com/lithammer/dedent v1.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect - github.com/mitchellh/mapstructure v1.4.1 // indirect - github.com/mitchellh/pointerstructure v1.2.0 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -59,15 +53,11 @@ require ( github.com/prometheus/procfs v0.9.0 // indirect github.com/rivo/uniseg v0.4.3 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect - github.com/rs/cors v1.7.0 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/supranational/blst v0.3.11 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect - github.com/urfave/cli/v2 v2.25.7 // indirect - github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect golang.org/x/crypto v0.14.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect @@ -77,6 +67,6 @@ require ( golang.org/x/text v0.13.0 // indirect golang.org/x/tools v0.13.0 // indirect google.golang.org/protobuf v1.28.1 // indirect - gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect + gopkg.in/inconshreveable/log15.v2 v2.0.0-20200109203555-b30bc20e4fd1 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) diff --git a/simulators/ethereum/sync/go.sum b/simulators/ethereum/sync/go.sum index 53ffa0f90d..aa33968c89 100644 --- a/simulators/ethereum/sync/go.sum +++ b/simulators/ethereum/sync/go.sum @@ -1,7 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= @@ -53,9 +52,9 @@ github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5U github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/crate-crypto/go-kzg-4844 v0.7.0 h1:C0vgZRk4q4EZ/JgPfzuSoxdCq3C3mOZMBShovmncxvA= github.com/crate-crypto/go-kzg-4844 v0.7.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -80,12 +79,11 @@ github.com/ethereum/c-kzg-4844 v0.4.0 h1:3MS1s4JtA868KpJxroZoepdV0ZKBp3u/O5HcZ7R github.com/ethereum/c-kzg-4844 v0.4.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= github.com/ethereum/go-ethereum v1.13.5-0.20231031113925-bc42e88415d3 h1:i4TE0DmzMYcr2IiCdlTGGk+7Q/EzXvBKbbG4uRxMjJ0= github.com/ethereum/go-ethereum v1.13.5-0.20231031113925-bc42e88415d3/go.mod h1:yMTu38GSuyxaYzQMViqNmQ1s3cE84abZexQmTgenWk0= -github.com/ethereum/hive v0.0.0-20231031133732-dcd7ddb75960 h1:7H9z2o/KImWQ/1PACU3ewsSFNxT/lzwYRnwit2YFqMg= -github.com/ethereum/hive v0.0.0-20231031133732-dcd7ddb75960/go.mod h1:sV7LrFBlEii71kI9udVbUVj7SXIifZ2VzFjQ8S6rZns= +github.com/ethereum/hive v0.0.0-20240131232337-d38a51d4e475 h1:IlQwBa8MmXq/pfdDbzCwYYqf3PVEXIvI6xkf3ZB8p4E= +github.com/ethereum/hive v0.0.0-20240131232337-d38a51d4e475/go.mod h1:D0RJuJaAolOejqq/n0YoX7VbPzkooLHwCSoaQn2z6xY= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= -github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= @@ -160,7 +158,6 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= -github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/holiman/billy v0.0.0-20230718173358-1c7e68d277a7 h1:3JQNjnMRil1yD0IfZKHF9GxxWKDJGj8I0IqOUol//sw= @@ -169,7 +166,6 @@ github.com/holiman/uint256 v1.2.3 h1:K8UWO1HUJpRMXBxbmaY1Y8IAMZC/RsKB+ArEnnK4l5o github.com/holiman/uint256 v1.2.3/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= -github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/hydrogen18/memlistener v0.0.0-20200120041712-dcc25e7acd91/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= @@ -180,7 +176,6 @@ github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7Ua github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= -github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= @@ -209,6 +204,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= +github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= +github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -234,9 +231,7 @@ github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/le github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= -github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= @@ -292,10 +287,9 @@ github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4 github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= -github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= @@ -333,7 +327,6 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= -github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= @@ -345,7 +338,6 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1: github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= @@ -516,10 +508,10 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/inconshreveable/log15.v2 v2.0.0-20200109203555-b30bc20e4fd1 h1:iiHuQZCNgYPmFQxd3BBN/Nc5+dAwzZuq5y40s20oQw0= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20200109203555-b30bc20e4fd1/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= -gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/simulators/ethereum/sync/sync.go b/simulators/ethereum/sync/sync.go index adc34cc4e7..823cbea583 100644 --- a/simulators/ethereum/sync/sync.go +++ b/simulators/ethereum/sync/sync.go @@ -10,8 +10,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" - gnode "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/hive/hivesim" ) @@ -151,12 +149,10 @@ func (n *node) triggerSync(t *hivesim.T) error { if err := common.LoadJSON("chain/headnewpayload.json", &newpayload); err != nil { return err } - c := n.engineClient() - // deliver newPayload t.Logf("%s: %s", newpayload.Method, jsonString(newpayload.Params)) var resp engine.PayloadStatusV1 - if err := c.Call(&resp, newpayload.Method, conv2any(newpayload.Params)...); err != nil { + if err := n.EngineAPI().Call(&resp, newpayload.Method, conv2any(newpayload.Params)...); err != nil { return err } t.Logf("response: %+v", jsonString(resp)) @@ -169,26 +165,15 @@ func (n *node) sendForkchoiceUpdated(t *hivesim.T) error { if err := common.LoadJSON("chain/headfcu.json", &fcu); err != nil { return err } - c := n.engineClient() - t.Logf("%s: %s", fcu.Method, jsonString(fcu.Params)) var fcuresp engine.ForkChoiceResponse - if err := c.Call(&fcuresp, fcu.Method, conv2any(fcu.Params)...); err != nil { + if err := n.EngineAPI().Call(&fcuresp, fcu.Method, conv2any(fcu.Params)...); err != nil { return err } t.Logf("response: %s", jsonString(&fcuresp)) return nil } -func (n *node) engineClient() *rpc.Client { - // engine client setup - token := [32]byte{0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x65} - engineURL := fmt.Sprintf("http://%v:8551/", n.IP) - ctx := context.Background() - c, _ := rpc.DialOptions(ctx, engineURL, rpc.WithHTTPAuth(gnode.NewJWTAuth(token))) - return c -} - // checkHead checks whether the remote chain head matches the given values. func (n *node) checkHead() error { var expected types.Header diff --git a/simulators/portal/beacon/Cargo.toml b/simulators/portal/beacon/Cargo.toml new file mode 100755 index 0000000000..6199c2fbde --- /dev/null +++ b/simulators/portal/beacon/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "beacon" +version = "0.1.0" +authors = ["Ognyan Genev ", "Kolby ML (Moroz Liebl) "] +edition = "2021" + +[dependencies] +ethportal-api = { git = "https://github.com/ethereum/trin", rev = "1829841efca273844be93adf53ab84c2d1a020ed" } +hivesim = { git = "https://github.com/ethereum/portal-hive", rev = "8ff1e3d3c941dd00d56dacd777a5dfb71edf402f" } +futures = "0.3.25" +serde_json = "1.0.87" +tracing = "0.1.37" +tracing-subscriber = "0.3.16" +tokio = { version = "1", features = ["full"] } diff --git a/simulators/portal/beacon/Dockerfile b/simulators/portal/beacon/Dockerfile new file mode 100644 index 0000000000..9df9fbd849 --- /dev/null +++ b/simulators/portal/beacon/Dockerfile @@ -0,0 +1,26 @@ +FROM rust:1.75.0 AS builder + +# create a new empty shell project +RUN USER=root cargo new --bin beacon +WORKDIR /beacon + +RUN apt-get update && apt-get install clang -y + +# copy over manifests and source to build image +COPY Cargo.toml ./Cargo.toml +COPY src ./src + +# build for release +RUN cargo build --release + +# final base +FROM ubuntu:22.04 + +RUN apt update && apt install wget -y + +# copy build artifacts from build stage +COPY --from=builder /beacon/target/release/beacon . + +ENV RUST_LOG=debug + +ENTRYPOINT ["./beacon"] diff --git a/simulators/portal/beacon/src/main.rs b/simulators/portal/beacon/src/main.rs new file mode 100644 index 0000000000..c1a346fe2e --- /dev/null +++ b/simulators/portal/beacon/src/main.rs @@ -0,0 +1,43 @@ +mod suites; + +use hivesim::{Simulation, Suite, TestSpec}; +use suites::rpc_compat::run_rpc_compat_test_suite; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + let mut beacon_rpc_compat = Suite { + name: "beacon-rpc-compat".to_string(), + description: "The RPC-compatibility test suite runs a set of RPC related tests against a + running node. It tests client implementations of the JSON-RPC API for + conformance with the portal network API specification." + .to_string(), + tests: vec![], + }; + + beacon_rpc_compat.add(TestSpec { + name: "client launch".to_string(), + description: "This test launches the client and collects its logs.".to_string(), + always_run: false, + run: run_rpc_compat_test_suite, + client: None, + }); + + let sim = Simulation::new(); + run_suite(sim, vec![beacon_rpc_compat]).await; +} + +async fn run_suite(host: Simulation, suites: Vec) { + for suite in suites { + let name = suite.clone().name; + let description = suite.clone().description; + + let suite_id = host.start_suite(name, description, "".to_string()).await; + + for test in &suite.tests { + test.run_test(host.clone(), suite_id, suite.clone()).await; + } + + host.end_suite(suite_id).await; + } +} diff --git a/simulators/portal/beacon/src/suites/constants.rs b/simulators/portal/beacon/src/suites/constants.rs new file mode 100644 index 0000000000..1ae0093185 --- /dev/null +++ b/simulators/portal/beacon/src/suites/constants.rs @@ -0,0 +1,5 @@ +pub const HIVE_PORTAL_NETWORKS_SELECTED: &str = "HIVE_PORTAL_NETWORKS_SELECTED"; +pub const BEACON_STRING: &str = "beacon"; + +// trin-bridge constants +pub const TRIN_BRIDGE_CLIENT_TYPE: &str = "trin-bridge"; diff --git a/simulators/portal/beacon/src/suites/mod.rs b/simulators/portal/beacon/src/suites/mod.rs new file mode 100644 index 0000000000..7e92c503f8 --- /dev/null +++ b/simulators/portal/beacon/src/suites/mod.rs @@ -0,0 +1,2 @@ +pub mod constants; +pub mod rpc_compat; diff --git a/simulators/portal/beacon/src/suites/rpc_compat.rs b/simulators/portal/beacon/src/suites/rpc_compat.rs new file mode 100644 index 0000000000..2bdc2ce73a --- /dev/null +++ b/simulators/portal/beacon/src/suites/rpc_compat.rs @@ -0,0 +1,600 @@ +use crate::suites::constants::BEACON_STRING; +use crate::suites::constants::HIVE_PORTAL_NETWORKS_SELECTED; +use crate::suites::constants::TRIN_BRIDGE_CLIENT_TYPE; +use ethportal_api::types::beacon::ContentInfo; +use ethportal_api::types::enr::generate_random_remote_enr; +use ethportal_api::Discv5ApiClient; +use ethportal_api::PossibleBeaconContentValue::{ContentAbsent, ContentPresent}; +use ethportal_api::{BeaconContentKey, BeaconNetworkApiClient}; +use hivesim::types::ClientDefinition; +use hivesim::{dyn_async, Client, NClientTestSpec, Test}; +use serde_json::json; +use std::collections::HashMap; + +// Bootstrap https://github.com/ethereum/portal-spec-tests/blob/master/tests/mainnet/beacon_chain/light_client/bootstrap.json +const CONTENT_KEY: &str = "0x10bd9f42d9a42d972bdaf4dee84e5b419dd432b52867258acb7bcc7f567b6e3af1"; +const CONTENT_VALUE: &str = "0xbba4da96d4600000814453665c4b46dad568d69d0a3d211c70829ce7c5c17549713ed0996c8743e6b55b3797ea19c0eebac07b0e163fae9aa71bc2561705e492dd206730e8d7e731e621d2d7039ded027d9910bd41b23f642c609986a33f46fb3187faf6a0abc1809811c919b69e9ecbe5c129e3bc6cc6811de879149bf856984cc2a5162aed445600abaccb10a1b48694f150de473b893184c8200c822abc86d3d8d3bab7918b55963553bf9be9d2144a36827db66449e2302a49c72837b3e793666321ffa0a45089cb5f6af2380686485c4a85aa460dafff2c2681ffc4dd2a57ad488e53e904734908fa7759d18edb7e58d54ae6565774b607fd7488f013b6d02cd13beb3ed2a41d983f02f7eeac4b7f4222474252ef4538b5b09c3fe7ba6899412839fb79588387fd75faf7225e9dfd65bb46a795929d1a6a83113cc89c390ef293f9b73f336157e2f4abd6867a7a3aad6cb0472e1f17a73f913822642688fb28944b9df775593ac8a77bac24608066588ffc11a91b0c85d3f078be13ff586f7cdc20a23473f7a8c800c039edaa2ea42e15be9dbd47638ff4389b0a4e7daebe365aaf47ddbc16d3737ef8f4ed64da4ff89064056a794f9232ab744f72c5ae7fc674890d07027dc5541eaaf8dc1edc8d8fb4d5c763053a53697d9e5100ad9afa1e9fc888cf1197ad56d34d0f290b5d73dcb191ca444671fa433332f9d451efa4623ccaa1ca061827b86eafc6f771e184c48f4a3ab88c44829cb86aa86516cddcbe07bb52957e926dbb7bea95e9adb19ab3f99ff83279b5e41dd5cab1b5ee617340c8b46536a4e0b60d7235423760c2a72910dc838457a5a6c2d96bb1fe3c8b1cc292af040bcc5fb030d407a283dab7cf28b5fc8f651afb812b2b138c2dc05c2606320fd82ebe8ca7843ab0058ccdbdac8395bd65df3327fcbe1d8ac52b5e8e8970acf53a46d1ffa4ef16a472e24746a05f13399a7613a2b483855e7d67348cc8b51afec047355d3ba2a1f19d1e2fe3ee7b5137b4d0d257942118700ce0b506ee1b8b0c542ac600ffa734a960b60c13e89d3f60c5de14e3a557159326ae0eaa2328ee3d57ee2406ae9dae30d2bda566d5e22a2bf051a5ca0485b32edb939b949a967d315c0b771ae169433c6b683e0f58e5a4d874e4577d8ec74ee5466491a61cbd96f3d8ae11bb3d5818fda22b5f7914e82ff54d868983315df52ed8ce53b438b6f147343af4508813063e68b6416ddeb0eb0de1c494f0b40211bdf757919a28ef984ca6091f15f5953e0c8250fa5cfe4f1f5e54acd9faa8b5efe29a5daaf0494151b73f6dcc9f8b08558535b1b4887d275cd3feac9424be96efd63191c6fadebe8825917b58a7a286cbdba16cb3df97f1a94c1e41260bc4980ec4ed34032e8c714fe1ff769f8a79217a4a25c1efc38714c68eee4b7f8088e75fc044349471caa4b7fe94793d82e46e81f7bcac3ee933c043bcafb97882ee20d9d45d7c459934926b3079411afea06ae8cba6f092824313ca9998e067051b638b77ea5b90fb69af4ccc66218945554d9b78c3a5ea25d7e4eb9d68bbb40cb99176dd8ced6478af77e10570e5329683d66a2dcddfbcfac27f44ca51e7eb5ebe686e3bfae17a8415a2a13b29149c4392e4226a31225ba4af8df06e20d34c0a0b50376c28e0ff61f55588f8d77e481a3bda3eafb6af54015a2fe2b85c1292d883e4b074123a873f7bace327f77b37239841b1849e87fcd9f1f4c0b5e02ebaac21e3fda7c1f6e5c0bbde58071ff0101cb20fd117b265a8e87063f0b4c0ae6342af61107c2c2d0c9a68b39e8e609dcaea876bb2257d5c5a1e09c0451782ad560b8be7489a4c1e446b1fefe710d1b5d5fb2960d05e931f6fe0b6b85d4a9d4609370308774952614e889827eb7fa6017d5ab5285494620d2a3ead19245e4b6d389d503e7fb3faa7d71eaebd63d106bddee15ccbf80221a12d4cdee182fdabfcb8bd84db3e84cfa6b67d20ba17b7f96f2c9a5206dbdb251094ea01b174f664e3ecd145b876db7b5f900e23ff823e32cd19ee8a2dd30265e196615c6ce66d688fd344d68ffa7a7ad53e1570e5849d06a9d35c26cd41edfeaba7c0fffa5e2e017876d4a10453c906ac9f569ebe129692d49e6d4bfa42a4fe8f0838fcba8107576fa0b970433e1930c575f5827593bacbf77162a8187f7befbfc427bfca2273d24ddc67d7dea6116a1f25a02baf10460040120b6570c01b69a742d260351e64c1911daa80a4c8ca60e2c94b275c864f76a27f8edd99ae770d666f359cd4596978bbc50151ef313955cd867e9b1d4a76d501bc9999c1a5390cdc76333912ed4a3dd34290669a44392917052b1867d68f5ddd332f64e380d4631ded0ebecb92d09bcd7ac2a6f5f39e251f6b1070d614f716e2fb95d9f26aa8b1f8f201e6d473b2ea2624eca1cf297934720ce8e708b2cd1b992a9296232cda34f470a6d79f0ae5f2025f893bd15028988e14dd8074ce97010091de70685584e28bee1963f58a262c85df328377cb0c5cb3f39fe568684439916ec121766afe27abe8fd25ed47c06700b8629f27be989ae3e439aefec3861d3ddc2eab0281868e35001e26f98d7bff81c63c22ecbfb48060d0d8d1235ce28d9080dd2de2ac0408d583b8567309c4dbb0318dadfdeca8b8815fb08dd266955a05101c8a2432530535333ecdfb4f428eed385fb25a101c10a1f06e0d921978f3ea9f999459c488dde6035c1a6eca88998528f8e144b36320d6bb688450c9d394c3089d509cc8a906abb6c43a8b0537707c5bf7b1a9a7e573829fa3c9a415fb0001ffa4b4227e15f1e1f22d65996933cdf20ce49ceff76834f690caf6e012e209e34a4e814ed62d25b66424d5a788a4efc7aae897eb12ae3e51665f478332181fd04a995696afa831ebd6056629bc066cf36ddc88381d3c88fffd68e3cdc1fd9b06105066119994ae81040503f1f0a10c4153a11d1e609adfb9018431c31c1ff2807faab0e7af0239488bc063dec1190981964affee89ab22faaf23e3c24cbcc7d769e7102208d576bd26a640878650ca345aa680befa367dd105b3ebfd99391d53c66afd6154cfe7dd8813fe39059e8dc5027924e25cf94d13a9fa4ba1f16cfb442c8a8d3f898d141d9efae49249fab1d05cb3f9fb900aa0ee6aa232911822e0c17998201c896ee15deff92aab0c34413d132ab731c682c9f3b3d745e6c5e18e673c16a56c4646ec0c2ca04654bab28bb7a5ca579a9170ba9ff3e5363bce737acfdb1eac18377e33978ae4a82dba9bbd4419221e58a10ae80f91067754051ad9c16606b72752f43ecaeeefd9f0af140d0d4461a3d890cb07cbe2a9b001571001fefcc7f0e1b92abaa097e1566d5340853515a15fe4256b83ffe2b723e60e206b75f27ca1b99b1164484197d115a93f85aada209f8ab8a31ce3a66f23c820698febb8e95ba96ac28fca74bca0d504dca4cdba7e85bdaf7fef7b0f17d4b6fafe4c3e9810ee9fc1a83466fc3875eb8e22b2ad6e068b9b18bd9f60f9e66f823cc52a2ea0eea5a1c3e7756d82811c9da030bbc3a51c1150ca8cbf5ba38b106b185e0ac4331e8a0d653a66cad60427efc8b7dbdb817999e346152c197e1b2d61a5a9d7f2ed328aecab1ffee39b76c17dd3577cc399ac7c7e1cc48e4fac0aacc1451793db4f91b593f0ca06236425417238ea6d40e1837a9928af864d8b34fde1cee5bc2bc7e69bb9908c8de49df8c821f0fd6b497657b4ed5cde9502e86ae3e6f7c15189a96b31d30f59faa760de83831397c7f99cfc7e23f2687aaf2ae64deba1902f491299ae927ed9c10ff485f576cef160dd32c11e33003c9eaf468db444bdb715d97acc8df5e08e6aec88d91f747e897fffad73af7e05b0c5cfe23deb931f489763e876088841e9d7358c94619229ef27149b51f0a322200c103e80588e1f6f31a47f40b3b858d0995af11b99d7baa4c7cc145d58e14c240954234e32691001e9cdcde67429cad3962f37fd84a7c61857540799b8ff3d3cda254ba05880e680230712a0618054dd0c89b862e419172a0282277865a2dd45e14a49e31e73db870d70f238a29546e54eb601cc76b8837ceea671faa84f096bfb4b7eded007e12d2efc4f9ff8a421b2000a75c5a01695ab33f027429361fc9f46d0bd883d96a1f9b4b329a9c7256e45f13a298590c0ad15e3c16c73b541bef836e81ba5e18c844c1165fa23932c40287257bedeea72af52893990bd5a6bb57df63a2b6a1565680945f87326e38508918c0020e7f8cc2a26e381e34d8197cc45e0dfe975331d65494aeabece73b741ac9b192e930439dab6b62b91a9dac9f909d78111dd62a5f0561f5f47a9a60c5d55f309ddd4b7b94efc87dfab8effaba010041abafb8757d0f681e1a25939cc5ecf764bd83f2832653feaa0a18d86336aed2c73c84199afdd03171e74fc96b865d0bf7f4f5a54c15c823f27b209b5d34b64cedc5baeb0184b2d9524b012a2e4fd70dd539db32f6964c34d16399a16c5499597423149a97723cdf5b5deb33693d456213793adbe52ec75ea8934e38ecd9299ff8d57ef12e9b4e14806d103954b86321ea42aad93daf0ea876befa5bc2a7659b3608e9efff93a7b5201478798d65d892aeae9ded2fad2ea4007c0cfd35727d44f8ca4c7a52b4e331621e7dab1aa1f7aec02c7e7c3ae36489b421042a9f331cc609990c589f1048c10d1ff1873e9a871477ec91acfbdedcec1cc83e70447a117325ab16b1a69389f4f786acd857064e82ad1da1257e3ed9820be84d6972a5f90ccd96c6409124c845a3feb23a7b862dd28e094e2620ecc39fb590e6cb7444e66f202cf8922ccc47b807776a672aa35e4434e27ba331d0666ee19b472556951fa250b021dce47ac13a33264fcb87597ce6a7d66391d19da9758ef1ee7d19e1f774ce54c5e92a487735cbffd941210fcfc8c42b7af9fcbe376863e4e919916f0d0ccf8fd05d1d7b3b41401864f4c179bcbdfff434dfc4dc4171d35ce0887a0ac8e3d17f150411a09a74e02e47fb8f537f3add4377e0381015a79a5fc12fd86b521dde790030f0ddc7cc34305c0bc5e9e3b22460d405a3cf28761c0e7ada75fc69d11430c04bf856870b689e40491f49f76d18b6c62ee4a4937d2506e5658b2e71adf7fc9aefe03e6fedc0251a99760209fdad863928ff8050c39c8abeced80ecdb13fe10f0f65045f2e2d41958e49bbb4c4fc1c5fc15fddc7644e5366cb4e4416bb1a6afda3a8bcc5f7c51ff01135f7643dabe355f4cc241943521cfd15ea8c2e51bf44e17354353436ea6059f90e393790af9a011bda79f84675f83d3e4ab18a7530369a953490c20ebf4430a5398bf4738abe51a6e43974999d316259501c60c8d3a4efcbe8177833c67658921351d44588c2339704b9d6bcbe30f95b94328cf423be2fd387321e447dd5571a0c1992d091f4397fc47a835e25c6355488cbf4e2a06984410a8df4fb8bf596927531d59bb74f72e147453de54b4b82c99b836fe255878b4156eca6caac99209c9f0054f32c2138c9ecc834a2640d8ea4d6cb521bdc12f12d055774414ab86f995032ef0bf03ad45f1403161b848bd4ef4400e1778f32f5cba7274ffa117a1f3c96fcd6f0e387af6302ce40ed69cdb50b88c76388a3b80c3954c710f6617e44a986cd65909ed542614c1c6f80da78461192553d6e077e9bf8ea20b6016cdc0c995ae07bf473df39af8d0dafa160f252d58d394930d46ef2b2f0a7d1e48d8ef2aaac8a9ec0b7f00145922883de52436598fdf1fba9f1283d31b70ce0ae4daf3db610d970ec2582863e40458de4bc082dfaca451d756ef83a93baa3fa37b3f82ab8cdaa487087eb9175d846a789099e344b2ec58eac67185e6305a81f5f691330fad31715f8fef45e835a9da43a571a016a35bae46b3415ac704c66bc651fe079c797786e047187eb8c3eac665b521beff69db528d5a49568f2eb0030ccd6d5e19ad4adfb271ed46d44024356808f77e16573c4f0aa225694857c34c5f00bcae4ff15c91bc5917502dcb84b78f04e2627794268b54d9a670f0b97d9cb3a5ec637c0eb9f0922a70ba445fcc20710c56b7b05b902262c081958163c0036ddd7aca4daf4814ca4fb003b3f59a76adb7390155e0dd635b8c04ca59be1a0768463d94f49a021efb43645da70770e4b2f4d00bacaa91df4572fdc8903ff47df32da286a5f14a3ed8717e537b640aa123f6b64e5db9ea7e3abe9907fca059493075091e10815eb342b85326d35a5d978515882b8258c71ad6eaf9eebac6ef8284e4d7925d40b48fb1c4ed93ab6b4bc06411cc178e8ecea37cb50a7bea428417ab63dfa931f0885ac82f5330764294d0297003418e70fdc994adf9d0f23486520b8caa560e8c78251fe9c95861a45050797db4586e18d968f6104dc18c6dcb6652ba7f5f8504a76cdbd56c2fd9bb5c3b33384ff6718e9cf70fd97b62a4a2384807a6d5e9151e890ebf9e797c61b0eef785cf25b6e1f930320cf1ed97cc43490f3318d74cb4a5835e742f8ff9abba5562e791eac25fd8450b4f5458fab279ba50b06e2902d04d1f63dd6d08f515a87b1f13822f7fd88e464e384b2ebc91c2ff0a834655c632b5b7e3099132fe6adae081fa6d756e57949bdfaf00a3474b5b4b47442d160c66ad76d9bb755b144f49553feac5e772dfcaa944a31f4ecfe989ff4dc092525d7e95eeb938c87e22de2e18de8891db8fa0aff02dc0c936ee1c5846f454e6dd5a2a8a84111f213242ca48ec4a0ca42de208114b425884bf754f9891a511b6e149009346e2978a89df0394777a636bd6d6cc91cd06cd1395d9c42cd5f278e1ed46ec4799910d6ac90fa9fc346a09a83a27a096602b4269da59eddd4ae1c2f3f30cdb05131399cf3c50c913c683ee1a95ae748b674ccd3ae95b037aef9c98bc4693eeb2c68ec35bad4dfe85ff73b1df1584d3c99ca62f44bde2d01b4a1a20ed9e8309042005d373d3b93bfdc4ac7899a2f4fa9498275caa522098e7ac5d756414e3acbd875643c2525229105cd82a32669590f23573755c92518134dc29ed20ff7391a7d809a4a90d4f3ccd98417306079861565f24088c9f7145f44093f6dc62ed87b0b877110d22048df35d11a9c55f0242a1a4671ec29a9089afd2f394cba94281f8c21f56ae82332b7b04a61d99762498993fef839a27bcc305b47f0746f24394920e5ecb28ec54e49e6a6207c63fdf445b561ae777a5af4717bf84bbcbfd27b47f5a72308e46589dc13056956be4ae308bedd8e7e8f0e796679de5855aca48d26dad61e97f1b66681ab7d3277c7f77e1d69643db6ef966831fec812c326dca60b33f317a1cd6d055aecdc40bd5bd2199ab738595dc59d4fa794d63d7758c06a4aac989686cc8abd3c63122f089cbf5398e933313ccdd960903eb597954b2749b848056b996f5949ed81acead2823d11de1925c547b07fdd7a0764e65d42ba4cfa5277424bd750b6dd6a446752b4b8821949fdd4460c3ef0340de7e9687f468a9b77105354b55bc5eeffba2a27d845f79b7396446353efa313d4422bea82d8bcf95897b3f15aba174e91059115e5e0bd02dd9aa92397f35a424e925c297d70be682f17c470035b58ea2f17703c86333e5790dd1630559e4d4a0313cf7b069bb2467ed1a53f4ec4e2587c49d582a4b21ea988e620b91bd4647297050d64aec86b628bcbafc2106ce8c6e8f52101c0fbce580b18b309d3fafef0c81a3f1c7ffc66585b007270b3868a45bf9168ecbf82489324ca7c1feb0f99b27f9d9a294481c95d5ccfe1dbc888d1e2c4c506d375cbc89925113d25c47ffdf60818744c832a7abc4ab36dfb4a13dff1d2830e9e6fe3fb843a442303b1eb8cb8778ba9b3fbb273a8a91f5e19e09fd2a37738aa63fd26a0b630bb3b36c219b79831562877b484273e72c8b4320ab00d328c3bb1854c99b6aa8e2ee69d767cd5a205c8982ff75ce1a7a46cdde1651e3f8f1bd79ed3eb595e9efe010d59ee3c3fa9470bb03c285389790f8ab442884e4d64b0a479066e1f62f2a1561e0012e44def9309af5566e94d68e4f5ce455ba4f9afb0e662287f87b5b96e0b215a1b9baeb8fb46a52e711afacd1ba7bdfc50e1ff7305ab07264186c83ceb77cacf008b6c10d56fe1f4610cd05844f7e44af4f08e7d7d0e8f55d0f3734956fc9287e2a8294102c2d97ac21f9d741b32c3eb9086bde7712e164865bbf1a84f14d48e0b38f98ef54f06af33c916db45e6abbe3370c9e47c5a5f5cf73b4b784e47bc3f5574155c804c18ee4954f7e89c50ae9ba4d03123093a6500ded8a6118f7dca1fcf62614e68f38d2e598c54a065f228eef188cb7aaf1f41df6f3d4d788a88d204e9e285034a0c6c5b6b894e232b844a4696a3ef7d8f0811e32f6bbfffd557dab2b588eb918d8443d0a9f4a7c82b42c275bca9865e7ac38ca3e49dd150fe2f01fa8df60d6f581f050b3bbe0ef416ff657db803c0c4995b9b871a38a5fab90bb90732766d35452dd0ac64971d75a65d744baf6ca2340c26934f6527c29665ef128f17c20d8c1fcca12fac9d898893a664c600896a922ad526c6283050ae67863d2ec2f47eb01b1a3e19531a2213078edcbb953b57fe701f0445e95b6a78e45d1ba06e3b9077847e1017f6085f75d3bf0223c98fd32000d555b73e6c51ab57246e15d22531693894f793cd27d8fb68355030b0f7a357ab43192956b96afc377de9b07c41d30a6a23f461c47f40d5e2a023795c06fda75737e2a5143d325b86d62aee8500d1a76c1a96269817f465ed46b40140b7d892b68ab40d3430cea6f598b8279625deabc631a7114e99e43a36c83fb7d37a70d742d297b973fa7e0caf2b47ac62f0750e0c22a59ca5952ea74f0b59e16a1e3ce1b391f1a9f28345da7a5f0b37064ab736772f7941bc9b7c758a4c56f1fb5455ee551f5e9e02e8de051e339bf90ffbfa75c45319cad15e2c5aa87b98b604568dd1eb46c9b92d553c5c4268a1a2e44707b3e6538ebdfb5f504e73fd80b0d971a6c280fbf80a35a4723b9e1db4566ae59354322f6a6a2ef45c60ac12ea88edb34c90fbdf7b8bd8708cab7bd1a0b29be454c3479f2629c5018fea8285f03390c355e0accd89f6824d449a38e6d0a2ff8690da806ee42530867b3f363b3d49e738df1563c5600702ecb5bab1726ba55fd0d4ace1581d4f4108368b24fe4d4090b371052098c4974faa7e46e82cf07589822a39aba6b03c2f19746827fc4d2f2ba82df5923d5e2876872d840b657bb2646c9713cd61a83510e5d00a754fd43eaa7fa3a2509a74bc7076096b720b73ea207b4ca9aa638b78f4c0d4a4257fa3d3de17c30507af0a058d99db8706820cd18dad632b954374503edff3191fcbfc5b2b55be2b0b7504de5fe097d97147f788b55f6332331101ad4204cf5b18173d4d0809c7d9eec152c5cacc981b25c8aab74e3adfc472fa13e6f6b3212bf2293f96a2482ada1f4ef9dc524c2499e970eb4496da67b6948958f998dcd398b3aed89d5943d1529ffe74cb7a7c2df90043a24ec03fb8adfd9ed6cbec980b54d199f665e755060c796c9351353a23aa988ceedc8cee0d73be4ed1e3fc765edbf7d8dd5e102cab4181e53ad02a5fdcca3c534f9110e4e7f292f5f4aa5c3fa49ab9877cd3b1c6196f62ccc3f8fd616e08eb364faee03e79b96212a1f1887b85b3502ab1cc20df3480d71aa3db7f60ee2a3167ca7a530698c998ac2352f5043b632650175dd345a3bb805c2fb200eca4612732d72fca8a5501d73777b3bba9cc398f16ef01998e922d41ba8b0bac453b80d453b7ec869bd0d79e9a87cbe0fcbaf47a11849c6695f3b877ec1df5cd248bfb2296192da2af337d4767bc3008ae01b8228d94efd3b1adec17a5f9b2a12ab50175246356edd18bd8d5b363d1bdb7f58943d8bd84d91f1f324a74c654c6307450612e94635ff12c03f08ac4d9602e231bc81eeed25901c51e8c786f66448774e8d79492af9721ddf0e37a9c168287307f2b519864c2f897fea1d8a4b53bcc9b2c5ddc91c84b8c8b090a09da8fee5f148abef2304a840444bc005dd7652b89ba5576f6481891a8a71397348ac7829bf53e3c58e5e095567ebbc9848aca8f2b8d3b6c09af750a10f6a47bcc6a45480820742de945173cf4655d93916154597d1b8de6b5b01f18db2ff6176f0cb89b68f62a1bd48bf7ffb44274c599670ae58084d57d85f244811901762194be0023975a453bb177f136ad9495e9f24b0a781da39ab7832855952722ed031fd1820aab81752d67f23f30a6e3301f3729f6cb068ec65e1ceddeec8b2c163e8624f574a65cf8e7b81570277a08e035407611bc2b136459c96dc986f140dcca94d6c69784c79ee9100ec11d240f95c7920652b63d056b35fba4582471c23a9e5dfbd717a689d9776fe49d1f8ef8fc43b530597b6cc734edec8aaf1c19138f7c65b21766ba935b11ae420d2fd0945cf46e15868805dcbd39a9223c32176cdcc19f49db6ee857aa5b6ec01185255871c4f65edd837e51082cf069fbaf7c841f13eb91cf801ad1de660a0acd43fa5b2a7c49bcc61b744b05d25cc605c81fbfd829032ac6bfb8f328685938eedc8919de180badaf8980ca0745b79e18bfaf31ca778080ea486602c2f9b80db583b32c07353f23b31130770b37863f310ac5c5a9560b57430fbf5fc37359e66735c5867012f7059418714a76fa7c5cabb914f3f9531e4c8a0ff7f8bb5bc7b263660982be7e50e565c0ade5633d94f5de6c71ceb4af1ca4ac2137bb7c02f34d88860e312681e195320727fde90f17f982d04076ab3fdd2357fdc2037db0ed17506f194426e43b065b70ff8460a6234a93d0a5c54024e1c7593815c108cf6d545718b06178fdd7dffdafd222c94f649948e1290cb85f8b166d82b7ae97b3e3dfbf3d0f381318b0a15eee35bb3877e3b966181b5b521aeb4ce1bb11554e7f56d9065e4c849e2046ab6470bb9aa3fc407b0b676e60a60ea7570b3260c7a987a5b2e949ca039f3db17608b6586c169099482869b07f7325720e27f56d07a4656914b5328648296fb66bf95b76578a8fa4fdb83daa7a395008149fd3cdf6fdcf1c80a236b3a1a96e511898cb24b631cc56aa7c7173c36162bf4c2e4f5afac8da6c4385ae6405e88b25c719a464b9a6eb2a3453ed21f62728750af1ebe768c7205e8bbbbad32f61e1ddb6bdd2556b5b21bdaa11dd85717b5eb28d995889ab0b839d8c4a6183d3804cb4403c361b0b1d27bd0cdfb98631ada8be88e17ac77f2a697ba4fb4d98d96c01400454e9d80ef89d29139079ed143e50151bda9ff1f43eeadc6cea6e693d74f4cb1cfe09696d396c25ff629064aa2d8fe449bf9702b19634605d66471b2f74f6daada1d0528fbe4c76c5ac2918e48f0428f0d4388073784e68fc2c0cfa83bd1787849fd849762ad5bafe0ee3ebf8b0599b5a98a520a5be01732d5031bf92ad9ef0c235c582f4f970a502dc4e38ef8bd6e7963731a8e1fb1f2d8bda64d5989d9b8e8971140d5fc36ca356330ae603795e61ef15758b603b9cc62fc0fd03adc9988a7f62b6a18b60eccd1bccc3b27f03dea73d9300f38d7dc40123b365264b04aad43e6857ce22584d3ddd0646e318e204412350dced56e02bb8f30ad1cc553be2c11c989ad8969ebff9c3be0db0ba9cbfca59e7b053463a5fa16ef00c33c308df36f2a8d994e1d1d12297eca66ebf0295772d0e9ba8e6fc8d6fb36d74b86c4459ebc2001110da0b6273d04f8b0c23a16f64e405d72515578b0f2111a7e9c438fec08c60e21b42e396bd9cbc1eeff6e374978fe3e39d75e8fb3eb96a6fd485e4ce9361875470a5c3ff43a2efa1a00a78fc104bed02130fb180409ff8d2a8472285a0016c50a9a41901a4902c19f21e2c120044eacb6c48d3ca1ee79780370d47f296d8cf2b8d473b0e0dcd5feabe4d1d42ae77e7264cba987404969dbe514d2b1ef5b95e06b09d2688f2503ceefb50d21fdc4a897dd6deb6c4ca27eca219c58c1182383a04f500798a3a714802a2fd33cf2cfecc8719d925705e4fd8c491932131413b9f5498acd0463b02d4e6345a4f5ccb2c95a9ec60ca4f6acdd6ae0f2ab1c1f78ba945fd1c8d0d77b374281b49ecf0fae9b27afd520e577c01460bdd07ac1c521f94f3d21c4a87871ea9dd103323ace0bfd122a4993cde5584b69f58f2ecce55e424232db799bf2441aae86c51d22c8c9f4d17b94f786157a07dd8701a1dbb2878c3e6b3911a1d716160e45f83e7565d364d4b3ada1374b7696061d42552dbcb7647c8e4d91ad3e1846cb902c373b5b36e5a09b8ef95628c6c5f846e954d6c02e446210ae89385453948fe1669e52e185f7104efca5b505b14d17c0e6f9719ce4b22f71da941caff7827a8311a0a366afd8d274c76ccaf108626260bcee8f9c0f1d8bd5f979b6b8282103c2af61aded55e0f5a80b2b192f12c42b6929364648d9b4b5680b0ed1a77a21086910bb0c041ab86fc2b25aa395ecfd5d255413eac364998a091181ecc091c64050fdce03e6484815e580f21eb07e6267719a0d88b2291648fc9ae9ab444a5de7e5aedab55d27ce3f400c7dcb57bafcc24a30daac9eae10503b52054ccc8b18220418a850392bc20ec3f768b3a6a236fc237e0a197df2a164a3e49d4133423226783f49c7dd07cb840d128eba5316878e0f85c3ef30bad7ca5ea8a681ac8f6a216abb0d90ace55f37c7179797e8715c5640a0daa39981fa48ff73125f2e1ab1591402515f7ab7b533948d7987b3f237acb2821f2746099d4b714fa228fffa92f42d0c9929e912ca5eae07e07fdd35e62de79ab35f49543eb0a03ac1acc99570289106966173fbc2d594cef0da9da3ce46c046802c1716b380a307c10496b2bc330c2d146f62ca8d5dd254e58fb846bd1a3e50a1faf580735a473f6e40ce67a4696a4612098ef832ef13a82e756bf9b6431a598a12ca8fd80e1c1c52b6e66ca01f6331ff5bcd1821cf35df7e1288e3d72ce727ded3ad0ffee267e28737607c70aa57f72dc4aff85027dd2095866d46b070b26eb4cfaf5b2e5591e9898acd7be659d7780f9de34fd04aad926dc2cf06cbf7992965e7db0ff906029eb798f09924500a6ea5454425783a4f8f6cf623753e36f13f1487d4909bf552f60b64467def6ba257ffc25aa955b46ae2378025acce6a65e90f2ad6474c09ddb1f24ed893baba3facfe0033fe4e5649357d59a4077684a69df61933829bdd84909ab6d684ba7bccc57b5d4b67aec5a1ccfa2c858017c579cbed366c29b6cd1ce009d5879ffba0c650dc18e683b9610e651586954365d8dc567dd0a80f7785b3359e6371e699d57c61c08b9f65be0a23be376f5241a6688b918bf67f32e4576d58a6af7851edcb66b6b8745fbecf1064cc60ee6bc3de69b18cc411733e9a6af08115c2049018342146a9e518299b1e1631d9a2bcda23078ad50fcd658666cc7573031dc837d59df6836f02583ff70950c495b82c8ac1c7c1e15d9c4c83ac8fd9dcbdb44c81120f1f94695aa903ce6db48950003a556b7dc06627069806e71398860c3b619f1060a774e16f4b6dbba5c6c831a8a95f290e17b178dfe3f935fa9e897559e2539e4ba3eea81c61218b26c931c8a1224895f1de6468c97a89d14e5cd400845900af012b4d4d6e29f8a79215b052092c0a9e158e43cdb839c176d75e6e39ee6083494547cf175408d7d2effe17cc862f1246be9434d9e12b490f186be8f6ad7d3c7b5bd068ec6ef7962f49207b05af20eb5aaf7e99f80408556177c7a186b7c7432bdb577d7bfcdf3f485b91a57b60780d4dc41def326e3915a319aabf3e82a6b9708245648314723d2a331974f2a6bf02cd1af9dbbba3ebc42cd8d743724a4cf20eceed48902d52b9ab9f3c3e18f2377970a3fc5c194bd4cdf57c51e2278187b0e9a57f77ccbb39b15d7bcb76a84b0defeae1cd30b59f1c1e41f78f665e6d0a1de233b6d37a8bea9a3bb7fccfc9b102ca6fd636e48a71473c867893ce71f783c3659650266911965ad2ceb2003457a5016281f672f94644c5b58d285f9784e53a64021f3f48c949db5940e9153bf0c34d4ac8ec2628eb792535dbbe5d4547f97606e8d0811a4a342fcd95040371af516303c686793e16b51c8c107b6d1f22b60fc13ec118307aa1bec5041f5d0a9d6c7ac570632a609dae75d58a9b8004873b5f805a0e08482834477a8a9562482c88a9288b48afc8b2624b975f7a0637a5da4851cba6e5a751b891c46f2e2d818f4dbd516b7e6b46c83e8246d0aff2459703d9376138858c08d86ef6d6f3228d44218eb94244698a0bc448c70c340bfe857d4f27e988fa7467820742d641d5fe2679602bb5a146549e0e3e67fb6726d85834950e5c53dede23e0240ee9e3b26ca6cbd9c74562d77cfcf94b4269bd5cf892698a91c3ae42726ee9e07c69e6e38803f6631fd69c2de4a20c4f214a774a8b99eb737b4e8da5727b9dc0d0915c7eee78b1bfb42b9825b1d2c4c9442c952213d17cef7c181c23487149b5b681d7159ea45450e0f25e71aee4b9b50032f86ffc9f36ed9bd7ce63d688b9d8c1f084639bbc7c5262e9e0bb9eed8f7ddba446ecdca3984cde5d0e6bd1195226ddf47c895a64b44f27ba82218c48eafa271502f0705ff2e6f2cd291479c623b3477d67712c94902cca20f4d6dd7e09b1d252effa7c3800e1739f1fc220b91ff440bcd4500176a509889a104944318fd4bb4805d748b8a5a09656e497b46e4ad5ee6bd9477fc41cca86b66014c4dabbdd2825ec7256082774a6fe9ecef481b98ecb3237adeab1f476efe315fedc0d82356f60cc17f8ec26d9f565b16380f4d93f273c26c22f2a2c25c2b03e6ab418cae19711b2cf47a297f51dadab1dd129b0cbf9883553eb7f3f052a18a010f802c2d4d0fb40d891e4256476799d7e11ba0efda9aad41939a1498cc7db0198b09411d1760388527bf4a972a7a06cdc7d211bf85248cd0b783624e5c836c2cf3e1406d8427001ed79a8663fb9174d20d2a6c7adf02b5a0e1d4f7446252e082a0cb1bc524cef2dd07a9d5ffa7bee2f58d8579add29521a9ddc944532622424725bed49f2a6f047a1c770ea9bcbd8b9bf99e3e7ab09e8289b495780a2eb48ec0c45430942e7778dcdccb1de1411d40ba7de1f29cd3270d63ad58951f58275db500eb7c7781843cbef8360837a200b9dae2ef916de0c7c5c5e4c99bce7c2b0f4ac3491bbbe14c01f9e09717e7813beaf694768b53ff3e10dd2bf72ed87d239d8a7c9be4b0e4cb05e08bcb8d93819e197a462ca20b84ddfc071566f4124668c6c0f3f821fb2fc49d1587b58c87f56d3bf1aefee1868abcf44968cb526b7d572e9524bda19d38b57d55a13e529f457e1e2b2f8141396d01461774e350c1155320997151e870cc57dcea9dba96ae9962b0da0e26f21a55e8feafb8ab6383620d01c2b5564bcbdff7f79cd43034846f6a2b20b3da0717f99d394ed1e89349ca3d08a91e5f9595cb8ede82e07bffa174a1c99bd34887850917f10af5ade7e2bb7b1676b86d80e68492b838d6aab7f3f38f7463f2b944c8c2e8f050f93dd47b1ad02e3d7d3b4bcff207f6607eaac83aac5c5aab88cef53f6cc9c25ebcf8e4dbb0027ee977265e85dad4d35a70af240e1714eafe427ed99e7df48dd99c16e9e9a4ec56b0ef79b829d26590cbcf396bf41e06952a3ed7594837b434f5b90c166508aac0b6ebe8141e177e60caf14fd1acdce80c56cdea4182fca2da0b30ab58cff9b80a73302027f19aed6655fd9a86f236bcf134917ed8d7525c3447aec2d2207cb6c4b6761e885b59fb113824188068c32cdf08e973e0fc623e67fbd5bccecbf1ed00a0012df83dc254ebad178b28601d8f8eab90fcf97ff9d0189f9588fbddc7a1a7ba27f1c20427c61a46bdc2873bc8ef8c546503bdced0742951e3dc2a1755f1b0f042c14d5374d68310f10a87fd66287409129ca260d1e9aa34c0976f8cb5bd8d2daaf2761b36a5d3001bcf5fbe1c9b2b3c2b59013d6249a97f1708eead3eef3958ff9086e58e027aa039887b743eb03e02af6d465935df9605072207b6c595f0d9ad56896f010c8f7014cb9eac8f2187ecbe589010cc6563c2d9799ce3e10b66c546e9ee22e814c80d911ea853de53e83190286a87cf262d96441825ddfa696368214fa6dc5658e2c7aefdfa1644376b467bd295c9f1ac597c3a9e8011f03b02adda1cb0f77705a068d6ca6de5eace04fd4240d7972c777453e4f502f0f8d6f7861748250e448baf04f53ed5192ca782d008ed4159215c1031c37876e15a28988071384783d0983bb111940b9881f9c3b4864d2d220d742f9f1dddcaefc58bb9a27d30142b67fb074df8e9324c7bd3452377b6b761e2ff614c6f00e476fefd5396e6305101926fe8d6d48ce6da776b839ed7e1e6e64ac6bfd4dc597c221bb813c741912ec952a1611285b0a001adc61e51b05715129f9682252cbc8d40228401e5b8da773a3b44b23115d908ed8864b6632e95b676914634f4e779b76c4d920b0ac381bc348f2a7b496b90a4306b53a4ae2b8ddcc99b5fe0a10eeb9dc2081649237d24d52095f388f0baf408eaa1075ec09da6eed829ea2031cf95ab4365c9b73fd3b37305aa3a08964c995fd3da9badb11751d98163778a02abc1a08e1d52a4f315224371a8e582bde2ca33c0515d238927e1ed7712f03724b6280ec30f71fa84a30974a54bc0fd48215b6f062216407db840ed952379fc0a8a8c8a24bb1cad4c6f25d00e0f5a84252c3b9d11c2646de4b17cbf2c1cc6d651d3d479014258da22d75789291c510d28f2289b947309a42a0d8ae0f44a173c4c9eaa09893a271d8d7461a774027c234344c4c230ddaa857e1bf4dcd3ad3735f5b4d3448edf90c062e85055588062916434db91a0032c35b6d031425ea9eba1fbe4b3149591c90b5aa94338f5538dccd2654acfabf47daa81da2909f0988ddd16a61b2d4f6565ca852d94a1332f0d0c87c036ac4a9c867ad26030f216e054c9402a49fa5b406eb658e5f0ae9652c0c4575f0872d6065c0264afe7320ac52c3bac84b7285d0c07bac6eab329fdc8131604afd108707080e8df0375dffbc5dd180a798a327d50fd7e2279521b2899888504b9a33dcdb71f89e61093e5f128b5515f00eba90b8f0123f4edb0a485bc1e754e706b7aadc93d89e72e37df01ddcfe622a9ac8e33a4c30bfb7ebcd14296bff62d931007e62c97690636242af6d9875e260faa1d1aa4de4d906c679281d298ac932818fcdbf1ffef2ab9287cdf8662b6a6870f46c497d2364c0cedc1001f3d3ef7b34843f74a1e573a15d91e00ddbf851b3802f171288e0f2fde77937efcc50c2526c5cce34ade8478b148ef19f9454610c2fa62d8cc3983163be291b748dc4ed563b853fdd82b45d0533f5d5585b8d25f6c08f2ab378b803cb0e7802575fbbfd7ebe91c429db8766bd825f3b8ff98c76cee1a76105e2ff7685f4705499108d920dfe2389c2c65222f4ad435c78fa86ad94bcb34a168328161f74c445c8df82f296c5a6fe90d40c0b58d67dd0a07890eba26e2e3ea159ee0265067cc64a364eaaab228a60a9d4a153327cea5267f0aa7d6767ba2109727c244db2ac2d428e1442c200061d33e714976203a663913cc343b973a81b47c0a6832f5e36810c31f2e5fb9726a2a2c8bc7eb2c052125b5c287cefc7362c646c50427b721781d5fb006d018683ec7cc115ed664284748147adec635fb1d8d7e2f355239cfc55a325865523b44b795d389b21e93698eebc368e175b69a5f5faecfb58b96759b7164f193bbd5d2ecf5abfa2602cc23697cfddc6cca661ec574ca2993393ff985fb918fac4435f8177f1c84b8b51b98ec2c0a3e4c36fb09a181b89d93ff4082c9bdaae020a647728ed02ca1fffd408817e63f6d6f0d588873a25a3b8c84b6b6e9d1890ec888aa75eb293463848db0856e34a92be0183e2eb6b06e9ae045d7b9bf9714649fe70a1a4a0d348dab35962e42989d9c2ae38f92224d020ed5f851a3753b581261da8e2ac9511fe1ec0bfa29d3dfebf7a52cafc86ca47c3d25b89d850b20dba9818b375648562e28f93efbba12cf14b7a4203eb946d497b2db4ac73952e72064a48d6e894a35e5f2a82713b81eee2a2c754bc6ee9fa3ebb180c6bb438b9148c53f40c5076098a89a97be4952690c1f5635671f6917c34fa089850b77eb0a4b323ffe8abe46d9f16feb3fcfd7f40f2ded29ff70a9debe2531f343d0e1af3b0764bf5969d8b9fa03429759c29d22701a66bd246c6ddf5939136ad5eae6fa8775dcfcd28cd4e42ff7c8e05a5cdb1d99577a28a4b37aafc0d3f1553e2d2f3a31ffd3a1179e0b0d4dcc248b3b0ad89e3379aa1e404a85a9d33e4d332b970391c22d72c0c128e8c60103cf9a1ebdeee02843e03db59e1e9b8b007dae8a5f12ba87744bde58a11a1c5fa8e137dae0615b7ba3f2b0edc6697141c4b5e4d2e2c00bdb091907501ff54277e541ddf29897d3b90b45de171a78f54f25d7888bff7d0c5b0e3b061a463a093042a9026cf5112142cb504f9906f35cf99ba6726f366315c01dfbfe99d7ef249132b0c3a687dfaf7da86dff39929ab822328798f52de6d546632bb5b866598868a8cb3bb366fd9da63845e2c893d992201349e422e5c912956f2c21ec96db9a7c27c9829f1c7096dc2685ef66294f47023bddc418f91a211e23b25ad2991bf3cd62a81c4511494fbe06f7cca4c5281e14307e52f9da671e7627de5cc5c089697437bb683dfc9e0dac291d47e18586e0578e1d5365961b44451e282f6a54b928b514ec459c54c96e18f98851ab7e15348e34f47b932ae50078019f5d45f42d36f0b76fa4a8f7bb57952374c8e9ce09020053f97e818e2097212edd14bc23e7a165296c4e7b6939fd0720a865e05029bb7590f213541f05cb8edb197e6532b88741eb78ffa6c1103e7be4565111ac12e5c3d34cf595df0700d2ec64d312dbba5018218cf5a2b3e571c79d89f066c538ce63c1049de02982e7e2589da2eb5276a751aef35a676702a8033d4b491e03edf1a70d9fb16c9808a67474dccabc91981d6a6824f26441118059f1f60c72c0ee2225fcf7acb78229101f02b6b8421c9def1306a9f452e304ccfc0f715de13d48ac05a3684a130da6440838a789e946dbe6c7f44c44ffc7fd7c77b9497229052882ecfd0898168232cb4d796781d1054973dddf04e09577481baf23b5468b95a5416c76f28396411e7aad4cea8046591f664dfaa3af8985727ddcd8e8e4b87308ad0328ca4294e8b94fa41b4c2d0b137f93e4d9fa24a301804f5338ee5661eaea06be44df99db421712a1e6bf81713548807beb5aed4a1d78ad06f5c245564d377be61744d0a6db264741d50560469a055a1b8593fbf80ca2f99f819290043d4a68248d47f02432a6cad46b49b945835b230ad0630bf1fa12f698804cb07fafaac84ef483affffda64d7d23edfee1e35955111d1147682c9be955ffdc21298acab5c5b297e7d8e590db6f7a45e4b574cbbf30287fb72eca1a791319a4fd9b9dd8e3dd180d9ad7bbefcc28ab181d65ee89902c71ddcabe414cbfec469e8c6e7b11a2888d858cab007de7df4cd209354468918cd9196663192dd46b732e31235c603c46b00175bae3f536003a21eba94080cf5598533d633b5ba4aceee3d935aff8c6b960ab1f1f21f490c2b7d886377ef1f63d171f4b595e4001ea94a332c292fd6adc7870803e60a0f8a9d50607a2d35acef61f285ebc72479c4d4f7b73487caebddc99c65072eca60ec01543722e6423b02ebb527fbd4392ab3f3a617c89917813fe0f13de5712f80026acec24f31162b61da0a2781bc2676ef17c0eb9571c8b83b5669f22041134dc6574447832ec580337852fbe60a707747e4d4120050805fec5b36aef8d51729da48518bb27750752e7f7636d582edc45bb7cc45c6875fb80da53cdefcc845c4de6d75f593d59d06657dad71e7190551a471130215458df3b789228ab6530a8ab03af0f807228bb454ad9426e0b9dba26abcde8c2ce080b3c708e702057587db6e3078f1053ada7cb26bf2cad2c3cd54df2045b575b0f7a8925689d0344bb4655937c35fb3cfa8204a704cf7c0c8d74bfe5ba6c9e15ec26e38e46d44ef37da3466b28469a79828a99f08a4b2acb5b53b6ac6eaae47ff32d31f25b94d88aeb042b477c63e0f3bd414dd7dec80c23b22fb7f68cfeee7f2cc8e9d7d2638055aabcc033d7b563111f7fc746990f1f9bcba5a2c02dfa6fda46c53e19b150f6d7811d32d464bd76be286a36025679e47ab8cc83d79e08cfe97058872adc80e9bfd4ecad14cc948125ea34778ee0660f22bd7f6b4cdfc18f61eca9987ee73a1dde815cbd79f3e9e8bdc7ae1a03460c72326690e3ab6d68892e80ef0b28e7d7d1051031bdc8aac92f4f17b8abe7485c0dd9c4543e9ad148d7884d4bfe630f7b15d084f375d3c2246ab48992d8b251eb12af12b0fd75e07bfe05bc483b3d3c521b12206f223b24d0513faf45656fd70c4e40a2d85b06c14e633807463d35440c6e6ff9c90782677f1de6f8099f2e97cf700d04add6f380b772ff656a372f8bfe91bc16b8151fdef80d3482f7752adcc9f82c10f582f261ccc647958b3a1b34226972dde904d9ea698c2faf61adab492e3bb492060e2c2af0102e77de5bbfcc2dd0da70c87046be6e089a1be8ecd532b60d9581fcc9a75a9f97361290d2db5d2e4e5d216e8aae0851d8e3c5914b034206b99acd3627c23cc0e726c3da152529c0d0aeb2516b0375096af1da6e3e6ef8baf4bdc5285bc84ae41c351716d0252f073f149fa1e00bf834d44fb62b55f240c4cc22200144cf1b443e55e65d53af7ca9efc873f6040d98c571e5c010d88e20b85b804afdb1d42dbe07853e9934ff798f0c8f26190d28e5b3c1598ebf63781b49f1b5f5b944ef56ffff52b6565c5b96667ac49cc9536d1e7513024328350033b9a8a102aaaa49e9ecd0d0c142dc42c8faaab741240dddf76b02071c58deb5704324a5febe3709b9a87a3bd388d4376ece76ec4fa996f07618fecbb57e1a55c9cc6e5a63143d323003e935243c49552f83975569a59306521a267d826b1abc22f0db3b5db89a367f69c17053e309758a791a147408705182587226df276e11aeb6979114d6e6ea61bb45f68209215ffdc5a8c9d6fb7e775600e3710f3dd532a195a893a86a9324e7e1f5f2bf5bfe9383d6857a3243cbd1b8f726af46ab93ad06fba88b192ad46a15068027dc42131b0f482d146387b0b261a54664c81d41587a196963b55526d56e7317eff6f9859a9f580330bc37ba95be2bc8eabc45a50fe63929bd8b37f7ed3f4cb072272f50f61a66c2b25d676ecfc200a67b015a7255554d8633ec9a0025e249a4c746167c2a1060cc09d6bd69057cde004e8eafbfc6bd4574992ece5a5f6d03b9eaee48a75c72da57395e169e26041d6ed3b65b9c306254b06b09f394fda392e26d911e2f1ced495a2347e9c68138d96e1d84fb26f7e284626fcf756fbc9a4575305555b099f6832a2193c9eb6b5abb95fab25703852be6ff25ec40bbe0c8bfd118aa38f0e9554e00ba3bc7eef0213060d25b23cd6c32e5ff461d82124b519220b115b6f2f18cd3371cd837ced26b69814f0f09027cf1338dc424e9da44d1ffd5c3e0625bb2b8936d36f9430345af3e119f92994899174a0df8c0e4aefb3dd69119b7b957902caf98b1174df0e3b6e7eaf365178511f0ed1b31157730a389c558dfb71105d985c5ddf6e60588431d8adff58f0b5b6fb92ec73b16a6abafe862001f559c85850cffa583c07f0bb766af7d1a11d91bd1c51d4b2e760703b0868df98c4c79093782d7caef607b330a44e31281a812a80dd1ca1df8232a7eb9c7b12decc2ad8c1273051620fee9ba58de4b2879394a701880f75a372f50f25afaa95c4bd2318007851e1c7b04898446c8e792c9f467f169978fe22b4ff6d7f86573631ebf795e5c1ab8fe85b76022ae4b1ed6a5efc02ecfae8b8c96875a101670ec783394b3388b6bb2e49d93f35570fea9f66cb5aa13bf7c211ad307df3f270a4de667759673c4c0c8c70a740abbbf866d3ce9758c92732e8316cab0fd7b44083adda52db9206455dc03020d62863541165a35cc77dec1ff2f610d35d41e16841d87a4f2cb1a8b0e9f048c2c69649052bc798e91b8f7b1650707ebf7fd0b24509ce0e83c5d622bd0d4427262cd09752c22f3156e400fb256402f06603d1ca839ee8754f408603c21b35dffafa3ef07270f1014352be11744d77e1c365110e52cd516ec3d7258cb23037f35f963bf2eeee1ed77caaa2618444fdc739b0624540ceb791d912d077de3386641f2a1463aee3c73f61457b2d51ff1e18d5256da064a7cb04fd1682a48d26f6dc4be147e682957815d1e1c31ab208f79c254681bd008b01d9bbfd6df63fbd59a03dba903d39cb51dec5a38fd27e740310d70cfd2cd4d0839ef24e27725293f2af1d2b728dcda6fc444e3e35145c4cac1affce73b7b99181881ecbad24e47775d475fbb4869e216782ada651ce95af7ea1820f71baf1956f4cc4a487e33e1b284077e4a30509cd068d13b0aa146532ce8e10feb52e71bf2bf0476d99b634d198a1b85c4a7b8001a39d05ed4413ad53c1fc242e58a834d8d2c2c537ad6ba9d6b7e87373568b42cc3b8df4717812092c6f5ea91dbd72b59c827623bafe3999ca7fec5f1c4af8b66b305c9b138d6d79af07d0c780a27dad35ef50a711fc23a1bb70567a6754f3253090147984215275787afd2bb2c12787942e7dc7ec996f90be2173d3984206a6594899c181464fad9b78b718fdbca1ff0db5bf9720c1fceea2ebd62f0b81d943e2986eb204b6d7c64a0502c9f60458c349a1da820c25ff4341a72fea8f66251ee97f8f5721a4e691465f8f24c8454f7aa05a1f339581ca88fee21f5b27874a662eba05b2a28a8d10d17a1d8b5eb8d62386b74d8e87557137272619fbd3520c47ffb0371b04a88e548edf1e85657e912b658ecacdfc63cf007c2e3636575514ce402ea217756bc74d2f1fd8f57824d7718b7b80a27aa5bd9dd9f6375a28e009794cc8ea671e01e3c76a190f86d0647b941e779da687d1a47cefa9c52e85e4d715107e1c4e42ad65a834b403245dee643c2f7b0c355bc2aabc3e5502f6c3395899df540ff194bb0f614441b1104776e549634f217912aaf56fe539cf52436be520e49c6413af7be5f2bc567ce0b0030888491aaadcf5048e4be5534318b63ca18d24ae22ea8b84c534bb16b5e89be9a97df5f26bf9c196cbda125380ace6283f5ca1138096adc4cbfabef87ec0a214e489765302f5ba8d22d175f4c44a60f071c4a4cd70c72df3d3b679b80e8adb29f506a352b95f4a047cade72c49c8cf57f7256fc4bd2a80a76ce25bb2f69c56ef539a6fbfa5771f5b524d7c9668b9bdcf36b42f4972262f9620fd16623dcd7a01163972135544e28c7d7b3ba773eb4fc19a9f356147e87ceef0fae8239cd9af066e786b7e36f207bac0c99ca40c3fa8d174506bde6daada87881bab469fba3f374ce4d28a88b93c40275b4313f58a042bbd3661d67562b97fa0de72ddd9d3489b02508854e4496caffce26df866a25ec7e13e2402bb690d973ba0b0ace987a979de8e3c2b897018962203ffbe2cec697960adac97aa6d5789062eee3d39cba18c42c1aa6af4f22b20f1dddc734f611324debe43d44d2c349e45f16323ef62133aec203d3f3315f98377002665086077e31e210b49ac5a263cc58e55dffe61dcb46fde9b3254309d314e9aa8cdb7a578c46a797521760d51a565ca0d45d6dced98e7d215d1373d135088f434f9732b3a606548b4bbd6356c3cd6aa3d7bd785e3983016a4d81ad153813d24010ba8672d8060f538b759ec30bd1cad9e82fdd7da0a8b381603e9325978c0a466d1aadb59a29d3c71b1bcd1a9b3b0bedb29c3364e0e8ef26b85484fa43f6a37a3215fe6e3c12207cc8bb9c94b96e92b260e33e91cf90f21fb31083632b316424b7b19f2f5213fbca06f53b5091c3173b5fc94c1045e2e73f57ead0fcdb4458a35fdce2c0575225955d2f5939981731d22b33cff03fe767a013da0b22388ccf168a134b3a353513b7c2a6c4b529761a2838fddf68dfbd0a1abe473eace950cac266671f7857955fb84121e0a505a51f024bc94007a21354cdb48e884a4960c1ad204b6b55eaf941fc57fbfed7aa8af71fa142e9fb1746deacdaf0ef81a13e51d98525673b977ad737ad4fba87b7c3af16be2ca81a6b0b73040259380e99939bbf1074c47adf2990204d39101cf9449a7c29f4e21c583b7c40eca3aa0d3cb7e9f5566cb2114a70630df071375d7966eb0b79483698a170b21881c775ba624b80b668f70d714a6d87a0a051f7679852db65fb78ea5df2b5787cec29244eaaf1528b08d65169d6b9699a5598e76d9eb0390863c9ff2b96ea6991c7448c66a651474086a04d9f28b7a72e8bfb35a7eb575c1b9771d21863eaee37c14975c8826ab99b6d4d8a11ea511b60740dddcc6bd8a49b252814ad7512375cd966aed0ba3478a7ae1b3b3ffb0ca843f05160354c7c2afd4ac2ffd164f9b80c60ef761af30a2f379582d4cc9f94bd2df255983378295d436d0eb2bf3739f38eaa1577f9061662825c51cc09bf576b71ba4975b9a4a66ea37205ecb930f83327238518757ab2dfc9d25b63be8abf4d67657d5d98f9bdd7799183f9679443873a1a99160e5f1f66d2257c90baa19505fa70cec3335943bcb3dcca3028f9b34aef83f5e4355fef3790df44b4763aa7ddb5cbba7c04b08edf545e38316663b838302d412cbafb0fa14d633a3dabf53ffbcebf4f08844d5d8c333bff147af758fe9ceab7cfaeccf1fbe95cb6d96465a0bf8d6aa80cb96992fc7545f0b1098c426ddb2fed5ac2975ec6045e44b4025bc9d6c8a4a861758b92f30e5fb57f00dbe89dc7a5c6e0cd4b18ad48df4b359c814d1d7b9aa1b46cec8731ee5b519059934190fa2f7251db9d013dc3318c20aefbb428c6945360d32a598265e95d4512377c38d985bcd1b1ed0dc6a4e82173218eed37e922bfbbf00d9d5eea08abc367903b57fda42539fe2b6abca9670b23e3ce2866d7a0f0bd916551b93a0ab8c96b9a3cbe87a17fe600d691a9bd3e2d587efaedc2820060461a8b2dc3a1a178f0f7056503991b02e3e3ca5d98063d203dcc507faf5d902789acd973464bcf39e57907253c5cf18325d33a1e4b7b52c566aad61b2dd133368b1d892e904d012653fcc3bb7f8217b6cb912271a904764cc049f66a7ca75076ad6e88d7069ed7afd58e74fab8afc8eb34ada73e99d1f6657064e1396d91d7aa190e06502631f738a095baa076ba395dc989f9134b9b2cf562b344e1af1de8990c117e5423a93fbf2c6e18220fd5af039b5c40f87c05b969290d582e602e98abd58ee80e152f74c3094378722c31453e60a9167b2a6c10beba701d623d3f83fa4e77de477ee8dff968977c281f7df560e7d0cabc8ce1df034667d86b8613e8dc26268726ed726618f84e95aa27717d0a15f1c1aa21e7f7a19622df62fe68c8f539ccd858288250c23695bac0d7c97e4f4994cf2aa929edc0b13315f047ad463709dd2679023d79b06ae0deded9888c0a2a56389086c44ced7872fef9d7b6032f79be609d5009f94c89f35a31497d781aceacf77de80108cd70b72cdd70a0a96e03cb48bf429d7b8605e0cbb5e4af9ca117c509bd366ed7c6fe1244571acb410bc6f8d176426abcc73de789285187e0522a17a80aee5a7ec1f0e3ab1f7fde3e66e014b42f28e6590a56fb4d6162aa9e395ec4cf2cf54b4fa779674edc7ea49d6e837a783ea8488ff03f5d603490d1558393bcdfdfca159ea1c207885f2fa5fda4f16587238b18c45e2d83dfaa028ca2d67f0b7acb1c9935fc2301f7f4ba9bcbc06b9af3a326f611e1e9e6d93c93c11758c8092681d371bd85f25f00dcc46c1015d15d0911fa9f4b4db06e604e070bc64fb6169caf17a4844e39d3ca4ccd0efa930f91617d14bc64b2c915ab7d158bfd1a3edf99562af377bb50f371b38c6feac8424e8a034d1471d92a188478a7e41ef947052a0f2c1685a5ec2b1a66c08e5eb9d163d8e7201400a98a0ffd38ba2c917f07bb114ee0250eb2524f09abbcde8d7855bfcf44cf6430236bcb265571f1455e03c29909c9faf61bbc181120b72574627262e70792cc8149c015b475bfb19a6865aa98127ad20ce29306d624b5f1f9bf26ded991f6659a40f2f5d17b46514efb11f90afa868b03bf498392453f2e2bce86727f5eb77124e05ca41704640d5429f1d4aa3ef19661a8ad650e34f177ece60c059080d4d8149c39ab686569ce2aaff7943d992730a830f7584578fdbd3ebc98401a137e6cdeefe363838b3a2a56ea2ca66bdff94a20ef39affb210329181b356beb70545520cd5d9b4b6fcbb2407f97252acea3ed87f4218266291b9238c2a4eeda12f7ae2ef634e104c769d998216ca7392c91a65f7559e0108a636fbdc33a8eab7b545e4ff3da4a6f32fdb804f5c4c431ba0ed780d5214bc68399232a83774582743c696b89cc0ebd110ee1cb72fbf85991e0c2bd055801ae4d9484ec3c94ccf8c3f85190e64826fe955b55e4b04efe3e4cf406c0bea84c992a18af6d3640e88ab34ec92e69f4b51b7a715d2212dd768a019690f655d68d19a0805a7a53939124ee6f5b5e48b7de6bc3f9098fdb5facab6f973b5a1477c397aefe691c2099efb4fbdfbe99509f43c2f3f8452dfe1f63df92e7a0dbeecad2f2ee6012ccf33a627b33c7e2b70aae4bb7bef47dc6935ca79b085fb3094773b2367eb3b0e8d95c5a8c987fbc72d15170a3f966cd6308729a4a6e032033c781680641a0accab1e2682f7e0c19bbc3831d1b97efa0fba00b8bb99ece27370f16a6b4bd3926d4f98017d8b7a23584b34964dcc7602b368d82d2555a346b96677addc77573bed662d7862b576e7334a6223b6f36e141d06bf658bb5e47f405760322b46d37f63955bea7555f6065ac17a580d2eda371309d48759aa462bfd4067aac6730ea6391a9bdb0aaf00d7a5311c5e52a32259dc49466b2e5945f063ba34388c1b15b6900e8c1d8d6c90bb3b4e64e596024919bd4b331eb98fdf08e22b31dd59f9a791ef24c276fe1561a07f89666bee2ebc9916e94475150f9bc3e240db6a803f1ae34ad8efa474b9ffc8178a68e0ab05192f2b7a73389d47088c4a450b85838997a1404afec5cfcf9ad604a49203d1581048d1d8fa1d55a74c0da87171a6e1ed2f0f9f112460a7117a6aa267f4015f3406ff9cc475197d879e0301de7b2abe50b0cbf6cf55d3548f6104705be4b5216784db2effcd539a7956498ba4a2bcde46eaf08cc8baa5316d993b54599bd3b79201880e233c1473b196abc3d15da0b9cf5e51a82052bbf09af7c0e02290ebc360b63e713102ace0446810e80936f234af849dd6d5783cceaffbcc7e493d695c943787b51ff33a7d9ccbd2008ba86035673c8e8670d543ff3b2ba40387de9d7bc0419ebfa88e34058fdf39cf650c8a291446da83f976ee9bfbbe48385601dd0bcb5f2d85834fabe0fc0957338b04576ddd9d1c2fb8e65a36968d7c0e80d797b096603ab4436c3e980553c94808b4dc74e2e66afe93786afa9f2b7e521fae7730084b6ab8777fbfe6b7ba34a0504806f44ce87d02b9abe856a24baac924e6b1e9b0e4f666dfa8aee90d07135503bf628a3a8578b702673f98a8a81cd7833ccff398d692363c82a845516efd2ae4b9a74b72dd0ee69950113c59e61b31203d618882d73a7eba59f932a7b2aadcc971b743a65daed8b0c8d61d146358220844e4eb32302fe5ea871259374c57bb0f62195918a954ea74519530a6e960f38cfc57fe7dc575dc33697fef0ae3173bf23f359dab53fb25741ad295f2ced8924fb1902894e94e195920ea5596e49e05b3783abc3b946faae16c8ba8b3f9b1aa7aeebade70a9fd0b52ead76f176f355d7de239f77e63b038526144e1b49f6299c89fa3e18db1b3b0fbcccc40d0028c043e9783524c4b85fc6338fae6973f5293a55d9c118339777be91bfaa3932eae0f8d7525109a19bab187ca814a484ffce65f413a8ea22ea627f6ecc03a149f9254d45f9d5956e2083493e6f8aa874aeff7c6429d1a18335175c294377694438d3037b96e4aab9454f7dffef76b5813d31a5f377dddcc01e1ec860b88b5b67be6abb889d82da155739dd4860339d33e7ed7d2aa309646bef3390f43f8e1b8ee75468f9c89c8b798febe1acdd493858c3121e9d1e0c4887d9eccc200b7b6492656fd6e22786b6adbf49a0e11456da10afb1d3e896d10ff2c637c0add4e44b9d0156f599b4495ead0c91574cba37c422e5b67d3857e9b035c232ffe1173a387544f1d3735193e00d52bcd0a08402691955c0ec3576209bdb30d4b3dab09d2cf1731e9f36820ffafe6f1e958239fe606a4adde758c5a2fabd59f2a143aa9ff8e5f9b4b8e06eff7750c4d3b2e9f785f332ee74db963c0aad8565fb8f43852a2bba1c04c401f6ab2cbd33beef28da3d399ee4ef17bc8ec2edfb5a6d666e70a6038b6c92c3501c901b1e56510d9059d2f94b7c3e098f9237f62839dcea1dfd1df8daa3919321cdf69efa80b3c2c1b106dbf0bd644323911b0ecfacada1cb3ced99dc03a21781699af040c49cd45dd6292840951e0171922275a557720613e73638a7f0867090293a965081048cb13d17b76f5136ce638c70646157ee7e447a33ca6019d0d778f773ac2d14df176dd1f497cb0e8b7a97e4f71da1874db722b19dec913732aebac806c797798cbe1feec74a86c2f538e8125407db0b2e6edfd2f1b02d05c43e508641338204e633f278a92d1e4baa33db9b892d0c42e509ffba299949cfeb39d47220e65c0b47505d2639c92fa91afaaa31a11bc5d31db74782e1595ceed4b699e259df053642cb27feae1a5a6883c54ca9b5efbe8b802f94a14343ef10a1040649e29b9b886475942b32c57f68e7434d8756641d3d5b101dd36e09483e93bfcb4f25fc7c515779489b8c0a000a9980f21a50dd14c9b2c5d8c396dbde7a69d6d20e95f114996bbab845ac2944f9a81fc5ac476cbccda30e85acfc5c04b84f566b47dac340206744e800a7f4f54c4a7ff3b2f9af933d57ce277b334ad22f686d5603632bb7e2f08a8258ae75eff36b0d97f3d3613ff39f8730936513b8cd07542da0763942b20e66cc32fbf91600cdf92e7fc5255a6adbc1a3bfb6707d19a4c9f61261ce12387ec59d44ea032d10a282f038aa4907eb6fe8f317a89ae74ea6c1489dc3afa1d7d770e1d57d3db0a154ae981469129646f31821e1dd4cdd32d9bbb53044a9a84a52a97cccd41ac38d56075472104c6041a0776fcb067037f3a9117a7ad4c0e08210a35540aab1fe1c3d27535b8d0453ecbc2467a67d9b70b1a8006b7968a776532b04e6f688082651ad296d0b302c7908fdefe09320a67be5775025929120a509dc7dec726c8a0e82aa664a40972ade66cef98ada9cd14c77a11ff055d136c7fe89d3df498973dd5464586c80eb0e53e0e48370eef6faef20dcc95ae3b449920005194bc928290b0bacfc49e12a676f1f23c4cc1bc33dc6cbd86358fa9a30aa89eb2483c3cd6b83cca79891d0160de91d4ce57d5904007ef581d439586c80bbf1ae709f57f55db82553ae0bc18e6d47c2fcae7afd536b90ff52675d0133120dd85612fd7555cbc558343ea08806b1bcf3b7787f2c613080c4e2841489bfb180deb1b60cbc8119699b036dc0f1194c1a4070e8b91d30b7c47ec61e0bb6da86eb29c3ec1ad236aa1232a6c9abd2a1cc47c82b75c9f54b2b8956e9ba3aa11f82c98c1465d2108d0ceb71f527047a1a8160b84aa5fd28eb6f4f477964c3361cc1a20f5f9d0b66b5a8ced1f1fa37f868c513e6da1abe9fff7fe66434276703f6cc0612af0bf5f60f248735019c3140f05281e269d7f002ea6928ec0e8c18eb725b5351134a9ae9f7a6b067b2308a48145576b9019586725c23a80cbc830800c9ad1b9232f6ca0358eaeb361ffff7846fd261068ad6baed747b3dc550b416835fe3ce67ddfc698ef683804c227cb2302f6d82e85c5512f88539404f613a0ff0d438354ef7e52a28a36b98f66a805dd74484d6f3f73f0dc28edf76f3a7430a6130315154cf2550d953ca377bd83fc5df978058357fc6b714a52bf116c630b485916cfecc2b9bbf1b5ab4ccf26da8d1fa2a115ce51e09ae1c07596027f971273d977d42a8bce76daa6649e31da51709cb1cc5bdf9f7a8d25904f8f384531cd881e57b93cfee106e48f53946be9de00526872b46a40ec0abe3a6992b4f52c12643dcf8c0f4d4185eddad2f9d257eed1f49c18340871ff32de637892c5fa825e1227edad6f21dc5f808e08ffb44ac4e275f100925456630effb7e03f091cc7860c9698abd34b56171541edb31bb515a34b84a8d89ea0fbfbe7b2b8c88ddc963f8fa39da45aa99f3aa5693210f20cd848808dcbced3f730f0336b338456233bf5ec5b21df1a2a09f2379a8595ea637fcba01c7f6b765e330554e1966ab7219d004e855df754f5f0f1df247b8e9be7a1aae56a3fea12b3c4f376625b3c1bd50acf0da503314c38bf2c6cef380265fb62879c94d97a552f24fa431ca2a44f6c9c9c4baa2ad1169d3e95c75b29ef3c2922609cd4e08d7f38c3f996f79ce3353a6b79291b3490983a6b54126935b683f1e85e384909f2611f7f127149251b84c741700dc09372ee75bd35cabe2f4d78613d190d84b950159e0136fde672b03a1a3819679202df50d2e72a780e18bd1941bdd9d5284e0f3ad5ed2bfb8fabe287ca2f17267ca7a91cd73f15d2f7b4b9e40a0642c9c03adf2b2d276a2fd0e17991e6f9de0f7258e2a47c69f2544cb02aa8248f320f5967fc5a27cfdd3e1ab7887f2cac10640e1c872d29d01118aa37ceff8b9c7ce4522338891f168bf5c0b5322e003c57d679d9b5f4328f69a05c8fae75e1c7d893a96d759285341473fda21d0e33df6e7e01fd7f8b8fee4b6a3f168bb18e97175f19dbcf56a70bd985612d8e545b6c006c80202e53a1a2742bec72b68c03b36e7a08e9fff36973cb7888cbddc139d3bcff7f9bfb24fabf5de213c293790a9f878e43df13a17dc5828188f51b750bb7373cb932d85da51f8b23b37258e1c2fdb2a823986fcd4149e0d90b8a81b8fd59029b28d27e207e0cf2d4597c1ad425ccc655809a5e6bb1d6345509ed6c8af83685cd76b20de12c3a8dd23f6fb30263553f86d481f91f3c1d43b596ddf7eda69f5897a8ce6b2d080a9380bbb5af7eb3285fcb23d28eafb7648fb2065da3e52c8dbd0fd2bb195e87c2ebeaf72183e521347c2d69ce72ca4bef390868bec18586fd0d77282a92d7ac9f75b3bf58944485a236632f7d22b93984eae51fcff4f452829ea085fcc3fed18399f67240a8028fc578f8fc99b3e896fb845dd593a493e9706a174d89c0136e51df4e04b5a04c4cfca4130a830264fc3f3b7519bb81ccebccccf3a50a666006b264db55c68efd0a03e3c821b4e0aceed5eaea23a70d14956dabb9596674f2caba60f70e50d2733d8c1da23c7c6508eb6e16058377a07abb5056220bc98597969e33ce10d12cbc07cbea15f0195849a22629163755f829fad8227edc54c0a10039680330e490803ad441b8195afdd1ee7f9915f0b6daf48e8d88f46697ea697857a209d8b2c36a8dd89537b9454f23cef2d20258c08cb519dcf557d163a938db68e7058b6c6251380e69da1e751640fa32c12351aa0fd67b7662a65a3ce96f18b417bd6730a57bedc6c020fb2d9f39b2476afca83e7ac3eef594bdbc842b12f2838effa939b2952371af51144b1fd836bb63594501869dccd34f516f26f5251a3b05172eab489da8b4102b69276cc72c4a35502bdf92ad5d857616cf4dc9f919af32c2885aa928b9a8f432316f80101611772def843dd4be19d7bdaf083599b2a9a792bda771f4f4f646d9504fd3c29f8564c616e8820c48bb41898fa309168dde2ca94a0d45c84f950e632cd658c04fde9fb4b8c5ad242713137a21ece27c0fe739c197268e6a6528a158ae88a7eafc34f8ae4fe0f7cd04ec2e33dfe7b042a965e0f05fe2e3ac30f9f33698912aff56edf5756ec081b01b2816cdf34169b6064e69486e87fa0cdc7ecf330a65334fc7d7f7bbf08164466d956b0cc62cfe798fb4d6bd3bc4a429d5eb1d196177aa2ae5f31ae21dd3e7cc524da63a556858c0ccf6b3e0616be091f585b318f4f0c2a8622d0d9684dda67c3d28808495e8efd62b1f0fdae157e961c6c5bee6ee63b90cc61f612088800418609c3b197b7409d7bcd8a53369b4a9bc962548ec50fd3b752c88c6ae41785545617b49da4052fe9ed173899f98271e065ea8cf4d70400128a08c99e153a78efd9a24075b531e71ad876854bb22c3108229a55704627927535b43cdf4a438f046fe1785811347acc2004a91e9c60aa87ed660228f145f29e65e0f774d674a6869262aa14ade1588c4be8f198bf8e7df619b0e4331a5931a2e8b1246bde1938685266c8d6c4913cd5c6d5a278587c789176dde4bcd4390246123c8eaf7230b954f15d7b3ad89df310271a52276afe7a875509432a7658e6b96206d93e5b28056df1f10c9c4a35f40c1131f30290e7798e61d197ca0ea29d9edfa23bd269cc68b022a0236e2bf6d49f288641edd012ba7bb40c130ed612154f0ff101d43a924d06f9199f55b9dbf5a834d251957537db3d7d817be1de33bff93a56d72c9b3fb6f8a9050ec425bcd7666dc4677e954295c61d47743991acee7e796bd682080daa5fa12e314287337674ea0c0394b1e3660d8a3aec378fc8e2e1a159bab309bc2e3dc4a32744c9bb7672d2cc8f193059797683c8614abd3f9f8be9d255fba4cb3fbcd5023a3ab1118590b6b93c65d2440fee25409e92a246622edac62103c7bfaaf22feff70e956d64528b7c4f91e84197524a77807e68bd0a3933a96b488f8f02e5220b6577dc672b9a8b7d6ddf90ff6b585bef46708062b14438a4f3e950239e533a6313fa39afd4952307c996e359629da872755c30fc16b39a6a94cfd862ca86fe13b979b0ac6735ece74be12072ac15941fe6b0337f0ccc2e57c38a764390141847b855015c8b02bfd97fe79ba4a90a58d5ee2a17a734772ce4131d9323b4d5ce1d8df2c339489d7f862320c62357ff37c1b09f627f6f116599b51905402812a45651aad8e420bfc1ca5c31d9046a48bde485f2d326f1c775904afa48790dbc0ad56e1062fdaf4c88dd82645ff648baa453deadbcc18be9f94a8539650df746d13a85db47d45c8268a7c1ccc7c3a4245fa8eebdfdc0b172dc360201002afa077fa07fa5543960a82f81d81417f8d7c3070c3bb5dab0128a47838f2c43ac7ea4bc75b73afbab9fa42ee0deb10b93aaea0059bc38468e3e2b511833e5ebb0062645048c6106044866ed52d331a21fd90e9d2918d1e2b9e60110c6d1dfed8cab1a7e913ac3c4bd681493d398a0fc64108f7971c30184b5b202e69750f1d53140c0809a974341550e5faef1392a266c18333a19e48eafc6252138c7bf610aa440efee6ada47e619a1be9b9e89dbef8df1aab0446daa2e7f3ac9ceeed93886be8b63f56295bd43bde11be741238fb5754ef749df2962088c089adcb47612f1cf5a868e4c0a0c2437ecb5759da41989899951f96261d31343dc131de60243f48384e06faa87baac8da66ac6323a104f3c1b9013f79d9712befaf860c639739de3ab7f6f1376dd99cb7f6c8aa42fb0d0f2dc586b708bf2b8af0e681350663125e913f7c8f2cf03d566acc35f09bf46680fbb699bdbc06d2af725cebcca87caba9553d5fde4248360dde795a78dad54241cce04425902deab8aa6de34fea7ec003a8b9e94a1c82eb83ff19cf2ae9a5ddf59d6f5e0e96ade83eb8a07072fe4b497b26c09843f98a89d10719a4899c886240f395a527055b3b6a8f208bba4cf495dd8b04c5c4f684331929d638400cb8b4927368e7a2365753cdd5f206545a022cb412ae27176214efaaecc0578b9dc9b87610aea8dbe799a94c6ea88bf5ab3945534addd3f63d76856a0360a1eb505abb4be88ef6b571e8a692453f7b3c6e6051d5cc27bf7715af743dd6b6067ec7fb3e13219833198286ea8315faa6f367a606d8148972617d044f9af75f9f62e7db95fcf329273a8d3a328047cf1d25686cf6a696f5127ec71cdc7d5e1ecb212d8fa94f38a7c84d5cb5730f0a4ff8f70b4f41996092cff2ddd991ab30e22ba1ac0144ca776facd476ca07cb50d9289a234e9faf84926e322d271368682f3a0d403c7c80a04c48788742fd29c567806640b7f1bd8d1dd961d92edd846aead2d9f4ad6c6363b35ed5e7f872c32194fa6ff03d3696f2bf78f055e4019146b144ed1ad63be7c3022dec556cd8d2f3a0bbfe725ae304de8affffa097e7f8e2a91bbe59eb6cdeaa4246cff640dcf9824768418535effe74534331242e689734fc393ff38a8f3e88b450112ef26eb58df6d45d4c677aed1e7cfe1e5a681bb5f72aeace6b762cc72408cb004ec049c1cf4a31701068a1c7058cd2f0e98df911627bd791a85817cc503906256240fe4f830ebcc290ce216dfe5b6331d3cc612098e39b821e314b37858a40d1268f31f23bdb75ea64a5bfe3cbbd9284ea4caa57bc27ed3501e2419aa407e5f7b7a5dc7c64566f4abeaffb54d094357eaab85314c953a520eede2f4bbf2784b3cee19c5420e2d5a6a8b86c313605f16731ab67db8c8310290332d36e5db455da9a7c72de8a95c3794eb85614ba70eb39cddf29dc8971f253c73b14c073bda91d88f8acf246aaedbec7e3baa71bd7dbb6a9e2e610340374fd4f9a0e9ca8f3326126e07545f7ec6e32826f96837657dddbed096f2e9b488fe98165f70d23d3e8602ffdad9480a7dcec031ef6b62061c3121f284b609fa8c8f06c7a4a53324338e65db9af2a4607d2e2977ab4ef41ff93304539c9c0aa831dbbac328a5484a08366000000000037b10700000000002a7315c8ddfc25dc2266a6b221cb8f9fdf641970ab1f65a2754df4e14c432b9c446c604131913bed45976c4a8ea27df843a72d622f80ef15da622f0d56ec1ffe3021190177b0405c4fa1a0311a493c4dd095f24d386be6a03f5838a6b1008665f4000000f5c98fc152bf40e1ce216c8839b8ddd42ea5b355f0b5c700fc9b2cb7c802e1c8336488033fe5f3ef4ccc12af07b9370b92e553e35ecb4a337a1b1c0e4afe1e0edb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71a533e05d648cb9635382f38b778dc3c76b18e4eab2d017fdb24c1c0e32d2991707a45ce3f26e443dafa697a76bf24122c9b19b57eebe798668dbd1a0da7c900795222290dd7278aa3ddd389cc1e1d165cc4bafe5c658153cdef5850f97c4d4fb6706b82310414a00fe7dc34b3043e0c7a8a24e810e3cec9de3e1dd7ce06bc191efd2273a357da4436fc4bf82069f6cb87d3d605aeda1336ac513f358b4296b8da51232896ad271063d93404eaa6b80960fa2d4b41d74a544eef94731f0b83ba2701e33b49e3191098c3369bc4658d82f536cb9d38285c51c7844292ced06dd2e8148f0bcd585dc1104e64aa930045e83d828ab436a0e587637e5ef83044d1c2918da284d7072847132885d443640979907485847368e32501345b0dc24c904c06732cc17d944b26181b0c05c6461bccbe4f7b2180a282b6871aa6472fa36cdc49ec50cf69d3eee4b26fe02bf48f39efb549d3963d19e4112c7250c580543516204ff1b0675680cfea02ac09f0b23d6e28c14b08421b3a10a9788442457d732b120c5c466c54ad938dedd3cd3701d9b32fa15dc8db3d6b1ea5d8495732477a72b34efcaf8c349aa40d8e1f21f226ca11182cff00921930b010000000080c3c901000000009104e90000000000d75b9464000000003802000076ab1c8604000000000000000000000000000000000000000000000000000000844f7a7e7585362114fc88a70654146215ab4eaad65ad54b64975840b78d365e8b75e78616a92294e747589c3ee4a845c2962e1cf6cd7fe8bfbd09439e5e6d5e4d4efdf181473803e734d97891388e1b50628658ac818599cf2cb069c8e6b38b6265617665726275696c642e6f7267"; + +dyn_async! { + pub async fn run_rpc_compat_test_suite<'a> (test: &'a mut Test, _client: Option) { + // Get all available portal clients + let clients = test.sim.client_types().await; + // todo: remove this once we implement role in hivesim-rs + let clients: Vec = clients.into_iter().filter(|client| client.name != *TRIN_BRIDGE_CLIENT_TYPE).collect(); + + // Test single type of client + for client in &clients { + test.run( + NClientTestSpec { + name: "discv5_nodeInfo".to_string(), + description: "".to_string(), + always_run: false, + run: test_node_info, + environments: Some(vec![Some(HashMap::from([(HIVE_PORTAL_NETWORKS_SELECTED.to_string(), BEACON_STRING.to_string())]))]), + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_beaconLocalContent Expect ContentAbsent".to_string(), + description: "".to_string(), + always_run: false, + run: test_local_content_expect_content_absent, + environments: Some(vec![Some(HashMap::from([(HIVE_PORTAL_NETWORKS_SELECTED.to_string(), BEACON_STRING.to_string())]))]), + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_beaconStore".to_string(), + description: "".to_string(), + always_run: false, + run: test_store, + environments: Some(vec![Some(HashMap::from([(HIVE_PORTAL_NETWORKS_SELECTED.to_string(), BEACON_STRING.to_string())]))]), + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_beaconLocalContent Expect ContentPresent".to_string(), + description: "".to_string(), + always_run: false, + run: test_local_content_expect_content_present, + environments: Some(vec![Some(HashMap::from([(HIVE_PORTAL_NETWORKS_SELECTED.to_string(), BEACON_STRING.to_string())]))]), + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_beaconAddEnr Expect true".to_string(), + description: "".to_string(), + always_run: false, + run: test_add_enr_expect_true, + environments: Some(vec![Some(HashMap::from([(HIVE_PORTAL_NETWORKS_SELECTED.to_string(), BEACON_STRING.to_string())]))]), + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_beaconGetEnr None Found".to_string(), + description: "".to_string(), + always_run: false, + run: test_get_enr_non_present, + environments: Some(vec![Some(HashMap::from([(HIVE_PORTAL_NETWORKS_SELECTED.to_string(), BEACON_STRING.to_string())]))]), + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_beaconGetEnr ENR Found".to_string(), + description: "".to_string(), + always_run: false, + run: test_get_enr_enr_present, + environments: Some(vec![Some(HashMap::from([(HIVE_PORTAL_NETWORKS_SELECTED.to_string(), BEACON_STRING.to_string())]))]), + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_beaconGetEnr Local Enr".to_string(), + description: "".to_string(), + always_run: false, + run: test_get_enr_local_enr, + environments: Some(vec![Some(HashMap::from([(HIVE_PORTAL_NETWORKS_SELECTED.to_string(), BEACON_STRING.to_string())]))]), + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_beaconDeleteEnr None Found".to_string(), + description: "".to_string(), + always_run: false, + run: test_delete_enr_non_present, + environments: Some(vec![Some(HashMap::from([(HIVE_PORTAL_NETWORKS_SELECTED.to_string(), BEACON_STRING.to_string())]))]), + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_beaconDeleteEnr ENR Found".to_string(), + description: "".to_string(), + always_run: false, + run: test_delete_enr_enr_present, + environments: Some(vec![Some(HashMap::from([(HIVE_PORTAL_NETWORKS_SELECTED.to_string(), BEACON_STRING.to_string())]))]), + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_beaconLookupEnr None Found".to_string(), + description: "".to_string(), + always_run: false, + run: test_lookup_enr_non_present, + environments: Some(vec![Some(HashMap::from([(HIVE_PORTAL_NETWORKS_SELECTED.to_string(), BEACON_STRING.to_string())]))]), + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_beaconLookupEnr ENR Found".to_string(), + description: "".to_string(), + always_run: false, + run: test_lookup_enr_enr_present, + environments: Some(vec![Some(HashMap::from([(HIVE_PORTAL_NETWORKS_SELECTED.to_string(), BEACON_STRING.to_string())]))]), + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_beaconLookupEnr Local Enr".to_string(), + description: "".to_string(), + always_run: false, + run: test_lookup_enr_local_enr, + environments: Some(vec![Some(HashMap::from([(HIVE_PORTAL_NETWORKS_SELECTED.to_string(), BEACON_STRING.to_string())]))]), + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_beaconRecursiveFindContent Content Absent".to_string(), + description: "".to_string(), + always_run: false, + run: test_recursive_find_content_content_absent, + environments: Some(vec![Some(HashMap::from([(HIVE_PORTAL_NETWORKS_SELECTED.to_string(), BEACON_STRING.to_string())]))]), + test_data: None, + clients: vec![client.clone()], + } + ).await; + } + } +} + +dyn_async! { + async fn test_node_info<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + + let response = Discv5ApiClient::node_info(&client.rpc).await; + + if let Err(err) = response { + panic!("Expected response not received: {err}"); + } + } +} + +dyn_async! { + async fn test_local_content_expect_content_absent<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let content_key = + serde_json::from_value(json!(CONTENT_KEY)); + + match content_key { + Ok(content_key) => { + let response = BeaconNetworkApiClient::local_content(&client.rpc, content_key).await; + + match response { + Ok(response) => { + match response { + ContentAbsent => (), + _ => panic!("Expected ContentAbsent, got ContentPresent") + } + }, + Err(err) => { + panic!("{}", &err.to_string()); + }, + } + } + Err(err) => { + panic!("{}", &err.to_string()); + } + } + } +} + +dyn_async! { + async fn test_store<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let content_key = + serde_json::from_value(json!(CONTENT_KEY)); + + let content_value = + serde_json::from_value(json!(CONTENT_VALUE)); + + match content_key { + Ok(content_key) => { + match content_value { + Ok(content_value) => { + let response = BeaconNetworkApiClient::store(&client.rpc, content_key, content_value).await; + + if let Err(err) = response { + panic!("{}", &err.to_string()); + } + } + Err(err) => { + panic!("{}", &err.to_string()); + } + } + } + Err(err) => { + panic!("{}", &err.to_string()); + } + } + } +} + +dyn_async! { + async fn test_local_content_expect_content_present<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let content_key: Result = + serde_json::from_value(json!(CONTENT_KEY)); + + let content_value = + serde_json::from_value(json!(CONTENT_VALUE)); + + + match content_key { + Ok(content_key) => { + // seed content_key/content_value onto the local node to test local_content expect content present + match content_value { + Ok(content_value) => { + let response = BeaconNetworkApiClient::store(&client.rpc, content_key.clone(), content_value).await; + + if let Err(err) = response { + panic!("{}", &err.to_string()); + } + } + Err(err) => { + panic!("{}", &err.to_string()); + } + } + + // Here we are calling local_content RPC to test if the content is present + let response = BeaconNetworkApiClient::local_content(&client.rpc, content_key).await; + + match response { + Ok(response) => { + match response { + ContentPresent(_) => (), + _ => panic!("Expected ContentPresent, got ContentAbsent") + } + }, + Err(err) => { + panic!("{}", &err.to_string()); + }, + } + } + Err(err) => { + panic!("{}", &err.to_string()); + } + } + } +} + +dyn_async! { + async fn test_add_enr_expect_true<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let (_, enr) = generate_random_remote_enr(); + match BeaconNetworkApiClient::add_enr(&client.rpc, enr).await { + Ok(response) => match response { + true => (), + false => panic!("AddEnr expected to get true and instead got false") + }, + Err(err) => panic!("{}", &err.to_string()), + } + } +} + +dyn_async! { + async fn test_get_enr_non_present<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let (_, enr) = generate_random_remote_enr(); + + if (BeaconNetworkApiClient::get_enr(&client.rpc, enr.node_id()).await).is_ok() { + panic!("GetEnr in this case is not supposed to return a value") + } + } +} + +dyn_async! { + async fn test_get_enr_local_enr<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + // get our local enr from NodeInfo + let target_enr = match Discv5ApiClient::node_info(&client.rpc).await { + Ok(node_info) => node_info.enr, + Err(err) => { + panic!("Error getting node info: {err:?}"); + } + }; + + // check if we can fetch data from routing table + match BeaconNetworkApiClient::get_enr(&client.rpc, target_enr.node_id()).await { + Ok(response) => { + if response != target_enr { + panic!("Response from GetEnr didn't return expected Enr") + } + }, + Err(err) => panic!("{}", &err.to_string()), + } + } +} + +dyn_async! { + async fn test_get_enr_enr_present<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let (_, enr) = generate_random_remote_enr(); + + // seed enr into routing table + match BeaconNetworkApiClient::add_enr(&client.rpc, enr.clone()).await { + Ok(response) => match response { + true => (), + false => panic!("AddEnr expected to get true and instead got false") + }, + Err(err) => panic!("{}", &err.to_string()), + } + + // check if we can fetch data from routing table + match BeaconNetworkApiClient::get_enr(&client.rpc, enr.node_id()).await { + Ok(response) => { + if response != enr { + panic!("Response from GetEnr didn't return expected Enr") + } + }, + Err(err) => panic!("{}", &err.to_string()), + } + } +} + +dyn_async! { + async fn test_delete_enr_non_present<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let (_, enr) = generate_random_remote_enr(); + match BeaconNetworkApiClient::delete_enr(&client.rpc, enr.node_id()).await { + Ok(response) => match response { + true => panic!("DeleteEnr expected to get false and instead got true"), + false => () + }, + Err(err) => panic!("{}", &err.to_string()), + }; + } +} + +dyn_async! { + async fn test_delete_enr_enr_present<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let (_, enr) = generate_random_remote_enr(); + + // seed enr into routing table + match BeaconNetworkApiClient::add_enr(&client.rpc, enr.clone()).await { + Ok(response) => match response { + true => (), + false => panic!("AddEnr expected to get true and instead got false") + }, + Err(err) => panic!("{}", &err.to_string()), + } + + // check if data was seeded into the table + match BeaconNetworkApiClient::get_enr(&client.rpc, enr.node_id()).await { + Ok(response) => { + if response != enr { + panic!("Response from GetEnr didn't return expected Enr") + } + }, + Err(err) => panic!("{}", &err.to_string()), + } + + // delete the data from routing table + match BeaconNetworkApiClient::delete_enr(&client.rpc, enr.node_id()).await { + Ok(response) => match response { + true => (), + false => panic!("DeleteEnr expected to get true and instead got false") + }, + Err(err) => panic!("{}", &err.to_string()), + }; + + // check if the enr was actually deleted out of the table or not + if (BeaconNetworkApiClient::get_enr(&client.rpc, enr.node_id()).await).is_ok() { + panic!("GetEnr in this case is not supposed to return a value") + } + } +} + +dyn_async! { + async fn test_lookup_enr_non_present<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let (_, enr) = generate_random_remote_enr(); + + if (BeaconNetworkApiClient::lookup_enr(&client.rpc, enr.node_id()).await).is_ok() { + panic!("LookupEnr in this case is not supposed to return a value") + } + } +} + +dyn_async! { + async fn test_lookup_enr_enr_present<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let (_, enr) = generate_random_remote_enr(); + + // seed enr into routing table + match BeaconNetworkApiClient::add_enr(&client.rpc, enr.clone()).await { + Ok(response) => match response { + true => (), + false => panic!("AddEnr expected to get true and instead got false") + }, + Err(err) => panic!("{}", &err.to_string()), + } + + // check if we can fetch data from routing table + match BeaconNetworkApiClient::lookup_enr(&client.rpc, enr.node_id()).await { + Ok(response) => { + if response != enr { + panic!("Response from LookupEnr didn't return expected Enr") + } + }, + Err(err) => panic!("{}", &err.to_string()), + } + } +} + +dyn_async! { + async fn test_lookup_enr_local_enr<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + // get our local enr from NodeInfo + let target_enr = match Discv5ApiClient::node_info(&client.rpc).await { + Ok(node_info) => node_info.enr, + Err(err) => { + panic!("Error getting node info: {err:?}"); + } + }; + + // check if we can fetch data from routing table + match BeaconNetworkApiClient::lookup_enr(&client.rpc, target_enr.node_id()).await { + Ok(response) => { + if response != target_enr { + panic!("Response from LookupEnr didn't return expected Enr") + } + }, + Err(err) => panic!("{}", &err.to_string()), + } + } +} + +dyn_async! { + // test that a node will return a AbsentContent via RecursiveFindContent when the data doesn't exist + async fn test_recursive_find_content_content_absent<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let header_with_proof_key: BeaconContentKey = serde_json::from_value(json!(CONTENT_KEY)).unwrap(); + + match BeaconNetworkApiClient::recursive_find_content(&client.rpc, header_with_proof_key).await { + Ok(result) => { + match result { + ContentInfo::Content{ content: ethportal_api::PossibleBeaconContentValue::ContentAbsent, utp_transfer } => { + if utp_transfer { + panic!("Error: Unexpected RecursiveFindContent response: utp_transfer was supposed to be false"); + } + }, + other => { + panic!("Error: Unexpected RecursiveFindContent response: {other:?}"); + } + } + }, + Err(err) => { + panic!("Error: Unable to get response from RecursiveFindContent request: {err:?}"); + } + } + } +} diff --git a/simulators/portal/history/Cargo.toml b/simulators/portal/history/Cargo.toml new file mode 100755 index 0000000000..e765ef9721 --- /dev/null +++ b/simulators/portal/history/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "history" +version = "0.1.0" +authors = ["Ognyan Genev ", "Kolby ML (Moroz Liebl) "] +edition = "2021" + +[dependencies] +ethportal-api = { git = "https://github.com/ethereum/trin", rev = "2a32224e3c2b0b80bc37c1b692c33016371f197a" } +portal-spec-test-utils-rs = { git = "https://github.com/ethereum/portal-spec-tests", rev = "d1e996d0d4dc2136b3cd38d9e25cdc3a6b74dcd9" } +hivesim = { git = "https://github.com/ethereum/portal-hive", rev = "8ff1e3d3c941dd00d56dacd777a5dfb71edf402f" } +futures = "0.3.25" +serde_json = "1.0.87" +tracing = "0.1.37" +tracing-subscriber = "0.3.16" +itertools = "0.10.5" +serde_yaml = "0.9" +tokio = { version = "1", features = ["full"] } diff --git a/simulators/portal/history/Dockerfile b/simulators/portal/history/Dockerfile new file mode 100644 index 0000000000..de403323fa --- /dev/null +++ b/simulators/portal/history/Dockerfile @@ -0,0 +1,25 @@ +FROM rust:1.75.0 AS builder + +# create a new empty shell project +RUN USER=root cargo new --bin history +WORKDIR /history + +# copy over manifests and source to build image +COPY Cargo.toml ./Cargo.toml +COPY src ./src + +# build for release +RUN cargo build --release + +# final base +FROM ubuntu:22.04 + +RUN apt update && apt install wget -y + +# copy build artifacts from build stage +COPY --from=builder /history/target/release/history . +ADD https://raw.githubusercontent.com/ethereum/portal-spec-tests/master/tests/mainnet/history/hive/test_data_collection_of_forks_blocks.yaml ./test-data/test_data_collection_of_forks_blocks.yaml + +ENV RUST_LOG=debug + +ENTRYPOINT ["./history"] diff --git a/simulators/portal/history/src/main.rs b/simulators/portal/history/src/main.rs new file mode 100644 index 0000000000..39216444fa --- /dev/null +++ b/simulators/portal/history/src/main.rs @@ -0,0 +1,92 @@ +pub mod suites; + +use hivesim::{Simulation, Suite, TestSpec}; +use suites::interop::test_portal_interop; +use suites::mesh::test_portal_scenarios; +use suites::rpc_compat::run_rpc_compat_test_suite; +use suites::trin_bridge::test_portal_bridge; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + let mut rpc_compat = Suite { + name: "history-rpc-compat".to_string(), + description: "The RPC-compatibility test suite runs a set of RPC related tests against a + running node. It tests client implementations of the JSON-RPC API for + conformance with the portal network API specification." + .to_string(), + tests: vec![], + }; + + rpc_compat.add(TestSpec { + name: "client launch".to_string(), + description: "This test launches the client and collects its logs.".to_string(), + always_run: false, + run: run_rpc_compat_test_suite, + client: None, + }); + + let mut interop = Suite { + name: "history-interop".to_string(), + description: + "The interop test suite runs a set of scenarios to test interoperability between + portal network clients" + .to_string(), + tests: vec![], + }; + + interop.add(TestSpec { + name: "client launch".to_string(), + description: "This test launches the client and collects its logs.".to_string(), + always_run: false, + run: test_portal_interop, + client: None, + }); + + let mut mesh = Suite { + name: "history-mesh".to_string(), + description: "The portal mesh test suite runs a set of scenarios to test 3 clients" + .to_string(), + tests: vec![], + }; + + mesh.add(TestSpec { + name: "client launch".to_string(), + description: "This test launches the client and collects its logs.".to_string(), + always_run: false, + run: test_portal_scenarios, + client: None, + }); + + let mut trin_bridge = Suite { + name: "history-trin-bridge".to_string(), + description: "The portal bridge test suite".to_string(), + tests: vec![], + }; + + trin_bridge.add(TestSpec { + name: "client launch".to_string(), + description: "This test launches the client and collects its logs.".to_string(), + always_run: false, + run: test_portal_bridge, + client: None, + }); + + let sim = Simulation::new(); + run_suite(sim, vec![rpc_compat, interop, mesh, trin_bridge]).await; +} + +async fn run_suite(host: Simulation, suites: Vec) { + for suite in suites { + let name = suite.clone().name; + let description = suite.clone().description; + + let suite_id = host.start_suite(name, description, "".to_string()).await; + + for test in &suite.tests { + test.run_test(host.clone(), suite_id, suite.clone()).await; + } + + host.end_suite(suite_id).await; + } +} diff --git a/simulators/portal/history/src/suites/constants.rs b/simulators/portal/history/src/suites/constants.rs new file mode 100644 index 0000000000..b7f0a53430 --- /dev/null +++ b/simulators/portal/history/src/suites/constants.rs @@ -0,0 +1,6 @@ +pub const TEST_DATA_FILE_PATH: &str = "./test-data/test_data_collection_of_forks_blocks.yaml"; + +// trin-bridge constants +pub const TRIN_BRIDGE_CLIENT_TYPE: &str = "trin-bridge"; +pub const BOOTNODES_ENVIRONMENT_VARIABLE: &str = "HIVE_BOOTNODES"; +pub const HIVE_CHECK_LIVE_PORT: &str = "HIVE_CHECK_LIVE_PORT"; diff --git a/simulators/portal/history/src/suites/interop.rs b/simulators/portal/history/src/suites/interop.rs new file mode 100644 index 0000000000..df6e3ceec7 --- /dev/null +++ b/simulators/portal/history/src/suites/interop.rs @@ -0,0 +1,662 @@ +use crate::suites::constants::{TEST_DATA_FILE_PATH, TRIN_BRIDGE_CLIENT_TYPE}; +use ethportal_api::types::portal::ContentInfo; +use ethportal_api::utils::bytes::hex_encode; +use ethportal_api::{ + ContentValue, Discv5ApiClient, HistoryContentKey, HistoryContentValue, HistoryNetworkApiClient, + OverlayContentKey, PossibleHistoryContentValue, +}; +use hivesim::types::ClientDefinition; +use hivesim::{dyn_async, Client, NClientTestSpec, Test}; +use itertools::Itertools; +use portal_spec_test_utils_rs::get_flair; +use serde_json::json; +use serde_yaml::Value; +use tokio::time::Duration; + +// This is taken from Trin. It should be fairly standard +const MAX_PORTAL_CONTENT_PAYLOAD_SIZE: usize = 1165; + +// Header with proof for block number 14764013 +const HEADER_WITH_PROOF_KEY: &str = + "0x00720704f3aa11c53cf344ea069db95cecb81ad7453c8f276b2a1062979611f09c"; + +fn content_pair_to_string_pair( + content_pair: (HistoryContentKey, HistoryContentValue), +) -> (String, String) { + let (content_key, content_value) = content_pair; + (content_key.to_hex(), hex_encode(content_value.encode())) +} + +/// Processed content data for history tests +struct ProcessedContent { + content_type: String, + block_number: u64, + test_data: Vec<(String, String)>, +} + +fn process_content( + content: Vec<(HistoryContentKey, HistoryContentValue)>, +) -> Vec { + let mut last_header = content.first().unwrap().clone(); + + let mut result: Vec = vec![]; + for history_content in content.into_iter() { + if let HistoryContentKey::BlockHeaderWithProof(_) = &history_content.0 { + last_header = history_content.clone(); + } + let (content_type, block_number, test_data) = + if let HistoryContentValue::BlockHeaderWithProof(header_with_proof) = &last_header.1 { + match &history_content.0 { + HistoryContentKey::BlockHeaderWithProof(_) => ( + "Block Header".to_string(), + header_with_proof.header.number, + vec![content_pair_to_string_pair(last_header.clone())], + ), + HistoryContentKey::BlockBody(_) => ( + "Block Body".to_string(), + header_with_proof.header.number, + vec![ + content_pair_to_string_pair(history_content), + content_pair_to_string_pair(last_header.clone()), + ], + ), + HistoryContentKey::BlockReceipts(_) => ( + "Block Receipt".to_string(), + header_with_proof.header.number, + vec![ + content_pair_to_string_pair(history_content), + content_pair_to_string_pair(last_header.clone()), + ], + ), + HistoryContentKey::EpochAccumulator(_) => ( + "Epoch Accumulator".to_string(), + header_with_proof.header.number, + vec![], + ), + } + } else { + unreachable!("History test dated is formatted incorrectly") + }; + result.push(ProcessedContent { + content_type, + block_number, + test_data, + }) + } + result +} + +dyn_async! { + pub async fn test_portal_interop<'a> (test: &'a mut Test, _client: Option) { + // Get all available portal clients + let clients = test.sim.client_types().await; + // todo: remove this once we implement role in hivesim-rs + let clients: Vec = clients.into_iter().filter(|client| client.name != *TRIN_BRIDGE_CLIENT_TYPE).collect(); + + let values = std::fs::read_to_string(TEST_DATA_FILE_PATH) + .expect("cannot find test asset"); + let values: Value = serde_yaml::from_str(&values).unwrap(); + let content: Vec<(HistoryContentKey, HistoryContentValue)> = values.as_sequence().unwrap().iter().map(|value| { + let content_key: HistoryContentKey = + serde_yaml::from_value(value.get("content_key").unwrap().clone()).unwrap(); + let content_value: HistoryContentValue = + serde_yaml::from_value(value.get("content_value").unwrap().clone()).unwrap(); + (content_key, content_value) + }).collect(); + + // Iterate over all possible pairings of clients and run the tests (including self-pairings) + for (client_a, client_b) in clients.iter().cartesian_product(clients.iter()) { + for ProcessedContent { content_type, block_number, test_data } in process_content(content.clone()) { + test.run( + NClientTestSpec { + name: format!("OFFER {}: block number {}{} {} --> {}", content_type, block_number, get_flair(block_number), client_a.name, client_b.name), + description: "".to_string(), + always_run: false, + run: test_offer, + environments: None, + test_data: Some(test_data.clone()), + clients: vec![client_a.clone(), client_b.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: format!("RecursiveFindContent {}: block number {}{} {} --> {}", content_type, block_number, get_flair(block_number), client_a.name, client_b.name), + description: "".to_string(), + always_run: false, + run: test_recursive_find_content, + environments: None, + test_data: Some(test_data.clone()), + clients: vec![client_a.clone(), client_b.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: format!("FindContent {}: block number {}{} {} --> {}", content_type, block_number, get_flair(block_number), client_a.name, client_b.name), + description: "".to_string(), + always_run: false, + run: test_find_content, + environments: None, + test_data: Some(test_data), + clients: vec![client_a.clone(), client_b.clone()], + } + ).await; + } + + // Test portal history ping + test.run(NClientTestSpec { + name: format!("PING {} --> {}", client_a.name, client_b.name), + description: "".to_string(), + always_run: false, + run: test_ping, + environments: None, + test_data: None, + clients: vec![client_a.clone(), client_b.clone()], + } + ).await; + + // Test find content non-present + test.run(NClientTestSpec { + name: format!("FIND_CONTENT non present {} --> {}", client_a.name, client_b.name), + description: "find content: calls find content that doesn't exist".to_string(), + always_run: false, + run: test_find_content_non_present, + environments: None, + test_data: None, + clients: vec![client_a.clone(), client_b.clone()], + } + ).await; + + // Test find nodes distance zero + test.run(NClientTestSpec { + name: format!("FIND_NODES Distance 0 {} --> {}", client_a.name, client_b.name), + description: "find nodes: distance zero expect called nodes enr".to_string(), + always_run: false, + run: test_find_nodes_zero_distance, + environments: None, + test_data: None, + clients: vec![client_a.clone(), client_b.clone()], + } + ).await; + + // Test gossiping a collection of blocks to node B (B will gossip back to A) + test.run( + NClientTestSpec { + name: format!("GOSSIP blocks from A:{} --> B:{}", client_a.name, client_b.name), + description: "".to_string(), + always_run: false, + run: test_gossip_two_nodes, + environments: None, + test_data: Some(content.clone().into_iter().map(content_pair_to_string_pair).collect()), + clients: vec![client_a.clone(), client_b.clone()], + } + ).await; + } + } +} + +dyn_async! { + // test that a node will not return content via FINDCONTENT. + async fn test_find_content_non_present<'a>(clients: Vec, _: Option>) { + let (client_a, client_b) = match clients.iter().collect_tuple() { + Some((client_a, client_b)) => (client_a, client_b), + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let header_with_proof_key: HistoryContentKey = serde_json::from_value(json!(HEADER_WITH_PROOF_KEY)).unwrap(); + + let target_enr = match client_b.rpc.node_info().await { + Ok(node_info) => node_info.enr, + Err(err) => { + panic!("Error getting node info: {err:?}"); + } + }; + + let result = client_a.rpc.find_content(target_enr, header_with_proof_key.clone()).await; + + match result { + Ok(result) => { + match result { + ContentInfo::Enrs{ enrs: val } => { + if !val.is_empty() { + panic!("Error: Unexpected FINDCONTENT response: expected ContentInfo::Enrs length 0 got {}", val.len()); + } + }, + ContentInfo::Content{ content: _, .. } => { + panic!("Error: Unexpected FINDCONTENT response: wasn't supposed to return back content"); + }, + other => { + panic!("Error: Unexpected FINDCONTENT response: {other:?}"); + } + } + }, + Err(err) => { + panic!("Error: Unable to get response from FINDCONTENT request: {err:?}"); + } + } + } +} + +dyn_async! { + async fn test_offer<'a>(clients: Vec, test_data: Option>) { + let (client_a, client_b) = match clients.iter().collect_tuple() { + Some((client_a, client_b)) => (client_a, client_b), + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let test_data = match test_data { + Some(test_data) => test_data, + None => panic!("Expected test data non was provided"), + }; + if let Some((optional_key, optional_value)) = test_data.get(1) { + let optional_key: HistoryContentKey = + serde_json::from_value(json!(optional_key)).unwrap(); + let optional_value: HistoryContentValue = + serde_json::from_value(json!(optional_value)).unwrap(); + match client_b.rpc.store(optional_key, optional_value).await { + Ok(result) => if !result { + panic!("Unable to store optional content for recursive find content"); + }, + Err(err) => { + panic!("Error storing optional content for recursive find content: {err:?}"); + } + } + } + let (target_key, target_value) = test_data.first().expect("Target content is required for this test"); + let target_key: HistoryContentKey = + serde_json::from_value(json!(target_key)).unwrap(); + let target_value: HistoryContentValue = + serde_json::from_value(json!(target_value)).unwrap(); + match client_b.rpc.store(target_key.clone(), target_value.clone()).await { + Ok(result) => if !result { + panic!("Error storing target content for recursive find content"); + }, + Err(err) => { + panic!("Error storing target content: {err:?}"); + } + } + + let target_enr = match client_b.rpc.node_info().await { + Ok(node_info) => node_info.enr, + Err(err) => { + panic!("Error getting node info: {err:?}"); + } + }; + + let _ = client_a.rpc.offer(target_enr, target_key.clone(), Some(target_value.clone())).await; + + tokio::time::sleep(Duration::from_secs(8)).await; + + match client_b.rpc.local_content(target_key).await { + Ok(possible_content) => { + match possible_content { + PossibleHistoryContentValue::ContentPresent(content) => { + if content != target_value { + panic!("Error receiving content: Expected content: {target_value:?}, Received content: {content:?}"); + } + } + PossibleHistoryContentValue::ContentAbsent => { + panic!("Expected content not found!"); + } + } + } + Err(err) => { + panic!("Unable to get received content: {err:?}"); + } + } + } +} + +dyn_async! { + async fn test_ping<'a>(clients: Vec, _: Option>) { + let (client_a, client_b) = match clients.iter().collect_tuple() { + Some((client_a, client_b)) => (client_a, client_b), + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let target_enr = match client_b.rpc.node_info().await { + Ok(node_info) => node_info.enr, + Err(err) => { + panic!("Error getting node info: {err:?}"); + } + }; + + let pong = client_a.rpc.ping(target_enr).await; + + if let Err(err) = pong { + panic!("Unable to receive pong info: {err:?}"); + } + + // Verify that client_b stored client_a its ENR through the base layer + // handshake mechanism. + let stored_enr = match client_a.rpc.node_info().await { + Ok(node_info) => node_info.enr, + Err(err) => { + panic!("Error getting node info: {err:?}"); + } + }; + + match HistoryNetworkApiClient::get_enr(&client_b.rpc, stored_enr.node_id()).await { + Ok(response) => { + if response != stored_enr { + panic!("Response from GetEnr didn't return expected ENR. Got: {response}; Expected: {stored_enr}") + } + }, + Err(err) => panic!("Failed while trying to get client A's ENR from client B: {err}"), + } + } +} + +dyn_async! { + async fn test_find_nodes_zero_distance<'a>(clients: Vec, _: Option>) { + let (client_a, client_b) = match clients.iter().collect_tuple() { + Some((client_a, client_b)) => (client_a, client_b), + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let target_enr = match client_b.rpc.node_info().await { + Ok(node_info) => node_info.enr, + Err(err) => { + panic!("Error getting node info: {err:?}"); + } + }; + + match client_a.rpc.find_nodes(target_enr.clone(), vec![0]).await { + Ok(response) => { + if response.len() != 1 { + panic!("Response from FindNodes didn't return expected length of 1"); + } + + match response.first() { + Some(response_enr) => { + if *response_enr != target_enr { + panic!("Response from FindNodes didn't return expected Enr"); + } + }, + None => panic!("Error find nodes zero distance wasn't supposed to return None"), + } + } + Err(err) => panic!("{}", &err.to_string()), + } + } +} + +dyn_async! { + // test that a node will return a content via RECURSIVEFINDCONTENT template that it has stored locally + async fn test_recursive_find_content<'a>(clients: Vec, test_data: Option>) { + let (client_a, client_b) = match clients.iter().collect_tuple() { + Some((client_a, client_b)) => (client_a, client_b), + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let test_data = match test_data { + Some(test_data) => test_data, + None => panic!("Expected test data non was provided"), + }; + if let Some((optional_key, optional_value)) = test_data.get(1) { + let optional_key: HistoryContentKey = + serde_json::from_value(json!(optional_key)).unwrap(); + let optional_value: HistoryContentValue = + serde_json::from_value(json!(optional_value)).unwrap(); + match client_b.rpc.store(optional_key, optional_value).await { + Ok(result) => if !result { + panic!("Unable to store optional content for recursive find content"); + }, + Err(err) => { + panic!("Error storing optional content for recursive find content: {err:?}"); + } + } + } + + let (target_key, target_value) = test_data.first().expect("Target content is required for this test"); + let target_key: HistoryContentKey = + serde_json::from_value(json!(target_key)).unwrap(); + let target_value: HistoryContentValue = + serde_json::from_value(json!(target_value)).unwrap(); + match client_b.rpc.store(target_key.clone(), target_value.clone()).await { + Ok(result) => if !result { + panic!("Error storing target content for recursive find content"); + }, + Err(err) => { + panic!("Error storing target content: {err:?}"); + } + } + + let target_enr = match client_b.rpc.node_info().await { + Ok(node_info) => node_info.enr, + Err(err) => { + panic!("Error getting node info: {err:?}"); + } + }; + + match HistoryNetworkApiClient::add_enr(&client_a.rpc, target_enr.clone()).await { + Ok(response) => match response { + true => (), + false => panic!("AddEnr expected to get true and instead got false") + }, + Err(err) => panic!("{}", &err.to_string()), + } + + match client_a.rpc.recursive_find_content(target_key.clone()).await { + Ok(result) => { + match result { + ContentInfo::Content{ content: ethportal_api::PossibleHistoryContentValue::ContentPresent(val), utp_transfer } => { + if val != target_value { + panic!("Error: Unexpected RECURSIVEFINDCONTENT response: didn't return expected target content"); + } + + if target_value.encode().len() < MAX_PORTAL_CONTENT_PAYLOAD_SIZE { + if utp_transfer { + panic!("Error: Unexpected RECURSIVEFINDCONTENT response: utp_transfer was supposed to be false"); + } + } else if !utp_transfer { + panic!("Error: Unexpected RECURSIVEFINDCONTENT response: utp_transfer was supposed to be true"); + } + }, + other => { + panic!("Error: Unexpected RECURSIVEFINDCONTENT response: {other:?}"); + } + } + }, + Err(err) => { + panic!("Error: Unable to get response from RECURSIVEFINDCONTENT request: {err:?}"); + } + } + } +} + +dyn_async! { + // test that a node will return a x content via FINDCONTENT that it has stored locally + async fn test_find_content<'a> (clients: Vec, test_data: Option>) { + let (client_a, client_b) = match clients.iter().collect_tuple() { + Some((client_a, client_b)) => (client_a, client_b), + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let test_data = match test_data { + Some(test_data) => test_data, + None => panic!("Expected test data none was provided"), + }; + if let Some((optional_key, optional_value)) = test_data.get(1) { + let optional_key: HistoryContentKey = + serde_json::from_value(json!(optional_key)).unwrap(); + let optional_value: HistoryContentValue = + serde_json::from_value(json!(optional_value)).unwrap(); + match client_b.rpc.store(optional_key, optional_value).await { + Ok(result) => if !result { + panic!("Unable to store optional content for find content"); + }, + Err(err) => { + panic!("Error storing optional content for find content: {err:?}"); + } + } + } + + let (target_key, target_value) = test_data.first().expect("Target content is required for this test"); + let target_key: HistoryContentKey = + serde_json::from_value(json!(target_key)).unwrap(); + let target_value: HistoryContentValue = + serde_json::from_value(json!(target_value)).unwrap(); + match client_b.rpc.store(target_key.clone(), target_value.clone()).await { + Ok(result) => if !result { + panic!("Error storing target content for find content"); + }, + Err(err) => { + panic!("Error storing target content: {err:?}"); + } + } + + let target_enr = match client_b.rpc.node_info().await { + Ok(node_info) => node_info.enr, + Err(err) => { + panic!("Error getting node info: {err:?}"); + } + }; + + match client_a.rpc.find_content(target_enr, target_key.clone()).await { + Ok(result) => { + match result { + ContentInfo::Content{ content: ethportal_api::PossibleHistoryContentValue::ContentPresent(val), utp_transfer } => { + if val != target_value { + panic!("Error: Unexpected FINDCONTENT response: didn't return expected block body"); + } + + if target_value.encode().len() < MAX_PORTAL_CONTENT_PAYLOAD_SIZE { + if utp_transfer { + panic!("Error: Unexpected FINDCONTENT response: utp_transfer was supposed to be false"); + } + } else if !utp_transfer { + panic!("Error: Unexpected FINDCONTENT response: utp_transfer was supposed to be true"); + } + }, + other => { + panic!("Error: Unexpected FINDCONTENT response: {other:?}"); + } + } + }, + Err(err) => { + panic!("Error: Unable to get response from FINDCONTENT request: {err:?}"); + } + } + } +} + +dyn_async! { + async fn test_gossip_two_nodes<'a> (clients: Vec, test_data: Option>) { + let (client_a, client_b) = match clients.iter().collect_tuple() { + Some((client_a, client_b)) => (client_a, client_b), + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let test_data = match test_data { + Some(test_data) => test_data, + None => panic!("Expected test data non was provided"), + }; + // connect clients + let client_b_enr = match client_b.rpc.node_info().await { + Ok(node_info) => node_info.enr, + Err(err) => { + panic!("Error getting node info: {err:?}"); + } + }; + match HistoryNetworkApiClient::add_enr(&client_a.rpc, client_b_enr.clone()).await { + Ok(response) => match response { + true => (), + false => panic!("AddEnr expected to get true and instead got false") + }, + Err(err) => panic!("{}", &err.to_string()), + } + + // With default node settings nodes should be storing all content + for (content_key, content_value) in test_data.clone() { + let content_key: HistoryContentKey = + serde_json::from_value(json!(content_key)).unwrap(); + let content_value: HistoryContentValue = + serde_json::from_value(json!(content_value)).unwrap(); + + match client_a.rpc.gossip(content_key.clone(), content_value.clone()).await { + Ok(nodes_gossiped_to) => { + if nodes_gossiped_to != 1 { + panic!("We expected to gossip to 1 node instead we gossiped to: {nodes_gossiped_to}"); + } + } + Err(err) => { + panic!("Unable to get received content: {err:?}"); + } + } + + if let HistoryContentKey::BlockHeaderWithProof(_) = content_key { + tokio::time::sleep(Duration::from_secs(1)).await; + } + } + + // wait test_data.len() seconds for data to propagate, giving more time if more items are propagating + tokio::time::sleep(Duration::from_secs(test_data.len() as u64)).await; + + // process raw test data to generate content details for error output + let (first_header_key, first_header_value) = test_data.first().unwrap(); + let first_header_key: HistoryContentKey = + serde_json::from_value(json!(first_header_key)).unwrap(); + let first_header_value: HistoryContentValue = + serde_json::from_value(json!(first_header_value)).unwrap(); + let mut last_header_seen: (HistoryContentKey, HistoryContentValue) = (first_header_key, first_header_value); + let mut result = vec![]; + for (content_key, content_value) in test_data.into_iter() { + let content_key: HistoryContentKey = + serde_json::from_value(json!(content_key)).unwrap(); + let content_value: HistoryContentValue = + serde_json::from_value(json!(content_value)).unwrap(); + + if let HistoryContentKey::BlockHeaderWithProof(_) = &content_key { + last_header_seen = (content_key.clone(), content_value.clone()); + } + let content_details = + if let HistoryContentValue::BlockHeaderWithProof(header_with_proof) = &last_header_seen.1 { + let content_type = match &content_key { + HistoryContentKey::BlockHeaderWithProof(_) => "header".to_string(), + HistoryContentKey::BlockBody(_) => "body".to_string(), + HistoryContentKey::BlockReceipts(_) => "receipt".to_string(), + HistoryContentKey::EpochAccumulator(_) => "epoch accumulator".to_string(), + }; + format!( + "{}{} {}", + header_with_proof.header.number, + get_flair(header_with_proof.header.number), + content_type + ) + } else { + unreachable!("History test data is formatted incorrectly. Header wasn't infront of data. Please refer to test data file for more information") + }; + + match client_b.rpc.local_content(content_key.clone()).await { + Ok(expected_value) => { + match expected_value { + PossibleHistoryContentValue::ContentPresent(actual_value) => { + if actual_value != content_value { + result.push(format!("Error content received for block {content_details} was different then expected")); + } + } + PossibleHistoryContentValue::ContentAbsent => { + result.push(format!("Error content for block {content_details} was absent")); + } + } + } + Err(err) => { + panic!("Unable to get received content: {err:?}"); + } + } + } + + if !result.is_empty() { + panic!("Client B: {:?}", result); + } + } +} diff --git a/simulators/portal/history/src/suites/mesh.rs b/simulators/portal/history/src/suites/mesh.rs new file mode 100644 index 0000000000..5b9c299e6c --- /dev/null +++ b/simulators/portal/history/src/suites/mesh.rs @@ -0,0 +1,218 @@ +use crate::suites::constants::TRIN_BRIDGE_CLIENT_TYPE; +use ethportal_api::jsonrpsee::core::__reexports::serde_json; +use ethportal_api::types::distance::{Metric, XorMetric}; +use ethportal_api::types::portal::ContentInfo; +use ethportal_api::{ + Discv5ApiClient, HistoryContentKey, HistoryContentValue, HistoryNetworkApiClient, +}; +use hivesim::types::ClientDefinition; +use hivesim::{dyn_async, Client, NClientTestSpec, Test}; +use itertools::Itertools; +use serde_json::json; +use std::collections::HashMap; + +// Header with proof for block number 14764013 +const HEADER_WITH_PROOF_KEY: &str = + "0x00720704f3aa11c53cf344ea069db95cecb81ad7453c8f276b2a1062979611f09c"; +const HEADER_WITH_PROOF_VALUE: &str = "0x080000002d020000f90222a02c58e3212c085178dbb1277e2f3c24b3f451267a75a234945c1581af639f4a7aa058a694212e0416353a4d3865ccf475496b55af3a3d3b002057000741af9731919400192fb10df37c9fb26829eb2cc623cd1bf599e8a067a9fb631f4579f9015ef3c6f1f3830dfa2dc08afe156f750e90022134b9ebf6a018a2978fc62cd1a23e90de920af68c0c3af3330327927cda4c005faccefb5ce7a0168a3827607627e781941dc777737fc4b6beb69a8b139240b881992b35b854eab9010000200000400000001000400080080000000000010004010001000008000000002000110000000000000090020001110402008000080208040010000000a8000000000000000000210822000900205020000000000160020020000400800040000000000042080000000400004008084020001000001004004000001000000000000001000000110000040000010200844040048101000008002000404810082002800000108020000200408008000100000000000000002020000b00010080600902000200000050000400000000000000400000002002101000000a00002000003420000800400000020100002000000000000000c00040000001000000100187327bd7ad3116ce83e147ed8401c9c36483140db184627d9afa9a457468657265756d50504c4e532f326d696e6572735f55534133a0f1a32e24eb62f01ec3f2b3b5893f7be9062fbf5482bc0d490a54352240350e26882087fbb243327696851aae1651b6010cc53ffa2df1bae1550a0000000000000000000000000000000000000000000063d45d0a2242d35484f289108b3c80cccf943005db0db6c67ffea4c4a47fd529f64d74fa6068a3fd89a2c0d9938c3a751c4706d0b0e8f99dec6b517cf12809cb413795c8c678b3171303ddce2fa1a91af6a0961b9db72750d4d5ea7d5103d8d25f23f522d9af4c13fe8ac7a7d9d64bb08d980281eea5298b93cb1085fedc19d4c60afdd52d116cfad030cf4223e50afa8031154a2263c76eb08b96b5b8fdf5e5c30825d5c918eefb89daaf0e8573f20643614d9843a1817b6186074e4e53b22cf49046d977c901ec00aef1555fa89468adc2a51a081f186c995153d1cba0f2887d585212d68be4b958d309fbe611abe98a9bfc3f4b7a7b72bb881b888d89a04ecfe08b1c1a48554a48328646e4f864fe722f12d850f0be29e3829d1f94b34083032a9b6f43abd559785c996229f8e022d4cd6dcde4aafcce6445fe8743e1fcbe8672a99f9d9e3a5ca10c01f3751d69fbd22197f0680bc1529151130b22759bf185f4dbce357f46eb9cc8e21ea78f49b298eea2756d761fe23de8bea0d2e15aed136d689f6d252c54ebadc3e46b84a397b681edf7ec63522b9a298301084d019d0020000000000000000000000000000000000000000000000000000000000000"; + +// private key hive environment variable +const PRIVATE_KEY_ENVIRONMENT_VARIABLE: &str = "HIVE_CLIENT_PRIVATE_KEY"; + +dyn_async! { + pub async fn test_portal_scenarios<'a> (test: &'a mut Test, _client: Option) { + // Get all available portal clients + let clients = test.sim.client_types().await; + // todo: remove this once we implement role in hivesim-rs + let clients: Vec = clients.into_iter().filter(|client| client.name != *TRIN_BRIDGE_CLIENT_TYPE).collect(); + + let private_key_1 = "fc34e57cc83ed45aae140152fd84e2c21d1f4d46e19452e13acc7ee90daa5bac".to_string(); + let private_key_2 = "e5add57dc4c9ef382509e61ce106ec86f60eb73bbfe326b00f54bf8e1819ba11".to_string(); + + // Iterate over all possible pairings of clients and run the tests (including self-pairings) + for ((client_a, client_b), client_c) in clients.iter().cartesian_product(clients.iter()).cartesian_product(clients.iter()) { + test.run( + NClientTestSpec { + name: format!("FIND_CONTENT content stored 2 nodes away stored in client C (Client B closer to content then C). A:{} --> B:{} --> C:{}", client_a.name, client_b.name, client_c.name), + description: "".to_string(), + always_run: false, + run: test_find_content_two_jumps, + environments: Some(vec![None, Some(HashMap::from([(PRIVATE_KEY_ENVIRONMENT_VARIABLE.to_string(), private_key_2.clone())])), Some(HashMap::from([(PRIVATE_KEY_ENVIRONMENT_VARIABLE.to_string(), private_key_1.clone())]))]), + test_data: None, + clients: vec![client_a.clone(), client_b.clone(), client_c.clone()], + } + ).await; + + // Remove this after the clients are stable across two jumps test + test.run( + NClientTestSpec { + name: format!("FIND_CONTENT content stored 2 nodes away stored in client C (Client C closer to content then B). A:{} --> B:{} --> C:{}", client_a.name, client_b.name, client_c.name), + description: "".to_string(), + always_run: false, + run: test_find_content_two_jumps, + environments: Some(vec![None, Some(HashMap::from([(PRIVATE_KEY_ENVIRONMENT_VARIABLE.to_string(), private_key_1.clone())])), Some(HashMap::from([(PRIVATE_KEY_ENVIRONMENT_VARIABLE.to_string(), private_key_2.clone())]))]), + test_data: None, + clients: vec![client_a.clone(), client_b.clone(), client_c.clone()], + } + ).await; + + // Test find nodes distance of client a + test.run(NClientTestSpec { + name: format!("FIND_NODES distance of client C {} --> {} --> {}", client_a.name, client_b.name, client_c.name), + description: "find nodes: distance of client A expect seeded enr returned".to_string(), + always_run: false, + run: test_find_nodes_distance_of_client_c, + environments: None, + test_data: None, + clients: vec![client_a.clone(), client_b.clone(), client_c.clone()], + } + ).await; + } + } +} + +dyn_async! { + async fn test_find_content_two_jumps<'a> (clients: Vec, _: Option>) { + let (client_a, client_b, client_c) = match clients.iter().collect_tuple() { + Some((client_a, client_b, client_c)) => (client_a, client_b, client_c), + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + + let header_with_proof_key: HistoryContentKey = serde_json::from_value(json!(HEADER_WITH_PROOF_KEY)).unwrap(); + let header_with_proof_value: HistoryContentValue = serde_json::from_value(json!(HEADER_WITH_PROOF_VALUE)).unwrap(); + + // get enr for b and c to seed for the jumps + let client_b_enr = match client_b.rpc.node_info().await { + Ok(node_info) => node_info.enr, + Err(err) => { + panic!("Error getting node info: {err:?}"); + } + }; + + let client_c_enr = match client_c.rpc.node_info().await { + Ok(node_info) => node_info.enr, + Err(err) => { + panic!("Error getting node info: {err:?}"); + } + }; + + // seed client_c_enr into routing table of client_b + match HistoryNetworkApiClient::add_enr(&client_b.rpc, client_c_enr.clone()).await { + Ok(response) => match response { + true => (), + false => panic!("AddEnr expected to get true and instead got false") + }, + Err(err) => panic!("{}", &err.to_string()), + } + + // send a ping from client B to C to connect the clients + if let Err(err) = client_b.rpc.ping(client_c_enr.clone()).await { + panic!("Unable to receive pong info: {err:?}"); + } + + // seed the data into client_c + match client_c.rpc.store(header_with_proof_key.clone(), header_with_proof_value.clone()).await { + Ok(result) => if !result { + panic!("Unable to store header with proof for find content immediate return test"); + }, + Err(err) => { + panic!("Error storing header with proof for find content immediate return test: {err:?}"); + } + } + + let enrs = match client_a.rpc.find_content(client_b_enr.clone(), header_with_proof_key.clone()).await { + Ok(result) => { + match result { + ContentInfo::Enrs{ enrs } => { + enrs + }, + other => { + panic!("Error: (Enrs) Unexpected FINDCONTENT response not: {other:?}"); + } + } + }, + Err(err) => { + panic!("Error: (Enrs) Unable to get response from FINDCONTENT request: {err:?}"); + } + }; + + if enrs.len() != 1 { + panic!("Known node is closer to content, Enrs returned should be 0 instead got: length {}", enrs.len()); + } + + match client_a.rpc.find_content(enrs[0].clone(), header_with_proof_key.clone()).await { + Ok(result) => { + match result { + ContentInfo::Content{ content: ethportal_api::PossibleHistoryContentValue::ContentPresent(val), utp_transfer } => { + if val != header_with_proof_value { + panic!("Error: Unexpected FINDCONTENT response: didn't return expected header with proof value"); + } + + if utp_transfer { + panic!("Error: Unexpected FINDCONTENT response: utp_transfer was supposed to be false"); + } + }, + other => { + panic!("Error: Unexpected FINDCONTENT response: {other:?}"); + } + } + }, + Err(err) => { + panic!("Error: Unable to get response from FINDCONTENT request: {err:?}"); + } + } + } +} + +dyn_async! { + async fn test_find_nodes_distance_of_client_c<'a>(clients: Vec, _: Option>) { + let (client_a, client_b, client_c) = match clients.iter().collect_tuple() { + Some((client_a, client_b, client_c)) => (client_a, client_b, client_c), + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + + let target_enr = match client_b.rpc.node_info().await { + Ok(node_info) => node_info.enr, + Err(err) => { + panic!("Error getting node info: {err:?}"); + } + }; + + // We are adding client C to our list so we then can assume only one client per bucket + let client_c_enr = match client_c.rpc.node_info().await { + Ok(node_info) => node_info.enr, + Err(err) => { + panic!("Error getting node info: {err:?}"); + } + }; + + // seed enr into routing table + match HistoryNetworkApiClient::add_enr(&client_b.rpc, client_c_enr.clone()).await { + Ok(response) => if !response { + panic!("AddEnr expected to get true and instead got false") + }, + Err(err) => panic!("{}", &err.to_string()), + } + + if let Some(distance) = XorMetric::distance(&target_enr.node_id().raw(), &client_c_enr.node_id().raw()).log2() { + match client_a.rpc.find_nodes(target_enr.clone(), vec![distance as u16]).await { + Ok(response) => { + if response.is_empty() { + panic!("FindNodes expected to have received a non-empty response"); + } + + if !response.contains(&client_c_enr) { + panic!("FindNodes {distance} distance expected to contained seeded Enr"); + } + } + Err(err) => panic!("{}", &err.to_string()), + } + } else { + panic!("Distance calculation failed"); + } + } +} diff --git a/simulators/portal/history/src/suites/mod.rs b/simulators/portal/history/src/suites/mod.rs new file mode 100644 index 0000000000..a6bd87537c --- /dev/null +++ b/simulators/portal/history/src/suites/mod.rs @@ -0,0 +1,5 @@ +pub mod constants; +pub mod interop; +pub mod mesh; +pub mod rpc_compat; +pub mod trin_bridge; diff --git a/simulators/portal/history/src/suites/rpc_compat.rs b/simulators/portal/history/src/suites/rpc_compat.rs new file mode 100644 index 0000000000..bdae9c3526 --- /dev/null +++ b/simulators/portal/history/src/suites/rpc_compat.rs @@ -0,0 +1,597 @@ +use crate::suites::constants::TRIN_BRIDGE_CLIENT_TYPE; +use ethportal_api::types::enr::generate_random_remote_enr; +use ethportal_api::types::portal::ContentInfo; +use ethportal_api::Discv5ApiClient; +use ethportal_api::PossibleHistoryContentValue::{ContentAbsent, ContentPresent}; +use ethportal_api::{HistoryContentKey, HistoryNetworkApiClient}; +use hivesim::types::ClientDefinition; +use hivesim::{dyn_async, Client, NClientTestSpec, Test}; +use serde_json::json; + +// Header with proof for block number 14764013 +const CONTENT_KEY: &str = "0x00720704f3aa11c53cf344ea069db95cecb81ad7453c8f276b2a1062979611f09c"; +const CONTENT_VALUE: &str = "0x080000002d020000f90222a02c58e3212c085178dbb1277e2f3c24b3f451267a75a234945c1581af639f4a7aa058a694212e0416353a4d3865ccf475496b55af3a3d3b002057000741af9731919400192fb10df37c9fb26829eb2cc623cd1bf599e8a067a9fb631f4579f9015ef3c6f1f3830dfa2dc08afe156f750e90022134b9ebf6a018a2978fc62cd1a23e90de920af68c0c3af3330327927cda4c005faccefb5ce7a0168a3827607627e781941dc777737fc4b6beb69a8b139240b881992b35b854eab9010000200000400000001000400080080000000000010004010001000008000000002000110000000000000090020001110402008000080208040010000000a8000000000000000000210822000900205020000000000160020020000400800040000000000042080000000400004008084020001000001004004000001000000000000001000000110000040000010200844040048101000008002000404810082002800000108020000200408008000100000000000000002020000b00010080600902000200000050000400000000000000400000002002101000000a00002000003420000800400000020100002000000000000000c00040000001000000100187327bd7ad3116ce83e147ed8401c9c36483140db184627d9afa9a457468657265756d50504c4e532f326d696e6572735f55534133a0f1a32e24eb62f01ec3f2b3b5893f7be9062fbf5482bc0d490a54352240350e26882087fbb243327696851aae1651b6010cc53ffa2df1bae1550a0000000000000000000000000000000000000000000063d45d0a2242d35484f289108b3c80cccf943005db0db6c67ffea4c4a47fd529f64d74fa6068a3fd89a2c0d9938c3a751c4706d0b0e8f99dec6b517cf12809cb413795c8c678b3171303ddce2fa1a91af6a0961b9db72750d4d5ea7d5103d8d25f23f522d9af4c13fe8ac7a7d9d64bb08d980281eea5298b93cb1085fedc19d4c60afdd52d116cfad030cf4223e50afa8031154a2263c76eb08b96b5b8fdf5e5c30825d5c918eefb89daaf0e8573f20643614d9843a1817b6186074e4e53b22cf49046d977c901ec00aef1555fa89468adc2a51a081f186c995153d1cba0f2887d585212d68be4b958d309fbe611abe98a9bfc3f4b7a7b72bb881b888d89a04ecfe08b1c1a48554a48328646e4f864fe722f12d850f0be29e3829d1f94b34083032a9b6f43abd559785c996229f8e022d4cd6dcde4aafcce6445fe8743e1fcbe8672a99f9d9e3a5ca10c01f3751d69fbd22197f0680bc1529151130b22759bf185f4dbce357f46eb9cc8e21ea78f49b298eea2756d761fe23de8bea0d2e15aed136d689f6d252c54ebadc3e46b84a397b681edf7ec63522b9a298301084d019d0020000000000000000000000000000000000000000000000000000000000000"; + +dyn_async! { + pub async fn run_rpc_compat_test_suite<'a> (test: &'a mut Test, _client: Option) { + // Get all available portal clients + let clients = test.sim.client_types().await; + // todo: remove this once we implement role in hivesim-rs + let clients: Vec = clients.into_iter().filter(|client| client.name != *TRIN_BRIDGE_CLIENT_TYPE).collect(); + + // Test single type of client + for client in &clients { + test.run( + NClientTestSpec { + name: "discv5_nodeInfo".to_string(), + description: "".to_string(), + always_run: false, + run: test_node_info, + environments: None, + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_historyLocalContent Expect ContentAbsent".to_string(), + description: "".to_string(), + always_run: false, + run: test_local_content_expect_content_absent, + environments: None, + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_historyStore".to_string(), + description: "".to_string(), + always_run: false, + run: test_store, + environments: None, + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_historyLocalContent Expect ContentPresent".to_string(), + description: "".to_string(), + always_run: false, + run: test_local_content_expect_content_present, + environments: None, + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_historyAddEnr Expect true".to_string(), + description: "".to_string(), + always_run: false, + run: test_add_enr_expect_true, + environments: None, + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_historyGetEnr None Found".to_string(), + description: "".to_string(), + always_run: false, + run: test_get_enr_non_present, + environments: None, + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_historyGetEnr ENR Found".to_string(), + description: "".to_string(), + always_run: false, + run: test_get_enr_enr_present, + environments: None, + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_historyGetEnr Local Enr".to_string(), + description: "".to_string(), + always_run: false, + run: test_get_enr_local_enr, + environments: None, + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_historyDeleteEnr None Found".to_string(), + description: "".to_string(), + always_run: false, + run: test_delete_enr_non_present, + environments: None, + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_historyDeleteEnr ENR Found".to_string(), + description: "".to_string(), + always_run: false, + run: test_delete_enr_enr_present, + environments: None, + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_historyLookupEnr None Found".to_string(), + description: "".to_string(), + always_run: false, + run: test_lookup_enr_non_present, + environments: None, + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_historyLookupEnr ENR Found".to_string(), + description: "".to_string(), + always_run: false, + run: test_lookup_enr_enr_present, + environments: None, + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_historyLookupEnr Local Enr".to_string(), + description: "".to_string(), + always_run: false, + run: test_lookup_enr_local_enr, + environments: None, + test_data: None, + clients: vec![client.clone()], + } + ).await; + + test.run( + NClientTestSpec { + name: "portal_historyRecursiveFindContent Content Absent".to_string(), + description: "".to_string(), + always_run: false, + run: test_recursive_find_content_content_absent, + environments: None, + test_data: None, + clients: vec![client.clone()], + } + ).await; + } + } +} + +dyn_async! { + async fn test_node_info<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + + let response = Discv5ApiClient::node_info(&client.rpc).await; + + if let Err(err) = response { + panic!("Expected response not received: {err}"); + } + } +} + +dyn_async! { + async fn test_local_content_expect_content_absent<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let content_key = + serde_json::from_value(json!(CONTENT_KEY)); + + match content_key { + Ok(content_key) => { + let response = HistoryNetworkApiClient::local_content(&client.rpc, content_key).await; + + match response { + Ok(response) => { + match response { + ContentAbsent => (), + _ => panic!("Expected ContentAbsent, got ContentPresent") + } + }, + Err(err) => { + panic!("{}", &err.to_string()); + }, + } + } + Err(err) => { + panic!("{}", &err.to_string()); + } + } + } +} + +dyn_async! { + async fn test_store<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let content_key = + serde_json::from_value(json!(CONTENT_KEY)); + + let content_value = + serde_json::from_value(json!(CONTENT_VALUE)); + + match content_key { + Ok(content_key) => { + match content_value { + Ok(content_value) => { + let response = HistoryNetworkApiClient::store(&client.rpc, content_key, content_value).await; + + if let Err(err) = response { + panic!("{}", &err.to_string()); + } + } + Err(err) => { + panic!("{}", &err.to_string()); + } + } + } + Err(err) => { + panic!("{}", &err.to_string()); + } + } + } +} + +dyn_async! { + async fn test_local_content_expect_content_present<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let content_key: Result = + serde_json::from_value(json!(CONTENT_KEY)); + + let content_value = + serde_json::from_value(json!(CONTENT_VALUE)); + + + match content_key { + Ok(content_key) => { + // seed content_key/content_value onto the local node to test local_content expect content present + match content_value { + Ok(content_value) => { + let response = HistoryNetworkApiClient::store(&client.rpc, content_key.clone(), content_value).await; + + if let Err(err) = response { + panic!("{}", &err.to_string()); + } + } + Err(err) => { + panic!("{}", &err.to_string()); + } + } + + // Here we are calling local_content RPC to test if the content is present + let response = HistoryNetworkApiClient::local_content(&client.rpc, content_key).await; + + match response { + Ok(response) => { + match response { + ContentPresent(_) => (), + _ => panic!("Expected ContentPresent, got ContentAbsent") + } + }, + Err(err) => { + panic!("{}", &err.to_string()); + }, + } + } + Err(err) => { + panic!("{}", &err.to_string()); + } + } + } +} + +dyn_async! { + async fn test_add_enr_expect_true<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let (_, enr) = generate_random_remote_enr(); + match HistoryNetworkApiClient::add_enr(&client.rpc, enr).await { + Ok(response) => match response { + true => (), + false => panic!("AddEnr expected to get true and instead got false") + }, + Err(err) => panic!("{}", &err.to_string()), + } + } +} + +dyn_async! { + async fn test_get_enr_non_present<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let (_, enr) = generate_random_remote_enr(); + + if (HistoryNetworkApiClient::get_enr(&client.rpc, enr.node_id()).await).is_ok() { + panic!("GetEnr in this case is not supposed to return a value") + } + } +} + +dyn_async! { + async fn test_get_enr_local_enr<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + // get our local enr from NodeInfo + let target_enr = match Discv5ApiClient::node_info(&client.rpc).await { + Ok(node_info) => node_info.enr, + Err(err) => { + panic!("Error getting node info: {err:?}"); + } + }; + + // check if we can fetch data from routing table + match HistoryNetworkApiClient::get_enr(&client.rpc, target_enr.node_id()).await { + Ok(response) => { + if response != target_enr { + panic!("Response from GetEnr didn't return expected Enr") + } + }, + Err(err) => panic!("{}", &err.to_string()), + } + } +} + +dyn_async! { + async fn test_get_enr_enr_present<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let (_, enr) = generate_random_remote_enr(); + + // seed enr into routing table + match HistoryNetworkApiClient::add_enr(&client.rpc, enr.clone()).await { + Ok(response) => match response { + true => (), + false => panic!("AddEnr expected to get true and instead got false") + }, + Err(err) => panic!("{}", &err.to_string()), + } + + // check if we can fetch data from routing table + match HistoryNetworkApiClient::get_enr(&client.rpc, enr.node_id()).await { + Ok(response) => { + if response != enr { + panic!("Response from GetEnr didn't return expected Enr") + } + }, + Err(err) => panic!("{}", &err.to_string()), + } + } +} + +dyn_async! { + async fn test_delete_enr_non_present<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let (_, enr) = generate_random_remote_enr(); + match HistoryNetworkApiClient::delete_enr(&client.rpc, enr.node_id()).await { + Ok(response) => match response { + true => panic!("DeleteEnr expected to get false and instead got true"), + false => () + }, + Err(err) => panic!("{}", &err.to_string()), + }; + } +} + +dyn_async! { + async fn test_delete_enr_enr_present<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let (_, enr) = generate_random_remote_enr(); + + // seed enr into routing table + match HistoryNetworkApiClient::add_enr(&client.rpc, enr.clone()).await { + Ok(response) => match response { + true => (), + false => panic!("AddEnr expected to get true and instead got false") + }, + Err(err) => panic!("{}", &err.to_string()), + } + + // check if data was seeded into the table + match HistoryNetworkApiClient::get_enr(&client.rpc, enr.node_id()).await { + Ok(response) => { + if response != enr { + panic!("Response from GetEnr didn't return expected Enr") + } + }, + Err(err) => panic!("{}", &err.to_string()), + } + + // delete the data from routing table + match HistoryNetworkApiClient::delete_enr(&client.rpc, enr.node_id()).await { + Ok(response) => match response { + true => (), + false => panic!("DeleteEnr expected to get true and instead got false") + }, + Err(err) => panic!("{}", &err.to_string()), + }; + + // check if the enr was actually deleted out of the table or not + if (HistoryNetworkApiClient::get_enr(&client.rpc, enr.node_id()).await).is_ok() { + panic!("GetEnr in this case is not supposed to return a value") + } + } +} + +dyn_async! { + async fn test_lookup_enr_non_present<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let (_, enr) = generate_random_remote_enr(); + + if (HistoryNetworkApiClient::lookup_enr(&client.rpc, enr.node_id()).await).is_ok() { + panic!("LookupEnr in this case is not supposed to return a value") + } + } +} + +dyn_async! { + async fn test_lookup_enr_enr_present<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let (_, enr) = generate_random_remote_enr(); + + // seed enr into routing table + match HistoryNetworkApiClient::add_enr(&client.rpc, enr.clone()).await { + Ok(response) => match response { + true => (), + false => panic!("AddEnr expected to get true and instead got false") + }, + Err(err) => panic!("{}", &err.to_string()), + } + + // check if we can fetch data from routing table + match HistoryNetworkApiClient::lookup_enr(&client.rpc, enr.node_id()).await { + Ok(response) => { + if response != enr { + panic!("Response from LookupEnr didn't return expected Enr") + } + }, + Err(err) => panic!("{}", &err.to_string()), + } + } +} + +dyn_async! { + async fn test_lookup_enr_local_enr<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + // get our local enr from NodeInfo + let target_enr = match Discv5ApiClient::node_info(&client.rpc).await { + Ok(node_info) => node_info.enr, + Err(err) => { + panic!("Error getting node info: {err:?}"); + } + }; + + // check if we can fetch data from routing table + match HistoryNetworkApiClient::lookup_enr(&client.rpc, target_enr.node_id()).await { + Ok(response) => { + if response != target_enr { + panic!("Response from LookupEnr didn't return expected Enr") + } + }, + Err(err) => panic!("{}", &err.to_string()), + } + } +} + +dyn_async! { + // test that a node will return a AbsentContent via RecursiveFindContent when the data doesn't exist + async fn test_recursive_find_content_content_absent<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + let header_with_proof_key: HistoryContentKey = serde_json::from_value(json!(CONTENT_KEY)).unwrap(); + + match HistoryNetworkApiClient::recursive_find_content(&client.rpc, header_with_proof_key).await { + Ok(result) => { + match result { + ContentInfo::Content{ content: ethportal_api::PossibleHistoryContentValue::ContentAbsent, utp_transfer } => { + if utp_transfer { + panic!("Error: Unexpected RecursiveFindContent response: utp_transfer was supposed to be false"); + } + }, + other => { + panic!("Error: Unexpected RecursiveFindContent response: {other:?}"); + } + } + }, + Err(err) => { + panic!("Error: Unable to get response from RecursiveFindContent request: {err:?}"); + } + } + } +} diff --git a/simulators/portal/history/src/suites/trin_bridge.rs b/simulators/portal/history/src/suites/trin_bridge.rs new file mode 100644 index 0000000000..f630164e29 --- /dev/null +++ b/simulators/portal/history/src/suites/trin_bridge.rs @@ -0,0 +1,130 @@ +use super::constants::{ + BOOTNODES_ENVIRONMENT_VARIABLE, HIVE_CHECK_LIVE_PORT, TEST_DATA_FILE_PATH, + TRIN_BRIDGE_CLIENT_TYPE, +}; +use ethportal_api::HistoryContentKey; +use ethportal_api::HistoryContentValue; +use ethportal_api::PossibleHistoryContentValue; +use ethportal_api::{Discv5ApiClient, HistoryNetworkApiClient}; +use hivesim::types::ClientDefinition; +use hivesim::{dyn_async, Client, NClientTestSpec, Test}; +use portal_spec_test_utils_rs::get_flair; +use serde_yaml::Value; +use std::collections::HashMap; +use tokio::time::Duration; + +fn process_content(content: Vec<(HistoryContentKey, HistoryContentValue)>) -> Vec { + let mut last_header = content.first().unwrap().clone(); + + let mut result: Vec = vec![]; + for history_content in content.into_iter() { + if let HistoryContentKey::BlockHeaderWithProof(_) = &history_content.0 { + last_header = history_content.clone(); + } + let comment = + if let HistoryContentValue::BlockHeaderWithProof(header_with_proof) = &last_header.1 { + let content_type = match &history_content.0 { + HistoryContentKey::BlockHeaderWithProof(_) => "header".to_string(), + HistoryContentKey::BlockBody(_) => "body".to_string(), + HistoryContentKey::BlockReceipts(_) => "receipt".to_string(), + HistoryContentKey::EpochAccumulator(_) => "epoch accumulator".to_string(), + }; + format!( + "{}{} {}", + header_with_proof.header.number, + get_flair(header_with_proof.header.number), + content_type + ) + } else { + unreachable!("History test dated is formatted incorrectly") + }; + result.push(comment) + } + result +} + +dyn_async! { + pub async fn test_portal_bridge<'a> (test: &'a mut Test, _client: Option) { + // Get all available portal clients + let clients = test.sim.client_types().await; + if !clients.iter().any(|client_definition| client_definition.name == *TRIN_BRIDGE_CLIENT_TYPE) { + panic!("This simulator is required to be ran with client `trin-bridge`") + } + let clients: Vec = clients.into_iter().filter(|client| client.name != *TRIN_BRIDGE_CLIENT_TYPE).collect(); + + // Iterate over all possible pairings of clients and run the tests (including self-pairings) + for client in &clients { + test.run( + NClientTestSpec { + name: format!("Bridge test. A:Trin Bridge --> B:{}", client.name), + description: "".to_string(), + always_run: false, + run: test_bridge, + environments: None, + test_data: None, + clients: vec![client.clone()], + } + ).await; + } + } +} + +dyn_async! { + async fn test_bridge<'a>(clients: Vec, _: Option>) { + let client = match clients.into_iter().next() { + Some((client)) => client, + None => { + panic!("Unable to get expected amount of clients from NClientTestSpec"); + } + }; + + let client_enr = match client.rpc.node_info().await { + Ok(node_info) => node_info.enr, + Err(err) => { + panic!("Error getting node info: {err:?}"); + } + }; + client.test.start_client(TRIN_BRIDGE_CLIENT_TYPE.to_string(), Some(HashMap::from([(BOOTNODES_ENVIRONMENT_VARIABLE.to_string(), client_enr.to_base64()), (HIVE_CHECK_LIVE_PORT.to_string(), 0.to_string())]))).await; + + // With default node settings nodes should be storing all content + let values = std::fs::read_to_string(TEST_DATA_FILE_PATH) + .expect("cannot find test asset"); + let values: Value = serde_yaml::from_str(&values).unwrap(); + let content_vec: Vec<(HistoryContentKey, HistoryContentValue)> = values.as_sequence().unwrap().iter().map(|value| { + let content_key: HistoryContentKey = + serde_yaml::from_value(value.get("content_key").unwrap().clone()).unwrap(); + let content_value: HistoryContentValue = + serde_yaml::from_value(value.get("content_value").unwrap().clone()).unwrap(); + (content_key, content_value) + }).collect(); + let comments = process_content(content_vec.clone()); + + // wait content_vec.len() seconds for data to propagate, giving more time if more items are propagating + tokio::time::sleep(Duration::from_secs(content_vec.len() as u64) * 2).await; + + let mut result = vec![]; + for (index, (content_key, content_value)) in content_vec.into_iter().enumerate() { + match client.rpc.local_content(content_key.clone()).await { + Ok(possible_content) => { + match possible_content { + PossibleHistoryContentValue::ContentPresent(content) => { + if content != content_value { + result.push(format!("Error content received for block {} was different then expected", comments[index])); + } + } + PossibleHistoryContentValue::ContentAbsent => { + result.push(format!("Error content for block {} was absent", comments[index])); + } + } + } + Err(err) => { + panic!("Unable to get received content: {err:?}"); + } + } + } + + if !result.is_empty() { + panic!("Client: {:?}", result); + } + } +}