diff --git a/.ai/CODE_REVIEW.md b/.ai/CODE_REVIEW.md new file mode 100644 index 0000000000..2ce60c80fd --- /dev/null +++ b/.ai/CODE_REVIEW.md @@ -0,0 +1,286 @@ +# Lighthouse Code Review Guidelines + +Code review guidelines based on patterns from Lighthouse maintainers. + +## Core Principles + +- **Correctness** over clever code +- **Clarity** through good documentation and naming +- **Safety** through proper error handling and panic avoidance +- **Maintainability** for long-term health + +## Critical: Consensus Crate (`consensus/` excluding `types/`) + +**Extra scrutiny required** - bugs here cause consensus failures. + +### Requirements + +1. **Safe Math Only** + ```rust + // NEVER + let result = a + b; + + // ALWAYS + let result = a.saturating_add(b); + // or use safe_arith crate + let result = a.safe_add(b)?; + ``` + +2. **Zero Panics** + - No `.unwrap()`, `.expect()`, array indexing `[i]` + - Return `Result` or `Option` instead + +3. **Deterministic Behavior** + - Identical results across all platforms + - No undefined behavior + +## Panic Avoidance (All Code) + +```rust +// NEVER at runtime +let value = option.unwrap(); +let item = array[1]; + +// ALWAYS +let value = option.ok_or(Error::Missing)?; +let item = array.get(1)?; + +// Only acceptable during startup for CLI/config validation +let flag = matches.get_one::("flag") + .expect("Required due to clap validation"); +``` + +## Code Clarity + +### Variable Naming +```rust +// BAD - ambiguous +let bb = ...; +let bl = ...; + +// GOOD - clear +let beacon_block = ...; +let blob = ...; +``` + +### Comments +- Explain the "why" not just the "what" +- All `TODO` comments must link to a GitHub issue +- Remove dead/commented-out code + +## Error Handling + +### Don't Silently Swallow Errors +```rust +// BAD +self.store.get_info().unwrap_or(None) + +// GOOD +self.store.get_info().unwrap_or_else(|e| { + error!(self.log, "Failed to read info"; "error" => ?e); + None +}) +``` + +### Check Return Values +Ask: "What happens if this returns `Ok(Failed)`?" Don't ignore results that might indicate failure. + +## Performance & Concurrency + +### Lock Safety +- Document lock ordering requirements +- Keep lock scopes narrow +- Seek detailed review for lock-related changes +- Use `try_read` when falling back to an alternative is acceptable +- Use blocking `read` when alternative is more expensive (e.g., state reconstruction) + +### Async Patterns +```rust +// NEVER block in async context +async fn handler() { + expensive_computation(); // blocks runtime +} + +// ALWAYS spawn blocking +async fn handler() { + tokio::task::spawn_blocking(|| expensive_computation()).await?; +} +``` + +### Rayon +- Use scoped rayon pools from beacon processor +- Avoid global thread pool (causes CPU oversubscription) + +## Review Process + +### Focus on Actionable Issues + +**Limit to 3-5 key comments.** Prioritize: +1. Correctness issues - bugs, race conditions, panics +2. Missing test coverage - especially edge cases +3. Complex logic needing documentation +4. API design concerns + +**Don't comment on:** +- Minor style issues +- Things caught by CI (formatting, linting) +- Nice-to-haves that aren't important + +### Keep Comments Natural and Minimal + +**Tone**: Natural and conversational, not robotic. + +**Good review comment:** +``` +Missing test coverage for the None blobs path. The existing test at +`store_tests.rs:2874` still provides blobs. Should add a test passing +None to verify backfill handles this correctly. +``` + +**Good follow-up after author addresses comments:** +``` +LGTM, thanks! +``` +or +``` +Thanks for the updates, looks good! +``` + +**Avoid:** +- Checklists or structured formatting (✅ Item 1 fixed...) +- Repeating what was fixed (makes it obvious it's AI-generated) +- Headers, subsections, "Summary" sections +- Verbose multi-paragraph explanations + +### Use Natural Language + +``` +BAD (prescriptive): +"This violates coding standards which strictly prohibit runtime panics." + +GOOD (conversational): +"Should we avoid `.expect()` here? This gets called in hot paths and +we typically try to avoid runtime panics outside of startup." +``` + +### Verify Before Commenting + +- If CI passes, trust it - types/imports must exist +- Check the full diff, not just visible parts +- Ask for verification rather than asserting things are missing + +## Common Review Patterns + +### Fork-Specific Changes +- Verify production fork code path unchanged +- Check SSZ compatibility (field order) +- Verify rollback/error paths handle edge cases + +### API Design +- Constructor signatures should be consistent +- Avoid `Option` parameters when value is always required + +### Concurrency +- Lock ordering documented? +- Potential deadlocks? +- Race conditions? + +### Error Handling +- Errors logged? +- Edge cases handled? +- Context provided with errors? + +## Large PR Strategy + +Large PRs (10+ files) make it easy to miss subtle bugs in individual files. + +- **Group files by subsystem** (networking, store, types, etc.) and review each group, but pay extra attention to changes that cross subsystem boundaries. +- **Review shared type/interface changes first** — changes to function signatures, return types, or struct definitions ripple through all callers. When reviewing a large PR, identify these first and trace their impact across the codebase. Downstream code may silently change behavior even if it looks untouched. +- **Flag missing test coverage for changed behavior** — if a code path's semantics change (even subtly), check that tests exercise it. If not, flag the gap. + +## Deep Review Techniques + +### Verify Against Specifications +- Read the actual spec in `./consensus-specs/` +- Compare formulas exactly +- Check constant values match spec definitions + +### Trace Data Flow End-to-End +For new config fields: +1. Config file - Does YAML contain the field? +2. Config struct - Is it parsed with serde attributes? +3. apply_to_chain_spec - Is it actually applied? +4. Runtime usage - Used correctly everywhere? + +### Check Error Handling Fallbacks +Examine every `.unwrap_or()`, `.unwrap_or_else()`: +- If the fallback triggers, does code behave correctly? +- Does it silently degrade or fail loudly? + +### Look for Incomplete Migrations +When a PR changes a pattern across the codebase: +- Search for old pattern - all occurrences updated? +- Check test files - often lag behind implementation + +## Architecture & Design + +### Avoid Dependency Bloat +- Question whether imports add unnecessary dependencies +- Consider feature flags for optional functionality +- Large imports when only primitives are needed may warrant a `core` or `primitives` feature + +### Schema Migrations +- Database schema changes require migrations +- Don't forget to add migration code when changing stored types +- Review pattern: "Needs a schema migration" + +### Backwards Compatibility +- Consider existing users when changing behavior +- Document breaking changes clearly +- Prefer additive changes when possible + +## Anti-Patterns to Avoid + +### Over-Engineering +- Don't add abstractions until needed +- Keep solutions simple and focused +- "Three similar lines of code is better than a premature abstraction" + +### Unnecessary Complexity +- Avoid feature flags for simple changes +- Don't add fallbacks for scenarios that can't happen +- Trust internal code and framework guarantees + +### Premature Optimization +- Optimize hot paths based on profiling, not assumptions +- Document performance considerations but don't over-optimize + +### Hiding Important Information +- Don't use generic variable names when specific ones are clearer +- Don't skip logging just to keep code shorter +- Don't omit error context + +## Design Principles + +### Simplicity First +Question every layer of abstraction: +- Is this `Arc` needed, or is the inner type already `Clone`? +- Is this `Mutex` needed, or can ownership be restructured? +- Is this wrapper type adding value or just indirection? + +If you can't articulate why a layer of abstraction exists, it probably shouldn't. + +### High Cohesion +Group related state and behavior together. If two fields are always set together, used together, and invalid without each other, they belong in a struct. + +## Before Approval Checklist + +- [ ] No panics: No `.unwrap()`, `.expect()`, unchecked array indexing +- [ ] Consensus safe: If touching consensus crate, all arithmetic is safe +- [ ] Errors logged: Not silently swallowed +- [ ] Clear naming: Variable names are unambiguous +- [ ] TODOs linked: All TODOs have GitHub issue links +- [ ] Tests present: Non-trivial changes have tests +- [ ] Lock safety: Lock ordering is safe and documented +- [ ] No blocking: Async code doesn't block runtime + diff --git a/.ai/DEVELOPMENT.md b/.ai/DEVELOPMENT.md new file mode 100644 index 0000000000..1204f21ead --- /dev/null +++ b/.ai/DEVELOPMENT.md @@ -0,0 +1,200 @@ +# Lighthouse Development Guide + +Development patterns, commands, and architecture for AI assistants and contributors. + +## Development Commands + +**Important**: Always branch from `unstable` and target `unstable` when creating pull requests. + +### Building + +- `make install` - Build and install Lighthouse in release mode +- `make install-lcli` - Build and install `lcli` utility +- `cargo build --release` - Standard release build +- `cargo build --bin lighthouse --features "gnosis,slasher-lmdb"` - Build with specific features + +### Testing + +- `make test` - Full test suite in release mode +- `make test-release` - Run tests using nextest (faster parallel runner) +- `cargo nextest run -p ` - Run tests for specific package (preferred for iteration) +- `cargo nextest run -p ` - Run individual test +- `FORK_NAME=electra cargo nextest run -p beacon_chain` - Run tests for specific fork +- `make test-ef` - Ethereum Foundation test vectors + +**Fork-specific testing**: `beacon_chain` and `http_api` tests support fork-specific testing via `FORK_NAME` env var when `beacon_chain/fork_from_env` feature is enabled. + +**Note**: Full test suite takes ~20 minutes. Prefer targeted tests when iterating. + +### Linting + +- `make lint` - Run Clippy with project rules +- `make lint-full` - Comprehensive linting including tests +- `cargo fmt --all && make lint-fix` - Format and fix linting issues +- `cargo sort` - Sort dependencies (enforced on CI) + +## Architecture Overview + +Lighthouse is a modular Ethereum consensus client with two main components: + +### Beacon Node (`beacon_node/`) + +- Main consensus client syncing with Ethereum network +- Beacon chain state transition logic (`beacon_node/beacon_chain/`) +- Networking, storage, P2P communication +- HTTP API for validator clients +- Entry point: `beacon_node/src/lib.rs` + +### Validator Client (`validator_client/`) + +- Manages validator keystores and duties +- Block proposals, attestations, sync committee duties +- Slashing protection and doppelganger detection +- Entry point: `validator_client/src/lib.rs` + +### Key Subsystems + +| Subsystem | Location | Purpose | +|-----------|----------|---------| +| Consensus Types | `consensus/types/` | Core data structures, SSZ encoding | +| Storage | `beacon_node/store/` | Hot/cold database (LevelDB, RocksDB, REDB backends) | +| Networking | `beacon_node/lighthouse_network/` | Libp2p, gossipsub, discovery | +| Fork Choice | `consensus/fork_choice/` | Proto-array fork choice | +| Execution Layer | `beacon_node/execution_layer/` | EL client integration | +| Slasher | `slasher/` | Optional slashing detection | + +### Utilities + +- `account_manager/` - Validator account management +- `lcli/` - Command-line debugging utilities +- `database_manager/` - Database maintenance tools + +## Code Quality Standards + +### Panic Avoidance (Critical) + +**Panics should be avoided at all costs.** + +```rust +// NEVER at runtime +let value = some_result.unwrap(); +let item = array[1]; + +// ALWAYS prefer +let value = some_result?; +let item = array.get(1)?; + +// Only acceptable during startup +let config = matches.get_one::("flag") + .expect("Required due to clap validation"); +``` + +### Consensus Crate Safety (`consensus/` excluding `types/`) + +Extra scrutiny required - bugs here cause consensus failures. + +```rust +// NEVER standard arithmetic +let result = a + b; + +// ALWAYS safe math +let result = a.saturating_add(b); +// or +use safe_arith::SafeArith; +let result = a.safe_add(b)?; +``` + +Requirements: +- Use `saturating_*` or `checked_*` operations +- Zero panics - no `.unwrap()`, `.expect()`, or `array[i]` +- Deterministic behavior across all platforms + +### Error Handling + +- Return `Result` or `Option` instead of panicking +- Log errors, don't silently swallow them +- Provide context with errors + +### Async Patterns + +```rust +// NEVER block in async context +async fn handler() { + expensive_computation(); // blocks runtime +} + +// ALWAYS spawn blocking +async fn handler() { + tokio::task::spawn_blocking(|| expensive_computation()).await?; +} +``` + +### Concurrency + +- **Lock ordering**: Document lock ordering to avoid deadlocks. See [`canonical_head.rs:9-32`](beacon_node/beacon_chain/src/canonical_head.rs) for excellent example documenting three locks and safe acquisition order. +- Keep lock scopes narrow +- Seek detailed review for lock-related changes + +### Rayon Thread Pools + +Avoid using the rayon global thread pool - it causes CPU oversubscription when beacon processor has fully allocated all CPUs to workers. Use scoped rayon pools started by beacon processor for computationally intensive tasks. + +### Tracing Spans + +- Avoid spans on simple getter methods (performance overhead) +- Be cautious of span explosion with recursive functions +- Use spans per meaningful computation step, not every function +- **Never** use `span.enter()` or `span.entered()` in async tasks + +### Documentation + +- All `TODO` comments must link to a GitHub issue +- Prefer line comments (`//`) over block comments +- Keep comments concise, explain "why" not "what" + +## Logging Levels + +| Level | Use Case | +|-------|----------| +| `crit` | Lighthouse may not function - needs immediate attention | +| `error` | Moderate impact - expect user reports | +| `warn` | Unexpected but recoverable | +| `info` | High-level status - not excessive | +| `debug` | Developer events, expected errors | + +## Testing Patterns + +- **Unit tests**: Single component edge cases +- **Integration tests**: Use [`BeaconChainHarness`](beacon_node/beacon_chain/src/test_utils.rs) for end-to-end workflows +- **Sync components**: Use [`TestRig`](beacon_node/network/src/sync/tests/mod.rs) pattern with event-based testing +- **Mocking**: `mockall` for unit tests, `mockito` for HTTP APIs +- **Adapter pattern**: For testing `BeaconChain` dependent components, create adapter structs. See [`fetch_blobs/tests.rs`](beacon_node/beacon_chain/src/fetch_blobs/tests.rs) +- **Local testnet**: See `scripts/local_testnet/README.md` + +## Build Notes + +- Full builds take 5+ minutes - use large timeouts (300s+) +- Use `cargo check` for faster iteration +- MSRV documented in `Cargo.toml` + +### Cross-compilation + +- `make build-x86_64` - Cross-compile for x86_64 Linux +- `make build-aarch64` - Cross-compile for ARM64 Linux +- `make build-riscv64` - Cross-compile for RISC-V 64-bit Linux + +## Parallel Development + +For working on multiple branches simultaneously, use git worktrees: + +```bash +git worktree add -b my-feature ../lighthouse-my-feature unstable +``` + +This creates a separate working directory without needing multiple clones. To save disk space across worktrees, configure a shared target directory: + +```bash +# In .cargo/config.toml at your workspace root +[build] +target-dir = "/path/to/shared-target" +``` diff --git a/.ai/ISSUES.md b/.ai/ISSUES.md new file mode 100644 index 0000000000..ce79198b4d --- /dev/null +++ b/.ai/ISSUES.md @@ -0,0 +1,130 @@ +# GitHub Issue & PR Guidelines + +Guidelines for creating well-structured GitHub issues and PRs for Lighthouse. + +## Issue Structure + +### Start with Description + +Always begin with `## Description`: + +```markdown +## Description + +We presently prune all knowledge of non-canonical blocks once they conflict with +finalization. The pruning is not always immediate, fork choice currently prunes +once the number of nodes reaches a threshold of 256. + +It would be nice to develop a simple system for handling messages relating to +blocks that are non-canonical. +``` + +**Guidelines:** +- First paragraph: problem and brief solution +- Provide context about current behavior +- Link to related issues, PRs, or specs +- Be technical and specific + +### Steps to Resolve (when applicable) + +```markdown +## Steps to resolve + +I see two ways to fix this: a strict approach, and a pragmatic one. + +The strict approach would only check once the slot is finalized. This would have +0 false positives, but would be slower to detect missed blocks. + +The pragmatic approach might be to only process `BeaconState`s from the canonical +chain. I don't have a strong preference between approaches. +``` + +**Guidelines:** +- Don't be overly prescriptive - present options +- Mention relevant constraints +- It's okay to say "I don't have a strong preference" + +### Optional Sections + +- `## Additional Info` - Edge cases, related issues +- `## Metrics` - Performance data, observations +- `## Version` - For bug reports + +## Code References + +**Use GitHub permalinks with commit hashes** so code renders properly: + +``` +https://github.com/sigp/lighthouse/blob/261322c3e3ee/beacon_node/beacon_processor/src/lib.rs#L809 +``` + +Get commit hash: `git rev-parse unstable` + +For line ranges: `#L809-L825` + +## Writing Style + +### Be Natural and Concise +- Direct and objective +- Precise technical terminology +- Avoid AI-sounding language + +### Be Honest About Uncertainty +- Don't guess - ask questions +- Use tentative language when appropriate ("might", "I think") +- Present multiple options without picking one + +### Think About Trade-offs +- Present multiple approaches +- Discuss pros and cons +- Consider backward compatibility +- Note performance implications + +## Labels + +**Type:** `bug`, `enhancement`, `optimization`, `code-quality`, `security`, `RFC` + +**Component:** `database`, `HTTP-API`, `fork-choice`, `beacon-processor`, etc. + +**Effort:** `good first issue`, `low-hanging-fruit`, `major-task` + +## Pull Request Guidelines + +```markdown +## Description + +[What does this PR do? Why is it needed? Be concise and technical.] + +Closes #[issue-number] + +## Additional Info + +[Breaking changes, performance impacts, migration steps, etc.] +``` + +### Commit Messages + +Format: +- First line: Brief summary (imperative mood) +- Blank line +- Additional details if needed + +``` +Add custody info API for data columns + +Implements `/lighthouse/custody/info` endpoint that returns custody group +count, custodied columns, and earliest available data column slot. +``` + +## Anti-Patterns + +- Vague descriptions without details +- No code references when describing code +- Premature solutions without understanding the problem +- Making claims without validating against codebase + +## Good Examples + +- https://github.com/sigp/lighthouse/issues/6120 +- https://github.com/sigp/lighthouse/issues/4388 +- https://github.com/sigp/lighthouse/issues/8216 diff --git a/.claude/commands/issue.md b/.claude/commands/issue.md new file mode 100644 index 0000000000..85ff46fe22 --- /dev/null +++ b/.claude/commands/issue.md @@ -0,0 +1,49 @@ +# GitHub Issue Creation Task + +You are creating a GitHub issue for the Lighthouse project. + +## Required Reading + +**Before creating an issue, read `.ai/ISSUES.md`** for issue and PR writing guidelines. + +## Structure + +1. **Description** (required) + - First paragraph: problem and brief solution + - Context about current behavior + - Links to related issues, PRs, or specs + - Technical and specific + +2. **Steps to Resolve** (when applicable) + - Present options and considerations + - Don't be overly prescriptive + - Mention relevant constraints + +3. **Code References** + - Use GitHub permalinks with commit hashes + - Get hash: `git rev-parse unstable` + +## Style + +- Natural, concise, direct +- Avoid AI-sounding language +- Be honest about uncertainty +- Present trade-offs + +## Labels to Suggest + +- **Type**: bug, enhancement, optimization, code-quality +- **Component**: database, HTTP-API, fork-choice, beacon-processor +- **Effort**: good first issue, low-hanging-fruit, major-task + +## Output + +Provide the complete issue text ready to paste into GitHub. + +## After Feedback + +If the developer refines your issue/PR text or suggests a different format: + +1. **Apply their feedback** to the current issue +2. **Offer to update docs** - Ask: "Should I update `.ai/ISSUES.md` to capture this preference?" +3. **Document patterns** the team prefers that aren't yet in the guidelines diff --git a/.claude/commands/release.md b/.claude/commands/release.md new file mode 100644 index 0000000000..1694e90cc5 --- /dev/null +++ b/.claude/commands/release.md @@ -0,0 +1,85 @@ +# Release Notes Generation Task + +You are generating release notes for a new Lighthouse version. + +## Input Required + +- **Version number** (e.g., v8.1.0) +- **Base branch** (typically `stable` for previous release) +- **Release branch** (e.g., `release-v8.1`) +- **Release name** (Rick and Morty character - check existing to avoid duplicates) + +## Step 1: Gather Changes + +```bash +# Get commits between branches +git log --oneline origin/..origin/ + +# Check existing release names +gh release list --repo sigp/lighthouse --limit 50 +``` + +## Step 2: Analyze PRs + +For each PR: +1. Extract PR numbers from commit messages +2. Check for `backwards-incompat` label: + ```bash + gh pr view --repo sigp/lighthouse --json labels --jq '[.labels[].name] | join(",")' + ``` +3. Get PR details for context + +## Step 3: Categorize + +Group into sections (skip empty): +- **Breaking Changes** - schema changes, CLI changes, API changes +- **Performance Improvements** - user-noticeable optimizations +- **Validator Client Improvements** - VC-specific changes +- **Other Notable Changes** - new features, metrics +- **CLI Changes** - new/changed flags (note if BN or VC) +- **Bug Fixes** - significant user-facing fixes only + +## Step 4: Write Release Notes + +```markdown +## + +## Summary + +Lighthouse v includes . + +This is a upgrade for . + +##
+ +- **** (#<PR>): <User-facing description> + +## Update Priority + +| User Class | Beacon Node | Validator Client | +|:------------------|:------------|:-----------------| +| Staking Users | Low/Medium/High | Low/Medium/High | +| Non-Staking Users | Low/Medium/High | --- | + +## All Changes + +- <commit title> (#<PR>) + +## Binaries + +[See pre-built binaries documentation.](https://lighthouse-book.sigmaprime.io/installation_binaries.html) +``` + +## Guidelines + +- State **user impact**, not implementation details +- Avoid jargon users won't understand +- For CLI flags, mention if BN or VC +- Check PR descriptions for context + +## Step 5: Generate Announcements + +Create drafts for: +- **Email** - Formal, include priority table +- **Discord** - Tag @everyone, shorter +- **Twitter** - Single tweet, 2-3 highlights diff --git a/.claude/commands/review.md b/.claude/commands/review.md new file mode 100644 index 0000000000..7867716c79 --- /dev/null +++ b/.claude/commands/review.md @@ -0,0 +1,57 @@ +# Code Review Task + +You are reviewing code for the Lighthouse project. + +## Required Reading + +**Before reviewing, read `.ai/CODE_REVIEW.md`** for Lighthouse-specific safety requirements and review etiquette. + +## Focus Areas + +1. **Consensus Crate Safety** (if applicable) + - Safe math operations (saturating_*, checked_*) + - Zero panics + - Deterministic behavior + +2. **General Code Safety** + - No `.unwrap()` or `.expect()` at runtime + - No array indexing without bounds checks + - Proper error handling + +3. **Code Clarity** + - Clear variable names (avoid ambiguous abbreviations) + - Well-documented complex logic + - TODOs linked to GitHub issues + +4. **Error Handling** + - Errors are logged, not silently swallowed + - Edge cases are handled + - Return values are checked + +5. **Concurrency & Performance** + - Lock ordering is safe + - No blocking in async context + - Proper use of rayon thread pools + +## Output + +- Keep to 3-5 actionable comments +- Use natural, conversational language +- Provide specific line references +- Ask questions rather than making demands + +## After Review Discussion + +If the developer corrects your feedback or you learn something new: + +1. **Acknowledge and learn** - Note what you got wrong +2. **Offer to update docs** - Ask: "Should I update `.ai/CODE_REVIEW.md` with this lesson?" +3. **Format the lesson:** + ```markdown + ### Lesson: [Title] + **Issue:** [What went wrong] + **Feedback:** [What developer said] + **Learning:** [What to do differently] + ``` + +This keeps the review guidelines improving over time. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000..ae426dd254 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "echo '\n[Reminder] Run: cargo fmt --all && make lint-fix'" + } + ] + } + ] + } +} diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000000..42a5ca79e0 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,5 @@ +#!/bin/sh +# Pre-commit hook: runs cargo fmt --check +# Install with: make install-hooks + +exec cargo fmt --check diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cdec442276..e9ec8740a0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,4 @@ /beacon_node/network/ @jxs /beacon_node/lighthouse_network/ @jxs /beacon_node/store/ @michaelsproul +/.github/forbidden-files.txt @michaelsproul diff --git a/.github/forbidden-files.txt b/.github/forbidden-files.txt new file mode 100644 index 0000000000..a08a6b4e98 --- /dev/null +++ b/.github/forbidden-files.txt @@ -0,0 +1,10 @@ +# Files that have been intentionally deleted and should not be re-added. +# This prevents accidentally reviving files during botched merges. +# Add one file path per line (relative to repo root). + +beacon_node/beacon_chain/src/otb_verification_service.rs +beacon_node/store/src/partial_beacon_state.rs +beacon_node/store/src/consensus_context.rs +beacon_node/beacon_chain/src/block_reward.rs +beacon_node/http_api/src/block_rewards.rs +common/eth2/src/lighthouse/block_rewards.rs diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 46fa15da86..d9efbfc148 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -72,6 +72,27 @@ jobs: steps: - name: Check that the pull request is not targeting the stable branch run: test ${{ github.base_ref }} != "stable" + + forbidden-files-check: + name: forbidden-files-check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check for forbidden files + run: | + if [ -f .github/forbidden-files.txt ]; then + status=0 + while IFS= read -r file || [ -n "$file" ]; do + # Skip comments and empty lines + [[ "$file" =~ ^#.*$ || -z "$file" ]] && continue + if [ -f "$file" ]; then + echo "::error::Forbidden file exists: $file" + status=1 + fi + done < .github/forbidden-files.txt + exit $status + fi + release-tests-ubuntu: name: release-tests-ubuntu needs: [check-labels] @@ -319,6 +340,8 @@ jobs: bins: cargo-audit,cargo-deny - name: Check formatting with cargo fmt run: make cargo-fmt + - name: Check dependencies for unencrypted HTTP links + run: make insecure-deps - name: Lint code for quality and style with Clippy run: make lint-full - name: Certify Cargo.lock freshness @@ -428,6 +451,7 @@ jobs: needs: [ 'check-labels', 'target-branch-check', + 'forbidden-files-check', 'release-tests-ubuntu', 'beacon-chain-tests', 'op-pool-tests', diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..4ab3ec9333 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,10 @@ +# Lighthouse AI Assistant Guide + +See [`CLAUDE.md`](CLAUDE.md) for AI assistant guidance. + +This file exists for OpenAI Codex compatibility. Codex can read files, so refer to `CLAUDE.md` for the full documentation including: + +- Quick reference commands +- Critical rules (panics, safe math, async) +- Project structure +- Pointers to detailed guides in `.ai/` diff --git a/CLAUDE.md b/CLAUDE.md index 3e9ab169f3..79ed344e35 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,332 +1,159 @@ -# CLAUDE.md +# Lighthouse AI Assistant Guide -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance for AI assistants (Claude Code, Codex, etc.) working with Lighthouse. -## Development Commands +## CRITICAL - Always Follow -**Important**: Always branch from `unstable` and target `unstable` when creating pull requests. +After completing ANY code changes: +1. **MUST** run `cargo fmt --all && make lint-fix` to format and fix linting issues +2. **MUST** run `cargo check` to verify compilation before considering task complete -### Building and Installation +Run `make install-hooks` if you have not already to install git hooks. Never skip git hooks. If cargo is not available install the toolchain. -- `make install` - Build and install the main Lighthouse binary in release mode -- `make install-lcli` - Build and install the `lcli` utility binary -- `cargo build --release` - Standard Rust release build -- `cargo build --bin lighthouse --features "gnosis,slasher-lmdb"` - Build with specific features +## Quick Reference -### Testing +```bash +# Build +make install # Build and install Lighthouse +cargo build --release # Standard release build -- `make test` - Run the full test suite in release mode (excludes EF tests, beacon_chain, slasher, network, http_api) -- `make test-release` - Run tests using nextest (faster parallel test runner) -- `make test-beacon-chain` - Run beacon chain tests for all supported forks -- `make test-slasher` - Run slasher tests with all database backend combinations -- `make test-ef` - Download and run Ethereum Foundation test vectors -- `make test-full` - Complete test suite including linting, EF tests, and execution engine tests -- `cargo nextest run -p <package_name>` - Run tests for a specific package -- `cargo nextest run -p <package_name> <test_name>` - Run individual test (preferred during development iteration) -- `FORK_NAME=electra cargo nextest run -p beacon_chain` - Run tests for specific fork +# Test (prefer targeted tests when iterating) +cargo nextest run -p <package> # Test specific package +cargo nextest run -p <package> <test> # Run individual test +make test # Full test suite (~20 min) -**Note**: Full test suite takes ~20 minutes. When iterating, prefer running individual tests. - -### Linting and Code Quality - -- `make lint` - Run Clippy linter with project-specific rules -- `make lint-full` - Run comprehensive linting including tests (recommended for thorough checking) -- `make cargo-fmt` - Check code formatting with rustfmt -- `make check-benches` - Typecheck benchmark code -- `make audit` - Run security audit on dependencies - -### Cross-compilation - -- `make build-x86_64` - Cross-compile for x86_64 Linux -- `make build-aarch64` - Cross-compile for ARM64 Linux -- `make build-riscv64` - Cross-compile for RISC-V 64-bit Linux - -## Architecture Overview - -Lighthouse is a modular Ethereum consensus client with two main components: - -### Core Components - -**Beacon Node** (`beacon_node/`) - -- Main consensus client that syncs with the Ethereum network -- Contains the beacon chain state transition logic (`beacon_node/beacon_chain/`) -- Handles networking, storage, and P2P communication -- Provides HTTP API for validator clients and external tools -- Entry point: `beacon_node/src/lib.rs` - -**Validator Client** (`validator_client/`) - -- Manages validator keystores and performs validator duties -- Connects to beacon nodes via HTTP API -- Handles block proposals, attestations, and sync committee duties -- Includes slashing protection and doppelganger detection -- Entry point: `validator_client/src/lib.rs` - -### Key Subsystems - -**Consensus Types** (`consensus/types/`) - -- Core Ethereum consensus data structures (BeaconState, BeaconBlock, etc.) -- Ethereum specification implementations for different networks (mainnet, gnosis) -- SSZ encoding/decoding and state transition primitives - -**Storage** (`beacon_node/store/`) - -- Hot/cold database architecture for efficient beacon chain storage -- Supports multiple backends (LevelDB, RocksDB, REDB) -- Handles state pruning and historical data management - -**Networking** (`beacon_node/lighthouse_network/`, `beacon_node/network/`) - -- Libp2p-based P2P networking stack -- Gossipsub for message propagation -- Discovery v5 for peer discovery -- Request/response protocols for sync - -**Fork Choice** (`consensus/fork_choice/`, `consensus/proto_array/`) - -- Implements Ethereum's fork choice algorithm (proto-array) -- Manages chain reorganizations and finality - -**Execution Layer Integration** (`beacon_node/execution_layer/`) - -- Interfaces with execution clients -- Retrieves payloads from local execution layer or external block builders -- Handles payload validation and builder integration - -**Slasher** (`slasher/`) - -- Optional slashing detection service -- Supports LMDB, MDBX, and REDB database backends -- Can be enabled with `--slasher` flag - -### Utilities - -**Account Manager** (`account_manager/`) - CLI tool for managing validator accounts and keystores -**LCLI** (`lcli/`) - Lighthouse command-line utilities for debugging and testing -**Database Manager** (`database_manager/`) - Database maintenance and migration tools - -### Build System Notes - -- Uses Cargo workspace with 90+ member crates -- Supports multiple Ethereum specifications via feature flags (`gnosis`, `spec-minimal`) -- Cross-compilation support for Linux x86_64, ARM64, and RISC-V -- Multiple build profiles: `release`, `maxperf`, `reproducible` -- Feature-based compilation for different database backends and optional components - -### Network Support - -- **Mainnet**: Default production network -- **Gnosis**: Alternative network (requires `gnosis` feature) -- **Testnets**: Holesky, Sepolia via built-in network configs -- **Custom networks**: Via `--testnet-dir` flag - -### Key Configuration - -- Default data directory: `~/.lighthouse/{network}` -- Beacon node data: `~/.lighthouse/{network}/beacon` -- Validator data: `~/.lighthouse/{network}/validators` -- Configuration primarily via CLI flags and YAML files - -## Common Review Standards - -### CI/Testing Requirements - -- All checks must pass before merge -- Test coverage expected for significant changes -- Flaky tests are actively addressed and fixed -- New features often require corresponding tests -- `beacon_chain` and `http_api` tests support fork-specific testing using `FORK_NAME` env var when `beacon_chain/fork_from_env` feature is enabled - -### Code Quality Standards - -- Clippy warnings must be fixed promptly (multiple PRs show this pattern) -- Code formatting with `cargo fmt` enforced -- Must run `cargo sort` when adding dependencies - dependency order is enforced on CI -- Performance considerations for hot paths - -### Documentation and Context - -- PRs require clear descriptions of what and why -- Breaking changes need migration documentation -- API changes require documentation updates -- When CLI is updated, run `make cli-local` to generate updated help text in lighthouse book -- Comments appreciated for complex logic - -### Security and Safety - -- Careful review of consensus-critical code paths -- Error handling patterns must be comprehensive -- Input validation for external data - -## Development Patterns and Best Practices - -### Panics and Error Handling - -- **Panics should be avoided at all costs** -- Always prefer returning a `Result` or `Option` over causing a panic (e.g., prefer `array.get(1)?` over `array[1]`) -- Avoid `expect` or `unwrap` at runtime - only acceptable during startup when validating CLI flags or configurations -- If you must make assumptions about panics, use `.expect("Helpful message")` instead of `.unwrap()` and provide detailed reasoning in nearby comments -- Use proper error handling with `Result` types and graceful error propagation - -### Rayon Usage - -- Avoid using the rayon global thread pool as it results in CPU oversubscription when the beacon processor has fully allocated all CPUs to workers -- Use scoped rayon pools started by beacon processor for computational intensive tasks - -### Locks - -- Take great care to avoid deadlocks when working with fork choice locks - seek detailed review ([reference](beacon_node/beacon_chain/src/canonical_head.rs:9)) -- Keep lock scopes as narrow as possible to avoid blocking fast-responding functions like the networking stack - -### Async Patterns - -- Avoid blocking computations in async tasks -- Spawn a blocking task instead for CPU-intensive work - -### Tracing - -- Design spans carefully and avoid overuse of spans just to add context data to events -- Avoid using spans on simple getter methods as it can result in performance overhead -- Be cautious of span explosion with recursive functions -- Use spans per meaningful step or computationally critical step -- Avoid using `span.enter()` or `span.entered()` in async tasks - -### Database - -- Maintain schema continuity on `unstable` branch -- Database migrations must be backward compatible - -### Consensus Crate - -- Use safe math methods like `saturating_xxx` or `checked_xxx` -- Critical that this crate behaves deterministically and MUST not have undefined behavior - -### Testing Patterns - -- **Use appropriate test types for the right scenarios**: - - **Unit tests** for single component edge cases and isolated logic - - **Integration tests** using [`BeaconChainHarness`](beacon_node/beacon_chain/src/test_utils.rs:668) for end-to-end workflows -- **`BeaconChainHarness` guidelines**: - - Excellent for integration testing but slower than unit tests - - Prefer unit tests instead for testing edge cases of single components - - Reserve for testing component interactions and full workflows -- **Mocking strategies**: - - Use `mockall` crate for unit test mocking - - Use `mockito` for HTTP API mocking (see [`validator_test_rig`](testing/validator_test_rig/src/mock_beacon_node.rs:20) for examples) -- **Event-based testing for sync components**: - - Use [`TestRig`](beacon_node/network/src/sync/tests/mod.rs) pattern for testing sync components - - Sync components interact with the network and beacon chain via events (their public API), making event-based testing more suitable than using internal functions and mutating internal states - - Enables testing of complex state transitions and timing-sensitive scenarios -- **Testing `BeaconChain` dependent components**: - - `BeaconChain` is difficult to create for TDD - - Create intermediate adapter structs to enable easy mocking - - See [`beacon_node/beacon_chain/src/fetch_blobs/tests.rs`](beacon_node/beacon_chain/src/fetch_blobs/tests.rs) for the adapter pattern -- **Local testnet for manual/full E2E testing**: - - Use Kurtosis-based local testnet setup for comprehensive testing - - See [`scripts/local_testnet/README.md`](scripts/local_testnet/README.md) for setup instructions - -### TODOs and Comments - -- All `TODO` statements must be accompanied by a GitHub issue link -- Prefer line (`//`) comments to block comments (`/* ... */`) -- Use doc comments (`///`) before attributes for public items -- Keep documentation concise and clear - avoid verbose explanations -- Provide examples in doc comments for public APIs when helpful - -## Logging Guidelines - -Use appropriate log levels for different scenarios: - -- **`crit`**: Critical issues with major impact to Lighthouse functionality - Lighthouse may not function correctly without resolving. Needs immediate attention. -- **`error`**: Error cases that may have moderate impact to Lighthouse functionality. Expect to receive reports from users for this level. -- **`warn`**: Unexpected code paths that don't have major impact - fully recoverable. Expect user reports if excessive warning logs occur. -- **`info`**: High-level logs indicating beacon node status and block import status. Should not be used excessively. -- **`debug`**: Events lower level than info useful for developers. Can also log errors expected during normal operation that users don't need to action. - -## Code Examples - -### Safe Math in Consensus Crate - -```rust -// ❌ Avoid - could panic -let result = a + b; - -// ✅ Preferred -let result = a.saturating_add(b); -// or -use safe_arith::SafeArith; - -let result = a.safe_add(b)?; +# Lint +make lint # Run Clippy +cargo fmt --all && make lint-fix # Format and fix ``` -### Panics and Error Handling +## Before You Start + +Read the relevant guide for your task: + +| Task | Read This First | +|------|-----------------| +| **Code review** | `.ai/CODE_REVIEW.md` | +| **Creating issues/PRs** | `.ai/ISSUES.md` | +| **Development patterns** | `.ai/DEVELOPMENT.md` | + +## Critical Rules (consensus failures or crashes) + +### 1. No Panics at Runtime ```rust -// ❌ Avoid - could panic at runtime -let value = some_result.unwrap(); +// NEVER +let value = option.unwrap(); let item = array[1]; -// ✅ Preferred - proper error handling -let value = some_result.map_err(|e| CustomError::SomeVariant(e))?; +// ALWAYS +let value = option?; let item = array.get(1)?; - -// ✅ Acceptable during startup for CLI/config validation -let config_value = matches.get_one::<String>("required-flag") - .expect("Required flag must be present due to clap validation"); - -// ✅ If you must make runtime assumptions, use expect with explanation -let item = array.get(1).expect("Array always has at least 2 elements due to validation in constructor"); -// Detailed reasoning should be provided in nearby comments ``` -### TODO Format +Only acceptable during startup for CLI/config validation. + +### 2. Consensus Crate: Safe Math Only + +In `consensus/` (excluding `types/`), use saturating or checked arithmetic: ```rust -pub fn my_function(&mut self, _something: &[u8]) -> Result<String, Error> { - // TODO: Implement proper validation here - // https://github.com/sigp/lighthouse/issues/1234 -} +// NEVER +let result = a + b; + +// ALWAYS +let result = a.saturating_add(b); ``` -### Async Task Spawning for Blocking Work +## Important Rules (bugs or performance issues) + +### 3. Never Block Async ```rust -// ❌ Avoid - blocking in async context -async fn some_handler() { - let result = expensive_computation(); // blocks async runtime -} +// NEVER +async fn handler() { expensive_computation(); } -// ✅ Preferred -async fn some_handler() { - let result = tokio::task::spawn_blocking(|| { - expensive_computation() - }).await?; +// ALWAYS +async fn handler() { + tokio::task::spawn_blocking(|| expensive_computation()).await?; } ``` -### Tracing Span Usage +### 4. Lock Ordering -```rust -// ❌ Avoid - span on simple getter -#[instrument] -fn get_head_block_root(&self) -> Hash256 { - self.head_block_root -} +Document lock ordering to avoid deadlocks. See [`canonical_head.rs:9-32`](beacon_node/beacon_chain/src/canonical_head.rs) for the pattern. -// ✅ Preferred - span on meaningful operations -#[instrument(skip(self))] -async fn process_block(&self, block: Block) -> Result<(), Error> { - // meaningful computation -} +### 5. Rayon Thread Pools + +Use scoped rayon pools from beacon processor, not global pool. Global pool causes CPU oversubscription when beacon processor has allocated all CPUs. + +## Good Practices + +### 6. TODOs Need Issues + +All `TODO` comments must link to a GitHub issue. + +### 7. Clear Variable Names + +Avoid ambiguous abbreviations (`bb`, `bl`). Use `beacon_block`, `blob`. + +## Branch & PR Guidelines + +- Branch from `unstable`, target `unstable` for PRs +- Run `cargo sort` when adding dependencies +- Run `make cli-local` when updating CLI flags + +## Project Structure + +``` +beacon_node/ # Consensus client + beacon_chain/ # State transition logic + store/ # Database (hot/cold) + network/ # P2P networking + execution_layer/ # EL integration +validator_client/ # Validator duties +consensus/ + types/ # Core data structures + fork_choice/ # Proto-array ``` -## Build and Development Notes +See `.ai/DEVELOPMENT.md` for detailed architecture. -- Full builds and tests take 5+ minutes - use large timeouts (300s+) for any `cargo build`, `cargo nextest`, or `make` commands -- Use `cargo check` for faster iteration during development and always run after code changes -- Prefer targeted package tests (`cargo nextest run -p <package>`) and individual tests over full test suite when debugging specific issues -- Use `cargo fmt --all && make lint-fix` to format code and fix linting issues once a task is complete -- Always understand the broader codebase patterns before making changes -- Minimum Supported Rust Version (MSRV) is documented in `lighthouse/Cargo.toml` - ensure Rust version meets or exceeds this requirement +## Maintaining These Docs + +**These AI docs should evolve based on real interactions.** + +### After Code Reviews + +If a developer corrects your review feedback or points out something you missed: +- Ask: "Should I update `.ai/CODE_REVIEW.md` with this lesson?" +- Add to the "Common Review Patterns" or create a new "Lessons Learned" entry +- Include: what went wrong, what the feedback was, what to do differently + +### After PR/Issue Creation + +If a developer refines your PR description or issue format: +- Ask: "Should I update `.ai/ISSUES.md` to capture this?" +- Document the preferred style or format + +### After Development Work + +If you learn something about the codebase architecture or patterns: +- Ask: "Should I update `.ai/DEVELOPMENT.md` with this?" +- Add to relevant section or create new patterns + +### Format for Lessons + +```markdown +### Lesson: [Brief Title] + +**Context:** [What task were you doing?] +**Issue:** [What went wrong or was corrected?] +**Learning:** [What to do differently next time] +``` + +### When NOT to Update + +- Minor preference differences (not worth documenting) +- One-off edge cases unlikely to recur +- Already covered by existing documentation diff --git a/Cargo.lock b/Cargo.lock index 69204ccaec..fac8f42232 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "account_manager" -version = "8.1.0" +version = "8.1.1" dependencies = [ "account_utils", "bls", @@ -1285,7 +1285,7 @@ dependencies = [ [[package]] name = "beacon_node" -version = "8.1.0" +version = "8.1.1" dependencies = [ "account_utils", "beacon_chain", @@ -1548,7 +1548,7 @@ dependencies = [ [[package]] name = "boot_node" -version = "8.1.0" +version = "8.1.1" dependencies = [ "beacon_node", "bytes", @@ -3120,13 +3120,16 @@ dependencies = [ "context_deserialize", "educe", "eip_3076", + "enr", "eth2_keystore", "ethereum_serde_utils", "ethereum_ssz", "ethereum_ssz_derive", "futures", "futures-util", + "libp2p-identity", "mediatype", + "multiaddr", "pretty_reqwest_error", "proto_array", "rand 0.9.2", @@ -3196,7 +3199,7 @@ dependencies = [ "sha2", "tempfile", "unicode-normalization", - "uuid 0.8.2", + "uuid", "zeroize", ] @@ -3236,7 +3239,7 @@ dependencies = [ "serde_repr", "tempfile", "tiny-bip39", - "uuid 0.8.2", + "uuid", ] [[package]] @@ -3424,9 +3427,9 @@ dependencies = [ [[package]] name = "fallible-iterator" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" [[package]] name = "fallible-streaming-iterator" @@ -3930,7 +3933,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", - "allocator-api2", ] [[package]] @@ -3955,15 +3957,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" -dependencies = [ - "hashbrown 0.14.5", -] - [[package]] name = "hashlink" version = "0.9.1" @@ -3982,6 +3975,15 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "hashlink" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "hdrhistogram" version = "7.5.4" @@ -4214,6 +4216,7 @@ dependencies = [ "parking_lot", "proto_array", "rand 0.9.2", + "reqwest", "safe_arith", "sensitive_url", "serde", @@ -4799,9 +4802,9 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ "cpufeatures", ] @@ -4864,7 +4867,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lcli" -version = "8.1.0" +version = "8.1.1" dependencies = [ "account_utils", "beacon_chain", @@ -5319,9 +5322,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.25.2" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa" +checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a" dependencies = [ "cc", "pkg-config", @@ -5350,7 +5353,7 @@ dependencies = [ [[package]] name = "lighthouse" -version = "8.1.0" +version = "8.1.1" dependencies = [ "account_manager", "account_utils", @@ -5482,7 +5485,7 @@ dependencies = [ [[package]] name = "lighthouse_version" -version = "8.1.0" +version = "8.1.1" dependencies = [ "regex", ] @@ -5898,7 +5901,7 @@ dependencies = [ "rustc_version 0.4.1", "smallvec", "tagptr", - "uuid 1.19.0", + "uuid", ] [[package]] @@ -6078,6 +6081,7 @@ dependencies = [ "metrics", "operation_pool", "parking_lot", + "paste", "rand 0.8.5", "rand 0.9.2", "rand_chacha 0.3.1", @@ -6155,6 +6159,7 @@ dependencies = [ "environment", "eth2", "execution_layer", + "reqwest", "sensitive_url", "tempfile", "tokio", @@ -7157,12 +7162,13 @@ dependencies = [ [[package]] name = "r2d2_sqlite" -version = "0.21.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4f5d0337e99cd5cacd91ffc326c6cc9d8078def459df560c4f9bf9ba4a51034" +checksum = "a2ebd03c29250cdf191da93a35118b4567c2ef0eacab54f65e058d6f4c9965f6" dependencies = [ "r2d2", "rusqlite", + "uuid", ] [[package]] @@ -7497,6 +7503,16 @@ dependencies = [ "archery", ] +[[package]] +name = "rsqlite-vfs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.17", +] + [[package]] name = "rtnetlink" version = "0.13.1" @@ -7552,16 +7568,17 @@ checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" [[package]] name = "rusqlite" -version = "0.28.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a" +checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.10.0", "fallible-iterator", "fallible-streaming-iterator", - "hashlink 0.8.4", + "hashlink 0.11.0", "libsqlite3-sys", "smallvec", + "sqlite-wasm-rs", ] [[package]] @@ -8368,6 +8385,18 @@ dependencies = [ "der", ] +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + [[package]] name = "ssz_types" version = "0.14.0" @@ -9490,16 +9519,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "uuid" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" -dependencies = [ - "getrandom 0.2.16", - "serde", -] - [[package]] name = "uuid" version = "1.19.0" @@ -9508,12 +9527,14 @@ checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.4", "js-sys", + "rand 0.9.2", + "serde_core", "wasm-bindgen", ] [[package]] name = "validator_client" -version = "8.1.0" +version = "8.1.1" dependencies = [ "account_utils", "beacon_node_fallback", @@ -9684,6 +9705,7 @@ dependencies = [ "graffiti_file", "logging", "parking_lot", + "reqwest", "safe_arith", "slot_clock", "task_executor", @@ -9712,6 +9734,7 @@ dependencies = [ "eth2", "mockito", "regex", + "reqwest", "sensitive_url", "serde_json", "tracing", @@ -9806,6 +9829,7 @@ dependencies = [ "bytes", "eth2", "headers", + "reqwest", "safe_arith", "serde", "serde_array_query", @@ -10470,13 +10494,13 @@ dependencies = [ [[package]] name = "yaml-rust2" -version = "0.8.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" +checksum = "631a50d867fafb7093e709d75aaee9e0e0d5deb934021fcea25ac2fe09edc51e" dependencies = [ "arraydeque", "encoding_rs", - "hashlink 0.8.4", + "hashlink 0.11.0", ] [[package]] @@ -10497,8 +10521,7 @@ dependencies = [ [[package]] name = "yamux" version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deab71f2e20691b4728b349c6cee8fc7223880fa67b6b4f92225ec32225447e5" +source = "git+https://github.com/sigp/rust-yamux?rev=575b17c0f44f4253079a6bafaa2de74ca1d6dfaa#575b17c0f44f4253079a6bafaa2de74ca1d6dfaa" dependencies = [ "futures", "log", diff --git a/Cargo.toml b/Cargo.toml index 100a916c50..222392bcb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,7 @@ resolver = "2" [workspace.package] edition = "2024" -version = "8.1.0" +version = "8.1.1" [workspace.dependencies] account_utils = { path = "common/account_utils" } @@ -166,20 +166,7 @@ initialized_validators = { path = "validator_client/initialized_validators" } int_to_bytes = { path = "consensus/int_to_bytes" } itertools = "0.14" kzg = { path = "crypto/kzg" } -libp2p = { git = "https://github.com/libp2p/rust-libp2p.git", default-features = false, features = [ - "identify", - "yamux", - "noise", - "dns", - "tcp", - "tokio", - "secp256k1", - "macros", - "metrics", - "quic", - "upnp", - "gossipsub", -] } +libp2p = { git = "https://github.com/libp2p/rust-libp2p.git", default-features = false, features = ["identify", "yamux", "noise", "dns", "tcp", "tokio", "secp256k1", "macros", "metrics", "quic", "upnp", "gossipsub"] } libsecp256k1 = "0.7" lighthouse_network = { path = "beacon_node/lighthouse_network" } lighthouse_validator_store = { path = "validator_client/lighthouse_validator_store" } @@ -219,15 +206,10 @@ r2d2 = "0.8" rand = "0.9.0" rayon = "1.7" regex = "1" -reqwest = { version = "0.12", default-features = false, features = [ - "blocking", - "json", - "stream", - "rustls-tls", -] } +reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "stream", "rustls-tls"] } ring = "0.17" rpds = "0.11" -rusqlite = { version = "0.28", features = ["bundled"] } +rusqlite = { version = "0.38", features = ["bundled"] } rust_eth_kzg = "0.9" safe_arith = "0.1" sensitive_url = { version = "0.1", features = ["serde"] } @@ -240,7 +222,7 @@ signing_method = { path = "validator_client/signing_method" } slasher = { path = "slasher", default-features = false } slashing_protection = { path = "validator_client/slashing_protection" } slot_clock = { path = "common/slot_clock" } -smallvec = { version = "1.11.2", features = ["arbitrary"] } +smallvec = "1" snap = "1" ssz_types = { version = "0.14.0", features = ["context_deserialize", "runtime_types"] } state_processing = { path = "consensus/state_processing" } @@ -253,12 +235,7 @@ sysinfo = "0.26" system_health = { path = "common/system_health" } task_executor = { path = "common/task_executor" } tempfile = "3" -tokio = { version = "1", features = [ - "rt-multi-thread", - "sync", - "signal", - "macros", -] } +tokio = { version = "1", features = ["rt-multi-thread", "sync", "signal", "macros"] } tokio-stream = { version = "0.1", features = ["sync"] } tokio-util = { version = "0.7", features = ["codec", "compat", "time"] } tracing = "0.1.40" @@ -271,9 +248,9 @@ tracing_samplers = { path = "common/tracing_samplers" } tree_hash = "0.12.0" tree_hash_derive = "0.12.0" typenum = "1" -types = { path = "consensus/types" } +types = { path = "consensus/types", features = ["saturating-arith"] } url = "2" -uuid = { version = "0.8", features = ["serde", "v4"] } +uuid = { version = "1", features = ["serde", "v4"] } validator_client = { path = "validator_client" } validator_dir = { path = "common/validator_dir" } validator_http_api = { path = "validator_client/http_api" } @@ -302,3 +279,4 @@ debug = true [patch.crates-io] quick-protobuf = { git = "https://github.com/sigp/quick-protobuf.git", rev = "681f413312404ab6e51f0b46f39b0075c6f4ebfd" } +yamux = { git = "https://github.com/sigp/rust-yamux", rev = "575b17c0f44f4253079a6bafaa2de74ca1d6dfaa" } diff --git a/Makefile b/Makefile index 9d08c3ebe1..ad1bbbb8e8 100644 --- a/Makefile +++ b/Makefile @@ -36,8 +36,12 @@ PROFILE ?= release RECENT_FORKS_BEFORE_GLOAS=electra fulu # List of all recent hard forks. This list is used to set env variables for http_api tests +# Include phase0 to test the code paths in sync that are pre blobs RECENT_FORKS=electra fulu gloas +# For network tests include phase0 to cover genesis syncing (blocks without blobs or columns) +TEST_NETWORK_FORKS=phase0 $(RECENT_FORKS_BEFORE_GLOAS) + # Extra flags for Cargo CARGO_INSTALL_EXTRA_FLAGS?= @@ -226,12 +230,15 @@ test-op-pool-%: # Run the tests in the `network` crate for all known forks. # TODO(EIP-7732) Extend to support gloas by using RECENT_FORKS instead -test-network: $(patsubst %,test-network-%,$(RECENT_FORKS_BEFORE_GLOAS)) +test-network: $(patsubst %,test-network-%,$(TEST_NETWORK_FORKS)) test-network-%: - env FORK_NAME=$* cargo nextest run --release \ - --features "fork_from_env,$(TEST_FEATURES)" \ + env FORK_NAME=$* cargo nextest run --no-fail-fast --release \ + --features "fork_from_env,fake_crypto,$(TEST_FEATURES)" \ -p network + env FORK_NAME=$* cargo nextest run --no-fail-fast --release \ + --features "fork_from_env,$(TEST_FEATURES)" \ + -p network crypto_on # Run the tests in the `slasher` crate for all supported database backends. test-slasher: @@ -314,8 +321,8 @@ make-ef-tests-nightly: # Verifies that crates compile with fuzzing features enabled arbitrary-fuzz: - cargo check -p state_processing --features arbitrary-fuzz,$(TEST_FEATURES) - cargo check -p slashing_protection --features arbitrary-fuzz,$(TEST_FEATURES) + cargo check -p state_processing --features arbitrary,$(TEST_FEATURES) + cargo check -p slashing_protection --features arbitrary,$(TEST_FEATURES) # Runs cargo audit (Audit Cargo.lock files for crates with security vulnerabilities reported to the RustSec Advisory Database) audit: install-audit audit-CI @@ -343,8 +350,20 @@ vendor: udeps: cargo +$(PINNED_NIGHTLY) udeps --tests --all-targets --release --features "$(TEST_FEATURES)" +# Checks Cargo.toml files for unencrypted HTTP links +insecure-deps: + @ BAD_LINKS=$$(find . -name Cargo.toml | xargs grep -n "http://" || true); \ + if [ -z "$$BAD_LINKS" ]; then echo "No insecure HTTP links found"; \ + else echo "$$BAD_LINKS"; echo "Using plain HTTP in Cargo.toml files is forbidden"; exit 1; fi + # Performs a `cargo` clean and cleans the `ef_tests` directory. clean: cargo clean make -C $(EF_TESTS) clean make -C $(STATE_TRANSITION_VECTORS) clean + +# Installs git hooks from .githooks/ directory +install-hooks: + @ln -sf ../../.githooks/pre-commit .git/hooks/pre-commit + @chmod +x .githooks/pre-commit + @echo "Git hooks installed. Pre-commit hook runs 'cargo fmt --check'." diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index 8dd50cbc6e..05e6f12554 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -1,10 +1,7 @@ [package] name = "account_manager" version = { workspace = true } -authors = [ - "Paul Hauner <paul@paulhauner.com>", - "Luke Anderson <luke@sigmaprime.io>", -] +authors = ["Paul Hauner <paul@paulhauner.com>", "Luke Anderson <luke@sigmaprime.io>"] edition = { workspace = true } [dependencies] diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index 5352814dd5..ebefa6a451 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -1,10 +1,7 @@ [package] name = "beacon_node" version = { workspace = true } -authors = [ - "Paul Hauner <paul@paulhauner.com>", - "Age Manning <Age@AgeManning.com", -] +authors = ["Paul Hauner <paul@paulhauner.com>", "Age Manning <Age@AgeManning.com"] edition = { workspace = true } [lib] @@ -12,10 +9,10 @@ name = "beacon_node" path = "src/lib.rs" [features] -write_ssz_files = [ - "beacon_chain/write_ssz_files", -] # Writes debugging .ssz files to /tmp during block processing. -testing = [] # Enables testing-only CLI flags +# Writes debugging .ssz files to /tmp during block processing. +write_ssz_files = ["beacon_chain/write_ssz_files"] +# Enables testing-only CLI flags. +testing = [] [dependencies] account_utils = { workspace = true } diff --git a/beacon_node/beacon_chain/Cargo.toml b/beacon_node/beacon_chain/Cargo.toml index 5e1c41b830..a06db8934b 100644 --- a/beacon_node/beacon_chain/Cargo.toml +++ b/beacon_node/beacon_chain/Cargo.toml @@ -8,9 +8,12 @@ autotests = false # using a single test binary compiles faster [features] default = ["participation_metrics"] -write_ssz_files = [] # Writes debugging .ssz files to /tmp during block processing. -participation_metrics = [] # Exposes validator participation metrics to Prometheus. -fork_from_env = [] # Initialise the harness chain spec from the FORK_NAME env variable +# Writes debugging .ssz files to /tmp during block processing. +write_ssz_files = [] +# Exposes validator participation metrics to Prometheus. +participation_metrics = [] +# Initialise the harness chain spec from the FORK_NAME env variable +fork_from_env = [] portable = ["bls/supranational-portable"] test_backfill = [] @@ -19,7 +22,7 @@ alloy-primitives = { workspace = true } bitvec = { workspace = true } bls = { workspace = true } educe = { workspace = true } -eth2 = { workspace = true, features = ["lighthouse"] } +eth2 = { workspace = true, features = ["lighthouse", "network"] } eth2_network_config = { workspace = true } ethereum_hashing = { workspace = true } ethereum_serde_utils = { workspace = true } diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index faa396966f..667bafe445 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -61,8 +61,9 @@ use tracing::{debug, error}; use tree_hash::TreeHash; use types::{ Attestation, AttestationData, AttestationRef, BeaconCommittee, - BeaconStateError::NoCommitteeFound, ChainSpec, CommitteeIndex, Epoch, EthSpec, Hash256, - IndexedAttestation, SelectionProof, SignedAggregateAndProof, SingleAttestation, Slot, SubnetId, + BeaconStateError::NoCommitteeFound, ChainSpec, CommitteeIndex, Epoch, EthSpec, ForkName, + Hash256, IndexedAttestation, SelectionProof, SignedAggregateAndProof, SingleAttestation, Slot, + SubnetId, }; pub use batch::{batch_verify_aggregated_attestations, batch_verify_unaggregated_attestations}; @@ -160,6 +161,12 @@ pub enum Error { /// /// The peer has sent an invalid message. CommitteeIndexNonZero(usize), + /// The validator index is set to an invalid value after Gloas. + /// + /// ## Peer scoring + /// + /// The peer has sent an invalid message. + CommitteeIndexInvalid, /// The `attestation.data.beacon_block_root` block is unknown. /// /// ## Peer scoring @@ -550,8 +557,12 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { } .tree_hash_root(); + let fork_name = chain + .spec + .fork_name_at_slot::<T::EthSpec>(attestation.data().slot); + // [New in Electra:EIP7549] - verify_committee_index(attestation)?; + verify_committee_index(attestation, fork_name)?; if chain .observed_attestations @@ -595,6 +606,17 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { // attestation and do not delay consideration for later. let head_block = verify_head_block_is_known(chain, attestation.data(), None)?; + // [New in Gloas]: If the attested block is from the same slot as the attestation, + // index must be 0. + if fork_name.gloas_enabled() + && head_block.slot == attestation.data().slot + && attestation.data().index != 0 + { + return Err(Error::CommitteeIndexNonZero( + attestation.data().index as usize, + )); + } + // Check the attestation target root is consistent with the head root. // // This check is not in the specification, however we guard against it since it opens us up @@ -871,7 +893,12 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { let fork_name = chain .spec .fork_name_at_slot::<T::EthSpec>(attestation.data.slot); - if fork_name.electra_enabled() { + if fork_name.gloas_enabled() { + // [New in Gloas] + if attestation.data.index >= 2 { + return Err(Error::CommitteeIndexInvalid); + } + } else if fork_name.electra_enabled() { // [New in Electra:EIP7549] if attestation.data.index != 0 { return Err(Error::CommitteeIndexNonZero( @@ -890,6 +917,17 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { chain.config.import_max_skip_slots, )?; + // [New in Gloas]: If the attested block is from the same slot as the attestation, + // index must be 0. + if fork_name.gloas_enabled() + && head_block.slot == attestation.data.slot + && attestation.data.index != 0 + { + return Err(Error::CommitteeIndexNonZero( + attestation.data.index as usize, + )); + } + // Check the attestation target root is consistent with the head root. verify_attestation_target_root::<T::EthSpec>(&head_block, &attestation.data)?; @@ -1404,7 +1442,10 @@ pub fn verify_signed_aggregate_signatures<T: BeaconChainTypes>( /// Verify that the `attestation` committee index is properly set for the attestation's fork. /// This function will only apply verification post-Electra. -pub fn verify_committee_index<E: EthSpec>(attestation: AttestationRef<E>) -> Result<(), Error> { +pub fn verify_committee_index<E: EthSpec>( + attestation: AttestationRef<E>, + fork_name: ForkName, +) -> Result<(), Error> { if let Ok(committee_bits) = attestation.committee_bits() { // Check to ensure that the attestation is for a single committee. let num_committee_bits = get_committee_indices::<E>(committee_bits); @@ -1414,11 +1455,18 @@ pub fn verify_committee_index<E: EthSpec>(attestation: AttestationRef<E>) -> Res )); } - // Ensure the attestation index is set to zero post Electra. - if attestation.data().index != 0 { - return Err(Error::CommitteeIndexNonZero( - attestation.data().index as usize, - )); + // Ensure the attestation index is valid for the fork. + let index = attestation.data().index; + if fork_name.gloas_enabled() { + // [New in Gloas]: index must be < 2. + if index >= 2 { + return Err(Error::CommitteeIndexInvalid); + } + } else { + // [New in Electra:EIP7549]: index must be 0. + if index != 0 { + return Err(Error::CommitteeIndexNonZero(index as usize)); + } } } Ok(()) diff --git a/beacon_node/beacon_chain/src/beacon_block_streamer.rs b/beacon_node/beacon_chain/src/beacon_block_streamer.rs index edbdd6d4d9..9ddc50a9f7 100644 --- a/beacon_node/beacon_chain/src/beacon_block_streamer.rs +++ b/beacon_node/beacon_chain/src/beacon_block_streamer.rs @@ -686,7 +686,6 @@ mod tests { use crate::beacon_block_streamer::{BeaconBlockStreamer, CheckCaches}; use crate::test_utils::{BeaconChainHarness, EphemeralHarnessType, test_spec}; use bls::Keypair; - use execution_layer::test_utils::Block; use fixed_bytes::FixedBytesExtended; use std::sync::Arc; use std::sync::LazyLock; @@ -720,7 +719,7 @@ mod tests { async fn check_all_blocks_from_altair_to_fulu() { let slots_per_epoch = MinimalEthSpec::slots_per_epoch() as usize; let num_epochs = 12; - let bellatrix_fork_epoch = 2usize; + let bellatrix_fork_epoch = 0usize; let capella_fork_epoch = 4usize; let deneb_fork_epoch = 6usize; let electra_fork_epoch = 8usize; @@ -737,32 +736,8 @@ mod tests { let spec = Arc::new(spec); let harness = get_harness(VALIDATOR_COUNT, spec.clone()); - // go to bellatrix fork - harness - .extend_slots(bellatrix_fork_epoch * slots_per_epoch) - .await; - // extend half an epoch - harness.extend_slots(slots_per_epoch / 2).await; - // trigger merge - harness - .execution_block_generator() - .move_to_terminal_block() - .expect("should move to terminal block"); - let timestamp = - harness.get_timestamp_at_slot() + harness.spec.get_slot_duration().as_secs(); - harness - .execution_block_generator() - .modify_last_block(|block| { - if let Block::PoW(terminal_block) = block { - terminal_block.timestamp = timestamp; - } - }); - // finish out merge epoch - harness.extend_slots(slots_per_epoch / 2).await; // finish rest of epochs - harness - .extend_slots((num_epochs - 1 - bellatrix_fork_epoch) * slots_per_epoch) - .await; + harness.extend_slots(num_epochs * slots_per_epoch).await; let head = harness.chain.head_snapshot(); let state = &head.beacon_state; diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index b41db18ccf..8ca490c523 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -4,12 +4,9 @@ use crate::attestation_verification::{ batch_verify_unaggregated_attestations, }; use crate::beacon_block_streamer::{BeaconBlockStreamer, CheckCaches}; -use crate::beacon_proposer_cache::{ - BeaconProposerCache, EpochBlockProposers, ensure_state_can_determine_proposers_for_epoch, -}; +use crate::beacon_proposer_cache::{BeaconProposerCache, EpochBlockProposers}; use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use crate::block_times_cache::BlockTimesCache; -use crate::block_verification::POS_PANDA_BANNER; use crate::block_verification::{ BlockError, ExecutionPendingBlock, GossipVerifiedBlock, IntoExecutionPendingBlock, check_block_is_finalized_checkpoint_or_descendant, check_block_relevancy, @@ -31,11 +28,12 @@ use crate::data_availability_router::{ }; use crate::data_column_verification::{GossipDataColumnError, GossipVerifiedDataColumn}; use crate::early_attester_cache::EarlyAttesterCache; +use crate::envelope_times_cache::EnvelopeTimesCache; use crate::errors::{BeaconChainError as Error, BlockProductionError}; use crate::events::ServerSentEventHandler; use crate::execution_payload::{NotifyExecutionLayer, PreparePayloadHandle, get_execution_payload}; use crate::fetch_blobs::EngineGetBlobsOutput; -use crate::fork_choice_signal::{ForkChoiceSignalRx, ForkChoiceSignalTx, ForkChoiceWaitResult}; +use crate::fork_choice_signal::{ForkChoiceSignalRx, ForkChoiceSignalTx}; use crate::graffiti_calculator::{GraffitiCalculator, GraffitiSettings}; use crate::kzg_utils::reconstruct_blobs; use crate::light_client_finality_update_verification::{ @@ -60,6 +58,7 @@ use crate::observed_block_producers::ObservedBlockProducers; use crate::observed_data_sidecars::ObservedDataSidecars; use crate::observed_operations::{ObservationOutcome, ObservedOperations}; use crate::observed_slashable::ObservedSlashable; +use crate::pending_payload_envelopes::PendingPayloadEnvelopes; use crate::persisted_beacon_chain::PersistedBeaconChain; use crate::persisted_custody::persist_custody_context; use crate::persisted_fork_choice::PersistedForkChoice; @@ -70,7 +69,6 @@ use crate::sync_committee_verification::{ }; use crate::validator_monitor::{ HISTORIC_EPOCHS as VALIDATOR_MONITOR_HISTORIC_EPOCHS, ValidatorMonitor, get_slot_delay_ms, - timestamp_now, }; use crate::validator_pubkey_cache::ValidatorPubkeyCache; use crate::{ @@ -239,7 +237,7 @@ pub struct PrePayloadAttributes { /// /// The parent block number is not part of the payload attributes sent to the EL, but *is* /// sent to builders via SSE. - pub parent_block_number: u64, + pub parent_block_number: Option<u64>, /// The block root of the block being built upon (same block as fcU `headBlockHash`). pub parent_beacon_block_root: Hash256, } @@ -423,6 +421,9 @@ pub struct BeaconChain<T: BeaconChainTypes> { RwLock<ObservedDataSidecars<DataColumnSidecar<T::EthSpec>, T::EthSpec>>, /// Maintains a record of slashable message seen over the gossip network or RPC. pub observed_slashable: RwLock<ObservedSlashable<T::EthSpec>>, + /// Cache of pending execution payload envelopes for local block building. + /// Envelopes are stored here during block production and eventually published. + pub pending_payload_envelopes: RwLock<PendingPayloadEnvelopes<T::EthSpec>>, /// Maintains a record of which validators have submitted voluntary exits. pub observed_voluntary_exits: Mutex<ObservedOperations<SignedVoluntaryExit, T::EthSpec>>, /// Maintains a record of which validators we've seen proposer slashings for. @@ -463,6 +464,8 @@ pub struct BeaconChain<T: BeaconChainTypes> { pub early_attester_cache: EarlyAttesterCache<T::EthSpec>, /// A cache used to keep track of various block timings. pub block_times_cache: Arc<RwLock<BlockTimesCache>>, + /// A cache used to keep track of various envelope timings. + pub envelope_times_cache: Arc<RwLock<EnvelopeTimesCache>>, /// A cache used to track pre-finalization block roots for quick rejection. pub pre_finalization_block_cache: PreFinalizationBlockCache, /// A cache used to produce light_client server messages @@ -663,7 +666,18 @@ impl<T: BeaconChainTypes> BeaconChain<T> { .custody_context() .as_ref() .into(); - debug!(?custody_context, "Persisting custody context to store"); + + // Pattern match to avoid accidentally missing fields and to ignore deprecated fields. + let CustodyContextSsz { + validator_custody_at_head, + epoch_validator_custody_requirements, + persisted_is_supernode: _, + } = &custody_context; + debug!( + validator_custody_at_head, + ?epoch_validator_custody_requirements, + "Persisting custody context to store" + ); persist_custody_context::<T::EthSpec, T::HotStore, T::ColdStore>( self.store.clone(), @@ -1670,7 +1684,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { let validator_index = *validator_index as usize; committee_cache.get_attestation_duties(validator_index) }) - .collect(); + .collect::<Result<Vec<_>, _>>()?; Ok((duties, dependent_root)) }, @@ -3399,11 +3413,19 @@ impl<T: BeaconChainTypes> BeaconChain<T> { ); } - self.data_availability_checker.put_pre_execution_block( - block_root, - unverified_block.block_cloned(), - block_source, - )?; + // Gloas blocks dont need to be inserted into the DA cache + // they are always available. + if !unverified_block + .block() + .fork_name_unchecked() + .gloas_enabled() + { + self.data_availability_checker.put_pre_execution_block( + block_root, + unverified_block.block_cloned(), + block_source, + )?; + } // Start the Prometheus timer. let _full_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_TIMES); @@ -3526,28 +3548,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> { .map_err(BeaconChainError::TokioJoin)? .ok_or(BeaconChainError::RuntimeShutdown)??; - // Log the PoS pandas if a merge transition just occurred. - if payload_verification_outcome.is_valid_merge_transition_block { - info!("{}", POS_PANDA_BANNER); - info!(slot = %block.slot(), "Proof of Stake Activated"); - info!( - terminal_pow_block_hash = ?block - .message() - .execution_payload()? - .parent_hash() - .into_root(), - ); - info!( - merge_transition_block_root = ?block.message().tree_hash_root(), - ); - info!( - merge_transition_execution_hash = ?block - .message() - .execution_payload()? - .block_hash() - .into_root(), - ); - } Ok(ExecutedBlock::new( block, import_data, @@ -4102,23 +4102,10 @@ impl<T: BeaconChainTypes> BeaconChain<T> { // See https://github.com/sigp/lighthouse/issues/2028 let (_, signed_block, block_data) = signed_block.deconstruct(); - match self.get_blobs_or_columns_store_op(block_root, signed_block.slot(), block_data) { - Ok(Some(blobs_or_columns_store_op)) => { - ops.push(blobs_or_columns_store_op); - } - Ok(None) => {} - Err(e) => { - error!( - msg = "Restoring fork choice from disk", - error = &e, - ?block_root, - "Failed to store data columns into the database" - ); - return Err(self - .handle_import_block_db_write_error(fork_choice) - .err() - .unwrap_or(BlockError::InternalError(e))); - } + if let Some(blobs_or_columns_store_op) = + self.get_blobs_or_columns_store_op(block_root, signed_block.slot(), block_data) + { + ops.push(blobs_or_columns_store_op); } let block = signed_block.message(); @@ -4148,7 +4135,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { // We're declaring the block "imported" at this point, since fork choice and the DB know // about it. - let block_time_imported = timestamp_now(); + let block_time_imported = self.slot_clock.now_duration().unwrap_or(Duration::MAX); // compute state proofs for light client updates before inserting the state into the // snapshot cache. @@ -4217,7 +4204,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { } /// Check block's consistentency with any configured weak subjectivity checkpoint. - fn check_block_against_weak_subjectivity_checkpoint( + pub(crate) fn check_block_against_weak_subjectivity_checkpoint( &self, block: BeaconBlockRef<T::EthSpec>, block_root: Hash256, @@ -4564,55 +4551,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> { Ok(()) } - /// If configured, wait for the fork choice run at the start of the slot to complete. - #[instrument(level = "debug", skip_all)] - fn wait_for_fork_choice_before_block_production( - self: &Arc<Self>, - slot: Slot, - ) -> Result<(), BlockProductionError> { - if let Some(rx) = &self.fork_choice_signal_rx { - let current_slot = self - .slot() - .map_err(|_| BlockProductionError::UnableToReadSlot)?; - - let timeout = Duration::from_millis(self.config.fork_choice_before_proposal_timeout_ms); - - if slot == current_slot || slot == current_slot + 1 { - match rx.wait_for_fork_choice(slot, timeout) { - ForkChoiceWaitResult::Success(fc_slot) => { - debug!( - %slot, - fork_choice_slot = %fc_slot, - "Fork choice successfully updated before block production" - ); - } - ForkChoiceWaitResult::Behind(fc_slot) => { - warn!( - fork_choice_slot = %fc_slot, - %slot, - message = "this block may be orphaned", - "Fork choice notifier out of sync with block production" - ); - } - ForkChoiceWaitResult::TimeOut => { - warn!( - message = "this block may be orphaned", - "Timed out waiting for fork choice before proposal" - ); - } - } - } else { - error!( - %slot, - %current_slot, - message = "check clock sync, this block may be orphaned", - "Producing block at incorrect slot" - ); - } - } - Ok(()) - } - pub async fn produce_block_with_verification( self: &Arc<Self>, randao_reveal: Signature, @@ -4659,165 +4597,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> { .await } - /// Load a beacon state from the database for block production. This is a long-running process - /// that should not be performed in an `async` context. - fn load_state_for_block_production( - self: &Arc<Self>, - slot: Slot, - ) -> Result<(BeaconState<T::EthSpec>, Option<Hash256>), BlockProductionError> { - let fork_choice_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_FORK_CHOICE_TIMES); - self.wait_for_fork_choice_before_block_production(slot)?; - drop(fork_choice_timer); - - let state_load_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_STATE_LOAD_TIMES); - - // Atomically read some values from the head whilst avoiding holding cached head `Arc` any - // longer than necessary. - let (head_slot, head_block_root, head_state_root) = { - let head = self.canonical_head.cached_head(); - ( - head.head_slot(), - head.head_block_root(), - head.head_state_root(), - ) - }; - let (state, state_root_opt) = if head_slot < slot { - // Attempt an aggressive re-org if configured and the conditions are right. - if let Some((re_org_state, re_org_state_root)) = - self.get_state_for_re_org(slot, head_slot, head_block_root) - { - info!( - %slot, - head_to_reorg = %head_block_root, - "Proposing block to re-org current head" - ); - (re_org_state, Some(re_org_state_root)) - } else { - // Fetch the head state advanced through to `slot`, which should be present in the - // state cache thanks to the state advance timer. - let (state_root, state) = self - .store - .get_advanced_hot_state(head_block_root, slot, head_state_root) - .map_err(BlockProductionError::FailedToLoadState)? - .ok_or(BlockProductionError::UnableToProduceAtSlot(slot))?; - (state, Some(state_root)) - } - } else { - warn!( - message = "this block is more likely to be orphaned", - %slot, - "Producing block that conflicts with head" - ); - let state = self - .state_at_slot(slot - 1, StateSkipConfig::WithStateRoots) - .map_err(|_| BlockProductionError::UnableToProduceAtSlot(slot))?; - - (state, None) - }; - - drop(state_load_timer); - - Ok((state, state_root_opt)) - } - - /// Fetch the beacon state to use for producing a block if a 1-slot proposer re-org is viable. - /// - /// This function will return `None` if proposer re-orgs are disabled. - #[instrument(skip_all, level = "debug")] - fn get_state_for_re_org( - &self, - slot: Slot, - head_slot: Slot, - canonical_head: Hash256, - ) -> Option<(BeaconState<T::EthSpec>, Hash256)> { - let re_org_head_threshold = self.config.re_org_head_threshold?; - let re_org_parent_threshold = self.config.re_org_parent_threshold?; - - if self.spec.proposer_score_boost.is_none() { - warn!( - reason = "this network does not have proposer boost enabled", - "Ignoring proposer re-org configuration" - ); - return None; - } - - let slot_delay = self - .slot_clock - .seconds_from_current_slot_start() - .or_else(|| { - warn!(error = "unable to read slot clock", "Not attempting re-org"); - None - })?; - - // Attempt a proposer re-org if: - // - // 1. It seems we have time to propagate and still receive the proposer boost. - // 2. The current head block was seen late. - // 3. The `get_proposer_head` conditions from fork choice pass. - let proposing_on_time = - slot_delay < self.config.re_org_cutoff(self.spec.get_slot_duration()); - if !proposing_on_time { - debug!(reason = "not proposing on time", "Not attempting re-org"); - return None; - } - - let head_late = self.block_observed_after_attestation_deadline(canonical_head, head_slot); - if !head_late { - debug!(reason = "head not late", "Not attempting re-org"); - return None; - } - - // Is the current head weak and appropriate for re-orging? - let proposer_head_timer = - metrics::start_timer(&metrics::BLOCK_PRODUCTION_GET_PROPOSER_HEAD_TIMES); - let proposer_head = self - .canonical_head - .fork_choice_read_lock() - .get_proposer_head( - slot, - canonical_head, - re_org_head_threshold, - re_org_parent_threshold, - &self.config.re_org_disallowed_offsets, - self.config.re_org_max_epochs_since_finalization, - ) - .map_err(|e| match e { - ProposerHeadError::DoNotReOrg(reason) => { - debug!( - %reason, - "Not attempting re-org" - ); - } - ProposerHeadError::Error(e) => { - warn!( - error = ?e, - "Not attempting re-org" - ); - } - }) - .ok()?; - drop(proposer_head_timer); - let re_org_parent_block = proposer_head.parent_node.root; - - let (state_root, state) = self - .store - .get_advanced_hot_state_from_cache(re_org_parent_block, slot) - .or_else(|| { - warn!(reason = "no state in cache", "Not attempting re-org"); - None - })?; - - info!( - weak_head = ?canonical_head, - parent = ?re_org_parent_block, - head_weight = proposer_head.head_node.weight, - threshold_weight = proposer_head.re_org_head_weight_threshold, - "Attempting re-org due to weak head" - ); - - Some((state, state_root)) - } - /// Get the proposer index and `prev_randao` value for a proposal at slot `proposal_slot`. /// /// The `proposer_head` may be the head block of `cached_head` or its parent. An error will @@ -4900,15 +4679,25 @@ impl<T: BeaconChainTypes> BeaconChain<T> { return Ok(None); }; - // Get the `prev_randao` and parent block number. - let head_block_number = cached_head.head_block_number()?; - let (prev_randao, parent_block_number) = if proposer_head == head_parent_block_root { - ( - cached_head.parent_random()?, - head_block_number.saturating_sub(1), - ) + // TODO(gloas) not sure what to do here see this issue + // https://github.com/sigp/lighthouse/issues/8817 + let (prev_randao, parent_block_number) = if self + .spec + .fork_name_at_slot::<T::EthSpec>(proposal_slot) + .gloas_enabled() + { + (cached_head.head_random()?, None) } else { - (cached_head.head_random()?, head_block_number) + // Get the `prev_randao` and parent block number. + let head_block_number = cached_head.head_block_number()?; + if proposer_head == head_parent_block_root { + ( + cached_head.parent_random()?, + Some(head_block_number.saturating_sub(1)), + ) + } else { + (cached_head.head_random()?, Some(head_block_number)) + } }; Ok(Some(PrePayloadAttributes { @@ -5153,7 +4942,11 @@ impl<T: BeaconChainTypes> BeaconChain<T> { } /// Check if the block with `block_root` was observed after the attestation deadline of `slot`. - fn block_observed_after_attestation_deadline(&self, block_root: Hash256, slot: Slot) -> bool { + pub(crate) fn block_observed_after_attestation_deadline( + &self, + block_root: Hash256, + slot: Slot, + ) -> bool { let block_delays = self.block_times_cache.read().get_block_delays( block_root, self.slot_clock @@ -5512,7 +5305,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { err = ?e, block_slot = %state.slot(), ?exit, - "Attempted to include an invalid proposer slashing" + "Attempted to include an invalid voluntary exit" ); }) .is_ok() @@ -5920,7 +5713,11 @@ impl<T: BeaconChainTypes> BeaconChain<T> { execution_payload_value, ) } - BeaconState::Gloas(_) => return Err(BlockProductionError::GloasNotImplemented), + BeaconState::Gloas(_) => { + return Err(BlockProductionError::GloasNotImplemented( + "Attempting to produce gloas beacon block via non gloas code path".to_owned(), + )); + } }; let block = SignedBeaconBlock::from_block( @@ -6258,13 +6055,14 @@ impl<T: BeaconChainTypes> BeaconChain<T> { // Push a server-sent event (probably to a block builder or relay). if let Some(event_handler) = &self.event_handler && event_handler.has_payload_attributes_subscribers() + && let Some(parent_block_number) = pre_payload_attributes.parent_block_number { event_handler.register(EventKind::PayloadAttributes(ForkVersionedResponse { data: SseExtendedPayloadAttributes { proposal_slot: prepare_slot, proposer_index: proposer, parent_block_root: head_root, - parent_block_number: pre_payload_attributes.parent_block_number, + parent_block_number, parent_block_hash: forkchoice_update_params.head_hash.unwrap_or_default(), payload_attributes: payload_attributes.into(), }, @@ -6315,21 +6113,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> { input_params: ForkchoiceUpdateParameters, override_forkchoice_update: OverrideForkchoiceUpdate, ) -> Result<(), Error> { - let next_slot = current_slot + 1; - - // There is no need to issue a `forkchoiceUpdated` (fcU) message unless the Bellatrix fork - // has: - // - // 1. Already happened. - // 2. Will happen in the next slot. - // - // The reason for a fcU message in the slot prior to the Bellatrix fork is in case the - // terminal difficulty has already been reached and a payload preparation message needs to - // be issued. - if self.slot_is_prior_to_bellatrix(next_slot) { - return Ok(()); - } - let execution_layer = self .execution_layer .as_ref() @@ -6377,50 +6160,8 @@ impl<T: BeaconChainTypes> BeaconChain<T> { .unwrap_or_else(ExecutionBlockHash::zero), ) } else { - // The head block does not have an execution block hash. We must check to see if we - // happen to be the proposer of the transition block, in which case we still need to - // send forkchoice_updated. - if self - .spec - .fork_name_at_slot::<T::EthSpec>(next_slot) - .bellatrix_enabled() - { - // We are post-bellatrix - if let Some(payload_attributes) = execution_layer - .payload_attributes(next_slot, params.head_root) - .await - { - // We are a proposer, check for terminal_pow_block_hash - if let Some(terminal_pow_block_hash) = execution_layer - .get_terminal_pow_block_hash(&self.spec, payload_attributes.timestamp()) - .await - .map_err(Error::ForkchoiceUpdate)? - { - info!( - slot = %next_slot, - "Prepared POS transition block proposer" - ); - ( - params.head_root, - terminal_pow_block_hash, - params - .justified_hash - .unwrap_or_else(ExecutionBlockHash::zero), - params - .finalized_hash - .unwrap_or_else(ExecutionBlockHash::zero), - ) - } else { - // TTD hasn't been reached yet, no need to update the EL. - return Ok(()); - } - } else { - // We are not a proposer, no need to update the EL. - return Ok(()); - } - } else { - return Ok(()); - } + // Proposing the block for the merge is no longer supported. + return Ok(()); }; let forkchoice_updated_response = execution_layer @@ -6713,6 +6454,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { // sync anyway). self.naive_aggregation_pool.write().prune(slot); self.block_times_cache.write().prune(slot); + self.envelope_times_cache.write().prune(slot); // Don't run heavy-weight tasks during sync. if self.best_slot() + MAX_PER_SLOT_FORK_CHOICE_DISTANCE < slot { @@ -6772,62 +6514,14 @@ impl<T: BeaconChainTypes> BeaconChain<T> { accessor: impl Fn(&EpochBlockProposers) -> Result<V, BeaconChainError>, state_provider: impl FnOnce() -> Result<(Hash256, BeaconState<T::EthSpec>), E>, ) -> Result<V, E> { - let cache_entry = self - .beacon_proposer_cache - .lock() - .get_or_insert_key(proposal_epoch, shuffling_decision_block); - - // If the cache entry is not initialised, run the code to initialise it inside a OnceCell. - // This prevents duplication of work across multiple threads. - // - // If it is already initialised, then `get_or_try_init` will return immediately without - // executing the initialisation code at all. - let epoch_block_proposers = cache_entry.get_or_try_init(|| { - // Fetch the state on-demand if the required epoch was missing from the cache. - // If the caller wants to not compute the state they must return an error here and then - // catch it at the call site. - let (state_root, mut state) = state_provider()?; - - // Ensure the state can compute proposer duties for `epoch`. - ensure_state_can_determine_proposers_for_epoch( - &mut state, - state_root, - proposal_epoch, - &self.spec, - )?; - - // Sanity check the state. - let latest_block_root = state.get_latest_block_root(state_root); - let state_decision_block_root = state.proposer_shuffling_decision_root_at_epoch( - proposal_epoch, - latest_block_root, - &self.spec, - )?; - if state_decision_block_root != shuffling_decision_block { - return Err(Error::ProposerCacheIncorrectState { - state_decision_block_root, - requested_decision_block_root: shuffling_decision_block, - } - .into()); - } - - let proposers = state.get_beacon_proposer_indices(proposal_epoch, &self.spec)?; - - // Use fork_at_epoch rather than the state's fork, because post-Fulu we may not have - // advanced the state completely into the new epoch. - let fork = self.spec.fork_at_epoch(proposal_epoch); - - debug!( - ?shuffling_decision_block, - epoch = %proposal_epoch, - "Priming proposer shuffling cache" - ); - - Ok::<_, E>(EpochBlockProposers::new(proposal_epoch, fork, proposers)) - })?; - - // Run the accessor function on the computed epoch proposers. - accessor(epoch_block_proposers).map_err(Into::into) + crate::beacon_proposer_cache::with_proposer_cache( + &self.beacon_proposer_cache, + shuffling_decision_block, + proposal_epoch, + accessor, + state_provider, + &self.spec, + ) } /// Runs the `map_fn` with the committee cache for `shuffling_epoch` from the chain with head @@ -7503,16 +7197,16 @@ impl<T: BeaconChainTypes> BeaconChain<T> { block_root: Hash256, block_slot: Slot, block_data: AvailableBlockData<T::EthSpec>, - ) -> Result<Option<StoreOp<'_, T::EthSpec>>, String> { + ) -> Option<StoreOp<'_, T::EthSpec>> { match block_data { - AvailableBlockData::NoData => Ok(None), + AvailableBlockData::NoData => None, AvailableBlockData::Blobs(blobs) => { debug!( %block_root, count = blobs.len(), "Writing blobs to store" ); - Ok(Some(StoreOp::PutBlobs(block_root, blobs))) + Some(StoreOp::PutBlobs(block_root, blobs)) } AvailableBlockData::DataColumns(mut data_columns) => { let columns_to_custody = self.custody_columns_for_epoch(Some( @@ -7528,7 +7222,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { count = data_columns.len(), "Writing data columns to store" ); - Ok(Some(StoreOp::PutDataColumns(block_root, data_columns))) + Some(StoreOp::PutDataColumns(block_root, data_columns)) } } } diff --git a/beacon_node/beacon_chain/src/beacon_proposer_cache.rs b/beacon_node/beacon_chain/src/beacon_proposer_cache.rs index 912f7f3bad..b258d7471f 100644 --- a/beacon_node/beacon_chain/src/beacon_proposer_cache.rs +++ b/beacon_node/beacon_chain/src/beacon_proposer_cache.rs @@ -12,12 +12,13 @@ use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; use fork_choice::ExecutionStatus; use lru::LruCache; use once_cell::sync::OnceCell; +use parking_lot::Mutex; use safe_arith::SafeArith; use smallvec::SmallVec; use state_processing::state_advance::partial_state_advance; use std::num::NonZeroUsize; use std::sync::Arc; -use tracing::instrument; +use tracing::{debug, instrument}; use typenum::Unsigned; use types::new_non_zero_usize; use types::{BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, Fork, Hash256, Slot}; @@ -164,6 +165,82 @@ impl BeaconProposerCache { } } +/// Access the proposer cache, computing and caching the proposers if necessary. +/// +/// This is a free function that operates on references to the cache and spec, decoupled from +/// `BeaconChain`. The `accessor` is called with the cached `EpochBlockProposers` for the given +/// `(proposal_epoch, shuffling_decision_block)` key. If the cache entry is missing, the +/// `state_provider` closure is called to produce a state which is then used to compute and +/// cache the proposers. +pub fn with_proposer_cache<Spec, V, Err>( + beacon_proposer_cache: &Mutex<BeaconProposerCache>, + shuffling_decision_block: Hash256, + proposal_epoch: Epoch, + accessor: impl Fn(&EpochBlockProposers) -> Result<V, BeaconChainError>, + state_provider: impl FnOnce() -> Result<(Hash256, BeaconState<Spec>), Err>, + spec: &ChainSpec, +) -> Result<V, Err> +where + Spec: EthSpec, + Err: From<BeaconChainError> + From<BeaconStateError>, +{ + let cache_entry = beacon_proposer_cache + .lock() + .get_or_insert_key(proposal_epoch, shuffling_decision_block); + + // If the cache entry is not initialised, run the code to initialise it inside a OnceCell. + // This prevents duplication of work across multiple threads. + // + // If it is already initialised, then `get_or_try_init` will return immediately without + // executing the initialisation code at all. + let epoch_block_proposers = cache_entry.get_or_try_init(|| { + // Fetch the state on-demand if the required epoch was missing from the cache. + // If the caller wants to not compute the state they must return an error here and then + // catch it at the call site. + let (state_root, mut state) = state_provider()?; + + // Ensure the state can compute proposer duties for `epoch`. + ensure_state_can_determine_proposers_for_epoch( + &mut state, + state_root, + proposal_epoch, + spec, + )?; + + // Sanity check the state. + let latest_block_root = state.get_latest_block_root(state_root); + let state_decision_block_root = state.proposer_shuffling_decision_root_at_epoch( + proposal_epoch, + latest_block_root, + spec, + )?; + if state_decision_block_root != shuffling_decision_block { + return Err(BeaconChainError::ProposerCacheIncorrectState { + state_decision_block_root, + requested_decision_block_root: shuffling_decision_block, + } + .into()); + } + + let proposers = state.get_beacon_proposer_indices(proposal_epoch, spec)?; + + // Use fork_at_epoch rather than the state's fork, because post-Fulu we may not have + // advanced the state completely into the new epoch. + let fork = spec.fork_at_epoch(proposal_epoch); + + debug!( + ?shuffling_decision_block, + epoch = %proposal_epoch, + "Priming proposer shuffling cache" + ); + + Ok::<_, Err>(EpochBlockProposers::new(proposal_epoch, fork, proposers)) + })?; + + // Run the accessor function on the computed epoch proposers. + accessor(epoch_block_proposers).map_err(Into::into) +} + /// Compute the proposer duties using the head state without cache. /// /// Return: diff --git a/beacon_node/beacon_chain/src/bellatrix_readiness.rs b/beacon_node/beacon_chain/src/bellatrix_readiness.rs index 88ccc21b85..34d9795b84 100644 --- a/beacon_node/beacon_chain/src/bellatrix_readiness.rs +++ b/beacon_node/beacon_chain/src/bellatrix_readiness.rs @@ -1,126 +1,9 @@ -//! Provides tools for checking if a node is ready for the Bellatrix upgrade and following merge -//! transition. +//! Provides tools for checking genesis execution payload consistency. use crate::{BeaconChain, BeaconChainError as Error, BeaconChainTypes}; use execution_layer::BlockByNumberQuery; -use serde::{Deserialize, Serialize, Serializer}; -use std::fmt; -use std::fmt::Write; use types::*; -/// The time before the Bellatrix fork when we will start issuing warnings about preparation. -pub const SECONDS_IN_A_WEEK: u64 = 604800; -pub const BELLATRIX_READINESS_PREPARATION_SECONDS: u64 = SECONDS_IN_A_WEEK * 2; - -#[derive(Default, Debug, Serialize, Deserialize)] -pub struct MergeConfig { - #[serde(serialize_with = "serialize_uint256")] - pub terminal_total_difficulty: Option<Uint256>, - #[serde(skip_serializing_if = "Option::is_none")] - pub terminal_block_hash: Option<ExecutionBlockHash>, - #[serde(skip_serializing_if = "Option::is_none")] - pub terminal_block_hash_epoch: Option<Epoch>, -} - -impl fmt::Display for MergeConfig { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.terminal_block_hash.is_none() - && self.terminal_block_hash_epoch.is_none() - && self.terminal_total_difficulty.is_none() - { - return write!( - f, - "Merge terminal difficulty parameters not configured, check your config" - ); - } - let mut display_string = String::new(); - if let Some(terminal_total_difficulty) = self.terminal_total_difficulty { - write!( - display_string, - "terminal_total_difficulty: {},", - terminal_total_difficulty - )?; - } - if let Some(terminal_block_hash) = self.terminal_block_hash { - write!( - display_string, - "terminal_block_hash: {},", - terminal_block_hash - )?; - } - if let Some(terminal_block_hash_epoch) = self.terminal_block_hash_epoch { - write!( - display_string, - "terminal_block_hash_epoch: {},", - terminal_block_hash_epoch - )?; - } - write!(f, "{}", display_string.trim_end_matches(','))?; - Ok(()) - } -} -impl MergeConfig { - /// Instantiate `self` from the values in a `ChainSpec`. - pub fn from_chainspec(spec: &ChainSpec) -> Self { - let mut params = MergeConfig::default(); - if spec.terminal_total_difficulty != Uint256::MAX { - params.terminal_total_difficulty = Some(spec.terminal_total_difficulty); - } - if spec.terminal_block_hash != ExecutionBlockHash::zero() { - params.terminal_block_hash = Some(spec.terminal_block_hash); - } - if spec.terminal_block_hash_activation_epoch != Epoch::max_value() { - params.terminal_block_hash_epoch = Some(spec.terminal_block_hash_activation_epoch); - } - params - } -} - -/// Indicates if a node is ready for the Bellatrix upgrade and subsequent merge transition. -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -#[serde(tag = "type")] -pub enum BellatrixReadiness { - /// The node is ready, as far as we can tell. - Ready { - config: MergeConfig, - #[serde(serialize_with = "serialize_uint256")] - current_difficulty: Option<Uint256>, - }, - /// The EL can be reached and has the correct configuration, however it's not yet synced. - NotSynced, - /// The user has not configured this node to use an execution endpoint. - NoExecutionEndpoint, -} - -impl fmt::Display for BellatrixReadiness { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - BellatrixReadiness::Ready { - config: params, - current_difficulty, - } => { - write!( - f, - "This node appears ready for Bellatrix \ - Params: {}, current_difficulty: {:?}", - params, current_difficulty - ) - } - BellatrixReadiness::NotSynced => write!( - f, - "The execution endpoint is connected and configured, \ - however it is not yet synced" - ), - BellatrixReadiness::NoExecutionEndpoint => write!( - f, - "The --execution-endpoint flag is not specified, this is a \ - requirement for Bellatrix" - ), - } - } -} - pub enum GenesisExecutionPayloadStatus { Correct(ExecutionBlockHash), BlockHashMismatch { @@ -141,47 +24,6 @@ pub enum GenesisExecutionPayloadStatus { } impl<T: BeaconChainTypes> BeaconChain<T> { - /// Returns `true` if user has an EL configured, or if the Bellatrix fork has occurred or will - /// occur within `BELLATRIX_READINESS_PREPARATION_SECONDS`. - pub fn is_time_to_prepare_for_bellatrix(&self, current_slot: Slot) -> bool { - if let Some(bellatrix_epoch) = self.spec.bellatrix_fork_epoch { - let bellatrix_slot = bellatrix_epoch.start_slot(T::EthSpec::slots_per_epoch()); - let bellatrix_readiness_preparation_slots = - BELLATRIX_READINESS_PREPARATION_SECONDS / self.spec.get_slot_duration().as_secs(); - - if self.execution_layer.is_some() { - // The user has already configured an execution layer, start checking for readiness - // right away. - true - } else { - // Return `true` if Bellatrix has happened or is within the preparation time. - current_slot + bellatrix_readiness_preparation_slots > bellatrix_slot - } - } else { - // The Bellatrix fork epoch has not been defined yet, no need to prepare. - false - } - } - - /// Attempts to connect to the EL and confirm that it is ready for Bellatrix. - pub async fn check_bellatrix_readiness(&self, current_slot: Slot) -> BellatrixReadiness { - if let Some(el) = self.execution_layer.as_ref() { - if !el.is_synced_for_notifier(current_slot).await { - // The EL is not synced. - return BellatrixReadiness::NotSynced; - } - let params = MergeConfig::from_chainspec(&self.spec); - let current_difficulty = el.get_current_difficulty().await.ok().flatten(); - BellatrixReadiness::Ready { - config: params, - current_difficulty, - } - } else { - // There is no EL configured. - BellatrixReadiness::NoExecutionEndpoint - } - } - /// Check that the execution payload embedded in the genesis state matches the EL's genesis /// block. pub async fn check_genesis_execution_payload_is_correct( @@ -223,14 +65,3 @@ impl<T: BeaconChainTypes> BeaconChain<T> { Ok(GenesisExecutionPayloadStatus::Correct(exec_block_hash)) } } - -/// Utility function to serialize a Uint256 as a decimal string. -fn serialize_uint256<S>(val: &Option<Uint256>, s: S) -> Result<S::Ok, S::Error> -where - S: Serializer, -{ - match val { - Some(v) => v.to_string().serialize(s), - None => s.serialize_none(), - } -} diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs new file mode 100644 index 0000000000..607090c59d --- /dev/null +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -0,0 +1,862 @@ +use std::collections::HashMap; +use std::marker::PhantomData; +use std::sync::Arc; + +use bls::Signature; +use execution_layer::{ + BlockProposalContentsGloas, BuilderParams, PayloadAttributes, PayloadParameters, +}; +use operation_pool::CompactAttestationRef; +use ssz::Encode; +use state_processing::common::get_attesting_indices_from_state; +use state_processing::envelope_processing::{VerifyStateRoot, process_execution_payload_envelope}; +use state_processing::epoch_cache::initialize_epoch_cache; +use state_processing::per_block_processing::{ + compute_timestamp_at_slot, get_expected_withdrawals, verify_attestation_for_block_inclusion, +}; +use state_processing::{ + BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, VerifySignatures, +}; +use state_processing::{VerifyOperation, state_advance::complete_state_advance}; +use task_executor::JoinHandle; +use tracing::{Instrument, Span, debug, debug_span, error, instrument, trace, warn}; +use tree_hash::TreeHash; +use types::consts::gloas::BUILDER_INDEX_SELF_BUILD; +use types::{ + Address, Attestation, AttestationElectra, AttesterSlashing, AttesterSlashingElectra, + BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, BeaconState, BeaconStateError, + BuilderIndex, Deposit, Eth1Data, EthSpec, ExecutionBlockHash, ExecutionPayloadBid, + ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, FullPayload, Graffiti, + Hash256, PayloadAttestation, ProposerSlashing, RelativeEpoch, SignedBeaconBlock, + SignedBlsToExecutionChange, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, + SignedVoluntaryExit, Slot, SyncAggregate, Withdrawal, Withdrawals, +}; + +use crate::{ + BeaconChain, BeaconChainError, BeaconChainTypes, BlockProductionError, + ProduceBlockVerification, graffiti_calculator::GraffitiSettings, metrics, +}; + +pub const BID_VALUE_SELF_BUILD: u64 = 0; +pub const EXECUTION_PAYMENT_TRUSTLESS_BUILD: u64 = 0; + +type ConsensusBlockValue = u64; +type BlockProductionResult<E> = (BeaconBlock<E, FullPayload<E>>, ConsensusBlockValue); + +pub type PreparePayloadResult<E> = Result<BlockProposalContentsGloas<E>, BlockProductionError>; +pub type PreparePayloadHandle<E> = JoinHandle<Option<PreparePayloadResult<E>>>; + +pub struct PartialBeaconBlock<E: EthSpec> { + slot: Slot, + proposer_index: u64, + parent_root: Hash256, + randao_reveal: Signature, + eth1_data: Eth1Data, + graffiti: Graffiti, + proposer_slashings: Vec<ProposerSlashing>, + attester_slashings: Vec<AttesterSlashingElectra<E>>, + attestations: Vec<AttestationElectra<E>>, + payload_attestations: Vec<PayloadAttestation<E>>, + deposits: Vec<Deposit>, + voluntary_exits: Vec<SignedVoluntaryExit>, + sync_aggregate: SyncAggregate<E>, + bls_to_execution_changes: Vec<SignedBlsToExecutionChange>, +} + +/// Data needed to construct an ExecutionPayloadEnvelope. +/// The envelope requires the beacon_block_root which can only be computed after the block exists. +pub struct ExecutionPayloadData<E: types::EthSpec> { + pub payload: ExecutionPayloadGloas<E>, + pub execution_requests: ExecutionRequests<E>, + pub builder_index: BuilderIndex, + pub slot: Slot, +} + +impl<T: BeaconChainTypes> BeaconChain<T> { + pub async fn produce_block_with_verification_gloas( + self: &Arc<Self>, + randao_reveal: Signature, + slot: Slot, + graffiti_settings: GraffitiSettings, + verification: ProduceBlockVerification, + _builder_boost_factor: Option<u64>, + ) -> Result<BlockProductionResult<T::EthSpec>, BlockProductionError> { + metrics::inc_counter(&metrics::BLOCK_PRODUCTION_REQUESTS); + let _complete_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_TIMES); + // Part 1/2 (blocking) + // + // Load the parent state from disk. + let chain = self.clone(); + let span = Span::current(); + let (state, state_root_opt) = self + .task_executor + .spawn_blocking_handle( + move || { + let _guard = + debug_span!(parent: span, "load_state_for_block_production").entered(); + chain.load_state_for_block_production(slot) + }, + "load_state_for_block_production", + ) + .ok_or(BlockProductionError::ShuttingDown)? + .await + .map_err(BlockProductionError::TokioJoin)??; + + // Part 2/2 (async, with some blocking components) + // + // Produce the block upon the state + self.produce_block_on_state_gloas( + state, + state_root_opt, + slot, + randao_reveal, + graffiti_settings, + verification, + ) + .await + } + + // TODO(gloas) need to implement builder boost factor logic + #[instrument(level = "debug", skip_all)] + pub async fn produce_block_on_state_gloas( + self: &Arc<Self>, + state: BeaconState<T::EthSpec>, + state_root_opt: Option<Hash256>, + produce_at_slot: Slot, + randao_reveal: Signature, + graffiti_settings: GraffitiSettings, + verification: ProduceBlockVerification, + ) -> Result<BlockProductionResult<T::EthSpec>, BlockProductionError> { + // Part 1/3 (blocking) + // + // Perform the state advance and block-packing functions. + let chain = self.clone(); + let graffiti = self + .graffiti_calculator + .get_graffiti(graffiti_settings) + .await; + let span = Span::current(); + let (partial_beacon_block, state) = self + .task_executor + .spawn_blocking_handle( + move || { + let _guard = + debug_span!(parent: span, "produce_partial_beacon_block_gloas").entered(); + chain.produce_partial_beacon_block_gloas( + state, + state_root_opt, + produce_at_slot, + randao_reveal, + graffiti, + ) + }, + "produce_partial_beacon_block_gloas", + ) + .ok_or(BlockProductionError::ShuttingDown)? + .await + .map_err(BlockProductionError::TokioJoin)??; + + // Part 2/3 (async) + // + // Produce the execution payload bid. + // TODO(gloas) this is strictly for building local bids + // We'll need to build out trustless/trusted bid paths. + let (execution_payload_bid, state, payload_data) = self + .clone() + .produce_execution_payload_bid( + state, + produce_at_slot, + BID_VALUE_SELF_BUILD, + BUILDER_INDEX_SELF_BUILD, + ) + .await?; + + // Part 3/3 (blocking) + // + // Complete the block with the execution payload bid. + let chain = self.clone(); + let span = Span::current(); + self.task_executor + .spawn_blocking_handle( + move || { + let _guard = + debug_span!(parent: span, "complete_partial_beacon_block_gloas").entered(); + chain.complete_partial_beacon_block_gloas( + partial_beacon_block, + execution_payload_bid, + payload_data, + state, + verification, + ) + }, + "complete_partial_beacon_block_gloas", + ) + .ok_or(BlockProductionError::ShuttingDown)? + .await + .map_err(BlockProductionError::TokioJoin)? + } + + #[allow(clippy::too_many_arguments)] + #[allow(clippy::type_complexity)] + fn produce_partial_beacon_block_gloas( + self: &Arc<Self>, + mut state: BeaconState<T::EthSpec>, + state_root_opt: Option<Hash256>, + produce_at_slot: Slot, + randao_reveal: Signature, + graffiti: Graffiti, + ) -> Result<(PartialBeaconBlock<T::EthSpec>, BeaconState<T::EthSpec>), BlockProductionError> + { + // It is invalid to try to produce a block using a state from a future slot. + if state.slot() > produce_at_slot { + return Err(BlockProductionError::StateSlotTooHigh { + produce_at_slot, + state_slot: state.slot(), + }); + } + + let slot_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_SLOT_PROCESS_TIMES); + + // Ensure the state has performed a complete transition into the required slot. + complete_state_advance(&mut state, state_root_opt, produce_at_slot, &self.spec)?; + + drop(slot_timer); + + state.build_committee_cache(RelativeEpoch::Current, &self.spec)?; + state.apply_pending_mutations()?; + + let parent_root = if state.slot() > 0 { + *state + .get_block_root(state.slot() - 1) + .map_err(|_| BlockProductionError::UnableToGetBlockRootFromState)? + } else { + state.latest_block_header().canonical_root() + }; + + let proposer_index = state.get_beacon_proposer_index(state.slot(), &self.spec)? as u64; + + let slashings_and_exits_span = debug_span!("get_slashings_and_exits").entered(); + let (mut proposer_slashings, mut attester_slashings, mut voluntary_exits) = + self.op_pool.get_slashings_and_exits(&state, &self.spec); + + drop(slashings_and_exits_span); + + let eth1_data = state.eth1_data().clone(); + + let deposits = vec![]; + + let bls_changes_span = debug_span!("get_bls_to_execution_changes").entered(); + let bls_to_execution_changes = self + .op_pool + .get_bls_to_execution_changes(&state, &self.spec); + drop(bls_changes_span); + + // Iterate through the naive aggregation pool and ensure all the attestations from there + // are included in the operation pool. + { + let _guard = debug_span!("import_naive_aggregation_pool").entered(); + let _unagg_import_timer = + metrics::start_timer(&metrics::BLOCK_PRODUCTION_UNAGGREGATED_TIMES); + for attestation in self.naive_aggregation_pool.read().iter() { + let import = |attestation: &Attestation<T::EthSpec>| { + let attesting_indices = + get_attesting_indices_from_state(&state, attestation.to_ref())?; + self.op_pool + .insert_attestation(attestation.clone(), attesting_indices) + }; + if let Err(e) = import(attestation) { + // Don't stop block production if there's an error, just create a log. + error!( + reason = ?e, + "Attestation did not transfer to op pool" + ); + } + } + }; + + let mut attestations = { + let _guard = debug_span!("pack_attestations").entered(); + let _attestation_packing_timer = + metrics::start_timer(&metrics::BLOCK_PRODUCTION_ATTESTATION_TIMES); + + // Epoch cache and total balance cache are required for op pool packing. + state.build_total_active_balance_cache(&self.spec)?; + initialize_epoch_cache(&mut state, &self.spec)?; + + let mut prev_filter_cache = HashMap::new(); + let prev_attestation_filter = |att: &CompactAttestationRef<T::EthSpec>| { + self.filter_op_pool_attestation(&mut prev_filter_cache, att, &state) + }; + let mut curr_filter_cache = HashMap::new(); + let curr_attestation_filter = |att: &CompactAttestationRef<T::EthSpec>| { + self.filter_op_pool_attestation(&mut curr_filter_cache, att, &state) + }; + + self.op_pool + .get_attestations( + &state, + prev_attestation_filter, + curr_attestation_filter, + &self.spec, + ) + .map_err(BlockProductionError::OpPoolError)? + }; + + // If paranoid mode is enabled re-check the signatures of every included message. + // This will be a lot slower but guards against bugs in block production and can be + // quickly rolled out without a release. + if self.config.paranoid_block_proposal { + let mut tmp_ctxt = ConsensusContext::new(state.slot()); + attestations.retain(|att| { + verify_attestation_for_block_inclusion( + &state, + att.to_ref(), + &mut tmp_ctxt, + VerifySignatures::True, + &self.spec, + ) + .map_err(|e| { + warn!( + err = ?e, + block_slot = %state.slot(), + attestation = ?att, + "Attempted to include an invalid attestation" + ); + }) + .is_ok() + }); + + proposer_slashings.retain(|slashing| { + slashing + .clone() + .validate(&state, &self.spec) + .map_err(|e| { + warn!( + err = ?e, + block_slot = %state.slot(), + ?slashing, + "Attempted to include an invalid proposer slashing" + ); + }) + .is_ok() + }); + + attester_slashings.retain(|slashing| { + slashing + .clone() + .validate(&state, &self.spec) + .map_err(|e| { + warn!( + err = ?e, + block_slot = %state.slot(), + ?slashing, + "Attempted to include an invalid attester slashing" + ); + }) + .is_ok() + }); + + voluntary_exits.retain(|exit| { + exit.clone() + .validate(&state, &self.spec) + .map_err(|e| { + warn!( + err = ?e, + block_slot = %state.slot(), + ?exit, + "Attempted to include an invalid voluntary exit" + ); + }) + .is_ok() + }); + + // TODO(gloas) verify payload attestation signature here as well + } + + let attester_slashings = attester_slashings + .into_iter() + .filter_map(|a| match a { + AttesterSlashing::Base(_) => None, + AttesterSlashing::Electra(a) => Some(a), + }) + .collect::<Vec<_>>(); + + let attestations = attestations + .into_iter() + .filter_map(|a| match a { + Attestation::Base(_) => None, + Attestation::Electra(a) => Some(a), + }) + .collect::<Vec<_>>(); + + let slot = state.slot(); + + let sync_aggregate = self + .op_pool + .get_sync_aggregate(&state) + .map_err(BlockProductionError::OpPoolError)? + .unwrap_or_else(|| { + warn!( + slot = %state.slot(), + "Producing block with no sync contributions" + ); + SyncAggregate::new() + }); + + Ok(( + PartialBeaconBlock { + slot, + proposer_index, + parent_root, + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + // TODO(gloas) need to implement payload attestations + payload_attestations: vec![], + bls_to_execution_changes, + }, + state, + )) + } + + #[allow(clippy::type_complexity)] + fn complete_partial_beacon_block_gloas( + &self, + partial_beacon_block: PartialBeaconBlock<T::EthSpec>, + signed_execution_payload_bid: SignedExecutionPayloadBid<T::EthSpec>, + payload_data: Option<ExecutionPayloadData<T::EthSpec>>, + mut state: BeaconState<T::EthSpec>, + verification: ProduceBlockVerification, + ) -> Result<(BeaconBlock<T::EthSpec, FullPayload<T::EthSpec>>, u64), BlockProductionError> { + let PartialBeaconBlock { + slot, + proposer_index, + parent_root, + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + payload_attestations, + bls_to_execution_changes, + } = partial_beacon_block; + + let beacon_block = match &state { + BeaconState::Base(_) + | BeaconState::Altair(_) + | BeaconState::Bellatrix(_) + | BeaconState::Capella(_) + | BeaconState::Deneb(_) + | BeaconState::Electra(_) + | BeaconState::Fulu(_) => { + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot construct a block pre-Gloas".to_owned(), + )); + } + BeaconState::Gloas(_) => BeaconBlock::Gloas(BeaconBlockGloas { + slot, + proposer_index, + parent_root, + state_root: Hash256::ZERO, + body: BeaconBlockBodyGloas { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings: proposer_slashings + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attester_slashings: attester_slashings + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attestations: attestations + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + deposits: deposits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + voluntary_exits: voluntary_exits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + sync_aggregate, + bls_to_execution_changes: bls_to_execution_changes + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + signed_execution_payload_bid, + payload_attestations: payload_attestations + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + _phantom: PhantomData::<FullPayload<T::EthSpec>>, + }, + }), + }; + + let signed_beacon_block = SignedBeaconBlock::from_block( + beacon_block, + // The block is not signed here, that is the task of a validator client. + Signature::empty(), + ); + + let block_size = signed_beacon_block.ssz_bytes_len(); + debug!(%block_size, "Produced block on state"); + + metrics::observe(&metrics::BLOCK_SIZE, block_size as f64); + + if block_size > self.config.max_network_size { + return Err(BlockProductionError::BlockTooLarge(block_size)); + } + + let process_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_PROCESS_TIMES); + let signature_strategy = match verification { + ProduceBlockVerification::VerifyRandao => BlockSignatureStrategy::VerifyRandao, + ProduceBlockVerification::NoVerification => BlockSignatureStrategy::NoVerification, + }; + + // Use a context without block root or proposer index so that both are checked. + let mut ctxt = ConsensusContext::new(signed_beacon_block.slot()); + + let consensus_block_value = self + .compute_beacon_block_reward(signed_beacon_block.message(), &mut state) + .map(|reward| reward.total) + .unwrap_or(0); + + state_processing::per_block_processing( + &mut state, + &signed_beacon_block, + signature_strategy, + VerifyBlockRoot::True, + &mut ctxt, + &self.spec, + )?; + drop(process_timer); + + let state_root_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_STATE_ROOT_TIMES); + + let state_root = state.update_tree_hash_cache()?; + + drop(state_root_timer); + + let (mut block, _) = signed_beacon_block.deconstruct(); + *block.state_root_mut() = state_root; + + // Construct and cache the ExecutionPayloadEnvelope if we have payload data. + // For local building, we always have payload data. + // For trustless building, the builder will provide the envelope separately. + if let Some(payload_data) = payload_data { + let beacon_block_root = block.tree_hash_root(); + let execution_payload_envelope = ExecutionPayloadEnvelope { + payload: payload_data.payload, + execution_requests: payload_data.execution_requests, + builder_index: payload_data.builder_index, + beacon_block_root, + slot: payload_data.slot, + state_root: Hash256::ZERO, + }; + + let mut signed_envelope = SignedExecutionPayloadEnvelope { + message: execution_payload_envelope, + signature: Signature::empty(), + }; + + // We skip state root verification here because the relevant state root + // cant be calculated until after the new block has been constructed. + process_execution_payload_envelope( + &mut state, + None, + &signed_envelope, + VerifySignatures::False, + VerifyStateRoot::False, + &self.spec, + ) + .map_err(BlockProductionError::EnvelopeProcessingError)?; + + signed_envelope.message.state_root = state.update_tree_hash_cache()?; + + // Cache the envelope for later retrieval by the validator for signing and publishing. + let envelope_slot = payload_data.slot; + // TODO(gloas) might be safer to cache by root instead of by slot. + // We should revisit this once this code path + beacon api spec matures + self.pending_payload_envelopes + .write() + .insert(envelope_slot, signed_envelope.message); + + debug!( + %beacon_block_root, + slot = %envelope_slot, + "Cached pending execution payload envelope" + ); + } + + metrics::inc_counter(&metrics::BLOCK_PRODUCTION_SUCCESSES); + + trace!( + parent = ?block.parent_root(), + attestations = block.body().attestations_len(), + slot = %block.slot(), + "Produced beacon block" + ); + + Ok((block, consensus_block_value)) + } + + // TODO(gloas) introduce `ProposerPreferences` so we can build out trustless + // bid building. Right now this only works for local building. + /// Produce an `ExecutionPayloadBid` for some `slot` upon the given `state`. + /// This function assumes we've already advanced `state`. + /// + /// Returns the signed bid, the state, and optionally the payload data needed to construct + /// the `ExecutionPayloadEnvelope` after the beacon block is created. + /// + /// For local building, payload data is always returned (`Some`). + /// For trustless building, the builder provides the envelope separately, so `None` is returned. + #[allow(clippy::type_complexity)] + #[instrument(level = "debug", skip_all)] + pub async fn produce_execution_payload_bid( + self: Arc<Self>, + mut state: BeaconState<T::EthSpec>, + produce_at_slot: Slot, + bid_value: u64, + builder_index: BuilderIndex, + ) -> Result< + ( + SignedExecutionPayloadBid<T::EthSpec>, + BeaconState<T::EthSpec>, + Option<ExecutionPayloadData<T::EthSpec>>, + ), + BlockProductionError, + > { + // TODO(gloas) For non local building, add sanity check on value + // The builder MUST have enough excess balance to fulfill this bid (i.e. `value`) and all pending payments. + + // TODO(gloas) add metrics for execution payload bid production + + let parent_root = if state.slot() > 0 { + *state + .get_block_root(state.slot() - 1) + .map_err(|_| BlockProductionError::UnableToGetBlockRootFromState)? + } else { + state.latest_block_header().canonical_root() + }; + + let proposer_index = state.get_beacon_proposer_index(state.slot(), &self.spec)? as u64; + + let pubkey = state + .validators() + .get(proposer_index as usize) + .map(|v| v.pubkey) + .ok_or(BlockProductionError::BeaconChain(Box::new( + BeaconChainError::ValidatorIndexUnknown(proposer_index as usize), + )))?; + + let builder_params = BuilderParams { + pubkey, + slot: state.slot(), + chain_health: self + .is_healthy(&parent_root) + .map_err(|e| BlockProductionError::BeaconChain(Box::new(e)))?, + }; + + // TODO(gloas) this should be BlockProductionVersion::V4 + // V3 is okay for now as long as we're not connected to a builder + // TODO(gloas) add builder boost factor + let prepare_payload_handle = get_execution_payload_gloas( + self.clone(), + &state, + parent_root, + proposer_index, + builder_params, + )?; + + let block_proposal_contents = prepare_payload_handle + .await + .map_err(BlockProductionError::TokioJoin)? + .ok_or(BlockProductionError::ShuttingDown)??; + + let BlockProposalContentsGloas { + payload, + payload_value: _, + execution_requests, + blob_kzg_commitments, + blobs_and_proofs: _, + } = block_proposal_contents; + + let state_root = state.update_tree_hash_cache()?; + + // TODO(gloas) since we are defaulting to local building, execution payment is 0 + // execution payment should only be set to > 0 for trusted building. + let bid = ExecutionPayloadBid::<T::EthSpec> { + parent_block_hash: state.latest_block_hash()?.to_owned(), + parent_block_root: state.get_latest_block_root(state_root), + block_hash: payload.block_hash, + prev_randao: payload.prev_randao, + fee_recipient: Address::ZERO, + gas_limit: payload.gas_limit, + builder_index, + slot: produce_at_slot, + value: bid_value, + execution_payment: EXECUTION_PAYMENT_TRUSTLESS_BUILD, + blob_kzg_commitments, + }; + + // Store payload data for envelope construction after block is created + let payload_data = ExecutionPayloadData { + payload, + execution_requests, + builder_index, + slot: produce_at_slot, + }; + + // TODO(gloas) this is only local building + // we'll need to implement builder signature for the trustless path + Ok(( + SignedExecutionPayloadBid { + message: bid, + signature: Signature::infinity().map_err(BlockProductionError::BlsError)?, + }, + state, + // Local building always returns payload data. + // Trustless building would return None here. + Some(payload_data), + )) + } +} + +/// Gets an execution payload for inclusion in a block. +/// +/// ## Errors +/// +/// Will return an error when using a pre-Gloas `state`. Ensure to only run this function +/// after the Gloas fork. +fn get_execution_payload_gloas<T: BeaconChainTypes>( + chain: Arc<BeaconChain<T>>, + state: &BeaconState<T::EthSpec>, + parent_beacon_block_root: Hash256, + proposer_index: u64, + builder_params: BuilderParams, +) -> Result<PreparePayloadHandle<T::EthSpec>, BlockProductionError> { + // Compute all required values from the `state` now to avoid needing to pass it into a spawned + // task. + let spec = &chain.spec; + let current_epoch = state.current_epoch(); + let timestamp = + compute_timestamp_at_slot(state, state.slot(), spec).map_err(BeaconStateError::from)?; + let random = *state.get_randao_mix(current_epoch)?; + + let latest_execution_block_hash = *state.latest_block_hash()?; + let latest_gas_limit = state.latest_execution_payload_bid()?.gas_limit; + + let withdrawals = + Withdrawals::<T::EthSpec>::from(get_expected_withdrawals(state, spec)?).into(); + + // Spawn a task to obtain the execution payload from the EL via a series of async calls. The + // `join_handle` can be used to await the result of the function. + let join_handle = chain + .task_executor + .clone() + .spawn_handle( + async move { + prepare_execution_payload::<T>( + &chain, + timestamp, + random, + proposer_index, + latest_execution_block_hash, + latest_gas_limit, + builder_params, + withdrawals, + parent_beacon_block_root, + ) + .await + } + .instrument(debug_span!("prepare_execution_payload")), + "prepare_execution_payload", + ) + .ok_or(BlockProductionError::ShuttingDown)?; + + Ok(join_handle) +} + +/// Prepares an execution payload for inclusion in a block. +/// +/// ## Errors +/// +/// Will return an error when using a pre-Gloas fork `state`. Ensure to only run this function +/// after the Gloas fork. +#[allow(clippy::too_many_arguments)] +async fn prepare_execution_payload<T>( + chain: &Arc<BeaconChain<T>>, + timestamp: u64, + random: Hash256, + proposer_index: u64, + parent_block_hash: ExecutionBlockHash, + parent_gas_limit: u64, + builder_params: BuilderParams, + withdrawals: Vec<Withdrawal>, + parent_beacon_block_root: Hash256, +) -> Result<BlockProposalContentsGloas<T::EthSpec>, BlockProductionError> +where + T: BeaconChainTypes, +{ + let spec = &chain.spec; + let fork = spec.fork_name_at_slot::<T::EthSpec>(builder_params.slot); + let execution_layer = chain + .execution_layer + .as_ref() + .ok_or(BlockProductionError::ExecutionLayerMissing)?; + + // Try to obtain the fork choice update parameters from the cached head. + // + // Use a blocking task to interact with the `canonical_head` lock otherwise we risk blocking the + // core `tokio` executor. + let inner_chain = chain.clone(); + let forkchoice_update_params = chain + .spawn_blocking_handle( + move || { + inner_chain + .canonical_head + .cached_head() + .forkchoice_update_parameters() + }, + "prepare_execution_payload_forkchoice_update_params", + ) + .instrument(debug_span!("forkchoice_update_params")) + .await + .map_err(|e| BlockProductionError::BeaconChain(Box::new(e)))?; + + let suggested_fee_recipient = execution_layer + .get_suggested_fee_recipient(proposer_index) + .await; + let payload_attributes = PayloadAttributes::new( + timestamp, + random, + suggested_fee_recipient, + Some(withdrawals), + Some(parent_beacon_block_root), + ); + + let target_gas_limit = execution_layer.get_proposer_gas_limit(proposer_index).await; + let payload_parameters = PayloadParameters { + parent_hash: parent_block_hash, + parent_gas_limit, + proposer_gas_limit: target_gas_limit, + payload_attributes: &payload_attributes, + forkchoice_update_params: &forkchoice_update_params, + current_fork: fork, + }; + + let block_contents = execution_layer + .get_payload_gloas(payload_parameters) + .await + .map_err(BlockProductionError::GetPayloadFailed)?; + + Ok(block_contents) +} diff --git a/beacon_node/beacon_chain/src/block_production/mod.rs b/beacon_node/beacon_chain/src/block_production/mod.rs new file mode 100644 index 0000000000..76c8b77e93 --- /dev/null +++ b/beacon_node/beacon_chain/src/block_production/mod.rs @@ -0,0 +1,223 @@ +use std::{sync::Arc, time::Duration}; + +use proto_array::ProposerHeadError; +use slot_clock::SlotClock; +use tracing::{debug, error, info, instrument, warn}; +use types::{BeaconState, Hash256, Slot}; + +use crate::{ + BeaconChain, BeaconChainTypes, BlockProductionError, StateSkipConfig, + fork_choice_signal::ForkChoiceWaitResult, metrics, +}; + +mod gloas; + +impl<T: BeaconChainTypes> BeaconChain<T> { + /// Load a beacon state from the database for block production. This is a long-running process + /// that should not be performed in an `async` context. + pub(crate) fn load_state_for_block_production( + self: &Arc<Self>, + slot: Slot, + ) -> Result<(BeaconState<T::EthSpec>, Option<Hash256>), BlockProductionError> { + let fork_choice_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_FORK_CHOICE_TIMES); + self.wait_for_fork_choice_before_block_production(slot)?; + drop(fork_choice_timer); + + let state_load_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_STATE_LOAD_TIMES); + + // Atomically read some values from the head whilst avoiding holding cached head `Arc` any + // longer than necessary. + let (head_slot, head_block_root, head_state_root) = { + let head = self.canonical_head.cached_head(); + ( + head.head_slot(), + head.head_block_root(), + head.head_state_root(), + ) + }; + let (state, state_root_opt) = if head_slot < slot { + // Attempt an aggressive re-org if configured and the conditions are right. + if let Some((re_org_state, re_org_state_root)) = + self.get_state_for_re_org(slot, head_slot, head_block_root) + { + info!( + %slot, + head_to_reorg = %head_block_root, + "Proposing block to re-org current head" + ); + (re_org_state, Some(re_org_state_root)) + } else { + // Fetch the head state advanced through to `slot`, which should be present in the + // state cache thanks to the state advance timer. + let (state_root, state) = self + .store + .get_advanced_hot_state(head_block_root, slot, head_state_root) + .map_err(BlockProductionError::FailedToLoadState)? + .ok_or(BlockProductionError::UnableToProduceAtSlot(slot))?; + (state, Some(state_root)) + } + } else { + warn!( + message = "this block is more likely to be orphaned", + %slot, + "Producing block that conflicts with head" + ); + let state = self + .state_at_slot(slot - 1, StateSkipConfig::WithStateRoots) + .map_err(|_| BlockProductionError::UnableToProduceAtSlot(slot))?; + + (state, None) + }; + + drop(state_load_timer); + + Ok((state, state_root_opt)) + } + + /// If configured, wait for the fork choice run at the start of the slot to complete. + #[instrument(level = "debug", skip_all)] + fn wait_for_fork_choice_before_block_production( + self: &Arc<Self>, + slot: Slot, + ) -> Result<(), BlockProductionError> { + if let Some(rx) = &self.fork_choice_signal_rx { + let current_slot = self + .slot() + .map_err(|_| BlockProductionError::UnableToReadSlot)?; + + let timeout = Duration::from_millis(self.config.fork_choice_before_proposal_timeout_ms); + + if slot == current_slot || slot == current_slot + 1 { + match rx.wait_for_fork_choice(slot, timeout) { + ForkChoiceWaitResult::Success(fc_slot) => { + debug!( + %slot, + fork_choice_slot = %fc_slot, + "Fork choice successfully updated before block production" + ); + } + ForkChoiceWaitResult::Behind(fc_slot) => { + warn!( + fork_choice_slot = %fc_slot, + %slot, + message = "this block may be orphaned", + "Fork choice notifier out of sync with block production" + ); + } + ForkChoiceWaitResult::TimeOut => { + warn!( + message = "this block may be orphaned", + "Timed out waiting for fork choice before proposal" + ); + } + } + } else { + error!( + %slot, + %current_slot, + message = "check clock sync, this block may be orphaned", + "Producing block at incorrect slot" + ); + } + } + Ok(()) + } + + /// Fetch the beacon state to use for producing a block if a 1-slot proposer re-org is viable. + /// + /// This function will return `None` if proposer re-orgs are disabled. + #[instrument(skip_all, level = "debug")] + fn get_state_for_re_org( + &self, + slot: Slot, + head_slot: Slot, + canonical_head: Hash256, + ) -> Option<(BeaconState<T::EthSpec>, Hash256)> { + let re_org_head_threshold = self.config.re_org_head_threshold?; + let re_org_parent_threshold = self.config.re_org_parent_threshold?; + + if self.spec.proposer_score_boost.is_none() { + warn!( + reason = "this network does not have proposer boost enabled", + "Ignoring proposer re-org configuration" + ); + return None; + } + + let slot_delay = self + .slot_clock + .seconds_from_current_slot_start() + .or_else(|| { + warn!(error = "unable to read slot clock", "Not attempting re-org"); + None + })?; + + // Attempt a proposer re-org if: + // + // 1. It seems we have time to propagate and still receive the proposer boost. + // 2. The current head block was seen late. + // 3. The `get_proposer_head` conditions from fork choice pass. + let proposing_on_time = + slot_delay < self.config.re_org_cutoff(self.spec.get_slot_duration()); + if !proposing_on_time { + debug!(reason = "not proposing on time", "Not attempting re-org"); + return None; + } + + let head_late = self.block_observed_after_attestation_deadline(canonical_head, head_slot); + if !head_late { + debug!(reason = "head not late", "Not attempting re-org"); + return None; + } + + // Is the current head weak and appropriate for re-orging? + let proposer_head_timer = + metrics::start_timer(&metrics::BLOCK_PRODUCTION_GET_PROPOSER_HEAD_TIMES); + let proposer_head = self + .canonical_head + .fork_choice_read_lock() + .get_proposer_head( + slot, + canonical_head, + re_org_head_threshold, + re_org_parent_threshold, + &self.config.re_org_disallowed_offsets, + self.config.re_org_max_epochs_since_finalization, + ) + .map_err(|e| match e { + ProposerHeadError::DoNotReOrg(reason) => { + debug!( + %reason, + "Not attempting re-org" + ); + } + ProposerHeadError::Error(e) => { + warn!( + error = ?e, + "Not attempting re-org" + ); + } + }) + .ok()?; + drop(proposer_head_timer); + let re_org_parent_block = proposer_head.parent_node.root; + + let (state_root, state) = self + .store + .get_advanced_hot_state_from_cache(re_org_parent_block, slot) + .or_else(|| { + warn!(reason = "no state in cache", "Not attempting re-org"); + None + })?; + + info!( + weak_head = ?canonical_head, + parent = ?re_org_parent_block, + head_weight = proposer_head.head_node.weight, + threshold_weight = proposer_head.re_org_head_weight_threshold, + "Attempting re-org due to weak head" + ); + + Some((state, state_root)) + } +} diff --git a/beacon_node/beacon_chain/src/block_reward.rs b/beacon_node/beacon_chain/src/block_reward.rs deleted file mode 100644 index f3924bb473..0000000000 --- a/beacon_node/beacon_chain/src/block_reward.rs +++ /dev/null @@ -1,140 +0,0 @@ -use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; -use eth2::lighthouse::{AttestationRewards, BlockReward, BlockRewardMeta}; -use operation_pool::{ - AttMaxCover, MaxCover, PROPOSER_REWARD_DENOMINATOR, RewardCache, SplitAttestation, -}; -use state_processing::{ - common::get_attesting_indices_from_state, - per_block_processing::altair::sync_committee::compute_sync_aggregate_rewards, -}; -use types::{AbstractExecPayload, BeaconBlockRef, BeaconState, EthSpec, Hash256}; - -impl<T: BeaconChainTypes> BeaconChain<T> { - pub fn compute_block_reward<Payload: AbstractExecPayload<T::EthSpec>>( - &self, - block: BeaconBlockRef<'_, T::EthSpec, Payload>, - block_root: Hash256, - state: &BeaconState<T::EthSpec>, - reward_cache: &mut RewardCache, - include_attestations: bool, - ) -> Result<BlockReward, BeaconChainError> { - if block.slot() != state.slot() { - return Err(BeaconChainError::BlockRewardSlotError); - } - - reward_cache.update(state)?; - - let total_active_balance = state.get_total_active_balance()?; - - let split_attestations = block - .body() - .attestations() - .map(|att| { - let attesting_indices = get_attesting_indices_from_state(state, att)?; - Ok(SplitAttestation::new( - att.clone_as_attestation(), - attesting_indices, - )) - }) - .collect::<Result<Vec<_>, BeaconChainError>>()?; - - let mut per_attestation_rewards = split_attestations - .iter() - .map(|att| { - AttMaxCover::new( - att.as_ref(), - state, - reward_cache, - total_active_balance, - &self.spec, - ) - .ok_or(BeaconChainError::BlockRewardAttestationError) - }) - .collect::<Result<Vec<_>, _>>()?; - - // Update the attestation rewards for each previous attestation included. - // This is O(n^2) in the number of attestations n. - for i in 0..per_attestation_rewards.len() { - let (updated, to_update) = per_attestation_rewards.split_at_mut(i + 1); - let latest_att = &updated[i]; - - for att in to_update { - att.update_covering_set(latest_att.intermediate(), latest_att.covering_set()); - } - } - - let mut prev_epoch_total = 0; - let mut curr_epoch_total = 0; - - for cover in &per_attestation_rewards { - if cover.att.data.slot.epoch(T::EthSpec::slots_per_epoch()) == state.current_epoch() { - curr_epoch_total += cover.score() as u64; - } else { - prev_epoch_total += cover.score() as u64; - } - } - - let attestation_total = prev_epoch_total + curr_epoch_total; - - // Drop the covers. - let per_attestation_rewards = per_attestation_rewards - .into_iter() - .map(|cover| { - // Divide each reward numerator by the denominator. This can lead to the total being - // less than the sum of the individual rewards due to the fact that integer division - // does not distribute over addition. - let mut rewards = cover.fresh_validators_rewards; - rewards - .values_mut() - .for_each(|reward| *reward /= PROPOSER_REWARD_DENOMINATOR); - rewards - }) - .collect(); - - // Add the attestation data if desired. - let attestations = if include_attestations { - block - .body() - .attestations() - .map(|a| a.data().clone()) - .collect() - } else { - vec![] - }; - - let attestation_rewards = AttestationRewards { - total: attestation_total, - prev_epoch_total, - curr_epoch_total, - per_attestation_rewards, - attestations, - }; - - // Sync committee rewards. - let sync_committee_rewards = if let Ok(sync_aggregate) = block.body().sync_aggregate() { - let (_, proposer_reward_per_bit) = compute_sync_aggregate_rewards(state, &self.spec) - .map_err(|_| BeaconChainError::BlockRewardSyncError)?; - sync_aggregate.sync_committee_bits.num_set_bits() as u64 * proposer_reward_per_bit - } else { - 0 - }; - - // Total, metadata - let total = attestation_total + sync_committee_rewards; - - let meta = BlockRewardMeta { - slot: block.slot(), - parent_slot: state.latest_block_header().slot, - proposer_index: block.proposer_index(), - graffiti: block.body().graffiti().as_utf8_lossy(), - }; - - Ok(BlockReward { - total, - block_root, - meta, - attestation_rewards, - sync_committee_rewards, - }) - } -} diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 6edb6e1ada..0ab6287ed3 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -51,11 +51,12 @@ use crate::beacon_snapshot::PreProcessingSnapshot; use crate::blob_verification::GossipBlobError; use crate::block_verification_types::{AsBlock, BlockImportData, RpcBlock}; -use crate::data_availability_checker::{AvailabilityCheckError, MaybeAvailableBlock}; +use crate::data_availability_checker::{ + AvailabilityCheckError, AvailableBlock, AvailableBlockData, MaybeAvailableBlock, +}; use crate::data_column_verification::GossipDataColumnError; use crate::execution_payload::{ - AllowOptimisticImport, NotifyExecutionLayer, PayloadNotifier, - validate_execution_payload_for_gossip, validate_merge_block, + NotifyExecutionLayer, PayloadNotifier, validate_execution_payload_for_gossip, }; use crate::kzg_utils::blobs_to_data_column_sidecars; use crate::observed_block_producers::SeenBlock; @@ -78,7 +79,7 @@ use safe_arith::ArithError; use slot_clock::SlotClock; use ssz::Encode; use ssz_derive::{Decode, Encode}; -use state_processing::per_block_processing::{errors::IntoWithIndex, is_merge_transition_block}; +use state_processing::per_block_processing::errors::IntoWithIndex; use state_processing::{ AllCaches, BlockProcessingError, BlockSignatureStrategy, ConsensusContext, SlotProcessingError, VerifyBlockRoot, @@ -97,34 +98,10 @@ use task_executor::JoinHandle; use tracing::{Instrument, Span, debug, debug_span, error, info_span, instrument}; use types::{ BeaconBlockRef, BeaconState, BeaconStateError, BlobsList, ChainSpec, DataColumnSidecarList, - Epoch, EthSpec, ExecutionBlockHash, FullPayload, Hash256, InconsistentFork, KzgProofs, - RelativeEpoch, SignedBeaconBlock, SignedBeaconBlockHeader, Slot, data::DataColumnSidecarError, + Epoch, EthSpec, FullPayload, Hash256, InconsistentFork, KzgProofs, RelativeEpoch, + SignedBeaconBlock, SignedBeaconBlockHeader, Slot, data::DataColumnSidecarError, }; -pub const POS_PANDA_BANNER: &str = r#" - ,,, ,,, ,,, ,,, - ;" ^; ;' ", ;" ^; ;' ", - ; s$$$$$$$s ; ; s$$$$$$$s ; - , ss$$$$$$$$$$s ,' ooooooooo. .oooooo. .oooooo..o , ss$$$$$$$$$$s ,' - ;s$$$$$$$$$$$$$$$ `888 `Y88. d8P' `Y8b d8P' `Y8 ;s$$$$$$$$$$$$$$$ - $$$$$$$$$$$$$$$$$$ 888 .d88'888 888Y88bo. $$$$$$$$$$$$$$$$$$ - $$$$P""Y$$$Y""W$$$$$ 888ooo88P' 888 888 `"Y8888o. $$$$P""Y$$$Y""W$$$$$ - $$$$ p"LFG"q $$$$$ 888 888 888 `"Y88b $$$$ p"LFG"q $$$$$ - $$$$ .$$$$$. $$$$ 888 `88b d88'oo .d8P $$$$ .$$$$$. $$$$ - $$DcaU$$$$$$$$$$ o888o `Y8bood8P' 8""88888P' $$DcaU$$$$$$$$$$ - "Y$$$"*"$$$Y" "Y$$$"*"$$$Y" - "$b.$$" "$b.$$" - - .o. . o8o . .o8 - .888. .o8 `"' .o8 "888 - .8"888. .ooooo. .o888oooooo oooo ooo .oooo. .o888oo .ooooo. .oooo888 - .8' `888. d88' `"Y8 888 `888 `88. .8' `P )88b 888 d88' `88bd88' `888 - .88ooo8888. 888 888 888 `88..8' .oP"888 888 888ooo888888 888 - .8' `888. 888 .o8 888 . 888 `888' d8( 888 888 .888 .o888 888 - o88o o8888o`Y8bod8P' "888"o888o `8' `Y888""8o "888"`Y8bod8P'`Y8bod88P" - -"#; - /// Maximum block slot number. Block with slots bigger than this constant will NOT be processed. const MAXIMUM_BLOCK_SLOT_NUMBER: u64 = 4_294_967_296; // 2^32 @@ -334,6 +311,15 @@ pub enum BlockError { max_blobs_at_epoch: usize, block: usize, }, + /// The bid's parent_block_root does not match the block's parent_root. + /// + /// ## Peer scoring + /// + /// The block is invalid and the peer should be penalized. + BidParentRootMismatch { + bid_parent_root: Hash256, + block_parent_root: Hash256, + }, } /// Which specific signature(s) are invalid in a SignedBeaconBlock @@ -381,13 +367,6 @@ pub enum ExecutionPayloadError { /// /// The block is invalid and the peer is faulty InvalidPayloadTimestamp { expected: u64, found: u64 }, - /// The execution payload references an execution block that cannot trigger the merge. - /// - /// ## Peer scoring - /// - /// The block is invalid and the peer sent us a block that passes gossip propagation conditions, - /// but is invalid upon further verification. - InvalidTerminalPoWBlock { parent_hash: ExecutionBlockHash }, /// The `TERMINAL_BLOCK_HASH` is set, but the block has not reached the /// `TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH`. /// @@ -399,16 +378,6 @@ pub enum ExecutionPayloadError { activation_epoch: Epoch, epoch: Epoch, }, - /// The `TERMINAL_BLOCK_HASH` is set, but does not match the value specified by the block. - /// - /// ## Peer scoring - /// - /// The block is invalid and the peer sent us a block that passes gossip propagation conditions, - /// but is invalid upon further verification. - InvalidTerminalBlockHash { - terminal_block_hash: ExecutionBlockHash, - payload_parent_hash: ExecutionBlockHash, - }, /// The execution node is syncing but we fail the conditions for optimistic sync /// /// ## Peer scoring @@ -433,16 +402,11 @@ impl ExecutionPayloadError { // This is a trivial gossip validation condition, there is no reason for an honest peer // to propagate a block with an invalid payload time stamp. ExecutionPayloadError::InvalidPayloadTimestamp { .. } => true, - // An honest optimistic node may propagate blocks with an invalid terminal PoW block, we - // should not penalized them. - ExecutionPayloadError::InvalidTerminalPoWBlock { .. } => false, // This condition is checked *after* gossip propagation, therefore penalizing gossip // peers for this block would be unfair. There may be an argument to penalize RPC // blocks, since even an optimistic node shouldn't verify this block. We will remove the // penalties for all block imports to keep things simple. ExecutionPayloadError::InvalidActivationEpoch { .. } => false, - // As per `Self::InvalidActivationEpoch`. - ExecutionPayloadError::InvalidTerminalBlockHash { .. } => false, // Do not penalize the peer since it's not their fault that *we're* optimistic. ExecutionPayloadError::UnverifiedNonOptimisticCandidate => false, } @@ -526,7 +490,6 @@ impl From<ArithError> for BlockError { #[derive(Debug, PartialEq, Clone, Encode, Decode)] pub struct PayloadVerificationOutcome { pub payload_verification_status: PayloadVerificationStatus, - pub is_valid_merge_transition_block: bool, } /// Information about invalid blocks which might still be slashable despite being invalid. @@ -719,7 +682,8 @@ pub struct SignatureVerifiedBlock<T: BeaconChainTypes> { } /// Used to await the result of executing payload with an EE. -type PayloadVerificationHandle = JoinHandle<Option<Result<PayloadVerificationOutcome, BlockError>>>; +pub type PayloadVerificationHandle = + JoinHandle<Option<Result<PayloadVerificationOutcome, BlockError>>>; /// A wrapper around a `SignedBeaconBlock` that indicates that this block is fully verified and /// ready to import into the `BeaconChain`. The validation includes: @@ -888,15 +852,15 @@ impl<T: BeaconChainTypes> GossipVerifiedBlock<T> { // Do not gossip blocks that claim to contain more blobs than the max allowed // at the given block epoch. - if let Ok(commitments) = block.message().body().blob_kzg_commitments() { + if let Some(blob_kzg_commitments_len) = block.message().blob_kzg_commitments_len() { let max_blobs_at_epoch = chain .spec .max_blobs_per_block(block.slot().epoch(T::EthSpec::slots_per_epoch())) as usize; - if commitments.len() > max_blobs_at_epoch { + if blob_kzg_commitments_len > max_blobs_at_epoch { return Err(BlockError::InvalidBlobCount { max_blobs_at_epoch, - block: commitments.len(), + block: blob_kzg_commitments_len, }); } } @@ -933,6 +897,24 @@ impl<T: BeaconChainTypes> GossipVerifiedBlock<T> { let block_epoch = block.slot().epoch(T::EthSpec::slots_per_epoch()); let (parent_block, block) = verify_parent_block_is_known::<T>(&fork_choice_read_lock, block)?; + + // [New in Gloas]: Verify bid.parent_block_root matches block.parent_root. + if let Ok(bid) = block.message().body().signed_execution_payload_bid() + && bid.message.parent_block_root != block.message().parent_root() + { + return Err(BlockError::BidParentRootMismatch { + bid_parent_root: bid.message.parent_block_root, + block_parent_root: block.message().parent_root(), + }); + } + + // TODO(gloas) The following validation can only be completed once fork choice has been implemented: + // The block's parent execution payload (defined by bid.parent_block_hash) has been seen + // (via gossip or non-gossip sources) (a client MAY queue blocks for processing + // once the parent payload is retrieved). If execution_payload verification of block's execution + // payload parent by an execution node is complete, verify the block's execution payload + // parent (defined by bid.parent_block_hash) passes all validation. + drop(fork_choice_read_lock); // Track the number of skip slots between the block and its parent. @@ -1039,8 +1021,15 @@ impl<T: BeaconChainTypes> GossipVerifiedBlock<T> { }); } - // Validate the block's execution_payload (if any). - validate_execution_payload_for_gossip(&parent_block, block.message(), chain)?; + // [New in Gloas]: Skip payload validation checks. The payload now arrives separately + // via `ExecutionPayloadEnvelope`. + if !chain + .spec + .fork_name_at_slot::<T::EthSpec>(block.slot()) + .gloas_enabled() + { + validate_execution_payload_for_gossip(&parent_block, block.message(), chain)?; + } // Beacon API block_gossip events if let Some(event_handler) = chain.event_handler.as_ref() @@ -1212,15 +1201,36 @@ impl<T: BeaconChainTypes> SignatureVerifiedBlock<T> { let result = info_span!("signature_verify").in_scope(|| signature_verifier.verify()); match result { - Ok(_) => Ok(Self { - block: MaybeAvailableBlock::AvailabilityPending { + Ok(_) => { + // gloas blocks are always available. + let maybe_available = if chain + .spec + .fork_name_at_slot::<T::EthSpec>(block.slot()) + .gloas_enabled() + { + MaybeAvailableBlock::Available( + AvailableBlock::new( + block, + AvailableBlockData::NoData, + // TODO(gloas) shouldnt matter which da checker we pass? + chain.data_availability_checker.v1().as_ref(), + chain.spec.clone(), + ) + .map_err(BlockError::AvailabilityCheck)?, + ) + } else { + MaybeAvailableBlock::AvailabilityPending { + block_root: from.block_root, + block, + } + }; + Ok(Self { + block: maybe_available, block_root: from.block_root, - block, - }, - block_root: from.block_root, - parent: Some(parent), - consensus_context, - }), + parent: Some(parent), + consensus_context, + }) + } Err(_) => Err(BlockError::InvalidSignature( InvalidSignature::BlockBodySignatures, )), @@ -1352,7 +1362,7 @@ impl<T: BeaconChainTypes> ExecutionPendingBlock<T> { /// verification must be done upstream (e.g., via a `SignatureVerifiedBlock` /// /// Returns an error if the block is invalid, or if the block was unable to be verified. - #[instrument(skip_all, level = "debug")] + #[instrument(skip_all, level = "debug", fields(?block_root))] pub fn from_signature_verified_components( block: MaybeAvailableBlock<T::EthSpec>, block_root: Hash256, @@ -1416,27 +1426,10 @@ impl<T: BeaconChainTypes> ExecutionPendingBlock<T> { &parent.pre_state, notify_execution_layer, )?; - let is_valid_merge_transition_block = - is_merge_transition_block(&parent.pre_state, block.message().body()); - let payload_verification_future = async move { let chain = payload_notifier.chain.clone(); let block = payload_notifier.block.clone(); - // If this block triggers the merge, check to ensure that it references valid execution - // blocks. - // - // The specification defines this check inside `on_block` in the fork-choice specification, - // however we perform the check here for two reasons: - // - // - There's no point in importing a block that will fail fork choice, so it's best to fail - // early. - // - Doing the check here means we can keep our fork-choice implementation "pure". I.e., no - // calls to remote servers. - if is_valid_merge_transition_block { - validate_merge_block(&chain, block.message(), AllowOptimisticImport::Yes).await?; - }; - // The specification declares that this should be run *inside* `per_block_processing`, // however we run it here to keep `per_block_processing` pure (i.e., no calls to external // servers). @@ -1451,7 +1444,6 @@ impl<T: BeaconChainTypes> ExecutionPendingBlock<T> { Ok(PayloadVerificationOutcome { payload_verification_status, - is_valid_merge_transition_block, }) }; // Spawn the payload verification future as a new task, but don't wait for it to complete. @@ -1584,24 +1576,6 @@ impl<T: BeaconChainTypes> ExecutionPendingBlock<T> { metrics::stop_timer(committee_timer); - /* - * If we have block reward listeners, compute the block reward and push it to the - * event handler. - */ - if let Some(ref event_handler) = chain.event_handler - && event_handler.has_block_reward_subscribers() - { - let mut reward_cache = Default::default(); - let block_reward = chain.compute_block_reward( - block.message(), - block_root, - &state, - &mut reward_cache, - true, - )?; - event_handler.register(EventKind::BlockReward(block_reward)); - } - /* * Perform `per_block_processing` on the block and state, returning early if the block is * invalid. @@ -1801,10 +1775,12 @@ pub fn check_block_relevancy<T: BeaconChainTypes>( ) -> Result<Hash256, BlockError> { let block = signed_block.message(); + let present_slot = chain.slot()?; + // Do not process blocks from the future. - if block.slot() > chain.slot()? { + if block.slot() > present_slot { return Err(BlockError::FutureSlot { - present_slot: chain.slot()?, + present_slot, block_slot: block.slot(), }); } diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index 6a028e6c98..f98cd40d08 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -287,21 +287,6 @@ pub struct BlockImportData<E: EthSpec> { pub consensus_context: ConsensusContext<E>, } -impl<E: EthSpec> BlockImportData<E> { - pub fn __new_for_test( - block_root: Hash256, - state: BeaconState<E>, - parent_block: SignedBeaconBlock<E, BlindedPayload<E>>, - ) -> Self { - Self { - block_root, - state, - parent_block, - consensus_context: ConsensusContext::new(Slot::new(0)), - } - } -} - /// Trait for common block operations. pub trait AsBlock<E: EthSpec> { fn slot(&self) -> Slot; diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 59004367fa..e9dba6cfd6 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -9,7 +9,6 @@ use crate::data_availability_checker::DataAvailabilityChecker; use crate::data_availability_checker_v2::DataAvailabilityChecker as DataAvailabilityCheckerV2; use crate::data_availability_router::DataAvailabilityRouter; use crate::fork_choice_signal::ForkChoiceSignalTx; -use crate::fork_revert::{reset_fork_choice_to_finalization, revert_to_fork_boundary}; use crate::graffiti_calculator::{GraffitiCalculator, GraffitiOrigin}; use crate::kzg_utils::{build_data_column_sidecars_fulu, build_data_column_sidecars_gloas}; use crate::light_client_server_cache::LightClientServerCache; @@ -43,7 +42,7 @@ use std::sync::Arc; use std::time::Duration; use store::{Error as StoreError, HotColdDB, ItemStore, KeyValueStoreOp}; use task_executor::{ShutdownReason, TaskExecutor}; -use tracing::{debug, error, info}; +use tracing::{debug, error, info, warn}; use tree_hash::TreeHash; use types::data::CustodyIndex; use types::{ @@ -374,8 +373,8 @@ where // Initialize anchor info before attempting to write the genesis state. // Since v4.4.0 we will set the anchor with a dummy state upper limit in order to prevent - // historic states from being retained (unless `--reconstruct-historic-states` is set). - let retain_historic_states = self.chain_config.reconstruct_historic_states; + // historic states from being retained (unless `--archive` is set). + let retain_historic_states = self.chain_config.archive; let genesis_beacon_block = genesis_block(&mut beacon_state, &self.spec)?; self.pending_io_batch.push( store @@ -531,7 +530,7 @@ where // case it will be stored in the hot DB. In this case, we need to ensure the store's anchor // is initialised prior to storing the state, as the anchor is required for working out // hdiff storage strategies. - let retain_historic_states = self.chain_config.reconstruct_historic_states; + let retain_historic_states = self.chain_config.archive; self.pending_io_batch.push( store .init_anchor_info( @@ -780,49 +779,17 @@ where .get_head(current_slot, &self.spec) .map_err(|e| format!("Unable to get fork choice head: {:?}", e))?; - // Try to decode the head block according to the current fork, if that fails, try - // to backtrack to before the most recent fork. - let (head_block_root, head_block, head_reverted) = - match store.get_full_block(&initial_head_block_root) { - Ok(Some(block)) => (initial_head_block_root, block, false), - Ok(None) => return Err("Head block not found in store".into()), - Err(StoreError::SszDecodeError(_)) => { - error!( - message = "This node has likely missed a hard fork. \ - It will try to revert the invalid blocks and keep running, \ - but any stray blocks and states will not be deleted. \ - Long-term you should consider re-syncing this node.", - "Error decoding head block" - ); - let (block_root, block) = revert_to_fork_boundary( - current_slot, - initial_head_block_root, - store.clone(), - &self.spec, - )?; - - (block_root, block, true) - } - Err(e) => return Err(descriptive_db_error("head block", &e)), - }; + let head_block_root = initial_head_block_root; + let head_block = store + .get_full_block(&initial_head_block_root) + .map_err(|e| descriptive_db_error("head block", &e))? + .ok_or("Head block not found in store")?; let (_head_state_root, head_state) = store .get_advanced_hot_state(head_block_root, current_slot, head_block.state_root()) .map_err(|e| descriptive_db_error("head state", &e))? .ok_or("Head state not found in store")?; - // If the head reverted then we need to reset fork choice using the new head's finalized - // checkpoint. - if head_reverted { - fork_choice = reset_fork_choice_to_finalization( - head_block_root, - &head_state, - store.clone(), - Some(current_slot), - &self.spec, - )?; - } - let head_shuffling_ids = BlockShufflingIds::try_from_head(head_block_root, &head_state)?; let mut head_snapshot = BeaconSnapshot { @@ -850,6 +817,33 @@ where )); } + // Check if the head snapshot is within the weak subjectivity period + let head_state = &head_snapshot.beacon_state; + let Ok(ws_period) = head_state.compute_weak_subjectivity_period(&self.spec) else { + return Err(format!( + "Unable to compute the weak subjectivity period at the head snapshot slot: {:?}", + head_state.slot() + )); + }; + if current_slot.epoch(E::slots_per_epoch()) + > head_state.slot().epoch(E::slots_per_epoch()) + ws_period + { + if self.chain_config.ignore_ws_check { + warn!( + head_slot=%head_state.slot(), + %current_slot, + "The current head state is outside the weak subjectivity period. You are currently running a node that is susceptible to long range attacks. \ + It is highly recommended to purge your db and checkpoint sync. For more information please \ + read this blog post: https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak-subjectivity" + ) + } + return Err( + "The current head state is outside the weak subjectivity period. A node in this state is susceptible to long range attacks. You should purge your db and \ + checkpoint sync. For more information please read this blog post: https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak-subjectivity \ + If you understand the risks, it is possible to ignore this error with the --ignore-ws-check flag.".to_string() + ); + } + let validator_pubkey_cache = self .validator_pubkey_cache .map(|mut validator_pubkey_cache| { @@ -1039,6 +1033,7 @@ where observed_column_sidecars: RwLock::new(ObservedDataSidecars::new(self.spec.clone())), observed_blob_sidecars: RwLock::new(ObservedDataSidecars::new(self.spec.clone())), observed_slashable: <_>::default(), + pending_payload_envelopes: <_>::default(), observed_voluntary_exits: <_>::default(), observed_proposer_slashings: <_>::default(), observed_attester_slashings: <_>::default(), @@ -1058,6 +1053,7 @@ where )), beacon_proposer_cache, block_times_cache: <_>::default(), + envelope_times_cache: <_>::default(), pre_finalization_block_cache: <_>::default(), validator_pubkey_cache: RwLock::new(validator_pubkey_cache), early_attester_cache: <_>::default(), @@ -1109,6 +1105,11 @@ where let cgc_change_effective_slot = cgc_changed.effective_epoch.start_slot(E::slots_per_epoch()); beacon_chain.update_data_column_custody_info(Some(cgc_change_effective_slot)); + + // Persist change to disk. + beacon_chain + .persist_custody_context() + .map_err(|e| format!("Failed writing updated CGC: {e:?}"))?; } info!( @@ -1119,9 +1120,7 @@ where ); // Check for states to reconstruct (in the background). - if beacon_chain.config.reconstruct_historic_states - && beacon_chain.store.get_oldest_block_slot() == 0 - { + if beacon_chain.config.archive && beacon_chain.store.get_oldest_block_slot() == 0 { beacon_chain.store_migrator.process_reconstruction(); } diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index 711ffdc99c..e9cc4f24e9 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -38,7 +38,7 @@ pub struct ChainConfig { /// If `None`, there is no weak subjectivity verification. pub weak_subjectivity_checkpoint: Option<Checkpoint>, /// Determine whether to reconstruct historic states, usually after a checkpoint sync. - pub reconstruct_historic_states: bool, + pub archive: bool, /// The max size of a message that can be sent over the network. pub max_network_size: usize, /// Maximum percentage of the head committee weight at which to attempt re-orging the canonical head. @@ -117,6 +117,8 @@ pub struct ChainConfig { /// On Holesky there is a block which is added to this set by default but which can be removed /// by using `--invalid-block-roots ""`. pub invalid_block_roots: HashSet<Hash256>, + /// When set to true, the beacon node can be started even if the head state is outside the weak subjectivity period. + pub ignore_ws_check: bool, /// Disable the getBlobs optimisation to fetch blobs from the EL mempool. pub disable_get_blobs: bool, /// The node's custody type, determining how many data columns to custody and sample. @@ -128,7 +130,7 @@ impl Default for ChainConfig { Self { import_max_skip_slots: None, weak_subjectivity_checkpoint: None, - reconstruct_historic_states: false, + archive: false, max_network_size: 10 * 1_048_576, // 10M re_org_head_threshold: Some(DEFAULT_RE_ORG_HEAD_THRESHOLD), re_org_parent_threshold: Some(DEFAULT_RE_ORG_PARENT_THRESHOLD), @@ -160,6 +162,7 @@ impl Default for ChainConfig { block_publishing_delay: None, data_column_publishing_delay: None, invalid_block_roots: HashSet::new(), + ignore_ws_check: false, disable_get_blobs: false, node_custody_type: NodeCustodyType::Fullnode, } diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index f7bd646f82..c0403595ee 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -698,6 +698,8 @@ impl<T: BeaconChainTypes> DataAvailabilityCheckerInner<T> { pub fn remove_pre_execution_block(&self, block_root: &Hash256) { // The read lock is immediately dropped so we can safely remove the block from the cache. if let Some(BlockProcessStatus::NotValidated(_, _)) = self.get_cached_block(block_root) { + // If the block is execution invalid, this status is permanent and idempotent to this + // block_root. We drop its components (e.g. columns) because they will never be useful. self.critical.write().pop(block_root); } } @@ -789,8 +791,8 @@ mod test { use store::{HotColdDB, ItemStore, StoreConfig, database::interface::BeaconNodeBackend}; use tempfile::{TempDir, tempdir}; use tracing::info; + use types::MinimalEthSpec; use types::new_non_zero_usize; - use types::{ExecPayload, MinimalEthSpec}; const LOW_VALIDATOR_COUNT: usize = 32; @@ -818,9 +820,8 @@ mod test { async fn get_deneb_chain<E: EthSpec>( db_path: &TempDir, ) -> BeaconChainHarness<DiskHarnessType<E>> { - let altair_fork_epoch = Epoch::new(1); - let bellatrix_fork_epoch = Epoch::new(2); - let bellatrix_fork_slot = bellatrix_fork_epoch.start_slot(E::slots_per_epoch()); + let altair_fork_epoch = Epoch::new(0); + let bellatrix_fork_epoch = Epoch::new(0); let capella_fork_epoch = Epoch::new(3); let deneb_fork_epoch = Epoch::new(4); let deneb_fork_slot = deneb_fork_epoch.start_slot(E::slots_per_epoch()); @@ -842,25 +843,6 @@ mod test { .mock_execution_layer() .build(); - // go to bellatrix slot - harness.extend_to_slot(bellatrix_fork_slot).await; - let bellatrix_head = &harness.chain.head_snapshot().beacon_block; - assert!(bellatrix_head.as_bellatrix().is_ok()); - assert_eq!(bellatrix_head.slot(), bellatrix_fork_slot); - assert!( - bellatrix_head - .message() - .body() - .execution_payload() - .unwrap() - .is_default_with_empty_roots(), - "Bellatrix head is default payload" - ); - // Trigger the terminal PoW block. - harness - .execution_block_generator() - .move_to_terminal_block() - .unwrap(); // go right before deneb slot harness.extend_to_slot(deneb_fork_slot - 1).await; @@ -940,7 +922,6 @@ mod test { let payload_verification_outcome = PayloadVerificationOutcome { payload_verification_status: PayloadVerificationStatus::Verified, - is_valid_merge_transition_block: false, }; let availability_pending_block = AvailabilityPendingExecutedBlock { @@ -1181,7 +1162,6 @@ mod pending_components_tests { }, payload_verification_outcome: PayloadVerificationOutcome { payload_verification_status: PayloadVerificationStatus::Verified, - is_valid_merge_transition_block: false, }, }; (block, blobs, invalid_blobs) diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index 20952552ef..5d22aee50c 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -498,7 +498,6 @@ where Ok(()) } -// TODO(gloas) make sure the gloas variant uses the same span name #[instrument( skip_all, name = "validate_data_column_sidecar_for_gossip", diff --git a/beacon_node/beacon_chain/src/early_attester_cache.rs b/beacon_node/beacon_chain/src/early_attester_cache.rs index 8d9eb950f3..752e4d1a96 100644 --- a/beacon_node/beacon_chain/src/early_attester_cache.rs +++ b/beacon_node/beacon_chain/src/early_attester_cache.rs @@ -2,6 +2,7 @@ use crate::data_availability_checker::{AvailableBlock, AvailableBlockData}; use crate::{BeaconChainError as Error, metrics}; use parking_lot::RwLock; use proto_array::Block as ProtoBlock; +use safe_arith::SafeArith; use std::sync::Arc; use tracing::instrument; use types::*; @@ -59,12 +60,13 @@ impl CommitteeLengths { slots_per_epoch, committees_per_slot, committee_index as usize, - ); + )?; + let epoch_committee_count = committees_per_slot.safe_mul(slots_per_epoch)?; let range = compute_committee_range_in_epoch( - epoch_committee_count(committees_per_slot, slots_per_epoch), + epoch_committee_count, index_in_epoch, self.active_validator_indices_len, - ) + )? .ok_or(Error::EarlyAttesterCacheError)?; range diff --git a/beacon_node/beacon_chain/src/envelope_times_cache.rs b/beacon_node/beacon_chain/src/envelope_times_cache.rs new file mode 100644 index 0000000000..84c936c210 --- /dev/null +++ b/beacon_node/beacon_chain/src/envelope_times_cache.rs @@ -0,0 +1,197 @@ +//! This module provides the `EnvelopeTimesCache` which contains information regarding payload +//! envelope timings. +//! +//! This provides `BeaconChain` and associated functions with access to the timestamps of when a +//! payload envelope was observed, verified, executed, and imported. +//! This allows for better traceability and allows us to determine the root cause for why an +//! envelope was imported late. +//! This allows us to distinguish between the following scenarios: +//! - The envelope was observed late. +//! - Consensus verification was slow. +//! - Execution verification was slow. +//! - The DB write was slow. + +use eth2::types::{Hash256, Slot}; +use std::collections::HashMap; +use std::time::Duration; + +type BlockRoot = Hash256; + +#[derive(Clone, Default)] +pub struct EnvelopeTimestamps { + /// When the envelope was first observed (gossip or RPC). + pub observed: Option<Duration>, + /// When consensus verification (state transition) completed. + pub consensus_verified: Option<Duration>, + /// When execution layer verification started. + pub started_execution: Option<Duration>, + /// When execution layer verification completed. + pub executed: Option<Duration>, + /// When the envelope was imported into the DB. + pub imported: Option<Duration>, +} + +/// Delay data for envelope processing, computed relative to the slot start time. +#[derive(Debug, Default)] +pub struct EnvelopeDelays { + /// Time after start of slot we saw the envelope. + pub observed: Option<Duration>, + /// The time it took to complete consensus verification of the envelope. + pub consensus_verification_time: Option<Duration>, + /// The time it took to complete execution verification of the envelope. + pub execution_time: Option<Duration>, + /// Time after execution until the envelope was imported. + pub imported: Option<Duration>, +} + +impl EnvelopeDelays { + fn new(times: EnvelopeTimestamps, slot_start_time: Duration) -> EnvelopeDelays { + let observed = times + .observed + .and_then(|observed_time| observed_time.checked_sub(slot_start_time)); + let consensus_verification_time = times + .consensus_verified + .and_then(|consensus_verified| consensus_verified.checked_sub(times.observed?)); + let execution_time = times + .executed + .and_then(|executed| executed.checked_sub(times.started_execution?)); + let imported = times + .imported + .and_then(|imported_time| imported_time.checked_sub(times.executed?)); + EnvelopeDelays { + observed, + consensus_verification_time, + execution_time, + imported, + } + } +} + +pub struct EnvelopeTimesCacheValue { + pub slot: Slot, + pub timestamps: EnvelopeTimestamps, + pub peer_id: Option<String>, +} + +impl EnvelopeTimesCacheValue { + fn new(slot: Slot) -> Self { + EnvelopeTimesCacheValue { + slot, + timestamps: Default::default(), + peer_id: None, + } + } +} + +#[derive(Default)] +pub struct EnvelopeTimesCache { + pub cache: HashMap<BlockRoot, EnvelopeTimesCacheValue>, +} + +impl EnvelopeTimesCache { + /// Set the observation time for `block_root` to `timestamp` if `timestamp` is less than + /// any previous timestamp at which this envelope was observed. + pub fn set_time_observed( + &mut self, + block_root: BlockRoot, + slot: Slot, + timestamp: Duration, + peer_id: Option<String>, + ) { + let entry = self + .cache + .entry(block_root) + .or_insert_with(|| EnvelopeTimesCacheValue::new(slot)); + match entry.timestamps.observed { + Some(existing) if existing <= timestamp => { + // Existing timestamp is earlier, do nothing. + } + _ => { + entry.timestamps.observed = Some(timestamp); + entry.peer_id = peer_id; + } + } + } + + /// Set the timestamp for `field` if that timestamp is less than any previously known value. + fn set_time_if_less( + &mut self, + block_root: BlockRoot, + slot: Slot, + field: impl Fn(&mut EnvelopeTimestamps) -> &mut Option<Duration>, + timestamp: Duration, + ) { + let entry = self + .cache + .entry(block_root) + .or_insert_with(|| EnvelopeTimesCacheValue::new(slot)); + let existing_timestamp = field(&mut entry.timestamps); + if existing_timestamp.is_none_or(|prev| timestamp < prev) { + *existing_timestamp = Some(timestamp); + } + } + + pub fn set_time_consensus_verified( + &mut self, + block_root: BlockRoot, + slot: Slot, + timestamp: Duration, + ) { + self.set_time_if_less( + block_root, + slot, + |timestamps| &mut timestamps.consensus_verified, + timestamp, + ) + } + + pub fn set_time_started_execution( + &mut self, + block_root: BlockRoot, + slot: Slot, + timestamp: Duration, + ) { + self.set_time_if_less( + block_root, + slot, + |timestamps| &mut timestamps.started_execution, + timestamp, + ) + } + + pub fn set_time_executed(&mut self, block_root: BlockRoot, slot: Slot, timestamp: Duration) { + self.set_time_if_less( + block_root, + slot, + |timestamps| &mut timestamps.executed, + timestamp, + ) + } + + pub fn set_time_imported(&mut self, block_root: BlockRoot, slot: Slot, timestamp: Duration) { + self.set_time_if_less( + block_root, + slot, + |timestamps| &mut timestamps.imported, + timestamp, + ) + } + + pub fn get_envelope_delays( + &self, + block_root: BlockRoot, + slot_start_time: Duration, + ) -> EnvelopeDelays { + if let Some(entry) = self.cache.get(&block_root) { + EnvelopeDelays::new(entry.timestamps.clone(), slot_start_time) + } else { + EnvelopeDelays::default() + } + } + + /// Prune the cache to only store the most recent 2 epochs. + pub fn prune(&mut self, current_slot: Slot) { + self.cache + .retain(|_, entry| entry.slot > current_slot.saturating_sub(64_u64)); + } +} diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 816e75fd24..6c8f0d2794 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -16,6 +16,7 @@ use milhouse::Error as MilhouseError; use operation_pool::OpPoolError; use safe_arith::ArithError; use ssz_types::Error as SszTypesError; +use state_processing::envelope_processing::EnvelopeProcessingError; use state_processing::{ BlockProcessingError, BlockReplayError, EpochProcessingError, SlotProcessingError, block_signature_verifier::Error as BlockSignatureVerifierError, @@ -318,8 +319,10 @@ pub enum BlockProductionError { FailedToBuildBlobSidecars(String), MissingExecutionRequests, SszTypesError(ssz_types::Error), + EnvelopeProcessingError(EnvelopeProcessingError), + BlsError(bls::Error), // TODO(gloas): Remove this once Gloas is implemented - GloasNotImplemented, + GloasNotImplemented(String), } easy_from_to!(BlockProcessingError, BlockProductionError); diff --git a/beacon_node/beacon_chain/src/events.rs b/beacon_node/beacon_chain/src/events.rs index 63be944eea..276edc3fe6 100644 --- a/beacon_node/beacon_chain/src/events.rs +++ b/beacon_node/beacon_chain/src/events.rs @@ -21,7 +21,6 @@ pub struct ServerSentEventHandler<E: EthSpec> { late_head: Sender<EventKind<E>>, light_client_finality_update_tx: Sender<EventKind<E>>, light_client_optimistic_update_tx: Sender<EventKind<E>>, - block_reward_tx: Sender<EventKind<E>>, proposer_slashing_tx: Sender<EventKind<E>>, attester_slashing_tx: Sender<EventKind<E>>, bls_to_execution_change_tx: Sender<EventKind<E>>, @@ -48,7 +47,6 @@ impl<E: EthSpec> ServerSentEventHandler<E> { let (late_head, _) = broadcast::channel(capacity); let (light_client_finality_update_tx, _) = broadcast::channel(capacity); let (light_client_optimistic_update_tx, _) = broadcast::channel(capacity); - let (block_reward_tx, _) = broadcast::channel(capacity); let (proposer_slashing_tx, _) = broadcast::channel(capacity); let (attester_slashing_tx, _) = broadcast::channel(capacity); let (bls_to_execution_change_tx, _) = broadcast::channel(capacity); @@ -69,7 +67,6 @@ impl<E: EthSpec> ServerSentEventHandler<E> { late_head, light_client_finality_update_tx, light_client_optimistic_update_tx, - block_reward_tx, proposer_slashing_tx, attester_slashing_tx, bls_to_execution_change_tx, @@ -142,10 +139,6 @@ impl<E: EthSpec> ServerSentEventHandler<E> { .light_client_optimistic_update_tx .send(kind) .map(|count| log_count("light client optimistic update", count)), - EventKind::BlockReward(_) => self - .block_reward_tx - .send(kind) - .map(|count| log_count("block reward", count)), EventKind::ProposerSlashing(_) => self .proposer_slashing_tx .send(kind) @@ -224,10 +217,6 @@ impl<E: EthSpec> ServerSentEventHandler<E> { self.light_client_optimistic_update_tx.subscribe() } - pub fn subscribe_block_reward(&self) -> Receiver<EventKind<E>> { - self.block_reward_tx.subscribe() - } - pub fn subscribe_attester_slashing(&self) -> Receiver<EventKind<E>> { self.attester_slashing_tx.subscribe() } @@ -292,10 +281,6 @@ impl<E: EthSpec> ServerSentEventHandler<E> { self.late_head.receiver_count() > 0 } - pub fn has_block_reward_subscribers(&self) -> bool { - self.block_reward_tx.receiver_count() > 0 - } - pub fn has_proposer_slashing_subscribers(&self) -> bool { self.proposer_slashing_tx.receiver_count() > 0 } diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index bdf3ab9594..2b03a095f1 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -12,32 +12,25 @@ use crate::{ ExecutionPayloadError, }; use execution_layer::{ - BlockProposalContents, BlockProposalContentsType, BuilderParams, NewPayloadRequest, - PayloadAttributes, PayloadParameters, PayloadStatus, + BlockProposalContentsType, BuilderParams, NewPayloadRequest, PayloadAttributes, + PayloadParameters, PayloadStatus, }; use fork_choice::{InvalidationOperation, PayloadVerificationStatus}; use proto_array::{Block as ProtoBlock, ExecutionStatus}; use slot_clock::SlotClock; use state_processing::per_block_processing::{ compute_timestamp_at_slot, get_expected_withdrawals, is_execution_enabled, - is_merge_transition_complete, partially_verify_execution_payload, + partially_verify_execution_payload, }; use std::sync::Arc; use tokio::task::JoinHandle; -use tracing::{Instrument, debug, debug_span, warn}; -use tree_hash::TreeHash; +use tracing::{Instrument, debug_span, warn}; use types::execution::BlockProductionVersion; use types::*; pub type PreparePayloadResult<E> = Result<BlockProposalContentsType<E>, BlockProductionError>; pub type PreparePayloadHandle<E> = JoinHandle<Option<PreparePayloadResult<E>>>; -#[derive(PartialEq)] -pub enum AllowOptimisticImport { - Yes, - No, -} - /// Signal whether the execution payloads of new blocks should be /// immediately verified with the EL or imported optimistically without /// any EL communication. @@ -62,7 +55,10 @@ impl<T: BeaconChainTypes> PayloadNotifier<T> { state: &BeaconState<T::EthSpec>, notify_execution_layer: NotifyExecutionLayer, ) -> Result<Self, BlockError> { - let payload_verification_status = if is_execution_enabled(state, block.message().body()) { + let payload_verification_status = if block.fork_name_unchecked().gloas_enabled() { + // Gloas blocks don't contain an execution payload. + Some(PayloadVerificationStatus::Irrelevant) + } else if is_execution_enabled(state, block.message().body()) { // Perform the initial stages of payload verification. // // We will duplicate these checks again during `per_block_processing`, however these @@ -112,12 +108,18 @@ impl<T: BeaconChainTypes> PayloadNotifier<T> { if let Some(precomputed_status) = self.payload_verification_status { Ok(precomputed_status) } else { - notify_new_payload(&self.chain, self.block.message()).await + notify_new_payload( + &self.chain, + self.block.message().slot(), + self.block.message().parent_root(), + self.block.message().try_into()?, + ) + .await } } } -/// Verify that `execution_payload` contained by `block` is considered valid by an execution +/// Verify that `execution_payload` is considered valid by an execution /// engine. /// /// ## Specification @@ -126,17 +128,21 @@ impl<T: BeaconChainTypes> PayloadNotifier<T> { /// contains a few extra checks by running `partially_verify_execution_payload` first: /// /// https://github.com/ethereum/consensus-specs/blob/v1.1.9/specs/bellatrix/beacon-chain.md#notify_new_payload -async fn notify_new_payload<T: BeaconChainTypes>( +pub async fn notify_new_payload<T: BeaconChainTypes>( chain: &Arc<BeaconChain<T>>, - block: BeaconBlockRef<'_, T::EthSpec>, + slot: Slot, + parent_beacon_block_root: Hash256, + new_payload_request: NewPayloadRequest<'_, T::EthSpec>, ) -> Result<PayloadVerificationStatus, BlockError> { let execution_layer = chain .execution_layer .as_ref() .ok_or(ExecutionPayloadError::NoExecutionConnection)?; - let execution_block_hash = block.execution_payload()?.block_hash(); - let new_payload_response = execution_layer.notify_new_payload(block.try_into()?).await; + let execution_block_hash = new_payload_request.execution_payload_ref().block_hash(); + let new_payload_response = execution_layer + .notify_new_payload(new_payload_request.clone()) + .await; match new_payload_response { Ok(status) => match status { @@ -152,10 +158,7 @@ async fn notify_new_payload<T: BeaconChainTypes>( ?validation_error, ?latest_valid_hash, ?execution_block_hash, - root = ?block.tree_hash_root(), - graffiti = block.body().graffiti().as_utf8_lossy(), - proposer_index = block.proposer_index(), - slot = %block.slot(), + %slot, method = "new_payload", "Invalid execution payload" ); @@ -178,11 +181,9 @@ async fn notify_new_payload<T: BeaconChainTypes>( { // This block has not yet been applied to fork choice, so the latest block that was // imported to fork choice was the parent. - let latest_root = block.parent_root(); - chain .process_invalid_execution_payload(&InvalidationOperation::InvalidateMany { - head_block_root: latest_root, + head_block_root: parent_beacon_block_root, always_invalidate_head: false, latest_valid_ancestor: latest_valid_hash, }) @@ -197,10 +198,7 @@ async fn notify_new_payload<T: BeaconChainTypes>( warn!( ?validation_error, ?execution_block_hash, - root = ?block.tree_hash_root(), - graffiti = block.body().graffiti().as_utf8_lossy(), - proposer_index = block.proposer_index(), - slot = %block.slot(), + %slot, method = "new_payload", "Invalid execution payload block hash" ); @@ -215,78 +213,6 @@ async fn notify_new_payload<T: BeaconChainTypes>( } } -/// Verify that the block which triggers the merge is valid to be imported to fork choice. -/// -/// ## Errors -/// -/// Will return an error when using a pre-merge fork `state`. Ensure to only run this function -/// after the merge fork. -/// -/// ## Specification -/// -/// Equivalent to the `validate_merge_block` function in the merge Fork Choice Changes: -/// -/// https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/merge/fork-choice.md#validate_merge_block -pub async fn validate_merge_block<T: BeaconChainTypes>( - chain: &Arc<BeaconChain<T>>, - block: BeaconBlockRef<'_, T::EthSpec>, - allow_optimistic_import: AllowOptimisticImport, -) -> Result<(), BlockError> { - let spec = &chain.spec; - let block_epoch = block.slot().epoch(T::EthSpec::slots_per_epoch()); - let execution_payload = block.execution_payload()?; - - if spec.terminal_block_hash != ExecutionBlockHash::zero() { - if block_epoch < spec.terminal_block_hash_activation_epoch { - return Err(ExecutionPayloadError::InvalidActivationEpoch { - activation_epoch: spec.terminal_block_hash_activation_epoch, - epoch: block_epoch, - } - .into()); - } - - if execution_payload.parent_hash() != spec.terminal_block_hash { - return Err(ExecutionPayloadError::InvalidTerminalBlockHash { - terminal_block_hash: spec.terminal_block_hash, - payload_parent_hash: execution_payload.parent_hash(), - } - .into()); - } - - return Ok(()); - } - - let execution_layer = chain - .execution_layer - .as_ref() - .ok_or(ExecutionPayloadError::NoExecutionConnection)?; - - let is_valid_terminal_pow_block = execution_layer - .is_valid_terminal_pow_block_hash(execution_payload.parent_hash(), spec) - .await - .map_err(ExecutionPayloadError::from)?; - - match is_valid_terminal_pow_block { - Some(true) => Ok(()), - Some(false) => Err(ExecutionPayloadError::InvalidTerminalPoWBlock { - parent_hash: execution_payload.parent_hash(), - } - .into()), - None => { - if allow_optimistic_import == AllowOptimisticImport::Yes { - debug!( - block_hash = ?execution_payload.parent_hash(), - msg = "the terminal block/parent was unavailable", - "Optimistically importing merge transition block" - ); - Ok(()) - } else { - Err(ExecutionPayloadError::UnverifiedNonOptimisticCandidate.into()) - } - } - } -} - /// Validate the gossip block's execution_payload according to the checks described here: /// https://github.com/ethereum/consensus-specs/blob/dev/specs/merge/p2p-interface.md#beacon_block pub fn validate_execution_payload_for_gossip<T: BeaconChainTypes>( @@ -294,16 +220,22 @@ pub fn validate_execution_payload_for_gossip<T: BeaconChainTypes>( block: BeaconBlockRef<'_, T::EthSpec>, chain: &BeaconChain<T>, ) -> Result<(), BlockError> { + // Gloas blocks don't have an execution payload in the block body. + // Bid-related validations are handled in gossip block verification. + if block.fork_name_unchecked().gloas_enabled() { + return Ok(()); + } + // Only apply this validation if this is a Bellatrix beacon block. if let Ok(execution_payload) = block.body().execution_payload() { - // This logic should match `is_execution_enabled`. We use only the execution block hash of - // the parent here in order to avoid loading the parent state during gossip verification. + // Check parent execution status to determine if we should validate the payload. + // We use only the execution status of the parent here to avoid loading the parent state + // during gossip verification. - let is_merge_transition_complete = match parent_block.execution_status { - // Optimistically declare that an "unknown" status block has completed the merge. + let parent_has_execution = match parent_block.execution_status { + // Parent has valid or optimistic execution status. ExecutionStatus::Valid(_) | ExecutionStatus::Optimistic(_) => true, - // It's impossible for an irrelevant block to have completed the merge. It is pre-merge - // by definition. + // Pre-merge blocks have irrelevant execution status. ExecutionStatus::Irrelevant(_) => false, // If the parent has an invalid payload then it's impossible to build a valid block upon // it. Reject the block. @@ -314,7 +246,7 @@ pub fn validate_execution_payload_for_gossip<T: BeaconChainTypes>( } }; - if is_merge_transition_complete || !execution_payload.is_default_with_empty_roots() { + if parent_has_execution || !execution_payload.is_default_with_empty_roots() { let expected_timestamp = chain .slot_clock .start_of(block.slot()) @@ -363,7 +295,6 @@ pub fn get_execution_payload<T: BeaconChainTypes>( // task. let spec = &chain.spec; let current_epoch = state.current_epoch(); - let is_merge_transition_complete = is_merge_transition_complete(state); let timestamp = compute_timestamp_at_slot(state, state.slot(), spec).map_err(BeaconStateError::from)?; let random = *state.get_randao_mix(current_epoch)?; @@ -390,7 +321,6 @@ pub fn get_execution_payload<T: BeaconChainTypes>( async move { prepare_execution_payload::<T>( &chain, - is_merge_transition_complete, timestamp, random, proposer_index, @@ -414,8 +344,6 @@ pub fn get_execution_payload<T: BeaconChainTypes>( /// Prepares an execution payload for inclusion in a block. /// -/// Will return `Ok(None)` if the Bellatrix fork has occurred, but a terminal block has not been found. -/// /// ## Errors /// /// Will return an error when using a pre-Bellatrix fork `state`. Ensure to only run this function @@ -429,7 +357,6 @@ pub fn get_execution_payload<T: BeaconChainTypes>( #[allow(clippy::too_many_arguments)] pub async fn prepare_execution_payload<T>( chain: &Arc<BeaconChain<T>>, - is_merge_transition_complete: bool, timestamp: u64, random: Hash256, proposer_index: u64, @@ -444,7 +371,6 @@ pub async fn prepare_execution_payload<T>( where T: BeaconChainTypes, { - let current_epoch = builder_params.slot.epoch(T::EthSpec::slots_per_epoch()); let spec = &chain.spec; let fork = spec.fork_name_at_slot::<T::EthSpec>(builder_params.slot); let execution_layer = chain @@ -452,42 +378,7 @@ where .as_ref() .ok_or(BlockProductionError::ExecutionLayerMissing)?; - let parent_hash = if !is_merge_transition_complete { - let is_terminal_block_hash_set = spec.terminal_block_hash != ExecutionBlockHash::zero(); - let is_activation_epoch_reached = - current_epoch >= spec.terminal_block_hash_activation_epoch; - - if is_terminal_block_hash_set && !is_activation_epoch_reached { - // Use the "empty" payload if there's a terminal block hash, but we haven't reached the - // terminal block epoch yet. - return Ok(BlockProposalContentsType::Full( - BlockProposalContents::Payload { - payload: FullPayload::default_at_fork(fork)?, - block_value: Uint256::ZERO, - }, - )); - } - - let terminal_pow_block_hash = execution_layer - .get_terminal_pow_block_hash(spec, timestamp) - .await - .map_err(BlockProductionError::TerminalPoWBlockLookupFailed)?; - - if let Some(terminal_pow_block_hash) = terminal_pow_block_hash { - terminal_pow_block_hash - } else { - // If the merge transition hasn't occurred yet and the EL hasn't found the terminal - // block, return an "empty" payload. - return Ok(BlockProposalContentsType::Full( - BlockProposalContents::Payload { - payload: FullPayload::default_at_fork(fork)?, - block_value: Uint256::ZERO, - }, - )); - } - } else { - latest_execution_payload_header_block_hash - }; + let parent_hash = latest_execution_payload_header_block_hash; // Try to obtain the fork choice update parameters from the cached head. // diff --git a/beacon_node/beacon_chain/src/fork_revert.rs b/beacon_node/beacon_chain/src/fork_revert.rs deleted file mode 100644 index 4db79790d3..0000000000 --- a/beacon_node/beacon_chain/src/fork_revert.rs +++ /dev/null @@ -1,204 +0,0 @@ -use crate::{BeaconForkChoiceStore, BeaconSnapshot}; -use fork_choice::{ForkChoice, PayloadVerificationStatus}; -use itertools::process_results; -use state_processing::state_advance::complete_state_advance; -use state_processing::{ - ConsensusContext, VerifyBlockRoot, per_block_processing, - per_block_processing::BlockSignatureStrategy, -}; -use std::sync::Arc; -use std::time::Duration; -use store::{HotColdDB, ItemStore, iter::ParentRootBlockIterator}; -use tracing::{info, warn}; -use types::{BeaconState, ChainSpec, EthSpec, ForkName, Hash256, SignedBeaconBlock, Slot}; - -const CORRUPT_DB_MESSAGE: &str = "The database could be corrupt. Check its file permissions or \ - consider deleting it by running with the --purge-db flag."; - -/// Revert the head to the last block before the most recent hard fork. -/// -/// This function is destructive and should only be used if there is no viable alternative. It will -/// cause the reverted blocks and states to be completely forgotten, lying dormant in the database -/// forever. -/// -/// Return the `(head_block_root, head_block)` that should be used post-reversion. -pub fn revert_to_fork_boundary<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>>( - current_slot: Slot, - head_block_root: Hash256, - store: Arc<HotColdDB<E, Hot, Cold>>, - spec: &ChainSpec, -) -> Result<(Hash256, SignedBeaconBlock<E>), String> { - let current_fork = spec.fork_name_at_slot::<E>(current_slot); - let fork_epoch = spec - .fork_epoch(current_fork) - .ok_or_else(|| format!("Current fork '{}' never activates", current_fork))?; - - if current_fork == ForkName::Base { - return Err(format!( - "Cannot revert to before phase0 hard fork. {}", - CORRUPT_DB_MESSAGE - )); - } - - warn!( - target_fork = %current_fork, - %fork_epoch, - "Reverting invalid head block" - ); - let block_iter = ParentRootBlockIterator::fork_tolerant(&store, head_block_root); - - let (block_root, blinded_block) = process_results(block_iter, |mut iter| { - iter.find_map(|(block_root, block)| { - if block.slot() < fork_epoch.start_slot(E::slots_per_epoch()) { - Some((block_root, block)) - } else { - info!( - ?block_root, - slot = %block.slot(), - "Reverting block" - ); - None - } - }) - }) - .map_err(|e| { - format!( - "Error fetching blocks to revert: {:?}. {}", - e, CORRUPT_DB_MESSAGE - ) - })? - .ok_or_else(|| format!("No pre-fork blocks found. {}", CORRUPT_DB_MESSAGE))?; - - let block = store - .make_full_block(&block_root, blinded_block) - .map_err(|e| format!("Unable to add payload to new head block: {:?}", e))?; - - Ok((block_root, block)) -} - -/// Reset fork choice to the finalized checkpoint of the supplied head state. -/// -/// The supplied `head_block_root` should correspond to the most recently applied block on -/// `head_state`. -/// -/// This function avoids quirks of fork choice initialization by replaying all of the blocks from -/// the checkpoint to the head. -/// -/// See this issue for details: https://github.com/ethereum/consensus-specs/issues/2566 -/// -/// It will fail if the finalized state or any of the blocks to replay are unavailable. -/// -/// WARNING: this function is destructive and causes fork choice to permanently forget all -/// chains other than the chain leading to `head_block_root`. It should only be used in extreme -/// circumstances when there is no better alternative. -pub fn reset_fork_choice_to_finalization<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>>( - head_block_root: Hash256, - head_state: &BeaconState<E>, - store: Arc<HotColdDB<E, Hot, Cold>>, - current_slot: Option<Slot>, - spec: &ChainSpec, -) -> Result<ForkChoice<BeaconForkChoiceStore<E, Hot, Cold>, E>, String> { - // Fetch finalized block. - let finalized_checkpoint = head_state.finalized_checkpoint(); - let finalized_block_root = finalized_checkpoint.root; - let finalized_block = store - .get_full_block(&finalized_block_root) - .map_err(|e| format!("Error loading finalized block: {:?}", e))? - .ok_or_else(|| { - format!( - "Finalized block missing for revert: {:?}", - finalized_block_root - ) - })?; - - // Advance finalized state to finalized epoch (to handle skipped slots). - let finalized_state_root = finalized_block.state_root(); - // The enshrined finalized state should be in the state cache. - let mut finalized_state = store - .get_state(&finalized_state_root, Some(finalized_block.slot()), true) - .map_err(|e| format!("Error loading finalized state: {:?}", e))? - .ok_or_else(|| { - format!( - "Finalized block state missing from database: {:?}", - finalized_state_root - ) - })?; - let finalized_slot = finalized_checkpoint.epoch.start_slot(E::slots_per_epoch()); - complete_state_advance( - &mut finalized_state, - Some(finalized_state_root), - finalized_slot, - spec, - ) - .map_err(|e| { - format!( - "Error advancing finalized state to finalized epoch: {:?}", - e - ) - })?; - let finalized_snapshot = BeaconSnapshot { - beacon_block_root: finalized_block_root, - beacon_block: Arc::new(finalized_block), - beacon_state: finalized_state, - }; - - let fc_store = - BeaconForkChoiceStore::get_forkchoice_store(store.clone(), finalized_snapshot.clone()) - .map_err(|e| format!("Unable to reset fork choice store for revert: {e:?}"))?; - - let mut fork_choice = ForkChoice::from_anchor( - fc_store, - finalized_block_root, - &finalized_snapshot.beacon_block, - &finalized_snapshot.beacon_state, - current_slot, - spec, - ) - .map_err(|e| format!("Unable to reset fork choice for revert: {:?}", e))?; - - // Replay blocks from finalized checkpoint back to head. - // We do not replay attestations presently, relying on the absence of other blocks - // to guarantee `head_block_root` as the head. - let blocks = store - .load_blocks_to_replay(finalized_slot + 1, head_state.slot(), head_block_root) - .map_err(|e| format!("Error loading blocks to replay for fork choice: {:?}", e))?; - - let mut state = finalized_snapshot.beacon_state; - for block in blocks { - complete_state_advance(&mut state, None, block.slot(), spec) - .map_err(|e| format!("State advance failed: {:?}", e))?; - - let mut ctxt = ConsensusContext::new(block.slot()) - .set_proposer_index(block.message().proposer_index()); - per_block_processing( - &mut state, - &block, - BlockSignatureStrategy::NoVerification, - VerifyBlockRoot::True, - &mut ctxt, - spec, - ) - .map_err(|e| format!("Error replaying block: {:?}", e))?; - - // Setting this to unverified is the safest solution, since we don't have a way to - // retro-actively determine if they were valid or not. - // - // This scenario is so rare that it seems OK to double-verify some blocks. - let payload_verification_status = PayloadVerificationStatus::Optimistic; - - fork_choice - .on_block( - block.slot(), - block.message(), - block.canonical_root(), - // Reward proposer boost. We are reinforcing the canonical chain. - Duration::from_secs(0), - &state, - payload_verification_status, - spec, - ) - .map_err(|e| format!("Error applying replayed block to fork choice: {:?}", e))?; - } - - Ok(fork_choice) -} diff --git a/beacon_node/beacon_chain/src/historical_blocks.rs b/beacon_node/beacon_chain/src/historical_blocks.rs index 45ae9d7b84..bfda52558e 100644 --- a/beacon_node/beacon_chain/src/historical_blocks.rs +++ b/beacon_node/beacon_chain/src/historical_blocks.rs @@ -12,7 +12,7 @@ use std::time::Duration; use store::metadata::DataColumnInfo; use store::{AnchorInfo, BlobInfo, DBColumn, Error as StoreError, KeyValueStore, KeyValueStoreOp}; use strum::IntoStaticStr; -use tracing::{debug, instrument}; +use tracing::{debug, debug_span, instrument}; use types::{Hash256, Slot}; /// Use a longer timeout on the pubkey cache. @@ -165,13 +165,8 @@ impl<T: BeaconChainTypes> BeaconChain<T> { } // Store the blobs or data columns too - if let Some(op) = self - .get_blobs_or_columns_store_op(block_root, block.slot(), block_data) - .map_err(|e| { - HistoricalBlockError::StoreError(StoreError::DBError { - message: format!("get_blobs_or_columns_store_op error {e:?}"), - }) - })? + if let Some(op) = + self.get_blobs_or_columns_store_op(block_root, block.slot(), block_data) { blob_batch.extend(self.store.convert_to_kv_batch(vec![op])?); } @@ -256,9 +251,18 @@ impl<T: BeaconChainTypes> BeaconChain<T> { // Write the I/O batches to disk, writing the blocks themselves first, as it's better // for the hot DB to contain extra blocks than for the cold DB to point to blocks that // do not exist. - self.store.blobs_db.do_atomically(blob_batch)?; - self.store.hot_db.do_atomically(hot_batch)?; - self.store.cold_db.do_atomically(cold_batch)?; + { + let _span = debug_span!("backfill_write_blobs_db").entered(); + self.store.blobs_db.do_atomically(blob_batch)?; + } + { + let _span = debug_span!("backfill_write_hot_db").entered(); + self.store.hot_db.do_atomically(hot_batch)?; + } + { + let _span = debug_span!("backfill_write_cold_db").entered(); + self.store.cold_db.do_atomically(cold_batch)?; + } let mut anchor_and_blob_batch = Vec::with_capacity(3); @@ -305,10 +309,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> { // If backfill has completed and the chain is configured to reconstruct historic states, // send a message to the background migrator instructing it to begin reconstruction. // This can only happen if we have backfilled all the way to genesis. - if backfill_complete - && self.genesis_backfill_slot == Slot::new(0) - && self.config.reconstruct_historic_states - { + if backfill_complete && self.genesis_backfill_slot == Slot::new(0) && self.config.archive { self.store_migrator.process_reconstruction(); } diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 2c7746c896..82b47d0c79 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -9,7 +9,7 @@ pub mod beacon_proposer_cache; mod beacon_snapshot; pub mod bellatrix_readiness; pub mod blob_verification; -pub mod block_reward; +mod block_production; mod block_times_cache; mod block_verification; pub mod block_verification_types; @@ -22,12 +22,12 @@ pub mod data_availability_checker_v2; pub mod data_availability_router; pub mod data_column_verification; mod early_attester_cache; +pub mod envelope_times_cache; mod errors; pub mod events; pub mod execution_payload; pub mod fetch_blobs; pub mod fork_choice_signal; -pub mod fork_revert; pub mod graffiti_calculator; pub mod historical_blocks; pub mod historical_data_columns; @@ -44,6 +44,8 @@ pub mod observed_block_producers; pub mod observed_data_sidecars; pub mod observed_operations; mod observed_slashable; +pub mod payload_envelope_verification; +pub mod pending_payload_envelopes; pub mod persisted_beacon_chain; pub mod persisted_custody; mod persisted_fork_choice; diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index f8044b9175..89421637a9 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -21,6 +21,34 @@ pub const VALIDATOR_MONITOR_ATTESTATION_SIMULATOR_SOURCE_ATTESTER_HIT_TOTAL: &st pub const VALIDATOR_MONITOR_ATTESTATION_SIMULATOR_SOURCE_ATTESTER_MISS_TOTAL: &str = "validator_monitor_attestation_simulator_source_attester_miss_total"; +/* +* Execution Payload Envelope Processing +*/ + +pub static ENVELOPE_PROCESSING_REQUESTS: LazyLock<Result<IntCounter>> = LazyLock::new(|| { + try_create_int_counter( + "payload_envelope_processing_requests_total", + "Count of payload envelopes submitted for processing", + ) +}); +pub static ENVELOPE_PROCESSING_SUCCESSES: LazyLock<Result<IntCounter>> = LazyLock::new(|| { + try_create_int_counter( + "payload_envelope_processing_successes_total", + "Count of payload envelopes processed without error", + ) +}); +pub static ENVELOPE_PROCESSING_TIMES: LazyLock<Result<Histogram>> = LazyLock::new(|| { + try_create_histogram( + "payload_envelope_processing_seconds", + "Full runtime of payload envelope processing", + ) +}); +pub static ENVELOPE_PROCESSING_DB_WRITE: LazyLock<Result<Histogram>> = LazyLock::new(|| { + try_create_histogram( + "payload_envelope_processing_db_write_seconds", + "Time spent writing a newly processed payload envelope and state to DB", + ) +}); /* * Block Processing */ diff --git a/beacon_node/beacon_chain/src/otb_verification_service.rs b/beacon_node/beacon_chain/src/otb_verification_service.rs deleted file mode 100644 index e02705f5da..0000000000 --- a/beacon_node/beacon_chain/src/otb_verification_service.rs +++ /dev/null @@ -1,369 +0,0 @@ -use crate::execution_payload::{validate_merge_block, AllowOptimisticImport}; -use crate::{ - BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, ExecutionPayloadError, - INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON, -}; -use itertools::process_results; -use logging::crit; -use proto_array::InvalidationOperation; -use slot_clock::SlotClock; -use ssz::{Decode, Encode}; -use ssz_derive::{Decode, Encode}; -use state_processing::per_block_processing::is_merge_transition_complete; -use std::sync::Arc; -use store::{DBColumn, Error as StoreError, HotColdDB, KeyValueStore, StoreItem}; -use task_executor::{ShutdownReason, TaskExecutor}; -use tokio::time::sleep; -use tracing::{debug, error, info, warn}; -use tree_hash::TreeHash; -use types::{BeaconBlockRef, EthSpec, Hash256, Slot}; -use DBColumn::OptimisticTransitionBlock as OTBColumn; - -#[derive(Clone, Debug, Decode, Encode, PartialEq)] -pub struct OptimisticTransitionBlock { - root: Hash256, - slot: Slot, -} - -impl OptimisticTransitionBlock { - // types::BeaconBlockRef<'_, <T as BeaconChainTypes>::EthSpec> - pub fn from_block<E: EthSpec>(block: BeaconBlockRef<E>) -> Self { - Self { - root: block.tree_hash_root(), - slot: block.slot(), - } - } - - pub fn root(&self) -> &Hash256 { - &self.root - } - - pub fn slot(&self) -> &Slot { - &self.slot - } - - pub fn persist_in_store<T, A>(&self, store: A) -> Result<(), StoreError> - where - T: BeaconChainTypes, - A: AsRef<HotColdDB<T::EthSpec, T::HotStore, T::ColdStore>>, - { - if store - .as_ref() - .item_exists::<OptimisticTransitionBlock>(&self.root)? - { - Ok(()) - } else { - store.as_ref().put_item(&self.root, self) - } - } - - pub fn remove_from_store<T, A>(&self, store: A) -> Result<(), StoreError> - where - T: BeaconChainTypes, - A: AsRef<HotColdDB<T::EthSpec, T::HotStore, T::ColdStore>>, - { - store - .as_ref() - .hot_db - .key_delete(OTBColumn.into(), self.root.as_slice()) - } - - fn is_canonical<T: BeaconChainTypes>( - &self, - chain: &BeaconChain<T>, - ) -> Result<bool, BeaconChainError> { - Ok(chain - .forwards_iter_block_roots_until(self.slot, self.slot)? - .next() - .transpose()? - .map(|(root, _)| root) - == Some(self.root)) - } -} - -impl StoreItem for OptimisticTransitionBlock { - fn db_column() -> DBColumn { - OTBColumn - } - - fn as_store_bytes(&self) -> Vec<u8> { - self.as_ssz_bytes() - } - - fn from_store_bytes(bytes: &[u8]) -> Result<Self, StoreError> { - Ok(Self::from_ssz_bytes(bytes)?) - } -} - -/// The routine is expected to run once per epoch, 1/4th through the epoch. -pub const EPOCH_DELAY_FACTOR: u32 = 4; - -/// Spawns a routine which checks the validity of any optimistically imported transition blocks -/// -/// This routine will run once per epoch, at `epoch_duration / EPOCH_DELAY_FACTOR` after -/// the start of each epoch. -/// -/// The service will not be started if there is no `execution_layer` on the `chain`. -pub fn start_otb_verification_service<T: BeaconChainTypes>( - executor: TaskExecutor, - chain: Arc<BeaconChain<T>>, -) { - // Avoid spawning the service if there's no EL, it'll just error anyway. - if chain.execution_layer.is_some() { - executor.spawn( - async move { otb_verification_service(chain).await }, - "otb_verification_service", - ); - } -} - -pub fn load_optimistic_transition_blocks<T: BeaconChainTypes>( - chain: &BeaconChain<T>, -) -> Result<Vec<OptimisticTransitionBlock>, StoreError> { - process_results( - chain.store.hot_db.iter_column::<Hash256>(OTBColumn), - |iter| { - iter.map(|(_, bytes)| OptimisticTransitionBlock::from_store_bytes(&bytes)) - .collect() - }, - )? -} - -#[derive(Debug)] -pub enum Error { - ForkChoice(String), - BeaconChain(BeaconChainError), - StoreError(StoreError), - NoBlockFound(OptimisticTransitionBlock), -} - -pub async fn validate_optimistic_transition_blocks<T: BeaconChainTypes>( - chain: &Arc<BeaconChain<T>>, - otbs: Vec<OptimisticTransitionBlock>, -) -> Result<(), Error> { - let finalized_slot = chain - .canonical_head - .fork_choice_read_lock() - .get_finalized_block() - .map_err(|e| Error::ForkChoice(format!("{:?}", e)))? - .slot; - - // separate otbs into - // non-canonical - // finalized canonical - // unfinalized canonical - let mut non_canonical_otbs = vec![]; - let (finalized_canonical_otbs, unfinalized_canonical_otbs) = process_results( - otbs.into_iter().map(|otb| { - otb.is_canonical(chain) - .map(|is_canonical| (otb, is_canonical)) - }), - |pair_iter| { - pair_iter - .filter_map(|(otb, is_canonical)| { - if is_canonical { - Some(otb) - } else { - non_canonical_otbs.push(otb); - None - } - }) - .partition::<Vec<_>, _>(|otb| *otb.slot() <= finalized_slot) - }, - ) - .map_err(Error::BeaconChain)?; - - // remove non-canonical blocks that conflict with finalized checkpoint from the database - for otb in non_canonical_otbs { - if *otb.slot() <= finalized_slot { - otb.remove_from_store::<T, _>(&chain.store) - .map_err(Error::StoreError)?; - } - } - - // ensure finalized canonical otb are valid, otherwise kill client - for otb in finalized_canonical_otbs { - match chain.get_block(otb.root()).await { - Ok(Some(block)) => { - match validate_merge_block(chain, block.message(), AllowOptimisticImport::No).await - { - Ok(()) => { - // merge transition block is valid, remove it from OTB - otb.remove_from_store::<T, _>(&chain.store) - .map_err(Error::StoreError)?; - info!( - block_root = %otb.root(), - "type" = "finalized", - "Validated merge transition block" - ); - } - // The block was not able to be verified by the EL. Leave the OTB in the - // database since the EL is likely still syncing and may verify the block - // later. - Err(BlockError::ExecutionPayloadError( - ExecutionPayloadError::UnverifiedNonOptimisticCandidate, - )) => (), - Err(BlockError::ExecutionPayloadError( - ExecutionPayloadError::InvalidTerminalPoWBlock { .. }, - )) => { - // Finalized Merge Transition Block is Invalid! Kill the Client! - crit!( - msg = "You must use the `--purge-db` flag to clear the database and restart sync. \ - You may be on a hostile network.", - block_hash = ?block.canonical_root(), - "Finalized merge transition block is invalid!" - ); - let mut shutdown_sender = chain.shutdown_sender(); - if let Err(e) = shutdown_sender.try_send(ShutdownReason::Failure( - INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON, - )) { - crit!( - error = ?e, - shutdown_reason = INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON, - "Failed to shut down client" - ); - } - } - _ => {} - } - } - Ok(None) => return Err(Error::NoBlockFound(otb)), - // Our database has pruned the payload and the payload was unavailable on the EL since - // the EL is still syncing or the payload is non-canonical. - Err(BeaconChainError::BlockHashMissingFromExecutionLayer(_)) => (), - Err(e) => return Err(Error::BeaconChain(e)), - } - } - - // attempt to validate any non-finalized canonical otb blocks - for otb in unfinalized_canonical_otbs { - match chain.get_block(otb.root()).await { - Ok(Some(block)) => { - match validate_merge_block(chain, block.message(), AllowOptimisticImport::No).await - { - Ok(()) => { - // merge transition block is valid, remove it from OTB - otb.remove_from_store::<T, _>(&chain.store) - .map_err(Error::StoreError)?; - info!( - block_root = ?otb.root(), - "type" = "not finalized", - "Validated merge transition block" - ); - } - // The block was not able to be verified by the EL. Leave the OTB in the - // database since the EL is likely still syncing and may verify the block - // later. - Err(BlockError::ExecutionPayloadError( - ExecutionPayloadError::UnverifiedNonOptimisticCandidate, - )) => (), - Err(BlockError::ExecutionPayloadError( - ExecutionPayloadError::InvalidTerminalPoWBlock { .. }, - )) => { - // Unfinalized Merge Transition Block is Invalid -> Run process_invalid_execution_payload - warn!( - block_root = ?otb.root(), - "Merge transition block invalid" - ); - chain - .process_invalid_execution_payload( - &InvalidationOperation::InvalidateOne { - block_root: *otb.root(), - }, - ) - .await - .map_err(|e| { - warn!( - error = ?e, - location = "process_invalid_execution_payload", - "Error checking merge transition block" - ); - Error::BeaconChain(e) - })?; - } - _ => {} - } - } - Ok(None) => return Err(Error::NoBlockFound(otb)), - // Our database has pruned the payload and the payload was unavailable on the EL since - // the EL is still syncing or the payload is non-canonical. - Err(BeaconChainError::BlockHashMissingFromExecutionLayer(_)) => (), - Err(e) => return Err(Error::BeaconChain(e)), - } - } - - Ok(()) -} - -/// Loop until any optimistically imported merge transition blocks have been verified and -/// the merge has been finalized. -async fn otb_verification_service<T: BeaconChainTypes>(chain: Arc<BeaconChain<T>>) { - let epoch_duration = chain.slot_clock.slot_duration() * T::EthSpec::slots_per_epoch() as u32; - loop { - match chain - .slot_clock - .duration_to_next_epoch(T::EthSpec::slots_per_epoch()) - { - Some(duration) => { - let additional_delay = epoch_duration / EPOCH_DELAY_FACTOR; - sleep(duration + additional_delay).await; - - debug!("OTB verification service firing"); - - if !is_merge_transition_complete( - &chain.canonical_head.cached_head().snapshot.beacon_state, - ) { - // We are pre-merge. Nothing to do yet. - continue; - } - - // load all optimistically imported transition blocks from the database - match load_optimistic_transition_blocks(chain.as_ref()) { - Ok(otbs) => { - if otbs.is_empty() { - if chain - .canonical_head - .fork_choice_read_lock() - .get_finalized_block() - .map_or(false, |block| { - block.execution_status.is_execution_enabled() - }) - { - // there are no optimistic blocks in the database, we can exit - // the service since the merge transition is finalized and we'll - // never see another transition block - break; - } else { - debug!( - info = "waiting for the merge transition to finalize", - "No optimistic transition blocks" - ) - } - } - if let Err(e) = validate_optimistic_transition_blocks(&chain, otbs).await { - warn!( - error = ?e, - "Error while validating optimistic transition blocks" - ); - } - } - Err(e) => { - error!( - error = ?e, - "Error loading optimistic transition blocks" - ); - } - }; - } - None => { - error!("Failed to read slot clock"); - // If we can't read the slot clock, just wait another slot. - sleep(chain.slot_clock.slot_duration()).await; - } - }; - } - debug!( - msg = "shutting down OTB verification service", - "No optimistic transition blocks in database" - ); -} diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/execution_pending_envelope.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/execution_pending_envelope.rs new file mode 100644 index 0000000000..86f9293c8f --- /dev/null +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/execution_pending_envelope.rs @@ -0,0 +1,105 @@ +use std::sync::Arc; + +use slot_clock::SlotClock; +use state_processing::{ + VerifySignatures, + envelope_processing::{VerifyStateRoot, process_execution_payload_envelope}, +}; +use types::EthSpec; + +use crate::{ + BeaconChain, BeaconChainError, BeaconChainTypes, NotifyExecutionLayer, + PayloadVerificationOutcome, + block_verification::PayloadVerificationHandle, + payload_envelope_verification::{ + EnvelopeError, EnvelopeImportData, MaybeAvailableEnvelope, + gossip_verified_envelope::GossipVerifiedEnvelope, load_snapshot_from_state_root, + payload_notifier::PayloadNotifier, + }, +}; + +pub struct ExecutionPendingEnvelope<E: EthSpec> { + pub signed_envelope: MaybeAvailableEnvelope<E>, + pub import_data: EnvelopeImportData<E>, + pub payload_verification_handle: PayloadVerificationHandle, +} + +impl<T: BeaconChainTypes> GossipVerifiedEnvelope<T> { + pub fn into_execution_pending_envelope( + self, + chain: &Arc<BeaconChain<T>>, + notify_execution_layer: NotifyExecutionLayer, + ) -> Result<ExecutionPendingEnvelope<T::EthSpec>, EnvelopeError> { + let signed_envelope = self.signed_envelope; + let envelope = &signed_envelope.message; + let payload = &envelope.payload; + + // Define a future that will verify the execution payload with an execution engine. + // + // We do this as early as possible so that later parts of this function can run in parallel + // with the payload verification. + let payload_notifier = PayloadNotifier::new( + chain.clone(), + signed_envelope.clone(), + self.block.clone(), + notify_execution_layer, + )?; + let block_root = envelope.beacon_block_root; + let slot = self.block.slot(); + + let payload_verification_future = async move { + let chain = payload_notifier.chain.clone(); + if let Some(started_execution) = chain.slot_clock.now_duration() { + chain + .envelope_times_cache + .write() + .set_time_started_execution(block_root, slot, started_execution); + } + + let payload_verification_status = payload_notifier.notify_new_payload().await?; + Ok(PayloadVerificationOutcome { + payload_verification_status, + }) + }; + // Spawn the payload verification future as a new task, but don't wait for it to complete. + // The `payload_verification_future` will be awaited later to ensure verification completed + // successfully. + let payload_verification_handle = chain + .task_executor + .spawn_handle( + payload_verification_future, + "execution_payload_verification", + ) + .ok_or(BeaconChainError::RuntimeShutdown)?; + + let snapshot = if let Some(snapshot) = self.snapshot { + *snapshot + } else { + load_snapshot_from_state_root::<T>(block_root, self.block.state_root(), &chain.store)? + }; + let mut state = snapshot.pre_state; + + // All the state modifications are done in envelope_processing + process_execution_payload_envelope( + &mut state, + Some(snapshot.state_root), + &signed_envelope, + // verify signature already done for GossipVerifiedEnvelope + VerifySignatures::False, + VerifyStateRoot::True, + &chain.spec, + )?; + + Ok(ExecutionPendingEnvelope { + signed_envelope: MaybeAvailableEnvelope::AvailabilityPending { + block_hash: payload.block_hash, + envelope: signed_envelope, + }, + import_data: EnvelopeImportData { + block_root, + post_state: Box::new(state), + }, + payload_verification_handle, + }) + } +} diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/gossip_verified_envelope.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/gossip_verified_envelope.rs new file mode 100644 index 0000000000..03a3a91ac5 --- /dev/null +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/gossip_verified_envelope.rs @@ -0,0 +1,445 @@ +use std::sync::Arc; + +use educe::Educe; +use parking_lot::{Mutex, RwLock}; +use store::DatabaseBlock; +use tracing::{Span, debug}; +use types::{ + ChainSpec, EthSpec, ExecutionPayloadBid, ExecutionPayloadEnvelope, Hash256, SignedBeaconBlock, + SignedExecutionPayloadEnvelope, Slot, consts::gloas::BUILDER_INDEX_SELF_BUILD, +}; + +use crate::{ + BeaconChain, BeaconChainError, BeaconChainTypes, BeaconStore, + beacon_proposer_cache::{self, BeaconProposerCache}, + canonical_head::CanonicalHead, + payload_envelope_verification::{ + EnvelopeError, EnvelopeProcessingSnapshot, load_snapshot_from_state_root, + }, + validator_pubkey_cache::ValidatorPubkeyCache, +}; + +/// Bundles only the dependencies needed for gossip verification of execution payload envelopes, +/// decoupling `GossipVerifiedEnvelope::new` from the full `BeaconChain`. +pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { + pub canonical_head: &'a CanonicalHead<T>, + pub store: &'a BeaconStore<T>, + pub spec: &'a ChainSpec, + pub beacon_proposer_cache: &'a Mutex<BeaconProposerCache>, + pub validator_pubkey_cache: &'a RwLock<ValidatorPubkeyCache<T>>, + pub genesis_validators_root: Hash256, +} + +/// Verify that an execution payload envelope is consistent with its beacon block +/// and execution bid. +pub(crate) fn verify_envelope_consistency<E: EthSpec>( + envelope: &ExecutionPayloadEnvelope<E>, + block: &SignedBeaconBlock<E>, + execution_bid: &ExecutionPayloadBid<E>, + latest_finalized_slot: Slot, +) -> Result<(), EnvelopeError> { + // Check that the envelope's slot isn't from a slot prior + // to the latest finalized slot. + if envelope.slot < latest_finalized_slot { + return Err(EnvelopeError::PriorToFinalization { + payload_slot: envelope.slot, + latest_finalized_slot, + }); + } + + // Check that the slot of the envelope matches the slot of the block. + if envelope.slot != block.slot() { + return Err(EnvelopeError::SlotMismatch { + block: block.slot(), + envelope: envelope.slot, + }); + } + + // Builder index matches committed bid. + if envelope.builder_index != execution_bid.builder_index { + return Err(EnvelopeError::BuilderIndexMismatch { + committed_bid: execution_bid.builder_index, + envelope: envelope.builder_index, + }); + } + + // The block hash should match the block hash of the execution bid. + if envelope.payload.block_hash != execution_bid.block_hash { + return Err(EnvelopeError::BlockHashMismatch { + committed_bid: execution_bid.block_hash, + envelope: envelope.payload.block_hash, + }); + } + + Ok(()) +} + +/// A wrapper around a `SignedExecutionPayloadEnvelope` that indicates it has been approved for re-gossiping on +/// the p2p network. +#[derive(Educe)] +#[educe(Debug(bound = "T: BeaconChainTypes"))] +pub struct GossipVerifiedEnvelope<T: BeaconChainTypes> { + pub signed_envelope: Arc<SignedExecutionPayloadEnvelope<T::EthSpec>>, + pub block: Arc<SignedBeaconBlock<T::EthSpec>>, + pub snapshot: Option<Box<EnvelopeProcessingSnapshot<T::EthSpec>>>, +} + +impl<T: BeaconChainTypes> GossipVerifiedEnvelope<T> { + pub fn new( + signed_envelope: Arc<SignedExecutionPayloadEnvelope<T::EthSpec>>, + ctx: &GossipVerificationContext<'_, T>, + ) -> Result<Self, EnvelopeError> { + let envelope = &signed_envelope.message; + let beacon_block_root = envelope.beacon_block_root; + + // Check that we've seen the beacon block for this envelope and that it passes validation. + // TODO(EIP-7732): We might need some type of status table in order to differentiate between: + // If we have a block_processing_table, we could have a Processed(Bid, bool) state that is only + // entered post adding to fork choice. That way, we could potentially need only a single call to make + // sure the block is valid and to do all consequent checks with the bid + // + // 1. Blocks we haven't seen (IGNORE), and + // 2. Blocks we've seen that are invalid (REJECT). + // + // Presently these two cases are conflated. + let fork_choice_read_lock = ctx.canonical_head.fork_choice_read_lock(); + let Some(proto_block) = fork_choice_read_lock.get_block(&beacon_block_root) else { + return Err(EnvelopeError::BlockRootUnknown { + block_root: beacon_block_root, + }); + }; + + drop(fork_choice_read_lock); + + let latest_finalized_slot = ctx + .canonical_head + .cached_head() + .finalized_checkpoint() + .epoch + .start_slot(T::EthSpec::slots_per_epoch()); + + // TODO(EIP-7732): check that we haven't seen another valid `SignedExecutionPayloadEnvelope` + // for this block root from this builder - envelope status table check + let block = match ctx.store.try_get_full_block(&beacon_block_root)? { + Some(DatabaseBlock::Full(block)) => Arc::new(block), + Some(DatabaseBlock::Blinded(_)) | None => { + return Err(EnvelopeError::from(BeaconChainError::MissingBeaconBlock( + beacon_block_root, + ))); + } + }; + let execution_bid = &block + .message() + .body() + .signed_execution_payload_bid()? + .message; + + verify_envelope_consistency(envelope, &block, execution_bid, latest_finalized_slot)?; + + // Verify the envelope signature. + // + // For self-built envelopes, we can use the proposer cache for the fork and the + // validator pubkey cache for the proposer's pubkey, avoiding a state load from disk. + // For external builder envelopes, we must load the state to access the builder registry. + let builder_index = envelope.builder_index; + let block_slot = envelope.slot; + let envelope_epoch = block_slot.epoch(T::EthSpec::slots_per_epoch()); + // Since the payload's block is already guaranteed to be imported, the associated `proto_block.current_epoch_shuffling_id` + // already carries the correct `shuffling_decision_block`. + let proposer_shuffling_decision_block = proto_block + .current_epoch_shuffling_id + .shuffling_decision_block; + + let (signature_is_valid, opt_snapshot) = if builder_index == BUILDER_INDEX_SELF_BUILD { + // Fast path: self-built envelopes can be verified without loading the state. + let mut opt_snapshot = None; + let proposer = beacon_proposer_cache::with_proposer_cache( + ctx.beacon_proposer_cache, + proposer_shuffling_decision_block, + envelope_epoch, + |proposers| proposers.get_slot::<T::EthSpec>(block_slot), + || { + debug!( + %beacon_block_root, + "Proposer shuffling cache miss for envelope verification" + ); + let snapshot = load_snapshot_from_state_root::<T>( + beacon_block_root, + proto_block.state_root, + ctx.store, + )?; + opt_snapshot = Some(Box::new(snapshot.clone())); + Ok::<_, EnvelopeError>((snapshot.state_root, snapshot.pre_state)) + }, + ctx.spec, + )?; + let expected_proposer = proposer.index; + let fork = proposer.fork; + + if block.message().proposer_index() != expected_proposer as u64 { + return Err(EnvelopeError::IncorrectBlockProposer { + proposer_index: block.message().proposer_index(), + local_shuffling: expected_proposer as u64, + }); + } + + let pubkey_cache = ctx.validator_pubkey_cache.read(); + let pubkey = pubkey_cache + .get(block.message().proposer_index() as usize) + .ok_or_else(|| EnvelopeError::UnknownValidator { + proposer_index: block.message().proposer_index(), + })?; + let is_valid = signed_envelope.verify_signature( + pubkey, + &fork, + ctx.genesis_validators_root, + ctx.spec, + ); + (is_valid, opt_snapshot) + } else { + // TODO(gloas) if we implement a builder pubkey cache, we'll need to use it here. + // External builder: must load the state to get the builder pubkey. + let snapshot = load_snapshot_from_state_root::<T>( + beacon_block_root, + proto_block.state_root, + ctx.store, + )?; + let is_valid = + signed_envelope.verify_signature_with_state(&snapshot.pre_state, ctx.spec)?; + (is_valid, Some(Box::new(snapshot))) + }; + + if !signature_is_valid { + return Err(EnvelopeError::BadSignature); + } + + Ok(Self { + signed_envelope, + block, + snapshot: opt_snapshot, + }) + } + + pub fn envelope_cloned(&self) -> Arc<SignedExecutionPayloadEnvelope<T::EthSpec>> { + self.signed_envelope.clone() + } +} + +impl<T: BeaconChainTypes> BeaconChain<T> { + /// Build a `GossipVerificationContext` from this `BeaconChain`. + pub fn gossip_verification_context(&self) -> GossipVerificationContext<'_, T> { + GossipVerificationContext { + canonical_head: &self.canonical_head, + store: &self.store, + spec: &self.spec, + beacon_proposer_cache: &self.beacon_proposer_cache, + validator_pubkey_cache: &self.validator_pubkey_cache, + genesis_validators_root: self.genesis_validators_root, + } + } + + /// Returns `Ok(GossipVerifiedEnvelope)` if the supplied `envelope` should be forwarded onto the + /// gossip network. The envelope is not imported into the chain, it is just partially verified. + /// + /// The returned `GossipVerifiedEnvelope` should be provided to `Self::process_execution_payload_envelope` immediately + /// after it is returned, unless some other circumstance decides it should not be imported at + /// all. + /// + /// ## Errors + /// + /// Returns an `Err` if the given envelope was invalid, or an error was encountered during verification. + pub async fn verify_envelope_for_gossip( + self: &Arc<Self>, + envelope: Arc<SignedExecutionPayloadEnvelope<T::EthSpec>>, + ) -> Result<GossipVerifiedEnvelope<T>, EnvelopeError> { + let chain = self.clone(); + let span = Span::current(); + self.task_executor + .clone() + .spawn_blocking_handle( + move || { + let _guard = span.enter(); + let slot = envelope.slot(); + let beacon_block_root = envelope.message.beacon_block_root; + + let ctx = chain.gossip_verification_context(); + match GossipVerifiedEnvelope::new(envelope, &ctx) { + Ok(verified) => { + debug!( + %slot, + ?beacon_block_root, + "Successfully verified gossip envelope" + ); + + Ok(verified) + } + Err(e) => { + debug!( + error = e.to_string(), + ?beacon_block_root, + %slot, + "Rejected gossip envelope" + ); + + Err(e) + } + } + }, + "gossip_envelope_verification_handle", + ) + .ok_or(BeaconChainError::RuntimeShutdown)? + .await + .map_err(BeaconChainError::TokioJoin)? + } +} + +#[cfg(test)] +mod tests { + use std::marker::PhantomData; + + use bls::Signature; + use ssz_types::VariableList; + use types::{ + BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, Eth1Data, ExecutionBlockHash, + ExecutionPayloadBid, ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, + Graffiti, Hash256, MinimalEthSpec, SignedBeaconBlock, SignedExecutionPayloadBid, Slot, + SyncAggregate, + }; + + use super::verify_envelope_consistency; + use crate::payload_envelope_verification::EnvelopeError; + + type E = MinimalEthSpec; + + fn make_envelope( + slot: Slot, + builder_index: u64, + block_hash: ExecutionBlockHash, + ) -> ExecutionPayloadEnvelope<E> { + ExecutionPayloadEnvelope { + payload: ExecutionPayloadGloas { + block_hash, + ..ExecutionPayloadGloas::default() + }, + execution_requests: ExecutionRequests::default(), + builder_index, + beacon_block_root: Hash256::ZERO, + slot, + state_root: Hash256::ZERO, + } + } + + fn make_block(slot: Slot) -> SignedBeaconBlock<E> { + let block = BeaconBlock::Gloas(BeaconBlockGloas { + slot, + proposer_index: 0, + parent_root: Hash256::ZERO, + state_root: Hash256::ZERO, + body: BeaconBlockBodyGloas { + randao_reveal: Signature::empty(), + eth1_data: Eth1Data { + deposit_root: Hash256::ZERO, + block_hash: Hash256::ZERO, + deposit_count: 0, + }, + graffiti: Graffiti::default(), + proposer_slashings: VariableList::empty(), + attester_slashings: VariableList::empty(), + attestations: VariableList::empty(), + deposits: VariableList::empty(), + voluntary_exits: VariableList::empty(), + sync_aggregate: SyncAggregate::empty(), + bls_to_execution_changes: VariableList::empty(), + signed_execution_payload_bid: SignedExecutionPayloadBid::empty(), + payload_attestations: VariableList::empty(), + _phantom: PhantomData, + }, + }); + SignedBeaconBlock::from_block(block, Signature::empty()) + } + + fn make_bid(builder_index: u64, block_hash: ExecutionBlockHash) -> ExecutionPayloadBid<E> { + ExecutionPayloadBid { + builder_index, + block_hash, + ..ExecutionPayloadBid::default() + } + } + + #[test] + fn test_valid_envelope() { + let slot = Slot::new(10); + let builder_index = 5; + let block_hash = ExecutionBlockHash::repeat_byte(0xaa); + + let envelope = make_envelope(slot, builder_index, block_hash); + let block = make_block(slot); + let bid = make_bid(builder_index, block_hash); + + assert!(verify_envelope_consistency::<E>(&envelope, &block, &bid, Slot::new(0)).is_ok()); + } + + #[test] + fn test_prior_to_finalization() { + let slot = Slot::new(5); + let builder_index = 1; + let block_hash = ExecutionBlockHash::repeat_byte(0xbb); + + let envelope = make_envelope(slot, builder_index, block_hash); + let block = make_block(slot); + let bid = make_bid(builder_index, block_hash); + let latest_finalized_slot = Slot::new(10); + + let result = + verify_envelope_consistency::<E>(&envelope, &block, &bid, latest_finalized_slot); + assert!(matches!( + result, + Err(EnvelopeError::PriorToFinalization { .. }) + )); + } + + #[test] + fn test_slot_mismatch() { + let builder_index = 1; + let block_hash = ExecutionBlockHash::repeat_byte(0xcc); + + let envelope = make_envelope(Slot::new(10), builder_index, block_hash); + let block = make_block(Slot::new(20)); + let bid = make_bid(builder_index, block_hash); + + let result = verify_envelope_consistency::<E>(&envelope, &block, &bid, Slot::new(0)); + assert!(matches!(result, Err(EnvelopeError::SlotMismatch { .. }))); + } + + #[test] + fn test_builder_index_mismatch() { + let slot = Slot::new(10); + let block_hash = ExecutionBlockHash::repeat_byte(0xdd); + + let envelope = make_envelope(slot, 1, block_hash); + let block = make_block(slot); + let bid = make_bid(2, block_hash); + + let result = verify_envelope_consistency::<E>(&envelope, &block, &bid, Slot::new(0)); + assert!(matches!( + result, + Err(EnvelopeError::BuilderIndexMismatch { .. }) + )); + } + + #[test] + fn test_block_hash_mismatch() { + let slot = Slot::new(10); + let builder_index = 1; + + let envelope = make_envelope(slot, builder_index, ExecutionBlockHash::repeat_byte(0xee)); + let block = make_block(slot); + let bid = make_bid(builder_index, ExecutionBlockHash::repeat_byte(0xff)); + + let result = verify_envelope_consistency::<E>(&envelope, &block, &bid, Slot::new(0)); + assert!(matches!( + result, + Err(EnvelopeError::BlockHashMismatch { .. }) + )); + } +} diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs new file mode 100644 index 0000000000..2ee315e559 --- /dev/null +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -0,0 +1,354 @@ +use std::sync::Arc; +use std::time::Duration; + +use fork_choice::PayloadVerificationStatus; +use slot_clock::SlotClock; +use store::StoreOp; +use tracing::{debug, error, info, info_span, instrument, warn}; +use types::{BeaconState, BlockImportSource, Hash256, Slot}; + +use super::{ + AvailableEnvelope, AvailableExecutedEnvelope, EnvelopeError, EnvelopeImportData, + ExecutedEnvelope, gossip_verified_envelope::GossipVerifiedEnvelope, +}; +use crate::{ + AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, + NotifyExecutionLayer, block_verification_types::AvailableBlockData, metrics, + payload_envelope_verification::ExecutionPendingEnvelope, validator_monitor::get_slot_delay_ms, +}; + +const ENVELOPE_METRICS_CACHE_SLOT_LIMIT: u32 = 64; + +impl<T: BeaconChainTypes> BeaconChain<T> { + /// Returns `Ok(status)` if the given `unverified_envelope` was successfully verified and + /// imported into the chain. + /// + /// ## Errors + /// + /// Returns an `Err` if the given payload envelope was invalid, or an error was encountered during + /// verification. + #[instrument(skip_all, fields(block_root = ?block_root, block_source = %block_source))] + pub async fn process_execution_payload_envelope( + self: &Arc<Self>, + block_root: Hash256, + unverified_envelope: GossipVerifiedEnvelope<T>, + notify_execution_layer: NotifyExecutionLayer, + block_source: BlockImportSource, + publish_fn: impl FnOnce() -> Result<(), EnvelopeError>, + ) -> Result<AvailabilityProcessingStatus, EnvelopeError> { + let block_slot = unverified_envelope.signed_envelope.slot(); + + // Set observed time if not already set. Usually this should be set by gossip or RPC, + // but just in case we set it again here (useful for tests). + if let Some(seen_timestamp) = self.slot_clock.now_duration() { + self.envelope_times_cache.write().set_time_observed( + block_root, + block_slot, + seen_timestamp, + None, + ); + } + + // TODO(gloas) insert the pre-executed envelope into some type of cache. + + let _full_timer = metrics::start_timer(&metrics::ENVELOPE_PROCESSING_TIMES); + + metrics::inc_counter(&metrics::ENVELOPE_PROCESSING_REQUESTS); + + // A small closure to group the verification and import errors. + let chain = self.clone(); + let import_envelope = async move { + let execution_pending = unverified_envelope + .into_execution_pending_envelope(&chain, notify_execution_layer)?; + publish_fn()?; + + // Record the time it took to complete consensus verification. + if let Some(timestamp) = chain.slot_clock.now_duration() { + chain + .envelope_times_cache + .write() + .set_time_consensus_verified(block_root, block_slot, timestamp); + } + + let envelope_times_cache = chain.envelope_times_cache.clone(); + let slot_clock = chain.slot_clock.clone(); + + // TODO(gloas): rename/refactor these `into_` names to be less similar and more clear + // about what the function actually does. + let executed_envelope = chain + .into_executed_payload_envelope(execution_pending) + .await + .inspect_err(|_| { + // TODO(gloas) If the envelope fails execution for whatever reason (e.g. engine offline), + // and we keep it in the cache, then the node will NOT perform lookup and + // reprocess this block until the block is evicted from DA checker, causing the + // chain to get stuck temporarily if the block is canonical. Therefore we remove + // it from the cache if execution fails. + })?; + + // Record the time it took to wait for execution layer verification. + if let Some(timestamp) = slot_clock.now_duration() { + envelope_times_cache + .write() + .set_time_executed(block_root, block_slot, timestamp); + } + + match executed_envelope { + ExecutedEnvelope::Available(envelope) => { + self.import_available_execution_payload_envelope(Box::new(envelope)) + .await + } + ExecutedEnvelope::AvailabilityPending() => Err(EnvelopeError::InternalError( + "Pending payload envelope not yet implemented".to_owned(), + )), + } + }; + + // Verify and import the payload envelope. + match import_envelope.await { + // The payload envelope was successfully verified and imported. + Ok(status @ AvailabilityProcessingStatus::Imported(block_root)) => { + info!( + ?block_root, + %block_slot, + source = %block_source, + "Execution payload envelope imported" + ); + + // TODO(gloas) do we need to send a `PayloadImported` event to the reprocess queue? + // TODO(gloas) do we need to recompute head? + // should canonical_head return the block and the payload now? + self.recompute_head_at_current_slot().await; + + metrics::inc_counter(&metrics::ENVELOPE_PROCESSING_SUCCESSES); + + Ok(status) + } + Ok(status @ AvailabilityProcessingStatus::MissingComponents(slot, block_root)) => { + debug!(?block_root, %slot, "Payload envelope awaiting blobs"); + + Ok(status) + } + Err(EnvelopeError::BeaconChainError(e)) => { + if matches!(e.as_ref(), BeaconChainError::TokioJoin(_)) { + debug!(error = ?e, "Envelope processing cancelled"); + } else { + warn!(error = ?e, "Execution payload envelope rejected"); + } + Err(EnvelopeError::BeaconChainError(e)) + } + Err(other) => { + warn!( + reason = other.to_string(), + "Execution payload envelope rejected" + ); + Err(other) + } + } + } + + /// Accepts a fully-verified payload envelope and awaits on its payload verification handle to + /// get a fully `ExecutedEnvelope`. + /// + /// An error is returned if the verification handle couldn't be awaited. + #[instrument(skip_all, level = "debug")] + async fn into_executed_payload_envelope( + self: Arc<Self>, + pending_envelope: ExecutionPendingEnvelope<T::EthSpec>, + ) -> Result<ExecutedEnvelope<T::EthSpec>, EnvelopeError> { + let ExecutionPendingEnvelope { + signed_envelope, + import_data, + payload_verification_handle, + } = pending_envelope; + + let payload_verification_outcome = payload_verification_handle + .await + .map_err(BeaconChainError::TokioJoin)? + .ok_or(BeaconChainError::RuntimeShutdown)??; + + Ok(ExecutedEnvelope::new( + signed_envelope, + import_data, + payload_verification_outcome, + )) + } + + #[instrument(skip_all)] + pub async fn import_available_execution_payload_envelope( + self: &Arc<Self>, + envelope: Box<AvailableExecutedEnvelope<T::EthSpec>>, + ) -> Result<AvailabilityProcessingStatus, EnvelopeError> { + let AvailableExecutedEnvelope { + envelope, + import_data, + payload_verification_outcome, + } = *envelope; + + let EnvelopeImportData { + block_root, + post_state, + } = import_data; + + let block_root = { + // Capture the current span before moving into the blocking task + let current_span = tracing::Span::current(); + let chain = self.clone(); + self.spawn_blocking_handle( + move || { + // Enter the captured span in the blocking thread + let _guard = current_span.enter(); + chain.import_execution_payload_envelope( + envelope, + block_root, + *post_state, + payload_verification_outcome.payload_verification_status, + ) + }, + "payload_verification_handle", + ) + .await?? + }; + + Ok(AvailabilityProcessingStatus::Imported(block_root)) + } + + /// Accepts a fully-verified and available envelope and imports it into the chain without performing any + /// additional verification. + /// + /// An error is returned if the envelope was unable to be imported. It may be partially imported + /// (i.e., this function is not atomic). + #[allow(clippy::too_many_arguments)] + #[instrument(skip_all)] + fn import_execution_payload_envelope( + &self, + signed_envelope: AvailableEnvelope<T::EthSpec>, + block_root: Hash256, + state: BeaconState<T::EthSpec>, + _payload_verification_status: PayloadVerificationStatus, + ) -> Result<Hash256, EnvelopeError> { + // Everything in this initial section is on the hot path for processing the envelope. + // Take an upgradable read lock on fork choice so we can check if this block has already + // been imported. We don't want to repeat work importing a block that is already imported. + let fork_choice_reader = self.canonical_head.fork_choice_upgradable_read_lock(); + if !fork_choice_reader.contains_block(&block_root) { + return Err(EnvelopeError::BlockRootUnknown { block_root }); + } + + // TODO(gloas) add defensive check to see if payload envelope is already in fork choice + // Note that a duplicate cache/payload status table should prevent this from happening + // but it doesnt hurt to be defensive. + + // TODO(gloas) when the code below is implemented we can delete this drop + drop(fork_choice_reader); + + // TODO(gloas) no fork choice logic yet + // Take an exclusive write-lock on fork choice. It's very important to prevent deadlocks by + // avoiding taking other locks whilst holding this lock. + // let fork_choice = parking_lot::RwLockUpgradableReadGuard::upgrade(fork_choice_reader); + + // TODO(gloas) Do we need this check? Do not import a block that doesn't descend from the finalized root. + // let signed_block = check_block_is_finalized_checkpoint_or_descendant(self, &fork_choice, signed_block)?; + + // TODO(gloas) emit SSE event if the payload became the new head payload + + // It is important NOT to return errors here before the database commit, because the envelope + // has already been added to fork choice and the database would be left in an inconsistent + // state if we returned early without committing. In other words, an error here would + // corrupt the node's database permanently. + + // Store the envelope, its post-state, and any data columns. + // If the write fails, revert fork choice to the version from disk, else we can + // end up with envelopes in fork choice that are missing from disk. + // See https://github.com/sigp/lighthouse/issues/2028 + let (signed_envelope, columns) = signed_envelope.deconstruct(); + + let mut ops = vec![]; + + if let Some(blobs_or_columns_store_op) = self.get_blobs_or_columns_store_op( + block_root, + signed_envelope.slot(), + AvailableBlockData::DataColumns(columns), + ) { + ops.push(blobs_or_columns_store_op); + } + + let db_write_timer = metrics::start_timer(&metrics::ENVELOPE_PROCESSING_DB_WRITE); + + ops.push(StoreOp::PutPayloadEnvelope( + block_root, + signed_envelope.clone(), + )); + ops.push(StoreOp::PutState( + signed_envelope.message.state_root, + &state, + )); + + let db_span = info_span!("persist_payloads_and_blobs").entered(); + + if let Err(e) = self.store.do_atomically_with_block_and_blobs_cache(ops) { + error!( + msg = "Restoring fork choice from disk", + error = ?e, + "Database write failed!" + ); + return Err(e.into()); + // TODO(gloas) handle db write failure + // return Err(self + // .handle_import_block_db_write_error(fork_choice) + // .err() + // .unwrap_or(e.into())); + } + + drop(db_span); + + // TODO(gloas) drop fork choice lock + // The fork choice write-lock is dropped *after* the on-disk database has been updated. + // This prevents inconsistency between the two at the expense of concurrency. + // drop(fork_choice); + + // We're declaring the envelope "imported" at this point, since fork choice and the DB know + // about it. + let envelope_time_imported = self.slot_clock.now_duration().unwrap_or(Duration::MAX); + + // TODO(gloas) depending on what happens with light clients + // we might need to do some light client related computations here + + metrics::stop_timer(db_write_timer); + + self.import_envelope_update_metrics_and_events( + block_root, + signed_envelope.slot(), + envelope_time_imported, + ); + + Ok(block_root) + } + + fn import_envelope_update_metrics_and_events( + &self, + block_root: Hash256, + envelope_slot: Slot, + envelope_time_imported: Duration, + ) { + let envelope_delay_total = + get_slot_delay_ms(envelope_time_imported, envelope_slot, &self.slot_clock); + + // Do not write to the cache for envelopes older than 2 epochs, this helps reduce writes + // to the cache during sync. + if envelope_delay_total + < self + .slot_clock + .slot_duration() + .saturating_mul(ENVELOPE_METRICS_CACHE_SLOT_LIMIT) + { + self.envelope_times_cache.write().set_time_imported( + block_root, + envelope_slot, + envelope_time_imported, + ); + } + + // TODO(gloas) emit SSE event for envelope import (similar to SseBlock for blocks). + } +} diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs new file mode 100644 index 0000000000..c707d62dc7 --- /dev/null +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -0,0 +1,285 @@ +//! The incremental processing steps (e.g., signatures verified but not the state transition) is +//! represented as a sequence of wrapper-types around the envelope. There is a linear progression of +//! types, starting at a `SignedExecutionPayloadEnvelope` and finishing with an `AvailableExecutedEnvelope` (see +//! diagram below). +//! +//! ```ignore +//! SignedExecutionPayloadEnvelope +//! | +//! ▼ +//! GossipVerifiedEnvelope +//! | +//! ▼ +//! ExecutionPendingEnvelope +//! | +//! await +//! ▼ +//! ExecutedEnvelope +//! +//! ``` + +use std::sync::Arc; + +use store::Error as DBError; + +use state_processing::{BlockProcessingError, envelope_processing::EnvelopeProcessingError}; +use tracing::instrument; +use types::{ + BeaconState, BeaconStateError, ChainSpec, DataColumnSidecarList, EthSpec, ExecutionBlockHash, + ExecutionPayloadEnvelope, Hash256, SignedExecutionPayloadEnvelope, Slot, +}; + +use crate::{ + BeaconChainError, BeaconChainTypes, BeaconStore, BlockError, ExecutionPayloadError, + PayloadVerificationOutcome, +}; + +pub mod execution_pending_envelope; +pub mod gossip_verified_envelope; +pub mod import; +mod payload_notifier; + +pub use execution_pending_envelope::ExecutionPendingEnvelope; + +#[derive(PartialEq)] +pub struct EnvelopeImportData<E: EthSpec> { + pub block_root: Hash256, + pub post_state: Box<BeaconState<E>>, +} + +#[derive(Debug)] +#[allow(dead_code)] +pub struct AvailableEnvelope<E: EthSpec> { + execution_block_hash: ExecutionBlockHash, + envelope: Arc<SignedExecutionPayloadEnvelope<E>>, + columns: DataColumnSidecarList<E>, + /// Timestamp at which this envelope first became available (UNIX timestamp, time since 1970). + columns_available_timestamp: Option<std::time::Duration>, + pub spec: Arc<ChainSpec>, +} + +impl<E: EthSpec> AvailableEnvelope<E> { + pub fn message(&self) -> &ExecutionPayloadEnvelope<E> { + &self.envelope.message + } + + #[allow(clippy::type_complexity)] + pub fn deconstruct( + self, + ) -> ( + Arc<SignedExecutionPayloadEnvelope<E>>, + DataColumnSidecarList<E>, + ) { + let AvailableEnvelope { + envelope, columns, .. + } = self; + (envelope, columns) + } +} + +pub enum MaybeAvailableEnvelope<E: EthSpec> { + Available(AvailableEnvelope<E>), + AvailabilityPending { + block_hash: ExecutionBlockHash, + envelope: Arc<SignedExecutionPayloadEnvelope<E>>, + }, +} + +/// This snapshot is to be used for verifying a payload envelope. +#[derive(Debug, Clone)] +pub struct EnvelopeProcessingSnapshot<E: EthSpec> { + /// This state is equivalent to the `self.beacon_block.state_root()` before applying the envelope. + pub pre_state: BeaconState<E>, + pub state_root: Hash256, + pub beacon_block_root: Hash256, +} + +/// A payload envelope that has gone through processing checks and execution by an EL client. +/// This envelope hasn't necessarily completed data availability checks. +/// +/// +/// It contains 2 variants: +/// 1. `Available`: This envelope has been executed and also contains all data to consider it +/// fully available. +/// 2. `AvailabilityPending`: This envelope hasn't received all required blobs to consider it +/// fully available. +pub enum ExecutedEnvelope<E: EthSpec> { + Available(AvailableExecutedEnvelope<E>), + // TODO(gloas) implement availability pending + AvailabilityPending(), +} + +impl<E: EthSpec> ExecutedEnvelope<E> { + pub fn new( + envelope: MaybeAvailableEnvelope<E>, + import_data: EnvelopeImportData<E>, + payload_verification_outcome: PayloadVerificationOutcome, + ) -> Self { + match envelope { + MaybeAvailableEnvelope::Available(available_envelope) => { + Self::Available(AvailableExecutedEnvelope::new( + available_envelope, + import_data, + payload_verification_outcome, + )) + } + // TODO(gloas) implement availability pending + MaybeAvailableEnvelope::AvailabilityPending { + block_hash: _, + envelope: _, + } => Self::AvailabilityPending(), + } + } +} + +/// A payload envelope that has completed all payload processing checks including verification +/// by an EL client **and** has all requisite blob data to be imported into fork choice. +pub struct AvailableExecutedEnvelope<E: EthSpec> { + pub envelope: AvailableEnvelope<E>, + pub import_data: EnvelopeImportData<E>, + pub payload_verification_outcome: PayloadVerificationOutcome, +} + +impl<E: EthSpec> AvailableExecutedEnvelope<E> { + pub fn new( + envelope: AvailableEnvelope<E>, + import_data: EnvelopeImportData<E>, + payload_verification_outcome: PayloadVerificationOutcome, + ) -> Self { + Self { + envelope, + import_data, + payload_verification_outcome, + } + } +} + +#[derive(Debug)] +pub enum EnvelopeError { + /// The envelope's block root is unknown. + BlockRootUnknown { block_root: Hash256 }, + /// The signature is invalid. + BadSignature, + /// The builder index doesn't match the committed bid + BuilderIndexMismatch { committed_bid: u64, envelope: u64 }, + /// The envelope slot doesn't match the block + SlotMismatch { block: Slot, envelope: Slot }, + /// The validator index is unknown + UnknownValidator { proposer_index: u64 }, + /// The block hash doesn't match the committed bid + BlockHashMismatch { + committed_bid: ExecutionBlockHash, + envelope: ExecutionBlockHash, + }, + /// The block's proposer_index does not match the locally computed proposer + IncorrectBlockProposer { + proposer_index: u64, + local_shuffling: u64, + }, + /// The slot belongs to a block that is from a slot prior than + /// to most recently finalized slot + PriorToFinalization { + payload_slot: Slot, + latest_finalized_slot: Slot, + }, + /// Some Beacon Chain Error + BeaconChainError(Arc<BeaconChainError>), + /// Some Beacon State error + BeaconStateError(BeaconStateError), + /// Some BlockProcessingError (for electra operations) + BlockProcessingError(BlockProcessingError), + /// Some EnvelopeProcessingError + EnvelopeProcessingError(EnvelopeProcessingError), + /// Error verifying the execution payload + ExecutionPayloadError(ExecutionPayloadError), + /// An error from block-level checks reused during envelope import + BlockError(BlockError), + /// Internal error + InternalError(String), +} + +impl std::fmt::Display for EnvelopeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl From<BeaconChainError> for EnvelopeError { + fn from(e: BeaconChainError) -> Self { + EnvelopeError::BeaconChainError(Arc::new(e)) + } +} + +impl From<ExecutionPayloadError> for EnvelopeError { + fn from(e: ExecutionPayloadError) -> Self { + EnvelopeError::ExecutionPayloadError(e) + } +} + +impl From<BeaconStateError> for EnvelopeError { + fn from(e: BeaconStateError) -> Self { + EnvelopeError::BeaconStateError(e) + } +} + +impl From<DBError> for EnvelopeError { + fn from(e: DBError) -> Self { + EnvelopeError::BeaconChainError(Arc::new(BeaconChainError::DBError(e))) + } +} + +impl From<BlockError> for EnvelopeError { + fn from(e: BlockError) -> Self { + EnvelopeError::BlockError(e) + } +} + +/// Pull errors up from EnvelopeProcessingError to EnvelopeError +impl From<EnvelopeProcessingError> for EnvelopeError { + fn from(e: EnvelopeProcessingError) -> Self { + match e { + EnvelopeProcessingError::BadSignature => EnvelopeError::BadSignature, + EnvelopeProcessingError::BeaconStateError(e) => EnvelopeError::BeaconStateError(e), + EnvelopeProcessingError::BlockHashMismatch { + committed_bid, + envelope, + } => EnvelopeError::BlockHashMismatch { + committed_bid, + envelope, + }, + EnvelopeProcessingError::BlockProcessingError(e) => { + EnvelopeError::BlockProcessingError(e) + } + e => EnvelopeError::EnvelopeProcessingError(e), + } + } +} + +#[instrument(skip_all, level = "debug", fields(beacon_block_root = %beacon_block_root))] +/// Load state from store given a known state root and block root. +/// Use this when the proto block has already been looked up from fork choice. +pub(crate) fn load_snapshot_from_state_root<T: BeaconChainTypes>( + beacon_block_root: Hash256, + block_state_root: Hash256, + store: &BeaconStore<T>, +) -> Result<EnvelopeProcessingSnapshot<T::EthSpec>, EnvelopeError> { + // TODO(EIP-7732): add metrics here + + // We can use `get_hot_state` here rather than `get_advanced_hot_state` because the envelope + // must be from the same slot as its block (so no advance is required). + let cache_state = true; + let state = store + .get_hot_state(&block_state_root, cache_state) + .map_err(EnvelopeError::from)? + .ok_or_else(|| { + BeaconChainError::DBInconsistent(format!( + "Missing state for envelope block {block_state_root:?}", + )) + })?; + + Ok(EnvelopeProcessingSnapshot { + pre_state: state, + state_root: block_state_root, + beacon_block_root, + }) +} diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs new file mode 100644 index 0000000000..df21d33493 --- /dev/null +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs @@ -0,0 +1,94 @@ +use std::sync::Arc; + +use execution_layer::{NewPayloadRequest, NewPayloadRequestGloas}; +use fork_choice::PayloadVerificationStatus; +use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash; +use tracing::warn; +use types::{SignedBeaconBlock, SignedExecutionPayloadEnvelope}; + +use crate::{ + BeaconChain, BeaconChainTypes, BlockError, NotifyExecutionLayer, + execution_payload::notify_new_payload, payload_envelope_verification::EnvelopeError, +}; + +/// Used to await the result of executing payload with a remote EE. +pub struct PayloadNotifier<T: BeaconChainTypes> { + pub chain: Arc<BeaconChain<T>>, + envelope: Arc<SignedExecutionPayloadEnvelope<T::EthSpec>>, + block: Arc<SignedBeaconBlock<T::EthSpec>>, + payload_verification_status: Option<PayloadVerificationStatus>, +} + +impl<T: BeaconChainTypes> PayloadNotifier<T> { + pub fn new( + chain: Arc<BeaconChain<T>>, + envelope: Arc<SignedExecutionPayloadEnvelope<T::EthSpec>>, + block: Arc<SignedBeaconBlock<T::EthSpec>>, + notify_execution_layer: NotifyExecutionLayer, + ) -> Result<Self, EnvelopeError> { + let payload_verification_status = { + let payload_message = &envelope.message; + + match notify_execution_layer { + NotifyExecutionLayer::No if chain.config.optimistic_finalized_sync => { + let new_payload_request = Self::build_new_payload_request(&envelope, &block)?; + // TODO(gloas): check and test RLP block hash calculation post-Gloas + if let Err(e) = new_payload_request.perform_optimistic_sync_verifications() { + warn!( + block_number = ?payload_message.payload.block_number, + info = "you can silence this warning with --disable-optimistic-finalized-sync", + error = ?e, + "Falling back to slow block hash verification" + ); + None + } else { + Some(PayloadVerificationStatus::Optimistic) + } + } + _ => None, + } + }; + + Ok(Self { + chain, + envelope, + block, + payload_verification_status, + }) + } + + pub async fn notify_new_payload(self) -> Result<PayloadVerificationStatus, BlockError> { + if let Some(precomputed_status) = self.payload_verification_status { + Ok(precomputed_status) + } else { + let parent_root = self.block.message().parent_root(); + let request = Self::build_new_payload_request(&self.envelope, &self.block)?; + notify_new_payload(&self.chain, self.envelope.slot(), parent_root, request).await + } + } + + fn build_new_payload_request<'a>( + envelope: &'a SignedExecutionPayloadEnvelope<T::EthSpec>, + block: &'a SignedBeaconBlock<T::EthSpec>, + ) -> Result<NewPayloadRequest<'a, T::EthSpec>, BlockError> { + let bid = &block + .message() + .body() + .signed_execution_payload_bid() + .map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))? + .message; + + let versioned_hashes = bid + .blob_kzg_commitments + .iter() + .map(kzg_commitment_to_versioned_hash) + .collect(); + + Ok(NewPayloadRequest::Gloas(NewPayloadRequestGloas { + execution_payload: &envelope.message.payload, + versioned_hashes, + parent_beacon_block_root: block.message().parent_root(), + execution_requests: &envelope.message.execution_requests, + })) + } +} diff --git a/beacon_node/beacon_chain/src/pending_payload_envelopes.rs b/beacon_node/beacon_chain/src/pending_payload_envelopes.rs new file mode 100644 index 0000000000..336ab5323f --- /dev/null +++ b/beacon_node/beacon_chain/src/pending_payload_envelopes.rs @@ -0,0 +1,151 @@ +//! Provides the `PendingPayloadEnvelopes` cache for storing execution payload envelopes +//! that have been produced during local block production. +//! +//! For local building, the envelope is created during block production. +//! This cache holds the envelopes temporarily until the validator fetches, signs, +//! and publishes the payload. + +use std::collections::HashMap; +use types::{EthSpec, ExecutionPayloadEnvelope, Slot}; + +/// Cache for pending execution payload envelopes awaiting publishing. +/// +/// Envelopes are keyed by slot and pruned based on slot age. +/// This cache is only used for local building. +pub struct PendingPayloadEnvelopes<E: EthSpec> { + /// Maximum number of slots to keep envelopes before pruning. + max_slot_age: u64, + /// The envelopes, keyed by slot. + envelopes: HashMap<Slot, ExecutionPayloadEnvelope<E>>, +} + +impl<E: EthSpec> Default for PendingPayloadEnvelopes<E> { + fn default() -> Self { + Self::new(Self::DEFAULT_MAX_SLOT_AGE) + } +} + +impl<E: EthSpec> PendingPayloadEnvelopes<E> { + /// Default maximum slot age before pruning (2 slots). + pub const DEFAULT_MAX_SLOT_AGE: u64 = 2; + + /// Create a new cache with the specified maximum slot age. + pub fn new(max_slot_age: u64) -> Self { + Self { + max_slot_age, + envelopes: HashMap::new(), + } + } + + /// Insert a pending envelope into the cache. + pub fn insert(&mut self, slot: Slot, envelope: ExecutionPayloadEnvelope<E>) { + // TODO(gloas): we may want to check for duplicates here, which shouldn't be allowed + self.envelopes.insert(slot, envelope); + } + + /// Get a pending envelope by slot. + pub fn get(&self, slot: Slot) -> Option<&ExecutionPayloadEnvelope<E>> { + self.envelopes.get(&slot) + } + + /// Remove and return a pending envelope by slot. + pub fn remove(&mut self, slot: Slot) -> Option<ExecutionPayloadEnvelope<E>> { + self.envelopes.remove(&slot) + } + + /// Check if an envelope exists for the given slot. + pub fn contains(&self, slot: Slot) -> bool { + self.envelopes.contains_key(&slot) + } + + /// Prune envelopes older than `current_slot - max_slot_age`. + /// + /// This removes stale envelopes from blocks that were never published. + // TODO(gloas) implement pruning + pub fn prune(&mut self, current_slot: Slot) { + let min_slot = current_slot.saturating_sub(self.max_slot_age); + self.envelopes.retain(|slot, _| *slot >= min_slot); + } + + /// Returns the number of pending envelopes in the cache. + pub fn len(&self) -> usize { + self.envelopes.len() + } + + /// Returns true if the cache is empty. + pub fn is_empty(&self) -> bool { + self.envelopes.is_empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use types::{ExecutionPayloadGloas, ExecutionRequests, Hash256, MainnetEthSpec}; + + type E = MainnetEthSpec; + + fn make_envelope(slot: Slot) -> ExecutionPayloadEnvelope<E> { + ExecutionPayloadEnvelope { + payload: ExecutionPayloadGloas::default(), + execution_requests: ExecutionRequests::default(), + builder_index: 0, + beacon_block_root: Hash256::ZERO, + slot, + state_root: Hash256::ZERO, + } + } + + #[test] + fn insert_and_get() { + let mut cache = PendingPayloadEnvelopes::<E>::default(); + let slot = Slot::new(1); + let envelope = make_envelope(slot); + + assert!(!cache.contains(slot)); + assert_eq!(cache.len(), 0); + + cache.insert(slot, envelope.clone()); + + assert!(cache.contains(slot)); + assert_eq!(cache.len(), 1); + assert_eq!(cache.get(slot), Some(&envelope)); + } + + #[test] + fn remove() { + let mut cache = PendingPayloadEnvelopes::<E>::default(); + let slot = Slot::new(1); + let envelope = make_envelope(slot); + + cache.insert(slot, envelope.clone()); + assert!(cache.contains(slot)); + + let removed = cache.remove(slot); + assert_eq!(removed, Some(envelope)); + assert!(!cache.contains(slot)); + assert_eq!(cache.len(), 0); + } + + #[test] + fn prune_old_envelopes() { + let mut cache = PendingPayloadEnvelopes::<E>::new(2); + + // Insert envelope at slot 5 + let slot_1 = Slot::new(5); + cache.insert(slot_1, make_envelope(slot_1)); + + // Insert envelope at slot 10 + let slot_2 = Slot::new(10); + cache.insert(slot_2, make_envelope(slot_2)); + + assert_eq!(cache.len(), 2); + + // Prune at slot 10 with max_slot_age=2, should keep slots >= 8 + cache.prune(Slot::new(10)); + + assert_eq!(cache.len(), 1); + assert!(!cache.contains(slot_1)); // slot 5 < 8, pruned + assert!(cache.contains(slot_2)); // slot 10 >= 8, kept + } +} diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index e738be7692..e154c08735 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -29,10 +29,7 @@ use execution_layer::test_utils::generate_genesis_header; use execution_layer::{ ExecutionLayer, auth::JwtKey, - test_utils::{ - DEFAULT_JWT_SECRET, DEFAULT_TERMINAL_BLOCK, ExecutionBlockGenerator, MockBuilder, - MockExecutionLayer, - }, + test_utils::{DEFAULT_JWT_SECRET, ExecutionBlockGenerator, MockBuilder, MockExecutionLayer}, }; use fixed_bytes::FixedBytesExtended; use futures::channel::mpsc::Receiver; @@ -52,7 +49,11 @@ use rayon::prelude::*; use sensitive_url::SensitiveUrl; use slot_clock::{SlotClock, TestingSlotClock}; use ssz_types::{RuntimeVariableList, VariableList}; +use state_processing::ConsensusContext; use state_processing::per_block_processing::compute_timestamp_at_slot; +use state_processing::per_block_processing::{ + BlockSignatureStrategy, VerifyBlockRoot, per_block_processing, +}; use state_processing::state_advance::complete_state_advance; use std::borrow::Cow; use std::collections::{HashMap, HashSet}; @@ -202,11 +203,12 @@ pub fn fork_name_from_env() -> Option<ForkName> { /// Return a `ChainSpec` suitable for test usage. /// /// If the `fork_from_env` feature is enabled, read the fork to use from the FORK_NAME environment -/// variable. Otherwise use the default spec. +/// variable. Otherwise we default to Bellatrix as the minimum fork (we no longer support +/// starting test networks prior to Bellatrix). pub fn test_spec<E: EthSpec>() -> ChainSpec { let mut spec = fork_name_from_env() .map(|fork| fork.make_genesis_spec(E::default_spec())) - .unwrap_or_else(|| E::default_spec()); + .unwrap_or_else(|| ForkName::Bellatrix.make_genesis_spec(E::default_spec())); // Set target aggregators to a high value by default. spec.target_aggregators_per_committee = DEFAULT_TARGET_AGGREGATORS; @@ -277,16 +279,25 @@ impl<E: EthSpec> Builder<EphemeralHarnessType<E>> { }); let mutator = move |builder: BeaconChainBuilder<_>| { - let header = generate_genesis_header::<E>(builder.get_spec(), false); + let spec = builder.get_spec(); + let header = generate_genesis_header::<E>(spec); let genesis_state = genesis_state_builder - .set_opt_execution_payload_header(header) + .set_opt_execution_payload_header(header.clone()) .build_genesis_state( &validator_keypairs, HARNESS_GENESIS_TIME, Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH), - builder.get_spec(), + spec, ) .expect("should generate interop state"); + // For post-Bellatrix forks, verify the merge is complete at genesis + if header.is_some() { + assert!( + state_processing::per_block_processing::is_merge_transition_complete( + &genesis_state + ) + ); + } builder .genesis_state(genesis_state) .expect("should build state using recent genesis") @@ -344,7 +355,7 @@ impl<E: EthSpec> Builder<DiskHarnessType<E>> { }); let mutator = move |builder: BeaconChainBuilder<_>| { - let header = generate_genesis_header::<E>(builder.get_spec(), false); + let header = generate_genesis_header::<E>(builder.get_spec()); let genesis_state = genesis_state_builder .set_opt_execution_payload_header(header) .build_genesis_state( @@ -688,7 +699,6 @@ pub fn mock_execution_layer_from_parts<E: EthSpec>( MockExecutionLayer::new( task_executor, - DEFAULT_TERMINAL_BLOCK, shanghai_time, cancun_time, prague_time, @@ -818,7 +828,11 @@ where } pub fn get_full_block(&self, block_root: &Hash256) -> RpcBlock<E> { - let block = self.chain.get_blinded_block(block_root).unwrap().unwrap(); + let block = self + .chain + .get_blinded_block(block_root) + .unwrap() + .unwrap_or_else(|| panic!("block root does not exist in harness {block_root:?}")); let full_block = self.chain.store.make_full_block(block_root, block).unwrap(); self.build_rpc_block_from_store_blobs(Some(*block_root), Arc::new(full_block)) } @@ -1174,6 +1188,94 @@ where ) } + /// Build a Bellatrix block with the given execution payload, compute the + /// correct state root, sign it, and import it into the chain. + /// + /// This bypasses the normal block production pipeline, which always requests + /// a payload from the execution layer. That makes it possible to construct + /// blocks with **default (zeroed) payloads** — something the EL-backed flow + /// cannot do — which is needed to simulate the pre-merge portion of a chain + /// that starts at Bellatrix genesis with `is_merge_transition_complete = false`. + /// + /// `state` is expected to be the head state *before* `slot`. It will be + /// advanced to `slot` in-place via `complete_state_advance`, then used to + /// derive the proposer, RANDAO reveal, and parent root. After processing, + /// the caller should typically replace `state` with the chain's new head + /// state (`self.get_current_state()`). + pub async fn build_and_import_block_with_payload( + &self, + state: &mut BeaconState<E>, + slot: Slot, + execution_payload: ExecutionPayloadBellatrix<E>, + ) { + complete_state_advance(state, None, slot, &self.spec).expect("should advance state"); + state.build_caches(&self.spec).expect("should build caches"); + + let proposer_index = state.get_beacon_proposer_index(slot, &self.spec).unwrap(); + let randao_reveal = self.sign_randao_reveal(state, proposer_index, slot); + let parent_root = state.latest_block_header().canonical_root(); + + let mut block = BeaconBlock::Bellatrix(BeaconBlockBellatrix { + slot, + proposer_index: proposer_index as u64, + parent_root, + state_root: Hash256::zero(), + body: BeaconBlockBodyBellatrix { + randao_reveal, + eth1_data: state.eth1_data().clone(), + graffiti: Graffiti::default(), + proposer_slashings: VariableList::empty(), + attester_slashings: VariableList::empty(), + attestations: VariableList::empty(), + deposits: VariableList::empty(), + voluntary_exits: VariableList::empty(), + sync_aggregate: SyncAggregate::new(), + execution_payload: FullPayloadBellatrix { execution_payload }, + }, + }); + + // Run per_block_processing on a clone to compute the post-state root. + let signed_tmp = block.clone().sign( + &self.validator_keypairs[proposer_index].sk, + &state.fork(), + state.genesis_validators_root(), + &self.spec, + ); + let mut ctxt = ConsensusContext::new(slot).set_proposer_index(proposer_index as u64); + let mut post_state = state.clone(); + per_block_processing( + &mut post_state, + &signed_tmp, + BlockSignatureStrategy::NoVerification, + VerifyBlockRoot::False, + &mut ctxt, + &self.spec, + ) + .unwrap_or_else(|e| panic!("per_block_processing failed at slot {}: {e:?}", slot)); + + let state_root = post_state.update_tree_hash_cache().unwrap(); + *block.state_root_mut() = state_root; + + let signed_block = self.sign_beacon_block(block, state); + let block_root = signed_block.canonical_root(); + let rpc_block = RpcBlock::BlockOnly { + block_root, + block: Arc::new(signed_block), + }; + self.chain.slot_clock.set_slot(slot.as_u64()); + self.chain + .process_block( + block_root, + rpc_block, + NotifyExecutionLayer::No, + BlockImportSource::Lookup, + || Ok(()), + ) + .await + .unwrap_or_else(|e| panic!("import failed at slot {}: {e:?}", slot)); + self.chain.recompute_head_at_current_slot().await; + } + #[allow(clippy::too_many_arguments)] pub fn produce_single_attestation_for_block( &self, diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs index 208798dfdf..acf326430b 100644 --- a/beacon_node/beacon_chain/tests/attestation_verification.rs +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -1,4 +1,5 @@ #![cfg(not(debug_assertions))] +#![allow(clippy::result_large_err)] use beacon_chain::attestation_verification::{ Error, batch_verify_aggregated_attestations, batch_verify_unaggregated_attestations, @@ -14,6 +15,7 @@ use beacon_chain::{ }, }; use bls::{AggregateSignature, Keypair, SecretKey}; +use execution_layer::test_utils::generate_genesis_header; use fixed_bytes::FixedBytesExtended; use genesis::{DEFAULT_ETH1_BLOCK_HASH, interop_genesis_state}; use int_to_bytes::int_to_bytes32; @@ -54,7 +56,7 @@ fn get_harness(validator_count: usize) -> BeaconChainHarness<EphemeralHarnessTyp let harness = BeaconChainHarness::builder(MainnetEthSpec) .spec(spec) .chain_config(ChainConfig { - reconstruct_historic_states: true, + archive: true, ..ChainConfig::default() }) .keypairs(KEYPAIRS[0..validator_count].to_vec()) @@ -79,11 +81,13 @@ fn get_harness_capella_spec( let spec = Arc::new(spec); let validator_keypairs = KEYPAIRS[0..validator_count].to_vec(); + // Use the proper genesis execution payload header that matches the mock execution layer + let execution_payload_header = generate_genesis_header(&spec); let genesis_state = interop_genesis_state( &validator_keypairs, HARNESS_GENESIS_TIME, Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH), - None, + execution_payload_header, &spec, ) .unwrap(); @@ -91,7 +95,7 @@ fn get_harness_capella_spec( let harness = BeaconChainHarness::builder(MainnetEthSpec) .spec(spec.clone()) .chain_config(ChainConfig { - reconstruct_historic_states: true, + archive: true, ..ChainConfig::default() }) .keypairs(validator_keypairs) @@ -106,11 +110,6 @@ fn get_harness_capella_spec( .mock_execution_layer() .build(); - harness - .execution_block_generator() - .move_to_terminal_block() - .unwrap(); - harness.advance_slot(); (harness, spec) @@ -368,6 +367,13 @@ impl GossipTester { self.harness.chain.epoch().unwrap() } + pub fn is_gloas(&self) -> bool { + self.harness + .spec + .fork_name_at_slot::<E>(self.valid_attestation.data.slot) + .gloas_enabled() + } + pub fn earliest_valid_attestation_slot(&self) -> Slot { let offset = if self .harness @@ -522,6 +528,44 @@ impl GossipTester { self } + + /// Like `inspect_aggregate_err`, but only runs the check if gloas is enabled. + /// If gloas is not enabled, this is a no-op that returns self. + pub fn inspect_aggregate_err_if_gloas<G, I>( + self, + desc: &str, + get_attn: G, + inspect_err: I, + ) -> Self + where + G: Fn(&Self, &mut SignedAggregateAndProof<E>), + I: Fn(&Self, AttnError), + { + if self.is_gloas() { + self.inspect_aggregate_err(desc, get_attn, inspect_err) + } else { + self + } + } + + /// Like `inspect_unaggregate_err`, but only runs the check if gloas is enabled. + /// If gloas is not enabled, this is a no-op that returns self. + pub fn inspect_unaggregate_err_if_gloas<G, I>( + self, + desc: &str, + get_attn: G, + inspect_err: I, + ) -> Self + where + G: Fn(&Self, &mut SingleAttestation, &mut SubnetId, &ChainSpec), + I: Fn(&Self, AttnError), + { + if self.is_gloas() { + self.inspect_unaggregate_err(desc, get_attn, inspect_err) + } else { + self + } + } } /// Tests verification of `SignedAggregateAndProof` from the gossip network. #[tokio::test] @@ -854,6 +898,27 @@ async fn aggregated_gossip_verification() { )) }, ) + /* + * [New in Gloas]: attestation.data.index must be < 2 + */ + .inspect_aggregate_err_if_gloas( + "gloas: aggregate with index >= 2", + |_, a| match a.to_mut() { + SignedAggregateAndProofRefMut::Base(_) => { + panic!("Expected Electra attestation variant"); + } + SignedAggregateAndProofRefMut::Electra(att) => { + att.message.aggregate.data.index = 2; + } + }, + |_, err| { + assert!( + matches!(err, AttnError::CommitteeIndexInvalid), + "expected CommitteeIndexInvalid, got {:?}", + err + ) + }, + ) // NOTE: from here on, the tests are stateful, and rely on the valid attestation having // been seen. .import_valid_aggregate() @@ -1071,6 +1136,22 @@ async fn unaggregated_gossip_verification() { )) }, ) + /* + * [New in Gloas]: attestation.data.index must be < 2 + */ + .inspect_unaggregate_err_if_gloas( + "gloas: attestation with index >= 2", + |_, a, _, _| { + a.data.index = 2; + }, + |_, err| { + assert!( + matches!(err, AttnError::CommitteeIndexInvalid), + "expected CommitteeIndexInvalid, got {:?}", + err + ) + }, + ) // NOTE: from here on, the tests are stateful, and rely on the valid attestation having // been seen. .import_valid_unaggregate() @@ -1700,3 +1781,180 @@ async fn aggregated_attestation_verification_use_head_state_fork() { ); } } + +/// [New in Gloas]: Tests that unaggregated attestations with `data.index == 1` are rejected +/// when `head_block.slot == attestation.data.slot`. +/// +/// This test only runs when `FORK_NAME=gloas` is set with `fork_from_env` feature. +// TODO(EIP-7732): Enable this test once gloas block production works in test harness. +// `state.latest_execution_payload_header()` not available in Gloas. +#[ignore] +#[tokio::test] +async fn gloas_unaggregated_attestation_same_slot_index_must_be_zero() { + let harness = get_harness(VALIDATOR_COUNT); + + // Skip this test if not running with gloas fork + if !harness + .spec + .fork_name_at_epoch(Epoch::new(0)) + .gloas_enabled() + { + return; + } + + // Extend the chain out a few epochs so we have some chain depth to play with. + harness + .extend_chain( + MainnetEthSpec::slots_per_epoch() as usize * 3 - 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Produce a block in the current slot (this creates the same-slot scenario) + harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::SomeValidators(vec![]), + ) + .await; + + let current_slot = harness.chain.slot().expect("should get slot"); + let head = harness.chain.head_snapshot(); + + // Verify head block is in the current slot + assert_eq!( + head.beacon_block.slot(), + current_slot, + "head block should be in current slot for same-slot test" + ); + + // Produce an attestation for the current slot + let (mut attestation, _attester_sk, subnet_id) = + get_valid_unaggregated_attestation(&harness.chain); + + // Verify we have a same-slot scenario + let attested_block_slot = harness + .chain + .canonical_head + .fork_choice_read_lock() + .get_block(&attestation.data.beacon_block_root) + .expect("block should exist") + .slot; + assert_eq!( + attested_block_slot, attestation.data.slot, + "attested block slot should equal attestation slot for same-slot test" + ); + + // index == 1 should be rejected when head_block.slot == attestation.data.slot + attestation.data.index = 1; + let result = harness + .chain + .verify_unaggregated_attestation_for_gossip(&attestation, Some(subnet_id)); + assert!( + matches!(result, Err(AttnError::CommitteeIndexNonZero(_))), + "gloas: attestation with index == 1 when head_block.slot == attestation.data.slot should be rejected, got {:?}", + result.err() + ); +} + +/// [New in Gloas]: Tests that aggregated attestations with `data.index == 1` are rejected +/// when `head_block.slot == attestation.data.slot`. +/// +/// This test only runs when `FORK_NAME=gloas` is set with `fork_from_env` feature. +// TODO(EIP-7732): Enable this test once gloas block production works in test harness. +// `state.latest_execution_payload_header()` not available in Gloas. +#[ignore] +#[tokio::test] +async fn gloas_aggregated_attestation_same_slot_index_must_be_zero() { + let harness = get_harness(VALIDATOR_COUNT); + + // Skip this test if not running with gloas fork + if !harness + .spec + .fork_name_at_epoch(Epoch::new(0)) + .gloas_enabled() + { + return; + } + + // Extend the chain out a few epochs so we have some chain depth to play with. + harness + .extend_chain( + MainnetEthSpec::slots_per_epoch() as usize * 3 - 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Produce a block in the current slot (this creates the same-slot scenario) + harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::SomeValidators(vec![]), + ) + .await; + + let current_slot = harness.chain.slot().expect("should get slot"); + let head = harness.chain.head_snapshot(); + + // Verify head block is in the current slot + assert_eq!( + head.beacon_block.slot(), + current_slot, + "head block should be in current slot for same-slot test" + ); + + // Produce an attestation for the current slot + let (valid_attestation, _attester_sk, _subnet_id) = + get_valid_unaggregated_attestation(&harness.chain); + + // Verify we have a same-slot scenario + let attested_block_slot = harness + .chain + .canonical_head + .fork_choice_read_lock() + .get_block(&valid_attestation.data.beacon_block_root) + .expect("block should exist") + .slot; + assert_eq!( + attested_block_slot, valid_attestation.data.slot, + "attested block slot should equal attestation slot for same-slot test" + ); + + // Convert to aggregate + let committee = head + .beacon_state + .get_beacon_committee(current_slot, valid_attestation.committee_index) + .expect("should get committee"); + let fork_name = harness + .spec + .fork_name_at_slot::<E>(valid_attestation.data.slot); + let aggregate_attestation = + single_attestation_to_attestation(&valid_attestation, committee.committee, fork_name) + .unwrap(); + + let (mut valid_aggregate, _, _) = + get_valid_aggregated_attestation(&harness.chain, aggregate_attestation); + + // index == 1 should be rejected when head_block.slot == attestation.data.slot + match valid_aggregate.to_mut() { + SignedAggregateAndProofRefMut::Base(att) => { + att.message.aggregate.data.index = 1; + } + SignedAggregateAndProofRefMut::Electra(att) => { + att.message.aggregate.data.index = 1; + } + } + + let result = harness + .chain + .verify_aggregated_attestation_for_gossip(&valid_aggregate); + assert!( + matches!(result, Err(AttnError::CommitteeIndexNonZero(_))), + "gloas: aggregate with index == 1 when head_block.slot == attestation.data.slot should be rejected, got {:?}", + result.err() + ); +} diff --git a/beacon_node/beacon_chain/tests/bellatrix.rs b/beacon_node/beacon_chain/tests/bellatrix.rs deleted file mode 100644 index fc0f96ef88..0000000000 --- a/beacon_node/beacon_chain/tests/bellatrix.rs +++ /dev/null @@ -1,212 +0,0 @@ -#![cfg(not(debug_assertions))] // Tests run too slow in debug. - -use beacon_chain::test_utils::BeaconChainHarness; -use execution_layer::test_utils::{Block, DEFAULT_TERMINAL_BLOCK, generate_pow_block}; -use types::*; - -const VALIDATOR_COUNT: usize = 32; - -type E = MainnetEthSpec; - -fn verify_execution_payload_chain<E: EthSpec>(chain: &[FullPayload<E>]) { - let mut prev_ep: Option<FullPayload<E>> = None; - - for ep in chain { - assert!(!ep.is_default_with_empty_roots()); - assert!(ep.block_hash() != ExecutionBlockHash::zero()); - - // Check against previous `ExecutionPayload`. - if let Some(prev_ep) = prev_ep { - assert_eq!(prev_ep.block_hash(), ep.parent_hash()); - assert_eq!(prev_ep.block_number() + 1, ep.block_number()); - assert!(ep.timestamp() > prev_ep.timestamp()); - } - prev_ep = Some(ep.clone()); - } -} - -#[tokio::test] -// TODO(merge): This isn't working cause the non-zero values in `initialize_beacon_state_from_eth1` -// are causing failed lookups to the execution node. I need to come back to this. -#[should_panic] -async fn merge_with_terminal_block_hash_override() { - let altair_fork_epoch = Epoch::new(0); - let bellatrix_fork_epoch = Epoch::new(0); - - let mut spec = E::default_spec(); - spec.altair_fork_epoch = Some(altair_fork_epoch); - spec.bellatrix_fork_epoch = Some(bellatrix_fork_epoch); - - let genesis_pow_block_hash = generate_pow_block( - spec.terminal_total_difficulty, - DEFAULT_TERMINAL_BLOCK, - 0, - ExecutionBlockHash::zero(), - ) - .unwrap() - .block_hash; - - spec.terminal_block_hash = genesis_pow_block_hash; - - let harness = BeaconChainHarness::builder(E::default()) - .spec(spec.into()) - .deterministic_keypairs(VALIDATOR_COUNT) - .fresh_ephemeral_store() - .mock_execution_layer() - .build(); - - assert_eq!( - harness - .execution_block_generator() - .latest_block() - .unwrap() - .block_hash(), - genesis_pow_block_hash, - "pre-condition" - ); - - assert!( - harness - .chain - .head_snapshot() - .beacon_block - .as_bellatrix() - .is_ok(), - "genesis block should be a bellatrix block" - ); - - let mut execution_payloads = vec![]; - for i in 0..E::slots_per_epoch() * 3 { - harness.extend_slots(1).await; - - let block = &harness.chain.head_snapshot().beacon_block; - - let execution_payload = block.message().body().execution_payload().unwrap(); - if i == 0 { - assert_eq!(execution_payload.block_hash(), genesis_pow_block_hash); - } - execution_payloads.push(execution_payload.into()); - } - - verify_execution_payload_chain(execution_payloads.as_slice()); -} - -#[tokio::test] -async fn base_altair_bellatrix_with_terminal_block_after_fork() { - let altair_fork_epoch = Epoch::new(4); - let altair_fork_slot = altair_fork_epoch.start_slot(E::slots_per_epoch()); - let bellatrix_fork_epoch = Epoch::new(8); - let bellatrix_fork_slot = bellatrix_fork_epoch.start_slot(E::slots_per_epoch()); - - let mut spec = E::default_spec(); - spec.altair_fork_epoch = Some(altair_fork_epoch); - spec.bellatrix_fork_epoch = Some(bellatrix_fork_epoch); - - let mut execution_payloads = vec![]; - - let harness = BeaconChainHarness::builder(E::default()) - .spec(spec.into()) - .deterministic_keypairs(VALIDATOR_COUNT) - .fresh_ephemeral_store() - .mock_execution_layer() - .build(); - - /* - * Start with the base fork. - */ - - assert!(harness.chain.head_snapshot().beacon_block.as_base().is_ok()); - - /* - * Do the Altair fork. - */ - - harness.extend_to_slot(altair_fork_slot).await; - - let altair_head = &harness.chain.head_snapshot().beacon_block; - assert!(altair_head.as_altair().is_ok()); - assert_eq!(altair_head.slot(), altair_fork_slot); - - /* - * Do the Bellatrix fork, without a terminal PoW block. - */ - - Box::pin(harness.extend_to_slot(bellatrix_fork_slot)).await; - - let bellatrix_head = &harness.chain.head_snapshot().beacon_block; - assert!(bellatrix_head.as_bellatrix().is_ok()); - assert_eq!(bellatrix_head.slot(), bellatrix_fork_slot); - assert!( - bellatrix_head - .message() - .body() - .execution_payload() - .unwrap() - .is_default_with_empty_roots(), - "Bellatrix head is default payload" - ); - - /* - * Next Bellatrix block shouldn't include an exec payload. - */ - - harness.extend_slots(1).await; - - let one_after_bellatrix_head = &harness.chain.head_snapshot().beacon_block; - assert!( - one_after_bellatrix_head - .message() - .body() - .execution_payload() - .unwrap() - .is_default_with_empty_roots(), - "One after bellatrix head is default payload" - ); - assert_eq!(one_after_bellatrix_head.slot(), bellatrix_fork_slot + 1); - - /* - * Trigger the terminal PoW block. - */ - - harness - .execution_block_generator() - .move_to_terminal_block() - .unwrap(); - - // Add a slot duration to get to the next slot - let timestamp = harness.get_timestamp_at_slot() + harness.spec.get_slot_duration().as_secs(); - - harness - .execution_block_generator() - .modify_last_block(|block| { - if let Block::PoW(terminal_block) = block { - terminal_block.timestamp = timestamp; - } - }); - - harness.extend_slots(1).await; - - let two_after_bellatrix_head = &harness.chain.head_snapshot().beacon_block; - assert!( - two_after_bellatrix_head - .message() - .body() - .execution_payload() - .unwrap() - .is_default_with_empty_roots(), - "Two after bellatrix head is default payload" - ); - assert_eq!(two_after_bellatrix_head.slot(), bellatrix_fork_slot + 2); - - /* - * Next Bellatrix block should include an exec payload. - */ - for _ in 0..4 { - harness.extend_slots(1).await; - - let block = &harness.chain.head_snapshot().beacon_block; - execution_payloads.push(block.message().body().execution_payload().unwrap().into()); - } - - verify_execution_payload_chain(execution_payloads.as_slice()); -} diff --git a/beacon_node/beacon_chain/tests/blob_verification.rs b/beacon_node/beacon_chain/tests/blob_verification.rs index e39c53729f..ee61177b2a 100644 --- a/beacon_node/beacon_chain/tests/blob_verification.rs +++ b/beacon_node/beacon_chain/tests/blob_verification.rs @@ -29,7 +29,7 @@ fn get_harness( let harness = BeaconChainHarness::builder(MainnetEthSpec) .spec(spec) .chain_config(ChainConfig { - reconstruct_historic_states: true, + archive: true, ..ChainConfig::default() }) .keypairs(KEYPAIRS[0..validator_count].to_vec()) diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index b6cac025a0..bace5ea9ca 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -1,5 +1,5 @@ #![cfg(not(debug_assertions))] - +// TODO(gloas) we probably need similar test for payload envelope verification use beacon_chain::block_verification_types::{AsBlock, ExecutedBlock, RpcBlock}; use beacon_chain::data_availability_checker::{AvailabilityCheckError, AvailableBlockData}; use beacon_chain::data_column_verification::CustodyDataColumn; @@ -20,10 +20,9 @@ use fixed_bytes::FixedBytesExtended; use logging::create_test_tracing_subscriber; use slasher::{Config as SlasherConfig, Slasher}; use state_processing::{ - BlockProcessingError, ConsensusContext, VerifyBlockRoot, + BlockProcessingError, BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, common::{attesting_indices_base, attesting_indices_electra}, - per_block_processing::{BlockSignatureStrategy, per_block_processing}, - per_slot_processing, + per_block_processing, per_slot_processing, }; use std::marker::PhantomData; use std::sync::{Arc, LazyLock}; @@ -119,7 +118,7 @@ fn get_harness( let harness = BeaconChainHarness::builder(MainnetEthSpec) .default_spec() .chain_config(ChainConfig { - reconstruct_historic_states: true, + archive: true, ..ChainConfig::default() }) .keypairs(KEYPAIRS[0..validator_count].to_vec()) @@ -1849,10 +1848,8 @@ async fn add_altair_block_to_base_chain() { // https://github.com/sigp/lighthouse/issues/4332#issuecomment-1565092279 #[tokio::test] async fn import_duplicate_block_unrealized_justification() { - let spec = MainnetEthSpec::default_spec(); - let harness = BeaconChainHarness::builder(MainnetEthSpec) - .spec(spec.into()) + .default_spec() .keypairs(KEYPAIRS[..].to_vec()) .fresh_ephemeral_store() .mock_execution_layer() diff --git a/beacon_node/beacon_chain/tests/capella.rs b/beacon_node/beacon_chain/tests/capella.rs deleted file mode 100644 index e8ab795366..0000000000 --- a/beacon_node/beacon_chain/tests/capella.rs +++ /dev/null @@ -1,156 +0,0 @@ -#![cfg(not(debug_assertions))] // Tests run too slow in debug. - -use beacon_chain::test_utils::BeaconChainHarness; -use execution_layer::test_utils::Block; -use types::*; - -const VALIDATOR_COUNT: usize = 32; -type E = MainnetEthSpec; - -fn verify_execution_payload_chain<E: EthSpec>(chain: &[FullPayload<E>]) { - let mut prev_ep: Option<FullPayload<E>> = None; - - for ep in chain { - assert!(!ep.is_default_with_empty_roots()); - assert!(ep.block_hash() != ExecutionBlockHash::zero()); - - // Check against previous `ExecutionPayload`. - if let Some(prev_ep) = prev_ep { - assert_eq!(prev_ep.block_hash(), ep.parent_hash()); - assert_eq!(prev_ep.block_number() + 1, ep.block_number()); - assert!(ep.timestamp() > prev_ep.timestamp()); - } - prev_ep = Some(ep.clone()); - } -} - -#[tokio::test] -async fn base_altair_bellatrix_capella() { - let altair_fork_epoch = Epoch::new(4); - let altair_fork_slot = altair_fork_epoch.start_slot(E::slots_per_epoch()); - let bellatrix_fork_epoch = Epoch::new(8); - let bellatrix_fork_slot = bellatrix_fork_epoch.start_slot(E::slots_per_epoch()); - let capella_fork_epoch = Epoch::new(12); - let capella_fork_slot = capella_fork_epoch.start_slot(E::slots_per_epoch()); - - let mut spec = E::default_spec(); - spec.altair_fork_epoch = Some(altair_fork_epoch); - spec.bellatrix_fork_epoch = Some(bellatrix_fork_epoch); - spec.capella_fork_epoch = Some(capella_fork_epoch); - - let harness = BeaconChainHarness::builder(E::default()) - .spec(spec.into()) - .deterministic_keypairs(VALIDATOR_COUNT) - .fresh_ephemeral_store() - .mock_execution_layer() - .build(); - - /* - * Start with the base fork. - */ - assert!(harness.chain.head_snapshot().beacon_block.as_base().is_ok()); - - /* - * Do the Altair fork. - */ - Box::pin(harness.extend_to_slot(altair_fork_slot)).await; - - let altair_head = &harness.chain.head_snapshot().beacon_block; - assert!(altair_head.as_altair().is_ok()); - assert_eq!(altair_head.slot(), altair_fork_slot); - - /* - * Do the Bellatrix fork, without a terminal PoW block. - */ - Box::pin(harness.extend_to_slot(bellatrix_fork_slot)).await; - - let bellatrix_head = &harness.chain.head_snapshot().beacon_block; - assert!(bellatrix_head.as_bellatrix().is_ok()); - assert_eq!(bellatrix_head.slot(), bellatrix_fork_slot); - assert!( - bellatrix_head - .message() - .body() - .execution_payload() - .unwrap() - .is_default_with_empty_roots(), - "Bellatrix head is default payload" - ); - - /* - * Next Bellatrix block shouldn't include an exec payload. - */ - Box::pin(harness.extend_slots(1)).await; - - let one_after_bellatrix_head = &harness.chain.head_snapshot().beacon_block; - assert!( - one_after_bellatrix_head - .message() - .body() - .execution_payload() - .unwrap() - .is_default_with_empty_roots(), - "One after bellatrix head is default payload" - ); - assert_eq!(one_after_bellatrix_head.slot(), bellatrix_fork_slot + 1); - - /* - * Trigger the terminal PoW block. - */ - harness - .execution_block_generator() - .move_to_terminal_block() - .unwrap(); - - // Add a slot duration to get to the next slot - let timestamp = harness.get_timestamp_at_slot() + harness.spec.get_slot_duration().as_secs(); - harness - .execution_block_generator() - .modify_last_block(|block| { - if let Block::PoW(terminal_block) = block { - terminal_block.timestamp = timestamp; - } - }); - Box::pin(harness.extend_slots(1)).await; - - let two_after_bellatrix_head = &harness.chain.head_snapshot().beacon_block; - assert!( - two_after_bellatrix_head - .message() - .body() - .execution_payload() - .unwrap() - .is_default_with_empty_roots(), - "Two after bellatrix head is default payload" - ); - assert_eq!(two_after_bellatrix_head.slot(), bellatrix_fork_slot + 2); - - /* - * Next Bellatrix block should include an exec payload. - */ - let mut execution_payloads = vec![]; - for _ in (bellatrix_fork_slot.as_u64() + 3)..capella_fork_slot.as_u64() { - harness.extend_slots(1).await; - let block = &harness.chain.head_snapshot().beacon_block; - let full_payload: FullPayload<E> = - block.message().body().execution_payload().unwrap().into(); - // pre-capella shouldn't have withdrawals - assert!(full_payload.withdrawals_root().is_err()); - execution_payloads.push(full_payload); - } - - /* - * Should enter capella fork now. - */ - for _ in 0..16 { - harness.extend_slots(1).await; - let block = &harness.chain.head_snapshot().beacon_block; - let full_payload: FullPayload<E> = - block.message().body().execution_payload().unwrap().into(); - // post-capella should have withdrawals - assert!(full_payload.withdrawals_root().is_ok()); - execution_payloads.push(full_payload); - } - - verify_execution_payload_chain(execution_payloads.as_slice()); -} diff --git a/beacon_node/beacon_chain/tests/column_verification.rs b/beacon_node/beacon_chain/tests/column_verification.rs index ca9893941a..9941c957e2 100644 --- a/beacon_node/beacon_chain/tests/column_verification.rs +++ b/beacon_node/beacon_chain/tests/column_verification.rs @@ -32,7 +32,7 @@ fn get_harness( let harness = BeaconChainHarness::builder(MainnetEthSpec) .spec(spec) .chain_config(ChainConfig { - reconstruct_historic_states: true, + archive: true, ..ChainConfig::default() }) .keypairs(KEYPAIRS[0..validator_count].to_vec()) diff --git a/beacon_node/beacon_chain/tests/events.rs b/beacon_node/beacon_chain/tests/events.rs index 92727ffd76..121f8c255d 100644 --- a/beacon_node/beacon_chain/tests/events.rs +++ b/beacon_node/beacon_chain/tests/events.rs @@ -115,7 +115,7 @@ async fn data_column_sidecar_event_on_process_gossip_data_column() { /// Verifies that a blob event is emitted when blobs are received via RPC. #[tokio::test] async fn blob_sidecar_event_on_process_rpc_blobs() { - if fork_name_from_env().is_some_and(|f| !f.deneb_enabled() || f.fulu_enabled()) { + if fork_name_from_env().is_none_or(|f| !f.deneb_enabled() || f.fulu_enabled()) { return; }; @@ -170,7 +170,7 @@ async fn blob_sidecar_event_on_process_rpc_blobs() { #[tokio::test] async fn data_column_sidecar_event_on_process_rpc_columns() { - if fork_name_from_env().is_some_and(|f| !f.fulu_enabled()) { + if fork_name_from_env().is_none_or(|f| !f.fulu_enabled()) { return; }; diff --git a/beacon_node/beacon_chain/tests/main.rs b/beacon_node/beacon_chain/tests/main.rs index aec4416419..e02c488ac6 100644 --- a/beacon_node/beacon_chain/tests/main.rs +++ b/beacon_node/beacon_chain/tests/main.rs @@ -1,9 +1,7 @@ mod attestation_production; mod attestation_verification; -mod bellatrix; mod blob_verification; mod block_verification; -mod capella; mod column_verification; mod events; mod op_verification; diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index b3a8b80e9b..c340400596 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -1,12 +1,13 @@ #![cfg(not(debug_assertions))] +#![allow(clippy::result_large_err)] use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::{ BeaconChainError, BlockError, ChainConfig, ExecutionPayloadError, - INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, NotifyExecutionLayer, OverrideForkchoiceUpdate, - StateSkipConfig, WhenSlotSkipped, + INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, NotifyExecutionLayer, StateSkipConfig, + WhenSlotSkipped, canonical_head::{CachedHead, CanonicalHead}, - test_utils::{BeaconChainHarness, EphemeralHarnessType}, + test_utils::{BeaconChainHarness, EphemeralHarnessType, fork_name_from_env, test_spec}, }; use execution_layer::{ ExecutionLayer, ForkchoiceState, PayloadAttributes, @@ -42,18 +43,15 @@ struct InvalidPayloadRig { impl InvalidPayloadRig { fn new() -> Self { - let spec = E::default_spec(); + let spec = test_spec::<E>(); Self::new_with_spec(spec) } - fn new_with_spec(mut spec: ChainSpec) -> Self { - spec.altair_fork_epoch = Some(Epoch::new(0)); - spec.bellatrix_fork_epoch = Some(Epoch::new(0)); - + fn new_with_spec(spec: ChainSpec) -> Self { let harness = BeaconChainHarness::builder(MainnetEthSpec) .spec(spec.into()) .chain_config(ChainConfig { - reconstruct_historic_states: true, + archive: true, ..ChainConfig::default() }) .deterministic_keypairs(VALIDATOR_COUNT) @@ -141,25 +139,6 @@ impl InvalidPayloadRig { payload_attributes } - fn move_to_terminal_block(&self) { - let mock_execution_layer = self.harness.mock_execution_layer.as_ref().unwrap(); - mock_execution_layer - .server - .execution_block_generator() - .move_to_terminal_block() - .unwrap(); - } - - fn latest_execution_block_hash(&self) -> ExecutionBlockHash { - let mock_execution_layer = self.harness.mock_execution_layer.as_ref().unwrap(); - mock_execution_layer - .server - .execution_block_generator() - .latest_execution_block() - .unwrap() - .block_hash - } - async fn build_blocks(&mut self, num_blocks: u64, is_valid: Payload) -> Vec<Hash256> { let mut roots = Vec::with_capacity(num_blocks as usize); for _ in 0..num_blocks { @@ -392,8 +371,10 @@ impl InvalidPayloadRig { /// Simple test of the different import types. #[tokio::test] async fn valid_invalid_syncing() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let mut rig = InvalidPayloadRig::new(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; rig.import_block(Payload::Invalid { @@ -407,8 +388,10 @@ async fn valid_invalid_syncing() { /// `latest_valid_hash`. #[tokio::test] async fn invalid_payload_invalidates_parent() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. rig.move_to_first_justification(Payload::Syncing).await; @@ -440,7 +423,6 @@ async fn immediate_forkchoice_update_invalid_test( invalid_payload: impl FnOnce(Option<ExecutionBlockHash>) -> Payload, ) { let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. rig.move_to_first_justification(Payload::Syncing).await; @@ -463,6 +445,9 @@ async fn immediate_forkchoice_update_invalid_test( #[tokio::test] async fn immediate_forkchoice_update_payload_invalid() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } immediate_forkchoice_update_invalid_test(|latest_valid_hash| Payload::Invalid { latest_valid_hash, }) @@ -471,11 +456,17 @@ async fn immediate_forkchoice_update_payload_invalid() { #[tokio::test] async fn immediate_forkchoice_update_payload_invalid_block_hash() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } immediate_forkchoice_update_invalid_test(|_| Payload::InvalidBlockHash).await } #[tokio::test] async fn immediate_forkchoice_update_payload_invalid_terminal_block() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } immediate_forkchoice_update_invalid_test(|_| Payload::Invalid { latest_valid_hash: Some(ExecutionBlockHash::zero()), }) @@ -485,8 +476,10 @@ async fn immediate_forkchoice_update_payload_invalid_terminal_block() { /// Ensure the client tries to exit when the justified checkpoint is invalidated. #[tokio::test] async fn justified_checkpoint_becomes_invalid() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. rig.move_to_first_justification(Payload::Syncing).await; @@ -527,11 +520,13 @@ async fn justified_checkpoint_becomes_invalid() { /// Ensure that a `latest_valid_hash` for a pre-finality block only reverts a single block. #[tokio::test] async fn pre_finalized_latest_valid_hash() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let num_blocks = E::slots_per_epoch() * 4; let finalized_epoch = 2; let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); let mut blocks = vec![]; blocks.push(rig.import_block(Payload::Valid).await); // Import a valid transition block. blocks.extend(rig.build_blocks(num_blocks - 1, Payload::Syncing).await); @@ -574,10 +569,12 @@ async fn pre_finalized_latest_valid_hash() { /// - Will not validate `latest_valid_root` and its ancestors. #[tokio::test] async fn latest_valid_hash_will_not_validate() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } const LATEST_VALID_SLOT: u64 = 3; let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); let mut blocks = vec![]; blocks.push(rig.import_block(Payload::Valid).await); // Import a valid transition block. @@ -621,11 +618,13 @@ async fn latest_valid_hash_will_not_validate() { /// Check behaviour when the `latest_valid_hash` is a junk value. #[tokio::test] async fn latest_valid_hash_is_junk() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let num_blocks = E::slots_per_epoch() * 5; let finalized_epoch = 3; let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); let mut blocks = vec![]; blocks.push(rig.import_block(Payload::Valid).await); // Import a valid transition block. blocks.extend(rig.build_blocks(num_blocks, Payload::Syncing).await); @@ -662,12 +661,14 @@ async fn latest_valid_hash_is_junk() { /// Check that descendants of invalid blocks are also invalidated. #[tokio::test] async fn invalidates_all_descendants() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let num_blocks = E::slots_per_epoch() * 4 + E::slots_per_epoch() / 2; let finalized_epoch = 2; let finalized_slot = E::slots_per_epoch() * 2; let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. let blocks = rig.build_blocks(num_blocks, Payload::Syncing).await; @@ -769,12 +770,14 @@ async fn invalidates_all_descendants() { /// Check that the head will switch after the canonical branch is invalidated. #[tokio::test] async fn switches_heads() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let num_blocks = E::slots_per_epoch() * 4 + E::slots_per_epoch() / 2; let finalized_epoch = 2; let finalized_slot = E::slots_per_epoch() * 2; let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. let blocks = rig.build_blocks(num_blocks, Payload::Syncing).await; @@ -872,8 +875,10 @@ async fn switches_heads() { #[tokio::test] async fn invalid_during_processing() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let mut rig = InvalidPayloadRig::new(); - rig.move_to_terminal_block(); let roots = &[ rig.import_block(Payload::Valid).await, @@ -904,8 +909,10 @@ async fn invalid_during_processing() { #[tokio::test] async fn invalid_after_optimistic_sync() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. let mut roots = vec![ @@ -942,8 +949,10 @@ async fn invalid_after_optimistic_sync() { #[tokio::test] async fn manually_validate_child() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. let parent = rig.import_block(Payload::Syncing).await; @@ -960,8 +969,10 @@ async fn manually_validate_child() { #[tokio::test] async fn manually_validate_parent() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. let parent = rig.import_block(Payload::Syncing).await; @@ -978,8 +989,10 @@ async fn manually_validate_parent() { #[tokio::test] async fn payload_preparation() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let mut rig = InvalidPayloadRig::new(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; let el = rig.execution_layer(); @@ -1039,8 +1052,10 @@ async fn payload_preparation() { #[tokio::test] async fn invalid_parent() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let mut rig = InvalidPayloadRig::new(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. // Import a syncing block atop the transition block (we'll call this the "parent block" since we @@ -1108,83 +1123,12 @@ async fn invalid_parent() { )); } -/// Tests to ensure that we will still send a proposer preparation -#[tokio::test] -async fn payload_preparation_before_transition_block() { - let rig = InvalidPayloadRig::new(); - let el = rig.execution_layer(); - - // Run the watchdog routine so that the status of the execution engine is set. This ensures - // that we don't end up with `eth_syncing` requests later in this function that will impede - // testing. - el.watchdog_task().await; - - let head = rig.harness.chain.head_snapshot(); - assert_eq!( - head.beacon_block - .message() - .body() - .execution_payload() - .unwrap() - .block_hash(), - ExecutionBlockHash::zero(), - "the head block is post-bellatrix but pre-transition" - ); - - let current_slot = rig.harness.chain.slot().unwrap(); - let next_slot = current_slot + 1; - let proposer = head - .beacon_state - .get_beacon_proposer_index(next_slot, &rig.harness.chain.spec) - .unwrap(); - let fee_recipient = Address::repeat_byte(99); - - // Provide preparation data to the EL for `proposer`. - el.update_proposer_preparation( - Epoch::new(0), - [( - &ProposerPreparationData { - validator_index: proposer as u64, - fee_recipient, - }, - &None, - )], - ) - .await; - - rig.move_to_terminal_block(); - - rig.harness - .chain - .prepare_beacon_proposer(current_slot) - .await - .unwrap(); - let forkchoice_update_params = rig - .harness - .chain - .canonical_head - .fork_choice_read_lock() - .get_forkchoice_update_parameters(); - rig.harness - .chain - .update_execution_engine_forkchoice( - current_slot, - forkchoice_update_params, - OverrideForkchoiceUpdate::Yes, - ) - .await - .unwrap(); - - let (fork_choice_state, payload_attributes) = rig.previous_forkchoice_update_params(); - let latest_block_hash = rig.latest_execution_block_hash(); - assert_eq!(payload_attributes.suggested_fee_recipient(), fee_recipient); - assert_eq!(fork_choice_state.head_block_hash, latest_block_hash); -} - #[tokio::test] async fn attesting_to_optimistic_head() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let mut rig = InvalidPayloadRig::new(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. let root = rig.import_block(Payload::Syncing).await; @@ -1307,7 +1251,6 @@ impl InvalidHeadSetup { async fn new() -> InvalidHeadSetup { let slots_per_epoch = E::slots_per_epoch(); let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. // Import blocks until the first time the chain finalizes. This avoids @@ -1395,6 +1338,9 @@ impl InvalidHeadSetup { #[tokio::test] async fn recover_from_invalid_head_by_importing_blocks() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let InvalidHeadSetup { rig, fork_block, @@ -1440,6 +1386,9 @@ async fn recover_from_invalid_head_by_importing_blocks() { #[tokio::test] async fn recover_from_invalid_head_after_persist_and_reboot() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let InvalidHeadSetup { rig, fork_block: _, @@ -1482,8 +1431,10 @@ async fn recover_from_invalid_head_after_persist_and_reboot() { #[tokio::test] async fn weights_after_resetting_optimistic_status() { + if fork_name_from_env().is_some_and(|f| !f.bellatrix_enabled()) { + return; + } let mut rig = InvalidPayloadRig::new().enable_attestations(); - rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. let mut roots = vec![]; diff --git a/beacon_node/beacon_chain/tests/rewards.rs b/beacon_node/beacon_chain/tests/rewards.rs index 94ad97c963..bc7c98041f 100644 --- a/beacon_node/beacon_chain/tests/rewards.rs +++ b/beacon_node/beacon_chain/tests/rewards.rs @@ -29,7 +29,7 @@ static KEYPAIRS: LazyLock<Vec<Keypair>> = fn get_harness(spec: ChainSpec) -> BeaconChainHarness<EphemeralHarnessType<E>> { let chain_config = ChainConfig { - reconstruct_historic_states: true, + archive: true, ..Default::default() }; @@ -48,7 +48,7 @@ fn get_harness(spec: ChainSpec) -> BeaconChainHarness<EphemeralHarnessType<E>> { fn get_electra_harness(spec: ChainSpec) -> BeaconChainHarness<EphemeralHarnessType<E>> { let chain_config = ChainConfig { - reconstruct_historic_states: true, + archive: true, ..Default::default() }; diff --git a/beacon_node/beacon_chain/tests/schema_stability.rs b/beacon_node/beacon_chain/tests/schema_stability.rs index 3dc009366d..8200748ae6 100644 --- a/beacon_node/beacon_chain/tests/schema_stability.rs +++ b/beacon_node/beacon_chain/tests/schema_stability.rs @@ -70,7 +70,7 @@ async fn schema_stability() { let store = get_store(&datadir, store_config, spec.clone()); let chain_config = ChainConfig { - reconstruct_historic_states: true, + archive: true, ..ChainConfig::default() }; diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 8fbba81038..8601084b9f 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -1,4 +1,5 @@ #![cfg(not(debug_assertions))] +#![allow(clippy::result_large_err)] use beacon_chain::attestation_verification::Error as AttnError; use beacon_chain::block_verification_types::RpcBlock; @@ -100,7 +101,7 @@ fn get_harness( ) -> TestHarness { // Most tests expect to retain historic states, so we use this as the default. let chain_config = ChainConfig { - reconstruct_historic_states: true, + archive: true, ..ChainConfig::default() }; get_harness_generic( @@ -117,7 +118,8 @@ fn get_harness_import_all_data_columns( ) -> TestHarness { // Most tests expect to retain historic states, so we use this as the default. let chain_config = ChainConfig { - reconstruct_historic_states: true, + ignore_ws_check: true, + archive: true, ..ChainConfig::default() }; get_harness_generic( @@ -2875,7 +2877,7 @@ async fn reproduction_unaligned_checkpoint_sync_pruned_payload() { slot_clock.set_slot(harness.get_current_slot().as_u64()); let chain_config = ChainConfig { - reconstruct_historic_states: true, + archive: true, ..ChainConfig::default() }; @@ -3029,9 +3031,9 @@ async fn weak_subjectivity_sync_test( slot_clock.set_slot(harness.get_current_slot().as_u64()); let chain_config = ChainConfig { - // Set reconstruct_historic_states to true from the start in the genesis case. This makes + // Set archive to true from the start in the genesis case. This makes // some of the later checks more uniform across the genesis/non-genesis cases. - reconstruct_historic_states: checkpoint_slot == 0, + archive: checkpoint_slot == 0, ..ChainConfig::default() }; @@ -3684,7 +3686,7 @@ async fn process_blocks_and_attestations_for_unaligned_checkpoint() { let temp = tempdir().unwrap(); let store = get_store(&temp); let chain_config = ChainConfig { - reconstruct_historic_states: false, + archive: false, ..ChainConfig::default() }; let harness = get_harness_generic( @@ -3923,202 +3925,17 @@ async fn finalizes_after_resuming_from_db() { ); } -#[allow(clippy::large_stack_frames)] -#[tokio::test] -async fn revert_minority_fork_on_resume() { - let validator_count = 16; - let slots_per_epoch = MinimalEthSpec::slots_per_epoch(); - - let fork_epoch = Epoch::new(4); - let fork_slot = fork_epoch.start_slot(slots_per_epoch); - let initial_blocks = slots_per_epoch * fork_epoch.as_u64() - 1; - let post_fork_blocks = slots_per_epoch * 3; - - let mut spec1 = MinimalEthSpec::default_spec(); - spec1.altair_fork_epoch = None; - let mut spec2 = MinimalEthSpec::default_spec(); - spec2.altair_fork_epoch = Some(fork_epoch); - - let all_validators = (0..validator_count).collect::<Vec<usize>>(); - - // Chain with no fork epoch configured. - let db_path1 = tempdir().unwrap(); - let store1 = get_store_generic(&db_path1, StoreConfig::default(), spec1.clone()); - let harness1 = BeaconChainHarness::builder(MinimalEthSpec) - .spec(spec1.clone().into()) - .keypairs(KEYPAIRS[0..validator_count].to_vec()) - .fresh_disk_store(store1) - .mock_execution_layer() - .build(); - - // Chain with fork epoch configured. - let db_path2 = tempdir().unwrap(); - let store2 = get_store_generic(&db_path2, StoreConfig::default(), spec2.clone()); - let harness2 = BeaconChainHarness::builder(MinimalEthSpec) - .spec(spec2.clone().into()) - .keypairs(KEYPAIRS[0..validator_count].to_vec()) - .fresh_disk_store(store2) - .mock_execution_layer() - .build(); - - // Apply the same blocks to both chains initially. - let mut state = harness1.get_current_state(); - let mut block_root = harness1.chain.genesis_block_root; - for slot in (1..=initial_blocks).map(Slot::new) { - let state_root = state.update_tree_hash_cache().unwrap(); - - let attestations = harness1.make_attestations( - &all_validators, - &state, - state_root, - block_root.into(), - slot, - ); - harness1.set_current_slot(slot); - harness2.set_current_slot(slot); - harness1.process_attestations(attestations.clone(), &state); - harness2.process_attestations(attestations, &state); - - let ((block, blobs), new_state) = harness1.make_block(state, slot).await; - - harness1 - .process_block(slot, block.canonical_root(), (block.clone(), blobs.clone())) - .await - .unwrap(); - harness2 - .process_block(slot, block.canonical_root(), (block.clone(), blobs.clone())) - .await - .unwrap(); - - state = new_state; - block_root = block.canonical_root(); - } - - assert_eq!(harness1.head_slot(), fork_slot - 1); - assert_eq!(harness2.head_slot(), fork_slot - 1); - - // Fork the two chains. - let mut state1 = state.clone(); - let mut state2 = state.clone(); - - let mut majority_blocks = vec![]; - - for i in 0..post_fork_blocks { - let slot = fork_slot + i; - - // Attestations on majority chain. - let state_root = state.update_tree_hash_cache().unwrap(); - - let attestations = harness2.make_attestations( - &all_validators, - &state2, - state_root, - block_root.into(), - slot, - ); - harness2.set_current_slot(slot); - harness2.process_attestations(attestations, &state2); - - // Minority chain block (no attesters). - let ((block1, blobs1), new_state1) = harness1.make_block(state1, slot).await; - harness1 - .process_block(slot, block1.canonical_root(), (block1, blobs1)) - .await - .unwrap(); - state1 = new_state1; - - // Majority chain block (all attesters). - let ((block2, blobs2), new_state2) = harness2.make_block(state2, slot).await; - harness2 - .process_block(slot, block2.canonical_root(), (block2.clone(), blobs2)) - .await - .unwrap(); - - state2 = new_state2; - block_root = block2.canonical_root(); - - majority_blocks.push(block2); - } - - let end_slot = fork_slot + post_fork_blocks - 1; - assert_eq!(harness1.head_slot(), end_slot); - assert_eq!(harness2.head_slot(), end_slot); - - // Resume from disk with the hard-fork activated: this should revert the post-fork blocks. - // We have to do some hackery with the `slot_clock` so that the correct slot is set when - // the beacon chain builder loads the head block. - drop(harness1); - let resume_store = get_store_generic(&db_path1, StoreConfig::default(), spec2.clone()); - - let resumed_harness = TestHarness::builder(MinimalEthSpec) - .spec(spec2.clone().into()) - .keypairs(KEYPAIRS[0..validator_count].to_vec()) - .resumed_disk_store(resume_store) - .override_store_mutator(Box::new(move |mut builder| { - builder = builder - .resume_from_db() - .unwrap() - .testing_slot_clock(spec2.get_slot_duration()) - .unwrap(); - builder - .get_slot_clock() - .unwrap() - .set_slot(end_slot.as_u64()); - builder - })) - .mock_execution_layer() - .build(); - - // Head should now be just before the fork. - resumed_harness.chain.recompute_head_at_current_slot().await; - assert_eq!(resumed_harness.head_slot(), fork_slot - 1); - - // Fork choice should only know the canonical head. When we reverted the head we also should - // have called `reset_fork_choice_to_finalization` which rebuilds fork choice from scratch - // without the reverted block. - assert_eq!( - resumed_harness.chain.heads(), - vec![(resumed_harness.head_block_root(), fork_slot - 1)] - ); - - // Apply blocks from the majority chain and trigger finalization. - let initial_split_slot = resumed_harness.chain.store.get_split_slot(); - for block in &majority_blocks { - resumed_harness - .process_block_result((block.clone(), None)) - .await - .unwrap(); - - // The canonical head should be the block from the majority chain. - resumed_harness.chain.recompute_head_at_current_slot().await; - assert_eq!(resumed_harness.head_slot(), block.slot()); - assert_eq!(resumed_harness.head_block_root(), block.canonical_root()); - } - let advanced_split_slot = resumed_harness.chain.store.get_split_slot(); - - // Check that the migration ran successfully. - assert!(advanced_split_slot > initial_split_slot); - - // Check that there is only a single head now matching harness2 (the minority chain is gone). - let heads = resumed_harness.chain.heads(); - assert_eq!(heads, harness2.chain.heads()); - assert_eq!(heads.len(), 1); -} - // This test checks whether the schema downgrade from the latest version to some minimum supported // version is correct. This is the easiest schema test to write without historic versions of // Lighthouse on-hand, but has the disadvantage that the min version needs to be adjusted manually // as old downgrades are deprecated. -async fn schema_downgrade_to_min_version( - store_config: StoreConfig, - reconstruct_historic_states: bool, -) { +async fn schema_downgrade_to_min_version(store_config: StoreConfig, archive: bool) { let num_blocks_produced = E::slots_per_epoch() * 4; let db_path = tempdir().unwrap(); let spec = test_spec::<E>(); let chain_config = ChainConfig { - reconstruct_historic_states, + archive, ..ChainConfig::default() }; @@ -4173,7 +3990,7 @@ async fn schema_downgrade_to_min_version( .build(); // Check chain dump for appropriate range depending on whether this is an archive node. - let chain_dump_start_slot = if reconstruct_historic_states { + let chain_dump_start_slot = if archive { Slot::new(0) } else { store.get_split_slot() @@ -5153,7 +4970,7 @@ async fn ancestor_state_root_prior_to_split() { ..StoreConfig::default() }; let chain_config = ChainConfig { - reconstruct_historic_states: false, + archive: false, ..ChainConfig::default() }; @@ -5246,7 +5063,7 @@ async fn replay_from_split_state() { ..StoreConfig::default() }; let chain_config = ChainConfig { - reconstruct_historic_states: false, + archive: false, ..ChainConfig::default() }; @@ -5742,6 +5559,226 @@ fn check_iterators_from_slot(harness: &TestHarness, slot: Slot) { ); } +/// Test that blocks with default (pre-merge) execution payloads and non-default (post-merge) +/// execution payloads can be produced, stored, and retrieved correctly through a merge transition. +/// +/// Spec (see .claude/plans/8658.md): +/// - Bellatrix at epoch 0 (genesis), genesis has default execution payload header +/// - Slots 1-9: blocks have default (zeroed) execution payloads +/// - Slot 10: first block with a non-default execution payload (merge transition block) +/// - Slots 11-32+: non-default payloads, each with parent_hash == prev payload block_hash +/// - Chain must finalize past genesis +#[tokio::test] +async fn bellatrix_produce_and_store_payloads() { + use beacon_chain::test_utils::{ + DEFAULT_ETH1_BLOCK_HASH, HARNESS_GENESIS_TIME, InteropGenesisBuilder, + }; + use safe_arith::SafeArith; + use state_processing::per_block_processing::is_merge_transition_complete; + use tree_hash::TreeHash; + + let merge_slot = 10u64; + let total_slots = 48u64; + let spec = ForkName::Bellatrix.make_genesis_spec(E::default_spec()); + + // Build genesis state with a default (zeroed) execution payload header so that + // is_merge_transition_complete = false at genesis. + let keypairs = KEYPAIRS[0..LOW_VALIDATOR_COUNT].to_vec(); + let genesis_state = InteropGenesisBuilder::default() + .set_alternating_eth1_withdrawal_credentials() + .set_opt_execution_payload_header(None) + .build_genesis_state( + &keypairs, + HARNESS_GENESIS_TIME, + Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH), + &spec, + ) + .unwrap(); + + assert!( + !is_merge_transition_complete(&genesis_state), + "genesis should NOT have merge complete" + ); + + let db_path = tempdir().unwrap(); + let store = get_store_generic( + &db_path, + StoreConfig { + prune_payloads: false, + ..StoreConfig::default() + }, + spec.clone(), + ); + + let chain_config = ChainConfig { + archive: true, + ..ChainConfig::default() + }; + let harness = TestHarness::builder(MinimalEthSpec) + .spec(store.get_chain_spec().clone()) + .keypairs(keypairs.clone()) + .fresh_disk_store(store.clone()) + .override_store_mutator(Box::new(move |builder: BeaconChainBuilder<_>| { + builder + .genesis_state(genesis_state) + .expect("should set genesis state") + })) + .mock_execution_layer() + .chain_config(chain_config) + .build(); + + harness + .mock_execution_layer + .as_ref() + .unwrap() + .server + .all_payloads_valid(); + + harness.advance_slot(); + + // Phase 1: slots 1 to merge_slot-1 — blocks with default execution payloads. + let mut state = harness.get_current_state(); + for slot_num in 1..merge_slot { + let slot = Slot::new(slot_num); + harness.advance_slot(); + harness + .build_and_import_block_with_payload( + &mut state, + slot, + ExecutionPayloadBellatrix::default(), + ) + .await; + state = harness.get_current_state(); + } + + // Phase 2: slot merge_slot — the merge transition block with a real payload. + { + let slot = Slot::new(merge_slot); + harness.advance_slot(); + + // Advance state to compute correct timestamp and randao. + let mut pre_state = state.clone(); + complete_state_advance(&mut pre_state, None, slot, &harness.spec) + .expect("should advance state"); + pre_state + .build_caches(&harness.spec) + .expect("should build caches"); + + let timestamp = pre_state + .genesis_time() + .safe_add( + slot.as_u64() + .safe_mul(harness.spec.seconds_per_slot) + .unwrap(), + ) + .unwrap(); + let prev_randao = *pre_state.get_randao_mix(pre_state.current_epoch()).unwrap(); + + let mut transition_payload = ExecutionPayloadBellatrix { + parent_hash: ExecutionBlockHash::zero(), + fee_recipient: Address::repeat_byte(42), + receipts_root: Hash256::repeat_byte(42), + state_root: Hash256::repeat_byte(43), + logs_bloom: vec![0; 256].try_into().unwrap(), + prev_randao, + block_number: 1, + gas_limit: 30_000_000, + gas_used: 0, + timestamp, + extra_data: VariableList::empty(), + base_fee_per_gas: Uint256::from(1u64), + block_hash: ExecutionBlockHash::zero(), + transactions: VariableList::empty(), + }; + transition_payload.block_hash = + ExecutionBlockHash::from_root(transition_payload.tree_hash_root()); + + // Insert the transition payload into the mock EL so subsequent blocks can chain. + { + let mock_el = harness.mock_execution_layer.as_ref().unwrap(); + let mut block_gen = mock_el.server.execution_block_generator(); + block_gen.insert_block_without_checks(execution_layer::test_utils::Block::PoS( + ExecutionPayload::Bellatrix(transition_payload.clone()), + )); + } + + harness + .build_and_import_block_with_payload(&mut state, slot, transition_payload) + .await; + state = harness.get_current_state(); + + assert!( + is_merge_transition_complete(&state), + "merge should be complete after slot {merge_slot}" + ); + } + + // Phase 3: slots merge_slot+1 to total_slots — use harness with attestations. + let post_merge_slots = (total_slots - merge_slot) as usize; + harness.extend_slots(post_merge_slots).await; + + // ---- Verification: check all blocks in the store against plan invariants ---- + + let mut prev_payload_block_hash: Option<ExecutionBlockHash> = None; + + for slot_num in 1..=total_slots { + let slot = Slot::new(slot_num); + let block_root = harness + .chain + .block_root_at_slot(slot, WhenSlotSkipped::Prev) + .unwrap() + .unwrap_or_else(|| panic!("missing block at slot {slot_num}")); + let block = store + .get_blinded_block(&block_root) + .unwrap() + .unwrap_or_else(|| panic!("block not in store at slot {slot_num}")); + let payload = block + .message() + .body() + .execution_payload() + .expect("bellatrix block should have execution payload"); + + if slot_num < merge_slot { + // Slots 1 to merge_slot-1: payload must be default. + assert!( + payload.is_default_with_empty_roots(), + "slot {slot_num} should have default payload" + ); + } else if slot_num == merge_slot { + // Merge transition block: first non-default payload. + assert!( + !payload.is_default_with_empty_roots(), + "slot {slot_num} (merge) should have non-default payload" + ); + prev_payload_block_hash = Some(payload.block_hash()); + } else { + // Post-merge: non-default payload with valid parent_hash chain. + assert!( + !payload.is_default_with_empty_roots(), + "slot {slot_num} should have non-default payload" + ); + assert_eq!( + payload.parent_hash(), + prev_payload_block_hash.unwrap(), + "slot {slot_num} payload parent_hash should chain from previous payload" + ); + prev_payload_block_hash = Some(payload.block_hash()); + } + } + + // Verify finalization. + let finalized_epoch = harness + .chain + .canonical_head + .cached_head() + .finalized_checkpoint() + .epoch; + assert!( + finalized_epoch > 0, + "chain should have finalized past genesis" + ); +} + fn get_finalized_epoch_boundary_blocks( dump: &[BeaconSnapshot<MinimalEthSpec, BlindedPayload<MinimalEthSpec>>], ) -> HashSet<SignedBeaconBlockHash> { diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index 1884429a6a..b052ba66f1 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -15,7 +15,8 @@ use state_processing::EpochProcessingError; use state_processing::{per_slot_processing, per_slot_processing::Error as SlotProcessingError}; use std::sync::LazyLock; use types::{ - BeaconState, BeaconStateError, BlockImportSource, Checkpoint, EthSpec, Hash256, MinimalEthSpec, + BeaconState, BeaconStateError, BlockImportSource, ChainSpec, Checkpoint, + DEFAULT_PRE_ELECTRA_WS_PERIOD, EthSpec, ForkName, Hash256, MainnetEthSpec, MinimalEthSpec, RelativeEpoch, Slot, }; @@ -32,12 +33,33 @@ fn get_harness(validator_count: usize) -> BeaconChainHarness<EphemeralHarnessTyp get_harness_with_config( validator_count, ChainConfig { - reconstruct_historic_states: true, + archive: true, ..Default::default() }, ) } +fn get_harness_with_spec( + validator_count: usize, + spec: &ChainSpec, +) -> BeaconChainHarness<EphemeralHarnessType<MainnetEthSpec>> { + let chain_config = ChainConfig { + archive: true, + ..Default::default() + }; + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec.clone().into()) + .chain_config(chain_config) + .keypairs(KEYPAIRS[0..validator_count].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + harness.advance_slot(); + + harness +} + fn get_harness_with_config( validator_count: usize, chain_config: ChainConfig, @@ -63,7 +85,7 @@ fn get_harness_semi_supernode( let harness = BeaconChainHarness::builder(MinimalEthSpec) .default_spec() .chain_config(ChainConfig { - reconstruct_historic_states: true, + archive: true, ..Default::default() }) .keypairs(KEYPAIRS[0..validator_count].to_vec()) @@ -928,7 +950,7 @@ async fn pseudo_finalize_test_generic( let num_blocks_produced = MinimalEthSpec::slots_per_epoch() * 5; let chain_config = ChainConfig { - reconstruct_historic_states: true, + archive: true, epochs_per_migration, ..Default::default() }; @@ -1083,3 +1105,28 @@ async fn pseudo_finalize_with_lagging_split_update() { let expect_true_migration = false; pseudo_finalize_test_generic(epochs_per_migration, expect_true_migration).await; } + +#[tokio::test] +async fn test_compute_weak_subjectivity_period() { + type E = MainnetEthSpec; + let expected_ws_period_pre_electra = DEFAULT_PRE_ELECTRA_WS_PERIOD; + let expected_ws_period_post_electra = 256; + + // test Base variant + let spec = ForkName::Altair.make_genesis_spec(E::default_spec()); + let harness = get_harness_with_spec(VALIDATOR_COUNT, &spec); + let head_state = harness.get_current_state(); + + let calculated_ws_period = head_state.compute_weak_subjectivity_period(&spec).unwrap(); + + assert_eq!(calculated_ws_period, expected_ws_period_pre_electra); + + // test Electra variant + let spec = ForkName::Electra.make_genesis_spec(E::default_spec()); + let harness = get_harness_with_spec(VALIDATOR_COUNT, &spec); + let head_state = harness.get_current_state(); + + let calculated_ws_period = head_state.compute_weak_subjectivity_period(&spec).unwrap(); + + assert_eq!(calculated_ws_period, expected_ws_period_post_electra); +} diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index d3e9133542..33a00bfa49 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -243,12 +243,15 @@ impl<E: EthSpec> From<ReadyWork> for WorkEvent<E> { }, }, ReadyWork::RpcBlock(QueuedRpcBlock { - beacon_block_root: _, + beacon_block_root, process_fn, ignore_fn: _, }) => Self { drop_during_sync: false, - work: Work::RpcBlock { process_fn }, + work: Work::RpcBlock { + process_fn, + beacon_block_root, + }, }, ReadyWork::IgnoredRpcBlock(IgnoredRpcBlock { process_fn }) => Self { drop_during_sync: false, @@ -389,6 +392,7 @@ pub enum Work<E: EthSpec> { GossipLightClientFinalityUpdate(BlockingFn), GossipLightClientOptimisticUpdate(BlockingFn), RpcBlock { + beacon_block_root: Hash256, process_fn: AsyncFn, }, RpcBlobs { @@ -479,7 +483,7 @@ pub enum WorkType { } impl<E: EthSpec> Work<E> { - fn str_id(&self) -> &'static str { + pub fn str_id(&self) -> &'static str { self.to_type().into() } @@ -1432,7 +1436,10 @@ impl<E: EthSpec> BeaconProcessor<E> { beacon_block_root: _, process_fn, } => task_spawner.spawn_async(process_fn), - Work::RpcBlock { process_fn } + Work::RpcBlock { + process_fn, + beacon_block_root: _, + } | Work::RpcBlobs { process_fn } | Work::RpcCustodyColumn(process_fn) | Work::ColumnReconstruction(process_fn) => task_spawner.spawn_async(process_fn), diff --git a/beacon_node/builder_client/src/lib.rs b/beacon_node/builder_client/src/lib.rs index b17a824fd7..7dc0cbfc6d 100644 --- a/beacon_node/builder_client/src/lib.rs +++ b/beacon_node/builder_client/src/lib.rs @@ -10,10 +10,10 @@ use eth2::types::{ use eth2::types::{FullPayloadContents, SignedBlindedBeaconBlock}; use eth2::{ CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, JSON_CONTENT_TYPE_HEADER, - SSZ_CONTENT_TYPE_HEADER, StatusCode, ok_or_error, success_or_error, + SSZ_CONTENT_TYPE_HEADER, ok_or_error, success_or_error, }; use reqwest::header::{ACCEPT, HeaderMap, HeaderValue}; -use reqwest::{IntoUrl, Response}; +use reqwest::{IntoUrl, Response, StatusCode}; use sensitive_url::SensitiveUrl; use serde::Serialize; use serde::de::DeserializeOwned; diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index ede19d6b37..75d0455ac3 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -282,7 +282,7 @@ where validator_count, genesis_time, } => { - let execution_payload_header = generate_genesis_header(&spec, true); + let execution_payload_header = generate_genesis_header(&spec); let keypairs = generate_deterministic_keypairs(validator_count); let genesis_state = interop_genesis_state( &keypairs, diff --git a/beacon_node/client/src/notifier.rs b/beacon_node/client/src/notifier.rs index 3f01622c35..c1d8cae573 100644 --- a/beacon_node/client/src/notifier.rs +++ b/beacon_node/client/src/notifier.rs @@ -1,9 +1,7 @@ use crate::metrics; use beacon_chain::{ BeaconChain, BeaconChainTypes, ExecutionStatus, - bellatrix_readiness::{ - BellatrixReadiness, GenesisExecutionPayloadStatus, MergeConfig, SECONDS_IN_A_WEEK, - }, + bellatrix_readiness::GenesisExecutionPayloadStatus, }; use execution_layer::{ EngineCapabilities, @@ -36,6 +34,7 @@ const SPEEDO_OBSERVATIONS: usize = 4; /// The number of slots between logs that give detail about backfill process. const BACKFILL_LOG_INTERVAL: u64 = 5; +const SECONDS_IN_A_WEEK: u64 = 604800; pub const FORK_READINESS_PREPARATION_SECONDS: u64 = SECONDS_IN_A_WEEK * 2; pub const ENGINE_CAPABILITIES_REFRESH_INTERVAL: u64 = 300; @@ -70,7 +69,6 @@ pub fn spawn_notifier<T: BeaconChainTypes>( wait_time = estimated_time_pretty(Some(next_slot.as_secs() as f64)), "Waiting for genesis" ); - bellatrix_readiness_logging(Slot::new(0), &beacon_chain).await; post_bellatrix_readiness_logging(Slot::new(0), &beacon_chain).await; genesis_execution_payload_logging(&beacon_chain).await; sleep(slot_duration).await; @@ -414,7 +412,6 @@ pub fn spawn_notifier<T: BeaconChainTypes>( ); } - bellatrix_readiness_logging(current_slot, &beacon_chain).await; post_bellatrix_readiness_logging(current_slot, &beacon_chain).await; } }; @@ -425,78 +422,7 @@ pub fn spawn_notifier<T: BeaconChainTypes>( Ok(()) } -/// Provides some helpful logging to users to indicate if their node is ready for the Bellatrix -/// fork and subsequent merge transition. -async fn bellatrix_readiness_logging<T: BeaconChainTypes>( - current_slot: Slot, - beacon_chain: &BeaconChain<T>, -) { - let merge_completed = beacon_chain - .canonical_head - .cached_head() - .snapshot - .beacon_block - .message() - .body() - .execution_payload() - .is_ok_and(|payload| payload.parent_hash() != ExecutionBlockHash::zero()); - - let has_execution_layer = beacon_chain.execution_layer.is_some(); - - if merge_completed && has_execution_layer - || !beacon_chain.is_time_to_prepare_for_bellatrix(current_slot) - { - return; - } - - match beacon_chain.check_bellatrix_readiness(current_slot).await { - BellatrixReadiness::Ready { - config, - current_difficulty, - } => match config { - MergeConfig { - terminal_total_difficulty: Some(ttd), - terminal_block_hash: None, - terminal_block_hash_epoch: None, - } => { - info!( - terminal_total_difficulty = %ttd, - current_difficulty = current_difficulty - .map(|d| d.to_string()) - .unwrap_or_else(|| "??".into()), - "Ready for Bellatrix" - ) - } - MergeConfig { - terminal_total_difficulty: _, - terminal_block_hash: Some(terminal_block_hash), - terminal_block_hash_epoch: Some(terminal_block_hash_epoch), - } => { - info!( - info = "you are using override parameters, please ensure that you \ - understand these parameters and their implications.", - ?terminal_block_hash, - ?terminal_block_hash_epoch, - "Ready for Bellatrix" - ) - } - other => error!( - config = ?other, - "Inconsistent merge configuration" - ), - }, - readiness @ BellatrixReadiness::NotSynced => warn!( - info = %readiness, - "Not ready Bellatrix" - ), - readiness @ BellatrixReadiness::NoExecutionEndpoint => warn!( - info = %readiness, - "Not ready for Bellatrix" - ), - } -} - -/// Provides some helpful logging to users to indicate if their node is ready for Capella +/// Provides some helpful logging to users to indicate if their node is ready for upcoming forks async fn post_bellatrix_readiness_logging<T: BeaconChainTypes>( current_slot: Slot, beacon_chain: &BeaconChain<T>, diff --git a/beacon_node/execution_layer/Cargo.toml b/beacon_node/execution_layer/Cargo.toml index c443e94574..a23ea948e4 100644 --- a/beacon_node/execution_layer/Cargo.toml +++ b/beacon_node/execution_layer/Cargo.toml @@ -13,7 +13,7 @@ arc-swap = "1.6.0" bls = { workspace = true } builder_client = { path = "../builder_client" } bytes = { workspace = true } -eth2 = { workspace = true, features = ["events", "lighthouse"] } +eth2 = { workspace = true, features = ["events", "lighthouse", "network"] } ethereum_serde_utils = { workspace = true } ethereum_ssz = { workspace = true } fixed_bytes = { workspace = true } diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 33b83aab09..90968fa213 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -22,7 +22,6 @@ use eth2::types::{ForkVersionedResponse, builder::SignedBuilderBid}; use fixed_bytes::UintExtended; use fork_choice::ForkchoiceUpdateParameters; use logging::crit; -use lru::LruCache; pub use payload_status::PayloadStatus; use payload_status::process_payload_status; use sensitive_url::SensitiveUrl; @@ -32,7 +31,6 @@ use std::collections::{HashMap, hash_map::Entry}; use std::fmt; use std::future::Future; use std::io::Write; -use std::num::NonZeroUsize; use std::path::PathBuf; use std::sync::Arc; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; @@ -45,10 +43,10 @@ use tokio::{ use tokio_stream::wrappers::WatchStream; use tracing::{Instrument, debug, debug_span, error, info, instrument, warn}; use tree_hash::TreeHash; +use types::ExecutionPayloadGloas; use types::builder::BuilderBid; use types::execution::BlockProductionVersion; use types::kzg_ext::KzgCommitments; -use types::new_non_zero_usize; use types::{ AbstractExecPayload, BlobsList, ExecutionPayloadDeneb, ExecutionRequests, KzgProofs, SignedBlindedBeaconBlock, @@ -75,10 +73,6 @@ pub const DEFAULT_EXECUTION_ENDPOINT: &str = "http://localhost:8551/"; /// Name for the default file used for the jwt secret. pub const DEFAULT_JWT_FILE: &str = "jwt.hex"; -/// Each time the `ExecutionLayer` retrieves a block from an execution node, it stores that block -/// in an LRU cache to avoid redundant lookups. This is the size of that cache. -const EXECUTION_BLOCKS_LRU_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(128); - /// A fee recipient address for use during block production. Only used as a very last resort if /// there is no address provided by the user. /// @@ -168,6 +162,7 @@ pub enum Error { BeaconStateError(BeaconStateError), PayloadTypeMismatch, VerifyingVersionedHashes(versioned_hashes::Error), + Unexpected(String), } impl From<ssz_types::Error> for Error { @@ -204,6 +199,26 @@ pub enum BlockProposalContentsType<E: EthSpec> { Blinded(BlockProposalContents<E, BlindedPayload<E>>), } +pub struct BlockProposalContentsGloas<E: EthSpec> { + pub payload: ExecutionPayloadGloas<E>, + pub payload_value: Uint256, + pub blob_kzg_commitments: KzgCommitments<E>, + pub blobs_and_proofs: (BlobsList<E>, KzgProofs<E>), + pub execution_requests: ExecutionRequests<E>, +} + +impl<E: EthSpec> From<GetPayloadResponseGloas<E>> for BlockProposalContentsGloas<E> { + fn from(response: GetPayloadResponseGloas<E>) -> Self { + Self { + payload: response.execution_payload, + payload_value: response.block_value, + blob_kzg_commitments: response.blobs_bundle.commitments, + blobs_and_proofs: (response.blobs_bundle.blobs, response.blobs_bundle.proofs), + execution_requests: response.requests, + } + } +} + pub enum BlockProposalContents<E: EthSpec, Payload: AbstractExecPayload<E>> { Payload { payload: Payload, @@ -431,7 +446,6 @@ struct Inner<E: EthSpec> { execution_engine_forkchoice_lock: Mutex<()>, suggested_fee_recipient: Option<Address>, proposer_preparation_data: Mutex<HashMap<u64, ProposerPreparationDataEntry>>, - execution_blocks: Mutex<LruCache<ExecutionBlockHash, ExecutionBlock>>, proposers: RwLock<HashMap<ProposerKey, Proposer>>, executor: TaskExecutor, payload_cache: PayloadCache<E>, @@ -542,7 +556,6 @@ impl<E: EthSpec> ExecutionLayer<E> { suggested_fee_recipient, proposer_preparation_data: Mutex::new(HashMap::new()), proposers: RwLock::new(HashMap::new()), - execution_blocks: Mutex::new(LruCache::new(EXECUTION_BLOCKS_LRU_CACHE_SIZE)), executor, payload_cache: PayloadCache::default(), last_new_payload_errored: RwLock::new(false), @@ -634,12 +647,6 @@ impl<E: EthSpec> ExecutionLayer<E> { .ok_or(ApiError::ExecutionHeadBlockNotFound)?; Ok(block.total_difficulty) } - /// Note: this function returns a mutex guard, be careful to avoid deadlocks. - async fn execution_blocks( - &self, - ) -> MutexGuard<'_, LruCache<ExecutionBlockHash, ExecutionBlock>> { - self.inner.execution_blocks.lock().await - } /// Gives access to a channel containing if the last engine state is online or not. /// @@ -884,6 +891,44 @@ impl<E: EthSpec> ExecutionLayer<E> { .and_then(|entry| entry.gas_limit) } + /// Maps to the `engine_getPayload` JSON-RPC call for post-Gloas payload construction. + /// + /// However, it will attempt to call `self.prepare_payload` if it cannot find an existing + /// payload id for the given parameters. + /// + /// ## Fallback Behavior + /// + /// The result will be returned from the first node that returns successfully. No more nodes + /// will be contacted. + #[instrument(level = "debug", skip_all)] + pub async fn get_payload_gloas( + &self, + payload_parameters: PayloadParameters<'_>, + ) -> Result<BlockProposalContentsGloas<E>, Error> { + let payload_response_type = self.get_full_payload_caching(payload_parameters).await?; + let GetPayloadResponseType::Full(payload_response) = payload_response_type else { + return Err(Error::Unexpected( + "get_payload_gloas should never return a blinded payload".to_owned(), + )); + }; + let GetPayloadResponse::Gloas(payload_response) = payload_response else { + return Err(Error::Unexpected( + "get_payload_gloas should always return a gloas `GetPayloadResponse` variant" + .to_owned(), + )); + }; + metrics::inc_counter_vec( + &metrics::EXECUTION_LAYER_GET_PAYLOAD_OUTCOME, + &[metrics::SUCCESS], + ); + metrics::inc_counter_vec( + &metrics::EXECUTION_LAYER_GET_PAYLOAD_SOURCE, + &[metrics::LOCAL], + ); + + Ok(payload_response.into()) + } + /// Maps to the `engine_getPayload` JSON-RPC call. /// /// However, it will attempt to call `self.prepare_payload` if it cannot find an existing @@ -1582,208 +1627,6 @@ impl<E: EthSpec> ExecutionLayer<E> { Ok(versions) } - /// Used during block production to determine if the merge has been triggered. - /// - /// ## Specification - /// - /// `get_terminal_pow_block_hash` - /// - /// https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/merge/validator.md - pub async fn get_terminal_pow_block_hash( - &self, - spec: &ChainSpec, - timestamp: u64, - ) -> Result<Option<ExecutionBlockHash>, Error> { - let _timer = metrics::start_timer_vec( - &metrics::EXECUTION_LAYER_REQUEST_TIMES, - &[metrics::GET_TERMINAL_POW_BLOCK_HASH], - ); - - let hash_opt = self - .engine() - .request(|engine| async move { - let terminal_block_hash = spec.terminal_block_hash; - if terminal_block_hash != ExecutionBlockHash::zero() { - if self - .get_pow_block(engine, terminal_block_hash) - .await? - .is_some() - { - return Ok(Some(terminal_block_hash)); - } else { - return Ok(None); - } - } - - let block = self.get_pow_block_at_total_difficulty(engine, spec).await?; - if let Some(pow_block) = block { - // If `terminal_block.timestamp == transition_block.timestamp`, - // we violate the invariant that a block's timestamp must be - // strictly greater than its parent's timestamp. - // The execution layer will reject a fcu call with such payload - // attributes leading to a missed block. - // Hence, we return `None` in such a case. - if pow_block.timestamp >= timestamp { - return Ok(None); - } - } - Ok(block.map(|b| b.block_hash)) - }) - .await - .map_err(Box::new) - .map_err(Error::EngineError)?; - - if let Some(hash) = &hash_opt { - info!( - terminal_block_hash_override = ?spec.terminal_block_hash, - terminal_total_difficulty = ?spec.terminal_total_difficulty, - block_hash = ?hash, - "Found terminal block hash" - ); - } - - Ok(hash_opt) - } - - /// This function should remain internal. External users should use - /// `self.get_terminal_pow_block` instead, since it checks against the terminal block hash - /// override. - /// - /// ## Specification - /// - /// `get_pow_block_at_terminal_total_difficulty` - /// - /// https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/merge/validator.md - async fn get_pow_block_at_total_difficulty( - &self, - engine: &Engine, - spec: &ChainSpec, - ) -> Result<Option<ExecutionBlock>, ApiError> { - let mut block = engine - .api - .get_block_by_number(BlockByNumberQuery::Tag(LATEST_TAG)) - .await? - .ok_or(ApiError::ExecutionHeadBlockNotFound)?; - - self.execution_blocks().await.put(block.block_hash, block); - - loop { - let block_reached_ttd = - block.terminal_total_difficulty_reached(spec.terminal_total_difficulty); - if block_reached_ttd { - if block.parent_hash == ExecutionBlockHash::zero() { - return Ok(Some(block)); - } - let parent = self - .get_pow_block(engine, block.parent_hash) - .await? - .ok_or(ApiError::ExecutionBlockNotFound(block.parent_hash))?; - let parent_reached_ttd = - parent.terminal_total_difficulty_reached(spec.terminal_total_difficulty); - - if block_reached_ttd && !parent_reached_ttd { - return Ok(Some(block)); - } else { - block = parent; - } - } else { - return Ok(None); - } - } - } - - /// Used during block verification to check that a block correctly triggers the merge. - /// - /// ## Returns - /// - /// - `Some(true)` if the given `block_hash` is the terminal proof-of-work block. - /// - `Some(false)` if the given `block_hash` is certainly *not* the terminal proof-of-work - /// block. - /// - `None` if the `block_hash` or its parent were not present on the execution engine. - /// - `Err(_)` if there was an error connecting to the execution engine. - /// - /// ## Fallback Behaviour - /// - /// The request will be broadcast to all nodes, simultaneously. It will await a response (or - /// failure) from all nodes and then return based on the first of these conditions which - /// returns true: - /// - /// - Terminal, if any node indicates it is terminal. - /// - Not terminal, if any node indicates it is non-terminal. - /// - Block not found, if any node cannot find the block. - /// - An error, if all nodes return an error. - /// - /// ## Specification - /// - /// `is_valid_terminal_pow_block` - /// - /// https://github.com/ethereum/consensus-specs/blob/v1.1.0/specs/merge/fork-choice.md - pub async fn is_valid_terminal_pow_block_hash( - &self, - block_hash: ExecutionBlockHash, - spec: &ChainSpec, - ) -> Result<Option<bool>, Error> { - let _timer = metrics::start_timer_vec( - &metrics::EXECUTION_LAYER_REQUEST_TIMES, - &[metrics::IS_VALID_TERMINAL_POW_BLOCK_HASH], - ); - - self.engine() - .request(|engine| async move { - if let Some(pow_block) = self.get_pow_block(engine, block_hash).await? - && let Some(pow_parent) = - self.get_pow_block(engine, pow_block.parent_hash).await? - { - return Ok(Some( - self.is_valid_terminal_pow_block(pow_block, pow_parent, spec), - )); - } - Ok(None) - }) - .await - .map_err(Box::new) - .map_err(Error::EngineError) - } - - /// This function should remain internal. - /// - /// External users should use `self.is_valid_terminal_pow_block_hash`. - fn is_valid_terminal_pow_block( - &self, - block: ExecutionBlock, - parent: ExecutionBlock, - spec: &ChainSpec, - ) -> bool { - let is_total_difficulty_reached = - block.terminal_total_difficulty_reached(spec.terminal_total_difficulty); - let is_parent_total_difficulty_valid = parent - .total_difficulty - .is_some_and(|td| td < spec.terminal_total_difficulty); - is_total_difficulty_reached && is_parent_total_difficulty_valid - } - - /// Maps to the `eth_getBlockByHash` JSON-RPC call. - async fn get_pow_block( - &self, - engine: &Engine, - hash: ExecutionBlockHash, - ) -> Result<Option<ExecutionBlock>, ApiError> { - if let Some(cached) = self.execution_blocks().await.get(&hash).copied() { - // The block was in the cache, no need to request it from the execution - // engine. - return Ok(Some(cached)); - } - - // The block was *not* in the cache, request it from the execution - // engine and cache it for future reference. - if let Some(block) = engine.api.get_block_by_hash(hash).await? { - self.execution_blocks().await.put(hash, block); - Ok(Some(block)) - } else { - Ok(None) - } - } - pub async fn get_payload_bodies_by_hash( &self, hashes: Vec<ExecutionBlockHash>, @@ -2205,7 +2048,7 @@ fn verify_builder_bid<E: EthSpec>( .cloned() .map(|withdrawals| { Withdrawals::<E>::try_from(withdrawals) - .map_err(InvalidBuilderPayload::SszTypesError) + .map_err(|e| Box::new(InvalidBuilderPayload::SszTypesError(e))) .map(|w| w.tree_hash_root()) }) .transpose()?; @@ -2271,15 +2114,6 @@ async fn timed_future<F: Future<Output = T>, T>(metric: &str, future: F) -> (T, (result, duration) } -#[cfg(test)] -/// Returns the duration since the unix epoch. -fn timestamp_now() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_else(|_| Duration::from_secs(0)) - .as_secs() -} - fn noop<E: EthSpec>( _: &ExecutionLayer<E>, _: PayloadContentsRefTuple<E>, @@ -2300,7 +2134,6 @@ mod test { async fn produce_three_valid_pos_execution_blocks() { let runtime = TestRuntime::default(); MockExecutionLayer::default_params(runtime.task_executor.clone()) - .move_to_terminal_block() .produce_valid_execution_payload_on_head() .await .produce_valid_execution_payload_on_head() @@ -2329,129 +2162,4 @@ mod test { Some(30_029_266) ); } - - #[tokio::test] - async fn test_forked_terminal_block() { - let runtime = TestRuntime::default(); - let (mock, block_hash) = MockExecutionLayer::default_params(runtime.task_executor.clone()) - .move_to_terminal_block() - .produce_forked_pow_block(); - assert!( - mock.el - .is_valid_terminal_pow_block_hash(block_hash, &mock.spec) - .await - .unwrap() - .unwrap() - ); - } - - #[tokio::test] - async fn finds_valid_terminal_block_hash() { - let runtime = TestRuntime::default(); - MockExecutionLayer::default_params(runtime.task_executor.clone()) - .move_to_block_prior_to_terminal_block() - .with_terminal_block(|spec, el, _| async move { - el.engine().upcheck().await; - assert_eq!( - el.get_terminal_pow_block_hash(&spec, timestamp_now()) - .await - .unwrap(), - None - ) - }) - .await - .move_to_terminal_block() - .with_terminal_block(|spec, el, terminal_block| async move { - assert_eq!( - el.get_terminal_pow_block_hash(&spec, timestamp_now()) - .await - .unwrap(), - Some(terminal_block.unwrap().block_hash) - ) - }) - .await; - } - - #[tokio::test] - async fn rejects_terminal_block_with_equal_timestamp() { - let runtime = TestRuntime::default(); - MockExecutionLayer::default_params(runtime.task_executor.clone()) - .move_to_block_prior_to_terminal_block() - .with_terminal_block(|spec, el, _| async move { - el.engine().upcheck().await; - assert_eq!( - el.get_terminal_pow_block_hash(&spec, timestamp_now()) - .await - .unwrap(), - None - ) - }) - .await - .move_to_terminal_block() - .with_terminal_block(|spec, el, terminal_block| async move { - let timestamp = terminal_block.as_ref().map(|b| b.timestamp).unwrap(); - assert_eq!( - el.get_terminal_pow_block_hash(&spec, timestamp) - .await - .unwrap(), - None - ) - }) - .await; - } - - #[tokio::test] - async fn verifies_valid_terminal_block_hash() { - let runtime = TestRuntime::default(); - MockExecutionLayer::default_params(runtime.task_executor.clone()) - .move_to_terminal_block() - .with_terminal_block(|spec, el, terminal_block| async move { - el.engine().upcheck().await; - assert_eq!( - el.is_valid_terminal_pow_block_hash(terminal_block.unwrap().block_hash, &spec) - .await - .unwrap(), - Some(true) - ) - }) - .await; - } - - #[tokio::test] - async fn rejects_invalid_terminal_block_hash() { - let runtime = TestRuntime::default(); - MockExecutionLayer::default_params(runtime.task_executor.clone()) - .move_to_terminal_block() - .with_terminal_block(|spec, el, terminal_block| async move { - el.engine().upcheck().await; - let invalid_terminal_block = terminal_block.unwrap().parent_hash; - - assert_eq!( - el.is_valid_terminal_pow_block_hash(invalid_terminal_block, &spec) - .await - .unwrap(), - Some(false) - ) - }) - .await; - } - - #[tokio::test] - async fn rejects_unknown_terminal_block_hash() { - let runtime = TestRuntime::default(); - MockExecutionLayer::default_params(runtime.task_executor.clone()) - .move_to_terminal_block() - .with_terminal_block(|spec, el, _| async move { - el.engine().upcheck().await; - let missing_terminal_block = ExecutionBlockHash::repeat_byte(42); - - assert_eq!( - el.is_valid_terminal_pow_block_hash(missing_terminal_block, &spec) - .await - .unwrap(), - None - ) - }) - .await; - } } diff --git a/beacon_node/execution_layer/src/metrics.rs b/beacon_node/execution_layer/src/metrics.rs index 859f33bc81..79bdc37aea 100644 --- a/beacon_node/execution_layer/src/metrics.rs +++ b/beacon_node/execution_layer/src/metrics.rs @@ -10,8 +10,6 @@ pub const GET_BLINDED_PAYLOAD_BUILDER: &str = "get_blinded_payload_builder"; pub const POST_BLINDED_PAYLOAD_BUILDER: &str = "post_blinded_payload_builder"; pub const NEW_PAYLOAD: &str = "new_payload"; pub const FORKCHOICE_UPDATED: &str = "forkchoice_updated"; -pub const GET_TERMINAL_POW_BLOCK_HASH: &str = "get_terminal_pow_block_hash"; -pub const IS_VALID_TERMINAL_POW_BLOCK_HASH: &str = "is_valid_terminal_pow_block_hash"; pub const LOCAL: &str = "local"; pub const BUILDER: &str = "builder"; pub const SUCCESS: &str = "success"; diff --git a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs index 6b247a4cd4..62a46246da 100644 --- a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs +++ b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs @@ -1,7 +1,8 @@ use crate::engine_api::{ ExecutionBlock, PayloadAttributes, PayloadId, PayloadStatusV1, PayloadStatusV1Status, json_structures::{ - JsonForkchoiceUpdatedV1Response, JsonPayloadStatusV1, JsonPayloadStatusV1Status, + BlobAndProof, BlobAndProofV1, BlobAndProofV2, JsonForkchoiceUpdatedV1Response, + JsonPayloadStatusV1, JsonPayloadStatusV1Status, }, }; use crate::engines::ForkchoiceState; @@ -15,9 +16,11 @@ use rand::{Rng, SeedableRng, rngs::StdRng}; use serde::{Deserialize, Serialize}; use ssz::Decode; use ssz_types::VariableList; +use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash; use std::cmp::max; use std::collections::HashMap; use std::sync::Arc; +use tracing::warn; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; use types::{ @@ -27,8 +30,6 @@ use types::{ Transactions, Uint256, }; -use super::DEFAULT_TERMINAL_BLOCK; - const TEST_BLOB_BUNDLE: &[u8] = include_bytes!("fixtures/mainnet/test_blobs_bundle.ssz"); const TEST_BLOB_BUNDLE_V2: &[u8] = include_bytes!("fixtures/mainnet/test_blobs_bundle_v2.ssz"); @@ -171,9 +172,6 @@ fn make_rng() -> Arc<Mutex<StdRng>> { impl<E: EthSpec> ExecutionBlockGenerator<E> { #[allow(clippy::too_many_arguments)] pub fn new( - terminal_total_difficulty: Uint256, - terminal_block_number: u64, - terminal_block_hash: ExecutionBlockHash, shanghai_time: Option<u64>, cancun_time: Option<u64>, prague_time: Option<u64>, @@ -186,9 +184,9 @@ impl<E: EthSpec> ExecutionBlockGenerator<E> { finalized_block_hash: <_>::default(), blocks: <_>::default(), block_hashes: <_>::default(), - terminal_total_difficulty, - terminal_block_number, - terminal_block_hash, + terminal_total_difficulty: Default::default(), + terminal_block_number: 0, + terminal_block_hash: Default::default(), pending_payloads: <_>::default(), next_payload_id: 0, payload_ids: <_>::default(), @@ -292,25 +290,6 @@ impl<E: EthSpec> ExecutionBlockGenerator<E> { .and_then(|block| block.as_execution_payload()) } - pub fn move_to_block_prior_to_terminal_block(&mut self) -> Result<(), String> { - let target_block = self - .terminal_block_number - .checked_sub(1) - .ok_or("terminal pow block is 0")?; - self.move_to_pow_block(target_block) - } - - pub fn move_to_terminal_block(&mut self) -> Result<(), String> { - self.move_to_pow_block(self.terminal_block_number) - } - - pub fn move_to_pow_block(&mut self, target_block: u64) -> Result<(), String> { - let next_block = self.latest_block().unwrap().block_number() + 1; - assert!(target_block >= next_block); - - self.insert_pow_blocks(next_block..=target_block) - } - pub fn drop_all_blocks(&mut self) { self.blocks = <_>::default(); self.block_hashes = <_>::default(); @@ -479,6 +458,40 @@ impl<E: EthSpec> ExecutionBlockGenerator<E> { self.blobs_bundles.get(id).cloned() } + /// Look up a blob and proof by versioned hash across all stored bundles. + pub fn get_blob_and_proof(&self, versioned_hash: &Hash256) -> Option<BlobAndProof<E>> { + self.blobs_bundles + .iter() + .find_map(|(payload_id, blobs_bundle)| { + let (blob_idx, _) = + blobs_bundle + .commitments + .iter() + .enumerate() + .find(|(_, commitment)| { + &kzg_commitment_to_versioned_hash(commitment) == versioned_hash + })?; + let is_fulu = self.payload_ids.get(payload_id)?.fork_name().fulu_enabled(); + let blob = blobs_bundle.blobs.get(blob_idx)?.clone(); + if is_fulu { + let start = blob_idx * E::cells_per_ext_blob(); + let end = start + E::cells_per_ext_blob(); + let proofs = blobs_bundle + .proofs + .get(start..end)? + .to_vec() + .try_into() + .ok()?; + Some(BlobAndProof::V2(BlobAndProofV2 { blob, proofs })) + } else { + Some(BlobAndProof::V1(BlobAndProofV1 { + blob, + proof: *blobs_bundle.proofs.get(blob_idx)?, + })) + } + }) + } + pub fn new_payload(&mut self, payload: ExecutionPayload<E>) -> PayloadStatusV1 { let Some(parent) = self.blocks.get(&payload.parent_hash()) else { return PayloadStatusV1 { @@ -537,6 +550,21 @@ impl<E: EthSpec> ExecutionBlockGenerator<E> { .contains_key(&forkchoice_state.finalized_block_hash); if unknown_head_block_hash || unknown_safe_block_hash || unknown_finalized_block_hash { + if unknown_head_block_hash { + warn!(?head_block_hash, "Received unknown head block hash"); + } + if unknown_safe_block_hash { + warn!( + safe_block_hash = ?forkchoice_state.safe_block_hash, + "Received unknown safe block hash" + ); + } + if unknown_finalized_block_hash { + warn!( + finalized_block_hash = ?forkchoice_state.finalized_block_hash, + "Received unknown finalized block hash" + ) + } return Ok(JsonForkchoiceUpdatedV1Response { payload_status: JsonPayloadStatusV1 { status: JsonPayloadStatusV1Status::Syncing, @@ -863,27 +891,22 @@ fn payload_id_from_u64(n: u64) -> PayloadId { n.to_le_bytes() } -pub fn generate_genesis_header<E: EthSpec>( - spec: &ChainSpec, - post_transition_merge: bool, -) -> Option<ExecutionPayloadHeader<E>> { +pub fn generate_genesis_header<E: EthSpec>(spec: &ChainSpec) -> Option<ExecutionPayloadHeader<E>> { let genesis_fork = spec.fork_name_at_slot::<E>(spec.genesis_slot); - let genesis_block_hash = - generate_genesis_block(spec.terminal_total_difficulty, DEFAULT_TERMINAL_BLOCK) - .ok() - .map(|block| block.block_hash); + let genesis_block_hash = generate_genesis_block(Default::default(), 0) + .ok() + .map(|block| block.block_hash); let empty_transactions_root = Transactions::<E>::empty().tree_hash_root(); match genesis_fork { - ForkName::Base | ForkName::Altair => None, + ForkName::Base | ForkName::Altair => { + // Pre-Bellatrix forks have no execution payload + None + } ForkName::Bellatrix => { - if post_transition_merge { - let mut header = ExecutionPayloadHeader::Bellatrix(<_>::default()); - *header.block_hash_mut() = genesis_block_hash.unwrap_or_default(); - *header.transactions_root_mut() = empty_transactions_root; - Some(header) - } else { - Some(ExecutionPayloadHeader::<E>::Bellatrix(<_>::default())) - } + let mut header = ExecutionPayloadHeader::Bellatrix(<_>::default()); + *header.block_hash_mut() = genesis_block_hash.unwrap_or_default(); + *header.transactions_root_mut() = empty_transactions_root; + Some(header) } ForkName::Capella => { let mut header = ExecutionPayloadHeader::Capella(<_>::default()); @@ -969,70 +992,6 @@ mod test { use kzg::{Bytes48, CellRef, KzgBlobRef, trusted_setup::get_trusted_setup}; use types::{MainnetEthSpec, MinimalEthSpec}; - #[test] - fn pow_chain_only() { - const TERMINAL_DIFFICULTY: u64 = 10; - const TERMINAL_BLOCK: u64 = 10; - const DIFFICULTY_INCREMENT: u64 = 1; - - let mut generator: ExecutionBlockGenerator<MainnetEthSpec> = ExecutionBlockGenerator::new( - Uint256::from(TERMINAL_DIFFICULTY), - TERMINAL_BLOCK, - ExecutionBlockHash::zero(), - None, - None, - None, - None, - None, - None, - ); - - for i in 0..=TERMINAL_BLOCK { - if i > 0 { - generator.insert_pow_block(i).unwrap(); - } - - /* - * Generate a block, inspect it. - */ - - let block = generator.latest_block().unwrap(); - assert_eq!(block.block_number(), i); - - let expected_parent = i - .checked_sub(1) - .map(|i| generator.block_by_number(i).unwrap().block_hash()) - .unwrap_or_else(ExecutionBlockHash::zero); - assert_eq!(block.parent_hash(), expected_parent); - - assert_eq!( - block.total_difficulty().unwrap(), - Uint256::from(i * DIFFICULTY_INCREMENT) - ); - - assert_eq!(generator.block_by_hash(block.block_hash()).unwrap(), block); - assert_eq!(generator.block_by_number(i).unwrap(), block); - - /* - * Check the parent is accessible. - */ - - if let Some(prev_i) = i.checked_sub(1) { - assert_eq!( - generator.block_by_number(prev_i).unwrap(), - generator.block_by_hash(block.parent_hash()).unwrap() - ); - } - - /* - * Check the next block is inaccessible. - */ - - let next_i = i + 1; - assert!(generator.block_by_number(next_i).is_none()); - } - } - #[test] fn valid_test_blobs_bundle_v1() { assert!( diff --git a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs index 2168ed8961..7a81017b3f 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -5,6 +5,7 @@ use crate::test_utils::{DEFAULT_CLIENT_VERSION, DEFAULT_MOCK_EL_PAYLOAD_VALUE_WE use serde::{Deserialize, de::DeserializeOwned}; use serde_json::Value as JsonValue; use std::sync::Arc; +use tracing::debug; pub const GENERIC_ERROR_CODE: i64 = -1234; pub const BAD_PARAMS_ERROR_CODE: i64 = -32602; @@ -28,6 +29,8 @@ pub async fn handle_rpc<E: EthSpec>( .ok_or_else(|| "missing/invalid params field".to_string()) .map_err(|s| (s, GENERIC_ERROR_CODE))?; + debug!(method, "Mock execution engine"); + match method { ETH_SYNCING => ctx .syncing_response @@ -465,6 +468,35 @@ pub async fn handle_rpc<E: EthSpec>( _ => unreachable!(), } } + ENGINE_GET_BLOBS_V1 => { + let versioned_hashes = + get_param::<Vec<Hash256>>(params, 0).map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?; + let generator = ctx.execution_block_generator.read(); + // V1: per-element nullable array, positionally matching the request. + let response: Vec<Option<BlobAndProofV1<E>>> = versioned_hashes + .iter() + .map(|hash| match generator.get_blob_and_proof(hash) { + Some(BlobAndProof::V1(v1)) => Some(v1), + _ => None, + }) + .collect(); + Ok(serde_json::to_value(response).unwrap()) + } + ENGINE_GET_BLOBS_V2 => { + let versioned_hashes = + get_param::<Vec<Hash256>>(params, 0).map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?; + let generator = ctx.execution_block_generator.read(); + // V2: all-or-nothing — null if any blob is missing. + let results: Vec<Option<BlobAndProofV2<E>>> = versioned_hashes + .iter() + .map(|hash| match generator.get_blob_and_proof(hash) { + Some(BlobAndProof::V2(v2)) => Some(v2), + _ => None, + }) + .collect(); + let response: Option<Vec<BlobAndProofV2<E>>> = results.into_iter().collect(); + Ok(serde_json::to_value(response).unwrap()) + } ENGINE_FORKCHOICE_UPDATED_V1 | ENGINE_FORKCHOICE_UPDATED_V2 | ENGINE_FORKCHOICE_UPDATED_V3 => { @@ -517,6 +549,12 @@ pub async fn handle_rpc<E: EthSpec>( _ => unreachable!(), }; + debug!( + ?payload_attributes, + ?forkchoice_state, + "ENGINE_FORKCHOICE_UPDATED" + ); + // validate method called correctly according to fork time if let Some(pa) = payload_attributes.as_ref() { match ctx diff --git a/beacon_node/execution_layer/src/test_utils/mock_builder.rs b/beacon_node/execution_layer/src/test_utils/mock_builder.rs index 464879288b..7b6c4e8310 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -245,7 +245,7 @@ impl<E: EthSpec> BidStuff<E> for BuilderBid<E> { } fn sign_builder_message(&mut self, sk: &SecretKey, spec: &ChainSpec) -> Signature { - let domain = spec.get_builder_domain(); + let domain = spec.get_builder_application_domain(); let message = self.signing_root(domain); sk.sign(message) } diff --git a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs index c69edb8f39..91966ff65e 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs @@ -1,9 +1,4 @@ -use crate::{ - test_utils::{ - DEFAULT_JWT_SECRET, DEFAULT_TERMINAL_BLOCK, DEFAULT_TERMINAL_DIFFICULTY, MockServer, - }, - *, -}; +use crate::{test_utils::DEFAULT_JWT_SECRET, test_utils::MockServer, *}; use alloy_primitives::B256 as H256; use fixed_bytes::FixedBytesExtended; use kzg::Kzg; @@ -20,12 +15,10 @@ pub struct MockExecutionLayer<E: EthSpec> { impl<E: EthSpec> MockExecutionLayer<E> { pub fn default_params(executor: TaskExecutor) -> Self { let mut spec = MainnetEthSpec::default_spec(); - spec.terminal_total_difficulty = Uint256::from(DEFAULT_TERMINAL_DIFFICULTY); spec.terminal_block_hash = ExecutionBlockHash::zero(); spec.terminal_block_hash_activation_epoch = Epoch::new(0); Self::new( executor, - DEFAULT_TERMINAL_BLOCK, None, None, None, @@ -40,7 +33,6 @@ impl<E: EthSpec> MockExecutionLayer<E> { #[allow(clippy::too_many_arguments)] pub fn new( executor: TaskExecutor, - terminal_block: u64, shanghai_time: Option<u64>, cancun_time: Option<u64>, prague_time: Option<u64>, @@ -56,9 +48,6 @@ impl<E: EthSpec> MockExecutionLayer<E> { let server = MockServer::new( &handle, jwt_key, - spec.terminal_total_difficulty, - terminal_block, - spec.terminal_block_hash, shanghai_time, cancun_time, prague_time, @@ -293,53 +282,4 @@ impl<E: EthSpec> MockExecutionLayer<E> { assert_eq!(head_execution_block.block_hash(), block_hash); assert_eq!(head_execution_block.parent_hash(), parent_hash); } - - pub fn move_to_block_prior_to_terminal_block(self) -> Self { - self.server - .execution_block_generator() - .move_to_block_prior_to_terminal_block() - .unwrap(); - self - } - - pub fn move_to_terminal_block(self) -> Self { - self.server - .execution_block_generator() - .move_to_terminal_block() - .unwrap(); - self - } - - pub fn produce_forked_pow_block(self) -> (Self, ExecutionBlockHash) { - let head_block = self - .server - .execution_block_generator() - .latest_block() - .unwrap(); - - let block_hash = self - .server - .execution_block_generator() - .insert_pow_block_by_hash(head_block.parent_hash(), 1) - .unwrap(); - (self, block_hash) - } - - pub async fn with_terminal_block<U, V>(self, func: U) -> Self - where - U: Fn(Arc<ChainSpec>, ExecutionLayer<E>, Option<ExecutionBlock>) -> V, - V: Future<Output = ()>, - { - let terminal_block_number = self - .server - .execution_block_generator() - .terminal_block_number; - let terminal_block = self - .server - .execution_block_generator() - .execution_block_by_number(terminal_block_number); - - func(self.spec.clone(), self.el.clone(), terminal_block).await; - self - } } diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index 2465a41d8b..d8e1e70e49 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -35,8 +35,6 @@ pub use hook::Hook; pub use mock_builder::{MockBuilder, Operation, mock_builder_extra_data}; pub use mock_execution_layer::MockExecutionLayer; -pub const DEFAULT_TERMINAL_DIFFICULTY: u64 = 6400; -pub const DEFAULT_TERMINAL_BLOCK: u64 = 64; pub const DEFAULT_JWT_SECRET: [u8; 32] = [42; 32]; pub const DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI: u128 = 10_000_000_000_000_000; pub const DEFAULT_BUILDER_PAYLOAD_VALUE_WEI: u128 = 20_000_000_000_000_000; @@ -79,9 +77,6 @@ mod mock_execution_layer; pub struct MockExecutionConfig { pub server_config: Config, pub jwt_key: JwtKey, - pub terminal_difficulty: Uint256, - pub terminal_block: u64, - pub terminal_block_hash: ExecutionBlockHash, pub shanghai_time: Option<u64>, pub cancun_time: Option<u64>, pub prague_time: Option<u64>, @@ -93,9 +88,6 @@ impl Default for MockExecutionConfig { fn default() -> Self { Self { jwt_key: JwtKey::random(), - terminal_difficulty: Uint256::from(DEFAULT_TERMINAL_DIFFICULTY), - terminal_block: DEFAULT_TERMINAL_BLOCK, - terminal_block_hash: ExecutionBlockHash::zero(), server_config: Config::default(), shanghai_time: None, cancun_time: None, @@ -118,9 +110,6 @@ impl<E: EthSpec> MockServer<E> { Self::new( &runtime::Handle::current(), JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap(), - Uint256::from(DEFAULT_TERMINAL_DIFFICULTY), - DEFAULT_TERMINAL_BLOCK, - ExecutionBlockHash::zero(), None, // FIXME(capella): should this be the default? None, // FIXME(deneb): should this be the default? None, // FIXME(electra): should this be the default? @@ -138,9 +127,6 @@ impl<E: EthSpec> MockServer<E> { create_test_tracing_subscriber(); let MockExecutionConfig { jwt_key, - terminal_difficulty, - terminal_block, - terminal_block_hash, server_config, shanghai_time, cancun_time, @@ -151,9 +137,6 @@ impl<E: EthSpec> MockServer<E> { let last_echo_request = Arc::new(RwLock::new(None)); let preloaded_responses = Arc::new(Mutex::new(vec![])); let execution_block_generator = ExecutionBlockGenerator::new( - terminal_difficulty, - terminal_block, - terminal_block_hash, shanghai_time, cancun_time, prague_time, @@ -215,9 +198,6 @@ impl<E: EthSpec> MockServer<E> { pub fn new( handle: &runtime::Handle, jwt_key: JwtKey, - terminal_difficulty: Uint256, - terminal_block: u64, - terminal_block_hash: ExecutionBlockHash, shanghai_time: Option<u64>, cancun_time: Option<u64>, prague_time: Option<u64>, @@ -230,9 +210,6 @@ impl<E: EthSpec> MockServer<E> { MockExecutionConfig { server_config: Config::default(), jwt_key, - terminal_difficulty, - terminal_block, - terminal_block_hash, shanghai_time, cancun_time, prague_time, diff --git a/beacon_node/http_api/Cargo.toml b/beacon_node/http_api/Cargo.toml index 6211ac6726..dd15a76c7a 100644 --- a/beacon_node/http_api/Cargo.toml +++ b/beacon_node/http_api/Cargo.toml @@ -14,7 +14,7 @@ bytes = { workspace = true } context_deserialize = { workspace = true } directory = { workspace = true } either = { workspace = true } -eth2 = { workspace = true, features = ["lighthouse"] } +eth2 = { workspace = true, features = ["lighthouse", "network"] } ethereum_serde_utils = { workspace = true } ethereum_ssz = { workspace = true } execution_layer = { workspace = true } @@ -33,6 +33,7 @@ operation_pool = { workspace = true } parking_lot = { workspace = true } proto_array = { workspace = true } rand = { workspace = true } +reqwest = { workspace = true } safe_arith = { workspace = true } sensitive_url = { workspace = true } serde = { workspace = true } diff --git a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs new file mode 100644 index 0000000000..81f2ea41ea --- /dev/null +++ b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs @@ -0,0 +1,116 @@ +use crate::task_spawner::{Priority, TaskSpawner}; +use crate::utils::{ChainFilter, EthV1Filter, NetworkTxFilter, ResponseFilter, TaskSpawnerFilter}; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use bytes::Bytes; +use eth2::{CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER}; +use lighthouse_network::PubsubMessage; +use network::NetworkMessage; +use ssz::Decode; +use std::sync::Arc; +use tokio::sync::mpsc::UnboundedSender; +use tracing::{info, warn}; +use types::SignedExecutionPayloadEnvelope; +use warp::{Filter, Rejection, Reply, reply::Response}; + +// POST beacon/execution_payload_envelope (SSZ) +pub(crate) fn post_beacon_execution_payload_envelope_ssz<T: BeaconChainTypes>( + eth_v1: EthV1Filter, + task_spawner_filter: TaskSpawnerFilter<T>, + chain_filter: ChainFilter<T>, + network_tx_filter: NetworkTxFilter<T>, +) -> ResponseFilter { + eth_v1 + .and(warp::path("beacon")) + .and(warp::path("execution_payload_envelope")) + .and(warp::path::end()) + .and(warp::header::exact( + CONTENT_TYPE_HEADER, + SSZ_CONTENT_TYPE_HEADER, + )) + .and(warp::body::bytes()) + .and(task_spawner_filter) + .and(chain_filter) + .and(network_tx_filter) + .then( + |body_bytes: Bytes, + task_spawner: TaskSpawner<T::EthSpec>, + chain: Arc<BeaconChain<T>>, + network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + let envelope = + SignedExecutionPayloadEnvelope::<T::EthSpec>::from_ssz_bytes(&body_bytes) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}")) + })?; + publish_execution_payload_envelope(envelope, chain, &network_tx).await + }) + }, + ) + .boxed() +} + +// POST beacon/execution_payload_envelope +pub(crate) fn post_beacon_execution_payload_envelope<T: BeaconChainTypes>( + eth_v1: EthV1Filter, + task_spawner_filter: TaskSpawnerFilter<T>, + chain_filter: ChainFilter<T>, + network_tx_filter: NetworkTxFilter<T>, +) -> ResponseFilter { + eth_v1 + .and(warp::path("beacon")) + .and(warp::path("execution_payload_envelope")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .and(network_tx_filter.clone()) + .then( + |envelope: SignedExecutionPayloadEnvelope<T::EthSpec>, + task_spawner: TaskSpawner<T::EthSpec>, + chain: Arc<BeaconChain<T>>, + network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + publish_execution_payload_envelope(envelope, chain, &network_tx).await + }) + }, + ) + .boxed() +} +/// Publishes a signed execution payload envelope to the network. +pub async fn publish_execution_payload_envelope<T: BeaconChainTypes>( + envelope: SignedExecutionPayloadEnvelope<T::EthSpec>, + chain: Arc<BeaconChain<T>>, + network_tx: &UnboundedSender<NetworkMessage<T::EthSpec>>, +) -> Result<Response, Rejection> { + let slot = envelope.message.slot; + let beacon_block_root = envelope.message.beacon_block_root; + + // TODO(gloas): Replace this check once we have gossip validation. + if !chain.spec.is_gloas_scheduled() { + return Err(warp_utils::reject::custom_bad_request( + "Execution payload envelopes are not supported before the Gloas fork".into(), + )); + } + + // TODO(gloas): We should probably add validation here i.e. BroadcastValidation::Gossip + info!( + %slot, + %beacon_block_root, + builder_index = envelope.message.builder_index, + "Publishing signed execution payload envelope to network" + ); + + // Publish to the network + crate::utils::publish_pubsub_message( + network_tx, + PubsubMessage::ExecutionPayload(Box::new(envelope)), + ) + .map_err(|_| { + warn!(%slot, "Failed to publish execution payload envelope to network"); + warp_utils::reject::custom_server_error( + "Unable to publish execution payload envelope to network".into(), + ) + })?; + + Ok(warp::reply().into_response()) +} diff --git a/beacon_node/http_api/src/beacon/mod.rs b/beacon_node/http_api/src/beacon/mod.rs index df5e6eee5c..9ec1c476f6 100644 --- a/beacon_node/http_api/src/beacon/mod.rs +++ b/beacon_node/http_api/src/beacon/mod.rs @@ -1,2 +1,3 @@ +pub mod execution_payload_envelope; pub mod pool; pub mod states; diff --git a/beacon_node/http_api/src/beacon/states.rs b/beacon_node/http_api/src/beacon/states.rs index 828efb86a7..50be7211d8 100644 --- a/beacon_node/http_api/src/beacon/states.rs +++ b/beacon_node/http_api/src/beacon/states.rs @@ -28,7 +28,6 @@ pub fn get_beacon_state_pending_consolidations<T: BeaconChainTypes>( beacon_states_path: BeaconStatesPath<T>, ) -> ResponseFilter { beacon_states_path - .clone() .and(warp::path("pending_consolidations")) .and(warp::path::end()) .then( diff --git a/beacon_node/http_api/src/block_rewards.rs b/beacon_node/http_api/src/block_rewards.rs deleted file mode 100644 index 891f024bf9..0000000000 --- a/beacon_node/http_api/src/block_rewards.rs +++ /dev/null @@ -1,178 +0,0 @@ -use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes, WhenSlotSkipped}; -use eth2::lighthouse::{BlockReward, BlockRewardsQuery}; -use lru::LruCache; -use state_processing::BlockReplayer; -use std::num::NonZeroUsize; -use std::sync::Arc; -use tracing::{debug, warn}; -use types::block::BlindedBeaconBlock; -use types::new_non_zero_usize; -use warp_utils::reject::{beacon_state_error, custom_bad_request, unhandled_error}; - -const STATE_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(2); - -/// Fetch block rewards for blocks from the canonical chain. -pub fn get_block_rewards<T: BeaconChainTypes>( - query: BlockRewardsQuery, - chain: Arc<BeaconChain<T>>, -) -> Result<Vec<BlockReward>, warp::Rejection> { - let start_slot = query.start_slot; - let end_slot = query.end_slot; - let prior_slot = start_slot - 1; - - if start_slot > end_slot || start_slot == 0 { - return Err(custom_bad_request(format!( - "invalid start and end: {}, {}", - start_slot, end_slot - ))); - } - - let end_block_root = chain - .block_root_at_slot(end_slot, WhenSlotSkipped::Prev) - .map_err(unhandled_error)? - .ok_or_else(|| custom_bad_request(format!("block at end slot {} unknown", end_slot)))?; - - let blocks = chain - .store - .load_blocks_to_replay(start_slot, end_slot, end_block_root) - .map_err(|e| unhandled_error(BeaconChainError::from(e)))?; - - let state_root = chain - .state_root_at_slot(prior_slot) - .map_err(unhandled_error)? - .ok_or_else(|| custom_bad_request(format!("prior state at slot {} unknown", prior_slot)))?; - - // This branch is reached from the HTTP API. We assume the user wants - // to cache states so that future calls are faster. - let mut state = chain - .get_state(&state_root, Some(prior_slot), true) - .and_then(|maybe_state| maybe_state.ok_or(BeaconChainError::MissingBeaconState(state_root))) - .map_err(unhandled_error)?; - - state - .build_caches(&chain.spec) - .map_err(beacon_state_error)?; - - let mut reward_cache = Default::default(); - let mut block_rewards = Vec::with_capacity(blocks.len()); - - let block_replayer = BlockReplayer::new(state, &chain.spec) - .pre_block_hook(Box::new(|state, block| { - state.build_all_committee_caches(&chain.spec)?; - - // Compute block reward. - let block_reward = chain.compute_block_reward( - block.message(), - block.canonical_root(), - state, - &mut reward_cache, - query.include_attestations, - )?; - block_rewards.push(block_reward); - Ok(()) - })) - .state_root_iter( - chain - .forwards_iter_state_roots_until(prior_slot, end_slot) - .map_err(unhandled_error)?, - ) - .no_signature_verification() - .minimal_block_root_verification() - .apply_blocks(blocks, None) - .map_err(unhandled_error)?; - - if block_replayer.state_root_miss() { - warn!(%start_slot, %end_slot, "Block reward state root miss"); - } - - drop(block_replayer); - - Ok(block_rewards) -} - -/// Compute block rewards for blocks passed in as input. -pub fn compute_block_rewards<T: BeaconChainTypes>( - blocks: Vec<BlindedBeaconBlock<T::EthSpec>>, - chain: Arc<BeaconChain<T>>, -) -> Result<Vec<BlockReward>, warp::Rejection> { - let mut block_rewards = Vec::with_capacity(blocks.len()); - let mut state_cache = LruCache::new(STATE_CACHE_SIZE); - let mut reward_cache = Default::default(); - - for block in blocks { - let parent_root = block.parent_root(); - - // Check LRU cache for a constructed state from a previous iteration. - let state = if let Some(state) = state_cache.get(&(parent_root, block.slot())) { - debug!( - ?parent_root, - slot = %block.slot(), - "Re-using cached state for block rewards" - ); - state - } else { - debug!( - ?parent_root, - slot = %block.slot(), - "Fetching state for block rewards" - ); - let parent_block = chain - .get_blinded_block(&parent_root) - .map_err(unhandled_error)? - .ok_or_else(|| { - custom_bad_request(format!( - "parent block not known or not canonical: {:?}", - parent_root - )) - })?; - - // This branch is reached from the HTTP API. We assume the user wants - // to cache states so that future calls are faster. - let parent_state = chain - .get_state(&parent_block.state_root(), Some(parent_block.slot()), true) - .map_err(unhandled_error)? - .ok_or_else(|| { - custom_bad_request(format!( - "no state known for parent block: {:?}", - parent_root - )) - })?; - - let block_replayer = BlockReplayer::new(parent_state, &chain.spec) - .no_signature_verification() - .state_root_iter([Ok((parent_block.state_root(), parent_block.slot()))].into_iter()) - .minimal_block_root_verification() - .apply_blocks(vec![], Some(block.slot())) - .map_err(unhandled_error::<BeaconChainError>)?; - - if block_replayer.state_root_miss() { - warn!( - parent_slot = %parent_block.slot(), - slot = %block.slot(), - "Block reward state root miss" - ); - } - - let mut state = block_replayer.into_state(); - state - .build_all_committee_caches(&chain.spec) - .map_err(beacon_state_error)?; - - state_cache.get_or_insert((parent_root, block.slot()), || state) - }; - - // Compute block reward. - let block_reward = chain - .compute_block_reward( - block.to_ref(), - block.canonical_root(), - state, - &mut reward_cache, - true, - ) - .map_err(unhandled_error)?; - block_rewards.push(block_reward); - } - - Ok(block_rewards) -} diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 095c52fb29..0a0ae683ca 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -1,3 +1,4 @@ +#![allow(clippy::result_large_err)] //! This crate contains a HTTP server which serves the endpoints listed here: //! //! https://github.com/ethereum/beacon-APIs @@ -11,7 +12,6 @@ mod attester_duties; mod beacon; mod block_id; mod block_packing_efficiency; -mod block_rewards; mod build_block_contents; mod builder_states; mod custody; @@ -36,6 +36,9 @@ mod validator_inclusion; mod validators; mod version; +use crate::beacon::execution_payload_envelope::{ + post_beacon_execution_payload_envelope, post_beacon_execution_payload_envelope_ssz, +}; use crate::beacon::pool::*; use crate::light_client::{get_light_client_bootstrap, get_light_client_updates}; use crate::utils::{AnyVersionFilter, EthV1Filter}; @@ -50,7 +53,6 @@ use builder_states::get_next_withdrawals; use bytes::Bytes; use context_deserialize::ContextDeserialize; use directory::DEFAULT_ROOT_DIR; -use eth2::StatusCode; use eth2::lighthouse::sync_state::SyncState; use eth2::types::{ self as api_types, BroadcastValidation, EndpointVersion, ForkChoice, ForkChoiceExtraData, @@ -69,6 +71,7 @@ use parking_lot::RwLock; pub use publish_blocks::{ ProvenancedBlock, publish_blinded_block, publish_block, reconstruct_block, }; +use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use slot_clock::SlotClock; use ssz::Encode; @@ -90,8 +93,9 @@ use tokio_stream::{ use tracing::{debug, info, warn}; use types::{ BeaconStateError, Checkpoint, ConfigAndPreset, Epoch, EthSpec, ForkName, Hash256, - SignedBlindedBeaconBlock, Slot, + SignedBlindedBeaconBlock, }; +use validator::execution_payload_envelope::get_validator_execution_payload_envelope; use version::{ ResponseIncludesVersion, V1, V2, add_consensus_version_header, add_ssz_content_type_header, execution_optimistic_finalized_beacon_response, inconsistent_fork_rejection, @@ -1486,6 +1490,22 @@ pub fn serve<T: BeaconChainTypes>( let post_beacon_pool_bls_to_execution_changes = post_beacon_pool_bls_to_execution_changes(&network_tx_filter, &beacon_pool_path); + // POST beacon/execution_payload_envelope + let post_beacon_execution_payload_envelope = post_beacon_execution_payload_envelope( + eth_v1.clone(), + task_spawner_filter.clone(), + chain_filter.clone(), + network_tx_filter.clone(), + ); + + // POST beacon/execution_payload_envelope (SSZ) + let post_beacon_execution_payload_envelope_ssz = post_beacon_execution_payload_envelope_ssz( + eth_v1.clone(), + task_spawner_filter.clone(), + chain_filter.clone(), + network_tx_filter.clone(), + ); + let beacon_rewards_path = eth_v1 .clone() .and(warp::path("beacon")) @@ -2140,12 +2160,9 @@ pub fn serve<T: BeaconChainTypes>( let discovery_addresses = enr.multiaddr_p2p_udp(); Ok(api_types::GenericResponse::from(api_types::IdentityData { peer_id: network_globals.local_peer_id().to_base58(), - enr: enr.to_base64(), - p2p_addresses: p2p_addresses.iter().map(|a| a.to_string()).collect(), - discovery_addresses: discovery_addresses - .iter() - .map(|a| a.to_string()) - .collect(), + enr, + p2p_addresses, + discovery_addresses, metadata: utils::from_meta_data::<T::EthSpec>( &network_globals.local_metadata, &chain.spec, @@ -2447,7 +2464,7 @@ pub fn serve<T: BeaconChainTypes>( // GET validator/duties/proposer/{epoch} let get_validator_duties_proposer = get_validator_duties_proposer( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2455,7 +2472,7 @@ pub fn serve<T: BeaconChainTypes>( // GET validator/blocks/{slot} let get_validator_blocks = get_validator_blocks( - any_version.clone().clone(), + any_version.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2463,7 +2480,15 @@ pub fn serve<T: BeaconChainTypes>( // GET validator/blinded_blocks/{slot} let get_validator_blinded_blocks = get_validator_blinded_blocks( - eth_v1.clone().clone(), + eth_v1.clone(), + chain_filter.clone(), + not_while_syncing_filter.clone(), + task_spawner_filter.clone(), + ); + + // GET validator/execution_payload_envelope/{slot}/{builder_index} + let get_validator_execution_payload_envelope = get_validator_execution_payload_envelope( + eth_v1.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2471,7 +2496,7 @@ pub fn serve<T: BeaconChainTypes>( // GET validator/attestation_data?slot,committee_index let get_validator_attestation_data = get_validator_attestation_data( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2479,7 +2504,7 @@ pub fn serve<T: BeaconChainTypes>( // GET validator/aggregate_attestation?attestation_data_root,slot let get_validator_aggregate_attestation = get_validator_aggregate_attestation( - any_version.clone().clone(), + any_version.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2487,7 +2512,7 @@ pub fn serve<T: BeaconChainTypes>( // POST validator/duties/attester/{epoch} let post_validator_duties_attester = post_validator_duties_attester( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2495,7 +2520,7 @@ pub fn serve<T: BeaconChainTypes>( // POST validator/duties/sync/{epoch} let post_validator_duties_sync = post_validator_duties_sync( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2503,7 +2528,7 @@ pub fn serve<T: BeaconChainTypes>( // GET validator/sync_committee_contribution let get_validator_sync_committee_contribution = get_validator_sync_committee_contribution( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2511,7 +2536,7 @@ pub fn serve<T: BeaconChainTypes>( // POST validator/aggregate_and_proofs let post_validator_aggregate_and_proofs = post_validator_aggregate_and_proofs( - any_version.clone().clone(), + any_version.clone(), chain_filter.clone(), network_tx_filter.clone(), not_while_syncing_filter.clone(), @@ -2519,7 +2544,7 @@ pub fn serve<T: BeaconChainTypes>( ); let post_validator_contribution_and_proofs = post_validator_contribution_and_proofs( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), network_tx_filter.clone(), not_while_syncing_filter.clone(), @@ -2529,7 +2554,7 @@ pub fn serve<T: BeaconChainTypes>( // POST validator/beacon_committee_subscriptions let post_validator_beacon_committee_subscriptions = post_validator_beacon_committee_subscriptions( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), validator_subscription_tx_filter.clone(), task_spawner_filter.clone(), @@ -2537,7 +2562,7 @@ pub fn serve<T: BeaconChainTypes>( // POST validator/prepare_beacon_proposer let post_validator_prepare_beacon_proposer = post_validator_prepare_beacon_proposer( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), network_tx_filter.clone(), not_while_syncing_filter.clone(), @@ -2546,13 +2571,13 @@ pub fn serve<T: BeaconChainTypes>( // POST validator/register_validator let post_validator_register_validator = post_validator_register_validator( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), task_spawner_filter.clone(), ); // POST validator/sync_committee_subscriptions let post_validator_sync_committee_subscriptions = post_validator_sync_committee_subscriptions( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), validator_subscription_tx_filter.clone(), task_spawner_filter.clone(), @@ -2560,7 +2585,7 @@ pub fn serve<T: BeaconChainTypes>( // POST validator/liveness/{epoch} let post_validator_liveness_epoch = post_validator_liveness_epoch( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), task_spawner_filter.clone(), ); @@ -3040,34 +3065,6 @@ pub fn serve<T: BeaconChainTypes>( }, ); - // GET lighthouse/analysis/block_rewards - let get_lighthouse_block_rewards = warp::path("lighthouse") - .and(warp::path("analysis")) - .and(warp::path("block_rewards")) - .and(warp::query::<eth2::lighthouse::BlockRewardsQuery>()) - .and(warp::path::end()) - .and(task_spawner_filter.clone()) - .and(chain_filter.clone()) - .then(|query, task_spawner: TaskSpawner<T::EthSpec>, chain| { - task_spawner.blocking_json_task(Priority::P1, move || { - block_rewards::get_block_rewards(query, chain) - }) - }); - - // POST lighthouse/analysis/block_rewards - let post_lighthouse_block_rewards = warp::path("lighthouse") - .and(warp::path("analysis")) - .and(warp::path("block_rewards")) - .and(warp_utils::json::json()) - .and(warp::path::end()) - .and(task_spawner_filter.clone()) - .and(chain_filter.clone()) - .then(|blocks, task_spawner: TaskSpawner<T::EthSpec>, chain| { - task_spawner.blocking_json_task(Priority::P1, move || { - block_rewards::compute_block_rewards(blocks, chain) - }) - }); - // GET lighthouse/analysis/attestation_performance/{index} let get_lighthouse_attestation_performance = warp::path("lighthouse") .and(warp::path("analysis")) @@ -3101,25 +3098,6 @@ pub fn serve<T: BeaconChainTypes>( }, ); - // GET lighthouse/merge_readiness - let get_lighthouse_merge_readiness = warp::path("lighthouse") - .and(warp::path("merge_readiness")) - .and(warp::path::end()) - .and(task_spawner_filter.clone()) - .and(chain_filter.clone()) - .then( - |task_spawner: TaskSpawner<T::EthSpec>, chain: Arc<BeaconChain<T>>| { - task_spawner.spawn_async_with_rejection(Priority::P1, async move { - let current_slot = chain.slot_clock.now_or_genesis().unwrap_or(Slot::new(0)); - let merge_readiness = chain.check_bellatrix_readiness(current_slot).await; - Ok::<_, warp::reject::Rejection>( - warp::reply::json(&api_types::GenericResponse::from(merge_readiness)) - .into_response(), - ) - }) - }, - ); - let get_events = eth_v1 .clone() .and(warp::path("events")) @@ -3177,9 +3155,6 @@ pub fn serve<T: BeaconChainTypes>( api_types::EventTopic::LightClientOptimisticUpdate => { event_handler.subscribe_light_client_optimistic_update() } - api_types::EventTopic::BlockReward => { - event_handler.subscribe_block_reward() - } api_types::EventTopic::AttesterSlashing => { event_handler.subscribe_attester_slashing() } @@ -3339,6 +3314,7 @@ pub fn serve<T: BeaconChainTypes>( .uor(get_validator_duties_proposer) .uor(get_validator_blocks) .uor(get_validator_blinded_blocks) + .uor(get_validator_execution_payload_envelope) .uor(get_validator_attestation_data) .uor(get_validator_aggregate_attestation) .uor(get_validator_sync_committee_contribution) @@ -3355,14 +3331,12 @@ pub fn serve<T: BeaconChainTypes>( .uor(get_lighthouse_staking) .uor(get_lighthouse_database_info) .uor(get_lighthouse_custody_info) - .uor(get_lighthouse_block_rewards) .uor(get_lighthouse_attestation_performance) .uor(get_beacon_light_client_optimistic_update) .uor(get_beacon_light_client_finality_update) .uor(get_beacon_light_client_bootstrap) .uor(get_beacon_light_client_updates) .uor(get_lighthouse_block_packing_efficiency) - .uor(get_lighthouse_merge_readiness) .uor(get_events) .uor(get_expected_withdrawals) .uor(lighthouse_log_events.boxed()) @@ -3377,7 +3351,8 @@ pub fn serve<T: BeaconChainTypes>( post_beacon_blocks_ssz .uor(post_beacon_blocks_v2_ssz) .uor(post_beacon_blinded_blocks_ssz) - .uor(post_beacon_blinded_blocks_v2_ssz), + .uor(post_beacon_blinded_blocks_v2_ssz) + .uor(post_beacon_execution_payload_envelope_ssz), ) .uor(post_beacon_blocks) .uor(post_beacon_blinded_blocks) @@ -3389,6 +3364,7 @@ pub fn serve<T: BeaconChainTypes>( .uor(post_beacon_pool_voluntary_exits) .uor(post_beacon_pool_sync_committees) .uor(post_beacon_pool_bls_to_execution_changes) + .uor(post_beacon_execution_payload_envelope) .uor(post_beacon_state_validators) .uor(post_beacon_state_validator_balances) .uor(post_beacon_state_validator_identities) @@ -3405,7 +3381,6 @@ pub fn serve<T: BeaconChainTypes>( .uor(post_validator_liveness_epoch) .uor(post_lighthouse_liveness) .uor(post_lighthouse_database_reconstruct) - .uor(post_lighthouse_block_rewards) .uor(post_lighthouse_ui_validator_metrics) .uor(post_lighthouse_ui_validator_info) .uor(post_lighthouse_finalize) diff --git a/beacon_node/http_api/src/produce_block.rs b/beacon_node/http_api/src/produce_block.rs index 6a549c91ef..607221686f 100644 --- a/beacon_node/http_api/src/produce_block.rs +++ b/beacon_node/http_api/src/produce_block.rs @@ -10,8 +10,8 @@ use beacon_chain::graffiti_calculator::GraffitiSettings; use beacon_chain::{ BeaconBlockResponseWrapper, BeaconChain, BeaconChainTypes, ProduceBlockVerification, }; -use eth2::beacon_response::ForkVersionedResponse; use eth2::types::{self as api_types, ProduceBlockV3Metadata, SkipRandaoVerification}; +use eth2::{beacon_response::ForkVersionedResponse, types::ProduceBlockV4Metadata}; use ssz::Encode; use std::sync::Arc; use tracing::instrument; @@ -43,6 +43,49 @@ pub fn get_randao_verification( Ok(randao_verification) } +#[instrument( + name = "lh_produce_block_v4", + skip_all, + fields(%slot) +)] +pub async fn produce_block_v4<T: BeaconChainTypes>( + accept_header: Option<api_types::Accept>, + chain: Arc<BeaconChain<T>>, + slot: Slot, + query: api_types::ValidatorBlocksQuery, +) -> Result<Response<Body>, warp::Rejection> { + let randao_reveal = query.randao_reveal.decompress().map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "randao reveal is not a valid BLS signature: {:?}", + e + )) + })?; + + let randao_verification = get_randao_verification(&query, randao_reveal.is_infinity())?; + let builder_boost_factor = if query.builder_boost_factor == Some(DEFAULT_BOOST_FACTOR) { + None + } else { + query.builder_boost_factor + }; + + let graffiti_settings = GraffitiSettings::new(query.graffiti, query.graffiti_policy); + + let (block, consensus_block_value) = chain + .produce_block_with_verification_gloas( + randao_reveal, + slot, + graffiti_settings, + randao_verification, + builder_boost_factor, + ) + .await + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("failed to fetch a block: {:?}", e)) + })?; + + build_response_v4::<T>(block, consensus_block_value, accept_header, &chain.spec) +} + #[instrument( name = "lh_produce_block_v3", skip_all, @@ -87,6 +130,45 @@ pub async fn produce_block_v3<T: BeaconChainTypes>( build_response_v3(chain, block_response_type, accept_header) } +pub fn build_response_v4<T: BeaconChainTypes>( + block: BeaconBlock<T::EthSpec, FullPayload<T::EthSpec>>, + consensus_block_value: u64, + accept_header: Option<api_types::Accept>, + spec: &ChainSpec, +) -> Result<Response<Body>, warp::Rejection> { + let fork_name = block + .to_ref() + .fork_name(spec) + .map_err(inconsistent_fork_rejection)?; + let consensus_block_value_wei = + Uint256::from(consensus_block_value) * Uint256::from(1_000_000_000u64); + + let metadata = ProduceBlockV4Metadata { + consensus_version: fork_name, + consensus_block_value: consensus_block_value_wei, + }; + + match accept_header { + Some(api_types::Accept::Ssz) => Response::builder() + .status(200) + .body(block.as_ssz_bytes().into()) + .map(|res: Response<Body>| add_ssz_content_type_header(res)) + .map(|res: Response<Body>| add_consensus_version_header(res, fork_name)) + .map(|res| add_consensus_block_value_header(res, consensus_block_value_wei)) + .map_err(|e| -> warp::Rejection { + warp_utils::reject::custom_server_error(format!("failed to create response: {}", e)) + }), + _ => Ok(warp::reply::json(&ForkVersionedResponse { + version: fork_name, + metadata, + data: block, + }) + .into_response()) + .map(|res| add_consensus_version_header(res, fork_name)) + .map(|res| add_consensus_block_value_header(res, consensus_block_value_wei)), + } +} + pub fn build_response_v3<T: BeaconChainTypes>( chain: Arc<BeaconChain<T>>, block_response: BeaconBlockResponseWrapper<T::EthSpec>, diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 2d22dda66a..272dc0484e 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -9,18 +9,16 @@ use beacon_chain::{ AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, IntoGossipVerifiedBlock, NotifyExecutionLayer, build_blob_data_column_sidecars, }; -use eth2::{ - StatusCode, - types::{ - BlobsBundle, BroadcastValidation, ErrorMessage, ExecutionPayloadAndBlobs, - FullPayloadContents, PublishBlockRequest, SignedBlockContents, - }, +use eth2::types::{ + BlobsBundle, BroadcastValidation, ErrorMessage, ExecutionPayloadAndBlobs, FullPayloadContents, + PublishBlockRequest, SignedBlockContents, }; use execution_layer::{ProvenancedPayload, SubmitBlindedBlockResponse}; use futures::TryFutureExt; use lighthouse_network::PubsubMessage; use network::NetworkMessage; use rand::prelude::SliceRandom; +use reqwest::StatusCode; use std::marker::PhantomData; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; diff --git a/beacon_node/http_api/src/validator/execution_payload_envelope.rs b/beacon_node/http_api/src/validator/execution_payload_envelope.rs new file mode 100644 index 0000000000..c40b375e49 --- /dev/null +++ b/beacon_node/http_api/src/validator/execution_payload_envelope.rs @@ -0,0 +1,110 @@ +use crate::task_spawner::{Priority, TaskSpawner}; +use crate::utils::{ + ChainFilter, EthV1Filter, NotWhileSyncingFilter, ResponseFilter, TaskSpawnerFilter, +}; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use eth2::beacon_response::{EmptyMetadata, ForkVersionedResponse}; +use eth2::types::Accept; +use ssz::Encode; +use std::sync::Arc; +use tracing::debug; +use types::Slot; +use warp::http::Response; +use warp::{Filter, Rejection}; + +// GET validator/execution_payload_envelope/{slot}/{builder_index} +pub fn get_validator_execution_payload_envelope<T: BeaconChainTypes>( + eth_v1: EthV1Filter, + chain_filter: ChainFilter<T>, + not_while_syncing_filter: NotWhileSyncingFilter, + task_spawner_filter: TaskSpawnerFilter<T>, +) -> ResponseFilter { + eth_v1 + .and(warp::path("validator")) + .and(warp::path("execution_payload_envelope")) + .and(warp::path::param::<Slot>().or_else(|_| async { + Err(warp_utils::reject::custom_bad_request( + "Invalid slot".to_string(), + )) + })) + .and(warp::path::param::<u64>().or_else(|_| async { + Err(warp_utils::reject::custom_bad_request( + "Invalid builder_index".to_string(), + )) + })) + .and(warp::path::end()) + .and(warp::header::optional::<Accept>("accept")) + .and(not_while_syncing_filter) + .and(task_spawner_filter) + .and(chain_filter) + .then( + |slot: Slot, + // TODO(gloas) we're only doing local building + // we'll need to implement builder index logic + // eventually. + _builder_index: u64, + accept_header: Option<Accept>, + not_synced_filter: Result<(), Rejection>, + task_spawner: TaskSpawner<T::EthSpec>, + chain: Arc<BeaconChain<T>>| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + debug!(?slot, "Execution payload envelope request from HTTP API"); + + not_synced_filter?; + + // Get the envelope from the pending cache (local building only) + let envelope = chain + .pending_payload_envelopes + .read() + .get(slot) + .cloned() + .ok_or_else(|| { + warp_utils::reject::custom_not_found(format!( + "Execution payload envelope not available for slot {slot}" + )) + })?; + + let fork_name = chain.spec.fork_name_at_slot::<T::EthSpec>(slot); + + match accept_header { + Some(Accept::Ssz) => Response::builder() + .status(200) + .header("Content-Type", "application/octet-stream") + .header("Eth-Consensus-Version", fork_name.to_string()) + .body(envelope.as_ssz_bytes().into()) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "Failed to build SSZ response: {e}" + )) + }), + _ => { + let json_response = ForkVersionedResponse { + version: fork_name, + metadata: EmptyMetadata {}, + data: envelope, + }; + Response::builder() + .status(200) + .header("Content-Type", "application/json") + .header("Eth-Consensus-Version", fork_name.to_string()) + .body( + serde_json::to_string(&json_response) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "Failed to serialize response: {e}" + )) + })? + .into(), + ) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "Failed to build JSON response: {e}" + )) + }) + } + } + }) + }, + ) + .boxed() +} diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index b1ab4c648a..a9082df715 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -1,4 +1,6 @@ -use crate::produce_block::{produce_blinded_block_v2, produce_block_v2, produce_block_v3}; +use crate::produce_block::{ + produce_blinded_block_v2, produce_block_v2, produce_block_v3, produce_block_v4, +}; use crate::task_spawner::{Priority, TaskSpawner}; use crate::utils::{ AnyVersionFilter, ChainFilter, EthV1Filter, NetworkTxFilter, NotWhileSyncingFilter, @@ -10,7 +12,6 @@ use beacon_chain::attestation_verification::VerifiedAttestation; use beacon_chain::validator_monitor::timestamp_now; use beacon_chain::{AttestationError, BeaconChain, BeaconChainError, BeaconChainTypes}; use bls::PublicKeyBytes; -use eth2::StatusCode; use eth2::types::{ Accept, BeaconCommitteeSubscription, EndpointVersion, Failure, GenericResponse, StandardLivenessResponseData, StateId as CoreStateId, ValidatorAggregateAttestationQuery, @@ -18,6 +19,7 @@ use eth2::types::{ }; use lighthouse_network::PubsubMessage; use network::{NetworkMessage, ValidatorSubscriptionMessage}; +use reqwest::StatusCode; use slot_clock::SlotClock; use std::sync::Arc; use tokio::sync::mpsc::{Sender, UnboundedSender}; @@ -31,6 +33,8 @@ use types::{ use warp::{Filter, Rejection, Reply}; use warp_utils::reject::convert_rejection; +pub mod execution_payload_envelope; + /// Uses the `chain.validator_pubkey_cache` to resolve a pubkey to a validator /// index and then ensures that the validator exists in the given `state`. pub fn pubkey_to_validator_index<T: BeaconChainTypes>( @@ -316,7 +320,11 @@ pub fn get_validator_blocks<T: BeaconChainTypes>( not_synced_filter?; - if endpoint_version == V3 { + // Use V4 block production for Gloas fork + let fork_name = chain.spec.fork_name_at_slot::<T::EthSpec>(slot); + if fork_name.gloas_enabled() { + produce_block_v4(accept_header, chain, slot, query).await + } else if endpoint_version == V3 { produce_block_v3(accept_header, chain, slot, query).await } else { produce_block_v2(accept_header, chain, slot, query).await @@ -662,15 +670,26 @@ pub fn post_validator_prepare_beacon_proposer<T: BeaconChainTypes>( ) .await; - chain - .prepare_beacon_proposer(current_slot) - .await - .map_err(|e| { - warp_utils::reject::custom_bad_request(format!( - "error updating proposer preparations: {:?}", - e - )) - })?; + // TODO(gloas): verify this is correct. We skip proposer preparation for + // GLOAS because the execution payload is no longer embedded in the beacon + // block (it's in the payload envelope), so the head block's + // execution_payload() is unavailable. + let next_slot = current_slot + 1; + if !chain + .spec + .fork_name_at_slot::<T::EthSpec>(next_slot) + .gloas_enabled() + { + chain + .prepare_beacon_proposer(current_slot) + .await + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "error updating proposer preparations: {:?}", + e + )) + })?; + } if chain.spec.is_peer_das_scheduled() { let (finalized_beacon_state, _, _) = @@ -708,6 +727,18 @@ pub fn post_validator_prepare_beacon_proposer<T: BeaconChainTypes>( debug!(error = %e, "Could not send message to the network service. \ Likely shutdown") }); + + // Write the updated custody context to disk. This happens at most 128 + // times ever, so the I/O burden should be extremely minimal. Without a + // write here we risk forgetting custody backfill progress upon an + // unclean shutdown. The custody context is otherwise only persisted in + // `BeaconChain::drop`. + if let Err(error) = chain.persist_custody_context() { + error!( + ?error, + "Failed to persist custody context after CGC update" + ); + } } } diff --git a/beacon_node/http_api/tests/broadcast_validation_tests.rs b/beacon_node/http_api/tests/broadcast_validation_tests.rs index 357b78cf41..a380f62ecf 100644 --- a/beacon_node/http_api/tests/broadcast_validation_tests.rs +++ b/beacon_node/http_api/tests/broadcast_validation_tests.rs @@ -4,11 +4,11 @@ use beacon_chain::{ GossipVerifiedBlock, IntoGossipVerifiedBlock, WhenSlotSkipped, test_utils::{AttestationStrategy, BlockStrategy}, }; -use eth2::reqwest::{Response, StatusCode}; use eth2::types::{BroadcastValidation, PublishBlockRequest}; use fixed_bytes::FixedBytesExtended; use http_api::test_utils::InteractiveTester; use http_api::{Config, ProvenancedBlock, publish_blinded_block, publish_block, reconstruct_block}; +use reqwest::{Response, StatusCode}; use std::collections::HashSet; use std::sync::Arc; use types::{ColumnIndex, Epoch, EthSpec, ForkName, Hash256, MainnetEthSpec, Slot}; @@ -85,14 +85,18 @@ pub async fn gossip_invalid() { /* mandated by Beacon API spec */ assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); + // The error depends on whether blobs exist (which affects validation order): + // - Pre-Deneb (no blobs): block validation runs first -> NotFinalizedDescendant + // - Deneb/Electra (blobs): blob validation runs first -> ParentUnknown + // - Fulu+ (columns): block validation runs first -> NotFinalizedDescendant let pre_finalized_block_root = Hash256::zero(); - let expected_error_msg = if tester.harness.spec.is_fulu_scheduled() { + let expected_error_msg = if tester.harness.spec.deneb_fork_epoch.is_none() + || tester.harness.spec.is_fulu_scheduled() + { format!( "BAD_REQUEST: NotFinalizedDescendant {{ block_parent_root: {pre_finalized_block_root:?} }}" ) } else { - // Since Deneb, the invalidity of the blobs will be detected prior to the invalidity of the - // block. format!("BAD_REQUEST: ParentUnknown {{ parent_root: {pre_finalized_block_root:?} }}") }; @@ -283,13 +287,13 @@ pub async fn consensus_invalid() { assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); let pre_finalized_block_root = Hash256::zero(); - let expected_error_msg = if tester.harness.spec.is_fulu_scheduled() { + let expected_error_msg = if tester.harness.spec.deneb_fork_epoch.is_none() + || tester.harness.spec.is_fulu_scheduled() + { format!( "BAD_REQUEST: NotFinalizedDescendant {{ block_parent_root: {pre_finalized_block_root:?} }}" ) } else { - // Since Deneb, the invalidity of the blobs will be detected prior to the invalidity of the - // block. format!("BAD_REQUEST: ParentUnknown {{ parent_root: {pre_finalized_block_root:?} }}") }; @@ -520,13 +524,13 @@ pub async fn equivocation_invalid() { assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); let pre_finalized_block_root = Hash256::zero(); - let expected_error_msg = if tester.harness.spec.is_fulu_scheduled() { + let expected_error_msg = if tester.harness.spec.deneb_fork_epoch.is_none() + || tester.harness.spec.is_fulu_scheduled() + { format!( "BAD_REQUEST: NotFinalizedDescendant {{ block_parent_root: {pre_finalized_block_root:?} }}" ) } else { - // Since Deneb, the invalidity of the blobs will be detected prior to the invalidity of the - // block. format!("BAD_REQUEST: ParentUnknown {{ parent_root: {pre_finalized_block_root:?} }}") }; @@ -845,16 +849,17 @@ pub async fn blinded_gossip_invalid() { assert!(response.is_err()); let error_response: eth2::Error = response.err().unwrap(); + /* mandated by Beacon API spec */ assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); let pre_finalized_block_root = Hash256::zero(); - let expected_error_msg = if tester.harness.spec.is_fulu_scheduled() { + let expected_error_msg = if tester.harness.spec.deneb_fork_epoch.is_none() + || tester.harness.spec.is_fulu_scheduled() + { format!( "BAD_REQUEST: NotFinalizedDescendant {{ block_parent_root: {pre_finalized_block_root:?} }}" ) } else { - // Since Deneb, the invalidity of the blobs will be detected prior to the invalidity of the - // block. format!("BAD_REQUEST: ParentUnknown {{ parent_root: {pre_finalized_block_root:?} }}") }; @@ -1070,10 +1075,16 @@ pub async fn blinded_consensus_invalid() { ); } else { assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); - assert_server_message_error( - error_response, - format!("BAD_REQUEST: ParentUnknown {{ parent_root: {pre_finalized_block_root:?} }}"), - ); + let expected_error_msg = if tester.harness.spec.deneb_fork_epoch.is_none() + || tester.harness.spec.is_fulu_scheduled() + { + format!( + "BAD_REQUEST: NotFinalizedDescendant {{ block_parent_root: {pre_finalized_block_root:?} }}" + ) + } else { + format!("BAD_REQUEST: ParentUnknown {{ parent_root: {pre_finalized_block_root:?} }}") + }; + assert_server_message_error(error_response, expected_error_msg); } } @@ -1253,10 +1264,16 @@ pub async fn blinded_equivocation_invalid() { ); } else { assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); - assert_server_message_error( - error_response, - format!("BAD_REQUEST: ParentUnknown {{ parent_root: {pre_finalized_block_root:?} }}"), - ); + let expected_error_msg = if tester.harness.spec.deneb_fork_epoch.is_none() + || tester.harness.spec.is_fulu_scheduled() + { + format!( + "BAD_REQUEST: NotFinalizedDescendant {{ block_parent_root: {pre_finalized_block_root:?} }}" + ) + } else { + format!("BAD_REQUEST: ParentUnknown {{ parent_root: {pre_finalized_block_root:?} }}") + }; + assert_server_message_error(error_response, expected_error_msg); } } @@ -1957,6 +1974,13 @@ pub async fn duplicate_block_status_code() { let validator_count = 64; let num_initial: u64 = 31; let duplicate_block_status_code = StatusCode::IM_A_TEAPOT; + + // Check if deneb is enabled, which is required for blobs. + let spec = test_spec::<E>(); + if !spec.fork_name_at_slot::<E>(Slot::new(0)).deneb_enabled() { + return; + } + let tester = InteractiveTester::<E>::new_with_initializer_and_mutator( None, validator_count, diff --git a/beacon_node/http_api/tests/fork_tests.rs b/beacon_node/http_api/tests/fork_tests.rs index b96c8bd112..4ba35c238c 100644 --- a/beacon_node/http_api/tests/fork_tests.rs +++ b/beacon_node/http_api/tests/fork_tests.rs @@ -404,7 +404,7 @@ async fn bls_to_execution_changes_update_all_around_capella_fork() { bls_withdrawal_credentials(&keypair.pk, spec) } - let header = generate_genesis_header(&spec, true); + let header = generate_genesis_header(&spec); let genesis_state = InteropGenesisBuilder::new() .set_opt_execution_payload_header(header) diff --git a/beacon_node/http_api/tests/interactive_tests.rs b/beacon_node/http_api/tests/interactive_tests.rs index 21458057c4..a18dd10464 100644 --- a/beacon_node/http_api/tests/interactive_tests.rs +++ b/beacon_node/http_api/tests/interactive_tests.rs @@ -450,13 +450,7 @@ pub async fn proposer_boost_re_org_test( let execution_ctx = mock_el.server.ctx.clone(); let slot_clock = &harness.chain.slot_clock; - // Move to terminal block. mock_el.server.all_payloads_valid(); - execution_ctx - .execution_block_generator - .write() - .move_to_terminal_block() - .unwrap(); // Send proposer preparation data for all validators. let proposer_preparation_data = all_validators diff --git a/beacon_node/http_api/tests/status_tests.rs b/beacon_node/http_api/tests/status_tests.rs index 556b75cb85..791e643ec4 100644 --- a/beacon_node/http_api/tests/status_tests.rs +++ b/beacon_node/http_api/tests/status_tests.rs @@ -3,9 +3,9 @@ use beacon_chain::{ BlockError, test_utils::{AttestationStrategy, BlockStrategy, LightClientStrategy, SyncCommitteeStrategy}, }; -use eth2::StatusCode; use execution_layer::{PayloadStatusV1, PayloadStatusV1Status}; use http_api::test_utils::InteractiveTester; +use reqwest::StatusCode; use types::{EthSpec, ExecPayload, ForkName, MinimalEthSpec, Slot, Uint256}; type E = MinimalEthSpec; @@ -21,15 +21,8 @@ async fn post_merge_tester(chain_depth: u64, validator_count: u64) -> Interactiv let tester = InteractiveTester::<E>::new(Some(spec), validator_count as usize).await; let harness = &tester.harness; let mock_el = harness.mock_execution_layer.as_ref().unwrap(); - let execution_ctx = mock_el.server.ctx.clone(); - // Move to terminal block. mock_el.server.all_payloads_valid(); - execution_ctx - .execution_block_generator - .write() - .move_to_terminal_block() - .unwrap(); // Create some chain depth. harness.advance_slot(); diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index bef9fe6acd..6696e109a5 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -6,15 +6,15 @@ use beacon_chain::{ AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, test_spec, }, }; -use bls::{AggregateSignature, Keypair, PublicKeyBytes, Signature, SignatureBytes}; +use bls::{AggregateSignature, Keypair, PublicKeyBytes, SecretKey, Signature, SignatureBytes}; use eth2::{ BeaconNodeHttpClient, Error, Error::ServerMessage, - StatusCode, Timeouts, + Timeouts, mixin::{RequestAccept, ResponseForkName, ResponseOptional}, - reqwest::{RequestBuilder, Response}, types::{ - BlockId as CoreBlockId, ForkChoiceNode, ProduceBlockV3Response, StateId as CoreStateId, *, + BlockId as CoreBlockId, ForkChoiceNode, ProduceBlockV3Response, ProduceBlockV4Metadata, + StateId as CoreStateId, *, }, }; use execution_layer::expected_gas_limit; @@ -34,6 +34,7 @@ use network::NetworkReceivers; use network_utils::enr_ext::EnrExt; use operation_pool::attestation_storage::CheckpointKey; use proto_array::ExecutionStatus; +use reqwest::{RequestBuilder, Response, StatusCode}; use sensitive_url::SensitiveUrl; use slot_clock::SlotClock; use ssz::BitList; @@ -47,7 +48,8 @@ use tree_hash::TreeHash; use types::ApplicationDomain; use types::{ Domain, EthSpec, ExecutionBlockHash, Hash256, MainnetEthSpec, RelativeEpoch, SelectionProof, - SignedRoot, SingleAttestation, Slot, attestation::AttestationBase, + SignedExecutionPayloadEnvelope, SignedRoot, SingleAttestation, Slot, + attestation::AttestationBase, consts::gloas::BUILDER_INDEX_SELF_BUILD, }; type E = MainnetEthSpec; @@ -135,7 +137,7 @@ impl ApiTester { let mut harness = BeaconChainHarness::builder(MainnetEthSpec) .spec(spec.clone()) .chain_config(ChainConfig { - reconstruct_historic_states: config.retain_historic_states, + archive: config.retain_historic_states, ..ChainConfig::default() }) .deterministic_keypairs(VALIDATOR_COUNT) @@ -145,15 +147,6 @@ impl ApiTester { .node_custody_type(config.node_custody_type) .build(); - harness - .mock_execution_layer - .as_ref() - .unwrap() - .server - .execution_block_generator() - .move_to_terminal_block() - .unwrap(); - harness.advance_slot(); for _ in 0..CHAIN_LENGTH { @@ -2855,19 +2848,9 @@ impl ApiTester { let expected = IdentityData { peer_id: self.local_enr.peer_id().to_string(), - enr: self.local_enr.to_base64(), - p2p_addresses: self - .local_enr - .multiaddr_p2p_tcp() - .iter() - .map(|a| a.to_string()) - .collect(), - discovery_addresses: self - .local_enr - .multiaddr_p2p_udp() - .iter() - .map(|a| a.to_string()) - .collect(), + enr: self.local_enr.clone(), + p2p_addresses: self.local_enr.multiaddr_p2p_tcp(), + discovery_addresses: self.local_enr.multiaddr_p2p_udp(), metadata: MetaData::V2(MetaDataV2 { seq_number: 0, attnets: "0x0000000000000000".to_string(), @@ -2896,7 +2879,7 @@ impl ApiTester { pub async fn test_get_node_peers_by_id(self) -> Self { let result = self .client - .get_node_peers_by_id(&self.external_peer_id.to_string()) + .get_node_peers_by_id(self.external_peer_id) .await .unwrap() .data; @@ -3736,6 +3719,229 @@ impl ApiTester { self } + /// Get the proposer secret key and randao reveal for the given slot. + async fn proposer_setup( + &self, + slot: Slot, + epoch: Epoch, + fork: &Fork, + genesis_validators_root: Hash256, + ) -> (SecretKey, SignatureBytes) { + let proposer_pubkey_bytes = self + .client + .get_validator_duties_proposer(epoch) + .await + .unwrap() + .data + .into_iter() + .find(|duty| duty.slot == slot) + .map(|duty| duty.pubkey) + .unwrap(); + let proposer_pubkey = (&proposer_pubkey_bytes).try_into().unwrap(); + + let sk = self + .validator_keypairs() + .iter() + .find(|kp| kp.pk == proposer_pubkey) + .map(|kp| kp.sk.clone()) + .unwrap(); + + let randao_reveal = { + let domain = + self.chain + .spec + .get_domain(epoch, Domain::Randao, fork, genesis_validators_root); + let message = epoch.signing_root(domain); + sk.sign(message).into() + }; + + (sk, randao_reveal) + } + + /// Assert block metadata and verify the envelope cache. + fn assert_v4_block_metadata( + &self, + block: &BeaconBlock<E>, + metadata: &ProduceBlockV4Metadata, + slot: Slot, + ) { + assert_eq!( + metadata.consensus_version, + block.to_ref().fork_name(&self.chain.spec).unwrap() + ); + assert!(!metadata.consensus_block_value.is_zero()); + + let block_root = block.tree_hash_root(); + let envelope = self + .chain + .pending_payload_envelopes + .read() + .get(slot) + .cloned() + .expect("envelope should exist in pending cache for local building"); + assert_eq!(envelope.beacon_block_root, block_root); + assert_eq!(envelope.slot, slot); + } + + /// Assert envelope fields match the expected block root and slot. + fn assert_envelope_fields( + &self, + envelope: &ExecutionPayloadEnvelope<E>, + block_root: Hash256, + slot: Slot, + ) { + assert_eq!(envelope.beacon_block_root, block_root); + assert_eq!(envelope.slot, slot); + assert_eq!(envelope.builder_index, BUILDER_INDEX_SELF_BUILD); + assert_ne!(envelope.state_root, Hash256::ZERO); + } + + /// Sign an execution payload envelope. + fn sign_envelope( + &self, + envelope: ExecutionPayloadEnvelope<E>, + sk: &SecretKey, + epoch: Epoch, + fork: &Fork, + genesis_validators_root: Hash256, + ) -> SignedExecutionPayloadEnvelope<E> { + let domain = + self.chain + .spec + .get_domain(epoch, Domain::BeaconBuilder, fork, genesis_validators_root); + let signing_root = envelope.signing_root(domain); + let signature = sk.sign(signing_root); + + SignedExecutionPayloadEnvelope { + message: envelope, + signature, + } + } + + /// Test V4 block production (JSON). Only runs if Gloas is scheduled. + pub async fn test_block_production_v4(self) -> Self { + if !self.chain.spec.is_gloas_scheduled() { + return self; + } + + let fork = self.chain.canonical_head.cached_head().head_fork(); + let genesis_validators_root = self.chain.genesis_validators_root; + + for _ in 0..E::slots_per_epoch() * 3 { + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + let fork_name = self.chain.spec.fork_name_at_slot::<E>(slot); + + if !fork_name.gloas_enabled() { + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + continue; + } + + let (sk, randao_reveal) = self + .proposer_setup(slot, epoch, &fork, genesis_validators_root) + .await; + + let (response, metadata) = self + .client + .get_validator_blocks_v4::<E>(slot, &randao_reveal, None, None, None) + .await + .unwrap(); + let block = response.data; + + self.assert_v4_block_metadata(&block, &metadata, slot); + + let envelope = self + .client + .get_validator_execution_payload_envelope::<E>(slot, BUILDER_INDEX_SELF_BUILD) + .await + .unwrap() + .data; + + self.assert_envelope_fields(&envelope, block.tree_hash_root(), slot); + + let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); + let signed_block_request = + PublishBlockRequest::try_from(Arc::new(signed_block.clone())).unwrap(); + self.client + .post_beacon_blocks_v2(&signed_block_request, None) + .await + .unwrap(); + assert_eq!(self.chain.head_beacon_block(), Arc::new(signed_block)); + + let signed_envelope = + self.sign_envelope(envelope, &sk, epoch, &fork, genesis_validators_root); + self.client + .post_beacon_execution_payload_envelope(&signed_envelope, fork_name) + .await + .unwrap(); + + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + } + + self + } + + /// Test V4 block production (SSZ). Only runs if Gloas is scheduled. + pub async fn test_block_production_v4_ssz(self) -> Self { + if !self.chain.spec.is_gloas_scheduled() { + return self; + } + + let fork = self.chain.canonical_head.cached_head().head_fork(); + let genesis_validators_root = self.chain.genesis_validators_root; + + for _ in 0..E::slots_per_epoch() * 3 { + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + let fork_name = self.chain.spec.fork_name_at_slot::<E>(slot); + + if !fork_name.gloas_enabled() { + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + continue; + } + + let (sk, randao_reveal) = self + .proposer_setup(slot, epoch, &fork, genesis_validators_root) + .await; + + let (block, metadata) = self + .client + .get_validator_blocks_v4_ssz::<E>(slot, &randao_reveal, None, None, None) + .await + .unwrap(); + + self.assert_v4_block_metadata(&block, &metadata, slot); + + let envelope = self + .client + .get_validator_execution_payload_envelope_ssz::<E>(slot, BUILDER_INDEX_SELF_BUILD) + .await + .unwrap(); + + self.assert_envelope_fields(&envelope, block.tree_hash_root(), slot); + + let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); + let signed_block_request = + PublishBlockRequest::try_from(Arc::new(signed_block.clone())).unwrap(); + self.client + .post_beacon_blocks_v2_ssz(&signed_block_request, None) + .await + .unwrap(); + assert_eq!(self.chain.head_beacon_block(), Arc::new(signed_block)); + + let signed_envelope = + self.sign_envelope(envelope, &sk, epoch, &fork, genesis_validators_root); + self.client + .post_beacon_execution_payload_envelope_ssz(&signed_envelope, fork_name) + .await + .unwrap(); + + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + } + + self + } + pub async fn test_block_production_no_verify_randao(self) -> Self { for _ in 0..E::slots_per_epoch() { let slot = self.chain.slot().unwrap(); @@ -7469,6 +7675,22 @@ async fn block_production_v3_ssz_with_skip_slots() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn block_production_v4() { + ApiTester::new_with_hard_forks() + .await + .test_block_production_v4() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn block_production_v4_ssz() { + ApiTester::new_with_hard_forks() + .await + .test_block_production_v4_ssz() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn blinded_block_production_full_payload_premerge() { ApiTester::new().await.test_blinded_block_production().await; diff --git a/beacon_node/lighthouse_network/src/discovery/enr.rs b/beacon_node/lighthouse_network/src/discovery/enr.rs index 4c285ea86c..01a01d55ab 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr.rs @@ -200,11 +200,23 @@ pub fn build_enr<E: EthSpec>( builder.ip6(*ip); } - if let Some(udp4_port) = config.enr_udp4_port { + // If the ENR port is not set, and we are listening over that ip version, use the listening + // discovery port instead. + if let Some(udp4_port) = config.enr_udp4_port.or_else(|| { + config + .listen_addrs() + .v4() + .and_then(|v4_addr| v4_addr.disc_port.try_into().ok()) + }) { builder.udp4(udp4_port.get()); } - if let Some(udp6_port) = config.enr_udp6_port { + if let Some(udp6_port) = config.enr_udp6_port.or_else(|| { + config + .listen_addrs() + .v6() + .and_then(|v6_addr| v6_addr.disc_port.try_into().ok()) + }) { builder.udp6(udp6_port.get()); } diff --git a/beacon_node/lighthouse_network/src/discovery/mod.rs b/beacon_node/lighthouse_network/src/discovery/mod.rs index 38a6a84b44..21b1146aff 100644 --- a/beacon_node/lighthouse_network/src/discovery/mod.rs +++ b/beacon_node/lighthouse_network/src/discovery/mod.rs @@ -674,7 +674,7 @@ impl<E: EthSpec> Discovery<E> { /// updates the min_ttl field. fn add_subnet_query(&mut self, subnet: Subnet, min_ttl: Option<Instant>, retries: usize) { // remove the entry and complete the query if greater than the maximum search count - if retries > MAX_DISCOVERY_RETRY { + if retries >= MAX_DISCOVERY_RETRY { debug!("Subnet peer discovery did not find sufficient peers. Reached max retry limit"); return; } diff --git a/beacon_node/lighthouse_network/src/rpc/codec.rs b/beacon_node/lighthouse_network/src/rpc/codec.rs index 34a3bfb026..90730d8416 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec.rs @@ -457,6 +457,9 @@ fn handle_error<T>( Ok(None) } } + // All snappy errors from the snap crate bubble up as `Other` kind errors + // that imply invalid response + ErrorKind::Other => Err(RPCError::InvalidData(err.to_string())), _ => Err(RPCError::from(err)), } } @@ -2319,4 +2322,43 @@ mod tests { RPCError::InvalidData(_) )); } + + /// Test invalid snappy response. + #[test] + fn test_invalid_snappy_response() { + let spec = spec_with_all_forks_enabled(); + let fork_ctx = Arc::new(fork_context(ForkName::latest(), &spec)); + let max_packet_size = spec.max_payload_size as usize; // 10 MiB. + + let protocol = ProtocolId::new(SupportedProtocol::BlocksByRangeV2, Encoding::SSZSnappy); + + let mut codec = SSZSnappyOutboundCodec::<Spec>::new( + protocol.clone(), + max_packet_size, + fork_ctx.clone(), + ); + + let mut payload = BytesMut::new(); + payload.extend_from_slice(&[0u8]); + let deneb_epoch = spec.deneb_fork_epoch.unwrap(); + payload.extend_from_slice(&fork_ctx.context_bytes(deneb_epoch)); + + // Claim the MAXIMUM allowed size (10 MiB) + let claimed_size = max_packet_size; + let mut uvi_codec: Uvi<usize> = Uvi::default(); + uvi_codec.encode(claimed_size, &mut payload).unwrap(); + payload.extend_from_slice(&[0xBB; 16]); // Junk snappy. + + let result = codec.decode(&mut payload); + + assert!(result.is_err(), "Expected decode to fail"); + + // IoError = reached snappy decode (allocation happened). + let err = result.unwrap_err(); + assert!( + matches!(err, RPCError::InvalidData(_)), + "Should return invalid data variant {}", + err + ); + } } diff --git a/beacon_node/lighthouse_network/src/rpc/handler.rs b/beacon_node/lighthouse_network/src/rpc/handler.rs index 720895bbe7..9861119ac1 100644 --- a/beacon_node/lighthouse_network/src/rpc/handler.rs +++ b/beacon_node/lighthouse_network/src/rpc/handler.rs @@ -13,7 +13,8 @@ use futures::prelude::*; use libp2p::PeerId; use libp2p::swarm::handler::{ ConnectionEvent, ConnectionHandler, ConnectionHandlerEvent, DialUpgradeError, - FullyNegotiatedInbound, FullyNegotiatedOutbound, StreamUpgradeError, SubstreamProtocol, + FullyNegotiatedInbound, FullyNegotiatedOutbound, ListenUpgradeError, StreamUpgradeError, + SubstreamProtocol, }; use libp2p::swarm::{ConnectionId, Stream}; use logging::crit; @@ -888,6 +889,16 @@ where ConnectionEvent::DialUpgradeError(DialUpgradeError { info, error }) => { self.on_dial_upgrade_error(info, error) } + ConnectionEvent::ListenUpgradeError(ListenUpgradeError { + error: (proto, error), + .. + }) => { + self.events_out.push(HandlerEvent::Err(HandlerErr::Inbound { + id: self.current_inbound_substream_id, + proto, + error, + })); + } _ => { // NOTE: ConnectionEvent is a non exhaustive enum so updates should be based on // release notes more than compiler feedback @@ -924,7 +935,7 @@ where request.count() )), })); - return self.shutdown(None); + return; } } RequestType::BlobsByRange(request) => { @@ -940,7 +951,7 @@ where max_allowed, max_requested_blobs )), })); - return self.shutdown(None); + return; } } _ => {} diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index f0ac9d00f9..b75ca72eda 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -675,7 +675,7 @@ where E: EthSpec, { type Output = InboundOutput<TSocket, E>; - type Error = RPCError; + type Error = (Protocol, RPCError); type Future = BoxFuture<'static, Result<Self::Output, Self::Error>>; fn upgrade_inbound(self, socket: TSocket, protocol: ProtocolId) -> Self::Future { @@ -717,10 +717,12 @@ where ) .await { - Err(e) => Err(RPCError::from(e)), + Err(e) => Err((versioned_protocol.protocol(), RPCError::from(e))), Ok((Some(Ok(request)), stream)) => Ok((request, stream)), - Ok((Some(Err(e)), _)) => Err(e), - Ok((None, _)) => Err(RPCError::IncompleteStream), + Ok((Some(Err(e)), _)) => Err((versioned_protocol.protocol(), e)), + Ok((None, _)) => { + Err((versioned_protocol.protocol(), RPCError::IncompleteStream)) + } } } } @@ -729,7 +731,7 @@ where } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, IntoStaticStr)] pub enum RequestType<E: EthSpec> { Status(StatusMessage), Goodbye(GoodbyeReason), diff --git a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs index 8b364f506c..2407038bc3 100644 --- a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs @@ -77,6 +77,14 @@ impl Quota { max_tokens: n, } } + + #[cfg(test)] + pub const fn n_every_millis(n: NonZeroU64, millis: u64) -> Self { + Quota { + replenish_all_every: Duration::from_millis(millis), + max_tokens: n, + } + } } /// Manages rate limiting of requests per peer, with differentiated rates per protocol. diff --git a/beacon_node/lighthouse_network/src/rpc/self_limiter.rs b/beacon_node/lighthouse_network/src/rpc/self_limiter.rs index 90e2db9135..2a7ef955a1 100644 --- a/beacon_node/lighthouse_network/src/rpc/self_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/self_limiter.rs @@ -4,6 +4,10 @@ use super::{ rate_limiter::{RPCRateLimiter as RateLimiter, RateLimitedErr}, }; use crate::rpc::rate_limiter::RateLimiterItem; +use futures::FutureExt; +use libp2p::{PeerId, swarm::NotifyHandler}; +use logging::crit; +use smallvec::SmallVec; use std::time::{SystemTime, UNIX_EPOCH}; use std::{ collections::{HashMap, VecDeque, hash_map::Entry}, @@ -11,11 +15,6 @@ use std::{ task::{Context, Poll}, time::Duration, }; - -use futures::FutureExt; -use libp2p::{PeerId, swarm::NotifyHandler}; -use logging::crit; -use smallvec::SmallVec; use tokio_util::time::DelayQueue; use tracing::debug; use types::{EthSpec, ForkContext}; @@ -234,9 +233,29 @@ impl<Id: ReqId, E: EthSpec> SelfRateLimiter<Id, E> { pub fn peer_disconnected(&mut self, peer_id: PeerId) -> Vec<(Id, Protocol)> { self.active_requests.remove(&peer_id); + let mut failed_requests = Vec::new(); + + self.ready_requests.retain(|(req_peer_id, rpc_send, _)| { + if let RPCSend::Request(request_id, req) = rpc_send { + if req_peer_id == &peer_id { + failed_requests.push((*request_id, req.protocol())); + // Remove the entry + false + } else { + // Keep the entry + true + } + } else { + debug_assert!( + false, + "Coding error: unexpected RPCSend variant {rpc_send:?}." + ); + false + } + }); + // It's not ideal to iterate this map, but the key is (PeerId, Protocol) and this map // should never really be large. So we iterate for simplicity - let mut failed_requests = Vec::new(); self.delayed_requests .retain(|(map_peer_id, protocol), queue| { if map_peer_id == &peer_id { @@ -252,6 +271,7 @@ impl<Id: ReqId, E: EthSpec> SelfRateLimiter<Id, E> { true } }); + failed_requests } @@ -549,4 +569,72 @@ mod tests { .contains_key(&(peer2, Protocol::Ping)) ); } + + /// Test that `peer_disconnected` returns the IDs of pending requests. + #[tokio::test] + async fn test_peer_disconnected_returns_failed_requests() { + const REPLENISH_DURATION: u64 = 50; + let fork_context = std::sync::Arc::new(ForkContext::new::<MainnetEthSpec>( + Slot::new(0), + Hash256::ZERO, + &MainnetEthSpec::default_spec(), + )); + let config = OutboundRateLimiterConfig(RateLimiterConfig { + ping_quota: Quota::n_every_millis(NonZeroU64::new(1).unwrap(), REPLENISH_DURATION), + ..Default::default() + }); + let mut limiter: SelfRateLimiter<AppRequestId, MainnetEthSpec> = + SelfRateLimiter::new(Some(config), fork_context).unwrap(); + let peer_id = PeerId::random(); + + for i in 1..=5u32 { + let result = limiter.allows( + peer_id, + AppRequestId::Sync(SyncRequestId::SingleBlock { + id: SingleLookupReqId { + req_id: i, + lookup_id: i, + }, + }), + RequestType::Ping(Ping { data: i as u64 }), + ); + + // Check that the limiter allows the first request while other requests are added to the queue. + if i == 1 { + assert!(result.is_ok()); + } else { + assert!(result.is_err()); + } + } + + // Wait until the tokens have been regenerated, then run `next_peer_request_ready`. + tokio::time::sleep(Duration::from_millis(REPLENISH_DURATION + 10)).await; + limiter.next_peer_request_ready(peer_id, Protocol::Ping); + + // Check that one of the pending requests has moved to ready_requests. + assert_eq!( + limiter + .delayed_requests + .get(&(peer_id, Protocol::Ping)) + .unwrap() + .len(), + 3 + ); + assert_eq!(limiter.ready_requests.len(), 1); + + let mut failed_requests = limiter.peer_disconnected(peer_id); + + // Check that the limiter returns the IDs of pending requests and that the IDs are ordered correctly. + assert_eq!(failed_requests.len(), 4); + for i in 2..=5u32 { + let (request_id, protocol) = failed_requests.remove(0); + assert!(matches!( + request_id, + AppRequestId::Sync(SyncRequestId::SingleBlock { + id: SingleLookupReqId { req_id, .. }, + }) if req_id == i + )); + assert_eq!(protocol, Protocol::Ping); + } + } } diff --git a/beacon_node/lighthouse_network/src/service/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index f1a4d87de7..d0323bab52 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -135,7 +135,7 @@ pub struct CustodyId { pub struct CustodyRequester(pub SingleLookupReqId); /// Application level requests sent to the network. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum AppRequestId { Sync(SyncRequestId), Router, diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index 3d709ed9b5..94e0ad0710 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -1861,8 +1861,6 @@ impl<E: EthSpec> Network<E> { self.inject_upnp_event(e); None } - #[allow(unreachable_patterns)] - BehaviourEvent::ConnectionLimits(le) => libp2p::core::util::unreachable(le), }, SwarmEvent::ConnectionEstablished { .. } => None, SwarmEvent::ConnectionClosed { .. } => None, diff --git a/beacon_node/lighthouse_network/tests/rpc_tests.rs b/beacon_node/lighthouse_network/tests/rpc_tests.rs index d73a3957cb..9ee9f5a190 100644 --- a/beacon_node/lighthouse_network/tests/rpc_tests.rs +++ b/beacon_node/lighthouse_network/tests/rpc_tests.rs @@ -5,8 +5,12 @@ use crate::common::spec_with_all_forks_enabled; use crate::common::{Protocol, build_tracing_subscriber}; use bls::Signature; use fixed_bytes::FixedBytesExtended; +use libp2p::PeerId; use lighthouse_network::rpc::{RequestType, methods::*}; -use lighthouse_network::service::api_types::AppRequestId; +use lighthouse_network::service::api_types::{ + AppRequestId, BlobsByRangeRequestId, BlocksByRangeRequestId, ComponentsByRangeRequestId, + DataColumnsByRangeRequestId, DataColumnsByRangeRequester, RangeRequestId, SyncRequestId, +}; use lighthouse_network::{NetworkEvent, ReportSource, Response}; use ssz::Encode; use ssz_types::{RuntimeVariableList, VariableList}; @@ -1785,3 +1789,157 @@ fn test_active_requests() { } }) } + +// Test that when a node receives an invalid BlocksByRange request exceeding the maximum count, +// it bans the sender. +#[test] +fn test_request_too_large_blocks_by_range() { + let spec = Arc::new(spec_with_all_forks_enabled()); + + test_request_too_large( + AppRequestId::Sync(SyncRequestId::BlocksByRange(BlocksByRangeRequestId { + id: 1, + parent_request_id: ComponentsByRangeRequestId { + id: 1, + requester: RangeRequestId::RangeSync { + chain_id: 1, + batch_id: Epoch::new(1), + }, + }, + })), + RequestType::BlocksByRange(OldBlocksByRangeRequest::new( + 0, + spec.max_request_blocks(ForkName::Base) as u64 + 1, // exceeds the max request defined in the spec. + 1, + )), + ); +} + +// Test that when a node receives an invalid BlobsByRange request exceeding the maximum count, +// it bans the sender. +#[test] +fn test_request_too_large_blobs_by_range() { + let spec = Arc::new(spec_with_all_forks_enabled()); + + let max_request_blobs_count = spec.max_request_blob_sidecars(ForkName::Base) as u64 + / spec.max_blobs_per_block_within_fork(ForkName::Base); + test_request_too_large( + AppRequestId::Sync(SyncRequestId::BlobsByRange(BlobsByRangeRequestId { + id: 1, + parent_request_id: ComponentsByRangeRequestId { + id: 1, + requester: RangeRequestId::RangeSync { + chain_id: 1, + batch_id: Epoch::new(1), + }, + }, + })), + RequestType::BlobsByRange(BlobsByRangeRequest { + start_slot: 0, + count: max_request_blobs_count + 1, // exceeds the max request defined in the spec. + }), + ); +} + +// Test that when a node receives an invalid DataColumnsByRange request exceeding the columns count, +// it bans the sender. +#[test] +fn test_request_too_large_data_columns_by_range() { + test_request_too_large( + AppRequestId::Sync(SyncRequestId::DataColumnsByRange( + DataColumnsByRangeRequestId { + id: 1, + parent_request_id: DataColumnsByRangeRequester::ComponentsByRange( + ComponentsByRangeRequestId { + id: 1, + requester: RangeRequestId::RangeSync { + chain_id: 1, + batch_id: Epoch::new(1), + }, + }, + ), + peer: PeerId::random(), + }, + )), + RequestType::DataColumnsByRange(DataColumnsByRangeRequest { + start_slot: 0, + count: 0, + // exceeds the max request defined in the spec. + columns: vec![0; E::number_of_columns() + 1], + }), + ); +} + +fn test_request_too_large(app_request_id: AppRequestId, request: RequestType<E>) { + // Set up the logging. + let log_level = "debug"; + let enable_logging = true; + let _subscriber = build_tracing_subscriber(log_level, enable_logging); + let rt = Arc::new(Runtime::new().unwrap()); + let spec = Arc::new(spec_with_all_forks_enabled()); + + rt.block_on(async { + let (mut sender, mut receiver) = common::build_node_pair( + Arc::downgrade(&rt), + ForkName::Base, + spec, + Protocol::Tcp, + false, + None, + ) + .await; + + // Build the sender future + let sender_future = async { + loop { + match sender.next_event().await { + NetworkEvent::PeerConnectedOutgoing(peer_id) => { + debug!(?request, %peer_id, "Sending RPC request"); + sender + .send_request(peer_id, app_request_id, request.clone()) + .unwrap(); + } + NetworkEvent::ResponseReceived { + app_request_id, + response, + .. + } => { + debug!(?app_request_id, ?response, "Received response"); + } + NetworkEvent::RPCFailed { error, .. } => { + // This variant should be unreachable, as the receiver doesn't respond with an error when a request exceeds the limit. + debug!(?error, "RPC failed"); + unreachable!(); + } + NetworkEvent::PeerDisconnected(peer_id) => { + // The receiver should disconnect as a result of the invalid request. + debug!(%peer_id, "Peer disconnected"); + // End the test. + return; + } + _ => {} + } + } + } + .instrument(info_span!("Sender")); + + // Build the receiver future + let receiver_future = async { + loop { + if let NetworkEvent::RequestReceived { .. } = receiver.next_event().await { + // This event should be unreachable, as the handler drops the invalid request. + unreachable!(); + } + } + } + .instrument(info_span!("Receiver")); + + tokio::select! { + _ = sender_future => {} + _ = receiver_future => {} + _ = sleep(Duration::from_secs(30)) => { + panic!("Future timed out"); + } + } + }); +} diff --git a/beacon_node/network/Cargo.toml b/beacon_node/network/Cargo.toml index 78dc0c48a7..68c77252ab 100644 --- a/beacon_node/network/Cargo.toml +++ b/beacon_node/network/Cargo.toml @@ -8,6 +8,7 @@ edition = { workspace = true } # NOTE: This can be run via cargo build --bin lighthouse --features network/disable-backfill disable-backfill = [] fork_from_env = ["beacon_chain/fork_from_env"] +fake_crypto = ["bls/fake_crypto", "kzg/fake_crypto"] portable = ["beacon_chain/portable"] test_logger = [] @@ -57,6 +58,7 @@ k256 = "0.13.4" kzg = { workspace = true } libp2p = { workspace = true } matches = "0.1.8" +paste = { workspace = true } rand_08 = { package = "rand", version = "0.8.5" } rand_chacha = "0.9.0" rand_chacha_03 = { package = "rand_chacha", version = "0.3.1" } diff --git a/beacon_node/network/src/metrics.rs b/beacon_node/network/src/metrics.rs index cea06a28c8..2119acf946 100644 --- a/beacon_node/network/src/metrics.rs +++ b/beacon_node/network/src/metrics.rs @@ -462,6 +462,13 @@ pub static SYNCING_CHAIN_BATCH_AWAITING_PROCESSING: LazyLock<Result<Histogram>> ]), ) }); +pub static SYNCING_CHAIN_BATCHES: LazyLock<Result<IntGaugeVec>> = LazyLock::new(|| { + try_create_int_gauge_vec( + "sync_batches", + "Number of batches in sync chains by sync type and state", + &["sync_type", "state"], + ) +}); pub static SYNC_SINGLE_BLOCK_LOOKUPS: LazyLock<Result<IntGauge>> = LazyLock::new(|| { try_create_int_gauge( "sync_single_block_lookups", @@ -507,6 +514,40 @@ pub static SYNC_UNKNOWN_NETWORK_REQUESTS: LazyLock<Result<IntCounterVec>> = Lazy &["type"], ) }); +pub static SYNC_RPC_REQUEST_SUCCESSES: LazyLock<Result<IntCounterVec>> = LazyLock::new(|| { + try_create_int_counter_vec( + "sync_rpc_requests_success_total", + "Total count of sync RPC requests successes", + &["protocol"], + ) +}); +pub static SYNC_RPC_REQUEST_ERRORS: LazyLock<Result<IntCounterVec>> = LazyLock::new(|| { + try_create_int_counter_vec( + "sync_rpc_requests_error_total", + "Total count of sync RPC requests errors", + &["protocol", "error"], + ) +}); +pub static SYNC_RPC_REQUEST_TIME: LazyLock<Result<HistogramVec>> = LazyLock::new(|| { + try_create_histogram_vec_with_buckets( + "sync_rpc_request_duration_sec", + "Time to complete a successful sync RPC requesst", + Ok(vec![ + 0.001, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 1.0, 2.0, + ]), + &["protocol"], + ) +}); + +/* + * Execution Payload Envelope Delay Metrics + */ +pub static ENVELOPE_DELAY_GOSSIP: LazyLock<Result<IntGauge>> = LazyLock::new(|| { + try_create_int_gauge( + "payload_envelope_delay_gossip", + "The first time we see this payload envelope from gossip as a delay from the start of the slot", + ) +}); /* * Block Delay Metrics diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index d38089c6da..f94eeca2d4 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -4,7 +4,6 @@ use crate::{ service::NetworkMessage, sync::SyncMessage, }; -use beacon_chain::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use beacon_chain::block_verification_types::AsBlock; use beacon_chain::data_column_verification::{GossipDataColumnError, GossipVerifiedDataColumn}; use beacon_chain::store::Error; @@ -19,6 +18,10 @@ use beacon_chain::{ sync_committee_verification::{self, Error as SyncCommitteeError}, validator_monitor::{get_block_delay_ms, get_slot_delay_ms}, }; +use beacon_chain::{ + blob_verification::{GossipBlobError, GossipVerifiedBlob}, + payload_envelope_verification::gossip_verified_envelope::GossipVerifiedEnvelope, +}; use beacon_processor::{Work, WorkEvent}; use lighthouse_network::{Client, MessageAcceptance, MessageId, PeerAction, PeerId, ReportSource}; use logging::crit; @@ -1357,7 +1360,8 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { | Err(e @ BlockError::ParentExecutionPayloadInvalid { .. }) | Err(e @ BlockError::KnownInvalidExecutionPayload(_)) | Err(e @ BlockError::GenesisBlock) - | Err(e @ BlockError::InvalidBlobCount { .. }) => { + | Err(e @ BlockError::InvalidBlobCount { .. }) + | Err(e @ BlockError::BidParentRootMismatch { .. }) => { warn!(error = %e, "Could not verify block for gossip. Rejecting the block"); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); self.gossip_penalize_peer( @@ -1491,19 +1495,23 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { // Block is gossip valid. Attempt to fetch blobs from the EL using versioned hashes derived // from kzg commitments, without having to wait for all blobs to be sent from the peers. - let publish_blobs = true; - let self_clone = self.clone(); - let block_clone = block.clone(); - let current_span = Span::current(); - self.executor.spawn( - async move { - self_clone - .fetch_engine_blobs_and_publish(block_clone, block_root, publish_blobs) - .await - } - .instrument(current_span), - "fetch_blobs_gossip", - ); + // TODO(gloas) we'll want to use this same optimization, but we need to refactor the + // `fetch_and_process_engine_blobs` flow to support gloas. + if !block.fork_name_unchecked().gloas_enabled() { + let publish_blobs = true; + let self_clone = self.clone(); + let block_clone = block.clone(); + let current_span = Span::current(); + self.executor.spawn( + async move { + self_clone + .fetch_engine_blobs_and_publish(block_clone, block_root, publish_blobs) + .await + } + .instrument(current_span), + "fetch_blobs_gossip", + ); + } let result = self .chain @@ -2416,6 +2424,25 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { "attn_comm_index_non_zero", ); } + AttnError::CommitteeIndexInvalid => { + /* + * The committee index is invalid after Gloas. + * + * The peer has published an invalid consensus message. + */ + debug!( + %peer_id, + block = ?beacon_block_root, + ?attestation_type, + "Committee index invalid" + ); + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); + self.gossip_penalize_peer( + peer_id, + PeerAction::LowToleranceError, + "attn_comm_index_invalid", + ); + } AttnError::UnknownHeadBlock { beacon_block_root } => { trace!( %peer_id, @@ -3225,25 +3252,166 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { } } - pub async fn process_gossip_execution_payload( + #[allow(clippy::too_many_arguments)] + #[instrument( + name = "lh_process_execution_payload_envelope", + parent = None, + level = "debug", + skip_all, + fields(beacon_block_root = tracing::field::Empty), + )] + pub async fn process_gossip_execution_payload_envelope( + self: Arc<Self>, + message_id: MessageId, + peer_id: PeerId, + envelope: Arc<SignedExecutionPayloadEnvelope<T::EthSpec>>, + seen_timestamp: Duration, + ) { + if let Some(gossip_verified_envelope) = self + .process_gossip_unverified_execution_payload_envelope( + message_id, + peer_id, + envelope.clone(), + seen_timestamp, + ) + .await + { + let beacon_block_root = gossip_verified_envelope.signed_envelope.beacon_block_root(); + + Span::current().record("beacon_block_root", beacon_block_root.to_string()); + + // TODO(gloas) in process_gossip_block here we check_and_insert on the duplicate cache + // before calling gossip_verified_block. We need this to ensure we dont try to execute the + // payload multiple times. + + self.process_gossip_verified_execution_payload_envelope( + peer_id, + gossip_verified_envelope, + ) + .await; + } + } + + async fn process_gossip_unverified_execution_payload_envelope( self: &Arc<Self>, message_id: MessageId, peer_id: PeerId, - execution_payload: SignedExecutionPayloadEnvelope<T::EthSpec>, + envelope: Arc<SignedExecutionPayloadEnvelope<T::EthSpec>>, + seen_duration: Duration, + ) -> Option<GossipVerifiedEnvelope<T>> { + let envelope_delay = + get_slot_delay_ms(seen_duration, envelope.slot(), &self.chain.slot_clock); + + let verification_result = self + .chain + .clone() + .verify_envelope_for_gossip(envelope.clone()) + .await; + + let verified_envelope = match verification_result { + Ok(verified_envelope) => { + metrics::set_gauge( + &metrics::ENVELOPE_DELAY_GOSSIP, + envelope_delay.as_millis() as i64, + ); + + // Write the time the envelope was observed into the delay cache. + self.chain.envelope_times_cache.write().set_time_observed( + verified_envelope.signed_envelope.beacon_block_root(), + envelope.slot(), + seen_duration, + Some(peer_id.to_string()), + ); + + info!( + slot = %verified_envelope.signed_envelope.slot(), + root = ?verified_envelope.signed_envelope.beacon_block_root(), + "New envelope received" + ); + + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); + + verified_envelope + } + // TODO(gloas) penalize peers accordingly + Err(_) => return None, + }; + + let envelope_slot = verified_envelope.signed_envelope.slot(); + let beacon_block_root = verified_envelope.signed_envelope.beacon_block_root(); + match self.chain.slot() { + // We only need to do a simple check about the envelope slot vs the current slot because + // `verify_envelope_for_gossip` already ensures that the envelope slot is within tolerance + // for envelope imports. + Ok(current_slot) if envelope_slot > current_slot => { + warn!( + ?envelope_slot, + ?beacon_block_root, + msg = "if this happens consistently, check system clock", + "envelope arrived early" + ); + + // TODO(gloas) update metrics to note how early the envelope arrived + + let inner_self = self.clone(); + let _process_fn = Box::pin(async move { + inner_self + .process_gossip_verified_execution_payload_envelope( + peer_id, + verified_envelope, + ) + .await; + }); + + // TODO(gloas) send to reprocess queue + None + } + Ok(_) => Some(verified_envelope), + Err(e) => { + error!( + error = ?e, + %envelope_slot, + ?beacon_block_root, + location = "envelope gossip", + "Failed to defer envelope import" + ); + None + } + } + } + + async fn process_gossip_verified_execution_payload_envelope( + self: Arc<Self>, + _peer_id: PeerId, + verified_envelope: GossipVerifiedEnvelope<T>, ) { - // TODO(EIP-7732): Implement proper execution payload envelope gossip processing. - // This should integrate with the envelope_verification.rs module once it's implemented. + let _processing_start_time = Instant::now(); + let beacon_block_root = verified_envelope.signed_envelope.beacon_block_root(); - trace!( - %peer_id, - builder_index = execution_payload.message.builder_index, - slot = %execution_payload.message.slot, - beacon_block_root = %execution_payload.message.beacon_block_root, - "Processing execution payload envelope" - ); + #[allow(clippy::result_large_err)] + let result = self + .chain + .process_execution_payload_envelope( + beacon_block_root, + verified_envelope, + NotifyExecutionLayer::Yes, + BlockImportSource::Gossip, + || Ok(()), + ) + .await; - // For now, ignore all envelopes since verification is not implemented - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); + // TODO(gloas) metrics + // register_process_result_metrics(&result, metrics::BlockSource::Gossip, "envelope"); + + match &result { + Ok(AvailabilityProcessingStatus::Imported(_)) + | Ok(AvailabilityProcessingStatus::MissingComponents(_, _)) => { + // Nothing to do + } + Err(_) => { + // TODO(gloas) implement peer penalties + } + } } pub fn process_gossip_execution_payload_bid( diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 29ca584a6a..676c059657 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -429,11 +429,17 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { message_id: MessageId, peer_id: PeerId, execution_payload: Box<SignedExecutionPayloadEnvelope<T::EthSpec>>, + seen_timestamp: Duration, ) -> Result<(), Error<T::EthSpec>> { let processor = self.clone(); let process_fn = async move { processor - .process_gossip_execution_payload(message_id, peer_id, *execution_payload) + .process_gossip_execution_payload_envelope( + message_id, + peer_id, + Arc::new(*execution_payload), + seen_timestamp, + ) .await }; @@ -526,7 +532,10 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { ); self.try_send(BeaconWorkEvent { drop_during_sync: false, - work: Work::RpcBlock { process_fn }, + work: Work::RpcBlock { + process_fn, + beacon_block_root: block_root, + }, }) } diff --git a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs index 4095ed847f..5df15470b7 100644 --- a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs @@ -978,7 +978,10 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { }; // remove all skip slots i.e. duplicated roots - Ok(block_roots.into_iter().unique().collect::<Vec<_>>()) + Ok(block_roots + .into_iter() + .unique_by(|(root, _)| *root) + .collect::<Vec<_>>()) } /// Handle a `BlobsByRange` request from the peer. diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index 8bc5b22be5..91537b4e6f 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -219,7 +219,7 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> { // to be sent from the peers if we already have them. let publish_blobs = false; self.fetch_engine_blobs_and_publish(signed_beacon_block, block_root, publish_blobs) - .await + .await; } _ => {} } diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index 4764e77377..914fcecf2a 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -120,6 +120,39 @@ impl TestRig { .await } + pub async fn new_with_skip_slots(chain_length: u64, skip_slots: &HashSet<u64>) -> Self { + let mut spec = test_spec::<E>(); + spec.shard_committee_period = 2; + let spec = Arc::new(spec); + let beacon_processor_config = BeaconProcessorConfig::default(); + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec.clone()) + .deterministic_keypairs(VALIDATOR_COUNT) + .fresh_ephemeral_store() + .mock_execution_layer() + .node_custody_type(NodeCustodyType::Fullnode) + .chain_config(<_>::default()) + .build(); + + harness.advance_slot(); + + for slot in 1..=chain_length { + if !skip_slots.contains(&slot) { + harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + } + + harness.advance_slot(); + } + + Self::from_harness(harness, beacon_processor_config, spec).await + } + pub async fn new_parametric( chain_length: u64, beacon_processor_config: BeaconProcessorConfig, @@ -150,6 +183,14 @@ impl TestRig { harness.advance_slot(); } + Self::from_harness(harness, beacon_processor_config, spec).await + } + + async fn from_harness( + harness: BeaconChainHarness<T>, + beacon_processor_config: BeaconProcessorConfig, + spec: Arc<ChainSpec>, + ) -> Self { let head = harness.chain.head_snapshot(); assert_eq!( @@ -940,20 +981,20 @@ async fn data_column_reconstruction_at_deadline() { .set_current_time(slot_start + Duration::from_millis(reconstruction_deadline_millis)); let min_columns_for_reconstruction = E::number_of_columns() / 2; + + // Enqueue all columns first - at deadline, reconstruction races with gossip drain for i in 0..min_columns_for_reconstruction { rig.enqueue_gossip_data_columns(i); - rig.assert_event_journal_completes(&[WorkType::GossipDataColumnSidecar]) - .await; } - // Since we're at the reconstruction deadline, reconstruction should be triggered immediately - rig.assert_event_journal_with_timeout( - &[WorkType::ColumnReconstruction.into()], - Duration::from_millis(50), - false, - false, - ) - .await; + // Expect all gossip events + reconstruction + let mut expected_events: Vec<WorkType> = (0..min_columns_for_reconstruction) + .map(|_| WorkType::GossipDataColumnSidecar) + .collect(); + expected_events.push(WorkType::ColumnReconstruction); + + rig.assert_event_journal_contains_ordered(&expected_events) + .await; } // Test the column reconstruction is delayed for columns that arrive for a previous slot. @@ -1986,3 +2027,78 @@ async fn test_data_columns_by_range_request_only_returns_requested_columns() { "Should have received at least some data columns" ); } + +/// Test that DataColumnsByRange does not return duplicate data columns for skip slots. +/// +/// When skip slots occur, `forwards_iter_block_roots` returns the same block root for +/// consecutive slots. The deduplication in `get_block_roots_from_store` must use +/// `unique_by` on the root (not the full `(root, slot)` tuple) to avoid serving +/// duplicate data columns for the same block. +#[tokio::test] +async fn test_data_columns_by_range_no_duplicates_with_skip_slots() { + if test_spec::<E>().fulu_fork_epoch.is_none() { + return; + }; + + // Build a chain of 128 slots (4 epochs) with skip slots at positions 5 and 6. + // After 4 epochs, finalized_epoch=2 (finalized_slot=64). Requesting slots 0-9 + // satisfies req_start_slot + req_count <= finalized_slot (10 <= 64), which routes + // through `get_block_roots_from_store` — the code path with the bug. + let skip_slots: HashSet<u64> = [5, 6].into_iter().collect(); + let mut rig = TestRig::new_with_skip_slots(128, &skip_slots).await; + + let all_custody_columns = rig.chain.custody_columns_for_epoch(Some(Epoch::new(0))); + let requested_column = vec![all_custody_columns[0]]; + + // Request a range that spans the skip slots (slots 0 through 9). + let start_slot = 0; + let slot_count = 10; + + rig.network_beacon_processor + .send_data_columns_by_range_request( + PeerId::random(), + InboundRequestId::new_unchecked(42, 24), + DataColumnsByRangeRequest { + start_slot, + count: slot_count, + columns: requested_column.clone(), + }, + ) + .unwrap(); + + // Collect block roots from all data column responses. + let mut block_roots: Vec<Hash256> = Vec::new(); + + while let Some(next) = rig.network_rx.recv().await { + if let NetworkMessage::SendResponse { + peer_id: _, + response: Response::DataColumnsByRange(data_column), + inbound_request_id: _, + } = next + { + if let Some(column) = data_column { + block_roots.push(column.block_root()); + } else { + break; + } + } else { + panic!("unexpected message {:?}", next); + } + } + + assert!( + !block_roots.is_empty(), + "Should have received at least some data columns" + ); + + // Before the fix, skip slots caused the same block root to appear multiple times + // (once per skip slot) because .unique() on (Hash256, Slot) tuples didn't deduplicate. + let unique_roots: HashSet<_> = block_roots.iter().collect(); + assert_eq!( + block_roots.len(), + unique_roots.len(), + "Response contained duplicate block roots: got {} columns but only {} unique roots", + block_roots.len(), + unique_roots.len(), + ); +} diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index 8373dec322..77d64c92e6 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -493,6 +493,7 @@ impl<T: BeaconChainTypes> Router<T> { message_id, peer_id, signed_execution_payload_envelope, + timestamp_now(), ), ) } diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index 9802ec56a1..801c9eca4d 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -8,9 +8,11 @@ //! If a batch fails, the backfill sync cannot progress. In this scenario, we mark the backfill //! sync as failed, log an error and attempt to retry once a new peer joins the node. +use crate::metrics; use crate::network_beacon_processor::ChainSegmentProcessId; use crate::sync::batch::{ - BatchConfig, BatchId, BatchInfo, BatchOperationOutcome, BatchProcessingResult, BatchState, + BatchConfig, BatchId, BatchInfo, BatchMetricsState, BatchOperationOutcome, + BatchProcessingResult, BatchState, }; use crate::sync::block_sidecar_coupling::CouplingError; use crate::sync::manager::BatchProcessResult; @@ -31,6 +33,7 @@ use std::collections::{ use std::hash::{Hash, Hasher}; use std::marker::PhantomData; use std::sync::Arc; +use strum::IntoEnumIterator; use tracing::{debug, error, info, warn}; use types::{ColumnIndex, Epoch, EthSpec}; @@ -1071,7 +1074,7 @@ impl<T: BeaconChainTypes> BackFillSync<T> { .iter() .filter(|&(_epoch, batch)| in_buffer(batch)) .count() - > BACKFILL_BATCH_BUFFER_SIZE as usize + >= BACKFILL_BATCH_BUFFER_SIZE as usize { return None; } @@ -1181,6 +1184,21 @@ impl<T: BeaconChainTypes> BackFillSync<T> { .epoch(T::EthSpec::slots_per_epoch()) } + pub fn register_metrics(&self) { + for state in BatchMetricsState::iter() { + let count = self + .batches + .values() + .filter(|b| b.state().metrics_state() == state) + .count(); + metrics::set_gauge_vec( + &metrics::SYNCING_CHAIN_BATCHES, + &["backfill", state.into()], + count as i64, + ); + } + } + /// Updates the global network state indicating the current state of a backfill sync. fn set_state(&self, state: BackFillState) { *self.network_globals.backfill_state.write() = state; diff --git a/beacon_node/network/src/sync/batch.rs b/beacon_node/network/src/sync/batch.rs index 8de386f5be..e87ffd119e 100644 --- a/beacon_node/network/src/sync/batch.rs +++ b/beacon_node/network/src/sync/batch.rs @@ -10,10 +10,22 @@ use std::marker::PhantomData; use std::ops::Sub; use std::time::Duration; use std::time::Instant; -use strum::Display; +use strum::{Display, EnumIter, IntoStaticStr}; use types::Slot; use types::{DataColumnSidecarList, Epoch, EthSpec}; +/// Batch states used as metrics labels. +#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, IntoStaticStr)] +#[strum(serialize_all = "snake_case")] +pub enum BatchMetricsState { + AwaitingDownload, + Downloading, + AwaitingProcessing, + Processing, + AwaitingValidation, + Failed, +} + pub type BatchId = Epoch; /// Type of expected batch. @@ -142,6 +154,18 @@ impl<D: Hash> BatchState<D> { pub fn poison(&mut self) -> BatchState<D> { std::mem::replace(self, BatchState::Poisoned) } + + /// Returns the metrics state for this batch. + pub fn metrics_state(&self) -> BatchMetricsState { + match self { + BatchState::AwaitingDownload => BatchMetricsState::AwaitingDownload, + BatchState::Downloading(_) => BatchMetricsState::Downloading, + BatchState::AwaitingProcessing(..) => BatchMetricsState::AwaitingProcessing, + BatchState::Processing(_) => BatchMetricsState::Processing, + BatchState::AwaitingValidation(_) => BatchMetricsState::AwaitingValidation, + BatchState::Poisoned | BatchState::Failed => BatchMetricsState::Failed, + } + } } impl<E: EthSpec, B: BatchConfig, D: Hash> BatchInfo<E, B, D> { @@ -213,6 +237,9 @@ impl<E: EthSpec, B: BatchConfig, D: Hash> BatchInfo<E, B, D> { /// After different operations over a batch, this could be in a state that allows it to /// continue, or in failed state. When the batch has failed, we check if it did mainly due to /// processing failures. In this case the batch is considered failed and faulty. + /// + /// When failure counts are equal, `blacklist` is `false` — we assume network issues over + /// peer fault when the evidence is ambiguous. pub fn outcome(&self) -> BatchOperationOutcome { match self.state { BatchState::Poisoned => unreachable!("Poisoned batch"), @@ -255,8 +282,10 @@ impl<E: EthSpec, B: BatchConfig, D: Hash> BatchInfo<E, B, D> { /// Mark the batch as failed and return whether we can attempt a re-download. /// /// This can happen if a peer disconnects or some error occurred that was not the peers fault. - /// The `peer` parameter, when set to None, does not increment the failed attempts of - /// this batch and register the peer, rather attempts a re-download. + /// The `peer` parameter, when set to `None`, still counts toward + /// `max_batch_download_attempts` (to prevent infinite retries on persistent failures) + /// but does not register a peer in `failed_peers()`. Use + /// [`Self::downloading_to_awaiting_download`] to retry without counting a failed attempt. #[must_use = "Batch may have failed"] pub fn download_failed( &mut self, @@ -272,7 +301,6 @@ impl<E: EthSpec, B: BatchConfig, D: Hash> BatchInfo<E, B, D> { { BatchState::Failed } else { - // drop the blocks BatchState::AwaitingDownload }; Ok(self.outcome()) @@ -524,3 +552,196 @@ impl<D: Hash> BatchState<D> { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::sync::range_sync::RangeSyncBatchConfig; + use types::MinimalEthSpec; + + type Cfg = RangeSyncBatchConfig<MinimalEthSpec>; + type TestBatch = BatchInfo<MinimalEthSpec, Cfg, Vec<u64>>; + + fn max_dl() -> u8 { + Cfg::max_batch_download_attempts() + } + + fn max_proc() -> u8 { + Cfg::max_batch_processing_attempts() + } + + fn new_batch() -> TestBatch { + BatchInfo::new(&Epoch::new(0), 1, ByRangeRequestType::Blocks) + } + + fn peer() -> PeerId { + PeerId::random() + } + + fn advance_to_processing(batch: &mut TestBatch, req_id: Id, peer_id: PeerId) { + batch.start_downloading(req_id).unwrap(); + batch.download_completed(vec![1, 2, 3], peer_id).unwrap(); + batch.start_processing().unwrap(); + } + + fn advance_to_awaiting_validation(batch: &mut TestBatch, req_id: Id, peer_id: PeerId) { + advance_to_processing(batch, req_id, peer_id); + batch + .processing_completed(BatchProcessingResult::Success) + .unwrap(); + } + + #[test] + fn happy_path_lifecycle() { + let mut batch = new_batch(); + let p = peer(); + + assert!(matches!(batch.state(), BatchState::AwaitingDownload)); + + batch.start_downloading(1).unwrap(); + assert!(matches!(batch.state(), BatchState::Downloading(1))); + + batch.download_completed(vec![10, 20], p).unwrap(); + assert!(matches!(batch.state(), BatchState::AwaitingProcessing(..))); + + let (data, _duration) = batch.start_processing().unwrap(); + assert_eq!(data, vec![10, 20]); + assert!(matches!(batch.state(), BatchState::Processing(..))); + + let outcome = batch + .processing_completed(BatchProcessingResult::Success) + .unwrap(); + assert!(matches!(outcome, BatchOperationOutcome::Continue)); + assert!(matches!(batch.state(), BatchState::AwaitingValidation(..))); + } + + #[test] + fn download_failures_count_toward_limit() { + let mut batch = new_batch(); + + for i in 1..max_dl() as Id { + batch.start_downloading(i).unwrap(); + let outcome = batch.download_failed(Some(peer())).unwrap(); + assert!(matches!(outcome, BatchOperationOutcome::Continue)); + } + + // Next failure hits the limit + batch.start_downloading(max_dl() as Id).unwrap(); + let outcome = batch.download_failed(Some(peer())).unwrap(); + assert!(matches!( + outcome, + BatchOperationOutcome::Failed { blacklist: false } + )); + } + + #[test] + fn download_failed_none_counts_but_does_not_blame_peer() { + let mut batch = new_batch(); + + // None still counts toward the limit (prevents infinite retry on persistent + // network failures), but doesn't register a peer in failed_peers(). + for i in 0..max_dl() as Id { + batch.start_downloading(i).unwrap(); + batch.download_failed(None).unwrap(); + } + assert!(matches!(batch.state(), BatchState::Failed)); + assert!(batch.failed_peers().is_empty()); + } + + #[test] + fn faulty_processing_failures_count_toward_limit() { + let mut batch = new_batch(); + + for i in 1..max_proc() as Id { + advance_to_processing(&mut batch, i, peer()); + let outcome = batch + .processing_completed(BatchProcessingResult::FaultyFailure) + .unwrap(); + assert!(matches!(outcome, BatchOperationOutcome::Continue)); + } + + // Next faulty failure: limit reached + advance_to_processing(&mut batch, max_proc() as Id, peer()); + let outcome = batch + .processing_completed(BatchProcessingResult::FaultyFailure) + .unwrap(); + assert!(matches!( + outcome, + BatchOperationOutcome::Failed { blacklist: true } + )); + } + + #[test] + fn non_faulty_processing_failures_never_exhaust_batch() { + let mut batch = new_batch(); + + // Well past both limits — non-faulty failures should never cause failure + let iterations = (max_dl() + max_proc()) as Id * 2; + for i in 0..iterations { + advance_to_processing(&mut batch, i, peer()); + let outcome = batch + .processing_completed(BatchProcessingResult::NonFaultyFailure) + .unwrap(); + assert!(matches!(outcome, BatchOperationOutcome::Continue)); + } + // Non-faulty failures also don't register peers as failed + assert!(batch.failed_peers().is_empty()); + } + + #[test] + fn validation_failures_count_toward_processing_limit() { + let mut batch = new_batch(); + + for i in 1..max_proc() as Id { + advance_to_awaiting_validation(&mut batch, i, peer()); + let outcome = batch.validation_failed().unwrap(); + assert!(matches!(outcome, BatchOperationOutcome::Continue)); + } + + advance_to_awaiting_validation(&mut batch, max_proc() as Id, peer()); + let outcome = batch.validation_failed().unwrap(); + assert!(matches!( + outcome, + BatchOperationOutcome::Failed { blacklist: true } + )); + } + + #[test] + fn mixed_failure_types_interact_correctly() { + let mut batch = new_batch(); + let mut req_id: Id = 0; + let mut next_id = || { + req_id += 1; + req_id + }; + + // One download failure + batch.start_downloading(next_id()).unwrap(); + batch.download_failed(Some(peer())).unwrap(); + + // One faulty processing failure (requires a successful download first) + advance_to_processing(&mut batch, next_id(), peer()); + batch + .processing_completed(BatchProcessingResult::FaultyFailure) + .unwrap(); + + // One non-faulty processing failure + advance_to_processing(&mut batch, next_id(), peer()); + batch + .processing_completed(BatchProcessingResult::NonFaultyFailure) + .unwrap(); + assert!(matches!(batch.state(), BatchState::AwaitingDownload)); + + // Fill remaining download failures to hit the limit + for _ in 1..max_dl() { + batch.start_downloading(next_id()).unwrap(); + batch.download_failed(Some(peer())).unwrap(); + } + + // Download failures > processing failures → blacklist: false + assert!(matches!( + batch.outcome(), + BatchOperationOutcome::Failed { blacklist: false } + )); + } +} diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index 9065f05753..394f2fc37d 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -121,15 +121,24 @@ pub struct BlockLookups<T: BeaconChainTypes> { // TODO: Why not index lookups by block_root? single_block_lookups: FnvHashMap<SingleLookupId, SingleBlockLookup<T>>, + + /// Used for testing assertions + metrics: BlockLookupsMetrics, } #[cfg(test)] use lighthouse_network::service::api_types::Id; #[cfg(test)] -/// Tuple of `SingleLookupId`, requested block root, awaiting parent block root (if any), -/// and list of peers that claim to have imported this set of block components. -pub(crate) type BlockLookupSummary = (Id, Hash256, Option<Hash256>, Vec<PeerId>); +#[derive(Debug)] +pub(crate) struct BlockLookupSummary { + /// Lookup ID + pub id: Id, + /// Requested block root + pub block_root: Hash256, + /// List of peers that claim to have imported this set of block components. + pub peers: Vec<PeerId>, +} impl<T: BeaconChainTypes> BlockLookups<T> { pub fn new() -> Self { @@ -138,9 +147,15 @@ impl<T: BeaconChainTypes> BlockLookups<T> { IGNORED_CHAINS_CACHE_EXPIRY_SECONDS, )), single_block_lookups: Default::default(), + metrics: <_>::default(), } } + #[cfg(test)] + pub(crate) fn metrics(&self) -> &BlockLookupsMetrics { + &self.metrics + } + #[cfg(test)] pub(crate) fn insert_ignored_chain(&mut self, block_root: Hash256) { self.ignored_chains.insert(block_root); @@ -155,7 +170,11 @@ impl<T: BeaconChainTypes> BlockLookups<T> { pub(crate) fn active_single_lookups(&self) -> Vec<BlockLookupSummary> { self.single_block_lookups .iter() - .map(|(id, l)| (*id, l.block_root(), l.awaiting_parent(), l.all_peers())) + .map(|(id, l)| BlockLookupSummary { + id: *id, + block_root: l.block_root(), + peers: l.all_peers(), + }) .collect() } @@ -306,7 +325,7 @@ impl<T: BeaconChainTypes> BlockLookups<T> { // attributability. A peer can send us garbage blocks over blocks_by_root, and // then correct blocks via blocks_by_range. - self.drop_lookup_and_children(*lookup_id); + self.drop_lookup_and_children(*lookup_id, "chain_too_long"); } else { // Should never happen error!( @@ -379,7 +398,7 @@ impl<T: BeaconChainTypes> BlockLookups<T> { // Lookups contain untrusted data, bound the total count of lookups hold in memory to reduce // the risk of OOM in case of bugs of malicious activity. - if self.single_block_lookups.len() > MAX_LOOKUPS { + if self.single_block_lookups.len() >= MAX_LOOKUPS { warn!(?block_root, "Dropping lookup reached max"); return false; } @@ -414,6 +433,7 @@ impl<T: BeaconChainTypes> BlockLookups<T> { "Created block lookup" ); metrics::inc_counter(&metrics::SYNC_LOOKUP_CREATED); + self.metrics.created_lookups += 1; let result = lookup.continue_requests(cx); if self.on_lookup_result(id, result, "new_current_lookup", cx) { @@ -513,8 +533,11 @@ impl<T: BeaconChainTypes> BlockLookups<T> { /* Error responses */ pub fn peer_disconnected(&mut self, peer_id: &PeerId) { - for (_, lookup) in self.single_block_lookups.iter_mut() { + for (id, lookup) in self.single_block_lookups.iter_mut() { lookup.remove_peer(peer_id); + if lookup.has_no_peers() { + debug!(%id, "Lookup has no peers"); + } } } @@ -566,7 +589,8 @@ impl<T: BeaconChainTypes> BlockLookups<T> { let action = match result { BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(_)) - | BlockProcessingResult::Err(BlockError::DuplicateFullyImported(..)) => { + | BlockProcessingResult::Err(BlockError::DuplicateFullyImported(..)) + | BlockProcessingResult::Err(BlockError::GenesisBlock) => { // Successfully imported request_state.on_processing_success()?; Action::Continue @@ -747,6 +771,15 @@ impl<T: BeaconChainTypes> BlockLookups<T> { let lookup_result = if imported { Ok(LookupResult::Completed) } else { + // A lookup may be in the following state: + // - Block awaiting processing from a different source + // - Blobs downloaded processed, and inserted into the da_checker + // + // At this point the block fails processing (e.g. execution engine offline) and it is + // removed from the da_checker. Note that ALL components are removed from the da_checker + // so when we re-download and process the block we get the error + // MissingComponentsAfterAllProcessed and get stuck. + lookup.reset_requests(); lookup.continue_requests(cx) }; let id = *id; @@ -779,14 +812,17 @@ impl<T: BeaconChainTypes> BlockLookups<T> { /// Drops `dropped_id` lookup and all its children recursively. Lookups awaiting a parent need /// the parent to make progress to resolve, therefore we must drop them if the parent is /// dropped. - pub fn drop_lookup_and_children(&mut self, dropped_id: SingleLookupId) { + pub fn drop_lookup_and_children(&mut self, dropped_id: SingleLookupId, reason: &'static str) { if let Some(dropped_lookup) = self.single_block_lookups.remove(&dropped_id) { debug!( id = ?dropped_id, block_root = ?dropped_lookup.block_root(), awaiting_parent = ?dropped_lookup.awaiting_parent(), + reason, "Dropping lookup" ); + metrics::inc_counter_vec(&metrics::SYNC_LOOKUP_DROPPED, &[reason]); + self.metrics.dropped_lookups += 1; let child_lookups = self .single_block_lookups @@ -796,7 +832,7 @@ impl<T: BeaconChainTypes> BlockLookups<T> { .collect::<Vec<_>>(); for id in child_lookups { - self.drop_lookup_and_children(id); + self.drop_lookup_and_children(id, reason); } } } @@ -814,8 +850,13 @@ impl<T: BeaconChainTypes> BlockLookups<T> { Ok(LookupResult::Pending) => true, // no action Ok(LookupResult::Completed) => { if let Some(lookup) = self.single_block_lookups.remove(&id) { - debug!(block = ?lookup.block_root(), id, "Dropping completed lookup"); + debug!( + block = ?lookup.block_root(), + id, + "Dropping completed lookup" + ); metrics::inc_counter(&metrics::SYNC_LOOKUP_COMPLETED); + self.metrics.completed_lookups += 1; // Block imported, continue the requests of pending child blocks self.continue_child_lookups(lookup.block_root(), cx); self.update_metrics(); @@ -829,8 +870,7 @@ impl<T: BeaconChainTypes> BlockLookups<T> { Err(LookupRequestError::UnknownLookup) => false, Err(error) => { debug!(id, source, ?error, "Dropping lookup on request error"); - metrics::inc_counter_vec(&metrics::SYNC_LOOKUP_DROPPED, &[error.into()]); - self.drop_lookup_and_children(id); + self.drop_lookup_and_children(id, error.into()); self.update_metrics(); false } @@ -897,7 +937,7 @@ impl<T: BeaconChainTypes> BlockLookups<T> { %block_root, "Dropping lookup with no peers" ); - self.drop_lookup_and_children(lookup_id); + self.drop_lookup_and_children(lookup_id, "no_peers"); } } @@ -946,7 +986,7 @@ impl<T: BeaconChainTypes> BlockLookups<T> { } metrics::inc_counter(&metrics::SYNC_LOOKUPS_STUCK); - self.drop_lookup_and_children(ancestor_stuck_lookup.id); + self.drop_lookup_and_children(ancestor_stuck_lookup.id, "lookup_stuck"); } } @@ -1022,3 +1062,10 @@ impl<T: BeaconChainTypes> BlockLookups<T> { } } } + +#[derive(Default, Clone, Debug)] +pub(crate) struct BlockLookupsMetrics { + pub created_lookups: usize, + pub dropped_lookups: usize, + pub completed_lookups: usize, +} diff --git a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs index 0250e52468..2d9f6ac3c8 100644 --- a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs +++ b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs @@ -109,6 +109,12 @@ impl<T: BeaconChainTypes> SingleBlockLookup<T> { } } + /// Reset the status of all internal requests + pub fn reset_requests(&mut self) { + self.block_request_state = BlockRequestState::new(self.block_root); + self.component_requests = ComponentRequests::WaitingForBlock; + } + /// Return the slot of this lookup's block if it's currently cached as `AwaitingProcessing` pub fn peek_downloaded_block_slot(&self) -> Option<Slot> { self.block_request_state diff --git a/beacon_node/network/src/sync/custody_backfill_sync/mod.rs b/beacon_node/network/src/sync/custody_backfill_sync/mod.rs index fa8b70c8b4..fe4c7dfe4c 100644 --- a/beacon_node/network/src/sync/custody_backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/custody_backfill_sync/mod.rs @@ -12,14 +12,16 @@ use lighthouse_network::{ }; use logging::crit; use std::hash::{DefaultHasher, Hash, Hasher}; +use strum::IntoEnumIterator; use tracing::{debug, error, info, info_span, warn}; use types::{DataColumnSidecarList, Epoch, EthSpec}; +use crate::metrics; use crate::sync::{ backfill_sync::{BACKFILL_EPOCHS_PER_BATCH, ProcessResult, SyncStart}, batch::{ - BatchConfig, BatchId, BatchInfo, BatchOperationOutcome, BatchProcessingResult, BatchState, - ByRangeRequestType, + BatchConfig, BatchId, BatchInfo, BatchMetricsState, BatchOperationOutcome, + BatchProcessingResult, BatchState, ByRangeRequestType, }, block_sidecar_coupling::CouplingError, manager::CustodyBatchProcessResult, @@ -422,7 +424,7 @@ impl<T: BeaconChainTypes> CustodyBackFillSync<T> { .iter() .filter(|&(_epoch, batch)| in_buffer(batch)) .count() - > BACKFILL_BATCH_BUFFER_SIZE as usize + >= BACKFILL_BATCH_BUFFER_SIZE as usize { return None; } @@ -1114,6 +1116,21 @@ impl<T: BeaconChainTypes> CustodyBackFillSync<T> { *self.network_globals.custody_sync_state.write() = state; } + pub fn register_metrics(&self) { + for state in BatchMetricsState::iter() { + let count = self + .batches + .values() + .filter(|b| b.state().metrics_state() == state) + .count(); + metrics::set_gauge_vec( + &metrics::SYNCING_CHAIN_BATCHES, + &["custody_backfill", state.into()], + count as i64, + ); + } + } + /// A fully synced peer has joined us. /// If we are in a failed state, update a local variable to indicate we are able to restart /// the failed sync on the next attempt. diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 096ed9c328..7e618d8980 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -70,6 +70,7 @@ use slot_clock::SlotClock; use std::ops::Sub; use std::sync::Arc; use std::time::Duration; +use strum::IntoStaticStr; use tokio::sync::mpsc; use tracing::{debug, error, info, trace}; use types::{ @@ -90,7 +91,7 @@ pub const SLOT_IMPORT_TOLERANCE: usize = 32; /// arbitrary number that covers a full slot, but allows recovery if sync get stuck for a few slots. const NOTIFIED_UNKNOWN_ROOT_EXPIRY_SECONDS: u64 = 30; -#[derive(Debug)] +#[derive(Debug, IntoStaticStr)] /// A message that can be sent to the sync manager thread. pub enum SyncMessage<E: EthSpec> { /// A useful peer has been discovered. @@ -323,17 +324,18 @@ impl<T: BeaconChainTypes> SyncManager<T> { } #[cfg(test)] - pub(crate) fn active_single_lookups(&self) -> Vec<super::block_lookups::BlockLookupSummary> { - self.block_lookups.active_single_lookups() + pub(crate) fn send_sync_message(&mut self, sync_message: SyncMessage<<T>::EthSpec>) { + self.network.send_sync_message(sync_message); } #[cfg(test)] - pub(crate) fn active_parent_lookups(&self) -> Vec<Vec<Hash256>> { - self.block_lookups - .active_parent_lookups() - .iter() - .map(|c| c.chain.clone()) - .collect() + pub(crate) fn block_lookups(&self) -> &BlockLookups<T> { + &self.block_lookups + } + + #[cfg(test)] + pub(crate) fn range_sync(&self) -> &RangeSync<T> { + &self.range_sync } #[cfg(test)] @@ -512,17 +514,18 @@ impl<T: BeaconChainTypes> SyncManager<T> { /// there is no way to guarantee that libp2p always emits a error along with /// the disconnect. fn peer_disconnect(&mut self, peer_id: &PeerId) { - // Inject a Disconnected error on all requests associated with the disconnected peer - // to retry all batches/lookups - for sync_request_id in self.network.peer_disconnected(peer_id) { - self.inject_error(*peer_id, sync_request_id, RPCError::Disconnected); - } - // Remove peer from all data structures self.range_sync.peer_disconnect(&mut self.network, peer_id); let _ = self.backfill_sync.peer_disconnected(peer_id); self.block_lookups.peer_disconnected(peer_id); + // Inject a Disconnected error on all requests associated with the disconnected peer + // to retry all batches/lookups. Only after removing the peer from the data structures to + // avoid sending retry requests to the disconnecting peer. + for sync_request_id in self.network.peer_disconnected(peer_id) { + self.inject_error(*peer_id, sync_request_id, RPCError::Disconnected); + } + // Regardless of the outcome, we update the sync status. self.update_sync_state(); } @@ -784,6 +787,9 @@ impl<T: BeaconChainTypes> SyncManager<T> { } _ = register_metrics_interval.tick() => { self.network.register_metrics(); + self.range_sync.register_metrics(); + self.backfill_sync.register_metrics(); + self.custody_backfill_sync.register_metrics(); } _ = epoch_interval.tick() => { self.update_sync_state(); diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 416d455736..bd3adcb8de 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -17,7 +17,7 @@ use crate::sync::block_lookups::SingleLookupId; use crate::sync::block_sidecar_coupling::CouplingError; use crate::sync::network_context::requests::BlobsByRootSingleBlockRequest; use crate::sync::range_data_column_batch_request::RangeDataColumnBatchRequest; -use beacon_chain::block_verification_types::RpcBlock; +use beacon_chain::block_verification_types::{AsBlock, RpcBlock}; use beacon_chain::{BeaconChain, BeaconChainTypes, BlockProcessStatus, EngineState}; use custody::CustodyRequestResult; use fnv::FnvHashMap; @@ -1096,13 +1096,14 @@ impl<T: BeaconChainTypes> SyncNetworkContext<T> { })?; // Include only the blob indexes not yet imported (received through gossip) - let custody_indexes_to_fetch = self + let mut custody_indexes_to_fetch = self .chain .sampling_columns_for_epoch(current_epoch) .iter() .copied() .filter(|index| !custody_indexes_imported.contains(index)) .collect::<Vec<_>>(); + custody_indexes_to_fetch.sort_unstable(); if custody_indexes_to_fetch.is_empty() { // No indexes required, do not issue any request @@ -1431,7 +1432,7 @@ impl<T: BeaconChainTypes> SyncNetworkContext<T> { } }) }); - self.on_rpc_response_result(id, "BlocksByRoot", resp, peer_id, |_| 1) + self.on_rpc_response_result(resp, peer_id) } pub(crate) fn on_single_blob_response( @@ -1460,7 +1461,7 @@ impl<T: BeaconChainTypes> SyncNetworkContext<T> { } }) }); - self.on_rpc_response_result(id, "BlobsByRoot", resp, peer_id, |_| 1) + self.on_rpc_response_result(resp, peer_id) } #[allow(clippy::type_complexity)] @@ -1473,7 +1474,7 @@ impl<T: BeaconChainTypes> SyncNetworkContext<T> { let resp = self .data_columns_by_root_requests .on_response(id, rpc_event); - self.on_rpc_response_result(id, "DataColumnsByRoot", resp, peer_id, |_| 1) + self.on_rpc_response_result(resp, peer_id) } #[allow(clippy::type_complexity)] @@ -1484,7 +1485,7 @@ impl<T: BeaconChainTypes> SyncNetworkContext<T> { rpc_event: RpcEvent<Arc<SignedBeaconBlock<T::EthSpec>>>, ) -> Option<RpcResponseResult<Vec<Arc<SignedBeaconBlock<T::EthSpec>>>>> { let resp = self.blocks_by_range_requests.on_response(id, rpc_event); - self.on_rpc_response_result(id, "BlocksByRange", resp, peer_id, |b| b.len()) + self.on_rpc_response_result(resp, peer_id) } #[allow(clippy::type_complexity)] @@ -1495,7 +1496,7 @@ impl<T: BeaconChainTypes> SyncNetworkContext<T> { rpc_event: RpcEvent<Arc<BlobSidecar<T::EthSpec>>>, ) -> Option<RpcResponseResult<Vec<Arc<BlobSidecar<T::EthSpec>>>>> { let resp = self.blobs_by_range_requests.on_response(id, rpc_event); - self.on_rpc_response_result(id, "BlobsByRangeRequest", resp, peer_id, |b| b.len()) + self.on_rpc_response_result(resp, peer_id) } #[allow(clippy::type_complexity)] @@ -1508,36 +1509,15 @@ impl<T: BeaconChainTypes> SyncNetworkContext<T> { let resp = self .data_columns_by_range_requests .on_response(id, rpc_event); - self.on_rpc_response_result(id, "DataColumnsByRange", resp, peer_id, |d| d.len()) + self.on_rpc_response_result(resp, peer_id) } - fn on_rpc_response_result<I: std::fmt::Display, R, F: FnOnce(&R) -> usize>( + /// Common handler for consistent scoring of RpcResponseError + fn on_rpc_response_result<R>( &mut self, - id: I, - method: &'static str, resp: Option<RpcResponseResult<R>>, peer_id: PeerId, - get_count: F, ) -> Option<RpcResponseResult<R>> { - match &resp { - None => {} - Some(Ok((v, _))) => { - debug!( - %id, - method, - count = get_count(v), - "Sync RPC request completed" - ); - } - Some(Err(e)) => { - debug!( - %id, - method, - error = ?e, - "Sync RPC request error" - ); - } - } if let Some(Err(RpcResponseError::VerifyError(e))) = &resp { self.report_peer(peer_id, PeerAction::LowToleranceError, e.into()); } @@ -1617,7 +1597,7 @@ impl<T: BeaconChainTypes> SyncNetworkContext<T> { ) .map_err(|_| SendErrorProcessor::SendError)?; - debug!(block = ?block_root, id, "Sending block for processing"); + debug!(block = ?block_root, block_slot = %block.slot(), id, "Sending block for processing"); // Lookup sync event safety: If `beacon_processor.send_rpc_beacon_block` returns Ok() sync // must receive a single `SyncMessage::BlockComponentProcessed` with this process type beacon_processor diff --git a/beacon_node/network/src/sync/network_context/custody.rs b/beacon_node/network/src/sync/network_context/custody.rs index 61ae95ee70..ae0eee9964 100644 --- a/beacon_node/network/src/sync/network_context/custody.rs +++ b/beacon_node/network/src/sync/network_context/custody.rs @@ -198,7 +198,14 @@ impl<T: BeaconChainTypes> ActiveCustodyRequest<T> { cx: &mut SyncNetworkContext<T>, ) -> CustodyRequestResult<T::EthSpec> { let _guard = self.span.clone().entered(); - if self.column_requests.values().all(|r| r.is_downloaded()) { + let total_requests = self.column_requests.len(); + let completed_requests = self + .column_requests + .values() + .filter(|r| r.is_downloaded()) + .count(); + + if completed_requests >= total_requests { // All requests have completed successfully. let mut peers = HashMap::<PeerId, Vec<usize>>::new(); let mut seen_timestamps = vec![]; @@ -222,6 +229,7 @@ impl<T: BeaconChainTypes> ActiveCustodyRequest<T> { let active_request_count_by_peer = cx.active_request_count_by_peer(); let mut columns_to_request_by_peer = HashMap::<PeerId, Vec<ColumnIndex>>::new(); + let mut columns_without_peers = vec![]; let lookup_peers = self.lookup_peers.read(); // Create deterministic hasher per request to ensure consistent peer ordering within // this request (avoiding fragmentation) while varying selection across different requests @@ -231,7 +239,7 @@ impl<T: BeaconChainTypes> ActiveCustodyRequest<T> { if let Some(wait_duration) = request.is_awaiting_download() { // Note: an empty response is considered a successful response, so we may end up // retrying many more times than `MAX_CUSTODY_COLUMN_DOWNLOAD_ATTEMPTS`. - if request.download_failures > MAX_CUSTODY_COLUMN_DOWNLOAD_ATTEMPTS { + if request.download_failures >= MAX_CUSTODY_COLUMN_DOWNLOAD_ATTEMPTS { return Err(Error::TooManyFailures); } @@ -256,6 +264,7 @@ impl<T: BeaconChainTypes> ActiveCustodyRequest<T> { return Err(Error::NoPeer(*column_index)); } else { // Do not issue requests if there is no custody peer on this column + columns_without_peers.push(*column_index); } } } @@ -270,10 +279,13 @@ impl<T: BeaconChainTypes> ActiveCustodyRequest<T> { lookup_peers = lookup_peers.len(), "Requesting {} columns from {} peers", columns_requested_count, peer_requests, ); - } else { + } else if !columns_without_peers.is_empty() { debug!( lookup_peers = lookup_peers.len(), - "No column peers found for look up", + total_requests, + completed_requests, + ?columns_without_peers, + "No column peers found for lookup", ); } @@ -288,7 +300,7 @@ impl<T: BeaconChainTypes> ActiveCustodyRequest<T> { }, // If peer is in the lookup peer set, it claims to have imported the block and // must have its columns in custody. In that case, set `true = enforce max_requests` - // and downscore if data_columns_by_root does not returned the expected custody + // and downscore if data_columns_by_root does not return the expected custody // columns. For the rest of peers, don't downscore if columns are missing. lookup_peers.contains(&peer_id), ) diff --git a/beacon_node/network/src/sync/network_context/requests.rs b/beacon_node/network/src/sync/network_context/requests.rs index 3183c06d76..8f9540693e 100644 --- a/beacon_node/network/src/sync/network_context/requests.rs +++ b/beacon_node/network/src/sync/network_context/requests.rs @@ -1,10 +1,11 @@ +use std::time::Instant; use std::{collections::hash_map::Entry, hash::Hash}; use beacon_chain::validator_monitor::timestamp_now; use fnv::FnvHashMap; use lighthouse_network::PeerId; use strum::IntoStaticStr; -use tracing::Span; +use tracing::{Span, debug}; use types::{Hash256, Slot}; pub use blobs_by_range::BlobsByRangeRequestItems; @@ -18,7 +19,7 @@ pub use data_columns_by_root::{ use crate::metrics; -use super::{RpcEvent, RpcResponseResult}; +use super::{RpcEvent, RpcResponseError, RpcResponseResult}; mod blobs_by_range; mod blobs_by_root; @@ -51,6 +52,7 @@ struct ActiveRequest<T: ActiveRequestItems> { peer_id: PeerId, // Error if the request terminates before receiving max expected responses expect_max_responses: bool, + start_instant: Instant, span: Span, } @@ -60,7 +62,7 @@ enum State<T> { Errored, } -impl<K: Eq + Hash, T: ActiveRequestItems> ActiveRequests<K, T> { +impl<K: Copy + Eq + Hash + std::fmt::Display, T: ActiveRequestItems> ActiveRequests<K, T> { pub fn new(name: &'static str) -> Self { Self { requests: <_>::default(), @@ -83,6 +85,7 @@ impl<K: Eq + Hash, T: ActiveRequestItems> ActiveRequests<K, T> { state: State::Active(items), peer_id, expect_max_responses, + start_instant: Instant::now(), span, }, ); @@ -112,7 +115,7 @@ impl<K: Eq + Hash, T: ActiveRequestItems> ActiveRequests<K, T> { return None; }; - match rpc_event { + let result = match rpc_event { // Handler of a success ReqResp chunk. Adds the item to the request accumulator. // `ActiveRequestItems` validates the item before appending to its internal state. RpcEvent::Response(item, seen_timestamp) => { @@ -126,7 +129,7 @@ impl<K: Eq + Hash, T: ActiveRequestItems> ActiveRequests<K, T> { Ok(true) => { let items = items.consume(); request.state = State::CompletedEarly; - Some(Ok((items, seen_timestamp))) + Some(Ok((items, seen_timestamp, request.start_instant.elapsed()))) } // Received item, but we are still expecting more Ok(false) => None, @@ -163,7 +166,11 @@ impl<K: Eq + Hash, T: ActiveRequestItems> ActiveRequests<K, T> { } .into())) } else { - Some(Ok((items.consume(), timestamp_now()))) + Some(Ok(( + items.consume(), + timestamp_now(), + request.start_instant.elapsed(), + ))) } } // Items already returned, ignore stream termination @@ -188,7 +195,41 @@ impl<K: Eq + Hash, T: ActiveRequestItems> ActiveRequests<K, T> { State::Errored => None, } } - } + }; + + result.map(|result| match result { + Ok((items, seen_timestamp, duration)) => { + metrics::inc_counter_vec(&metrics::SYNC_RPC_REQUEST_SUCCESSES, &[self.name]); + metrics::observe_timer_vec(&metrics::SYNC_RPC_REQUEST_TIME, &[self.name], duration); + debug!( + %id, + method = self.name, + count = items.len(), + "Sync RPC request completed" + ); + + Ok((items, seen_timestamp)) + } + Err(e) => { + let err_str: &'static str = match &e { + RpcResponseError::RpcError(e) => e.into(), + RpcResponseError::VerifyError(e) => e.into(), + RpcResponseError::CustodyRequestError(_) => "CustodyRequestError", + RpcResponseError::BlockComponentCouplingError(_) => { + "BlockComponentCouplingError" + } + }; + metrics::inc_counter_vec(&metrics::SYNC_RPC_REQUEST_ERRORS, &[self.name, err_str]); + debug!( + %id, + method = self.name, + error = ?e, + "Sync RPC request error" + ); + + Err(e) + } + }) } pub fn active_requests_of_peer(&self, peer_id: &PeerId) -> Vec<&K> { diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index d67d6468a9..e3ff638121 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -3,7 +3,8 @@ use crate::metrics; use crate::network_beacon_processor::ChainSegmentProcessId; use crate::sync::batch::BatchId; use crate::sync::batch::{ - BatchConfig, BatchInfo, BatchOperationOutcome, BatchProcessingResult, BatchState, + BatchConfig, BatchInfo, BatchMetricsState, BatchOperationOutcome, BatchProcessingResult, + BatchState, }; use crate::sync::block_sidecar_coupling::CouplingError; use crate::sync::network_context::{RangeRequestId, RpcRequestSendError, RpcResponseError}; @@ -234,6 +235,14 @@ impl<T: BeaconChainTypes> SyncingChain<T> { .sum() } + /// Returns the number of batches in the given metrics state. + pub fn count_batches_in_state(&self, state: BatchMetricsState) -> usize { + self.batches + .values() + .filter(|b| b.state().metrics_state() == state) + .count() + } + /// Removes a peer from the chain. /// If the peer has active batches, those are considered failed and re-requested. pub fn remove_peer(&mut self, peer_id: &PeerId) -> ProcessingResult { @@ -1277,7 +1286,7 @@ impl<T: BeaconChainTypes> SyncingChain<T> { .iter() .filter(|&(_epoch, batch)| in_buffer(batch)) .count() - > BATCH_BUFFER_SIZE as usize + >= BATCH_BUFFER_SIZE as usize { return None; } diff --git a/beacon_node/network/src/sync/range_sync/chain_collection.rs b/beacon_node/network/src/sync/range_sync/chain_collection.rs index 1d57ee6c3d..a087fdecdf 100644 --- a/beacon_node/network/src/sync/range_sync/chain_collection.rs +++ b/beacon_node/network/src/sync/range_sync/chain_collection.rs @@ -6,6 +6,7 @@ use super::chain::{ChainId, ProcessingResult, RemoveChain, SyncingChain}; use super::sync_type::RangeSyncType; use crate::metrics; +use crate::sync::batch::BatchMetricsState; use crate::sync::network_context::SyncNetworkContext; use beacon_chain::{BeaconChain, BeaconChainTypes}; use fnv::FnvHashMap; @@ -17,6 +18,7 @@ use smallvec::SmallVec; use std::collections::HashMap; use std::collections::hash_map::Entry; use std::sync::Arc; +use strum::IntoEnumIterator; use tracing::{debug, error}; use types::EthSpec; use types::{Epoch, Hash256, Slot}; @@ -41,6 +43,13 @@ pub enum RangeSyncState { pub type SyncChainStatus = Result<Option<(RangeSyncType, Slot /* from */, Slot /* to */)>, &'static str>; +#[cfg(test)] +#[derive(Default, Debug)] +pub struct ChainCollectionMetrics { + pub chains_added: usize, + pub chains_removed: usize, +} + /// A collection of finalized and head chains currently being processed. pub struct ChainCollection<T: BeaconChainTypes> { /// The beacon chain for processing. @@ -51,6 +60,9 @@ pub struct ChainCollection<T: BeaconChainTypes> { head_chains: FnvHashMap<ChainId, SyncingChain<T>>, /// The current sync state of the process. state: RangeSyncState, + #[cfg(test)] + /// Used for testing assertions + metrics: ChainCollectionMetrics, } impl<T: BeaconChainTypes> ChainCollection<T> { @@ -60,12 +72,23 @@ impl<T: BeaconChainTypes> ChainCollection<T> { finalized_chains: FnvHashMap::default(), head_chains: FnvHashMap::default(), state: RangeSyncState::Idle, + #[cfg(test)] + metrics: <_>::default(), } } + #[cfg(test)] + pub(crate) fn metrics(&self) -> &ChainCollectionMetrics { + &self.metrics + } + /// Updates the Syncing state of the collection after a chain is removed. fn on_chain_removed(&mut self, id: &ChainId, was_syncing: bool, sync_type: RangeSyncType) { metrics::inc_counter_vec(&metrics::SYNCING_CHAINS_REMOVED, &[sync_type.as_str()]); + #[cfg(test)] + { + self.metrics.chains_removed += 1; + } self.update_metrics(); match self.state { @@ -351,7 +374,8 @@ impl<T: BeaconChainTypes> ChainCollection<T> { .iter() .map(|(id, chain)| (chain.available_peers(), !chain.is_syncing(), *id)) .collect::<Vec<_>>(); - preferred_ids.sort_unstable(); + // Sort in descending order + preferred_ids.sort_unstable_by(|a, b| b.cmp(a)); let mut syncing_chains = SmallVec::<[Id; PARALLEL_HEAD_CHAINS]>::new(); for (_, _, id) in preferred_ids { @@ -510,11 +534,34 @@ impl<T: BeaconChainTypes> ChainCollection<T> { ); collection.insert(id, new_chain); metrics::inc_counter_vec(&metrics::SYNCING_CHAINS_ADDED, &[sync_type.as_str()]); + #[cfg(test)] + { + self.metrics.chains_added += 1; + } self.update_metrics(); } } } + pub fn register_metrics(&self) { + for (sync_type, chains) in [ + ("range_finalized", &self.finalized_chains), + ("range_head", &self.head_chains), + ] { + for state in BatchMetricsState::iter() { + let count: usize = chains + .values() + .map(|chain| chain.count_batches_in_state(state)) + .sum(); + metrics::set_gauge_vec( + &metrics::SYNCING_CHAIN_BATCHES, + &[sync_type, state.into()], + count as i64, + ); + } + } + } + fn update_metrics(&self) { metrics::set_gauge_vec( &metrics::SYNCING_CHAINS_COUNT, diff --git a/beacon_node/network/src/sync/range_sync/mod.rs b/beacon_node/network/src/sync/range_sync/mod.rs index dd9f17bfd1..3b65e1c84a 100644 --- a/beacon_node/network/src/sync/range_sync/mod.rs +++ b/beacon_node/network/src/sync/range_sync/mod.rs @@ -5,6 +5,8 @@ mod chain_collection; mod range; mod sync_type; +#[cfg(test)] +pub use chain::RangeSyncBatchConfig; pub use chain::{ChainId, EPOCHS_PER_BATCH}; #[cfg(test)] pub use chain_collection::SyncChainStatus; diff --git a/beacon_node/network/src/sync/range_sync/range.rs b/beacon_node/network/src/sync/range_sync/range.rs index c9656ad1d0..9fd72ac98a 100644 --- a/beacon_node/network/src/sync/range_sync/range.rs +++ b/beacon_node/network/src/sync/range_sync/range.rs @@ -98,6 +98,11 @@ where self.failed_chains.keys().copied().collect() } + #[cfg(test)] + pub(crate) fn metrics(&self) -> &super::chain_collection::ChainCollectionMetrics { + self.chains.metrics() + } + pub fn state(&self) -> SyncChainStatus { self.chains.state() } @@ -371,6 +376,10 @@ where .update(network, &local, &mut self.awaiting_head_peers); } + pub fn register_metrics(&self) { + self.chains.register_metrics(); + } + /// Kickstarts sync. pub fn resume(&mut self, network: &mut SyncNetworkContext<T>) { for (removed_chain, sync_type, remove_reason) in diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index e31570eaab..b71e59c8d1 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -1,79 +1,171 @@ +use super::*; use crate::NetworkMessage; -use crate::network_beacon_processor::NetworkBeaconProcessor; -use crate::sync::block_lookups::{ - BlockLookupSummary, PARENT_DEPTH_TOLERANCE, SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS, -}; +use crate::network_beacon_processor::{InvalidBlockStorage, NetworkBeaconProcessor}; +use crate::sync::block_lookups::{BlockLookupSummary, PARENT_DEPTH_TOLERANCE}; use crate::sync::{ SyncMessage, manager::{BlockProcessType, BlockProcessingResult, SyncManager}, }; -use std::sync::Arc; -use std::time::Duration; - -use super::*; - -use crate::sync::block_lookups::common::ResponseType; -use beacon_chain::observed_data_sidecars::Observe; +use beacon_chain::blob_verification::KzgVerifiedBlob; +use beacon_chain::custody_context::NodeCustodyType; use beacon_chain::{ - AvailabilityPendingExecutedBlock, AvailabilityProcessingStatus, BlockError, - PayloadVerificationOutcome, PayloadVerificationStatus, - blob_verification::GossipVerifiedBlob, - block_verification_types::{AsBlock, BlockImportData}, - custody_context::NodeCustodyType, + AvailabilityProcessingStatus, BlockError, NotifyExecutionLayer, + block_verification_types::{AsBlock, AvailableBlockData}, data_availability_checker::Availability, test_utils::{ - BeaconChainHarness, EphemeralHarnessType, NumBlobs, generate_rand_block_and_blobs, - generate_rand_block_and_data_columns, test_spec, + AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, NumBlobs, + generate_rand_block_and_blobs, test_spec, }, - validator_monitor::timestamp_now, }; -use beacon_processor::WorkEvent; +use beacon_processor::{BeaconProcessorChannels, DuplicateCache, Work, WorkEvent}; +use educe::Educe; +use itertools::Itertools; use lighthouse_network::discovery::CombinedKey; use lighthouse_network::{ NetworkConfig, NetworkGlobals, PeerId, - rpc::{RPCError, RequestType, RpcErrorResponse}, - service::api_types::{ - AppRequestId, DataColumnsByRootRequestId, DataColumnsByRootRequester, Id, - SingleLookupReqId, SyncRequestId, - }, + rpc::{RPCError, RequestType}, + service::api_types::{AppRequestId, SyncRequestId}, types::SyncState, }; use slot_clock::{SlotClock, TestingSlotClock}; +use std::sync::Arc; +use std::time::Duration; use tokio::sync::mpsc; use tracing::info; use types::{ - BeaconState, BeaconStateBase, BlobSidecar, BlockImportSource, DataColumnSidecar, EthSpec, - ForkContext, ForkName, Hash256, MinimalEthSpec as E, SignedBeaconBlock, Slot, - data::ColumnIndex, - test_utils::{SeedableRng, TestRandom, XorShiftRng}, + BlobSidecar, BlockImportSource, ColumnIndex, DataColumnSidecar, EthSpec, ForkContext, ForkName, + Hash256, MinimalEthSpec as E, SignedBeaconBlock, Slot, + test_utils::{SeedableRng, XorShiftRng}, }; const D: Duration = Duration::new(0, 0); -const PARENT_FAIL_TOLERANCE: u8 = SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS; -type DCByRootIds = Vec<DCByRootId>; -type DCByRootId = (SyncRequestId, Vec<ColumnIndex>); -impl TestRig { - pub fn test_setup() -> Self { - Self::test_setup_with_custody_type(NodeCustodyType::Fullnode) +/// Configuration for how the test rig should respond to sync requests. +/// +/// Controls simulated peer behavior during lookup tests, including RPC errors, +/// invalid responses, and custom block processing results. Use builder methods +/// to configure specific failure scenarios. +#[derive(Default, Educe)] +#[educe(Debug)] +pub struct SimulateConfig { + return_rpc_error: Option<RPCError>, + return_wrong_blocks_n_times: usize, + return_wrong_sidecar_for_block_n_times: usize, + return_no_blocks_n_times: usize, + return_no_data_n_times: usize, + return_too_few_data_n_times: usize, + return_no_columns_on_indices_n_times: usize, + return_no_columns_on_indices: Vec<ColumnIndex>, + skip_by_range_routes: bool, + // Use a callable fn because BlockProcessingResult does not implement Clone + #[educe(Debug(ignore))] + process_result_conditional: + Option<Box<dyn Fn(Hash256) -> Option<BlockProcessingResult> + Send + Sync>>, + // Import a block directly before processing it (for simulating race conditions) + import_block_before_process: HashSet<Hash256>, +} + +impl SimulateConfig { + fn new() -> Self { + Self::default() } - pub fn test_setup_with_custody_type(node_custody_type: NodeCustodyType) -> Self { + fn happy_path() -> Self { + Self::default() + } + + fn return_no_blocks_always(mut self) -> Self { + self.return_no_blocks_n_times = usize::MAX; + self + } + + fn return_no_blocks_once(mut self) -> Self { + self.return_no_blocks_n_times = 1; + self + } + + fn return_no_data_once(mut self) -> Self { + self.return_no_data_n_times = 1; + self + } + + fn return_wrong_blocks_once(mut self) -> Self { + self.return_wrong_blocks_n_times = 1; + self + } + + fn return_wrong_sidecar_for_block_once(mut self) -> Self { + self.return_wrong_sidecar_for_block_n_times = 1; + self + } + + fn return_too_few_data_once(mut self) -> Self { + self.return_too_few_data_n_times = 1; + self + } + + fn return_no_columns_on_indices(mut self, indices: &[ColumnIndex], times: usize) -> Self { + self.return_no_columns_on_indices_n_times = times; + self.return_no_columns_on_indices = indices.to_vec(); + self + } + + fn return_rpc_error(mut self, error: RPCError) -> Self { + self.return_rpc_error = Some(error); + self + } + + fn no_range_sync(mut self) -> Self { + self.skip_by_range_routes = true; + self + } + + fn with_process_result<F>(mut self, f: F) -> Self + where + F: Fn() -> BlockProcessingResult + Send + Sync + 'static, + { + self.process_result_conditional = Some(Box::new(move |_| Some(f()))); + self + } + + fn with_import_block_before_process(mut self, block_root: Hash256) -> Self { + self.import_block_before_process.insert(block_root); + self + } +} + +fn genesis_fork() -> ForkName { + test_spec::<E>().fork_name_at_slot::<E>(Slot::new(0)) +} + +pub(crate) struct TestRigConfig { + fulu_test_type: FuluTestType, + /// Override the node custody type derived from `fulu_test_type` + node_custody_type_override: Option<NodeCustodyType>, +} + +impl TestRig { + pub(crate) fn new(test_rig_config: TestRigConfig) -> Self { // Use `fork_from_env` logic to set correct fork epochs - let spec = test_spec::<E>(); + let spec = Arc::new(test_spec::<E>()); + let clock = TestingSlotClock::new( + Slot::new(0), + Duration::from_secs(0), + Duration::from_secs(12), + ); // Initialise a new beacon chain let harness = BeaconChainHarness::<EphemeralHarnessType<E>>::builder(E) - .spec(Arc::new(spec)) + .spec(spec.clone()) .deterministic_keypairs(1) .fresh_ephemeral_store() .mock_execution_layer() - .testing_slot_clock(TestingSlotClock::new( - Slot::new(0), - Duration::from_secs(0), - Duration::from_secs(12), - )) - .node_custody_type(node_custody_type) + .testing_slot_clock(clock.clone()) + .node_custody_type( + test_rig_config + .node_custody_type_override + .unwrap_or_else(|| test_rig_config.fulu_test_type.we_node_custody_type()), + ) .build(); let chain = harness.chain.clone(); @@ -93,12 +185,23 @@ impl TestRig { network_config, chain.spec.clone(), )); - let (beacon_processor, beacon_processor_rx) = NetworkBeaconProcessor::null_for_testing( - globals, + + let BeaconProcessorChannels { + beacon_processor_tx, + beacon_processor_rx, + } = <_>::default(); + + let beacon_processor = NetworkBeaconProcessor { + beacon_processor_send: beacon_processor_tx, + duplicate_cache: DuplicateCache::default(), + chain: chain.clone(), + // TODO: What is this sender used for? + network_tx: mpsc::unbounded_channel().0, sync_tx, - chain.clone(), - harness.runtime.task_executor.clone(), - ); + network_globals: globals.clone(), + invalid_block_storage: InvalidBlockStorage::Disabled, + executor: harness.runtime.task_executor.clone(), + }; let fork_name = chain.spec.fork_name_at_slot::<E>(chain.slot().unwrap()); @@ -119,6 +222,7 @@ impl TestRig { network_rx, network_rx_queue: vec![], sync_rx, + sync_rx_queue: vec![], rng_08, rng, network_globals: beacon_processor.network_globals.clone(), @@ -132,36 +236,985 @@ impl TestRig { ), harness, fork_name, + network_blocks_by_root: <_>::default(), + network_blocks_by_slot: <_>::default(), + penalties: <_>::default(), + seen_lookups: <_>::default(), + requests: <_>::default(), + complete_strategy: <_>::default(), + initial_block_lookups_metrics: <_>::default(), + fulu_test_type: test_rig_config.fulu_test_type, } } - fn test_setup_after_deneb_before_fulu() -> Option<Self> { - let r = Self::test_setup(); - if r.after_deneb() && !r.fork_name.fulu_enabled() { - Some(r) + pub fn default() -> Self { + // Before Fulu, FuluTestType is irrelevant + Self::new(TestRigConfig { + fulu_test_type: FuluTestType::WeFullnodeThemSupernode, + node_custody_type_override: None, + }) + } + + pub fn with_custody_type(node_custody_type: NodeCustodyType) -> Self { + Self::new(TestRigConfig { + fulu_test_type: FuluTestType::WeFullnodeThemSupernode, + node_custody_type_override: Some(node_custody_type), + }) + } + + /// Runs the sync simulation until all event queues are empty. + /// + /// Processes events from sync_rx (sink), beacon processor, and network queues in fixed + /// priority order each tick. Handles completed work before pulling new requests. + async fn simulate(&mut self, complete_strategy: SimulateConfig) { + self.complete_strategy = complete_strategy; + self.log(&format!( + "Running simulate with config {:?}", + self.complete_strategy + )); + + let mut i = 0; + + loop { + i += 1; + + // Record current status + for BlockLookupSummary { + id, + block_root, + peers, + .. + } in self.active_single_lookups() + { + let lookup = self.seen_lookups.entry(id).or_insert(SeenLookup { + id, + block_root, + seen_peers: <_>::default(), + }); + for peer in peers { + lookup.seen_peers.insert(peer); + } + } + + // Drain all channels into queues + while let Ok(ev) = self.network_rx.try_recv() { + self.network_rx_queue.push(ev); + } + while let Ok(ev) = self.beacon_processor_rx.try_recv() { + self.beacon_processor_rx_queue.push(ev); + } + while let Ok(ev) = self.sync_rx.try_recv() { + self.sync_rx_queue.push(ev); + } + + // Process one event per tick in fixed priority: sink → processor → network + if !self.sync_rx_queue.is_empty() { + let sync_message = self.sync_rx_queue.remove(0); + self.log(&format!( + "Tick {i}: sync_rx event: {}", + Into::<&'static str>::into(&sync_message) + )); + self.sync_manager.handle_message(sync_message); + } else if !self.beacon_processor_rx_queue.is_empty() { + let event = self.beacon_processor_rx_queue.remove(0); + self.log(&format!("Tick {i}: beacon_processor event: {event:?}")); + match event.work { + Work::RpcBlock { + process_fn, + beacon_block_root, + } => { + // Import block before processing if configured (for simulating race conditions) + if self + .complete_strategy + .import_block_before_process + .contains(&beacon_block_root) + { + self.log(&format!( + "Importing block {} before processing (race condition simulation)", + beacon_block_root + )); + self.import_block_by_root(beacon_block_root).await; + } + + if let Some(f) = self.complete_strategy.process_result_conditional.as_ref() + && let Some(result) = f(beacon_block_root) + { + let id = self.lookup_by_root(beacon_block_root).id; + self.log(&format!( + "Sending custom process result to lookup id {id}: {result:?}" + )); + self.push_sync_message(SyncMessage::BlockComponentProcessed { + process_type: BlockProcessType::SingleBlock { id }, + result, + }); + } else { + process_fn.await + } + } + Work::RpcBlobs { process_fn } + | Work::RpcCustodyColumn(process_fn) + | Work::ChainSegment(process_fn) => process_fn.await, + Work::Reprocess(_) => {} // ignore + other => panic!("Unsupported Work event {}", other.str_id()), + } + } else if !self.network_rx_queue.is_empty() { + let event = self.network_rx_queue.remove(0); + self.log(&format!("Tick {i}: network_rx event: {event:?}")); + match event { + NetworkMessage::SendRequest { + peer_id, + request, + app_request_id, + } => { + self.simulate_on_request(peer_id, request, app_request_id); + } + NetworkMessage::ReportPeer { peer_id, msg, .. } => { + self.penalties.push(ReportedPenalty { peer_id, msg }); + } + _ => {} + } + } else { + break; + } + } + + self.log("No more events in simulation"); + self.log(&format!( + "Lookup metrics: {:?}", + self.sync_manager.block_lookups().metrics() + )); + self.log(&format!( + "Range sync metrics: {:?}", + self.sync_manager.range_sync().metrics() + )); + self.log(&format!( + "Max known slot: {}, Head slot: {}", + self.max_known_slot(), + self.head_slot() + )); + self.log(&format!("Penalties: {:?}", self.penalties)); + self.log(&format!( + "Total requests {}: {:?}", + self.requests.len(), + self.requests_count() + )) + } + + fn simulate_on_request( + &mut self, + peer_id: PeerId, + request: RequestType<E>, + app_req_id: AppRequestId, + ) { + self.requests.push((request.clone(), app_req_id)); + + if let AppRequestId::Sync(req_id) = app_req_id + && let Some(error) = self.complete_strategy.return_rpc_error.take() + { + self.log(&format!( + "Completing request {req_id:?} to {peer_id} with RPCError {error:?}" + )); + self.send_sync_message(SyncMessage::RpcError { + sync_request_id: req_id, + peer_id, + error, + }); + return; + } + + match (request, app_req_id) { + (RequestType::BlocksByRoot(req), AppRequestId::Sync(req_id)) => { + let blocks = + req.block_roots() + .iter() + .filter_map(|block_root| { + if self.complete_strategy.return_no_blocks_n_times > 0 { + self.complete_strategy.return_no_blocks_n_times -= 1; + None + } else if self.complete_strategy.return_wrong_blocks_n_times > 0 { + self.complete_strategy.return_wrong_blocks_n_times -= 1; + Some(Arc::new(self.rand_block())) + } else { + Some(self.network_blocks_by_root + .get(block_root) + .unwrap_or_else(|| { + panic!("Test consumer requested unknown block: {block_root:?}") + }) + .block_cloned()) + } + }) + .collect::<Vec<_>>(); + + self.send_rpc_blocks_response(req_id, peer_id, &blocks); + } + + (RequestType::BlobsByRoot(req), AppRequestId::Sync(req_id)) => { + if self.complete_strategy.return_no_data_n_times > 0 { + self.complete_strategy.return_no_data_n_times -= 1; + return self.send_rpc_blobs_response(req_id, peer_id, &[]); + } + + let mut blobs = req + .blob_ids + .iter() + .map(|id| { + self.network_blocks_by_root + .get(&id.block_root) + .unwrap_or_else(|| { + panic!("Test consumer requested unknown block: {id:?}") + }) + .block_data() + .and_then(|d| d.blobs()) + .unwrap_or_else(|| panic!("Block {id:?} has no blobs")) + .iter() + .find(|blob| blob.index == id.index) + .unwrap_or_else(|| panic!("Blob id {id:?} not avail")) + .clone() + }) + .collect::<Vec<_>>(); + + if self.complete_strategy.return_too_few_data_n_times > 0 { + self.complete_strategy.return_too_few_data_n_times -= 1; + blobs.pop(); + } + + if self + .complete_strategy + .return_wrong_sidecar_for_block_n_times + > 0 + { + self.complete_strategy + .return_wrong_sidecar_for_block_n_times -= 1; + let first = blobs.first_mut().expect("empty blobs"); + let mut blob = Arc::make_mut(first).clone(); + blob.signed_block_header.message.body_root = Hash256::ZERO; + *first = Arc::new(blob); + } + + self.send_rpc_blobs_response(req_id, peer_id, &blobs); + } + + (RequestType::DataColumnsByRoot(req), AppRequestId::Sync(req_id)) => { + if self.complete_strategy.return_no_data_n_times > 0 { + self.complete_strategy.return_no_data_n_times -= 1; + return self.send_rpc_columns_response(req_id, peer_id, &[]); + } + + let will_omit_columns = req.data_column_ids.iter().any(|id| { + id.columns.iter().any(|c| { + self.complete_strategy + .return_no_columns_on_indices + .contains(c) + }) + }); + let columns_to_omit = if will_omit_columns + && self.complete_strategy.return_no_columns_on_indices_n_times > 0 + { + self.log(&format!("OMIT {:?}", req)); + self.complete_strategy.return_no_columns_on_indices_n_times -= 1; + self.complete_strategy.return_no_columns_on_indices.clone() + } else { + vec![] + }; + + let mut columns = req + .data_column_ids + .iter() + .flat_map(|id| { + let block_columns = self + .network_blocks_by_root + .get(&id.block_root) + .unwrap_or_else(|| { + panic!("Test consumer requested unknown block: {id:?}") + }) + .block_data() + .and_then(|d| d.data_columns()) + .unwrap_or_else(|| panic!("Block id {id:?} has no columns")); + id.columns + .iter() + .filter(|index| !columns_to_omit.contains(index)) + .map(move |index| { + block_columns + .iter() + .find(|c| *c.index() == *index) + .unwrap_or_else(|| { + panic!("Column {index:?} {:?} not found", id.block_root) + }) + .clone() + }) + }) + .collect::<Vec<_>>(); + + if self.complete_strategy.return_too_few_data_n_times > 0 { + self.complete_strategy.return_too_few_data_n_times -= 1; + columns.pop(); + } + + if self + .complete_strategy + .return_wrong_sidecar_for_block_n_times + > 0 + { + self.complete_strategy + .return_wrong_sidecar_for_block_n_times -= 1; + let first = columns.first_mut().expect("empty columns"); + let column = Arc::make_mut(first); + column + .signed_block_header_mut() + .expect("not fulu") + .message + .body_root = Hash256::ZERO; + } + self.send_rpc_columns_response(req_id, peer_id, &columns); + } + + (RequestType::BlocksByRange(req), AppRequestId::Sync(req_id)) => { + if self.complete_strategy.skip_by_range_routes { + return; + } + let blocks = (*req.start_slot()..req.start_slot() + req.count()) + .filter_map(|slot| { + self.network_blocks_by_slot + .get(&Slot::new(slot)) + .map(|block| block.block_cloned()) + }) + .collect::<Vec<_>>(); + + self.send_rpc_blocks_response(req_id, peer_id, &blocks); + } + + (RequestType::BlobsByRange(req), AppRequestId::Sync(req_id)) => { + if self.complete_strategy.skip_by_range_routes { + return; + } + + // Note: This function is permissive, blocks may have zero blobs and it won't + // error. Some caveats: + // - The genesis block never has blobs + // - Some blocks may not have blobs as the blob count is random + let blobs = (req.start_slot..req.start_slot + req.count) + .filter_map(|slot| self.network_blocks_by_slot.get(&Slot::new(slot))) + .filter_map(|block| block.block_data().and_then(|d| d.blobs())) + .flat_map(|blobs| blobs.into_iter()) + .collect::<Vec<_>>(); + self.send_rpc_blobs_response(req_id, peer_id, &blobs); + } + + (RequestType::DataColumnsByRange(req), AppRequestId::Sync(req_id)) => { + if self.complete_strategy.skip_by_range_routes { + return; + } + // Note: This function is permissive, blocks may have zero columns and it won't + // error. Some caveats: + // - The genesis block never has columns + // - Some blocks may not have columns as the blob count is random + let columns = (req.start_slot..req.start_slot + req.count) + .filter_map(|slot| self.network_blocks_by_slot.get(&Slot::new(slot))) + .filter_map(|block| block.block_data().and_then(|d| d.data_columns())) + .flat_map(|columns| { + columns + .into_iter() + .filter(|c| req.columns.contains(c.index())) + }) + .collect::<Vec<_>>(); + self.send_rpc_columns_response(req_id, peer_id, &columns); + } + + (RequestType::Status(_req), AppRequestId::Router) => { + // Ignore Status requests for now + } + + other => panic!("Request not supported: {app_req_id:?} {other:?}"), + } + } + + fn send_rpc_blocks_response( + &mut self, + sync_request_id: SyncRequestId, + peer_id: PeerId, + blocks: &[Arc<SignedBeaconBlock<E>>], + ) { + let slots = blocks.iter().map(|block| block.slot()).collect::<Vec<_>>(); + self.log(&format!( + "Completing request {sync_request_id:?} to {peer_id} with blocks {slots:?}" + )); + + for block in blocks { + self.push_sync_message(SyncMessage::RpcBlock { + sync_request_id, + peer_id, + beacon_block: Some(block.clone()), + seen_timestamp: D, + }); + } + self.push_sync_message(SyncMessage::RpcBlock { + sync_request_id, + peer_id, + beacon_block: None, + seen_timestamp: D, + }); + } + + fn send_rpc_blobs_response( + &mut self, + sync_request_id: SyncRequestId, + peer_id: PeerId, + blobs: &[Arc<BlobSidecar<E>>], + ) { + let slots = blobs + .iter() + .map(|block| block.slot()) + .unique() + .collect::<Vec<_>>(); + self.log(&format!( + "Completing request {sync_request_id:?} to {peer_id} with blobs {slots:?}" + )); + + for blob in blobs { + self.push_sync_message(SyncMessage::RpcBlob { + sync_request_id, + peer_id, + blob_sidecar: Some(blob.clone()), + seen_timestamp: D, + }); + } + self.push_sync_message(SyncMessage::RpcBlob { + sync_request_id, + peer_id, + blob_sidecar: None, + seen_timestamp: D, + }); + } + + fn send_rpc_columns_response( + &mut self, + sync_request_id: SyncRequestId, + peer_id: PeerId, + columns: &[Arc<DataColumnSidecar<E>>], + ) { + let slots = columns + .iter() + .map(|block| block.slot()) + .unique() + .collect::<Vec<_>>(); + let indices = columns + .iter() + .map(|column| *column.index()) + .unique() + .collect::<Vec<_>>(); + self.log(&format!( + "Completing request {sync_request_id:?} to {peer_id} with columns {slots:?} indices {indices:?}" + )); + + for column in columns { + self.push_sync_message(SyncMessage::RpcDataColumn { + sync_request_id, + peer_id, + data_column: Some(column.clone()), + seen_timestamp: D, + }); + } + self.push_sync_message(SyncMessage::RpcDataColumn { + sync_request_id, + peer_id, + data_column: None, + seen_timestamp: D, + }); + } + + // Preparation steps + + /// Returns the block root of the tip of the built chain + async fn build_chain(&mut self, block_count: usize) -> Hash256 { + let mut blocks = vec![]; + + // Initialise a new beacon chain + let external_harness = BeaconChainHarness::<EphemeralHarnessType<E>>::builder(E) + .spec(self.harness.spec.clone()) + .deterministic_keypairs(1) + .fresh_ephemeral_store() + .mock_execution_layer() + .testing_slot_clock(self.harness.chain.slot_clock.clone()) + // Make the external harness a supernode so all columns are available + .node_custody_type(NodeCustodyType::Supernode) + .build(); + // Ensure all blocks have data. Otherwise, the triggers for unknown blob parent and unknown + // data column parent fail. + external_harness + .execution_block_generator() + .set_min_blob_count(1); + + // Add genesis block for completeness + let genesis_block = external_harness.get_head_block(); + self.network_blocks_by_root + .insert(genesis_block.canonical_root(), genesis_block.clone()); + self.network_blocks_by_slot + .insert(genesis_block.slot(), genesis_block); + + for i in 0..block_count { + external_harness.advance_slot(); + let block_root = external_harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + let block = external_harness.get_full_block(&block_root); + let block_root = block.canonical_root(); + let block_slot = block.slot(); + self.network_blocks_by_root + .insert(block_root, block.clone()); + self.network_blocks_by_slot.insert(block_slot, block); + self.log(&format!( + "Produced block {} index {i} in external harness", + block_slot, + )); + blocks.push((block_slot, block_root)); + } + + // Re-log to have a nice list of block roots at the end + for block in &blocks { + self.log(&format!("Build chain {block:?}")); + } + + // Auto-update the clock on the main harness to accept the blocks + self.harness + .set_current_slot(external_harness.get_current_slot()); + + blocks.last().expect("empty blocks").1 + } + + fn corrupt_last_block_signature(&mut self) { + let rpc_block = self.get_last_block().clone(); + let mut block = (*rpc_block.block_cloned()).clone(); + let blobs = rpc_block.block_data().and_then(|d| d.blobs()); + let columns = rpc_block.block_data().and_then(|d| d.data_columns()); + *block.signature_mut() = self.valid_signature(); + self.re_insert_block(Arc::new(block), blobs, columns); + } + + fn valid_signature(&mut self) -> bls::Signature { + let keypair = bls::Keypair::random(); + let msg = Hash256::random(); + keypair.sk.sign(msg) + } + + fn corrupt_last_blob_proposer_signature(&mut self) { + let rpc_block = self.get_last_block().clone(); + let block = rpc_block.block_cloned(); + let mut blobs = rpc_block + .block_data() + .and_then(|d| d.blobs()) + .expect("no blobs") + .into_iter() + .collect::<Vec<_>>(); + let columns = rpc_block.block_data().and_then(|d| d.data_columns()); + let first = blobs.first_mut().expect("empty blobs"); + Arc::make_mut(first).signed_block_header.signature = self.valid_signature(); + let max_blobs = + self.harness + .spec + .max_blobs_per_block(block.slot().epoch(E::slots_per_epoch())) as usize; + let blobs = + types::BlobSidecarList::new(blobs, max_blobs).expect("invalid blob sidecar list"); + self.re_insert_block(block, Some(blobs), columns); + } + + fn corrupt_last_blob_kzg_proof(&mut self) { + let rpc_block = self.get_last_block().clone(); + let block = rpc_block.block_cloned(); + let mut blobs = rpc_block + .block_data() + .and_then(|d| d.blobs()) + .expect("no blobs") + .into_iter() + .collect::<Vec<_>>(); + let columns = rpc_block.block_data().and_then(|d| d.data_columns()); + let first = blobs.first_mut().expect("empty blobs"); + Arc::make_mut(first).kzg_proof = kzg::KzgProof::empty(); + let max_blobs = + self.harness + .spec + .max_blobs_per_block(block.slot().epoch(E::slots_per_epoch())) as usize; + let blobs = + types::BlobSidecarList::new(blobs, max_blobs).expect("invalid blob sidecar list"); + self.re_insert_block(block, Some(blobs), columns); + } + + fn corrupt_last_column_proposer_signature(&mut self) { + let rpc_block = self.get_last_block().clone(); + let block = rpc_block.block_cloned(); + let blobs = rpc_block.block_data().and_then(|d| d.blobs()); + let mut columns = rpc_block + .block_data() + .and_then(|d| d.data_columns()) + .expect("no columns"); + let first = columns.first_mut().expect("empty columns"); + Arc::make_mut(first) + .signed_block_header_mut() + .expect("not fulu") + .signature = self.valid_signature(); + self.re_insert_block(block, blobs, Some(columns)); + } + + fn corrupt_last_column_kzg_proof(&mut self) { + let rpc_block = self.get_last_block().clone(); + let block = rpc_block.block_cloned(); + let blobs = rpc_block.block_data().and_then(|d| d.blobs()); + let mut columns = rpc_block + .block_data() + .and_then(|d| d.data_columns()) + .expect("no columns"); + let first = columns.first_mut().expect("empty columns"); + let column = Arc::make_mut(first); + let proof = column.kzg_proofs_mut().first_mut().expect("no kzg proofs"); + *proof = kzg::KzgProof::empty(); + self.re_insert_block(block, blobs, Some(columns)); + } + + fn get_last_block(&self) -> &RpcBlock<E> { + let (_, last_block) = self + .network_blocks_by_root + .iter() + .max_by_key(|(_, block)| block.slot()) + .expect("no blocks"); + last_block + } + + fn re_insert_block( + &mut self, + block: Arc<SignedBeaconBlock<E>>, + blobs: Option<types::BlobSidecarList<E>>, + columns: Option<types::DataColumnSidecarList<E>>, + ) { + self.network_blocks_by_slot.clear(); + self.network_blocks_by_root.clear(); + let block_root = block.canonical_root(); + let block_slot = block.slot(); + let block_data = if let Some(columns) = columns { + Some(AvailableBlockData::new_with_data_columns(columns)) + } else if let Some(blobs) = blobs { + Some(AvailableBlockData::new_with_blobs(blobs)) + } else { + Some(AvailableBlockData::NoData) + }; + let rpc_block = RpcBlock::new( + block, + block_data, + self.harness.chain.data_availability_checker.v1().as_ref(), + self.harness.chain.spec.clone(), + ) + .unwrap(); + self.network_blocks_by_slot + .insert(block_slot, rpc_block.clone()); + self.network_blocks_by_root.insert(block_root, rpc_block); + } + + /// Trigger a lookup with the last created block + fn trigger_with_last_block(&mut self) { + let peer_id = match self.fulu_test_type.them_node_custody_type() { + NodeCustodyType::Fullnode => self.new_connected_peer(), + NodeCustodyType::Supernode | NodeCustodyType::SemiSupernode => { + self.new_connected_supernode_peer() + } + }; + let last_block = self.get_last_block().canonical_root(); + self.trigger_unknown_block_from_attestation(last_block, peer_id); + } + + fn block_at_slot(&self, slot: u64) -> Arc<SignedBeaconBlock<E>> { + self.network_blocks_by_slot + .get(&Slot::new(slot)) + .unwrap_or_else(|| panic!("No block for slot {slot}")) + .block_cloned() + } + + fn block_root_at_slot(&self, slot: u64) -> Hash256 { + self.block_at_slot(slot).canonical_root() + } + + fn trigger_with_block_at_slot(&mut self, slot: u64) { + let peer_id = self.new_connected_supernode_peer(); + let block = self.block_at_slot(slot); + self.trigger_unknown_block_from_attestation(block.canonical_root(), peer_id); + } + + async fn build_chain_and_trigger_last_block(&mut self, block_count: usize) { + self.build_chain(block_count).await; + self.trigger_with_last_block(); + } + + /// Import a block directly into the chain without going through lookup sync + async fn import_block_by_root(&mut self, block_root: Hash256) { + let rpc_block = self + .network_blocks_by_root + .get(&block_root) + .unwrap_or_else(|| panic!("No block for root {block_root}")) + .clone(); + + self.harness + .chain + .process_block( + block_root, + rpc_block, + NotifyExecutionLayer::Yes, + BlockImportSource::Gossip, + || Ok(()), + ) + .await + .unwrap(); + + self.harness.chain.recompute_head_at_current_slot().await; + } + + fn trigger_with_last_unknown_block_parent(&mut self) { + let peer_id = self.new_connected_supernode_peer(); + let last_block = self.get_last_block().block_cloned(); + self.trigger_unknown_parent_block(peer_id, last_block); + } + + fn trigger_with_last_unknown_blob_parent(&mut self) { + let peer_id = self.new_connected_supernode_peer(); + let blobs = self + .get_last_block() + .block_data() + .and_then(|d| d.blobs()) + .expect("no blobs"); + let blob = blobs.first().expect("empty blobs"); + self.trigger_unknown_parent_blob(peer_id, blob.clone()); + } + + fn trigger_with_last_unknown_data_column_parent(&mut self) { + let peer_id = self.new_connected_supernode_peer(); + let columns = self + .get_last_block() + .block_data() + .and_then(|d| d.data_columns()) + .expect("No data columns"); + let column = columns.first().expect("empty columns"); + self.trigger_unknown_parent_column(peer_id, column.clone()); + } + + // Post-test assertions + + fn head_slot(&self) -> Slot { + self.harness.chain.head().head_slot() + } + + fn assert_head_slot(&self, slot: u64) { + assert_eq!(self.head_slot(), Slot::new(slot), "Unexpected head slot"); + } + + fn max_known_slot(&self) -> Slot { + self.network_blocks_by_slot + .keys() + .max() + .copied() + .expect("no blocks") + } + + fn assert_penalties(&self, expected_penalties: &[&'static str]) { + let penalties = self + .penalties + .iter() + .map(|penalty| penalty.msg) + .collect::<Vec<_>>(); + if penalties != expected_penalties { + panic!( + "Expected penalties: {:#?} but got {:#?}", + expected_penalties, + self.penalties + .iter() + .map(|p| format!("{} for peer {}", p.msg, p.peer_id)) + .collect::<Vec<_>>() + ); + } + } + + fn assert_penalties_of_type(&self, expected_penalty: &'static str) { + if self.penalties.is_empty() { + panic!("No penalties but expected some of type {expected_penalty}"); + } + let non_matching_penalties = self + .penalties + .iter() + .filter(|penalty| penalty.msg != expected_penalty) + .collect::<Vec<_>>(); + if !non_matching_penalties.is_empty() { + panic!( + "Found non-matching penalties to {}: {:?}", + expected_penalty, non_matching_penalties + ); + } + } + + fn assert_no_penalties(&mut self) { + if !self.penalties.is_empty() { + panic!("Some downscore events: {:?}", self.penalties); + } + } + fn assert_failed_lookup_sync(&mut self) { + assert!(self.created_lookups() > 0, "no created lookups"); + assert_eq!(self.completed_lookups(), 0, "some completed lookups"); + assert_eq!( + self.dropped_lookups(), + self.created_lookups(), + "not all dropped. Current lookups {:?}", + self.active_single_lookups(), + ); + self.assert_empty_network(); + self.assert_no_active_lookups(); + } + + fn assert_successful_lookup_sync(&mut self) { + assert!(self.created_lookups() > 0, "no created lookups"); + assert_eq!(self.dropped_lookups(), 0, "some dropped lookups"); + assert_eq!( + self.completed_lookups(), + self.created_lookups(), + "not all lookups completed. Current lookups {:?}", + self.active_single_lookups(), + ); + self.assert_empty_network(); + self.assert_no_active_lookups(); + } + + /// There is a lookup created with the block that triggers the unknown message that can't be + /// completed because it has zero peers + fn assert_successful_lookup_sync_parent_trigger(&mut self) { + assert!(self.created_lookups() > 0, "no created lookups"); + assert_eq!( + self.completed_lookups() + 1, + self.created_lookups(), + "all completed" + ); + assert_eq!(self.dropped_lookups(), 0, "some dropped lookups"); + self.assert_empty_network(); + } + + fn assert_pending_lookup_sync(&self) { + assert!(self.created_lookups() > 0, "no created lookups"); + assert_eq!(self.dropped_lookups(), 0, "some dropped lookups"); + assert_eq!(self.completed_lookups(), 0, "some completed lookups"); + } + + /// Assert there is at least one range sync chain created and that all sync chains completed + fn assert_successful_range_sync(&self) { + assert!( + self.range_sync_chains_added() > 0, + "No created range sync chains" + ); + assert_eq!( + self.range_sync_chains_added(), + self.range_sync_chains_removed(), + "Not all chains completed" + ); + } + + fn lookup_at_slot(&self, slot: u64) -> &SeenLookup { + let block_root = self.block_root_at_slot(slot); + self.seen_lookups + .values() + .find(|lookup| lookup.block_root == block_root) + .unwrap_or_else(|| panic!("No lookup for block_root {block_root} of slot {slot}")) + } + + fn assert_peers_at_lookup_of_slot(&self, slot: u64, expected_peers: usize) { + let lookup = self.lookup_at_slot(slot); + if lookup.seen_peers.len() != expected_peers { + panic!( + "Expected lookup of slot {slot} to have {expected_peers} peers but had {:?}", + lookup.seen_peers + ) + } + } + + /// Total count of unique lookups created + fn created_lookups(&self) -> usize { + // Subtract initial value to allow resetting metrics mid test + self.sync_manager.block_lookups().metrics().created_lookups + - self.initial_block_lookups_metrics.created_lookups + } + + /// Total count of lookups completed or dropped + fn dropped_lookups(&self) -> usize { + // Subtract initial value to allow resetting metrics mid test + self.sync_manager.block_lookups().metrics().dropped_lookups + - self.initial_block_lookups_metrics.dropped_lookups + } + + fn completed_lookups(&self) -> usize { + // Subtract initial value to allow resetting metrics mid test + self.sync_manager + .block_lookups() + .metrics() + .completed_lookups + - self.initial_block_lookups_metrics.completed_lookups + } + + fn capture_metrics_baseline(&mut self) { + self.initial_block_lookups_metrics = self.sync_manager.block_lookups().metrics().clone() + } + + /// Returns the last lookup seen with matching block_root + fn lookup_by_root(&self, block_root: Hash256) -> &SeenLookup { + self.seen_lookups + .values() + .filter(|lookup| lookup.block_root == block_root) + .max_by_key(|lookup| lookup.id) + .unwrap_or_else(|| panic!("No loookup for block_root {block_root}")) + } + + fn range_sync_chains_added(&self) -> usize { + self.sync_manager.range_sync().metrics().chains_added + } + + fn range_sync_chains_removed(&self) -> usize { + self.sync_manager.range_sync().metrics().chains_removed + } + + fn custody_columns(&self) -> &[ColumnIndex] { + self.harness + .chain + .data_availability_checker + .custody_context() + .custody_columns_for_epoch(None, &self.harness.spec) + } + + // Test setup + + fn new_after_deneb() -> Option<Self> { + genesis_fork().deneb_enabled().then(Self::default) + } + + fn new_after_deneb_before_fulu() -> Option<Self> { + let fork = genesis_fork(); + if fork.deneb_enabled() && !fork.fulu_enabled() { + Some(Self::default()) } else { None } } - pub fn test_setup_after_fulu() -> Option<Self> { - let r = Self::test_setup(); - if r.fork_name.fulu_enabled() { - Some(r) - } else { - None - } + pub fn new_fulu_peer_test(fulu_test_type: FuluTestType) -> Option<Self> { + genesis_fork().fulu_enabled().then(|| { + Self::new(TestRigConfig { + fulu_test_type, + node_custody_type_override: None, + }) + }) } pub fn log(&self, msg: &str) { info!(msg, "TEST_RIG"); } - pub fn after_deneb(&self) -> bool { + pub fn is_after_deneb(&self) -> bool { self.fork_name.deneb_enabled() } - pub fn after_fulu(&self) -> bool { + pub fn is_after_fulu(&self) -> bool { self.fork_name.fulu_enabled() } @@ -170,8 +1223,16 @@ impl TestRig { self.send_sync_message(SyncMessage::UnknownParentBlock(peer_id, block, block_root)) } - fn trigger_unknown_parent_blob(&mut self, peer_id: PeerId, blob: BlobSidecar<E>) { - self.send_sync_message(SyncMessage::UnknownParentBlob(peer_id, blob.into())); + fn trigger_unknown_parent_blob(&mut self, peer_id: PeerId, blob: Arc<BlobSidecar<E>>) { + self.send_sync_message(SyncMessage::UnknownParentBlob(peer_id, blob)); + } + + fn trigger_unknown_parent_column( + &mut self, + peer_id: PeerId, + column: Arc<DataColumnSidecar<E>>, + ) { + self.send_sync_message(SyncMessage::UnknownParentDataColumn(peer_id, column)); } fn trigger_unknown_block_from_attestation(&mut self, block_root: Hash256, peer_id: PeerId) { @@ -180,13 +1241,6 @@ impl TestRig { )); } - /// Drain all sync messages in the sync_rx attached to the beacon processor - fn drain_sync_rx(&mut self) { - while let Ok(sync_message) = self.sync_rx.try_recv() { - self.send_sync_message(sync_message); - } - } - fn rand_block(&mut self) -> SignedBeaconBlock<E> { self.rand_block_and_blobs(NumBlobs::None).0 } @@ -200,105 +1254,36 @@ impl TestRig { generate_rand_block_and_blobs::<E>(fork_name, num_blobs, rng) } - fn rand_block_and_data_columns( - &mut self, - ) -> (SignedBeaconBlock<E>, Vec<Arc<DataColumnSidecar<E>>>) { - let num_blobs = NumBlobs::Number(1); - generate_rand_block_and_data_columns::<E>( - self.fork_name, - num_blobs, - &mut self.rng, - &self.harness.spec, - ) - } - - pub fn rand_block_and_parent( - &mut self, - ) -> (SignedBeaconBlock<E>, SignedBeaconBlock<E>, Hash256, Hash256) { - let parent = self.rand_block(); - let parent_root = parent.canonical_root(); - let mut block = self.rand_block(); - *block.message_mut().parent_root_mut() = parent_root; - let block_root = block.canonical_root(); - (parent, block, parent_root, block_root) - } - pub fn send_sync_message(&mut self, sync_message: SyncMessage<E>) { self.sync_manager.handle_message(sync_message); } + pub fn push_sync_message(&mut self, sync_message: SyncMessage<E>) { + self.sync_manager.send_sync_message(sync_message); + } + fn active_single_lookups(&self) -> Vec<BlockLookupSummary> { - self.sync_manager.active_single_lookups() + self.sync_manager.block_lookups().active_single_lookups() } fn active_single_lookups_count(&self) -> usize { - self.sync_manager.active_single_lookups().len() - } - - fn active_parent_lookups(&self) -> Vec<Vec<Hash256>> { - self.sync_manager.active_parent_lookups() - } - - fn active_parent_lookups_count(&self) -> usize { - self.sync_manager.active_parent_lookups().len() - } - - fn active_range_sync_chain(&self) -> (RangeSyncType, Slot, Slot) { - self.sync_manager.get_range_sync_chains().unwrap().unwrap() + self.active_single_lookups().len() } fn assert_single_lookups_count(&self, count: usize) { assert_eq!( self.active_single_lookups_count(), count, - "Unexpected count of single lookups. Current lookups: {:?}", + "Unexpected count of single lookups. Current lookups: {:#?}", self.active_single_lookups() ); } - fn assert_parent_lookups_count(&self, count: usize) { - assert_eq!( - self.active_parent_lookups_count(), - count, - "Unexpected count of parent lookups. Parent lookups: {:?}. Current lookups: {:?}", - self.active_parent_lookups(), - self.active_single_lookups() - ); - } - - fn assert_lookup_is_active(&self, block_root: Hash256) { - let lookups = self.sync_manager.active_single_lookups(); - if !lookups.iter().any(|l| l.1 == block_root) { - panic!("Expected lookup {block_root} to be the only active: {lookups:?}"); - } - } - - fn assert_lookup_peers(&self, block_root: Hash256, mut expected_peers: Vec<PeerId>) { - let mut lookup = self - .sync_manager - .active_single_lookups() - .into_iter() - .find(|l| l.1 == block_root) - .unwrap_or_else(|| panic!("no lookup for {block_root}")); - lookup.3.sort(); - expected_peers.sort(); - assert_eq!( - lookup.3, expected_peers, - "unexpected peers on lookup {block_root}" - ); - } - fn insert_ignored_chain(&mut self, block_root: Hash256) { + self.log(&format!("Inserting block in ignored chains {block_root:?}")); self.sync_manager.insert_ignored_chain(block_root); } - fn assert_not_ignored_chain(&mut self, chain_hash: Hash256) { - let chains = self.sync_manager.get_ignored_chains(); - if chains.contains(&chain_hash) { - panic!("ignored chains contain {chain_hash:?}: {chains:?}"); - } - } - fn assert_ignored_chain(&mut self, chain_hash: Hash256) { let chains = self.sync_manager.get_ignored_chains(); if !chains.contains(&chain_hash) { @@ -306,16 +1291,8 @@ impl TestRig { } } - fn find_single_lookup_for(&self, block_root: Hash256) -> Id { - self.active_single_lookups() - .iter() - .find(|l| l.1 == block_root) - .unwrap_or_else(|| panic!("no single block lookup found for {block_root}")) - .0 - } - #[track_caller] - fn expect_no_active_single_lookups(&self) { + fn assert_no_active_single_lookups(&self) { assert!( self.active_single_lookups().is_empty(), "expect no single block lookups: {:?}", @@ -324,13 +1301,8 @@ impl TestRig { } #[track_caller] - fn expect_no_active_lookups(&self) { - self.expect_no_active_single_lookups(); - } - - fn expect_no_active_lookups_empty_network(&mut self) { - self.expect_no_active_lookups(); - self.expect_empty_network(); + fn assert_no_active_lookups(&self) { + self.assert_no_active_single_lookups(); } pub fn new_connected_peer(&mut self) -> PeerId { @@ -340,367 +1312,62 @@ impl TestRig { .peers .write() .__add_connected_peer_testing_only(false, &self.harness.spec, key); - self.log(&format!("Added new peer for testing {peer_id:?}")); + + // Assumes custody subnet count == column count + let custody_subnets = self + .network_globals + .peers + .read() + .peer_info(&peer_id) + .expect("Peer should be known") + .custody_subnets_iter() + .copied() + .collect::<Vec<_>>(); + let peer_custody_str = + if custody_subnets.len() == self.harness.spec.number_of_custody_groups as usize { + "all".to_owned() + } else { + format!("{custody_subnets:?}") + }; + + self.log(&format!( + "Added new peer for testing {peer_id:?}, custody: {peer_custody_str}" + )); peer_id } pub fn new_connected_supernode_peer(&mut self) -> PeerId { let key = self.determinstic_key(); - self.network_globals + let peer_id = self + .network_globals .peers .write() - .__add_connected_peer_testing_only(true, &self.harness.spec, key) + .__add_connected_peer_testing_only(true, &self.harness.spec, key); + self.log(&format!( + "Added new peer for testing {peer_id:?}, custody: supernode" + )); + peer_id } fn determinstic_key(&mut self) -> CombinedKey { k256::ecdsa::SigningKey::random(&mut self.rng_08).into() } - pub fn new_connected_peers_for_peerdas(&mut self) { - // Enough sampling peers with few columns - for _ in 0..100 { - self.new_connected_peer(); - } - // One supernode peer to ensure all columns have at least one peer - self.new_connected_supernode_peer(); - } - - fn parent_chain_processed_success( - &mut self, - chain_hash: Hash256, - blocks: &[Arc<SignedBeaconBlock<E>>], - ) { - // Send import events for all pending parent blocks - for _ in blocks { - self.parent_block_processed_imported(chain_hash); - } - // Send final import event for the block that triggered the lookup - self.single_block_component_processed_imported(chain_hash); - } - - /// Locate a parent lookup chain with tip hash `chain_hash` - fn find_oldest_parent_lookup(&self, chain_hash: Hash256) -> Hash256 { - let parent_chain = self - .active_parent_lookups() - .into_iter() - .find(|chain| chain.first() == Some(&chain_hash)) - .unwrap_or_else(|| { - panic!( - "No parent chain with chain_hash {chain_hash:?}: Parent lookups {:?} Single lookups {:?}", - self.active_parent_lookups(), - self.active_single_lookups(), - ) - }); - *parent_chain.last().unwrap() - } - - fn parent_block_processed(&mut self, chain_hash: Hash256, result: BlockProcessingResult) { - let id = self.find_single_lookup_for(self.find_oldest_parent_lookup(chain_hash)); - self.single_block_component_processed(id, result); - } - - fn parent_blob_processed(&mut self, chain_hash: Hash256, result: BlockProcessingResult) { - let id = self.find_single_lookup_for(self.find_oldest_parent_lookup(chain_hash)); - self.single_blob_component_processed(id, result); - } - - fn parent_block_processed_imported(&mut self, chain_hash: Hash256) { - self.parent_block_processed( - chain_hash, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(chain_hash)), - ); - } - - fn single_block_component_processed(&mut self, id: Id, result: BlockProcessingResult) { - self.send_sync_message(SyncMessage::BlockComponentProcessed { - process_type: BlockProcessType::SingleBlock { id }, - result, - }) - } - - fn single_block_component_processed_imported(&mut self, block_root: Hash256) { - let id = self.find_single_lookup_for(block_root); - self.single_block_component_processed( - id, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(block_root)), - ) - } - - fn single_blob_component_processed(&mut self, id: Id, result: BlockProcessingResult) { - self.send_sync_message(SyncMessage::BlockComponentProcessed { - process_type: BlockProcessType::SingleBlob { id }, - result, - }) - } - - fn parent_lookup_block_response( - &mut self, - id: SingleLookupReqId, - peer_id: PeerId, - beacon_block: Option<Arc<SignedBeaconBlock<E>>>, - ) { - self.log("parent_lookup_block_response"); - self.send_sync_message(SyncMessage::RpcBlock { - sync_request_id: SyncRequestId::SingleBlock { id }, - peer_id, - beacon_block, - seen_timestamp: D, - }); - } - - fn single_lookup_block_response( - &mut self, - id: SingleLookupReqId, - peer_id: PeerId, - beacon_block: Option<Arc<SignedBeaconBlock<E>>>, - ) { - self.log("single_lookup_block_response"); - self.send_sync_message(SyncMessage::RpcBlock { - sync_request_id: SyncRequestId::SingleBlock { id }, - peer_id, - beacon_block, - seen_timestamp: D, - }); - } - - fn parent_lookup_blob_response( - &mut self, - id: SingleLookupReqId, - peer_id: PeerId, - blob_sidecar: Option<Arc<BlobSidecar<E>>>, - ) { - self.log(&format!( - "parent_lookup_blob_response {:?}", - blob_sidecar.as_ref().map(|b| b.index) - )); - self.send_sync_message(SyncMessage::RpcBlob { - sync_request_id: SyncRequestId::SingleBlob { id }, - peer_id, - blob_sidecar, - seen_timestamp: D, - }); - } - - fn single_lookup_blob_response( - &mut self, - id: SingleLookupReqId, - peer_id: PeerId, - blob_sidecar: Option<Arc<BlobSidecar<E>>>, - ) { - self.send_sync_message(SyncMessage::RpcBlob { - sync_request_id: SyncRequestId::SingleBlob { id }, - peer_id, - blob_sidecar, - seen_timestamp: D, - }); - } - - fn complete_single_lookup_blob_download( - &mut self, - id: SingleLookupReqId, - peer_id: PeerId, - blobs: Vec<BlobSidecar<E>>, - ) { - for blob in blobs { - self.single_lookup_blob_response(id, peer_id, Some(blob.into())); - } - self.single_lookup_blob_response(id, peer_id, None); - } - - fn complete_single_lookup_blob_lookup_valid( - &mut self, - id: SingleLookupReqId, - peer_id: PeerId, - blobs: Vec<BlobSidecar<E>>, - import: bool, - ) { - let block_root = blobs.first().unwrap().block_root(); - let block_slot = blobs.first().unwrap().slot(); - self.complete_single_lookup_blob_download(id, peer_id, blobs); - self.expect_block_process(ResponseType::Blob); - self.single_blob_component_processed( - id.lookup_id, - if import { - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(block_root)) - } else { - BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents( - block_slot, block_root, - )) - }, - ); - } - - fn complete_lookup_block_download(&mut self, block: SignedBeaconBlock<E>) { - let block_root = block.canonical_root(); - let id = self.expect_block_lookup_request(block_root); - self.expect_empty_network(); - let peer_id = self.new_connected_peer(); - self.single_lookup_block_response(id, peer_id, Some(block.into())); - self.single_lookup_block_response(id, peer_id, None); - } - - fn complete_lookup_block_import_valid(&mut self, block_root: Hash256, import: bool) { - self.expect_block_process(ResponseType::Block); - let id = self.find_single_lookup_for(block_root); - self.single_block_component_processed( - id, - if import { - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(block_root)) - } else { - BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents( - Slot::new(0), - block_root, - )) - }, - ) - } - - fn complete_single_lookup_block_valid(&mut self, block: SignedBeaconBlock<E>, import: bool) { - let block_root = block.canonical_root(); - self.complete_lookup_block_download(block); - self.complete_lookup_block_import_valid(block_root, import) - } - - fn parent_lookup_failed(&mut self, id: SingleLookupReqId, peer_id: PeerId, error: RPCError) { - self.send_sync_message(SyncMessage::RpcError { - peer_id, - sync_request_id: SyncRequestId::SingleBlock { id }, - error, - }) - } - - fn parent_lookup_failed_unavailable(&mut self, id: SingleLookupReqId, peer_id: PeerId) { - self.parent_lookup_failed( - id, - peer_id, - RPCError::ErrorResponse( - RpcErrorResponse::ResourceUnavailable, - "older than deneb".into(), - ), - ); - } - - fn single_lookup_failed(&mut self, id: SingleLookupReqId, peer_id: PeerId, error: RPCError) { - self.send_sync_message(SyncMessage::RpcError { - peer_id, - sync_request_id: SyncRequestId::SingleBlock { id }, - error, - }) - } - - fn complete_valid_block_request( - &mut self, - id: SingleLookupReqId, - block: Arc<SignedBeaconBlock<E>>, - missing_components: bool, - ) { - // Complete download - let peer_id = PeerId::random(); - let slot = block.slot(); - let block_root = block.canonical_root(); - self.single_lookup_block_response(id, peer_id, Some(block)); - self.single_lookup_block_response(id, peer_id, None); - // Expect processing and resolve with import - self.expect_block_process(ResponseType::Block); - self.single_block_component_processed( - id.lookup_id, - if missing_components { - BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents( - slot, block_root, - )) - } else { - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(block_root)) - }, - ) - } - - fn complete_valid_custody_request( - &mut self, - ids: DCByRootIds, - data_columns: Vec<Arc<DataColumnSidecar<E>>>, - missing_components: bool, - ) { - let lookup_id = if let SyncRequestId::DataColumnsByRoot(DataColumnsByRootRequestId { - requester: DataColumnsByRootRequester::Custody(id), - .. - }) = ids.first().unwrap().0 - { - id.requester.0.lookup_id - } else { - panic!("not a custody requester") - }; - - let first_column = data_columns.first().cloned().unwrap(); - - for id in ids { - self.log(&format!("return valid data column for {id:?}")); - let indices = &id.1; - let columns_to_send = indices - .iter() - .map(|&i| data_columns[i as usize].clone()) - .collect::<Vec<_>>(); - self.complete_data_columns_by_root_request(id, &columns_to_send); - } - - // Expect work event - self.expect_rpc_custody_column_work_event(); - - // Respond with valid result - self.send_sync_message(SyncMessage::BlockComponentProcessed { - process_type: BlockProcessType::SingleCustodyColumn(lookup_id), - result: if missing_components { - BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents( - first_column.slot(), - first_column.block_root(), - )) - } else { - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported( - first_column.block_root(), - )) - }, - }); - } - - fn complete_data_columns_by_root_request( - &mut self, - (sync_request_id, _): DCByRootId, - data_columns: &[Arc<DataColumnSidecar<E>>], - ) { - let peer_id = PeerId::random(); - for data_column in data_columns { - // Send chunks - self.send_sync_message(SyncMessage::RpcDataColumn { - sync_request_id, - peer_id, - data_column: Some(data_column.clone()), - seen_timestamp: timestamp_now(), - }); - } - // Send stream termination - self.send_sync_message(SyncMessage::RpcDataColumn { - sync_request_id, - peer_id, - data_column: None, - seen_timestamp: timestamp_now(), - }); - } - - /// Return RPCErrors for all active requests of peer - fn rpc_error_all_active_requests(&mut self, disconnected_peer_id: PeerId) { - self.drain_network_rx(); - while let Ok(sync_request_id) = self.pop_received_network_event(|ev| match ev { - NetworkMessage::SendRequest { - peer_id, - app_request_id: AppRequestId::Sync(id), - .. - } if *peer_id == disconnected_peer_id => Some(*id), - _ => None, - }) { - self.send_sync_message(SyncMessage::RpcError { - peer_id: disconnected_peer_id, - sync_request_id, - error: RPCError::Disconnected, - }); + pub fn new_connected_peers_for_peerdas(&mut self) -> Vec<PeerId> { + match self.fulu_test_type.them_node_custody_type() { + NodeCustodyType::Fullnode => { + // Enough sampling peers with few columns + let mut peers = (0..100) + .map(|_| self.new_connected_peer()) + .collect::<Vec<_>>(); + // One supernode peer to ensure all columns have at least one peer + peers.push(self.new_connected_supernode_peer()); + peers + } + NodeCustodyType::Supernode | NodeCustodyType::SemiSupernode => { + let peer = self.new_connected_supernode_peer(); + vec![peer] + } } } @@ -708,6 +1375,22 @@ impl TestRig { self.send_sync_message(SyncMessage::Disconnect(peer_id)); } + fn get_connected_peers(&self) -> Vec<PeerId> { + self.network_globals + .peers + .read() + .peers() + .map(|(peer, _)| *peer) + .collect::<Vec<_>>() + } + + fn disconnect_all_peers(&mut self) { + for peer in self.get_connected_peers() { + self.log(&format!("Disconnecting peer {peer}")); + self.send_sync_message(SyncMessage::Disconnect(peer)); + } + } + fn drain_network_rx(&mut self) { while let Ok(event) = self.network_rx.try_recv() { self.network_rx_queue.push(event); @@ -764,7 +1447,7 @@ impl TestRig { } } - pub fn expect_empty_processor(&mut self) { + pub fn assert_empty_processor(&mut self) { self.drain_processor_rx(); if !self.beacon_processor_rx_queue.is_empty() { panic!( @@ -774,215 +1457,8 @@ impl TestRig { } } - fn find_block_lookup_request( - &mut self, - for_block: Hash256, - ) -> Result<SingleLookupReqId, String> { - self.pop_received_network_event(|ev| match ev { - NetworkMessage::SendRequest { - peer_id: _, - request: RequestType::BlocksByRoot(request), - app_request_id: AppRequestId::Sync(SyncRequestId::SingleBlock { id }), - } if request.block_roots().to_vec().contains(&for_block) => Some(*id), - _ => None, - }) - } - #[track_caller] - fn expect_block_lookup_request(&mut self, for_block: Hash256) -> SingleLookupReqId { - self.find_block_lookup_request(for_block) - .unwrap_or_else(|e| panic!("Expected block request for {for_block:?}: {e}")) - } - - fn find_blob_lookup_request( - &mut self, - for_block: Hash256, - ) -> Result<SingleLookupReqId, String> { - self.pop_received_network_event(|ev| match ev { - NetworkMessage::SendRequest { - peer_id: _, - request: RequestType::BlobsByRoot(request), - app_request_id: AppRequestId::Sync(SyncRequestId::SingleBlob { id }), - } if request - .blob_ids - .to_vec() - .iter() - .any(|r| r.block_root == for_block) => - { - Some(*id) - } - _ => None, - }) - } - - #[track_caller] - fn expect_blob_lookup_request(&mut self, for_block: Hash256) -> SingleLookupReqId { - self.find_blob_lookup_request(for_block) - .unwrap_or_else(|e| panic!("Expected blob request for {for_block:?}: {e}")) - } - - #[track_caller] - fn expect_block_parent_request(&mut self, for_block: Hash256) -> SingleLookupReqId { - self.pop_received_network_event(|ev| match ev { - NetworkMessage::SendRequest { - peer_id: _, - request: RequestType::BlocksByRoot(request), - app_request_id: AppRequestId::Sync(SyncRequestId::SingleBlock { id }), - } if request.block_roots().to_vec().contains(&for_block) => Some(*id), - _ => None, - }) - .unwrap_or_else(|e| panic!("Expected block parent request for {for_block:?}: {e}")) - } - - fn expect_no_requests_for(&mut self, block_root: Hash256) { - if let Ok(request) = self.find_block_lookup_request(block_root) { - panic!("Expected no block request for {block_root:?} found {request:?}"); - } - if let Ok(request) = self.find_blob_lookup_request(block_root) { - panic!("Expected no blob request for {block_root:?} found {request:?}"); - } - } - - #[track_caller] - fn expect_blob_parent_request(&mut self, for_block: Hash256) -> SingleLookupReqId { - self.pop_received_network_event(|ev| match ev { - NetworkMessage::SendRequest { - peer_id: _, - request: RequestType::BlobsByRoot(request), - app_request_id: AppRequestId::Sync(SyncRequestId::SingleBlob { id }), - } if request - .blob_ids - .to_vec() - .iter() - .all(|r| r.block_root == for_block) => - { - Some(*id) - } - _ => None, - }) - .unwrap_or_else(|e| panic!("Expected blob parent request for {for_block:?}: {e}")) - } - - /// Retrieves an unknown number of requests for data columns of `block_root`. Because peer ENRs - /// are random, and peer selection is random, the total number of batched requests is unknown. - fn expect_data_columns_by_root_requests( - &mut self, - block_root: Hash256, - count: usize, - ) -> DCByRootIds { - let mut requests: DCByRootIds = vec![]; - loop { - let req = self - .pop_received_network_event(|ev| match ev { - NetworkMessage::SendRequest { - peer_id: _, - request: RequestType::DataColumnsByRoot(request), - app_request_id: - AppRequestId::Sync(id @ SyncRequestId::DataColumnsByRoot { .. }), - } => { - let matching = request - .data_column_ids - .iter() - .find(|id| id.block_root == block_root)?; - - let indices = matching.columns.iter().copied().collect(); - Some((*id, indices)) - } - _ => None, - }) - .unwrap_or_else(|e| { - panic!("Expected more DataColumnsByRoot requests for {block_root:?}: {e}") - }); - requests.push(req); - - // Should never infinite loop because sync does not send requests for 0 columns - if requests.iter().map(|r| r.1.len()).sum::<usize>() >= count { - return requests; - } - } - } - - fn expect_only_data_columns_by_root_requests( - &mut self, - for_block: Hash256, - count: usize, - ) -> DCByRootIds { - let ids = self.expect_data_columns_by_root_requests(for_block, count); - self.expect_empty_network(); - ids - } - - #[track_caller] - fn expect_block_process(&mut self, response_type: ResponseType) { - match response_type { - ResponseType::Block => self - .pop_received_processor_event(|ev| { - (ev.work_type() == beacon_processor::WorkType::RpcBlock).then_some(()) - }) - .unwrap_or_else(|e| panic!("Expected block work event: {e}")), - ResponseType::Blob => self - .pop_received_processor_event(|ev| { - (ev.work_type() == beacon_processor::WorkType::RpcBlobs).then_some(()) - }) - .unwrap_or_else(|e| panic!("Expected blobs work event: {e}")), - ResponseType::CustodyColumn => self - .pop_received_processor_event(|ev| { - (ev.work_type() == beacon_processor::WorkType::RpcCustodyColumn).then_some(()) - }) - .unwrap_or_else(|e| panic!("Expected column work event: {e}")), - } - } - - fn expect_rpc_custody_column_work_event(&mut self) { - self.pop_received_processor_event(|ev| { - if ev.work_type() == beacon_processor::WorkType::RpcCustodyColumn { - Some(()) - } else { - None - } - }) - .unwrap_or_else(|e| panic!("Expected RPC custody column work: {e}")) - } - - #[allow(dead_code)] - fn expect_no_work_event(&mut self) { - self.drain_processor_rx(); - assert!(self.network_rx_queue.is_empty()); - } - - fn expect_no_penalty_for(&mut self, peer_id: PeerId) { - self.drain_network_rx(); - let downscore_events = self - .network_rx_queue - .iter() - .filter_map(|ev| match ev { - NetworkMessage::ReportPeer { - peer_id: p_id, msg, .. - } if p_id == &peer_id => Some(msg), - _ => None, - }) - .collect::<Vec<_>>(); - if !downscore_events.is_empty() { - panic!("Some downscore events for {peer_id}: {downscore_events:?}"); - } - } - - #[track_caller] - fn expect_parent_chain_process(&mut self) { - match self.beacon_processor_rx.try_recv() { - Ok(work) => { - // Parent chain sends blocks one by one - assert_eq!(work.work_type(), beacon_processor::WorkType::RpcBlock); - } - other => panic!( - "Expected rpc_block from chain segment process, found {:?}", - other - ), - } - } - - #[track_caller] - pub fn expect_empty_network(&mut self) { + pub fn assert_empty_network(&mut self) { self.drain_network_rx(); if !self.network_rx_queue.is_empty() { let n = self.network_rx_queue.len(); @@ -993,115 +1469,52 @@ impl TestRig { } } - #[track_caller] - fn expect_empty_beacon_processor(&mut self) { - match self.beacon_processor_rx.try_recv() { - Err(mpsc::error::TryRecvError::Empty) => {} // ok - Ok(event) => panic!("expected empty beacon processor: {:?}", event), - other => panic!("unexpected err {:?}", other), - } - } - - #[track_caller] - pub fn expect_penalty(&mut self, peer_id: PeerId, expect_penalty_msg: &'static str) { - let penalty_msg = self - .pop_received_network_event(|ev| match ev { - NetworkMessage::ReportPeer { - peer_id: p_id, msg, .. - } if p_id == &peer_id => Some(msg.to_owned()), - _ => None, - }) - .unwrap_or_else(|_| { - panic!( - "Expected '{expect_penalty_msg}' penalty for peer {peer_id}: {:#?}", - self.network_rx_queue - ) - }); - assert_eq!( - penalty_msg, expect_penalty_msg, - "Unexpected penalty msg for {peer_id}" - ); - self.log(&format!("Found expected penalty {penalty_msg}")); - } - - pub fn block_with_parent_and_blobs( + async fn import_block_to_da_checker( &mut self, - parent_root: Hash256, - num_blobs: NumBlobs, - ) -> (SignedBeaconBlock<E>, Vec<BlobSidecar<E>>) { - let (mut block, mut blobs) = self.rand_block_and_blobs(num_blobs); - *block.message_mut().parent_root_mut() = parent_root; - blobs.iter_mut().for_each(|blob| { - blob.signed_block_header = block.signed_block_header(); - }); - (block, blobs) - } - - pub fn rand_blockchain(&mut self, depth: usize) -> Vec<Arc<SignedBeaconBlock<E>>> { - let mut blocks = Vec::<Arc<SignedBeaconBlock<E>>>::with_capacity(depth); - for slot in 0..depth { - let parent = blocks - .last() - .map(|b| b.canonical_root()) - .unwrap_or_else(Hash256::random); - let mut block = self.rand_block(); - *block.message_mut().parent_root_mut() = parent; - *block.message_mut().slot_mut() = slot.into(); - blocks.push(block.into()); - } - self.log(&format!( - "Blockchain dump {:#?}", - blocks - .iter() - .map(|b| format!( - "block {} {} parent {}", - b.slot(), - b.canonical_root(), - b.parent_root() - )) - .collect::<Vec<_>>() - )); - blocks - } - - fn insert_block_to_da_checker(&mut self, block: Arc<SignedBeaconBlock<E>>) { - let state = BeaconState::Base(BeaconStateBase::random_for_test(&mut self.rng)); - let parent_block = self.rand_block(); - let import_data = BlockImportData::<E>::__new_for_test( - block.canonical_root(), - state, - parent_block.into(), - ); - let payload_verification_outcome = PayloadVerificationOutcome { - payload_verification_status: PayloadVerificationStatus::Verified, - is_valid_merge_transition_block: false, - }; - let executed_block = - AvailabilityPendingExecutedBlock::new(block, import_data, payload_verification_outcome); - match self - .harness + block: Arc<SignedBeaconBlock<E>>, + ) -> AvailabilityProcessingStatus { + // Simulate importing block from another source. Don't use GossipVerified as it checks with + // the clock, which does not match the timestamp in the payload. + let block_root = block.canonical_root(); + let rpc_block = RpcBlock::BlockOnly { block_root, block }; + self.harness .chain - .data_availability_checker - .put_executed_block(executed_block) - .unwrap() - { - Availability::Available(_) => panic!("block removed from da_checker, available"), - Availability::MissingComponents(block_root) => { + .process_block( + block_root, + rpc_block, + NotifyExecutionLayer::Yes, + BlockImportSource::Gossip, + || Ok(()), + ) + .await + .expect("Error processing block") + } + + async fn insert_block_to_da_chain_and_assert_missing_componens( + &mut self, + block: Arc<SignedBeaconBlock<E>>, + ) { + match self.import_block_to_da_checker(block).await { + AvailabilityProcessingStatus::Imported(_) => { + panic!("block removed from da_checker, available") + } + AvailabilityProcessingStatus::MissingComponents(_, block_root) => { self.log(&format!("inserted block to da_checker {block_root:?}")) } - }; + } } - fn insert_blob_to_da_checker(&mut self, blob: BlobSidecar<E>) { + fn insert_blob_to_da_checker(&mut self, blob: Arc<BlobSidecar<E>>) { match self .harness .chain .data_availability_checker - .put_gossip_verified_blobs( + .put_kzg_verified_blobs( blob.block_root(), - std::iter::once(GossipVerifiedBlob::<_, Observe>::__assumed_valid( - blob.into(), - )), + std::iter::once( + KzgVerifiedBlob::new(blob, &self.harness.chain.kzg, Duration::new(0, 0)) + .expect("Invalid blob"), + ), ) .unwrap() { @@ -1112,7 +1525,11 @@ impl TestRig { }; } - fn insert_block_to_availability_cache(&mut self, block: Arc<SignedBeaconBlock<E>>) { + fn insert_block_to_da_checker_as_pre_execution(&mut self, block: Arc<SignedBeaconBlock<E>>) { + self.log(&format!( + "Inserting block to availability_cache as pre_execution_block {:?}", + block.canonical_root() + )); self.harness .chain .data_availability_checker @@ -1121,6 +1538,9 @@ impl TestRig { } fn simulate_block_gossip_processing_becomes_invalid(&mut self, block_root: Hash256) { + self.log(&format!( + "Marking block {block_root:?} in da_checker as execution error" + )); self.harness .chain .data_availability_checker @@ -1132,19 +1552,38 @@ impl TestRig { }); } - fn simulate_block_gossip_processing_becomes_valid_missing_components( + async fn simulate_block_gossip_processing_becomes_valid( &mut self, block: Arc<SignedBeaconBlock<E>>, ) { let block_root = block.canonical_root(); - self.insert_block_to_da_checker(block); + match self.import_block_to_da_checker(block).await { + AvailabilityProcessingStatus::Imported(block_root) => { + self.log(&format!( + "insert block to da_checker and it imported {block_root:?}" + )); + } + AvailabilityProcessingStatus::MissingComponents(_, _) => { + panic!("block not imported after adding to da_checker"); + } + } self.send_sync_message(SyncMessage::GossipBlockProcessResult { block_root, imported: false, }); } + + fn requests_count(&self) -> HashMap<&'static str, usize> { + let mut requests_count = HashMap::new(); + for (request, _) in &self.requests { + *requests_count + .entry(Into::<&'static str>::into(request)) + .or_default() += 1; + } + requests_count + } } #[test] @@ -1161,1558 +1600,803 @@ fn stable_rng() { ); } -#[test] -fn test_single_block_lookup_happy_path() { - let mut rig = TestRig::test_setup(); - let block = rig.rand_block(); - let peer_id = rig.new_connected_peer(); - let block_root = block.canonical_root(); - // Trigger the request - rig.trigger_unknown_block_from_attestation(block_root, peer_id); - let id = rig.expect_block_lookup_request(block_root); +macro_rules! run_lookups_tests_for_depths { + ($($depth:literal),+ $(,)?) => { + paste::paste! { + $( + #[tokio::test] + async fn [<happy_path_unknown_attestation_depth_ $depth>]() { + happy_path_unknown_attestation($depth).await; + } - // The peer provides the correct block, should not be penalized. Now the block should be sent - // for processing. - rig.single_lookup_block_response(id, peer_id, Some(block.into())); - rig.expect_empty_network(); - rig.expect_block_process(ResponseType::Block); + #[tokio::test] + async fn [<happy_path_unknown_block_parent_depth_ $depth>]() { + happy_path_unknown_block_parent($depth).await; + } - // The request should still be active. - assert_eq!(rig.active_single_lookups_count(), 1); + #[tokio::test] + async fn [<happy_path_unknown_data_parent_depth_ $depth>]() { + happy_path_unknown_data_parent($depth).await; + } - // Send the stream termination. Peer should have not been penalized, and the request removed - // after processing. - rig.single_lookup_block_response(id, peer_id, None); - rig.single_block_component_processed_imported(block_root); - rig.expect_empty_network(); - rig.expect_no_active_lookups(); + #[tokio::test] + async fn [<happy_path_multiple_triggers_depth_ $depth>]() { + happy_path_multiple_triggers($depth).await; + } + + #[tokio::test] + async fn [<bad_peer_empty_block_response_depth_ $depth>]() { + bad_peer_empty_block_response($depth).await; + } + + #[tokio::test] + async fn [<bad_peer_empty_data_response_depth_ $depth>]() { + bad_peer_empty_data_response($depth).await; + } + + #[tokio::test] + async fn [<bad_peer_too_few_data_response_depth_ $depth>]() { + bad_peer_too_few_data_response($depth).await; + } + + #[tokio::test] + async fn [<bad_peer_wrong_block_response_depth_ $depth>]() { + bad_peer_wrong_block_response($depth).await; + } + + #[tokio::test] + async fn [<bad_peer_wrong_data_response_depth_ $depth>]() { + bad_peer_wrong_data_response($depth).await; + } + + #[tokio::test] + async fn [<bad_peer_rpc_failure_depth_ $depth>]() { + bad_peer_rpc_failure($depth).await; + } + + #[tokio::test] + async fn [<too_many_download_failures_depth_ $depth>]() { + too_many_download_failures($depth).await; + } + + #[tokio::test] + async fn [<too_many_processing_failures_depth_ $depth>]() { + too_many_processing_failures($depth).await; + } + + #[tokio::test] + async fn [<peer_disconnected_then_rpc_error_depth_ $depth>]() { + peer_disconnected_then_rpc_error($depth).await; + } + )+ + } + }; } -// Tests that if a peer does not respond with a block, we downscore and retry the block only -#[test] -fn test_single_block_lookup_empty_response() { - let mut r = TestRig::test_setup(); +run_lookups_tests_for_depths!(1, 2); - let block = r.rand_block(); - let block_root = block.canonical_root(); - let peer_id = r.new_connected_peer(); - - // Trigger the request - r.trigger_unknown_block_from_attestation(block_root, peer_id); - let id = r.expect_block_lookup_request(block_root); - - // The peer does not have the block. It should be penalized. - r.single_lookup_block_response(id, peer_id, None); - r.expect_penalty(peer_id, "NotEnoughResponsesReturned"); - // it should be retried - let id = r.expect_block_lookup_request(block_root); - // Send the right block this time. - r.single_lookup_block_response(id, peer_id, Some(block.into())); - r.expect_block_process(ResponseType::Block); - r.single_block_component_processed_imported(block_root); - r.expect_no_active_lookups(); +/// Assert that lookup sync succeeds with the happy case +async fn happy_path_unknown_attestation(depth: usize) { + let mut r = TestRig::default(); + // We get attestation for a block descendant (depth) blocks of current head + r.build_chain_and_trigger_last_block(depth).await; + // Complete the request with good peer behaviour + r.simulate(SimulateConfig::happy_path()).await; + r.assert_successful_lookup_sync(); } -#[test] -fn test_single_block_lookup_wrong_response() { - let mut rig = TestRig::test_setup(); - - let block_hash = Hash256::random(); - let peer_id = rig.new_connected_peer(); - - // Trigger the request - rig.trigger_unknown_block_from_attestation(block_hash, peer_id); - let id = rig.expect_block_lookup_request(block_hash); - - // Peer sends something else. It should be penalized. - let bad_block = rig.rand_block(); - rig.single_lookup_block_response(id, peer_id, Some(bad_block.into())); - rig.expect_penalty(peer_id, "UnrequestedBlockRoot"); - rig.expect_block_lookup_request(block_hash); // should be retried - - // Send the stream termination. This should not produce an additional penalty. - rig.single_lookup_block_response(id, peer_id, None); - rig.expect_empty_network(); +async fn happy_path_unknown_block_parent(depth: usize) { + let mut r = TestRig::default(); + r.build_chain(depth).await; + r.trigger_with_last_unknown_block_parent(); + r.simulate(SimulateConfig::happy_path()).await; + // All lookups should NOT complete on this test, however note the following for the tip lookup, + // it's the lookup for the tip block which has 0 peers and a block cached: + // - before deneb the block is cached, so it's sent for processing, and success + // - before fulu the block is cached, but we can't fetch blobs so it's stuck + // - after fulu the block is cached, we start a custody request and since we use the global pool + // of peers we DO have 1 connected synced supernode peer, which gives us the columns and the + // lookup succeeds + if r.is_after_deneb() && !r.is_after_fulu() { + r.assert_successful_lookup_sync_parent_trigger() + } else { + r.assert_successful_lookup_sync(); + } } -#[test] -fn test_single_block_lookup_failure() { - let mut rig = TestRig::test_setup(); - - let block_hash = Hash256::random(); - let peer_id = rig.new_connected_peer(); - - // Trigger the request - rig.trigger_unknown_block_from_attestation(block_hash, peer_id); - let id = rig.expect_block_lookup_request(block_hash); - - // The request fails. RPC failures are handled elsewhere so we should not penalize the peer. - rig.single_lookup_failed(id, peer_id, RPCError::UnsupportedProtocol); - rig.expect_block_lookup_request(block_hash); - rig.expect_empty_network(); +/// Assert that sync completes from a GossipUnknownParentBlob / UnknownDataColumnParent +async fn happy_path_unknown_data_parent(depth: usize) { + let Some(mut r) = TestRig::new_after_deneb() else { + return; + }; + r.build_chain(depth).await; + if r.is_after_fulu() { + r.trigger_with_last_unknown_data_column_parent(); + } else if r.is_after_deneb() { + r.trigger_with_last_unknown_blob_parent(); + } + r.simulate(SimulateConfig::happy_path()).await; + r.assert_successful_lookup_sync_parent_trigger(); } -#[test] -fn test_single_block_lookup_peer_disconnected_then_rpc_error() { - let mut rig = TestRig::test_setup(); +/// Assert that multiple trigger types don't create extra lookups +async fn happy_path_multiple_triggers(depth: usize) { + let mut r = TestRig::default(); + // + 1, because the unknown parent trigger needs two new blocks + r.build_chain(depth + 1).await; + r.trigger_with_last_block(); + r.trigger_with_last_block(); + r.trigger_with_last_unknown_block_parent(); + r.trigger_with_last_unknown_block_parent(); + if r.is_after_fulu() { + r.trigger_with_last_unknown_data_column_parent(); + } else if r.is_after_deneb() { + r.trigger_with_last_unknown_blob_parent(); + } + r.simulate(SimulateConfig::happy_path()).await; + assert_eq!(r.created_lookups(), depth + 1, "Don't create extra lookups"); + r.assert_successful_lookup_sync(); +} - let block_hash = Hash256::random(); - let peer_id = rig.new_connected_peer(); +// Test bad behaviour of peers - // Trigger the request. - rig.trigger_unknown_block_from_attestation(block_hash, peer_id); - let id = rig.expect_block_lookup_request(block_hash); +/// Assert that if peer responds with no blocks, we downscore, and retry the same lookup +async fn bad_peer_empty_block_response(depth: usize) { + let mut r = TestRig::default(); + r.build_chain_and_trigger_last_block(depth).await; + // Simulate that peer returns empty response once, then good behaviour + r.simulate(SimulateConfig::new().return_no_blocks_once()) + .await; + // We register a penalty, retry and complete sync successfully + r.assert_penalties(&["NotEnoughResponsesReturned"]); + r.assert_successful_lookup_sync(); + // TODO(tree-sync) For post-deneb assert that the blobs are not re-fetched + // TODO(tree-sync) Assert that a single lookup is created (no drops) +} + +/// Assert that if peer responds with no blobs / columns, we downscore, and retry the same lookup +async fn bad_peer_empty_data_response(depth: usize) { + let Some(mut r) = TestRig::new_after_deneb() else { + return; + }; + r.build_chain_and_trigger_last_block(depth).await; + r.simulate(SimulateConfig::new().return_no_data_once()) + .await; + // We register a penalty, retry and complete sync successfully + r.assert_penalties(&["NotEnoughResponsesReturned"]); + r.assert_successful_lookup_sync(); + // TODO(tree-sync) Assert that a single lookup is created (no drops) +} + +/// Assert that if peer responds with not enough blobs / columns, we downscore, and retry the same +/// lookup +async fn bad_peer_too_few_data_response(depth: usize) { + let Some(mut r) = TestRig::new_after_deneb() else { + return; + }; + r.build_chain_and_trigger_last_block(depth).await; + r.simulate(SimulateConfig::new().return_too_few_data_once()) + .await; + // We register a penalty, retry and complete sync successfully + r.assert_penalties(&["NotEnoughResponsesReturned"]); + r.assert_successful_lookup_sync(); + // TODO(tree-sync) Assert that a single lookup is created (no drops) +} + +/// Assert that if peer responds with bad blocks, we downscore, and retry the same lookup +async fn bad_peer_wrong_block_response(depth: usize) { + let mut r = TestRig::default(); + r.build_chain_and_trigger_last_block(depth).await; + r.simulate(SimulateConfig::new().return_wrong_blocks_once()) + .await; + r.assert_penalties(&["UnrequestedBlockRoot"]); + r.assert_successful_lookup_sync(); + + // TODO(tree-sync) Assert that a single lookup is created (no drops) +} + +/// Assert that if peer responds with bad blobs / columns, we downscore, and retry the same lookup +async fn bad_peer_wrong_data_response(depth: usize) { + let Some(mut r) = TestRig::new_after_deneb() else { + return; + }; + r.build_chain_and_trigger_last_block(depth).await; + r.simulate(SimulateConfig::new().return_wrong_sidecar_for_block_once()) + .await; + // We register a penalty, retry and complete sync successfully + r.assert_penalties(&["UnrequestedBlockRoot"]); + r.assert_successful_lookup_sync(); + // TODO(tree-sync) Assert that a single lookup is created (no drops) +} + +/// Assert that on network error, we DON'T downscore, and retry the same lookup +async fn bad_peer_rpc_failure(depth: usize) { + let mut r = TestRig::default(); + r.build_chain_and_trigger_last_block(depth).await; + r.simulate(SimulateConfig::new().return_rpc_error(RPCError::UnsupportedProtocol)) + .await; + r.assert_no_penalties(); + r.assert_successful_lookup_sync(); +} + +// Test retry logic + +/// Assert that on too many download failures the lookup fails, but we can still sync +async fn too_many_download_failures(depth: usize) { + let mut r = TestRig::default(); + r.build_chain_and_trigger_last_block(depth).await; + // Simulate that a peer always returns empty + r.simulate(SimulateConfig::new().return_no_blocks_always()) + .await; + // We register multiple penalties, the lookup fails and sync does not progress + r.assert_penalties_of_type("NotEnoughResponsesReturned"); + r.assert_failed_lookup_sync(); + + // Trigger sync again for same block, and complete successfully. + // Asserts that the lookup is not on a blacklist + r.capture_metrics_baseline(); + r.trigger_with_last_block(); + r.simulate(SimulateConfig::happy_path()).await; + r.assert_successful_lookup_sync(); +} + +/// Assert that on too many processing failures the lookup fails, but we can still sync +async fn too_many_processing_failures(depth: usize) { + let mut r = TestRig::default(); + r.build_chain_and_trigger_last_block(depth).await; + // Simulate that a peer always returns empty + r.simulate( + SimulateConfig::new() + .with_process_result(|| BlockProcessingResult::Err(BlockError::BlockSlotLimitReached)), + ) + .await; + // We register multiple penalties, the lookup fails and sync does not progress + r.assert_penalties_of_type("lookup_block_processing_failure"); + r.assert_failed_lookup_sync(); + + // Trigger sync again for same block, and complete successfully. + // Asserts that the lookup is not on a blacklist + r.capture_metrics_baseline(); + r.trigger_with_last_block(); + r.simulate(SimulateConfig::happy_path()).await; + r.assert_successful_lookup_sync(); +} + +#[tokio::test] +/// Assert that multiple trigger types don't create extra lookups +async fn unknown_parent_does_not_add_peers_to_itself() { + let Some(mut r) = TestRig::new_after_deneb() else { + return; + }; + // 2, because the unknown parent trigger needs two new blocks + r.build_chain(2).await; + r.trigger_with_last_unknown_block_parent(); + r.trigger_with_last_unknown_block_parent(); + if r.is_after_fulu() { + r.trigger_with_last_unknown_data_column_parent(); + } else if r.is_after_deneb() { + r.trigger_with_last_unknown_blob_parent(); + } + r.simulate(SimulateConfig::happy_path()).await; + r.assert_peers_at_lookup_of_slot(2, 0); + r.assert_peers_at_lookup_of_slot(1, 3); + assert_eq!(r.created_lookups(), 2, "Don't create extra lookups"); + // All lookups should NOT complete on this test, however note the following for the tip lookup, + // it's the lookup for the tip block which has 0 peers and a block cached: + // - before fulu the block is cached, but we can't fetch blobs so it's stuck + // - after fulu the block is cached, we start a custody request and since we use the global pool + // of peers we DO have >1 connected synced supernode peer, which gives us the columns and the + // lookup succeeds + if r.is_after_fulu() { + r.assert_successful_lookup_sync() + } else { + r.assert_successful_lookup_sync_parent_trigger(); + } +} + +#[tokio::test] +/// Assert that if the beacon processor returns Ignored, the lookup is dropped +async fn test_single_block_lookup_ignored_response() { + let mut r = TestRig::default(); + r.build_chain_and_trigger_last_block(1).await; + // Send an Ignored response, the request should be dropped + r.simulate(SimulateConfig::new().with_process_result(|| BlockProcessingResult::Ignored)) + .await; + // The block was not actually imported + r.assert_head_slot(0); + assert_eq!(r.created_lookups(), 1, "no created lookups"); + assert_eq!(r.dropped_lookups(), 1, "no dropped lookups"); + assert_eq!(r.completed_lookups(), 0, "some completed lookups"); +} + +#[tokio::test] +/// Assert that if the beacon processor returns DuplicateFullyImported, the lookup completes successfully +async fn test_single_block_lookup_duplicate_response() { + let mut r = TestRig::default(); + r.build_chain_and_trigger_last_block(1).await; + // Send a DuplicateFullyImported response, the lookup should complete successfully + r.simulate(SimulateConfig::new().with_process_result(|| { + BlockProcessingResult::Err(BlockError::DuplicateFullyImported(Hash256::ZERO)) + })) + .await; + // The block was not actually imported + r.assert_head_slot(0); + r.assert_successful_lookup_sync(); +} + +/// Assert that when peers disconnect the lookups are not dropped (kept with zero peers) +async fn peer_disconnected_then_rpc_error(depth: usize) { + let mut r = TestRig::default(); + r.build_chain_and_trigger_last_block(depth).await; + r.assert_single_lookups_count(1); // The peer disconnect event reaches sync before the rpc error. - rig.peer_disconnected(peer_id); + r.disconnect_all_peers(); // The lookup is not removed as it can still potentially make progress. - rig.assert_single_lookups_count(1); - // The request fails. - rig.single_lookup_failed(id, peer_id, RPCError::Disconnected); - rig.expect_block_lookup_request(block_hash); - // The request should be removed from the network context on disconnection. - rig.expect_empty_network(); + r.assert_single_lookups_count(1); + r.simulate(SimulateConfig::new().return_rpc_error(RPCError::Disconnected)) + .await; + + // Regardless of depth, only the initial lookup is created, because the peer disconnects before + // being able to download the block + assert_eq!(r.created_lookups(), 1, "no created lookups"); + assert_eq!(r.completed_lookups(), 0, "some completed lookups"); + assert_eq!(r.dropped_lookups(), 0, "some dropped lookups"); + r.assert_empty_network(); + r.assert_single_lookups_count(1); } -#[test] -fn test_single_block_lookup_becomes_parent_request() { - let mut rig = TestRig::test_setup(); - - let block = Arc::new(rig.rand_block()); - let block_root = block.canonical_root(); - let parent_root = block.parent_root(); - let peer_id = rig.new_connected_peer(); - - // Trigger the request - rig.trigger_unknown_block_from_attestation(block.canonical_root(), peer_id); - let id = rig.expect_block_parent_request(block_root); - - // The peer provides the correct block, should not be penalized. Now the block should be sent - // for processing. - rig.single_lookup_block_response(id, peer_id, Some(block.clone())); - rig.expect_empty_network(); - rig.expect_block_process(ResponseType::Block); - - // The request should still be active. - assert_eq!(rig.active_single_lookups_count(), 1); - - // Send the stream termination. Peer should have not been penalized, and the request moved to a - // parent request after processing. - rig.single_block_component_processed( - id.lookup_id, - BlockProcessingResult::Err(BlockError::ParentUnknown { - parent_root: block.parent_root(), - }), - ); - assert_eq!(rig.active_single_lookups_count(), 2); // 2 = current + parent - rig.expect_block_parent_request(parent_root); - rig.expect_empty_network(); - assert_eq!(rig.active_parent_lookups_count(), 1); -} - -#[test] -fn test_parent_lookup_happy_path() { - let mut rig = TestRig::test_setup(); - - let (parent, block, parent_root, block_root) = rig.rand_block_and_parent(); - let peer_id = rig.new_connected_peer(); - - // Trigger the request - rig.trigger_unknown_parent_block(peer_id, block.into()); - let id = rig.expect_block_parent_request(parent_root); - - // Peer sends the right block, it should be sent for processing. Peer should not be penalized. - rig.parent_lookup_block_response(id, peer_id, Some(parent.into())); - // No request of blobs because the block has not data - rig.expect_empty_network(); - rig.expect_block_process(ResponseType::Block); - rig.expect_empty_network(); - - // Add peer to child lookup to prevent it being dropped - rig.trigger_unknown_block_from_attestation(block_root, peer_id); - // Processing succeeds, now the rest of the chain should be sent for processing. - rig.parent_block_processed( - block_root, - BlockError::DuplicateFullyImported(block_root).into(), - ); - rig.expect_parent_chain_process(); - rig.parent_chain_processed_success(block_root, &[]); - rig.expect_no_active_lookups_empty_network(); -} - -#[test] -fn test_parent_lookup_wrong_response() { - let mut rig = TestRig::test_setup(); - - let (parent, block, parent_root, block_root) = rig.rand_block_and_parent(); - let peer_id = rig.new_connected_peer(); - - // Trigger the request - rig.trigger_unknown_parent_block(peer_id, block.into()); - let id1 = rig.expect_block_parent_request(parent_root); - - // Peer sends the wrong block, peer should be penalized and the block re-requested. - let bad_block = rig.rand_block(); - rig.parent_lookup_block_response(id1, peer_id, Some(bad_block.into())); - rig.expect_penalty(peer_id, "UnrequestedBlockRoot"); - let id2 = rig.expect_block_parent_request(parent_root); - - // Send the stream termination for the first request. This should not produce extra penalties. - rig.parent_lookup_block_response(id1, peer_id, None); - rig.expect_empty_network(); - - // Send the right block this time. - rig.parent_lookup_block_response(id2, peer_id, Some(parent.into())); - rig.expect_block_process(ResponseType::Block); - - // Add peer to child lookup to prevent it being dropped - rig.trigger_unknown_block_from_attestation(block_root, peer_id); - // Processing succeeds, now the rest of the chain should be sent for processing. - rig.parent_block_processed_imported(block_root); - rig.expect_parent_chain_process(); - rig.parent_chain_processed_success(block_root, &[]); - rig.expect_no_active_lookups_empty_network(); -} - -#[test] -fn test_parent_lookup_rpc_failure() { - let mut rig = TestRig::test_setup(); - - let (parent, block, parent_root, block_root) = rig.rand_block_and_parent(); - let peer_id = rig.new_connected_peer(); - - // Trigger the request - rig.trigger_unknown_parent_block(peer_id, block.into()); - let id = rig.expect_block_parent_request(parent_root); - - // The request fails. It should be tried again. - rig.parent_lookup_failed_unavailable(id, peer_id); - let id = rig.expect_block_parent_request(parent_root); - - // Send the right block this time. - rig.parent_lookup_block_response(id, peer_id, Some(parent.into())); - rig.expect_block_process(ResponseType::Block); - - // Add peer to child lookup to prevent it being dropped - rig.trigger_unknown_block_from_attestation(block_root, peer_id); - // Processing succeeds, now the rest of the chain should be sent for processing. - rig.parent_block_processed_imported(block_root); - rig.expect_parent_chain_process(); - rig.parent_chain_processed_success(block_root, &[]); - rig.expect_no_active_lookups_empty_network(); -} - -#[test] -fn test_parent_lookup_too_many_attempts() { - let mut rig = TestRig::test_setup(); - - let block = rig.rand_block(); - let parent_root = block.parent_root(); - let peer_id = rig.new_connected_peer(); - - // Trigger the request - rig.trigger_unknown_parent_block(peer_id, block.into()); - for i in 1..=PARENT_FAIL_TOLERANCE { - let id = rig.expect_block_parent_request(parent_root); - // Blobs are only requested in the first iteration as this test only retries blocks - - if i % 2 == 0 { - // make sure every error is accounted for - // The request fails. It should be tried again. - rig.parent_lookup_failed_unavailable(id, peer_id); - } else { - // Send a bad block this time. It should be tried again. - let bad_block = rig.rand_block(); - rig.parent_lookup_block_response(id, peer_id, Some(bad_block.into())); - // Send the stream termination - - // Note, previously we would send the same lookup id with a stream terminator, - // we'd ignore it because we'd intrepret it as an unrequested response, since - // we already got one response for the block. I'm not sure what the intent is - // for having this stream terminator line in this test at all. Receiving an invalid - // block and a stream terminator with the same Id now results in two failed attempts, - // I'm unsure if this is how it should behave? - // - rig.parent_lookup_block_response(id, peer_id, None); - rig.expect_penalty(peer_id, "UnrequestedBlockRoot"); - } +#[tokio::test] +/// Assert that when creating multiple lookups their parent-child relation is discovered and we add +/// peers recursively from child to parent. +async fn lookups_form_chain() { + let depth = 5; + let mut r = TestRig::default(); + r.build_chain(depth).await; + for slot in (1..=depth).rev() { + r.trigger_with_block_at_slot(slot as u64); } + // TODO(tree-sync): Assert that there are `depth` disjoint chains + r.simulate(SimulateConfig::happy_path()).await; + r.assert_successful_lookup_sync(); - rig.expect_no_active_lookups_empty_network(); + // Assert that the peers are added to ancestor lookups, + // - The lookup with max slot has 1 peer + // - The lookup with min slot has all the peers + for slot in 1..=(depth as u64) { + let lookup = r.lookup_by_root(r.block_root_at_slot(slot)); + assert_eq!( + lookup.seen_peers.len(), + 1 + depth - slot as usize, + "Unexpected peer count for lookup at slot {slot}" + ); + } } -#[test] -fn test_parent_lookup_too_many_download_attempts_no_blacklist() { - let mut rig = TestRig::test_setup(); +#[tokio::test] +/// Assert that if a lookup chain (by appending ancestors) is too long we drop it +async fn test_parent_lookup_too_deep_grow_ancestor_one() { + let mut r = TestRig::default(); + r.build_chain(PARENT_DEPTH_TOLERANCE + 1).await; + r.trigger_with_last_block(); + r.simulate(SimulateConfig::happy_path()).await; - let (parent, block, parent_root, block_root) = rig.rand_block_and_parent(); - let peer_id = rig.new_connected_peer(); - - // Trigger the request - rig.trigger_unknown_parent_block(peer_id, block.into()); - for i in 1..=PARENT_FAIL_TOLERANCE { - rig.assert_not_ignored_chain(block_root); - let id = rig.expect_block_parent_request(parent_root); - if i % 2 != 0 { - // The request fails. It should be tried again. - rig.parent_lookup_failed_unavailable(id, peer_id); - } else { - // Send a bad block this time. It should be tried again. - let bad_block = rig.rand_block(); - rig.parent_lookup_block_response(id, peer_id, Some(bad_block.into())); - rig.expect_penalty(peer_id, "UnrequestedBlockRoot"); - } - } - - rig.assert_not_ignored_chain(block_root); - rig.assert_not_ignored_chain(parent.canonical_root()); - rig.expect_no_active_lookups_empty_network(); -} - -#[test] -fn test_parent_lookup_too_many_processing_attempts_must_blacklist() { - const PROCESSING_FAILURES: u8 = PARENT_FAIL_TOLERANCE / 2 + 1; - let mut rig = TestRig::test_setup(); - let (parent, block, parent_root, block_root) = rig.rand_block_and_parent(); - let peer_id = rig.new_connected_peer(); - - // Trigger the request - rig.trigger_unknown_parent_block(peer_id, block.into()); - - rig.log("Fail downloading the block"); - for _ in 0..(PARENT_FAIL_TOLERANCE - PROCESSING_FAILURES) { - let id = rig.expect_block_parent_request(parent_root); - // The request fails. It should be tried again. - rig.parent_lookup_failed_unavailable(id, peer_id); - } - - rig.log("Now fail processing a block in the parent request"); - for _ in 0..PROCESSING_FAILURES { - let id = rig.expect_block_parent_request(parent_root); - // Blobs are only requested in the previous first iteration as this test only retries blocks - rig.assert_not_ignored_chain(block_root); - // send the right parent but fail processing - rig.parent_lookup_block_response(id, peer_id, Some(parent.clone().into())); - rig.parent_block_processed(block_root, BlockError::BlockSlotLimitReached.into()); - rig.parent_lookup_block_response(id, peer_id, None); - rig.expect_penalty(peer_id, "lookup_block_processing_failure"); - } - - rig.assert_not_ignored_chain(block_root); - rig.expect_no_active_lookups_empty_network(); -} - -#[test] -fn test_parent_lookup_too_deep_grow_ancestor() { - let mut rig = TestRig::test_setup(); - let mut blocks = rig.rand_blockchain(PARENT_DEPTH_TOLERANCE); - - let peer_id = rig.new_connected_peer(); - let trigger_block = blocks.pop().unwrap(); - let chain_hash = trigger_block.canonical_root(); - rig.trigger_unknown_parent_block(peer_id, trigger_block); - - for block in blocks.into_iter().rev() { - let id = rig.expect_block_parent_request(block.canonical_root()); - // the block - rig.parent_lookup_block_response(id, peer_id, Some(block.clone())); - // the stream termination - rig.parent_lookup_block_response(id, peer_id, None); - // the processing request - rig.expect_block_process(ResponseType::Block); - // the processing result - rig.parent_block_processed( - chain_hash, - BlockProcessingResult::Err(BlockError::ParentUnknown { - parent_root: block.parent_root(), - }), - ) - } - - // Should create a new syncing chain - rig.drain_sync_rx(); - assert_eq!( - rig.active_range_sync_chain(), - ( - RangeSyncType::Head, - Slot::new(0), - Slot::new(PARENT_DEPTH_TOLERANCE as u64 - 1) - ) - ); + r.assert_head_slot(PARENT_DEPTH_TOLERANCE as u64 + 1); + r.assert_no_penalties(); // Should not penalize peer, but network is not clear because of the blocks_by_range requests - rig.expect_no_penalty_for(peer_id); - rig.assert_ignored_chain(chain_hash); + // r.assert_ignored_chain(chain_hash); + // + // Assert that chain is in failed chains + // Assert that there were 0 lookups completed, 33 dropped + // Assert that there were 1 range sync chains + // Bound resources: + // - Limit amount of requests + // - Limit the types of sync used + assert_eq!(r.completed_lookups(), 0, "no completed lookups"); + assert_eq!( + r.dropped_lookups(), + PARENT_DEPTH_TOLERANCE, + "All lookups dropped" + ); + r.assert_successful_range_sync(); +} + +#[tokio::test] +async fn test_parent_lookup_too_deep_grow_ancestor_zero() { + let mut r = TestRig::default(); + r.build_chain(PARENT_DEPTH_TOLERANCE).await; + r.trigger_with_last_block(); + r.simulate(SimulateConfig::happy_path()).await; + + r.assert_head_slot(PARENT_DEPTH_TOLERANCE as u64); + r.assert_no_penalties(); + assert_eq!( + r.completed_lookups(), + PARENT_DEPTH_TOLERANCE, + "completed all lookups" + ); + assert_eq!(r.dropped_lookups(), 0, "no dropped lookups"); } // Regression test for https://github.com/sigp/lighthouse/pull/7118 // 8042 UPDATE: block was previously added to the failed_chains cache, now it's inserted into the -// ignored chains cache. The regression test still applies as the chaild lookup is not created -#[test] -fn test_child_lookup_not_created_for_ignored_chain_parent_after_processing() { - // GIVEN: A parent chain longer than PARENT_DEPTH_TOLERANCE. - let mut rig = TestRig::test_setup(); - let mut blocks = rig.rand_blockchain(PARENT_DEPTH_TOLERANCE + 1); - let peer_id = rig.new_connected_peer(); - - // The child of the trigger block to be used to extend the chain. - let trigger_block_child = blocks.pop().unwrap(); - // The trigger block that starts the lookup. - let trigger_block = blocks.pop().unwrap(); - let tip_root = trigger_block.canonical_root(); - - // Trigger the initial unknown parent block for the tip. - rig.trigger_unknown_parent_block(peer_id, trigger_block.clone()); - - // Simulate the lookup chain building up via `ParentUnknown` errors. - for block in blocks.into_iter().rev() { - let id = rig.expect_block_parent_request(block.canonical_root()); - rig.parent_lookup_block_response(id, peer_id, Some(block.clone())); - rig.parent_lookup_block_response(id, peer_id, None); - rig.expect_block_process(ResponseType::Block); - rig.parent_block_processed( - tip_root, - BlockProcessingResult::Err(BlockError::ParentUnknown { - parent_root: block.parent_root(), - }), - ); - } +// ignored chains cache. The regression test still applies as the child lookup is not created +#[tokio::test] +async fn test_child_lookup_not_created_for_ignored_chain_parent_after_processing() { + let mut r = TestRig::default(); + let depth = PARENT_DEPTH_TOLERANCE + 1; + r.build_chain(depth + 1).await; + r.trigger_with_block_at_slot(depth as u64); + r.simulate(SimulateConfig::new().no_range_sync()).await; // At this point, the chain should have been deemed too deep and pruned. // The tip root should have been inserted into ignored chains. - rig.assert_ignored_chain(tip_root); - rig.expect_no_penalty_for(peer_id); + // Ensure no blocks have been synced + r.assert_head_slot(0); + r.assert_no_active_lookups(); + r.assert_no_penalties(); + r.assert_ignored_chain(r.block_at_slot(depth as u64).canonical_root()); // WHEN: Trigger the extending block that points to the tip. - let trigger_block_child_root = trigger_block_child.canonical_root(); - rig.trigger_unknown_block_from_attestation(trigger_block_child_root, peer_id); - let id = rig.expect_block_lookup_request(trigger_block_child_root); - rig.single_lookup_block_response(id, peer_id, Some(trigger_block_child.clone())); - rig.single_lookup_block_response(id, peer_id, None); - rig.expect_block_process(ResponseType::Block); - rig.single_block_component_processed( - id.lookup_id, - BlockProcessingResult::Err(BlockError::ParentUnknown { - parent_root: tip_root, - }), - ); - + let peer = r.new_connected_peer(); + r.trigger_unknown_parent_block(peer, r.block_at_slot(depth as u64 + 1)); // THEN: The extending block should not create a lookup because the tip was inserted into // ignored chains. - rig.expect_no_active_lookups(); - rig.expect_no_penalty_for(peer_id); - rig.expect_empty_network(); + r.assert_no_active_lookups(); + r.assert_no_penalties(); + r.assert_empty_network(); } -#[test] -fn test_parent_lookup_too_deep_grow_tip() { - let mut rig = TestRig::test_setup(); - let blocks = rig.rand_blockchain(PARENT_DEPTH_TOLERANCE - 1); - let peer_id = rig.new_connected_peer(); - let tip = blocks.last().unwrap().clone(); - - for block in blocks.into_iter() { - let block_root = block.canonical_root(); - rig.trigger_unknown_block_from_attestation(block_root, peer_id); - let id = rig.expect_block_parent_request(block_root); - rig.single_lookup_block_response(id, peer_id, Some(block.clone())); - rig.single_lookup_block_response(id, peer_id, None); - rig.expect_block_process(ResponseType::Block); - rig.single_block_component_processed( - id.lookup_id, - BlockError::ParentUnknown { - parent_root: block.parent_root(), - } - .into(), - ); +#[tokio::test] +/// Assert that if a lookup chain (by appending tips) is too long we drop it +async fn test_parent_lookup_too_deep_grow_tip() { + let depth = PARENT_DEPTH_TOLERANCE + 1; + let mut r = TestRig::default(); + r.build_chain(depth).await; + for slot in (1..=depth).rev() { + r.trigger_with_block_at_slot(slot as u64); } + r.simulate(SimulateConfig::happy_path()).await; - // Should create a new syncing chain - rig.drain_sync_rx(); + // Even if the chain is longer than `PARENT_DEPTH_TOLERANCE` because the lookups are created all + // at once they chain by sections and it's possible that the oldest ancestors start processing + // before the full chain is connected. + assert!(r.created_lookups() > 0, "no created lookups"); assert_eq!( - rig.active_range_sync_chain(), - ( - RangeSyncType::Head, - Slot::new(0), - Slot::new(PARENT_DEPTH_TOLERANCE as u64 - 2) - ) + r.completed_lookups(), + r.created_lookups(), + "not all completed lookups" ); + assert_eq!(r.dropped_lookups(), 0, "some dropped lookups"); + r.assert_successful_lookup_sync(); // Should not penalize peer, but network is not clear because of the blocks_by_range requests - rig.expect_no_penalty_for(peer_id); - rig.assert_ignored_chain(tip.canonical_root()); + r.assert_no_penalties(); } -#[test] -fn test_lookup_peer_disconnected_no_peers_left_while_request() { - let mut rig = TestRig::test_setup(); - let peer_id = rig.new_connected_peer(); - let trigger_block = rig.rand_block(); - rig.trigger_unknown_parent_block(peer_id, trigger_block.into()); - rig.peer_disconnected(peer_id); - rig.rpc_error_all_active_requests(peer_id); - // Erroring all rpc requests and disconnecting the peer shouldn't remove the requests - // from the lookups map as they can still progress. - rig.assert_single_lookups_count(2); -} - -#[test] -fn test_lookup_disconnection_peer_left() { - let mut rig = TestRig::test_setup(); - let peer_ids = (0..2).map(|_| rig.new_connected_peer()).collect::<Vec<_>>(); - let disconnecting_peer = *peer_ids.first().unwrap(); - let block_root = Hash256::random(); - // lookup should have two peers associated with the same block - for peer_id in peer_ids.iter() { - rig.trigger_unknown_block_from_attestation(block_root, *peer_id); - } - // Disconnect the first peer only, which is the one handling the request - rig.peer_disconnected(disconnecting_peer); - rig.rpc_error_all_active_requests(disconnecting_peer); - rig.assert_single_lookups_count(1); -} - -#[test] -fn test_lookup_add_peers_to_parent() { - let mut r = TestRig::test_setup(); - let peer_id_1 = r.new_connected_peer(); - let peer_id_2 = r.new_connected_peer(); - let blocks = r.rand_blockchain(5); - let last_block_root = blocks.last().unwrap().canonical_root(); - // Create a chain of lookups - for block in &blocks { - r.trigger_unknown_parent_block(peer_id_1, block.clone()); - } - r.trigger_unknown_block_from_attestation(last_block_root, peer_id_2); - for block in blocks.iter().take(blocks.len() - 1) { - // Parent has the original unknown parent event peer + new peer - r.assert_lookup_peers(block.canonical_root(), vec![peer_id_1, peer_id_2]); - } - // Child lookup only has the unknown attestation peer - r.assert_lookup_peers(last_block_root, vec![peer_id_2]); -} - -#[test] -fn test_skip_creating_ignored_parent_lookup() { - let mut rig = TestRig::test_setup(); - let (_, block, parent_root, _) = rig.rand_block_and_parent(); - let peer_id = rig.new_connected_peer(); - rig.insert_ignored_chain(parent_root); - rig.trigger_unknown_parent_block(peer_id, block.into()); - rig.expect_no_penalty_for(peer_id); +#[tokio::test] +async fn test_skip_creating_ignored_parent_lookup() { + let mut r = TestRig::default(); + r.build_chain(2).await; + r.insert_ignored_chain(r.block_root_at_slot(1)); + r.trigger_with_last_block(); + r.simulate(SimulateConfig::happy_path()).await; + r.assert_no_penalties(); // Both current and parent lookup should not be created - rig.expect_no_active_lookups(); + r.assert_no_active_lookups(); } -#[test] -fn test_single_block_lookup_ignored_response() { - let mut rig = TestRig::test_setup(); +#[tokio::test] +/// Assert that if the oldest block in a chain is already imported (DuplicateFullyImported), +/// the remaining blocks in the chain are still processed successfully. This tests a race +/// condition where a block gets imported elsewhere while the lookup is processing. +/// +/// The processing sequence is: +/// - Block 3: UnknownParent (needs block 2) +/// - Block 2: UnknownParent (needs block 1) +/// - Block 1: About to be processed, but gets imported via gossip (race condition) +/// - Block 1: DuplicateFullyImported (already in chain from race) +/// - Block 2: Import ok (parent block 1 is available) +/// - Block 3: Import ok (parent block 2 is available) +async fn test_same_chain_race_condition() { + let mut r = TestRig::default(); + r.build_chain(3).await; - let block = rig.rand_block(); - let peer_id = rig.new_connected_peer(); + let block_1_root = r.block_root_at_slot(1); - // Trigger the request - rig.trigger_unknown_block_from_attestation(block.canonical_root(), peer_id); - let id = rig.expect_block_lookup_request(block.canonical_root()); + // Trigger a lookup with block 3. This creates a parent lookup chain that will + // request blocks 3 → 2 → 1. + r.trigger_with_block_at_slot(3); - // The peer provides the correct block, should not be penalized. Now the block should be sent - // for processing. - rig.single_lookup_block_response(id, peer_id, Some(block.into())); - rig.expect_empty_network(); - rig.expect_block_process(ResponseType::Block); + // Configure simulate to import block 1 right before it's processed by the lookup. + // This simulates the race condition where block 1 arrives via gossip at the same + // time the lookup is trying to process it. + r.simulate(SimulateConfig::new().with_import_block_before_process(block_1_root)) + .await; - // The request should still be active. - assert_eq!(rig.active_single_lookups_count(), 1); - - // Send the stream termination. Peer should have not been penalized, and the request removed - // after processing. - rig.single_lookup_block_response(id, peer_id, None); - // Send an Ignored response, the request should be dropped - rig.single_block_component_processed(id.lookup_id, BlockProcessingResult::Ignored); - rig.expect_no_active_lookups_empty_network(); + // The chain should complete successfully with head at slot 3, proving that + // the lookup correctly handled the DuplicateFullyImported for block 1 and + // continued processing blocks 2 and 3. + r.assert_head_slot(3); + r.assert_successful_lookup_sync(); } -#[test] -fn test_parent_lookup_ignored_response() { - let mut rig = TestRig::test_setup(); - - let (parent, block, parent_root, block_root) = rig.rand_block_and_parent(); - let peer_id = rig.new_connected_peer(); - - // Trigger the request - rig.trigger_unknown_parent_block(peer_id, block.clone().into()); - let id = rig.expect_block_parent_request(parent_root); - // Note: single block lookup for current `block` does not trigger any request because it does - // not have blobs, and the block is already cached - - // Peer sends the right block, it should be sent for processing. Peer should not be penalized. - rig.parent_lookup_block_response(id, peer_id, Some(parent.into())); - rig.expect_block_process(ResponseType::Block); - rig.expect_empty_network(); - - // Return an Ignored result. The request should be dropped - rig.parent_block_processed(block_root, BlockProcessingResult::Ignored); - rig.expect_empty_network(); - rig.expect_no_active_lookups(); -} - -/// This is a regression test. -#[test] -fn test_same_chain_race_condition() { - let mut rig = TestRig::test_setup(); - - // if we use one or two blocks it will match on the hash or the parent hash, so make a longer - // chain. - let depth = 4; - let mut blocks = rig.rand_blockchain(depth); - let peer_id = rig.new_connected_peer(); - let trigger_block = blocks.pop().unwrap(); - let chain_hash = trigger_block.canonical_root(); - rig.trigger_unknown_parent_block(peer_id, trigger_block.clone()); - - for (i, block) in blocks.clone().into_iter().rev().enumerate() { - let id = rig.expect_block_parent_request(block.canonical_root()); - // the block - rig.parent_lookup_block_response(id, peer_id, Some(block.clone())); - // the stream termination - rig.parent_lookup_block_response(id, peer_id, None); - // the processing request - rig.expect_block_process(ResponseType::Block); - // the processing result - if i + 2 == depth { - rig.log(&format!("Block {i} was removed and is already known")); - rig.parent_block_processed( - chain_hash, - BlockError::DuplicateFullyImported(block.canonical_root()).into(), - ) - } else { - rig.log(&format!("Block {i} ParentUnknown")); - rig.parent_block_processed( - chain_hash, - BlockProcessingResult::Err(BlockError::ParentUnknown { - parent_root: block.parent_root(), - }), - ) - } - } - - // Try to get this block again while the chain is being processed. We should not request it again. - let peer_id = rig.new_connected_peer(); - rig.trigger_unknown_parent_block(peer_id, trigger_block.clone()); - rig.expect_empty_network(); - - // Add a peer to the tip child lookup which has zero peers - rig.trigger_unknown_block_from_attestation(trigger_block.canonical_root(), peer_id); - - rig.log("Processing succeeds, now the rest of the chain should be sent for processing."); - for block in blocks.iter().skip(1).chain(&[trigger_block]) { - rig.expect_parent_chain_process(); - rig.single_block_component_processed_imported(block.canonical_root()); - } - rig.expect_no_active_lookups_empty_network(); -} - -#[test] -fn block_in_da_checker_skips_download() { - let Some(mut r) = TestRig::test_setup_after_deneb_before_fulu() else { +#[tokio::test] +/// Assert that if the lookup's block is in the da_checker we don't download it again +async fn block_in_da_checker_skips_download() { + // Only in Deneb, as the block needs blobs to remain in the da_checker + let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { return; }; - let (block, blobs) = r.rand_block_and_blobs(NumBlobs::Number(1)); - let block_root = block.canonical_root(); - let peer_id = r.new_connected_peer(); - r.insert_block_to_da_checker(block.into()); - r.trigger_unknown_block_from_attestation(block_root, peer_id); - // Should not trigger block request - let id = r.expect_blob_lookup_request(block_root); - r.expect_empty_network(); - // Resolve blob and expect lookup completed - r.complete_single_lookup_blob_lookup_valid(id, peer_id, blobs, true); - r.expect_no_active_lookups(); + // Add block to da_checker + // Complete test with happy path + // Assert that there were no requests for blocks + r.build_chain(1).await; + r.insert_block_to_da_chain_and_assert_missing_componens(r.block_at_slot(1)) + .await; + r.trigger_with_block_at_slot(1); + r.simulate(SimulateConfig::happy_path()).await; + r.assert_successful_lookup_sync(); + assert_eq!( + r.requests + .iter() + .filter(|(request, _)| matches!(request, RequestType::BlocksByRoot(_))) + .collect::<Vec<_>>(), + Vec::<&(RequestType<E>, AppRequestId)>::new(), + "There should be no block requests" + ); } -#[test] -fn block_in_processing_cache_becomes_invalid() { - let Some(mut r) = TestRig::test_setup_after_deneb_before_fulu() else { +#[tokio::test] +async fn block_in_processing_cache_becomes_invalid() { + let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { return; }; - let (block, blobs) = r.rand_block_and_blobs(NumBlobs::Number(1)); - let block_root = block.canonical_root(); - let peer_id = r.new_connected_peer(); - r.insert_block_to_availability_cache(block.clone().into()); - r.trigger_unknown_block_from_attestation(block_root, peer_id); - // Should trigger blob request - let id = r.expect_blob_lookup_request(block_root); - // Should not trigger block request - r.expect_empty_network(); + r.build_chain(1).await; + let block = r.block_at_slot(1); + r.insert_block_to_da_checker_as_pre_execution(block.clone()); + r.trigger_with_last_block(); + r.simulate(SimulateConfig::happy_path()).await; + r.assert_pending_lookup_sync(); + // Here the only active lookup is waiting for the block to finish processing + // Simulate invalid block, removing it from processing cache - r.simulate_block_gossip_processing_becomes_invalid(block_root); + r.simulate_block_gossip_processing_becomes_invalid(block.canonical_root()); // Should download block, then issue blobs request - r.complete_lookup_block_download(block); - // Should not trigger block or blob request - r.expect_empty_network(); - r.complete_lookup_block_import_valid(block_root, false); - // Resolve blob and expect lookup completed - r.complete_single_lookup_blob_lookup_valid(id, peer_id, blobs, true); - r.expect_no_active_lookups(); + r.simulate(SimulateConfig::happy_path()).await; + r.assert_successful_lookup_sync(); } -#[test] -fn block_in_processing_cache_becomes_valid_imported() { - let Some(mut r) = TestRig::test_setup_after_deneb_before_fulu() else { +#[tokio::test] +async fn block_in_processing_cache_becomes_valid_imported() { + let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { return; }; - let (block, blobs) = r.rand_block_and_blobs(NumBlobs::Number(1)); - let block_root = block.canonical_root(); - let peer_id = r.new_connected_peer(); - r.insert_block_to_availability_cache(block.clone().into()); - r.trigger_unknown_block_from_attestation(block_root, peer_id); - // Should trigger blob request - let id = r.expect_blob_lookup_request(block_root); - // Should not trigger block request - r.expect_empty_network(); + r.build_chain(1).await; + let block = r.block_at_slot(1); + r.insert_block_to_da_checker_as_pre_execution(block.clone()); + r.trigger_with_last_block(); + r.simulate(SimulateConfig::happy_path()).await; + r.assert_pending_lookup_sync(); + // Here the only active lookup is waiting for the block to finish processing + // Resolve the block from processing step - r.simulate_block_gossip_processing_becomes_valid_missing_components(block.into()); + r.simulate_block_gossip_processing_becomes_valid(block) + .await; // Should not trigger block or blob request - r.expect_empty_network(); + r.assert_empty_network(); // Resolve blob and expect lookup completed - r.complete_single_lookup_blob_lookup_valid(id, peer_id, blobs, true); - r.expect_no_active_lookups(); + r.assert_no_active_lookups(); } // IGNORE: wait for change that delays blob fetching to knowing the block -#[ignore] -#[test] -fn blobs_in_da_checker_skip_download() { - let Some(mut r) = TestRig::test_setup_after_deneb_before_fulu() else { +#[tokio::test] +async fn blobs_in_da_checker_skip_download() { + let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { return; }; - let (block, blobs) = r.rand_block_and_blobs(NumBlobs::Number(1)); - let block_root = block.canonical_root(); - let peer_id = r.new_connected_peer(); - for blob in blobs { - r.insert_blob_to_da_checker(blob); + r.build_chain(1).await; + let block = r.get_last_block().clone(); + let blobs = block + .block_data() + .and_then(|d| d.blobs()) + .expect("block with no blobs"); + for blob in &blobs { + r.insert_blob_to_da_checker(blob.clone()); } - r.trigger_unknown_block_from_attestation(block_root, peer_id); - // Should download and process the block - r.complete_single_lookup_block_valid(block, true); - // Should not trigger blob request - r.expect_empty_network(); - r.expect_no_active_lookups(); + r.trigger_with_last_block(); + r.simulate(SimulateConfig::happy_path()).await; + + r.assert_successful_lookup_sync(); + assert_eq!( + r.requests + .iter() + .filter(|(request, _)| matches!(request, RequestType::BlobsByRoot(_))) + .collect::<Vec<_>>(), + Vec::<&(RequestType<E>, AppRequestId)>::new(), + "There should be no blob requests" + ); } -#[test] -fn custody_lookup_happy_path() { - let Some(mut r) = TestRig::test_setup_after_fulu() else { +macro_rules! fulu_peer_matrix_tests { + ( + [$($name:ident => $variant:expr),+ $(,)?] + ) => { + paste::paste! { + $( + #[tokio::test] + async fn [<custody_lookup_happy_path _ $name>]() { + custody_lookup_happy_path($variant).await; + } + + #[tokio::test] + async fn [<custody_lookup_some_custody_failures _ $name>]() { + custody_lookup_some_custody_failures($variant).await; + } + + #[tokio::test] + async fn [<custody_lookup_permanent_custody_failures _ $name>]() { + custody_lookup_permanent_custody_failures($variant).await; + } + )+ + } + }; +} + +fulu_peer_matrix_tests!( + [ + we_supernode_them_supernode => FuluTestType::WeSupernodeThemSupernode, + we_supernode_them_fullnodes => FuluTestType::WeSupernodeThemFullnodes, + we_fullnode_them_supernode => FuluTestType::WeFullnodeThemSupernode, + we_fullnode_them_fullnodes => FuluTestType::WeFullnodeThemFullnodes, + ] +); + +async fn custody_lookup_happy_path(test_type: FuluTestType) { + let Some(mut r) = TestRig::new_fulu_peer_test(test_type) else { return; }; - let spec = E::default_spec(); + r.build_chain(1).await; r.new_connected_peers_for_peerdas(); - let (block, data_columns) = r.rand_block_and_data_columns(); - let block_root = block.canonical_root(); - let peer_id = r.new_connected_peer(); - r.trigger_unknown_block_from_attestation(block_root, peer_id); - // Should not request blobs - let id = r.expect_block_lookup_request(block.canonical_root()); - r.complete_valid_block_request(id, block.into(), true); - // for each slot we download `samples_per_slot` columns - let sample_column_count = spec.samples_per_slot * spec.data_columns_per_group::<E>(); - let custody_ids = - r.expect_only_data_columns_by_root_requests(block_root, sample_column_count as usize); - r.complete_valid_custody_request(custody_ids, data_columns, false); - r.expect_no_active_lookups(); + r.trigger_with_last_block(); + r.simulate(SimulateConfig::happy_path()).await; + r.assert_no_penalties(); + r.assert_successful_lookup_sync(); } +async fn custody_lookup_some_custody_failures(test_type: FuluTestType) { + let Some(mut r) = TestRig::new_fulu_peer_test(test_type) else { + return; + }; + let block_root = r.build_chain(1).await; + // Send the same trigger from all peers, so that the lookup has all peers + for peer in r.new_connected_peers_for_peerdas() { + r.trigger_unknown_block_from_attestation(block_root, peer); + } + let custody_columns = r.custody_columns(); + r.simulate(SimulateConfig::new().return_no_columns_on_indices(&custody_columns[..4], 3)) + .await; + r.assert_penalties_of_type("NotEnoughResponsesReturned"); + r.assert_successful_lookup_sync(); +} + +async fn custody_lookup_permanent_custody_failures(test_type: FuluTestType) { + let Some(mut r) = TestRig::new_fulu_peer_test(test_type) else { + return; + }; + let block_root = r.build_chain(1).await; + + // Send the same trigger from all peers, so that the lookup has all peers + for peer in r.new_connected_peers_for_peerdas() { + r.trigger_unknown_block_from_attestation(block_root, peer); + } + + let custody_columns = r.custody_columns(); + r.simulate( + SimulateConfig::new().return_no_columns_on_indices(&custody_columns[..2], usize::MAX), + ) + .await; + // Every peer that does not return a column is part of the lookup because it claimed to have + // imported the lookup, so we will penalize. + r.assert_penalties_of_type("NotEnoughResponsesReturned"); + r.assert_failed_lookup_sync(); +} + +// We supernode, diverse peers +// We not supernode, diverse peers + // TODO(das): Test retries of DataColumnByRoot: // - Expect request for column_index // - Respond with bad data // - Respond with stream terminator // ^ The stream terminator should be ignored and not close the next retry -mod deneb_only { - use super::*; - use beacon_chain::{ - block_verification_types::{AsBlock, RpcBlock}, - data_availability_checker::AvailabilityCheckError, - }; - use std::collections::VecDeque; - - struct DenebTester { - rig: TestRig, - block: Arc<SignedBeaconBlock<E>>, - blobs: Vec<Arc<BlobSidecar<E>>>, - parent_block_roots: Vec<Hash256>, - parent_block: VecDeque<Arc<SignedBeaconBlock<E>>>, - parent_blobs: VecDeque<Vec<Arc<BlobSidecar<E>>>>, - unknown_parent_block: Option<Arc<SignedBeaconBlock<E>>>, - unknown_parent_blobs: Option<Vec<Arc<BlobSidecar<E>>>>, - peer_id: PeerId, - block_req_id: Option<SingleLookupReqId>, - parent_block_req_id: Option<SingleLookupReqId>, - blob_req_id: Option<SingleLookupReqId>, - parent_blob_req_id: Option<SingleLookupReqId>, - slot: Slot, - block_root: Hash256, - } - - enum RequestTrigger { - AttestationUnknownBlock, - GossipUnknownParentBlock(usize), - GossipUnknownParentBlob(usize), - } - - impl RequestTrigger { - fn num_parents(&self) -> usize { - match self { - RequestTrigger::AttestationUnknownBlock => 0, - RequestTrigger::GossipUnknownParentBlock(num_parents) => *num_parents, - RequestTrigger::GossipUnknownParentBlob(num_parents) => *num_parents, - } - } - } - - impl DenebTester { - fn new(request_trigger: RequestTrigger) -> Option<Self> { - let Some(mut rig) = TestRig::test_setup_after_deneb_before_fulu() else { - return None; - }; - let (block, blobs) = rig.rand_block_and_blobs(NumBlobs::Random); - let mut block = Arc::new(block); - let mut blobs = blobs.into_iter().map(Arc::new).collect::<Vec<_>>(); - let slot = block.slot(); - - let num_parents = request_trigger.num_parents(); - let mut parent_block_chain = VecDeque::with_capacity(num_parents); - let mut parent_blobs_chain = VecDeque::with_capacity(num_parents); - let mut parent_block_roots = vec![]; - for _ in 0..num_parents { - // Set the current block as the parent. - let parent_root = block.canonical_root(); - let parent_block = block.clone(); - let parent_blobs = blobs.clone(); - parent_block_chain.push_front(parent_block); - parent_blobs_chain.push_front(parent_blobs); - parent_block_roots.push(parent_root); - - // Create the next block. - let (child_block, child_blobs) = - rig.block_with_parent_and_blobs(parent_root, NumBlobs::Random); - let mut child_block = Arc::new(child_block); - let mut child_blobs = child_blobs.into_iter().map(Arc::new).collect::<Vec<_>>(); - - // Update the new block to the current block. - std::mem::swap(&mut child_block, &mut block); - std::mem::swap(&mut child_blobs, &mut blobs); - } - let block_root = block.canonical_root(); - - let peer_id = rig.new_connected_peer(); - - // Trigger the request - let (block_req_id, blob_req_id, parent_block_req_id, parent_blob_req_id) = - match request_trigger { - RequestTrigger::AttestationUnknownBlock => { - rig.send_sync_message(SyncMessage::UnknownBlockHashFromAttestation( - peer_id, block_root, - )); - let block_req_id = rig.expect_block_lookup_request(block_root); - (Some(block_req_id), None, None, None) - } - RequestTrigger::GossipUnknownParentBlock { .. } => { - rig.send_sync_message(SyncMessage::UnknownParentBlock( - peer_id, - block.clone(), - block_root, - )); - - let parent_root = block.parent_root(); - let parent_block_req_id = rig.expect_block_parent_request(parent_root); - rig.expect_empty_network(); // expect no more requests - (None, None, Some(parent_block_req_id), None) - } - RequestTrigger::GossipUnknownParentBlob { .. } => { - let single_blob = blobs.first().cloned().unwrap(); - let parent_root = single_blob.block_parent_root(); - rig.send_sync_message(SyncMessage::UnknownParentBlob(peer_id, single_blob)); - - let parent_block_req_id = rig.expect_block_parent_request(parent_root); - rig.expect_empty_network(); // expect no more requests - (None, None, Some(parent_block_req_id), None) - } - }; - - Some(Self { - rig, - block, - blobs, - parent_block: parent_block_chain, - parent_blobs: parent_blobs_chain, - parent_block_roots, - unknown_parent_block: None, - unknown_parent_blobs: None, - peer_id, - block_req_id, - parent_block_req_id, - blob_req_id, - parent_blob_req_id, - slot, - block_root, - }) - } - - fn trigger_unknown_block_from_attestation(mut self) -> Self { - let block_root = self.block.canonical_root(); - self.rig - .trigger_unknown_block_from_attestation(block_root, self.peer_id); - self - } - - fn parent_block_response(mut self) -> Self { - self.rig.expect_empty_network(); - let block = self.parent_block.pop_front().unwrap().clone(); - let _ = self.unknown_parent_block.insert(block.clone()); - self.rig.parent_lookup_block_response( - self.parent_block_req_id.expect("parent request id"), - self.peer_id, - Some(block), - ); - - self.rig.assert_parent_lookups_count(1); - self - } - - fn parent_block_response_expect_blobs(mut self) -> Self { - self.rig.expect_empty_network(); - let block = self.parent_block.pop_front().unwrap().clone(); - let _ = self.unknown_parent_block.insert(block.clone()); - self.rig.parent_lookup_block_response( - self.parent_block_req_id.expect("parent request id"), - self.peer_id, - Some(block), - ); - - // Expect blobs request after sending block - let s = self.expect_parent_blobs_request(); - - s.rig.assert_parent_lookups_count(1); - s - } - - fn parent_blob_response(mut self) -> Self { - let blobs = self.parent_blobs.pop_front().unwrap(); - let _ = self.unknown_parent_blobs.insert(blobs.clone()); - for blob in &blobs { - self.rig.parent_lookup_blob_response( - self.parent_blob_req_id.expect("parent blob request id"), - self.peer_id, - Some(blob.clone()), - ); - assert_eq!(self.rig.active_parent_lookups_count(), 1); - } - self.rig.parent_lookup_blob_response( - self.parent_blob_req_id.expect("parent blob request id"), - self.peer_id, - None, - ); - - self - } - - fn block_response_triggering_process(self) -> Self { - let mut me = self.block_response_and_expect_blob_request(); - me.rig.expect_block_process(ResponseType::Block); - - // The request should still be active. - assert_eq!(me.rig.active_single_lookups_count(), 1); - me - } - - fn block_response_and_expect_blob_request(mut self) -> Self { - // The peer provides the correct block, should not be penalized. Now the block should be sent - // for processing. - self.rig.single_lookup_block_response( - self.block_req_id.expect("block request id"), - self.peer_id, - Some(self.block.clone()), - ); - // After responding with block the node will issue a blob request - let mut s = self.expect_blobs_request(); - - s.rig.expect_empty_network(); - - // The request should still be active. - s.rig.assert_lookup_is_active(s.block.canonical_root()); - s - } - - fn blobs_response(mut self) -> Self { - self.rig - .log(&format!("blobs response {}", self.blobs.len())); - for blob in &self.blobs { - self.rig.single_lookup_blob_response( - self.blob_req_id.expect("blob request id"), - self.peer_id, - Some(blob.clone()), - ); - self.rig - .assert_lookup_is_active(self.block.canonical_root()); - } - self.rig.single_lookup_blob_response( - self.blob_req_id.expect("blob request id"), - self.peer_id, - None, - ); - self - } - - fn blobs_response_was_valid(mut self) -> Self { - self.rig.expect_empty_network(); - if !self.blobs.is_empty() { - self.rig.expect_block_process(ResponseType::Blob); - } - self - } - - fn expect_empty_beacon_processor(mut self) -> Self { - self.rig.expect_empty_beacon_processor(); - self - } - - fn empty_block_response(mut self) -> Self { - self.rig.single_lookup_block_response( - self.block_req_id.expect("block request id"), - self.peer_id, - None, - ); - self - } - - fn empty_blobs_response(mut self) -> Self { - self.rig.single_lookup_blob_response( - self.blob_req_id.expect("blob request id"), - self.peer_id, - None, - ); - self - } - - fn empty_parent_blobs_response(mut self) -> Self { - self.rig.parent_lookup_blob_response( - self.parent_blob_req_id.expect("blob request id"), - self.peer_id, - None, - ); - self - } - - fn block_missing_components(mut self) -> Self { - self.rig.single_block_component_processed( - self.block_req_id.expect("block request id").lookup_id, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents( - self.block.slot(), - self.block_root, - )), - ); - self.rig.expect_empty_network(); - self.rig.assert_single_lookups_count(1); - self - } - - fn blob_imported(mut self) -> Self { - self.rig.single_blob_component_processed( - self.blob_req_id.expect("blob request id").lookup_id, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(self.block_root)), - ); - self.rig.expect_empty_network(); - self.rig.assert_single_lookups_count(0); - self - } - - fn block_imported(mut self) -> Self { - // Missing blobs should be the request is not removed, the outstanding blobs request should - // mean we do not send a new request. - self.rig.single_block_component_processed( - self.block_req_id - .or(self.blob_req_id) - .expect("block request id") - .lookup_id, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(self.block_root)), - ); - self.rig.expect_empty_network(); - self.rig.assert_single_lookups_count(0); - self - } - - fn parent_block_imported(mut self) -> Self { - let parent_root = *self.parent_block_roots.first().unwrap(); - self.rig - .log(&format!("parent_block_imported {parent_root:?}")); - self.rig.parent_block_processed( - self.block_root, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(parent_root)), - ); - self.rig.expect_no_requests_for(parent_root); - self.rig.assert_parent_lookups_count(0); - self - } - - fn parent_block_missing_components(mut self) -> Self { - let parent_root = *self.parent_block_roots.first().unwrap(); - self.rig - .log(&format!("parent_block_missing_components {parent_root:?}")); - self.rig.parent_block_processed( - self.block_root, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents( - Slot::new(0), - parent_root, - )), - ); - self.rig.expect_no_requests_for(parent_root); - self - } - - fn parent_blob_imported(mut self) -> Self { - let parent_root = *self.parent_block_roots.first().unwrap(); - self.rig - .log(&format!("parent_blob_imported {parent_root:?}")); - self.rig.parent_blob_processed( - self.block_root, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(parent_root)), - ); - - self.rig.expect_no_requests_for(parent_root); - self.rig.assert_parent_lookups_count(0); - self - } - - fn parent_block_unknown_parent(mut self) -> Self { - self.rig.log("parent_block_unknown_parent"); - let block = self.unknown_parent_block.take().unwrap(); - // Now this block is the one we expect requests from - self.block = block.clone(); - let block = RpcBlock::new( - block, - None, - self.rig.harness.chain.data_availability_checker.v1(), - self.rig.harness.chain.spec.clone(), - ) - .unwrap(); - self.rig.parent_block_processed( - self.block_root, - BlockProcessingResult::Err(BlockError::ParentUnknown { - parent_root: block.parent_root(), - }), - ); - assert_eq!(self.rig.active_parent_lookups_count(), 1); - self - } - - fn invalid_parent_processed(mut self) -> Self { - self.rig.parent_block_processed( - self.block_root, - BlockProcessingResult::Err(BlockError::BlockSlotLimitReached), - ); - assert_eq!(self.rig.active_parent_lookups_count(), 1); - self - } - - fn invalid_block_processed(mut self) -> Self { - self.rig.single_block_component_processed( - self.block_req_id.expect("block request id").lookup_id, - BlockProcessingResult::Err(BlockError::BlockSlotLimitReached), - ); - self.rig.assert_single_lookups_count(1); - self - } - - fn invalid_blob_processed(mut self) -> Self { - self.rig.log("invalid_blob_processed"); - self.rig.single_blob_component_processed( - self.blob_req_id.expect("blob request id").lookup_id, - BlockProcessingResult::Err(BlockError::AvailabilityCheck( - AvailabilityCheckError::InvalidBlobs(kzg::Error::KzgVerificationFailed), - )), - ); - self.rig.assert_single_lookups_count(1); - self - } - - fn missing_components_from_block_request(mut self) -> Self { - self.rig.single_block_component_processed( - self.block_req_id.expect("block request id").lookup_id, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents( - self.slot, - self.block_root, - )), - ); - // Add block to da_checker so blobs request can continue - self.rig.insert_block_to_da_checker(self.block.clone()); - - self.rig.assert_single_lookups_count(1); - self - } - - fn complete_current_block_and_blobs_lookup(self) -> Self { - self.expect_block_request() - .block_response_and_expect_blob_request() - .blobs_response() - // TODO: Should send blobs for processing - .expect_block_process() - .block_imported() - } - - fn log(self, msg: &str) -> Self { - self.rig.log(msg); - self - } - - fn parent_block_then_empty_parent_blobs(self) -> Self { - self.log( - " Return empty blobs for parent, block errors with missing components, downscore", - ) - .parent_block_response() - .expect_parent_blobs_request() - .empty_parent_blobs_response() - .expect_penalty("NotEnoughResponsesReturned") - .log("Re-request parent blobs, succeed and import parent") - .expect_parent_blobs_request() - .parent_blob_response() - .expect_block_process() - .parent_block_missing_components() - // Insert new peer into child request before completing parent - .trigger_unknown_block_from_attestation() - .parent_blob_imported() - } - - fn expect_penalty(mut self, expect_penalty_msg: &'static str) -> Self { - self.rig.expect_penalty(self.peer_id, expect_penalty_msg); - self - } - fn expect_no_penalty(mut self) -> Self { - self.rig.expect_empty_network(); - self - } - fn expect_no_penalty_and_no_requests(mut self) -> Self { - self.rig.expect_empty_network(); - self - } - fn expect_block_request(mut self) -> Self { - let id = self - .rig - .expect_block_lookup_request(self.block.canonical_root()); - self.block_req_id = Some(id); - self - } - fn expect_blobs_request(mut self) -> Self { - let id = self - .rig - .expect_blob_lookup_request(self.block.canonical_root()); - self.blob_req_id = Some(id); - self - } - fn expect_parent_block_request(mut self) -> Self { - let id = self - .rig - .expect_block_parent_request(self.block.parent_root()); - self.parent_block_req_id = Some(id); - self - } - fn expect_parent_blobs_request(mut self) -> Self { - let id = self - .rig - .expect_blob_parent_request(self.block.parent_root()); - self.parent_blob_req_id = Some(id); - self - } - fn expect_no_blobs_request(mut self) -> Self { - self.rig.expect_empty_network(); - self - } - fn expect_no_block_request(mut self) -> Self { - self.rig.expect_empty_network(); - self - } - fn invalidate_blobs_too_few(mut self) -> Self { - self.blobs.pop().expect("blobs"); - self - } - fn expect_block_process(mut self) -> Self { - self.rig.expect_block_process(ResponseType::Block); - self - } - fn expect_no_active_lookups(self) -> Self { - self.rig.expect_no_active_lookups(); - self - } - fn search_parent_dup(mut self) -> Self { - self.rig - .trigger_unknown_parent_block(self.peer_id, self.block.clone()); - self - } - } - - #[test] - fn single_block_and_blob_lookup_block_returned_first_attestation() { - let Some(tester) = DenebTester::new(RequestTrigger::AttestationUnknownBlock) else { - return; - }; - tester - .block_response_and_expect_blob_request() - .blobs_response() - .block_missing_components() // blobs not yet imported - .blobs_response_was_valid() - .blob_imported(); // now blobs resolve as imported - } - - #[test] - fn single_block_response_then_empty_blob_response_attestation() { - let Some(tester) = DenebTester::new(RequestTrigger::AttestationUnknownBlock) else { - return; - }; - tester - .block_response_and_expect_blob_request() - .missing_components_from_block_request() - .empty_blobs_response() - .expect_penalty("NotEnoughResponsesReturned") - .expect_blobs_request() - .expect_no_block_request(); - } - - #[test] - fn single_invalid_block_response_then_blob_response_attestation() { - let Some(tester) = DenebTester::new(RequestTrigger::AttestationUnknownBlock) else { - return; - }; - tester - .block_response_triggering_process() - .invalid_block_processed() - .expect_penalty("lookup_block_processing_failure") - .expect_block_request() - .expect_no_blobs_request() - .blobs_response() - // blobs not sent for processing until the block is processed - .expect_no_penalty_and_no_requests(); - } - - #[test] - fn single_block_response_then_invalid_blob_response_attestation() { - let Some(tester) = DenebTester::new(RequestTrigger::AttestationUnknownBlock) else { - return; - }; - tester - .block_response_triggering_process() - .missing_components_from_block_request() - .blobs_response() - .invalid_blob_processed() - .expect_penalty("lookup_blobs_processing_failure") - .expect_blobs_request() - .expect_no_block_request(); - } - - #[test] - fn single_block_response_then_too_few_blobs_response_attestation() { - let Some(tester) = DenebTester::new(RequestTrigger::AttestationUnknownBlock) else { - return; - }; - tester - .block_response_triggering_process() - .missing_components_from_block_request() - .invalidate_blobs_too_few() - .blobs_response() - .expect_penalty("NotEnoughResponsesReturned") - .expect_blobs_request() - .expect_no_block_request(); - } - - // Test peer returning block that has unknown parent, and a new lookup is created - #[test] - fn parent_block_unknown_parent() { - let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlock(1)) else { - return; - }; - tester - .expect_empty_beacon_processor() - .parent_block_response_expect_blobs() - .parent_blob_response() - .expect_block_process() - .parent_block_unknown_parent() - .expect_parent_block_request() - .expect_empty_beacon_processor(); - } - - // Test peer returning invalid (processing) block, expect retry - #[test] - fn parent_block_invalid_parent() { - let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlock(1)) else { - return; - }; - tester - .parent_block_response_expect_blobs() - .parent_blob_response() - .expect_block_process() - .invalid_parent_processed() - .expect_penalty("lookup_block_processing_failure") - .expect_parent_block_request() - .expect_empty_beacon_processor(); - } - - // Tests that if a peer does not respond with a block, we downscore and retry the block only - #[test] - fn empty_block_is_retried() { - let Some(tester) = DenebTester::new(RequestTrigger::AttestationUnknownBlock) else { - return; - }; - tester - .empty_block_response() - .expect_penalty("NotEnoughResponsesReturned") - .expect_block_request() - .expect_no_blobs_request() - .block_response_and_expect_blob_request() - .blobs_response() - .block_imported() - .expect_no_active_lookups(); - } - - #[test] - fn parent_block_then_empty_parent_blobs() { - let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlock(1)) else { - return; - }; - tester - .parent_block_then_empty_parent_blobs() - .log("resolve original block trigger blobs request and import") - // Should not have block request, it is cached - .expect_blobs_request() - // TODO: Should send blobs for processing - .block_imported() - .expect_no_active_lookups(); - } - - #[test] - fn parent_blob_unknown_parent() { - let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlob(1)) else { - return; - }; - tester - .expect_empty_beacon_processor() - .parent_block_response_expect_blobs() - .parent_blob_response() - .expect_block_process() - .parent_block_unknown_parent() - .expect_parent_block_request() - .expect_empty_beacon_processor(); - } - - #[test] - fn parent_blob_invalid_parent() { - let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlob(1)) else { - return; - }; - tester - .expect_empty_beacon_processor() - .parent_block_response_expect_blobs() - .parent_blob_response() - .expect_block_process() - .invalid_parent_processed() - .expect_penalty("lookup_block_processing_failure") - .expect_parent_block_request() - // blobs are not sent until block is processed - .expect_empty_beacon_processor(); - } - - #[test] - fn parent_block_and_blob_lookup_parent_returned_first_blob_trigger() { - let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlob(1)) else { - return; - }; - tester - .parent_block_response() - .expect_parent_blobs_request() - .parent_blob_response() - .expect_block_process() - .trigger_unknown_block_from_attestation() - .parent_block_imported() - .complete_current_block_and_blobs_lookup() - .expect_no_active_lookups(); - } - - #[test] - fn parent_block_then_empty_parent_blobs_blob_trigger() { - let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlob(1)) else { - return; - }; - tester - .parent_block_then_empty_parent_blobs() - .log("resolve original block trigger blobs request and import") - .complete_current_block_and_blobs_lookup() - .expect_no_active_lookups(); - } - - #[test] - fn parent_blob_unknown_parent_chain() { - let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlob(2)) else { - return; - }; - tester - .expect_empty_beacon_processor() - .parent_block_response_expect_blobs() - .parent_blob_response() - .expect_no_penalty() - .expect_block_process() - .parent_block_unknown_parent() - .expect_parent_block_request() - .expect_empty_beacon_processor() - .parent_block_response() - .expect_parent_blobs_request() - .parent_blob_response() - .expect_no_penalty() - .expect_block_process(); - } - - #[test] - fn unknown_parent_block_dup() { - let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlock(1)) else { - return; - }; - tester - .search_parent_dup() - .expect_no_blobs_request() - .expect_no_block_request(); - } - - #[test] - fn unknown_parent_blob_dup() { - let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlob(1)) else { - return; - }; - tester - .search_parent_dup() - .expect_no_blobs_request() - .expect_no_block_request(); - } - - // This test no longer applies, we don't issue requests for child lookups - // Keep for after updating rules on fetching blocks only first - #[ignore] - #[test] - fn no_peer_penalty_when_rpc_response_already_known_from_gossip() { - let Some(mut r) = TestRig::test_setup_after_deneb_before_fulu() else { - return; - }; - let (block, blobs) = r.rand_block_and_blobs(NumBlobs::Number(2)); - let block_root = block.canonical_root(); - let blob_0 = blobs[0].clone(); - let blob_1 = blobs[1].clone(); - let peer_a = r.new_connected_peer(); - let peer_b = r.new_connected_peer(); - // Send unknown parent block lookup - r.trigger_unknown_parent_block(peer_a, block.into()); - // Expect network request for blobs - let id = r.expect_blob_lookup_request(block_root); - // Peer responses with blob 0 - r.single_lookup_blob_response(id, peer_a, Some(blob_0.into())); - // Blob 1 is received via gossip unknown parent blob from a different peer - r.trigger_unknown_parent_blob(peer_b, blob_1.clone()); - // Original peer sends blob 1 via RPC - r.single_lookup_blob_response(id, peer_a, Some(blob_1.into())); - // Assert no downscore event for original peer - r.expect_no_penalty_for(peer_a); +// These `crypto_on` tests assert that the fake_crytpo feature works as expected. We run only the +// `crypto_on` tests without the fake_crypto feature and make sure that processing fails, = to +// assert that signatures and kzg proofs are checked +#[tokio::test] +async fn crypto_on_fail_with_invalid_block_signature() { + let mut r = TestRig::default(); + r.build_chain(1).await; + r.corrupt_last_block_signature(); + r.trigger_with_last_block(); + r.simulate(SimulateConfig::happy_path()).await; + if cfg!(feature = "fake_crypto") { + r.assert_successful_lookup_sync(); + r.assert_no_penalties(); + } else { + r.assert_failed_lookup_sync(); + r.assert_penalties_of_type("lookup_block_processing_failure"); + } +} + +#[tokio::test] +async fn crypto_on_fail_with_bad_blob_proposer_signature() { + let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { + return; + }; + r.build_chain(1).await; + r.corrupt_last_blob_proposer_signature(); + r.trigger_with_last_block(); + r.simulate(SimulateConfig::happy_path()).await; + if cfg!(feature = "fake_crypto") { + r.assert_successful_lookup_sync(); + r.assert_no_penalties(); + } else { + r.assert_failed_lookup_sync(); + r.assert_penalties_of_type("lookup_blobs_processing_failure"); + } +} + +#[tokio::test] +async fn crypto_on_fail_with_bad_blob_kzg_proof() { + let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { + return; + }; + r.build_chain(1).await; + r.corrupt_last_blob_kzg_proof(); + r.trigger_with_last_block(); + r.simulate(SimulateConfig::happy_path()).await; + if cfg!(feature = "fake_crypto") { + r.assert_successful_lookup_sync(); + r.assert_no_penalties(); + } else { + r.assert_failed_lookup_sync(); + r.assert_penalties_of_type("lookup_blobs_processing_failure"); + } +} + +#[tokio::test] +async fn crypto_on_fail_with_bad_column_proposer_signature() { + let Some(mut r) = TestRig::new_fulu_peer_test(FuluTestType::WeSupernodeThemSupernode) else { + return; + }; + r.build_chain(1).await; + r.corrupt_last_column_proposer_signature(); + r.trigger_with_last_block(); + r.simulate(SimulateConfig::happy_path()).await; + if cfg!(feature = "fake_crypto") { + r.assert_successful_lookup_sync(); + r.assert_no_penalties(); + } else { + r.assert_failed_lookup_sync(); + r.assert_penalties_of_type("lookup_custody_column_processing_failure"); + } +} + +#[tokio::test] +async fn crypto_on_fail_with_bad_column_kzg_proof() { + let Some(mut r) = TestRig::new_fulu_peer_test(FuluTestType::WeSupernodeThemSupernode) else { + return; + }; + r.build_chain(1).await; + r.corrupt_last_column_kzg_proof(); + r.trigger_with_last_block(); + r.simulate(SimulateConfig::happy_path()).await; + if cfg!(feature = "fake_crypto") { + r.assert_successful_lookup_sync(); + r.assert_no_penalties(); + } else { + r.assert_failed_lookup_sync(); + r.assert_penalties_of_type("lookup_custody_column_processing_failure"); } } diff --git a/beacon_node/network/src/sync/tests/mod.rs b/beacon_node/network/src/sync/tests/mod.rs index dcc7e3e49d..f00cf5841d 100644 --- a/beacon_node/network/src/sync/tests/mod.rs +++ b/beacon_node/network/src/sync/tests/mod.rs @@ -1,13 +1,19 @@ use crate::NetworkMessage; use crate::sync::SyncMessage; +use crate::sync::block_lookups::BlockLookupsMetrics; use crate::sync::manager::SyncManager; -use crate::sync::range_sync::RangeSyncType; +use crate::sync::tests::lookups::SimulateConfig; +use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::builder::Witness; +use beacon_chain::custody_context::NodeCustodyType; use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; use beacon_processor::WorkEvent; -use lighthouse_network::NetworkGlobals; +use lighthouse_network::rpc::RequestType; +use lighthouse_network::service::api_types::{AppRequestId, Id}; +use lighthouse_network::{NetworkGlobals, PeerId}; use rand_chacha::ChaCha20Rng; use slot_clock::ManualSlotClock; +use std::collections::{HashMap, HashSet}; use std::fs::OpenOptions; use std::io::Write; use std::sync::{Arc, Once}; @@ -16,7 +22,7 @@ use tokio::sync::mpsc; use tracing_subscriber::fmt::MakeWriter; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; -use types::{ForkName, MinimalEthSpec as E}; +use types::{ForkName, Hash256, MinimalEthSpec as E, Slot}; mod lookups; mod range; @@ -58,6 +64,8 @@ struct TestRig { network_rx_queue: Vec<NetworkMessage<E>>, /// Receiver for `SyncMessage` from the network sync_rx: mpsc::UnboundedReceiver<SyncMessage<E>>, + /// Stores all `SyncMessage`s received from `sync_rx` + sync_rx_queue: Vec<SyncMessage<E>>, /// To send `SyncMessage`. For sending RPC responses or block processing results to sync. sync_manager: SyncManager<T>, /// To manipulate sync state and peer connection status @@ -68,6 +76,65 @@ struct TestRig { rng_08: rand_chacha_03::ChaCha20Rng, rng: ChaCha20Rng, fork_name: ForkName, + /// Blocks that will be used in the test but may not be known to `harness` yet. + network_blocks_by_root: HashMap<Hash256, RpcBlock<E>>, + network_blocks_by_slot: HashMap<Slot, RpcBlock<E>>, + penalties: Vec<ReportedPenalty>, + /// All seen lookups through the test run + seen_lookups: HashMap<Id, SeenLookup>, + /// Registry of all requests done by the test + requests: Vec<(RequestType<E>, AppRequestId)>, + /// Persistent config on how to complete request + complete_strategy: SimulateConfig, + /// Metrics values to allow a reset + initial_block_lookups_metrics: BlockLookupsMetrics, + /// Fulu test type + fulu_test_type: FuluTestType, +} + +enum FuluTestType { + WeSupernodeThemSupernode, + WeSupernodeThemFullnodes, + WeFullnodeThemSupernode, + WeFullnodeThemFullnodes, +} + +impl FuluTestType { + fn we_node_custody_type(&self) -> NodeCustodyType { + match self { + Self::WeSupernodeThemSupernode | Self::WeSupernodeThemFullnodes => { + NodeCustodyType::Supernode + } + Self::WeFullnodeThemSupernode | Self::WeFullnodeThemFullnodes => { + NodeCustodyType::Fullnode + } + } + } + + fn them_node_custody_type(&self) -> NodeCustodyType { + match self { + Self::WeSupernodeThemSupernode | Self::WeFullnodeThemSupernode => { + NodeCustodyType::Supernode + } + Self::WeSupernodeThemFullnodes | Self::WeFullnodeThemFullnodes => { + NodeCustodyType::Fullnode + } + } + } +} + +#[derive(Debug)] +struct SeenLookup { + /// Lookup's Id + id: Id, + block_root: Hash256, + seen_peers: HashSet<PeerId>, +} + +#[derive(Debug)] +struct ReportedPenalty { + pub peer_id: PeerId, + pub msg: &'static str, } // Environment variable to read if `fork_from_env` feature is enabled. diff --git a/beacon_node/network/src/sync/tests/range.rs b/beacon_node/network/src/sync/tests/range.rs index 9283387ebd..f063fa19f7 100644 --- a/beacon_node/network/src/sync/tests/range.rs +++ b/beacon_node/network/src/sync/tests/range.rs @@ -185,7 +185,7 @@ impl TestRig { } #[track_caller] - fn expect_chain_segments(&mut self, count: usize) { + fn assert_chain_segments(&mut self, count: usize) { for i in 0..count { self.pop_received_processor_event(|ev| { (ev.work_type() == beacon_processor::WorkType::ChainSegment).then_some(()) @@ -235,7 +235,7 @@ impl TestRig { panic!("Should have a BlocksByRange request, filter {request_filter:?}: {e:?}") }); - let by_range_data_requests = if self.after_fulu() { + let by_range_data_requests = if self.is_after_fulu() { let mut data_columns_requests = vec![]; while let Ok(data_columns_request) = self.pop_received_network_event(|ev| match ev { NetworkMessage::SendRequest { @@ -254,7 +254,7 @@ impl TestRig { panic!("Found zero DataColumnsByRange requests, filter {request_filter:?}"); } ByRangeDataRequestIds::PostPeerDAS(data_columns_requests) - } else if self.after_deneb() { + } else if self.is_after_deneb() { let (id, peer) = self .pop_received_network_event(|ev| match ev { NetworkMessage::SendRequest { @@ -489,7 +489,7 @@ fn build_rpc_block( fn head_chain_removed_while_finalized_syncing() { // NOTE: this is a regression test. // Added in PR https://github.com/sigp/lighthouse/pull/2821 - let mut rig = TestRig::test_setup(); + let mut rig = TestRig::default(); // Get a peer with an advanced head let head_peer = rig.add_head_peer(); @@ -514,11 +514,11 @@ fn head_chain_removed_while_finalized_syncing() { async fn state_update_while_purging() { // NOTE: this is a regression test. // Added in PR https://github.com/sigp/lighthouse/pull/2827 - let mut rig = TestRig::test_setup_with_custody_type(NodeCustodyType::SemiSupernode); + let mut rig = TestRig::with_custody_type(NodeCustodyType::SemiSupernode); // Create blocks on a separate harness // SemiSupernode ensures enough columns are stored for sampling + custody RPC block validation - let mut rig_2 = TestRig::test_setup_with_custody_type(NodeCustodyType::SemiSupernode); + let mut rig_2 = TestRig::with_custody_type(NodeCustodyType::SemiSupernode); // Need to create blocks that can be inserted into the fork-choice and fit the "known // conditions" below. let head_peer_block = rig_2.create_canonical_block().await; @@ -550,7 +550,7 @@ async fn state_update_while_purging() { #[test] fn pause_and_resume_on_ee_offline() { - let mut rig = TestRig::test_setup(); + let mut rig = TestRig::default(); // add some peers let peer1 = rig.add_head_peer(); @@ -559,7 +559,7 @@ fn pause_and_resume_on_ee_offline() { // send the response to the request rig.find_and_complete_blocks_by_range_request(filter().peer(peer1).epoch(0)); // the beacon processor shouldn't have received any work - rig.expect_empty_processor(); + rig.assert_empty_processor(); // while the ee is offline, more peers might arrive. Add a new finalized peer. let _peer2 = rig.add_finalized_peer(); @@ -570,14 +570,14 @@ fn pause_and_resume_on_ee_offline() { // epoch for the other batch. So we can either filter by epoch of by sync type. rig.find_and_complete_blocks_by_range_request(filter().epoch(0)); // the beacon processor shouldn't have received any work - rig.expect_empty_processor(); + rig.assert_empty_processor(); // make the beacon processor available again. // update_execution_engine_state implicitly calls resume // now resume range, we should have two processing requests in the beacon processor. rig.update_execution_engine_state(EngineState::Online); // The head chain and finalized chain (2) should be in the processing queue - rig.expect_chain_segments(2); + rig.assert_chain_segments(2); } /// To attempt to finalize the peer's status finalized checkpoint we synced to its finalized epoch + @@ -587,7 +587,7 @@ const EXTRA_SYNCED_EPOCHS: u64 = 2 + 1; #[test] fn finalized_sync_enough_global_custody_peers_few_chain_peers() { // Run for all forks - let mut r = TestRig::test_setup(); + let mut r = TestRig::default(); let advanced_epochs: u64 = 2; let remote_info = r.finalized_remote_info_advanced_by(advanced_epochs.into()); @@ -604,7 +604,7 @@ fn finalized_sync_enough_global_custody_peers_few_chain_peers() { #[test] fn finalized_sync_not_enough_custody_peers_on_start() { - let mut r = TestRig::test_setup(); + let mut r = TestRig::default(); // Only run post-PeerDAS if !r.fork_name.fulu_enabled() { return; @@ -621,7 +621,7 @@ fn finalized_sync_not_enough_custody_peers_on_start() { // Because we don't have enough peers on all columns we haven't sent any request. // NOTE: There's a small chance that this single peer happens to custody exactly the set we // expect, in that case the test will fail. Find a way to make the test deterministic. - r.expect_empty_network(); + r.assert_empty_network(); // Generate enough peers and supernodes to cover all custody columns let peer_count = 100; diff --git a/beacon_node/operation_pool/src/lib.rs b/beacon_node/operation_pool/src/lib.rs index 81423d6abd..b3bd091691 100644 --- a/beacon_node/operation_pool/src/lib.rs +++ b/beacon_node/operation_pool/src/lib.rs @@ -1851,8 +1851,11 @@ mod release_tests { let mut spec = E::default_spec(); // Give some room to sign surround slashings. - spec.altair_fork_epoch = Some(Epoch::new(3)); - spec.bellatrix_fork_epoch = Some(Epoch::new(6)); + spec.altair_fork_epoch = Some(Epoch::new(0)); + spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + spec.capella_fork_epoch = Some(Epoch::new(0)); + spec.deneb_fork_epoch = Some(Epoch::new(2)); + spec.electra_fork_epoch = Some(Epoch::new(4)); // To make exits immediately valid. spec.shard_committee_period = 0; @@ -1860,185 +1863,114 @@ mod release_tests { let num_validators = 32; let harness = get_harness::<E>(num_validators, Some(spec.clone())); + if let Some(mock_el) = harness.mock_execution_layer.as_ref() { + mock_el.server.all_payloads_valid(); + } (harness, spec) } - /// Test several cross-fork voluntary exits: - /// - /// - phase0 exit (not valid after Bellatrix) - /// - phase0 exit signed with Altair fork version (only valid after Bellatrix) - #[tokio::test] - async fn cross_fork_exits() { - let (harness, spec) = cross_fork_harness::<MainnetEthSpec>(); - let altair_fork_epoch = spec.altair_fork_epoch.unwrap(); - let bellatrix_fork_epoch = spec.bellatrix_fork_epoch.unwrap(); - let slots_per_epoch = MainnetEthSpec::slots_per_epoch(); - - let op_pool = OperationPool::<MainnetEthSpec>::new(); - - // Sign an exit in phase0 with a phase0 epoch. - let exit1 = harness.make_voluntary_exit(0, Epoch::new(0)); - - // Advance to Altair. - harness - .extend_to_slot(altair_fork_epoch.start_slot(slots_per_epoch)) - .await; - let altair_head = harness.chain.canonical_head.cached_head().snapshot; - assert_eq!(altair_head.beacon_state.current_epoch(), altair_fork_epoch); - - // Add exit 1 to the op pool during Altair. It's still valid at this point and should be - // returned. - let verified_exit1 = exit1 - .clone() - .validate(&altair_head.beacon_state, &harness.chain.spec) - .unwrap(); - op_pool.insert_voluntary_exit(verified_exit1); - let exits = - op_pool.get_voluntary_exits(&altair_head.beacon_state, |_| true, &harness.chain.spec); - assert!(exits.contains(&exit1)); - assert_eq!(exits.len(), 1); - - // Advance to Bellatrix. - harness - .extend_to_slot(bellatrix_fork_epoch.start_slot(slots_per_epoch)) - .await; - let bellatrix_head = harness.chain.canonical_head.cached_head().snapshot; - assert_eq!( - bellatrix_head.beacon_state.current_epoch(), - bellatrix_fork_epoch - ); - - // Sign an exit with the Altair domain and a phase0 epoch. This is a weird type of exit - // that is valid because after the Bellatrix fork we'll use the Altair fork domain to verify - // all prior epochs. - let unsigned_exit = VoluntaryExit { - epoch: Epoch::new(0), - validator_index: 2, - }; - let exit2 = SignedVoluntaryExit { - message: unsigned_exit.clone(), - signature: harness.validator_keypairs[2] - .sk - .sign(unsigned_exit.signing_root(spec.compute_domain( - Domain::VoluntaryExit, - harness.spec.altair_fork_version, - harness.chain.genesis_validators_root, - ))), - }; - - let verified_exit2 = exit2 - .clone() - .validate(&bellatrix_head.beacon_state, &harness.chain.spec) - .unwrap(); - op_pool.insert_voluntary_exit(verified_exit2); - - // Attempting to fetch exit1 now should fail, despite it still being in the pool. - // exit2 should still be valid, because it was signed with the Altair fork domain. - assert_eq!(op_pool.voluntary_exits.read().len(), 2); - let exits = - op_pool.get_voluntary_exits(&bellatrix_head.beacon_state, |_| true, &harness.spec); - assert_eq!(&exits, &[exit2]); - } + // Voluntary exits signed post-Capella are perpetually valid across forks, so no + // cross-fork test is required here. /// Test several cross-fork proposer slashings: /// - /// - phase0 slashing (not valid after Bellatrix) - /// - Bellatrix signed with Altair fork version (not valid after Bellatrix) - /// - phase0 exit signed with Altair fork version (only valid after Bellatrix) + /// - Capella slashing (not valid after Electra) + /// - Electra signed with Deneb fork version (not valid after Electra) + /// - Capella exit signed with Deneb fork version (only valid after Electra) #[tokio::test] async fn cross_fork_proposer_slashings() { let (harness, spec) = cross_fork_harness::<MainnetEthSpec>(); let slots_per_epoch = MainnetEthSpec::slots_per_epoch(); - let altair_fork_epoch = spec.altair_fork_epoch.unwrap(); - let bellatrix_fork_epoch = spec.bellatrix_fork_epoch.unwrap(); - let bellatrix_fork_slot = bellatrix_fork_epoch.start_slot(slots_per_epoch); + let deneb_fork_epoch = spec.deneb_fork_epoch.unwrap(); + let electra_fork_epoch = spec.electra_fork_epoch.unwrap(); + let electra_fork_slot = electra_fork_epoch.start_slot(slots_per_epoch); let op_pool = OperationPool::<MainnetEthSpec>::new(); - // Sign a proposer slashing in phase0 with a phase0 epoch. + // Sign a proposer slashing in Capella with a Capella slot. let slashing1 = harness.make_proposer_slashing_at_slot(0, Some(Slot::new(1))); - // Advance to Altair. + // Advance to Deneb. harness - .extend_to_slot(altair_fork_epoch.start_slot(slots_per_epoch)) + .extend_to_slot(deneb_fork_epoch.start_slot(slots_per_epoch)) .await; - let altair_head = harness.chain.canonical_head.cached_head().snapshot; - assert_eq!(altair_head.beacon_state.current_epoch(), altair_fork_epoch); + let deneb_head = harness.chain.canonical_head.cached_head().snapshot; + assert_eq!(deneb_head.beacon_state.current_epoch(), deneb_fork_epoch); - // Add slashing1 to the op pool during Altair. It's still valid at this point and should be + // Add slashing1 to the op pool during Deneb. It's still valid at this point and should be // returned. let verified_slashing1 = slashing1 .clone() - .validate(&altair_head.beacon_state, &harness.chain.spec) + .validate(&deneb_head.beacon_state, &harness.chain.spec) .unwrap(); op_pool.insert_proposer_slashing(verified_slashing1); let (proposer_slashings, _, _) = - op_pool.get_slashings_and_exits(&altair_head.beacon_state, &harness.chain.spec); + op_pool.get_slashings_and_exits(&deneb_head.beacon_state, &harness.chain.spec); assert!(proposer_slashings.contains(&slashing1)); assert_eq!(proposer_slashings.len(), 1); - // Sign a proposer slashing with a Bellatrix slot using the Altair fork domain. + // Sign a proposer slashing with a Electra slot using the Deneb fork domain. // - // This slashing is valid only before the Bellatrix fork epoch. - let slashing2 = harness.make_proposer_slashing_at_slot(1, Some(bellatrix_fork_slot)); + // This slashing is valid only before the Electra fork epoch. + let slashing2 = harness.make_proposer_slashing_at_slot(1, Some(electra_fork_slot)); let verified_slashing2 = slashing2 .clone() - .validate(&altair_head.beacon_state, &harness.chain.spec) + .validate(&deneb_head.beacon_state, &harness.chain.spec) .unwrap(); op_pool.insert_proposer_slashing(verified_slashing2); let (proposer_slashings, _, _) = - op_pool.get_slashings_and_exits(&altair_head.beacon_state, &harness.chain.spec); + op_pool.get_slashings_and_exits(&deneb_head.beacon_state, &harness.chain.spec); assert!(proposer_slashings.contains(&slashing1)); assert!(proposer_slashings.contains(&slashing2)); assert_eq!(proposer_slashings.len(), 2); - // Advance to Bellatrix. - harness.extend_to_slot(bellatrix_fork_slot).await; - let bellatrix_head = harness.chain.canonical_head.cached_head().snapshot; + // Advance to Electra. + harness.extend_to_slot(electra_fork_slot).await; + let electra_head = harness.chain.canonical_head.cached_head().snapshot; assert_eq!( - bellatrix_head.beacon_state.current_epoch(), - bellatrix_fork_epoch + electra_head.beacon_state.current_epoch(), + electra_fork_epoch ); - // Sign a proposer slashing with the Altair domain and a phase0 slot. This is a weird type - // of slashing that is only valid after the Bellatrix fork because we'll use the Altair fork + // Sign a proposer slashing with the Deneb domain and a Capella slot. This is a weird type + // of slashing that is only valid after the Electra fork because we'll use the Deneb fork // domain to verify all prior epochs. let slashing3 = harness.make_proposer_slashing_at_slot(2, Some(Slot::new(1))); let verified_slashing3 = slashing3 .clone() - .validate(&bellatrix_head.beacon_state, &harness.chain.spec) + .validate(&electra_head.beacon_state, &harness.chain.spec) .unwrap(); op_pool.insert_proposer_slashing(verified_slashing3); // Attempting to fetch slashing1 now should fail, despite it still being in the pool. // Likewise slashing2 is also invalid now because it should be signed with the - // Bellatrix fork version. - // slashing3 should still be valid, because it was signed with the Altair fork domain. + // Electra fork version. + // slashing3 should still be valid, because it was signed with the Deneb fork domain. assert_eq!(op_pool.proposer_slashings.read().len(), 3); let (proposer_slashings, _, _) = - op_pool.get_slashings_and_exits(&bellatrix_head.beacon_state, &harness.spec); + op_pool.get_slashings_and_exits(&electra_head.beacon_state, &harness.spec); assert!(proposer_slashings.contains(&slashing3)); assert_eq!(proposer_slashings.len(), 1); } /// Test several cross-fork attester slashings: /// - /// - both target epochs in phase0 (not valid after Bellatrix) - /// - both target epochs in Bellatrix but signed with Altair domain (not valid after Bellatrix) - /// - Altair attestation that surrounds a phase0 attestation (not valid after Bellatrix) - /// - both target epochs in phase0 but signed with Altair domain (only valid after Bellatrix) + /// - both target epochs in Capella (not valid after Electra) + /// - both target epochs in Electra but signed with Deneb domain (not valid after Electra) + /// - Deneb attestation that surrounds a Capella attestation (not valid after Electra) + /// - both target epochs in Capella but signed with Deneb domain (only valid after Electra) #[tokio::test] async fn cross_fork_attester_slashings() { let (harness, spec) = cross_fork_harness::<MainnetEthSpec>(); let slots_per_epoch = MainnetEthSpec::slots_per_epoch(); let zero_epoch = Epoch::new(0); - let altair_fork_epoch = spec.altair_fork_epoch.unwrap(); - let bellatrix_fork_epoch = spec.bellatrix_fork_epoch.unwrap(); - let bellatrix_fork_slot = bellatrix_fork_epoch.start_slot(slots_per_epoch); + let deneb_fork_epoch = spec.deneb_fork_epoch.unwrap(); + let electra_fork_epoch = spec.electra_fork_epoch.unwrap(); + let electra_fork_slot = electra_fork_epoch.start_slot(slots_per_epoch); let op_pool = OperationPool::<MainnetEthSpec>::new(); - // Sign an attester slashing with the phase0 fork version, with both target epochs in phase0. + // Sign an attester slashing with the Capella fork version, with both target epochs in Capella. let slashing1 = harness.make_attester_slashing_with_epochs( vec![0], None, @@ -2047,55 +1979,55 @@ mod release_tests { Some(zero_epoch), ); - // Advance to Altair. + // Advance to Deneb. harness - .extend_to_slot(altair_fork_epoch.start_slot(slots_per_epoch)) + .extend_to_slot(deneb_fork_epoch.start_slot(slots_per_epoch)) .await; - let altair_head = harness.chain.canonical_head.cached_head().snapshot; - assert_eq!(altair_head.beacon_state.current_epoch(), altair_fork_epoch); + let deneb_head = harness.chain.canonical_head.cached_head().snapshot; + assert_eq!(deneb_head.beacon_state.current_epoch(), deneb_fork_epoch); - // Add slashing1 to the op pool during Altair. It's still valid at this point and should be + // Add slashing1 to the op pool during Deneb. It's still valid at this point and should be // returned. let verified_slashing1 = slashing1 .clone() - .validate(&altair_head.beacon_state, &harness.chain.spec) + .validate(&deneb_head.beacon_state, &harness.chain.spec) .unwrap(); op_pool.insert_attester_slashing(verified_slashing1); - // Sign an attester slashing with two Bellatrix epochs using the Altair fork domain. + // Sign an attester slashing with two Electra epochs using the Deneb fork domain. // - // This slashing is valid only before the Bellatrix fork epoch. + // This slashing is valid only before the Electra fork epoch. let slashing2 = harness.make_attester_slashing_with_epochs( vec![1], None, - Some(bellatrix_fork_epoch), + Some(electra_fork_epoch), None, - Some(bellatrix_fork_epoch), + Some(electra_fork_epoch), ); let verified_slashing2 = slashing2 .clone() - .validate(&altair_head.beacon_state, &harness.chain.spec) + .validate(&deneb_head.beacon_state, &harness.chain.spec) .unwrap(); op_pool.insert_attester_slashing(verified_slashing2); let (_, attester_slashings, _) = - op_pool.get_slashings_and_exits(&altair_head.beacon_state, &harness.chain.spec); + op_pool.get_slashings_and_exits(&deneb_head.beacon_state, &harness.chain.spec); assert!(attester_slashings.contains(&slashing1)); assert!(attester_slashings.contains(&slashing2)); assert_eq!(attester_slashings.len(), 2); - // Sign an attester slashing where an Altair attestation surrounds a phase0 one. + // Sign an attester slashing where a Deneb attestation surrounds a Capella one. // - // This slashing is valid only before the Bellatrix fork epoch. + // This slashing is valid only before the Electra fork epoch. let slashing3 = harness.make_attester_slashing_with_epochs( vec![2], Some(Epoch::new(0)), - Some(altair_fork_epoch), + Some(deneb_fork_epoch), Some(Epoch::new(1)), - Some(altair_fork_epoch - 1), + Some(deneb_fork_epoch - 1), ); let verified_slashing3 = slashing3 .clone() - .validate(&altair_head.beacon_state, &harness.chain.spec) + .validate(&deneb_head.beacon_state, &harness.chain.spec) .unwrap(); op_pool.insert_attester_slashing(verified_slashing3); @@ -2104,44 +2036,43 @@ mod release_tests { // slashed. let mut to_be_slashed = hashset! {0}; let attester_slashings = - op_pool.get_attester_slashings(&altair_head.beacon_state, &mut to_be_slashed); + op_pool.get_attester_slashings(&deneb_head.beacon_state, &mut to_be_slashed); assert!(attester_slashings.contains(&slashing2)); assert!(attester_slashings.contains(&slashing3)); assert_eq!(attester_slashings.len(), 2); - // Advance to Bellatrix. - harness.extend_to_slot(bellatrix_fork_slot).await; - let bellatrix_head = harness.chain.canonical_head.cached_head().snapshot; + // Advance to Electra + harness.extend_to_slot(electra_fork_slot).await; + let electra_head = harness.chain.canonical_head.cached_head().snapshot; assert_eq!( - bellatrix_head.beacon_state.current_epoch(), - bellatrix_fork_epoch + electra_head.beacon_state.current_epoch(), + electra_fork_epoch ); - // Sign an attester slashing with the Altair domain and phase0 epochs. This is a weird type - // of slashing that is only valid after the Bellatrix fork because we'll use the Altair fork - // domain to verify all prior epochs. + // Sign an attester slashing with the Deneb domain and Capella epochs. This is only valid + // after the Electra fork. let slashing4 = harness.make_attester_slashing_with_epochs( vec![3], Some(Epoch::new(0)), - Some(altair_fork_epoch - 1), + Some(deneb_fork_epoch - 1), Some(Epoch::new(0)), - Some(altair_fork_epoch - 1), + Some(deneb_fork_epoch - 1), ); let verified_slashing4 = slashing4 .clone() - .validate(&bellatrix_head.beacon_state, &harness.chain.spec) + .validate(&electra_head.beacon_state, &harness.chain.spec) .unwrap(); op_pool.insert_attester_slashing(verified_slashing4); // All slashings except slashing4 are now invalid (despite being present in the pool). assert_eq!(op_pool.attester_slashings.read().len(), 4); let (_, attester_slashings, _) = - op_pool.get_slashings_and_exits(&bellatrix_head.beacon_state, &harness.spec); + op_pool.get_slashings_and_exits(&electra_head.beacon_state, &harness.spec); assert!(attester_slashings.contains(&slashing4)); assert_eq!(attester_slashings.len(), 1); // Pruning the attester slashings should remove all but slashing4. - op_pool.prune_attester_slashings(&bellatrix_head.beacon_state); + op_pool.prune_attester_slashings(&electra_head.beacon_state); assert_eq!(op_pool.attester_slashings.read().len(), 1); } } diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 9553fe60ba..61dccc9674 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -1246,9 +1246,12 @@ pub fn cli_app() -> Command { .display_order(0) ) .arg( - Arg::new("reconstruct-historic-states") - .long("reconstruct-historic-states") - .help("After a checkpoint sync, reconstruct historic states in the database. This requires syncing all the way back to genesis.") + Arg::new("archive") + .long("archive") + .alias("reconstruct-historic-states") + .help("Store all beacon states in the database. When checkpoint syncing, \ + states are reconstructed after backfill completes. This requires \ + syncing all the way back to genesis.") .action(ArgAction::SetTrue) .help_heading(FLAG_HEADER) .display_order(0) @@ -1404,6 +1407,16 @@ pub fn cli_app() -> Command { .help_heading(FLAG_HEADER) .display_order(0) ) + .arg( + Arg::new("ignore-ws-check") + .long("ignore-ws-check") + .help("Using this flag allows a node to run in a state that may expose it to long-range attacks. \ + For more information please read this blog post: https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak-subjectivity \ + If you understand the risks, you can use this flag to disable the Weak Subjectivity check at startup.") + .action(ArgAction::SetTrue) + .help_heading(FLAG_HEADER) + .display_order(0) + ) .arg( Arg::new("builder-fallback-skips") .long("builder-fallback-skips") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 752cf10550..0a52bcef06 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -554,8 +554,8 @@ pub fn get_config<E: EthSpec>( ClientGenesis::DepositContract }; - if cli_args.get_flag("reconstruct-historic-states") { - client_config.chain.reconstruct_historic_states = true; + if cli_args.get_flag("archive") { + client_config.chain.archive = true; client_config.chain.genesis_backfill = true; } @@ -780,6 +780,8 @@ pub fn get_config<E: EthSpec>( client_config.chain.paranoid_block_proposal = cli_args.get_flag("paranoid-block-proposal"); + client_config.chain.ignore_ws_check = cli_args.get_flag("ignore-ws-check"); + /* * Builder fallback configs. */ diff --git a/beacon_node/src/lib.rs b/beacon_node/src/lib.rs index 6db2150e5f..e33da17e26 100644 --- a/beacon_node/src/lib.rs +++ b/beacon_node/src/lib.rs @@ -22,14 +22,9 @@ use types::{ChainSpec, Epoch, EthSpec, ForkName}; pub type ProductionClient<E> = Client<Witness<SystemTimeSlotClock, E, BeaconNodeBackend<E>, BeaconNodeBackend<E>>>; -/// The beacon node `Client` that will be used in production. +/// The beacon node `Client` that is used in production. /// /// Generic over some `EthSpec`. -/// -/// ## Notes: -/// -/// Despite being titled `Production...`, this code is not ready for production. The name -/// demonstrates an intention, not a promise. pub struct ProductionBeaconNode<E: EthSpec>(ProductionClient<E>); impl<E: EthSpec> ProductionBeaconNode<E> { diff --git a/beacon_node/store/src/consensus_context.rs b/beacon_node/store/src/consensus_context.rs deleted file mode 100644 index 281106d9aa..0000000000 --- a/beacon_node/store/src/consensus_context.rs +++ /dev/null @@ -1,65 +0,0 @@ -use ssz_derive::{Decode, Encode}; -use state_processing::ConsensusContext; -use std::collections::HashMap; -use types::{EthSpec, Hash256, IndexedAttestation, Slot}; - -/// The consensus context is stored on disk as part of the data availability overflow cache. -/// -/// We use this separate struct to keep the on-disk format stable in the presence of changes to the -/// in-memory `ConsensusContext`. You MUST NOT change the fields of this struct without -/// superstructing it and implementing a schema migration. -#[derive(Debug, PartialEq, Clone, Encode, Decode)] -pub struct OnDiskConsensusContext<E: EthSpec> { - /// Slot to act as an identifier/safeguard - slot: Slot, - /// Proposer index of the block at `slot`. - proposer_index: Option<u64>, - /// Block root of the block at `slot`. - current_block_root: Option<Hash256>, - /// We keep the indexed attestations in the *in-memory* version of this struct so that we don't - /// need to regenerate them if roundtripping via this type *without* going to disk. - /// - /// They are not part of the on-disk format. - #[ssz(skip_serializing, skip_deserializing)] - indexed_attestations: HashMap<Hash256, IndexedAttestation<E>>, -} - -impl<E: EthSpec> OnDiskConsensusContext<E> { - pub fn from_consensus_context(ctxt: ConsensusContext<E>) -> Self { - // Match exhaustively on fields here so we are forced to *consider* updating the on-disk - // format when the `ConsensusContext` fields change. - let ConsensusContext { - slot, - previous_epoch: _, - current_epoch: _, - proposer_index, - current_block_root, - indexed_attestations, - } = ctxt; - OnDiskConsensusContext { - slot, - proposer_index, - current_block_root, - indexed_attestations, - } - } - - pub fn into_consensus_context(self) -> ConsensusContext<E> { - let OnDiskConsensusContext { - slot, - proposer_index, - current_block_root, - indexed_attestations, - } = self; - - let mut ctxt = ConsensusContext::new(slot); - - if let Some(proposer_index) = proposer_index { - ctxt = ctxt.set_proposer_index(proposer_index); - } - if let Some(block_root) = current_block_root { - ctxt = ctxt.set_current_block_root(block_root); - } - ctxt.set_indexed_attestations(indexed_attestations) - } -} diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 6e165702a2..fe3477dbfe 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -38,7 +38,7 @@ use std::num::NonZeroUsize; use std::path::Path; use std::sync::Arc; use std::time::Duration; -use tracing::{debug, error, info, instrument, warn}; +use tracing::{debug, debug_span, error, info, instrument, warn}; use typenum::Unsigned; use types::data::{ColumnIndex, DataColumnSidecar, DataColumnSidecarList}; use types::*; @@ -721,14 +721,6 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> }) } - /// Fetch a block from the store, ignoring which fork variant it *should* be for. - pub fn get_block_any_variant<Payload: AbstractExecPayload<E>>( - &self, - block_root: &Hash256, - ) -> Result<Option<SignedBeaconBlock<E, Payload>>, Error> { - self.get_block_with(block_root, SignedBeaconBlock::any_from_ssz_bytes) - } - /// Fetch a block from the store using a custom decode function. /// /// This is useful for e.g. ignoring the slot-indicated fork to forcefully load a block as if it @@ -1518,14 +1510,24 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> let blob_cache_ops = blobs_ops.clone(); // Try to execute blobs store ops. - self.blobs_db - .do_atomically(self.convert_to_kv_batch(blobs_ops)?)?; + let kv_blob_ops = self.convert_to_kv_batch(blobs_ops)?; + { + let _span = debug_span!("write_blobs_db").entered(); + self.blobs_db.do_atomically(kv_blob_ops)?; + } let hot_db_cache_ops = hot_db_ops.clone(); // Try to execute hot db store ops. - let tx_res = match self.convert_to_kv_batch(hot_db_ops) { - Ok(kv_store_ops) => self.hot_db.do_atomically(kv_store_ops), - Err(e) => Err(e), + let tx_res = { + let _convert_span = debug_span!("convert_hot_db_ops").entered(); + match self.convert_to_kv_batch(hot_db_ops) { + Ok(kv_store_ops) => { + drop(_convert_span); + let _span = debug_span!("write_hot_db").entered(); + self.hot_db.do_atomically(kv_store_ops) + } + Err(e) => Err(e), + } }; // Rollback on failure if let Err(e) = tx_res { diff --git a/beacon_node/store/src/iter.rs b/beacon_node/store/src/iter.rs index e2b666e597..0cb803d1ed 100644 --- a/beacon_node/store/src/iter.rs +++ b/beacon_node/store/src/iter.rs @@ -249,7 +249,6 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> Iterator pub struct ParentRootBlockIterator<'a, E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> { store: &'a HotColdDB<E, Hot, Cold>, next_block_root: Hash256, - decode_any_variant: bool, _phantom: PhantomData<E>, } @@ -260,17 +259,6 @@ impl<'a, E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> Self { store, next_block_root: start_block_root, - decode_any_variant: false, - _phantom: PhantomData, - } - } - - /// Block iterator that is tolerant of blocks that have the wrong fork for their slot. - pub fn fork_tolerant(store: &'a HotColdDB<E, Hot, Cold>, start_block_root: Hash256) -> Self { - Self { - store, - next_block_root: start_block_root, - decode_any_variant: true, _phantom: PhantomData, } } @@ -285,12 +273,10 @@ impl<'a, E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> Ok(None) } else { let block_root = self.next_block_root; - let block = if self.decode_any_variant { - self.store.get_block_any_variant(&block_root) - } else { - self.store.get_blinded_block(&block_root) - }? - .ok_or(Error::BlockNotFound(block_root))?; + let block = self + .store + .get_blinded_block(&block_root)? + .ok_or(Error::BlockNotFound(block_root))?; self.next_block_root = block.message().parent_root(); Ok(Some((block_root, block))) } diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index ee9cfce0ec..3363eb800c 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -9,7 +9,6 @@ //! tests for implementation examples. pub mod blob_sidecar_list_from_root; pub mod config; -pub mod consensus_context; pub mod errors; mod forwards_iter; pub mod hdiff; @@ -27,7 +26,6 @@ pub mod iter; pub use self::blob_sidecar_list_from_root::BlobSidecarListFromRoot; pub use self::config::StoreConfig; -pub use self::consensus_context::OnDiskConsensusContext; pub use self::hot_cold_store::{HotColdDB, HotStateSummary, Split}; pub use self::memory_store::MemoryStore; pub use crate::metadata::BlobInfo; diff --git a/book/src/advanced_checkpoint_sync.md b/book/src/advanced_checkpoint_sync.md index 7c30598928..0682310dcd 100644 --- a/book/src/advanced_checkpoint_sync.md +++ b/book/src/advanced_checkpoint_sync.md @@ -102,7 +102,7 @@ lack of historic states. _You do not need these states to run a staking node_, b for historical API calls (as used by block explorers and researchers). To run an archived node, you can opt-in to reconstructing all of the historic states by providing the -`--reconstruct-historic-states` flag to the beacon node at any point (before, during or after sync). +`--archive` flag to the beacon node at any point (before, during or after sync). The database keeps track of three markers to determine the availability of historic blocks and states: diff --git a/book/src/api_lighthouse.md b/book/src/api_lighthouse.md index 0442bf4ec0..2fd7290cb2 100644 --- a/book/src/api_lighthouse.md +++ b/book/src/api_lighthouse.md @@ -590,60 +590,6 @@ Caveats: This is because the state *prior* to the `start_epoch` needs to be loaded from the database, and loading a state on a boundary is most efficient. -## `/lighthouse/analysis/block_rewards` - -Fetch information about the block rewards paid to proposers for a range of consecutive blocks. - -Two query parameters are required: - -- `start_slot` (inclusive): the slot of the first block to compute rewards for. -- `end_slot` (inclusive): the slot of the last block to compute rewards for. - -Example: - -```bash -curl -X GET "http://localhost:5052/lighthouse/analysis/block_rewards?start_slot=1&end_slot=1" | jq -``` - -The first few lines of the response would look like: - -```json -[ - { - "total": 637260, - "block_root": "0x4a089c5e390bb98e66b27358f157df825128ea953cee9d191229c0bcf423a4f6", - "meta": { - "slot": "1", - "parent_slot": "0", - "proposer_index": 93, - "graffiti": "EF #vm-eth2-raw-iron-101" - }, - "attestation_rewards": { - "total": 637260, - "prev_epoch_total": 0, - "curr_epoch_total": 637260, - "per_attestation_rewards": [ - { - "50102": 780, - } - ] - } - } -] -``` - -Caveats: - -- Presently only attestation and sync committee rewards are computed. -- The output format is verbose and subject to change. Please see [`BlockReward`][block_reward_src] - in the source. -- For maximum efficiency the `start_slot` should satisfy `start_slot % slots_per_restore_point == 1`. - This is because the state *prior* to the `start_slot` needs to be loaded from the database, and - loading a state on a boundary is most efficient. - -[block_reward_src]: -https://github.com/sigp/lighthouse/tree/unstable/common/eth2/src/lighthouse/block_rewards.rs - ## `/lighthouse/analysis/block_packing` Fetch information about the block packing efficiency of blocks for a range of consecutive diff --git a/book/src/api_validator_inclusion.md b/book/src/api_validator_inclusion.md index eef563dcdb..d86483e0ea 100644 --- a/book/src/api_validator_inclusion.md +++ b/book/src/api_validator_inclusion.md @@ -8,7 +8,7 @@ These endpoints are not stable or included in the Ethereum consensus standard AP they are subject to change or removal without a change in major release version. -In order to apply these APIs, you need to have historical states information in the database of your node. This means adding the flag `--reconstruct-historic-states` in the beacon node. Once the state reconstruction process is completed, you can apply these APIs to any epoch. +In order to apply these APIs, you need to have historical states information in the database of your node. This means adding the flag `--archive` in the beacon node. Once the state reconstruction process is completed, you can apply these APIs to any epoch. ## Endpoints diff --git a/book/src/faq.md b/book/src/faq.md index c9bc53533f..5ba2c3407f 100644 --- a/book/src/faq.md +++ b/book/src/faq.md @@ -167,19 +167,19 @@ This is a known [bug](https://github.com/sigp/lighthouse/issues/3707) that will ### <a name="bn-partial-history"></a> How can I construct only partial state history? -Lighthouse prunes finalized states by default. Nevertheless, it is quite often that users may be interested in the state history of a few epochs before finalization. To have access to these pruned states, Lighthouse typically requires a full reconstruction of states using the flag `--reconstruct-historic-states` (which will usually take a week). Partial state history can be achieved with some "tricks". Here are the general steps: +Lighthouse prunes finalized states by default. Nevertheless, it is quite often that users may be interested in the state history of a few epochs before finalization. To have access to these pruned states, Lighthouse typically requires a full reconstruction of states using the flag `--archive` (which will usually take a week). Partial state history can be achieved with some "tricks". Here are the general steps: 1. Delete the current database. You can do so with `--purge-db-force` or manually deleting the database from the data directory: `$datadir/beacon`. - 1. If you are interested in the states from the current slot and beyond, perform a checkpoint sync with the flag `--reconstruct-historic-states`, then you can skip the following and jump straight to Step 5 to check the database. + 1. If you are interested in the states from the current slot and beyond, perform a checkpoint sync with the flag `--archive`, then you can skip the following and jump straight to Step 5 to check the database. - If you are interested in the states before the current slot, identify the slot to perform a manual checkpoint sync. With the default configuration, this slot should be divisible by 2<sup>21</sup>, as this is where a full state snapshot is stored. With the flag `--reconstruct-historic-states`, the state upper limit will be adjusted to the next full snapshot slot, a slot that satisfies: `slot % 2**21 == 0`. In other words, to have the state history available before the current slot, we have to checkpoint sync 2<sup>21</sup> slots before the next full snapshot slot. + If you are interested in the states before the current slot, identify the slot to perform a manual checkpoint sync. With the default configuration, this slot should be divisible by 2<sup>21</sup>, as this is where a full state snapshot is stored. With the flag `--archive`, the state upper limit will be adjusted to the next full snapshot slot, a slot that satisfies: `slot % 2**21 == 0`. In other words, to have the state history available before the current slot, we have to checkpoint sync 2<sup>21</sup> slots before the next full snapshot slot. Example: Say the current mainnet is at slot `12000000`. As the next full state snapshot is at slot `12582912`, the slot that we want is slot `10485760`. You can calculate this (in Python) using `12000000 // 2**21 * 2**21`. 1. [Export](./advanced_checkpoint_sync.md#manual-checkpoint-sync) the blobs, block and state data for the slot identified in Step 2. This can be done from another beacon node that you have access to, or you could use any available public beacon API, e.g., [QuickNode](https://www.quicknode.com/docs/ethereum). - 1. Perform a [manual checkpoint sync](./advanced_checkpoint_sync.md#manual-checkpoint-sync) using the data from the previous step, and provide the flag `--reconstruct-historic-states`. + 1. Perform a [manual checkpoint sync](./advanced_checkpoint_sync.md#manual-checkpoint-sync) using the data from the previous step, and provide the flag `--archive`. 1. Check the database: @@ -193,9 +193,9 @@ Lighthouse prunes finalized states by default. Nevertheless, it is quite often t "state_upper_limit": "10485760", ``` -Lighthouse will now start to reconstruct historic states from slot `10485760`. At this point, if you do not want a full state reconstruction, you may remove the flag `--reconstruct-historic-states` (and restart). When the process is completed, you will have the state data from slot `10485760`. Going forward, Lighthouse will continue retaining all historical states newer than the snapshot. Eventually this can lead to increased disk usage, which presently can only be reduced by repeating the process starting from a more recent snapshot. +Lighthouse will now start to reconstruct historic states from slot `10485760`. At this point, if you do not want a full state reconstruction, you may remove the flag `--archive` (and restart). When the process is completed, you will have the state data from slot `10485760`. Going forward, Lighthouse will continue retaining all historical states newer than the snapshot. Eventually this can lead to increased disk usage, which presently can only be reduced by repeating the process starting from a more recent snapshot. -> Note: You may only be interested in very recent historic states. To do so, you may configure the full snapshot to be, for example, every 2<sup>11</sup> slots, see [database configuration](./advanced_database.md#hierarchical-state-diffs) for more details. This can be configured with the flag `--hierarchy-exponents 5,7,11` together with the flag `--reconstruct-historic-states`. This will affect the slot number in Step 2, while other steps remain the same. Note that this comes at the expense of a higher storage requirement. +> Note: You may only be interested in very recent historic states. To do so, you may configure the full snapshot to be, for example, every 2<sup>11</sup> slots, see [database configuration](./advanced_database.md#hierarchical-state-diffs) for more details. This can be configured with the flag `--hierarchy-exponents 5,7,11` together with the flag `--archive`. This will affect the slot number in Step 2, while other steps remain the same. Note that this comes at the expense of a higher storage requirement. > With `--hierarchy-exponents 5,7,11`, using the same example as above, the next full state snapshot is at slot `12001280`. So the slot to checkpoint sync from is: slot `11999232`. diff --git a/book/src/help_bn.md b/book/src/help_bn.md index d3aa27c8a7..cad21a3e78 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -439,6 +439,10 @@ Flags: intended for use by block builders, relays and developers. You should set a fee recipient on this BN and also consider adjusting the --prepare-payload-lookahead flag. + --archive + Store all beacon states in the database. When checkpoint syncing, + states are reconstructed after backfill completes. This requires + syncing all the way back to genesis. --builder-fallback-disable-checks This flag disables all checks related to chain health. This means the builder API will always be used for payload construction, regardless @@ -509,6 +513,12 @@ Flags: --http-enable-tls Serves the RESTful HTTP API server over TLS. This feature is currently experimental. + --ignore-ws-check + Using this flag allows a node to run in a state that may expose it to + long-range attacks. For more information please read this blog post: + https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak-subjectivity + If you understand the risks, you can use this flag to disable the Weak + Subjectivity check at startup. --import-all-attestations Import and aggregate all attestations, regardless of validator subscriptions. This will only import attestations from @@ -546,9 +556,6 @@ Flags: --purge-db-force If present, the chain database will be deleted without confirmation. Use with caution. - --reconstruct-historic-states - After a checkpoint sync, reconstruct historic states in the database. - This requires syncing all the way back to genesis. --reset-payload-statuses When present, Lighthouse will forget the payload statuses of any already-imported blocks. This can assist in the recovery from a diff --git a/book/src/help_vm_import.md b/book/src/help_vm_import.md index 3c768f6705..09c1b74f4d 100644 --- a/book/src/help_vm_import.md +++ b/book/src/help_vm_import.md @@ -24,6 +24,9 @@ Options: --debug-level <LEVEL> Specifies the verbosity level used when emitting logs to the terminal. [default: info] [possible values: info, debug, trace, warn, error] + --enabled <enabled> + When provided, the imported validator will be enabled or disabled. + [possible values: true, false] --gas-limit <UINT64> When provided, the imported validator will use this gas limit. It is recommended to leave this as the default value by not specifying this diff --git a/common/eip_3076/Cargo.toml b/common/eip_3076/Cargo.toml index 058e1fd1a0..157fe12cb3 100644 --- a/common/eip_3076/Cargo.toml +++ b/common/eip_3076/Cargo.toml @@ -6,7 +6,7 @@ edition = { workspace = true } [features] default = [] -arbitrary-fuzz = ["dep:arbitrary", "types/arbitrary"] +arbitrary = ["dep:arbitrary", "types/arbitrary"] json = ["dep:serde_json"] [dependencies] diff --git a/common/eip_3076/src/lib.rs b/common/eip_3076/src/lib.rs index cdd05d7b1e..0bf1a94d0e 100644 --- a/common/eip_3076/src/lib.rs +++ b/common/eip_3076/src/lib.rs @@ -13,7 +13,7 @@ pub enum Error { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] #[serde(deny_unknown_fields)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct InterchangeMetadata { #[serde(with = "serde_utils::quoted_u64::require_quotes")] pub interchange_format_version: u64, @@ -22,7 +22,7 @@ pub struct InterchangeMetadata { #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] #[serde(deny_unknown_fields)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct InterchangeData { pub pubkey: PublicKeyBytes, pub signed_blocks: Vec<SignedBlock>, @@ -31,7 +31,7 @@ pub struct InterchangeData { #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] #[serde(deny_unknown_fields)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct SignedBlock { #[serde(with = "serde_utils::quoted_u64::require_quotes")] pub slot: Slot, @@ -41,7 +41,7 @@ pub struct SignedBlock { #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] #[serde(deny_unknown_fields)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct SignedAttestation { #[serde(with = "serde_utils::quoted_u64::require_quotes")] pub source_epoch: Epoch, @@ -52,7 +52,7 @@ pub struct SignedAttestation { } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct Interchange { pub metadata: InterchangeMetadata, pub data: Vec<InterchangeData>, diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index da8aba5ded..974508492a 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -8,19 +8,23 @@ edition = { workspace = true } default = [] lighthouse = ["proto_array", "eth2_keystore", "eip_3076", "zeroize"] events = ["reqwest-eventsource", "futures", "futures-util"] +network = ["libp2p-identity", "enr", "multiaddr"] [dependencies] bls = { workspace = true } context_deserialize = { workspace = true } educe = { workspace = true } eip_3076 = { workspace = true, optional = true } +enr = { version = "0.13.0", features = ["ed25519"], optional = true } eth2_keystore = { workspace = true, optional = true } ethereum_serde_utils = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } futures = { workspace = true, optional = true } futures-util = { version = "0.3.8", optional = true } +libp2p-identity = { version = "0.2", features = ["peerid"], optional = true } mediatype = "0.19.13" +multiaddr = { version = "0.18.2", optional = true } pretty_reqwest_error = { workspace = true } proto_array = { workspace = true, optional = true } reqwest = { workspace = true } diff --git a/common/eth2/src/error.rs b/common/eth2/src/error.rs index 671a617c9e..45f2599493 100644 --- a/common/eth2/src/error.rs +++ b/common/eth2/src/error.rs @@ -102,6 +102,7 @@ impl Error { None } } + #[cfg(feature = "events")] Error::SseEventSource(_) => None, Error::ServerMessage(msg) => StatusCode::try_from(msg.code).ok(), Error::ServerIndexedMessage(msg) => StatusCode::try_from(msg.code).ok(), diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 10382b028a..ac96da6173 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -22,8 +22,6 @@ pub use beacon_response::{ }; pub use self::error::{Error, ok_or_error, success_or_error}; -pub use reqwest; -pub use reqwest::{StatusCode, Url}; pub use sensitive_url::SensitiveUrl; use self::mixin::{RequestAccept, ResponseOptional}; @@ -35,14 +33,16 @@ use educe::Educe; use futures::Stream; #[cfg(feature = "events")] use futures_util::StreamExt; +#[cfg(feature = "network")] +use libp2p_identity::PeerId; use reqwest::{ - Body, IntoUrl, RequestBuilder, Response, + Body, IntoUrl, RequestBuilder, Response, StatusCode, Url, header::{HeaderMap, HeaderValue}, }; #[cfg(feature = "events")] use reqwest_eventsource::{Event, RequestBuilderExt}; use serde::{Serialize, de::DeserializeOwned}; -use ssz::Encode; +use ssz::{Decode, Encode}; use std::fmt; use std::future::Future; use std::time::Duration; @@ -50,6 +50,7 @@ use std::time::Duration; pub const V1: EndpointVersion = EndpointVersion(1); pub const V2: EndpointVersion = EndpointVersion(2); pub const V3: EndpointVersion = EndpointVersion(3); +pub const V4: EndpointVersion = EndpointVersion(4); pub const CONSENSUS_VERSION_HEADER: &str = "Eth-Consensus-Version"; pub const EXECUTION_PAYLOAD_BLINDED_HEADER: &str = "Eth-Execution-Payload-Blinded"; @@ -76,8 +77,6 @@ const HTTP_GET_BEACON_BLOCK_SSZ_TIMEOUT_QUOTIENT: u32 = 4; const HTTP_GET_DEBUG_BEACON_STATE_QUOTIENT: u32 = 4; const HTTP_GET_DEPOSIT_SNAPSHOT_QUOTIENT: u32 = 4; const HTTP_GET_VALIDATOR_BLOCK_TIMEOUT_QUOTIENT: u32 = 4; -// Generally the timeout for events should be longer than a slot. -const HTTP_GET_EVENTS_TIMEOUT_MULTIPLIER: u32 = 50; const HTTP_DEFAULT_TIMEOUT_QUOTIENT: u32 = 4; /// A struct to define a variety of different timeouts for different validator tasks to ensure @@ -98,7 +97,6 @@ pub struct Timeouts { pub get_debug_beacon_states: Duration, pub get_deposit_snapshot: Duration, pub get_validator_block: Duration, - pub events: Duration, pub default: Duration, } @@ -119,7 +117,6 @@ impl Timeouts { get_debug_beacon_states: timeout, get_deposit_snapshot: timeout, get_validator_block: timeout, - events: HTTP_GET_EVENTS_TIMEOUT_MULTIPLIER * timeout, default: timeout, } } @@ -142,7 +139,6 @@ impl Timeouts { get_debug_beacon_states: base_timeout / HTTP_GET_DEBUG_BEACON_STATE_QUOTIENT, get_deposit_snapshot: base_timeout / HTTP_GET_DEPOSIT_SNAPSHOT_QUOTIENT, get_validator_block: base_timeout / HTTP_GET_VALIDATOR_BLOCK_TIMEOUT_QUOTIENT, - events: HTTP_GET_EVENTS_TIMEOUT_MULTIPLIER * base_timeout, default: base_timeout / HTTP_DEFAULT_TIMEOUT_QUOTIENT, } } @@ -1939,6 +1935,7 @@ impl BeaconNodeHttpClient { } /// `GET node/identity` + #[cfg(feature = "network")] pub async fn get_node_identity(&self) -> Result<GenericResponse<IdentityData>, Error> { let mut path = self.eth_path(V1)?; @@ -1986,9 +1983,10 @@ impl BeaconNodeHttpClient { } /// `GET node/peers/{peer_id}` + #[cfg(feature = "network")] pub async fn get_node_peers_by_id( &self, - peer_id: &str, + peer_id: PeerId, ) -> Result<GenericResponse<PeerData>, Error> { let mut path = self.eth_path(V1)?; @@ -1996,7 +1994,7 @@ impl BeaconNodeHttpClient { .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("node") .push("peers") - .push(peer_id); + .push(&peer_id.to_string()); self.get(path).await } @@ -2404,6 +2402,277 @@ impl BeaconNodeHttpClient { opt_response.ok_or(Error::StatusCode(StatusCode::NOT_FOUND)) } + /// returns `GET v4/validator/blocks/{slot}` URL path + pub async fn get_validator_blocks_v4_path( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + skip_randao_verification: SkipRandaoVerification, + builder_booster_factor: Option<u64>, + graffiti_policy: Option<GraffitiPolicy>, + ) -> Result<Url, Error> { + let mut path = self.eth_path(V4)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("blocks") + .push(&slot.to_string()); + + path.query_pairs_mut() + .append_pair("randao_reveal", &randao_reveal.to_string()); + + if let Some(graffiti) = graffiti { + path.query_pairs_mut() + .append_pair("graffiti", &graffiti.to_string()); + } + + if skip_randao_verification == SkipRandaoVerification::Yes { + path.query_pairs_mut() + .append_pair("skip_randao_verification", ""); + } + + if let Some(builder_booster_factor) = builder_booster_factor { + path.query_pairs_mut() + .append_pair("builder_boost_factor", &builder_booster_factor.to_string()); + } + + if let Some(GraffitiPolicy::AppendClientVersions) = graffiti_policy { + path.query_pairs_mut() + .append_pair("graffiti_policy", "AppendClientVersions"); + } + + Ok(path) + } + + /// `GET v4/validator/blocks/{slot}` + pub async fn get_validator_blocks_v4<E: EthSpec>( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + builder_booster_factor: Option<u64>, + graffiti_policy: Option<GraffitiPolicy>, + ) -> Result< + ( + ForkVersionedResponse<BeaconBlock<E>, ProduceBlockV4Metadata>, + ProduceBlockV4Metadata, + ), + Error, + > { + self.get_validator_blocks_v4_modular( + slot, + randao_reveal, + graffiti, + SkipRandaoVerification::No, + builder_booster_factor, + graffiti_policy, + ) + .await + } + + /// `GET v4/validator/blocks/{slot}` + pub async fn get_validator_blocks_v4_modular<E: EthSpec>( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + skip_randao_verification: SkipRandaoVerification, + builder_booster_factor: Option<u64>, + graffiti_policy: Option<GraffitiPolicy>, + ) -> Result< + ( + ForkVersionedResponse<BeaconBlock<E>, ProduceBlockV4Metadata>, + ProduceBlockV4Metadata, + ), + Error, + > { + let path = self + .get_validator_blocks_v4_path( + slot, + randao_reveal, + graffiti, + skip_randao_verification, + builder_booster_factor, + graffiti_policy, + ) + .await?; + + let opt_result = self + .get_response_with_response_headers( + path, + Accept::Json, + self.timeouts.get_validator_block, + |response, headers| async move { + let header_metadata = ProduceBlockV4Metadata::try_from(&headers) + .map_err(Error::InvalidHeaders)?; + let block_response = response + .json::<ForkVersionedResponse<BeaconBlock<E>, ProduceBlockV4Metadata>>() + .await?; + Ok((block_response, header_metadata)) + }, + ) + .await?; + + opt_result.ok_or(Error::StatusCode(StatusCode::NOT_FOUND)) + } + + /// `GET v4/validator/blocks/{slot}` in ssz format + pub async fn get_validator_blocks_v4_ssz<E: EthSpec>( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + builder_booster_factor: Option<u64>, + graffiti_policy: Option<GraffitiPolicy>, + ) -> Result<(BeaconBlock<E>, ProduceBlockV4Metadata), Error> { + self.get_validator_blocks_v4_modular_ssz::<E>( + slot, + randao_reveal, + graffiti, + SkipRandaoVerification::No, + builder_booster_factor, + graffiti_policy, + ) + .await + } + + /// `GET v4/validator/blocks/{slot}` in ssz format + pub async fn get_validator_blocks_v4_modular_ssz<E: EthSpec>( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + skip_randao_verification: SkipRandaoVerification, + builder_booster_factor: Option<u64>, + graffiti_policy: Option<GraffitiPolicy>, + ) -> Result<(BeaconBlock<E>, ProduceBlockV4Metadata), Error> { + let path = self + .get_validator_blocks_v4_path( + slot, + randao_reveal, + graffiti, + skip_randao_verification, + builder_booster_factor, + graffiti_policy, + ) + .await?; + + let opt_response = self + .get_response_with_response_headers( + path, + Accept::Ssz, + self.timeouts.get_validator_block, + |response, headers| async move { + let metadata = ProduceBlockV4Metadata::try_from(&headers) + .map_err(Error::InvalidHeaders)?; + let response_bytes = response.bytes().await?; + + let block = BeaconBlock::from_ssz_bytes_for_fork( + &response_bytes, + metadata.consensus_version, + ) + .map_err(Error::InvalidSsz)?; + + Ok((block, metadata)) + }, + ) + .await?; + + opt_response.ok_or(Error::StatusCode(StatusCode::NOT_FOUND)) + } + + /// `GET v1/validator/execution_payload_envelope/{slot}/{builder_index}` + pub async fn get_validator_execution_payload_envelope<E: EthSpec>( + &self, + slot: Slot, + builder_index: u64, + ) -> Result<ForkVersionedResponse<ExecutionPayloadEnvelope<E>>, Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("execution_payload_envelope") + .push(&slot.to_string()) + .push(&builder_index.to_string()); + + self.get(path).await + } + + /// `GET v1/validator/execution_payload_envelope/{slot}/{builder_index}` in SSZ format + pub async fn get_validator_execution_payload_envelope_ssz<E: EthSpec>( + &self, + slot: Slot, + builder_index: u64, + ) -> Result<ExecutionPayloadEnvelope<E>, Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("execution_payload_envelope") + .push(&slot.to_string()) + .push(&builder_index.to_string()); + + let opt_response = self + .get_bytes_opt_accept_header(path, Accept::Ssz, self.timeouts.get_validator_block) + .await?; + + let response_bytes = opt_response.ok_or(Error::StatusCode(StatusCode::NOT_FOUND))?; + + ExecutionPayloadEnvelope::from_ssz_bytes(&response_bytes).map_err(Error::InvalidSsz) + } + + /// `POST v1/beacon/execution_payload_envelope` + pub async fn post_beacon_execution_payload_envelope<E: EthSpec>( + &self, + envelope: &SignedExecutionPayloadEnvelope<E>, + fork_name: ForkName, + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("execution_payload_envelope"); + + self.post_generic_with_consensus_version( + path, + envelope, + Some(self.timeouts.proposal), + fork_name, + ) + .await?; + + Ok(()) + } + + /// `POST v1/beacon/execution_payload_envelope` in SSZ format + pub async fn post_beacon_execution_payload_envelope_ssz<E: EthSpec>( + &self, + envelope: &SignedExecutionPayloadEnvelope<E>, + fork_name: ForkName, + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("execution_payload_envelope"); + + self.post_generic_with_consensus_version_and_ssz_body( + path, + envelope.as_ssz_bytes(), + Some(self.timeouts.proposal), + fork_name, + ) + .await?; + + Ok(()) + } + /// `GET v2/validator/blocks/{slot}` in ssz format pub async fn get_validator_blocks_ssz<E: EthSpec>( &self, @@ -2805,10 +3074,14 @@ impl BeaconNodeHttpClient { .join(","); path.query_pairs_mut().append_pair("topics", &topic_string); + // Do not use a timeout for the events endpoint. Using a regular timeout will trigger a + // timeout every `timeout` seconds, regardless of any data streamed from the endpoint. + // In future we could add a read_timeout, but that can only be configured globally on the + // Client. let mut es = self .client .get(path) - .timeout(self.timeouts.events) + .timeout(Duration::MAX) .eventsource() .map_err(Error::SseEventSource)?; // If we don't await `Event::Open` here, then the consumer diff --git a/common/eth2/src/lighthouse.rs b/common/eth2/src/lighthouse.rs index 993c263cbf..3c039b16b3 100644 --- a/common/eth2/src/lighthouse.rs +++ b/common/eth2/src/lighthouse.rs @@ -2,12 +2,11 @@ mod attestation_performance; mod block_packing_efficiency; -mod block_rewards; mod custody; pub mod sync_state; use crate::{ - BeaconNodeHttpClient, DepositData, Error, Hash256, Slot, + BeaconNodeHttpClient, DepositData, Error, Hash256, lighthouse::sync_state::SyncState, types::{AdminPeer, Epoch, GenericResponse, ValidatorId}, }; @@ -22,7 +21,6 @@ pub use attestation_performance::{ pub use block_packing_efficiency::{ BlockPackingEfficiency, BlockPackingEfficiencyQuery, ProposerInfo, UniqueAttestation, }; -pub use block_rewards::{AttestationRewards, BlockReward, BlockRewardMeta, BlockRewardsQuery}; pub use custody::CustodyInfo; // Define "legacy" implementations of `Option<T>` which use four bytes for encoding the union @@ -317,27 +315,6 @@ impl BeaconNodeHttpClient { Analysis endpoints. */ - /// `GET` lighthouse/analysis/block_rewards?start_slot,end_slot - pub async fn get_lighthouse_analysis_block_rewards( - &self, - start_slot: Slot, - end_slot: Slot, - ) -> Result<Vec<BlockReward>, Error> { - let mut path = self.server.expose_full().clone(); - - path.path_segments_mut() - .map_err(|()| Error::InvalidUrl(self.server.clone()))? - .push("lighthouse") - .push("analysis") - .push("block_rewards"); - - path.query_pairs_mut() - .append_pair("start_slot", &start_slot.to_string()) - .append_pair("end_slot", &end_slot.to_string()); - - self.get(path).await - } - /// `GET` lighthouse/analysis/block_packing?start_epoch,end_epoch pub async fn get_lighthouse_analysis_block_packing( &self, diff --git a/common/eth2/src/lighthouse/block_rewards.rs b/common/eth2/src/lighthouse/block_rewards.rs deleted file mode 100644 index 38070f3539..0000000000 --- a/common/eth2/src/lighthouse/block_rewards.rs +++ /dev/null @@ -1,60 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use types::{AttestationData, Hash256, Slot}; - -/// Details about the rewards paid to a block proposer for proposing a block. -/// -/// All rewards in GWei. -/// -/// Presently this only counts attestation rewards, but in future should be expanded -/// to include information on slashings and sync committee aggregates too. -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -pub struct BlockReward { - /// Sum of all reward components. - pub total: u64, - /// Block root of the block that these rewards are for. - pub block_root: Hash256, - /// Metadata about the block, particularly reward-relevant metadata. - pub meta: BlockRewardMeta, - /// Rewards due to attestations. - pub attestation_rewards: AttestationRewards, - /// Sum of rewards due to sync committee signatures. - pub sync_committee_rewards: u64, -} - -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -pub struct BlockRewardMeta { - pub slot: Slot, - pub parent_slot: Slot, - pub proposer_index: u64, - pub graffiti: String, -} - -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -pub struct AttestationRewards { - /// Total block reward from attestations included. - pub total: u64, - /// Total rewards from previous epoch attestations. - pub prev_epoch_total: u64, - /// Total rewards from current epoch attestations. - pub curr_epoch_total: u64, - /// Vec of attestation rewards for each attestation included. - /// - /// Each element of the vec is a map from validator index to reward. - pub per_attestation_rewards: Vec<HashMap<u64, u64>>, - /// The attestations themselves (optional). - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub attestations: Vec<AttestationData>, -} - -/// Query parameters for the `/lighthouse/block_rewards` endpoint. -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -pub struct BlockRewardsQuery { - /// Lower slot limit for block rewards returned (inclusive). - pub start_slot: Slot, - /// Upper slot limit for block rewards returned (inclusive). - pub end_slot: Slot, - /// Include the full attestations themselves? - #[serde(default)] - pub include_attestations: bool, -} diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index ba0d2ca3fb..ebd925fb8d 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -9,7 +9,11 @@ use crate::{ }; use bls::{PublicKeyBytes, SecretKey, Signature, SignatureBytes}; use context_deserialize::ContextDeserialize; +#[cfg(feature = "network")] +use enr::{CombinedKey, Enr}; use mediatype::{MediaType, MediaTypeList, names}; +#[cfg(feature = "network")] +use multiaddr::Multiaddr; use reqwest::header::HeaderMap; use serde::{Deserialize, Deserializer, Serialize}; use serde_utils::quoted_u64::Quoted; @@ -33,9 +37,6 @@ pub mod beacon_response { pub use crate::beacon_response::*; } -#[cfg(feature = "lighthouse")] -use crate::lighthouse::BlockReward; - // Re-export error types from the unified error module pub use crate::error::{ErrorMessage, Failure, IndexedErrorMessage, ResponseError as Error}; @@ -559,12 +560,13 @@ pub struct ChainHeadData { pub execution_optimistic: Option<bool>, } +#[cfg(feature = "network")] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct IdentityData { pub peer_id: String, - pub enr: String, - pub p2p_addresses: Vec<String>, - pub discovery_addresses: Vec<String>, + pub enr: Enr<CombinedKey>, + pub p2p_addresses: Vec<Multiaddr>, + pub discovery_addresses: Vec<Multiaddr>, pub metadata: MetaData, } @@ -1194,8 +1196,6 @@ pub enum EventKind<E: EthSpec> { LateHead(SseLateHead), LightClientFinalityUpdate(Box<BeaconResponse<LightClientFinalityUpdate<E>>>), LightClientOptimisticUpdate(Box<BeaconResponse<LightClientOptimisticUpdate<E>>>), - #[cfg(feature = "lighthouse")] - BlockReward(BlockReward), PayloadAttributes(VersionedSsePayloadAttributes), ProposerSlashing(Box<ProposerSlashing>), AttesterSlashing(Box<AttesterSlashing<E>>), @@ -1220,8 +1220,6 @@ impl<E: EthSpec> EventKind<E> { EventKind::LateHead(_) => "late_head", EventKind::LightClientFinalityUpdate(_) => "light_client_finality_update", EventKind::LightClientOptimisticUpdate(_) => "light_client_optimistic_update", - #[cfg(feature = "lighthouse")] - EventKind::BlockReward(_) => "block_reward", EventKind::ProposerSlashing(_) => "proposer_slashing", EventKind::AttesterSlashing(_) => "attester_slashing", EventKind::BlsToExecutionChange(_) => "bls_to_execution_change", @@ -1297,10 +1295,6 @@ impl<E: EthSpec> EventKind<E> { })?), ))) } - #[cfg(feature = "lighthouse")] - "block_reward" => Ok(EventKind::BlockReward(serde_json::from_str(data).map_err( - |e| ServerError::InvalidServerSentEvent(format!("Block Reward: {:?}", e)), - )?)), "attester_slashing" => Ok(EventKind::AttesterSlashing( serde_json::from_str(data).map_err(|e| { ServerError::InvalidServerSentEvent(format!("Attester Slashing: {:?}", e)) @@ -1350,8 +1344,6 @@ pub enum EventTopic { PayloadAttributes, LightClientFinalityUpdate, LightClientOptimisticUpdate, - #[cfg(feature = "lighthouse")] - BlockReward, AttesterSlashing, ProposerSlashing, BlsToExecutionChange, @@ -1377,8 +1369,6 @@ impl FromStr for EventTopic { "late_head" => Ok(EventTopic::LateHead), "light_client_finality_update" => Ok(EventTopic::LightClientFinalityUpdate), "light_client_optimistic_update" => Ok(EventTopic::LightClientOptimisticUpdate), - #[cfg(feature = "lighthouse")] - "block_reward" => Ok(EventTopic::BlockReward), "attester_slashing" => Ok(EventTopic::AttesterSlashing), "proposer_slashing" => Ok(EventTopic::ProposerSlashing), "bls_to_execution_change" => Ok(EventTopic::BlsToExecutionChange), @@ -1405,8 +1395,6 @@ impl fmt::Display for EventTopic { EventTopic::LateHead => write!(f, "late_head"), EventTopic::LightClientFinalityUpdate => write!(f, "light_client_finality_update"), EventTopic::LightClientOptimisticUpdate => write!(f, "light_client_optimistic_update"), - #[cfg(feature = "lighthouse")] - EventTopic::BlockReward => write!(f, "block_reward"), EventTopic::AttesterSlashing => write!(f, "attester_slashing"), EventTopic::ProposerSlashing => write!(f, "proposer_slashing"), EventTopic::BlsToExecutionChange => write!(f, "bls_to_execution_change"), @@ -1599,7 +1587,7 @@ pub struct BroadcastValidationQuery { } pub mod serde_status_code { - use crate::StatusCode; + use reqwest::StatusCode; use serde::{Deserialize, Serialize, de::Error}; pub fn serialize<S>(status_code: &StatusCode, ser: S) -> Result<S::Ok, S::Error> @@ -1718,7 +1706,7 @@ pub type JsonProduceBlockV3Response<E> = pub enum FullBlockContents<E: EthSpec> { /// This is a full deneb variant with block and blobs. BlockContents(BlockContents<E>), - /// This variant is for all pre-deneb full blocks. + /// This variant is for all pre-deneb full blocks or post-gloas beacon block. Block(BeaconBlock<E>), } @@ -1746,6 +1734,20 @@ pub struct ProduceBlockV3Metadata { pub consensus_block_value: Uint256, } +/// Metadata about a `produce_block_v4` response which is returned in the body & headers. +#[derive(Debug, Deserialize, Serialize)] +pub struct ProduceBlockV4Metadata { + // The consensus version is serialized & deserialized by `ForkVersionedResponse`. + #[serde( + skip_serializing, + skip_deserializing, + default = "dummy_consensus_version" + )] + pub consensus_version: ForkName, + #[serde(with = "serde_utils::u256_dec")] + pub consensus_block_value: Uint256, +} + impl<E: EthSpec> FullBlockContents<E> { pub fn new(block: BeaconBlock<E>, blob_data: Option<(KzgProofs<E>, BlobsList<E>)>) -> Self { match blob_data { @@ -1907,6 +1909,27 @@ impl TryFrom<&HeaderMap> for ProduceBlockV3Metadata { } } +impl TryFrom<&HeaderMap> for ProduceBlockV4Metadata { + type Error = String; + + fn try_from(headers: &HeaderMap) -> Result<Self, Self::Error> { + let consensus_version = parse_required_header(headers, CONSENSUS_VERSION_HEADER, |s| { + s.parse::<ForkName>() + .map_err(|e| format!("invalid {CONSENSUS_VERSION_HEADER}: {e:?}")) + })?; + let consensus_block_value = + parse_required_header(headers, CONSENSUS_BLOCK_VALUE_HEADER, |s| { + Uint256::from_str_radix(s, 10) + .map_err(|e| format!("invalid {CONSENSUS_BLOCK_VALUE_HEADER}: {e:?}")) + })?; + + Ok(ProduceBlockV4Metadata { + consensus_version, + consensus_block_value, + }) + } +} + /// A wrapper over a [`SignedBeaconBlock`] or a [`SignedBlockContents`]. #[derive(Clone, Debug, PartialEq, Encode, Serialize)] #[serde(untagged)] @@ -1954,7 +1977,7 @@ impl<E: EthSpec> PublishBlockRequest<E> { /// SSZ decode with fork variant determined by `fork_name`. pub fn from_ssz_bytes(bytes: &[u8], fork_name: ForkName) -> Result<Self, DecodeError> { - if fork_name.deneb_enabled() { + if fork_name.deneb_enabled() && !fork_name.gloas_enabled() { let mut builder = ssz::SszDecoderBuilder::new(bytes); builder.register_anonymous_variable_length_item()?; builder.register_type::<KzgProofs<E>>()?; diff --git a/common/health_metrics/src/observe.rs b/common/health_metrics/src/observe.rs index 81bb8e6f7e..5bc3770301 100644 --- a/common/health_metrics/src/observe.rs +++ b/common/health_metrics/src/observe.rs @@ -121,7 +121,7 @@ impl Observe for ProcessHealth { pid_mem_shared_memory_size: process_mem.shared(), pid_process_seconds_total: process_times.busy().as_secs() + process_times.children_system().as_secs() - + process_times.children_system().as_secs(), + + process_times.children_user().as_secs(), }) } } diff --git a/common/logging/Cargo.toml b/common/logging/Cargo.toml index 41c82dbd61..1606b8ceb4 100644 --- a/common/logging/Cargo.toml +++ b/common/logging/Cargo.toml @@ -5,7 +5,8 @@ authors = ["blacktemplar <blacktemplar@a1.net>"] edition = { workspace = true } [features] -test_logger = [] # Print log output to stderr when running tests instead of dropping it +# Print log output to stderr when running tests instead of dropping it. +test_logger = [] [dependencies] chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } @@ -13,7 +14,7 @@ logroller = { workspace = true } metrics = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -tokio = { workspace = true, features = [ "time" ] } +tokio = { workspace = true, features = ["time"] } tracing = { workspace = true } tracing-appender = { workspace = true } tracing-core = { workspace = true } diff --git a/common/malloc_utils/Cargo.toml b/common/malloc_utils/Cargo.toml index 1052128852..e90490bf09 100644 --- a/common/malloc_utils/Cargo.toml +++ b/common/malloc_utils/Cargo.toml @@ -35,7 +35,4 @@ tikv-jemallocator = { version = "0.6.0", optional = true, features = ["stats"] } # Jemalloc's background_threads feature requires Linux (pthreads). [target.'cfg(target_os = "linux")'.dependencies] -tikv-jemallocator = { version = "0.6.0", optional = true, features = [ - "stats", - "background_threads", -] } +tikv-jemallocator = { version = "0.6.0", optional = true, features = ["stats", "background_threads"] } diff --git a/common/warp_utils/Cargo.toml b/common/warp_utils/Cargo.toml index 32a540a69d..80bc247cbf 100644 --- a/common/warp_utils/Cargo.toml +++ b/common/warp_utils/Cargo.toml @@ -9,6 +9,7 @@ edition = { workspace = true } bytes = { workspace = true } eth2 = { workspace = true } headers = "0.3.2" +reqwest = { workspace = true } safe_arith = { workspace = true } serde = { workspace = true } serde_array_query = "0.1.0" diff --git a/common/warp_utils/src/status_code.rs b/common/warp_utils/src/status_code.rs index 1b05297359..a654b6d2c5 100644 --- a/common/warp_utils/src/status_code.rs +++ b/common/warp_utils/src/status_code.rs @@ -1,4 +1,4 @@ -use eth2::StatusCode; +use reqwest::StatusCode; use warp::Rejection; /// Convert from a "new" `http::StatusCode` to a `warp` compatible one. diff --git a/consensus/int_to_bytes/Cargo.toml b/consensus/int_to_bytes/Cargo.toml index c639dfce8d..75196d7437 100644 --- a/consensus/int_to_bytes/Cargo.toml +++ b/consensus/int_to_bytes/Cargo.toml @@ -9,4 +9,4 @@ bytes = { workspace = true } [dev-dependencies] hex = { workspace = true } -yaml-rust2 = "0.8" +yaml-rust2 = "0.11" diff --git a/consensus/state_processing/Cargo.toml b/consensus/state_processing/Cargo.toml index a08035d583..ae0af03231 100644 --- a/consensus/state_processing/Cargo.toml +++ b/consensus/state_processing/Cargo.toml @@ -5,11 +5,12 @@ authors = ["Paul Hauner <paul@paulhauner.com>", "Michael Sproul <michael@sigmapr edition = { workspace = true } [features] -default = ["legacy-arith"] +default = [] fake_crypto = ["bls/fake_crypto"] -legacy-arith = ["types/legacy-arith"] -arbitrary-fuzz = [ - "types/arbitrary-fuzz", +arbitrary = [ + "dep:arbitrary", + "smallvec/arbitrary", + "types/arbitrary", "merkle_proof/arbitrary", "ethereum_ssz/arbitrary", "ssz_types/arbitrary", @@ -18,7 +19,7 @@ arbitrary-fuzz = [ portable = ["bls/supranational-portable"] [dependencies] -arbitrary = { workspace = true } +arbitrary = { workspace = true, optional = true } bls = { workspace = true } educe = { workspace = true } ethereum_hashing = { workspace = true } diff --git a/consensus/state_processing/src/common/get_payload_attesting_indices.rs b/consensus/state_processing/src/common/get_payload_attesting_indices.rs new file mode 100644 index 0000000000..407e4f1372 --- /dev/null +++ b/consensus/state_processing/src/common/get_payload_attesting_indices.rs @@ -0,0 +1,42 @@ +use crate::per_block_processing::errors::{ + BlockOperationError, PayloadAttestationInvalid as Invalid, +}; +use ssz_types::VariableList; +use types::{ + BeaconState, BeaconStateError, ChainSpec, EthSpec, IndexedPayloadAttestation, + PayloadAttestation, +}; + +pub fn get_indexed_payload_attestation<E: EthSpec>( + state: &BeaconState<E>, + payload_attestation: &PayloadAttestation<E>, + spec: &ChainSpec, +) -> Result<IndexedPayloadAttestation<E>, BlockOperationError<Invalid>> { + let attesting_indices = get_payload_attesting_indices(state, payload_attestation, spec)?; + + Ok(IndexedPayloadAttestation { + attesting_indices: VariableList::new(attesting_indices)?, + data: payload_attestation.data.clone(), + signature: payload_attestation.signature.clone(), + }) +} + +pub fn get_payload_attesting_indices<E: EthSpec>( + state: &BeaconState<E>, + payload_attestation: &PayloadAttestation<E>, + spec: &ChainSpec, +) -> Result<Vec<u64>, BeaconStateError> { + let slot = payload_attestation.data.slot; + let ptc = state.get_ptc(slot, spec)?; + let bits = &payload_attestation.aggregation_bits; + + let mut attesting_indices = vec![]; + for (i, index) in ptc.into_iter().enumerate() { + if let Ok(true) = bits.get(i) { + attesting_indices.push(index as u64); + } + } + attesting_indices.sort_unstable(); + + Ok(attesting_indices) +} diff --git a/consensus/state_processing/src/common/mod.rs b/consensus/state_processing/src/common/mod.rs index 0287748fd0..e550a6c48b 100644 --- a/consensus/state_processing/src/common/mod.rs +++ b/consensus/state_processing/src/common/mod.rs @@ -1,6 +1,7 @@ mod deposit_data_tree; mod get_attestation_participation; mod get_attesting_indices; +mod get_payload_attesting_indices; mod initiate_validator_exit; mod slash_validator; @@ -13,6 +14,9 @@ pub use get_attestation_participation::get_attestation_participation_flag_indice pub use get_attesting_indices::{ attesting_indices_base, attesting_indices_electra, get_attesting_indices_from_state, }; +pub use get_payload_attesting_indices::{ + get_indexed_payload_attestation, get_payload_attesting_indices, +}; pub use initiate_validator_exit::initiate_validator_exit; pub use slash_validator::slash_validator; diff --git a/consensus/state_processing/src/consensus_context.rs b/consensus/state_processing/src/consensus_context.rs index 07d554e303..bc7bd20384 100644 --- a/consensus/state_processing/src/consensus_context.rs +++ b/consensus/state_processing/src/consensus_context.rs @@ -1,11 +1,16 @@ use crate::EpochCacheError; -use crate::common::{attesting_indices_base, attesting_indices_electra}; -use crate::per_block_processing::errors::{AttestationInvalid, BlockOperationError}; +use crate::common::{ + attesting_indices_base, attesting_indices_electra, get_indexed_payload_attestation, +}; +use crate::per_block_processing::errors::{ + AttestationInvalid, BlockOperationError, PayloadAttestationInvalid, +}; use std::collections::{HashMap, hash_map::Entry}; use tree_hash::TreeHash; use types::{ AbstractExecPayload, AttestationRef, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, - Hash256, IndexedAttestation, IndexedAttestationRef, SignedBeaconBlock, Slot, + Hash256, IndexedAttestation, IndexedAttestationRef, IndexedPayloadAttestation, + PayloadAttestation, SignedBeaconBlock, Slot, }; #[derive(Debug, PartialEq, Clone)] @@ -22,6 +27,8 @@ pub struct ConsensusContext<E: EthSpec> { pub current_block_root: Option<Hash256>, /// Cache of indexed attestations constructed during block processing. pub indexed_attestations: HashMap<Hash256, IndexedAttestation<E>>, + /// Cache of indexed payload attestations constructed during block processing. + pub indexed_payload_attestations: HashMap<Hash256, IndexedPayloadAttestation<E>>, } #[derive(Debug, PartialEq, Clone)] @@ -55,6 +62,7 @@ impl<E: EthSpec> ConsensusContext<E> { proposer_index: None, current_block_root: None, indexed_attestations: HashMap::new(), + indexed_payload_attestations: HashMap::new(), } } @@ -177,6 +185,24 @@ impl<E: EthSpec> ConsensusContext<E> { .map(|indexed_attestation| (*indexed_attestation).to_ref()) } + pub fn get_indexed_payload_attestation<'a>( + &'a mut self, + state: &BeaconState<E>, + payload_attestation: &'a PayloadAttestation<E>, + spec: &ChainSpec, + ) -> Result<&'a IndexedPayloadAttestation<E>, BlockOperationError<PayloadAttestationInvalid>> + { + let key = payload_attestation.tree_hash_root(); + match self.indexed_payload_attestations.entry(key) { + Entry::Occupied(occupied) => Ok(occupied.into_mut()), + Entry::Vacant(vacant) => { + let indexed_payload_attestation = + get_indexed_payload_attestation(state, payload_attestation, spec)?; + Ok(vacant.insert(indexed_payload_attestation)) + } + } + } + pub fn num_cached_indexed_attestations(&self) -> usize { self.indexed_attestations.len() } diff --git a/consensus/state_processing/src/envelope_processing.rs b/consensus/state_processing/src/envelope_processing.rs new file mode 100644 index 0000000000..be6b7c1b29 --- /dev/null +++ b/consensus/state_processing/src/envelope_processing.rs @@ -0,0 +1,298 @@ +use crate::BlockProcessingError; +use crate::VerifySignatures; +use crate::per_block_processing::compute_timestamp_at_slot; +use crate::per_block_processing::process_operations::{ + process_consolidation_requests, process_deposit_requests_post_gloas, + process_withdrawal_requests, +}; +use safe_arith::{ArithError, SafeArith}; +use tree_hash::TreeHash; +use types::{ + BeaconState, BeaconStateError, BuilderIndex, BuilderPendingPayment, ChainSpec, EthSpec, + ExecutionBlockHash, Hash256, SignedExecutionPayloadEnvelope, Slot, +}; + +macro_rules! envelope_verify { + ($condition: expr, $result: expr) => { + if !$condition { + return Err($result); + } + }; +} + +/// The strategy to be used when validating the payloads state root. +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(PartialEq, Clone, Copy)] +pub enum VerifyStateRoot { + /// Validate state root. + True, + /// Do not validate state root. Use with caution. + /// This should only be used when first constructing the payload envelope. + False, +} + +impl VerifyStateRoot { + pub fn is_true(self) -> bool { + self == VerifyStateRoot::True + } +} + +#[derive(Debug, Clone)] +pub enum EnvelopeProcessingError { + /// Bad Signature + BadSignature, + BeaconStateError(BeaconStateError), + BlockProcessingError(BlockProcessingError), + ArithError(ArithError), + /// Envelope doesn't match latest beacon block header + LatestBlockHeaderMismatch { + envelope_root: Hash256, + block_header_root: Hash256, + }, + /// Envelope doesn't match latest beacon block slot + SlotMismatch { + envelope_slot: Slot, + parent_state_slot: Slot, + }, + /// The payload withdrawals don't match the state's payload withdrawals. + WithdrawalsRootMismatch { + state: Hash256, + payload: Hash256, + }, + // The builder index doesn't match the committed bid. + BuilderIndexMismatch { + committed_bid: BuilderIndex, + envelope: BuilderIndex, + }, + // The gas limit doesn't match the committed bid + GasLimitMismatch { + committed_bid: u64, + envelope: u64, + }, + // The block hash doesn't match the committed bid + BlockHashMismatch { + committed_bid: ExecutionBlockHash, + envelope: ExecutionBlockHash, + }, + // The parent hash doesn't match the previous execution payload + ParentHashMismatch { + state: ExecutionBlockHash, + envelope: ExecutionBlockHash, + }, + // The previous randao didn't match the payload + PrevRandaoMismatch { + committed_bid: Hash256, + envelope: Hash256, + }, + // The timestamp didn't match the payload + TimestampMismatch { + state: u64, + envelope: u64, + }, + // Invalid state root + InvalidStateRoot { + state: Hash256, + envelope: Hash256, + }, + // BitFieldError + BitFieldError(ssz::BitfieldError), + // Some kind of error calculating the builder payment index + BuilderPaymentIndexOutOfBounds(usize), + /// The envelope was deemed invalid by the execution engine. + ExecutionInvalid, +} + +impl From<BeaconStateError> for EnvelopeProcessingError { + fn from(e: BeaconStateError) -> Self { + EnvelopeProcessingError::BeaconStateError(e) + } +} + +impl From<BlockProcessingError> for EnvelopeProcessingError { + fn from(e: BlockProcessingError) -> Self { + EnvelopeProcessingError::BlockProcessingError(e) + } +} + +impl From<ArithError> for EnvelopeProcessingError { + fn from(e: ArithError) -> Self { + EnvelopeProcessingError::ArithError(e) + } +} + +/// Processes a `SignedExecutionPayloadEnvelope` +/// +/// This function does all the state modifications inside `process_execution_payload()` +pub fn process_execution_payload_envelope<E: EthSpec>( + state: &mut BeaconState<E>, + parent_state_root: Option<Hash256>, + signed_envelope: &SignedExecutionPayloadEnvelope<E>, + verify_signatures: VerifySignatures, + verify_state_root: VerifyStateRoot, + spec: &ChainSpec, +) -> Result<(), EnvelopeProcessingError> { + if verify_signatures.is_true() { + // Verify Signed Envelope Signature + if !signed_envelope.verify_signature_with_state(state, spec)? { + return Err(EnvelopeProcessingError::BadSignature); + } + } + + let envelope = &signed_envelope.message; + let payload = &envelope.payload; + let execution_requests = &envelope.execution_requests; + + // Cache latest block header state root + if state.latest_block_header().state_root == Hash256::default() { + let previous_state_root = parent_state_root + .map(Ok) + .unwrap_or_else(|| state.canonical_root())?; + state.latest_block_header_mut().state_root = previous_state_root; + } + + // Verify consistency with the beacon block + let latest_block_header_root = state.latest_block_header().tree_hash_root(); + envelope_verify!( + envelope.beacon_block_root == latest_block_header_root, + EnvelopeProcessingError::LatestBlockHeaderMismatch { + envelope_root: envelope.beacon_block_root, + block_header_root: latest_block_header_root, + } + ); + envelope_verify!( + envelope.slot == state.slot(), + EnvelopeProcessingError::SlotMismatch { + envelope_slot: envelope.slot, + parent_state_slot: state.slot(), + } + ); + + // Verify consistency with the committed bid + let committed_bid = state.latest_execution_payload_bid()?; + envelope_verify!( + envelope.builder_index == committed_bid.builder_index, + EnvelopeProcessingError::BuilderIndexMismatch { + committed_bid: committed_bid.builder_index, + envelope: envelope.builder_index, + } + ); + envelope_verify!( + committed_bid.prev_randao == payload.prev_randao, + EnvelopeProcessingError::PrevRandaoMismatch { + committed_bid: committed_bid.prev_randao, + envelope: payload.prev_randao, + } + ); + + // Verify consistency with expected withdrawals + // NOTE: we don't bother hashing here except in case of error, because we can just compare for + // equality directly. This equality check could be more straight-forward if the types were + // changed to match (currently we are comparing VariableList to List). This could happen + // coincidentally when we adopt ProgressiveList. + envelope_verify!( + payload.withdrawals.len() == state.payload_expected_withdrawals()?.len() + && payload + .withdrawals + .iter() + .eq(state.payload_expected_withdrawals()?.iter()), + EnvelopeProcessingError::WithdrawalsRootMismatch { + state: state.payload_expected_withdrawals()?.tree_hash_root(), + payload: payload.withdrawals.tree_hash_root(), + } + ); + + // Verify the gas limit + envelope_verify!( + committed_bid.gas_limit == payload.gas_limit, + EnvelopeProcessingError::GasLimitMismatch { + committed_bid: committed_bid.gas_limit, + envelope: payload.gas_limit, + } + ); + + // Verify the block hash + envelope_verify!( + committed_bid.block_hash == payload.block_hash, + EnvelopeProcessingError::BlockHashMismatch { + committed_bid: committed_bid.block_hash, + envelope: payload.block_hash, + } + ); + + // Verify consistency of the parent hash with respect to the previous execution payload + envelope_verify!( + payload.parent_hash == *state.latest_block_hash()?, + EnvelopeProcessingError::ParentHashMismatch { + state: *state.latest_block_hash()?, + envelope: payload.parent_hash, + } + ); + + // Verify timestamp + let state_timestamp = compute_timestamp_at_slot(state, state.slot(), spec)?; + envelope_verify!( + payload.timestamp == state_timestamp, + EnvelopeProcessingError::TimestampMismatch { + state: state_timestamp, + envelope: payload.timestamp, + } + ); + + // TODO(gloas): newPayload happens here in the spec, ensure we wire that up correctly + + process_deposit_requests_post_gloas(state, &execution_requests.deposits, spec)?; + + // TODO(gloas): gotta update these + process_withdrawal_requests(state, &execution_requests.withdrawals, spec)?; + process_consolidation_requests(state, &execution_requests.consolidations, spec)?; + + // Queue the builder payment + let payment_index = E::slots_per_epoch() + .safe_add(state.slot().as_u64().safe_rem(E::slots_per_epoch())?)? + as usize; + let payment_mut = state + .builder_pending_payments_mut()? + .get_mut(payment_index) + .ok_or(EnvelopeProcessingError::BuilderPaymentIndexOutOfBounds( + payment_index, + ))?; + + // We have re-ordered the blanking out of the pending payment to avoid a double-lookup. + // This is semantically equivalent to the ordering used by the spec because we have taken a + // clone of the payment prior to doing the write. + let payment_withdrawal = payment_mut.withdrawal.clone(); + *payment_mut = BuilderPendingPayment::default(); + + let amount = payment_withdrawal.amount; + if amount > 0 { + state + .builder_pending_withdrawals_mut()? + .push(payment_withdrawal) + .map_err(|e| EnvelopeProcessingError::BeaconStateError(e.into()))?; + } + + // Cache the execution payload hash + let availability_index = state + .slot() + .as_usize() + .safe_rem(E::slots_per_historical_root())?; + state + .execution_payload_availability_mut()? + .set(availability_index, true) + .map_err(EnvelopeProcessingError::BitFieldError)?; + *state.latest_block_hash_mut()? = payload.block_hash; + + if verify_state_root.is_true() { + // Verify the state root + let state_root = state.canonical_root()?; + envelope_verify!( + envelope.state_root == state_root, + EnvelopeProcessingError::InvalidStateRoot { + state: state_root, + envelope: envelope.state_root, + } + ); + } + + Ok(()) +} diff --git a/consensus/state_processing/src/lib.rs b/consensus/state_processing/src/lib.rs index 9b2696c6d5..e37c526579 100644 --- a/consensus/state_processing/src/lib.rs +++ b/consensus/state_processing/src/lib.rs @@ -20,6 +20,7 @@ pub mod all_caches; pub mod block_replayer; pub mod common; pub mod consensus_context; +pub mod envelope_processing; pub mod epoch_cache; pub mod genesis; pub mod per_block_processing; diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 1de5083f6f..5aa610e98e 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -1,12 +1,17 @@ use crate::consensus_context::ConsensusContext; -use errors::{BlockOperationError, BlockProcessingError, HeaderInvalid}; +use errors::{ + BlockOperationError, BlockProcessingError, ExecutionPayloadBidInvalid, HeaderInvalid, +}; use rayon::prelude::*; use safe_arith::{ArithError, SafeArith}; -use signature_sets::{block_proposal_signature_set, get_pubkey_from_state, randao_signature_set}; +use signature_sets::{ + block_proposal_signature_set, execution_payload_bid_signature_set, + get_builder_pubkey_from_state, get_pubkey_from_state, randao_signature_set, +}; use std::borrow::Cow; use tree_hash::TreeHash; use typenum::Unsigned; -use types::*; +use types::{consts::gloas::BUILDER_INDEX_SELF_BUILD, *}; pub use self::verify_attester_slashing::{ get_slashable_indices, get_slashable_indices_modular, verify_attester_slashing, @@ -15,6 +20,7 @@ pub use self::verify_proposer_slashing::verify_proposer_slashing; pub use altair::sync_committee::process_sync_aggregate; pub use block_signature_verifier::{BlockSignatureVerifier, ParallelSignatureSets}; pub use is_valid_indexed_attestation::is_valid_indexed_attestation; +pub use is_valid_indexed_payload_attestation::is_valid_indexed_payload_attestation; pub use process_operations::process_operations; pub use verify_attestation::{ verify_attestation_for_block_inclusion, verify_attestation_for_state, @@ -32,6 +38,7 @@ pub mod builder; pub mod deneb; pub mod errors; mod is_valid_indexed_attestation; +mod is_valid_indexed_payload_attestation; pub mod process_operations; pub mod signature_sets; pub mod tests; @@ -40,6 +47,7 @@ mod verify_attester_slashing; mod verify_bls_to_execution_change; mod verify_deposit; mod verify_exit; +mod verify_payload_attestation; mod verify_proposer_slashing; pub mod withdrawals; @@ -47,12 +55,12 @@ use crate::common::update_progressive_balances_cache::{ initialize_progressive_balances_cache, update_progressive_balances_metrics, }; use crate::epoch_cache::initialize_epoch_cache; -#[cfg(feature = "arbitrary-fuzz")] +#[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; use tracing::instrument; /// The strategy to be used when validating the block's signatures. -#[cfg_attr(feature = "arbitrary-fuzz", derive(Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[derive(PartialEq, Clone, Copy, Debug)] pub enum BlockSignatureStrategy { /// Do not validate any signature. Use with caution. @@ -66,7 +74,7 @@ pub enum BlockSignatureStrategy { } /// The strategy to be used when validating the block's signatures. -#[cfg_attr(feature = "arbitrary-fuzz", derive(Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[derive(PartialEq, Clone, Copy)] pub enum VerifySignatures { /// Validate all signatures encountered. @@ -82,7 +90,7 @@ impl VerifySignatures { } /// Control verification of the latest block header. -#[cfg_attr(feature = "arbitrary-fuzz", derive(Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[derive(PartialEq, Clone, Copy)] pub enum VerifyBlockRoot { True, @@ -176,7 +184,7 @@ pub fn per_block_processing<E: EthSpec, Payload: AbstractExecPayload<E>>( let body = block.body(); if state.fork_name_unchecked().gloas_enabled() { withdrawals::gloas::process_withdrawals::<E>(state, spec)?; - // TODO(EIP-7732): process execution payload bid + process_execution_payload_bid(state, block, verify_signatures, spec)?; } else { if state.fork_name_unchecked().capella_enabled() { withdrawals::capella_electra::process_withdrawals::<E, Payload>( @@ -522,3 +530,162 @@ pub fn compute_timestamp_at_slot<E: EthSpec>( .safe_mul(spec.get_slot_duration().as_secs()) .and_then(|since_genesis| state.genesis_time().safe_add(since_genesis)) } + +pub fn can_builder_cover_bid<E: EthSpec>( + state: &BeaconState<E>, + builder_index: BuilderIndex, + builder: &Builder, + bid_amount: u64, + spec: &ChainSpec, +) -> Result<bool, BlockProcessingError> { + let builder_balance = builder.balance; + let pending_withdrawals_amount = + state.get_pending_balance_to_withdraw_for_builder(builder_index)?; + let min_balance = spec + .min_deposit_amount + .safe_add(pending_withdrawals_amount)?; + if builder_balance < min_balance { + Ok(false) + } else { + Ok(builder_balance.safe_sub(min_balance)? >= bid_amount) + } +} + +pub fn process_execution_payload_bid<E: EthSpec, Payload: AbstractExecPayload<E>>( + state: &mut BeaconState<E>, + block: BeaconBlockRef<'_, E, Payload>, + verify_signatures: VerifySignatures, + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> { + // Verify the bid signature + let signed_bid = block.body().signed_execution_payload_bid()?; + + let bid = &signed_bid.message; + let amount = bid.value; + let builder_index = bid.builder_index; + + // For self-builds, amount must be zero regardless of withdrawal credential prefix + if builder_index == BUILDER_INDEX_SELF_BUILD { + block_verify!( + amount == 0, + ExecutionPayloadBidInvalid::SelfBuildNonZeroAmount.into() + ); + block_verify!( + signed_bid.signature.is_infinity(), + ExecutionPayloadBidInvalid::BadSignature.into() + ); + } else { + let builder = state.get_builder(builder_index)?; + + // Verify that the builder is active + block_verify!( + builder.is_active_at_finalized_epoch(state.finalized_checkpoint().epoch, spec), + ExecutionPayloadBidInvalid::BuilderNotActive(builder_index).into() + ); + + // Verify that the builder has funds to cover the bid + block_verify!( + can_builder_cover_bid(state, builder_index, builder, amount, spec)?, + ExecutionPayloadBidInvalid::InsufficientBalance { + builder_index, + builder_balance: builder.balance, + bid_value: amount, + } + .into() + ); + + if verify_signatures.is_true() { + block_verify!( + // We know this is NOT a self-build, so there MUST be a signature set (func does not + // return None). + execution_payload_bid_signature_set( + state, + |i| get_builder_pubkey_from_state(state, i), + signed_bid, + spec + )? + .ok_or(ExecutionPayloadBidInvalid::BadSignature)? + .verify(), + ExecutionPayloadBidInvalid::BadSignature.into() + ); + } + } + + // Verify commitments are under limit + let max_blobs_per_block = spec.max_blobs_per_block(state.current_epoch()) as usize; + block_verify!( + bid.blob_kzg_commitments.len() <= max_blobs_per_block, + ExecutionPayloadBidInvalid::ExcessBlobCommitments { + max: max_blobs_per_block, + bid: bid.blob_kzg_commitments.len(), + } + .into() + ); + + // Verify that the bid is for the current slot + block_verify!( + bid.slot == block.slot(), + ExecutionPayloadBidInvalid::SlotMismatch { + bid_slot: bid.slot, + block_slot: block.slot(), + } + .into() + ); + + // Verify that the bid is for the right parent block + let latest_block_hash = state.latest_block_hash()?; + block_verify!( + bid.parent_block_hash == *latest_block_hash, + ExecutionPayloadBidInvalid::ParentBlockHashMismatch { + state_block_hash: *latest_block_hash, + bid_parent_hash: bid.parent_block_hash, + } + .into() + ); + + block_verify!( + bid.parent_block_root == block.parent_root(), + ExecutionPayloadBidInvalid::ParentBlockRootMismatch { + block_parent_root: block.parent_root(), + bid_parent_root: bid.parent_block_root, + } + .into() + ); + + let expected_randao = *state.get_randao_mix(state.current_epoch())?; + block_verify!( + bid.prev_randao == expected_randao, + ExecutionPayloadBidInvalid::PrevRandaoMismatch { + expected: expected_randao, + bid: bid.prev_randao, + } + .into() + ); + + // Record the pending payment if there is some payment + if amount > 0 { + let pending_payment = BuilderPendingPayment { + weight: 0, + withdrawal: BuilderPendingWithdrawal { + fee_recipient: bid.fee_recipient, + amount, + builder_index, + }, + }; + + let payment_index = E::SlotsPerEpoch::to_usize() + .safe_add(bid.slot.as_usize().safe_rem(E::SlotsPerEpoch::to_usize())?)?; + + *state + .builder_pending_payments_mut()? + .get_mut(payment_index) + .ok_or(BlockProcessingError::BeaconStateError( + BeaconStateError::InvalidBuilderPendingPaymentsIndex(payment_index), + ))? = pending_payment; + } + + // Cache the execution bid + *state.latest_execution_payload_bid_mut()? = bid.clone(); + + Ok(()) +} diff --git a/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs b/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs index 9aa44137d8..eea9c17a14 100644 --- a/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs +++ b/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs @@ -1,7 +1,9 @@ #![allow(clippy::arithmetic_side_effects)] use super::signature_sets::{Error as SignatureSetError, *}; -use crate::per_block_processing::errors::{AttestationInvalid, BlockOperationError}; +use crate::per_block_processing::errors::{ + AttestationInvalid, BlockOperationError, PayloadAttestationInvalid, +}; use crate::{ConsensusContext, ContextError}; use bls::{PublicKey, PublicKeyBytes, SignatureSet, verify_signature_sets}; use std::borrow::Cow; @@ -18,6 +20,8 @@ pub enum Error { SignatureInvalid, /// An attestation in the block was invalid. The block is invalid. AttestationValidationError(BlockOperationError<AttestationInvalid>), + /// A payload attestation in the block was invalid. The block is invalid. + PayloadAttestationValidationError(BlockOperationError<PayloadAttestationInvalid>), /// There was an error attempting to read from a `BeaconState`. Block /// validity was not determined. BeaconStateError(BeaconStateError), @@ -66,6 +70,12 @@ impl From<BlockOperationError<AttestationInvalid>> for Error { } } +impl From<BlockOperationError<PayloadAttestationInvalid>> for Error { + fn from(e: BlockOperationError<PayloadAttestationInvalid>) -> Error { + Error::PayloadAttestationValidationError(e) + } +} + /// Reads the BLS signatures and keys from a `SignedBeaconBlock`, storing them as a `Vec<SignatureSet>`. /// /// This allows for optimizations related to batch BLS operations (see the @@ -170,6 +180,8 @@ where self.include_exits(block)?; self.include_sync_aggregate(block)?; self.include_bls_to_execution_changes(block)?; + self.include_execution_payload_bid(block)?; + self.include_payload_attestations(block, ctxt)?; Ok(()) } @@ -295,6 +307,39 @@ where }) } + /// Includes all signatures in `self.block.body.payload_attestations` for verification. + pub fn include_payload_attestations<Payload: AbstractExecPayload<E>>( + &mut self, + block: &'a SignedBeaconBlock<E, Payload>, + ctxt: &mut ConsensusContext<E>, + ) -> Result<()> { + let Ok(payload_attestations) = block.message().body().payload_attestations() else { + // Nothing to do pre-Gloas. + return Ok(()); + }; + + self.sets.sets.reserve(payload_attestations.len()); + + payload_attestations + .iter() + .try_for_each(|payload_attestation| { + let indexed_payload_attestation = ctxt.get_indexed_payload_attestation( + self.state, + payload_attestation, + self.spec, + )?; + + self.sets.push(indexed_payload_attestation_signature_set( + self.state, + self.get_pubkey.clone(), + &payload_attestation.signature, + indexed_payload_attestation, + self.spec, + )?); + Ok(()) + }) + } + /// Includes all signatures in `self.block.body.voluntary_exits` for verification. pub fn include_exits<Payload: AbstractExecPayload<E>>( &mut self, @@ -357,6 +402,27 @@ where Ok(()) } + /// Include the signature of the block's execution payload bid. + pub fn include_execution_payload_bid<Payload: AbstractExecPayload<E>>( + &mut self, + block: &'a SignedBeaconBlock<E, Payload>, + ) -> Result<()> { + if let Ok(signed_execution_payload_bid) = + block.message().body().signed_execution_payload_bid() + { + // TODO(gloas): if we implement a global builder pubkey cache we need to inject it here + if let Some(signature_set) = execution_payload_bid_signature_set( + self.state, + |builder_index| get_builder_pubkey_from_state(self.state, builder_index), + signed_execution_payload_bid, + self.spec, + )? { + self.sets.push(signature_set); + } + } + Ok(()) + } + /// Verify all the signatures that have been included in `self`, returning `true` if and only if /// all the signatures are valid. /// diff --git a/consensus/state_processing/src/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index 5c1db9d732..71083378db 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -41,6 +41,10 @@ pub enum BlockProcessingError { index: usize, reason: AttestationInvalid, }, + PayloadAttestationInvalid { + index: usize, + reason: PayloadAttestationInvalid, + }, DepositInvalid { index: usize, reason: DepositInvalid, @@ -99,6 +103,9 @@ pub enum BlockProcessingError { IncorrectExpectedWithdrawalsVariant, MissingLastWithdrawal, PendingAttestationInElectra, + ExecutionPayloadBidInvalid { + reason: ExecutionPayloadBidInvalid, + }, /// Builder payment index out of bounds (Gloas) BuilderPaymentIndexOutOfBounds(usize), } @@ -157,6 +164,12 @@ impl From<milhouse::Error> for BlockProcessingError { } } +impl From<ExecutionPayloadBidInvalid> for BlockProcessingError { + fn from(reason: ExecutionPayloadBidInvalid) -> Self { + Self::ExecutionPayloadBidInvalid { reason } + } +} + impl From<BlockOperationError<HeaderInvalid>> for BlockProcessingError { fn from(e: BlockOperationError<HeaderInvalid>) -> BlockProcessingError { match e { @@ -208,6 +221,7 @@ impl_into_block_processing_error_with_index!( AttesterSlashingInvalid, IndexedAttestationInvalid, AttestationInvalid, + PayloadAttestationInvalid, DepositInvalid, ExitInvalid, BlsExecutionChangeInvalid @@ -413,6 +427,52 @@ pub enum IndexedAttestationInvalid { SignatureSetError(SignatureSetError), } +#[derive(Debug, PartialEq, Clone)] +pub enum PayloadAttestationInvalid { + /// Block root does not match the parent beacon block root. + BlockRootMismatch { + expected: Hash256, + found: Hash256, + }, + /// The attestation slot is not the previous slot. + SlotMismatch { + expected: Slot, + found: Slot, + }, + BadIndexedPayloadAttestation(IndexedPayloadAttestationInvalid), +} + +impl From<BlockOperationError<IndexedPayloadAttestationInvalid>> + for BlockOperationError<PayloadAttestationInvalid> +{ + fn from(e: BlockOperationError<IndexedPayloadAttestationInvalid>) -> Self { + match e { + BlockOperationError::Invalid(e) => BlockOperationError::invalid( + PayloadAttestationInvalid::BadIndexedPayloadAttestation(e), + ), + BlockOperationError::BeaconStateError(e) => BlockOperationError::BeaconStateError(e), + BlockOperationError::SignatureSetError(e) => BlockOperationError::SignatureSetError(e), + BlockOperationError::SszTypesError(e) => BlockOperationError::SszTypesError(e), + BlockOperationError::BitfieldError(e) => BlockOperationError::BitfieldError(e), + BlockOperationError::ConsensusContext(e) => BlockOperationError::ConsensusContext(e), + BlockOperationError::ArithError(e) => BlockOperationError::ArithError(e), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum IndexedPayloadAttestationInvalid { + /// The number of indices is 0. + IndicesEmpty, + /// The validator indices were not in increasing order. + BadValidatorIndicesOrdering, + /// The indexed attestation aggregate signature was not valid. + BadSignature, + /// There was an error whilst attempting to get a set of signatures. The signatures may have + /// been invalid or an internal error occurred. + SignatureSetError(SignatureSetError), +} + #[derive(Debug, PartialEq, Clone)] pub enum DepositInvalid { /// The signature (proof-of-possession) does not match the given pubkey. @@ -452,6 +512,38 @@ pub enum ExitInvalid { PendingWithdrawalInQueue(u64), } +#[derive(Debug, PartialEq, Clone)] +pub enum ExecutionPayloadBidInvalid { + /// The validator set a non-zero amount for a self-build. + SelfBuildNonZeroAmount, + /// The signature is invalid. + BadSignature, + /// The builder is not active. + BuilderNotActive(u64), + /// The builder has insufficient balance to cover the bid + InsufficientBalance { + builder_index: u64, + builder_balance: u64, + bid_value: u64, + }, + /// Bid slot doesn't match block slot + SlotMismatch { bid_slot: Slot, block_slot: Slot }, + /// The bid's parent block hash doesn't match the state's latest block hash + ParentBlockHashMismatch { + state_block_hash: ExecutionBlockHash, + bid_parent_hash: ExecutionBlockHash, + }, + /// The bid's parent block root doesn't match the block's parent root + ParentBlockRootMismatch { + block_parent_root: Hash256, + bid_parent_root: Hash256, + }, + /// The bid's prev randao doesn't match the state. + PrevRandaoMismatch { expected: Hash256, bid: Hash256 }, + /// The bid contains more than the maximum number of kzg blob commitments. + ExcessBlobCommitments { max: usize, bid: usize }, +} + #[derive(Debug, PartialEq, Clone)] pub enum BlsExecutionChangeInvalid { /// The specified validator is not in the state's validator registry. diff --git a/consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs b/consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs new file mode 100644 index 0000000000..534f553247 --- /dev/null +++ b/consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs @@ -0,0 +1,32 @@ +use super::errors::{BlockOperationError, IndexedPayloadAttestationInvalid as Invalid}; +use super::signature_sets::{get_pubkey_from_state, indexed_payload_attestation_signature_set}; +use crate::VerifySignatures; +use types::*; + +pub fn is_valid_indexed_payload_attestation<E: EthSpec>( + state: &BeaconState<E>, + indexed_payload_attestation: &IndexedPayloadAttestation<E>, + verify_signatures: VerifySignatures, + spec: &ChainSpec, +) -> Result<(), BlockOperationError<Invalid>> { + // Verify indices are non-empty and sorted (duplicates allowed) + let indices = &indexed_payload_attestation.attesting_indices; + verify!(!indices.is_empty(), Invalid::IndicesEmpty); + verify!(indices.is_sorted(), Invalid::BadValidatorIndicesOrdering); + + if verify_signatures.is_true() { + verify!( + indexed_payload_attestation_signature_set( + state, + |i| get_pubkey_from_state(state, i), + &indexed_payload_attestation.signature, + indexed_payload_attestation, + spec + )? + .verify(), + Invalid::BadSignature + ); + } + + Ok(()) +} diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index 9db439b543..9743812632 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -5,6 +5,8 @@ use crate::common::{ slash_validator, }; use crate::per_block_processing::errors::{BlockProcessingError, IntoWithIndex}; +use crate::per_block_processing::verify_payload_attestation::verify_payload_attestation; +use bls::{PublicKeyBytes, SignatureBytes}; use ssz_types::FixedVector; use typenum::U33; use types::consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}; @@ -38,9 +40,21 @@ pub fn process_operations<E: EthSpec, Payload: AbstractExecPayload<E>>( process_bls_to_execution_changes(state, bls_to_execution_changes, verify_signatures, spec)?; } - if state.fork_name_unchecked().electra_enabled() { + if state.fork_name_unchecked().gloas_enabled() { + process_payload_attestations( + state, + block_body.payload_attestations()?.iter(), + verify_signatures, + ctxt, + spec, + )?; + } else if state.fork_name_unchecked().electra_enabled() { state.update_pubkey_cache()?; - process_deposit_requests(state, &block_body.execution_requests()?.deposits, spec)?; + process_deposit_requests_pre_gloas( + state, + &block_body.execution_requests()?.deposits, + spec, + )?; process_withdrawal_requests(state, &block_body.execution_requests()?.withdrawals, spec)?; process_consolidation_requests( state, @@ -377,6 +391,31 @@ pub fn process_proposer_slashings<E: EthSpec>( verify_proposer_slashing(proposer_slashing, state, verify_signatures, spec) .map_err(|e| e.into_with_index(i))?; + // [New in Gloas:EIP7732] + // Remove the BuilderPendingPayment corresponding to this proposal + // if it is still in the 2-epoch window. + if state.fork_name_unchecked().gloas_enabled() { + let slot = proposer_slashing.signed_header_1.message.slot; + let proposal_epoch = slot.epoch(E::slots_per_epoch()); + let slot_in_epoch = slot.as_usize().safe_rem(E::SlotsPerEpoch::to_usize())?; + + let payment_index = if proposal_epoch == state.current_epoch() { + Some(E::SlotsPerEpoch::to_usize().safe_add(slot_in_epoch)?) + } else if proposal_epoch == state.previous_epoch() { + Some(slot_in_epoch) + } else { + None + }; + + if let Some(index) = payment_index { + let payment = state + .builder_pending_payments_mut()? + .get_mut(index) + .ok_or(BlockProcessingError::BuilderPaymentIndexOutOfBounds(index))?; + *payment = BuilderPendingPayment::default(); + } + } + slash_validator( state, proposer_slashing.signed_header_1.message.proposer_index as usize, @@ -736,7 +775,7 @@ pub fn process_withdrawal_requests<E: EthSpec>( Ok(()) } -pub fn process_deposit_requests<E: EthSpec>( +pub fn process_deposit_requests_pre_gloas<E: EthSpec>( state: &mut BeaconState<E>, deposit_requests: &[DepositRequest], spec: &ChainSpec, @@ -763,6 +802,112 @@ pub fn process_deposit_requests<E: EthSpec>( Ok(()) } +pub fn process_deposit_requests_post_gloas<E: EthSpec>( + state: &mut BeaconState<E>, + deposit_requests: &[DepositRequest], + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> { + for request in deposit_requests { + process_deposit_request_post_gloas(state, request, spec)?; + } + + Ok(()) +} + +pub fn process_deposit_request_post_gloas<E: EthSpec>( + state: &mut BeaconState<E>, + deposit_request: &DepositRequest, + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> { + // [New in Gloas:EIP7732] + // Regardless of the withdrawal credentials prefix, if a builder/validator + // already exists with this pubkey, apply the deposit to their balance + // TODO(gloas): this could be more efficient in the builder case, see: + // https://github.com/sigp/lighthouse/issues/8783 + let builder_index = state + .builders()? + .iter() + .enumerate() + .find(|(_, builder)| builder.pubkey == deposit_request.pubkey) + .map(|(i, _)| i as u64); + let is_builder = builder_index.is_some(); + + let validator_index = state.get_validator_index(&deposit_request.pubkey)?; + let is_validator = validator_index.is_some(); + + let is_builder_prefix = + is_builder_withdrawal_credential(deposit_request.withdrawal_credentials, spec); + + if is_builder || (is_builder_prefix && !is_validator) { + // Apply builder deposits immediately + apply_deposit_for_builder( + state, + builder_index, + deposit_request.pubkey, + deposit_request.withdrawal_credentials, + deposit_request.amount, + deposit_request.signature.clone(), + state.slot(), + spec, + )?; + return Ok(()); + } + + // Add validator deposits to the queue + let slot = state.slot(); + state.pending_deposits_mut()?.push(PendingDeposit { + pubkey: deposit_request.pubkey, + withdrawal_credentials: deposit_request.withdrawal_credentials, + amount: deposit_request.amount, + signature: deposit_request.signature.clone(), + slot, + })?; + + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +pub fn apply_deposit_for_builder<E: EthSpec>( + state: &mut BeaconState<E>, + builder_index_opt: Option<BuilderIndex>, + pubkey: PublicKeyBytes, + withdrawal_credentials: Hash256, + amount: u64, + signature: SignatureBytes, + slot: Slot, + spec: &ChainSpec, +) -> Result<(), BeaconStateError> { + match builder_index_opt { + None => { + // Verify the deposit signature (proof of possession) which is not checked by the deposit contract + let deposit_data = DepositData { + pubkey, + withdrawal_credentials, + amount, + signature, + }; + if is_valid_deposit_signature(&deposit_data, spec).is_ok() { + state.add_builder_to_registry( + pubkey, + withdrawal_credentials, + amount, + slot, + spec, + )?; + } + } + Some(builder_index) => { + state + .builders_mut()? + .get_mut(builder_index as usize) + .ok_or(BeaconStateError::UnknownBuilder(builder_index))? + .balance + .safe_add_assign(amount)?; + } + } + Ok(()) +} + // Make sure to build the pubkey cache before calling this function pub fn process_consolidation_requests<E: EthSpec>( state: &mut BeaconState<E>, @@ -937,3 +1082,45 @@ pub fn process_consolidation_request<E: EthSpec>( Ok(()) } + +pub fn process_payload_attestation<E: EthSpec>( + state: &mut BeaconState<E>, + payload_attestation: &PayloadAttestation<E>, + att_index: usize, + verify_signatures: VerifySignatures, + ctxt: &mut ConsensusContext<E>, + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> { + verify_payload_attestation(state, payload_attestation, ctxt, verify_signatures, spec) + .map_err(|e| e.into_with_index(att_index)) +} + +pub fn process_payload_attestations<'a, E: EthSpec, I>( + state: &mut BeaconState<E>, + payload_attestations: I, + verify_signatures: VerifySignatures, + ctxt: &mut ConsensusContext<E>, + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> +where + I: Iterator<Item = &'a PayloadAttestation<E>>, +{ + // Presently the PTC cache requires the committee cache for `state.slot() - 1` which is either + // in the current or previous epoch. + // TODO(gloas): These requirements may change if we introduce a PTC cache. + state.build_committee_cache(RelativeEpoch::Current, spec)?; + state.build_committee_cache(RelativeEpoch::Previous, spec)?; + + payload_attestations + .enumerate() + .try_for_each(|(i, payload_attestation)| { + process_payload_attestation( + state, + payload_attestation, + i, + verify_signatures, + ctxt, + spec, + ) + }) +} diff --git a/consensus/state_processing/src/per_block_processing/signature_sets.rs b/consensus/state_processing/src/per_block_processing/signature_sets.rs index 0e936007ee..71ee1f8993 100644 --- a/consensus/state_processing/src/per_block_processing/signature_sets.rs +++ b/consensus/state_processing/src/per_block_processing/signature_sets.rs @@ -9,11 +9,12 @@ use tree_hash::TreeHash; use typenum::Unsigned; use types::{ AbstractExecPayload, AttesterSlashingRef, BeaconBlockRef, BeaconState, BeaconStateError, - ChainSpec, DepositData, Domain, Epoch, EthSpec, Fork, Hash256, InconsistentFork, - IndexedAttestation, IndexedAttestationRef, ProposerSlashing, SignedAggregateAndProof, - SignedBeaconBlock, SignedBeaconBlockHeader, SignedBlsToExecutionChange, - SignedContributionAndProof, SignedRoot, SignedVoluntaryExit, SigningData, Slot, SyncAggregate, - SyncAggregatorSelectionData, + BuilderIndex, ChainSpec, DepositData, Domain, Epoch, EthSpec, Fork, Hash256, InconsistentFork, + IndexedAttestation, IndexedAttestationRef, IndexedPayloadAttestation, ProposerSlashing, + SignedAggregateAndProof, SignedBeaconBlock, SignedBeaconBlockHeader, + SignedBlsToExecutionChange, SignedContributionAndProof, SignedExecutionPayloadBid, SignedRoot, + SignedVoluntaryExit, SigningData, Slot, SyncAggregate, SyncAggregatorSelectionData, + consts::gloas::BUILDER_INDEX_SELF_BUILD, }; pub type Result<T> = std::result::Result<T, Error>; @@ -28,6 +29,9 @@ pub enum Error { /// Attempted to find the public key of a validator that does not exist. You cannot distinguish /// between an error and an invalid block in this case. ValidatorUnknown(u64), + /// Attempted to find the public key of a builder that does not exist. You cannot distinguish + /// between an error and an invalid block in this case. + BuilderUnknown(BuilderIndex), /// Attempted to find the public key of a validator that does not exist. You cannot distinguish /// between an error and an invalid block in this case. ValidatorPubkeyUnknown(PublicKeyBytes), @@ -53,7 +57,7 @@ impl From<BeaconStateError> for Error { } } -/// Helper function to get a public key from a `state`. +/// Helper function to get a validator public key from a `state`. pub fn get_pubkey_from_state<E>( state: &BeaconState<E>, validator_index: usize, @@ -71,6 +75,25 @@ where .map(Cow::Owned) } +/// Helper function to get a builder public key from a `state`. +pub fn get_builder_pubkey_from_state<E>( + state: &BeaconState<E>, + builder_index: BuilderIndex, +) -> Option<Cow<'_, PublicKey>> +where + E: EthSpec, +{ + state + .builders() + .ok()? + .get(builder_index as usize) + .and_then(|b| { + let pk: Option<PublicKey> = b.pubkey.decompress().ok(); + pk + }) + .map(Cow::Owned) +} + /// A signature set that is valid if a block was signed by the expected block producer. pub fn block_proposal_signature_set<'a, E, F, Payload: AbstractExecPayload<E>>( state: &'a BeaconState<E>, @@ -332,6 +355,75 @@ where Ok(SignatureSet::multiple_pubkeys(signature, pubkeys, message)) } +pub fn indexed_payload_attestation_signature_set<'a, 'b, E, F>( + state: &'a BeaconState<E>, + get_pubkey: F, + signature: &'a AggregateSignature, + indexed_payload_attestation: &'b IndexedPayloadAttestation<E>, + spec: &'a ChainSpec, +) -> Result<SignatureSet<'a>> +where + E: EthSpec, + F: Fn(usize) -> Option<Cow<'a, PublicKey>>, +{ + let mut pubkeys = Vec::with_capacity(indexed_payload_attestation.attesting_indices.len()); + for &validator_idx in indexed_payload_attestation.attesting_indices.iter() { + pubkeys.push( + get_pubkey(validator_idx as usize).ok_or(Error::ValidatorUnknown(validator_idx))?, + ); + } + + let epoch = indexed_payload_attestation + .data + .slot + .epoch(E::slots_per_epoch()); + let domain = spec.get_domain( + epoch, + Domain::PTCAttester, + &state.fork(), + state.genesis_validators_root(), + ); + + let message = indexed_payload_attestation.data.signing_root(domain); + + Ok(SignatureSet::multiple_pubkeys(signature, pubkeys, message)) +} + +pub fn execution_payload_bid_signature_set<'a, E, F>( + state: &'a BeaconState<E>, + get_builder_pubkey: F, + signed_execution_payload_bid: &'a SignedExecutionPayloadBid<E>, + spec: &'a ChainSpec, +) -> Result<Option<SignatureSet<'a>>> +where + E: EthSpec, + F: Fn(BuilderIndex) -> Option<Cow<'a, PublicKey>>, +{ + let execution_payload_bid = &signed_execution_payload_bid.message; + let builder_index = execution_payload_bid.builder_index; + if builder_index == BUILDER_INDEX_SELF_BUILD { + // No signatures to verify in case of a self-build, but consensus code MUST check that + // the signature is the point at infinity. + // See `process_execution_payload_bid`. + return Ok(None); + } + let domain = spec.get_domain( + state.current_epoch(), + Domain::BeaconBuilder, + &state.fork(), + state.genesis_validators_root(), + ); + + let pubkey = get_builder_pubkey(builder_index).ok_or(Error::BuilderUnknown(builder_index))?; + let message = execution_payload_bid.signing_root(domain); + + Ok(Some(SignatureSet::single_pubkey( + &signed_execution_payload_bid.signature, + pubkey, + message, + ))) +} + /// Returns the signature set for the given `attester_slashing` and corresponding `pubkeys`. pub fn attester_slashing_signature_sets<'a, E, F>( state: &'a BeaconState<E>, diff --git a/consensus/state_processing/src/per_block_processing/tests.rs b/consensus/state_processing/src/per_block_processing/tests.rs index 739717b33f..96610c2010 100644 --- a/consensus/state_processing/src/per_block_processing/tests.rs +++ b/consensus/state_processing/src/per_block_processing/tests.rs @@ -8,7 +8,7 @@ use crate::per_block_processing::errors::{ use crate::{BlockReplayError, BlockReplayer, per_block_processing}; use crate::{ BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, VerifySignatures, - per_block_processing::{process_operations, verify_exit::verify_exit}, + per_block_processing::process_operations, }; use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; use bls::{AggregateSignature, Keypair, PublicKeyBytes, Signature, SignatureBytes}; @@ -39,10 +39,13 @@ async fn get_harness<E: EthSpec>( // Set the state and block to be in the last slot of the `epoch_offset`th epoch. let last_slot_of_epoch = (MainnetEthSpec::genesis_epoch() + epoch_offset).end_slot(E::slots_per_epoch()); + // Use Electra spec to ensure blocks are created at the same fork as the state + let spec = Arc::new(ForkName::Electra.make_genesis_spec(E::default_spec())); let harness = BeaconChainHarness::<EphemeralHarnessType<E>>::builder(E::default()) - .default_spec() + .spec(spec.clone()) .keypairs(KEYPAIRS[0..num_validators].to_vec()) .fresh_ephemeral_store() + .mock_execution_layer() .build(); let state = harness.get_current_state(); if last_slot_of_epoch > Slot::new(0) { @@ -63,8 +66,8 @@ async fn get_harness<E: EthSpec>( #[tokio::test] async fn valid_block_ok() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let state = harness.get_current_state(); let slot = state.slot(); @@ -87,8 +90,8 @@ async fn valid_block_ok() { #[tokio::test] async fn invalid_block_header_state_slot() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let state = harness.get_current_state(); let slot = state.slot() + Slot::new(1); @@ -107,18 +110,18 @@ async fn invalid_block_header_state_slot() { &spec, ); - assert_eq!( + assert!(matches!( result, Err(BlockProcessingError::HeaderInvalid { - reason: HeaderInvalid::StateSlotMismatch + reason: HeaderInvalid::StateSlotMismatch, }) - ); + )); } #[tokio::test] async fn invalid_parent_block_root() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let state = harness.get_current_state(); let slot = state.slot(); @@ -139,21 +142,18 @@ async fn invalid_parent_block_root() { &spec, ); - assert_eq!( + assert!(matches!( result, Err(BlockProcessingError::HeaderInvalid { - reason: HeaderInvalid::ParentBlockRootMismatch { - state: state.latest_block_header().canonical_root(), - block: Hash256::from([0xAA; 32]) - } + reason: HeaderInvalid::ParentBlockRootMismatch { .. }, }) - ); + )); } #[tokio::test] async fn invalid_block_signature() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let state = harness.get_current_state(); let slot = state.slot(); @@ -172,19 +172,18 @@ async fn invalid_block_signature() { &spec, ); - // should get a BadSignature error - assert_eq!( + assert!(matches!( result, Err(BlockProcessingError::HeaderInvalid { - reason: HeaderInvalid::ProposalSignatureInvalid + reason: HeaderInvalid::ProposalSignatureInvalid, }) - ); + )); } #[tokio::test] async fn invalid_randao_reveal_signature() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let state = harness.get_current_state(); let slot = state.slot(); @@ -211,8 +210,8 @@ async fn invalid_randao_reveal_signature() { #[tokio::test] async fn valid_4_deposits() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let (deposits, state) = harness.make_deposits(&mut state, 4, None, None); @@ -235,8 +234,8 @@ async fn valid_4_deposits() { #[tokio::test] async fn invalid_deposit_deposit_count_too_big() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let (deposits, state) = harness.make_deposits(&mut state, 1, None, None); @@ -267,8 +266,8 @@ async fn invalid_deposit_deposit_count_too_big() { #[tokio::test] async fn invalid_deposit_count_too_small() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let (deposits, state) = harness.make_deposits(&mut state, 1, None, None); @@ -299,8 +298,8 @@ async fn invalid_deposit_count_too_small() { #[tokio::test] async fn invalid_deposit_bad_merkle_proof() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let (deposits, state) = harness.make_deposits(&mut state, 1, None, None); @@ -333,8 +332,8 @@ async fn invalid_deposit_bad_merkle_proof() { #[tokio::test] async fn invalid_deposit_wrong_sig() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let (deposits, state) = @@ -357,8 +356,8 @@ async fn invalid_deposit_wrong_sig() { #[tokio::test] async fn invalid_deposit_invalid_pub_key() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let (deposits, state) = @@ -382,8 +381,8 @@ async fn invalid_deposit_invalid_pub_key() { #[tokio::test] async fn invalid_attestation_no_committee_for_index() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let mut head_block = harness @@ -422,8 +421,8 @@ async fn invalid_attestation_no_committee_for_index() { #[tokio::test] async fn invalid_attestation_wrong_justified_checkpoint() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let mut head_block = harness @@ -477,8 +476,8 @@ async fn invalid_attestation_wrong_justified_checkpoint() { #[tokio::test] async fn invalid_attestation_bad_aggregation_bitfield_len() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let mut head_block = harness @@ -488,13 +487,14 @@ async fn invalid_attestation_bad_aggregation_bitfield_len() { .clone() .deconstruct() .0; + // Use Electra method since harness runs at Electra fork *head_block .to_mut() .body_mut() .attestations_mut() .next() .unwrap() - .aggregation_bits_base_mut() + .aggregation_bits_electra_mut() .unwrap() = Bitfield::with_capacity(spec.target_committee_size).unwrap(); let mut ctxt = ConsensusContext::new(state.slot()); @@ -506,19 +506,20 @@ async fn invalid_attestation_bad_aggregation_bitfield_len() { &spec, ); - // Expecting InvalidBitfield because the size of the aggregation_bitfield is bigger than the committee size. + // In Electra, setting wrong aggregation_bits capacity causes EmptyCommittee error + // (validation order changed - committee check happens before bitfield check) assert_eq!( result, Err(BlockProcessingError::BeaconStateError( - BeaconStateError::InvalidBitfield + BeaconStateError::EmptyCommittee )) ); } #[tokio::test] async fn invalid_attestation_bad_signature() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, 97).await; // minimal number of required validators for this test + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let mut head_block = harness @@ -558,8 +559,8 @@ async fn invalid_attestation_bad_signature() { #[tokio::test] async fn invalid_attestation_included_too_early() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let mut head_block = harness @@ -603,56 +604,15 @@ async fn invalid_attestation_included_too_early() { ); } -#[tokio::test] -async fn invalid_attestation_included_too_late() { - let spec = MainnetEthSpec::default_spec(); - // note to maintainer: might need to increase validator count if we get NoCommittee - let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; - - let mut state = harness.get_current_state(); - let mut head_block = harness - .chain - .head_beacon_block() - .as_ref() - .clone() - .deconstruct() - .0; - let new_attesation_slot = head_block.body().attestations().next().unwrap().data().slot - - Slot::new(MainnetEthSpec::slots_per_epoch()); - head_block - .to_mut() - .body_mut() - .attestations_mut() - .next() - .unwrap() - .data_mut() - .slot = new_attesation_slot; - - let mut ctxt = ConsensusContext::new(state.slot()); - let result = process_operations::process_attestations( - &mut state, - head_block.body(), - VerifySignatures::True, - &mut ctxt, - &spec, - ); - assert_eq!( - result, - Err(BlockProcessingError::AttestationInvalid { - index: 0, - reason: AttestationInvalid::IncludedTooLate { - state: state.slot(), - attestation: new_attesation_slot, - } - }) - ); -} +// Note: `invalid_attestation_included_too_late` test removed. +// The `IncludedTooLate` check was removed in Deneb (EIP7045), so this test is no longer +// applicable when running with Electra spec (which the harness uses by default). #[tokio::test] async fn invalid_attestation_target_epoch_slot_mismatch() { - let spec = MainnetEthSpec::default_spec(); // note to maintainer: might need to increase validator count if we get NoCommittee let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut state = harness.get_current_state(); let mut head_block = harness @@ -694,8 +654,8 @@ async fn invalid_attestation_target_epoch_slot_mismatch() { #[tokio::test] async fn valid_insert_attester_slashing() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let attester_slashing = harness.make_attester_slashing(vec![1, 2]); @@ -715,8 +675,8 @@ async fn valid_insert_attester_slashing() { #[tokio::test] async fn invalid_attester_slashing_not_slashable() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut attester_slashing = harness.make_attester_slashing(vec![1, 2]); match &mut attester_slashing { @@ -750,8 +710,8 @@ async fn invalid_attester_slashing_not_slashable() { #[tokio::test] async fn invalid_attester_slashing_1_invalid() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut attester_slashing = harness.make_attester_slashing(vec![1, 2]); match &mut attester_slashing { @@ -790,8 +750,8 @@ async fn invalid_attester_slashing_1_invalid() { #[tokio::test] async fn invalid_attester_slashing_2_invalid() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut attester_slashing = harness.make_attester_slashing(vec![1, 2]); match &mut attester_slashing { @@ -830,8 +790,8 @@ async fn invalid_attester_slashing_2_invalid() { #[tokio::test] async fn valid_insert_proposer_slashing() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let proposer_slashing = harness.make_proposer_slashing(1); let mut state = harness.get_current_state(); let mut ctxt = ConsensusContext::new(state.slot()); @@ -848,8 +808,8 @@ async fn valid_insert_proposer_slashing() { #[tokio::test] async fn invalid_proposer_slashing_proposals_identical() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut proposer_slashing = harness.make_proposer_slashing(1); proposer_slashing.signed_header_1.message = proposer_slashing.signed_header_2.message.clone(); @@ -876,8 +836,8 @@ async fn invalid_proposer_slashing_proposals_identical() { #[tokio::test] async fn invalid_proposer_slashing_proposer_unknown() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut proposer_slashing = harness.make_proposer_slashing(1); proposer_slashing.signed_header_1.message.proposer_index = 3_141_592; @@ -905,8 +865,8 @@ async fn invalid_proposer_slashing_proposer_unknown() { #[tokio::test] async fn invalid_proposer_slashing_duplicate_slashing() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let proposer_slashing = harness.make_proposer_slashing(1); let mut state = harness.get_current_state(); @@ -939,8 +899,8 @@ async fn invalid_proposer_slashing_duplicate_slashing() { #[tokio::test] async fn invalid_bad_proposal_1_signature() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut proposer_slashing = harness.make_proposer_slashing(1); proposer_slashing.signed_header_1.signature = Signature::empty(); let mut state = harness.get_current_state(); @@ -965,8 +925,8 @@ async fn invalid_bad_proposal_1_signature() { #[tokio::test] async fn invalid_bad_proposal_2_signature() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut proposer_slashing = harness.make_proposer_slashing(1); proposer_slashing.signed_header_2.signature = Signature::empty(); let mut state = harness.get_current_state(); @@ -991,8 +951,8 @@ async fn invalid_bad_proposal_2_signature() { #[tokio::test] async fn invalid_proposer_slashing_proposal_epoch_mismatch() { - let spec = MainnetEthSpec::default_spec(); let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); let mut proposer_slashing = harness.make_proposer_slashing(1); proposer_slashing.signed_header_1.message.slot = Slot::new(0); proposer_slashing.signed_header_2.message.slot = Slot::new(128); @@ -1019,92 +979,6 @@ async fn invalid_proposer_slashing_proposal_epoch_mismatch() { ); } -#[tokio::test] -async fn fork_spanning_exit() { - let mut spec = MainnetEthSpec::default_spec(); - let slots_per_epoch = MainnetEthSpec::slots_per_epoch(); - - spec.altair_fork_epoch = Some(Epoch::new(2)); - spec.bellatrix_fork_epoch = Some(Epoch::new(4)); - spec.shard_committee_period = 0; - let spec = Arc::new(spec); - - let harness = BeaconChainHarness::builder(MainnetEthSpec) - .spec(spec.clone()) - .deterministic_keypairs(VALIDATOR_COUNT) - .mock_execution_layer() - .fresh_ephemeral_store() - .build(); - - harness.extend_to_slot(slots_per_epoch.into()).await; - - /* - * Produce an exit *before* Altair. - */ - - let signed_exit = harness.make_voluntary_exit(0, Epoch::new(1)); - assert!(signed_exit.message.epoch < spec.altair_fork_epoch.unwrap()); - - /* - * Ensure the exit verifies before Altair. - */ - - let head = harness.chain.canonical_head.cached_head(); - let head_state = &head.snapshot.beacon_state; - assert!(head_state.current_epoch() < spec.altair_fork_epoch.unwrap()); - verify_exit( - head_state, - None, - &signed_exit, - VerifySignatures::True, - &spec, - ) - .expect("phase0 exit verifies against phase0 state"); - - /* - * Ensure the exit verifies after Altair. - */ - - harness - .extend_to_slot(spec.altair_fork_epoch.unwrap().start_slot(slots_per_epoch)) - .await; - let head = harness.chain.canonical_head.cached_head(); - let head_state = &head.snapshot.beacon_state; - assert!(head_state.current_epoch() >= spec.altair_fork_epoch.unwrap()); - assert!(head_state.current_epoch() < spec.bellatrix_fork_epoch.unwrap()); - verify_exit( - head_state, - None, - &signed_exit, - VerifySignatures::True, - &spec, - ) - .expect("phase0 exit verifies against altair state"); - - /* - * Ensure the exit no longer verifies after Bellatrix. - */ - - harness - .extend_to_slot( - spec.bellatrix_fork_epoch - .unwrap() - .start_slot(slots_per_epoch), - ) - .await; - let head = harness.chain.canonical_head.cached_head(); - let head_state = &head.snapshot.beacon_state; - assert!(head_state.current_epoch() >= spec.bellatrix_fork_epoch.unwrap()); - verify_exit( - head_state, - None, - &signed_exit, - VerifySignatures::True, - &spec, - ) - .expect_err("phase0 exit does not verify against bellatrix state"); -} - /// Check that the block replayer does not consume state roots unnecessarily. #[tokio::test] async fn block_replayer_peeking_state_roots() { diff --git a/consensus/state_processing/src/per_block_processing/verify_attestation.rs b/consensus/state_processing/src/per_block_processing/verify_attestation.rs index 00105f323c..64b7a31afb 100644 --- a/consensus/state_processing/src/per_block_processing/verify_attestation.rs +++ b/consensus/state_processing/src/per_block_processing/verify_attestation.rs @@ -52,8 +52,6 @@ pub fn verify_attestation_for_block_inclusion<'ctxt, E: EthSpec>( /// /// Returns a descriptive `Err` if the attestation is malformed or does not accurately reflect the /// prior blocks in `state`. -/// -/// Spec v0.12.1 pub fn verify_attestation_for_state<'ctxt, E: EthSpec>( state: &BeaconState<E>, attestation: AttestationRef<'ctxt, E>, @@ -94,8 +92,6 @@ pub fn verify_attestation_for_state<'ctxt, E: EthSpec>( } /// Check target epoch and source checkpoint. -/// -/// Spec v0.12.1 fn verify_casper_ffg_vote<E: EthSpec>( attestation: AttestationRef<E>, state: &BeaconState<E>, diff --git a/consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs b/consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs new file mode 100644 index 0000000000..1c15e1c21c --- /dev/null +++ b/consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs @@ -0,0 +1,46 @@ +use super::VerifySignatures; +use super::errors::{BlockOperationError, PayloadAttestationInvalid as Invalid}; +use crate::ConsensusContext; +use crate::per_block_processing::is_valid_indexed_payload_attestation; +use safe_arith::SafeArith; +use types::*; + +pub fn verify_payload_attestation<'ctxt, E: EthSpec>( + state: &mut BeaconState<E>, + payload_attestation: &'ctxt PayloadAttestation<E>, + ctxt: &'ctxt mut ConsensusContext<E>, + verify_signatures: VerifySignatures, + spec: &ChainSpec, +) -> Result<(), BlockOperationError<Invalid>> { + let data = &payload_attestation.data; + + // Check that the attestation is for the parent beacon block + verify!( + data.beacon_block_root == state.latest_block_header().parent_root, + Invalid::BlockRootMismatch { + expected: state.latest_block_header().parent_root, + found: data.beacon_block_root, + } + ); + + // Check that the attestation is for the previous slot + verify!( + data.slot.safe_add(1)? == state.slot(), + Invalid::SlotMismatch { + expected: state.slot().saturating_sub(Slot::new(1)), + found: data.slot, + } + ); + + let indexed_payload_attestation = + ctxt.get_indexed_payload_attestation(state, payload_attestation, spec)?; + + is_valid_indexed_payload_attestation( + state, + indexed_payload_attestation, + verify_signatures, + spec, + )?; + + Ok(()) +} diff --git a/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs b/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs index c5ec80b92a..3e4f7e8189 100644 --- a/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs +++ b/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs @@ -2,7 +2,7 @@ use crate::common::attesting_indices_base::get_attesting_indices; use safe_arith::SafeArith; use types::{BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, PendingAttestation}; -#[cfg(feature = "arbitrary-fuzz")] +#[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; /// Sets the boolean `var` on `self` to be true if it is true on `other`. Otherwise leaves `self` @@ -16,7 +16,7 @@ macro_rules! set_self_if_other_is_true { } /// The information required to reward a block producer for including an attestation in a block. -#[cfg_attr(feature = "arbitrary-fuzz", derive(Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[derive(Debug, Clone, Copy, PartialEq)] pub struct InclusionInfo { /// The distance between the attestation slot and the slot that attestation was included in a @@ -48,7 +48,7 @@ impl InclusionInfo { } /// Information required to reward some validator during the current and previous epoch. -#[cfg_attr(feature = "arbitrary-fuzz", derive(Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[derive(Debug, Default, Clone, PartialEq)] pub struct ValidatorStatus { /// True if the validator has been slashed, ever. @@ -118,7 +118,7 @@ impl ValidatorStatus { /// epochs. #[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] pub struct TotalBalances { /// The effective balance increment from the spec. effective_balance_increment: u64, @@ -175,7 +175,7 @@ impl TotalBalances { /// Summarised information about validator participation in the _previous and _current_ epochs of /// some `BeaconState`. -#[cfg_attr(feature = "arbitrary-fuzz", derive(Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[derive(Debug, Clone)] pub struct ValidatorStatuses { /// Information about each individual validator from the state's validator registry. diff --git a/consensus/state_processing/src/per_epoch_processing/single_pass.rs b/consensus/state_processing/src/per_epoch_processing/single_pass.rs index 3e07803aa6..4eb1e36628 100644 --- a/consensus/state_processing/src/per_epoch_processing/single_pass.rs +++ b/consensus/state_processing/src/per_epoch_processing/single_pass.rs @@ -15,9 +15,9 @@ use std::collections::{BTreeSet, HashMap}; use tracing::instrument; use typenum::Unsigned; use types::{ - ActivationQueue, BeaconState, BeaconStateError, ChainSpec, Checkpoint, DepositData, Epoch, - EthSpec, ExitCache, ForkName, ParticipationFlags, PendingDeposit, ProgressiveBalancesCache, - RelativeEpoch, Validator, + ActivationQueue, BeaconState, BeaconStateError, BuilderPendingPayment, ChainSpec, Checkpoint, + DepositData, Epoch, EthSpec, ExitCache, ForkName, ParticipationFlags, PendingDeposit, + ProgressiveBalancesCache, RelativeEpoch, Validator, consts::altair::{ NUM_FLAG_INDICES, PARTICIPATION_FLAG_WEIGHTS, TIMELY_HEAD_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX, WEIGHT_DENOMINATOR, @@ -33,6 +33,7 @@ pub struct SinglePassConfig { pub pending_consolidations: bool, pub effective_balance_updates: bool, pub proposer_lookahead: bool, + pub builder_pending_payments: bool, } impl Default for SinglePassConfig { @@ -52,6 +53,7 @@ impl SinglePassConfig { pending_consolidations: true, effective_balance_updates: true, proposer_lookahead: true, + builder_pending_payments: true, } } @@ -65,6 +67,7 @@ impl SinglePassConfig { pending_consolidations: false, effective_balance_updates: false, proposer_lookahead: false, + builder_pending_payments: false, } } } @@ -455,6 +458,12 @@ pub fn process_epoch_single_pass<E: EthSpec>( )?; } + // Process builder pending payments outside the single-pass loop, as they depend on balances for + // multiple validators and cannot be computed accurately inside the loop. + if fork_name.gloas_enabled() && conf.builder_pending_payments { + process_builder_pending_payments(state, state_ctxt, spec)?; + } + // Finally, finish updating effective balance caches. We need this to happen *after* processing // of pending consolidations, which recomputes some effective balances. if conf.effective_balance_updates { @@ -503,6 +512,58 @@ pub fn process_proposer_lookahead<E: EthSpec>( Ok(()) } +/// Calculate the quorum threshold for builder payments based on total active balance. +fn get_builder_payment_quorum_threshold<E: EthSpec>( + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result<u64, Error> { + let per_slot_balance = state_ctxt + .total_active_balance + .safe_div(E::slots_per_epoch())?; + let quorum = per_slot_balance.safe_mul(spec.builder_payment_threshold_numerator)?; + quorum + .safe_div(spec.builder_payment_threshold_denominator) + .map_err(Error::from) +} + +/// Processes the builder pending payments from the previous epoch. +fn process_builder_pending_payments<E: EthSpec>( + state: &mut BeaconState<E>, + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result<(), Error> { + let quorum = get_builder_payment_quorum_threshold::<E>(state_ctxt, spec)?; + + // Collect qualifying payments and append to `builder_pending_withdrawals`. + // We use this pattern rather than a loop to avoid multiple borrows of the state's fields. + let new_pending_builder_withdrawals = state + .builder_pending_payments()? + .iter() + .take(E::SlotsPerEpoch::to_usize()) + .filter(|payment| payment.weight >= quorum) + .map(|payment| payment.withdrawal.clone()) + .collect::<Vec<_>>(); + for payment_withdrawal in new_pending_builder_withdrawals { + state + .builder_pending_withdrawals_mut()? + .push(payment_withdrawal)?; + } + + // NOTE: this could be a little more memory-efficient with some juggling to reuse parts + // of the persistent tree (could convert to list, use pop_front, convert back). + let updated_payments = state + .builder_pending_payments()? + .iter() + .skip(E::SlotsPerEpoch::to_usize()) + .cloned() + .chain((0..E::SlotsPerEpoch::to_usize()).map(|_| BuilderPendingPayment::default())) + .collect::<Vec<_>>(); + + *state.builder_pending_payments_mut()? = Vector::new(updated_payments)?; + + Ok(()) +} + fn process_single_inactivity_update( inactivity_score: &mut Cow<u64>, validator_info: &ValidatorInfo, diff --git a/consensus/state_processing/src/per_epoch_processing/tests.rs b/consensus/state_processing/src/per_epoch_processing/tests.rs index f042e8766c..c04b7f843d 100644 --- a/consensus/state_processing/src/per_epoch_processing/tests.rs +++ b/consensus/state_processing/src/per_epoch_processing/tests.rs @@ -11,10 +11,10 @@ async fn runs_without_error() { .default_spec() .deterministic_keypairs(8) .fresh_ephemeral_store() + .mock_execution_layer() .build(); harness.advance_slot(); - let spec = MinimalEthSpec::default_spec(); let target_slot = (MinimalEthSpec::genesis_epoch() + 4).end_slot(MinimalEthSpec::slots_per_epoch()); @@ -32,7 +32,7 @@ async fn runs_without_error() { .await; let mut new_head_state = harness.get_current_state(); - process_epoch(&mut new_head_state, &spec).unwrap(); + process_epoch(&mut new_head_state, &harness.spec).unwrap(); } #[cfg(not(debug_assertions))] diff --git a/consensus/state_processing/src/per_slot_processing.rs b/consensus/state_processing/src/per_slot_processing.rs index 0f8e5dc52d..f26ea567a2 100644 --- a/consensus/state_processing/src/per_slot_processing.rs +++ b/consensus/state_processing/src/per_slot_processing.rs @@ -14,6 +14,7 @@ pub enum Error { EpochProcessingError(EpochProcessingError), ArithError(ArithError), InconsistentStateFork(InconsistentFork), + BitfieldError(ssz::BitfieldError), } impl From<ArithError> for Error { @@ -22,6 +23,12 @@ impl From<ArithError> for Error { } } +impl From<ssz::BitfieldError> for Error { + fn from(e: ssz::BitfieldError) -> Self { + Self::BitfieldError(e) + } +} + /// Advances a state forward by one slot, performing per-epoch processing if required. /// /// If the root of the supplied `state` is known, then it can be passed as `state_root`. If @@ -48,6 +55,18 @@ pub fn per_slot_processing<E: EthSpec>( None }; + // Unset the next payload availability + if state.fork_name_unchecked().gloas_enabled() { + let next_slot_index = state + .slot() + .as_usize() + .safe_add(1)? + .safe_rem(E::slots_per_historical_root())?; + state + .execution_payload_availability_mut()? + .set(next_slot_index, false)?; + } + state.slot_mut().safe_add_assign(1)?; // Process fork upgrades here. Note that multiple upgrades can potentially run diff --git a/consensus/state_processing/src/upgrade/gloas.rs b/consensus/state_processing/src/upgrade/gloas.rs index 0e0f39fa02..7a88383ab0 100644 --- a/consensus/state_processing/src/upgrade/gloas.rs +++ b/consensus/state_processing/src/upgrade/gloas.rs @@ -1,10 +1,14 @@ +use crate::per_block_processing::{ + is_valid_deposit_signature, process_operations::apply_deposit_for_builder, +}; use milhouse::{List, Vector}; use ssz_types::BitVector; +use std::collections::HashSet; use std::mem; use typenum::Unsigned; use types::{ BeaconState, BeaconStateError as Error, BeaconStateGloas, BuilderPendingPayment, ChainSpec, - EthSpec, ExecutionPayloadBid, Fork, + DepositData, EthSpec, ExecutionPayloadBid, Fork, is_builder_withdrawal_credential, }; /// Transform a `Fulu` state into a `Gloas` state. @@ -30,7 +34,7 @@ pub fn upgrade_state_to_gloas<E: EthSpec>( // // Fixed size vectors get cloned because replacing them would require the same size // allocation as cloning. - let post = BeaconState::Gloas(BeaconStateGloas { + let mut post = BeaconState::Gloas(BeaconStateGloas { // Versioning genesis_time: pre.genesis_time, genesis_validators_root: pre.genesis_validators_root, @@ -114,5 +118,81 @@ pub fn upgrade_state_to_gloas<E: EthSpec>( slashings_cache: mem::take(&mut pre.slashings_cache), epoch_cache: mem::take(&mut pre.epoch_cache), }); + // [New in Gloas:EIP7732] + onboard_builders_from_pending_deposits(&mut post, spec)?; + Ok(post) } + +/// Applies any pending deposit for builders, effectively onboarding builders at the fork. +fn onboard_builders_from_pending_deposits<E: EthSpec>( + state: &mut BeaconState<E>, + spec: &ChainSpec, +) -> Result<(), Error> { + // Rather than tracking all `validator_pubkeys` in one place as the spec does, we keep a + // hashset for *just* the new validator pubkeys, and use the state's efficient + // `get_validator_index` function instead of an O(n) iteration over the full validator list. + let mut new_validator_pubkeys = HashSet::new(); + + // Clone pending deposits to avoid borrow conflicts when mutating state. + let current_pending_deposits = state.pending_deposits()?.clone(); + + let mut pending_deposits = List::empty(); + + for deposit in ¤t_pending_deposits { + // Deposits for existing validators stay in the pending queue. + if new_validator_pubkeys.contains(&deposit.pubkey) + || state.get_validator_index(&deposit.pubkey)?.is_some() + { + pending_deposits.push(deposit.clone())?; + continue; + } + + // Re-scan builder list each iteration because `apply_deposit_for_builder` may add + // new builders to the registry. + // TODO(gloas): this linear scan could be optimized, see: + // https://github.com/sigp/lighthouse/issues/8783 + let builder_index = state + .builders()? + .iter() + .position(|b| b.pubkey == deposit.pubkey); + + let has_builder_credentials = + is_builder_withdrawal_credential(deposit.withdrawal_credentials, spec); + + if builder_index.is_some() || has_builder_credentials { + let builder_index_opt = builder_index.map(|i| i as u64); + apply_deposit_for_builder( + state, + builder_index_opt, + deposit.pubkey, + deposit.withdrawal_credentials, + deposit.amount, + deposit.signature.clone(), + deposit.slot, + spec, + )?; + continue; + } + + // If there is a pending deposit for a new validator that has a valid signature, + // track the pubkey so that subsequent builder deposits for the same pubkey stay + // in pending (applied to the validator later) rather than creating a builder. + // Deposits with invalid signatures are dropped since they would fail in + // apply_pending_deposit anyway. + let deposit_data = DepositData { + pubkey: deposit.pubkey, + withdrawal_credentials: deposit.withdrawal_credentials, + amount: deposit.amount, + signature: deposit.signature.clone(), + }; + if is_valid_deposit_signature(&deposit_data, spec).is_ok() { + new_validator_pubkeys.insert(deposit.pubkey); + pending_deposits.push(deposit.clone())?; + } + } + + *state.pending_deposits_mut()? = pending_deposits; + + Ok(()) +} diff --git a/consensus/state_processing/src/verify_operation.rs b/consensus/state_processing/src/verify_operation.rs index 1f76f19586..1e9c3d5fe3 100644 --- a/consensus/state_processing/src/verify_operation.rs +++ b/consensus/state_processing/src/verify_operation.rs @@ -7,6 +7,7 @@ use crate::per_block_processing::{ verify_attester_slashing, verify_bls_to_execution_change, verify_exit, verify_proposer_slashing, }; +#[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; use educe::Educe; use smallvec::{SmallVec, smallvec}; @@ -39,13 +40,17 @@ pub trait TransformPersist { /// /// The inner `op` field is private, meaning instances of this type can only be constructed /// by calling `validate`. -#[derive(Educe, Debug, Clone, Arbitrary)] +#[derive(Educe, Debug, Clone)] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[educe( PartialEq, Eq, Hash(bound(T: TransformPersist + std::hash::Hash, E: EthSpec)) )] -#[arbitrary(bound = "T: TransformPersist + Arbitrary<'arbitrary>, E: EthSpec")] +#[cfg_attr( + feature = "arbitrary", + arbitrary(bound = "T: TransformPersist + Arbitrary<'arbitrary>, E: EthSpec") +)] pub struct SigVerifiedOp<T: TransformPersist, E: EthSpec> { op: T, verified_against: VerifiedAgainst, @@ -133,7 +138,8 @@ struct SigVerifiedOpDecode<P: Decode> { /// /// We need to store multiple `ForkVersion`s because attester slashings contain two indexed /// attestations which may be signed using different versions. -#[derive(Debug, PartialEq, Eq, Clone, Hash, Encode, Decode, TestRandom, Arbitrary)] +#[derive(Debug, PartialEq, Eq, Clone, Hash, Encode, Decode, TestRandom)] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] pub struct VerifiedAgainst { fork_versions: SmallVec<[ForkVersion; MAX_FORKS_VERIFIED_AGAINST]>, } diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index feea855c84..c09e3d6931 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -1,26 +1,24 @@ [package] name = "types" version = "0.2.1" -authors = [ - "Paul Hauner <paul@paulhauner.com>", - "Age Manning <Age@AgeManning.com>", -] +authors = ["Paul Hauner <paul@paulhauner.com>", "Age Manning <Age@AgeManning.com>"] edition = { workspace = true } [features] -default = ["legacy-arith"] -# Allow saturating arithmetic on slots and epochs. Enabled by default, but deprecated. -legacy-arith = [] +default = [] +# Enable +, -, *, /, % operators for Slot and Epoch types. +# Operations saturate instead of wrapping. +saturating-arith = [] sqlite = ["dep:rusqlite"] arbitrary = [ "dep:arbitrary", "bls/arbitrary", + "kzg/arbitrary", "ethereum_ssz/arbitrary", "milhouse/arbitrary", "ssz_types/arbitrary", "swap_or_not_shuffle/arbitrary", ] -arbitrary-fuzz = ["arbitrary"] portable = ["bls/supranational-portable"] [dependencies] diff --git a/consensus/types/src/block/beacon_block.rs b/consensus/types/src/block/beacon_block.rs index bee3cdb274..5634d842b6 100644 --- a/consensus/types/src/block/beacon_block.rs +++ b/consensus/types/src/block/beacon_block.rs @@ -309,6 +309,26 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload<E>> BeaconBlockRef<'a, E, Payl pub fn execution_payload(&self) -> Result<Payload::Ref<'a>, BeaconStateError> { self.body().execution_payload() } + + pub fn blob_kzg_commitments_len(&self) -> Option<usize> { + match self { + BeaconBlockRef::Base(_) => None, + BeaconBlockRef::Altair(_) => None, + BeaconBlockRef::Bellatrix(_) => None, + BeaconBlockRef::Capella(_) => None, + BeaconBlockRef::Deneb(block) => Some(block.body.blob_kzg_commitments.len()), + BeaconBlockRef::Electra(block) => Some(block.body.blob_kzg_commitments.len()), + BeaconBlockRef::Fulu(block) => Some(block.body.blob_kzg_commitments.len()), + BeaconBlockRef::Gloas(block) => Some( + block + .body + .signed_execution_payload_bid + .message + .blob_kzg_commitments + .len(), + ), + } + } } impl<'a, E: EthSpec, Payload: AbstractExecPayload<E>> BeaconBlockRefMut<'a, E, Payload> { diff --git a/consensus/types/src/builder/builder.rs b/consensus/types/src/builder/builder.rs index 2bd50f42cc..7d494da3ee 100644 --- a/consensus/types/src/builder/builder.rs +++ b/consensus/types/src/builder/builder.rs @@ -1,5 +1,5 @@ use crate::test_utils::TestRandom; -use crate::{Address, Epoch, ForkName}; +use crate::{Address, ChainSpec, Epoch, ForkName}; use bls::PublicKeyBytes; use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; @@ -24,3 +24,12 @@ pub struct Builder { pub deposit_epoch: Epoch, pub withdrawable_epoch: Epoch, } + +impl Builder { + /// Check if a builder is active in a state with `finalized_epoch`. + /// + /// This implements `is_active_builder` from the spec. + pub fn is_active_at_finalized_epoch(&self, finalized_epoch: Epoch, spec: &ChainSpec) -> bool { + self.deposit_epoch < finalized_epoch && self.withdrawable_epoch == spec.far_future_epoch + } +} diff --git a/consensus/types/src/builder/builder_bid.rs b/consensus/types/src/builder/builder_bid.rs index 1018fadb64..e706b01283 100644 --- a/consensus/types/src/builder/builder_bid.rs +++ b/consensus/types/src/builder/builder_bid.rs @@ -196,7 +196,7 @@ impl<E: EthSpec> SignedBuilderBid<E> { .pubkey() .decompress() .map(|pubkey| { - let domain = spec.get_builder_domain(); + let domain = spec.get_builder_application_domain(); let message = self.message.signing_root(domain); self.signature.verify(&pubkey, message) }) diff --git a/consensus/types/src/core/application_domain.rs b/consensus/types/src/core/application_domain.rs index 5e33f2dfd5..ff55a91034 100644 --- a/consensus/types/src/core/application_domain.rs +++ b/consensus/types/src/core/application_domain.rs @@ -4,6 +4,7 @@ pub const APPLICATION_DOMAIN_BUILDER: u32 = 16777216; #[derive(Debug, PartialEq, Clone, Copy)] pub enum ApplicationDomain { + /// NOTE: This domain is only used for out-of-protocol block building, DO NOT use it for Gloas/ePBS. Builder, } diff --git a/consensus/types/src/core/chain_spec.rs b/consensus/types/src/core/chain_spec.rs index 6d25e3baf4..adf87dee94 100644 --- a/consensus/types/src/core/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -549,7 +549,9 @@ impl ChainSpec { // This should be updated to include the current fork and the genesis validators root, but discussion is ongoing: // // https://github.com/ethereum/builder-specs/issues/14 - pub fn get_builder_domain(&self) -> Hash256 { + // + // NOTE: This domain is only used for out-of-protocol block building, DO NOT use it for Gloas/ePBS. + pub fn get_builder_application_domain(&self) -> Hash256 { self.compute_domain( Domain::ApplicationMask(ApplicationDomain::Builder), self.genesis_fork_version, diff --git a/consensus/types/src/core/slot_epoch.rs b/consensus/types/src/core/slot_epoch.rs index 97457701b1..837391546c 100644 --- a/consensus/types/src/core/slot_epoch.rs +++ b/consensus/types/src/core/slot_epoch.rs @@ -22,7 +22,7 @@ use crate::{ test_utils::TestRandom, }; -#[cfg(feature = "legacy-arith")] +#[cfg(feature = "saturating-arith")] use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Rem, Sub, SubAssign}; #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] diff --git a/consensus/types/src/core/slot_epoch_macros.rs b/consensus/types/src/core/slot_epoch_macros.rs index eee267355a..1b0c3bcfc1 100644 --- a/consensus/types/src/core/slot_epoch_macros.rs +++ b/consensus/types/src/core/slot_epoch_macros.rs @@ -117,7 +117,7 @@ macro_rules! impl_safe_arith { } // Deprecated: prefer `SafeArith` methods for new code. -#[cfg(feature = "legacy-arith")] +#[cfg(feature = "saturating-arith")] macro_rules! impl_math_between { ($main: ident, $other: ident) => { impl Add<$other> for $main { @@ -321,9 +321,9 @@ macro_rules! impl_common { impl_u64_eq_ord!($type); impl_safe_arith!($type, $type); impl_safe_arith!($type, u64); - #[cfg(feature = "legacy-arith")] + #[cfg(feature = "saturating-arith")] impl_math_between!($type, $type); - #[cfg(feature = "legacy-arith")] + #[cfg(feature = "saturating-arith")] impl_math_between!($type, u64); impl_math!($type); impl_display!($type); diff --git a/consensus/types/src/execution/signed_execution_payload_envelope.rs b/consensus/types/src/execution/signed_execution_payload_envelope.rs index ca48a9ec9b..b1d949f863 100644 --- a/consensus/types/src/execution/signed_execution_payload_envelope.rs +++ b/consensus/types/src/execution/signed_execution_payload_envelope.rs @@ -1,7 +1,8 @@ use crate::test_utils::TestRandom; use crate::{ - ChainSpec, Domain, Epoch, EthSpec, ExecutionBlockHash, ExecutionPayloadEnvelope, Fork, - ForkName, Hash256, SignedRoot, Slot, + BeaconState, BeaconStateError, ChainSpec, Domain, Epoch, EthSpec, ExecutionBlockHash, + ExecutionPayloadEnvelope, Fork, ForkName, Hash256, SignedRoot, Slot, + consts::gloas::BUILDER_INDEX_SELF_BUILD, }; use bls::{PublicKey, Signature}; use context_deserialize::context_deserialize; @@ -58,6 +59,42 @@ impl<E: EthSpec> SignedExecutionPayloadEnvelope<E> { self.signature.verify(pubkey, message) } + + /// Verify `self.signature` using keys drawn from the beacon state. + pub fn verify_signature_with_state( + &self, + state: &BeaconState<E>, + spec: &ChainSpec, + ) -> Result<bool, BeaconStateError> { + let builder_index = self.message.builder_index; + + let pubkey_bytes = if builder_index == BUILDER_INDEX_SELF_BUILD { + let validator_index = state.latest_block_header().proposer_index; + state.get_validator(validator_index as usize)?.pubkey + } else { + state.get_builder(builder_index)?.pubkey + }; + + // TODO(gloas): Could use pubkey cache on state here, but it probably isn't worth + // it because this function is rarely used. Almost always the envelope should be signature + // verified prior to consensus code running. + let pubkey = pubkey_bytes.decompress()?; + + // Ensure the state's epoch matches the message's epoch before determining the Fork. + if self.epoch() != state.current_epoch() { + return Err(BeaconStateError::SignedEnvelopeIncorrectEpoch { + state_epoch: state.current_epoch(), + envelope_epoch: self.epoch(), + }); + } + + Ok(self.verify_signature( + &pubkey, + &state.fork(), + state.genesis_validators_root(), + spec, + )) + } } #[cfg(test)] diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index 2720745b01..bd67f469d2 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -9,7 +9,7 @@ use fixed_bytes::FixedBytesExtended; use int_to_bytes::{int_to_bytes4, int_to_bytes8}; use metastruct::{NumFields, metastruct}; use milhouse::{List, Vector}; -use safe_arith::{ArithError, SafeArith}; +use safe_arith::{ArithError, SafeArith, SafeArithIter}; use serde::{Deserialize, Deserializer, Serialize}; use ssz::{Decode, DecodeError, Encode, ssz_encode}; use ssz_derive::{Decode, Encode}; @@ -23,7 +23,7 @@ use tree_hash_derive::TreeHash; use typenum::Unsigned; use crate::{ - ExecutionBlockHash, ExecutionPayloadBid, Withdrawal, + Address, ExecutionBlockHash, ExecutionPayloadBid, Withdrawal, attestation::{ AttestationData, AttestationDuty, BeaconCommittee, Checkpoint, CommitteeIndex, PTC, ParticipationFlags, PendingAttestation, @@ -55,9 +55,22 @@ use crate::{ }; pub const CACHED_EPOCHS: usize = 3; + +// Pre-electra WS calculations are not supported. On mainnet, pre-electra epochs are outside the +// weak subjectivity period. The default pre-electra WS value is set to 256 to allow for `basic-sim` +// and `fallback-sim` tests to pass. 256 is a small enough number to trigger the WS safety check +// pre-electra on mainnet. +pub const DEFAULT_PRE_ELECTRA_WS_PERIOD: u64 = 256; + const MAX_RANDOM_BYTE: u64 = (1 << 8) - 1; const MAX_RANDOM_VALUE: u64 = (1 << 16) - 1; +// `SAFETY_DECAY` is defined as the maximum percentage tolerable loss in the one-third +// safety margin of FFG finality. Thus, any attack exploiting the Weak Subjectivity Period has +// a safety margin of at least `1/3 - SAFETY_DECAY/100`. +// Spec: https://github.com/ethereum/consensus-specs/blob/1937aff86b41b5171a9bc3972515986f1bbbf303/specs/phase0/weak-subjectivity.md?plain=1#L50-L71 +const SAFETY_DECAY: u64 = 10; + pub type Validators<E> = List<Validator, <E as EthSpec>::ValidatorRegistryLimit>; pub type Balances<E> = List<u64, <E as EthSpec>::ValidatorRegistryLimit>; @@ -174,6 +187,8 @@ pub enum BeaconStateError { MerkleTreeError(merkle_proof::MerkleTreeError), PartialWithdrawalCountInvalid(usize), NonExecutionAddressWithdrawalCredential, + WithdrawalCredentialMissingVersion, + WithdrawalCredentialMissingAddress, NoCommitteeFound(CommitteeIndex), InvalidCommitteeIndex(CommitteeIndex), /// `Attestation.data.index` field is invalid in overloaded data index scenario. @@ -199,7 +214,12 @@ pub enum BeaconStateError { ProposerLookaheadOutOfBounds { i: usize, }, + SignedEnvelopeIncorrectEpoch { + state_epoch: Epoch, + envelope_epoch: Epoch, + }, InvalidIndicesCount, + InvalidBuilderPendingPaymentsIndex(usize), InvalidExecutionPayloadAvailabilityIndex(usize), } @@ -857,7 +877,7 @@ impl<E: EthSpec> BeaconState<E> { relative_epoch: RelativeEpoch, ) -> Result<u64, BeaconStateError> { let cache = self.committee_cache(relative_epoch)?; - Ok(cache.epoch_committee_count() as u64) + Ok(cache.epoch_committee_count()? as u64) } /// Return the cached active validator indices at some epoch. @@ -1920,6 +1940,15 @@ impl<E: EthSpec> BeaconState<E> { .ok_or(BeaconStateError::UnknownValidator(validator_index)) } + /// Safe indexer for the `builders` list. + /// + /// Will return an error pre-Gloas, or for out-of-bounds indices. + pub fn get_builder(&self, builder_index: BuilderIndex) -> Result<&Builder, BeaconStateError> { + self.builders()? + .get(builder_index as usize) + .ok_or(BeaconStateError::UnknownBuilder(builder_index)) + } + /// Add a validator to the registry and return the validator index that was allocated for it. pub fn add_validator_to_registry( &mut self, @@ -1966,6 +1995,64 @@ impl<E: EthSpec> BeaconState<E> { Ok(index) } + /// Add a builder to the registry and return the builder index that was allocated for it. + pub fn add_builder_to_registry( + &mut self, + pubkey: PublicKeyBytes, + withdrawal_credentials: Hash256, + amount: u64, + slot: Slot, + spec: &ChainSpec, + ) -> Result<BuilderIndex, BeaconStateError> { + // We are not yet using the spec's `set_or_append_list`, but could consider it if it crops + // up elsewhere. It has been retconned into the spec to support index reuse but so far + // index reuse is only relevant for builders. + let builder_index = self.get_index_for_new_builder()?; + let builders = self.builders_mut()?; + + let version = *withdrawal_credentials + .as_slice() + .first() + .ok_or(BeaconStateError::WithdrawalCredentialMissingVersion)?; + let execution_address = withdrawal_credentials + .as_slice() + .get(12..) + .and_then(|bytes| Address::try_from(bytes).ok()) + .ok_or(BeaconStateError::WithdrawalCredentialMissingAddress)?; + + let builder = Builder { + pubkey, + version, + execution_address, + balance: amount, + deposit_epoch: slot.epoch(E::slots_per_epoch()), + withdrawable_epoch: spec.far_future_epoch, + }; + + if builder_index == builders.len() as u64 { + builders.push(builder)?; + } else { + *builders + .get_mut(builder_index as usize) + .ok_or(BeaconStateError::UnknownBuilder(builder_index))? = builder; + } + Ok(builder_index) + } + + // TODO(gloas): Optimize this function if we see a lot of registered builders on-chain. + // A cache here could be quite fiddly because this calculation depends on withdrawable epoch + // and balance - a cache for this would need to be updated whenever either of those fields + // changes. + pub fn get_index_for_new_builder(&self) -> Result<BuilderIndex, BeaconStateError> { + let current_epoch = self.current_epoch(); + for (index, builder) in self.builders()?.iter().enumerate() { + if builder.withdrawable_epoch <= current_epoch && builder.balance == 0 { + return Ok(index as u64); + } + } + Ok(self.builders()?.len() as u64) + } + /// Safe copy-on-write accessor for the `validators` list. pub fn get_validator_cow( &mut self, @@ -2064,7 +2151,7 @@ impl<E: EthSpec> BeaconState<E> { ) -> Result<Option<AttestationDuty>, BeaconStateError> { let cache = self.committee_cache(relative_epoch)?; - Ok(cache.get_attestation_duties(validator_index)) + Ok(cache.get_attestation_duties(validator_index)?) } /// Check if the attestation is for the block proposed at the attestation slot. @@ -2664,6 +2751,30 @@ impl<E: EthSpec> BeaconState<E> { Ok(pending_balance) } + pub fn get_pending_balance_to_withdraw_for_builder( + &self, + builder_index: BuilderIndex, + ) -> Result<u64, BeaconStateError> { + let pending_withdrawals_total = self + .builder_pending_withdrawals()? + .iter() + .filter_map(|withdrawal| { + (withdrawal.builder_index == builder_index).then_some(withdrawal.amount) + }) + .safe_sum()?; + let pending_payments_total = self + .builder_pending_payments()? + .iter() + .filter_map(|payment| { + (payment.withdrawal.builder_index == builder_index) + .then_some(payment.withdrawal.amount) + }) + .safe_sum()?; + pending_withdrawals_total + .safe_add(pending_payments_total) + .map_err(Into::into) + } + // ******* Electra mutators ******* pub fn queue_excess_active_balance( @@ -2799,7 +2910,6 @@ impl<E: EthSpec> BeaconState<E> { } } - #[allow(clippy::arithmetic_side_effects)] pub fn rebase_on(&mut self, base: &Self, spec: &ChainSpec) -> Result<(), BeaconStateError> { // Required for macros (which use type-hints internally). @@ -2934,6 +3044,26 @@ impl<E: EthSpec> BeaconState<E> { Ok(()) } + /// Returns the weak subjectivity period for `self` + pub fn compute_weak_subjectivity_period( + &self, + spec: &ChainSpec, + ) -> Result<Epoch, BeaconStateError> { + let total_active_balance = self.get_total_active_balance()?; + let fork_name = self.fork_name_unchecked(); + + if fork_name.electra_enabled() { + let balance_churn_limit = self.get_balance_churn_limit(spec)?; + compute_weak_subjectivity_period_electra( + total_active_balance, + balance_churn_limit, + spec, + ) + } else { + Ok(Epoch::new(DEFAULT_PRE_ELECTRA_WS_PERIOD)) + } + } + /// Get the payload timeliness committee for the given `slot`. /// /// Requires the committee cache to be initialized. @@ -3088,7 +3218,6 @@ impl<E: EthSpec> BeaconState<E> { )) } - #[allow(clippy::arithmetic_side_effects)] pub fn apply_pending_mutations(&mut self) -> Result<(), BeaconStateError> { match self { Self::Base(inner) => { @@ -3191,7 +3320,6 @@ impl<E: EthSpec> BeaconState<E> { pub fn get_beacon_state_leaves(&self) -> Vec<Hash256> { let mut leaves = vec![]; - #[allow(clippy::arithmetic_side_effects)] match self { BeaconState::Base(state) => { map_beacon_state_base_fields!(state, |_, field| { @@ -3309,3 +3437,75 @@ impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for BeaconState<E> { )) } } + +/// Spec: https://github.com/ethereum/consensus-specs/blob/1937aff86b41b5171a9bc3972515986f1bbbf303/specs/electra/weak-subjectivity.md?plain=1#L30 +pub fn compute_weak_subjectivity_period_electra( + total_active_balance: u64, + balance_churn_limit: u64, + spec: &ChainSpec, +) -> Result<Epoch, BeaconStateError> { + let epochs_for_validator_set_churn = SAFETY_DECAY + .safe_mul(total_active_balance)? + .safe_div(balance_churn_limit.safe_mul(200)?)?; + let ws_period = spec + .min_validator_withdrawability_delay + .safe_add(epochs_for_validator_set_churn)?; + + Ok(ws_period) +} + +#[cfg(test)] +mod weak_subjectivity_tests { + use crate::state::beacon_state::compute_weak_subjectivity_period_electra; + use crate::{ChainSpec, Epoch, EthSpec, MainnetEthSpec}; + + const GWEI_PER_ETH: u64 = 1_000_000_000; + + #[test] + fn test_compute_weak_subjectivity_period_electra() { + let mut spec = MainnetEthSpec::default_spec(); + spec.altair_fork_epoch = Some(Epoch::new(0)); + spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + spec.capella_fork_epoch = Some(Epoch::new(0)); + spec.deneb_fork_epoch = Some(Epoch::new(0)); + spec.electra_fork_epoch = Some(Epoch::new(0)); + + // A table of some expected values: + // https://github.com/ethereum/consensus-specs/blob/1937aff86b41b5171a9bc3972515986f1bbbf303/specs/electra/weak-subjectivity.md?plain=1#L44-L54 + // (total_active_balance, expected_ws_period) + let expected_values: Vec<(u64, u64)> = vec![ + (1_048_576 * GWEI_PER_ETH, 665), + (2_097_152 * GWEI_PER_ETH, 1_075), + (4_194_304 * GWEI_PER_ETH, 1_894), + (8_388_608 * GWEI_PER_ETH, 3_532), + (16_777_216 * GWEI_PER_ETH, 3_532), + (33_554_432 * GWEI_PER_ETH, 3_532), + // This value cross referenced w/ + // beacon_chain/tests/tests.rs:test_compute_weak_subjectivity_period + (1536 * GWEI_PER_ETH, 256), + ]; + + for (total_active_balance, expected_ws_period) in expected_values { + let balance_churn_limit = get_balance_churn_limit(total_active_balance, &spec); + + let calculated_ws_period = compute_weak_subjectivity_period_electra( + total_active_balance, + balance_churn_limit, + &spec, + ) + .unwrap(); + + assert_eq!(calculated_ws_period, expected_ws_period); + } + } + + // caclulate the balance_churn_limit without dealing with states + // and without initializing the active balance cache + fn get_balance_churn_limit(total_active_balance: u64, spec: &ChainSpec) -> u64 { + let churn = std::cmp::max( + spec.min_per_epoch_churn_limit_electra, + total_active_balance / spec.churn_limit_quotient, + ); + churn - (churn % spec.effective_balance_increment) + } +} diff --git a/consensus/types/src/state/committee_cache.rs b/consensus/types/src/state/committee_cache.rs index 39e9011ef4..4a28f3c689 100644 --- a/consensus/types/src/state/committee_cache.rs +++ b/consensus/types/src/state/committee_cache.rs @@ -1,9 +1,7 @@ -#![allow(clippy::arithmetic_side_effects)] - use std::{num::NonZeroUsize, ops::Range, sync::Arc}; use educe::Educe; -use safe_arith::SafeArith; +use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Serialize}; use ssz::{Decode, DecodeError, Encode, four_byte_option_impl}; use ssz_derive::{Decode, Encode}; @@ -79,7 +77,13 @@ impl CommitteeCache { .saturating_sub(spec.min_seed_lookahead) .saturating_sub(1u64); - if reqd_randao_epoch < state.min_randao_epoch() || epoch > state.current_epoch() + 1 { + if reqd_randao_epoch < state.min_randao_epoch() + || epoch + > state + .current_epoch() + .safe_add(1) + .map_err(BeaconStateError::ArithError)? + { return Err(BeaconStateError::EpochOutOfBounds); } @@ -118,7 +122,7 @@ impl CommitteeCache { *shuffling_positions .get_mut(v) .ok_or(BeaconStateError::ShuffleIndexOutOfBounds(v))? = - NonZeroUsize::new(i + 1).into(); + NonZeroUsize::new(i.safe_add(1).map_err(BeaconStateError::ArithError)?).into(); } Ok(Arc::new(CommitteeCache { @@ -177,8 +181,9 @@ impl CommitteeCache { self.slots_per_epoch as usize, self.committees_per_slot as usize, index as usize, - ); - let committee = self.compute_committee(committee_index)?; + ) + .ok()?; + let committee = self.compute_committee(committee_index).ok()??; Some(BeaconCommittee { slot, @@ -212,8 +217,9 @@ impl CommitteeCache { .initialized_epoch .ok_or(BeaconStateError::CommitteeCacheUninitialized(None))?; + let capacity = self.epoch_committee_count()?; initialized_epoch.slot_iter(self.slots_per_epoch).try_fold( - Vec::with_capacity(self.epoch_committee_count()), + Vec::with_capacity(capacity), |mut vec, slot| { vec.append(&mut self.get_beacon_committees_at_slot(slot)?); Ok(vec) @@ -225,43 +231,53 @@ impl CommitteeCache { /// /// Returns `None` if the `validator_index` does not exist, does not have duties or `Self` is /// non-initialized. - pub fn get_attestation_duties(&self, validator_index: usize) -> Option<AttestationDuty> { - let i = self.shuffled_position(validator_index)?; + pub fn get_attestation_duties( + &self, + validator_index: usize, + ) -> Result<Option<AttestationDuty>, ArithError> { + let Some(i) = self.shuffled_position(validator_index) else { + return Ok(None); + }; - (0..self.epoch_committee_count()) - .map(|nth_committee| (nth_committee, self.compute_committee_range(nth_committee))) - .find(|(_, range)| { - if let Some(range) = range { - range.start <= i && range.end > i - } else { - false - } - }) - .and_then(|(nth_committee, range)| { - let (slot, index) = self.convert_to_slot_and_index(nth_committee as u64)?; - let range = range?; - let committee_position = i - range.start; - let committee_len = range.end - range.start; + for nth_committee in 0..self.epoch_committee_count()? { + let Some(range) = self.compute_committee_range(nth_committee)? else { + continue; + }; - Some(AttestationDuty { + if range.start <= i && range.end > i { + let Some((slot, index)) = self.convert_to_slot_and_index(nth_committee as u64)? + else { + return Ok(None); + }; + + let committee_position = i.safe_sub(range.start)?; + let committee_len = range.end.safe_sub(range.start)?; + + return Ok(Some(AttestationDuty { slot, index, committee_position, committee_len, committees_at_slot: self.committees_per_slot(), - }) - }) + })); + } + } + + Ok(None) } /// Convert an index addressing the list of all epoch committees into a slot and per-slot index. fn convert_to_slot_and_index( &self, global_committee_index: u64, - ) -> Option<(Slot, CommitteeIndex)> { - let epoch_start_slot = self.initialized_epoch?.start_slot(self.slots_per_epoch); - let slot_offset = global_committee_index / self.committees_per_slot; - let index = global_committee_index % self.committees_per_slot; - Some((epoch_start_slot.safe_add(slot_offset).ok()?, index)) + ) -> Result<Option<(Slot, CommitteeIndex)>, ArithError> { + let Some(epoch) = self.initialized_epoch else { + return Ok(None); + }; + let epoch_start_slot = epoch.start_slot(self.slots_per_epoch); + let slot_offset = global_committee_index.safe_div(self.committees_per_slot)?; + let index = global_committee_index.safe_rem(self.committees_per_slot)?; + Ok(Some((epoch_start_slot.safe_add(slot_offset)?, index))) } /// Returns the number of active validators in the initialized epoch. @@ -278,11 +294,8 @@ impl CommitteeCache { /// Always returns `usize::default()` for a non-initialized epoch. /// /// Spec v0.12.1 - pub fn epoch_committee_count(&self) -> usize { - epoch_committee_count( - self.committees_per_slot as usize, - self.slots_per_epoch as usize, - ) + pub fn epoch_committee_count(&self) -> Result<usize, ArithError> { + (self.committees_per_slot as usize).safe_mul(self.slots_per_epoch as usize) } /// Returns the number of committees per slot for this cache's epoch. @@ -293,19 +306,23 @@ impl CommitteeCache { /// Returns a slice of `self.shuffling` that represents the `index`'th committee in the epoch. /// /// Spec v0.12.1 - fn compute_committee(&self, index: usize) -> Option<&[usize]> { - self.shuffling.get(self.compute_committee_range(index)?) + fn compute_committee(&self, index: usize) -> Result<Option<&[usize]>, ArithError> { + if let Some(range) = self.compute_committee_range(index)? { + Ok(self.shuffling.get(range)) + } else { + Ok(None) + } } /// Returns a range of `self.shuffling` that represents the `index`'th committee in the epoch. /// - /// To avoid a divide-by-zero, returns `None` if `self.committee_count` is zero. + /// To avoid a divide-by-zero, returns `Ok(None)` if `self.committee_count` is zero. /// - /// Will also return `None` if the index is out of bounds. + /// Will also return `Ok(None)` if the index is out of bounds. /// /// Spec v0.12.1 - fn compute_committee_range(&self, index: usize) -> Option<Range<usize>> { - compute_committee_range_in_epoch(self.epoch_committee_count(), index, self.shuffling.len()) + fn compute_committee_range(&self, index: usize) -> Result<Option<Range<usize>>, ArithError> { + compute_committee_range_in_epoch(self.epoch_committee_count()?, index, self.shuffling.len()) } /// Returns the index of some validator in `self.shuffling`. @@ -329,8 +346,10 @@ pub fn compute_committee_index_in_epoch( slots_per_epoch: usize, committees_per_slot: usize, committee_index: usize, -) -> usize { - (slot.as_usize() % slots_per_epoch) * committees_per_slot + committee_index +) -> Result<usize, ArithError> { + (slot.as_usize().safe_rem(slots_per_epoch)?) + .safe_mul(committees_per_slot)? + .safe_add(committee_index) } /// Computes the range for slicing the shuffled indices to determine the members of a committee. @@ -341,20 +360,16 @@ pub fn compute_committee_range_in_epoch( epoch_committee_count: usize, index_in_epoch: usize, shuffling_len: usize, -) -> Option<Range<usize>> { +) -> Result<Option<Range<usize>>, ArithError> { if epoch_committee_count == 0 || index_in_epoch >= epoch_committee_count { - return None; + return Ok(None); } - let start = (shuffling_len * index_in_epoch) / epoch_committee_count; - let end = (shuffling_len * (index_in_epoch + 1)) / epoch_committee_count; + let start = (shuffling_len.safe_mul(index_in_epoch))?.safe_div(epoch_committee_count)?; + let end = + (shuffling_len.safe_mul(index_in_epoch.safe_add(1)?))?.safe_div(epoch_committee_count)?; - Some(start..end) -} - -/// Returns the total number of committees in an epoch. -pub fn epoch_committee_count(committees_per_slot: usize, slots_per_epoch: usize) -> usize { - committees_per_slot * slots_per_epoch + Ok(Some(start..end)) } /// Returns a list of all `validators` indices where the validator is active at the given diff --git a/consensus/types/src/state/mod.rs b/consensus/types/src/state/mod.rs index 309796d359..096bb67167 100644 --- a/consensus/types/src/state/mod.rs +++ b/consensus/types/src/state/mod.rs @@ -17,11 +17,11 @@ pub use balance::Balance; pub use beacon_state::{ BeaconState, BeaconStateAltair, BeaconStateBase, BeaconStateBellatrix, BeaconStateCapella, BeaconStateDeneb, BeaconStateElectra, BeaconStateError, BeaconStateFulu, BeaconStateGloas, - BeaconStateHash, BeaconStateRef, CACHED_EPOCHS, + BeaconStateHash, BeaconStateRef, CACHED_EPOCHS, DEFAULT_PRE_ELECTRA_WS_PERIOD, }; pub use committee_cache::{ CommitteeCache, compute_committee_index_in_epoch, compute_committee_range_in_epoch, - epoch_committee_count, get_active_validator_indices, + get_active_validator_indices, }; pub use epoch_cache::{EpochCache, EpochCacheError, EpochCacheKey}; pub use exit_cache::ExitCache; diff --git a/consensus/types/src/validator/mod.rs b/consensus/types/src/validator/mod.rs index 8a67407821..23f7a2a0e1 100644 --- a/consensus/types/src/validator/mod.rs +++ b/consensus/types/src/validator/mod.rs @@ -4,6 +4,8 @@ mod validator_registration_data; mod validator_subscription; pub use proposer_preparation_data::ProposerPreparationData; -pub use validator::{Validator, is_compounding_withdrawal_credential}; +pub use validator::{ + Validator, is_builder_withdrawal_credential, is_compounding_withdrawal_credential, +}; pub use validator_registration_data::{SignedValidatorRegistrationData, ValidatorRegistrationData}; pub use validator_subscription::ValidatorSubscription; diff --git a/consensus/types/src/validator/validator.rs b/consensus/types/src/validator/validator.rs index 7898ab9073..5c5bfc761f 100644 --- a/consensus/types/src/validator/validator.rs +++ b/consensus/types/src/validator/validator.rs @@ -319,6 +319,14 @@ pub fn is_compounding_withdrawal_credential( .unwrap_or(false) } +pub fn is_builder_withdrawal_credential(withdrawal_credentials: Hash256, spec: &ChainSpec) -> bool { + withdrawal_credentials + .as_slice() + .first() + .map(|prefix_byte| *prefix_byte == spec.builder_withdrawal_prefix_byte) + .unwrap_or(false) +} + #[cfg(test)] mod tests { use super::*; diff --git a/consensus/types/src/validator/validator_registration_data.rs b/consensus/types/src/validator/validator_registration_data.rs index a0a1df7dc5..df2293cbae 100644 --- a/consensus/types/src/validator/validator_registration_data.rs +++ b/consensus/types/src/validator/validator_registration_data.rs @@ -31,7 +31,7 @@ impl SignedValidatorRegistrationData { .pubkey .decompress() .map(|pubkey| { - let domain = spec.get_builder_domain(); + let domain = spec.get_builder_application_domain(); let message = self.message.signing_root(domain); self.signature.verify(&pubkey, message) }) diff --git a/consensus/types/tests/committee_cache.rs b/consensus/types/tests/committee_cache.rs index 751ef05d29..5c1962276f 100644 --- a/consensus/types/tests/committee_cache.rs +++ b/consensus/types/tests/committee_cache.rs @@ -21,6 +21,7 @@ fn get_harness<E: EthSpec>(validator_count: usize) -> BeaconChainHarness<Ephemer .default_spec() .keypairs(KEYPAIRS[0..validator_count].to_vec()) .fresh_ephemeral_store() + .mock_execution_layer() .build(); harness.advance_slot(); harness @@ -33,9 +34,9 @@ fn default_values() { assert!(!cache.is_initialized_at(Epoch::new(0))); assert!(&cache.active_validator_indices().is_empty()); assert_eq!(cache.get_beacon_committee(Slot::new(0), 0), None); - assert_eq!(cache.get_attestation_duties(0), None); + assert_eq!(cache.get_attestation_duties(0), Ok(None)); assert_eq!(cache.active_validator_count(), 0); - assert_eq!(cache.epoch_committee_count(), 0); + assert_eq!(cache.epoch_committee_count(), Ok(0)); assert!(cache.get_beacon_committees_at_slot(Slot::new(0)).is_err()); } diff --git a/consensus/types/tests/state.rs b/consensus/types/tests/state.rs index 63ab3b8084..5e223092cf 100644 --- a/consensus/types/tests/state.rs +++ b/consensus/types/tests/state.rs @@ -28,6 +28,7 @@ async fn get_harness<E: EthSpec>( .default_spec() .keypairs(KEYPAIRS[0..validator_count].to_vec()) .fresh_ephemeral_store() + .mock_execution_layer() .build(); let skip_to_slot = slot - SLOT_OFFSET; diff --git a/crypto/bls/Cargo.toml b/crypto/bls/Cargo.toml index 4661288679..ac04e1fecf 100644 --- a/crypto/bls/Cargo.toml +++ b/crypto/bls/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Paul Hauner <paul@paulhauner.com>"] edition = { workspace = true } [features] -arbitrary = [] +arbitrary = ["dep:arbitrary"] default = ["supranational"] fake_crypto = [] supranational = ["blst"] @@ -14,7 +14,7 @@ supranational-force-adx = ["supranational", "blst/force-adx"] [dependencies] alloy-primitives = { workspace = true } -arbitrary = { workspace = true } +arbitrary = { workspace = true, optional = true } blst = { version = "0.3.3", optional = true } ethereum_hashing = { workspace = true } ethereum_serde_utils = { workspace = true } diff --git a/crypto/bls/src/impls/fake_crypto.rs b/crypto/bls/src/impls/fake_crypto.rs index e7eee05077..5fe0c3baab 100644 --- a/crypto/bls/src/impls/fake_crypto.rs +++ b/crypto/bls/src/impls/fake_crypto.rs @@ -49,7 +49,9 @@ impl TPublicKey for PublicKey { } fn serialize_uncompressed(&self) -> [u8; PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN] { - panic!("fake_crypto does not support uncompressed keys") + let mut bytes = [0; PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN]; + bytes[0..PUBLIC_KEY_BYTES_LEN].copy_from_slice(&self.0); + bytes } fn deserialize(bytes: &[u8]) -> Result<Self, Error> { @@ -58,8 +60,17 @@ impl TPublicKey for PublicKey { Ok(pubkey) } - fn deserialize_uncompressed(_: &[u8]) -> Result<Self, Error> { - panic!("fake_crypto does not support uncompressed keys") + fn deserialize_uncompressed(bytes: &[u8]) -> Result<Self, Error> { + if bytes.len() == PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN { + let mut pubkey = Self([0; PUBLIC_KEY_BYTES_LEN]); + pubkey.0.copy_from_slice(&bytes[0..PUBLIC_KEY_BYTES_LEN]); + Ok(pubkey) + } else { + Err(Error::InvalidByteLength { + got: bytes.len(), + expected: PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN, + }) + } } } @@ -97,7 +108,7 @@ pub struct Signature([u8; SIGNATURE_BYTES_LEN]); impl Signature { fn infinity() -> Self { - Self([0; SIGNATURE_BYTES_LEN]) + Self(INFINITY_SIGNATURE) } } @@ -213,7 +224,11 @@ impl TSecretKey<Signature, PublicKey> for SecretKey { } fn public_key(&self) -> PublicKey { - PublicKey::infinity() + let mut bytes = [0; PUBLIC_KEY_BYTES_LEN]; + bytes[0] = 0x01; + let to_copy = std::cmp::min(self.0.len(), bytes.len() - 1); + bytes[1..1 + to_copy].copy_from_slice(&self.0[..to_copy]); + PublicKey(bytes) } fn sign(&self, _msg: Hash256) -> Signature { diff --git a/crypto/kzg/Cargo.toml b/crypto/kzg/Cargo.toml index 5a36eb74f7..840f8cfc9c 100644 --- a/crypto/kzg/Cargo.toml +++ b/crypto/kzg/Cargo.toml @@ -5,8 +5,13 @@ authors = ["Pawan Dhananjay <pawandhananjay@gmail.com>"] edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = [] +arbitrary = ["dep:arbitrary"] +fake_crypto = [] + [dependencies] -arbitrary = { workspace = true } +arbitrary = { workspace = true, optional = true } c-kzg = { workspace = true } educe = { workspace = true } ethereum_hashing = { workspace = true } diff --git a/crypto/kzg/src/kzg_commitment.rs b/crypto/kzg/src/kzg_commitment.rs index 5a5e689429..bc5fc5f5aa 100644 --- a/crypto/kzg/src/kzg_commitment.rs +++ b/crypto/kzg/src/kzg_commitment.rs @@ -114,6 +114,7 @@ impl Debug for KzgCommitment { } } +#[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for KzgCommitment { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> { let mut bytes = [0u8; BYTES_PER_COMMITMENT]; diff --git a/crypto/kzg/src/kzg_proof.rs b/crypto/kzg/src/kzg_proof.rs index 5a83466d0c..aa9ed185a0 100644 --- a/crypto/kzg/src/kzg_proof.rs +++ b/crypto/kzg/src/kzg_proof.rs @@ -110,6 +110,7 @@ impl Debug for KzgProof { } } +#[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for KzgProof { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> { let mut bytes = [0u8; BYTES_PER_PROOF]; diff --git a/crypto/kzg/src/lib.rs b/crypto/kzg/src/lib.rs index 0fe95b7723..66499dad8e 100644 --- a/crypto/kzg/src/lib.rs +++ b/crypto/kzg/src/lib.rs @@ -134,6 +134,9 @@ impl Kzg { kzg_commitment: KzgCommitment, kzg_proof: KzgProof, ) -> Result<(), Error> { + if cfg!(feature = "fake_crypto") { + return Ok(()); + } if !self.trusted_setup.verify_blob_kzg_proof( blob, &kzg_commitment.into(), @@ -155,6 +158,9 @@ impl Kzg { kzg_commitments: &[KzgCommitment], kzg_proofs: &[KzgProof], ) -> Result<(), Error> { + if cfg!(feature = "fake_crypto") { + return Ok(()); + } let commitments_bytes = kzg_commitments .iter() .map(|comm| Bytes48::from(*comm)) @@ -204,6 +210,9 @@ impl Kzg { y: &Bytes32, kzg_proof: KzgProof, ) -> Result<bool, Error> { + if cfg!(feature = "fake_crypto") { + return Ok(true); + } self.trusted_setup .verify_kzg_proof(&kzg_commitment.into(), z, y, &kzg_proof.into()) .map_err(Into::into) @@ -240,6 +249,9 @@ impl Kzg { indices: Vec<CellIndex>, kzg_commitments: &[Bytes48], ) -> Result<(), (Option<u64>, Error)> { + if cfg!(feature = "fake_crypto") { + return Ok(()); + } let mut column_groups: HashMap<u64, Vec<(CellRef, Bytes48, Bytes48)>> = HashMap::new(); let expected_len = cells.len(); diff --git a/deny.toml b/deny.toml index e6c30f6a48..3b230155f7 100644 --- a/deny.toml +++ b/deny.toml @@ -18,6 +18,7 @@ deny = [ { crate = "pbkdf2", deny-multiple-versions = true, reason = "takes a long time to compile" }, { crate = "scrypt", deny-multiple-versions = true, reason = "takes a long time to compile" }, { crate = "syn", deny-multiple-versions = true, reason = "takes a long time to compile" }, + { crate = "uuid", deny-multiple-versions = true, reason = "dependency hygiene" }, ] [sources] diff --git a/lcli/src/main.rs b/lcli/src/main.rs index a21dfd4386..63dd0f2c5b 100644 --- a/lcli/src/main.rs +++ b/lcli/src/main.rs @@ -492,10 +492,20 @@ fn main() { .long("jwt-output-path") .value_name("PATH") .action(ArgAction::Set) - .required(true) + .required_unless_present("jwt-secret-path") + .conflicts_with("jwt-secret-path") .help("Path to write the JWT secret.") .display_order(0) ) + .arg( + Arg::new("jwt-secret-path") + .long("jwt-secret-path") + .value_name("PATH") + .action(ArgAction::Set) + .help("Path to an existing hex-encoded JWT secret file. \ + When provided, this secret is used instead of the default.") + .display_order(0) + ) .arg( Arg::new("listen-address") .long("listen-address") diff --git a/lcli/src/mock_el.rs b/lcli/src/mock_el.rs index d6bdfb0d71..6086067a47 100644 --- a/lcli/src/mock_el.rs +++ b/lcli/src/mock_el.rs @@ -2,18 +2,16 @@ use clap::ArgMatches; use clap_utils::{parse_optional, parse_required}; use environment::Environment; use execution_layer::{ - auth::JwtKey, - test_utils::{ - Config, DEFAULT_JWT_SECRET, DEFAULT_TERMINAL_BLOCK, MockExecutionConfig, MockServer, - }, + auth::{JwtKey, strip_prefix}, + test_utils::{Config, DEFAULT_JWT_SECRET, MockExecutionConfig, MockServer}, }; use std::net::Ipv4Addr; use std::path::PathBuf; -use std::sync::Arc; use types::*; pub fn run<E: EthSpec>(mut env: Environment<E>, matches: &ArgMatches) -> Result<(), String> { - let jwt_path: PathBuf = parse_required(matches, "jwt-output-path")?; + let jwt_output_path: Option<PathBuf> = parse_optional(matches, "jwt-output-path")?; + let jwt_secret_path: Option<PathBuf> = parse_optional(matches, "jwt-secret-path")?; let listen_addr: Ipv4Addr = parse_required(matches, "listen-address")?; let listen_port: u16 = parse_required(matches, "listen-port")?; let all_payloads_valid: bool = parse_required(matches, "all-payloads-valid")?; @@ -24,9 +22,23 @@ pub fn run<E: EthSpec>(mut env: Environment<E>, matches: &ArgMatches) -> Result< let amsterdam_time = parse_optional(matches, "amsterdam-time")?; let handle = env.core_context().executor.handle().unwrap(); - let spec = Arc::new(E::default_spec()); - let jwt_key = JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap(); - std::fs::write(jwt_path, hex::encode(DEFAULT_JWT_SECRET)).unwrap(); + + let jwt_key = if let Some(secret_path) = jwt_secret_path { + let hex_str = std::fs::read_to_string(&secret_path) + .map_err(|e| format!("Failed to read JWT secret file: {}", e))?; + let secret_bytes = hex::decode(strip_prefix(hex_str.trim())) + .map_err(|e| format!("Invalid hex in JWT secret file: {}", e))?; + JwtKey::from_slice(&secret_bytes) + .map_err(|e| format!("Invalid JWT secret length (expected 32 bytes): {}", e))? + } else if let Some(jwt_path) = jwt_output_path { + let jwt_key = JwtKey::from_slice(&DEFAULT_JWT_SECRET) + .map_err(|e| format!("Default JWT secret invalid: {}", e))?; + std::fs::write(jwt_path, hex::encode(jwt_key.as_bytes())) + .map_err(|e| format!("Failed to write JWT secret to output path: {}", e))?; + jwt_key + } else { + return Err("either --jwt-secret-path or --jwt-output-path must be provided".to_string()); + }; let config = MockExecutionConfig { server_config: Config { @@ -34,9 +46,6 @@ pub fn run<E: EthSpec>(mut env: Environment<E>, matches: &ArgMatches) -> Result< listen_port, }, jwt_key, - terminal_difficulty: spec.terminal_total_difficulty, - terminal_block: DEFAULT_TERMINAL_BLOCK, - terminal_block_hash: spec.terminal_block_hash, shanghai_time: Some(shanghai_time), cancun_time, prague_time, diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index a2fad31f65..ded1f2b765 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -295,6 +295,21 @@ fn paranoid_block_proposal_on() { .with_config(|config| assert!(config.chain.paranoid_block_proposal)); } +#[test] +fn ignore_ws_check_enabled() { + CommandLineTest::new() + .flag("ignore-ws-check", None) + .run_with_zero_port() + .with_config(|config| assert!(config.chain.ignore_ws_check)); +} + +#[test] +fn ignore_ws_check_default() { + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| assert!(!config.chain.ignore_ws_check)); +} + #[test] fn reset_payload_statuses_default() { CommandLineTest::new() @@ -386,9 +401,9 @@ fn genesis_backfill_flag() { /// The genesis backfill flag should be enabled if historic states flag is set. #[test] -fn genesis_backfill_with_historic_flag() { +fn genesis_backfill_with_archive_flag() { CommandLineTest::new() - .flag("reconstruct-historic-states", None) + .flag("archive", None) .run_with_zero_port() .with_config(|config| assert!(config.chain.genesis_backfill)); } @@ -2015,17 +2030,24 @@ fn blob_prune_margin_epochs_on_startup_ten() { .with_config(|config| assert!(config.store.blob_prune_margin_epochs == 10)); } #[test] -fn reconstruct_historic_states_flag() { +fn archive_flag() { + CommandLineTest::new() + .flag("archive", None) + .run_with_zero_port() + .with_config(|config| assert!(config.chain.archive)); +} +#[test] +fn archive_flag_alias() { CommandLineTest::new() .flag("reconstruct-historic-states", None) .run_with_zero_port() - .with_config(|config| assert!(config.chain.reconstruct_historic_states)); + .with_config(|config| assert!(config.chain.archive)); } #[test] -fn no_reconstruct_historic_states_flag() { +fn no_archive_flag() { CommandLineTest::new() .run_with_zero_port() - .with_config(|config| assert!(!config.chain.reconstruct_historic_states)); + .with_config(|config| assert!(!config.chain.archive)); } #[test] fn epochs_per_migration_default() { diff --git a/lighthouse/tests/validator_manager.rs b/lighthouse/tests/validator_manager.rs index d6d720a561..9bad1cdc91 100644 --- a/lighthouse/tests/validator_manager.rs +++ b/lighthouse/tests/validator_manager.rs @@ -16,6 +16,7 @@ use validator_manager::{ list_validators::ListConfig, move_validators::{MoveConfig, PasswordSource, Validators}, }; +use zeroize::Zeroizing; const EXAMPLE_ETH1_ADDRESS: &str = "0x00000000219ab540356cBB839Cbe05303d7705Fa"; @@ -280,6 +281,40 @@ pub fn validator_import_using_both_file_flags() { .assert_failed(); } +#[test] +pub fn validator_import_keystore_file_without_password_flag_should_fail() { + CommandLineTest::validators_import() + .flag("--vc-token", Some("./token.json")) + .flag("--keystore-file", Some("./keystore.json")) + .assert_failed(); +} + +#[test] +pub fn validator_import_keystore_file_with_password_flag_should_pass() { + CommandLineTest::validators_import() + .flag("--vc-token", Some("./token.json")) + .flag("--keystore-file", Some("./keystore.json")) + .flag("--password", Some("abcd")) + .assert_success(|config| { + let expected = ImportConfig { + validators_file_path: None, + keystore_file_path: Some(PathBuf::from("./keystore.json")), + vc_url: SensitiveUrl::parse("http://localhost:5062").unwrap(), + vc_token_path: PathBuf::from("./token.json"), + ignore_duplicates: false, + password: Some(Zeroizing::new("abcd".into())), + fee_recipient: None, + builder_boost_factor: None, + gas_limit: None, + builder_proposals: None, + enabled: None, + prefer_builder_proposals: None, + }; + assert_eq!(expected, config); + println!("{:?}", expected); + }); +} + #[test] pub fn validator_import_missing_both_file_flags() { CommandLineTest::validators_import() @@ -287,6 +322,36 @@ pub fn validator_import_missing_both_file_flags() { .assert_failed(); } +#[test] +pub fn validator_import_fee_recipient_override() { + CommandLineTest::validators_import() + .flag("--validators-file", Some("./vals.json")) + .flag("--vc-token", Some("./token.json")) + .flag("--suggested-fee-recipient", Some(EXAMPLE_ETH1_ADDRESS)) + .flag("--gas-limit", Some("1337")) + .flag("--builder-proposals", Some("true")) + .flag("--builder-boost-factor", Some("150")) + .flag("--prefer-builder-proposals", Some("true")) + .flag("--enabled", Some("false")) + .assert_success(|config| { + let expected = ImportConfig { + validators_file_path: Some(PathBuf::from("./vals.json")), + keystore_file_path: None, + vc_url: SensitiveUrl::parse("http://localhost:5062").unwrap(), + vc_token_path: PathBuf::from("./token.json"), + ignore_duplicates: false, + password: None, + fee_recipient: Some(Address::from_str(EXAMPLE_ETH1_ADDRESS).unwrap()), + builder_boost_factor: Some(150), + gas_limit: Some(1337), + builder_proposals: Some(true), + enabled: Some(false), + prefer_builder_proposals: Some(true), + }; + assert_eq!(expected, config); + }); +} + #[test] pub fn validator_move_defaults() { CommandLineTest::validators_move() diff --git a/slasher/service/src/lib.rs b/slasher/service/src/lib.rs index ac15b49ee9..69ec59aa2c 100644 --- a/slasher/service/src/lib.rs +++ b/slasher/service/src/lib.rs @@ -1,3 +1,4 @@ +#![allow(clippy::result_large_err)] mod service; pub use service::SlasherService; diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index ed220fbe8c..782b554ff1 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -47,29 +47,8 @@ excluded_paths = [ "bls12-381-tests/hash_to_G2", "tests/.*/eip7732", "tests/.*/eip7805", - # TODO(gloas): remove these ignores as more Gloas operations are implemented - "tests/.*/gloas/operations/attester_slashing/.*", - "tests/.*/gloas/operations/block_header/.*", - "tests/.*/gloas/operations/bls_to_execution_change/.*", - "tests/.*/gloas/operations/consolidation_request/.*", - "tests/.*/gloas/operations/deposit/.*", - "tests/.*/gloas/operations/deposit_request/.*", - "tests/.*/gloas/operations/execution_payload/.*", - "tests/.*/gloas/operations/execution_payload_bid/.*", - "tests/.*/gloas/operations/payload_attestation/.*", - "tests/.*/gloas/operations/proposer_slashing/.*", - "tests/.*/gloas/operations/sync_aggregate/.*", - "tests/.*/gloas/operations/voluntary_exit/.*", - "tests/.*/gloas/operations/withdrawal_request/.*", - # TODO(EIP-7732): remove these ignores as Gloas consensus is implemented - "tests/.*/gloas/epoch_processing/.*", - "tests/.*/gloas/finality/.*", - "tests/.*/gloas/fork/.*", + # TODO(gloas): remove these ignores as Gloas consensus is implemented "tests/.*/gloas/fork_choice/.*", - "tests/.*/gloas/networking/.*", - "tests/.*/gloas/rewards/.*", - "tests/.*/gloas/sanity/.*", - "tests/.*/gloas/transition/.*", # Ignore MatrixEntry SSZ tests for now. "tests/.*/.*/ssz_static/MatrixEntry/.*", # TODO(gloas): Ignore Gloas light client stuff for now @@ -88,8 +67,6 @@ excluded_paths = [ # Ignore full epoch tests for now (just test the sub-transitions). "tests/.*/.*/epoch_processing/.*/pre_epoch.ssz_snappy", "tests/.*/.*/epoch_processing/.*/post_epoch.ssz_snappy", - # Ignore inactivity_scores tests for now (should implement soon). - "tests/.*/.*/rewards/inactivity_scores/.*", # Ignore KZG tests that target internal kzg library functions "tests/.*/compute_verify_cell_kzg_proof_batch_challenge/.*", "tests/.*/compute_challenge/.*", diff --git a/testing/ef_tests/download_test_vectors.sh b/testing/ef_tests/download_test_vectors.sh index 21f74e817f..ff5b61bb47 100755 --- a/testing/ef_tests/download_test_vectors.sh +++ b/testing/ef_tests/download_test_vectors.sh @@ -4,7 +4,7 @@ set -Eeuo pipefail TESTS=("general" "minimal" "mainnet") version=${1} -if [[ "$version" == "nightly" ]]; then +if [[ "$version" == "nightly" || "$version" =~ ^nightly-[0-9]+$ ]]; then if [[ -z "${GITHUB_TOKEN:-}" ]]; then echo "Error GITHUB_TOKEN is not set" exit 1 @@ -21,9 +21,13 @@ if [[ "$version" == "nightly" ]]; then api="https://api.github.com" auth_header="Authorization: token ${GITHUB_TOKEN}" - run_id=$(curl -s -H "${auth_header}" \ - "${api}/repos/${repo}/actions/workflows/generate_vectors.yml/runs?branch=dev&status=success&per_page=1" | - jq -r '.workflow_runs[0].id') + if [[ "$version" == "nightly" ]]; then + run_id=$(curl --fail -s -H "${auth_header}" \ + "${api}/repos/${repo}/actions/workflows/nightly-reftests.yml/runs?branch=master&status=success&per_page=1" | + jq -r '.workflow_runs[0].id') + else + run_id="${version#nightly-}" + fi if [[ "${run_id}" == "null" || -z "${run_id}" ]]; then echo "No successful nightly workflow run found" @@ -31,7 +35,7 @@ if [[ "$version" == "nightly" ]]; then fi echo "Downloading nightly test vectors for run: ${run_id}" - curl -s -H "${auth_header}" "${api}/repos/${repo}/actions/runs/${run_id}/artifacts" | + curl --fail -H "${auth_header}" "${api}/repos/${repo}/actions/runs/${run_id}/artifacts" | jq -c '.artifacts[] | {name, url: .archive_download_url}' | while read -r artifact; do name=$(echo "${artifact}" | jq -r .name) diff --git a/testing/ef_tests/src/cases/epoch_processing.rs b/testing/ef_tests/src/cases/epoch_processing.rs index f143643ec3..7a90fc70d0 100644 --- a/testing/ef_tests/src/cases/epoch_processing.rs +++ b/testing/ef_tests/src/cases/epoch_processing.rs @@ -79,6 +79,8 @@ pub struct InactivityUpdates; pub struct ParticipationFlagUpdates; #[derive(Debug)] pub struct ProposerLookahead; +#[derive(Debug)] +pub struct BuilderPendingPayments; type_name!( JustificationAndFinalization, @@ -100,6 +102,7 @@ type_name!(SyncCommitteeUpdates, "sync_committee_updates"); type_name!(InactivityUpdates, "inactivity_updates"); type_name!(ParticipationFlagUpdates, "participation_flag_updates"); type_name!(ProposerLookahead, "proposer_lookahead"); +type_name!(BuilderPendingPayments, "builder_pending_payments"); impl<E: EthSpec> EpochTransition<E> for JustificationAndFinalization { fn run(state: &mut BeaconState<E>, spec: &ChainSpec) -> Result<(), EpochProcessingError> { @@ -293,6 +296,20 @@ impl<E: EthSpec> EpochTransition<E> for ProposerLookahead { } } +impl<E: EthSpec> EpochTransition<E> for BuilderPendingPayments { + fn run(state: &mut BeaconState<E>, spec: &ChainSpec) -> Result<(), EpochProcessingError> { + process_epoch_single_pass( + state, + spec, + SinglePassConfig { + builder_pending_payments: true, + ..SinglePassConfig::disable_all() + }, + ) + .map(|_| ()) + } +} + impl<E: EthSpec, T: EpochTransition<E>> LoadCase for EpochProcessing<E, T> { fn load_from_dir(path: &Path, fork_name: ForkName) -> Result<Self, Error> { let spec = &testing_spec::<E>(fork_name); @@ -356,6 +373,10 @@ impl<E: EthSpec, T: EpochTransition<E>> Case for EpochProcessing<E, T> { return false; } + if !fork_name.gloas_enabled() && T::name() == "builder_pending_payments" { + return false; + } + true } diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index c62a2593ae..9eb9855df9 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -448,7 +448,7 @@ impl<E: EthSpec> Tester<E> { .spec(spec.clone()) .keypairs(vec![]) .chain_config(ChainConfig { - reconstruct_historic_states: true, + archive: true, ..ChainConfig::default() }) .genesis_state_ephemeral_store(case.anchor_state.clone()) diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index 9133378ac5..ca0124e1aa 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -5,20 +5,23 @@ use crate::decode::{ssz_decode_file, ssz_decode_file_with, ssz_decode_state, yam use serde::Deserialize; use ssz::Decode; use state_processing::common::update_progressive_balances_cache::initialize_progressive_balances_cache; +use state_processing::envelope_processing::VerifyStateRoot; use state_processing::epoch_cache::initialize_epoch_cache; use state_processing::per_block_processing::process_operations::{ - process_consolidation_requests, process_deposit_requests, process_withdrawal_requests, + process_consolidation_requests, process_deposit_requests_post_gloas, + process_deposit_requests_pre_gloas, process_withdrawal_requests, }; use state_processing::{ ConsensusContext, + envelope_processing::{EnvelopeProcessingError, process_execution_payload_envelope}, per_block_processing::{ VerifyBlockRoot, VerifySignatures, errors::BlockProcessingError, - process_block_header, process_execution_payload, + process_block_header, process_execution_payload, process_execution_payload_bid, process_operations::{ altair_deneb, base, gloas, process_attester_slashings, process_bls_to_execution_changes, process_deposits, process_exits, - process_proposer_slashings, + process_payload_attestation, process_proposer_slashings, }, process_sync_aggregate, withdrawals, }, @@ -28,8 +31,9 @@ use types::{ Attestation, AttesterSlashing, BeaconBlock, BeaconBlockBody, BeaconBlockBodyBellatrix, BeaconBlockBodyCapella, BeaconBlockBodyDeneb, BeaconBlockBodyElectra, BeaconBlockBodyFulu, BeaconState, BlindedPayload, ConsolidationRequest, Deposit, DepositRequest, ExecutionPayload, - ForkVersionDecode, FullPayload, ProposerSlashing, SignedBlsToExecutionChange, - SignedVoluntaryExit, SyncAggregate, WithdrawalRequest, + ForkVersionDecode, FullPayload, PayloadAttestation, ProposerSlashing, + SignedBlsToExecutionChange, SignedExecutionPayloadEnvelope, SignedVoluntaryExit, SyncAggregate, + WithdrawalRequest, }; #[derive(Debug, Clone, Default, Deserialize)] @@ -49,6 +53,12 @@ pub struct WithdrawalsPayload<E: EthSpec> { payload: Option<ExecutionPayload<E>>, } +/// Newtype for testing execution payload bids. +#[derive(Debug, Clone, Deserialize)] +pub struct ExecutionPayloadBidBlock<E: EthSpec> { + block: BeaconBlock<E>, +} + #[derive(Debug, Clone)] pub struct Operations<E: EthSpec, O: Operation<E>> { metadata: Metadata, @@ -59,6 +69,8 @@ pub struct Operations<E: EthSpec, O: Operation<E>> { } pub trait Operation<E: EthSpec>: Debug + Sync + Sized { + type Error: Debug; + fn handler_name() -> String; fn filename() -> String { @@ -76,10 +88,12 @@ pub trait Operation<E: EthSpec>: Debug + Sync + Sized { state: &mut BeaconState<E>, spec: &ChainSpec, _: &Operations<E, Self>, - ) -> Result<(), BlockProcessingError>; + ) -> Result<(), Self::Error>; } impl<E: EthSpec> Operation<E> for Attestation<E> { + type Error = BlockProcessingError; + fn handler_name() -> String { "attestation".into() } @@ -132,6 +146,8 @@ impl<E: EthSpec> Operation<E> for Attestation<E> { } impl<E: EthSpec> Operation<E> for AttesterSlashing<E> { + type Error = BlockProcessingError; + fn handler_name() -> String { "attester_slashing".into() } @@ -163,6 +179,8 @@ impl<E: EthSpec> Operation<E> for AttesterSlashing<E> { } impl<E: EthSpec> Operation<E> for Deposit { + type Error = BlockProcessingError; + fn handler_name() -> String { "deposit".into() } @@ -187,6 +205,8 @@ impl<E: EthSpec> Operation<E> for Deposit { } impl<E: EthSpec> Operation<E> for ProposerSlashing { + type Error = BlockProcessingError; + fn handler_name() -> String { "proposer_slashing".into() } @@ -214,6 +234,8 @@ impl<E: EthSpec> Operation<E> for ProposerSlashing { } impl<E: EthSpec> Operation<E> for SignedVoluntaryExit { + type Error = BlockProcessingError; + fn handler_name() -> String { "voluntary_exit".into() } @@ -238,6 +260,8 @@ impl<E: EthSpec> Operation<E> for SignedVoluntaryExit { } impl<E: EthSpec> Operation<E> for BeaconBlock<E> { + type Error = BlockProcessingError; + fn handler_name() -> String { "block_header".into() } @@ -269,6 +293,8 @@ impl<E: EthSpec> Operation<E> for BeaconBlock<E> { } impl<E: EthSpec> Operation<E> for SyncAggregate<E> { + type Error = BlockProcessingError; + fn handler_name() -> String { "sync_aggregate".into() } @@ -297,6 +323,8 @@ impl<E: EthSpec> Operation<E> for SyncAggregate<E> { } impl<E: EthSpec> Operation<E> for BeaconBlockBody<E, FullPayload<E>> { + type Error = BlockProcessingError; + fn handler_name() -> String { "execution_payload".into() } @@ -306,7 +334,7 @@ impl<E: EthSpec> Operation<E> for BeaconBlockBody<E, FullPayload<E>> { } fn is_enabled_for_fork(fork_name: ForkName) -> bool { - fork_name.bellatrix_enabled() + fork_name.bellatrix_enabled() && !fork_name.gloas_enabled() } fn decode(path: &Path, fork_name: ForkName, _spec: &ChainSpec) -> Result<Self, Error> { @@ -317,8 +345,7 @@ impl<E: EthSpec> Operation<E> for BeaconBlockBody<E, FullPayload<E>> { ForkName::Deneb => BeaconBlockBody::Deneb(<_>::from_ssz_bytes(bytes)?), ForkName::Electra => BeaconBlockBody::Electra(<_>::from_ssz_bytes(bytes)?), ForkName::Fulu => BeaconBlockBody::Fulu(<_>::from_ssz_bytes(bytes)?), - // TODO(EIP-7732): See if we need to handle Gloas here - _ => panic!(), + _ => panic!("Not supported after Gloas"), }) }) } @@ -340,7 +367,10 @@ impl<E: EthSpec> Operation<E> for BeaconBlockBody<E, FullPayload<E>> { } } } + impl<E: EthSpec> Operation<E> for BeaconBlockBody<E, BlindedPayload<E>> { + type Error = BlockProcessingError; + fn handler_name() -> String { "execution_payload".into() } @@ -350,7 +380,7 @@ impl<E: EthSpec> Operation<E> for BeaconBlockBody<E, BlindedPayload<E>> { } fn is_enabled_for_fork(fork_name: ForkName) -> bool { - fork_name.bellatrix_enabled() + fork_name.bellatrix_enabled() && !fork_name.gloas_enabled() } fn decode(path: &Path, fork_name: ForkName, _spec: &ChainSpec) -> Result<Self, Error> { @@ -377,8 +407,7 @@ impl<E: EthSpec> Operation<E> for BeaconBlockBody<E, BlindedPayload<E>> { let inner = <BeaconBlockBodyFulu<E, FullPayload<E>>>::from_ssz_bytes(bytes)?; BeaconBlockBody::Fulu(inner.clone_as_blinded()) } - // TODO(EIP-7732): See if we need to handle Gloas here - _ => panic!(), + _ => panic!("Not supported after Gloas"), }) }) } @@ -401,7 +430,84 @@ impl<E: EthSpec> Operation<E> for BeaconBlockBody<E, BlindedPayload<E>> { } } +impl<E: EthSpec> Operation<E> for SignedExecutionPayloadEnvelope<E> { + type Error = EnvelopeProcessingError; + + fn handler_name() -> String { + "execution_payload".into() + } + + fn filename() -> String { + "signed_envelope.ssz_snappy".into() + } + + fn is_enabled_for_fork(fork_name: ForkName) -> bool { + fork_name.gloas_enabled() + } + + fn decode(path: &Path, _: ForkName, _spec: &ChainSpec) -> Result<Self, Error> { + ssz_decode_file(path) + } + + fn apply_to( + &self, + state: &mut BeaconState<E>, + spec: &ChainSpec, + extra: &Operations<E, Self>, + ) -> Result<(), Self::Error> { + let valid = extra + .execution_metadata + .as_ref() + .is_some_and(|e| e.execution_valid); + if valid { + process_execution_payload_envelope( + state, + None, + self, + VerifySignatures::True, + VerifyStateRoot::True, + spec, + ) + } else { + Err(EnvelopeProcessingError::ExecutionInvalid) + } + } +} + +impl<E: EthSpec> Operation<E> for ExecutionPayloadBidBlock<E> { + type Error = BlockProcessingError; + + fn handler_name() -> String { + "execution_payload_bid".into() + } + + fn filename() -> String { + "block.ssz_snappy".into() + } + + fn is_enabled_for_fork(fork_name: ForkName) -> bool { + fork_name.gloas_enabled() + } + + fn decode(path: &Path, _fork_name: ForkName, spec: &ChainSpec) -> Result<Self, Error> { + ssz_decode_file_with(path, |bytes| BeaconBlock::from_ssz_bytes(bytes, spec)) + .map(|block| ExecutionPayloadBidBlock { block }) + } + + fn apply_to( + &self, + state: &mut BeaconState<E>, + spec: &ChainSpec, + _: &Operations<E, Self>, + ) -> Result<(), BlockProcessingError> { + process_execution_payload_bid(state, self.block.to_ref(), VerifySignatures::True, spec)?; + Ok(()) + } +} + impl<E: EthSpec> Operation<E> for WithdrawalsPayload<E> { + type Error = BlockProcessingError; + fn handler_name() -> String { "withdrawals".into() } @@ -448,6 +554,8 @@ impl<E: EthSpec> Operation<E> for WithdrawalsPayload<E> { } impl<E: EthSpec> Operation<E> for SignedBlsToExecutionChange { + type Error = BlockProcessingError; + fn handler_name() -> String { "bls_to_execution_change".into() } @@ -480,6 +588,8 @@ impl<E: EthSpec> Operation<E> for SignedBlsToExecutionChange { } impl<E: EthSpec> Operation<E> for WithdrawalRequest { + type Error = BlockProcessingError; + fn handler_name() -> String { "withdrawal_request".into() } @@ -504,6 +614,8 @@ impl<E: EthSpec> Operation<E> for WithdrawalRequest { } impl<E: EthSpec> Operation<E> for DepositRequest { + type Error = BlockProcessingError; + fn handler_name() -> String { "deposit_request".into() } @@ -522,11 +634,17 @@ impl<E: EthSpec> Operation<E> for DepositRequest { spec: &ChainSpec, _extra: &Operations<E, Self>, ) -> Result<(), BlockProcessingError> { - process_deposit_requests(state, std::slice::from_ref(self), spec) + if state.fork_name_unchecked().gloas_enabled() { + process_deposit_requests_post_gloas(state, std::slice::from_ref(self), spec) + } else { + process_deposit_requests_pre_gloas(state, std::slice::from_ref(self), spec) + } } } impl<E: EthSpec> Operation<E> for ConsolidationRequest { + type Error = BlockProcessingError; + fn handler_name() -> String { "consolidation_request".into() } @@ -550,6 +668,32 @@ impl<E: EthSpec> Operation<E> for ConsolidationRequest { } } +impl<E: EthSpec> Operation<E> for PayloadAttestation<E> { + type Error = BlockProcessingError; + + fn handler_name() -> String { + "payload_attestation".into() + } + + fn is_enabled_for_fork(fork_name: ForkName) -> bool { + fork_name.gloas_enabled() + } + + fn decode(path: &Path, _fork_name: ForkName, _spec: &ChainSpec) -> Result<Self, Error> { + ssz_decode_file(path) + } + + fn apply_to( + &self, + state: &mut BeaconState<E>, + spec: &ChainSpec, + _extra: &Operations<E, Self>, + ) -> Result<(), BlockProcessingError> { + let mut ctxt = ConsensusContext::new(state.slot()); + process_payload_attestation(state, self, 0, VerifySignatures::True, &mut ctxt, spec) + } +} + impl<E: EthSpec, O: Operation<E>> LoadCase for Operations<E, O> { fn load_from_dir(path: &Path, fork_name: ForkName) -> Result<Self, Error> { let spec = &testing_spec::<E>(fork_name); diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index 39ddff46e7..da3c5533b6 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -22,7 +22,7 @@ pub trait Handler { // Add forks here to exclude them from EF spec testing. Helpful for adding future or // unspecified forks. fn disabled_forks(&self) -> Vec<ForkName> { - vec![ForkName::Gloas] + vec![] } fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { @@ -395,11 +395,6 @@ where T::name().into() } - fn disabled_forks(&self) -> Vec<ForkName> { - // TODO(gloas): Can be removed once we enable Gloas on all tests - vec![] - } - fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { self.supported_forks.contains(&fork_name) } @@ -422,11 +417,6 @@ where fn handler_name(&self) -> String { BeaconState::<E>::name().into() } - - fn disabled_forks(&self) -> Vec<ForkName> { - // TODO(gloas): Can be removed once we enable Gloas on all tests - vec![] - } } impl<T, E> Handler for SszStaticWithSpecHandler<T, E> @@ -449,11 +439,6 @@ where T::name().into() } - fn disabled_forks(&self) -> Vec<ForkName> { - // TODO(gloas): Can be removed once we enable Gloas on all tests - vec![] - } - fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { self.supported_forks.contains(&fork_name) } @@ -552,6 +537,11 @@ impl<E: EthSpec + TypeName> Handler for RandomHandler<E> { fn handler_name(&self) -> String { "random".into() } + + fn disabled_forks(&self) -> Vec<ForkName> { + // TODO(gloas): remove once we have Gloas random tests + vec![ForkName::Gloas] + } } #[derive(Educe)] @@ -602,6 +592,15 @@ impl<E: EthSpec + TypeName> Handler for RewardsHandler<E> { fn handler_name(&self) -> String { self.handler_name.to_string() } + + fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { + if self.handler_name == "inactivity_scores" { + // These tests were added in v1.7.0-alpha.2 and are available for Altair and later. + fork_name.altair_enabled() + } else { + true + } + } } #[derive(Educe)] @@ -726,6 +725,11 @@ impl<E: EthSpec + TypeName> Handler for ForkChoiceHandler<E> { // run them with fake crypto. cfg!(not(feature = "fake_crypto")) } + + fn disabled_forks(&self) -> Vec<ForkName> { + // TODO(gloas): remove once we have Gloas fork choice tests + vec![ForkName::Gloas] + } } #[derive(Educe)] @@ -755,6 +759,11 @@ impl<E: EthSpec + TypeName> Handler for OptimisticSyncHandler<E> { fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { fork_name.bellatrix_enabled() && cfg!(not(feature = "fake_crypto")) } + + fn disabled_forks(&self) -> Vec<ForkName> { + // TODO(gloas): remove once we have Gloas optimistic sync tests + vec![ForkName::Gloas] + } } #[derive(Educe)] @@ -975,6 +984,11 @@ impl<E: EthSpec> Handler for KZGComputeCellsHandler<E> { fn handler_name(&self) -> String { "compute_cells".into() } + + fn disabled_forks(&self) -> Vec<ForkName> { + // TODO(gloas): remove once we have Gloas KZG tests + vec![ForkName::Gloas] + } } #[derive(Educe)] @@ -995,6 +1009,11 @@ impl<E: EthSpec> Handler for KZGComputeCellsAndKZGProofHandler<E> { fn handler_name(&self) -> String { "compute_cells_and_kzg_proofs".into() } + + fn disabled_forks(&self) -> Vec<ForkName> { + // TODO(gloas): remove once we have Gloas KZG tests + vec![ForkName::Gloas] + } } #[derive(Educe)] @@ -1015,6 +1034,11 @@ impl<E: EthSpec> Handler for KZGVerifyCellKZGProofBatchHandler<E> { fn handler_name(&self) -> String { "verify_cell_kzg_proof_batch".into() } + + fn disabled_forks(&self) -> Vec<ForkName> { + // TODO(gloas): remove once we have Gloas KZG tests + vec![ForkName::Gloas] + } } #[derive(Educe)] @@ -1035,6 +1059,11 @@ impl<E: EthSpec> Handler for KZGRecoverCellsAndKZGProofHandler<E> { fn handler_name(&self) -> String { "recover_cells_and_kzg_proofs".into() } + + fn disabled_forks(&self) -> Vec<ForkName> { + // TODO(gloas): remove once we have Gloas KZG tests + vec![ForkName::Gloas] + } } #[derive(Educe)] @@ -1059,6 +1088,11 @@ impl<E: EthSpec + TypeName> Handler for KzgInclusionMerkleProofValidityHandler<E fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { fork_name.deneb_enabled() } + + fn disabled_forks(&self) -> Vec<ForkName> { + // TODO(gloas): remove once we have Gloas KZG merkle proof tests + vec![ForkName::Gloas] + } } #[derive(Educe)] @@ -1083,6 +1117,11 @@ impl<E: EthSpec + TypeName> Handler for MerkleProofValidityHandler<E> { fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { fork_name.altair_enabled() } + + fn disabled_forks(&self) -> Vec<ForkName> { + // TODO(gloas): remove once we have Gloas light client tests + vec![ForkName::Gloas] + } } #[derive(Educe)] @@ -1108,6 +1147,11 @@ impl<E: EthSpec + TypeName> Handler for LightClientUpdateHandler<E> { // Enabled in Altair fork_name.altair_enabled() } + + fn disabled_forks(&self) -> Vec<ForkName> { + // TODO(gloas): remove once we have Gloas light client tests + vec![ForkName::Gloas] + } } #[derive(Educe)] @@ -1128,19 +1172,6 @@ impl<E: EthSpec + TypeName, O: Operation<E>> Handler for OperationsHandler<E, O> fn handler_name(&self) -> String { O::handler_name() } - - fn disabled_forks(&self) -> Vec<ForkName> { - // TODO(gloas): Can be removed once we enable Gloas on all tests - vec![] - } - - fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { - // TODO(gloas): So far only withdrawals tests are enabled for Gloas. - Self::Case::is_enabled_for_fork(fork_name) - && (!fork_name.gloas_enabled() - || self.handler_name() == "withdrawals" - || self.handler_name() == "attestation") - } } #[derive(Educe)] diff --git a/testing/ef_tests/src/lib.rs b/testing/ef_tests/src/lib.rs index 8ec4860cab..94b19b6644 100644 --- a/testing/ef_tests/src/lib.rs +++ b/testing/ef_tests/src/lib.rs @@ -1,11 +1,11 @@ pub use case_result::CaseResult; -pub use cases::WithdrawalsPayload; pub use cases::{ - Case, EffectiveBalanceUpdates, Eth1DataReset, FeatureName, HistoricalRootsUpdate, - HistoricalSummariesUpdate, InactivityUpdates, JustificationAndFinalization, - ParticipationFlagUpdates, ParticipationRecordUpdates, PendingBalanceDeposits, - PendingConsolidations, ProposerLookahead, RandaoMixesReset, RegistryUpdates, - RewardsAndPenalties, Slashings, SlashingsReset, SyncCommitteeUpdates, + BuilderPendingPayments, Case, EffectiveBalanceUpdates, Eth1DataReset, ExecutionPayloadBidBlock, + FeatureName, HistoricalRootsUpdate, HistoricalSummariesUpdate, InactivityUpdates, + JustificationAndFinalization, ParticipationFlagUpdates, ParticipationRecordUpdates, + PendingBalanceDeposits, PendingConsolidations, ProposerLookahead, RandaoMixesReset, + RegistryUpdates, RewardsAndPenalties, Slashings, SlashingsReset, SyncCommitteeUpdates, + WithdrawalsPayload, }; pub use decode::log_file_access; pub use error::Error; diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index b47d39a6fa..3893df2ef7 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -87,6 +87,24 @@ fn operations_execution_payload_blinded() { OperationsHandler::<MainnetEthSpec, BeaconBlockBody<_, BlindedPayload<_>>>::default().run(); } +#[test] +fn operations_execution_payload_envelope() { + OperationsHandler::<MinimalEthSpec, SignedExecutionPayloadEnvelope<_>>::default().run(); + OperationsHandler::<MainnetEthSpec, SignedExecutionPayloadEnvelope<_>>::default().run(); +} + +#[test] +fn operations_execution_payload_bid() { + OperationsHandler::<MinimalEthSpec, ExecutionPayloadBidBlock<_>>::default().run(); + OperationsHandler::<MainnetEthSpec, ExecutionPayloadBidBlock<_>>::default().run(); +} + +#[test] +fn operations_payload_attestation() { + OperationsHandler::<MinimalEthSpec, PayloadAttestation<_>>::default().run(); + OperationsHandler::<MainnetEthSpec, PayloadAttestation<_>>::default().run(); +} + #[test] fn operations_withdrawals() { OperationsHandler::<MinimalEthSpec, WithdrawalsPayload<_>>::default().run(); @@ -94,7 +112,7 @@ fn operations_withdrawals() { } #[test] -fn operations_withdrawal_reqeusts() { +fn operations_withdrawal_requests() { OperationsHandler::<MinimalEthSpec, WithdrawalRequest>::default().run(); OperationsHandler::<MainnetEthSpec, WithdrawalRequest>::default().run(); } @@ -942,6 +960,12 @@ fn epoch_processing_proposer_lookahead() { EpochProcessingHandler::<MainnetEthSpec, ProposerLookahead>::default().run(); } +#[test] +fn epoch_processing_builder_pending_payments() { + EpochProcessingHandler::<MinimalEthSpec, BuilderPendingPayments>::default().run(); + EpochProcessingHandler::<MainnetEthSpec, BuilderPendingPayments>::default().run(); +} + #[test] fn fork_upgrade() { ForkHandler::<MinimalEthSpec>::default().run(); @@ -1095,7 +1119,7 @@ fn kzg_inclusion_merkle_proof_validity() { #[test] fn rewards() { - for handler in &["basic", "leak", "random"] { + for handler in &["basic", "leak", "random", "inactivity_scores"] { RewardsHandler::<MinimalEthSpec>::new(handler).run(); RewardsHandler::<MainnetEthSpec>::new(handler).run(); } diff --git a/testing/execution_engine_integration/src/test_rig.rs b/testing/execution_engine_integration/src/test_rig.rs index 24d75f5a11..6bf4a1aa52 100644 --- a/testing/execution_engine_integration/src/test_rig.rs +++ b/testing/execution_engine_integration/src/test_rig.rs @@ -9,8 +9,8 @@ use alloy_signer_local::PrivateKeySigner; use bls::PublicKeyBytes; use execution_layer::test_utils::DEFAULT_GAS_LIMIT; use execution_layer::{ - BlockProposalContentsType, BuilderParams, ChainHealth, ExecutionLayer, PayloadAttributes, - PayloadParameters, PayloadStatus, + BlockByNumberQuery, BlockProposalContentsType, BuilderParams, ChainHealth, ExecutionLayer, + LATEST_TAG, PayloadAttributes, PayloadParameters, PayloadStatus, }; use fixed_bytes::FixedBytesExtended; use fork_choice::ForkchoiceUpdateParameters; @@ -210,25 +210,29 @@ impl<Engine: GenericExecutionEngine> TestRig<Engine> { let account2 = AlloyAddress::from_slice(&hex::decode(ACCOUNT2).unwrap()); /* - * Read the terminal block hash from both pairs, check it's equal. + * Read the genesis block hash from both pairs, check it's equal. + * Since TTD=0, the genesis block is the terminal PoW block. */ - let terminal_pow_block_hash = self + let genesis_block = self .ee_a .execution_layer - .get_terminal_pow_block_hash(&self.spec, timestamp_now()) + .get_block_by_number(BlockByNumberQuery::Tag(LATEST_TAG)) .await .unwrap() - .unwrap(); + .expect("should have genesis block"); + + let terminal_pow_block_hash = genesis_block.block_hash; assert_eq!( terminal_pow_block_hash, self.ee_b .execution_layer - .get_terminal_pow_block_hash(&self.spec, timestamp_now()) + .get_block_by_number(BlockByNumberQuery::Tag(LATEST_TAG)) .await .unwrap() - .unwrap() + .expect("should have genesis block") + .block_hash ); // Submit transactions before getting payload @@ -563,7 +567,7 @@ impl<Engine: GenericExecutionEngine> TestRig<Engine> { * * Indicate that the payload is the head of the chain, providing payload attributes. */ - let head_block_hash = valid_payload.block_hash(); + let head_block_hash = second_payload.block_hash(); let finalized_block_hash = ExecutionBlockHash::zero(); // To save sending proposer preparation data, just set the fee recipient // to the fee recipient configured for EE A. diff --git a/testing/node_test_rig/Cargo.toml b/testing/node_test_rig/Cargo.toml index 0d9db528da..21ec6fac12 100644 --- a/testing/node_test_rig/Cargo.toml +++ b/testing/node_test_rig/Cargo.toml @@ -10,6 +10,7 @@ beacon_node_fallback = { workspace = true } environment = { workspace = true } eth2 = { workspace = true } execution_layer = { workspace = true } +reqwest = { workspace = true } sensitive_url = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true } diff --git a/testing/node_test_rig/src/lib.rs b/testing/node_test_rig/src/lib.rs index e49d11ee1e..76a5b7ddb2 100644 --- a/testing/node_test_rig/src/lib.rs +++ b/testing/node_test_rig/src/lib.rs @@ -4,7 +4,8 @@ use beacon_node::ProductionBeaconNode; use environment::RuntimeContext; -use eth2::{BeaconNodeHttpClient, Timeouts, reqwest::ClientBuilder}; +use eth2::{BeaconNodeHttpClient, Timeouts}; +use reqwest::ClientBuilder; use sensitive_url::SensitiveUrl; use std::path::PathBuf; use std::time::Duration; @@ -115,7 +116,7 @@ pub fn testing_client_config() -> ClientConfig { }; // Simulator tests expect historic states to be available for post-run checks. - client_config.chain.reconstruct_historic_states = true; + client_config.chain.archive = true; // Specify a constant count of beacon processor workers. Having this number // too low can cause annoying HTTP timeouts, especially on Github runners diff --git a/testing/state_transition_vectors/src/exit.rs b/testing/state_transition_vectors/src/exit.rs index f8ece0218f..3b0fe7d8ec 100644 --- a/testing/state_transition_vectors/src/exit.rs +++ b/testing/state_transition_vectors/src/exit.rs @@ -1,4 +1,5 @@ use super::*; +use beacon_chain::test_utils::test_spec; use state_processing::{ BlockProcessingError, BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, per_block_processing, per_block_processing::errors::ExitInvalid, @@ -70,13 +71,13 @@ impl ExitTest { BlockSignatureStrategy::VerifyIndividual, VerifyBlockRoot::True, &mut ctxt, - &E::default_spec(), + &test_spec::<E>(), ) } #[cfg(all(test, not(debug_assertions)))] async fn run(self) -> BeaconState<E> { - let spec = &E::default_spec(); + let spec = &test_spec::<E>(); let expected = self.expected.clone(); assert_eq!(STATE_EPOCH, spec.shard_committee_period); diff --git a/testing/state_transition_vectors/src/main.rs b/testing/state_transition_vectors/src/main.rs index 80c30489b7..6a212f034d 100644 --- a/testing/state_transition_vectors/src/main.rs +++ b/testing/state_transition_vectors/src/main.rs @@ -57,6 +57,7 @@ async fn get_harness<E: EthSpec>( .default_spec() .keypairs(KEYPAIRS[0..validator_count].to_vec()) .fresh_ephemeral_store() + .mock_execution_layer() .build(); let skip_to_slot = slot - SLOT_OFFSET; if skip_to_slot > Slot::new(0) { diff --git a/testing/validator_test_rig/Cargo.toml b/testing/validator_test_rig/Cargo.toml index f28a423433..2057a9fdc8 100644 --- a/testing/validator_test_rig/Cargo.toml +++ b/testing/validator_test_rig/Cargo.toml @@ -7,6 +7,7 @@ edition = { workspace = true } eth2 = { workspace = true } mockito = { workspace = true } regex = { workspace = true } +reqwest = { workspace = true } sensitive_url = { workspace = true } serde_json = { workspace = true } tracing = { workspace = true } diff --git a/testing/validator_test_rig/src/mock_beacon_node.rs b/testing/validator_test_rig/src/mock_beacon_node.rs index ff1e772d54..1ecdd85f3b 100644 --- a/testing/validator_test_rig/src/mock_beacon_node.rs +++ b/testing/validator_test_rig/src/mock_beacon_node.rs @@ -1,7 +1,8 @@ use eth2::types::{GenericResponse, SyncingData}; -use eth2::{BeaconNodeHttpClient, StatusCode, Timeouts}; +use eth2::{BeaconNodeHttpClient, Timeouts}; use mockito::{Matcher, Mock, Server, ServerGuard}; use regex::Regex; +use reqwest::StatusCode; use sensitive_url::SensitiveUrl; use std::marker::PhantomData; use std::str::FromStr; diff --git a/validator_client/http_api/Cargo.toml b/validator_client/http_api/Cargo.toml index 2bd57867ac..e334ab9db0 100644 --- a/validator_client/http_api/Cargo.toml +++ b/validator_client/http_api/Cargo.toml @@ -8,14 +8,17 @@ authors = ["Sigma Prime <contact@sigmaprime.io>"] name = "validator_http_api" path = "src/lib.rs" +[features] +testing = ["dep:deposit_contract", "dep:doppelganger_service", "dep:tempfile"] + [dependencies] account_utils = { workspace = true } beacon_node_fallback = { workspace = true } bls = { workspace = true } -deposit_contract = { workspace = true } +deposit_contract = { workspace = true, optional = true } directory = { workspace = true } dirs = { workspace = true } -doppelganger_service = { workspace = true } +doppelganger_service = { workspace = true, optional = true } eth2 = { workspace = true, features = ["lighthouse"] } eth2_keystore = { workspace = true } ethereum_serde_utils = { workspace = true } @@ -38,7 +41,7 @@ slot_clock = { workspace = true } sysinfo = { workspace = true } system_health = { workspace = true } task_executor = { workspace = true } -tempfile = { workspace = true } +tempfile = { workspace = true, optional = true } tokio = { workspace = true } tokio-stream = { workspace = true } tracing = { workspace = true } @@ -53,7 +56,10 @@ warp_utils = { workspace = true } zeroize = { workspace = true } [dev-dependencies] +deposit_contract = { workspace = true } +doppelganger_service = { workspace = true } futures = { workspace = true } itertools = { workspace = true } rand = { workspace = true, features = ["small_rng"] } ssz_types = { workspace = true } +tempfile = { workspace = true } diff --git a/validator_client/http_api/src/lib.rs b/validator_client/http_api/src/lib.rs index a35b4ec6c6..8e9c077e57 100644 --- a/validator_client/http_api/src/lib.rs +++ b/validator_client/http_api/src/lib.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "testing")] +pub mod test_utils; + mod api_secret; mod create_signed_voluntary_exit; mod create_validator; @@ -6,7 +9,6 @@ mod keystores; mod remotekeys; mod tests; -pub mod test_utils; pub use api_secret::PK_FILENAME; use graffiti::{delete_graffiti, get_graffiti, set_graffiti}; diff --git a/validator_client/lighthouse_validator_store/src/lib.rs b/validator_client/lighthouse_validator_store/src/lib.rs index 7b6a582363..7806482ffb 100644 --- a/validator_client/lighthouse_validator_store/src/lib.rs +++ b/validator_client/lighthouse_validator_store/src/lib.rs @@ -20,12 +20,12 @@ use task_executor::TaskExecutor; use tracing::{error, info, instrument, warn}; use types::{ AbstractExecPayload, Address, AggregateAndProof, Attestation, BeaconBlock, BlindedPayload, - ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, Fork, Graffiti, Hash256, - SelectionProof, SignedAggregateAndProof, SignedBeaconBlock, SignedContributionAndProof, - SignedRoot, SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, - SyncAggregatorSelectionData, SyncCommitteeContribution, SyncCommitteeMessage, - SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, VoluntaryExit, - graffiti::GraffitiString, + ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, ExecutionPayloadEnvelope, Fork, + FullPayload, Graffiti, Hash256, SelectionProof, SignedAggregateAndProof, SignedBeaconBlock, + SignedContributionAndProof, SignedExecutionPayloadEnvelope, SignedRoot, + SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, SyncAggregatorSelectionData, + SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, + ValidatorRegistrationData, VoluntaryExit, graffiti::GraffitiString, }; use validator_store::{ DoppelgangerStatus, Error as ValidatorStoreError, ProposalData, SignedBlock, UnsignedBlock, @@ -954,7 +954,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore for LighthouseValidatorS &self, validator_registration_data: ValidatorRegistrationData, ) -> Result<SignedValidatorRegistrationData, Error> { - let domain_hash = self.spec.get_builder_domain(); + let domain_hash = self.spec.get_builder_application_domain(); let signing_root = validator_registration_data.signing_root(domain_hash); let signing_method = @@ -1242,4 +1242,35 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore for LighthouseValidatorS .get_builder_proposals_defaulting(validator.get_builder_proposals()), }) } + + /// Sign an `ExecutionPayloadEnvelope` for Gloas (local building). + /// The proposer acts as the builder and signs with the BeaconBuilder domain. + async fn sign_execution_payload_envelope( + &self, + validator_pubkey: PublicKeyBytes, + envelope: ExecutionPayloadEnvelope<E>, + ) -> Result<SignedExecutionPayloadEnvelope<E>, Error> { + let signing_context = self.signing_context( + Domain::BeaconBuilder, + envelope.slot.epoch(E::slots_per_epoch()), + ); + + // Execution payload envelope signing is not slashable, bypass doppelganger protection. + let signing_method = self.doppelganger_bypassed_signing_method(validator_pubkey)?; + + let signature = signing_method + .get_signature::<E, FullPayload<E>>( + SignableMessage::ExecutionPayloadEnvelope(&envelope), + signing_context, + &self.spec, + &self.task_executor, + ) + .await + .map_err(Error::SpecificError)?; + + Ok(SignedExecutionPayloadEnvelope { + message: envelope, + signature, + }) + } } diff --git a/validator_client/signing_method/src/lib.rs b/validator_client/signing_method/src/lib.rs index bf3cc6a17d..c132d86c17 100644 --- a/validator_client/signing_method/src/lib.rs +++ b/validator_client/signing_method/src/lib.rs @@ -49,6 +49,7 @@ pub enum SignableMessage<'a, E: EthSpec, Payload: AbstractExecPayload<E> = FullP SignedContributionAndProof(&'a ContributionAndProof<E>), ValidatorRegistration(&'a ValidatorRegistrationData), VoluntaryExit(&'a VoluntaryExit), + ExecutionPayloadEnvelope(&'a ExecutionPayloadEnvelope<E>), } impl<E: EthSpec, Payload: AbstractExecPayload<E>> SignableMessage<'_, E, Payload> { @@ -70,6 +71,7 @@ impl<E: EthSpec, Payload: AbstractExecPayload<E>> SignableMessage<'_, E, Payload SignableMessage::SignedContributionAndProof(c) => c.signing_root(domain), SignableMessage::ValidatorRegistration(v) => v.signing_root(domain), SignableMessage::VoluntaryExit(exit) => exit.signing_root(domain), + SignableMessage::ExecutionPayloadEnvelope(e) => e.signing_root(domain), } } } @@ -233,6 +235,9 @@ impl SigningMethod { Web3SignerObject::ValidatorRegistration(v) } SignableMessage::VoluntaryExit(e) => Web3SignerObject::VoluntaryExit(e), + SignableMessage::ExecutionPayloadEnvelope(e) => { + Web3SignerObject::ExecutionPayloadEnvelope(e) + } }; // Determine the Web3Signer message type. diff --git a/validator_client/signing_method/src/web3signer.rs b/validator_client/signing_method/src/web3signer.rs index 246d9e9e09..e6fc8f3ba2 100644 --- a/validator_client/signing_method/src/web3signer.rs +++ b/validator_client/signing_method/src/web3signer.rs @@ -19,6 +19,8 @@ pub enum MessageType { SyncCommitteeSelectionProof, SyncCommitteeContributionAndProof, ValidatorRegistration, + // TODO(gloas) verify w/ web3signer specs + ExecutionPayloadEnvelope, } #[derive(Debug, PartialEq, Copy, Clone, Serialize)] @@ -75,6 +77,7 @@ pub enum Web3SignerObject<'a, E: EthSpec, Payload: AbstractExecPayload<E>> { SyncAggregatorSelectionData(&'a SyncAggregatorSelectionData), ContributionAndProof(&'a ContributionAndProof<E>), ValidatorRegistration(&'a ValidatorRegistrationData), + ExecutionPayloadEnvelope(&'a ExecutionPayloadEnvelope<E>), } impl<'a, E: EthSpec, Payload: AbstractExecPayload<E>> Web3SignerObject<'a, E, Payload> { @@ -140,6 +143,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload<E>> Web3SignerObject<'a, E, Pa MessageType::SyncCommitteeContributionAndProof } Web3SignerObject::ValidatorRegistration(_) => MessageType::ValidatorRegistration, + Web3SignerObject::ExecutionPayloadEnvelope(_) => MessageType::ExecutionPayloadEnvelope, } } } diff --git a/validator_client/slashing_protection/Cargo.toml b/validator_client/slashing_protection/Cargo.toml index 86df6d01fe..8017941ca6 100644 --- a/validator_client/slashing_protection/Cargo.toml +++ b/validator_client/slashing_protection/Cargo.toml @@ -6,18 +6,18 @@ edition = { workspace = true } autotests = false [features] -arbitrary-fuzz = ["types/arbitrary-fuzz", "eip_3076/arbitrary-fuzz"] +arbitrary = ["dep:arbitrary", "types/arbitrary", "eip_3076/arbitrary"] portable = ["types/portable"] [dependencies] -arbitrary = { workspace = true, features = ["derive"] } +arbitrary = { workspace = true, features = ["derive"], optional = true } bls = { workspace = true } eip_3076 = { workspace = true, features = ["json"] } ethereum_serde_utils = { workspace = true } filesystem = { workspace = true } fixed_bytes = { workspace = true } r2d2 = { workspace = true } -r2d2_sqlite = "0.21.0" +r2d2_sqlite = "0.32" rusqlite = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/validator_client/slashing_protection/src/interchange_test.rs b/validator_client/slashing_protection/src/interchange_test.rs index c5c3df7ea4..996116dd1c 100644 --- a/validator_client/slashing_protection/src/interchange_test.rs +++ b/validator_client/slashing_protection/src/interchange_test.rs @@ -11,7 +11,7 @@ use tempfile::tempdir; use types::{Epoch, Hash256, Slot}; #[derive(Debug, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct MultiTestCase { pub name: String, pub genesis_validators_root: Hash256, @@ -19,7 +19,7 @@ pub struct MultiTestCase { } #[derive(Debug, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct TestCase { pub should_succeed: bool, pub contains_slashable_data: bool, @@ -29,7 +29,7 @@ pub struct TestCase { } #[derive(Debug, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct TestBlock { pub pubkey: PublicKeyBytes, pub slot: Slot, @@ -39,7 +39,7 @@ pub struct TestBlock { } #[derive(Debug, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct TestAttestation { pub pubkey: PublicKeyBytes, pub source_epoch: Epoch, diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index c0d561b175..f70d5830ec 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -19,11 +19,11 @@ use beacon_node_fallback::{ use clap::ArgMatches; use doppelganger_service::DoppelgangerService; use environment::RuntimeContext; -use eth2::{BeaconNodeHttpClient, StatusCode, Timeouts, reqwest::ClientBuilder}; +use eth2::{BeaconNodeHttpClient, Timeouts}; use initialized_validators::Error::UnableToOpenVotingKeystore; use lighthouse_validator_store::LighthouseValidatorStore; use parking_lot::RwLock; -use reqwest::Certificate; +use reqwest::{Certificate, ClientBuilder, StatusCode}; use slot_clock::SlotClock; use slot_clock::SystemTimeSlotClock; use std::fs::File; diff --git a/validator_client/validator_services/Cargo.toml b/validator_client/validator_services/Cargo.toml index c914940914..2582968265 100644 --- a/validator_client/validator_services/Cargo.toml +++ b/validator_client/validator_services/Cargo.toml @@ -13,6 +13,7 @@ futures = { workspace = true } graffiti_file = { workspace = true } logging = { workspace = true } parking_lot = { workspace = true } +reqwest = { workspace = true } safe_arith = { workspace = true } slot_clock = { workspace = true } task_executor = { workspace = true } diff --git a/validator_client/validator_services/src/block_service.rs b/validator_client/validator_services/src/block_service.rs index 625f8db7cb..1535f50663 100644 --- a/validator_client/validator_services/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -1,9 +1,10 @@ use beacon_node_fallback::{ApiTopic, BeaconNodeFallback, Error as FallbackError, Errors}; use bls::PublicKeyBytes; +use eth2::BeaconNodeHttpClient; use eth2::types::GraffitiPolicy; -use eth2::{BeaconNodeHttpClient, StatusCode}; use graffiti_file::{GraffitiFile, determine_graffiti}; use logging::crit; +use reqwest::StatusCode; use slot_clock::SlotClock; use std::fmt::Debug; use std::future::Future; @@ -13,6 +14,7 @@ use std::time::Duration; use task_executor::TaskExecutor; use tokio::sync::mpsc; use tracing::{Instrument, debug, error, info, info_span, instrument, trace, warn}; +use types::consts::gloas::BUILDER_INDEX_SELF_BUILD; use types::{BlockType, ChainSpec, EthSpec, Graffiti, Slot}; use validator_store::{Error as ValidatorStoreError, SignedBlock, UnsignedBlock, ValidatorStore}; @@ -333,7 +335,7 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> BlockService<S, T> { #[instrument(skip_all, fields(%slot, ?validator_pubkey))] async fn sign_and_publish_block( &self, - proposer_fallback: ProposerFallback<T>, + proposer_fallback: &ProposerFallback<T>, slot: Slot, graffiti: Option<Graffiti>, validator_pubkey: &PublicKeyBytes, @@ -459,73 +461,145 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> BlockService<S, T> { info!(slot = slot.as_u64(), "Requesting unsigned block"); - // Request an SSZ block from all beacon nodes in order, returning on the first successful response. - // If all nodes fail, run a second pass falling back to JSON. - // - // Proposer nodes will always be tried last during each pass since it's likely that they don't have a - // great view of attestations on the network. - let ssz_block_response = proposer_fallback - .request_proposers_last(|beacon_node| async move { - let _get_timer = validator_metrics::start_timer_vec( - &validator_metrics::BLOCK_SERVICE_TIMES, - &[validator_metrics::BEACON_BLOCK_HTTP_GET], - ); - beacon_node - .get_validator_blocks_v3_ssz::<S::E>( - slot, - randao_reveal_ref, - graffiti.as_ref(), - builder_boost_factor, - self_ref.graffiti_policy, - ) - .await - }) - .await; + // Check if Gloas fork is active at this slot + let fork_name = self_ref.chain_spec.fork_name_at_slot::<S::E>(slot); - let block_response = match ssz_block_response { - Ok((ssz_block_response, _metadata)) => ssz_block_response, - Err(e) => { - warn!( - slot = slot.as_u64(), - error = %e, - "SSZ block production failed, falling back to JSON" - ); + let (block_proposer, unsigned_block) = if fork_name.gloas_enabled() { + // Use V4 block production for Gloas + // Request an SSZ block from all beacon nodes in order, returning on the first successful response. + // If all nodes fail, run a second pass falling back to JSON. + let ssz_block_response = proposer_fallback + .request_proposers_last(|beacon_node| async move { + let _get_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_GET], + ); + beacon_node + .get_validator_blocks_v4_ssz::<S::E>( + slot, + randao_reveal_ref, + graffiti.as_ref(), + builder_boost_factor, + self_ref.graffiti_policy, + ) + .await + }) + .await; - proposer_fallback - .request_proposers_last(|beacon_node| async move { - let _get_timer = validator_metrics::start_timer_vec( - &validator_metrics::BLOCK_SERVICE_TIMES, - &[validator_metrics::BEACON_BLOCK_HTTP_GET], - ); - let (json_block_response, _metadata) = beacon_node - .get_validator_blocks_v3::<S::E>( - slot, - randao_reveal_ref, - graffiti.as_ref(), - builder_boost_factor, - self_ref.graffiti_policy, - ) - .await - .map_err(|e| { - BlockError::Recoverable(format!( - "Error from beacon node when producing block: {:?}", - e - )) - })?; + let block_response = match ssz_block_response { + Ok((ssz_block_response, _metadata)) => ssz_block_response, + Err(e) => { + warn!( + slot = slot.as_u64(), + error = %e, + "SSZ V4 block production failed, falling back to JSON" + ); - Ok(json_block_response.data) - }) - .await - .map_err(BlockError::from)? - } - }; + proposer_fallback + .request_proposers_last(|beacon_node| async move { + let _get_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_GET], + ); + let (json_block_response, _metadata) = beacon_node + .get_validator_blocks_v4::<S::E>( + slot, + randao_reveal_ref, + graffiti.as_ref(), + builder_boost_factor, + self_ref.graffiti_policy, + ) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error from beacon node when producing block: {:?}", + e + )) + })?; - let (block_proposer, unsigned_block) = match block_response { - eth2::types::ProduceBlockV3Response::Full(block) => { - (block.block().proposer_index(), UnsignedBlock::Full(block)) - } - eth2::types::ProduceBlockV3Response::Blinded(block) => { - (block.proposer_index(), UnsignedBlock::Blinded(block)) + Ok(json_block_response.data) + }) + .await + .map_err(BlockError::from)? + } + }; + + // Gloas blocks don't have blobs (they're in the execution layer) + let block_contents = eth2::types::FullBlockContents::Block(block_response); + ( + block_contents.block().proposer_index(), + UnsignedBlock::Full(block_contents), + ) + } else { + // Use V3 block production for pre-Gloas forks + // Request an SSZ block from all beacon nodes in order, returning on the first successful response. + // If all nodes fail, run a second pass falling back to JSON. + // + // Proposer nodes will always be tried last during each pass since it's likely that they don't have a + // great view of attestations on the network. + let ssz_block_response = proposer_fallback + .request_proposers_last(|beacon_node| async move { + let _get_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_GET], + ); + beacon_node + .get_validator_blocks_v3_ssz::<S::E>( + slot, + randao_reveal_ref, + graffiti.as_ref(), + builder_boost_factor, + self_ref.graffiti_policy, + ) + .await + }) + .await; + + let block_response = match ssz_block_response { + Ok((ssz_block_response, _metadata)) => ssz_block_response, + Err(e) => { + warn!( + slot = slot.as_u64(), + error = %e, + "SSZ block production failed, falling back to JSON" + ); + + proposer_fallback + .request_proposers_last(|beacon_node| async move { + let _get_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_GET], + ); + let (json_block_response, _metadata) = beacon_node + .get_validator_blocks_v3::<S::E>( + slot, + randao_reveal_ref, + graffiti.as_ref(), + builder_boost_factor, + self_ref.graffiti_policy, + ) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error from beacon node when producing block: {:?}", + e + )) + })?; + + Ok(json_block_response.data) + }) + .await + .map_err(BlockError::from)? + } + }; + + match block_response { + eth2::types::ProduceBlockV3Response::Full(block) => { + (block.block().proposer_index(), UnsignedBlock::Full(block)) + } + eth2::types::ProduceBlockV3Response::Blinded(block) => { + (block.proposer_index(), UnsignedBlock::Blinded(block)) + } } }; @@ -538,7 +612,7 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> BlockService<S, T> { self_ref .sign_and_publish_block( - proposer_fallback, + &proposer_fallback, slot, graffiti, &validator_pubkey, @@ -546,6 +620,108 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> BlockService<S, T> { ) .await?; + // TODO(gloas) we only need to fetch, sign and publish the envelope in the local building case. + // Right now we always default to local building. Once we implement trustless/trusted builder logic + // we should check the bid for index == BUILDER_INDEX_SELF_BUILD + if fork_name.gloas_enabled() { + self_ref + .fetch_sign_and_publish_payload_envelope( + &proposer_fallback, + slot, + &validator_pubkey, + ) + .await?; + } + + Ok(()) + } + + /// Fetch, sign, and publish the execution payload envelope for Gloas. + /// This should be called after the block has been published. + /// + /// TODO(gloas): For multi-BN setups, we need to track which beacon node produced the block + /// and fetch the envelope from that same node. The envelope is cached per-BN, + /// so fetching from a different BN than the one that built the block will fail. + /// See: https://github.com/sigp/lighthouse/pull/8313 + #[instrument(skip_all)] + async fn fetch_sign_and_publish_payload_envelope( + &self, + _proposer_fallback: &ProposerFallback<T>, + slot: Slot, + validator_pubkey: &PublicKeyBytes, + ) -> Result<(), BlockError> { + info!(slot = slot.as_u64(), "Fetching execution payload envelope"); + + // Fetch the envelope from the beacon node. Use builder_index=BUILDER_INDEX_SELF_BUILD for local building. + // TODO(gloas): Use proposer_fallback once multi-BN is supported. + let envelope = self + .beacon_nodes + .first_success(|beacon_node| async move { + beacon_node + .get_validator_execution_payload_envelope_ssz::<S::E>( + slot, + BUILDER_INDEX_SELF_BUILD, + ) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error fetching execution payload envelope: {:?}", + e + )) + }) + }) + .await?; + + info!( + slot = slot.as_u64(), + beacon_block_root = %envelope.beacon_block_root, + "Received execution payload envelope, signing" + ); + + // Sign the envelope + let signed_envelope = self + .validator_store + .sign_execution_payload_envelope(*validator_pubkey, envelope) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error signing execution payload envelope: {:?}", + e + )) + })?; + + info!( + slot = slot.as_u64(), + "Signed execution payload envelope, publishing" + ); + + let fork_name = self.chain_spec.fork_name_at_slot::<S::E>(slot); + + // Publish the signed envelope + // TODO(gloas): Use proposer_fallback once multi-BN is supported. + self.beacon_nodes + .first_success(|beacon_node| { + let signed_envelope = signed_envelope.clone(); + async move { + beacon_node + .post_beacon_execution_payload_envelope_ssz(&signed_envelope, fork_name) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error publishing execution payload envelope: {:?}", + e + )) + }) + } + }) + .await?; + + info!( + slot = slot.as_u64(), + beacon_block_root = %signed_envelope.message.beacon_block_root, + "Successfully published signed execution payload envelope" + ); + Ok(()) } diff --git a/validator_client/validator_store/src/lib.rs b/validator_client/validator_store/src/lib.rs index 4fdbb8064c..87ab669e8d 100644 --- a/validator_client/validator_store/src/lib.rs +++ b/validator_client/validator_store/src/lib.rs @@ -5,8 +5,9 @@ use std::fmt::Debug; use std::future::Future; use std::sync::Arc; use types::{ - Address, Attestation, AttestationError, BlindedBeaconBlock, Epoch, EthSpec, Graffiti, Hash256, - SelectionProof, SignedAggregateAndProof, SignedBlindedBeaconBlock, SignedContributionAndProof, + Address, Attestation, AttestationError, BlindedBeaconBlock, Epoch, EthSpec, + ExecutionPayloadEnvelope, Graffiti, Hash256, SelectionProof, SignedAggregateAndProof, + SignedBlindedBeaconBlock, SignedContributionAndProof, SignedExecutionPayloadEnvelope, SignedValidatorRegistrationData, Slot, SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, }; @@ -178,6 +179,13 @@ pub trait ValidatorStore: Send + Sync { /// runs. fn prune_slashing_protection_db(&self, current_epoch: Epoch, first_run: bool); + /// Sign an `ExecutionPayloadEnvelope` for Gloas. + fn sign_execution_payload_envelope( + &self, + validator_pubkey: PublicKeyBytes, + envelope: ExecutionPayloadEnvelope<Self::E>, + ) -> impl Future<Output = Result<SignedExecutionPayloadEnvelope<Self::E>, Error<Self::Error>>> + Send; + /// Returns `ProposalData` for the provided `pubkey` if it exists in `InitializedValidators`. /// `ProposalData` fields include defaulting logic described in `get_fee_recipient_defaulting`, /// `get_gas_limit_defaulting`, and `get_builder_proposals_defaulting`. diff --git a/validator_manager/Cargo.toml b/validator_manager/Cargo.toml index 16ce1e023f..d0155698b4 100644 --- a/validator_manager/Cargo.toml +++ b/validator_manager/Cargo.toml @@ -29,4 +29,4 @@ beacon_chain = { workspace = true } http_api = { workspace = true } regex = { workspace = true } tempfile = { workspace = true } -validator_http_api = { workspace = true } +validator_http_api = { workspace = true, features = ["testing"] } diff --git a/validator_manager/src/exit_validators.rs b/validator_manager/src/exit_validators.rs index 8ddcc7e419..00bcb36e80 100644 --- a/validator_manager/src/exit_validators.rs +++ b/validator_manager/src/exit_validators.rs @@ -322,7 +322,7 @@ mod test { let mut spec = ChainSpec::mainnet(); spec.shard_committee_period = 1; spec.altair_fork_epoch = Some(Epoch::new(0)); - spec.bellatrix_fork_epoch = Some(Epoch::new(1)); + spec.bellatrix_fork_epoch = Some(Epoch::new(0)); spec.capella_fork_epoch = Some(Epoch::new(2)); spec.deneb_fork_epoch = Some(Epoch::new(3)); @@ -330,15 +330,8 @@ mod test { let harness = &beacon_node.harness; let mock_el = harness.mock_execution_layer.as_ref().unwrap(); - let execution_ctx = mock_el.server.ctx.clone(); - // Move to terminal block. mock_el.server.all_payloads_valid(); - execution_ctx - .execution_block_generator - .write() - .move_to_terminal_block() - .unwrap(); Self { exit_config: None, diff --git a/validator_manager/src/import_validators.rs b/validator_manager/src/import_validators.rs index 24917f7d1b..0d6d358edb 100644 --- a/validator_manager/src/import_validators.rs +++ b/validator_manager/src/import_validators.rs @@ -112,8 +112,7 @@ pub fn cli_app() -> Command { .value_name("ETH1_ADDRESS") .help("When provided, the imported validator will use the suggested fee recipient. Omit this flag to use the default value from the VC.") .action(ArgAction::Set) - .display_order(0) - .requires(KEYSTORE_FILE_FLAG), + .display_order(0), ) .arg( Arg::new(GAS_LIMIT) @@ -122,8 +121,7 @@ pub fn cli_app() -> Command { .help("When provided, the imported validator will use this gas limit. It is recommended \ to leave this as the default value by not specifying this flag.",) .action(ArgAction::Set) - .display_order(0) - .requires(KEYSTORE_FILE_FLAG), + .display_order(0), ) .arg( Arg::new(BUILDER_PROPOSALS) @@ -132,8 +130,7 @@ pub fn cli_app() -> Command { blocks via builder rather than the local EL.",) .value_parser(["true","false"]) .action(ArgAction::Set) - .display_order(0) - .requires(KEYSTORE_FILE_FLAG), + .display_order(0), ) .arg( Arg::new(BUILDER_BOOST_FACTOR) @@ -144,8 +141,7 @@ pub fn cli_app() -> Command { when choosing between a builder payload header and payload from \ the local execution node.",) .action(ArgAction::Set) - .display_order(0) - .requires(KEYSTORE_FILE_FLAG), + .display_order(0), ) .arg( Arg::new(PREFER_BUILDER_PROPOSALS) @@ -154,8 +150,16 @@ pub fn cli_app() -> Command { constructed by builders, regardless of payload value.",) .value_parser(["true","false"]) .action(ArgAction::Set) - .display_order(0) - .requires(KEYSTORE_FILE_FLAG), + .display_order(0), + ) + .arg( + Arg::new(ENABLED) + .long(ENABLED) + .help("When provided, the imported validator will be \ + enabled or disabled.",) + .value_parser(["true","false"]) + .action(ArgAction::Set) + .display_order(0), ) } @@ -225,48 +229,113 @@ async fn run(config: ImportConfig) -> Result<(), String> { enabled, } = config; - let validators: Vec<ValidatorSpecification> = - if let Some(validators_format_path) = &validators_file_path { - if !validators_format_path.exists() { - return Err(format!( - "Unable to find file at {:?}", - validators_format_path - )); - } + let validators: Vec<ValidatorSpecification> = if let Some(validators_format_path) = + &validators_file_path + { + if !validators_format_path.exists() { + return Err(format!( + "Unable to find file at {:?}", + validators_format_path + )); + } - let validators_file = fs::OpenOptions::new() - .read(true) - .create(false) - .open(validators_format_path) - .map_err(|e| format!("Unable to open {:?}: {:?}", validators_format_path, e))?; + let validators_file = fs::OpenOptions::new() + .read(true) + .create(false) + .open(validators_format_path) + .map_err(|e| format!("Unable to open {:?}: {:?}", validators_format_path, e))?; - serde_json::from_reader(&validators_file).map_err(|e| { + // Define validators as mutable so that if a relevant flag is supplied, the fields can be overridden. + let mut validators: Vec<ValidatorSpecification> = serde_json::from_reader(&validators_file) + .map_err(|e| { format!( "Unable to parse JSON in {:?}: {:?}", validators_format_path, e ) - })? - } else if let Some(keystore_format_path) = &keystore_file_path { - vec![ValidatorSpecification { - voting_keystore: KeystoreJsonStr( - Keystore::from_json_file(keystore_format_path).map_err(|e| format!("{e:?}"))?, - ), - voting_keystore_password: password.ok_or_else(|| { - "The --password flag is required to supply the keystore password".to_string() - })?, - slashing_protection: None, - fee_recipient, - gas_limit, - builder_proposals, - builder_boost_factor, - prefer_builder_proposals, - enabled, - }] - } else { - return Err(format!( - "One of the flag --{VALIDATORS_FILE_FLAG} or --{KEYSTORE_FILE_FLAG} is required." - )); - }; + })?; + + // Log the overridden note when one or more flags is supplied + if let Some(override_fee_recipient) = fee_recipient { + eprintln!( + "Please note! --suggested-fee-recipient is provided. This will override existing fee recipient defined in validators.json with: {:?}", + override_fee_recipient + ); + } + if let Some(override_gas_limit) = gas_limit { + eprintln!( + "Please note! --gas-limit is provided. This will override existing gas limit defined in validators.json with: {}", + override_gas_limit + ); + } + if let Some(override_builder_proposals) = builder_proposals { + eprintln!( + "Please note! --builder-proposals is provided. This will override existing builder proposal setting defined in validators.json with: {}", + override_builder_proposals + ); + } + if let Some(override_builder_boost_factor) = builder_boost_factor { + eprintln!( + "Please note! --builder-boost-factor is provided. This will override existing builder boost factor defined in validators.json with: {}", + override_builder_boost_factor + ); + } + if let Some(override_prefer_builder_proposals) = prefer_builder_proposals { + eprintln!( + "Please note! --prefer-builder-proposals is provided. This will override existing prefer builder proposal setting defined in validators.json with: {}", + override_prefer_builder_proposals + ); + } + if let Some(override_enabled) = enabled { + eprintln!( + "Please note! --enabled flag is provided. This will override existing setting defined in validators.json with: {}", + override_enabled + ); + } + + // Override the fields in validators.json file if the flag is supplied + for validator in &mut validators { + if let Some(override_fee_recipient) = fee_recipient { + validator.fee_recipient = Some(override_fee_recipient); + } + if let Some(override_gas_limit) = gas_limit { + validator.gas_limit = Some(override_gas_limit); + } + if let Some(override_builder_proposals) = builder_proposals { + validator.builder_proposals = Some(override_builder_proposals); + } + if let Some(override_builder_boost_factor) = builder_boost_factor { + validator.builder_boost_factor = Some(override_builder_boost_factor); + } + if let Some(override_prefer_builder_proposals) = prefer_builder_proposals { + validator.prefer_builder_proposals = Some(override_prefer_builder_proposals); + } + if let Some(override_enabled) = enabled { + validator.enabled = Some(override_enabled); + } + } + + validators + } else if let Some(keystore_format_path) = &keystore_file_path { + vec![ValidatorSpecification { + voting_keystore: KeystoreJsonStr( + Keystore::from_json_file(keystore_format_path).map_err(|e| format!("{e:?}"))?, + ), + voting_keystore_password: password.ok_or_else(|| { + "The --password flag is required to supply the keystore password".to_string() + })?, + slashing_protection: None, + fee_recipient, + gas_limit, + builder_proposals, + builder_boost_factor, + prefer_builder_proposals, + enabled, + }] + } else { + return Err(format!( + "One of the flag --{VALIDATORS_FILE_FLAG} or --{KEYSTORE_FILE_FLAG} is required." + )); + }; let count = validators.len();