mirror of
https://github.com/sigp/lighthouse.git
synced 2026-05-29 20:27:14 +00:00
Compare commits
17 Commits
fc-complia
...
v7.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc320e3faa | ||
|
|
e473500456 | ||
|
|
627c1bac89 | ||
|
|
0c5580f23f | ||
|
|
279afb0696 | ||
|
|
fd1ca8ef23 | ||
|
|
df3038d902 | ||
|
|
88c0f9d60a | ||
|
|
c61cf26622 | ||
|
|
2883429f69 | ||
|
|
19fc31a75b | ||
|
|
ff2376efec | ||
|
|
d4586ea92d | ||
|
|
1b9b61bb77 | ||
|
|
11f17e52a0 | ||
|
|
d472689fa2 | ||
|
|
bbc1200b2d |
@@ -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
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
130
.ai/ISSUES.md
130
.ai/ISSUES.md
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "echo '\n[Reminder] Run: cargo fmt --all && make lint-fix'"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
6
.github/CODEOWNERS
vendored
@@ -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
|
||||
|
||||
@@ -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.
|
||||
17
.github/forbidden-files.txt
vendored
17
.github/forbidden-files.txt
vendored
@@ -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
104
.github/mergify.yml
vendored
@@ -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 you’d 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"
|
||||
|
||||
2
.github/workflows/book.yml
vendored
2
.github/workflows/book.yml
vendored
@@ -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
|
||||
|
||||
183
.github/workflows/docker-reproducible.yml
vendored
183
.github/workflows/docker-reproducible.yml
vendored
@@ -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
|
||||
17
.github/workflows/docker.yml
vendored
17
.github/workflows/docker.yml
vendored
@@ -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
|
||||
|
||||
|
||||
2
.github/workflows/linkcheck.yml
vendored
2
.github/workflows/linkcheck.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run mdbook server
|
||||
run: |
|
||||
|
||||
161
.github/workflows/local-testnet.yml
vendored
161
.github/workflows/local-testnet.yml
vendored
@@ -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
|
||||
|
||||
124
.github/workflows/nightly-tests.yml
vendored
124
.github/workflows/nightly-tests.yml
vendored
@@ -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
|
||||
63
.github/workflows/release.yml
vendored
63
.github/workflows/release.yml
vendored
@@ -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*)
|
||||
|
||||
249
.github/workflows/test-suite.yml
vendored
249
.github/workflows/test-suite.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
1
.gitignore
vendored
@@ -9,6 +9,7 @@ perf.data*
|
||||
*.tar.gz
|
||||
/bin
|
||||
genesis.ssz
|
||||
/clippy.toml
|
||||
/.cargo
|
||||
|
||||
# IntelliJ
|
||||
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"rust-analyzer.cargo.cfgs": [
|
||||
"!debug_assertions"
|
||||
]
|
||||
}
|
||||
10
AGENTS.md
10
AGENTS.md
@@ -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
158
CLAUDE.md
@@ -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
|
||||
@@ -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
7202
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
284
Cargo.toml
284
Cargo.toml
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@@ -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 \
|
||||
|
||||
@@ -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
177
Makefile
@@ -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'."
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<
|
||||
ð2_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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
|
||||
@@ -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)),
|
||||
};
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(),
|
||||
))
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
386
beacon_node/beacon_chain/src/attester_cache.rs
Normal file
386
beacon_node/beacon_chain/src/attester_cache.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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)?,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>,
|
||||
|
||||
@@ -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(()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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))
|
||||
}
|
||||
}
|
||||
132
beacon_node/beacon_chain/src/block_reward.rs
Normal file
132
beacon_node/beacon_chain/src/block_reward.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
121
beacon_node/beacon_chain/src/capella_readiness.rs
Normal file
121
beacon_node/beacon_chain/src/capella_readiness.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 { .. }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
121
beacon_node/beacon_chain/src/deneb_readiness.rs
Normal file
121
beacon_node/beacon_chain/src/deneb_readiness.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
115
beacon_node/beacon_chain/src/electra_readiness.rs
Normal file
115
beacon_node/beacon_chain/src/electra_readiness.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
1216
beacon_node/beacon_chain/src/eth1_chain.rs
Normal file
1216
beacon_node/beacon_chain/src/eth1_chain.rs
Normal file
File diff suppressed because it is too large
Load Diff
499
beacon_node/beacon_chain/src/eth1_finalization_cache.rs
Normal file
499
beacon_node/beacon_chain/src/eth1_finalization_cache.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
329
beacon_node/beacon_chain/src/fetch_blobs.rs
Normal file
329
beacon_node/beacon_chain/src/fetch_blobs.rs
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
205
beacon_node/beacon_chain/src/fork_revert.rs
Normal file
205
beacon_node/beacon_chain/src/fork_revert.rs
Normal 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)
|
||||
}
|
||||
115
beacon_node/beacon_chain/src/fulu_readiness.rs
Normal file
115
beacon_node/beacon_chain/src/fulu_readiness.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
214
beacon_node/beacon_chain/src/head_tracker.rs
Normal file
214
beacon_node/beacon_chain/src/head_tracker.rs
Normal 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user