Compare commits

..

17 Commits

Author SHA1 Message Date
Paul Hauner
dc320e3faa Bump version 2025-02-26 14:57:58 +11:00
Eitan Seri-Levi
e473500456 discard unused code 2025-02-25 14:20:12 -08:00
Pawan Dhananjay
627c1bac89 take config value 2025-02-25 10:37:29 -08:00
Pawan Dhananjay
0c5580f23f Bump sync-tolerance-epoch and make it a cli param 2025-02-25 09:45:51 -08:00
Michael Sproul
279afb0696 Add vc --disable-attesting flag 2025-02-26 00:36:33 +11:00
Michael Sproul
fd1ca8ef23 Ban peers with banned payloads 2025-02-25 23:30:11 +11:00
Jimmy Chen
df3038d902 Add log 2025-02-25 18:17:15 +11:00
Jimmy Chen
88c0f9d60a Blacklist invalid block root in block verification and blacklist invalid finalized epochs in sync. 2025-02-25 18:10:19 +11:00
Jimmy Chen
c61cf26622 Blacklist invalid block root in block verification and blacklist invalid finalized epochs in sync. 2025-02-25 18:08:44 +11:00
Michael Sproul
2883429f69 MORE 2025-02-25 16:44:42 +11:00
Michael Sproul
19fc31a75b Allow invalidation of "valid" nodes 2025-02-25 16:30:27 +11:00
Michael Sproul
ff2376efec Implement invalidation API 2025-02-25 15:42:25 +11:00
Michael Sproul
d4586ea92d Remove more liveness risks 2025-02-25 15:18:54 +11:00
Michael Sproul
1b9b61bb77 lcli http-sync hacks 2025-02-25 14:00:28 +11:00
Michael Sproul
11f17e52a0 Disable liveness risk 2025-02-25 12:36:13 +11:00
Michael Sproul
d472689fa2 Fix flag 2025-02-25 11:48:41 +11:00
Michael Sproul
bbc1200b2d Add flag to disable attestation APIs 2025-02-25 11:39:48 +11:00
1222 changed files with 91317 additions and 118201 deletions

View File

@@ -1,286 +0,0 @@
# 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::<String>("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

View File

@@ -1,200 +0,0 @@
# 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 <package>` - Run tests for specific package (preferred for iteration)
- `cargo nextest run -p <package> <test_name>` - 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::<String>("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"
```

View File

@@ -1,130 +0,0 @@
# 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

View File

@@ -1,49 +0,0 @@
# 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

View File

@@ -1,85 +0,0 @@
# 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/<base-branch>..origin/<release-branch>
# 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 <PR> --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
## <Release Name>
## Summary
Lighthouse v<VERSION> includes <brief description>.
This is a <recommended/mandatory> upgrade for <target users>.
## <Section>
- **<Title>** (#<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

View File

@@ -1,57 +0,0 @@
# 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.

View File

@@ -1,15 +0,0 @@
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "echo '\n[Reminder] Run: cargo fmt --all && make lint-fix'"
}
]
}
]
}
}

View File

@@ -1,5 +0,0 @@
#!/bin/sh
# Pre-commit hook: runs cargo fmt --check
# Install with: make install-hooks
exec cargo fmt --check

6
.github/CODEOWNERS vendored
View File

@@ -1,4 +1,2 @@
/beacon_node/network/ @jxs
/beacon_node/lighthouse_network/ @jxs
/beacon_node/store/ @michaelsproul
/.github/forbidden-files.txt @michaelsproul
beacon_node/network/ @jxs
beacon_node/lighthouse_network/ @jxs

View File

@@ -1,12 +1,3 @@
---
name: Default issue template
about: Use this template for all issues
title: ''
labels: ''
assignees: ''
---
## Description
Please provide a brief description of the issue.

View File

@@ -1,17 +0,0 @@
# 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/attestation_performance.rs
beacon_node/http_api/src/block_packing_efficiency.rs
beacon_node/http_api/src/block_rewards.rs
common/eth2/src/lighthouse/attestation_performance.rs
common/eth2/src/lighthouse/block_packing_efficiency.rs
common/eth2/src/lighthouse/block_rewards.rs
common/test_random_derive/
consensus/types/src/execution/state_payload_status.rs
consensus/types/src/test_utils/test_random/

104
.github/mergify.yml vendored
View File

@@ -1,97 +1,3 @@
pull_request_rules:
- name: Ask to resolve conflict
conditions:
- -closed
- conflict
- -author=dependabot[bot]
- label=ready-for-review
- or:
- -draft # Don't report conflicts on regular draft.
- and: # Do report conflicts on draft that are scheduled for the next major release.
- draft
- milestone~=v[0-9]\.[0-9]{2}
actions:
comment:
message: This pull request has merge conflicts. Could you please resolve them
@{{author}}? 🙏
label:
add:
- waiting-on-author
remove:
- ready-for-review
- name: Ask to resolve CI failures
conditions:
- -closed
- label=ready-for-review
- or:
- check-skipped=test-suite-success
- check-skipped=local-testnet-success
- check-failure=test-suite-success
- check-failure=local-testnet-success
actions:
comment:
message: Some required checks have failed. Could you please take a look @{{author}}? 🙏
label:
add:
- waiting-on-author
remove:
- ready-for-review
- name: Update labels when PR is unblocked
conditions:
- -closed
- -draft
- label=waiting-on-author
- -conflict
# Unfortunately, it doesn't look like there's an easy way to check for PRs pending
# CI workflows approvals.
- check-success=test-suite-success
- check-success=local-testnet-success
# Update the label only if there are no more change requests from any reviewers and no unresolved threads.
# This rule ensures that a PR with passing CI can be marked as `waiting-on-author`.
- "#changes-requested-reviews-by = 0"
- "#review-threads-unresolved = 0"
actions:
label:
remove:
- waiting-on-author
add:
- ready-for-review
- name: Close stale pull request after 30 days of inactivity
conditions:
- -closed
- label=waiting-on-author
- updated-at<=30 days ago
actions:
close:
message: >
Hi @{{author}}, this pull request has been closed automatically due to 30 days of inactivity.
If youd like to continue working on it, feel free to reopen at any time.
label:
add:
- stale
- name: Approve trivial maintainer PRs
conditions:
- base!=stable
- label=trivial
- author=@sigp/lighthouse
- -conflict
actions:
review:
type: APPROVE
- name: Add ready-to-merge labeled PRs to merge queue
conditions:
# All branch protection rules are implicit: https://docs.mergify.com/conditions/#about-branch-protection
- base!=stable
- label=ready-for-merge
- label!=do-not-merge
actions:
queue:
queue_rules:
- name: default
batch_size: 8
@@ -100,20 +6,14 @@ queue_rules:
merge_method: squash
commit_message_template: |
{{ title }} (#{{ number }})
{{ body | get_section("## Issue Addressed", "") }}
{{ body | get_section("## Proposed Changes", "") }}
{% for commit in commits | unique(attribute='email_author') %}
Co-Authored-By: {{ commit.author }} <{{ commit.email_author }}>
{% for commit in commits %}
* {{ commit.commit_message }}
{% endfor %}
queue_conditions:
- "#approved-reviews-by >= 1"
- "check-success=license/cla"
- "check-success=target-branch-check"
- "label!=do-not-merge"
merge_conditions:
- "check-success=test-suite-success"
- "check-success=local-testnet-success"

View File

@@ -13,7 +13,7 @@ jobs:
build-and-upload-to-s3:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Setup mdBook
uses: peaceiris/actions-mdbook@v1

View File

@@ -1,183 +0,0 @@
name: docker-reproducible
on:
push:
branches:
- unstable
tags:
- v*
workflow_dispatch: # allows manual triggering for testing purposes and skips publishing an image
env:
DOCKER_REPRODUCIBLE_IMAGE_NAME: >-
${{ github.repository_owner }}/lighthouse-reproducible
DOCKER_PASSWORD: ${{ secrets.DH_KEY }}
DOCKER_USERNAME: ${{ secrets.DH_ORG }}
jobs:
extract-version:
name: extract version
runs-on: ubuntu-22.04
steps:
- name: Extract version
run: |
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
# It's a tag (e.g., v1.2.3)
VERSION="${GITHUB_REF#refs/tags/}"
elif [[ "${{ github.ref }}" == refs/heads/unstable ]]; then
# unstable branch -> latest-unstable
VERSION="latest-unstable"
else
# For manual triggers from other branches and will not publish any image
VERSION="test-build"
fi
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
id: extract_version
outputs:
VERSION: ${{ steps.extract_version.outputs.VERSION }}
verify-and-build:
name: verify reproducibility and build
needs: extract-version
strategy:
matrix:
arch: [amd64, arm64]
include:
- arch: amd64
rust_target: x86_64-unknown-linux-gnu
rust_image: >-
rust:1.88-bullseye@sha256:8e3c421122bf4cd3b2a866af41a4dd52d87ad9e315fd2cb5100e87a7187a9816
platform: linux/amd64
runner: ubuntu-22.04
- arch: arm64
rust_target: aarch64-unknown-linux-gnu
rust_image: >-
rust:1.88-bullseye@sha256:8b22455a7ce2adb1355067638284ee99d21cc516fab63a96c4514beaf370aa94
platform: linux/arm64
runner: ubuntu-22.04-arm
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker
- name: Verify reproducible builds (${{ matrix.arch }})
run: |
# Build first image
docker build -f Dockerfile.reproducible \
--platform ${{ matrix.platform }} \
--build-arg RUST_TARGET="${{ matrix.rust_target }}" \
--build-arg RUST_IMAGE="${{ matrix.rust_image }}" \
-t lighthouse-verify-1-${{ matrix.arch }} .
# Extract binary from first build
docker create --name extract-1-${{ matrix.arch }} lighthouse-verify-1-${{ matrix.arch }}
docker cp extract-1-${{ matrix.arch }}:/lighthouse ./lighthouse-1-${{ matrix.arch }}
docker rm extract-1-${{ matrix.arch }}
# Clean state for second build
docker buildx prune -f
docker system prune -f
# Build second image
docker build -f Dockerfile.reproducible \
--platform ${{ matrix.platform }} \
--build-arg RUST_TARGET="${{ matrix.rust_target }}" \
--build-arg RUST_IMAGE="${{ matrix.rust_image }}" \
-t lighthouse-verify-2-${{ matrix.arch }} .
# Extract binary from second build
docker create --name extract-2-${{ matrix.arch }} lighthouse-verify-2-${{ matrix.arch }}
docker cp extract-2-${{ matrix.arch }}:/lighthouse ./lighthouse-2-${{ matrix.arch }}
docker rm extract-2-${{ matrix.arch }}
# Compare binaries
echo "=== Comparing binaries ==="
echo "Build 1 SHA256: $(sha256sum lighthouse-1-${{ matrix.arch }})"
echo "Build 2 SHA256: $(sha256sum lighthouse-2-${{ matrix.arch }})"
if cmp lighthouse-1-${{ matrix.arch }} lighthouse-2-${{ matrix.arch }}; then
echo "Reproducible build verified for ${{ matrix.arch }}"
else
echo "Reproducible build FAILED for ${{ matrix.arch }}"
echo "BLOCKING RELEASE: Builds are not reproducible!"
echo "First 10 differences:"
cmp -l lighthouse-1-${{ matrix.arch }} lighthouse-2-${{ matrix.arch }} | head -10
exit 1
fi
# Clean up verification artifacts but keep one image for publishing
rm -f lighthouse-*-${{ matrix.arch }}
docker rmi lighthouse-verify-1-${{ matrix.arch }} || true
# Re-tag the second image for publishing (we verified it's identical to first)
VERSION=${{ needs.extract-version.outputs.VERSION }}
FINAL_TAG="${{ env.DOCKER_REPRODUCIBLE_IMAGE_NAME }}:${VERSION}-${{ matrix.arch }}"
docker tag lighthouse-verify-2-${{ matrix.arch }} "$FINAL_TAG"
- name: Log in to Docker Hub
if: ${{ github.event_name != 'workflow_dispatch' }}
uses: docker/login-action@v3
with:
username: ${{ env.DOCKER_USERNAME }}
password: ${{ env.DOCKER_PASSWORD }}
- name: Push verified image (${{ matrix.arch }})
if: ${{ github.event_name != 'workflow_dispatch' }}
run: |
VERSION=${{ needs.extract-version.outputs.VERSION }}
IMAGE_TAG="${{ env.DOCKER_REPRODUCIBLE_IMAGE_NAME }}:${VERSION}-${{ matrix.arch }}"
docker push "$IMAGE_TAG"
- name: Clean up local images
run: |
docker rmi lighthouse-verify-2-${{ matrix.arch }} || true
VERSION=${{ needs.extract-version.outputs.VERSION }}
docker rmi "${{ env.DOCKER_REPRODUCIBLE_IMAGE_NAME }}:${VERSION}-${{ matrix.arch }}" || true
- name: Upload verification artifacts (on failure)
if: failure()
uses: actions/upload-artifact@v4
with:
name: verification-failure-${{ matrix.arch }}
path: |
lighthouse-*-${{ matrix.arch }}
create-manifest:
name: create multi-arch manifest
runs-on: ubuntu-22.04
needs: [extract-version, verify-and-build]
if: ${{ github.event_name != 'workflow_dispatch' }}
steps:
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ env.DOCKER_USERNAME }}
password: ${{ env.DOCKER_PASSWORD }}
- name: Create and push multi-arch manifest
run: |
IMAGE_NAME=${{ env.DOCKER_REPRODUCIBLE_IMAGE_NAME }}
VERSION=${{ needs.extract-version.outputs.VERSION }}
# Create manifest for the version tag
docker manifest create \
${IMAGE_NAME}:${VERSION} \
${IMAGE_NAME}:${VERSION}-amd64 \
${IMAGE_NAME}:${VERSION}-arm64
docker manifest push ${IMAGE_NAME}:${VERSION}
# For version tags, also create/update the latest tag to keep stable up to date
# Only create latest tag for proper release versions (e.g. v1.2.3, not v1.2.3-alpha)
if [[ "${GITHUB_REF}" == refs/tags/* ]] && [[ "${VERSION}" =~ ^v[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}$ ]]; then
docker manifest create \
${IMAGE_NAME}:latest \
${IMAGE_NAME}:${VERSION}-amd64 \
${IMAGE_NAME}:${VERSION}-arm64
docker manifest push ${IMAGE_NAME}:latest
fi

View File

@@ -4,6 +4,7 @@ on:
push:
branches:
- unstable
- stable
tags:
- v*
@@ -27,6 +28,11 @@ jobs:
extract-version:
runs-on: ubuntu-22.04
steps:
- name: Extract version (if stable)
if: github.event.ref == 'refs/heads/stable'
run: |
echo "VERSION=latest" >> $GITHUB_ENV
echo "VERSION_SUFFIX=" >> $GITHUB_ENV
- name: Extract version (if unstable)
if: github.event.ref == 'refs/heads/unstable'
run: |
@@ -58,7 +64,7 @@ jobs:
VERSION: ${{ needs.extract-version.outputs.VERSION }}
VERSION_SUFFIX: ${{ needs.extract-version.outputs.VERSION_SUFFIX }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Update Rust
if: env.SELF_HOSTED_RUNNERS == 'false'
run: rustup update stable
@@ -153,16 +159,7 @@ jobs:
- name: Create and push multiarch manifests
run: |
# Create the main tag (versioned for releases, latest-unstable for unstable)
docker buildx imagetools create -t ${{ github.repository_owner}}/${{ matrix.binary }}:${VERSION}${VERSION_SUFFIX} \
${{ github.repository_owner}}/${{ matrix.binary }}:${VERSION}-arm64${VERSION_SUFFIX} \
${{ github.repository_owner}}/${{ matrix.binary }}:${VERSION}-amd64${VERSION_SUFFIX};
# For version tags, also create/update the latest tag to keep stable up to date
# Only create latest tag for proper release versions (e.g. v1.2.3, not v1.2.3-alpha)
if [[ "${GITHUB_REF}" == refs/tags/* ]] && [[ "${VERSION}" =~ ^v[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}$ ]]; then
docker buildx imagetools create -t ${{ github.repository_owner}}/${{ matrix.binary }}:latest \
${{ github.repository_owner}}/${{ matrix.binary }}:${VERSION}-arm64${VERSION_SUFFIX} \
${{ github.repository_owner}}/${{ matrix.binary }}:${VERSION}-amd64${VERSION_SUFFIX};
fi

View File

@@ -20,7 +20,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Run mdbook server
run: |

View File

@@ -14,13 +14,13 @@ concurrency:
jobs:
dockerfile-ubuntu:
runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x;snapshot.key=lighthouse-ubuntu-latest-v1' || 'ubuntu-latest' }}
runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "large"]') || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Build Docker image
run: |
docker build --build-arg FEATURES=portable,spec-minimal -t lighthouse:local .
docker build --build-arg FEATURES=portable -t lighthouse:local .
docker save lighthouse:local -o lighthouse-docker.tar
- name: Upload Docker image artifact
@@ -31,14 +31,14 @@ jobs:
retention-days: 3
run-local-testnet:
runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x;snapshot.key=lighthouse-ubuntu-latest-v1' || 'ubuntu-latest' }}
runs-on: ubuntu-22.04
needs: dockerfile-ubuntu
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Install Kurtosis
run: |
echo "deb [trusted=yes] https://sdk.kurtosis.com/kurtosis-cli-release-artifacts/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list
echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list
sudo apt update
sudo apt install -y kurtosis-cli
kurtosis analytics disable
@@ -52,22 +52,23 @@ jobs:
- name: Load Docker image
run: docker load -i lighthouse-docker.tar
- name: Start local testnet with Assertoor
run: ./start_local_testnet.sh -e local-assertoor -c -a -b false && sleep 60
- name: Start local testnet
run: ./start_local_testnet.sh -e local -c -b false && sleep 60
working-directory: scripts/local_testnet
- name: Await Assertoor test result
id: assertoor_test_result
uses: ethpandaops/assertoor-github-action@v1
with:
kurtosis_enclave_name: local-assertoor
- name: Stop local testnet and dump logs
run: ./stop_local_testnet.sh local
working-directory: scripts/local_testnet
- name: Start local testnet with blinded block production
run: ./start_local_testnet.sh -e local-blinded -c -p -b false && sleep 60
working-directory: scripts/local_testnet
- name: Stop local testnet and dump logs
run: ./stop_local_testnet.sh local-assertoor
run: ./stop_local_testnet.sh local-blinded
working-directory: scripts/local_testnet
- name: Upload logs artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: logs-local-testnet
@@ -75,38 +76,15 @@ jobs:
scripts/local_testnet/logs
retention-days: 3
- name: Return Assertoor test result
shell: bash
run: |
test_result="${{ steps.assertoor_test_result.outputs.result }}"
test_status=$(
cat <<"EOF"
${{ steps.assertoor_test_result.outputs.test_overview }}
EOF
)
failed_test_status=$(
cat <<"EOF"
${{ steps.assertoor_test_result.outputs.failed_test_details }}
EOF
)
echo "Test Result: $test_result"
echo "$test_status"
if ! [ "$test_result" == "success" ]; then
echo "Failed Test Task Status:"
echo "$failed_test_status"
exit 1
fi
doppelganger-protection-success-test:
needs: dockerfile-ubuntu
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Install Kurtosis
run: |
echo "deb [trusted=yes] https://sdk.kurtosis.com/kurtosis-cli-release-artifacts/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list
echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list
sudo apt update
sudo apt install -y kurtosis-cli
kurtosis analytics disable
@@ -126,7 +104,6 @@ jobs:
working-directory: scripts/tests
- name: Upload logs artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: logs-doppelganger-protection-success
@@ -136,13 +113,13 @@ jobs:
doppelganger-protection-failure-test:
needs: dockerfile-ubuntu
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Install Kurtosis
run: |
echo "deb [trusted=yes] https://sdk.kurtosis.com/kurtosis-cli-release-artifacts/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list
echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list
sudo apt update
sudo apt install -y kurtosis-cli
kurtosis analytics disable
@@ -162,7 +139,6 @@ jobs:
working-directory: scripts/tests
- name: Upload logs artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: logs-doppelganger-protection-failure
@@ -170,106 +146,19 @@ jobs:
scripts/local_testnet/logs
retention-days: 3
# Tests checkpoint syncing to a live network (current fork) and a running devnet (usually next scheduled fork)
checkpoint-sync-test:
name: checkpoint-sync-test-${{ matrix.network }}
runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x;snapshot.key=lighthouse-ubuntu-latest-v1' || 'ubuntu-latest' }}
needs: dockerfile-ubuntu
if: contains(github.event.pull_request.labels.*.name, 'syncing')
continue-on-error: true
strategy:
matrix:
network: [sepolia]
steps:
- uses: actions/checkout@v5
- name: Install Kurtosis
run: |
echo "deb [trusted=yes] https://sdk.kurtosis.com/kurtosis-cli-release-artifacts/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list
sudo apt update
sudo apt install -y kurtosis-cli
kurtosis analytics disable
- name: Download Docker image artifact
uses: actions/download-artifact@v4
with:
name: lighthouse-docker
path: .
- name: Load Docker image
run: docker load -i lighthouse-docker.tar
- name: Run the checkpoint sync test script
run: |
./checkpoint-sync.sh "sync-${{ matrix.network }}" "checkpoint-sync-config-${{ matrix.network }}.yaml"
working-directory: scripts/tests
- name: Upload logs artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: logs-checkpoint-sync-${{ matrix.network }}
path: |
scripts/local_testnet/logs
retention-days: 3
# Test syncing from genesis on a local testnet. Aims to cover forward syncing both short and long distances.
genesis-sync-test:
name: genesis-sync-test-${{ matrix.fork }}-${{ matrix.offline_secs }}s
runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x;snapshot.key=lighthouse-ubuntu-latest-v1' || 'ubuntu-latest' }}
needs: dockerfile-ubuntu
strategy:
matrix:
fork: [electra, fulu]
offline_secs: [120, 300]
steps:
- uses: actions/checkout@v5
- name: Install Kurtosis
run: |
echo "deb [trusted=yes] https://sdk.kurtosis.com/kurtosis-cli-release-artifacts/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list
sudo apt update
sudo apt install -y kurtosis-cli
kurtosis analytics disable
- name: Download Docker image artifact
uses: actions/download-artifact@v4
with:
name: lighthouse-docker
path: .
- name: Load Docker image
run: docker load -i lighthouse-docker.tar
- name: Run the genesis sync test script
run: |
./genesis-sync.sh "sync-${{ matrix.fork }}-${{ matrix.offline_secs }}s" "genesis-sync-config-${{ matrix.fork }}.yaml" "${{ matrix.fork }}" "${{ matrix.offline_secs }}"
working-directory: scripts/tests
- name: Upload logs artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: logs-genesis-sync-${{ matrix.fork }}-${{ matrix.offline_secs }}s
path: |
scripts/local_testnet/logs
retention-days: 3
# This job succeeds ONLY IF all others succeed. It is used by the merge queue to determine whether
# a PR is safe to merge. New jobs should be added here.
local-testnet-success:
name: local-testnet-success
runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x;snapshot.key=lighthouse-ubuntu-latest-v1' || 'ubuntu-latest' }}
runs-on: ubuntu-latest
needs: [
'dockerfile-ubuntu',
'run-local-testnet',
'doppelganger-protection-success-test',
'doppelganger-protection-failure-test',
'genesis-sync-test'
]
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Check that success job is dependent on all others
run: |
exclude_jobs='checkpoint-sync-test'
./scripts/ci/check-success-job.sh ./.github/workflows/local-testnet.yml local-testnet-success "$exclude_jobs"
run: ./scripts/ci/check-success-job.sh ./.github/workflows/local-testnet.yml local-testnet-success

View File

@@ -1,124 +0,0 @@
# We only run tests on `RECENT_FORKS` on CI. To make sure we don't break prior forks, we run nightly tests to cover all prior forks.
name: nightly-tests
on:
schedule:
# Run at 8:30 AM UTC every day
- cron: '30 8 * * *'
workflow_dispatch: # Allow manual triggering
inputs:
branch:
description: 'Branch to test'
required: false
default: 'unstable'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
# Deny warnings in CI
# Disable debug info (see https://github.com/sigp/lighthouse/issues/4005)
RUSTFLAGS: "-D warnings -C debuginfo=0"
# Prevent Github API rate limiting.
LIGHTHOUSE_GITHUB_TOKEN: ${{ secrets.LIGHTHOUSE_GITHUB_TOKEN }}
# Disable incremental compilation
CARGO_INCREMENTAL: 0
# Enable portable to prevent issues with caching `blst` for the wrong CPU type
TEST_FEATURES: portable
jobs:
setup-matrix:
name: setup-matrix
runs-on: ubuntu-latest
outputs:
forks: ${{ steps.set-matrix.outputs.forks }}
steps:
- name: Set matrix
id: set-matrix
run: |
# All prior forks to cover in nightly tests. This list should be updated when we remove a fork from `RECENT_FORKS`.
echo 'forks=["phase0", "altair", "bellatrix", "capella", "deneb"]' >> $GITHUB_OUTPUT
beacon-chain-tests:
name: beacon-chain-tests
needs: setup-matrix
runs-on: 'ubuntu-latest'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
strategy:
matrix:
fork: ${{ fromJson(needs.setup-matrix.outputs.forks) }}
fail-fast: false
steps:
- uses: actions/checkout@v5
with:
ref: ${{ inputs.branch || 'unstable' }}
- name: Get latest version of stable Rust
uses: moonrepo/setup-rust@v1
with:
channel: stable
cache-target: release
bins: cargo-nextest
- name: Run beacon_chain tests for ${{ matrix.fork }}
run: make test-beacon-chain-${{ matrix.fork }}
timeout-minutes: 60
op-pool-tests:
name: op-pool-tests
needs: setup-matrix
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
strategy:
matrix:
fork: ${{ fromJson(needs.setup-matrix.outputs.forks) }}
fail-fast: false
steps:
- uses: actions/checkout@v5
with:
ref: ${{ inputs.branch || 'unstable' }}
- name: Get latest version of stable Rust
uses: moonrepo/setup-rust@v1
with:
channel: stable
cache-target: release
bins: cargo-nextest
- name: Run operation_pool tests for ${{ matrix.fork }}
run: make test-op-pool-${{ matrix.fork }}
timeout-minutes: 60
network-tests:
name: network-tests
needs: setup-matrix
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
strategy:
matrix:
fork: ${{ fromJson(needs.setup-matrix.outputs.forks) }}
fail-fast: false
steps:
- uses: actions/checkout@v5
with:
ref: ${{ inputs.branch || 'unstable' }}
- name: Get latest version of stable Rust
uses: moonrepo/setup-rust@v1
with:
channel: stable
cache-target: release
bins: cargo-nextest
- name: Create CI logger dir
run: mkdir ${{ runner.temp }}/network_test_logs
- name: Run network tests for ${{ matrix.fork }}
run: make test-network-${{ matrix.fork }}
timeout-minutes: 60
env:
TEST_FEATURES: portable
CI_LOGGER_DIR: ${{ runner.temp }}/network_test_logs
- name: Upload logs
if: always()
uses: actions/upload-artifact@v4
with:
name: network_test_logs_${{ matrix.fork }}
path: ${{ runner.temp }}/network_test_logs

View File

@@ -32,7 +32,8 @@ jobs:
matrix:
arch: [aarch64-unknown-linux-gnu,
x86_64-unknown-linux-gnu,
aarch64-apple-darwin]
x86_64-apple-darwin,
x86_64-windows]
include:
- arch: aarch64-unknown-linux-gnu
runner: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "release", "large"]') || 'ubuntu-latest' }}
@@ -40,19 +41,35 @@ jobs:
- arch: x86_64-unknown-linux-gnu
runner: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "release", "large"]') || 'ubuntu-latest' }}
profile: maxperf
- arch: aarch64-apple-darwin
runner: macos-14
- arch: x86_64-apple-darwin
runner: macos-13
profile: maxperf
- arch: x86_64-windows
runner: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "windows", "release"]') || 'windows-2019' }}
profile: maxperf
runs-on: ${{ matrix.runner }}
needs: extract-version
steps:
- name: Checkout sources
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Get latest version of stable Rust
if: env.SELF_HOSTED_RUNNERS == 'false'
run: rustup update stable
# ==============================
# Windows dependencies
# ==============================
- uses: KyleMayes/install-llvm-action@v1
if: env.SELF_HOSTED_RUNNERS == 'false' && startsWith(matrix.arch, 'x86_64-windows')
with:
version: "17.0"
directory: ${{ runner.temp }}/llvm
- name: Set LIBCLANG_PATH
if: startsWith(matrix.arch, 'x86_64-windows')
run: echo "LIBCLANG_PATH=$((gcm clang).source -replace "clang.exe")" >> $env:GITHUB_ENV
# ==============================
# Builds
# ==============================
@@ -73,11 +90,16 @@ jobs:
if: contains(matrix.arch, 'unknown-linux-gnu')
run: mv target/${{ matrix.arch }}/${{ matrix.profile }}/lighthouse ~/.cargo/bin/lighthouse
- name: Build Lighthouse for aarch64-apple-darwin
if: matrix.arch == 'aarch64-apple-darwin'
- name: Build Lighthouse for x86_64-apple-darwin
if: matrix.arch == 'x86_64-apple-darwin'
run: cargo install --path lighthouse --force --locked --features portable,gnosis --profile ${{ matrix.profile }}
- name: Build Lighthouse for Windows
if: matrix.arch == 'x86_64-windows'
run: cargo install --path lighthouse --force --locked --features portable,gnosis --profile ${{ matrix.profile }}
- name: Configure GPG and create artifacts
if: startsWith(matrix.arch, 'x86_64-windows') != true
env:
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
@@ -96,6 +118,20 @@ jobs:
done
mv *tar.gz* ..
- name: Configure GPG and create artifacts Windows
if: startsWith(matrix.arch, 'x86_64-windows')
env:
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: |
echo $env:GPG_SIGNING_KEY | gpg --batch --import
mkdir artifacts
move $env:USERPROFILE/.cargo/bin/lighthouse.exe ./artifacts
cd artifacts
tar -czf lighthouse-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.arch }}.tar.gz lighthouse.exe
gpg --passphrase "$env:GPG_PASSPHRASE" --batch --pinentry-mode loopback -ab lighthouse-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.arch }}.tar.gz
move *tar.gz* ..
# =======================================================================
# Upload artifacts
# This is required to share artifacts between different jobs
@@ -124,7 +160,7 @@ jobs:
steps:
# This is necessary for generating the changelog. It has to come before "Download Artifacts" or else it deletes the artifacts.
- name: Checkout sources
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -185,7 +221,7 @@ jobs:
|Non-Staking Users| <TODO>|---|
*See [Update
Priorities](https://lighthouse-book.sigmaprime.io/installation_priorities.html)
Priorities](https://lighthouse-book.sigmaprime.io/installation-priorities.html)
more information about this table.*
## All Changes
@@ -194,18 +230,19 @@ jobs:
## Binaries
[See pre-built binaries documentation.](https://lighthouse-book.sigmaprime.io/installation_binaries.html)
[See pre-built binaries documentation.](https://lighthouse-book.sigmaprime.io/installation-binaries.html)
The binaries are signed with Sigma Prime's PGP key: `15E66D941F697E28F49381F426416DC3F30674B0`
| System | Architecture | Binary | PGP Signature |
|:---:|:---:|:---:|:---|
| <picture> <source media="(prefers-color-scheme: dark)" srcset="https://cdn.simpleicons.org/apple/white" > <source media="(prefers-color-scheme: light)" srcset="https://cdn.simpleicons.org/apple" ><img src="https://cdn.simpleicons.org/apple" width="32" alt="Apple logo"> </picture> | aarch64 | [lighthouse-${{ env.VERSION }}-aarch64-apple-darwin.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-aarch64-apple-darwin.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-aarch64-apple-darwin.tar.gz.asc) |
| <picture> <source media="(prefers-color-scheme: dark)" srcset="https://cdn.simpleicons.org/linux/white" > <source media="(prefers-color-scheme: light)" srcset="https://cdn.simpleicons.org/linux/black" ><img src="https://cdn.simpleicons.org/linux" width="32" alt="Linux logo"> </picture> | x86_64 | [lighthouse-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz.asc) |
| <picture> <source media="(prefers-color-scheme: dark)" srcset="https://cdn.simpleicons.org/raspberrypi/white" > <source media="(prefers-color-scheme: light)" srcset="https://cdn.simpleicons.org/raspberrypi/black" > <img src="https://cdn.simpleicons.org/raspberrypi" width="32" alt="Raspberrypi logo"> </picture> | aarch64 | [lighthouse-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz.asc) |
| <img src="https://simpleicons.org/icons/apple.svg" style="width: 32px;"/> | x86_64 | [lighthouse-${{ env.VERSION }}-x86_64-apple-darwin.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-apple-darwin.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-apple-darwin.tar.gz.asc) |
| <img src="https://simpleicons.org/icons/linux.svg" style="width: 32px;"/> | x86_64 | [lighthouse-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz.asc) |
| <img src="https://simpleicons.org/icons/raspberrypi.svg" style="width: 32px;"/> | aarch64 | [lighthouse-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz.asc) |
| <img src="https://upload.wikimedia.org/wikipedia/commons/c/c4/Windows_logo_-_2021_%28Black%29.svg" style="width: 32px;"/> | x86_64 | [lighthouse-${{ env.VERSION }}-x86_64-windows.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-windows.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-windows.tar.gz.asc) |
| | | | |
| **System** | **Option** | - | **Resource** |
| <picture> <source media="(prefers-color-scheme: dark)" srcset="https://cdn.simpleicons.org/docker/white" > <source media="(prefers-color-scheme: light)" srcset="https://cdn.simpleicons.org/docker/black" > <img src="https://cdn.simpleicons.org/docker/black" width="32" alt="Docker logo"></picture> | Docker | [${{ env.VERSION }}](https://hub.docker.com/r/${{ env.IMAGE_NAME }}/tags?page=1&ordering=last_updated&name=${{ env.VERSION }}) | [${{ env.IMAGE_NAME }}](https://hub.docker.com/r/${{ env.IMAGE_NAME }}) |
| <img src="https://simpleicons.org/icons/docker.svg" style="width: 32px;"/> | Docker | [${{ env.VERSION }}](https://hub.docker.com/r/${{ env.IMAGE_NAME }}/tags?page=1&ordering=last_updated&name=${{ env.VERSION }}) | [${{ env.IMAGE_NAME }}](https://hub.docker.com/r/${{ env.IMAGE_NAME }}) |
ENDBODY
)
assets=(./lighthouse-*.tar.gz*/lighthouse-*.tar.gz*)

View File

@@ -22,6 +22,10 @@ env:
# NOTE: this token is a personal access token on Jimmy's account due to the default GITHUB_TOKEN
# not having access to other repositories. We should eventually devise a better solution here.
LIGHTHOUSE_GITHUB_TOKEN: ${{ secrets.LIGHTHOUSE_GITHUB_TOKEN }}
# Enable self-hosted runners for the sigp repo only.
SELF_HOSTED_RUNNERS: ${{ github.repository == 'sigp/lighthouse' }}
# Self-hosted runners need to reference a different host for `./watch` tests.
WATCH_HOST: ${{ github.repository == 'sigp/lighthouse' && 'host.docker.internal' || 'localhost' }}
# Disable incremental compilation
CARGO_INCREMENTAL: 0
# Enable portable to prevent issues with caching `blst` for the wrong CPU type
@@ -72,43 +76,21 @@ 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 [ -e "$file" ]; then
echo "::error::Forbidden file or directory exists: $file"
status=1
fi
done < .github/forbidden-files.txt
exit $status
fi
release-tests-ubuntu:
name: release-tests-ubuntu
needs: [check-labels]
if: needs.check-labels.outputs.skip_ci != 'true'
runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x;snapshot.key=lighthouse-ubuntu-latest-v1' || 'ubuntu-latest' }}
# Use self-hosted runners only on the sigp repo.
runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "large"]') || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
# Set Java version to 21. (required since Web3Signer 24.12.0).
# On sigp/lighthouse, Java 21 is baked into the snapshot.
- if: github.repository != 'sigp/lighthouse'
uses: actions/setup-java@v4
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
- if: github.repository != 'sigp/lighthouse'
name: Get latest version of stable Rust
- name: Get latest version of stable Rust
if: env.SELF_HOSTED_RUNNERS == 'false'
uses: moonrepo/setup-rust@v1
with:
channel: stable
@@ -116,60 +98,69 @@ jobs:
bins: cargo-nextest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: github.repository == 'sigp/lighthouse'
uses: Swatinem/rust-cache@v2
- name: Install Foundry (anvil)
if: env.SELF_HOSTED_RUNNERS == 'false'
uses: foundry-rs/foundry-toolchain@v1
with:
cache-provider: warpbuild
version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d
- name: Run tests in release
run: make test-release
run: make nextest-release
- name: Show cache stats
if: env.SELF_HOSTED_RUNNERS == 'true'
run: sccache --show-stats
release-tests-windows:
name: release-tests-windows
needs: [check-labels]
if: needs.check-labels.outputs.skip_ci != 'true'
runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "windows", "CI"]') || 'windows-2019' }}
steps:
- uses: actions/checkout@v4
- name: Get latest version of stable Rust
if: env.SELF_HOSTED_RUNNERS == 'false'
uses: moonrepo/setup-rust@v1
with:
channel: stable
cache-target: release
bins: cargo-nextest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install Foundry (anvil)
if: env.SELF_HOSTED_RUNNERS == 'false'
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d
- name: Install make
if: env.SELF_HOSTED_RUNNERS == 'false'
run: choco install -y make
- name: Set LIBCLANG_PATH
run: echo "LIBCLANG_PATH=$((gcm clang).source -replace "clang.exe")" >> $env:GITHUB_ENV
- name: Run tests in release
run: make nextest-release
- name: Show cache stats
if: env.SELF_HOSTED_RUNNERS == 'true'
continue-on-error: true
run: sccache --show-stats
beacon-chain-tests:
name: beacon-chain-tests
needs: [check-labels]
if: needs.check-labels.outputs.skip_ci != 'true'
runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x;snapshot.key=lighthouse-ubuntu-latest-v1' || 'ubuntu-latest' }}
# Use self-hosted runners only on the sigp repo.
runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "large"]') || 'ubuntu-latest' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v5
- if: github.repository != 'sigp/lighthouse'
name: Get latest version of stable Rust
- uses: actions/checkout@v4
- name: Get latest version of stable Rust
if: env.SELF_HOSTED_RUNNERS == 'false'
uses: moonrepo/setup-rust@v1
with:
channel: stable
cache-target: release
bins: cargo-nextest
- if: github.repository == 'sigp/lighthouse'
uses: Swatinem/rust-cache@v2
with:
cache-provider: warpbuild
- name: Run beacon_chain tests for all known forks
run: make test-beacon-chain
http-api-tests:
name: http-api-tests
needs: [check-labels]
if: needs.check-labels.outputs.skip_ci != 'true'
runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x;snapshot.key=lighthouse-ubuntu-latest-v1' || 'ubuntu-latest' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v5
- if: github.repository != 'sigp/lighthouse'
name: Get latest version of stable Rust
uses: moonrepo/setup-rust@v1
with:
channel: stable
cache-target: release
bins: cargo-nextest
- if: github.repository == 'sigp/lighthouse'
uses: Swatinem/rust-cache@v2
with:
cache-provider: warpbuild
- name: Run http_api tests for all recent forks
run: make test-http-api
- name: Show cache stats
if: env.SELF_HOSTED_RUNNERS == 'true'
run: sccache --show-stats
op-pool-tests:
name: op-pool-tests
needs: [check-labels]
@@ -178,7 +169,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Get latest version of stable Rust
uses: moonrepo/setup-rust@v1
with:
@@ -195,7 +186,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Get latest version of stable Rust
uses: moonrepo/setup-rust@v1
with:
@@ -207,10 +198,9 @@ jobs:
- name: Run network tests for all known forks
run: make test-network
env:
TEST_FEATURES: portable
TEST_FEATURES: portable,ci_logger
CI_LOGGER_DIR: ${{ runner.temp }}/network_test_logs
- name: Upload logs
if: always()
uses: actions/upload-artifact@v4
with:
name: network_test_logs
@@ -224,7 +214,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Get latest version of stable Rust
uses: moonrepo/setup-rust@v1
with:
@@ -237,30 +227,35 @@ jobs:
name: debug-tests-ubuntu
needs: [check-labels]
if: needs.check-labels.outputs.skip_ci != 'true'
runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x;snapshot.key=lighthouse-ubuntu-latest-v1' || 'ubuntu-latest' }}
# Use self-hosted runners only on the sigp repo.
runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "large"]') || 'ubuntu-latest' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v5
- if: github.repository != 'sigp/lighthouse'
name: Get latest version of stable Rust
- uses: actions/checkout@v4
- name: Get latest version of stable Rust
if: env.SELF_HOSTED_RUNNERS == 'false'
uses: moonrepo/setup-rust@v1
with:
channel: stable
bins: cargo-nextest
- if: github.repository == 'sigp/lighthouse'
uses: Swatinem/rust-cache@v2
- name: Install Foundry (anvil)
if: env.SELF_HOSTED_RUNNERS == 'false'
uses: foundry-rs/foundry-toolchain@v1
with:
cache-provider: warpbuild
version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d
- name: Run tests in debug
run: make test-debug
run: make nextest-debug
- name: Show cache stats
if: env.SELF_HOSTED_RUNNERS == 'true'
run: sccache --show-stats
state-transition-vectors-ubuntu:
name: state-transition-vectors-ubuntu
needs: [check-labels]
if: needs.check-labels.outputs.skip_ci != 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Get latest version of stable Rust
uses: moonrepo/setup-rust@v1
with:
@@ -272,83 +267,71 @@ jobs:
name: ef-tests-ubuntu
needs: [check-labels]
if: needs.check-labels.outputs.skip_ci != 'true'
runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x;snapshot.key=lighthouse-ubuntu-latest-v1' || 'ubuntu-latest' }}
# Use self-hosted runners only on the sigp repo.
runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "small"]') || 'ubuntu-latest' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v5
- if: github.repository != 'sigp/lighthouse'
name: Get latest version of stable Rust
- uses: actions/checkout@v4
- name: Get latest version of stable Rust
if: env.SELF_HOSTED_RUNNERS == 'false'
uses: moonrepo/setup-rust@v1
with:
channel: stable
cache-target: release
bins: cargo-nextest
- if: github.repository == 'sigp/lighthouse'
uses: Swatinem/rust-cache@v2
with:
cache-provider: warpbuild
- name: Run consensus-spec-tests with blst and fake_crypto
run: make test-ef
run: make nextest-ef
- name: Show cache stats
if: env.SELF_HOSTED_RUNNERS == 'true'
run: sccache --show-stats
basic-simulator-ubuntu:
name: basic-simulator-ubuntu
needs: [check-labels]
if: needs.check-labels.outputs.skip_ci != 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Get latest version of stable Rust
uses: moonrepo/setup-rust@v1
with:
channel: stable
cache-target: release
- name: Create log dir
run: mkdir ${{ runner.temp }}/basic_simulator_logs
- name: Run a basic beacon chain sim that starts from Deneb
run: cargo run --release --bin simulator basic-sim --disable-stdout-logging --log-dir ${{ runner.temp }}/basic_simulator_logs
- name: Upload logs
if: always()
uses: actions/upload-artifact@v4
with:
name: basic_simulator_logs
path: ${{ runner.temp }}/basic_simulator_logs
- name: Run a basic beacon chain sim that starts from Bellatrix
run: cargo run --release --bin simulator basic-sim
fallback-simulator-ubuntu:
name: fallback-simulator-ubuntu
needs: [check-labels]
if: needs.check-labels.outputs.skip_ci != 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Get latest version of stable Rust
uses: moonrepo/setup-rust@v1
with:
channel: stable
cache-target: release
- name: Create log dir
run: mkdir ${{ runner.temp }}/fallback_simulator_logs
- name: Run a beacon chain sim which tests VC fallback behaviour
run: cargo run --release --bin simulator fallback-sim --disable-stdout-logging --log-dir ${{ runner.temp }}/fallback_simulator_logs
- name: Upload logs
if: always()
uses: actions/upload-artifact@v4
with:
name: fallback_simulator_logs
path: ${{ runner.temp }}/fallback_simulator_logs
run: cargo run --release --bin simulator fallback-sim
execution-engine-integration-ubuntu:
name: execution-engine-integration-ubuntu
needs: [check-labels]
if: needs.check-labels.outputs.skip_ci != 'true'
runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x;snapshot.key=lighthouse-ubuntu-latest-v1' || 'ubuntu-latest' }}
runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "small"]') || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v5
- if: github.repository != 'sigp/lighthouse'
name: Get latest version of stable Rust
- uses: actions/checkout@v4
- name: Get latest version of stable Rust
if: env.SELF_HOSTED_RUNNERS == 'false'
uses: moonrepo/setup-rust@v1
with:
channel: stable
cache-target: release
cache: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Add go compiler to $PATH
if: env.SELF_HOSTED_RUNNERS == 'true'
run: echo "/usr/local/go/bin" >> $GITHUB_PATH
- name: Run exec engine integration tests in release
run: make test-exec-engine
check-code:
@@ -357,18 +340,16 @@ jobs:
env:
CARGO_INCREMENTAL: 1
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Get latest version of stable Rust
uses: moonrepo/setup-rust@v1
with:
channel: stable
cache-target: release
components: rustfmt,clippy
bins: cargo-audit,cargo-deny
bins: cargo-audit
- 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
@@ -379,8 +360,6 @@ jobs:
run: make arbitrary-fuzz
- name: Run cargo audit
run: make audit-CI
- name: Run cargo deny
run: make deny-CI
- name: Run cargo vendor to make sure dependencies can be vendored for packaging, reproducibility and archival purpose
run: CARGO_HOME=$(readlink -f $HOME) make vendor
- name: Markdown-linter
@@ -391,7 +370,7 @@ jobs:
name: check-msrv
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Install Rust at Minimum Supported Rust Version (MSRV)
run: |
metadata=$(cargo metadata --no-deps --format-version 1)
@@ -405,7 +384,7 @@ jobs:
if: needs.check-labels.outputs.skip_ci != 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Get latest version of nightly Rust
uses: moonrepo/setup-rust@v1
with:
@@ -414,6 +393,10 @@ jobs:
cache: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Fetch libssl1.1
run: wget https://nz2.archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2_amd64.deb
- name: Install libssl1.1
run: sudo dpkg -i libssl1.1_1.1.1f-1ubuntu2_amd64.deb
- name: Create Cargo config dir
run: mkdir -p .cargo
- name: Install custom Cargo config
@@ -429,7 +412,7 @@ jobs:
if: needs.check-labels.outputs.skip_ci != 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Install dependencies
run: sudo apt update && sudo apt install -y git gcc g++ make cmake pkg-config llvm-dev libclang-dev clang
- name: Use Rust beta
@@ -442,7 +425,7 @@ jobs:
if: needs.check-labels.outputs.skip_ci != 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Get latest version of stable Rust
uses: moonrepo/setup-rust@v1
with:
@@ -450,29 +433,13 @@ jobs:
cache-target: release
- name: Run Makefile to trigger the bash script
run: make cli-local
cargo-hack:
name: cargo-hack
needs: [check-labels]
if: needs.check-labels.outputs.skip_ci != 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Get latest version of stable Rust
uses: moonrepo/setup-rust@v1
with:
channel: stable
- uses: taiki-e/install-action@cargo-hack
- name: Check types feature powerset
run: cargo hack check -p types --feature-powerset --no-dev-deps --exclude-features arbitrary-fuzz,portable
- name: Check eth2 feature powerset
run: cargo hack check -p eth2 --feature-powerset --no-dev-deps
cargo-sort:
name: cargo-sort
needs: [check-labels]
if: needs.check-labels.outputs.skip_ci != 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Get latest version of stable Rust
uses: moonrepo/setup-rust@v1
with:
@@ -490,13 +457,12 @@ jobs:
needs: [
'check-labels',
'target-branch-check',
'forbidden-files-check',
'release-tests-ubuntu',
'release-tests-windows',
'beacon-chain-tests',
'op-pool-tests',
'network-tests',
'slasher-tests',
'http-api-tests',
'debug-tests-ubuntu',
'state-transition-vectors-ubuntu',
'ef-tests-ubuntu',
@@ -509,10 +475,9 @@ jobs:
'compile-with-beta-compiler',
'cli-check',
'lockbud',
'cargo-hack',
'cargo-sort',
]
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Check that success job is dependent on all others
run: ./scripts/ci/check-success-job.sh ./.github/workflows/test-suite.yml test-suite-success

View File

@@ -1,63 +0,0 @@
name: Bake warpbuild snapshot (lighthouse-ubuntu-latest)
on:
workflow_dispatch:
schedule:
# Every week (Sunday at 00:00 UTC)
- cron: "0 0 * * 0"
pull_request:
branches: [stable, unstable]
paths:
- '.github/workflows/warpbuild-ubuntu-latest-snapshot.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
bake:
runs-on: warp-ubuntu-latest-x64-8x
steps:
- name: Install system deps
run: |
set -euxo pipefail
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends \
pkg-config \
libssl-dev \
build-essential \
cmake \
clang \
llvm-dev \
libclang-dev \
protobuf-compiler \
git \
gcc \
g++ \
make
- name: Install Rust toolchain (stable)
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt,clippy
- name: Install cargo bins
run: |
cargo install --locked cargo-nextest
cargo install --locked cargo-audit
cargo install --locked cargo-deny
cargo install --locked cargo-sort
cargo install --locked cargo-hack
- name: Install Java (Temurin 21)
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
- name: Save snapshot
uses: WarpBuilds/snapshot-save@v1
with:
alias: 'lighthouse-ubuntu-latest-v1'
fail-on-error: true
wait-timeout-minutes: 60

1
.gitignore vendored
View File

@@ -9,6 +9,7 @@ perf.data*
*.tar.gz
/bin
genesis.ssz
/clippy.toml
/.cargo
# IntelliJ

View File

@@ -1,5 +0,0 @@
{
"rust-analyzer.cargo.cfgs": [
"!debug_assertions"
]
}

View File

@@ -1,10 +0,0 @@
# 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/`

158
CLAUDE.md
View File

@@ -1,158 +0,0 @@
# Lighthouse AI Assistant Guide
This file provides guidance for AI assistants (Claude Code, Codex, etc.) working with Lighthouse.
## CRITICAL - Always Follow
After completing ANY code changes:
1. **MUST** run `cargo check` to verify compilation before considering task complete
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.
## Quick Reference
```bash
# Build
make install # Build and install Lighthouse
cargo build --release # Standard release build
# 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)
# Lint
make lint # Run Clippy
cargo fmt --all && make lint-fix # Format and fix
```
## 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
// NEVER
let value = option.unwrap();
let item = array[1];
// ALWAYS
let value = option?;
let item = array.get(1)?;
```
Only acceptable during startup for CLI/config validation.
### 2. Consensus Crate: Safe Math Only
In `consensus/` (excluding `types/`), use saturating or checked arithmetic:
```rust
// NEVER
let result = a + b;
// ALWAYS
let result = a.saturating_add(b);
```
## Important Rules (bugs or performance issues)
### 3. Never Block Async
```rust
// NEVER
async fn handler() { expensive_computation(); }
// ALWAYS
async fn handler() {
tokio::task::spawn_blocking(|| expensive_computation()).await?;
}
```
### 4. Lock Ordering
Document lock ordering to avoid deadlocks. See [`canonical_head.rs:9-32`](beacon_node/beacon_chain/src/canonical_head.rs) for the pattern.
### 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
```
See `.ai/DEVELOPMENT.md` for detailed architecture.
## 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

View File

@@ -37,15 +37,6 @@ Requests](https://github.com/sigp/lighthouse/pulls) is where code gets
reviewed. We use [discord](https://discord.gg/cyAszAh) to chat
informally.
### A Note on LLM usage
We are happy to support contributors who are genuinely engaging with the code base. Our general policy regarding LLM usage:
- Please refrain from submissions that you haven't thoroughly understood, reviewed, and tested.
- Please disclose if a significant portion of your contribution was AI-generated.
- Descriptions and comments should be made by you.
- We reserve the right to reject any contributions we feel are violating the spirit of open source contribution.
### General Work-Flow
We recommend the following work-flow for contributors:

7202
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,32 @@
[workspace]
members = [
"account_manager",
"beacon_node",
"beacon_node/beacon_chain",
"beacon_node/beacon_processor",
"beacon_node/builder_client",
"beacon_node/client",
"beacon_node/eth1",
"beacon_node/execution_layer",
"beacon_node/genesis",
"beacon_node/http_api",
"beacon_node/http_metrics",
"beacon_node/lighthouse_network",
"beacon_node/lighthouse_network/gossipsub",
"beacon_node/network",
"beacon_node/operation_pool",
"beacon_node/store",
"beacon_node/timer",
"boot_node",
"common/account_utils",
"common/clap_utils",
"common/compare_fields",
"common/compare_fields_derive",
"common/deposit_contract",
"common/directory",
"common/eip_3076",
"common/eth2",
"common/eth2_config",
"common/eth2_interop_keypairs",
@@ -35,43 +41,55 @@ members = [
"common/malloc_utils",
"common/metrics",
"common/monitoring_api",
"common/network_utils",
"common/oneshot_broadcast",
"common/pretty_reqwest_error",
"common/sensitive_url",
"common/slot_clock",
"common/system_health",
"common/target_check",
"common/task_executor",
"common/tracing_samplers",
"common/test_random_derive",
"common/unused_port",
"common/validator_dir",
"common/warp_utils",
"common/workspace_members",
"consensus/fixed_bytes",
"consensus/fork_choice",
"consensus/int_to_bytes",
"consensus/merkle_proof",
"consensus/proto_array",
"consensus/safe_arith",
"consensus/state_processing",
"consensus/swap_or_not_shuffle",
"consensus/types",
"crypto/bls",
"crypto/eth2_key_derivation",
"crypto/eth2_keystore",
"crypto/eth2_wallet",
"crypto/kzg",
"database_manager",
"lcli",
"lighthouse",
"lighthouse/environment",
"slasher",
"slasher/service",
"testing/ef_tests",
"testing/eth1_test_rig",
"testing/execution_engine_integration",
"testing/node_test_rig",
"testing/simulator",
"testing/state_transition_vectors",
"testing/test-test_logger",
"testing/validator_test_rig",
"testing/web3signer_tests",
"validator_client",
"validator_client/beacon_node_fallback",
"validator_client/doppelganger_service",
@@ -79,188 +97,199 @@ members = [
"validator_client/http_api",
"validator_client/http_metrics",
"validator_client/initialized_validators",
"validator_client/lighthouse_validator_store",
"validator_client/signing_method",
"validator_client/slashing_protection",
"validator_client/validator_metrics",
"validator_client/validator_services",
"validator_client/validator_store",
"validator_manager",
"watch",
]
resolver = "2"
[workspace.package]
edition = "2024"
version = "8.1.3"
edition = "2021"
[workspace.dependencies]
account_utils = { path = "common/account_utils" }
alloy-consensus = { version = "1", default-features = false }
alloy-dyn-abi = { version = "1", default-features = false }
alloy-json-abi = { version = "1", default-features = false }
alloy-network = { version = "1", default-features = false }
alloy-primitives = { version = "1", default-features = false, features = ["rlp", "getrandom"] }
alloy-provider = { version = "1", default-features = false, features = ["reqwest"] }
alloy-rlp = { version = "0.3", default-features = false }
alloy-rpc-types-eth = { version = "1", default-features = false, features = ["serde"] }
alloy-signer-local = { version = "1", default-features = false }
alloy-primitives = { version = "0.8", features = ["rlp", "getrandom"] }
alloy-rlp = "0.3.4"
alloy-consensus = "0.3.0"
anyhow = "1"
arbitrary = { version = "1", features = ["derive"] }
async-channel = "1.9.0"
axum = "0.7.7"
beacon_chain = { path = "beacon_node/beacon_chain" }
beacon_node = { path = "beacon_node" }
beacon_node_fallback = { path = "validator_client/beacon_node_fallback" }
beacon_processor = { path = "beacon_node/beacon_processor" }
bincode = "1"
bitvec = "1"
bls = { path = "crypto/bls" }
byteorder = "1"
bytes = "1.11.1"
cargo_metadata = "0.19"
bytes = "1"
clap = { version = "4.5.4", features = ["derive", "cargo", "wrap_help"] }
clap_utils = { path = "common/clap_utils" }
compare_fields = "0.1"
console-subscriber = "0.4"
context_deserialize = "0.2"
criterion = "0.8"
# Turn off c-kzg's default features which include `blst/portable`. We can turn on blst's portable
# feature ourselves when desired.
c-kzg = { version = "1", default-features = false }
compare_fields_derive = { path = "common/compare_fields_derive" }
criterion = "0.5"
delay_map = "0.4"
deposit_contract = { path = "common/deposit_contract" }
directory = { path = "common/directory" }
derivative = "2"
dirs = "3"
discv5 = { version = "0.10", features = ["libp2p"] }
doppelganger_service = { path = "validator_client/doppelganger_service" }
educe = "0.6"
eip_3076 = { path = "common/eip_3076" }
either = "1.9"
environment = { path = "lighthouse/environment" }
eth2 = { path = "common/eth2" }
eth2_config = { path = "common/eth2_config" }
eth2_key_derivation = { path = "crypto/eth2_key_derivation" }
eth2_keystore = { path = "crypto/eth2_keystore" }
eth2_network_config = { path = "common/eth2_network_config" }
eth2_wallet = { path = "crypto/eth2_wallet" }
ethereum_hashing = "0.8.0"
ethereum_serde_utils = "0.8.0"
ethereum_ssz = { version = "0.10.0", features = ["context_deserialize"] }
ethereum_ssz_derive = "0.10.0"
execution_layer = { path = "beacon_node/execution_layer" }
filesystem = { path = "common/filesystem" }
fixed_bytes = { path = "consensus/fixed_bytes" }
rust_eth_kzg = "0.5.3"
discv5 = { version = "0.9", features = ["libp2p"] }
env_logger = "0.9"
ethereum_hashing = "0.7.0"
ethereum_serde_utils = "0.7"
ethereum_ssz = "0.7"
ethereum_ssz_derive = "0.7"
ethers-core = "1"
ethers-providers = { version = "1", default-features = false }
exit-future = "0.2"
fnv = "1"
fork_choice = { path = "consensus/fork_choice" }
fs2 = "0.4"
futures = "0.3"
genesis = { path = "beacon_node/genesis" }
graffiti_file = { path = "validator_client/graffiti_file" }
hashlink = "0.9.0"
health_metrics = { path = "common/health_metrics" }
hex = "0.4"
http_api = { path = "beacon_node/http_api" }
hashlink = "0.9.0"
hyper = "1"
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"] }
itertools = "0.10"
libsecp256k1 = "0.7"
lighthouse_network = { path = "beacon_node/lighthouse_network" }
lighthouse_validator_store = { path = "validator_client/lighthouse_validator_store" }
lighthouse_version = { path = "common/lighthouse_version" }
lockfile = { path = "common/lockfile" }
log = "0.4"
logging = { path = "common/logging" }
logroller = "0.1.8"
lru = "0.12"
lru_cache = { path = "common/lru_cache" }
malloc_utils = { path = "common/malloc_utils" }
maplit = "1"
merkle_proof = { path = "consensus/merkle_proof" }
metrics = { path = "common/metrics" }
milhouse = { version = "0.9", default-features = false, features = ["context_deserialize"] }
mockall = "0.13"
mockall_double = "0.3"
milhouse = "0.3"
mockito = "1.5.0"
monitoring_api = { path = "common/monitoring_api" }
network = { path = "beacon_node/network" }
network_utils = { path = "common/network_utils" }
node_test_rig = { path = "testing/node_test_rig" }
num_cpus = "1"
once_cell = "1.17.1"
opentelemetry = "0.30.0"
opentelemetry-otlp = { version = "0.30.0", features = ["grpc-tonic", "tls-roots"] }
opentelemetry_sdk = "0.30.0"
operation_pool = { path = "beacon_node/operation_pool" }
parking_lot = "0.12"
paste = "1"
pretty_reqwest_error = { path = "common/pretty_reqwest_error" }
prometheus = { version = "0.13", default-features = false }
proptest = "1"
proto_array = { path = "consensus/proto_array" }
prometheus = "0.13"
quickcheck = "1"
quickcheck_macros = "1"
quote = "1"
r2d2 = "0.8"
rand = "0.9.0"
rand_xorshift = "0.4.0"
rand = "0.8"
rayon = "1.7"
regex = "1"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "stream", "rustls-tls"] }
ring = "0.17"
reqwest = { version = "0.11", default-features = false, features = [
"blocking",
"json",
"stream",
"rustls-tls",
"native-tls-vendored",
] }
ring = "0.16"
rpds = "0.11"
rusqlite = { version = "0.38", features = ["bundled"] }
rust_eth_kzg = "0.9"
safe_arith = "0.1"
sensitive_url = { version = "0.1", features = ["serde"] }
rusqlite = { version = "0.28", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_repr = "0.1"
sha2 = "0.10"
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 = "1"
serde_yaml = "0.9"
sha2 = "0.9"
slog = { version = "2", features = [
"max_level_debug",
"release_max_level_debug",
"nested-values",
] }
slog-async = "2"
slog-term = "2"
sloggers = { version = "2", features = ["json"] }
smallvec = { version = "1.11.2", features = ["arbitrary"] }
snap = "1"
ssz_types = { version = "0.14.0", features = ["context_deserialize", "runtime_types"] }
state_processing = { path = "consensus/state_processing" }
store = { path = "beacon_node/store" }
strum = { version = "0.27", features = ["derive"] }
superstruct = "0.10"
swap_or_not_shuffle = { path = "consensus/swap_or_not_shuffle" }
syn = "2"
ssz_types = "0.8"
strum = { version = "0.24", features = ["derive"] }
superstruct = "0.8"
syn = "1"
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"
tracing-appender = "0.2"
tracing-core = "0.1"
tracing-log = "0.2"
tracing-opentelemetry = "0.31.0"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
tracing_samplers = { path = "common/tracing_samplers" }
tree_hash = "0.12.0"
tree_hash_derive = "0.12.0"
typenum = "1"
types = { path = "consensus/types", features = ["saturating-arith"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tree_hash = "0.8"
tree_hash_derive = "0.8"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
uuid = { version = "0.8", features = ["serde", "v4"] }
warp = { version = "0.3.7", default-features = false, features = ["tls"] }
zeroize = { version = "1", features = ["zeroize_derive", "serde"] }
zip = "0.6"
# Local crates.
account_utils = { path = "common/account_utils" }
beacon_chain = { path = "beacon_node/beacon_chain" }
beacon_node = { path = "beacon_node" }
beacon_node_fallback = { path = "validator_client/beacon_node_fallback" }
beacon_processor = { path = "beacon_node/beacon_processor" }
bls = { path = "crypto/bls" }
clap_utils = { path = "common/clap_utils" }
compare_fields = { path = "common/compare_fields" }
deposit_contract = { path = "common/deposit_contract" }
directory = { path = "common/directory" }
doppelganger_service = { path = "validator_client/doppelganger_service" }
validator_services = { path = "validator_client/validator_services" }
environment = { path = "lighthouse/environment" }
eth1 = { path = "beacon_node/eth1" }
eth1_test_rig = { path = "testing/eth1_test_rig" }
eth2 = { path = "common/eth2" }
eth2_config = { path = "common/eth2_config" }
eth2_key_derivation = { path = "crypto/eth2_key_derivation" }
eth2_keystore = { path = "crypto/eth2_keystore" }
eth2_network_config = { path = "common/eth2_network_config" }
eth2_wallet = { path = "crypto/eth2_wallet" }
execution_layer = { path = "beacon_node/execution_layer" }
fixed_bytes = { path = "consensus/fixed_bytes" }
filesystem = { path = "common/filesystem" }
fork_choice = { path = "consensus/fork_choice" }
genesis = { path = "beacon_node/genesis" }
gossipsub = { path = "beacon_node/lighthouse_network/gossipsub/" }
health_metrics = { path = "common/health_metrics" }
http_api = { path = "beacon_node/http_api" }
initialized_validators = { path = "validator_client/initialized_validators" }
int_to_bytes = { path = "consensus/int_to_bytes" }
kzg = { path = "crypto/kzg" }
metrics = { path = "common/metrics" }
lighthouse_network = { path = "beacon_node/lighthouse_network" }
lighthouse_version = { path = "common/lighthouse_version" }
lockfile = { path = "common/lockfile" }
logging = { path = "common/logging" }
lru_cache = { path = "common/lru_cache" }
malloc_utils = { path = "common/malloc_utils" }
merkle_proof = { path = "consensus/merkle_proof" }
monitoring_api = { path = "common/monitoring_api" }
network = { path = "beacon_node/network" }
node_test_rig = { path = "testing/node_test_rig" }
operation_pool = { path = "beacon_node/operation_pool" }
pretty_reqwest_error = { path = "common/pretty_reqwest_error" }
proto_array = { path = "consensus/proto_array" }
safe_arith = { path = "consensus/safe_arith" }
sensitive_url = { path = "common/sensitive_url" }
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" }
state_processing = { path = "consensus/state_processing" }
store = { path = "beacon_node/store" }
swap_or_not_shuffle = { path = "consensus/swap_or_not_shuffle" }
system_health = { path = "common/system_health" }
task_executor = { path = "common/task_executor" }
types = { path = "consensus/types" }
unused_port = { path = "common/unused_port" }
validator_client = { path = "validator_client" }
validator_dir = { path = "common/validator_dir" }
validator_http_api = { path = "validator_client/http_api" }
validator_http_metrics = { path = "validator_client/http_metrics" }
validator_metrics = { path = "validator_client/validator_metrics" }
validator_services = { path = "validator_client/validator_services" }
validator_store = { path = "validator_client/validator_store" }
validator_test_rig = { path = "testing/validator_test_rig" }
warp = { version = "0.3.7", default-features = false, features = ["tls"] }
warp_utils = { path = "common/warp_utils" }
workspace_members = { path = "common/workspace_members" }
xdelta3 = { git = "https://github.com/sigp/xdelta3-rs", rev = "fe3906605c87b6c0515bd7c8fc671f47875e3ccc" }
yaml_serde = "0.10"
zeroize = { version = "1", features = ["zeroize_derive", "serde"] }
zip = { version = "6.0", default-features = false, features = ["deflate"] }
xdelta3 = { git = "http://github.com/sigp/xdelta3-rs", rev = "50d63cdf1878e5cf3538e9aae5eed34a22c64e4a" }
zstd = "0.13"
[profile.maxperf]
@@ -269,10 +298,5 @@ lto = "fat"
codegen-units = 1
incremental = false
[profile.release-debug]
inherits = "release"
debug = true
[patch.crates-io]
quick-protobuf = { git = "https://github.com/sigp/quick-protobuf.git", rev = "87c4ccb9bb2af494de375f5f6c62850badd26304" }
quick-protobuf = { git = "https://github.com/sigp/quick-protobuf.git", rev = "681f413312404ab6e51f0b46f39b0075c6f4ebfd" }

View File

@@ -4,11 +4,6 @@ pre-build = ["apt-get install -y cmake clang-5.0"]
[target.aarch64-unknown-linux-gnu]
pre-build = ["apt-get install -y cmake clang-5.0"]
[target.riscv64gc-unknown-linux-gnu]
pre-build = ["apt-get install -y cmake clang"]
# Use the most recent Cross image for RISCV because the stable 0.2.5 image doesn't work
image = "ghcr.io/cross-rs/riscv64gc-unknown-linux-gnu:main"
# Allow setting page size limits for jemalloc at build time:
# For certain architectures (like aarch64), we must compile
# jemalloc with support for large page sizes, otherwise the host's

View File

@@ -1,19 +1,13 @@
FROM rust:1.88.0-bullseye AS builder
FROM rust:1.84.0-bullseye AS builder
RUN apt-get update && apt-get -y upgrade && apt-get install -y cmake libclang-dev
COPY . lighthouse
ARG FEATURES
ARG PROFILE=release
ARG CARGO_USE_GIT_CLI=true
ENV FEATURES=$FEATURES
ENV PROFILE=$PROFILE
ENV CARGO_NET_GIT_FETCH_WITH_CLI=$CARGO_USE_GIT_CLI
ENV CARGO_INCREMENTAL=1
WORKDIR /lighthouse
COPY . .
# Persist the registry and target file across builds. See: https://docs.docker.com/build/cache/optimize/#use-cache-mounts
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/lighthouse/target \
make
RUN cd lighthouse && make
FROM ubuntu:22.04
RUN apt-get update && apt-get -y upgrade && apt-get install -y --no-install-recommends \

View File

@@ -1,24 +0,0 @@
# Define the Rust image as an argument with a default to x86_64 Rust 1.88 image based on Debian Bullseye
ARG RUST_IMAGE="rust:1.88-bullseye@sha256:8e3c421122bf4cd3b2a866af41a4dd52d87ad9e315fd2cb5100e87a7187a9816"
FROM ${RUST_IMAGE} AS builder
# Install specific version of the build dependencies
RUN apt-get update && apt-get install -y libclang-dev=1:11.0-51+nmu5 cmake=3.18.4-2+deb11u1 libjemalloc-dev=5.2.1-3
ARG RUST_TARGET="x86_64-unknown-linux-gnu"
# Copy the project to the container
COPY ./ /app
WORKDIR /app
# Build the project with the reproducible settings
RUN make build-reproducible
# Move the binary to a standard location
RUN mv /app/target/${RUST_TARGET}/release/lighthouse /lighthouse
# Create a minimal final image with just the binary
FROM gcr.io/distroless/cc-debian12:nonroot-6755e21ccd99ddead6edc8106ba03888cbeed41a
COPY --from=builder /lighthouse /lighthouse
ENTRYPOINT [ "/lighthouse" ]

177
Makefile
View File

@@ -3,20 +3,18 @@
EF_TESTS = "testing/ef_tests"
STATE_TRANSITION_VECTORS = "testing/state_transition_vectors"
EXECUTION_ENGINE_INTEGRATION = "testing/execution_engine_integration"
GIT_TAG = $(shell git describe --tags --candidates 1)
GIT_TAG := $(shell git describe --tags --candidates 1)
BIN_DIR = "bin"
X86_64_TAG = "x86_64-unknown-linux-gnu"
BUILD_PATH_X86_64 = "target/$(X86_64_TAG)/release"
AARCH64_TAG = "aarch64-unknown-linux-gnu"
BUILD_PATH_AARCH64 = "target/$(AARCH64_TAG)/release"
RISCV64_TAG = "riscv64gc-unknown-linux-gnu"
BUILD_PATH_RISCV64 = "target/$(RISCV64_TAG)/release"
PINNED_NIGHTLY ?= nightly
# List of features to use when cross-compiling. Can be overridden via the environment.
CROSS_FEATURES ?= gnosis,slasher-lmdb,slasher-mdbx,slasher-redb,beacon-node-leveldb,beacon-node-redb
CROSS_FEATURES ?= gnosis,slasher-lmdb,slasher-mdbx,slasher-redb,jemalloc,beacon-node-leveldb,beacon-node-redb
# Cargo profile for Cross builds. Default is for local builds, CI uses an override.
CROSS_PROFILE ?= release
@@ -30,17 +28,9 @@ TEST_FEATURES ?=
# Cargo profile for regular builds.
PROFILE ?= release
# List of all hard forks up to gloas. This list is used to set env variables for several tests so that
# List of all hard forks. This list is used to set env variables for several tests so that
# they run for different forks.
# TODO(EIP-7732) Remove this once we extend network tests to support gloas and use RECENT_FORKS instead
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)
FORKS=phase0 altair bellatrix capella deneb electra fulu
# Extra flags for Cargo
CARGO_INSTALL_EXTRA_FLAGS?=
@@ -77,8 +67,6 @@ build-aarch64:
# pages, which are commonly used by aarch64 systems.
# See: https://github.com/sigp/lighthouse/issues/5244
JEMALLOC_SYS_WITH_LG_PAGE=16 cross build --bin lighthouse --target aarch64-unknown-linux-gnu --features "portable,$(CROSS_FEATURES)" --profile "$(CROSS_PROFILE)" --locked
build-riscv64:
cross build --bin lighthouse --target riscv64gc-unknown-linux-gnu --features "portable,$(CROSS_FEATURES)" --profile "$(CROSS_PROFILE)" --locked
build-lcli-x86_64:
cross build --bin lcli --target x86_64-unknown-linux-gnu --features "portable" --profile "$(CROSS_PROFILE)" --locked
@@ -87,70 +75,6 @@ build-lcli-aarch64:
# pages, which are commonly used by aarch64 systems.
# See: https://github.com/sigp/lighthouse/issues/5244
JEMALLOC_SYS_WITH_LG_PAGE=16 cross build --bin lcli --target aarch64-unknown-linux-gnu --features "portable" --profile "$(CROSS_PROFILE)" --locked
build-lcli-riscv64:
cross build --bin lcli --target riscv64gc-unknown-linux-gnu --features "portable" --profile "$(CROSS_PROFILE)" --locked
# Environment variables for reproducible builds
# Initialize RUSTFLAGS
RUST_BUILD_FLAGS =
# Remove build ID from the binary to ensure reproducibility across builds
RUST_BUILD_FLAGS += -C link-arg=-Wl,--build-id=none
# Remove metadata hash from symbol names to ensure reproducible builds
RUST_BUILD_FLAGS += -C metadata=''
# Set timestamp from last git commit for reproducible builds
SOURCE_DATE ?= $(shell git log -1 --pretty=%ct)
# Disable incremental compilation to avoid non-deterministic artifacts
CARGO_INCREMENTAL_VAL = 0
# Set C locale for consistent string handling and sorting
LOCALE_VAL = C
# Set UTC timezone for consistent time handling across builds
TZ_VAL = UTC
# Features for reproducible builds
FEATURES_REPRODUCIBLE = $(CROSS_FEATURES),jemalloc-unprefixed
# Derive the architecture-specific library path from RUST_TARGET
JEMALLOC_LIB_ARCH = $(word 1,$(subst -, ,$(RUST_TARGET)))
JEMALLOC_OVERRIDE = /usr/lib/$(JEMALLOC_LIB_ARCH)-linux-gnu/libjemalloc.a
# Default target architecture
RUST_TARGET ?= x86_64-unknown-linux-gnu
# Default images for different architectures
RUST_IMAGE_AMD64 ?= rust:1.88-bullseye@sha256:8e3c421122bf4cd3b2a866af41a4dd52d87ad9e315fd2cb5100e87a7187a9816
RUST_IMAGE_ARM64 ?= rust:1.88-bullseye@sha256:8b22455a7ce2adb1355067638284ee99d21cc516fab63a96c4514beaf370aa94
.PHONY: build-reproducible
build-reproducible: ## Build the lighthouse binary into `target` directory with reproducible builds
SOURCE_DATE_EPOCH=$(SOURCE_DATE) \
RUSTFLAGS="${RUST_BUILD_FLAGS} --remap-path-prefix $$(pwd)=." \
CARGO_INCREMENTAL=${CARGO_INCREMENTAL_VAL} \
LC_ALL=${LOCALE_VAL} \
TZ=${TZ_VAL} \
JEMALLOC_OVERRIDE=${JEMALLOC_OVERRIDE} \
cargo build --bin lighthouse --features "$(FEATURES_REPRODUCIBLE)" --profile "$(PROFILE)" --locked --target $(RUST_TARGET)
.PHONY: build-reproducible-x86_64
build-reproducible-x86_64: ## Build reproducible x86_64 Docker image
DOCKER_BUILDKIT=1 docker build \
--build-arg RUST_TARGET="x86_64-unknown-linux-gnu" \
--build-arg RUST_IMAGE=$(RUST_IMAGE_AMD64) \
-f Dockerfile.reproducible \
-t lighthouse:reproducible-amd64 .
.PHONY: build-reproducible-aarch64
build-reproducible-aarch64: ## Build reproducible aarch64 Docker image
DOCKER_BUILDKIT=1 docker build \
--platform linux/arm64 \
--build-arg RUST_TARGET="aarch64-unknown-linux-gnu" \
--build-arg RUST_IMAGE=$(RUST_IMAGE_ARM64) \
-f Dockerfile.reproducible \
-t lighthouse:reproducible-arm64 .
.PHONY: build-reproducible-all
build-reproducible-all: build-reproducible-x86_64 build-reproducible-aarch64 ## Build both x86_64 and aarch64 reproducible Docker images
# Create a `.tar.gz` containing a binary for a specific target.
define tarball_release_binary
@@ -171,24 +95,30 @@ build-release-tarballs:
$(call tarball_release_binary,$(BUILD_PATH_X86_64),$(X86_64_TAG),"")
$(MAKE) build-aarch64
$(call tarball_release_binary,$(BUILD_PATH_AARCH64),$(AARCH64_TAG),"")
$(MAKE) build-riscv64
$(call tarball_release_binary,$(BUILD_PATH_RISCV64),$(RISCV64_TAG),"")
# Runs the full workspace tests in **release**, without downloading any additional
# test vectors.
test-release:
cargo nextest run --workspace --release --features "$(TEST_FEATURES)" \
--exclude ef_tests --exclude beacon_chain --exclude slasher --exclude network \
--exclude http_api
cargo test --workspace --release --features "$(TEST_FEATURES)" \
--exclude ef_tests --exclude beacon_chain --exclude slasher --exclude network
# Runs the full workspace tests in **release**, without downloading any additional
# test vectors, using nextest.
nextest-release:
cargo nextest run --workspace --release --features "$(TEST_FEATURES)" \
--exclude ef_tests --exclude beacon_chain --exclude slasher --exclude network
# Runs the full workspace tests in **debug**, without downloading any additional test
# vectors.
test-debug:
cargo test --workspace --features "$(TEST_FEATURES)" \
--exclude ef_tests --exclude beacon_chain --exclude network
# Runs the full workspace tests in **debug**, without downloading any additional test
# vectors, using nextest.
nextest-debug:
cargo nextest run --workspace --features "$(TEST_FEATURES)" \
--exclude ef_tests --exclude beacon_chain --exclude network --exclude http_api
--exclude ef_tests --exclude beacon_chain --exclude network
# Runs cargo-fmt (linter).
cargo-fmt:
@@ -198,29 +128,28 @@ cargo-fmt:
check-benches:
cargo check --workspace --benches --features "$(TEST_FEATURES)"
# Runs EF test vectors
# Runs only the ef-test vectors.
run-ef-tests:
rm -rf $(EF_TESTS)/.accessed_file_log.txt
cargo test --release -p ef_tests --features "ef_tests,$(EF_TEST_FEATURES)"
cargo test --release -p ef_tests --features "ef_tests,$(EF_TEST_FEATURES),fake_crypto"
./$(EF_TESTS)/check_all_files_accessed.py $(EF_TESTS)/.accessed_file_log.txt $(EF_TESTS)/consensus-spec-tests
# Runs EF test vectors with nextest
nextest-run-ef-tests:
rm -rf $(EF_TESTS)/.accessed_file_log.txt
cargo nextest run --release -p ef_tests --features "ef_tests,$(EF_TEST_FEATURES)"
cargo nextest run --release -p ef_tests --features "ef_tests,$(EF_TEST_FEATURES),fake_crypto"
./$(EF_TESTS)/check_all_files_accessed.py $(EF_TESTS)/.accessed_file_log.txt $(EF_TESTS)/consensus-spec-tests
# Run the tests in the `beacon_chain` crate for all known forks.
test-beacon-chain: $(patsubst %,test-beacon-chain-%,$(RECENT_FORKS))
test-beacon-chain: $(patsubst %,test-beacon-chain-%,$(FORKS))
test-beacon-chain-%:
env FORK_NAME=$* cargo nextest run --release --features "fork_from_env,slasher/lmdb,$(TEST_FEATURES)" -p beacon_chain --no-fail-fast
# Run the tests in the `http_api` crate for recent forks.
test-http-api: $(patsubst %,test-http-api-%,$(RECENT_FORKS))
test-http-api-%:
env FORK_NAME=$* cargo nextest run --release --features "beacon_chain/fork_from_env" -p http_api
env FORK_NAME=$* cargo nextest run --release --features "fork_from_env,slasher/lmdb,$(TEST_FEATURES)" -p beacon_chain
# Run the tests in the `operation_pool` crate for all known forks.
test-op-pool: $(patsubst %,test-op-pool-%,$(RECENT_FORKS_BEFORE_GLOAS))
test-op-pool: $(patsubst %,test-op-pool-%,$(FORKS))
test-op-pool-%:
env FORK_NAME=$* cargo nextest run --release \
@@ -228,16 +157,12 @@ test-op-pool-%:
-p operation_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-%,$(TEST_NETWORK_FORKS))
test-network: $(patsubst %,test-network-%,$(FORKS))
test-network-%:
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 \
env FORK_NAME=$* cargo nextest run --release \
--features "fork_from_env,$(TEST_FEATURES)" \
-p network crypto_on
-p network
# Run the tests in the `slasher` crate for all supported database backends.
test-slasher:
@@ -253,8 +178,8 @@ run-state-transition-tests:
# Downloads and runs the EF test vectors.
test-ef: make-ef-tests run-ef-tests
# Downloads and runs the nightly EF test vectors.
test-ef-nightly: make-ef-tests-nightly run-ef-tests
# Downloads and runs the EF test vectors with nextest.
nextest-ef: make-ef-tests nextest-run-ef-tests
# Runs tests checking interop between Lighthouse and execution clients.
test-exec-engine:
@@ -289,7 +214,6 @@ lint:
-D clippy::fn_to_numeric_cast_any \
-D clippy::manual_let_else \
-D clippy::large_stack_frames \
-D clippy::disallowed_methods \
-D warnings \
-A clippy::derive_partial_eq_without_eq \
-A clippy::upper-case-acronyms \
@@ -300,7 +224,7 @@ lint:
# Lints the code using Clippy and automatically fix some simple compiler warnings.
lint-fix:
EXTRA_CLIPPY_OPTS="--fix --allow-staged --allow-dirty" $(MAKE) lint-full
EXTRA_CLIPPY_OPTS="--fix --allow-staged --allow-dirty" $(MAKE) lint
# Also run the lints on the optimized-only tests
lint-full:
@@ -314,14 +238,10 @@ lint-full:
make-ef-tests:
make -C $(EF_TESTS)
# Download/extract the nightly EF test vectors.
make-ef-tests-nightly:
CONSENSUS_SPECS_TEST_VERSION=nightly make -C $(EF_TESTS)
# Verifies that crates compile with fuzzing features enabled
arbitrary-fuzz:
cargo check -p state_processing --features arbitrary,$(TEST_FEATURES)
cargo check -p slashing_protection --features arbitrary,$(TEST_FEATURES)
cargo check -p state_processing --features arbitrary-fuzz,$(TEST_FEATURES)
cargo check -p slashing_protection --features arbitrary-fuzz,$(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
@@ -330,16 +250,7 @@ install-audit:
cargo install --force cargo-audit
audit-CI:
cargo audit --ignore RUSTSEC-2026-0049 --ignore RUSTSEC-2026-0098 --ignore RUSTSEC-2026-0099 --ignore RUSTSEC-2026-0104 --ignore RUSTSEC-2026-0118 --ignore RUSTSEC-2026-0119
# Runs cargo deny (check for banned crates, duplicate versions, and source restrictions)
deny: install-deny deny-CI
install-deny:
cargo install --force cargo-deny --version 0.18.2
deny-CI:
cargo deny check bans sources
cargo audit
# Runs `cargo vendor` to make sure dependencies can be vendored for packaging, reproducibility and archival purpose.
vendor:
@@ -349,20 +260,8 @@ 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'."

View File

@@ -1,7 +1,10 @@
[package]
name = "account_manager"
version = { workspace = true }
authors = ["Paul Hauner <paul@paulhauner.com>", "Luke Anderson <luke@sigmaprime.io>"]
version = "0.3.5"
authors = [
"Paul Hauner <paul@paulhauner.com>",
"Luke Anderson <luke@sigmaprime.io>",
]
edition = { workspace = true }
[dependencies]
@@ -19,7 +22,6 @@ eth2_wallet_manager = { path = "../common/eth2_wallet_manager" }
filesystem = { workspace = true }
safe_arith = { workspace = true }
sensitive_url = { workspace = true }
serde_json = { workspace = true }
slashing_protection = { workspace = true }
slot_clock = { workspace = true }
tokio = { workspace = true }

View File

@@ -1,15 +1,15 @@
use crate::common::read_wallet_name_from_cli;
use crate::{SECRETS_DIR_FLAG, WALLETS_DIR_FLAG};
use account_utils::{
PlainText, STDIN_INPUTS_FLAG, random_password, read_password_from_user, strip_off_newlines,
validator_definitions,
random_password, read_password_from_user, strip_off_newlines, validator_definitions, PlainText,
STDIN_INPUTS_FLAG,
};
use clap::{Arg, ArgAction, ArgMatches, Command};
use clap_utils::FLAG_HEADER;
use directory::{DEFAULT_SECRET_DIR, DEFAULT_WALLET_DIR, parse_path_or_default_with_flag};
use directory::{parse_path_or_default_with_flag, DEFAULT_SECRET_DIR, DEFAULT_WALLET_DIR};
use environment::Environment;
use eth2_wallet_manager::WalletManager;
use slashing_protection::{SLASHING_PROTECTION_FILENAME, SlashingDatabase};
use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME};
use std::ffi::OsStr;
use std::fs;
use std::fs::create_dir_all;
@@ -148,9 +148,7 @@ pub fn cli_run<E: EthSpec>(
return Err(format!(
"No wallet directory at {:?}. Use the `lighthouse --network {} {} {} {}` command to create a wallet",
wallet_base_dir,
matches
.get_one::<String>("network")
.unwrap_or(&String::from("<NETWORK>")),
matches.get_one::<String>("network").unwrap_or(&String::from("<NETWORK>")),
crate::CMD,
crate::wallet::CMD,
crate::wallet::create::CMD

View File

@@ -4,14 +4,13 @@ use clap::{Arg, ArgAction, ArgMatches, Command};
use clap_utils::FLAG_HEADER;
use environment::Environment;
use eth2::{
BeaconNodeHttpClient, Timeouts,
types::{GenesisData, StateId, ValidatorData, ValidatorId, ValidatorStatus},
BeaconNodeHttpClient, Timeouts,
};
use eth2_keystore::Keystore;
use eth2_network_config::Eth2NetworkConfig;
use safe_arith::SafeArith;
use sensitive_url::SensitiveUrl;
use serde_json;
use slot_clock::{SlotClock, SystemTimeSlotClock};
use std::path::{Path, PathBuf};
use std::time::Duration;
@@ -25,11 +24,10 @@ pub const BEACON_SERVER_FLAG: &str = "beacon-node";
pub const NO_WAIT: &str = "no-wait";
pub const NO_CONFIRMATION: &str = "no-confirmation";
pub const PASSWORD_PROMPT: &str = "Enter the keystore password";
pub const PRESIGN: &str = "presign";
pub const DEFAULT_BEACON_NODE: &str = "http://localhost:5052/";
pub const CONFIRMATION_PHRASE: &str = "Exit my validator";
pub const WEBSITE_URL: &str = "https://lighthouse-book.sigmaprime.io/validator_voluntary_exit.html";
pub const WEBSITE_URL: &str = "https://lighthouse-book.sigmaprime.io/voluntary-exit.html";
pub fn cli_app() -> Command {
Command::new("exit")
@@ -76,15 +74,6 @@ pub fn cli_app() -> Command {
.action(ArgAction::SetTrue)
.help_heading(FLAG_HEADER)
)
.arg(
Arg::new(PRESIGN)
.long(PRESIGN)
.help("Only presign the voluntary exit message without publishing it")
.default_value("false")
.action(ArgAction::SetTrue)
.help_heading(FLAG_HEADER)
.display_order(0)
)
}
pub fn cli_run<E: EthSpec>(matches: &ArgMatches, env: Environment<E>) -> Result<(), String> {
@@ -95,14 +84,13 @@ pub fn cli_run<E: EthSpec>(matches: &ArgMatches, env: Environment<E>) -> Result<
let stdin_inputs = cfg!(windows) || matches.get_flag(STDIN_INPUTS_FLAG);
let no_wait = matches.get_flag(NO_WAIT);
let no_confirmation = matches.get_flag(NO_CONFIRMATION);
let presign = matches.get_flag(PRESIGN);
let spec = env.eth2_config().spec.clone();
let server_url: String = clap_utils::parse_required(matches, BEACON_SERVER_FLAG)?;
let client = BeaconNodeHttpClient::new(
SensitiveUrl::parse(&server_url)
.map_err(|e| format!("Failed to parse beacon http server: {:?}", e))?,
Timeouts::set_all(env.eth2_config.spec.get_slot_duration()),
Timeouts::set_all(Duration::from_secs(env.eth2_config.spec.seconds_per_slot)),
);
let eth2_network_config = env
@@ -119,7 +107,6 @@ pub fn cli_run<E: EthSpec>(matches: &ArgMatches, env: Environment<E>) -> Result<
&eth2_network_config,
no_wait,
no_confirmation,
presign,
))?;
Ok(())
@@ -136,7 +123,6 @@ async fn publish_voluntary_exit<E: EthSpec>(
eth2_network_config: &Eth2NetworkConfig,
no_wait: bool,
no_confirmation: bool,
presign: bool,
) -> Result<(), String> {
let genesis_data = get_geneisis_data(client).await?;
let testnet_genesis_root = eth2_network_config
@@ -168,23 +154,6 @@ async fn publish_voluntary_exit<E: EthSpec>(
validator_index,
};
// Sign the voluntary exit. We sign ahead of the prompt as that step is only important for the broadcast
let signed_voluntary_exit =
voluntary_exit.sign(&keypair.sk, genesis_data.genesis_validators_root, spec);
if presign {
eprintln!(
"Successfully pre-signed voluntary exit for validator {}. Not publishing.",
keypair.pk
);
// Convert to JSON and print
let string_output = serde_json::to_string_pretty(&signed_voluntary_exit)
.map_err(|e| format!("Unable to convert to JSON: {}", e))?;
println!("{}", string_output);
return Ok(());
}
eprintln!(
"Publishing a voluntary exit for validator: {} \n",
keypair.pk
@@ -205,7 +174,9 @@ async fn publish_voluntary_exit<E: EthSpec>(
};
if confirmation == CONFIRMATION_PHRASE {
// Publish the voluntary exit to network
// Sign and publish the voluntary exit to network
let signed_voluntary_exit =
voluntary_exit.sign(&keypair.sk, genesis_data.genesis_validators_root, spec);
client
.post_beacon_pool_voluntary_exits(&signed_voluntary_exit)
.await
@@ -230,7 +201,7 @@ async fn publish_voluntary_exit<E: EthSpec>(
loop {
// Sleep for a slot duration and then check if voluntary exit was processed
// by checking the validator status.
sleep(spec.get_slot_duration()).await;
sleep(Duration::from_secs(spec.seconds_per_slot)).await;
let validator_data = get_validator_data(client, &keypair.pk).await?;
match validator_data.status {
@@ -239,11 +210,9 @@ async fn publish_voluntary_exit<E: EthSpec>(
let withdrawal_epoch = validator_data.validator.withdrawable_epoch;
let current_epoch = get_current_epoch::<E>(genesis_data.genesis_time, spec)
.ok_or("Failed to get current epoch. Please check your system time")?;
eprintln!(
"Voluntary exit has been accepted into the beacon chain, but not yet finalized. \
eprintln!("Voluntary exit has been accepted into the beacon chain, but not yet finalized. \
Finalization may take several minutes or longer. Before finalization there is a low \
probability that the exit may be reverted."
);
probability that the exit may be reverted.");
eprintln!(
"Current epoch: {}, Exit epoch: {}, Withdrawable epoch: {}",
current_epoch, exit_epoch, withdrawal_epoch
@@ -251,9 +220,7 @@ async fn publish_voluntary_exit<E: EthSpec>(
eprintln!("Please keep your validator running till exit epoch");
eprintln!(
"Exit epoch in approximately {} secs",
(exit_epoch - current_epoch)
* spec.get_slot_duration().as_secs()
* E::slots_per_epoch()
(exit_epoch - current_epoch) * spec.seconds_per_slot * E::slots_per_epoch()
);
break;
}
@@ -352,7 +319,7 @@ fn get_current_epoch<E: EthSpec>(genesis_time: u64, spec: &ChainSpec) -> Option<
let slot_clock = SystemTimeSlotClock::new(
spec.genesis_slot,
Duration::from_secs(genesis_time),
spec.get_slot_duration(),
Duration::from_secs(spec.seconds_per_slot),
);
slot_clock.now().map(|s| s.epoch(E::slots_per_epoch()))
}
@@ -405,7 +372,7 @@ mod tests {
use eth2_keystore::KeystoreBuilder;
use std::fs::File;
use std::io::Write;
use tempfile::{TempDir, tempdir};
use tempfile::{tempdir, TempDir};
const PASSWORD: &str = "cats";
const KEYSTORE_NAME: &str = "keystore-m_12381_3600_0_0_0-1595406747.json";

View File

@@ -1,17 +1,17 @@
use crate::wallet::create::PASSWORD_FLAG;
use account_utils::validator_definitions::SigningDefinition;
use account_utils::{
STDIN_INPUTS_FLAG,
eth2_keystore::Keystore,
read_password_from_user,
validator_definitions::{
CONFIG_FILENAME, PasswordStorage, ValidatorDefinition, ValidatorDefinitions,
recursively_find_voting_keystores,
recursively_find_voting_keystores, PasswordStorage, ValidatorDefinition,
ValidatorDefinitions, CONFIG_FILENAME,
},
STDIN_INPUTS_FLAG,
};
use clap::{Arg, ArgAction, ArgMatches, Command};
use clap_utils::FLAG_HEADER;
use slashing_protection::{SLASHING_PROTECTION_FILENAME, SlashingDatabase};
use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME};
use std::fs;
use std::path::PathBuf;
use std::thread::sleep;
@@ -32,7 +32,7 @@ pub fn cli_app() -> Command {
.about(
"Imports one or more EIP-2335 passwords into a Lighthouse VC directory, \
requesting passwords interactively. The directory flag provides a convenient \
method for importing a directory of keys generated by the ethstaker-deposit-cli \
method for importing a directory of keys generated by the eth2-deposit-cli \
Python utility.",
)
.arg(
@@ -133,7 +133,7 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin
return Err(format!(
"Must supply either --{} or --{}",
KEYSTORE_FLAG, DIR_FLAG
));
))
}
};
@@ -227,20 +227,19 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin
if let Some(ValidatorDefinition {
signing_definition:
SigningDefinition::LocalKeystore {
voting_keystore_password: old_passwd,
voting_keystore_password: ref mut old_passwd,
..
},
..
}) = old_validator_def_opt
&& old_passwd.is_none()
&& password_opt.is_some()
{
*old_passwd = password_opt;
defs.save(&validator_dir)
.map_err(|e| format!("Unable to save {}: {:?}", CONFIG_FILENAME, e))?;
eprintln!("Password updated for public key {}", voting_pubkey);
if old_passwd.is_none() && password_opt.is_some() {
*old_passwd = password_opt;
defs.save(&validator_dir)
.map_err(|e| format!("Unable to save {}: {:?}", CONFIG_FILENAME, e))?;
eprintln!("Password updated for public key {}", voting_pubkey);
}
}
eprintln!(
"Skipping import of keystore for existing public key: {:?}",
src_keystore

View File

@@ -8,7 +8,7 @@ pub mod slashing_protection;
use crate::{VALIDATOR_DIR_FLAG, VALIDATOR_DIR_FLAG_ALIAS};
use clap::{Arg, ArgAction, ArgMatches, Command};
use directory::{DEFAULT_VALIDATOR_DIR, parse_path_or_default_with_flag};
use directory::{parse_path_or_default_with_flag, DEFAULT_VALIDATOR_DIR};
use environment::Environment;
use std::path::PathBuf;
use types::EthSpec;
@@ -28,7 +28,6 @@ pub fn cli_app() -> Command {
"The path to search for validator directories. \
Defaults to ~/.lighthouse/{network}/validators",
)
.global(true)
.action(ArgAction::Set)
.conflicts_with("datadir"),
)

View File

@@ -69,7 +69,7 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin
return Err(format!(
"{} does not have a {} command. See --help",
CMD, unknown
));
))
}
_ => return Err(format!("No command provided for {}. See --help", CMD)),
};

View File

@@ -1,13 +1,13 @@
use super::create::STORE_WITHDRAW_FLAG;
use crate::SECRETS_DIR_FLAG;
use crate::validator::create::COUNT_FLAG;
use account_utils::eth2_keystore::{Keystore, KeystoreBuilder, keypair_from_secret};
use account_utils::{STDIN_INPUTS_FLAG, random_password, read_mnemonic_from_cli};
use crate::SECRETS_DIR_FLAG;
use account_utils::eth2_keystore::{keypair_from_secret, Keystore, KeystoreBuilder};
use account_utils::{random_password, read_mnemonic_from_cli, STDIN_INPUTS_FLAG};
use clap::{Arg, ArgAction, ArgMatches, Command};
use clap_utils::FLAG_HEADER;
use directory::{DEFAULT_SECRET_DIR, parse_path_or_default_with_flag};
use directory::{parse_path_or_default_with_flag, DEFAULT_SECRET_DIR};
use eth2_wallet::bip39::Seed;
use eth2_wallet::{KeyType, ValidatorKeystores, recover_validator_secret_from_mnemonic};
use eth2_wallet::{recover_validator_secret_from_mnemonic, KeyType, ValidatorKeystores};
use std::fs::create_dir_all;
use std::path::PathBuf;
use validator_dir::Builder as ValidatorDirBuilder;
@@ -97,9 +97,7 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin
.map_err(|e| format!("Could not create secrets dir at {secrets_dir:?}: {e:?}"))?;
eprintln!();
eprintln!(
"WARNING: KEY RECOVERY CAN LEAD TO DUPLICATING VALIDATORS KEYS, WHICH CAN LEAD TO SLASHING."
);
eprintln!("WARNING: KEY RECOVERY CAN LEAD TO DUPLICATING VALIDATORS KEYS, WHICH CAN LEAD TO SLASHING.");
eprintln!();
let mnemonic = read_mnemonic_from_cli(mnemonic_path, stdin_inputs)?;

View File

@@ -1,14 +1,13 @@
use bls::PublicKeyBytes;
use clap::{Arg, ArgAction, ArgMatches, Command};
use environment::Environment;
use slashing_protection::{
InterchangeError, InterchangeImportOutcome, SLASHING_PROTECTION_FILENAME, SlashingDatabase,
interchange::Interchange,
interchange::Interchange, InterchangeError, InterchangeImportOutcome, SlashingDatabase,
SLASHING_PROTECTION_FILENAME,
};
use std::fs::File;
use std::path::PathBuf;
use std::str::FromStr;
use types::{Epoch, EthSpec, Slot};
use types::{Epoch, EthSpec, PublicKeyBytes, Slot};
pub const CMD: &str = "slashing-protection";
pub const IMPORT_CMD: &str = "import";
@@ -91,7 +90,7 @@ pub fn cli_run<E: EthSpec>(
let slashing_protection_database =
SlashingDatabase::open_or_create(&slashing_protection_db_path).map_err(|e| {
format!(
"Unable to open slashing protection database at {}: {:?}",
"Unable to open database at {}: {:?}",
slashing_protection_db_path.display(),
e
)
@@ -199,7 +198,7 @@ pub fn cli_run<E: EthSpec>(
let slashing_protection_database = SlashingDatabase::open(&slashing_protection_db_path)
.map_err(|e| {
format!(
"Unable to open slashing protection database at {}: {:?}",
"Unable to open database at {}: {:?}",
slashing_protection_db_path.display(),
e
)

View File

@@ -1,13 +1,13 @@
use crate::WALLETS_DIR_FLAG;
use crate::common::read_wallet_name_from_cli;
use crate::WALLETS_DIR_FLAG;
use account_utils::{
STDIN_INPUTS_FLAG, is_password_sufficiently_complex, random_password, read_password_from_user,
strip_off_newlines,
is_password_sufficiently_complex, random_password, read_password_from_user, strip_off_newlines,
STDIN_INPUTS_FLAG,
};
use clap::{Arg, ArgAction, ArgMatches, Command};
use eth2_wallet::{
PlainText,
bip39::{Language, Mnemonic, MnemonicType},
PlainText,
};
use eth2_wallet_manager::{LockedWallet, WalletManager, WalletType};
use filesystem::create_with_600_perms;

View File

@@ -4,7 +4,7 @@ pub mod recover;
use crate::WALLETS_DIR_FLAG;
use clap::{Arg, ArgAction, ArgMatches, Command};
use directory::{DEFAULT_WALLET_DIR, parse_path_or_default_with_flag};
use directory::{parse_path_or_default_with_flag, DEFAULT_WALLET_DIR};
use std::fs::create_dir_all;
use std::path::PathBuf;

View File

@@ -1,6 +1,6 @@
use crate::wallet::create::create_wallet_from_mnemonic;
use crate::wallet::create::{HD_TYPE, NAME_FLAG, PASSWORD_FLAG, TYPE_FLAG};
use account_utils::{STDIN_INPUTS_FLAG, read_mnemonic_from_cli};
use account_utils::{read_mnemonic_from_cli, STDIN_INPUTS_FLAG};
use clap::{Arg, ArgAction, ArgMatches, Command};
use std::path::PathBuf;
@@ -63,9 +63,7 @@ pub fn cli_run(matches: &ArgMatches, wallet_base_dir: PathBuf) -> Result<(), Str
let stdin_inputs = cfg!(windows) || matches.get_flag(STDIN_INPUTS_FLAG);
eprintln!();
eprintln!(
"WARNING: KEY RECOVERY CAN LEAD TO DUPLICATING VALIDATORS KEYS, WHICH CAN LEAD TO SLASHING."
);
eprintln!("WARNING: KEY RECOVERY CAN LEAD TO DUPLICATING VALIDATORS KEYS, WHICH CAN LEAD TO SLASHING.");
eprintln!();
let mnemonic = read_mnemonic_from_cli(mnemonic_path, stdin_inputs)?;

View File

@@ -1,23 +1,27 @@
[package]
name = "beacon_node"
version = { workspace = true }
authors = ["Paul Hauner <paul@paulhauner.com>", "Age Manning <Age@AgeManning.com"]
version = "7.0.0-beta.1"
authors = [
"Paul Hauner <paul@paulhauner.com>",
"Age Manning <Age@AgeManning.com",
]
edition = { workspace = true }
[lib]
name = "beacon_node"
path = "src/lib.rs"
[dev-dependencies]
node_test_rig = { path = "../testing/node_test_rig" }
[features]
# Writes debugging .ssz files to /tmp during block processing.
write_ssz_files = ["beacon_chain/write_ssz_files"]
# Enables testing-only CLI flags.
testing = []
write_ssz_files = [
"beacon_chain/write_ssz_files",
] # Writes debugging .ssz files to /tmp during block processing.
[dependencies]
account_utils = { workspace = true }
beacon_chain = { workspace = true }
bls = { workspace = true }
clap = { workspace = true }
clap_utils = { workspace = true }
client = { path = "client" }
@@ -32,15 +36,12 @@ http_api = { workspace = true }
hyper = { workspace = true }
lighthouse_network = { workspace = true }
monitoring_api = { workspace = true }
network_utils = { workspace = true }
sensitive_url = { workspace = true }
serde_json = { workspace = true }
slasher = { workspace = true }
slog = { workspace = true }
store = { workspace = true }
strum = { workspace = true }
task_executor = { workspace = true }
tracing = { workspace = true }
types = { workspace = true }
[dev-dependencies]
node_test_rig = { path = "../testing/node_test_rig" }
unused_port = { workspace = true }

View File

@@ -1,4 +1,3 @@
[package]
name = "beacon_chain"
version = "0.2.0"
@@ -6,32 +5,36 @@ authors = ["Paul Hauner <paul@paulhauner.com>", "Age Manning <Age@AgeManning.com
edition = { workspace = true }
autotests = false # using a single test binary compiles faster
[[bench]]
name = "benches"
harness = false
[features]
default = ["participation_metrics"]
# 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 = []
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
portable = ["bls/supranational-portable"]
test_backfill = []
arbitrary = ["dep:arbitrary", "types/arbitrary"]
[dev-dependencies]
criterion = { workspace = true }
maplit = { workspace = true }
serde_json = { workspace = true }
[dependencies]
alloy-primitives = { workspace = true }
arbitrary = { workspace = true, optional = true }
bitvec = { workspace = true }
bls = { workspace = true }
educe = { workspace = true }
eth2 = { workspace = true, features = ["lighthouse", "network"] }
derivative = { workspace = true }
eth1 = { workspace = true }
eth2 = { workspace = true }
eth2_network_config = { workspace = true }
ethereum_hashing = { workspace = true }
ethereum_serde_utils = { workspace = true }
ethereum_ssz = { workspace = true }
ethereum_ssz_derive = { workspace = true }
execution_layer = { workspace = true }
fixed_bytes = { workspace = true }
fork_choice = { workspace = true }
futures = { workspace = true }
genesis = { workspace = true }
@@ -44,8 +47,6 @@ logging = { workspace = true }
lru = { workspace = true }
merkle_proof = { workspace = true }
metrics = { workspace = true }
milhouse = { workspace = true }
once_cell = { workspace = true }
oneshot_broadcast = { path = "../../common/oneshot_broadcast/" }
operation_pool = { workspace = true }
parking_lot = { workspace = true }
@@ -57,6 +58,10 @@ sensitive_url = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
slasher = { workspace = true }
slog = { workspace = true }
slog-async = { workspace = true }
slog-term = { workspace = true }
sloggers = { workspace = true }
slot_clock = { workspace = true }
smallvec = { workspace = true }
ssz_types = { workspace = true }
@@ -68,27 +73,9 @@ task_executor = { workspace = true }
tempfile = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
tracing = { workspace = true }
tree_hash = { workspace = true }
tree_hash_derive = { workspace = true }
typenum = { workspace = true }
types = { workspace = true }
zstd = { workspace = true }
[dev-dependencies]
arbitrary = { workspace = true }
beacon_chain = { path = ".", features = ["arbitrary"] }
criterion = { workspace = true }
maplit = { workspace = true }
mockall = { workspace = true }
mockall_double = { workspace = true }
rand_xorshift = { workspace = true }
serde_json = { workspace = true }
types = { workspace = true, features = ["arbitrary"] }
[[bench]]
name = "benches"
harness = false
[[test]]
name = "beacon_chain_tests"

View File

@@ -1,22 +1,21 @@
use std::hint::black_box;
use std::sync::Arc;
use beacon_chain::kzg_utils::{blobs_to_data_column_sidecars, reconstruct_data_columns};
use beacon_chain::test_utils::get_kzg;
use criterion::{Criterion, criterion_group, criterion_main};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use bls::Signature;
use kzg::{KzgCommitment, KzgProof};
use kzg::KzgCommitment;
use types::{
BeaconBlock, BeaconBlockFulu, Blob, BlobsList, ChainSpec, EmptyBlock, EthSpec, KzgProofs,
MainnetEthSpec, SignedBeaconBlock, kzg_ext::KzgCommitments,
beacon_block_body::KzgCommitments, BeaconBlock, BeaconBlockDeneb, Blob, BlobsList, ChainSpec,
EmptyBlock, EthSpec, MainnetEthSpec, SignedBeaconBlock,
};
fn create_test_block_and_blobs<E: EthSpec>(
num_of_blobs: usize,
spec: &ChainSpec,
) -> (SignedBeaconBlock<E>, BlobsList<E>, KzgProofs<E>) {
let mut block = BeaconBlock::Fulu(BeaconBlockFulu::empty(spec));
) -> (SignedBeaconBlock<E>, BlobsList<E>) {
let mut block = BeaconBlock::Deneb(BeaconBlockDeneb::empty(spec));
let mut body = block.body_mut();
let blob_kzg_commitments = body.blob_kzg_commitments_mut().unwrap();
*blob_kzg_commitments =
@@ -27,13 +26,9 @@ fn create_test_block_and_blobs<E: EthSpec>(
let blobs = (0..num_of_blobs)
.map(|_| Blob::<E>::default())
.collect::<Vec<_>>()
.try_into()
.unwrap();
let proofs = vec![KzgProof::empty(); num_of_blobs * E::number_of_columns()]
.try_into()
.unwrap();
.into();
(signed_block, blobs, proofs)
(signed_block, blobs)
}
fn all_benches(c: &mut Criterion) {
@@ -42,32 +37,23 @@ fn all_benches(c: &mut Criterion) {
let kzg = get_kzg(&spec);
for blob_count in [1, 2, 3, 6] {
let (signed_block, blobs, proofs) = create_test_block_and_blobs::<E>(blob_count, &spec);
let (signed_block, blobs) = create_test_block_and_blobs::<E>(blob_count, &spec);
let column_sidecars = blobs_to_data_column_sidecars(
&blobs.iter().collect::<Vec<_>>(),
proofs.to_vec(),
&signed_block,
&kzg,
&spec,
)
.unwrap();
let kzg_commitments = signed_block
.message()
.body()
.blob_kzg_commitments()
.unwrap()
.clone();
let spec = spec.clone();
c.bench_function(&format!("reconstruct_{}", blob_count), |b| {
b.iter(|| {
black_box(reconstruct_data_columns(
&kzg,
column_sidecars.iter().as_slice()[0..column_sidecars.len() / 2].to_vec(),
&kzg_commitments,
&column_sidecars.iter().as_slice()[0..column_sidecars.len() / 2],
spec.as_ref(),
))
})

View File

@@ -1,21 +1,22 @@
use crate::{BeaconChain, BeaconChainError, BeaconChainTypes};
use eth2::types::{
IdealAttestationRewards, StandardAttestationRewards, TotalAttestationRewards, ValidatorId,
};
use eth2::lighthouse::attestation_rewards::{IdealAttestationRewards, TotalAttestationRewards};
use eth2::lighthouse::StandardAttestationRewards;
use eth2::types::ValidatorId;
use safe_arith::SafeArith;
use serde_utils::quoted_u64::Quoted;
use slog::debug;
use state_processing::common::base::{self, SqrtTotalActiveBalance};
use state_processing::per_epoch_processing::altair::{
process_inactivity_updates_slow, process_justification_and_finalization,
};
use state_processing::per_epoch_processing::base::rewards_and_penalties::{
ProposerRewardCalculation, get_attestation_component_delta, get_attestation_deltas_all,
get_attestation_deltas_subset, get_inactivity_penalty_delta, get_inclusion_delay_delta,
get_attestation_component_delta, get_attestation_deltas_all, get_attestation_deltas_subset,
get_inactivity_penalty_delta, get_inclusion_delay_delta, ProposerRewardCalculation,
};
use state_processing::per_epoch_processing::base::validator_statuses::InclusionInfo;
use state_processing::per_epoch_processing::base::{
TotalBalances, ValidatorStatus, ValidatorStatuses,
process_justification_and_finalization as process_justification_and_finalization_base,
TotalBalances, ValidatorStatus, ValidatorStatuses,
};
use state_processing::{
common::altair::BaseRewardPerIncrement,
@@ -28,7 +29,6 @@ use store::consts::altair::{
PARTICIPATION_FLAG_WEIGHTS, TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX,
TIMELY_TARGET_FLAG_INDEX,
};
use tracing::debug;
use types::consts::altair::WEIGHT_DENOMINATOR;
use types::{BeaconState, Epoch, EthSpec, RelativeEpoch};
@@ -38,11 +38,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
epoch: Epoch,
validators: Vec<ValidatorId>,
) -> Result<StandardAttestationRewards, BeaconChainError> {
debug!(
%epoch,
validator_count = validators.len(),
"computing attestation rewards"
);
debug!(self.log, "computing attestation rewards"; "epoch" => epoch, "validator_count" => validators.len());
// Get state
let state_slot = (epoch + 1).end_slot(T::EthSpec::slots_per_epoch());
@@ -51,10 +47,8 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.state_root_at_slot(state_slot)?
.ok_or(BeaconChainError::NoStateForSlot(state_slot))?;
// This branch is reached from the HTTP API. We assume the user wants
// to cache states so that future calls are faster.
let state = self
.get_state(&state_root, Some(state_slot), true)?
.get_state(&state_root, Some(state_slot))?
.ok_or(BeaconChainError::MissingBeaconState(state_root))?;
if state.fork_name_unchecked().altair_enabled() {
@@ -220,9 +214,10 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
// Return 0s for unknown/inactive validator indices.
let Ok(validator) = state.get_validator(validator_index) else {
debug!(
index = validator_index,
epoch = %previous_epoch,
"No rewards for inactive/unknown validator"
self.log,
"No rewards for inactive/unknown validator";
"index" => validator_index,
"epoch" => previous_epoch
);
total_rewards.push(TotalAttestationRewards {
validator_index: validator_index as u64,
@@ -320,7 +315,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
)
.into_values()
.collect::<Vec<IdealAttestationRewards>>();
ideal_rewards.sort_by_key(|a| a.effective_balance);
ideal_rewards.sort_by(|a, b| a.effective_balance.cmp(&b.effective_balance));
Ok(StandardAttestationRewards {
ideal_rewards,

View File

@@ -1,9 +1,9 @@
use crate::{BeaconChain, BeaconChainTypes};
use slog::{debug, error};
use slot_clock::SlotClock;
use std::sync::Arc;
use task_executor::TaskExecutor;
use tokio::time::sleep;
use tracing::{debug, error};
use types::{EthSpec, Slot};
/// Don't run the attestation simulator if the head slot is this many epochs
@@ -36,7 +36,10 @@ async fn attestation_simulator_service<T: BeaconChainTypes>(
Some(duration) => {
sleep(duration + additional_delay).await;
debug!("Simulating unagg. attestation production");
debug!(
chain.log,
"Simulating unagg. attestation production";
);
// Run the task in the executor
let inner_chain = chain.clone();
@@ -50,7 +53,7 @@ async fn attestation_simulator_service<T: BeaconChainTypes>(
);
}
None => {
error!("Failed to read slot clock");
error!(chain.log, "Failed to read slot clock");
// If we can't read the slot clock, just wait another slot.
sleep(slot_duration).await;
}
@@ -82,9 +85,10 @@ pub fn produce_unaggregated_attestation<T: BeaconChainTypes>(
let data = unaggregated_attestation.data();
debug!(
attestation_source = data.source.root.to_string(),
attestation_target = data.target.root.to_string(),
"Produce unagg. attestation"
chain.log,
"Produce unagg. attestation";
"attestation_source" => data.source.root.to_string(),
"attestation_target" => data.target.root.to_string(),
);
chain
@@ -94,8 +98,9 @@ pub fn produce_unaggregated_attestation<T: BeaconChainTypes>(
}
Err(e) => {
debug!(
error = ?e,
"Failed to simulate attestation"
chain.log,
"Failed to simulate attestation";
"error" => ?e
);
}
}

View File

@@ -35,14 +35,15 @@
mod batch;
use crate::{
BeaconChain, BeaconChainError, BeaconChainTypes, metrics,
metrics,
observed_aggregates::{ObserveOutcome, ObservedAttestationKey},
observed_attesters::Error as ObservedAttestersError,
single_attestation::single_attestation_to_attestation,
BeaconChain, BeaconChainError, BeaconChainTypes,
};
use bls::verify_signature_sets;
use itertools::Itertools;
use proto_array::Block as ProtoBlock;
use slog::debug;
use slot_clock::SlotClock;
use state_processing::{
common::{
@@ -57,13 +58,11 @@ use state_processing::{
};
use std::borrow::Cow;
use strum::AsRefStr;
use tracing::{debug, error};
use tree_hash::TreeHash;
use types::{
Attestation, AttestationData, AttestationRef, BeaconCommittee,
BeaconStateError::NoCommitteeFound, ChainSpec, CommitteeIndex, Epoch, EthSpec, ForkName,
Hash256, IndexedAttestation, SelectionProof, SignedAggregateAndProof, SingleAttestation, Slot,
SubnetId,
BeaconStateError::NoCommitteeFound, ChainSpec, CommitteeIndex, Epoch, EthSpec, Hash256,
IndexedAttestation, SelectionProof, SignedAggregateAndProof, SingleAttestation, Slot, SubnetId,
};
pub use batch::{batch_verify_aggregated_attestations, batch_verify_unaggregated_attestations};
@@ -161,12 +160,6 @@ 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
@@ -209,6 +202,12 @@ pub enum Error {
///
/// The peer has sent an invalid message.
NoCommitteeForSlotAndIndex { slot: Slot, index: CommitteeIndex },
/// The unaggregated attestation doesn't have only one aggregation bit set.
///
/// ## Peer scoring
///
/// The peer has sent an invalid message.
NotExactlyOneAggregationBitSet(usize),
/// The attestation doesn't have only one aggregation bit set.
///
/// ## Peer scoring
@@ -273,26 +272,12 @@ pub enum Error {
///
/// We were unable to process this attestation due to an internal error. It's unclear if the
/// attestation is valid.
BeaconChainError(Box<BeaconChainError>),
/// A critical error occurred while converting SSZ types.
/// This can only occur when a VariableList was not able to be constructed from a single
/// attestation.
///
/// ## Peer scoring
///
/// The peer has sent an invalid message.
SszTypesError(ssz_types::Error),
BeaconChainError(BeaconChainError),
}
impl From<BeaconChainError> for Error {
fn from(e: BeaconChainError) -> Self {
Self::BeaconChainError(Box::new(e))
}
}
impl From<ssz_types::Error> for Error {
fn from(e: ssz_types::Error) -> Self {
Self::SszTypesError(e)
Self::BeaconChainError(e)
}
}
@@ -319,9 +304,9 @@ struct IndexedAggregatedAttestation<'a, T: BeaconChainTypes> {
///
/// These attestations have *not* undergone signature verification.
struct IndexedUnaggregatedAttestation<'a, T: BeaconChainTypes> {
attestation: &'a SingleAttestation,
attestation: AttestationRef<'a, T::EthSpec>,
indexed_attestation: IndexedAttestation<T::EthSpec>,
subnet_id: Option<SubnetId>,
subnet_id: SubnetId,
validator_index: u64,
}
@@ -338,13 +323,12 @@ impl<T: BeaconChainTypes> VerifiedAggregatedAttestation<'_, T> {
}
}
#[derive(Clone)]
/// Wraps an `Attestation` that has been fully verified for propagation on the gossip network.
pub struct VerifiedUnaggregatedAttestation<'a, T: BeaconChainTypes> {
attestation: Attestation<T::EthSpec>,
single_attestation: &'a SingleAttestation,
attestation: AttestationRef<'a, T::EthSpec>,
indexed_attestation: IndexedAttestation<T::EthSpec>,
subnet_id: SubnetId,
validator_index: usize,
}
impl<T: BeaconChainTypes> VerifiedUnaggregatedAttestation<'_, T> {
@@ -352,8 +336,13 @@ impl<T: BeaconChainTypes> VerifiedUnaggregatedAttestation<'_, T> {
self.indexed_attestation
}
pub fn single_attestation(&self) -> SingleAttestation {
self.single_attestation.clone()
pub fn single_attestation(&self) -> Option<SingleAttestation> {
Some(SingleAttestation {
committee_index: self.attestation.committee_index()?,
attester_index: self.validator_index as u64,
data: self.attestation.data().clone(),
signature: self.attestation.signature().clone(),
})
}
}
@@ -373,7 +362,7 @@ impl<T: BeaconChainTypes> Clone for IndexedUnaggregatedAttestation<'_, T> {
/// A helper trait implemented on wrapper types that can be progressed to a state where they can be
/// verified for application to fork choice.
pub trait VerifiedAttestation<T: BeaconChainTypes>: Sized {
fn attestation(&self) -> AttestationRef<'_, T::EthSpec>;
fn attestation(&self) -> AttestationRef<T::EthSpec>;
fn indexed_attestation(&self) -> &IndexedAttestation<T::EthSpec>;
@@ -386,7 +375,7 @@ pub trait VerifiedAttestation<T: BeaconChainTypes>: Sized {
}
impl<T: BeaconChainTypes> VerifiedAttestation<T> for VerifiedAggregatedAttestation<'_, T> {
fn attestation(&self) -> AttestationRef<'_, T::EthSpec> {
fn attestation(&self) -> AttestationRef<T::EthSpec> {
self.attestation()
}
@@ -396,8 +385,8 @@ impl<T: BeaconChainTypes> VerifiedAttestation<T> for VerifiedAggregatedAttestati
}
impl<T: BeaconChainTypes> VerifiedAttestation<T> for VerifiedUnaggregatedAttestation<'_, T> {
fn attestation(&self) -> AttestationRef<'_, T::EthSpec> {
self.attestation.to_ref()
fn attestation(&self) -> AttestationRef<T::EthSpec> {
self.attestation
}
fn indexed_attestation(&self) -> &IndexedAttestation<T::EthSpec> {
@@ -411,8 +400,6 @@ pub enum AttestationSlashInfo<'a, T: BeaconChainTypes, TErr> {
SignatureNotChecked(AttestationRef<'a, T::EthSpec>, TErr),
/// As for `SignatureNotChecked`, but we know the `IndexedAttestation`.
SignatureNotCheckedIndexed(IndexedAttestation<T::EthSpec>, TErr),
/// As for `SignatureNotChecked`, but for the `SingleAttestation`.
SignatureNotCheckedSingle(&'a SingleAttestation, TErr),
/// The attestation's signature is invalid, so it will never be slashable.
SignatureInvalid(TErr),
/// The signature is valid but the attestation is invalid in some other way.
@@ -434,61 +421,38 @@ fn process_slash_info<T: BeaconChainTypes>(
if let Some(slasher) = chain.slasher.as_ref() {
let (indexed_attestation, check_signature, err) = match slash_info {
SignatureNotChecked(attestation, err) => {
if let Error::UnknownHeadBlock { .. } = err
&& attestation.data().beacon_block_root == attestation.data().target.root
{
return err;
if let Error::UnknownHeadBlock { .. } = err {
if attestation.data().beacon_block_root == attestation.data().target.root {
return err;
}
}
match obtain_indexed_attestation_and_committees_per_slot(chain, attestation) {
Ok((indexed, _)) => (indexed, true, err),
Err(e) => {
debug!(
attestation_root = ?attestation.tree_hash_root(),
error = ?e,
"Unable to obtain indexed form of attestation for slasher"
chain.log,
"Unable to obtain indexed form of attestation for slasher";
"attestation_root" => format!("{:?}", attestation.tree_hash_root()),
"error" => format!("{:?}", e)
);
return err;
}
}
}
SignatureNotCheckedSingle(attestation, err) => {
if let Error::UnknownHeadBlock { .. } = err
&& attestation.data.beacon_block_root == attestation.data.target.root
{
return err;
}
let fork_name = chain
.spec
.fork_name_at_slot::<T::EthSpec>(attestation.data.slot);
let indexed_attestation = match attestation.to_indexed(fork_name) {
Ok(indexed) => indexed,
Err(e) => {
error!(
attestation_root = ?attestation.data.tree_hash_root(),
error = ?e,
"Unable to construct VariableList from a single attestation. \
This indicates a serious bug in SSZ handling"
);
return Error::SszTypesError(e);
}
};
(indexed_attestation, true, err)
}
SignatureNotCheckedIndexed(indexed, err) => (indexed, true, err),
SignatureInvalid(e) => return e,
SignatureValid(indexed, err) => (indexed, false, err),
};
if check_signature && let Err(e) = verify_attestation_signature(chain, &indexed_attestation)
{
debug!(
error = ?e,
"Signature verification for slasher failed"
);
return err;
if check_signature {
if let Err(e) = verify_attestation_signature(chain, &indexed_attestation) {
debug!(
chain.log,
"Signature verification for slasher failed";
"error" => format!("{:?}", e),
);
return err;
}
}
// Supply to slasher.
@@ -499,7 +463,6 @@ fn process_slash_info<T: BeaconChainTypes>(
match slash_info {
SignatureNotChecked(_, e)
| SignatureNotCheckedIndexed(_, e)
| SignatureNotCheckedSingle(_, e)
| SignatureInvalid(e)
| SignatureValid(_, e) => e,
}
@@ -514,6 +477,11 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> {
chain: &BeaconChain<T>,
) -> Result<Self, Error> {
Self::verify_slashable(signed_aggregate, chain)
.inspect(|verified_aggregate| {
if let Some(slasher) = chain.slasher.as_ref() {
slasher.accept_attestation(verified_aggregate.indexed_attestation.clone());
}
})
.map_err(|slash_info| process_slash_info(slash_info, chain))
}
@@ -552,18 +520,14 @@ 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, fork_name)?;
verify_committee_index(attestation)?;
if chain
.observed_attestations
.write()
.is_known_subset(attestation, observed_attestation_key_root)
.map_err(|e| Error::BeaconChainError(Box::new(e.into())))?
.map_err(|e| Error::BeaconChainError(e.into()))?
{
metrics::inc_counter(&metrics::AGGREGATED_ATTESTATION_SUBSETS);
return Err(Error::AttestationSupersetKnown(
@@ -599,18 +563,7 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> {
//
// Attestations must be for a known block. If the block is unknown, we simply drop the
// 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,
));
}
let head_block = verify_head_block_is_known(chain, attestation, None)?;
// Check the attestation target root is consistent with the head root.
//
@@ -619,7 +572,7 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> {
//
// Whilst this attestation *technically* could be used to add value to a block, it is
// invalid in the spirit of the protocol. Here we choose safety over profit.
verify_attestation_target_root::<T::EthSpec>(&head_block, attestation.data())?;
verify_attestation_target_root::<T::EthSpec>(&head_block, attestation)?;
// Ensure that the attestation has participants.
if attestation.is_aggregation_bits_zero() {
@@ -642,7 +595,7 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> {
return Err(SignatureNotChecked(
signed_aggregate.message().aggregate(),
e,
));
))
}
};
@@ -677,7 +630,7 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> {
if !SelectionProof::from(selection_proof)
.is_aggregator(committee.committee.len(), &chain.spec)
.map_err(|e| Error::BeaconChainError(Box::new(e.into())))?
.map_err(|e| Error::BeaconChainError(e.into()))?
{
return Err(Error::InvalidSelectionProof { aggregator_index });
}
@@ -718,7 +671,7 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> {
return Err(SignatureNotChecked(
signed_aggregate.message().aggregate(),
e,
));
))
}
};
Ok(IndexedAggregatedAttestation {
@@ -747,7 +700,7 @@ impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> {
.observed_attestations
.write()
.observe_item(attestation, Some(observed_attestation_key_root))
.map_err(|e| Error::BeaconChainError(Box::new(e.into())))?
.map_err(|e| Error::BeaconChainError(e.into()))?
{
metrics::inc_counter(&metrics::AGGREGATED_ATTESTATION_SUBSETS);
return Err(Error::AttestationSupersetKnown(
@@ -862,16 +815,16 @@ impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> {
impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> {
/// Run the checks that happen before an indexed attestation is constructed.
pub fn verify_early_checks(
attestation: &'a SingleAttestation,
attestation: AttestationRef<T::EthSpec>,
chain: &BeaconChain<T>,
) -> Result<(), Error> {
let attestation_epoch = attestation.data.slot.epoch(T::EthSpec::slots_per_epoch());
let attestation_epoch = attestation.data().slot.epoch(T::EthSpec::slots_per_epoch());
// Check the attestation's epoch matches its target.
if attestation_epoch != attestation.data.target.epoch {
if attestation_epoch != attestation.data().target.epoch {
return Err(Error::InvalidTargetEpoch {
slot: attestation.data.slot,
epoch: attestation.data.target.epoch,
slot: attestation.data().slot,
epoch: attestation.data().target.epoch,
});
}
@@ -881,60 +834,61 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> {
// We do not queue future attestations for later processing.
verify_propagation_slot_range::<_, T::EthSpec>(
&chain.slot_clock,
&attestation.data,
attestation.data(),
&chain.spec,
)?;
let fork_name = chain
.spec
.fork_name_at_slot::<T::EthSpec>(attestation.data.slot);
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(
attestation.data.index as usize,
));
}
// Check to ensure that the attestation is "unaggregated". I.e., it has exactly one
// aggregation bit set.
let num_aggregation_bits = attestation.num_set_aggregation_bits();
if num_aggregation_bits != 1 {
return Err(Error::NotExactlyOneAggregationBitSet(num_aggregation_bits));
}
// [New in Electra:EIP7549]
verify_committee_index(attestation)?;
// Attestations must be for a known block. If the block is unknown, we simply drop the
// attestation and do not delay consideration for later.
//
// Enforce a maximum skip distance for unaggregated attestations.
let head_block = verify_head_block_is_known(
chain,
&attestation.data,
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,
));
}
let head_block =
verify_head_block_is_known(chain, attestation, chain.config.import_max_skip_slots)?;
// Check the attestation target root is consistent with the head root.
verify_attestation_target_root::<T::EthSpec>(&head_block, &attestation.data)?;
verify_attestation_target_root::<T::EthSpec>(&head_block, attestation)?;
Ok(())
}
/// Run the checks that apply to the indexed attestation before the signature is checked.
pub fn verify_middle_checks(
attestation: &'a SingleAttestation,
attestation: AttestationRef<T::EthSpec>,
indexed_attestation: &IndexedAttestation<T::EthSpec>,
committees_per_slot: u64,
subnet_id: Option<SubnetId>,
chain: &BeaconChain<T>,
) -> Result<u64, Error> {
let validator_index = attestation.attester_index;
) -> Result<(u64, SubnetId), Error> {
let expected_subnet_id = SubnetId::compute_subnet_for_attestation::<T::EthSpec>(
attestation,
committees_per_slot,
&chain.spec,
)
.map_err(BeaconChainError::from)?;
// If a subnet was specified, ensure that subnet is correct.
if let Some(subnet_id) = subnet_id {
if subnet_id != expected_subnet_id {
return Err(Error::InvalidSubnetId {
received: subnet_id,
expected: expected_subnet_id,
});
}
};
let validator_index = *indexed_attestation
.attesting_indices_first()
.ok_or(Error::NotExactlyOneAggregationBitSet(0))?;
/*
* The attestation is the first valid attestation received for the participating validator
@@ -943,16 +897,16 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> {
if chain
.observed_gossip_attesters
.read()
.validator_has_been_observed(attestation.data.target.epoch, validator_index as usize)
.validator_has_been_observed(attestation.data().target.epoch, validator_index as usize)
.map_err(BeaconChainError::from)?
{
return Err(Error::PriorAttestationKnown {
validator_index,
epoch: attestation.data.target.epoch,
epoch: attestation.data().target.epoch,
});
}
Ok(validator_index)
Ok((validator_index, expected_subnet_id))
}
/// Returns `Ok(Self)` if the `attestation` is valid to be (re)published on the gossip
@@ -961,35 +915,46 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> {
/// `subnet_id` is the subnet from which we received this attestation. This function will
/// verify that it was received on the correct subnet.
pub fn verify(
attestation: &'a SingleAttestation,
attestation: &'a Attestation<T::EthSpec>,
subnet_id: Option<SubnetId>,
chain: &BeaconChain<T>,
) -> Result<Self, Error> {
Self::verify_slashable(attestation, subnet_id, chain)
Self::verify_slashable(attestation.to_ref(), subnet_id, chain)
.inspect(|verified_unaggregated| {
if let Some(slasher) = chain.slasher.as_ref() {
slasher.accept_attestation(verified_unaggregated.indexed_attestation.clone());
}
})
.map_err(|slash_info| process_slash_info(slash_info, chain))
}
/// Verify the attestation, producing extra information about whether it might be slashable.
pub fn verify_slashable(
attestation: &'a SingleAttestation,
attestation: AttestationRef<'a, T::EthSpec>,
subnet_id: Option<SubnetId>,
chain: &BeaconChain<T>,
) -> Result<Self, AttestationSlashInfo<'a, T, Error>> {
use AttestationSlashInfo::*;
if let Err(e) = Self::verify_early_checks(attestation, chain) {
return Err(SignatureNotCheckedSingle(attestation, e));
return Err(SignatureNotChecked(attestation, e));
}
let fork_name = chain
.spec
.fork_name_at_slot::<T::EthSpec>(attestation.data.slot);
let (indexed_attestation, committees_per_slot) =
match obtain_indexed_attestation_and_committees_per_slot(chain, attestation) {
Ok(x) => x,
Err(e) => {
return Err(SignatureNotChecked(attestation, e));
}
};
let indexed_attestation = attestation
.to_indexed(fork_name)
.map_err(|e| SignatureNotCheckedSingle(attestation, Error::SszTypesError(e)))?;
let validator_index = match Self::verify_middle_checks(attestation, chain) {
let (validator_index, expected_subnet_id) = match Self::verify_middle_checks(
attestation,
&indexed_attestation,
committees_per_slot,
subnet_id,
chain,
) {
Ok(t) => t,
Err(e) => return Err(SignatureNotCheckedIndexed(indexed_attestation, e)),
};
@@ -997,7 +962,7 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> {
Ok(Self {
attestation,
indexed_attestation,
subnet_id,
subnet_id: expected_subnet_id,
validator_index,
})
}
@@ -1014,56 +979,10 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> {
impl<'a, T: BeaconChainTypes> VerifiedUnaggregatedAttestation<'a, T> {
/// Run the checks that apply after the signature has been checked.
fn verify_late_checks(
attestation: &'a SingleAttestation,
attestation: AttestationRef<T::EthSpec>,
validator_index: u64,
subnet_id: Option<SubnetId>,
chain: &BeaconChain<T>,
) -> Result<(Attestation<T::EthSpec>, SubnetId), Error> {
// Check that the attester is a member of the committee
let (committee_opt, committees_per_slot) = chain.with_committee_cache(
attestation.data.target.root,
attestation.data.slot.epoch(T::EthSpec::slots_per_epoch()),
|cached_shuffling, _| {
let committee_cache = cached_shuffling.committee_cache.as_ref();
let committee_opt = committee_cache
.get_beacon_committee(attestation.data.slot, attestation.committee_index)
.map(|beacon_committee| beacon_committee.committee.to_vec());
Ok((committee_opt, committee_cache.committees_per_slot()))
},
)?;
let Some(committee) = committee_opt else {
return Err(Error::NoCommitteeForSlotAndIndex {
slot: attestation.data.slot,
index: attestation.committee_index,
});
};
if !committee.contains(&(attestation.attester_index as usize)) {
return Err(Error::AttesterNotInCommittee {
attester_index: attestation.attester_index,
committee_index: attestation.committee_index,
slot: attestation.data.slot,
});
}
let expected_subnet_id = SubnetId::compute_subnet_for_single_attestation::<T::EthSpec>(
attestation,
committees_per_slot,
&chain.spec,
)
.map_err(BeaconChainError::from)?;
// If a subnet was specified, ensure that subnet is correct.
if let Some(subnet_id) = subnet_id
&& subnet_id != expected_subnet_id
{
return Err(Error::InvalidSubnetId {
received: subnet_id,
expected: expected_subnet_id,
});
};
) -> Result<(), Error> {
// Now that the attestation has been fully verified, store that we have received a valid
// attestation from this validator.
//
@@ -1073,28 +992,20 @@ impl<'a, T: BeaconChainTypes> VerifiedUnaggregatedAttestation<'a, T> {
if chain
.observed_gossip_attesters
.write()
.observe_validator(attestation.data.target.epoch, validator_index as usize)
.observe_validator(attestation.data().target.epoch, validator_index as usize)
.map_err(BeaconChainError::from)?
{
return Err(Error::PriorAttestationKnown {
validator_index,
epoch: attestation.data.target.epoch,
epoch: attestation.data().target.epoch,
});
}
let fork_name = chain
.spec
.fork_name_at_slot::<T::EthSpec>(attestation.data.slot);
let unaggregated_attestation =
single_attestation_to_attestation(attestation, &committee, fork_name)?;
Ok((unaggregated_attestation, expected_subnet_id))
Ok(())
}
/// Verify the `unaggregated_attestation`.
pub fn verify(
unaggregated_attestation: &'a SingleAttestation,
unaggregated_attestation: &'a Attestation<T::EthSpec>,
subnet_id: Option<SubnetId>,
chain: &BeaconChain<T>,
) -> Result<Self, Error> {
@@ -1145,17 +1056,15 @@ impl<'a, T: BeaconChainTypes> VerifiedUnaggregatedAttestation<'a, T> {
CheckAttestationSignature::No => (),
};
let (unaggregated_attestation, subnet_id) =
match Self::verify_late_checks(attestation, validator_index, subnet_id, chain) {
Ok(a) => a,
Err(e) => return Err(SignatureValid(indexed_attestation, e)),
};
if let Err(e) = Self::verify_late_checks(attestation, validator_index, chain) {
return Err(SignatureValid(indexed_attestation, e));
}
Ok(Self {
single_attestation: attestation,
attestation: unaggregated_attestation,
attestation,
indexed_attestation,
subnet_id,
validator_index: validator_index as usize,
})
}
@@ -1164,6 +1073,11 @@ impl<'a, T: BeaconChainTypes> VerifiedUnaggregatedAttestation<'a, T> {
self.subnet_id
}
/// Returns the wrapped `attestation`.
pub fn attestation(&self) -> AttestationRef<T::EthSpec> {
self.attestation
}
/// Returns the wrapped `indexed_attestation`.
pub fn indexed_attestation(&self) -> &IndexedAttestation<T::EthSpec> {
&self.indexed_attestation
@@ -1190,40 +1104,34 @@ impl<'a, T: BeaconChainTypes> VerifiedUnaggregatedAttestation<'a, T> {
/// already finalized.
fn verify_head_block_is_known<T: BeaconChainTypes>(
chain: &BeaconChain<T>,
attestation_data: &AttestationData,
attestation: AttestationRef<T::EthSpec>,
max_skip_slots: Option<u64>,
) -> Result<ProtoBlock, Error> {
let block_opt = chain
.canonical_head
.fork_choice_read_lock()
.get_block(&attestation_data.beacon_block_root)
.get_block(&attestation.data().beacon_block_root)
.or_else(|| {
chain
.early_attester_cache
.get_proto_block(attestation_data.beacon_block_root)
.get_proto_block(attestation.data().beacon_block_root)
});
if let Some(block) = block_opt {
// Reject any block that exceeds our limit on skipped slots.
if let Some(max_skip_slots) = max_skip_slots
&& attestation_data.slot > block.slot + max_skip_slots
{
return Err(Error::TooManySkippedSlots {
head_block_slot: block.slot,
attestation_slot: attestation_data.slot,
});
}
if !verify_attestation_is_finalized_checkpoint_or_descendant(attestation_data, chain) {
return Err(Error::HeadBlockFinalized {
beacon_block_root: attestation_data.beacon_block_root,
});
if let Some(max_skip_slots) = max_skip_slots {
if attestation.data().slot > block.slot + max_skip_slots {
return Err(Error::TooManySkippedSlots {
head_block_slot: block.slot,
attestation_slot: attestation.data().slot,
});
}
}
Ok(block)
} else if chain.is_pre_finalization_block(attestation_data.beacon_block_root)? {
} else if chain.is_pre_finalization_block(attestation.data().beacon_block_root)? {
Err(Error::HeadBlockFinalized {
beacon_block_root: attestation_data.beacon_block_root,
beacon_block_root: attestation.data().beacon_block_root,
})
} else {
// The block is either:
@@ -1233,7 +1141,7 @@ fn verify_head_block_is_known<T: BeaconChainTypes>(
// 2) A post-finalization block that we don't know about yet. We'll queue
// the attestation until the block becomes available (or we time out).
Err(Error::UnknownHeadBlock {
beacon_block_root: attestation_data.beacon_block_root,
beacon_block_root: attestation.data().beacon_block_root,
})
}
}
@@ -1325,11 +1233,11 @@ pub fn verify_attestation_signature<T: BeaconChainTypes>(
/// `attestation.data.beacon_block_root`.
pub fn verify_attestation_target_root<E: EthSpec>(
head_block: &ProtoBlock,
attestation_data: &AttestationData,
attestation: AttestationRef<E>,
) -> Result<(), Error> {
// Check the attestation target root.
let head_block_epoch = head_block.slot.epoch(E::slots_per_epoch());
let attestation_epoch = attestation_data.slot.epoch(E::slots_per_epoch());
let attestation_epoch = attestation.data().slot.epoch(E::slots_per_epoch());
if head_block_epoch > attestation_epoch {
// The epoch references an invalid head block from a future epoch.
//
@@ -1342,7 +1250,7 @@ pub fn verify_attestation_target_root<E: EthSpec>(
// Reference:
// https://github.com/ethereum/eth2.0-specs/pull/2001#issuecomment-699246659
return Err(Error::InvalidTargetRoot {
attestation: attestation_data.target.root,
attestation: attestation.data().target.root,
// It is not clear what root we should expect in this case, since the attestation is
// fundamentally invalid.
expected: None,
@@ -1361,9 +1269,9 @@ pub fn verify_attestation_target_root<E: EthSpec>(
};
// Reject any attestation with an invalid target root.
if target_root != attestation_data.target.root {
if target_root != attestation.data().target.root {
return Err(Error::InvalidTargetRoot {
attestation: attestation_data.target.root,
attestation: attestation.data().target.root,
expected: Some(target_root),
});
}
@@ -1400,7 +1308,7 @@ pub fn verify_signed_aggregate_signatures<T: BeaconChainTypes>(
.spec
.fork_at_epoch(indexed_attestation.data().target.epoch);
let signature_sets = [
let signature_sets = vec![
signed_aggregate_selection_proof_signature_set(
|validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed),
signed_aggregate,
@@ -1433,10 +1341,7 @@ 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>,
fork_name: ForkName,
) -> Result<(), Error> {
pub fn verify_committee_index<E: EthSpec>(attestation: AttestationRef<E>) -> 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);
@@ -1446,46 +1351,16 @@ pub fn verify_committee_index<E: EthSpec>(
));
}
// 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));
}
// Ensure the attestation index is set to zero post Electra.
if attestation.data().index != 0 {
return Err(Error::CommitteeIndexNonZero(
attestation.data().index as usize,
));
}
}
Ok(())
}
fn verify_attestation_is_finalized_checkpoint_or_descendant<T: BeaconChainTypes>(
attestation_data: &AttestationData,
chain: &BeaconChain<T>,
) -> bool {
// If we have a split block newer than finalization then we also ban attestations which are not
// descended from that split block. It's important not to try checking `is_descendant` if
// finality is ahead of the split and the split block has been pruned, as `is_descendant` will
// return `false` in this case.
let fork_choice = chain.canonical_head.fork_choice_read_lock();
let attestation_block_root = attestation_data.beacon_block_root;
let finalized_slot = fork_choice
.finalized_checkpoint()
.epoch
.start_slot(T::EthSpec::slots_per_epoch());
let split = chain.store.get_split_info();
let is_descendant_from_split_block = split.slot == 0
|| split.slot <= finalized_slot
|| fork_choice.is_descendant(split.block_root, attestation_block_root);
fork_choice.is_finalized_checkpoint_or_descendant(attestation_block_root)
&& is_descendant_from_split_block
}
/// Assists in readability.
type CommitteesPerSlot = u64;
@@ -1575,8 +1450,7 @@ where
return Err(Error::UnknownTargetRoot(target.root));
}
chain.with_committee_cache(target.root, attestation_epoch, |cached_shuffling, _| {
let committee_cache = cached_shuffling.committee_cache.as_ref();
chain.with_committee_cache(target.root, attestation_epoch, |committee_cache, _| {
let committees_per_slot = committee_cache.committees_per_slot();
Ok(committee_cache

View File

@@ -13,7 +13,7 @@ use super::{
CheckAttestationSignature, Error, IndexedAggregatedAttestation, IndexedUnaggregatedAttestation,
VerifiedAggregatedAttestation, VerifiedUnaggregatedAttestation,
};
use crate::{BeaconChain, BeaconChainError, BeaconChainTypes, metrics};
use crate::{metrics, BeaconChain, BeaconChainError, BeaconChainTypes};
use bls::verify_signature_sets;
use state_processing::signature_sets::{
indexed_attestation_signature_set_from_pubkeys, signed_aggregate_selection_proof_signature_set,
@@ -136,7 +136,7 @@ pub fn batch_verify_unaggregated_attestations<'a, T, I>(
) -> Result<Vec<Result<VerifiedUnaggregatedAttestation<'a, T>, Error>>, Error>
where
T: BeaconChainTypes,
I: Iterator<Item = (&'a SingleAttestation, Option<SubnetId>)> + ExactSizeIterator,
I: Iterator<Item = (&'a Attestation<T::EthSpec>, Option<SubnetId>)> + ExactSizeIterator,
{
let mut num_partially_verified = 0;
let mut num_failed = 0;

View File

@@ -0,0 +1,386 @@
//! This module provides the `AttesterCache`, a cache designed for reducing state-reads when
//! validators produce `AttestationData`.
//!
//! This cache is required *as well as* the `ShufflingCache` since the `ShufflingCache` does not
//! provide any information about the `state.current_justified_checkpoint`. It is not trivial to add
//! the justified checkpoint to the `ShufflingCache` since that cache is keyed by shuffling decision
//! root, which is not suitable for the justified checkpoint. Whilst we can know the shuffling for
//! epoch `n` during `n - 1`, we *cannot* know the justified checkpoint. Instead, we *must* perform
//! `per_epoch_processing` to transform the state from epoch `n - 1` to epoch `n` so that rewards
//! and penalties can be computed and the `state.current_justified_checkpoint` can be updated.
use crate::{BeaconChain, BeaconChainError, BeaconChainTypes};
use parking_lot::RwLock;
use state_processing::state_advance::{partial_state_advance, Error as StateAdvanceError};
use std::collections::HashMap;
use std::ops::Range;
use types::{
attestation::Error as AttestationError,
beacon_state::{
compute_committee_index_in_epoch, compute_committee_range_in_epoch, epoch_committee_count,
},
BeaconState, BeaconStateError, ChainSpec, Checkpoint, Epoch, EthSpec, FixedBytesExtended,
Hash256, RelativeEpoch, Slot,
};
type JustifiedCheckpoint = Checkpoint;
type CommitteeLength = usize;
type CommitteeIndex = u64;
type CacheHashMap = HashMap<AttesterCacheKey, AttesterCacheValue>;
/// The maximum number of `AttesterCacheValues` to be kept in memory.
///
/// Each `AttesterCacheValues` is very small (~16 bytes) and the cache will generally be kept small
/// by pruning on finality.
///
/// The value provided here is much larger than will be used during ideal network conditions,
/// however we make it large since the values are so small.
const MAX_CACHE_LEN: usize = 1_024;
#[derive(Debug)]
pub enum Error {
BeaconState(BeaconStateError),
// Boxed to avoid an infinite-size recursion issue.
BeaconChain(Box<BeaconChainError>),
MissingBeaconState(Hash256),
FailedToTransitionState(StateAdvanceError),
CannotAttestToFutureState {
state_slot: Slot,
request_slot: Slot,
},
/// Indicates a cache inconsistency.
WrongEpoch {
request_epoch: Epoch,
epoch: Epoch,
},
InvalidCommitteeIndex {
committee_index: u64,
},
/// Indicates an inconsistency with the beacon state committees.
InverseRange {
range: Range<usize>,
},
AttestationError(AttestationError),
}
impl From<BeaconStateError> for Error {
fn from(e: BeaconStateError) -> Self {
Error::BeaconState(e)
}
}
impl From<BeaconChainError> for Error {
fn from(e: BeaconChainError) -> Self {
Error::BeaconChain(Box::new(e))
}
}
/// Stores the minimal amount of data required to compute the committee length for any committee at any
/// slot in a given `epoch`.
pub struct CommitteeLengths {
/// The `epoch` to which the lengths pertain.
epoch: Epoch,
/// The length of the shuffling in `self.epoch`.
active_validator_indices_len: usize,
}
impl CommitteeLengths {
/// Instantiate `Self` using `state.current_epoch()`.
pub fn new<E: EthSpec>(state: &BeaconState<E>, spec: &ChainSpec) -> Result<Self, Error> {
let active_validator_indices_len = if let Ok(committee_cache) =
state.committee_cache(RelativeEpoch::Current)
{
committee_cache.active_validator_indices().len()
} else {
// Building the cache like this avoids taking a mutable reference to `BeaconState`.
let committee_cache = state.initialize_committee_cache(state.current_epoch(), spec)?;
committee_cache.active_validator_indices().len()
};
Ok(Self {
epoch: state.current_epoch(),
active_validator_indices_len,
})
}
/// Get the count of committees per each slot of `self.epoch`.
pub fn get_committee_count_per_slot<E: EthSpec>(
&self,
spec: &ChainSpec,
) -> Result<usize, Error> {
E::get_committee_count_per_slot(self.active_validator_indices_len, spec).map_err(Into::into)
}
/// Get the length of the committee at the given `slot` and `committee_index`.
pub fn get_committee_length<E: EthSpec>(
&self,
slot: Slot,
committee_index: CommitteeIndex,
spec: &ChainSpec,
) -> Result<CommitteeLength, Error> {
let slots_per_epoch = E::slots_per_epoch();
let request_epoch = slot.epoch(slots_per_epoch);
// Sanity check.
if request_epoch != self.epoch {
return Err(Error::WrongEpoch {
request_epoch,
epoch: self.epoch,
});
}
let slots_per_epoch = slots_per_epoch as usize;
let committees_per_slot = self.get_committee_count_per_slot::<E>(spec)?;
let index_in_epoch = compute_committee_index_in_epoch(
slot,
slots_per_epoch,
committees_per_slot,
committee_index as usize,
);
let range = compute_committee_range_in_epoch(
epoch_committee_count(committees_per_slot, slots_per_epoch),
index_in_epoch,
self.active_validator_indices_len,
)
.ok_or(Error::InvalidCommitteeIndex { committee_index })?;
range
.end
.checked_sub(range.start)
.ok_or(Error::InverseRange { range })
}
}
/// Provides the following information for some epoch:
///
/// - The `state.current_justified_checkpoint` value.
/// - The committee lengths for all indices and slots.
///
/// These values are used during attestation production.
pub struct AttesterCacheValue {
current_justified_checkpoint: Checkpoint,
committee_lengths: CommitteeLengths,
}
impl AttesterCacheValue {
/// Instantiate `Self` using `state.current_epoch()`.
pub fn new<E: EthSpec>(state: &BeaconState<E>, spec: &ChainSpec) -> Result<Self, Error> {
let current_justified_checkpoint = state.current_justified_checkpoint();
let committee_lengths = CommitteeLengths::new(state, spec)?;
Ok(Self {
current_justified_checkpoint,
committee_lengths,
})
}
/// Get the justified checkpoint and committee length for some `slot` and `committee_index`.
fn get<E: EthSpec>(
&self,
slot: Slot,
committee_index: CommitteeIndex,
spec: &ChainSpec,
) -> Result<(JustifiedCheckpoint, CommitteeLength), Error> {
self.committee_lengths
.get_committee_length::<E>(slot, committee_index, spec)
.map(|committee_length| (self.current_justified_checkpoint, committee_length))
}
}
/// The `AttesterCacheKey` is fundamentally the same thing as the proposer shuffling decision root,
/// however here we use it as an identity for both of the following values:
///
/// 1. The `state.current_justified_checkpoint`.
/// 2. The attester shuffling.
///
/// This struct relies upon the premise that the `state.current_justified_checkpoint` in epoch `n`
/// is determined by the root of the latest block in epoch `n - 1`. Notably, this is identical to
/// how the proposer shuffling is keyed in `BeaconProposerCache`.
///
/// It is also safe, but not maximally efficient, to key the attester shuffling with the same
/// strategy. For better shuffling keying strategies, see the `ShufflingCache`.
#[derive(Eq, PartialEq, Hash, Clone, Copy)]
pub struct AttesterCacheKey {
/// The epoch from which the justified checkpoint should be observed.
///
/// Attestations which use `self.epoch` as `target.epoch` should use this key.
epoch: Epoch,
/// The root of the block at the last slot of `self.epoch - 1`.
decision_root: Hash256,
}
impl AttesterCacheKey {
/// Instantiate `Self` to key `state.current_epoch()`.
///
/// The `latest_block_root` should be the latest block that has been applied to `state`. This
/// parameter is required since the state does not store the block root for any block with the
/// same slot as `state.slot()`.
///
/// ## Errors
///
/// May error if `epoch` is out of the range of `state.block_roots`.
pub fn new<E: EthSpec>(
epoch: Epoch,
state: &BeaconState<E>,
latest_block_root: Hash256,
) -> Result<Self, Error> {
let slots_per_epoch = E::slots_per_epoch();
let decision_slot = epoch.start_slot(slots_per_epoch).saturating_sub(1_u64);
let decision_root = if decision_slot.epoch(slots_per_epoch) == epoch {
// This scenario is only possible during the genesis epoch. In this scenario, all-zeros
// is used as an alias to the genesis block.
Hash256::zero()
} else if epoch > state.current_epoch() {
// If the requested epoch is higher than the current epoch, the latest block will always
// be the decision root.
latest_block_root
} else {
*state.get_block_root(decision_slot)?
};
Ok(Self {
epoch,
decision_root,
})
}
}
/// Provides a cache for the justified checkpoint and committee length when producing an
/// attestation.
///
/// See the module-level documentation for more information.
#[derive(Default)]
pub struct AttesterCache {
cache: RwLock<CacheHashMap>,
}
impl AttesterCache {
/// Get the justified checkpoint and committee length for the `slot` and `committee_index` in
/// the state identified by the cache `key`.
pub fn get<E: EthSpec>(
&self,
key: &AttesterCacheKey,
slot: Slot,
committee_index: CommitteeIndex,
spec: &ChainSpec,
) -> Result<Option<(JustifiedCheckpoint, CommitteeLength)>, Error> {
self.cache
.read()
.get(key)
.map(|cache_item| cache_item.get::<E>(slot, committee_index, spec))
.transpose()
}
/// Cache the `state.current_epoch()` values if they are not already present in the state.
pub fn maybe_cache_state<E: EthSpec>(
&self,
state: &BeaconState<E>,
latest_block_root: Hash256,
spec: &ChainSpec,
) -> Result<(), Error> {
let key = AttesterCacheKey::new(state.current_epoch(), state, latest_block_root)?;
let mut cache = self.cache.write();
if !cache.contains_key(&key) {
let cache_item = AttesterCacheValue::new(state, spec)?;
Self::insert_respecting_max_len(&mut cache, key, cache_item);
}
Ok(())
}
/// Read the state identified by `state_root` from the database, advance it to the required
/// slot, use it to prime the cache and return the values for the provided `slot` and
/// `committee_index`.
///
/// ## Notes
///
/// This function takes a write-lock on the internal cache. Prefer attempting a `Self::get` call
/// before running this function as `Self::get` only takes a read-lock and is therefore less
/// likely to create contention.
pub fn load_and_cache_state<T: BeaconChainTypes>(
&self,
state_root: Hash256,
key: AttesterCacheKey,
slot: Slot,
committee_index: CommitteeIndex,
chain: &BeaconChain<T>,
) -> Result<(JustifiedCheckpoint, CommitteeLength), Error> {
let spec = &chain.spec;
let slots_per_epoch = T::EthSpec::slots_per_epoch();
let epoch = slot.epoch(slots_per_epoch);
// Take a write-lock on the cache before starting the state read.
//
// Whilst holding the write-lock during the state read will create contention, it prevents
// the scenario where multiple requests from separate threads cause duplicate state reads.
let mut cache = self.cache.write();
// Try the cache to see if someone has already primed it between the time the function was
// called and when the cache write-lock was obtained. This avoids performing duplicate state
// reads.
if let Some(value) = cache
.get(&key)
.map(|cache_item| cache_item.get::<T::EthSpec>(slot, committee_index, spec))
.transpose()?
{
return Ok(value);
}
let mut state: BeaconState<T::EthSpec> = chain
.get_state(&state_root, None)?
.ok_or(Error::MissingBeaconState(state_root))?;
if state.slot() > slot {
// This indicates an internal inconsistency.
return Err(Error::CannotAttestToFutureState {
state_slot: state.slot(),
request_slot: slot,
});
} else if state.current_epoch() < epoch {
// Only perform a "partial" state advance since we do not require the state roots to be
// accurate.
partial_state_advance(
&mut state,
Some(state_root),
epoch.start_slot(slots_per_epoch),
spec,
)
.map_err(Error::FailedToTransitionState)?;
state.build_committee_cache(RelativeEpoch::Current, spec)?;
}
let cache_item = AttesterCacheValue::new(&state, spec)?;
let value = cache_item.get::<T::EthSpec>(slot, committee_index, spec)?;
Self::insert_respecting_max_len(&mut cache, key, cache_item);
Ok(value)
}
/// Insert a value to `cache`, ensuring it does not exceed the maximum length.
///
/// If the cache is already full, the item with the lowest epoch will be removed.
fn insert_respecting_max_len(
cache: &mut CacheHashMap,
key: AttesterCacheKey,
value: AttesterCacheValue,
) {
while cache.len() >= MAX_CACHE_LEN {
if let Some(oldest) = cache
.iter()
.map(|(key, _)| *key)
.min_by_key(|key| key.epoch)
{
cache.remove(&oldest);
} else {
break;
}
}
cache.insert(key, value);
}
/// Remove all entries where the `key.epoch` is lower than the given `epoch`.
///
/// Generally, the provided `epoch` should be the finalized epoch.
pub fn prune_below(&self, epoch: Epoch) {
self.cache.write().retain(|target, _| target.epoch >= epoch);
}
}

View File

@@ -1,7 +1,8 @@
use crate::{BeaconChain, BeaconChainError, BeaconChainTypes, StateSkipConfig};
use attesting_indices_base::get_attesting_indices;
use eth2::types::StandardBlockReward;
use eth2::lighthouse::StandardBlockReward;
use safe_arith::SafeArith;
use slog::error;
use state_processing::common::attesting_indices_base;
use state_processing::{
common::{
@@ -15,10 +16,9 @@ use state_processing::{
};
use std::collections::HashSet;
use store::{
RelativeEpoch,
consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR},
RelativeEpoch,
};
use tracing::error;
use types::{AbstractExecPayload, BeaconBlockRef, BeaconState, BeaconStateError, EthSpec};
type BeaconBlockSubRewardValue = u64;
@@ -56,8 +56,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.compute_beacon_block_proposer_slashing_reward(block, state)
.map_err(|e| {
error!(
error = ?e,
"Error calculating proposer slashing reward"
self.log,
"Error calculating proposer slashing reward";
"error" => ?e
);
BeaconChainError::BlockRewardError
})?;
@@ -66,8 +67,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.compute_beacon_block_attester_slashing_reward(block, state)
.map_err(|e| {
error!(
error = ?e,
"Error calculating attester slashing reward"
self.log,
"Error calculating attester slashing reward";
"error" => ?e
);
BeaconChainError::BlockRewardError
})?;
@@ -76,8 +78,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
self.compute_beacon_block_attestation_reward_base(block, state)
.map_err(|e| {
error!(
error = ?e,
"Error calculating base block attestation reward"
self.log,
"Error calculating base block attestation reward";
"error" => ?e
);
BeaconChainError::BlockRewardAttestationError
})?
@@ -85,8 +88,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
self.compute_beacon_block_attestation_reward_altair_deneb(block, state)
.map_err(|e| {
error!(
error = ?e,
"Error calculating altair block attestation reward"
self.log,
"Error calculating altair block attestation reward";
"error" => ?e
);
BeaconChainError::BlockRewardAttestationError
})?
@@ -135,7 +139,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
state
.get_validator(proposer_slashing.proposer_index() as usize)?
.effective_balance
.safe_div(state.get_whistleblower_reward_quotient(&self.spec))?,
.safe_div(self.spec.whistleblower_reward_quotient)?,
)?;
}
@@ -157,7 +161,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
state
.get_validator(attester_index as usize)?
.effective_balance
.safe_div(state.get_whistleblower_reward_quotient(&self.spec))?,
.safe_div(self.spec.whistleblower_reward_quotient)?,
)?;
}
}

View File

@@ -1,22 +1,21 @@
use crate::{BeaconChain, BeaconChainError, BeaconChainTypes, BlockProcessStatus, metrics};
use crate::{metrics, BeaconChain, BeaconChainError, BeaconChainTypes, BlockProcessStatus};
use execution_layer::{ExecutionLayer, ExecutionPayloadBodyV1};
use logging::crit;
use slog::{crit, debug, error, Logger};
use std::collections::HashMap;
use std::sync::Arc;
use store::{DatabaseBlock, ExecutionPayloadDeneb};
use tokio::sync::{
RwLock,
mpsc::{self, UnboundedSender},
RwLock,
};
use tokio_stream::{Stream, wrappers::UnboundedReceiverStream};
use tracing::{debug, error};
use tokio_stream::{wrappers::UnboundedReceiverStream, Stream};
use types::{
ChainSpec, EthSpec, ExecPayload, ExecutionBlockHash, ForkName, Hash256, SignedBeaconBlock,
SignedBlindedBeaconBlock, Slot,
};
use types::{
ExecutionPayload, ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadElectra,
ExecutionPayloadFulu, ExecutionPayloadGloas, ExecutionPayloadHeader,
ExecutionPayloadFulu, ExecutionPayloadHeader,
};
#[derive(PartialEq)]
@@ -101,13 +100,12 @@ fn reconstruct_default_header_block<E: EthSpec>(
ForkName::Deneb => ExecutionPayloadDeneb::default().into(),
ForkName::Electra => ExecutionPayloadElectra::default().into(),
ForkName::Fulu => ExecutionPayloadFulu::default().into(),
ForkName::Gloas => ExecutionPayloadGloas::default().into(),
ForkName::Base | ForkName::Altair => {
return Err(Error::PayloadReconstruction(format!(
"Block with fork variant {} has execution payload",
fork
))
.into());
.into())
}
};
@@ -131,6 +129,7 @@ fn reconstruct_default_header_block<E: EthSpec>(
fn reconstruct_blocks<E: EthSpec>(
block_map: &mut HashMap<Hash256, Arc<BlockResult<E>>>,
block_parts_with_bodies: HashMap<Hash256, BlockParts<E>>,
log: &Logger,
) {
for (root, block_parts) in block_parts_with_bodies {
if let Some(payload_body) = block_parts.body {
@@ -157,7 +156,7 @@ fn reconstruct_blocks<E: EthSpec>(
reconstructed_transactions_root: header_from_payload
.transactions_root(),
};
debug!(?root, ?error, "Failed to reconstruct block");
debug!(log, "Failed to reconstruct block"; "root" => ?root, "error" => ?error);
block_map.insert(root, Arc::new(Err(error)));
}
}
@@ -233,7 +232,7 @@ impl<E: EthSpec> BodiesByRange<E> {
}
}
async fn execute(&mut self, execution_layer: &ExecutionLayer<E>) {
async fn execute(&mut self, execution_layer: &ExecutionLayer<E>, log: &Logger) {
if let RequestState::UnSent(blocks_parts_ref) = &mut self.state {
let block_parts_vec = std::mem::take(blocks_parts_ref);
@@ -262,12 +261,12 @@ impl<E: EthSpec> BodiesByRange<E> {
});
}
reconstruct_blocks(&mut block_map, with_bodies);
reconstruct_blocks(&mut block_map, with_bodies, log);
}
Err(e) => {
let block_result =
Arc::new(Err(Error::BlocksByRangeFailure(Box::new(e)).into()));
debug!(error = ?block_result, "Payload bodies by range failure");
debug!(log, "Payload bodies by range failure"; "error" => ?block_result);
for block_parts in block_parts_vec {
block_map.insert(block_parts.root(), block_result.clone());
}
@@ -281,8 +280,9 @@ impl<E: EthSpec> BodiesByRange<E> {
&mut self,
root: &Hash256,
execution_layer: &ExecutionLayer<E>,
log: &Logger,
) -> Option<Arc<BlockResult<E>>> {
self.execute(execution_layer).await;
self.execute(execution_layer, log).await;
if let RequestState::Sent(map) = &self.state {
return map.get(root).cloned();
}
@@ -313,7 +313,7 @@ impl<E: EthSpec> EngineRequest<E> {
}
}
pub async fn push_block_parts(&mut self, block_parts: BlockParts<E>) {
pub async fn push_block_parts(&mut self, block_parts: BlockParts<E>, log: &Logger) {
match self {
Self::ByRange(bodies_by_range) => {
let mut request = bodies_by_range.write().await;
@@ -327,21 +327,28 @@ impl<E: EthSpec> EngineRequest<E> {
Self::NoRequest(_) => {
// this should _never_ happen
crit!(
beacon_block_streamer = "push_block_parts called on NoRequest Variant",
"Please notify the devs"
log,
"Please notify the devs";
"beacon_block_streamer" => "push_block_parts called on NoRequest Variant",
);
}
}
}
pub async fn push_block_result(&mut self, root: Hash256, block_result: BlockResult<E>) {
pub async fn push_block_result(
&mut self,
root: Hash256,
block_result: BlockResult<E>,
log: &Logger,
) {
// this function will only fail if something is seriously wrong
match self {
Self::ByRange(_) => {
// this should _never_ happen
crit!(
beacon_block_streamer = "push_block_result called on ByRange",
"Please notify the devs"
log,
"Please notify the devs";
"beacon_block_streamer" => "push_block_result called on ByRange",
);
}
Self::NoRequest(results) => {
@@ -354,22 +361,24 @@ impl<E: EthSpec> EngineRequest<E> {
&self,
root: &Hash256,
execution_layer: &ExecutionLayer<E>,
log: &Logger,
) -> Arc<BlockResult<E>> {
match self {
Self::ByRange(by_range) => {
by_range
.write()
.await
.get_block_result(root, execution_layer)
.get_block_result(root, execution_layer, log)
.await
}
Self::NoRequest(map) => map.read().await.get(root).cloned(),
}
.unwrap_or_else(|| {
crit!(
beacon_block_streamer = "block_result not found in request",
?root,
"Please notify the devs"
log,
"Please notify the devs";
"beacon_block_streamer" => "block_result not found in request",
"root" => ?root,
);
Arc::new(Err(Error::BlockResultNotFound.into()))
})
@@ -404,7 +413,7 @@ impl<T: BeaconChainTypes> BeaconBlockStreamer<T> {
if self.check_caches == CheckCaches::Yes {
match self.beacon_chain.get_block_process_status(&root) {
BlockProcessStatus::Unknown => None,
BlockProcessStatus::NotValidated(block, _)
BlockProcessStatus::NotValidated(block)
| BlockProcessStatus::ExecutionValidated(block) => {
metrics::inc_counter(&metrics::BEACON_REQRESP_PRE_IMPORT_CACHE_HITS);
Some(block)
@@ -509,7 +518,9 @@ impl<T: BeaconChainTypes> BeaconBlockStreamer<T> {
}
};
no_request.push_block_result(root, block_result).await;
no_request
.push_block_result(root, block_result, &self.beacon_chain.log)
.await;
requests.insert(root, no_request.clone());
}
@@ -518,7 +529,9 @@ impl<T: BeaconChainTypes> BeaconBlockStreamer<T> {
by_range_blocks.sort_by_key(|block_parts| block_parts.slot());
for block_parts in by_range_blocks {
let root = block_parts.root();
by_range.push_block_parts(block_parts).await;
by_range
.push_block_parts(block_parts, &self.beacon_chain.log)
.await;
requests.insert(root, by_range.clone());
}
@@ -528,12 +541,17 @@ impl<T: BeaconChainTypes> BeaconBlockStreamer<T> {
result.push((root, request.clone()))
} else {
crit!(
beacon_block_streamer = "request not found",
?root,
"Please notify the devs"
self.beacon_chain.log,
"Please notify the devs";
"beacon_block_streamer" => "request not found",
"root" => ?root,
);
no_request
.push_block_result(root, Err(Error::RequestNotFound.into()))
.push_block_result(
root,
Err(Error::RequestNotFound.into()),
&self.beacon_chain.log,
)
.await;
result.push((root, no_request.clone()));
}
@@ -548,7 +566,10 @@ impl<T: BeaconChainTypes> BeaconBlockStreamer<T> {
block_roots: Vec<Hash256>,
sender: UnboundedSender<(Hash256, Arc<BlockResult<T::EthSpec>>)>,
) {
debug!("Using slower fallback method of eth_getBlockByHash()");
debug!(
self.beacon_chain.log,
"Using slower fallback method of eth_getBlockByHash()"
);
for root in block_roots {
let cached_block = self.check_caches(root);
let block_result = if cached_block.is_some() {
@@ -580,8 +601,9 @@ impl<T: BeaconChainTypes> BeaconBlockStreamer<T> {
Ok(payloads) => payloads,
Err(e) => {
error!(
error = ?e,
"BeaconBlockStreamer: Failed to load payloads"
self.beacon_chain.log,
"BeaconBlockStreamer: Failed to load payloads";
"error" => ?e
);
return;
}
@@ -593,7 +615,9 @@ impl<T: BeaconChainTypes> BeaconBlockStreamer<T> {
engine_requests += 1;
}
let result = request.get_block_result(&root, &self.execution_layer).await;
let result = request
.get_block_result(&root, &self.execution_layer, &self.beacon_chain.log)
.await;
let successful = result
.as_ref()
@@ -612,12 +636,13 @@ impl<T: BeaconChainTypes> BeaconBlockStreamer<T> {
}
debug!(
requested_blocks = n_roots,
sent = n_sent,
succeeded = n_success,
failed = (n_sent - n_success),
engine_requests,
"BeaconBlockStreamer finished"
self.beacon_chain.log,
"BeaconBlockStreamer finished";
"requested blocks" => n_roots,
"sent" => n_sent,
"succeeded" => n_success,
"failed" => (n_sent - n_success),
"engine requests" => engine_requests,
);
}
@@ -653,8 +678,9 @@ impl<T: BeaconChainTypes> BeaconBlockStreamer<T> {
) -> impl Stream<Item = (Hash256, Arc<BlockResult<T::EthSpec>>)> {
let (block_tx, block_rx) = mpsc::unbounded_channel();
debug!(
blocks = block_roots.len(),
"Launching a BeaconBlockStreamer"
self.beacon_chain.log,
"Launching a BeaconBlockStreamer";
"blocks" => block_roots.len(),
);
let executor = self.beacon_chain.task_executor.clone();
executor.spawn(self.stream(block_roots, block_tx), "get_blocks_sender");
@@ -684,13 +710,14 @@ impl From<Error> for BeaconChainError {
#[cfg(test)]
mod tests {
use crate::beacon_block_streamer::{BeaconBlockStreamer, CheckCaches};
use crate::test_utils::{BeaconChainHarness, EphemeralHarnessType, test_spec};
use bls::Keypair;
use fixed_bytes::FixedBytesExtended;
use crate::test_utils::{test_spec, BeaconChainHarness, EphemeralHarnessType};
use execution_layer::test_utils::Block;
use std::sync::Arc;
use std::sync::LazyLock;
use tokio::sync::mpsc;
use types::{ChainSpec, Epoch, EthSpec, Hash256, MinimalEthSpec, Slot};
use types::{
ChainSpec, Epoch, EthSpec, FixedBytesExtended, Hash256, Keypair, MinimalEthSpec, Slot,
};
const VALIDATOR_COUNT: usize = 48;
@@ -705,6 +732,7 @@ mod tests {
let harness = BeaconChainHarness::builder(MinimalEthSpec)
.spec(spec)
.keypairs(KEYPAIRS[0..validator_count].to_vec())
.logger(logging::test_logger())
.fresh_ephemeral_store()
.mock_execution_layer()
.build();
@@ -714,12 +742,11 @@ mod tests {
harness
}
// TODO(EIP-7732) Extend this test for gloas
#[tokio::test]
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 = 0usize;
let bellatrix_fork_epoch = 2usize;
let capella_fork_epoch = 4usize;
let deneb_fork_epoch = 6usize;
let electra_fork_epoch = 8usize;
@@ -733,12 +760,34 @@ mod tests {
spec.deneb_fork_epoch = Some(Epoch::new(deneb_fork_epoch as u64));
spec.electra_fork_epoch = Some(Epoch::new(electra_fork_epoch as u64));
spec.fulu_fork_epoch = Some(Epoch::new(fulu_fork_epoch as u64));
spec.gloas_fork_epoch = None;
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.seconds_per_slot;
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 * slots_per_epoch).await;
harness
.extend_slots((num_epochs - 1 - bellatrix_fork_epoch) * slots_per_epoch)
.await;
let head = harness.chain.head_snapshot();
let state = &head.beacon_state;

File diff suppressed because it is too large Load Diff

View File

@@ -4,9 +4,8 @@
//! Additionally, the `BalancesCache` struct is defined; a cache designed to avoid database
//! reads when fork choice requires the validator balances of the justified state.
use crate::{BeaconSnapshot, metrics};
use educe::Educe;
use fixed_bytes::FixedBytesExtended;
use crate::{metrics, BeaconSnapshot};
use derivative::Derivative;
use fork_choice::ForkChoiceStore;
use proto_array::JustifiedBalances;
use safe_arith::ArithError;
@@ -18,7 +17,7 @@ use store::{Error as StoreError, HotColdDB, ItemStore};
use superstruct::superstruct;
use types::{
AbstractExecPayload, BeaconBlockRef, BeaconState, BeaconStateError, Checkpoint, Epoch, EthSpec,
Hash256, Slot,
FixedBytesExtended, Hash256, Slot,
};
#[derive(Debug)]
@@ -28,7 +27,6 @@ pub enum Error {
FailedToReadState(StoreError),
MissingState(Hash256),
BeaconStateError(BeaconStateError),
UnalignedCheckpoint { block_slot: Slot, state_slot: Slot },
Arith(ArithError),
}
@@ -128,19 +126,17 @@ impl BalancesCache {
/// Implements `fork_choice::ForkChoiceStore` in order to provide a persistent backing to the
/// `fork_choice::ForkChoice` struct.
#[derive(Debug, Educe)]
#[educe(PartialEq(bound(E: EthSpec, Hot: ItemStore, Cold: ItemStore)))]
pub struct BeaconForkChoiceStore<E: EthSpec, Hot: ItemStore, Cold: ItemStore> {
#[educe(PartialEq(ignore))]
#[derive(Debug, Derivative)]
#[derivative(PartialEq(bound = "E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>"))]
pub struct BeaconForkChoiceStore<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> {
#[derivative(PartialEq = "ignore")]
store: Arc<HotColdDB<E, Hot, Cold>>,
balances_cache: BalancesCache,
time: Slot,
finalized_checkpoint: Checkpoint,
justified_checkpoint: Checkpoint,
justified_balances: JustifiedBalances,
justified_state_root: Hash256,
unrealized_justified_checkpoint: Checkpoint,
unrealized_justified_state_root: Hash256,
unrealized_finalized_checkpoint: Checkpoint,
proposer_boost_root: Hash256,
equivocating_indices: BTreeSet<u64>,
@@ -150,8 +146,8 @@ pub struct BeaconForkChoiceStore<E: EthSpec, Hot: ItemStore, Cold: ItemStore> {
impl<E, Hot, Cold> BeaconForkChoiceStore<E, Hot, Cold>
where
E: EthSpec,
Hot: ItemStore,
Cold: ItemStore,
Hot: ItemStore<E>,
Cold: ItemStore<E>,
{
/// Initialize `Self` from some `anchor` checkpoint which may or may not be the genesis state.
///
@@ -166,37 +162,21 @@ where
/// It is assumed that `anchor` is already persisted in `store`.
pub fn get_forkchoice_store(
store: Arc<HotColdDB<E, Hot, Cold>>,
anchor: BeaconSnapshot<E>,
anchor: &BeaconSnapshot<E>,
) -> Result<Self, Error> {
let unadvanced_state_root = anchor.beacon_state_root();
let mut anchor_state = anchor.beacon_state;
let anchor_state = &anchor.beacon_state;
let mut anchor_block_header = anchor_state.latest_block_header().clone();
// The anchor state MUST be on an epoch boundary (it should be advanced by the caller).
if !anchor_state
.slot()
.as_u64()
.is_multiple_of(E::slots_per_epoch())
{
return Err(Error::UnalignedCheckpoint {
block_slot: anchor_block_header.slot,
state_slot: anchor_state.slot(),
});
if anchor_block_header.state_root == Hash256::zero() {
anchor_block_header.state_root = anchor.beacon_state_root();
}
// Compute the accurate block root for the checkpoint block.
if anchor_block_header.state_root.is_zero() {
anchor_block_header.state_root = unadvanced_state_root;
}
let anchor_block_root = anchor_block_header.canonical_root();
let anchor_root = anchor_block_header.canonical_root();
let anchor_epoch = anchor_state.current_epoch();
let justified_checkpoint = Checkpoint {
epoch: anchor_epoch,
root: anchor_block_root,
root: anchor_root,
};
let finalized_checkpoint = justified_checkpoint;
let justified_balances = JustifiedBalances::from_justified_state(&anchor_state)?;
let justified_state_root = anchor_state.canonical_root()?;
let justified_balances = JustifiedBalances::from_justified_state(anchor_state)?;
Ok(Self {
store,
@@ -204,10 +184,8 @@ where
time: anchor_state.slot(),
justified_checkpoint,
justified_balances,
justified_state_root,
finalized_checkpoint,
unrealized_justified_checkpoint: justified_checkpoint,
unrealized_justified_state_root: justified_state_root,
unrealized_finalized_checkpoint: finalized_checkpoint,
proposer_boost_root: Hash256::zero(),
equivocating_indices: BTreeSet::new(),
@@ -219,12 +197,12 @@ where
/// on-disk database.
pub fn to_persisted(&self) -> PersistedForkChoiceStore {
PersistedForkChoiceStore {
balances_cache: self.balances_cache.clone(),
time: self.time,
finalized_checkpoint: self.finalized_checkpoint,
justified_checkpoint: self.justified_checkpoint,
justified_state_root: self.justified_state_root,
justified_balances: self.justified_balances.effective_balances.clone(),
unrealized_justified_checkpoint: self.unrealized_justified_checkpoint,
unrealized_justified_state_root: self.unrealized_justified_state_root,
unrealized_finalized_checkpoint: self.unrealized_finalized_checkpoint,
proposer_boost_root: self.proposer_boost_root,
equivocating_indices: self.equivocating_indices.clone(),
@@ -236,26 +214,16 @@ where
persisted: PersistedForkChoiceStore,
store: Arc<HotColdDB<E, Hot, Cold>>,
) -> Result<Self, Error> {
let justified_checkpoint = persisted.justified_checkpoint;
let justified_state_root = persisted.justified_state_root;
let update_cache = true;
let justified_state = store
.get_hot_state(&justified_state_root, update_cache)
.map_err(Error::FailedToReadState)?
.ok_or(Error::MissingState(justified_state_root))?;
let justified_balances = JustifiedBalances::from_justified_state(&justified_state)?;
let justified_balances =
JustifiedBalances::from_effective_balances(persisted.justified_balances)?;
Ok(Self {
store,
balances_cache: <_>::default(),
balances_cache: persisted.balances_cache,
time: persisted.time,
finalized_checkpoint: persisted.finalized_checkpoint,
justified_checkpoint,
justified_checkpoint: persisted.justified_checkpoint,
justified_balances,
justified_state_root,
unrealized_justified_checkpoint: persisted.unrealized_justified_checkpoint,
unrealized_justified_state_root: persisted.unrealized_justified_state_root,
unrealized_finalized_checkpoint: persisted.unrealized_finalized_checkpoint,
proposer_boost_root: persisted.proposer_boost_root,
equivocating_indices: persisted.equivocating_indices,
@@ -267,8 +235,8 @@ where
impl<E, Hot, Cold> ForkChoiceStore<E> for BeaconForkChoiceStore<E, Hot, Cold>
where
E: EthSpec,
Hot: ItemStore,
Cold: ItemStore,
Hot: ItemStore<E>,
Cold: ItemStore<E>,
{
type Error = Error;
@@ -293,10 +261,6 @@ where
&self.justified_checkpoint
}
fn justified_state_root(&self) -> Hash256 {
self.justified_state_root
}
fn justified_balances(&self) -> &JustifiedBalances {
&self.justified_balances
}
@@ -309,10 +273,6 @@ where
&self.unrealized_justified_checkpoint
}
fn unrealized_justified_state_root(&self) -> Hash256 {
self.unrealized_justified_state_root
}
fn unrealized_finalized_checkpoint(&self) -> &Checkpoint {
&self.unrealized_finalized_checkpoint
}
@@ -325,13 +285,8 @@ where
self.finalized_checkpoint = checkpoint
}
fn set_justified_checkpoint(
&mut self,
checkpoint: Checkpoint,
justified_state_root: Hash256,
) -> Result<(), Error> {
fn set_justified_checkpoint(&mut self, checkpoint: Checkpoint) -> Result<(), Error> {
self.justified_checkpoint = checkpoint;
self.justified_state_root = justified_state_root;
if let Some(balances) = self.balances_cache.get(
self.justified_checkpoint.root,
@@ -342,14 +297,27 @@ where
self.justified_balances = JustifiedBalances::from_effective_balances(balances)?;
} else {
metrics::inc_counter(&metrics::BALANCES_CACHE_MISSES);
// Justified state is reasonably useful to cache, it might be finalized soon.
let update_cache = true;
let state = self
let justified_block = self
.store
.get_hot_state(&self.justified_state_root, update_cache)
.get_blinded_block(&self.justified_checkpoint.root)
.map_err(Error::FailedToReadBlock)?
.ok_or(Error::MissingBlock(self.justified_checkpoint.root))?
.deconstruct()
.0;
let max_slot = self
.justified_checkpoint
.epoch
.start_slot(E::slots_per_epoch());
let (_, state) = self
.store
.get_advanced_hot_state(
self.justified_checkpoint.root,
max_slot,
justified_block.state_root(),
)
.map_err(Error::FailedToReadState)?
.ok_or(Error::MissingState(self.justified_state_root))?;
.ok_or_else(|| Error::MissingState(justified_block.state_root()))?;
self.justified_balances = JustifiedBalances::from_justified_state(&state)?;
}
@@ -357,9 +325,8 @@ where
Ok(())
}
fn set_unrealized_justified_checkpoint(&mut self, checkpoint: Checkpoint, state_root: Hash256) {
fn set_unrealized_justified_checkpoint(&mut self, checkpoint: Checkpoint) {
self.unrealized_justified_checkpoint = checkpoint;
self.unrealized_justified_state_root = state_root;
}
fn set_unrealized_finalized_checkpoint(&mut self, checkpoint: Checkpoint) {
@@ -379,17 +346,17 @@ where
}
}
pub type PersistedForkChoiceStore = PersistedForkChoiceStoreV28;
pub type PersistedForkChoiceStore = PersistedForkChoiceStoreV17;
/// A container which allows persisting the `BeaconForkChoiceStore` to the on-disk database.
#[superstruct(variants(V28), variant_attributes(derive(Encode, Decode)), no_enum)]
#[superstruct(variants(V17), variant_attributes(derive(Encode, Decode)), no_enum)]
pub struct PersistedForkChoiceStore {
pub balances_cache: BalancesCacheV8,
pub time: Slot,
pub finalized_checkpoint: Checkpoint,
pub justified_checkpoint: Checkpoint,
pub justified_state_root: Hash256,
pub justified_balances: Vec<u64>,
pub unrealized_justified_checkpoint: Checkpoint,
pub unrealized_justified_state_root: Hash256,
pub unrealized_finalized_checkpoint: Checkpoint,
pub proposer_boost_root: Hash256,
pub equivocating_indices: BTreeSet<u64>,

View File

@@ -11,17 +11,14 @@
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::cmp::Ordering;
use std::num::NonZeroUsize;
use std::sync::Arc;
use tracing::{debug, instrument};
use typenum::Unsigned;
use types::new_non_zero_usize;
use types::{BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, Fork, Hash256, Slot};
use types::non_zero_usize::new_non_zero_usize;
use types::{
BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, Fork, Hash256, Slot, Unsigned,
};
/// The number of sets of proposer indices that should be cached.
const CACHE_SIZE: NonZeroUsize = new_non_zero_usize(16);
@@ -42,49 +39,21 @@ pub struct Proposer {
/// their signatures.
pub struct EpochBlockProposers {
/// The epoch to which the proposers pertain.
pub(crate) epoch: Epoch,
epoch: Epoch,
/// The fork that should be used to verify proposer signatures.
pub(crate) fork: Fork,
fork: Fork,
/// A list of length `T::EthSpec::slots_per_epoch()`, representing the proposers for each slot
/// in that epoch.
///
/// E.g., if `self.epoch == 1`, then `self.proposers[0]` contains the proposer for slot `32`.
pub(crate) proposers: SmallVec<[usize; TYPICAL_SLOTS_PER_EPOCH]>,
}
impl EpochBlockProposers {
pub fn new(epoch: Epoch, fork: Fork, proposers: Vec<usize>) -> Self {
Self {
epoch,
fork,
proposers: proposers.into(),
}
}
pub fn get_slot<E: EthSpec>(&self, slot: Slot) -> Result<Proposer, BeaconChainError> {
let epoch = slot.epoch(E::slots_per_epoch());
if epoch == self.epoch {
self.proposers
.get(slot.as_usize() % E::SlotsPerEpoch::to_usize())
.map(|&index| Proposer {
index,
fork: self.fork,
})
.ok_or(BeaconChainError::ProposerCacheOutOfBounds { slot, epoch })
} else {
Err(BeaconChainError::ProposerCacheWrongEpoch {
request_epoch: epoch,
cache_epoch: self.epoch,
})
}
}
proposers: SmallVec<[usize; TYPICAL_SLOTS_PER_EPOCH]>,
}
/// A cache to store the proposers for some epoch.
///
/// See the module-level documentation for more information.
pub struct BeaconProposerCache {
cache: LruCache<(Epoch, Hash256), Arc<OnceCell<EpochBlockProposers>>>,
cache: LruCache<(Epoch, Hash256), EpochBlockProposers>,
}
impl Default for BeaconProposerCache {
@@ -105,8 +74,22 @@ impl BeaconProposerCache {
) -> Option<Proposer> {
let epoch = slot.epoch(E::slots_per_epoch());
let key = (epoch, shuffling_decision_block);
let cache = self.cache.get(&key)?.get()?;
cache.get_slot::<E>(slot).ok()
if let Some(cache) = self.cache.get(&key) {
// This `if` statement is likely unnecessary, but it feels like good practice.
if epoch == cache.epoch {
cache
.proposers
.get(slot.as_usize() % E::SlotsPerEpoch::to_usize())
.map(|&index| Proposer {
index,
fork: cache.fork,
})
} else {
None
}
} else {
None
}
}
/// As per `Self::get_slot`, but returns all proposers in all slots for the given `epoch`.
@@ -120,26 +103,7 @@ impl BeaconProposerCache {
epoch: Epoch,
) -> Option<&SmallVec<[usize; TYPICAL_SLOTS_PER_EPOCH]>> {
let key = (epoch, shuffling_decision_block);
self.cache
.get(&key)
.and_then(|cache_once_cell| cache_once_cell.get().map(|proposers| &proposers.proposers))
}
/// Returns the `OnceCell` for the given `(epoch, shuffling_decision_block)` key,
/// inserting an empty one if it doesn't exist.
///
/// The returned `OnceCell` allows the caller to initialise the value externally
/// using `get_or_try_init`, enabling deferred computation without holding a mutable
/// reference to the cache.
pub fn get_or_insert_key(
&mut self,
epoch: Epoch,
shuffling_decision_block: Hash256,
) -> Arc<OnceCell<EpochBlockProposers>> {
let key = (epoch, shuffling_decision_block);
self.cache
.get_or_insert(key, || Arc::new(OnceCell::new()))
.clone()
self.cache.get(&key).map(|cache| &cache.proposers)
}
/// Insert the proposers into the cache.
@@ -156,103 +120,25 @@ impl BeaconProposerCache {
) -> Result<(), BeaconStateError> {
let key = (epoch, shuffling_decision_block);
if !self.cache.contains(&key) {
let epoch_proposers = EpochBlockProposers::new(epoch, fork, proposers);
self.cache
.put(key, Arc::new(OnceCell::with_value(epoch_proposers)));
self.cache.put(
key,
EpochBlockProposers {
epoch,
fork,
proposers: proposers.into(),
},
);
}
Ok(())
}
}
/// 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:
/// - Proposer indices.
/// - True dependent root.
/// - Legacy dependent root (last block of epoch `N - 1`).
/// - Head execution status.
/// - Fork at `request_epoch`.
pub fn compute_proposer_duties_from_head<T: BeaconChainTypes>(
request_epoch: Epoch,
chain: &BeaconChain<T>,
) -> Result<(Vec<usize>, Hash256, Hash256, ExecutionStatus, Fork), BeaconChainError> {
) -> Result<(Vec<usize>, Hash256, ExecutionStatus, Fork), BeaconChainError> {
// Atomically collect information about the head whilst holding the canonical head `Arc` as
// short as possible.
let (mut state, head_state_root, head_block_root) = {
@@ -271,74 +157,44 @@ pub fn compute_proposer_duties_from_head<T: BeaconChainTypes>(
.ok_or(BeaconChainError::HeadMissingFromForkChoice(head_block_root))?;
// Advance the state into the requested epoch.
ensure_state_can_determine_proposers_for_epoch(
&mut state,
head_state_root,
request_epoch,
&chain.spec,
)?;
ensure_state_is_in_epoch(&mut state, head_state_root, request_epoch, &chain.spec)?;
let indices = state
.get_beacon_proposer_indices(request_epoch, &chain.spec)
.get_beacon_proposer_indices(&chain.spec)
.map_err(BeaconChainError::from)?;
let dependent_root = state
.proposer_shuffling_decision_root_at_epoch(request_epoch, head_block_root, &chain.spec)
// The only block which decides its own shuffling is the genesis block.
.proposer_shuffling_decision_root(chain.genesis_block_root)
.map_err(BeaconChainError::from)?;
// This is only required because the V1 proposer duties endpoint spec wasn't updated for Fulu. We
// can delete this once the V1 endpoint is deprecated at the Glamsterdam fork.
let legacy_dependent_root = state
.legacy_proposer_shuffling_decision_root_at_epoch(request_epoch, head_block_root)
.map_err(BeaconChainError::from)?;
// 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 = chain.spec.fork_at_epoch(request_epoch);
Ok((
indices,
dependent_root,
legacy_dependent_root,
execution_status,
fork,
))
Ok((indices, dependent_root, execution_status, state.fork()))
}
/// If required, advance `state` to the epoch required to determine proposer indices in `target_epoch`.
/// If required, advance `state` to `target_epoch`.
///
/// ## Details
///
/// - Returns an error if `state.current_epoch() > target_epoch`.
/// - No-op if `state.current_epoch() == target_epoch`.
/// - It must be the case that `state.canonical_root() == state_root`, but this function will not
/// check that.
#[instrument(skip_all, fields(?state_root, %target_epoch, state_slot = %state.slot()), level = "debug")]
pub fn ensure_state_can_determine_proposers_for_epoch<E: EthSpec>(
/// check that.
pub fn ensure_state_is_in_epoch<E: EthSpec>(
state: &mut BeaconState<E>,
state_root: Hash256,
target_epoch: Epoch,
spec: &ChainSpec,
) -> Result<(), BeaconChainError> {
// The decision slot is the end of an epoch, so we add 1 to reach the first slot of the epoch
// at which the shuffling is determined.
let minimum_slot = spec
.proposer_shuffling_decision_slot::<E>(target_epoch)
.safe_add(1)?;
let minimum_epoch = minimum_slot.epoch(E::slots_per_epoch());
// Before and after Fulu, the oldest epoch reachable from a state at epoch N is epoch N itself,
// i.e. we can never "look back".
let maximum_epoch = target_epoch;
if state.current_epoch() > maximum_epoch {
Err(BeaconStateError::SlotOutOfBounds.into())
} else if state.current_epoch() >= minimum_epoch {
Ok(())
} else {
// State's current epoch is less than the minimum epoch.
// Advance the state up to the minimum epoch.
partial_state_advance(state, Some(state_root), minimum_slot, spec)
.map_err(BeaconChainError::from)
match state.current_epoch().cmp(&target_epoch) {
// Protects against an inconsistent slot clock.
Ordering::Greater => Err(BeaconStateError::SlotOutOfBounds.into()),
// The state needs to be advanced.
Ordering::Less => {
let target_slot = target_epoch.start_slot(E::slots_per_epoch());
partial_state_advance(state, Some(state_root), target_slot, spec)
.map_err(BeaconChainError::from)
}
// The state is suitable, nothing to do.
Ordering::Equal => Ok(()),
}
}

View File

@@ -2,7 +2,7 @@ use serde::Serialize;
use std::sync::Arc;
use types::{
AbstractExecPayload, BeaconState, EthSpec, FullPayload, Hash256, SignedBeaconBlock,
SignedBlindedBeaconBlock, SignedExecutionPayloadEnvelope,
SignedBlindedBeaconBlock,
};
/// Represents some block and its associated state. Generally, this will be used for tracking the
@@ -10,7 +10,6 @@ use types::{
#[derive(Clone, Serialize, PartialEq, Debug)]
pub struct BeaconSnapshot<E: EthSpec, Payload: AbstractExecPayload<E> = FullPayload<E>> {
pub beacon_block: Arc<SignedBeaconBlock<E, Payload>>,
pub execution_envelope: Option<Arc<SignedExecutionPayloadEnvelope<E>>>,
pub beacon_block_root: Hash256,
pub beacon_state: BeaconState<E>,
}
@@ -32,13 +31,11 @@ impl<E: EthSpec, Payload: AbstractExecPayload<E>> BeaconSnapshot<E, Payload> {
/// Create a new checkpoint.
pub fn new(
beacon_block: Arc<SignedBeaconBlock<E, Payload>>,
execution_envelope: Option<Arc<SignedExecutionPayloadEnvelope<E>>>,
beacon_block_root: Hash256,
beacon_state: BeaconState<E>,
) -> Self {
Self {
beacon_block,
execution_envelope,
beacon_block_root,
beacon_state,
}
@@ -57,12 +54,10 @@ impl<E: EthSpec, Payload: AbstractExecPayload<E>> BeaconSnapshot<E, Payload> {
pub fn update(
&mut self,
beacon_block: Arc<SignedBeaconBlock<E, Payload>>,
execution_envelope: Option<Arc<SignedExecutionPayloadEnvelope<E>>>,
beacon_block_root: Hash256,
beacon_state: BeaconState<E>,
) {
self.beacon_block = beacon_block;
self.execution_envelope = execution_envelope;
self.beacon_block_root = beacon_block_root;
self.beacon_state = beacon_state;
}

View File

@@ -1,9 +1,126 @@
//! Provides tools for checking genesis execution payload consistency.
//! Provides tools for checking if a node is ready for the Bellatrix upgrade and following merge
//! transition.
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 {
@@ -24,6 +141,47 @@ 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.seconds_per_slot;
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();
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(
@@ -65,3 +223,14 @@ 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(),
}
}

View File

@@ -1,23 +1,22 @@
use educe::Educe;
use derivative::Derivative;
use slot_clock::SlotClock;
use std::marker::PhantomData;
use std::sync::Arc;
use crate::beacon_chain::{BeaconChain, BeaconChainTypes};
use crate::block_verification::{
BlockSlashInfo, get_validator_pubkey_cache, process_block_slash_info,
cheap_state_advance_to_obtain_committees, get_validator_pubkey_cache, process_block_slash_info,
BlockSlashInfo,
};
use crate::kzg_utils::{validate_blob, validate_blobs};
use crate::observed_data_sidecars::{
Error as ObservedDataSidecarsError, ObservationStrategy, Observe,
};
use crate::{BeaconChainError, metrics};
use crate::observed_data_sidecars::{DoNotObserve, ObservationStrategy, Observe};
use crate::{metrics, BeaconChainError};
use kzg::{Error as KzgError, Kzg, KzgCommitment};
use slog::debug;
use ssz_derive::{Decode, Encode};
use std::time::Duration;
use tracing::{debug, instrument};
use tree_hash::TreeHash;
use types::data::BlobIdentifier;
use types::blob_sidecar::BlobIdentifier;
use types::{
BeaconStateError, BlobSidecar, Epoch, EthSpec, Hash256, SignedBeaconBlockHeader, Slot,
};
@@ -43,7 +42,7 @@ pub enum GossipBlobError {
///
/// We were unable to process this blob due to an internal error. It's
/// unclear if the blob is valid.
BeaconChainError(Box<BeaconChainError>),
BeaconChainError(BeaconChainError),
/// The `BlobSidecar` was gossiped over an incorrect subnet.
///
@@ -97,7 +96,7 @@ pub enum GossipBlobError {
/// ## Peer scoring
///
/// We cannot process the blob without validating its parent, the peer isn't necessarily faulty.
ParentUnknown { parent_root: Hash256 },
BlobParentUnknown { parent_root: Hash256 },
/// Invalid kzg commitment inclusion proof
/// ## Peer scoring
@@ -148,13 +147,13 @@ impl std::fmt::Display for GossipBlobError {
impl From<BeaconChainError> for GossipBlobError {
fn from(e: BeaconChainError) -> Self {
GossipBlobError::BeaconChainError(e.into())
GossipBlobError::BeaconChainError(e)
}
}
impl From<BeaconStateError> for GossipBlobError {
fn from(e: BeaconStateError) -> Self {
GossipBlobError::BeaconChainError(BeaconChainError::BeaconStateError(e).into())
GossipBlobError::BeaconChainError(BeaconChainError::BeaconStateError(e))
}
}
@@ -167,16 +166,6 @@ pub struct GossipVerifiedBlob<T: BeaconChainTypes, O: ObservationStrategy = Obse
_phantom: PhantomData<O>,
}
impl<T: BeaconChainTypes, O: ObservationStrategy> Clone for GossipVerifiedBlob<T, O> {
fn clone(&self) -> Self {
Self {
block_root: self.block_root,
blob: self.blob.clone(),
_phantom: PhantomData,
}
}
}
impl<T: BeaconChainTypes, O: ObservationStrategy> GossipVerifiedBlob<T, O> {
pub fn new(
blob: Arc<BlobSidecar<T::EthSpec>>,
@@ -247,8 +236,8 @@ impl<T: BeaconChainTypes, O: ObservationStrategy> GossipVerifiedBlob<T, O> {
/// Wrapper over a `BlobSidecar` for which we have completed kzg verification.
/// i.e. `verify_blob_kzg_proof(blob, commitment, proof) == true`.
#[derive(Debug, Educe, Clone, Encode, Decode)]
#[educe(PartialEq, Eq)]
#[derive(Debug, Derivative, Clone, Encode, Decode)]
#[derivative(PartialEq, Eq)]
#[ssz(struct_behaviour = "transparent")]
pub struct KzgVerifiedBlob<E: EthSpec> {
blob: Arc<BlobSidecar<E>>,
@@ -305,14 +294,6 @@ impl<E: EthSpec> KzgVerifiedBlob<E> {
seen_timestamp: Duration::from_secs(0),
}
}
/// Mark a blob as KZG verified. Caller must ONLY use this on blob sidecars constructed
/// from EL blobs.
pub fn from_execution_verified(blob: Arc<BlobSidecar<E>>, seen_timestamp: Duration) -> Self {
Self {
blob,
seen_timestamp,
}
}
}
/// Complete kzg verification for a `BlobSidecar`.
@@ -354,9 +335,21 @@ impl<E: EthSpec> KzgVerifiedBlobList<E> {
}
/// Create a `KzgVerifiedBlobList` from `blobs` that are already KZG verified.
pub fn from_verified<I: IntoIterator<Item = KzgVerifiedBlob<E>>>(blobs: I) -> Self {
///
/// This should be used with caution, as used incorrectly it could result in KZG verification
/// being skipped and invalid blobs being deemed valid.
pub fn from_verified<I: IntoIterator<Item = Arc<BlobSidecar<E>>>>(
blobs: I,
seen_timestamp: Duration,
) -> Self {
Self {
verified_blobs: blobs.into_iter().collect(),
verified_blobs: blobs
.into_iter()
.map(|blob| KzgVerifiedBlob {
blob,
seen_timestamp,
})
.collect(),
}
}
}
@@ -375,7 +368,6 @@ impl<E: EthSpec> IntoIterator for KzgVerifiedBlobList<E> {
///
/// Note: This function should be preferred over calling `verify_kzg_for_blob`
/// in a loop since this function kzg verifies a list of blobs more efficiently.
#[instrument(skip_all, level = "debug")]
pub fn verify_kzg_for_blob_list<'a, E: EthSpec, I>(
blob_iter: I,
kzg: &'a Kzg,
@@ -453,9 +445,8 @@ pub fn validate_blob_sidecar_for_gossip<T: BeaconChainTypes, O: ObservationStrat
if chain
.observed_blob_sidecars
.read()
.observation_key_is_known(&blob_sidecar)
.map_err(|e| GossipBlobError::BeaconChainError(Box::new(e.into())))?
.is_some()
.proposer_is_known(&blob_sidecar)
.map_err(|e| GossipBlobError::BeaconChainError(e.into()))?
{
return Err(GossipBlobError::RepeatBlob {
proposer: blob_proposer_index,
@@ -476,7 +467,7 @@ pub fn validate_blob_sidecar_for_gossip<T: BeaconChainTypes, O: ObservationStrat
// We have already verified that the blob is past finalization, so we can
// just check fork choice for the block's parent.
let Some(parent_block) = fork_choice.get_block(&block_parent_root) else {
return Err(GossipBlobError::ParentUnknown {
return Err(GossipBlobError::BlobParentUnknown {
parent_root: block_parent_root,
});
};
@@ -496,33 +487,59 @@ pub fn validate_blob_sidecar_for_gossip<T: BeaconChainTypes, O: ObservationStrat
}
let proposer_shuffling_root =
parent_block.proposer_shuffling_root_for_child_block(blob_epoch, &chain.spec);
if parent_block.slot.epoch(T::EthSpec::slots_per_epoch()) == blob_epoch {
parent_block
.next_epoch_shuffling_id
.shuffling_decision_block
} else {
parent_block.root
};
let proposer = chain.with_proposer_cache(
proposer_shuffling_root,
blob_epoch,
|proposers| proposers.get_slot::<T::EthSpec>(blob_slot),
|| {
debug!(
%block_root,
index = %blob_index,
"Proposer shuffling cache miss for blob verification"
);
// Blob verification is only relevant pre-Fulu and pre-Gloas, so `Pending` payload
// status is sufficient.
chain
.store
.get_advanced_hot_state(block_parent_root, blob_slot, parent_block.state_root)
.map_err(|e| GossipBlobError::BeaconChainError(Box::new(e.into())))?
.ok_or_else(|| {
GossipBlobError::BeaconChainError(Box::new(BeaconChainError::DBInconsistent(
format!("Missing state for parent block {block_parent_root:?}",),
)))
})
},
)?;
let proposer_index = proposer.index;
let fork = proposer.fork;
let proposer_opt = chain
.beacon_proposer_cache
.lock()
.get_slot::<T::EthSpec>(proposer_shuffling_root, blob_slot);
let (proposer_index, fork) = if let Some(proposer) = proposer_opt {
(proposer.index, proposer.fork)
} else {
debug!(
chain.log,
"Proposer shuffling cache miss for blob verification";
"block_root" => %block_root,
"index" => %blob_index,
);
let (parent_state_root, mut parent_state) = chain
.store
.get_advanced_hot_state(block_parent_root, blob_slot, parent_block.state_root)
.map_err(|e| GossipBlobError::BeaconChainError(e.into()))?
.ok_or_else(|| {
BeaconChainError::DBInconsistent(format!(
"Missing state for parent block {block_parent_root:?}",
))
})?;
let state = cheap_state_advance_to_obtain_committees::<_, GossipBlobError>(
&mut parent_state,
Some(parent_state_root),
blob_slot,
&chain.spec,
)?;
let proposers = state.get_beacon_proposer_indices(&chain.spec)?;
let proposer_index = *proposers
.get(blob_slot.as_usize() % T::EthSpec::slots_per_epoch() as usize)
.ok_or_else(|| BeaconChainError::NoProposerForSlot(blob_slot))?;
// Prime the proposer shuffling cache with the newly-learned value.
chain.beacon_proposer_cache.lock().insert(
blob_epoch,
proposer_shuffling_root,
proposers,
state.fork(),
)?;
(proposer_index, state.fork())
};
// Signature verify the signed block header.
let signature_is_valid = {
@@ -566,7 +583,7 @@ pub fn validate_blob_sidecar_for_gossip<T: BeaconChainTypes, O: ObservationStrat
blob_sidecar.block_proposer_index(),
block_root,
)
.map_err(|e| GossipBlobError::BeaconChainError(Box::new(e.into())))?;
.map_err(|e| GossipBlobError::BeaconChainError(e.into()))?;
if O::observe() {
observe_gossip_blob(&kzg_verified_blob.blob, chain)?;
@@ -579,7 +596,21 @@ pub fn validate_blob_sidecar_for_gossip<T: BeaconChainTypes, O: ObservationStrat
})
}
pub fn observe_gossip_blob<T: BeaconChainTypes>(
impl<T: BeaconChainTypes> GossipVerifiedBlob<T, DoNotObserve> {
pub fn observe(
self,
chain: &BeaconChain<T>,
) -> Result<GossipVerifiedBlob<T, Observe>, GossipBlobError> {
observe_gossip_blob(&self.blob.blob, chain)?;
Ok(GossipVerifiedBlob {
block_root: self.block_root,
blob: self.blob,
_phantom: PhantomData,
})
}
}
fn observe_gossip_blob<T: BeaconChainTypes>(
blob_sidecar: &BlobSidecar<T::EthSpec>,
chain: &BeaconChain<T>,
) -> Result<(), GossipBlobError> {
@@ -598,10 +629,7 @@ pub fn observe_gossip_blob<T: BeaconChainTypes>(
.observed_blob_sidecars
.write()
.observe_sidecar(blob_sidecar)
.map_err(|e: ObservedDataSidecarsError| {
GossipBlobError::BeaconChainError(Box::new(e.into()))
})?
.is_some()
.map_err(|e| GossipBlobError::BeaconChainError(e.into()))?
{
return Err(GossipBlobError::RepeatBlob {
proposer: blob_sidecar.block_proposer_index(),

File diff suppressed because it is too large Load Diff

View File

@@ -1,270 +0,0 @@
use std::{sync::Arc, time::Duration};
use fork_choice::PayloadStatus;
use proto_array::{ProposerHeadError, ReOrgThreshold};
use slot_clock::SlotClock;
use tracing::{debug, error, info, instrument, warn};
use types::{BeaconState, Epoch, Hash256, SignedExecutionPayloadEnvelope, Slot};
use crate::{
BeaconChain, BeaconChainTypes, BlockProductionError, StateSkipConfig,
fork_choice_signal::ForkChoiceWaitResult, metrics,
};
mod gloas;
/// State loaded from the database for block production.
pub(crate) struct BlockProductionState<E: types::EthSpec> {
pub state: BeaconState<E>,
pub state_root: Option<Hash256>,
pub parent_payload_status: PayloadStatus,
pub parent_envelope: Option<Arc<SignedExecutionPayloadEnvelope<E>>>,
}
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.
///
/// The returned `PayloadStatus` is the payload status of the parent block to be built upon.
#[instrument(skip_all, level = "debug")]
pub(crate) fn load_state_for_block_production(
self: &Arc<Self>,
slot: Slot,
) -> Result<BlockProductionState<T::EthSpec>, 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. If the head has a payload envelope (Gloas full head), cheaply
// clone the `Arc` so we can pass it to block production without a DB load.
let (head_slot, head_block_root, head_state_root, head_payload_status, head_envelope) = {
let head = self.canonical_head.cached_head();
(
head.head_slot(),
head.head_block_root(),
head.head_state_root(),
head.head_payload_status(),
head.snapshot.execution_envelope.clone(),
)
};
let result = if head_slot < slot {
// Attempt an aggressive re-org if configured and the conditions are right.
// TODO(gloas): re-enable reorgs
let gloas_enabled = self
.spec
.fork_name_at_slot::<T::EthSpec>(slot)
.gloas_enabled();
if !gloas_enabled
&& 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"
);
// TODO(gloas): ensure we use a sensible payload status when we enable reorgs
// for Gloas
BlockProductionState {
state: re_org_state,
state_root: Some(re_org_state_root),
parent_payload_status: PayloadStatus::Pending,
parent_envelope: None,
}
} else {
// Fetch the head state advanced through to `slot`, which should be present in the
// state cache thanks to the state advance timer.
let parent_state_root = head_state_root;
let (state_root, state) = self
.store
.get_advanced_hot_state(head_block_root, slot, parent_state_root)
.map_err(BlockProductionError::FailedToLoadState)?
.ok_or(BlockProductionError::UnableToProduceAtSlot(slot))?;
BlockProductionState {
state,
state_root: Some(state_root),
parent_payload_status: head_payload_status,
parent_envelope: head_envelope,
}
}
} 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))?;
// TODO(gloas): update this to read payload canonicity from fork choice once ready
let parent_payload_status = PayloadStatus::Pending;
BlockProductionState {
state,
state_root: None,
parent_payload_status,
parent_envelope: None,
}
};
drop(state_load_timer);
Ok(result)
}
/// 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 = ReOrgThreshold(self.spec.reorg_head_weight_threshold);
let re_org_parent_threshold = ReOrgThreshold(self.spec.reorg_parent_weight_threshold);
let re_org_max_epochs_since_finalization =
Epoch::new(self.spec.reorg_max_epochs_since_finalization);
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 re_org_cutoff_duration = self
.spec
.compute_slot_component_duration(self.spec.proposer_reorg_cutoff_bps)
.ok()?;
let proposing_on_time = slot_delay < re_org_cutoff_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,
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))
}
}

View File

@@ -0,0 +1,132 @@
use crate::{BeaconChain, BeaconChainError, BeaconChainTypes};
use eth2::lighthouse::{AttestationRewards, BlockReward, BlockRewardMeta};
use operation_pool::{AttMaxCover, MaxCover, 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 {
for &reward in cover.fresh_validators_rewards.values() {
if cover.att.data.slot.epoch(T::EthSpec::slots_per_epoch()) == state.current_epoch()
{
curr_epoch_total += reward;
} else {
prev_epoch_total += reward;
}
}
}
let attestation_total = prev_epoch_total + curr_epoch_total;
// Drop the covers.
let per_attestation_rewards = per_attestation_rewards
.into_iter()
.map(|cover| cover.fresh_validators_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,
})
}
}

View File

@@ -294,7 +294,7 @@ impl BlockTimesCache {
#[cfg(test)]
mod test {
use super::*;
use fixed_bytes::FixedBytesExtended;
use types::FixedBytesExtended;
#[test]
fn observed_time_uses_minimum() {

File diff suppressed because it is too large Load Diff

View File

@@ -1,126 +1,211 @@
use crate::data_availability_checker::{AvailabilityCheckError, DataAvailabilityChecker};
pub use crate::data_availability_checker::{
AvailableBlock, AvailableBlockData, MaybeAvailableBlock,
};
use crate::{BeaconChainTypes, PayloadVerificationOutcome};
use educe::Educe;
use crate::data_availability_checker::AvailabilityCheckError;
pub use crate::data_availability_checker::{AvailableBlock, MaybeAvailableBlock};
use crate::data_column_verification::{CustodyDataColumn, CustodyDataColumnList};
use crate::eth1_finalization_cache::Eth1FinalizationData;
use crate::{get_block_root, PayloadVerificationOutcome};
use derivative::Derivative;
use state_processing::ConsensusContext;
use std::fmt::{Debug, Formatter};
use std::sync::Arc;
use types::data::BlobIdentifier;
use tokio::sync::oneshot;
use types::blob_sidecar::BlobIdentifier;
use types::{
BeaconBlockRef, BeaconState, BlindedPayload, ChainSpec, Epoch, EthSpec, Hash256,
SignedBeaconBlock, SignedBeaconBlockHeader, Slot,
BeaconBlockRef, BeaconState, BlindedPayload, BlobSidecarList, ChainSpec, DataColumnSidecarList,
Epoch, EthSpec, Hash256, RuntimeVariableList, SignedBeaconBlock, SignedBeaconBlockHeader, Slot,
};
/// A wrapper around a `SignedBeaconBlock`. This varaint is constructed
/// when lookup sync only fetches a single block. It does not contain
/// any blobs or data columns.
pub struct LookupBlock<E: EthSpec> {
block: Arc<SignedBeaconBlock<E>>,
/// A block that has been received over RPC. It has 2 internal variants:
///
/// 1. `BlockAndBlobs`: A fully available post deneb block with all the blobs available. This variant
/// is only constructed after making consistency checks between blocks and blobs.
/// Hence, it is fully self contained w.r.t verification. i.e. this block has all the required
/// data to get verified and imported into fork choice.
///
/// 2. `Block`: This can be a fully available pre-deneb block **or** a post-deneb block that may or may
/// not require blobs to be considered fully available.
///
/// Note: We make a distinction over blocks received over gossip because
/// in a post-deneb world, the blobs corresponding to a given block that are received
/// over rpc do not contain the proposer signature for dos resistance.
#[derive(Clone, Derivative)]
#[derivative(Hash(bound = "E: EthSpec"))]
pub struct RpcBlock<E: EthSpec> {
block_root: Hash256,
block: RpcBlockInner<E>,
}
impl<E: EthSpec> LookupBlock<E> {
pub fn new(block: Arc<SignedBeaconBlock<E>>) -> Self {
let block_root = block.canonical_root();
Self { block, block_root }
}
pub fn block(&self) -> &SignedBeaconBlock<E> {
&self.block
impl<E: EthSpec> Debug for RpcBlock<E> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "RpcBlock({:?})", self.block_root)
}
}
impl<E: EthSpec> RpcBlock<E> {
pub fn block_root(&self) -> Hash256 {
self.block_root
}
pub fn block_cloned(&self) -> Arc<SignedBeaconBlock<E>> {
self.block.clone()
}
}
/// A fully available block that has been constructed by range sync.
/// The block contains all the data required to import into fork choice.
/// This includes any and all blobs/columns required, including zero if
/// none are required. This can happen if the block is pre-deneb or if
/// it's simply past the DA boundary.
#[derive(Clone, Educe)]
#[educe(Hash(bound(E: EthSpec)))]
pub struct RangeSyncBlock<E: EthSpec> {
block: AvailableBlock<E>,
}
impl<E: EthSpec> Debug for RangeSyncBlock<E> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "RpcBlock({:?})", self.block_root())
}
}
impl<E: EthSpec> RangeSyncBlock<E> {
pub fn block_root(&self) -> Hash256 {
self.block.block_root()
}
pub fn as_block(&self) -> &SignedBeaconBlock<E> {
self.block.block()
match &self.block {
RpcBlockInner::Block(block) => block,
RpcBlockInner::BlockAndBlobs(block, _) => block,
RpcBlockInner::BlockAndCustodyColumns(block, _) => block,
}
}
pub fn block_cloned(&self) -> Arc<SignedBeaconBlock<E>> {
self.block.block_cloned()
match &self.block {
RpcBlockInner::Block(block) => block.clone(),
RpcBlockInner::BlockAndBlobs(block, _) => block.clone(),
RpcBlockInner::BlockAndCustodyColumns(block, _) => block.clone(),
}
}
pub fn block_data(&self) -> &AvailableBlockData<E> {
self.block.data()
pub fn blobs(&self) -> Option<&BlobSidecarList<E>> {
match &self.block {
RpcBlockInner::Block(_) => None,
RpcBlockInner::BlockAndBlobs(_, blobs) => Some(blobs),
RpcBlockInner::BlockAndCustodyColumns(_, _) => None,
}
}
pub fn custody_columns(&self) -> Option<&CustodyDataColumnList<E>> {
match &self.block {
RpcBlockInner::Block(_) => None,
RpcBlockInner::BlockAndBlobs(_, _) => None,
RpcBlockInner::BlockAndCustodyColumns(_, data_columns) => Some(data_columns),
}
}
}
impl<E: EthSpec> RangeSyncBlock<E> {
/// Constructs an `RangeSyncBlock` from a block and availability data.
///
/// # Errors
///
/// Returns `AvailabilityCheckError` if:
/// - `InvalidAvailableBlockData`: Block data is provided but not required.
/// - `MissingBlobs`: Block requires blobs but they are missing or incomplete.
/// - `MissingCustodyColumns`: Block requires custody columns but they are incomplete.
pub fn new<T>(
/// Note: This variant is intentionally private because we want to safely construct the
/// internal variants after applying consistency checks to ensure that the block and blobs
/// are consistent with respect to each other.
#[derive(Debug, Clone, Derivative)]
#[derivative(Hash(bound = "E: EthSpec"))]
enum RpcBlockInner<E: EthSpec> {
/// Single block lookup response. This should potentially hit the data availability cache.
Block(Arc<SignedBeaconBlock<E>>),
/// This variant is used with parent lookups and by-range responses. It should have all blobs
/// ordered, all block roots matching, and the correct number of blobs for this block.
BlockAndBlobs(Arc<SignedBeaconBlock<E>>, BlobSidecarList<E>),
/// This variant is used with parent lookups and by-range responses. It should have all
/// requested data columns, all block roots matching for this block.
BlockAndCustodyColumns(Arc<SignedBeaconBlock<E>>, CustodyDataColumnList<E>),
}
impl<E: EthSpec> RpcBlock<E> {
/// Constructs a `Block` variant.
pub fn new_without_blobs(
block_root: Option<Hash256>,
block: Arc<SignedBeaconBlock<E>>,
block_data: AvailableBlockData<E>,
da_checker: &DataAvailabilityChecker<T>,
spec: Arc<ChainSpec>,
) -> Result<Self, AvailabilityCheckError>
where
T: BeaconChainTypes<EthSpec = E>,
{
let available_block = AvailableBlock::new(block, block_data, da_checker, spec)?;
) -> Self {
let block_root = block_root.unwrap_or_else(|| get_block_root(&block));
Self {
block_root,
block: RpcBlockInner::Block(block),
}
}
/// Constructs a new `BlockAndBlobs` variant after making consistency
/// checks between the provided blocks and blobs. This struct makes no
/// guarantees about whether blobs should be present, only that they are
/// consistent with the block. An empty list passed in for `blobs` is
/// viewed the same as `None` passed in.
pub fn new(
block_root: Option<Hash256>,
block: Arc<SignedBeaconBlock<E>>,
blobs: Option<BlobSidecarList<E>>,
) -> Result<Self, AvailabilityCheckError> {
let block_root = block_root.unwrap_or_else(|| get_block_root(&block));
// Treat empty blob lists as if they are missing.
let blobs = blobs.filter(|b| !b.is_empty());
if let (Some(blobs), Ok(block_commitments)) = (
blobs.as_ref(),
block.message().body().blob_kzg_commitments(),
) {
if blobs.len() != block_commitments.len() {
return Err(AvailabilityCheckError::MissingBlobs);
}
for (blob, &block_commitment) in blobs.iter().zip(block_commitments.iter()) {
let blob_commitment = blob.kzg_commitment;
if blob_commitment != block_commitment {
return Err(AvailabilityCheckError::KzgCommitmentMismatch {
block_commitment,
blob_commitment,
});
}
}
}
let inner = match blobs {
Some(blobs) => RpcBlockInner::BlockAndBlobs(block, blobs),
None => RpcBlockInner::Block(block),
};
Ok(Self {
block: available_block,
block_root,
block: inner,
})
}
pub fn new_with_custody_columns(
block_root: Option<Hash256>,
block: Arc<SignedBeaconBlock<E>>,
custody_columns: Vec<CustodyDataColumn<E>>,
spec: &ChainSpec,
) -> Result<Self, AvailabilityCheckError> {
let block_root = block_root.unwrap_or_else(|| get_block_root(&block));
if block.num_expected_blobs() > 0 && custody_columns.is_empty() {
// The number of required custody columns is out of scope here.
return Err(AvailabilityCheckError::MissingCustodyColumns);
}
// Treat empty data column lists as if they are missing.
let inner = if !custody_columns.is_empty() {
RpcBlockInner::BlockAndCustodyColumns(
block,
RuntimeVariableList::new(custody_columns, spec.number_of_columns as usize)?,
)
} else {
RpcBlockInner::Block(block)
};
Ok(Self {
block_root,
block: inner,
})
}
#[allow(clippy::type_complexity)]
pub fn deconstruct(self) -> (Hash256, Arc<SignedBeaconBlock<E>>, AvailableBlockData<E>) {
self.block.deconstruct()
pub fn deconstruct(
self,
) -> (
Hash256,
Arc<SignedBeaconBlock<E>>,
Option<BlobSidecarList<E>>,
Option<CustodyDataColumnList<E>>,
) {
let block_root = self.block_root();
match self.block {
RpcBlockInner::Block(block) => (block_root, block, None, None),
RpcBlockInner::BlockAndBlobs(block, blobs) => (block_root, block, Some(blobs), None),
RpcBlockInner::BlockAndCustodyColumns(block, data_columns) => {
(block_root, block, None, Some(data_columns))
}
}
}
pub fn n_blobs(&self) -> usize {
match self.block_data() {
AvailableBlockData::NoData | AvailableBlockData::DataColumns(_) => 0,
AvailableBlockData::Blobs(blobs) => blobs.len(),
match &self.block {
RpcBlockInner::Block(_) | RpcBlockInner::BlockAndCustodyColumns(_, _) => 0,
RpcBlockInner::BlockAndBlobs(_, blobs) => blobs.len(),
}
}
pub fn n_data_columns(&self) -> usize {
match self.block_data() {
AvailableBlockData::NoData | AvailableBlockData::Blobs(_) => 0,
AvailableBlockData::DataColumns(columns) => columns.len(),
match &self.block {
RpcBlockInner::Block(_) | RpcBlockInner::BlockAndBlobs(_, _) => 0,
RpcBlockInner::BlockAndCustodyColumns(_, data_columns) => data_columns.len(),
}
}
pub fn into_available_block(self) -> AvailableBlock<E> {
self.block
}
}
/// A block that has gone through all pre-deneb block processing checks including block processing
@@ -180,6 +265,7 @@ impl<E: EthSpec> ExecutedBlock<E> {
/// A block that has completed all pre-deneb block processing checks including verification
/// by an EL client **and** has all requisite blob data to be imported into fork choice.
#[derive(PartialEq)]
pub struct AvailableExecutedBlock<E: EthSpec> {
pub block: AvailableBlock<E>,
pub import_data: BlockImportData<E>,
@@ -252,12 +338,42 @@ impl<E: EthSpec> AvailabilityPendingExecutedBlock<E> {
}
}
#[derive(Clone, Debug, PartialEq)]
#[derive(Debug, Derivative)]
#[derivative(PartialEq)]
pub struct BlockImportData<E: EthSpec> {
pub block_root: Hash256,
pub state: BeaconState<E>,
pub parent_block: SignedBeaconBlock<E, BlindedPayload<E>>,
pub parent_eth1_finalization_data: Eth1FinalizationData,
pub confirmed_state_roots: Vec<Hash256>,
pub consensus_context: ConsensusContext<E>,
#[derivative(PartialEq = "ignore")]
/// An optional receiver for `DataColumnSidecarList`.
///
/// This field is `Some` when data columns are being computed asynchronously.
/// The resulting `DataColumnSidecarList` will be sent through this receiver.
pub data_column_recv: Option<oneshot::Receiver<DataColumnSidecarList<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,
parent_eth1_finalization_data: Eth1FinalizationData {
eth1_data: <_>::default(),
eth1_deposit_index: 0,
},
confirmed_state_roots: vec![],
consensus_context: ConsensusContext::new(Slot::new(0)),
data_column_recv: None,
}
}
}
/// Trait for common block operations.
@@ -267,7 +383,7 @@ pub trait AsBlock<E: EthSpec> {
fn parent_root(&self) -> Hash256;
fn state_root(&self) -> Hash256;
fn signed_block_header(&self) -> SignedBeaconBlockHeader;
fn message(&self) -> BeaconBlockRef<'_, E>;
fn message(&self) -> BeaconBlockRef<E>;
fn as_block(&self) -> &SignedBeaconBlock<E>;
fn block_cloned(&self) -> Arc<SignedBeaconBlock<E>>;
fn canonical_root(&self) -> Hash256;
@@ -294,7 +410,7 @@ impl<E: EthSpec> AsBlock<E> for Arc<SignedBeaconBlock<E>> {
SignedBeaconBlock::signed_block_header(self)
}
fn message(&self) -> BeaconBlockRef<'_, E> {
fn message(&self) -> BeaconBlockRef<E> {
SignedBeaconBlock::message(self)
}
@@ -327,19 +443,25 @@ impl<E: EthSpec> AsBlock<E> for MaybeAvailableBlock<E> {
fn signed_block_header(&self) -> SignedBeaconBlockHeader {
self.as_block().signed_block_header()
}
fn message(&self) -> BeaconBlockRef<'_, E> {
fn message(&self) -> BeaconBlockRef<E> {
self.as_block().message()
}
fn as_block(&self) -> &SignedBeaconBlock<E> {
match &self {
MaybeAvailableBlock::Available(block) => block.as_block(),
MaybeAvailableBlock::AvailabilityPending { block, .. } => block,
MaybeAvailableBlock::AvailabilityPending {
block_root: _,
block,
} => block,
}
}
fn block_cloned(&self) -> Arc<SignedBeaconBlock<E>> {
match &self {
MaybeAvailableBlock::Available(block) => block.block_cloned(),
MaybeAvailableBlock::AvailabilityPending { block, .. } => block.clone(),
MaybeAvailableBlock::AvailabilityPending {
block_root: _,
block,
} => block.clone(),
}
}
fn canonical_root(&self) -> Hash256 {
@@ -368,7 +490,7 @@ impl<E: EthSpec> AsBlock<E> for AvailableBlock<E> {
self.block().signed_block_header()
}
fn message(&self) -> BeaconBlockRef<'_, E> {
fn message(&self) -> BeaconBlockRef<E> {
self.block().message()
}
@@ -385,7 +507,7 @@ impl<E: EthSpec> AsBlock<E> for AvailableBlock<E> {
}
}
impl<E: EthSpec> AsBlock<E> for RangeSyncBlock<E> {
impl<E: EthSpec> AsBlock<E> for RpcBlock<E> {
fn slot(&self) -> Slot {
self.as_block().slot()
}
@@ -401,46 +523,24 @@ impl<E: EthSpec> AsBlock<E> for RangeSyncBlock<E> {
fn signed_block_header(&self) -> SignedBeaconBlockHeader {
self.as_block().signed_block_header()
}
fn message(&self) -> BeaconBlockRef<'_, E> {
fn message(&self) -> BeaconBlockRef<E> {
self.as_block().message()
}
fn as_block(&self) -> &SignedBeaconBlock<E> {
self.block.as_block()
match &self.block {
RpcBlockInner::Block(block) => block,
RpcBlockInner::BlockAndBlobs(block, _) => block,
RpcBlockInner::BlockAndCustodyColumns(block, _) => block,
}
}
fn block_cloned(&self) -> Arc<SignedBeaconBlock<E>> {
self.block.block_cloned()
match &self.block {
RpcBlockInner::Block(block) => block.clone(),
RpcBlockInner::BlockAndBlobs(block, _) => block.clone(),
RpcBlockInner::BlockAndCustodyColumns(block, _) => block.clone(),
}
}
fn canonical_root(&self) -> Hash256 {
self.block.block_root()
}
}
impl<E: EthSpec> AsBlock<E> for LookupBlock<E> {
fn slot(&self) -> Slot {
self.block().slot()
}
fn epoch(&self) -> Epoch {
self.block().epoch()
}
fn parent_root(&self) -> Hash256 {
self.block().parent_root()
}
fn state_root(&self) -> Hash256 {
self.block().state_root()
}
fn signed_block_header(&self) -> SignedBeaconBlockHeader {
self.block().signed_block_header()
}
fn message(&self) -> BeaconBlockRef<'_, E> {
self.block().message()
}
fn as_block(&self) -> &SignedBeaconBlock<E> {
self.block()
}
fn block_cloned(&self) -> Arc<SignedBeaconBlock<E>> {
self.block_cloned()
}
fn canonical_root(&self) -> Hash256 {
self.block_root
self.as_block().canonical_root()
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,121 @@
//! Provides tools for checking if a node is ready for the Capella upgrade.
use crate::{BeaconChain, BeaconChainTypes};
use execution_layer::http::{
ENGINE_FORKCHOICE_UPDATED_V2, ENGINE_GET_PAYLOAD_V2, ENGINE_NEW_PAYLOAD_V2,
};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::time::Duration;
use types::*;
/// The time before the Capella fork when we will start issuing warnings about preparation.
use super::bellatrix_readiness::SECONDS_IN_A_WEEK;
pub const CAPELLA_READINESS_PREPARATION_SECONDS: u64 = SECONDS_IN_A_WEEK * 2;
pub const ENGINE_CAPABILITIES_REFRESH_INTERVAL: u64 = 300;
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "type")]
pub enum CapellaReadiness {
/// The execution engine is capella-enabled (as far as we can tell)
Ready,
/// We are connected to an execution engine which doesn't support the V2 engine api methods
V2MethodsNotSupported { error: String },
/// The transition configuration with the EL failed, there might be a problem with
/// connectivity, authentication or a difference in configuration.
ExchangeCapabilitiesFailed { error: String },
/// The user has not configured an execution endpoint
NoExecutionEndpoint,
}
impl fmt::Display for CapellaReadiness {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CapellaReadiness::Ready => {
write!(f, "This node appears ready for Capella.")
}
CapellaReadiness::ExchangeCapabilitiesFailed { error } => write!(
f,
"Could not exchange capabilities with the \
execution endpoint: {}",
error
),
CapellaReadiness::NoExecutionEndpoint => write!(
f,
"The --execution-endpoint flag is not specified, this is a \
requirement post-merge"
),
CapellaReadiness::V2MethodsNotSupported { error } => write!(
f,
"Execution endpoint does not support Capella methods: {}",
error
),
}
}
}
impl<T: BeaconChainTypes> BeaconChain<T> {
/// Returns `true` if capella epoch is set and Capella fork has occurred or will
/// occur within `CAPELLA_READINESS_PREPARATION_SECONDS`
pub fn is_time_to_prepare_for_capella(&self, current_slot: Slot) -> bool {
if let Some(capella_epoch) = self.spec.capella_fork_epoch {
let capella_slot = capella_epoch.start_slot(T::EthSpec::slots_per_epoch());
let capella_readiness_preparation_slots =
CAPELLA_READINESS_PREPARATION_SECONDS / self.spec.seconds_per_slot;
// Return `true` if Capella has happened or is within the preparation time.
current_slot + capella_readiness_preparation_slots > capella_slot
} else {
// The Capella 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 capella.
pub async fn check_capella_readiness(&self) -> CapellaReadiness {
if let Some(el) = self.execution_layer.as_ref() {
match el
.get_engine_capabilities(Some(Duration::from_secs(
ENGINE_CAPABILITIES_REFRESH_INTERVAL,
)))
.await
{
Err(e) => {
// The EL was either unreachable or responded with an error
CapellaReadiness::ExchangeCapabilitiesFailed {
error: format!("{:?}", e),
}
}
Ok(capabilities) => {
let mut missing_methods = String::from("Required Methods Unsupported:");
let mut all_good = true;
if !capabilities.get_payload_v2 {
missing_methods.push(' ');
missing_methods.push_str(ENGINE_GET_PAYLOAD_V2);
all_good = false;
}
if !capabilities.forkchoice_updated_v2 {
missing_methods.push(' ');
missing_methods.push_str(ENGINE_FORKCHOICE_UPDATED_V2);
all_good = false;
}
if !capabilities.new_payload_v2 {
missing_methods.push(' ');
missing_methods.push_str(ENGINE_NEW_PAYLOAD_V2);
all_good = false;
}
if all_good {
CapellaReadiness::Ready
} else {
CapellaReadiness::V2MethodsNotSupported {
error: missing_methods,
}
}
}
}
} else {
CapellaReadiness::NoExecutionEndpoint
}
}
}

View File

@@ -1,10 +1,13 @@
use crate::custody_context::NodeCustodyType;
pub use proto_array::DisallowedReOrgOffsets;
pub use proto_array::{DisallowedReOrgOffsets, ReOrgThreshold};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use std::{collections::HashSet, sync::LazyLock, time::Duration};
use types::{Checkpoint, Hash256};
use std::time::Duration;
use types::{Checkpoint, Epoch};
pub const DEFAULT_RE_ORG_HEAD_THRESHOLD: ReOrgThreshold = ReOrgThreshold(20);
pub const DEFAULT_RE_ORG_PARENT_THRESHOLD: ReOrgThreshold = ReOrgThreshold(160);
pub const DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION: Epoch = Epoch::new(2);
/// Default to 1/12th of the slot, which is 1 second on mainnet.
pub const DEFAULT_RE_ORG_CUTOFF_DENOMINATOR: u32 = 12;
pub const DEFAULT_FORK_CHOICE_BEFORE_PROPOSAL_TIMEOUT: u64 = 250;
/// Default fraction of a slot lookahead for payload preparation (12/3 = 4 seconds on mainnet).
@@ -13,15 +16,6 @@ pub const DEFAULT_PREPARE_PAYLOAD_LOOKAHEAD_FACTOR: u32 = 3;
/// Fraction of a slot lookahead for fork choice in the state advance timer (500ms on mainnet).
pub const FORK_CHOICE_LOOKAHEAD_FACTOR: u32 = 24;
/// Default sync tolerance epochs.
pub const DEFAULT_SYNC_TOLERANCE_EPOCHS: u64 = 2;
/// Invalid block root to be banned from processing and importing on Holesky network by default.
pub static INVALID_HOLESKY_BLOCK_ROOT: LazyLock<Hash256> = LazyLock::new(|| {
Hash256::from_str("2db899881ed8546476d0b92c6aa9110bea9a4cd0dbeb5519eb0ea69575f1f359")
.expect("valid block root")
});
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
pub struct ChainConfig {
/// Maximum number of slots to skip when importing an attestation.
@@ -33,9 +27,17 @@ 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 archive: bool,
pub reconstruct_historic_states: 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.
pub re_org_head_threshold: Option<ReOrgThreshold>,
/// Minimum percentage of the parent committee weight at which to attempt re-orging the canonical head.
pub re_org_parent_threshold: Option<ReOrgThreshold>,
/// Maximum number of epochs since finalization for attempting a proposer re-org.
pub re_org_max_epochs_since_finalization: Epoch,
/// Maximum delay after the start of the slot at which to propose a reorging block.
pub re_org_cutoff_millis: Option<u64>,
/// Additional epoch offsets at which re-orging block proposals are not permitted.
///
/// By default this list is empty, but it can be useful for reacting to network conditions, e.g.
@@ -74,8 +76,6 @@ pub struct ChainConfig {
/// If using a weak-subjectivity sync, whether we should download blocks all the way back to
/// genesis.
pub genesis_backfill: bool,
/// EXPERIMENTAL: backfill blobs and data columns beyond the data availability window.
pub complete_blob_backfill: bool,
/// Whether to send payload attributes every slot, regardless of connected proposers.
///
/// This is useful for block builders and testing.
@@ -86,34 +86,16 @@ pub struct ChainConfig {
pub enable_light_client_server: bool,
/// The number of data columns to withhold / exclude from publishing when proposing a block.
pub malicious_withhold_count: usize,
/// Enable peer sampling on blocks.
pub enable_sampling: bool,
/// Number of batches that the node splits blobs or data columns into during publication.
/// This doesn't apply if the node is the block proposer. For PeerDAS only.
pub blob_publication_batches: usize,
/// The delay in milliseconds applied by the node between sending each blob or data column batch.
/// This doesn't apply if the node is the block proposer.
pub blob_publication_batch_interval: Duration,
/// The max distance between the head block and the current slot at which Lighthouse will
/// consider itself synced and still serve validator-related requests.
pub disable_attesting: bool,
pub sync_tolerance_epochs: u64,
/// Artificial delay for block publishing. For PeerDAS testing only.
pub block_publishing_delay: Option<Duration>,
/// Artificial delay for data column publishing. For PeerDAS testing only.
pub data_column_publishing_delay: Option<Duration>,
/// Block roots of "banned" blocks which Lighthouse will refuse to import.
///
/// 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,
/// Whether to enable partial data column support.
pub enable_partial_columns: bool,
/// The node's custody type, determining how many data columns to custody and sample.
pub node_custody_type: NodeCustodyType,
/// Disable proposer re-org
pub disable_proposer_reorg: bool,
}
impl Default for ChainConfig {
@@ -121,8 +103,12 @@ impl Default for ChainConfig {
Self {
import_max_skip_slots: None,
weak_subjectivity_checkpoint: None,
archive: false,
reconstruct_historic_states: 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),
re_org_max_epochs_since_finalization: DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION,
re_org_cutoff_millis: None,
re_org_disallowed_offsets: DisallowedReOrgOffsets::default(),
fork_choice_before_proposal_timeout_ms: DEFAULT_FORK_CHOICE_BEFORE_PROPOSAL_TIMEOUT,
// Builder fallback configs that are set in `clap` will override these.
@@ -138,22 +124,26 @@ impl Default for ChainConfig {
optimistic_finalized_sync: true,
shuffling_cache_size: crate::shuffling_cache::DEFAULT_CACHE_SIZE,
genesis_backfill: false,
complete_blob_backfill: false,
always_prepare_payload: false,
epochs_per_migration: crate::migrate::DEFAULT_EPOCHS_PER_MIGRATION,
enable_light_client_server: true,
malicious_withhold_count: 0,
enable_sampling: false,
blob_publication_batches: 4,
blob_publication_batch_interval: Duration::from_millis(300),
sync_tolerance_epochs: DEFAULT_SYNC_TOLERANCE_EPOCHS,
block_publishing_delay: None,
data_column_publishing_delay: None,
invalid_block_roots: HashSet::new(),
ignore_ws_check: false,
disable_get_blobs: false,
enable_partial_columns: false,
node_custody_type: NodeCustodyType::Fullnode,
disable_proposer_reorg: false,
disable_attesting: false,
sync_tolerance_epochs: 16,
}
}
}
impl ChainConfig {
/// The latest delay from the start of the slot at which to attempt a 1-slot re-org.
pub fn re_org_cutoff(&self, seconds_per_slot: u64) -> Duration {
self.re_org_cutoff_millis
.map(Duration::from_millis)
.unwrap_or_else(|| {
Duration::from_secs(seconds_per_slot) / DEFAULT_RE_ORG_CUTOFF_DENOMINATOR
})
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4,14 +4,13 @@ use types::{BeaconStateError, ColumnIndex, Hash256};
#[derive(Debug)]
pub enum Error {
InvalidBlobs(KzgError),
MissingBid(Hash256),
InvalidColumn((Option<ColumnIndex>, KzgError)),
InvalidColumn(Vec<(ColumnIndex, KzgError)>),
ReconstructColumnsError(KzgError),
KzgCommitmentMismatch {
blob_commitment: KzgCommitment,
block_commitment: KzgCommitment,
},
Unexpected(String),
Unexpected,
SszTypes(ssz_types::Error),
MissingBlobs,
MissingCustodyColumns,
@@ -23,8 +22,6 @@ pub enum Error {
BlockReplayError(state_processing::BlockReplayError),
RebuildingStateCaches(BeaconStateError),
SlotClockError,
InvalidAvailableBlockData,
InvalidVariant,
}
#[derive(PartialEq, Eq)]
@@ -40,17 +37,14 @@ impl Error {
match self {
Error::SszTypes(_)
| Error::MissingBlobs
| Error::MissingBid(_)
| Error::MissingCustodyColumns
| Error::StoreError(_)
| Error::DecodeError(_)
| Error::Unexpected(_)
| Error::Unexpected
| Error::ParentStateMissing(_)
| Error::BlockReplayError(_)
| Error::RebuildingStateCaches(_)
| Error::SlotClockError
| Error::InvalidAvailableBlockData
| Error::InvalidVariant => ErrorCategory::Internal,
| Error::SlotClockError => ErrorCategory::Internal,
Error::InvalidBlobs { .. }
| Error::InvalidColumn { .. }
| Error::ReconstructColumnsError { .. }

View File

@@ -0,0 +1,231 @@
use crate::block_verification_types::AsBlock;
use crate::{
block_verification_types::BlockImportData,
data_availability_checker::{AvailabilityCheckError, STATE_LRU_CAPACITY_NON_ZERO},
eth1_finalization_cache::Eth1FinalizationData,
AvailabilityPendingExecutedBlock, BeaconChainTypes, BeaconStore, PayloadVerificationOutcome,
};
use lru::LruCache;
use parking_lot::RwLock;
use ssz_derive::{Decode, Encode};
use state_processing::BlockReplayer;
use std::sync::Arc;
use store::OnDiskConsensusContext;
use types::beacon_block_body::KzgCommitments;
use types::{ssz_tagged_signed_beacon_block, ssz_tagged_signed_beacon_block_arc};
use types::{BeaconState, BlindedPayload, ChainSpec, Epoch, EthSpec, Hash256, SignedBeaconBlock};
/// This mirrors everything in the `AvailabilityPendingExecutedBlock`, except
/// that it is much smaller because it contains only a state root instead of
/// a full `BeaconState`.
#[derive(Encode, Decode, Clone)]
pub struct DietAvailabilityPendingExecutedBlock<E: EthSpec> {
#[ssz(with = "ssz_tagged_signed_beacon_block_arc")]
block: Arc<SignedBeaconBlock<E>>,
state_root: Hash256,
#[ssz(with = "ssz_tagged_signed_beacon_block")]
parent_block: SignedBeaconBlock<E, BlindedPayload<E>>,
parent_eth1_finalization_data: Eth1FinalizationData,
confirmed_state_roots: Vec<Hash256>,
consensus_context: OnDiskConsensusContext<E>,
payload_verification_outcome: PayloadVerificationOutcome,
}
/// just implementing the same methods as `AvailabilityPendingExecutedBlock`
impl<E: EthSpec> DietAvailabilityPendingExecutedBlock<E> {
pub fn as_block(&self) -> &SignedBeaconBlock<E> {
&self.block
}
pub fn block_cloned(&self) -> Arc<SignedBeaconBlock<E>> {
self.block.clone()
}
pub fn num_blobs_expected(&self) -> usize {
self.block
.message()
.body()
.blob_kzg_commitments()
.map_or(0, |commitments| commitments.len())
}
pub fn get_commitments(&self) -> KzgCommitments<E> {
self.as_block()
.message()
.body()
.blob_kzg_commitments()
.cloned()
.unwrap_or_default()
}
/// Returns the epoch corresponding to `self.slot()`.
pub fn epoch(&self) -> Epoch {
self.block.slot().epoch(E::slots_per_epoch())
}
}
/// This LRU cache holds BeaconStates used for block import. If the cache overflows,
/// the least recently used state will be dropped. If the dropped state is needed
/// later on, it will be recovered from the parent state and replaying the block.
///
/// WARNING: This cache assumes the parent block of any `AvailabilityPendingExecutedBlock`
/// has already been imported into ForkChoice. If this is not the case, the cache
/// will fail to recover the state when the cache overflows because it can't load
/// the parent state!
pub struct StateLRUCache<T: BeaconChainTypes> {
states: RwLock<LruCache<Hash256, BeaconState<T::EthSpec>>>,
store: BeaconStore<T>,
spec: Arc<ChainSpec>,
}
impl<T: BeaconChainTypes> StateLRUCache<T> {
pub fn new(store: BeaconStore<T>, spec: Arc<ChainSpec>) -> Self {
Self {
states: RwLock::new(LruCache::new(STATE_LRU_CAPACITY_NON_ZERO)),
store,
spec,
}
}
/// This will store the state in the LRU cache and return a
/// `DietAvailabilityPendingExecutedBlock` which is much cheaper to
/// keep around in memory.
pub fn register_pending_executed_block(
&self,
executed_block: AvailabilityPendingExecutedBlock<T::EthSpec>,
) -> DietAvailabilityPendingExecutedBlock<T::EthSpec> {
let state = executed_block.import_data.state;
let state_root = executed_block.block.state_root();
self.states.write().put(state_root, state);
DietAvailabilityPendingExecutedBlock {
block: executed_block.block,
state_root,
parent_block: executed_block.import_data.parent_block,
parent_eth1_finalization_data: executed_block.import_data.parent_eth1_finalization_data,
confirmed_state_roots: executed_block.import_data.confirmed_state_roots,
consensus_context: OnDiskConsensusContext::from_consensus_context(
executed_block.import_data.consensus_context,
),
payload_verification_outcome: executed_block.payload_verification_outcome,
}
}
/// Recover the `AvailabilityPendingExecutedBlock` from the diet version.
/// This method will first check the cache and if the state is not found
/// it will reconstruct the state by loading the parent state from disk and
/// replaying the block.
pub fn recover_pending_executed_block(
&self,
diet_executed_block: DietAvailabilityPendingExecutedBlock<T::EthSpec>,
) -> Result<AvailabilityPendingExecutedBlock<T::EthSpec>, AvailabilityCheckError> {
let state = if let Some(state) = self.states.write().pop(&diet_executed_block.state_root) {
state
} else {
self.reconstruct_state(&diet_executed_block)?
};
let block_root = diet_executed_block.block.canonical_root();
Ok(AvailabilityPendingExecutedBlock {
block: diet_executed_block.block,
import_data: BlockImportData {
block_root,
state,
parent_block: diet_executed_block.parent_block,
parent_eth1_finalization_data: diet_executed_block.parent_eth1_finalization_data,
confirmed_state_roots: diet_executed_block.confirmed_state_roots,
consensus_context: diet_executed_block
.consensus_context
.into_consensus_context(),
data_column_recv: None,
},
payload_verification_outcome: diet_executed_block.payload_verification_outcome,
})
}
/// Reconstruct the state by loading the parent state from disk and replaying
/// the block.
fn reconstruct_state(
&self,
diet_executed_block: &DietAvailabilityPendingExecutedBlock<T::EthSpec>,
) -> Result<BeaconState<T::EthSpec>, AvailabilityCheckError> {
let parent_block_root = diet_executed_block.parent_block.canonical_root();
let parent_block_state_root = diet_executed_block.parent_block.state_root();
let (parent_state_root, parent_state) = self
.store
.get_advanced_hot_state(
parent_block_root,
diet_executed_block.parent_block.slot(),
parent_block_state_root,
)
.map_err(AvailabilityCheckError::StoreError)?
.ok_or(AvailabilityCheckError::ParentStateMissing(
parent_block_state_root,
))?;
let state_roots = vec![
Ok((parent_state_root, diet_executed_block.parent_block.slot())),
Ok((
diet_executed_block.state_root,
diet_executed_block.block.slot(),
)),
];
let block_replayer: BlockReplayer<'_, T::EthSpec, AvailabilityCheckError, _> =
BlockReplayer::new(parent_state, &self.spec)
.no_signature_verification()
.state_root_iter(state_roots.into_iter())
.minimal_block_root_verification();
block_replayer
.apply_blocks(vec![diet_executed_block.block.clone_as_blinded()], None)
.map(|block_replayer| block_replayer.into_state())
.and_then(|mut state| {
state
.build_exit_cache(&self.spec)
.map_err(AvailabilityCheckError::RebuildingStateCaches)?;
state
.update_tree_hash_cache()
.map_err(AvailabilityCheckError::RebuildingStateCaches)?;
Ok(state)
})
}
/// returns the state cache for inspection
pub fn lru_cache(&self) -> &RwLock<LruCache<Hash256, BeaconState<T::EthSpec>>> {
&self.states
}
/// remove any states from the cache from before the given epoch
pub fn do_maintenance(&self, cutoff_epoch: Epoch) {
let mut write_lock = self.states.write();
while let Some((_, state)) = write_lock.peek_lru() {
if state.slot().epoch(T::EthSpec::slots_per_epoch()) < cutoff_epoch {
write_lock.pop_lru();
} else {
break;
}
}
}
}
/// This can only be used during testing. The intended way to
/// obtain a `DietAvailabilityPendingExecutedBlock` is to call
/// `register_pending_executed_block` on the `StateLRUCache`.
#[cfg(test)]
impl<E: EthSpec> From<AvailabilityPendingExecutedBlock<E>>
for DietAvailabilityPendingExecutedBlock<E>
{
fn from(mut value: AvailabilityPendingExecutedBlock<E>) -> Self {
Self {
block: value.block,
state_root: value.import_data.state.canonical_root().unwrap(),
parent_block: value.import_data.parent_block,
parent_eth1_finalization_data: value.import_data.parent_eth1_finalization_data,
confirmed_state_roots: value.import_data.confirmed_state_roots,
consensus_context: OnDiskConsensusContext::from_consensus_context(
value.import_data.consensus_context,
),
payload_verification_outcome: value.payload_verification_outcome,
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,121 @@
//! Provides tools for checking if a node is ready for the Deneb upgrade.
use crate::{BeaconChain, BeaconChainTypes};
use execution_layer::http::{
ENGINE_FORKCHOICE_UPDATED_V3, ENGINE_GET_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V3,
};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::time::Duration;
use types::*;
/// The time before the Deneb fork when we will start issuing warnings about preparation.
use super::bellatrix_readiness::SECONDS_IN_A_WEEK;
pub const DENEB_READINESS_PREPARATION_SECONDS: u64 = SECONDS_IN_A_WEEK * 2;
pub const ENGINE_CAPABILITIES_REFRESH_INTERVAL: u64 = 300;
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "type")]
pub enum DenebReadiness {
/// The execution engine is deneb-enabled (as far as we can tell)
Ready,
/// We are connected to an execution engine which doesn't support the V3 engine api methods
V3MethodsNotSupported { error: String },
/// The transition configuration with the EL failed, there might be a problem with
/// connectivity, authentication or a difference in configuration.
ExchangeCapabilitiesFailed { error: String },
/// The user has not configured an execution endpoint
NoExecutionEndpoint,
}
impl fmt::Display for DenebReadiness {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DenebReadiness::Ready => {
write!(f, "This node appears ready for Deneb.")
}
DenebReadiness::ExchangeCapabilitiesFailed { error } => write!(
f,
"Could not exchange capabilities with the \
execution endpoint: {}",
error
),
DenebReadiness::NoExecutionEndpoint => write!(
f,
"The --execution-endpoint flag is not specified, this is a \
requirement post-merge"
),
DenebReadiness::V3MethodsNotSupported { error } => write!(
f,
"Execution endpoint does not support Deneb methods: {}",
error
),
}
}
}
impl<T: BeaconChainTypes> BeaconChain<T> {
/// Returns `true` if deneb epoch is set and Deneb fork has occurred or will
/// occur within `DENEB_READINESS_PREPARATION_SECONDS`
pub fn is_time_to_prepare_for_deneb(&self, current_slot: Slot) -> bool {
if let Some(deneb_epoch) = self.spec.deneb_fork_epoch {
let deneb_slot = deneb_epoch.start_slot(T::EthSpec::slots_per_epoch());
let deneb_readiness_preparation_slots =
DENEB_READINESS_PREPARATION_SECONDS / self.spec.seconds_per_slot;
// Return `true` if Deneb has happened or is within the preparation time.
current_slot + deneb_readiness_preparation_slots > deneb_slot
} else {
// The Deneb 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 capella.
pub async fn check_deneb_readiness(&self) -> DenebReadiness {
if let Some(el) = self.execution_layer.as_ref() {
match el
.get_engine_capabilities(Some(Duration::from_secs(
ENGINE_CAPABILITIES_REFRESH_INTERVAL,
)))
.await
{
Err(e) => {
// The EL was either unreachable or responded with an error
DenebReadiness::ExchangeCapabilitiesFailed {
error: format!("{:?}", e),
}
}
Ok(capabilities) => {
let mut missing_methods = String::from("Required Methods Unsupported:");
let mut all_good = true;
if !capabilities.get_payload_v3 {
missing_methods.push(' ');
missing_methods.push_str(ENGINE_GET_PAYLOAD_V3);
all_good = false;
}
if !capabilities.forkchoice_updated_v3 {
missing_methods.push(' ');
missing_methods.push_str(ENGINE_FORKCHOICE_UPDATED_V3);
all_good = false;
}
if !capabilities.new_payload_v3 {
missing_methods.push(' ');
missing_methods.push_str(ENGINE_NEW_PAYLOAD_V3);
all_good = false;
}
if all_good {
DenebReadiness::Ready
} else {
DenebReadiness::V3MethodsNotSupported {
error: missing_methods,
}
}
}
}
} else {
DenebReadiness::NoExecutionEndpoint
}
}
}

View File

@@ -1,81 +1,13 @@
use crate::data_availability_checker::{AvailableBlock, AvailableBlockData};
use crate::{BeaconChainError as Error, metrics};
use crate::data_availability_checker::AvailableBlock;
use crate::{
attester_cache::{CommitteeLengths, 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::*;
/// Stores the minimal amount of data required to compute the committee length for any committee at any
/// slot in a given `epoch`.
pub struct CommitteeLengths {
/// The `epoch` to which the lengths pertain.
epoch: Epoch,
/// The length of the shuffling in `self.epoch`.
active_validator_indices_len: usize,
}
impl CommitteeLengths {
/// Instantiate `Self` using `state.current_epoch()`.
pub fn new<E: EthSpec>(state: &BeaconState<E>) -> Result<Self, Error> {
let active_validator_indices_len = state
.committee_cache(RelativeEpoch::Current)?
.active_validator_indices()
.len();
Ok(Self {
epoch: state.current_epoch(),
active_validator_indices_len,
})
}
/// Get the count of committees per each slot of `self.epoch`.
pub fn get_committee_count_per_slot<E: EthSpec>(
&self,
spec: &ChainSpec,
) -> Result<usize, Error> {
E::get_committee_count_per_slot(self.active_validator_indices_len, spec).map_err(Into::into)
}
/// Get the length of the committee at the given `slot` and `committee_index`.
pub fn get_committee_length<E: EthSpec>(
&self,
slot: Slot,
committee_index: CommitteeIndex,
spec: &ChainSpec,
) -> Result<usize, Error> {
let slots_per_epoch = E::slots_per_epoch();
let request_epoch = slot.epoch(slots_per_epoch);
// Sanity check.
if request_epoch != self.epoch {
return Err(Error::EarlyAttesterCacheError);
}
let slots_per_epoch = slots_per_epoch as usize;
let committees_per_slot = self.get_committee_count_per_slot::<E>(spec)?;
let index_in_epoch = compute_committee_index_in_epoch(
slot,
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,
index_in_epoch,
self.active_validator_indices_len,
)?
.ok_or(Error::EarlyAttesterCacheError)?;
range
.end
.checked_sub(range.start)
.ok_or(Error::EarlyAttesterCacheError)
}
}
pub struct CacheItem<E: EthSpec> {
/*
* Values used to create attestations.
@@ -101,7 +33,7 @@ pub struct CacheItem<E: EthSpec> {
///
/// - Produce an attestation without using `chain.canonical_head`.
/// - Verify that a block root exists (i.e., will be imported in the future) during attestation
/// verification.
/// verification.
/// - Provide a block which can be sent to peers via RPC.
#[derive(Default)]
pub struct EarlyAttesterCache<E: EthSpec> {
@@ -120,12 +52,13 @@ impl<E: EthSpec> EarlyAttesterCache<E> {
pub fn add_head_block(
&self,
beacon_block_root: Hash256,
block: &AvailableBlock<E>,
block: AvailableBlock<E>,
proto_block: ProtoBlock,
state: &BeaconState<E>,
spec: &ChainSpec,
) -> Result<(), Error> {
let epoch = state.current_epoch();
let committee_lengths = CommitteeLengths::new(state)?;
let committee_lengths = CommitteeLengths::new(state, spec)?;
let source = state.current_justified_checkpoint();
let target_slot = epoch.start_slot(E::slots_per_epoch());
let target = Checkpoint {
@@ -137,19 +70,14 @@ impl<E: EthSpec> EarlyAttesterCache<E> {
},
};
let (blobs, data_columns) = match block.data() {
AvailableBlockData::NoData => (None, None),
AvailableBlockData::Blobs(blobs) => (Some(blobs.clone()), None),
AvailableBlockData::DataColumns(data_columns) => (None, Some(data_columns.clone())),
};
let (_, block, blobs, data_columns) = block.deconstruct();
let item = CacheItem {
epoch,
committee_lengths,
beacon_block_root,
source,
target,
block: block.block_cloned(),
block,
blobs,
data_columns,
proto_block,
@@ -165,13 +93,6 @@ impl<E: EthSpec> EarlyAttesterCache<E> {
/// - There is a cache `item` present.
/// - If `request_slot` is in the same epoch as `item.epoch`.
/// - If `request_index` does not exceed `item.committee_count`.
///
/// Post gloas an additional condition must be met:
/// - `request_slot` is the same slot as `item.block.slot` (i.e. a same slot attestation).
///
/// Non-same-slot Gloas attestations need `data.index` set from the canonical payload
/// status, which the cache doesn't track. Returning `None` falls through to fork choice.
#[instrument(skip_all, fields(%request_slot, %request_index), level = "debug")]
pub fn try_attest(
&self,
request_slot: Slot,
@@ -203,12 +124,6 @@ impl<E: EthSpec> EarlyAttesterCache<E> {
item.committee_lengths
.get_committee_length::<E>(request_slot, request_index, spec)?;
let is_same_slot_attestation = request_slot == item.block.slot();
if spec.fork_name_at_slot::<E>(request_slot).gloas_enabled() && !is_same_slot_attestation {
return Ok(None);
}
let payload_present = false;
let attestation = Attestation::empty_for_signing(
request_index,
committee_len,
@@ -216,7 +131,6 @@ impl<E: EthSpec> EarlyAttesterCache<E> {
item.beacon_block_root,
item.source,
item.target,
payload_present,
spec,
)
.map_err(Error::AttestationError)?;
@@ -269,12 +183,4 @@ impl<E: EthSpec> EarlyAttesterCache<E> {
.filter(|item| item.beacon_block_root == block_root)
.map(|item| item.proto_block.clone())
}
/// Fetch the slot and block root of the current head block.
pub fn get_head_block_root(&self) -> Option<(Slot, Hash256)> {
self.item
.read()
.as_ref()
.map(|item| (item.block.slot(), item.beacon_block_root))
}
}

View File

@@ -0,0 +1,115 @@
//! Provides tools for checking if a node is ready for the Electra upgrade and following merge
//! transition.
use crate::{BeaconChain, BeaconChainTypes};
use execution_layer::http::{ENGINE_GET_PAYLOAD_V4, ENGINE_NEW_PAYLOAD_V4};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::time::Duration;
use types::*;
/// The time before the Electra fork when we will start issuing warnings about preparation.
use super::bellatrix_readiness::SECONDS_IN_A_WEEK;
pub const ELECTRA_READINESS_PREPARATION_SECONDS: u64 = SECONDS_IN_A_WEEK * 2;
pub const ENGINE_CAPABILITIES_REFRESH_INTERVAL: u64 = 300;
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "type")]
pub enum ElectraReadiness {
/// The execution engine is electra-enabled (as far as we can tell)
Ready,
/// We are connected to an execution engine which doesn't support the V4 engine api methods
V4MethodsNotSupported { error: String },
/// The transition configuration with the EL failed, there might be a problem with
/// connectivity, authentication or a difference in configuration.
ExchangeCapabilitiesFailed { error: String },
/// The user has not configured an execution endpoint
NoExecutionEndpoint,
}
impl fmt::Display for ElectraReadiness {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ElectraReadiness::Ready => {
write!(f, "This node appears ready for Electra.")
}
ElectraReadiness::ExchangeCapabilitiesFailed { error } => write!(
f,
"Could not exchange capabilities with the \
execution endpoint: {}",
error
),
ElectraReadiness::NoExecutionEndpoint => write!(
f,
"The --execution-endpoint flag is not specified, this is a \
requirement post-merge"
),
ElectraReadiness::V4MethodsNotSupported { error } => write!(
f,
"Execution endpoint does not support Electra methods: {}",
error
),
}
}
}
impl<T: BeaconChainTypes> BeaconChain<T> {
/// Returns `true` if electra epoch is set and Electra fork has occurred or will
/// occur within `ELECTRA_READINESS_PREPARATION_SECONDS`
pub fn is_time_to_prepare_for_electra(&self, current_slot: Slot) -> bool {
if let Some(electra_epoch) = self.spec.electra_fork_epoch {
let electra_slot = electra_epoch.start_slot(T::EthSpec::slots_per_epoch());
let electra_readiness_preparation_slots =
ELECTRA_READINESS_PREPARATION_SECONDS / self.spec.seconds_per_slot;
// Return `true` if Electra has happened or is within the preparation time.
current_slot + electra_readiness_preparation_slots > electra_slot
} else {
// The Electra 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 electra.
pub async fn check_electra_readiness(&self) -> ElectraReadiness {
if let Some(el) = self.execution_layer.as_ref() {
match el
.get_engine_capabilities(Some(Duration::from_secs(
ENGINE_CAPABILITIES_REFRESH_INTERVAL,
)))
.await
{
Err(e) => {
// The EL was either unreachable or responded with an error
ElectraReadiness::ExchangeCapabilitiesFailed {
error: format!("{:?}", e),
}
}
Ok(capabilities) => {
let mut missing_methods = String::from("Required Methods Unsupported:");
let mut all_good = true;
if !capabilities.get_payload_v4 {
missing_methods.push(' ');
missing_methods.push_str(ENGINE_GET_PAYLOAD_V4);
all_good = false;
}
if !capabilities.new_payload_v4 {
missing_methods.push(' ');
missing_methods.push_str(ENGINE_NEW_PAYLOAD_V4);
all_good = false;
}
if all_good {
ElectraReadiness::Ready
} else {
ElectraReadiness::V4MethodsNotSupported {
error: missing_methods,
}
}
}
}
} else {
ElectraReadiness::NoExecutionEndpoint
}
}
}

View File

@@ -1,197 +0,0 @@
//! 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));
}
}

View File

@@ -1,25 +1,22 @@
use crate::attester_cache::Error as AttesterCacheError;
use crate::beacon_block_streamer::Error as BlockStreamerError;
use crate::beacon_chain::ForkChoiceError;
use crate::beacon_fork_choice_store::Error as ForkChoiceStoreError;
use crate::data_availability_checker::AvailabilityCheckError;
use crate::eth1_chain::Error as Eth1ChainError;
use crate::migrate::PruningError;
use crate::naive_aggregation_pool::Error as NaiveAggregationError;
use crate::observed_aggregates::Error as ObservedAttestationsError;
use crate::observed_attesters::Error as ObservedAttestersError;
use crate::observed_block_producers::Error as ObservedBlockProducersError;
use crate::observed_data_sidecars::Error as ObservedDataSidecarsError;
use crate::payload_envelope_streamer::Error as EnvelopeStreamerError;
use bls::PublicKeyBytes;
use execution_layer::PayloadStatus;
use fork_choice::ExecutionStatus;
use futures::channel::mpsc::TrySendError;
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,
per_block_processing::errors::{
AttestationValidationError, AttesterSlashingValidationError,
@@ -28,9 +25,11 @@ use state_processing::{
},
signature_sets::Error as SignatureSetError,
state_advance::Error as StateAdvanceError,
BlockProcessingError, BlockReplayError, EpochProcessingError, SlotProcessingError,
};
use task_executor::ShutdownReason;
use tokio::task::JoinError;
use types::milhouse::Error as MilhouseError;
use types::*;
macro_rules! easy_from_to {
@@ -54,7 +53,6 @@ pub enum BeaconChainError {
},
SlotClockDidNotStart,
NoStateForSlot(Slot),
NoBlockForSlot(Slot),
BeaconStateError(BeaconStateError),
EpochCacheError(EpochCacheError),
DBInconsistent(String),
@@ -63,8 +61,6 @@ pub enum BeaconChainError {
ForkChoiceStoreError(ForkChoiceStoreError),
MissingBeaconBlock(Hash256),
MissingBeaconState(Hash256),
MissingExecutionPayloadEnvelope(Hash256),
MissingHotStateSummary(Hash256),
SlotProcessingError(SlotProcessingError),
EpochProcessingError(EpochProcessingError),
StateAdvanceError(StateAdvanceError),
@@ -102,7 +98,7 @@ pub enum BeaconChainError {
ObservedAttestersError(ObservedAttestersError),
ObservedBlockProducersError(ObservedBlockProducersError),
ObservedDataSidecarsError(ObservedDataSidecarsError),
EarlyAttesterCacheError,
AttesterCacheError(AttesterCacheError),
PruningError(PruningError),
ArithError(ArithError),
InvalidShufflingId {
@@ -160,7 +156,6 @@ pub enum BeaconChainError {
reconstructed_transactions_root: Hash256,
},
BlockStreamerError(BlockStreamerError),
EnvelopeStreamerError(EnvelopeStreamerError),
AddPayloadLogicError,
ExecutionForkChoiceUpdateFailed(execution_layer::Error),
PrepareProposerFailed(BlockProcessingError),
@@ -186,9 +181,9 @@ pub enum BeaconChainError {
execution_block_hash: Option<ExecutionBlockHash>,
},
ForkchoiceUpdate(execution_layer::Error),
InvalidCheckpoint {
state_root: Hash256,
checkpoint: Checkpoint,
FinalizedCheckpointMismatch {
head_state: Checkpoint,
fork_choice: Hash256,
},
InvalidSlot(Slot),
HeadBlockNotFullyVerified {
@@ -224,7 +219,7 @@ pub enum BeaconChainError {
UnableToPublish,
UnableToBuildColumnSidecar(String),
AvailabilityCheckError(AvailabilityCheckError),
LightClientError(LightClientError),
LightClientUpdateError(LightClientUpdateError),
LightClientBootstrapError(String),
UnsupportedFork,
MilhouseError(MilhouseError),
@@ -235,31 +230,6 @@ pub enum BeaconChainError {
columns_found: usize,
},
FailedToReconstructBlobs(String),
ProposerCacheIncorrectState {
state_decision_block_root: Hash256,
requested_decision_block_root: Hash256,
},
ProposerCacheAccessorFailure {
decision_block_root: Hash256,
proposal_epoch: Epoch,
},
ProposerCacheOutOfBounds {
slot: Slot,
epoch: Epoch,
},
ProposerCacheWrongEpoch {
request_epoch: Epoch,
cache_epoch: Epoch,
},
AttesterCachePtcOutOfBounds {
slot: Slot,
epoch: Epoch,
},
AttesterCacheNoPtcPreGloas {
slot: Slot,
},
SkipProposerPreparation,
FailedColumnCustodyInfoUpdate,
}
easy_from_to!(SlotProcessingError, BeaconChainError);
@@ -277,6 +247,7 @@ easy_from_to!(ObservedAttestationsError, BeaconChainError);
easy_from_to!(ObservedAttestersError, BeaconChainError);
easy_from_to!(ObservedBlockProducersError, BeaconChainError);
easy_from_to!(ObservedDataSidecarsError, BeaconChainError);
easy_from_to!(AttesterCacheError, BeaconChainError);
easy_from_to!(BlockSignatureVerifierError, BeaconChainError);
easy_from_to!(PruningError, BeaconChainError);
easy_from_to!(ArithError, BeaconChainError);
@@ -286,7 +257,7 @@ easy_from_to!(BlockReplayError, BeaconChainError);
easy_from_to!(InconsistentFork, BeaconChainError);
easy_from_to!(AvailabilityCheckError, BeaconChainError);
easy_from_to!(EpochCacheError, BeaconChainError);
easy_from_to!(LightClientError, BeaconChainError);
easy_from_to!(LightClientUpdateError, BeaconChainError);
easy_from_to!(MilhouseError, BeaconChainError);
easy_from_to!(AttestationError, BeaconChainError);
@@ -299,9 +270,13 @@ pub enum BlockProductionError {
BlockProcessingError(BlockProcessingError),
EpochCacheError(EpochCacheError),
ForkChoiceError(ForkChoiceError),
Eth1ChainError(Eth1ChainError),
BeaconStateError(BeaconStateError),
StateAdvanceError(StateAdvanceError),
OpPoolError(OpPoolError),
/// The `BeaconChain` was explicitly configured _without_ a connection to eth1, therefore it
/// cannot produce blocks.
NoEth1ChainConnection,
StateSlotTooHigh {
produce_at_slot: Slot,
state_slot: Slot,
@@ -320,24 +295,18 @@ pub enum BlockProductionError {
MissingExecutionPayload,
MissingKzgCommitment(String),
TokioJoin(JoinError),
BeaconChain(Box<BeaconChainError>),
BeaconChain(BeaconChainError),
InvalidPayloadFork,
InvalidBlockVariant(String),
KzgError(kzg::Error),
FailedToBuildBlobSidecars(String),
MissingExecutionRequests,
SszTypesError(ssz_types::Error),
EnvelopeProcessingError(EnvelopeProcessingError),
BlsError(bls::Error),
MissingParentExecutionPayload,
MissingExecutionPayloadEnvelope(Hash256),
// TODO(gloas): Remove this once Gloas is implemented
GloasNotImplemented(String),
}
easy_from_to!(BlockProcessingError, BlockProductionError);
easy_from_to!(BeaconStateError, BlockProductionError);
easy_from_to!(SlotProcessingError, BlockProductionError);
easy_from_to!(Eth1ChainError, BlockProductionError);
easy_from_to!(StateAdvanceError, BlockProductionError);
easy_from_to!(ForkChoiceError, BlockProductionError);
easy_from_to!(EpochCacheError, BlockProductionError);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,499 @@
use slog::{debug, Logger};
use ssz_derive::{Decode, Encode};
use std::cmp;
use std::collections::BTreeMap;
use types::{Checkpoint, Epoch, Eth1Data, Hash256 as Root};
/// The default size of the cache.
/// The beacon chain only looks at the last 4 epochs for finalization.
/// Add 1 for current epoch and 4 earlier epochs.
pub const DEFAULT_ETH1_CACHE_SIZE: usize = 5;
/// These fields are named the same as the corresponding fields in the `BeaconState`
/// as this structure stores these values from the `BeaconState` at a `Checkpoint`
#[derive(Clone, Debug, PartialEq, Encode, Decode)]
pub struct Eth1FinalizationData {
pub eth1_data: Eth1Data,
pub eth1_deposit_index: u64,
}
impl Eth1FinalizationData {
/// Ensures the deposit finalization conditions have been met. See:
/// https://eips.ethereum.org/EIPS/eip-4881#deposit-finalization-conditions
fn fully_imported(&self) -> bool {
self.eth1_deposit_index >= self.eth1_data.deposit_count
}
}
/// Implements map from Checkpoint -> Eth1CacheData
pub struct CheckpointMap {
capacity: usize,
// There shouldn't be more than a couple of potential checkpoints at the same
// epoch. Searching through a vector for the matching Root should be faster
// than using another map from Root->Eth1CacheData
store: BTreeMap<Epoch, Vec<(Root, Eth1FinalizationData)>>,
}
impl Default for CheckpointMap {
fn default() -> Self {
Self::new()
}
}
/// Provides a map of `Eth1CacheData` referenced by `Checkpoint`
///
/// ## Cache Queuing
///
/// The cache keeps a maximum number of (`capacity`) epochs. Because there may be
/// forks at the epoch boundary, it's possible that there exists more than one
/// `Checkpoint` for the same `Epoch`. This cache will store all checkpoints for
/// a given `Epoch`. When adding data for a new `Checkpoint` would cause the number
/// of `Epoch`s stored to exceed `capacity`, the data for oldest `Epoch` is dropped
impl CheckpointMap {
pub fn new() -> Self {
CheckpointMap {
capacity: DEFAULT_ETH1_CACHE_SIZE,
store: BTreeMap::new(),
}
}
pub fn with_capacity(capacity: usize) -> Self {
CheckpointMap {
capacity: cmp::max(1, capacity),
store: BTreeMap::new(),
}
}
pub fn insert(&mut self, checkpoint: Checkpoint, eth1_finalization_data: Eth1FinalizationData) {
self.store
.entry(checkpoint.epoch)
.or_default()
.push((checkpoint.root, eth1_finalization_data));
// faster to reduce size after the fact than do pre-checking to see
// if the current data would increase the size of the BTreeMap
while self.store.len() > self.capacity {
let oldest_stored_epoch = self.store.keys().next().cloned().unwrap();
self.store.remove(&oldest_stored_epoch);
}
}
pub fn get(&self, checkpoint: &Checkpoint) -> Option<&Eth1FinalizationData> {
match self.store.get(&checkpoint.epoch) {
Some(vec) => {
for (root, data) in vec {
if *root == checkpoint.root {
return Some(data);
}
}
None
}
None => None,
}
}
#[cfg(test)]
pub fn len(&self) -> usize {
self.store.len()
}
}
/// This cache stores `Eth1CacheData` that could potentially be finalized within 4
/// future epochs.
pub struct Eth1FinalizationCache {
by_checkpoint: CheckpointMap,
pending_eth1: BTreeMap<u64, Eth1Data>,
last_finalized: Option<Eth1Data>,
log: Logger,
}
/// Provides a cache of `Eth1CacheData` at epoch boundaries. This is used to
/// finalize deposits when a new epoch is finalized.
///
impl Eth1FinalizationCache {
pub fn new(log: Logger) -> Self {
Eth1FinalizationCache {
by_checkpoint: CheckpointMap::new(),
pending_eth1: BTreeMap::new(),
last_finalized: None,
log,
}
}
pub fn with_capacity(log: Logger, capacity: usize) -> Self {
Eth1FinalizationCache {
by_checkpoint: CheckpointMap::with_capacity(capacity),
pending_eth1: BTreeMap::new(),
last_finalized: None,
log,
}
}
pub fn insert(&mut self, checkpoint: Checkpoint, eth1_finalization_data: Eth1FinalizationData) {
if !eth1_finalization_data.fully_imported() {
self.pending_eth1.insert(
eth1_finalization_data.eth1_data.deposit_count,
eth1_finalization_data.eth1_data.clone(),
);
debug!(
self.log,
"Eth1Cache: inserted pending eth1";
"eth1_data.deposit_count" => eth1_finalization_data.eth1_data.deposit_count,
"eth1_deposit_index" => eth1_finalization_data.eth1_deposit_index,
);
}
self.by_checkpoint
.insert(checkpoint, eth1_finalization_data);
}
pub fn finalize(&mut self, checkpoint: &Checkpoint) -> Option<Eth1Data> {
if let Some(eth1_finalized_data) = self.by_checkpoint.get(checkpoint) {
let finalized_deposit_index = eth1_finalized_data.eth1_deposit_index;
let mut result = None;
while let Some(pending_count) = self.pending_eth1.keys().next().cloned() {
if finalized_deposit_index >= pending_count {
result = self.pending_eth1.remove(&pending_count);
debug!(
self.log,
"Eth1Cache: dropped pending eth1";
"pending_count" => pending_count,
"finalized_deposit_index" => finalized_deposit_index,
);
} else {
break;
}
}
if eth1_finalized_data.fully_imported() {
result = Some(eth1_finalized_data.eth1_data.clone())
}
if result.is_some() {
self.last_finalized = result;
}
self.last_finalized.clone()
} else {
debug!(
self.log,
"Eth1Cache: cache miss";
"epoch" => checkpoint.epoch,
);
None
}
}
#[cfg(test)]
pub fn by_checkpoint(&self) -> &CheckpointMap {
&self.by_checkpoint
}
#[cfg(test)]
pub fn pending_eth1(&self) -> &BTreeMap<u64, Eth1Data> {
&self.pending_eth1
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use sloggers::null::NullLoggerBuilder;
use sloggers::Build;
use std::collections::HashMap;
const SLOTS_PER_EPOCH: u64 = 32;
const MAX_DEPOSITS: u64 = 16;
const EPOCHS_PER_ETH1_VOTING_PERIOD: u64 = 64;
fn eth1cache() -> Eth1FinalizationCache {
let log_builder = NullLoggerBuilder;
Eth1FinalizationCache::new(log_builder.build().expect("should build log"))
}
fn random_eth1_data(deposit_count: u64) -> Eth1Data {
Eth1Data {
deposit_root: Root::random(),
deposit_count,
block_hash: Root::random(),
}
}
fn random_checkpoint(epoch: u64) -> Checkpoint {
Checkpoint {
epoch: epoch.into(),
root: Root::random(),
}
}
fn random_checkpoints(n: usize) -> Vec<Checkpoint> {
let mut result = Vec::with_capacity(n);
for epoch in 0..n {
result.push(random_checkpoint(epoch as u64))
}
result
}
#[test]
fn fully_imported_deposits() {
let epochs = 16;
let deposits_imported = 128;
let eth1data = random_eth1_data(deposits_imported);
let checkpoints = random_checkpoints(epochs as usize);
let mut eth1cache = eth1cache();
for epoch in 4..epochs {
assert_eq!(
eth1cache.by_checkpoint().len(),
cmp::min((epoch - 4) as usize, DEFAULT_ETH1_CACHE_SIZE),
"Unexpected cache size"
);
let checkpoint = checkpoints
.get(epoch as usize)
.expect("should get checkpoint");
eth1cache.insert(
*checkpoint,
Eth1FinalizationData {
eth1_data: eth1data.clone(),
eth1_deposit_index: deposits_imported,
},
);
let finalized_checkpoint = checkpoints
.get((epoch - 4) as usize)
.expect("should get finalized checkpoint");
assert!(
eth1cache.pending_eth1().is_empty(),
"Deposits are fully imported so pending cache should be empty"
);
if epoch < 8 {
assert_eq!(
eth1cache.finalize(finalized_checkpoint),
None,
"Should have cache miss"
);
} else {
assert_eq!(
eth1cache.finalize(finalized_checkpoint),
Some(eth1data.clone()),
"Should have cache hit"
)
}
}
}
#[test]
fn partially_imported_deposits() {
let epochs = 16;
let initial_deposits_imported = 1024;
let deposits_imported_per_epoch = MAX_DEPOSITS * SLOTS_PER_EPOCH;
let full_import_epoch = 13;
let total_deposits =
initial_deposits_imported + deposits_imported_per_epoch * full_import_epoch;
let eth1data = random_eth1_data(total_deposits);
let checkpoints = random_checkpoints(epochs as usize);
let mut eth1cache = eth1cache();
for epoch in 0..epochs {
assert_eq!(
eth1cache.by_checkpoint().len(),
cmp::min(epoch as usize, DEFAULT_ETH1_CACHE_SIZE),
"Unexpected cache size"
);
let checkpoint = checkpoints
.get(epoch as usize)
.expect("should get checkpoint");
let deposits_imported = cmp::min(
total_deposits,
initial_deposits_imported + deposits_imported_per_epoch * epoch,
);
eth1cache.insert(
*checkpoint,
Eth1FinalizationData {
eth1_data: eth1data.clone(),
eth1_deposit_index: deposits_imported,
},
);
if epoch >= 4 {
let finalized_epoch = epoch - 4;
let finalized_checkpoint = checkpoints
.get(finalized_epoch as usize)
.expect("should get finalized checkpoint");
if finalized_epoch < full_import_epoch {
assert_eq!(
eth1cache.finalize(finalized_checkpoint),
None,
"Deposits not fully finalized so cache should return no Eth1Data",
);
assert_eq!(
eth1cache.pending_eth1().len(),
1,
"Deposits not fully finalized. Pending eth1 cache should have 1 entry"
);
} else {
assert_eq!(
eth1cache.finalize(finalized_checkpoint),
Some(eth1data.clone()),
"Deposits fully imported and finalized. Cache should return Eth1Data. finalized_deposits[{}]",
(initial_deposits_imported + deposits_imported_per_epoch * finalized_epoch),
);
assert!(
eth1cache.pending_eth1().is_empty(),
"Deposits fully imported and finalized. Pending cache should be empty"
);
}
}
}
}
#[test]
fn fork_at_epoch_boundary() {
let epochs = 12;
let deposits_imported = 128;
let eth1data = random_eth1_data(deposits_imported);
let checkpoints = random_checkpoints(epochs as usize);
let mut forks = HashMap::new();
let mut eth1cache = eth1cache();
for epoch in 0..epochs {
assert_eq!(
eth1cache.by_checkpoint().len(),
cmp::min(epoch as usize, DEFAULT_ETH1_CACHE_SIZE),
"Unexpected cache size"
);
let checkpoint = checkpoints
.get(epoch as usize)
.expect("should get checkpoint");
eth1cache.insert(
*checkpoint,
Eth1FinalizationData {
eth1_data: eth1data.clone(),
eth1_deposit_index: deposits_imported,
},
);
// lets put a fork at every third epoch
if epoch % 3 == 0 {
let fork = random_checkpoint(epoch);
eth1cache.insert(
fork,
Eth1FinalizationData {
eth1_data: eth1data.clone(),
eth1_deposit_index: deposits_imported,
},
);
forks.insert(epoch as usize, fork);
}
assert!(
eth1cache.pending_eth1().is_empty(),
"Deposits are fully imported so pending cache should be empty"
);
if epoch >= 4 {
let finalized_epoch = (epoch - 4) as usize;
let finalized_checkpoint = if finalized_epoch % 3 == 0 {
forks.get(&finalized_epoch).expect("should get fork")
} else {
checkpoints
.get(finalized_epoch)
.expect("should get checkpoint")
};
assert_eq!(
eth1cache.finalize(finalized_checkpoint),
Some(eth1data.clone()),
"Should have cache hit"
);
if finalized_epoch >= 3 {
let dropped_epoch = finalized_epoch - 3;
if let Some(dropped_checkpoint) = forks.get(&dropped_epoch) {
// got checkpoint for an old fork that should no longer
// be in the cache because it is from too long ago
assert_eq!(
eth1cache.finalize(dropped_checkpoint),
None,
"Should have cache miss"
);
}
}
}
}
}
#[test]
fn massive_deposit_queue() {
// Simulating a situation where deposits don't get imported within an eth1 voting period
let eth1_voting_periods = 8;
let initial_deposits_imported = 1024;
let deposits_imported_per_epoch = MAX_DEPOSITS * SLOTS_PER_EPOCH;
let initial_deposit_queue =
deposits_imported_per_epoch * EPOCHS_PER_ETH1_VOTING_PERIOD * 2 + 32;
let new_deposits_per_voting_period =
EPOCHS_PER_ETH1_VOTING_PERIOD * deposits_imported_per_epoch / 2;
let mut epoch_data = BTreeMap::new();
let mut eth1s_by_count = BTreeMap::new();
let mut eth1cache = eth1cache();
let mut last_period_deposits = initial_deposits_imported;
for period in 0..eth1_voting_periods {
let period_deposits = initial_deposits_imported
+ initial_deposit_queue
+ period * new_deposits_per_voting_period;
let period_eth1_data = random_eth1_data(period_deposits);
eth1s_by_count.insert(period_eth1_data.deposit_count, period_eth1_data.clone());
for epoch_mod_period in 0..EPOCHS_PER_ETH1_VOTING_PERIOD {
let epoch = period * EPOCHS_PER_ETH1_VOTING_PERIOD + epoch_mod_period;
let checkpoint = random_checkpoint(epoch);
let deposits_imported = cmp::min(
period_deposits,
last_period_deposits + deposits_imported_per_epoch * epoch_mod_period,
);
eth1cache.insert(
checkpoint,
Eth1FinalizationData {
eth1_data: period_eth1_data.clone(),
eth1_deposit_index: deposits_imported,
},
);
epoch_data.insert(epoch, (checkpoint, deposits_imported));
if epoch >= 4 {
let finalized_epoch = epoch - 4;
let (finalized_checkpoint, finalized_deposits) = epoch_data
.get(&finalized_epoch)
.expect("should get epoch data");
let pending_eth1s = eth1s_by_count.range((finalized_deposits + 1)..).count();
let last_finalized_eth1 = eth1s_by_count
.range(0..(finalized_deposits + 1))
.map(|(_, eth1)| eth1)
.last()
.cloned();
assert_eq!(
eth1cache.finalize(finalized_checkpoint),
last_finalized_eth1,
"finalized checkpoint mismatch",
);
assert_eq!(
eth1cache.pending_eth1().len(),
pending_eth1s,
"pending eth1 mismatch"
);
}
}
// remove unneeded stuff from old epochs
while epoch_data.len() > DEFAULT_ETH1_CACHE_SIZE {
let oldest_stored_epoch = epoch_data
.keys()
.next()
.cloned()
.expect("should get oldest epoch");
epoch_data.remove(&oldest_stored_epoch);
}
last_period_deposits = period_deposits;
}
}
}

View File

@@ -1,7 +1,7 @@
pub use eth2::types::{EventKind, SseBlock, SseFinalizedCheckpoint, SseHead};
use slog::{trace, Logger};
use tokio::sync::broadcast;
use tokio::sync::broadcast::{Receiver, Sender, error::SendError};
use tracing::trace;
use tokio::sync::broadcast::{error::SendError, Receiver, Sender};
use types::EthSpec;
const DEFAULT_CHANNEL_CAPACITY: usize = 16;
@@ -11,7 +11,6 @@ pub struct ServerSentEventHandler<E: EthSpec> {
single_attestation_tx: Sender<EventKind<E>>,
block_tx: Sender<EventKind<E>>,
blob_sidecar_tx: Sender<EventKind<E>>,
data_column_sidecar_tx: Sender<EventKind<E>>,
finalized_tx: Sender<EventKind<E>>,
head_tx: Sender<EventKind<E>>,
exit_tx: Sender<EventKind<E>>,
@@ -21,28 +20,27 @@ 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>>,
block_gossip_tx: Sender<EventKind<E>>,
execution_payload_tx: Sender<EventKind<E>>,
execution_payload_gossip_tx: Sender<EventKind<E>>,
execution_payload_available_tx: Sender<EventKind<E>>,
execution_payload_bid_tx: Sender<EventKind<E>>,
payload_attestation_message_tx: Sender<EventKind<E>>,
log: Logger,
}
impl<E: EthSpec> ServerSentEventHandler<E> {
pub fn new(capacity_multiplier: usize) -> Self {
Self::new_with_capacity(capacity_multiplier.saturating_mul(DEFAULT_CHANNEL_CAPACITY))
pub fn new(log: Logger, capacity_multiplier: usize) -> Self {
Self::new_with_capacity(
log,
capacity_multiplier.saturating_mul(DEFAULT_CHANNEL_CAPACITY),
)
}
pub fn new_with_capacity(capacity: usize) -> Self {
pub fn new_with_capacity(log: Logger, capacity: usize) -> Self {
let (attestation_tx, _) = broadcast::channel(capacity);
let (single_attestation_tx, _) = broadcast::channel(capacity);
let (block_tx, _) = broadcast::channel(capacity);
let (blob_sidecar_tx, _) = broadcast::channel(capacity);
let (data_column_sidecar_tx, _) = broadcast::channel(capacity);
let (finalized_tx, _) = broadcast::channel(capacity);
let (head_tx, _) = broadcast::channel(capacity);
let (exit_tx, _) = broadcast::channel(capacity);
@@ -52,22 +50,17 @@ 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);
let (block_gossip_tx, _) = broadcast::channel(capacity);
let (execution_payload_tx, _) = broadcast::channel(capacity);
let (execution_payload_gossip_tx, _) = broadcast::channel(capacity);
let (execution_payload_available_tx, _) = broadcast::channel(capacity);
let (execution_payload_bid_tx, _) = broadcast::channel(capacity);
let (payload_attestation_message_tx, _) = broadcast::channel(capacity);
Self {
attestation_tx,
single_attestation_tx,
block_tx,
blob_sidecar_tx,
data_column_sidecar_tx,
finalized_tx,
head_tx,
exit_tx,
@@ -77,24 +70,22 @@ 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,
block_gossip_tx,
execution_payload_tx,
execution_payload_gossip_tx,
execution_payload_available_tx,
execution_payload_bid_tx,
payload_attestation_message_tx,
log,
}
}
pub fn register(&self, kind: EventKind<E>) {
let log_count = |name, count| {
trace!(
kind = name,
receiver_count = count,
"Registering server-sent event"
self.log,
"Registering server-sent event";
"kind" => name,
"receiver_count" => count
);
};
let result = match &kind {
@@ -114,10 +105,6 @@ impl<E: EthSpec> ServerSentEventHandler<E> {
.blob_sidecar_tx
.send(kind)
.map(|count| log_count("blob sidecar", count)),
EventKind::DataColumnSidecar(_) => self
.data_column_sidecar_tx
.send(kind)
.map(|count| log_count("data_column_sidecar", count)),
EventKind::FinalizedCheckpoint(_) => self
.finalized_tx
.send(kind)
@@ -154,6 +141,10 @@ 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)
@@ -170,29 +161,9 @@ impl<E: EthSpec> ServerSentEventHandler<E> {
.block_gossip_tx
.send(kind)
.map(|count| log_count("block gossip", count)),
EventKind::ExecutionPayload(_) => self
.execution_payload_tx
.send(kind)
.map(|count| log_count("execution payload", count)),
EventKind::ExecutionPayloadGossip(_) => self
.execution_payload_gossip_tx
.send(kind)
.map(|count| log_count("execution payload gossip", count)),
EventKind::ExecutionPayloadAvailable(_) => self
.execution_payload_available_tx
.send(kind)
.map(|count| log_count("execution payload available", count)),
EventKind::ExecutionPayloadBid(_) => self
.execution_payload_bid_tx
.send(kind)
.map(|count| log_count("execution payload bid", count)),
EventKind::PayloadAttestationMessage(_) => self
.payload_attestation_message_tx
.send(kind)
.map(|count| log_count("payload attestation message", count)),
};
if let Err(SendError(event)) = result {
trace!(?event, "No receivers registered to listen for event");
trace!(self.log, "No receivers registered to listen for event"; "event" => ?event);
}
}
@@ -212,10 +183,6 @@ impl<E: EthSpec> ServerSentEventHandler<E> {
self.blob_sidecar_tx.subscribe()
}
pub fn subscribe_data_column_sidecar(&self) -> Receiver<EventKind<E>> {
self.data_column_sidecar_tx.subscribe()
}
pub fn subscribe_finalized(&self) -> Receiver<EventKind<E>> {
self.finalized_tx.subscribe()
}
@@ -252,6 +219,10 @@ 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()
}
@@ -268,26 +239,6 @@ impl<E: EthSpec> ServerSentEventHandler<E> {
self.block_gossip_tx.subscribe()
}
pub fn subscribe_execution_payload(&self) -> Receiver<EventKind<E>> {
self.execution_payload_tx.subscribe()
}
pub fn subscribe_execution_payload_gossip(&self) -> Receiver<EventKind<E>> {
self.execution_payload_gossip_tx.subscribe()
}
pub fn subscribe_execution_payload_available(&self) -> Receiver<EventKind<E>> {
self.execution_payload_available_tx.subscribe()
}
pub fn subscribe_execution_payload_bid(&self) -> Receiver<EventKind<E>> {
self.execution_payload_bid_tx.subscribe()
}
pub fn subscribe_payload_attestation_message(&self) -> Receiver<EventKind<E>> {
self.payload_attestation_message_tx.subscribe()
}
pub fn has_attestation_subscribers(&self) -> bool {
self.attestation_tx.receiver_count() > 0
}
@@ -304,10 +255,6 @@ impl<E: EthSpec> ServerSentEventHandler<E> {
self.blob_sidecar_tx.receiver_count() > 0
}
pub fn has_data_column_sidecar_subscribers(&self) -> bool {
self.data_column_sidecar_tx.receiver_count() > 0
}
pub fn has_finalized_subscribers(&self) -> bool {
self.finalized_tx.receiver_count() > 0
}
@@ -336,6 +283,10 @@ 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
}
@@ -351,24 +302,4 @@ impl<E: EthSpec> ServerSentEventHandler<E> {
pub fn has_block_gossip_subscribers(&self) -> bool {
self.block_gossip_tx.receiver_count() > 0
}
pub fn has_execution_payload_subscribers(&self) -> bool {
self.execution_payload_tx.receiver_count() > 0
}
pub fn has_execution_payload_gossip_subscribers(&self) -> bool {
self.execution_payload_gossip_tx.receiver_count() > 0
}
pub fn has_execution_payload_available_subscribers(&self) -> bool {
self.execution_payload_available_tx.receiver_count() > 0
}
pub fn has_execution_payload_bid_subscribers(&self) -> bool {
self.execution_payload_bid_tx.receiver_count() > 0
}
pub fn has_payload_attestation_message_subscribers(&self) -> bool {
self.payload_attestation_message_tx.receiver_count() > 0
}
}

View File

@@ -12,25 +12,32 @@ use crate::{
ExecutionPayloadError,
};
use execution_layer::{
BlockProposalContentsType, BuilderParams, NewPayloadRequest, PayloadAttributes,
PayloadParameters, PayloadStatus,
BlockProposalContents, BlockProposalContentsType, BuilderParams, NewPayloadRequest,
PayloadAttributes, PayloadParameters, PayloadStatus,
};
use fork_choice::{InvalidationOperation, PayloadVerificationStatus};
use proto_array::{Block as ProtoBlock, ExecutionStatus};
use slog::{debug, warn};
use slot_clock::SlotClock;
use state_processing::per_block_processing::{
compute_timestamp_at_slot, get_expected_withdrawals, is_execution_enabled,
partially_verify_execution_payload,
is_merge_transition_complete, partially_verify_execution_payload,
};
use std::sync::Arc;
use tokio::task::JoinHandle;
use tracing::{Instrument, debug_span, warn};
use types::execution::BlockProductionVersion;
use tree_hash::TreeHash;
use types::payload::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.
@@ -55,10 +62,7 @@ impl<T: BeaconChainTypes> PayloadNotifier<T> {
state: &BeaconState<T::EthSpec>,
notify_execution_layer: NotifyExecutionLayer,
) -> Result<Self, BlockError> {
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()) {
let payload_verification_status = 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
@@ -81,10 +85,11 @@ impl<T: BeaconChainTypes> PayloadNotifier<T> {
block_message.try_into()?;
if let Err(e) = new_payload_request.perform_optimistic_sync_verifications() {
warn!(
block_number = ?block_message.execution_payload().map(|payload| payload.block_number()),
info = "you can silence this warning with --disable-optimistic-finalized-sync",
error = ?e,
"Falling back to slow block hash verification"
chain.log,
"Falling back to slow block hash verification";
"block_number" => ?block_message.execution_payload().map(|payload| payload.block_number()),
"info" => "you can silence this warning with --disable-optimistic-finalized-sync",
"error" => ?e,
);
None
} else {
@@ -108,18 +113,12 @@ 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().slot(),
self.block.message().parent_root(),
self.block.message().try_into()?,
)
.await
notify_new_payload(&self.chain, self.block.message()).await
}
}
}
/// Verify that `execution_payload` is considered valid by an execution
/// Verify that `execution_payload` contained by `block` is considered valid by an execution
/// engine.
///
/// ## Specification
@@ -128,21 +127,17 @@ 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
pub async fn notify_new_payload<T: BeaconChainTypes>(
async fn notify_new_payload<T: BeaconChainTypes>(
chain: &Arc<BeaconChain<T>>,
slot: Slot,
parent_beacon_block_root: Hash256,
new_payload_request: NewPayloadRequest<'_, T::EthSpec>,
block: BeaconBlockRef<'_, T::EthSpec>,
) -> Result<PayloadVerificationStatus, BlockError> {
let execution_layer = chain
.execution_layer
.as_ref()
.ok_or(ExecutionPayloadError::NoExecutionConnection)?;
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;
let execution_block_hash = block.execution_payload()?.block_hash();
let new_payload_response = execution_layer.notify_new_payload(block.try_into()?).await;
match new_payload_response {
Ok(status) => match status {
@@ -155,12 +150,16 @@ pub async fn notify_new_payload<T: BeaconChainTypes>(
ref validation_error,
} => {
warn!(
?validation_error,
?latest_valid_hash,
?execution_block_hash,
%slot,
method = "new_payload",
"Invalid execution payload"
chain.log,
"Invalid execution payload";
"validation_error" => ?validation_error,
"latest_valid_hash" => ?latest_valid_hash,
"execution_block_hash" => ?execution_block_hash,
"root" => ?block.tree_hash_root(),
"graffiti" => block.body().graffiti().as_utf8_lossy(),
"proposer_index" => block.proposer_index(),
"slot" => block.slot(),
"method" => "new_payload",
);
// Only trigger payload invalidation in fork choice if the
@@ -181,9 +180,11 @@ pub 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: parent_beacon_block_root,
head_block_root: latest_root,
always_invalidate_head: false,
latest_valid_ancestor: latest_valid_hash,
})
@@ -196,11 +197,15 @@ pub async fn notify_new_payload<T: BeaconChainTypes>(
ref validation_error,
} => {
warn!(
?validation_error,
?execution_block_hash,
%slot,
method = "new_payload",
"Invalid execution payload block hash"
chain.log,
"Invalid execution payload block hash";
"validation_error" => ?validation_error,
"execution_block_hash" => ?execution_block_hash,
"root" => ?block.tree_hash_root(),
"graffiti" => block.body().graffiti().as_utf8_lossy(),
"proposer_index" => block.proposer_index(),
"slot" => block.slot(),
"method" => "new_payload",
);
// Returning an error here should be sufficient to invalidate the block. We have no
@@ -213,6 +218,79 @@ pub 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!(
chain.log,
"Optimistically importing merge transition block";
"block_hash" => ?execution_payload.parent_hash(),
"msg" => "the terminal block/parent was unavailable"
);
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>(
@@ -220,40 +298,34 @@ 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() {
// 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.
// 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.
let parent_has_execution = match parent_block.execution_status {
// Parent has valid or optimistic execution status.
let is_merge_transition_complete = match parent_block.execution_status {
// Optimistically declare that an "unknown" status block has completed the merge.
ExecutionStatus::Valid(_) | ExecutionStatus::Optimistic(_) => true,
// Pre-merge blocks have irrelevant execution status.
// It's impossible for an irrelevant block to have completed the merge. It is pre-merge
// by definition.
ExecutionStatus::Irrelevant(_) => false,
// If the parent has an invalid payload then it's impossible to build a valid block upon
// it. Reject the block.
ExecutionStatus::Invalid(_) => {
return Err(BlockError::ParentExecutionPayloadInvalid {
parent_root: parent_block.root,
});
})
}
};
if parent_has_execution || !execution_payload.is_default_with_empty_roots() {
if is_merge_transition_complete || !execution_payload.is_default_with_empty_roots() {
let expected_timestamp = chain
.slot_clock
.start_of(block.slot())
.map(|d| d.as_secs())
.ok_or(BlockError::BeaconChainError(Box::new(
.ok_or(BlockError::BeaconChainError(
BeaconChainError::UnableToComputeTimeAtSlot,
)))?;
))?;
// The block's execution payload timestamp is correct with respect to the slot
if execution_payload.timestamp() != expected_timestamp {
@@ -295,6 +367,7 @@ 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)?;
@@ -302,7 +375,7 @@ pub fn get_execution_payload<T: BeaconChainTypes>(
let latest_execution_payload_header_block_hash = latest_execution_payload_header.block_hash();
let latest_execution_payload_header_gas_limit = latest_execution_payload_header.gas_limit();
let withdrawals = if state.fork_name_unchecked().capella_enabled() {
Some(Withdrawals::<T::EthSpec>::from(get_expected_withdrawals(state, spec)?).into())
Some(get_expected_withdrawals(state, spec)?.0.into())
} else {
None
};
@@ -321,6 +394,7 @@ pub fn get_execution_payload<T: BeaconChainTypes>(
async move {
prepare_execution_payload::<T>(
&chain,
is_merge_transition_complete,
timestamp,
random,
proposer_index,
@@ -333,16 +407,17 @@ pub fn get_execution_payload<T: BeaconChainTypes>(
block_production_version,
)
.await
}
.instrument(debug_span!("prepare_execution_payload")),
"prepare_execution_payload",
},
"get_execution_payload",
)
.ok_or(BlockProductionError::ShuttingDown)?;
Ok(join_handle)
}
/// Prepares an execution payload (pre-gloas) for inclusion in a block.
/// 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
///
@@ -357,6 +432,7 @@ 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,
@@ -371,21 +447,50 @@ 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);
if fork.gloas_enabled() {
return Err(BlockProductionError::InvalidBlockVariant(
"Called pre-gloas prepare_execution_payload on a gloas block".to_string(),
));
}
let execution_layer = chain
.execution_layer
.as_ref()
.ok_or(BlockProductionError::ExecutionLayerMissing)?;
let parent_hash = latest_execution_payload_header_block_hash;
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
};
// Try to obtain the fork choice update parameters from the cached head.
//
@@ -402,28 +507,24 @@ where
},
"prepare_execution_payload_forkchoice_update_params",
)
.instrument(debug_span!("forkchoice_update_params"))
.await
.map_err(|e| BlockProductionError::BeaconChain(Box::new(e)))?;
.map_err(BlockProductionError::BeaconChain)?;
let suggested_fee_recipient = execution_layer
.get_suggested_fee_recipient(proposer_index)
.await;
let payload_attributes = PayloadAttributes::new(
timestamp,
random,
suggested_fee_recipient,
withdrawals,
parent_beacon_block_root,
None,
None,
);
let target_gas_limit = execution_layer.get_proposer_gas_limit(proposer_index).await;
let payload_parameters = PayloadParameters {
parent_hash,
parent_gas_limit: Some(latest_execution_payload_header_gas_limit),
parent_gas_limit: latest_execution_payload_header_gas_limit,
proposer_gas_limit: target_gas_limit,
payload_attributes: &payload_attributes,
forkchoice_update_params: &forkchoice_update_params,

View File

@@ -0,0 +1,329 @@
//! This module implements an optimisation to fetch blobs via JSON-RPC from the EL.
//! If a blob has already been seen in the public mempool, then it is often unnecessary to wait for
//! it to arrive on P2P gossip. This PR uses a new JSON-RPC method (`engine_getBlobsV1`) which
//! allows the CL to load the blobs quickly from the EL's blob pool.
//!
//! Once the node fetches the blobs from EL, it then publishes the remaining blobs that it hasn't seen
//! on P2P gossip to the network. From PeerDAS onwards, together with the increase in blob count,
//! broadcasting blobs requires a much higher bandwidth, and is only done by high capacity
//! supernodes.
use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob};
use crate::kzg_utils::blobs_to_data_column_sidecars;
use crate::observed_data_sidecars::DoNotObserve;
use crate::{metrics, AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError};
use execution_layer::json_structures::BlobAndProofV1;
use execution_layer::Error as ExecutionLayerError;
use metrics::{inc_counter, inc_counter_by, TryExt};
use slog::{debug, error, o, Logger};
use ssz_types::FixedVector;
use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash;
use std::sync::Arc;
use tokio::sync::oneshot;
use types::blob_sidecar::{BlobSidecarError, FixedBlobSidecarList};
use types::{
BeaconStateError, BlobSidecar, ChainSpec, DataColumnSidecar, DataColumnSidecarList, EthSpec,
FullPayload, Hash256, SignedBeaconBlock, SignedBeaconBlockHeader,
};
pub enum BlobsOrDataColumns<T: BeaconChainTypes> {
Blobs(Vec<GossipVerifiedBlob<T, DoNotObserve>>),
DataColumns(DataColumnSidecarList<T::EthSpec>),
}
#[derive(Debug)]
pub enum FetchEngineBlobError {
BeaconStateError(BeaconStateError),
BlobProcessingError(BlockError),
BlobSidecarError(BlobSidecarError),
ExecutionLayerMissing,
InternalError(String),
GossipBlob(GossipBlobError),
RequestFailed(ExecutionLayerError),
RuntimeShutdown,
}
/// Fetches blobs from the EL mempool and processes them. It also broadcasts unseen blobs or
/// data columns (PeerDAS onwards) to the network, using the supplied `publish_fn`.
pub async fn fetch_and_process_engine_blobs<T: BeaconChainTypes>(
chain: Arc<BeaconChain<T>>,
block_root: Hash256,
block: Arc<SignedBeaconBlock<T::EthSpec, FullPayload<T::EthSpec>>>,
publish_fn: impl Fn(BlobsOrDataColumns<T>) + Send + 'static,
) -> Result<Option<AvailabilityProcessingStatus>, FetchEngineBlobError> {
let block_root_str = format!("{:?}", block_root);
let log = chain
.log
.new(o!("service" => "fetch_engine_blobs", "block_root" => block_root_str));
let versioned_hashes = if let Some(kzg_commitments) = block
.message()
.body()
.blob_kzg_commitments()
.ok()
.filter(|blobs| !blobs.is_empty())
{
kzg_commitments
.iter()
.map(kzg_commitment_to_versioned_hash)
.collect::<Vec<_>>()
} else {
debug!(
log,
"Fetch blobs not triggered - none required";
);
return Ok(None);
};
let num_expected_blobs = versioned_hashes.len();
let execution_layer = chain
.execution_layer
.as_ref()
.ok_or(FetchEngineBlobError::ExecutionLayerMissing)?;
debug!(
log,
"Fetching blobs from the EL";
"num_expected_blobs" => num_expected_blobs,
);
let response = execution_layer
.get_blobs(versioned_hashes)
.await
.map_err(FetchEngineBlobError::RequestFailed)?;
if response.is_empty() || response.iter().all(|opt| opt.is_none()) {
debug!(
log,
"No blobs fetched from the EL";
"num_expected_blobs" => num_expected_blobs,
);
inc_counter(&metrics::BLOBS_FROM_EL_MISS_TOTAL);
return Ok(None);
} else {
inc_counter(&metrics::BLOBS_FROM_EL_HIT_TOTAL);
}
let (signed_block_header, kzg_commitments_proof) = block
.signed_block_header_and_kzg_commitments_proof()
.map_err(FetchEngineBlobError::BeaconStateError)?;
let fixed_blob_sidecar_list = build_blob_sidecars(
&block,
response,
signed_block_header,
&kzg_commitments_proof,
&chain.spec,
)?;
let num_fetched_blobs = fixed_blob_sidecar_list
.iter()
.filter(|b| b.is_some())
.count();
inc_counter_by(
&metrics::BLOBS_FROM_EL_EXPECTED_TOTAL,
num_expected_blobs as u64,
);
inc_counter_by(
&metrics::BLOBS_FROM_EL_RECEIVED_TOTAL,
num_fetched_blobs as u64,
);
// Gossip verify blobs before publishing. This prevents blobs with invalid KZG proofs from
// the EL making it into the data availability checker. We do not immediately add these
// blobs to the observed blobs/columns cache because we want to allow blobs/columns to arrive on gossip
// and be accepted (and propagated) while we are waiting to publish. Just before publishing
// we will observe the blobs/columns and only proceed with publishing if they are not yet seen.
let blobs_to_import_and_publish = fixed_blob_sidecar_list
.iter()
.filter_map(|opt_blob| {
let blob = opt_blob.as_ref()?;
match GossipVerifiedBlob::<T, DoNotObserve>::new(blob.clone(), blob.index, &chain) {
Ok(verified) => Some(Ok(verified)),
// Ignore already seen blobs.
Err(GossipBlobError::RepeatBlob { .. }) => None,
Err(e) => Some(Err(e)),
}
})
.collect::<Result<Vec<_>, _>>()
.map_err(FetchEngineBlobError::GossipBlob)?;
let peer_das_enabled = chain.spec.is_peer_das_enabled_for_epoch(block.epoch());
let data_columns_receiver_opt = if peer_das_enabled {
// Partial blobs response isn't useful for PeerDAS, so we don't bother building and publishing data columns.
if num_fetched_blobs != num_expected_blobs {
debug!(
log,
"Not all blobs fetched from the EL";
"info" => "Unable to compute data columns",
"num_fetched_blobs" => num_fetched_blobs,
"num_expected_blobs" => num_expected_blobs,
);
return Ok(None);
}
if chain
.canonical_head
.fork_choice_read_lock()
.contains_block(&block_root)
{
// Avoid computing columns if block has already been imported.
debug!(
log,
"Ignoring EL blobs response";
"info" => "block has already been imported",
);
return Ok(None);
}
let data_columns_receiver = spawn_compute_and_publish_data_columns_task(
&chain,
block.clone(),
fixed_blob_sidecar_list.clone(),
publish_fn,
log.clone(),
);
Some(data_columns_receiver)
} else {
if !blobs_to_import_and_publish.is_empty() {
publish_fn(BlobsOrDataColumns::Blobs(blobs_to_import_and_publish));
}
None
};
debug!(
log,
"Processing engine blobs";
"num_fetched_blobs" => num_fetched_blobs,
);
let availability_processing_status = chain
.process_engine_blobs(
block.slot(),
block_root,
fixed_blob_sidecar_list.clone(),
data_columns_receiver_opt,
)
.await
.map_err(FetchEngineBlobError::BlobProcessingError)?;
Ok(Some(availability_processing_status))
}
/// Spawn a blocking task here for long computation tasks, so it doesn't block processing, and it
/// allows blobs / data columns to propagate without waiting for processing.
///
/// An `mpsc::Sender` is then used to send the produced data columns to the `beacon_chain` for it
/// to be persisted, **after** the block is made attestable.
///
/// The reason for doing this is to make the block available and attestable as soon as possible,
/// while maintaining the invariant that block and data columns are persisted atomically.
fn spawn_compute_and_publish_data_columns_task<T: BeaconChainTypes>(
chain: &Arc<BeaconChain<T>>,
block: Arc<SignedBeaconBlock<T::EthSpec, FullPayload<T::EthSpec>>>,
blobs: FixedBlobSidecarList<T::EthSpec>,
publish_fn: impl Fn(BlobsOrDataColumns<T>) + Send + 'static,
log: Logger,
) -> oneshot::Receiver<Vec<Arc<DataColumnSidecar<T::EthSpec>>>> {
let chain_cloned = chain.clone();
let (data_columns_sender, data_columns_receiver) = oneshot::channel();
chain.task_executor.spawn_blocking(
move || {
let mut timer = metrics::start_timer_vec(
&metrics::DATA_COLUMN_SIDECAR_COMPUTATION,
&[&blobs.len().to_string()],
);
let blob_refs = blobs
.iter()
.filter_map(|b| b.as_ref().map(|b| &b.blob))
.collect::<Vec<_>>();
let data_columns_result = blobs_to_data_column_sidecars(
&blob_refs,
&block,
&chain_cloned.kzg,
&chain_cloned.spec,
)
.discard_timer_on_break(&mut timer);
drop(timer);
let all_data_columns = match data_columns_result {
Ok(d) => d,
Err(e) => {
error!(
log,
"Failed to build data column sidecars from blobs";
"error" => ?e
);
return;
}
};
if data_columns_sender.send(all_data_columns.clone()).is_err() {
// Data column receiver have been dropped - block may have already been imported.
// This race condition exists because gossip columns may arrive and trigger block
// import during the computation. Here we just drop the computed columns.
debug!(
log,
"Failed to send computed data columns";
);
return;
};
// At the moment non supernodes are not required to publish any columns.
// TODO(das): we could experiment with having full nodes publish their custodied
// columns here.
if !chain_cloned.data_availability_checker.is_supernode() {
return;
}
publish_fn(BlobsOrDataColumns::DataColumns(all_data_columns));
},
"compute_and_publish_data_columns",
);
data_columns_receiver
}
fn build_blob_sidecars<E: EthSpec>(
block: &Arc<SignedBeaconBlock<E, FullPayload<E>>>,
response: Vec<Option<BlobAndProofV1<E>>>,
signed_block_header: SignedBeaconBlockHeader,
kzg_commitments_inclusion_proof: &FixedVector<Hash256, E::KzgCommitmentsInclusionProofDepth>,
spec: &ChainSpec,
) -> Result<FixedBlobSidecarList<E>, FetchEngineBlobError> {
let epoch = block.epoch();
let mut fixed_blob_sidecar_list =
FixedBlobSidecarList::default(spec.max_blobs_per_block(epoch) as usize);
for (index, blob_and_proof) in response
.into_iter()
.enumerate()
.filter_map(|(i, opt_blob)| Some((i, opt_blob?)))
{
match BlobSidecar::new_with_existing_proof(
index,
blob_and_proof.blob,
block,
signed_block_header.clone(),
kzg_commitments_inclusion_proof,
blob_and_proof.proof,
) {
Ok(blob) => {
if let Some(blob_mut) = fixed_blob_sidecar_list.get_mut(index) {
*blob_mut = Some(Arc::new(blob));
} else {
return Err(FetchEngineBlobError::InternalError(format!(
"Blobs from EL contains blob with invalid index {index}"
)));
}
}
Err(e) => {
return Err(FetchEngineBlobError::BlobSidecarError(e));
}
}
}
Ok(fixed_blob_sidecar_list)
}

View File

@@ -1,162 +0,0 @@
use crate::fetch_blobs::{EngineGetBlobsOutput, FetchEngineBlobError};
use crate::observed_data_sidecars::ObservationKey;
use crate::partial_data_column_assembler::PartialDataColumnAssembler;
use crate::{AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes};
use execution_layer::json_structures::{BlobAndProofV1, BlobAndProofV2, BlobAndProofV3};
use kzg::Kzg;
#[cfg(test)]
use mockall::automock;
use std::collections::HashSet;
use std::sync::Arc;
use task_executor::TaskExecutor;
use types::{ChainSpec, ColumnIndex, Hash256, Slot};
/// An adapter to the `BeaconChain` functionalities to remove `BeaconChain` from direct dependency to enable testing fetch blobs logic.
pub(crate) struct FetchBlobsBeaconAdapter<T: BeaconChainTypes> {
chain: Arc<BeaconChain<T>>,
spec: Arc<ChainSpec>,
}
#[cfg_attr(test, automock, allow(dead_code))]
impl<T: BeaconChainTypes> FetchBlobsBeaconAdapter<T> {
pub(crate) fn new(chain: Arc<BeaconChain<T>>) -> Self {
let spec = chain.spec.clone();
Self { chain, spec }
}
pub(crate) fn spec(&self) -> &Arc<ChainSpec> {
&self.spec
}
pub(crate) fn kzg(&self) -> &Arc<Kzg> {
&self.chain.kzg
}
pub(crate) fn executor(&self) -> &TaskExecutor {
&self.chain.task_executor
}
pub(crate) fn partial_assembler(&self) -> Option<Arc<PartialDataColumnAssembler<T::EthSpec>>> {
self.chain
.data_availability_checker
.partial_assembler()
.cloned()
}
pub(crate) async fn get_blobs_v1(
&self,
versioned_hashes: Vec<Hash256>,
) -> Result<Vec<Option<BlobAndProofV1<T::EthSpec>>>, FetchEngineBlobError> {
let execution_layer = self
.chain
.execution_layer
.as_ref()
.ok_or(FetchEngineBlobError::ExecutionLayerMissing)?;
execution_layer
.get_blobs_v1(versioned_hashes)
.await
.map_err(FetchEngineBlobError::RequestFailed)
}
pub(crate) async fn get_blobs_v2(
&self,
versioned_hashes: Vec<Hash256>,
) -> Result<Option<Vec<BlobAndProofV2<T::EthSpec>>>, FetchEngineBlobError> {
let execution_layer = self
.chain
.execution_layer
.as_ref()
.ok_or(FetchEngineBlobError::ExecutionLayerMissing)?;
execution_layer
.get_blobs_v2(versioned_hashes)
.await
.map_err(FetchEngineBlobError::RequestFailed)
}
pub(crate) async fn get_blobs_v3(
&self,
versioned_hashes: Vec<Hash256>,
) -> Result<Option<Vec<BlobAndProofV3<T::EthSpec>>>, FetchEngineBlobError> {
let execution_layer = self
.chain
.execution_layer
.as_ref()
.ok_or(FetchEngineBlobError::ExecutionLayerMissing)?;
execution_layer
.get_blobs_v3(versioned_hashes)
.await
.map_err(FetchEngineBlobError::RequestFailed)
}
pub(crate) fn blobs_known_for_observation_key(
&self,
observation_key: ObservationKey,
) -> Option<HashSet<u64>> {
self.chain
.observed_blob_sidecars
.read()
.known_for_observation_key(&observation_key)
.cloned()
}
pub(crate) fn data_column_known_for_observation_key(
&self,
observation_key: ObservationKey,
) -> Option<HashSet<ColumnIndex>> {
self.chain
.observed_column_sidecars
.read()
.known_for_observation_key(&observation_key)
.cloned()
}
pub(crate) fn cached_blob_indexes(&self, block_root: &Hash256) -> Option<Vec<u64>> {
self.chain
.data_availability_checker
.cached_blob_indexes(block_root)
}
pub(crate) fn cached_data_column_indexes(
&self,
block_root: &Hash256,
slot: Slot,
) -> Option<Vec<u64>> {
self.chain.cached_data_column_indexes(block_root, slot)
}
pub(crate) async fn process_engine_blobs(
&self,
slot: Slot,
block_root: Hash256,
blobs: EngineGetBlobsOutput<T>,
) -> Result<AvailabilityProcessingStatus, FetchEngineBlobError> {
self.chain
.process_engine_blobs(slot, block_root, blobs)
.await
.map_err(FetchEngineBlobError::BlobProcessingError)
}
pub(crate) fn fork_choice_contains_block(&self, block_root: &Hash256) -> bool {
self.chain
.canonical_head
.fork_choice_read_lock()
.contains_block(block_root)
}
pub(crate) async fn supports_get_blobs_v3(&self) -> Result<bool, FetchEngineBlobError> {
let execution_layer = self
.chain
.execution_layer
.as_ref()
.ok_or(FetchEngineBlobError::ExecutionLayerMissing)?;
execution_layer
.get_engine_capabilities(None)
.await
.map_err(FetchEngineBlobError::RequestFailed)
.map(|caps| caps.get_blobs_v3)
}
}

View File

@@ -1,490 +0,0 @@
//! This module implements an optimisation to fetch blobs via JSON-RPC from the EL.
//! If a blob has already been seen in the public mempool, then it is often unnecessary to wait for
//! it to arrive on P2P gossip. This PR uses a new JSON-RPC method (`engine_getBlobsV1`) which
//! allows the CL to load the blobs quickly from the EL's blob pool.
//!
//! Once the node fetches the blobs from EL, it then publishes the remaining blobs that it hasn't seen
//! on P2P gossip to the network. From PeerDAS onwards, together with the increase in blob count,
//! broadcasting blobs requires a much higher bandwidth, and is only done by high capacity
//! supernodes.
mod fetch_blobs_beacon_adapter;
#[cfg(test)]
mod tests;
use crate::blob_verification::{GossipBlobError, KzgVerifiedBlob};
use crate::data_column_verification::{
KzgVerifiedCustodyDataColumn, KzgVerifiedCustodyPartialDataColumn, KzgVerifiedPartialDataColumn,
};
#[cfg_attr(test, double)]
use crate::fetch_blobs::fetch_blobs_beacon_adapter::FetchBlobsBeaconAdapter;
use crate::kzg_utils::blobs_to_partial_data_columns;
use crate::observed_data_sidecars::ObservationKey;
use crate::{
AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, BlockError,
metrics,
};
use execution_layer::Error as ExecutionLayerError;
use execution_layer::json_structures::{BlobAndProofV1, BlobAndProofV2, BlobAndProofV3};
use metrics::{TryExt, inc_counter};
#[cfg(test)]
use mockall_double::double;
use slot_clock::timestamp_now;
use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash;
use std::sync::Arc;
use tracing::{debug, instrument, warn};
use types::data::{BlobSidecarError, ColumnIndex, DataColumnSidecarError, PartialDataColumnHeader};
use types::{BeaconStateError, BlobSidecar, EthSpec, Hash256, VersionedHash};
/// Result from engine get blobs to be passed onto `DataAvailabilityChecker` and published to the
/// gossip network. The blobs / data columns have not been marked as observed yet, as they may not
/// be published immediately.
#[derive(Debug)]
pub enum EngineGetBlobsOutput<T: BeaconChainTypes> {
Blobs(Vec<KzgVerifiedBlob<T::EthSpec>>),
/// A filtered list of custody data columns to be imported into the `DataAvailabilityChecker`.
CustodyColumns(Vec<KzgVerifiedCustodyDataColumn<T::EthSpec>>),
}
#[derive(Debug)]
pub enum FetchEngineBlobError {
BeaconStateError(BeaconStateError),
BeaconChainError(Box<BeaconChainError>),
BlobProcessingError(BlockError),
BlobSidecarError(BlobSidecarError),
DataColumnSidecarError(DataColumnSidecarError),
ExecutionLayerMissing,
InternalError(String),
GossipBlob(GossipBlobError),
KzgError(kzg::Error),
RequestFailed(ExecutionLayerError),
RuntimeShutdown,
TokioJoin(tokio::task::JoinError),
}
/// Fetches blobs from the EL mempool and processes them. It also broadcasts unseen blobs or
/// data columns (PeerDAS onwards) to the network, using the supplied `publish_fn`.
#[instrument(skip_all)]
pub async fn fetch_and_process_engine_blobs<T: BeaconChainTypes>(
chain: Arc<BeaconChain<T>>,
block_root: Hash256,
header: Arc<PartialDataColumnHeader<T::EthSpec>>,
custody_columns: &[ColumnIndex],
publish_fn: impl Fn(EngineGetBlobsOutput<T>) + Send + 'static,
) -> Result<Option<AvailabilityProcessingStatus>, FetchEngineBlobError> {
fetch_and_process_engine_blobs_inner(
FetchBlobsBeaconAdapter::new(chain),
block_root,
header,
custody_columns,
publish_fn,
)
.await
}
/// Internal implementation of fetch blobs, which uses `FetchBlobsBeaconAdapter` instead of
/// `BeaconChain` for better testability.
async fn fetch_and_process_engine_blobs_inner<T: BeaconChainTypes>(
chain_adapter: FetchBlobsBeaconAdapter<T>,
block_root: Hash256,
header: Arc<PartialDataColumnHeader<T::EthSpec>>,
custody_columns: &[ColumnIndex],
publish_fn: impl Fn(EngineGetBlobsOutput<T>) + Send + 'static,
) -> Result<Option<AvailabilityProcessingStatus>, FetchEngineBlobError> {
let versioned_hashes = header
.kzg_commitments
.iter()
.map(kzg_commitment_to_versioned_hash)
.collect::<Vec<_>>();
if versioned_hashes.is_empty() {
debug!("Fetch blobs not triggered - none required");
return Ok(None);
};
debug!(
num_expected_blobs = versioned_hashes.len(),
"Fetching blobs from the EL"
);
if chain_adapter
.spec()
.is_peer_das_enabled_for_epoch(header.slot().epoch(T::EthSpec::slots_per_epoch()))
{
fetch_and_process_blobs_v2_or_v3(
chain_adapter,
block_root,
header,
versioned_hashes,
custody_columns,
publish_fn,
)
.await
} else {
fetch_and_process_blobs_v1(
chain_adapter,
block_root,
&header,
versioned_hashes,
publish_fn,
)
.await
}
}
#[instrument(skip_all, level = "debug")]
async fn fetch_and_process_blobs_v1<T: BeaconChainTypes>(
chain_adapter: FetchBlobsBeaconAdapter<T>,
block_root: Hash256,
header: &PartialDataColumnHeader<T::EthSpec>,
versioned_hashes: Vec<VersionedHash>,
publish_fn: impl Fn(EngineGetBlobsOutput<T>) + Send + Sized,
) -> Result<Option<AvailabilityProcessingStatus>, FetchEngineBlobError> {
let num_expected_blobs = versioned_hashes.len();
metrics::observe(&metrics::BLOBS_FROM_EL_EXPECTED, num_expected_blobs as f64);
debug!(num_expected_blobs, "Fetching blobs from the EL");
let response = chain_adapter
.get_blobs_v1(versioned_hashes)
.await
.inspect_err(|_| {
inc_counter(&metrics::BLOBS_FROM_EL_ERROR_TOTAL);
})?;
let num_fetched_blobs = response.iter().filter(|opt| opt.is_some()).count();
metrics::observe(&metrics::BLOBS_FROM_EL_RECEIVED, num_fetched_blobs as f64);
if num_fetched_blobs == 0 {
debug!(num_expected_blobs, "No blobs fetched from the EL");
inc_counter(&metrics::BLOBS_FROM_EL_MISS_TOTAL);
return Ok(None);
} else {
debug!(
num_expected_blobs,
num_fetched_blobs, "Received blobs from the EL"
);
inc_counter(&metrics::BLOBS_FROM_EL_HIT_TOTAL);
}
if chain_adapter.fork_choice_contains_block(&block_root) {
// Avoid computing sidecars if the block has already been imported.
debug!(
info = "block has already been imported",
"Ignoring EL blobs response"
);
return Ok(None);
}
let mut blob_sidecar_list = build_blob_sidecars(header, response)?;
let observation_key = ObservationKey::new_proposer_key(
header.signed_block_header.message.proposer_index,
header.slot(),
);
if let Some(observed_blobs) = chain_adapter.blobs_known_for_observation_key(observation_key) {
blob_sidecar_list.retain(|blob| !observed_blobs.contains(&blob.blob_index()));
if blob_sidecar_list.is_empty() {
debug!(
info = "blobs have already been seen on gossip",
"Ignoring EL blobs response"
);
return Ok(None);
}
}
if let Some(known_blobs) = chain_adapter.cached_blob_indexes(&block_root) {
blob_sidecar_list.retain(|blob| !known_blobs.contains(&blob.blob_index()));
if blob_sidecar_list.is_empty() {
debug!(
info = "blobs have already been imported into data availability checker",
"Ignoring EL blobs response"
);
return Ok(None);
}
}
// Up until this point we have not observed the blobs in the gossip cache, which allows them to
// arrive independently while this function is running. In `publish_fn` we will observe them
// and then publish any blobs that had not already been observed.
publish_fn(EngineGetBlobsOutput::Blobs(blob_sidecar_list.clone()));
let availability_processing_status = chain_adapter
.process_engine_blobs(
header.slot(),
block_root,
EngineGetBlobsOutput::Blobs(blob_sidecar_list),
)
.await?;
Ok(Some(availability_processing_status))
}
#[instrument(skip_all, level = "debug")]
async fn fetch_and_process_blobs_v2_or_v3<T: BeaconChainTypes>(
chain_adapter: FetchBlobsBeaconAdapter<T>,
block_root: Hash256,
header: Arc<PartialDataColumnHeader<T::EthSpec>>,
versioned_hashes: Vec<VersionedHash>,
custody_columns_indices: &[ColumnIndex],
publish_fn: impl Fn(EngineGetBlobsOutput<T>) + Send + 'static,
) -> Result<Option<AvailabilityProcessingStatus>, FetchEngineBlobError> {
let num_expected_blobs = versioned_hashes.len();
let slot = header.slot();
metrics::observe(&metrics::BLOBS_FROM_EL_EXPECTED, num_expected_blobs as f64);
let get_blobs_v3 = chain_adapter.supports_get_blobs_v3().await?;
let response = if get_blobs_v3 {
debug!(num_expected_blobs, "Fetching available blobs from the EL");
// Track request count and duration for standardized metrics
inc_counter(&metrics::BEACON_ENGINE_GET_BLOBS_V3_REQUESTS_TOTAL);
let _timer =
metrics::start_timer(&metrics::BEACON_ENGINE_GET_BLOBS_V3_REQUEST_DURATION_SECONDS);
chain_adapter
.get_blobs_v3(versioned_hashes)
.await
.inspect_err(|_| {
inc_counter(&metrics::BLOBS_FROM_EL_ERROR_TOTAL);
})?
} else {
debug!(num_expected_blobs, "Fetching all blobs from the EL");
// Track request count and duration for standardized metrics
inc_counter(&metrics::BEACON_ENGINE_GET_BLOBS_V2_REQUESTS_TOTAL);
let _timer =
metrics::start_timer(&metrics::BEACON_ENGINE_GET_BLOBS_V2_REQUEST_DURATION_SECONDS);
let response = chain_adapter
.get_blobs_v2(versioned_hashes)
.await
.inspect_err(|_| {
inc_counter(&metrics::BLOBS_FROM_EL_ERROR_TOTAL);
})?;
// Track successful response
inc_counter(&metrics::BEACON_ENGINE_GET_BLOBS_V2_RESPONSES_TOTAL);
response.map(|vec| vec.into_iter().map(Some).collect())
};
let Some(blobs_and_proofs) = response else {
debug!(num_expected_blobs, "No blobs fetched from the EL");
inc_counter(&metrics::BLOBS_FROM_EL_MISS_TOTAL);
return Ok(None);
};
let num_fetched_blobs = blobs_and_proofs.iter().filter(|opt| opt.is_some()).count();
metrics::observe(&metrics::BLOBS_FROM_EL_RECEIVED, num_fetched_blobs as f64);
if num_fetched_blobs != num_expected_blobs {
if !get_blobs_v3 {
// This scenario is not supposed to happen if the EL is spec compliant.
// It should either return all requested blobs or none, but NOT partial responses.
// If we attempt to compute columns with partial blobs, we'd end up with invalid columns.
warn!(
num_fetched_blobs,
num_expected_blobs, "The EL did not return all requested blobs"
);
inc_counter(&metrics::BLOBS_FROM_EL_MISS_TOTAL);
return Ok(None);
} else {
inc_counter(&metrics::BEACON_ENGINE_GET_BLOBS_V3_PARTIAL_RESPONSES_TOTAL);
debug!(
num_fetched_blobs,
num_expected_blobs, "Blobs partially received from the EL"
);
}
} else {
debug!(num_fetched_blobs, "All blobs received from the EL");
inc_counter(&metrics::BLOBS_FROM_EL_HIT_TOTAL);
if get_blobs_v3 {
inc_counter(&metrics::BEACON_ENGINE_GET_BLOBS_V3_COMPLETE_RESPONSES_TOTAL);
}
}
if chain_adapter.fork_choice_contains_block(&block_root) {
// Avoid computing columns if the block has already been imported.
debug!(
info = "block has already been imported",
"Ignoring EL blobs response"
);
return Ok(None);
}
let chain_adapter = Arc::new(chain_adapter);
let custody_columns_to_import = compute_custody_columns_to_import(
&chain_adapter,
block_root,
&header,
blobs_and_proofs,
custody_columns_indices,
)
.await?;
if custody_columns_to_import.is_empty() {
debug!(
info = "No new data columns to import",
"Ignoring EL blobs response"
);
return Ok(None);
}
let full_columns = match chain_adapter.partial_assembler() {
Some(assembler) => {
// Initialize the partial assembler with the columns from the engine and return any full
// columns for publishing
assembler
.merge_partials(block_root, custody_columns_to_import, header)
.ok_or_else(|| {
FetchEngineBlobError::InternalError(
"Failed to merge partials into assembler".to_string(),
)
})?
.full_columns
}
None => {
// Partial columns are disabled, so let's try to directly convert the columns we got
// from the EL into full columns.
custody_columns_to_import
.into_iter()
.filter_map(|col| col.try_into_full(&header))
.collect()
}
};
// Publish complete columns
if !full_columns.is_empty() {
publish_fn(EngineGetBlobsOutput::CustodyColumns(full_columns.clone()));
}
// We publish all partials at the calling site, regardless of result, as previous publishs
// have been blocked, waiting for the results of this call
// Process complete columns through DA checker
let availability_processing_status = if !full_columns.is_empty() {
chain_adapter
.process_engine_blobs(
slot,
block_root,
EngineGetBlobsOutput::CustodyColumns(full_columns),
)
.await?
} else {
// No complete columns yet, still missing components
AvailabilityProcessingStatus::MissingComponents(slot, block_root)
};
Ok(Some(availability_processing_status))
}
/// Offload the data column computation to a blocking task to avoid holding up the async runtime.
async fn compute_custody_columns_to_import<T: BeaconChainTypes>(
chain_adapter: &Arc<FetchBlobsBeaconAdapter<T>>,
block_root: Hash256,
header: &PartialDataColumnHeader<T::EthSpec>,
blobs_and_proofs: Vec<BlobAndProofV3<T::EthSpec>>,
custody_columns_indices: &[ColumnIndex],
) -> Result<Vec<KzgVerifiedCustodyPartialDataColumn<T::EthSpec>>, FetchEngineBlobError> {
let kzg = chain_adapter.kzg().clone();
let spec = chain_adapter.spec().clone();
let chain_adapter_cloned = chain_adapter.clone();
let custody_columns_indices = custody_columns_indices.to_vec();
let header = header.clone();
chain_adapter
.executor()
.spawn_blocking_handle(
move || {
let mut timer = metrics::start_timer_vec(
&metrics::DATA_COLUMN_SIDECAR_COMPUTATION,
&[&blobs_and_proofs.len().to_string()],
);
let blob_and_proof_refs = blobs_and_proofs
.iter()
.map(|option| {
option
.as_ref()
.map(|BlobAndProofV2 { blob, proofs }| (blob, proofs.as_ref()))
})
.collect::<Vec<_>>();
let data_columns_result =
blobs_to_partial_data_columns(blob_and_proof_refs, &header, &kzg, &spec)
.discard_timer_on_break(&mut timer);
drop(timer);
// This filtering ensures we only import and publish the custody columns.
// `DataAvailabilityChecker` requires a strict match on custody columns count to
// consider a block available.
let mut custody_columns = data_columns_result
.map(|data_columns| {
data_columns
.into_iter()
.filter(|col| custody_columns_indices.contains(&col.index))
.map(|col| {
KzgVerifiedCustodyPartialDataColumn::from_asserted_custody(
KzgVerifiedPartialDataColumn::from_execution_verified(
Arc::new(col),
),
)
})
.collect::<Vec<_>>()
})
.map_err(FetchEngineBlobError::DataColumnSidecarError)?;
// Only consider columns that are not already observed on gossip.
let observation_key =
ObservationKey::from_partial_column_header(&header, block_root, &spec);
if let Some(observed_columns) =
chain_adapter_cloned.data_column_known_for_observation_key(observation_key)
{
custody_columns.retain(|col| !observed_columns.contains(&col.index()));
if custody_columns.is_empty() {
return Ok(vec![]);
}
}
// Only consider columns that are not already known to data availability.
if let Some(known_columns) =
chain_adapter_cloned.cached_data_column_indexes(&block_root, header.slot())
{
custody_columns.retain(|col| !known_columns.contains(&col.index()));
if custody_columns.is_empty() {
return Ok(vec![]);
}
}
Ok(custody_columns)
},
"compute_custody_columns_to_import",
)
.ok_or(FetchEngineBlobError::RuntimeShutdown)?
.await
.map_err(FetchEngineBlobError::TokioJoin)?
}
fn build_blob_sidecars<E: EthSpec>(
header: &PartialDataColumnHeader<E>,
response: Vec<Option<BlobAndProofV1<E>>>,
) -> Result<Vec<KzgVerifiedBlob<E>>, FetchEngineBlobError> {
let mut sidecars = vec![];
for (index, blob_and_proof) in response
.into_iter()
.enumerate()
.filter_map(|(index, opt_blob)| Some((index, opt_blob?)))
{
let blob_sidecar = BlobSidecar::new_with_existing_proof(
index,
blob_and_proof.blob,
header.clone(),
blob_and_proof.proof,
)
.map_err(FetchEngineBlobError::BlobSidecarError)?;
sidecars.push(KzgVerifiedBlob::from_execution_verified(
Arc::new(blob_sidecar),
timestamp_now(),
));
}
Ok(sidecars)
}

View File

@@ -1,632 +0,0 @@
use crate::AvailabilityProcessingStatus;
use crate::fetch_blobs::fetch_blobs_beacon_adapter::MockFetchBlobsBeaconAdapter;
use crate::fetch_blobs::{
EngineGetBlobsOutput, FetchEngineBlobError, fetch_and_process_engine_blobs_inner,
};
use crate::partial_data_column_assembler::PartialDataColumnAssembler;
use crate::test_utils::{EphemeralHarnessType, get_kzg};
use bls::Signature;
use eth2::types::BlobsBundle;
use execution_layer::json_structures::{BlobAndProof, BlobAndProofV1, BlobAndProofV2};
use execution_layer::test_utils::generate_blobs;
use maplit::hashset;
use std::num::NonZeroUsize;
use std::sync::{Arc, Mutex};
use task_executor::test_utils::TestRuntime;
use types::{
BeaconBlock, BeaconBlockFulu, EmptyBlock, EthSpec, ForkName, Hash256, MainnetEthSpec,
SignedBeaconBlock, SignedBeaconBlockFulu,
};
type E = MainnetEthSpec;
type T = EphemeralHarnessType<E>;
mod get_blobs_v2 {
use super::*;
use types::{ColumnIndex, PartialDataColumnHeader};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_fetch_blobs_v2_no_blobs_in_block() {
let mut mock_adapter = mock_beacon_adapter(ForkName::Fulu, false);
let (publish_fn, _s) = mock_publish_fn();
let block = SignedBeaconBlock::<E>::Fulu(SignedBeaconBlockFulu {
message: BeaconBlockFulu::empty(mock_adapter.spec()),
signature: Signature::empty(),
});
let block_root = block.canonical_root();
// Expectations: engine fetch blobs should not be triggered
mock_adapter.expect_get_blobs_v2().times(0);
mock_adapter.expect_process_engine_blobs().times(0);
let custody_columns: [ColumnIndex; 3] = [0, 1, 2];
let processing_status = fetch_and_process_engine_blobs_inner(
mock_adapter,
block_root,
Arc::new((&block).try_into().unwrap()),
&custody_columns,
publish_fn,
)
.await
.expect("fetch blobs should succeed");
assert_eq!(processing_status, None);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_fetch_blobs_v2_no_blobs_returned() {
let mut mock_adapter = mock_beacon_adapter(ForkName::Fulu, false);
let (publish_fn, _) = mock_publish_fn();
let (block, _blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2);
let block_root = block.canonical_root();
// No blobs in EL response
mock_get_blobs_v2_response(&mut mock_adapter, None);
// Trigger fetch blobs on the block
let custody_columns: [ColumnIndex; 3] = [0, 1, 2];
let processing_status = fetch_and_process_engine_blobs_inner(
mock_adapter,
block_root,
Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()),
&custody_columns,
publish_fn,
)
.await
.expect("fetch blobs should succeed");
assert_eq!(processing_status, None);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_fetch_blobs_v2_partial_blobs_returned() {
let mut mock_adapter = mock_beacon_adapter(ForkName::Fulu, false);
let (publish_fn, publish_fn_args) = mock_publish_fn();
let (block, mut blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2);
let block_root = block.canonical_root();
// Missing blob in EL response
blobs_and_proofs.pop();
mock_get_blobs_v2_response(&mut mock_adapter, Some(blobs_and_proofs));
// No blobs should be processed
mock_adapter.expect_process_engine_blobs().times(0);
// Trigger fetch blobs on the block
let custody_columns: [ColumnIndex; 3] = [0, 1, 2];
let processing_status = fetch_and_process_engine_blobs_inner(
mock_adapter,
block_root,
Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()),
&custody_columns,
publish_fn,
)
.await
.expect("fetch blobs should succeed");
assert_eq!(processing_status, None);
assert_eq!(
publish_fn_args.lock().unwrap().len(),
0,
"no columns should be published"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_fetch_blobs_v2_block_imported_after_el_response() {
let mut mock_adapter = mock_beacon_adapter(ForkName::Fulu, false);
let (publish_fn, publish_fn_args) = mock_publish_fn();
let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2);
let block_root = block.canonical_root();
// All blobs returned, but fork choice already imported the block
mock_get_blobs_v2_response(&mut mock_adapter, Some(blobs_and_proofs));
mock_fork_choice_contains_block(&mut mock_adapter, vec![block.canonical_root()]);
// No blobs should be processed
mock_adapter.expect_process_engine_blobs().times(0);
// Trigger fetch blobs on the block
let custody_columns: [ColumnIndex; 3] = [0, 1, 2];
let processing_status = fetch_and_process_engine_blobs_inner(
mock_adapter,
block_root,
Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()),
&custody_columns,
publish_fn,
)
.await
.expect("fetch blobs should succeed");
assert_eq!(processing_status, None);
assert_eq!(
publish_fn_args.lock().unwrap().len(),
0,
"no columns should be published"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_fetch_blobs_v2_no_new_columns_to_import() {
let mut mock_adapter = mock_beacon_adapter(ForkName::Fulu, false);
let (publish_fn, publish_fn_args) = mock_publish_fn();
let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2);
let block_root = block.canonical_root();
// **GIVEN**:
// All blobs returned
mock_get_blobs_v2_response(&mut mock_adapter, Some(blobs_and_proofs));
// block not yet imported into fork choice
mock_fork_choice_contains_block(&mut mock_adapter, vec![]);
// All data columns already seen on gossip
mock_adapter
.expect_data_column_known_for_observation_key()
.returning(|_| Some(hashset![0, 1, 2]));
// No blobs should be processed
mock_adapter.expect_process_engine_blobs().times(0);
// **WHEN**: Trigger `fetch_blobs` on the block
let custody_columns: [ColumnIndex; 3] = [0, 1, 2];
let processing_status = fetch_and_process_engine_blobs_inner(
mock_adapter,
block_root,
Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()),
&custody_columns,
publish_fn,
)
.await
.expect("fetch blobs should succeed");
// **THEN**: Should NOT be processed and no columns should be published.
assert_eq!(processing_status, None);
assert_eq!(
publish_fn_args.lock().unwrap().len(),
0,
"no columns should be published"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_fetch_blobs_v2_success() {
let mut mock_adapter = mock_beacon_adapter(ForkName::Fulu, false);
let (publish_fn, publish_fn_args) = mock_publish_fn();
let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2);
let block_root = block.canonical_root();
// All blobs returned, fork choice doesn't contain block
mock_get_blobs_v2_response(&mut mock_adapter, Some(blobs_and_proofs));
mock_fork_choice_contains_block(&mut mock_adapter, vec![]);
mock_adapter
.expect_data_column_known_for_observation_key()
.returning(|_| None);
mock_adapter
.expect_cached_data_column_indexes()
.returning(|_, _| None);
mock_process_engine_blobs_result(
&mut mock_adapter,
Ok(AvailabilityProcessingStatus::Imported(block_root)),
);
// Trigger fetch blobs on the block
let custody_columns: [ColumnIndex; 3] = [0, 1, 2];
let processing_status = fetch_and_process_engine_blobs_inner(
mock_adapter,
block_root,
Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()),
&custody_columns,
publish_fn,
)
.await
.expect("fetch blobs should succeed");
assert_eq!(
processing_status,
Some(AvailabilityProcessingStatus::Imported(block_root))
);
let published_columns = extract_published_blobs(publish_fn_args);
assert!(
matches!(
published_columns,
EngineGetBlobsOutput::CustodyColumns(columns) if columns.len() == custody_columns.len()
),
"should publish custody columns"
);
}
fn mock_get_blobs_v2_response(
mock_adapter: &mut MockFetchBlobsBeaconAdapter<T>,
blobs_and_proofs_opt: Option<Vec<BlobAndProof<E>>>,
) {
let blobs_and_proofs_v2_opt = blobs_and_proofs_opt.map(|blobs_and_proofs| {
blobs_and_proofs
.into_iter()
.map(|blob_and_proof| match blob_and_proof {
BlobAndProof::V2(inner) => inner,
_ => panic!("BlobAndProofV2 not expected"),
})
.collect()
});
mock_adapter
.expect_get_blobs_v2()
.return_once(move |_| Ok(blobs_and_proofs_v2_opt));
}
}
mod get_blobs_v1 {
use super::*;
use crate::block_verification_types::AsBlock;
use std::collections::HashSet;
use types::{ColumnIndex, FullPayload, PartialDataColumnHeader};
const ELECTRA_FORK: ForkName = ForkName::Electra;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_fetch_blobs_v1_no_blobs_in_block() {
let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false);
let spec = mock_adapter.spec();
let (publish_fn, _s) = mock_publish_fn();
let block_no_blobs = SignedBeaconBlock::<E, FullPayload<E>>::from_block(
BeaconBlock::empty(spec),
Signature::empty(),
);
let block_root = block_no_blobs.canonical_root();
// Expectations: engine fetch blobs should not be triggered
mock_adapter.expect_get_blobs_v1().times(0);
// WHEN: Trigger fetch blobs on the block
let custody_columns: [ColumnIndex; 3] = [0, 1, 2];
let processing_status = fetch_and_process_engine_blobs_inner(
mock_adapter,
block_root,
Arc::new(PartialDataColumnHeader::try_from(&block_no_blobs).unwrap()),
&custody_columns,
publish_fn,
)
.await
.expect("fetch blobs should succeed");
// THEN: No blob is processed
assert_eq!(processing_status, None);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_fetch_blobs_v1_no_blobs_returned() {
let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false);
let (publish_fn, _) = mock_publish_fn();
let (block, _blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2);
let block_root = block.canonical_root();
// GIVEN: No blobs in EL response
let expected_blob_count = block.message().body().blob_kzg_commitments().unwrap().len();
mock_get_blobs_v1_response(&mut mock_adapter, vec![None; expected_blob_count]);
// WHEN: Trigger fetch blobs on the block
let custody_columns: [ColumnIndex; 3] = [0, 1, 2];
let processing_status = fetch_and_process_engine_blobs_inner(
mock_adapter,
block_root,
Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()),
&custody_columns,
publish_fn,
)
.await
.expect("fetch blobs should succeed");
// THEN: No blob is processed
assert_eq!(processing_status, None);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_fetch_blobs_v1_partial_blobs_returned() {
let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false);
let (publish_fn, publish_fn_args) = mock_publish_fn();
let blob_count = 2;
let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, blob_count);
let block_slot = block.slot();
let block_root = block.canonical_root();
// GIVEN: Missing a blob in EL response (remove 1 blob from response)
let mut blob_and_proof_opts = blobs_and_proofs.into_iter().map(Some).collect::<Vec<_>>();
blob_and_proof_opts.first_mut().unwrap().take();
mock_get_blobs_v1_response(&mut mock_adapter, blob_and_proof_opts);
// AND block is not imported into fork choice
mock_fork_choice_contains_block(&mut mock_adapter, vec![]);
// AND all blobs have not yet been seen
mock_adapter
.expect_cached_blob_indexes()
.returning(|_| None);
mock_adapter
.expect_blobs_known_for_observation_key()
.returning(|_| None);
// Returned blobs should be processed
mock_process_engine_blobs_result(
&mut mock_adapter,
Ok(AvailabilityProcessingStatus::MissingComponents(
block_slot, block_root,
)),
);
// WHEN: Trigger fetch blobs on the block
let custody_columns: [ColumnIndex; 3] = [0, 1, 2];
let processing_status = fetch_and_process_engine_blobs_inner(
mock_adapter,
block_root,
Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()),
&custody_columns,
publish_fn,
)
.await
.expect("fetch blobs should succeed");
// THEN: Returned blobs are processed and published
assert_eq!(
processing_status,
Some(AvailabilityProcessingStatus::MissingComponents(
block_slot, block_root,
))
);
assert!(
matches!(
extract_published_blobs(publish_fn_args),
EngineGetBlobsOutput::Blobs(blobs) if blobs.len() == blob_count - 1
),
"partial blob results should still be published"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_fetch_blobs_v1_block_imported_after_el_response() {
let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false);
let (publish_fn, publish_fn_args) = mock_publish_fn();
let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2);
let block_root = block.canonical_root();
// GIVEN: All blobs returned, but fork choice already imported the block
let blob_and_proof_opts = blobs_and_proofs.into_iter().map(Some).collect::<Vec<_>>();
mock_get_blobs_v1_response(&mut mock_adapter, blob_and_proof_opts);
mock_fork_choice_contains_block(&mut mock_adapter, vec![block.canonical_root()]);
// WHEN: Trigger fetch blobs on the block
let custody_columns: [ColumnIndex; 3] = [0, 1, 2];
let processing_status = fetch_and_process_engine_blobs_inner(
mock_adapter,
block_root,
Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()),
&custody_columns,
publish_fn,
)
.await
.expect("fetch blobs should succeed");
// THEN: Returned blobs should NOT be processed or published.
assert_eq!(processing_status, None);
assert_eq!(
publish_fn_args.lock().unwrap().len(),
0,
"no blobs should be published"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_fetch_blobs_v1_no_new_blobs_to_import() {
let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false);
let (publish_fn, publish_fn_args) = mock_publish_fn();
let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2);
let block_root = block.canonical_root();
// **GIVEN**:
// All blobs returned
let blob_and_proof_opts = blobs_and_proofs.into_iter().map(Some).collect::<Vec<_>>();
let all_blob_indices = blob_and_proof_opts
.iter()
.enumerate()
.map(|(i, _)| i as u64)
.collect::<HashSet<_>>();
mock_get_blobs_v1_response(&mut mock_adapter, blob_and_proof_opts);
// block not yet imported into fork choice
mock_fork_choice_contains_block(&mut mock_adapter, vec![]);
// All blobs already seen on gossip
mock_adapter
.expect_cached_blob_indexes()
.returning(|_| None);
mock_adapter
.expect_blobs_known_for_observation_key()
.returning(move |_| Some(all_blob_indices.clone()));
// **WHEN**: Trigger `fetch_blobs` on the block
let custody_columns: [ColumnIndex; 3] = [0, 1, 2];
let processing_status = fetch_and_process_engine_blobs_inner(
mock_adapter,
block_root,
Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()),
&custody_columns,
publish_fn,
)
.await
.expect("fetch blobs should succeed");
// **THEN**: Should NOT be processed and no blobs should be published.
assert_eq!(processing_status, None);
assert_eq!(
publish_fn_args.lock().unwrap().len(),
0,
"no blobs should be published"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_fetch_blobs_v1_success() {
let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false);
let (publish_fn, publish_fn_args) = mock_publish_fn();
let blob_count = 2;
let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, blob_count);
let block_root = block.canonical_root();
// All blobs returned, fork choice doesn't contain block
let blob_and_proof_opts = blobs_and_proofs.into_iter().map(Some).collect::<Vec<_>>();
mock_get_blobs_v1_response(&mut mock_adapter, blob_and_proof_opts);
mock_fork_choice_contains_block(&mut mock_adapter, vec![]);
mock_adapter
.expect_cached_blob_indexes()
.returning(|_| None);
mock_adapter
.expect_blobs_known_for_observation_key()
.returning(|_| None);
mock_process_engine_blobs_result(
&mut mock_adapter,
Ok(AvailabilityProcessingStatus::Imported(block_root)),
);
// Trigger fetch blobs on the block
let custody_columns: [ColumnIndex; 3] = [0, 1, 2];
let processing_status = fetch_and_process_engine_blobs_inner(
mock_adapter,
block_root,
Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()),
&custody_columns,
publish_fn,
)
.await
.expect("fetch blobs should succeed");
// THEN all fetched blobs are processed and published
assert_eq!(
processing_status,
Some(AvailabilityProcessingStatus::Imported(block_root))
);
let published_blobs = extract_published_blobs(publish_fn_args);
assert!(
matches!(
published_blobs,
EngineGetBlobsOutput::Blobs(blobs) if blobs.len() == blob_count
),
"should publish fetched blobs"
);
}
fn mock_get_blobs_v1_response(
mock_adapter: &mut MockFetchBlobsBeaconAdapter<T>,
blobs_and_proofs_opt: Vec<Option<BlobAndProof<E>>>,
) {
let blobs_and_proofs_v1 = blobs_and_proofs_opt
.into_iter()
.map(|blob_and_proof_opt| {
blob_and_proof_opt.map(|blob_and_proof| match blob_and_proof {
BlobAndProof::V1(inner) => inner,
_ => panic!("BlobAndProofV1 not expected"),
})
})
.collect();
mock_adapter
.expect_get_blobs_v1()
.return_once(move |_| Ok(blobs_and_proofs_v1));
}
}
/// Extract the `EngineGetBlobsOutput` passed to the `publish_fn`.
fn extract_published_blobs(
publish_fn_args: Arc<Mutex<Vec<EngineGetBlobsOutput<T>>>>,
) -> EngineGetBlobsOutput<T> {
let mut calls = publish_fn_args.lock().unwrap();
assert_eq!(calls.len(), 1);
calls.pop().unwrap()
}
fn mock_process_engine_blobs_result(
mock_adapter: &mut MockFetchBlobsBeaconAdapter<T>,
result: Result<AvailabilityProcessingStatus, FetchEngineBlobError>,
) {
mock_adapter
.expect_process_engine_blobs()
.return_once(move |_, _, _| result);
}
fn mock_fork_choice_contains_block(
mock_adapter: &mut MockFetchBlobsBeaconAdapter<T>,
block_roots: Vec<Hash256>,
) {
mock_adapter
.expect_fork_choice_contains_block()
.returning(move |block_root| block_roots.contains(block_root));
}
fn create_test_block_and_blobs(
mock_adapter: &MockFetchBlobsBeaconAdapter<T>,
blob_count: usize,
) -> (Arc<SignedBeaconBlock<E>>, Vec<BlobAndProof<E>>) {
let mut block =
SignedBeaconBlock::from_block(BeaconBlock::empty(mock_adapter.spec()), Signature::empty());
let fork = block.fork_name_unchecked();
let (blobs_bundle, _tx) = generate_blobs::<E>(blob_count, fork).unwrap();
let BlobsBundle {
commitments,
proofs,
blobs,
} = blobs_bundle;
*block
.message_mut()
.body_mut()
.blob_kzg_commitments_mut()
.unwrap() = commitments;
let blobs_and_proofs = if fork.fulu_enabled() {
let proofs_len = proofs.len() / blobs.len();
blobs
.into_iter()
.zip(proofs.chunks(proofs_len))
.map(|(blob, proofs)| {
BlobAndProof::V2(BlobAndProofV2 {
blob,
proofs: proofs.to_vec().try_into().unwrap(),
})
})
.collect()
} else {
blobs
.into_iter()
.zip(proofs)
.map(|(blob, proof)| BlobAndProof::V1(BlobAndProofV1 { blob, proof }))
.collect()
};
(Arc::new(block), blobs_and_proofs)
}
#[allow(clippy::type_complexity)]
fn mock_publish_fn() -> (
impl Fn(EngineGetBlobsOutput<T>) + Send + 'static,
Arc<Mutex<Vec<EngineGetBlobsOutput<T>>>>,
) {
// Keep track of the arguments captured by `publish_fn`.
let captured_args = Arc::new(Mutex::new(vec![]));
let captured_args_clone = captured_args.clone();
let publish_fn = move |args| {
let mut lock = captured_args_clone.lock().unwrap();
lock.push(args);
};
(publish_fn, captured_args)
}
fn mock_beacon_adapter(fork_name: ForkName, get_blobs_v3: bool) -> MockFetchBlobsBeaconAdapter<T> {
let test_runtime = TestRuntime::default();
let spec = Arc::new(fork_name.make_genesis_spec(E::default_spec()));
let kzg = get_kzg(&spec);
let partial_assembler = PartialDataColumnAssembler::new(NonZeroUsize::new(32).unwrap());
let mut mock_adapter = MockFetchBlobsBeaconAdapter::default();
mock_adapter.expect_spec().return_const(spec.clone());
mock_adapter.expect_kzg().return_const(kzg.clone());
mock_adapter
.expect_executor()
.return_const(test_runtime.task_executor.clone());
mock_adapter
.expect_supports_get_blobs_v3()
.returning(move || Ok(get_blobs_v3));
mock_adapter
.expect_partial_assembler()
.return_const(Some(Arc::new(partial_assembler)));
mock_adapter
}

View File

@@ -0,0 +1,205 @@
use crate::{BeaconForkChoiceStore, BeaconSnapshot};
use fork_choice::{ForkChoice, PayloadVerificationStatus};
use itertools::process_results;
use slog::{info, warn, Logger};
use state_processing::state_advance::complete_state_advance;
use state_processing::{
per_block_processing, per_block_processing::BlockSignatureStrategy, ConsensusContext,
VerifyBlockRoot,
};
use std::sync::Arc;
use std::time::Duration;
use store::{iter::ParentRootBlockIterator, HotColdDB, ItemStore};
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,
log: &Logger,
) -> 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!(
log,
"Reverting invalid head block";
"target_fork" => %current_fork,
"fork_epoch" => fork_epoch,
);
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!(
log,
"Reverting block";
"block_root" => ?block_root,
"slot" => block.slot(),
);
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();
let mut finalized_state = store
.get_state(&finalized_state_root, Some(finalized_block.slot()))
.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)
.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)
}

View File

@@ -0,0 +1,115 @@
//! Provides tools for checking if a node is ready for the Fulu upgrade.
use crate::{BeaconChain, BeaconChainTypes};
use execution_layer::http::{ENGINE_GET_PAYLOAD_V4, ENGINE_NEW_PAYLOAD_V4};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::time::Duration;
use types::*;
/// The time before the Fulu fork when we will start issuing warnings about preparation.
use super::bellatrix_readiness::SECONDS_IN_A_WEEK;
pub const FULU_READINESS_PREPARATION_SECONDS: u64 = SECONDS_IN_A_WEEK * 2;
pub const ENGINE_CAPABILITIES_REFRESH_INTERVAL: u64 = 300;
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "type")]
pub enum FuluReadiness {
/// The execution engine is fulu-enabled (as far as we can tell)
Ready,
/// We are connected to an execution engine which doesn't support the V5 engine api methods
V5MethodsNotSupported { error: String },
/// The transition configuration with the EL failed, there might be a problem with
/// connectivity, authentication or a difference in configuration.
ExchangeCapabilitiesFailed { error: String },
/// The user has not configured an execution endpoint
NoExecutionEndpoint,
}
impl fmt::Display for FuluReadiness {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FuluReadiness::Ready => {
write!(f, "This node appears ready for Fulu.")
}
FuluReadiness::ExchangeCapabilitiesFailed { error } => write!(
f,
"Could not exchange capabilities with the \
execution endpoint: {}",
error
),
FuluReadiness::NoExecutionEndpoint => write!(
f,
"The --execution-endpoint flag is not specified, this is a \
requirement post-merge"
),
FuluReadiness::V5MethodsNotSupported { error } => write!(
f,
"Execution endpoint does not support Fulu methods: {}",
error
),
}
}
}
impl<T: BeaconChainTypes> BeaconChain<T> {
/// Returns `true` if fulu epoch is set and Fulu fork has occurred or will
/// occur within `FULU_READINESS_PREPARATION_SECONDS`
pub fn is_time_to_prepare_for_fulu(&self, current_slot: Slot) -> bool {
if let Some(fulu_epoch) = self.spec.fulu_fork_epoch {
let fulu_slot = fulu_epoch.start_slot(T::EthSpec::slots_per_epoch());
let fulu_readiness_preparation_slots =
FULU_READINESS_PREPARATION_SECONDS / self.spec.seconds_per_slot;
// Return `true` if Fulu has happened or is within the preparation time.
current_slot + fulu_readiness_preparation_slots > fulu_slot
} else {
// The Fulu 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 fulu.
pub async fn check_fulu_readiness(&self) -> FuluReadiness {
if let Some(el) = self.execution_layer.as_ref() {
match el
.get_engine_capabilities(Some(Duration::from_secs(
ENGINE_CAPABILITIES_REFRESH_INTERVAL,
)))
.await
{
Err(e) => {
// The EL was either unreachable or responded with an error
FuluReadiness::ExchangeCapabilitiesFailed {
error: format!("{:?}", e),
}
}
Ok(capabilities) => {
let mut missing_methods = String::from("Required Methods Unsupported:");
let mut all_good = true;
// TODO(fulu) switch to v5 when the EL is ready
if !capabilities.get_payload_v4 {
missing_methods.push(' ');
missing_methods.push_str(ENGINE_GET_PAYLOAD_V4);
all_good = false;
}
if !capabilities.new_payload_v4 {
missing_methods.push(' ');
missing_methods.push_str(ENGINE_NEW_PAYLOAD_V4);
all_good = false;
}
if all_good {
FuluReadiness::Ready
} else {
FuluReadiness::V5MethodsNotSupported {
error: missing_methods,
}
}
}
}
} else {
FuluReadiness::NoExecutionEndpoint
}
}
}

View File

@@ -1,14 +1,12 @@
use crate::BeaconChain;
use crate::BeaconChainTypes;
use eth2::types::GraffitiPolicy;
use execution_layer::{CommitPrefix, ExecutionLayer, http::ENGINE_GET_CLIENT_VERSION_V1};
use logging::crit;
use execution_layer::{http::ENGINE_GET_CLIENT_VERSION_V1, CommitPrefix, ExecutionLayer};
use serde::{Deserialize, Serialize};
use slog::{crit, debug, error, warn, Logger};
use slot_clock::SlotClock;
use std::{fmt::Debug, time::Duration};
use task_executor::TaskExecutor;
use tracing::{debug, error, warn};
use types::{EthSpec, GRAFFITI_BYTES_LEN, Graffiti};
use types::{EthSpec, Graffiti, GRAFFITI_BYTES_LEN};
const ENGINE_VERSION_AGE_LIMIT_EPOCH_MULTIPLE: u32 = 6; // 6 epochs
const ENGINE_VERSION_CACHE_REFRESH_EPOCH_MULTIPLE: u32 = 2; // 2 epochs
@@ -49,29 +47,11 @@ impl Debug for GraffitiOrigin {
}
}
pub enum GraffitiSettings {
Unspecified,
Specified {
graffiti: Graffiti,
policy: GraffitiPolicy,
},
}
impl GraffitiSettings {
pub fn new(validator_graffiti: Option<Graffiti>, policy: Option<GraffitiPolicy>) -> Self {
validator_graffiti
.map(|graffiti| Self::Specified {
graffiti,
policy: policy.unwrap_or(GraffitiPolicy::PreserveUserGraffiti),
})
.unwrap_or(Self::Unspecified)
}
}
pub struct GraffitiCalculator<T: BeaconChainTypes> {
pub beacon_graffiti: GraffitiOrigin,
execution_layer: Option<ExecutionLayer<T::EthSpec>>,
pub epoch_duration: Duration,
log: Logger,
}
impl<T: BeaconChainTypes> GraffitiCalculator<T> {
@@ -79,11 +59,13 @@ impl<T: BeaconChainTypes> GraffitiCalculator<T> {
beacon_graffiti: GraffitiOrigin,
execution_layer: Option<ExecutionLayer<T::EthSpec>>,
epoch_duration: Duration,
log: Logger,
) -> Self {
Self {
beacon_graffiti,
execution_layer,
epoch_duration,
log,
}
}
@@ -93,28 +75,18 @@ impl<T: BeaconChainTypes> GraffitiCalculator<T> {
/// 2. Graffiti specified by the user via beacon node CLI options.
/// 3. The EL & CL client version string, applicable when the EL supports version specification.
/// 4. The default lighthouse version string, used if the EL lacks version specification support.
pub async fn get_graffiti(&self, graffiti_settings: GraffitiSettings) -> Graffiti {
match graffiti_settings {
GraffitiSettings::Specified { graffiti, policy } => match policy {
GraffitiPolicy::PreserveUserGraffiti => graffiti,
GraffitiPolicy::AppendClientVersions => {
self.calculate_combined_graffiti(Some(graffiti)).await
}
},
GraffitiSettings::Unspecified => self.calculate_combined_graffiti(None).await,
pub async fn get_graffiti(&self, validator_graffiti: Option<Graffiti>) -> Graffiti {
if let Some(graffiti) = validator_graffiti {
return graffiti;
}
}
async fn calculate_combined_graffiti(&self, validator_graffiti: Option<Graffiti>) -> Graffiti {
match self.beacon_graffiti {
GraffitiOrigin::UserSpecified(graffiti) => graffiti,
GraffitiOrigin::Calculated(default_graffiti) => {
let Some(execution_layer) = self.execution_layer.as_ref() else {
// Return default graffiti if there is no execution layer. This
// shouldn't occur if we're actually producing blocks.
crit!(
"No execution layer available for graffiti calculation during block production!"
);
crit!(self.log, "No execution layer available for graffiti calculation during block production!");
return default_graffiti;
};
@@ -129,7 +101,7 @@ impl<T: BeaconChainTypes> GraffitiCalculator<T> {
{
Ok(engine_versions) => engine_versions,
Err(el_error) => {
warn!(error = ?el_error, "Failed to determine execution engine version for graffiti");
warn!(self.log, "Failed to determine execution engine version for graffiti"; "error" => ?el_error);
return default_graffiti;
}
};
@@ -137,8 +109,9 @@ impl<T: BeaconChainTypes> GraffitiCalculator<T> {
let Some(engine_version) = engine_versions.first() else {
// Got an empty array which indicates the EL doesn't support the method
debug!(
self.log,
"Using default lighthouse graffiti: EL does not support {} method",
ENGINE_GET_CLIENT_VERSION_V1
ENGINE_GET_CLIENT_VERSION_V1;
);
return default_graffiti;
};
@@ -146,22 +119,21 @@ impl<T: BeaconChainTypes> GraffitiCalculator<T> {
// More than one version implies lighthouse is connected to
// an EL multiplexer. We don't support modifying the graffiti
// with these configurations.
warn!("Execution Engine multiplexer detected, using default graffiti");
warn!(
self.log,
"Execution Engine multiplexer detected, using default graffiti"
);
return default_graffiti;
}
let lighthouse_commit_prefix =
CommitPrefix::try_from(lighthouse_version::COMMIT_PREFIX.to_string())
.unwrap_or_else(|error_message| {
// This really shouldn't happen but we want to definitly log if it does
crit!(
error = error_message,
"Failed to parse lighthouse commit prefix"
);
CommitPrefix("00000000".to_string())
});
let lighthouse_commit_prefix = CommitPrefix::try_from(lighthouse_version::COMMIT_PREFIX.to_string())
.unwrap_or_else(|error_message| {
// This really shouldn't happen but we want to definitly log if it does
crit!(self.log, "Failed to parse lighthouse commit prefix"; "error" => error_message);
CommitPrefix("00000000".to_string())
});
engine_version.calculate_graffiti(lighthouse_commit_prefix, validator_graffiti)
engine_version.calculate_graffiti(lighthouse_commit_prefix)
}
}
}
@@ -172,24 +144,36 @@ pub fn start_engine_version_cache_refresh_service<T: BeaconChainTypes>(
executor: TaskExecutor,
) {
let Some(el_ref) = chain.execution_layer.as_ref() else {
debug!("No execution layer configured, not starting engine version cache refresh service");
debug!(
chain.log,
"No execution layer configured, not starting engine version cache refresh service"
);
return;
};
if matches!(
chain.graffiti_calculator.beacon_graffiti,
GraffitiOrigin::UserSpecified(_)
) {
debug!("Graffiti is user-specified, not starting engine version cache refresh service");
debug!(
chain.log,
"Graffiti is user-specified, not starting engine version cache refresh service"
);
return;
}
let execution_layer = el_ref.clone();
let log = chain.log.clone();
let slot_clock = chain.slot_clock.clone();
let epoch_duration = chain.graffiti_calculator.epoch_duration;
executor.spawn(
async move {
engine_version_cache_refresh_service::<T>(execution_layer, slot_clock, epoch_duration)
.await
engine_version_cache_refresh_service::<T>(
execution_layer,
slot_clock,
epoch_duration,
log,
)
.await
},
"engine_version_cache_refresh_service",
);
@@ -199,15 +183,13 @@ async fn engine_version_cache_refresh_service<T: BeaconChainTypes>(
execution_layer: ExecutionLayer<T::EthSpec>,
slot_clock: T::SlotClock,
epoch_duration: Duration,
log: Logger,
) {
// Preload the engine version cache after a brief delay to allow for EL initialization.
// This initial priming ensures cache readiness before the service's regular update cycle begins.
tokio::time::sleep(ENGINE_VERSION_CACHE_PRELOAD_STARTUP_DELAY).await;
if let Err(e) = execution_layer.get_engine_version(None).await {
debug!(
error = ?e,
"Failed to preload engine version cache"
);
debug!(log, "Failed to preload engine version cache"; "error" => format!("{:?}", e));
}
// this service should run 3/8 of the way through the epoch
@@ -221,14 +203,18 @@ async fn engine_version_cache_refresh_service<T: BeaconChainTypes>(
let firing_delay = partial_firing_delay + duration_to_next_epoch + epoch_delay;
tokio::time::sleep(firing_delay).await;
debug!("Engine version cache refresh service firing");
debug!(
log,
"Engine version cache refresh service firing";
);
match execution_layer.get_engine_version(None).await {
Err(e) => warn!( error = ?e, "Failed to populate engine version cache"),
Err(e) => warn!(log, "Failed to populate engine version cache"; "error" => ?e),
Ok(versions) => {
if versions.is_empty() {
// Empty array indicates the EL doesn't support the method
debug!(
log,
"EL does not support {} method. Sleeping twice as long before retry",
ENGINE_GET_CLIENT_VERSION_V1
);
@@ -241,7 +227,7 @@ async fn engine_version_cache_refresh_service<T: BeaconChainTypes>(
}
}
None => {
error!("Failed to read slot clock");
error!(log, "Failed to read slot clock");
// If we can't read the slot clock, just wait another slot.
tokio::time::sleep(slot_clock.slot_duration()).await;
}
@@ -251,18 +237,15 @@ async fn engine_version_cache_refresh_service<T: BeaconChainTypes>(
#[cfg(test)]
mod tests {
use crate::test_utils::{test_spec, BeaconChainHarness, EphemeralHarnessType};
use crate::ChainConfig;
use crate::graffiti_calculator::GraffitiSettings;
use crate::test_utils::{BeaconChainHarness, EphemeralHarnessType, test_spec};
use bls::Keypair;
use eth2::types::GraffitiPolicy;
use execution_layer::EngineCapabilities;
use execution_layer::test_utils::{DEFAULT_CLIENT_VERSION, DEFAULT_ENGINE_CAPABILITIES};
use execution_layer::EngineCapabilities;
use slog::info;
use std::sync::Arc;
use std::sync::LazyLock;
use std::time::Duration;
use tracing::info;
use types::{ChainSpec, GRAFFITI_BYTES_LEN, Graffiti, MinimalEthSpec};
use types::{ChainSpec, Graffiti, Keypair, MinimalEthSpec, GRAFFITI_BYTES_LEN};
const VALIDATOR_COUNT: usize = 48;
/// A cached set of keys.
@@ -278,6 +261,7 @@ mod tests {
.spec(spec)
.chain_config(chain_config.unwrap_or_default())
.keypairs(KEYPAIRS[0..validator_count].to_vec())
.logger(logging::test_logger())
.fresh_ephemeral_store()
.mock_execution_layer()
.build();
@@ -311,21 +295,14 @@ mod tests {
let version_bytes = std::cmp::min(lighthouse_version::VERSION.len(), GRAFFITI_BYTES_LEN);
// grab the slice of the graffiti that corresponds to the lighthouse version
let graffiti_slice = &harness
.chain
.graffiti_calculator
.get_graffiti(GraffitiSettings::Unspecified)
.await
.0[..version_bytes];
let graffiti_slice =
&harness.chain.graffiti_calculator.get_graffiti(None).await.0[..version_bytes];
// convert graffiti bytes slice to ascii for easy debugging if this test should fail
let graffiti_str =
std::str::from_utf8(graffiti_slice).expect("bytes should convert nicely to ascii");
info!(
lighthouse_version = lighthouse_version::VERSION,
graffiti_str, "results"
);
info!(harness.chain.log, "results"; "lighthouse_version" => lighthouse_version::VERSION, "graffiti_str" => graffiti_str);
println!("lighthouse_version: '{}'", lighthouse_version::VERSION);
println!("graffiti_str: '{}'", graffiti_str);
@@ -337,12 +314,7 @@ mod tests {
let spec = Arc::new(test_spec::<MinimalEthSpec>());
let harness = get_harness(VALIDATOR_COUNT, spec, None);
let found_graffiti_bytes = harness
.chain
.graffiti_calculator
.get_graffiti(GraffitiSettings::Unspecified)
.await
.0;
let found_graffiti_bytes = harness.chain.graffiti_calculator.get_graffiti(None).await.0;
let mock_commit = DEFAULT_CLIENT_VERSION.commit.clone();
let expected_graffiti_string = format!(
@@ -367,7 +339,7 @@ mod tests {
std::str::from_utf8(&found_graffiti_bytes[..expected_graffiti_prefix_len])
.expect("bytes should convert nicely to ascii");
info!(expected_graffiti_string, found_graffiti_string, "results");
info!(harness.chain.log, "results"; "expected_graffiti_string" => &expected_graffiti_string, "found_graffiti_string" => &found_graffiti_string);
println!("expected_graffiti_string: '{}'", expected_graffiti_string);
println!("found_graffiti_string: '{}'", found_graffiti_string);
@@ -391,10 +363,7 @@ mod tests {
let found_graffiti = harness
.chain
.graffiti_calculator
.get_graffiti(GraffitiSettings::new(
Some(Graffiti::from(graffiti_bytes)),
Some(GraffitiPolicy::PreserveUserGraffiti),
))
.get_graffiti(Some(Graffiti::from(graffiti_bytes)))
.await;
assert_eq!(
@@ -402,98 +371,4 @@ mod tests {
"0x6e6963652067726166666974692062726f000000000000000000000000000000"
);
}
#[tokio::test]
async fn check_append_el_version_graffiti_various_length() {
let spec = Arc::new(test_spec::<MinimalEthSpec>());
let harness = get_harness(VALIDATOR_COUNT, spec, None);
let graffiti_vec = vec![
// less than 20 characters, example below is 19 characters
"This is my graffiti",
// 20-23 characters, example below is 22 characters
"This is my graffiti yo",
// 24-27 characters, example below is 26 characters
"This is my graffiti string",
// 28-29 characters, example below is 29 characters
"This is my graffiti string yo",
// 30-32 characters, example below is 32 characters
"This is my graffiti string yo yo",
];
for graffiti in graffiti_vec {
let mut graffiti_bytes = [0; GRAFFITI_BYTES_LEN];
graffiti_bytes[..graffiti.len()].copy_from_slice(graffiti.as_bytes());
// To test appending client version info with user specified graffiti
let policy = GraffitiPolicy::AppendClientVersions;
let found_graffiti_bytes = harness
.chain
.graffiti_calculator
.get_graffiti(GraffitiSettings::Specified {
graffiti: Graffiti::from(graffiti_bytes),
policy,
})
.await
.0;
let mock_commit = DEFAULT_CLIENT_VERSION.commit.clone();
let graffiti_length = graffiti.len();
let append_graffiti_string = match graffiti_length {
0..=19 => format!(
"{}{}{}{}",
DEFAULT_CLIENT_VERSION.code,
mock_commit
.strip_prefix("0x")
.unwrap_or(&mock_commit)
.get(0..4)
.expect("should get first 2 bytes in hex"),
"LH",
lighthouse_version::COMMIT_PREFIX
.get(0..4)
.expect("should get first 2 bytes in hex")
),
20..=23 => format!(
"{}{}{}{}",
DEFAULT_CLIENT_VERSION.code,
mock_commit
.strip_prefix("0x")
.unwrap_or(&mock_commit)
.get(0..2)
.expect("should get first 2 bytes in hex"),
"LH",
lighthouse_version::COMMIT_PREFIX
.get(0..2)
.expect("should get first 2 bytes in hex")
),
24..=27 => format!("{}{}", DEFAULT_CLIENT_VERSION.code, "LH",),
28..=29 => DEFAULT_CLIENT_VERSION.code.to_string(),
// when user graffiti length is 30-32 characters, append nothing
30..=32 => String::new(),
_ => panic!(
"graffiti length should be less than or equal to GRAFFITI_BYTES_LEN (32 characters)"
),
};
let expected_graffiti_string = if append_graffiti_string.is_empty() {
// for the case of empty append_graffiti_string, i.e., user-specified graffiti is 30-32 characters
graffiti.to_string()
} else {
// There is a space between the client version info and user graffiti
// as defined in calculate_graffiti function in engine_api.rs
format!("{} {}", append_graffiti_string, graffiti)
};
let expected_graffiti_prefix_bytes = expected_graffiti_string.as_bytes();
let expected_graffiti_prefix_len =
std::cmp::min(expected_graffiti_prefix_bytes.len(), GRAFFITI_BYTES_LEN);
let found_graffiti_string =
std::str::from_utf8(&found_graffiti_bytes[..expected_graffiti_prefix_len])
.expect("bytes should convert nicely to ascii");
assert_eq!(expected_graffiti_string, found_graffiti_string);
}
}
}

View File

@@ -0,0 +1,214 @@
use parking_lot::{RwLock, RwLockReadGuard};
use ssz_derive::{Decode, Encode};
use std::collections::HashMap;
use types::{Hash256, Slot};
#[derive(Debug, PartialEq)]
pub enum Error {
MismatchingLengths { roots_len: usize, slots_len: usize },
}
/// Maintains a list of `BeaconChain` head block roots and slots.
///
/// Each time a new block is imported, it should be applied to the `Self::register_block` function.
/// In order for this struct to be effective, every single block that is imported must be
/// registered here.
#[derive(Default, Debug)]
pub struct HeadTracker(pub RwLock<HashMap<Hash256, Slot>>);
pub type HeadTrackerReader<'a> = RwLockReadGuard<'a, HashMap<Hash256, Slot>>;
impl HeadTracker {
/// Register a block with `Self`, so it may or may not be included in a `Self::heads` call.
///
/// This function assumes that no block is imported without its parent having already been
/// imported. It cannot detect an error if this is not the case, it is the responsibility of
/// the upstream user.
pub fn register_block(&self, block_root: Hash256, parent_root: Hash256, slot: Slot) {
let mut map = self.0.write();
map.remove(&parent_root);
map.insert(block_root, slot);
}
/// Returns true iff `block_root` is a recognized head.
pub fn contains_head(&self, block_root: Hash256) -> bool {
self.0.read().contains_key(&block_root)
}
/// Returns the list of heads in the chain.
pub fn heads(&self) -> Vec<(Hash256, Slot)> {
self.0
.read()
.iter()
.map(|(root, slot)| (*root, *slot))
.collect()
}
/// Returns a `SszHeadTracker`, which contains all necessary information to restore the state
/// of `Self` at some later point.
///
/// Should ONLY be used for tests, due to the potential for database races.
///
/// See <https://github.com/sigp/lighthouse/issues/4773>
#[cfg(test)]
pub fn to_ssz_container(&self) -> SszHeadTracker {
SszHeadTracker::from_map(&self.0.read())
}
/// Creates a new `Self` from the given `SszHeadTracker`, restoring `Self` to the same state of
/// the `Self` that created the `SszHeadTracker`.
pub fn from_ssz_container(ssz_container: &SszHeadTracker) -> Result<Self, Error> {
let roots_len = ssz_container.roots.len();
let slots_len = ssz_container.slots.len();
if roots_len != slots_len {
Err(Error::MismatchingLengths {
roots_len,
slots_len,
})
} else {
let map = ssz_container
.roots
.iter()
.zip(ssz_container.slots.iter())
.map(|(root, slot)| (*root, *slot))
.collect::<HashMap<_, _>>();
Ok(Self(RwLock::new(map)))
}
}
}
impl PartialEq<HeadTracker> for HeadTracker {
fn eq(&self, other: &HeadTracker) -> bool {
*self.0.read() == *other.0.read()
}
}
/// Helper struct that is used to encode/decode the state of the `HeadTracker` as SSZ bytes.
///
/// This is used when persisting the state of the `BeaconChain` to disk.
#[derive(Encode, Decode, Clone)]
pub struct SszHeadTracker {
roots: Vec<Hash256>,
slots: Vec<Slot>,
}
impl SszHeadTracker {
pub fn from_map(map: &HashMap<Hash256, Slot>) -> Self {
let (roots, slots) = map.iter().map(|(hash, slot)| (*hash, *slot)).unzip();
SszHeadTracker { roots, slots }
}
}
#[cfg(test)]
mod test {
use super::*;
use ssz::{Decode, Encode};
use types::{BeaconBlock, EthSpec, FixedBytesExtended, MainnetEthSpec};
type E = MainnetEthSpec;
#[test]
fn block_add() {
let spec = &E::default_spec();
let head_tracker = HeadTracker::default();
for i in 0..16 {
let mut block: BeaconBlock<E> = BeaconBlock::empty(spec);
let block_root = Hash256::from_low_u64_be(i);
*block.slot_mut() = Slot::new(i);
*block.parent_root_mut() = if i == 0 {
Hash256::random()
} else {
Hash256::from_low_u64_be(i - 1)
};
head_tracker.register_block(block_root, block.parent_root(), block.slot());
}
assert_eq!(
head_tracker.heads(),
vec![(Hash256::from_low_u64_be(15), Slot::new(15))],
"should only have one head"
);
let mut block: BeaconBlock<E> = BeaconBlock::empty(spec);
let block_root = Hash256::from_low_u64_be(42);
*block.slot_mut() = Slot::new(15);
*block.parent_root_mut() = Hash256::from_low_u64_be(14);
head_tracker.register_block(block_root, block.parent_root(), block.slot());
let heads = head_tracker.heads();
assert_eq!(heads.len(), 2, "should only have two heads");
assert!(
heads
.iter()
.any(|(root, slot)| *root == Hash256::from_low_u64_be(15) && *slot == Slot::new(15)),
"should contain first head"
);
assert!(
heads
.iter()
.any(|(root, slot)| *root == Hash256::from_low_u64_be(42) && *slot == Slot::new(15)),
"should contain second head"
);
}
#[test]
fn empty_round_trip() {
let non_empty = HeadTracker::default();
for i in 0..16 {
non_empty.0.write().insert(Hash256::random(), Slot::new(i));
}
let bytes = non_empty.to_ssz_container().as_ssz_bytes();
assert_eq!(
HeadTracker::from_ssz_container(
&SszHeadTracker::from_ssz_bytes(&bytes).expect("should decode")
),
Ok(non_empty),
"non_empty should pass round trip"
);
}
#[test]
fn non_empty_round_trip() {
let non_empty = HeadTracker::default();
for i in 0..16 {
non_empty.0.write().insert(Hash256::random(), Slot::new(i));
}
let bytes = non_empty.to_ssz_container().as_ssz_bytes();
assert_eq!(
HeadTracker::from_ssz_container(
&SszHeadTracker::from_ssz_bytes(&bytes).expect("should decode")
),
Ok(non_empty),
"non_empty should pass round trip"
);
}
#[test]
fn bad_length() {
let container = SszHeadTracker {
roots: vec![Hash256::random()],
slots: vec![],
};
let bytes = container.as_ssz_bytes();
assert_eq!(
HeadTracker::from_ssz_container(
&SszHeadTracker::from_ssz_bytes(&bytes).expect("should decode")
),
Err(Error::MismatchingLengths {
roots_len: 1,
slots_len: 0
}),
"should fail decoding with bad lengths"
);
}
}

View File

@@ -1,10 +1,10 @@
use crate::data_availability_checker::{AvailableBlock, AvailableBlockData};
use crate::{BeaconChain, BeaconChainTypes, WhenSlotSkipped, metrics};
use fixed_bytes::FixedBytesExtended;
use crate::data_availability_checker::AvailableBlock;
use crate::{metrics, BeaconChain, BeaconChainTypes};
use itertools::Itertools;
use slog::debug;
use state_processing::{
per_block_processing::ParallelSignatureSets,
signature_sets::{Error as SignatureSetError, block_proposal_signature_set_from_parts},
signature_sets::{block_proposal_signature_set_from_parts, Error as SignatureSetError},
};
use std::borrow::Cow;
use std::iter;
@@ -12,8 +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, debug_span, instrument};
use types::{Hash256, Slot};
use types::{FixedBytesExtended, Hash256, Slot};
/// Use a longer timeout on the pubkey cache.
///
@@ -35,8 +34,6 @@ pub enum HistoricalBlockError {
ValidatorPubkeyCacheTimeout,
/// Logic error: should never occur.
IndexOutOfBounds,
/// Logic error: should never occur.
MissingOldestBlockRoot { slot: Slot },
/// Internal store error
StoreError(StoreError),
}
@@ -59,15 +56,13 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
/// `SignatureSetError` or `InvalidSignature` will be returned.
///
/// To align with sync we allow some excess blocks with slots greater than or equal to
/// `oldest_block_slot` to be provided. They will be re-imported to fill the columns of the
/// checkpoint sync block.
/// `oldest_block_slot` to be provided. They will be ignored without being checked.
///
/// This function should not be called concurrently with any other function that mutates
/// the anchor info (including this function itself). If a concurrent mutation occurs that
/// would violate consistency then an `AnchorInfoConcurrentMutation` error will be returned.
///
/// Return the number of blocks successfully imported.
#[instrument(skip_all)]
pub fn import_historical_block_batch(
&self,
mut blocks: Vec<AvailableBlock<T::EthSpec>>,
@@ -76,12 +71,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
let blob_info = self.store.get_blob_info();
let data_column_info = self.store.get_data_column_info();
// Take all blocks with slots less than or equal to the oldest block slot.
//
// This allows for reimport of the blobs/columns for the finalized block after checkpoint
// sync.
// Take all blocks with slots less than the oldest block slot.
let num_relevant = blocks.partition_point(|available_block| {
available_block.block().slot() <= anchor_info.oldest_block_slot
available_block.block().slot() < anchor_info.oldest_block_slot
});
let total_blocks = blocks.len();
@@ -90,10 +82,11 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
if blocks_to_import.len() != total_blocks {
debug!(
oldest_block_slot = %anchor_info.oldest_block_slot,
total_blocks,
ignored = total_blocks.saturating_sub(blocks_to_import.len()),
"Ignoring some historic blocks"
self.log,
"Ignoring some historic blocks";
"oldest_block_slot" => anchor_info.oldest_block_slot,
"total_blocks" => total_blocks,
"ignored" => total_blocks.saturating_sub(blocks_to_import.len()),
);
}
@@ -101,41 +94,36 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
return Ok(0);
}
// Blobs are stored per block, and data columns are each stored individually
let n_blob_ops_per_block = if self.spec.is_peer_das_scheduled() {
// TODO(das): `available_block includes all sampled columns, but we only need to store
// custody columns. To be clarified in spec PR.
self.data_availability_checker.get_sampling_column_count()
} else {
1
};
let blob_batch_size = blocks_to_import
.iter()
.filter(|available_block| available_block.blobs().is_some())
.count()
.saturating_mul(n_blob_ops_per_block);
let mut expected_block_root = anchor_info.oldest_block_parent;
let mut last_block_root = expected_block_root;
let mut prev_block_slot = anchor_info.oldest_block_slot;
let mut new_oldest_blob_slot = blob_info.oldest_blob_slot;
let mut new_oldest_data_column_slot = data_column_info.oldest_data_column_slot;
let mut blob_batch = Vec::<KeyValueStoreOp>::new();
let mut blob_batch = Vec::with_capacity(blob_batch_size);
let mut cold_batch = Vec::with_capacity(blocks_to_import.len());
let mut hot_batch = Vec::with_capacity(blocks_to_import.len());
let mut signed_blocks = Vec::with_capacity(blocks_to_import.len());
for available_block in blocks_to_import.into_iter().rev() {
let (block_root, block, block_data) = available_block.deconstruct();
let (block_root, block, maybe_blobs, maybe_data_columns) =
available_block.deconstruct();
if block.slot() == anchor_info.oldest_block_slot {
// When reimporting, verify that this is actually the same block (same block root).
let oldest_block_root = self
.block_root_at_slot(block.slot(), WhenSlotSkipped::None)
.ok()
.flatten()
.ok_or(HistoricalBlockError::MissingOldestBlockRoot { slot: block.slot() })?;
if block_root != oldest_block_root {
return Err(HistoricalBlockError::MismatchedBlockRoot {
block_root,
expected_block_root: oldest_block_root,
});
}
debug!(
?block_root,
slot = %block.slot(),
"Re-importing historic block"
);
last_block_root = block_root;
} else if block_root != expected_block_root {
if block_root != expected_block_root {
return Err(HistoricalBlockError::MismatchedBlockRoot {
block_root,
expected_block_root,
@@ -156,24 +144,21 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
);
}
match &block_data {
AvailableBlockData::NoData => (),
AvailableBlockData::Blobs(_) => new_oldest_blob_slot = Some(block.slot()),
AvailableBlockData::DataColumns(_) => {
new_oldest_data_column_slot = Some(block.slot())
}
// Store the blobs too
if let Some(blobs) = maybe_blobs {
new_oldest_blob_slot = Some(block.slot());
self.store
.blobs_as_kv_store_ops(&block_root, blobs, &mut blob_batch);
}
// Store the blobs or data columns too
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])?);
// Store the data columns too
if let Some(data_columns) = maybe_data_columns {
new_oldest_data_column_slot = Some(block.slot());
self.store
.data_columns_as_kv_store_ops(&block_root, data_columns, &mut blob_batch);
}
// Store block roots, including at all skip slots in the freezer DB.
for slot in (block.slot().as_u64()..prev_block_slot.as_u64()).rev() {
debug!(%slot, ?block_root, "Storing frozen block to root mapping");
cold_batch.push(KeyValueStoreOp::PutKeyValue(
DBColumn::BeaconBlockRoots,
slot.to_be_bytes().to_vec(),
@@ -219,11 +204,11 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.ok_or(HistoricalBlockError::IndexOutOfBounds)?
.iter()
.map(|block| block.parent_root())
.chain(iter::once(last_block_root));
.chain(iter::once(anchor_info.oldest_block_parent));
let signature_set = signed_blocks
.iter()
.zip_eq(block_roots)
.filter(|&(_block, block_root)| block_root != self.genesis_block_root)
.filter(|&(_block, block_root)| (block_root != self.genesis_block_root))
.map(|(block, block_root)| {
block_proposal_signature_set_from_parts(
block,
@@ -251,46 +236,37 @@ 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.
{
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)?;
}
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 mut anchor_and_blob_batch = Vec::with_capacity(3);
// Update the blob info.
if new_oldest_blob_slot != blob_info.oldest_blob_slot
&& let Some(oldest_blob_slot) = new_oldest_blob_slot
{
let new_blob_info = BlobInfo {
oldest_blob_slot: Some(oldest_blob_slot),
..blob_info.clone()
};
anchor_and_blob_batch.push(
self.store
.compare_and_set_blob_info(blob_info, new_blob_info)?,
);
if new_oldest_blob_slot != blob_info.oldest_blob_slot {
if let Some(oldest_blob_slot) = new_oldest_blob_slot {
let new_blob_info = BlobInfo {
oldest_blob_slot: Some(oldest_blob_slot),
..blob_info.clone()
};
anchor_and_blob_batch.push(
self.store
.compare_and_set_blob_info(blob_info, new_blob_info)?,
);
}
}
// Update the data column info.
if new_oldest_data_column_slot != data_column_info.oldest_data_column_slot
&& let Some(oldest_data_column_slot) = new_oldest_data_column_slot
{
let new_data_column_info = DataColumnInfo {
oldest_data_column_slot: Some(oldest_data_column_slot),
};
anchor_and_blob_batch.push(
self.store
.compare_and_set_data_column_info(data_column_info, new_data_column_info)?,
);
if new_oldest_data_column_slot != data_column_info.oldest_data_column_slot {
if let Some(oldest_data_column_slot) = new_oldest_data_column_slot {
let new_data_column_info = DataColumnInfo {
oldest_data_column_slot: Some(oldest_data_column_slot),
};
anchor_and_blob_batch.push(
self.store
.compare_and_set_data_column_info(data_column_info, new_data_column_info)?,
);
}
}
// Update the anchor.
@@ -309,7 +285,10 @@ 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.archive {
if backfill_complete
&& self.genesis_backfill_slot == Slot::new(0)
&& self.config.reconstruct_historic_states
{
self.store_migrator.process_reconstruction();
}

View File

@@ -1,148 +0,0 @@
use std::collections::{HashMap, HashSet};
use crate::{
BeaconChain, BeaconChainError, BeaconChainTypes,
data_column_verification::verify_kzg_for_data_column_list,
};
use store::{Error as StoreError, KeyValueStore};
use tracing::{Span, debug, instrument};
use types::{ColumnIndex, DataColumnSidecarList, Epoch, EthSpec, Hash256, Slot};
#[derive(Debug)]
pub enum HistoricalDataColumnError {
// The provided data column sidecar pertains to a block that doesn't exist in the database.
NoBlockFound {
data_column_block_root: Hash256,
expected_block_root: Hash256,
},
/// Logic error: should never occur.
IndexOutOfBounds,
/// The provided data column sidecar list doesn't contain columns for the full range of slots for the given epoch.
MissingDataColumns {
missing_slots_and_data_columns: Vec<(Slot, ColumnIndex)>,
},
/// The provided data column sidecar list contains at least one column with an invalid kzg commitment.
InvalidKzg,
/// Internal store error
StoreError(StoreError),
/// Internal beacon chain error
BeaconChainError(Box<BeaconChainError>),
}
impl From<StoreError> for HistoricalDataColumnError {
fn from(e: StoreError) -> Self {
Self::StoreError(e)
}
}
impl<T: BeaconChainTypes> BeaconChain<T> {
/// Store a batch of historical data columns in the database.
///
/// The data columns block roots and proposer signatures are verified with the existing
/// block stored in the DB. This function also verifies the columns KZG committments.
///
/// This function requires that the data column sidecar list contains columns for a full epoch.
///
/// Return the number of `data_columns` successfully imported.
#[instrument(skip_all, fields(columns_imported_count = tracing::field::Empty ))]
pub fn import_historical_data_column_batch(
&self,
epoch: Epoch,
historical_data_column_sidecar_list: DataColumnSidecarList<T::EthSpec>,
expected_cgc: u64,
) -> Result<usize, HistoricalDataColumnError> {
let mut total_imported = 0;
let mut ops = vec![];
let unique_column_indices = historical_data_column_sidecar_list
.iter()
.map(|item| *item.index())
.collect::<HashSet<_>>();
let mut slot_and_column_index_to_data_columns = historical_data_column_sidecar_list
.iter()
.map(|data_column| ((data_column.slot(), *data_column.index()), data_column))
.collect::<HashMap<_, _>>();
let forward_blocks_iter = self
.forwards_iter_block_roots_until(
epoch.start_slot(T::EthSpec::slots_per_epoch()),
epoch.end_slot(T::EthSpec::slots_per_epoch()),
)
.map_err(|e| HistoricalDataColumnError::BeaconChainError(Box::new(e)))?;
for block_iter_result in forward_blocks_iter {
let (block_root, slot) = block_iter_result
.map_err(|e| HistoricalDataColumnError::BeaconChainError(Box::new(e)))?;
let fork_name = self.spec.fork_name_at_slot::<T::EthSpec>(slot);
for column_index in unique_column_indices.clone() {
if let Some(data_column) =
slot_and_column_index_to_data_columns.remove(&(slot, column_index))
{
if self
.store
.get_data_column(&block_root, data_column.index(), fork_name)?
.is_some()
{
continue;
}
if block_root != data_column.block_root() {
return Err(HistoricalDataColumnError::NoBlockFound {
data_column_block_root: data_column.block_root(),
expected_block_root: block_root,
});
}
self.store.data_column_as_kv_store_ops(
&block_root,
data_column.clone(),
&mut ops,
);
total_imported += 1;
}
}
}
// If we've made it to here with no columns to import, this means there are no blobs for this epoch.
// `RangeDataColumnBatchRequest` logic should have caught any bad peers withholding columns
if historical_data_column_sidecar_list.is_empty() {
if !ops.is_empty() {
// This shouldn't be a valid case. If there are no columns to import,
// there should be no generated db operations.
return Err(HistoricalDataColumnError::IndexOutOfBounds);
}
} else {
verify_kzg_for_data_column_list(historical_data_column_sidecar_list.iter(), &self.kzg)
.map_err(|_| HistoricalDataColumnError::InvalidKzg)?;
self.store.blobs_db.do_atomically(ops)?;
}
if !slot_and_column_index_to_data_columns.is_empty() {
debug!(
?epoch,
extra_data = ?slot_and_column_index_to_data_columns.keys().map(|(slot, _)| slot),
"We've received unexpected extra data columns, these will not be imported"
);
}
self.data_availability_checker
.custody_context()
.update_and_backfill_custody_count_at_epoch(epoch, expected_cgc);
self.safely_backfill_data_column_custody_info(epoch)
.map_err(|e| HistoricalDataColumnError::BeaconChainError(Box::new(e)))?;
debug!(?epoch, total_imported, "Imported historical data columns");
let current_span = Span::current();
current_span.record("columns_imported_count", total_imported);
Ok(total_imported)
}
}

View File

@@ -1,56 +0,0 @@
//! Beacon chain database invariant checks.
//!
//! Builds the `InvariantContext` from beacon chain state and delegates all checks
//! to `HotColdDB::check_invariants`.
use crate::BeaconChain;
use crate::beacon_chain::BeaconChainTypes;
use store::invariants::{InvariantCheckResult, InvariantContext};
impl<T: BeaconChainTypes> BeaconChain<T> {
/// Run all database invariant checks.
///
/// Collects context from fork choice, state cache, custody columns, and pubkey cache,
/// then delegates to the store-level `check_invariants` method.
pub fn check_database_invariants(&self) -> Result<InvariantCheckResult, store::Error> {
let fork_choice_blocks = {
let fc = self.canonical_head.fork_choice_read_lock();
let proto_array = fc.proto_array().core_proto_array();
proto_array
.nodes
.iter()
.filter(|node| {
// Only check blocks that are descendants of the finalized checkpoint.
// Pruned non-canonical fork blocks may linger in the proto-array but
// are legitimately absent from the database.
fc.is_finalized_checkpoint_or_descendant(node.root())
})
.map(|node| (node.root(), node.slot()))
.collect()
};
let custody_context = self.data_availability_checker.custody_context();
let ctx = InvariantContext {
fork_choice_blocks,
state_cache_roots: self.store.state_cache.lock().state_roots(),
custody_columns: custody_context
.custody_columns_for_epoch(None, &self.spec)
.to_vec(),
pubkey_cache_pubkeys: {
let cache = self.validator_pubkey_cache.read();
(0..cache.len())
.filter_map(|i| {
cache.get(i).map(|pk| {
use store::StoreItem;
crate::validator_pubkey_cache::DatabasePubkey::from_pubkey(pk)
.as_store_bytes()
})
})
.collect()
},
};
self.store.check_invariants(&ctx)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
pub mod attestation_rewards;
pub mod attestation_simulator;
pub mod attestation_verification;
mod attester_cache;
pub mod beacon_block_reward;
mod beacon_block_streamer;
mod beacon_chain;
@@ -9,27 +10,31 @@ pub mod beacon_proposer_cache;
mod beacon_snapshot;
pub mod bellatrix_readiness;
pub mod blob_verification;
mod block_production;
pub mod block_reward;
mod block_times_cache;
mod block_verification;
pub mod block_verification_types;
pub mod builder;
pub mod canonical_head;
pub mod capella_readiness;
pub mod chain_config;
pub mod custody_context;
pub mod data_availability_checker;
pub mod data_column_verification;
pub mod deneb_readiness;
mod early_attester_cache;
pub mod envelope_times_cache;
pub mod electra_readiness;
mod errors;
pub mod eth1_chain;
mod eth1_finalization_cache;
pub mod events;
pub mod execution_payload;
pub mod fetch_blobs;
pub mod fork_choice_signal;
pub mod fork_revert;
pub mod fulu_readiness;
pub mod graffiti_calculator;
mod head_tracker;
pub mod historical_blocks;
pub mod historical_data_columns;
pub mod invariants;
pub mod kzg_utils;
pub mod light_client_finality_update_verification;
pub mod light_client_optimistic_update_verification;
@@ -43,24 +48,14 @@ pub mod observed_block_producers;
pub mod observed_data_sidecars;
pub mod observed_operations;
mod observed_slashable;
pub mod partial_data_column_assembler;
pub mod payload_attestation_verification;
pub mod payload_bid_verification;
pub mod payload_envelope_streamer;
pub mod payload_envelope_verification;
pub mod pending_payload_cache;
pub mod pending_payload_envelopes;
pub mod persisted_beacon_chain;
pub mod persisted_custody;
mod persisted_beacon_chain;
mod persisted_fork_choice;
mod pre_finalization_cache;
pub mod proposer_preferences_verification;
pub mod proposer_prep_service;
pub mod schema_change;
pub mod shuffling_cache;
pub mod single_attestation;
pub mod state_advance_timer;
pub mod summaries_dag;
pub mod sync_committee_rewards;
pub mod sync_committee_verification;
pub mod test_utils;
@@ -70,29 +65,26 @@ pub mod validator_pubkey_cache;
pub use self::beacon_chain::{
AttestationProcessingOutcome, AvailabilityProcessingStatus, BeaconBlockResponse,
BeaconBlockResponseWrapper, BeaconChain, BeaconChainTypes, BeaconStore, BlockProcessStatus,
ChainSegmentResult, ForkChoiceError, INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON,
INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, LightClientProducerEvent, OverrideForkchoiceUpdate,
ChainSegmentResult, ForkChoiceError, LightClientProducerEvent, OverrideForkchoiceUpdate,
ProduceBlockVerification, StateSkipConfig, WhenSlotSkipped,
INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON,
INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON,
};
pub use self::beacon_snapshot::BeaconSnapshot;
pub use self::chain_config::ChainConfig;
pub use self::errors::{BeaconChainError, BlockProductionError};
pub use self::historical_blocks::HistoricalBlockError;
pub use attestation_verification::Error as AttestationError;
pub use beacon_fork_choice_store::{
BeaconForkChoiceStore, Error as ForkChoiceStoreError, PersistedForkChoiceStore,
PersistedForkChoiceStoreV28,
};
pub use beacon_fork_choice_store::{BeaconForkChoiceStore, Error as ForkChoiceStoreError};
pub use block_verification::{
BlockError, ExecutionPayloadError, ExecutionPendingBlock, GossipVerifiedBlock,
IntoExecutionPendingBlock, IntoGossipVerifiedBlock, InvalidSignature,
PayloadVerificationOutcome, PayloadVerificationStatus, build_blob_data_column_sidecars,
get_block_root, signature_verify_chain_segment,
build_blob_data_column_sidecars, get_block_root, BlockError, ExecutionPayloadError,
ExecutionPendingBlock, GossipVerifiedBlock, IntoExecutionPendingBlock, IntoGossipVerifiedBlock,
InvalidSignature, PayloadVerificationOutcome, PayloadVerificationStatus,
};
pub use block_verification_types::AvailabilityPendingExecutedBlock;
pub use block_verification_types::ExecutedBlock;
pub use canonical_head::{CachedHead, CanonicalHead, CanonicalHeadRwLock};
pub use custody_context::CustodyContext;
pub use eth1_chain::{Eth1Chain, Eth1ChainBackend};
pub use events::ServerSentEventHandler;
pub use execution_layer::EngineState;
pub use execution_payload::NotifyExecutionLayer;

View File

@@ -1,9 +1,9 @@
use crate::{BeaconChain, BeaconChainTypes};
use educe::Educe;
use derivative::Derivative;
use slot_clock::SlotClock;
use std::time::Duration;
use strum::AsRefStr;
use types::{Hash256, LightClientFinalityUpdate, Slot};
use types::LightClientFinalityUpdate;
/// Returned when a light client finality update was not successfully verified. It might not have been verified for
/// two reasons:
@@ -21,42 +21,17 @@ pub enum Error {
///
/// Assuming the local clock is correct, the peer has sent an invalid message.
TooEarly,
/// Light client finalized update message does not match the locally constructed one, it has a
/// different signature slot.
MismatchedSignatureSlot { local: Slot, observed: Slot },
/// Light client finalized update message does not match the locally constructed one, it has a
/// different finalized block header for the same signature slot.
MismatchedFinalizedHeader {
local_finalized_header_root: Hash256,
observed_finalized_header_root: Hash256,
signature_slot: Slot,
},
/// Light client finalized update message does not match the locally constructed one, it has a
/// different attested block header for the same signature slot and finalized header.
MismatchedAttestedHeader {
local_attested_header_root: Hash256,
observed_attested_header_root: Hash256,
finalized_header_root: Hash256,
signature_slot: Slot,
},
/// Light client finalized update message does not match the locally constructed one, it has a
/// different proof or sync aggregate for the same slot, attested header and finalized header.
MismatchedProofOrSyncAggregate {
attested_header_root: Hash256,
finalized_header_root: Hash256,
signature_slot: Slot,
},
/// Light client finality update message does not match the locally constructed one.
InvalidLightClientFinalityUpdate,
/// Signature slot start time is none.
SigSlotStartIsNone,
/// Failed to construct a LightClientFinalityUpdate from state.
FailedConstructingUpdate,
/// Silently ignore this light client finality update
Ignore,
}
/// Wraps a `LightClientFinalityUpdate` that has been verified for propagation on the gossip network.
#[derive(Educe)]
#[educe(Clone(bound(T: BeaconChainTypes)))]
#[derive(Derivative)]
#[derivative(Clone(bound = "T: BeaconChainTypes"))]
pub struct VerifiedLightClientFinalityUpdate<T: BeaconChainTypes> {
light_client_finality_update: LightClientFinalityUpdate<T::EthSpec>,
seen_timestamp: Duration,
@@ -73,91 +48,25 @@ impl<T: BeaconChainTypes> VerifiedLightClientFinalityUpdate<T> {
// verify that enough time has passed for the block to have been propagated
let start_time = chain
.slot_clock
.start_of(rcv_finality_update.signature_slot())
.start_of(*rcv_finality_update.signature_slot())
.ok_or(Error::SigSlotStartIsNone)?;
let sync_message_due = chain.spec.get_sync_message_due();
let one_third_slot_duration = Duration::new(chain.spec.seconds_per_slot / 3, 0);
if seen_timestamp + chain.spec.maximum_gossip_clock_disparity()
< start_time + sync_message_due
< start_time + one_third_slot_duration
{
return Err(Error::TooEarly);
}
if let Some(latest_broadcasted_finality_update) = chain
.light_client_server_cache
.get_latest_broadcasted_finality_update()
{
// Ignore the incoming finality update if we've already broadcasted it
if latest_broadcasted_finality_update == rcv_finality_update {
return Err(Error::Ignore);
}
// Ignore the incoming finality update if the latest broadcasted attested header slot
// is greater than the incoming attested header slot.
if latest_broadcasted_finality_update.get_attested_header_slot()
> rcv_finality_update.get_attested_header_slot()
{
return Err(Error::Ignore);
}
}
let latest_finality_update = chain
.light_client_server_cache
.get_latest_finality_update()
.ok_or(Error::FailedConstructingUpdate)?;
// Ignore the incoming finality update if the latest constructed attested header slot
// is greater than the incoming attested header slot.
if latest_finality_update.get_attested_header_slot()
> rcv_finality_update.get_attested_header_slot()
{
return Err(Error::Ignore);
}
// Verify that the gossiped finality update is the same as the locally constructed one.
// verify that the gossiped finality update is the same as the locally constructed one.
if latest_finality_update != rcv_finality_update {
let signature_slot = latest_finality_update.signature_slot();
if signature_slot != rcv_finality_update.signature_slot() {
// The locally constructed finality update is not up to date, probably
// because the node has fallen behind and needs to sync.
if rcv_finality_update.signature_slot() > signature_slot {
return Err(Error::Ignore);
}
return Err(Error::MismatchedSignatureSlot {
local: signature_slot,
observed: rcv_finality_update.signature_slot(),
});
}
let local_finalized_header_root = latest_finality_update.get_finalized_header_root();
let observed_finalized_header_root = rcv_finality_update.get_finalized_header_root();
if local_finalized_header_root != observed_finalized_header_root {
return Err(Error::MismatchedFinalizedHeader {
local_finalized_header_root,
observed_finalized_header_root,
signature_slot,
});
}
let local_attested_header_root = latest_finality_update.get_attested_header_root();
let observed_attested_header_root = rcv_finality_update.get_attested_header_root();
if local_attested_header_root != observed_attested_header_root {
return Err(Error::MismatchedAttestedHeader {
local_attested_header_root,
observed_attested_header_root,
finalized_header_root: local_finalized_header_root,
signature_slot,
});
}
return Err(Error::MismatchedProofOrSyncAggregate {
attested_header_root: local_attested_header_root,
finalized_header_root: local_finalized_header_root,
signature_slot,
});
return Err(Error::InvalidLightClientFinalityUpdate);
}
chain
.light_client_server_cache
.set_latest_broadcasted_finality_update(rcv_finality_update.clone());
Ok(Self {
light_client_finality_update: rcv_finality_update,
seen_timestamp,

View File

@@ -1,10 +1,10 @@
use crate::{BeaconChain, BeaconChainTypes};
use educe::Educe;
use derivative::Derivative;
use eth2::types::Hash256;
use slot_clock::SlotClock;
use std::time::Duration;
use strum::AsRefStr;
use types::{LightClientOptimisticUpdate, Slot};
use types::LightClientOptimisticUpdate;
/// Returned when a light client optimistic update was not successfully verified. It might not have been verified for
/// two reasons:
@@ -22,35 +22,19 @@ pub enum Error {
///
/// Assuming the local clock is correct, the peer has sent an invalid message.
TooEarly,
/// Light client optimistic update message does not match the locally constructed one, it has a
/// different signature slot.
MismatchedSignatureSlot { local: Slot, observed: Slot },
/// Light client optimistic update message does not match the locally constructed one, it has a
/// different block header at the same slot.
MismatchedAttestedHeader {
local_attested_header_root: Hash256,
observed_attested_header_root: Hash256,
signature_slot: Slot,
},
/// Light client optimistic update message does not match the locally constructed one, it has a
/// different sync aggregate for the same slot and attested header.
MismatchedSyncAggregate {
attested_header_root: Hash256,
signature_slot: Slot,
},
/// Light client optimistic update message does not match the locally constructed one.
InvalidLightClientOptimisticUpdate,
/// Signature slot start time is none.
SigSlotStartIsNone,
/// Failed to construct a LightClientOptimisticUpdate from state.
FailedConstructingUpdate,
/// Unknown block with parent root.
UnknownBlockParentRoot(Hash256),
/// Silently ignore this light client optimistic update
Ignore,
}
/// Wraps a `LightClientOptimisticUpdate` that has been verified for propagation on the gossip network.
#[derive(Educe)]
#[educe(Clone(bound(T: BeaconChainTypes)))]
#[derive(Derivative)]
#[derivative(Clone(bound = "T: BeaconChainTypes"))]
pub struct VerifiedLightClientOptimisticUpdate<T: BeaconChainTypes> {
light_client_optimistic_update: LightClientOptimisticUpdate<T::EthSpec>,
pub parent_root: Hash256,
@@ -68,33 +52,15 @@ impl<T: BeaconChainTypes> VerifiedLightClientOptimisticUpdate<T> {
// verify that enough time has passed for the block to have been propagated
let start_time = chain
.slot_clock
.start_of(rcv_optimistic_update.signature_slot())
.start_of(*rcv_optimistic_update.signature_slot())
.ok_or(Error::SigSlotStartIsNone)?;
let sync_message_due = chain.spec.get_sync_message_due();
let one_third_slot_duration = Duration::new(chain.spec.seconds_per_slot / 3, 0);
if seen_timestamp + chain.spec.maximum_gossip_clock_disparity()
< start_time + sync_message_due
< start_time + one_third_slot_duration
{
return Err(Error::TooEarly);
}
if let Some(latest_broadcasted_optimistic_update) = chain
.light_client_server_cache
.get_latest_broadcasted_optimistic_update()
{
// Ignore the incoming optimistic update if we've already broadcasted it
if latest_broadcasted_optimistic_update == rcv_optimistic_update {
return Err(Error::Ignore);
}
// Ignore the incoming optimistic update if the latest broadcasted slot
// is greater than the incoming slot.
if latest_broadcasted_optimistic_update.get_slot() > rcv_optimistic_update.get_slot() {
return Err(Error::Ignore);
}
}
let head = chain.canonical_head.cached_head();
let head_block = &head.snapshot.beacon_block;
// check if we can process the optimistic update immediately
@@ -110,45 +76,11 @@ impl<T: BeaconChainTypes> VerifiedLightClientOptimisticUpdate<T> {
.get_latest_optimistic_update()
.ok_or(Error::FailedConstructingUpdate)?;
// Ignore the incoming optimistic update if the latest constructed slot
// is greater than the incoming slot.
if latest_optimistic_update.get_slot() > rcv_optimistic_update.get_slot() {
return Err(Error::Ignore);
}
// Verify that the gossiped optimistic update is the same as the locally constructed one.
// verify that the gossiped optimistic update is the same as the locally constructed one.
if latest_optimistic_update != rcv_optimistic_update {
let signature_slot = latest_optimistic_update.signature_slot();
if signature_slot != rcv_optimistic_update.signature_slot() {
// The locally constructed optimistic update is not up to date, probably
// because the node has fallen behind and needs to sync.
if rcv_optimistic_update.signature_slot() > signature_slot {
return Err(Error::Ignore);
}
return Err(Error::MismatchedSignatureSlot {
local: signature_slot,
observed: rcv_optimistic_update.signature_slot(),
});
}
let local_attested_header_root = latest_optimistic_update.get_canonical_root();
let observed_attested_header_root = rcv_optimistic_update.get_canonical_root();
if local_attested_header_root != observed_attested_header_root {
return Err(Error::MismatchedAttestedHeader {
local_attested_header_root,
observed_attested_header_root,
signature_slot,
});
}
return Err(Error::MismatchedSyncAggregate {
attested_header_root: local_attested_header_root,
signature_slot,
});
return Err(Error::InvalidLightClientOptimisticUpdate);
}
chain
.light_client_server_cache
.set_latest_broadcasted_optimistic_update(rcv_optimistic_update.clone());
let parent_root = rcv_optimistic_update.get_parent_root();
Ok(Self {
light_client_optimistic_update: rcv_optimistic_update,

View File

@@ -1,15 +1,15 @@
use crate::errors::BeaconChainError;
use crate::{BeaconChainTypes, BeaconStore, metrics};
use crate::{metrics, BeaconChainTypes, BeaconStore};
use parking_lot::{Mutex, RwLock};
use safe_arith::SafeArith;
use slog::{debug, Logger};
use ssz::Decode;
use std::num::NonZeroUsize;
use std::sync::Arc;
use store::DBColumn;
use store::KeyValueStore;
use tracing::debug;
use tree_hash::TreeHash;
use types::new_non_zero_usize;
use types::non_zero_usize::new_non_zero_usize;
use types::{
BeaconBlockRef, BeaconState, ChainSpec, Checkpoint, EthSpec, ForkName, Hash256,
LightClientBootstrap, LightClientFinalityUpdate, LightClientOptimisticUpdate,
@@ -40,10 +40,6 @@ pub struct LightClientServerCache<T: BeaconChainTypes> {
latest_written_current_sync_committee: RwLock<Option<Arc<SyncCommittee<T::EthSpec>>>>,
/// Caches state proofs by block root
prev_block_cache: Mutex<lru::LruCache<Hash256, LightClientCachedData<T::EthSpec>>>,
/// Tracks the latest broadcasted finality update
latest_broadcasted_finality_update: RwLock<Option<LightClientFinalityUpdate<T::EthSpec>>>,
/// Tracks the latest broadcasted optimistic update
latest_broadcasted_optimistic_update: RwLock<Option<LightClientOptimisticUpdate<T::EthSpec>>>,
}
impl<T: BeaconChainTypes> LightClientServerCache<T> {
@@ -53,8 +49,6 @@ impl<T: BeaconChainTypes> LightClientServerCache<T> {
latest_optimistic_update: None.into(),
latest_light_client_update: None.into(),
latest_written_current_sync_committee: None.into(),
latest_broadcasted_finality_update: None.into(),
latest_broadcasted_optimistic_update: None.into(),
prev_block_cache: lru::LruCache::new(PREV_BLOCK_CACHE_SIZE).into(),
}
}
@@ -88,6 +82,7 @@ impl<T: BeaconChainTypes> LightClientServerCache<T> {
block_slot: Slot,
block_parent_root: &Hash256,
sync_aggregate: &SyncAggregate<T::EthSpec>,
log: &Logger,
chain_spec: &ChainSpec,
) -> Result<(), BeaconChainError> {
metrics::inc_counter(&metrics::LIGHT_CLIENT_SERVER_CACHE_PROCESSING_REQUESTS);
@@ -175,8 +170,9 @@ impl<T: BeaconChainTypes> LightClientServerCache<T> {
)?);
} else {
debug!(
finalized_block_root = %cached_parts.finalized_block_root,
"Finalized block not available in store for light_client server"
log,
"Finalized block not available in store for light_client server";
"finalized_block_root" => format!("{}", cached_parts.finalized_block_root),
);
}
}
@@ -223,9 +219,10 @@ impl<T: BeaconChainTypes> LightClientServerCache<T> {
) -> Result<(), BeaconChainError> {
if let Some(latest_sync_committee) =
self.latest_written_current_sync_committee.read().clone()
&& latest_sync_committee == cached_parts.current_sync_committee
{
return Ok(());
if latest_sync_committee == cached_parts.current_sync_committee {
return Ok(());
}
};
if finalized_period + 1 >= sync_committee_period {
@@ -322,11 +319,8 @@ impl<T: BeaconChainTypes> LightClientServerCache<T> {
metrics::inc_counter(&metrics::LIGHT_CLIENT_SERVER_CACHE_PREV_BLOCK_CACHE_MISS);
// Compute the value, handling potential errors.
// This state should already be cached. By electing not to cache it here
// we remove any chance of the light client server from affecting the state cache.
// We'd like the light client server to be as minimally invasive as possible.
let mut state = store
.get_state(block_state_root, Some(block_slot), false)?
.get_state(block_state_root, Some(block_slot))?
.ok_or_else(|| {
BeaconChainError::DBInconsistent(format!("Missing state {:?}", block_state_root))
})?;
@@ -339,89 +333,10 @@ impl<T: BeaconChainTypes> LightClientServerCache<T> {
Ok(new_value)
}
/// Checks if we've already broadcasted the latest finality update.
/// If we haven't, update the `latest_broadcasted_finality_update` cache
/// and return the latest finality update for broadcasting, else return `None`.
pub fn should_broadcast_latest_finality_update(
&self,
) -> Option<LightClientFinalityUpdate<T::EthSpec>> {
if let Some(latest_finality_update) = self.get_latest_finality_update() {
let latest_broadcasted_finality_update = self.get_latest_broadcasted_finality_update();
match latest_broadcasted_finality_update {
Some(latest_broadcasted_finality_update) => {
if latest_broadcasted_finality_update != latest_finality_update {
self.set_latest_broadcasted_finality_update(latest_finality_update.clone());
return Some(latest_finality_update);
}
}
None => {
self.set_latest_broadcasted_finality_update(latest_finality_update.clone());
return Some(latest_finality_update);
}
}
}
None
}
pub fn get_latest_finality_update(&self) -> Option<LightClientFinalityUpdate<T::EthSpec>> {
self.latest_finality_update.read().clone()
}
pub fn get_latest_broadcasted_optimistic_update(
&self,
) -> Option<LightClientOptimisticUpdate<T::EthSpec>> {
self.latest_broadcasted_optimistic_update.read().clone()
}
pub fn get_latest_broadcasted_finality_update(
&self,
) -> Option<LightClientFinalityUpdate<T::EthSpec>> {
self.latest_broadcasted_finality_update.read().clone()
}
pub fn set_latest_broadcasted_optimistic_update(
&self,
optimistic_update: LightClientOptimisticUpdate<T::EthSpec>,
) {
*self.latest_broadcasted_optimistic_update.write() = Some(optimistic_update.clone());
}
pub fn set_latest_broadcasted_finality_update(
&self,
finality_update: LightClientFinalityUpdate<T::EthSpec>,
) {
*self.latest_broadcasted_finality_update.write() = Some(finality_update.clone());
}
/// Checks if we've already broadcasted the latest optimistic update.
/// If we haven't, update the `latest_broadcasted_optimistic_update` cache
/// and return the latest optimistic update for broadcasting, else return `None`.
pub fn should_broadcast_latest_optimistic_update(
&self,
) -> Option<LightClientOptimisticUpdate<T::EthSpec>> {
if let Some(latest_optimistic_update) = self.get_latest_optimistic_update() {
let latest_broadcasted_optimistic_update =
self.get_latest_broadcasted_optimistic_update();
match latest_broadcasted_optimistic_update {
Some(latest_broadcasted_optimistic_update) => {
if latest_broadcasted_optimistic_update != latest_optimistic_update {
self.set_latest_broadcasted_optimistic_update(
latest_optimistic_update.clone(),
);
return Some(latest_optimistic_update);
}
}
None => {
self.set_latest_broadcasted_optimistic_update(latest_optimistic_update.clone());
return Some(latest_optimistic_update);
}
}
}
None
}
pub fn get_latest_optimistic_update(&self) -> Option<LightClientOptimisticUpdate<T::EthSpec>> {
self.latest_optimistic_update.read().clone()
}
@@ -458,15 +373,15 @@ impl<T: BeaconChainTypes> LightClientServerCache<T> {
let Some(current_sync_committee_branch) = store.get_sync_committee_branch(block_root)?
else {
return Err(BeaconChainError::LightClientBootstrapError(format!(
"Sync committee branch for block root {:?} not found. This typically occurs when the block is not a finalized checkpoint. Light client bootstrap is only supported for finalized checkpoint block roots.",
"Sync committee branch for block root {:?} not found",
block_root
)));
};
if sync_committee_period > finalized_period {
return Err(BeaconChainError::LightClientBootstrapError(format!(
"The blocks sync committee period {sync_committee_period} is greater than the current finalized period {finalized_period}"
)));
return Err(BeaconChainError::LightClientBootstrapError(
format!("The blocks sync committee period {sync_committee_period} is greater than the current finalized period {finalized_period}"),
));
}
let Some(current_sync_committee) = store.get_sync_committee(sync_committee_period)? else {
@@ -505,13 +420,18 @@ struct LightClientCachedData<E: EthSpec> {
impl<E: EthSpec> LightClientCachedData<E> {
fn from_state(state: &mut BeaconState<E>) -> Result<Self, BeaconChainError> {
let (finality_branch, next_sync_committee_branch, current_sync_committee_branch) = (
state.compute_finalized_root_proof()?,
state.compute_current_sync_committee_proof()?,
state.compute_next_sync_committee_proof()?,
);
Ok(Self {
finalized_checkpoint: state.finalized_checkpoint(),
finality_branch: state.compute_finalized_root_proof()?,
finality_branch,
next_sync_committee: state.next_sync_committee()?.clone(),
current_sync_committee: state.current_sync_committee()?.clone(),
next_sync_committee_branch: state.compute_next_sync_committee_proof()?,
current_sync_committee_branch: state.compute_current_sync_committee_proof()?,
next_sync_committee_branch,
current_sync_committee_branch,
finalized_block_root: state.finalized_checkpoint().root,
})
}

Some files were not shown because too many files have changed in this diff Show More