Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,17 @@ default** on its two SSZ endpoints and bare integers in JSON, because
`checkpoint_sync.rs` and other clients' lean sync read those bytes and may send no
`Accept` at all; `/lean/v0/blocks/finalized` now answers JSON when asked for it by name.

With `--builder`, the beacon surface also runs the **fulu builder flow
(MEV-Boost)**. `register_validator` forwards registrations to the builder
(`beacon/registrations.rs`, client in `beacon/builder_client/`).
`produceBlockV3` asks for the builder's bid alongside the local build and
serves a blinded block when the bid passes its checks and wins on value
(`beacon/builder_flow/`: bid checks, circuit breaker, boost-factor choice).
`blinded_blocks` hands the signed block back for the builder to publish, with
no local fallback, since a second block for the slot would be an equivocation. Gloas
has no blinded blocks; its builder market is separate (the **Gloas** bullet
under Beacon Chain STF).

See [`docs/rpc.md`](docs/rpc.md) for the full reference: CLI flags and defaults, the lean API endpoints (health, finalized state/block, justified checkpoint, blocks by root/slot, fork-choice tree + D3.js UI, runtime aggregator toggle), the Beacon API endpoints and the three ids they refuse, the metrics/debug endpoints (Prometheus `/metrics`, jemalloc heap profiling), the Hive test-driver endpoints, plus request/response shapes, status codes, and content types.

## Beacon Chain types (`crates/common/types/src/beacon/`)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ what they sign.
| `--checkpoint-sync-url` | Where the anchor comes from. Required on a fresh data directory for a built-in network, since those never start from genesis. A resumable data directory is used before it; a network loaded from a directory anchors at its own `genesis.ssz` when no URL is given |
| `--node-key` | Optional, but worth persisting: the columns this node custodies are a function of its node id, so without a key file it changes identity, and custody set, on every restart |
| `--execution-endpoint`, `--execution-jwt-secret` | Optional Engine API pairing, given together. Without them, blocks import without payload validation |
| `--builder` | Optional builder API endpoint, normally MEV-Boost. With it, a fulu proposal from a validator registered through this node gets a blinded block when the builder's bid wins; without it every block is built locally |

A built-in network needs nothing else on disk. Its `eth-clients` `config.yaml`
and bootnode list, and the two genesis values the fork digest is computed from,
Expand Down
67 changes: 67 additions & 0 deletions bin/ethlambda/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,21 @@ pub(crate) struct MainnetOptions {
value_parser = clap::value_parser!(u64).range(1..),
)]
pub(crate) builder_header_timeout: u64,

/// Build locally, skipping the builder, after more than this many
/// consecutive empty slots right before the proposal (the circuit breaker).
#[arg(long, default_value_t = ethlambda_rpc::FallbackThresholds::DEFAULT.skips, requires = "builder")]
pub(crate) builder_fallback_skips: u64,

/// Build locally after more than this many empty slots in the epoch's
/// worth of slots before the proposal.
#[arg(long, default_value_t = ethlambda_rpc::FallbackThresholds::DEFAULT.skips_per_epoch, requires = "builder")]
pub(crate) builder_fallback_skips_per_epoch: u64,

/// Build locally when the proposal is more than this many epochs past the
/// finalized checkpoint.
#[arg(long, default_value_t = ethlambda_rpc::FallbackThresholds::DEFAULT.epochs_since_finalization, requires = "builder")]
pub(crate) builder_fallback_epochs_since_finalization: u64,
}

/// Flags only the lean chain takes.
Expand Down Expand Up @@ -1067,6 +1082,58 @@ mod tests {
}
}

#[test]
fn the_fallback_flags_parse_and_default_to_lighthouses() {
let beacon = parse_beacon(beacon_args());
assert_eq!(beacon.mainnet.builder_fallback_skips, 3);
assert_eq!(beacon.mainnet.builder_fallback_skips_per_epoch, 8);
assert_eq!(beacon.mainnet.builder_fallback_epochs_since_finalization, 3);

let mut args = beacon_args();
args.extend([
"--builder",
"http://127.0.0.1:18550",
"--builder-fallback-skips",
"5",
"--builder-fallback-skips-per-epoch",
"12",
"--builder-fallback-epochs-since-finalization",
"2",
]);
let beacon = parse_beacon(args);
assert_eq!(beacon.mainnet.builder_fallback_skips, 5);
assert_eq!(beacon.mainnet.builder_fallback_skips_per_epoch, 12);
assert_eq!(beacon.mainnet.builder_fallback_epochs_since_finalization, 2);
}

#[test]
fn each_fallback_flag_needs_a_builder() {
for flag in [
"--builder-fallback-skips",
"--builder-fallback-skips-per-epoch",
"--builder-fallback-epochs-since-finalization",
] {
let mut args = beacon_args();
args.extend([flag, "5"]);
try_parse_from(args).expect_err(flag);
}
}

#[test]
fn a_bad_fallback_value_is_refused() {
for flag in [
"--builder-fallback-skips",
"--builder-fallback-skips-per-epoch",
"--builder-fallback-epochs-since-finalization",
] {
for value in ["-1", "many"] {
let mut args = beacon_args();
args.extend(["--builder", "http://127.0.0.1:18550", flag, value]);
try_parse_from(args).expect_err(flag);
}
}
}

#[test]
fn a_zero_header_timeout_is_refused() {
let mut args = beacon_args();
Expand Down
9 changes: 9 additions & 0 deletions bin/ethlambda/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,14 @@ async fn run_node(options: Options) -> eyre::Result<()> {
})
.transpose()?,
};
let builder_fallback = match &network {
Network::Lean(_) => ethlambda_rpc::FallbackThresholds::DEFAULT,
Network::Mainnet { mainnet, .. } => ethlambda_rpc::FallbackThresholds {
skips: mainnet.builder_fallback_skips,
skips_per_epoch: mainnet.builder_fallback_skips_per_epoch,
epochs_since_finalization: mainnet.builder_fallback_epochs_since_finalization,
},
};
if let Some(builder) = builder.clone() {
tokio::spawn(async move {
let _ = builder
Expand Down Expand Up @@ -872,6 +880,7 @@ async fn run_node(options: Options) -> eyre::Result<()> {
client_version: version::engine_client_version(),
identity: rpc_identity,
builder,
builder_fallback,
},
local_peer_id,
rpc_shutdown,
Expand Down
27 changes: 23 additions & 4 deletions crates/blockchain/state_transition/src/beacon/stf/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ use crate::beacon::helpers::accessors::{ActiveBalanceCache, CommitteeCache, get_
use crate::beacon::helpers::misc::compute_signing_root;
use crate::beacon::lean_state_unreachable;
use crate::beacon::preset;
use crate::beacon::primitives::{HashTreeRoot as _, Slot};
use crate::beacon::primitives::{BlsSignature, HashTreeRoot as _, Root, Slot, ValidatorIndex};
use crate::beacon::{bls, config::Config, constants};

/// What the execution layer would answer for a block's payload.
Expand Down Expand Up @@ -212,12 +212,31 @@ pub fn verify_block_signature(
state: &BeaconState,
signed_block: &containers::SignedBeaconBlock,
) -> bool {
let Ok(proposer) = state.validator(signed_block.proposer_index()) else {
verify_proposer_signature(
state,
signed_block.proposer_index(),
signed_block.message_hash_tree_root(),
&signed_block.signature(),
)
}

/// Whether `signature` is `proposer_index`'s over the block whose root is
/// `block_root`, under `state`'s proposer domain.
///
/// A blinded block has the root of its full block, so one check serves
/// both.
pub fn verify_proposer_signature(
state: &BeaconState,
proposer_index: ValidatorIndex,
block_root: Root,
signature: &BlsSignature,
) -> bool {
let Ok(proposer) = state.validator(proposer_index) else {
return false;
};
let domain = get_domain(state, constants::DOMAIN_BEACON_PROPOSER, None);
let signing_root = compute_signing_root(signed_block.message_hash_tree_root(), domain);
bls::verify(&proposer.pubkey, signing_root, &signed_block.signature())
let signing_root = compute_signing_root(block_root, domain);
bls::verify(&proposer.pubkey, signing_root, signature)
}

/// Advances the state to `slot`, running epoch processing at each boundary and
Expand Down
29 changes: 29 additions & 0 deletions crates/net/engine/src/building.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ pub struct BuiltPayload {
pub blobs_bundle: BlobsBundle,
/// The EIP-7685 request list, each entry its type byte then its data.
pub execution_requests: Vec<Vec<u8>>,
/// The engine's `shouldOverrideBuilder`: it wants this payload built
/// locally whatever a builder bid pays. Absent from an answer means false.
pub should_override_builder: bool,
}

/// `engine_getPayloadV6`'s answer, decoded: [`BuiltPayload`] with Amsterdam's
Expand Down Expand Up @@ -151,6 +154,8 @@ pub(crate) struct GetPayloadV5Response {
block_value: String,
blobs_bundle: BlobsBundleJson,
#[serde(default)]
should_override_builder: bool,
#[serde(default)]
execution_requests: Vec<String>,
}

Expand Down Expand Up @@ -259,6 +264,7 @@ impl TryFrom<GetPayloadV5Response> for BuiltPayload {
block_value: parse_uint256(&response.block_value)?,
blobs_bundle,
execution_requests: decode_requests(&response.execution_requests)?,
should_override_builder: response.should_override_builder,
})
}
}
Expand Down Expand Up @@ -454,6 +460,29 @@ mod tests {
assert_eq!(built.execution_requests, vec![vec![0x00, 0xaa]]);
}

#[test]
fn a_v5_answer_carries_should_override_builder_and_defaults_it_to_false() {
let mut json = response_json();
json["shouldOverrideBuilder"] = true.into();
let response: GetPayloadV5Response = serde_json::from_value(json).unwrap();
assert!(
BuiltPayload::try_from(response)
.unwrap()
.should_override_builder
);

let mut json = response_json();
json.as_object_mut()
.unwrap()
.remove("shouldOverrideBuilder");
let response: GetPayloadV5Response = serde_json::from_value(json).unwrap();
assert!(
!BuiltPayload::try_from(response)
.unwrap()
.should_override_builder
);
}

#[test]
fn a_wrong_width_hash_is_a_decode_error() {
let mut json = response_json();
Expand Down
Loading
Loading