Make transactions in execution layer integration tests (#3320)

## Issue Addressed

Resolves #3159 

## Proposed Changes

Sends transactions to the EE before requesting for a payload in the `execution_integration_tests`. Made some changes to the integration tests in order to be able to sign and publish transactions to the EE:

1. `genesis.json` for both geth and nethermind was modified to include pre-funded accounts that we know private keys for 
2. Using the unauthenticated port again in order to make `eth_sendTransaction` and calls from the `personal` namespace to import keys

Also added a `fcu` call with `PayloadAttributes` before calling `getPayload` in order to give EEs sufficient time to pack transactions into the payload.
This commit is contained in:
Pawan Dhananjay
2022-07-18 01:51:36 +00:00
parent 2ed51c364d
commit da7b7a0f60
9 changed files with 541 additions and 13 deletions

View File

@@ -1,3 +1,4 @@
use ethers_providers::{Http, Provider};
use execution_layer::DEFAULT_JWT_FILE;
use sensitive_url::SensitiveUrl;
use std::path::PathBuf;
@@ -5,6 +6,14 @@ use std::process::Child;
use tempfile::TempDir;
use unused_port::unused_tcp_port;
pub const KEYSTORE_PASSWORD: &str = "testpwd";
pub const ACCOUNT1: &str = "7b8C3a386C0eea54693fFB0DA17373ffC9228139";
pub const ACCOUNT2: &str = "dA2DD7560DB7e212B945fC72cEB54B7D8C886D77";
pub const PRIVATE_KEYS: [&str; 2] = [
"115fe42a60e5ef45f5490e599add1f03c73aeaca129c2c41451eca6cf8ff9e04",
"6a692e710077d9000be1326acbe32f777b403902ac8779b19eb1398b849c99c3",
];
/// Defined for each EE type (e.g., Geth, Nethermind, etc).
pub trait GenericExecutionEngine: Clone {
fn init_datadir() -> TempDir;
@@ -22,8 +31,10 @@ pub struct ExecutionEngine<E> {
engine: E,
#[allow(dead_code)]
datadir: TempDir,
http_port: u16,
http_auth_port: u16,
child: Child,
pub provider: Provider<Http>,
}
impl<E> Drop for ExecutionEngine<E> {
@@ -42,11 +53,15 @@ impl<E: GenericExecutionEngine> ExecutionEngine<E> {
let http_port = unused_tcp_port().unwrap();
let http_auth_port = unused_tcp_port().unwrap();
let child = E::start_client(&datadir, http_port, http_auth_port, jwt_secret_path);
let provider = Provider::<Http>::try_from(format!("http://localhost:{}", http_port))
.expect("failed to instantiate ethers provider");
Self {
engine,
datadir,
http_port,
http_auth_port,
child,
provider,
}
}
@@ -54,6 +69,10 @@ impl<E: GenericExecutionEngine> ExecutionEngine<E> {
SensitiveUrl::parse(&format!("http://127.0.0.1:{}", self.http_auth_port)).unwrap()
}
pub fn http_url(&self) -> SensitiveUrl {
SensitiveUrl::parse(&format!("http://127.0.0.1:{}", self.http_port)).unwrap()
}
pub fn datadir(&self) -> PathBuf {
self.datadir.path().to_path_buf()
}

View File

@@ -32,7 +32,12 @@ pub fn geth_genesis_json() -> Value {
"mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000",
"coinbase":"0x0000000000000000000000000000000000000000",
"alloc":{
"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b":{"balance":"0x6d6172697573766477000000"}
"0x7b8C3a386C0eea54693fFB0DA17373ffC9228139": {
"balance": "10000000000000000000000000"
},
"0xdA2DD7560DB7e212B945fC72cEB54B7D8C886D77": {
"balance": "10000000000000000000000000"
},
},
"number":"0x0",
"gasUsed":"0x0",
@@ -40,3 +45,87 @@ pub fn geth_genesis_json() -> Value {
"baseFeePerGas":"0x7"
})
}
/// Modified kiln config
pub fn nethermind_genesis_json() -> Value {
json!(
{
"name": "lighthouse_test_network",
"engine": {
"Ethash": {
"params": {
"minimumDifficulty": "0x20000",
"difficultyBoundDivisor": "0x800",
"durationLimit": "0xd",
"blockReward": {
"0x0": "0x1BC16D674EC80000"
},
"homesteadTransition": "0x0",
"eip100bTransition": "0x0",
"difficultyBombDelays": {}
}
}
},
"params": {
"gasLimitBoundDivisor": "0x400",
"registrar": "0x0000000000000000000000000000000000000000",
"accountStartNonce": "0x0",
"maximumExtraDataSize": "0x20",
"minGasLimit": "0x1388",
"networkID": "0x1469ca",
"MergeForkIdTransition": "0x3e8",
"eip150Transition": "0x0",
"eip158Transition": "0x0",
"eip160Transition": "0x0",
"eip161abcTransition": "0x0",
"eip161dTransition": "0x0",
"eip155Transition": "0x0",
"eip140Transition": "0x0",
"eip211Transition": "0x0",
"eip214Transition": "0x0",
"eip658Transition": "0x0",
"eip145Transition": "0x0",
"eip1014Transition": "0x0",
"eip1052Transition": "0x0",
"eip1283Transition": "0x0",
"eip1283DisableTransition": "0x0",
"eip152Transition": "0x0",
"eip1108Transition": "0x0",
"eip1344Transition": "0x0",
"eip1884Transition": "0x0",
"eip2028Transition": "0x0",
"eip2200Transition": "0x0",
"eip2565Transition": "0x0",
"eip2929Transition": "0x0",
"eip2930Transition": "0x0",
"eip1559Transition": "0x0",
"eip3198Transition": "0x0",
"eip3529Transition": "0x0",
"eip3541Transition": "0x0"
},
"genesis": {
"seal": {
"ethereum": {
"nonce": "0x1234",
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000"
}
},
"difficulty": "0x01",
"author": "0x0000000000000000000000000000000000000000",
"timestamp": "0x0",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"extraData": "",
"gasLimit": "0x1C9C380"
},
"accounts": {
"0x7b8C3a386C0eea54693fFB0DA17373ffC9228139": {
"balance": "10000000000000000000000000"
},
"0xdA2DD7560DB7e212B945fC72cEB54B7D8C886D77": {
"balance": "10000000000000000000000000"
},
},
"nodes": []
}
)
}

View File

@@ -90,13 +90,14 @@ impl GenericExecutionEngine for GethEngine {
.arg(datadir.path().to_str().unwrap())
.arg("--http")
.arg("--http.api")
.arg("engine,eth")
.arg("engine,eth,personal")
.arg("--http.port")
.arg(http_port.to_string())
.arg("--authrpc.port")
.arg(http_auth_port.to_string())
.arg("--port")
.arg(network_port.to_string())
.arg("--allow-insecure-unlock")
.arg("--authrpc.jwtsecret")
.arg(jwt_secret_path.as_path().to_str().unwrap())
.stdout(build_utils::build_stdio())

View File

@@ -1,3 +1,4 @@
#![recursion_limit = "1024"]
/// This binary runs integration tests between Lighthouse and execution engines.
///
/// It will first attempt to build any supported integration clients, then it will run tests.
@@ -9,6 +10,7 @@ mod genesis_json;
mod geth;
mod nethermind;
mod test_rig;
mod transactions;
use geth::GethEngine;
use nethermind::NethermindEngine;

View File

@@ -1,6 +1,8 @@
use crate::build_utils;
use crate::execution_engine::GenericExecutionEngine;
use crate::genesis_json::nethermind_genesis_json;
use std::env;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Output};
use tempfile::TempDir;
@@ -69,33 +71,43 @@ impl NethermindEngine {
impl GenericExecutionEngine for NethermindEngine {
fn init_datadir() -> TempDir {
TempDir::new().unwrap()
let datadir = TempDir::new().unwrap();
let genesis_json_path = datadir.path().join("genesis.json");
let mut file = File::create(&genesis_json_path).unwrap();
let json = nethermind_genesis_json();
serde_json::to_writer(&mut file, &json).unwrap();
datadir
}
fn start_client(
datadir: &TempDir,
_http_port: u16,
http_port: u16,
http_auth_port: u16,
jwt_secret_path: PathBuf,
) -> Child {
let network_port = unused_tcp_port().unwrap();
let genesis_json_path = datadir.path().join("genesis.json");
Command::new(Self::binary_path())
.arg("--datadir")
.arg(datadir.path().to_str().unwrap())
.arg("--config")
.arg("kiln")
.arg("--Init.ChainSpecPath")
.arg(genesis_json_path.to_str().unwrap())
.arg("--Merge.TerminalTotalDifficulty")
.arg("0")
.arg("--JsonRpc.Enabled")
.arg("true")
.arg("--JsonRpc.EnabledModules")
.arg("net,eth,subscribe,web3,admin,personal")
.arg("--JsonRpc.Port")
.arg(http_port.to_string())
.arg("--JsonRpc.AdditionalRpcUrls")
.arg(format!(
"http://localhost:{}|http;ws|net;eth;subscribe;engine;web3;client",
http_auth_port
))
.arg("--JsonRpc.EnabledModules")
.arg("net,eth,subscribe,web3,admin,engine")
.arg("--JsonRpc.Port")
.arg(http_auth_port.to_string())
.arg("--Network.DiscoveryPort")
.arg(network_port.to_string())
.arg("--Network.P2PPort")

View File

@@ -1,5 +1,12 @@
use crate::execution_engine::{ExecutionEngine, GenericExecutionEngine};
use crate::execution_engine::{
ExecutionEngine, GenericExecutionEngine, ACCOUNT1, ACCOUNT2, KEYSTORE_PASSWORD, PRIVATE_KEYS,
};
use crate::transactions::transactions;
use ethers_providers::Middleware;
use execution_layer::{ExecutionLayer, PayloadAttributes, PayloadStatus};
use reqwest::{header::CONTENT_TYPE, Client};
use sensitive_url::SensitiveUrl;
use serde_json::{json, Value};
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use task_executor::TaskExecutor;
@@ -8,7 +15,6 @@ use types::{
Address, ChainSpec, EthSpec, ExecutionBlockHash, ExecutionPayload, FullPayload, Hash256,
MainnetEthSpec, Slot, Uint256,
};
const EXECUTION_ENGINE_START_TIMEOUT: Duration = Duration::from_secs(20);
struct ExecutionPair<E, T: EthSpec> {
@@ -32,6 +38,63 @@ pub struct TestRig<E, T: EthSpec = MainnetEthSpec> {
_runtime_shutdown: exit_future::Signal,
}
/// Import a private key into the execution engine and unlock it so that we can
/// make transactions with the corresponding account.
async fn import_and_unlock(http_url: SensitiveUrl, priv_keys: &[&str], password: &str) {
for priv_key in priv_keys {
let body = json!(
{
"jsonrpc":"2.0",
"method":"personal_importRawKey",
"params":[priv_key, password],
"id":1
}
);
let client = Client::builder().build().unwrap();
let request = client
.post(http_url.full.clone())
.header(CONTENT_TYPE, "application/json")
.json(&body);
let response: Value = request
.send()
.await
.unwrap()
.error_for_status()
.unwrap()
.json()
.await
.unwrap();
let account = response.get("result").unwrap().as_str().unwrap();
let body = json!(
{
"jsonrpc":"2.0",
"method":"personal_unlockAccount",
"params":[account, password],
"id":1
}
);
let request = client
.post(http_url.full.clone())
.header(CONTENT_TYPE, "application/json")
.json(&body);
let _response: Value = request
.send()
.await
.unwrap()
.error_for_status()
.unwrap()
.json()
.await
.unwrap();
}
}
impl<E: GenericExecutionEngine> TestRig<E> {
pub fn new(generic_engine: E) -> Self {
let log = environment::null_logger().unwrap();
@@ -125,6 +188,20 @@ impl<E: GenericExecutionEngine> TestRig<E> {
pub async fn perform_tests(&self) {
self.wait_until_synced().await;
// Import and unlock all private keys to sign transactions
let _ = futures::future::join_all([&self.ee_a, &self.ee_b].iter().map(|ee| {
import_and_unlock(
ee.execution_engine.http_url(),
&PRIVATE_KEYS,
KEYSTORE_PASSWORD,
)
}))
.await;
// We hardcode the accounts here since some EEs start with a default unlocked account
let account1 = ethers_core::types::Address::from_slice(&hex::decode(&ACCOUNT1).unwrap());
let account2 = ethers_core::types::Address::from_slice(&hex::decode(&ACCOUNT2).unwrap());
/*
* Check the transition config endpoint.
*/
@@ -157,6 +234,17 @@ impl<E: GenericExecutionEngine> TestRig<E> {
.unwrap()
);
// Submit transactions before getting payload
let txs = transactions::<MainnetEthSpec>(account1, account2);
for tx in txs.clone().into_iter() {
self.ee_a
.execution_engine
.provider
.send_transaction(tx, None)
.await
.unwrap();
}
/*
* Execution Engine A:
*
@@ -168,6 +256,45 @@ impl<E: GenericExecutionEngine> TestRig<E> {
let prev_randao = Hash256::zero();
let finalized_block_hash = ExecutionBlockHash::zero();
let proposer_index = 0;
let prepared = self
.ee_a
.execution_layer
.insert_proposer(
Slot::new(1), // Insert proposer for the next slot
Hash256::zero(),
proposer_index,
PayloadAttributes {
timestamp,
prev_randao,
suggested_fee_recipient: Address::zero(),
},
)
.await;
assert!(!prepared, "Inserting proposer for the first time");
// Make a fcu call with the PayloadAttributes that we inserted previously
let prepare = self
.ee_a
.execution_layer
.notify_forkchoice_updated(
parent_hash,
finalized_block_hash,
Slot::new(0),
Hash256::zero(),
)
.await
.unwrap();
assert_eq!(prepare, PayloadStatus::Valid);
// Add a delay to give the EE sufficient time to pack the
// submitted transactions into a payload.
// This is required when running on under resourced nodes and
// in CI.
sleep(Duration::from_secs(3)).await;
let valid_payload = self
.ee_a
.execution_layer
@@ -184,6 +311,8 @@ impl<E: GenericExecutionEngine> TestRig<E> {
.unwrap()
.execution_payload;
assert_eq!(valid_payload.transactions.len(), txs.len());
/*
* Execution Engine A:
*

View File

@@ -0,0 +1,87 @@
use deposit_contract::{encode_eth1_tx_data, BYTECODE, CONTRACT_DEPLOY_GAS, DEPOSIT_GAS};
use ethers_core::types::{
transaction::{eip2718::TypedTransaction, eip2930::AccessList},
Address, Bytes, Eip1559TransactionRequest, TransactionRequest,
};
use types::{DepositData, EthSpec, Hash256, Keypair, Signature};
/// Hardcoded deposit contract address based on sender address and nonce
pub const DEPOSIT_CONTRACT_ADDRESS: &str = "64f43BEc7F86526686C931d65362bB8698872F90";
#[derive(Debug)]
pub enum Transaction {
Transfer(Address, Address),
TransferLegacy(Address, Address),
TransferAccessList(Address, Address),
DeployDepositContract(Address),
DepositDepositContract {
sender: Address,
deposit_contract_address: Address,
},
}
/// Get a list of transactions to publish to the execution layer.
pub fn transactions<E: EthSpec>(account1: Address, account2: Address) -> Vec<TypedTransaction> {
vec![
Transaction::Transfer(account1, account2).transaction::<E>(),
Transaction::TransferLegacy(account1, account2).transaction::<E>(),
Transaction::TransferAccessList(account1, account2).transaction::<E>(),
Transaction::DeployDepositContract(account1).transaction::<E>(),
Transaction::DepositDepositContract {
sender: account1,
deposit_contract_address: ethers_core::types::Address::from_slice(
&hex::decode(&DEPOSIT_CONTRACT_ADDRESS).unwrap(),
),
}
.transaction::<E>(),
]
}
impl Transaction {
pub fn transaction<E: EthSpec>(&self) -> TypedTransaction {
match &self {
Self::TransferLegacy(from, to) => TransactionRequest::new()
.from(*from)
.to(*to)
.value(1)
.into(),
Self::Transfer(from, to) => Eip1559TransactionRequest::new()
.from(*from)
.to(*to)
.value(1)
.into(),
Self::TransferAccessList(from, to) => TransactionRequest::new()
.from(*from)
.to(*to)
.value(1)
.with_access_list(AccessList::default())
.into(),
Self::DeployDepositContract(addr) => TransactionRequest::new()
.from(*addr)
.data(Bytes::from(BYTECODE.to_vec()))
.gas(CONTRACT_DEPLOY_GAS)
.into(),
Self::DepositDepositContract {
sender,
deposit_contract_address,
} => {
let keypair = Keypair::random();
let mut deposit = DepositData {
pubkey: keypair.pk.into(),
withdrawal_credentials: Hash256::zero(),
amount: 32_000_000_000,
signature: Signature::empty().into(),
};
deposit.signature = deposit.create_signature(&keypair.sk, &E::default_spec());
TransactionRequest::new()
.from(*sender)
.to(*deposit_contract_address)
.data(Bytes::from(encode_eth1_tx_data(&deposit).unwrap()))
.gas(DEPOSIT_GAS)
.into()
}
}
}
}