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

feat(net): test syncing from geth #623

Merged
merged 27 commits into from
Jan 31, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
cacb064
feat(tests): add geth interop integration tests
Rjected Jan 18, 2023
705ac1e
add keepalive test with only the network
Rjected Jan 19, 2023
cc497fe
use header and status conversions
Rjected Jan 19, 2023
68f5fc7
use chainspec to initialize network
Rjected Jan 20, 2023
8e464f7
correct local genesis hash calculation
Rjected Jan 20, 2023
588b030
set keepalive test timeout to 30secs
Rjected Jan 20, 2023
9299a4e
tell the pipeline to stop when it's done
Rjected Jan 20, 2023
67b1b38
refactor with geth utilities from ethers
Rjected Jan 22, 2023
0bb5a81
accomodate staged-sync refactor
Rjected Jan 22, 2023
89d07d8
remove unnecessary stage config fields
Rjected Jan 22, 2023
54522bd
correct reth builder comment
Rjected Jan 22, 2023
caa053f
use new PipelineBuilder instead of RethTestBuilder
Rjected Jan 27, 2023
c7c5017
integrate sync tests in staged-sync crate
Rjected Jan 30, 2023
b0c9765
remove tests crate
Rjected Jan 30, 2023
d50660b
use a result in enable_mining
Rjected Jan 30, 2023
ca770fd
add CliqueGethInstance example
Rjected Jan 30, 2023
75e4ff3
remove final assert in CliqueMiddleware
Rjected Jan 30, 2023
359c9b4
properly configure geth with p2p port
Rjected Jan 30, 2023
ebada9a
update ethers
Rjected Jan 30, 2023
d4881bc
revert forkkind change
Rjected Jan 30, 2023
50dc5d2
init genesis and switch to tempdir path
Rjected Jan 30, 2023
3e56757
fix doctests
Rjected Jan 30, 2023
eea9265
add no_run to clique doctest
Rjected Jan 30, 2023
eec3449
fix: remove lib.rs, caused tests to double execute
gakonst Jan 31, 2023
e6b4844
Merge branch 'main' into dan/geth-sync-test
gakonst Jan 31, 2023
96bcea0
cleanup: only run 1 test and check we got the right peerid
gakonst Jan 31, 2023
2c3f45e
chore: fix doctest
gakonst Jan 31, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
476 changes: 252 additions & 224 deletions Cargo.lock

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions crates/staged-sync/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ reth-network = {path = "../../crates/net/network", features = ["serde"] }
reth-primitives = { path = "../../crates/primitives" }
reth-provider = { path = "../../crates/storage/provider", features = ["test-utils"] }
reth-net-nat = { path = "../../crates/net/nat" }
reth-interfaces = { path = "../interfaces", optional = true }

# io
serde = "1.0"
Expand All @@ -32,3 +33,64 @@ walkdir = "2.3.2"
eyre = "0.6.8"
shellexpand = "3.0.0"
tracing = "0.1.37"

# crypto
rand = { version = "0.8", optional = true }

# errors
thiserror = { version = "1", optional = true }

# enr
enr = { version = "0.7.0", features = ["serde", "rust-secp256k1"], optional = true }

# ethers
ethers-core = { git = "https://github.com/gakonst/ethers-rs", default-features = false, optional = true }
ethers-providers = { git = "https://github.com/gakonst/ethers-rs", features = ["ws"], default-features = false, optional = true }
ethers-middleware = { git = "https://github.com/gakonst/ethers-rs", default-features = false, optional = true }
ethers-signers = { git = "https://github.com/gakonst/ethers-rs", default-features = false, optional = true }

# async / futures
async-trait = { version = "0.1", optional = true }
tokio = { version = "1", features = ["io-util", "net", "macros", "rt-multi-thread", "time"], optional = true }

# misc
tempfile = { version = "3.3", optional = true }
hex = { version = "0.4", optional = true }

[dev-dependencies]
# reth crates
reth-tracing = { path = "../tracing" }
reth-stages = { path = "../stages" }
reth-downloaders = { path = "../net/downloaders" }
reth-staged-sync = { path = ".", features = ["test-utils"] }

# async/futures
futures = "0.3"
tokio = { version = "1", features = ["io-util", "net", "macros", "rt-multi-thread", "time"] }
tokio-stream = "0.1"

# crypto
secp256k1 = { version = "0.24", features = [
"global-context",
"rand-std",
"recovery",
] }

[features]
test-utils = [
"reth-network/test-utils",
"reth-interfaces/test-utils",
"reth-network/test-utils",
"reth-provider/test-utils",
"dep:enr",
"dep:ethers-core",
"dep:tempfile",
"dep:thiserror",
"dep:hex",
"dep:rand",
"dep:tokio",
"dep:ethers-signers",
"dep:ethers-providers",
"dep:ethers-middleware",
"dep:async-trait"
]
4 changes: 4 additions & 0 deletions crates/staged-sync/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@ pub mod config;
pub use config::Config;

pub mod utils;

#[cfg(any(test, feature = "test-utils"))]
/// Common helpers for integration testing.
pub mod test_utils;
Comment on lines +5 to +8
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if staged-sync is the right crate here, I guess so we can use the pipeline and test certain parts of it against a GethInstance?

perhaps it would be appropriate to move to its own crate?

wdyt @onbjerg ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefer it here, or in any node-named packages (ref #1079 )

120 changes: 120 additions & 0 deletions crates/staged-sync/src/test_utils/clique.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
//! Helper struct for working with a clique geth instance.

use enr::k256::ecdsa::SigningKey;
use ethers_core::utils::{Geth, GethInstance};
use ethers_middleware::SignerMiddleware;
use ethers_providers::{Provider, Ws};
use ethers_signers::{LocalWallet, Wallet};
use std::{
io::{BufRead, BufReader},
net::SocketAddr,
};

/// A [`Geth`](ethers_core::utils::Geth) instance configured with Clique and a custom
/// [`Genesis`](ethers_core::utils::Genesis).
///
/// This holds a [`SignerMiddleware`](ethers_middleware::signer_middleware::SignerMiddleware) for
/// enabling block production and creating transactions.
///
/// # Example
/// ```
/// # use ethers_core::utils::Geth;
/// # use reth_staged_sync::test_utils::CliqueGethInstance;
///
/// // this creates a funded geth
/// let clique_geth = Geth::new()
/// .chain_id(chain_id);
///
/// // build the funded geth, generating a random signing key and enabling clique
/// let (mut clique, provider) = CliqueGethInstance::new(clique_geth, None).await;
///
/// // don't print logs, but drain the stderr
/// clique.prevent_blocking().await;
/// ```
pub struct CliqueGethInstance(
/// The spawned [`GethInstance`](ethers_core::utils::GethInstance).
pub GethInstance,
);

impl CliqueGethInstance {
/// Sets up a new [`SignerMiddleware`](ethers_middleware::signer_middleware::SignerMiddleware)
/// for the [`Geth`](ethers_core::utils::Geth) instance and returns the
/// [`CliqueGethInstance`].
///
/// The signer is assumed to be the clique signer and the signer for any transactions sent for
/// block production.
///
/// This also spawns the geth instance.
pub async fn new(
geth: Geth,
signer: Option<SigningKey>,
) -> (Self, SignerMiddleware<Provider<Ws>, Wallet<SigningKey>>) {
let signer = signer.unwrap_or_else(|| SigningKey::random(&mut rand::thread_rng()));

let geth = geth.set_clique_private_key(signer.clone());

// spawn the geth instance
let instance = geth.spawn();

// create the signer
let wallet: LocalWallet = signer.clone().into();

// set up ethers provider
let geth_endpoint = SocketAddr::new([127, 0, 0, 1].into(), instance.port()).to_string();
let provider = Provider::<Ws>::connect(format!("ws://{geth_endpoint}")).await.unwrap();
let provider =
SignerMiddleware::new_with_provider_chain(provider, wallet.clone()).await.unwrap();

(Self(instance), provider)
}

/// Prints the logs of the [`Geth`](ethers_core::utils::Geth) instance in a new
/// [`task`](tokio::task).
#[allow(dead_code)]
pub async fn print_logs(&mut self) {
// take the stderr of the geth instance and print it
let stderr = self.0.stderr().unwrap();

// print logs in a new task
let mut err_reader = BufReader::new(stderr);

tokio::spawn(async move {
loop {
if let (Ok(line), line_str) = {
let mut buf = String::new();
(err_reader.read_line(&mut buf), buf.clone())
} {
if line == 0 {
break
}
if !line_str.is_empty() {
dbg!(line_str);
}
}
}
});
}

/// Prevents the [`Geth`](ethers_core::utils::Geth) instance from blocking due to the `stderr`
/// filling up.
pub async fn prevent_blocking(&mut self) {
// take the stderr of the geth instance and print it
let stderr = self.0.stderr().unwrap();

// print logs in a new task
let mut err_reader = BufReader::new(stderr);

tokio::spawn(async move {
loop {
if let (Ok(line), _line_str) = {
let mut buf = String::new();
(err_reader.read_line(&mut buf), buf.clone())
} {
if line == 0 {
break
}
}
}
Comment on lines +109 to +119
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is just for debugging, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

print_logs is for debugging, but prevent_blocking is necessary to prevent the stderr pipe buffer from filling up and causing the test to hang. Using Drop doesn't seem to work here because it causes geth to crash (probably due to EPIPE, but I'm not entirely sure). I'm not sure if there's a better way to prevent the stderr pipe buffer from filling though

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't we send to /dev/null instead and not bother with this? either way let's do in a followup

Copy link
Member Author

@Rjected Rjected Jan 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can't do it by default because Geth captures the stderr to make sure rpc, p2p, etc are up when spawn is called. So have to first pipe the Command's output with Stdio::piped(). Maybe we could spawn cat or something, with geth's stderr as cat's stdin, and pipe cat to /dev/null

});
}
}
126 changes: 126 additions & 0 deletions crates/staged-sync/src/test_utils/clique_middleware.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//! Helper extension traits for working with clique providers.

use async_trait::async_trait;
use enr::k256::ecdsa::SigningKey;
use ethers_core::{
types::{transaction::eip2718::TypedTransaction, Address, Block, BlockNumber, H256},
utils::secret_key_to_address,
};
use ethers_middleware::SignerMiddleware;
use ethers_providers::Middleware;
use ethers_signers::Signer;
use reth_network::test_utils::enr_to_peer_id;
use reth_primitives::PeerId;
use thiserror::Error;
use tracing::trace;

/// An error that can occur when using the [`CliqueMiddleware`].
#[derive(Error, Debug)]
pub enum CliqueError<E> {
/// Error encountered when using the provider
#[error(transparent)]
ProviderError(#[from] E),

/// No genesis block returned from the provider
#[error("no genesis block returned from the provider")]
NoGenesis,

/// No tip block returned from the provider
#[error("no tip block returned from the provider")]
NoTip,

/// Account was not successfully unlocked on the provider
#[error("account was not successfully unlocked on the provider")]
AccountNotUnlocked,

/// Mining was not successfully enabled on the provider
#[error("mining was not successfully enabled on the provider")]
MiningNotEnabled,

/// Mismatch between locally computed address and address returned from the provider
#[error("local address {local} does not match remote address {remote}")]
AddressMismatch {
/// The locally computed address
local: Address,

/// The address returned from the provider
remote: Address,
},
}

/// Error type for [`CliqueMiddleware`].
pub type CliqueMiddlewareError<M> = CliqueError<<M as Middleware>::Error>;

/// Extension trait for [`Middleware`] to provide clique specific functionality.
#[async_trait(?Send)]
pub trait CliqueMiddleware: Send + Sync + Middleware {
/// Enable mining on the clique geth instance by importing and unlocking the signer account
/// derived from given private key and password.
async fn enable_mining(
&self,
signer: SigningKey,
password: String,
) -> Result<(), CliqueMiddlewareError<Self>> {
let our_address = secret_key_to_address(&signer);

// send the private key to geth and unlock it
let key_bytes = signer.to_bytes().to_vec().into();
trace!(
private_key=%hex::encode(&key_bytes),
"Importing private key"
);

let unlocked_addr = self.import_raw_key(key_bytes, password.to_string()).await?;
if unlocked_addr != our_address {
return Err(CliqueError::AddressMismatch { local: our_address, remote: unlocked_addr })
}

let unlock_success = self.unlock_account(our_address, password.to_string(), None).await?;

if !unlock_success {
return Err(CliqueError::AccountNotUnlocked)
}

// start mining?
self.start_mining(None).await?;

// check that we are mining
let mining = self.mining().await?;
if !mining {
return Err(CliqueError::MiningNotEnabled)
}
Ok(())
}

/// Returns the chain tip of the [`Geth`](ethers_core::utils::Geth) instance by calling
/// geth's `eth_getBlock`.
async fn remote_tip_block(&self) -> Result<Block<H256>, CliqueMiddlewareError<Self>> {
self.get_block(BlockNumber::Latest).await?.ok_or(CliqueError::NoTip)
}

/// Returns the genesis block of the [`Geth`](ethers_core::utils::Geth) instance by calling
/// geth's `eth_getBlock`.
async fn remote_genesis_block(&self) -> Result<Block<H256>, CliqueMiddlewareError<Self>> {
self.get_block(BlockNumber::Earliest).await?.ok_or(CliqueError::NoGenesis)
}

/// Signs and sends the given unsigned transactions sequentially, signing with the private key
/// used to configure the [`CliqueGethInstance`].
async fn send_requests<T: IntoIterator<Item = TypedTransaction>>(
&self,
txs: T,
) -> Result<(), CliqueMiddlewareError<Self>> {
for tx in txs {
self.send_transaction(tx, None).await?;
}
Ok(())
}

/// Returns the [`Geth`](ethers_core::utils::Geth) instance [`PeerId`](reth_primitives::PeerId)
/// by calling geth's `admin_nodeInfo`.
async fn peer_id(&self) -> Result<PeerId, CliqueMiddlewareError<Self>> {
Ok(enr_to_peer_id(self.node_info().await?.enr))
}
}

impl<M: Middleware, S: Signer> CliqueMiddleware for SignerMiddleware<M, S> {}
9 changes: 9 additions & 0 deletions crates/staged-sync/src/test_utils/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#![warn(missing_docs, unreachable_pub)]

//! Common helpers for staged sync integration testing.

pub mod clique;
pub mod clique_middleware;

pub use clique::CliqueGethInstance;
pub use clique_middleware::{CliqueError, CliqueMiddleware, CliqueMiddlewareError};
10 changes: 10 additions & 0 deletions crates/staged-sync/tests/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//! Integration tests and test helpers for reth.
#![cfg(test)]
#![warn(missing_docs, unreachable_pub)]
#![deny(unused_must_use, rust_2018_idioms)]
#![doc(test(
no_crate_inject,
attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables))
))]

pub mod sync;