diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a629124a7e..a34b1ab392b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,10 @@ name: CI checks -on: +on: pull_request: types: [synchronize, opened, reopened, ready_for_review] -## `actions-rs/toolchain@v1` overwrite set to false so that +## `actions-rs/toolchain@v1` overwrite set to false so that ## `rust-toolchain` is always used and the only source of truth. jobs: @@ -98,7 +98,7 @@ jobs: fmt: if: github.event.pull_request.draft == false - + name: Rustfmt timeout-minutes: 30 runs-on: ubuntu-latest diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 00000000000..f467ea9a31e --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,36 @@ +name: Integration Tests + +on: + pull_request: + types: [synchronize, opened, reopened, ready_for_review] + +jobs: + integration-tests: + if: github.event.pull_request.draft == false + + name: Integration Tests + runs-on: ubuntu-latest + + defaults: + run: + working-directory: ./integration-tests + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + override: false + - name: Set PATH + run: echo "${HOME}/bin" >> $GITHUB_PATH + - name: Install Solc + run: | + mkdir -p "$HOME/bin" + wget -q https://github.com/ethereum/solidity/releases/download/v0.8.0/solc-static-linux -O $HOME/bin/solc + chmod u+x "$HOME/bin/solc" + solc --version + # Run an initial build in a sepparate step to split the build time from execution time + - name: Build gendata bin + run: cargo build --bin gen_blockchain_data + - run: ./run.sh --steps "setup" + - run: ./run.sh --steps "gendata" + - run: ./run.sh --steps "tests" --tests "rpc" + - run: ./run.sh --steps "cleanup" diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index de705d7ad2f..7d08c132cf5 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -2,14 +2,14 @@ name: Lints # We only run these lints on trial-merges of PRs to reduce noise. -on: +on: pull_request: types: [synchronize, opened, reopened, ready_for_review] jobs: clippy: if: github.event.pull_request.draft == false - + name: Clippy timeout-minutes: 30 runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index eea61af3dd6..29da530b8f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "bus-mapping", "keccak256", "geth-utils", + "integration-tests", ] [patch.crates-io] diff --git a/bus-mapping/Cargo.toml b/bus-mapping/Cargo.toml index b96b2b1975a..ed1aa9ab113 100644 --- a/bus-mapping/Cargo.toml +++ b/bus-mapping/Cargo.toml @@ -14,8 +14,8 @@ serde_json = "1.0.66" hex = "0.4" geth-utils = { path = "../geth-utils" } uint = "0.9.1" -ethers-providers = "0.6.1" -ethers-core = "0.6.1" +ethers-providers = "0.6.2" +ethers-core = "0.6.2" regex = "1.5.4" [dev-dependencies] diff --git a/bus-mapping/src/eth_types.rs b/bus-mapping/src/eth_types.rs index 10219856842..861a572158b 100644 --- a/bus-mapping/src/eth_types.rs +++ b/bus-mapping/src/eth_types.rs @@ -4,8 +4,8 @@ use crate::evm::{memory::Memory, stack::Stack, storage::Storage}; use crate::evm::{Gas, GasCost, OpcodeId, ProgramCounter}; use ethers_core::types; pub use ethers_core::types::{ - transaction::response::Transaction, Address, Block, Bytes, - EIP1186ProofResponse, H160, H256, U256, U64, + transaction::response::Transaction, Address, Block, Bytes, H160, H256, + U256, U64, }; use pairing::arithmetic::FieldExt; use serde::{de, Deserialize}; @@ -120,6 +120,37 @@ impl ToScalar for Address { } } +/// Struct used to define the storage proof +#[derive(Debug, Default, Clone, PartialEq, Deserialize)] +pub struct StorageProof { + /// Storage key + pub key: U256, + /// Storage Value + pub value: U256, + /// Storage proof: rlp-encoded trie nodes from root to value. + pub proof: Vec, +} + +/// Struct used to define the result of `eth_getProof` call +#[derive(Debug, Default, Clone, PartialEq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EIP1186ProofResponse { + /// Account address + pub address: Address, + /// The balance of the account + pub balance: U256, + /// The hash of the code of the account + pub code_hash: H256, + /// The nonce of the account + pub nonce: U256, + /// SHA3 of the StorageRoot + pub storage_hash: H256, + /// Array of rlp-serialized MerkleTree-Nodes + pub account_proof: Vec, + /// Array of storage-entries as requested + pub storage_proof: Vec, +} + #[derive(Clone, Debug, Eq, PartialEq, Deserialize)] #[doc(hidden)] struct GethExecStepInternal { diff --git a/bus-mapping/src/rpc.rs b/bus-mapping/src/rpc.rs index d92b60fd53e..6fe87c773db 100644 --- a/bus-mapping/src/rpc.rs +++ b/bus-mapping/src/rpc.rs @@ -2,11 +2,12 @@ //! query a Geth node in order to get a Block, Tx or Trace info. use crate::eth_types::{ - Address, Block, EIP1186ProofResponse, GethExecTrace, Hash, + Address, Block, Bytes, EIP1186ProofResponse, GethExecTrace, Hash, ResultGethExecTraces, Transaction, Word, U64, }; use crate::Error; use ethers_providers::JsonRpcClient; +use serde::{Serialize, Serializer}; /// Serialize a type. /// @@ -22,7 +23,7 @@ pub fn serialize(t: &T) -> serde_json::Value { #[derive(Debug)] pub enum BlockNumber { /// Specific block number - Num(U64), + Num(u64), /// Earliest block Earliest, /// Latest block @@ -33,26 +34,27 @@ pub enum BlockNumber { impl From for BlockNumber { fn from(num: u64) -> Self { - BlockNumber::Num(U64::from(num)) + BlockNumber::Num(num) } } -impl BlockNumber { - /// Serializes a BlockNumber as a [`Value`](serde_json::Value) to be able to - /// throw it into a JSON-RPC request. - pub fn serialize(self) -> serde_json::Value { +impl Serialize for BlockNumber { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { match self { - BlockNumber::Num(num) => serialize(&num), - BlockNumber::Earliest => serialize(&"earliest"), - BlockNumber::Latest => serialize(&"latest"), - BlockNumber::Pending => serialize(&"pending"), + BlockNumber::Num(num) => U64::from(*num).serialize(serializer), + BlockNumber::Earliest => "earliest".serialize(serializer), + BlockNumber::Latest => "latest".serialize(serializer), + BlockNumber::Pending => "pending".serialize(serializer), } } } /// Placeholder structure designed to contain the methods that the BusMapping /// needs in order to enable Geth queries. -pub struct GethClient(P); +pub struct GethClient(pub P); impl GethClient

{ /// Generates a new `GethClient` instance. @@ -81,7 +83,7 @@ impl GethClient

{ &self, block_num: BlockNumber, ) -> Result, Error> { - let num = block_num.serialize(); + let num = serialize(&block_num); let flag = serialize(&true); self.0 .request("eth_getBlockByNumber", [num, flag]) @@ -112,7 +114,7 @@ impl GethClient

{ &self, block_num: BlockNumber, ) -> Result, Error> { - let num = block_num.serialize(); + let num = serialize(&block_num); let resp: ResultGethExecTraces = self .0 .request("debug_traceBlockByNumber", [num]) @@ -121,9 +123,25 @@ impl GethClient

{ Ok(resp.0.into_iter().map(|step| step.result).collect()) } - /// Calls `eth_getProof` via JSON-RPC returning a [`EIP1186ProofResponse`] - /// returning the account and storage-values of the specified - /// account including the Merkle-proof. + /// Calls `eth_getCode` via JSON-RPC returning a contract code + pub async fn get_code_by_address( + &self, + contract_address: Address, + block_num: BlockNumber, + ) -> Result, Error> { + let address = serialize(&contract_address); + let num = serialize(&block_num); + let resp: Bytes = self + .0 + .request("eth_getCode", [address, num]) + .await + .map_err(|e| Error::JSONRpcError(e.into()))?; + Ok(resp.to_vec()) + } + + /// Calls `eth_getProof` via JSON-RPC returning a + /// [`EIP1186ProofResponse`] returning the account and + /// storage-values of the specified account including the Merkle-proof. pub async fn get_proof( &self, account: Address, @@ -132,7 +150,7 @@ impl GethClient

{ ) -> Result { let account = serialize(&account); let keys = serialize(&keys); - let num = block_num.serialize(); + let num = serialize(&block_num); self.0 .request("eth_getProof", [account, keys, num]) .await @@ -140,122 +158,4 @@ impl GethClient

{ } } -#[cfg(test)] -mod rpc_tests { - use super::*; - use ethers_providers::Http; - use std::str::FromStr; - use url::Url; - - // The test is ignored as the values used depend on the Geth instance used - // each time you run the tests. And we can't assume that everyone will - // have a Geth client synced with mainnet to have unified "test-vectors". - #[ignore] - #[tokio::test] - async fn test_get_block_by_hash() { - let transport = Http::new(Url::parse("http://localhost:8545").unwrap()); - - let hash = Hash::from_str("0xe4f7aa19a76fcf31a6adff3b400300849e39dd84076765fb3af09d05ee9d787a").unwrap(); - let prov = GethClient::new(transport); - let block_by_hash = prov.get_block_by_hash(hash).await.unwrap(); - assert!(hash == block_by_hash.hash.unwrap()); - } - - // The test is ignored as the values used depend on the Geth instance used - // each time you run the tests. And we can't assume that everyone will - // have a Geth client synced with mainnet to have unified "test-vectors". - #[ignore] - #[tokio::test] - async fn test_get_block_by_number() { - let transport = Http::new(Url::parse("http://localhost:8545").unwrap()); - - let hash = Hash::from_str("0xe4f7aa19a76fcf31a6adff3b400300849e39dd84076765fb3af09d05ee9d787a").unwrap(); - let prov = GethClient::new(transport); - let block_by_num_latest = - prov.get_block_by_number(BlockNumber::Latest).await.unwrap(); - assert!(hash == block_by_num_latest.hash.unwrap()); - let block_by_num = prov.get_block_by_number(1u64.into()).await.unwrap(); - assert!( - block_by_num.transactions[0].hash - == block_by_num_latest.transactions[0].hash - ); - } - - // The test is ignored as the values used depend on the Geth instance used - // each time you run the tests. And we can't assume that everyone will - // have a Geth client synced with mainnet to have unified "test-vectors". - #[ignore] - #[tokio::test] - async fn test_trace_block_by_hash() { - let transport = Http::new(Url::parse("http://localhost:8545").unwrap()); - - let hash = Hash::from_str("0xe2d191e9f663a3a950519eadeadbd614965b694a65a318a0b8f053f2d14261ff").unwrap(); - let prov = GethClient::new(transport); - let trace_by_hash = prov.trace_block_by_hash(hash).await.unwrap(); - // Since we called in the test block the same transaction twice the len - // should be the same and != 0. - assert!( - trace_by_hash[0].struct_logs.len() - == trace_by_hash[1].struct_logs.len() - ); - assert!(!trace_by_hash[0].struct_logs.is_empty()); - } - - // The test is ignored as the values used depend on the Geth instance used - // each time you run the tests. And we can't assume that everyone will - // have a Geth client synced with mainnet to have unified "test-vectors". - #[ignore] - #[tokio::test] - async fn test_trace_block_by_number() { - let transport = Http::new(Url::parse("http://localhost:8545").unwrap()); - let prov = GethClient::new(transport); - let trace_by_hash = prov.trace_block_by_number(5.into()).await.unwrap(); - // Since we called in the test block the same transaction twice the len - // should be the same and != 0. - assert!( - trace_by_hash[0].struct_logs.len() - == trace_by_hash[1].struct_logs.len() - ); - assert!(!trace_by_hash[0].struct_logs.is_empty()); - } - - // The test is ignored as the values used depend on the Geth instance used - // each time you run the tests. And we can't assume that everyone will - // have a Geth client synced with mainnet to have unified "test-vectors". - #[ignore] - #[tokio::test] - async fn test_get_proof() { - let transport = Http::new(Url::parse("http://localhost:8545").unwrap()); - let prov = GethClient::new(transport); - - let address = - Address::from_str("0x7F0d15C7FAae65896648C8273B6d7E43f58Fa842") - .unwrap(); - let keys = vec![Word::from_str("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").unwrap()]; - let proof = prov - .get_proof(address, keys, BlockNumber::Latest) - .await - .unwrap(); - const TARGET_PROOF: &str = r#"{ - "address": "0x7f0d15c7faae65896648c8273b6d7e43f58fa842", - "accountProof": [ - "0xf873a12050fb4d3174ec89ef969c09fd4391602169760fb005ad516f5d172cbffb80e955b84ff84d8089056bc75e2d63100000a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" - ], - "balance": "0x0", - "codeHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", - "nonce": "0x0", - "storageHash": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "storageProof": [ - { - "key": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "value": "0x0", - "proof": [] - } - ] - }"#; - assert!( - serde_json::from_str::(TARGET_PROOF).unwrap() - == proof - ); - } -} +// Integration tests found in `integration-tests/tests/rpc.rs`. diff --git a/integration-tests/.gitignore b/integration-tests/.gitignore new file mode 100644 index 00000000000..bbcc9ac930d --- /dev/null +++ b/integration-tests/.gitignore @@ -0,0 +1 @@ +gendata_output.json diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml new file mode 100644 index 00000000000..30152e2cb16 --- /dev/null +++ b/integration-tests/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "integration-tests" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +lazy_static = "1.4" +ethers = "0.6.2" +serde_json = "1.0.66" +serde = {version = "1.0.130", features = ["derive"] } +bus-mapping = { path = "../bus-mapping"} +tokio = { version = "1.13", features = ["macros", "rt-multi-thread"] } +url = "2.2.2" +pretty_assertions = "1.0.0" + +[dev-dependencies] +pretty_assertions = "1.0.0" + +[features] +default = [] +rpc = [] diff --git a/integration-tests/README.md b/integration-tests/README.md new file mode 100644 index 00000000000..d500207bb6e --- /dev/null +++ b/integration-tests/README.md @@ -0,0 +1,42 @@ +# Integration Tests + +Integration tests are located in this crate. Each test group can be found +under a different file in `tests/`. + +Contracts for tests are found in `contracts/`. + +The full integration tests flow can be executed with the `run.sh` script, whic is used like this: +``` +$ ./run.sh --help + Usage: ./run.sh [OPTIONS] + Options: + --sudo Use sudo for docker-compoes commands. + --steps ARG Space separated list of steps to do. + Default: "setup gendata tests cleanup". + --tests ARG Space separated list of tests to run. + Default: "rpc". + -h | --help Show help +``` + +## Steps +1. Setup: Start the docker container that runs a fresh geth in dev mode, via + docker-compose. +2. Gendata: Run the `gen_blockchain_data` binary found in + `src/bin/gen_blockchain_data.rs` which compiles the contracts, deploys them, + and executes transactions; in order to generate blocks with a variety of + data and transactions to be used in the tests. The compiled output of the + contracts will be written as json files in `contracts` next to the solidity + source. After completion, `gendata_output.json` will be generated with + details of the executed transactions to be used as input vectors for the tests. +3. Tests: Run the specified tests groups. +4. Cleanup: Remove the geth docker container. + +By default the `run.sh` script runs all the steps. Specifying a smaller +combination of steps can be very useful for development: you can run the +`setup` and `gendata` once, and then iterate over the `tests` step to debug +specific functions being tested. + +## Lib + +Functions and constant parameters shared both in the `gendata` step and the tests +themselves are defined in `lib.rs`. diff --git a/integration-tests/contracts/.gitignore b/integration-tests/contracts/.gitignore new file mode 100644 index 00000000000..a6c57f5fb2f --- /dev/null +++ b/integration-tests/contracts/.gitignore @@ -0,0 +1 @@ +*.json diff --git a/integration-tests/contracts/greeter/Greeter.sol b/integration-tests/contracts/greeter/Greeter.sol new file mode 100644 index 00000000000..5ad5b2f7521 --- /dev/null +++ b/integration-tests/contracts/greeter/Greeter.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.7.0 <0.9.0; + +/** + * @title Greeter + * @dev Store & retrieve value in a variable + */ +contract Greeter { + + uint256 number; + + constructor(uint256 num) { + number = num; + } + + function retrieve() public view returns (uint256){ + return number; + } + + function retrieve_failing() public view returns (uint256){ + require(false); + return number; + } + + function set_value(uint256 num) public{ + number = num; + } + + function set_value_failing(uint256 num) public{ + number = num; + require(false); + } +} diff --git a/integration-tests/docker-compose.yml b/integration-tests/docker-compose.yml new file mode 100644 index 00000000000..b439cedc55f --- /dev/null +++ b/integration-tests/docker-compose.yml @@ -0,0 +1,9 @@ +version: '3' +services: + geth0: + image: "ethereum/client-go:stable" + container_name: zkevm-geth0 + ports: + - 8545:8545 + command: --dev --vmdebug --gcmode=archive --http --http.addr 0.0.0.0 --http.port 8545 --http.vhosts "*" --http.corsdomain "*" --http.api "eth,net,web3,personal,txpool,miner,debug" --verbosity 6 + diff --git a/integration-tests/run.sh b/integration-tests/run.sh new file mode 100755 index 00000000000..c3ebaeee82c --- /dev/null +++ b/integration-tests/run.sh @@ -0,0 +1,108 @@ +#!/bin/sh +set -e + +ARG_DEFAULT_SUDO= +ARG_DEFAULT_STEPS="setup gendata tests cleanup" +ARG_DEFAULT_TESTS="rpc" +# ARG_DEFAULT_TESTS="rpc bus-mapping" + +usage() { + cat >&2 << EOF + Usage: $0 [OPTIONS] + Options: + --sudo Use sudo for docker-compoes commands. + --steps ARG Space separated list of steps to do. + Default: "${ARG_DEFAULT_STEPS}". + --tests ARG Space separated list of tests to run. + Default: "${ARG_DEFAULT_TESTS}". + -h | --help Show help + +EOF +} + +ARG_SUDO="${ARG_DEFAULT_SUDO}" +ARG_STEPS="${ARG_DEFAULT_STEPS}" +ARG_TESTS="${ARG_DEFAULT_TESTS}" + +while [ "$1" != "" ]; do + case "$1" in + --sudo ) + ARG_SUDO=1 + ;; + --steps ) + shift + ARG_STEPS="$1" + ;; + --tests ) + shift + ARG_TESTS="$1" + ;; + -h | --help ) + usage + exit + ;; + * ) + echo "Unknown flag \"$1\"" + usage + exit 1 + esac + shift +done + +STEP_SETUP= +STEP_GENDATA= +STEP_TESTS= +STEP_CLEANUP= + +for step in $ARG_STEPS; do + case "$step" in + setup ) + STEP_SETUP=1 + ;; + gendata ) + STEP_GENDATA=1 + ;; + tests ) + STEP_TESTS=1 + ;; + cleanup ) + STEP_CLEANUP=1 + ;; + * ) + echo "Unknown step \"$step\"" + usage + exit 1 + esac +done + +docker_compose_cmd() { + if [ -n "$ARG_SUDO" ]; then + sudo docker-compose $@ + else + docker-compose $@ + fi +} + +if [ -n "$STEP_SETUP" ]; then + echo "+ Setup..." + docker_compose_cmd down -v --remove-orphans + docker_compose_cmd up -d geth0 +fi + +if [ -n "$STEP_GENDATA" ]; then + echo "+ Gen blockchain data..." + rm gendata_output.json > /dev/null 2>&1 || true + cargo run --bin gen_blockchain_data +fi + +if [ -n "$STEP_TESTS" ]; then + for testname in $ARG_TESTS; do + echo "+ Running test group $testname" + cargo test --test $testname --features $testname + done +fi + +if [ -n "$STEP_CLEANUP" ]; then + echo "+ Cleanup..." + docker_compose_cmd down -v --remove-orphans +fi diff --git a/integration-tests/src/bin/gen_blockchain_data.rs b/integration-tests/src/bin/gen_blockchain_data.rs new file mode 100644 index 00000000000..7fb69112aae --- /dev/null +++ b/integration-tests/src/bin/gen_blockchain_data.rs @@ -0,0 +1,150 @@ +use ethers::{ + abi::Tokenize, + contract::{Contract, ContractFactory}, + core::types::{TransactionRequest, U256}, + core::utils::WEI_IN_ETHER, + middleware::SignerMiddleware, + providers::Middleware, + signers::Signer, + solc::Solc, +}; +use integration_tests::{ + get_provider, get_wallet, CompiledContract, GenDataOutput, CONTRACTS, + CONTRACTS_PATH, +}; +use std::collections::HashMap; +use std::fs::File; +use std::path::Path; +use std::sync::Arc; +use std::thread::sleep; +use std::time::Duration; + +async fn deploy( + prov: Arc, + compiled: &CompiledContract, + args: T, +) -> Contract +where + T: Tokenize, + M: Middleware, +{ + println!("Deploying {}...", compiled.name); + let factory = + ContractFactory::new(compiled.abi.clone(), compiled.bin.clone(), prov); + factory + .deploy(args) + .expect("cannot deploy") + .confirmations(0usize) + .send() + .await + .expect("cannot confirm deploy") +} + +#[tokio::main] +async fn main() { + // Compile contracts + let mut contracts = HashMap::new(); + for (name, contract_path) in CONTRACTS { + let path_sol = Path::new(CONTRACTS_PATH).join(contract_path); + let compiled = Solc::default() + .compile_source(&path_sol) + .expect("solc compile error"); + if !compiled.errors.is_empty() { + panic!("Errors compiling {:?}:\n{:#?}", &path_sol, compiled.errors) + } + + let contract = compiled + .get( + &path_sol.to_str().expect("path is not str").to_string(), + name, + ) + .expect("contract not found"); + let abi = contract.abi.expect("no abi found").clone(); + let bin = contract.bin.expect("no bin found").clone(); + let bin_runtime = + contract.bin_runtime.expect("no bin_runtime found").clone(); + let compiled_contract = CompiledContract { + path: path_sol.to_str().expect("path is not str").to_string(), + name: name.to_string(), + abi, + bin, + bin_runtime, + }; + + let mut path_json = path_sol.clone(); + path_json.set_extension("json"); + serde_json::to_writer( + &File::create(&path_json).expect("cannot create file"), + &compiled_contract, + ) + .expect("cannot serialize json into file"); + + contracts.insert(name.to_string(), compiled_contract); + } + + let prov = get_provider(); + + // Wait for geth to be online. + loop { + match prov.client_version().await { + Ok(version) => { + println!("Geth online: {}", version); + break; + } + Err(err) => { + println!("Geth not available: {:?}", err); + sleep(Duration::from_millis(500)); + } + } + } + + // Make sure the blockchain is in a clean state: block 0 is the last block. + let block_number = prov + .get_block_number() + .await + .expect("cannot get block number"); + if block_number.as_u64() != 0 { + panic!( + "Blockchain is not in a clean state. Last block number: {}", + block_number + ); + } + + let accounts = prov.get_accounts().await.expect("cannot get accounts"); + let wallet0 = get_wallet(0); + println!("wallet0: {:x}", wallet0.address()); + + // Transfer funds to our account. + let tx = TransactionRequest::new() + .to(wallet0.address()) + .value(WEI_IN_ETHER) // send 1 ETH + .from(accounts[0]); + prov.send_transaction(tx, None) + .await + .expect("cannot send tx") + .await + .expect("cannot confirm tx"); + + // Deploy smart contracts + let mut deployments = HashMap::new(); + let prov_wallet0 = Arc::new(SignerMiddleware::new(get_provider(), wallet0)); + let contract = deploy( + prov_wallet0.clone(), + contracts.get("Greeter").expect("contract not found"), + U256::from(42), + ) + .await; + let block_num = + prov.get_block_number().await.expect("cannot get block_num"); + deployments.insert( + "Greeter".to_string(), + (block_num.as_u64(), contract.address()), + ); + + let gen_data = GenDataOutput { + coinbase: accounts[0], + wallets: vec![get_wallet(0).address()], + deployments, + }; + gen_data.store(); +} diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs new file mode 100644 index 00000000000..0cc20a0cf60 --- /dev/null +++ b/integration-tests/src/lib.rs @@ -0,0 +1,115 @@ +//! Integration testing + +#![deny(rustdoc::broken_intra_doc_links)] +#![deny(missing_docs)] + +use bus_mapping::eth_types::Address; +use bus_mapping::rpc::GethClient; +use ethers::{ + abi, + core::k256::ecdsa::SigningKey, + core::types::Bytes, + providers::{Http, Provider}, + signers::{coins_bip39::English, MnemonicBuilder, Signer, Wallet}, +}; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::env::{self, VarError}; +use std::fs::File; +use std::time::Duration; +use url::Url; + +/// Geth dev chain ID +pub const CHAIN_ID: u64 = 1337; +/// Path to the test contracts +pub const CONTRACTS_PATH: &str = "contracts"; +/// List of contracts as (ContractName, ContractSolidityFile) +pub const CONTRACTS: &[(&str, &str)] = &[("Greeter", "greeter/Greeter.sol")]; +/// Path to gen_blockchain_data output file +pub const GENDATA_OUTPUT_PATH: &str = "gendata_output.json"; + +const GETH0_URL_DEFAULT: &str = "http://localhost:8545"; + +lazy_static! { + /// URL of the integration test geth0 instance, which contains blocks for which proofs will be + /// generated. + pub static ref GETH0_URL: String = match env::var("GETH0_URL") { + Ok(val) => val, + Err(VarError::NotPresent) => GETH0_URL_DEFAULT.to_string(), + Err(e) => panic!("Error in GETH0_URL env var: {:?}", e), + }; +} + +/// Get the integration test [`GethClient`] +pub fn get_client() -> GethClient { + let transport = Http::new(Url::parse(&GETH0_URL).expect("invalid url")); + GethClient::new(transport) +} + +/// Get the integration test [`Provider`] +pub fn get_provider() -> Provider { + let transport = Http::new(Url::parse(&GETH0_URL).expect("invalid url")); + Provider::new(transport).interval(Duration::from_millis(100)) +} + +const PHRASE: &str = "work man father plunge mystery proud hollow address reunion sauce theory bonus"; + +/// Get a wallet by index +pub fn get_wallet(index: u32) -> Wallet { + // Access mnemonic phrase. + // Child key at derivation path: m/44'/60'/0'/0/{index} + MnemonicBuilder::::default() + .phrase(PHRASE) + .index(index) + .expect("invalid index") + .build() + .expect("cannot build wallet from mnemonic") + .with_chain_id(CHAIN_ID) +} + +/// Output information of the blockchain data generated by +/// `gen_blockchain_data`. +#[derive(Serialize, Deserialize)] +pub struct GenDataOutput { + /// Coinbase of the blockchain + pub coinbase: Address, + /// Wallets used by `gen_blockchain_data` + pub wallets: Vec

, + /// Contracts deployed map: ContractName -> (BlockNum, Address) + pub deployments: HashMap, +} + +impl GenDataOutput { + /// Load [`GenDataOutput`] from the json file. + pub fn load() -> Self { + serde_json::from_reader( + File::open(GENDATA_OUTPUT_PATH).expect("cannot read file"), + ) + .expect("cannot deserialize json from file") + } + + /// Store [`GenDataOutput`] into the json file. + pub fn store(&self) { + serde_json::to_writer( + &File::create(GENDATA_OUTPUT_PATH).expect("cannot create file"), + self, + ) + .expect("cannot serialize json into file"); + } +} + +/// Solc-compiled contract output +#[derive(Serialize, Deserialize)] +pub struct CompiledContract { + /// Contract path + pub path: String, + /// Contract name + pub name: String, + /// ABI + pub abi: abi::Contract, + /// Bytecode + pub bin: Bytes, + /// Runtime Bytecode + pub bin_runtime: Bytes, +} diff --git a/integration-tests/tests/rpc.rs b/integration-tests/tests/rpc.rs new file mode 100644 index 00000000000..b2b76859a52 --- /dev/null +++ b/integration-tests/tests/rpc.rs @@ -0,0 +1,84 @@ +#![cfg(feature = "rpc")] + +use bus_mapping::eth_types::{StorageProof, Word}; +use integration_tests::{ + get_client, CompiledContract, GenDataOutput, CONTRACTS_PATH, +}; +use lazy_static::lazy_static; +use pretty_assertions::assert_eq; +use std::fs::File; +use std::path::Path; + +lazy_static! { + pub static ref GEN_DATA: GenDataOutput = GenDataOutput::load(); +} + +#[tokio::test] +async fn test_get_block_by_number_by_hash() { + let cli = get_client(); + let block_by_num = cli.get_block_by_number(1.into()).await.unwrap(); + let block_by_hash = cli + .get_block_by_hash(block_by_num.hash.unwrap()) + .await + .unwrap(); + assert!(block_by_num == block_by_hash); + // Transaction 1 is a transfer from coinbase to wallet0 + assert_eq!(block_by_num.transactions.len(), 1); + assert_eq!(block_by_num.transactions[0].from, GEN_DATA.coinbase); + assert_eq!(block_by_num.transactions[0].to, Some(GEN_DATA.wallets[0])); +} + +#[tokio::test] +async fn test_trace_block_by_number_by_hash() { + let block_num = GEN_DATA.deployments.get("Greeter").unwrap().0; + + let cli = get_client(); + let block = cli.get_block_by_number(block_num.into()).await.unwrap(); + let trace_by_number = + cli.trace_block_by_number(block_num.into()).await.unwrap(); + let trace_by_hash = + cli.trace_block_by_hash(block.hash.unwrap()).await.unwrap(); + assert_eq!(trace_by_number, trace_by_hash); + assert!(!trace_by_number[0].struct_logs.is_empty()) +} + +#[tokio::test] +async fn test_get_contract_code() { + let contract_name = "Greeter"; + let contract_path_json = "greeter/Greeter.json"; + + let (block_num, address) = GEN_DATA.deployments.get(contract_name).unwrap(); + let path_json = Path::new(CONTRACTS_PATH).join(contract_path_json); + let compiled: CompiledContract = serde_json::from_reader( + File::open(path_json).expect("cannot read file"), + ) + .expect("cannot deserialize json from file"); + + let cli = get_client(); + let code = cli + .get_code_by_address(*address, (*block_num).into()) + .await + .unwrap(); + assert_eq!(compiled.bin_runtime.to_vec(), code); +} + +#[tokio::test] +async fn test_get_proof() { + let (block_num, address) = GEN_DATA.deployments.get("Greeter").unwrap(); + // Key 0 corresponds to `Greeter.number`, which is initialized with 0x2a. + let expected_storage_proof_json = r#"{ + "key": "0x0", + "value": "0x2a", + "proof": ["0xe3a120290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e5632a"] + }"#; + let expected_storage_proof: StorageProof = + serde_json::from_str(expected_storage_proof_json).unwrap(); + + let cli = get_client(); + let keys = vec![Word::from(0)]; + let proof = cli + .get_proof(*address, keys, (*block_num).into()) + .await + .unwrap(); + assert_eq!(expected_storage_proof, proof.storage_proof[0]); +} diff --git a/rustfmt.toml b/rustfmt.toml index 3450fc407a6..ef7d233553e 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,2 +1,3 @@ max_width = 80 wrap_comments = true +edition = "2021"