Repository navigation
docs: add initial roadmap - #1
Merged
Merged
Conversation
MegaRedHand
marked this pull request as ready for review
December 23, 2025 19:29
2 of 3 tasks
This was referenced Feb 2, 2026
pablodeymo
pushed a commit
that referenced
this pull request
Feb 11, 2026
See lambdaclass/ethrex#6187 for more context. Review agents make use of `#N` for numbering comments, which triggers GitHub references, linking all reviewed PRs with links to the first PRs of the repo (see #1 for an example).
MegaRedHand
added a commit
that referenced
this pull request
May 14, 2026
* lib.rs (#5): decode proposer proposal pubkey once and reuse it for the singleton Type-1 wrap and the merge inputs; was deserialized twice on the crypto hot path. * store.rs (#4): block / attestation slot u64→u32 overflow now maps to a dedicated `SlotOutOfRange(u64)` variant instead of being misreported as `ProposerSignatureVerificationFailed`. * store.rs (#1): explicitly document the security caveat of the empty- SNARK structural-only branch (proposer XMSS not crypto-verified) and note the two upstream mitigations (STF `process_block_header` rejects wrong proposer_index; per-attestation crypto still runs at gossip ingestion). The structural log line now flags it explicitly. * crypto/src/lib.rs (#2): outer Type-2 decompression failure in `split_type_2_by_message` returns a new `DeserializationFailed` variant instead of `ChildDeserializationFailed(0)`, which had implied a child at index 0 had failed. * types/src/block.rs (#3): annotate the intentional duplication of proof bytes between `TypeOneMultiSignature::info.proof` and the outer `proof` field — mirrors leanSpec PR #717's shape so a Type-1 embedded inside a Type-2's info[i] reads the same as a standalone Type-1.
4 tasks done
3 tasks done
This was referenced Oct 26, 2026
tcoratger
pushed a commit
to tcoratger/ethlambda
that referenced
this pull request
Oct 30, 2026
…daclass#1) * build(deps): track libssz from git, and add ethereum-types and sha2 The Beacon Chain containers landing in the next commit hand out `&mut` into an `SszList` (the validator registry, through `altair_validator_lists_mut`), which published libssz-types 0.3.0 cannot do: it exposes only `Deref` and `Index`. `DerefMut` and `IndexMut` landed after that release, in lambdaclass/libssz#33. A git dependency rather than a `[patch.crates-io]` override: nothing outside this workspace depends on libssz, so there are no two copies to unify, which is the job a patch table would be doing. Pointing the four dependencies at the repository says the same thing with less machinery, and avoids committing a `[patch]` table that `shadow/cargo-patch.toml` would then collide with, since that fragment is appended verbatim to this manifest and declares one itself. All four move together because libssz-types depends on libssz and libssz-merkle by path inside that repository, so a mixed set would put two incompatible copies of libssz in the graph. `Cargo.lock` pins the exact commit, so tracking a branch does not make the build drift on its own; it moves when someone runs `cargo update`. Return these to a crates.io version once a release carrying lambdaclass#33 exists. `ethereum-types` and `sha2` join the workspace dependencies for the same incoming containers. `sha2`'s `asm` feature selects the CPU's SHA-256 instructions, which merkleizing a beacon state feels directly, since a state's `hash_tree_root` is almost entirely SHA-256 compressions. * feat(types): add the Beacon Chain containers under a beacon namespace The Beacon Chain is a different protocol from the Lean consensus this repository implements, but the two need to meet at one state type: a single BlockChainServer should be able to dispatch on the state variant rather than existing twice. That means the containers cannot live in a crate that also carries the state transition, because everything reaching for a container would then also pull in blst and c-kzg. They live here instead. Seven forks, phase0 through fulu. Containers that change between forks are defined once per fork as plain structs deriving their SSZ encoding and merkleization, wrapped in an enum. Deriving is the reason for that shape: the per-fork field lists are not a growing tail. Two of phase0's fields are *replaced* in altair, one field changes type in five separate forks, and the state's merkle tree gains a level at electra, so a single container with fork-conditional serialization would have to reproduce all of that by hand. The preset is a compile-time choice, `preset-minimal` against an implicit mainnet default, because SSZ list and vector bounds are const-generic arguments and cannot be selected at run time. Fork *scheduling* is runtime configuration in `config` instead, since fork epochs move per case in the upstream test suites. `constants` holds what the spec fixes outright, `primitives` the scalar aliases and fixed-length byte strings, `error` what the containers return, and `fork` the `ForkName` ordering that later gating reads. This is a port of code verified against the consensus-specs v1.6.1 fixture suites, where it passes 5705 mainnet and 40009 minimal cases across both presets. Those suites exercise the state transition that consumes these containers and are not part of this crate, so what runs here is the containers' own round-trip and shape tests. * feat(types): give ForkName and BeaconState a Lean variant This is the point of moving the containers here. One BlockChainServer should dispatch on the state variant rather than existing once per chain, and that needs a single state type spanning both. `BeaconState::Lean` holds lean's `State`, which gains `PartialEq` because `BeaconState` derives it. `ForkName::Lean` is declared last, so the derived `Ord` puts it after every beacon fork. That is what `fork >= ForkName::X` gating reads, so such a gate reads as true for a lean state rather than accidentally selecting phase0 rules. It is deliberately absent from `ForkName::ALL`. `ALL` is what `parse`, `previous`, `next`, and the upstream fixture harness search, so its absence is what makes `parse("lean")` return `None`, keeps `Fulu.next()` at `None` so a fork upgrade cannot walk off the end into lean, and stops a fixture directory from ever resolving to a lean case. Lean is not a point on the Beacon Chain's fork timeline and must not be reachable by traversing it. Only `fork_name()` and `from_ssz()` are real arms. Every other exhaustive match on `BeaconState` answers `unreachable!()` naming itself, and the accessors on `ForkName` do the same, except those already returning `Result`, which answer `UnsupportedForFork`. That asymmetry is the design, not an oversight. The guarantee that a lean state never reaches a beacon accessor is the single `match` at the top of each handler, not the type system, so the failure mode when that guarantee is broken should be a named panic pointing at the function that was reached, rather than a silent wrong answer computed from a state of the wrong shape. * feat(types): add what the storage and networking layers need Four additions, each pulled here by a crate that must not depend on a Beacon Chain state transition. `fork_digest` computes `compute_fork_data_root` from the fork schedule. Gossip topic names and the eth2 ENR entry both need it, and both belong to the networking layer, which cannot reach for a crate carrying blst and c-kzg just to name a topic. The fulu branch is EIP-7892's: a blob-parameter-only fork perturbs the digest, so mainnet's is 8c9f62fe rather than fulu's bare 82fae541. `fork_choice` holds `LatestMessage` and `PowBlock`. Both are data a fork choice store holds rather than behavior it runs, and the storage layer cannot depend on the consensus layer because the dependency runs the other way. `ForkName` gains a stable one-byte storage selector. A stored state has to record which fork's shape it has, and the derived discriminant is not a stable encoding: it shifts whenever a variant is added, which silently reinterprets every state already on disk. An explicit byte survives that. Lean's `primitives` gains conversions between the beacon `Root` and lean's `H256`. Same 32 bytes in two types, and the boundary is crossed by every store lookup on the beacon path, so the conversion belongs here rather than open-coded at each call site. * docs(types): resolve the beacon namespace's doc links Ten rustdoc warnings, inherited from the source these containers were ported from rather than introduced here, but there is no reason to carry them onto main. Four kinds: - `shared_state_accessors` is a `macro_rules!` with no `#[macro_export]`, so rustdoc has no item to point at and the link never resolved. It becomes a plain code span, which is what it always meant. - `SYNC_COMMITTEE_SUBNET_COUNT` is a private const. altair.rs already refers to it as a plain code span thirteen lines further down, so this follows the convention the file had already settled on. - capella's three links spelled out a target identical to what the label already resolved to. - preset.rs's two sat in an outer doc comment on `pub mod retuned`, which resolves in the parent scope rather than inside the module, so they pointed at nothing. Qualifying them with `retuned::` makes them mean what they say. `cargo doc -p ethlambda-types --no-deps` is now warning-free. * docs: describe the beacon namespace in ethlambda-types Records what a reader needs before touching these types: that the Beacon Chain is a separate protocol sharing the crate only so one state type spans both, that ForkName::Lean sits outside ForkName::ALL on purpose, why the beacon accessors panic rather than error on the Lean variant, and that the preset is a compile-time feature every lean crate inherits. Also records where the verification lives, since it is not in this repo: the state transition that exercises these containers against the consensus-specs fixtures is on feat/beacon-chain-stf. * build(deps): pin the libssz crates by rev, not branch `branch = "main"` leaves a routine `cargo update` free to move the SSZ encoder and merkleizer that decides every `hash_tree_root`. The requirement is a commit carrying lambdaclass/libssz#33, not whatever `main` holds today, so pin the rev `Cargo.lock` already resolved to and make moving it an explicit edit. Also correct what sha2's `asm` feature actually buys: on x86/x86_64 the SHA-NI dispatch happens through `cpufeatures` with or without it, and `asm` only swaps that dispatch's fallback. aarch64 is where the feature is the whole hardware path. * refactor(types): dispatch the beacon fork ladders from one place Every beacon accessor spelled out its own seven-arm match plus a lean arm, so a new fork meant editing twenty ladders, and the boundary panic's wording had already drifted between them. Route the arms through `dispatch_state!`, `dispatch_state_from!` and `dispatch_block!`, and write the panic once as `lean_unreachable!`, in the two forms the two boundaries are crossed by. Also, from the same reviewing pass: - Make the fork matches in `preset::retuned` exhaustive, so a fork added later is a build error instead of falling silently into bellatrix's value. - Keep `get_blob_parameters` on `Config`, where `max_blobs_per_block` and `compute_fork_digest` both reach it; the digest hashes the pair's epoch as well as its limit, so the search cannot be written once per caller. - Convert both presets' `TERMINAL_TOTAL_DIFFICULTY` at compile time, with a test pinning the limbs against the decimal strings the config files carry. - Move the `Root` <-> `H256` conversion to the beacon side of the namespace: beacon is the half that knows about lean, so lean's `primitives` names nothing beacon. * ci: check the minimal preset The beacon containers' SSZ bounds are const-generic arguments, so the minimal preset is a whole second compilation of ethlambda-types that no other step reaches: everything else builds the default mainnet preset only. The fixture suites that select the feature live on another branch, so without this step it would rot between merges. * refactor(types): turn lean_unreachable! into two functions The macro's two arms were already two unrelated diagnostics: one for a beacon accessor handed lean's state, one for a beacon-only function handed ForkName::Lean. Nothing about either needed macro expansion, so they become lean_state_unreachable and lean_fork_unreachable. Both return `!` so they still type-check in every arm position the macro occupied: value arms, the Ok/Err arms of dispatch_state_from!, and the unit arm of Config::with_fork_epoch. #[track_caller] is what a plain function would otherwise have cost us, since without it every panic would report beacon/mod.rs instead of the arm that was actually reached. * refactor(types): replace byte_vector! with plain newtypes Four expansions did not earn a macro, and the $(#[$doc:meta])* passthrough it needed to carry each type's documentation was the least readable part of the module. Written out rather than made newtypes over a generic ByteVector<N>, which would have deduplicated Default and AsRef but turned every construction site into BlsPubkey(ByteVector([9; 48])) and every field read into .0.0. Type aliases are not an option at all: BLS_PUBKEY_SIZE and KZG_POINT_SIZE are both 48, so aliasing would make BlsPubkey and KzgCommitment the same type and give up exactly the confusion the newtypes exist to catch. Default stays hand-written because the standard library implements it for arrays only up to length 32, so a derive has nothing to call for [u8; 48] or [u8; 96]. The Debug bodies share a debug_byte_vector helper so the four cannot drift into printing the same kind of value four ways. * test(types): drop a_state_equals_its_own_clone The assertion was that a derived PartialEq agrees with a derived Clone, which holds by construction and would not fail short of a compiler bug. It was the only test in partial_eq_tests, so the module goes with it. * ci: scope the minimal-preset check to --lib, so the lint job can pass The step runs in the `lint` job, which downloads no fixtures. `cargo test -p ethlambda-types --features preset-minimal` builds every test target of the crate, including `ssz_spectests`, whose datatest-stable harness walks `leanSpec/fixtures/consensus/ssz` and panics with "error reading directory" when that tree is absent. Only the `test` job fetches it, so the step exits 101 on every run, on pull requests and on main alike, without ever reaching the thing it exists to check. `--lib` runs the 102 unit tests instead, which read nothing from disk. Those are what the preset actually changes: it selects the const-generic bounds the beacon containers are declared with, so the whole point is that the crate compiles under it and its own assertions still hold. The fixture suite is preset-independent and already runs, at the default preset, in the `test` job. * feat(types): let BeaconState encode and merkleize its lean variant `BeaconState::from_ssz(ForkName::Lean, ..)` builds `BeaconState::Lean`, while `to_ssz` and `hash_tree_root` dispatched through `dispatch_state!` and so panicked on it. Decoding a state and re-encoding it therefore panicked: the enum accepted a value on the way in that it refused on the way out. Nothing reachable today hits it, since no production caller constructs the variant and `ForkName::parse` cannot return `Lean`. What makes it worth closing anyway is who the first caller will be. The variant exists so one `BlockChainServer` can dispatch on a single state type; that server will decode a state whose fork came from configuration, where `Lean` is a legal value, and re-encoding it to store it is the next thing it does. The other `Lean` arms panic because there is genuinely no beacon answer to give: asking a lean state for its `validators` or its `slot` has no meaning, and a named panic beats a wrong number. These two are not like that. Every variant is an SSZ container, so encoding and merkleizing mean the same thing whichever chain the state belongs to, and the answer was already sitting in the variant. `dispatch_state_including_lean!` is the arm list for exactly those cases, kept separate from `dispatch_state!` so that a beacon accessor cannot reach it by accident: it takes no `$function`, because it has no arm that panics. One subtlety worth naming, since it is what makes a single macro body typecheck for all eight arms. This crate has two `HashTreeRoot` traits, both blanket implemented over `libssz_merkle::HashTreeRoot`, differing only in the wrapper type around the digest. `containers/mod.rs` imports the beacon one alone, so lean's state answers it too, with the same bytes lean's own trait would produce. The new test asserts that equality rather than merely asserting the call does not panic, because a block's `state_root` is checked against this digest. It names lean's trait through its full path: importing it would make the call ambiguous. `SignedBeaconBlock` is left as it is. Its `from_ssz` returns `Error::UnsupportedForFork` for `Lean` and it has no `Lean` variant to encode, so it has no asymmetry to fix. * refactor(types): declare the H-types instead of depending on ethereum-types The beacon namespace pulled `H160`, `H256` and `U256` from `ethereum-types`, which meant the crate carried two 32-byte hashes: that one and lean's own `primitives::H256`. Every beacon block root reaching a store lookup had to be converted between them, and the two `HashTreeRoot` convenience traits over the two hashes were otherwise identical. `beacon::primitives::Root` is now `primitives::H256` itself and `beacon::primitives::HashTreeRoot` re-exports lean's trait, so both conversions and the duplicate trait are gone. What an external crate was actually supplying beyond the hash is two newtypes, `H160` and `U256`, which this module now declares. `U256` holds the 32 little-endian bytes SSZ encodes it as, so encoding and merkleizing are the inner array's. That puts the stored byte order opposite the numeric one, so `Ord` is written out: derived, it would compare the least significant byte first and invert the `terminal_total_difficulty` comparisons that are the only place the specification orders a `uint256`. Mainnet's value fits 128 bits and is now spelled as its decimal digits rather than as limbs; both values stay pinned against the configuration files' decimal strings. `H160` writes out `libssz_merkle::HashTreeRoot` because the derive drops `is_basic_type`, and at 20 bytes wide that answer decides whether a collection of addresses packs or pads. No container holds one today, so both answers agree everywhere it is currently asked; writing it out is what keeps the first container that does hold one from silently getting the other tree. `ethereum-types` stays in `Cargo.lock` as an indirect dependency of `ethrex-common`, which `ethlambda-p2p` needs for discv5. * refactor(types): print an H256 as hex under Debug, not as 32 numbers The derived `Debug` printed the inner array, so a hash came out as `H256([171, 171, ...])`: unreadable alone, and unreadable in bulk inside the `Debug` of a container holding thousands of roots. `ethereum-types` printed hex, so with the beacon `Root` now being this type, keeping the derive would have made every spec-suite assertion failure that prints a state harder to read than before. Same output as `Display`. Nothing asserted on the old form. `ShortRoot` is still the way to truncate at a call site that wants it short.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR adds a roadmap to the readme.