Skip to content

Commit da95706

Browse files
authored
feat(rpc): Hive lean spec-assets test-driver endpoints (lambdaclass#357)
## Summary - Adds an opt-in `/lean/v0/test_driver/...` HTTP router gated by `HIVE_LEAN_TEST_DRIVER`, enabling the new lean spec-asset suites from [ethereum/hive#1480](ethereum/hive#1480) (`lean-spec-tests-{fork-choice,state-transition,verify-signatures}`) to drive ethlambda over HTTP. - In driver mode the binary skips the BlockChain actor and P2P swarm entirely and serves only the four new endpoints plus `/lean/v0/health`. State lives behind `Arc<RwLock<Store>>` so each `fork_choice/init` request replaces the store and a single container can replay many fixtures. - Reuses the production code paths (`store::on_tick` / `on_block_without_verification` / `on_gossip_*`, `state_transition::state_transition`, `verify_block_signatures`) so the driver exercises the same logic as the consensus stack. - Lifts the fork-choice and verify-signatures fixture deserialization types out of `crates/blockchain/tests/` into the shared `ethlambda-test-fixtures` crate so the RPC handler and offline spec-test runner consume the same JSON schemas. ### Endpoints (camelCase per the hive contract) ``` POST /lean/v0/test_driver/fork_choice/init -> 204 / 400 POST /lean/v0/test_driver/fork_choice/step -> { accepted, error?, snapshot } POST /lean/v0/test_driver/state_transition/run -> { succeeded, error?, post? } POST /lean/v0/test_driver/verify_signatures/run -> { succeeded, error? } ``` ### Hive run result End-to-end run against a local hive instance with ethlambda built from this branch — all three spec-asset suites green: | Suite | Result | |---|---| | `lean-spec-tests-fork-choice` | **85/85** ✅ | | `lean-spec-tests-state-transition`| **51/51** ✅ | | `lean-spec-tests-verify-signatures`| **12/12** ✅ | | **Total** | **148/148** ✅ | First fork-choice run was 84/85: a single fixture (`test_store_from_anchor_rejects_mismatched_state_root`) caught a missing anchor invariant in the init handler. Fix in `1aa9f8f` enforces `block.state_root == state.tree_hash_root()` (with the header's `state_root` zeroed), mirroring Ream's check, and a new e2e regression test (`init_with_garbage_block_state_root_returns_400`) covers the exact shape. The rerun went 85/85. ### Commits - `41b86fd` feat(rpc): add Hive lean spec-assets test-driver endpoints - `b1f2ba5` fix(bin): check `HIVE_LEAN_TEST_DRIVER` before reading `--node-key` - `1aa9f8f` fix(rpc): reject anchors whose `block.state_root` disagrees with the state ## Test plan - [x] `cargo fmt --all -- --check` - [x] `cargo clippy --workspace --tests --all-targets -- -D warnings` - [x] `cargo test -p ethlambda-rpc` — 18 lib + 8 e2e tests (the 8 e2e tests use `tower::ServiceExt::oneshot` to exercise every endpoint with real JSON bodies; one of them is a regression test for the anchor invariant from `1aa9f8f`) - [x] `cargo test --workspace --lib --bins` — all suites green - [x] Smoke test the binary in driver mode with a non-existent `--node-key`; confirm short-circuit + `/lean/v0/health` + `/state_transition/run` respond correctly - [x] Hive `lean-spec-tests-fork-choice` — **85/85 pass** - [x] Hive `lean-spec-tests-state-transition` — **51/51 pass** - [x] Hive `lean-spec-tests-verify-signatures` — **12/12 pass**
1 parent ee922d7 commit da95706

17 files changed

Lines changed: 1497 additions & 494 deletions

File tree

Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bin/ethlambda/src/main.rs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,6 @@ async fn main() -> eyre::Result<()> {
109109
ethlambda_blockchain::metrics::set_node_info("ethlambda", version::CLIENT_VERSION);
110110
ethlambda_blockchain::metrics::set_node_start_time();
111111

112-
let node_p2p_key = read_hex_file_bytes(&options.node_key);
113-
let p2p_socket = SocketAddr::new(IpAddr::from([0, 0, 0, 0]), options.gossipsub_port);
114112
let rpc_config = RpcConfig {
115113
http_address: options.http_address,
116114
api_port: options.api_port,
@@ -120,6 +118,22 @@ async fn main() -> eyre::Result<()> {
120118
println!("{ASCII_ART}");
121119

122120
info!(version = version::CLIENT_VERSION, "Starting ethlambda");
121+
122+
// Hive lean spec-asset suites boot the client with
123+
// HIVE_LEAN_TEST_DRIVER=1 so it skips the consensus/p2p stack and
124+
// exposes only the `/lean/v0/test_driver/...` endpoints driven by the
125+
// simulator. Detected here before any config / key / genesis loading
126+
// so the driver run doesn't touch --node-key, --custom-network-config-dir,
127+
// or any other consensus prerequisite the hive shim doesn't bother to
128+
// provision.
129+
if ethlambda_rpc::test_driver::test_driver_enabled() {
130+
info!("HIVE_LEAN_TEST_DRIVER detected; booting in test-driver mode");
131+
return run_test_driver(rpc_config).await;
132+
}
133+
134+
let node_p2p_key = read_hex_file_bytes(&options.node_key);
135+
let p2p_socket = SocketAddr::new(IpAddr::from([0, 0, 0, 0]), options.gossipsub_port);
136+
123137
#[cfg(not(target_env = "msvc"))]
124138
info!("Using jemalloc allocator with heap profiling enabled");
125139
#[cfg(target_env = "msvc")]
@@ -275,6 +289,40 @@ async fn main() -> eyre::Result<()> {
275289
Ok(())
276290
}
277291

292+
/// Boot the binary in Hive test-driver mode.
293+
///
294+
/// Skips every consensus/p2p subsystem and just exposes the
295+
/// `/lean/v0/test_driver/...` HTTP endpoints over the configured API port.
296+
/// The driver-mode store is seeded with an empty in-memory state and is
297+
/// replaced on every `fork_choice/init` request from the simulator.
298+
async fn run_test_driver(rpc_config: RpcConfig) -> eyre::Result<()> {
299+
use tokio::sync::RwLock;
300+
301+
let driver: ethlambda_rpc::test_driver::DriverState =
302+
Arc::new(RwLock::new(ethlambda_rpc::test_driver::empty_driver_store()));
303+
304+
let shutdown_token = CancellationToken::new();
305+
let rpc_shutdown = shutdown_token.clone();
306+
307+
let rpc_handle = tokio::spawn(async move {
308+
if let Err(err) =
309+
ethlambda_rpc::start_test_driver_rpc_server(rpc_config, driver, rpc_shutdown).await
310+
{
311+
error!(%err, "Test-driver RPC server failed");
312+
}
313+
});
314+
315+
info!("Test-driver RPC ready");
316+
317+
tokio::signal::ctrl_c().await.ok();
318+
info!("Shutdown signal received, stopping test-driver RPC...");
319+
shutdown_token.cancel();
320+
let _ = rpc_handle.await;
321+
info!("Shutdown complete");
322+
323+
Ok(())
324+
}
325+
278326
/// Subset of `validator-config.yaml` consumed by ethlambda.
279327
///
280328
/// The `config` block is a network-wide settings bag shared across clients;

crates/blockchain/src/store.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ fn on_block_core(
477477
let sig_verification_start = std::time::Instant::now();
478478
if verify {
479479
// Validate cryptographic signatures
480-
verify_signatures(&parent_state, &signed_block)?;
480+
verify_block_signatures(&parent_state, &signed_block)?;
481481
}
482482
let sig_verification = sig_verification_start.elapsed();
483483

@@ -1157,7 +1157,15 @@ fn build_block(
11571157
/// Verify all signatures in a signed block.
11581158
///
11591159
/// Each attestation has a corresponding proof in the signature list.
1160-
fn verify_signatures(state: &State, signed_block: &SignedBlock) -> Result<(), StoreError> {
1160+
///
1161+
/// Exposed publicly so RPC handlers (notably the Hive test-driver
1162+
/// `verify_signatures/run` endpoint) can run the exact same verification path
1163+
/// the import pipeline uses; the production import path also calls this from
1164+
/// [`on_block_core`].
1165+
pub fn verify_block_signatures(
1166+
state: &State,
1167+
signed_block: &SignedBlock,
1168+
) -> Result<(), StoreError> {
11611169
use ethlambda_crypto::verify_aggregated_signature;
11621170
use ethlambda_types::signature::ValidatorSignature;
11631171

@@ -1372,7 +1380,7 @@ mod tests {
13721380
},
13731381
};
13741382

1375-
let result = verify_signatures(&state, &signed_block);
1383+
let result = verify_block_signatures(&state, &signed_block);
13761384
assert!(
13771385
matches!(result, Err(StoreError::ParticipantsMismatch)),
13781386
"Expected ParticipantsMismatch, got: {result:?}"

crates/blockchain/tests/common.rs

Lines changed: 0 additions & 3 deletions
This file was deleted.

crates/blockchain/tests/forkchoice_spectests.rs

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,16 @@ use std::{
77
use ethlambda_blockchain::{MILLISECONDS_PER_INTERVAL, MILLISECONDS_PER_SLOT, store};
88
use ethlambda_storage::{Store, backend::InMemoryBackend};
99
use ethlambda_types::{
10-
attestation::{AttestationData, SignedAggregatedAttestation, SignedAttestation, XmssSignature},
11-
block::{AggregatedSignatureProof, Block, BlockSignatures, SignedBlock},
10+
attestation::{AttestationData, SignedAggregatedAttestation, SignedAttestation},
11+
block::{AggregatedSignatureProof, Block},
1212
primitives::{ByteList, H256, HashTreeRoot as _},
13-
signature::SIGNATURE_SIZE,
1413
state::State,
1514
};
1615

17-
use crate::types::{ForkChoiceTestVector, StoreChecks};
16+
use ethlambda_test_fixtures::fork_choice::{AttestationCheck, ForkChoiceTestVector, StoreChecks};
1817

1918
const SUPPORTED_FIXTURE_FORMAT: &str = "fork_choice_test";
2019

21-
mod common;
22-
mod types;
23-
2420
/// List of skipped tests.
2521
const SKIP_TESTS: &[&str] = &[];
2622

@@ -66,7 +62,7 @@ fn run(path: &Path) -> datatest_stable::Result<()> {
6662
block_registry.insert(label.clone(), root);
6763
}
6864

69-
let signed_block = build_signed_block(block_data);
65+
let signed_block = block_data.to_blank_signed_block();
7066

7167
let block_time_ms =
7268
genesis_time * 1000 + signed_block.message.slot * MILLISECONDS_PER_SLOT;
@@ -161,29 +157,6 @@ fn assert_step_outcome<T, E: std::fmt::Debug>(
161157
}
162158
}
163159

164-
fn build_signed_block(block_data: types::BlockStepData) -> SignedBlock {
165-
let block: Block = block_data.to_block();
166-
167-
// Build one empty proof per attestation, matching the aggregation_bits from
168-
// each attestation in the block body. Block processing zips attestations with
169-
// signatures, so they must be the same length for attestations to reach
170-
// fork choice.
171-
let proofs: Vec<_> = block
172-
.body
173-
.attestations
174-
.iter()
175-
.map(|att| AggregatedSignatureProof::empty(att.aggregation_bits.clone()))
176-
.collect();
177-
178-
SignedBlock {
179-
message: block,
180-
signature: BlockSignatures {
181-
proposer_signature: XmssSignature::try_from(vec![0u8; SIGNATURE_SIZE]).unwrap(),
182-
attestation_signatures: proofs.try_into().expect("attestation proofs within limit"),
183-
},
184-
}
185-
}
186-
187160
fn validate_checks(
188161
st: &Store,
189162
checks: &StoreChecks,
@@ -374,7 +347,7 @@ fn validate_checks(
374347

375348
fn validate_attestation_check(
376349
st: &Store,
377-
check: &types::AttestationCheck,
350+
check: &AttestationCheck,
378351
step_idx: usize,
379352
) -> datatest_stable::Result<()> {
380353
let validator_id = check.validator;

crates/blockchain/tests/signature_spectests.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ use ethlambda_types::{
99
state::State,
1010
};
1111

12-
mod common;
13-
mod signature_types;
14-
use signature_types::VerifySignaturesTestVector;
12+
use ethlambda_test_fixtures::verify_signatures::VerifySignaturesTestVector;
1513

1614
const SUPPORTED_FIXTURE_FORMAT: &str = "verify_signatures_test";
1715

crates/blockchain/tests/signature_types.rs

Lines changed: 0 additions & 112 deletions
This file was deleted.

crates/common/test-fixtures/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ ethlambda-types.workspace = true
1414
libssz-types.workspace = true
1515

1616
serde.workspace = true
17+
serde_json.workspace = true
1718
hex.workspace = true

0 commit comments

Comments
 (0)