From 3e2fa7cebead6c2f9a413de03b497d50dcd8c6fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Fri, 22 Nov 2024 18:55:39 +0100 Subject: [PATCH 01/25] feat(l1): add hive report to workflow summary (#1238) --- .github/workflows/hive_coverage.yaml | 2 +- cmd/hive_report/src/main.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/hive_coverage.yaml b/.github/workflows/hive_coverage.yaml index b11ec2ffb..ec0656565 100644 --- a/.github/workflows/hive_coverage.yaml +++ b/.github/workflows/hive_coverage.yaml @@ -48,4 +48,4 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Generate the hive report - run: cargo run -p hive_report + run: cargo run -p hive_report >> $GITHUB_STEP_SUMMARY diff --git a/cmd/hive_report/src/main.rs b/cmd/hive_report/src/main.rs index b367aa597..5d7fea914 100644 --- a/cmd/hive_report/src/main.rs +++ b/cmd/hive_report/src/main.rs @@ -61,8 +61,10 @@ fn main() -> Result<(), Box> { // Sort by file name. results.sort_by(|a, b| a.0.cmp(&b.0)); + println!("# Hive coverage report\n"); + for (file_name, passed, total) in results { - println!("{}: {}/{}", file_name, passed, total); + println!("- {}: {}/{}", file_name, passed, total); } Ok(()) From de288cdd59a9d147320b88ae444c1b40093c08d7 Mon Sep 17 00:00:00 2001 From: Martin Paulucci Date: Fri, 22 Nov 2024 20:04:15 +0100 Subject: [PATCH 02/25] build(l1): add paralellism on hive test in CI. (#1237) **Motivation** Make hive tests faster **Description** - Added `--sim.parallelism 4` to hive tests to try it. - Divided Assertor and Hive into two different workflows - Made all the jobs names use snake case for consistency --- .github/workflows/asertoor.yaml | 46 +++++++++++++++++++ .github/workflows/ci.yaml | 2 +- .github/workflows/ci_levm.yaml | 2 - .github/workflows/ci_skipped.yaml | 2 +- .../{docker-build.yaml => docker_build.yaml} | 2 +- .../{hive_and_assertoor.yaml => hive.yaml} | 44 +++--------------- .github/workflows/hive_coverage.yaml | 2 +- .../{lint-pr-title.yml => lint_pr_title.yml} | 0 Makefile | 2 +- 9 files changed, 58 insertions(+), 44 deletions(-) create mode 100644 .github/workflows/asertoor.yaml rename .github/workflows/{docker-build.yaml => docker_build.yaml} (97%) rename .github/workflows/{hive_and_assertoor.yaml => hive.yaml} (64%) rename .github/workflows/{lint-pr-title.yml => lint_pr_title.yml} (100%) diff --git a/.github/workflows/asertoor.yaml b/.github/workflows/asertoor.yaml new file mode 100644 index 000000000..f08a1609f --- /dev/null +++ b/.github/workflows/asertoor.yaml @@ -0,0 +1,46 @@ +name: "Assertoor" +on: + merge_group: + pull_request: + branches: ["**"] + paths-ignore: + - 'README.md' + - 'LICENSE' + - "**/README.md" + - "**/docs/**" + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +env: + RUST_VERSION: 1.80.1 + +jobs: + build: + uses: ./.github/workflows/docker_build.yaml + + run-assertoor: + name: Stability Check + runs-on: ubuntu-latest + needs: [build] + steps: + - uses: actions/checkout@v4 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: ethrex_image + path: /tmp + + - name: Load image + run: | + docker load --input /tmp/ethrex_image.tar + + - name: Setup kurtosis testnet and run assertoor tests + uses: ethpandaops/kurtosis-assertoor-github-action@v1 + with: + kurtosis_version: '1.3.1' + ethereum_package_url: 'github.com/lambdaclass/ethereum-package' + ethereum_package_branch: 'ethrex-integration' + ethereum_package_args: './test_data/network_params.yaml' diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b98a92d72..33087270f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -78,7 +78,7 @@ jobs: run: | make test - docker-build: + docker_build: name: Build Docker image runs-on: ubuntu-latest steps: diff --git a/.github/workflows/ci_levm.yaml b/.github/workflows/ci_levm.yaml index 05b500e4d..f5d3a3a77 100644 --- a/.github/workflows/ci_levm.yaml +++ b/.github/workflows/ci_levm.yaml @@ -2,8 +2,6 @@ name: CI LEVM on: merge_group: - paths: - - "crates/vm/levm/**" pull_request: paths: - "crates/vm/levm/**" diff --git a/.github/workflows/ci_skipped.yaml b/.github/workflows/ci_skipped.yaml index dd034fdfd..5488778a9 100644 --- a/.github/workflows/ci_skipped.yaml +++ b/.github/workflows/ci_skipped.yaml @@ -21,7 +21,7 @@ jobs: if: false steps: [run: true] - docker-build: + docker_build: name: Build Docker image runs-on: ubuntu-latest if: false diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker_build.yaml similarity index 97% rename from .github/workflows/docker-build.yaml rename to .github/workflows/docker_build.yaml index 4ae4be06e..4b59ed342 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker_build.yaml @@ -4,7 +4,7 @@ on: workflow_call: jobs: - docker-build: + docker_build: name: Docker Build image runs-on: ubuntu-latest steps: diff --git a/.github/workflows/hive_and_assertoor.yaml b/.github/workflows/hive.yaml similarity index 64% rename from .github/workflows/hive_and_assertoor.yaml rename to .github/workflows/hive.yaml index 78287eab7..5e8cce178 100644 --- a/.github/workflows/hive_and_assertoor.yaml +++ b/.github/workflows/hive.yaml @@ -1,11 +1,6 @@ -name: "Hive & Assertoor" +name: "Hive" on: merge_group: - paths-ignore: - - 'README.md' - - 'LICENSE' - - "**/README.md" - - "**/docs/**" pull_request: branches: ["**"] paths-ignore: @@ -23,10 +18,10 @@ env: jobs: build: - uses: ./.github/workflows/docker-build.yaml + uses: ./.github/workflows/docker_build.yaml run-hive: - name: Hive - ${{ matrix.name }} + name: ${{ matrix.name }} needs: [build] runs-on: ubuntu-latest strategy: @@ -36,8 +31,8 @@ jobs: name: "Rpc Compat tests" run_command: make run-hive-on-latest SIMULATION=ethereum/rpc-compat TEST_PATTERN="/eth_chainId|eth_getTransactionByBlockHashAndIndex|eth_getTransactionByBlockNumberAndIndex|eth_getCode|eth_getStorageAt|eth_call|eth_getTransactionByHash|eth_getBlockByHash|eth_getBlockByNumber|eth_createAccessList|eth_getBlockTransactionCountByNumber|eth_getBlockTransactionCountByHash|eth_getBlockReceipts|eth_getTransactionReceipt|eth_blobGasPrice|eth_blockNumber|ethGetTransactionCount|debug_getRawHeader|debug_getRawBlock|debug_getRawTransaction|debug_getRawReceipts|eth_estimateGas|eth_getBalance|eth_sendRawTransaction|eth_getProof|eth_getLogs" - simulation: rpc-auth - name: "Rpc Auth tests" - run_command: make run-hive-on-latest SIMULATION=ethereum/rpc-compat TEST_PATTERN="/engine-auth" + name: "Engine Auth tests" + run_command: make run-hive-on-latest SIMULATION=ethereum/engine TEST_PATTERN="auth/engine-auth" - simulation: discv4 name: "Devp2p discv4 tests" run_command: make run-hive-on-latest SIMULATION=devp2p TEST_PATTERN="discv4" @@ -46,13 +41,13 @@ jobs: run_command: make run-hive-on-latest SIMULATION=devp2p TEST_PATTERN="/AccountRange|StorageRanges|ByteCodes|TrieNodes" - simulation: eth name: "Devp2p eth tests" - run_command: make run-hive SIMULATION=devp2p TEST_PATTERN="eth/status|getblockheaders|getblockbodies|transaction" + run_command: make run-hive-on-latest SIMULATION=devp2p TEST_PATTERN="eth/Status|GetBlockHeaders|SimultaneousRequests|SameRequestID|ZeroRequestID|GetBlockBodies|MaliciousHandshake|MaliciousStatus|Transaction" - simulation: engine name: "Engine tests" run_command: make run-hive-on-latest SIMULATION=ethereum/engine TEST_PATTERN="/Blob Transactions On Block 1, Cancun Genesis|Blob Transactions On Block 1, Shanghai Genesis|Blob Transaction Ordering, Single Account, Single Blob|Blob Transaction Ordering, Single Account, Dual Blob|Blob Transaction Ordering, Multiple Accounts|Replace Blob Transactions|Parallel Blob Transactions|ForkchoiceUpdatedV3 Modifies Payload ID on Different Beacon Root|NewPayloadV3 After Cancun|NewPayloadV3 Versioned Hashes|ForkchoiceUpdated Version on Payload Request" - simulation: engine-cancun name: "Cancun Engine tests" - run_command: make run-hive-on-latest SIMULATION=ethereum/engine TEST_PATTERN="cancun/Unique Payload ID|ParentHash equals BlockHash on NewPayload|Re-Execute Payload|Payload Build after New Invalid Payload|RPC|Build Payload with Invalid ChainID|Invalid PayloadAttributes, Zero timestamp, Syncing=False|Invalid PayloadAttributes, Parent timestamp, Syncing=False|Invalid PayloadAttributes, Missing BeaconRoot, Syncing=False|Suggested Fee Recipient Test|PrevRandao Opcode Transactions Test|Invalid Missing Ancestor ReOrg, StateRoot" + run_command: make run-hive-on-latest SIMULATION=ethereum/engine HIVE_EXTRA_ARGS="--sim.parallelism 4" TEST_PATTERN="cancun/Unique Payload ID|ParentHash equals BlockHash on NewPayload|Re-Execute Payload|Payload Build after New Invalid Payload|RPC|Build Payload with Invalid ChainID|Invalid PayloadAttributes, Zero timestamp, Syncing=False|Invalid PayloadAttributes, Parent timestamp, Syncing=False|Invalid PayloadAttributes, Missing BeaconRoot, Syncing=False|Suggested Fee Recipient Test|PrevRandao Opcode Transactions Test|Invalid Missing Ancestor ReOrg, StateRoot" steps: - name: Download artifacts uses: actions/download-artifact@v4 @@ -77,28 +72,3 @@ jobs: - name: Run Hive Simulation run: ${{ matrix.run_command }} - - run-assertoor: - name: Assertoor - Stability Check - runs-on: ubuntu-latest - needs: [build] - steps: - - uses: actions/checkout@v4 - - - name: Download artifacts - uses: actions/download-artifact@v4 - with: - name: ethrex_image - path: /tmp - - - name: Load image - run: | - docker load --input /tmp/ethrex_image.tar - - - name: Setup kurtosis testnet and run assertoor tests - uses: ethpandaops/kurtosis-assertoor-github-action@v1 - with: - kurtosis_version: '1.3.1' - ethereum_package_url: 'github.com/lambdaclass/ethereum-package' - ethereum_package_branch: 'ethrex-integration' - ethereum_package_args: './test_data/network_params.yaml' diff --git a/.github/workflows/hive_coverage.yaml b/.github/workflows/hive_coverage.yaml index ec0656565..70bc4badc 100644 --- a/.github/workflows/hive_coverage.yaml +++ b/.github/workflows/hive_coverage.yaml @@ -11,7 +11,7 @@ env: jobs: build: - uses: ./.github/workflows/docker-build.yaml + uses: ./.github/workflows/docker_build.yaml hive-coverage: name: Run engine hive simulator to gather coverage information. diff --git a/.github/workflows/lint-pr-title.yml b/.github/workflows/lint_pr_title.yml similarity index 100% rename from .github/workflows/lint-pr-title.yml rename to .github/workflows/lint_pr_title.yml diff --git a/Makefile b/Makefile index 706a9c7b0..38367ba89 100644 --- a/Makefile +++ b/Makefile @@ -98,7 +98,7 @@ run-hive: build-image setup-hive ## πŸ§ͺ Run Hive testing suite cd hive && ./hive --sim $(SIMULATION) --client ethrex --sim.limit "$(TEST_PATTERN)" run-hive-on-latest: setup-hive ## πŸ§ͺ Run Hive testing suite with the latest docker image - cd hive && ./hive --sim $(SIMULATION) --client ethrex --sim.limit "$(TEST_PATTERN)" + cd hive && ./hive --sim $(SIMULATION) --client ethrex --sim.limit "$(TEST_PATTERN)" $(HIVE_EXTRA_ARGS) run-hive-debug: build-image setup-hive ## 🐞 Run Hive testing suite in debug mode cd hive && ./hive --sim $(SIMULATION) --client ethrex --sim.limit "$(TEST_PATTERN)" --docker.output From 46cc5274c6cb6dd182de78ea23ac9cf639e828d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Fri, 22 Nov 2024 20:04:20 +0100 Subject: [PATCH 03/25] feat(l1): add workflow to push image to registry when pushing to main (#1239) Closes #1236 --- .github/workflows/docker_publish.yaml | 56 +++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/docker_publish.yaml diff --git a/.github/workflows/docker_publish.yaml b/.github/workflows/docker_publish.yaml new file mode 100644 index 000000000..43dbdd0a8 --- /dev/null +++ b/.github/workflows/docker_publish.yaml @@ -0,0 +1,56 @@ +name: Publish docker image to Github Packages + +on: + push: + branches: ['main'] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + + # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. + permissions: + contents: read + packages: write + attestations: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Generates the tags and labels based on the image name. The id allows using it in the next step. + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + # Pushes to ghcr.io/ethrex + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v4 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v1 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true + From 2fde54978292fc9a492d27da1ecfec58678b6467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Est=C3=A9fano=20Bargas?= Date: Fri, 22 Nov 2024 16:12:37 -0300 Subject: [PATCH 04/25] fix(l2): fix RPC port specified in .env.example (#1240) **Motivation** The port should be 8552. This may make the CI fail. --------- Co-authored-by: fborello-lambda --- crates/l2/.env.example | 2 +- crates/l2/docker-compose-l2.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/l2/.env.example b/crates/l2/.env.example index 865056bcb..01181ddd7 100644 --- a/crates/l2/.env.example +++ b/crates/l2/.env.example @@ -13,7 +13,7 @@ L1_WATCHER_TOPICS=0x6f65d68a35457dd88c1f8641be5da191aa122bc76de22ab0789dcc71929d L1_WATCHER_CHECK_INTERVAL_MS=1000 L1_WATCHER_MAX_BLOCK_STEP=5000 L1_WATCHER_L2_PROPOSER_PRIVATE_KEY=0x385c546456b6a603a1cfcaa9ec9494ba4832da08dd6bcf4de9a71e4a01b74924 -ENGINE_API_RPC_URL=http://localhost:8551 +ENGINE_API_RPC_URL=http://localhost:8552 ENGINE_API_JWT_PATH=./jwt.hex PROVER_SERVER_LISTEN_IP=127.0.0.1 PROVER_SERVER_LISTEN_PORT=3000 diff --git a/crates/l2/docker-compose-l2.yaml b/crates/l2/docker-compose-l2.yaml index a0871823c..1f48484ec 100644 --- a/crates/l2/docker-compose-l2.yaml +++ b/crates/l2/docker-compose-l2.yaml @@ -40,7 +40,7 @@ services: volumes: - ../../test_data/genesis-l2.json:/genesis-l2.json - .env:/.env:ro - command: --network /genesis-l2.json --http.addr 0.0.0.0 --http.port 1729 --authrpc.port 8551 + command: --network /genesis-l2.json --http.addr 0.0.0.0 --http.port 1729 --authrpc.port 8552 depends_on: contract_deployer: condition: service_completed_successfully From b9ec164a52248957d712195774bd39679552d2e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Est=C3=A9fano=20Bargas?= Date: Fri, 22 Nov 2024 19:10:40 -0300 Subject: [PATCH 05/25] feat(l2): initial state pruned trie in zkVM program (#1133) **Motivation** To validate the initial and final state values after the execution of a block in the prover, we introduce state and storage tries into the zkVM program to verify the initial values and then to apply account updates and compute the new state hash. **Description** - moved zkVM program input structure into interface and modified it into `ProgramInput` (because the zkvm program fails to build when importing the `prover` crate) - created `ProgramOutput` also - introduced `NullDB` database for tries (tries whose nodes are cached) - removed trie verification logic in favor of "pruned trie" logic (functions `from_nodes()`, `get_pruned_state()`, `build_tries()`, `update_tries()` and use them when creating the `ExecutionDB` and in the zkVM program) - added execution program documentation --- crates/l2/docs/README.md | 3 +- crates/l2/docs/program.md | 43 ++++ crates/l2/proposer/prover_server.rs | 8 +- crates/l2/prover/Makefile | 2 +- crates/l2/prover/src/prover.rs | 63 ++---- crates/l2/prover/src/prover_client.rs | 15 +- crates/l2/prover/tests/perf_zkvm.rs | 22 +- crates/l2/prover/zkvm/interface/Cargo.toml | 18 +- .../l2/prover/zkvm/interface/guest/Cargo.toml | 5 +- .../prover/zkvm/interface/guest/src/main.rs | 64 +++--- crates/l2/prover/zkvm/interface/src/lib.rs | 124 +++++++++++ crates/storage/trie/db.rs | 1 + crates/storage/trie/db/null.rs | 15 ++ crates/storage/trie/trie.rs | 120 +++++------ crates/vm/errors.rs | 20 +- crates/vm/execution_db.rs | 192 ++++++------------ crates/vm/vm.rs | 12 +- 17 files changed, 425 insertions(+), 302 deletions(-) create mode 100644 crates/l2/docs/program.md create mode 100644 crates/storage/trie/db/null.rs diff --git a/crates/l2/docs/README.md b/crates/l2/docs/README.md index d92f09df6..d39a88343 100644 --- a/crates/l2/docs/README.md +++ b/crates/l2/docs/README.md @@ -6,9 +6,10 @@ For a high level overview of the L2: For more detailed documentation on each part of the system: +- [Contracts](./contracts.md) +- [Execution program](./program.md) - [Proposer](./proposer.md) - [Prover](./prover.md) -- [Contracts](./contracts.md) ## Configuration diff --git a/crates/l2/docs/program.md b/crates/l2/docs/program.md new file mode 100644 index 000000000..35a92ba9f --- /dev/null +++ b/crates/l2/docs/program.md @@ -0,0 +1,43 @@ +# Prover's block execution program + +The zkVM block execution program will: +1. Take as input: + - the block to verify and its parent's header + - the L2 initial state, stored in a `ExecutionDB` struct, including the nodes for state and storage [pruned tries](#pruned-tries) +1. Build the initial state tries. This includes: + - verifying that the initial state values stored in the `ExecutionDB` are included in the tries. + - checking that the state trie root hash is the same as the one in the parent's header + - building the trie structures +1. Execute the block +1. Perform validations before and after execution +1. Apply account updates to the tries and compute the new state root +1. Check that the final state root is the same as the one stored in the block's header +1. Commit the program's output + +## Public and private inputs +The program interface defines a `ProgramInput` and `ProgramOutput` structures. + +`ProgramInput` contains: +- the block to verify and its parent's header +- an `ExecutionDB` which only holds the relevant initial state data for executing the block. This is built from pre-executing the block outside the zkVM to get the resulting account updates and retrieving the accounts and storage values touched by the execution. +- the `ExecutionDB` will also include all the (encoded) nodes necessary to build [pruned tries](#pruned-tries) for the stored accounts and storage values. + +`ProgramOutput` contains: +- the initial state hash +- the final state hash +these outputs will be committed as part of the proof. Both hashes are verified by the program, with the initial hash being checked at the time of building the initial tries (equivalent to verifying inclusion proofs) and the final hash by applying the account updates (that resulted from the block's execution) in the tries and recomputing the state root. + +## Pruned Tries +The EVM state is stored in Merkle Patricia Tries, which work differently than standard Merkle binary trees. In particular we have a *state trie* for each block, which contains all account states, and then for each account we have a *storage trie* that contains every storage value if the account in question corresponds to a deployed smart contract. + +We need a way to check the integrity of the account and storage values we pass as input to the block execution program. The "Merkle" in Merkle Patricia Tries means that we can cryptographically check inclusion of any value in a trie, and then use the trie's root to check the integrity of the whole data at once. + +Particularly, the root node points to its child nodes by storing their hashes, and these also contain the hashes of *their* child nodes, and so and so, until arriving at nodes that contain the values themselves. This means that the root contains the information of the whole trie (which can be compressed in a single word (32 byte value) by hashing the root), and by traversing down the trie we are checking nodes with more specific information until arriving to some value. + +So if we store only the necessary nodes that make up a path from the root into a particular value of interest (including the latter and the former), then: +- we know the root hash of this trie +- we know that this trie includes the value we're interested in +thereby **we're storing a proof of inclusion of the value in a trie with some root hash we can check*, which is equivalent to having a "pruned trie" that only contains the path of interest, but contains information of all other non included nodes and paths (subtries) thanks to nodes storing their childs hashes as mentioned earlier. This way we can verify the inclusion of values in some state, and thus the validity of the initial state values in the `ExecutionDB`, because we know the correct root hash. + +We can mutate this pruned trie by modifying/removing some value or inserting a new one, and then recalculate all the hashes from the node we inserted/modified up to the root, finally computing the new root hash. Because we know the correct final state root hash, this way we can make sure that the execution lead to the correct final state values. + diff --git a/crates/l2/proposer/prover_server.rs b/crates/l2/proposer/prover_server.rs index ffc2a18ed..7ab1c0253 100644 --- a/crates/l2/proposer/prover_server.rs +++ b/crates/l2/proposer/prover_server.rs @@ -25,9 +25,9 @@ use risc0_zkvm::sha::{Digest, Digestible}; #[derive(Debug, Serialize, Deserialize, Default)] pub struct ProverInputData { - pub db: ExecutionDB, pub block: Block, - pub parent_header: BlockHeader, + pub parent_block_header: BlockHeader, + pub db: ExecutionDB, } use crate::utils::{ @@ -419,7 +419,7 @@ impl ProverServer { let db = ExecutionDB::from_exec(&block, &self.store).map_err(|err| err.to_string())?; - let parent_header = self + let parent_block_header = self .store .get_block_header_by_hash(block.header.parent_hash) .map_err(|err| err.to_string())? @@ -430,7 +430,7 @@ impl ProverServer { Ok(ProverInputData { db, block, - parent_header, + parent_block_header, }) } diff --git a/crates/l2/prover/Makefile b/crates/l2/prover/Makefile index 468e14c7a..280d07e1f 100644 --- a/crates/l2/prover/Makefile +++ b/crates/l2/prover/Makefile @@ -1,5 +1,5 @@ RISC0_DEV_MODE?=1 -RUST_LOG?="debug" +RUST_LOG?="info" perf_test_proving: @echo "Using RISC0_DEV_MODE: ${RISC0_DEV_MODE}" RISC0_DEV_MODE=${RISC0_DEV_MODE} RUST_LOG=${RUST_LOG} cargo test --release --test perf_zkvm --features build_zkvm -- --show-output diff --git a/crates/l2/prover/src/prover.rs b/crates/l2/prover/src/prover.rs index 45328188d..24ff09a25 100644 --- a/crates/l2/prover/src/prover.rs +++ b/crates/l2/prover/src/prover.rs @@ -1,34 +1,19 @@ -use serde::Deserialize; use tracing::info; // risc0 -use zkvm_interface::methods::{ZKVM_PROGRAM_ELF, ZKVM_PROGRAM_ID}; - -use risc0_zkvm::{default_prover, ExecutorEnv, ExecutorEnvBuilder, ProverOpts}; - -use ethrex_core::types::Receipt; -use ethrex_l2::{ - proposer::prover_server::ProverInputData, utils::config::prover_client::ProverClientConfig, +use zkvm_interface::{ + io::{ProgramInput, ProgramOutput}, + methods::{ZKVM_PROGRAM_ELF, ZKVM_PROGRAM_ID}, }; -use ethrex_rlp::encode::RLPEncode; -use ethrex_vm::execution_db::ExecutionDB; -// The order of variables in this structure should match the order in which they were -// committed in the zkVM, with each variable represented by a field. -#[derive(Debug, Deserialize)] -pub struct ProverOutputData { - /// It is rlp encoded, it has to be decoded. - /// Block::decode(&prover_output_data.block).unwrap()); - pub _block: Vec, - pub _execution_db: ExecutionDB, - pub _parent_block_header: Vec, - pub block_receipts: Vec, -} +use risc0_zkvm::{default_prover, ExecutorEnv, ProverOpts}; + +use ethrex_l2::utils::config::prover_client::ProverClientConfig; pub struct Prover<'a> { - env_builder: ExecutorEnvBuilder<'a>, elf: &'a [u8], pub id: [u32; 8], + pub stdout: Vec, } impl<'a> Default for Prover<'a> { @@ -41,29 +26,20 @@ impl<'a> Default for Prover<'a> { impl<'a> Prover<'a> { pub fn new() -> Self { Self { - env_builder: ExecutorEnv::builder(), elf: ZKVM_PROGRAM_ELF, id: ZKVM_PROGRAM_ID, + stdout: Vec::new(), } } - pub fn set_input(&mut self, input: ProverInputData) -> &mut Self { - let head_block_rlp = input.block.encode_to_vec(); - let parent_header_rlp = input.parent_header.encode_to_vec(); - - // We should pass the inputs as a whole struct - self.env_builder.write(&head_block_rlp).unwrap(); - self.env_builder.write(&input.db).unwrap(); - self.env_builder.write(&parent_header_rlp).unwrap(); - - self - } - - /// Example: - /// let prover = Prover::new(); - /// let proof = prover.set_input(inputs).prove().unwrap(); - pub fn prove(&mut self) -> Result> { - let env = self.env_builder.build()?; + pub fn prove( + &mut self, + input: ProgramInput, + ) -> Result> { + let env = ExecutorEnv::builder() + .stdout(&mut self.stdout) + .write(&input)? + .build()?; // Generate the Receipt let prover = default_prover(); @@ -72,7 +48,7 @@ impl<'a> Prover<'a> { // This struct contains the receipt along with statistics about execution of the guest let prove_info = prover.prove_with_opts(env, self.elf, &ProverOpts::groth16())?; - // extract the receipt. + // Extract the receipt. let receipt = prove_info.receipt; info!("Successfully generated execution receipt."); @@ -87,8 +63,7 @@ impl<'a> Prover<'a> { pub fn get_commitment( receipt: &risc0_zkvm::Receipt, - ) -> Result> { - let commitment: ProverOutputData = receipt.journal.decode()?; - Ok(commitment) + ) -> Result> { + Ok(receipt.journal.decode()?) } } diff --git a/crates/l2/prover/src/prover_client.rs b/crates/l2/prover/src/prover_client.rs index 8c297109c..be1e0c6e7 100644 --- a/crates/l2/prover/src/prover_client.rs +++ b/crates/l2/prover/src/prover_client.rs @@ -7,9 +7,10 @@ use std::{ use tokio::time::sleep; use tracing::{debug, error, info, warn}; +use zkvm_interface::io::ProgramInput; + use ethrex_l2::{ - proposer::prover_server::{ProofData, ProverInputData}, - utils::config::prover_client::ProverClientConfig, + proposer::prover_server::ProofData, utils::config::prover_client::ProverClientConfig, }; use super::prover::Prover; @@ -38,7 +39,7 @@ impl ProverClient { loop { match self.request_new_input() { Ok((block_number, input)) => { - match prover.set_input(input).prove() { + match prover.prove(input) { Ok(proof) => { if let Err(e) = self.submit_proof(block_number, proof, prover.id.to_vec()) @@ -58,7 +59,7 @@ impl ProverClient { } } - fn request_new_input(&self) -> Result<(u64, ProverInputData), String> { + fn request_new_input(&self) -> Result<(u64, ProgramInput), String> { // Request the input with the correct block_number let request = ProofData::Request; let response = connect_to_prover_server_wr(&self.prover_server_endpoint, &request) @@ -71,7 +72,11 @@ impl ProverClient { } => match (block_number, input) { (Some(n), Some(i)) => { info!("Received Response for block_number: {n}"); - Ok((n, i)) + Ok((n, ProgramInput { + block: i.block, + parent_block_header: i.parent_block_header, + db: i.db + })) } _ => Err( "Received Empty Response, meaning that the ProverServer doesn't have blocks to prove.\nThe Prover may be advancing faster than the Proposer." diff --git a/crates/l2/prover/tests/perf_zkvm.rs b/crates/l2/prover/tests/perf_zkvm.rs index c4ac54cd1..558a1c0f8 100644 --- a/crates/l2/prover/tests/perf_zkvm.rs +++ b/crates/l2/prover/tests/perf_zkvm.rs @@ -2,10 +2,10 @@ use std::path::Path; use tracing::info; use ethrex_blockchain::add_block; -use ethrex_l2::proposer::prover_server::ProverInputData; use ethrex_prover_lib::prover::Prover; use ethrex_storage::{EngineType, Store}; use ethrex_vm::execution_db::ExecutionDB; +use zkvm_interface::io::ProgramInput; #[tokio::test] async fn test_performance_zkvm() { @@ -34,23 +34,22 @@ async fn test_performance_zkvm() { let db = ExecutionDB::from_exec(block_to_prove, &store).unwrap(); - let parent_header = store + let parent_block_header = store .get_block_header_by_hash(block_to_prove.header.parent_hash) .unwrap() .unwrap(); - let input = ProverInputData { - db, + let input = ProgramInput { block: block_to_prove.clone(), - parent_header, + parent_block_header, + db, }; let mut prover = Prover::new(); - prover.set_input(input); let start = std::time::Instant::now(); - let receipt = prover.prove().unwrap(); + let receipt = prover.prove(input).unwrap(); let duration = start.elapsed(); info!( @@ -62,12 +61,5 @@ async fn test_performance_zkvm() { prover.verify(&receipt).unwrap(); - let output = Prover::get_commitment(&receipt).unwrap(); - - let execution_cumulative_gas_used = output.block_receipts.last().unwrap().cumulative_gas_used; - info!("Cumulative Gas Used {execution_cumulative_gas_used}"); - - let gas_per_second = execution_cumulative_gas_used as f64 / duration.as_secs_f64(); - - info!("Gas per Second: {}", gas_per_second); + let _program_output = Prover::get_commitment(&receipt).unwrap(); } diff --git a/crates/l2/prover/zkvm/interface/Cargo.toml b/crates/l2/prover/zkvm/interface/Cargo.toml index 3e89bdf33..165de4996 100644 --- a/crates/l2/prover/zkvm/interface/Cargo.toml +++ b/crates/l2/prover/zkvm/interface/Cargo.toml @@ -4,17 +4,15 @@ version = "0.1.0" edition = "2021" [dependencies] -serde = { version = "1.0", default-features = false, features = ["derive"] } -thiserror = "1.0.64" +serde = { version = "1.0.203", features = ["derive"] } +serde_with = "3.11.0" +thiserror = "1.0.61" -ethrex-storage = { path = "../../../../storage/store" } - -# revm -revm = { version = "14.0.3", features = [ - "std", - "serde", - "kzg-rs", -], default-features = false } +ethrex-core = { path = "../../../../common/", default-features = false } +ethrex-vm = { path = "../../../../vm", default-features = false } +ethrex-rlp = { path = "../../../../common/rlp", default-features = false } +ethrex-storage = { path = "../../../../storage/store", default-features = false } +ethrex-trie = { path = "../../../../storage/trie", default-features = false } [build-dependencies] risc0-build = { version = "1.1.2" } diff --git a/crates/l2/prover/zkvm/interface/guest/Cargo.toml b/crates/l2/prover/zkvm/interface/guest/Cargo.toml index 19a0c440a..86d1c2c8f 100644 --- a/crates/l2/prover/zkvm/interface/guest/Cargo.toml +++ b/crates/l2/prover/zkvm/interface/guest/Cargo.toml @@ -7,10 +7,13 @@ edition = "2021" [dependencies] risc0-zkvm = { version = "1.1.2", default-features = false, features = ["std"] } +zkvm_interface = { path = "../" } ethrex-core = { path = "../../../../../common", default-features = false } ethrex-rlp = { path = "../../../../../common/rlp" } -ethrex-vm = { path = "../../../../../vm", default-features = false } +ethrex-vm = { path = "../../../../../vm", default-features = false, features = [ + "l2", +] } ethrex-blockchain = { path = "../../../../../blockchain", default-features = false } [build-dependencies] diff --git a/crates/l2/prover/zkvm/interface/guest/src/main.rs b/crates/l2/prover/zkvm/interface/guest/src/main.rs index 425729ff7..cad02e40e 100644 --- a/crates/l2/prover/zkvm/interface/guest/src/main.rs +++ b/crates/l2/prover/zkvm/interface/guest/src/main.rs @@ -1,49 +1,49 @@ -use ethrex_rlp::{decode::RLPDecode, encode::RLPEncode, error::RLPDecodeError}; use risc0_zkvm::guest::env; use ethrex_blockchain::{validate_block, validate_gas_used}; -use ethrex_core::types::{Block, BlockHeader}; -use ethrex_vm::{execute_block, execution_db::ExecutionDB, get_state_transitions, EvmState}; +use ethrex_vm::{execute_block, get_state_transitions, EvmState}; +use zkvm_interface::{ + io::{ProgramInput, ProgramOutput}, + trie::update_tries, +}; fn main() { - let (block, execution_db, parent_header) = read_inputs().expect("failed to read inputs"); - let mut state = EvmState::from(execution_db.clone()); + let ProgramInput { + block, + parent_block_header, + db, + } = env::read(); + let mut state = EvmState::from(db.clone()); // Validate the block pre-execution - validate_block(&block, &parent_header, &state).expect("invalid block"); + validate_block(&block, &parent_block_header, &state).expect("invalid block"); // Validate the initial state - if !execution_db - .verify_initial_state(parent_header.state_root) - .expect("failed to verify initial state") - { - panic!("initial state is not valid"); - }; + let (mut state_trie, mut storage_tries) = db + .build_tries() + .expect("failed to build state and storage tries or state is not valid"); - let receipts = execute_block(&block, &mut state).unwrap(); - - env::commit(&receipts); + let initial_state_hash = state_trie.hash_no_commit(); + if initial_state_hash != parent_block_header.state_root { + panic!("invalid initial state trie"); + } + let receipts = execute_block(&block, &mut state).expect("failed to execute block"); validate_gas_used(&receipts, &block.header).expect("invalid gas used"); - let _account_updates = get_state_transitions(&mut state); - - // TODO: compute new state root from account updates and check it matches with the block's - // header one. -} - -fn read_inputs() -> Result<(Block, ExecutionDB, BlockHeader), RLPDecodeError> { - let head_block_bytes = env::read::>(); - let execution_db = env::read::(); - let parent_header_bytes = env::read::>(); + let account_updates = get_state_transitions(&mut state); - let block = Block::decode(&head_block_bytes)?; - let parent_header = BlockHeader::decode(&parent_header_bytes)?; + // Update tries and calculate final state root hash + update_tries(&mut state_trie, &mut storage_tries, &account_updates) + .expect("failed to update state and storage tries"); + let final_state_hash = state_trie.hash_no_commit(); - // make inputs public - env::commit(&block.encode_to_vec()); - env::commit(&execution_db); - env::commit(&parent_header.encode_to_vec()); + if final_state_hash != block.header.state_root { + panic!("invalid final state trie"); + } - Ok((block, execution_db, parent_header)) + env::commit(&ProgramOutput { + initial_state_hash, + final_state_hash, + }); } diff --git a/crates/l2/prover/zkvm/interface/src/lib.rs b/crates/l2/prover/zkvm/interface/src/lib.rs index ddec54513..9e5b0a5ff 100644 --- a/crates/l2/prover/zkvm/interface/src/lib.rs +++ b/crates/l2/prover/zkvm/interface/src/lib.rs @@ -7,3 +7,127 @@ pub mod methods { #[cfg(all(not(clippy), feature = "build_zkvm"))] include!(concat!(env!("OUT_DIR"), "/methods.rs")); } + +pub mod io { + use ethrex_core::{ + types::{Block, BlockHeader}, + H256, + }; + use ethrex_rlp::{decode::RLPDecode, encode::RLPEncode}; + use ethrex_vm::execution_db::ExecutionDB; + use serde::{Deserialize, Serialize}; + use serde_with::{serde_as, DeserializeAs, SerializeAs}; + + /// Private input variables passed into the zkVM execution program. + #[serde_as] + #[derive(Serialize, Deserialize)] + pub struct ProgramInput { + /// block to execute + #[serde_as(as = "RLPBlock")] + pub block: Block, + /// header of the previous block + pub parent_block_header: BlockHeader, + /// database containing only the data necessary to execute + pub db: ExecutionDB, + } + + /// Public output variables exposed by the zkVM execution program. Some of these are part of + /// the program input. + #[derive(Serialize, Deserialize)] + pub struct ProgramOutput { + /// initial state trie root hash + pub initial_state_hash: H256, + /// final state trie root hash + pub final_state_hash: H256, + } + + /// Used with [serde_with] to encode a Block into RLP before serializing its bytes. This is + /// necessary because the [ethrex_core::types::Transaction] type doesn't serializes into any + /// format other than JSON. + pub struct RLPBlock; + + impl SerializeAs for RLPBlock { + fn serialize_as(val: &Block, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut encoded = Vec::new(); + val.encode(&mut encoded); + serde_with::Bytes::serialize_as(&encoded, serializer) + } + } + + impl<'de> DeserializeAs<'de, Block> for RLPBlock { + fn deserialize_as(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let encoded: Vec = serde_with::Bytes::deserialize_as(deserializer)?; + Block::decode(&encoded).map_err(serde::de::Error::custom) + } + } +} + +pub mod trie { + use std::collections::HashMap; + + use ethrex_core::{types::AccountState, H160}; + use ethrex_rlp::{decode::RLPDecode, encode::RLPEncode, error::RLPDecodeError}; + use ethrex_storage::{hash_address, hash_key, AccountUpdate}; + use ethrex_trie::{Trie, TrieError}; + use thiserror::Error; + + #[derive(Debug, Error)] + pub enum Error { + #[error(transparent)] + TrieError(#[from] TrieError), + #[error(transparent)] + RLPDecode(#[from] RLPDecodeError), + #[error("Missing storage trie for address {0}")] + StorageNotFound(H160), + } + + pub fn update_tries( + state_trie: &mut Trie, + storage_tries: &mut HashMap, + account_updates: &[AccountUpdate], + ) -> Result<(), Error> { + for update in account_updates.iter() { + let hashed_address = hash_address(&update.address); + if update.removed { + // Remove account from trie + state_trie.remove(hashed_address)?; + } else { + // Add or update AccountState in the trie + // Fetch current state or create a new state to be inserted + let mut account_state = match state_trie.get(&hashed_address)? { + Some(encoded_state) => AccountState::decode(&encoded_state)?, + None => AccountState::default(), + }; + if let Some(info) = &update.info { + account_state.nonce = info.nonce; + account_state.balance = info.balance; + account_state.code_hash = info.code_hash; + } + // Store the added storage in the account's storage trie and compute its new root + if !update.added_storage.is_empty() { + let storage_trie = storage_tries + .get_mut(&update.address) + .ok_or(Error::StorageNotFound(update.address))?; + for (storage_key, storage_value) in &update.added_storage { + let hashed_key = hash_key(storage_key); + if storage_value.is_zero() { + storage_trie.remove(hashed_key)?; + } else { + storage_trie.insert(hashed_key, storage_value.encode_to_vec())?; + } + } + account_state.storage_root = storage_trie.hash_no_commit(); + } + state_trie.insert(hashed_address, account_state.encode_to_vec())?; + println!("inserted new state"); + } + } + Ok(()) + } +} diff --git a/crates/storage/trie/db.rs b/crates/storage/trie/db.rs index e2f5249de..c124ff848 100644 --- a/crates/storage/trie/db.rs +++ b/crates/storage/trie/db.rs @@ -3,6 +3,7 @@ pub mod in_memory; pub mod libmdbx; #[cfg(feature = "libmdbx")] pub mod libmdbx_dupsort; +pub mod null; use crate::error::TrieError; diff --git a/crates/storage/trie/db/null.rs b/crates/storage/trie/db/null.rs new file mode 100644 index 000000000..69df1a52d --- /dev/null +++ b/crates/storage/trie/db/null.rs @@ -0,0 +1,15 @@ +use super::TrieDB; +use crate::error::TrieError; + +/// Used for small/pruned tries that don't have a database and just cache their nodes. +pub struct NullTrieDB; + +impl TrieDB for NullTrieDB { + fn get(&self, _key: Vec) -> Result>, TrieError> { + Ok(None) + } + + fn put(&self, _key: Vec, _value: Vec) -> Result<(), TrieError> { + Ok(()) + } +} diff --git a/crates/storage/trie/trie.rs b/crates/storage/trie/trie.rs index 389bb5e7a..af1e610e8 100644 --- a/crates/storage/trie/trie.rs +++ b/crates/storage/trie/trie.rs @@ -8,6 +8,9 @@ mod state; #[cfg(test)] mod test_utils; +use std::collections::HashSet; + +use db::null::NullTrieDB; mod trie_iter; mod verify_range; use ethereum_types::H256; @@ -40,8 +43,10 @@ lazy_static! { /// RLP-encoded trie path pub type PathRLP = Vec; -// RLP-encoded trie value +/// RLP-encoded trie value pub type ValueRLP = Vec; +/// RLP-encoded trie node +pub type NodeRLP = Vec; /// Libmdx-based Ethereum Compatible Merkle Patricia Trie pub struct Trie { @@ -136,10 +141,19 @@ impl Trie { .unwrap_or(*EMPTY_TRIE_HASH)) } + /// Return the hash of the trie's root node. + /// Returns keccak(RLP_NULL) if the trie is empty + pub fn hash_no_commit(&self) -> H256 { + self.root + .as_ref() + .map(|root| root.clone().finalize()) + .unwrap_or(*EMPTY_TRIE_HASH) + } + /// Obtain a merkle proof for the given path. /// The proof will contain all the encoded nodes traversed until reaching the node where the path is stored (including this last node). /// The proof will still be constructed even if the path is not stored in the trie, proving its absence. - pub fn get_proof(&self, path: &PathRLP) -> Result>, TrieError> { + pub fn get_proof(&self, path: &PathRLP) -> Result, TrieError> { // Will store all the encoded nodes traversed until reaching the node containing the path let mut node_path = Vec::new(); let Some(root) = &self.root else { @@ -155,48 +169,56 @@ impl Trie { Ok(node_path) } - pub fn verify_proof( - _proof: &[Vec], - _root_hash: NodeHash, - _path: &PathRLP, - _value: &ValueRLP, - ) -> Result { - // This is a mockup function for verifying proof of inclusions. This function will be - // possible to implement after refactoring the current Trie implementation. + /// Obtains all encoded nodes traversed until reaching the node where every path is stored. + /// The list doesn't include the root node, this is returned separately. + /// Will still be constructed even if some path is not stored in the trie. + pub fn get_proofs( + &self, + paths: &[PathRLP], + ) -> Result<(Option, Vec), TrieError> { + let Some(root_node) = self + .root + .as_ref() + .map(|root| self.state.get_node(root.clone())) + .transpose()? + .flatten() + else { + return Ok((None, Vec::new())); + }; - // We'll build a trie from the proof nodes and check whether: - // 1. the trie root hash is the one we expect - // 2. the trie contains the (key, value) pair to verify + let mut node_path = Vec::new(); + for path in paths { + let mut nodes = self.get_proof(path)?; + nodes.swap_remove(0); + node_path.extend(nodes); // skip root node + } - // We will only be using the trie's cache so we don't need a working DB + // dedup + // TODO: really inefficient, by making the traversing smarter we can avoid having + // duplicates + let node_path: HashSet<_> = node_path.drain(..).collect(); + let node_path = Vec::from_iter(node_path); + Ok((Some(root_node.encode_raw()), node_path)) + } + + /// Creates a cached Trie (with [NullTrieDB]) from a list of encoded nodes. + /// Generally used in conjuction with [Trie::get_proofs]. + pub fn from_nodes( + root_node: Option<&NodeRLP>, + other_nodes: &[NodeRLP], + ) -> Result { + let mut trie = Trie::new(Box::new(NullTrieDB)); + + if let Some(root_node) = root_node { + let root_node = Node::decode_raw(root_node)?; + trie.root = Some(root_node.insert_self(&mut trie.state)?); + } + + for node in other_nodes.iter().map(|node| Node::decode_raw(node)) { + node?.insert_self(&mut trie.state)?; + } - // let mut trie = Trie::stateless(); - - // Insert root into trie - // let mut proof = proof.into_iter(); - // let root_node = proof.next(); - // trie.root = Some(root_node.insert_self(path_offset, &mut trie.state)?); - - // Insert rest of nodes - // for node in proof { - // node.insert_self(path_offset, &mut trie.state)?; - // } - // let expected_root_hash = trie.hash_no_commit()?.into(); - - // Check key exists - // let Some(retrieved_value) = trie.get(path)? else { - // return Ok(false); - // }; - // // Check value is correct - // if retrieved_value != *value { - // return Ok(false); - // } - // // Check root hash - // if root_hash != expected_root_hash { - // return Ok(false); - // } - - Ok(true) + Ok(trie) } /// Builds an in-memory trie from the given elements and returns its hash @@ -1108,20 +1130,4 @@ mod test { let trie_proof = trie.get_proof(&a).unwrap(); assert_eq!(cita_proof, trie_proof); } - - #[test] - fn verify_proof_one_leaf() { - let mut trie = Trie::new_temp(); - trie.insert(b"duck".to_vec(), b"duckling".to_vec()).unwrap(); - - let root_hash = trie.hash().unwrap().into(); - let trie_proof = trie.get_proof(&b"duck".to_vec()).unwrap(); - assert!(Trie::verify_proof( - &trie_proof, - root_hash, - &b"duck".to_vec(), - &b"duckling".to_vec(), - ) - .unwrap()); - } } diff --git a/crates/vm/errors.rs b/crates/vm/errors.rs index a2635b5f6..4fbd723ed 100644 --- a/crates/vm/errors.rs +++ b/crates/vm/errors.rs @@ -1,4 +1,4 @@ -use ethereum_types::H160; +use ethereum_types::{H160, H256}; use ethrex_core::types::BlockHash; use ethrex_storage::error::StoreError; use ethrex_trie::TrieError; @@ -37,8 +37,10 @@ pub enum ExecutionDBError { AccountNotFound(RevmAddress), #[error("Code by hash {0} not found")] CodeNotFound(RevmB256), - #[error("Storage value for address {0} and slot {1} not found")] - StorageNotFound(RevmAddress, RevmU256), + #[error("Storage for address {0} not found")] + StorageNotFound(RevmAddress), + #[error("Storage value for address {0} and key {1} not found")] + StorageValueNotFound(RevmAddress, RevmU256), #[error("Hash of block with number {0} not found")] BlockHashNotFound(u64), #[error("Missing account {0} info while trying to create ExecutionDB")] @@ -48,7 +50,17 @@ pub enum ExecutionDBError { #[error( "Missing storage trie of block {0} and address {1} while trying to create ExecutionDB" )] - NewMissingStorageTrie(BlockHash, RevmAddress), + NewMissingStorageTrie(BlockHash, H160), + #[error("The account {0} is not included in the stored pruned state trie")] + MissingAccountInStateTrie(H160), + #[error("Missing storage trie of account {0}")] + MissingStorageTrie(H160), + #[error("Storage trie root for account {0} does not match account storage root")] + InvalidStorageTrieRoot(H160), + #[error("The pruned storage trie of account {0} is missing the storage key {1}")] + MissingKeyInStorageTrie(H160, H256), + #[error("Storage trie value for account {0} and key {1} does not match value stored in db")] + InvalidStorageTrieValue(H160, H256), #[error("{0}")] Custom(String), } diff --git a/crates/vm/execution_db.rs b/crates/vm/execution_db.rs index edf65021c..a7a81040d 100644 --- a/crates/vm/execution_db.rs +++ b/crates/vm/execution_db.rs @@ -1,13 +1,13 @@ use std::collections::HashMap; -use ethereum_types::{Address, H160, U256}; +use ethereum_types::H160; use ethrex_core::{ types::{AccountState, Block, ChainConfig}, H256, }; use ethrex_rlp::encode::RLPEncode; use ethrex_storage::{hash_address, hash_key, Store}; -use ethrex_trie::Trie; +use ethrex_trie::{NodeRLP, Trie}; use revm::{ primitives::{ AccountInfo as RevmAccountInfo, Address as RevmAddress, Bytecode as RevmBytecode, @@ -17,10 +17,7 @@ use revm::{ }; use serde::{Deserialize, Serialize}; -use crate::{ - errors::{ExecutionDBError, StateProofsError}, - evm_state, execute_block, get_state_transitions, -}; +use crate::{errors::ExecutionDBError, evm_state, execute_block, get_state_transitions}; /// In-memory EVM database for caching execution data. /// @@ -38,17 +35,12 @@ pub struct ExecutionDB { pub block_hashes: HashMap, /// stored chain config pub chain_config: ChainConfig, - /// proofs of inclusion of account and storage values of the initial state - pub initial_proofs: StateProofs, -} - -/// Merkle proofs of inclusion of state values. -/// -/// Contains Merkle proofs to verfy the inclusion of values in the state and storage tries. -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct StateProofs { - account: HashMap>>, - storage: HashMap>>>, + /// encoded nodes to reconstruct a state trie, but only including relevant data (pruned). + /// root node is stored separately from the rest. + pub pruned_state_trie: (Option, Vec), + /// encoded nodes to reconstruct every storage trie, but only including relevant data (pruned) + /// root nodes are stored separately from the rest. + pub pruned_storage_tries: HashMap, Vec)>, } impl ExecutionDB { @@ -104,33 +96,31 @@ impl ExecutionDB { ); } - // Compute Merkle proofs for the initial state values - let initial_state_trie = store.state_trie(block.header.parent_hash)?.ok_or( + // Get pruned state and storage tries. For this we get the "state" (all relevant nodes) of every trie. + // "Pruned" because we're only getting the nodes that make paths to the relevant + // key-values. + let state_trie = store.state_trie(block.header.parent_hash)?.ok_or( ExecutionDBError::NewMissingStateTrie(block.header.parent_hash), )?; - let initial_storage_tries = accounts - .keys() - .map(|address| { - Ok(( - H160::from_slice(address.as_slice()), - store - .storage_trie( - block.header.parent_hash, - H160::from_slice(address.as_slice()), - )? - .ok_or(ExecutionDBError::NewMissingStorageTrie( - block.header.parent_hash, - *address, - ))?, - )) - }) - .collect::, ExecutionDBError>>()?; - let initial_proofs = StateProofs::new( - &initial_state_trie, - &initial_storage_tries, - &address_storage_keys, - )?; + // Get pruned state trie + let state_paths: Vec<_> = address_storage_keys.keys().map(hash_address).collect(); + let pruned_state_trie = state_trie.get_proofs(&state_paths)?; + + // Get pruned storage tries for every account + let mut pruned_storage_tries = HashMap::new(); + for (address, keys) in address_storage_keys { + let storage_trie = store + .storage_trie(block.header.parent_hash, address)? + .ok_or(ExecutionDBError::NewMissingStorageTrie( + block.header.parent_hash, + address, + ))?; + let storage_paths: Vec<_> = keys.iter().map(hash_key).collect(); + let (storage_trie_root, storage_trie_nodes) = + storage_trie.get_proofs(&storage_paths)?; + pruned_storage_tries.insert(address, (storage_trie_root, storage_trie_nodes)); + } Ok(Self { accounts, @@ -138,7 +128,8 @@ impl ExecutionDB { storage, block_hashes, chain_config, - initial_proofs, + pruned_state_trie, + pruned_storage_tries, }) } @@ -146,101 +137,52 @@ impl ExecutionDB { self.chain_config } - /// Verifies that [self] holds the initial state (prior to block execution) with some root - /// hash. - pub fn verify_initial_state(&self, state_root: H256) -> Result { - self.verify_state_proofs(state_root, &self.initial_proofs) - } - - fn verify_state_proofs( - &self, - state_root: H256, - proofs: &StateProofs, - ) -> Result { - proofs.verify(state_root, &self.accounts, &self.storage) - } -} - -impl StateProofs { - fn new( - state_trie: &Trie, - storage_tries: &HashMap, - address_storage_keys: &HashMap>, - ) -> Result { - let mut account = HashMap::default(); - let mut storage = HashMap::default(); + /// Verifies that all data in [self] is included in the stored tries, and then builds the + /// pruned tries from the stored nodes. + pub fn build_tries(&self) -> Result<(Trie, HashMap), ExecutionDBError> { + let (state_trie_root, state_trie_nodes) = &self.pruned_state_trie; + let state_trie = Trie::from_nodes(state_trie_root.as_ref(), state_trie_nodes)?; + let mut storage_tries = HashMap::new(); - for (address, storage_keys) in address_storage_keys { - let storage_trie = storage_tries - .get(address) - .ok_or(StateProofsError::StorageTrieNotFound(*address))?; + for (revm_address, account) in &self.accounts { + let address = H160::from_slice(revm_address.as_slice()); - let proof = state_trie.get_proof(&hash_address(address))?; - let address = RevmAddress::from_slice(address.as_bytes()); - account.insert(address, proof); - - let mut storage_proofs = HashMap::new(); - for key in storage_keys { - let proof = storage_trie.get_proof(&hash_key(key))?; - let key = RevmU256::from_be_bytes(key.to_fixed_bytes()); - storage_proofs.insert(key, proof); + // check account is in state trie + if state_trie.get(&hash_address(&address))?.is_none() { + return Err(ExecutionDBError::MissingAccountInStateTrie(address)); } - storage.insert(address, storage_proofs); - } - - Ok(Self { account, storage }) - } - - fn verify( - &self, - state_root: H256, - accounts: &HashMap, - storages: &HashMap>, - ) -> Result { - // Check accounts inclusion in the state trie - for (address, account) in accounts { - let proof = self - .account - .get(address) - .ok_or(StateProofsError::AccountProofNotFound(*address))?; - let hashed_address = hash_address(&H160::from_slice(address.as_slice())); - let mut encoded_account = Vec::new(); - account.encode(&mut encoded_account); + let (storage_trie_root, storage_trie_nodes) = + self.pruned_storage_tries + .get(&address) + .ok_or(ExecutionDBError::MissingStorageTrie(address))?; - if !Trie::verify_proof(proof, state_root.into(), &hashed_address, &encoded_account)? { - return Ok(false); + // compare account storage root with storage trie root + let storage_trie = Trie::from_nodes(storage_trie_root.as_ref(), storage_trie_nodes)?; + if storage_trie.hash_no_commit() != account.storage_root { + return Err(ExecutionDBError::InvalidStorageTrieRoot(address)); } - } - // so all account storage roots are valid at this point - // Check storage values inclusion in storage tries - for (address, storage) in storages { - let storage_root = accounts - .get(address) - .map(|account| account.storage_root) - .ok_or(StateProofsError::StorageNotFound(*address))?; - - let storage_proofs = self + // check all storage keys are in storage trie and compare values + let storage = self .storage - .get(address) - .ok_or(StateProofsError::StorageProofsNotFound(*address))?; - + .get(revm_address) + .ok_or(ExecutionDBError::StorageNotFound(*revm_address))?; for (key, value) in storage { - let proof = storage_proofs - .get(key) - .ok_or(StateProofsError::StorageProofNotFound(*address, *key))?; - - let hashed_key = hash_key(&H256::from_slice(&key.to_be_bytes_vec())); - let encoded_value = U256::from_big_endian(&value.to_be_bytes_vec()).encode_to_vec(); - - if !Trie::verify_proof(proof, storage_root.into(), &hashed_key, &encoded_value)? { - return Ok(false); + let key = H256::from_slice(&key.to_be_bytes_vec()); + let value = H256::from_slice(&value.to_be_bytes_vec()); + let retrieved_value = storage_trie + .get(&hash_key(&key))? + .ok_or(ExecutionDBError::MissingKeyInStorageTrie(address, key))?; + if value.encode_to_vec() != retrieved_value { + return Err(ExecutionDBError::InvalidStorageTrieValue(address, key)); } } + + storage_tries.insert(address, storage_trie); } - Ok(true) + Ok((state_trie, storage_tries)) } } @@ -281,7 +223,7 @@ impl DatabaseRef for ExecutionDB { .ok_or(ExecutionDBError::AccountNotFound(address))? .get(&index) .cloned() - .ok_or(ExecutionDBError::StorageNotFound(address, index)) + .ok_or(ExecutionDBError::StorageValueNotFound(address, index)) } /// Get block hash by block number. diff --git a/crates/vm/vm.rs b/crates/vm/vm.rs index 89d9e24fe..f17af79ce 100644 --- a/crates/vm/vm.rs +++ b/crates/vm/vm.rs @@ -94,9 +94,10 @@ cfg_if::cfg_if! { state: &mut EvmState, ) -> Result<(Vec, Vec), EvmError> { let block_header = &block.header; - let spec_id = spec_id(&state.chain_config()?, block_header.timestamp); //eip 4788: execute beacon_root_contract_call before block transactions + #[cfg(not(feature = "l2"))] if block_header.parent_beacon_block_root.is_some() && spec_id == SpecId::CANCUN { + let spec_id = spec_id(&state.chain_config()?, block_header.timestamp); beacon_root_contract_call(state, block_header, spec_id)?; } let mut receipts = Vec::new(); @@ -198,8 +199,13 @@ cfg_if::cfg_if! { let block_header = &block.header; let spec_id = spec_id(&state.chain_config()?, block_header.timestamp); //eip 4788: execute beacon_root_contract_call before block transactions - if block_header.parent_beacon_block_root.is_some() && spec_id == SpecId::CANCUN { - beacon_root_contract_call(state, block_header, spec_id)?; + cfg_if::cfg_if! { + if #[cfg(not(feature = "l2"))] { + //eip 4788: execute beacon_root_contract_call before block transactions + if block_header.parent_beacon_block_root.is_some() && spec_id == SpecId::CANCUN { + beacon_root_contract_call(state, block_header, spec_id)?; + } + } } let mut receipts = Vec::new(); let mut cumulative_gas_used = 0; From a49590bb60b1a5c50df4782c6b6ff3f4b4697935 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Fri, 22 Nov 2024 22:28:54 -0300 Subject: [PATCH 06/25] fix(l1): fix engine api v3 hive test related to latest valid hash and errors on ForkchoiceState (#1233) **Motivation** Fix the `engine-cancun` suite tests that we are able to fix with our current implementation (without v1, v2 or reorg/syncing working as expected) **Description** After a couples of back and forths the actual solution for both errors are pretty simple, in the case of the latest valid hash it was needed to send the parent when the evm fails after the parent was checked. In the case of the ForkchoiceState we were sending an incorrect code we needed to use the RpcError for FokchoiceState that we already had implemented. This pass **31** new tests. On regards the Engine Cancun step in the CI, previously it ran 38 tests, now it runs 149. **Summary of the outstanding failing test in engine-cancun** - 1 test (Blob Transaction Ordering) expect 6 blobs but got 5 - 2 test (Invalid NewPayload, ReceiptsRoot) Expected INVALID but got VALID - 4 test (Sidechain Reorg|TransactionRe-Org|Re-Org back into canonical Chain) fails on Reorg or latest valid hash - 15 tests ({exec_method}V[3|2]) are related to Shanghai (V2) compatibility (v2 method not found) - 13 tests(Fork ID: Genesis|Pre-Merge) are also related to previous versions, (v2|1 method not found) - 2 test are related to pooled blobs and depend on DevP2PRequestPooled - 24 tests (Invalid Missing Ancestors) are related to syncing and timeout Resolves #1235 --- .github/workflows/hive.yaml | 6 +++--- crates/networking/rpc/engine/fork_choice.rs | 4 +--- crates/networking/rpc/engine/payload.rs | 6 +++++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/hive.yaml b/.github/workflows/hive.yaml index 5e8cce178..9ef3a66fe 100644 --- a/.github/workflows/hive.yaml +++ b/.github/workflows/hive.yaml @@ -43,11 +43,11 @@ jobs: name: "Devp2p eth tests" run_command: make run-hive-on-latest SIMULATION=devp2p TEST_PATTERN="eth/Status|GetBlockHeaders|SimultaneousRequests|SameRequestID|ZeroRequestID|GetBlockBodies|MaliciousHandshake|MaliciousStatus|Transaction" - simulation: engine - name: "Engine tests" - run_command: make run-hive-on-latest SIMULATION=ethereum/engine TEST_PATTERN="/Blob Transactions On Block 1, Cancun Genesis|Blob Transactions On Block 1, Shanghai Genesis|Blob Transaction Ordering, Single Account, Single Blob|Blob Transaction Ordering, Single Account, Dual Blob|Blob Transaction Ordering, Multiple Accounts|Replace Blob Transactions|Parallel Blob Transactions|ForkchoiceUpdatedV3 Modifies Payload ID on Different Beacon Root|NewPayloadV3 After Cancun|NewPayloadV3 Versioned Hashes|ForkchoiceUpdated Version on Payload Request" + name: "Engine Auth and EC tests" + run_command: make run-hive-on-latest SIMULATION=ethereum/engine TEST_PATTERN="engine-(auth|exchange-capabilities)/" - simulation: engine-cancun name: "Cancun Engine tests" - run_command: make run-hive-on-latest SIMULATION=ethereum/engine HIVE_EXTRA_ARGS="--sim.parallelism 4" TEST_PATTERN="cancun/Unique Payload ID|ParentHash equals BlockHash on NewPayload|Re-Execute Payload|Payload Build after New Invalid Payload|RPC|Build Payload with Invalid ChainID|Invalid PayloadAttributes, Zero timestamp, Syncing=False|Invalid PayloadAttributes, Parent timestamp, Syncing=False|Invalid PayloadAttributes, Missing BeaconRoot, Syncing=False|Suggested Fee Recipient Test|PrevRandao Opcode Transactions Test|Invalid Missing Ancestor ReOrg, StateRoot" + run_command: make run-hive-on-latest SIMULATION=ethereum/engine HIVE_EXTRA_ARGS="--sim.parallelism 4" TEST_PATTERN="engine-cancun/Blob Transactions On Block 1|Blob Transaction Ordering, Single|Blob Transaction Ordering, Multiple Accounts|Replace Blob Transactions|Parallel Blob Transactions|ForkchoiceUpdatedV3 Modifies Payload ID on Different Beacon Root|NewPayloadV3 After Cancun|NewPayloadV3 Versioned Hashes|Incorrect BlobGasUsed|Bad Hash|ParentHash equals BlockHash|RPC:|in ForkchoiceState|Unknown|Invalid PayloadAttributes|Unique|ForkchoiceUpdated Version on Payload Request|Re-Execute Payload|In-Order Consecutive Payload|Multiple New Payloads|Valid NewPayload->|NewPayload with|Payload Build after|Build Payload with|Invalid Missing Ancestor ReOrg, StateRoot|Re-Org Back to|Re-org to Previously|Safe Re-Org to Side Chain|Transaction Re-Org, Re-Org Back In|Re-Org Back into Canonical Chain, Depth=5|Suggested Fee Recipient Test|PrevRandao Opcode|Invalid NewPayload, [^R][^e]|Fork ID Genesis=0, Cancun=0|Fork ID Genesis=0, Cancun=1|Fork ID Genesis=1, Cancun=0|Fork ID Genesis=1, Cancun=2, Shanghai=2" steps: - name: Download artifacts uses: actions/download-artifact@v4 diff --git a/crates/networking/rpc/engine/fork_choice.rs b/crates/networking/rpc/engine/fork_choice.rs index a6bd6c1e1..d6834ba09 100644 --- a/crates/networking/rpc/engine/fork_choice.rs +++ b/crates/networking/rpc/engine/fork_choice.rs @@ -73,9 +73,7 @@ impl RpcHandler for ForkChoiceUpdatedV3 { InvalidForkChoice::Syncing => ForkChoiceResponse::from(PayloadStatus::syncing()), reason => { warn!("Invalid fork choice state. Reason: {:#?}", reason); - ForkChoiceResponse::from(PayloadStatus::invalid_with_err( - reason.to_string().as_str(), - )) + return Err(RpcErr::InvalidForkChoiceState(reason.to_string())); } }; diff --git a/crates/networking/rpc/engine/payload.rs b/crates/networking/rpc/engine/payload.rs index f0d351e9d..9d7eb0089 100644 --- a/crates/networking/rpc/engine/payload.rs +++ b/crates/networking/rpc/engine/payload.rs @@ -58,6 +58,7 @@ impl RpcHandler for NewPayloadV3Request { fn handle(&self, context: RpcApiContext) -> Result { let storage = &context.storage; + let block_hash = self.payload.block_hash; info!("Received new payload with block hash: {block_hash:#x}"); @@ -136,7 +137,10 @@ impl RpcHandler for NewPayloadV3Request { } Err(ChainError::EvmError(error)) => { warn!("Error executing block: {error}"); - Ok(PayloadStatus::invalid_with_err(&error.to_string())) + Ok(PayloadStatus::invalid_with( + block.header.parent_hash, + error.to_string(), + )) } Err(ChainError::StoreError(error)) => { warn!("Error storing block: {error}"); From 08f817d32733b62f91a3baa5174c4f0d3d96fe1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Rodr=C3=ADguez=20Chatruc?= <49622509+jrchatruc@users.noreply.github.com> Date: Mon, 25 Nov 2024 08:21:08 -0300 Subject: [PATCH 07/25] chore(core): separate L2 Integration test CI from the L1 CI (#1242) **Motivation** **Description** --- .github/workflows/ci.yaml | 6 ------ .github/workflows/ci_l2.yaml | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/ci_l2.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 33087270f..25650c1d2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -68,12 +68,6 @@ jobs: run: | make download-test-vectors - - name: Run L2 integration test - run: | - cd crates/l2 - cp .env.example .env - make test - - name: Run tests run: | make test diff --git a/.github/workflows/ci_l2.yaml b/.github/workflows/ci_l2.yaml new file mode 100644 index 000000000..1f97bafc0 --- /dev/null +++ b/.github/workflows/ci_l2.yaml @@ -0,0 +1,40 @@ +name: CI L2 +on: + merge_group: + pull_request: + branches: ["**"] + paths-ignore: + - "README.md" + - "LICENSE" + - "**/README.md" + - "**/docs/**" + - "crates/vm/levm/**" # We ran this in a separate workflow + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +env: + RUST_VERSION: 1.80.1 + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Rustup toolchain install + uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ env.RUST_VERSION }} + + - name: Caching + uses: Swatinem/rust-cache@v2 + + - name: Run L2 integration test + run: | + cd crates/l2 + cp .env.example .env + make test From 2e42cdfc82ca8ea9bfab74c6debe1d7a8c045b61 Mon Sep 17 00:00:00 2001 From: Ivan Litteri <67517699+ilitteri@users.noreply.github.com> Date: Mon, 25 Nov 2024 11:45:08 -0300 Subject: [PATCH 08/25] fix(levm): compilation error (#1250) --- crates/vm/vm.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/vm/vm.rs b/crates/vm/vm.rs index f17af79ce..2c3c84b10 100644 --- a/crates/vm/vm.rs +++ b/crates/vm/vm.rs @@ -95,10 +95,13 @@ cfg_if::cfg_if! { ) -> Result<(Vec, Vec), EvmError> { let block_header = &block.header; //eip 4788: execute beacon_root_contract_call before block transactions - #[cfg(not(feature = "l2"))] - if block_header.parent_beacon_block_root.is_some() && spec_id == SpecId::CANCUN { - let spec_id = spec_id(&state.chain_config()?, block_header.timestamp); - beacon_root_contract_call(state, block_header, spec_id)?; + cfg_if::cfg_if! { + if #[cfg(not(feature = "l2"))] { + let spec_id = spec_id(&state.chain_config()?, block_header.timestamp); + if block_header.parent_beacon_block_root.is_some() && spec_id == SpecId::CANCUN { + beacon_root_contract_call(state, block_header, spec_id)?; + } + } } let mut receipts = Vec::new(); let mut cumulative_gas_used = 0; From 2bf96b370b19044b28d1e99f3c4978b78df19685 Mon Sep 17 00:00:00 2001 From: Maximo Palopoli <96491141+maximopalopoli@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:04:22 -0300 Subject: [PATCH 09/25] fix(levm): handle sdiv with zero dividend and negative divisor (#1241) **Motivation** Fixes a bug found by [FuzzingLabs](https://github.com/FuzzingLabs) in `sdiv` opcode implementation. **Description** The error happened when executing sdiv with a zero dividend and a negative divisor, and returned something that is not zero (it's negated). Closes #1199 --- .../vm/levm/src/opcode_handlers/arithmetic.rs | 21 ++++++++++--------- crates/vm/levm/tests/edge_case_tests.rs | 13 ++++++++++++ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/crates/vm/levm/src/opcode_handlers/arithmetic.rs b/crates/vm/levm/src/opcode_handlers/arithmetic.rs index 975392dc1..6b1dba166 100644 --- a/crates/vm/levm/src/opcode_handlers/arithmetic.rs +++ b/crates/vm/levm/src/opcode_handlers/arithmetic.rs @@ -75,7 +75,7 @@ impl VM { let dividend = current_call_frame.stack.pop()?; let divisor = current_call_frame.stack.pop()?; - if divisor.is_zero() { + if divisor.is_zero() || dividend.is_zero() { current_call_frame.stack.push(U256::zero())?; return Ok(OpcodeSuccess::Continue); } @@ -92,15 +92,16 @@ impl VM { } else { divisor }; - let Some(quotient) = dividend.checked_div(divisor) else { - current_call_frame.stack.push(U256::zero())?; - return Ok(OpcodeSuccess::Continue); - }; - let quotient_is_negative = dividend_is_negative ^ divisor_is_negative; - let quotient = if quotient_is_negative { - negate(quotient) - } else { - quotient + let quotient = match dividend.checked_div(divisor) { + Some(quot) => { + let quotient_is_negative = dividend_is_negative ^ divisor_is_negative; + if quotient_is_negative { + negate(quot) + } else { + quot + } + } + None => U256::zero(), }; current_call_frame.stack.push(quotient)?; diff --git a/crates/vm/levm/tests/edge_case_tests.rs b/crates/vm/levm/tests/edge_case_tests.rs index 99b198d05..e406e36a3 100644 --- a/crates/vm/levm/tests/edge_case_tests.rs +++ b/crates/vm/levm/tests/edge_case_tests.rs @@ -101,3 +101,16 @@ fn test_is_negative() { let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); } + +#[test] +fn test_sdiv_zero_dividend_and_negative_divisor() { + let mut vm = new_vm_with_bytecode(Bytes::copy_from_slice(&[ + 0x7F, 0xC5, 0xD2, 0x46, 0x01, 0x86, 0xF7, 0x23, 0x3C, 0x92, 0x7E, 0x7D, 0xB2, 0xDC, 0xC7, + 0x03, 0xC0, 0xE5, 0x00, 0xB6, 0x53, 0xCA, 0x82, 0x27, 0x3B, 0x7B, 0xFA, 0xD8, 0x04, 0x5D, + 0x85, 0xA4, 0x70, 0x5F, 0x05, + ])) + .unwrap(); + let mut current_call_frame = vm.call_frames.pop().unwrap(); + vm.execute(&mut current_call_frame); + assert_eq!(current_call_frame.stack.pop().unwrap(), U256::zero()); +} From 2c63c2ad3f7d9e65514bb4e98d172b102282c99e Mon Sep 17 00:00:00 2001 From: Maximo Palopoli <96491141+maximopalopoli@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:05:53 -0300 Subject: [PATCH 10/25] fix(levm): set calldata as empty bytes at contract creation (#1243) **Motivation** Fixes a bug found by [FuzzingLabs](https://github.com/FuzzingLabs) in creation type transactions. **Description** Previously, in create transactions the calldata field was set with a copy of the bytecode, which was wrong, as it should be an empty set of bytes. Closes #1223 --- crates/vm/levm/src/vm.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/vm/levm/src/vm.rs b/crates/vm/levm/src/vm.rs index f3041fbbf..f6d21a1b9 100644 --- a/crates/vm/levm/src/vm.rs +++ b/crates/vm/levm/src/vm.rs @@ -134,7 +134,7 @@ impl VM { new_contract_address, code, value, - calldata.clone(), + Bytes::new(), false, env.gas_limit.min(MAX_BLOCK_GAS_LIMIT), TX_BASE_COST, From e7cddf1d4332963c65f447ac1108aea2fde57227 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Mon, 25 Nov 2024 12:31:29 -0300 Subject: [PATCH 11/25] fix(l1): renamed all `lambda_ethrex` references to `ethrex` as the repo name (#1255) **Motivation** Renaming incorrect repo references --- cmd/ethrex/ethrex.rs | 4 ++-- crates/l2/docs/prover.md | 4 ++-- crates/networking/p2p/bootnode.rs | 2 +- crates/networking/p2p/net.rs | 2 +- .../src/opcode_handlers/stack_memory_storage_flow.rs | 2 +- crates/vm/levm/src/opcode_handlers/system.rs | 10 +++++----- crates/vm/levm/src/vm.rs | 6 +++--- crates/vm/vm.rs | 2 +- test_data/network_params.yaml | 2 +- 9 files changed, 17 insertions(+), 17 deletions(-) diff --git a/cmd/ethrex/ethrex.rs b/cmd/ethrex/ethrex.rs index 0d905fc8e..371929734 100644 --- a/cmd/ethrex/ethrex.rs +++ b/cmd/ethrex/ethrex.rs @@ -152,7 +152,7 @@ async fn main() { let jwt_secret = read_jwtsecret_file(authrpc_jwtsecret); // TODO Learn how should the key be created - // https://github.com/lambdaclass/lambda_ethrex/issues/836 + // https://github.com/lambdaclass/ethrex/issues/836 //let signer = SigningKey::random(&mut OsRng); let key_bytes = H256::from_str("577d8278cc7748fad214b5378669b420f8221afb45ce930b7f22da49cbc545f3").unwrap(); @@ -186,7 +186,7 @@ async fn main() { .into_future(); // TODO Find a proper place to show node information - // https://github.com/lambdaclass/lambda_ethrex/issues/836 + // https://github.com/lambdaclass/ethrex/issues/836 let enode = local_p2p_node.enode_url(); info!("Node: {enode}"); diff --git a/crates/l2/docs/prover.md b/crates/l2/docs/prover.md index 35d70a117..daaf3c2a9 100644 --- a/crates/l2/docs/prover.md +++ b/crates/l2/docs/prover.md @@ -116,7 +116,7 @@ make perf_gpu Two servers are required: one for the `prover` and another for the `proposer`. If you run both components on the same machine, the `prover` may consume all available resources, leading to potential stuttering or performance issues for the `proposer`/`node`. 1. `prover`/`zkvm` → prover with gpu, make sure to have all the required dependencies described at the beginning of [Gpu Mode](#gpu-mode) section. - 1. `cd lambda_ethrex/crates/l2` + 1. `cd ethrex/crates/l2` 2. `cp .example.env` and change the `PROVER_CLIENT_PROVER_SERVER_ENDPOINT` with the ip of the other server. The env variables needed are: @@ -131,7 +131,7 @@ Finally, to start the `prover_client`/`zkvm`, run: - `make init-l2-prover-gpu` 2. Β `proposer` → this server just needs rust installed. - 1. `cd lambda_ethrex/crates/l2` + 1. `cd ethrex/crates/l2` 2. `cp .example.env` and change the addresses and the following fields: - `PROVER_SERVER_LISTEN_IP=0.0.0.0`Β → used to handle the tcp communication with the other server. - The `COMMITTER` and `PROVER_SERVER_VERIFIER` must be different accounts, the `DEPLOYER_ADDRESS` as well as the `L1_WATCHER` may be the same account used by the `COMMITTER` diff --git a/crates/networking/p2p/bootnode.rs b/crates/networking/p2p/bootnode.rs index c4caed177..638eb2768 100644 --- a/crates/networking/p2p/bootnode.rs +++ b/crates/networking/p2p/bootnode.rs @@ -12,7 +12,7 @@ impl FromStr for BootNode { /// Takes a str with the format "enode://nodeID@IPaddress:port" and /// parses it to a BootNode // TODO: fix it to support different UDP and TCP ports, according to - // https://github.com/lambdaclass/lambda_ethrex/issues/905 + // https://github.com/lambdaclass/ethrex/issues/905 fn from_str(input: &str) -> Result { // TODO: error handling let node_id = H512::from_str(&input[8..136]).expect("Failed to parse node id"); diff --git a/crates/networking/p2p/net.rs b/crates/networking/p2p/net.rs index b7cb064f7..4c3b00e68 100644 --- a/crates/networking/p2p/net.rs +++ b/crates/networking/p2p/net.rs @@ -345,7 +345,7 @@ async fn discovery_startup( ip: bootnode.socket_address.ip(), udp_port: bootnode.socket_address.port(), // TODO: udp port can differ from tcp port. - // see https://github.com/lambdaclass/lambda_ethrex/issues/905 + // see https://github.com/lambdaclass/ethrex/issues/905 tcp_port: bootnode.socket_address.port(), node_id: bootnode.node_id, }); diff --git a/crates/vm/levm/src/opcode_handlers/stack_memory_storage_flow.rs b/crates/vm/levm/src/opcode_handlers/stack_memory_storage_flow.rs index 55b38884d..0226d059b 100644 --- a/crates/vm/levm/src/opcode_handlers/stack_memory_storage_flow.rs +++ b/crates/vm/levm/src/opcode_handlers/stack_memory_storage_flow.rs @@ -159,7 +159,7 @@ impl VM { } // SSTORE operation - // TODO: https://github.com/lambdaclass/lambda_ethrex/issues/1087 + // TODO: https://github.com/lambdaclass/ethrex/issues/1087 pub fn op_sstore( &mut self, current_call_frame: &mut CallFrame, diff --git a/crates/vm/levm/src/opcode_handlers/system.rs b/crates/vm/levm/src/opcode_handlers/system.rs index 1012a2e95..fe676f2bb 100644 --- a/crates/vm/levm/src/opcode_handlers/system.rs +++ b/crates/vm/levm/src/opcode_handlers/system.rs @@ -87,7 +87,7 @@ impl VM { } // CALLCODE operation - // TODO: https://github.com/lambdaclass/lambda_ethrex/issues/1086 + // TODO: https://github.com/lambdaclass/ethrex/issues/1086 pub fn op_callcode( &mut self, current_call_frame: &mut CallFrame, @@ -192,7 +192,7 @@ impl VM { } // DELEGATECALL operation - // TODO: https://github.com/lambdaclass/lambda_ethrex/issues/1086 + // TODO: https://github.com/lambdaclass/ethrex/issues/1086 pub fn op_delegatecall( &mut self, current_call_frame: &mut CallFrame, @@ -260,7 +260,7 @@ impl VM { } // STATICCALL operation - // TODO: https://github.com/lambdaclass/lambda_ethrex/issues/1086 + // TODO: https://github.com/lambdaclass/ethrex/issues/1086 pub fn op_staticcall( &mut self, current_call_frame: &mut CallFrame, @@ -328,7 +328,7 @@ impl VM { } // CREATE operation - // TODO: https://github.com/lambdaclass/lambda_ethrex/issues/1086 + // TODO: https://github.com/lambdaclass/ethrex/issues/1086 pub fn op_create( &mut self, current_call_frame: &mut CallFrame, @@ -357,7 +357,7 @@ impl VM { } // CREATE2 operation - // TODO: https://github.com/lambdaclass/lambda_ethrex/issues/1086 + // TODO: https://github.com/lambdaclass/ethrex/issues/1086 pub fn op_create2( &mut self, current_call_frame: &mut CallFrame, diff --git a/crates/vm/levm/src/vm.rs b/crates/vm/levm/src/vm.rs index f6d21a1b9..dd0fcf7dc 100644 --- a/crates/vm/levm/src/vm.rs +++ b/crates/vm/levm/src/vm.rs @@ -151,7 +151,7 @@ impl VM { }) } } - // TODO: https://github.com/lambdaclass/lambda_ethrex/issues/1088 + // TODO: https://github.com/lambdaclass/ethrex/issues/1088 } pub fn execute(&mut self, current_call_frame: &mut CallFrame) -> TransactionReport { @@ -600,7 +600,7 @@ impl VM { )) } - // TODO: Improve and test REVERT behavior for XCALL opcodes. Issue: https://github.com/lambdaclass/lambda_ethrex/issues/1061 + // TODO: Improve and test REVERT behavior for XCALL opcodes. Issue: https://github.com/lambdaclass/ethrex/issues/1061 #[allow(clippy::too_many_arguments)] pub fn generic_call( &mut self, @@ -782,7 +782,7 @@ impl VM { /// Common behavior for CREATE and CREATE2 opcodes /// /// Could be used for CREATE type transactions - // TODO: Improve and test REVERT behavior for CREATE. Issue: https://github.com/lambdaclass/lambda_ethrex/issues/1061 + // TODO: Improve and test REVERT behavior for CREATE. Issue: https://github.com/lambdaclass/ethrex/issues/1061 pub fn create( &mut self, value_in_wei_to_send: U256, diff --git a/crates/vm/vm.rs b/crates/vm/vm.rs index 2c3c84b10..e095a7554 100644 --- a/crates/vm/vm.rs +++ b/crates/vm/vm.rs @@ -120,7 +120,7 @@ cfg_if::cfg_if! { transaction.tx_type(), matches!(result.result, TxResult::Success), cumulative_gas_used, - // TODO: https://github.com/lambdaclass/lambda_ethrex/issues/1089 + // TODO: https://github.com/lambdaclass/ethrex/issues/1089 vec![], ); receipts.push(receipt); diff --git a/test_data/network_params.yaml b/test_data/network_params.yaml index 6e251057b..201594715 100644 --- a/test_data/network_params.yaml +++ b/test_data/network_params.yaml @@ -20,4 +20,4 @@ assertoor_params: run_block_proposal_check: false run_blob_transaction_test: true tests: - - 'https://raw.githubusercontent.com/lambdaclass/lambda_ethrex/refs/heads/main/test_data/el-stability-check.yml' + - 'https://raw.githubusercontent.com/lambdaclass/ethrex/refs/heads/main/test_data/el-stability-check.yml' From 7f4e76fa4f6f7a76c6de06b2641544eee29e66db Mon Sep 17 00:00:00 2001 From: Ivan Litteri <67517699+ilitteri@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:37:44 -0300 Subject: [PATCH 12/25] refactor(levm): ef test runner (#1206) **Description** - Fixes EF tests parsing - Adds `levm_runner` module - Adds `revm_runner` module - Refactors `runner` module - Fixes post-state validations - Refactors `report` module. - Adds more reports. --------- Co-authored-by: JereSalo --- .gitignore | 2 + cmd/ef_tests/levm/Cargo.toml | 13 +- cmd/ef_tests/levm/deserialize.rs | 171 +++---- cmd/ef_tests/levm/ef_tests.rs | 3 +- cmd/ef_tests/levm/parser.rs | 112 +++++ cmd/ef_tests/levm/report.rs | 612 ++++++++++++++++++++--- cmd/ef_tests/levm/runner.rs | 289 ----------- cmd/ef_tests/levm/runner/levm_runner.rs | 247 +++++++++ cmd/ef_tests/levm/runner/mod.rs | 156 ++++++ cmd/ef_tests/levm/runner/revm_runner.rs | 286 +++++++++++ cmd/ef_tests/levm/tests/ef_tests_levm.rs | 13 + cmd/ef_tests/levm/tests/test.rs | 6 - cmd/ef_tests/levm/types.rs | 111 +++- cmd/ef_tests/levm/utils.rs | 7 +- crates/common/types/account.rs | 2 +- crates/storage/store/storage.rs | 3 +- crates/vm/levm/src/account.rs | 7 +- crates/vm/levm/src/errors.rs | 11 +- crates/vm/vm.rs | 4 +- 19 files changed, 1575 insertions(+), 480 deletions(-) create mode 100644 cmd/ef_tests/levm/parser.rs delete mode 100644 cmd/ef_tests/levm/runner.rs create mode 100644 cmd/ef_tests/levm/runner/levm_runner.rs create mode 100644 cmd/ef_tests/levm/runner/mod.rs create mode 100644 cmd/ef_tests/levm/runner/revm_runner.rs create mode 100644 cmd/ef_tests/levm/tests/ef_tests_levm.rs delete mode 100644 cmd/ef_tests/levm/tests/test.rs diff --git a/.gitignore b/.gitignore index d0a4bbbc2..3ee1c729c 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,5 @@ jwt.hex tests_v3.0.0.tar.gz .env + +levm_ef_tests_report.txt diff --git a/cmd/ef_tests/levm/Cargo.toml b/cmd/ef_tests/levm/Cargo.toml index 51504dbc0..6a553046e 100644 --- a/cmd/ef_tests/levm/Cargo.toml +++ b/cmd/ef_tests/levm/Cargo.toml @@ -17,6 +17,17 @@ hex.workspace = true keccak-hash = "0.11.0" colored = "2.1.0" spinoff = "0.8.0" +thiserror = "2.0.3" +clap = { version = "4.3", features = ["derive"] } +clap_complete = "4.5.17" + +revm = { version = "14.0.3", features = [ + "serde", + "std", + "serde-json", + "optional_no_base_fee", + "optional_block_gas_limit", +], default-features = false } [dev-dependencies] hex = "0.4.3" @@ -25,5 +36,5 @@ hex = "0.4.3" path = "./ef_tests.rs" [[test]] -name = "test" +name = "ef_tests_levm" harness = false diff --git a/cmd/ef_tests/levm/deserialize.rs b/cmd/ef_tests/levm/deserialize.rs index b81f4208b..612e17045 100644 --- a/cmd/ef_tests/levm/deserialize.rs +++ b/cmd/ef_tests/levm/deserialize.rs @@ -1,4 +1,4 @@ -use crate::types::EFTest; +use crate::types::{EFTest, EFTests}; use bytes::Bytes; use ethrex_core::U256; use serde::Deserialize; @@ -122,103 +122,104 @@ where .collect() } -impl<'de> Deserialize<'de> for EFTest { +impl<'de> Deserialize<'de> for EFTests { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { + let mut ef_tests = Vec::new(); let aux: HashMap> = HashMap::deserialize(deserializer)?; - let test_name = aux - .keys() - .next() - .ok_or(serde::de::Error::missing_field("test name"))?; - let test_data = aux - .get(test_name) - .ok_or(serde::de::Error::missing_field("test data value"))?; - let raw_tx: EFTestRawTransaction = serde_json::from_value( - test_data - .get("transaction") - .ok_or(serde::de::Error::missing_field("transaction"))? - .clone(), - ) - .map_err(|err| { - serde::de::Error::custom(format!( - "error deserializing test \"{test_name}\", \"transaction\" field: {err}" - )) - })?; - - let mut transactions = Vec::new(); + for test_name in aux.keys() { + let test_data = aux + .get(test_name) + .ok_or(serde::de::Error::missing_field("test data value"))?; - // Note that inthis order of iteration, in an example tx with 2 datas, 2 gasLimit and 2 values, order would be - // 111, 112, 121, 122, 211, 212, 221, 222 - for (data_id, data) in raw_tx.data.iter().enumerate() { - for (gas_limit_id, gas_limit) in raw_tx.gas_limit.iter().enumerate() { - for (value_id, value) in raw_tx.value.iter().enumerate() { - let tx = EFTestTransaction { - data: data.clone(), - gas_limit: *gas_limit, - gas_price: raw_tx.gas_price, - nonce: raw_tx.nonce, - secret_key: raw_tx.secret_key, - sender: raw_tx.sender, - to: raw_tx.to.clone(), - value: *value, - }; - transactions.push(((data_id, gas_limit_id, value_id), tx)); - } - } - } - - let ef_test = Self { - name: test_name.to_owned().to_owned(), - _info: serde_json::from_value( - test_data - .get("_info") - .ok_or(serde::de::Error::missing_field("_info"))? - .clone(), - ) - .map_err(|err| { - serde::de::Error::custom(format!( - "error deserializing test \"{test_name}\", \"_info\" field: {err}" - )) - })?, - env: serde_json::from_value( - test_data - .get("env") - .ok_or(serde::de::Error::missing_field("env"))? - .clone(), - ) - .map_err(|err| { - serde::de::Error::custom(format!( - "error deserializing test \"{test_name}\", \"env\" field: {err}" - )) - })?, - post: serde_json::from_value( - test_data - .get("post") - .ok_or(serde::de::Error::missing_field("post"))? - .clone(), - ) - .map_err(|err| { - serde::de::Error::custom(format!( - "error deserializing test \"{test_name}\", \"post\" field: {err}" - )) - })?, - pre: serde_json::from_value( + let raw_tx: EFTestRawTransaction = serde_json::from_value( test_data - .get("pre") - .ok_or(serde::de::Error::missing_field("pre"))? + .get("transaction") + .ok_or(serde::de::Error::missing_field("transaction"))? .clone(), ) .map_err(|err| { serde::de::Error::custom(format!( - "error deserializing test \"{test_name}\", \"pre\" field: {err}" + "error deserializing test \"{test_name}\", \"transaction\" field: {err}" )) - })?, - transactions, - }; - Ok(ef_test) + })?; + + let mut transactions = HashMap::new(); + + // Note that inthis order of iteration, in an example tx with 2 datas, 2 gasLimit and 2 values, order would be + // 111, 112, 121, 122, 211, 212, 221, 222 + for (data_id, data) in raw_tx.data.iter().enumerate() { + for (gas_limit_id, gas_limit) in raw_tx.gas_limit.iter().enumerate() { + for (value_id, value) in raw_tx.value.iter().enumerate() { + let tx = EFTestTransaction { + data: data.clone(), + gas_limit: *gas_limit, + gas_price: raw_tx.gas_price, + nonce: raw_tx.nonce, + secret_key: raw_tx.secret_key, + sender: raw_tx.sender, + to: raw_tx.to.clone(), + value: *value, + }; + transactions.insert((data_id, gas_limit_id, value_id), tx); + } + } + } + + let ef_test = EFTest { + name: test_name.to_owned().to_owned(), + _info: serde_json::from_value( + test_data + .get("_info") + .ok_or(serde::de::Error::missing_field("_info"))? + .clone(), + ) + .map_err(|err| { + serde::de::Error::custom(format!( + "error deserializing test \"{test_name}\", \"_info\" field: {err}" + )) + })?, + env: serde_json::from_value( + test_data + .get("env") + .ok_or(serde::de::Error::missing_field("env"))? + .clone(), + ) + .map_err(|err| { + serde::de::Error::custom(format!( + "error deserializing test \"{test_name}\", \"env\" field: {err}" + )) + })?, + post: serde_json::from_value( + test_data + .get("post") + .ok_or(serde::de::Error::missing_field("post"))? + .clone(), + ) + .map_err(|err| { + serde::de::Error::custom(format!( + "error deserializing test \"{test_name}\", \"post\" field: {err}" + )) + })?, + pre: serde_json::from_value( + test_data + .get("pre") + .ok_or(serde::de::Error::missing_field("pre"))? + .clone(), + ) + .map_err(|err| { + serde::de::Error::custom(format!( + "error deserializing test \"{test_name}\", \"pre\" field: {err}" + )) + })?, + transactions, + }; + ef_tests.push(ef_test); + } + Ok(Self(ef_tests)) } } diff --git a/cmd/ef_tests/levm/ef_tests.rs b/cmd/ef_tests/levm/ef_tests.rs index 74e2ea7d5..27e09b709 100644 --- a/cmd/ef_tests/levm/ef_tests.rs +++ b/cmd/ef_tests/levm/ef_tests.rs @@ -1,5 +1,6 @@ mod deserialize; +pub mod parser; mod report; pub mod runner; -mod types; +pub mod types; mod utils; diff --git a/cmd/ef_tests/levm/parser.rs b/cmd/ef_tests/levm/parser.rs new file mode 100644 index 000000000..6cfb23f51 --- /dev/null +++ b/cmd/ef_tests/levm/parser.rs @@ -0,0 +1,112 @@ +use crate::{ + report::format_duration_as_mm_ss, + runner::EFTestRunnerOptions, + types::{EFTest, EFTests}, +}; +use colored::Colorize; +use spinoff::{spinners::Dots, Color, Spinner}; +use std::fs::DirEntry; + +#[derive(Debug, thiserror::Error)] +pub enum EFTestParseError { + #[error("Failed to read directory: {0}")] + FailedToReadDirectory(String), + #[error("Failed to read file: {0}")] + FailedToReadFile(String), + #[error("Failed to get file type: {0}")] + FailedToGetFileType(String), + #[error("Failed to parse test file: {0}")] + FailedToParseTestFile(String), +} + +pub fn parse_ef_tests(opts: &EFTestRunnerOptions) -> Result, EFTestParseError> { + let parsing_time = std::time::Instant::now(); + let cargo_manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let ef_general_state_tests_path = cargo_manifest_dir.join("vectors/GeneralStateTests"); + let mut spinner = Spinner::new(Dots, "Parsing EF Tests".bold().to_string(), Color::Cyan); + let mut tests = Vec::new(); + for test_dir in std::fs::read_dir(ef_general_state_tests_path.clone()) + .map_err(|err| { + EFTestParseError::FailedToReadDirectory(format!( + "{:?}: {err}", + ef_general_state_tests_path.file_name() + )) + })? + .flatten() + { + let directory_tests = parse_ef_test_dir(test_dir, opts, &mut spinner)?; + tests.extend(directory_tests); + } + spinner.success( + &format!( + "Parsed EF Tests in {}", + format_duration_as_mm_ss(parsing_time.elapsed()) + ) + .bold(), + ); + Ok(tests) +} + +pub fn parse_ef_test_dir( + test_dir: DirEntry, + opts: &EFTestRunnerOptions, + directory_parsing_spinner: &mut Spinner, +) -> Result, EFTestParseError> { + directory_parsing_spinner.update_text(format!("Parsing directory {:?}", test_dir.file_name())); + + let mut directory_tests = Vec::new(); + for test in std::fs::read_dir(test_dir.path()) + .map_err(|err| { + EFTestParseError::FailedToReadDirectory(format!("{:?}: {err}", test_dir.file_name())) + })? + .flatten() + { + if test + .file_type() + .map_err(|err| { + EFTestParseError::FailedToGetFileType(format!("{:?}: {err}", test.file_name())) + })? + .is_dir() + { + let sub_directory_tests = parse_ef_test_dir(test, opts, directory_parsing_spinner)?; + directory_tests.extend(sub_directory_tests); + continue; + } + // Skip non-JSON files. + if test.path().extension().is_some_and(|ext| ext != "json") + | test.path().extension().is_none() + { + continue; + } + // Skip the ValueOverflowParis.json file. + if test + .path() + .file_name() + .is_some_and(|name| name == "ValueOverflowParis.json") + { + continue; + } + + // Skip tests that are not in the list of tests to run. + if !opts.tests.is_empty() + && !opts + .tests + .contains(&test_dir.file_name().to_str().unwrap().to_owned()) + { + directory_parsing_spinner.update_text(format!( + "Skipping test {:?} as it is not in the list of tests to run", + test.path().file_name() + )); + return Ok(Vec::new()); + } + + let test_file = std::fs::File::open(test.path()).map_err(|err| { + EFTestParseError::FailedToReadFile(format!("{:?}: {err}", test.path())) + })?; + let test: EFTests = serde_json::from_reader(test_file).map_err(|err| { + EFTestParseError::FailedToParseTestFile(format!("{:?} parse error: {err}", test.path())) + })?; + directory_tests.extend(test.0); + } + Ok(directory_tests) +} diff --git a/cmd/ef_tests/levm/report.rs b/cmd/ef_tests/levm/report.rs index 012af8b01..9a90a1521 100644 --- a/cmd/ef_tests/levm/report.rs +++ b/cmd/ef_tests/levm/report.rs @@ -1,101 +1,573 @@ -// Note: I use this to do not affect the EF tests logic with this side effects -// The cost to add this would be to return a Result<(), InternalError> in EFTestsReport methods - +use crate::runner::{EFTestRunnerError, InternalError}; use colored::Colorize; -use std::{collections::HashMap, fmt}; - -#[derive(Debug, Default)] -pub struct EFTestsReport { - group_passed: u64, - group_failed: u64, - group_run: u64, - test_reports: HashMap, - passed_tests: Vec, - failed_tests: Vec<(String, (usize, usize, usize), String)>, +use ethrex_core::{Address, H256}; +use ethrex_levm::errors::{TransactionReport, TxResult, VMError}; +use ethrex_storage::{error::StoreError, AccountUpdate}; +use ethrex_vm::SpecId; +use revm::primitives::{EVMError, ExecutionResult as RevmExecutionResult}; +use serde::{Deserialize, Serialize}; +use spinoff::{spinners::Dots, Color, Spinner}; +use std::{ + collections::{HashMap, HashSet}, + fmt::{self, Display}, + path::PathBuf, + time::Duration, +}; + +pub type TestVector = (usize, usize, usize); + +pub fn progress(reports: &[EFTestReport], time: Duration) -> String { + format!( + "{}: {} {} {} - {}", + "Ethereum Foundation Tests".bold(), + format!( + "{} passed", + reports.iter().filter(|report| report.passed()).count() + ) + .green() + .bold(), + format!( + "{} failed", + reports.iter().filter(|report| !report.passed()).count() + ) + .red() + .bold(), + format!("{} total run", reports.len()).blue().bold(), + format_duration_as_mm_ss(time) + ) +} +pub fn summary(reports: &[EFTestReport]) -> String { + let total_passed = reports.iter().filter(|report| report.passed()).count(); + let total_run = reports.len(); + format!( + "{} {}/{total_run}\n\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n", + "Summary:".bold(), + if total_passed == total_run { + format!("{}", total_passed).green() + } else if total_passed > 0 { + format!("{}", total_passed).yellow() + } else { + format!("{}", total_passed).red() + }, + fork_summary(reports, SpecId::CANCUN), + fork_summary(reports, SpecId::SHANGHAI), + fork_summary(reports, SpecId::HOMESTEAD), + fork_summary(reports, SpecId::ISTANBUL), + fork_summary(reports, SpecId::LONDON), + fork_summary(reports, SpecId::BYZANTIUM), + fork_summary(reports, SpecId::BERLIN), + fork_summary(reports, SpecId::CONSTANTINOPLE), + fork_summary(reports, SpecId::MERGE), + fork_summary(reports, SpecId::FRONTIER), + ) +} + +pub fn write(reports: &[EFTestReport]) -> Result { + let report_file_path = PathBuf::from("./levm_ef_tests_report.txt"); + let failed_test_reports = EFTestsReport( + reports + .iter() + .filter(|&report| !report.passed()) + .cloned() + .collect(), + ); + std::fs::write( + "./levm_ef_tests_report.txt", + failed_test_reports.to_string(), + ) + .map_err(|err| { + EFTestRunnerError::Internal(InternalError::MainRunnerInternal(format!( + "Failed to write report to file: {err}" + ))) + })?; + Ok(report_file_path) +} + +pub const EF_TESTS_CACHE_FILE_PATH: &str = "./levm_ef_tests_cache.json"; + +pub fn cache(reports: &[EFTestReport]) -> Result { + let cache_file_path = PathBuf::from(EF_TESTS_CACHE_FILE_PATH); + let cache = serde_json::to_string_pretty(&reports).map_err(|err| { + EFTestRunnerError::Internal(InternalError::MainRunnerInternal(format!( + "Failed to serialize cache: {err}" + ))) + })?; + std::fs::write(&cache_file_path, cache).map_err(|err| { + EFTestRunnerError::Internal(InternalError::MainRunnerInternal(format!( + "Failed to write cache to file: {err}" + ))) + })?; + Ok(cache_file_path) +} + +pub fn load() -> Result, EFTestRunnerError> { + let mut reports_loading_spinner = + Spinner::new(Dots, "Loading reports...".to_owned(), Color::Cyan); + match std::fs::read_to_string(EF_TESTS_CACHE_FILE_PATH).ok() { + Some(cache) => { + reports_loading_spinner.success("Reports loaded"); + serde_json::from_str(&cache).map_err(|err| { + EFTestRunnerError::Internal(InternalError::MainRunnerInternal(format!( + "Cache exists but there was an error loading it: {err}" + ))) + }) + } + None => { + reports_loading_spinner.success("No cache found"); + Ok(Vec::default()) + } + } +} + +pub fn format_duration_as_mm_ss(duration: Duration) -> String { + let total_seconds = duration.as_secs(); + let minutes = total_seconds / 60; + let seconds = total_seconds % 60; + format!("{minutes:02}:{seconds:02}") } #[derive(Debug, Default, Clone)] +pub struct EFTestsReport(pub Vec); + +impl Display for EFTestsReport { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let total_passed = self.0.iter().filter(|report| report.passed()).count(); + let total_run = self.0.len(); + writeln!( + f, + "{} {}/{total_run}", + "Summary:".bold(), + if total_passed == total_run { + format!("{}", total_passed).green() + } else if total_passed > 0 { + format!("{}", total_passed).yellow() + } else { + format!("{}", total_passed).red() + }, + )?; + writeln!(f)?; + writeln!(f, "{}", fork_summary(&self.0, SpecId::CANCUN))?; + writeln!(f, "{}", fork_summary(&self.0, SpecId::SHANGHAI))?; + writeln!(f, "{}", fork_summary(&self.0, SpecId::HOMESTEAD))?; + writeln!(f, "{}", fork_summary(&self.0, SpecId::ISTANBUL))?; + writeln!(f, "{}", fork_summary(&self.0, SpecId::LONDON))?; + writeln!(f, "{}", fork_summary(&self.0, SpecId::BYZANTIUM))?; + writeln!(f, "{}", fork_summary(&self.0, SpecId::BERLIN))?; + writeln!(f, "{}", fork_summary(&self.0, SpecId::CONSTANTINOPLE))?; + writeln!(f, "{}", fork_summary(&self.0, SpecId::MERGE))?; + writeln!(f, "{}", fork_summary(&self.0, SpecId::FRONTIER))?; + writeln!(f)?; + writeln!(f, "{}", "Failed tests:".bold())?; + writeln!(f)?; + for report in self.0.iter() { + if report.failed_vectors.is_empty() { + continue; + } + writeln!(f, "{}", format!("Test: {}", report.name).bold())?; + writeln!(f)?; + for (failed_vector, error) in &report.failed_vectors { + writeln!( + f, + "{} (data_index: {}, gas_limit_index: {}, value_index: {})", + "Vector:".bold(), + failed_vector.0, + failed_vector.1, + failed_vector.2 + )?; + writeln!(f, "{} {}", "Error:".bold(), error.to_string().red())?; + if let Some(re_run_report) = &report.re_run_report { + if let Some(execution_report) = + re_run_report.execution_report.get(failed_vector) + { + if let Some((levm_result, revm_result)) = + &execution_report.execution_result_mismatch + { + writeln!( + f, + "{}: LEVM: {levm_result:?}, REVM: {revm_result:?}", + "Execution result mismatch".bold() + )?; + } + if let Some((levm_gas_used, revm_gas_used)) = + &execution_report.gas_used_mismatch + { + writeln!( + f, + "{}: LEVM: {levm_gas_used}, REVM: {revm_gas_used} (diff: {})", + "Gas used mismatch".bold(), + levm_gas_used.abs_diff(*revm_gas_used) + )?; + } + if let Some((levm_gas_refunded, revm_gas_refunded)) = + &execution_report.gas_refunded_mismatch + { + writeln!( + f, + "{}: LEVM: {levm_gas_refunded}, REVM: {revm_gas_refunded} (diff: {})", + "Gas refunded mismatch".bold(), levm_gas_refunded.abs_diff(*revm_gas_refunded) + )?; + } + if let Some((levm_result, revm_error)) = &execution_report.re_runner_error { + writeln!( + f, + "{}: LEVM: {levm_result:?}, REVM: {revm_error}", + "Re-run error".bold() + )?; + } + } + + if let Some(account_update) = + re_run_report.account_updates_report.get(failed_vector) + { + writeln!(f, "{}", &account_update.to_string())?; + } else { + writeln!(f, "No account updates report found. Account update reports are only generated for tests that failed at the post-state validation stage.")?; + } + } else { + writeln!(f, "No re-run report found. Re-run reports are only generated for tests that failed at the post-state validation stage.")?; + } + writeln!(f)?; + } + } + Ok(()) + } +} + +fn fork_summary(reports: &[EFTestReport], fork: SpecId) -> String { + let fork_str: &str = fork.into(); + let fork_tests = reports.iter().filter(|report| report.fork == fork).count(); + let fork_passed_tests = reports + .iter() + .filter(|report| report.fork == fork && report.passed()) + .count(); + format!( + "{}: {}/{fork_tests}", + fork_str.bold(), + if fork_passed_tests == fork_tests { + format!("{}", fork_passed_tests).green() + } else if fork_passed_tests > 0 { + format!("{}", fork_passed_tests).yellow() + } else { + format!("{}", fork_passed_tests).red() + }, + ) +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct EFTestReport { - passed: u64, - failed: u64, - run: u64, - failed_tests: Vec<((usize, usize, usize), String)>, + pub name: String, + pub test_hash: H256, + pub fork: SpecId, + pub skipped: bool, + pub failed_vectors: HashMap, + pub re_run_report: Option, } -impl EFTestsReport { - pub fn register_pass(&mut self, test_name: &str) { - self.passed_tests.push(test_name.to_string()); +impl EFTestReport { + pub fn new(name: String, test_hash: H256, fork: SpecId) -> Self { + EFTestReport { + name, + test_hash, + fork, + ..Default::default() + } + } - let report = self.test_reports.entry(test_name.to_string()).or_default(); - report.passed += 1; - report.run += 1; + pub fn new_skipped() -> Self { + EFTestReport { + skipped: true, + ..Default::default() + } } - pub fn register_fail( + pub fn register_unexpected_execution_failure( &mut self, - tx_indexes: (usize, usize, usize), - test_name: &str, - reason: &str, + error: VMError, + failed_vector: TestVector, ) { - self.failed_tests - .push((test_name.to_owned(), tx_indexes, reason.to_owned())); + self.failed_vectors.insert( + failed_vector, + EFTestRunnerError::ExecutionFailedUnexpectedly(error), + ); + } - let report = self.test_reports.entry(test_name.to_string()).or_default(); - report.failed += 1; - report.failed_tests.push((tx_indexes, reason.to_owned())); - report.run += 1; + pub fn register_vm_initialization_failure( + &mut self, + reason: String, + failed_vector: TestVector, + ) { + self.failed_vectors.insert( + failed_vector, + EFTestRunnerError::VMInitializationFailed(reason), + ); } - pub fn register_group_pass(&mut self) { - self.group_passed += 1; - self.group_run += 1; + pub fn register_pre_state_validation_failure( + &mut self, + reason: String, + failed_vector: TestVector, + ) { + self.failed_vectors.insert( + failed_vector, + EFTestRunnerError::FailedToEnsurePreState(reason), + ); } - pub fn register_group_fail(&mut self) { - self.group_failed += 1; - self.group_run += 1; + pub fn register_post_state_validation_failure( + &mut self, + transaction_report: TransactionReport, + reason: String, + failed_vector: TestVector, + ) { + self.failed_vectors.insert( + failed_vector, + EFTestRunnerError::FailedToEnsurePostState(transaction_report, reason), + ); } - pub fn progress(&self) -> String { - format!( - "{}: {} {} {}", - "Ethereum Foundation Tests Run".bold(), - format!("{} passed", self.group_passed).green().bold(), - format!("{} failed", self.group_failed).red().bold(), - format!("{} total run", self.group_run).blue().bold(), - ) + pub fn register_re_run_report(&mut self, re_run_report: TestReRunReport) { + self.re_run_report = Some(re_run_report); + } + + pub fn iter_failed(&self) -> impl Iterator { + self.failed_vectors.iter() + } + + pub fn passed(&self) -> bool { + self.failed_vectors.is_empty() } } -impl fmt::Display for EFTestsReport { +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct AccountUpdatesReport { + pub levm_account_updates: Vec, + pub revm_account_updates: Vec, + pub levm_updated_accounts_only: HashSet
, + pub revm_updated_accounts_only: HashSet
, + pub shared_updated_accounts: HashSet
, +} + +impl fmt::Display for AccountUpdatesReport { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "Report:")?; - writeln!(f, "Total results: {}", self.progress())?; - for (test_name, report) in self.test_reports.clone() { - if report.failed == 0 { - continue; - } - writeln!(f)?; + writeln!(f, "Account Updates:")?; + for levm_updated_account_only in self.levm_updated_accounts_only.iter() { + writeln!(f, " {levm_updated_account_only:#x}:")?; + writeln!(f, "{}", " Was updated in LEVM but not in REVM".red())?; + } + for revm_updated_account_only in self.revm_updated_accounts_only.iter() { + writeln!(f, " {revm_updated_account_only:#x}:")?; + writeln!(f, "{}", " Was updated in REVM but not in LEVM".red())?; + } + for shared_updated_account in self.shared_updated_accounts.iter() { + writeln!(f, " {shared_updated_account:#x}:")?; + writeln!( f, - "Test results for {}: {} {} {}", - test_name, - format!("{} passed", report.passed).green().bold(), - format!("{} failed", report.failed).red().bold(), - format!("{} run", report.run).blue().bold(), + "{}", + " Was updated in both LEVM and REVM".to_string().green() )?; - for failing_test in report.failed_tests.clone() { - writeln!( - f, - "(data_index: {}, gas_limit_index: {}, value_index: {}). Err: {}", - failing_test.0 .0, - failing_test.0 .1, - failing_test.0 .2, - failing_test.1.bright_red().bold() - )?; + + let levm_updated_account = self + .levm_account_updates + .iter() + .find(|account_update| &account_update.address == shared_updated_account) + .unwrap(); + let revm_updated_account = self + .revm_account_updates + .iter() + .find(|account_update| &account_update.address == shared_updated_account) + .unwrap(); + + match (levm_updated_account.removed, revm_updated_account.removed) { + (true, false) => { + writeln!( + f, + "{}", + " Removed in LEVM but not in REVM".to_string().red() + )?; + } + (false, true) => { + writeln!( + f, + "{}", + " Removed in REVM but not in LEVM".to_string().red() + )?; + } + // Account was removed in both VMs. + (false, false) | (true, true) => {} + } + + match (&levm_updated_account.code, &revm_updated_account.code) { + (None, Some(_)) => { + writeln!( + f, + "{}", + " Has code in REVM but not in LEVM".to_string().red() + )?; + } + (Some(_), None) => { + writeln!( + f, + "{}", + " Has code in LEVM but not in REVM".to_string().red() + )?; + } + (Some(levm_account_code), Some(revm_account_code)) => { + if levm_account_code != revm_account_code { + writeln!(f, + "{}", + format!( + " Code mismatch: LEVM: {levm_account_code}, REVM: {revm_account_code}", + levm_account_code = hex::encode(levm_account_code), + revm_account_code = hex::encode(revm_account_code) + ) + .red() + )?; + } + } + (None, None) => {} } - } + match (&levm_updated_account.info, &revm_updated_account.info) { + (None, Some(_)) => { + writeln!( + f, + "{}", + format!(" Account {shared_updated_account:#x} has info in REVM but not in LEVM",) + .red() + .bold() + )?; + } + (Some(levm_account_info), Some(revm_account_info)) => { + if levm_account_info.balance != revm_account_info.balance { + writeln!(f, + "{}", + format!( + " Balance mismatch: LEVM: {levm_account_balance}, REVM: {revm_account_balance}", + levm_account_balance = levm_account_info.balance, + revm_account_balance = revm_account_info.balance + ) + .red() + .bold() + )?; + } + if levm_account_info.nonce != revm_account_info.nonce { + writeln!(f, + "{}", + format!( + " Nonce mismatch: LEVM: {levm_account_nonce}, REVM: {revm_account_nonce}", + levm_account_nonce = levm_account_info.nonce, + revm_account_nonce = revm_account_info.nonce + ) + .red() + .bold() + )?; + } + } + // We ignore the case (Some(_), None) because we always add the account info to the account update. + (Some(_), None) | (None, None) => {} + } + } Ok(()) } } + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct TestReRunExecutionReport { + pub execution_result_mismatch: Option<(TxResult, RevmExecutionResult)>, + pub gas_used_mismatch: Option<(u64, u64)>, + pub gas_refunded_mismatch: Option<(u64, u64)>, + pub re_runner_error: Option<(TxResult, String)>, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct TestReRunReport { + pub execution_report: HashMap, + pub account_updates_report: HashMap, +} + +impl TestReRunReport { + pub fn new() -> Self { + Self::default() + } + + pub fn register_execution_result_mismatch( + &mut self, + vector: TestVector, + levm_result: TxResult, + revm_result: RevmExecutionResult, + ) { + let value = Some((levm_result, revm_result)); + self.execution_report + .entry(vector) + .and_modify(|report| { + report.execution_result_mismatch = value.clone(); + }) + .or_insert(TestReRunExecutionReport { + execution_result_mismatch: value, + ..Default::default() + }); + } + + pub fn register_gas_used_mismatch( + &mut self, + vector: TestVector, + levm_gas_used: u64, + revm_gas_used: u64, + ) { + let value = Some((levm_gas_used, revm_gas_used)); + self.execution_report + .entry(vector) + .and_modify(|report| { + report.gas_used_mismatch = value; + }) + .or_insert(TestReRunExecutionReport { + gas_used_mismatch: value, + ..Default::default() + }); + } + + pub fn register_gas_refunded_mismatch( + &mut self, + vector: TestVector, + levm_gas_refunded: u64, + revm_gas_refunded: u64, + ) { + let value = Some((levm_gas_refunded, revm_gas_refunded)); + self.execution_report + .entry(vector) + .and_modify(|report| { + report.gas_refunded_mismatch = value; + }) + .or_insert(TestReRunExecutionReport { + gas_refunded_mismatch: value, + ..Default::default() + }); + } + + pub fn register_account_updates_report( + &mut self, + vector: TestVector, + report: AccountUpdatesReport, + ) { + self.account_updates_report.insert(vector, report); + } + + pub fn register_re_run_failure( + &mut self, + vector: TestVector, + levm_result: TxResult, + revm_error: EVMError, + ) { + let value = Some((levm_result, revm_error.to_string())); + self.execution_report + .entry(vector) + .and_modify(|report| { + report.re_runner_error = value.clone(); + }) + .or_insert(TestReRunExecutionReport { + re_runner_error: value, + ..Default::default() + }); + } +} diff --git a/cmd/ef_tests/levm/runner.rs b/cmd/ef_tests/levm/runner.rs deleted file mode 100644 index d3b4eaa95..000000000 --- a/cmd/ef_tests/levm/runner.rs +++ /dev/null @@ -1,289 +0,0 @@ -use crate::{report::EFTestsReport, types::EFTest, utils}; -use ethrex_core::{ - types::{code_hash, AccountInfo}, - H256, U256, -}; -use ethrex_levm::{ - db::Cache, - errors::{TransactionReport, VMError}, - vm::VM, - Environment, -}; -use ethrex_storage::AccountUpdate; -use ethrex_vm::db::StoreWrapper; -use keccak_hash::keccak; -use spinoff::{spinners::Dots, Color, Spinner}; -use std::{collections::HashMap, error::Error, sync::Arc}; - -pub fn run_ef_tests() -> Result> { - let mut report = EFTestsReport::default(); - let cargo_manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let ef_general_state_tests_path = cargo_manifest_dir.join("vectors/GeneralStateTests"); - let mut spinner = Spinner::new(Dots, report.progress(), Color::Cyan); - for test_dir in std::fs::read_dir(ef_general_state_tests_path)?.flatten() { - for test in std::fs::read_dir(test_dir.path())? - .flatten() - .filter(|entry| { - entry - .path() - .extension() - .map(|ext| ext == "json") - .unwrap_or(false) - }) - { - // TODO: Figure out what to do with overflowed value: 0x10000000000000000000000000000000000000000000000000000000000000001. - // Deserialization fails because the value is too big for U256. - if test - .path() - .file_name() - .is_some_and(|name| name == "ValueOverflowParis.json") - { - continue; - } - let test_result = run_ef_test( - serde_json::from_reader(std::fs::File::open(test.path())?)?, - &mut report, - ); - if test_result.is_err() { - continue; - } - } - spinner.update_text(report.progress()); - } - spinner.success(&report.progress()); - let mut spinner = Spinner::new(Dots, "Loading report...".to_owned(), Color::Cyan); - spinner.success(&report.to_string()); - Ok(report) -} - -pub fn run_ef_test_tx( - tx_id: usize, - test: &EFTest, - report: &mut EFTestsReport, -) -> Result<(), Box> { - let mut evm = prepare_vm_for_tx(tx_id, test)?; - ensure_pre_state(&evm, test)?; - let execution_result = evm.transact(); - ensure_post_state(execution_result, test, report)?; - Ok(()) -} - -pub fn run_ef_test(test: EFTest, report: &mut EFTestsReport) -> Result<(), Box> { - let mut failed = false; - for (tx_id, (tx_indexes, _tx)) in test.transactions.iter().enumerate() { - match run_ef_test_tx(tx_id, &test, report) { - Ok(_) => {} - Err(e) => { - failed = true; - let error_message: &str = &e.to_string(); - report.register_fail(tx_indexes.to_owned(), &test.name, error_message); - } - } - } - if failed { - report.register_group_fail(); - } else { - report.register_group_pass(); - } - Ok(()) -} - -pub fn prepare_vm_for_tx(tx_id: usize, test: &EFTest) -> Result> { - let (initial_state, block_hash) = utils::load_initial_state(test); - let db = Arc::new(StoreWrapper { - store: initial_state.database().unwrap().clone(), - block_hash, - }); - let vm_result = VM::new( - test.transactions.get(tx_id).unwrap().1.to.clone(), - Environment { - origin: test.transactions.get(tx_id).unwrap().1.sender, - consumed_gas: U256::default(), - refunded_gas: U256::default(), - gas_limit: test.env.current_gas_limit, - block_number: test.env.current_number, - coinbase: test.env.current_coinbase, - timestamp: test.env.current_timestamp, - prev_randao: Some(test.env.current_random), - chain_id: U256::from(1729), - base_fee_per_gas: test.env.current_base_fee, - gas_price: test - .transactions - .get(tx_id) - .unwrap() - .1 - .gas_price - .unwrap_or_default(), // or max_fee_per_gas? - block_excess_blob_gas: Some(test.env.current_excess_blob_gas), - block_blob_gas_used: None, - tx_blob_hashes: None, - }, - test.transactions.get(tx_id).unwrap().1.value, - test.transactions.get(tx_id).unwrap().1.data.clone(), - db, - Cache::default(), - ); - - match vm_result { - Ok(vm) => Ok(vm), - Err(err) => { - let error_reason = format!("VM initialization failed: {err:?}"); - Err(error_reason.into()) - } - } -} - -pub fn ensure_pre_state(evm: &VM, test: &EFTest) -> Result<(), Box> { - let world_state = &evm.db; - for (address, pre_value) in &test.pre.0 { - let account = world_state.get_account_info(*address); - ensure_pre_state_condition( - account.nonce == pre_value.nonce.as_u64(), - format!( - "Nonce mismatch for account {:#x}: expected {}, got {}", - address, pre_value.nonce, account.nonce - ), - )?; - ensure_pre_state_condition( - account.balance == pre_value.balance, - format!( - "Balance mismatch for account {:#x}: expected {}, got {}", - address, pre_value.balance, account.balance - ), - )?; - for (k, v) in &pre_value.storage { - let mut key_bytes = [0u8; 32]; - k.to_big_endian(&mut key_bytes); - let storage_slot = world_state.get_storage_slot(*address, H256::from_slice(&key_bytes)); - ensure_pre_state_condition( - &storage_slot == v, - format!( - "Storage slot mismatch for account {:#x} at key {:?}: expected {}, got {}", - address, k, v, storage_slot - ), - )?; - } - ensure_pre_state_condition( - keccak(account.bytecode.clone()) == keccak(pre_value.code.as_ref()), - format!( - "Code hash mismatch for account {:#x}: expected {}, got {}", - address, - keccak(pre_value.code.as_ref()), - keccak(account.bytecode) - ), - )?; - } - Ok(()) -} - -fn ensure_pre_state_condition(condition: bool, error_reason: String) -> Result<(), Box> { - if !condition { - let error_reason = format!("Pre-state condition failed: {error_reason}"); - return Err(error_reason.into()); - } - Ok(()) -} - -pub fn ensure_post_state( - execution_result: Result, - test: &EFTest, - report: &mut EFTestsReport, -) -> Result<(), Box> { - match execution_result { - Ok(execution_report) => { - match test - .post - .clone() - .values() - .first() - .map(|v| v.clone().expect_exception) - { - // Execution result was successful but an exception was expected. - Some(Some(expected_exception)) => { - let error_reason = format!("Expected exception: {expected_exception}"); - return Err(format!("Post-state condition failed: {error_reason}").into()); - } - // Execution result was successful and no exception was expected. - // TODO: Check that the post-state matches the expected post-state. - None | Some(None) => { - let pos_state_root = post_state_root(execution_report, test); - let expected_post_state_value = test.post.iter().next().cloned(); - if let Some(expected_post_state_root_hash) = expected_post_state_value { - let expected_post_state_root_hash = expected_post_state_root_hash.hash; - if expected_post_state_root_hash != pos_state_root { - let error_reason = format!( - "Post-state root mismatch: expected {expected_post_state_root_hash:#x}, got {pos_state_root:#x}", - ); - return Err( - format!("Post-state condition failed: {error_reason}").into() - ); - } - } else { - let error_reason = "No post-state root hash provided"; - return Err(format!("Post-state condition failed: {error_reason}").into()); - } - } - } - } - Err(err) => { - match test - .post - .clone() - .values() - .first() - .map(|v| v.clone().expect_exception) - { - // Execution result was unsuccessful and an exception was expected. - // TODO: Check that the exception matches the expected exception. - Some(Some(_expected_exception)) => {} - // Execution result was unsuccessful but no exception was expected. - None | Some(None) => { - let error_reason = format!("Unexpected exception: {err:?}"); - return Err(format!("Post-state condition failed: {error_reason}").into()); - } - } - } - }; - report.register_pass(&test.name); - Ok(()) -} - -pub fn post_state_root(execution_report: TransactionReport, test: &EFTest) -> H256 { - let (initial_state, block_hash) = utils::load_initial_state(test); - - let mut account_updates: Vec = vec![]; - for (address, account) in execution_report.new_state { - let mut added_storage = HashMap::new(); - - for (key, value) in account.storage { - added_storage.insert(key, value.current_value); - } - - let code = if account.info.bytecode.is_empty() { - None - } else { - Some(account.info.bytecode.clone()) - }; - - let account_update = AccountUpdate { - address, - removed: false, - info: Some(AccountInfo { - code_hash: code_hash(&account.info.bytecode), - balance: account.info.balance, - nonce: account.info.nonce, - }), - code, - added_storage, - }; - - account_updates.push(account_update); - } - - initial_state - .database() - .unwrap() - .apply_account_updates(block_hash, &account_updates) - .unwrap() - .unwrap() -} diff --git a/cmd/ef_tests/levm/runner/levm_runner.rs b/cmd/ef_tests/levm/runner/levm_runner.rs new file mode 100644 index 000000000..4cf9ee66b --- /dev/null +++ b/cmd/ef_tests/levm/runner/levm_runner.rs @@ -0,0 +1,247 @@ +use crate::{ + report::{EFTestReport, TestVector}, + runner::{EFTestRunnerError, InternalError}, + types::EFTest, + utils, +}; +use ethrex_core::{ + types::{code_hash, AccountInfo}, + H256, U256, +}; +use ethrex_levm::{ + db::Cache, + errors::{TransactionReport, VMError}, + vm::VM, + Environment, +}; +use ethrex_storage::AccountUpdate; +use ethrex_vm::db::StoreWrapper; +use keccak_hash::keccak; +use std::{collections::HashMap, sync::Arc}; + +pub fn run_ef_test(test: &EFTest) -> Result { + let mut ef_test_report = EFTestReport::new( + test.name.clone(), + test._info.generated_test_hash, + test.fork(), + ); + for (vector, _tx) in test.transactions.iter() { + match run_ef_test_tx(vector, test) { + Ok(_) => continue, + Err(EFTestRunnerError::VMInitializationFailed(reason)) => { + ef_test_report.register_vm_initialization_failure(reason, *vector); + } + Err(EFTestRunnerError::FailedToEnsurePreState(reason)) => { + ef_test_report.register_pre_state_validation_failure(reason, *vector); + } + Err(EFTestRunnerError::ExecutionFailedUnexpectedly(error)) => { + ef_test_report.register_unexpected_execution_failure(error, *vector); + } + Err(EFTestRunnerError::FailedToEnsurePostState(transaction_report, reason)) => { + ef_test_report.register_post_state_validation_failure( + transaction_report, + reason, + *vector, + ); + } + Err(EFTestRunnerError::VMExecutionMismatch(_)) => { + return Err(EFTestRunnerError::Internal(InternalError::FirstRunInternal( + "VM execution mismatch errors should only happen when running with revm. This failed during levm's execution." + .to_owned(), + ))); + } + Err(EFTestRunnerError::Internal(reason)) => { + return Err(EFTestRunnerError::Internal(reason)); + } + } + } + Ok(ef_test_report) +} + +pub fn run_ef_test_tx(vector: &TestVector, test: &EFTest) -> Result<(), EFTestRunnerError> { + let mut levm = prepare_vm_for_tx(vector, test)?; + ensure_pre_state(&levm, test)?; + let levm_execution_result = levm.transact(); + ensure_post_state(&levm_execution_result, vector, test)?; + Ok(()) +} + +pub fn prepare_vm_for_tx(vector: &TestVector, test: &EFTest) -> Result { + let (initial_state, block_hash) = utils::load_initial_state(test); + let db = Arc::new(StoreWrapper { + store: initial_state.database().unwrap().clone(), + block_hash, + }); + VM::new( + test.transactions.get(vector).unwrap().to.clone(), + Environment { + origin: test.transactions.get(vector).unwrap().sender, + consumed_gas: U256::default(), + refunded_gas: U256::default(), + gas_limit: test.env.current_gas_limit, + block_number: test.env.current_number, + coinbase: test.env.current_coinbase, + timestamp: test.env.current_timestamp, + prev_randao: test.env.current_random, + chain_id: U256::from(1729), + base_fee_per_gas: test.env.current_base_fee.unwrap_or_default(), + gas_price: test + .transactions + .get(vector) + .unwrap() + .gas_price + .unwrap_or_default(), // or max_fee_per_gas? + block_excess_blob_gas: test.env.current_excess_blob_gas, + block_blob_gas_used: None, + tx_blob_hashes: None, + }, + test.transactions.get(vector).unwrap().value, + test.transactions.get(vector).unwrap().data.clone(), + db, + Cache::default(), + ) + .map_err(|err| EFTestRunnerError::VMInitializationFailed(err.to_string())) +} + +pub fn ensure_pre_state(evm: &VM, test: &EFTest) -> Result<(), EFTestRunnerError> { + let world_state = &evm.db; + for (address, pre_value) in &test.pre.0 { + let account = world_state.get_account_info(*address); + ensure_pre_state_condition( + account.nonce == pre_value.nonce.as_u64(), + format!( + "Nonce mismatch for account {:#x}: expected {}, got {}", + address, pre_value.nonce, account.nonce + ), + )?; + ensure_pre_state_condition( + account.balance == pre_value.balance, + format!( + "Balance mismatch for account {:#x}: expected {}, got {}", + address, pre_value.balance, account.balance + ), + )?; + for (k, v) in &pre_value.storage { + let mut key_bytes = [0u8; 32]; + k.to_big_endian(&mut key_bytes); + let storage_slot = world_state.get_storage_slot(*address, H256::from_slice(&key_bytes)); + ensure_pre_state_condition( + &storage_slot == v, + format!( + "Storage slot mismatch for account {:#x} at key {:?}: expected {}, got {}", + address, k, v, storage_slot + ), + )?; + } + ensure_pre_state_condition( + keccak(account.bytecode.clone()) == keccak(pre_value.code.as_ref()), + format!( + "Code hash mismatch for account {:#x}: expected {}, got {}", + address, + keccak(pre_value.code.as_ref()), + keccak(account.bytecode) + ), + )?; + } + Ok(()) +} + +fn ensure_pre_state_condition( + condition: bool, + error_reason: String, +) -> Result<(), EFTestRunnerError> { + if !condition { + return Err(EFTestRunnerError::FailedToEnsurePreState(error_reason)); + } + Ok(()) +} + +pub fn ensure_post_state( + levm_execution_result: &Result, + vector: &TestVector, + test: &EFTest, +) -> Result<(), EFTestRunnerError> { + match levm_execution_result { + Ok(execution_report) => { + match test.post.vector_post_value(vector).expect_exception { + // Execution result was successful but an exception was expected. + Some(expected_exception) => { + let error_reason = format!("Expected exception: {expected_exception}"); + return Err(EFTestRunnerError::FailedToEnsurePostState( + execution_report.clone(), + error_reason, + )); + } + // Execution result was successful and no exception was expected. + None => { + let levm_account_updates = get_state_transitions(execution_report); + let pos_state_root = post_state_root(&levm_account_updates, test); + let expected_post_state_root_hash = test.post.vector_post_value(vector).hash; + if expected_post_state_root_hash != pos_state_root { + let error_reason = format!( + "Post-state root mismatch: expected {expected_post_state_root_hash:#x}, got {pos_state_root:#x}", + ); + return Err(EFTestRunnerError::FailedToEnsurePostState( + execution_report.clone(), + error_reason, + )); + } + } + } + } + Err(err) => { + match test.post.vector_post_value(vector).expect_exception { + // Execution result was unsuccessful and an exception was expected. + // TODO: Check that the exception matches the expected exception. + Some(_expected_exception) => {} + // Execution result was unsuccessful but no exception was expected. + None => { + return Err(EFTestRunnerError::ExecutionFailedUnexpectedly(err.clone())); + } + } + } + }; + Ok(()) +} + +pub fn get_state_transitions(execution_report: &TransactionReport) -> Vec { + let mut account_updates: Vec = vec![]; + for (address, account) in &execution_report.new_state { + let mut added_storage = HashMap::new(); + + for (key, value) in &account.storage { + added_storage.insert(*key, value.current_value); + } + + let code = if account.info.bytecode.is_empty() { + None + } else { + Some(account.info.bytecode.clone()) + }; + + let account_update = AccountUpdate { + address: *address, + removed: false, + info: Some(AccountInfo { + code_hash: code_hash(&account.info.bytecode), + balance: account.info.balance, + nonce: account.info.nonce, + }), + code, + added_storage, + }; + + account_updates.push(account_update); + } + account_updates +} + +pub fn post_state_root(account_updates: &[AccountUpdate], test: &EFTest) -> H256 { + let (initial_state, block_hash) = utils::load_initial_state(test); + initial_state + .database() + .unwrap() + .apply_account_updates(block_hash, account_updates) + .unwrap() + .unwrap() +} diff --git a/cmd/ef_tests/levm/runner/mod.rs b/cmd/ef_tests/levm/runner/mod.rs new file mode 100644 index 000000000..95957393f --- /dev/null +++ b/cmd/ef_tests/levm/runner/mod.rs @@ -0,0 +1,156 @@ +use crate::{ + report::{self, format_duration_as_mm_ss, EFTestReport, TestReRunReport}, + types::EFTest, +}; +use clap::Parser; +use colored::Colorize; +use ethrex_levm::errors::{TransactionReport, VMError}; +use ethrex_vm::SpecId; +use serde::{Deserialize, Serialize}; +use spinoff::{spinners::Dots, Color, Spinner}; + +pub mod levm_runner; +pub mod revm_runner; + +#[derive(Debug, thiserror::Error, Clone, Serialize, Deserialize)] +pub enum EFTestRunnerError { + #[error("VM initialization failed: {0}")] + VMInitializationFailed(String), + #[error("Transaction execution failed when it was not expected to fail: {0}")] + ExecutionFailedUnexpectedly(VMError), + #[error("Failed to ensure post-state: {0}")] + FailedToEnsurePreState(String), + #[error("Failed to ensure post-state: {1}")] + FailedToEnsurePostState(TransactionReport, String), + #[error("VM run mismatch: {0}")] + VMExecutionMismatch(String), + #[error("This is a bug: {0}")] + Internal(#[from] InternalError), +} + +#[derive(Debug, thiserror::Error, Clone, Serialize, Deserialize)] +pub enum InternalError { + #[error("First run failed unexpectedly: {0}")] + FirstRunInternal(String), + #[error("Re-runner failed unexpectedly: {0}")] + ReRunInternal(String, TestReRunReport), + #[error("Main runner failed unexpectedly: {0}")] + MainRunnerInternal(String), +} + +#[derive(Parser)] +pub struct EFTestRunnerOptions { + #[arg(short, long, value_name = "FORK", default_value = "Cancun")] + pub fork: Vec, + #[arg(short, long, value_name = "TESTS")] + pub tests: Vec, +} + +pub fn run_ef_tests( + ef_tests: Vec, + _opts: &EFTestRunnerOptions, +) -> Result<(), EFTestRunnerError> { + let mut reports = report::load()?; + if reports.is_empty() { + run_with_levm(&mut reports, &ef_tests)?; + } + re_run_with_revm(&mut reports, &ef_tests)?; + write_report(&reports) +} + +fn run_with_levm( + reports: &mut Vec, + ef_tests: &[EFTest], +) -> Result<(), EFTestRunnerError> { + let levm_run_time = std::time::Instant::now(); + let mut levm_run_spinner = Spinner::new( + Dots, + report::progress(reports, levm_run_time.elapsed()), + Color::Cyan, + ); + for test in ef_tests.iter() { + let ef_test_report = match levm_runner::run_ef_test(test) { + Ok(ef_test_report) => ef_test_report, + Err(EFTestRunnerError::Internal(err)) => return Err(EFTestRunnerError::Internal(err)), + non_internal_errors => { + return Err(EFTestRunnerError::Internal(InternalError::FirstRunInternal(format!( + "Non-internal error raised when executing levm. This should not happen: {non_internal_errors:?}", + )))) + } + }; + reports.push(ef_test_report); + levm_run_spinner.update_text(report::progress(reports, levm_run_time.elapsed())); + } + levm_run_spinner.success(&report::progress(reports, levm_run_time.elapsed())); + + let mut summary_spinner = Spinner::new(Dots, "Loading summary...".to_owned(), Color::Cyan); + summary_spinner.success(&report::summary(reports)); + Ok(()) +} + +fn re_run_with_revm( + reports: &mut [EFTestReport], + ef_tests: &[EFTest], +) -> Result<(), EFTestRunnerError> { + let revm_run_time = std::time::Instant::now(); + let mut revm_run_spinner = Spinner::new( + Dots, + "Running failed tests with REVM...".to_owned(), + Color::Cyan, + ); + let failed_tests = reports.iter().filter(|report| !report.passed()).count(); + for (idx, failed_test_report) in reports.iter_mut().enumerate() { + if failed_test_report.passed() { + continue; + } + revm_run_spinner.update_text(format!( + "{} {}/{failed_tests} - {}", + "Re-running failed tests with REVM".bold(), + idx + 1, + format_duration_as_mm_ss(revm_run_time.elapsed()) + )); + match revm_runner::re_run_failed_ef_test( + ef_tests + .iter() + .find(|test| test._info.generated_test_hash == failed_test_report.test_hash) + .unwrap(), + failed_test_report, + ) { + Ok(re_run_report) => { + failed_test_report.register_re_run_report(re_run_report.clone()); + } + Err(EFTestRunnerError::Internal(InternalError::ReRunInternal(reason, re_run_report))) => { + write_report(reports)?; + cache_re_run(reports)?; + return Err(EFTestRunnerError::Internal(InternalError::ReRunInternal( + reason, + re_run_report, + ))) + }, + non_re_run_internal_errors => { + return Err(EFTestRunnerError::Internal(InternalError::MainRunnerInternal(format!( + "Non-internal error raised when executing revm. This should not happen: {non_re_run_internal_errors:?}" + )))) + } + } + } + revm_run_spinner.success(&format!( + "Re-ran failed tests with REVM in {}", + format_duration_as_mm_ss(revm_run_time.elapsed()) + )); + Ok(()) +} + +fn write_report(reports: &[EFTestReport]) -> Result<(), EFTestRunnerError> { + let mut report_spinner = Spinner::new(Dots, "Loading report...".to_owned(), Color::Cyan); + let report_file_path = report::write(reports)?; + report_spinner.success(&format!("Report written to file {report_file_path:?}").bold()); + Ok(()) +} + +fn cache_re_run(reports: &[EFTestReport]) -> Result<(), EFTestRunnerError> { + let mut cache_spinner = Spinner::new(Dots, "Caching re-run...".to_owned(), Color::Cyan); + let cache_file_path = report::cache(reports)?; + cache_spinner.success(&format!("Re-run cached to file {cache_file_path:?}").bold()); + Ok(()) +} diff --git a/cmd/ef_tests/levm/runner/revm_runner.rs b/cmd/ef_tests/levm/runner/revm_runner.rs new file mode 100644 index 000000000..9594d6d9d --- /dev/null +++ b/cmd/ef_tests/levm/runner/revm_runner.rs @@ -0,0 +1,286 @@ +use crate::{ + report::{AccountUpdatesReport, EFTestReport, TestReRunReport, TestVector}, + runner::{levm_runner, EFTestRunnerError, InternalError}, + types::EFTest, + utils::load_initial_state, +}; +use ethrex_core::{types::TxKind, Address}; +use ethrex_levm::errors::{TransactionReport, TxResult}; +use ethrex_storage::{error::StoreError, AccountUpdate}; +use ethrex_vm::{db::StoreWrapper, spec_id, EvmState, RevmAddress, RevmU256}; +use revm::{ + db::State, + inspectors::TracerEip3155 as RevmTracerEip3155, + primitives::{ + BlobExcessGasAndPrice, BlockEnv as RevmBlockEnv, EVMError as REVMError, + ExecutionResult as RevmExecutionResult, TxEnv as RevmTxEnv, TxKind as RevmTxKind, + }, + Evm as Revm, +}; +use std::collections::HashSet; + +pub fn re_run_failed_ef_test( + test: &EFTest, + failed_test_report: &EFTestReport, +) -> Result { + assert_eq!(test.name, failed_test_report.name); + let mut re_run_report = TestReRunReport::new(); + for (vector, vector_failure) in failed_test_report.failed_vectors.iter() { + match vector_failure { + // We only want to re-run tests that failed in the post-state validation. + EFTestRunnerError::FailedToEnsurePostState(transaction_report, _) => { + match re_run_failed_ef_test_tx(vector, test, transaction_report, &mut re_run_report) { + Ok(_) => continue, + Err(EFTestRunnerError::VMInitializationFailed(reason)) => { + return Err(EFTestRunnerError::Internal(InternalError::ReRunInternal( + format!("REVM initialization failed when re-running failed test: {reason}"), re_run_report.clone() + ))); + } + Err(EFTestRunnerError::Internal(reason)) => { + return Err(EFTestRunnerError::Internal(reason)); + } + unexpected_error => { + return Err(EFTestRunnerError::Internal(InternalError::ReRunInternal(format!( + "Unexpected error when re-running failed test: {unexpected_error:?}" + ), re_run_report.clone()))); + } + } + }, + EFTestRunnerError::VMInitializationFailed(_) + | EFTestRunnerError::ExecutionFailedUnexpectedly(_) + | EFTestRunnerError::FailedToEnsurePreState(_) => continue, + EFTestRunnerError::VMExecutionMismatch(reason) => return Err(EFTestRunnerError::Internal(InternalError::ReRunInternal( + format!("VM execution mismatch errors should only happen when running with revm. This failed during levm's execution: {reason}"), re_run_report.clone()))), + EFTestRunnerError::Internal(reason) => return Err(EFTestRunnerError::Internal(reason.to_owned())), + } + } + Ok(re_run_report) +} + +pub fn re_run_failed_ef_test_tx( + vector: &TestVector, + test: &EFTest, + levm_execution_report: &TransactionReport, + re_run_report: &mut TestReRunReport, +) -> Result<(), EFTestRunnerError> { + let (mut state, _block_hash) = load_initial_state(test); + let mut revm = prepare_revm_for_tx(&mut state, vector, test)?; + let revm_execution_result = revm.transact_commit(); + drop(revm); // Need to drop the state mutable reference. + compare_levm_revm_execution_results( + vector, + levm_execution_report, + revm_execution_result, + re_run_report, + )?; + ensure_post_state( + levm_execution_report, + vector, + &mut state, + test, + re_run_report, + )?; + Ok(()) +} + +pub fn prepare_revm_for_tx<'state>( + initial_state: &'state mut EvmState, + vector: &TestVector, + test: &EFTest, +) -> Result>, EFTestRunnerError> { + let chain_spec = initial_state + .chain_config() + .map_err(|err| EFTestRunnerError::VMInitializationFailed(err.to_string()))?; + let block_env = RevmBlockEnv { + number: RevmU256::from_limbs(test.env.current_number.0), + coinbase: RevmAddress(test.env.current_coinbase.0.into()), + timestamp: RevmU256::from_limbs(test.env.current_timestamp.0), + gas_limit: RevmU256::from_limbs(test.env.current_gas_limit.0), + basefee: RevmU256::from_limbs(test.env.current_base_fee.unwrap_or_default().0), + difficulty: RevmU256::from_limbs(test.env.current_difficulty.0), + prevrandao: test.env.current_random.map(|v| v.0.into()), + blob_excess_gas_and_price: Some(BlobExcessGasAndPrice::new( + test.env + .current_excess_blob_gas + .unwrap_or_default() + .as_u64(), + )), + }; + let tx = &test + .transactions + .get(vector) + .ok_or(EFTestRunnerError::VMInitializationFailed(format!( + "Vector {vector:?} not found in test {}", + test.name + )))?; + let tx_env = RevmTxEnv { + caller: tx.sender.0.into(), + gas_limit: tx.gas_limit.as_u64(), + gas_price: RevmU256::from_limbs(tx.gas_price.unwrap_or_default().0), + transact_to: match tx.to { + TxKind::Call(to) => RevmTxKind::Call(to.0.into()), + TxKind::Create => RevmTxKind::Create, + }, + value: RevmU256::from_limbs(tx.value.0), + data: tx.data.to_vec().into(), + nonce: Some(tx.nonce.as_u64()), + chain_id: None, + access_list: Vec::default(), + gas_priority_fee: None, + blob_hashes: Vec::default(), + max_fee_per_blob_gas: None, + authorization_list: None, + }; + let evm_builder = Revm::builder() + .with_block_env(block_env) + .with_tx_env(tx_env) + .modify_cfg_env(|cfg| cfg.chain_id = chain_spec.chain_id) + .with_spec_id(spec_id(&chain_spec, test.env.current_timestamp.as_u64())) + .with_external_context( + RevmTracerEip3155::new(Box::new(std::io::stderr())).without_summary(), + ); + match initial_state { + EvmState::Store(db) => Ok(evm_builder.with_db(db).build()), + _ => Err(EFTestRunnerError::VMInitializationFailed( + "Expected LEVM state to be a Store".to_owned(), + )), + } +} + +pub fn compare_levm_revm_execution_results( + vector: &TestVector, + levm_execution_report: &TransactionReport, + revm_execution_result: Result>, + re_run_report: &mut TestReRunReport, +) -> Result<(), EFTestRunnerError> { + match (levm_execution_report, revm_execution_result) { + (levm_tx_report, Ok(revm_execution_result)) => { + match (&levm_tx_report.result, revm_execution_result.clone()) { + ( + TxResult::Success, + RevmExecutionResult::Success { + reason: _, + gas_used: revm_gas_used, + gas_refunded: revm_gas_refunded, + logs: _, + output: _, + }, + ) => { + if levm_tx_report.gas_used != revm_gas_used { + re_run_report.register_gas_used_mismatch( + *vector, + levm_tx_report.gas_used, + revm_gas_used, + ); + } + if levm_tx_report.gas_refunded != revm_gas_refunded { + re_run_report.register_gas_refunded_mismatch( + *vector, + levm_tx_report.gas_refunded, + revm_gas_refunded, + ); + } + } + ( + TxResult::Revert(_error), + RevmExecutionResult::Revert { + gas_used: revm_gas_used, + output: _, + }, + ) => { + if levm_tx_report.gas_used != revm_gas_used { + re_run_report.register_gas_used_mismatch( + *vector, + levm_tx_report.gas_used, + revm_gas_used, + ); + } + } + ( + TxResult::Revert(_error), + RevmExecutionResult::Halt { + reason: _, + gas_used: revm_gas_used, + }, + ) => { + // TODO: Register the revert reasons. + if levm_tx_report.gas_used != revm_gas_used { + re_run_report.register_gas_used_mismatch( + *vector, + levm_tx_report.gas_used, + revm_gas_used, + ); + } + } + _ => { + re_run_report.register_execution_result_mismatch( + *vector, + levm_tx_report.result.clone(), + revm_execution_result.clone(), + ); + } + } + } + (levm_transaction_report, Err(revm_error)) => { + re_run_report.register_re_run_failure( + *vector, + levm_transaction_report.result.clone(), + revm_error, + ); + } + } + Ok(()) +} + +pub fn ensure_post_state( + levm_execution_report: &TransactionReport, + vector: &TestVector, + revm_state: &mut EvmState, + test: &EFTest, + re_run_report: &mut TestReRunReport, +) -> Result<(), EFTestRunnerError> { + match test.post.vector_post_value(vector).expect_exception { + Some(_expected_exception) => {} + // We only want to compare account updates when no exception is expected. + None => { + let levm_account_updates = levm_runner::get_state_transitions(levm_execution_report); + let revm_account_updates = ethrex_vm::get_state_transitions(revm_state); + let account_updates_report = + compare_levm_revm_account_updates(&levm_account_updates, &revm_account_updates); + re_run_report.register_account_updates_report(*vector, account_updates_report); + } + } + + Ok(()) +} + +pub fn compare_levm_revm_account_updates( + levm_account_updates: &[AccountUpdate], + revm_account_updates: &[AccountUpdate], +) -> AccountUpdatesReport { + let levm_updated_accounts = levm_account_updates + .iter() + .map(|account_update| account_update.address) + .collect::>(); + let revm_updated_accounts = revm_account_updates + .iter() + .map(|account_update| account_update.address) + .collect::>(); + + AccountUpdatesReport { + levm_account_updates: levm_account_updates.to_vec(), + revm_account_updates: revm_account_updates.to_vec(), + levm_updated_accounts_only: levm_updated_accounts + .difference(&revm_updated_accounts) + .cloned() + .collect::>(), + revm_updated_accounts_only: revm_updated_accounts + .difference(&levm_updated_accounts) + .cloned() + .collect::>(), + shared_updated_accounts: levm_updated_accounts + .intersection(&revm_updated_accounts) + .cloned() + .collect::>(), + } +} diff --git a/cmd/ef_tests/levm/tests/ef_tests_levm.rs b/cmd/ef_tests/levm/tests/ef_tests_levm.rs new file mode 100644 index 000000000..bc7bd00cb --- /dev/null +++ b/cmd/ef_tests/levm/tests/ef_tests_levm.rs @@ -0,0 +1,13 @@ +use clap::Parser; +use ef_tests_levm::{ + parser, + runner::{self, EFTestRunnerOptions}, +}; +use std::error::Error; + +fn main() -> Result<(), Box> { + let opts = EFTestRunnerOptions::parse(); + let ef_tests = parser::parse_ef_tests(&opts)?; + runner::run_ef_tests(ef_tests, &opts)?; + Ok(()) +} diff --git a/cmd/ef_tests/levm/tests/test.rs b/cmd/ef_tests/levm/tests/test.rs deleted file mode 100644 index e726edb91..000000000 --- a/cmd/ef_tests/levm/tests/test.rs +++ /dev/null @@ -1,6 +0,0 @@ -use ef_tests_levm::runner; - -fn main() { - let report = runner::run_ef_tests().unwrap(); - println!("{report}"); -} diff --git a/cmd/ef_tests/levm/types.rs b/cmd/ef_tests/levm/types.rs index 9721b1e60..be399ca5e 100644 --- a/cmd/ef_tests/levm/types.rs +++ b/cmd/ef_tests/levm/types.rs @@ -1,16 +1,23 @@ -use crate::deserialize::{ - deserialize_ef_post_value_indexes, deserialize_hex_bytes, deserialize_hex_bytes_vec, - deserialize_u256_optional_safe, deserialize_u256_safe, deserialize_u256_valued_hashmap_safe, - deserialize_u256_vec_safe, +use crate::{ + deserialize::{ + deserialize_ef_post_value_indexes, deserialize_hex_bytes, deserialize_hex_bytes_vec, + deserialize_u256_optional_safe, deserialize_u256_safe, + deserialize_u256_valued_hashmap_safe, deserialize_u256_vec_safe, + }, + report::TestVector, }; use bytes::Bytes; use ethrex_core::{ types::{Genesis, GenesisAccount, TxKind}, Address, H256, U256, }; +use ethrex_vm::SpecId; use serde::Deserialize; use std::collections::HashMap; +#[derive(Debug)] +pub struct EFTests(pub Vec); + #[derive(Debug)] pub struct EFTest { pub name: String, @@ -18,7 +25,26 @@ pub struct EFTest { pub env: EFTestEnv, pub post: EFTestPost, pub pre: EFTestPre, - pub transactions: Vec<((usize, usize, usize), EFTestTransaction)>, + pub transactions: HashMap, +} + +impl EFTest { + pub fn fork(&self) -> SpecId { + match &self.post { + EFTestPost::Cancun(_) => SpecId::CANCUN, + EFTestPost::Shanghai(_) => SpecId::SHANGHAI, + EFTestPost::Homestead(_) => SpecId::HOMESTEAD, + EFTestPost::Istanbul(_) => SpecId::ISTANBUL, + EFTestPost::London(_) => SpecId::LONDON, + EFTestPost::Byzantium(_) => SpecId::BYZANTIUM, + EFTestPost::Berlin(_) => SpecId::BERLIN, + EFTestPost::Constantinople(_) | EFTestPost::ConstantinopleFix(_) => { + SpecId::CONSTANTINOPLE + } + EFTestPost::Paris(_) => SpecId::MERGE, + EFTestPost::Frontier(_) => SpecId::FRONTIER, + } + } } impl From<&EFTest> for Genesis { @@ -34,10 +60,10 @@ impl From<&EFTest> for Genesis { coinbase: test.env.current_coinbase, difficulty: test.env.current_difficulty, gas_limit: test.env.current_gas_limit.as_u64(), - mix_hash: test.env.current_random, + mix_hash: test.env.current_random.unwrap_or_default(), timestamp: test.env.current_timestamp.as_u64(), - base_fee_per_gas: Some(test.env.current_base_fee.as_u64()), - excess_blob_gas: Some(test.env.current_excess_blob_gas.as_u64()), + base_fee_per_gas: test.env.current_base_fee.map(|v| v.as_u64()), + excess_blob_gas: test.env.current_excess_blob_gas.map(|v| v.as_u64()), ..Default::default() } } @@ -64,18 +90,18 @@ pub struct EFTestInfo { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct EFTestEnv { - #[serde(deserialize_with = "deserialize_u256_safe")] - pub current_base_fee: U256, + #[serde(default, deserialize_with = "deserialize_u256_optional_safe")] + pub current_base_fee: Option, pub current_coinbase: Address, #[serde(deserialize_with = "deserialize_u256_safe")] pub current_difficulty: U256, - #[serde(deserialize_with = "deserialize_u256_safe")] - pub current_excess_blob_gas: U256, + #[serde(default, deserialize_with = "deserialize_u256_optional_safe")] + pub current_excess_blob_gas: Option, #[serde(deserialize_with = "deserialize_u256_safe")] pub current_gas_limit: U256, #[serde(deserialize_with = "deserialize_u256_safe")] pub current_number: U256, - pub current_random: H256, + pub current_random: Option, #[serde(deserialize_with = "deserialize_u256_safe")] pub current_timestamp: U256, } @@ -83,18 +109,77 @@ pub struct EFTestEnv { #[derive(Debug, Deserialize, Clone)] pub enum EFTestPost { Cancun(Vec), + Shanghai(Vec), + Homestead(Vec), + Istanbul(Vec), + London(Vec), + Byzantium(Vec), + Berlin(Vec), + Constantinople(Vec), + Paris(Vec), + ConstantinopleFix(Vec), + Frontier(Vec), } impl EFTestPost { pub fn values(self) -> Vec { match self { EFTestPost::Cancun(v) => v, + EFTestPost::Shanghai(v) => v, + EFTestPost::Homestead(v) => v, + EFTestPost::Istanbul(v) => v, + EFTestPost::London(v) => v, + EFTestPost::Byzantium(v) => v, + EFTestPost::Berlin(v) => v, + EFTestPost::Constantinople(v) => v, + EFTestPost::Paris(v) => v, + EFTestPost::ConstantinopleFix(v) => v, + EFTestPost::Frontier(v) => v, + } + } + + pub fn vector_post_value(&self, vector: &TestVector) -> EFTestPostValue { + match self { + EFTestPost::Cancun(v) => Self::find_vector_post_value(v, vector), + EFTestPost::Shanghai(v) => Self::find_vector_post_value(v, vector), + EFTestPost::Homestead(v) => Self::find_vector_post_value(v, vector), + EFTestPost::Istanbul(v) => Self::find_vector_post_value(v, vector), + EFTestPost::London(v) => Self::find_vector_post_value(v, vector), + EFTestPost::Byzantium(v) => Self::find_vector_post_value(v, vector), + EFTestPost::Berlin(v) => Self::find_vector_post_value(v, vector), + EFTestPost::Constantinople(v) => Self::find_vector_post_value(v, vector), + EFTestPost::Paris(v) => Self::find_vector_post_value(v, vector), + EFTestPost::ConstantinopleFix(v) => Self::find_vector_post_value(v, vector), + EFTestPost::Frontier(v) => Self::find_vector_post_value(v, vector), } } + fn find_vector_post_value(values: &[EFTestPostValue], vector: &TestVector) -> EFTestPostValue { + values + .iter() + .find(|v| { + let data_index = v.indexes.get("data").unwrap().as_usize(); + let gas_limit_index = v.indexes.get("gas").unwrap().as_usize(); + let value_index = v.indexes.get("value").unwrap().as_usize(); + vector == &(data_index, gas_limit_index, value_index) + }) + .unwrap() + .clone() + } + pub fn iter(&self) -> impl Iterator { match self { EFTestPost::Cancun(v) => v.iter(), + EFTestPost::Shanghai(v) => v.iter(), + EFTestPost::Homestead(v) => v.iter(), + EFTestPost::Istanbul(v) => v.iter(), + EFTestPost::London(v) => v.iter(), + EFTestPost::Byzantium(v) => v.iter(), + EFTestPost::Berlin(v) => v.iter(), + EFTestPost::Constantinople(v) => v.iter(), + EFTestPost::Paris(v) => v.iter(), + EFTestPost::ConstantinopleFix(v) => v.iter(), + EFTestPost::Frontier(v) => v.iter(), } } } diff --git a/cmd/ef_tests/levm/utils.rs b/cmd/ef_tests/levm/utils.rs index dedb641c9..b09944b93 100644 --- a/cmd/ef_tests/levm/utils.rs +++ b/cmd/ef_tests/levm/utils.rs @@ -9,10 +9,11 @@ pub fn load_initial_state(test: &EFTest) -> (EvmState, H256) { let storage = Store::new("./temp", EngineType::InMemory).expect("Failed to create Store"); storage.add_initial_state(genesis.clone()).unwrap(); - let parent_hash = genesis.get_block().header.parent_hash; - ( - evm_state(storage.clone(), parent_hash), + evm_state( + storage.clone(), + genesis.get_block().header.compute_block_hash(), + ), genesis.get_block().header.compute_block_hash(), ) } diff --git a/crates/common/types/account.rs b/crates/common/types/account.rs index 378419199..28ac9b9bf 100644 --- a/crates/common/types/account.rs +++ b/crates/common/types/account.rs @@ -37,7 +37,7 @@ pub struct Account { pub storage: HashMap, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct AccountInfo { pub code_hash: H256, pub balance: U256, diff --git a/crates/storage/store/storage.rs b/crates/storage/store/storage.rs index 96073c485..c500093e4 100644 --- a/crates/storage/store/storage.rs +++ b/crates/storage/store/storage.rs @@ -13,6 +13,7 @@ use ethrex_core::types::{ use ethrex_rlp::decode::RLPDecode; use ethrex_rlp::encode::RLPEncode; use ethrex_trie::Trie; +use serde::{Deserialize, Serialize}; use sha3::{Digest as _, Keccak256}; use std::collections::HashMap; use std::fmt::Debug; @@ -39,7 +40,7 @@ pub enum EngineType { Libmdbx, } -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct AccountUpdate { pub address: Address, pub removed: bool, diff --git a/crates/vm/levm/src/account.rs b/crates/vm/levm/src/account.rs index da8872a92..a374ac463 100644 --- a/crates/vm/levm/src/account.rs +++ b/crates/vm/levm/src/account.rs @@ -5,9 +5,10 @@ use crate::{ use bytes::Bytes; use ethrex_core::{H256, U256}; use keccak_hash::keccak; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; -#[derive(Clone, Default, Debug, PartialEq, Eq)] +#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct AccountInfo { pub balance: U256, pub bytecode: Bytes, @@ -20,13 +21,13 @@ impl AccountInfo { } } -#[derive(Clone, Default, Debug, PartialEq, Eq)] +#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Account { pub info: AccountInfo, pub storage: HashMap, } -#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct StorageSlot { pub original_value: U256, pub current_value: U256, diff --git a/crates/vm/levm/src/errors.rs b/crates/vm/levm/src/errors.rs index b6bad188c..943f3c55b 100644 --- a/crates/vm/levm/src/errors.rs +++ b/crates/vm/levm/src/errors.rs @@ -1,11 +1,12 @@ use crate::account::Account; use bytes::Bytes; use ethrex_core::{types::Log, Address}; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use thiserror; /// Errors that halt the program -#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, Serialize, Deserialize)] pub enum VMError { #[error("Stack Underflow")] StackUnderflow, @@ -73,7 +74,7 @@ pub enum VMError { Internal(#[from] InternalError), } -#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error, Serialize, Deserialize)] pub enum OutOfGasError { #[error("Gas Cost Overflow")] GasCostOverflow, @@ -89,7 +90,7 @@ pub enum OutOfGasError { ArithmeticOperationDividedByZero, } -#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, Serialize, Deserialize)] pub enum InternalError { #[error("Overflowed when incrementing nonce")] NonceOverflowed, @@ -145,13 +146,13 @@ pub enum ResultReason { SelfDestruct, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum TxResult { Success, Revert(VMError), } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct TransactionReport { pub result: TxResult, pub new_state: HashMap, diff --git a/crates/vm/vm.rs b/crates/vm/vm.rs index e095a7554..68675a6ac 100644 --- a/crates/vm/vm.rs +++ b/crates/vm/vm.rs @@ -23,7 +23,7 @@ use revm::{ inspector_handle_register, inspectors::TracerEip3155, precompile::{PrecompileSpecId, Precompiles}, - primitives::{BlobExcessGasAndPrice, BlockEnv, TxEnv, B256, U256 as RevmU256}, + primitives::{BlobExcessGasAndPrice, BlockEnv, TxEnv, B256}, Database, DatabaseCommit, Evm, }; use revm_inspectors::access_list::AccessListInspector; @@ -35,7 +35,7 @@ use revm_primitives::{ // Export needed types pub use errors::EvmError; pub use execution_result::*; -pub use revm::primitives::{Address as RevmAddress, SpecId}; +pub use revm::primitives::{Address as RevmAddress, SpecId, U256 as RevmU256}; type AccessList = Vec<(Address, Vec)>; From 10073225884a7697f0af629b94390c1e3d4dd3cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Mon, 25 Nov 2024 16:58:17 +0100 Subject: [PATCH 13/25] feat(l1): post hive report to slack as well as summary (#1257) --- .github/workflows/hive_coverage.yaml | 26 +++++++++++++++++++++++++- cmd/hive_report/src/main.rs | 2 -- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/.github/workflows/hive_coverage.yaml b/.github/workflows/hive_coverage.yaml index 70bc4badc..8268c9c7f 100644 --- a/.github/workflows/hive_coverage.yaml +++ b/.github/workflows/hive_coverage.yaml @@ -48,4 +48,28 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Generate the hive report - run: cargo run -p hive_report >> $GITHUB_STEP_SUMMARY + id: report + run: | + cargo run -p hive_report > results.md + echo "content=$(cat results.md)" >> $GITHUB_OUTPUT + + - name: Post results in summary + run: | + echo "# Hive coverage report\n\n" >> $GITHUB_STEP_SUMMARY + $(cat results.md) >> $GITHUB_STEP_SUMMARY + + - name: Post results to slack + uses: slackapi/slack-github-action@v2.0.0 + with: + webhook: ${{ secrets.SLACK_WEBHOOK_URL }} + webhook-type: incoming-webhook + payload: | + blocks: + - type: "header" + text: + type: "plain_text" + text: Hive coverage report + - type: "section" + text: + type: "mrkdwn" + text: ${{steps.report.outputs.content}} diff --git a/cmd/hive_report/src/main.rs b/cmd/hive_report/src/main.rs index 5d7fea914..dd14e0d66 100644 --- a/cmd/hive_report/src/main.rs +++ b/cmd/hive_report/src/main.rs @@ -61,8 +61,6 @@ fn main() -> Result<(), Box> { // Sort by file name. results.sort_by(|a, b| a.0.cmp(&b.0)); - println!("# Hive coverage report\n"); - for (file_name, passed, total) in results { println!("- {}: {}/{}", file_name, passed, total); } From 1b96b4c52933bd47ef0f03cf56986def3319b956 Mon Sep 17 00:00:00 2001 From: Ivan Litteri <67517699+ilitteri@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:40:01 -0300 Subject: [PATCH 14/25] fix(levm): makefile EVM EF tests run command (#1261) --- .github/workflows/ci_levm.yaml | 4 ++-- crates/vm/levm/Makefile | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci_levm.yaml b/.github/workflows/ci_levm.yaml index f5d3a3a77..ff8d5734e 100644 --- a/.github/workflows/ci_levm.yaml +++ b/.github/workflows/ci_levm.yaml @@ -34,12 +34,12 @@ jobs: - name: Download EF Tests run: | cd crates/vm/levm - make download-ef-tests + make download-evm-ef-tests - name: Run tests run: | cd crates/vm/levm - make run-ef-tests + make run-evm-ef-tests test: name: Tests runs-on: ubuntu-latest diff --git a/crates/vm/levm/Makefile b/crates/vm/levm/Makefile index b2947f224..552c4015d 100644 --- a/crates/vm/levm/Makefile +++ b/crates/vm/levm/Makefile @@ -29,13 +29,13 @@ $(SPECTEST_VECTORS_DIR): $(SPECTEST_ARTIFACT) tar -xzf $(SPECTEST_ARTIFACT) -C tmp mv tmp/tests-14.1/GeneralStateTests $(SPECTEST_VECTORS_DIR) -download-ef-tests: $(SPECTEST_VECTORS_DIR) ## πŸ“₯ Download EF Tests +download-evm-ef-tests: $(SPECTEST_VECTORS_DIR) ## πŸ“₯ Download EF Tests -run-ef-tests: ## πŸƒβ€β™‚οΈ Run EF Tests +run-evm-ef-tests: ## πŸƒβ€β™‚οΈ Run EF Tests cd ../../../ && \ - cargo test -p ef_tests-levm --tests test + time cargo test -p ef_tests-levm --test ef_tests_levm -clean-ef-tests: ## πŸ—‘οΈ Clean test vectors +clean-evm-ef-tests: ## πŸ—‘οΈ Clean test vectors rm -rf $(SPECTEST_VECTORS_DIR) ###### Benchmarks ###### From 18fbe437f8fa1ab36c8fcffe24afcebe6037e14b Mon Sep 17 00:00:00 2001 From: Maximo Palopoli <96491141+maximopalopoli@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:22:29 -0300 Subject: [PATCH 15/25] fix(levm): return error in returndatacopy in unexpected behavior (#1244) **Motivation** Fixes a bug found by [FuzzingLabs](https://github.com/FuzzingLabs) in returndatacopy opcode implementation. **Description** Now returns an error in `returndatacopy` if offset is larger than the return data size. Closes #1232 --- .../levm/src/opcode_handlers/environment.rs | 26 +++++++++---------- crates/vm/levm/tests/edge_case_tests.rs | 10 +++++++ 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/crates/vm/levm/src/opcode_handlers/environment.rs b/crates/vm/levm/src/opcode_handlers/environment.rs index f0d7e02c8..106317472 100644 --- a/crates/vm/levm/src/opcode_handlers/environment.rs +++ b/crates/vm/levm/src/opcode_handlers/environment.rs @@ -393,19 +393,19 @@ impl VM { } let sub_return_data_len = current_call_frame.sub_return_data.len(); - let data = if returndata_offset < sub_return_data_len { - current_call_frame.sub_return_data.slice( - returndata_offset - ..(returndata_offset - .checked_add(size) - .ok_or(VMError::Internal( - InternalError::ArithmeticOperationOverflow, - ))?) - .min(sub_return_data_len), - ) - } else { - vec![0u8; size].into() - }; + + if returndata_offset >= sub_return_data_len { + return Err(VMError::VeryLargeNumber); // Maybe can create a new error instead of using this one + } + let data = current_call_frame.sub_return_data.slice( + returndata_offset + ..(returndata_offset + .checked_add(size) + .ok_or(VMError::Internal( + InternalError::ArithmeticOperationOverflow, + ))?) + .min(sub_return_data_len), + ); current_call_frame.memory.store_bytes(dest_offset, &data)?; diff --git a/crates/vm/levm/tests/edge_case_tests.rs b/crates/vm/levm/tests/edge_case_tests.rs index e406e36a3..3da16d0de 100644 --- a/crates/vm/levm/tests/edge_case_tests.rs +++ b/crates/vm/levm/tests/edge_case_tests.rs @@ -1,6 +1,7 @@ use bytes::Bytes; use ethrex_core::U256; use ethrex_levm::{ + errors::{TxResult, VMError}, operations::Operation, utils::{new_vm_with_bytecode, new_vm_with_ops}, }; @@ -114,3 +115,12 @@ fn test_sdiv_zero_dividend_and_negative_divisor() { vm.execute(&mut current_call_frame); assert_eq!(current_call_frame.stack.pop().unwrap(), U256::zero()); } + +#[test] +fn test_non_compliance_returndatacopy() { + let mut vm = + new_vm_with_bytecode(Bytes::copy_from_slice(&[56, 56, 56, 56, 56, 56, 62, 56])).unwrap(); + let mut current_call_frame = vm.call_frames.pop().unwrap(); + let txreport = vm.execute(&mut current_call_frame); + assert_eq!(txreport.result, TxResult::Revert(VMError::VeryLargeNumber)); +} From 1c771bcedf8fc2daa020ea6988f0d4ca5c0067e3 Mon Sep 17 00:00:00 2001 From: Maximo Palopoli <96491141+maximopalopoli@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:39:50 -0300 Subject: [PATCH 16/25] fix(levm): remove the push for success in return (#1248) **Motivation** Fixes a bug found by [FuzzingLabs](https://github.com/FuzzingLabs) in return opcode implementation. **Description** Now return opcode does not pushes 1 in the stack in case of success, since it is not the documented behavior. Closes #1224 --- crates/vm/levm/src/opcode_handlers/system.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/vm/levm/src/opcode_handlers/system.rs b/crates/vm/levm/src/opcode_handlers/system.rs index fe676f2bb..d2a883347 100644 --- a/crates/vm/levm/src/opcode_handlers/system.rs +++ b/crates/vm/levm/src/opcode_handlers/system.rs @@ -1,6 +1,5 @@ use crate::{ call_frame::CallFrame, - constants::SUCCESS_FOR_RETURN, errors::{InternalError, OpcodeSuccess, ResultReason, VMError}, gas_cost, vm::{word_to_address, VM}, @@ -184,9 +183,6 @@ impl VM { let return_data = current_call_frame.memory.load_range(offset, size)?.into(); current_call_frame.returndata = return_data; - current_call_frame - .stack - .push(U256::from(SUCCESS_FOR_RETURN))?; Ok(OpcodeSuccess::Result(ResultReason::Return)) } From a9d45d6fa9d4a233b6e035e711aec0a1bc51a799 Mon Sep 17 00:00:00 2001 From: Ivan Litteri <67517699+ilitteri@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:41:51 -0300 Subject: [PATCH 17/25] feat(l1, l2, levm): improve loc reporter (#1264) - Print summary - Send summary to slack --- .github/workflows/loc.yaml | 23 +++++++++++++++++++++++ .gitignore | 2 ++ cmd/loc/src/main.rs | 25 +++++++++++++++---------- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/.github/workflows/loc.yaml b/.github/workflows/loc.yaml index b8a7046b4..c26b87b66 100644 --- a/.github/workflows/loc.yaml +++ b/.github/workflows/loc.yaml @@ -26,4 +26,27 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Generate the loc report + id: loc-report run: make loc + echo "content=$(cat loc_report.md)" >> $GITHUB_OUTPUT + + - name: Post results in summary + run: | + echo "# `ethrex` lines of code report:\n\n" >> $GITHUB_STEP_SUMMARY + $(cat loc_report.md) >> $GITHUB_STEP_SUMMARY + + - name: Post results to slack + uses: slackapi/slack-github-action@v2.0.0 + with: + webhook: ${{ secrets.SLACK_WEBHOOK_URL }} + webhook-type: incoming-webhook + payload: | + blocks: + - type: "header" + text: + type: "plain_text" + text: ethrex lines of code report + - type: "section" + text: + type: "mrkdwn" + text: ${{steps.loc-report.outputs.content}} diff --git a/.gitignore b/.gitignore index 3ee1c729c..6cfabcf47 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,5 @@ tests_v3.0.0.tar.gz .env levm_ef_tests_report.txt + +loc_report.md diff --git a/cmd/loc/src/main.rs b/cmd/loc/src/main.rs index c2a8eb30f..0b180b380 100644 --- a/cmd/loc/src/main.rs +++ b/cmd/loc/src/main.rs @@ -1,4 +1,3 @@ -use colored::Colorize; use std::path::PathBuf; use tokei::{Config, LanguageType, Languages}; @@ -23,14 +22,20 @@ fn main() { languages.get_statistics(&[ethrex_l2], &[], &config); let ethrex_l2_loc = &languages.get(&LanguageType::Rust).unwrap(); - println!("{}", "ethrex loc summary".bold()); - println!("{}", "====================".bold()); - println!( - "{}: {:?}", - "ethrex L1".bold(), - ethrex_loc.code - ethrex_l2_loc.code - levm_loc.code + let report = format!( + r#"``` +ethrex loc summary +==================== +ethrex L1: {} +ethrex L2: {} +levm: {} +ethrex (total): {} +```"#, + ethrex_loc.code - ethrex_l2_loc.code - levm_loc.code, + ethrex_l2_loc.code, + levm_loc.code, + ethrex_loc.code, ); - println!("{}: {:?}", "ethrex L2".bold(), ethrex_l2_loc.code); - println!("{}: {:?}", "levm".bold(), levm_loc.code); - println!("{}: {:?}", "ethrex (total)".bold(), ethrex_loc.code); + + std::fs::write("loc_report.md", report).unwrap(); } From 08d15bb65168bad219fed79b05fe56233755abd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jerem=C3=ADas=20Salom=C3=B3n?= <48994069+JereSalo@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:43:08 -0300 Subject: [PATCH 18/25] fix(levm): fix revm_runner test execution (#1265) **Motivation** - When running Cancun tests revm Halted because the spec_id was older than Cancun. **Description** Closes #issue_number --- cmd/ef_tests/levm/runner/revm_runner.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/ef_tests/levm/runner/revm_runner.rs b/cmd/ef_tests/levm/runner/revm_runner.rs index 9594d6d9d..9c7052558 100644 --- a/cmd/ef_tests/levm/runner/revm_runner.rs +++ b/cmd/ef_tests/levm/runner/revm_runner.rs @@ -7,7 +7,7 @@ use crate::{ use ethrex_core::{types::TxKind, Address}; use ethrex_levm::errors::{TransactionReport, TxResult}; use ethrex_storage::{error::StoreError, AccountUpdate}; -use ethrex_vm::{db::StoreWrapper, spec_id, EvmState, RevmAddress, RevmU256}; +use ethrex_vm::{db::StoreWrapper, EvmState, RevmAddress, RevmU256, SpecId}; use revm::{ db::State, inspectors::TracerEip3155 as RevmTracerEip3155, @@ -135,7 +135,7 @@ pub fn prepare_revm_for_tx<'state>( .with_block_env(block_env) .with_tx_env(tx_env) .modify_cfg_env(|cfg| cfg.chain_id = chain_spec.chain_id) - .with_spec_id(spec_id(&chain_spec, test.env.current_timestamp.as_u64())) + .with_spec_id(SpecId::CANCUN) .with_external_context( RevmTracerEip3155::new(Box::new(std::io::stderr())).without_summary(), ); From 450476a3b46b591f7e03e15533bf8ba8bebedff1 Mon Sep 17 00:00:00 2001 From: Maximo Palopoli <96491141+maximopalopoli@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:57:05 -0300 Subject: [PATCH 19/25] fix(levm): prevent inefficient memory allocation in extcodecopy (#1258) **Motivation** Fixes an memory inefficiency found by [FuzzingLabs](https://github.com/FuzzingLabs) in extcodecopy opcode implementation. **Description** Before, there was memory allocation even if the size to copy was zero. Now, if that happens, returns. Closes #1245 --- crates/vm/levm/src/opcode_handlers/environment.rs | 4 ++++ crates/vm/levm/tests/edge_case_tests.rs | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/crates/vm/levm/src/opcode_handlers/environment.rs b/crates/vm/levm/src/opcode_handlers/environment.rs index 106317472..7e1d90d34 100644 --- a/crates/vm/levm/src/opcode_handlers/environment.rs +++ b/crates/vm/levm/src/opcode_handlers/environment.rs @@ -314,6 +314,10 @@ impl VM { self.increase_consumed_gas(current_call_frame, gas_cost)?; + if size == 0 { + return Ok(OpcodeSuccess::Continue); + } + if !is_cached { self.cache_from_db(&address); }; diff --git a/crates/vm/levm/tests/edge_case_tests.rs b/crates/vm/levm/tests/edge_case_tests.rs index 3da16d0de..2ae94e7eb 100644 --- a/crates/vm/levm/tests/edge_case_tests.rs +++ b/crates/vm/levm/tests/edge_case_tests.rs @@ -124,3 +124,11 @@ fn test_non_compliance_returndatacopy() { let txreport = vm.execute(&mut current_call_frame); assert_eq!(txreport.result, TxResult::Revert(VMError::VeryLargeNumber)); } + +#[test] +fn test_non_compliance_extcodecopy() { + let mut vm = new_vm_with_bytecode(Bytes::copy_from_slice(&[88, 88, 88, 89, 60, 89])).unwrap(); + let mut current_call_frame = vm.call_frames.pop().unwrap(); + vm.execute(&mut current_call_frame); + assert_eq!(current_call_frame.stack.stack.pop().unwrap(), U256::zero()); +} From ec09fed25dfc8274c579090e4b6775f12aad578d Mon Sep 17 00:00:00 2001 From: Maximo Palopoli <96491141+maximopalopoli@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:41:48 -0300 Subject: [PATCH 20/25] fix(levm): add memory alignment in extcodecopy (#1263) **Motivation** Fixes an implementation error found by [FuzzingLabs](https://github.com/FuzzingLabs) in extcodecopy opcode implementation. **Description** Previously, in EXTCODECOPY the allocated memory size was not aligned to a multiple of 32 bytes, and was added in, for example, 12 bytes (should increase in blocks of 32). Now the increases are done in groups of 32. Closes #1251 --- crates/vm/levm/src/opcode_handlers/environment.rs | 8 ++++++-- crates/vm/levm/tests/edge_case_tests.rs | 11 +++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/vm/levm/src/opcode_handlers/environment.rs b/crates/vm/levm/src/opcode_handlers/environment.rs index 7e1d90d34..0edce6a62 100644 --- a/crates/vm/levm/src/opcode_handlers/environment.rs +++ b/crates/vm/levm/src/opcode_handlers/environment.rs @@ -324,9 +324,13 @@ impl VM { let bytecode = self.get_account(&address).info.bytecode; - let new_memory_size = dest_offset.checked_add(size).ok_or(VMError::Internal( + let new_memory_size = (((!size).checked_add(1).ok_or(VMError::Internal( InternalError::ArithmeticOperationOverflow, - ))?; + ))?) & 31) + .checked_add(size) + .ok_or(VMError::Internal( + InternalError::ArithmeticOperationOverflow, + ))?; let current_memory_size = current_call_frame.memory.data.len(); if current_memory_size < new_memory_size { current_call_frame.memory.data.resize(new_memory_size, 0); diff --git a/crates/vm/levm/tests/edge_case_tests.rs b/crates/vm/levm/tests/edge_case_tests.rs index 2ae94e7eb..33845f3bd 100644 --- a/crates/vm/levm/tests/edge_case_tests.rs +++ b/crates/vm/levm/tests/edge_case_tests.rs @@ -132,3 +132,14 @@ fn test_non_compliance_extcodecopy() { vm.execute(&mut current_call_frame); assert_eq!(current_call_frame.stack.stack.pop().unwrap(), U256::zero()); } + +#[test] +fn test_non_compliance_extcodecopy_memory_resize() { + let mut vm = new_vm_with_bytecode(Bytes::copy_from_slice(&[ + 0x60, 12, 0x5f, 0x5f, 0x5f, 0x3c, 89, + ])) + .unwrap(); + let mut current_call_frame = vm.call_frames.pop().unwrap(); + vm.execute(&mut current_call_frame); + assert_eq!(current_call_frame.stack.pop().unwrap(), U256::from(32)); +} From 40e606805b6d6e46e69489dcb19090b17a69ea4c Mon Sep 17 00:00:00 2001 From: Federico Borello <156438142+fborello-lambda@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:50:50 -0300 Subject: [PATCH 21/25] chore(l2): rename cli's binary (#1271) **Motivation** Change the CLI's name so it has `ethrex` in the binary's name. --- cmd/ethrex_l2/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/ethrex_l2/Cargo.toml b/cmd/ethrex_l2/Cargo.toml index 5b969aa03..dbc3cc791 100644 --- a/cmd/ethrex_l2/Cargo.toml +++ b/cmd/ethrex_l2/Cargo.toml @@ -34,5 +34,5 @@ ethrex-rlp.workspace = true ethrex-rpc.workspace = true [[bin]] -name = "l2" +name = "ethrex_l2" path = "./src/main.rs" From 2330a51c30061b6eac13b6b0c1343d4410e8f2fc Mon Sep 17 00:00:00 2001 From: Ivan Litteri <67517699+ilitteri@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:55:23 -0300 Subject: [PATCH 22/25] fix(l1, l2, levm): error in loc reporter (#1272) --- .github/workflows/loc.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/loc.yaml b/.github/workflows/loc.yaml index c26b87b66..f72172d0c 100644 --- a/.github/workflows/loc.yaml +++ b/.github/workflows/loc.yaml @@ -27,8 +27,9 @@ jobs: - name: Generate the loc report id: loc-report - run: make loc - echo "content=$(cat loc_report.md)" >> $GITHUB_OUTPUT + run: | + make loc + echo "content=$(cat loc_report.md)" >> $GITHUB_OUTPUT - name: Post results in summary run: | From ae10d075b8b87d1fac60b2c53e17e8398a91f680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Mon, 25 Nov 2024 22:03:24 +0100 Subject: [PATCH 23/25] fix(l1): fix hive report workflow (#1262) **Motivation** The step that builds the report and saves it to an output so the report is not being published. --- .github/workflows/hive_coverage.yaml | 22 +++++----------------- publish.sh | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 17 deletions(-) create mode 100644 publish.sh diff --git a/.github/workflows/hive_coverage.yaml b/.github/workflows/hive_coverage.yaml index 8268c9c7f..6f9f856f4 100644 --- a/.github/workflows/hive_coverage.yaml +++ b/.github/workflows/hive_coverage.yaml @@ -51,25 +51,13 @@ jobs: id: report run: | cargo run -p hive_report > results.md - echo "content=$(cat results.md)" >> $GITHUB_OUTPUT - name: Post results in summary run: | - echo "# Hive coverage report\n\n" >> $GITHUB_STEP_SUMMARY - $(cat results.md) >> $GITHUB_STEP_SUMMARY + echo "# Hive coverage report" >> $GITHUB_STEP_SUMMARY + cat results.md >> $GITHUB_STEP_SUMMARY - name: Post results to slack - uses: slackapi/slack-github-action@v2.0.0 - with: - webhook: ${{ secrets.SLACK_WEBHOOK_URL }} - webhook-type: incoming-webhook - payload: | - blocks: - - type: "header" - text: - type: "plain_text" - text: Hive coverage report - - type: "section" - text: - type: "mrkdwn" - text: ${{steps.report.outputs.content}} + env: + url: ${{ secrets.SLACK_WEBHOOK_URL }} + run: sh publish.sh diff --git a/publish.sh b/publish.sh new file mode 100644 index 000000000..37d7bc37f --- /dev/null +++ b/publish.sh @@ -0,0 +1,22 @@ +curl -X POST $url \ +-H 'Content-Type: application/json; charset=utf-8' \ +--data @- < Date: Mon, 25 Nov 2024 18:08:46 -0300 Subject: [PATCH 24/25] fix(levm): keccak256 opcode (#1253) Closes #1246. --- crates/vm/levm/src/opcode_handlers/keccak.rs | 9 ++++++--- crates/vm/levm/tests/edge_case_tests.rs | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/crates/vm/levm/src/opcode_handlers/keccak.rs b/crates/vm/levm/src/opcode_handlers/keccak.rs index 77a11c4df..29cd2872a 100644 --- a/crates/vm/levm/src/opcode_handlers/keccak.rs +++ b/crates/vm/levm/src/opcode_handlers/keccak.rs @@ -31,14 +31,17 @@ impl VM { self.increase_consumed_gas(current_call_frame, gas_cost)?; - let value_bytes = current_call_frame.memory.load_range(offset, size)?; + let value_bytes = if size == 0 { + vec![] + } else { + current_call_frame.memory.load_range(offset, size)? + }; let mut hasher = Keccak256::new(); hasher.update(value_bytes); - let result = hasher.finalize(); current_call_frame .stack - .push(U256::from_big_endian(&result))?; + .push(U256::from_big_endian(&hasher.finalize()))?; Ok(OpcodeSuccess::Continue) } diff --git a/crates/vm/levm/tests/edge_case_tests.rs b/crates/vm/levm/tests/edge_case_tests.rs index 33845f3bd..13e8bd867 100644 --- a/crates/vm/levm/tests/edge_case_tests.rs +++ b/crates/vm/levm/tests/edge_case_tests.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use bytes::Bytes; use ethrex_core::U256; use ethrex_levm::{ @@ -103,6 +105,22 @@ fn test_is_negative() { vm.execute(&mut current_call_frame); } +#[test] +fn test_non_compliance_keccak256() { + let mut vm = new_vm_with_bytecode(Bytes::copy_from_slice(&[88, 88, 32, 89])).unwrap(); + let mut current_call_frame = vm.call_frames.pop().unwrap(); + vm.execute(&mut current_call_frame); + assert_eq!( + *current_call_frame.stack.stack.first().unwrap(), + U256::from_str("0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470") + .unwrap() + ); + assert_eq!( + *current_call_frame.stack.stack.get(1).unwrap(), + U256::zero() + ); +} + #[test] fn test_sdiv_zero_dividend_and_negative_divisor() { let mut vm = new_vm_with_bytecode(Bytes::copy_from_slice(&[ From a11d8dbe52226510860d28daccc1e2d7dc4b0a44 Mon Sep 17 00:00:00 2001 From: Ivan Litteri <67517699+ilitteri@users.noreply.github.com> Date: Tue, 26 Nov 2024 07:16:40 -0300 Subject: [PATCH 25/25] fix(l1, l2, levm): loc reporter (#1274) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: TomΓ‘s Arjovsky --- .github/workflows/loc.yaml | 23 +++++------------------ publish_loc.sh | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 18 deletions(-) create mode 100644 publish_loc.sh diff --git a/.github/workflows/loc.yaml b/.github/workflows/loc.yaml index f72172d0c..46468453c 100644 --- a/.github/workflows/loc.yaml +++ b/.github/workflows/loc.yaml @@ -26,28 +26,15 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Generate the loc report - id: loc-report run: | make loc - echo "content=$(cat loc_report.md)" >> $GITHUB_OUTPUT - name: Post results in summary run: | - echo "# `ethrex` lines of code report:\n\n" >> $GITHUB_STEP_SUMMARY - $(cat loc_report.md) >> $GITHUB_STEP_SUMMARY + echo "# `ethrex` lines of code report" >> $GITHUB_STEP_SUMMARY + cat loc_report.md >> $GITHUB_STEP_SUMMARY - name: Post results to slack - uses: slackapi/slack-github-action@v2.0.0 - with: - webhook: ${{ secrets.SLACK_WEBHOOK_URL }} - webhook-type: incoming-webhook - payload: | - blocks: - - type: "header" - text: - type: "plain_text" - text: ethrex lines of code report - - type: "section" - text: - type: "mrkdwn" - text: ${{steps.loc-report.outputs.content}} + env: + url: ${{ secrets.SLACK_WEBHOOK_URL }} + run: sh publish_loc.sh diff --git a/publish_loc.sh b/publish_loc.sh new file mode 100644 index 000000000..4348fc335 --- /dev/null +++ b/publish_loc.sh @@ -0,0 +1,22 @@ +curl -X POST $url \ +-H 'Content-Type: application/json; charset=utf-8' \ +--data @- <