Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EVM: Implement eth_call #921

Merged
merged 16 commits into from
Sep 27, 2023
Merged
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ ethers-signers = { version = "=2.0.10", default-features = false }
ethers-middleware = { version = "=2.0.10", default-features = false }

reth-primitives = { git = "https://github.com/paradigmxyz/reth", rev = "e83d3aa" }
reth-interfaces = { git = "https://github.com/paradigmxyz/reth", rev = "e83d3aa" }
reth-rpc-types = { git = "https://github.com/paradigmxyz/reth", rev = "e83d3aa" }
reth-rpc-types-compat = { git = "https://github.com/paradigmxyz/reth", rev = "e83d3aa" }
reth-revm = { git = "https://github.com/paradigmxyz/reth", rev = "e83d3aa" }
Expand Down
74 changes: 73 additions & 1 deletion examples/demo-rollup/tests/evm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ use ethereum_types::H160;
use ethers_core::abi::Address;
use ethers_core::k256::ecdsa::SigningKey;
use ethers_core::types::transaction::eip2718::TypedTransaction;
use ethers_core::types::{Block, Eip1559TransactionRequest, Transaction, TxHash};
use ethers_core::types::{
Block, Eip1559TransactionRequest, Transaction, TransactionRequest, TxHash,
};
use ethers_middleware::SignerMiddleware;
use ethers_providers::{Http, Middleware, PendingTransaction, Provider};
use ethers_signers::{LocalWallet, Signer, Wallet};
use jsonrpsee::core::client::ClientT;
use jsonrpsee::http_client::{HttpClient, HttpClientBuilder};
use jsonrpsee::rpc_params;
use reth_primitives::Bytes;
use sov_evm::SimpleStorageContract;
use sov_risc0_adapter::host::Risc0Host;

Expand Down Expand Up @@ -131,6 +134,55 @@ impl TestClient {
.unwrap()
}

async fn set_value_call(
&self,
contract_address: H160,
set_arg: u32,
) -> Result<Bytes, Box<dyn std::error::Error>> {
let nonce = self.eth_get_transaction_count(self.from_addr).await;

// Any type of transaction can be used for eth_call
let req = TransactionRequest::new()
.from(self.from_addr)
.to(contract_address)
.chain_id(self.chain_id)
.nonce(nonce)
.data(self.contract.set_call_data(set_arg))
.gas_price(10u64)
.gas(900000u64);

let typed_transaction = TypedTransaction::Legacy(req);

let response = self
.eth_call(typed_transaction, Some("latest".to_owned()))
.await?;

Ok(response)
}

async fn failing_call(
&self,
contract_address: H160,
) -> Result<Bytes, Box<dyn std::error::Error>> {
let nonce = self.eth_get_transaction_count(self.from_addr).await;

// Any type of transaction can be used for eth_call
let req = Eip1559TransactionRequest::new()
.from(self.from_addr)
.to(contract_address)
.chain_id(self.chain_id)
.nonce(nonce)
.data(self.contract.failing_function_call_data())
.max_priority_fee_per_gas(10u64)
.max_fee_per_gas(MAX_FEE_PER_GAS)
.gas(900000u64);

let typed_transaction = TypedTransaction::Eip1559(req);

self.eth_call(typed_transaction, Some("latest".to_owned()))
.await
}

async fn query_contract(
&self,
contract_address: H160,
Expand Down Expand Up @@ -207,6 +259,17 @@ impl TestClient {
.unwrap()
}

async fn eth_call(
&self,
tx: TypedTransaction,
block_number: Option<String>,
) -> Result<Bytes, Box<dyn std::error::Error>> {
self.http_client
.request("eth_call", rpc_params![tx, block_number])
.await
.map_err(|e| e.into())
}

async fn execute(self) -> Result<(), Box<dyn std::error::Error>> {
// Nonce should be 0 in genesis
let nonce = self.eth_get_transaction_count(self.from_addr).await;
Expand Down Expand Up @@ -253,6 +316,15 @@ impl TestClient {
assert_eq!(latest_block.transactions.len(), 1);
assert_eq!(latest_block.transactions[0].hash, tx_hash);

// This should just pass without error
self.set_value_call(contract_address, set_arg)
.await
.unwrap();

// This call should fail because function does not exist
let failing_call = self.failing_call(contract_address).await;
assert!(failing_call.is_err());

// Create a blob with multiple transactions.
let mut requests = Vec::default();
for value in 100..103 {
Expand Down
2 changes: 1 addition & 1 deletion full-node/sov-ethereum/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ pub mod experimental {
let tx_hash = signed_transaction.hash();
let sender = signed_transaction
.recover_signer()
.ok_or(sov_evm::RawEvmTxConversionError::FailedToRecoverSigner)?;
.ok_or(sov_evm::EthApiError::InvalidTransactionSignature)?;

let mut nonces = self.nonces.lock().unwrap();
let nonce = *nonces.entry(sender).and_modify(|n| *n += 1).or_insert(0);
Expand Down
1 change: 1 addition & 0 deletions module-system/module-implementations/sov-evm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ revm = { workspace = true, features = [
"optional_no_base_fee",
] }
reth-primitives = { workspace = true }
reth-interfaces = { workspace = true }
reth-rpc-types = { workspace = true }
reth-rpc-types-compat = { workspace = true }
reth-revm = { workspace = true }
Expand Down
192 changes: 192 additions & 0 deletions module-system/module-implementations/sov-evm/src/evm/call.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// https://github.com/paradigmxyz/reth/blob/main/crates/rpc/rpc/src/eth/revm_utils.rs

use reth_primitives::{AccessList, H256, U256};
use reth_rpc_types::CallRequest;
use revm::primitives::{TransactTo, TxEnv};

use crate::error::rpc::{EthApiError, EthResult, RpcInvalidTransactionError};
use crate::primitive_types::BlockEnv;

/// Helper type for representing the fees of a [CallRequest]
pub(crate) struct CallFees {
/// EIP-1559 priority fee
max_priority_fee_per_gas: Option<U256>,
/// Unified gas price setting
///
/// Will be the configured `basefee` if unset in the request
///
/// `gasPrice` for legacy,
/// `maxFeePerGas` for EIP-1559
gas_price: U256,
/// Max Fee per Blob gas for EIP-4844 transactions
// https://github.com/Sovereign-Labs/sovereign-sdk/issues/912
#[allow(dead_code)]
max_fee_per_blob_gas: Option<U256>,
}

// === impl CallFees ===

impl CallFees {
/// Ensures the fields of a [CallRequest] are not conflicting.
///
/// If no `gasPrice` or `maxFeePerGas` is set, then the `gas_price` in the returned `gas_price`
/// will be `0`. See: <https://github.com/ethereum/go-ethereum/blob/2754b197c935ee63101cbbca2752338246384fec/internal/ethapi/transaction_args.go#L242-L255>
///
/// # EIP-4844 transactions
///
/// Blob transactions have an additional fee parameter `maxFeePerBlobGas`.
/// If the `maxFeePerBlobGas` or `blobVersionedHashes` are set we treat it as an EIP-4844
/// transaction.
///
/// Note: Due to the `Default` impl of [BlockEnv] (Some(0)) this assumes the `block_blob_fee` is
/// always `Some`
fn ensure_fees(
call_gas_price: Option<U256>,
call_max_fee: Option<U256>,
call_priority_fee: Option<U256>,
block_base_fee: U256,
blob_versioned_hashes: Option<&[H256]>,
max_fee_per_blob_gas: Option<U256>,
block_blob_fee: Option<U256>,
) -> EthResult<CallFees> {
/// Ensures that the transaction's max fee is lower than the priority fee, if any.
fn ensure_valid_fee_cap(
max_fee: U256,
max_priority_fee_per_gas: Option<U256>,
) -> EthResult<()> {
if let Some(max_priority) = max_priority_fee_per_gas {
if max_priority > max_fee {
// Fail early
return Err(
// `max_priority_fee_per_gas` is greater than the `max_fee_per_gas`
RpcInvalidTransactionError::TipAboveFeeCap.into(),
);
}
}
Ok(())
}

let has_blob_hashes = blob_versioned_hashes
.as_ref()
.map(|blobs| !blobs.is_empty())
.unwrap_or(false);

match (
call_gas_price,
call_max_fee,
call_priority_fee,
max_fee_per_blob_gas,
) {
(gas_price, None, None, None) => {
// either legacy transaction or no fee fields are specified
// when no fields are specified, set gas price to zero
let gas_price = gas_price.unwrap_or(U256::ZERO);
Ok(CallFees {
gas_price,
max_priority_fee_per_gas: None,
max_fee_per_blob_gas: has_blob_hashes.then_some(block_blob_fee).flatten(),
})
}
(None, max_fee_per_gas, max_priority_fee_per_gas, None) => {
// request for eip-1559 transaction
let max_fee = max_fee_per_gas.unwrap_or(block_base_fee);
ensure_valid_fee_cap(max_fee, max_priority_fee_per_gas)?;

let max_fee_per_blob_gas = has_blob_hashes.then_some(block_blob_fee).flatten();

Ok(CallFees {
gas_price: max_fee,
max_priority_fee_per_gas,
max_fee_per_blob_gas,
})
}
(None, max_fee_per_gas, max_priority_fee_per_gas, Some(max_fee_per_blob_gas)) => {
// request for eip-4844 transaction
let max_fee = max_fee_per_gas.unwrap_or(block_base_fee);
ensure_valid_fee_cap(max_fee, max_priority_fee_per_gas)?;

// Ensure blob_hashes are present
if !has_blob_hashes {
// Blob transaction but no blob hashes
return Err(RpcInvalidTransactionError::BlobTransactionMissingBlobHashes.into());
}

Ok(CallFees {
gas_price: max_fee,
max_priority_fee_per_gas,
max_fee_per_blob_gas: Some(max_fee_per_blob_gas),
})
}
_ => {
// this fallback covers incompatible combinations of fields
Err(EthApiError::ConflictingFeeFieldsInRequest)
}
}
}
}

// https://github.com/paradigmxyz/reth/blob/d8677b4146f77c7c82d659c59b79b38caca78778/crates/rpc/rpc/src/eth/revm_utils.rs#L201
pub(crate) fn prepare_call_env(block_env: &BlockEnv, request: CallRequest) -> EthResult<TxEnv> {
let CallRequest {
from,
to,
gas_price,
max_fee_per_gas,
max_priority_fee_per_gas,
gas,
value,
input,
nonce,
access_list,
chain_id,
..
} = request;

let CallFees {
max_priority_fee_per_gas,
gas_price,
// https://github.com/Sovereign-Labs/sovereign-sdk/issues/912
max_fee_per_blob_gas: _,
} = CallFees::ensure_fees(
gas_price,
max_fee_per_gas,
max_priority_fee_per_gas,
U256::from(block_env.basefee),
// EIP-4844 related fields
// https://github.com/Sovereign-Labs/sovereign-sdk/issues/912
None,
None,
None,
)?;

let gas_limit = gas.unwrap_or(U256::from(block_env.gas_limit.min(u64::MAX)));

let env = TxEnv {
gas_limit: gas_limit
.try_into()
.map_err(|_| RpcInvalidTransactionError::GasUintOverflow)?,
nonce: nonce
.map(|n| {
n.try_into()
.map_err(|_| RpcInvalidTransactionError::NonceTooHigh)
})
.transpose()?,
caller: from.unwrap_or_default(),
gas_price,
gas_priority_fee: max_priority_fee_per_gas,
transact_to: to.map(TransactTo::Call).unwrap_or_else(TransactTo::create),
value: value.unwrap_or_default(),
data: input
.try_into_unique_input()?
.map(|data| data.0)
.unwrap_or_default(),
chain_id: chain_id.map(|c| c.as_u64()),
access_list: access_list.map(AccessList::flattened).unwrap_or_default(),
// EIP-4844 related fields
// https://github.com/Sovereign-Labs/sovereign-sdk/issues/912
blob_hashes: vec![],
max_fee_per_blob_gas: None,
};

Ok(env)
}
Loading