diff --git a/.github/workflows/check-specrefs.yml b/.github/workflows/check-specrefs.yml new file mode 100644 index 000000000000..b7dc415253e8 --- /dev/null +++ b/.github/workflows/check-specrefs.yml @@ -0,0 +1,52 @@ +name: Check Spec References + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +on: + push: + branches: [unstable, stable] + pull_request: + workflow_dispatch: + +jobs: + check-specrefs: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check version consistency + run: | + SPEC_TEST_VERSION=$(grep 'specVersion:' packages/beacon-node/test/spec/specTestVersioning.ts | head -1 | sed 's/.*specVersion: "\(.*\)".*/\1/') + ETHSPECIFY_VERSION=$(grep '^version:' specrefs/.ethspecify.yml | sed 's/version: //') + if [ "$SPEC_TEST_VERSION" != "$ETHSPECIFY_VERSION" ]; then + echo "Version mismatch between specTestVersioning.ts and ethspecify" + echo " packages/beacon-node/test/spec/specTestVersioning.ts: $SPEC_TEST_VERSION" + echo " specrefs/.ethspecify.yml: $ETHSPECIFY_VERSION" + exit 1 + else + echo "Versions match: $SPEC_TEST_VERSION" + fi + + - name: Install ethspecify + run: python3 -mpip install ethspecify + + - name: Update spec references + run: ethspecify process --path=specrefs + + - name: Check for differences + run: | + if ! git diff --exit-code -- specrefs >/dev/null; then + echo "Spec references are out-of-date!" + echo "" + git --no-pager diff -- specrefs + exit 1 + else + echo "Spec references are up-to-date!" + fi + + - name: Check spec references + run: ethspecify check --path=specrefs diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 000000000000..0692e557e186 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,108 @@ +name: Build and publish Docker images + +on: + workflow_call: + inputs: + tag: + description: "Docker image tag" + required: true + type: string + extra-tags: + description: "Extra tags to apply (comma-separated, e.g. 'latest,rc')" + required: false + type: string + ref: + description: "Git ref to checkout (defaults to triggering ref)" + required: false + type: string + +jobs: + docker: + name: Build Docker (${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: true + matrix: + include: + - arch: amd64 + runner: buildjet-4vcpu-ubuntu-2204 + - arch: arm64 + runner: buildjet-4vcpu-ubuntu-2204-arm + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref || github.sha }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push lodestar + run: > + docker buildx build . --push + --tag chainsafe/lodestar:${{ inputs.tag }}-${{ matrix.arch }} + --platform linux/${{ matrix.arch }} + --build-arg COMMIT=$(git rev-parse HEAD) + + - name: Build and push custom Grafana + run: > + docker buildx build ./docker/grafana/ --push + --file ./docker/grafana/Dockerfile + --build-context dashboards=./dashboards + --tag chainsafe/lodestar-grafana:${{ inputs.tag }}-${{ matrix.arch }} + --platform linux/${{ matrix.arch }} + + - name: Build and push custom Prometheus + run: > + docker buildx build ./docker/prometheus/ --push + --file ./docker/prometheus/Dockerfile + --tag chainsafe/lodestar-prometheus:${{ inputs.tag }}-${{ matrix.arch }} + --platform linux/${{ matrix.arch }} + + docker-manifest: + name: Create Docker manifest + runs-on: ubuntu-latest + needs: docker + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Create and push lodestar manifest + run: | + EXTRA_TAGS="" + if [ -n "${{ inputs.extra-tags }}" ]; then + for t in $(echo "${{ inputs.extra-tags }}" | tr ',' ' '); do + EXTRA_TAGS="$EXTRA_TAGS -t chainsafe/lodestar:$t" + done + fi + docker buildx imagetools create -t chainsafe/lodestar:${{ inputs.tag }} $EXTRA_TAGS \ + chainsafe/lodestar:${{ inputs.tag }}-amd64 \ + chainsafe/lodestar:${{ inputs.tag }}-arm64 + + - name: Create and push grafana manifest + run: | + docker buildx imagetools create -t chainsafe/lodestar-grafana:${{ inputs.tag }} \ + chainsafe/lodestar-grafana:${{ inputs.tag }}-amd64 \ + chainsafe/lodestar-grafana:${{ inputs.tag }}-arm64 + + - name: Create and push prometheus manifest + run: | + docker buildx imagetools create -t chainsafe/lodestar-prometheus:${{ inputs.tag }} \ + chainsafe/lodestar-prometheus:${{ inputs.tag }}-amd64 \ + chainsafe/lodestar-prometheus:${{ inputs.tag }}-arm64 + + - name: Sanity check image + run: | + docker run -d --name lodestar-sanity -p 9596:9596 chainsafe/lodestar:${{ inputs.tag }} dev --rest.address 0.0.0.0 + sleep 30 + curl -f http://127.0.0.1:9596/eth/v1/node/version || (docker logs lodestar-sanity && exit 1) + docker stop lodestar-sanity + + - run: docker image history chainsafe/lodestar:${{ inputs.tag }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ee726ab12e92..c605199aee46 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -67,7 +67,7 @@ jobs: - name: Build docs working-directory: docs - run: pnpm && pnpm build + run: pnpm install && pnpm build - name: Deploy uses: peaceiris/actions-gh-pages@v3 diff --git a/.github/workflows/publish-dev.yml b/.github/workflows/publish-dev.yml index d2a43fb343b8..0abec636425a 100644 --- a/.github/workflows/publish-dev.yml +++ b/.github/workflows/publish-dev.yml @@ -112,44 +112,8 @@ jobs: docker: name: Publish to Docker Hub - runs-on: buildjet-4vcpu-ubuntu-2204 needs: npm - steps: - - uses: actions/checkout@v4 - # https://github.com/docker/setup-qemu-action - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - # https://github.com/docker/setup-buildx-action - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push lodestar - run: > - docker buildx build . --push - --tag chainsafe/lodestar:next - --platform linux/amd64,linux/arm64 - --build-arg COMMIT=$(git rev-parse HEAD) - - - run: docker run chainsafe/lodestar:next --help - # Display history to know byte size of each layer - # Image is available only because of the previous `docker run` command - - run: docker image history chainsafe/lodestar:next - - - name: Build and push custom Grafana - run: > - docker buildx build ./docker/grafana/ --push - --file ./docker/grafana/Dockerfile - --build-context dashboards=./dashboards - --tag chainsafe/lodestar-grafana:next - --platform linux/amd64,linux/arm64 - - - name: Build and push custom Prometheus - run: > - docker buildx build ./docker/prometheus/ --push - --file ./docker/prometheus/Dockerfile - --tag chainsafe/lodestar-prometheus:next - --platform linux/amd64,linux/arm64 + uses: ./.github/workflows/docker.yml + with: + tag: next + secrets: inherit diff --git a/.github/workflows/publish-manual.yml b/.github/workflows/publish-manual.yml new file mode 100644 index 000000000000..ad7bc6a31321 --- /dev/null +++ b/.github/workflows/publish-manual.yml @@ -0,0 +1,49 @@ +name: Publish manual Docker image + +# Manual workflow for tagging Docker images from any commit +# Useful for testing specific commits or creating custom releases + +on: + workflow_dispatch: + inputs: + ref: + description: "Git ref to build from (commit SHA, branch, or tag)" + required: true + type: string + tag: + description: "Docker tag to apply (e.g., test-feature, debug-v1.2.3)" + required: true + type: string + +permissions: + contents: read + +jobs: + validate: + name: Validate inputs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + fetch-depth: 0 + + - name: Validate inputs + run: | + # Ensure tag doesn't conflict with protected tags + if [[ "${{ inputs.tag }}" =~ ^(latest|next|stable|rc)$ ]]; then + echo "Error: Cannot use reserved tag '${{ inputs.tag }}'. Use a custom tag name." + exit 1 + fi + echo "Building from ref: ${{ inputs.ref }}" + echo "Commit: $(git rev-parse HEAD)" + echo "Tag: ${{ inputs.tag }}" + + docker: + name: Build and publish Docker + needs: validate + uses: ./.github/workflows/docker.yml + with: + tag: ${{ inputs.tag }} + ref: ${{ inputs.ref }} + secrets: inherit diff --git a/.github/workflows/publish-nextfork.yml b/.github/workflows/publish-nextfork.yml index 3cb87462bd45..84eabc546728 100644 --- a/.github/workflows/publish-nextfork.yml +++ b/.github/workflows/publish-nextfork.yml @@ -113,44 +113,8 @@ jobs: docker: name: Publish to Docker Hub - runs-on: buildjet-4vcpu-ubuntu-2204 needs: npm - steps: - - uses: actions/checkout@v4 - # https://github.com/docker/setup-qemu-action - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - # https://github.com/docker/setup-buildx-action - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push lodestar - run: > - docker buildx build . --push - --tag chainsafe/lodestar:nextfork - --platform linux/amd64,linux/arm64 - --build-arg COMMIT=$(git rev-parse HEAD) - - - run: docker run chainsafe/lodestar:nextfork --help - # Display history to know byte size of each layer - # Image is available only because of the previous `docker run` command - - run: docker image history chainsafe/lodestar:nextfork - - - name: Build and push custom Grafana - run: > - docker buildx build ./docker/grafana/ --push - --file ./docker/grafana/Dockerfile - --build-context dashboards=./dashboards - --tag chainsafe/lodestar-grafana:nextfork - --platform linux/amd64,linux/arm64 - - - name: Build and push custom Prometheus - run: > - docker buildx build ./docker/prometheus/ --push - --file ./docker/prometheus/Dockerfile - --tag chainsafe/lodestar-prometheus:nextfork - --platform linux/amd64,linux/arm64 + uses: ./.github/workflows/docker.yml + with: + tag: nextfork + secrets: inherit diff --git a/.github/workflows/publish-rc.yml b/.github/workflows/publish-rc.yml index 87d0a357fdae..aabcde7ee3d5 100644 --- a/.github/workflows/publish-rc.yml +++ b/.github/workflows/publish-rc.yml @@ -119,49 +119,21 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - docker: - name: Publish to Docker Hub - runs-on: buildjet-4vcpu-ubuntu-2204 + await-release: + name: Await npm release + runs-on: ubuntu-latest needs: [tag, npm] if: needs.tag.outputs.is_rc == 'true' steps: - uses: actions/checkout@v4 - run: scripts/await-release.sh ${{ needs.tag.outputs.tag }} rc 900 - # https://github.com/docker/setup-qemu-action - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - # https://github.com/docker/setup-buildx-action - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push - run: > - docker buildx build . --push - --tag chainsafe/lodestar:rc - --tag chainsafe/lodestar:${{ needs.tag.outputs.tag }} - --platform linux/amd64,linux/arm64 - --build-arg COMMIT=$(git rev-parse HEAD) - - - run: docker run chainsafe/lodestar:${{ needs.tag.outputs.tag }} --help - # Display history to know byte size of each layer - # Image is available only because of the previous `docker run` command - - run: docker image history chainsafe/lodestar:${{ needs.tag.outputs.tag }} - - - name: Build and push custom Grafana - run: > - docker buildx build ./docker/grafana/ --push - --file ./docker/grafana/Dockerfile - --build-context dashboards=./dashboards - --tag chainsafe/lodestar-grafana:${{ needs.tag.outputs.tag }} - --platform linux/amd64,linux/arm64 - - - name: Build and push custom Prometheus - run: > - docker buildx build ./docker/prometheus/ --push - --file ./docker/prometheus/Dockerfile - --tag chainsafe/lodestar-prometheus:${{ needs.tag.outputs.tag }} - --platform linux/amd64,linux/arm64 + + docker: + name: Publish to Docker Hub + needs: [tag, await-release] + if: needs.tag.outputs.is_rc == 'true' + uses: ./.github/workflows/docker.yml + with: + tag: ${{ needs.tag.outputs.tag }} + extra-tags: rc + secrets: inherit diff --git a/.github/workflows/publish-stable.yml b/.github/workflows/publish-stable.yml index d56787dceb2f..7bf82a6fe45d 100644 --- a/.github/workflows/publish-stable.yml +++ b/.github/workflows/publish-stable.yml @@ -119,49 +119,21 @@ jobs: with: token: ${{ secrets.GH_PAGES_TOKEN }} - docker: - name: Publish to Docker Hub - runs-on: buildjet-4vcpu-ubuntu-2204 + await-release: + name: Await npm release + runs-on: ubuntu-latest needs: [tag, npm] if: needs.tag.outputs.is_stable == 'true' steps: - uses: actions/checkout@v4 - run: scripts/await-release.sh ${{ needs.tag.outputs.tag }} latest 900 - # https://github.com/docker/setup-qemu-action - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - # https://github.com/docker/setup-buildx-action - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push - run: > - docker buildx build . --push - --tag chainsafe/lodestar:latest - --tag chainsafe/lodestar:${{ needs.tag.outputs.tag }} - --platform linux/amd64,linux/arm64 - --build-arg COMMIT=$(git rev-parse HEAD) - - - run: docker run chainsafe/lodestar:${{ needs.tag.outputs.tag }} --help - # Display history to know byte size of each layer - # Image is available only because of the previous `docker run` command - - run: docker image history chainsafe/lodestar:${{ needs.tag.outputs.tag }} - - - name: Build and push custom Grafana - run: > - docker buildx build ./docker/grafana/ --push - --file ./docker/grafana/Dockerfile - --build-context dashboards=./dashboards - --tag chainsafe/lodestar-grafana:${{ needs.tag.outputs.tag }} - --platform linux/amd64,linux/arm64 - - - name: Build and push custom Prometheus - run: > - docker buildx build ./docker/prometheus/ --push - --file ./docker/prometheus/Dockerfile - --tag chainsafe/lodestar-prometheus:${{ needs.tag.outputs.tag }} - --platform linux/amd64,linux/arm64 + + docker: + name: Publish to Docker Hub + needs: [tag, await-release] + if: needs.tag.outputs.is_stable == 'true' + uses: ./.github/workflows/docker.yml + with: + tag: ${{ needs.tag.outputs.tag }} + extra-tags: latest + secrets: inherit diff --git a/.github/workflows/test-sim.yml b/.github/workflows/test-sim.yml index f08456e9195d..6cfaf1be3c27 100644 --- a/.github/workflows/test-sim.yml +++ b/.github/workflows/test-sim.yml @@ -91,35 +91,6 @@ jobs: name: sim-test-endpoints-logs path: packages/cli/test-logs - sim-test-deneb: - name: Deneb sim tests - needs: build - runs-on: buildjet-4vcpu-ubuntu-2204 - steps: - # - Uses YAML anchors in the future - - uses: actions/checkout@v4 - - uses: "./.github/actions/setup-and-build" - with: - node: 24 - - name: Load env variables - uses: ./.github/actions/dotenv - - name: Download required docker images before running tests - run: | - docker pull ${{env.GETH_DOCKER_IMAGE}} - docker pull ${{env.LIGHTHOUSE_DOCKER_IMAGE}} - docker pull ${{env.NETHERMIND_DOCKER_IMAGE}} - - name: Sim tests deneb - run: DEBUG='${{github.event.inputs.debug}}' pnpm test:sim:deneb - working-directory: packages/cli - env: - GENESIS_DELAY_SLOTS: ${{github.event.inputs.genesisDelaySlots}} - - name: Upload debug log test files for "packages/cli" - if: ${{ always() }} - uses: actions/upload-artifact@v4 - with: - name: sim-test-deneb-logs - path: packages/cli/test-logs - sim-test-eth-backup-provider: name: Eth backup provider sim tests needs: build diff --git a/.gitignore b/.gitignore index a23ae27ac3d2..4edee34dcdb4 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,7 @@ packages/cli/.git-data.json dictionary.dic temp/ + +# Local development artifacts +.venv/ +checkpoint_states/ diff --git a/.vscode/launch.template.json b/.vscode/launch.template.json index 5f724eb001ee..37b5578dd573 100644 --- a/.vscode/launch.template.json +++ b/.vscode/launch.template.json @@ -42,7 +42,7 @@ "name": "Test Current File", "type": "node", "request": "launch", - "program": "${workspaceFolder}/node_modules/.bin/vitest", + "program": "${workspaceFolder}/node_modules/vitest/vitest.mjs", "args": [ "--run", "${file}", diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000000..e4c3bbe551d8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,350 @@ +# AGENTS.md + +## Project overview + +Lodestar is a TypeScript implementation of the Ethereum consensus client +(beacon node and validator client). It is maintained by ChainSafe Systems +and serves as: + +- **Production beacon node** for Ethereum's proof-of-stake consensus layer +- **Validator client** for stakers running validators +- **Light client** implementation with browser support +- **Reference implementation** for TypeScript/JavaScript ecosystem + +## Directory structure + +``` +/packages/ + api/ # REST API client and server + beacon-node/ # Beacon chain node implementation + cli/ # Command-line interface + config/ # Network configuration (mainnet, sepolia, etc.) + db/ # Database abstraction (LevelDB) + era/ # Era file handling for historical data + flare/ # CLI debugging/testing tool + fork-choice/ # Fork choice implementation (proto-array) + light-client/ # Light client implementation + logger/ # Logging utilities + params/ # Consensus parameters and presets + prover/ # Execution API prover + reqresp/ # libp2p request/response protocol + spec-test-util/ # Test harness for consensus spec tests + state-transition/ # State transition functions + test-utils/ # Shared utilities for testing + types/ # SSZ type definitions + utils/ # Shared utilities + validator/ # Validator client + +/configs/ # Network configuration files +/docs/ # Documentation source +/scripts/ # Build and release scripts +/dashboards/ # Grafana dashboard JSON files +``` + +## Build commands + +All commands use `pnpm` as the package manager. + +```bash +# Install dependencies +corepack enable +pnpm install + +# Build all packages +pnpm build + +# Run linter (biome) +pnpm lint + +# Fix lint issues automatically +pnpm lint:fix + +# Type check all packages +pnpm check-types + +# Run unit tests (fast, minimal preset) +pnpm test:unit + +# Run specific test file (faster - run from package directory) +cd packages/beacon-node +pnpm vitest run test/unit/path/to/test.test.ts + +# Run tests matching a pattern +pnpm vitest run -t "pattern" + +# Run spec tests (requires downloading first) +pnpm download-spec-tests +pnpm test:spec + +# Run e2e tests (requires docker environment) +./scripts/run_e2e_env.sh start +pnpm test:e2e +``` + +## Code style + +Lodestar uses [Biome](https://biomejs.dev/) for linting and formatting. + +### General conventions + +- **ES modules**: All code uses ES module syntax (`import`/`export`) +- **Naming**: `camelCase` for functions/variables, `PascalCase` for classes, + `UPPER_SNAKE_CASE` for constants +- **Quotes**: Use double quotes (`"`) not single quotes +- **Types**: All functions must have explicit parameter and return types +- **No `any`**: Avoid TypeScript `any` type +- **Private fields**: No underscore prefix (use `private dirty`, not `private _dirty`) + +### Import organization + +Imports are auto-sorted by Biome in this order: + +1. Node.js/Bun built-ins +2. External packages +3. `@chainsafe/*` and `@lodestar/*` packages +4. Relative paths + +Always use `.js` extension for relative imports (even for `.ts` files): + +```typescript +import {something} from "./utils.js"; +``` + +### Comments + +- Use `//` for implementation comments +- Use `/** */` JSDoc format for documenting public APIs +- Add comments when code behavior is non-obvious or deviates from standards +- Whitespace helps readability in complex code + +### Metrics + +Metrics are critical for production monitoring: + +- Follow [Prometheus naming conventions](https://prometheus.io/docs/practices/naming/) +- Always suffix metric names with units: `_seconds`, `_bytes`, `_total` +- Do NOT suffix code variables with units (no `Sec` suffix) +- Time-based metrics must use seconds + +## Testing guidelines + +### Test organization + +Tests live alongside source code in `test/` directories: + +``` +packages/beacon-node/ + src/ + test/ + unit/ # Unit tests + e2e/ # End-to-end tests + perf/ # Performance benchmarks + spec/ # Consensus spec tests +``` + +### Test requirements + +- Tests must be deterministic (no external live resources) +- Do not pull from external APIs (run local nodes instead) +- Use pinned Docker tags and git commits (not branches) +- Add assertion messages for loops or repeated assertions: + +```typescript +for (const block of blocks) { + expect(block.status).equals("processed", `wrong status for block ${block.slot}`); +} +``` + +### Running specific tests + +For faster iteration, run tests from the package directory: + +```bash +cd packages/beacon-node +pnpm vitest run test/unit/chain/validation/block.test.ts -t "should reject" +``` + +For spec tests with minimal preset (faster): + +```bash +LODESTAR_PRESET=minimal pnpm vitest run --config vitest.spec.config.ts +``` + +## Pull request guidelines + +### Branch naming + +If contributing from the main repository: + +``` +username/short-description +``` + +### Commit messages + +Follow [Conventional Commits](https://www.conventionalcommits.org/): + +- `feat:` new features +- `fix:` bug fixes +- `refactor:` code changes that don't add features or fix bugs +- `perf:` performance improvements +- `test:` adding or updating tests +- `chore:` maintenance tasks +- `docs:` documentation changes + +Examples: + +``` +feat: add lodestar prover for execution api +fix: ignore known block in publish blinded block flow +refactor(reqresp)!: support byte based handlers +``` + +### AI assistance disclosure + +**Required**: Disclose any AI assistance in your PR description: + +``` +> This PR was written primarily by Claude Code. +> I consulted Claude Code to understand the codebase, but the solution +> was fully authored manually by myself. +``` + +### PR etiquette + +- Keep PRs as drafts until ready for review +- Don't force push after review starts (use incremental commits) +- Close stale PRs rather than letting them sit +- Respond to review feedback promptly + +## Common tasks + +### Adding a new feature + +1. Create a feature branch from `unstable` +2. Implement the feature with tests +3. Run `pnpm lint` and `pnpm check-types` +4. Run `pnpm test:unit` to verify tests pass +5. Open PR with clear description and any AI disclosure + +### Fixing a bug + +1. Write a failing test that reproduces the bug +2. Fix the bug +3. Verify the test passes +4. Run full test suite: `pnpm test:unit` + +### Adding a new SSZ type + +1. Add the type definition in the relevant fork file (e.g., `packages/types/src/phase0/sszTypes.ts`) +2. Export the new type from that file's `ssz` object +3. The type will be automatically aggregated (no central `sszTypes` to modify) +4. Run `pnpm check-types` to verify + +## Style learnings from reviews + +### Prefer inline logic over helper functions + +For simple validation logic, inline the check rather than creating a helper: + +```typescript +// Preferred +if (error.code === RegenErrorCode.BLOCK_NOT_IN_FORKCHOICE) { + return GossipAction.REJECT; +} + +// Avoid (unless logic is complex and reused) +function shouldReject(error: Error): boolean { + return error.code === RegenErrorCode.BLOCK_NOT_IN_FORKCHOICE; +} +``` + +### Match existing comment style + +When adding comments to containers or functions modified across forks, +follow the existing style in that file. Don't add unnecessary markers. + +### Error handling patterns + +Use specific error codes when available: + +```typescript +// Preferred +throw new BlockError(block, {code: BlockErrorCode.PARENT_UNKNOWN}); + +// Avoid generic errors when specific ones exist +throw new Error("Parent not found"); +``` + +### Config value coercion + +When reading optional config values, handle undefined explicitly: + +```typescript +const peers = config.directPeers ?? []; +const trimmed = value?.trim() ?? ""; +``` + +## Implementing consensus specs + +The primary reference for implementing consensus specs is the +[Ethereum consensus-specs repository](https://github.com/ethereum/consensus-specs). +Additionally, [eth2book.info](https://eth2book.info) is a valuable resource for +understanding phase0, altair, bellatrix, and capella specs and how the spec +evolved over time (though no longer actively maintained). + +When implementing changes from the consensus specs, the mapping is typically: + +| Spec Document | Lodestar Package | +| ---------------------------- | -------------------------------------------- | +| beacon-chain.md (containers) | `@lodestar/types` | +| beacon-chain.md (functions) | `@lodestar/state-transition` | +| p2p-interface.md | `@lodestar/beacon-node` (networking, gossip) | +| validator.md | `@lodestar/validator` | +| fork-choice.md | `@lodestar/fork-choice` | + +### Fork organization + +Specs and code are organized by fork: `phase0`, `altair`, `bellatrix`, +`capella`, `deneb`, `electra`, `fulu`, `gloas`. + +- **@lodestar/types/src/** - Each fork has its own directory with SSZ type definitions +- **@lodestar/state-transition/src/block/** - Block processing functions + (e.g., `processAttestations`, `processDeposit`, `processWithdrawals`) +- **@lodestar/state-transition/src/epoch/** - Epoch processing functions +- **@lodestar/state-transition/src/slot/** - Slot processing functions + +## Important notes + +### Default branch is `unstable` + +All PRs should target `unstable`. The `stable` branch is for releases only +(see RELEASE.md for details). + +### Spec tests require download + +Before running `pnpm test:spec`, download test vectors: + +```bash +pnpm download-spec-tests +``` + +### E2E tests require Docker + +Start the e2e environment before running e2e tests: + +```bash +./scripts/run_e2e_env.sh start +pnpm test:e2e +./scripts/run_e2e_env.sh stop +``` + +### Generated files + +Do not edit files in `packages/*/lib/` - these are build outputs. +Edit source files in `packages/*/src/` instead. + +### Consensus spec references + +The `specrefs/` directory contains pinned consensus spec versions. +When implementing spec changes, reference the exact spec version. diff --git a/RELEASE.md b/RELEASE.md index 6c6c7e5c548f..6b040f091c2b 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -105,7 +105,7 @@ Tagging a stable release will trigger CI to publish to NPM, dockerhub, and Githu ### 6. Announce -- Double check that Github release is correct +- Double check that Github release is correct and [add release notes to the Github release page](#edit-the-release) - Follow [Publish to Social Media](#publish-to-social-media) steps ## Hotfix release @@ -216,7 +216,7 @@ Tagging a stable release will trigger CI to publish to NPM, dockerhub, and Githu ### 6. Announce -- Double check that Github release is correct +- Double check that Github release is correct and [add release notes to the Github release page](#edit-the-release) - Follow [Publish to Social Media](#publish-to-social-media) steps ## Dev release @@ -312,7 +312,7 @@ This section is to guide the Release Manager tasked with the next version releas - Release Manager can now complete Step 4: Merge release candidate. - Disable "Allow merge commits" under the Lodestar repository settings - Complete Step 5: Tag stable release -- Double check that Github release is correct and inform the Project Manager of completion +- Double check that Github release is correct, [add release notes to the Github release page](#edit-the-release), and inform the Project Manager of completion - Project Manager to follow up with Devops updating both `bn` and `vc` stable servers ## Alternatives considered diff --git a/dashboards/lodestar_beacon_chain.json b/dashboards/lodestar_beacon_chain.json index 04817bfbe768..affcaf153d05 100644 --- a/dashboards/lodestar_beacon_chain.json +++ b/dashboards/lodestar_beacon_chain.json @@ -948,26 +948,13 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "expr": "rate(lodestar_shuffling_cache_recalculated_shuffling_count[32m]) * 384", + "expr": "rate(lodestar_shuffling_cache_set_multiple_times_count[32m]) * 384", "hide": false, "instant": false, - "legendFormat": "built_multiple_times", + "legendFormat": "set_multiple_times", "range": true, "refId": "D" }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "editorMode": "code", - "expr": "rate(lodestar_shuffling_cache_promise_not_resolved_and_thrown_away_count[32m]) * 384", - "hide": false, - "instant": false, - "legendFormat": "not_resolved_thrown_away", - "range": true, - "refId": "E" - }, { "datasource": { "type": "prometheus", @@ -980,19 +967,6 @@ "legendFormat": "not_resolved", "range": true, "refId": "F" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "editorMode": "code", - "expr": "rate(lodestar_shuffling_cache_next_shuffling_not_on_epoch_cache[32m]) * 384", - "hide": false, - "instant": false, - "legendFormat": "next_shuffling_not_on_cache", - "range": true, - "refId": "G" } ], "title": "Insert vs Hit vs Miss", @@ -1076,19 +1050,6 @@ "legendFormat": "resolution", "range": true, "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "editorMode": "code", - "expr": "rate(lodestar_shuffling_cache_shuffling_calculation_time_seconds_sum[32m])\n/\nrate(lodestar_shuffling_cache_shuffling_calculation_time_seconds_count[32m])", - "hide": false, - "instant": false, - "legendFormat": "calculation_{{source}}", - "range": true, - "refId": "B" } ], "title": "Timing", @@ -1365,6 +1326,343 @@ ], "title": "Grow", "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 60 + }, + "id": 39, + "panels": [], + "title": "Unfinalized Block Writes Queue", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 61 + }, + "id": 40, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "lodestar_unfinalized_block_writes_queue_length", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Queue Length", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 61 + }, + "id": 41, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Magma", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false + } + }, + "pluginVersion": "10.4.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "rate(lodestar_unfinalized_block_writes_queue_job_time_seconds_bucket[$rate_interval])", + "format": "heatmap", + "instant": false, + "legendFormat": "{{le}}", + "range": true, + "refId": "A" + } + ], + "title": "Job Time", + "type": "heatmap" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 69 + }, + "id": 43, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "lodestar_unfinalized_block_writes_queue_concurrency", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Queue Concurrency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 69 + }, + "id": 42, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Magma", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false + } + }, + "pluginVersion": "10.4.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "rate(lodestar_unfinalized_block_writes_queue_job_wait_time_seconds_bucket[$rate_interval])", + "format": "heatmap", + "instant": false, + "legendFormat": "{{le}}", + "range": true, + "refId": "A" + } + ], + "title": "Job Wait Time", + "type": "heatmap" } ], "refresh": "10s", diff --git a/dashboards/lodestar_summary.json b/dashboards/lodestar_summary.json index 69f81dfcef73..38f72923835f 100644 --- a/dashboards/lodestar_summary.json +++ b/dashboards/lodestar_summary.json @@ -1361,7 +1361,7 @@ "uid": "${DS_PROMETHEUS}" }, "exemplar": false, - "expr": "avg(\n rate(validator_monitor_prev_epoch_on_chain_balance[32m])\n)", + "expr": "rate(validator_monitor_prev_epoch_on_chain_balance[32m]) / validator_monitor_validators", "hide": false, "interval": "", "legendFormat": "balance_delta", diff --git a/dashboards/lodestar_validator_monitor.json b/dashboards/lodestar_validator_monitor.json index dd1684f0da81..5448eb6daf8a 100644 --- a/dashboards/lodestar_validator_monitor.json +++ b/dashboards/lodestar_validator_monitor.json @@ -650,7 +650,7 @@ "uid": "${DS_PROMETHEUS}" }, "exemplar": false, - "expr": "avg(\n rate(validator_monitor_prev_epoch_on_chain_balance[32m])\n)", + "expr": "rate(validator_monitor_prev_epoch_on_chain_balance[32m]) / validator_monitor_validators", "hide": false, "interval": "", "legendFormat": "balance_delta", diff --git a/docs/pages/contribution/testing/simulation-tests.md b/docs/pages/contribution/testing/simulation-tests.md index a92d0c25b7e7..c7c096f3e6a1 100644 --- a/docs/pages/contribution/testing/simulation-tests.md +++ b/docs/pages/contribution/testing/simulation-tests.md @@ -34,10 +34,6 @@ This tests that various endpoints of the beacon node and validator client are wo pnpm -r test:sim:endpoints --filter @chainsafe/lodestar ``` -### `test:sim:deneb` - -This test is still included in our CI but is no longer as important as it once was. Lodestar is often the first client to implement new features and this test was created before geth was upgraded with the features required to support the Deneb fork. To test that Lodestar was ready this test uses mocked geth instances. It is left as a placeholder for when the next fork comes along that requires a similar approach. - ### `test:sim:mixedcleint` Checks that Lodestar is compatible with other consensus validators and vice-versa. All tests use Geth as the EL. diff --git a/lerna.json b/lerna.json index bd88d311751e..e6666ade7b69 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "packages": [ "packages/*" ], - "version": "1.39.1", + "version": "1.40.0", "stream": true, "command": { "version": { diff --git a/package.json b/package.json index 2f4bfa68d56f..ef1622ec947c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "root", "private": true, "engines": { - "node": "^22.22.0 || ^24.13.0" + "node": "^24.13.0" }, "packageManager": "pnpm@10.24.0", "workspaces": [ @@ -56,6 +56,7 @@ "@vitest/browser": "catalog:", "@vitest/browser-playwright": "catalog:", "@vitest/coverage-v8": "catalog:", + "axios": "^1.13.2", "bun-types": "^1.2.21", "crypto-browserify": "^3.12.0", "dotenv": "^16.4.5", diff --git a/packages/api/package.json b/packages/api/package.json index 7245b04be1c9..2b6b616151ac 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -11,7 +11,7 @@ "bugs": { "url": "https://github.com/ChainSafe/lodestar/issues" }, - "version": "1.39.1", + "version": "1.40.0", "type": "module", "exports": { ".": { @@ -87,7 +87,7 @@ "@types/eventsource": "^1.1.11", "@types/qs": "^6.9.7", "ajv": "^8.12.0", - "fastify": "^5.2.1" + "fastify": "^5.7.4" }, "keywords": [ "ethereum", diff --git a/packages/api/src/beacon/routes/lodestar.ts b/packages/api/src/beacon/routes/lodestar.ts index 07cc45508862..5236d3d40e38 100644 --- a/packages/api/src/beacon/routes/lodestar.ts +++ b/packages/api/src/beacon/routes/lodestar.ts @@ -1,6 +1,6 @@ import {ContainerType, Type, ValueOf} from "@chainsafe/ssz"; import {ChainForkConfig} from "@lodestar/config"; -import {ArrayOf, BeaconState, Epoch, RootHex, Slot, ssz} from "@lodestar/types"; +import {ArrayOf, BeaconState, Epoch, RootHex, Slot, ValidatorIndex, ssz} from "@lodestar/types"; import { EmptyArgs, EmptyMeta, @@ -54,6 +54,18 @@ export type GossipPeerScoreStat = { // + Other un-typed options }; +/** + * A multiaddr with peer ID or ENR string. + * + * Supported formats: + * - Multiaddr with peer ID: `/ip4/192.168.1.1/tcp/9000/p2p/16Uiu2HAmKLhW7...` + * - ENR: `enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOo...` + * + * For multiaddrs, the string must contain a /p2p/ component with the peer ID. + * For ENRs, the TCP multiaddr and peer ID are extracted from the encoded record. + */ +export type DirectPeer = string; + export type RegenQueueItem = { key: string; args: unknown; @@ -101,6 +113,15 @@ const HistoricalSummariesResponseType = new ContainerType( export type HistoricalSummariesResponse = ValueOf; +export type CustodyInfo = { + /** Earliest slot for which the node has custodied data columns */ + earliestCustodiedSlot: Slot; + /** Number of custody groups the node is responsible for */ + custodyGroupCount: number; + /** List of column indices the node is custodying */ + custodyColumns: number[]; +}; + export type Endpoints = { /** Trigger to write a heapdump to disk at `dirpath`. May take > 1min */ writeHeapdump: Endpoint< @@ -231,6 +252,41 @@ export type Endpoints = { EmptyResponseData, EmptyMeta >; + + /** + * Add a direct peer at runtime. + * Direct peers maintain permanent mesh connections without GRAFT/PRUNE negotiation. + * Accepts either a multiaddr with peer ID or an ENR string. + */ + addDirectPeer: Endpoint< + // ⏎ + "POST", + {peer: DirectPeer}, + {query: {peer: string}}, + {peerId: string}, + EmptyMeta + >; + + /** Remove a peer from direct peers */ + removeDirectPeer: Endpoint< + // ⏎ + "DELETE", + {peerId: string}, + {query: {peerId: string}}, + {removed: boolean}, + EmptyMeta + >; + + /** Get list of direct peer IDs */ + getDirectPeers: Endpoint< + // ⏎ + "GET", + EmptyArgs, + EmptyRequest, + string[], + EmptyMeta + >; + /** Same to node api with new fields */ getPeers: Endpoint< "GET", @@ -271,6 +327,18 @@ export type Endpoints = { VersionMeta >; + /** + * Returns the validator indices that are currently being monitored by the validator monitor. + */ + getMonitoredValidatorIndices: Endpoint< + // ⏎ + "GET", + EmptyArgs, + EmptyRequest, + ValidatorIndex[], + EmptyMeta + >; + /** Dump Discv5 Kad values */ discv5GetKadValues: Endpoint< // ⏎ @@ -304,6 +372,16 @@ export type Endpoints = { {root: RootHex; slot: Slot}[], EmptyMeta >; + + /** Get custody information for data columns */ + getCustodyInfo: Endpoint< + // ⏎ + "GET", + EmptyArgs, + EmptyRequest, + CustodyInfo, + EmptyMeta + >; }; export function getDefinitions(_config: ChainForkConfig): RouteDefinitions { @@ -412,6 +490,32 @@ export function getDefinitions(_config: ChainForkConfig): RouteDefinitions ({query: {peer}}), + parseReq: ({query}) => ({peer: query.peer}), + schema: {query: {peer: Schema.StringRequired}}, + }, + resp: JsonOnlyResponseCodec, + }, + removeDirectPeer: { + url: "/eth/v1/lodestar/direct_peers", + method: "DELETE", + req: { + writeReq: ({peerId}) => ({query: {peerId}}), + parseReq: ({query}) => ({peerId: query.peerId}), + schema: {query: {peerId: Schema.StringRequired}}, + }, + resp: JsonOnlyResponseCodec, + }, + getDirectPeers: { + url: "/eth/v1/lodestar/direct_peers", + method: "GET", + req: EmptyRequestCodec, + resp: JsonOnlyResponseCodec, + }, getPeers: { url: "/eth/v1/lodestar/peers", method: "GET", @@ -462,6 +566,12 @@ export function getDefinitions(_config: ChainForkConfig): RouteDefinitions Promise} { const server = fastify({ ajv: {customOptions: {coerceTypes: "array"}}, - querystringParser: (str) => parseQueryString(str, {comma: true, parseArrays: false}), + routerOptions: { + querystringParser: (str) => parseQueryString(str, {comma: true, parseArrays: false}), + }, }); addSszContentTypeParser(server); diff --git a/packages/beacon-node/package.json b/packages/beacon-node/package.json index b2d7bc4b5adc..da0a4cc3fb5b 100644 --- a/packages/beacon-node/package.json +++ b/packages/beacon-node/package.json @@ -11,7 +11,7 @@ "bugs": { "url": "https://github.com/ChainSafe/lodestar/issues" }, - "version": "1.39.1", + "version": "1.40.0", "type": "module", "exports": { ".": { @@ -151,7 +151,7 @@ "datastore-fs": "^10.0.6", "datastore-level": "^11.0.3", "deepmerge": "^4.3.1", - "fastify": "^5.2.1", + "fastify": "^5.7.4", "interface-datastore": "^8.3.0", "it-all": "^3.0.4", "it-pipe": "^3.0.1", diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index 9748c88c50c7..7b7ab22c13f9 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -30,7 +30,7 @@ import { isDenebBlockContents, sszTypesFor, } from "@lodestar/types"; -import {fromAsync, fromHex, sleep, toHex, toRootHex} from "@lodestar/utils"; +import {fromHex, sleep, toHex, toRootHex} from "@lodestar/utils"; import {BlockInputSource, isBlockInputBlobs, isBlockInputColumns} from "../../../../chain/blocks/blockInput/index.js"; import {ImportBlockOpts} from "../../../../chain/blocks/types.js"; import {verifyBlocksInEpoch} from "../../../../chain/blocks/verifyBlock.js"; @@ -133,21 +133,31 @@ export function getBeaconBlockApi({ if (isBlockInputColumns(blockForImport)) { for (const dataColumnSidecar of dataColumnSidecars) { - blockForImport.addColumn({ - blockRootHex: blockRoot, - columnSidecar: dataColumnSidecar, - source: BlockInputSource.api, - seenTimestampSec, - }); + blockForImport.addColumn( + { + blockRootHex: blockRoot, + columnSidecar: dataColumnSidecar, + source: BlockInputSource.api, + seenTimestampSec, + }, + // In multi-BN setups (DVT, fallback), the same block may be published to multiple nodes. + // Data columns may arrive via gossip from another node before the API publish completes, + // so we allow duplicates here instead of throwing an error. + {throwOnDuplicateAdd: false} + ); } } else if (isBlockInputBlobs(blockForImport)) { for (const blobSidecar of blobSidecars) { - blockForImport.addBlob({ - blockRootHex: blockRoot, - blobSidecar, - source: BlockInputSource.api, - seenTimestampSec, - }); + blockForImport.addBlob( + { + blockRootHex: blockRoot, + blobSidecar, + source: BlockInputSource.api, + seenTimestampSec, + }, + // Same as above for columns + {throwOnDuplicateAdd: false} + ); } } @@ -285,7 +295,7 @@ export function getBeaconBlockApi({ () => // there is no rush to persist block since we published it to gossip anyway chain - .processBlock(blockForImport, {...opts, eagerPersistBlock: false}) + .processBlock(blockForImport, opts) .catch((e) => { if (e instanceof BlockError && e.type.code === BlockErrorCode.PARENT_UNKNOWN) { chain.emitter.emit(ChainEvent.unknownParent, { @@ -434,11 +444,13 @@ export function getBeaconBlockApi({ const nonFinalizedBlocks = chain.forkChoice.getBlockSummariesByParentRoot(parentRoot); await Promise.all( nonFinalizedBlocks.map(async (summary) => { - const block = await db.block.get(fromHex(summary.blockRoot)); - if (block) { - const canonical = chain.forkChoice.getCanonicalBlockAtSlot(block.message.slot); + const blockResult = await chain.getBlockByRoot(summary.blockRoot); + if (blockResult) { + const canonical = chain.forkChoice.getCanonicalBlockAtSlot(blockResult.block.message.slot); if (canonical) { - result.push(toBeaconHeaderResponse(config, block, canonical.blockRoot === summary.blockRoot)); + result.push( + toBeaconHeaderResponse(config, blockResult.block, canonical.blockRoot === summary.blockRoot) + ); if (isOptimisticBlock(canonical)) { executionOptimistic = true; } @@ -492,9 +504,9 @@ export function getBeaconBlockApi({ finalized = false; if (summary.blockRoot !== toRootHex(canonicalRoot)) { - const block = await db.block.get(fromHex(summary.blockRoot)); - if (block) { - result.push(toBeaconHeaderResponse(config, block)); + const blockResult = await chain.getBlockByRoot(summary.blockRoot); + if (blockResult) { + result.push(toBeaconHeaderResponse(config, blockResult.block)); } } }) @@ -626,6 +638,7 @@ export function getBeaconBlockApi({ const {block, executionOptimistic, finalized} = await getBlockResponse(chain, blockId); const fork = config.getForkName(block.message.slot); const blockRoot = sszTypesFor(fork).BeaconBlock.hashTreeRoot(block.message); + const blockRootHex = toRootHex(blockRoot); let data: deneb.BlobSidecars; @@ -642,10 +655,7 @@ export function getBeaconBlockApi({ const blobCount = blobKzgCommitments.length; if (blobCount > 0) { - let dataColumnSidecars = await fromAsync(db.dataColumnSidecar.valuesStream(blockRoot)); - if (dataColumnSidecars.length === 0) { - dataColumnSidecars = await fromAsync(db.dataColumnSidecarArchive.valuesStream(block.message.slot)); - } + const dataColumnSidecars = await chain.getDataColumnSidecars(block.message.slot, blockRootHex); if (dataColumnSidecars.length === 0) { throw new ApiError( @@ -682,10 +692,7 @@ export function getBeaconBlockApi({ data = []; } } else if (isForkPostDeneb(fork)) { - let {blobSidecars} = (await db.blobSidecars.get(blockRoot)) ?? {}; - if (!blobSidecars) { - ({blobSidecars} = (await db.blobSidecarsArchive.get(block.message.slot)) ?? {}); - } + const blobSidecars = await chain.getBlobSidecars(block.message.slot, blockRootHex); if (!blobSidecars) { throw new ApiError( @@ -715,6 +722,7 @@ export function getBeaconBlockApi({ const {block, executionOptimistic, finalized} = await getBlockResponse(chain, blockId); const fork = config.getForkName(block.message.slot); const blockRoot = sszTypesFor(fork).BeaconBlock.hashTreeRoot(block.message); + const blockRootHex = toRootHex(blockRoot); let blobs: deneb.Blobs; @@ -731,10 +739,7 @@ export function getBeaconBlockApi({ const blobCount = blobKzgCommitments.length; if (blobCount > 0) { - let dataColumnSidecars = await fromAsync(db.dataColumnSidecar.valuesStream(blockRoot)); - if (dataColumnSidecars.length === 0) { - dataColumnSidecars = await fromAsync(db.dataColumnSidecarArchive.valuesStream(block.message.slot)); - } + const dataColumnSidecars = await chain.getDataColumnSidecars(block.message.slot, blockRootHex); if (dataColumnSidecars.length === 0) { throw new ApiError( @@ -766,10 +771,7 @@ export function getBeaconBlockApi({ blobs = []; } } else if (isForkPostDeneb(fork)) { - let {blobSidecars} = (await db.blobSidecars.get(blockRoot)) ?? {}; - if (!blobSidecars) { - ({blobSidecars} = (await db.blobSidecarsArchive.get(block.message.slot)) ?? {}); - } + const blobSidecars = await chain.getBlobSidecars(block.message.slot, blockRootHex); if (!blobSidecars) { throw new ApiError( diff --git a/packages/beacon-node/src/api/impl/beacon/state/utils.ts b/packages/beacon-node/src/api/impl/beacon/state/utils.ts index bd5364199ce0..8353f0823ed2 100644 --- a/packages/beacon-node/src/api/impl/beacon/state/utils.ts +++ b/packages/beacon-node/src/api/impl/beacon/state/utils.ts @@ -2,7 +2,7 @@ import {PubkeyIndexMap} from "@chainsafe/pubkey-index-map"; import {routes} from "@lodestar/api"; import {CheckpointWithHex, IForkChoice} from "@lodestar/fork-choice"; import {GENESIS_SLOT} from "@lodestar/params"; -import {BeaconStateAllForks} from "@lodestar/state-transition"; +import {BeaconStateAllForks, CachedBeaconStateAllForks} from "@lodestar/state-transition"; import {BLSPubkey, Epoch, RootHex, Slot, ValidatorIndex, getValidatorStatus, phase0} from "@lodestar/types"; import {fromHex} from "@lodestar/utils"; import {IBeaconChain} from "../../../../chain/index.js"; @@ -41,30 +41,10 @@ export function resolveStateId( return blockSlot; } -export async function getStateResponse( - chain: IBeaconChain, - inStateId: routes.beacon.StateId -): Promise<{state: BeaconStateAllForks; executionOptimistic: boolean; finalized: boolean}> { - const stateId = resolveStateId(chain.forkChoice, inStateId); - - const res = - typeof stateId === "string" - ? await chain.getStateByStateRoot(stateId) - : typeof stateId === "number" - ? await chain.getStateBySlot(stateId) - : chain.getStateByCheckpoint(stateId); - - if (!res) { - throw new ApiError(404, `State not found for id '${inStateId}'`); - } - - return res; -} - export async function getStateResponseWithRegen( chain: IBeaconChain, inStateId: routes.beacon.StateId -): Promise<{state: BeaconStateAllForks | Uint8Array; executionOptimistic: boolean; finalized: boolean}> { +): Promise<{state: CachedBeaconStateAllForks | Uint8Array; executionOptimistic: boolean; finalized: boolean}> { const stateId = resolveStateId(chain.forkChoice, inStateId); const res = diff --git a/packages/beacon-node/src/api/impl/config/constants.ts b/packages/beacon-node/src/api/impl/config/constants.ts index 64988f5626b8..ae5f6b065e00 100644 --- a/packages/beacon-node/src/api/impl/config/constants.ts +++ b/packages/beacon-node/src/api/impl/config/constants.ts @@ -5,6 +5,8 @@ import { BASE_REWARDS_PER_EPOCH, BLOB_TX_TYPE, BLS_WITHDRAWAL_PREFIX, + BUILDER_INDEX_FLAG, + BUILDER_INDEX_SELF_BUILD, BUILDER_PAYMENT_THRESHOLD_DENOMINATOR, BUILDER_PAYMENT_THRESHOLD_NUMERATOR, BUILDER_WITHDRAWAL_PREFIX, @@ -21,6 +23,7 @@ import { DOMAIN_BLS_TO_EXECUTION_CHANGE, DOMAIN_CONTRIBUTION_AND_PROOF, DOMAIN_DEPOSIT, + DOMAIN_PROPOSER_PREFERENCES, DOMAIN_PTC_ATTESTER, DOMAIN_RANDAO, DOMAIN_SELECTION_PROOF, @@ -82,6 +85,7 @@ export const specConstants = { DOMAIN_APPLICATION_MASK, DOMAIN_APPLICATION_BUILDER, DOMAIN_BEACON_BUILDER, + DOMAIN_PROPOSER_PREFERENCES, DOMAIN_PTC_ATTESTER, // phase0/validator.md @@ -128,6 +132,10 @@ export const specConstants = { DEPOSIT_REQUEST_TYPE: toHexByte(DEPOSIT_REQUEST_TYPE), WITHDRAWAL_REQUEST_TYPE: toHexByte(WITHDRAWAL_REQUEST_TYPE), CONSOLIDATION_REQUEST_TYPE: toHexByte(CONSOLIDATION_REQUEST_TYPE), + + // gloas + BUILDER_INDEX_FLAG, + BUILDER_INDEX_SELF_BUILD, BUILDER_PAYMENT_THRESHOLD_NUMERATOR, BUILDER_PAYMENT_THRESHOLD_DENOMINATOR, }; diff --git a/packages/beacon-node/src/api/impl/debug/index.ts b/packages/beacon-node/src/api/impl/debug/index.ts index f1f0562fd067..ae2b21611221 100644 --- a/packages/beacon-node/src/api/impl/debug/index.ts +++ b/packages/beacon-node/src/api/impl/debug/index.ts @@ -3,7 +3,7 @@ import {ApplicationMethods} from "@lodestar/api/server"; import {ExecutionStatus} from "@lodestar/fork-choice"; import {ZERO_HASH_HEX, isForkPostDeneb, isForkPostFulu} from "@lodestar/params"; import {BeaconState, deneb, fulu, sszTypesFor} from "@lodestar/types"; -import {fromAsync, toRootHex} from "@lodestar/utils"; +import {toRootHex} from "@lodestar/utils"; import {isOptimisticBlock} from "../../../util/forkChoice.js"; import {getStateSlotFromBytes} from "../../../util/multifork.js"; import {getBlockResponse} from "../beacon/blocks/utils.js"; @@ -14,7 +14,6 @@ import {assertUniqueItems} from "../utils.js"; export function getDebugApi({ chain, config, - db, }: Pick): ApplicationMethods { return { async getDebugChainHeadsV2() { @@ -104,10 +103,7 @@ export function getDebugApi({ : 0; if (isForkPostFulu(fork) && blobCount > 0) { - dataColumnSidecars = await fromAsync(db.dataColumnSidecar.valuesStream(blockRoot)); - if (dataColumnSidecars.length === 0) { - dataColumnSidecars = await fromAsync(db.dataColumnSidecarArchive.valuesStream(block.message.slot)); - } + dataColumnSidecars = await chain.getDataColumnSidecars(block.message.slot, toRootHex(blockRoot)); if (dataColumnSidecars.length === 0) { throw Error( diff --git a/packages/beacon-node/src/api/impl/lodestar/index.ts b/packages/beacon-node/src/api/impl/lodestar/index.ts index 1cc3f765dc21..9f94bdc1ad15 100644 --- a/packages/beacon-node/src/api/impl/lodestar/index.ts +++ b/packages/beacon-node/src/api/impl/lodestar/index.ts @@ -154,6 +154,23 @@ export function getLodestarApi({ await network.disconnectPeer(peerId); }, + async addDirectPeer({peer}) { + const peerId = await network.addDirectPeer(peer); + if (peerId === null) { + throw new ApiError(400, `Failed to add direct peer: invalid peer address or ENR "${peer}"`); + } + return {data: {peerId}}; + }, + + async removeDirectPeer({peerId}) { + const removed = await network.removeDirectPeer(peerId); + return {data: {removed}}; + }, + + async getDirectPeers() { + return {data: await network.getDirectPeers()}; + }, + async getPeers({state, direction}) { const peers = (await network.dumpPeers()).filter( (nodePeer) => @@ -198,7 +215,7 @@ export function getLodestarApi({ const {state, executionOptimistic, finalized} = await getStateResponseWithRegen(chain, stateId); const stateView = ( - state instanceof Uint8Array ? loadState(config, chain.getHeadState(), state).state : state.clone() + state instanceof Uint8Array ? loadState(config, chain.getHeadState(), state).state : state ) as BeaconStateCapella; const fork = config.getForkName(stateView.slot); @@ -238,6 +255,24 @@ export function getLodestarApi({ }, }; }, + + async getMonitoredValidatorIndices() { + return { + data: chain.validatorMonitor?.getMonitoredValidatorIndices() ?? [], + }; + }, + + async getCustodyInfo() { + const {custodyColumns, targetCustodyGroupCount} = chain.custodyConfig; + + return { + data: { + earliestCustodiedSlot: chain.earliestAvailableSlot, + custodyGroupCount: targetCustodyGroupCount, + custodyColumns, + }, + }; + }, }; } @@ -249,9 +284,6 @@ function regenRequestToJson(config: ChainForkConfig, regenRequest: RegenRequest) slot: regenRequest.args[1], }; - case "getCheckpointState": - return ssz.phase0.Checkpoint.toJson(regenRequest.args[0]); - case "getPreState": { const slot = regenRequest.args[0].slot; return { diff --git a/packages/beacon-node/src/api/impl/proof/index.ts b/packages/beacon-node/src/api/impl/proof/index.ts index 9e4d6dd048a2..5b77a29ace95 100644 --- a/packages/beacon-node/src/api/impl/proof/index.ts +++ b/packages/beacon-node/src/api/impl/proof/index.ts @@ -27,8 +27,7 @@ export function getProofApi( const state = res.state instanceof Uint8Array ? loadState(config, chain.getHeadState(), res.state).state : res.state; - // Commit any changes before computing the state root. In normal cases the state should have no changes here - state.commit(); + // there should be no state changes in beacon-node so no need to commit() here const stateNode = state.node; const proof = createProof(stateNode, {type: ProofType.compactMulti, descriptor}); diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 7a391ac0b0c1..18c52450c17e 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -1,7 +1,7 @@ import {PubkeyIndexMap} from "@chainsafe/pubkey-index-map"; import {routes} from "@lodestar/api"; import {ApplicationMethods} from "@lodestar/api/server"; -import {ExecutionStatus} from "@lodestar/fork-choice"; +import {ExecutionStatus, ProtoBlock} from "@lodestar/fork-choice"; import { ForkName, ForkPostBellatrix, @@ -67,10 +67,11 @@ import { SyncCommitteeError, SyncCommitteeErrorCode, } from "../../../chain/errors/index.js"; -import {ChainEvent, CheckpointHex, CommonBlockBody} from "../../../chain/index.js"; +import {ChainEvent, CommonBlockBody} from "../../../chain/index.js"; import {PREPARE_NEXT_SLOT_BPS} from "../../../chain/prepareNextSlot.js"; import {BlockType, ProduceFullDeneb} from "../../../chain/produceBlock/index.js"; import {RegenCaller} from "../../../chain/regen/index.js"; +import {CheckpointHex} from "../../../chain/stateCache/types.js"; import {validateApiAggregateAndProof} from "../../../chain/validation/index.js"; import {validateSyncCommitteeGossipContributionAndProof} from "../../../chain/validation/syncCommitteeContributionAndProof.js"; import {ZERO_HASH} from "../../../constants/index.js"; @@ -413,10 +414,10 @@ export function getValidatorApi( // as of now fee recipient checks can not be performed because builder does not return bid recipient { commonBlockBodyPromise, - parentBlockRoot, + parentBlock, }: Omit & { commonBlockBodyPromise: Promise; - parentBlockRoot: Root; + parentBlock: ProtoBlock; } ): Promise { const version = config.getForkName(slot); @@ -447,7 +448,7 @@ export function getValidatorApi( timer = metrics?.blockProductionTime.startTimer(); const {block, executionPayloadValue, consensusBlockValue} = await chain.produceBlindedBlock({ slot, - parentBlockRoot, + parentBlock, randaoReveal, graffiti, commonBlockBodyPromise, @@ -482,10 +483,10 @@ export function getValidatorApi( feeRecipient, strictFeeRecipientCheck, commonBlockBodyPromise, - parentBlockRoot, + parentBlock, }: Omit & { commonBlockBodyPromise: Promise; - parentBlockRoot: Root; + parentBlock: ProtoBlock; } ): Promise { const source = ProducedBlockSource.engine; @@ -496,7 +497,7 @@ export function getValidatorApi( timer = metrics?.blockProductionTime.startTimer(); const {block, executionPayloadValue, consensusBlockValue, shouldOverrideBuilder} = await chain.produceBlock({ slot, - parentBlockRoot, + parentBlock, randaoReveal, graffiti, feeRecipient, @@ -569,7 +570,8 @@ export function getValidatorApi( notWhileSyncing(); await waitForSlot(slot); // Must never request for a future slot > currentSlot - const {blockRoot: parentBlockRootHex, slot: parentSlot} = chain.getProposerHead(slot); + const parentBlock = chain.getProposerHead(slot); + const {blockRoot: parentBlockRootHex, slot: parentSlot} = parentBlock; const parentBlockRoot = fromHex(parentBlockRootHex); notOnOutOfRangeData(parentBlockRoot); metrics?.blockProductionSlotDelta.set(slot - parentSlot); @@ -638,7 +640,7 @@ export function getValidatorApi( // can't do fee recipient checks as builder bid doesn't return feeRecipient as of now strictFeeRecipientCheck: false, commonBlockBodyPromise, - parentBlockRoot, + parentBlock, }) : Promise.reject(new Error("Builder disabled")); @@ -647,7 +649,7 @@ export function getValidatorApi( feeRecipient, strictFeeRecipientCheck, commonBlockBodyPromise, - parentBlockRoot, + parentBlock, }).then((engineBlock) => { // Once the engine returns a block, in the event of either: // - suspected builder censorship @@ -689,7 +691,7 @@ export function getValidatorApi( chain .produceCommonBlockBody({ slot, - parentBlockRoot, + parentBlock, randaoReveal, graffiti: graffitiBytes, }) @@ -1040,9 +1042,7 @@ export function getValidatorApi( const res = await getStateResponseWithRegen(chain, startSlot); const stateViewDU = - res.state instanceof Uint8Array - ? loadState(config, chain.getHeadState(), res.state).state - : res.state.clone(); + res.state instanceof Uint8Array ? loadState(config, chain.getHeadState(), res.state).state : res.state; state = createCachedBeaconState( stateViewDU, diff --git a/packages/beacon-node/src/api/rest/base.ts b/packages/beacon-node/src/api/rest/base.ts index 19ee14442fe4..50d4c3d69821 100644 --- a/packages/beacon-node/src/api/rest/base.ts +++ b/packages/beacon-node/src/api/rest/base.ts @@ -1,6 +1,6 @@ import bearerAuthPlugin from "@fastify/bearer-auth"; import {fastifyCors} from "@fastify/cors"; -import {FastifyInstance, FastifyRequest, errorCodes, fastify} from "fastify"; +import {FastifyError, FastifyInstance, FastifyRequest, errorCodes, fastify} from "fastify"; import {parse as parseQueryString} from "qs"; import {addSszContentTypeParser} from "@lodestar/api/server"; import {ErrorAborted, Gauge, Histogram, Logger} from "@lodestar/utils"; @@ -73,15 +73,17 @@ export class RestApiServer { const server = fastify({ logger: false, ajv: {customOptions: {coerceTypes: "array"}}, - querystringParser: (str) => - parseQueryString(str, { - // Array as comma-separated values must be supported to be OpenAPI spec compliant - comma: true, - // Drop support for array query strings like `id[0]=1&id[1]=2&id[2]=3` as those are not required to - // be OpenAPI spec compliant and results are inconsistent, see https://github.com/ljharb/qs/issues/331. - // The schema validation will catch this and throw an error as parsed query string results in an object. - parseArrays: false, - }), + routerOptions: { + querystringParser: (str) => + parseQueryString(str, { + // Array as comma-separated values must be supported to be OpenAPI spec compliant + comma: true, + // Drop support for array query strings like `id[0]=1&id[1]=2&id[2]=3` as those are not required to + // be OpenAPI spec compliant and results are inconsistent, see https://github.com/ljharb/qs/issues/331. + // The schema validation will catch this and throw an error as parsed query string results in an object. + parseArrays: false, + }), + }, bodyLimit: opts.bodyLimit, http: {maxHeaderSize: opts.headerLimit}, }); @@ -91,10 +93,10 @@ export class RestApiServer { this.activeSockets = new HttpActiveSocketsTracker(server.server, metrics); // To parse our ApiError -> statusCode - server.setErrorHandler((err, _req, res) => { + server.setErrorHandler((err, _req, res) => { const stacktraces = opts.stacktraces ? err.stack?.split("\n") : undefined; - if (err.validation) { - const {instancePath, message} = err.validation[0]; + if ("validation" in err && err.validation) { + const {instancePath = "unknown", message} = err.validation?.[0] ?? {}; const payload: ErrorResponse = { code: 400, message: `${instancePath.substring(instancePath.lastIndexOf("/") + 1)} ${message}`, diff --git a/packages/beacon-node/src/chain/ColumnReconstructionTracker.ts b/packages/beacon-node/src/chain/ColumnReconstructionTracker.ts index 1a81efc0a394..337fedb039f3 100644 --- a/packages/beacon-node/src/chain/ColumnReconstructionTracker.ts +++ b/packages/beacon-node/src/chain/ColumnReconstructionTracker.ts @@ -8,12 +8,12 @@ import {ChainEventEmitter} from "./emitter.js"; /** * Minimum time to wait before attempting reconstruction */ -const RECONSTRUCTION_DELAY_MIN_MS = 800; +const RECONSTRUCTION_DELAY_MIN_BPS = 667; /** * Maximum time to wait before attempting reconstruction */ -const RECONSTRUCTION_DELAY_MAX_MS = 1200; +const RECONSTRUCTION_DELAY_MAX_BPS = 1000; export type ColumnReconstructionTrackerInit = { logger: Logger; @@ -41,11 +41,16 @@ export class ColumnReconstructionTracker { /** Track if a reconstruction attempt is in-flight */ running = false; + private readonly minDelayMs: number; + private readonly maxDelayMs: number; + constructor(init: ColumnReconstructionTrackerInit) { this.logger = init.logger; this.emitter = init.emitter; this.metrics = init.metrics; this.config = init.config; + this.minDelayMs = this.config.getSlotComponentDurationMs(RECONSTRUCTION_DELAY_MIN_BPS); + this.maxDelayMs = this.config.getSlotComponentDurationMs(RECONSTRUCTION_DELAY_MAX_BPS); } triggerColumnReconstruction(blockInput: BlockInputColumns): void { @@ -61,8 +66,7 @@ export class ColumnReconstructionTracker { // just that it has been triggered for this block root. this.running = true; this.lastBlockRootHex = blockInput.blockRootHex; - const delay = - RECONSTRUCTION_DELAY_MIN_MS + Math.random() * (RECONSTRUCTION_DELAY_MAX_MS - RECONSTRUCTION_DELAY_MIN_MS); + const delay = this.minDelayMs + Math.random() * (this.maxDelayMs - this.minDelayMs); sleep(delay) .then(() => { const logCtx = {slot: blockInput.slot, root: blockInput.blockRootHex}; diff --git a/packages/beacon-node/src/chain/archiveStore/historicalState/getHistoricalState.ts b/packages/beacon-node/src/chain/archiveStore/historicalState/getHistoricalState.ts index b438b4b92be7..e6381575e2e0 100644 --- a/packages/beacon-node/src/chain/archiveStore/historicalState/getHistoricalState.ts +++ b/packages/beacon-node/src/chain/archiveStore/historicalState/getHistoricalState.ts @@ -8,7 +8,9 @@ import { createCachedBeaconState, stateTransition, } from "@lodestar/state-transition"; +import {byteArrayEquals} from "@lodestar/utils"; import {IBeaconDb} from "../../../db/index.js"; +import {getStateTypeFromBytes} from "../../../util/multifork.js"; import {HistoricalStateRegenMetrics} from "./metrics.js"; import {RegenErrorType} from "./types.js"; @@ -35,12 +37,13 @@ export async function getNearestState( db: IBeaconDb, pubkey2index: PubkeyIndexMap ): Promise { - const states = await db.stateArchive.values({limit: 1, lte: slot, reverse: true}); - if (!states.length) { + const stateBytesArr = await db.stateArchive.binaries({limit: 1, lte: slot, reverse: true}); + if (!stateBytesArr.length) { throw new Error("No near state found in the database"); } - const state = states[0]; + const stateBytes = stateBytesArr[0]; + const state = getStateTypeFromBytes(config, stateBytes).deserializeToViewDU(stateBytes); syncPubkeyCache(state, pubkey2index); return createCachedBeaconState( @@ -96,7 +99,7 @@ export async function getHistoricalState( throw e; } blockCount++; - if (Buffer.compare(state.hashTreeRoot(), block.message.stateRoot) !== 0) { + if (!byteArrayEquals(state.hashTreeRoot(), block.message.stateRoot)) { metrics?.regenErrorCount.inc({reason: RegenErrorType.invalidStateRoot}); } } diff --git a/packages/beacon-node/src/chain/archiveStore/utils/archiveBlocks.ts b/packages/beacon-node/src/chain/archiveStore/utils/archiveBlocks.ts index 217a755667b8..75cd1df77d1a 100644 --- a/packages/beacon-node/src/chain/archiveStore/utils/archiveBlocks.ts +++ b/packages/beacon-node/src/chain/archiveStore/utils/archiveBlocks.ts @@ -73,9 +73,12 @@ export async function archiveBlocks( root: fromHex(block.blockRoot), })); + const logCtx = {currentEpoch, finalizedEpoch: finalizedCheckpoint.epoch, finalizedRoot: finalizedCheckpoint.rootHex}; + if (finalizedCanonicalBlockRoots.length > 0) { await migrateBlocksFromHotToColdDb(db, finalizedCanonicalBlockRoots); logger.verbose("Migrated blocks from hot DB to cold DB", { + ...logCtx, fromSlot: finalizedCanonicalBlockRoots[0].slot, toSlot: finalizedCanonicalBlockRoots.at(-1)?.slot, size: finalizedCanonicalBlockRoots.length, @@ -88,7 +91,7 @@ export async function archiveBlocks( finalizedCanonicalBlockRoots, currentEpoch ); - logger.verbose("Migrated blobSidecars from hot DB to cold DB", {migratedEntries}); + logger.verbose("Migrated blobSidecars from hot DB to cold DB", {...logCtx, migratedEntries}); } if (finalizedPostFulu) { @@ -99,7 +102,7 @@ export async function archiveBlocks( finalizedCanonicalBlockRoots, currentEpoch ); - logger.verbose("Migrated dataColumnSidecars from hot DB to cold DB", {migratedEntries}); + logger.verbose("Migrated dataColumnSidecars from hot DB to cold DB", {...logCtx, migratedEntries}); } } @@ -114,14 +117,14 @@ export async function archiveBlocks( nonCanonicalBlockRoots.map(async (root, index) => { const block = finalizedNonCanonicalBlocks[index]; const blockBytes = await db.block.getBinary(root); - const logCtx = {slot: block.slot, root: block.blockRoot}; + const blockLogCtx = {slot: block.slot, root: block.blockRoot}; if (blockBytes) { await persistOrphanedBlock(block.slot, block.blockRoot, blockBytes, { persistOrphanedBlocksDir: persistOrphanedBlocksDir ?? "orphaned_blocks", }); - logger.verbose("Persisted orphaned block", logCtx); + logger.verbose("Persisted orphaned block", {...logCtx, ...blockLogCtx}); } else { - logger.warn("Tried to persist orphaned block but no block found", logCtx); + logger.warn("Tried to persist orphaned block but no block found", {...logCtx, ...blockLogCtx}); } }) ); @@ -129,17 +132,18 @@ export async function archiveBlocks( await db.block.batchDelete(nonCanonicalBlockRoots); logger.verbose("Deleted non canonical blocks from hot DB", { + ...logCtx, slots: finalizedNonCanonicalBlocks.map((summary) => summary.slot).join(","), }); if (finalizedPostDeneb) { await db.blobSidecars.batchDelete(nonCanonicalBlockRoots); - logger.verbose("Deleted non canonical blobSidecars from hot DB"); + logger.verbose("Deleted non canonical blobSidecars from hot DB", logCtx); } if (finalizedPostFulu) { await db.dataColumnSidecar.deleteMany(nonCanonicalBlockRoots); - logger.verbose("Deleted non canonical dataColumnSidecars from hot DB"); + logger.verbose("Deleted non canonical dataColumnSidecars from hot DB", logCtx); } } @@ -154,13 +158,13 @@ export async function archiveBlocks( const slotsToDelete = await db.blobSidecarsArchive.keys({lt: computeStartSlotAtEpoch(blobSidecarsMinEpoch)}); if (slotsToDelete.length > 0) { await db.blobSidecarsArchive.batchDelete(slotsToDelete); - logger.verbose(`blobSidecars prune: batchDelete range ${slotsToDelete[0]}..${slotsToDelete.at(-1)}`); + logger.verbose(`blobSidecars prune: batchDelete range ${slotsToDelete[0]}..${slotsToDelete.at(-1)}`, logCtx); } else { - logger.verbose(`blobSidecars prune: no entries before epoch ${blobSidecarsMinEpoch}`); + logger.verbose(`blobSidecars prune: no entries before epoch ${blobSidecarsMinEpoch}`, logCtx); } } } else { - logger.verbose("blobSidecars pruning skipped: archiveDataEpochs set to Infinity"); + logger.verbose("blobSidecars pruning skipped: archiveDataEpochs set to Infinity", logCtx); } } @@ -184,20 +188,22 @@ export async function archiveBlocks( if (slotsToDelete.length > 0) { await db.dataColumnSidecarArchive.deleteMany(slotsToDelete); logger.verbose("dataColumnSidecars prune", { + ...logCtx, slotRange: prettyPrintIndices(slotsToDelete), numOfSlots: slotsToDelete.length, totalNumOfSidecars: prefixedKeys.length, }); } else { - logger.verbose(`dataColumnSidecars prune: no entries before epoch ${dataColumnSidecarsMinEpoch}`); + logger.verbose(`dataColumnSidecars prune: no entries before epoch ${dataColumnSidecarsMinEpoch}`, logCtx); } } else { logger.verbose( - `dataColumnSidecars pruning skipped: ${dataColumnSidecarsMinEpoch} is before fulu fork epoch ${config.FULU_FORK_EPOCH}` + `dataColumnSidecars pruning skipped: ${dataColumnSidecarsMinEpoch} is before fulu fork epoch ${config.FULU_FORK_EPOCH}`, + logCtx ); } } else { - logger.verbose("dataColumnSidecars pruning skipped: archiveDataEpochs set to Infinity"); + logger.verbose("dataColumnSidecars pruning skipped: archiveDataEpochs set to Infinity", logCtx); } } @@ -213,8 +219,8 @@ export async function archiveBlocks( } logger.verbose("Archiving of finalized blocks complete", { + ...logCtx, totalArchived: finalizedCanonicalBlocks.length, - finalizedEpoch: finalizedCheckpoint.epoch, }); } @@ -232,6 +238,7 @@ async function migrateBlocksFromHotToColdDb(db: IBeaconDb, blocks: BlockRootSlot // load Buffer instead of SignedBeaconBlock to improve performance const canonicalBlockEntries: BlockArchiveBatchPutBinaryItem[] = await Promise.all( canonicalBlocks.map(async (block) => { + // Here we assume the blocks are already in the hot db const blockBuffer = await db.block.getBinary(block.root); if (!blockBuffer) { throw Error(`Block not found for slot ${block.slot} root ${toRootHex(block.root)}`); @@ -288,6 +295,8 @@ async function migrateBlobSidecarsFromHotToColdDb( ); }) .map(async (block) => { + // Here we assume the blob sidecars are already in the hot db + // instead of checking first the block input cache const bytes = await db.blobSidecars.getBinary(block.root); if (!bytes) { throw Error(`No blobSidecars found for slot ${block.slot} root ${toRootHex(block.root)}`); @@ -337,9 +346,11 @@ async function migrateDataColumnSidecarsFromHotToColdDb( continue; } + // Here we assume the data column sidecars are already in the hot db const dataColumnSidecarBytes = await fromAsync(db.dataColumnSidecar.valuesStreamBinary(block.root)); // there could be 0 dataColumnSidecarBytes if block has no blob logger.verbose("migrateDataColumnSidecarsFromHotToColdDb", { + currentEpoch, slot: block.slot, root: toRootHex(block.root), numSidecars: dataColumnSidecarBytes.length, diff --git a/packages/beacon-node/src/chain/blocks/blockInput/blockInput.ts b/packages/beacon-node/src/chain/blocks/blockInput/blockInput.ts index d464b1362ec5..be8a4609b12d 100644 --- a/packages/beacon-node/src/chain/blocks/blockInput/blockInput.ts +++ b/packages/beacon-node/src/chain/blocks/blockInput/blockInput.ts @@ -1,6 +1,6 @@ -import {ForkName, ForkPostFulu, ForkPreDeneb, ForkPreGloas} from "@lodestar/params"; +import {ForkName, ForkPostFulu, ForkPreDeneb, ForkPreGloas, NUMBER_OF_COLUMNS} from "@lodestar/params"; import {BeaconBlockBody, BlobIndex, ColumnIndex, SignedBeaconBlock, Slot, deneb, fulu} from "@lodestar/types"; -import {fromHex, prettyBytes, toRootHex, withTimeout} from "@lodestar/utils"; +import {byteArrayEquals, fromHex, prettyBytes, toRootHex, withTimeout} from "@lodestar/utils"; import {VersionedHashes} from "../../../execution/index.js"; import {kzgCommitmentToVersionedHash} from "../../../util/blobs.js"; import {BlockInputError, BlockInputErrorCode} from "./errors.js"; @@ -412,6 +412,10 @@ export class BlockInputBlobs extends AbstractBlockInput, blobSideca if (!blockCommitment || !blobSidecar.kzgCommitment) { return false; } - return Buffer.compare(blockCommitment, blobSidecar.kzgCommitment) === 0; + return byteArrayEquals(blockCommitment, blobSidecar.kzgCommitment); } function assertBlockAndBlobArePaired( @@ -557,6 +561,7 @@ type BlockInputColumnsState = | { hasBlock: true; hasAllData: true; + hasComputedAllData: boolean; versionedHashes: VersionedHashes; block: SignedBeaconBlock; source: SourceMeta; @@ -565,6 +570,7 @@ type BlockInputColumnsState = | { hasBlock: true; hasAllData: false; + hasComputedAllData: false; versionedHashes: VersionedHashes; block: SignedBeaconBlock; source: SourceMeta; @@ -572,11 +578,13 @@ type BlockInputColumnsState = | { hasBlock: false; hasAllData: true; + hasComputedAllData: boolean; versionedHashes: VersionedHashes; } | { hasBlock: false; hasAllData: false; + hasComputedAllData: false; versionedHashes: VersionedHashes; }; /** @@ -594,6 +602,12 @@ export class BlockInputColumns extends AbstractBlockInput(); private readonly sampledColumns: ColumnIndex[]; private readonly custodyColumns: ColumnIndex[]; + /** + * This promise resolves when all sampled columns are available + * + * This is different from `dataPromise` which resolves when all data is available or could become available (e.g. through reconstruction) + */ + protected computedDataPromise = createPromise(); private constructor( init: BlockInputInit, @@ -622,6 +636,7 @@ export class BlockInputColumns extends AbstractBlockInput).blobKzgCommitments.length === 0 || this.state.hasAllData; + const hasComputedAllData = + props.block.message.body.blobKzgCommitments.length === 0 || this.state.hasComputedAllData; this.state = { ...this.state, hasBlock: true, hasAllData, + hasComputedAllData, block: props.block, source: { source: props.source, @@ -770,23 +791,42 @@ export class BlockInputColumns extends AbstractBlockInput= NUMBER_OF_COLUMNS / 2; + + const hasComputedAllData = + // has all sampled columns + sampledColumns.length === this.sampledColumns.length; this.state = { ...this.state, hasAllData: hasAllData || this.state.hasAllData, + hasComputedAllData: hasComputedAllData || this.state.hasComputedAllData, timeCompleteSec: hasAllData ? seenTimestampSec : undefined, } as BlockInputColumnsState; if (hasAllData && sampledColumns !== null) { this.dataPromise.resolve(sampledColumns); } + + if (hasComputedAllData && sampledColumns !== null) { + this.computedDataPromise.resolve(sampledColumns); + } } hasColumn(columnIndex: number): boolean { return this.columnsCache.has(columnIndex); } + getColumn(columnIndex: number): fulu.DataColumnSidecar | undefined { + return this.columnsCache.get(columnIndex)?.columnSidecar; + } + getVersionedHashes(): VersionedHashes { return this.state.versionedHashes; } @@ -851,4 +891,15 @@ export class BlockInputColumns extends AbstractBlockInput { + if (!this.state.hasComputedAllData) { + return withTimeout(() => this.computedDataPromise.promise, timeout, signal); + } + return Promise.resolve(this.getSampledColumns()); + } } diff --git a/packages/beacon-node/src/chain/blocks/importBlock.ts b/packages/beacon-node/src/chain/blocks/importBlock.ts index 2d5e8fbf50b7..cfb34865618d 100644 --- a/packages/beacon-node/src/chain/blocks/importBlock.ts +++ b/packages/beacon-node/src/chain/blocks/importBlock.ts @@ -30,11 +30,10 @@ import type {BeaconChain} from "../chain.js"; import {ChainEvent, ReorgEventData} from "../emitter.js"; import {ForkchoiceCaller} from "../forkChoice/index.js"; import {REPROCESS_MIN_TIME_TO_NEXT_SLOT_SEC} from "../reprocess.js"; -import {toCheckpointHex} from "../stateCache/index.js"; +import {toCheckpointHex} from "../stateCache/persistentCheckpointsCache.js"; import {isBlockInputBlobs, isBlockInputColumns} from "./blockInput/blockInput.js"; import {AttestationImportOpt, FullyVerifiedBlock, ImportBlockOpts} from "./types.js"; import {getCheckpointFromState} from "./utils/checkpoint.js"; -import {writeBlockInputToDb} from "./writeBlockInputToDb.js"; /** * Fork-choice allows to import attestations from current (0) or past (1) epoch. @@ -91,11 +90,8 @@ export async function importBlock( throw Error("Unavailable block can not be imported in forkchoice"); } - // 1. Persist block to hot DB (pre-emptively) - // If eagerPersistBlock = true we do that in verifyBlocksInEpoch to batch all I/O operations to save block time to head - if (!opts.eagerPersistBlock) { - await writeBlockInputToDb.call(this, [blockInput]); - } + // 1. Persist block to hot DB (performed asynchronously to avoid blocking head selection) + void this.unfinalizedBlockWrites.push([blockInput]); // Without forcefully clearing this cache, we would rely on WeakMap to evict memory which is not reliable this.serializedCache.clear(); @@ -418,13 +414,20 @@ export async function importBlock( this.logger.verbose("After importBlock caching postState without SSZ cache", {slot: postState.slot}); } + // Cache shufflings when crossing an epoch boundary + const parentEpoch = computeEpochAtSlot(parentBlockSlot); + if (parentEpoch < blockEpoch) { + this.shufflingCache.processState(postState); + this.logger.verbose("Processed shuffling for next epoch", {parentEpoch, blockEpoch, slot: blockSlot}); + } + if (blockSlot % SLOTS_PER_EPOCH === 0) { // Cache state to preserve epoch transition work const checkpointState = postState; const cp = getCheckpointFromState(checkpointState); this.regen.addCheckpointState(cp, checkpointState); - // consumers should not mutate or get the transfered cache - this.emitter.emit(ChainEvent.checkpoint, cp, checkpointState.clone(true)); + // consumers should not mutate state ever + this.emitter.emit(ChainEvent.checkpoint, cp, checkpointState); // Note: in-lined code from previos handler of ChainEvent.checkpoint this.logger.verbose("Checkpoint processed", toCheckpointHex(cp)); @@ -584,7 +587,10 @@ export function addAttestationPostElectra( true ); } else { - const committees = epochCtx.getBeaconCommittees(attestation.data.slot, committeeIndices); + const attSlot = attestation.data.slot; + const attEpoch = computeEpochAtSlot(attSlot); + const decisionRoot = epochCtx.getShufflingDecisionRoot(attEpoch); + const committees = this.shufflingCache.getBeaconCommittees(attEpoch, decisionRoot, attSlot, committeeIndices); const aggregationBools = attestation.aggregationBits.toBoolArray(); let offset = 0; for (let i = 0; i < committees.length; i++) { diff --git a/packages/beacon-node/src/chain/blocks/index.ts b/packages/beacon-node/src/chain/blocks/index.ts index a44fa70d342a..ca5f1468f051 100644 --- a/packages/beacon-node/src/chain/blocks/index.ts +++ b/packages/beacon-node/src/chain/blocks/index.ts @@ -11,7 +11,6 @@ import {FullyVerifiedBlock, ImportBlockOpts} from "./types.js"; import {assertLinearChainSegment} from "./utils/chainSegment.js"; import {verifyBlocksInEpoch} from "./verifyBlock.js"; import {verifyBlocksSanityChecks} from "./verifyBlocksSanityChecks.js"; -import {removeEagerlyPersistedBlockInputs} from "./writeBlockInputToDb.js"; export {AttestationImportOpt, type ImportBlockOpts} from "./types.js"; @@ -143,24 +142,6 @@ export async function processBlocks( } } - // Clean db if we don't have blocks in forkchoice but already persisted them to db - // - // NOTE: this function is awaited to ensure that DB size remains constant, otherwise an attacker may bloat the - // disk with big malicious payloads. Our sequential block importer will wait for this promise before importing - // another block. The removal call error is not propagated since that would halt the chain. - // - // LOG: Because the error is not propagated and there's a risk of db bloat, the error is logged at warn level - // to alert the user of potential db bloat. This error _should_ never happen user must act and report to us - if (opts.eagerPersistBlock) { - await removeEagerlyPersistedBlockInputs.call(this, blocks).catch((e) => { - this.logger.warn( - "Error pruning eagerly imported block inputs, DB may grow in size if this error happens frequently", - {slot: blocks.map((block) => block.getBlock().message.slot).join(",")}, - e - ); - }); - } - throw err; } } diff --git a/packages/beacon-node/src/chain/blocks/types.ts b/packages/beacon-node/src/chain/blocks/types.ts index f8f091a5b07f..50ed0076515c 100644 --- a/packages/beacon-node/src/chain/blocks/types.ts +++ b/packages/beacon-node/src/chain/blocks/types.ts @@ -78,8 +78,6 @@ export type ImportBlockOpts = { validBlobSidecars?: BlobSidecarValidation; /** Seen timestamp seconds */ seenTimestampSec?: number; - /** Set to true if persist block right at verification time */ - eagerPersistBlock?: boolean; }; /** diff --git a/packages/beacon-node/src/chain/blocks/verifyBlock.ts b/packages/beacon-node/src/chain/blocks/verifyBlock.ts index 9b4e7900fb55..4be4e5c6a387 100644 --- a/packages/beacon-node/src/chain/blocks/verifyBlock.ts +++ b/packages/beacon-node/src/chain/blocks/verifyBlock.ts @@ -21,7 +21,6 @@ import {verifyBlocksDataAvailability} from "./verifyBlocksDataAvailability.js"; import {SegmentExecStatus, verifyBlocksExecutionPayload} from "./verifyBlocksExecutionPayloads.js"; import {verifyBlocksSignatures} from "./verifyBlocksSignatures.js"; import {verifyBlocksStateTransitionOnly} from "./verifyBlocksStateTransitionOnly.js"; -import {writeBlockInputToDb} from "./writeBlockInputToDb.js"; /** * Verifies 1 or more blocks are fully valid; from a linear sequence of blocks. @@ -75,6 +74,10 @@ export async function verifyBlocksInEpoch( throw new BlockError(block0, {code: BlockErrorCode.PRESTATE_MISSING, error: e as Error}); }); + // in forky condition, make sure to populate ShufflingCache with regened state + // otherwise it may fail to get indexed attestations from shuffling cache later + this.shufflingCache.processState(preState0); + if (!isStateValidatorsNodesPopulated(preState0)) { this.logger.verbose("verifyBlocksInEpoch preState0 SSZ cache stats", { slot: preState0.slot, @@ -105,9 +108,11 @@ export async function verifyBlocksInEpoch( // Store indexed attestations for each block to avoid recomputing them during import const indexedAttestationsByBlock: IndexedAttestation[][] = []; for (const [i, block] of blocks.entries()) { - indexedAttestationsByBlock[i] = block.message.body.attestations.map((attestation) => - preState0.epochCtx.getIndexedAttestation(fork, attestation) - ); + indexedAttestationsByBlock[i] = block.message.body.attestations.map((attestation) => { + const attEpoch = computeEpochAtSlot(attestation.data.slot); + const decisionRoot = preState0.epochCtx.getShufflingDecisionRoot(attEpoch); + return this.shufflingCache.getIndexedAttestation(attEpoch, decisionRoot, fork, attestation); + }); } // batch all I/O operations to reduce overhead @@ -140,7 +145,6 @@ export async function verifyBlocksInEpoch( opts.skipVerifyBlockSignatures !== true ? verifyBlocksSignatures( this.config, - this.index2pubkey, this.bls, this.logger, this.metrics, @@ -150,13 +154,6 @@ export async function verifyBlocksInEpoch( opts ) : Promise.resolve({verifySignaturesTime: Date.now()}), - - // ideally we want to only persist blocks after verifying them however the reality is there are - // rarely invalid blocks we'll batch all I/O operation here to reduce the overhead if there's - // an error, we'll remove blocks not in forkchoice - opts.verifyOnly !== true && opts.eagerPersistBlock - ? writeBlockInputToDb.call(this, blockInputs) - : Promise.resolve(), ]); if (opts.verifyOnly !== true) { diff --git a/packages/beacon-node/src/chain/blocks/verifyBlocksSignatures.ts b/packages/beacon-node/src/chain/blocks/verifyBlocksSignatures.ts index 19783c89577f..f547c3d64340 100644 --- a/packages/beacon-node/src/chain/blocks/verifyBlocksSignatures.ts +++ b/packages/beacon-node/src/chain/blocks/verifyBlocksSignatures.ts @@ -1,5 +1,5 @@ import {BeaconConfig} from "@lodestar/config"; -import {CachedBeaconStateAllForks, Index2PubkeyCache, getBlockSignatureSets} from "@lodestar/state-transition"; +import {CachedBeaconStateAllForks, getBlockSignatureSets} from "@lodestar/state-transition"; import {IndexedAttestation, SignedBeaconBlock} from "@lodestar/types"; import {Logger} from "@lodestar/utils"; import {Metrics} from "../../metrics/metrics.js"; @@ -17,7 +17,6 @@ import {ImportBlockOpts} from "./types.js"; */ export async function verifyBlocksSignatures( config: BeaconConfig, - index2pubkey: Index2PubkeyCache, bls: IBlsVerifier, logger: Logger, metrics: Metrics | null, @@ -42,16 +41,9 @@ export async function verifyBlocksSignatures( : // // Verify signatures per block to track which block is invalid bls.verifySignatureSets( - getBlockSignatureSets( - config, - index2pubkey, - currentSyncCommitteeIndexed, - block, - indexedAttestationsByBlock[i], - { - skipProposerSignature: opts.validProposerSignature, - } - ) + getBlockSignatureSets(config, currentSyncCommitteeIndexed, block, indexedAttestationsByBlock[i], { + skipProposerSignature: opts.validProposerSignature, + }) ); // getBlockSignatureSets() takes 45ms in benchmarks for 2022Q2 mainnet blocks (100 sigs). When syncing a 32 blocks diff --git a/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts b/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts index b877dfa0910b..d553b8f0f28f 100644 --- a/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts +++ b/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts @@ -5,9 +5,8 @@ import { StateHashTreeRootSource, stateTransition, } from "@lodestar/state-transition"; -import {ErrorAborted, Logger} from "@lodestar/utils"; +import {ErrorAborted, Logger, byteArrayEquals} from "@lodestar/utils"; import {Metrics} from "../../metrics/index.js"; -import {byteArrayEquals} from "../../util/bytes.js"; import {nextEventLoop} from "../../util/eventLoop.js"; import {BlockError, BlockErrorCode} from "../errors/index.js"; import {BlockProcessOpts} from "../options.js"; @@ -59,6 +58,7 @@ export async function verifyBlocksStateTransitionOnly( // if block is trusted don't verify proposer or op signature verifyProposer: !useBlsBatchVerify && !validSignatures && !validProposerSignature, verifySignatures: !useBlsBatchVerify && !validSignatures, + dontTransferCache: false, }, {metrics, validatorMonitor} ); diff --git a/packages/beacon-node/src/chain/blocks/writeBlockInputToDb.ts b/packages/beacon-node/src/chain/blocks/writeBlockInputToDb.ts index c2d96afe41f8..07d1e9f508d4 100644 --- a/packages/beacon-node/src/chain/blocks/writeBlockInputToDb.ts +++ b/packages/beacon-node/src/chain/blocks/writeBlockInputToDb.ts @@ -44,6 +44,15 @@ export async function writeBlockInputToDb(this: BeaconChain, blocksInputs: IBloc // NOTE: Old data is pruned on archive if (isBlockInputColumns(blockInput)) { + if (!blockInput.hasComputedAllData()) { + // Supernodes may only have a subset of the data columns by the time the block begins to be imported + // because full data availability can be assumed after NUMBER_OF_COLUMNS / 2 columns are available. + // Here, however, all data columns must be fully available/reconstructed before persisting to the DB. + await blockInput.waitForComputedAllData(BLOB_AVAILABILITY_TIMEOUT).catch(() => { + this.logger.debug("Failed to wait for computed all data", {slot, blockRoot: blockRootHex}); + }); + } + const {custodyColumns} = this.custodyConfig; const blobsLen = (block.message as fulu.BeaconBlock).body.blobKzgCommitments.length; let dataColumnsLen: number; @@ -98,35 +107,29 @@ export async function writeBlockInputToDb(this: BeaconChain, blocksInputs: IBloc } } -/** - * Prunes eagerly persisted block inputs only if not known to the fork-choice - */ -export async function removeEagerlyPersistedBlockInputs(this: BeaconChain, blockInputs: IBlockInput[]): Promise { - const blockToRemove = []; - const blobsToRemove = []; - const dataColumnsToRemove = []; - - for (const blockInput of blockInputs) { - const block = blockInput.getBlock(); - const slot = block.message.slot; - const blockRoot = this.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block.message); - const blockRootHex = toRootHex(blockRoot); - if (!this.forkChoice.hasBlockHex(blockRootHex)) { - blockToRemove.push(block); - - if (isBlockInputColumns(blockInput) && blockInput.getCustodyColumns().length > 0) { - dataColumnsToRemove.push(blockRoot); - } else if (isBlockInputBlobs(blockInput)) { - const blobSidecars = blockInput.getBlobs(); - blobsToRemove.push({blockRoot, slot, blobSidecars}); +export async function persistBlockInputs(this: BeaconChain, blockInputs: IBlockInput[]): Promise { + await writeBlockInputToDb + .call(this, blockInputs) + .catch((e) => { + this.logger.debug( + "Error persisting block input in hot db", + { + count: blockInputs.length, + slot: blockInputs[0].slot, + root: blockInputs[0].blockRootHex, + }, + e + ); + }) + .finally(() => { + for (const blockInput of blockInputs) { + this.seenBlockInputCache.prune(blockInput.blockRootHex); } - } - } - - await Promise.all([ - // TODO: Batch DB operations not with Promise.all but with level db ops - this.db.block.batchRemove(blockToRemove), - this.db.blobSidecars.batchRemove(blobsToRemove), - this.db.dataColumnSidecar.deleteMany(dataColumnsToRemove), - ]); + if (blockInputs.length === 1) { + this.logger.debug("Pruned block input", { + slot: blockInputs[0].slot, + root: blockInputs[0].blockRootHex, + }); + } + }); } diff --git a/packages/beacon-node/src/chain/bls/multithread/index.ts b/packages/beacon-node/src/chain/bls/multithread/index.ts index 52b2efdd1ec7..a3ca9ca803b6 100644 --- a/packages/beacon-node/src/chain/bls/multithread/index.ts +++ b/packages/beacon-node/src/chain/bls/multithread/index.ts @@ -7,7 +7,7 @@ import {Worker, spawn} from "@chainsafe/threads"; self = undefined; import {PublicKey} from "@chainsafe/blst"; -import {ISignatureSet} from "@lodestar/state-transition"; +import {ISignatureSet, Index2PubkeyCache} from "@lodestar/state-transition"; import {Logger} from "@lodestar/utils"; import {Metrics} from "../../../metrics/index.js"; import {LinkedList} from "../../../util/array.js"; @@ -34,6 +34,7 @@ const workerDir = process.env.NODE_ENV === "test" ? "../../../../lib/chain/bls/m export type BlsMultiThreadWorkerPoolModules = { logger: Logger; metrics: Metrics | null; + index2pubkey: Index2PubkeyCache; }; export type BlsMultiThreadWorkerPoolOptions = { @@ -113,6 +114,7 @@ type WorkerDescriptor = { export class BlsMultiThreadWorkerPool implements IBlsVerifier { private readonly logger: Logger; private readonly metrics: Metrics | null; + private readonly index2pubkey: Index2PubkeyCache; private readonly workers: WorkerDescriptor[]; private readonly jobs = new LinkedList(); @@ -128,9 +130,10 @@ export class BlsMultiThreadWorkerPool implements IBlsVerifier { private workersBusy = 0; constructor(options: BlsMultiThreadWorkerPoolOptions, modules: BlsMultiThreadWorkerPoolModules) { - const {logger, metrics} = modules; + const {logger, metrics, index2pubkey} = modules; this.logger = logger; this.metrics = metrics; + this.index2pubkey = index2pubkey; this.blsVerifyAllMultiThread = options.blsVerifyAllMultiThread ?? false; // Use compressed for herumi for now. @@ -170,7 +173,7 @@ export class BlsMultiThreadWorkerPool implements IBlsVerifier { try { return verifySignatureSetsMaybeBatch( sets.map((set) => ({ - publicKey: getAggregatedPubkey(set), + publicKey: getAggregatedPubkey(set, this.index2pubkey), message: set.signingRoot.valueOf(), signature: set.signature, })) @@ -395,7 +398,7 @@ export class BlsMultiThreadWorkerPool implements IBlsVerifier { try { // Note: This can throw, must be handled per-job. // Pubkey and signature aggregation is defered here - workReq = await jobItemWorkReq(job, this.metrics); + workReq = await jobItemWorkReq(job, this.index2pubkey, this.metrics); } catch (e) { this.metrics?.blsThreadPool.errorAggregateSignatureSetsCount.inc({type: job.type}); diff --git a/packages/beacon-node/src/chain/bls/multithread/jobItem.ts b/packages/beacon-node/src/chain/bls/multithread/jobItem.ts index 873aaba8fb8a..a5d28a524490 100644 --- a/packages/beacon-node/src/chain/bls/multithread/jobItem.ts +++ b/packages/beacon-node/src/chain/bls/multithread/jobItem.ts @@ -1,5 +1,5 @@ import {PublicKey, asyncAggregateWithRandomness} from "@chainsafe/blst"; -import {ISignatureSet, SignatureSetType} from "@lodestar/state-transition"; +import {ISignatureSet, Index2PubkeyCache, SignatureSetType} from "@lodestar/state-transition"; import {Metrics} from "../../../metrics/metrics.js"; import {LinkedList} from "../../../util/array.js"; import {VerifySignatureOpts} from "../interface.js"; @@ -48,14 +48,18 @@ export function jobItemSigSets(job: JobQueueItem): number { * Prepare BlsWorkReq from JobQueueItem * WARNING: May throw with untrusted user input */ -export async function jobItemWorkReq(job: JobQueueItem, metrics: Metrics | null): Promise { +export async function jobItemWorkReq( + job: JobQueueItem, + index2pubkey: Index2PubkeyCache, + metrics: Metrics | null +): Promise { switch (job.type) { case JobQueueItemType.default: return { opts: job.opts, sets: job.sets.map((set) => ({ // this can throw, handled in the consumer code - publicKey: getAggregatedPubkey(set, metrics).toBytes(), + publicKey: getAggregatedPubkey(set, index2pubkey, metrics).toBytes(), signature: set.signature, message: set.signingRoot, })), diff --git a/packages/beacon-node/src/chain/bls/singleThread.ts b/packages/beacon-node/src/chain/bls/singleThread.ts index 4e2875b1cea9..673035e204c7 100644 --- a/packages/beacon-node/src/chain/bls/singleThread.ts +++ b/packages/beacon-node/src/chain/bls/singleThread.ts @@ -1,5 +1,5 @@ import {PublicKey, Signature, aggregatePublicKeys, aggregateSignatures, verify} from "@chainsafe/blst"; -import {ISignatureSet} from "@lodestar/state-transition"; +import {ISignatureSet, Index2PubkeyCache} from "@lodestar/state-transition"; import {Metrics} from "../../metrics/index.js"; import {IBlsVerifier} from "./interface.js"; import {verifySignatureSetsMaybeBatch} from "./maybeBatch.js"; @@ -7,16 +7,18 @@ import {getAggregatedPubkey, getAggregatedPubkeysCount} from "./utils.js"; export class BlsSingleThreadVerifier implements IBlsVerifier { private readonly metrics: Metrics | null; + private readonly index2pubkey: Index2PubkeyCache; - constructor({metrics = null}: {metrics: Metrics | null}) { + constructor({metrics = null, index2pubkey}: {metrics: Metrics | null; index2pubkey: Index2PubkeyCache}) { this.metrics = metrics; + this.index2pubkey = index2pubkey; } async verifySignatureSets(sets: ISignatureSet[]): Promise { this.metrics?.bls.aggregatedPubkeys.inc(getAggregatedPubkeysCount(sets)); const setsAggregated = sets.map((set) => ({ - publicKey: getAggregatedPubkey(set), + publicKey: getAggregatedPubkey(set, this.index2pubkey, this.metrics), message: set.signingRoot, signature: set.signature, })); diff --git a/packages/beacon-node/src/chain/bls/utils.ts b/packages/beacon-node/src/chain/bls/utils.ts index 63f2bdd80458..e01ab788026f 100644 --- a/packages/beacon-node/src/chain/bls/utils.ts +++ b/packages/beacon-node/src/chain/bls/utils.ts @@ -1,17 +1,25 @@ import {PublicKey, aggregatePublicKeys} from "@chainsafe/blst"; -import {ISignatureSet, SignatureSetType} from "@lodestar/state-transition"; +import {ISignatureSet, Index2PubkeyCache, SignatureSetType} from "@lodestar/state-transition"; import {Metrics} from "../../metrics/metrics.js"; -export function getAggregatedPubkey(signatureSet: ISignatureSet, metrics: Metrics | null = null): PublicKey { +export function getAggregatedPubkey( + signatureSet: ISignatureSet, + index2pubkey: Index2PubkeyCache, + metrics: Metrics | null = null +): PublicKey { switch (signatureSet.type) { case SignatureSetType.single: return signatureSet.pubkey; + case SignatureSetType.indexed: + return index2pubkey[signatureSet.index]; + case SignatureSetType.aggregate: { const timer = metrics?.blsThreadPool.pubkeysAggregationMainThreadDuration.startTimer(); - const pubkeys = aggregatePublicKeys(signatureSet.pubkeys); + const pubkeys = signatureSet.indices.map((i) => index2pubkey[i]); + const aggregated = aggregatePublicKeys(pubkeys); timer?.(); - return pubkeys; + return aggregated; } default: @@ -20,11 +28,11 @@ export function getAggregatedPubkey(signatureSet: ISignatureSet, metrics: Metric } export function getAggregatedPubkeysCount(signatureSets: ISignatureSet[]): number { - let pubkeysConut = 0; + let pubkeysCount = 0; for (const set of signatureSets) { if (set.type === SignatureSetType.aggregate) { - pubkeysConut += set.pubkeys.length; + pubkeysCount += set.indices.length; } } - return pubkeysConut; + return pubkeysCount; } diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 4e27c3354cdd..1c5bb212b8c1 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -37,14 +37,19 @@ import { UintNum64, ValidatorIndex, Wei, + deneb, + fulu, isBlindedBeaconBlock, phase0, rewards, + ssz, + sszTypesFor, } from "@lodestar/types"; import {Logger, fromHex, gweiToWei, isErrorAborted, pruneSetToMax, sleep, toRootHex} from "@lodestar/utils"; import {ProcessShutdownCallback} from "@lodestar/validator"; import {GENESIS_EPOCH, ZERO_HASH} from "../constants/index.js"; import {IBeaconDb} from "../db/index.js"; +import {BLOB_SIDECARS_IN_WRAPPER_INDEX} from "../db/repositories/blobSidecars.ts"; import {BuilderStatus} from "../execution/builder/http.js"; import {IExecutionBuilder, IExecutionEngine} from "../execution/index.js"; import {Metrics} from "../metrics/index.js"; @@ -52,14 +57,18 @@ import {computeNodeIdFromPrivateKey} from "../network/subnets/interface.js"; import {BufferPool} from "../util/bufferPool.js"; import {Clock, ClockEvent, IClock} from "../util/clock.js"; import {CustodyConfig, getValidatorsCustodyRequirement} from "../util/dataColumns.js"; +import {callInNextEventLoop} from "../util/eventLoop.js"; import {ensureDir, writeIfNotExist} from "../util/file.js"; import {isOptimisticBlock} from "../util/forkChoice.js"; +import {JobItemQueue} from "../util/queue/itemQueue.ts"; import {SerializedCache} from "../util/serializedCache.js"; +import {getSlotFromSignedBeaconBlockSerialized} from "../util/sszBytes.ts"; import {ArchiveStore} from "./archiveStore/archiveStore.js"; import {CheckpointBalancesCache} from "./balancesCache.js"; import {BeaconProposerCache} from "./beaconProposerCache.js"; -import {IBlockInput} from "./blocks/blockInput/index.js"; +import {IBlockInput, isBlockInputBlobs, isBlockInputColumns} from "./blocks/blockInput/index.js"; import {BlockProcessor, ImportBlockOpts} from "./blocks/index.js"; +import {persistBlockInputs} from "./blocks/writeBlockInputToDb.ts"; import {BlsMultiThreadWorkerPool, BlsSingleThreadVerifier, IBlsVerifier} from "./bls/index.js"; import {ColumnReconstructionTracker} from "./ColumnReconstructionTracker.js"; import {ChainEvent, ChainEventEmitter} from "./emitter.js"; @@ -70,7 +79,9 @@ import {LightClientServer} from "./lightClient/index.js"; import { AggregatedAttestationPool, AttestationPool, + ExecutionPayloadBidPool, OpPool, + PayloadAttestationPool, SyncCommitteeMessagePool, SyncContributionAndProofPool, } from "./opPools/index.js"; @@ -86,6 +97,9 @@ import { SeenAttesters, SeenBlockProposers, SeenContributionAndProof, + SeenExecutionPayloadBids, + SeenExecutionPayloadEnvelopes, + SeenPayloadAttesters, SeenSyncCommitteeMessages, } from "./seenCache/index.js"; import {SeenAggregatedAttestations} from "./seenCache/seenAggregateAndProof.js"; @@ -93,12 +107,10 @@ import {SeenAttestationDatas} from "./seenCache/seenAttestationData.js"; import {SeenBlockAttesters} from "./seenCache/seenBlockAttesters.js"; import {SeenBlockInput} from "./seenCache/seenGossipBlockInput.js"; import {ShufflingCache} from "./shufflingCache.js"; -import {BlockStateCacheImpl} from "./stateCache/blockStateCacheImpl.js"; import {DbCPStateDatastore, checkpointToDatastoreKey} from "./stateCache/datastore/db.js"; import {FileCPStateDatastore} from "./stateCache/datastore/file.js"; import {CPStateDatastore} from "./stateCache/datastore/types.js"; import {FIFOBlockStateCache} from "./stateCache/fifoBlockStateCache.js"; -import {InMemoryCheckpointStateCache} from "./stateCache/inMemoryCheckpointsCache.js"; import {PersistentCheckpointStateCache} from "./stateCache/persistentCheckpointsCache.js"; import {CheckpointStateCache} from "./stateCache/types.js"; import {ValidatorMonitor} from "./validatorMonitor.js"; @@ -112,6 +124,11 @@ import {ValidatorMonitor} from "./validatorMonitor.js"; */ const DEFAULT_MAX_CACHED_PRODUCED_RESULTS = 4; +/** + * The maximum number of pending unfinalized block writes to the database before backpressure is applied. + */ +const DEFAULT_MAX_PENDING_UNFINALIZED_BLOCK_WRITES = 32; + export class BeaconChain implements IBeaconChain { readonly genesisTime: UintNum64; readonly genesisValidatorsRoot: Root; @@ -123,7 +140,7 @@ export class BeaconChain implements IBeaconChain { readonly logger: Logger; readonly metrics: Metrics | null; readonly validatorMonitor: ValidatorMonitor | null; - readonly bufferPool: BufferPool | null; + readonly bufferPool: BufferPool; readonly anchorStateLatestBlockSlot: Slot; @@ -135,18 +152,24 @@ export class BeaconChain implements IBeaconChain { readonly lightClientServer?: LightClientServer; readonly reprocessController: ReprocessController; readonly archiveStore: ArchiveStore; + readonly unfinalizedBlockWrites: JobItemQueue<[IBlockInput[]], void>; // Ops pool readonly attestationPool: AttestationPool; readonly aggregatedAttestationPool: AggregatedAttestationPool; readonly syncCommitteeMessagePool: SyncCommitteeMessagePool; readonly syncContributionAndProofPool; + readonly executionPayloadBidPool: ExecutionPayloadBidPool; + readonly payloadAttestationPool: PayloadAttestationPool; readonly opPool: OpPool; // Gossip seen cache readonly seenAttesters = new SeenAttesters(); readonly seenAggregators = new SeenAggregators(); + readonly seenPayloadAttesters = new SeenPayloadAttesters(); readonly seenAggregatedAttestations: SeenAggregatedAttestations; + readonly seenExecutionPayloadEnvelopes = new SeenExecutionPayloadEnvelopes(); + readonly seenExecutionPayloadBids = new SeenExecutionPayloadBids(); readonly seenBlockProposers = new SeenBlockProposers(); readonly seenSyncCommitteeMessages = new SeenSyncCommitteeMessages(); readonly seenContributionAndProof: SeenContributionAndProof; @@ -253,8 +276,8 @@ export class BeaconChain implements IBeaconChain { const emitter = new ChainEventEmitter(); // by default, verify signatures on both main threads and worker threads const bls = opts.blsVerifyAllMainThread - ? new BlsSingleThreadVerifier({metrics}) - : new BlsMultiThreadWorkerPool(opts, {logger, metrics}); + ? new BlsSingleThreadVerifier({metrics, index2pubkey}) + : new BlsMultiThreadWorkerPool(opts, {logger, metrics, index2pubkey}); if (!clock) clock = new Clock({config, genesisTime: this.genesisTime, signal}); @@ -263,6 +286,8 @@ export class BeaconChain implements IBeaconChain { this.aggregatedAttestationPool = new AggregatedAttestationPool(this.config, metrics); this.syncCommitteeMessagePool = new SyncCommitteeMessagePool(config, clock, this.opts?.preaggregateSlotDistance); this.syncContributionAndProofPool = new SyncContributionAndProofPool(config, clock, metrics, logger); + this.executionPayloadBidPool = new ExecutionPayloadBidPool(); + this.payloadAttestationPool = new PayloadAttestationPool(config, clock, metrics); this.opPool = new OpPool(config); this.seenAggregatedAttestations = new SeenAggregatedAttestations(metrics); @@ -291,7 +316,8 @@ export class BeaconChain implements IBeaconChain { }); this._earliestAvailableSlot = anchorState.slot; - this.shufflingCache = anchorState.epochCtx.shufflingCache = new ShufflingCache(metrics, logger, this.opts, [ + + this.shufflingCache = new ShufflingCache(metrics, logger, this.opts, [ { shuffling: anchorState.epochCtx.previousShuffling, decisionRoot: anchorState.epochCtx.previousDecisionRoot, @@ -311,32 +337,22 @@ export class BeaconChain implements IBeaconChain { this.index2pubkey = index2pubkey; const fileDataStore = opts.nHistoricalStatesFileDataStore ?? true; - const blockStateCache = this.opts.nHistoricalStates - ? new FIFOBlockStateCache(this.opts, {metrics}) - : new BlockStateCacheImpl({metrics}); - this.bufferPool = this.opts.nHistoricalStates - ? new BufferPool(anchorState.type.tree_serializedSize(anchorState.node), metrics) - : null; - - let checkpointStateCache: CheckpointStateCache; - this.cpStateDatastore = undefined; - if (this.opts.nHistoricalStates) { - this.cpStateDatastore = fileDataStore ? new FileCPStateDatastore(dataDir) : new DbCPStateDatastore(this.db); - checkpointStateCache = new PersistentCheckpointStateCache( - { - config, - metrics, - logger, - clock, - blockStateCache, - bufferPool: this.bufferPool, - datastore: this.cpStateDatastore, - }, - this.opts - ); - } else { - checkpointStateCache = new InMemoryCheckpointStateCache({metrics}); - } + const blockStateCache = new FIFOBlockStateCache(this.opts, {metrics}); + this.bufferPool = new BufferPool(anchorState.type.tree_serializedSize(anchorState.node), metrics); + + this.cpStateDatastore = fileDataStore ? new FileCPStateDatastore(dataDir) : new DbCPStateDatastore(this.db); + const checkpointStateCache: CheckpointStateCache = new PersistentCheckpointStateCache( + { + config, + metrics, + logger, + clock, + blockStateCache, + bufferPool: this.bufferPool, + datastore: this.cpStateDatastore, + }, + this.opts + ); const {checkpoint} = computeAnchorCheckpoint(config, anchorState); blockStateCache.add(anchorState); @@ -359,6 +375,7 @@ export class BeaconChain implements IBeaconChain { forkChoice, blockStateCache, checkpointStateCache, + seenBlockInputCache: this.seenBlockInputCache, db, metrics, validatorMonitor, @@ -403,6 +420,15 @@ export class BeaconChain implements IBeaconChain { signal ); + this.unfinalizedBlockWrites = new JobItemQueue( + persistBlockInputs.bind(this), + { + maxLength: DEFAULT_MAX_PENDING_UNFINALIZED_BLOCK_WRITES, + signal, + }, + metrics?.unfinalizedBlockWritesQueue + ); + // always run PrepareNextSlotScheduler except for fork_choice spec tests if (!opts?.disablePrepareNextSlot) { new PrepareNextSlotScheduler(this, this.config, metrics, this.logger, signal); @@ -417,6 +443,7 @@ export class BeaconChain implements IBeaconChain { clock.addListener(ClockEvent.epoch, this.onClockEpoch.bind(this)); emitter.addListener(ChainEvent.forkChoiceFinalized, this.onForkChoiceFinalized.bind(this)); emitter.addListener(ChainEvent.forkChoiceJustified, this.onForkChoiceJustified.bind(this)); + emitter.addListener(ChainEvent.checkpoint, this.onCheckpoint.bind(this)); } async init(): Promise { @@ -427,6 +454,12 @@ export class BeaconChain implements IBeaconChain { async close(): Promise { await this.archiveStore.close(); await this.bls.close(); + + // Since we don't persist unfinalized fork-choice, + // we can abort any ongoing unfinalized block writes. + // TODO: persist fork choice to disk and allow unfinalized block writes to complete. + this.unfinalizedBlockWrites.dropAllJobs(); + this.abortController.abort(); } @@ -456,6 +489,8 @@ export class BeaconChain implements IBeaconChain { this.seenAttesters.isKnown(epoch, index) || // seenAggregators = single aggregator index, not participants of the aggregate this.seenAggregators.isKnown(epoch, index) || + // seenPayloadAttesters = single signer of payload attestation message + this.seenPayloadAttesters.isKnown(epoch, index) || // seenBlockProposers = single block proposer this.seenBlockProposers.seenAtEpoch(epoch, index) ); @@ -498,13 +533,13 @@ export class BeaconChain implements IBeaconChain { // only use regen queue if necessary, it'll cache in checkpointStateCache if regen gets through epoch transition const head = this.forkChoice.getHead(); const startSlot = computeStartSlotAtEpoch(epoch); - return this.regen.getBlockSlotState(head.blockRoot, startSlot, {dontTransferCache: true}, regenCaller); + return this.regen.getBlockSlotState(head, startSlot, {dontTransferCache: true}, regenCaller); } async getStateBySlot( slot: Slot, opts?: StateGetOpts - ): Promise<{state: BeaconStateAllForks; executionOptimistic: boolean; finalized: boolean} | null> { + ): Promise<{state: CachedBeaconStateAllForks; executionOptimistic: boolean; finalized: boolean} | null> { const finalizedBlock = this.forkChoice.getFinalizedBlock(); if (slot < finalizedBlock.slot) { @@ -516,12 +551,7 @@ export class BeaconChain implements IBeaconChain { if (opts?.allowRegen) { // Find closest canonical block to slot, then trigger regen const block = this.forkChoice.getCanonicalBlockClosestLteSlot(slot) ?? finalizedBlock; - const state = await this.regen.getBlockSlotState( - block.blockRoot, - slot, - {dontTransferCache: true}, - RegenCaller.restApi - ); + const state = await this.regen.getBlockSlotState(block, slot, {dontTransferCache: true}, RegenCaller.restApi); return { state, executionOptimistic: isOptimisticBlock(block), @@ -559,7 +589,7 @@ export class BeaconChain implements IBeaconChain { async getStateByStateRoot( stateRoot: RootHex, opts?: StateGetOpts - ): Promise<{state: BeaconStateAllForks; executionOptimistic: boolean; finalized: boolean} | null> { + ): Promise<{state: CachedBeaconStateAllForks | Uint8Array; executionOptimistic: boolean; finalized: boolean} | null> { if (opts?.allowRegen) { const state = await this.regen.getState(stateRoot, RegenCaller.restApi); const block = this.forkChoice.getBlock(state.latestBlockHeader.hashTreeRoot()); @@ -587,7 +617,8 @@ export class BeaconChain implements IBeaconChain { }; } - const data = await this.db.stateArchive.getByRoot(fromHex(stateRoot)); + // this is mostly useful for a node with `--chain.archiveStateEpochFrequency 1` + const data = await this.db.stateArchive.getBinaryByRoot(fromHex(stateRoot)); return data && {state: data, executionOptimistic: false, finalized: true}; } @@ -648,6 +679,13 @@ export class BeaconChain implements IBeaconChain { // Unfinalized slot, attempt to find in fork-choice const block = this.forkChoice.getCanonicalBlockAtSlot(slot); if (block) { + // Block found in fork-choice. + // It may be in the block input cache, awaiting full DA reconstruction, check there first + // Otherwise (most likely), check the hot db + const blockInput = this.seenBlockInputCache.get(block.blockRoot); + if (blockInput?.hasBlock()) { + return {block: blockInput.getBlock(), executionOptimistic: isOptimisticBlock(block), finalized: false}; + } const data = await this.db.block.get(fromHex(block.blockRoot)); if (data) { return {block: data, executionOptimistic: isOptimisticBlock(block), finalized: false}; @@ -667,6 +705,13 @@ export class BeaconChain implements IBeaconChain { ): Promise<{block: SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null> { const block = this.forkChoice.getBlockHex(root); if (block) { + // Block found in fork-choice. + // It may be in the block input cache, awaiting full DA reconstruction, check there first + // Otherwise (most likely), check the hot db + const blockInput = this.seenBlockInputCache.get(block.blockRoot); + if (blockInput?.hasBlock()) { + return {block: blockInput.getBlock(), executionOptimistic: isOptimisticBlock(block), finalized: false}; + } const data = await this.db.block.get(fromHex(root)); if (data) { return {block: data, executionOptimistic: isOptimisticBlock(block), finalized: false}; @@ -679,10 +724,137 @@ export class BeaconChain implements IBeaconChain { return data && {block: data, executionOptimistic: false, finalized: true}; } + async getSerializedBlockByRoot( + root: string + ): Promise<{block: Uint8Array; executionOptimistic: boolean; finalized: boolean; slot: Slot} | null> { + const block = this.forkChoice.getBlockHex(root); + if (block) { + // Block found in fork-choice. + // It may be in the block input cache, awaiting full DA reconstruction, check there first + // Otherwise (most likely), check the hot db + const blockInput = this.seenBlockInputCache.get(block.blockRoot); + if (blockInput?.hasBlock()) { + const signedBlock = blockInput.getBlock(); + const serialized = this.serializedCache.get(signedBlock); + if (serialized) { + return { + block: serialized, + executionOptimistic: isOptimisticBlock(block), + finalized: false, + slot: blockInput.slot, + }; + } + return { + block: sszTypesFor(blockInput.forkName).SignedBeaconBlock.serialize(signedBlock), + executionOptimistic: isOptimisticBlock(block), + finalized: false, + slot: blockInput.slot, + }; + } + const data = await this.db.block.getBinary(fromHex(root)); + if (data) { + const slot = getSlotFromSignedBeaconBlockSerialized(data); + if (slot === null) throw new Error(`Invalid block data stored in DB for root: ${root}`); + return {block: data, executionOptimistic: isOptimisticBlock(block), finalized: false, slot}; + } + // If block is not found in hot db, try cold db since there could be an archive cycle happening + // TODO: Add a lock to the archiver to have deterministic behavior on where are blocks + } + + const data = await this.db.blockArchive.getBinaryEntryByRoot(fromHex(root)); + return data && {block: data.value, executionOptimistic: false, finalized: true, slot: data.key}; + } + + async getBlobSidecars(blockSlot: Slot, blockRootHex: string): Promise { + const blockInput = this.seenBlockInputCache.get(blockRootHex); + if (blockInput) { + if (!isBlockInputBlobs(blockInput)) { + throw new Error(`Expected block input to have blobs: slot=${blockSlot} root=${blockRootHex}`); + } + if (!blockInput.hasAllData()) { + return null; + } + return blockInput.getBlobs(); + } + const unfinalizedBlobSidecars = (await this.db.blobSidecars.get(fromHex(blockRootHex)))?.blobSidecars ?? null; + if (unfinalizedBlobSidecars) { + return unfinalizedBlobSidecars; + } + return (await this.db.blobSidecarsArchive.get(blockSlot))?.blobSidecars ?? null; + } + + async getSerializedBlobSidecars(blockSlot: Slot, blockRootHex: string): Promise { + const blockInput = this.seenBlockInputCache.get(blockRootHex); + if (blockInput) { + if (!isBlockInputBlobs(blockInput)) { + throw new Error(`Expected block input to have blobs: slot=${blockSlot} root=${blockRootHex}`); + } + if (!blockInput.hasAllData()) { + return null; + } + return ssz.deneb.BlobSidecars.serialize(blockInput.getBlobs()); + } + const unfinalizedBlobSidecarsWrapper = await this.db.blobSidecars.getBinary(fromHex(blockRootHex)); + if (unfinalizedBlobSidecarsWrapper) { + return unfinalizedBlobSidecarsWrapper.slice(BLOB_SIDECARS_IN_WRAPPER_INDEX); + } + const finalizedBlobSidecarsWrapper = await this.db.blobSidecarsArchive.getBinary(blockSlot); + if (finalizedBlobSidecarsWrapper) { + return finalizedBlobSidecarsWrapper.slice(BLOB_SIDECARS_IN_WRAPPER_INDEX); + } + return null; + } + + async getDataColumnSidecars(blockSlot: Slot, blockRootHex: string): Promise { + const blockInput = this.seenBlockInputCache.get(blockRootHex); + if (blockInput) { + if (!isBlockInputColumns(blockInput)) { + throw new Error(`Expected block input to have columns: slot=${blockSlot} root=${blockRootHex}`); + } + return blockInput.getAllColumns(); + } + const sidecarsUnfinalized = await this.db.dataColumnSidecar.values(fromHex(blockRootHex)); + if (sidecarsUnfinalized.length > 0) { + return sidecarsUnfinalized; + } + const sidecarsFinalized = await this.db.dataColumnSidecarArchive.values(blockSlot); + return sidecarsFinalized; + } + + async getSerializedDataColumnSidecars( + blockSlot: Slot, + blockRootHex: string, + indices: number[] + ): Promise<(Uint8Array | undefined)[]> { + const blockInput = this.seenBlockInputCache.get(blockRootHex); + if (blockInput) { + if (!isBlockInputColumns(blockInput)) { + throw new Error(`Expected block input to have columns: slot=${blockSlot} root=${blockRootHex}`); + } + return indices.map((index) => { + const sidecar = blockInput.getColumn(index); + if (!sidecar) { + return undefined; + } + const serialized = this.serializedCache.get(sidecar); + if (serialized) { + return serialized; + } + return ssz.fulu.DataColumnSidecar.serialize(sidecar); + }); + } + const sidecarsUnfinalized = await this.db.dataColumnSidecar.getManyBinary(fromHex(blockRootHex), indices); + if (sidecarsUnfinalized.some((sidecar) => sidecar != null)) { + return sidecarsUnfinalized; + } + const sidecarsFinalized = await this.db.dataColumnSidecarArchive.getManyBinary(blockSlot, indices); + return sidecarsFinalized; + } + async produceCommonBlockBody(blockAttributes: BlockAttributes): Promise { - const {slot, parentBlockRoot} = blockAttributes; + const {slot, parentBlock} = blockAttributes; const state = await this.regen.getBlockSlotState( - toRootHex(parentBlockRoot), + parentBlock, slot, {dontTransferCache: true}, RegenCaller.produceBlock @@ -719,7 +891,7 @@ export class BeaconChain implements IBeaconChain { slot, feeRecipient, commonBlockBodyPromise, - parentBlockRoot, + parentBlock, }: BlockAttributes & {commonBlockBodyPromise: Promise} ): Promise<{ block: AssembledBlockType; @@ -728,7 +900,7 @@ export class BeaconChain implements IBeaconChain { shouldOverrideBuilder?: boolean; }> { const state = await this.regen.getBlockSlotState( - toRootHex(parentBlockRoot), + parentBlock, slot, {dontTransferCache: true}, RegenCaller.produceBlock @@ -745,7 +917,7 @@ export class BeaconChain implements IBeaconChain { graffiti, slot, feeRecipient, - parentBlockRoot, + parentBlock, proposerIndex, proposerPubKey, commonBlockBodyPromise, @@ -768,7 +940,7 @@ export class BeaconChain implements IBeaconChain { const block = { slot, proposerIndex, - parentRoot: parentBlockRoot, + parentRoot: fromHex(parentBlock.blockRoot), stateRoot: ZERO_HASH, body, } as AssembledBlockType; @@ -964,12 +1136,7 @@ export class BeaconChain implements IBeaconChain { // thanks to one epoch look ahead, we don't need to dial up to attEpoch const targetSlot = computeStartSlotAtEpoch(attEpoch - 1); this.metrics?.gossipAttestation.useHeadBlockStateDialedToTargetEpoch.inc({caller: regenCaller}); - state = await this.regen.getBlockSlotState( - attHeadBlock.blockRoot, - targetSlot, - {dontTransferCache: true}, - regenCaller - ); + state = await this.regen.getBlockSlotState(attHeadBlock, targetSlot, {dontTransferCache: true}, regenCaller); } else if (blockEpoch > attEpoch) { // should not happen, handled inside attestation verification code throw Error(`Block epoch ${blockEpoch} is after attestation epoch ${attEpoch}`); @@ -980,8 +1147,8 @@ export class BeaconChain implements IBeaconChain { this.metrics?.gossipAttestation.useHeadBlockState.inc({caller: regenCaller}); state = await this.regen.getState(attHeadBlock.stateRoot, regenCaller); } - - // should always be the current epoch of the active context so no need to await a result from the ShufflingCache + // resolve the promise to unblock other calls of the same epoch and dependent root + this.shufflingCache.processState(state); return state.epochCtx.getShufflingAtEpoch(attEpoch); } @@ -1098,6 +1265,8 @@ export class BeaconChain implements IBeaconChain { metrics.opPool.proposerSlashingPoolSize.set(this.opPool.proposerSlashingsSize); metrics.opPool.voluntaryExitPoolSize.set(this.opPool.voluntaryExitsSize); metrics.opPool.syncCommitteeMessagePoolSize.set(this.syncCommitteeMessagePool.size); + metrics.opPool.payloadAttestationPool.size.set(this.payloadAttestationPool.size); + metrics.opPool.executionPayloadBidPool.size.set(this.executionPayloadBidPool.size); // syncContributionAndProofPool tracks metrics on its own metrics.opPool.blsToExecutionChangePoolSize.set(this.opPool.blsToExecutionChangeSize); metrics.chain.blacklistedBlocks.set(this.blacklistedBlocks.size); @@ -1128,6 +1297,9 @@ export class BeaconChain implements IBeaconChain { this.aggregatedAttestationPool.prune(slot); this.syncCommitteeMessagePool.prune(slot); this.seenSyncCommitteeMessages.prune(slot); + this.payloadAttestationPool.prune(slot); + this.executionPayloadBidPool.prune(slot); + this.seenExecutionPayloadBids.prune(slot); this.seenAttestationDatas.onSlot(slot); this.reprocessController.onSlot(slot); @@ -1151,6 +1323,7 @@ export class BeaconChain implements IBeaconChain { this.seenAttesters.prune(epoch); this.seenAggregators.prune(epoch); + this.seenPayloadAttesters.prune(epoch); this.seenAggregatedAttestations.prune(epoch); this.seenBlockAttesters.prune(epoch); this.beaconProposerCache.prune(epoch); @@ -1165,9 +1338,18 @@ export class BeaconChain implements IBeaconChain { this.logger.verbose("Fork choice justified", {epoch: cp.epoch, root: cp.rootHex}); } + private onCheckpoint(this: BeaconChain, _checkpoint: phase0.Checkpoint, state: CachedBeaconStateAllForks): void { + // Defer to not block other checkpoint event handlers, which can cause lightclient update delays + callInNextEventLoop(() => { + this.shufflingCache.processState(state); + }); + } + private async onForkChoiceFinalized(this: BeaconChain, cp: CheckpointWithHex): Promise { this.logger.verbose("Fork choice finalized", {epoch: cp.epoch, root: cp.rootHex}); - this.seenBlockProposers.prune(computeStartSlotAtEpoch(cp.epoch)); + const finalizedSlot = computeStartSlotAtEpoch(cp.epoch); + this.seenBlockProposers.prune(finalizedSlot); + this.seenExecutionPayloadEnvelopes.prune(finalizedSlot); // Update validator custody to account for effective balance changes await this.updateValidatorsCustodyRequirement(cp); @@ -1175,13 +1357,13 @@ export class BeaconChain implements IBeaconChain { // TODO: Improve using regen here const {blockRoot, stateRoot, slot} = this.forkChoice.getHead(); const headState = this.regen.getStateSync(stateRoot); - const headBlock = await this.db.block.get(fromHex(blockRoot)); - if (headBlock == null) { - throw Error(`Head block ${slot} ${headBlock} is not available in database`); + const blockResult = await this.getBlockByRoot(blockRoot); + if (blockResult == null) { + throw Error(`Head block for ${slot} is not available in cache or database`); } if (headState) { - this.opPool.pruneAll(headBlock, headState); + this.opPool.pruneAll(blockResult.block, headState); } if (headState === null) { @@ -1295,9 +1477,9 @@ export class BeaconChain implements IBeaconChain { preState = processSlots(preState, block.slot); // Dial preState's slot to block.slot - const postState = this.regen.getStateSync(toRootHex(block.stateRoot)) ?? undefined; + const proposerRewards = this.regen.getStateSync(toRootHex(block.stateRoot))?.proposerRewards ?? undefined; - return computeBlockRewards(this.config, block, preState.clone(), postState?.clone()); + return computeBlockRewards(this.config, block, preState, proposerRewards); } async getAttestationsRewards( @@ -1338,6 +1520,6 @@ export class BeaconChain implements IBeaconChain { preState = processSlots(preState, block.slot); // Dial preState's slot to block.slot - return computeSyncCommitteeRewards(this.config, this.index2pubkey, block, preState.clone(), validatorIds); + return computeSyncCommitteeRewards(this.config, this.index2pubkey, block, preState, validatorIds); } } diff --git a/packages/beacon-node/src/chain/errors/attestationError.ts b/packages/beacon-node/src/chain/errors/attestationError.ts index 1f907be96e7c..f904721c58d0 100644 --- a/packages/beacon-node/src/chain/errors/attestationError.ts +++ b/packages/beacon-node/src/chain/errors/attestationError.ts @@ -139,6 +139,14 @@ export enum AttestationErrorCode { * Electra: Attester not in committee */ ATTESTER_NOT_IN_COMMITTEE = "ATTESTATION_ERROR_ATTESTER_NOT_IN_COMMITTEE", + /** + * Gloas: Invalid attestationData index: is non-zero and non-one + */ + INVALID_PAYLOAD_STATUS_VALUE = "ATTESTATION_ERROR_INVALID_PAYLOAD_STATUS_VALUE", + /** + * Gloas: Current slot attestation is marking payload as present + */ + PREMATURELY_INDICATED_PAYLOAD_PRESENT = "ATTESTATION_ERROR_PREMATURELY_INDICATED_PAYLOAD_PRESENT", } export type AttestationErrorType = @@ -175,7 +183,9 @@ export type AttestationErrorType = | {code: AttestationErrorCode.TOO_MANY_SKIPPED_SLOTS; headBlockSlot: Slot; attestationSlot: Slot} | {code: AttestationErrorCode.NOT_EXACTLY_ONE_COMMITTEE_BIT_SET} | {code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX} - | {code: AttestationErrorCode.ATTESTER_NOT_IN_COMMITTEE}; + | {code: AttestationErrorCode.ATTESTER_NOT_IN_COMMITTEE} + | {code: AttestationErrorCode.INVALID_PAYLOAD_STATUS_VALUE; attDataIndex: number} + | {code: AttestationErrorCode.PREMATURELY_INDICATED_PAYLOAD_PRESENT}; export class AttestationError extends GossipActionError { getMetadata(): Record { diff --git a/packages/beacon-node/src/chain/errors/executionPayloadBid.ts b/packages/beacon-node/src/chain/errors/executionPayloadBid.ts new file mode 100644 index 000000000000..5770d5efc045 --- /dev/null +++ b/packages/beacon-node/src/chain/errors/executionPayloadBid.ts @@ -0,0 +1,35 @@ +import {BuilderIndex, RootHex, Slot} from "@lodestar/types"; +import {GossipActionError} from "./gossipValidation.ts"; + +export enum ExecutionPayloadBidErrorCode { + BUILDER_NOT_ELIGIBLE = "EXECUTION_PAYLOAD_BID_ERROR_BUILDER_NOT_ELIGIBLE", + NON_ZERO_EXECUTION_PAYMENT = "EXECUTION_PAYLOAD_BID_ERROR_NON_ZERO_EXECUTION_PAYMENT", + BID_ALREADY_KNOWN = "EXECUTION_PAYLOAD_BID_ERROR_BID_ALREADY_KNOWN", + BID_TOO_LOW = "EXECUTION_PAYLOAD_BID_ERROR_BID_TOO_LOW", + BID_TOO_HIGH = "EXECUTION_PAYLOAD_BID_ERROR_BID_TOO_HIGH", + UNKNOWN_BLOCK_ROOT = "EXECUTION_PAYLOAD_BID_ERROR_UNKNOWN_BLOCK_ROOT", + INVALID_SLOT = "EXECUTION_PAYLOAD_BID_ERROR_INVALID_SLOT", + INVALID_SIGNATURE = "EXECUTION_PAYLOAD_BID_ERROR_INVALID_SIGNATURE", +} + +export type ExecutionPayloadBidErrorType = + | {code: ExecutionPayloadBidErrorCode.BUILDER_NOT_ELIGIBLE; builderIndex: BuilderIndex} + | { + code: ExecutionPayloadBidErrorCode.NON_ZERO_EXECUTION_PAYMENT; + builderIndex: BuilderIndex; + executionPayment: number; + } + | { + code: ExecutionPayloadBidErrorCode.BID_ALREADY_KNOWN; + builderIndex: BuilderIndex; + slot: Slot; + parentBlockRoot: RootHex; + parentBlockHash: RootHex; + } + | {code: ExecutionPayloadBidErrorCode.BID_TOO_LOW; bidValue: number; currentHighestBid: number} + | {code: ExecutionPayloadBidErrorCode.BID_TOO_HIGH; bidValue: number; builderBalance: number} + | {code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT; parentBlockRoot: RootHex} + | {code: ExecutionPayloadBidErrorCode.INVALID_SLOT; builderIndex: BuilderIndex; slot: Slot} + | {code: ExecutionPayloadBidErrorCode.INVALID_SIGNATURE; builderIndex: BuilderIndex; slot: Slot}; + +export class ExecutionPayloadBidError extends GossipActionError {} diff --git a/packages/beacon-node/src/chain/errors/executionPayloadEnvelope.ts b/packages/beacon-node/src/chain/errors/executionPayloadEnvelope.ts new file mode 100644 index 000000000000..051f07e04ba1 --- /dev/null +++ b/packages/beacon-node/src/chain/errors/executionPayloadEnvelope.ts @@ -0,0 +1,34 @@ +import {BuilderIndex, RootHex, Slot} from "@lodestar/types"; +import {GossipActionError} from "./gossipValidation.js"; + +export enum ExecutionPayloadEnvelopeErrorCode { + BELONG_TO_FINALIZED_BLOCK = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_BELONG_TO_FINALIZED_BLOCK", + BLOCK_ROOT_UNKNOWN = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_BLOCK_ROOT_UNKNOWN", + ENVELOPE_ALREADY_KNOWN = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_ALREADY_KNOWN", + INVALID_BLOCK = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_INVALID_BLOCK", + SLOT_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_SLOT_MISMATCH", + BUILDER_INDEX_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_BUILDER_INDEX_MISMATCH", + BLOCK_HASH_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_BLOCK_HASH_MISMATCH", + INVALID_SIGNATURE = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_INVALID_SIGNATURE", + CACHE_FAIL = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_CACHE_FAIL", +} +export type ExecutionPayloadEnvelopeErrorType = + | {code: ExecutionPayloadEnvelopeErrorCode.BELONG_TO_FINALIZED_BLOCK; envelopeSlot: Slot; finalizedSlot: Slot} + | {code: ExecutionPayloadEnvelopeErrorCode.BLOCK_ROOT_UNKNOWN; blockRoot: RootHex} + | { + code: ExecutionPayloadEnvelopeErrorCode.ENVELOPE_ALREADY_KNOWN; + blockRoot: RootHex; + slot: Slot; + } + | {code: ExecutionPayloadEnvelopeErrorCode.INVALID_BLOCK; blockRoot: RootHex} + | {code: ExecutionPayloadEnvelopeErrorCode.SLOT_MISMATCH; envelopeSlot: Slot; blockSlot: Slot} + | { + code: ExecutionPayloadEnvelopeErrorCode.BUILDER_INDEX_MISMATCH; + envelopeBuilderIndex: BuilderIndex; + bidBuilderIndex: BuilderIndex; + } + | {code: ExecutionPayloadEnvelopeErrorCode.BLOCK_HASH_MISMATCH; envelopeBlockHash: RootHex; bidBlockHash: RootHex} + | {code: ExecutionPayloadEnvelopeErrorCode.INVALID_SIGNATURE} + | {code: ExecutionPayloadEnvelopeErrorCode.CACHE_FAIL; blockRoot: RootHex}; + +export class ExecutionPayloadEnvelopeError extends GossipActionError {} diff --git a/packages/beacon-node/src/chain/errors/index.ts b/packages/beacon-node/src/chain/errors/index.ts index b4bdf1f38fbc..106d61021213 100644 --- a/packages/beacon-node/src/chain/errors/index.ts +++ b/packages/beacon-node/src/chain/errors/index.ts @@ -4,7 +4,10 @@ export * from "./blobSidecarError.js"; export * from "./blockError.js"; export * from "./blsToExecutionChangeError.js"; export * from "./dataColumnSidecarError.js"; +export * from "./executionPayloadBid.js"; +export * from "./executionPayloadEnvelope.js"; export * from "./gossipValidation.js"; +export * from "./payloadAttestation.js"; export * from "./proposerSlashingError.js"; export * from "./syncCommitteeError.js"; export * from "./voluntaryExitError.js"; diff --git a/packages/beacon-node/src/chain/errors/payloadAttestation.ts b/packages/beacon-node/src/chain/errors/payloadAttestation.ts new file mode 100644 index 000000000000..6acbed4a47f7 --- /dev/null +++ b/packages/beacon-node/src/chain/errors/payloadAttestation.ts @@ -0,0 +1,25 @@ +import {RootHex, Slot, ValidatorIndex} from "@lodestar/types"; +import {GossipActionError} from "./gossipValidation.ts"; + +export enum PayloadAttestationErrorCode { + NOT_CURRENT_SLOT = "PAYLOAD_ATTESTATION_ERROR_NOT_CURRENT_SLOT", + PAYLOAD_ATTESTATION_ALREADY_KNOWN = "PAYLOAD_ATTESTATION_ERROR_PAYLOAD_ATTESTATION_ALREADY_KNOWN", + UNKNOWN_BLOCK_ROOT = "PAYLOAD_ATTESTATION_ERROR_UNKNOWN_BLOCK_ROOT", + INVALID_BLOCK = "PAYLOAD_ATTESTATION_ERROR_INVALID_BLOCK", + INVALID_ATTESTER = "PAYLOAD_ATTESTATION_ERROR_INVALID_ATTESTER", + INVALID_SIGNATURE = "PAYLOAD_ATTESTATION_ERROR_INVALID_SIGNATURE", +} +export type PayloadAttestationErrorType = + | {code: PayloadAttestationErrorCode.NOT_CURRENT_SLOT; currentSlot: Slot; slot: Slot} + | { + code: PayloadAttestationErrorCode.PAYLOAD_ATTESTATION_ALREADY_KNOWN; + validatorIndex: ValidatorIndex; + slot: Slot; + blockRoot: RootHex; + } + | {code: PayloadAttestationErrorCode.UNKNOWN_BLOCK_ROOT; blockRoot: RootHex} + | {code: PayloadAttestationErrorCode.INVALID_BLOCK; blockRoot: RootHex} + | {code: PayloadAttestationErrorCode.INVALID_ATTESTER; attesterIndex: ValidatorIndex} + | {code: PayloadAttestationErrorCode.INVALID_SIGNATURE}; + +export class PayloadAttestationError extends GossipActionError {} diff --git a/packages/beacon-node/src/chain/forkChoice/index.ts b/packages/beacon-node/src/chain/forkChoice/index.ts index d83dc9949d7e..31548ddbf8f6 100644 --- a/packages/beacon-node/src/chain/forkChoice/index.ts +++ b/packages/beacon-node/src/chain/forkChoice/index.ts @@ -11,6 +11,7 @@ import { import {ZERO_HASH_HEX} from "@lodestar/params"; import { CachedBeaconStateAllForks, + CachedBeaconStateGloas, DataAvailabilityStatus, computeAnchorCheckpoint, computeEpochAtSlot, @@ -144,6 +145,15 @@ export function initializeForkChoiceFromFinalizedState( : {executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}), dataAvailabilityStatus: DataAvailabilityStatus.PreData, + ...(computeEpochAtSlot(blockHeader.slot) < state.config.GLOAS_FORK_EPOCH + ? { + builderIndex: undefined, + blockHashHex: undefined, + } + : { + builderIndex: (state as CachedBeaconStateGloas).latestExecutionPayloadBid.builderIndex, + blockHashHex: toRootHex((state as CachedBeaconStateGloas).latestExecutionPayloadBid.blockHash), + }), }, currentSlot ), @@ -225,6 +235,15 @@ export function initializeForkChoiceFromUnfinalizedState( : {executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}), dataAvailabilityStatus: DataAvailabilityStatus.PreData, + ...(computeEpochAtSlot(blockHeader.slot) < unfinalizedState.config.GLOAS_FORK_EPOCH + ? { + builderIndex: undefined, + blockHashHex: undefined, + } + : { + builderIndex: (unfinalizedState as CachedBeaconStateGloas).latestExecutionPayloadBid.builderIndex, + blockHashHex: toRootHex((unfinalizedState as CachedBeaconStateGloas).latestExecutionPayloadBid.blockHash), + }), }; const parentSlot = blockHeader.slot - 1; diff --git a/packages/beacon-node/src/chain/initState.ts b/packages/beacon-node/src/chain/initState.ts index 7eed602a910b..2d13f68d273d 100644 --- a/packages/beacon-node/src/chain/initState.ts +++ b/packages/beacon-node/src/chain/initState.ts @@ -2,10 +2,11 @@ import {ChainForkConfig} from "@lodestar/config"; import {ZERO_HASH} from "@lodestar/params"; import {BeaconStateAllForks, computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {SignedBeaconBlock, ssz} from "@lodestar/types"; -import {Logger, toHex, toRootHex} from "@lodestar/utils"; +import {Logger, byteArrayEquals, toHex, toRootHex} from "@lodestar/utils"; import {GENESIS_SLOT} from "../constants/index.js"; import {IBeaconDb} from "../db/index.js"; import {Metrics} from "../metrics/index.js"; +import {getStateTypeFromBytes} from "../util/multifork.js"; export async function persistAnchorState( config: ChainForkConfig, @@ -25,7 +26,7 @@ export async function persistAnchorState( const latestBlockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(latestBlockHeader); - if (Buffer.compare(blockRoot, latestBlockRoot) !== 0) { + if (!byteArrayEquals(blockRoot, latestBlockRoot)) { throw Error( `Genesis block root ${toRootHex(blockRoot)} does not match genesis state latest block root ${toRootHex(latestBlockRoot)}` ); @@ -53,14 +54,15 @@ export function createGenesisBlock(config: ChainForkConfig, genesisState: Beacon * Restore the latest beacon state from db */ export async function initStateFromDb( - _config: ChainForkConfig, + config: ChainForkConfig, db: IBeaconDb, logger: Logger ): Promise { - const state = await db.stateArchive.lastValue(); - if (!state) { + const stateBytes = await db.stateArchive.lastBinary(); + if (stateBytes == null) { throw new Error("No state exists in database"); } + const state = getStateTypeFromBytes(config, stateBytes).deserializeToViewDU(stateBytes); logger.info("Initializing beacon state from db", { slot: state.slot, diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index 93ce377bc8aa..5f5525716f68 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -22,6 +22,8 @@ import { Wei, altair, capella, + deneb, + fulu, phase0, rewards, } from "@lodestar/types"; @@ -44,7 +46,14 @@ import {ForkchoiceCaller} from "./forkChoice/index.js"; import {GetBlobsTracker} from "./GetBlobsTracker.js"; import {LightClientServer} from "./lightClient/index.js"; import {AggregatedAttestationPool} from "./opPools/aggregatedAttestationPool.js"; -import {AttestationPool, OpPool, SyncCommitteeMessagePool, SyncContributionAndProofPool} from "./opPools/index.js"; +import { + AttestationPool, + ExecutionPayloadBidPool, + OpPool, + PayloadAttestationPool, + SyncCommitteeMessagePool, + SyncContributionAndProofPool, +} from "./opPools/index.js"; import {IChainOptions} from "./options.js"; import {AssembledBlockType, BlockAttributes, BlockType, ProduceResult} from "./produceBlock/produceBlockBody.js"; import {IStateRegenerator, RegenCaller} from "./regen/index.js"; @@ -54,6 +63,9 @@ import { SeenAttesters, SeenBlockProposers, SeenContributionAndProof, + SeenExecutionPayloadBids, + SeenExecutionPayloadEnvelopes, + SeenPayloadAttesters, SeenSyncCommitteeMessages, } from "./seenCache/index.js"; import {SeenAggregatedAttestations} from "./seenCache/seenAggregateAndProof.js"; @@ -114,12 +126,17 @@ export interface IBeaconChain { readonly aggregatedAttestationPool: AggregatedAttestationPool; readonly syncCommitteeMessagePool: SyncCommitteeMessagePool; readonly syncContributionAndProofPool: SyncContributionAndProofPool; + readonly executionPayloadBidPool: ExecutionPayloadBidPool; + readonly payloadAttestationPool: PayloadAttestationPool; readonly opPool: OpPool; // Gossip seen cache readonly seenAttesters: SeenAttesters; readonly seenAggregators: SeenAggregators; + readonly seenPayloadAttesters: SeenPayloadAttesters; readonly seenAggregatedAttestations: SeenAggregatedAttestations; + readonly seenExecutionPayloadEnvelopes: SeenExecutionPayloadEnvelopes; + readonly seenExecutionPayloadBids: SeenExecutionPayloadBids; readonly seenBlockProposers: SeenBlockProposers; readonly seenSyncCommitteeMessages: SeenSyncCommitteeMessages; readonly seenContributionAndProof: SeenContributionAndProof; @@ -168,12 +185,12 @@ export interface IBeaconChain { getStateBySlot( slot: Slot, opts?: StateGetOpts - ): Promise<{state: BeaconStateAllForks; executionOptimistic: boolean; finalized: boolean} | null>; + ): Promise<{state: CachedBeaconStateAllForks; executionOptimistic: boolean; finalized: boolean} | null>; /** Returns a local state by state root */ getStateByStateRoot( stateRoot: RootHex, opts?: StateGetOpts - ): Promise<{state: BeaconStateAllForks; executionOptimistic: boolean; finalized: boolean} | null>; + ): Promise<{state: CachedBeaconStateAllForks | Uint8Array; executionOptimistic: boolean; finalized: boolean} | null>; /** Return serialized bytes of a persisted checkpoint state */ getPersistedCheckpointState(checkpoint?: phase0.Checkpoint): Promise; /** Returns a cached state by checkpoint */ @@ -193,12 +210,26 @@ export interface IBeaconChain { getCanonicalBlockAtSlot( slot: Slot ): Promise<{block: SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null>; + /** + * Get local block by root, does not fetch from the network + */ + getSerializedBlockByRoot( + root: RootHex + ): Promise<{block: Uint8Array; executionOptimistic: boolean; finalized: boolean; slot: Slot} | null>; /** * Get local block by root, does not fetch from the network */ getBlockByRoot( root: RootHex ): Promise<{block: SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null>; + getBlobSidecars(blockSlot: Slot, blockRootHex: string): Promise; + getSerializedBlobSidecars(blockSlot: Slot, blockRootHex: string): Promise; + getDataColumnSidecars(blockSlot: Slot, blockRootHex: string): Promise; + getSerializedDataColumnSidecars( + blockSlot: Slot, + blockRootHex: string, + indices: number[] + ): Promise<(Uint8Array | undefined)[]>; produceCommonBlockBody(blockAttributes: BlockAttributes): Promise; produceBlock(blockAttributes: BlockAttributes & {commonBlockBodyPromise: Promise}): Promise<{ diff --git a/packages/beacon-node/src/chain/lightClient/index.ts b/packages/beacon-node/src/chain/lightClient/index.ts index a02319e0f568..02028b0227b9 100644 --- a/packages/beacon-node/src/chain/lightClient/index.ts +++ b/packages/beacon-node/src/chain/lightClient/index.ts @@ -46,12 +46,11 @@ import { ssz, sszTypesFor, } from "@lodestar/types"; -import {Logger, MapDef, pruneSetToMax, toRootHex} from "@lodestar/utils"; +import {Logger, MapDef, byteArrayEquals, pruneSetToMax, toRootHex} from "@lodestar/utils"; import {ZERO_HASH} from "../../constants/index.js"; import {IBeaconDb} from "../../db/index.js"; import {NUM_WITNESS, NUM_WITNESS_ELECTRA} from "../../db/repositories/lightclientSyncCommitteeWitness.js"; import {Metrics} from "../../metrics/index.js"; -import {byteArrayEquals} from "../../util/bytes.js"; import {IClock} from "../../util/clock.js"; import {ChainEventEmitter} from "../emitter.js"; import {LightClientServerError, LightClientServerErrorCode} from "../errors/lightClientError.js"; diff --git a/packages/beacon-node/src/chain/lightClient/proofs.ts b/packages/beacon-node/src/chain/lightClient/proofs.ts index ae8ac61f5e3f..8636abc5ff7c 100644 --- a/packages/beacon-node/src/chain/lightClient/proofs.ts +++ b/packages/beacon-node/src/chain/lightClient/proofs.ts @@ -12,7 +12,6 @@ import {BeaconBlockBody, SSZTypesFor, ssz} from "@lodestar/types"; import {SyncCommitteeWitness} from "./types.js"; export function getSyncCommitteesWitness(fork: ForkName, state: BeaconStateAllForks): SyncCommitteeWitness { - state.commit(); const n1 = state.node; let witness: Uint8Array[]; let currentSyncCommitteeRoot: Uint8Array; @@ -71,7 +70,6 @@ export function getCurrentSyncCommitteeBranch(syncCommitteesWitness: SyncCommitt } export function getFinalizedRootProof(state: CachedBeaconStateAllForks): Uint8Array[] { - state.commit(); const finalizedRootGindex = state.epochCtx.isPostElectra() ? FINALIZED_ROOT_GINDEX_ELECTRA : FINALIZED_ROOT_GINDEX; return new Tree(state.node).getSingleProof(BigInt(finalizedRootGindex)); } diff --git a/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts b/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts index 038ef031356a..196b8d590fea 100644 --- a/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts +++ b/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts @@ -5,7 +5,6 @@ import {IForkChoice} from "@lodestar/fork-choice"; import { ForkName, ForkSeq, - MAX_ATTESTATIONS, MAX_ATTESTATIONS_ELECTRA, MAX_COMMITTEES_PER_SLOT, MIN_ATTESTATION_INCLUSION_DELAY, @@ -23,7 +22,6 @@ import { CachedBeaconStateAllForks, CachedBeaconStateAltair, CachedBeaconStateGloas, - CachedBeaconStatePhase0, EffectiveBalanceIncrements, RootCache, computeEpochAtSlot, @@ -32,21 +30,12 @@ import { getAttestationParticipationStatus, getBlockRootAtSlot, } from "@lodestar/state-transition"; -import { - Attestation, - Epoch, - RootHex, - Slot, - ValidatorIndex, - electra, - isElectraAttestation, - phase0, - ssz, -} from "@lodestar/types"; +import {Attestation, Epoch, RootHex, Slot, electra, isElectraAttestation, phase0, ssz} from "@lodestar/types"; import {MapDef, assert, toRootHex} from "@lodestar/utils"; import {Metrics} from "../../metrics/metrics.js"; import {IntersectResult, intersectUint8Arrays} from "../../util/bitArray.js"; import {getShufflingDependentRoot} from "../../util/dependentRoot.js"; +import {ShufflingCache} from "../shufflingCache.js"; import {InsertOutcome} from "./types.js"; import {pruneBySlot, signatureFromBytesNoCheck} from "./utils.js"; @@ -54,8 +43,6 @@ type DataRootHex = string; type CommitteeIndex = number; -// for pre-electra -type AttestationWithScore = {attestation: Attestation; score: number}; /** * for electra, this is to consolidate aggregated attestations of the same attestation data into a single attestation to be included in block * note that this is local definition in this file and it's NOT validator consolidation @@ -110,15 +97,6 @@ const MAX_RETAINED_ATTESTATIONS_PER_GROUP = 4; */ const MAX_RETAINED_ATTESTATIONS_PER_GROUP_ELECTRA = 8; -/** - * Pre-electra, each slot has 64 committees, and each block has 128 attestations max so in average - * we get 2 attestation per groups. - * Starting from Jan 2024, we have a performance issue getting attestations for a block. Based on the - * fact that lot of groups will have only 1 full participation attestation, increase this number - * a bit higher than average. This also help decrease number of slots to search for attestations. - */ -const MAX_ATTESTATIONS_PER_GROUP = 3; - /** * For electra, there is on chain aggregation of attestations across committees, so we can just pick up to 8 * attestations per group, sort by scores to get first 8. @@ -230,123 +208,18 @@ export class AggregatedAttestationPool { this.lowestPermissibleSlot = Math.max(clockSlot - slotsToRetain, 0); } - getAttestationsForBlock(fork: ForkName, forkChoice: IForkChoice, state: CachedBeaconStateAllForks): Attestation[] { - const forkSeq = ForkSeq[fork]; - return forkSeq >= ForkSeq.electra - ? this.getAttestationsForBlockElectra(fork, forkChoice, state) - : this.getAttestationsForBlockPreElectra(fork, forkChoice, state); - } - - /** - * Get attestations to be included in a block pre-electra. Returns up to $MAX_ATTESTATIONS items - */ - getAttestationsForBlockPreElectra( + getAttestationsForBlock( fork: ForkName, forkChoice: IForkChoice, + shufflingCache: ShufflingCache, state: CachedBeaconStateAllForks - ): phase0.Attestation[] { - const stateSlot = state.slot; - const stateEpoch = state.epochCtx.epoch; - const statePrevEpoch = stateEpoch - 1; - - const notSeenValidatorsFn = getNotSeenValidatorsFn(this.config, state); - const validateAttestationDataFn = getValidateAttestationDataFn(forkChoice, state); - - const attestationsByScore: AttestationWithScore[] = []; - - const slots = Array.from(this.attestationGroupByIndexByDataHexBySlot.keys()).sort((a, b) => b - a); - let minScore = Number.MAX_SAFE_INTEGER; - let slotCount = 0; - slot: for (const slot of slots) { - slotCount++; - const attestationGroupByIndexByDataHash = this.attestationGroupByIndexByDataHexBySlot.get(slot); - // should not happen - if (!attestationGroupByIndexByDataHash) { - throw Error(`No aggregated attestation pool for slot=${slot}`); - } - - const epoch = computeEpochAtSlot(slot); - // validateAttestation condition: Attestation target epoch not in previous or current epoch - if (!(epoch === stateEpoch || epoch === statePrevEpoch)) { - continue; // Invalid attestations - } - // validateAttestation condition: Attestation slot not within inclusion window - if ( - !( - slot + MIN_ATTESTATION_INCLUSION_DELAY <= stateSlot && - // Post deneb, attestations are valid for current and previous epoch - (ForkSeq[fork] >= ForkSeq.deneb || stateSlot <= slot + SLOTS_PER_EPOCH) - ) - ) { - continue; // Invalid attestations - } - - const inclusionDistance = stateSlot - slot; - for (const attestationGroupByIndex of attestationGroupByIndexByDataHash.values()) { - for (const [committeeIndex, attestationGroup] of attestationGroupByIndex.entries()) { - const notSeenCommitteeMembers = notSeenValidatorsFn(epoch, slot, committeeIndex); - if (notSeenCommitteeMembers === null || notSeenCommitteeMembers.size === 0) { - continue; - } - - if ( - slotCount > 2 && - attestationsByScore.length >= MAX_ATTESTATIONS && - notSeenCommitteeMembers.size / inclusionDistance < minScore - ) { - // after 2 slots, there are a good chance that we have 2 * MAX_ATTESTATIONS attestations and break the for loop early - // if not, we may have to scan all slots in the pool - // if we have enough attestations and the max possible score is lower than scores of `attestationsByScore`, we should skip - // otherwise it takes time to check attestation, add it and remove it later after the sort by score - continue; - } - - if (validateAttestationDataFn(attestationGroup.data) !== null) { - continue; - } - - // TODO: Is it necessary to validateAttestation for: - // - Attestation committee index not within current committee count - // - Attestation aggregation bits length does not match committee length - // - // These properties should not change after being validate in gossip - // IF they have to be validated, do it only with one attestation per group since same data - // The committeeCountPerSlot can be precomputed once per slot - const getAttestationsResult = attestationGroup.getAttestationsForBlock( - fork, - state.epochCtx.effectiveBalanceIncrements, - notSeenCommitteeMembers, - MAX_ATTESTATIONS_PER_GROUP - ); - for (const {attestation, newSeenEffectiveBalance} of getAttestationsResult.result) { - const score = newSeenEffectiveBalance / inclusionDistance; - if (score < minScore) { - minScore = score; - } - attestationsByScore.push({ - attestation, - score, - }); - } - - // Stop accumulating attestations there are enough that may have good scoring - if (attestationsByScore.length >= MAX_ATTESTATIONS * 2) { - break slot; - } - } - } + ): Attestation[] { + const forkSeq = ForkSeq[fork]; + if (forkSeq < ForkSeq.electra) { + throw new Error("Does not support producing blocks for pre-electra forks anymore"); } - const sortedAttestationsByScore = attestationsByScore.sort((a, b) => b.score - a.score); - const attestationsForBlock: phase0.Attestation[] = []; - for (const [i, attestationWithScore] of sortedAttestationsByScore.entries()) { - if (i >= MAX_ATTESTATIONS) { - break; - } - // attestations could be modified in this op pool, so we need to clone for block - attestationsForBlock.push(ssz.phase0.Attestation.clone(attestationWithScore.attestation)); - } - return attestationsForBlock; + return this.getAttestationsForBlockElectra(fork, forkChoice, shufflingCache, state); } /** @@ -355,6 +228,7 @@ export class AggregatedAttestationPool { getAttestationsForBlockElectra( fork: ForkName, forkChoice: IForkChoice, + shufflingCache: ShufflingCache, state: CachedBeaconStateAllForks ): electra.Attestation[] { const stateSlot = state.slot; @@ -362,7 +236,7 @@ export class AggregatedAttestationPool { const statePrevEpoch = stateEpoch - 1; const rootCache = new RootCache(state); - const notSeenValidatorsFn = getNotSeenValidatorsFn(this.config, state); + const notSeenValidatorsFn = getNotSeenValidatorsFn(this.config, shufflingCache, state); const validateAttestationDataFn = getValidateAttestationDataFn(forkChoice, state); const slots = Array.from(this.attestationGroupByIndexByDataHexBySlot.keys()).sort((a, b) => b - a); @@ -864,41 +738,14 @@ export function aggregateConsolidation({byCommittee, attData}: AttestationsConso * Pre-compute participation from a CachedBeaconStateAllForks, for use to check if an attestation's committee * has already attested or not. */ -export function getNotSeenValidatorsFn(config: BeaconConfig, state: CachedBeaconStateAllForks): GetNotSeenValidatorsFn { +export function getNotSeenValidatorsFn( + config: BeaconConfig, + shufflingCache: ShufflingCache, + state: CachedBeaconStateAllForks +): GetNotSeenValidatorsFn { const stateSlot = state.slot; if (config.getForkName(stateSlot) === ForkName.phase0) { - // Get attestations to be included in a phase0 block. - // As we are close to altair, this is not really important, it's mainly for e2e. - // The performance is not great due to the different BeaconState data structure to altair. - // check for phase0 block already - const phase0State = state as CachedBeaconStatePhase0; - const stateEpoch = computeEpochAtSlot(stateSlot); - - const previousEpochParticipants = extractParticipationPhase0( - phase0State.previousEpochAttestations.getAllReadonly(), - state - ); - const currentEpochParticipants = extractParticipationPhase0( - phase0State.currentEpochAttestations.getAllReadonly(), - state - ); - - return (epoch: Epoch, slot: Slot, committeeIndex: number) => { - const participants = - epoch === stateEpoch ? currentEpochParticipants : epoch === stateEpoch - 1 ? previousEpochParticipants : null; - if (participants === null) { - return null; - } - const committee = state.epochCtx.getBeaconCommittee(slot, committeeIndex); - - const notSeenCommitteeMembers = new Set(); - for (const [i, validatorIndex] of committee.entries()) { - if (!participants.has(validatorIndex)) { - notSeenCommitteeMembers.add(i); - } - } - return notSeenCommitteeMembers.size === 0 ? null : notSeenCommitteeMembers; - }; + throw new Error("getNotSeenValidatorsFn is not supported phase0 state"); } // altair and future forks @@ -927,7 +774,8 @@ export function getNotSeenValidatorsFn(config: BeaconConfig, state: CachedBeacon return notSeenCommitteeMembers.size === 0 ? null : notSeenCommitteeMembers; } - const committee = state.epochCtx.getBeaconCommittee(slot, committeeIndex); + const decisionRoot = state.epochCtx.getShufflingDecisionRoot(computeEpochAtSlot(slot)); + const committee = shufflingCache.getBeaconCommittee(epoch, decisionRoot, slot, committeeIndex); notSeenCommitteeMembers = new Set(); for (const [i, validatorIndex] of committee.entries()) { // no need to check flagIsTimelySource as if validator is not seen, it's participation status is 0 @@ -942,26 +790,6 @@ export function getNotSeenValidatorsFn(config: BeaconConfig, state: CachedBeacon }; } -export function extractParticipationPhase0( - attestations: phase0.PendingAttestation[], - state: CachedBeaconStateAllForks -): Set { - const {epochCtx} = state; - const allParticipants = new Set(); - for (const att of attestations) { - const aggregationBits = att.aggregationBits; - const attData = att.data; - const attSlot = attData.slot; - const committeeIndex = attData.index; - const committee = epochCtx.getBeaconCommittee(attSlot, committeeIndex); - const participants = aggregationBits.intersectValues(committee); - for (const participant of participants) { - allParticipants.add(participant); - } - } - return allParticipants; -} - /** * This returns a function to validate if an attestation data is compatible to a state. * diff --git a/packages/beacon-node/src/chain/opPools/executionPayloadBidPool.ts b/packages/beacon-node/src/chain/opPools/executionPayloadBidPool.ts new file mode 100644 index 000000000000..5ca3d66c6f36 --- /dev/null +++ b/packages/beacon-node/src/chain/opPools/executionPayloadBidPool.ts @@ -0,0 +1,77 @@ +import {Slot, gloas} from "@lodestar/types"; +import {MapDef, toRootHex} from "@lodestar/utils"; +import {InsertOutcome} from "./types.js"; +import {pruneBySlot} from "./utils.js"; + +/** + * TODO GLOAS: Revisit this value and add rational for choosing it + */ +const SLOTS_RETAINED = 2; + +type BlockRootHex = string; +type BlockHashHex = string; + +/** + * Store the best execution payload bid per slot / (parent block root, parent block hash). + */ +export class ExecutionPayloadBidPool { + private readonly bidByParentHashByParentRootBySlot = new MapDef< + Slot, + MapDef> + >(() => new MapDef>(() => new Map())); + private lowestPermissibleSlot = 0; + + get size(): number { + let count = 0; + for (const byParentRoot of this.bidByParentHashByParentRootBySlot.values()) { + for (const byParentHash of byParentRoot.values()) { + count += byParentHash.size; + } + } + return count; + } + + add(bid: gloas.ExecutionPayloadBid): InsertOutcome { + const {slot, parentBlockRoot, parentBlockHash, value} = bid; + const lowestPermissibleSlot = this.lowestPermissibleSlot; + + if (slot < lowestPermissibleSlot) { + return InsertOutcome.Old; + } + + const parentRootHex = toRootHex(parentBlockRoot); + const parentHashHex = toRootHex(parentBlockHash); + const bidByParentHash = this.bidByParentHashByParentRootBySlot.getOrDefault(slot).getOrDefault(parentRootHex); + const existing = bidByParentHash.get(parentHashHex); + + if (existing) { + const existingValue = existing.value; + const newValue = value; + if (newValue > existingValue) { + bidByParentHash.set(parentHashHex, bid); + return InsertOutcome.NewData; + } + return newValue === existingValue ? InsertOutcome.AlreadyKnown : InsertOutcome.NotBetterThan; + } + + bidByParentHash.set(parentHashHex, bid); + return InsertOutcome.NewData; + } + + /** + * Return the highest-value bid matching slot, parent block root, and parent block hash. + * Used for gossip validation and block production. + */ + getBestBid( + parentBlockRoot: BlockRootHex, + parentBlockHash: BlockHashHex, + slot: Slot + ): gloas.ExecutionPayloadBid | null { + const bidByParentHash = this.bidByParentHashByParentRootBySlot.get(slot)?.get(parentBlockRoot); + return bidByParentHash?.get(parentBlockHash) ?? null; + } + + prune(clockSlot: Slot): void { + this.lowestPermissibleSlot = pruneBySlot(this.bidByParentHashByParentRootBySlot, clockSlot, SLOTS_RETAINED); + } +} diff --git a/packages/beacon-node/src/chain/opPools/index.ts b/packages/beacon-node/src/chain/opPools/index.ts index edbcdea8410b..262fb9419856 100644 --- a/packages/beacon-node/src/chain/opPools/index.ts +++ b/packages/beacon-node/src/chain/opPools/index.ts @@ -1,5 +1,7 @@ export {AggregatedAttestationPool} from "./aggregatedAttestationPool.js"; export {AttestationPool} from "./attestationPool.js"; +export {ExecutionPayloadBidPool} from "./executionPayloadBidPool.js"; export {OpPool} from "./opPool.js"; +export {PayloadAttestationPool} from "./payloadAttestationPool.js"; export {SyncCommitteeMessagePool} from "./syncCommitteeMessagePool.js"; export {SyncContributionAndProofPool} from "./syncContributionAndProofPool.js"; diff --git a/packages/beacon-node/src/chain/opPools/opPool.ts b/packages/beacon-node/src/chain/opPools/opPool.ts index fb9900aa2032..c546f92e9de1 100644 --- a/packages/beacon-node/src/chain/opPools/opPool.ts +++ b/packages/beacon-node/src/chain/opPools/opPool.ts @@ -1,5 +1,5 @@ import {BeaconConfig} from "@lodestar/config"; -import {Id, Repository} from "@lodestar/db"; +import {DbBatch, Id, Repository} from "@lodestar/db"; import { BLS_WITHDRAWAL_PREFIX, ForkName, @@ -440,23 +440,21 @@ async function persistDiff( serializeKey: (key: K) => number | string ): Promise { const persistedKeys = await dbRepo.keys(); - const itemsToPut: {key: K; value: V}[] = []; - const keysToDelete: K[] = []; + const batch: DbBatch = []; const persistedKeysSerialized = new Set(persistedKeys.map(serializeKey)); for (const item of items) { if (!persistedKeysSerialized.has(serializeKey(item.key))) { - itemsToPut.push(item); + batch.push({type: "put", key: item.key, value: item.value}); } } const targetKeysSerialized = new Set(items.map((item) => serializeKey(item.key))); for (const persistedKey of persistedKeys) { if (!targetKeysSerialized.has(serializeKey(persistedKey))) { - keysToDelete.push(persistedKey); + batch.push({type: "del", key: persistedKey}); } } - if (itemsToPut.length > 0) await dbRepo.batchPut(itemsToPut); - if (keysToDelete.length > 0) await dbRepo.batchDelete(keysToDelete); + if (batch.length > 0) await dbRepo.batch(batch); } diff --git a/packages/beacon-node/src/chain/opPools/payloadAttestationPool.ts b/packages/beacon-node/src/chain/opPools/payloadAttestationPool.ts new file mode 100644 index 000000000000..961aab3ecb53 --- /dev/null +++ b/packages/beacon-node/src/chain/opPools/payloadAttestationPool.ts @@ -0,0 +1,157 @@ +import {Signature, aggregateSignatures} from "@chainsafe/blst"; +import {BitArray} from "@chainsafe/ssz"; +import {ChainForkConfig} from "@lodestar/config"; +import {MAX_COMMITTEES_PER_SLOT, PTC_SIZE} from "@lodestar/params"; +import {RootHex, Slot, gloas} from "@lodestar/types"; +import {MapDef, toRootHex} from "@lodestar/utils"; +import {Metrics} from "../../metrics/metrics.js"; +import {IClock} from "../../util/clock.js"; +import {InsertOutcome, OpPoolError, OpPoolErrorCode} from "./types.js"; +import {pruneBySlot, signatureFromBytesNoCheck} from "./utils.js"; + +/** + * TODO GLOAS: Revisit this value and add rational for choosing it + */ +const SLOTS_RETAINED = 2; + +/** + * The maximum number of distinct `PayloadAttestationData` that will be stored in each slot. + * + * This is a DoS protection measure. + */ +// TODO GLOAS: Revisit this value. Educated guess would be MAX_ATTESTATIONS_PER_SLOT in AttestationPool divided by MAX_COMMITTEES_PER_SLOT +const MAX_PAYLOAD_ATTESTATIONS_PER_SLOT = 16_384 / MAX_COMMITTEES_PER_SLOT; + +type DataRootHex = string; +type BlockRootHex = string; + +type AggregateFast = { + aggregationBits: BitArray; + data: gloas.PayloadAttestationData; + signature: Signature; +}; + +export class PayloadAttestationPool { + private readonly aggregateByDataRootByBlockRootBySlot = new MapDef< + Slot, + Map> + >(() => new Map>()); + private lowestPermissibleSlot = 0; + + constructor( + private readonly config: ChainForkConfig, + private readonly clock: IClock, + private readonly metrics: Metrics | null = null + ) {} + + get size(): number { + let count = 0; + for (const aggregateByDataRootByBlockRoot of this.aggregateByDataRootByBlockRootBySlot.values()) { + for (const aggregateByDataRoot of aggregateByDataRootByBlockRoot.values()) { + count += aggregateByDataRoot.size; + } + } + return count; + } + + add( + message: gloas.PayloadAttestationMessage, + payloadAttDataRootHex: RootHex, + validatorCommitteeIndex: number + ): InsertOutcome { + const slot = message.data.slot; + const lowestPermissibleSlot = this.lowestPermissibleSlot; + + if (slot < lowestPermissibleSlot) { + return InsertOutcome.Old; + } + + if (slot < this.clock.slotWithPastTolerance(this.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY / 1000)) { + return InsertOutcome.Late; + } + + const aggregateByDataRootByBlockRoot = this.aggregateByDataRootByBlockRootBySlot.getOrDefault(slot); + let aggregateByDataRoot = aggregateByDataRootByBlockRoot.get(toRootHex(message.data.beaconBlockRoot)); + + if (!aggregateByDataRoot) { + aggregateByDataRoot = new Map(); + aggregateByDataRootByBlockRoot.set(toRootHex(message.data.beaconBlockRoot), aggregateByDataRoot); + } + + if (aggregateByDataRoot.size >= MAX_PAYLOAD_ATTESTATIONS_PER_SLOT) { + throw new OpPoolError({code: OpPoolErrorCode.REACHED_MAX_PER_SLOT}); + } + + const aggregate = aggregateByDataRoot.get(payloadAttDataRootHex); + if (aggregate) { + // Aggregate msg into aggregate + return aggregateMessageInto(message, validatorCommitteeIndex, aggregate); + } + // Create a new aggregate with data + aggregateByDataRoot.set(payloadAttDataRootHex, messageToAggregate(message, validatorCommitteeIndex)); + + return InsertOutcome.NewData; + } + + /** + * Get payload attestations to be included in a block. + * Pick the top `maxAttestation` number of attestations with the most votes + */ + getPayloadAttestationsForBlock( + beaconBlockRoot: BlockRootHex, + slot: Slot, + maxAttestation: number + ): gloas.PayloadAttestation[] { + const aggregateByDataRootByBlockRoot = this.aggregateByDataRootByBlockRootBySlot.get(slot); + + if (!aggregateByDataRootByBlockRoot) { + this.metrics?.opPool.payloadAttestationPool.getPayloadAttestationsCacheMisses.inc(); + return []; + } + + const aggregateByDataRoot = aggregateByDataRootByBlockRoot.get(beaconBlockRoot); + + if (!aggregateByDataRoot) { + this.metrics?.opPool.payloadAttestationPool.getPayloadAttestationsCacheMisses.inc(); + return []; + } + + return Array.from(aggregateByDataRoot.values()) + .slice() + .sort((a, b) => b.aggregationBits.getTrueBitIndexes().length - a.aggregationBits.getTrueBitIndexes().length) + .slice(0, maxAttestation) + .map(fastToPayloadAttestation); + } + + prune(clockSlot: Slot): void { + pruneBySlot(this.aggregateByDataRootByBlockRootBySlot, clockSlot, SLOTS_RETAINED); + this.lowestPermissibleSlot = clockSlot; + } +} + +function messageToAggregate(message: gloas.PayloadAttestationMessage, validatorCommitteeIndex: number): AggregateFast { + return { + aggregationBits: BitArray.fromSingleBit(PTC_SIZE, validatorCommitteeIndex), + data: message.data, + signature: signatureFromBytesNoCheck(message.signature), + }; +} + +function aggregateMessageInto( + message: gloas.PayloadAttestationMessage, + validatorCommitteeIndex: number, + aggregate: AggregateFast +): InsertOutcome { + if (aggregate.aggregationBits.get(validatorCommitteeIndex) === true) { + return InsertOutcome.AlreadyKnown; + } + + aggregate.aggregationBits.set(validatorCommitteeIndex, true); + aggregate.signature = aggregateSignatures([aggregate.signature, signatureFromBytesNoCheck(message.signature)]); + + return InsertOutcome.Aggregated; +} + +function fastToPayloadAttestation(aggFast: AggregateFast): gloas.PayloadAttestation { + return {...aggFast, signature: aggFast.signature.toBytes()}; +} diff --git a/packages/beacon-node/src/chain/options.ts b/packages/beacon-node/src/chain/options.ts index 5d95927232e0..e25a7b86a40e 100644 --- a/packages/beacon-node/src/chain/options.ts +++ b/packages/beacon-node/src/chain/options.ts @@ -45,7 +45,6 @@ export type IChainOptions = BlockProcessOpts & broadcastValidationStrictness?: string; minSameMessageSignatureSetsToBatch: number; archiveDateEpochs?: number; - nHistoricalStates?: boolean; nHistoricalStatesFileDataStore?: boolean; }; @@ -119,7 +118,6 @@ export const defaultChainOptions: IChainOptions = { // batching too much may block the I/O thread so if useWorker=false, suggest this value to be 32 // since this batch attestation work is designed to work with useWorker=true, make this the lowest value minSameMessageSignatureSetsToBatch: 2, - nHistoricalStates: true, // as of Feb 2025, this option turned out to be very useful: // - it allows to share a persisted checkpoint state to other nodes // - users can prune the persisted checkpoint state files manually to save disc space diff --git a/packages/beacon-node/src/chain/prepareNextSlot.ts b/packages/beacon-node/src/chain/prepareNextSlot.ts index 24bbf399e859..f58fcbe48f1a 100644 --- a/packages/beacon-node/src/chain/prepareNextSlot.ts +++ b/packages/beacon-node/src/chain/prepareNextSlot.ts @@ -80,9 +80,8 @@ export class PrepareNextSlotScheduler { await sleep(this.config.getSlotComponentDurationMs(PREPARE_NEXT_SLOT_BPS), this.signal); // calling updateHead() here before we produce a block to reduce reorg possibility - const {slot: headSlot, blockRoot: headRoot} = this.chain.recomputeForkChoiceHead( - ForkchoiceCaller.prepareNextSlot - ); + const headBlock = this.chain.recomputeForkChoiceHead(ForkchoiceCaller.prepareNextSlot); + const {slot: headSlot, blockRoot: headRoot} = headBlock; // PS: previously this was comparing slots, but that gave no leway on the skipped // slots on epoch bounday. Making it more fluid. @@ -112,17 +111,12 @@ export class PrepareNextSlotScheduler { // Pre Bellatrix: we only do precompute state transition for the last slot of epoch // For Bellatrix, we always do the `processSlots()` to prepare payload for the next slot const prepareState = await this.chain.regen.getBlockSlotState( - headRoot, + headBlock, prepareSlot, // the slot 0 of next epoch will likely use this Previous Root Checkpoint state for state transition so we transfer cache here // the resulting state with cache will be cached in Checkpoint State Cache which is used for the upcoming block processing // for other slots dontTransferCached=true because we don't run state transition on this state - // - // Shuffling calculation will be done asynchronously when passing asyncShufflingCalculation=true. Shuffling will be queued in - // beforeProcessEpoch and should theoretically be ready immediately after the synchronous epoch transition finished and the - // event loop is free. In long periods of non-finality too many forks will cause the shufflingCache to throw an error for - // too many queued shufflings so only run async during normal epoch transition. See issue ChainSafe/lodestar#7244 - {dontTransferCache: !isEpochTransition, asyncShufflingCalculation: true}, + {dontTransferCache: !isEpochTransition}, RegenCaller.precomputeEpoch ); @@ -134,7 +128,8 @@ export class PrepareNextSlotScheduler { if (feeRecipient) { // If we are proposing next slot, we need to predict if we can proposer-boost-reorg or not - const {slot: proposerHeadSlot, blockRoot: proposerHeadRoot} = this.chain.predictProposerHead(clockSlot); + const proposerHead = this.chain.predictProposerHead(clockSlot); + const {slot: proposerHeadSlot, blockRoot: proposerHeadRoot} = proposerHead; // If we predict we can reorg, update prepareState with proposer head block if (proposerHeadRoot !== headRoot || proposerHeadSlot !== headSlot) { @@ -146,8 +141,9 @@ export class PrepareNextSlotScheduler { }); this.metrics?.weakHeadDetected.inc(); updatedPrepareState = (await this.chain.regen.getBlockSlotState( - proposerHeadRoot, + proposerHead, prepareSlot, + // only transfer cache if epoch transition because that's the state we will use to stateTransition() the 1st block of epoch {dontTransferCache: !isEpochTransition}, RegenCaller.predictProposerHead )) as CachedBeaconStateExecutions; diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index d6516c728497..b386065231e0 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -1,5 +1,5 @@ import {ChainForkConfig} from "@lodestar/config"; -import {getSafeExecutionBlockHash} from "@lodestar/fork-choice"; +import {ProtoBlock, getSafeExecutionBlockHash} from "@lodestar/fork-choice"; import { ForkName, ForkPostBellatrix, @@ -43,7 +43,7 @@ import { electra, fulu, } from "@lodestar/types"; -import {Logger, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils"; +import {Logger, fromHex, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils"; import {ZERO_HASH_HEX} from "../../constants/index.js"; import {numToQuantity} from "../../execution/engine/utils.js"; import { @@ -86,7 +86,7 @@ export type BlockAttributes = { randaoReveal: BLSSignature; graffiti: Bytes32; slot: Slot; - parentBlockRoot: Root; + parentBlock: ProtoBlock; feeRecipient?: string; }; @@ -155,13 +155,14 @@ export async function produceBlockBody( const { slot: blockSlot, feeRecipient: requestedFeeRecipient, - parentBlockRoot, + parentBlock, proposerIndex, proposerPubKey, commonBlockBodyPromise, } = blockAttr; let executionPayloadValue: Wei; let blockBody: AssembledBodyType; + const parentBlockRoot = fromHex(parentBlock.blockRoot); // even though shouldOverrideBuilder is relevant for the engine response, for simplicity of typing // we just return it undefined for the builder which anyway doesn't get consumed downstream let shouldOverrideBuilder: boolean | undefined; @@ -637,7 +638,7 @@ export async function produceCommonBlockBody( this: BeaconChain, blockType: T, currentState: CachedBeaconStateAllForks, - {randaoReveal, graffiti, slot, parentBlockRoot}: BlockAttributes + {randaoReveal, graffiti, slot, parentBlock}: BlockAttributes ): Promise { const stepsMetrics = blockType === BlockType.Full @@ -661,7 +662,12 @@ export async function produceCommonBlockBody( this.opPool.getSlashingsAndExits(currentState, blockType, this.metrics); const endAttestations = stepsMetrics?.startTimer(); - const attestations = this.aggregatedAttestationPool.getAttestationsForBlock(fork, this.forkChoice, currentState); + const attestations = this.aggregatedAttestationPool.getAttestationsForBlock( + fork, + this.forkChoice, + this.shufflingCache, + currentState + ); endAttestations?.({ step: BlockProductionStep.attestations, }); @@ -686,6 +692,7 @@ export async function produceCommonBlockBody( const endSyncAggregate = stepsMetrics?.startTimer(); if (ForkSeq[fork] >= ForkSeq.altair) { + const parentBlockRoot = fromHex(parentBlock.blockRoot); const previousSlot = slot - 1; const syncAggregate = this.syncContributionAndProofPool.getAggregate(previousSlot, parentBlockRoot); this.metrics?.production.producedSyncAggregateParticipants.observe( diff --git a/packages/beacon-node/src/chain/regen/interface.ts b/packages/beacon-node/src/chain/regen/interface.ts index dcb604747f4e..61b68fa55625 100644 --- a/packages/beacon-node/src/chain/regen/interface.ts +++ b/packages/beacon-node/src/chain/regen/interface.ts @@ -2,7 +2,7 @@ import {routes} from "@lodestar/api"; import {ProtoBlock} from "@lodestar/fork-choice"; import {CachedBeaconStateAllForks} from "@lodestar/state-transition"; import {BeaconBlock, Epoch, RootHex, Slot, phase0} from "@lodestar/types"; -import {CheckpointHex} from "../stateCache/index.js"; +import {CheckpointHex} from "../stateCache/types.js"; export enum RegenCaller { getDuties = "getDuties", @@ -18,6 +18,7 @@ export enum RegenCaller { validateGossipAggregateAndProof = "validateGossipAggregateAndProof", validateGossipAttestation = "validateGossipAttestation", validateGossipVoluntaryExit = "validateGossipVoluntaryExit", + validateGossipExecutionPayloadBid = "validateGossipExecutionPayloadBid", onForkChoiceFinalized = "onForkChoiceFinalized", restApi = "restApi", } @@ -26,15 +27,10 @@ export enum RegenFnName { getBlockSlotState = "getBlockSlotState", getState = "getState", getPreState = "getPreState", - getCheckpointState = "getCheckpointState", } export type StateRegenerationOpts = { dontTransferCache: boolean; - /** - * Do not queue shuffling calculation async. Forces sync JIT calculation in afterProcessEpoch if not passed as `true` - */ - asyncShufflingCalculation?: boolean; }; export interface IStateRegenerator extends IStateRegeneratorInternal { @@ -67,21 +63,11 @@ export interface IStateRegeneratorInternal { rCaller: RegenCaller ): Promise; - /** - * Return a valid checkpoint state - * This will always return a state with `state.slot % SLOTS_PER_EPOCH === 0` - */ - getCheckpointState( - cp: phase0.Checkpoint, - opts: StateRegenerationOpts, - rCaller: RegenCaller - ): Promise; - /** * Return the state of `blockRoot` processed to slot `slot` */ getBlockSlotState( - blockRoot: RootHex, + block: ProtoBlock, slot: Slot, opts: StateRegenerationOpts, rCaller: RegenCaller @@ -90,5 +76,5 @@ export interface IStateRegeneratorInternal { /** * Return the exact state with `stateRoot` */ - getState(stateRoot: RootHex, rCaller: RegenCaller, opts?: StateRegenerationOpts): Promise; + getState(stateRoot: RootHex, rCaller: RegenCaller): Promise; } diff --git a/packages/beacon-node/src/chain/regen/queued.ts b/packages/beacon-node/src/chain/regen/queued.ts index d69d6bc7e686..e99b06e78577 100644 --- a/packages/beacon-node/src/chain/regen/queued.ts +++ b/packages/beacon-node/src/chain/regen/queued.ts @@ -5,8 +5,7 @@ import {BeaconBlock, Epoch, RootHex, Slot, phase0} from "@lodestar/types"; import {Logger, toRootHex} from "@lodestar/utils"; import {Metrics} from "../../metrics/index.js"; import {JobItemQueue} from "../../util/queue/index.js"; -import {CheckpointHex, toCheckpointHex} from "../stateCache/index.js"; -import {BlockStateCache, CheckpointStateCache} from "../stateCache/types.js"; +import {BlockStateCache, CheckpointHex, CheckpointStateCache} from "../stateCache/types.js"; import {RegenError, RegenErrorCode} from "./errors.js"; import { IStateRegenerator, @@ -79,21 +78,15 @@ export class QueuedStateRegenerator implements IStateRegenerator { /** * Get a state from block state cache. - * This is not for block processing so don't transfer cache */ getStateSync(stateRoot: RootHex): CachedBeaconStateAllForks | null { - return this.blockStateCache.get(stateRoot, {dontTransferCache: true}); + return this.blockStateCache.get(stateRoot); } /** * Get state for block processing. - * By default, do not transfer cache except for the block at clock slot - * which is usually the gossip block. */ - getPreStateSync( - block: BeaconBlock, - opts: StateRegenerationOpts = {dontTransferCache: true} - ): CachedBeaconStateAllForks | null { + getPreStateSync(block: BeaconBlock): CachedBeaconStateAllForks | null { const parentRoot = toRootHex(block.parentRoot); const parentBlock = this.forkChoice.getBlockHex(parentRoot); if (!parentBlock) { @@ -108,7 +101,7 @@ export class QueuedStateRegenerator implements IStateRegenerator { // Check the checkpoint cache (if the pre-state is a checkpoint state) if (parentEpoch < blockEpoch) { - const checkpointState = this.checkpointStateCache.getLatest(parentRoot, blockEpoch, opts); + const checkpointState = this.checkpointStateCache.getLatest(parentRoot, blockEpoch); if (checkpointState && computeEpochAtSlot(checkpointState.slot) === blockEpoch) { return checkpointState; } @@ -118,7 +111,7 @@ export class QueuedStateRegenerator implements IStateRegenerator { // Otherwise the state transition may not be cached and wasted. Queue for regen since the // work required will still be significant. if (parentEpoch === blockEpoch) { - const state = this.blockStateCache.get(parentBlock.stateRoot, opts); + const state = this.blockStateCache.get(parentBlock.stateRoot); if (state) { return state; } @@ -132,21 +125,17 @@ export class QueuedStateRegenerator implements IStateRegenerator { } /** - * Get checkpoint state from cache, this function is not for block processing so don't transfer cache + * Get checkpoint state from cache */ getCheckpointStateSync(cp: CheckpointHex): CachedBeaconStateAllForks | null { - return this.checkpointStateCache.get(cp, {dontTransferCache: true}); + return this.checkpointStateCache.get(cp); } /** - * Get state closest to head, this function is not for block processing so don't transfer cache + * Get state closest to head */ getClosestHeadState(head: ProtoBlock): CachedBeaconStateAllForks | null { - const opts = {dontTransferCache: true}; - return ( - this.checkpointStateCache.getLatest(head.blockRoot, Infinity, opts) || - this.blockStateCache.get(head.stateRoot, opts) - ); + return this.checkpointStateCache.getLatest(head.blockRoot, Infinity) || this.blockStateCache.get(head.stateRoot); } pruneOnCheckpoint(finalizedEpoch: Epoch, justifiedEpoch: Epoch, headStateRoot: RootHex): void { @@ -181,10 +170,7 @@ export class QueuedStateRegenerator implements IStateRegenerator { maybeHeadStateRoot, }; const headState = - newHeadStateRoot === maybeHeadStateRoot - ? maybeHeadState - : // maybeHeadState was already in block state cache so we don't transfer the cache - this.blockStateCache.get(newHeadStateRoot, {dontTransferCache: true}); + newHeadStateRoot === maybeHeadStateRoot ? maybeHeadState : this.blockStateCache.get(newHeadStateRoot); if (headState) { this.blockStateCache.setHeadState(headState); @@ -199,9 +185,7 @@ export class QueuedStateRegenerator implements IStateRegenerator { // for the new FIFOBlockStateCache, it's important to reload state to regen head state here if needed const allowDiskReload = true; - // transfer cache here because we want to regen state asap - const cloneOpts = {dontTransferCache: false}; - this.regen.getState(newHeadStateRoot, RegenCaller.processBlock, cloneOpts, allowDiskReload).then( + this.regen.getState(newHeadStateRoot, RegenCaller.processBlock, allowDiskReload).then( (headStateRegen) => this.blockStateCache.setHeadState(headStateRegen), (e) => this.logger.error("Error on head state regen", logCtx, e) ); @@ -224,7 +208,7 @@ export class QueuedStateRegenerator implements IStateRegenerator { this.metrics?.regenFnCallTotal.inc({caller: rCaller, entrypoint: RegenFnName.getPreState}); // First attempt to fetch the state from caches before queueing - const cachedState = this.getPreStateSync(block, opts); + const cachedState = this.getPreStateSync(block); if (cachedState !== null) { return cachedState; @@ -235,24 +219,6 @@ export class QueuedStateRegenerator implements IStateRegenerator { return this.jobQueue.push({key: "getPreState", args: [block, opts, rCaller]}); } - async getCheckpointState( - cp: phase0.Checkpoint, - opts: StateRegenerationOpts, - rCaller: RegenCaller - ): Promise { - this.metrics?.regenFnCallTotal.inc({caller: rCaller, entrypoint: RegenFnName.getCheckpointState}); - - // First attempt to fetch the state from cache before queueing - const checkpointState = this.checkpointStateCache.get(toCheckpointHex(cp), opts); - if (checkpointState) { - return checkpointState; - } - - // The state is not immediately available in the caches, enqueue the job - this.metrics?.regenFnQueuedTotal.inc({caller: rCaller, entrypoint: RegenFnName.getCheckpointState}); - return this.jobQueue.push({key: "getCheckpointState", args: [cp, opts, rCaller]}); - } - /** * Get state of provided `blockRoot` and dial forward to `slot` * Use this api with care because we don't want the queue to be busy @@ -260,7 +226,7 @@ export class QueuedStateRegenerator implements IStateRegenerator { * @returns */ async getBlockSlotState( - blockRoot: RootHex, + block: ProtoBlock, slot: Slot, opts: StateRegenerationOpts, rCaller: RegenCaller @@ -268,25 +234,21 @@ export class QueuedStateRegenerator implements IStateRegenerator { this.metrics?.regenFnCallTotal.inc({caller: rCaller, entrypoint: RegenFnName.getBlockSlotState}); // The state is not immediately available in the caches, enqueue the job - return this.jobQueue.push({key: "getBlockSlotState", args: [blockRoot, slot, opts, rCaller]}); + return this.jobQueue.push({key: "getBlockSlotState", args: [block, slot, opts, rCaller]}); } - async getState( - stateRoot: RootHex, - rCaller: RegenCaller, - opts: StateRegenerationOpts = {dontTransferCache: true} - ): Promise { + async getState(stateRoot: RootHex, rCaller: RegenCaller): Promise { this.metrics?.regenFnCallTotal.inc({caller: rCaller, entrypoint: RegenFnName.getState}); // First attempt to fetch the state from cache before queueing - const state = this.blockStateCache.get(stateRoot, opts); + const state = this.blockStateCache.get(stateRoot); if (state) { return state; } // The state is not immediately available in the cache, enqueue the job this.metrics?.regenFnQueuedTotal.inc({caller: rCaller, entrypoint: RegenFnName.getState}); - return this.jobQueue.push({key: "getState", args: [stateRoot, rCaller, opts]}); + return this.jobQueue.push({key: "getState", args: [stateRoot, rCaller]}); } private jobQueueProcessor = async (regenRequest: RegenRequest): Promise => { @@ -300,8 +262,6 @@ export class QueuedStateRegenerator implements IStateRegenerator { switch (regenRequest.key) { case "getPreState": return await this.regen.getPreState(...regenRequest.args); - case "getCheckpointState": - return await this.regen.getCheckpointState(...regenRequest.args); case "getBlockSlotState": return await this.regen.getBlockSlotState(...regenRequest.args); case "getState": diff --git a/packages/beacon-node/src/chain/regen/regen.ts b/packages/beacon-node/src/chain/regen/regen.ts index df2e9098b8e7..7dfebb9af51d 100644 --- a/packages/beacon-node/src/chain/regen/regen.ts +++ b/packages/beacon-node/src/chain/regen/regen.ts @@ -11,13 +11,14 @@ import { processSlots, stateTransition, } from "@lodestar/state-transition"; -import {BeaconBlock, RootHex, SignedBeaconBlock, Slot, phase0} from "@lodestar/types"; +import {BeaconBlock, RootHex, SignedBeaconBlock, Slot} from "@lodestar/types"; import {Logger, fromHex, toRootHex} from "@lodestar/utils"; import {IBeaconDb} from "../../db/index.js"; import {Metrics} from "../../metrics/index.js"; import {nextEventLoop} from "../../util/eventLoop.js"; import {getCheckpointFromState} from "../blocks/utils/checkpoint.js"; import {ChainEvent, ChainEventEmitter} from "../emitter.js"; +import {SeenBlockInput} from "../seenCache/seenGossipBlockInput.js"; import {BlockStateCache, CheckpointStateCache} from "../stateCache/types.js"; import {ValidatorMonitor} from "../validatorMonitor.js"; import {RegenError, RegenErrorCode} from "./errors.js"; @@ -28,6 +29,7 @@ export type RegenModules = { forkChoice: IForkChoice; blockStateCache: BlockStateCache; checkpointStateCache: CheckpointStateCache; + seenBlockInputCache: SeenBlockInput; config: ChainForkConfig; emitter: ChainEventEmitter; logger: Logger; @@ -74,24 +76,11 @@ export class StateRegenerator implements IStateRegeneratorInternal { // We may have the checkpoint state with parent root inside the checkpoint state cache // through gossip validation. if (parentEpoch < blockEpoch) { - return this.getCheckpointState({root: block.parentRoot, epoch: blockEpoch}, opts, regenCaller, allowDiskReload); + return this.getBlockSlotState(parentBlock, block.slot, opts, regenCaller, allowDiskReload); } // Otherwise, get the state normally. - return this.getState(parentBlock.stateRoot, regenCaller, opts, allowDiskReload); - } - - /** - * Get state after block `cp.root` dialed forward to first slot of `cp.epoch` - */ - async getCheckpointState( - cp: phase0.Checkpoint, - opts: StateRegenerationOpts, - regenCaller: RegenCaller, - allowDiskReload = false - ): Promise { - const checkpointStartSlot = computeStartSlotAtEpoch(cp.epoch); - return this.getBlockSlotState(toRootHex(cp.root), checkpointStartSlot, opts, regenCaller, allowDiskReload); + return this.getState(parentBlock.stateRoot, regenCaller, allowDiskReload); } /** @@ -99,20 +88,12 @@ export class StateRegenerator implements IStateRegeneratorInternal { * - allowDiskReload should be used with care, as it will cause the state to be reloaded from disk */ async getBlockSlotState( - blockRoot: RootHex, + block: ProtoBlock, slot: Slot, opts: StateRegenerationOpts, regenCaller: RegenCaller, allowDiskReload = false ): Promise { - const block = this.modules.forkChoice.getBlockHex(blockRoot); - if (!block) { - throw new RegenError({ - code: RegenErrorCode.BLOCK_NOT_IN_FORKCHOICE, - blockRoot, - }); - } - if (slot < block.slot) { throw new RegenError({ code: RegenErrorCode.SLOT_BEFORE_BLOCK_SLOT, @@ -121,11 +102,12 @@ export class StateRegenerator implements IStateRegeneratorInternal { }); } + const {blockRoot} = block; const {checkpointStateCache} = this.modules; const epoch = computeEpochAtSlot(slot); const latestCheckpointStateCtx = allowDiskReload - ? await checkpointStateCache.getOrReloadLatest(blockRoot, epoch, opts) - : checkpointStateCache.getLatest(blockRoot, epoch, opts); + ? await checkpointStateCache.getOrReloadLatest(blockRoot, epoch) + : checkpointStateCache.getLatest(blockRoot, epoch); // If a checkpoint state exists with the given checkpoint root, it either is in requested epoch // or needs to have empty slots processed until the requested epoch @@ -136,7 +118,7 @@ export class StateRegenerator implements IStateRegeneratorInternal { // Otherwise, use the fork choice to get the stateRoot from block at the checkpoint root // regenerate that state, // then process empty slots until the requested epoch - const blockStateCtx = await this.getState(block.stateRoot, regenCaller, opts, allowDiskReload); + const blockStateCtx = await this.getState(block.stateRoot, regenCaller, allowDiskReload); return processSlotsByCheckpoint(this.modules, blockStateCtx, slot, regenCaller, opts); } @@ -148,23 +130,15 @@ export class StateRegenerator implements IStateRegeneratorInternal { async getState( stateRoot: RootHex, caller: RegenCaller, - opts?: StateRegenerationOpts, // internal option, don't want to expose to external caller allowDiskReload = false ): Promise { // Trivial case, state at stateRoot is already cached - const cachedStateCtx = this.modules.blockStateCache.get(stateRoot, opts); + const cachedStateCtx = this.modules.blockStateCache.get(stateRoot); if (cachedStateCtx) { return cachedStateCtx; } - // in block gossip validation (getPreState() call), dontTransferCache is specified as true because we only want to transfer cache in verifyBlocksStateTransitionOnly() - // but here we want to process blocks as fast as possible so force to transfer cache in this case - if (opts && allowDiskReload) { - // if there is no `opts` specified, it already means "false" - opts.dontTransferCache = false; - } - // Otherwise we have to use the fork choice to traverse backwards, block by block, // searching the state caches // then replay blocks forward to the desired stateRoot @@ -179,7 +153,7 @@ export class StateRegenerator implements IStateRegeneratorInternal { const getSeedStateTimer = this.modules.metrics?.regenGetState.getSeedState.startTimer({caller}); // iterateAncestorBlocks only returns ancestor blocks, not the block itself for (const b of this.modules.forkChoice.iterateAncestorBlocks(block.blockRoot)) { - state = this.modules.blockStateCache.get(b.stateRoot, opts); + state = this.modules.blockStateCache.get(b.stateRoot); if (state) { break; } @@ -187,8 +161,8 @@ export class StateRegenerator implements IStateRegeneratorInternal { if (!lastBlockToReplay) continue; const epoch = computeEpochAtSlot(lastBlockToReplay.slot - 1); state = allowDiskReload - ? await checkpointStateCache.getOrReloadLatest(b.blockRoot, epoch, opts) - : checkpointStateCache.getLatest(b.blockRoot, epoch, opts); + ? await checkpointStateCache.getOrReloadLatest(b.blockRoot, epoch) + : checkpointStateCache.getLatest(b.blockRoot, epoch); if (state) { break; } @@ -219,7 +193,10 @@ export class StateRegenerator implements IStateRegeneratorInternal { const protoBlocksAsc = blocksToReplay.reverse(); for (const [i, protoBlock] of protoBlocksAsc.entries()) { replaySlots[i] = protoBlock.slot; - blockPromises[i] = this.modules.db.block.get(fromHex(protoBlock.blockRoot)); + const blockInput = this.modules.seenBlockInputCache.get(protoBlock.blockRoot); + blockPromises[i] = blockInput?.hasBlock() + ? Promise.resolve(blockInput.getBlock()) + : this.modules.db.block.get(fromHex(protoBlock.blockRoot)); } const logCtx = {stateRoot, caller, replaySlots: replaySlots.join(",")}; @@ -255,6 +232,7 @@ export class StateRegenerator implements IStateRegeneratorInternal { try { // Only advances state trusting block's signture and hashes. // We are only running the state transition to get a specific state's data. + // stateTransition() does the clone() inside, transfer cache to make the regen faster state = stateTransition( state, block, @@ -265,6 +243,7 @@ export class StateRegenerator implements IStateRegeneratorInternal { verifyStateRoot: false, verifyProposer: false, verifySignatures: false, + dontTransferCache: false, }, this.modules ); @@ -390,8 +369,8 @@ export async function processSlotsToNearestCheckpoint( const checkpointState = postState; const cp = getCheckpointFromState(checkpointState); checkpointStateCache.add(cp, checkpointState); - // consumers should not mutate or get the transfered cache - emitter?.emit(ChainEvent.checkpoint, cp, checkpointState.clone(true)); + // consumers should not mutate state ever + emitter?.emit(ChainEvent.checkpoint, cp, checkpointState); if (count >= 1) { // in normal condition, we only process 1 epoch so never reach this diff --git a/packages/beacon-node/src/chain/seenCache/index.ts b/packages/beacon-node/src/chain/seenCache/index.ts index 2aa218fc20fb..f16ae79f7f2e 100644 --- a/packages/beacon-node/src/chain/seenCache/index.ts +++ b/packages/beacon-node/src/chain/seenCache/index.ts @@ -1,5 +1,7 @@ -export {SeenAggregators, SeenAttesters} from "./seenAttesters.js"; +export {SeenAggregators, SeenAttesters, SeenPayloadAttesters} from "./seenAttesters.js"; export {SeenBlockProposers} from "./seenBlockProposers.js"; export {SeenSyncCommitteeMessages} from "./seenCommittee.js"; export {SeenContributionAndProof} from "./seenCommitteeContribution.js"; +export {SeenExecutionPayloadBids} from "./seenExecutionPayloadBids.js"; +export {SeenExecutionPayloadEnvelopes} from "./seenExecutionPayloadEnvelope.js"; export {SeenBlockInput} from "./seenGossipBlockInput.js"; diff --git a/packages/beacon-node/src/chain/seenCache/seenAttesters.ts b/packages/beacon-node/src/chain/seenCache/seenAttesters.ts index 5a8b5078bbbb..c23088ee78a3 100644 --- a/packages/beacon-node/src/chain/seenCache/seenAttesters.ts +++ b/packages/beacon-node/src/chain/seenCache/seenAttesters.ts @@ -56,3 +56,8 @@ export class SeenAttesters { * Keeps a cache to filter aggregated attestations from the same aggregators in the same epoch */ export class SeenAggregators extends SeenAttesters {} + +/** + * Keeps a cache to filter payload attestations from the same attesters in the same epoch + */ +export class SeenPayloadAttesters extends SeenAttesters {} diff --git a/packages/beacon-node/src/chain/seenCache/seenExecutionPayloadBids.ts b/packages/beacon-node/src/chain/seenCache/seenExecutionPayloadBids.ts new file mode 100644 index 000000000000..624484e958f7 --- /dev/null +++ b/packages/beacon-node/src/chain/seenCache/seenExecutionPayloadBids.ts @@ -0,0 +1,35 @@ +import {BuilderIndex, Slot} from "@lodestar/types"; +import {MapDef} from "@lodestar/utils"; + +/** + * TODO GLOAS: Revisit this value and add rational for choosing it + */ +const SLOTS_RETAINED = 2; + +/** + * Tracks execution payload bids we've already seen per (slot, builder). + */ +export class SeenExecutionPayloadBids { + private readonly builderIndexesBySlot = new MapDef>(() => new Set()); + private lowestPermissibleSlot: Slot = 0; + + isKnown(slot: Slot, builderIndex: BuilderIndex): boolean { + return this.builderIndexesBySlot.get(slot)?.has(builderIndex) === true; + } + + add(slot: Slot, builderIndex: BuilderIndex): void { + if (slot < this.lowestPermissibleSlot) { + throw Error(`slot ${slot} < lowestPermissibleSlot ${this.lowestPermissibleSlot}`); + } + this.builderIndexesBySlot.getOrDefault(slot).add(builderIndex); + } + + prune(currentSlot: Slot): void { + this.lowestPermissibleSlot = Math.max(currentSlot - SLOTS_RETAINED, 0); + for (const slot of this.builderIndexesBySlot.keys()) { + if (slot < this.lowestPermissibleSlot) { + this.builderIndexesBySlot.delete(slot); + } + } + } +} diff --git a/packages/beacon-node/src/chain/seenCache/seenExecutionPayloadEnvelope.ts b/packages/beacon-node/src/chain/seenCache/seenExecutionPayloadEnvelope.ts new file mode 100644 index 000000000000..cbd389d29449 --- /dev/null +++ b/packages/beacon-node/src/chain/seenCache/seenExecutionPayloadEnvelope.ts @@ -0,0 +1,34 @@ +import {RootHex, Slot} from "@lodestar/types"; + +/** + * Cache to prevent processing multiple execution payload envelopes for the same block root. + * Only one builder qualifies to submit an execution payload for a given slot. + * We only keep track of envelopes of unfinalized slots. + * [IGNORE] The node has not seen another valid `SignedExecutionPayloadEnvelope` for this block root. + */ +export class SeenExecutionPayloadEnvelopes { + private readonly slotByBlockRoot = new Map(); + private finalizedSlot: Slot = 0; + + isKnown(blockRoot: RootHex): boolean { + return this.slotByBlockRoot.has(blockRoot); + } + + add(blockRoot: RootHex, slot: Slot): void { + if (slot < this.finalizedSlot) { + throw Error(`slot ${slot} < finalizedSlot ${this.finalizedSlot}`); + } + + this.slotByBlockRoot.set(blockRoot, slot); + } + + prune(finalizedSlot: Slot): void { + this.finalizedSlot = finalizedSlot; + + for (const [blockRoot, slot] of this.slotByBlockRoot.entries()) { + if (slot < finalizedSlot) { + this.slotByBlockRoot.delete(blockRoot); + } + } + } +} diff --git a/packages/beacon-node/src/chain/seenCache/seenGossipBlockInput.ts b/packages/beacon-node/src/chain/seenCache/seenGossipBlockInput.ts index 5cb29e8525cd..bce9c76e46a2 100644 --- a/packages/beacon-node/src/chain/seenCache/seenGossipBlockInput.ts +++ b/packages/beacon-node/src/chain/seenCache/seenGossipBlockInput.ts @@ -3,7 +3,7 @@ import {CheckpointWithHex} from "@lodestar/fork-choice"; import {ForkName, ForkPostFulu, ForkPreGloas, isForkPostDeneb, isForkPostFulu, isForkPostGloas} from "@lodestar/params"; import {computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {BLSSignature, RootHex, SignedBeaconBlock, Slot, deneb, fulu} from "@lodestar/types"; -import {LodestarError, Logger, pruneSetToMax} from "@lodestar/utils"; +import {LodestarError, Logger, byteArrayEquals, pruneSetToMax} from "@lodestar/utils"; import {Metrics} from "../../metrics/metrics.js"; import {IClock} from "../../util/clock.js"; import {CustodyConfig} from "../../util/dataColumns.js"; @@ -344,7 +344,7 @@ export class SeenBlockInput { return false; } // Only consider verified if the signature matches - return Buffer.compare(cachedSignature, signature) === 0; + return byteArrayEquals(cachedSignature, signature); } /** @@ -379,7 +379,7 @@ export class SeenBlockInput { let itemsToDelete = this.blockInputs.size - MAX_BLOCK_INPUT_CACHE_SIZE; if (itemsToDelete > 0) { - const sorted = [...this.blockInputs.entries()].sort((a, b) => b[1].slot - a[1].slot); + const sorted = [...this.blockInputs.entries()].sort((a, b) => a[1].slot - b[1].slot); for (const [rootHex] of sorted) { this.blockInputs.delete(rootHex); itemsToDelete--; diff --git a/packages/beacon-node/src/chain/serializeState.ts b/packages/beacon-node/src/chain/serializeState.ts index c6e796cd614c..d696def3df51 100644 --- a/packages/beacon-node/src/chain/serializeState.ts +++ b/packages/beacon-node/src/chain/serializeState.ts @@ -20,7 +20,8 @@ export async function serializeState( stateBytes = bufferWithKey.buffer; const dataView = new DataView(stateBytes.buffer, stateBytes.byteOffset, stateBytes.byteLength); state.serializeToBytes({uint8Array: stateBytes, dataView}, 0); - return processFn(stateBytes); + // Await to ensure buffer is not released back to pool until processFn completes + return await processFn(stateBytes); } // release the buffer back to the pool automatically } diff --git a/packages/beacon-node/src/chain/shufflingCache.ts b/packages/beacon-node/src/chain/shufflingCache.ts index bdedddacf6db..a8666c7bc2cb 100644 --- a/packages/beacon-node/src/chain/shufflingCache.ts +++ b/packages/beacon-node/src/chain/shufflingCache.ts @@ -1,12 +1,12 @@ +import {ForkSeq} from "@lodestar/params"; import { - BeaconStateAllForks, + CachedBeaconStateAllForks, EpochShuffling, - IShufflingCache, - ShufflingBuildProps, - computeEpochShuffling, - computeEpochShufflingAsync, + getAttestingIndices, + getBeaconCommittees, + getIndexedAttestation, } from "@lodestar/state-transition"; -import {Epoch, RootHex} from "@lodestar/types"; +import {Attestation, CommitteeIndex, Epoch, IndexedAttestation, RootHex, Slot} from "@lodestar/types"; import {LodestarError, Logger, MapDef, pruneSetToMax} from "@lodestar/utils"; import {Metrics} from "../metrics/metrics.js"; @@ -53,7 +53,7 @@ export type ShufflingCacheOpts = { * - if a shuffling is not available (which does not happen with default chain option of maxSkipSlots = 32), track a promise to make sure we don't compute the same shuffling twice * - skip computing shuffling when loading state bytes from disk */ -export class ShufflingCache implements IShufflingCache { +export class ShufflingCache { /** LRU cache implemented as a map, pruned every time we add an item */ private readonly itemsByDecisionRootByEpoch: MapDef> = new MapDef( () => new Map() @@ -136,60 +136,76 @@ export class ShufflingCache implements IShufflingCache { } /** - * Gets a cached shuffling via the epoch and decision root. If the shuffling is not - * available it will build it synchronously and return the shuffling. - * - * NOTE: If a shuffling is already queued and not calculated it will build and resolve - * the promise but the already queued build will happen at some later time + * Get a shuffling synchronously, return null if not present. + * The only time we have a promise cache item is when we regen shuffling for attestation, which never happens + * with default chain option. */ - getSync( - epoch: Epoch, - decisionRoot: RootHex, - buildProps?: T - ): T extends ShufflingBuildProps ? EpochShuffling : EpochShuffling | null { + getSync(epoch: Epoch, decisionRoot: RootHex): EpochShuffling | null { const cacheItem = this.itemsByDecisionRootByEpoch.getOrDefault(epoch).get(decisionRoot); - if (!cacheItem) { + if (cacheItem === undefined) { this.metrics?.shufflingCache.miss.inc(); - } else if (isShufflingCacheItem(cacheItem)) { + return null; + } + + if (isShufflingCacheItem(cacheItem)) { this.metrics?.shufflingCache.hit.inc(); return cacheItem.shuffling; - } else if (buildProps) { - // TODO: (@matthewkeil) This should possible log a warning?? - this.metrics?.shufflingCache.shufflingPromiseNotResolvedAndThrownAway.inc(); - } else { - this.metrics?.shufflingCache.shufflingPromiseNotResolved.inc(); } - let shuffling: EpochShuffling | null = null; - if (buildProps) { - const timer = this.metrics?.shufflingCache.shufflingCalculationTime.startTimer({source: "getSync"}); - shuffling = computeEpochShuffling(buildProps.state, buildProps.activeIndices, epoch); - timer?.(); - this.set(shuffling, decisionRoot); - } - return shuffling as T extends ShufflingBuildProps ? EpochShuffling : EpochShuffling | null; + return null; } /** - * Queue asynchronous build for an EpochShuffling, triggered from state-transition + * Process a state to extract and cache all shufflings (previous, current, next). + * Uses the stored decision roots from epochCtx. */ - build(epoch: number, decisionRoot: string, state: BeaconStateAllForks, activeIndices: Uint32Array): void { - this.insertPromise(epoch, decisionRoot); - /** - * TODO: (@matthewkeil) This will get replaced by a proper build queue and a worker to do calculations - * on a NICE thread - */ - const timer = this.metrics?.shufflingCache.shufflingCalculationTime.startTimer({source: "build"}); - computeEpochShufflingAsync(state, activeIndices, epoch) - .then((shuffling) => { - this.set(shuffling, decisionRoot); - }) - .catch((err) => - this.logger?.error(`error building shuffling for epoch ${epoch} at decisionRoot ${decisionRoot}`, {}, err) - ) - .finally(() => { - timer?.(); + processState(state: CachedBeaconStateAllForks): void { + const {epochCtx} = state; + + // Cache previous shuffling + this.set(epochCtx.previousShuffling, epochCtx.previousDecisionRoot); + + // Cache current shuffling + this.set(epochCtx.currentShuffling, epochCtx.currentDecisionRoot); + + // Cache next shuffling + this.set(epochCtx.nextShuffling, epochCtx.nextDecisionRoot); + } + + getIndexedAttestation( + epoch: number, + decisionRoot: string, + fork: ForkSeq, + attestation: Attestation + ): IndexedAttestation { + const shuffling = this.getShufflingOrThrow(epoch, decisionRoot); + return getIndexedAttestation(shuffling, fork, attestation); + } + + getAttestingIndices(epoch: number, decisionRoot: string, fork: ForkSeq, attestation: Attestation): number[] { + const shuffling = this.getShufflingOrThrow(epoch, decisionRoot); + return getAttestingIndices(shuffling, fork, attestation); + } + + getBeaconCommittee(epoch: number, decisionRoot: string, slot: Slot, index: CommitteeIndex): Uint32Array { + return this.getBeaconCommittees(epoch, decisionRoot, slot, [index])[0]; + } + + getBeaconCommittees(epoch: number, decisionRoot: string, slot: Slot, indices: CommitteeIndex[]): Uint32Array[] { + const shuffling = this.getShufflingOrThrow(epoch, decisionRoot); + return getBeaconCommittees(shuffling, slot, indices); + } + + private getShufflingOrThrow(epoch: number, decisionRoot: string): EpochShuffling { + const shuffling = this.getSync(epoch, decisionRoot); + if (shuffling === null) { + throw new ShufflingCacheError({ + code: ShufflingCacheErrorCode.NO_SHUFFLING_FOUND, + epoch, + decisionRoot, }); + } + return shuffling; } /** @@ -207,7 +223,8 @@ export class ShufflingCache implements IShufflingCache { (Date.now() - cacheItem.timeInsertedMs) / 1000 ); } else { - this.metrics?.shufflingCache.shufflingBuiltMultipleTimes.inc(); + this.metrics?.shufflingCache.shufflingSetMultipleTimes.inc(); + return; } } // set the shuffling diff --git a/packages/beacon-node/src/chain/stateCache/blockStateCacheImpl.ts b/packages/beacon-node/src/chain/stateCache/blockStateCacheImpl.ts deleted file mode 100644 index 7d87675b7bbc..000000000000 --- a/packages/beacon-node/src/chain/stateCache/blockStateCacheImpl.ts +++ /dev/null @@ -1,150 +0,0 @@ -import {routes} from "@lodestar/api"; -import {CachedBeaconStateAllForks} from "@lodestar/state-transition"; -import {Epoch, RootHex} from "@lodestar/types"; -import {toRootHex} from "@lodestar/utils"; -import {Metrics} from "../../metrics/index.js"; -import {StateRegenerationOpts} from "../regen/interface.js"; -import {MapTracker} from "./mapMetrics.js"; -import {BlockStateCache} from "./types.js"; - -const MAX_STATES = 3 * 32; - -/** - * Old implementation of StateCache (used to call `StateContextCache`) - * - Prune per checkpoint so number of states ranges from 96 to 128 - * - Keep a separate head state to make sure it is always available - */ -export class BlockStateCacheImpl implements BlockStateCache { - /** - * Max number of states allowed in the cache - */ - readonly maxStates: number; - - private readonly cache: MapTracker; - /** Epoch -> Set */ - private readonly epochIndex = new Map>(); - private readonly metrics: Metrics["stateCache"] | null | undefined; - /** - * Strong reference to prevent head state from being pruned. - * null if head state is being regen and not available at the moment. - */ - private head: {state: CachedBeaconStateAllForks; stateRoot: RootHex} | null = null; - - constructor({maxStates = MAX_STATES, metrics}: {maxStates?: number; metrics?: Metrics | null}) { - this.maxStates = maxStates; - this.cache = new MapTracker(metrics?.stateCache); - if (metrics) { - this.metrics = metrics.stateCache; - metrics.stateCache.size.addCollect(() => metrics.stateCache.size.set(this.cache.size)); - } - } - - get(rootHex: RootHex, opts?: StateRegenerationOpts): CachedBeaconStateAllForks | null { - this.metrics?.lookups.inc(); - const item = this.head?.stateRoot === rootHex ? this.head.state : this.cache.get(rootHex); - if (!item) { - return null; - } - - this.metrics?.hits.inc(); - this.metrics?.stateClonedCount.observe(item.clonedCount); - - return item.clone(opts?.dontTransferCache); - } - - add(item: CachedBeaconStateAllForks): void { - const key = toRootHex(item.hashTreeRoot()); - if (this.cache.get(key)) { - return; - } - this.metrics?.adds.inc(); - this.cache.set(key, item); - const epoch = item.epochCtx.epoch; - const blockRoots = this.epochIndex.get(epoch); - if (blockRoots) { - blockRoots.add(key); - } else { - this.epochIndex.set(epoch, new Set([key])); - } - } - - setHeadState(item: CachedBeaconStateAllForks | null): void { - if (item) { - const key = toRootHex(item.hashTreeRoot()); - this.head = {state: item, stateRoot: key}; - } else { - this.head = null; - } - } - - /** - * Get a seed state for state reload. - * This is to conform to the api only as this cache is not used in n-historical state. - * See ./fifoBlockStateCache.ts for implementation - */ - getSeedState(): CachedBeaconStateAllForks { - throw Error("Not implemented for BlockStateCacheImpl"); - } - - clear(): void { - this.cache.clear(); - this.epochIndex.clear(); - } - - get size(): number { - return this.cache.size; - } - - /** - * TODO make this more robust. - * Without more thought, this currently breaks our assumptions about recent state availablity - */ - prune(headStateRootHex: RootHex): void { - const keys = Array.from(this.cache.keys()); - if (keys.length > this.maxStates) { - // object keys are stored in insertion order, delete keys starting from the front - for (const key of keys.slice(0, keys.length - this.maxStates)) { - if (key !== headStateRootHex) { - const item = this.cache.get(key); - if (item) { - this.epochIndex.get(item.epochCtx.epoch)?.delete(key); - this.cache.delete(key); - } - } - } - } - } - - /** - * Prune per finalized epoch. - */ - deleteAllBeforeEpoch(finalizedEpoch: Epoch): void { - for (const epoch of this.epochIndex.keys()) { - if (epoch < finalizedEpoch) { - this.deleteAllEpochItems(epoch); - } - } - } - - /** ONLY FOR DEBUGGING PURPOSES. For lodestar debug API */ - dumpSummary(): routes.lodestar.StateCacheItem[] { - return Array.from(this.cache.entries()).map(([key, state]) => ({ - slot: state.slot, - root: toRootHex(state.hashTreeRoot()), - reads: this.cache.readCount.get(key) ?? 0, - lastRead: this.cache.lastRead.get(key) ?? 0, - checkpointState: false, - })); - } - - getStates(): IterableIterator { - return this.cache.values(); - } - - private deleteAllEpochItems(epoch: Epoch): void { - for (const rootHex of this.epochIndex.get(epoch) || []) { - this.cache.delete(rootHex); - } - this.epochIndex.delete(epoch); - } -} diff --git a/packages/beacon-node/src/chain/stateCache/fifoBlockStateCache.ts b/packages/beacon-node/src/chain/stateCache/fifoBlockStateCache.ts index a119efe66887..37af369e28fd 100644 --- a/packages/beacon-node/src/chain/stateCache/fifoBlockStateCache.ts +++ b/packages/beacon-node/src/chain/stateCache/fifoBlockStateCache.ts @@ -4,7 +4,6 @@ import {RootHex} from "@lodestar/types"; import {toRootHex} from "@lodestar/utils"; import {Metrics} from "../../metrics/index.js"; import {LinkedList} from "../../util/array.js"; -import {StateRegenerationOpts} from "../regen/interface.js"; import {MapTracker} from "./mapMetrics.js"; import {BlockStateCache} from "./types.js"; @@ -86,14 +85,14 @@ export class FIFOBlockStateCache implements BlockStateCache { } const firstState = firstValue.value; - // don't transfer cache because consumer only use this cache to reload another state from disc - return firstState.clone(true); + // consumers should not mutate the returned state + return firstState; } /** * Get a state from this cache given a state root hex. */ - get(rootHex: RootHex, opts?: StateRegenerationOpts): CachedBeaconStateAllForks | null { + get(rootHex: RootHex): CachedBeaconStateAllForks | null { this.metrics?.lookups.inc(); const item = this.cache.get(rootHex); if (!item) { @@ -103,7 +102,7 @@ export class FIFOBlockStateCache implements BlockStateCache { this.metrics?.hits.inc(); this.metrics?.stateClonedCount.observe(item.clonedCount); - return item.clone(opts?.dontTransferCache); + return item; } /** diff --git a/packages/beacon-node/src/chain/stateCache/inMemoryCheckpointsCache.ts b/packages/beacon-node/src/chain/stateCache/inMemoryCheckpointsCache.ts deleted file mode 100644 index 81562d669365..000000000000 --- a/packages/beacon-node/src/chain/stateCache/inMemoryCheckpointsCache.ts +++ /dev/null @@ -1,198 +0,0 @@ -import {routes} from "@lodestar/api"; -import {CachedBeaconStateAllForks} from "@lodestar/state-transition"; -import {Epoch, RootHex, phase0} from "@lodestar/types"; -import {MapDef, toRootHex} from "@lodestar/utils"; -import {Metrics} from "../../metrics/index.js"; -import {StateRegenerationOpts} from "../regen/interface.js"; -import {MapTracker} from "./mapMetrics.js"; -import {CacheItemType, CheckpointStateCache} from "./types.js"; - -export type CheckpointHex = {epoch: Epoch; rootHex: RootHex}; -const MAX_EPOCHS = 10; - -/** - * In memory cache of CachedBeaconState - * belonging to checkpoint - * - * Similar API to Repository - */ -export class InMemoryCheckpointStateCache implements CheckpointStateCache { - private readonly cache: MapTracker; - /** Epoch -> Set */ - private readonly epochIndex = new MapDef>(() => new Set()); - /** - * Max number of epochs allowed in the cache - */ - private readonly maxEpochs: number; - private readonly metrics: Metrics["cpStateCache"] | null | undefined; - private preComputedCheckpoint: string | null = null; - private preComputedCheckpointHits: number | null = null; - - constructor({metrics = null}: {metrics?: Metrics | null}, {maxEpochs = MAX_EPOCHS}: {maxEpochs?: number} = {}) { - this.cache = new MapTracker(metrics?.cpStateCache); - if (metrics) { - this.metrics = metrics.cpStateCache; - metrics.cpStateCache.size.addCollect(() => - metrics.cpStateCache.size.set({type: CacheItemType.inMemory}, this.cache.size) - ); - metrics.cpStateCache.epochSize.addCollect(() => - metrics.cpStateCache.epochSize.set({type: CacheItemType.inMemory}, this.epochIndex.size) - ); - } - this.maxEpochs = maxEpochs; - } - - async getOrReload(cp: CheckpointHex, opts?: StateRegenerationOpts): Promise { - return this.get(cp, opts); - } - - async getStateOrBytes(cp: CheckpointHex): Promise { - // no need to transfer cache for this api - return this.get(cp, {dontTransferCache: true}); - } - - async getOrReloadLatest( - rootHex: string, - maxEpoch: number, - opts?: StateRegenerationOpts - ): Promise { - return this.getLatest(rootHex, maxEpoch, opts); - } - - async processState(): Promise { - // do nothing, this class does not support prunning - return 0; - } - - get(cp: CheckpointHex, opts?: StateRegenerationOpts): CachedBeaconStateAllForks | null { - this.metrics?.lookups.inc(); - const cpKey = toCheckpointKey(cp); - const item = this.cache.get(cpKey); - - if (!item) { - return null; - } - - this.metrics?.hits.inc(); - - if (cpKey === this.preComputedCheckpoint) { - this.preComputedCheckpointHits = (this.preComputedCheckpointHits ?? 0) + 1; - } - - this.metrics?.stateClonedCount.observe(item.clonedCount); - - return item.clone(opts?.dontTransferCache); - } - - add(cp: phase0.Checkpoint, item: CachedBeaconStateAllForks): void { - const cpHex = toCheckpointHex(cp); - const key = toCheckpointKey(cpHex); - if (this.cache.has(key)) { - return; - } - this.metrics?.adds.inc(); - this.cache.set(key, item); - this.epochIndex.getOrDefault(cp.epoch).add(cpHex.rootHex); - } - - /** - * Searches for the latest cached state with a `root`, starting with `epoch` and descending - */ - getLatest(rootHex: RootHex, maxEpoch: Epoch, opts?: StateRegenerationOpts): CachedBeaconStateAllForks | null { - // sort epochs in descending order, only consider epochs lte `epoch` - const epochs = Array.from(this.epochIndex.keys()) - .sort((a, b) => b - a) - .filter((e) => e <= maxEpoch); - for (const epoch of epochs) { - if (this.epochIndex.get(epoch)?.has(rootHex)) { - return this.get({rootHex, epoch}, opts); - } - } - return null; - } - - /** - * Update the precomputed checkpoint and return the number of his for the - * previous one (if any). - */ - updatePreComputedCheckpoint(rootHex: RootHex, epoch: Epoch): number | null { - const previousHits = this.preComputedCheckpointHits; - this.preComputedCheckpoint = toCheckpointKey({rootHex, epoch}); - this.preComputedCheckpointHits = 0; - return previousHits; - } - - pruneFinalized(finalizedEpoch: Epoch): void { - for (const epoch of this.epochIndex.keys()) { - if (epoch < finalizedEpoch) { - this.deleteAllEpochItems(epoch); - } - } - } - - prune(finalizedEpoch: Epoch, justifiedEpoch: Epoch): void { - const epochs = Array.from(this.epochIndex.keys()).filter( - (epoch) => epoch !== finalizedEpoch && epoch !== justifiedEpoch - ); - if (epochs.length > this.maxEpochs) { - for (const epoch of epochs.slice(0, epochs.length - this.maxEpochs)) { - this.deleteAllEpochItems(epoch); - } - } - } - - delete(cp: phase0.Checkpoint): void { - this.cache.delete(toCheckpointKey(toCheckpointHex(cp))); - const epochKey = toRootHex(cp.root); - const value = this.epochIndex.get(cp.epoch); - if (value) { - value.delete(epochKey); - if (value.size === 0) { - this.epochIndex.delete(cp.epoch); - } - } - } - - deleteAllEpochItems(epoch: Epoch): void { - for (const rootHex of this.epochIndex.get(epoch) || []) { - this.cache.delete(toCheckpointKey({rootHex, epoch})); - } - this.epochIndex.delete(epoch); - } - - clear(): void { - this.cache.clear(); - this.epochIndex.clear(); - } - - /** ONLY FOR DEBUGGING PURPOSES. For lodestar debug API */ - dumpSummary(): routes.lodestar.StateCacheItem[] { - return Array.from(this.cache.entries()).map(([key, state]) => ({ - slot: state.slot, - root: toRootHex(state.hashTreeRoot()), - reads: this.cache.readCount.get(key) ?? 0, - lastRead: this.cache.lastRead.get(key) ?? 0, - checkpointState: true, - })); - } - - getStates(): IterableIterator { - return this.cache.values(); - } - - /** ONLY FOR DEBUGGING PURPOSES. For spec tests on error */ - dumpCheckpointKeys(): string[] { - return Array.from(this.cache.keys()); - } -} - -export function toCheckpointHex(checkpoint: phase0.Checkpoint): CheckpointHex { - return { - epoch: checkpoint.epoch, - rootHex: toRootHex(checkpoint.root), - }; -} - -export function toCheckpointKey(cp: CheckpointHex): string { - return `${cp.rootHex}:${cp.epoch}`; -} diff --git a/packages/beacon-node/src/chain/stateCache/index.ts b/packages/beacon-node/src/chain/stateCache/index.ts index 0eedc6b6ff52..b866cd6f5a1e 100644 --- a/packages/beacon-node/src/chain/stateCache/index.ts +++ b/packages/beacon-node/src/chain/stateCache/index.ts @@ -1,3 +1 @@ -export * from "./blockStateCacheImpl.js"; export * from "./fifoBlockStateCache.js"; -export * from "./inMemoryCheckpointsCache.js"; diff --git a/packages/beacon-node/src/chain/stateCache/persistentCheckpointsCache.ts b/packages/beacon-node/src/chain/stateCache/persistentCheckpointsCache.ts index 86afbbf0fedb..862b063711f3 100644 --- a/packages/beacon-node/src/chain/stateCache/persistentCheckpointsCache.ts +++ b/packages/beacon-node/src/chain/stateCache/persistentCheckpointsCache.ts @@ -11,7 +11,6 @@ import {Logger, MapDef, fromHex, sleep, toHex, toRootHex} from "@lodestar/utils" import {Metrics} from "../../metrics/index.js"; import {AllocSource, BufferPool, BufferWithKey} from "../../util/bufferPool.js"; import {IClock} from "../../util/clock.js"; -import {StateRegenerationOpts} from "../regen/interface.js"; import {serializeState} from "../serializeState.js"; import {CPStateDatastore, DatastoreKey} from "./datastore/index.js"; import {MapTracker} from "./mapMetrics.js"; @@ -32,7 +31,7 @@ type PersistentCheckpointStateCacheModules = { signal?: AbortSignal; datastore: CPStateDatastore; blockStateCache: BlockStateCache; - bufferPool?: BufferPool | null; + bufferPool?: BufferPool; }; /** checkpoint serialized as a string */ @@ -120,7 +119,7 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache { private readonly maxEpochsOnDisk: number; private readonly datastore: CPStateDatastore; private readonly blockStateCache: BlockStateCache; - private readonly bufferPool?: BufferPool | null; + private readonly bufferPool?: BufferPool; constructor( { @@ -204,10 +203,10 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache { * - Get block for processing * - Regen head state */ - async getOrReload(cp: CheckpointHex, opts?: StateRegenerationOpts): Promise { - const stateOrStateBytesData = await this.getStateOrLoadDb(cp, opts); + async getOrReload(cp: CheckpointHex): Promise { + const stateOrStateBytesData = await this.getStateOrLoadDb(cp); if (stateOrStateBytesData === null || isCachedBeaconState(stateOrStateBytesData)) { - return stateOrStateBytesData?.clone(opts?.dontTransferCache) ?? null; + return stateOrStateBytesData ?? null; } const {persistedKey, stateBytes} = stateOrStateBytesData; const logMeta = {persistedKey: toHex(persistedKey)}; @@ -233,7 +232,8 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache { sszTimer?.(); const timer = this.metrics?.cpStateCache.stateReloadDuration.startTimer(); const newCachedState = loadCachedBeaconState(seedState, stateBytes, {}, validatorsBytes); - newCachedState.commit(); + // hashTreeRoot() calls the commit() inside + // there is no modification inside the state, it's just that we want to compute and cache all roots const stateRoot = toRootHex(newCachedState.hashTreeRoot()); timer?.(); @@ -252,7 +252,7 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache { this.cache.set(cpKey, {type: CacheItemType.inMemory, state: newCachedState, persistedKey}); this.epochIndex.getOrDefault(cp.epoch).add(cp.rootHex); // don't prune from memory here, call it at the last 1/3 of slot 0 of an epoch - return newCachedState.clone(opts?.dontTransferCache); + return newCachedState; } catch (e) { this.logger.debug("Reload: error loading cached state", logMeta, e as Error); return null; @@ -263,8 +263,7 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache { * Return either state or state bytes loaded from db. */ async getStateOrBytes(cp: CheckpointHex): Promise { - // don't have to transfer cache for this specific api - const stateOrLoadedState = await this.getStateOrLoadDb(cp, {dontTransferCache: true}); + const stateOrLoadedState = await this.getStateOrLoadDb(cp); if (stateOrLoadedState === null || isCachedBeaconState(stateOrLoadedState)) { return stateOrLoadedState; } @@ -274,12 +273,9 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache { /** * Return either state or state bytes with persisted key loaded from db. */ - async getStateOrLoadDb( - cp: CheckpointHex, - opts?: StateRegenerationOpts - ): Promise { + async getStateOrLoadDb(cp: CheckpointHex): Promise { const cpKey = toCacheKey(cp); - const inMemoryState = this.get(cpKey, opts); + const inMemoryState = this.get(cpKey); if (inMemoryState) { return inMemoryState; } @@ -308,7 +304,7 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache { /** * Similar to get() api without reloading from disk */ - get(cpOrKey: CheckpointHex | string, opts?: StateRegenerationOpts): CachedBeaconStateAllForks | null { + get(cpOrKey: CheckpointHex | string): CachedBeaconStateAllForks | null { this.metrics?.cpStateCache.lookups.inc(); const cpKey = typeof cpOrKey === "string" ? cpOrKey : toCacheKey(cpOrKey); const cacheItem = this.cache.get(cpKey); @@ -326,7 +322,7 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache { if (isInMemoryCacheItem(cacheItem)) { const {state} = cacheItem; this.metrics?.cpStateCache.stateClonedCount.observe(state.clonedCount); - return state.clone(opts?.dontTransferCache); + return state; } return null; @@ -360,14 +356,14 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache { /** * Searches in-memory state for the latest cached state with a `root` without reload, starting with `epoch` and descending */ - getLatest(rootHex: RootHex, maxEpoch: Epoch, opts?: StateRegenerationOpts): CachedBeaconStateAllForks | null { + getLatest(rootHex: RootHex, maxEpoch: Epoch): CachedBeaconStateAllForks | null { // sort epochs in descending order, only consider epochs lte `epoch` const epochs = Array.from(this.epochIndex.keys()) .sort((a, b) => b - a) .filter((e) => e <= maxEpoch); for (const epoch of epochs) { if (this.epochIndex.get(epoch)?.has(rootHex)) { - const inMemoryClonedState = this.get({rootHex, epoch}, opts); + const inMemoryClonedState = this.get({rootHex, epoch}); if (inMemoryClonedState) { return inMemoryClonedState; } @@ -383,11 +379,7 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache { * - Get block for processing * - Regen head state */ - async getOrReloadLatest( - rootHex: RootHex, - maxEpoch: Epoch, - opts?: StateRegenerationOpts - ): Promise { + async getOrReloadLatest(rootHex: RootHex, maxEpoch: Epoch): Promise { // sort epochs in descending order, only consider epochs lte `epoch` const epochs = Array.from(this.epochIndex.keys()) .sort((a, b) => b - a) @@ -395,9 +387,9 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache { for (const epoch of epochs) { if (this.epochIndex.get(epoch)?.has(rootHex)) { try { - const clonedState = await this.getOrReload({rootHex, epoch}, opts); - if (clonedState) { - return clonedState; + const state = await this.getOrReload({rootHex, epoch}); + if (state) { + return state; } } catch (e) { this.logger.debug("Error get or reload state", {epoch, rootHex}, e as Error); @@ -859,6 +851,10 @@ export function toCheckpointHex(checkpoint: phase0.Checkpoint): CheckpointHex { }; } +export function toCheckpointKey(cp: CheckpointHex): string { + return `${cp.rootHex}:${cp.epoch}`; +} + function toCacheKey(cp: CheckpointHex | phase0.Checkpoint): CacheKey { if (isCheckpointHex(cp)) { return `${cp.rootHex}_${cp.epoch}`; diff --git a/packages/beacon-node/src/chain/stateCache/types.ts b/packages/beacon-node/src/chain/stateCache/types.ts index 19f05c23ee35..b16590967c9d 100644 --- a/packages/beacon-node/src/chain/stateCache/types.ts +++ b/packages/beacon-node/src/chain/stateCache/types.ts @@ -1,7 +1,6 @@ import {routes} from "@lodestar/api"; import {CachedBeaconStateAllForks} from "@lodestar/state-transition"; import {Epoch, RootHex, phase0} from "@lodestar/types"; -import {StateRegenerationOpts} from "../regen/interface.js"; export type CheckpointHex = {epoch: Epoch; rootHex: RootHex}; @@ -21,7 +20,7 @@ export type CheckpointHex = {epoch: Epoch; rootHex: RootHex}; * The cache key is state root */ export interface BlockStateCache { - get(rootHex: RootHex, opts?: StateRegenerationOpts): CachedBeaconStateAllForks | null; + get(rootHex: RootHex): CachedBeaconStateAllForks | null; add(item: CachedBeaconStateAllForks): void; setHeadState(item: CachedBeaconStateAllForks | null): void; /** @@ -60,16 +59,12 @@ export interface BlockStateCache { */ export interface CheckpointStateCache { init?: () => Promise; - getOrReload(cp: CheckpointHex, opts?: StateRegenerationOpts): Promise; + getOrReload(cp: CheckpointHex): Promise; getStateOrBytes(cp: CheckpointHex): Promise; - get(cpOrKey: CheckpointHex | string, opts?: StateRegenerationOpts): CachedBeaconStateAllForks | null; + get(cpOrKey: CheckpointHex | string): CachedBeaconStateAllForks | null; add(cp: phase0.Checkpoint, state: CachedBeaconStateAllForks): void; - getLatest(rootHex: RootHex, maxEpoch: Epoch, opts?: StateRegenerationOpts): CachedBeaconStateAllForks | null; - getOrReloadLatest( - rootHex: RootHex, - maxEpoch: Epoch, - opts?: StateRegenerationOpts - ): Promise; + getLatest(rootHex: RootHex, maxEpoch: Epoch): CachedBeaconStateAllForks | null; + getOrReloadLatest(rootHex: RootHex, maxEpoch: Epoch): Promise; updatePreComputedCheckpoint(rootHex: RootHex, epoch: Epoch): number | null; prune(finalizedEpoch: Epoch, justifiedEpoch: Epoch): void; pruneFinalized(finalizedEpoch: Epoch): void; diff --git a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts index c8cbebac6e33..162788f6caf1 100644 --- a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts +++ b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts @@ -71,11 +71,34 @@ async function validateAggregateAndProof( const attData = aggregate.data; const attSlot = attData.slot; - let attIndex: number | null; - if (ForkSeq[fork] >= ForkSeq.electra) { - attIndex = (aggregate as electra.Attestation).committeeBits.getSingleTrueBit(); + let committeeIndex: number | null; + if (ForkSeq[fork] >= ForkSeq.gloas) { + // [REJECT] `aggregate.data.index < 2`. + if (attData.index >= 2) { + throw new AttestationError(GossipAction.REJECT, { + code: AttestationErrorCode.INVALID_PAYLOAD_STATUS_VALUE, + attDataIndex: attData.index, + }); + } + // [REJECT] `aggregate.data.index == 0` if `block.slot == aggregate.data.slot`. + const block = chain.forkChoice.getBlock(attData.beaconBlockRoot); + + // If block is unknown, we don't handle it here. It will throw error later on at `verifyHeadBlockAndTargetRoot()` + if (block !== null && block.slot === attData.slot && attData.index !== 0) { + throw new AttestationError(GossipAction.REJECT, { + code: AttestationErrorCode.PREMATURELY_INDICATED_PAYLOAD_PRESENT, + }); + } + + // [REJECT] len(committee_indices) == 1, where committee_indices = get_committee_indices(aggregate) + committeeIndex = (aggregate as electra.Attestation).committeeBits.getSingleTrueBit(); + if (committeeIndex === null) { + throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.NOT_EXACTLY_ONE_COMMITTEE_BIT_SET}); + } + } else if (ForkSeq[fork] >= ForkSeq.electra) { + committeeIndex = (aggregate as electra.Attestation).committeeBits.getSingleTrueBit(); // [REJECT] len(committee_indices) == 1, where committee_indices = get_committee_indices(aggregate) - if (attIndex === null) { + if (committeeIndex === null) { throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.NOT_EXACTLY_ONE_COMMITTEE_BIT_SET}); } // [REJECT] aggregate.data.index == 0 @@ -83,11 +106,11 @@ async function validateAggregateAndProof( throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX}); } } else { - attIndex = attData.index; + committeeIndex = attData.index; } const seenAttDataKey = serializedData ? getSeenAttDataKeyFromSignedAggregateAndProof(fork, serializedData) : null; - const cachedAttData = seenAttDataKey ? chain.seenAttestationDatas.get(attSlot, attIndex, seenAttDataKey) : null; + const cachedAttData = seenAttDataKey ? chain.seenAttestationDatas.get(attSlot, committeeIndex, seenAttDataKey) : null; const attEpoch = computeEpochAtSlot(attSlot); const attTarget = attData.target; @@ -136,7 +159,7 @@ async function validateAggregateAndProof( : toRootHex(ssz.phase0.AttestationData.hashTreeRoot(attData)); if ( !skipValidationKnownAttesters && - chain.seenAggregatedAttestations.isKnown(targetEpoch, attIndex, attDataRootHex, aggregationBits) + chain.seenAggregatedAttestations.isKnown(targetEpoch, committeeIndex, attDataRootHex, aggregationBits) ) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.ATTESTERS_ALREADY_KNOWN, @@ -177,7 +200,7 @@ async function validateAggregateAndProof( // -- i.e. data.index < get_committee_count_per_slot(state, data.target.epoch) const committeeValidatorIndices = cachedAttData ? cachedAttData.committeeValidatorIndices - : getCommitteeValidatorIndices(shuffling, attSlot, attIndex); + : getCommitteeValidatorIndices(shuffling, attSlot, committeeIndex); // [REJECT] The number of aggregation bits matches the committee size // -- i.e. `len(aggregation_bits) == len(get_beacon_committee(state, aggregate.data.slot, index))`. @@ -216,16 +239,15 @@ async function validateAggregateAndProof( // by the validator with index aggregate_and_proof.aggregator_index. // [REJECT] The aggregator signature, signed_aggregate_and_proof.signature, is valid. // [REJECT] The signature of aggregate is valid. - const aggregator = chain.index2pubkey[aggregateAndProof.aggregatorIndex]; const signingRoot = cachedAttData ? cachedAttData.signingRoot : getAttestationDataSigningRoot(chain.config, attData); const indexedAttestationSignatureSet = createAggregateSignatureSetFromComponents( - indexedAttestation.attestingIndices.map((i) => chain.index2pubkey[i]), + indexedAttestation.attestingIndices, signingRoot, indexedAttestation.signature ); const signatureSets = [ - getSelectionProofSignatureSet(chain.config, attSlot, aggregator, signedAggregateAndProof), - getAggregateAndProofSignatureSet(chain.config, attEpoch, aggregator, signedAggregateAndProof), + getSelectionProofSignatureSet(chain.config, attSlot, aggregatorIndex, signedAggregateAndProof), + getAggregateAndProofSignatureSet(chain.config, attEpoch, aggregatorIndex, signedAggregateAndProof), indexedAttestationSignatureSet, ]; // no need to write to SeenAttestationDatas @@ -248,7 +270,7 @@ async function validateAggregateAndProof( // Same race-condition check as above for seen aggregators if ( !skipValidationKnownAttesters && - chain.seenAggregatedAttestations.isKnown(targetEpoch, attIndex, attDataRootHex, aggregationBits) + chain.seenAggregatedAttestations.isKnown(targetEpoch, committeeIndex, attDataRootHex, aggregationBits) ) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.ATTESTERS_ALREADY_KNOWN, @@ -260,7 +282,7 @@ async function validateAggregateAndProof( chain.seenAggregators.add(targetEpoch, aggregatorIndex); chain.seenAggregatedAttestations.add( targetEpoch, - attIndex, + committeeIndex, attDataRootHex, {aggregationBits, trueBitCount: attestingIndices.length}, false diff --git a/packages/beacon-node/src/chain/validation/attestation.ts b/packages/beacon-node/src/chain/validation/attestation.ts index cf026222a3a9..fd49a8fb494d 100644 --- a/packages/beacon-node/src/chain/validation/attestation.ts +++ b/packages/beacon-node/src/chain/validation/attestation.ts @@ -10,16 +10,17 @@ import { ForkSeq, SLOTS_PER_EPOCH, isForkPostElectra, + isForkPostGloas, } from "@lodestar/params"; import { - EpochCacheError, - EpochCacheErrorCode, EpochShuffling, - SingleSignatureSet, + IndexedSignatureSet, + ShufflingError, + ShufflingErrorCode, computeEpochAtSlot, computeSigningRoot, computeStartSlotAtEpoch, - createSingleSignatureSetFromComponents, + createIndexedSignatureSetFromComponents, } from "@lodestar/state-transition"; import { CommitteeIndex, @@ -89,7 +90,7 @@ export type GossipAttestation = { }; export type Step0Result = AttestationValidationResult & { - signatureSet: SingleSignatureSet; + signatureSet: IndexedSignatureSet; validatorIndex: number; }; @@ -124,7 +125,7 @@ export async function validateGossipAttestationsSameAttData( // step1: verify signatures of all valid attestations // map new index to index in resultOrErrors const newIndexToOldIndex = new Map(); - const signatureSets: SingleSignatureSet[] = []; + const signatureSets: IndexedSignatureSet[] = []; let newIndex = 0; const step0Results: Step0Result[] = []; for (const [i, resultOrError] of step0ResultOrErrors.entries()) { @@ -142,7 +143,7 @@ export async function validateGossipAttestationsSameAttData( if (batchableBls) { // all signature sets should have same signing root since we filtered in network processor signatureValids = await chain.bls.verifySignatureSetsSameMessage( - signatureSets.map((set) => ({publicKey: set.pubkey, signature: set.signature})), + signatureSets.map((set) => ({publicKey: chain.index2pubkey[set.index], signature: set.signature})), signatureSets[0].signingRoot ); } else { @@ -224,7 +225,7 @@ export async function validateApiAttestation( code: AttestationErrorCode.INVALID_SIGNATURE, }); } catch (err) { - if (err instanceof EpochCacheError && err.type.code === EpochCacheErrorCode.COMMITTEE_INDEX_OUT_OF_RANGE) { + if (err instanceof ShufflingError && err.type.code === ShufflingErrorCode.COMMITTEE_INDEX_OUT_OF_RANGE) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.BAD_TARGET_EPOCH, }); @@ -293,9 +294,29 @@ async function validateAttestationNoSignatureCheck( // api or first time validation of a gossip attestation committeeIndex = attestationOrCache.attestation.committeeIndex; - // [REJECT] attestation.data.index == 0 - if (attData.index !== 0) { - throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX}); + if (isForkPostGloas(fork)) { + // [REJECT] `attestation.data.index < 2`. + if (attData.index >= 2) { + throw new AttestationError(GossipAction.REJECT, { + code: AttestationErrorCode.INVALID_PAYLOAD_STATUS_VALUE, + attDataIndex: attData.index, + }); + } + + // [REJECT] `attestation.data.index == 0` if `block.slot == attestation.data.slot`. + const block = chain.forkChoice.getBlock(attData.beaconBlockRoot); + + // block being null will be handled by `verifyHeadBlockAndTargetRoot` + if (block !== null && block.slot === attSlot && attData.index !== 0) { + throw new AttestationError(GossipAction.REJECT, { + code: AttestationErrorCode.PREMATURELY_INDICATED_PAYLOAD_PRESENT, + }); + } + } else { + // [REJECT] attestation.data.index == 0 + if (attData.index !== 0) { + throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX}); + } } } else { // phase0 attestation @@ -477,7 +498,7 @@ async function validateAttestationNoSignatureCheck( // [REJECT] The signature of attestation is valid. const attestingIndices = [validatorIndex]; - let signatureSet: SingleSignatureSet; + let signatureSet: IndexedSignatureSet; let attDataRootHex: RootHex; const signature = attestationOrCache.attestation ? attestationOrCache.attestation.signature @@ -492,18 +513,14 @@ async function validateAttestationNoSignatureCheck( if (attestationOrCache.cache) { // there could be up to 6% of cpu time to compute signing root if we don't clone the signature set - signatureSet = createSingleSignatureSetFromComponents( - chain.index2pubkey[validatorIndex], + signatureSet = createIndexedSignatureSetFromComponents( + validatorIndex, attestationOrCache.cache.signingRoot, signature ); attDataRootHex = attestationOrCache.cache.attDataRootHex; } else { - signatureSet = createSingleSignatureSetFromComponents( - chain.index2pubkey[validatorIndex], - getSigningRoot(), - signature - ); + signatureSet = createIndexedSignatureSetFromComponents(validatorIndex, getSigningRoot(), signature); // add cached attestation data before verifying signature attDataRootHex = toRootHex(ssz.phase0.AttestationData.hashTreeRoot(attData)); diff --git a/packages/beacon-node/src/chain/validation/attesterSlashing.ts b/packages/beacon-node/src/chain/validation/attesterSlashing.ts index 99b5ce474a32..df6774f26b22 100644 --- a/packages/beacon-node/src/chain/validation/attesterSlashing.ts +++ b/packages/beacon-node/src/chain/validation/attesterSlashing.ts @@ -43,7 +43,14 @@ export async function validateAttesterSlashing( // [REJECT] All of the conditions within process_attester_slashing pass validation. try { // verifySignature = false, verified in batch below - assertValidAttesterSlashing(chain.index2pubkey, state, attesterSlashing, false); + assertValidAttesterSlashing( + chain.config, + chain.index2pubkey, + state.slot, + state.validators.length, + attesterSlashing, + false + ); } catch (e) { throw new AttesterSlashingError(GossipAction.REJECT, { code: AttesterSlashingErrorCode.INVALID, @@ -51,12 +58,7 @@ export async function validateAttesterSlashing( }); } - const signatureSets = getAttesterSlashingSignatureSets( - chain.config, - chain.index2pubkey, - state.slot, - attesterSlashing - ); + const signatureSets = getAttesterSlashingSignatureSets(chain.config, state.slot, attesterSlashing); if (!(await chain.bls.verifySignatureSets(signatureSets, {batchable: true, priority: prioritizeBls}))) { throw new AttesterSlashingError(GossipAction.REJECT, { code: AttesterSlashingErrorCode.INVALID, diff --git a/packages/beacon-node/src/chain/validation/blobSidecar.ts b/packages/beacon-node/src/chain/validation/blobSidecar.ts index b7f507d5fbca..cbf27dcf03ae 100644 --- a/packages/beacon-node/src/chain/validation/blobSidecar.ts +++ b/packages/beacon-node/src/chain/validation/blobSidecar.ts @@ -12,7 +12,7 @@ import { getBlockHeaderProposerSignatureSetByParentStateSlot, } from "@lodestar/state-transition"; import {BlobIndex, Root, Slot, SubnetID, deneb, ssz} from "@lodestar/types"; -import {toRootHex, verifyMerkleBranch} from "@lodestar/utils"; +import {byteArrayEquals, toRootHex, verifyMerkleBranch} from "@lodestar/utils"; import {kzg} from "../../util/kzg.js"; import {BlobSidecarErrorCode, BlobSidecarGossipError, BlobSidecarValidationError} from "../errors/blobSidecarError.js"; import {GossipAction} from "../errors/gossipValidation.js"; @@ -124,7 +124,7 @@ export async function validateGossipBlobSidecar( // [IGNORE] The block's parent (defined by block.parent_root) has been seen (via both gossip and non-gossip sources) (a client MAY queue blocks for processing once the parent block is retrieved). // [REJECT] The block's parent (defined by block.parent_root) passes validation. const blockState = await chain.regen - .getBlockSlotState(parentRoot, blobSlot, {dontTransferCache: true}, RegenCaller.validateGossipBlock) + .getBlockSlotState(parentBlock, blobSlot, {dontTransferCache: true}, RegenCaller.validateGossipBlock) .catch(() => { throw new BlobSidecarGossipError(GossipAction.IGNORE, { code: BlobSidecarErrorCode.PARENT_UNKNOWN, @@ -139,7 +139,6 @@ export async function validateGossipBlobSidecar( if (!chain.seenBlockInputCache.isVerifiedProposerSignature(blobSlot, blockHex, signature)) { const signatureSet = getBlockHeaderProposerSignatureSetByParentStateSlot( chain.config, - chain.index2pubkey, blockState.slot, blobSidecar.signedBlockHeader ); @@ -227,7 +226,7 @@ export async function validateBlockBlobSidecars( const firstSidecarSignedBlockHeader = blobSidecars[0].signedBlockHeader; const firstSidecarBlockHeader = firstSidecarSignedBlockHeader.message; const firstBlockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(firstSidecarBlockHeader); - if (Buffer.compare(blockRoot, firstBlockRoot) !== 0) { + if (!byteArrayEquals(blockRoot, firstBlockRoot)) { throw new BlobSidecarValidationError( { code: BlobSidecarErrorCode.INCORRECT_BLOCK, @@ -244,11 +243,7 @@ export async function validateBlockBlobSidecars( const blockRootHex = toRootHex(blockRoot); const signature = firstSidecarSignedBlockHeader.signature; if (!chain.seenBlockInputCache.isVerifiedProposerSignature(blockSlot, blockRootHex, signature)) { - const signatureSet = getBlockHeaderProposerSignatureSetByHeaderSlot( - chain.config, - chain.index2pubkey, - firstSidecarSignedBlockHeader - ); + const signatureSet = getBlockHeaderProposerSignatureSetByHeaderSlot(chain.config, firstSidecarSignedBlockHeader); if ( !(await chain.bls.verifySignatureSets([signatureSet], { diff --git a/packages/beacon-node/src/chain/validation/block.ts b/packages/beacon-node/src/chain/validation/block.ts index 24b23aa43f6b..c8effa332c7c 100644 --- a/packages/beacon-node/src/chain/validation/block.ts +++ b/packages/beacon-node/src/chain/validation/block.ts @@ -1,5 +1,5 @@ import {ChainForkConfig} from "@lodestar/config"; -import {ForkName, isForkPostDeneb} from "@lodestar/params"; +import {ForkName, isForkPostBellatrix, isForkPostDeneb, isForkPostGloas} from "@lodestar/params"; import { computeEpochAtSlot, computeStartSlotAtEpoch, @@ -111,7 +111,7 @@ export async function validateGossipBlock( } // [REJECT] The length of KZG commitments is less than or equal to the limitation defined in Consensus Layer -- i.e. validate that len(body.signed_beacon_block.message.blob_kzg_commitments) <= MAX_BLOBS_PER_BLOCK - if (isForkPostDeneb(fork)) { + if (isForkPostDeneb(fork) && !isForkPostGloas(fork)) { const blobKzgCommitmentsLen = (block as deneb.BeaconBlock).body.blobKzgCommitments.length; const maxBlobsPerBlock = config.getMaxBlobsPerBlock(computeEpochAtSlot(blockSlot)); if (blobKzgCommitmentsLen > maxBlobsPerBlock) { @@ -128,17 +128,20 @@ export async function validateGossipBlock( // this is something we should change this in the future to make the code airtight to the spec. // [IGNORE] The block's parent (defined by block.parent_root) has been seen (via both gossip and non-gossip sources) (a client MAY queue blocks for processing once the parent block is retrieved). // [REJECT] The block's parent (defined by block.parent_root) passes validation. + // TODO GLOAS: post-gloas, we check the validity of bid's parent payload, not the entire beacon block const blockState = await chain.regen .getPreState(block, {dontTransferCache: true}, RegenCaller.validateGossipBlock) .catch(() => { throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.PARENT_UNKNOWN, parentRoot}); }); - // Extra conditions for merge fork blocks + // in forky condition, make sure to populate ShufflingCache with regened state + chain.shufflingCache.processState(blockState); + // [REJECT] The block's execution payload timestamp is correct with respect to the slot // -- i.e. execution_payload.timestamp == compute_timestamp_at_slot(state, block.slot). - if (fork === ForkName.bellatrix) { - if (!isExecutionBlockBodyType(block.body)) throw Error("Not merge block type"); + if (isForkPostBellatrix(fork) && !isForkPostGloas(fork)) { + if (!isExecutionBlockBodyType(block.body)) throw Error("Not execution block body type"); const executionPayload = block.body.executionPayload; if (isExecutionStateType(blockState) && isExecutionEnabled(blockState, block)) { const expectedTimestamp = computeTimeAtSlot(config, blockSlot, chain.genesisTime); @@ -154,7 +157,7 @@ export async function validateGossipBlock( // [REJECT] The proposer signature, signed_beacon_block.signature, is valid with respect to the proposer_index pubkey. if (!chain.seenBlockInputCache.isVerifiedProposerSignature(blockSlot, blockRoot, signedBlock.signature)) { - const signatureSet = getBlockProposerSignatureSet(chain.config, chain.index2pubkey, signedBlock); + const signatureSet = getBlockProposerSignatureSet(chain.config, signedBlock); // Don't batch so verification is not delayed if (!(await chain.bls.verifySignatureSets([signatureSet], {verifyOnMainThread: true}))) { throw new BlockGossipError(GossipAction.REJECT, { diff --git a/packages/beacon-node/src/chain/validation/blsToExecutionChange.ts b/packages/beacon-node/src/chain/validation/blsToExecutionChange.ts index 6225fa979d06..94446a42e3d1 100644 --- a/packages/beacon-node/src/chain/validation/blsToExecutionChange.ts +++ b/packages/beacon-node/src/chain/validation/blsToExecutionChange.ts @@ -1,8 +1,4 @@ -import { - CachedBeaconStateCapella, - getBlsToExecutionChangeSignatureSet, - isValidBlsToExecutionChange, -} from "@lodestar/state-transition"; +import {getBlsToExecutionChangeSignatureSet, isValidBlsToExecutionChange} from "@lodestar/state-transition"; import {capella} from "@lodestar/types"; import {BlsToExecutionChangeError, BlsToExecutionChangeErrorCode, GossipAction} from "../errors/index.js"; import {IBeaconChain} from "../index.js"; @@ -42,10 +38,16 @@ async function validateBlsToExecutionChange( // and chanes relevant to `isValidBlsToExecutionChange()` happen only on processBlock(), not processEpoch() const state = chain.getHeadState(); const {config} = chain; - + const addressChange = blsToExecutionChange.message; + if (addressChange.validatorIndex >= state.validators.length) { + throw new BlsToExecutionChangeError(GossipAction.REJECT, { + code: BlsToExecutionChangeErrorCode.INVALID, + }); + } + const validator = state.validators.getReadonly(addressChange.validatorIndex); // [REJECT] All of the conditions within process_bls_to_execution_change pass validation. // verifySignature = false, verified in batch below - const {valid} = isValidBlsToExecutionChange(state as CachedBeaconStateCapella, blsToExecutionChange, false); + const {valid} = isValidBlsToExecutionChange(config, validator, blsToExecutionChange, false); if (!valid) { throw new BlsToExecutionChangeError(GossipAction.REJECT, { code: BlsToExecutionChangeErrorCode.INVALID, diff --git a/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts b/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts index e20565863699..82855a6300c3 100644 --- a/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts +++ b/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts @@ -11,7 +11,7 @@ import { getBlockHeaderProposerSignatureSetByParentStateSlot, } from "@lodestar/state-transition"; import {Root, Slot, SubnetID, fulu, ssz} from "@lodestar/types"; -import {toRootHex, verifyMerkleBranch} from "@lodestar/utils"; +import {byteArrayEquals, toRootHex, verifyMerkleBranch} from "@lodestar/utils"; import {Metrics} from "../../metrics/metrics.js"; import {kzg} from "../../util/kzg.js"; import { @@ -106,7 +106,7 @@ export async function validateGossipDataColumnSidecar( // this is something we should change this in the future to make the code airtight to the spec. // 7) [REJECT] The sidecar's block's parent passes validation. const blockState = await chain.regen - .getBlockSlotState(parentRoot, blockHeader.slot, {dontTransferCache: true}, RegenCaller.validateGossipDataColumn) + .getBlockSlotState(parentBlock, blockHeader.slot, {dontTransferCache: true}, RegenCaller.validateGossipDataColumn) .catch(() => { throw new DataColumnSidecarGossipError(GossipAction.IGNORE, { code: DataColumnSidecarErrorCode.PARENT_UNKNOWN, @@ -136,7 +136,6 @@ export async function validateGossipDataColumnSidecar( if (!chain.seenBlockInputCache.isVerifiedProposerSignature(blockHeader.slot, blockRootHex, signature)) { const signatureSet = getBlockHeaderProposerSignatureSetByParentStateSlot( chain.config, - chain.index2pubkey, blockState.slot, dataColumnSidecar.signedBlockHeader ); @@ -319,7 +318,7 @@ export async function validateBlockDataColumnSidecars( const firstSidecarSignedBlockHeader = dataColumnSidecars[0].signedBlockHeader; const firstSidecarBlockHeader = firstSidecarSignedBlockHeader.message; const firstBlockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(firstSidecarBlockHeader); - if (Buffer.compare(blockRoot, firstBlockRoot) !== 0) { + if (!byteArrayEquals(blockRoot, firstBlockRoot)) { throw new DataColumnSidecarValidationError( { code: DataColumnSidecarErrorCode.INCORRECT_BLOCK, @@ -337,11 +336,7 @@ export async function validateBlockDataColumnSidecars( const slot = firstSidecarSignedBlockHeader.message.slot; const signature = firstSidecarSignedBlockHeader.signature; if (!chain.seenBlockInputCache.isVerifiedProposerSignature(slot, rootHex, signature)) { - const signatureSet = getBlockHeaderProposerSignatureSetByHeaderSlot( - chain.config, - chain.index2pubkey, - firstSidecarSignedBlockHeader - ); + const signatureSet = getBlockHeaderProposerSignatureSetByHeaderSlot(chain.config, firstSidecarSignedBlockHeader); if ( !(await chain.bls.verifySignatureSets([signatureSet], { diff --git a/packages/beacon-node/src/chain/validation/executionPayloadBid.ts b/packages/beacon-node/src/chain/validation/executionPayloadBid.ts new file mode 100644 index 000000000000..771f9d1e2842 --- /dev/null +++ b/packages/beacon-node/src/chain/validation/executionPayloadBid.ts @@ -0,0 +1,141 @@ +import {PublicKey} from "@chainsafe/blst"; +import { + CachedBeaconStateGloas, + canBuilderCoverBid, + createSingleSignatureSetFromComponents, + getExecutionPayloadBidSigningRoot, + isActiveBuilder, +} from "@lodestar/state-transition"; +import {gloas} from "@lodestar/types"; +import {toRootHex} from "@lodestar/utils"; +import {ExecutionPayloadBidError, ExecutionPayloadBidErrorCode, GossipAction} from "../errors/index.js"; +import {IBeaconChain} from "../index.js"; +import {RegenCaller} from "../regen/index.js"; + +export async function validateApiExecutionPayloadBid( + chain: IBeaconChain, + signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid +): Promise { + return validateExecutionPayloadBid(chain, signedExecutionPayloadBid); +} + +export async function validateGossipExecutionPayloadBid( + chain: IBeaconChain, + signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid +): Promise { + return validateExecutionPayloadBid(chain, signedExecutionPayloadBid); +} + +async function validateExecutionPayloadBid( + chain: IBeaconChain, + signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid +): Promise { + const bid = signedExecutionPayloadBid.message; + const parentBlockRootHex = toRootHex(bid.parentBlockRoot); + const parentBlockHashHex = toRootHex(bid.parentBlockHash); + const state = (await chain.getHeadStateAtCurrentEpoch( + RegenCaller.validateGossipExecutionPayloadBid + )) as CachedBeaconStateGloas; + + // [IGNORE] `bid.slot` is the current slot or the next slot. + const currentSlot = chain.clock.currentSlot; + if (bid.slot !== currentSlot && bid.slot !== currentSlot + 1) { + throw new ExecutionPayloadBidError(GossipAction.IGNORE, { + code: ExecutionPayloadBidErrorCode.INVALID_SLOT, + builderIndex: bid.builderIndex, + slot: bid.slot, + }); + } + + // [IGNORE] the `SignedProposerPreferences` where `preferences.proposal_slot` + // is equal to `bid.slot` has been seen. + // TODO GLOAS: Implement this along with proposer preference + + // [REJECT] `bid.builder_index` is a valid/active builder index -- i.e. + // `is_active_builder(state, bid.builder_index)` returns `True`. + const builder = state.builders.getReadonly(bid.builderIndex); + if (!isActiveBuilder(builder, state.finalizedCheckpoint.epoch)) { + throw new ExecutionPayloadBidError(GossipAction.REJECT, { + code: ExecutionPayloadBidErrorCode.BUILDER_NOT_ELIGIBLE, + builderIndex: bid.builderIndex, + }); + } + + // [REJECT] `bid.execution_payment` is zero. + if (bid.executionPayment !== 0) { + throw new ExecutionPayloadBidError(GossipAction.REJECT, { + code: ExecutionPayloadBidErrorCode.NON_ZERO_EXECUTION_PAYMENT, + builderIndex: bid.builderIndex, + executionPayment: bid.executionPayment, + }); + } + + // [REJECT] `bid.fee_recipient` matches the `fee_recipient` from the proposer's + // `SignedProposerPreferences` associated with `bid.slot`. + // [REJECT] `bid.gas_limit` matches the `gas_limit` from the proposer's + // `SignedProposerPreferences` associated with `bid.slot`. + // TODO GLOAS: Implement this along with proposer preference + + // [IGNORE] this is the first signed bid seen with a valid signature from the given builder for this slot. + if (chain.seenExecutionPayloadBids.isKnown(bid.slot, bid.builderIndex)) { + throw new ExecutionPayloadBidError(GossipAction.IGNORE, { + code: ExecutionPayloadBidErrorCode.BID_ALREADY_KNOWN, + builderIndex: bid.builderIndex, + slot: bid.slot, + parentBlockRoot: parentBlockRootHex, + parentBlockHash: parentBlockHashHex, + }); + } + + // [IGNORE] this bid is the highest value bid seen for the corresponding slot + // and the given parent block hash. + const bestBid = chain.executionPayloadBidPool.getBestBid(parentBlockRootHex, parentBlockHashHex, bid.slot); + if (bestBid !== null && bestBid.value >= bid.value) { + throw new ExecutionPayloadBidError(GossipAction.IGNORE, { + code: ExecutionPayloadBidErrorCode.BID_TOO_LOW, + bidValue: bid.value, + currentHighestBid: bestBid.value, + }); + } + // [IGNORE] `bid.value` is less or equal than the builder's excess balance -- + // i.e. `can_builder_cover_bid(state, builder_index, amount)` returns `True`. + if (!canBuilderCoverBid(state, bid.builderIndex, bid.value)) { + throw new ExecutionPayloadBidError(GossipAction.IGNORE, { + code: ExecutionPayloadBidErrorCode.BID_TOO_HIGH, + bidValue: bid.value, + builderBalance: builder.balance, + }); + } + + // [IGNORE] `bid.parent_block_hash` is the block hash of a known execution + // payload in fork choice. + // TODO GLOAS: implement this + + // [IGNORE] `bid.parent_block_root` is the hash tree root of a known beacon + // block in fork choice. + const block = chain.forkChoice.getBlock(bid.parentBlockRoot); + if (block === null) { + throw new ExecutionPayloadBidError(GossipAction.IGNORE, { + code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT, + parentBlockRoot: parentBlockRootHex, + }); + } + + // [REJECT] `signed_execution_payload_bid.signature` is valid with respect to the `bid.builder_index`. + const signatureSet = createSingleSignatureSetFromComponents( + PublicKey.fromBytes(builder.pubkey), + getExecutionPayloadBidSigningRoot(chain.config, state.slot, bid), + signedExecutionPayloadBid.signature + ); + + if (!(await chain.bls.verifySignatureSets([signatureSet]))) { + throw new ExecutionPayloadBidError(GossipAction.REJECT, { + code: ExecutionPayloadBidErrorCode.INVALID_SIGNATURE, + builderIndex: bid.builderIndex, + slot: bid.slot, + }); + } + + // Valid + chain.seenExecutionPayloadBids.add(bid.slot, bid.builderIndex); +} diff --git a/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts b/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts new file mode 100644 index 000000000000..9891ed54f647 --- /dev/null +++ b/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts @@ -0,0 +1,122 @@ +import {PublicKey} from "@chainsafe/blst"; +import { + CachedBeaconStateGloas, + computeStartSlotAtEpoch, + createSingleSignatureSetFromComponents, + getExecutionPayloadEnvelopeSigningRoot, +} from "@lodestar/state-transition"; +import {gloas} from "@lodestar/types"; +import {toRootHex} from "@lodestar/utils"; +import {ExecutionPayloadEnvelopeError, ExecutionPayloadEnvelopeErrorCode, GossipAction} from "../errors/index.js"; +import {IBeaconChain} from "../index.js"; + +export async function validateApiExecutionPayloadEnvelope( + chain: IBeaconChain, + executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope +): Promise { + return validateExecutionPayloadEnvelope(chain, executionPayloadEnvelope); +} + +export async function validateGossipExecutionPayloadEnvelope( + chain: IBeaconChain, + executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope +): Promise { + return validateExecutionPayloadEnvelope(chain, executionPayloadEnvelope); +} + +async function validateExecutionPayloadEnvelope( + chain: IBeaconChain, + executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope +): Promise { + const envelope = executionPayloadEnvelope.message; + const {payload} = envelope; + const blockRootHex = toRootHex(envelope.beaconBlockRoot); + + // [IGNORE] The envelope's block root `envelope.block_root` has been seen (via + // gossip or non-gossip sources) (a client MAY queue payload for processing once + // the block is retrieved). + // TODO GLOAS: Need to review this + const block = chain.forkChoice.getBlock(envelope.beaconBlockRoot); + if (block === null) { + throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { + code: ExecutionPayloadEnvelopeErrorCode.BLOCK_ROOT_UNKNOWN, + blockRoot: blockRootHex, + }); + } + + // [IGNORE] The node has not seen another valid + // `SignedExecutionPayloadEnvelope` for this block root from this builder. + if (chain.seenExecutionPayloadEnvelopes.isKnown(blockRootHex)) { + throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { + code: ExecutionPayloadEnvelopeErrorCode.ENVELOPE_ALREADY_KNOWN, + blockRoot: blockRootHex, + slot: envelope.slot, + }); + } + + // [IGNORE] The envelope is from a slot greater than or equal to the latest finalized slot -- i.e. validate that `envelope.slot >= compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)` + const finalizedCheckpoint = chain.forkChoice.getFinalizedCheckpoint(); + const finalizedSlot = computeStartSlotAtEpoch(finalizedCheckpoint.epoch); + if (envelope.slot < finalizedSlot) { + throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { + code: ExecutionPayloadEnvelopeErrorCode.BELONG_TO_FINALIZED_BLOCK, + envelopeSlot: envelope.slot, + finalizedSlot, + }); + } + + // [REJECT] `block` passes validation. + // TODO GLOAS: implement this. Technically if we cannot get proto block from fork choice, + // it is possible that the block didn't pass the validation + + // [REJECT] `block.slot` equals `envelope.slot`. + if (block.slot !== envelope.slot) { + throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, { + code: ExecutionPayloadEnvelopeErrorCode.SLOT_MISMATCH, + envelopeSlot: envelope.slot, + blockSlot: block.slot, + }); + } + + if (block.builderIndex === undefined || block.blockHashHex === undefined) { + // This indicates this block is a pre-gloas block which is wrong + throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { + code: ExecutionPayloadEnvelopeErrorCode.CACHE_FAIL, + blockRoot: blockRootHex, + }); + } + + // [REJECT] `envelope.builder_index == bid.builder_index` + if (envelope.builderIndex !== block.builderIndex) { + throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, { + code: ExecutionPayloadEnvelopeErrorCode.BUILDER_INDEX_MISMATCH, + envelopeBuilderIndex: envelope.builderIndex, + bidBuilderIndex: block.builderIndex, + }); + } + + // [REJECT] `payload.block_hash == bid.block_hash` + if (toRootHex(payload.blockHash) !== block.blockHashHex) { + throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, { + code: ExecutionPayloadEnvelopeErrorCode.BLOCK_HASH_MISMATCH, + envelopeBlockHash: toRootHex(payload.blockHash), + bidBlockHash: block.blockHashHex, + }); + } + + // [REJECT] `signed_execution_payload_envelope.signature` is valid with respect to the builder's public key. + const state = chain.getHeadState() as CachedBeaconStateGloas; + const signatureSet = createSingleSignatureSetFromComponents( + PublicKey.fromBytes(state.builders.getReadonly(envelope.builderIndex).pubkey), + getExecutionPayloadEnvelopeSigningRoot(chain.config, envelope), + executionPayloadEnvelope.signature + ); + + if (!(await chain.bls.verifySignatureSets([signatureSet]))) { + throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, { + code: ExecutionPayloadEnvelopeErrorCode.INVALID_SIGNATURE, + }); + } + + chain.seenExecutionPayloadEnvelopes.add(blockRootHex, envelope.slot); +} diff --git a/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts b/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts new file mode 100644 index 000000000000..33e7db7bbcb5 --- /dev/null +++ b/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts @@ -0,0 +1,109 @@ +import { + CachedBeaconStateGloas, + computeEpochAtSlot, + createSingleSignatureSetFromComponents, + getPayloadAttestationDataSigningRoot, +} from "@lodestar/state-transition"; +import {RootHex, gloas, ssz} from "@lodestar/types"; +import {toRootHex} from "@lodestar/utils"; +import {GossipAction, PayloadAttestationError, PayloadAttestationErrorCode} from "../errors/index.js"; +import {IBeaconChain} from "../index.js"; + +export type PayloadAttestationValidationResult = { + attDataRootHex: RootHex; + validatorCommitteeIndex: number; +}; + +export async function validateApiPayloadAttestationMessage( + chain: IBeaconChain, + payloadAttestationMessage: gloas.PayloadAttestationMessage +): Promise { + return validatePayloadAttestationMessage(chain, payloadAttestationMessage); +} + +export async function validateGossipPayloadAttestationMessage( + chain: IBeaconChain, + payloadAttestationMessage: gloas.PayloadAttestationMessage +): Promise { + return validatePayloadAttestationMessage(chain, payloadAttestationMessage); +} + +async function validatePayloadAttestationMessage( + chain: IBeaconChain, + payloadAttestationMessage: gloas.PayloadAttestationMessage +): Promise { + const {data, validatorIndex} = payloadAttestationMessage; + const epoch = computeEpochAtSlot(data.slot); + + // [IGNORE] The message's slot is for the current slot (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance), i.e. `data.slot == current_slot`. + if (!chain.clock.isCurrentSlotGivenGossipDisparity(data.slot)) { + throw new PayloadAttestationError(GossipAction.IGNORE, { + code: PayloadAttestationErrorCode.NOT_CURRENT_SLOT, + currentSlot: chain.clock.currentSlot, + slot: data.slot, + }); + } + + // [IGNORE] The `payload_attestation_message` is the first valid message received + // from the validator with index `payload_attestation_message.validator_index`. + // A single validator can participate PTC at most once per epoch + if (chain.seenPayloadAttesters.isKnown(epoch, validatorIndex)) { + throw new PayloadAttestationError(GossipAction.IGNORE, { + code: PayloadAttestationErrorCode.PAYLOAD_ATTESTATION_ALREADY_KNOWN, + validatorIndex, + slot: data.slot, + blockRoot: toRootHex(data.beaconBlockRoot), + }); + } + + // [IGNORE] The message's block `data.beacon_block_root` has been seen (via + // gossip or non-gossip sources) (a client MAY queue attestation for processing + // once the block is retrieved. Note a client might want to request payload after). + const block = chain.forkChoice.getBlock(data.beaconBlockRoot); + if (block === null) { + throw new PayloadAttestationError(GossipAction.IGNORE, { + code: PayloadAttestationErrorCode.UNKNOWN_BLOCK_ROOT, + blockRoot: toRootHex(data.beaconBlockRoot), + }); + } + + const state = chain.getHeadState() as CachedBeaconStateGloas; + + // [REJECT] The message's block `data.beacon_block_root` passes validation. + // TODO GLOAS: implement this. Technically if we cannot get proto block from fork choice, + // it is possible that the block didn't pass the validation + + // [REJECT] The message's validator index is within the payload committee in + // `get_ptc(state, data.slot)`. The `state` is the head state corresponding to + // processing the block up to the current slot as determined by the fork choice. + const ptc = state.epochCtx.getPayloadTimelinessCommittee(data.slot); + const validatorCommitteeIndex = ptc.indexOf(validatorIndex); + + if (validatorCommitteeIndex === -1) { + throw new PayloadAttestationError(GossipAction.REJECT, { + code: PayloadAttestationErrorCode.INVALID_ATTESTER, + attesterIndex: validatorIndex, + }); + } + + // [REJECT] `payload_attestation_message.signature` is valid with respect to the validator's public key. + const signatureSet = createSingleSignatureSetFromComponents( + chain.index2pubkey[validatorIndex], + getPayloadAttestationDataSigningRoot(chain.config, state.slot, data), + payloadAttestationMessage.signature + ); + + if (!(await chain.bls.verifySignatureSets([signatureSet]))) { + throw new PayloadAttestationError(GossipAction.REJECT, { + code: PayloadAttestationErrorCode.INVALID_SIGNATURE, + }); + } + + // Valid + chain.seenPayloadAttesters.add(epoch, validatorIndex); + + return { + attDataRootHex: toRootHex(ssz.gloas.PayloadAttestationData.hashTreeRoot(data)), + validatorCommitteeIndex, + }; +} diff --git a/packages/beacon-node/src/chain/validation/proposerSlashing.ts b/packages/beacon-node/src/chain/validation/proposerSlashing.ts index 231c5d5bf18c..350f2e701668 100644 --- a/packages/beacon-node/src/chain/validation/proposerSlashing.ts +++ b/packages/beacon-node/src/chain/validation/proposerSlashing.ts @@ -35,8 +35,9 @@ async function validateProposerSlashing( // [REJECT] All of the conditions within process_proposer_slashing pass validation. try { + const proposer = state.validators.getReadonly(proposerSlashing.signedHeader1.message.proposerIndex); // verifySignature = false, verified in batch below - assertValidProposerSlashing(state, proposerSlashing, false); + assertValidProposerSlashing(chain.config, chain.index2pubkey, state.slot, proposerSlashing, proposer, false); } catch (e) { throw new ProposerSlashingError(GossipAction.REJECT, { code: ProposerSlashingErrorCode.INVALID, @@ -44,12 +45,7 @@ async function validateProposerSlashing( }); } - const signatureSets = getProposerSlashingSignatureSets( - chain.config, - chain.index2pubkey, - state.slot, - proposerSlashing - ); + const signatureSets = getProposerSlashingSignatureSets(chain.config, state.slot, proposerSlashing); if (!(await chain.bls.verifySignatureSets(signatureSets, {batchable: true, priority: prioritizeBls}))) { throw new ProposerSlashingError(GossipAction.REJECT, { code: ProposerSlashingErrorCode.INVALID, diff --git a/packages/beacon-node/src/chain/validation/signatureSets/aggregateAndProof.ts b/packages/beacon-node/src/chain/validation/signatureSets/aggregateAndProof.ts index 9726241c37ec..fb42834ab424 100644 --- a/packages/beacon-node/src/chain/validation/signatureSets/aggregateAndProof.ts +++ b/packages/beacon-node/src/chain/validation/signatureSets/aggregateAndProof.ts @@ -1,13 +1,7 @@ -import {PublicKey} from "@chainsafe/blst"; import {BeaconConfig} from "@lodestar/config"; import {DOMAIN_AGGREGATE_AND_PROOF, ForkSeq} from "@lodestar/params"; -import { - ISignatureSet, - computeSigningRoot, - computeStartSlotAtEpoch, - createSingleSignatureSetFromComponents, -} from "@lodestar/state-transition"; -import {Epoch, SignedAggregateAndProof, ssz} from "@lodestar/types"; +import {ISignatureSet, SignatureSetType, computeSigningRoot, computeStartSlotAtEpoch} from "@lodestar/state-transition"; +import {Epoch, SignedAggregateAndProof, ValidatorIndex, ssz} from "@lodestar/types"; export function getAggregateAndProofSigningRoot( config: BeaconConfig, @@ -27,12 +21,13 @@ export function getAggregateAndProofSigningRoot( export function getAggregateAndProofSignatureSet( config: BeaconConfig, epoch: Epoch, - aggregator: PublicKey, + aggregatorIndex: ValidatorIndex, aggregateAndProof: SignedAggregateAndProof ): ISignatureSet { - return createSingleSignatureSetFromComponents( - aggregator, - getAggregateAndProofSigningRoot(config, epoch, aggregateAndProof), - aggregateAndProof.signature - ); + return { + type: SignatureSetType.indexed, + index: aggregatorIndex, + signingRoot: getAggregateAndProofSigningRoot(config, epoch, aggregateAndProof), + signature: aggregateAndProof.signature, + }; } diff --git a/packages/beacon-node/src/chain/validation/signatureSets/contributionAndProof.ts b/packages/beacon-node/src/chain/validation/signatureSets/contributionAndProof.ts index 5495a08e8ecb..6de3ecd64ed5 100644 --- a/packages/beacon-node/src/chain/validation/signatureSets/contributionAndProof.ts +++ b/packages/beacon-node/src/chain/validation/signatureSets/contributionAndProof.ts @@ -3,7 +3,6 @@ import {DOMAIN_CONTRIBUTION_AND_PROOF} from "@lodestar/params"; import { CachedBeaconStateAllForks, ISignatureSet, - Index2PubkeyCache, SignatureSetType, computeSigningRoot, } from "@lodestar/state-transition"; @@ -11,7 +10,6 @@ import {altair, ssz} from "@lodestar/types"; export function getContributionAndProofSignatureSet( config: BeaconConfig, - index2pubkey: Index2PubkeyCache, state: CachedBeaconStateAllForks, signedContributionAndProof: altair.SignedContributionAndProof ): ISignatureSet { @@ -22,8 +20,8 @@ export function getContributionAndProofSignatureSet( ); const signingData = signedContributionAndProof.message; return { - type: SignatureSetType.single, - pubkey: index2pubkey[signedContributionAndProof.message.aggregatorIndex], + type: SignatureSetType.indexed, + index: signedContributionAndProof.message.aggregatorIndex, signingRoot: computeSigningRoot(ssz.altair.ContributionAndProof, signingData, domain), signature: signedContributionAndProof.signature, }; diff --git a/packages/beacon-node/src/chain/validation/signatureSets/selectionProof.ts b/packages/beacon-node/src/chain/validation/signatureSets/selectionProof.ts index 6aa0cffc9b94..40aacaa22cc1 100644 --- a/packages/beacon-node/src/chain/validation/signatureSets/selectionProof.ts +++ b/packages/beacon-node/src/chain/validation/signatureSets/selectionProof.ts @@ -1,8 +1,7 @@ -import {PublicKey} from "@chainsafe/blst"; import {BeaconConfig} from "@lodestar/config"; import {DOMAIN_SELECTION_PROOF} from "@lodestar/params"; -import {ISignatureSet, computeSigningRoot, createSingleSignatureSetFromComponents} from "@lodestar/state-transition"; -import {Slot, phase0, ssz} from "@lodestar/types"; +import {ISignatureSet, SignatureSetType, computeSigningRoot} from "@lodestar/state-transition"; +import {Slot, ValidatorIndex, phase0, ssz} from "@lodestar/types"; export function getSelectionProofSigningRoot(config: BeaconConfig, slot: Slot): Uint8Array { // previously, we call `const selectionProofDomain = config.getDomain(state.slot, DOMAIN_SELECTION_PROOF, slot)` @@ -16,12 +15,13 @@ export function getSelectionProofSigningRoot(config: BeaconConfig, slot: Slot): export function getSelectionProofSignatureSet( config: BeaconConfig, slot: Slot, - aggregator: PublicKey, + aggregatorIndex: ValidatorIndex, aggregateAndProof: phase0.SignedAggregateAndProof ): ISignatureSet { - return createSingleSignatureSetFromComponents( - aggregator, - getSelectionProofSigningRoot(config, slot), - aggregateAndProof.message.selectionProof - ); + return { + type: SignatureSetType.indexed, + index: aggregatorIndex, + signingRoot: getSelectionProofSigningRoot(config, slot), + signature: aggregateAndProof.message.selectionProof, + }; } diff --git a/packages/beacon-node/src/chain/validation/signatureSets/syncCommittee.ts b/packages/beacon-node/src/chain/validation/signatureSets/syncCommittee.ts index f91dbd555688..df018739a669 100644 --- a/packages/beacon-node/src/chain/validation/signatureSets/syncCommittee.ts +++ b/packages/beacon-node/src/chain/validation/signatureSets/syncCommittee.ts @@ -3,7 +3,6 @@ import {DOMAIN_SYNC_COMMITTEE} from "@lodestar/params"; import { CachedBeaconStateAllForks, ISignatureSet, - Index2PubkeyCache, SignatureSetType, computeSigningRoot, } from "@lodestar/state-transition"; @@ -11,15 +10,14 @@ import {altair, ssz} from "@lodestar/types"; export function getSyncCommitteeSignatureSet( config: BeaconConfig, - index2pubkey: Index2PubkeyCache, state: CachedBeaconStateAllForks, syncCommittee: altair.SyncCommitteeMessage ): ISignatureSet { const domain = config.getDomain(state.slot, DOMAIN_SYNC_COMMITTEE, syncCommittee.slot); return { - type: SignatureSetType.single, - pubkey: index2pubkey[syncCommittee.validatorIndex], + type: SignatureSetType.indexed, + index: syncCommittee.validatorIndex, signingRoot: computeSigningRoot(ssz.Root, syncCommittee.beaconBlockRoot, domain), signature: syncCommittee.signature, }; diff --git a/packages/beacon-node/src/chain/validation/signatureSets/syncCommitteeContribution.ts b/packages/beacon-node/src/chain/validation/signatureSets/syncCommitteeContribution.ts index a105078e3340..b9b1c4544f94 100644 --- a/packages/beacon-node/src/chain/validation/signatureSets/syncCommitteeContribution.ts +++ b/packages/beacon-node/src/chain/validation/signatureSets/syncCommitteeContribution.ts @@ -1,4 +1,3 @@ -import {PublicKey} from "@chainsafe/blst"; import {BeaconConfig} from "@lodestar/config"; import {DOMAIN_SYNC_COMMITTEE} from "@lodestar/params"; import {CachedBeaconStateAltair, ISignatureSet, SignatureSetType, computeSigningRoot} from "@lodestar/state-transition"; @@ -8,12 +7,12 @@ export function getSyncCommitteeContributionSignatureSet( config: BeaconConfig, state: CachedBeaconStateAltair, contribution: altair.SyncCommitteeContribution, - pubkeys: PublicKey[] + participantIndices: number[] ): ISignatureSet { const domain = config.getDomain(state.slot, DOMAIN_SYNC_COMMITTEE, contribution.slot); return { type: SignatureSetType.aggregate, - pubkeys, + indices: participantIndices, signingRoot: computeSigningRoot(ssz.Root, contribution.beaconBlockRoot, domain), signature: contribution.signature, }; diff --git a/packages/beacon-node/src/chain/validation/signatureSets/syncCommitteeSelectionProof.ts b/packages/beacon-node/src/chain/validation/signatureSets/syncCommitteeSelectionProof.ts index 8b93b4259c1e..9bf1371ade14 100644 --- a/packages/beacon-node/src/chain/validation/signatureSets/syncCommitteeSelectionProof.ts +++ b/packages/beacon-node/src/chain/validation/signatureSets/syncCommitteeSelectionProof.ts @@ -3,7 +3,6 @@ import {DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF} from "@lodestar/params"; import { CachedBeaconStateAllForks, ISignatureSet, - Index2PubkeyCache, SignatureSetType, computeSigningRoot, } from "@lodestar/state-transition"; @@ -11,7 +10,6 @@ import {altair, ssz} from "@lodestar/types"; export function getSyncCommitteeSelectionProofSignatureSet( config: BeaconConfig, - index2pubkey: Index2PubkeyCache, state: CachedBeaconStateAllForks, contributionAndProof: altair.ContributionAndProof ): ISignatureSet { @@ -22,8 +20,8 @@ export function getSyncCommitteeSelectionProofSignatureSet( subcommitteeIndex: contributionAndProof.contribution.subcommitteeIndex, }; return { - type: SignatureSetType.single, - pubkey: index2pubkey[contributionAndProof.aggregatorIndex], + type: SignatureSetType.indexed, + index: contributionAndProof.aggregatorIndex, signingRoot: computeSigningRoot(ssz.altair.SyncAggregatorSelectionData, signingData, domain), signature: contributionAndProof.selectionProof, }; diff --git a/packages/beacon-node/src/chain/validation/syncCommittee.ts b/packages/beacon-node/src/chain/validation/syncCommittee.ts index c3606ca40fee..d1b5c0b31e84 100644 --- a/packages/beacon-node/src/chain/validation/syncCommittee.ts +++ b/packages/beacon-node/src/chain/validation/syncCommittee.ts @@ -89,7 +89,7 @@ async function validateSyncCommitteeSigOnly( syncCommittee: altair.SyncCommitteeMessage, prioritizeBls = false ): Promise { - const signatureSet = getSyncCommitteeSignatureSet(chain.config, chain.index2pubkey, headState, syncCommittee); + const signatureSet = getSyncCommitteeSignatureSet(chain.config, headState, syncCommittee); if (!(await chain.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) { throw new SyncCommitteeError(GossipAction.REJECT, { code: SyncCommitteeErrorCode.INVALID_SIGNATURE, diff --git a/packages/beacon-node/src/chain/validation/syncCommitteeContributionAndProof.ts b/packages/beacon-node/src/chain/validation/syncCommitteeContributionAndProof.ts index 577c684f326a..7d94b6bf8a53 100644 --- a/packages/beacon-node/src/chain/validation/syncCommitteeContributionAndProof.ts +++ b/packages/beacon-node/src/chain/validation/syncCommitteeContributionAndProof.ts @@ -21,7 +21,6 @@ export async function validateSyncCommitteeGossipContributionAndProof( const contributionAndProof = signedContributionAndProof.message; const {contribution, aggregatorIndex} = contributionAndProof; const {subcommitteeIndex, slot} = contribution; - const {index2pubkey} = chain; const headState = chain.getHeadState(); validateGossipSyncCommitteeExceptSig(chain, headState, subcommitteeIndex, { @@ -74,14 +73,13 @@ export async function validateSyncCommitteeGossipContributionAndProof( // i.e. state.validators[contribution_and_proof.aggregator_index].pubkey in get_sync_subcommittee_pubkeys(state, contribution.subcommittee_index). // > Checked in validateGossipSyncCommitteeExceptSig() - const participantPubkeys = syncCommitteeParticipantIndices.map((validatorIndex) => index2pubkey[validatorIndex]); const signatureSets = [ // [REJECT] The contribution_and_proof.selection_proof is a valid signature of the SyncAggregatorSelectionData // derived from the contribution by the validator with index contribution_and_proof.aggregator_index. - getSyncCommitteeSelectionProofSignatureSet(chain.config, index2pubkey, headState, contributionAndProof), + getSyncCommitteeSelectionProofSignatureSet(chain.config, headState, contributionAndProof), // [REJECT] The aggregator signature, signed_contribution_and_proof.signature, is valid. - getContributionAndProofSignatureSet(chain.config, index2pubkey, headState, signedContributionAndProof), + getContributionAndProofSignatureSet(chain.config, headState, signedContributionAndProof), // [REJECT] The aggregate signature is valid for the message beacon_block_root and aggregate pubkey derived from // the participation info in aggregation_bits for the subcommittee specified by the contribution.subcommittee_index. @@ -89,7 +87,7 @@ export async function validateSyncCommitteeGossipContributionAndProof( chain.config, headState as CachedBeaconStateAltair, contribution, - participantPubkeys + syncCommitteeParticipantIndices ), ]; diff --git a/packages/beacon-node/src/chain/validation/voluntaryExit.ts b/packages/beacon-node/src/chain/validation/voluntaryExit.ts index 537738053a3a..3779989c87b6 100644 --- a/packages/beacon-node/src/chain/validation/voluntaryExit.ts +++ b/packages/beacon-node/src/chain/validation/voluntaryExit.ts @@ -59,7 +59,7 @@ async function validateVoluntaryExit( }); } - const signatureSet = getVoluntaryExitSignatureSet(chain.config, chain.index2pubkey, state.slot, voluntaryExit); + const signatureSet = getVoluntaryExitSignatureSet(chain.config, state.slot, voluntaryExit); if (!(await chain.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) { throw new VoluntaryExitError(GossipAction.REJECT, { code: VoluntaryExitErrorCode.INVALID_SIGNATURE, diff --git a/packages/beacon-node/src/chain/validatorMonitor.ts b/packages/beacon-node/src/chain/validatorMonitor.ts index 9845f7a2c047..0b7a2c6a0381 100644 --- a/packages/beacon-node/src/chain/validatorMonitor.ts +++ b/packages/beacon-node/src/chain/validatorMonitor.ts @@ -24,7 +24,7 @@ import { altair, deneb, } from "@lodestar/types"; -import {LogData, LogHandler, LogLevel, Logger, MapDef, MapDefMax, toRootHex} from "@lodestar/utils"; +import {LogData, LogHandler, LogLevel, Logger, MapDef, MapDefMax, prettyPrintIndices, toRootHex} from "@lodestar/utils"; import {GENESIS_SLOT} from "../constants/constants.js"; import {RegistryMetricCreator} from "../metrics/index.js"; @@ -39,7 +39,8 @@ const MAX_CACHED_DISTINCT_TARGETS = 4; const LATE_ATTESTATION_SUBMISSION_BPS = 5000; const LATE_BLOCK_SUBMISSION_BPS = 2500; -const RETAIN_REGISTERED_VALIDATORS_MS = 1 * 3600 * 1000; // 1 hour +/** Number of epochs to retain registered validators after their last registration */ +const RETAIN_REGISTERED_VALIDATORS_EPOCHS = 2; type Seconds = number; export enum OpSource { @@ -97,6 +98,8 @@ export type ValidatorMonitor = { ): void; onceEveryEndOfEpoch(state: CachedBeaconStateAllForks): void; scrapeMetrics(slotClock: Slot): void; + /** Returns the list of validator indices currently being monitored */ + getMonitoredValidatorIndices(): ValidatorIndex[]; }; export type ValidatorMonitorOpts = { @@ -284,6 +287,9 @@ export function createValidatorMonitor( logger[logLevel](message, context); }; + // Calculate retain time dynamically based on slot duration (2 epochs) + const retainRegisteredValidatorsMs = SLOTS_PER_EPOCH * config.SLOT_DURATION_MS * RETAIN_REGISTERED_VALIDATORS_EPOCHS; + /** The validators that require additional monitoring. */ const validators = new MapDef(() => ({ summaries: new Map(), @@ -306,11 +312,19 @@ export function createValidatorMonitor( let lastRegisteredStatusEpoch = -1; + // Track validator additions/removals per epoch for logging + const addedValidatorsInEpoch: Set = new Set(); + const removedValidatorsInEpoch: Set = new Set(); + const validatorMonitorMetrics = metricsRegister ? createValidatorMonitorMetrics(metricsRegister) : null; const validatorMonitor: ValidatorMonitor = { registerLocalValidator(index) { + const isNewValidator = !validators.has(index); validators.getOrDefault(index).lastRegisteredTimeMs = Date.now(); + if (isNewValidator) { + addedValidatorsInEpoch.add(index); + } }, registerLocalValidatorInSyncCommittee(index, untilEpoch) { @@ -333,6 +347,9 @@ export function createValidatorMonitor( return; } + // Track total balance instead of per-validator balance to reduce metric cardinality + let totalBalance = 0; + for (const [index, monitoredValidator] of validators.entries()) { // We subtract two from the state of the epoch that generated these summaries. // @@ -391,7 +408,7 @@ export function createValidatorMonitor( const balance = balances?.[index]; if (balance !== undefined) { - validatorMonitorMetrics?.prevEpochOnChainBalance.set({index}, balance); + totalBalance += balance; } if (!summary.isPrevSourceAttester || !summary.isPrevTargetAttester || !summary.isPrevHeadAttester) { @@ -406,6 +423,10 @@ export function createValidatorMonitor( }); } } + + if (balances !== undefined) { + validatorMonitorMetrics?.prevEpochOnChainBalance.set(totalBalance); + } }, registerBeaconBlock(src, delaySec, block) { @@ -673,11 +694,29 @@ export function createValidatorMonitor( // Prune validators not seen in a while for (const [index, validator] of validators.entries()) { - if (Date.now() - validator.lastRegisteredTimeMs > RETAIN_REGISTERED_VALIDATORS_MS) { + if (Date.now() - validator.lastRegisteredTimeMs > retainRegisteredValidatorsMs) { validators.delete(index); + removedValidatorsInEpoch.add(index); } } + // Log validator monitor status every epoch + const allIndices = Array.from(validators.keys()).sort((a, b) => a - b); + const addedIndices = Array.from(addedValidatorsInEpoch).sort((a, b) => a - b); + const removedIndices = Array.from(removedValidatorsInEpoch).sort((a, b) => a - b); + + log("Validator monitor status", { + epoch: computeEpochAtSlot(headState.slot), + added: addedIndices.length > 0 ? prettyPrintIndices(addedIndices) : "none", + removed: removedIndices.length > 0 ? prettyPrintIndices(removedIndices) : "none", + total: validators.size, + indices: prettyPrintIndices(allIndices), + }); + + // Clear tracking sets for next epoch + addedValidatorsInEpoch.clear(); + removedValidatorsInEpoch.clear(); + // Compute summaries of previous epoch attestation performance const prevEpoch = computeEpochAtSlot(headState.slot) - 1; @@ -736,6 +775,13 @@ export function createValidatorMonitor( scrapeMetrics(slotClock) { validatorMonitorMetrics?.validatorsConnected.set(validators.size); + // Update static metric with connected validator indices + if (validatorMonitorMetrics?.validatorsConnectedIndices) { + validatorMonitorMetrics.validatorsConnectedIndices.reset(); + const allIndices = Array.from(validators.keys()).sort((a, b) => a - b); + validatorMonitorMetrics.validatorsConnectedIndices.set({indices: prettyPrintIndices(allIndices)}, 1); + } + const epoch = computeEpochAtSlot(slotClock); const slotInEpoch = slotClock % SLOTS_PER_EPOCH; @@ -815,6 +861,10 @@ export function createValidatorMonitor( validatorMonitorMetrics?.prevEpochSyncCommitteeHits.set(prevEpochSyncCommitteeHits); validatorMonitorMetrics?.prevEpochSyncCommitteeMisses.set(prevEpochSyncCommitteeMisses); }, + + getMonitoredValidatorIndices() { + return Array.from(validators.keys()).sort((a, b) => a - b); + }, }; // Register a single collect() function to run all validatorMonitor metrics @@ -1098,17 +1148,21 @@ function createValidatorMonitorMetrics(register: RegistryMetricCreator) { help: "Count of validators that are specifically monitored by this beacon node", }), + validatorsConnectedIndices: register.gauge<{indices: string}>({ + name: "validator_monitor_indices", + help: "Static metric with connected validator indices as label, value is always 1", + labelNames: ["indices"], + }), + validatorsInSyncCommittee: register.gauge({ name: "validator_monitor_validators_in_sync_committee", help: "Count of validators monitored by this beacon node that are part of sync committee", }), // Validator Monitor Metrics (per-epoch summaries) - // Only track prevEpochOnChainBalance per index - prevEpochOnChainBalance: register.gauge<{index: number}>({ + prevEpochOnChainBalance: register.gauge({ name: "validator_monitor_prev_epoch_on_chain_balance", - help: "Balance of validator after an epoch", - labelNames: ["index"], + help: "Total balance of all monitored validators after an epoch", }), prevEpochOnChainAttesterHit: register.gauge({ name: "validator_monitor_prev_epoch_on_chain_attester_hit_total", diff --git a/packages/beacon-node/src/db/repositories/checkpointState.ts b/packages/beacon-node/src/db/repositories/checkpointState.ts index 7bace3f3f3fc..bd40dc556250 100644 --- a/packages/beacon-node/src/db/repositories/checkpointState.ts +++ b/packages/beacon-node/src/db/repositories/checkpointState.ts @@ -1,31 +1,15 @@ import {ChainForkConfig} from "@lodestar/config"; -import {Db, Repository} from "@lodestar/db"; -import {BeaconStateAllForks} from "@lodestar/state-transition"; -import {ssz} from "@lodestar/types"; +import {BinaryRepository, Db} from "@lodestar/db"; import {Bucket, getBucketNameByValue} from "../buckets.js"; /** * Store temporary checkpoint states. * We should only put/get binary data from this repository, consumer will load it into an existing state ViewDU object. */ -export class CheckpointStateRepository extends Repository { +export class CheckpointStateRepository extends BinaryRepository { constructor(config: ChainForkConfig, db: Db) { - // Pick some type but won't be used. Casted to any because no type can match `BeaconStateAllForks` - const type = ssz.phase0.BeaconState; const bucket = Bucket.allForks_checkpointState; - // biome-ignore lint/suspicious/noExplicitAny: The type is complex to specify a proper override - super(config, db, bucket, type as any, getBucketNameByValue(bucket)); - } - - getId(): Uint8Array { - throw Error("CheckpointStateRepository does not work with value"); - } - - encodeValue(): Uint8Array { - throw Error("CheckpointStateRepository does not work with value"); - } - decodeValue(): BeaconStateAllForks { - throw Error("CheckpointStateRepository does not work with value"); + super(config, db, bucket, getBucketNameByValue(bucket)); } } diff --git a/packages/beacon-node/src/db/repositories/stateArchive.ts b/packages/beacon-node/src/db/repositories/stateArchive.ts index 964936da6d67..b6080f718720 100644 --- a/packages/beacon-node/src/db/repositories/stateArchive.ts +++ b/packages/beacon-node/src/db/repositories/stateArchive.ts @@ -1,39 +1,25 @@ import {ChainForkConfig} from "@lodestar/config"; -import {Db, Repository} from "@lodestar/db"; -import {BeaconStateAllForks} from "@lodestar/state-transition"; -import {Epoch, Root, RootHex, Slot, ssz} from "@lodestar/types"; +import {BinaryRepository, Db} from "@lodestar/db"; +import {Root, RootHex, Slot} from "@lodestar/types"; import {bytesToInt, toHex} from "@lodestar/utils"; -import {getStateTypeFromBytes} from "../../util/multifork.js"; import {Bucket, getBucketNameByValue} from "../buckets.js"; import {getRootIndex, getRootIndexKey, storeRootIndex} from "./stateArchiveIndex.js"; -export class StateArchiveRepository extends Repository { +export type BeaconStateArchive = { + serialize(): Uint8Array; + hashTreeRoot(): Root; +}; + +export class StateArchiveRepository extends BinaryRepository { constructor(config: ChainForkConfig, db: Db) { - // Pick some type but won't be used. Casted to any because no type can match `BeaconStateAllForks` - // biome-ignore lint/suspicious/noExplicitAny: We need to use `any` type here - const type = ssz.phase0.BeaconState as any; const bucket = Bucket.allForks_stateArchive; - super(config, db, bucket, type, getBucketNameByValue(bucket)); - } - - // Overrides for multi-fork - - encodeValue(value: BeaconStateAllForks): Uint8Array { - return value.serialize(); - } - - decodeValue(data: Uint8Array): BeaconStateAllForks { - return getStateTypeFromBytes(this.config, data).deserializeToViewDU(data); + super(config, db, bucket, getBucketNameByValue(bucket)); } // Handle key as slot - async put(key: Slot, value: BeaconStateAllForks): Promise { - await Promise.all([super.put(key, value), storeRootIndex(this.db, key, value.hashTreeRoot())]); - } - - getId(state: BeaconStateAllForks): Epoch { - return state.slot; + async put(key: Slot, value: BeaconStateArchive): Promise { + await Promise.all([super.putBinary(key, value.serialize()), storeRootIndex(this.db, key, value.hashTreeRoot())]); } decodeKey(data: Uint8Array): number { @@ -42,10 +28,10 @@ export class StateArchiveRepository extends Repository Slot - async getByRoot(stateRoot: Root): Promise { + async getBinaryByRoot(stateRoot: Root): Promise { const slot = await this.getSlotByRoot(stateRoot); if (slot !== null && Number.isInteger(slot)) { - return this.get(slot); + return this.getBinary(slot); } return null; } diff --git a/packages/beacon-node/src/execution/engine/mock.ts b/packages/beacon-node/src/execution/engine/mock.ts index f57fa548e874..2501ff031339 100644 --- a/packages/beacon-node/src/execution/engine/mock.ts +++ b/packages/beacon-node/src/execution/engine/mock.ts @@ -1,4 +1,5 @@ import crypto from "node:crypto"; +import {ChainConfig} from "@lodestar/config"; import { BLOB_TX_TYPE, BYTES_PER_FIELD_ELEMENT, @@ -7,7 +8,9 @@ import { ForkPostBellatrix, ForkPostCapella, ForkSeq, + SLOTS_PER_EPOCH, } from "@lodestar/params"; +import {computeTimeAtSlot} from "@lodestar/state-transition"; import {ExecutionPayload, RootHex, bellatrix, deneb, ssz} from "@lodestar/types"; import {fromHex, toRootHex} from "@lodestar/utils"; import {ZERO_HASH_HEX} from "../../constants/index.js"; @@ -34,14 +37,11 @@ const INTEROP_GAS_LIMIT = 30e6; const PRUNE_PAYLOAD_ID_AFTER_MS = 5000; export type ExecutionEngineMockOpts = { - genesisBlockHash: string; + genesisBlockHash?: string; eth1BlockHash?: string; onlyPredefinedResponses?: boolean; - capellaForkTimestamp?: number; - denebForkTimestamp?: number; - electraForkTimestamp?: number; - fuluForkTimestamp?: number; - gloasForkTimestamp?: number; + genesisTime?: number; + config?: ChainConfig; }; type ExecutionBlock = { @@ -74,17 +74,21 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend { /** Preparing payloads to be retrieved via engine_getPayloadV1 */ private readonly preparingPayloads = new Map(); private readonly payloadsForDeletion = new Map(); - private readonly predefinedPayloadStatuses = new Map(); private payloadId = 0; + private capellaForkTimestamp: number; + private denebForkTimestamp: number; + private electraForkTimestamp: number; + private fuluForkTimestamp: number; + private gloasForkTimestamp: number; readonly handlers: { [K in keyof EngineApiRpcParamTypes]: (...args: EngineApiRpcParamTypes[K]) => EngineApiRpcReturnTypes[K]; }; constructor(private readonly opts: ExecutionEngineMockOpts) { - this.validBlocks.set(opts.genesisBlockHash, { + this.validBlocks.set(opts.genesisBlockHash ?? ZERO_HASH_HEX, { parentHash: ZERO_HASH_HEX, blockHash: ZERO_HASH_HEX, timestamp: 0, @@ -100,6 +104,29 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend { blockNumber: 1, }); + const {config} = opts; + + this.capellaForkTimestamp = + opts.genesisTime && config + ? computeTimeAtSlot(config, config.CAPELLA_FORK_EPOCH * SLOTS_PER_EPOCH, opts.genesisTime) + : Infinity; + this.denebForkTimestamp = + opts.genesisTime && config + ? computeTimeAtSlot(config, config.DENEB_FORK_EPOCH * SLOTS_PER_EPOCH, opts.genesisTime) + : Infinity; + this.electraForkTimestamp = + opts.genesisTime && config + ? computeTimeAtSlot(config, config.ELECTRA_FORK_EPOCH * SLOTS_PER_EPOCH, opts.genesisTime) + : Infinity; + this.fuluForkTimestamp = + opts.genesisTime && config + ? computeTimeAtSlot(config, config.FULU_FORK_EPOCH * SLOTS_PER_EPOCH, opts.genesisTime) + : Infinity; + this.gloasForkTimestamp = + opts.genesisTime && config + ? computeTimeAtSlot(config, config.GLOAS_FORK_EPOCH * SLOTS_PER_EPOCH, opts.genesisTime) + : Infinity; + this.handlers = { engine_newPayloadV1: this.notifyNewPayload.bind(this), engine_newPayloadV2: this.notifyNewPayload.bind(this), @@ -448,11 +475,11 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend { } private timestampToFork(timestamp: number): ForkPostBellatrix { - if (timestamp >= (this.opts.gloasForkTimestamp ?? Infinity)) return ForkName.gloas; - if (timestamp >= (this.opts.fuluForkTimestamp ?? Infinity)) return ForkName.fulu; - if (timestamp >= (this.opts.electraForkTimestamp ?? Infinity)) return ForkName.electra; - if (timestamp >= (this.opts.denebForkTimestamp ?? Infinity)) return ForkName.deneb; - if (timestamp >= (this.opts.capellaForkTimestamp ?? Infinity)) return ForkName.capella; + if (timestamp >= this.gloasForkTimestamp) return ForkName.gloas; + if (timestamp >= this.fuluForkTimestamp) return ForkName.fulu; + if (timestamp >= this.electraForkTimestamp) return ForkName.electra; + if (timestamp >= this.denebForkTimestamp) return ForkName.deneb; + if (timestamp >= this.capellaForkTimestamp) return ForkName.capella; return ForkName.bellatrix; } } diff --git a/packages/beacon-node/src/index.ts b/packages/beacon-node/src/index.ts index d85fc6c20155..5c4a5398d10c 100644 --- a/packages/beacon-node/src/index.ts +++ b/packages/beacon-node/src/index.ts @@ -2,7 +2,7 @@ export type {RestApiServerMetrics, RestApiServerModules, RestApiServerOpts} from "./api/rest/base.js"; export {RestApiServer} from "./api/rest/base.js"; -export {checkAndPersistAnchorState, initStateFromDb} from "./chain/index.js"; +export {checkAndPersistAnchorState} from "./chain/index.js"; export {DbCPStateDatastore} from "./chain/stateCache/datastore/db.js"; export {FileCPStateDatastore} from "./chain/stateCache/datastore/file.js"; export {BeaconDb, type IBeaconDb} from "./db/index.js"; diff --git a/packages/beacon-node/src/metrics/metrics/lodestar.ts b/packages/beacon-node/src/metrics/metrics/lodestar.ts index dd3b96093a3c..b4c3eab0efef 100644 --- a/packages/beacon-node/src/metrics/metrics/lodestar.ts +++ b/packages/beacon-node/src/metrics/metrics/lodestar.ts @@ -212,6 +212,31 @@ export function createLodestarMetrics( }), }, + unfinalizedBlockWritesQueue: { + length: register.gauge({ + name: "lodestar_unfinalized_block_writes_queue_length", + help: "Count of total unfinalized block writes queue length", + }), + droppedJobs: register.gauge({ + name: "lodestar_unfinalized_block_writes_queue_dropped_jobs_total", + help: "Count of total unfinalized block writes queue dropped jobs", + }), + jobTime: register.histogram({ + name: "lodestar_unfinalized_block_writes_queue_job_time_seconds", + help: "Time to process unfinalized block writes queue job in seconds", + buckets: [0.01, 0.1, 1, 4, 12], + }), + jobWaitTime: register.histogram({ + name: "lodestar_unfinalized_block_writes_queue_job_wait_time_seconds", + help: "Time from job added to the unfinalized block writes queue to starting in seconds", + buckets: [0.01, 0.1, 1, 4, 12], + }), + concurrency: register.gauge({ + name: "lodestar_unfinalized_block_writes_queue_concurrency", + help: "Current concurrency of unfinalized block writes queue", + }), + }, + engineHttpProcessorQueue: { length: register.gauge({ name: "lodestar_engine_http_processor_queue_length", @@ -1116,6 +1141,46 @@ export function createLodestarMetrics( help: "Total number of empty returns in SyncContributionAndProofPool.getAggregate(slot, root)", }), }, + payloadAttestationPool: { + size: register.gauge({ + name: "lodestar_oppool_payload_attestation_pool_size", + help: "Current size of the PayloadAttestationPool = total payload attestations unique by data and slot", + }), + payloadAttDataPerSlot: register.gauge({ + name: "lodestar_oppool_payload_attestation_pool_payload_attestation_data_per_slot_total", + help: "Total number of payload attestation data per slot in PayloadAttestationPool", + }), + gossipInsertOutcome: register.counter<{insertOutcome: InsertOutcome}>({ + name: "lodestar_oppool_payload_attestation_pool_gossip_insert_outcome_total", + help: "Total number of InsertOutcome as a result of adding a payload attestation message from gossip to the pool", + labelNames: ["insertOutcome"], + }), + apiInsertOutcome: register.counter<{insertOutcome: InsertOutcome}>({ + name: "lodestar_oppool_payload_attestation_pool_api_insert_outcome_total", + help: "Total number of InsertOutcome as a result of adding a payload attestation message from api to the pool", + labelNames: ["insertOutcome"], + }), + getPayloadAttestationsCacheMisses: register.counter({ + name: "lodestar_oppool_payload_attestation_pool_get_payload_attestations_cache_misses_total", + help: "Total number of getPayloadAttestationsForBlock calls with no aggregate for slot and payload attestation data root", + }), + }, + executionPayloadBidPool: { + size: register.gauge({ + name: "lodestar_oppool_execution_payload_bid_pool_size", + help: "Current size of the ExecutionPayloadBidPool = total number of bids", + }), + gossipInsertOutcome: register.counter<{insertOutcome: InsertOutcome}>({ + name: "lodestar_oppool_execution_payload_bid_pool_gossip_insert_outcome_total", + help: "Total number of InsertOutcome as a result of adding an execution payload bid from gossip to the pool", + labelNames: ["insertOutcome"], + }), + apiInsertOutcome: register.counter<{insertOutcome: InsertOutcome}>({ + name: "lodestar_oppool_execution_payload_bid_pool_api_insert_outcome_total", + help: "Total number of InsertOutcome as a result of adding an execution payload bid from api to the pool", + labelNames: ["insertOutcome"], + }), + }, }, chain: { @@ -1308,33 +1373,19 @@ export function createLodestarMetrics( name: "lodestar_shuffling_cache_miss_count", help: "Count of shuffling cache miss", }), - shufflingBuiltMultipleTimes: register.gauge({ - name: "lodestar_shuffling_cache_recalculated_shuffling_count", - help: "Count of shuffling that were build multiple times", - }), - shufflingPromiseNotResolvedAndThrownAway: register.gauge({ - name: "lodestar_shuffling_cache_promise_not_resolved_and_thrown_away_count", - help: "Count of shuffling cache promises that were discarded and the shuffling was built synchronously", + shufflingSetMultipleTimes: register.gauge({ + name: "lodestar_shuffling_cache_set_multiple_times_count", + help: "Count of shuffling that were set multiple times", }), shufflingPromiseNotResolved: register.gauge({ name: "lodestar_shuffling_cache_promise_not_resolved_count", help: "Count of shuffling cache promises that were requested before the promise was resolved", }), - nextShufflingNotOnEpochCache: register.gauge({ - name: "lodestar_shuffling_cache_next_shuffling_not_on_epoch_cache", - help: "The next shuffling was not on the epoch cache before the epoch transition", - }), shufflingPromiseResolutionTime: register.histogram({ name: "lodestar_shuffling_cache_promise_resolution_time_seconds", help: "Time from promise insertion until promise resolution when shuffling was ready in seconds", buckets: [0.5, 1, 1.5, 2], }), - shufflingCalculationTime: register.histogram<{source: "build" | "getSync"}>({ - name: "lodestar_shuffling_cache_shuffling_calculation_time_seconds", - help: "Run time of shuffling calculation", - buckets: [0.5, 0.75, 1, 1.25, 1.5], - labelNames: ["source"], - }), }, seenCache: { diff --git a/packages/beacon-node/src/network/core/networkCore.ts b/packages/beacon-node/src/network/core/networkCore.ts index 06481890ca4e..d5ec1f6e9abe 100644 --- a/packages/beacon-node/src/network/core/networkCore.ts +++ b/packages/beacon-node/src/network/core/networkCore.ts @@ -454,6 +454,18 @@ export class NetworkCore implements INetworkCore { await this.libp2p.hangUp(peerIdFromString(peerIdStr)); } + async addDirectPeer(peer: routes.lodestar.DirectPeer): Promise { + return this.gossip.addDirectPeer(peer); + } + + async removeDirectPeer(peerIdStr: PeerIdStr): Promise { + return this.gossip.removeDirectPeer(peerIdStr); + } + + async getDirectPeers(): Promise { + return this.gossip.getDirectPeers(); + } + private _dumpPeer(peerIdStr: string, connections: Connection[]): routes.lodestar.LodestarNodePeer { const peerData = this.peersData.connectedPeers.get(peerIdStr); const fork = this.config.getForkName(this.clock.currentSlot); diff --git a/packages/beacon-node/src/network/core/networkCoreWorker.ts b/packages/beacon-node/src/network/core/networkCoreWorker.ts index e5cf1ba4dda8..8dd8229d5b70 100644 --- a/packages/beacon-node/src/network/core/networkCoreWorker.ts +++ b/packages/beacon-node/src/network/core/networkCoreWorker.ts @@ -153,6 +153,9 @@ const libp2pWorkerApi: NetworkWorkerApi = { getConnectedPeerCount: () => core.getConnectedPeerCount(), connectToPeer: (peer, multiaddr) => core.connectToPeer(peer, multiaddr), disconnectPeer: (peer) => core.disconnectPeer(peer), + addDirectPeer: (peer) => core.addDirectPeer(peer), + removeDirectPeer: (peerId) => core.removeDirectPeer(peerId), + getDirectPeers: () => core.getDirectPeers(), dumpPeers: () => core.dumpPeers(), dumpPeer: (peerIdStr) => core.dumpPeer(peerIdStr), dumpPeerScoreStats: () => core.dumpPeerScoreStats(), diff --git a/packages/beacon-node/src/network/core/networkCoreWorkerHandler.ts b/packages/beacon-node/src/network/core/networkCoreWorkerHandler.ts index 5ce810a30f6c..194a7a9ea73a 100644 --- a/packages/beacon-node/src/network/core/networkCoreWorkerHandler.ts +++ b/packages/beacon-node/src/network/core/networkCoreWorkerHandler.ts @@ -247,6 +247,15 @@ export class WorkerNetworkCore implements INetworkCore { disconnectPeer(peer: PeerIdStr): Promise { return this.getApi().disconnectPeer(peer); } + addDirectPeer(peer: routes.lodestar.DirectPeer): Promise { + return this.getApi().addDirectPeer(peer); + } + removeDirectPeer(peerId: PeerIdStr): Promise { + return this.getApi().removeDirectPeer(peerId); + } + getDirectPeers(): Promise { + return this.getApi().getDirectPeers(); + } dumpPeers(): Promise { return this.getApi().dumpPeers(); } diff --git a/packages/beacon-node/src/network/core/types.ts b/packages/beacon-node/src/network/core/types.ts index f372bc686778..1763b2e6bd61 100644 --- a/packages/beacon-node/src/network/core/types.ts +++ b/packages/beacon-node/src/network/core/types.ts @@ -30,6 +30,12 @@ export interface INetworkCorePublic { // Debug connectToPeer(peer: PeerIdStr, multiaddr: MultiaddrStr[]): Promise; disconnectPeer(peer: PeerIdStr): Promise; + + // Direct peers management + addDirectPeer(peer: routes.lodestar.DirectPeer): Promise; + removeDirectPeer(peerId: PeerIdStr): Promise; + getDirectPeers(): Promise; + dumpPeers(): Promise; dumpPeer(peerIdStr: PeerIdStr): Promise; dumpPeerScoreStats(): Promise; diff --git a/packages/beacon-node/src/network/gossip/gossipsub.ts b/packages/beacon-node/src/network/gossip/gossipsub.ts index fef681887412..2018591f781e 100644 --- a/packages/beacon-node/src/network/gossip/gossipsub.ts +++ b/packages/beacon-node/src/network/gossip/gossipsub.ts @@ -1,7 +1,11 @@ +import {peerIdFromString} from "@libp2p/peer-id"; +import {multiaddr} from "@multiformats/multiaddr"; +import {ENR} from "@chainsafe/enr"; import {GossipSub, GossipsubEvents} from "@chainsafe/libp2p-gossipsub"; import {MetricsRegister, TopicLabel, TopicStrToLabel} from "@chainsafe/libp2p-gossipsub/metrics"; import {PeerScoreParams} from "@chainsafe/libp2p-gossipsub/score"; -import {SignaturePolicy, TopicStr} from "@chainsafe/libp2p-gossipsub/types"; +import {AddrInfo, SignaturePolicy, TopicStr} from "@chainsafe/libp2p-gossipsub/types"; +import {routes} from "@lodestar/api"; import {BeaconConfig, ForkBoundary} from "@lodestar/config"; import {ATTESTATION_SUBNET_COUNT, SLOTS_PER_EPOCH, SYNC_COMMITTEE_SUBNET_COUNT} from "@lodestar/params"; import {SubnetID} from "@lodestar/types"; @@ -55,6 +59,12 @@ export type Eth2GossipsubOpts = { disableFloodPublish?: boolean; skipParamsLog?: boolean; disableLightClientServer?: boolean; + /** + * Direct peers for GossipSub - these peers maintain permanent mesh connections without GRAFT/PRUNE. + * Supports multiaddr strings with peer ID (e.g., "/ip4/192.168.1.1/tcp/9000/p2p/16Uiu2HAmKLhW7...") + * or ENR strings (e.g., "enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOo...") + */ + directPeers?: string[]; }; export type ForkBoundaryLabel = string; @@ -78,6 +88,7 @@ export class Eth2Gossipsub extends GossipSub { private readonly logger: Logger; private readonly peersData: PeersData; private readonly events: NetworkEventBus; + private readonly libp2p: Libp2p; // Internal caches private readonly gossipTopicCache: GossipTopicCache; @@ -97,6 +108,9 @@ export class Eth2Gossipsub extends GossipSub { ); } + // Parse direct peers from multiaddr strings to AddrInfo objects + const directPeers = parseDirectPeers(opts.directPeers ?? [], logger); + // Gossipsub parameters defined here: // https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md#the-gossip-domain-gossipsub super(modules.libp2p.services.components, { @@ -106,6 +120,7 @@ export class Eth2Gossipsub extends GossipSub { Dlo: gossipsubDLow ?? GOSSIP_D_LOW, Dhi: gossipsubDHigh ?? GOSSIP_D_HIGH, Dlazy: 6, + directPeers, heartbeatInterval: GOSSIPSUB_HEARTBEAT_INTERVAL, fanoutTTL: 60 * 1000, mcacheLength: 6, @@ -146,6 +161,7 @@ export class Eth2Gossipsub extends GossipSub { this.logger = logger; this.peersData = peersData; this.events = events; + this.libp2p = modules.libp2p; this.gossipTopicCache = gossipTopicCache; this.addEventListener("gossipsub:message", this.onGossipsubMessage.bind(this)); @@ -328,6 +344,64 @@ export class Eth2Gossipsub extends GossipSub { this.reportMessageValidationResult(data.msgId, data.propagationSource, data.acceptance); }); } + + /** + * Add a peer as a direct peer at runtime. Accepts multiaddr with peer ID or ENR string. + * Direct peers maintain permanent mesh connections without GRAFT/PRUNE negotiation. + */ + async addDirectPeer(peerStr: routes.lodestar.DirectPeer): Promise { + const parsed = parseDirectPeers([peerStr], this.logger); + if (parsed.length === 0) { + return null; + } + + const {id: peerId, addrs} = parsed[0]; + const peerIdStr = peerId.toString(); + + // Prevent adding self as a direct peer + if (peerId.equals(this.libp2p.peerId)) { + this.logger.warn("Cannot add self as a direct peer", {peerId: peerIdStr}); + return null; + } + + // Direct peers need addresses to connect - reject if none provided + if (addrs.length === 0) { + this.logger.warn("Cannot add direct peer without addresses", {peerId: peerIdStr}); + return null; + } + + // Add addresses to peer store first so we can connect + try { + await this.libp2p.peerStore.merge(peerId, {multiaddrs: addrs}); + } catch (e) { + this.logger.warn("Failed to add direct peer addresses to peer store", {peerId: peerIdStr}, e as Error); + return null; + } + + // Add to direct peers set only after addresses are stored + this.direct.add(peerIdStr); + + this.logger.info("Added direct peer via API", {peerId: peerIdStr}); + return peerIdStr; + } + + /** + * Remove a peer from direct peers. + */ + removeDirectPeer(peerIdStr: string): boolean { + const removed = this.direct.delete(peerIdStr); + if (removed) { + this.logger.info("Removed direct peer via API", {peerId: peerIdStr}); + } + return removed; + } + + /** + * Get list of current direct peer IDs. + */ + getDirectPeers(): string[] { + return Array.from(this.direct); + } } /** @@ -381,3 +455,75 @@ function getForkBoundaryLabel(boundary: ForkBoundary): ForkBoundaryLabel { return label; } + +/** + * Parse direct peer strings into AddrInfo objects for GossipSub. + * Direct peers maintain permanent mesh connections without GRAFT/PRUNE negotiation. + * + * Supported formats: + * - Multiaddr with peer ID: `/ip4/192.168.1.1/tcp/9000/p2p/16Uiu2HAmKLhW7...` + * - ENR: `enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOo...` + * + * For multiaddrs, the string must contain a /p2p/ component with the peer ID. + * For ENRs, the TCP multiaddr and peer ID are extracted from the encoded record. + */ +export function parseDirectPeers(directPeerStrs: routes.lodestar.DirectPeer[], logger: Logger): AddrInfo[] { + const directPeers: AddrInfo[] = []; + + for (const peerStr of directPeerStrs) { + // Check if this is an ENR (starts with "enr:") + if (peerStr.startsWith("enr:")) { + try { + const enr = ENR.decodeTxt(peerStr); + const peerId = enr.peerId; + + // Get TCP multiaddr from ENR + const multiaddrTCP = enr.getLocationMultiaddr("tcp"); + if (!multiaddrTCP) { + logger.warn("ENR does not contain TCP multiaddr", {enr: peerStr}); + continue; + } + + directPeers.push({ + id: peerId, + addrs: [multiaddrTCP], + }); + + logger.info("Added direct peer from ENR", {peerId: peerId.toString(), addr: multiaddrTCP.toString()}); + } catch (e) { + logger.warn("Failed to parse direct peer ENR", {enr: peerStr}, e as Error); + } + } else { + // Parse as multiaddr + try { + const ma = multiaddr(peerStr); + + const peerIdStr = ma.getPeerId(); + if (!peerIdStr) { + logger.warn("Direct peer multiaddr must contain /p2p/ component with peer ID", {multiaddr: peerStr}); + continue; + } + + try { + const peerId = peerIdFromString(peerIdStr); + + // Get the address without the /p2p/ component + const addr = ma.decapsulate("/p2p/" + peerIdStr); + + directPeers.push({ + id: peerId, + addrs: [addr], + }); + + logger.info("Added direct peer", {peerId: peerIdStr, addr: addr.toString()}); + } catch (e) { + logger.warn("Invalid peer ID in direct peer multiaddr", {multiaddr: peerStr, peerId: peerIdStr}, e as Error); + } + } catch (e) { + logger.warn("Failed to parse direct peer multiaddr", {multiaddr: peerStr}, e as Error); + } + } + } + + return directPeers; +} diff --git a/packages/beacon-node/src/network/gossip/interface.ts b/packages/beacon-node/src/network/gossip/interface.ts index 145430db30bc..54904cd28e26 100644 --- a/packages/beacon-node/src/network/gossip/interface.ts +++ b/packages/beacon-node/src/network/gossip/interface.ts @@ -15,6 +15,7 @@ import { capella, deneb, fulu, + gloas, phase0, } from "@lodestar/types"; import {Logger} from "@lodestar/utils"; @@ -37,6 +38,9 @@ export enum GossipType { light_client_finality_update = "light_client_finality_update", light_client_optimistic_update = "light_client_optimistic_update", bls_to_execution_change = "bls_to_execution_change", + execution_payload = "execution_payload", + payload_attestation_message = "payload_attestation_message", + execution_payload_bid = "execution_payload_bid", } export type SequentialGossipType = Exclude; @@ -71,6 +75,9 @@ export type GossipTopicTypeMap = { [GossipType.light_client_finality_update]: {type: GossipType.light_client_finality_update}; [GossipType.light_client_optimistic_update]: {type: GossipType.light_client_optimistic_update}; [GossipType.bls_to_execution_change]: {type: GossipType.bls_to_execution_change}; + [GossipType.execution_payload]: {type: GossipType.execution_payload}; + [GossipType.payload_attestation_message]: {type: GossipType.payload_attestation_message}; + [GossipType.execution_payload_bid]: {type: GossipType.execution_payload_bid}; }; export type GossipTopicMap = { @@ -100,6 +107,9 @@ export type GossipTypeMap = { [GossipType.light_client_finality_update]: LightClientFinalityUpdate; [GossipType.light_client_optimistic_update]: LightClientOptimisticUpdate; [GossipType.bls_to_execution_change]: capella.SignedBLSToExecutionChange; + [GossipType.execution_payload]: gloas.SignedExecutionPayloadEnvelope; + [GossipType.payload_attestation_message]: gloas.PayloadAttestationMessage; + [GossipType.execution_payload_bid]: gloas.SignedExecutionPayloadBid; }; export type GossipFnByType = { @@ -124,6 +134,13 @@ export type GossipFnByType = { [GossipType.bls_to_execution_change]: ( blsToExecutionChange: capella.SignedBLSToExecutionChange ) => Promise | void; + [GossipType.execution_payload]: ( + executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope + ) => Promise | void; + [GossipType.payload_attestation_message]: ( + payloadAttestationMessage: gloas.PayloadAttestationMessage + ) => Promise | void; + [GossipType.execution_payload_bid]: (executionPayloadBid: gloas.SignedExecutionPayloadBid) => Promise | void; }; export type GossipFn = GossipFnByType[keyof GossipFnByType]; diff --git a/packages/beacon-node/src/network/gossip/scoringParameters.ts b/packages/beacon-node/src/network/gossip/scoringParameters.ts index fa263d6c33c6..b9f2f24adf03 100644 --- a/packages/beacon-node/src/network/gossip/scoringParameters.ts +++ b/packages/beacon-node/src/network/gossip/scoringParameters.ts @@ -5,7 +5,7 @@ import { defaultTopicScoreParams, } from "@chainsafe/libp2p-gossipsub/score"; import {BeaconConfig} from "@lodestar/config"; -import {ATTESTATION_SUBNET_COUNT, SLOTS_PER_EPOCH, TARGET_AGGREGATORS_PER_COMMITTEE} from "@lodestar/params"; +import {ATTESTATION_SUBNET_COUNT, PTC_SIZE, SLOTS_PER_EPOCH, TARGET_AGGREGATORS_PER_COMMITTEE} from "@lodestar/params"; import {computeCommitteeCount} from "@lodestar/state-transition"; import {getActiveForkBoundaries} from "../forks.js"; import {Eth2Context} from "./gossipsub.js"; @@ -24,6 +24,9 @@ const VOLUNTARY_EXIT_WEIGHT = 0.05; const PROPOSER_SLASHING_WEIGHT = 0.05; const ATTESTER_SLASHING_WEIGHT = 0.05; const BLS_TO_EXECUTION_CHANGE_WEIGHT = 0.05; +const EXECUTION_PAYLOAD_WEIGHT = 0.5; +const PAYLOAD_ATTESTATION_WEIGHT = 0.05; +const EXECUTION_PAYLOAD_BID_WEIGHT = 0.05; const beaconAttestationSubnetWeight = 1 / ATTESTATION_SUBNET_COUNT; const maxPositiveScore = @@ -34,7 +37,10 @@ const maxPositiveScore = VOLUNTARY_EXIT_WEIGHT + PROPOSER_SLASHING_WEIGHT + ATTESTER_SLASHING_WEIGHT + - BLS_TO_EXECUTION_CHANGE_WEIGHT); + BLS_TO_EXECUTION_CHANGE_WEIGHT + + EXECUTION_PAYLOAD_WEIGHT + + PAYLOAD_ATTESTATION_WEIGHT + + EXECUTION_PAYLOAD_BID_WEIGHT); /** * The following params is implemented by Lighthouse at @@ -172,6 +178,26 @@ function getAllTopicsScoreParams( expectedMessageRate: 1 / 5 / SLOTS_PER_EPOCH, firstMessageDecayTime: epochDurationMs * 100, }); + topicsParams[ + stringifyGossipTopic(config, { + type: GossipType.payload_attestation_message, + boundary, + }) + ] = getTopicScoreParams(config, precomputedParams, { + topicWeight: PAYLOAD_ATTESTATION_WEIGHT, + expectedMessageRate: PTC_SIZE, + firstMessageDecayTime: epochDurationMs * 100, + }); + topicsParams[ + stringifyGossipTopic(config, { + type: GossipType.execution_payload_bid, + boundary, + }) + ] = getTopicScoreParams(config, precomputedParams, { + topicWeight: EXECUTION_PAYLOAD_BID_WEIGHT, + expectedMessageRate: 1024, // TODO GLOAS: Need an estimate for this + firstMessageDecayTime: epochDurationMs * 100, + }); // other topics topicsParams[ @@ -190,6 +216,22 @@ function getAllTopicsScoreParams( currentSlot: eth2Context.currentSlot, }, }); + topicsParams[ + stringifyGossipTopic(config, { + type: GossipType.execution_payload, + boundary, + }) + ] = getTopicScoreParams(config, precomputedParams, { + topicWeight: EXECUTION_PAYLOAD_WEIGHT, + expectedMessageRate: 1, + firstMessageDecayTime: epochDurationMs * 20, + meshMessageInfo: { + decaySlots: SLOTS_PER_EPOCH * 5, + capFactor: 3, + activationWindow: epochDurationMs, + currentSlot: eth2Context.currentSlot, + }, + }); const activeValidatorCount = eth2Context.activeValidatorCount; const {aggregatorsPerslot, committeesPerSlot} = expectedAggregatorCountPerSlot(activeValidatorCount); diff --git a/packages/beacon-node/src/network/gossip/topic.ts b/packages/beacon-node/src/network/gossip/topic.ts index e460c02c9157..e81087288ada 100644 --- a/packages/beacon-node/src/network/gossip/topic.ts +++ b/packages/beacon-node/src/network/gossip/topic.ts @@ -69,6 +69,9 @@ function stringifyGossipTopicType(topic: GossipTopic): string { case GossipType.light_client_finality_update: case GossipType.light_client_optimistic_update: case GossipType.bls_to_execution_change: + case GossipType.execution_payload: + case GossipType.payload_attestation_message: + case GossipType.execution_payload_bid: return topic.type; case GossipType.beacon_attestation: case GossipType.sync_committee: @@ -114,6 +117,12 @@ export function getGossipSSZType(topic: GossipTopic) { : ssz.altair.LightClientFinalityUpdate; case GossipType.bls_to_execution_change: return ssz.capella.SignedBLSToExecutionChange; + case GossipType.execution_payload: + return ssz.gloas.SignedExecutionPayloadEnvelope; + case GossipType.payload_attestation_message: + return ssz.gloas.PayloadAttestationMessage; + case GossipType.execution_payload_bid: + return ssz.gloas.SignedExecutionPayloadBid; } } @@ -190,6 +199,9 @@ export function parseGossipTopic(forkDigestContext: ForkDigestContext, topicStr: case GossipType.light_client_finality_update: case GossipType.light_client_optimistic_update: case GossipType.bls_to_execution_change: + case GossipType.execution_payload: + case GossipType.payload_attestation_message: + case GossipType.execution_payload_bid: return {type: gossipTypeStr, boundary, encoding}; } @@ -240,6 +252,12 @@ export function getCoreTopicsAtFork( {type: GossipType.attester_slashing}, ]; + if (ForkSeq[fork] >= ForkSeq.gloas) { + topics.push({type: GossipType.execution_payload}); + topics.push({type: GossipType.payload_attestation_message}); + topics.push({type: GossipType.execution_payload_bid}); + } + // After fulu also track data_column_sidecar_{index} if (ForkSeq[fork] >= ForkSeq.fulu) { topics.push(...getDataColumnSidecarTopics(networkConfig)); @@ -329,4 +347,7 @@ export const gossipTopicIgnoreDuplicatePublishError: Record [GossipType.light_client_finality_update]: false, [GossipType.light_client_optimistic_update]: false, [GossipType.bls_to_execution_change]: true, + [GossipType.execution_payload]: true, + [GossipType.payload_attestation_message]: true, + [GossipType.execution_payload_bid]: true, }; diff --git a/packages/beacon-node/src/network/network.ts b/packages/beacon-node/src/network/network.ts index 6169102af4d5..94e5b000909c 100644 --- a/packages/beacon-node/src/network/network.ts +++ b/packages/beacon-node/src/network/network.ts @@ -641,6 +641,18 @@ export class Network implements INetwork { return this.core.disconnectPeer(peer); } + addDirectPeer(peer: routes.lodestar.DirectPeer): Promise { + return this.core.addDirectPeer(peer); + } + + removeDirectPeer(peerId: string): Promise { + return this.core.removeDirectPeer(peerId); + } + + getDirectPeers(): Promise { + return this.core.getDirectPeers(); + } + dumpPeer(peerIdStr: string): Promise { return this.core.dumpPeer(peerIdStr); } diff --git a/packages/beacon-node/src/network/options.ts b/packages/beacon-node/src/network/options.ts index f7ad5deb8d6c..e3989ebd9502 100644 --- a/packages/beacon-node/src/network/options.ts +++ b/packages/beacon-node/src/network/options.ts @@ -15,6 +15,12 @@ export interface NetworkOptions Omit { localMultiaddrs: string[]; bootMultiaddrs?: string[]; + /** + * Direct peers for GossipSub - these peers maintain permanent mesh connections without GRAFT/PRUNE. + * Format: multiaddr strings with peer ID, e.g., "/ip4/192.168.1.1/tcp/9000/p2p/16Uiu2HAmKLhW7..." + * Both peers must configure each other as direct peers for the feature to work properly. + */ + directPeers?: string[]; subscribeAllSubnets?: boolean; mdns?: boolean; connectToDiscv5Bootnodes?: boolean; diff --git a/packages/beacon-node/src/network/peers/peerManager.ts b/packages/beacon-node/src/network/peers/peerManager.ts index f80dd58d96af..8cdd26b7dab6 100644 --- a/packages/beacon-node/src/network/peers/peerManager.ts +++ b/packages/beacon-node/src/network/peers/peerManager.ts @@ -721,6 +721,17 @@ export class PeerManager { // NOTE: libp2p may emit two "peer:connect" events: One for inbound, one for outbound // If that happens, it's okay. Only the "outbound" connection triggers immediate action const now = Date.now(); + + // Ethereum uses secp256k1 for node IDs, reject peers with other key types + if (remotePeer.type !== "secp256k1") { + this.logger.debug("Peer does not have secp256k1 key, disconnecting", { + peer: remotePeerPrettyStr, + type: remotePeer.type, + }); + void this.goodbyeAndDisconnect(remotePeer, GoodByeReasonCode.IRRELEVANT_NETWORK); + return; + } + const nodeId = computeNodeId(remotePeer); const peerData: PeerData = { lastReceivedMsgUnixTsMs: direction === "outbound" ? 0 : now, diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index 988834973fa2..996a26148785 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -48,6 +48,8 @@ import { import {IBeaconChain} from "../../chain/interface.js"; import {validateGossipBlobSidecar} from "../../chain/validation/blobSidecar.js"; import {validateGossipDataColumnSidecar} from "../../chain/validation/dataColumnSidecar.js"; +import {validateGossipExecutionPayloadBid} from "../../chain/validation/executionPayloadBid.js"; +import {validateGossipExecutionPayloadEnvelope} from "../../chain/validation/executionPayloadEnvelope.js"; import { AggregateAndProofValidationResult, GossipAttestation, @@ -64,6 +66,7 @@ import { } from "../../chain/validation/index.js"; import {validateLightClientFinalityUpdate} from "../../chain/validation/lightClientFinalityUpdate.js"; import {validateLightClientOptimisticUpdate} from "../../chain/validation/lightClientOptimisticUpdate.js"; +import {validateGossipPayloadAttestationMessage} from "../../chain/validation/payloadAttestationMessage.js"; import {OpSource} from "../../chain/validatorMonitor.js"; import {Metrics} from "../../metrics/index.js"; import {kzgCommitmentToVersionedHash} from "../../util/blobs.js"; @@ -436,14 +439,11 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand blsVerifyOnMainThread: true, // to track block process steps seenTimestampSec, - // gossip block is validated, we want to process it asap - eagerPersistBlock: true, }) .then(() => { // Returns the delay between the start of `block.slot` and `current time` const delaySec = chain.clock.secFromSlot(slot); metrics?.gossipBlock.elapsedTimeTillProcessed.observe(delaySec); - chain.seenBlockInputCache.prune(blockInput.blockRootHex); }) .catch((e) => { // Adjust verbosity based on error type @@ -579,7 +579,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand break; } - if (!blockInput.hasAllData()) { + if (!blockInput.hasComputedAllData()) { // immediately attempt fetch of data columns from execution engine chain.getBlobsTracker.triggerGetBlobs(blockInput); // if we've received at least half of the columns, trigger reconstruction of the rest @@ -818,6 +818,51 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand chain.emitter.emit(routes.events.EventType.blsToExecutionChange, blsToExecutionChange); }, + [GossipType.execution_payload]: async ({ + gossipData, + topic, + }: GossipHandlerParamGeneric) => { + const {serializedData} = gossipData; + const executionPayloadEnvelope = sszDeserialize(topic, serializedData); + await validateGossipExecutionPayloadEnvelope(chain, executionPayloadEnvelope); + + // TODO GLOAS: Handle valid envelope. Need an import flow that calls `processExecutionPayloadEnvelope` and fork choice + }, + [GossipType.payload_attestation_message]: async ({ + gossipData, + topic, + }: GossipHandlerParamGeneric) => { + const {serializedData} = gossipData; + const payloadAttestationMessage = sszDeserialize(topic, serializedData); + const validationResult = await validateGossipPayloadAttestationMessage(chain, payloadAttestationMessage); + + try { + const insertOutcome = chain.payloadAttestationPool.add( + payloadAttestationMessage, + validationResult.attDataRootHex, + validationResult.validatorCommitteeIndex + ); + metrics?.opPool.payloadAttestationPool.gossipInsertOutcome.inc({insertOutcome}); + } catch (e) { + logger.error("Error adding to payloadAttestation pool", {}, e as Error); + } + }, + [GossipType.execution_payload_bid]: async ({ + gossipData, + topic, + }: GossipHandlerParamGeneric) => { + const {serializedData} = gossipData; + const executionPayloadBid = sszDeserialize(topic, serializedData); + await validateGossipExecutionPayloadBid(chain, executionPayloadBid); + + // Handle valid payload bid by storing in a bid pool + try { + const insertOutcome = chain.executionPayloadBidPool.add(executionPayloadBid.message); + metrics?.opPool.executionPayloadBidPool.gossipInsertOutcome.inc({insertOutcome}); + } catch (e) { + logger.error("Error adding to executionPayloadBid pool", {}, e as Error); + } + }, }; } diff --git a/packages/beacon-node/src/network/processor/gossipQueues/index.ts b/packages/beacon-node/src/network/processor/gossipQueues/index.ts index 701411ef7dd8..5f4200b9eab9 100644 --- a/packages/beacon-node/src/network/processor/gossipQueues/index.ts +++ b/packages/beacon-node/src/network/processor/gossipQueues/index.ts @@ -67,6 +67,22 @@ const linearGossipQueueOpts: { type: QueueType.FIFO, dropOpts: {type: DropType.count, count: 1}, }, + [GossipType.execution_payload]: { + maxLength: 1024, + type: QueueType.FIFO, + dropOpts: {type: DropType.count, count: 1}, + }, + [GossipType.payload_attestation_message]: { + maxLength: 1024, + type: QueueType.FIFO, + dropOpts: {type: DropType.count, count: 1}, + }, + // TODO GLOAS: It is hard to predict how many bids are there. Put 1024 for max length for now + [GossipType.execution_payload_bid]: { + maxLength: 1024, + type: QueueType.FIFO, + dropOpts: {type: DropType.count, count: 1}, + }, }; const indexedGossipQueueOpts: { diff --git a/packages/beacon-node/src/network/processor/index.ts b/packages/beacon-node/src/network/processor/index.ts index 3ac1de30b378..cf20ab63d2e3 100644 --- a/packages/beacon-node/src/network/processor/index.ts +++ b/packages/beacon-node/src/network/processor/index.ts @@ -78,6 +78,9 @@ const executeGossipWorkOrderObj: Record = { [GossipType.sync_committee]: {}, [GossipType.light_client_finality_update]: {}, [GossipType.light_client_optimistic_update]: {}, + [GossipType.execution_payload]: {bypassQueue: true}, + [GossipType.payload_attestation_message]: {}, + [GossipType.execution_payload_bid]: {}, }; const executeGossipWorkOrder = Object.keys(executeGossipWorkOrderObj) as (keyof typeof executeGossipWorkOrderObj)[]; diff --git a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts index 4cfc43ea38a5..b4eda0db765e 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts @@ -4,7 +4,6 @@ import {GENESIS_SLOT, isForkPostDeneb, isForkPostFulu} from "@lodestar/params"; import {RespStatus, ResponseError, ResponseOutgoing} from "@lodestar/reqresp"; import {computeEpochAtSlot} from "@lodestar/state-transition"; import {deneb, phase0} from "@lodestar/types"; -import {fromHex} from "@lodestar/utils"; import {IBeaconChain} from "../../../chain/index.js"; import {IBeaconDb} from "../../../db/index.js"; import {prettyPrintPeerId} from "../../util.ts"; @@ -22,7 +21,6 @@ export async function* onBeaconBlocksByRange( const endSlot = startSlot + count; const finalized = db.blockArchive; - const unfinalized = db.block; // in the case of initializing from a non-finalized state, we don't have the finalized block so this api does not work // chain.forkChoice.getFinalizeBlock().slot const finalizedSlot = chain.forkChoice.getFinalizedCheckpointSlot(); @@ -65,7 +63,7 @@ export async function* onBeaconBlocksByRange( // re-org there's no need to abort the request // Spec: https://github.com/ethereum/consensus-specs/blob/a1e46d1ae47dd9d097725801575b46907c12a1f8/specs/eip4844/p2p-interface.md#blobssidecarsbyrange-v1 - const blockBytes = await unfinalized.getBinary(fromHex(block.blockRoot)); + const blockBytes = await chain.getSerializedBlockByRoot(block.blockRoot); if (!blockBytes) { throw new ResponseError( RespStatus.SERVER_ERROR, @@ -74,7 +72,7 @@ export async function* onBeaconBlocksByRange( } yield { - data: blockBytes, + data: blockBytes.block, boundary: chain.config.getForkBoundaryAtEpoch(computeEpochAtSlot(block.slot)), }; } diff --git a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts index 211c03495809..362ae9267a3b 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts @@ -1,48 +1,21 @@ import {ResponseOutgoing} from "@lodestar/reqresp"; import {computeEpochAtSlot} from "@lodestar/state-transition"; -import {Slot} from "@lodestar/types"; import {toRootHex} from "@lodestar/utils"; import {IBeaconChain} from "../../../chain/index.js"; -import {IBeaconDb} from "../../../db/index.js"; -import {getSlotFromSignedBeaconBlockSerialized} from "../../../util/sszBytes.js"; import {BeaconBlocksByRootRequest} from "../../../util/types.js"; export async function* onBeaconBlocksByRoot( requestBody: BeaconBlocksByRootRequest, - chain: IBeaconChain, - db: IBeaconDb + chain: IBeaconChain ): AsyncIterable { for (const blockRoot of requestBody) { const root = blockRoot; - const summary = chain.forkChoice.getBlock(root); - let blockBytes: Uint8Array | null = null; - - // finalized block has summary in forkchoice but it stays in blockArchive db - if (summary) { - blockBytes = await db.block.getBinary(root); - } - - let slot: Slot | undefined = undefined; - if (!blockBytes) { - const blockEntry = await db.blockArchive.getBinaryEntryByRoot(root); - if (blockEntry) { - slot = blockEntry.key; - blockBytes = blockEntry.value; - } - } - - if (blockBytes) { - if (slot === undefined) { - const slotFromBytes = getSlotFromSignedBeaconBlockSerialized(blockBytes); - if (slotFromBytes === null) { - throw Error(`Invalid block bytes for block root ${toRootHex(root)}`); - } - slot = slotFromBytes; - } + const block = await chain.getSerializedBlockByRoot(toRootHex(root)); + if (block) { yield { - data: blockBytes, - boundary: chain.config.getForkBoundaryAtEpoch(computeEpochAtSlot(slot)), + data: block.block, + boundary: chain.config.getForkBoundaryAtEpoch(computeEpochAtSlot(block.slot)), }; } } diff --git a/packages/beacon-node/src/network/reqresp/handlers/blobSidecarsByRoot.ts b/packages/beacon-node/src/network/reqresp/handlers/blobSidecarsByRoot.ts index 6416520a3ef6..751f9d4980ee 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/blobSidecarsByRoot.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/blobSidecarsByRoot.ts @@ -2,16 +2,13 @@ import {BLOB_SIDECAR_FIXED_SIZE} from "@lodestar/params"; import {RespStatus, ResponseError, ResponseOutgoing} from "@lodestar/reqresp"; import {computeEpochAtSlot} from "@lodestar/state-transition"; import {RootHex} from "@lodestar/types"; -import {fromHex, toRootHex} from "@lodestar/utils"; +import {toRootHex} from "@lodestar/utils"; import {IBeaconChain} from "../../../chain/index.js"; -import {IBeaconDb} from "../../../db/index.js"; -import {BLOB_SIDECARS_IN_WRAPPER_INDEX} from "../../../db/repositories/blobSidecars.js"; import {BlobSidecarsByRootRequest} from "../../../util/types.js"; export async function* onBlobSidecarsByRoot( requestBody: BlobSidecarsByRootRequest, - chain: IBeaconChain, - db: IBeaconDb + chain: IBeaconChain ): AsyncIterable { const finalizedSlot = chain.forkChoice.getFinalizedBlock().slot; @@ -34,14 +31,13 @@ export async function* onBlobSidecarsByRoot( // Check if we need to load sidecars for a new block root if (lastFetchedSideCars === null || lastFetchedSideCars.blockRoot !== blockRootHex) { - const blobSideCarsBytesWrapped = await db.blobSidecars.getBinary(fromHex(block.blockRoot)); - if (!blobSideCarsBytesWrapped) { + const blobSidecarsBytes = await chain.getSerializedBlobSidecars(block.slot, blockRootHex); + if (!blobSidecarsBytes) { // Handle the same to onBeaconBlocksByRange throw new ResponseError(RespStatus.SERVER_ERROR, `No item for root ${block.blockRoot} slot ${block.slot}`); } - const blobSideCarsBytes = blobSideCarsBytesWrapped.slice(BLOB_SIDECARS_IN_WRAPPER_INDEX); - lastFetchedSideCars = {blockRoot: blockRootHex, bytes: blobSideCarsBytes}; + lastFetchedSideCars = {blockRoot: blockRootHex, bytes: blobSidecarsBytes}; } const blobSidecarBytes = lastFetchedSideCars.bytes.slice( diff --git a/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts b/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts index 457eb0296917..3610b0136e8b 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts @@ -38,7 +38,6 @@ export async function* onDataColumnSidecarsByRange( } const finalized = db.dataColumnSidecarArchive; - const unfinalized = db.dataColumnSidecar; const finalizedSlot = chain.forkChoice.getFinalizedBlock().slot; // Finalized range of columns @@ -92,7 +91,11 @@ export async function* onDataColumnSidecarsByRange( // at the time of the start of the request. Spec is clear the chain of columns must be consistent, but on // re-org there's no need to abort the request // Spec: https://github.com/ethereum/consensus-specs/blob/ad36024441cf910d428d03f87f331fbbd2b3e5f1/specs/fulu/p2p-interface.md#L425-L429 - const dataColumnSidecars = await unfinalized.getManyBinary(fromHex(block.blockRoot), availableColumns); + const dataColumnSidecars = await chain.getSerializedDataColumnSidecars( + block.slot, + block.blockRoot, + availableColumns + ); const unavailableColumnIndices: ColumnIndex[] = []; for (let i = 0; i < dataColumnSidecars.length; i++) { diff --git a/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRoot.ts b/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRoot.ts index 24c88fd499d9..8e598ebe5a4d 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRoot.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRoot.ts @@ -61,11 +61,7 @@ export async function* onDataColumnSidecarsByRoot( continue; } - const dataColumns = block - ? // Non-finalized sidecars are stored by block root - await db.dataColumnSidecar.getManyBinary(blockRoot, availableColumns) - : // Finalized sidecars are archived and stored by slot - await db.dataColumnSidecarArchive.getManyBinary(slot, availableColumns); + const dataColumns = await chain.getSerializedDataColumnSidecars(slot, blockRootHex, availableColumns); const unavailableColumnIndices: ColumnIndex[] = []; for (let i = 0; i < dataColumns.length; i++) { diff --git a/packages/beacon-node/src/network/reqresp/handlers/index.ts b/packages/beacon-node/src/network/reqresp/handlers/index.ts index 985a34c8f40b..9777bbf23964 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/index.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/index.ts @@ -42,12 +42,12 @@ export function getReqRespHandlers({db, chain}: {db: IBeaconDb; chain: IBeaconCh [ReqRespMethod.BeaconBlocksByRoot]: (req) => { const fork = chain.config.getForkName(chain.clock.currentSlot); const body = BeaconBlocksByRootRequestType(fork, chain.config).deserialize(req.data); - return onBeaconBlocksByRoot(body, chain, db); + return onBeaconBlocksByRoot(body, chain); }, [ReqRespMethod.BlobSidecarsByRoot]: (req) => { const fork = chain.config.getForkName(chain.clock.currentSlot); const body = BlobSidecarsByRootRequestType(fork, chain.config).deserialize(req.data); - return onBlobSidecarsByRoot(body, chain, db); + return onBlobSidecarsByRoot(body, chain); }, [ReqRespMethod.BlobSidecarsByRange]: (req) => { const body = ssz.deneb.BlobSidecarsByRangeRequest.deserialize(req.data); diff --git a/packages/beacon-node/src/network/reqresp/utils/dataColumnResponseValidation.ts b/packages/beacon-node/src/network/reqresp/utils/dataColumnResponseValidation.ts index 5a22cbce9f1b..a8d36633147c 100644 --- a/packages/beacon-node/src/network/reqresp/utils/dataColumnResponseValidation.ts +++ b/packages/beacon-node/src/network/reqresp/utils/dataColumnResponseValidation.ts @@ -55,7 +55,7 @@ export async function handleColumnSidecarUnavailability({ const blobsCount = getBlobKzgCommitmentsCountFromSignedBeaconBlockSerialized(chain.config, blockBytes); // There are zero blobs for that column index, so we can safely return without any error - if (blobsCount > 0) return; + if (blobsCount === 0) return; // There are blobs for that column index so we should have synced for it // We need to inform to peers that we don't have that expected data diff --git a/packages/beacon-node/src/node/nodejs.ts b/packages/beacon-node/src/node/nodejs.ts index 90c6f9f9341c..18e7961c7604 100644 --- a/packages/beacon-node/src/node/nodejs.ts +++ b/packages/beacon-node/src/node/nodejs.ts @@ -6,9 +6,10 @@ import {PubkeyIndexMap} from "@chainsafe/pubkey-index-map"; import {BeaconApiMethods} from "@lodestar/api/beacon/server"; import {BeaconConfig} from "@lodestar/config"; import type {LoggerNode} from "@lodestar/logger/node"; -import {CachedBeaconStateAllForks, Index2PubkeyCache} from "@lodestar/state-transition"; +import {ZERO_HASH_HEX} from "@lodestar/params"; +import {CachedBeaconStateAllForks, Index2PubkeyCache, isExecutionCachedStateType} from "@lodestar/state-transition"; import {phase0} from "@lodestar/types"; -import {sleep} from "@lodestar/utils"; +import {sleep, toRootHex} from "@lodestar/utils"; import {ProcessShutdownCallback} from "@lodestar/validator"; import {BeaconRestApiServer, getApi} from "../api/index.js"; import {BeaconChain, IBeaconChain, initBeaconMetrics} from "../chain/index.js"; @@ -221,6 +222,20 @@ export class BeaconNode { ) : null; + let executionEngineOpts = opts.executionEngine; + if (opts.executionEngine.mode === "mock") { + const eth1BlockHash = isExecutionCachedStateType(anchorState) + ? toRootHex(anchorState.latestExecutionPayloadHeader.blockHash) + : undefined; + executionEngineOpts = { + ...opts.executionEngine, + genesisBlockHash: ZERO_HASH_HEX, + eth1BlockHash, + genesisTime: anchorState.genesisTime, + config, + }; + } + const chain = new BeaconChain(opts.chain, { privateKey, config, @@ -236,7 +251,7 @@ export class BeaconNode { validatorMonitor, anchorState, isAnchorStateFinalized, - executionEngine: initializeExecutionEngine(opts.executionEngine, { + executionEngine: initializeExecutionEngine(executionEngineOpts, { metrics, signal, logger: logger.child({module: LoggerModule.execution}), diff --git a/packages/beacon-node/src/sync/backfill/backfill.ts b/packages/beacon-node/src/sync/backfill/backfill.ts index 246d58734d08..52b0bd8646e6 100644 --- a/packages/beacon-node/src/sync/backfill/backfill.ts +++ b/packages/beacon-node/src/sync/backfill/backfill.ts @@ -4,13 +4,12 @@ import {BeaconConfig, ChainForkConfig} from "@lodestar/config"; import {SLOTS_PER_EPOCH} from "@lodestar/params"; import {BeaconStateAllForks, blockToHeader, computeAnchorCheckpoint} from "@lodestar/state-transition"; import {Root, SignedBeaconBlock, Slot, phase0, ssz} from "@lodestar/types"; -import {ErrorAborted, Logger, sleep, toRootHex} from "@lodestar/utils"; +import {ErrorAborted, Logger, byteArrayEquals, sleep, toRootHex} from "@lodestar/utils"; import {IBeaconChain} from "../../chain/index.js"; import {GENESIS_SLOT, ZERO_HASH} from "../../constants/index.js"; import {IBeaconDb} from "../../db/index.js"; import {Metrics} from "../../metrics/metrics.js"; import {INetwork, NetworkEvent, NetworkEventData, PeerAction} from "../../network/index.js"; -import {byteArrayEquals} from "../../util/bytes.js"; import {ItTrigger} from "../../util/itTrigger.js"; import {PeerIdStr} from "../../util/peerId.js"; import {shuffleOne} from "../../util/shuffle.js"; @@ -750,7 +749,7 @@ export class BackfillSync extends (EventEmitter as {new (): BackfillSyncEmitter} // GENESIS_SLOT doesn't has valid signature if (anchorBlock.message.slot === GENESIS_SLOT) return; - await verifyBlockProposerSignature(this.chain.config, this.chain.index2pubkey, this.chain.bls, [anchorBlock]); + await verifyBlockProposerSignature(this.chain.config, this.chain.bls, [anchorBlock]); // We can write to the disk if this is ahead of prevFinalizedCheckpointBlock otherwise // we will need to go make checks on the top of sync loop before writing as it might @@ -815,7 +814,7 @@ export class BackfillSync extends (EventEmitter as {new (): BackfillSyncEmitter} // If any of the block's proposer signature fail, we can't trust this peer at all if (verifiedBlocks.length > 0) { - await verifyBlockProposerSignature(this.chain.config, this.chain.index2pubkey, this.chain.bls, verifiedBlocks); + await verifyBlockProposerSignature(this.chain.config, this.chain.bls, verifiedBlocks); // This is bad, like super bad. Abort the backfill if (!nextAnchor) diff --git a/packages/beacon-node/src/sync/backfill/verify.ts b/packages/beacon-node/src/sync/backfill/verify.ts index 565c9d957d8d..ec5da81d4686 100644 --- a/packages/beacon-node/src/sync/backfill/verify.ts +++ b/packages/beacon-node/src/sync/backfill/verify.ts @@ -1,6 +1,6 @@ import {BeaconConfig} from "@lodestar/config"; import {GENESIS_SLOT} from "@lodestar/params"; -import {ISignatureSet, Index2PubkeyCache, getBlockProposerSignatureSet} from "@lodestar/state-transition"; +import {ISignatureSet, getBlockProposerSignatureSet} from "@lodestar/state-transition"; import {Root, SignedBeaconBlock, Slot, ssz} from "@lodestar/types"; import {IBlsVerifier} from "../../chain/bls/index.js"; import {BackfillSyncError, BackfillSyncErrorCode} from "./errors.js"; @@ -42,14 +42,13 @@ export function verifyBlockSequence( export async function verifyBlockProposerSignature( config: BeaconConfig, - index2pubkey: Index2PubkeyCache, bls: IBlsVerifier, blocks: SignedBeaconBlock[] ): Promise { if (blocks.length === 1 && blocks[0].message.slot === GENESIS_SLOT) return; const signatures = blocks.reduce((sigs: ISignatureSet[], block) => { // genesis block doesn't have valid signature - if (block.message.slot !== GENESIS_SLOT) sigs.push(getBlockProposerSignatureSet(config, index2pubkey, block)); + if (block.message.slot !== GENESIS_SLOT) sigs.push(getBlockProposerSignatureSet(config, block)); return sigs; }, []); diff --git a/packages/beacon-node/src/sync/range/chain.ts b/packages/beacon-node/src/sync/range/chain.ts index b0d07b1011e8..911ce93b5bb0 100644 --- a/packages/beacon-node/src/sync/range/chain.ts +++ b/packages/beacon-node/src/sync/range/chain.ts @@ -585,7 +585,6 @@ export class SyncChain { if (!res.err) { batch.processingSuccess(); - this.pruneBlockInputs(batch.getBlocks()); // If the processed batch is not empty, validate previous AwaitingValidation blocks. if (blocks.length > 0) { diff --git a/packages/beacon-node/src/sync/range/range.ts b/packages/beacon-node/src/sync/range/range.ts index 311311beae80..dbd5213ae4ef 100644 --- a/packages/beacon-node/src/sync/range/range.ts +++ b/packages/beacon-node/src/sync/range/range.ts @@ -188,9 +188,6 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) { // when this runs, syncing is the most important thing and gossip is not likely to run // so we can utilize worker threads to verify signatures blsVerifyOnMainThread: false, - // we want to be safe to only persist blocks after verifying it to avoid any attacks that may cause our DB - // to grow too much - eagerPersistBlock: false, }; if (this.opts?.disableProcessAsChainSegment) { diff --git a/packages/beacon-node/src/sync/unknownBlock.ts b/packages/beacon-node/src/sync/unknownBlock.ts index ba507a039d58..ad3e5cbc2ad9 100644 --- a/packages/beacon-node/src/sync/unknownBlock.ts +++ b/packages/beacon-node/src/sync/unknownBlock.ts @@ -423,8 +423,6 @@ export class BlockInputSync { // see https://github.com/ChainSafe/lodestar/issues/5650 ignoreIfFinalized: true, blsVerifyOnMainThread: true, - // block is validated with correct root, we want to process it as soon as possible - eagerPersistBlock: true, }) ); @@ -434,7 +432,6 @@ export class BlockInputSync { if (!res.err) { // no need to update status to "processed", delete anyway this.pendingBlocks.delete(pendingBlock.blockInput.blockRootHex); - this.chain.seenBlockInputCache.prune(pendingBlock.blockInput.blockRootHex); // Send child blocks to the processor for (const descendantBlock of getDescendantBlocks(pendingBlock.blockInput.blockRootHex, this.pendingBlocks)) { diff --git a/packages/beacon-node/src/sync/utils/downloadByRange.ts b/packages/beacon-node/src/sync/utils/downloadByRange.ts index f019bb0e8e37..e722cbdbd35a 100644 --- a/packages/beacon-node/src/sync/utils/downloadByRange.ts +++ b/packages/beacon-node/src/sync/utils/downloadByRange.ts @@ -8,7 +8,7 @@ import { isForkPostGloas, } from "@lodestar/params"; import {SignedBeaconBlock, Slot, deneb, fulu, phase0} from "@lodestar/types"; -import {LodestarError, Logger, fromHex, prettyPrintIndices, toRootHex} from "@lodestar/utils"; +import {LodestarError, Logger, byteArrayEquals, fromHex, prettyPrintIndices, toRootHex} from "@lodestar/utils"; import { BlockInputSource, DAType, @@ -475,7 +475,7 @@ export function validateBlockByRangeResponse( if (i < blocks.length - 1) { // compare the block root against the next block's parent root const parentRoot = blocks[i + 1].message.parentRoot; - if (Buffer.compare(blockRoot, parentRoot) !== 0) { + if (!byteArrayEquals(blockRoot, parentRoot)) { throw new DownloadByRangeError( { code: DownloadByRangeErrorCode.PARENT_ROOT_MISMATCH, diff --git a/packages/beacon-node/src/sync/utils/downloadByRoot.ts b/packages/beacon-node/src/sync/utils/downloadByRoot.ts index 28f1f52fcdc9..de2ba69c2eec 100644 --- a/packages/beacon-node/src/sync/utils/downloadByRoot.ts +++ b/packages/beacon-node/src/sync/utils/downloadByRoot.ts @@ -9,7 +9,7 @@ import { isForkPostFulu, } from "@lodestar/params"; import {BeaconBlockBody, BlobIndex, ColumnIndex, SignedBeaconBlock, Slot, deneb, fulu} from "@lodestar/types"; -import {LodestarError, fromHex, prettyPrintIndices, toHex, toRootHex} from "@lodestar/utils"; +import {LodestarError, byteArrayEquals, fromHex, prettyPrintIndices, toHex, toRootHex} from "@lodestar/utils"; import {isBlockInputBlobs, isBlockInputColumns} from "../../chain/blocks/blockInput/blockInput.js"; import {BlockInputSource, IBlockInput} from "../../chain/blocks/blockInput/types.js"; import {ChainEventEmitter} from "../../chain/emitter.js"; @@ -19,7 +19,6 @@ import {validateBlockDataColumnSidecars} from "../../chain/validation/dataColumn import {INetwork} from "../../network/interface.js"; import {PeerSyncMeta} from "../../network/peers/peersData.js"; import {prettyPrintPeerIdStr} from "../../network/util.js"; -import {byteArrayEquals} from "../../util/bytes.js"; import {PeerIdStr} from "../../util/peerId.js"; import {WarnResult} from "../../util/wrapError.js"; import { diff --git a/packages/beacon-node/src/util/bytes.ts b/packages/beacon-node/src/util/bytes.ts deleted file mode 100644 index 358693d7c261..000000000000 --- a/packages/beacon-node/src/util/bytes.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {Root} from "@lodestar/types"; - -export function byteArrayEquals(a: Uint8Array | Root, b: Uint8Array | Root): boolean { - if (a.length !== b.length) { - return false; - } - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) return false; - } - return true; -} diff --git a/packages/beacon-node/src/util/sszBytes.ts b/packages/beacon-node/src/util/sszBytes.ts index ed22de544b9c..8ba5ddf3291e 100644 --- a/packages/beacon-node/src/util/sszBytes.ts +++ b/packages/beacon-node/src/util/sszBytes.ts @@ -479,7 +479,7 @@ export function getBlobKzgCommitmentsCountFromSignedBeaconBlockSerialized( blockBytes: Uint8Array ): number { const slot = getSlotFromSignedBeaconBlockSerialized(blockBytes); - if (!slot) throw new Error("Can not parse the slot from block bytes"); + if (slot === null) throw new Error("Can not parse the slot from block bytes"); if (config.getForkSeq(slot) < ForkSeq.deneb) return 0; diff --git a/packages/beacon-node/test/e2e/api/impl/lightclient/endpoint.test.ts b/packages/beacon-node/test/e2e/api/impl/lightclient/endpoint.test.ts index c62e59dc2e4f..b3b91b0edb01 100644 --- a/packages/beacon-node/test/e2e/api/impl/lightclient/endpoint.test.ts +++ b/packages/beacon-node/test/e2e/api/impl/lightclient/endpoint.test.ts @@ -1,10 +1,9 @@ -import {afterEach, beforeEach, describe, expect, it} from "vitest"; -import {aggregateSerializedPublicKeys} from "@chainsafe/blst"; +import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"; import {HttpHeader, getClient, routes} from "@lodestar/api"; import {ChainConfig, createBeaconConfig} from "@lodestar/config"; -import {chainConfig as chainConfigDef} from "@lodestar/config/default"; -import {ForkName, SYNC_COMMITTEE_SIZE} from "@lodestar/params"; -import {phase0, ssz} from "@lodestar/types"; +import {ForkName} from "@lodestar/params"; +import {CachedBeaconStateAltair} from "@lodestar/state-transition"; +import {phase0} from "@lodestar/types"; import {sleep} from "@lodestar/utils"; import {Validator} from "@lodestar/validator"; import {BeaconNode} from "../../../../../src/node/nodejs.js"; @@ -14,12 +13,24 @@ import {getDevBeaconNode} from "../../../../utils/node/beacon.js"; import {getAndInitDevValidators} from "../../../../utils/node/validator.js"; describe("lightclient api", () => { + vi.setConfig({testTimeout: 10_000}); + const SLOT_DURATION_MS = 1000; - const ALTAIR_FORK_EPOCH = 0; const restPort = 9596; - const chainConfig: ChainConfig = {...chainConfigDef, SLOT_DURATION_MS, ALTAIR_FORK_EPOCH}; + const ELECTRA_FORK_EPOCH = 0; + const FULU_FORK_EPOCH = 1; + const testParams: Partial = { + SLOT_DURATION_MS, + ALTAIR_FORK_EPOCH: ELECTRA_FORK_EPOCH, + BELLATRIX_FORK_EPOCH: ELECTRA_FORK_EPOCH, + CAPELLA_FORK_EPOCH: ELECTRA_FORK_EPOCH, + DENEB_FORK_EPOCH: ELECTRA_FORK_EPOCH, + ELECTRA_FORK_EPOCH: ELECTRA_FORK_EPOCH, + FULU_FORK_EPOCH: FULU_FORK_EPOCH, + }; + const genesisValidatorsRoot = Buffer.alloc(32, 0xaa); - const config = createBeaconConfig(chainConfig, genesisValidatorsRoot); + const config = createBeaconConfig(testParams, genesisValidatorsRoot); const testLoggerOpts: TestLoggerOpts = {level: LogLevel.info}; const loggerNodeA = testLogger("lightclient-api", testLoggerOpts); const validatorCount = 2; @@ -30,7 +41,7 @@ describe("lightclient api", () => { beforeEach(async () => { bn = await getDevBeaconNode({ - params: chainConfig, + params: testParams, options: { sync: {isSingleNode: true}, network: {allowPublishToZeroPeers: true}, @@ -84,7 +95,7 @@ describe("lightclient api", () => { expect(updates.length).toBe(1); // best update could be any slots // version is set - expect(res.meta().versions[0]).toBe(ForkName.altair); + expect(res.meta().versions[0]).toBe(ForkName.electra); }); it("getLightClientOptimisticUpdate()", async () => { @@ -96,7 +107,7 @@ describe("lightclient api", () => { // at slot 2 we got attestedHeader for slot 1 expect(update.attestedHeader.beacon.slot).toBe(slot - 1); // version is set - expect(res.meta().version).toBe(ForkName.altair); + expect(res.meta().version).toBe(ForkName.electra); // Ensure version header is made available to scripts running in the browser expect(res.headers.get(HttpHeader.ExposeHeaders)?.includes("Eth-Consensus-Version")).toBe(true); }); @@ -116,21 +127,14 @@ describe("lightclient api", () => { const lightclient = getClient({baseUrl: `http://127.0.0.1:${restPort}`}, {config}).lightclient; const committeeRes = await lightclient.getLightClientCommitteeRoot({startPeriod: 0, count: 1}); committeeRes.assertOk(); - const client = getClient({baseUrl: `http://127.0.0.1:${restPort}`}, {config}).beacon; - const validators = (await client.postStateValidators({stateId: "head"})).value(); - const pubkeys = validators.map((v) => v.validator.pubkey); - expect(pubkeys.length).toBe(validatorCount); - // only 2 validators spreading to 512 committee slots - const committeePubkeys = Array.from({length: SYNC_COMMITTEE_SIZE}, (_, i) => - i % 2 === 0 ? pubkeys[0] : pubkeys[1] - ); - const aggregatePubkey = aggregateSerializedPublicKeys(committeePubkeys).toBytes(); + + // Get the actual sync committee root from the head state + // The sync committee is computed using a weighted random shuffle, not simple alternation + // Since the test starts at Electra, headState is always post-Altair and has currentSyncCommittee + const headState = bn.chain.getHeadState() as CachedBeaconStateAltair; + const expectedRoot = headState.currentSyncCommittee.hashTreeRoot(); + // single committee hash since we requested for the first period - expect(committeeRes.value()).toEqual([ - ssz.altair.SyncCommittee.hashTreeRoot({ - pubkeys: committeePubkeys, - aggregatePubkey, - }), - ]); + expect(committeeRes.value()).toEqual([expectedRoot]); }); }); diff --git a/packages/beacon-node/test/e2e/chain/bls/multithread.test.ts b/packages/beacon-node/test/e2e/chain/bls/multithread.test.ts index 376484d651da..12e5130c0208 100644 --- a/packages/beacon-node/test/e2e/chain/bls/multithread.test.ts +++ b/packages/beacon-node/test/e2e/chain/bls/multithread.test.ts @@ -13,6 +13,7 @@ describe("chain / bls / multithread queue", () => { const sets: ISignatureSet[] = []; const sameMessageSets: {publicKey: PublicKey; signature: Uint8Array}[] = []; const sameMessage = Buffer.alloc(32, 100); + const index2pubkey: PublicKey[] = []; beforeAll(() => { for (let i = 0; i < 3; i++) { @@ -30,6 +31,7 @@ describe("chain / bls / multithread queue", () => { publicKey: pk, signature: sk.sign(sameMessage).toBytes(), }); + index2pubkey.push(pk); } }); @@ -47,7 +49,7 @@ describe("chain / bls / multithread queue", () => { }); async function initializePool(): Promise { - const pool = new BlsMultiThreadWorkerPool({}, {logger, metrics: null}); + const pool = new BlsMultiThreadWorkerPool({}, {logger, metrics: null, index2pubkey}); // await terminating all workers afterEachCallbacks.push(() => pool.close()); // Wait until initialized diff --git a/packages/beacon-node/test/e2e/chain/lightclient.test.ts b/packages/beacon-node/test/e2e/chain/lightclient.test.ts index f35c3aa2364b..c5714e18089e 100644 --- a/packages/beacon-node/test/e2e/chain/lightclient.test.ts +++ b/packages/beacon-node/test/e2e/chain/lightclient.test.ts @@ -2,13 +2,13 @@ import {afterEach, describe, expect, it, vi} from "vitest"; import {CompactMultiProof, computeDescriptor} from "@chainsafe/persistent-merkle-tree"; import {JsonPath, fromHexString, toHexString} from "@chainsafe/ssz"; import {ApiClient, getClient, routes} from "@lodestar/api"; -import {ChainConfig} from "@lodestar/config"; +import {BeaconConfig, ChainConfig} from "@lodestar/config"; import {Lightclient} from "@lodestar/light-client"; import {LightClientRestTransport} from "@lodestar/light-client/transport"; import {TimestampFormatCode} from "@lodestar/logger"; import {EPOCHS_PER_SYNC_COMMITTEE_PERIOD, SLOTS_PER_EPOCH} from "@lodestar/params"; import {computeStartSlotAtEpoch} from "@lodestar/state-transition"; -import {altair, ssz} from "@lodestar/types"; +import {LightClientHeader} from "@lodestar/types"; import {HeadEventData} from "../../../src/chain/index.js"; import {LogLevel, TestLoggerOpts, testLogger} from "../../utils/logger.js"; import {getDevBeaconNode} from "../../utils/node/beacon.js"; @@ -33,9 +33,23 @@ describe("chain / lightclient", () => { const targetSlotToReach = computeStartSlotAtEpoch(finalizedEpochToReach + 2) - 1; const restPort = 9000; - const testParams: Pick = { - SLOT_DURATION_MS: 1000, - ALTAIR_FORK_EPOCH: 0, + const ELECTRA_FORK_EPOCH = 0; + const FULU_FORK_EPOCH = 1; + const SLOT_DURATION_MS = 1000; + const testParams: Partial = { + SLOT_DURATION_MS, + ALTAIR_FORK_EPOCH: ELECTRA_FORK_EPOCH, + BELLATRIX_FORK_EPOCH: ELECTRA_FORK_EPOCH, + CAPELLA_FORK_EPOCH: ELECTRA_FORK_EPOCH, + DENEB_FORK_EPOCH: ELECTRA_FORK_EPOCH, + ELECTRA_FORK_EPOCH: ELECTRA_FORK_EPOCH, + FULU_FORK_EPOCH: FULU_FORK_EPOCH, + BLOB_SCHEDULE: [ + { + EPOCH: 1, + MAX_BLOBS_PER_BLOCK: 3, + }, + ], }; const afterEachCallbacks: (() => Promise | void)[] = []; @@ -50,7 +64,7 @@ describe("chain / lightclient", () => { // delay a bit so regular sync sees it's up to date and sync is completed from the beginning // also delay to allow bls workers to be transpiled/initialized const genesisSlotsDelay = 7; - const genesisTime = Math.floor(Date.now() / 1000) + (genesisSlotsDelay * testParams.SLOT_DURATION_MS) / 1000; + const genesisTime = Math.floor(Date.now() / 1000) + (genesisSlotsDelay * SLOT_DURATION_MS) / 1000; const testLoggerOpts: TestLoggerOpts = { level: LogLevel.info, @@ -58,7 +72,7 @@ describe("chain / lightclient", () => { format: TimestampFormatCode.EpochSlot, genesisTime, slotsPerEpoch: SLOTS_PER_EPOCH, - secondsPerSlot: testParams.SLOT_DURATION_MS / 1000, + secondsPerSlot: SLOT_DURATION_MS / 1000, }, }; @@ -136,14 +150,19 @@ describe("chain / lightclient", () => { bn.chain.emitter.on(routes.events.EventType.head, async (head) => { try { // Test fetching proofs - const {proof, header} = await getHeadStateProof(lightclient, api, [["latestBlockHeader", "bodyRoot"]]); + const {proof, header} = await getHeadStateProof(bn.config, lightclient, api, [ + ["latestBlockHeader", "bodyRoot"], + ]); const stateRootHex = toHexString(header.beacon.stateRoot); const lcHeadState = bn.chain.regen.getStateSync(stateRootHex); if (!lcHeadState) { throw Error(`LC head state not in cache ${stateRootHex}`); } - const stateLcFromProof = ssz.altair.BeaconState.createFromProof(proof, header.beacon.stateRoot); + const slot = header.beacon.slot; + const stateLcFromProof = bn.config + .getForkTypes(slot) + .BeaconState.createFromProof(proof, header.beacon.stateRoot); expect(toHexString(stateLcFromProof.latestBlockHeader.bodyRoot)).toBe( toHexString(lcHeadState.latestBlockHeader.bodyRoot) ); @@ -183,13 +202,15 @@ describe("chain / lightclient", () => { // TODO: Re-incorporate for REST-only light-client async function getHeadStateProof( + config: BeaconConfig, lightclient: Lightclient, api: ApiClient, paths: JsonPath[] -): Promise<{proof: CompactMultiProof; header: altair.LightClientHeader}> { +): Promise<{proof: CompactMultiProof; header: LightClientHeader}> { const header = lightclient.getHead(); const stateId = toHexString(header.beacon.stateRoot); - const gindices = paths.map((path) => ssz.bellatrix.BeaconState.getPathInfo(path).gindex); + const slot = header.beacon.slot; + const gindices = paths.map((path) => config.getForkTypes(slot).BeaconState.getPathInfo(path).gindex); const descriptor = computeDescriptor(gindices); const proof = (await api.proof.getStateProof({stateId, descriptor})).value(); return {proof, header}; diff --git a/packages/beacon-node/test/e2e/chain/stateCache/nHistoricalStates.test.ts b/packages/beacon-node/test/e2e/chain/stateCache/nHistoricalStates.test.ts index 5463d9da4794..3b23a35a81fc 100644 --- a/packages/beacon-node/test/e2e/chain/stateCache/nHistoricalStates.test.ts +++ b/packages/beacon-node/test/e2e/chain/stateCache/nHistoricalStates.test.ts @@ -303,12 +303,11 @@ describe("regen/reload states with n-historical states configuration", () => { options: { sync: {isSingleNode: true}, network: {allowPublishToZeroPeers: true, mdns: true, useWorker: false}, - // run the first bn with ReorgedForkChoice, no nHistoricalStates flag so it does not have to reload + // by default, maxCPStateEpochsInMemory is 3 so that it does not have to persist/reload checkpoint states chain: { blsVerifyAllMainThread: true, forkchoiceConstructor: ReorgedForkChoice, // this node does not need to reload state - nHistoricalStates: false, proposerBoost: true, }, }, @@ -325,12 +324,10 @@ describe("regen/reload states with n-historical states configuration", () => { options: { api: {rest: {enabled: false}}, network: {mdns: true, useWorker: false}, - // run the 2nd bn with nHistoricalStates flag and the configured maxBlockStates, maxCPStateEpochsInMemory + // configure this node to be reloaded via maxBlockStates, maxCPStateEpochsInMemory options chain: { blsVerifyAllMainThread: true, forkchoiceConstructor: ReorgedForkChoice, - // this node can follow with nHistoricalStates flag and it has to reload state - nHistoricalStates: true, maxBlockStates, maxCPStateEpochsInMemory, proposerBoost: true, diff --git a/packages/beacon-node/test/e2e/network/onWorker/dataSerialization.test.ts b/packages/beacon-node/test/e2e/network/onWorker/dataSerialization.test.ts index 35fdc57f4d56..cbef30901e57 100644 --- a/packages/beacon-node/test/e2e/network/onWorker/dataSerialization.test.ts +++ b/packages/beacon-node/test/e2e/network/onWorker/dataSerialization.test.ts @@ -118,6 +118,9 @@ describe("data serialization through worker boundary", () => { unsubscribeGossipCoreTopics: [], connectToPeer: [peerId, ["/ip4/1.2.3.4/tcp/13000"]], disconnectPeer: [peerId], + addDirectPeer: ["/ip4/1.2.3.4/tcp/13000/p2p/" + peerId], + removeDirectPeer: [peerId], + getDirectPeers: [], dumpPeers: [], dumpPeer: [peerId], dumpPeerScoreStats: [], @@ -207,6 +210,9 @@ describe("data serialization through worker boundary", () => { writeDiscv5Profile: "", setAdvertisedGroupCount: null, setTargetGroupCount: null, + addDirectPeer: peerId, + removeDirectPeer: true, + getDirectPeers: [peerId], }; type TestCase = {id: string; data: unknown; shouldFail?: boolean}; diff --git a/packages/beacon-node/test/e2e/sync/unknownBlockSync.test.ts b/packages/beacon-node/test/e2e/sync/unknownBlockSync.test.ts index 428ccc82952d..7403cb5d7c11 100644 --- a/packages/beacon-node/test/e2e/sync/unknownBlockSync.test.ts +++ b/packages/beacon-node/test/e2e/sync/unknownBlockSync.test.ts @@ -5,6 +5,7 @@ import {ChainConfig} from "@lodestar/config"; import {TimestampFormatCode} from "@lodestar/logger"; import {SLOTS_PER_EPOCH} from "@lodestar/params"; import {fulu} from "@lodestar/types"; +import {retry} from "@lodestar/utils"; import {BlockInputColumns} from "../../../src/chain/blocks/blockInput/blockInput.js"; import {BlockInputSource} from "../../../src/chain/blocks/blockInput/types.js"; import {ChainEvent} from "../../../src/chain/emitter.js"; @@ -17,7 +18,7 @@ import {getDevBeaconNode} from "../../utils/node/beacon.js"; import {getAndInitDevValidators} from "../../utils/node/validator.js"; describe("sync / unknown block sync for fulu", () => { - vi.setConfig({testTimeout: 40_000}); + vi.setConfig({testTimeout: 60_000}); const validatorCount = 8; const ELECTRA_FORK_EPOCH = 0; @@ -133,8 +134,15 @@ describe("sync / unknown block sync for fulu", () => { afterEachCallbacks.push(() => bn2.close().catch(() => {})); const headSummary = bn.chain.forkChoice.getHead(); - const head = await bn.db.block.get(fromHexString(headSummary.blockRoot)); - if (!head) throw Error("First beacon node has no head block"); + // Retry getting head block from db in case of slow persistence + const head = await retry( + async () => { + const block = await bn.db.block.get(fromHexString(headSummary.blockRoot)); + if (!block) throw Error("First beacon node has no head block"); + return block; + }, + {retries: 5, retryDelay: 500} + ); const waitForSynced = waitForEvent( bn2.chain.emitter, routes.events.EventType.head, diff --git a/packages/beacon-node/test/mocks/mockedBeaconChain.ts b/packages/beacon-node/test/mocks/mockedBeaconChain.ts index 5b049e2a1f39..2907e6da4081 100644 --- a/packages/beacon-node/test/mocks/mockedBeaconChain.ts +++ b/packages/beacon-node/test/mocks/mockedBeaconChain.ts @@ -148,6 +148,7 @@ vi.mock("../../src/chain/chain.js", async (importActual) => { produceBlock: vi.fn(), produceBlindedBlock: vi.fn(), getCanonicalBlockAtSlot: vi.fn(), + getBlockByRoot: vi.fn(), recomputeForkChoiceHead: vi.fn(), predictProposerHead: vi.fn(), getHeadStateAtCurrentEpoch: vi.fn(), diff --git a/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts b/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts index 612e4876209b..9c04938cf492 100644 --- a/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts +++ b/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts @@ -15,6 +15,7 @@ import { import {ssz} from "@lodestar/types"; import {generatePerfTestCachedStateAltair} from "../../../../../state-transition/test/perf/util.js"; import {AggregatedAttestationPool} from "../../../../src/chain/opPools/aggregatedAttestationPool.js"; +import {ShufflingCache} from "../../../../src/chain/shufflingCache.js"; const vc = 1_500_000; @@ -163,10 +164,12 @@ describe(`getAttestationsForBlock vc=${vc}`, () => { }, beforeEach: (state) => { const pool = getAggregatedAttestationPool(state, numMissedVotes, numBadVotes); - return {state, pool}; + const shufflingCache = new ShufflingCache(); + shufflingCache.processState(state); + return {state, pool, shufflingCache}; }, - fn: ({state, pool}) => { - pool.getAttestationsForBlock(state.config.getForkName(state.slot), forkchoice, state); + fn: ({state, pool, shufflingCache}) => { + pool.getAttestationsForBlock(state.config.getForkName(state.slot), forkchoice, shufflingCache, state); }, }); } diff --git a/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts b/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts index 8262fd8a7517..304190626a9c 100644 --- a/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts +++ b/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts @@ -1,6 +1,5 @@ import {generateKeyPair} from "@libp2p/crypto/keys"; import {afterAll, beforeAll, bench, describe} from "@chainsafe/benchmark"; -import {fromHexString} from "@chainsafe/ssz"; import {config} from "@lodestar/config/default"; import {LevelDbController} from "@lodestar/db/controller/level"; import {CachedBeaconStateAltair} from "@lodestar/state-transition"; @@ -80,14 +79,14 @@ describe("produceBlockBody", () => { slot: slot + 1, graffiti: Buffer.alloc(32), randaoReveal: Buffer.alloc(96), - parentBlockRoot: fromHexString(head.blockRoot), + parentBlock: head, }); await produceBlockBody.call(chain, BlockType.Full, state, { slot: slot + 1, graffiti: Buffer.alloc(32), randaoReveal: Buffer.alloc(96), - parentBlockRoot: fromHexString(head.blockRoot), + parentBlock: head, proposerIndex, proposerPubKey, commonBlockBodyPromise, diff --git a/packages/beacon-node/test/perf/chain/stateCache/inMemoryCheckpointsCache.test.ts b/packages/beacon-node/test/perf/chain/stateCache/inMemoryCheckpointsCache.test.ts deleted file mode 100644 index f7a07495a413..000000000000 --- a/packages/beacon-node/test/perf/chain/stateCache/inMemoryCheckpointsCache.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {beforeAll, bench, describe, setBenchOpts} from "@chainsafe/benchmark"; -import {CachedBeaconStateAllForks} from "@lodestar/state-transition"; -import {phase0, ssz} from "@lodestar/types"; -import {InMemoryCheckpointStateCache, toCheckpointHex} from "../../../../src/chain/stateCache/index.js"; -import {generateCachedState} from "../../../utils/state.js"; - -describe("InMemoryCheckpointStateCache perf tests", () => { - setBenchOpts({noThreshold: true}); - - let state: CachedBeaconStateAllForks; - let checkpoint: phase0.Checkpoint; - let checkpointStateCache: InMemoryCheckpointStateCache; - - beforeAll(() => { - checkpointStateCache = new InMemoryCheckpointStateCache({}); - state = generateCachedState(); - checkpoint = ssz.phase0.Checkpoint.defaultValue(); - }); - - bench("InMemoryCheckpointStateCache - add get delete", () => { - checkpointStateCache.add(checkpoint, state); - checkpointStateCache.get(toCheckpointHex(checkpoint)); - checkpointStateCache.delete(checkpoint); - }); -}); diff --git a/packages/beacon-node/test/perf/misc/map.test.ts b/packages/beacon-node/test/perf/misc/map.test.ts index 73998d0a5113..3730dbbd1deb 100644 --- a/packages/beacon-node/test/perf/misc/map.test.ts +++ b/packages/beacon-node/test/perf/misc/map.test.ts @@ -1,6 +1,6 @@ import {bench, describe} from "@chainsafe/benchmark"; -describe("misc / Map", () => { +describe.skip("misc / Map", () => { const times = 1000; type ObjData = {obj: Record; keys: string[]}; diff --git a/packages/beacon-node/test/perf/network/gossip/snappy.test.ts b/packages/beacon-node/test/perf/network/gossip/snappy.test.ts index 954cfb550c7b..a2e6fad7e567 100644 --- a/packages/beacon-node/test/perf/network/gossip/snappy.test.ts +++ b/packages/beacon-node/test/perf/network/gossip/snappy.test.ts @@ -4,7 +4,10 @@ import * as snappyJs from "snappyjs"; import {bench, describe} from "@chainsafe/benchmark"; import snappyWasm from "@chainsafe/snappy-wasm"; -describe("network / gossip / snappy", () => { +/** + * Enable this benchmark only when we enhance snappy libraries. + */ +describe.skip("network / gossip / snappy", () => { const msgLens = [ // -> 100, diff --git a/packages/beacon-node/test/spec/presets/fork_choice.test.ts b/packages/beacon-node/test/spec/presets/fork_choice.test.ts index e8faa4d1afdf..621cd9c2bd50 100644 --- a/packages/beacon-node/test/spec/presets/fork_choice.test.ts +++ b/packages/beacon-node/test/spec/presets/fork_choice.test.ts @@ -511,7 +511,13 @@ const forkChoiceTest = // // This skip can be removed once a kzg lib with run-time minimal blob size setup is released and // integrated - shouldSkip: (_testcase, name, _index) => name.includes("invalid_incorrect_proof"), + shouldSkip: (_testcase, name, _index) => + name.includes("invalid_incorrect_proof") || + // TODO GLOAS: Proposer boost specs have been changed retroactively in v1.7.0-alpha.1, + // and these tests are failing until we update our implementation. + name.includes("voting_source_beyond_two_epoch") || + name.includes("justified_update_always_if_better") || + name.includes("justified_update_not_realized_finality"), }, }; }; diff --git a/packages/beacon-node/test/spec/presets/operations.test.ts b/packages/beacon-node/test/spec/presets/operations.test.ts index 4e7b17827949..99019c685272 100644 --- a/packages/beacon-node/test/spec/presets/operations.test.ts +++ b/packages/beacon-node/test/spec/presets/operations.test.ts @@ -103,12 +103,12 @@ const operationFns: Record> = }, deposit_request: (state, testCase: {deposit_request: electra.DepositRequest}) => { - blockFns.processDepositRequest(state as CachedBeaconStateElectra, testCase.deposit_request); + const fork = state.config.getForkSeq(state.slot); + blockFns.processDepositRequest(fork, state as CachedBeaconStateElectra, testCase.deposit_request); }, consolidation_request: (state, testCase: {consolidation_request: electra.ConsolidationRequest}) => { - const fork = state.config.getForkSeq(state.slot); - blockFns.processConsolidationRequest(fork, state as CachedBeaconStateElectra, testCase.consolidation_request); + blockFns.processConsolidationRequest(state as CachedBeaconStateElectra, testCase.consolidation_request); }, execution_payload_bid: (state, testCase: {block: gloas.BeaconBlock}) => { diff --git a/packages/beacon-node/test/spec/specTestVersioning.ts b/packages/beacon-node/test/spec/specTestVersioning.ts index c4826c0a8eb8..f954c4a379e8 100644 --- a/packages/beacon-node/test/spec/specTestVersioning.ts +++ b/packages/beacon-node/test/spec/specTestVersioning.ts @@ -14,7 +14,7 @@ import {DownloadTestsOptions} from "@lodestar/spec-test-util/downloadTests"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); export const ethereumConsensusSpecsTests: DownloadTestsOptions = { - specVersion: "v1.6.1", + specVersion: "v1.7.0-alpha.1", // Target directory is the host package root: 'packages/*/spec-tests' outputDir: path.join(__dirname, "../../spec-tests"), specTestsRepoUrl: "https://github.com/ethereum/consensus-specs", diff --git a/packages/beacon-node/test/spec/utils/specTestIterator.ts b/packages/beacon-node/test/spec/utils/specTestIterator.ts index 3286cc146f0b..ee6eda830b8f 100644 --- a/packages/beacon-node/test/spec/utils/specTestIterator.ts +++ b/packages/beacon-node/test/spec/utils/specTestIterator.ts @@ -72,7 +72,7 @@ export const defaultSkipOpts: SkipOpts = { /^electra\/light_client\/single_merkle_proof\/BeaconBlockBody.*/, /^fulu\/light_client\/single_merkle_proof\/BeaconBlockBody.*/, /^.+\/light_client\/data_collection\/.*/, - /^gloas\/(finality|fork_choice|networking|sanity|transition)\/.*$/, + /^gloas\/(finality|fork_choice|sanity|transition)\/.*$/, /^gloas\/ssz_static\/ForkChoiceNode.*$/, ], skippedTests: [], diff --git a/packages/beacon-node/test/unit-minimal/chain/opPools/aggregatedAttestationPool.test.ts b/packages/beacon-node/test/unit-minimal/chain/opPools/aggregatedAttestationPool.test.ts index f27b1134f0ee..e9d58fa1145b 100644 --- a/packages/beacon-node/test/unit-minimal/chain/opPools/aggregatedAttestationPool.test.ts +++ b/packages/beacon-node/test/unit-minimal/chain/opPools/aggregatedAttestationPool.test.ts @@ -4,21 +4,14 @@ import {BitArray, fromHexString, toHexString} from "@chainsafe/ssz"; import {createBeaconConfig, createChainForkConfig} from "@lodestar/config"; import {chainConfig as chainConfigDefault} from "@lodestar/config/default"; import { - ACTIVE_PRESET, FAR_FUTURE_EPOCH, ForkName, ForkPostElectra, MAX_COMMITTEES_PER_SLOT, MAX_EFFECTIVE_BALANCE, - PresetName, SLOTS_PER_EPOCH, } from "@lodestar/params"; -import { - CachedBeaconStateAllForks, - CachedBeaconStateAltair, - CachedBeaconStateElectra, - newFilledArray, -} from "@lodestar/state-transition"; +import {CachedBeaconStateAllForks, CachedBeaconStateElectra, newFilledArray} from "@lodestar/state-transition"; import {Attestation, electra, phase0, ssz} from "@lodestar/types"; import { AggregatedAttestationPool, @@ -26,14 +19,14 @@ import { MatchingDataAttestationGroup, aggregateConsolidation, aggregateInto, - getNotSeenValidatorsFn, } from "../../../../src/chain/opPools/aggregatedAttestationPool.js"; import {InsertOutcome} from "../../../../src/chain/opPools/types.js"; +import {ShufflingCache} from "../../../../src/chain/shufflingCache.js"; import {ZERO_HASH_HEX} from "../../../../src/constants/constants.js"; import {linspace} from "../../../../src/util/numpy.js"; import {MockedForkChoice, getMockedForkChoice} from "../../../mocks/mockedBeaconChain.js"; import {renderBitArray} from "../../../utils/render.js"; -import {generateCachedAltairState, generateCachedElectraState} from "../../../utils/state.js"; +import {generateCachedElectraState} from "../../../utils/state.js"; import {generateProtoBlock} from "../../../utils/typeGenerator.js"; import {generateValidators} from "../../../utils/validator.js"; @@ -42,131 +35,6 @@ const validSignature = fromHexString( "0xb2afb700f6c561ce5e1b4fedaec9d7c06b822d38c720cf588adfda748860a940adf51634b6788f298c552de40183b5a203b2bbe8b7dd147f0bb5bc97080a12efbb631c8888cb31a99cc4706eb3711865b8ea818c10126e4d818b542e9dbf9ae8" ); -describe("AggregatedAttestationPool - Altair", () => { - if (ACTIVE_PRESET !== PresetName.minimal) { - throw Error(`ACTIVE_PRESET '${ACTIVE_PRESET}' must be minimal`); - } - - let pool: AggregatedAttestationPool; - const fork = ForkName.altair; - const altairForkEpoch = 2020; - const currentEpoch = altairForkEpoch + 10; - const currentSlot = SLOTS_PER_EPOCH * currentEpoch; - - const committeeIndex = 0; - const attestation = ssz.phase0.Attestation.defaultValue(); - // state slot is (currentSlot + 1) so if set attestation slot to currentSlot, it will be included in the block - attestation.data.slot = currentSlot - 1; - attestation.data.index = committeeIndex; - attestation.data.target.epoch = currentEpoch; - const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(attestation.data)); - - const validatorOpts = { - activationEpoch: 0, - effectiveBalance: MAX_EFFECTIVE_BALANCE, - withdrawableEpoch: FAR_FUTURE_EPOCH, - exitEpoch: FAR_FUTURE_EPOCH, - }; - // this makes a committee length of 4 - const vc = 64; - const committeeLength = 4; - const validators = generateValidators(vc, validatorOpts); - const originalState = generateCachedAltairState({slot: currentSlot + 1, validators}, altairForkEpoch); - const committee = originalState.epochCtx.getBeaconCommittee(currentSlot - 1, committeeIndex); - expect(committee.length).toEqual(committeeLength); - // 0 and 1 in committee are fully participated - const epochParticipation = newFilledArray(vc, 0b111); - for (let i = 0; i < committeeLength; i++) { - if (i === 0 || i === 1) { - epochParticipation[committee[i]] = 0b111; - } else { - epochParticipation[committee[i]] = 0b000; - } - } - (originalState as CachedBeaconStateAltair).previousEpochParticipation = - ssz.altair.EpochParticipation.toViewDU(epochParticipation); - (originalState as CachedBeaconStateAltair).currentEpochParticipation = - ssz.altair.EpochParticipation.toViewDU(epochParticipation); - originalState.commit(); - let altairState: CachedBeaconStateAllForks; - - let forkchoiceStub: MockedForkChoice; - const config = createBeaconConfig( - createChainForkConfig({...chainConfigDefault, ALTAIR_FORK_EPOCH: altairForkEpoch}), - originalState.genesisValidatorsRoot - ); - - beforeEach(() => { - pool = new AggregatedAttestationPool(config); - altairState = originalState.clone(); - forkchoiceStub = getMockedForkChoice(); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - it("getNotSeenValidatorsFn", () => { - // previousEpochParticipation and currentEpochParticipation is created inside generateCachedState - // 0 and 1 are fully participated - const notSeenValidatorFn = getNotSeenValidatorsFn(config, altairState); - // seen attesting indices are 0, 1 => not seen are 2, 3 - expect(notSeenValidatorFn(currentEpoch, currentSlot - 1, committeeIndex)).toEqual(new Set([2, 3])); - // attestations in current slot are always included (since altairState.slot = currentSlot + 1) - expect(notSeenValidatorFn(currentEpoch, currentSlot, committeeIndex)).toEqual(new Set([0, 1, 2, 3])); - }); - - // previousEpochParticipation and currentEpochParticipation is created inside generateCachedState - // 0 and 1 are fully participated - const testCases: {name: string; attestingBits: number[]; isReturned: boolean}[] = [ - {name: "all validators are seen", attestingBits: [0b00000011], isReturned: false}, - {name: "all validators are NOT seen", attestingBits: [0b00001100], isReturned: true}, - {name: "one is seen and one is NOT", attestingBits: [0b00001101], isReturned: true}, - ]; - - for (const {name, attestingBits, isReturned} of testCases) { - it(name, () => { - const aggregationBits = new BitArray(new Uint8Array(attestingBits), committeeLength); - pool.add( - {...attestation, aggregationBits}, - attDataRootHex, - aggregationBits.getTrueBitIndexes().length, - committee - ); - forkchoiceStub.getBlockHex.mockReturnValue(generateProtoBlock({slot: attestation.data.slot})); - forkchoiceStub.getDependentRoot.mockReturnValue(ZERO_HASH_HEX); - if (isReturned) { - expect(pool.getAttestationsForBlock(fork, forkchoiceStub, altairState).length).toBeGreaterThan(0); - } else { - expect(pool.getAttestationsForBlock(fork, forkchoiceStub, altairState).length).toEqual(0); - } - // "forkchoice should be called to check pivot block" - expect(forkchoiceStub.getDependentRoot).toHaveBeenCalledTimes(1); - }); - } - - it("incorrect source", () => { - altairState.currentJustifiedCheckpoint.epoch = 1000; - // all attesters are not seen - const attestingIndices = [2, 3]; - pool.add(attestation, attDataRootHex, attestingIndices.length, committee); - expect(pool.getAttestationsForBlock(fork, forkchoiceStub, altairState)).toEqual([]); - // "forkchoice should not be called" - expect(forkchoiceStub.iterateAncestorBlocks).not.toHaveBeenCalledTimes(1); - }); - - it("incompatible shuffling - incorrect pivot block root", () => { - // all attesters are not seen - const attestingIndices = [2, 3]; - pool.add(attestation, attDataRootHex, attestingIndices.length, committee); - forkchoiceStub.getBlockHex.mockReturnValue(generateProtoBlock({slot: attestation.data.slot})); - forkchoiceStub.getDependentRoot.mockReturnValue("0xWeird"); - expect(pool.getAttestationsForBlock(fork, forkchoiceStub, altairState)).toEqual([]); - // "forkchoice should be called to check pivot block" - expect(forkchoiceStub.getDependentRoot).toHaveBeenCalledTimes(1); - }); -}); - describe("AggregatedAttestationPool - get packed attestations - Electra", () => { let pool: AggregatedAttestationPool; const fork = ForkName.electra; @@ -381,7 +249,9 @@ describe("AggregatedAttestationPool - get packed attestations - Electra", () => forkchoiceStub.getBlockHex.mockReturnValue(generateProtoBlock()); forkchoiceStub.getDependentRoot.mockReturnValue(ZERO_HASH_HEX); - const blockAttestations = pool.getAttestationsForBlock(fork, forkchoiceStub, electraState); + const shufflingCache = new ShufflingCache(); + shufflingCache.processState(electraState); + const blockAttestations = pool.getAttestationsForBlock(fork, forkchoiceStub, shufflingCache, electraState); // make sure test data is correct expect(packedCommitteeBits.length).toBe(packedAggregationBitsLen.length); expect(blockAttestations.length).toBe(packedCommitteeBits.length); diff --git a/packages/beacon-node/test/unit-minimal/chain/stateCache/persistentCheckpointsCache.test.ts b/packages/beacon-node/test/unit-minimal/chain/stateCache/persistentCheckpointsCache.test.ts index d3cfff70159a..aa503a0836fd 100644 --- a/packages/beacon-node/test/unit-minimal/chain/stateCache/persistentCheckpointsCache.test.ts +++ b/packages/beacon-node/test/unit-minimal/chain/stateCache/persistentCheckpointsCache.test.ts @@ -5,9 +5,12 @@ import {ACTIVE_PRESET, PresetName, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} f import {CachedBeaconStateAllForks, computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {RootHex, phase0} from "@lodestar/types"; import {mapValues, toHexString} from "@lodestar/utils"; -import {FIFOBlockStateCache, toCheckpointHex} from "../../../../src/chain/index.js"; +import {FIFOBlockStateCache} from "../../../../src/chain/index.js"; import {checkpointToDatastoreKey} from "../../../../src/chain/stateCache/datastore/index.js"; -import {PersistentCheckpointStateCache} from "../../../../src/chain/stateCache/persistentCheckpointsCache.js"; +import { + PersistentCheckpointStateCache, + toCheckpointHex, +} from "../../../../src/chain/stateCache/persistentCheckpointsCache.js"; import {CheckpointHex} from "../../../../src/chain/stateCache/types.js"; import {getTestDatastore} from "../../../utils/chain/stateCache/datastore.js"; import {testLogger} from "../../../utils/logger.js"; diff --git a/packages/beacon-node/test/unit/api/impl/beacon/blocks/getBlockHeaders.test.ts b/packages/beacon-node/test/unit/api/impl/beacon/blocks/getBlockHeaders.test.ts index 195c16c9a7cf..d7fe25bb7a4e 100644 --- a/packages/beacon-node/test/unit/api/impl/beacon/blocks/getBlockHeaders.test.ts +++ b/packages/beacon-node/test/unit/api/impl/beacon/blocks/getBlockHeaders.test.ts @@ -16,7 +16,6 @@ describe("api - beacon - getBlockHeaders", () => { modules = getApiTestModules(); api = getBeaconBlockApi(modules); - vi.spyOn(modules.db.block, "get"); vi.spyOn(modules.db.blockArchive, "getByParentRoot"); }); @@ -42,7 +41,11 @@ describe("api - beacon - getBlockHeaders", () => { const blockFromDb3 = ssz.phase0.SignedBeaconBlock.defaultValue(); blockFromDb3.message.slot = 3; - modules.db.block.get.mockResolvedValue(blockFromDb3); + modules.chain.getBlockByRoot.mockResolvedValue({ + block: blockFromDb3, + executionOptimistic: false, + finalized: false, + }); modules.db.blockArchive.get.mockResolvedValue(null); const {data: blockHeaders} = (await api.getBlockHeaders({})) as {data: routes.beacon.BlockHeaderResponse[]}; @@ -52,7 +55,7 @@ describe("api - beacon - getBlockHeaders", () => { expect(modules.forkChoice.getHead).toHaveBeenCalledTimes(1); expect(modules.chain.getCanonicalBlockAtSlot).toHaveBeenCalledTimes(1); expect(modules.forkChoice.getBlockSummariesAtSlot).toHaveBeenCalledTimes(1); - expect(modules.db.block.get).toHaveBeenCalledTimes(1); + expect(modules.chain.getBlockByRoot).toHaveBeenCalledTimes(1); }); it("future slot", async () => { @@ -90,8 +93,16 @@ describe("api - beacon - getBlockHeaders", () => { when(modules.forkChoice.getCanonicalBlockAtSlot) .calledWith(2) .thenReturn(generateProtoBlock({blockRoot: toHexString(ssz.phase0.BeaconBlock.hashTreeRoot(canonical.message))})); - modules.db.block.get.mockResolvedValue(generateSignedBlockAtSlot(1)); - modules.db.block.get.mockResolvedValue(generateSignedBlockAtSlot(2)); + modules.chain.getBlockByRoot.mockResolvedValueOnce({ + block: generateSignedBlockAtSlot(1), + executionOptimistic: false, + finalized: false, + }); + modules.chain.getBlockByRoot.mockResolvedValueOnce({ + block: generateSignedBlockAtSlot(2), + executionOptimistic: false, + finalized: false, + }); const {data: blockHeaders} = (await api.getBlockHeaders({parentRoot})) as { data: routes.beacon.BlockHeaderResponse[]; }; @@ -103,7 +114,11 @@ describe("api - beacon - getBlockHeaders", () => { modules.db.blockArchive.getByParentRoot.mockResolvedValue(null); modules.forkChoice.getBlockSummariesByParentRoot.mockReturnValue([generateProtoBlock({slot: 1})]); when(modules.forkChoice.getCanonicalBlockAtSlot).calledWith(1).thenReturn(generateProtoBlock()); - modules.db.block.get.mockResolvedValue(generateSignedBlockAtSlot(1)); + modules.chain.getBlockByRoot.mockResolvedValue({ + block: generateSignedBlockAtSlot(1), + executionOptimistic: false, + finalized: false, + }); const {data: blockHeaders} = await api.getBlockHeaders({parentRoot}); expect(blockHeaders.length).toBe(1); @@ -127,8 +142,16 @@ describe("api - beacon - getBlockHeaders", () => { when(modules.forkChoice.getCanonicalBlockAtSlot) .calledWith(2) .thenReturn(generateProtoBlock({blockRoot: toHexString(ssz.phase0.BeaconBlock.hashTreeRoot(canonical.message))})); - modules.db.block.get.mockResolvedValueOnce(generateSignedBlockAtSlot(1)); - modules.db.block.get.mockResolvedValueOnce(generateSignedBlockAtSlot(2)); + modules.chain.getBlockByRoot.mockResolvedValueOnce({ + block: generateSignedBlockAtSlot(1), + executionOptimistic: false, + finalized: false, + }); + modules.chain.getBlockByRoot.mockResolvedValueOnce({ + block: generateSignedBlockAtSlot(2), + executionOptimistic: false, + finalized: false, + }); const {data: blockHeaders} = await api.getBlockHeaders({ parentRoot: toHexString(Buffer.alloc(32, 1)), slot: 1, diff --git a/packages/beacon-node/test/unit/api/impl/validator/duties/proposer.test.ts b/packages/beacon-node/test/unit/api/impl/validator/duties/proposer.test.ts index ae946811e540..75af4f18eace 100644 --- a/packages/beacon-node/test/unit/api/impl/validator/duties/proposer.test.ts +++ b/packages/beacon-node/test/unit/api/impl/validator/duties/proposer.test.ts @@ -108,7 +108,7 @@ describe("get proposers api impl", () => { it("should get proposers for historical epoch", async () => { const historicalEpoch = currentEpoch - 2; initializeState(currentSlot - 2 * SLOTS_PER_EPOCH); - modules.chain.getStateBySlot.mockResolvedValue({state, executionOptimistic: false, finalized: true}); + modules.chain.getStateBySlot.mockResolvedValue({state: cachedState, executionOptimistic: false, finalized: true}); const {data: result} = (await api.getProposerDuties({epoch: historicalEpoch})) as { data: routes.validator.ProposerDutyList; diff --git a/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts b/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts index 773e7930e42c..5c76c2599cc6 100644 --- a/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts +++ b/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts @@ -1,11 +1,11 @@ import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"; -import {fromHexString, toHexString} from "@chainsafe/ssz"; import {routes} from "@lodestar/api"; import {createBeaconConfig, createChainForkConfig, defaultChainConfig} from "@lodestar/config"; import {ProtoBlock} from "@lodestar/fork-choice"; import {ForkName, SLOTS_PER_EPOCH, ZERO_HASH_HEX} from "@lodestar/params"; import {CachedBeaconStateBellatrix, G2_POINT_AT_INFINITY, computeTimeAtSlot} from "@lodestar/state-transition"; import {ssz} from "@lodestar/types"; +import {toRootHex} from "@lodestar/utils"; import {getValidatorApi} from "../../../../../src/api/impl/validator/index.js"; import {defaultApiOptions} from "../../../../../src/api/options.js"; import {BeaconChain} from "../../../../../src/chain/chain.js"; @@ -93,9 +93,9 @@ describe("api/validator - produceBlockV3", () => { vi.spyOn(modules.chain.clock, "currentSlot", "get").mockReturnValue(currentSlot); vi.spyOn(modules.sync, "state", "get").mockReturnValue(SyncState.Synced); modules.chain.recomputeForkChoiceHead.mockReturnValue({ - blockRoot: toHexString(fullBlock.parentRoot), + blockRoot: toRootHex(fullBlock.parentRoot), } as ProtoBlock); - modules.chain.getProposerHead.mockReturnValue({blockRoot: toHexString(fullBlock.parentRoot)} as ProtoBlock); + modules.chain.getProposerHead.mockReturnValue({blockRoot: toRootHex(fullBlock.parentRoot)} as ProtoBlock); modules.chain.forkChoice.getBlock.mockReturnValue(zeroProtoBlock); modules.chain.produceCommonBlockBody.mockResolvedValue({ attestations: fullBlock.body.attestations, @@ -179,16 +179,13 @@ describe("api/validator - produceBlockV3", () => { const slot = 100000; const randaoReveal = fullBlock.body.randaoReveal; const parentBlockRoot = fullBlock.parentRoot; + const parentBlock = generateProtoBlock({blockRoot: toRootHex(parentBlockRoot), slot: currentSlot - 1}); const graffiti = "a".repeat(32); const feeRecipient = "0xcccccccccccccccccccccccccccccccccccccccc"; - modules.chain.getProposerHead.mockReturnValue( - generateProtoBlock({blockRoot: toHexString(parentBlockRoot), slot: currentSlot - 1}) - ); - modules.chain.recomputeForkChoiceHead.mockReturnValue( - generateProtoBlock({blockRoot: toHexString(parentBlockRoot)}) - ); - modules.chain.forkChoice.getBlock.mockReturnValue(generateProtoBlock({blockRoot: toHexString(parentBlockRoot)})); + modules.chain.getProposerHead.mockReturnValue(parentBlock); + modules.chain.recomputeForkChoiceHead.mockReturnValue(parentBlock); + modules.chain.forkChoice.getBlock.mockReturnValue(parentBlock); modules.chain.produceBlock.mockResolvedValue({ block: fullBlock, executionPayloadValue, @@ -213,7 +210,7 @@ describe("api/validator - produceBlockV3", () => { randaoReveal, graffiti: toGraffitiBytes(graffiti), slot, - parentBlockRoot, + parentBlock, feeRecipient, commonBlockBodyPromise: expect.any(Promise), }); @@ -225,7 +222,7 @@ describe("api/validator - produceBlockV3", () => { randaoReveal, graffiti: toGraffitiBytes(graffiti), slot, - parentBlockRoot, + parentBlock, feeRecipient: undefined, commonBlockBodyPromise: expect.any(Promise), }); @@ -279,7 +276,7 @@ describe("api/validator - produceBlockV3", () => { graffiti: toGraffitiBytes(graffiti), slot, feeRecipient, - parentBlockRoot: fromHexString(ZERO_HASH_HEX), + parentBlock: generateProtoBlock({blockRoot: ZERO_HASH_HEX}), proposerIndex: 0, proposerPubKey: new Uint8Array(32).fill(1), commonBlockBodyPromise: createCommonBlockBodyPromise(), @@ -304,7 +301,7 @@ describe("api/validator - produceBlockV3", () => { randaoReveal, graffiti: toGraffitiBytes(graffiti), slot, - parentBlockRoot: fromHexString(ZERO_HASH_HEX), + parentBlock: generateProtoBlock({blockRoot: ZERO_HASH_HEX}), proposerIndex: 0, proposerPubKey: new Uint8Array(32).fill(1), commonBlockBodyPromise: createCommonBlockBodyPromise(), diff --git a/packages/beacon-node/test/unit/chain/bls/bls.test.ts b/packages/beacon-node/test/unit/chain/bls/bls.test.ts index c0a82454d156..5a325c492228 100644 --- a/packages/beacon-node/test/unit/chain/bls/bls.test.ts +++ b/packages/beacon-node/test/unit/chain/bls/bls.test.ts @@ -9,9 +9,11 @@ describe("BlsVerifier ", () => { // take time for creating thread pool const numKeys = 3; const secretKeys = Array.from({length: numKeys}, (_, i) => SecretKey.fromKeygen(Buffer.alloc(32, i))); + // Create a mock index2pubkey that maps indices to public keys + const index2pubkey = secretKeys.map((sk) => sk.toPublicKey()); const verifiers = [ - new BlsSingleThreadVerifier({metrics: null}), - new BlsMultiThreadWorkerPool({}, {metrics: null, logger: testLogger()}), + new BlsSingleThreadVerifier({metrics: null, index2pubkey}), + new BlsMultiThreadWorkerPool({}, {metrics: null, logger: testLogger(), index2pubkey}), ]; for (const verifier of verifiers) { diff --git a/packages/beacon-node/test/unit/chain/stateCache/blockStateCacheImpl.test.ts b/packages/beacon-node/test/unit/chain/stateCache/blockStateCacheImpl.test.ts deleted file mode 100644 index 18a0b8810abe..000000000000 --- a/packages/beacon-node/test/unit/chain/stateCache/blockStateCacheImpl.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import {beforeEach, describe, expect, it} from "vitest"; -import {toHexString} from "@chainsafe/ssz"; -import {SLOTS_PER_EPOCH} from "@lodestar/params"; -import {EpochShuffling} from "@lodestar/state-transition"; -import {Root} from "@lodestar/types"; -import {BlockStateCacheImpl} from "../../../../src/chain/stateCache/index.js"; -import {ZERO_HASH} from "../../../../src/constants/index.js"; -import {generateCachedState} from "../../../utils/state.js"; - -describe("BlockStateCacheImpl", () => { - let cache: BlockStateCacheImpl; - let key1: Root, key2: Root; - const shuffling: EpochShuffling = { - epoch: 0, - activeIndices: new Uint32Array(), - shuffling: new Uint32Array(), - committees: [], - committeesPerSlot: 1, - }; - - beforeEach(() => { - // max 2 items - cache = new BlockStateCacheImpl({maxStates: 2}); - const state1 = generateCachedState({slot: 0}); - key1 = state1.hashTreeRoot(); - state1.epochCtx.currentShuffling = {...shuffling, epoch: 0}; - cache.add(state1); - const state2 = generateCachedState({slot: 1 * SLOTS_PER_EPOCH}); - key2 = state2.hashTreeRoot(); - state2.epochCtx.currentShuffling = {...shuffling, epoch: 1}; - cache.add(state2); - }); - - it("should prune", () => { - expect(cache.size).toBe(2); - const state3 = generateCachedState({slot: 2 * SLOTS_PER_EPOCH}); - state3.epochCtx.currentShuffling = {...shuffling, epoch: 2}; - - cache.add(state3); - expect(cache.size).toBe(3); - cache.prune(toHexString(ZERO_HASH)); - expect(cache.size).toBe(2); - // "must have key1" - expect(cache.get(toHexString(key1))).toBeDefined(); - // "must have key2" - expect(cache.get(toHexString(key2))).toBeDefined(); - }); - - it("should deleteAllBeforeEpoch", () => { - cache.deleteAllBeforeEpoch(2); - expect(cache.size).toBe(0); - }); -}); diff --git a/packages/beacon-node/test/unit/chain/stateCache/inMemoryCheckpointsCache.test.ts b/packages/beacon-node/test/unit/chain/stateCache/inMemoryCheckpointsCache.test.ts deleted file mode 100644 index ad14d56b4462..000000000000 --- a/packages/beacon-node/test/unit/chain/stateCache/inMemoryCheckpointsCache.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -import {beforeAll, beforeEach, describe, expect, it} from "vitest"; -import {SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; -import {CachedBeaconStateAllForks, computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition"; -import {phase0} from "@lodestar/types"; -import { - CheckpointHex, - InMemoryCheckpointStateCache, - toCheckpointHex, -} from "../../../../src/chain/stateCache/inMemoryCheckpointsCache.js"; -import {generateCachedState} from "../../../utils/state.js"; - -describe("InMemoryCheckpointStateCache", () => { - let root0a: Buffer, root0b: Buffer, root1: Buffer, root2: Buffer; - let cp0a: phase0.Checkpoint, cp0b: phase0.Checkpoint, cp1: phase0.Checkpoint, cp2: phase0.Checkpoint; - let cp0aHex: CheckpointHex, cp0bHex: CheckpointHex, cp1Hex: CheckpointHex, cp2Hex: CheckpointHex; - let states: Record<"cp0a" | "cp0b" | "cp1" | "cp2", CachedBeaconStateAllForks>; - - let cache: InMemoryCheckpointStateCache; - - const startSlotEpoch20 = computeStartSlotAtEpoch(20); - const startSlotEpoch21 = computeStartSlotAtEpoch(21); - const startSlotEpoch22 = computeStartSlotAtEpoch(22); - - beforeAll(() => { - root0a = Buffer.alloc(32); - root0b = Buffer.alloc(32, 1); - root1 = Buffer.alloc(32, 2); - root2 = Buffer.alloc(32, 3); - root0b[31] = 1; - // epoch: 19 20 21 22 23 - // |-----------|-----------|-----------|-----------| - // ^^ ^ ^ - // || | | - // |0b--------root1--------root2 - // | - // 0a - // root0a is of the last slot of epoch 19 - cp0a = {epoch: 20, root: root0a}; - // root0b is of the first slot of epoch 20 - cp0b = {epoch: 20, root: root0b}; - cp1 = {epoch: 21, root: root1}; - cp2 = {epoch: 22, root: root2}; - [cp0aHex, cp0bHex, cp1Hex, cp2Hex] = [cp0a, cp0b, cp1, cp2].map((cp) => toCheckpointHex(cp)); - const allStates = [cp0a, cp0b, cp1, cp2] - .map((cp) => generateCachedState({slot: cp.epoch * SLOTS_PER_EPOCH})) - .map((state, i) => { - const stateEpoch = computeEpochAtSlot(state.slot); - if (stateEpoch === 20 && i === 0) { - // cp0a - state.blockRoots.set((startSlotEpoch20 - 1) % SLOTS_PER_HISTORICAL_ROOT, root0a); - state.blockRoots.set(startSlotEpoch20 % SLOTS_PER_HISTORICAL_ROOT, root0a); - return state; - } - - // other states based on cp0b - state.blockRoots.set((startSlotEpoch20 - 1) % SLOTS_PER_HISTORICAL_ROOT, root0a); - state.blockRoots.set(startSlotEpoch20 % SLOTS_PER_HISTORICAL_ROOT, root0b); - - if (stateEpoch >= 21) { - state.blockRoots.set(startSlotEpoch21 % SLOTS_PER_HISTORICAL_ROOT, root1); - } - if (stateEpoch >= 22) { - state.blockRoots.set(startSlotEpoch22 % SLOTS_PER_HISTORICAL_ROOT, root2); - } - return state; - }); - - states = { - // Previous Root Checkpoint State of epoch 20 - cp0a: allStates[0], - // Current Root Checkpoint State of epoch 20 - cp0b: allStates[1], - // Current Root Checkpoint State of epoch 21 - cp1: allStates[2], - // Current Root Checkpoint State of epoch 22 - cp2: allStates[3], - }; - - for (const state of allStates) { - state.hashTreeRoot(); - } - }); - - beforeEach(() => { - cache = new InMemoryCheckpointStateCache({}, {maxEpochs: 0}); - cache.add(cp0a, states["cp0a"]); - cache.add(cp0b, states["cp0b"]); - cache.add(cp1, states["cp1"]); - }); - - it("getLatest", () => { - // cp0 - expect(cache.getLatest(cp0aHex.rootHex, cp0a.epoch)?.hashTreeRoot()).toEqual(states["cp0a"].hashTreeRoot()); - expect(cache.getLatest(cp0aHex.rootHex, cp0a.epoch + 1)?.hashTreeRoot()).toEqual(states["cp0a"].hashTreeRoot()); - expect(cache.getLatest(cp0aHex.rootHex, cp0a.epoch - 1)?.hashTreeRoot()).toBeUndefined(); - - // cp1 - expect(cache.getLatest(cp1Hex.rootHex, cp1.epoch)?.hashTreeRoot()).toEqual(states["cp1"].hashTreeRoot()); - expect(cache.getLatest(cp1Hex.rootHex, cp1.epoch + 1)?.hashTreeRoot()).toEqual(states["cp1"].hashTreeRoot()); - expect(cache.getLatest(cp1Hex.rootHex, cp1.epoch - 1)?.hashTreeRoot()).toBeUndefined(); - - // cp2 - expect(cache.getLatest(cp2Hex.rootHex, cp2.epoch)?.hashTreeRoot()).toBeUndefined(); - }); - - it("getStateOrBytes", async () => { - expect(((await cache.getStateOrBytes(cp0aHex)) as CachedBeaconStateAllForks).hashTreeRoot()).toEqual( - states["cp0a"].hashTreeRoot() - ); - expect(((await cache.getStateOrBytes(cp0bHex)) as CachedBeaconStateAllForks).hashTreeRoot()).toEqual( - states["cp0b"].hashTreeRoot() - ); - expect(((await cache.getStateOrBytes(cp1Hex)) as CachedBeaconStateAllForks).hashTreeRoot()).toEqual( - states["cp1"].hashTreeRoot() - ); - expect(await cache.getStateOrBytes(cp2Hex)).toBeNull(); - }); - - it("get", () => { - expect((cache.get(cp0aHex) as CachedBeaconStateAllForks).hashTreeRoot()).toEqual(states["cp0a"].hashTreeRoot()); - expect((cache.get(cp0bHex) as CachedBeaconStateAllForks).hashTreeRoot()).toEqual(states["cp0b"].hashTreeRoot()); - expect((cache.get(cp1Hex) as CachedBeaconStateAllForks).hashTreeRoot()).toEqual(states["cp1"].hashTreeRoot()); - expect(cache.get(cp2Hex) as CachedBeaconStateAllForks).toBeNull(); - }); - - it("pruneFinalized", () => { - cache.pruneFinalized(21); - expect(cache.get(cp0aHex) as CachedBeaconStateAllForks).toBeNull(); - expect(cache.get(cp0bHex) as CachedBeaconStateAllForks).toBeNull(); - expect((cache.get(cp1Hex) as CachedBeaconStateAllForks).hashTreeRoot()).toEqual(states["cp1"].hashTreeRoot()); - }); - - it("prune", () => { - cache.add(cp2, states["cp2"]); - const finalizedEpoch = 21; - const justifiedEpoch = 22; - cache.prune(finalizedEpoch, justifiedEpoch); - expect(cache.get(cp0aHex) as CachedBeaconStateAllForks).toBeNull(); - expect(cache.get(cp0bHex) as CachedBeaconStateAllForks).toBeNull(); - expect((cache.get(cp1Hex) as CachedBeaconStateAllForks).hashTreeRoot()).toEqual(states["cp1"].hashTreeRoot()); - expect((cache.get(cp2Hex) as CachedBeaconStateAllForks).hashTreeRoot()).toEqual(states["cp2"].hashTreeRoot()); - }); -}); diff --git a/packages/beacon-node/test/unit/chain/validation/attestation/validateGossipAttestationsSameAttData.test.ts b/packages/beacon-node/test/unit/chain/validation/attestation/validateGossipAttestationsSameAttData.test.ts index d44413ba3eac..fa3e155af54d 100644 --- a/packages/beacon-node/test/unit/chain/validation/attestation/validateGossipAttestationsSameAttData.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/attestation/validateGossipAttestationsSameAttData.test.ts @@ -56,13 +56,22 @@ describe("validateGossipAttestationsSameAttData", () => { return keypair; } + // Build index2pubkey cache for test + const index2pubkey: PublicKey[] = []; + for (let i = 0; i < 10; i++) { + index2pubkey.push(getKeypair(i).publicKey); + } + // Add a special keypair for invalid signatures + index2pubkey[2023] = getKeypair(2023).publicKey; + let chain: IBeaconChain; const signingRoot = Buffer.alloc(32, 1); beforeEach(() => { chain = { - bls: new BlsSingleThreadVerifier({metrics: null}), + bls: new BlsSingleThreadVerifier({metrics: null, index2pubkey}), seenAttesters: new SeenAttesters(), + index2pubkey, opts: { minSameMessageSignatureSetsToBatch: 2, } as IBeaconChain["opts"], @@ -78,17 +87,19 @@ describe("validateGossipAttestationsSameAttData", () => { it(`test case ${testCaseIndex}`, async () => { const phase0Results: Promise[] = []; for (const [i, isValid] of phase0Result.entries()) { + // Create an indexed signature set + let signature = getKeypair(i).secretKey.sign(signingRoot).toBytes(); + if (isValid && !phase1Result[i]) { + // invalid signature - sign with a different key + signature = getKeypair(2023).secretKey.sign(signingRoot).toBytes(); + } const signatureSet = { - type: SignatureSetType.single, - pubkey: getKeypair(i).publicKey, + type: SignatureSetType.indexed as const, + index: i, signingRoot, - signature: getKeypair(i).secretKey.sign(signingRoot).toBytes(), + signature, }; if (isValid) { - if (!phase1Result[i]) { - // invalid signature - signatureSet.signature = getKeypair(2023).secretKey.sign(signingRoot).toBytes(); - } phase0Results.push( Promise.resolve({ attestation: ssz.phase0.Attestation.defaultValue(), diff --git a/packages/beacon-node/test/unit/chain/validatorMonitor.test.ts b/packages/beacon-node/test/unit/chain/validatorMonitor.test.ts new file mode 100644 index 000000000000..8b4a5687b43e --- /dev/null +++ b/packages/beacon-node/test/unit/chain/validatorMonitor.test.ts @@ -0,0 +1,130 @@ +import {describe, expect, it, vi} from "vitest"; +import {createChainForkConfig, defaultChainConfig} from "@lodestar/config"; +import {SLOTS_PER_EPOCH} from "@lodestar/params"; +import {createValidatorMonitor} from "../../../src/chain/validatorMonitor.js"; +import {testLogger} from "../../utils/logger.js"; + +describe("ValidatorMonitor", () => { + // Use phase0 config (no altair) to avoid needing full state with block roots + const config = createChainForkConfig({ + ...defaultChainConfig, + ALTAIR_FORK_EPOCH: Infinity, + BELLATRIX_FORK_EPOCH: Infinity, + CAPELLA_FORK_EPOCH: Infinity, + DENEB_FORK_EPOCH: Infinity, + ELECTRA_FORK_EPOCH: Infinity, + }); + + const genesisTime = Math.floor(Date.now() / 1000) - 3600; // 1 hour ago + const logger = testLogger("validatorMonitor"); + + // Helper to create a minimal mock head state for phase0 + function createMockHeadState(slot: number) { + return { + slot, + epochCtx: { + proposersPrevEpoch: null, + }, + } as any; + } + + describe("registerLocalValidator", () => { + it("should register new validators and track them", () => { + const monitor = createValidatorMonitor(null, config, genesisTime, logger, {}); + + monitor.registerLocalValidator(1); + monitor.registerLocalValidator(2); + monitor.registerLocalValidator(3); + + const indices = monitor.getMonitoredValidatorIndices(); + expect(indices).toHaveLength(3); + expect(indices).toContain(1); + expect(indices).toContain(2); + expect(indices).toContain(3); + }); + + it("should not duplicate validators on re-registration", () => { + const monitor = createValidatorMonitor(null, config, genesisTime, logger, {}); + + monitor.registerLocalValidator(1); + monitor.registerLocalValidator(1); // Register again + + const indices = monitor.getMonitoredValidatorIndices(); + expect(indices).toHaveLength(1); + expect(indices).toContain(1); + }); + }); + + describe("onceEveryEndOfEpoch pruning", () => { + it("should prune validators not seen within retain period", () => { + const monitor = createValidatorMonitor(null, config, genesisTime, logger, {}); + + // Register a validator + monitor.registerLocalValidator(1); + expect(monitor.getMonitoredValidatorIndices()).toContain(1); + + // Create a mock head state + const slot = SLOTS_PER_EPOCH * 2; // End of epoch 1 + const headState = createMockHeadState(slot); + + // Mock Date.now to be far in the future (beyond retain period) + const originalDateNow = Date.now; + const retainMs = SLOTS_PER_EPOCH * config.SLOT_DURATION_MS * 2; + vi.spyOn(Date, "now").mockReturnValue(originalDateNow() + retainMs + 1000); + + // Call onceEveryEndOfEpoch - this should prune the validator + monitor.onceEveryEndOfEpoch(headState); + + // Validator should be pruned + expect(monitor.getMonitoredValidatorIndices()).not.toContain(1); + + // Restore Date.now + vi.restoreAllMocks(); + }); + + it("should not prune validators within retain period", () => { + const monitor = createValidatorMonitor(null, config, genesisTime, logger, {}); + + // Register a validator + monitor.registerLocalValidator(1); + expect(monitor.getMonitoredValidatorIndices()).toContain(1); + + // Create a mock head state + const slot = SLOTS_PER_EPOCH * 2; + const headState = createMockHeadState(slot); + + // Call onceEveryEndOfEpoch without mocking time (validator was just registered) + monitor.onceEveryEndOfEpoch(headState); + + // Validator should still be there + expect(monitor.getMonitoredValidatorIndices()).toContain(1); + }); + + it("should not prune re-registered validators even after initial retain period", () => { + const monitor = createValidatorMonitor(null, config, genesisTime, logger, {}); + const retainMs = SLOTS_PER_EPOCH * config.SLOT_DURATION_MS * 2; + const baseTime = Date.now(); + + // Register a validator at initial time + vi.spyOn(Date, "now").mockReturnValue(baseTime); + monitor.registerLocalValidator(1); + expect(monitor.getMonitoredValidatorIndices()).toContain(1); + + // Advance time past the retain period, but re-register the validator before pruning + vi.spyOn(Date, "now").mockReturnValue(baseTime + retainMs + 1000); + monitor.registerLocalValidator(1); // Re-register updates lastRegisteredTimeMs + + // Create a mock head state + const slot = SLOTS_PER_EPOCH * 2; + const headState = createMockHeadState(slot); + + // Call onceEveryEndOfEpoch - validator should NOT be pruned due to re-registration + monitor.onceEveryEndOfEpoch(headState); + + // Validator should still be there because re-registration updated the timestamp + expect(monitor.getMonitoredValidatorIndices()).toContain(1); + + vi.restoreAllMocks(); + }); + }); +}); diff --git a/packages/beacon-node/test/unit/db/api/repository.test.ts b/packages/beacon-node/test/unit/db/api/repository.test.ts index b75d62e12310..3bed3675456d 100644 --- a/packages/beacon-node/test/unit/db/api/repository.test.ts +++ b/packages/beacon-node/test/unit/db/api/repository.test.ts @@ -21,6 +21,7 @@ vi.mock("@lodestar/db/controller/level", async (importOriginal) => { valuesStream: vi.fn(), batchDelete: vi.fn(), batchPut: vi.fn(), + batch: vi.fn(), }; }), }; diff --git a/packages/beacon-node/test/unit/network/gossip/directPeers.test.ts b/packages/beacon-node/test/unit/network/gossip/directPeers.test.ts new file mode 100644 index 000000000000..17ef0b151984 --- /dev/null +++ b/packages/beacon-node/test/unit/network/gossip/directPeers.test.ts @@ -0,0 +1,162 @@ +import {generateKeyPair} from "@libp2p/crypto/keys"; +import {multiaddr} from "@multiformats/multiaddr"; +import {beforeEach, describe, expect, it} from "vitest"; +import {SignableENR} from "@chainsafe/enr"; +import {parseDirectPeers} from "../../../../src/network/gossip/gossipsub.js"; +import {MockedLogger, getMockedLogger} from "../../../mocks/loggerMock.js"; + +describe("network / gossip / directPeers", () => { + let logger: MockedLogger; + + beforeEach(() => { + logger = getMockedLogger(); + }); + + describe("parseDirectPeers", () => { + it("should parse valid multiaddr with peer ID", () => { + const peerIdStr = "16Uiu2HAkuWPWqF4W3aw9oo5Yw79v5muzBaaGTGKMmuqjPfEyfkwu"; + const multiaddrs = [`/ip4/192.168.1.1/tcp/9000/p2p/${peerIdStr}`]; + + const result = parseDirectPeers(multiaddrs, logger); + + expect(result).toHaveLength(1); + expect(result[0].id.toString()).toBe(peerIdStr); + expect(result[0].addrs).toHaveLength(1); + expect(result[0].addrs[0].toString()).toBe("/ip4/192.168.1.1/tcp/9000"); + expect(logger.info).toHaveBeenCalledWith("Added direct peer", { + peerId: peerIdStr, + addr: "/ip4/192.168.1.1/tcp/9000", + }); + }); + + it("should parse multiple valid multiaddrs", () => { + const peerIdStr1 = "16Uiu2HAkuWPWqF4W3aw9oo5Yw79v5muzBaaGTGKMmuqjPfEyfkwu"; + const peerIdStr2 = "16Uiu2HAmKLhW7HiWkVNSbsZjThQTiMAqDptiqyE8FRWsRz6e8WPF"; + const multiaddrs = [`/ip4/192.168.1.1/tcp/9000/p2p/${peerIdStr1}`, `/ip6/::1/tcp/9001/p2p/${peerIdStr2}`]; + + const result = parseDirectPeers(multiaddrs, logger); + + expect(result).toHaveLength(2); + expect(result[0].id.toString()).toBe(peerIdStr1); + expect(result[0].addrs[0].toString()).toBe("/ip4/192.168.1.1/tcp/9000"); + expect(result[1].id.toString()).toBe(peerIdStr2); + expect(result[1].addrs[0].toString()).toBe("/ip6/::1/tcp/9001"); + }); + + it("should skip multiaddr without peer ID and log warning", () => { + const multiaddrs = ["/ip4/192.168.1.1/tcp/9000"]; + + const result = parseDirectPeers(multiaddrs, logger); + + expect(result).toHaveLength(0); + expect(logger.warn).toHaveBeenCalledWith("Direct peer multiaddr must contain /p2p/ component with peer ID", { + multiaddr: "/ip4/192.168.1.1/tcp/9000", + }); + }); + + it("should skip invalid multiaddr and log warning", () => { + const multiaddrs = ["not-a-valid-multiaddr"]; + + const result = parseDirectPeers(multiaddrs, logger); + + expect(result).toHaveLength(0); + expect(logger.warn).toHaveBeenCalledWith( + "Failed to parse direct peer multiaddr", + {multiaddr: "not-a-valid-multiaddr"}, + expect.any(Error) + ); + }); + + it("should handle empty array", () => { + const result = parseDirectPeers([], logger); + + expect(result).toHaveLength(0); + expect(logger.info).not.toHaveBeenCalled(); + expect(logger.warn).not.toHaveBeenCalled(); + }); + + it("should parse valid peers and skip invalid ones", () => { + const peerIdStr = "16Uiu2HAkuWPWqF4W3aw9oo5Yw79v5muzBaaGTGKMmuqjPfEyfkwu"; + const multiaddrs = [ + `/ip4/192.168.1.1/tcp/9000/p2p/${peerIdStr}`, + "/ip4/192.168.1.2/tcp/9000", // missing peer ID + "invalid", + ]; + + const result = parseDirectPeers(multiaddrs, logger); + + expect(result).toHaveLength(1); + expect(result[0].id.toString()).toBe(peerIdStr); + expect(logger.warn).toHaveBeenCalledTimes(2); + }); + + it("should handle DNS multiaddr with peer ID", () => { + const peerIdStr = "16Uiu2HAkuWPWqF4W3aw9oo5Yw79v5muzBaaGTGKMmuqjPfEyfkwu"; + const multiaddrs = [`/dns4/node.example.com/tcp/9000/p2p/${peerIdStr}`]; + + const result = parseDirectPeers(multiaddrs, logger); + + expect(result).toHaveLength(1); + expect(result[0].id.toString()).toBe(peerIdStr); + expect(result[0].addrs[0].toString()).toBe("/dns4/node.example.com/tcp/9000"); + }); + + it("should parse valid ENR with TCP multiaddr", async () => { + const privateKey = await generateKeyPair("secp256k1"); + const enr = SignableENR.createFromPrivateKey(privateKey); + enr.setLocationMultiaddr(multiaddr("/ip4/192.168.1.1/tcp/9000")); + const enrStr = enr.encodeTxt(); + + const result = parseDirectPeers([enrStr], logger); + + expect(result).toHaveLength(1); + expect(result[0].id.toString()).toBe(enr.peerId.toString()); + expect(result[0].addrs).toHaveLength(1); + expect(result[0].addrs[0].toString()).toBe("/ip4/192.168.1.1/tcp/9000"); + expect(logger.info).toHaveBeenCalledWith("Added direct peer from ENR", { + peerId: enr.peerId.toString(), + addr: "/ip4/192.168.1.1/tcp/9000", + }); + }); + + it("should skip ENR without TCP multiaddr and log warning", async () => { + const privateKey = await generateKeyPair("secp256k1"); + const enr = SignableENR.createFromPrivateKey(privateKey); + // Only set UDP, not TCP + enr.setLocationMultiaddr(multiaddr("/ip4/192.168.1.1/udp/9000")); + const enrStr = enr.encodeTxt(); + + const result = parseDirectPeers([enrStr], logger); + + expect(result).toHaveLength(0); + expect(logger.warn).toHaveBeenCalledWith("ENR does not contain TCP multiaddr", {enr: enrStr}); + }); + + it("should skip invalid ENR and log warning", () => { + const invalidEnr = "enr:-invalid-enr-string"; + + const result = parseDirectPeers([invalidEnr], logger); + + expect(result).toHaveLength(0); + expect(logger.warn).toHaveBeenCalledWith("Failed to parse direct peer ENR", {enr: invalidEnr}, expect.any(Error)); + }); + + it("should parse mixed multiaddrs and ENRs", async () => { + const peerIdStr = "16Uiu2HAkuWPWqF4W3aw9oo5Yw79v5muzBaaGTGKMmuqjPfEyfkwu"; + const privateKey = await generateKeyPair("secp256k1"); + const enr = SignableENR.createFromPrivateKey(privateKey); + enr.setLocationMultiaddr(multiaddr("/ip4/10.0.0.1/tcp/9001")); + const enrStr = enr.encodeTxt(); + + const mixedPeers = [`/ip4/192.168.1.1/tcp/9000/p2p/${peerIdStr}`, enrStr]; + + const result = parseDirectPeers(mixedPeers, logger); + + expect(result).toHaveLength(2); + expect(result[0].id.toString()).toBe(peerIdStr); + expect(result[0].addrs[0].toString()).toBe("/ip4/192.168.1.1/tcp/9000"); + expect(result[1].id.toString()).toBe(enr.peerId.toString()); + expect(result[1].addrs[0].toString()).toBe("/ip4/10.0.0.1/tcp/9001"); + }); + }); +}); diff --git a/packages/beacon-node/test/unit/network/gossip/scoringParameters.test.ts b/packages/beacon-node/test/unit/network/gossip/scoringParameters.test.ts index 61740ce1a9f9..168e4f346d67 100644 --- a/packages/beacon-node/test/unit/network/gossip/scoringParameters.test.ts +++ b/packages/beacon-node/test/unit/network/gossip/scoringParameters.test.ts @@ -80,7 +80,7 @@ describe("computeGossipPeerScoreParams", () => { expect(params.firstMessageDeliveriesWeight).closeTo(1.8407, TOLERANCE); expect(params.firstMessageDeliveriesDecay).closeTo(0.99856, TOLERANCE); expect(params.firstMessageDeliveriesCap).closeTo(21.73035, TOLERANCE); - expect(params.invalidMessageDeliveriesWeight).closeTo(-2200, TOLERANCE); + expect(params.invalidMessageDeliveriesWeight).closeTo(-2800, TOLERANCE); expect(params.invalidMessageDeliveriesDecay).closeTo(0.99713, TOLERANCE); } @@ -106,7 +106,7 @@ describe("computeGossipPeerScoreParams", () => { expect(params.firstMessageDeliveriesWeight).closeTo(36.81486, TOLERANCE); expect(params.firstMessageDeliveriesDecay).closeTo(0.998561, TOLERANCE); expect(params.firstMessageDeliveriesCap).closeTo(1.08652, TOLERANCE); - expect(params.invalidMessageDeliveriesWeight).closeTo(-2200.0, TOLERANCE); + expect(params.invalidMessageDeliveriesWeight).closeTo(-2800.0, TOLERANCE); expect(params.invalidMessageDeliveriesDecay).closeTo(0.99713, TOLERANCE); } @@ -124,7 +124,7 @@ describe("computeGossipPeerScoreParams", () => { expect(params.firstMessageDeliveriesWeight).closeTo(0.33509, TOLERANCE); expect(params.firstMessageDeliveriesDecay).closeTo(0.86596, TOLERANCE); expect(params.firstMessageDeliveriesCap).closeTo(119.3712, TOLERANCE); - expect(params.invalidMessageDeliveriesWeight).closeTo(-220.0, TOLERANCE); + expect(params.invalidMessageDeliveriesWeight).closeTo(-280.0, TOLERANCE); expect(params.invalidMessageDeliveriesDecay).closeTo(0.99713, TOLERANCE); // Check message rate penalty params @@ -132,11 +132,11 @@ describe("computeGossipPeerScoreParams", () => { expect(params.meshMessageDeliveriesCap).closeTo(68.6255, TOLERANCE); expect(params.meshMessageDeliveriesActivation).toEqual(384 * 1000); expect(params.meshMessageDeliveriesWindow).toEqual(12 * 1000); - expect(params.meshFailurePenaltyWeight).closeTo(-0.7474, TOLERANCE); + expect(params.meshFailurePenaltyWeight).closeTo(-0.95127, TOLERANCE); expect(params.meshFailurePenaltyDecay).closeTo(0.93057, TOLERANCE); if (penaltiesActive) { - expect(params.meshMessageDeliveriesWeight).closeTo(-0.7474, TOLERANCE); + expect(params.meshMessageDeliveriesWeight).closeTo(-0.95127, TOLERANCE); expect(params.meshMessageDeliveriesThreshold).closeTo(17.15638, TOLERANCE); } else { expect(params.meshMessageDeliveriesWeight).toEqual(0.0); @@ -158,7 +158,7 @@ describe("computeGossipPeerScoreParams", () => { expect(params.firstMessageDeliveriesWeight).closeTo(1.14716, TOLERANCE); expect(params.firstMessageDeliveriesDecay).closeTo(0.99283, TOLERANCE); expect(params.firstMessageDeliveriesCap).closeTo(34.8687, TOLERANCE); - expect(params.invalidMessageDeliveriesWeight).closeTo(-220.0, TOLERANCE); + expect(params.invalidMessageDeliveriesWeight).closeTo(-280.0, TOLERANCE); expect(params.invalidMessageDeliveriesDecay).closeTo(0.99713, TOLERANCE); // Check message rate penalty params @@ -166,11 +166,11 @@ describe("computeGossipPeerScoreParams", () => { expect(params.meshMessageDeliveriesCap).closeTo(2.0547574, TOLERANCE); expect(params.meshMessageDeliveriesActivation).toEqual(384 * 1000); expect(params.meshMessageDeliveriesWindow).toEqual(12 * 1000); - expect(params.meshFailurePenaltyWeight).closeTo(-468.9689, TOLERANCE); + expect(params.meshFailurePenaltyWeight).closeTo(-596.8696, TOLERANCE); expect(params.meshFailurePenaltyDecay).closeTo(0.97163, TOLERANCE); if (penaltiesActive) { - expect(params.meshMessageDeliveriesWeight).closeTo(-468.9689, TOLERANCE); + expect(params.meshMessageDeliveriesWeight).closeTo(-596.8696, TOLERANCE); expect(params.meshMessageDeliveriesThreshold).closeTo(0.68491, TOLERANCE); } else { expect(params.meshMessageDeliveriesWeight).toEqual(0.0); @@ -200,7 +200,7 @@ describe("computeGossipPeerScoreParams", () => { expect(params.firstMessageDeliveriesWeight).closeTo(2.6807, TOLERANCE); expect(params.firstMessageDeliveriesDecay).closeTo(0.86596, TOLERANCE); expect(params.firstMessageDeliveriesCap).closeTo(14.9214, TOLERANCE); - expect(params.invalidMessageDeliveriesWeight).closeTo(-7040.0, TOLERANCE); + expect(params.invalidMessageDeliveriesWeight).closeTo(-8960.0, TOLERANCE); expect(params.invalidMessageDeliveriesDecay).closeTo(0.99713, TOLERANCE); // Check message rate penalty params @@ -208,11 +208,11 @@ describe("computeGossipPeerScoreParams", () => { expect(params.meshMessageDeliveriesCap).closeTo(69.88248, TOLERANCE); expect(params.meshMessageDeliveriesActivation).toEqual(204 * 1000); expect(params.meshMessageDeliveriesWindow).toEqual(12 * 1000); - expect(params.meshFailurePenaltyWeight).closeTo(-369.0421, TOLERANCE); + expect(params.meshFailurePenaltyWeight).closeTo(-469.69001, TOLERANCE); expect(params.meshFailurePenaltyDecay).closeTo(0.96466, TOLERANCE); if (penaltiesActive) { - expect(params.meshMessageDeliveriesWeight).closeTo(-369.0421, TOLERANCE); + expect(params.meshMessageDeliveriesWeight).closeTo(-469.69001, TOLERANCE); expect(params.meshMessageDeliveriesThreshold).closeTo(4.367655, TOLERANCE); } else { expect(params.meshMessageDeliveriesWeight).toEqual(0.0); diff --git a/packages/beacon-node/test/unit/network/gossip/topic.test.ts b/packages/beacon-node/test/unit/network/gossip/topic.test.ts index a74994e3b1b4..7e9b3f6ddd46 100644 --- a/packages/beacon-node/test/unit/network/gossip/topic.test.ts +++ b/packages/beacon-node/test/unit/network/gossip/topic.test.ts @@ -6,7 +6,7 @@ import {GossipEncoding, GossipTopicMap, GossipType} from "../../../../src/networ import {parseGossipTopic, stringifyGossipTopic} from "../../../../src/network/gossip/topic.js"; describe("network / gossip / topic", () => { - const config = createBeaconConfig(chainConfig, ZERO_HASH); + const config = createBeaconConfig({...chainConfig, GLOAS_FORK_EPOCH: 700000}, ZERO_HASH); const encoding = GossipEncoding.ssz_snappy; // Enforce with Typescript that we test all GossipType @@ -137,6 +137,36 @@ describe("network / gossip / topic", () => { topicStr: "/eth2/16abab34/light_client_optimistic_update/ssz_snappy", }, ], + [GossipType.execution_payload]: [ + { + topic: { + type: GossipType.execution_payload, + boundary: {fork: ForkName.gloas, epoch: config.GLOAS_FORK_EPOCH}, + encoding, + }, + topicStr: "/eth2/a41d57bd/execution_payload/ssz_snappy", + }, + ], + [GossipType.payload_attestation_message]: [ + { + topic: { + type: GossipType.payload_attestation_message, + boundary: {fork: ForkName.gloas, epoch: config.GLOAS_FORK_EPOCH}, + encoding, + }, + topicStr: "/eth2/a41d57bd/payload_attestation_message/ssz_snappy", + }, + ], + [GossipType.execution_payload_bid]: [ + { + topic: { + type: GossipType.execution_payload_bid, + boundary: {fork: ForkName.gloas, epoch: config.GLOAS_FORK_EPOCH}, + encoding, + }, + topicStr: "/eth2/a41d57bd/execution_payload_bid/ssz_snappy", + }, + ], }; for (const topics of Object.values(testCases)) { diff --git a/packages/beacon-node/test/unit/util/bytes.test.ts b/packages/beacon-node/test/unit/util/bytes.test.ts index 9f99c508ea89..2d084a177fa0 100644 --- a/packages/beacon-node/test/unit/util/bytes.test.ts +++ b/packages/beacon-node/test/unit/util/bytes.test.ts @@ -1,6 +1,6 @@ import {describe, expect, it} from "vitest"; import {fromHexString, toHexString} from "@chainsafe/ssz"; -import {byteArrayEquals} from "../../../src/util/bytes.js"; +import {byteArrayEquals} from "@lodestar/utils"; /** Reference implementation of byteArrayConcat */ function byteArrayConcat(bytesArr: Uint8Array[]): Uint8Array { diff --git a/packages/beacon-node/test/utils/node/beacon.ts b/packages/beacon-node/test/utils/node/beacon.ts index 1629d35045b3..e97341004b43 100644 --- a/packages/beacon-node/test/utils/node/beacon.ts +++ b/packages/beacon-node/test/utils/node/beacon.ts @@ -9,16 +9,18 @@ import {ChainConfig, createBeaconConfig, createChainForkConfig} from "@lodestar/ import {config as minimalConfig} from "@lodestar/config/default"; import {LevelDbController} from "@lodestar/db/controller/level"; import {LoggerNode} from "@lodestar/logger/node"; -import {ForkSeq, GENESIS_SLOT, SLOTS_PER_EPOCH, ZERO_HASH_HEX} from "@lodestar/params"; +import {ForkSeq, GENESIS_SLOT} from "@lodestar/params"; import { BeaconStateAllForks, Index2PubkeyCache, - computeTimeAtSlot, + computeAnchorCheckpoint, + computeEpochAtSlot, createCachedBeaconState, syncPubkeys, } from "@lodestar/state-transition"; import {phase0, ssz} from "@lodestar/types"; -import {RecursivePartial, isPlainObject, toRootHex} from "@lodestar/utils"; +import {RecursivePartial, isPlainObject} from "@lodestar/utils"; +import {initStateFromDb} from "../../../src/chain/initState.js"; import {BeaconDb} from "../../../src/db/index.js"; import {BeaconNode} from "../../../src/index.js"; import {defaultNetworkOptions} from "../../../src/network/options.js"; @@ -37,6 +39,11 @@ export async function getDevBeaconNode( peerStoreDir?: string; anchorState?: BeaconStateAllForks; wsCheckpoint?: phase0.Checkpoint; + /** + * When true, load anchor state from existing DB instead of creating fresh genesis. + * Requires `options.db.name` to be set explicitly. + */ + resumeFromDb?: boolean; } & InteropStateOpts ): Promise { setHasher(hasher); @@ -48,20 +55,44 @@ export async function getDevBeaconNode( const config = createChainForkConfig({...minimalConfig, ...params}); logger = logger ?? testLogger(); - const db = new BeaconDb(config, await LevelDbController.create({name: tmpDir.name}, {logger})); + const db = new BeaconDb(config, await LevelDbController.create({name: options.db?.name ?? tmpDir.name}, {logger})); let anchorState = opts.anchorState; + let wsCheckpoint = opts.wsCheckpoint; + if (!anchorState) { - anchorState = initDevState(config, validatorCount, opts); + if (opts.resumeFromDb) { + if (!options.db?.name) { + throw new Error("resumeFromDb requires explicit options.db.name to be set"); + } + + // reuse production code for state loading from DB + anchorState = await initStateFromDb(config, db, logger); + const resumedEpoch = computeEpochAtSlot(anchorState.slot); + + // resuming from epoch 0 defeats the purpose of resuming + if (resumedEpoch === 0) { + logger.warn("Resumed state from epoch 0. Range Sync may trigger from genesis"); + } + + // derive wsCheckpoint if not provided + if (!wsCheckpoint) { + const {checkpoint} = computeAnchorCheckpoint(config, anchorState); + wsCheckpoint = {root: checkpoint.root, epoch: checkpoint.epoch}; + logger.debug("Derived wsCheckpoint", {epoch: checkpoint.epoch}); + } + } else { + anchorState = initDevState(config, validatorCount, opts); - const block = config.getForkTypes(GENESIS_SLOT).SignedBeaconBlock.defaultValue(); - block.message.stateRoot = anchorState.hashTreeRoot(); - await db.blockArchive.add(block); + const block = config.getForkTypes(GENESIS_SLOT).SignedBeaconBlock.defaultValue(); + block.message.stateRoot = anchorState.hashTreeRoot(); + await db.blockArchive.add(block); - if (config.getForkSeq(GENESIS_SLOT) >= ForkSeq.deneb) { - const blobSidecars = ssz.deneb.BlobSidecars.defaultValue(); - const blockRoot = config.getForkTypes(GENESIS_SLOT).BeaconBlock.hashTreeRoot(block.message); - await db.blobSidecars.add({blobSidecars, slot: GENESIS_SLOT, blockRoot}); + if (config.getForkSeq(GENESIS_SLOT) >= ForkSeq.deneb) { + const blobSidecars = ssz.deneb.BlobSidecars.defaultValue(); + const blockRoot = config.getForkTypes(GENESIS_SLOT).BeaconBlock.hashTreeRoot(block.message); + await db.blobSidecars.add({blobSidecars, slot: GENESIS_SLOT, blockRoot}); + } } } @@ -89,29 +120,8 @@ export async function getDevBeaconNode( initialCustodyGroupCount: config.NUMBER_OF_CUSTODY_GROUPS, }, executionEngine: { + // options for mock EL will be provided in Beacon.init() entry point mode: "mock", - genesisBlockHash: ZERO_HASH_HEX, - eth1BlockHash: opts.eth1BlockHash ? toRootHex(opts.eth1BlockHash) : undefined, - fuluForkTimestamp: computeTimeAtSlot( - config, - config.FULU_FORK_EPOCH * SLOTS_PER_EPOCH, - anchorState.genesisTime - ), - electraForkTimestamp: computeTimeAtSlot( - config, - config.ELECTRA_FORK_EPOCH * SLOTS_PER_EPOCH, - anchorState.genesisTime - ), - denebForkTimestamp: computeTimeAtSlot( - config, - config.DENEB_FORK_EPOCH * SLOTS_PER_EPOCH, - anchorState.genesisTime - ), - capellaForkTimestamp: computeTimeAtSlot( - config, - config.CAPELLA_FORK_EPOCH * SLOTS_PER_EPOCH, - anchorState.genesisTime - ), }, } as Partial, options @@ -148,7 +158,7 @@ export async function getDevBeaconNode( dataDir: ".", peerStoreDir, anchorState: cachedState, - wsCheckpoint: opts.wsCheckpoint, + wsCheckpoint, isAnchorStateFinalized: true, }); } diff --git a/packages/beacon-node/test/utils/node/simTest.ts b/packages/beacon-node/test/utils/node/simTest.ts index 87c2cc2a9c3e..e8a862bc3da6 100644 --- a/packages/beacon-node/test/utils/node/simTest.ts +++ b/packages/beacon-node/test/utils/node/simTest.ts @@ -1,4 +1,3 @@ -import {toHexString} from "@chainsafe/ssz"; import {routes} from "@lodestar/api"; import {BeaconConfig} from "@lodestar/config"; import {SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; @@ -10,7 +9,7 @@ import { } from "@lodestar/state-transition"; import {BeaconBlock, Epoch, Slot} from "@lodestar/types"; import {Checkpoint} from "@lodestar/types/phase0"; -import {Logger, mapValues} from "@lodestar/utils"; +import {Logger, mapValues, toRootHex} from "@lodestar/utils"; import {ChainEvent, HeadEventData} from "../../../src/chain/index.js"; import {RegenCaller} from "../../../src/chain/regen/index.js"; import {BeaconNode} from "../../../src/index.js"; @@ -64,15 +63,16 @@ export function simTestInfoTracker(bn: BeaconNode, logger: Logger): () => void { if (checkpoint.epoch <= lastSeenEpoch) return; lastSeenEpoch = checkpoint.epoch; - // Recover the pre-epoch transition state, use any random caller for regen - const checkpointState = await bn.chain.regen.getCheckpointState( - checkpoint, - {dontTransferCache: true}, - RegenCaller.onForkChoiceFinalized - ); + const checkpointState = bn.chain.regen.getCheckpointStateSync({ + ...checkpoint, + rootHex: toRootHex(checkpoint.root), + }); + if (checkpointState == null) { + throw Error(`Checkpoint state not found for epoch ${checkpoint.epoch} root ${toRootHex(checkpoint.root)}`); + } const lastSlot = computeStartSlotAtEpoch(checkpoint.epoch) - 1; const lastStateRoot = checkpointState.stateRoots.get(lastSlot % SLOTS_PER_HISTORICAL_ROOT); - const lastState = await bn.chain.regen.getState(toHexString(lastStateRoot), RegenCaller.onForkChoiceFinalized); + const lastState = await bn.chain.regen.getState(toRootHex(lastStateRoot), RegenCaller.onForkChoiceFinalized); logParticipation(lastState); } diff --git a/packages/beacon-node/test/utils/validationData/attestation.ts b/packages/beacon-node/test/utils/validationData/attestation.ts index 798303e62887..3932d38a38cb 100644 --- a/packages/beacon-node/test/utils/validationData/attestation.ts +++ b/packages/beacon-node/test/utils/validationData/attestation.ts @@ -153,8 +153,11 @@ export function getAttestationValidData(opts: AttestationValidDataOpts): { seenAggregatedAttestations: new SeenAggregatedAttestations(null), seenAttestationDatas: new SeenAttestationDatas(null, 0, 0), bls: blsVerifyAllMainThread - ? new BlsSingleThreadVerifier({metrics: null}) - : new BlsMultiThreadWorkerPool({}, {logger: testLogger(), metrics: null}), + ? new BlsSingleThreadVerifier({metrics: null, index2pubkey: state.epochCtx.index2pubkey}) + : new BlsMultiThreadWorkerPool( + {}, + {logger: testLogger(), metrics: null, index2pubkey: state.epochCtx.index2pubkey} + ), waitForBlock: () => Promise.resolve(false), index2pubkey: state.epochCtx.index2pubkey, shufflingCache, diff --git a/packages/cli/package.json b/packages/cli/package.json index 154db82c3478..de708f1b7f97 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@chainsafe/lodestar", - "version": "1.39.1", + "version": "1.40.0", "description": "Command line interface for lodestar", "author": "ChainSafe Systems", "license": "Apache-2.0", @@ -42,7 +42,6 @@ "test:sim:multifork": "LODESTAR_PRESET=minimal DOTENV_CONFIG_PATH=../../.env.test node -r dotenv/config --loader ts-node/esm test/sim/multiFork.test.ts", "test:sim:mixedclient": "LODESTAR_PRESET=minimal DOTENV_CONFIG_PATH=../../.env.test node -r dotenv/config --loader ts-node/esm test/sim/mixedClient.test.ts", "test:sim:endpoints": "LODESTAR_PRESET=minimal DOTENV_CONFIG_PATH=../../.env.test node -r dotenv/config --loader ts-node/esm test/sim/endpoints.test.ts", - "test:sim:deneb": "LODESTAR_PRESET=minimal DOTENV_CONFIG_PATH=../../.env.test node -r dotenv/config --loader ts-node/esm test/sim/deneb.test.ts", "test:sim:backup_eth_provider": "LODESTAR_PRESET=minimal DOTENV_CONFIG_PATH=../../.env.test node -r dotenv/config --loader ts-node/esm test/sim/backupEthProvider.test.ts", "test": "pnpm test:unit && pnpm test:e2e", "check-readme": "pnpm exec ts-node ../../scripts/check_readme.ts" @@ -105,7 +104,7 @@ "@types/tmp": "^0.2.3", "@types/yargs": "^17.0.24", "ethereum-cryptography": "^2.2.1", - "fastify": "^5.2.1", + "fastify": "^5.7.4", "tmp": "^0.2.4", "web3": "^4.0.3", "web3-eth-accounts": "^4.0.3" diff --git a/packages/cli/src/cmds/dev/options.ts b/packages/cli/src/cmds/dev/options.ts index 5c23a1e473c1..a79aff6e6707 100644 --- a/packages/cli/src/cmds/dev/options.ts +++ b/packages/cli/src/cmds/dev/options.ts @@ -99,6 +99,10 @@ const externalOptionsOverrides: Partial = { + ALTAIR_FORK_EPOCH: 0, + BELLATRIX_FORK_EPOCH: 0, + CAPELLA_FORK_EPOCH: 0, + DENEB_FORK_EPOCH: 0, + ELECTRA_FORK_EPOCH: 0, + FULU_FORK_EPOCH: 1, +}; + let chainConfig: ChainConfig; switch (ACTIVE_PRESET) { case PresetName.mainnet: - chainConfig = mainnetChainConfig; + chainConfig = {...mainnetChainConfig, ...devConfig}; break; case PresetName.minimal: - chainConfig = minimalChainConfig; + chainConfig = {...minimalChainConfig, ...devConfig}; break; case PresetName.gnosis: - chainConfig = gnosisChainConfig; + chainConfig = {...gnosisChainConfig, ...devConfig}; break; default: throw Error(`Preset ${ACTIVE_PRESET} not supported with dev command`); diff --git a/packages/cli/src/options/beaconNodeOptions/chain.ts b/packages/cli/src/options/beaconNodeOptions/chain.ts index 04af60b9086b..1ff8c69feeb1 100644 --- a/packages/cli/src/options/beaconNodeOptions/chain.ts +++ b/packages/cli/src/options/beaconNodeOptions/chain.ts @@ -30,7 +30,6 @@ export type ChainArgs = { "chain.archiveStateEpochFrequency": number; "chain.archiveDataEpochs"?: number; "chain.archiveMode": ArchiveMode; - "chain.nHistoricalStates"?: boolean; "chain.nHistoricalStatesFileDataStore"?: boolean; "chain.maxBlockStates"?: number; "chain.maxCPStateEpochsInMemory"?: number; @@ -70,7 +69,6 @@ export function parseArgs(args: ChainArgs): IBeaconNodeOptions["chain"] { archiveStateEpochFrequency: args["chain.archiveStateEpochFrequency"], archiveDataEpochs: args["chain.archiveDataEpochs"], archiveMode: args["chain.archiveMode"] ?? defaultOptions.chain.archiveMode, - nHistoricalStates: args["chain.nHistoricalStates"] ?? defaultOptions.chain.nHistoricalStates, nHistoricalStatesFileDataStore: args["chain.nHistoricalStatesFileDataStore"] ?? defaultOptions.chain.nHistoricalStatesFileDataStore, maxBlockStates: args["chain.maxBlockStates"] ?? defaultOptions.chain.maxBlockStates, @@ -274,15 +272,6 @@ Will double processing times. Use only for debugging purposes.", group: "chain", }, - "chain.nHistoricalStates": { - hidden: true, - description: - "Use the new FIFOBlockStateCache and PersistentCheckpointStateCache or not which make lodestar heap size bounded instead of unbounded as before", - type: "boolean", - default: defaultOptions.chain.nHistoricalStates, - group: "chain", - }, - "chain.nHistoricalStatesFileDataStore": { hidden: true, description: "Use fs to store checkpoint state for PersistentCheckpointStateCache or not", diff --git a/packages/cli/src/options/beaconNodeOptions/execution.ts b/packages/cli/src/options/beaconNodeOptions/execution.ts index 79ea4ade98de..4f2317383c52 100644 --- a/packages/cli/src/options/beaconNodeOptions/execution.ts +++ b/packages/cli/src/options/beaconNodeOptions/execution.ts @@ -15,9 +15,9 @@ export type ExecutionEngineArgs = { export function parseArgs(args: ExecutionEngineArgs): IBeaconNodeOptions["executionEngine"] { if (args["execution.engineMock"]) { + // mock EL options will be provided later by Beacon.init() entry point return { mode: "mock", - genesisBlockHash: "", }; } diff --git a/packages/cli/src/options/beaconNodeOptions/network.ts b/packages/cli/src/options/beaconNodeOptions/network.ts index 9770658213af..301a0a0e3bbc 100644 --- a/packages/cli/src/options/beaconNodeOptions/network.ts +++ b/packages/cli/src/options/beaconNodeOptions/network.ts @@ -22,6 +22,7 @@ export type NetworkArgs = { slotsToSubscribeBeforeAggregatorDuty?: number; disablePeerScoring?: boolean; mdns?: boolean; + directPeers?: string[]; "network.maxPeers"?: number; "network.connectToDiscv5Bootnodes"?: boolean; "network.discv5FirstQueryDelayMs"?: number; @@ -156,6 +157,7 @@ export function parseArgs(args: NetworkArgs): IBeaconNodeOptions["network"] { useWorker: args["network.useWorker"], maxYoungGenerationSizeMb: args["network.maxYoungGenerationSizeMb"], targetGroupPeers: args["network.targetGroupPeers"] ?? defaultOptions.network.targetGroupPeers, + directPeers: args.directPeers, }; } @@ -259,6 +261,22 @@ export const options: CliCommandOptions = { group: "network", }, + directPeers: { + type: "array", + description: + "Direct peers for GossipSub mesh. These peers maintain permanent connections without GRAFT/PRUNE. " + + "Supports multiaddr with peer ID (e.g., `/ip4/192.168.1.1/tcp/9000/p2p/16Uiu2HAmKLhW7...`) " + + "or ENR (e.g., `enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOo...`). " + + "Both peers must configure each other as direct peers for the feature to work properly.", + defaultDescription: "[]", + group: "network", + coerce: (args: string[] | undefined) => + (args ?? []) + .flatMap((item) => item.split(",")) + .map((s) => s.trim()) + .filter(Boolean), + }, + "network.maxPeers": { hidden: true, type: "number", diff --git a/packages/cli/test/sim/deneb.test.ts b/packages/cli/test/sim/deneb.test.ts deleted file mode 100644 index 5a1b2012a6af..000000000000 --- a/packages/cli/test/sim/deneb.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import path from "node:path"; -import {createBlobsAssertion} from "../utils/crucible/assertions/blobsAssertion.js"; -import {BeaconClient, ExecutionClient, ValidatorClient} from "../utils/crucible/interfaces.js"; -import {Simulation} from "../utils/crucible/simulation.js"; -import {defineSimTestConfig, logFilesDir} from "../utils/crucible/utils/index.js"; -import {connectAllNodes, waitForSlot} from "../utils/crucible/utils/network.js"; -import {assertCheckpointSync, assertRangeSync} from "../utils/crucible/utils/syncing.js"; - -const altairForkEpoch = 0; -const bellatrixForkEpoch = 0; -const capellaForkEpoch = 0; -const denebForkEpoch = 0; -const runTillEpoch = 2; -const syncWaitEpoch = 4; - -const {estimatedTimeoutMs, forkConfig} = defineSimTestConfig({ - ALTAIR_FORK_EPOCH: altairForkEpoch, - BELLATRIX_FORK_EPOCH: bellatrixForkEpoch, - CAPELLA_FORK_EPOCH: capellaForkEpoch, - DENEB_FORK_EPOCH: denebForkEpoch, - runTillEpoch: runTillEpoch + syncWaitEpoch, - initialNodes: 2, - additionalSlotsForTTD: 0, -}); - -const env = await Simulation.initWithDefaults( - { - id: "deneb", - logsDir: path.join(logFilesDir, "deneb"), - forkConfig, - }, - [ - { - id: "node-1", - beacon: BeaconClient.Lodestar, - validator: { - type: ValidatorClient.Lodestar, - options: {}, - }, - execution: ExecutionClient.Geth, - keysCount: 32, - mining: true, - }, - { - id: "node-2", - beacon: BeaconClient.Lodestar, - validator: { - type: ValidatorClient.Lodestar, - options: {}, - }, - execution: ExecutionClient.Geth, - keysCount: 32, - remote: true, - }, - ] -); - -await env.start({runTimeoutMs: estimatedTimeoutMs}); -await connectAllNodes(env.nodes); - -env.tracker.register( - createBlobsAssertion(env.nodes, { - sendBlobsAtSlot: 2, - validateBlobsAt: env.clock.getLastSlotOfEpoch(2), - }) -); - -await waitForSlot("Waiting for the 2nd epoch to pass", { - slot: env.clock.getLastSlotOfEpoch(2), - env, -}); - -await assertRangeSync(env); -await assertCheckpointSync(env); - -await env.stop(); diff --git a/packages/cli/test/sim/endpoints.test.ts b/packages/cli/test/sim/endpoints.test.ts index aa72ac76836d..1d1b4b2fcdbf 100644 --- a/packages/cli/test/sim/endpoints.test.ts +++ b/packages/cli/test/sim/endpoints.test.ts @@ -9,17 +9,17 @@ import {Simulation} from "../utils/crucible/simulation.js"; import {defineSimTestConfig, logFilesDir} from "../utils/crucible/utils/index.js"; import {waitForSlot} from "../utils/crucible/utils/network.js"; -const altairForkEpoch = 0; -const bellatrixForkEpoch = 0; -const capellaForkEpoch = 0; -const denebForkEpoch = 0; +const ELECTRA_FORK_EPOCH = 0; +const FULU_FORK_EPOCH = 1; const validatorCount = 2; const {estimatedTimeoutMs, forkConfig} = defineSimTestConfig({ - ALTAIR_FORK_EPOCH: altairForkEpoch, - BELLATRIX_FORK_EPOCH: bellatrixForkEpoch, - CAPELLA_FORK_EPOCH: capellaForkEpoch, - DENEB_FORK_EPOCH: denebForkEpoch, + ALTAIR_FORK_EPOCH: ELECTRA_FORK_EPOCH, + BELLATRIX_FORK_EPOCH: ELECTRA_FORK_EPOCH, + CAPELLA_FORK_EPOCH: ELECTRA_FORK_EPOCH, + DENEB_FORK_EPOCH: ELECTRA_FORK_EPOCH, + ELECTRA_FORK_EPOCH, + FULU_FORK_EPOCH, runTillEpoch: 2, initialNodes: 1, }); @@ -120,7 +120,7 @@ await env.tracker.assert("should return HTTP error responses in a spec compliant assert.deepStrictEqual(JSON.parse(await res2.errorBody()), {code: 400, message: "slot must be integer"}); // Error processing multiple items - const signedAttestations = Array.from({length: 3}, () => ssz.phase0.Attestation.defaultValue()); + const signedAttestations = Array.from({length: 3}, () => ssz.electra.SingleAttestation.defaultValue()); const res3 = await node.api.beacon.submitPoolAttestationsV2({signedAttestations}); const errBody = JSON.parse(await res3.errorBody()) as {code: number; message: string; failures: unknown[]}; assert.equal(errBody.code, 400); @@ -128,7 +128,7 @@ await env.tracker.assert("should return HTTP error responses in a spec compliant assert.equal(errBody.failures.length, signedAttestations.length); assert.deepStrictEqual(errBody.failures[0], { index: 0, - message: "ATTESTATION_ERROR_NOT_EXACTLY_ONE_AGGREGATION_BIT_SET", + message: "ATTESTATION_ERROR_UNKNOWN_OR_PREFINALIZED_BEACON_BLOCK_ROOT", }); // Route does not exist diff --git a/packages/cli/test/unit/options/beaconNodeOptions.test.ts b/packages/cli/test/unit/options/beaconNodeOptions.test.ts index 9843bffda907..cef866b30624 100644 --- a/packages/cli/test/unit/options/beaconNodeOptions.test.ts +++ b/packages/cli/test/unit/options/beaconNodeOptions.test.ts @@ -35,7 +35,6 @@ describe("options / beaconNodeOptions", () => { "chain.minSameMessageSignatureSetsToBatch": 32, "chain.maxShufflingCacheEpochs": 100, "chain.archiveDataEpochs": 10000, - "chain.nHistoricalStates": true, "chain.nHistoricalStatesFileDataStore": true, "chain.maxBlockStates": 100, "chain.maxCPStateEpochsInMemory": 100, @@ -94,6 +93,7 @@ describe("options / beaconNodeOptions", () => { "network.useWorker": true, "network.maxYoungGenerationSizeMb": 152, "network.targetGroupPeers": 12, + directPeers: ["/ip4/192.168.1.1/tcp/9000/p2p/16Uiu2HAkuWPWqF4W3aw9oo5Yw79v5muzBaaGTGKMmuqjPfEyfkwu"], "sync.isSingleNode": true, "sync.disableProcessAsChainSegment": true, @@ -136,7 +136,6 @@ describe("options / beaconNodeOptions", () => { maxShufflingCacheEpochs: 100, archiveDataEpochs: 10000, archiveMode: ArchiveMode.Frequency, - nHistoricalStates: true, nHistoricalStatesFileDataStore: true, maxBlockStates: 100, maxCPStateEpochsInMemory: 100, @@ -197,6 +196,7 @@ describe("options / beaconNodeOptions", () => { useWorker: true, maxYoungGenerationSizeMb: 152, targetGroupPeers: 12, + directPeers: ["/ip4/192.168.1.1/tcp/9000/p2p/16Uiu2HAkuWPWqF4W3aw9oo5Yw79v5muzBaaGTGKMmuqjPfEyfkwu"], }, sync: { isSingleNode: true, diff --git a/packages/cli/test/utils/crucible/assertions/defaults/headAssertion.ts b/packages/cli/test/utils/crucible/assertions/defaults/headAssertion.ts index 3a3f910a4489..8d028304afb7 100644 --- a/packages/cli/test/utils/crucible/assertions/defaults/headAssertion.ts +++ b/packages/cli/test/utils/crucible/assertions/defaults/headAssertion.ts @@ -46,7 +46,7 @@ export const headAssertion: Assertion<"head", HeadSummary> = { */ const result = [`Slot,${nodes.map((n) => n.beacon.id).join(", ")}`]; for (let s = 1; s <= slot; s++) { - result.push(`${s}, ${nodes.map((n) => store[n.beacon.id][s].blockRoot ?? "-").join(",")}`); + result.push(`${s}, ${nodes.map((n) => store[n.beacon.id][s]?.blockRoot ?? "-").join(",")}`); } return {"headAssertion.csv": result.join("\n")}; }, diff --git a/packages/cli/test/utils/crucible/runner/childProcessRunner.ts b/packages/cli/test/utils/crucible/runner/childProcessRunner.ts index 404e12206191..f6bea15d0b89 100644 --- a/packages/cli/test/utils/crucible/runner/childProcessRunner.ts +++ b/packages/cli/test/utils/crucible/runner/childProcessRunner.ts @@ -23,7 +23,7 @@ export class ChildProcessRunner implements RunnerEnv { const health = jobOption.health; if (health) { - spawnOpts.healthTimeoutMs = 30000; + spawnOpts.healthTimeoutMs = 60000; spawnOpts.health = health; } else { spawnOpts.resolveOn = ChildProcessResolve.Completion; diff --git a/packages/cli/test/utils/crucible/runner/dockerRunner.ts b/packages/cli/test/utils/crucible/runner/dockerRunner.ts index bb4e370ffb2a..2fc6be49aa7b 100644 --- a/packages/cli/test/utils/crucible/runner/dockerRunner.ts +++ b/packages/cli/test/utils/crucible/runner/dockerRunner.ts @@ -89,7 +89,7 @@ export class DockerRunner implements RunnerEnv { const health = jobOption.health; if (health) { - spawnOpts.healthTimeoutMs = 30000; + spawnOpts.healthTimeoutMs = 60000; spawnOpts.health = health; } else { spawnOpts.resolveOn = ChildProcessResolve.Completion; diff --git a/packages/config/package.json b/packages/config/package.json index 854d4fe43fb3..2c296fef32be 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,6 +1,6 @@ { "name": "@lodestar/config", - "version": "1.39.1", + "version": "1.40.0", "description": "Chain configuration required for lodestar", "author": "ChainSafe Systems", "license": "Apache-2.0", diff --git a/packages/config/src/chainConfig/configs/mainnet.ts b/packages/config/src/chainConfig/configs/mainnet.ts index a366633858cb..38139c0053a0 100644 --- a/packages/config/src/chainConfig/configs/mainnet.ts +++ b/packages/config/src/chainConfig/configs/mainnet.ts @@ -70,6 +70,8 @@ export const chainConfig: ChainConfig = { SECONDS_PER_ETH1_BLOCK: 14, // 2**8 (= 256) epochs ~27 hours MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256, + // 2**12 (= 4,096) epochs ~18 days + MIN_BUILDER_WITHDRAWABILITY_DELAY: 4096, // 2**8 (= 256) epochs ~27 hours SHARD_COMMITTEE_PERIOD: 256, // 2**11 (= 2,048) Eth1 blocks ~8 hours diff --git a/packages/config/src/chainConfig/configs/minimal.ts b/packages/config/src/chainConfig/configs/minimal.ts index bbc7cd832076..a4371418e326 100644 --- a/packages/config/src/chainConfig/configs/minimal.ts +++ b/packages/config/src/chainConfig/configs/minimal.ts @@ -64,6 +64,8 @@ export const chainConfig: ChainConfig = { SECONDS_PER_ETH1_BLOCK: 14, // 2**8 (= 256) epochs MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256, + // [customized] 2**3 (= 8) epochs + MIN_BUILDER_WITHDRAWABILITY_DELAY: 8, // [customized] higher frequency of committee turnover and faster time to acceptable voluntary exit SHARD_COMMITTEE_PERIOD: 64, // [customized] process deposits more quickly, but insecure diff --git a/packages/config/src/chainConfig/types.ts b/packages/config/src/chainConfig/types.ts index 41e33cf84a7b..f75b30536286 100644 --- a/packages/config/src/chainConfig/types.ts +++ b/packages/config/src/chainConfig/types.ts @@ -57,6 +57,7 @@ export type ChainConfig = { SLOT_DURATION_MS: number; SECONDS_PER_ETH1_BLOCK: number; MIN_VALIDATOR_WITHDRAWABILITY_DELAY: number; + MIN_BUILDER_WITHDRAWABILITY_DELAY: number; SHARD_COMMITTEE_PERIOD: number; ETH1_FOLLOW_DISTANCE: number; PROPOSER_REORG_CUTOFF_BPS: number; @@ -172,6 +173,7 @@ export const chainConfigTypes: SpecTypes = { SLOT_DURATION_MS: "number", SECONDS_PER_ETH1_BLOCK: "number", MIN_VALIDATOR_WITHDRAWABILITY_DELAY: "number", + MIN_BUILDER_WITHDRAWABILITY_DELAY: "number", SHARD_COMMITTEE_PERIOD: "number", ETH1_FOLLOW_DISTANCE: "number", PROPOSER_REORG_CUTOFF_BPS: "number", diff --git a/packages/config/test/e2e/ensure-config-is-synced.test.ts b/packages/config/test/e2e/ensure-config-is-synced.test.ts new file mode 100644 index 000000000000..96afac82d784 --- /dev/null +++ b/packages/config/test/e2e/ensure-config-is-synced.test.ts @@ -0,0 +1,172 @@ +import {describe, expect, it, vi} from "vitest"; +import {fetch, fromHex} from "@lodestar/utils"; +import {ethereumConsensusSpecsTests} from "../../../beacon-node/test/spec/specTestVersioning.js"; +import {chainConfig as mainnetChainConfig} from "../../src/chainConfig/configs/mainnet.js"; +import {chainConfig as minimalChainConfig} from "../../src/chainConfig/configs/minimal.js"; +import {ChainConfig} from "../../src/chainConfig/types.js"; + +// Not e2e, but slow. Run with e2e tests + +/** + * Fields that we filter from remote config when doing comparison. + * These are network-specific values that differ from the spec defaults, + * have special formats that require custom handling, or are not yet implemented. + */ +const ignoredRemoteConfigFields: (keyof ChainConfig)[] = [ + // BLOB_SCHEDULE is an array/JSON format that requires special parsing + "BLOB_SCHEDULE" as keyof ChainConfig, + // EIP-7805 (Inclusion Lists) - not yet implemented in Lodestar + "VIEW_FREEZE_CUTOFF_BPS" as keyof ChainConfig, + "INCLUSION_LIST_SUBMISSION_DUE_BPS" as keyof ChainConfig, + "PROPOSER_INCLUSION_LIST_CUTOFF_BPS" as keyof ChainConfig, + "MAX_REQUEST_INCLUSION_LIST" as keyof ChainConfig, + "MAX_BYTES_PER_INCLUSION_LIST" as keyof ChainConfig, + // Networking params that may be in presets instead of chainConfig + "ATTESTATION_SUBNET_COUNT" as keyof ChainConfig, + "ATTESTATION_SUBNET_EXTRA_BITS" as keyof ChainConfig, + "ATTESTATION_SUBNET_PREFIX_BITS" as keyof ChainConfig, + // Future spec params not yet in Lodestar + "EPOCHS_PER_SHUFFLING_PHASE" as keyof ChainConfig, + "PROPOSER_SELECTION_GAP" as keyof ChainConfig, + // Network-specific fork epochs and versions - these vary per network deployment + // and are not meant to be synced from the spec defaults + "ALTAIR_FORK_EPOCH", + "BELLATRIX_FORK_EPOCH", + "CAPELLA_FORK_EPOCH", + "DENEB_FORK_EPOCH", + "ELECTRA_FORK_EPOCH", + "FULU_FORK_EPOCH", + "GLOAS_FORK_EPOCH", + // Terminal values are network-specific + "TERMINAL_TOTAL_DIFFICULTY", + "TERMINAL_BLOCK_HASH", + "TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH", + // Genesis values are network-specific + "MIN_GENESIS_TIME", + "MIN_GENESIS_ACTIVE_VALIDATOR_COUNT", + "GENESIS_DELAY", + "GENESIS_FORK_VERSION", + // These are preset values, not config values - they're tested separately + "PRESET_BASE", + "CONFIG_NAME", +]; + +/** + * Fields that we filter from local config when doing comparison. + * Ideally this should be empty as it is not spec compliant. + */ +const ignoredLocalConfigFields: (keyof ChainConfig)[] = []; + +describe("Ensure chainConfig is synced", () => { + vi.setConfig({testTimeout: 60 * 1000}); + + it("mainnet chainConfig values match spec", async () => { + const remoteConfig = await downloadRemoteConfig("mainnet", ethereumConsensusSpecsTests.specVersion); + assertCorrectConfig({...mainnetChainConfig}, remoteConfig); + }); + + it("minimal chainConfig values match spec", async () => { + const remoteConfig = await downloadRemoteConfig("minimal", ethereumConsensusSpecsTests.specVersion); + assertCorrectConfig({...minimalChainConfig}, remoteConfig); + }); +}); + +function assertCorrectConfig(localConfig: ChainConfig, remoteConfig: Partial): void { + // Filter out ignored fields from local config + const filteredLocalConfig: Partial = {}; + for (const key of Object.keys(localConfig) as (keyof ChainConfig)[]) { + if (!ignoredLocalConfigFields.includes(key)) { + (filteredLocalConfig as Record)[key] = localConfig[key]; + } + } + + // Filter out ignored fields from remote config + const filteredRemoteConfig: Partial = {}; + for (const key of Object.keys(remoteConfig) as (keyof ChainConfig)[]) { + if (!ignoredRemoteConfigFields.includes(key)) { + (filteredRemoteConfig as Record)[key] = remoteConfig[key]; + } + } + + // Check each key for better debuggability + for (const key of Object.keys(filteredRemoteConfig) as (keyof ChainConfig)[]) { + const localValue = filteredLocalConfig[key]; + const remoteValue = filteredRemoteConfig[key]; + + // If localValue is undefined, it means a config is missing from our local implementation + if (localValue === undefined) { + expect(localValue).toBeWithMessage(remoteValue, `${key} is present in remote spec but not in local config`); + continue; + } + + // Skip if remoteValue is undefined (local-only field) + if (remoteValue === undefined) { + continue; + } + + // Handle BigInt comparison + if (typeof localValue === "bigint" || typeof remoteValue === "bigint") { + expect(BigInt(localValue as bigint)).toBeWithMessage( + BigInt(remoteValue as bigint), + `${key} does not match: local=${localValue}, remote=${remoteValue}` + ); + } + // Handle Uint8Array (hex bytes) comparison + else if (localValue instanceof Uint8Array || remoteValue instanceof Uint8Array) { + const localHex = Buffer.from(localValue as Uint8Array).toString("hex"); + const remoteHex = Buffer.from(remoteValue as Uint8Array).toString("hex"); + expect(localHex).toBeWithMessage(remoteHex, `${key} does not match: local=0x${localHex}, remote=0x${remoteHex}`); + } + // Handle number/string comparison + else { + expect(localValue).toBeWithMessage( + remoteValue, + `${key} does not match: local=${localValue}, remote=${remoteValue}` + ); + } + } +} + +async function downloadRemoteConfig(network: "mainnet" | "minimal", commit: string): Promise> { + const url = `https://raw.githubusercontent.com/ethereum/consensus-specs/${commit}/configs/${network}.yaml`; + const response = await fetch(url, {signal: AbortSignal.timeout(30_000)}); + + if (!response.ok) { + throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`); + } + + return parseConfigYaml(await response.text()); +} + +function parseConfigYaml(yaml: string): Partial { + const config: Record = {}; + + for (const line of yaml.split("\n")) { + // Skip comments and empty lines + if (line.startsWith("#") || line.trim() === "") { + continue; + } + + const match = line.match(/^([A-Z_]+):\s*(.+)$/); + if (match) { + const [, key, rawValue] = match; + const value = rawValue.trim().replace(/^(['"])(.*)\\1$/, "$2"); // Remove matching quotes + + // Parse the value based on its format + if (value.startsWith("0x")) { + // Hex bytes + config[key] = fromHex(value); + } else if (/^\d+$/.test(value)) { + // Integer - use BigInt for large numbers, number for small ones + const num = BigInt(value); + // Use number if it fits, BigInt for large values + config[key] = num <= Number.MAX_SAFE_INTEGER ? Number(num) : num; + } else { + // String value (like preset name) + config[key] = value; + } + } + } + + return config as Partial; +} diff --git a/packages/db/package.json b/packages/db/package.json index 3e8641974658..dba1d1c5af7d 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@lodestar/db", - "version": "1.39.1", + "version": "1.40.0", "description": "DB modules of Lodestar", "author": "ChainSafe Systems", "homepage": "https://github.com/ChainSafe/lodestar#readme", diff --git a/packages/db/src/abstractPrefixedRepository.ts b/packages/db/src/abstractPrefixedRepository.ts index d27e630d165a..76cf6b58e0d2 100644 --- a/packages/db/src/abstractPrefixedRepository.ts +++ b/packages/db/src/abstractPrefixedRepository.ts @@ -2,7 +2,7 @@ import {Type} from "@chainsafe/ssz"; import {ChainForkConfig} from "@lodestar/config"; import {BUCKET_LENGTH} from "./const.js"; import {KeyValue} from "./controller/index.js"; -import {Db, DbReqOpts, FilterOptions} from "./controller/interface.js"; +import {Db, DbBatch, DbReqOpts, FilterOptions} from "./controller/interface.js"; import {encodeKey} from "./util.js"; type Id = Uint8Array | string | number | bigint; @@ -148,6 +148,58 @@ export abstract class PrefixedRepository { await this.db.batchDelete(keys.flat(), this.dbReqOpts); } + async batch(prefix: P, batch: DbBatch): Promise { + const batchWithKeys = []; + for (const b of batch) { + if (b.type === "del") { + batchWithKeys.push({type: b.type, key: this.wrapKey(this.encodeKeyRaw(prefix, b.key))}); + } else { + batchWithKeys.push({ + type: b.type, + key: this.wrapKey(this.encodeKeyRaw(prefix, b.key)), + value: this.encodeValue(b.value), + }); + } + } + await this.db.batch(batchWithKeys, this.dbReqOpts); + } + + async batchBinary(prefix: P, batch: DbBatch): Promise { + const batchWithKeys = []; + for (const b of batch) { + batchWithKeys.push({...b, key: this.wrapKey(this.encodeKeyRaw(prefix, b.key))}); + } + await this.db.batch(batchWithKeys, this.dbReqOpts); + } + + async values(prefix: P | P[]): Promise { + const result: T[] = []; + for (const p of Array.isArray(prefix) ? prefix : [prefix]) { + for await (const vb of this.db.valuesStream({ + gte: this.wrapKey(this.getMinKeyRaw(p)), + lte: this.wrapKey(this.getMaxKeyRaw(p)), + bucketId: this.bucketId, + })) { + result.push(this.decodeValue(vb)); + } + } + return result; + } + + async valuesBinary(prefix: P | P[]): Promise { + const result: Uint8Array[] = []; + for (const p of Array.isArray(prefix) ? prefix : [prefix]) { + for await (const vb of this.db.valuesStream({ + gte: this.wrapKey(this.getMinKeyRaw(p)), + lte: this.wrapKey(this.getMaxKeyRaw(p)), + bucketId: this.bucketId, + })) { + result.push(vb); + } + } + return result; + } + async *valuesStream(prefix: P | P[]): AsyncIterable { for (const p of Array.isArray(prefix) ? prefix : [prefix]) { for await (const vb of this.db.valuesStream({ diff --git a/packages/db/src/abstractRepository.ts b/packages/db/src/abstractRepository.ts index c42c9459b2ca..6ed7384b511c 100644 --- a/packages/db/src/abstractRepository.ts +++ b/packages/db/src/abstractRepository.ts @@ -2,30 +2,26 @@ import {Type} from "@chainsafe/ssz"; import {ChainForkConfig} from "@lodestar/config"; import {BUCKET_LENGTH} from "./const.js"; import {FilterOptions, KeyValue} from "./controller/index.js"; -import {Db, DbReqOpts} from "./controller/interface.js"; +import {Db, DbBatch, DbReqOpts} from "./controller/interface.js"; import {encodeKey as _encodeKey} from "./util.js"; export type Id = Uint8Array | string | number | bigint; /** - * Repository is a high level kv storage + * BinaryRepository is a high level kv storage * managing a Uint8Array to Uint8Array kv database - * It translates typed keys and values to Uint8Arrays required by the underlying database - * - * By default, SSZ-encoded values, - * indexed by root + * It translates typed keys and Uint8Array values required by the underlying database */ -export abstract class Repository { - private readonly dbReqOpts: DbReqOpts; +export abstract class BinaryRepository { + protected readonly dbReqOpts: DbReqOpts; - private readonly minKey: Uint8Array; - private readonly maxKey: Uint8Array; + protected readonly minKey: Uint8Array; + protected readonly maxKey: Uint8Array; protected constructor( protected config: ChainForkConfig, protected db: Db, protected bucket: number, - protected type: Type, protected readonly bucketId: string ) { this.dbReqOpts = {bucketId: this.bucketId}; @@ -33,12 +29,35 @@ export abstract class Repository { this.maxKey = _encodeKey(bucket + 1, Buffer.alloc(0)); } - encodeValue(value: T): Uint8Array { - return this.type.serialize(value); + async keys(opts?: FilterOptions): Promise { + const data = await this.db.keys(this.dbFilterOptions(opts)); + return (data ?? []).map((data) => this.decodeKey(data)); } - decodeValue(data: Uint8Array): T { - return this.type.deserialize(data); + async *keysStream(opts?: FilterOptions): AsyncIterable { + const keysStream = this.db.keysStream(this.dbFilterOptions(opts)); + const decodeKey = this.decodeKey.bind(this); + for await (const key of keysStream) { + yield decodeKey(key); + } + } + + async firstKey(): Promise { + // Metrics accounted in this.keys() + const keys = await this.keys({limit: 1, bucketId: this.bucketId}); + if (!keys.length) { + return null; + } + return keys[0]; + } + + async lastKey(): Promise { + // Metrics accounted in this.keys() + const keys = await this.keys({limit: 1, reverse: true, bucketId: this.bucketId}); + if (!keys.length) { + return null; + } + return keys[0]; } encodeKey(id: I): Uint8Array { @@ -49,34 +68,147 @@ export abstract class Repository { return key.slice(BUCKET_LENGTH) as I; } - async get(id: I): Promise { - const value = await this.db.get(this.encodeKey(id), this.dbReqOpts); - if (!value) return null; - return this.decodeValue(value); - } - async getBinary(id: I): Promise { const value = await this.db.get(this.encodeKey(id), this.dbReqOpts); if (!value) return null; return value; } - async has(id: I): Promise { - return (await this.get(id)) !== null; + async putBinary(id: I, value: Uint8Array): Promise { + await this.db.put(this.encodeKey(id), value, this.dbReqOpts); } - async put(id: I, value: T): Promise { - await this.db.put(this.encodeKey(id), this.encodeValue(value), this.dbReqOpts); + async binaries(opts?: FilterOptions): Promise { + const data = await this.db.values(this.dbFilterOptions(opts)); + return data ?? []; } - async putBinary(id: I, value: Uint8Array): Promise { - await this.db.put(this.encodeKey(id), value, this.dbReqOpts); + async lastBinary(): Promise { + // Metrics accounted in this.values() + const binaryValues = await this.binaries({limit: 1, reverse: true, bucketId: this.bucketId}); + if (!binaryValues.length) { + return null; + } + return binaryValues[0]; + } + + // Similar to batchPut but we support value as Uint8Array + async batchPutBinary(items: KeyValue[]): Promise { + if (items.length === 1) { + return this.db.put(this.encodeKey(items[0].key), items[0].value, this.dbReqOpts); + } + + await this.db.batchPut( + Array.from({length: items.length}, (_, i) => ({ + key: this.encodeKey(items[i].key), + value: items[i].value, + })), + this.dbReqOpts + ); + } + + async *binaryEntriesStream(opts?: FilterOptions): AsyncIterable> { + yield* this.db.entriesStream(this.dbFilterOptions(opts)); + } + + async has(id: I): Promise { + return (await this.getBinary(id)) !== null; } async delete(id: I): Promise { await this.db.delete(this.encodeKey(id), this.dbReqOpts); } + async batchDelete(ids: I[]): Promise { + if (ids.length === 1) { + return this.delete(ids[0]); + } + + await this.db.batchDelete( + Array.from({length: ids.length}, (_, i) => this.encodeKey(ids[i])), + this.dbReqOpts + ); + } + + async batchBinary(batch: DbBatch): Promise { + const batchWithKeys: DbBatch = []; + for (const b of batch) { + batchWithKeys.push({...b, key: this.encodeKey(b.key)}); + } + await this.db.batch(batchWithKeys, this.dbReqOpts); + } + + /** + * Transforms opts from I to Uint8Array + */ + protected dbFilterOptions(opts?: FilterOptions): FilterOptions { + const optsBuff: FilterOptions = { + bucketId: this.bucketId, + }; + + // Set at least one min key + if (opts?.lt !== undefined) { + optsBuff.lt = this.encodeKey(opts.lt); + } else if (opts?.lte !== undefined) { + optsBuff.lte = this.encodeKey(opts.lte); + } else { + optsBuff.lt = this.maxKey; + } + + // Set at least one max key + if (opts?.gt !== undefined) { + optsBuff.gt = this.encodeKey(opts.gt); + } else if (opts?.gte !== undefined) { + optsBuff.gte = this.encodeKey(opts.gte); + } else { + optsBuff.gte = this.minKey; + } + + if (opts?.reverse !== undefined) optsBuff.reverse = opts.reverse; + if (opts?.limit !== undefined) optsBuff.limit = opts.limit; + + return optsBuff; + } +} + +/** + * Repository is a high level kv storage + * managing a Uint8Array to Uint8Array kv database + * It translates typed keys and values to Uint8Arrays required by the underlying database + * + * By default, SSZ-encoded values, + * indexed by root + */ +export abstract class Repository extends BinaryRepository { + protected constructor( + config: ChainForkConfig, + db: Db, + bucket: number, + protected type: Type, + bucketId: string + ) { + super(config, db, bucket, bucketId); + this.type = type; + } + + encodeValue(value: T): Uint8Array { + return this.type.serialize(value); + } + + decodeValue(data: Uint8Array): T { + return this.type.deserialize(data); + } + + async get(id: I): Promise { + const value = await this.db.get(this.encodeKey(id), this.dbReqOpts); + if (!value) return null; + return this.decodeValue(value); + } + + async put(id: I, value: T): Promise { + await this.db.put(this.encodeKey(id), this.encodeValue(value), this.dbReqOpts); + } + // The Id can be inferred from the value getId(value: T): I { return this.type.hashTreeRoot(value) as I; @@ -104,30 +236,16 @@ export abstract class Repository { ); } - // Similar to batchPut but we support value as Uint8Array - async batchPutBinary(items: KeyValue[]): Promise { - if (items.length === 1) { - return this.db.put(this.encodeKey(items[0].key), items[0].value, this.dbReqOpts); - } - - await this.db.batchPut( - Array.from({length: items.length}, (_, i) => ({ - key: this.encodeKey(items[i].key), - value: items[i].value, - })), - this.dbReqOpts - ); - } - - async batchDelete(ids: I[]): Promise { - if (ids.length === 1) { - return this.delete(ids[0]); + async batch(batch: DbBatch): Promise { + const batchWithKeys: DbBatch = []; + for (const b of batch) { + if (b.type === "del") { + batchWithKeys.push({...b, key: this.encodeKey(b.key)}); + } else { + batchWithKeys.push({...b, key: this.encodeKey(b.key), value: this.encodeValue(b.value)}); + } } - - await this.db.batchDelete( - Array.from({length: ids.length}, (_, i) => this.encodeKey(ids[i])), - this.dbReqOpts - ); + await this.db.batch(batchWithKeys, this.dbReqOpts); } async batchAdd(values: T[]): Promise { @@ -145,21 +263,8 @@ export abstract class Repository { await this.batchDelete(Array.from({length: values.length}, (_ignored, i) => this.getId(values[i]))); } - async keys(opts?: FilterOptions): Promise { - const data = await this.db.keys(this.dbFilterOptions(opts)); - return (data ?? []).map((data) => this.decodeKey(data)); - } - - async *keysStream(opts?: FilterOptions): AsyncIterable { - const keysStream = this.db.keysStream(this.dbFilterOptions(opts)); - const decodeKey = this.decodeKey.bind(this); - for await (const key of keysStream) { - yield decodeKey(key); - } - } - async values(opts?: FilterOptions): Promise { - const data = await this.db.values(this.dbFilterOptions(opts)); + const data = await this.binaries(opts); return (data ?? []).map((data) => this.decodeValue(data)); } @@ -171,10 +276,6 @@ export abstract class Repository { } } - async *binaryEntriesStream(opts?: FilterOptions): AsyncIterable> { - yield* this.db.entriesStream(this.dbFilterOptions(opts)); - } - async entries(opts?: FilterOptions): Promise[]> { const data = await this.db.entries(this.dbFilterOptions(opts)); return (data ?? []).map((data) => ({ @@ -195,24 +296,6 @@ export abstract class Repository { } } - async firstKey(): Promise { - // Metrics accounted in this.keys() - const keys = await this.keys({limit: 1, bucketId: this.bucketId}); - if (!keys.length) { - return null; - } - return keys[0]; - } - - async lastKey(): Promise { - // Metrics accounted in this.keys() - const keys = await this.keys({limit: 1, reverse: true, bucketId: this.bucketId}); - if (!keys.length) { - return null; - } - return keys[0]; - } - async firstValue(): Promise { // Metrics accounted in this.values() const values = await this.values({limit: 1, bucketId: this.bucketId}); @@ -248,36 +331,4 @@ export abstract class Repository { } return entries[0]; } - - /** - * Transforms opts from I to Uint8Array - */ - protected dbFilterOptions(opts?: FilterOptions): FilterOptions { - const optsBuff: FilterOptions = { - bucketId: this.bucketId, - }; - - // Set at least one min key - if (opts?.lt !== undefined) { - optsBuff.lt = this.encodeKey(opts.lt); - } else if (opts?.lte !== undefined) { - optsBuff.lte = this.encodeKey(opts.lte); - } else { - optsBuff.lt = this.maxKey; - } - - // Set at least on max key - if (opts?.gt !== undefined) { - optsBuff.gt = this.encodeKey(opts.gt); - } else if (opts?.gte !== undefined) { - optsBuff.gte = this.encodeKey(opts.gte); - } else { - optsBuff.gte = this.minKey; - } - - if (opts?.reverse !== undefined) optsBuff.reverse = opts.reverse; - if (opts?.limit !== undefined) optsBuff.limit = opts.limit; - - return optsBuff; - } } diff --git a/packages/db/src/controller/index.ts b/packages/db/src/controller/index.ts index e1884b623995..8beb2d7a4349 100644 --- a/packages/db/src/controller/index.ts +++ b/packages/db/src/controller/index.ts @@ -1,3 +1,11 @@ -export type {DatabaseController, Db, DbReqOpts, FilterOptions, KeyValue} from "./interface.js"; +export type { + DatabaseController, + Db, + DbBatch, + DbBatchOperation, + DbReqOpts, + FilterOptions, + KeyValue, +} from "./interface.js"; export {LevelDbController} from "./level.js"; export type {LevelDbControllerMetrics} from "./metrics.js"; diff --git a/packages/db/src/controller/interface.ts b/packages/db/src/controller/interface.ts index 7a3a3d7f96bc..43d6819403db 100644 --- a/packages/db/src/controller/interface.ts +++ b/packages/db/src/controller/interface.ts @@ -28,6 +28,9 @@ export interface KeyValue { value: V; } +export type DbBatchOperation = {type: "del"; key: K} | {type: "put"; key: K; value: V}; +export type DbBatch = DbBatchOperation[]; + export interface DatabaseController { // service start / stop @@ -48,6 +51,7 @@ export interface DatabaseController { batchPut(items: KeyValue[], opts?: DbReqOpts): Promise; batchDelete(keys: K[], opts?: DbReqOpts): Promise; + batch(batch: DbBatch, opts?: DbReqOpts): Promise; // Iterate over entries diff --git a/packages/db/src/controller/level.ts b/packages/db/src/controller/level.ts index a945634ba58d..65512abdcf42 100644 --- a/packages/db/src/controller/level.ts +++ b/packages/db/src/controller/level.ts @@ -1,6 +1,6 @@ import {ClassicLevel} from "classic-level"; import {Logger} from "@lodestar/utils"; -import {DatabaseController, DatabaseOptions, DbReqOpts, FilterOptions, KeyValue} from "./interface.js"; +import {DatabaseController, DatabaseOptions, DbBatch, DbReqOpts, FilterOptions, KeyValue} from "./interface.js"; import {LevelDbControllerMetrics} from "./metrics.js"; enum Status { @@ -143,6 +143,13 @@ export class LevelDbController implements DatabaseController ({type: "del", key: key}))); } + batch(batch: DbBatch, opts?: DbReqOpts): Promise { + this.metrics?.dbWriteReq.inc({bucket: opts?.bucketId ?? BUCKET_ID_UNKNOWN}, 1); + this.metrics?.dbWriteItems.inc({bucket: opts?.bucketId ?? BUCKET_ID_UNKNOWN}, batch.length); + + return this.db.batch(batch); + } + keysStream(opts: FilterOptions = {}): AsyncIterable { return this.metricsIterator(this.db.keys(opts), (key) => key, opts.bucketId ?? BUCKET_ID_UNKNOWN); } diff --git a/packages/db/test/e2e/abstractPrefixedRepository.test.ts b/packages/db/test/e2e/abstractPrefixedRepository.test.ts index 06c6dc9306ae..411a2bccb7c3 100644 --- a/packages/db/test/e2e/abstractPrefixedRepository.test.ts +++ b/packages/db/test/e2e/abstractPrefixedRepository.test.ts @@ -177,6 +177,55 @@ describe("abstractPrefixedRepository", () => { await expect(fromAsync(repo.valuesStream(p2))).resolves.toEqual([]); }); + it("batch mixes put and del operations within a prefix", async () => { + const prefix = 40; + const col1 = testData[prefix][1]; + const col2 = testData[prefix][2]; + const col3 = testData[prefix][3]; + + // Setup initial state + await repo.put(prefix, col1); + await repo.put(prefix, col2); + expect(await repo.get(prefix, col1.column)).toEqual(col1); + expect(await repo.get(prefix, col2.column)).toEqual(col2); + expect(await repo.get(prefix, col3.column)).toBeNull(); + + // Mix put and del in a single batch + await repo.batch(prefix, [ + {type: "del", key: col1.column}, + {type: "put", key: col3.column, value: col3}, + {type: "del", key: col2.column}, + ]); + + expect(await repo.get(prefix, col1.column)).toBeNull(); + expect(await repo.get(prefix, col2.column)).toBeNull(); + expect(await repo.get(prefix, col3.column)).toEqual(col3); + }); + + it("batchBinary stores raw Uint8Array values without encoding", async () => { + const prefix = 41; + const id1 = 100; + const id2 = 101; + const rawValue1 = Buffer.from("raw-binary-1", "utf8"); + const rawValue2 = Buffer.from("raw-binary-2", "utf8"); + + await repo.batchBinary(prefix, [ + {type: "put", key: id1, value: rawValue1}, + {type: "put", key: id2, value: rawValue2}, + ]); + + // Values should be stored as-is (raw), not encoded via the type serializer + const binA = await repo.getBinary(prefix, id1); + const binB = await repo.getBinary(prefix, id2); + expect(Buffer.from(binA!)).toEqual(rawValue1); + expect(Buffer.from(binB!)).toEqual(rawValue2); + + // batchBinary can also delete + await repo.batchBinary(prefix, [{type: "del", key: id1}]); + expect(await repo.getBinary(prefix, id1)).toBeNull(); + expect(await repo.getBinary(prefix, id2)).not.toBeNull(); + }); + describe("valuesStream,valuesStreamBinary,entriesStream,entriesStreamBinary", () => { it("valuesStream should fetch for single and multiple prefixes", async () => { const p1 = 7; diff --git a/packages/db/test/e2e/abstractRepository.test.ts b/packages/db/test/e2e/abstractRepository.test.ts index 5bc53c15de29..6122d371165d 100644 --- a/packages/db/test/e2e/abstractRepository.test.ts +++ b/packages/db/test/e2e/abstractRepository.test.ts @@ -76,6 +76,53 @@ describe("abstractRepository", () => { expect(await repo.get(b)).toBeNull(); }); + it("batch mixes put and del operations atomically", async () => { + const a = Buffer.from([10]); + const b = Buffer.from([11]); + const c = Buffer.from([12]); + + // Setup initial state + await repo.put(a, "a"); + await repo.put(b, "b"); + expect(await repo.get(a)).toBe("a"); + expect(await repo.get(b)).toBe("b"); + expect(await repo.get(c)).toBeNull(); + + // Mix put and del in a single batch + await repo.batch([ + {type: "del", key: a}, + {type: "put", key: c, value: "c"}, + {type: "del", key: b}, + ]); + + expect(await repo.get(a)).toBeNull(); + expect(await repo.get(b)).toBeNull(); + expect(await repo.get(c)).toBe("c"); + }); + + it("batchBinary stores raw Uint8Array values without encoding", async () => { + const a = Buffer.from([20]); + const b = Buffer.from([21]); + const rawValue1 = Buffer.from("raw1", "utf8"); + const rawValue2 = Buffer.from("raw2", "utf8"); + + await repo.batchBinary([ + {type: "put", key: a, value: rawValue1}, + {type: "put", key: b, value: rawValue2}, + ]); + + // Values should be stored as-is (raw), not encoded via the type serializer + const binA = await repo.getBinary(a); + const binB = await repo.getBinary(b); + expect(Buffer.from(binA!)).toEqual(rawValue1); + expect(Buffer.from(binB!)).toEqual(rawValue2); + + // batchBinary can also delete + await repo.batchBinary([{type: "del", key: a}]); + expect(await repo.getBinary(a)).toBeNull(); + expect(await repo.getBinary(b)).not.toBeNull(); + }); + it("keys/values/entries and filters", async () => { const k10 = Buffer.from([10]); const k15 = Buffer.from([15]); diff --git a/packages/db/test/unit/controller/level.test.ts b/packages/db/test/unit/controller/level.test.ts index 835f56011bed..9bd0b68f6dbd 100644 --- a/packages/db/test/unit/controller/level.test.ts +++ b/packages/db/test/unit/controller/level.test.ts @@ -82,6 +82,55 @@ describe("LevelDB controller", () => { expect((await db.entries()).length).toBe(0); }); + it("test batch", async () => { + const [ + {key: k1, value: v1}, + {key: k2, value: v2}, + {key: k3, value: v3}, + {key: k4, value: v4}, + {key: k5, value: v5}, + ] = Array.from({length: 5}, (_, i) => ({ + key: Buffer.from(`test${i}`), + value: Buffer.from(`some value ${i}`), + })); + await db.put(k1, v1); + await db.put(k2, v2); + await db.put(k3, v3); + + expect(await db.entries()).toEqual([ + {key: k1, value: v1}, + {key: k2, value: v2}, + {key: k3, value: v3}, + ]); + + await db.batch([ + { + type: "del", + key: k1, + }, + { + type: "put", + key: k4, + value: v4, + }, + { + type: "del", + key: k3, + }, + { + type: "put", + key: k5, + value: v5, + }, + ]); + + expect(await db.entries()).toEqual([ + {key: k2, value: v2}, + {key: k4, value: v4}, + {key: k5, value: v5}, + ]); + }); + it("test entries", async () => { const k1 = Buffer.from("test1"); const k2 = Buffer.from("test2"); diff --git a/packages/era/package.json b/packages/era/package.json index 711603ff6ec6..7947184eeb17 100644 --- a/packages/era/package.json +++ b/packages/era/package.json @@ -11,7 +11,7 @@ "bugs": { "url": "https://github.com/ChainSafe/lodestar/issues" }, - "version": "1.39.1", + "version": "1.40.0", "type": "module", "exports": { ".": { @@ -44,6 +44,7 @@ "@lodestar/params": "workspace:^", "@lodestar/reqresp": "workspace:^", "@lodestar/types": "workspace:^", + "@lodestar/utils": "workspace:^", "uint8arraylist": "^2.4.7" } } diff --git a/packages/era/src/e2s.ts b/packages/era/src/e2s.ts index e3cccec86bde..0060cd167d7f 100644 --- a/packages/era/src/e2s.ts +++ b/packages/era/src/e2s.ts @@ -1,5 +1,6 @@ import type {FileHandle} from "node:fs/promises"; import {Slot} from "@lodestar/types"; +import {byteArrayEquals} from "@lodestar/utils"; import {readInt48, readUint16, readUint32, writeInt48, writeUint16, writeUint32} from "./util.ts"; /** @@ -94,7 +95,7 @@ export function parseEntryHeader(header: Uint8Array): {type: EntryType; length: export async function readVersion(fh: FileHandle, offset: number): Promise { const versionHeader = new Uint8Array(E2STORE_HEADER_SIZE); await fh.read(versionHeader, 0, E2STORE_HEADER_SIZE, offset); - if (Buffer.compare(versionHeader, VERSION_RECORD_BYTES) !== 0) { + if (!byteArrayEquals(versionHeader, VERSION_RECORD_BYTES)) { throw new Error("Invalid E2Store version record"); } } diff --git a/packages/era/src/era/reader.ts b/packages/era/src/era/reader.ts index 78e8008d2cb6..bf7f8df3460b 100644 --- a/packages/era/src/era/reader.ts +++ b/packages/era/src/era/reader.ts @@ -4,6 +4,7 @@ import {PublicKey, Signature, verify} from "@chainsafe/blst"; import {ChainForkConfig, createCachedGenesis} from "@lodestar/config"; import {DOMAIN_BEACON_PROPOSER, GENESIS_SLOT, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; import {BeaconState, SignedBeaconBlock, Slot, ssz} from "@lodestar/types"; +import {byteArrayEquals} from "@lodestar/utils"; import {E2STORE_HEADER_SIZE, EntryType, readEntry, readVersion} from "../e2s.ts"; import {snappyUncompress} from "../util.ts"; import { @@ -166,10 +167,10 @@ export class EraReader { if (block === null) { if (slot === index.blocksIndex.startSlot) continue; // first slot in the era can't be easily validated if ( - Buffer.compare( + !byteArrayEquals( state.blockRoots[(slot - 1) % SLOTS_PER_HISTORICAL_ROOT], state.blockRoots[slot % SLOTS_PER_HISTORICAL_ROOT] - ) !== 0 + ) ) { throw new Error(`Block root mismatch at slot ${slot} for empty slot`); } @@ -177,7 +178,7 @@ export class EraReader { } const blockRoot = this.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block.message); - if (Buffer.compare(blockRoot, state.blockRoots[slot % SLOTS_PER_HISTORICAL_ROOT]) !== 0) { + if (!byteArrayEquals(blockRoot, state.blockRoots[slot % SLOTS_PER_HISTORICAL_ROOT])) { throw new Error(`Block root mismatch at slot ${slot}`); } // genesis block doesn't have valid signature diff --git a/packages/flare/package.json b/packages/flare/package.json index d6d9ff5edeae..45ada05108ce 100644 --- a/packages/flare/package.json +++ b/packages/flare/package.json @@ -1,6 +1,6 @@ { "name": "@lodestar/flare", - "version": "1.39.1", + "version": "1.40.0", "description": "Beacon chain debugging tool", "author": "ChainSafe Systems", "license": "Apache-2.0", diff --git a/packages/fork-choice/package.json b/packages/fork-choice/package.json index afc19a63be45..cdea9f49076e 100644 --- a/packages/fork-choice/package.json +++ b/packages/fork-choice/package.json @@ -11,7 +11,7 @@ "bugs": { "url": "https://github.com/ChainSafe/lodestar/issues" }, - "version": "1.39.1", + "version": "1.40.0", "type": "module", "exports": { ".": { diff --git a/packages/fork-choice/src/forkChoice/forkChoice.ts b/packages/fork-choice/src/forkChoice/forkChoice.ts index 779583d3f9b4..1d87d6608c40 100644 --- a/packages/fork-choice/src/forkChoice/forkChoice.ts +++ b/packages/fork-choice/src/forkChoice/forkChoice.ts @@ -23,6 +23,7 @@ import { RootHex, Slot, ValidatorIndex, + isGloasBeaconBlock, phase0, ssz, } from "@lodestar/types"; @@ -754,6 +755,15 @@ export class ForkChoice implements IForkChoice { executionStatus: this.getPreMergeExecStatus(executionStatus), dataAvailabilityStatus: this.getPreMergeDataStatus(dataAvailabilityStatus), }), + ...(isGloasBeaconBlock(block) + ? { + builderIndex: block.body.signedExecutionPayloadBid.message.builderIndex, + blockHashHex: toRootHex(block.body.signedExecutionPayloadBid.message.blockHash), + } + : { + builderIndex: undefined, + blockHashHex: undefined, + }), }; this.protoArray.onBlock(protoBlock, currentSlot); diff --git a/packages/fork-choice/src/protoArray/interface.ts b/packages/fork-choice/src/protoArray/interface.ts index 74b908cb1fc6..9c476b29a372 100644 --- a/packages/fork-choice/src/protoArray/interface.ts +++ b/packages/fork-choice/src/protoArray/interface.ts @@ -89,6 +89,10 @@ export type ProtoBlock = BlockExtraMeta & { // Indicate whether block arrives in a timely manner ie. before the 4 second mark timeliness: boolean; + + // GLOAS: The followings are from bids. Used for execution payload gossip validation + builderIndex?: number; + blockHashHex?: RootHex; }; /** diff --git a/packages/light-client/package.json b/packages/light-client/package.json index ca79c4f4e406..af18c1d13b87 100644 --- a/packages/light-client/package.json +++ b/packages/light-client/package.json @@ -11,7 +11,7 @@ "bugs": { "url": "https://github.com/ChainSafe/lodestar/issues" }, - "version": "1.39.1", + "version": "1.40.0", "type": "module", "exports": { ".": { @@ -79,7 +79,7 @@ "@fastify/cors": "^10.0.1", "@lodestar/state-transition": "workspace:^", "@types/qs": "^6.9.7", - "fastify": "^5.2.1", + "fastify": "^5.7.4", "qs": "^6.11.1", "uint8arrays": "^5.0.1" }, diff --git a/packages/light-client/src/spec/utils.ts b/packages/light-client/src/spec/utils.ts index 84cea8177a05..43f092ea1c8c 100644 --- a/packages/light-client/src/spec/utils.ts +++ b/packages/light-client/src/spec/utils.ts @@ -1,4 +1,4 @@ -import {BitArray, byteArrayEquals} from "@chainsafe/ssz"; +import {BitArray} from "@chainsafe/ssz"; import {ChainForkConfig} from "@lodestar/config"; import { BLOCK_BODY_EXECUTION_PAYLOAD_DEPTH as EXECUTION_PAYLOAD_DEPTH, @@ -22,6 +22,7 @@ import { isElectraLightClientUpdate, ssz, } from "@lodestar/types"; +import {byteArrayEquals} from "@lodestar/utils"; import {computeEpochAtSlot, computeSyncPeriodAtSlot, isValidMerkleBranch} from "../utils/index.js"; import {normalizeMerkleBranch} from "../utils/normalizeMerkleBranch.js"; import {LightClientStore} from "./store.js"; diff --git a/packages/light-client/src/spec/validateLightClientBootstrap.ts b/packages/light-client/src/spec/validateLightClientBootstrap.ts index 79c1f7a68fff..9d7b98954707 100644 --- a/packages/light-client/src/spec/validateLightClientBootstrap.ts +++ b/packages/light-client/src/spec/validateLightClientBootstrap.ts @@ -1,8 +1,7 @@ -import {byteArrayEquals} from "@chainsafe/ssz"; import {ChainForkConfig} from "@lodestar/config"; import {isForkPostElectra} from "@lodestar/params"; import {LightClientBootstrap, Root, ssz} from "@lodestar/types"; -import {toHex} from "@lodestar/utils"; +import {byteArrayEquals, toHex} from "@lodestar/utils"; import {isValidMerkleBranch} from "../utils/verifyMerkleBranch.js"; import {isValidLightClientHeader} from "./utils.js"; diff --git a/packages/light-client/src/utils/verifyMerkleBranch.ts b/packages/light-client/src/utils/verifyMerkleBranch.ts index a9a5b8643783..5d712a4eaeec 100644 --- a/packages/light-client/src/utils/verifyMerkleBranch.ts +++ b/packages/light-client/src/utils/verifyMerkleBranch.ts @@ -1,5 +1,5 @@ import {hasher} from "@chainsafe/persistent-merkle-tree"; -import {byteArrayEquals} from "@chainsafe/ssz"; +import {byteArrayEquals} from "@lodestar/utils"; export const SYNC_COMMITTEES_DEPTH = 4; export const SYNC_COMMITTEES_INDEX = 11; diff --git a/packages/light-client/test/unit/sync.node.test.ts b/packages/light-client/test/unit/sync.node.test.ts index 246e12293ec5..7841b1b78bbd 100644 --- a/packages/light-client/test/unit/sync.node.test.ts +++ b/packages/light-client/test/unit/sync.node.test.ts @@ -26,7 +26,7 @@ import { const SOME_HASH = Buffer.alloc(32, 0xff); describe("sync", () => { - vi.setConfig({testTimeout: 30_000}); + vi.setConfig({testTimeout: 45_000}); const afterEachCbs: (() => Promise | unknown)[] = []; afterEach(async () => { diff --git a/packages/light-client/test/unit/syncInMemory.test.ts b/packages/light-client/test/unit/syncInMemory.test.ts index 05aa882edd55..c3904fa8d0bb 100644 --- a/packages/light-client/test/unit/syncInMemory.test.ts +++ b/packages/light-client/test/unit/syncInMemory.test.ts @@ -23,8 +23,8 @@ function getSyncCommittee( } describe("syncInMemory", () => { - // In browser test this process is taking more time than default 2000ms - vi.setConfig({testTimeout: 20000, hookTimeout: 20000}); + // In browser test this process is taking more time than default 3000ms + vi.setConfig({testTimeout: 30000, hookTimeout: 30000}); // Fixed params const genValiRoot = Buffer.alloc(32, 9); diff --git a/packages/light-client/test/utils/server.ts b/packages/light-client/test/utils/server.ts index 06205ad1c040..21ad80da129b 100644 --- a/packages/light-client/test/utils/server.ts +++ b/packages/light-client/test/utils/server.ts @@ -21,7 +21,9 @@ export async function startServer( const server = fastify({ logger: false, ajv: {customOptions: {coerceTypes: "array"}}, - querystringParser: (str) => parseQueryString(str, {comma: true, parseArrays: false}), + routerOptions: { + querystringParser: (str) => parseQueryString(str, {comma: true, parseArrays: false}), + }, }); addSszContentTypeParser(server); diff --git a/packages/logger/package.json b/packages/logger/package.json index ad32106dcf1a..1350175546a0 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -11,7 +11,7 @@ "bugs": { "url": "https://github.com/ChainSafe/lodestar/issues" }, - "version": "1.39.1", + "version": "1.40.0", "type": "module", "exports": { ".": { diff --git a/packages/params/package.json b/packages/params/package.json index 647862cc8747..5e14929685e7 100644 --- a/packages/params/package.json +++ b/packages/params/package.json @@ -1,6 +1,6 @@ { "name": "@lodestar/params", - "version": "1.39.1", + "version": "1.40.0", "description": "Chain parameters required for lodestar", "author": "ChainSafe Systems", "license": "Apache-2.0", @@ -68,7 +68,7 @@ ], "devDependencies": { "@types/js-yaml": "^4.0.5", - "axios": "^1.3.4", + "axios": "^1.13.2", "js-yaml": "^4.1.0" } } diff --git a/packages/params/src/index.ts b/packages/params/src/index.ts index abecfb024368..34bc96977b4f 100644 --- a/packages/params/src/index.ts +++ b/packages/params/src/index.ts @@ -117,7 +117,9 @@ export const { PTC_SIZE, MAX_PAYLOAD_ATTESTATIONS, + BUILDER_REGISTRY_LIMIT, BUILDER_PENDING_WITHDRAWALS_LIMIT, + MAX_BUILDERS_PER_WITHDRAWALS_SWEEP, } = activePreset; //////////// @@ -157,8 +159,9 @@ export const DOMAIN_SYNC_COMMITTEE = Uint8Array.from([7, 0, 0, 0]); export const DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF = Uint8Array.from([8, 0, 0, 0]); export const DOMAIN_CONTRIBUTION_AND_PROOF = Uint8Array.from([9, 0, 0, 0]); export const DOMAIN_BLS_TO_EXECUTION_CHANGE = Uint8Array.from([10, 0, 0, 0]); -export const DOMAIN_BEACON_BUILDER = Uint8Array.from([27, 0, 0, 0]); +export const DOMAIN_BEACON_BUILDER = Uint8Array.from([11, 0, 0, 0]); export const DOMAIN_PTC_ATTESTER = Uint8Array.from([12, 0, 0, 0]); +export const DOMAIN_PROPOSER_PREFERENCES = Uint8Array.from([13, 0, 0, 0]); // Application specific domains @@ -313,5 +316,7 @@ export const KZG_COMMITMENTS_GINDEX = 27; export const KZG_COMMITMENTS_SUBTREE_INDEX = KZG_COMMITMENTS_GINDEX - 2 ** KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH; // Gloas Misc +export const BUILDER_INDEX_FLAG = 2 ** 40; +export const BUILDER_INDEX_SELF_BUILD = Infinity; export const BUILDER_PAYMENT_THRESHOLD_NUMERATOR = 6; export const BUILDER_PAYMENT_THRESHOLD_DENOMINATOR = 10; diff --git a/packages/params/src/presets/mainnet.ts b/packages/params/src/presets/mainnet.ts index 48c24bc48475..349f0058031e 100644 --- a/packages/params/src/presets/mainnet.ts +++ b/packages/params/src/presets/mainnet.ts @@ -145,5 +145,7 @@ export const mainnetPreset: BeaconPreset = { // GLOAS PTC_SIZE: 512, MAX_PAYLOAD_ATTESTATIONS: 4, + BUILDER_REGISTRY_LIMIT: 1099511627776, // 2**40 BUILDER_PENDING_WITHDRAWALS_LIMIT: 1048576, // 2**20 + MAX_BUILDERS_PER_WITHDRAWALS_SWEEP: 16384, // 2**14 }; diff --git a/packages/params/src/presets/minimal.ts b/packages/params/src/presets/minimal.ts index bb015227432b..42486961ce2d 100644 --- a/packages/params/src/presets/minimal.ts +++ b/packages/params/src/presets/minimal.ts @@ -146,5 +146,7 @@ export const minimalPreset: BeaconPreset = { // GLOAS PTC_SIZE: 2, MAX_PAYLOAD_ATTESTATIONS: 4, + BUILDER_REGISTRY_LIMIT: 1099511627776, // 2**40 BUILDER_PENDING_WITHDRAWALS_LIMIT: 1048576, // 2**20 + MAX_BUILDERS_PER_WITHDRAWALS_SWEEP: 16, // 2**4 }; diff --git a/packages/params/src/types.ts b/packages/params/src/types.ts index 0d8f4940a8fb..e4451914fdfd 100644 --- a/packages/params/src/types.ts +++ b/packages/params/src/types.ts @@ -107,7 +107,9 @@ export type BeaconPreset = { // GLOAS PTC_SIZE: number; MAX_PAYLOAD_ATTESTATIONS: number; + BUILDER_REGISTRY_LIMIT: number; BUILDER_PENDING_WITHDRAWALS_LIMIT: number; + MAX_BUILDERS_PER_WITHDRAWALS_SWEEP: number; }; /** @@ -220,7 +222,9 @@ export const beaconPresetTypes: BeaconPresetTypes = { // GLOAS PTC_SIZE: "number", MAX_PAYLOAD_ATTESTATIONS: "number", + BUILDER_REGISTRY_LIMIT: "number", BUILDER_PENDING_WITHDRAWALS_LIMIT: "number", + MAX_BUILDERS_PER_WITHDRAWALS_SWEEP: "number", }; type BeaconPresetTypes = { diff --git a/packages/params/test/e2e/ensure-config-is-synced.test.ts b/packages/params/test/e2e/ensure-config-is-synced.test.ts index 21dabba8ec29..ba9849eff410 100644 --- a/packages/params/test/e2e/ensure-config-is-synced.test.ts +++ b/packages/params/test/e2e/ensure-config-is-synced.test.ts @@ -1,36 +1,28 @@ import axios from "axios"; import {describe, expect, it, vi} from "vitest"; +import {ethereumConsensusSpecsTests} from "../../../beacon-node/test/spec/specTestVersioning.js"; import {BeaconPreset, ForkName} from "../../src/index.js"; import {mainnetPreset} from "../../src/presets/mainnet.js"; import {minimalPreset} from "../../src/presets/minimal.js"; import {loadConfigYaml} from "../yaml.js"; // Not e2e, but slow. Run with e2e tests - -/** https://github.com/ethereum/consensus-specs/releases */ -const specConfigCommit = "v1.6.1"; /** * Fields that we filter from local config when doing comparison. * Ideally this should be empty as it is not spec compliant */ -// TODO GLOAS: These fields are supposed to be in the preset. However Gloas's preset in consensus-specs are still not up to date. -/// Remove these fields after a spec is released that includes this fix https://github.com/ethereum/consensus-specs/pull/4607 -const ignoredLocalPresetFields: (keyof BeaconPreset)[] = [ - "MAX_PAYLOAD_ATTESTATIONS", - "PTC_SIZE", - "BUILDER_PENDING_WITHDRAWALS_LIMIT", -]; +const ignoredLocalPresetFields: (keyof BeaconPreset)[] = []; describe("Ensure config is synced", () => { vi.setConfig({testTimeout: 60 * 1000}); it("mainnet", async () => { - const remotePreset = await downloadRemoteConfig("mainnet", specConfigCommit); + const remotePreset = await downloadRemoteConfig("mainnet", ethereumConsensusSpecsTests.specVersion); assertCorrectPreset({...mainnetPreset}, remotePreset); }); it("minimal", async () => { - const remotePreset = await downloadRemoteConfig("minimal", specConfigCommit); + const remotePreset = await downloadRemoteConfig("minimal", ethereumConsensusSpecsTests.specVersion); assertCorrectPreset({...minimalPreset}, remotePreset); }); }); @@ -61,10 +53,6 @@ async function downloadRemoteConfig(preset: "mainnet" | "minimal", commit: strin const downloadedParams: Record[] = []; for (const forkName of Object.values(ForkName)) { - // TODO GLOAS: Remove this when gloas spec is available - if (forkName === ForkName.gloas) { - continue; - } const response = await axios({ url: `https://raw.githubusercontent.com/ethereum/consensus-specs/${commit}/presets/${preset}/${forkName}.yaml`, timeout: 30 * 1000, diff --git a/packages/prover/package.json b/packages/prover/package.json index e3845c36022a..3e3c6ed187ff 100644 --- a/packages/prover/package.json +++ b/packages/prover/package.json @@ -11,7 +11,7 @@ "bugs": { "url": "https://github.com/ChainSafe/lodestar/issues" }, - "version": "1.39.1", + "version": "1.40.0", "type": "module", "exports": { ".": { @@ -79,7 +79,7 @@ "@types/http-proxy": "^1.17.10", "@types/js-yaml": "^4.0.5", "@types/yargs": "^17.0.24", - "axios": "^1.3.4", + "axios": "^1.13.2", "deepmerge": "^4.3.1", "ethers": "^6.7.0", "web3": "^4.0.3" diff --git a/packages/prover/test/e2e/cli/cmds/start.test.ts b/packages/prover/test/e2e/cli/cmds/start.test.ts index 6f6b2978a281..fd850b83150d 100644 --- a/packages/prover/test/e2e/cli/cmds/start.test.ts +++ b/packages/prover/test/e2e/cli/cmds/start.test.ts @@ -53,7 +53,7 @@ describe("prover/proxy", () => { ); // Give sometime to the prover to start proxy server await sleep(3000); - }, 50000); + }, 80000); afterAll(async () => { if (proc) { diff --git a/packages/prover/test/utils/e2e_env.ts b/packages/prover/test/utils/e2e_env.ts index 102719513e7c..2cbcee106afd 100644 --- a/packages/prover/test/utils/e2e_env.ts +++ b/packages/prover/test/utils/e2e_env.ts @@ -15,8 +15,8 @@ const denebForkEpoch = 0; const electraForkEpoch = 0; const genesisDelaySeconds = 30 * secondsPerSlot; -// Wait for at least 2 epochs to ensure light client can sync from a finalized checkpoint -export const minFinalizedTimeMs = 2 * 8 * 4 * 1000; +// Wait for at least 3 epochs to ensure light client can sync from a finalized checkpoint +export const minFinalizedTimeMs = 3 * 8 * 4 * 1000; export const config = { ALTAIR_FORK_EPOCH: altairForkEpoch, diff --git a/packages/reqresp/package.json b/packages/reqresp/package.json index e568c226e5c8..174de78130e3 100644 --- a/packages/reqresp/package.json +++ b/packages/reqresp/package.json @@ -11,7 +11,7 @@ "bugs": { "url": "https://github.com/ChainSafe/lodestar/issues" }, - "version": "1.39.1", + "version": "1.40.0", "type": "module", "exports": { ".": { diff --git a/packages/spec-test-util/package.json b/packages/spec-test-util/package.json index 6ad5969224e7..14575761f351 100644 --- a/packages/spec-test-util/package.json +++ b/packages/spec-test-util/package.json @@ -1,6 +1,6 @@ { "name": "@lodestar/spec-test-util", - "version": "1.39.1", + "version": "1.40.0", "description": "Spec test suite generator from yaml test files", "author": "ChainSafe Systems", "license": "Apache-2.0", diff --git a/packages/state-transition/package.json b/packages/state-transition/package.json index 9fbc538f546f..ebf94eb284fa 100644 --- a/packages/state-transition/package.json +++ b/packages/state-transition/package.json @@ -11,7 +11,7 @@ "bugs": { "url": "https://github.com/ChainSafe/lodestar/issues" }, - "version": "1.39.1", + "version": "1.40.0", "type": "module", "exports": { ".": { @@ -66,7 +66,7 @@ "@lodestar/params": "workspace:^", "@lodestar/types": "workspace:^", "@lodestar/utils": "workspace:^", - "bigint-buffer": "^1.1.5" + "@vekexasia/bigint-buffer2": "^1.1.0" }, "devDependencies": { "@lodestar/api": "workspace:^" diff --git a/packages/state-transition/src/block/index.ts b/packages/state-transition/src/block/index.ts index c208c7964ffa..1104a52e2fa9 100644 --- a/packages/state-transition/src/block/index.ts +++ b/packages/state-transition/src/block/index.ts @@ -38,6 +38,7 @@ export { export * from "./externalData.js"; export * from "./initiateValidatorExit.js"; export * from "./isValidIndexedAttestation.js"; +export * from "./processDepositRequest.js"; export * from "./processOperations.js"; export function processBlock( diff --git a/packages/state-transition/src/block/isValidIndexedAttestation.ts b/packages/state-transition/src/block/isValidIndexedAttestation.ts index 4cb09b0e46f1..d37504ef192b 100644 --- a/packages/state-transition/src/block/isValidIndexedAttestation.ts +++ b/packages/state-transition/src/block/isValidIndexedAttestation.ts @@ -1,9 +1,8 @@ import {BeaconConfig} from "@lodestar/config"; import {ForkSeq, MAX_COMMITTEES_PER_SLOT, MAX_VALIDATORS_PER_COMMITTEE} from "@lodestar/params"; -import {IndexedAttestation, IndexedAttestationBigint} from "@lodestar/types"; +import {IndexedAttestation, IndexedAttestationBigint, Slot} from "@lodestar/types"; import {Index2PubkeyCache} from "../cache/pubkeyCache.js"; import {getIndexedAttestationBigintSignatureSet, getIndexedAttestationSignatureSet} from "../signatureSets/index.js"; -import {CachedBeaconStateAllForks} from "../types.js"; import {verifySignatureSet} from "../util/index.js"; /** @@ -12,16 +11,17 @@ import {verifySignatureSet} from "../util/index.js"; export function isValidIndexedAttestation( config: BeaconConfig, index2pubkey: Index2PubkeyCache, - state: CachedBeaconStateAllForks, + stateSlot: Slot, + validatorsLen: number, indexedAttestation: IndexedAttestation, verifySignature: boolean ): boolean { - if (!isValidIndexedAttestationIndices(state, indexedAttestation.attestingIndices)) { + if (!isValidIndexedAttestationIndices(config, stateSlot, validatorsLen, indexedAttestation.attestingIndices)) { return false; } if (verifySignature) { - return verifySignatureSet(getIndexedAttestationSignatureSet(config, index2pubkey, state.slot, indexedAttestation)); + return verifySignatureSet(getIndexedAttestationSignatureSet(config, stateSlot, indexedAttestation), index2pubkey); } return true; } @@ -29,17 +29,19 @@ export function isValidIndexedAttestation( export function isValidIndexedAttestationBigint( config: BeaconConfig, index2pubkey: Index2PubkeyCache, - state: CachedBeaconStateAllForks, + stateSlot: Slot, + validatorsLen: number, indexedAttestation: IndexedAttestationBigint, verifySignature: boolean ): boolean { - if (!isValidIndexedAttestationIndices(state, indexedAttestation.attestingIndices)) { + if (!isValidIndexedAttestationIndices(config, stateSlot, validatorsLen, indexedAttestation.attestingIndices)) { return false; } if (verifySignature) { return verifySignatureSet( - getIndexedAttestationBigintSignatureSet(config, index2pubkey, state.slot, indexedAttestation) + getIndexedAttestationBigintSignatureSet(config, stateSlot, indexedAttestation), + index2pubkey ); } return true; @@ -48,10 +50,15 @@ export function isValidIndexedAttestationBigint( /** * Check if `indexedAttestation` has sorted and unique indices and a valid aggregate signature. */ -export function isValidIndexedAttestationIndices(state: CachedBeaconStateAllForks, indices: number[]): boolean { +export function isValidIndexedAttestationIndices( + config: BeaconConfig, + stateSlot: Slot, + validatorsLen: number, + indices: number[] +): boolean { // verify max number of indices const maxIndices = - state.config.getForkSeq(state.slot) >= ForkSeq.electra + config.getForkSeq(stateSlot) >= ForkSeq.electra ? MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT : MAX_VALIDATORS_PER_COMMITTEE; if (!(indices.length > 0 && indices.length <= maxIndices)) { @@ -68,9 +75,8 @@ export function isValidIndexedAttestationIndices(state: CachedBeaconStateAllFork } // check if indices are out of bounds, by checking the highest index (since it is sorted) - // TODO - SLOW CODE - Does this .length check the tree and is expensive? const lastIndex = indices.at(-1); - if (lastIndex && lastIndex >= state.validators.length) { + if (lastIndex && lastIndex >= validatorsLen) { return false; } diff --git a/packages/state-transition/src/block/isValidIndexedPayloadAttestation.ts b/packages/state-transition/src/block/isValidIndexedPayloadAttestation.ts index 3ab208c6c89c..3f669c56ada1 100644 --- a/packages/state-transition/src/block/isValidIndexedPayloadAttestation.ts +++ b/packages/state-transition/src/block/isValidIndexedPayloadAttestation.ts @@ -16,7 +16,10 @@ export function isValidIndexedPayloadAttestation( } if (verifySignature) { - return verifySignatureSet(getIndexedPayloadAttestationSignatureSet(state, indexedPayloadAttestation)); + return verifySignatureSet( + getIndexedPayloadAttestationSignatureSet(state, indexedPayloadAttestation), + state.epochCtx.index2pubkey + ); } return true; diff --git a/packages/state-transition/src/block/processAttestationPhase0.ts b/packages/state-transition/src/block/processAttestationPhase0.ts index b50ce38c22f7..20414784a17b 100644 --- a/packages/state-transition/src/block/processAttestationPhase0.ts +++ b/packages/state-transition/src/block/processAttestationPhase0.ts @@ -54,7 +54,8 @@ export function processAttestationPhase0( !isValidIndexedAttestation( state.config, epochCtx.index2pubkey, - state, + state.slot, + state.validators.length, epochCtx.getIndexedAttestation(ForkSeq.phase0, attestation), verifySignature ) diff --git a/packages/state-transition/src/block/processAttestationsAltair.ts b/packages/state-transition/src/block/processAttestationsAltair.ts index 3cf366bb8335..e13cfd7d3d38 100644 --- a/packages/state-transition/src/block/processAttestationsAltair.ts +++ b/packages/state-transition/src/block/processAttestationsAltair.ts @@ -1,4 +1,3 @@ -import {byteArrayEquals} from "@chainsafe/ssz"; import { EFFECTIVE_BALANCE_INCREMENT, ForkSeq, @@ -15,7 +14,7 @@ import { WEIGHT_DENOMINATOR, } from "@lodestar/params"; import {Attestation, Epoch, phase0} from "@lodestar/types"; -import {intSqrt} from "@lodestar/utils"; +import {byteArrayEquals, intSqrt} from "@lodestar/utils"; import {BeaconStateTransitionMetrics} from "../metrics.js"; import {getAttestationWithIndicesSignatureSet} from "../signatureSets/indexedAttestation.js"; import {CachedBeaconStateAltair, CachedBeaconStateGloas} from "../types.js"; @@ -64,14 +63,8 @@ export function processAttestationsAltair( // TODO: Why should we verify an indexed attestation that we just created? If it's just for the signature // we can verify only that and nothing else. if (verifySignature) { - const sigSet = getAttestationWithIndicesSignatureSet( - state.config, - epochCtx.index2pubkey, - state.slot, - attestation, - attestingIndices - ); - if (!verifySignatureSet(sigSet)) { + const sigSet = getAttestationWithIndicesSignatureSet(state.config, state.slot, attestation, attestingIndices); + if (!verifySignatureSet(sigSet, state.epochCtx.index2pubkey)) { throw new Error("Attestation signature is not valid"); } } diff --git a/packages/state-transition/src/block/processAttesterSlashing.ts b/packages/state-transition/src/block/processAttesterSlashing.ts index 91973d8ebb5f..c8739ae506ae 100644 --- a/packages/state-transition/src/block/processAttesterSlashing.ts +++ b/packages/state-transition/src/block/processAttesterSlashing.ts @@ -1,5 +1,6 @@ +import {BeaconConfig} from "@lodestar/config"; import {ForkSeq} from "@lodestar/params"; -import {AttesterSlashing} from "@lodestar/types"; +import {AttesterSlashing, Slot} from "@lodestar/types"; import {Index2PubkeyCache} from "../cache/pubkeyCache.js"; import {CachedBeaconStateAllForks} from "../types.js"; import {getAttesterSlashableIndices, isSlashableAttestationData, isSlashableValidator} from "../util/index.js"; @@ -19,7 +20,14 @@ export function processAttesterSlashing( verifySignatures = true ): void { const {epochCtx} = state; - assertValidAttesterSlashing(epochCtx.index2pubkey, state, attesterSlashing, verifySignatures); + assertValidAttesterSlashing( + state.config, + epochCtx.index2pubkey, + state.slot, + state.validators.length, + attesterSlashing, + verifySignatures + ); const intersectingIndices = getAttesterSlashableIndices(attesterSlashing); @@ -39,8 +47,10 @@ export function processAttesterSlashing( } export function assertValidAttesterSlashing( + config: BeaconConfig, index2pubkey: Index2PubkeyCache, - state: CachedBeaconStateAllForks, + stateSlot: Slot, + validatorsLen: number, attesterSlashing: AttesterSlashing, verifySignatures = true ): void { @@ -55,7 +65,9 @@ export function assertValidAttesterSlashing( // be higher than the clock and the slashing would still be valid. Same applies to attestation data index, which // can be any arbitrary value. Must use bigint variants to hash correctly to all possible values for (const [i, attestation] of [attestation1, attestation2].entries()) { - if (!isValidIndexedAttestationBigint(state.config, index2pubkey, state, attestation, verifySignatures)) { + if ( + !isValidIndexedAttestationBigint(config, index2pubkey, stateSlot, validatorsLen, attestation, verifySignatures) + ) { throw new Error(`AttesterSlashing attestation${i} is invalid`); } } diff --git a/packages/state-transition/src/block/processBlockHeader.ts b/packages/state-transition/src/block/processBlockHeader.ts index b4e07ffd5f73..aa01852a9c54 100644 --- a/packages/state-transition/src/block/processBlockHeader.ts +++ b/packages/state-transition/src/block/processBlockHeader.ts @@ -1,6 +1,5 @@ -import {byteArrayEquals} from "@chainsafe/ssz"; import {BeaconBlock, BlindedBeaconBlock, ssz} from "@lodestar/types"; -import {toRootHex} from "@lodestar/utils"; +import {byteArrayEquals, toRootHex} from "@lodestar/utils"; import {ZERO_HASH} from "../constants/index.js"; import {CachedBeaconStateAllForks} from "../types.js"; import {blindedOrFullBlockToHeader} from "../util/index.js"; diff --git a/packages/state-transition/src/block/processBlsToExecutionChange.ts b/packages/state-transition/src/block/processBlsToExecutionChange.ts index a8ff1ccdf3ba..979156ddd26f 100644 --- a/packages/state-transition/src/block/processBlsToExecutionChange.ts +++ b/packages/state-transition/src/block/processBlsToExecutionChange.ts @@ -1,8 +1,9 @@ import {digest} from "@chainsafe/as-sha256"; -import {byteArrayEquals} from "@chainsafe/ssz"; +import {BeaconConfig} from "@lodestar/config"; import {BLS_WITHDRAWAL_PREFIX, ETH1_ADDRESS_WITHDRAWAL_PREFIX} from "@lodestar/params"; import {capella} from "@lodestar/types"; -import {toHex} from "@lodestar/utils"; +import {Validator} from "@lodestar/types/phase0"; +import {byteArrayEquals, toHex} from "@lodestar/utils"; import {verifyBlsToExecutionChangeSignature} from "../signatureSets/index.js"; import {CachedBeaconStateCapella} from "../types.js"; @@ -12,12 +13,18 @@ export function processBlsToExecutionChange( ): void { const addressChange = signedBlsToExecutionChange.message; - const validation = isValidBlsToExecutionChange(state, signedBlsToExecutionChange, true); + if (addressChange.validatorIndex >= state.validators.length) { + throw Error( + `withdrawalValidatorIndex ${addressChange.validatorIndex} >= state.validators len ${state.validators.length}` + ); + } + + const validator = state.validators.get(addressChange.validatorIndex); + const validation = isValidBlsToExecutionChange(state.config, validator, signedBlsToExecutionChange, true); if (!validation.valid) { throw validation.error; } - const validator = state.validators.get(addressChange.validatorIndex); const newWithdrawalCredentials = new Uint8Array(32); newWithdrawalCredentials[0] = ETH1_ADDRESS_WITHDRAWAL_PREFIX; newWithdrawalCredentials.set(addressChange.toExecutionAddress, 12); @@ -27,22 +34,13 @@ export function processBlsToExecutionChange( } export function isValidBlsToExecutionChange( - state: CachedBeaconStateCapella, + config: BeaconConfig, + validator: Validator, signedBLSToExecutionChange: capella.SignedBLSToExecutionChange, verifySignature = true ): {valid: true} | {valid: false; error: Error} { const addressChange = signedBLSToExecutionChange.message; - if (addressChange.validatorIndex >= state.validators.length) { - return { - valid: false, - error: Error( - `withdrawalValidatorIndex ${addressChange.validatorIndex} > state.validators len ${state.validators.length}` - ), - }; - } - - const validator = state.validators.getReadonly(addressChange.validatorIndex); const {withdrawalCredentials} = validator; if (withdrawalCredentials[0] !== BLS_WITHDRAWAL_PREFIX) { return { @@ -65,7 +63,7 @@ export function isValidBlsToExecutionChange( }; } - if (verifySignature && !verifyBlsToExecutionChangeSignature(state, signedBLSToExecutionChange)) { + if (verifySignature && !verifyBlsToExecutionChangeSignature(config, signedBLSToExecutionChange)) { return { valid: false, error: Error( diff --git a/packages/state-transition/src/block/processConsolidationRequest.ts b/packages/state-transition/src/block/processConsolidationRequest.ts index 8860b41c5b7c..481c2c4d500c 100644 --- a/packages/state-transition/src/block/processConsolidationRequest.ts +++ b/packages/state-transition/src/block/processConsolidationRequest.ts @@ -1,5 +1,6 @@ -import {FAR_FUTURE_EPOCH, ForkSeq, MIN_ACTIVATION_BALANCE, PENDING_CONSOLIDATIONS_LIMIT} from "@lodestar/params"; +import {FAR_FUTURE_EPOCH, MIN_ACTIVATION_BALANCE, PENDING_CONSOLIDATIONS_LIMIT} from "@lodestar/params"; import {electra, ssz} from "@lodestar/types"; +import {byteArrayEquals} from "@lodestar/utils"; import {CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js"; import {hasEth1WithdrawalCredential} from "../util/capella.js"; import { @@ -13,7 +14,6 @@ import {getConsolidationChurnLimit, getPendingBalanceToWithdraw, isActiveValidat // TODO Electra: Clean up necessary as there is a lot of overlap with isValidSwitchToCompoundRequest export function processConsolidationRequest( - fork: ForkSeq, state: CachedBeaconStateElectra | CachedBeaconStateGloas, consolidationRequest: electra.ConsolidationRequest ): void { @@ -57,7 +57,7 @@ export function processConsolidationRequest( // Verify source withdrawal credentials const hasCorrectCredential = hasExecutionWithdrawalCredential(sourceValidator.withdrawalCredentials); - const isCorrectSourceAddress = Buffer.compare(sourceWithdrawalAddress, sourceAddress) === 0; + const isCorrectSourceAddress = byteArrayEquals(sourceWithdrawalAddress, sourceAddress); if (!(hasCorrectCredential && isCorrectSourceAddress)) { return; } @@ -83,7 +83,7 @@ export function processConsolidationRequest( } // Verify the source has no pending withdrawals in the queue - if (getPendingBalanceToWithdraw(fork, state, sourceIndex) > 0) { + if (getPendingBalanceToWithdraw(state, sourceIndex) > 0) { return; } @@ -125,7 +125,7 @@ function isValidSwitchToCompoundRequest( const sourceValidator = state.validators.getReadonly(sourceIndex); const sourceWithdrawalAddress = sourceValidator.withdrawalCredentials.subarray(12); // Verify request has been authorized - if (Buffer.compare(sourceWithdrawalAddress, sourceAddress) !== 0) { + if (!byteArrayEquals(sourceWithdrawalAddress, sourceAddress)) { return false; } diff --git a/packages/state-transition/src/block/processDepositRequest.ts b/packages/state-transition/src/block/processDepositRequest.ts index 96172eec86f6..b322fe223675 100644 --- a/packages/state-transition/src/block/processDepositRequest.ts +++ b/packages/state-transition/src/block/processDepositRequest.ts @@ -1,21 +1,114 @@ -import {UNSET_DEPOSIT_REQUESTS_START_INDEX} from "@lodestar/params"; -import {electra, ssz} from "@lodestar/types"; +import {FAR_FUTURE_EPOCH, ForkSeq, UNSET_DEPOSIT_REQUESTS_START_INDEX} from "@lodestar/params"; +import {BLSPubkey, Bytes32, UintNum64, electra, ssz} from "@lodestar/types"; import {CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js"; +import {findBuilderIndexByPubkey, isBuilderWithdrawalCredential} from "../util/gloas.js"; +import {computeEpochAtSlot, isValidatorKnown} from "../util/index.js"; +import {isValidDepositSignature} from "./processDeposit.js"; + +/** + * Apply a deposit for a builder. Either increases balance for existing builder or adds new builder to registry. + * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-apply_deposit_for_builder + */ +export function applyDepositForBuilder( + state: CachedBeaconStateGloas, + pubkey: BLSPubkey, + withdrawalCredentials: Bytes32, + amount: UintNum64, + signature: Bytes32 +): void { + const builderIndex = findBuilderIndexByPubkey(state, pubkey); + + if (builderIndex !== null) { + // Existing builder - increase balance + const builder = state.builders.get(builderIndex); + builder.balance += amount; + } else { + // New builder - verify signature and add to registry + if (isValidDepositSignature(state.config, pubkey, withdrawalCredentials, amount, signature)) { + addBuilderToRegistry(state, pubkey, withdrawalCredentials, amount); + } + } +} + +/** + * Add a new builder to the builders registry. + * Reuses slots from exited and fully withdrawn builders if available. + */ +function addBuilderToRegistry( + state: CachedBeaconStateGloas, + pubkey: BLSPubkey, + withdrawalCredentials: Bytes32, + amount: UintNum64 +): void { + const currentEpoch = computeEpochAtSlot(state.slot); + + // Try to find a reusable slot from an exited builder with zero balance + let builderIndex = state.builders.length; + for (let i = 0; i < state.builders.length; i++) { + const builder = state.builders.getReadonly(i); + if (builder.withdrawableEpoch <= currentEpoch && builder.balance === 0) { + builderIndex = i; + break; + } + } + + // Create new builder + const newBuilder = ssz.gloas.Builder.toViewDU({ + pubkey, + version: withdrawalCredentials[0], + executionAddress: withdrawalCredentials.subarray(12), + balance: amount, + depositEpoch: currentEpoch, + withdrawableEpoch: FAR_FUTURE_EPOCH, + }); + + if (builderIndex < state.builders.length) { + // Reuse existing slot + state.builders.set(builderIndex, newBuilder); + } else { + // Append to end + state.builders.push(newBuilder); + } +} export function processDepositRequest( + fork: ForkSeq, state: CachedBeaconStateElectra | CachedBeaconStateGloas, depositRequest: electra.DepositRequest ): void { - if (state.depositRequestsStartIndex === UNSET_DEPOSIT_REQUESTS_START_INDEX) { + const {pubkey, withdrawalCredentials, amount, signature} = depositRequest; + + // Check if this is a builder or validator deposit + if (fork >= ForkSeq.gloas) { + const stateGloas = state as CachedBeaconStateGloas; + const builderIndex = findBuilderIndexByPubkey(stateGloas, pubkey); + const validatorIndex = state.epochCtx.getValidatorIndex(pubkey); + + // Regardless of the withdrawal credentials prefix, if a builder/validator + // already exists with this pubkey, apply the deposit to their balance + const isBuilder = builderIndex !== null; + const isValidator = isValidatorKnown(state, validatorIndex); + const isBuilderPrefix = isBuilderWithdrawalCredential(withdrawalCredentials); + + // Route to builder if it's an existing builder OR has builder prefix and is not a validator + if (isBuilder || (isBuilderPrefix && !isValidator)) { + // Apply builder deposits immediately + applyDepositForBuilder(stateGloas, pubkey, withdrawalCredentials, amount, signature); + return; + } + } + + // Only set deposit_requests_start_index in Electra fork, not Gloas + if (fork < ForkSeq.gloas && state.depositRequestsStartIndex === UNSET_DEPOSIT_REQUESTS_START_INDEX) { state.depositRequestsStartIndex = depositRequest.index; } - // Create pending deposit + // Add validator deposits to the queue const pendingDeposit = ssz.electra.PendingDeposit.toViewDU({ - pubkey: depositRequest.pubkey, - withdrawalCredentials: depositRequest.withdrawalCredentials, - amount: depositRequest.amount, - signature: depositRequest.signature, + pubkey, + withdrawalCredentials, + amount, + signature, slot: state.slot, }); state.pendingDeposits.push(pendingDeposit); diff --git a/packages/state-transition/src/block/processExecutionPayload.ts b/packages/state-transition/src/block/processExecutionPayload.ts index 0af784074cdb..604c135b89c8 100644 --- a/packages/state-transition/src/block/processExecutionPayload.ts +++ b/packages/state-transition/src/block/processExecutionPayload.ts @@ -1,7 +1,6 @@ -import {byteArrayEquals} from "@chainsafe/ssz"; import {ForkName, ForkSeq, isForkPostDeneb} from "@lodestar/params"; import {BeaconBlockBody, BlindedBeaconBlockBody, deneb, isExecutionPayload} from "@lodestar/types"; -import {toHex, toRootHex} from "@lodestar/utils"; +import {byteArrayEquals, toHex, toRootHex} from "@lodestar/utils"; import {CachedBeaconStateBellatrix, CachedBeaconStateCapella} from "../types.js"; import { executionPayloadToPayloadHeader, diff --git a/packages/state-transition/src/block/processExecutionPayloadBid.ts b/packages/state-transition/src/block/processExecutionPayloadBid.ts index a4bcbf23af87..84e849d14567 100644 --- a/packages/state-transition/src/block/processExecutionPayloadBid.ts +++ b/packages/state-transition/src/block/processExecutionPayloadBid.ts @@ -1,68 +1,47 @@ import {PublicKey, Signature, verify} from "@chainsafe/blst"; -import {byteArrayEquals} from "@chainsafe/ssz"; -import { - DOMAIN_BEACON_BUILDER, - FAR_FUTURE_EPOCH, - ForkPostGloas, - MIN_ACTIVATION_BALANCE, - SLOTS_PER_EPOCH, -} from "@lodestar/params"; +import {BUILDER_INDEX_SELF_BUILD, ForkPostGloas, SLOTS_PER_EPOCH} from "@lodestar/params"; import {BeaconBlock, gloas, ssz} from "@lodestar/types"; -import {toHex, toRootHex} from "@lodestar/utils"; +import {byteArrayEquals, toHex, toRootHex} from "@lodestar/utils"; import {G2_POINT_AT_INFINITY} from "../constants/constants.ts"; +import {getExecutionPayloadBidSigningRoot} from "../signatureSets/executionPayloadBid.js"; import {CachedBeaconStateGloas} from "../types.ts"; -import {hasBuilderWithdrawalCredential} from "../util/gloas.ts"; -import {computeSigningRoot, getCurrentEpoch, getRandaoMix, isActiveValidator} from "../util/index.ts"; +import {canBuilderCoverBid, isActiveBuilder} from "../util/gloas.ts"; +import {getCurrentEpoch, getRandaoMix} from "../util/index.ts"; export function processExecutionPayloadBid(state: CachedBeaconStateGloas, block: BeaconBlock): void { const signedBid = block.body.signedExecutionPayloadBid; const bid = signedBid.message; const {builderIndex, value: amount} = bid; - const builder = state.validators.getReadonly(builderIndex); // For self-builds, amount must be zero regardless of withdrawal credential prefix - if (builderIndex === block.proposerIndex) { + if (builderIndex === BUILDER_INDEX_SELF_BUILD) { if (amount !== 0) { throw Error(`Invalid execution payload bid: self-build with non-zero amount ${amount}`); } if (!byteArrayEquals(signedBid.signature, G2_POINT_AT_INFINITY)) { throw Error("Invalid execution payload bid: self-build with non-zero signature"); } - // Non-self builds require builder withdrawal credential - } else { - if (!hasBuilderWithdrawalCredential(builder.withdrawalCredentials)) { - throw Error(`Invalid execution payload bid: builder ${builderIndex} does not have builder withdrawal credential`); + } + // Non-self builds require active builder with valid signature + else { + const builder = state.builders.getReadonly(builderIndex); + + // Verify that the builder is active + if (!isActiveBuilder(builder, state.finalizedCheckpoint.epoch)) { + throw Error(`Invalid execution payload bid: builder ${builderIndex} is not active`); + } + + // Verify that the builder has funds to cover the bid + if (!canBuilderCoverBid(state, builderIndex, amount)) { + throw Error(`Invalid execution payload bid: builder ${builderIndex} has insufficient balance`); } + // Verify that the bid signature is valid if (!verifyExecutionPayloadBidSignature(state, builder.pubkey, signedBid)) { throw Error(`Invalid execution payload bid: invalid signature for builder ${builderIndex}`); } } - if (!isActiveValidator(builder, getCurrentEpoch(state))) { - throw Error(`Invalid execution payload bid: builder ${builderIndex} is not active`); - } - - if (builder.slashed) { - throw Error(`Invalid execution payload bid: builder ${builderIndex} is slashed`); - } - - const pendingPayments = state.builderPendingPayments - .getAllReadonly() - .filter((payment) => payment.withdrawal.builderIndex === builderIndex) - .reduce((acc, payment) => acc + payment.withdrawal.amount, 0); - const pendingWithdrawals = state.builderPendingWithdrawals - .getAllReadonly() - .filter((withdrawal) => withdrawal.builderIndex === builderIndex) - .reduce((acc, withdrawal) => acc + withdrawal.amount, 0); - - if ( - amount !== 0 && - state.balances.get(builderIndex) < amount + pendingPayments + pendingWithdrawals + MIN_ACTIVATION_BALANCE - ) { - throw Error("Insufficient builder balance"); - } - if (bid.slot !== block.slot) { throw Error(`Bid slot ${bid.slot} does not match block slot ${block.slot}`); } @@ -91,7 +70,6 @@ export function processExecutionPayloadBid(state: CachedBeaconStateGloas, block: feeRecipient: bid.feeRecipient, amount, builderIndex, - withdrawableEpoch: FAR_FUTURE_EPOCH, }), }); @@ -106,8 +84,7 @@ function verifyExecutionPayloadBidSignature( pubkey: Uint8Array, signedBid: gloas.SignedExecutionPayloadBid ): boolean { - const domain = state.config.getDomain(state.slot, DOMAIN_BEACON_BUILDER); - const signingRoot = computeSigningRoot(ssz.gloas.ExecutionPayloadBid, signedBid.message, domain); + const signingRoot = getExecutionPayloadBidSigningRoot(state.config, state.slot, signedBid.message); try { const publicKey = PublicKey.fromBytes(pubkey); diff --git a/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts b/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts index c9ef937af362..55b6069b01cb 100644 --- a/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts +++ b/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts @@ -1,10 +1,14 @@ import {PublicKey, Signature, verify} from "@chainsafe/blst"; -import {byteArrayEquals} from "@chainsafe/ssz"; -import {DOMAIN_BEACON_BUILDER, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; +import { + BUILDER_INDEX_SELF_BUILD, + DOMAIN_BEACON_BUILDER, + SLOTS_PER_EPOCH, + SLOTS_PER_HISTORICAL_ROOT, +} from "@lodestar/params"; import {gloas, ssz} from "@lodestar/types"; -import {toHex, toRootHex} from "@lodestar/utils"; +import {byteArrayEquals, toHex, toRootHex} from "@lodestar/utils"; import {CachedBeaconStateGloas} from "../types.ts"; -import {computeExitEpochAndUpdateChurn, computeSigningRoot, computeTimeAtSlot} from "../util/index.ts"; +import {computeSigningRoot, computeTimeAtSlot} from "../util/index.ts"; import {processConsolidationRequest} from "./processConsolidationRequest.ts"; import {processDepositRequest} from "./processDepositRequest.ts"; import {processWithdrawalRequest} from "./processWithdrawalRequest.ts"; @@ -19,13 +23,8 @@ export function processExecutionPayloadEnvelope( const payload = envelope.payload; const fork = state.config.getForkSeq(envelope.slot); - if (verify) { - const builderIndex = envelope.builderIndex; - const pubkey = state.validators.getReadonly(builderIndex).pubkey; - - if (!verifyExecutionPayloadEnvelopeSignature(state, pubkey, signedEnvelope)) { - throw new Error("Payload Envelope has invalid signature"); - } + if (verify && !verifyExecutionPayloadEnvelopeSignature(state, signedEnvelope)) { + throw Error(`Execution payload envelope has invalid signature builderIndex=${envelope.builderIndex}`); } validateExecutionPayloadEnvelope(state, envelope); @@ -33,7 +32,7 @@ export function processExecutionPayloadEnvelope( const requests = envelope.executionRequests; for (const deposit of requests.deposits) { - processDepositRequest(state, deposit); + processDepositRequest(fork, state, deposit); } for (const withdrawal of requests.withdrawals) { @@ -41,7 +40,7 @@ export function processExecutionPayloadEnvelope( } for (const consolidation of requests.consolidations) { - processConsolidationRequest(fork, state, consolidation); + processConsolidationRequest(state, consolidation); } // Queue the builder payment @@ -50,9 +49,6 @@ export function processExecutionPayloadEnvelope( const amount = payment.withdrawal.amount; if (amount > 0) { - const exitQueueEpoch = computeExitEpochAndUpdateChurn(state, BigInt(amount)); - - payment.withdrawal.withdrawableEpoch = exitQueueEpoch + state.config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY; state.builderPendingWithdrawals.push(payment.withdrawal); } @@ -75,6 +71,7 @@ function validateExecutionPayloadEnvelope( ): void { const payload = envelope.payload; + // Cache latest block header state root if (byteArrayEquals(state.latestBlockHeader.stateRoot, ssz.Root.defaultValue())) { const previousStateRoot = state.hashTreeRoot(); state.latestBlockHeader.stateRoot = previousStateRoot; @@ -87,20 +84,18 @@ function validateExecutionPayloadEnvelope( ); } - // Verify consistency with the beacon block if (envelope.slot !== state.slot) { throw new Error(`Slot mismatch between envelope and state envelope=${envelope.slot} state=${state.slot}`); } - const committedBid = state.latestExecutionPayloadBid; // Verify consistency with the committed bid + const committedBid = state.latestExecutionPayloadBid; if (envelope.builderIndex !== committedBid.builderIndex) { throw new Error( `Builder index mismatch between envelope and committed bid envelope=${envelope.builderIndex} committedBid=${committedBid.builderIndex}` ); } - // Verify consistency with the committed bid const envelopeKzgRoot = ssz.deneb.BlobKzgCommitments.hashTreeRoot(envelope.blobKzgCommitments); if (!byteArrayEquals(committedBid.blobKzgCommitmentsRoot, envelopeKzgRoot)) { throw new Error( @@ -108,11 +103,18 @@ function validateExecutionPayloadEnvelope( ); } - // Verify the withdrawals root - const envelopeWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(envelope.payload.withdrawals); - if (!byteArrayEquals(state.latestWithdrawalsRoot, envelopeWithdrawalsRoot)) { + if (!byteArrayEquals(committedBid.prevRandao, payload.prevRandao)) { + throw new Error( + `Prev randao mismatch between committed bid and payload committedBid=${toHex(committedBid.prevRandao)} payload=${toHex(payload.prevRandao)}` + ); + } + + // Verify consistency with expected withdrawals + const payloadWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(payload.withdrawals); + const expectedWithdrawalsRoot = state.payloadExpectedWithdrawals.hashTreeRoot(); + if (!byteArrayEquals(payloadWithdrawalsRoot, expectedWithdrawalsRoot)) { throw new Error( - `Withdrawals root mismatch between envelope and latest withdrawals root envelope=${toRootHex(envelopeWithdrawalsRoot)} latestWithdrawalRoot=${toRootHex(state.latestWithdrawalsRoot)}` + `Withdrawals mismatch between payload and expected withdrawals payload=${toRootHex(payloadWithdrawalsRoot)} expected=${toRootHex(expectedWithdrawalsRoot)}` ); } @@ -137,13 +139,6 @@ function validateExecutionPayloadEnvelope( ); } - // Verify prev_randao matches committed bid - if (!byteArrayEquals(committedBid.prevRandao, payload.prevRandao)) { - throw new Error( - `Prev randao mismatch between committed bid and payload committedBid=${toHex(committedBid.prevRandao)} payload=${toHex(payload.prevRandao)}` - ); - } - // Verify timestamp if (payload.timestamp !== computeTimeAtSlot(state.config, state.slot, state.genesisTime)) { throw new Error( @@ -164,14 +159,22 @@ function validateExecutionPayloadEnvelope( function verifyExecutionPayloadEnvelopeSignature( state: CachedBeaconStateGloas, - pubkey: Uint8Array, signedEnvelope: gloas.SignedExecutionPayloadEnvelope ): boolean { + const builderIndex = signedEnvelope.message.builderIndex; + const domain = state.config.getDomain(state.slot, DOMAIN_BEACON_BUILDER); const signingRoot = computeSigningRoot(ssz.gloas.ExecutionPayloadEnvelope, signedEnvelope.message, domain); try { - const publicKey = PublicKey.fromBytes(pubkey); + let publicKey: PublicKey; + + if (builderIndex === BUILDER_INDEX_SELF_BUILD) { + const validatorIndex = state.latestBlockHeader.proposerIndex; + publicKey = state.epochCtx.index2pubkey[validatorIndex]; + } else { + publicKey = PublicKey.fromBytes(state.builders.getReadonly(builderIndex).pubkey); + } const signature = Signature.fromBytes(signedEnvelope.signature, true); return verify(signingRoot, publicKey, signature); diff --git a/packages/state-transition/src/block/processOperations.ts b/packages/state-transition/src/block/processOperations.ts index 3309111044ee..34b21984c124 100644 --- a/packages/state-transition/src/block/processOperations.ts +++ b/packages/state-transition/src/block/processOperations.ts @@ -75,7 +75,7 @@ export function processOperations( const bodyElectra = body as electra.BeaconBlockBody; for (const depositRequest of bodyElectra.executionRequests.deposits) { - processDepositRequest(stateElectra, depositRequest); + processDepositRequest(fork, stateElectra, depositRequest); } for (const elWithdrawalRequest of bodyElectra.executionRequests.withdrawals) { @@ -83,7 +83,7 @@ export function processOperations( } for (const elConsolidationRequest of bodyElectra.executionRequests.consolidations) { - processConsolidationRequest(fork, stateElectra, elConsolidationRequest); + processConsolidationRequest(stateElectra, elConsolidationRequest); } } diff --git a/packages/state-transition/src/block/processPayloadAttestation.ts b/packages/state-transition/src/block/processPayloadAttestation.ts index de6c5980bbb1..d58cf704dc00 100644 --- a/packages/state-transition/src/block/processPayloadAttestation.ts +++ b/packages/state-transition/src/block/processPayloadAttestation.ts @@ -1,5 +1,5 @@ -import {byteArrayEquals} from "@chainsafe/ssz"; import {gloas} from "@lodestar/types"; +import {byteArrayEquals} from "@lodestar/utils"; import {CachedBeaconStateGloas} from "../types.ts"; import {isValidIndexedPayloadAttestation} from "./isValidIndexedPayloadAttestation.ts"; diff --git a/packages/state-transition/src/block/processProposerSlashing.ts b/packages/state-transition/src/block/processProposerSlashing.ts index aec8047c753f..f66269666678 100644 --- a/packages/state-transition/src/block/processProposerSlashing.ts +++ b/packages/state-transition/src/block/processProposerSlashing.ts @@ -1,5 +1,8 @@ +import {BeaconConfig} from "@lodestar/config"; import {ForkSeq, SLOTS_PER_EPOCH} from "@lodestar/params"; -import {phase0, ssz} from "@lodestar/types"; +import {Slot, phase0, ssz} from "@lodestar/types"; +import {Validator} from "@lodestar/types/phase0"; +import {Index2PubkeyCache} from "../cache/pubkeyCache.js"; import {getProposerSlashingSignatureSets} from "../signatureSets/index.js"; import {CachedBeaconStateAllForks, CachedBeaconStateGloas} from "../types.js"; import {computeEpochAtSlot, isSlashableValidator} from "../util/index.js"; @@ -18,7 +21,15 @@ export function processProposerSlashing( proposerSlashing: phase0.ProposerSlashing, verifySignatures = true ): void { - assertValidProposerSlashing(state, proposerSlashing, verifySignatures); + const proposer = state.validators.getReadonly(proposerSlashing.signedHeader1.message.proposerIndex); + assertValidProposerSlashing( + state.config, + state.epochCtx.index2pubkey, + state.slot, + proposerSlashing, + proposer, + verifySignatures + ); if (fork >= ForkSeq.gloas) { const slot = Number(proposerSlashing.signedHeader1.message.slot); @@ -45,8 +56,11 @@ export function processProposerSlashing( } export function assertValidProposerSlashing( - state: CachedBeaconStateAllForks, + config: BeaconConfig, + index2pubkey: Index2PubkeyCache, + stateSlot: Slot, proposerSlashing: phase0.ProposerSlashing, + proposer: Validator, verifySignatures = true ): void { const header1 = proposerSlashing.signedHeader1.message; @@ -70,21 +84,17 @@ export function assertValidProposerSlashing( } // verify the proposer is slashable - const proposer = state.validators.getReadonly(header1.proposerIndex); - if (!isSlashableValidator(proposer, state.epochCtx.epoch)) { + // ideally we would get the proposer from state.validators using proposerIndex but that requires access to state + // instead of that we pass in the proposer directly from the consumer side + if (!isSlashableValidator(proposer, computeEpochAtSlot(stateSlot))) { throw new Error("ProposerSlashing proposer is not slashable"); } // verify signatures if (verifySignatures) { - const signatureSets = getProposerSlashingSignatureSets( - state.config, - state.epochCtx.index2pubkey, - state.slot, - proposerSlashing - ); + const signatureSets = getProposerSlashingSignatureSets(config, stateSlot, proposerSlashing); for (let i = 0; i < signatureSets.length; i++) { - if (!verifySignatureSet(signatureSets[i])) { + if (!verifySignatureSet(signatureSets[i], index2pubkey)) { throw new Error(`ProposerSlashing header${i + 1} signature invalid`); } } diff --git a/packages/state-transition/src/block/processSyncCommittee.ts b/packages/state-transition/src/block/processSyncCommittee.ts index ec7cae676b95..243c9935a575 100644 --- a/packages/state-transition/src/block/processSyncCommittee.ts +++ b/packages/state-transition/src/block/processSyncCommittee.ts @@ -1,8 +1,7 @@ -import {byteArrayEquals} from "@chainsafe/ssz"; import {BeaconConfig} from "@lodestar/config"; import {DOMAIN_SYNC_COMMITTEE, SYNC_COMMITTEE_SIZE} from "@lodestar/params"; import {altair, ssz} from "@lodestar/types"; -import {Index2PubkeyCache} from "../cache/pubkeyCache.js"; +import {byteArrayEquals} from "@lodestar/utils"; import {SyncCommitteeCache} from "../cache/syncCommitteeCache.js"; import {G2_POINT_AT_INFINITY} from "../constants/index.js"; import {CachedBeaconStateAllForks} from "../types.js"; @@ -28,13 +27,12 @@ export function processSyncAggregate( const participantIndices = block.body.syncAggregate.syncCommitteeBits.intersectValues(committeeIndices); const signatureSet = getSyncCommitteeSignatureSet( state.config, - state.epochCtx.index2pubkey, state.epochCtx.currentSyncCommitteeIndexed, block, participantIndices ); - // When there's no participation we consider the signature valid and just ignore i - if (signatureSet !== null && !verifySignatureSet(signatureSet)) { + // When there's no participation we consider the signature valid and just ignore it + if (signatureSet !== null && !verifySignatureSet(signatureSet, state.epochCtx.index2pubkey)) { throw Error("Sync committee signature invalid"); } } @@ -73,7 +71,6 @@ export function processSyncAggregate( export function getSyncCommitteeSignatureSet( config: BeaconConfig, - index2pubkey: Index2PubkeyCache, currentSyncCommitteeIndexed: SyncCommitteeCache, block: altair.BeaconBlock, /** Optional parameter to prevent computing it twice */ @@ -122,7 +119,7 @@ export function getSyncCommitteeSignatureSet( return { type: SignatureSetType.aggregate, - pubkeys: participantIndices.map((i) => index2pubkey[i]), + indices: participantIndices, signingRoot: computeSigningRoot(ssz.Root, rootSigned, domain), signature, }; diff --git a/packages/state-transition/src/block/processVoluntaryExit.ts b/packages/state-transition/src/block/processVoluntaryExit.ts index 2a5f103c29bf..46141a96180c 100644 --- a/packages/state-transition/src/block/processVoluntaryExit.ts +++ b/packages/state-transition/src/block/processVoluntaryExit.ts @@ -1,8 +1,16 @@ +import {PublicKey, Signature, verify} from "@chainsafe/blst"; import {FAR_FUTURE_EPOCH, ForkSeq} from "@lodestar/params"; -import {phase0} from "@lodestar/types"; +import {phase0, ssz} from "@lodestar/types"; import {verifyVoluntaryExitSignature} from "../signatureSets/index.js"; -import {CachedBeaconStateAllForks, CachedBeaconStateElectra} from "../types.js"; -import {getPendingBalanceToWithdraw, isActiveValidator} from "../util/index.js"; +import {CachedBeaconStateAllForks, CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js"; +import { + convertValidatorIndexToBuilderIndex, + getPendingBalanceToWithdrawForBuilder, + initiateBuilderExit, + isActiveBuilder, + isBuilderIndex, +} from "../util/gloas.js"; +import {computeSigningRoot, getCurrentEpoch, getPendingBalanceToWithdraw, isActiveValidator} from "../util/index.js"; import {initiateValidatorExit} from "./index.js"; export enum VoluntaryExitValidity { @@ -16,7 +24,7 @@ export enum VoluntaryExitValidity { } /** - * Process a VoluntaryExit operation. Initiates the exit of a validator. + * Process a VoluntaryExit operation. Initiates the exit of a validator or builder. * * PERF: Work depends on number of VoluntaryExit per block. On regular networks the average is 0 / block. */ @@ -26,6 +34,53 @@ export function processVoluntaryExit( signedVoluntaryExit: phase0.SignedVoluntaryExit, verifySignature = true ): void { + const voluntaryExit = signedVoluntaryExit.message; + const currentEpoch = getCurrentEpoch(state); + + // Exits must specify an epoch when they become valid; they are not valid before then + if (currentEpoch < voluntaryExit.epoch) { + throw Error(`Voluntary exit epoch ${voluntaryExit.epoch} is after current epoch ${currentEpoch}`); + } + + // Check if this is a builder exit + if (fork >= ForkSeq.gloas && isBuilderIndex(voluntaryExit.validatorIndex)) { + const stateGloas = state as CachedBeaconStateGloas; + const builderIndex = convertValidatorIndexToBuilderIndex(voluntaryExit.validatorIndex); + const builder = stateGloas.builders.getReadonly(builderIndex); + + // Verify the builder is active + if (!isActiveBuilder(builder, state.finalizedCheckpoint.epoch)) { + throw Error(`Builder ${builderIndex} is not active`); + } + + // Only exit builder if it has no pending withdrawals in the queue + if (getPendingBalanceToWithdrawForBuilder(stateGloas, builderIndex) !== 0) { + throw Error(`Builder ${builderIndex} has pending withdrawals`); + } + + // Verify signature + if (verifySignature) { + const domain = state.config.getDomainForVoluntaryExit(state.slot); + const signingRoot = computeSigningRoot(ssz.phase0.VoluntaryExit, voluntaryExit, domain); + + try { + const publicKey = PublicKey.fromBytes(builder.pubkey); + const signature = Signature.fromBytes(signedVoluntaryExit.signature, true); + + if (!verify(signingRoot, publicKey, signature)) { + throw Error("BLS verify failed"); + } + } catch (e) { + throw Error(`Builder ${builderIndex} invalid exit signature reason=${(e as Error).message}`); + } + } + + // Initiate builder exit + initiateBuilderExit(stateGloas, builderIndex); + return; + } + + // Handle validator exit const validity = getVoluntaryExitValidity(fork, state, signedVoluntaryExit, verifySignature); if (validity !== VoluntaryExitValidity.valid) { throw Error(`Invalid voluntary exit at forkSeq=${fork} reason=${validity}`); @@ -69,7 +124,7 @@ export function getVoluntaryExitValidity( // only exit validator if it has no pending withdrawals in the queue if ( fork >= ForkSeq.electra && - getPendingBalanceToWithdraw(fork, state as CachedBeaconStateElectra, voluntaryExit.validatorIndex) !== 0 + getPendingBalanceToWithdraw(state as CachedBeaconStateElectra, voluntaryExit.validatorIndex) !== 0 ) { return VoluntaryExitValidity.pendingWithdrawals; } diff --git a/packages/state-transition/src/block/processWithdrawalRequest.ts b/packages/state-transition/src/block/processWithdrawalRequest.ts index 435f99d05496..6b79bc3aaf2f 100644 --- a/packages/state-transition/src/block/processWithdrawalRequest.ts +++ b/packages/state-transition/src/block/processWithdrawalRequest.ts @@ -42,7 +42,7 @@ export function processWithdrawalRequest( } // TODO Electra: Consider caching pendingPartialWithdrawals - const pendingBalanceToWithdraw = getPendingBalanceToWithdraw(fork, state, validatorIndex); + const pendingBalanceToWithdraw = getPendingBalanceToWithdraw(state, validatorIndex); const validatorBalance = state.balances.get(validatorIndex); if (isFullExitRequest) { diff --git a/packages/state-transition/src/block/processWithdrawals.ts b/packages/state-transition/src/block/processWithdrawals.ts index 20f81e0ec35a..c9614d82e30d 100644 --- a/packages/state-transition/src/block/processWithdrawals.ts +++ b/packages/state-transition/src/block/processWithdrawals.ts @@ -1,23 +1,29 @@ -import {byteArrayEquals} from "@chainsafe/ssz"; import { FAR_FUTURE_EPOCH, ForkSeq, + MAX_BUILDERS_PER_WITHDRAWALS_SWEEP, MAX_EFFECTIVE_BALANCE, MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP, MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP, MAX_WITHDRAWALS_PER_PAYLOAD, MIN_ACTIVATION_BALANCE, } from "@lodestar/params"; -import {ValidatorIndex, capella, ssz} from "@lodestar/types"; -import {toRootHex} from "@lodestar/utils"; +import {BuilderIndex, ValidatorIndex, capella, ssz} from "@lodestar/types"; +import {byteArrayEquals, toRootHex} from "@lodestar/utils"; import {CachedBeaconStateCapella, CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js"; -import {isBuilderPaymentWithdrawable, isParentBlockFull} from "../util/gloas.ts"; +import { + convertBuilderIndexToValidatorIndex, + convertValidatorIndexToBuilderIndex, + isBuilderIndex, + isParentBlockFull, +} from "../util/gloas.ts"; import { decreaseBalance, getMaxEffectiveBalance, hasEth1WithdrawalCredential, hasExecutionWithdrawalCredential, isCapellaPayloadHeader, + isPartiallyWithdrawableValidator, } from "../util/index.js"; export function processWithdrawals( @@ -32,9 +38,14 @@ export function processWithdrawals( // processedBuilderWithdrawalsCount is withdrawals coming from builder payment since gloas (EIP-7732) // processedPartialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002) + // processedBuildersSweepCount is withdrawals from builder sweep since gloas (EIP-7732) // processedValidatorSweepCount is withdrawals coming from validator sweep - const {expectedWithdrawals, processedBuilderWithdrawalsCount, processedPartialWithdrawalsCount} = - getExpectedWithdrawals(fork, state); + const { + expectedWithdrawals, + processedBuilderWithdrawalsCount, + processedPartialWithdrawalsCount, + processedBuildersSweepCount, + } = getExpectedWithdrawals(fork, state); const numWithdrawals = expectedWithdrawals.length; // After gloas, withdrawals are verified later in processExecutionPayloadEnvelope @@ -78,20 +89,20 @@ export function processWithdrawals( if (fork >= ForkSeq.gloas) { const stateGloas = state as CachedBeaconStateGloas; - stateGloas.latestWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(expectedWithdrawals); - - const unprocessedWithdrawals = stateGloas.builderPendingWithdrawals - .getAllReadonly() - .slice(0, processedBuilderWithdrawalsCount) - .filter((w) => !isBuilderPaymentWithdrawable(stateGloas, w)); - const remainingWithdrawals = stateGloas.builderPendingWithdrawals - .sliceFrom(processedBuilderWithdrawalsCount) - .getAllReadonly(); - - stateGloas.builderPendingWithdrawals = ssz.gloas.BeaconState.fields.builderPendingWithdrawals.toViewDU([ - ...unprocessedWithdrawals, - ...remainingWithdrawals, - ]); + + // Store expected withdrawals for verification + stateGloas.payloadExpectedWithdrawals = ssz.capella.Withdrawals.toViewDU(expectedWithdrawals); + + // Update builder pending withdrawals queue + stateGloas.builderPendingWithdrawals = stateGloas.builderPendingWithdrawals.sliceFrom( + processedBuilderWithdrawalsCount + ); + + // Update next builder index for sweep + if (stateGloas.builders.length > 0) { + const nextIndex = stateGloas.nextWithdrawalBuilderIndex + processedBuildersSweepCount; + stateGloas.nextWithdrawalBuilderIndex = nextIndex % stateGloas.builders.length; + } } // Update the nextWithdrawalIndex // https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.0/specs/capella/beacon-chain.md#new-update_next_withdrawal_index @@ -116,10 +127,14 @@ export function processWithdrawals( function getBuilderWithdrawals( state: CachedBeaconStateGloas, withdrawalIndex: number, - balanceAfterWithdrawals: Map + priorWithdrawals: capella.Withdrawal[], + builderBalanceAfterWithdrawals: Map ): {builderWithdrawals: capella.Withdrawal[]; withdrawalIndex: number; processedCount: number} { + const withdrawalsLimit = MAX_WITHDRAWALS_PER_PAYLOAD - 1; + if (priorWithdrawals.length > withdrawalsLimit) { + throw Error(`Prior withdrawals exceed limit: ${priorWithdrawals.length} > ${withdrawalsLimit}`); + } const builderWithdrawals: capella.Withdrawal[] = []; - const epoch = state.epochCtx.epoch; const allBuilderPendingWithdrawals = state.builderPendingWithdrawals.length <= MAX_WITHDRAWALS_PER_PAYLOAD ? state.builderPendingWithdrawals.getAllReadonly() @@ -127,55 +142,103 @@ function getBuilderWithdrawals( let processedCount = 0; for (let i = 0; i < state.builderPendingWithdrawals.length; i++) { + // Check combined length against limit + const allWithdrawals = priorWithdrawals.length + builderWithdrawals.length; + if (allWithdrawals >= withdrawalsLimit) { + break; + } + const withdrawal = allBuilderPendingWithdrawals ? allBuilderPendingWithdrawals[i] : state.builderPendingWithdrawals.getReadonly(i); - if (withdrawal.withdrawableEpoch > epoch || builderWithdrawals.length === MAX_WITHDRAWALS_PER_PAYLOAD) { - break; + const builderIndex = withdrawal.builderIndex; + + // Get builder balance (from builder.balance, not state.balances) + let balance = builderBalanceAfterWithdrawals.get(builderIndex); + if (balance === undefined) { + balance = state.builders.getReadonly(builderIndex).balance; + builderBalanceAfterWithdrawals.set(builderIndex, balance); } - if (isBuilderPaymentWithdrawable(state, withdrawal)) { - const builderIndex = withdrawal.builderIndex; - const builder = state.validators.get(withdrawal.builderIndex); + // Use the withdrawal amount directly as specified in the spec + builderWithdrawals.push({ + index: withdrawalIndex, + validatorIndex: convertBuilderIndexToValidatorIndex(builderIndex), + address: withdrawal.feeRecipient, + amount: BigInt(withdrawal.amount), + }); + withdrawalIndex++; + builderBalanceAfterWithdrawals.set(builderIndex, balance - withdrawal.amount); - let balance = balanceAfterWithdrawals.get(builderIndex); - if (balance === undefined) { - balance = state.balances.get(builderIndex); - balanceAfterWithdrawals.set(builderIndex, balance); - } + processedCount++; + } - let withdrawableBalance = 0; + return {builderWithdrawals, withdrawalIndex, processedCount}; +} - if (builder.slashed) { - withdrawableBalance = balance < withdrawal.amount ? balance : withdrawal.amount; - } else if (balance > MIN_ACTIVATION_BALANCE) { - withdrawableBalance = - balance - MIN_ACTIVATION_BALANCE < withdrawal.amount ? balance - MIN_ACTIVATION_BALANCE : withdrawal.amount; - } +function getBuildersSweepWithdrawals( + state: CachedBeaconStateGloas, + withdrawalIndex: number, + numPriorWithdrawal: number, + builderBalanceAfterWithdrawals: Map +): {buildersSweepWithdrawals: capella.Withdrawal[]; withdrawalIndex: number; processedCount: number} { + const withdrawalsLimit = MAX_WITHDRAWALS_PER_PAYLOAD - 1; + if (numPriorWithdrawal > withdrawalsLimit) { + throw Error(`Prior withdrawals exceed limit: ${numPriorWithdrawal} > ${withdrawalsLimit}`); + } + const buildersSweepWithdrawals: capella.Withdrawal[] = []; + const epoch = state.epochCtx.epoch; + const builders = state.builders; - if (withdrawableBalance > 0) { - builderWithdrawals.push({ - index: withdrawalIndex, - validatorIndex: withdrawal.builderIndex, - address: withdrawal.feeRecipient, - amount: BigInt(withdrawableBalance), - }); - withdrawalIndex++; - balanceAfterWithdrawals.set(builderIndex, balance - withdrawableBalance); - } + // Return early if no builders + if (builders.length === 0) { + return {buildersSweepWithdrawals, withdrawalIndex, processedCount: 0}; + } + + const buildersLimit = Math.min(builders.length, MAX_BUILDERS_PER_WITHDRAWALS_SWEEP); + let processedCount = 0; + + for (let n = 0; n < buildersLimit; n++) { + if (buildersSweepWithdrawals.length + numPriorWithdrawal >= withdrawalsLimit) { + break; + } + + // Get next builder in turn + const builderIndex = (state.nextWithdrawalBuilderIndex + n) % builders.length; + const builder = builders.getReadonly(builderIndex); + + // Get builder balance + let balance = builderBalanceAfterWithdrawals.get(builderIndex); + if (balance === undefined) { + balance = builder.balance; + builderBalanceAfterWithdrawals.set(builderIndex, balance); + } + + // Check if builder is withdrawable and has balance + if (builder.withdrawableEpoch <= epoch && balance > 0) { + // Withdraw full balance to builder's execution address + buildersSweepWithdrawals.push({ + index: withdrawalIndex, + validatorIndex: convertBuilderIndexToValidatorIndex(builderIndex), + address: builder.executionAddress, + amount: BigInt(balance), + }); + withdrawalIndex++; + builderBalanceAfterWithdrawals.set(builderIndex, 0); } + processedCount++; } - return {builderWithdrawals, withdrawalIndex, processedCount}; + return {buildersSweepWithdrawals, withdrawalIndex, processedCount}; } function getPendingPartialWithdrawals( state: CachedBeaconStateElectra, withdrawalIndex: number, numPriorWithdrawal: number, - balanceAfterWithdrawals: Map + validatorBalanceAfterWithdrawals: Map ): {pendingPartialWithdrawals: capella.Withdrawal[]; withdrawalIndex: number; processedCount: number} { const epoch = state.epochCtx.epoch; const pendingPartialWithdrawals: capella.Withdrawal[] = []; @@ -203,17 +266,17 @@ function getPendingPartialWithdrawals( : state.pendingPartialWithdrawals.getReadonly(i); if ( withdrawal.withdrawableEpoch > epoch || - pendingPartialWithdrawals.length + numPriorWithdrawal === partialWithdrawalBound + pendingPartialWithdrawals.length + numPriorWithdrawal >= partialWithdrawalBound ) { break; } const validatorIndex = withdrawal.validatorIndex; const validator = validators.getReadonly(validatorIndex); - let balance = balanceAfterWithdrawals.get(validatorIndex); + let balance = validatorBalanceAfterWithdrawals.get(validatorIndex); if (balance === undefined) { balance = state.balances.get(validatorIndex); - balanceAfterWithdrawals.set(validatorIndex, balance); + validatorBalanceAfterWithdrawals.set(validatorIndex, balance); } if ( @@ -231,7 +294,7 @@ function getPendingPartialWithdrawals( amount: withdrawableBalance, }); withdrawalIndex++; - balanceAfterWithdrawals.set(validatorIndex, balance - Number(withdrawableBalance)); + validatorBalanceAfterWithdrawals.set(validatorIndex, balance - Number(withdrawableBalance)); } processedCount++; } @@ -244,7 +307,7 @@ function getValidatorsSweepWithdrawals( state: CachedBeaconStateCapella | CachedBeaconStateElectra | CachedBeaconStateGloas, withdrawalIndex: number, numPriorWithdrawal: number, - balanceAfterWithdrawals: Map + validatorBalanceAfterWithdrawals: Map ): {sweepWithdrawals: capella.Withdrawal[]; processedCount: number} { const sweepWithdrawals: capella.Withdrawal[] = []; const epoch = state.epochCtx.epoch; @@ -264,13 +327,13 @@ function getValidatorsSweepWithdrawals( const validatorIndex = (nextWithdrawalValidatorIndex + n) % validators.length; const validator = validators.getReadonly(validatorIndex); - let balance = balanceAfterWithdrawals.get(validatorIndex); + let balance = validatorBalanceAfterWithdrawals.get(validatorIndex); if (balance === undefined) { balance = balances.get(validatorIndex); - balanceAfterWithdrawals.set(validatorIndex, balance); + validatorBalanceAfterWithdrawals.set(validatorIndex, balance); } - const {withdrawableEpoch, withdrawalCredentials, effectiveBalance} = validator; + const {withdrawableEpoch, withdrawalCredentials} = validator; const hasWithdrawableCredentials = isPostElectra ? hasExecutionWithdrawalCredential(withdrawalCredentials) : hasEth1WithdrawalCredential(withdrawalCredentials); @@ -290,13 +353,11 @@ function getValidatorsSweepWithdrawals( amount: BigInt(balance), }); withdrawalIndex++; - balanceAfterWithdrawals.set(validatorIndex, 0); - } else if ( - effectiveBalance === (isPostElectra ? getMaxEffectiveBalance(withdrawalCredentials) : MAX_EFFECTIVE_BALANCE) && - balance > effectiveBalance - ) { + validatorBalanceAfterWithdrawals.set(validatorIndex, 0); + } else if (isPartiallyWithdrawableValidator(fork, validator, balance)) { // capella partial withdrawal - const partialAmount = balance - effectiveBalance; + const maxEffectiveBalance = isPostElectra ? getMaxEffectiveBalance(withdrawalCredentials) : MAX_EFFECTIVE_BALANCE; + const partialAmount = balance - maxEffectiveBalance; sweepWithdrawals.push({ index: withdrawalIndex, validatorIndex, @@ -304,7 +365,7 @@ function getValidatorsSweepWithdrawals( amount: BigInt(partialAmount), }); withdrawalIndex++; - balanceAfterWithdrawals.set(validatorIndex, balance - partialAmount); + validatorBalanceAfterWithdrawals.set(validatorIndex, balance - partialAmount); } processedCount++; } @@ -317,7 +378,16 @@ function applyWithdrawals( withdrawals: capella.Withdrawal[] ): void { for (const withdrawal of withdrawals) { - decreaseBalance(state, withdrawal.validatorIndex, Number(withdrawal.amount)); + if (isBuilderIndex(withdrawal.validatorIndex)) { + // Handle builder withdrawal + const builderIndex = convertValidatorIndexToBuilderIndex(withdrawal.validatorIndex); + const builder = (state as CachedBeaconStateGloas).builders.get(builderIndex); + const withdrawalAmount = Number(withdrawal.amount); + builder.balance -= Math.min(withdrawalAmount, builder.balance); + } else { + // Handle validator withdrawal + decreaseBalance(state, withdrawal.validatorIndex, Number(withdrawal.amount)); + } } } @@ -328,6 +398,7 @@ export function getExpectedWithdrawals( expectedWithdrawals: capella.Withdrawal[]; processedBuilderWithdrawalsCount: number; processedPartialWithdrawalsCount: number; + processedBuildersSweepCount: number; processedValidatorSweepCount: number; } { if (fork < ForkSeq.capella) { @@ -337,20 +408,28 @@ export function getExpectedWithdrawals( let withdrawalIndex = state.nextWithdrawalIndex; const expectedWithdrawals: capella.Withdrawal[] = []; - // Map to track balances after applying withdrawals + // Separate maps to track balances after applying withdrawals // https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.0/specs/capella/beacon-chain.md#new-get_balance_after_withdrawals - const balanceAfterWithdrawals = new Map(); + const builderBalanceAfterWithdrawals = new Map(); + const validatorBalanceAfterWithdrawals = new Map(); // partialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002) let processedPartialWithdrawalsCount = 0; // builderWithdrawalsCount is withdrawals coming from builder payments since Gloas (EIP-7732) let processedBuilderWithdrawalsCount = 0; + // buildersSweepCount is withdrawals from builder sweep since Gloas (EIP-7732) + let processedBuildersSweepCount = 0; if (fork >= ForkSeq.gloas) { const { builderWithdrawals, withdrawalIndex: newWithdrawalIndex, processedCount, - } = getBuilderWithdrawals(state as CachedBeaconStateGloas, withdrawalIndex, balanceAfterWithdrawals); + } = getBuilderWithdrawals( + state as CachedBeaconStateGloas, + withdrawalIndex, + expectedWithdrawals, + builderBalanceAfterWithdrawals + ); expectedWithdrawals.push(...builderWithdrawals); withdrawalIndex = newWithdrawalIndex; @@ -366,7 +445,7 @@ export function getExpectedWithdrawals( state as CachedBeaconStateElectra, withdrawalIndex, expectedWithdrawals.length, - balanceAfterWithdrawals + validatorBalanceAfterWithdrawals ); expectedWithdrawals.push(...pendingPartialWithdrawals); @@ -374,12 +453,29 @@ export function getExpectedWithdrawals( processedPartialWithdrawalsCount = processedCount; } + if (fork >= ForkSeq.gloas) { + const { + buildersSweepWithdrawals, + withdrawalIndex: newWithdrawalIndex, + processedCount, + } = getBuildersSweepWithdrawals( + state as CachedBeaconStateGloas, + withdrawalIndex, + expectedWithdrawals.length, + builderBalanceAfterWithdrawals + ); + + expectedWithdrawals.push(...buildersSweepWithdrawals); + withdrawalIndex = newWithdrawalIndex; + processedBuildersSweepCount = processedCount; + } + const {sweepWithdrawals, processedCount: processedValidatorSweepCount} = getValidatorsSweepWithdrawals( fork, state, withdrawalIndex, expectedWithdrawals.length, - balanceAfterWithdrawals + validatorBalanceAfterWithdrawals ); expectedWithdrawals.push(...sweepWithdrawals); @@ -388,6 +484,7 @@ export function getExpectedWithdrawals( expectedWithdrawals, processedBuilderWithdrawalsCount, processedPartialWithdrawalsCount, + processedBuildersSweepCount, processedValidatorSweepCount, }; } diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index f47cd6acba4a..f02ece778433 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -23,16 +23,13 @@ import { SubnetID, SyncPeriod, ValidatorIndex, - electra, gloas, - phase0, } from "@lodestar/types"; import {LodestarError} from "@lodestar/utils"; import {getTotalSlashingsByIncrement} from "../epoch/processSlashings.js"; -import {AttesterDuty, calculateCommitteeAssignments} from "../util/calculateCommitteeAssignments.js"; import { EpochShuffling, - IShufflingCache, + calculateDecisionRoot, calculateShufflingDecisionRoot, computeEpochShuffling, } from "../util/epochShuffling.js"; @@ -40,7 +37,6 @@ import { computeActivationExitEpoch, computeEpochAtSlot, computeProposers, - computeStartSlotAtEpoch, computeSyncPeriodAtEpoch, getActivationChurnLimit, getChurnLimit, @@ -49,6 +45,13 @@ import { isAggregatorFromCommitteeLength, naiveGetPayloadTimlinessCommitteeIndices, } from "../util/index.js"; +import { + AttesterDuty, + calculateCommitteeAssignments, + getAttestingIndices, + getBeaconCommittees, + getIndexedAttestation, +} from "../util/shuffling.js"; import {computeBaseRewardPerIncrement, computeSyncParticipantReward} from "../util/syncCommittee.js"; import {sumTargetUnslashedBalanceIncrements} from "../util/targetUnslashedBalance.js"; import {EffectiveBalanceIncrements, getEffectiveBalanceIncrementsWithLen} from "./effectiveBalanceIncrements.js"; @@ -61,7 +64,7 @@ import { computeSyncCommitteeCache, getSyncCommitteeCache, } from "./syncCommitteeCache.js"; -import {BeaconStateAllForks, BeaconStateAltair, BeaconStateGloas} from "./types.js"; +import {BeaconStateAllForks, BeaconStateAltair, BeaconStateGloas, ShufflingGetter} from "./types.js"; /** `= PROPOSER_WEIGHT / (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT)` */ export const PROPOSER_WEIGHT_FACTOR = PROPOSER_WEIGHT / (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT); @@ -70,12 +73,12 @@ export type EpochCacheImmutableData = { config: BeaconConfig; pubkey2index: PubkeyIndexMap; index2pubkey: Index2PubkeyCache; - shufflingCache?: IShufflingCache; }; export type EpochCacheOpts = { skipSyncCommitteeCache?: boolean; skipSyncPubkeys?: boolean; + shufflingGetter?: ShufflingGetter; }; /** Defers computing proposers by persisting only the seed, and dropping it once indexes are computed */ @@ -117,12 +120,6 @@ export class EpochCache { * $VALIDATOR_COUNT x BLST deserialized pubkey (Jacobian coordinates) */ index2pubkey: Index2PubkeyCache; - /** - * ShufflingCache is passed in from `beacon-node` so should be available at runtime but may not be - * present during testing. - */ - shufflingCache?: IShufflingCache; - /** * Indexes of the block proposers for the current epoch. * For pre-fulu, this is computed and cached from the current shuffling. @@ -161,7 +158,7 @@ export class EpochCache { /** Same as previousShuffling */ currentShuffling: EpochShuffling; /** Same as previousShuffling */ - nextShuffling: EpochShuffling | null; + nextShuffling: EpochShuffling; /** * Cache nextActiveIndices so that in afterProcessEpoch the next shuffling can be build synchronously * in case it is not built or the ShufflingCache is not available @@ -254,7 +251,6 @@ export class EpochCache { config: BeaconConfig; pubkey2index: PubkeyIndexMap; index2pubkey: Index2PubkeyCache; - shufflingCache?: IShufflingCache; proposers: number[]; proposersPrevEpoch: number[] | null; proposersNextEpoch: ProposersDeferred; @@ -263,7 +259,7 @@ export class EpochCache { nextDecisionRoot: RootHex; previousShuffling: EpochShuffling; currentShuffling: EpochShuffling; - nextShuffling: EpochShuffling | null; + nextShuffling: EpochShuffling; nextActiveIndices: Uint32Array; effectiveBalanceIncrements: EffectiveBalanceIncrements; totalSlashingsByIncrement: number; @@ -286,7 +282,6 @@ export class EpochCache { this.config = data.config; this.pubkey2index = data.pubkey2index; this.index2pubkey = data.index2pubkey; - this.shufflingCache = data.shufflingCache; this.proposers = data.proposers; this.proposersPrevEpoch = data.proposersPrevEpoch; this.proposersNextEpoch = data.proposersNextEpoch; @@ -324,7 +319,7 @@ export class EpochCache { */ static createFromState( state: BeaconStateAllForks, - {config, pubkey2index, index2pubkey, shufflingCache}: EpochCacheImmutableData, + {config, pubkey2index, index2pubkey}: EpochCacheImmutableData, opts?: EpochCacheOpts ): EpochCache { const currentEpoch = computeEpochAtSlot(state.slot); @@ -351,14 +346,15 @@ export class EpochCache { const currentActiveIndicesAsNumberArray: ValidatorIndex[] = []; const nextActiveIndicesAsNumberArray: ValidatorIndex[] = []; - // BeaconChain could provide a shuffling cache to avoid re-computing shuffling every epoch + // BeaconChain could provide a shuffling getter to avoid re-computing shuffling every epoch // in that case, we don't need to compute shufflings again + const shufflingGetter = opts?.shufflingGetter; const previousDecisionRoot = calculateShufflingDecisionRoot(config, state, previousEpoch); - const cachedPreviousShuffling = shufflingCache?.getSync(previousEpoch, previousDecisionRoot); + const cachedPreviousShuffling = shufflingGetter?.(previousEpoch, previousDecisionRoot); const currentDecisionRoot = calculateShufflingDecisionRoot(config, state, currentEpoch); - const cachedCurrentShuffling = shufflingCache?.getSync(currentEpoch, currentDecisionRoot); + const cachedCurrentShuffling = shufflingGetter?.(currentEpoch, currentDecisionRoot); const nextDecisionRoot = calculateShufflingDecisionRoot(config, state, nextEpoch); - const cachedNextShuffling = shufflingCache?.getSync(nextEpoch, nextDecisionRoot); + const cachedNextShuffling = shufflingGetter?.(nextEpoch, nextDecisionRoot); for (let i = 0; i < validatorCount; i++) { const validator = validators[i]; @@ -366,8 +362,7 @@ export class EpochCache { // Note: Not usable for fork-choice balances since in-active validators are not zero'ed effectiveBalanceIncrements[i] = Math.floor(validator.effectiveBalance / EFFECTIVE_BALANCE_INCREMENT); - // we only need to track active indices for previous, current and next epoch if we have to compute shufflings - // skip doing that if we already have cached shufflings + // Collect active indices for each epoch to compute shufflings if (cachedPreviousShuffling == null && isActiveValidator(validator, previousEpoch)) { previousActiveIndicesAsNumberArray.push(i); } @@ -402,47 +397,19 @@ export class EpochCache { } const nextActiveIndices = new Uint32Array(nextActiveIndicesAsNumberArray); - let previousShuffling: EpochShuffling; - let currentShuffling: EpochShuffling; - let nextShuffling: EpochShuffling; - - if (!shufflingCache) { - // Only for testing. shufflingCache should always be available in prod - previousShuffling = computeEpochShuffling( - state, - new Uint32Array(previousActiveIndicesAsNumberArray), - previousEpoch - ); - currentShuffling = isGenesis - ? previousShuffling - : computeEpochShuffling(state, new Uint32Array(currentActiveIndicesAsNumberArray), currentEpoch); + // Use cached shufflings if available, otherwise compute + const currentShuffling = + cachedCurrentShuffling ?? + computeEpochShuffling(state, new Uint32Array(currentActiveIndicesAsNumberArray), currentEpoch); - nextShuffling = computeEpochShuffling(state, nextActiveIndices, nextEpoch); - } else { - currentShuffling = cachedCurrentShuffling - ? cachedCurrentShuffling - : shufflingCache.getSync(currentEpoch, currentDecisionRoot, { - state, - activeIndices: new Uint32Array(currentActiveIndicesAsNumberArray), - }); - - previousShuffling = cachedPreviousShuffling - ? cachedPreviousShuffling - : isGenesis - ? currentShuffling - : shufflingCache.getSync(previousEpoch, previousDecisionRoot, { - state, - activeIndices: new Uint32Array(previousActiveIndicesAsNumberArray), - }); - - nextShuffling = cachedNextShuffling - ? cachedNextShuffling - : shufflingCache.getSync(nextEpoch, nextDecisionRoot, { - state, - activeIndices: nextActiveIndices, - }); - } + const previousShuffling = + cachedPreviousShuffling ?? + (isGenesis + ? currentShuffling + : computeEpochShuffling(state, new Uint32Array(previousActiveIndicesAsNumberArray), previousEpoch)); + + const nextShuffling = cachedNextShuffling ?? computeEpochShuffling(state, nextActiveIndices, nextEpoch); const currentProposerSeed = getSeed(state, currentEpoch, DOMAIN_BEACON_PROPOSER); @@ -549,7 +516,6 @@ export class EpochCache { config, pubkey2index, index2pubkey, - shufflingCache, proposers, // On first epoch, set to null to prevent unnecessary work since this is only used for metrics proposersPrevEpoch: null, @@ -593,7 +559,6 @@ export class EpochCache { // Common append-only structures shared with all states, no need to clone pubkey2index: this.pubkey2index, index2pubkey: this.index2pubkey, - shufflingCache: this.shufflingCache, // Immutable data proposers: this.proposers, proposersPrevEpoch: this.proposersPrevEpoch, @@ -652,62 +617,26 @@ export class EpochCache { this.previousShuffling = this.currentShuffling; this.previousDecisionRoot = this.currentDecisionRoot; - // move next to current or calculate upcoming + // move next to current this.currentDecisionRoot = this.nextDecisionRoot; - if (this.nextShuffling) { - // was already pulled from the ShufflingCache to the EpochCache (should be in most cases) - this.currentShuffling = this.nextShuffling; - } else { - this.shufflingCache?.metrics?.shufflingCache.nextShufflingNotOnEpochCache.inc(); - this.currentShuffling = - this.shufflingCache?.getSync(upcomingEpoch, this.currentDecisionRoot, { - state, - // have to use the "nextActiveIndices" that were saved in the last transition here to calculate - // the upcoming shuffling if it is not already built (similar condition to the below computation) - activeIndices: this.nextActiveIndices, - }) ?? - // allow for this case during testing where the ShufflingCache is not present, may affect perf testing - // so should be taken into account when structuring tests. Should not affect unit or other tests though - computeEpochShuffling(state, this.nextActiveIndices, upcomingEpoch); - } + this.currentShuffling = this.nextShuffling; - // handle next values - this.nextDecisionRoot = epochTransitionCache.nextShufflingDecisionRoot; + // Compute shuffling for epoch n+2 + // + // Post-Fulu (EIP-7917), the beacon state includes a `proposer_lookahead` field that stores + // proposer indices for MIN_SEED_LOOKAHEAD + 1 epochs ahead (2 epochs with MIN_SEED_LOOKAHEAD=1). + // At each epoch boundary, processProposerLookahead() shifts out the current epoch's proposers + // and appends new proposers for epoch n + MIN_SEED_LOOKAHEAD + 1 (i.e., epoch n+2). + // + // processProposerLookahead() already computes the n+2 shuffling and stores it in + // epochTransitionCache.nextShuffling. Reuse it here to avoid duplicate computation. + // Pre-Fulu, we need to compute it here since processProposerLookahead doesn't run. + // + // See: https://eips.ethereum.org/EIPS/eip-7917 + this.nextDecisionRoot = calculateDecisionRoot(state, epochAfterUpcoming); this.nextActiveIndices = epochTransitionCache.nextShufflingActiveIndices; - if (this.shufflingCache) { - if (!epochTransitionCache.asyncShufflingCalculation) { - this.nextShuffling = this.shufflingCache.getSync(epochAfterUpcoming, this.nextDecisionRoot, { - state, - activeIndices: this.nextActiveIndices, - }); - } else { - this.nextShuffling = null; - // This promise will resolve immediately after the synchronous code of the state-transition runs. Until - // the build is done on a worker thread it will be calculated immediately after the epoch transition - // completes. Once the work is done concurrently it should be ready by time this get runs so the promise - // will resolve directly on the next spin of the event loop because the epoch transition and shuffling take - // about the same time to calculate so theoretically its ready now. Do not await here though in case it - // is not ready yet as the transition must not be asynchronous. - this.shufflingCache - .get(epochAfterUpcoming, this.nextDecisionRoot) - .then((shuffling) => { - if (!shuffling) { - throw new Error("EpochShuffling not returned from get in afterProcessEpoch"); - } - this.nextShuffling = shuffling; - }) - .catch((err) => { - this.shufflingCache?.logger?.error( - "EPOCH_CONTEXT_SHUFFLING_BUILD_ERROR", - {epoch: epochAfterUpcoming, decisionRoot: epochTransitionCache.nextShufflingDecisionRoot}, - err - ); - }); - } - } else { - // Only for testing. shufflingCache should always be available in prod - this.nextShuffling = computeEpochShuffling(state, this.nextActiveIndices, epochAfterUpcoming); - } + this.nextShuffling = + epochTransitionCache.nextShuffling ?? computeEpochShuffling(state, this.nextActiveIndices, epochAfterUpcoming); // TODO: DEDUPLICATE from createEpochCache // @@ -826,22 +755,7 @@ export class EpochCache { if (indices.length === 0) { throw new Error("Attempt to get committees without providing CommitteeIndex"); } - - const slotCommittees = this.getShufflingAtSlot(slot).committees[slot % SLOTS_PER_EPOCH]; - const committees = []; - - for (const index of indices) { - if (index >= slotCommittees.length) { - throw new EpochCacheError({ - code: EpochCacheErrorCode.COMMITTEE_INDEX_OUT_OF_RANGE, - index, - maxIndex: slotCommittees.length, - }); - } - committees.push(slotCommittees[index]); - } - - return committees; + return getBeaconCommittees(this.getShufflingAtSlot(slot), slot, indices); } getCommitteeCountPerSlot(epoch: Epoch): number { @@ -939,50 +853,16 @@ export class EpochCache { * Return the indexed attestation corresponding to ``attestation``. */ getIndexedAttestation(fork: ForkSeq, attestation: Attestation): IndexedAttestation { - const {data} = attestation; - const attestingIndices = this.getAttestingIndices(fork, attestation); - - // sort in-place - attestingIndices.sort((a, b) => a - b); - return { - attestingIndices: attestingIndices, - data: data, - signature: attestation.signature, - }; + const shuffling = this.getShufflingAtSlot(attestation.data.slot); + return getIndexedAttestation(shuffling, fork, attestation); } /** * Return indices of validators who attestested in `attestation` */ getAttestingIndices(fork: ForkSeq, attestation: Attestation): number[] { - if (fork < ForkSeq.electra) { - const {aggregationBits, data} = attestation; - const validatorIndices = this.getBeaconCommittee(data.slot, data.index); - - return aggregationBits.intersectValues(validatorIndices); - } - const {aggregationBits, committeeBits, data} = attestation as electra.Attestation; - - // There is a naming conflict on the term `committeeIndices` - // In Lodestar it usually means a list of validator indices of participants in a committee - // In the spec it means a list of committee indices according to committeeBits - // This `committeeIndices` refers to the latter - // TODO Electra: resolve the naming conflicts - const committeeIndices = committeeBits.getTrueBitIndexes(); - - const validatorsByCommittee = this.getBeaconCommittees(data.slot, committeeIndices); - - // Create a new Uint32Array to flatten `validatorsByCommittee` - const totalLength = validatorsByCommittee.reduce((acc, curr) => acc + curr.length, 0); - const committeeValidators = new Uint32Array(totalLength); - - let offset = 0; - for (const committee of validatorsByCommittee) { - committeeValidators.set(committee, offset); - offset += committee.length; - } - - return aggregationBits.intersectValues(committeeValidators); + const shuffling = this.getShufflingAtSlot(attestation.data.slot); + return getAttestingIndices(shuffling, fork, attestation); } getCommitteeAssignments( @@ -993,38 +873,6 @@ export class EpochCache { return calculateCommitteeAssignments(shuffling, requestedValidatorIndices); } - /** - * Return the committee assignment in the ``epoch`` for ``validator_index``. - * ``assignment`` returned is a tuple of the following form: - * ``assignment[0]`` is the list of validators in the committee - * ``assignment[1]`` is the index to which the committee is assigned - * ``assignment[2]`` is the slot at which the committee is assigned - * Return null if no assignment.. - */ - getCommitteeAssignment(epoch: Epoch, validatorIndex: ValidatorIndex): phase0.CommitteeAssignment | null { - if (epoch > this.currentShuffling.epoch + 1) { - throw Error( - `Requesting committee assignment for more than 1 epoch ahead: ${epoch} > ${this.currentShuffling.epoch} + 1` - ); - } - - const epochStartSlot = computeStartSlotAtEpoch(epoch); - const committeeCountPerSlot = this.getCommitteeCountPerSlot(epoch); - for (let slot = epochStartSlot; slot < epochStartSlot + SLOTS_PER_EPOCH; slot++) { - for (let i = 0; i < committeeCountPerSlot; i++) { - const committee = this.getBeaconCommittee(slot, i); - if (committee.includes(validatorIndex)) { - return { - validators: Array.from(committee), - committeeIndex: i, - slot, - }; - } - } - } - return null; - } - isAggregator(slot: Slot, index: CommitteeIndex, slotSignature: BLSSignature): boolean { const committee = this.getBeaconCommittee(slot, index); return isAggregatorFromCommitteeLength(committee.length, slotSignature); @@ -1100,10 +948,6 @@ export class EpochCache { case this.epoch: return this.currentShuffling; case this.nextEpoch: - if (!this.nextShuffling) { - this.nextShuffling = - this.shufflingCache?.getSync(this.nextEpoch, this.getShufflingDecisionRoot(this.nextEpoch)) ?? null; - } return this.nextShuffling; default: return null; @@ -1213,7 +1057,6 @@ function getEffectiveBalanceIncrementsByteLen(validatorCount: number): number { } export enum EpochCacheErrorCode { - COMMITTEE_INDEX_OUT_OF_RANGE = "EPOCH_CONTEXT_ERROR_COMMITTEE_INDEX_OUT_OF_RANGE", COMMITTEE_EPOCH_OUT_OF_RANGE = "EPOCH_CONTEXT_ERROR_COMMITTEE_EPOCH_OUT_OF_RANGE", DECISION_ROOT_EPOCH_OUT_OF_RANGE = "EPOCH_CONTEXT_ERROR_DECISION_ROOT_EPOCH_OUT_OF_RANGE", NEXT_SHUFFLING_NOT_AVAILABLE = "EPOCH_CONTEXT_ERROR_NEXT_SHUFFLING_NOT_AVAILABLE", @@ -1222,11 +1065,6 @@ export enum EpochCacheErrorCode { } type EpochCacheErrorType = - | { - code: EpochCacheErrorCode.COMMITTEE_INDEX_OUT_OF_RANGE; - index: number; - maxIndex: number; - } | { code: EpochCacheErrorCode.COMMITTEE_EPOCH_OUT_OF_RANGE; requestedEpoch: Epoch; diff --git a/packages/state-transition/src/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts index a3ec69956162..01d7e94ff153 100644 --- a/packages/state-transition/src/cache/epochTransitionCache.ts +++ b/packages/state-transition/src/cache/epochTransitionCache.ts @@ -1,12 +1,6 @@ -import { - EPOCHS_PER_SLASHINGS_VECTOR, - FAR_FUTURE_EPOCH, - ForkSeq, - MIN_ACTIVATION_BALANCE, - SLOTS_PER_HISTORICAL_ROOT, -} from "@lodestar/params"; -import {Epoch, RootHex, ValidatorIndex} from "@lodestar/types"; -import {intDiv, toRootHex} from "@lodestar/utils"; +import {EPOCHS_PER_SLASHINGS_VECTOR, FAR_FUTURE_EPOCH, ForkSeq, MIN_ACTIVATION_BALANCE} from "@lodestar/params"; +import {Epoch, ValidatorIndex} from "@lodestar/types"; +import {intDiv} from "@lodestar/utils"; import {processPendingAttestations} from "../epoch/processPendingAttestations.js"; import { CachedBeaconStateAllForks, @@ -26,16 +20,13 @@ import { FLAG_UNSLASHED, hasMarkers, } from "../util/attesterStatus.js"; +import {EpochShuffling} from "../util/epochShuffling.js"; export type EpochTransitionCacheOpts = { /** * Assert progressive balances the same to EpochTransitionCache */ assertCorrectProgressiveBalances?: boolean; - /** - * Do not queue shuffling calculation async. Forces sync JIT calculation in afterProcessEpoch - */ - asyncShufflingCalculation?: boolean; }; /** @@ -162,9 +153,10 @@ export interface EpochTransitionCache { nextShufflingActiveIndices: Uint32Array; /** - * Shuffling decision root that gets set on the EpochCache in afterProcessEpoch + * Pre-computed shuffling for epoch N+2, populated by processProposerLookahead (Fulu+). + * Used by afterProcessEpoch to avoid recomputing the same shuffling. */ - nextShufflingDecisionRoot: RootHex; + nextShuffling: EpochShuffling | null; /** * Altair specific, this is total active balances for the next epoch. @@ -179,12 +171,6 @@ export interface EpochTransitionCache { */ nextEpochTotalActiveBalanceByIncrement: number; - /** - * Compute the shuffling sync or async. Defaults to synchronous. Need to pass `true` with the - * `EpochTransitionCacheOpts` - */ - asyncShufflingCalculation: boolean; - /** * Track by validator index if it's active in the prev epoch. * Used in metrics @@ -379,12 +365,7 @@ export function beforeProcessEpoch( } }); - // Trigger async build of shuffling for epoch after next (nextShuffling post epoch transition) - const epochAfterNext = state.epochCtx.nextEpoch + 1; - // cannot call calculateShufflingDecisionRoot here because spec prevent getting current slot - // as a decision block. we are part way through the transition though and this was added in - // process slot beforeProcessEpoch happens so it available and valid - const nextShufflingDecisionRoot = toRootHex(state.blockRoots.get(state.slot % SLOTS_PER_HISTORICAL_ROOT)); + // Prepare shuffling data for epoch after next (nextShuffling post epoch transition) const nextShufflingActiveIndices = new Uint32Array(nextEpochShufflingActiveIndicesLength); if (nextEpochShufflingActiveIndicesLength > nextEpochShufflingActiveValidatorIndices.length) { throw new Error( @@ -396,11 +377,6 @@ export function beforeProcessEpoch( nextShufflingActiveIndices[i] = nextEpochShufflingActiveValidatorIndices[i]; } - const asyncShufflingCalculation = opts?.asyncShufflingCalculation ?? false; - if (asyncShufflingCalculation) { - state.epochCtx.shufflingCache?.build(epochAfterNext, nextShufflingDecisionRoot, state, nextShufflingActiveIndices); - } - if (totalActiveStakeByIncrement < 1) { totalActiveStakeByIncrement = 1; } else if (totalActiveStakeByIncrement >= Number.MAX_SAFE_INTEGER) { @@ -524,9 +500,8 @@ export function beforeProcessEpoch( indicesEligibleForActivationQueue, indicesEligibleForActivation: indicesEligibleForActivation.map(({validatorIndex}) => validatorIndex), indicesToEject, - nextShufflingDecisionRoot, nextShufflingActiveIndices, - asyncShufflingCalculation, + nextShuffling: null, // to be updated in processEffectiveBalanceUpdates nextEpochTotalActiveBalanceByIncrement: 0, isActivePrevEpoch, diff --git a/packages/state-transition/src/cache/stateCache.ts b/packages/state-transition/src/cache/stateCache.ts index 231a921d966b..f69cd2a44972 100644 --- a/packages/state-transition/src/cache/stateCache.ts +++ b/packages/state-transition/src/cache/stateCache.ts @@ -180,7 +180,7 @@ export function loadCachedBeaconState quorum) { - const exitQueueEpoch = computeExitEpochAndUpdateChurn(state, BigInt(payment.withdrawal.amount)); - payment.withdrawal.withdrawableEpoch = exitQueueEpoch + state.config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY; - + if (payment.weight >= quorum) { state.builderPendingWithdrawals.push(payment.withdrawal); } } diff --git a/packages/state-transition/src/epoch/processPendingAttestations.ts b/packages/state-transition/src/epoch/processPendingAttestations.ts index a6043be77524..2cabcc0c918d 100644 --- a/packages/state-transition/src/epoch/processPendingAttestations.ts +++ b/packages/state-transition/src/epoch/processPendingAttestations.ts @@ -1,5 +1,5 @@ -import {byteArrayEquals} from "@chainsafe/ssz"; import {Epoch, phase0} from "@lodestar/types"; +import {byteArrayEquals} from "@lodestar/utils"; import {CachedBeaconStatePhase0} from "../types.js"; import {computeStartSlotAtEpoch, getBlockRootAtSlot} from "../util/index.js"; diff --git a/packages/state-transition/src/epoch/processProposerLookahead.ts b/packages/state-transition/src/epoch/processProposerLookahead.ts index 244429272d3a..4d92afcf45cc 100644 --- a/packages/state-transition/src/epoch/processProposerLookahead.ts +++ b/packages/state-transition/src/epoch/processProposerLookahead.ts @@ -22,13 +22,9 @@ export function processProposerLookahead( // Fill in the last epoch with new proposer indices const epoch = state.epochCtx.epoch + MIN_SEED_LOOKAHEAD + 1; - const shuffling = - state.epochCtx.shufflingCache?.getSync(epoch, cache.nextShufflingDecisionRoot, { - state, - activeIndices: cache.nextShufflingActiveIndices, - }) ?? - // Only for testing. shufflingCache should always be available in prod - computeEpochShuffling(state, cache.nextShufflingActiveIndices, epoch); + const shuffling = computeEpochShuffling(state, cache.nextShufflingActiveIndices, epoch); + // Save shuffling to cache so afterProcessEpoch can reuse it instead of recomputing + cache.nextShuffling = shuffling; const lastEpochProposerLookahead = computeProposerIndices(fork, state, shuffling, epoch); diff --git a/packages/state-transition/src/index.ts b/packages/state-transition/src/index.ts index 67ae559fe138..c5836ffe9518 100644 --- a/packages/state-transition/src/index.ts +++ b/packages/state-transition/src/index.ts @@ -21,8 +21,6 @@ export { } from "./cache/effectiveBalanceIncrements.js"; export { EpochCache, - EpochCacheError, - EpochCacheErrorCode, type EpochCacheImmutableData, createEmptyEpochCacheImmutableData, } from "./cache/epochCache.js"; diff --git a/packages/state-transition/src/rewards/blockRewards.ts b/packages/state-transition/src/rewards/blockRewards.ts index 35e1875950e4..d455595aae33 100644 --- a/packages/state-transition/src/rewards/blockRewards.ts +++ b/packages/state-transition/src/rewards/blockRewards.ts @@ -7,6 +7,7 @@ import { } from "@lodestar/params"; import {BeaconBlock, altair, phase0, rewards} from "@lodestar/types"; import {processAttestationsAltair} from "../block/processAttestationsAltair.js"; +import {RewardCache} from "../cache/rewardCache.js"; import {CachedBeaconStateAllForks, CachedBeaconStateAltair, CachedBeaconStatePhase0} from "../cache/stateCache.js"; import {getAttesterSlashableIndices} from "../util/attestation.js"; @@ -23,12 +24,14 @@ type SubRewardValue = number; // All reward values should be integer export async function computeBlockRewards( config: BeaconConfig, block: BeaconBlock, - preState: CachedBeaconStateAllForks, - postState?: CachedBeaconStateAllForks + preStateIn: CachedBeaconStateAllForks, + proposerRewards?: RewardCache ): Promise { + const preState = preStateIn.clone(); + const fork = config.getForkName(block.slot); const {attestations: cachedAttestationsReward = 0, syncAggregate: cachedSyncAggregateReward = 0} = - postState?.proposerRewards ?? {}; + proposerRewards ?? {}; let blockAttestationReward = cachedAttestationsReward; let syncAggregateReward = cachedSyncAggregateReward; diff --git a/packages/state-transition/src/rewards/syncCommitteeRewards.ts b/packages/state-transition/src/rewards/syncCommitteeRewards.ts index 60b19d82ea43..27e2c24e3dd9 100644 --- a/packages/state-transition/src/rewards/syncCommitteeRewards.ts +++ b/packages/state-transition/src/rewards/syncCommitteeRewards.ts @@ -4,8 +4,6 @@ import {BeaconBlock, ValidatorIndex, altair, rewards} from "@lodestar/types"; import {Index2PubkeyCache} from "../cache/pubkeyCache.js"; import {CachedBeaconStateAllForks, CachedBeaconStateAltair} from "../cache/stateCache.js"; -type BalanceRecord = {val: number}; // Use val for convenient way to increment/decrement balance - export async function computeSyncCommitteeRewards( config: BeaconConfig, index2pubkey: Index2PubkeyCache, @@ -19,7 +17,7 @@ export async function computeSyncCommitteeRewards( } const altairBlock = block as altair.BeaconBlock; - const preStateAltair = preState as CachedBeaconStateAltair; + const preStateAltair = preState.clone() as CachedBeaconStateAltair; // Bound syncCommitteeValidatorIndices in case it goes beyond SYNC_COMMITTEE_SIZE just to be safe const syncCommitteeValidatorIndices = preStateAltair.epochCtx.currentSyncCommitteeIndexed.validatorIndices.slice( @@ -29,24 +27,23 @@ export async function computeSyncCommitteeRewards( const {syncParticipantReward} = preStateAltair.epochCtx; const {syncCommitteeBits} = altairBlock.body.syncAggregate; - // Use balance of each committee as starting point such that we cap the penalty to avoid balance dropping below 0 - const balances: Map = new Map(); - for (const i of syncCommitteeValidatorIndices) { - balances.set(i, {val: preStateAltair.balances.get(i)}); - } + // Track reward deltas per validator (can appear multiple times in sync committee) + const rewardDeltas: Map = new Map(); - for (const i of syncCommitteeValidatorIndices) { - const balanceRecord = balances.get(i) as BalanceRecord; + // Iterate by position index to correctly access syncCommitteeBits + for (let i = 0; i < syncCommitteeValidatorIndices.length; i++) { + const validatorIndex = syncCommitteeValidatorIndices[i]; + const currentDelta = rewardDeltas.get(validatorIndex) ?? 0; if (syncCommitteeBits.get(i)) { // Positive rewards for participants - balanceRecord.val += syncParticipantReward; + rewardDeltas.set(validatorIndex, currentDelta + syncParticipantReward); } else { // Negative rewards for non participants - balanceRecord.val = Math.max(0, balanceRecord.val - syncParticipantReward); + rewardDeltas.set(validatorIndex, currentDelta - syncParticipantReward); } } - const rewards = Array.from(balances, ([validatorIndex, v]) => ({validatorIndex, reward: v.val})); + const rewards = Array.from(rewardDeltas, ([validatorIndex, reward]) => ({validatorIndex, reward})); if (validatorIds.length) { const filtersSet = new Set(validatorIds); diff --git a/packages/state-transition/src/signatureSets/attesterSlashings.ts b/packages/state-transition/src/signatureSets/attesterSlashings.ts index 57256fda5f07..bd9a81542cfe 100644 --- a/packages/state-transition/src/signatureSets/attesterSlashings.ts +++ b/packages/state-transition/src/signatureSets/attesterSlashings.ts @@ -1,38 +1,34 @@ import {BeaconConfig} from "@lodestar/config"; import {DOMAIN_BEACON_ATTESTER} from "@lodestar/params"; import {AttesterSlashing, IndexedAttestationBigint, SignedBeaconBlock, Slot, ssz} from "@lodestar/types"; -import {Index2PubkeyCache} from "../cache/pubkeyCache.js"; import {ISignatureSet, SignatureSetType, computeSigningRoot, computeStartSlotAtEpoch} from "../util/index.js"; /** Get signature sets from all AttesterSlashing objects in a block */ export function getAttesterSlashingsSignatureSets( config: BeaconConfig, - index2pubkey: Index2PubkeyCache, signedBlock: SignedBeaconBlock ): ISignatureSet[] { // the getDomain() api requires the state slot as 1st param, however it's the same to block.slot in state-transition // and the same epoch when we verify blocks in batch in beacon-node. So we can safely use block.slot here. const blockSlot = signedBlock.message.slot; return signedBlock.message.body.attesterSlashings.flatMap((attesterSlashing) => - getAttesterSlashingSignatureSets(config, index2pubkey, blockSlot, attesterSlashing) + getAttesterSlashingSignatureSets(config, blockSlot, attesterSlashing) ); } /** Get signature sets from a single AttesterSlashing object */ export function getAttesterSlashingSignatureSets( config: BeaconConfig, - index2pubkey: Index2PubkeyCache, stateSlot: Slot, attesterSlashing: AttesterSlashing ): ISignatureSet[] { return [attesterSlashing.attestation1, attesterSlashing.attestation2].map((attestation) => - getIndexedAttestationBigintSignatureSet(config, index2pubkey, stateSlot, attestation) + getIndexedAttestationBigintSignatureSet(config, stateSlot, attestation) ); } export function getIndexedAttestationBigintSignatureSet( config: BeaconConfig, - index2pubkey: Index2PubkeyCache, stateSlot: Slot, indexedAttestation: IndexedAttestationBigint ): ISignatureSet { @@ -41,7 +37,7 @@ export function getIndexedAttestationBigintSignatureSet( return { type: SignatureSetType.aggregate, - pubkeys: indexedAttestation.attestingIndices.map((i) => index2pubkey[i]), + indices: indexedAttestation.attestingIndices.map((i) => Number(i)), signingRoot: computeSigningRoot(ssz.phase0.AttestationDataBigint, indexedAttestation.data, domain), signature: indexedAttestation.signature, }; diff --git a/packages/state-transition/src/signatureSets/blsToExecutionChange.ts b/packages/state-transition/src/signatureSets/blsToExecutionChange.ts index 1bb2350007e2..bc1c1f786321 100644 --- a/packages/state-transition/src/signatureSets/blsToExecutionChange.ts +++ b/packages/state-transition/src/signatureSets/blsToExecutionChange.ts @@ -2,14 +2,13 @@ import {PublicKey} from "@chainsafe/blst"; import {BeaconConfig} from "@lodestar/config"; import {DOMAIN_BLS_TO_EXECUTION_CHANGE, ForkName} from "@lodestar/params"; import {capella, ssz} from "@lodestar/types"; -import {CachedBeaconStateAllForks} from "../types.js"; -import {ISignatureSet, SignatureSetType, computeSigningRoot, verifySignatureSet} from "../util/index.js"; +import {SignatureSetType, SingleSignatureSet, computeSigningRoot, verifySignatureSet} from "../util/index.js"; export function verifyBlsToExecutionChangeSignature( - state: CachedBeaconStateAllForks, + config: BeaconConfig, signedBLSToExecutionChange: capella.SignedBLSToExecutionChange ): boolean { - return verifySignatureSet(getBlsToExecutionChangeSignatureSet(state.config, signedBLSToExecutionChange)); + return verifySignatureSet(getBlsToExecutionChangeSignatureSet(config, signedBLSToExecutionChange)); } /** @@ -18,7 +17,7 @@ export function verifyBlsToExecutionChangeSignature( export function getBlsToExecutionChangeSignatureSet( config: BeaconConfig, signedBLSToExecutionChange: capella.SignedBLSToExecutionChange -): ISignatureSet { +): SingleSignatureSet { // signatureFork for signing domain is fixed const signatureFork = ForkName.phase0; const domain = config.getDomainAtFork(signatureFork, DOMAIN_BLS_TO_EXECUTION_CHANGE); @@ -36,7 +35,7 @@ export function getBlsToExecutionChangeSignatureSet( export function getBlsToExecutionChangeSignatureSets( config: BeaconConfig, signedBlock: capella.SignedBeaconBlock -): ISignatureSet[] { +): SingleSignatureSet[] { return signedBlock.message.body.blsToExecutionChanges.map((blsToExecutionChange) => getBlsToExecutionChangeSignatureSet(config, blsToExecutionChange) ); diff --git a/packages/state-transition/src/signatureSets/executionPayloadBid.ts b/packages/state-transition/src/signatureSets/executionPayloadBid.ts new file mode 100644 index 000000000000..cebad9261b8e --- /dev/null +++ b/packages/state-transition/src/signatureSets/executionPayloadBid.ts @@ -0,0 +1,14 @@ +import {BeaconConfig} from "@lodestar/config"; +import {DOMAIN_BEACON_BUILDER} from "@lodestar/params"; +import {Slot, gloas, ssz} from "@lodestar/types"; +import {computeSigningRoot} from "../util/index.js"; + +export function getExecutionPayloadBidSigningRoot( + config: BeaconConfig, + stateSlot: Slot, + bid: gloas.ExecutionPayloadBid +): Uint8Array { + const domain = config.getDomain(stateSlot, DOMAIN_BEACON_BUILDER); + + return computeSigningRoot(ssz.gloas.ExecutionPayloadBid, bid, domain); +} diff --git a/packages/state-transition/src/signatureSets/executionPayloadEnvelope.ts b/packages/state-transition/src/signatureSets/executionPayloadEnvelope.ts new file mode 100644 index 000000000000..40c21f25fd77 --- /dev/null +++ b/packages/state-transition/src/signatureSets/executionPayloadEnvelope.ts @@ -0,0 +1,13 @@ +import {BeaconConfig} from "@lodestar/config"; +import {DOMAIN_BEACON_BUILDER} from "@lodestar/params"; +import {gloas, ssz} from "@lodestar/types"; +import {computeSigningRoot} from "../util/index.js"; + +export function getExecutionPayloadEnvelopeSigningRoot( + config: BeaconConfig, + envelope: gloas.ExecutionPayloadEnvelope +): Uint8Array { + const domain = config.getDomain(envelope.slot, DOMAIN_BEACON_BUILDER); + + return computeSigningRoot(ssz.gloas.ExecutionPayloadEnvelope, envelope, domain); +} diff --git a/packages/state-transition/src/signatureSets/index.ts b/packages/state-transition/src/signatureSets/index.ts index 7397074d074f..8c797e1adb8e 100644 --- a/packages/state-transition/src/signatureSets/index.ts +++ b/packages/state-transition/src/signatureSets/index.ts @@ -2,7 +2,6 @@ import {BeaconConfig} from "@lodestar/config"; import {ForkSeq} from "@lodestar/params"; import {IndexedAttestation, SignedBeaconBlock, altair, capella} from "@lodestar/types"; import {getSyncCommitteeSignatureSet} from "../block/processSyncCommittee.js"; -import {Index2PubkeyCache} from "../cache/pubkeyCache.js"; import {SyncCommitteeCache} from "../cache/syncCommitteeCache.js"; import {ISignatureSet} from "../util/index.js"; import {getAttesterSlashingsSignatureSets} from "./attesterSlashings.js"; @@ -15,6 +14,8 @@ import {getVoluntaryExitsSignatureSets} from "./voluntaryExits.js"; export * from "./attesterSlashings.js"; export * from "./blsToExecutionChange.js"; +export * from "./executionPayloadBid.js"; +export * from "./executionPayloadEnvelope.js"; export * from "./indexedAttestation.js"; export * from "./indexedPayloadAttestation.js"; export * from "./proposer.js"; @@ -28,7 +29,6 @@ export * from "./voluntaryExits.js"; */ export function getBlockSignatureSets( config: BeaconConfig, - index2pubkey: Index2PubkeyCache, currentSyncCommitteeIndexed: SyncCommitteeCache, signedBlock: SignedBeaconBlock, indexedAttestations: IndexedAttestation[], @@ -41,22 +41,21 @@ export function getBlockSignatureSets( const fork = config.getForkSeq(signedBlock.message.slot); const signatureSets = [ - getRandaoRevealSignatureSet(config, index2pubkey, signedBlock.message), - ...getProposerSlashingsSignatureSets(config, index2pubkey, signedBlock), - ...getAttesterSlashingsSignatureSets(config, index2pubkey, signedBlock), - ...getAttestationsSignatureSets(config, index2pubkey, signedBlock, indexedAttestations), - ...getVoluntaryExitsSignatureSets(config, index2pubkey, signedBlock), + getRandaoRevealSignatureSet(config, signedBlock.message), + ...getProposerSlashingsSignatureSets(config, signedBlock), + ...getAttesterSlashingsSignatureSets(config, signedBlock), + ...getAttestationsSignatureSets(config, signedBlock, indexedAttestations), + ...getVoluntaryExitsSignatureSets(config, signedBlock), ]; if (!opts?.skipProposerSignature) { - signatureSets.push(getBlockProposerSignatureSet(config, index2pubkey, signedBlock)); + signatureSets.push(getBlockProposerSignatureSet(config, signedBlock)); } // Only after altair fork, validate tSyncCommitteeSignature if (fork >= ForkSeq.altair) { const syncCommitteeSignatureSet = getSyncCommitteeSignatureSet( config, - index2pubkey, currentSyncCommitteeIndexed, (signedBlock as altair.SignedBeaconBlock).message ); diff --git a/packages/state-transition/src/signatureSets/indexedAttestation.ts b/packages/state-transition/src/signatureSets/indexedAttestation.ts index bb8d00e6b7de..309cf1aedbfe 100644 --- a/packages/state-transition/src/signatureSets/indexedAttestation.ts +++ b/packages/state-transition/src/signatureSets/indexedAttestation.ts @@ -1,7 +1,6 @@ import {BeaconConfig} from "@lodestar/config"; import {DOMAIN_BEACON_ATTESTER} from "@lodestar/params"; import {IndexedAttestation, SignedBeaconBlock, Slot, phase0, ssz} from "@lodestar/types"; -import {Index2PubkeyCache} from "../cache/pubkeyCache.js"; import { ISignatureSet, computeSigningRoot, @@ -22,13 +21,12 @@ export function getAttestationDataSigningRoot( export function getAttestationWithIndicesSignatureSet( config: BeaconConfig, - index2pubkey: Index2PubkeyCache, stateSlot: Slot, attestation: Pick, attestingIndices: number[] ): ISignatureSet { return createAggregateSignatureSetFromComponents( - attestingIndices.map((i) => index2pubkey[i]), + attestingIndices, getAttestationDataSigningRoot(config, stateSlot, attestation.data), attestation.signature ); @@ -36,13 +34,11 @@ export function getAttestationWithIndicesSignatureSet( export function getIndexedAttestationSignatureSet( config: BeaconConfig, - index2pubkey: Index2PubkeyCache, stateSlot: Slot, indexedAttestation: IndexedAttestation ): ISignatureSet { return getAttestationWithIndicesSignatureSet( config, - index2pubkey, stateSlot, indexedAttestation, indexedAttestation.attestingIndices @@ -51,7 +47,6 @@ export function getIndexedAttestationSignatureSet( export function getAttestationsSignatureSets( config: BeaconConfig, - index2pubkey: Index2PubkeyCache, signedBlock: SignedBeaconBlock, indexedAttestations: IndexedAttestation[] ): ISignatureSet[] { @@ -64,6 +59,6 @@ export function getAttestationsSignatureSets( // and the same epoch when we verify blocks in batch in beacon-node. So we can safely use block.slot here. const blockSlot = signedBlock.message.slot; return indexedAttestations.map((indexedAttestation) => - getIndexedAttestationSignatureSet(config, index2pubkey, blockSlot, indexedAttestation) + getIndexedAttestationSignatureSet(config, blockSlot, indexedAttestation) ); } diff --git a/packages/state-transition/src/signatureSets/indexedPayloadAttestation.ts b/packages/state-transition/src/signatureSets/indexedPayloadAttestation.ts index 13b5d8842d3e..15ae5169d272 100644 --- a/packages/state-transition/src/signatureSets/indexedPayloadAttestation.ts +++ b/packages/state-transition/src/signatureSets/indexedPayloadAttestation.ts @@ -1,24 +1,26 @@ +import {BeaconConfig} from "@lodestar/config"; import {DOMAIN_PTC_ATTESTER} from "@lodestar/params"; -import {gloas, ssz} from "@lodestar/types"; -import {CachedBeaconStateGloas} from "../types.ts"; -import {ISignatureSet, computeSigningRoot, createAggregateSignatureSetFromComponents} from "../util/index.ts"; +import {Slot, gloas, ssz} from "@lodestar/types"; +import {CachedBeaconStateGloas} from "../types.js"; +import {ISignatureSet, computeSigningRoot, createAggregateSignatureSetFromComponents} from "../util/index.js"; export function getIndexedPayloadAttestationSignatureSet( state: CachedBeaconStateGloas, indexedPayloadAttestation: gloas.IndexedPayloadAttestation ): ISignatureSet { return createAggregateSignatureSetFromComponents( - indexedPayloadAttestation.attestingIndices.map((i) => state.epochCtx.index2pubkey[i]), - getPayloadAttestationDataSigningRoot(state, indexedPayloadAttestation.data), + indexedPayloadAttestation.attestingIndices, + getPayloadAttestationDataSigningRoot(state.config, state.slot, indexedPayloadAttestation.data), indexedPayloadAttestation.signature ); } export function getPayloadAttestationDataSigningRoot( - state: CachedBeaconStateGloas, + config: BeaconConfig, + stateSlot: Slot, data: gloas.PayloadAttestationData ): Uint8Array { - const domain = state.config.getDomain(state.slot, DOMAIN_PTC_ATTESTER); + const domain = config.getDomain(stateSlot, DOMAIN_PTC_ATTESTER); return computeSigningRoot(ssz.gloas.PayloadAttestationData, data, domain); } diff --git a/packages/state-transition/src/signatureSets/proposer.ts b/packages/state-transition/src/signatureSets/proposer.ts index dfcb870948f0..8505ec22bd3c 100644 --- a/packages/state-transition/src/signatureSets/proposer.ts +++ b/packages/state-transition/src/signatureSets/proposer.ts @@ -10,13 +10,12 @@ export function verifyProposerSignature( index2pubkey: Index2PubkeyCache, signedBlock: SignedBeaconBlock | SignedBlindedBeaconBlock ): boolean { - const signatureSet = getBlockProposerSignatureSet(config, index2pubkey, signedBlock); - return verifySignatureSet(signatureSet); + const signatureSet = getBlockProposerSignatureSet(config, signedBlock); + return verifySignatureSet(signatureSet, index2pubkey); } export function getBlockProposerSignatureSet( config: BeaconConfig, - index2pubkey: Index2PubkeyCache, signedBlock: SignedBeaconBlock | SignedBlindedBeaconBlock ): ISignatureSet { // the getDomain() api requires the state slot as 1st param, however it's the same to block.slot in state-transition @@ -29,8 +28,8 @@ export function getBlockProposerSignatureSet( : config.getForkTypes(signedBlock.message.slot).BeaconBlock; return { - type: SignatureSetType.single, - pubkey: index2pubkey[signedBlock.message.proposerIndex], + type: SignatureSetType.indexed, + index: signedBlock.message.proposerIndex, signingRoot: computeSigningRoot(blockType, signedBlock.message, domain), signature: signedBlock.signature, }; @@ -38,32 +37,29 @@ export function getBlockProposerSignatureSet( export function getBlockHeaderProposerSignatureSetByParentStateSlot( config: BeaconConfig, - index2pubkey: Index2PubkeyCache, parentStateSlot: Slot, signedBlockHeader: phase0.SignedBeaconBlockHeader ) { - return getBlockHeaderProposerSignatureSet(config, index2pubkey, signedBlockHeader, parentStateSlot); + return getBlockHeaderProposerSignatureSet(config, signedBlockHeader, parentStateSlot); } export function getBlockHeaderProposerSignatureSetByHeaderSlot( config: BeaconConfig, - index2pubkey: Index2PubkeyCache, signedBlockHeader: phase0.SignedBeaconBlockHeader ) { - return getBlockHeaderProposerSignatureSet(config, index2pubkey, signedBlockHeader, signedBlockHeader.message.slot); + return getBlockHeaderProposerSignatureSet(config, signedBlockHeader, signedBlockHeader.message.slot); } function getBlockHeaderProposerSignatureSet( config: BeaconConfig, - index2pubkey: Index2PubkeyCache, signedBlockHeader: phase0.SignedBeaconBlockHeader, domainSlot: Slot ): ISignatureSet { const domain = config.getDomain(domainSlot, DOMAIN_BEACON_PROPOSER, signedBlockHeader.message.slot); return { - type: SignatureSetType.single, - pubkey: index2pubkey[signedBlockHeader.message.proposerIndex], + type: SignatureSetType.indexed, + index: signedBlockHeader.message.proposerIndex, signingRoot: computeSigningRoot(ssz.phase0.BeaconBlockHeader, signedBlockHeader.message, domain), signature: signedBlockHeader.signature, }; diff --git a/packages/state-transition/src/signatureSets/proposerSlashings.ts b/packages/state-transition/src/signatureSets/proposerSlashings.ts index 058d87cf490c..6e5a49fe0e03 100644 --- a/packages/state-transition/src/signatureSets/proposerSlashings.ts +++ b/packages/state-transition/src/signatureSets/proposerSlashings.ts @@ -1,7 +1,6 @@ import {BeaconConfig} from "@lodestar/config"; import {DOMAIN_BEACON_PROPOSER} from "@lodestar/params"; import {SignedBeaconBlock, Slot, phase0, ssz} from "@lodestar/types"; -import {Index2PubkeyCache} from "../cache/pubkeyCache.js"; import {ISignatureSet, SignatureSetType, computeSigningRoot} from "../util/index.js"; /** @@ -9,11 +8,10 @@ import {ISignatureSet, SignatureSetType, computeSigningRoot} from "../util/index */ export function getProposerSlashingSignatureSets( config: BeaconConfig, - index2pubkey: Index2PubkeyCache, stateSlot: Slot, proposerSlashing: phase0.ProposerSlashing ): ISignatureSet[] { - const pubkey = index2pubkey[proposerSlashing.signedHeader1.message.proposerIndex]; + const proposerIndex = proposerSlashing.signedHeader1.message.proposerIndex; // In state transition, ProposerSlashing headers are only partially validated. Their slot could be higher than the // clock and the slashing would still be valid. Must use bigint variants to hash correctly to all possible values @@ -21,8 +19,8 @@ export function getProposerSlashingSignatureSets( const domain = config.getDomain(stateSlot, DOMAIN_BEACON_PROPOSER, Number(signedHeader.message.slot as bigint)); return { - type: SignatureSetType.single, - pubkey, + type: SignatureSetType.indexed, + index: proposerIndex, signingRoot: computeSigningRoot(ssz.phase0.BeaconBlockHeaderBigint, signedHeader.message, domain), signature: signedHeader.signature, }; @@ -31,13 +29,12 @@ export function getProposerSlashingSignatureSets( export function getProposerSlashingsSignatureSets( config: BeaconConfig, - index2pubkey: Index2PubkeyCache, signedBlock: SignedBeaconBlock ): ISignatureSet[] { // the getDomain() api requires the state slot as 1st param, however it's the same to block.slot in state-transition // and the same epoch when we verify blocks in batch in beacon-node. So we can safely use block.slot here. const blockSlot = signedBlock.message.slot; return signedBlock.message.body.proposerSlashings.flatMap((proposerSlashing) => - getProposerSlashingSignatureSets(config, index2pubkey, blockSlot, proposerSlashing) + getProposerSlashingSignatureSets(config, blockSlot, proposerSlashing) ); } diff --git a/packages/state-transition/src/signatureSets/randao.ts b/packages/state-transition/src/signatureSets/randao.ts index 6cb99cee3016..d6a132c215b2 100644 --- a/packages/state-transition/src/signatureSets/randao.ts +++ b/packages/state-transition/src/signatureSets/randao.ts @@ -15,17 +15,13 @@ export function verifyRandaoSignature( index2pubkey: Index2PubkeyCache, block: BeaconBlock ): boolean { - return verifySignatureSet(getRandaoRevealSignatureSet(config, index2pubkey, block)); + return verifySignatureSet(getRandaoRevealSignatureSet(config, block), index2pubkey); } /** * Extract signatures to allow validating all block signatures at once */ -export function getRandaoRevealSignatureSet( - config: BeaconConfig, - index2pubkey: Index2PubkeyCache, - block: BeaconBlock -): ISignatureSet { +export function getRandaoRevealSignatureSet(config: BeaconConfig, block: BeaconBlock): ISignatureSet { // should not get epoch from epochCtx const epoch = computeEpochAtSlot(block.slot); // the getDomain() api requires the state slot as 1st param, however it's the same to block.slot in state-transition @@ -33,8 +29,8 @@ export function getRandaoRevealSignatureSet( const domain = config.getDomain(block.slot, DOMAIN_RANDAO, block.slot); return { - type: SignatureSetType.single, - pubkey: index2pubkey[block.proposerIndex], + type: SignatureSetType.indexed, + index: block.proposerIndex, signingRoot: computeSigningRoot(ssz.Epoch, epoch, domain), signature: block.body.randaoReveal, }; diff --git a/packages/state-transition/src/signatureSets/voluntaryExits.ts b/packages/state-transition/src/signatureSets/voluntaryExits.ts index 7c2086aa586e..2ed07c6caaee 100644 --- a/packages/state-transition/src/signatureSets/voluntaryExits.ts +++ b/packages/state-transition/src/signatureSets/voluntaryExits.ts @@ -15,7 +15,7 @@ export function verifyVoluntaryExitSignature( stateSlot: Slot, signedVoluntaryExit: phase0.SignedVoluntaryExit ): boolean { - return verifySignatureSet(getVoluntaryExitSignatureSet(config, index2pubkey, stateSlot, signedVoluntaryExit)); + return verifySignatureSet(getVoluntaryExitSignatureSet(config, stateSlot, signedVoluntaryExit), index2pubkey); } /** @@ -23,7 +23,6 @@ export function verifyVoluntaryExitSignature( */ export function getVoluntaryExitSignatureSet( config: BeaconConfig, - index2pubkey: Index2PubkeyCache, stateSlot: Slot, signedVoluntaryExit: phase0.SignedVoluntaryExit ): ISignatureSet { @@ -31,22 +30,18 @@ export function getVoluntaryExitSignatureSet( const domain = config.getDomainForVoluntaryExit(stateSlot, messageSlot); return { - type: SignatureSetType.single, - pubkey: index2pubkey[signedVoluntaryExit.message.validatorIndex], + type: SignatureSetType.indexed, + index: signedVoluntaryExit.message.validatorIndex, signingRoot: computeSigningRoot(ssz.phase0.VoluntaryExit, signedVoluntaryExit.message, domain), signature: signedVoluntaryExit.signature, }; } -export function getVoluntaryExitsSignatureSets( - config: BeaconConfig, - index2pubkey: Index2PubkeyCache, - signedBlock: SignedBeaconBlock -): ISignatureSet[] { +export function getVoluntaryExitsSignatureSets(config: BeaconConfig, signedBlock: SignedBeaconBlock): ISignatureSet[] { // the getDomain() api requires the state slot as 1st param, however it's the same to block.slot in state-transition // and the same epoch when we verify blocks in batch in beacon-node. So we can safely use block.slot here. const blockSlot = signedBlock.message.slot; return signedBlock.message.body.voluntaryExits.map((voluntaryExit) => - getVoluntaryExitSignatureSet(config, index2pubkey, blockSlot, voluntaryExit) + getVoluntaryExitSignatureSet(config, blockSlot, voluntaryExit) ); } diff --git a/packages/state-transition/src/slot/index.ts b/packages/state-transition/src/slot/index.ts index e3afefccd143..e373343fda3c 100644 --- a/packages/state-transition/src/slot/index.ts +++ b/packages/state-transition/src/slot/index.ts @@ -1,5 +1,5 @@ -import {byteArrayEquals} from "@chainsafe/ssz"; import {ForkSeq, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; +import {byteArrayEquals} from "@lodestar/utils"; import {ZERO_HASH} from "../constants/index.js"; import {CachedBeaconStateAllForks, CachedBeaconStateGloas} from "../types.js"; diff --git a/packages/state-transition/src/types.ts b/packages/state-transition/src/types.ts index 6dcf3e8b06f1..02a84f2b4c08 100644 --- a/packages/state-transition/src/types.ts +++ b/packages/state-transition/src/types.ts @@ -23,4 +23,5 @@ export type { BeaconStateFulu, BeaconStateGloas, BeaconStatePhase0, + ShufflingGetter, } from "./cache/types.js"; diff --git a/packages/state-transition/src/util/calculateCommitteeAssignments.ts b/packages/state-transition/src/util/calculateCommitteeAssignments.ts deleted file mode 100644 index 008161afa04b..000000000000 --- a/packages/state-transition/src/util/calculateCommitteeAssignments.ts +++ /dev/null @@ -1,43 +0,0 @@ -import {SLOTS_PER_EPOCH} from "@lodestar/params"; -import {CommitteeIndex, Slot, ValidatorIndex} from "@lodestar/types"; -import {EpochShuffling} from "./epochShuffling.js"; - -// Copied from lodestar-api package to avoid depending on the package -export interface AttesterDuty { - validatorIndex: ValidatorIndex; - committeeIndex: CommitteeIndex; - committeeLength: number; - committeesAtSlot: number; - validatorCommitteeIndex: number; - slot: Slot; -} - -export function calculateCommitteeAssignments( - epochShuffling: EpochShuffling, - requestedValidatorIndices: ValidatorIndex[] -): Map { - const requestedValidatorIndicesSet = new Set(requestedValidatorIndices); - const duties = new Map(); - - const epochCommittees = epochShuffling.committees; - for (let epochSlot = 0; epochSlot < SLOTS_PER_EPOCH; epochSlot++) { - const slotCommittees = epochCommittees[epochSlot]; - for (let i = 0, committeesAtSlot = slotCommittees.length; i < committeesAtSlot; i++) { - for (let j = 0, committeeLength = slotCommittees[i].length; j < committeeLength; j++) { - const validatorIndex = slotCommittees[i][j]; - if (requestedValidatorIndicesSet.has(validatorIndex)) { - duties.set(validatorIndex, { - validatorIndex, - committeeLength, - committeesAtSlot, - validatorCommitteeIndex: j, - committeeIndex: i, - slot: epochShuffling.epoch * SLOTS_PER_EPOCH + epochSlot, - }); - } - } - } - } - - return duties; -} diff --git a/packages/state-transition/src/util/electra.ts b/packages/state-transition/src/util/electra.ts index 0932c985aef0..9a9165fec886 100644 --- a/packages/state-transition/src/util/electra.ts +++ b/packages/state-transition/src/util/electra.ts @@ -3,12 +3,9 @@ import {ValidatorIndex, ssz} from "@lodestar/types"; import {G2_POINT_AT_INFINITY} from "../constants/constants.js"; import {CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js"; import {hasEth1WithdrawalCredential} from "./capella.js"; -import {hasBuilderWithdrawalCredential} from "./gloas.ts"; export function hasCompoundingWithdrawalCredential(withdrawalCredentials: Uint8Array): boolean { - return ( - withdrawalCredentials[0] === COMPOUNDING_WITHDRAWAL_PREFIX || hasBuilderWithdrawalCredential(withdrawalCredentials) - ); + return withdrawalCredentials[0] === COMPOUNDING_WITHDRAWAL_PREFIX; } export function hasExecutionWithdrawalCredential(withdrawalCredentials: Uint8Array): boolean { diff --git a/packages/state-transition/src/util/epochShuffling.ts b/packages/state-transition/src/util/epochShuffling.ts index 6d0acbd32455..24880ec83adf 100644 --- a/packages/state-transition/src/util/epochShuffling.ts +++ b/packages/state-transition/src/util/epochShuffling.ts @@ -9,54 +9,13 @@ import { TARGET_COMMITTEE_SIZE, } from "@lodestar/params"; import {Epoch, RootHex, ValidatorIndex, ssz} from "@lodestar/types"; -import {GaugeExtra, Logger, NoLabels, intDiv, toRootHex} from "@lodestar/utils"; +import {intDiv, toRootHex} from "@lodestar/utils"; import {BeaconStateAllForks} from "../types.js"; import {getBlockRootAtSlot} from "./blockRoot.js"; import {computeAnchorCheckpoint} from "./computeAnchorCheckpoint.js"; import {computeStartSlotAtEpoch} from "./epoch.js"; import {getSeed} from "./seed.js"; -export interface ShufflingBuildProps { - state: BeaconStateAllForks; - activeIndices: Uint32Array; -} - -export interface PublicShufflingCacheMetrics { - shufflingCache: { - nextShufflingNotOnEpochCache: GaugeExtra; - }; -} -export interface IShufflingCache { - metrics: PublicShufflingCacheMetrics | null; - logger: Logger | null; - /** - * Gets a cached shuffling via the epoch and decision root. If the state and - * activeIndices are passed and a shuffling is not available it will be built - * synchronously. If the state is not passed and the shuffling is not available - * nothing will be returned. - * - * NOTE: If a shuffling is already queued and not calculated it will build and resolve - * the promise but the already queued build will happen at some later time - */ - getSync( - epoch: Epoch, - decisionRoot: RootHex, - buildProps?: T - ): T extends ShufflingBuildProps ? EpochShuffling : EpochShuffling | null; - - /** - * Gets a cached shuffling via the epoch and decision root. Returns a promise - * for the shuffling if it hs not calculated yet. Returns null if a build has - * not been queued nor a shuffling was calculated. - */ - get(epoch: Epoch, decisionRoot: RootHex): Promise; - - /** - * Queue asynchronous build for an EpochShuffling - */ - build(epoch: Epoch, decisionRoot: RootHex, state: BeaconStateAllForks, activeIndices: Uint32Array): void; -} - /** * Readonly interface for EpochShuffling. */ @@ -164,7 +123,7 @@ export async function computeEpochShufflingAsync( }; } -function calculateDecisionRoot(state: BeaconStateAllForks, epoch: Epoch): RootHex { +export function calculateDecisionRoot(state: BeaconStateAllForks, epoch: Epoch): RootHex { const pivotSlot = computeStartSlotAtEpoch(epoch - 1) - 1; return toRootHex(getBlockRootAtSlot(state, pivotSlot)); } diff --git a/packages/state-transition/src/util/gloas.ts b/packages/state-transition/src/util/gloas.ts index ab82bda18a3c..716a900cfd3e 100644 --- a/packages/state-transition/src/util/gloas.ts +++ b/packages/state-transition/src/util/gloas.ts @@ -1,19 +1,22 @@ -import {byteArrayEquals} from "@chainsafe/ssz"; import { + BUILDER_INDEX_FLAG, BUILDER_PAYMENT_THRESHOLD_DENOMINATOR, BUILDER_PAYMENT_THRESHOLD_NUMERATOR, BUILDER_WITHDRAWAL_PREFIX, EFFECTIVE_BALANCE_INCREMENT, + FAR_FUTURE_EPOCH, + MIN_DEPOSIT_AMOUNT, SLOTS_PER_EPOCH, } from "@lodestar/params"; -import {gloas} from "@lodestar/types"; +import {BuilderIndex, Epoch, ValidatorIndex, gloas} from "@lodestar/types"; import {AttestationData} from "@lodestar/types/phase0"; -import {CachedBeaconStateGloas} from "../types.ts"; -import {getBlockRootAtSlot} from "./blockRoot.ts"; -import {computeEpochAtSlot} from "./epoch.ts"; -import {RootCache} from "./rootCache.ts"; +import {byteArrayEquals} from "@lodestar/utils"; +import {CachedBeaconStateGloas} from "../types.js"; +import {getBlockRootAtSlot} from "./blockRoot.js"; +import {computeEpochAtSlot} from "./epoch.js"; +import {RootCache} from "./rootCache.js"; -export function hasBuilderWithdrawalCredential(withdrawalCredentials: Uint8Array): boolean { +export function isBuilderWithdrawalCredential(withdrawalCredentials: Uint8Array): boolean { return withdrawalCredentials[0] === BUILDER_WITHDRAWAL_PREFIX; } @@ -25,14 +28,117 @@ export function getBuilderPaymentQuorumThreshold(state: CachedBeaconStateGloas): return Math.floor(quorum / BUILDER_PAYMENT_THRESHOLD_DENOMINATOR); } -export function isBuilderPaymentWithdrawable( +/** + * Check if a validator index represents a builder (has the builder flag set). + * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-is_builder_index + */ +export function isBuilderIndex(validatorIndex: number): boolean { + return (validatorIndex & BUILDER_INDEX_FLAG) !== 0; +} + +/** + * Convert a builder index to a flagged validator index for use in Withdrawal containers. + * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-convert_builder_index_to_validator_index + */ +export function convertBuilderIndexToValidatorIndex(builderIndex: BuilderIndex): ValidatorIndex { + return builderIndex | BUILDER_INDEX_FLAG; +} + +/** + * Convert a flagged validator index back to a builder index. + * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-convert_validator_index_to_builder_index + */ +export function convertValidatorIndexToBuilderIndex(validatorIndex: ValidatorIndex): BuilderIndex { + return validatorIndex & ~BUILDER_INDEX_FLAG; +} + +/** + * Check if a builder is active (deposited and not yet withdrawable). + * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#isactivebuilder + */ +export function isActiveBuilder(builder: gloas.Builder, finalizedEpoch: Epoch): boolean { + return builder.depositEpoch < finalizedEpoch && builder.withdrawableEpoch === FAR_FUTURE_EPOCH; +} + +/** + * Get the total pending balance to withdraw for a builder (from withdrawals + payments). + * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-get_pending_balance_to_withdraw_for_builder + */ +export function getPendingBalanceToWithdrawForBuilder( state: CachedBeaconStateGloas, - withdrawal: gloas.BuilderPendingWithdrawal + builderIndex: BuilderIndex +): number { + let pendingBalance = 0; + + // Sum pending withdrawals + for (let i = 0; i < state.builderPendingWithdrawals.length; i++) { + const withdrawal = state.builderPendingWithdrawals.getReadonly(i); + if (withdrawal.builderIndex === builderIndex) { + pendingBalance += withdrawal.amount; + } + } + + // Sum pending payments + for (let i = 0; i < state.builderPendingPayments.length; i++) { + const payment = state.builderPendingPayments.getReadonly(i); + if (payment.withdrawal.builderIndex === builderIndex) { + pendingBalance += payment.withdrawal.amount; + } + } + + return pendingBalance; +} + +/** + * Check if a builder has sufficient balance to cover a bid amount. + * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-can_builder_cover_bid + */ +export function canBuilderCoverBid( + state: CachedBeaconStateGloas, + builderIndex: BuilderIndex, + bidAmount: number ): boolean { - const builder = state.validators.getReadonly(withdrawal.builderIndex); + const builder = state.builders.getReadonly(builderIndex); + const pendingBalance = getPendingBalanceToWithdrawForBuilder(state, builderIndex); + const minBalance = MIN_DEPOSIT_AMOUNT + pendingBalance; + + if (builder.balance < minBalance) { + return false; + } + + return builder.balance - minBalance >= bidAmount; +} + +/** + * Initiate a builder exit by setting their withdrawable epoch. + * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-initiate_builder_exit + */ +export function initiateBuilderExit(state: CachedBeaconStateGloas, builderIndex: BuilderIndex): void { + const builder = state.builders.get(builderIndex); + + // Return if builder already initiated exit + if (builder.withdrawableEpoch !== FAR_FUTURE_EPOCH) { + return; + } + + // Set builder exit epoch const currentEpoch = computeEpochAtSlot(state.slot); + builder.withdrawableEpoch = currentEpoch + state.config.MIN_BUILDER_WITHDRAWABILITY_DELAY; +} - return builder.withdrawableEpoch >= currentEpoch || !builder.slashed; +/** + * Find the index of a builder by their public key. + * Returns null if not found. + * + * May consider builder pubkey cache if performance becomes an issue. + */ +export function findBuilderIndexByPubkey(state: CachedBeaconStateGloas, pubkey: Uint8Array): BuilderIndex | null { + for (let i = 0; i < state.builders.length; i++) { + if (byteArrayEquals(state.builders.getReadonly(i).pubkey, pubkey)) { + return i; + } + } + return null; } export function isAttestationSameSlot(state: CachedBeaconStateGloas, data: AttestationData): boolean { diff --git a/packages/state-transition/src/util/index.ts b/packages/state-transition/src/util/index.ts index 4754406c7f2f..bdbff4c11074 100644 --- a/packages/state-transition/src/util/index.ts +++ b/packages/state-transition/src/util/index.ts @@ -5,7 +5,6 @@ export * from "./attesterStatus.js"; export * from "./balance.js"; export * from "./blindedBlock.js"; export * from "./blockRoot.js"; -export * from "./calculateCommitteeAssignments.js"; export * from "./capella.js"; export * from "./computeAnchorCheckpoint.js"; export * from "./deposit.js"; @@ -16,11 +15,12 @@ export * from "./epochShuffling.js"; export * from "./execution.js"; export * from "./finality.js"; export * from "./genesis.js"; +export * from "./gloas.js"; export * from "./interop.js"; export * from "./loadState/index.js"; export * from "./rootCache.js"; export * from "./seed.js"; -export * from "./shufflingDecisionRoot.js"; +export * from "./shuffling.js"; export * from "./signatureSets.js"; export * from "./signingRoot.js"; export * from "./slot.js"; diff --git a/packages/state-transition/src/util/interop.ts b/packages/state-transition/src/util/interop.ts index e99799da127d..8325698b9561 100644 --- a/packages/state-transition/src/util/interop.ts +++ b/packages/state-transition/src/util/interop.ts @@ -1,4 +1,4 @@ -import {toBufferBE} from "bigint-buffer"; +import {toBufferBE} from "@vekexasia/bigint-buffer2"; import {digest} from "@chainsafe/as-sha256"; import {SecretKey} from "@chainsafe/blst"; import {bytesToBigInt, intToBytes} from "@lodestar/utils"; diff --git a/packages/state-transition/src/util/loadState/findModifiedInactivityScores.ts b/packages/state-transition/src/util/loadState/findModifiedInactivityScores.ts index f76e4dc650dc..38993ff79b4c 100644 --- a/packages/state-transition/src/util/loadState/findModifiedInactivityScores.ts +++ b/packages/state-transition/src/util/loadState/findModifiedInactivityScores.ts @@ -1,9 +1,11 @@ +import {byteArrayEquals} from "@lodestar/utils"; + // UintNum64 = 8 bytes export const INACTIVITY_SCORE_SIZE = 8; /** * As monitored on mainnet, inactivityScores are not changed much and they are mostly 0 - * Using Buffer.compare is the fastest way as noted in `./findModifiedValidators.ts` + * Using byteArrayEquals is the optimal way as noted in `./findModifiedValidators.ts` * @returns output parameter modifiedValidators: validator indices that are modified */ export function findModifiedInactivityScores( @@ -21,7 +23,7 @@ export function findModifiedInactivityScores( ); } - if (Buffer.compare(inactivityScoresBytes, inactivityScoresBytes2) === 0) { + if (byteArrayEquals(inactivityScoresBytes, inactivityScoresBytes2)) { return; } diff --git a/packages/state-transition/src/util/loadState/findModifiedValidators.ts b/packages/state-transition/src/util/loadState/findModifiedValidators.ts index b47789f42b47..869f320dc984 100644 --- a/packages/state-transition/src/util/loadState/findModifiedValidators.ts +++ b/packages/state-transition/src/util/loadState/findModifiedValidators.ts @@ -1,10 +1,11 @@ +import {byteArrayEquals} from "@lodestar/utils"; import {VALIDATOR_BYTES_SIZE} from "../sszBytes.js"; /** - * Find modified validators by comparing two validators bytes using Buffer.compare() recursively + * Find modified validators by comparing two validators bytes using byteArrayEquals() recursively * - As noted in packages/state-transition/test/perf/util/loadState/findModifiedValidators.test.ts, serializing validators and compare Uint8Array is the fastest way * - The performance is quite stable and can afford a lot of difference in validators (the benchmark tested up to 10k but it's not likely we have that difference in mainnet) - * - Also packages/state-transition/test/perf/misc/byteArrayEquals.test.ts shows that Buffer.compare() is very efficient for large Uint8Array + * - byteArrayEquals() uses the optimal comparison method based on array size * * @returns output parameter modifiedValidators: validator indices that are modified */ @@ -20,7 +21,7 @@ export function findModifiedValidators( ); } - if (Buffer.compare(validatorsBytes, validatorsBytes2) === 0) { + if (byteArrayEquals(validatorsBytes, validatorsBytes2)) { return; } diff --git a/packages/state-transition/src/util/loadState/loadValidator.ts b/packages/state-transition/src/util/loadState/loadValidator.ts index dffbd091d256..bac8c072cb62 100644 --- a/packages/state-transition/src/util/loadState/loadValidator.ts +++ b/packages/state-transition/src/util/loadState/loadValidator.ts @@ -1,6 +1,7 @@ import {CompositeViewDU} from "@chainsafe/ssz"; import {ChainForkConfig} from "@lodestar/config"; import {ValidatorIndex, deserializeContainerIgnoreFields, ssz} from "@lodestar/types"; +import {byteArrayEquals} from "@lodestar/utils"; import {getStateTypeFromBytes} from "../sszBytes.js"; /** @@ -32,12 +33,12 @@ function getSameFields( ): ("pubkey" | "withdrawalCredentials")[] { const ignoredFields: ("pubkey" | "withdrawalCredentials")[] = []; const pubkey = validatorBytes.subarray(0, 48); - if (Buffer.compare(pubkey, validator.pubkey) === 0) { + if (byteArrayEquals(pubkey, validator.pubkey)) { ignoredFields.push("pubkey"); } const withdrawalCredentials = validatorBytes.subarray(48, 80); - if (Buffer.compare(withdrawalCredentials, validator.withdrawalCredentials) === 0) { + if (byteArrayEquals(withdrawalCredentials, validator.withdrawalCredentials)) { ignoredFields.push("withdrawalCredentials"); } diff --git a/packages/state-transition/src/util/shuffling.ts b/packages/state-transition/src/util/shuffling.ts new file mode 100644 index 000000000000..402ca070e592 --- /dev/null +++ b/packages/state-transition/src/util/shuffling.ts @@ -0,0 +1,234 @@ +import {ForkName, ForkSeq, SLOTS_PER_EPOCH, isForkPostFulu} from "@lodestar/params"; +import { + Attestation, + CommitteeIndex, + Epoch, + IndexedAttestation, + Root, + Slot, + ValidatorIndex, + electra, +} from "@lodestar/types"; +import {LodestarError} from "@lodestar/utils"; +import {CachedBeaconStateAllForks} from "../cache/stateCache.js"; +import {getBlockRootAtSlot} from "./blockRoot.js"; +import {computeStartSlotAtEpoch} from "./epoch.js"; +import {EpochShuffling} from "./epochShuffling.js"; + +/** + * Returns the block root which decided the proposer shuffling for the current epoch. This root + * can be used to key this proposer shuffling. + * + * Returns `null` on the one-off scenario where the genesis block decides its own shuffling. + * It should be set to the latest block applied to this `state` or the genesis block root. + */ +export function proposerShufflingDecisionRoot(fork: ForkName, state: CachedBeaconStateAllForks): Root | null { + const decisionSlot = proposerShufflingDecisionSlot(fork, state); + if (state.slot === decisionSlot) { + return null; + } + return getBlockRootAtSlot(state, decisionSlot); +} + +/** + * Returns the slot at which the proposer shuffling was decided. The block root at this slot + * can be used to key the proposer shuffling for the current epoch. + */ +function proposerShufflingDecisionSlot(fork: ForkName, state: CachedBeaconStateAllForks): Slot { + // After fulu, the decision slot is in previous epoch due to deterministic proposer lookahead + const epoch = isForkPostFulu(fork) ? state.epochCtx.epoch - 1 : state.epochCtx.epoch; + const startSlot = computeStartSlotAtEpoch(epoch); + return Math.max(startSlot - 1, 0); +} + +/** + * Returns the block root which decided the attester shuffling for the given `requestedEpoch`. + * This root can be used to key that attester shuffling. + * + * Returns `null` on the one-off scenario where the genesis block decides its own shuffling. + * It should be set to the latest block applied to this `state` or the genesis block root. + */ +export function attesterShufflingDecisionRoot(state: CachedBeaconStateAllForks, requestedEpoch: Epoch): Root | null { + const decisionSlot = attesterShufflingDecisionSlot(state, requestedEpoch); + if (state.slot === decisionSlot) { + return null; + } + return getBlockRootAtSlot(state, decisionSlot); +} + +/** + * Returns the slot at which the proposer shuffling was decided. The block root at this slot + * can be used to key the proposer shuffling for the current epoch. + */ +function attesterShufflingDecisionSlot(state: CachedBeaconStateAllForks, requestedEpoch: Epoch): Slot { + const epoch = attesterShufflingDecisionEpoch(state, requestedEpoch); + const slot = computeStartSlotAtEpoch(epoch); + return Math.max(slot - 1, 0); +} + +/** + * Returns the epoch at which the attester shuffling was decided. + * + * Spec ref: https://github.com/ethereum/beacon-APIs/blob/v2.1.0/apis/validator/duties/attester.yaml#L15 + * + * Throws an error when: + * - `EpochTooLow` when `requestedEpoch` is more than 1 prior to `currentEpoch`. + * - `EpochTooHigh` when `requestedEpoch` is more than 1 after `currentEpoch`. + */ +function attesterShufflingDecisionEpoch(state: CachedBeaconStateAllForks, requestedEpoch: Epoch): Epoch { + const currentEpoch = state.epochCtx.epoch; + + // Next + if (requestedEpoch === currentEpoch + 1) return currentEpoch; + // Current + if (requestedEpoch === currentEpoch) return Math.max(currentEpoch - 1, 0); + // Previous + if (requestedEpoch === currentEpoch - 1) return Math.max(currentEpoch - 2, 0); + + if (requestedEpoch < currentEpoch) { + throw Error(`EpochTooLow: current ${currentEpoch} requested ${requestedEpoch}`); + } + throw Error(`EpochTooHigh: current ${currentEpoch} requested ${requestedEpoch}`); +} + +// Copied from lodestar-api package to avoid depending on the package +export interface AttesterDuty { + validatorIndex: ValidatorIndex; + committeeIndex: CommitteeIndex; + committeeLength: number; + committeesAtSlot: number; + validatorCommitteeIndex: number; + slot: Slot; +} + +export function calculateCommitteeAssignments( + epochShuffling: EpochShuffling, + requestedValidatorIndices: ValidatorIndex[] +): Map { + const requestedValidatorIndicesSet = new Set(requestedValidatorIndices); + const duties = new Map(); + + const epochCommittees = epochShuffling.committees; + for (let epochSlot = 0; epochSlot < SLOTS_PER_EPOCH; epochSlot++) { + const slotCommittees = epochCommittees[epochSlot]; + for (let i = 0, committeesAtSlot = slotCommittees.length; i < committeesAtSlot; i++) { + for (let j = 0, committeeLength = slotCommittees[i].length; j < committeeLength; j++) { + const validatorIndex = slotCommittees[i][j]; + if (requestedValidatorIndicesSet.has(validatorIndex)) { + duties.set(validatorIndex, { + validatorIndex, + committeeLength, + committeesAtSlot, + validatorCommitteeIndex: j, + committeeIndex: i, + slot: epochShuffling.epoch * SLOTS_PER_EPOCH + epochSlot, + }); + } + } + } + } + + return duties; +} + +/** + * Return the indexed attestation corresponding to ``attestation``. + */ +export function getIndexedAttestation( + epochShuffling: EpochShuffling, + fork: ForkSeq, + attestation: Attestation +): IndexedAttestation { + const {data} = attestation; + const attestingIndices = getAttestingIndices(epochShuffling, fork, attestation); + + // sort in-place + attestingIndices.sort((a, b) => a - b); + return { + attestingIndices: attestingIndices, + data: data, + signature: attestation.signature, + }; +} + +/** + * Return indices of validators who attestested in `attestation` + */ +export function getAttestingIndices(epochShuffling: EpochShuffling, fork: ForkSeq, attestation: Attestation): number[] { + if (fork < ForkSeq.electra) { + const {aggregationBits, data} = attestation; + const validatorIndices = getBeaconCommittee(epochShuffling, data.slot, data.index); + + return aggregationBits.intersectValues(validatorIndices); + } + const {aggregationBits, committeeBits, data} = attestation as electra.Attestation; + + // There is a naming conflict on the term `committeeIndices` + // In Lodestar it usually means a list of validator indices of participants in a committee + // In the spec it means a list of committee indices according to committeeBits + // This `committeeIndices` refers to the latter + // TODO Electra: resolve the naming conflicts + const committeeIndices = committeeBits.getTrueBitIndexes(); + + const validatorsByCommittee = getBeaconCommittees(epochShuffling, data.slot, committeeIndices); + + // Create a new Uint32Array to flatten `validatorsByCommittee` + const totalLength = validatorsByCommittee.reduce((acc, curr) => acc + curr.length, 0); + const committeeValidators = new Uint32Array(totalLength); + + let offset = 0; + for (const committee of validatorsByCommittee) { + committeeValidators.set(committee, offset); + offset += committee.length; + } + + return aggregationBits.intersectValues(committeeValidators); +} + +/** + * Return the beacon committee at slot for index. + */ +export function getBeaconCommittee(epochShuffling: EpochShuffling, slot: Slot, index: CommitteeIndex): Uint32Array { + return getBeaconCommittees(epochShuffling, slot, [index])[0]; +} + +/** + * Return a Uint32Array[] representing committees validator indices + */ +export function getBeaconCommittees( + epochShuffling: EpochShuffling, + slot: Slot, + indices: CommitteeIndex[] +): Uint32Array[] { + if (indices.length === 0) { + throw new Error("Attempt to get committees without providing CommitteeIndex"); + } + + const slotCommittees = epochShuffling.committees[slot % SLOTS_PER_EPOCH]; + const committees = []; + + for (const index of indices) { + if (index >= slotCommittees.length) { + throw new ShufflingError({ + code: ShufflingErrorCode.COMMITTEE_INDEX_OUT_OF_RANGE, + index, + maxIndex: slotCommittees.length, + }); + } + committees.push(slotCommittees[index]); + } + + return committees; +} + +export enum ShufflingErrorCode { + COMMITTEE_INDEX_OUT_OF_RANGE = "SHUFFLING_ERROR_COMMITTEE_INDEX_OUT_OF_RANGE", +} + +type ShufflingErrorType = { + code: ShufflingErrorCode.COMMITTEE_INDEX_OUT_OF_RANGE; + index: number; + maxIndex: number; +}; + +export class ShufflingError extends LodestarError {} diff --git a/packages/state-transition/src/util/shufflingDecisionRoot.ts b/packages/state-transition/src/util/shufflingDecisionRoot.ts deleted file mode 100644 index 2857fea36bdf..000000000000 --- a/packages/state-transition/src/util/shufflingDecisionRoot.ts +++ /dev/null @@ -1,81 +0,0 @@ -import {ForkName, isForkPostFulu} from "@lodestar/params"; -import {Epoch, Root, Slot} from "@lodestar/types"; -import {CachedBeaconStateAllForks} from "../types.js"; -import {getBlockRootAtSlot} from "./blockRoot.js"; -import {computeStartSlotAtEpoch} from "./epoch.js"; - -/** - * Returns the block root which decided the proposer shuffling for the current epoch. This root - * can be used to key this proposer shuffling. - * - * Returns `null` on the one-off scenario where the genesis block decides its own shuffling. - * It should be set to the latest block applied to this `state` or the genesis block root. - */ -export function proposerShufflingDecisionRoot(fork: ForkName, state: CachedBeaconStateAllForks): Root | null { - const decisionSlot = proposerShufflingDecisionSlot(fork, state); - if (state.slot === decisionSlot) { - return null; - } - return getBlockRootAtSlot(state, decisionSlot); -} - -/** - * Returns the slot at which the proposer shuffling was decided. The block root at this slot - * can be used to key the proposer shuffling for the current epoch. - */ -function proposerShufflingDecisionSlot(fork: ForkName, state: CachedBeaconStateAllForks): Slot { - // After fulu, the decision slot is in previous epoch due to deterministic proposer lookahead - const epoch = isForkPostFulu(fork) ? state.epochCtx.epoch - 1 : state.epochCtx.epoch; - const startSlot = computeStartSlotAtEpoch(epoch); - return Math.max(startSlot - 1, 0); -} - -/** - * Returns the block root which decided the attester shuffling for the given `requestedEpoch`. - * This root can be used to key that attester shuffling. - * - * Returns `null` on the one-off scenario where the genesis block decides its own shuffling. - * It should be set to the latest block applied to this `state` or the genesis block root. - */ -export function attesterShufflingDecisionRoot(state: CachedBeaconStateAllForks, requestedEpoch: Epoch): Root | null { - const decisionSlot = attesterShufflingDecisionSlot(state, requestedEpoch); - if (state.slot === decisionSlot) { - return null; - } - return getBlockRootAtSlot(state, decisionSlot); -} - -/** - * Returns the slot at which the proposer shuffling was decided. The block root at this slot - * can be used to key the proposer shuffling for the current epoch. - */ -function attesterShufflingDecisionSlot(state: CachedBeaconStateAllForks, requestedEpoch: Epoch): Slot { - const epoch = attesterShufflingDecisionEpoch(state, requestedEpoch); - const slot = computeStartSlotAtEpoch(epoch); - return Math.max(slot - 1, 0); -} - -/** - * Returns the epoch at which the attester shuffling was decided. - * - * Spec ref: https://github.com/ethereum/beacon-APIs/blob/v2.1.0/apis/validator/duties/attester.yaml#L15 - * - * Throws an error when: - * - `EpochTooLow` when `requestedEpoch` is more than 1 prior to `currentEpoch`. - * - `EpochTooHigh` when `requestedEpoch` is more than 1 after `currentEpoch`. - */ -function attesterShufflingDecisionEpoch(state: CachedBeaconStateAllForks, requestedEpoch: Epoch): Epoch { - const currentEpoch = state.epochCtx.epoch; - - // Next - if (requestedEpoch === currentEpoch + 1) return currentEpoch; - // Current - if (requestedEpoch === currentEpoch) return Math.max(currentEpoch - 1, 0); - // Previous - if (requestedEpoch === currentEpoch - 1) return Math.max(currentEpoch - 2, 0); - - if (requestedEpoch < currentEpoch) { - throw Error(`EpochTooLow: current ${currentEpoch} requested ${requestedEpoch}`); - } - throw Error(`EpochTooHigh: current ${currentEpoch} requested ${requestedEpoch}`); -} diff --git a/packages/state-transition/src/util/signatureSets.ts b/packages/state-transition/src/util/signatureSets.ts index d5a62c624b91..eb5a7e41977a 100644 --- a/packages/state-transition/src/util/signatureSets.ts +++ b/packages/state-transition/src/util/signatureSets.ts @@ -1,11 +1,21 @@ -import {PublicKey, Signature, fastAggregateVerify, verify} from "@chainsafe/blst"; +import {PublicKey, Signature, aggregatePublicKeys, fastAggregateVerify, verify} from "@chainsafe/blst"; import {Root} from "@lodestar/types"; +import {Index2PubkeyCache} from "../cache/pubkeyCache.js"; export enum SignatureSetType { single = "single", aggregate = "aggregate", + /** + * Single signature with validator index instead of pubkey. + * Pubkey lookup is deferred to verification time. + */ + indexed = "indexed", } +/** + * Single signature with pubkey directly. + * Used when pubkey comes from the message itself (e.g. BLS to execution change). + */ export type SingleSignatureSet = { type: SignatureSetType.single; pubkey: PublicKey; @@ -13,16 +23,57 @@ export type SingleSignatureSet = { signature: Uint8Array; }; +/** + * Single signature with validator index. + * Pubkey is looked up at verification time. + */ +export type IndexedSignatureSet = { + type: SignatureSetType.indexed; + index: number; + signingRoot: Root; + signature: Uint8Array; +}; + +/** + * Aggregate signature with validator indices. + * Pubkeys are looked up and aggregated at verification time. + */ export type AggregatedSignatureSet = { type: SignatureSetType.aggregate; - pubkeys: PublicKey[]; + indices: number[]; signingRoot: Root; signature: Uint8Array; }; -export type ISignatureSet = SingleSignatureSet | AggregatedSignatureSet; +export type ISignatureSet = SingleSignatureSet | IndexedSignatureSet | AggregatedSignatureSet; + +/** + * Get the pubkey for a signature set, performing aggregation if necessary. + * Requires index2pubkey cache for indexed and aggregate sets. + */ +export function getSignatureSetPubkey(signatureSet: ISignatureSet, index2pubkey: Index2PubkeyCache): PublicKey { + switch (signatureSet.type) { + case SignatureSetType.single: + return signatureSet.pubkey; + + case SignatureSetType.indexed: + return index2pubkey[signatureSet.index]; + + case SignatureSetType.aggregate: { + const pubkeys = signatureSet.indices.map((i) => index2pubkey[i]); + return aggregatePublicKeys(pubkeys); + } -export function verifySignatureSet(signatureSet: ISignatureSet): boolean { + default: + throw Error("Unknown signature set type"); + } +} + +export function verifySignatureSet(signatureSet: SingleSignatureSet, index2pubkey?: Index2PubkeyCache): boolean; +export function verifySignatureSet(signatureSet: IndexedSignatureSet, index2pubkey: Index2PubkeyCache): boolean; +export function verifySignatureSet(signatureSet: AggregatedSignatureSet, index2pubkey: Index2PubkeyCache): boolean; +export function verifySignatureSet(signatureSet: ISignatureSet, index2pubkey: Index2PubkeyCache): boolean; +export function verifySignatureSet(signatureSet: ISignatureSet, index2pubkey?: Index2PubkeyCache): boolean { // All signatures are not trusted and must be group checked (p2.subgroup_check) const signature = Signature.fromBytes(signatureSet.signature, true); @@ -30,8 +81,20 @@ export function verifySignatureSet(signatureSet: ISignatureSet): boolean { case SignatureSetType.single: return verify(signatureSet.signingRoot, signatureSet.pubkey, signature); - case SignatureSetType.aggregate: - return fastAggregateVerify(signatureSet.signingRoot, signatureSet.pubkeys, signature); + case SignatureSetType.indexed: { + if (!index2pubkey) { + throw Error("index2pubkey required for indexed signature set"); + } + return verify(signatureSet.signingRoot, index2pubkey[signatureSet.index], signature); + } + + case SignatureSetType.aggregate: { + if (!index2pubkey) { + throw Error("index2pubkey required for aggregate signature set"); + } + const pubkeys = signatureSet.indices.map((i) => index2pubkey[i]); + return fastAggregateVerify(signatureSet.signingRoot, pubkeys, signature); + } default: throw Error("Unknown signature set type"); @@ -51,14 +114,27 @@ export function createSingleSignatureSetFromComponents( }; } +export function createIndexedSignatureSetFromComponents( + index: number, + signingRoot: Root, + signature: Uint8Array +): IndexedSignatureSet { + return { + type: SignatureSetType.indexed, + index, + signingRoot, + signature, + }; +} + export function createAggregateSignatureSetFromComponents( - pubkeys: PublicKey[], + indices: number[], signingRoot: Root, signature: Uint8Array ): AggregatedSignatureSet { return { type: SignatureSetType.aggregate, - pubkeys, + indices, signingRoot, signature, }; diff --git a/packages/state-transition/src/util/validator.ts b/packages/state-transition/src/util/validator.ts index 59824def8808..64e5f698bb98 100644 --- a/packages/state-transition/src/util/validator.ts +++ b/packages/state-transition/src/util/validator.ts @@ -2,13 +2,15 @@ import {ChainForkConfig} from "@lodestar/config"; import { EFFECTIVE_BALANCE_INCREMENT, ForkSeq, + MAX_EFFECTIVE_BALANCE, MAX_EFFECTIVE_BALANCE_ELECTRA, MIN_ACTIVATION_BALANCE, } from "@lodestar/params"; import {Epoch, ValidatorIndex, phase0} from "@lodestar/types"; import {intDiv} from "@lodestar/utils"; import {BeaconStateAllForks, CachedBeaconStateElectra, CachedBeaconStateGloas, EpochCache} from "../types.js"; -import {hasCompoundingWithdrawalCredential} from "./electra.js"; +import {hasEth1WithdrawalCredential} from "./capella.js"; +import {hasCompoundingWithdrawalCredential, hasExecutionWithdrawalCredential} from "./electra.js"; /** * Check if [[validator]] is active @@ -94,8 +96,35 @@ export function getMaxEffectiveBalance(withdrawalCredentials: Uint8Array): numbe return MIN_ACTIVATION_BALANCE; } +/** + * Check if validator is partially withdrawable. + * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/electra/beacon-chain.md#modified-is_partially_withdrawable_validator + */ +export function isPartiallyWithdrawableValidator(fork: ForkSeq, validator: phase0.Validator, balance: number): boolean { + const isPostElectra = fork >= ForkSeq.electra; + + // Check withdrawal credentials + const hasWithdrawableCredentials = isPostElectra + ? hasExecutionWithdrawalCredential(validator.withdrawalCredentials) + : hasEth1WithdrawalCredential(validator.withdrawalCredentials); + + if (!hasWithdrawableCredentials) { + return false; + } + + // Get max effective balance based on fork + const maxEffectiveBalance = isPostElectra + ? getMaxEffectiveBalance(validator.withdrawalCredentials) + : MAX_EFFECTIVE_BALANCE; + + // Check if at max effective balance and has excess balance + const hasMaxEffectiveBalance = validator.effectiveBalance === maxEffectiveBalance; + const hasExcessBalance = balance > maxEffectiveBalance; + + return hasMaxEffectiveBalance && hasExcessBalance; +} + export function getPendingBalanceToWithdraw( - fork: ForkSeq, state: CachedBeaconStateElectra | CachedBeaconStateGloas, validatorIndex: ValidatorIndex ): number { @@ -106,19 +135,5 @@ export function getPendingBalanceToWithdraw( } } - if (fork >= ForkSeq.gloas) { - const stateGloas = state as CachedBeaconStateGloas; - for (const item of stateGloas.builderPendingWithdrawals.getAllReadonly()) { - if (item.builderIndex === validatorIndex) { - total += item.amount; - } - } - for (const item of stateGloas.builderPendingPayments.getAllReadonly()) { - if (item.withdrawal.builderIndex === validatorIndex) { - total += item.withdrawal.amount; - } - } - } - return total; } diff --git a/packages/state-transition/test/perf/misc/byteArrayEquals.test.ts b/packages/state-transition/test/perf/misc/byteArrayEquals.test.ts index bc4fdfc0183a..43a62be44ad3 100644 --- a/packages/state-transition/test/perf/misc/byteArrayEquals.test.ts +++ b/packages/state-transition/test/perf/misc/byteArrayEquals.test.ts @@ -1,29 +1,45 @@ import crypto from "node:crypto"; import {bench, describe} from "@chainsafe/benchmark"; -import {byteArrayEquals} from "@chainsafe/ssz"; +import {byteArrayEquals} from "@lodestar/utils"; import {generateState} from "../../utils/state.js"; import {generateValidators} from "../../utils/validator.js"; /** - * compare Uint8Array, the longer the array, the better performance Buffer.compare() is - * - with 32 bytes, Buffer.compare() is 1.5x faster (rootEquals.test.ts showed > 2x faster) - * ✔ byteArrayEquals 32 1.004480e+7 ops/s 99.55400 ns/op - 19199 runs 2.08 s - * ✔ Buffer.compare 32 1.553495e+7 ops/s 64.37100 ns/op - 3634 runs 0.303 s + * Original loop-based implementation from @chainsafe/ssz for benchmark comparison. + * This is what byteArrayEquals used to be before switching to Buffer.compare. + */ +function byteArrayEqualsLoop(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + return true; +} + +/** + * Compare loop-based byteArrayEquals (original @chainsafe/ssz implementation) + * vs hybrid byteArrayEquals (new @lodestar/utils implementation). + * + * Node v24.13.0 benchmark results: * - * - with 1024 bytes, Buffer.compare() is 21.8x faster - * ✔ byteArrayEquals 1024 379239.7 ops/s 2.636855 us/op - 117 runs 0.811 s - * ✔ Buffer.compare 1024 8269999 ops/s 120.9190 ns/op - 3330 runs 0.525 s + * For small arrays (<=48 bytes), loop is faster due to V8 JIT optimizations: + * - 32 bytes: Loop 14.7 ns/op vs Buffer.compare 49.7 ns/op (Loop 3.4x faster) * - * - with 16384 bytes, Buffer.compare() is 41x faster - * ✔ byteArrayEquals 16384 23808.76 ops/s 42.00135 us/op - 13 runs 1.05 s - * ✔ Buffer.compare 16384 975058.0 ops/s 1.025580 us/op - 297 runs 0.806 s + * For medium arrays, loop is still competitive: + * - 48 bytes: Loop 36 ns/op vs Buffer.compare 56 ns/op (Loop 1.5x faster) * - * - with 123687377 bytes, Buffer.compare() is 38x faster - * ✔ byteArrayEquals 123687377 3.077884 ops/s 324.8985 ms/op - 1 runs 64.5 s - * ✔ Buffer.compare 123687377 114.7834 ops/s 8.712061 ms/op - 13 runs 12.1 s + * For larger arrays, Buffer.compare is faster due to native code: + * - 96 bytes: Loop 130 ns/op vs Buffer.compare 50 ns/op (Buffer 2.6x faster) + * - 1024 bytes: Loop 940 ns/op vs Buffer.compare 55 ns/op (Buffer 17x faster) + * - 16384 bytes: Loop 14.8 μs/op vs Buffer.compare 270 ns/op (Buffer 55x faster) * + * The @lodestar/utils implementation uses a hybrid approach: + * - Loop for <=48 bytes (common case: roots, pubkeys) + * - Buffer.compare for >48 bytes (signatures, large data) */ -describe.skip("compare Uint8Array using byteArrayEquals() vs Buffer.compare()", () => { +describe.skip("compare Uint8Array using loop-based vs Buffer.compare-based byteArrayEquals", () => { const numValidator = 1_000_000; const validators = generateValidators(numValidator); const state = generateState({validators: validators}); @@ -36,20 +52,20 @@ describe.skip("compare Uint8Array using byteArrayEquals() vs Buffer.compare()", const bytes = stateBytes.subarray(0, length); const bytes2 = bytes.slice(); bench({ - id: `byteArrayEquals ${length}`, + id: `byteArrayEqualsLoop ${length}`, fn: () => { for (let i = 0; i < runsFactor; i++) { - byteArrayEquals(bytes, bytes2); + byteArrayEqualsLoop(bytes, bytes2); } }, runsFactor, }); bench({ - id: `Buffer.compare ${length}`, + id: `byteArrayEquals ${length}`, fn: () => { for (let i = 0; i < runsFactor; i++) { - Buffer.compare(bytes, bytes2); + byteArrayEquals(bytes, bytes2); } }, runsFactor, @@ -64,20 +80,20 @@ describe.skip("compare Uint8Array using byteArrayEquals() vs Buffer.compare()", const bytes2 = bytes.slice(); bytes2[bytes2.length - 1] = (bytes2.at(-1) as number) + 1; bench({ - id: `byteArrayEquals ${length} - diff last byte`, + id: `byteArrayEqualsLoop ${length} - diff last byte`, fn: () => { for (let i = 0; i < runsFactor; i++) { - byteArrayEquals(bytes, bytes2); + byteArrayEqualsLoop(bytes, bytes2); } }, runsFactor, }); bench({ - id: `Buffer.compare ${length} - diff last byte`, + id: `byteArrayEquals ${length} - diff last byte`, fn: () => { for (let i = 0; i < runsFactor; i++) { - Buffer.compare(bytes, bytes2); + byteArrayEquals(bytes, bytes2); } }, runsFactor, @@ -92,20 +108,20 @@ describe.skip("compare Uint8Array using byteArrayEquals() vs Buffer.compare()", const bytes2 = crypto.randomBytes(length); bench({ - id: `byteArrayEquals ${length} - random bytes`, + id: `byteArrayEqualsLoop ${length} - random bytes`, fn: () => { for (let i = 0; i < runsFactor; i++) { - byteArrayEquals(bytes, bytes2); + byteArrayEqualsLoop(bytes, bytes2); } }, runsFactor, }); bench({ - id: `Buffer.compare ${length} - random bytes`, + id: `byteArrayEquals ${length} - random bytes`, fn: () => { for (let i = 0; i < runsFactor; i++) { - Buffer.compare(bytes, bytes2); + byteArrayEquals(bytes, bytes2); } }, runsFactor, diff --git a/packages/state-transition/test/perf/misc/rootEquals.test.ts b/packages/state-transition/test/perf/misc/rootEquals.test.ts index 424a800a5e82..06d8c9183791 100644 --- a/packages/state-transition/test/perf/misc/rootEquals.test.ts +++ b/packages/state-transition/test/perf/misc/rootEquals.test.ts @@ -1,6 +1,7 @@ import {bench, describe, setBenchOpts} from "@chainsafe/benchmark"; -import {byteArrayEquals, fromHexString} from "@chainsafe/ssz"; +import {fromHexString} from "@chainsafe/ssz"; import {ssz} from "@lodestar/types"; +import {byteArrayEquals} from "@lodestar/utils"; // As of Sep 2023 // root equals diff --git a/packages/state-transition/test/perf/util/loadState/findModifiedValidators.test.ts b/packages/state-transition/test/perf/util/loadState/findModifiedValidators.test.ts index 018fb7ba8a7a..a7071aed52e9 100644 --- a/packages/state-transition/test/perf/util/loadState/findModifiedValidators.test.ts +++ b/packages/state-transition/test/perf/util/loadState/findModifiedValidators.test.ts @@ -2,7 +2,7 @@ import assert from "node:assert"; import {bench, describe} from "@chainsafe/benchmark"; import {CompositeViewDU} from "@chainsafe/ssz"; import {ssz} from "@lodestar/types"; -import {bytesToInt} from "@lodestar/utils"; +import {byteArrayEquals, bytesToInt} from "@lodestar/utils"; import {findModifiedValidators} from "../../../../src/util/loadState/findModifiedValidators.js"; import {VALIDATOR_BYTES_SIZE} from "../../../../src/util/sszBytes.js"; import {generateState} from "../../../utils/state.js"; @@ -103,13 +103,13 @@ describe("find modified validators by different ways", () => { for (let i = 0; i < state.validators.length; i++) { const validatorBytes = ssz.phase0.Validator.serialize(validators[i]); if ( - Buffer.compare( + !byteArrayEquals( validatorBytes, stateBytes.subarray( validatorsRange.start + i * VALIDATOR_BYTES_SIZE, validatorsRange.start + (i + 1) * VALIDATOR_BYTES_SIZE ) - ) !== 0 + ) ) { throw Error(`validator ${i} is not equal`); } @@ -139,12 +139,12 @@ describe("find modified validators by different ways", () => { function validatorDiff(validator: CompositeViewDU, bytes: Uint8Array): string | null { const pubkey = bytes.subarray(0, 48); - if (Buffer.compare(validator.pubkey, pubkey) !== 0) { + if (!byteArrayEquals(validator.pubkey, pubkey)) { return "pubkey"; } const withdrawalCredentials = bytes.subarray(48, 80); - if (Buffer.compare(validator.withdrawalCredentials, withdrawalCredentials) !== 0) { + if (!byteArrayEquals(validator.withdrawalCredentials, withdrawalCredentials)) { return "withdrawalCredentials"; } diff --git a/packages/state-transition/test/perf/util/loadState/loadState.test.ts b/packages/state-transition/test/perf/util/loadState/loadState.test.ts index b4b2bebc5336..526b02bd30e5 100644 --- a/packages/state-transition/test/perf/util/loadState/loadState.test.ts +++ b/packages/state-transition/test/perf/util/loadState/loadState.test.ts @@ -57,15 +57,15 @@ describe("loadState", () => { pubkey2index.set(pubkey, validatorIndex); index2pubkey[validatorIndex] = PublicKey.fromBytes(pubkey); } + const shufflingGetter = () => seedState.epochCtx.currentShuffling; createCachedBeaconState( migratedState, { config: seedState.config, pubkey2index, index2pubkey, - shufflingCache: seedState.epochCtx.shufflingCache, }, - {skipSyncPubkeys: true, skipSyncCommitteeCache: true} + {skipSyncPubkeys: true, skipSyncCommitteeCache: true, shufflingGetter} ); }, }); diff --git a/packages/state-transition/test/unit/block/isValidIndexedAttestation.test.ts b/packages/state-transition/test/unit/block/isValidIndexedAttestation.test.ts index 9c48d8db473a..9afa10327b31 100644 --- a/packages/state-transition/test/unit/block/isValidIndexedAttestation.test.ts +++ b/packages/state-transition/test/unit/block/isValidIndexedAttestation.test.ts @@ -45,8 +45,15 @@ describe("validate indexed attestation", () => { data: attestationData, signature: EMPTY_SIGNATURE, }; - expect(isValidIndexedAttestation(state.config, state.epochCtx.index2pubkey, state, indexedAttestation, false)).toBe( - expectedValue - ); + expect( + isValidIndexedAttestation( + state.config, + state.epochCtx.index2pubkey, + state.slot, + state.validators.length, + indexedAttestation, + false + ) + ).toBe(expectedValue); }); }); diff --git a/packages/state-transition/test/unit/block/processConsolidationRequest.test.ts b/packages/state-transition/test/unit/block/processConsolidationRequest.test.ts index 921068c169ca..86b9c646b33e 100644 --- a/packages/state-transition/test/unit/block/processConsolidationRequest.test.ts +++ b/packages/state-transition/test/unit/block/processConsolidationRequest.test.ts @@ -5,7 +5,6 @@ import { BLS_WITHDRAWAL_PREFIX, COMPOUNDING_WITHDRAWAL_PREFIX, FAR_FUTURE_EPOCH, - ForkSeq, SLOTS_PER_EPOCH, } from "@lodestar/params"; import {ssz} from "@lodestar/types"; @@ -52,7 +51,7 @@ describe("processConsolidationRequest", () => { expect(state.pendingConsolidations.length).eq(0); - processConsolidationRequest(ForkSeq.electra, state, request); + processConsolidationRequest(state, request); expect(state.pendingConsolidations.length).eq(0); }); diff --git a/packages/state-transition/test/unit/cachedBeaconState.test.ts b/packages/state-transition/test/unit/cachedBeaconState.test.ts index 61375bf772a9..5da5f698a5a2 100644 --- a/packages/state-transition/test/unit/cachedBeaconState.test.ts +++ b/packages/state-transition/test/unit/cachedBeaconState.test.ts @@ -3,15 +3,16 @@ import {PubkeyIndexMap} from "@chainsafe/pubkey-index-map"; import {fromHexString} from "@chainsafe/ssz"; import {createBeaconConfig} from "@lodestar/config"; import {config as defaultConfig} from "@lodestar/config/default"; -import {ssz} from "@lodestar/types"; +import {Epoch, RootHex, ssz} from "@lodestar/types"; import {toHexString} from "@lodestar/utils"; import {createCachedBeaconState, loadCachedBeaconState} from "../../src/cache/stateCache.js"; +import {EpochShuffling, calculateShufflingDecisionRoot} from "../../src/util/epochShuffling.js"; import {modifyStateSameValidator, newStateWithValidators} from "../utils/capella.js"; import {interopPubkeysCached} from "../utils/interop.js"; import {createCachedBeaconStateTest} from "../utils/state.js"; describe("CachedBeaconState", () => { - vi.setConfig({testTimeout: 20_000, hookTimeout: 20_000}); + vi.setConfig({testTimeout: 30_000, hookTimeout: 30_000}); it("Clone and mutate", () => { const stateView = ssz.altair.BeaconState.defaultViewDU(); @@ -159,8 +160,33 @@ describe("CachedBeaconState", () => { // confirm loadState() result const stateBytes = state.serialize(); + const shufflingGetter = (shufflingEpoch: Epoch, dependentRoot: RootHex): EpochShuffling | null => { + if ( + shufflingEpoch === seedState.epochCtx.epoch - 1 && + dependentRoot === calculateShufflingDecisionRoot(config, seedState, shufflingEpoch) + ) { + return seedState.epochCtx.previousShuffling; + } + + if ( + shufflingEpoch === seedState.epochCtx.epoch && + dependentRoot === calculateShufflingDecisionRoot(config, seedState, shufflingEpoch) + ) { + return seedState.epochCtx.currentShuffling; + } + + if ( + shufflingEpoch === seedState.epochCtx.epoch + 1 && + dependentRoot === calculateShufflingDecisionRoot(config, seedState, shufflingEpoch) + ) { + return seedState.epochCtx.nextShuffling; + } + + return null; + }; const newCachedState = loadCachedBeaconState(seedState, stateBytes, { skipSyncCommitteeCache: true, + shufflingGetter, }); const newStateBytes = newCachedState.serialize(); expect(newStateBytes).toEqual(stateBytes); @@ -171,9 +197,8 @@ describe("CachedBeaconState", () => { config, pubkey2index: new PubkeyIndexMap(), index2pubkey: [], - shufflingCache: seedState.epochCtx.shufflingCache, }, - {skipSyncCommitteeCache: true} + {skipSyncCommitteeCache: true, shufflingGetter} ); // validatorCountDelta < 0 is unrealistic and shuffling computation results in a different result if (validatorCountDelta >= 0) { diff --git a/packages/state-transition/test/unit/rewards/blockRewards.test.ts b/packages/state-transition/test/unit/rewards/blockRewards.test.ts index 522e494c51d9..dfdf8a4ee506 100644 --- a/packages/state-transition/test/unit/rewards/blockRewards.test.ts +++ b/packages/state-transition/test/unit/rewards/blockRewards.test.ts @@ -151,13 +151,13 @@ describe("chain / rewards / blockRewards", () => { // Set postState's reward cache const rewardCache = postState.proposerRewards; // Grab original reward cache before overwritten - postState.proposerRewards = {attestations: 1000, syncAggregate: 1001, slashing: 1002}; + const proposerRewards = {attestations: 1000, syncAggregate: 1001, slashing: 1002}; const calculatedBlockReward = await computeBlockRewards( config, block.message, preState as CachedBeaconStateAllForks, - postState + proposerRewards ); const {proposerIndex, total, attestations, syncAggregate, proposerSlashings, attesterSlashings} = calculatedBlockReward; diff --git a/packages/state-transition/test/unit/signatureSets/signatureSets.test.ts b/packages/state-transition/test/unit/signatureSets/signatureSets.test.ts index eead18b1e81e..14f97e9dcf9a 100644 --- a/packages/state-transition/test/unit/signatureSets/signatureSets.test.ts +++ b/packages/state-transition/test/unit/signatureSets/signatureSets.test.ts @@ -72,7 +72,6 @@ describe("signatureSets", () => { const signatureSets = getBlockSignatureSets( state.config, - state.epochCtx.index2pubkey, state.epochCtx.currentSyncCommitteeIndexed, signedBlock, indexedAttestations diff --git a/packages/state-transition/test/unit/util/loadState/loadValidator.test.ts b/packages/state-transition/test/unit/util/loadState/loadValidator.test.ts index 8b823474353a..cccdec2d5d73 100644 --- a/packages/state-transition/test/unit/util/loadState/loadValidator.test.ts +++ b/packages/state-transition/test/unit/util/loadState/loadValidator.test.ts @@ -2,6 +2,7 @@ import {describe, expect, it} from "vitest"; import {CompositeViewDU} from "@chainsafe/ssz"; import {config} from "@lodestar/config/default"; import {phase0, ssz} from "@lodestar/types"; +import {byteArrayEquals} from "@lodestar/utils"; import {getEffectiveBalancesFromStateBytes, loadValidator} from "../../../../src/util/loadState/loadValidator.js"; import {generateState} from "../../../utils/state.js"; import {generateValidators} from "../../../utils/validator.js"; @@ -112,8 +113,8 @@ describe("loadValidator", () => { const newValidator = getValidator(); const newValidatorBytes = newValidator.serialize(); const loadedValidator = loadValidator(validator, newValidatorBytes); - expect(Buffer.compare(loadedValidator.hashTreeRoot(), newValidator.hashTreeRoot())).toBe(0); - expect(Buffer.compare(loadedValidator.serialize(), newValidator.serialize())).toBe(0); + expect(byteArrayEquals(loadedValidator.hashTreeRoot(), newValidator.hashTreeRoot())).toBe(true); + expect(byteArrayEquals(loadedValidator.serialize(), newValidator.serialize())).toBe(true); }); }); diff --git a/packages/state-transition/test/unit/util/misc.test.ts b/packages/state-transition/test/unit/util/misc.test.ts index 3b4e905903c6..9b6bc20c6931 100644 --- a/packages/state-transition/test/unit/util/misc.test.ts +++ b/packages/state-transition/test/unit/util/misc.test.ts @@ -1,4 +1,4 @@ -import {toBigIntLE} from "bigint-buffer"; +import {toBigIntLE} from "@vekexasia/bigint-buffer2"; import {describe, expect, it} from "vitest"; import {GENESIS_SLOT, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; import {getBlockRoot} from "../../../src/util/index.js"; diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 8bb4af5fcf3d..1bc6dd34beea 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@lodestar/test-utils", "private": true, - "version": "1.39.1", + "version": "1.40.0", "description": "Test utilities reused across other packages", "author": "ChainSafe Systems", "license": "Apache-2.0", @@ -54,7 +54,7 @@ "@chainsafe/blst": "^2.2.0", "@lodestar/params": "workspace:^", "@lodestar/utils": "workspace:^", - "axios": "^1.3.4", + "axios": "^1.13.2", "tmp": "^0.2.1", "vitest": "catalog:" }, diff --git a/packages/types/package.json b/packages/types/package.json index 18252f12d07f..79b052e89058 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -11,7 +11,7 @@ "bugs": { "url": "https://github.com/ChainSafe/lodestar/issues" }, - "version": "1.39.1", + "version": "1.40.0", "type": "module", "exports": { ".": { diff --git a/packages/types/src/gloas/sszTypes.ts b/packages/types/src/gloas/sszTypes.ts index 8198b8a48616..2ee8bf34cf58 100644 --- a/packages/types/src/gloas/sszTypes.ts +++ b/packages/types/src/gloas/sszTypes.ts @@ -1,6 +1,7 @@ import {BitVectorType, ContainerType, ListBasicType, ListCompositeType, VectorCompositeType} from "@chainsafe/ssz"; import { BUILDER_PENDING_WITHDRAWALS_LIMIT, + BUILDER_REGISTRY_LIMIT, HISTORICAL_ROOTS_LIMIT, MAX_PAYLOAD_ATTESTATIONS, NUMBER_OF_COLUMNS, @@ -17,15 +18,42 @@ import {ssz as phase0Ssz} from "../phase0/index.js"; import {ssz as primitiveSsz} from "../primitive/index.js"; // biome-ignore lint/suspicious/noShadowRestrictedNames: We explicitly want `Boolean` name to be imported -const {Gwei, ExecutionAddress, ValidatorIndex, Epoch, BLSSignature, Bytes32, Root, Slot, Boolean, UintBn64, UintNum64} = - primitiveSsz; +const {Boolean} = primitiveSsz; + +const { + Gwei, + ExecutionAddress, + ValidatorIndex, + Epoch, + BLSSignature, + Bytes32, + Root, + Slot, + UintBn64, + UintNum64, + BLSPubkey, + Uint8, + BuilderIndex, + EpochInf, +} = primitiveSsz; + +export const Builder = new ContainerType( + { + pubkey: BLSPubkey, + version: Uint8, + executionAddress: ExecutionAddress, + balance: UintNum64, + depositEpoch: EpochInf, + withdrawableEpoch: EpochInf, + }, + {typeName: "Builder", jsonCase: "eth2"} +); export const BuilderPendingWithdrawal = new ContainerType( { feeRecipient: ExecutionAddress, amount: UintNum64, - builderIndex: ValidatorIndex, - withdrawableEpoch: Epoch, + builderIndex: BuilderIndex, }, {typeName: "BuilderPendingWithdrawal", jsonCase: "eth2"} ); @@ -75,6 +103,24 @@ export const IndexedPayloadAttestation = new ContainerType( {typeName: "IndexedPayloadAttestation", jsonCase: "eth2"} ); +export const ProposerPreferences = new ContainerType( + { + proposalSlot: Slot, + validatorIndex: ValidatorIndex, + feeRecipient: ExecutionAddress, + gasLimit: UintNum64, + }, + {typeName: "ProposerPreferences", jsonCase: "eth2"} +); + +export const SignedProposerPreferences = new ContainerType( + { + message: ProposerPreferences, + signature: BLSSignature, + }, + {typeName: "SignedProposerPreferences", jsonCase: "eth2"} +); + export const ExecutionPayloadBid = new ContainerType( { parentBlockHash: Bytes32, @@ -83,7 +129,7 @@ export const ExecutionPayloadBid = new ContainerType( prevRandao: Bytes32, feeRecipient: ExecutionAddress, gasLimit: UintBn64, - builderIndex: ValidatorIndex, + builderIndex: BuilderIndex, slot: Slot, value: UintNum64, executionPayment: UintNum64, @@ -104,7 +150,7 @@ export const ExecutionPayloadEnvelope = new ContainerType( { payload: electraSsz.ExecutionPayload, executionRequests: electraSsz.ExecutionRequests, - builderIndex: ValidatorIndex, + builderIndex: BuilderIndex, beaconBlockRoot: Root, slot: Slot, blobKzgCommitments: denebSsz.BlobKzgCommitments, @@ -211,11 +257,13 @@ export const BeaconState = new ContainerType( pendingPartialWithdrawals: electraSsz.BeaconState.fields.pendingPartialWithdrawals, pendingConsolidations: electraSsz.BeaconState.fields.pendingConsolidations, proposerLookahead: fuluSsz.BeaconState.fields.proposerLookahead, + builders: new ListCompositeType(Builder, BUILDER_REGISTRY_LIMIT), // New in GLOAS:EIP7732 + nextWithdrawalBuilderIndex: BuilderIndex, // New in GLOAS:EIP7732 executionPayloadAvailability: new BitVectorType(SLOTS_PER_HISTORICAL_ROOT), // New in GLOAS:EIP7732 builderPendingPayments: new VectorCompositeType(BuilderPendingPayment, 2 * SLOTS_PER_EPOCH), // New in GLOAS:EIP7732 builderPendingWithdrawals: new ListCompositeType(BuilderPendingWithdrawal, BUILDER_PENDING_WITHDRAWALS_LIMIT), // New in GLOAS:EIP7732 latestBlockHash: Bytes32, // New in GLOAS:EIP7732 - latestWithdrawalsRoot: Root, // New in GLOAS:EIP7732 + payloadExpectedWithdrawals: capellaSsz.Withdrawals, // New in GLOAS:EIP7732 }, {typeName: "BeaconState", jsonCase: "eth2"} ); diff --git a/packages/types/src/gloas/types.ts b/packages/types/src/gloas/types.ts index b527120d5712..6ef793c2e8cf 100644 --- a/packages/types/src/gloas/types.ts +++ b/packages/types/src/gloas/types.ts @@ -1,12 +1,15 @@ import {ValueOf} from "@chainsafe/ssz"; import * as ssz from "./sszTypes.js"; +export type Builder = ValueOf; export type BuilderPendingWithdrawal = ValueOf; export type BuilderPendingPayment = ValueOf; export type PayloadAttestationData = ValueOf; export type PayloadAttestation = ValueOf; export type PayloadAttestationMessage = ValueOf; export type IndexedPayloadAttestation = ValueOf; +export type ProposerPreferences = ValueOf; +export type SignedProposerPreferences = ValueOf; export type ExecutionPayloadBid = ValueOf; export type SignedExecutionPayloadBid = ValueOf; export type ExecutionPayloadEnvelope = ValueOf; diff --git a/packages/types/src/primitive/sszTypes.ts b/packages/types/src/primitive/sszTypes.ts index d4069e016778..dc79f11d7ba5 100644 --- a/packages/types/src/primitive/sszTypes.ts +++ b/packages/types/src/primitive/sszTypes.ts @@ -56,6 +56,7 @@ export const SyncPeriod = UintNum64; export const CommitteeIndex = UintNum64; /** @see CommitteeIndex */ export const SubcommitteeIndex = UintNum64; +export const BuilderIndex = UintNumInf64; // Builder index can be infinity in bid when self-build /** * Use JS Number for performance, values must be limited to 2**52-1. * ValidatorIndex is bounded by `VALIDATOR_REGISTRY_LIMIT` diff --git a/packages/types/src/primitive/types.ts b/packages/types/src/primitive/types.ts index 7b0fac50963b..062cf256d393 100644 --- a/packages/types/src/primitive/types.ts +++ b/packages/types/src/primitive/types.ts @@ -27,6 +27,7 @@ export type Epoch = UintNumInf64; export type SyncPeriod = UintNum64; export type CommitteeIndex = UintNum64; export type SubcommitteeIndex = UintNum64; +export type BuilderIndex = UintNumInf64; export type ValidatorIndex = UintNum64; export type WithdrawalIndex = UintNum64; export type BlobIndex = UintNum64; diff --git a/packages/types/src/utils/typeguards.ts b/packages/types/src/utils/typeguards.ts index e77179efe97d..4e58ce34be16 100644 --- a/packages/types/src/utils/typeguards.ts +++ b/packages/types/src/utils/typeguards.ts @@ -1,4 +1,10 @@ -import {FINALIZED_ROOT_DEPTH_ELECTRA, ForkPostBellatrix, ForkPostDeneb, ForkPostElectra} from "@lodestar/params"; +import { + FINALIZED_ROOT_DEPTH_ELECTRA, + ForkPostBellatrix, + ForkPostDeneb, + ForkPostElectra, + ForkPostGloas, +} from "@lodestar/params"; import { Attestation, BeaconBlock, @@ -96,3 +102,7 @@ export function isELectraLightClientFinalityUpdate( updatePostElectra.finalityBranch.length === FINALIZED_ROOT_DEPTH_ELECTRA ); } + +export function isGloasBeaconBlock(block: BeaconBlock): block is BeaconBlock { + return (block.body as BeaconBlockBody).signedExecutionPayloadBid !== undefined; +} diff --git a/packages/utils/package.json b/packages/utils/package.json index 724f21b34d85..c468025d36a8 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -11,7 +11,7 @@ "bugs": { "url": "https://github.com/ChainSafe/lodestar/issues" }, - "version": "1.39.1", + "version": "1.40.0", "type": "module", "exports": { ".": { @@ -47,8 +47,8 @@ "types": "lib/index.d.ts", "dependencies": { "@chainsafe/as-sha256": "^1.2.0", + "@vekexasia/bigint-buffer2": "^1.1.0", "any-signal": "^4.1.1", - "bigint-buffer": "^1.1.5", "case": "^1.6.3", "js-yaml": "^4.1.0" }, diff --git a/packages/utils/src/bytes/browser.ts b/packages/utils/src/bytes/browser.ts index 0b95fedb9cba..712e99eed25d 100644 --- a/packages/utils/src/bytes/browser.ts +++ b/packages/utils/src/bytes/browser.ts @@ -122,7 +122,7 @@ function charCodeToByte(charCode: number): number { throw new Error(`Invalid hex character code: ${charCode}`); } -import {toBigIntBE, toBigIntLE, toBufferBE, toBufferLE} from "bigint-buffer"; +import {toBigIntBE, toBigIntLE, toBufferBE, toBufferLE} from "@vekexasia/bigint-buffer2"; type Endianness = "le" | "be"; @@ -186,3 +186,18 @@ export function xor(a: Uint8Array, b: Uint8Array): Uint8Array { } return a; } + +/** + * Compare two byte arrays for equality. + * Note: In Node.js environment, the implementation in nodejs.ts uses Buffer.compare + * which is significantly faster due to native code. + */ +export function byteArrayEquals(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + return true; +} diff --git a/packages/utils/src/bytes/nodejs.ts b/packages/utils/src/bytes/nodejs.ts index 7f544d44fc4d..c98066c74cbd 100644 --- a/packages/utils/src/bytes/nodejs.ts +++ b/packages/utils/src/bytes/nodejs.ts @@ -62,4 +62,31 @@ export function fromHex(hex: string): Uint8Array { /// the performance of fromHexInto using a preallocated buffer is very bad compared to browser so I moved it to the benchmark +/** + * Compare two byte arrays for equality using the most performant method based on size. + * + * Node v24.13.0 benchmark results: + * - 32 bytes: Loop 14.7 ns/op vs Buffer.compare 49.7 ns/op (Loop 3.4x faster) + * - 48 bytes: Loop 36 ns/op vs Buffer.compare 56 ns/op (Loop 1.5x faster) + * - 96 bytes: Loop 130 ns/op vs Buffer.compare 50 ns/op (Buffer 2.6x faster) + * - 1024 bytes: Loop 940 ns/op vs Buffer.compare 55 ns/op (Buffer 17x faster) + * + * Uses loop for small arrays (<=48 bytes) where V8 JIT is more efficient, + * and Buffer.compare for larger arrays where native code wins. + */ +export function byteArrayEquals(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) { + return false; + } + // For small arrays (<=48 bytes: roots, pubkeys), loop is faster due to V8 JIT optimizations + if (a.length <= 48) { + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + return true; + } + // For larger arrays, Buffer.compare uses native code and is significantly faster + return Buffer.compare(a, b) === 0; +} + export {bigIntToBytes, bytesToBigInt, bytesToInt, fromHexInto, intToBytes, toHexString, xor} from "./browser.ts"; diff --git a/packages/utils/src/metrics.ts b/packages/utils/src/metrics.ts index f1ebb18ef079..f7b3db7bbb36 100644 --- a/packages/utils/src/metrics.ts +++ b/packages/utils/src/metrics.ts @@ -11,6 +11,8 @@ export interface Gauge { set: NoLabels extends Labels ? (value: number) => void : (labels: Labels, value: number) => void; collect?(): void; + + reset(): void; } export interface GaugeExtra extends Omit, "collect"> { diff --git a/packages/utils/test/perf/bytes.test.ts b/packages/utils/test/perf/bytes.test.ts index 9a8ed6bc92d3..17f7153fef2d 100644 --- a/packages/utils/test/perf/bytes.test.ts +++ b/packages/utils/test/perf/bytes.test.ts @@ -78,5 +78,40 @@ describe("bytes utils", async () => { }, runsFactor, }); + + /** + * Node v24.13.0 benchmark results for byteArrayEquals: + * + * Size | nodejs (hybrid) | browser (loop) + * -------------|------------------------------|---------------- + * 32 bytes | 14.7 ns/op (loop) | 14.7 ns/op + * 48 bytes | 36 ns/op (loop) | 36 ns/op + * 96 bytes | 50 ns/op (Buffer.compare) | 130 ns/op + * 1024 bytes | 55 ns/op (Buffer.compare) | 940 ns/op + * 131072 bytes | 270 ns/op (Buffer.compare) | 14.8 μs/op + * + * The nodejs implementation uses a hybrid approach: + * - Loop for <=48 bytes (V8 JIT optimized) + * - Buffer.compare for >48 bytes (native code) + */ + const arraysToCompare = [ + {name: "32 bytes (block root)", a: blockRoot, b: new Uint8Array(blockRoot)}, + {name: "48 bytes (pubkey)", a: new Uint8Array(48).fill(42), b: new Uint8Array(48).fill(42)}, + {name: "96 bytes (signature)", a: new Uint8Array(96).fill(42), b: new Uint8Array(96).fill(42)}, + {name: "1024 bytes", a: new Uint8Array(1024).fill(42), b: new Uint8Array(1024).fill(42)}, + {name: `${BLOB_LEN} bytes (blob)`, a: blob, b: new Uint8Array(blob)}, + ]; + + for (const {name: arrName, a, b} of arraysToCompare) { + bench({ + id: `${name} byteArrayEquals ${arrName}`, + fn: () => { + for (let i = 0; i < runsFactor; i++) { + impl.byteArrayEquals(a, b); + } + }, + runsFactor, + }); + } } }); diff --git a/packages/utils/test/unit/bytes.test.ts b/packages/utils/test/unit/bytes.test.ts index 42c5d7511585..720cf46eee06 100644 --- a/packages/utils/test/unit/bytes.test.ts +++ b/packages/utils/test/unit/bytes.test.ts @@ -36,7 +36,7 @@ describe("intToBytes", () => { const type = typeof input; const length = input[1]; it(`should correctly serialize ${type} to bytes length ${length}`, () => { - expect(intToBytes(input[0], input[1])).toEqual(output); + expect(toHex(intToBytes(input[0], input[1]))).toEqual(toHex(output)); }); } }); diff --git a/packages/validator/package.json b/packages/validator/package.json index 48211de9ed63..4711ddac7a52 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -1,6 +1,6 @@ { "name": "@lodestar/validator", - "version": "1.39.1", + "version": "1.40.0", "description": "A Typescript implementation of the validator client", "author": "ChainSafe Systems", "license": "Apache-2.0", @@ -63,7 +63,7 @@ "@lodestar/logger": "workspace:^", "@lodestar/spec-test-util": "workspace:^", "@lodestar/test-utils": "workspace:^", - "bigint-buffer": "^1.1.5", + "@vekexasia/bigint-buffer2": "^1.1.0", "rimraf": "^4.4.1" } } diff --git a/packages/validator/src/util/externalSignerClient.ts b/packages/validator/src/util/externalSignerClient.ts index d91ac9f9f7e3..9a74f47740fd 100644 --- a/packages/validator/src/util/externalSignerClient.ts +++ b/packages/validator/src/util/externalSignerClient.ts @@ -1,6 +1,6 @@ import {ContainerType, ValueOf} from "@chainsafe/ssz"; import {BeaconConfig} from "@lodestar/config"; -import {ForkPreBellatrix, ForkSeq} from "@lodestar/params"; +import {ForkName, ForkPreBellatrix, ForkSeq, isForkPostDeneb} from "@lodestar/params"; import {blindedOrFullBlockToHeader, computeEpochAtSlot} from "@lodestar/state-transition"; import { AggregateAndProof, @@ -11,7 +11,6 @@ import { RootHex, Slot, altair, - capella, phase0, ssz, sszTypesFor, @@ -33,7 +32,6 @@ export enum SignableMessageType { SYNC_COMMITTEE_SELECTION_PROOF = "SYNC_COMMITTEE_SELECTION_PROOF", SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF = "SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF", VALIDATOR_REGISTRATION = "VALIDATOR_REGISTRATION", - BLS_TO_EXECUTION_CHANGE = "BLS_TO_EXECUTION_CHANGE", } const AggregationSlotType = new ContainerType({ @@ -82,8 +80,7 @@ export type SignableMessage = | {type: SignableMessageType.SYNC_COMMITTEE_MESSAGE; data: ValueOf} | {type: SignableMessageType.SYNC_COMMITTEE_SELECTION_PROOF; data: ValueOf} | {type: SignableMessageType.SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF; data: altair.ContributionAndProof} - | {type: SignableMessageType.VALIDATOR_REGISTRATION; data: ValidatorRegistrationV1} - | {type: SignableMessageType.BLS_TO_EXECUTION_CHANGE; data: capella.BLSToExecutionChange}; + | {type: SignableMessageType.VALIDATOR_REGISTRATION; data: ValidatorRegistrationV1}; const requiresForkInfo: Record = { [SignableMessageType.AGGREGATION_SLOT]: true, @@ -98,7 +95,6 @@ const requiresForkInfo: Record = { [SignableMessageType.SYNC_COMMITTEE_SELECTION_PROOF]: true, [SignableMessageType.SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF]: true, [SignableMessageType.VALIDATOR_REGISTRATION]: false, - [SignableMessageType.BLS_TO_EXECUTION_CHANGE]: true, }; type Web3SignerSerializedRequest = { @@ -147,12 +143,12 @@ export async function externalSignerPostSignature( requestObj.signingRoot = toRootHex(signingRoot); if (requiresForkInfo[signableMessage.type]) { - const forkInfo = config.getForkInfo(signingSlot); + const forkInfo = getForkInfoForSigning(config, signingSlot, signableMessage.type); requestObj.fork_info = { fork: { previous_version: toHex(forkInfo.prevVersion), current_version: toHex(forkInfo.version), - epoch: String(computeEpochAtSlot(signingSlot)), + epoch: String(forkInfo.epoch), }, genesis_validators_root: toRootHex(config.genesisValidatorsRoot), }; @@ -270,8 +266,30 @@ function serializerSignableMessagePayload(config: BeaconConfig, payload: Signabl case SignableMessageType.VALIDATOR_REGISTRATION: return {validator_registration: ssz.bellatrix.ValidatorRegistrationV1.toJson(payload.data)}; + } +} - case SignableMessageType.BLS_TO_EXECUTION_CHANGE: - return {BLS_TO_EXECUTION_CHANGE: ssz.capella.BLSToExecutionChange.toJson(payload.data)}; +function getForkInfoForSigning( + config: BeaconConfig, + signingSlot: Slot, + messageType: SignableMessageType +): {version: Uint8Array; prevVersion: Uint8Array; epoch: number} { + const forkInfo = config.getForkInfo(signingSlot); + + if (messageType === SignableMessageType.VOLUNTARY_EXIT && isForkPostDeneb(forkInfo.name)) { + // Always uses Capella fork post-Deneb (EIP-7044) + const capellaFork = config.forks[ForkName.capella]; + return { + version: capellaFork.version, + prevVersion: capellaFork.prevVersion, + epoch: capellaFork.epoch, + }; } + + // Use the fork at the signing slot by default + return { + version: forkInfo.version, + prevVersion: forkInfo.prevVersion, + epoch: computeEpochAtSlot(signingSlot), + }; } diff --git a/packages/validator/src/util/params.ts b/packages/validator/src/util/params.ts index 0f3efaefcc68..7ffe133a59b0 100644 --- a/packages/validator/src/util/params.ts +++ b/packages/validator/src/util/params.ts @@ -323,6 +323,9 @@ function getSpecCriticalParams(localConfig: ChainConfig): Record=6.9.0'} - '@babel/helper-validator-identifier@7.25.9': - resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} - engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.28.5': resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} @@ -1973,8 +1975,8 @@ packages: '@fastify/accept-negotiator@2.0.0': resolution: {integrity: sha512-/Sce/kBzuTxIq5tJh85nVNOq9wKD8s+viIgX0fFMDBdw95gnpf53qmF1oBgJym3cPFliWUuSloVg/1w/rH0FcQ==} - '@fastify/ajv-compiler@4.0.1': - resolution: {integrity: sha512-DxrBdgsjNLP0YM6W5Hd6/Fmj43S8zMKiFJYgi+Ri3htTGAowPVG/tG1wpnWLMjufEnehRivUCKZ1pLDIoZdTuw==} + '@fastify/ajv-compiler@4.0.5': + resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} '@fastify/bearer-auth@10.0.1': resolution: {integrity: sha512-i2snRkAJsMmfFcsRS/fFIovcLL3WeZtxJP9pprx2NvB8N/l+fjMNmKeWWyX0hDS2Q0zEPqLz/G0DK92nqJYAJQ==} @@ -2676,6 +2678,9 @@ packages: resolution: {integrity: sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==} engines: {node: '>=8.0.0'} + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -3168,6 +3173,15 @@ packages: '@types/yauzl@2.10.0': resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} + '@vekexasia/bigint-buffer2@1.1.0': + resolution: {integrity: sha512-CB19/UHoop2Q+HaRU1lI3fWKUkwju7XtwGTvdDfJgZyPHI+lLXKDYOOkz+NOCLfcBSXpJXpue/vLN0PDZtBT/Q==} + engines: {node: '>= 14.0.0'} + peerDependencies: + '@vekexasia/bigint-uint8array': '*' + peerDependenciesMeta: + '@vekexasia/bigint-uint8array': + optional: true + '@vitest/browser-playwright@4.0.7': resolution: {integrity: sha512-j5vA74jIqKbMA6yRTQ4PwEwbuPx+Ldtdb12gAJt++eds3kDtuvmfqRe9SmCxXRJ50drZaSai6Vunh2WcjUw8Fg==} peerDependencies: @@ -3438,8 +3452,8 @@ packages: engines: {node: '>= 0.8.0'} deprecated: The AWS SDK for JavaScript (v2) has reached end-of-support, and no longer receives updates. Please migrate your code to use AWS SDK for JavaScript (v3). More info https://a.co/cUPnyil - axios@1.8.2: - resolution: {integrity: sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==} + axios@1.13.3: + resolution: {integrity: sha512-ERT8kdX7DZjtUm7IitEyV7InTHAF42iJuMArIiDIV5YtPanJkgw4hw5Dyg9fh0mihdWNn1GKaeIWErfe56UQ1g==} babel-runtime@6.26.0: resolution: {integrity: sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==} @@ -3459,10 +3473,6 @@ packages: before-after-hook@4.0.0: resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} - bigint-buffer@1.1.5: - resolution: {integrity: sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==} - engines: {node: '>= 10.0.0'} - bigint-crypto-utils@3.2.2: resolution: {integrity: sha512-U1RbE3aX9ayCUVcIPHuPDPKcK3SFOXf93J1UK/iHlJuQB7bhagPIX06/CLpLEsDThJ7KA4Dhrnzynl+d2weTiw==} engines: {node: '>=14.0.0'} @@ -3471,9 +3481,6 @@ packages: resolution: {integrity: sha512-X4CiKlcV2GjnCMwnKAfbVWpHa++65th9TuzAEYtZoATiOE2DQKhSp4CJlyLoTqdhBKlXjpXjCTYPNNFS33Fi6w==} engines: {node: ^20.17.0 || >=22.9.0} - bindings@1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - bintrees@1.0.1: resolution: {integrity: sha512-tbaUB1QpTIj4cKY8c1rvNAvEQXA+ekzHmbe4jzNfW3QWsF9GnnP/BRWyl6/qqS53heoYJ93naaFcm/jooONH8g==} @@ -3582,9 +3589,17 @@ packages: resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + call-bind@1.0.5: resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -4008,6 +4023,10 @@ packages: resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} engines: {node: '>=12'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -4055,11 +4074,23 @@ packages: resolution: {integrity: sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==} engines: {node: '>= 0.4'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - es-set-tostringtag@2.0.1: - resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} es-to-primitive@1.2.1: @@ -4183,10 +4214,6 @@ packages: fast-querystring@1.1.1: resolution: {integrity: sha512-qR2r+e3HvhEFmpdHMv//U8FnFlnYjaC6QKDuaXALDkw2kvHO8WDjxH+f/rHGR4Me4pnk8p9JAkRNTjYHAKRn2Q==} - fast-redact@3.1.2: - resolution: {integrity: sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==} - engines: {node: '>=6'} - fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} @@ -4209,8 +4236,8 @@ packages: fastify-plugin@5.0.1: resolution: {integrity: sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==} - fastify@5.3.2: - resolution: {integrity: sha512-AIPqBgtqBAwkOkrnwesEE+dOyU30dQ4kh7udxeGVR05CRGwubZx+p2H8P0C4cRnQT0+EPK4VGea2DTL2RtWttg==} + fastify@5.7.4: + resolution: {integrity: sha512-e6l5NsRdaEP8rdD8VR0ErJASeyaRbzXYpmkrpr2SuvuMq6Si3lvsaVy5C+7gLanEkvjpMDzBXWE5HPeb/hgTxA==} fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -4244,9 +4271,6 @@ packages: file-stream-rotator@0.6.1: resolution: {integrity: sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==} - file-uri-to-path@1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -4270,6 +4294,15 @@ packages: fn.name@1.1.0: resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + follow-redirects@1.15.6: resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} engines: {node: '>=4.0'} @@ -4286,14 +4319,18 @@ packages: resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} engines: {node: '>=14'} - form-data@2.5.1: - resolution: {integrity: sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==} + form-data@2.5.5: + resolution: {integrity: sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==} engines: {node: '>= 0.12'} form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + formidable@2.1.2: resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} deprecated: 'ACTION REQUIRED: SWITCH TO v3 - v1 and v2 are VULNERABLE! v1 is DEPRECATED FOR OVER 2 YEARS! Use formidable@latest or try formidable-mini for fresh projects' @@ -4369,9 +4406,17 @@ packages: get-intrinsic@1.2.2: resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-iterator@2.0.1: resolution: {integrity: sha512-7HuY/hebu4gryTDT7O/XY/fvY9wRByEGdK6QOa4of8npTcv0+NS6frFKABcf6S9EBAsveTuKTsZQQBFMMNILIg==} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stream@5.2.0: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} @@ -4435,6 +4480,10 @@ packages: gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + got@11.8.6: resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} engines: {node: '>=10.19.0'} @@ -4475,10 +4524,18 @@ packages: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + has-tostringtag@1.0.0: resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + has-unicode@2.0.1: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} @@ -4965,8 +5022,8 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true jsdom@23.0.1: @@ -5183,6 +5240,10 @@ packages: resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} engines: {node: '>=10'} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + mcl-wasm@0.7.9: resolution: {integrity: sha512-iJIUcQWA88IJB/5L15GnJVnSQJmf/YaxxV6zRavv83HILHaJQb6y0iFyDMdDO0gN8X37tdxmAOrH/P8B6RB8sQ==} engines: {node: '>=8.9.0'} @@ -5537,8 +5598,9 @@ packages: resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} engines: {node: '>= 6'} - object-inspect@1.13.1: - resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} object-is@1.1.5: resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} @@ -5797,14 +5859,14 @@ packages: resolution: {integrity: sha512-KocF8ve28eFjjuBKKGvzOBGzG8ew2OqOOSxTTZhirkzH7h3BI1vyzqlR0qbfcDBve1Yzo3FVlWUAtCRrbVN8Fw==} engines: {node: '>=14.16'} - pino-abstract-transport@1.2.0: - resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==} + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} - pino-std-serializers@6.2.0: - resolution: {integrity: sha512-IWgSzUL8X1w4BIWTwErRgtV8PyOGOOi60uqv0oKuS/fOA8Nco/OeI6lBuc4dyP8MMfdFwyHqTMcBIA7nDiqEqA==} + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} - pino@9.0.0: - resolution: {integrity: sha512-uI1ThkzTShNSwvsUM6b4ND8ANzWURk9zTELMztFkmnCQeR/4wkomJ+echHee5GMWGovoSfjwdeu80DsFIt7mbA==} + pino@10.3.0: + resolution: {integrity: sha512-0GNPNzHXBKw6U/InGe79A3Crzyk9bcSyObF9/Gfo9DLEf5qj5RF50RSjsu0W1rZ6ZqRGdzDFCRBQvi9/rSGPtA==} hasBin: true pixelmatch@7.1.0: @@ -5880,9 +5942,6 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - process-warning@3.0.0: - resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} - process-warning@4.0.0: resolution: {integrity: sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==} @@ -5960,8 +6019,8 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - qs@6.11.1: - resolution: {integrity: sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==} + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} engines: {node: '>=0.6'} quansync@0.2.11: @@ -6026,10 +6085,6 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} - readable-stream@4.3.0: - resolution: {integrity: sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} @@ -6255,8 +6310,21 @@ packages: shell-quote@1.7.3: resolution: {integrity: sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==} - side-channel@1.0.4: - resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -6310,8 +6378,8 @@ packages: resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} - sonic-boom@3.8.0: - resolution: {integrity: sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==} + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} sort-keys@5.1.0: resolution: {integrity: sha512-aSbHV0DaBcr7u0PVHXzM6NbZNAtrr9sF6+Qfs9UUVG7Ll3jQ6hHi8F/xqIIcn2rvIVbr0v/2zyjSdwSV47AgLQ==} @@ -6501,8 +6569,8 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - systeminformation@5.23.8: - resolution: {integrity: sha512-Osd24mNKe6jr/YoXLLK3k8TMdzaxDffhpCxgkfgBHcapykIkd50HXThM3TCEuHO2pPuCsSx2ms/SunqhU5MmsQ==} + systeminformation@5.27.14: + resolution: {integrity: sha512-3DoNDYSZBLxBwaJtQGWNpq0fonga/VZ47HY1+7/G3YoIPaPz93Df6egSzzTKbEMmlzUpy3eQ0nR9REuYIycXGg==} engines: {node: '>=8.0.0'} os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] hasBin: true @@ -6510,10 +6578,12 @@ packages: tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me tar@7.5.2: resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me tdigest@0.1.1: resolution: {integrity: sha512-CXcDY/NIgIbKZPx5H4JJNpq6JwJhU5Z4+yWj4ZghDc7/9nVajiRlPPyMXRePPPlBfcayUqtoCXjo7/Hm82ecUA==} @@ -6521,8 +6591,9 @@ packages: text-hex@1.0.0: resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} - thread-stream@2.7.0: - resolution: {integrity: sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==} + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -7428,7 +7499,7 @@ snapshots: dependencies: '@azure/core-auth': 1.9.0 abort-controller: 3.0.0 - form-data: 2.5.1 + form-data: 2.5.5 node-fetch: 2.7.0(encoding@0.1.13) tslib: 1.14.1 tunnel: 0.0.6 @@ -7463,8 +7534,6 @@ snapshots: '@babel/helper-string-parser@7.25.9': {} - '@babel/helper-validator-identifier@7.25.9': {} - '@babel/helper-validator-identifier@7.28.5': {} '@babel/parser@7.26.7': @@ -7474,7 +7543,7 @@ snapshots: '@babel/types@7.26.7': dependencies: '@babel/helper-string-parser': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 + '@babel/helper-validator-identifier': 7.28.5 '@bcoe/v8-coverage@1.0.2': {} @@ -8339,7 +8408,7 @@ snapshots: '@fastify/accept-negotiator@2.0.0': {} - '@fastify/ajv-compiler@4.0.1': + '@fastify/ajv-compiler@4.0.5': dependencies: ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) @@ -9454,6 +9523,8 @@ snapshots: '@opentelemetry/api@1.7.0': {} + '@pinojs/redact@0.4.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -9875,6 +9946,8 @@ snapshots: '@types/node': 24.10.1 optional: true + '@vekexasia/bigint-buffer2@1.1.0': {} + '@vitest/browser-playwright@4.0.7(playwright@1.56.1)(vite@6.1.6(@types/node@24.10.1)(yaml@2.8.2))(vitest@4.0.7)': dependencies: '@vitest/browser': 4.0.7(vite@6.1.6(@types/node@24.10.1)(yaml@2.8.2))(vitest@4.0.7) @@ -10219,10 +10292,10 @@ snapshots: uuid: 3.3.2 xml2js: 0.4.19 - axios@1.8.2(debug@4.4.3): + axios@1.13.3(debug@4.4.3): dependencies: - follow-redirects: 1.15.6(debug@4.4.3) - form-data: 4.0.0 + follow-redirects: 1.15.11(debug@4.4.3) + form-data: 4.0.5 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug @@ -10242,10 +10315,6 @@ snapshots: before-after-hook@4.0.0: {} - bigint-buffer@1.1.5: - dependencies: - bindings: 1.5.0 - bigint-crypto-utils@3.2.2: {} bin-links@6.0.0: @@ -10256,10 +10325,6 @@ snapshots: read-cmd-shim: 6.0.0 write-file-atomic: 7.0.0 - bindings@1.5.0: - dependencies: - file-uri-to-path: 1.0.0 - bintrees@1.0.1: {} bl@5.1.0: @@ -10426,12 +10491,22 @@ snapshots: normalize-url: 6.1.0 responselike: 2.0.1 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + call-bind@1.0.5: dependencies: function-bind: 1.1.2 get-intrinsic: 1.2.2 set-function-length: 1.2.0 + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + callsites@3.1.0: {} camel-case@4.1.2: @@ -10787,7 +10862,7 @@ snapshots: define-data-property@1.1.1: dependencies: - get-intrinsic: 1.2.2 + get-intrinsic: 1.3.0 gopd: 1.0.1 has-property-descriptors: 1.0.1 @@ -10857,6 +10932,12 @@ snapshots: dotenv@17.2.3: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + eastasianwidth@0.2.0: {} electron@26.2.4: @@ -10910,7 +10991,7 @@ snapshots: arraybuffer.prototype.slice: 1.0.2 available-typed-arrays: 1.0.5 call-bind: 1.0.5 - es-set-tostringtag: 2.0.1 + es-set-tostringtag: 2.1.0 es-to-primitive: 1.2.1 function.prototype.name: 1.1.6 get-intrinsic: 1.2.2 @@ -10930,7 +11011,7 @@ snapshots: is-string: 1.0.7 is-typed-array: 1.1.12 is-weakref: 1.0.2 - object-inspect: 1.13.1 + object-inspect: 1.13.4 object-keys: 1.1.1 object.assign: 4.1.4 regexp.prototype.flags: 1.5.1 @@ -10946,13 +11027,22 @@ snapshots: unbox-primitive: 1.0.2 which-typed-array: 1.1.13 + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} - es-set-tostringtag@2.0.1: + es-object-atoms@1.1.1: dependencies: - get-intrinsic: 1.2.2 - has: 1.0.3 - has-tostringtag: 1.0.0 + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 es-to-primitive@1.2.1: dependencies: @@ -11151,8 +11241,6 @@ snapshots: dependencies: fast-decode-uri-component: 1.0.1 - fast-redact@3.1.2: {} - fast-safe-stringify@2.1.1: {} fast-string-truncated-width@3.0.3: {} @@ -11171,9 +11259,9 @@ snapshots: fastify-plugin@5.0.1: {} - fastify@5.3.2: + fastify@5.7.4: dependencies: - '@fastify/ajv-compiler': 4.0.1 + '@fastify/ajv-compiler': 4.0.5 '@fastify/error': 4.0.0 '@fastify/fast-json-stringify-compiler': 5.0.1 '@fastify/proxy-addr': 5.0.0 @@ -11182,7 +11270,7 @@ snapshots: fast-json-stringify: 6.0.0 find-my-way: 9.0.1 light-my-request: 6.0.0 - pino: 9.0.0 + pino: 10.3.0 process-warning: 5.0.0 rfdc: 1.4.1 secure-json-parse: 4.0.0 @@ -11220,8 +11308,6 @@ snapshots: dependencies: moment: 2.29.4 - file-uri-to-path@1.0.0: {} - fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -11249,6 +11335,10 @@ snapshots: fn.name@1.1.0: {} + follow-redirects@1.15.11(debug@4.4.3): + optionalDependencies: + debug: 4.4.3 + follow-redirects@1.15.6(debug@4.4.3): optionalDependencies: debug: 4.4.3 @@ -11262,11 +11352,14 @@ snapshots: cross-spawn: 7.0.3 signal-exit: 4.1.0 - form-data@2.5.1: + form-data@2.5.5: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 mime-types: 2.1.35 + safe-buffer: 5.2.1 form-data@4.0.0: dependencies: @@ -11274,12 +11367,20 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + formidable@2.1.2: dependencies: dezalgo: 1.0.4 hexoid: 1.0.0 once: 1.4.0 - qs: 6.11.1 + qs: 6.14.1 fs-extra@11.3.2: dependencies: @@ -11360,8 +11461,26 @@ snapshots: has-symbols: 1.0.3 hasown: 2.0.2 + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-iterator@2.0.1: {} + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-stream@5.2.0: dependencies: pump: 3.0.0 @@ -11459,6 +11578,8 @@ snapshots: dependencies: get-intrinsic: 1.2.2 + gopd@1.2.0: {} + got@11.8.6: dependencies: '@sindresorhus/is': 4.6.0 @@ -11496,15 +11617,21 @@ snapshots: has-property-descriptors@1.0.1: dependencies: - get-intrinsic: 1.2.2 + get-intrinsic: 1.3.0 has-proto@1.0.1: {} has-symbols@1.0.3: {} + has-symbols@1.1.0: {} + has-tostringtag@1.0.0: dependencies: - has-symbols: 1.0.3 + has-symbols: 1.1.0 + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 has-unicode@2.0.1: {} @@ -11694,7 +11821,7 @@ snapshots: dependencies: get-intrinsic: 1.2.2 has: 1.0.3 - side-channel: 1.0.4 + side-channel: 1.1.0 ip-address@10.1.0: {} @@ -11723,7 +11850,7 @@ snapshots: is-boolean-object@1.1.2: dependencies: call-bind: 1.0.5 - has-tostringtag: 1.0.0 + has-tostringtag: 1.0.2 is-buffer@2.0.5: {} @@ -11735,7 +11862,7 @@ snapshots: is-date-object@1.0.5: dependencies: - has-tostringtag: 1.0.0 + has-tostringtag: 1.0.2 is-docker@2.2.1: {} @@ -11778,7 +11905,7 @@ snapshots: is-number-object@1.0.7: dependencies: - has-tostringtag: 1.0.0 + has-tostringtag: 1.0.2 is-number@7.0.0: {} @@ -11795,7 +11922,7 @@ snapshots: is-regex@1.1.4: dependencies: call-bind: 1.0.5 - has-tostringtag: 1.0.0 + has-tostringtag: 1.0.2 is-shared-array-buffer@1.0.2: dependencies: @@ -11811,7 +11938,7 @@ snapshots: is-string@1.0.7: dependencies: - has-tostringtag: 1.0.0 + has-tostringtag: 1.0.2 is-symbol@1.0.4: dependencies: @@ -12009,7 +12136,7 @@ snapshots: js-tokens@9.0.1: {} - js-yaml@4.1.0: + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -12316,6 +12443,8 @@ snapshots: escape-string-regexp: 4.0.0 optional: true + math-intrinsics@1.1.0: {} + mcl-wasm@0.7.9: {} md5.js@1.3.5: @@ -12719,7 +12848,7 @@ snapshots: object-hash@2.2.0: {} - object-inspect@1.13.1: {} + object-inspect@1.13.4: {} object-is@1.1.5: dependencies: @@ -12976,26 +13105,25 @@ snapshots: pify@6.1.0: {} - pino-abstract-transport@1.2.0: + pino-abstract-transport@3.0.0: dependencies: - readable-stream: 4.3.0 split2: 4.2.0 - pino-std-serializers@6.2.0: {} + pino-std-serializers@7.1.0: {} - pino@9.0.0: + pino@10.3.0: dependencies: + '@pinojs/redact': 0.4.0 atomic-sleep: 1.0.0 - fast-redact: 3.1.2 on-exit-leak-free: 2.1.0 - pino-abstract-transport: 1.2.0 - pino-std-serializers: 6.2.0 - process-warning: 3.0.0 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 quick-format-unescaped: 4.0.3 real-require: 0.2.0 safe-stable-stringify: 2.4.2 - sonic-boom: 3.8.0 - thread-stream: 2.7.0 + sonic-boom: 4.2.0 + thread-stream: 4.0.0 pixelmatch@7.1.0: dependencies: @@ -13068,8 +13196,6 @@ snapshots: process-nextick-args@2.0.1: {} - process-warning@3.0.0: {} - process-warning@4.0.0: {} process-warning@5.0.0: {} @@ -13138,9 +13264,9 @@ snapshots: punycode@2.3.1: {} - qs@6.11.1: + qs@6.14.1: dependencies: - side-channel: 1.0.4 + side-channel: 1.1.0 quansync@0.2.11: {} @@ -13205,13 +13331,6 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 - readable-stream@4.3.0: - dependencies: - abort-controller: 3.0.0 - buffer: 6.0.3 - events: 3.3.0 - process: 0.11.10 - real-require@0.2.0: {} regenerator-runtime@0.11.1: {} @@ -13446,11 +13565,33 @@ snapshots: shell-quote@1.7.3: {} - side-channel@1.0.4: + side-channel-list@1.0.0: dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 - object-inspect: 1.13.1 + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 siginfo@2.0.0: {} @@ -13527,7 +13668,7 @@ snapshots: ip-address: 10.1.0 smart-buffer: 4.2.0 - sonic-boom@3.8.0: + sonic-boom@4.2.0: dependencies: atomic-sleep: 1.0.0 @@ -13704,11 +13845,11 @@ snapshots: cookiejar: 2.1.4 debug: 4.4.3 fast-safe-stringify: 2.1.1 - form-data: 4.0.0 + form-data: 4.0.5 formidable: 2.1.2 methods: 1.1.2 mime: 2.6.0 - qs: 6.11.1 + qs: 6.14.1 semver: 7.7.3 transitivePeerDependencies: - supports-color @@ -13738,7 +13879,7 @@ snapshots: symbol-tree@3.2.4: {} - systeminformation@5.23.8: {} + systeminformation@5.27.14: {} tar@6.2.1: dependencies: @@ -13763,7 +13904,7 @@ snapshots: text-hex@1.0.0: {} - thread-stream@2.7.0: + thread-stream@4.0.0: dependencies: real-require: 0.2.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4c31d4b2f1a7..65e7c6f24ca9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -23,7 +23,7 @@ onlyBuiltDependencies: - "@parcel/watcher" - "@swc/core" - aws-sdk - - bigint-buffer + - "@vekexasia/bigint-buffer2" - classic-level - core-js - cpu-features diff --git a/specrefs/.ethspecify.yml b/specrefs/.ethspecify.yml new file mode 100644 index 000000000000..fe080740ccf4 --- /dev/null +++ b/specrefs/.ethspecify.yml @@ -0,0 +1,325 @@ +version: v1.7.0-alpha.1 +style: full + +specrefs: + auto_add_missing_entries: true + auto_standardize_names: true + require_exceptions_have_fork: true + + files: + - configs.yml + - constants.yml + - containers.yml + - dataclasses.yml + - functions.yml + - presets.yml + - types.yml + +exceptions: + constants: + # phase0 + - ENDIANNESS#phase0 + - ETH_TO_GWEI#phase0 + - SAFETY_DECAY#phase0 + - UINT64_MAX#phase0 + - UINT64_MAX_SQRT#phase0 + + # deneb + - BLS_MODULUS#deneb + - BYTES_PER_COMMITMENT#deneb + - BYTES_PER_PROOF#deneb + - FIAT_SHAMIR_PROTOCOL_DOMAIN#deneb + - G1_POINT_AT_INFINITY#deneb + - KZG_ENDIANNESS#deneb + - KZG_SETUP_G2_LENGTH#deneb + - KZG_SETUP_G2_MONOMIAL#deneb + - PRIMITIVE_ROOT_OF_UNITY#deneb + - RANDOM_CHALLENGE_KZG_BATCH_DOMAIN#deneb + + # fulu + - RANDOM_CHALLENGE_KZG_CELL_BATCH_DOMAIN#fulu + - UINT256_MAX#fulu + + # gloas + - ATTESTATION_TIMELINESS_INDEX#gloas + - NUM_BLOCK_TIMELINESS_DEADLINES#gloas + - PAYLOAD_STATUS_EMPTY#gloas + - PAYLOAD_STATUS_FULL#gloas + - PAYLOAD_STATUS_PENDING#gloas + - PTC_TIMELINESS_INDEX#gloas + + containers: + # gloas + - ForkChoiceNode#gloas + + dataclasses: + # phase0 + - LatestMessage#phase0 + + # bellatrix + - OptimisticStore#bellatrix + + # capella + - ExpectedWithdrawals#capella + + # electra + - ExpectedWithdrawals#electra + + # gloas + - ExpectedWithdrawals#gloas + - LatestMessage#gloas + - Store#gloas + + functions: + # phase0 + - bytes_to_uint64#phase0 + - compute_fork_version#phase0 + - compute_proposer_score#phase0 + - get_aggregate_signature#phase0 + - get_attestation_component_deltas#phase0 + - get_attestation_score#phase0 + - get_attesting_balance#phase0 + - get_base_reward#phase0 + - get_checkpoint_block#phase0 + - get_current_store_epoch#phase0 + - get_eligible_validator_indices#phase0 + - get_eth1_vote#phase0 + - get_filtered_block_tree#phase0 + - get_forkchoice_store#phase0 + - get_head_deltas#phase0 + - get_inactivity_penalty_deltas#phase0 + - get_inclusion_delay_deltas#phase0 + - get_matching_head_attestations#phase0 + - get_matching_source_attestations#phase0 + - get_matching_target_attestations#phase0 + - get_proposer_reward#phase0 + - get_proposer_score#phase0 + - get_source_deltas#phase0 + - get_target_deltas#phase0 + - get_total_active_balance#phase0 + - get_unslashed_attesting_indices#phase0 + - get_voting_source#phase0 + - get_weight#phase0 + - is_candidate_block#phase0 + - is_proposer#phase0 + - is_proposer_equivocation#phase0 + - max_compressed_len#phase0 + - max_message_size#phase0 + - record_block_timeliness#phase0 + - saturating_sub#phase0 + - seconds_to_milliseconds#phase0 + - store_target_checkpoint_state#phase0 + - update_proposer_boost_root#phase0 + - validate_target_epoch_against_current_time#phase0 + - voting_period_start_time#phase0 + + # altair + - add_flag#altair + - apply_light_client_update#altair + - compute_fork_version#altair + - compute_merkle_proof#altair + - compute_subnets_for_sync_committee#altair + - create_light_client_bootstrap#altair + - create_light_client_finality_update#altair + - create_light_client_optimistic_update#altair + - create_light_client_update#altair + - current_sync_committee_gindex_at_slot#altair + - eth_aggregate_pubkeys#altair + - eth_fast_aggregate_verify#altair + - finalized_root_gindex_at_slot#altair + - get_base_reward#altair + - get_subtree_index#altair + - get_sync_subcommittee_pubkeys#altair + - get_unslashed_participating_indices#altair + - is_assigned_to_sync_committee#altair + - is_next_sync_committee_known#altair + - is_valid_normalized_merkle_branch#altair + - next_sync_committee_gindex_at_slot#altair + - process_light_client_finality_update#altair + - process_light_client_optimistic_update#altair + - process_light_client_store_force_update#altair + - set_or_append_list#altair + + # bellatrix + - compute_fork_version#bellatrix + - get_execution_payload#bellatrix + - get_pow_block_at_terminal_total_difficulty#bellatrix + - get_terminal_pow_block#bellatrix + - is_execution_block#bellatrix + - is_merge_transition_block#bellatrix + - is_optimistic#bellatrix + - is_optimistic_candidate_block#bellatrix + - is_valid_terminal_pow_block#bellatrix + - latest_verified_ancestor#bellatrix + - validate_merge_block#bellatrix + + # capella + - apply_withdrawals#capella + - compute_fork_version#capella + - get_balance_after_withdrawals#capella + - get_lc_execution_root#capella + - get_validators_sweep_withdrawals#capella + - is_fully_withdrawable_validator#capella + - is_partially_withdrawable_validator#capella + - update_next_withdrawal_index#capella + - update_next_withdrawal_validator_index#capella + + # deneb + - bit_reversal_permutation#deneb + - blob_to_kzg_commitment#deneb + - blob_to_polynomial#deneb + - bls_field_to_bytes#deneb + - bytes_to_bls_field#deneb + - bytes_to_kzg_commitment#deneb + - bytes_to_kzg_proof#deneb + - compute_blob_kzg_proof#deneb + - compute_challenge#deneb + - compute_fork_version#deneb + - compute_kzg_proof#deneb + - compute_kzg_proof_impl#deneb + - compute_powers#deneb + - compute_quotient_eval_within_domain#deneb + - compute_roots_of_unity#deneb + - evaluate_polynomial_in_evaluation_form#deneb + - g1_lincomb#deneb + - get_lc_execution_root#deneb + - hash_to_bls_field#deneb + - is_power_of_two#deneb + - multi_exp#deneb + - reverse_bits#deneb + - validate_kzg_g1#deneb + - verify_blob_kzg_proof#deneb + - verify_blob_kzg_proof_batch#deneb + - verify_kzg_proof#deneb + - verify_kzg_proof_batch#deneb + - verify_kzg_proof_impl#deneb + + # electra + - compute_fork_version#electra + - compute_on_chain_aggregate#electra + - current_sync_committee_gindex_at_slot#electra + - finalized_root_gindex_at_slot#electra + - get_committee_indices#electra + - get_eth1_pending_deposit_count#electra + - get_eth1_vote#electra + - get_execution_requests#electra + - get_lc_execution_root#electra + - get_pending_partial_withdrawals#electra + - get_validators_sweep_withdrawals#electra + - is_eligible_for_partial_withdrawals#electra + - is_fully_withdrawable_validator#electra + - is_partially_withdrawable_validator#electra + - is_valid_switch_to_compounding_request#electra + - next_sync_committee_gindex_at_slot#electra + - update_pending_partial_withdrawals#electra + + # fulu + - _fft_field#fulu + - add_polynomialcoeff#fulu + - cell_to_coset_evals#fulu + - compute_cells#fulu + - compute_cells_and_kzg_proofs#fulu + - compute_cells_and_kzg_proofs_polynomialcoeff#fulu + - compute_fork_version#fulu + - compute_kzg_proof_multi_impl#fulu + - compute_verify_cell_kzg_proof_batch_challenge#fulu + - construct_vanishing_polynomial#fulu + - coset_evals_to_cell#fulu + - coset_fft_field#fulu + - coset_for_cell#fulu + - coset_shift_for_cell#fulu + - divide_polynomialcoeff#fulu + - evaluate_polynomialcoeff#fulu + - fft_field#fulu + - get_beacon_proposer_indices#fulu + - interpolate_polynomialcoeff#fulu + - multiply_polynomialcoeff#fulu + - polynomial_eval_to_coeff#fulu + - recover_cells_and_kzg_proofs#fulu + - recover_polynomialcoeff#fulu + - vanishing_polynomialcoeff#fulu + - verify_cell_kzg_proof_batch#fulu + - verify_cell_kzg_proof_batch_impl#fulu + + # gloas + - add_builder_to_registry#gloas + - apply_withdrawals#gloas + - compute_balance_weighted_acceptance#gloas + - compute_balance_weighted_selection#gloas + - compute_fork_version#gloas + - compute_proposer_indices#gloas + - get_ancestor#gloas + - get_attestation_participation_flag_indices#gloas + - get_attestation_score#gloas + - get_builder_from_deposit#gloas + - get_builder_withdrawals#gloas + - get_builders_sweep_withdrawals#gloas + - get_checkpoint_block#gloas + - get_data_column_sidecars#gloas + - get_data_column_sidecars_from_block#gloas + - get_data_column_sidecars_from_column_sidecar#gloas + - get_execution_payload_bid_signature#gloas + - get_execution_payload_envelope_signature#gloas + - get_forkchoice_store#gloas + - get_head#gloas + - get_index_for_new_builder#gloas + - get_next_sync_committee_indices#gloas + - get_node_children#gloas + - get_parent_payload_status#gloas + - get_payload_attestation_due_ms#gloas + - get_payload_attestation_message_signature#gloas + - get_payload_status_tiebreaker#gloas + - get_pending_balance_to_withdraw#gloas + - get_proposer_preferences_signature#gloas + - get_ptc_assignment#gloas + - get_upcoming_proposal_slots#gloas + - get_weight#gloas + - has_compounding_withdrawal_credential#gloas + - is_head_late#gloas + - is_head_weak#gloas + - is_parent_node_full#gloas + - is_parent_strong#gloas + - is_payload_timely#gloas + - is_supporting_vote#gloas + - is_valid_proposal_slot#gloas + - notify_ptc_messages#gloas + - on_block#gloas + - on_execution_payload#gloas + - on_payload_attestation_message#gloas + - prepare_execution_payload#gloas + - process_attestation#gloas + - process_block#gloas + - process_epoch#gloas + - process_execution_payload#gloas + - process_operations#gloas + - process_proposer_slashing#gloas + - process_slot#gloas + - process_withdrawals#gloas + - record_block_timeliness#gloas + - should_apply_proposer_boost#gloas + - should_extend_payload#gloas + - update_builder_pending_withdrawals#gloas + - update_latest_messages#gloas + - update_next_withdrawal_builder_index#gloas + - update_payload_expected_withdrawals#gloas + - update_proposer_boost_root#gloas + - validate_merge_block#gloas + - validate_on_attestation#gloas + - verify_data_column_sidecar#gloas + + custom_types: + # phase0 + - Ether#phase0 + - Hash32#phase0 + - NodeID#phase0 + - SubnetID#phase0 + + # bellatrix + - PayloadId#bellatrix + + # fulu + - CellIndex#fulu + - CommitmentIndex#fulu + + # gloas + - PayloadStatus#gloas diff --git a/specrefs/README.md b/specrefs/README.md new file mode 100644 index 000000000000..d6d10659a7e9 --- /dev/null +++ b/specrefs/README.md @@ -0,0 +1,35 @@ +# Specification References + +This directory contains specification reference tracking files managed by +[ethspecify](https://github.com/jtraglia/ethspecify). + +## Installation + +Install `ethspecify` with the following command: + +```bash +pipx install ethspecify +``` + +> [!NOTE] +> You can run `ethspecify ` in the `specrefs` directory or +> `ethspecify --path=specrefs` from the project's root directory. + +## Maintenance + +When adding support for a new specification version, follow these steps: + +0. Change directory into the `specrefs` directory. +1. Update the version in `.ethspecify.yml` configuration. +2. Run `ethspecify process` to update/populate specrefs. +3. Run `ethspecify check` to check specrefs. +4. If there are errors, use the error message as a guide to fix the issue. If + there are new specrefs with empty sources, implement/locate each item and + update each specref source list. If you choose not to implement an item, + add an exception to the appropriate section in the `.ethspecify.yml` + configuration. +5. Repeat steps 3 and 4 until `ethspecify check` passes. +6. Run `git diff` to view updated specrefs. If an object/function/etc has + changed, make the necessary updates to the implementation. +7. Lastly, in the project's root directory, run `act -j check-specrefs` to + ensure everything is correct. diff --git a/specrefs/configs.yml b/specrefs/configs.yml new file mode 100644 index 000000000000..795ae93a6e78 --- /dev/null +++ b/specrefs/configs.yml @@ -0,0 +1,730 @@ +- name: AGGREGATE_DUE_BPS#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "AGGREGATE_DUE_BPS:" + spec: | + + AGGREGATE_DUE_BPS: uint64 = 6667 + + +- name: AGGREGATE_DUE_BPS_GLOAS#gloas + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "AGGREGATE_DUE_BPS_GLOAS:" + spec: | + + AGGREGATE_DUE_BPS_GLOAS: uint64 = 5000 + + +- name: ALTAIR_FORK_EPOCH#altair + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "ALTAIR_FORK_EPOCH:" + spec: | + + ALTAIR_FORK_EPOCH: Epoch = 74240 + + +- name: ALTAIR_FORK_VERSION#altair + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "ALTAIR_FORK_VERSION:" + spec: | + + ALTAIR_FORK_VERSION: Version = '0x01000000' + + +- name: ATTESTATION_DUE_BPS#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: '^\s+ATTESTATION_DUE_BPS:' + regex: true + spec: | + + ATTESTATION_DUE_BPS: uint64 = 3333 + + +- name: ATTESTATION_DUE_BPS_GLOAS#gloas + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "ATTESTATION_DUE_BPS_GLOAS:" + spec: | + + ATTESTATION_DUE_BPS_GLOAS: uint64 = 2500 + + +- name: ATTESTATION_PROPAGATION_SLOT_RANGE#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "ATTESTATION_PROPAGATION_SLOT_RANGE:" + spec: | + + ATTESTATION_PROPAGATION_SLOT_RANGE = 32 + + +- name: ATTESTATION_SUBNET_COUNT#phase0 + sources: + - file: packages/params/src/index.ts + search: export const ATTESTATION_SUBNET_COUNT = + spec: | + + ATTESTATION_SUBNET_COUNT = 64 + + +- name: ATTESTATION_SUBNET_EXTRA_BITS#phase0 + sources: + - file: packages/params/src/index.ts + search: export const ATTESTATION_SUBNET_EXTRA_BITS = + spec: | + + ATTESTATION_SUBNET_EXTRA_BITS = 0 + + +- name: ATTESTATION_SUBNET_PREFIX_BITS#phase0 + sources: + - file: packages/params/src/index.ts + search: export const ATTESTATION_SUBNET_PREFIX_BITS = + spec: | + + ATTESTATION_SUBNET_PREFIX_BITS: int = 6 + + +- name: BALANCE_PER_ADDITIONAL_CUSTODY_GROUP#fulu + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "BALANCE_PER_ADDITIONAL_CUSTODY_GROUP:" + spec: | + + BALANCE_PER_ADDITIONAL_CUSTODY_GROUP: Gwei = 32000000000 + + +- name: BELLATRIX_FORK_EPOCH#bellatrix + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "BELLATRIX_FORK_EPOCH:" + spec: | + + BELLATRIX_FORK_EPOCH: Epoch = 144896 + + +- name: BELLATRIX_FORK_VERSION#bellatrix + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "BELLATRIX_FORK_VERSION:" + spec: | + + BELLATRIX_FORK_VERSION: Version = '0x02000000' + + +- name: BLOB_SCHEDULE#fulu + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "BLOB_SCHEDULE:" + spec: | + + BLOB_SCHEDULE: tuple[frozendict[str, Any], ...] = ( + frozendict({ + "EPOCH": 412672, + "MAX_BLOBS_PER_BLOCK": 15, + }), + frozendict({ + "EPOCH": 419072, + "MAX_BLOBS_PER_BLOCK": 21, + }), + ) + + +- name: BLOB_SIDECAR_SUBNET_COUNT#deneb + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "BLOB_SIDECAR_SUBNET_COUNT:" + spec: | + + BLOB_SIDECAR_SUBNET_COUNT = 6 + + +- name: BLOB_SIDECAR_SUBNET_COUNT_ELECTRA#electra + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "BLOB_SIDECAR_SUBNET_COUNT_ELECTRA:" + spec: | + + BLOB_SIDECAR_SUBNET_COUNT_ELECTRA = 9 + + +- name: CAPELLA_FORK_EPOCH#capella + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "CAPELLA_FORK_EPOCH:" + spec: | + + CAPELLA_FORK_EPOCH: Epoch = 194048 + + +- name: CAPELLA_FORK_VERSION#capella + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "CAPELLA_FORK_VERSION:" + spec: | + + CAPELLA_FORK_VERSION: Version = '0x03000000' + + +- name: CHURN_LIMIT_QUOTIENT#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "CHURN_LIMIT_QUOTIENT:" + spec: | + + CHURN_LIMIT_QUOTIENT: uint64 = 65536 + + +- name: CONTRIBUTION_DUE_BPS#altair + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "CONTRIBUTION_DUE_BPS:" + spec: | + + CONTRIBUTION_DUE_BPS: uint64 = 6667 + + +- name: CONTRIBUTION_DUE_BPS_GLOAS#gloas + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "CONTRIBUTION_DUE_BPS_GLOAS:" + spec: | + + CONTRIBUTION_DUE_BPS_GLOAS: uint64 = 5000 + + +- name: CUSTODY_REQUIREMENT#fulu + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: '^\s+CUSTODY_REQUIREMENT:' + regex: true + spec: | + + CUSTODY_REQUIREMENT = 4 + + +- name: DATA_COLUMN_SIDECAR_SUBNET_COUNT#fulu + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "DATA_COLUMN_SIDECAR_SUBNET_COUNT:" + spec: | + + DATA_COLUMN_SIDECAR_SUBNET_COUNT = 128 + + +- name: DENEB_FORK_EPOCH#deneb + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "DENEB_FORK_EPOCH:" + spec: | + + DENEB_FORK_EPOCH: Epoch = 269568 + + +- name: DENEB_FORK_VERSION#deneb + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "DENEB_FORK_VERSION:" + spec: | + + DENEB_FORK_VERSION: Version = '0x04000000' + + +- name: EJECTION_BALANCE#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "EJECTION_BALANCE:" + spec: | + + EJECTION_BALANCE: Gwei = 16000000000 + + +- name: ELECTRA_FORK_EPOCH#electra + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "ELECTRA_FORK_EPOCH:" + spec: | + + ELECTRA_FORK_EPOCH: Epoch = 364032 + + +- name: ELECTRA_FORK_VERSION#electra + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "ELECTRA_FORK_VERSION:" + spec: | + + ELECTRA_FORK_VERSION: Version = '0x05000000' + + +- name: EPOCHS_PER_SUBNET_SUBSCRIPTION#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "EPOCHS_PER_SUBNET_SUBSCRIPTION:" + spec: | + + EPOCHS_PER_SUBNET_SUBSCRIPTION = 256 + + +- name: ETH1_FOLLOW_DISTANCE#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "ETH1_FOLLOW_DISTANCE:" + spec: | + + ETH1_FOLLOW_DISTANCE: uint64 = 2048 + + +- name: FULU_FORK_EPOCH#fulu + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "FULU_FORK_EPOCH:" + spec: | + + FULU_FORK_EPOCH: Epoch = 411392 + + +- name: FULU_FORK_VERSION#fulu + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "FULU_FORK_VERSION:" + spec: | + + FULU_FORK_VERSION: Version = '0x06000000' + + +- name: GENESIS_DELAY#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "GENESIS_DELAY:" + spec: | + + GENESIS_DELAY: uint64 = 604800 + + +- name: GENESIS_FORK_VERSION#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "GENESIS_FORK_VERSION:" + spec: | + + GENESIS_FORK_VERSION: Version = '0x00000000' + + +- name: GLOAS_FORK_EPOCH#gloas + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "GLOAS_FORK_EPOCH:" + spec: | + + GLOAS_FORK_EPOCH: Epoch = 18446744073709551615 + + +- name: GLOAS_FORK_VERSION#gloas + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "GLOAS_FORK_VERSION:" + spec: | + + GLOAS_FORK_VERSION: Version = '0x07000000' + + +- name: INACTIVITY_SCORE_BIAS#altair + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "INACTIVITY_SCORE_BIAS:" + spec: | + + INACTIVITY_SCORE_BIAS: uint64 = 4 + + +- name: INACTIVITY_SCORE_RECOVERY_RATE#altair + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "INACTIVITY_SCORE_RECOVERY_RATE:" + spec: | + + INACTIVITY_SCORE_RECOVERY_RATE: uint64 = 16 + + +- name: MAXIMUM_GOSSIP_CLOCK_DISPARITY#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "MAXIMUM_GOSSIP_CLOCK_DISPARITY:" + spec: | + + MAXIMUM_GOSSIP_CLOCK_DISPARITY = 500 + + +- name: MAX_BLOBS_PER_BLOCK#deneb + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "MAX_BLOBS_PER_BLOCK: 6" + spec: | + + MAX_BLOBS_PER_BLOCK: uint64 = 6 + + +- name: MAX_BLOBS_PER_BLOCK_ELECTRA#electra + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "MAX_BLOBS_PER_BLOCK_ELECTRA:" + spec: | + + MAX_BLOBS_PER_BLOCK_ELECTRA: uint64 = 9 + + +- name: MAX_PAYLOAD_SIZE#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "MAX_PAYLOAD_SIZE:" + spec: | + + MAX_PAYLOAD_SIZE = 10485760 + + +- name: MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT#deneb + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT:" + spec: | + + MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: uint64 = 8 + + +- name: MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT#electra + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT:" + spec: | + + MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT: Gwei = 256000000000 + + +- name: MAX_REQUEST_BLOB_SIDECARS#deneb + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "MAX_REQUEST_BLOB_SIDECARS:" + spec: | + + MAX_REQUEST_BLOB_SIDECARS = 768 + + +- name: MAX_REQUEST_BLOB_SIDECARS_ELECTRA#electra + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "MAX_REQUEST_BLOB_SIDECARS_ELECTRA:" + spec: | + + MAX_REQUEST_BLOB_SIDECARS_ELECTRA = 1152 + + +- name: MAX_REQUEST_BLOCKS#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "MAX_REQUEST_BLOCKS:" + spec: | + + MAX_REQUEST_BLOCKS = 1024 + + +- name: MAX_REQUEST_BLOCKS_DENEB#deneb + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "MAX_REQUEST_BLOCKS_DENEB:" + spec: | + + MAX_REQUEST_BLOCKS_DENEB = 128 + + +- name: MAX_REQUEST_DATA_COLUMN_SIDECARS#fulu + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "MAX_REQUEST_DATA_COLUMN_SIDECARS:" + spec: | + + MAX_REQUEST_DATA_COLUMN_SIDECARS = 16384 + + +- name: MAX_REQUEST_PAYLOADS#gloas + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "MAX_REQUEST_PAYLOADS:" + spec: | + + MAX_REQUEST_PAYLOADS = 128 + + +- name: MESSAGE_DOMAIN_INVALID_SNAPPY#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "MESSAGE_DOMAIN_INVALID_SNAPPY:" + spec: | + + MESSAGE_DOMAIN_INVALID_SNAPPY: DomainType = '0x00000000' + + +- name: MESSAGE_DOMAIN_VALID_SNAPPY#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "MESSAGE_DOMAIN_VALID_SNAPPY:" + spec: | + + MESSAGE_DOMAIN_VALID_SNAPPY: DomainType = '0x01000000' + + +- name: MIN_BUILDER_WITHDRAWABILITY_DELAY#gloas + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "MIN_BUILDER_WITHDRAWABILITY_DELAY:" + spec: | + + MIN_BUILDER_WITHDRAWABILITY_DELAY: uint64 = 4096 + + +- name: MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS#deneb + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS:" + spec: | + + MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS = 4096 + + +- name: MIN_EPOCHS_FOR_BLOCK_REQUESTS#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "MIN_EPOCHS_FOR_BLOCK_REQUESTS:" + spec: | + + MIN_EPOCHS_FOR_BLOCK_REQUESTS = 33024 + + +- name: MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS#fulu + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS:" + spec: | + + MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS = 4096 + + +- name: MIN_GENESIS_ACTIVE_VALIDATOR_COUNT#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "MIN_GENESIS_ACTIVE_VALIDATOR_COUNT:" + spec: | + + MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: uint64 = 16384 + + +- name: MIN_GENESIS_TIME#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "MIN_GENESIS_TIME:" + spec: | + + MIN_GENESIS_TIME: uint64 = 1606824000 + + +- name: MIN_PER_EPOCH_CHURN_LIMIT#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "MIN_PER_EPOCH_CHURN_LIMIT:" + spec: | + + MIN_PER_EPOCH_CHURN_LIMIT: uint64 = 4 + + +- name: MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA#electra + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA:" + spec: | + + MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA: Gwei = 128000000000 + + +- name: MIN_VALIDATOR_WITHDRAWABILITY_DELAY#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "MIN_VALIDATOR_WITHDRAWABILITY_DELAY:" + spec: | + + MIN_VALIDATOR_WITHDRAWABILITY_DELAY: uint64 = 256 + + +- name: NUMBER_OF_CUSTODY_GROUPS#fulu + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "NUMBER_OF_CUSTODY_GROUPS:" + spec: | + + NUMBER_OF_CUSTODY_GROUPS = 128 + + +- name: PAYLOAD_ATTESTATION_DUE_BPS#gloas + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "PAYLOAD_ATTESTATION_DUE_BPS:" + spec: | + + PAYLOAD_ATTESTATION_DUE_BPS: uint64 = 7500 + + +- name: PROPOSER_REORG_CUTOFF_BPS#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "PROPOSER_REORG_CUTOFF_BPS:" + spec: | + + PROPOSER_REORG_CUTOFF_BPS: uint64 = 1667 + + +- name: PROPOSER_SCORE_BOOST#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "PROPOSER_SCORE_BOOST:" + spec: | + + PROPOSER_SCORE_BOOST: uint64 = 40 + + +- name: REORG_HEAD_WEIGHT_THRESHOLD#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "REORG_HEAD_WEIGHT_THRESHOLD:" + spec: | + + REORG_HEAD_WEIGHT_THRESHOLD: uint64 = 20 + + +- name: REORG_MAX_EPOCHS_SINCE_FINALIZATION#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "REORG_MAX_EPOCHS_SINCE_FINALIZATION:" + spec: | + + REORG_MAX_EPOCHS_SINCE_FINALIZATION: Epoch = 2 + + +- name: REORG_PARENT_WEIGHT_THRESHOLD#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "REORG_PARENT_WEIGHT_THRESHOLD:" + spec: | + + REORG_PARENT_WEIGHT_THRESHOLD: uint64 = 160 + + +- name: SAMPLES_PER_SLOT#fulu + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "SAMPLES_PER_SLOT:" + spec: | + + SAMPLES_PER_SLOT = 8 + + +- name: SECONDS_PER_ETH1_BLOCK#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "SECONDS_PER_ETH1_BLOCK:" + spec: | + + SECONDS_PER_ETH1_BLOCK: uint64 = 14 + + +- name: SECONDS_PER_SLOT#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "SECONDS_PER_SLOT:" + spec: | + + SECONDS_PER_SLOT: uint64 = 12 + + +- name: SHARD_COMMITTEE_PERIOD#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "SHARD_COMMITTEE_PERIOD:" + spec: | + + SHARD_COMMITTEE_PERIOD: uint64 = 256 + + +- name: SLOT_DURATION_MS#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "SLOT_DURATION_MS:" + spec: | + + SLOT_DURATION_MS: uint64 = 12000 + + +- name: SUBNETS_PER_NODE#phase0 + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "SUBNETS_PER_NODE:" + spec: | + + SUBNETS_PER_NODE = 2 + + +- name: SYNC_MESSAGE_DUE_BPS#altair + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "SYNC_MESSAGE_DUE_BPS:" + spec: | + + SYNC_MESSAGE_DUE_BPS: uint64 = 3333 + + +- name: SYNC_MESSAGE_DUE_BPS_GLOAS#gloas + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "SYNC_MESSAGE_DUE_BPS_GLOAS:" + spec: | + + SYNC_MESSAGE_DUE_BPS_GLOAS: uint64 = 2500 + + +- name: TERMINAL_BLOCK_HASH#bellatrix + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "TERMINAL_BLOCK_HASH:" + spec: | + + TERMINAL_BLOCK_HASH: Hash32 = '0x0000000000000000000000000000000000000000000000000000000000000000' + + +- name: TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH#bellatrix + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH:" + spec: | + + TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH = 18446744073709551615 + + +- name: TERMINAL_TOTAL_DIFFICULTY#bellatrix + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "TERMINAL_TOTAL_DIFFICULTY:" + spec: | + + TERMINAL_TOTAL_DIFFICULTY = 58750000000000000000000 + + +- name: VALIDATOR_CUSTODY_REQUIREMENT#fulu + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "VALIDATOR_CUSTODY_REQUIREMENT:" + spec: | + + VALIDATOR_CUSTODY_REQUIREMENT = 8 + diff --git a/specrefs/constants.yml b/specrefs/constants.yml new file mode 100644 index 000000000000..2c642759d5f5 --- /dev/null +++ b/specrefs/constants.yml @@ -0,0 +1,655 @@ +- name: ATTESTATION_TIMELINESS_INDEX#gloas + sources: [] + spec: | + + ATTESTATION_TIMELINESS_INDEX = 0 + + +- name: BASE_REWARDS_PER_EPOCH#phase0 + sources: + - file: packages/params/src/index.ts + search: export const BASE_REWARDS_PER_EPOCH = + spec: | + + BASE_REWARDS_PER_EPOCH: uint64 = 4 + + +- name: BASIS_POINTS#phase0 + sources: + - file: packages/params/src/index.ts + search: export const BASIS_POINTS = + spec: | + + BASIS_POINTS: uint64 = 10000 + + +- name: BLS_MODULUS#deneb + sources: [] + spec: | + + BLS_MODULUS = 52435875175126190479447740508185965837690552500527637822603658699938581184513 + + +- name: BLS_WITHDRAWAL_PREFIX#phase0 + sources: + - file: packages/params/src/index.ts + search: export const BLS_WITHDRAWAL_PREFIX = + spec: | + + BLS_WITHDRAWAL_PREFIX: Bytes1 = '0x00' + + +- name: BUILDER_INDEX_FLAG#gloas + sources: + - file: packages/params/src/index.ts + search: export const BUILDER_INDEX_FLAG = + spec: | + + BUILDER_INDEX_FLAG: uint64 = 2**40 + + +- name: BUILDER_INDEX_SELF_BUILD#gloas + sources: + - file: packages/params/src/index.ts + search: export const BUILDER_INDEX_SELF_BUILD = + spec: | + + BUILDER_INDEX_SELF_BUILD: BuilderIndex = UINT64_MAX + + +- name: BUILDER_PAYMENT_THRESHOLD_DENOMINATOR#gloas + sources: + - file: packages/params/src/index.ts + search: export const BUILDER_PAYMENT_THRESHOLD_DENOMINATOR = + spec: | + + BUILDER_PAYMENT_THRESHOLD_DENOMINATOR: uint64 = 10 + + +- name: BUILDER_PAYMENT_THRESHOLD_NUMERATOR#gloas + sources: + - file: packages/params/src/index.ts + search: export const BUILDER_PAYMENT_THRESHOLD_NUMERATOR = + spec: | + + BUILDER_PAYMENT_THRESHOLD_NUMERATOR: uint64 = 6 + + +- name: BUILDER_WITHDRAWAL_PREFIX#gloas + sources: + - file: packages/params/src/index.ts + search: export const BUILDER_WITHDRAWAL_PREFIX = + spec: | + + BUILDER_WITHDRAWAL_PREFIX: Bytes1 = '0x03' + + +- name: BYTES_PER_COMMITMENT#deneb + sources: [] + spec: | + + BYTES_PER_COMMITMENT: uint64 = 48 + + +- name: BYTES_PER_FIELD_ELEMENT#deneb + sources: + - file: packages/params/src/index.ts + search: export const BYTES_PER_FIELD_ELEMENT = + spec: | + + BYTES_PER_FIELD_ELEMENT: uint64 = 32 + + +- name: BYTES_PER_PROOF#deneb + sources: [] + spec: | + + BYTES_PER_PROOF: uint64 = 48 + + +- name: COMPOUNDING_WITHDRAWAL_PREFIX#electra + sources: + - file: packages/params/src/index.ts + search: export const COMPOUNDING_WITHDRAWAL_PREFIX = + spec: | + + COMPOUNDING_WITHDRAWAL_PREFIX: Bytes1 = '0x02' + + +- name: CONSOLIDATION_REQUEST_TYPE#electra + sources: + - file: packages/params/src/index.ts + search: export const CONSOLIDATION_REQUEST_TYPE = + spec: | + + CONSOLIDATION_REQUEST_TYPE: Bytes1 = '0x02' + + +- name: DEPOSIT_CONTRACT_TREE_DEPTH#phase0 + sources: + - file: packages/params/src/index.ts + search: export const DEPOSIT_CONTRACT_TREE_DEPTH = + spec: | + + DEPOSIT_CONTRACT_TREE_DEPTH: uint64 = 2**5 + + +- name: DEPOSIT_REQUEST_TYPE#electra + sources: + - file: packages/params/src/index.ts + search: export const DEPOSIT_REQUEST_TYPE = + spec: | + + DEPOSIT_REQUEST_TYPE: Bytes1 = '0x00' + + +- name: DOMAIN_AGGREGATE_AND_PROOF#phase0 + sources: + - file: packages/params/src/index.ts + search: export const DOMAIN_AGGREGATE_AND_PROOF = + spec: | + + DOMAIN_AGGREGATE_AND_PROOF: DomainType = '0x06000000' + + +- name: DOMAIN_APPLICATION_MASK#phase0 + sources: + - file: packages/params/src/index.ts + search: export const DOMAIN_APPLICATION_MASK = + spec: | + + DOMAIN_APPLICATION_MASK: DomainType = '0x00000001' + + +- name: DOMAIN_BEACON_ATTESTER#phase0 + sources: + - file: packages/params/src/index.ts + search: export const DOMAIN_BEACON_ATTESTER = + spec: | + + DOMAIN_BEACON_ATTESTER: DomainType = '0x01000000' + + +- name: DOMAIN_BEACON_BUILDER#gloas + sources: + - file: packages/params/src/index.ts + search: export const DOMAIN_BEACON_BUILDER = + spec: | + + DOMAIN_BEACON_BUILDER: DomainType = '0x0B000000' + + +- name: DOMAIN_BEACON_PROPOSER#phase0 + sources: + - file: packages/params/src/index.ts + search: export const DOMAIN_BEACON_PROPOSER = + spec: | + + DOMAIN_BEACON_PROPOSER: DomainType = '0x00000000' + + +- name: DOMAIN_BLS_TO_EXECUTION_CHANGE#capella + sources: + - file: packages/params/src/index.ts + search: export const DOMAIN_BLS_TO_EXECUTION_CHANGE = + spec: | + + DOMAIN_BLS_TO_EXECUTION_CHANGE: DomainType = '0x0A000000' + + +- name: DOMAIN_CONTRIBUTION_AND_PROOF#altair + sources: + - file: packages/params/src/index.ts + search: export const DOMAIN_CONTRIBUTION_AND_PROOF = + spec: | + + DOMAIN_CONTRIBUTION_AND_PROOF: DomainType = '0x09000000' + + +- name: DOMAIN_DEPOSIT#phase0 + sources: + - file: packages/params/src/index.ts + search: export const DOMAIN_DEPOSIT = + spec: | + + DOMAIN_DEPOSIT: DomainType = '0x03000000' + + +- name: DOMAIN_PROPOSER_PREFERENCES#gloas + sources: + - file: packages/params/src/index.ts + search: export const DOMAIN_PROPOSER_PREFERENCES = + spec: | + + DOMAIN_PROPOSER_PREFERENCES: DomainType = '0x0D000000' + + +- name: DOMAIN_PTC_ATTESTER#gloas + sources: + - file: packages/params/src/index.ts + search: export const DOMAIN_PTC_ATTESTER = + spec: | + + DOMAIN_PTC_ATTESTER: DomainType = '0x0C000000' + + +- name: DOMAIN_RANDAO#phase0 + sources: + - file: packages/params/src/index.ts + search: export const DOMAIN_RANDAO = + spec: | + + DOMAIN_RANDAO: DomainType = '0x02000000' + + +- name: DOMAIN_SELECTION_PROOF#phase0 + sources: + - file: packages/params/src/index.ts + search: export const DOMAIN_SELECTION_PROOF = + spec: | + + DOMAIN_SELECTION_PROOF: DomainType = '0x05000000' + + +- name: DOMAIN_SYNC_COMMITTEE#altair + sources: + - file: packages/params/src/index.ts + search: export const DOMAIN_SYNC_COMMITTEE = + spec: | + + DOMAIN_SYNC_COMMITTEE: DomainType = '0x07000000' + + +- name: DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF#altair + sources: + - file: packages/params/src/index.ts + search: export const DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF = + spec: | + + DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF: DomainType = '0x08000000' + + +- name: DOMAIN_VOLUNTARY_EXIT#phase0 + sources: + - file: packages/params/src/index.ts + search: export const DOMAIN_VOLUNTARY_EXIT = + spec: | + + DOMAIN_VOLUNTARY_EXIT: DomainType = '0x04000000' + + +- name: ENDIANNESS#phase0 + sources: [] + spec: | + + ENDIANNESS = 'little' + + +- name: ETH1_ADDRESS_WITHDRAWAL_PREFIX#phase0 + sources: + - file: packages/params/src/index.ts + search: export const ETH1_ADDRESS_WITHDRAWAL_PREFIX = + spec: | + + ETH1_ADDRESS_WITHDRAWAL_PREFIX: Bytes1 = '0x01' + + +- name: ETH_TO_GWEI#phase0 + sources: [] + spec: | + + ETH_TO_GWEI: uint64 = 10**9 + + +- name: FAR_FUTURE_EPOCH#phase0 + sources: + - file: packages/params/src/index.ts + search: export const FAR_FUTURE_EPOCH = + spec: | + + FAR_FUTURE_EPOCH: Epoch = 2**64 - 1 + + +- name: FIAT_SHAMIR_PROTOCOL_DOMAIN#deneb + sources: [] + spec: | + + FIAT_SHAMIR_PROTOCOL_DOMAIN = b'FSBLOBVERIFY_V1_' + + +- name: FULL_EXIT_REQUEST_AMOUNT#electra + sources: + - file: packages/params/src/index.ts + search: export const FULL_EXIT_REQUEST_AMOUNT = + spec: | + + FULL_EXIT_REQUEST_AMOUNT: uint64 = 0 + + +- name: G1_POINT_AT_INFINITY#deneb + sources: [] + spec: | + + G1_POINT_AT_INFINITY: Bytes48 = b'\xc0' + b'\x00' * 47 + + +- name: G2_POINT_AT_INFINITY#altair + sources: + - file: packages/state-transition/src/constants/constants.ts + search: export const G2_POINT_AT_INFINITY = + spec: | + + G2_POINT_AT_INFINITY: BLSSignature = b'\xc0' + b'\x00' * 95 + + +- name: GENESIS_EPOCH#phase0 + sources: + - file: packages/params/src/index.ts + search: export const GENESIS_EPOCH = + spec: | + + GENESIS_EPOCH: Epoch = 0 + + +- name: GENESIS_SLOT#phase0 + sources: + - file: packages/params/src/index.ts + search: export const GENESIS_SLOT = + spec: | + + GENESIS_SLOT: Slot = 0 + + +- name: JUSTIFICATION_BITS_LENGTH#phase0 + sources: + - file: packages/params/src/index.ts + search: export const JUSTIFICATION_BITS_LENGTH = + spec: | + + JUSTIFICATION_BITS_LENGTH: uint64 = 4 + + +- name: KZG_ENDIANNESS#deneb + sources: [] + spec: | + + KZG_ENDIANNESS = 'big' + + +- name: KZG_SETUP_G2_LENGTH#deneb + sources: [] + spec: | + + KZG_SETUP_G2_LENGTH = 65 + + +- name: KZG_SETUP_G2_MONOMIAL#deneb + sources: [] + spec: | + + KZG_SETUP_G2_MONOMIAL: Vector[G2Point, KZG_SETUP_G2_LENGTH] = ['0x93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8', '0xb5bfd7dd8cdeb128843bc287230af38926187075cbfbefa81009a2ce615ac53d2914e5870cb452d2afaaab24f3499f72185cbfee53492714734429b7b38608e23926c911cceceac9a36851477ba4c60b087041de621000edc98edada20c1def2', '0xb5337ba0ce5d37224290916e268e2060e5c14f3f9fc9e1ec3af5a958e7a0303122500ce18f1a4640bf66525bd10e763501fe986d86649d8d45143c08c3209db3411802c226e9fe9a55716ac4a0c14f9dcef9e70b2bb309553880dc5025eab3cc', '0xb3c1dcdc1f62046c786f0b82242ef283e7ed8f5626f72542aa2c7a40f14d9094dd1ebdbd7457ffdcdac45fd7da7e16c51200b06d791e5e43e257e45efdf0bd5b06cd2333beca2a3a84354eb48662d83aef5ecf4e67658c851c10b13d8d87c874', '0x954d91c7688983382609fca9e211e461f488a5971fd4e40d7e2892037268eacdfd495cfa0a7ed6eb0eb11ac3ae6f651716757e7526abe1e06c64649d80996fd3105c20c4c94bc2b22d97045356fe9d791f21ea6428ac48db6f9e68e30d875280', '0x88a6b6bb26c51cf9812260795523973bb90ce80f6820b6c9048ab366f0fb96e48437a7f7cb62aedf64b11eb4dfefebb0147608793133d32003cb1f2dc47b13b5ff45f1bb1b2408ea45770a08dbfaec60961acb8119c47b139a13b8641e2c9487', '0x85cd7be9728bd925d12f47fb04b32d9fad7cab88788b559f053e69ca18e463113ecc8bbb6dbfb024835f901b3a957d3108d6770fb26d4c8be0a9a619f6e3a4bf15cbfd48e61593490885f6cee30e4300c5f9cf5e1c08e60a2d5b023ee94fcad0', '0x80477dba360f04399821a48ca388c0fa81102dd15687fea792ee8c1114e00d1bc4839ad37ac58900a118d863723acfbe08126ea883be87f50e4eabe3b5e72f5d9e041db8d9b186409fd4df4a7dde38c0e0a3b1ae29b098e5697e7f110b6b27e4', '0xb7a6aec08715a9f8672a2b8c367e407be37e59514ac19dd4f0942a68007bba3923df22da48702c63c0d6b3efd3c2d04e0fe042d8b5a54d562f9f33afc4865dcbcc16e99029e25925580e87920c399e710d438ac1ce3a6dc9b0d76c064a01f6f7', '0xac1b001edcea02c8258aeffbf9203114c1c874ad88dae1184fadd7d94cd09053649efd0ca413400e6e9b5fa4eac33261000af88b6bd0d2abf877a4f0355d2fb4d6007adb181695201c5432e50b850b51b3969f893bddf82126c5a71b042b7686', '0x90043fda4de53fb364fab2c04be5296c215599105ecff0c12e4917c549257125775c29f2507124d15f56e30447f367db0596c33237242c02d83dfd058735f1e3c1ff99069af55773b6d51d32a68bf75763f59ec4ee7267932ae426522b8aaab6', '0xa8660ce853e9dc08271bf882e29cd53397d63b739584dda5263da4c7cc1878d0cf6f3e403557885f557e184700575fee016ee8542dec22c97befe1d10f414d22e84560741cdb3e74c30dda9b42eeaaf53e27822de2ee06e24e912bf764a9a533', '0x8fe3921a96d0d065e8aa8fce9aa42c8e1461ca0470688c137be89396dd05103606dab6cdd2a4591efd6addf72026c12e065da7be276dee27a7e30afa2bd81c18f1516e7f068f324d0bad9570b95f6bd02c727cd2343e26db0887c3e4e26dceda', '0x8ae1ad97dcb9c192c9a3933541b40447d1dc4eebf380151440bbaae1e120cc5cdf1bcea55180b128d8e180e3af623815191d063cc0d7a47d55fb7687b9d87040bf7bc1a7546b07c61db5ccf1841372d7c2fe4a5431ffff829f3c2eb590b0b710', '0x8c2fa96870a88150f7876c931e2d3cc2adeaaaf5c73ef5fa1cf9dfa0991ae4819f9321af7e916e5057d87338e630a2f21242c29d76963cf26035b548d2a63d8ad7bd6efefa01c1df502cbdfdfe0334fb21ceb9f686887440f713bf17a89b8081', '0xb9aa98e2f02bb616e22ee5dd74c7d1049321ac9214d093a738159850a1dbcc7138cb8d26ce09d8296368fd5b291d74fa17ac7cc1b80840fdd4ee35e111501e3fa8485b508baecda7c1ab7bd703872b7d64a2a40b3210b6a70e8a6ffe0e5127e3', '0x9292db67f8771cdc86854a3f614a73805bf3012b48f1541e704ea4015d2b6b9c9aaed36419769c87c49f9e3165f03edb159c23b3a49c4390951f78e1d9b0ad997129b17cdb57ea1a6638794c0cca7d239f229e589c5ae4f9fe6979f7f8cba1d7', '0x91cd9e86550f230d128664f7312591fee6a84c34f5fc7aed557bcf986a409a6de722c4330453a305f06911d2728626e611acfdf81284f77f60a3a1595053a9479964fd713117e27c0222cc679674b03bc8001501aaf9b506196c56de29429b46', '0xa9516b73f605cc31b89c68b7675dc451e6364595243d235339437f556cf22d745d4250c1376182273be2d99e02c10eee047410a43eff634d051aeb784e76cb3605d8e079b9eb6ad1957dfdf77e1cd32ce4a573c9dfcc207ca65af6eb187f6c3d', '0xa9667271f7d191935cc8ad59ef3ec50229945faea85bfdfb0d582090f524436b348aaa0183b16a6231c00332fdac2826125b8c857a2ed9ec66821cfe02b3a2279be2412441bc2e369b255eb98614e4be8490799c4df22f18d47d24ec70bba5f7', '0xa4371144d2aa44d70d3cb9789096d3aa411149a6f800cb46f506461ee8363c8724667974252f28aea61b6030c05930ac039c1ee64bb4bd56532a685cae182bf2ab935eee34718cffcb46cae214c77aaca11dbb1320faf23c47247db1da04d8dc', '0x89a7eb441892260b7e81168c386899cd84ffc4a2c5cad2eae0d1ab9e8b5524662e6f660fe3f8bfe4c92f60b060811bc605b14c5631d16709266886d7885a5eb5930097127ec6fb2ebbaf2df65909cf48f253b3d5e22ae48d3e9a2fd2b01f447e', '0x9648c42ca97665b5eccb49580d8532df05eb5a68db07f391a2340769b55119eaf4c52fe4f650c09250fa78a76c3a1e271799b8333cc2628e3d4b4a6a3e03da1f771ecf6516dd63236574a7864ff07e319a6f11f153406280d63af9e2b5713283', '0x9663bf6dd446ea7a90658ee458578d4196dc0b175ef7fcfa75f44d41670850774c2e46c5a6be132a2c072a3c0180a24f0305d1acac49d2d79878e5cda80c57feda3d01a6af12e78b5874e2a4b3717f11c97503b41a4474e2e95b179113726199', '0xb212aeb4814e0915b432711b317923ed2b09e076aaf558c3ae8ef83f9e15a83f9ea3f47805b2750ab9e8106cb4dc6ad003522c84b03dc02829978a097899c773f6fb31f7fe6b8f2d836d96580f216fec20158f1590c3e0d7850622e15194db05', '0x925f005059bf07e9ceccbe66c711b048e236ade775720d0fe479aebe6e23e8af281225ad18e62458dc1b03b42ad4ca290d4aa176260604a7aad0d9791337006fbdebe23746f8060d42876f45e4c83c3643931392fde1cd13ff8bddf8111ef974', '0x9553edb22b4330c568e156a59ef03b26f5c326424f830fe3e8c0b602f08c124730ffc40bc745bec1a22417adb22a1a960243a10565c2be3066bfdb841d1cd14c624cd06e0008f4beb83f972ce6182a303bee3fcbcabc6cfe48ec5ae4b7941bfc', '0x935f5a404f0a78bdcce709899eda0631169b366a669e9b58eacbbd86d7b5016d044b8dfc59ce7ed8de743ae16c2343b50e2f925e88ba6319e33c3fc76b314043abad7813677b4615c8a97eb83cc79de4fedf6ccbcfa4d4cbf759a5a84e4d9742', '0xa5b014ab936eb4be113204490e8b61cd38d71da0dec7215125bcd131bf3ab22d0a32ce645bca93e7b3637cf0c2db3d6601a0ddd330dc46f9fae82abe864ffc12d656c88eb50c20782e5bb6f75d18760666f43943abb644b881639083e122f557', '0x935b7298ae52862fa22bf03bfc1795b34c70b181679ae27de08a9f5b4b884f824ef1b276b7600efa0d2f1d79e4a470d51692fd565c5cf8343dd80e5d3336968fc21c09ba9348590f6206d4424eb229e767547daefa98bc3aa9f421158dee3f2a', '0x9830f92446e708a8f6b091cc3c38b653505414f8b6507504010a96ffda3bcf763d5331eb749301e2a1437f00e2415efb01b799ad4c03f4b02de077569626255ac1165f96ea408915d4cf7955047620da573e5c439671d1fa5c833fb11de7afe6', '0x840dcc44f673fff3e387af2bb41e89640f2a70bcd2b92544876daa92143f67c7512faf5f90a04b7191de01f3e2b1bde00622a20dc62ca23bbbfaa6ad220613deff43908382642d4d6a86999f662efd64b1df448b68c847cfa87630a3ffd2ec76', '0x92950c895ed54f7f876b2fda17ecc9c41b7accfbdd42c210cc5b475e0737a7279f558148531b5c916e310604a1de25a80940c94fe5389ae5d6a5e9c371be67bceea1877f5401725a6595bcf77ece60905151b6dfcb68b75ed2e708c73632f4fd', '0x8010246bf8e94c25fd029b346b5fbadb404ef6f44a58fd9dd75acf62433d8cc6db66974f139a76e0c26dddc1f329a88214dbb63276516cf325c7869e855d07e0852d622c332ac55609ba1ec9258c45746a2aeb1af0800141ee011da80af175d4', '0xb0f1bad257ebd187bdc3f37b23f33c6a5d6a8e1f2de586080d6ada19087b0e2bf23b79c1b6da1ee82271323f5bdf3e1b018586b54a5b92ab6a1a16bb3315190a3584a05e6c37d5ca1e05d702b9869e27f513472bcdd00f4d0502a107773097da', '0x9636d24f1ede773ce919f309448dd7ce023f424afd6b4b69cb98c2a988d849a283646dc3e469879daa1b1edae91ae41f009887518e7eb5578f88469321117303cd3ac2d7aee4d9cb5f82ab9ae3458e796dfe7c24284b05815acfcaa270ff22e2', '0xb373feb5d7012fd60578d7d00834c5c81df2a23d42794fed91aa9535a4771fde0341c4da882261785e0caca40bf83405143085e7f17e55b64f6c5c809680c20b050409bf3702c574769127c854d27388b144b05624a0e24a1cbcc4d08467005b', '0xb15680648949ce69f82526e9b67d9b55ce5c537dc6ab7f3089091a9a19a6b90df7656794f6edc87fb387d21573ffc847062623685931c2790a508cbc8c6b231dd2c34f4d37d4706237b1407673605a604bcf6a50cc0b1a2db20485e22b02c17e', '0x8817e46672d40c8f748081567b038a3165f87994788ec77ee8daea8587f5540df3422f9e120e94339be67f186f50952504cb44f61e30a5241f1827e501b2de53c4c64473bcc79ab887dd277f282fbfe47997a930dd140ac08b03efac88d81075', '0xa6e4ef6c1d1098f95aae119905f87eb49b909d17f9c41bcfe51127aa25fee20782ea884a7fdf7d5e9c245b5a5b32230b07e0dbf7c6743bf52ee20e2acc0b269422bd6cf3c07115df4aa85b11b2c16630a07c974492d9cdd0ec325a3fabd95044', '0x8634aa7c3d00e7f17150009698ce440d8e1b0f13042b624a722ace68ead870c3d2212fbee549a2c190e384d7d6ac37ce14ab962c299ea1218ef1b1489c98906c91323b94c587f1d205a6edd5e9d05b42d591c26494a6f6a029a2aadb5f8b6f67', '0x821a58092900bdb73decf48e13e7a5012a3f88b06288a97b855ef51306406e7d867d613d9ec738ebacfa6db344b677d21509d93f3b55c2ebf3a2f2a6356f875150554c6fff52e62e3e46f7859be971bf7dd9d5b3e1d799749c8a97c2e04325df', '0x8dba356577a3a388f782e90edb1a7f3619759f4de314ad5d95c7cc6e197211446819c4955f99c5fc67f79450d2934e3c09adefc91b724887e005c5190362245eec48ce117d0a94d6fa6db12eda4ba8dde608fbbd0051f54dcf3bb057adfb2493', '0xa32a690dc95c23ed9fb46443d9b7d4c2e27053a7fcc216d2b0020a8cf279729c46114d2cda5772fd60a97016a07d6c5a0a7eb085a18307d34194596f5b541cdf01b2ceb31d62d6b55515acfd2b9eec92b27d082fbc4dc59fc63b551eccdb8468', '0xa040f7f4be67eaf0a1d658a3175d65df21a7dbde99bfa893469b9b43b9d150fc2e333148b1cb88cfd0447d88fa1a501d126987e9fdccb2852ecf1ba907c2ca3d6f97b055e354a9789854a64ecc8c2e928382cf09dda9abde42bbdf92280cdd96', '0x864baff97fa60164f91f334e0c9be00a152a416556b462f96d7c43b59fe1ebaff42f0471d0bf264976f8aa6431176eb905bd875024cf4f76c13a70bede51dc3e47e10b9d5652d30d2663b3af3f08d5d11b9709a0321aba371d2ef13174dcfcaf', '0x95a46f32c994133ecc22db49bad2c36a281d6b574c83cfee6680b8c8100466ca034b815cfaedfbf54f4e75188e661df901abd089524e1e0eb0bf48d48caa9dd97482d2e8c1253e7e8ac250a32fd066d5b5cb08a8641bdd64ecfa48289dca83a3', '0xa2cce2be4d12144138cb91066e0cd0542c80b478bf467867ebef9ddaf3bd64e918294043500bf5a9f45ee089a8d6ace917108d9ce9e4f41e7e860cbce19ac52e791db3b6dde1c4b0367377b581f999f340e1d6814d724edc94cb07f9c4730774', '0xb145f203eee1ac0a1a1731113ffa7a8b0b694ef2312dabc4d431660f5e0645ef5838e3e624cfe1228cfa248d48b5760501f93e6ab13d3159fc241427116c4b90359599a4cb0a86d0bb9190aa7fabff482c812db966fd2ce0a1b48cb8ac8b3bca', '0xadabe5d215c608696e03861cbd5f7401869c756b3a5aadc55f41745ad9478145d44393fec8bb6dfc4ad9236dc62b9ada0f7ca57fe2bae1b71565dbf9536d33a68b8e2090b233422313cc96afc7f1f7e0907dc7787806671541d6de8ce47c4cd0', '0xae7845fa6b06db53201c1080e01e629781817f421f28956589c6df3091ec33754f8a4bd4647a6bb1c141ac22731e3c1014865d13f3ed538dcb0f7b7576435133d9d03be655f8fbb4c9f7d83e06d1210aedd45128c2b0c9bab45a9ddde1c862a5', '0x9159eaa826a24adfa7adf6e8d2832120ebb6eccbeb3d0459ffdc338548813a2d239d22b26451fda98cc0c204d8e1ac69150b5498e0be3045300e789bcb4e210d5cd431da4bdd915a21f407ea296c20c96608ded0b70d07188e96e6c1a7b9b86b', '0xa9fc6281e2d54b46458ef564ffaed6944bff71e389d0acc11fa35d3fcd8e10c1066e0dde5b9b6516f691bb478e81c6b20865281104dcb640e29dc116daae2e884f1fe6730d639dbe0e19a532be4fb337bf52ae8408446deb393d224eee7cfa50', '0x84291a42f991bfb36358eedead3699d9176a38f6f63757742fdbb7f631f2c70178b1aedef4912fed7b6cf27e88ddc7eb0e2a6aa4b999f3eb4b662b93f386c8d78e9ac9929e21f4c5e63b12991fcde93aa64a735b75b535e730ff8dd2abb16e04', '0xa1b7fcacae181495d91765dfddf26581e8e39421579c9cbd0dd27a40ea4c54af3444a36bf85a11dda2114246eaddbdd619397424bb1eb41b5a15004b902a590ede5742cd850cf312555be24d2df8becf48f5afba5a8cd087cb7be0a521728386', '0x92feaaf540dbd84719a4889a87cdd125b7e995a6782911931fef26da9afcfbe6f86aaf5328fe1f77631491ce6239c5470f44c7791506c6ef1626803a5794e76d2be0af92f7052c29ac6264b7b9b51f267ad820afc6f881460521428496c6a5f1', '0xa525c925bfae1b89320a5054acc1fa11820f73d0cf28d273092b305467b2831fab53b6daf75fb926f332782d50e2522a19edcd85be5eb72f1497193c952d8cd0bcc5d43b39363b206eae4cb1e61668bde28a3fb2fc1e0d3d113f6dfadb799717', '0x98752bb6f5a44213f40eda6aa4ff124057c1b13b6529ab42fe575b9afa66e59b9c0ed563fb20dff62130c436c3e905ee17dd8433ba02c445b1d67182ab6504a90bbe12c26a754bbf734665c622f76c62fe2e11dd43ce04fd2b91a8463679058b', '0xa9aa9a84729f7c44219ff9e00e651e50ddea3735ef2a73fdf8ed8cd271961d8ed7af5cd724b713a89a097a3fe65a3c0202f69458a8b4c157c62a85668b12fc0d3957774bc9b35f86c184dd03bfefd5c325da717d74192cc9751c2073fe9d170e', '0xb221c1fd335a4362eff504cd95145f122bf93ea02ae162a3fb39c75583fc13a932d26050e164da97cff3e91f9a7f6ff80302c19dd1916f24acf6b93b62f36e9665a8785413b0c7d930c7f1668549910f849bca319b00e59dd01e5dec8d2edacc', '0xa71e2b1e0b16d754b848f05eda90f67bedab37709550171551050c94efba0bfc282f72aeaaa1f0330041461f5e6aa4d11537237e955e1609a469d38ed17f5c2a35a1752f546db89bfeff9eab78ec944266f1cb94c1db3334ab48df716ce408ef', '0xb990ae72768779ba0b2e66df4dd29b3dbd00f901c23b2b4a53419226ef9232acedeb498b0d0687c463e3f1eead58b20b09efcefa566fbfdfe1c6e48d32367936142d0a734143e5e63cdf86be7457723535b787a9cfcfa32fe1d61ad5a2617220', '0x8d27e7fbff77d5b9b9bbc864d5231fecf817238a6433db668d5a62a2c1ee1e5694fdd90c3293c06cc0cb15f7cbeab44d0d42be632cb9ff41fc3f6628b4b62897797d7b56126d65b694dcf3e298e3561ac8813fbd7296593ced33850426df42db', '0xa92039a08b5502d5b211a7744099c9f93fa8c90cedcb1d05e92f01886219dd464eb5fb0337496ad96ed09c987da4e5f019035c5b01cc09b2a18b8a8dd419bc5895388a07e26958f6bd26751929c25f89b8eb4a299d822e2d26fec9ef350e0d3c', '0x92dcc5a1c8c3e1b28b1524e3dd6dbecd63017c9201da9dbe077f1b82adc08c50169f56fc7b5a3b28ec6b89254de3e2fd12838a761053437883c3e01ba616670cea843754548ef84bcc397de2369adcca2ab54cd73c55dc68d87aec3fc2fe4f10'] + + +- name: MAX_CONCURRENT_REQUESTS#phase0 + sources: + - file: packages/params/src/index.ts + search: export const MAX_CONCURRENT_REQUESTS = + spec: | + + MAX_CONCURRENT_REQUESTS = 2 + + +- name: MAX_REQUEST_LIGHT_CLIENT_UPDATES#altair + sources: + - file: packages/params/src/index.ts + search: export const MAX_REQUEST_LIGHT_CLIENT_UPDATES = + spec: | + + MAX_REQUEST_LIGHT_CLIENT_UPDATES = 2**7 + + +- name: NODE_ID_BITS#phase0 + sources: + - file: packages/params/src/index.ts + search: export const NODE_ID_BITS = + spec: | + + NODE_ID_BITS = 256 + + +- name: NUM_BLOCK_TIMELINESS_DEADLINES#gloas + sources: [] + spec: | + + NUM_BLOCK_TIMELINESS_DEADLINES = 2 + + +- name: PARTICIPATION_FLAG_WEIGHTS#altair + sources: + - file: packages/params/src/index.ts + search: export const PARTICIPATION_FLAG_WEIGHTS = + spec: | + + PARTICIPATION_FLAG_WEIGHTS = [TIMELY_SOURCE_WEIGHT, TIMELY_TARGET_WEIGHT, TIMELY_HEAD_WEIGHT] + + +- name: PAYLOAD_STATUS_EMPTY#gloas + sources: [] + spec: | + + PAYLOAD_STATUS_EMPTY: PayloadStatus = 1 + + +- name: PAYLOAD_STATUS_FULL#gloas + sources: [] + spec: | + + PAYLOAD_STATUS_FULL: PayloadStatus = 2 + + +- name: PAYLOAD_STATUS_PENDING#gloas + sources: [] + spec: | + + PAYLOAD_STATUS_PENDING: PayloadStatus = 0 + + +- name: PRIMITIVE_ROOT_OF_UNITY#deneb + sources: [] + spec: | + + PRIMITIVE_ROOT_OF_UNITY = 7 + + +- name: PROPOSER_WEIGHT#altair + sources: + - file: packages/params/src/index.ts + search: export const PROPOSER_WEIGHT = + spec: | + + PROPOSER_WEIGHT: uint64 = 8 + + +- name: PTC_TIMELINESS_INDEX#gloas + sources: [] + spec: | + + PTC_TIMELINESS_INDEX = 1 + + +- name: RANDOM_CHALLENGE_KZG_BATCH_DOMAIN#deneb + sources: [] + spec: | + + RANDOM_CHALLENGE_KZG_BATCH_DOMAIN = b'RCKZGBATCH___V1_' + + +- name: RANDOM_CHALLENGE_KZG_CELL_BATCH_DOMAIN#fulu + sources: [] + spec: | + + RANDOM_CHALLENGE_KZG_CELL_BATCH_DOMAIN = b'RCKZGCBATCH__V1_' + + +- name: SAFETY_DECAY#phase0 + sources: [] + spec: | + + SAFETY_DECAY: uint64 = 10 + + +- name: SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY#bellatrix + sources: + - file: packages/params/src/index.ts + search: export const SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY = + spec: | + + SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY = 128 + + +- name: SYNC_COMMITTEE_SUBNET_COUNT#altair + sources: + - file: packages/params/src/index.ts + search: export const SYNC_COMMITTEE_SUBNET_COUNT = + spec: | + + SYNC_COMMITTEE_SUBNET_COUNT = 4 + + +- name: SYNC_REWARD_WEIGHT#altair + sources: + - file: packages/params/src/index.ts + search: export const SYNC_REWARD_WEIGHT = + spec: | + + SYNC_REWARD_WEIGHT: uint64 = 2 + + +- name: TARGET_AGGREGATORS_PER_COMMITTEE#phase0 + sources: + - file: packages/params/src/index.ts + search: export const TARGET_AGGREGATORS_PER_COMMITTEE = + spec: | + + TARGET_AGGREGATORS_PER_COMMITTEE = 2**4 + + +- name: TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE#altair + sources: + - file: packages/params/src/index.ts + search: export const TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE = + spec: | + + TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE = 2**4 + + +- name: TIMELY_HEAD_FLAG_INDEX#altair + sources: + - file: packages/params/src/index.ts + search: export const TIMELY_HEAD_FLAG_INDEX = + spec: | + + TIMELY_HEAD_FLAG_INDEX = 2 + + +- name: TIMELY_HEAD_WEIGHT#altair + sources: + - file: packages/params/src/index.ts + search: export const TIMELY_HEAD_WEIGHT = + spec: | + + TIMELY_HEAD_WEIGHT: uint64 = 14 + + +- name: TIMELY_SOURCE_FLAG_INDEX#altair + sources: + - file: packages/params/src/index.ts + search: export const TIMELY_SOURCE_FLAG_INDEX = + spec: | + + TIMELY_SOURCE_FLAG_INDEX = 0 + + +- name: TIMELY_SOURCE_WEIGHT#altair + sources: + - file: packages/params/src/index.ts + search: export const TIMELY_SOURCE_WEIGHT = + spec: | + + TIMELY_SOURCE_WEIGHT: uint64 = 14 + + +- name: TIMELY_TARGET_FLAG_INDEX#altair + sources: + - file: packages/params/src/index.ts + search: export const TIMELY_TARGET_FLAG_INDEX = + spec: | + + TIMELY_TARGET_FLAG_INDEX = 1 + + +- name: TIMELY_TARGET_WEIGHT#altair + sources: + - file: packages/params/src/index.ts + search: export const TIMELY_TARGET_WEIGHT = + spec: | + + TIMELY_TARGET_WEIGHT: uint64 = 26 + + +- name: UINT256_MAX#fulu + sources: [] + spec: | + + UINT256_MAX: uint256 = 2**256 - 1 + + +- name: UINT64_MAX#phase0 + sources: [] + spec: | + + UINT64_MAX: uint64 = 2**64 - 1 + + +- name: UINT64_MAX_SQRT#phase0 + sources: [] + spec: | + + UINT64_MAX_SQRT: uint64 = 4294967295 + + +- name: UNSET_DEPOSIT_REQUESTS_START_INDEX#electra + sources: + - file: packages/params/src/index.ts + search: export const UNSET_DEPOSIT_REQUESTS_START_INDEX = + spec: | + + UNSET_DEPOSIT_REQUESTS_START_INDEX: uint64 = 2**64 - 1 + + +- name: VERSIONED_HASH_VERSION_KZG#deneb + sources: + - file: packages/params/src/index.ts + search: export const VERSIONED_HASH_VERSION_KZG = + spec: | + + VERSIONED_HASH_VERSION_KZG: Bytes1 = '0x01' + + +- name: WEIGHT_DENOMINATOR#altair + sources: + - file: packages/params/src/index.ts + search: export const WEIGHT_DENOMINATOR = + spec: | + + WEIGHT_DENOMINATOR: uint64 = 64 + + +- name: WITHDRAWAL_REQUEST_TYPE#electra + sources: + - file: packages/params/src/index.ts + search: export const WITHDRAWAL_REQUEST_TYPE = + spec: | + + WITHDRAWAL_REQUEST_TYPE: Bytes1 = '0x01' + diff --git a/specrefs/containers.yml b/specrefs/containers.yml new file mode 100644 index 000000000000..ff046173e885 --- /dev/null +++ b/specrefs/containers.yml @@ -0,0 +1,1682 @@ +- name: AggregateAndProof#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const AggregateAndProof = + spec: | + + class AggregateAndProof(Container): + aggregator_index: ValidatorIndex + aggregate: Attestation + selection_proof: BLSSignature + + +- name: AggregateAndProof#electra + sources: + - file: packages/types/src/electra/sszTypes.ts + search: export const AggregateAndProof = + spec: | + + class AggregateAndProof(Container): + aggregator_index: ValidatorIndex + # [Modified in Electra:EIP7549] + aggregate: Attestation + selection_proof: BLSSignature + + +- name: Attestation#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const Attestation = + spec: | + + class Attestation(Container): + aggregation_bits: Bitlist[MAX_VALIDATORS_PER_COMMITTEE] + data: AttestationData + signature: BLSSignature + + +- name: Attestation#electra + sources: + - file: packages/types/src/electra/sszTypes.ts + search: export const Attestation = + spec: | + + class Attestation(Container): + # [Modified in Electra:EIP7549] + aggregation_bits: Bitlist[MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT] + data: AttestationData + signature: BLSSignature + # [New in Electra:EIP7549] + committee_bits: Bitvector[MAX_COMMITTEES_PER_SLOT] + + +- name: AttestationData#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const AttestationData = + spec: | + + class AttestationData(Container): + slot: Slot + index: CommitteeIndex + beacon_block_root: Root + source: Checkpoint + target: Checkpoint + + +- name: AttesterSlashing#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const AttesterSlashing = + spec: | + + class AttesterSlashing(Container): + attestation_1: IndexedAttestation + attestation_2: IndexedAttestation + + +- name: AttesterSlashing#electra + sources: + - file: packages/types/src/electra/sszTypes.ts + search: export const AttesterSlashing = + spec: | + + class AttesterSlashing(Container): + # [Modified in Electra:EIP7549] + attestation_1: IndexedAttestation + # [Modified in Electra:EIP7549] + attestation_2: IndexedAttestation + + +- name: BLSToExecutionChange#capella + sources: + - file: packages/types/src/capella/sszTypes.ts + search: export const BLSToExecutionChange = + spec: | + + class BLSToExecutionChange(Container): + validator_index: ValidatorIndex + from_bls_pubkey: BLSPubkey + to_execution_address: ExecutionAddress + + +- name: BeaconBlock#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const BeaconBlock = + spec: | + + class BeaconBlock(Container): + slot: Slot + proposer_index: ValidatorIndex + parent_root: Root + state_root: Root + body: BeaconBlockBody + + +- name: BeaconBlockBody#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const BeaconBlockBody = + spec: | + + class BeaconBlockBody(Container): + randao_reveal: BLSSignature + eth1_data: Eth1Data + graffiti: Bytes32 + proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS] + attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS] + attestations: List[Attestation, MAX_ATTESTATIONS] + deposits: List[Deposit, MAX_DEPOSITS] + voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS] + + +- name: BeaconBlockBody#altair + sources: + - file: packages/types/src/altair/sszTypes.ts + search: export const BeaconBlockBody = + spec: | + + class BeaconBlockBody(Container): + randao_reveal: BLSSignature + eth1_data: Eth1Data + graffiti: Bytes32 + proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS] + attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS] + attestations: List[Attestation, MAX_ATTESTATIONS] + deposits: List[Deposit, MAX_DEPOSITS] + voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS] + # [New in Altair] + sync_aggregate: SyncAggregate + + +- name: BeaconBlockBody#bellatrix + sources: + - file: packages/types/src/bellatrix/sszTypes.ts + search: export const BeaconBlockBody = + spec: | + + class BeaconBlockBody(Container): + randao_reveal: BLSSignature + eth1_data: Eth1Data + graffiti: Bytes32 + proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS] + attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS] + attestations: List[Attestation, MAX_ATTESTATIONS] + deposits: List[Deposit, MAX_DEPOSITS] + voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS] + sync_aggregate: SyncAggregate + # [New in Bellatrix] + execution_payload: ExecutionPayload + + +- name: BeaconBlockBody#capella + sources: + - file: packages/types/src/capella/sszTypes.ts + search: export const BeaconBlockBody = + spec: | + + class BeaconBlockBody(Container): + randao_reveal: BLSSignature + eth1_data: Eth1Data + graffiti: Bytes32 + proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS] + attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS] + attestations: List[Attestation, MAX_ATTESTATIONS] + deposits: List[Deposit, MAX_DEPOSITS] + voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS] + sync_aggregate: SyncAggregate + execution_payload: ExecutionPayload + # [New in Capella] + bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES] + + +- name: BeaconBlockBody#deneb + sources: + - file: packages/types/src/deneb/sszTypes.ts + search: export const BeaconBlockBody = + spec: | + + class BeaconBlockBody(Container): + randao_reveal: BLSSignature + eth1_data: Eth1Data + graffiti: Bytes32 + proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS] + attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS] + attestations: List[Attestation, MAX_ATTESTATIONS] + deposits: List[Deposit, MAX_DEPOSITS] + voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS] + sync_aggregate: SyncAggregate + # [Modified in Deneb:EIP4844] + execution_payload: ExecutionPayload + bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES] + # [New in Deneb:EIP4844] + blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] + + +- name: BeaconBlockBody#electra + sources: + - file: packages/types/src/electra/sszTypes.ts + search: export const BeaconBlockBody = + spec: | + + class BeaconBlockBody(Container): + randao_reveal: BLSSignature + eth1_data: Eth1Data + graffiti: Bytes32 + proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS] + # [Modified in Electra:EIP7549] + attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS_ELECTRA] + # [Modified in Electra:EIP7549] + attestations: List[Attestation, MAX_ATTESTATIONS_ELECTRA] + deposits: List[Deposit, MAX_DEPOSITS] + voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS] + sync_aggregate: SyncAggregate + execution_payload: ExecutionPayload + bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES] + blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] + # [New in Electra] + execution_requests: ExecutionRequests + + +- name: BeaconBlockBody#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const BeaconBlockBody = + spec: | + + class BeaconBlockBody(Container): + randao_reveal: BLSSignature + eth1_data: Eth1Data + graffiti: Bytes32 + proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS] + attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS_ELECTRA] + attestations: List[Attestation, MAX_ATTESTATIONS_ELECTRA] + deposits: List[Deposit, MAX_DEPOSITS] + voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS] + sync_aggregate: SyncAggregate + # [Modified in Gloas:EIP7732] + # Removed `execution_payload` + bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES] + # [Modified in Gloas:EIP7732] + # Removed `blob_kzg_commitments` + # [Modified in Gloas:EIP7732] + # Removed `execution_requests` + # [New in Gloas:EIP7732] + signed_execution_payload_bid: SignedExecutionPayloadBid + # [New in Gloas:EIP7732] + payload_attestations: List[PayloadAttestation, MAX_PAYLOAD_ATTESTATIONS] + + +- name: BeaconBlockHeader#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const BeaconBlockHeader = + spec: | + + class BeaconBlockHeader(Container): + slot: Slot + proposer_index: ValidatorIndex + parent_root: Root + state_root: Root + body_root: Root + + +- name: BeaconState#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const BeaconState = + spec: | + + class BeaconState(Container): + genesis_time: uint64 + genesis_validators_root: Root + slot: Slot + fork: Fork + latest_block_header: BeaconBlockHeader + block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] + state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] + historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT] + eth1_data: Eth1Data + eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH] + eth1_deposit_index: uint64 + validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] + balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT] + randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR] + slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR] + previous_epoch_attestations: List[PendingAttestation, MAX_ATTESTATIONS * SLOTS_PER_EPOCH] + current_epoch_attestations: List[PendingAttestation, MAX_ATTESTATIONS * SLOTS_PER_EPOCH] + justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH] + previous_justified_checkpoint: Checkpoint + current_justified_checkpoint: Checkpoint + finalized_checkpoint: Checkpoint + + +- name: BeaconState#altair + sources: + - file: packages/types/src/altair/sszTypes.ts + search: export const BeaconState = + spec: | + + class BeaconState(Container): + genesis_time: uint64 + genesis_validators_root: Root + slot: Slot + fork: Fork + latest_block_header: BeaconBlockHeader + block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] + state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] + historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT] + eth1_data: Eth1Data + eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH] + eth1_deposit_index: uint64 + validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] + balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT] + randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR] + slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR] + # [Modified in Altair] + previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] + # [Modified in Altair] + current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] + justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH] + previous_justified_checkpoint: Checkpoint + current_justified_checkpoint: Checkpoint + finalized_checkpoint: Checkpoint + # [New in Altair] + inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT] + # [New in Altair] + current_sync_committee: SyncCommittee + # [New in Altair] + next_sync_committee: SyncCommittee + + +- name: BeaconState#bellatrix + sources: + - file: packages/types/src/bellatrix/sszTypes.ts + search: export const BeaconState = + spec: | + + class BeaconState(Container): + genesis_time: uint64 + genesis_validators_root: Root + slot: Slot + fork: Fork + latest_block_header: BeaconBlockHeader + block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] + state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] + historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT] + eth1_data: Eth1Data + eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH] + eth1_deposit_index: uint64 + validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] + balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT] + randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR] + slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR] + previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] + current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] + justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH] + previous_justified_checkpoint: Checkpoint + current_justified_checkpoint: Checkpoint + finalized_checkpoint: Checkpoint + inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT] + current_sync_committee: SyncCommittee + next_sync_committee: SyncCommittee + # [New in Bellatrix] + latest_execution_payload_header: ExecutionPayloadHeader + + +- name: BeaconState#capella + sources: + - file: packages/types/src/capella/sszTypes.ts + search: export const BeaconState = + spec: | + + class BeaconState(Container): + genesis_time: uint64 + genesis_validators_root: Root + slot: Slot + fork: Fork + latest_block_header: BeaconBlockHeader + block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] + state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] + historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT] + eth1_data: Eth1Data + eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH] + eth1_deposit_index: uint64 + validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] + balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT] + randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR] + slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR] + previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] + current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] + justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH] + previous_justified_checkpoint: Checkpoint + current_justified_checkpoint: Checkpoint + finalized_checkpoint: Checkpoint + inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT] + current_sync_committee: SyncCommittee + next_sync_committee: SyncCommittee + # [Modified in Capella] + latest_execution_payload_header: ExecutionPayloadHeader + # [New in Capella] + next_withdrawal_index: WithdrawalIndex + # [New in Capella] + next_withdrawal_validator_index: ValidatorIndex + # [New in Capella] + historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT] + + +- name: BeaconState#deneb + sources: + - file: packages/types/src/deneb/sszTypes.ts + search: export const BeaconState = + spec: | + + class BeaconState(Container): + genesis_time: uint64 + genesis_validators_root: Root + slot: Slot + fork: Fork + latest_block_header: BeaconBlockHeader + block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] + state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] + historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT] + eth1_data: Eth1Data + eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH] + eth1_deposit_index: uint64 + validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] + balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT] + randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR] + slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR] + previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] + current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] + justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH] + previous_justified_checkpoint: Checkpoint + current_justified_checkpoint: Checkpoint + finalized_checkpoint: Checkpoint + inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT] + current_sync_committee: SyncCommittee + next_sync_committee: SyncCommittee + # [Modified in Deneb:EIP4844] + latest_execution_payload_header: ExecutionPayloadHeader + next_withdrawal_index: WithdrawalIndex + next_withdrawal_validator_index: ValidatorIndex + historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT] + + +- name: BeaconState#electra + sources: + - file: packages/types/src/electra/sszTypes.ts + search: export const BeaconState = + spec: | + + class BeaconState(Container): + genesis_time: uint64 + genesis_validators_root: Root + slot: Slot + fork: Fork + latest_block_header: BeaconBlockHeader + block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] + state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] + historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT] + eth1_data: Eth1Data + eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH] + eth1_deposit_index: uint64 + validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] + balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT] + randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR] + slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR] + previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] + current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] + justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH] + previous_justified_checkpoint: Checkpoint + current_justified_checkpoint: Checkpoint + finalized_checkpoint: Checkpoint + inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT] + current_sync_committee: SyncCommittee + next_sync_committee: SyncCommittee + latest_execution_payload_header: ExecutionPayloadHeader + next_withdrawal_index: WithdrawalIndex + next_withdrawal_validator_index: ValidatorIndex + historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT] + # [New in Electra:EIP6110] + deposit_requests_start_index: uint64 + # [New in Electra:EIP7251] + deposit_balance_to_consume: Gwei + # [New in Electra:EIP7251] + exit_balance_to_consume: Gwei + # [New in Electra:EIP7251] + earliest_exit_epoch: Epoch + # [New in Electra:EIP7251] + consolidation_balance_to_consume: Gwei + # [New in Electra:EIP7251] + earliest_consolidation_epoch: Epoch + # [New in Electra:EIP7251] + pending_deposits: List[PendingDeposit, PENDING_DEPOSITS_LIMIT] + # [New in Electra:EIP7251] + pending_partial_withdrawals: List[PendingPartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT] + # [New in Electra:EIP7251] + pending_consolidations: List[PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT] + + +- name: BeaconState#fulu + sources: + - file: packages/types/src/fulu/sszTypes.ts + search: export const BeaconState = + spec: | + + class BeaconState(Container): + genesis_time: uint64 + genesis_validators_root: Root + slot: Slot + fork: Fork + latest_block_header: BeaconBlockHeader + block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] + state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] + historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT] + eth1_data: Eth1Data + eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH] + eth1_deposit_index: uint64 + validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] + balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT] + randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR] + slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR] + previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] + current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] + justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH] + previous_justified_checkpoint: Checkpoint + current_justified_checkpoint: Checkpoint + finalized_checkpoint: Checkpoint + inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT] + current_sync_committee: SyncCommittee + next_sync_committee: SyncCommittee + latest_execution_payload_header: ExecutionPayloadHeader + next_withdrawal_index: WithdrawalIndex + next_withdrawal_validator_index: ValidatorIndex + historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT] + deposit_requests_start_index: uint64 + deposit_balance_to_consume: Gwei + exit_balance_to_consume: Gwei + earliest_exit_epoch: Epoch + consolidation_balance_to_consume: Gwei + earliest_consolidation_epoch: Epoch + pending_deposits: List[PendingDeposit, PENDING_DEPOSITS_LIMIT] + pending_partial_withdrawals: List[PendingPartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT] + pending_consolidations: List[PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT] + # [New in Fulu:EIP7917] + proposer_lookahead: Vector[ValidatorIndex, (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH] + + +- name: BeaconState#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const BeaconState = + spec: | + + class BeaconState(Container): + genesis_time: uint64 + genesis_validators_root: Root + slot: Slot + fork: Fork + latest_block_header: BeaconBlockHeader + block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] + state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] + historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT] + eth1_data: Eth1Data + eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH] + eth1_deposit_index: uint64 + validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] + balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT] + randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR] + slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR] + previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] + current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] + justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH] + previous_justified_checkpoint: Checkpoint + current_justified_checkpoint: Checkpoint + finalized_checkpoint: Checkpoint + inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT] + current_sync_committee: SyncCommittee + next_sync_committee: SyncCommittee + # [Modified in Gloas:EIP7732] + # Removed `latest_execution_payload_header` + # [New in Gloas:EIP7732] + latest_execution_payload_bid: ExecutionPayloadBid + next_withdrawal_index: WithdrawalIndex + next_withdrawal_validator_index: ValidatorIndex + historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT] + deposit_requests_start_index: uint64 + deposit_balance_to_consume: Gwei + exit_balance_to_consume: Gwei + earliest_exit_epoch: Epoch + consolidation_balance_to_consume: Gwei + earliest_consolidation_epoch: Epoch + pending_deposits: List[PendingDeposit, PENDING_DEPOSITS_LIMIT] + pending_partial_withdrawals: List[PendingPartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT] + pending_consolidations: List[PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT] + proposer_lookahead: Vector[ValidatorIndex, (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH] + # [New in Gloas:EIP7732] + builders: List[Builder, BUILDER_REGISTRY_LIMIT] + # [New in Gloas:EIP7732] + next_withdrawal_builder_index: BuilderIndex + # [New in Gloas:EIP7732] + execution_payload_availability: Bitvector[SLOTS_PER_HISTORICAL_ROOT] + # [New in Gloas:EIP7732] + builder_pending_payments: Vector[BuilderPendingPayment, 2 * SLOTS_PER_EPOCH] + # [New in Gloas:EIP7732] + builder_pending_withdrawals: List[BuilderPendingWithdrawal, BUILDER_PENDING_WITHDRAWALS_LIMIT] + # [New in Gloas:EIP7732] + latest_block_hash: Hash32 + # [New in Gloas:EIP7732] + payload_expected_withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + + +- name: BlobIdentifier#deneb + sources: + - file: packages/types/src/deneb/sszTypes.ts + search: export const BlobIdentifier = + spec: | + + class BlobIdentifier(Container): + block_root: Root + index: BlobIndex + + +- name: BlobSidecar#deneb + sources: + - file: packages/types/src/deneb/sszTypes.ts + search: export const BlobSidecar = + spec: | + + class BlobSidecar(Container): + index: BlobIndex + blob: Blob + kzg_commitment: KZGCommitment + kzg_proof: KZGProof + signed_block_header: SignedBeaconBlockHeader + kzg_commitment_inclusion_proof: Vector[Bytes32, KZG_COMMITMENT_INCLUSION_PROOF_DEPTH] + + +- name: Builder#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const Builder = + spec: | + + class Builder(Container): + pubkey: BLSPubkey + version: uint8 + execution_address: ExecutionAddress + balance: Gwei + deposit_epoch: Epoch + withdrawable_epoch: Epoch + + +- name: BuilderPendingPayment#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const BuilderPendingPayment = + spec: | + + class BuilderPendingPayment(Container): + weight: Gwei + withdrawal: BuilderPendingWithdrawal + + +- name: BuilderPendingWithdrawal#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const BuilderPendingWithdrawal = + spec: | + + class BuilderPendingWithdrawal(Container): + fee_recipient: ExecutionAddress + amount: Gwei + builder_index: BuilderIndex + + +- name: Checkpoint#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const Checkpoint = + spec: | + + class Checkpoint(Container): + epoch: Epoch + root: Root + + +- name: ConsolidationRequest#electra + sources: + - file: packages/types/src/electra/sszTypes.ts + search: export const ConsolidationRequest = + spec: | + + class ConsolidationRequest(Container): + source_address: ExecutionAddress + source_pubkey: BLSPubkey + target_pubkey: BLSPubkey + + +- name: ContributionAndProof#altair + sources: + - file: packages/types/src/altair/sszTypes.ts + search: export const ContributionAndProof = + spec: | + + class ContributionAndProof(Container): + aggregator_index: ValidatorIndex + contribution: SyncCommitteeContribution + selection_proof: BLSSignature + + +- name: DataColumnSidecar#fulu + sources: + - file: packages/types/src/fulu/sszTypes.ts + search: export const DataColumnSidecar = + spec: | + + class DataColumnSidecar(Container): + index: ColumnIndex + column: List[Cell, MAX_BLOB_COMMITMENTS_PER_BLOCK] + kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] + kzg_proofs: List[KZGProof, MAX_BLOB_COMMITMENTS_PER_BLOCK] + signed_block_header: SignedBeaconBlockHeader + kzg_commitments_inclusion_proof: Vector[Bytes32, KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH] + + +- name: DataColumnSidecar#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const DataColumnSidecar = + spec: | + + class DataColumnSidecar(Container): + index: ColumnIndex + column: List[Cell, MAX_BLOB_COMMITMENTS_PER_BLOCK] + kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] + kzg_proofs: List[KZGProof, MAX_BLOB_COMMITMENTS_PER_BLOCK] + # [Modified in Gloas:EIP7732] + # Removed `signed_block_header` + # [Modified in Gloas:EIP7732] + # Removed `kzg_commitments_inclusion_proof` + # [New in Gloas:EIP7732] + slot: Slot + # [New in Gloas:EIP7732] + beacon_block_root: Root + + +- name: DataColumnsByRootIdentifier#fulu + sources: + - file: packages/types/src/fulu/sszTypes.ts + search: export const DataColumnsByRootIdentifier = + spec: | + + class DataColumnsByRootIdentifier(Container): + block_root: Root + columns: List[ColumnIndex, NUMBER_OF_COLUMNS] + + +- name: Deposit#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const Deposit = + spec: | + + class Deposit(Container): + proof: Vector[Bytes32, DEPOSIT_CONTRACT_TREE_DEPTH + 1] + data: DepositData + + +- name: DepositData#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const DepositData = + spec: | + + class DepositData(Container): + pubkey: BLSPubkey + withdrawal_credentials: Bytes32 + amount: Gwei + signature: BLSSignature + + +- name: DepositMessage#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const DepositMessage = + spec: | + + class DepositMessage(Container): + pubkey: BLSPubkey + withdrawal_credentials: Bytes32 + amount: Gwei + + +- name: DepositRequest#electra + sources: + - file: packages/types/src/electra/sszTypes.ts + search: export const DepositRequest = + spec: | + + class DepositRequest(Container): + pubkey: BLSPubkey + withdrawal_credentials: Bytes32 + amount: Gwei + signature: BLSSignature + index: uint64 + + +- name: Eth1Block#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const Eth1Block = + spec: | + + class Eth1Block(Container): + timestamp: uint64 + deposit_root: Root + deposit_count: uint64 + + +- name: Eth1Data#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const Eth1Data = + spec: | + + class Eth1Data(Container): + deposit_root: Root + deposit_count: uint64 + block_hash: Hash32 + + +- name: ExecutionPayload#bellatrix + sources: + - file: packages/types/src/bellatrix/sszTypes.ts + search: export const ExecutionPayload = + spec: | + + class ExecutionPayload(Container): + parent_hash: Hash32 + fee_recipient: ExecutionAddress + state_root: Bytes32 + receipts_root: Bytes32 + logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] + prev_randao: Bytes32 + block_number: uint64 + gas_limit: uint64 + gas_used: uint64 + timestamp: uint64 + extra_data: ByteList[MAX_EXTRA_DATA_BYTES] + base_fee_per_gas: uint256 + block_hash: Hash32 + transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD] + + +- name: ExecutionPayload#capella + sources: + - file: packages/types/src/capella/sszTypes.ts + search: export const ExecutionPayload = + spec: | + + class ExecutionPayload(Container): + parent_hash: Hash32 + fee_recipient: ExecutionAddress + state_root: Bytes32 + receipts_root: Bytes32 + logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] + prev_randao: Bytes32 + block_number: uint64 + gas_limit: uint64 + gas_used: uint64 + timestamp: uint64 + extra_data: ByteList[MAX_EXTRA_DATA_BYTES] + base_fee_per_gas: uint256 + block_hash: Hash32 + transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD] + # [New in Capella] + withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + + +- name: ExecutionPayload#deneb + sources: + - file: packages/types/src/deneb/sszTypes.ts + search: export const ExecutionPayload = + spec: | + + class ExecutionPayload(Container): + parent_hash: Hash32 + fee_recipient: ExecutionAddress + state_root: Bytes32 + receipts_root: Bytes32 + logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] + prev_randao: Bytes32 + block_number: uint64 + gas_limit: uint64 + gas_used: uint64 + timestamp: uint64 + extra_data: ByteList[MAX_EXTRA_DATA_BYTES] + base_fee_per_gas: uint256 + block_hash: Hash32 + transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD] + withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + # [New in Deneb:EIP4844] + blob_gas_used: uint64 + # [New in Deneb:EIP4844] + excess_blob_gas: uint64 + + +- name: ExecutionPayloadBid#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const ExecutionPayloadBid = + spec: | + + class ExecutionPayloadBid(Container): + parent_block_hash: Hash32 + parent_block_root: Root + block_hash: Hash32 + prev_randao: Bytes32 + fee_recipient: ExecutionAddress + gas_limit: uint64 + builder_index: BuilderIndex + slot: Slot + value: Gwei + execution_payment: Gwei + blob_kzg_commitments_root: Root + + +- name: ExecutionPayloadEnvelope#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const ExecutionPayloadEnvelope = + spec: | + + class ExecutionPayloadEnvelope(Container): + payload: ExecutionPayload + execution_requests: ExecutionRequests + builder_index: BuilderIndex + beacon_block_root: Root + slot: Slot + blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] + state_root: Root + + +- name: ExecutionPayloadHeader#bellatrix + sources: + - file: packages/types/src/bellatrix/sszTypes.ts + search: export const ExecutionPayloadHeader = + spec: | + + class ExecutionPayloadHeader(Container): + parent_hash: Hash32 + fee_recipient: ExecutionAddress + state_root: Bytes32 + receipts_root: Bytes32 + logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] + prev_randao: Bytes32 + block_number: uint64 + gas_limit: uint64 + gas_used: uint64 + timestamp: uint64 + extra_data: ByteList[MAX_EXTRA_DATA_BYTES] + base_fee_per_gas: uint256 + block_hash: Hash32 + transactions_root: Root + + +- name: ExecutionPayloadHeader#capella + sources: + - file: packages/types/src/capella/sszTypes.ts + search: export const ExecutionPayloadHeader = + spec: | + + class ExecutionPayloadHeader(Container): + parent_hash: Hash32 + fee_recipient: ExecutionAddress + state_root: Bytes32 + receipts_root: Bytes32 + logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] + prev_randao: Bytes32 + block_number: uint64 + gas_limit: uint64 + gas_used: uint64 + timestamp: uint64 + extra_data: ByteList[MAX_EXTRA_DATA_BYTES] + base_fee_per_gas: uint256 + block_hash: Hash32 + transactions_root: Root + # [New in Capella] + withdrawals_root: Root + + +- name: ExecutionPayloadHeader#deneb + sources: + - file: packages/types/src/deneb/sszTypes.ts + search: export const ExecutionPayloadHeader = + spec: | + + class ExecutionPayloadHeader(Container): + parent_hash: Hash32 + fee_recipient: ExecutionAddress + state_root: Bytes32 + receipts_root: Bytes32 + logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] + prev_randao: Bytes32 + block_number: uint64 + gas_limit: uint64 + gas_used: uint64 + timestamp: uint64 + extra_data: ByteList[MAX_EXTRA_DATA_BYTES] + base_fee_per_gas: uint256 + block_hash: Hash32 + transactions_root: Root + withdrawals_root: Root + # [New in Deneb:EIP4844] + blob_gas_used: uint64 + # [New in Deneb:EIP4844] + excess_blob_gas: uint64 + + +- name: ExecutionRequests#electra + sources: + - file: packages/types/src/electra/sszTypes.ts + search: export const ExecutionRequests = + spec: | + + class ExecutionRequests(Container): + # [New in Electra:EIP6110] + deposits: List[DepositRequest, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD] + # [New in Electra:EIP7002:EIP7251] + withdrawals: List[WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD] + # [New in Electra:EIP7251] + consolidations: List[ConsolidationRequest, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD] + + +- name: Fork#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const Fork = + spec: | + + class Fork(Container): + previous_version: Version + current_version: Version + epoch: Epoch + + +- name: ForkChoiceNode#gloas + sources: [] + spec: | + + class ForkChoiceNode(Container): + root: Root + payload_status: PayloadStatus # One of PAYLOAD_STATUS_* values + + +- name: ForkData#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const ForkData = + spec: | + + class ForkData(Container): + current_version: Version + genesis_validators_root: Root + + +- name: HistoricalBatch#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const HistoricalBatch = + spec: | + + class HistoricalBatch(Container): + block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] + state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] + + +- name: HistoricalSummary#capella + sources: + - file: packages/types/src/capella/sszTypes.ts + search: export const HistoricalSummary = + spec: | + + class HistoricalSummary(Container): + block_summary_root: Root + state_summary_root: Root + + +- name: IndexedAttestation#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const IndexedAttestation = + spec: | + + class IndexedAttestation(Container): + attesting_indices: List[ValidatorIndex, MAX_VALIDATORS_PER_COMMITTEE] + data: AttestationData + signature: BLSSignature + + +- name: IndexedAttestation#electra + sources: + - file: packages/types/src/electra/sszTypes.ts + search: export const IndexedAttestation = + spec: | + + class IndexedAttestation(Container): + # [Modified in Electra:EIP7549] + attesting_indices: List[ValidatorIndex, MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT] + data: AttestationData + signature: BLSSignature + + +- name: IndexedPayloadAttestation#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const IndexedPayloadAttestation = + spec: | + + class IndexedPayloadAttestation(Container): + attesting_indices: List[ValidatorIndex, PTC_SIZE] + data: PayloadAttestationData + signature: BLSSignature + + +- name: LightClientBootstrap#altair + sources: + - file: packages/types/src/altair/sszTypes.ts + search: export const LightClientBootstrap = + spec: | + + class LightClientBootstrap(Container): + # Header matching the requested beacon block root + header: LightClientHeader + # Current sync committee corresponding to `header.beacon.state_root` + current_sync_committee: SyncCommittee + current_sync_committee_branch: CurrentSyncCommitteeBranch + + +- name: LightClientBootstrap#capella + sources: + - file: packages/types/src/capella/sszTypes.ts + search: export const LightClientBootstrap = + spec: | + + class LightClientBootstrap(Container): + # [Modified in Capella] + header: LightClientHeader + current_sync_committee: SyncCommittee + current_sync_committee_branch: CurrentSyncCommitteeBranch + + +- name: LightClientFinalityUpdate#altair + sources: + - file: packages/types/src/altair/sszTypes.ts + search: export const LightClientFinalityUpdate = + spec: | + + class LightClientFinalityUpdate(Container): + # Header attested to by the sync committee + attested_header: LightClientHeader + # Finalized header corresponding to `attested_header.beacon.state_root` + finalized_header: LightClientHeader + finality_branch: FinalityBranch + # Sync committee aggregate signature + sync_aggregate: SyncAggregate + # Slot at which the aggregate signature was created (untrusted) + signature_slot: Slot + + +- name: LightClientFinalityUpdate#capella + sources: + - file: packages/types/src/capella/sszTypes.ts + search: export const LightClientFinalityUpdate = + spec: | + + class LightClientFinalityUpdate(Container): + # [Modified in Capella] + attested_header: LightClientHeader + # [Modified in Capella] + finalized_header: LightClientHeader + finality_branch: FinalityBranch + sync_aggregate: SyncAggregate + signature_slot: Slot + + +- name: LightClientHeader#altair + sources: + - file: packages/types/src/altair/sszTypes.ts + search: export const LightClientHeader = + spec: | + + class LightClientHeader(Container): + beacon: BeaconBlockHeader + + +- name: LightClientHeader#capella + sources: + - file: packages/types/src/capella/sszTypes.ts + search: export const LightClientHeader = + spec: | + + class LightClientHeader(Container): + beacon: BeaconBlockHeader + # [New in Capella] + execution: ExecutionPayloadHeader + # [New in Capella] + execution_branch: ExecutionBranch + + +- name: LightClientOptimisticUpdate#altair + sources: + - file: packages/types/src/altair/sszTypes.ts + search: export const LightClientOptimisticUpdate = + spec: | + + class LightClientOptimisticUpdate(Container): + # Header attested to by the sync committee + attested_header: LightClientHeader + # Sync committee aggregate signature + sync_aggregate: SyncAggregate + # Slot at which the aggregate signature was created (untrusted) + signature_slot: Slot + + +- name: LightClientOptimisticUpdate#capella + sources: + - file: packages/types/src/capella/sszTypes.ts + search: export const LightClientOptimisticUpdate = + spec: | + + class LightClientOptimisticUpdate(Container): + # [Modified in Capella] + attested_header: LightClientHeader + sync_aggregate: SyncAggregate + signature_slot: Slot + + +- name: LightClientUpdate#altair + sources: + - file: packages/types/src/altair/sszTypes.ts + search: export const LightClientUpdate = + spec: | + + class LightClientUpdate(Container): + # Header attested to by the sync committee + attested_header: LightClientHeader + # Next sync committee corresponding to `attested_header.beacon.state_root` + next_sync_committee: SyncCommittee + next_sync_committee_branch: NextSyncCommitteeBranch + # Finalized header corresponding to `attested_header.beacon.state_root` + finalized_header: LightClientHeader + finality_branch: FinalityBranch + # Sync committee aggregate signature + sync_aggregate: SyncAggregate + # Slot at which the aggregate signature was created (untrusted) + signature_slot: Slot + + +- name: LightClientUpdate#capella + sources: + - file: packages/types/src/capella/sszTypes.ts + search: export const LightClientUpdate = + spec: | + + class LightClientUpdate(Container): + # [Modified in Capella] + attested_header: LightClientHeader + next_sync_committee: SyncCommittee + next_sync_committee_branch: NextSyncCommitteeBranch + # [Modified in Capella] + finalized_header: LightClientHeader + finality_branch: FinalityBranch + sync_aggregate: SyncAggregate + signature_slot: Slot + + +- name: MatrixEntry#fulu + sources: + - file: packages/types/src/fulu/sszTypes.ts + search: export const MatrixEntry = + spec: | + + class MatrixEntry(Container): + cell: Cell + kzg_proof: KZGProof + column_index: ColumnIndex + row_index: RowIndex + + +- name: PayloadAttestation#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const PayloadAttestation = + spec: | + + class PayloadAttestation(Container): + aggregation_bits: Bitvector[PTC_SIZE] + data: PayloadAttestationData + signature: BLSSignature + + +- name: PayloadAttestationData#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const PayloadAttestationData = + spec: | + + class PayloadAttestationData(Container): + beacon_block_root: Root + slot: Slot + payload_present: boolean + blob_data_available: boolean + + +- name: PayloadAttestationMessage#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const PayloadAttestationMessage = + spec: | + + class PayloadAttestationMessage(Container): + validator_index: ValidatorIndex + data: PayloadAttestationData + signature: BLSSignature + + +- name: PendingAttestation#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const PendingAttestation = + spec: | + + class PendingAttestation(Container): + aggregation_bits: Bitlist[MAX_VALIDATORS_PER_COMMITTEE] + data: AttestationData + inclusion_delay: Slot + proposer_index: ValidatorIndex + + +- name: PendingConsolidation#electra + sources: + - file: packages/types/src/electra/sszTypes.ts + search: export const PendingConsolidation = + spec: | + + class PendingConsolidation(Container): + source_index: ValidatorIndex + target_index: ValidatorIndex + + +- name: PendingDeposit#electra + sources: + - file: packages/types/src/electra/sszTypes.ts + search: export const PendingDeposit = + spec: | + + class PendingDeposit(Container): + pubkey: BLSPubkey + withdrawal_credentials: Bytes32 + amount: Gwei + signature: BLSSignature + slot: Slot + + +- name: PendingPartialWithdrawal#electra + sources: + - file: packages/types/src/electra/sszTypes.ts + search: export const PendingPartialWithdrawal = + spec: | + + class PendingPartialWithdrawal(Container): + validator_index: ValidatorIndex + amount: Gwei + withdrawable_epoch: Epoch + + +- name: PowBlock#bellatrix + sources: + - file: packages/types/src/bellatrix/sszTypes.ts + search: export const PowBlock = + spec: | + + class PowBlock(Container): + block_hash: Hash32 + parent_hash: Hash32 + total_difficulty: uint256 + + +- name: ProposerPreferences#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const ProposerPreferences = + spec: | + + class ProposerPreferences(Container): + proposal_slot: Slot + validator_index: ValidatorIndex + fee_recipient: ExecutionAddress + gas_limit: uint64 + + +- name: ProposerSlashing#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const ProposerSlashing = + spec: | + + class ProposerSlashing(Container): + signed_header_1: SignedBeaconBlockHeader + signed_header_2: SignedBeaconBlockHeader + + +- name: SignedAggregateAndProof#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const SignedAggregateAndProof = + spec: | + + class SignedAggregateAndProof(Container): + message: AggregateAndProof + signature: BLSSignature + + +- name: SignedAggregateAndProof#electra + sources: + - file: packages/types/src/electra/sszTypes.ts + search: export const SignedAggregateAndProof = + spec: | + + class SignedAggregateAndProof(Container): + # [Modified in Electra:EIP7549] + message: AggregateAndProof + signature: BLSSignature + + +- name: SignedBLSToExecutionChange#capella + sources: + - file: packages/types/src/capella/sszTypes.ts + search: export const SignedBLSToExecutionChange = + spec: | + + class SignedBLSToExecutionChange(Container): + message: BLSToExecutionChange + signature: BLSSignature + + +- name: SignedBeaconBlock#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const SignedBeaconBlock = + spec: | + + class SignedBeaconBlock(Container): + message: BeaconBlock + signature: BLSSignature + + +- name: SignedBeaconBlockHeader#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const SignedBeaconBlockHeader = + spec: | + + class SignedBeaconBlockHeader(Container): + message: BeaconBlockHeader + signature: BLSSignature + + +- name: SignedContributionAndProof#altair + sources: + - file: packages/types/src/altair/sszTypes.ts + search: export const SignedContributionAndProof = + spec: | + + class SignedContributionAndProof(Container): + message: ContributionAndProof + signature: BLSSignature + + +- name: SignedExecutionPayloadBid#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const SignedExecutionPayloadBid = + spec: | + + class SignedExecutionPayloadBid(Container): + message: ExecutionPayloadBid + signature: BLSSignature + + +- name: SignedExecutionPayloadEnvelope#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const SignedExecutionPayloadEnvelope = + spec: | + + class SignedExecutionPayloadEnvelope(Container): + message: ExecutionPayloadEnvelope + signature: BLSSignature + + +- name: SignedProposerPreferences#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const SignedProposerPreferences = + spec: | + + class SignedProposerPreferences(Container): + message: ProposerPreferences + signature: BLSSignature + + +- name: SignedVoluntaryExit#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const SignedVoluntaryExit = + spec: | + + class SignedVoluntaryExit(Container): + message: VoluntaryExit + signature: BLSSignature + + +- name: SigningData#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const SigningData = + spec: | + + class SigningData(Container): + object_root: Root + domain: Domain + + +- name: SingleAttestation#electra + sources: + - file: packages/types/src/electra/sszTypes.ts + search: export const SingleAttestation = + spec: | + + class SingleAttestation(Container): + committee_index: CommitteeIndex + attester_index: ValidatorIndex + data: AttestationData + signature: BLSSignature + + +- name: SyncAggregate#altair + sources: + - file: packages/types/src/altair/sszTypes.ts + search: export const SyncAggregate = + spec: | + + class SyncAggregate(Container): + sync_committee_bits: Bitvector[SYNC_COMMITTEE_SIZE] + sync_committee_signature: BLSSignature + + +- name: SyncAggregatorSelectionData#altair + sources: + - file: packages/types/src/altair/sszTypes.ts + search: export const SyncAggregatorSelectionData = + spec: | + + class SyncAggregatorSelectionData(Container): + slot: Slot + subcommittee_index: uint64 + + +- name: SyncCommittee#altair + sources: + - file: packages/types/src/altair/sszTypes.ts + search: export const SyncCommittee = + spec: | + + class SyncCommittee(Container): + pubkeys: Vector[BLSPubkey, SYNC_COMMITTEE_SIZE] + aggregate_pubkey: BLSPubkey + + +- name: SyncCommitteeContribution#altair + sources: + - file: packages/types/src/altair/sszTypes.ts + search: export const SyncCommitteeContribution = + spec: | + + class SyncCommitteeContribution(Container): + slot: Slot + beacon_block_root: Root + subcommittee_index: uint64 + aggregation_bits: Bitvector[SYNC_COMMITTEE_SIZE // SYNC_COMMITTEE_SUBNET_COUNT] + signature: BLSSignature + + +- name: SyncCommitteeMessage#altair + sources: + - file: packages/types/src/altair/sszTypes.ts + search: export const SyncCommitteeMessage = + spec: | + + class SyncCommitteeMessage(Container): + slot: Slot + beacon_block_root: Root + validator_index: ValidatorIndex + signature: BLSSignature + + +- name: Validator#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const Validator = + spec: | + + class Validator(Container): + pubkey: BLSPubkey + withdrawal_credentials: Bytes32 + effective_balance: Gwei + slashed: boolean + activation_eligibility_epoch: Epoch + activation_epoch: Epoch + exit_epoch: Epoch + withdrawable_epoch: Epoch + + +- name: VoluntaryExit#phase0 + sources: + - file: packages/types/src/phase0/sszTypes.ts + search: export const VoluntaryExit = + spec: | + + class VoluntaryExit(Container): + epoch: Epoch + validator_index: ValidatorIndex + + +- name: Withdrawal#capella + sources: + - file: packages/types/src/capella/sszTypes.ts + search: export const Withdrawal = + spec: | + + class Withdrawal(Container): + index: WithdrawalIndex + validator_index: ValidatorIndex + address: ExecutionAddress + amount: Gwei + + +- name: WithdrawalRequest#electra + sources: + - file: packages/types/src/electra/sszTypes.ts + search: export const WithdrawalRequest = + spec: | + + class WithdrawalRequest(Container): + source_address: ExecutionAddress + validator_pubkey: BLSPubkey + amount: Gwei + diff --git a/specrefs/dataclasses.yml b/specrefs/dataclasses.yml new file mode 100644 index 000000000000..0549fbe66d68 --- /dev/null +++ b/specrefs/dataclasses.yml @@ -0,0 +1,340 @@ +- name: BlobParameters#fulu + sources: + - file: packages/config/src/forkConfig/types.ts + search: export type BlobParameters = + spec: | + + class BlobParameters: + epoch: Epoch + max_blobs_per_block: uint64 + + +- name: BlobsBundle#deneb + sources: + - file: packages/types/src/deneb/sszTypes.ts + search: export const BlobsBundle = + spec: | + + class BlobsBundle(object): + commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] + proofs: List[KZGProof, MAX_BLOB_COMMITMENTS_PER_BLOCK] + blobs: List[Blob, MAX_BLOB_COMMITMENTS_PER_BLOCK] + + +- name: BlobsBundle#fulu + sources: + - file: packages/types/src/fulu/sszTypes.ts + search: export const BlobsBundle = + spec: | + + class BlobsBundle(object): + commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] + # [Modified in Fulu:EIP7594] + proofs: List[KZGProof, FIELD_ELEMENTS_PER_EXT_BLOB * MAX_BLOB_COMMITMENTS_PER_BLOCK] + blobs: List[Blob, MAX_BLOB_COMMITMENTS_PER_BLOCK] + + +- name: ExpectedWithdrawals#capella + sources: [] + spec: | + + class ExpectedWithdrawals(object): + withdrawals: Sequence[Withdrawal] + processed_sweep_withdrawals_count: uint64 + + +- name: ExpectedWithdrawals#electra + sources: [] + spec: | + + class ExpectedWithdrawals(object): + withdrawals: Sequence[Withdrawal] + # [New in Electra:EIP7251] + processed_partial_withdrawals_count: uint64 + processed_sweep_withdrawals_count: uint64 + + +- name: ExpectedWithdrawals#gloas + sources: [] + spec: | + + class ExpectedWithdrawals(object): + withdrawals: Sequence[Withdrawal] + # [New in Gloas:EIP7732] + processed_builder_withdrawals_count: uint64 + processed_partial_withdrawals_count: uint64 + # [New in Gloas:EIP7732] + processed_builders_sweep_count: uint64 + processed_sweep_withdrawals_count: uint64 + + +- name: GetPayloadResponse#bellatrix + sources: + - file: packages/beacon-node/src/execution/engine/types.ts + search: type ExecutionPayloadRpcWithValue = + - file: packages/beacon-node/src/execution/engine/interface.ts + search: "getPayload(" + spec: | + + class GetPayloadResponse(object): + execution_payload: ExecutionPayload + + +- name: GetPayloadResponse#capella + sources: + - file: packages/beacon-node/src/execution/engine/types.ts + search: type ExecutionPayloadRpcWithValue = + - file: packages/beacon-node/src/execution/engine/interface.ts + search: "getPayload(" + spec: | + + class GetPayloadResponse(object): + execution_payload: ExecutionPayload + block_value: uint256 + + +- name: GetPayloadResponse#deneb + sources: + - file: packages/beacon-node/src/execution/engine/types.ts + search: type ExecutionPayloadRpcWithValue = + - file: packages/beacon-node/src/execution/engine/interface.ts + search: "getPayload(" + spec: | + + class GetPayloadResponse(object): + execution_payload: ExecutionPayload + block_value: uint256 + # [New in Deneb:EIP4844] + blobs_bundle: BlobsBundle + + +- name: GetPayloadResponse#electra + sources: + - file: packages/beacon-node/src/execution/engine/types.ts + search: type ExecutionPayloadRpcWithValue = + - file: packages/beacon-node/src/execution/engine/interface.ts + search: "getPayload(" + spec: | + + class GetPayloadResponse(object): + execution_payload: ExecutionPayload + block_value: uint256 + blobs_bundle: BlobsBundle + # [New in Electra] + execution_requests: Sequence[bytes] + + +- name: GetPayloadResponse#fulu + sources: + - file: packages/beacon-node/src/execution/engine/types.ts + search: type ExecutionPayloadRpcWithValue = + - file: packages/beacon-node/src/execution/engine/interface.ts + search: "getPayload(" + spec: | + + class GetPayloadResponse(object): + execution_payload: ExecutionPayload + block_value: uint256 + # [Modified in Fulu:EIP7594] + blobs_bundle: BlobsBundle + execution_requests: Sequence[bytes] + + +- name: LatestMessage#phase0 + sources: [] + spec: | + + @dataclass(eq=True, frozen=True) + class LatestMessage(object): + epoch: Epoch + root: Root + + +- name: LatestMessage#gloas + sources: [] + spec: | + + @dataclass(eq=True, frozen=True) + class LatestMessage(object): + slot: Slot + root: Root + payload_present: boolean + + +- name: LightClientStore#altair + sources: + - file: packages/types/src/altair/sszTypes.ts + search: export const LightClientStore = + spec: | + + class LightClientStore(object): + # Header that is finalized + finalized_header: LightClientHeader + # Sync committees corresponding to the finalized header + current_sync_committee: SyncCommittee + next_sync_committee: SyncCommittee + # Best available header to switch finalized head to if we see nothing else + best_valid_update: Optional[LightClientUpdate] + # Most recent available reasonably-safe header + optimistic_header: LightClientHeader + # Max number of active participants in a sync committee (used to calculate safety threshold) + previous_max_active_participants: uint64 + current_max_active_participants: uint64 + + +- name: LightClientStore#capella + sources: + - file: packages/types/src/capella/sszTypes.ts + search: export const LightClientStore = + spec: | + + class LightClientStore(object): + # [Modified in Capella] + finalized_header: LightClientHeader + current_sync_committee: SyncCommittee + next_sync_committee: SyncCommittee + # [Modified in Capella] + best_valid_update: Optional[LightClientUpdate] + # [Modified in Capella] + optimistic_header: LightClientHeader + previous_max_active_participants: uint64 + current_max_active_participants: uint64 + + +- name: NewPayloadRequest#bellatrix + sources: + - file: packages/beacon-node/src/execution/engine/interface.ts + search: "notifyNewPayload(" + spec: | + + class NewPayloadRequest(object): + execution_payload: ExecutionPayload + + +- name: NewPayloadRequest#deneb + sources: + - file: packages/beacon-node/src/execution/engine/interface.ts + search: "notifyNewPayload(" + spec: | + + class NewPayloadRequest(object): + execution_payload: ExecutionPayload + versioned_hashes: Sequence[VersionedHash] + parent_beacon_block_root: Root + + +- name: NewPayloadRequest#electra + sources: + - file: packages/beacon-node/src/execution/engine/interface.ts + search: "notifyNewPayload(" + spec: | + + class NewPayloadRequest(object): + execution_payload: ExecutionPayload + versioned_hashes: Sequence[VersionedHash] + parent_beacon_block_root: Root + # [New in Electra] + execution_requests: ExecutionRequests + + +- name: OptimisticStore#bellatrix + sources: [] + spec: | + + class OptimisticStore(object): + optimistic_roots: Set[Root] + head_block_root: Root + blocks: Dict[Root, BeaconBlock] = field(default_factory=dict) + block_states: Dict[Root, BeaconState] = field(default_factory=dict) + + +- name: PayloadAttributes#bellatrix + sources: + - file: packages/types/src/bellatrix/sszTypes.ts + search: export const PayloadAttributes = + spec: | + + class PayloadAttributes(object): + timestamp: uint64 + prev_randao: Bytes32 + suggested_fee_recipient: ExecutionAddress + + +- name: PayloadAttributes#capella + sources: + - file: packages/types/src/capella/sszTypes.ts + search: export const PayloadAttributes = + spec: | + + class PayloadAttributes(object): + timestamp: uint64 + prev_randao: Bytes32 + suggested_fee_recipient: ExecutionAddress + # [New in Capella] + withdrawals: Sequence[Withdrawal] + + +- name: PayloadAttributes#deneb + sources: + - file: packages/types/src/deneb/sszTypes.ts + search: export const PayloadAttributes = + spec: | + + class PayloadAttributes(object): + timestamp: uint64 + prev_randao: Bytes32 + suggested_fee_recipient: ExecutionAddress + withdrawals: Sequence[Withdrawal] + # [New in Deneb:EIP4788] + parent_beacon_block_root: Root + + +- name: Store#phase0 + sources: + - file: packages/fork-choice/src/forkChoice/store.ts + search: export interface IForkChoiceStore + spec: | + + class Store(object): + time: uint64 + genesis_time: uint64 + justified_checkpoint: Checkpoint + finalized_checkpoint: Checkpoint + unrealized_justified_checkpoint: Checkpoint + unrealized_finalized_checkpoint: Checkpoint + proposer_boost_root: Root + equivocating_indices: Set[ValidatorIndex] + blocks: Dict[Root, BeaconBlock] = field(default_factory=dict) + block_states: Dict[Root, BeaconState] = field(default_factory=dict) + block_timeliness: Dict[Root, boolean] = field(default_factory=dict) + checkpoint_states: Dict[Checkpoint, BeaconState] = field(default_factory=dict) + latest_messages: Dict[ValidatorIndex, LatestMessage] = field(default_factory=dict) + unrealized_justifications: Dict[Root, Checkpoint] = field(default_factory=dict) + + +- name: Store#gloas + sources: [] + spec: | + + class Store(object): + time: uint64 + genesis_time: uint64 + justified_checkpoint: Checkpoint + finalized_checkpoint: Checkpoint + unrealized_justified_checkpoint: Checkpoint + unrealized_finalized_checkpoint: Checkpoint + proposer_boost_root: Root + equivocating_indices: Set[ValidatorIndex] + blocks: Dict[Root, BeaconBlock] = field(default_factory=dict) + block_states: Dict[Root, BeaconState] = field(default_factory=dict) + block_timeliness: Dict[Root, Vector[boolean, NUM_BLOCK_TIMELINESS_DEADLINES]] = field( + default_factory=dict + ) + checkpoint_states: Dict[Checkpoint, BeaconState] = field(default_factory=dict) + latest_messages: Dict[ValidatorIndex, LatestMessage] = field(default_factory=dict) + unrealized_justifications: Dict[Root, Checkpoint] = field(default_factory=dict) + # [New in Gloas:EIP7732] + execution_payload_states: Dict[Root, BeaconState] = field(default_factory=dict) + # [New in Gloas:EIP7732] + ptc_vote: Dict[Root, Vector[boolean, PTC_SIZE]] = field(default_factory=dict) + diff --git a/specrefs/functions.yml b/specrefs/functions.yml new file mode 100644 index 000000000000..73a3d6769ca5 --- /dev/null +++ b/specrefs/functions.yml @@ -0,0 +1,11297 @@ +- name: add_builder_to_registry#gloas + sources: [] + spec: | + + def add_builder_to_registry( + state: BeaconState, pubkey: BLSPubkey, withdrawal_credentials: Bytes32, amount: uint64 + ) -> None: + index = get_index_for_new_builder(state) + builder = get_builder_from_deposit(state, pubkey, withdrawal_credentials, amount) + set_or_append_list(state.builders, index, builder) + + +- name: add_flag#altair + sources: [] + spec: | + + def add_flag(flags: ParticipationFlags, flag_index: int) -> ParticipationFlags: + """ + Return a new ``ParticipationFlags`` adding ``flag_index`` to ``flags``. + """ + flag = ParticipationFlags(2**flag_index) + return flags | flag + + +- name: add_validator_to_registry#phase0 + sources: + - file: packages/state-transition/src/block/processDeposit.ts + search: export function addValidatorToRegistry( + spec: | + + def add_validator_to_registry( + state: BeaconState, pubkey: BLSPubkey, withdrawal_credentials: Bytes32, amount: uint64 + ) -> None: + state.validators.append(get_validator_from_deposit(pubkey, withdrawal_credentials, amount)) + state.balances.append(amount) + + +- name: add_validator_to_registry#altair + sources: + - file: packages/state-transition/src/block/processDeposit.ts + search: export function addValidatorToRegistry( + spec: | + + def add_validator_to_registry( + state: BeaconState, pubkey: BLSPubkey, withdrawal_credentials: Bytes32, amount: uint64 + ) -> None: + index = get_index_for_new_validator(state) + validator = get_validator_from_deposit(pubkey, withdrawal_credentials, amount) + set_or_append_list(state.validators, index, validator) + set_or_append_list(state.balances, index, amount) + # [New in Altair] + set_or_append_list(state.previous_epoch_participation, index, ParticipationFlags(0b0000_0000)) + set_or_append_list(state.current_epoch_participation, index, ParticipationFlags(0b0000_0000)) + set_or_append_list(state.inactivity_scores, index, uint64(0)) + + +- name: add_validator_to_registry#electra + sources: + - file: packages/state-transition/src/block/processDeposit.ts + search: export function addValidatorToRegistry( + spec: | + + def add_validator_to_registry( + state: BeaconState, pubkey: BLSPubkey, withdrawal_credentials: Bytes32, amount: uint64 + ) -> None: + index = get_index_for_new_validator(state) + # [Modified in Electra:EIP7251] + validator = get_validator_from_deposit(pubkey, withdrawal_credentials, amount) + set_or_append_list(state.validators, index, validator) + set_or_append_list(state.balances, index, amount) + set_or_append_list(state.previous_epoch_participation, index, ParticipationFlags(0b0000_0000)) + set_or_append_list(state.current_epoch_participation, index, ParticipationFlags(0b0000_0000)) + set_or_append_list(state.inactivity_scores, index, uint64(0)) + + +- name: apply_deposit#phase0 + sources: + - file: packages/state-transition/src/block/processDeposit.ts + search: export function applyDeposit( + spec: | + + def apply_deposit( + state: BeaconState, + pubkey: BLSPubkey, + withdrawal_credentials: Bytes32, + amount: uint64, + signature: BLSSignature, + ) -> None: + validator_pubkeys = [v.pubkey for v in state.validators] + if pubkey not in validator_pubkeys: + # Verify the deposit signature (proof of possession) which is not checked by the deposit contract + deposit_message = DepositMessage( + pubkey=pubkey, + withdrawal_credentials=withdrawal_credentials, + amount=amount, + ) + # Fork-agnostic domain since deposits are valid across forks + domain = compute_domain(DOMAIN_DEPOSIT) + signing_root = compute_signing_root(deposit_message, domain) + if bls.Verify(pubkey, signing_root, signature): + add_validator_to_registry(state, pubkey, withdrawal_credentials, amount) + else: + # Increase balance by deposit amount + index = ValidatorIndex(validator_pubkeys.index(pubkey)) + increase_balance(state, index, amount) + + +- name: apply_deposit#electra + sources: + - file: packages/state-transition/src/block/processDeposit.ts + search: export function applyDeposit( + spec: | + + def apply_deposit( + state: BeaconState, + pubkey: BLSPubkey, + withdrawal_credentials: Bytes32, + amount: uint64, + signature: BLSSignature, + ) -> None: + validator_pubkeys = [v.pubkey for v in state.validators] + if pubkey not in validator_pubkeys: + # Verify the deposit signature (proof of possession) which is not checked by the deposit contract + if is_valid_deposit_signature(pubkey, withdrawal_credentials, amount, signature): + # [Modified in Electra:EIP7251] + add_validator_to_registry(state, pubkey, withdrawal_credentials, Gwei(0)) + else: + return + + # [Modified in Electra:EIP7251] + # Increase balance by deposit amount + state.pending_deposits.append( + PendingDeposit( + pubkey=pubkey, + withdrawal_credentials=withdrawal_credentials, + amount=amount, + signature=signature, + slot=GENESIS_SLOT, # Use GENESIS_SLOT to distinguish from a pending deposit request + ) + ) + + +- name: apply_deposit_for_builder#gloas + sources: + - file: packages/state-transition/src/block/processDepositRequest.ts + search: export function applyDepositForBuilder( + spec: | + + def apply_deposit_for_builder( + state: BeaconState, + pubkey: BLSPubkey, + withdrawal_credentials: Bytes32, + amount: uint64, + signature: BLSSignature, + ) -> None: + builder_pubkeys = [b.pubkey for b in state.builders] + if pubkey not in builder_pubkeys: + # Verify the deposit signature (proof of possession) which is not checked by the deposit contract + if is_valid_deposit_signature(pubkey, withdrawal_credentials, amount, signature): + add_builder_to_registry(state, pubkey, withdrawal_credentials, amount) + else: + # Increase balance by deposit amount + builder_index = builder_pubkeys.index(pubkey) + state.builders[builder_index].balance += amount + + +- name: apply_light_client_update#altair + sources: [] + spec: | + + def apply_light_client_update(store: LightClientStore, update: LightClientUpdate) -> None: + store_period = compute_sync_committee_period_at_slot(store.finalized_header.beacon.slot) + update_finalized_period = compute_sync_committee_period_at_slot( + update.finalized_header.beacon.slot + ) + if not is_next_sync_committee_known(store): + assert update_finalized_period == store_period + store.next_sync_committee = update.next_sync_committee + elif update_finalized_period == store_period + 1: + store.current_sync_committee = store.next_sync_committee + store.next_sync_committee = update.next_sync_committee + store.previous_max_active_participants = store.current_max_active_participants + store.current_max_active_participants = 0 + if update.finalized_header.beacon.slot > store.finalized_header.beacon.slot: + store.finalized_header = update.finalized_header + if store.finalized_header.beacon.slot > store.optimistic_header.beacon.slot: + store.optimistic_header = store.finalized_header + + +- name: apply_pending_deposit#electra + sources: + - file: packages/state-transition/src/epoch/processPendingDeposits.ts + search: function applyPendingDeposit( + spec: | + + def apply_pending_deposit(state: BeaconState, deposit: PendingDeposit) -> None: + """ + Applies ``deposit`` to the ``state``. + """ + validator_pubkeys = [v.pubkey for v in state.validators] + if deposit.pubkey not in validator_pubkeys: + # Verify the deposit signature (proof of possession) which is not checked by the deposit contract + if is_valid_deposit_signature( + deposit.pubkey, deposit.withdrawal_credentials, deposit.amount, deposit.signature + ): + add_validator_to_registry( + state, deposit.pubkey, deposit.withdrawal_credentials, deposit.amount + ) + else: + validator_index = ValidatorIndex(validator_pubkeys.index(deposit.pubkey)) + increase_balance(state, validator_index, deposit.amount) + + +- name: apply_withdrawals#capella + sources: [] + spec: | + + def apply_withdrawals(state: BeaconState, withdrawals: Sequence[Withdrawal]) -> None: + for withdrawal in withdrawals: + decrease_balance(state, withdrawal.validator_index, withdrawal.amount) + + +- name: apply_withdrawals#gloas + sources: [] + spec: | + + def apply_withdrawals(state: BeaconState, withdrawals: Sequence[Withdrawal]) -> None: + for withdrawal in withdrawals: + # [Modified in Gloas:EIP7732] + if is_builder_index(withdrawal.validator_index): + builder_index = convert_validator_index_to_builder_index(withdrawal.validator_index) + builder_balance = state.builders[builder_index].balance + state.builders[builder_index].balance -= min(withdrawal.amount, builder_balance) + else: + decrease_balance(state, withdrawal.validator_index, withdrawal.amount) + + +- name: block_to_light_client_header#altair + sources: + - file: packages/beacon-node/src/chain/lightClient/index.ts + search: export function blockToLightClientHeader( + spec: | + + def block_to_light_client_header(block: SignedBeaconBlock) -> LightClientHeader: + return LightClientHeader( + beacon=BeaconBlockHeader( + slot=block.message.slot, + proposer_index=block.message.proposer_index, + parent_root=block.message.parent_root, + state_root=block.message.state_root, + body_root=hash_tree_root(block.message.body), + ), + ) + + +- name: block_to_light_client_header#capella + sources: + - file: packages/beacon-node/src/chain/lightClient/index.ts + search: export function blockToLightClientHeader( + spec: | + + def block_to_light_client_header(block: SignedBeaconBlock) -> LightClientHeader: + epoch = compute_epoch_at_slot(block.message.slot) + + if epoch >= CAPELLA_FORK_EPOCH: + payload = block.message.body.execution_payload + execution_header = ExecutionPayloadHeader( + parent_hash=payload.parent_hash, + fee_recipient=payload.fee_recipient, + state_root=payload.state_root, + receipts_root=payload.receipts_root, + logs_bloom=payload.logs_bloom, + prev_randao=payload.prev_randao, + block_number=payload.block_number, + gas_limit=payload.gas_limit, + gas_used=payload.gas_used, + timestamp=payload.timestamp, + extra_data=payload.extra_data, + base_fee_per_gas=payload.base_fee_per_gas, + block_hash=payload.block_hash, + transactions_root=hash_tree_root(payload.transactions), + withdrawals_root=hash_tree_root(payload.withdrawals), + ) + execution_branch = ExecutionBranch( + compute_merkle_proof(block.message.body, EXECUTION_PAYLOAD_GINDEX) + ) + else: + # Note that during fork transitions, `finalized_header` may still point to earlier forks. + # While Bellatrix blocks also contain an `ExecutionPayload` (minus `withdrawals_root`), + # it was not included in the corresponding light client data. To ensure compatibility + # with legacy data going through `upgrade_lc_header_to_capella`, leave out execution data. + execution_header = ExecutionPayloadHeader() + execution_branch = ExecutionBranch() + + return LightClientHeader( + beacon=BeaconBlockHeader( + slot=block.message.slot, + proposer_index=block.message.proposer_index, + parent_root=block.message.parent_root, + state_root=block.message.state_root, + body_root=hash_tree_root(block.message.body), + ), + execution=execution_header, + execution_branch=execution_branch, + ) + + +- name: block_to_light_client_header#deneb + sources: + - file: packages/beacon-node/src/chain/lightClient/index.ts + search: export function blockToLightClientHeader( + spec: | + + def block_to_light_client_header(block: SignedBeaconBlock) -> LightClientHeader: + epoch = compute_epoch_at_slot(block.message.slot) + + if epoch >= CAPELLA_FORK_EPOCH: + payload = block.message.body.execution_payload + execution_header = ExecutionPayloadHeader( + parent_hash=payload.parent_hash, + fee_recipient=payload.fee_recipient, + state_root=payload.state_root, + receipts_root=payload.receipts_root, + logs_bloom=payload.logs_bloom, + prev_randao=payload.prev_randao, + block_number=payload.block_number, + gas_limit=payload.gas_limit, + gas_used=payload.gas_used, + timestamp=payload.timestamp, + extra_data=payload.extra_data, + base_fee_per_gas=payload.base_fee_per_gas, + block_hash=payload.block_hash, + transactions_root=hash_tree_root(payload.transactions), + withdrawals_root=hash_tree_root(payload.withdrawals), + ) + + # [New in Deneb:EIP4844] + if epoch >= DENEB_FORK_EPOCH: + execution_header.blob_gas_used = payload.blob_gas_used + execution_header.excess_blob_gas = payload.excess_blob_gas + + execution_branch = ExecutionBranch( + compute_merkle_proof(block.message.body, EXECUTION_PAYLOAD_GINDEX) + ) + else: + # Note that during fork transitions, `finalized_header` may still point to earlier forks. + # While Bellatrix blocks also contain an `ExecutionPayload` (minus `withdrawals_root`), + # it was not included in the corresponding light client data. To ensure compatibility + # with legacy data going through `upgrade_lc_header_to_capella`, leave out execution data. + execution_header = ExecutionPayloadHeader() + execution_branch = ExecutionBranch() + + return LightClientHeader( + beacon=BeaconBlockHeader( + slot=block.message.slot, + proposer_index=block.message.proposer_index, + parent_root=block.message.parent_root, + state_root=block.message.state_root, + body_root=hash_tree_root(block.message.body), + ), + execution=execution_header, + execution_branch=execution_branch, + ) + + +- name: bytes_to_uint64#phase0 + sources: [] + spec: | + + def bytes_to_uint64(data: bytes) -> uint64: + """ + Return the integer deserialization of ``data`` interpreted as ``ENDIANNESS``-endian. + """ + return uint64(int.from_bytes(data, ENDIANNESS)) + + +- name: calculate_committee_fraction#phase0 + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: export function getCommitteeFraction( + spec: | + + def calculate_committee_fraction(state: BeaconState, committee_percent: uint64) -> Gwei: + committee_weight = get_total_active_balance(state) // SLOTS_PER_EPOCH + return Gwei((committee_weight * committee_percent) // 100) + + +- name: can_builder_cover_bid#gloas + sources: + - file: packages/state-transition/src/util/gloas.ts + search: export function canBuilderCoverBid( + spec: | + + def can_builder_cover_bid( + state: BeaconState, builder_index: BuilderIndex, bid_amount: Gwei + ) -> bool: + builder_balance = state.builders[builder_index].balance + pending_withdrawals_amount = get_pending_balance_to_withdraw_for_builder(state, builder_index) + min_balance = MIN_DEPOSIT_AMOUNT + pending_withdrawals_amount + if builder_balance < min_balance: + return False + return builder_balance - min_balance >= bid_amount + + +- name: check_if_validator_active#phase0 + sources: + - file: packages/state-transition/src/util/validator.ts + search: export function isActiveValidator( + spec: | + + def check_if_validator_active(state: BeaconState, validator_index: ValidatorIndex) -> bool: + validator = state.validators[validator_index] + return is_active_validator(validator, get_current_epoch(state)) + + +- name: compute_activation_exit_epoch#phase0 + sources: + - file: packages/state-transition/src/util/epoch.ts + search: export function computeActivationExitEpoch( + spec: | + + def compute_activation_exit_epoch(epoch: Epoch) -> Epoch: + """ + Return the epoch during which validator activations and exits initiated in ``epoch`` take effect. + """ + return Epoch(epoch + 1 + MAX_SEED_LOOKAHEAD) + + +- name: compute_balance_weighted_acceptance#gloas + sources: [] + spec: | + + def compute_balance_weighted_acceptance( + state: BeaconState, index: ValidatorIndex, seed: Bytes32, i: uint64 + ) -> bool: + """ + Return whether to accept the selection of the validator ``index``, with probability + proportional to its ``effective_balance``, and randomness given by ``seed`` and ``i``. + """ + MAX_RANDOM_VALUE = 2**16 - 1 + random_bytes = hash(seed + uint_to_bytes(i // 16)) + offset = i % 16 * 2 + random_value = bytes_to_uint64(random_bytes[offset : offset + 2]) + effective_balance = state.validators[index].effective_balance + return effective_balance * MAX_RANDOM_VALUE >= MAX_EFFECTIVE_BALANCE_ELECTRA * random_value + + +- name: compute_balance_weighted_selection#gloas + sources: [] + spec: | + + def compute_balance_weighted_selection( + state: BeaconState, + indices: Sequence[ValidatorIndex], + seed: Bytes32, + size: uint64, + shuffle_indices: bool, + ) -> Sequence[ValidatorIndex]: + """ + Return ``size`` indices sampled by effective balance, using ``indices`` + as candidates. If ``shuffle_indices`` is ``True``, candidate indices + are themselves sampled from ``indices`` by shuffling it, otherwise + ``indices`` is traversed in order. + """ + total = uint64(len(indices)) + assert total > 0 + selected: List[ValidatorIndex] = [] + i = uint64(0) + while len(selected) < size: + next_index = i % total + if shuffle_indices: + next_index = compute_shuffled_index(next_index, total, seed) + candidate_index = indices[next_index] + if compute_balance_weighted_acceptance(state, candidate_index, seed, i): + selected.append(candidate_index) + i += 1 + return selected + + +- name: compute_columns_for_custody_group#fulu + sources: + - file: packages/beacon-node/src/util/dataColumns.ts + search: export function computeColumnsForCustodyGroup( + spec: | + + def compute_columns_for_custody_group(custody_group: CustodyIndex) -> Sequence[ColumnIndex]: + assert custody_group < NUMBER_OF_CUSTODY_GROUPS + columns_per_group = NUMBER_OF_COLUMNS // NUMBER_OF_CUSTODY_GROUPS + return [ + ColumnIndex(NUMBER_OF_CUSTODY_GROUPS * i + custody_group) for i in range(columns_per_group) + ] + + +- name: compute_committee#phase0 + sources: + - file: packages/state-transition/src/util/epochShuffling.ts + search: function buildCommitteesFromShuffling( + spec: | + + def compute_committee( + indices: Sequence[ValidatorIndex], seed: Bytes32, index: uint64, count: uint64 + ) -> Sequence[ValidatorIndex]: + """ + Return the committee corresponding to ``indices``, ``seed``, ``index``, and committee ``count``. + """ + start = (len(indices) * index) // count + end = (len(indices) * uint64(index + 1)) // count + return [ + indices[compute_shuffled_index(uint64(i), uint64(len(indices)), seed)] + for i in range(start, end) + ] + + +- name: compute_consolidation_epoch_and_update_churn#electra + sources: + - file: packages/state-transition/src/util/epoch.ts + search: export function computeConsolidationEpochAndUpdateChurn( + spec: | + + def compute_consolidation_epoch_and_update_churn( + state: BeaconState, consolidation_balance: Gwei + ) -> Epoch: + earliest_consolidation_epoch = max( + state.earliest_consolidation_epoch, compute_activation_exit_epoch(get_current_epoch(state)) + ) + per_epoch_consolidation_churn = get_consolidation_churn_limit(state) + # New epoch for consolidations. + if state.earliest_consolidation_epoch < earliest_consolidation_epoch: + consolidation_balance_to_consume = per_epoch_consolidation_churn + else: + consolidation_balance_to_consume = state.consolidation_balance_to_consume + + # Consolidation doesn't fit in the current earliest epoch. + if consolidation_balance > consolidation_balance_to_consume: + balance_to_process = consolidation_balance - consolidation_balance_to_consume + additional_epochs = (balance_to_process - 1) // per_epoch_consolidation_churn + 1 + earliest_consolidation_epoch += additional_epochs + consolidation_balance_to_consume += additional_epochs * per_epoch_consolidation_churn + + # Consume the balance and update state variables. + state.consolidation_balance_to_consume = ( + consolidation_balance_to_consume - consolidation_balance + ) + state.earliest_consolidation_epoch = earliest_consolidation_epoch + + return state.earliest_consolidation_epoch + + +- name: compute_domain#phase0 + sources: + - file: packages/state-transition/src/util/domain.ts + search: export function computeDomain( + spec: | + + def compute_domain( + domain_type: DomainType, fork_version: Version = None, genesis_validators_root: Root = None + ) -> Domain: + """ + Return the domain for the ``domain_type`` and ``fork_version``. + """ + if fork_version is None: + fork_version = GENESIS_FORK_VERSION + if genesis_validators_root is None: + genesis_validators_root = Root() # all bytes zero by default + fork_data_root = compute_fork_data_root(fork_version, genesis_validators_root) + return Domain(domain_type + fork_data_root[:28]) + + +- name: compute_epoch_at_slot#phase0 + sources: + - file: packages/state-transition/src/util/epoch.ts + search: export function computeEpochAtSlot( + spec: | + + def compute_epoch_at_slot(slot: Slot) -> Epoch: + """ + Return the epoch number at ``slot``. + """ + return Epoch(slot // SLOTS_PER_EPOCH) + + +- name: compute_exit_epoch_and_update_churn#electra + sources: + - file: packages/state-transition/src/util/epoch.ts + search: export function computeExitEpochAndUpdateChurn( + spec: | + + def compute_exit_epoch_and_update_churn(state: BeaconState, exit_balance: Gwei) -> Epoch: + earliest_exit_epoch = max( + state.earliest_exit_epoch, compute_activation_exit_epoch(get_current_epoch(state)) + ) + per_epoch_churn = get_activation_exit_churn_limit(state) + # New epoch for exits. + if state.earliest_exit_epoch < earliest_exit_epoch: + exit_balance_to_consume = per_epoch_churn + else: + exit_balance_to_consume = state.exit_balance_to_consume + + # Exit doesn't fit in the current earliest epoch. + if exit_balance > exit_balance_to_consume: + balance_to_process = exit_balance - exit_balance_to_consume + additional_epochs = (balance_to_process - 1) // per_epoch_churn + 1 + earliest_exit_epoch += additional_epochs + exit_balance_to_consume += additional_epochs * per_epoch_churn + + # Consume the balance and update state variables. + state.exit_balance_to_consume = exit_balance_to_consume - exit_balance + state.earliest_exit_epoch = earliest_exit_epoch + + return state.earliest_exit_epoch + + +- name: compute_fork_data_root#phase0 + sources: + - file: packages/state-transition/src/util/domain.ts + search: export function computeForkDataRoot( + spec: | + + def compute_fork_data_root(current_version: Version, genesis_validators_root: Root) -> Root: + """ + Return the 32-byte fork data root for the ``current_version`` and ``genesis_validators_root``. + This is used primarily in signature domains to avoid collisions across forks/chains. + """ + return hash_tree_root( + ForkData( + current_version=current_version, + genesis_validators_root=genesis_validators_root, + ) + ) + + +- name: compute_fork_digest#phase0 + sources: + - file: packages/config/src/genesisConfig/index.ts + search: export function computeForkDigest( + spec: | + + def compute_fork_digest( + genesis_validators_root: Root, + epoch: Epoch, + ) -> ForkDigest: + """ + Return the 4-byte fork digest for the ``genesis_validators_root`` at a given ``epoch``. + + This is a digest primarily used for domain separation on the p2p layer. + 4-bytes suffices for practical separation of forks/chains. + """ + fork_version = compute_fork_version(epoch) + base_digest = compute_fork_data_root(fork_version, genesis_validators_root) + return ForkDigest(base_digest[:4]) + + +- name: compute_fork_digest#fulu + sources: + - file: packages/config/src/genesisConfig/index.ts + search: export function computeForkDigest( + spec: | + + def compute_fork_digest( + genesis_validators_root: Root, + epoch: Epoch, + ) -> ForkDigest: + """ + Return the 4-byte fork digest for the ``genesis_validators_root`` at a given ``epoch``. + + This is a digest primarily used for domain separation on the p2p layer. + 4-bytes suffices for practical separation of forks/chains. + """ + fork_version = compute_fork_version(epoch) + base_digest = compute_fork_data_root(fork_version, genesis_validators_root) + + # [Modified in Fulu:EIP7892] + # Bitmask digest with hash of blob parameters + blob_parameters = get_blob_parameters(epoch) + return ForkDigest( + bytes( + xor( + base_digest, + hash( + uint_to_bytes(uint64(blob_parameters.epoch)) + + uint_to_bytes(uint64(blob_parameters.max_blobs_per_block)) + ), + ) + )[:4] + ) + + +- name: compute_fork_version#phase0 + sources: [] + spec: | + + def compute_fork_version(epoch: Epoch) -> Version: + """ + Return the fork version at the given ``epoch``. + """ + return GENESIS_FORK_VERSION + + +- name: compute_fork_version#altair + sources: [] + spec: | + + def compute_fork_version(epoch: Epoch) -> Version: + """ + Return the fork version at the given ``epoch``. + """ + if epoch >= ALTAIR_FORK_EPOCH: + return ALTAIR_FORK_VERSION + return GENESIS_FORK_VERSION + + +- name: compute_fork_version#bellatrix + sources: [] + spec: | + + def compute_fork_version(epoch: Epoch) -> Version: + """ + Return the fork version at the given ``epoch``. + """ + if epoch >= BELLATRIX_FORK_EPOCH: + return BELLATRIX_FORK_VERSION + if epoch >= ALTAIR_FORK_EPOCH: + return ALTAIR_FORK_VERSION + return GENESIS_FORK_VERSION + + +- name: compute_fork_version#capella + sources: [] + spec: | + + def compute_fork_version(epoch: Epoch) -> Version: + """ + Return the fork version at the given ``epoch``. + """ + if epoch >= CAPELLA_FORK_EPOCH: + return CAPELLA_FORK_VERSION + if epoch >= BELLATRIX_FORK_EPOCH: + return BELLATRIX_FORK_VERSION + if epoch >= ALTAIR_FORK_EPOCH: + return ALTAIR_FORK_VERSION + return GENESIS_FORK_VERSION + + +- name: compute_fork_version#deneb + sources: [] + spec: | + + def compute_fork_version(epoch: Epoch) -> Version: + """ + Return the fork version at the given ``epoch``. + """ + if epoch >= DENEB_FORK_EPOCH: + return DENEB_FORK_VERSION + if epoch >= CAPELLA_FORK_EPOCH: + return CAPELLA_FORK_VERSION + if epoch >= BELLATRIX_FORK_EPOCH: + return BELLATRIX_FORK_VERSION + if epoch >= ALTAIR_FORK_EPOCH: + return ALTAIR_FORK_VERSION + return GENESIS_FORK_VERSION + + +- name: compute_fork_version#electra + sources: [] + spec: | + + def compute_fork_version(epoch: Epoch) -> Version: + """ + Return the fork version at the given ``epoch``. + """ + if epoch >= ELECTRA_FORK_EPOCH: + return ELECTRA_FORK_VERSION + if epoch >= DENEB_FORK_EPOCH: + return DENEB_FORK_VERSION + if epoch >= CAPELLA_FORK_EPOCH: + return CAPELLA_FORK_VERSION + if epoch >= BELLATRIX_FORK_EPOCH: + return BELLATRIX_FORK_VERSION + if epoch >= ALTAIR_FORK_EPOCH: + return ALTAIR_FORK_VERSION + return GENESIS_FORK_VERSION + + +- name: compute_fork_version#fulu + sources: [] + spec: | + + def compute_fork_version(epoch: Epoch) -> Version: + """ + Return the fork version at the given ``epoch``. + """ + if epoch >= FULU_FORK_EPOCH: + return FULU_FORK_VERSION + if epoch >= ELECTRA_FORK_EPOCH: + return ELECTRA_FORK_VERSION + if epoch >= DENEB_FORK_EPOCH: + return DENEB_FORK_VERSION + if epoch >= CAPELLA_FORK_EPOCH: + return CAPELLA_FORK_VERSION + if epoch >= BELLATRIX_FORK_EPOCH: + return BELLATRIX_FORK_VERSION + if epoch >= ALTAIR_FORK_EPOCH: + return ALTAIR_FORK_VERSION + return GENESIS_FORK_VERSION + + +- name: compute_fork_version#gloas + sources: [] + spec: | + + def compute_fork_version(epoch: Epoch) -> Version: + """ + Return the fork version at the given ``epoch``. + """ + if epoch >= GLOAS_FORK_EPOCH: + return GLOAS_FORK_VERSION + if epoch >= FULU_FORK_EPOCH: + return FULU_FORK_VERSION + if epoch >= ELECTRA_FORK_EPOCH: + return ELECTRA_FORK_VERSION + if epoch >= DENEB_FORK_EPOCH: + return DENEB_FORK_VERSION + if epoch >= CAPELLA_FORK_EPOCH: + return CAPELLA_FORK_VERSION + if epoch >= BELLATRIX_FORK_EPOCH: + return BELLATRIX_FORK_VERSION + if epoch >= ALTAIR_FORK_EPOCH: + return ALTAIR_FORK_VERSION + return GENESIS_FORK_VERSION + + +- name: compute_matrix#fulu + sources: + - file: packages/beacon-node/src/util/dataColumns.ts + search: export async function getCellsAndProofs( + spec: | + + def compute_matrix(blobs: Sequence[Blob]) -> Sequence[MatrixEntry]: + """ + Return the full, flattened sequence of matrix entries. + + This helper demonstrates the relationship between blobs and the matrix of cells/proofs. + The data structure for storing cells/proofs is implementation-dependent. + """ + matrix = [] + for blob_index, blob in enumerate(blobs): + cells, proofs = compute_cells_and_kzg_proofs(blob) + for cell_index, (cell, proof) in enumerate(zip(cells, proofs)): + matrix.append( + MatrixEntry( + cell=cell, + kzg_proof=proof, + column_index=cell_index, + row_index=blob_index, + ) + ) + return matrix + + +- name: compute_new_state_root#phase0 + sources: + - file: packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts + search: export function computeNewStateRoot( + spec: | + + def compute_new_state_root(state: BeaconState, block: BeaconBlock) -> Root: + temp_state: BeaconState = state.copy() + signed_block = SignedBeaconBlock(message=block) + state_transition(temp_state, signed_block, validate_result=False) + return hash_tree_root(temp_state) + + +- name: compute_on_chain_aggregate#electra + sources: [] + spec: | + + def compute_on_chain_aggregate(network_aggregates: Sequence[Attestation]) -> Attestation: + aggregates = sorted( + network_aggregates, key=lambda a: get_committee_indices(a.committee_bits)[0] + ) + + data = aggregates[0].data + aggregation_bits = Bitlist[MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT]() + for a in aggregates: + for b in a.aggregation_bits: + aggregation_bits.append(b) + + signature = bls.Aggregate([a.signature for a in aggregates]) + + committee_indices = [get_committee_indices(a.committee_bits)[0] for a in aggregates] + committee_flags = [(index in committee_indices) for index in range(0, MAX_COMMITTEES_PER_SLOT)] + committee_bits = Bitvector[MAX_COMMITTEES_PER_SLOT](committee_flags) + + return Attestation( + aggregation_bits=aggregation_bits, + data=data, + signature=signature, + committee_bits=committee_bits, + ) + + +- name: compute_proposer_index#phase0 + sources: + - file: packages/state-transition/src/util/seed.ts + search: export function computeProposerIndex( + spec: | + + def compute_proposer_index( + state: BeaconState, indices: Sequence[ValidatorIndex], seed: Bytes32 + ) -> ValidatorIndex: + """ + Return from ``indices`` a random index sampled by effective balance. + """ + assert len(indices) > 0 + MAX_RANDOM_BYTE = 2**8 - 1 + i = uint64(0) + total = uint64(len(indices)) + while True: + candidate_index = indices[compute_shuffled_index(i % total, total, seed)] + random_byte = hash(seed + uint_to_bytes(uint64(i // 32)))[i % 32] + effective_balance = state.validators[candidate_index].effective_balance + if effective_balance * MAX_RANDOM_BYTE >= MAX_EFFECTIVE_BALANCE * random_byte: + return candidate_index + i += 1 + + +- name: compute_proposer_index#electra + sources: + - file: packages/state-transition/src/util/seed.ts + search: export function computeProposerIndex( + spec: | + + def compute_proposer_index( + state: BeaconState, indices: Sequence[ValidatorIndex], seed: Bytes32 + ) -> ValidatorIndex: + """ + Return from ``indices`` a random index sampled by effective balance. + """ + assert len(indices) > 0 + # [Modified in Electra] + MAX_RANDOM_VALUE = 2**16 - 1 + i = uint64(0) + total = uint64(len(indices)) + while True: + candidate_index = indices[compute_shuffled_index(i % total, total, seed)] + # [Modified in Electra] + random_bytes = hash(seed + uint_to_bytes(i // 16)) + offset = i % 16 * 2 + random_value = bytes_to_uint64(random_bytes[offset : offset + 2]) + effective_balance = state.validators[candidate_index].effective_balance + # [Modified in Electra:EIP7251] + if effective_balance * MAX_RANDOM_VALUE >= MAX_EFFECTIVE_BALANCE_ELECTRA * random_value: + return candidate_index + i += 1 + + +- name: compute_proposer_indices#fulu + sources: + - file: packages/state-transition/src/util/seed.ts + search: export function computeProposerIndices( + spec: | + + def compute_proposer_indices( + state: BeaconState, epoch: Epoch, seed: Bytes32, indices: Sequence[ValidatorIndex] + ) -> Vector[ValidatorIndex, SLOTS_PER_EPOCH]: + """ + Return the proposer indices for the given ``epoch``. + """ + start_slot = compute_start_slot_at_epoch(epoch) + seeds = [hash(seed + uint_to_bytes(Slot(start_slot + i))) for i in range(SLOTS_PER_EPOCH)] + return [compute_proposer_index(state, indices, seed) for seed in seeds] + + +- name: compute_proposer_indices#gloas + sources: [] + spec: | + + def compute_proposer_indices( + state: BeaconState, epoch: Epoch, seed: Bytes32, indices: Sequence[ValidatorIndex] + ) -> Vector[ValidatorIndex, SLOTS_PER_EPOCH]: + """ + Return the proposer indices for the given ``epoch``. + """ + start_slot = compute_start_slot_at_epoch(epoch) + seeds = [hash(seed + uint_to_bytes(Slot(start_slot + i))) for i in range(SLOTS_PER_EPOCH)] + # [Modified in Gloas:EIP7732] + return [ + compute_balance_weighted_selection(state, indices, seed, size=1, shuffle_indices=True)[0] + for seed in seeds + ] + + +- name: compute_proposer_score#phase0 + sources: [] + spec: | + + def compute_proposer_score(state: BeaconState) -> Gwei: + committee_weight = get_total_active_balance(state) // SLOTS_PER_EPOCH + return (committee_weight * PROPOSER_SCORE_BOOST) // 100 + + +- name: compute_pulled_up_tip#phase0 + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: // same logic to compute_pulled_up_tip in the spec + spec: | + + def compute_pulled_up_tip(store: Store, block_root: Root) -> None: + state = store.block_states[block_root].copy() + # Pull up the post-state of the block to the next epoch boundary + process_justification_and_finalization(state) + + store.unrealized_justifications[block_root] = state.current_justified_checkpoint + update_unrealized_checkpoints( + store, state.current_justified_checkpoint, state.finalized_checkpoint + ) + + # If the block is from a prior epoch, apply the realized values + block_epoch = compute_epoch_at_slot(store.blocks[block_root].slot) + current_epoch = get_current_store_epoch(store) + if block_epoch < current_epoch: + update_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint) + + +- name: compute_shuffled_index#phase0 + sources: + - file: packages/state-transition/src/util/seed.ts + search: export function computeShuffledIndex( + spec: | + + def compute_shuffled_index(index: uint64, index_count: uint64, seed: Bytes32) -> uint64: + """ + Return the shuffled index corresponding to ``seed`` (and ``index_count``). + """ + assert index < index_count + + # Swap or not (https://link.springer.com/content/pdf/10.1007%2F978-3-642-32009-5_1.pdf) + # See the 'generalized domain' algorithm on page 3 + for current_round in range(SHUFFLE_ROUND_COUNT): + pivot = bytes_to_uint64(hash(seed + uint_to_bytes(uint8(current_round)))[0:8]) % index_count + flip = (pivot + index_count - index) % index_count + position = max(index, flip) + source = hash( + seed + uint_to_bytes(uint8(current_round)) + uint_to_bytes(uint32(position // 256)) + ) + byte = uint8(source[(position % 256) // 8]) + bit = (byte >> (position % 8)) % 2 + index = flip if bit else index + + return index + + +- name: compute_signed_block_header#deneb + sources: + - file: packages/state-transition/src/util/blockRoot.ts + search: export function signedBlockToSignedHeader( + spec: | + + def compute_signed_block_header(signed_block: SignedBeaconBlock) -> SignedBeaconBlockHeader: + block = signed_block.message + block_header = BeaconBlockHeader( + slot=block.slot, + proposer_index=block.proposer_index, + parent_root=block.parent_root, + state_root=block.state_root, + body_root=hash_tree_root(block.body), + ) + return SignedBeaconBlockHeader(message=block_header, signature=signed_block.signature) + + +- name: compute_signing_root#phase0 + sources: + - file: packages/state-transition/src/util/signingRoot.ts + search: export function computeSigningRoot( + spec: | + + def compute_signing_root(ssz_object: SSZObject, domain: Domain) -> Root: + """ + Return the signing root for the corresponding signing data. + """ + return hash_tree_root( + SigningData( + object_root=hash_tree_root(ssz_object), + domain=domain, + ) + ) + + +- name: compute_slots_since_epoch_start#phase0 + sources: + - file: packages/state-transition/src/util/slot.ts + search: export function computeSlotsSinceEpochStart( + spec: | + + def compute_slots_since_epoch_start(slot: Slot) -> int: + return slot - compute_start_slot_at_epoch(compute_epoch_at_slot(slot)) + + +- name: compute_start_slot_at_epoch#phase0 + sources: + - file: packages/state-transition/src/util/epoch.ts + search: export function computeStartSlotAtEpoch( + spec: | + + def compute_start_slot_at_epoch(epoch: Epoch) -> Slot: + """ + Return the start slot of ``epoch``. + """ + return Slot(epoch * SLOTS_PER_EPOCH) + + +- name: compute_subnet_for_attestation#phase0 + sources: + - file: packages/beacon-node/src/chain/validation/attestation.ts + search: export function computeSubnetForSlot( + spec: | + + def compute_subnet_for_attestation( + committees_per_slot: uint64, slot: Slot, committee_index: CommitteeIndex + ) -> SubnetID: + """ + Compute the correct subnet for an attestation for Phase 0. + Note, this mimics expected future behavior where attestations will be mapped to their shard subnet. + """ + slots_since_epoch_start = uint64(slot % SLOTS_PER_EPOCH) + committees_since_epoch_start = committees_per_slot * slots_since_epoch_start + + return SubnetID((committees_since_epoch_start + committee_index) % ATTESTATION_SUBNET_COUNT) + + +- name: compute_subnet_for_blob_sidecar#deneb + sources: + - file: packages/beacon-node/src/chain/validation/blobSidecar.ts + search: function computeSubnetForBlobSidecar( + spec: | + + def compute_subnet_for_blob_sidecar(blob_index: BlobIndex) -> SubnetID: + return SubnetID(blob_index % BLOB_SIDECAR_SUBNET_COUNT) + + +- name: compute_subnet_for_blob_sidecar#electra + sources: + - file: packages/beacon-node/src/chain/validation/blobSidecar.ts + search: function computeSubnetForBlobSidecar( + spec: | + + def compute_subnet_for_blob_sidecar(blob_index: BlobIndex) -> SubnetID: + return SubnetID(blob_index % BLOB_SIDECAR_SUBNET_COUNT_ELECTRA) + + +- name: compute_subnet_for_data_column_sidecar#fulu + sources: + - file: packages/beacon-node/src/chain/validation/dataColumnSidecar.ts + search: export function computeSubnetForDataColumnSidecar( + spec: | + + def compute_subnet_for_data_column_sidecar(column_index: ColumnIndex) -> SubnetID: + return SubnetID(column_index % DATA_COLUMN_SIDECAR_SUBNET_COUNT) + + +- name: compute_subnets_for_sync_committee#altair + sources: [] + spec: | + + def compute_subnets_for_sync_committee( + state: BeaconState, validator_index: ValidatorIndex + ) -> Set[SubnetID]: + next_slot_epoch = compute_epoch_at_slot(Slot(state.slot + 1)) + if compute_sync_committee_period(get_current_epoch(state)) == compute_sync_committee_period( + next_slot_epoch + ): + sync_committee = state.current_sync_committee + else: + sync_committee = state.next_sync_committee + + target_pubkey = state.validators[validator_index].pubkey + sync_committee_indices = [ + index for index, pubkey in enumerate(sync_committee.pubkeys) if pubkey == target_pubkey + ] + return set( + [ + SubnetID(index // (SYNC_COMMITTEE_SIZE // SYNC_COMMITTEE_SUBNET_COUNT)) + for index in sync_committee_indices + ] + ) + + +- name: compute_subscribed_subnet#phase0 + sources: + - file: packages/beacon-node/src/network/subnets/util.ts + search: export function computeSubscribedSubnetByIndex( + spec: | + + def compute_subscribed_subnet(node_id: NodeID, epoch: Epoch, index: int) -> SubnetID: + node_id_prefix = node_id >> (NODE_ID_BITS - ATTESTATION_SUBNET_PREFIX_BITS) + node_offset = node_id % EPOCHS_PER_SUBNET_SUBSCRIPTION + permutation_seed = hash( + uint_to_bytes(uint64((epoch + node_offset) // EPOCHS_PER_SUBNET_SUBSCRIPTION)) + ) + permutated_prefix = compute_shuffled_index( + node_id_prefix, + 1 << ATTESTATION_SUBNET_PREFIX_BITS, + permutation_seed, + ) + return SubnetID((permutated_prefix + index) % ATTESTATION_SUBNET_COUNT) + + +- name: compute_subscribed_subnets#phase0 + sources: + - file: packages/beacon-node/src/network/subnets/util.ts + search: export function computeSubscribedSubnet( + spec: | + + def compute_subscribed_subnets(node_id: NodeID, epoch: Epoch) -> Sequence[SubnetID]: + return [compute_subscribed_subnet(node_id, epoch, index) for index in range(SUBNETS_PER_NODE)] + + +- name: compute_sync_committee_period#altair + sources: + - file: packages/state-transition/src/util/epoch.ts + search: export function computeSyncPeriodAtEpoch( + spec: | + + def compute_sync_committee_period(epoch: Epoch) -> uint64: + return epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + + +- name: compute_sync_committee_period_at_slot#altair + sources: + - file: packages/state-transition/src/util/epoch.ts + search: export function computeSyncPeriodAtSlot( + spec: | + + def compute_sync_committee_period_at_slot(slot: Slot) -> uint64: + return compute_sync_committee_period(compute_epoch_at_slot(slot)) + + +- name: compute_time_at_slot#phase0 + sources: + - file: packages/state-transition/src/util/slot.ts + search: export function computeTimeAtSlot( + spec: | + + def compute_time_at_slot(state: BeaconState, slot: Slot) -> uint64: + slots_since_genesis = slot - GENESIS_SLOT + return uint64(state.genesis_time + slots_since_genesis * SECONDS_PER_SLOT) + + +- name: compute_weak_subjectivity_period#phase0 + sources: + - file: packages/state-transition/src/util/weakSubjectivity.ts + search: export function computeWeakSubjectivityPeriod( + spec: | + + def compute_weak_subjectivity_period(state: BeaconState) -> uint64: + """ + Returns the weak subjectivity period for the current ``state``. + This computation takes into account the effect of: + - validator set churn (bounded by ``get_validator_churn_limit()`` per epoch), and + - validator balance top-ups (bounded by ``MAX_DEPOSITS * SLOTS_PER_EPOCH`` per epoch). + A detailed calculation can be found at: + https://github.com/runtimeverification/beacon-chain-verification/blob/master/weak-subjectivity/weak-subjectivity-analysis.pdf + """ + ws_period = MIN_VALIDATOR_WITHDRAWABILITY_DELAY + N = len(get_active_validator_indices(state, get_current_epoch(state))) + t = get_total_active_balance(state) // N // ETH_TO_GWEI + T = MAX_EFFECTIVE_BALANCE // ETH_TO_GWEI + delta = get_validator_churn_limit(state) + Delta = MAX_DEPOSITS * SLOTS_PER_EPOCH + D = SAFETY_DECAY + + if T * (200 + 3 * D) < t * (200 + 12 * D): + epochs_for_validator_set_churn = ( + N * (t * (200 + 12 * D) - T * (200 + 3 * D)) // (600 * delta * (2 * t + T)) + ) + epochs_for_balance_top_ups = N * (200 + 3 * D) // (600 * Delta) + ws_period += max(epochs_for_validator_set_churn, epochs_for_balance_top_ups) + else: + ws_period += 3 * N * D * t // (200 * Delta * (T - t)) + + return ws_period + + +- name: compute_weak_subjectivity_period#electra + sources: + - file: packages/state-transition/src/util/weakSubjectivity.ts + search: export function computeWeakSubjectivityPeriod( + spec: | + + def compute_weak_subjectivity_period(state: BeaconState) -> uint64: + """ + Returns the weak subjectivity period for the current ``state``. + This computation takes into account the effect of: + - validator set churn (bounded by ``get_balance_churn_limit()`` per epoch) + A detailed calculation can be found at: + https://notes.ethereum.org/@CarlBeek/electra_weak_subjectivity + """ + t = get_total_active_balance(state) + delta = get_balance_churn_limit(state) + epochs_for_validator_set_churn = SAFETY_DECAY * t // (2 * delta * 100) + return MIN_VALIDATOR_WITHDRAWABILITY_DELAY + epochs_for_validator_set_churn + + +- name: convert_builder_index_to_validator_index#gloas + sources: + - file: packages/state-transition/src/util/gloas.ts + search: export function convertBuilderIndexToValidatorIndex( + spec: | + + def convert_builder_index_to_validator_index(builder_index: BuilderIndex) -> ValidatorIndex: + return ValidatorIndex(builder_index | BUILDER_INDEX_FLAG) + + +- name: convert_validator_index_to_builder_index#gloas + sources: + - file: packages/state-transition/src/util/gloas.ts + search: export function convertValidatorIndexToBuilderIndex( + spec: | + + def convert_validator_index_to_builder_index(validator_index: ValidatorIndex) -> BuilderIndex: + return BuilderIndex(validator_index & ~BUILDER_INDEX_FLAG) + + +- name: create_light_client_bootstrap#altair + sources: [] + spec: | + + def create_light_client_bootstrap( + state: BeaconState, block: SignedBeaconBlock + ) -> LightClientBootstrap: + assert compute_epoch_at_slot(state.slot) >= ALTAIR_FORK_EPOCH + + assert state.slot == state.latest_block_header.slot + header = state.latest_block_header.copy() + header.state_root = hash_tree_root(state) + assert hash_tree_root(header) == hash_tree_root(block.message) + + return LightClientBootstrap( + header=block_to_light_client_header(block), + current_sync_committee=state.current_sync_committee, + current_sync_committee_branch=CurrentSyncCommitteeBranch( + compute_merkle_proof(state, current_sync_committee_gindex_at_slot(state.slot)) + ), + ) + + +- name: create_light_client_finality_update#altair + sources: [] + spec: | + + def create_light_client_finality_update(update: LightClientUpdate) -> LightClientFinalityUpdate: + return LightClientFinalityUpdate( + attested_header=update.attested_header, + finalized_header=update.finalized_header, + finality_branch=update.finality_branch, + sync_aggregate=update.sync_aggregate, + signature_slot=update.signature_slot, + ) + + +- name: create_light_client_optimistic_update#altair + sources: [] + spec: | + + def create_light_client_optimistic_update(update: LightClientUpdate) -> LightClientOptimisticUpdate: + return LightClientOptimisticUpdate( + attested_header=update.attested_header, + sync_aggregate=update.sync_aggregate, + signature_slot=update.signature_slot, + ) + + +- name: create_light_client_update#altair + sources: [] + spec: | + + def create_light_client_update( + state: BeaconState, + block: SignedBeaconBlock, + attested_state: BeaconState, + attested_block: SignedBeaconBlock, + finalized_block: Optional[SignedBeaconBlock], + ) -> LightClientUpdate: + assert compute_epoch_at_slot(attested_state.slot) >= ALTAIR_FORK_EPOCH + assert ( + sum(block.message.body.sync_aggregate.sync_committee_bits) + >= MIN_SYNC_COMMITTEE_PARTICIPANTS + ) + + assert state.slot == state.latest_block_header.slot + header = state.latest_block_header.copy() + header.state_root = hash_tree_root(state) + assert hash_tree_root(header) == hash_tree_root(block.message) + update_signature_period = compute_sync_committee_period_at_slot(block.message.slot) + + assert attested_state.slot == attested_state.latest_block_header.slot + attested_header = attested_state.latest_block_header.copy() + attested_header.state_root = hash_tree_root(attested_state) + assert ( + hash_tree_root(attested_header) + == hash_tree_root(attested_block.message) + == block.message.parent_root + ) + update_attested_period = compute_sync_committee_period_at_slot(attested_block.message.slot) + + update = LightClientUpdate() + + update.attested_header = block_to_light_client_header(attested_block) + + # `next_sync_committee` is only useful if the message is signed by the current sync committee + if update_attested_period == update_signature_period: + update.next_sync_committee = attested_state.next_sync_committee + update.next_sync_committee_branch = NextSyncCommitteeBranch( + compute_merkle_proof( + attested_state, next_sync_committee_gindex_at_slot(attested_state.slot) + ) + ) + + # Indicate finality whenever possible + if finalized_block is not None: + if finalized_block.message.slot != GENESIS_SLOT: + update.finalized_header = block_to_light_client_header(finalized_block) + assert ( + hash_tree_root(update.finalized_header.beacon) + == attested_state.finalized_checkpoint.root + ) + else: + assert attested_state.finalized_checkpoint.root == Bytes32() + update.finality_branch = FinalityBranch( + compute_merkle_proof(attested_state, finalized_root_gindex_at_slot(attested_state.slot)) + ) + + update.sync_aggregate = block.message.body.sync_aggregate + update.signature_slot = block.message.slot + + return update + + +- name: current_sync_committee_gindex_at_slot#altair + sources: [] + spec: | + + def current_sync_committee_gindex_at_slot(_slot: Slot) -> GeneralizedIndex: + return CURRENT_SYNC_COMMITTEE_GINDEX + + +- name: current_sync_committee_gindex_at_slot#electra + sources: [] + spec: | + + def current_sync_committee_gindex_at_slot(slot: Slot) -> GeneralizedIndex: + epoch = compute_epoch_at_slot(slot) + + # [Modified in Electra] + if epoch >= ELECTRA_FORK_EPOCH: + return CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA + return CURRENT_SYNC_COMMITTEE_GINDEX + + +- name: decrease_balance#phase0 + sources: + - file: packages/state-transition/src/util/balance.ts + search: export function decreaseBalance( + spec: | + + def decrease_balance(state: BeaconState, index: ValidatorIndex, delta: Gwei) -> None: + """ + Decrease the validator balance at index ``index`` by ``delta``, with underflow protection. + """ + state.balances[index] = 0 if delta > state.balances[index] else state.balances[index] - delta + + +- name: eth_aggregate_pubkeys#altair + sources: [] + spec: | + + def eth_aggregate_pubkeys(pubkeys: Sequence[BLSPubkey]) -> BLSPubkey: + """ + Return the aggregate public key for the public keys in ``pubkeys``. + + Note: the ``+`` operation should be interpreted as elliptic curve point addition, which takes as input + elliptic curve points that must be decoded from the input ``BLSPubkey``s. + This implementation is for demonstrative purposes only and ignores encoding/decoding concerns. + Refer to the BLS signature draft standard for more information. + """ + assert len(pubkeys) > 0 + # Ensure that the given inputs are valid pubkeys + assert all(bls.KeyValidate(pubkey) for pubkey in pubkeys) + + result = copy(pubkeys[0]) + for pubkey in pubkeys[1:]: + result += pubkey + return result + + +- name: eth_fast_aggregate_verify#altair + sources: [] + spec: | + + def eth_fast_aggregate_verify( + pubkeys: Sequence[BLSPubkey], message: Bytes32, signature: BLSSignature + ) -> bool: + """ + Wrapper to ``bls.FastAggregateVerify`` accepting the ``G2_POINT_AT_INFINITY`` signature when ``pubkeys`` is empty. + """ + if len(pubkeys) == 0 and signature == G2_POINT_AT_INFINITY: + return True + return bls.FastAggregateVerify(pubkeys, message, signature) + + +- name: filter_block_tree#phase0 + sources: + - file: packages/fork-choice/src/protoArray/protoArray.ts + search: '^\s+nodeIsViableForHead\(node:' + regex: true + spec: | + + def filter_block_tree(store: Store, block_root: Root, blocks: Dict[Root, BeaconBlock]) -> bool: + block = store.blocks[block_root] + children = [ + root for root in store.blocks.keys() if store.blocks[root].parent_root == block_root + ] + + # If any children branches contain expected finalized/justified checkpoints, + # add to filtered block-tree and signal viability to parent. + if any(children): + filter_block_tree_result = [filter_block_tree(store, child, blocks) for child in children] + if any(filter_block_tree_result): + blocks[block_root] = block + return True + return False + + current_epoch = get_current_store_epoch(store) + voting_source = get_voting_source(store, block_root) + + # The voting source should be either at the same height as the store's justified checkpoint or + # not more than two epochs ago + correct_justified = ( + store.justified_checkpoint.epoch == GENESIS_EPOCH + or voting_source.epoch == store.justified_checkpoint.epoch + or voting_source.epoch + 2 >= current_epoch + ) + + finalized_checkpoint_block = get_checkpoint_block( + store, + block_root, + store.finalized_checkpoint.epoch, + ) + + correct_finalized = ( + store.finalized_checkpoint.epoch == GENESIS_EPOCH + or store.finalized_checkpoint.root == finalized_checkpoint_block + ) + + # If expected finalized/justified, add to viable block-tree and signal viability to parent. + if correct_justified and correct_finalized: + blocks[block_root] = block + return True + + # Otherwise, branch not viable + return False + + +- name: finalized_root_gindex_at_slot#altair + sources: [] + spec: | + + def finalized_root_gindex_at_slot(_slot: Slot) -> GeneralizedIndex: + return FINALIZED_ROOT_GINDEX + + +- name: finalized_root_gindex_at_slot#electra + sources: [] + spec: | + + def finalized_root_gindex_at_slot(slot: Slot) -> GeneralizedIndex: + epoch = compute_epoch_at_slot(slot) + + # [Modified in Electra] + if epoch >= ELECTRA_FORK_EPOCH: + return FINALIZED_ROOT_GINDEX_ELECTRA + return FINALIZED_ROOT_GINDEX + + +- name: get_activation_exit_churn_limit#electra + sources: + - file: packages/state-transition/src/util/validator.ts + search: export function getActivationExitChurnLimit( + spec: | + + def get_activation_exit_churn_limit(state: BeaconState) -> Gwei: + """ + Return the churn limit for the current epoch dedicated to activations and exits. + """ + return min(MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT, get_balance_churn_limit(state)) + + +- name: get_active_validator_indices#phase0 + sources: + - file: packages/state-transition/src/util/validator.ts + search: export function getActiveValidatorIndices( + spec: | + + def get_active_validator_indices(state: BeaconState, epoch: Epoch) -> Sequence[ValidatorIndex]: + """ + Return the sequence of active validator indices at ``epoch``. + """ + return [ + ValidatorIndex(i) for i, v in enumerate(state.validators) if is_active_validator(v, epoch) + ] + + +- name: get_aggregate_and_proof#phase0 + sources: + - file: packages/validator/src/services/validatorStore.ts + search: async signAggregateAndProof( + spec: | + + def get_aggregate_and_proof( + state: BeaconState, aggregator_index: ValidatorIndex, aggregate: Attestation, privkey: int + ) -> AggregateAndProof: + return AggregateAndProof( + aggregator_index=aggregator_index, + aggregate=aggregate, + selection_proof=get_slot_signature(state, aggregate.data.slot, privkey), + ) + + +- name: get_aggregate_and_proof_signature#phase0 + sources: + - file: packages/validator/src/services/validatorStore.ts + search: async signAggregateAndProof( + spec: | + + def get_aggregate_and_proof_signature( + state: BeaconState, aggregate_and_proof: AggregateAndProof, privkey: int + ) -> BLSSignature: + aggregate = aggregate_and_proof.aggregate + domain = get_domain( + state, DOMAIN_AGGREGATE_AND_PROOF, compute_epoch_at_slot(aggregate.data.slot) + ) + signing_root = compute_signing_root(aggregate_and_proof, domain) + return bls.Sign(privkey, signing_root) + + +- name: get_aggregate_due_ms#phase0 + sources: + - file: packages/config/src/forkConfig/index.ts + search: "getAggregateDueMs(fork: ForkName): number {" + spec: | + + def get_aggregate_due_ms(epoch: Epoch) -> uint64: + return get_slot_component_duration_ms(AGGREGATE_DUE_BPS) + + +- name: get_aggregate_due_ms#gloas + sources: + - file: packages/config/src/forkConfig/index.ts + search: "getAggregateDueMs(fork: ForkName): number {" + spec: | + + def get_aggregate_due_ms(epoch: Epoch) -> uint64: + # [New in Gloas] + if epoch >= GLOAS_FORK_EPOCH: + return get_slot_component_duration_ms(AGGREGATE_DUE_BPS_GLOAS) + return get_slot_component_duration_ms(AGGREGATE_DUE_BPS) + + +- name: get_aggregate_signature#phase0 + sources: [] + spec: | + + def get_aggregate_signature(attestations: Sequence[Attestation]) -> BLSSignature: + signatures = [attestation.signature for attestation in attestations] + return bls.Aggregate(signatures) + + +- name: get_ancestor#phase0 + sources: + - file: packages/fork-choice/src/protoArray/protoArray.ts + search: '^\s+getAncestor\(' + regex: true + spec: | + + def get_ancestor(store: Store, root: Root, slot: Slot) -> Root: + block = store.blocks[root] + if block.slot > slot: + return get_ancestor(store, block.parent_root, slot) + return root + + +- name: get_ancestor#gloas + sources: [] + spec: | + + def get_ancestor(store: Store, root: Root, slot: Slot) -> ForkChoiceNode: + """ + Returns the beacon block root and the payload status of the ancestor of the beacon block + with ``root`` at ``slot``. If the beacon block with ``root`` is already at ``slot`` or we are + requesting an ancestor "in the future", it returns ``PAYLOAD_STATUS_PENDING``. + """ + block = store.blocks[root] + if block.slot <= slot: + return ForkChoiceNode(root=root, payload_status=PAYLOAD_STATUS_PENDING) + + parent = store.blocks[block.parent_root] + while parent.slot > slot: + block = parent + parent = store.blocks[block.parent_root] + + return ForkChoiceNode( + root=block.parent_root, + payload_status=get_parent_payload_status(store, block), + ) + + +- name: get_attestation_component_deltas#phase0 + sources: [] + spec: | + + def get_attestation_component_deltas( + state: BeaconState, attestations: Sequence[PendingAttestation] + ) -> Tuple[Sequence[Gwei], Sequence[Gwei]]: + """ + Helper with shared logic for use by get source, target, and head deltas functions + """ + rewards = [Gwei(0)] * len(state.validators) + penalties = [Gwei(0)] * len(state.validators) + total_balance = get_total_active_balance(state) + unslashed_attesting_indices = get_unslashed_attesting_indices(state, attestations) + attesting_balance = get_total_balance(state, unslashed_attesting_indices) + for index in get_eligible_validator_indices(state): + if index in unslashed_attesting_indices: + increment = EFFECTIVE_BALANCE_INCREMENT # Factored out from balance totals to avoid uint64 overflow + if is_in_inactivity_leak(state): + # Since full base reward will be canceled out by inactivity penalty deltas, + # optimal participation receives full base reward compensation here. + rewards[index] += get_base_reward(state, index) + else: + reward_numerator = get_base_reward(state, index) * (attesting_balance // increment) + rewards[index] += reward_numerator // (total_balance // increment) + else: + penalties[index] += get_base_reward(state, index) + return rewards, penalties + + +- name: get_attestation_deltas#phase0 + sources: + - file: packages/state-transition/src/epoch/getAttestationDeltas.ts + search: export function getAttestationDeltas( + spec: | + + def get_attestation_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequence[Gwei]]: + """ + Return attestation reward/penalty deltas for each validator. + """ + source_rewards, source_penalties = get_source_deltas(state) + target_rewards, target_penalties = get_target_deltas(state) + head_rewards, head_penalties = get_head_deltas(state) + inclusion_delay_rewards, _ = get_inclusion_delay_deltas(state) + _, inactivity_penalties = get_inactivity_penalty_deltas(state) + + rewards = [ + source_rewards[i] + target_rewards[i] + head_rewards[i] + inclusion_delay_rewards[i] + for i in range(len(state.validators)) + ] + + penalties = [ + source_penalties[i] + target_penalties[i] + head_penalties[i] + inactivity_penalties[i] + for i in range(len(state.validators)) + ] + + return rewards, penalties + + +- name: get_attestation_due_ms#phase0 + sources: + - file: packages/config/src/forkConfig/index.ts + search: "getAttestationDueMs(fork: ForkName): number {" + spec: | + + def get_attestation_due_ms(epoch: Epoch) -> uint64: + return get_slot_component_duration_ms(ATTESTATION_DUE_BPS) + + +- name: get_attestation_due_ms#gloas + sources: + - file: packages/config/src/forkConfig/index.ts + search: "getAttestationDueMs(fork: ForkName): number {" + spec: | + + def get_attestation_due_ms(epoch: Epoch) -> uint64: + # [New in Gloas] + if epoch >= GLOAS_FORK_EPOCH: + return get_slot_component_duration_ms(ATTESTATION_DUE_BPS_GLOAS) + return get_slot_component_duration_ms(ATTESTATION_DUE_BPS) + + +- name: get_attestation_participation_flag_indices#altair + sources: + - file: packages/state-transition/src/block/processAttestationsAltair.ts + search: export function getAttestationParticipationStatus( + spec: | + + def get_attestation_participation_flag_indices( + state: BeaconState, data: AttestationData, inclusion_delay: uint64 + ) -> Sequence[int]: + """ + Return the flag indices that are satisfied by an attestation. + """ + # Matching source + if data.target.epoch == get_current_epoch(state): + justified_checkpoint = state.current_justified_checkpoint + else: + justified_checkpoint = state.previous_justified_checkpoint + is_matching_source = data.source == justified_checkpoint + + # Matching target + target_root = get_block_root(state, data.target.epoch) + target_root_matches = data.target.root == target_root + is_matching_target = is_matching_source and target_root_matches + + # Matching head + head_root = get_block_root_at_slot(state, data.slot) + head_root_matches = data.beacon_block_root == head_root + is_matching_head = is_matching_target and head_root_matches + + assert is_matching_source + + participation_flag_indices = [] + if is_matching_source and inclusion_delay <= integer_squareroot(SLOTS_PER_EPOCH): + participation_flag_indices.append(TIMELY_SOURCE_FLAG_INDEX) + if is_matching_target and inclusion_delay <= SLOTS_PER_EPOCH: + participation_flag_indices.append(TIMELY_TARGET_FLAG_INDEX) + if is_matching_head and inclusion_delay == MIN_ATTESTATION_INCLUSION_DELAY: + participation_flag_indices.append(TIMELY_HEAD_FLAG_INDEX) + + return participation_flag_indices + + +- name: get_attestation_participation_flag_indices#deneb + sources: + - file: packages/state-transition/src/block/processAttestationsAltair.ts + search: export function getAttestationParticipationStatus( + spec: | + + def get_attestation_participation_flag_indices( + state: BeaconState, data: AttestationData, inclusion_delay: uint64 + ) -> Sequence[int]: + """ + Return the flag indices that are satisfied by an attestation. + """ + # Matching source + if data.target.epoch == get_current_epoch(state): + justified_checkpoint = state.current_justified_checkpoint + else: + justified_checkpoint = state.previous_justified_checkpoint + is_matching_source = data.source == justified_checkpoint + + # Matching target + target_root = get_block_root(state, data.target.epoch) + target_root_matches = data.target.root == target_root + is_matching_target = is_matching_source and target_root_matches + + # Matching head + head_root = get_block_root_at_slot(state, data.slot) + head_root_matches = data.beacon_block_root == head_root + is_matching_head = is_matching_target and head_root_matches + + assert is_matching_source + + participation_flag_indices = [] + if is_matching_source and inclusion_delay <= integer_squareroot(SLOTS_PER_EPOCH): + participation_flag_indices.append(TIMELY_SOURCE_FLAG_INDEX) + # [Modified in Deneb:EIP7045] + if is_matching_target: + participation_flag_indices.append(TIMELY_TARGET_FLAG_INDEX) + if is_matching_head and inclusion_delay == MIN_ATTESTATION_INCLUSION_DELAY: + participation_flag_indices.append(TIMELY_HEAD_FLAG_INDEX) + + return participation_flag_indices + + +- name: get_attestation_participation_flag_indices#gloas + sources: [] + spec: | + + def get_attestation_participation_flag_indices( + state: BeaconState, data: AttestationData, inclusion_delay: uint64 + ) -> Sequence[int]: + """ + Return the flag indices that are satisfied by an attestation. + """ + # Matching source + if data.target.epoch == get_current_epoch(state): + justified_checkpoint = state.current_justified_checkpoint + else: + justified_checkpoint = state.previous_justified_checkpoint + is_matching_source = data.source == justified_checkpoint + + # Matching target + target_root = get_block_root(state, data.target.epoch) + target_root_matches = data.target.root == target_root + is_matching_target = is_matching_source and target_root_matches + + # [New in Gloas:EIP7732] + if is_attestation_same_slot(state, data): + assert data.index == 0 + payload_matches = True + else: + slot_index = data.slot % SLOTS_PER_HISTORICAL_ROOT + payload_index = state.execution_payload_availability[slot_index] + payload_matches = data.index == payload_index + + # Matching head + head_root = get_block_root_at_slot(state, data.slot) + head_root_matches = data.beacon_block_root == head_root + # [Modified in Gloas:EIP7732] + is_matching_head = is_matching_target and head_root_matches and payload_matches + + assert is_matching_source + + participation_flag_indices = [] + if is_matching_source and inclusion_delay <= integer_squareroot(SLOTS_PER_EPOCH): + participation_flag_indices.append(TIMELY_SOURCE_FLAG_INDEX) + if is_matching_target: + participation_flag_indices.append(TIMELY_TARGET_FLAG_INDEX) + if is_matching_head and inclusion_delay == MIN_ATTESTATION_INCLUSION_DELAY: + participation_flag_indices.append(TIMELY_HEAD_FLAG_INDEX) + + return participation_flag_indices + + +- name: get_attestation_score#phase0 + sources: [] + spec: | + + def get_attestation_score(store: Store, root: Root, state: BeaconState) -> Gwei: + unslashed_and_active_indices = [ + i + for i in get_active_validator_indices(state, get_current_epoch(state)) + if not state.validators[i].slashed + ] + return Gwei( + sum( + state.validators[i].effective_balance + for i in unslashed_and_active_indices + if ( + i in store.latest_messages + and i not in store.equivocating_indices + and get_ancestor(store, store.latest_messages[i].root, store.blocks[root].slot) + == root + ) + ) + ) + + +- name: get_attestation_score#gloas + sources: [] + spec: | + + def get_attestation_score( + store: Store, + # [Modified in Gloas:EIP7732] + # Removed `root` + # [New in Gloas:EIP7732] + node: ForkChoiceNode, + state: BeaconState, + ) -> Gwei: + unslashed_and_active_indices = [ + i + for i in get_active_validator_indices(state, get_current_epoch(state)) + if not state.validators[i].slashed + ] + return Gwei( + sum( + state.validators[i].effective_balance + for i in unslashed_and_active_indices + if ( + i in store.latest_messages + and i not in store.equivocating_indices + # [Modified in Gloas:EIP7732] + and is_supporting_vote(store, node, store.latest_messages[i]) + ) + ) + ) + + +- name: get_attestation_signature#phase0 + sources: + - file: packages/validator/src/services/validatorStore.ts + search: async signAttestation( + spec: | + + def get_attestation_signature( + state: BeaconState, attestation_data: AttestationData, privkey: int + ) -> BLSSignature: + domain = get_domain(state, DOMAIN_BEACON_ATTESTER, attestation_data.target.epoch) + signing_root = compute_signing_root(attestation_data, domain) + return bls.Sign(privkey, signing_root) + + +- name: get_attesting_balance#phase0 + sources: [] + spec: | + + def get_attesting_balance(state: BeaconState, attestations: Sequence[PendingAttestation]) -> Gwei: + """ + Return the combined effective balance of the set of unslashed validators participating in ``attestations``. + Note: ``get_total_balance`` returns ``EFFECTIVE_BALANCE_INCREMENT`` Gwei minimum to avoid divisions by zero. + """ + return get_total_balance(state, get_unslashed_attesting_indices(state, attestations)) + + +- name: get_attesting_indices#phase0 + sources: + - file: packages/state-transition/src/cache/epochCache.ts + search: "getAttestingIndices(fork: ForkSeq, attestation: Attestation)" + spec: | + + def get_attesting_indices(state: BeaconState, attestation: Attestation) -> Set[ValidatorIndex]: + """ + Return the set of attesting indices corresponding to ``data`` and ``bits``. + """ + committee = get_beacon_committee(state, attestation.data.slot, attestation.data.index) + return set(index for i, index in enumerate(committee) if attestation.aggregation_bits[i]) + + +- name: get_attesting_indices#electra + sources: + - file: packages/state-transition/src/cache/epochCache.ts + search: "getAttestingIndices(fork: ForkSeq, attestation: Attestation)" + spec: | + + def get_attesting_indices(state: BeaconState, attestation: Attestation) -> Set[ValidatorIndex]: + """ + Return the set of attesting indices corresponding to ``aggregation_bits`` and ``committee_bits``. + """ + output: Set[ValidatorIndex] = set() + committee_indices = get_committee_indices(attestation.committee_bits) + committee_offset = 0 + for committee_index in committee_indices: + committee = get_beacon_committee(state, attestation.data.slot, committee_index) + committee_attesters = set( + attester_index + for i, attester_index in enumerate(committee) + if attestation.aggregation_bits[committee_offset + i] + ) + output = output.union(committee_attesters) + + committee_offset += len(committee) + + return output + + +- name: get_balance_after_withdrawals#capella + sources: [] + spec: | + + def get_balance_after_withdrawals( + state: BeaconState, + validator_index: ValidatorIndex, + withdrawals: Sequence[Withdrawal], + ) -> Gwei: + withdrawn = sum( + withdrawal.amount + for withdrawal in withdrawals + if withdrawal.validator_index == validator_index + ) + return state.balances[validator_index] - withdrawn + + +- name: get_balance_churn_limit#electra + sources: + - file: packages/state-transition/src/util/validator.ts + search: export function getBalanceChurnLimit( + spec: | + + def get_balance_churn_limit(state: BeaconState) -> Gwei: + """ + Return the churn limit for the current epoch. + """ + churn = max( + MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA, get_total_active_balance(state) // CHURN_LIMIT_QUOTIENT + ) + return churn - churn % EFFECTIVE_BALANCE_INCREMENT + + +- name: get_base_reward#phase0 + sources: [] + spec: | + + def get_base_reward(state: BeaconState, index: ValidatorIndex) -> Gwei: + total_balance = get_total_active_balance(state) + effective_balance = state.validators[index].effective_balance + return Gwei( + effective_balance + * BASE_REWARD_FACTOR + // integer_squareroot(total_balance) + // BASE_REWARDS_PER_EPOCH + ) + + +- name: get_base_reward#altair + sources: [] + spec: | + + def get_base_reward(state: BeaconState, index: ValidatorIndex) -> Gwei: + """ + Return the base reward for the validator defined by ``index`` with respect to the current ``state``. + """ + increments = state.validators[index].effective_balance // EFFECTIVE_BALANCE_INCREMENT + return Gwei(increments * get_base_reward_per_increment(state)) + + +- name: get_base_reward_per_increment#altair + sources: + - file: packages/state-transition/src/util/syncCommittee.ts + search: export function computeBaseRewardPerIncrement( + spec: | + + def get_base_reward_per_increment(state: BeaconState) -> Gwei: + return Gwei( + EFFECTIVE_BALANCE_INCREMENT + * BASE_REWARD_FACTOR + // integer_squareroot(get_total_active_balance(state)) + ) + + +- name: get_beacon_committee#phase0 + sources: + - file: packages/state-transition/src/cache/epochCache.ts + search: "getBeaconCommittee(slot: Slot, index: CommitteeIndex)" + spec: | + + def get_beacon_committee( + state: BeaconState, slot: Slot, index: CommitteeIndex + ) -> Sequence[ValidatorIndex]: + """ + Return the beacon committee at ``slot`` for ``index``. + """ + epoch = compute_epoch_at_slot(slot) + committees_per_slot = get_committee_count_per_slot(state, epoch) + return compute_committee( + indices=get_active_validator_indices(state, epoch), + seed=get_seed(state, epoch, DOMAIN_BEACON_ATTESTER), + index=(slot % SLOTS_PER_EPOCH) * committees_per_slot + index, + count=committees_per_slot * SLOTS_PER_EPOCH, + ) + + +- name: get_beacon_proposer_index#phase0 + sources: + - file: packages/state-transition/src/cache/epochCache.ts + search: "getBeaconProposer(slot: Slot)" + spec: | + + def get_beacon_proposer_index(state: BeaconState) -> ValidatorIndex: + """ + Return the beacon proposer index at the current slot. + """ + epoch = get_current_epoch(state) + seed = hash(get_seed(state, epoch, DOMAIN_BEACON_PROPOSER) + uint_to_bytes(state.slot)) + indices = get_active_validator_indices(state, epoch) + return compute_proposer_index(state, indices, seed) + + +- name: get_beacon_proposer_index#fulu + sources: + - file: packages/state-transition/src/cache/epochCache.ts + search: "getBeaconProposer(slot: Slot)" + spec: | + + def get_beacon_proposer_index(state: BeaconState) -> ValidatorIndex: + """ + Return the beacon proposer index at the current slot. + """ + return state.proposer_lookahead[state.slot % SLOTS_PER_EPOCH] + + +- name: get_beacon_proposer_indices#fulu + sources: [] + spec: | + + def get_beacon_proposer_indices( + state: BeaconState, epoch: Epoch + ) -> Vector[ValidatorIndex, SLOTS_PER_EPOCH]: + """ + Return the proposer indices for the given ``epoch``. + """ + indices = get_active_validator_indices(state, epoch) + seed = get_seed(state, epoch, DOMAIN_BEACON_PROPOSER) + return compute_proposer_indices(state, epoch, seed, indices) + + +- name: get_blob_parameters#fulu + sources: + - file: packages/config/src/forkConfig/index.ts + search: "getBlobParameters(epoch: Epoch): BlobParameters {" + spec: | + + def get_blob_parameters(epoch: Epoch) -> BlobParameters: + """ + Return the blob parameters at a given epoch. + """ + for entry in sorted(BLOB_SCHEDULE, key=lambda e: e["EPOCH"], reverse=True): + if epoch >= entry["EPOCH"]: + return BlobParameters(entry["EPOCH"], entry["MAX_BLOBS_PER_BLOCK"]) + return BlobParameters(ELECTRA_FORK_EPOCH, MAX_BLOBS_PER_BLOCK_ELECTRA) + + +- name: get_blob_sidecars#deneb + sources: + - file: packages/beacon-node/src/util/blobs.ts + search: export function getBlobSidecars( + spec: | + + def get_blob_sidecars( + signed_block: SignedBeaconBlock, blobs: Sequence[Blob], blob_kzg_proofs: Sequence[KZGProof] + ) -> Sequence[BlobSidecar]: + block = signed_block.message + signed_block_header = compute_signed_block_header(signed_block) + return [ + BlobSidecar( + index=index, + blob=blob, + kzg_commitment=block.body.blob_kzg_commitments[index], + kzg_proof=blob_kzg_proofs[index], + signed_block_header=signed_block_header, + kzg_commitment_inclusion_proof=compute_merkle_proof( + block.body, + get_generalized_index(BeaconBlockBody, "blob_kzg_commitments", index), + ), + ) + for index, blob in enumerate(blobs) + ] + + +- name: get_block_root#phase0 + sources: + - file: packages/state-transition/src/util/blockRoot.ts + search: export function getBlockRoot( + spec: | + + def get_block_root(state: BeaconState, epoch: Epoch) -> Root: + """ + Return the block root at the start of a recent ``epoch``. + """ + return get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch)) + + +- name: get_block_root_at_slot#phase0 + sources: + - file: packages/state-transition/src/util/blockRoot.ts + search: export function getBlockRootAtSlot( + spec: | + + def get_block_root_at_slot(state: BeaconState, slot: Slot) -> Root: + """ + Return the block root at a recent ``slot``. + """ + assert slot < state.slot <= slot + SLOTS_PER_HISTORICAL_ROOT + return state.block_roots[slot % SLOTS_PER_HISTORICAL_ROOT] + + +- name: get_block_signature#phase0 + sources: + - file: packages/validator/src/services/validatorStore.ts + search: async signBlock( + spec: | + + def get_block_signature(state: BeaconState, block: BeaconBlock, privkey: int) -> BLSSignature: + domain = get_domain(state, DOMAIN_BEACON_PROPOSER, compute_epoch_at_slot(block.slot)) + signing_root = compute_signing_root(block, domain) + return bls.Sign(privkey, signing_root) + + +- name: get_builder_from_deposit#gloas + sources: [] + spec: | + + def get_builder_from_deposit( + state: BeaconState, pubkey: BLSPubkey, withdrawal_credentials: Bytes32, amount: uint64 + ) -> Builder: + return Builder( + pubkey=pubkey, + version=uint8(withdrawal_credentials[0]), + execution_address=ExecutionAddress(withdrawal_credentials[12:]), + balance=amount, + deposit_epoch=get_current_epoch(state), + withdrawable_epoch=FAR_FUTURE_EPOCH, + ) + + +- name: get_builder_payment_quorum_threshold#gloas + sources: + - file: packages/state-transition/src/util/gloas.ts + search: export function getBuilderPaymentQuorumThreshold( + spec: | + + def get_builder_payment_quorum_threshold(state: BeaconState) -> uint64: + """ + Calculate the quorum threshold for builder payments. + """ + per_slot_balance = get_total_active_balance(state) // SLOTS_PER_EPOCH + quorum = per_slot_balance * BUILDER_PAYMENT_THRESHOLD_NUMERATOR + return uint64(quorum // BUILDER_PAYMENT_THRESHOLD_DENOMINATOR) + + +- name: get_builder_withdrawals#gloas + sources: [] + spec: | + + def get_builder_withdrawals( + state: BeaconState, + withdrawal_index: WithdrawalIndex, + prior_withdrawals: Sequence[Withdrawal], + ) -> Tuple[Sequence[Withdrawal], WithdrawalIndex, uint64]: + withdrawals_limit = MAX_WITHDRAWALS_PER_PAYLOAD + + processed_count: uint64 = 0 + withdrawals: List[Withdrawal] = [] + for withdrawal in state.builder_pending_withdrawals: + all_withdrawals = prior_withdrawals + withdrawals + has_reached_limit = len(all_withdrawals) == withdrawals_limit + if has_reached_limit: + break + + builder_index = withdrawal.builder_index + withdrawals.append( + Withdrawal( + index=withdrawal_index, + validator_index=convert_builder_index_to_validator_index(builder_index), + address=withdrawal.fee_recipient, + amount=withdrawal.amount, + ) + ) + withdrawal_index += WithdrawalIndex(1) + processed_count += 1 + + return withdrawals, withdrawal_index, processed_count + + +- name: get_builders_sweep_withdrawals#gloas + sources: [] + spec: | + + def get_builders_sweep_withdrawals( + state: BeaconState, + withdrawal_index: WithdrawalIndex, + prior_withdrawals: Sequence[Withdrawal], + ) -> Tuple[Sequence[Withdrawal], WithdrawalIndex, uint64]: + epoch = get_current_epoch(state) + builders_limit = min(len(state.builders), MAX_BUILDERS_PER_WITHDRAWALS_SWEEP) + withdrawals_limit = MAX_WITHDRAWALS_PER_PAYLOAD + + processed_count: uint64 = 0 + withdrawals: List[Withdrawal] = [] + builder_index = state.next_withdrawal_builder_index + for _ in range(builders_limit): + all_withdrawals = prior_withdrawals + withdrawals + has_reached_limit = len(all_withdrawals) == withdrawals_limit + if has_reached_limit: + break + + builder = state.builders[builder_index] + if builder.withdrawable_epoch <= epoch and builder.balance > 0: + withdrawals.append( + Withdrawal( + index=withdrawal_index, + validator_index=convert_builder_index_to_validator_index(builder_index), + address=builder.execution_address, + amount=builder.balance, + ) + ) + withdrawal_index += WithdrawalIndex(1) + + builder_index = BuilderIndex((builder_index + 1) % len(state.builders)) + processed_count += 1 + + return withdrawals, withdrawal_index, processed_count + + +- name: get_checkpoint_block#phase0 + sources: [] + spec: | + + def get_checkpoint_block(store: Store, root: Root, epoch: Epoch) -> Root: + """ + Compute the checkpoint block for epoch ``epoch`` in the chain of block ``root`` + """ + epoch_first_slot = compute_start_slot_at_epoch(epoch) + return get_ancestor(store, root, epoch_first_slot) + + +- name: get_checkpoint_block#gloas + sources: [] + spec: | + + def get_checkpoint_block(store: Store, root: Root, epoch: Epoch) -> Root: + """ + Compute the checkpoint block for epoch ``epoch`` in the chain of block ``root`` + """ + epoch_first_slot = compute_start_slot_at_epoch(epoch) + return get_ancestor(store, root, epoch_first_slot).root + + +- name: get_committee_assignment#phase0 + sources: + - file: packages/state-transition/src/cache/epochCache.ts + search: getCommitteeAssignments( + spec: | + + def get_committee_assignment( + state: BeaconState, epoch: Epoch, validator_index: ValidatorIndex + ) -> Optional[Tuple[Sequence[ValidatorIndex], CommitteeIndex, Slot]]: + """ + Return the committee assignment in the ``epoch`` for ``validator_index``. + ``assignment`` returned is a tuple of the following form: + * ``assignment[0]`` is the list of validators in the committee + * ``assignment[1]`` is the index to which the committee is assigned + * ``assignment[2]`` is the slot at which the committee is assigned + Return None if no assignment. + """ + next_epoch = Epoch(get_current_epoch(state) + 1) + assert epoch <= next_epoch + + start_slot = compute_start_slot_at_epoch(epoch) + committee_count_per_slot = get_committee_count_per_slot(state, epoch) + for slot in range(start_slot, start_slot + SLOTS_PER_EPOCH): + for index in range(committee_count_per_slot): + committee = get_beacon_committee(state, Slot(slot), CommitteeIndex(index)) + if validator_index in committee: + return committee, CommitteeIndex(index), Slot(slot) + return None + + +- name: get_committee_count_per_slot#phase0 + sources: + - file: packages/state-transition/src/util/epochShuffling.ts + search: export function computeCommitteeCount( + spec: | + + def get_committee_count_per_slot(state: BeaconState, epoch: Epoch) -> uint64: + """ + Return the number of committees in each slot for the given ``epoch``. + """ + return max( + uint64(1), + min( + MAX_COMMITTEES_PER_SLOT, + uint64(len(get_active_validator_indices(state, epoch))) + // SLOTS_PER_EPOCH + // TARGET_COMMITTEE_SIZE, + ), + ) + + +- name: get_committee_indices#electra + sources: [] + spec: | + + def get_committee_indices(committee_bits: Bitvector) -> Sequence[CommitteeIndex]: + return [CommitteeIndex(index) for index, bit in enumerate(committee_bits) if bit] + + +- name: get_consolidation_churn_limit#electra + sources: + - file: packages/state-transition/src/util/validator.ts + search: export function getConsolidationChurnLimit( + spec: | + + def get_consolidation_churn_limit(state: BeaconState) -> Gwei: + return get_balance_churn_limit(state) - get_activation_exit_churn_limit(state) + + +- name: get_contribution_and_proof#altair + sources: + - file: packages/validator/src/services/validatorStore.ts + search: async signContributionAndProof( + spec: | + + def get_contribution_and_proof( + state: BeaconState, + aggregator_index: ValidatorIndex, + contribution: SyncCommitteeContribution, + privkey: int, + ) -> ContributionAndProof: + selection_proof = get_sync_committee_selection_proof( + state, + contribution.slot, + contribution.subcommittee_index, + privkey, + ) + return ContributionAndProof( + aggregator_index=aggregator_index, + contribution=contribution, + selection_proof=selection_proof, + ) + + +- name: get_contribution_and_proof_signature#altair + sources: + - file: packages/validator/src/services/validatorStore.ts + search: async signContributionAndProof( + spec: | + + def get_contribution_and_proof_signature( + state: BeaconState, contribution_and_proof: ContributionAndProof, privkey: int + ) -> BLSSignature: + contribution = contribution_and_proof.contribution + domain = get_domain( + state, DOMAIN_CONTRIBUTION_AND_PROOF, compute_epoch_at_slot(contribution.slot) + ) + signing_root = compute_signing_root(contribution_and_proof, domain) + return bls.Sign(privkey, signing_root) + + +- name: get_contribution_due_ms#altair + sources: + - file: packages/config/src/forkConfig/index.ts + search: "getSyncContributionDueMs(fork: ForkName): number {" + spec: | + + def get_contribution_due_ms(epoch: Epoch) -> uint64: + return get_slot_component_duration_ms(CONTRIBUTION_DUE_BPS) + + +- name: get_contribution_due_ms#gloas + sources: + - file: packages/config/src/forkConfig/index.ts + search: "getSyncContributionDueMs(fork: ForkName): number {" + spec: | + + def get_contribution_due_ms(epoch: Epoch) -> uint64: + # [New in Gloas] + if epoch >= GLOAS_FORK_EPOCH: + return get_slot_component_duration_ms(CONTRIBUTION_DUE_BPS_GLOAS) + return get_slot_component_duration_ms(CONTRIBUTION_DUE_BPS) + + +- name: get_current_epoch#phase0 + sources: + - file: packages/state-transition/src/util/epoch.ts + search: export function getCurrentEpoch( + spec: | + + def get_current_epoch(state: BeaconState) -> Epoch: + """ + Return the current epoch. + """ + return compute_epoch_at_slot(state.slot) + + +- name: get_current_slot#phase0 + sources: + - file: packages/state-transition/src/util/slot.ts + search: export function getCurrentSlot( + spec: | + + def get_current_slot(store: Store) -> Slot: + return Slot(GENESIS_SLOT + get_slots_since_genesis(store)) + + +- name: get_current_store_epoch#phase0 + sources: [] + spec: | + + def get_current_store_epoch(store: Store) -> Epoch: + return compute_epoch_at_slot(get_current_slot(store)) + + +- name: get_custody_groups#fulu + sources: + - file: packages/beacon-node/src/util/dataColumns.ts + search: export function getCustodyGroups( + spec: | + + def get_custody_groups(node_id: NodeID, custody_group_count: uint64) -> Sequence[CustodyIndex]: + assert custody_group_count <= NUMBER_OF_CUSTODY_GROUPS + + # Skip computation if all groups are custodied + if custody_group_count == NUMBER_OF_CUSTODY_GROUPS: + return [CustodyIndex(i) for i in range(NUMBER_OF_CUSTODY_GROUPS)] + + current_id = uint256(node_id) + custody_groups: List[CustodyIndex] = [] + while len(custody_groups) < custody_group_count: + custody_group = CustodyIndex( + bytes_to_uint64(hash(uint_to_bytes(current_id))[0:8]) % NUMBER_OF_CUSTODY_GROUPS + ) + if custody_group not in custody_groups: + custody_groups.append(custody_group) + if current_id == UINT256_MAX: + # Overflow prevention + current_id = uint256(0) + else: + current_id += 1 + + assert len(custody_groups) == len(set(custody_groups)) + return sorted(custody_groups) + + +- name: get_data_column_sidecars#fulu + sources: + - file: packages/beacon-node/src/util/dataColumns.ts + search: export function getDataColumnSidecars( + spec: | + + def get_data_column_sidecars( + signed_block_header: SignedBeaconBlockHeader, + kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK], + kzg_commitments_inclusion_proof: Vector[Bytes32, KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH], + cells_and_kzg_proofs: Sequence[ + Tuple[Vector[Cell, CELLS_PER_EXT_BLOB], Vector[KZGProof, CELLS_PER_EXT_BLOB]] + ], + ) -> Sequence[DataColumnSidecar]: + """ + Given a signed block header and the commitments, inclusion proof, cells/proofs associated with + each blob in the block, assemble the sidecars which can be distributed to peers. + """ + assert len(cells_and_kzg_proofs) == len(kzg_commitments) + + sidecars = [] + for column_index in range(NUMBER_OF_COLUMNS): + column_cells, column_proofs = [], [] + for cells, proofs in cells_and_kzg_proofs: + column_cells.append(cells[column_index]) + column_proofs.append(proofs[column_index]) + sidecars.append( + DataColumnSidecar( + index=column_index, + column=column_cells, + kzg_commitments=kzg_commitments, + kzg_proofs=column_proofs, + signed_block_header=signed_block_header, + kzg_commitments_inclusion_proof=kzg_commitments_inclusion_proof, + ) + ) + return sidecars + + +- name: get_data_column_sidecars#gloas + sources: [] + spec: | + + def get_data_column_sidecars( + # [Modified in Gloas:EIP7732] + # Removed `signed_block_header` + # [New in Gloas:EIP7732] + beacon_block_root: Root, + # [New in Gloas:EIP7732] + slot: Slot, + kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK], + # [Modified in Gloas:EIP7732] + # Removed `kzg_commitments_inclusion_proof` + cells_and_kzg_proofs: Sequence[ + Tuple[Vector[Cell, CELLS_PER_EXT_BLOB], Vector[KZGProof, CELLS_PER_EXT_BLOB]] + ], + ) -> Sequence[DataColumnSidecar]: + """ + Given a beacon block root and the commitments, cells/proofs associated with + each blob in the block, assemble the sidecars which can be distributed to peers. + """ + assert len(cells_and_kzg_proofs) == len(kzg_commitments) + + sidecars = [] + for column_index in range(NUMBER_OF_COLUMNS): + column_cells, column_proofs = [], [] + for cells, proofs in cells_and_kzg_proofs: + column_cells.append(cells[column_index]) + column_proofs.append(proofs[column_index]) + sidecars.append( + DataColumnSidecar( + index=column_index, + column=column_cells, + kzg_commitments=kzg_commitments, + kzg_proofs=column_proofs, + slot=slot, + beacon_block_root=beacon_block_root, + ) + ) + return sidecars + + +- name: get_data_column_sidecars_from_block#fulu + sources: + - file: packages/beacon-node/src/util/dataColumns.ts + search: export function getDataColumnSidecarsFromBlock( + spec: | + + def get_data_column_sidecars_from_block( + signed_block: SignedBeaconBlock, + cells_and_kzg_proofs: Sequence[ + Tuple[Vector[Cell, CELLS_PER_EXT_BLOB], Vector[KZGProof, CELLS_PER_EXT_BLOB]] + ], + ) -> Sequence[DataColumnSidecar]: + """ + Given a signed block and the cells/proofs associated with each blob in the + block, assemble the sidecars which can be distributed to peers. + """ + blob_kzg_commitments = signed_block.message.body.blob_kzg_commitments + signed_block_header = compute_signed_block_header(signed_block) + kzg_commitments_inclusion_proof = compute_merkle_proof( + signed_block.message.body, + get_generalized_index(BeaconBlockBody, "blob_kzg_commitments"), + ) + return get_data_column_sidecars( + signed_block_header, + blob_kzg_commitments, + kzg_commitments_inclusion_proof, + cells_and_kzg_proofs, + ) + + +- name: get_data_column_sidecars_from_block#gloas + sources: [] + spec: | + + def get_data_column_sidecars_from_block( + signed_block: SignedBeaconBlock, + # [New in Gloas:EIP7732] + blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK], + cells_and_kzg_proofs: Sequence[ + Tuple[Vector[Cell, CELLS_PER_EXT_BLOB], Vector[KZGProof, CELLS_PER_EXT_BLOB]] + ], + ) -> Sequence[DataColumnSidecar]: + """ + Given a signed block and the cells/proofs associated with each blob in the + block, assemble the sidecars which can be distributed to peers. + """ + beacon_block_root = hash_tree_root(signed_block.message) + return get_data_column_sidecars( + beacon_block_root, + signed_block.message.slot, + blob_kzg_commitments, + cells_and_kzg_proofs, + ) + + +- name: get_data_column_sidecars_from_column_sidecar#fulu + sources: + - file: packages/beacon-node/src/util/dataColumns.ts + search: export function getDataColumnSidecarsFromColumnSidecar( + spec: | + + def get_data_column_sidecars_from_column_sidecar( + sidecar: DataColumnSidecar, + cells_and_kzg_proofs: Sequence[ + Tuple[Vector[Cell, CELLS_PER_EXT_BLOB], Vector[KZGProof, CELLS_PER_EXT_BLOB]] + ], + ) -> Sequence[DataColumnSidecar]: + """ + Given a DataColumnSidecar and the cells/proofs associated with each blob corresponding + to the commitments it contains, assemble all sidecars for distribution to peers. + """ + assert len(cells_and_kzg_proofs) == len(sidecar.kzg_commitments) + + return get_data_column_sidecars( + sidecar.signed_block_header, + sidecar.kzg_commitments, + sidecar.kzg_commitments_inclusion_proof, + cells_and_kzg_proofs, + ) + + +- name: get_data_column_sidecars_from_column_sidecar#gloas + sources: [] + spec: | + + def get_data_column_sidecars_from_column_sidecar( + sidecar: DataColumnSidecar, + cells_and_kzg_proofs: Sequence[ + Tuple[Vector[Cell, CELLS_PER_EXT_BLOB], Vector[KZGProof, CELLS_PER_EXT_BLOB]] + ], + ) -> Sequence[DataColumnSidecar]: + """ + Given a DataColumnSidecar and the cells/proofs associated with each blob corresponding + to the commitments it contains, assemble all sidecars for distribution to peers. + """ + assert len(cells_and_kzg_proofs) == len(sidecar.kzg_commitments) + + return get_data_column_sidecars( + sidecar.beacon_block_root, + sidecar.slot, + sidecar.kzg_commitments, + cells_and_kzg_proofs, + ) + + +- name: get_domain#phase0 + sources: + - file: packages/config/src/genesisConfig/index.ts + search: "getDomain(domainSlot: Slot, domainType: DomainType, messageSlot?: Slot)" + spec: | + + def get_domain(state: BeaconState, domain_type: DomainType, epoch: Epoch = None) -> Domain: + """ + Return the signature domain (fork version concatenated with domain type) of a message. + """ + epoch = get_current_epoch(state) if epoch is None else epoch + fork_version = ( + state.fork.previous_version if epoch < state.fork.epoch else state.fork.current_version + ) + return compute_domain(domain_type, fork_version, state.genesis_validators_root) + + +- name: get_eligible_validator_indices#phase0 + sources: [] + spec: | + + def get_eligible_validator_indices(state: BeaconState) -> Sequence[ValidatorIndex]: + previous_epoch = get_previous_epoch(state) + return [ + ValidatorIndex(index) + for index, v in enumerate(state.validators) + if is_active_validator(v, previous_epoch) + or (v.slashed and previous_epoch + 1 < v.withdrawable_epoch) + ] + + +- name: get_epoch_signature#phase0 + sources: + - file: packages/validator/src/services/validatorStore.ts + search: async signRandao( + spec: | + + def get_epoch_signature(state: BeaconState, block: BeaconBlock, privkey: int) -> BLSSignature: + domain = get_domain(state, DOMAIN_RANDAO, compute_epoch_at_slot(block.slot)) + signing_root = compute_signing_root(compute_epoch_at_slot(block.slot), domain) + return bls.Sign(privkey, signing_root) + + +- name: get_eth1_pending_deposit_count#electra + sources: [] + spec: | + + def get_eth1_pending_deposit_count(state: BeaconState) -> uint64: + eth1_deposit_index_limit = min( + state.eth1_data.deposit_count, state.deposit_requests_start_index + ) + if state.eth1_deposit_index < eth1_deposit_index_limit: + return min(MAX_DEPOSITS, eth1_deposit_index_limit - state.eth1_deposit_index) + else: + return uint64(0) + + +- name: get_eth1_vote#phase0 + sources: [] + spec: | + + def get_eth1_vote(state: BeaconState, eth1_chain: Sequence[Eth1Block]) -> Eth1Data: + period_start = voting_period_start_time(state) + # `eth1_chain` abstractly represents all blocks in the eth1 chain sorted by ascending block height + votes_to_consider = [ + get_eth1_data(block) + for block in eth1_chain + if ( + is_candidate_block(block, period_start) + # Ensure cannot move back to earlier deposit contract states + and get_eth1_data(block).deposit_count >= state.eth1_data.deposit_count + ) + ] + + # Valid votes already cast during this period + valid_votes = [vote for vote in state.eth1_data_votes if vote in votes_to_consider] + + # Default vote on latest eth1 block data in the period range unless eth1 chain is not live + # Non-substantive casting for linter + state_eth1_data: Eth1Data = state.eth1_data + default_vote = ( + votes_to_consider[len(votes_to_consider) - 1] if any(votes_to_consider) else state_eth1_data + ) + + return max( + valid_votes, + # Tiebreak by smallest distance + key=lambda v: ( + valid_votes.count(v), + -valid_votes.index(v), + ), + default=default_vote, + ) + + +- name: get_eth1_vote#electra + sources: [] + spec: | + + def get_eth1_vote(state: BeaconState, eth1_chain: Sequence[Eth1Block]) -> Eth1Data: + # [New in Electra:EIP6110] + if state.eth1_deposit_index == state.deposit_requests_start_index: + return state.eth1_data + + period_start = voting_period_start_time(state) + # `eth1_chain` abstractly represents all blocks in the eth1 chain sorted by ascending block height + votes_to_consider = [ + get_eth1_data(block) + for block in eth1_chain + if ( + is_candidate_block(block, period_start) + # Ensure cannot move back to earlier deposit contract states + and get_eth1_data(block).deposit_count >= state.eth1_data.deposit_count + ) + ] + + # Valid votes already cast during this period + valid_votes = [vote for vote in state.eth1_data_votes if vote in votes_to_consider] + + # Default vote on latest eth1 block data in the period range unless eth1 chain is not live + # Non-substantive casting for linter + state_eth1_data: Eth1Data = state.eth1_data + default_vote = ( + votes_to_consider[len(votes_to_consider) - 1] if any(votes_to_consider) else state_eth1_data + ) + + return max( + valid_votes, + # Tiebreak by smallest distance + key=lambda v: ( + valid_votes.count(v), + -valid_votes.index(v), + ), + default=default_vote, + ) + + +- name: get_execution_payload#bellatrix + sources: [] + spec: | + + def get_execution_payload( + payload_id: Optional[PayloadId], execution_engine: ExecutionEngine + ) -> ExecutionPayload: + if payload_id is None: + # Pre-merge, empty payload + return ExecutionPayload() + else: + return execution_engine.get_payload(payload_id).execution_payload + + +- name: get_execution_payload_bid_signature#gloas + sources: [] + spec: | + + def get_execution_payload_bid_signature( + state: BeaconState, bid: ExecutionPayloadBid, privkey: int + ) -> BLSSignature: + domain = get_domain(state, DOMAIN_BEACON_BUILDER, compute_epoch_at_slot(bid.slot)) + signing_root = compute_signing_root(bid, domain) + return bls.Sign(privkey, signing_root) + + +- name: get_execution_payload_envelope_signature#gloas + sources: [] + spec: | + + def get_execution_payload_envelope_signature( + state: BeaconState, envelope: ExecutionPayloadEnvelope, privkey: int + ) -> BLSSignature: + domain = get_domain(state, DOMAIN_BEACON_BUILDER, compute_epoch_at_slot(state.slot)) + signing_root = compute_signing_root(envelope, domain) + return bls.Sign(privkey, signing_root) + + +- name: get_execution_requests#electra + sources: [] + spec: | + + def get_execution_requests(execution_requests_list: Sequence[bytes]) -> ExecutionRequests: + deposits = [] + withdrawals = [] + consolidations = [] + + request_types = [ + DEPOSIT_REQUEST_TYPE, + WITHDRAWAL_REQUEST_TYPE, + CONSOLIDATION_REQUEST_TYPE, + ] + + prev_request_type = None + for request in execution_requests_list: + request_type, request_data = request[0:1], request[1:] + + # Check that the request type is valid + assert request_type in request_types + # Check that the request data is not empty + assert len(request_data) != 0 + # Check that requests are in strictly ascending order + # Each successive type must be greater than the last with no duplicates + assert prev_request_type is None or prev_request_type < request_type + prev_request_type = request_type + + if request_type == DEPOSIT_REQUEST_TYPE: + deposits = ssz_deserialize( + List[DepositRequest, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD], request_data + ) + elif request_type == WITHDRAWAL_REQUEST_TYPE: + withdrawals = ssz_deserialize( + List[WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD], request_data + ) + elif request_type == CONSOLIDATION_REQUEST_TYPE: + consolidations = ssz_deserialize( + List[ConsolidationRequest, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD], request_data + ) + + return ExecutionRequests( + deposits=deposits, + withdrawals=withdrawals, + consolidations=consolidations, + ) + + +- name: get_execution_requests_list#electra + sources: + - file: packages/beacon-node/src/execution/engine/types.ts + search: export function serializeExecutionRequests( + spec: | + + def get_execution_requests_list(execution_requests: ExecutionRequests) -> Sequence[bytes]: + requests = [ + (DEPOSIT_REQUEST_TYPE, execution_requests.deposits), + (WITHDRAWAL_REQUEST_TYPE, execution_requests.withdrawals), + (CONSOLIDATION_REQUEST_TYPE, execution_requests.consolidations), + ] + + return [ + request_type + ssz_serialize(request_data) + for request_type, request_data in requests + if len(request_data) != 0 + ] + + +- name: get_expected_withdrawals#capella + sources: + - file: packages/state-transition/src/block/processWithdrawals.ts + search: export function getExpectedWithdrawals( + spec: | + + def get_expected_withdrawals(state: BeaconState) -> ExpectedWithdrawals: + withdrawal_index = state.next_withdrawal_index + withdrawals: List[Withdrawal] = [] + + # Get validators sweep withdrawals + validators_sweep_withdrawals, withdrawal_index, processed_validators_sweep_count = ( + get_validators_sweep_withdrawals(state, withdrawal_index, withdrawals) + ) + withdrawals.extend(validators_sweep_withdrawals) + + return ExpectedWithdrawals( + withdrawals, + processed_validators_sweep_count, + ) + + +- name: get_expected_withdrawals#electra + sources: + - file: packages/state-transition/src/block/processWithdrawals.ts + search: export function getExpectedWithdrawals( + spec: | + + def get_expected_withdrawals(state: BeaconState) -> ExpectedWithdrawals: + withdrawal_index = state.next_withdrawal_index + withdrawals: List[Withdrawal] = [] + + # [New in Electra:EIP7251] + # Get partial withdrawals + partial_withdrawals, withdrawal_index, processed_partial_withdrawals_count = ( + get_pending_partial_withdrawals(state, withdrawal_index, withdrawals) + ) + withdrawals.extend(partial_withdrawals) + + # Get validators sweep withdrawals + validators_sweep_withdrawals, withdrawal_index, processed_validators_sweep_count = ( + get_validators_sweep_withdrawals(state, withdrawal_index, withdrawals) + ) + withdrawals.extend(validators_sweep_withdrawals) + + return ExpectedWithdrawals( + withdrawals, + # [New in Electra:EIP7251] + processed_partial_withdrawals_count, + processed_validators_sweep_count, + ) + + +- name: get_expected_withdrawals#gloas + sources: + - file: packages/state-transition/src/block/processWithdrawals.ts + search: export function getExpectedWithdrawals( + spec: | + + def get_expected_withdrawals(state: BeaconState) -> ExpectedWithdrawals: + withdrawal_index = state.next_withdrawal_index + withdrawals: List[Withdrawal] = [] + + # [New in Gloas:EIP7732] + # Get builder withdrawals + builder_withdrawals, withdrawal_index, processed_builder_withdrawals_count = ( + get_builder_withdrawals(state, withdrawal_index, withdrawals) + ) + withdrawals.extend(builder_withdrawals) + + # Get partial withdrawals + partial_withdrawals, withdrawal_index, processed_partial_withdrawals_count = ( + get_pending_partial_withdrawals(state, withdrawal_index, withdrawals) + ) + withdrawals.extend(partial_withdrawals) + + # [New in Gloas:EIP7732] + # Get builders sweep withdrawals + builders_sweep_withdrawals, withdrawal_index, processed_builders_sweep_count = ( + get_builders_sweep_withdrawals(state, withdrawal_index, withdrawals) + ) + withdrawals.extend(builders_sweep_withdrawals) + + # Get validators sweep withdrawals + validators_sweep_withdrawals, withdrawal_index, processed_validators_sweep_count = ( + get_validators_sweep_withdrawals(state, withdrawal_index, withdrawals) + ) + withdrawals.extend(validators_sweep_withdrawals) + + return ExpectedWithdrawals( + withdrawals, + # [New in Gloas:EIP7732] + processed_builder_withdrawals_count, + processed_partial_withdrawals_count, + # [New in Gloas:EIP7732] + processed_builders_sweep_count, + processed_validators_sweep_count, + ) + + +- name: get_filtered_block_tree#phase0 + sources: [] + spec: | + + def get_filtered_block_tree(store: Store) -> Dict[Root, BeaconBlock]: + """ + Retrieve a filtered block tree from ``store``, only returning branches + whose leaf state's justified/finalized info agrees with that in ``store``. + """ + base = store.justified_checkpoint.root + blocks: Dict[Root, BeaconBlock] = {} + filter_block_tree(store, base, blocks) + return blocks + + +- name: get_finality_delay#phase0 + sources: + - file: packages/state-transition/src/util/finality.ts + search: export function getFinalityDelay( + spec: | + + def get_finality_delay(state: BeaconState) -> uint64: + return get_previous_epoch(state) - state.finalized_checkpoint.epoch + + +- name: get_flag_index_deltas#altair + sources: + - file: packages/state-transition/src/epoch/getRewardsAndPenalties.ts + search: // same logic to getFlagIndexDeltas + spec: | + + def get_flag_index_deltas( + state: BeaconState, flag_index: int + ) -> Tuple[Sequence[Gwei], Sequence[Gwei]]: + """ + Return the deltas for a given ``flag_index`` by scanning through the participation flags. + """ + rewards = [Gwei(0)] * len(state.validators) + penalties = [Gwei(0)] * len(state.validators) + previous_epoch = get_previous_epoch(state) + unslashed_participating_indices = get_unslashed_participating_indices( + state, flag_index, previous_epoch + ) + weight = PARTICIPATION_FLAG_WEIGHTS[flag_index] + unslashed_participating_balance = get_total_balance(state, unslashed_participating_indices) + unslashed_participating_increments = ( + unslashed_participating_balance // EFFECTIVE_BALANCE_INCREMENT + ) + active_increments = get_total_active_balance(state) // EFFECTIVE_BALANCE_INCREMENT + for index in get_eligible_validator_indices(state): + base_reward = get_base_reward(state, index) + if index in unslashed_participating_indices: + if not is_in_inactivity_leak(state): + reward_numerator = base_reward * weight * unslashed_participating_increments + rewards[index] += Gwei(reward_numerator // (active_increments * WEIGHT_DENOMINATOR)) + elif flag_index != TIMELY_HEAD_FLAG_INDEX: + penalties[index] += Gwei(base_reward * weight // WEIGHT_DENOMINATOR) + return rewards, penalties + + +- name: get_forkchoice_store#phase0 + sources: [] + spec: | + + def get_forkchoice_store(anchor_state: BeaconState, anchor_block: BeaconBlock) -> Store: + assert anchor_block.state_root == hash_tree_root(anchor_state) + anchor_root = hash_tree_root(anchor_block) + anchor_epoch = get_current_epoch(anchor_state) + justified_checkpoint = Checkpoint(epoch=anchor_epoch, root=anchor_root) + finalized_checkpoint = Checkpoint(epoch=anchor_epoch, root=anchor_root) + proposer_boost_root = Root() + return Store( + time=uint64(anchor_state.genesis_time + SECONDS_PER_SLOT * anchor_state.slot), + genesis_time=anchor_state.genesis_time, + justified_checkpoint=justified_checkpoint, + finalized_checkpoint=finalized_checkpoint, + unrealized_justified_checkpoint=justified_checkpoint, + unrealized_finalized_checkpoint=finalized_checkpoint, + proposer_boost_root=proposer_boost_root, + equivocating_indices=set(), + blocks={anchor_root: copy(anchor_block)}, + block_states={anchor_root: copy(anchor_state)}, + checkpoint_states={justified_checkpoint: copy(anchor_state)}, + unrealized_justifications={anchor_root: justified_checkpoint}, + ) + + +- name: get_forkchoice_store#gloas + sources: [] + spec: | + + def get_forkchoice_store(anchor_state: BeaconState, anchor_block: BeaconBlock) -> Store: + assert anchor_block.state_root == hash_tree_root(anchor_state) + anchor_root = hash_tree_root(anchor_block) + anchor_epoch = get_current_epoch(anchor_state) + justified_checkpoint = Checkpoint(epoch=anchor_epoch, root=anchor_root) + finalized_checkpoint = Checkpoint(epoch=anchor_epoch, root=anchor_root) + proposer_boost_root = Root() + return Store( + time=uint64(anchor_state.genesis_time + SECONDS_PER_SLOT * anchor_state.slot), + genesis_time=anchor_state.genesis_time, + justified_checkpoint=justified_checkpoint, + finalized_checkpoint=finalized_checkpoint, + unrealized_justified_checkpoint=justified_checkpoint, + unrealized_finalized_checkpoint=finalized_checkpoint, + proposer_boost_root=proposer_boost_root, + equivocating_indices=set(), + blocks={anchor_root: copy(anchor_block)}, + block_states={anchor_root: copy(anchor_state)}, + checkpoint_states={justified_checkpoint: copy(anchor_state)}, + unrealized_justifications={anchor_root: justified_checkpoint}, + # [New in Gloas:EIP7732] + execution_payload_states={anchor_root: copy(anchor_state)}, + ptc_vote={anchor_root: Vector[boolean, PTC_SIZE]()}, + block_timeliness={anchor_root: [True, True]}, + ) + + +- name: get_head#phase0 + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: '^\s+getHead\(\):' + regex: true + spec: | + + def get_head(store: Store) -> Root: + # Get filtered block tree that only includes viable branches + blocks = get_filtered_block_tree(store) + # Execute the LMD-GHOST fork choice + head = store.justified_checkpoint.root + while True: + children = [root for root in blocks.keys() if blocks[root].parent_root == head] + if len(children) == 0: + return head + # Sort by latest attesting balance with ties broken lexicographically + # Ties broken by favoring block with lexicographically higher root + head = max(children, key=lambda root: (get_weight(store, root), root)) + + +- name: get_head#gloas + sources: [] + spec: | + + def get_head(store: Store) -> ForkChoiceNode: + # Get filtered block tree that only includes viable branches + blocks = get_filtered_block_tree(store) + # Execute the LMD-GHOST fork-choice + head = ForkChoiceNode( + root=store.justified_checkpoint.root, + payload_status=PAYLOAD_STATUS_PENDING, + ) + + while True: + children = get_node_children(store, blocks, head) + if len(children) == 0: + return head + # Sort by latest attesting balance with ties broken lexicographically + head = max( + children, + key=lambda child: ( + get_weight(store, child), + child.root, + get_payload_status_tiebreaker(store, child), + ), + ) + + +- name: get_head_deltas#phase0 + sources: [] + spec: | + + def get_head_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequence[Gwei]]: + """ + Return attester micro-rewards/penalties for head-vote for each validator. + """ + matching_head_attestations = get_matching_head_attestations(state, get_previous_epoch(state)) + return get_attestation_component_deltas(state, matching_head_attestations) + + +- name: get_inactivity_penalty_deltas#phase0 + sources: [] + spec: | + + def get_inactivity_penalty_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequence[Gwei]]: + """ + Return inactivity reward/penalty deltas for each validator. + """ + penalties = [Gwei(0) for _ in range(len(state.validators))] + if is_in_inactivity_leak(state): + matching_target_attestations = get_matching_target_attestations( + state, get_previous_epoch(state) + ) + matching_target_attesting_indices = get_unslashed_attesting_indices( + state, matching_target_attestations + ) + for index in get_eligible_validator_indices(state): + # If validator is performing optimally this cancels all rewards for a neutral balance + base_reward = get_base_reward(state, index) + penalties[index] += Gwei( + BASE_REWARDS_PER_EPOCH * base_reward - get_proposer_reward(state, index) + ) + if index not in matching_target_attesting_indices: + effective_balance = state.validators[index].effective_balance + penalties[index] += Gwei( + effective_balance * get_finality_delay(state) // INACTIVITY_PENALTY_QUOTIENT + ) + + # No rewards associated with inactivity penalties + rewards = [Gwei(0) for _ in range(len(state.validators))] + return rewards, penalties + + +- name: get_inactivity_penalty_deltas#altair + sources: + - file: packages/state-transition/src/epoch/getRewardsAndPenalties.ts + search: // Same logic to getInactivityPenaltyDeltas + spec: | + + def get_inactivity_penalty_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequence[Gwei]]: + """ + Return the inactivity penalty deltas by considering timely target participation flags and inactivity scores. + """ + rewards = [Gwei(0) for _ in range(len(state.validators))] + penalties = [Gwei(0) for _ in range(len(state.validators))] + previous_epoch = get_previous_epoch(state) + matching_target_indices = get_unslashed_participating_indices( + state, TIMELY_TARGET_FLAG_INDEX, previous_epoch + ) + for index in get_eligible_validator_indices(state): + if index not in matching_target_indices: + penalty_numerator = ( + state.validators[index].effective_balance * state.inactivity_scores[index] + ) + penalty_denominator = INACTIVITY_SCORE_BIAS * INACTIVITY_PENALTY_QUOTIENT_ALTAIR + penalties[index] += Gwei(penalty_numerator // penalty_denominator) + return rewards, penalties + + +- name: get_inactivity_penalty_deltas#bellatrix + sources: + - file: packages/state-transition/src/epoch/getRewardsAndPenalties.ts + search: // Same logic to getInactivityPenaltyDeltas + spec: | + + def get_inactivity_penalty_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequence[Gwei]]: + """ + Return the inactivity penalty deltas by considering timely target participation flags and inactivity scores. + """ + rewards = [Gwei(0) for _ in range(len(state.validators))] + penalties = [Gwei(0) for _ in range(len(state.validators))] + previous_epoch = get_previous_epoch(state) + matching_target_indices = get_unslashed_participating_indices( + state, TIMELY_TARGET_FLAG_INDEX, previous_epoch + ) + for index in get_eligible_validator_indices(state): + if index not in matching_target_indices: + penalty_numerator = ( + state.validators[index].effective_balance * state.inactivity_scores[index] + ) + # [Modified in Bellatrix] + penalty_denominator = INACTIVITY_SCORE_BIAS * INACTIVITY_PENALTY_QUOTIENT_BELLATRIX + penalties[index] += Gwei(penalty_numerator // penalty_denominator) + return rewards, penalties + + +- name: get_inclusion_delay_deltas#phase0 + sources: [] + spec: | + + def get_inclusion_delay_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequence[Gwei]]: + """ + Return proposer and inclusion delay micro-rewards/penalties for each validator. + """ + rewards = [Gwei(0) for _ in range(len(state.validators))] + matching_source_attestations = get_matching_source_attestations( + state, get_previous_epoch(state) + ) + for index in get_unslashed_attesting_indices(state, matching_source_attestations): + attestation = min( + [a for a in matching_source_attestations if index in get_attesting_indices(state, a)], + key=lambda a: a.inclusion_delay, + ) + rewards[attestation.proposer_index] += get_proposer_reward(state, index) + max_attester_reward = Gwei( + get_base_reward(state, index) - get_proposer_reward(state, index) + ) + rewards[index] += Gwei(max_attester_reward // attestation.inclusion_delay) + + # No penalties associated with inclusion delay + penalties = [Gwei(0) for _ in range(len(state.validators))] + return rewards, penalties + + +- name: get_index_for_new_builder#gloas + sources: [] + spec: | + + def get_index_for_new_builder(state: BeaconState) -> BuilderIndex: + for index, builder in enumerate(state.builders): + if builder.withdrawable_epoch <= get_current_epoch(state) and builder.balance == 0: + return BuilderIndex(index) + return BuilderIndex(len(state.builders)) + + +- name: get_index_for_new_validator#altair + sources: + - file: packages/state-transition/src/block/processDeposit.ts + search: export function addValidatorToRegistry( + spec: | + + def get_index_for_new_validator(state: BeaconState) -> ValidatorIndex: + return ValidatorIndex(len(state.validators)) + + +- name: get_indexed_attestation#phase0 + sources: + - file: packages/state-transition/src/cache/epochCache.ts + search: "getIndexedAttestation(fork: ForkSeq, attestation: Attestation)" + spec: | + + def get_indexed_attestation(state: BeaconState, attestation: Attestation) -> IndexedAttestation: + """ + Return the indexed attestation corresponding to ``attestation``. + """ + attesting_indices = get_attesting_indices(state, attestation) + + return IndexedAttestation( + attesting_indices=sorted(attesting_indices), + data=attestation.data, + signature=attestation.signature, + ) + + +- name: get_indexed_payload_attestation#gloas + sources: + - file: packages/state-transition/src/cache/epochCache.ts + search: '^\s+getIndexedPayloadAttestation\(' + regex: true + spec: | + + def get_indexed_payload_attestation( + state: BeaconState, payload_attestation: PayloadAttestation + ) -> IndexedPayloadAttestation: + """ + Return the indexed payload attestation corresponding to ``payload_attestation``. + """ + slot = payload_attestation.data.slot + ptc = get_ptc(state, slot) + bits = payload_attestation.aggregation_bits + attesting_indices = [index for i, index in enumerate(ptc) if bits[i]] + + return IndexedPayloadAttestation( + attesting_indices=sorted(attesting_indices), + data=payload_attestation.data, + signature=payload_attestation.signature, + ) + + +- name: get_lc_execution_root#capella + sources: [] + spec: | + + def get_lc_execution_root(header: LightClientHeader) -> Root: + epoch = compute_epoch_at_slot(header.beacon.slot) + + if epoch >= CAPELLA_FORK_EPOCH: + return hash_tree_root(header.execution) + + return Root() + + +- name: get_lc_execution_root#deneb + sources: [] + spec: | + + def get_lc_execution_root(header: LightClientHeader) -> Root: + epoch = compute_epoch_at_slot(header.beacon.slot) + + # [New in Deneb] + if epoch >= DENEB_FORK_EPOCH: + return hash_tree_root(header.execution) + + # [Modified in Deneb] + if epoch >= CAPELLA_FORK_EPOCH: + execution_header = capella.ExecutionPayloadHeader( + parent_hash=header.execution.parent_hash, + fee_recipient=header.execution.fee_recipient, + state_root=header.execution.state_root, + receipts_root=header.execution.receipts_root, + logs_bloom=header.execution.logs_bloom, + prev_randao=header.execution.prev_randao, + block_number=header.execution.block_number, + gas_limit=header.execution.gas_limit, + gas_used=header.execution.gas_used, + timestamp=header.execution.timestamp, + extra_data=header.execution.extra_data, + base_fee_per_gas=header.execution.base_fee_per_gas, + block_hash=header.execution.block_hash, + transactions_root=header.execution.transactions_root, + withdrawals_root=header.execution.withdrawals_root, + ) + return hash_tree_root(execution_header) + + return Root() + + +- name: get_lc_execution_root#electra + sources: [] + spec: | + + def get_lc_execution_root(header: LightClientHeader) -> Root: + epoch = compute_epoch_at_slot(header.beacon.slot) + + # [New in Electra] + if epoch >= ELECTRA_FORK_EPOCH: + return hash_tree_root(header.execution) + + # [Modified in Electra] + if epoch >= DENEB_FORK_EPOCH: + execution_header = deneb.ExecutionPayloadHeader( + parent_hash=header.execution.parent_hash, + fee_recipient=header.execution.fee_recipient, + state_root=header.execution.state_root, + receipts_root=header.execution.receipts_root, + logs_bloom=header.execution.logs_bloom, + prev_randao=header.execution.prev_randao, + block_number=header.execution.block_number, + gas_limit=header.execution.gas_limit, + gas_used=header.execution.gas_used, + timestamp=header.execution.timestamp, + extra_data=header.execution.extra_data, + base_fee_per_gas=header.execution.base_fee_per_gas, + block_hash=header.execution.block_hash, + transactions_root=header.execution.transactions_root, + withdrawals_root=header.execution.withdrawals_root, + blob_gas_used=header.execution.blob_gas_used, + excess_blob_gas=header.execution.excess_blob_gas, + ) + return hash_tree_root(execution_header) + + if epoch >= CAPELLA_FORK_EPOCH: + execution_header = capella.ExecutionPayloadHeader( + parent_hash=header.execution.parent_hash, + fee_recipient=header.execution.fee_recipient, + state_root=header.execution.state_root, + receipts_root=header.execution.receipts_root, + logs_bloom=header.execution.logs_bloom, + prev_randao=header.execution.prev_randao, + block_number=header.execution.block_number, + gas_limit=header.execution.gas_limit, + gas_used=header.execution.gas_used, + timestamp=header.execution.timestamp, + extra_data=header.execution.extra_data, + base_fee_per_gas=header.execution.base_fee_per_gas, + block_hash=header.execution.block_hash, + transactions_root=header.execution.transactions_root, + withdrawals_root=header.execution.withdrawals_root, + ) + return hash_tree_root(execution_header) + + return Root() + + +- name: get_matching_head_attestations#phase0 + sources: [] + spec: | + + def get_matching_head_attestations( + state: BeaconState, epoch: Epoch + ) -> Sequence[PendingAttestation]: + return [ + a + for a in get_matching_target_attestations(state, epoch) + if a.data.beacon_block_root == get_block_root_at_slot(state, a.data.slot) + ] + + +- name: get_matching_source_attestations#phase0 + sources: [] + spec: | + + def get_matching_source_attestations( + state: BeaconState, epoch: Epoch + ) -> Sequence[PendingAttestation]: + assert epoch in (get_previous_epoch(state), get_current_epoch(state)) + return ( + state.current_epoch_attestations + if epoch == get_current_epoch(state) + else state.previous_epoch_attestations + ) + + +- name: get_matching_target_attestations#phase0 + sources: [] + spec: | + + def get_matching_target_attestations( + state: BeaconState, epoch: Epoch + ) -> Sequence[PendingAttestation]: + return [ + a + for a in get_matching_source_attestations(state, epoch) + if a.data.target.root == get_block_root(state, epoch) + ] + + +- name: get_max_effective_balance#electra + sources: + - file: packages/state-transition/src/util/validator.ts + search: export function getMaxEffectiveBalance( + spec: | + + def get_max_effective_balance(validator: Validator) -> Gwei: + """ + Get max effective balance for ``validator``. + """ + if has_compounding_withdrawal_credential(validator): + return MAX_EFFECTIVE_BALANCE_ELECTRA + else: + return MIN_ACTIVATION_BALANCE + + +- name: get_next_sync_committee#altair + sources: + - file: packages/state-transition/src/util/syncCommittee.ts + search: export function getNextSyncCommittee( + spec: | + + def get_next_sync_committee(state: BeaconState) -> SyncCommittee: + """ + Return the next sync committee, with possible pubkey duplicates. + """ + indices = get_next_sync_committee_indices(state) + pubkeys = [state.validators[index].pubkey for index in indices] + aggregate_pubkey = eth_aggregate_pubkeys(pubkeys) + return SyncCommittee(pubkeys=pubkeys, aggregate_pubkey=aggregate_pubkey) + + +- name: get_next_sync_committee_indices#altair + sources: + - file: packages/state-transition/src/util/seed.ts + search: export function getNextSyncCommitteeIndices( + spec: | + + def get_next_sync_committee_indices(state: BeaconState) -> Sequence[ValidatorIndex]: + """ + Return the sync committee indices, with possible duplicates, for the next sync committee. + """ + epoch = Epoch(get_current_epoch(state) + 1) + + MAX_RANDOM_BYTE = 2**8 - 1 + active_validator_indices = get_active_validator_indices(state, epoch) + active_validator_count = uint64(len(active_validator_indices)) + seed = get_seed(state, epoch, DOMAIN_SYNC_COMMITTEE) + i = 0 + sync_committee_indices: List[ValidatorIndex] = [] + while len(sync_committee_indices) < SYNC_COMMITTEE_SIZE: + shuffled_index = compute_shuffled_index( + uint64(i % active_validator_count), active_validator_count, seed + ) + candidate_index = active_validator_indices[shuffled_index] + random_byte = hash(seed + uint_to_bytes(uint64(i // 32)))[i % 32] + effective_balance = state.validators[candidate_index].effective_balance + if effective_balance * MAX_RANDOM_BYTE >= MAX_EFFECTIVE_BALANCE * random_byte: + sync_committee_indices.append(candidate_index) + i += 1 + return sync_committee_indices + + +- name: get_next_sync_committee_indices#electra + sources: + - file: packages/state-transition/src/util/seed.ts + search: export function getNextSyncCommitteeIndices( + spec: | + + def get_next_sync_committee_indices(state: BeaconState) -> Sequence[ValidatorIndex]: + """ + Return the sync committee indices, with possible duplicates, for the next sync committee. + """ + epoch = Epoch(get_current_epoch(state) + 1) + + # [Modified in Electra] + MAX_RANDOM_VALUE = 2**16 - 1 + active_validator_indices = get_active_validator_indices(state, epoch) + active_validator_count = uint64(len(active_validator_indices)) + seed = get_seed(state, epoch, DOMAIN_SYNC_COMMITTEE) + i = uint64(0) + sync_committee_indices: List[ValidatorIndex] = [] + while len(sync_committee_indices) < SYNC_COMMITTEE_SIZE: + shuffled_index = compute_shuffled_index( + uint64(i % active_validator_count), active_validator_count, seed + ) + candidate_index = active_validator_indices[shuffled_index] + # [Modified in Electra] + random_bytes = hash(seed + uint_to_bytes(i // 16)) + offset = i % 16 * 2 + random_value = bytes_to_uint64(random_bytes[offset : offset + 2]) + effective_balance = state.validators[candidate_index].effective_balance + # [Modified in Electra:EIP7251] + if effective_balance * MAX_RANDOM_VALUE >= MAX_EFFECTIVE_BALANCE_ELECTRA * random_value: + sync_committee_indices.append(candidate_index) + i += 1 + return sync_committee_indices + + +- name: get_next_sync_committee_indices#gloas + sources: [] + spec: | + + def get_next_sync_committee_indices(state: BeaconState) -> Sequence[ValidatorIndex]: + """ + Return the sync committee indices, with possible duplicates, for the next sync committee. + """ + epoch = Epoch(get_current_epoch(state) + 1) + seed = get_seed(state, epoch, DOMAIN_SYNC_COMMITTEE) + indices = get_active_validator_indices(state, epoch) + return compute_balance_weighted_selection( + state, indices, seed, size=SYNC_COMMITTEE_SIZE, shuffle_indices=True + ) + + +- name: get_node_children#gloas + sources: [] + spec: | + + def get_node_children( + store: Store, blocks: Dict[Root, BeaconBlock], node: ForkChoiceNode + ) -> Sequence[ForkChoiceNode]: + if node.payload_status == PAYLOAD_STATUS_PENDING: + children = [ForkChoiceNode(root=node.root, payload_status=PAYLOAD_STATUS_EMPTY)] + if node.root in store.execution_payload_states: + children.append(ForkChoiceNode(root=node.root, payload_status=PAYLOAD_STATUS_FULL)) + return children + else: + return [ + ForkChoiceNode(root=root, payload_status=PAYLOAD_STATUS_PENDING) + for root in blocks.keys() + if ( + blocks[root].parent_root == node.root + and node.payload_status == get_parent_payload_status(store, blocks[root]) + ) + ] + + +- name: get_parent_payload_status#gloas + sources: [] + spec: | + + def get_parent_payload_status(store: Store, block: BeaconBlock) -> PayloadStatus: + parent = store.blocks[block.parent_root] + parent_block_hash = block.body.signed_execution_payload_bid.message.parent_block_hash + message_block_hash = parent.body.signed_execution_payload_bid.message.block_hash + return PAYLOAD_STATUS_FULL if parent_block_hash == message_block_hash else PAYLOAD_STATUS_EMPTY + + +- name: get_payload_attestation_due_ms#gloas + sources: [] + spec: | + + def get_payload_attestation_due_ms(epoch: Epoch) -> uint64: + return get_slot_component_duration_ms(PAYLOAD_ATTESTATION_DUE_BPS) + + +- name: get_payload_attestation_message_signature#gloas + sources: [] + spec: | + + def get_payload_attestation_message_signature( + state: BeaconState, attestation: PayloadAttestationMessage, privkey: int + ) -> BLSSignature: + domain = get_domain(state, DOMAIN_PTC_ATTESTER, compute_epoch_at_slot(attestation.data.slot)) + signing_root = compute_signing_root(attestation.data, domain) + return bls.Sign(privkey, signing_root) + + +- name: get_payload_status_tiebreaker#gloas + sources: [] + spec: | + + def get_payload_status_tiebreaker(store: Store, node: ForkChoiceNode) -> uint8: + if node.payload_status == PAYLOAD_STATUS_PENDING or store.blocks[ + node.root + ].slot + 1 != get_current_slot(store): + return node.payload_status + else: + # To decide on a payload from the previous slot, choose + # between FULL and EMPTY based on `should_extend_payload` + if node.payload_status == PAYLOAD_STATUS_EMPTY: + return 1 + else: + return 2 if should_extend_payload(store, node.root) else 0 + + +- name: get_pending_balance_to_withdraw#electra + sources: + - file: packages/state-transition/src/util/validator.ts + search: export function getPendingBalanceToWithdraw( + spec: | + + def get_pending_balance_to_withdraw(state: BeaconState, validator_index: ValidatorIndex) -> Gwei: + return sum( + withdrawal.amount + for withdrawal in state.pending_partial_withdrawals + if withdrawal.validator_index == validator_index + ) + + +- name: get_pending_balance_to_withdraw#gloas + sources: [] + spec: | + + def get_pending_balance_to_withdraw(state: BeaconState, validator_index: ValidatorIndex) -> Gwei: + return sum( + withdrawal.amount + for withdrawal in state.pending_partial_withdrawals + if withdrawal.validator_index == validator_index + ) + + +- name: get_pending_balance_to_withdraw_for_builder#gloas + sources: + - file: packages/state-transition/src/util/gloas.ts + search: export function getPendingBalanceToWithdrawForBuilder( + spec: | + + def get_pending_balance_to_withdraw_for_builder( + state: BeaconState, builder_index: BuilderIndex + ) -> Gwei: + return sum( + withdrawal.amount + for withdrawal in state.builder_pending_withdrawals + if withdrawal.builder_index == builder_index + ) + sum( + payment.withdrawal.amount + for payment in state.builder_pending_payments + if payment.withdrawal.builder_index == builder_index + ) + + +- name: get_pending_partial_withdrawals#electra + sources: [] + spec: | + + def get_pending_partial_withdrawals( + state: BeaconState, + withdrawal_index: WithdrawalIndex, + prior_withdrawals: Sequence[Withdrawal], + ) -> Tuple[Sequence[Withdrawal], WithdrawalIndex, uint64]: + epoch = get_current_epoch(state) + withdrawals_limit = min( + len(prior_withdrawals) + MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP, + MAX_WITHDRAWALS_PER_PAYLOAD - 1, + ) + + processed_count: uint64 = 0 + withdrawals: List[Withdrawal] = [] + for withdrawal in state.pending_partial_withdrawals: + all_withdrawals = prior_withdrawals + withdrawals + is_withdrawable = withdrawal.withdrawable_epoch <= epoch + has_reached_limit = len(all_withdrawals) == withdrawals_limit + if not is_withdrawable or has_reached_limit: + break + + validator_index = withdrawal.validator_index + validator = state.validators[validator_index] + balance = get_balance_after_withdrawals(state, validator_index, all_withdrawals) + if is_eligible_for_partial_withdrawals(validator, balance): + withdrawal_amount = min(balance - MIN_ACTIVATION_BALANCE, withdrawal.amount) + withdrawals.append( + Withdrawal( + index=withdrawal_index, + validator_index=validator_index, + address=ExecutionAddress(validator.withdrawal_credentials[12:]), + amount=withdrawal_amount, + ) + ) + withdrawal_index += WithdrawalIndex(1) + + processed_count += 1 + + return withdrawals, withdrawal_index, processed_count + + +- name: get_pow_block_at_terminal_total_difficulty#bellatrix + sources: [] + spec: | + + def get_pow_block_at_terminal_total_difficulty( + pow_chain: Dict[Hash32, PowBlock], + ) -> Optional[PowBlock]: + # `pow_chain` abstractly represents all blocks in the PoW chain + for block in pow_chain.values(): + block_reached_ttd = block.total_difficulty >= TERMINAL_TOTAL_DIFFICULTY + if block_reached_ttd: + # If genesis block, no parent exists so reaching TTD alone qualifies as valid terminal block + if block.parent_hash == Hash32(): + return block + parent = pow_chain[block.parent_hash] + parent_reached_ttd = parent.total_difficulty >= TERMINAL_TOTAL_DIFFICULTY + if not parent_reached_ttd: + return block + + return None + + +- name: get_previous_epoch#phase0 + sources: + - file: packages/state-transition/src/util/epoch.ts + search: export function getPreviousEpoch( + spec: | + + def get_previous_epoch(state: BeaconState) -> Epoch: + """` + Return the previous epoch (unless the current epoch is ``GENESIS_EPOCH``). + """ + current_epoch = get_current_epoch(state) + return GENESIS_EPOCH if current_epoch == GENESIS_EPOCH else Epoch(current_epoch - 1) + + +- name: get_proposer_head#phase0 + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: "* Same as https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#get_proposer_head" + spec: | + + def get_proposer_head(store: Store, head_root: Root, slot: Slot) -> Root: + head_block = store.blocks[head_root] + parent_root = head_block.parent_root + parent_block = store.blocks[parent_root] + + # Only re-org the head block if it arrived later than the attestation deadline. + head_late = is_head_late(store, head_root) + + # Do not re-org on an epoch boundary where the proposer shuffling could change. + shuffling_stable = is_shuffling_stable(slot) + + # Ensure that the FFG information of the new head will be competitive with the current head. + ffg_competitive = is_ffg_competitive(store, head_root, parent_root) + + # Do not re-org if the chain is not finalizing with acceptable frequency. + finalization_ok = is_finalization_ok(store, slot) + + # Only re-org if we are proposing on-time. + proposing_on_time = is_proposing_on_time(store) + + # Only re-org a single slot at most. + parent_slot_ok = parent_block.slot + 1 == head_block.slot + current_time_ok = head_block.slot + 1 == slot + single_slot_reorg = parent_slot_ok and current_time_ok + + # Check that the head has few enough votes to be overpowered by our proposer boost. + assert store.proposer_boost_root != head_root # ensure boost has worn off + head_weak = is_head_weak(store, head_root) + + # Check that the missing votes are assigned to the parent and not being hoarded. + parent_strong = is_parent_strong(store, head_root) + + # Re-org more aggressively if there is a proposer equivocation in the previous slot. + proposer_equivocation = is_proposer_equivocation(store, head_root) + + if all( + [ + head_late, + shuffling_stable, + ffg_competitive, + finalization_ok, + proposing_on_time, + single_slot_reorg, + head_weak, + parent_strong, + ] + ): + # We can re-org the current head by building upon its parent block. + return parent_root + elif all([head_weak, current_time_ok, proposer_equivocation]): + return parent_root + else: + return head_root + + +- name: get_proposer_preferences_signature#gloas + sources: [] + spec: | + + def get_proposer_preferences_signature( + state: BeaconState, preferences: ProposerPreferences, privkey: int + ) -> BLSSignature: + domain = get_domain( + state, DOMAIN_PROPOSER_PREFERENCES, compute_epoch_at_slot(preferences.proposal_slot) + ) + signing_root = compute_signing_root(preferences, domain) + return bls.Sign(privkey, signing_root) + + +- name: get_proposer_reorg_cutoff_ms#phase0 + sources: + - file: packages/config/src/forkConfig/index.ts + search: "getProposerReorgCutoffMs(_fork: ForkName): number {" + spec: | + + def get_proposer_reorg_cutoff_ms(epoch: Epoch) -> uint64: + return get_slot_component_duration_ms(PROPOSER_REORG_CUTOFF_BPS) + + +- name: get_proposer_reward#phase0 + sources: [] + spec: | + + def get_proposer_reward(state: BeaconState, attesting_index: ValidatorIndex) -> Gwei: + return Gwei(get_base_reward(state, attesting_index) // PROPOSER_REWARD_QUOTIENT) + + +- name: get_proposer_score#phase0 + sources: [] + spec: | + + def get_proposer_score(store: Store) -> Gwei: + justified_checkpoint_state = store.checkpoint_states[store.justified_checkpoint] + return compute_proposer_score(justified_checkpoint_state) + + +- name: get_ptc#gloas + sources: + - file: packages/state-transition/src/cache/epochCache.ts + search: '^\s+getPayloadTimelinessCommittee\(' + regex: true + spec: | + + def get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]: + """ + Get the payload timeliness committee for the given ``slot``. + """ + epoch = compute_epoch_at_slot(slot) + seed = hash(get_seed(state, epoch, DOMAIN_PTC_ATTESTER) + uint_to_bytes(slot)) + indices: List[ValidatorIndex] = [] + # Concatenate all committees for this slot in order + committees_per_slot = get_committee_count_per_slot(state, epoch) + for i in range(committees_per_slot): + committee = get_beacon_committee(state, slot, CommitteeIndex(i)) + indices.extend(committee) + return compute_balance_weighted_selection( + state, indices, seed, size=PTC_SIZE, shuffle_indices=False + ) + + +- name: get_ptc_assignment#gloas + sources: [] + spec: | + + def get_ptc_assignment( + state: BeaconState, epoch: Epoch, validator_index: ValidatorIndex + ) -> Optional[Slot]: + """ + Returns the slot during the requested epoch in which the validator with + index `validator_index` is a member of the PTC. Returns None if no + assignment is found. + """ + next_epoch = Epoch(get_current_epoch(state) + 1) + assert epoch <= next_epoch + + start_slot = compute_start_slot_at_epoch(epoch) + for slot in range(start_slot, start_slot + SLOTS_PER_EPOCH): + if validator_index in get_ptc(state, Slot(slot)): + return Slot(slot) + return None + + +- name: get_randao_mix#phase0 + sources: + - file: packages/state-transition/src/util/seed.ts + search: export function getRandaoMix( + spec: | + + def get_randao_mix(state: BeaconState, epoch: Epoch) -> Bytes32: + """ + Return the randao mix at a recent ``epoch``. + """ + return state.randao_mixes[epoch % EPOCHS_PER_HISTORICAL_VECTOR] + + +- name: get_safety_threshold#altair + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function getSafetyThreshold( + spec: | + + def get_safety_threshold(store: LightClientStore) -> uint64: + return ( + max( + store.previous_max_active_participants, + store.current_max_active_participants, + ) + // 2 + ) + + +- name: get_seed#phase0 + sources: + - file: packages/state-transition/src/util/seed.ts + search: export function getSeed( + spec: | + + def get_seed(state: BeaconState, epoch: Epoch, domain_type: DomainType) -> Bytes32: + """ + Return the seed at ``epoch``. + """ + mix = get_randao_mix( + state, Epoch(epoch + EPOCHS_PER_HISTORICAL_VECTOR - MIN_SEED_LOOKAHEAD - 1) + ) # Avoid underflow + return hash(domain_type + uint_to_bytes(epoch) + mix) + + +- name: get_slot_component_duration_ms#phase0 + sources: + - file: packages/config/src/forkConfig/index.ts + search: "getSlotComponentDurationMs(basisPoints: number): number {" + spec: | + + def get_slot_component_duration_ms(basis_points: uint64) -> uint64: + """ + Calculate the duration of a slot component in milliseconds. + """ + return basis_points * SLOT_DURATION_MS // BASIS_POINTS + + +- name: get_slot_signature#phase0 + sources: + - file: packages/validator/src/services/validatorStore.ts + search: async signAttestationSelectionProof( + spec: | + + def get_slot_signature(state: BeaconState, slot: Slot, privkey: int) -> BLSSignature: + domain = get_domain(state, DOMAIN_SELECTION_PROOF, compute_epoch_at_slot(slot)) + signing_root = compute_signing_root(slot, domain) + return bls.Sign(privkey, signing_root) + + +- name: get_slots_since_genesis#phase0 + sources: + - file: packages/state-transition/src/util/slot.ts + search: export function getSlotsSinceGenesis( + spec: | + + def get_slots_since_genesis(store: Store) -> int: + return (store.time - store.genesis_time) // SECONDS_PER_SLOT + + +- name: get_source_deltas#phase0 + sources: [] + spec: | + + def get_source_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequence[Gwei]]: + """ + Return attester micro-rewards/penalties for source-vote for each validator. + """ + matching_source_attestations = get_matching_source_attestations( + state, get_previous_epoch(state) + ) + return get_attestation_component_deltas(state, matching_source_attestations) + + +- name: get_subtree_index#altair + sources: [] + spec: | + + def get_subtree_index(generalized_index: GeneralizedIndex) -> uint64: + return uint64(generalized_index % 2 ** (floorlog2(generalized_index))) + + +- name: get_sync_committee_message#altair + sources: + - file: packages/validator/src/services/validatorStore.ts + search: async signSyncCommitteeSignature( + spec: | + + def get_sync_committee_message( + state: BeaconState, block_root: Root, validator_index: ValidatorIndex, privkey: int + ) -> SyncCommitteeMessage: + epoch = get_current_epoch(state) + domain = get_domain(state, DOMAIN_SYNC_COMMITTEE, epoch) + signing_root = compute_signing_root(block_root, domain) + signature = bls.Sign(privkey, signing_root) + + return SyncCommitteeMessage( + slot=state.slot, + beacon_block_root=block_root, + validator_index=validator_index, + signature=signature, + ) + + +- name: get_sync_committee_selection_proof#altair + sources: + - file: packages/validator/src/services/validatorStore.ts + search: async signSyncCommitteeSelectionProof( + spec: | + + def get_sync_committee_selection_proof( + state: BeaconState, slot: Slot, subcommittee_index: uint64, privkey: int + ) -> BLSSignature: + domain = get_domain(state, DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF, compute_epoch_at_slot(slot)) + signing_data = SyncAggregatorSelectionData( + slot=slot, + subcommittee_index=subcommittee_index, + ) + signing_root = compute_signing_root(signing_data, domain) + return bls.Sign(privkey, signing_root) + + +- name: get_sync_message_due_ms#altair + sources: + - file: packages/config/src/forkConfig/index.ts + search: "getSyncMessageDueMs(fork: ForkName): number {" + spec: | + + def get_sync_message_due_ms(epoch: Epoch) -> uint64: + return get_slot_component_duration_ms(SYNC_MESSAGE_DUE_BPS) + + +- name: get_sync_message_due_ms#gloas + sources: + - file: packages/config/src/forkConfig/index.ts + search: "getSyncMessageDueMs(fork: ForkName): number {" + spec: | + + def get_sync_message_due_ms(epoch: Epoch) -> uint64: + # [New in Gloas] + if epoch >= GLOAS_FORK_EPOCH: + return get_slot_component_duration_ms(SYNC_MESSAGE_DUE_BPS_GLOAS) + return get_slot_component_duration_ms(SYNC_MESSAGE_DUE_BPS) + + +- name: get_sync_subcommittee_pubkeys#altair + sources: [] + spec: | + + def get_sync_subcommittee_pubkeys( + state: BeaconState, subcommittee_index: uint64 + ) -> Sequence[BLSPubkey]: + # Committees assigned to `slot` sign for `slot - 1` + # This creates the exceptional logic below when transitioning between sync committee periods + next_slot_epoch = compute_epoch_at_slot(Slot(state.slot + 1)) + if compute_sync_committee_period(get_current_epoch(state)) == compute_sync_committee_period( + next_slot_epoch + ): + sync_committee = state.current_sync_committee + else: + sync_committee = state.next_sync_committee + + # Return pubkeys for the subcommittee index + sync_subcommittee_size = SYNC_COMMITTEE_SIZE // SYNC_COMMITTEE_SUBNET_COUNT + i = subcommittee_index * sync_subcommittee_size + return sync_committee.pubkeys[i : i + sync_subcommittee_size] + + +- name: get_target_deltas#phase0 + sources: [] + spec: | + + def get_target_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequence[Gwei]]: + """ + Return attester micro-rewards/penalties for target-vote for each validator. + """ + matching_target_attestations = get_matching_target_attestations( + state, get_previous_epoch(state) + ) + return get_attestation_component_deltas(state, matching_target_attestations) + + +- name: get_terminal_pow_block#bellatrix + sources: [] + spec: | + + def get_terminal_pow_block(pow_chain: Dict[Hash32, PowBlock]) -> Optional[PowBlock]: + if TERMINAL_BLOCK_HASH != Hash32(): + # Terminal block hash override takes precedence over terminal total difficulty + if TERMINAL_BLOCK_HASH in pow_chain: + return pow_chain[TERMINAL_BLOCK_HASH] + else: + return None + + return get_pow_block_at_terminal_total_difficulty(pow_chain) + + +- name: get_total_active_balance#phase0 + sources: [] + spec: | + + def get_total_active_balance(state: BeaconState) -> Gwei: + """ + Return the combined effective balance of the active validators. + Note: ``get_total_balance`` returns ``EFFECTIVE_BALANCE_INCREMENT`` Gwei minimum to avoid divisions by zero. + """ + return get_total_balance( + state, set(get_active_validator_indices(state, get_current_epoch(state))) + ) + + +- name: get_total_balance#phase0 + sources: + - file: packages/state-transition/src/util/balance.ts + search: export function getTotalBalance( + spec: | + + def get_total_balance(state: BeaconState, indices: Set[ValidatorIndex]) -> Gwei: + """ + Return the combined effective balance of the ``indices``. + ``EFFECTIVE_BALANCE_INCREMENT`` Gwei minimum to avoid divisions by zero. + Math safe up to ~10B ETH, after which this overflows uint64. + """ + return Gwei( + max( + EFFECTIVE_BALANCE_INCREMENT, + sum([state.validators[index].effective_balance for index in indices]), + ) + ) + + +- name: get_unslashed_attesting_indices#phase0 + sources: [] + spec: | + + def get_unslashed_attesting_indices( + state: BeaconState, attestations: Sequence[PendingAttestation] + ) -> Set[ValidatorIndex]: + output: Set[ValidatorIndex] = set() + for a in attestations: + output = output.union(get_attesting_indices(state, a)) + return set(filter(lambda index: not state.validators[index].slashed, output)) + + +- name: get_unslashed_participating_indices#altair + sources: [] + spec: | + + def get_unslashed_participating_indices( + state: BeaconState, flag_index: int, epoch: Epoch + ) -> Set[ValidatorIndex]: + """ + Return the set of validator indices that are both active and unslashed for the given ``flag_index`` and ``epoch``. + """ + assert epoch in (get_previous_epoch(state), get_current_epoch(state)) + if epoch == get_current_epoch(state): + epoch_participation = state.current_epoch_participation + else: + epoch_participation = state.previous_epoch_participation + active_validator_indices = get_active_validator_indices(state, epoch) + participating_indices = [ + i for i in active_validator_indices if has_flag(epoch_participation[i], flag_index) + ] + return set(filter(lambda index: not state.validators[index].slashed, participating_indices)) + + +- name: get_upcoming_proposal_slots#gloas + sources: [] + spec: | + + def get_upcoming_proposal_slots( + state: BeaconState, validator_index: ValidatorIndex + ) -> Sequence[Slot]: + """ + Get the slots in the next epoch for which ``validator_index`` is proposing. + """ + return [ + Slot(compute_start_slot_at_epoch(get_current_epoch(state) + Epoch(1)) + offset) + for offset, proposer_index in enumerate(state.proposer_lookahead[SLOTS_PER_EPOCH:]) + if validator_index == proposer_index + ] + + +- name: get_validator_activation_churn_limit#deneb + sources: + - file: packages/state-transition/src/util/validator.ts + search: export function getActivationChurnLimit( + spec: | + + def get_validator_activation_churn_limit(state: BeaconState) -> uint64: + """ + Return the validator activation churn limit for the current epoch. + """ + return min(MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT, get_validator_churn_limit(state)) + + +- name: get_validator_churn_limit#phase0 + sources: + - file: packages/state-transition/src/util/validator.ts + search: export function getChurnLimit( + spec: | + + def get_validator_churn_limit(state: BeaconState) -> uint64: + """ + Return the validator churn limit for the current epoch. + """ + active_validator_indices = get_active_validator_indices(state, get_current_epoch(state)) + return max( + MIN_PER_EPOCH_CHURN_LIMIT, uint64(len(active_validator_indices)) // CHURN_LIMIT_QUOTIENT + ) + + +- name: get_validator_from_deposit#phase0 + sources: + - file: packages/state-transition/src/block/processDeposit.ts + search: export function addValidatorToRegistry( + spec: | + + def get_validator_from_deposit( + pubkey: BLSPubkey, withdrawal_credentials: Bytes32, amount: uint64 + ) -> Validator: + effective_balance = min(amount - amount % EFFECTIVE_BALANCE_INCREMENT, MAX_EFFECTIVE_BALANCE) + + return Validator( + pubkey=pubkey, + withdrawal_credentials=withdrawal_credentials, + effective_balance=effective_balance, + slashed=False, + activation_eligibility_epoch=FAR_FUTURE_EPOCH, + activation_epoch=FAR_FUTURE_EPOCH, + exit_epoch=FAR_FUTURE_EPOCH, + withdrawable_epoch=FAR_FUTURE_EPOCH, + ) + + +- name: get_validator_from_deposit#electra + sources: + - file: packages/state-transition/src/block/processDeposit.ts + search: export function addValidatorToRegistry( + spec: | + + def get_validator_from_deposit( + pubkey: BLSPubkey, withdrawal_credentials: Bytes32, amount: uint64 + ) -> Validator: + validator = Validator( + pubkey=pubkey, + withdrawal_credentials=withdrawal_credentials, + effective_balance=Gwei(0), + slashed=False, + activation_eligibility_epoch=FAR_FUTURE_EPOCH, + activation_epoch=FAR_FUTURE_EPOCH, + exit_epoch=FAR_FUTURE_EPOCH, + withdrawable_epoch=FAR_FUTURE_EPOCH, + ) + + # [Modified in Electra:EIP7251] + max_effective_balance = get_max_effective_balance(validator) + validator.effective_balance = min( + amount - amount % EFFECTIVE_BALANCE_INCREMENT, max_effective_balance + ) + + return validator + + +- name: get_validators_custody_requirement#fulu + sources: + - file: packages/beacon-node/src/util/dataColumns.ts + search: export function getValidatorsCustodyRequirement( + spec: | + + def get_validators_custody_requirement( + state: BeaconState, validator_indices: Sequence[ValidatorIndex] + ) -> uint64: + total_node_balance = sum( + state.validators[index].effective_balance for index in validator_indices + ) + count = total_node_balance // BALANCE_PER_ADDITIONAL_CUSTODY_GROUP + return min(max(count, VALIDATOR_CUSTODY_REQUIREMENT), NUMBER_OF_CUSTODY_GROUPS) + + +- name: get_validators_sweep_withdrawals#capella + sources: [] + spec: | + + def get_validators_sweep_withdrawals( + state: BeaconState, + withdrawal_index: WithdrawalIndex, + prior_withdrawals: Sequence[Withdrawal], + ) -> Tuple[Sequence[Withdrawal], WithdrawalIndex, uint64]: + epoch = get_current_epoch(state) + validators_limit = min(len(state.validators), MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP) + withdrawals_limit = MAX_WITHDRAWALS_PER_PAYLOAD + + processed_count: uint64 = 0 + withdrawals: List[Withdrawal] = [] + validator_index = state.next_withdrawal_validator_index + for _ in range(validators_limit): + all_withdrawals = prior_withdrawals + withdrawals + has_reached_limit = len(all_withdrawals) == withdrawals_limit + if has_reached_limit: + break + + validator = state.validators[validator_index] + balance = get_balance_after_withdrawals(state, validator_index, all_withdrawals) + if is_fully_withdrawable_validator(validator, balance, epoch): + withdrawals.append( + Withdrawal( + index=withdrawal_index, + validator_index=validator_index, + address=ExecutionAddress(validator.withdrawal_credentials[12:]), + amount=balance, + ) + ) + withdrawal_index += WithdrawalIndex(1) + elif is_partially_withdrawable_validator(validator, balance): + withdrawals.append( + Withdrawal( + index=withdrawal_index, + validator_index=validator_index, + address=ExecutionAddress(validator.withdrawal_credentials[12:]), + amount=balance - MAX_EFFECTIVE_BALANCE, + ) + ) + withdrawal_index += WithdrawalIndex(1) + + validator_index = ValidatorIndex((validator_index + 1) % len(state.validators)) + processed_count += 1 + + return withdrawals, withdrawal_index, processed_count + + +- name: get_validators_sweep_withdrawals#electra + sources: [] + spec: | + + def get_validators_sweep_withdrawals( + state: BeaconState, + withdrawal_index: WithdrawalIndex, + prior_withdrawals: Sequence[Withdrawal], + ) -> Tuple[Sequence[Withdrawal], WithdrawalIndex, uint64]: + epoch = get_current_epoch(state) + validators_limit = min(len(state.validators), MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP) + withdrawals_limit = MAX_WITHDRAWALS_PER_PAYLOAD + + processed_count: uint64 = 0 + withdrawals: List[Withdrawal] = [] + validator_index = state.next_withdrawal_validator_index + for _ in range(validators_limit): + all_withdrawals = prior_withdrawals + withdrawals + has_reached_limit = len(all_withdrawals) == withdrawals_limit + if has_reached_limit: + break + + validator = state.validators[validator_index] + balance = get_balance_after_withdrawals(state, validator_index, all_withdrawals) + if is_fully_withdrawable_validator(validator, balance, epoch): + withdrawals.append( + Withdrawal( + index=withdrawal_index, + validator_index=validator_index, + address=ExecutionAddress(validator.withdrawal_credentials[12:]), + amount=balance, + ) + ) + withdrawal_index += WithdrawalIndex(1) + elif is_partially_withdrawable_validator(validator, balance): + withdrawals.append( + Withdrawal( + index=withdrawal_index, + validator_index=validator_index, + address=ExecutionAddress(validator.withdrawal_credentials[12:]), + # [Modified in Electra:EIP7251] + amount=balance - get_max_effective_balance(validator), + ) + ) + withdrawal_index += WithdrawalIndex(1) + + validator_index = ValidatorIndex((validator_index + 1) % len(state.validators)) + processed_count += 1 + + return withdrawals, withdrawal_index, processed_count + + +- name: get_voting_source#phase0 + sources: [] + spec: | + + def get_voting_source(store: Store, block_root: Root) -> Checkpoint: + """ + Compute the voting source checkpoint in event that block with root ``block_root`` is the head block + """ + block = store.blocks[block_root] + current_epoch = get_current_store_epoch(store) + block_epoch = compute_epoch_at_slot(block.slot) + if current_epoch > block_epoch: + # The block is from a prior epoch, the voting source will be pulled-up + return store.unrealized_justifications[block_root] + else: + # The block is not from a prior epoch, therefore the voting source is not pulled up + head_state = store.block_states[block_root] + return head_state.current_justified_checkpoint + + +- name: get_weight#phase0 + sources: [] + spec: | + + def get_weight(store: Store, root: Root) -> Gwei: + state = store.checkpoint_states[store.justified_checkpoint] + attestation_score = get_attestation_score(store, root, state) + if store.proposer_boost_root == Root(): + # Return only attestation score if ``proposer_boost_root`` is not set + return attestation_score + + # Calculate proposer score if ``proposer_boost_root`` is set + proposer_score = Gwei(0) + # Boost is applied if ``root`` is an ancestor of ``proposer_boost_root`` + if get_ancestor(store, store.proposer_boost_root, store.blocks[root].slot) == root: + proposer_score = get_proposer_score(store) + return attestation_score + proposer_score + + +- name: get_weight#gloas + sources: [] + spec: | + + def get_weight( + store: Store, + # [Modified in Gloas:EIP7732] + node: ForkChoiceNode, + ) -> Gwei: + if node.payload_status == PAYLOAD_STATUS_PENDING or store.blocks[ + node.root + ].slot + 1 != get_current_slot(store): + state = store.checkpoint_states[store.justified_checkpoint] + attestation_score = get_attestation_score(store, node, state) + if not should_apply_proposer_boost(store): + # Return only attestation score if + # proposer boost should not apply + return attestation_score + + # Calculate proposer score if `proposer_boost_root` is set + proposer_score = Gwei(0) + + # `proposer_boost_root` is treated as a vote for the + # proposer's block in the current slot. Proposer boost + # is applied accordingly to all ancestors + message = LatestMessage( + slot=get_current_slot(store), + root=store.proposer_boost_root, + payload_present=False, + ) + if is_supporting_vote(store, node, message): + proposer_score = get_proposer_score(store) + + return attestation_score + proposer_score + else: + return Gwei(0) + + +- name: has_compounding_withdrawal_credential#electra + sources: + - file: packages/state-transition/src/util/electra.ts + search: export function hasCompoundingWithdrawalCredential( + spec: | + + def has_compounding_withdrawal_credential(validator: Validator) -> bool: + """ + Check if ``validator`` has an 0x02 prefixed "compounding" withdrawal credential. + """ + return is_compounding_withdrawal_credential(validator.withdrawal_credentials) + + +- name: has_compounding_withdrawal_credential#gloas + sources: [] + spec: | + + def has_compounding_withdrawal_credential(validator: Validator) -> bool: + """ + Check if ``validator`` has an 0x02 prefixed "compounding" withdrawal credential. + """ + return is_compounding_withdrawal_credential(validator.withdrawal_credentials) + + +- name: has_eth1_withdrawal_credential#capella + sources: + - file: packages/state-transition/src/util/capella.ts + search: export function hasEth1WithdrawalCredential( + spec: | + + def has_eth1_withdrawal_credential(validator: Validator) -> bool: + """ + Check if ``validator`` has an 0x01 prefixed "eth1" withdrawal credential. + """ + return validator.withdrawal_credentials[:1] == ETH1_ADDRESS_WITHDRAWAL_PREFIX + + +- name: has_execution_withdrawal_credential#electra + sources: + - file: packages/state-transition/src/util/electra.ts + search: export function hasExecutionWithdrawalCredential( + spec: | + + def has_execution_withdrawal_credential(validator: Validator) -> bool: + """ + Check if ``validator`` has a 0x01 or 0x02 prefixed withdrawal credential. + """ + return ( + has_eth1_withdrawal_credential(validator) # 0x01 + or has_compounding_withdrawal_credential(validator) # 0x02 + ) + + +- name: has_flag#altair + sources: + - file: packages/state-transition/src/util/attesterStatus.ts + search: "/** Same to https://github.com/ethereum/eth2.0-specs/blob/v1.1.0-alpha.5/specs/altair/beacon-chain.md#has_flag */" + spec: | + + def has_flag(flags: ParticipationFlags, flag_index: int) -> bool: + """ + Return whether ``flags`` has ``flag_index`` set. + """ + flag = ParticipationFlags(2**flag_index) + return flags & flag == flag + + +- name: increase_balance#phase0 + sources: + - file: packages/state-transition/src/util/balance.ts + search: export function increaseBalance( + spec: | + + def increase_balance(state: BeaconState, index: ValidatorIndex, delta: Gwei) -> None: + """ + Increase the validator balance at index ``index`` by ``delta``. + """ + state.balances[index] += delta + + +- name: initialize_beacon_state_from_eth1#phase0 + sources: + - file: packages/state-transition/src/util/genesis.ts + search: export function initializeBeaconStateFromEth1( + spec: | + + def initialize_beacon_state_from_eth1( + eth1_block_hash: Hash32, eth1_timestamp: uint64, deposits: Sequence[Deposit] + ) -> BeaconState: + fork = Fork( + previous_version=GENESIS_FORK_VERSION, + current_version=GENESIS_FORK_VERSION, + epoch=GENESIS_EPOCH, + ) + state = BeaconState( + genesis_time=eth1_timestamp + GENESIS_DELAY, + fork=fork, + eth1_data=Eth1Data(deposit_count=uint64(len(deposits)), block_hash=eth1_block_hash), + latest_block_header=BeaconBlockHeader(body_root=hash_tree_root(BeaconBlockBody())), + randao_mixes=[eth1_block_hash] + * EPOCHS_PER_HISTORICAL_VECTOR, # Seed RANDAO with Eth1 entropy + ) + + # Process deposits + leaves = list(map(lambda deposit: deposit.data, deposits)) + for index, deposit in enumerate(deposits): + deposit_data_list = List[DepositData, 2**DEPOSIT_CONTRACT_TREE_DEPTH](*leaves[: index + 1]) + state.eth1_data.deposit_root = hash_tree_root(deposit_data_list) + process_deposit(state, deposit) + + # Process activations + for index, validator in enumerate(state.validators): + balance = state.balances[index] + validator.effective_balance = min( + balance - balance % EFFECTIVE_BALANCE_INCREMENT, MAX_EFFECTIVE_BALANCE + ) + if validator.effective_balance == MAX_EFFECTIVE_BALANCE: + validator.activation_eligibility_epoch = GENESIS_EPOCH + validator.activation_epoch = GENESIS_EPOCH + + # Set genesis validators root for domain separation and chain versioning + state.genesis_validators_root = hash_tree_root(state.validators) + + return state + + +- name: initialize_light_client_store#altair + sources: + - file: packages/light-client/src/spec/store.ts + search: export class LightClientStore + spec: | + + def initialize_light_client_store( + trusted_block_root: Root, bootstrap: LightClientBootstrap + ) -> LightClientStore: + assert is_valid_light_client_header(bootstrap.header) + assert hash_tree_root(bootstrap.header.beacon) == trusted_block_root + + assert is_valid_normalized_merkle_branch( + leaf=hash_tree_root(bootstrap.current_sync_committee), + branch=bootstrap.current_sync_committee_branch, + gindex=current_sync_committee_gindex_at_slot(bootstrap.header.beacon.slot), + root=bootstrap.header.beacon.state_root, + ) + + return LightClientStore( + finalized_header=bootstrap.header, + current_sync_committee=bootstrap.current_sync_committee, + next_sync_committee=SyncCommittee(), + best_valid_update=None, + optimistic_header=bootstrap.header, + previous_max_active_participants=0, + current_max_active_participants=0, + ) + + +- name: initialize_proposer_lookahead#fulu + sources: + - file: packages/state-transition/src/util/fulu.ts + search: export function initializeProposerLookahead( + spec: | + + def initialize_proposer_lookahead( + state: electra.BeaconState, + ) -> Vector[ValidatorIndex, (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH]: + """ + Return the proposer indices for the full available lookahead starting from current epoch. + Used to initialize the ``proposer_lookahead`` field in the beacon state at genesis and after forks. + """ + current_epoch = get_current_epoch(state) + lookahead = [] + for i in range(MIN_SEED_LOOKAHEAD + 1): + lookahead.extend(get_beacon_proposer_indices(state, Epoch(current_epoch + i))) + return lookahead + + +- name: initiate_builder_exit#gloas + sources: + - file: packages/state-transition/src/util/gloas.ts + search: export function initiateBuilderExit( + spec: | + + def initiate_builder_exit(state: BeaconState, builder_index: BuilderIndex) -> None: + """ + Initiate the exit of the builder with index ``index``. + """ + # Return if builder already initiated exit + builder = state.builders[builder_index] + if builder.withdrawable_epoch != FAR_FUTURE_EPOCH: + return + + # Set builder exit epoch + builder.withdrawable_epoch = get_current_epoch(state) + MIN_BUILDER_WITHDRAWABILITY_DELAY + + +- name: initiate_validator_exit#phase0 + sources: + - file: packages/state-transition/src/block/initiateValidatorExit.ts + search: export function initiateValidatorExit( + spec: | + + def initiate_validator_exit(state: BeaconState, index: ValidatorIndex) -> None: + """ + Initiate the exit of the validator with index ``index``. + """ + # Return if validator already initiated exit + validator = state.validators[index] + if validator.exit_epoch != FAR_FUTURE_EPOCH: + return + + # Compute exit queue epoch + exit_epochs = [v.exit_epoch for v in state.validators if v.exit_epoch != FAR_FUTURE_EPOCH] + exit_queue_epoch = max(exit_epochs + [compute_activation_exit_epoch(get_current_epoch(state))]) + exit_queue_churn = len([v for v in state.validators if v.exit_epoch == exit_queue_epoch]) + if exit_queue_churn >= get_validator_churn_limit(state): + exit_queue_epoch += Epoch(1) + + # Set validator exit epoch and withdrawable epoch + validator.exit_epoch = exit_queue_epoch + validator.withdrawable_epoch = Epoch(validator.exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY) + + +- name: initiate_validator_exit#electra + sources: + - file: packages/state-transition/src/block/initiateValidatorExit.ts + search: export function initiateValidatorExit( + spec: | + + def initiate_validator_exit(state: BeaconState, index: ValidatorIndex) -> None: + """ + Initiate the exit of the validator with index ``index``. + """ + # Return if validator already initiated exit + validator = state.validators[index] + if validator.exit_epoch != FAR_FUTURE_EPOCH: + return + + # Compute exit queue epoch [Modified in Electra:EIP7251] + exit_queue_epoch = compute_exit_epoch_and_update_churn(state, validator.effective_balance) + + # Set validator exit epoch and withdrawable epoch + validator.exit_epoch = exit_queue_epoch + validator.withdrawable_epoch = Epoch(validator.exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY) + + +- name: integer_squareroot#phase0 + sources: + - file: packages/utils/src/math.ts + search: export function intSqrt( + spec: | + + def integer_squareroot(n: uint64) -> uint64: + """ + Return the largest integer ``x`` such that ``x**2 <= n``. + """ + if n == UINT64_MAX: + return UINT64_MAX_SQRT + x = n + y = (x + 1) // 2 + while y < x: + x = y + y = (x + n // x) // 2 + return x + + +- name: is_active_builder#gloas + sources: + - file: packages/state-transition/src/util/gloas.ts + search: export function isActiveBuilder( + spec: | + + def is_active_builder(state: BeaconState, builder_index: BuilderIndex) -> bool: + """ + Check if the builder at ``builder_index`` is active for the given ``state``. + """ + builder = state.builders[builder_index] + return ( + # Placement in builder list is finalized + builder.deposit_epoch < state.finalized_checkpoint.epoch + # Has not initiated exit + and builder.withdrawable_epoch == FAR_FUTURE_EPOCH + ) + + +- name: is_active_validator#phase0 + sources: + - file: packages/state-transition/src/util/validator.ts + search: export function isActiveValidator( + spec: | + + def is_active_validator(validator: Validator, epoch: Epoch) -> bool: + """ + Check if ``validator`` is active. + """ + return validator.activation_epoch <= epoch < validator.exit_epoch + + +- name: is_aggregator#phase0 + sources: + - file: packages/state-transition/src/util/aggregator.ts + search: export function isAggregatorFromCommitteeLength( + spec: | + + def is_aggregator( + state: BeaconState, slot: Slot, index: CommitteeIndex, slot_signature: BLSSignature + ) -> bool: + committee = get_beacon_committee(state, slot, index) + modulo = max(1, len(committee) // TARGET_AGGREGATORS_PER_COMMITTEE) + return bytes_to_uint64(hash(slot_signature)[0:8]) % modulo == 0 + + +- name: is_assigned_to_sync_committee#altair + sources: [] + spec: | + + def is_assigned_to_sync_committee( + state: BeaconState, epoch: Epoch, validator_index: ValidatorIndex + ) -> bool: + sync_committee_period = compute_sync_committee_period(epoch) + current_epoch = get_current_epoch(state) + current_sync_committee_period = compute_sync_committee_period(current_epoch) + next_sync_committee_period = current_sync_committee_period + 1 + assert sync_committee_period in (current_sync_committee_period, next_sync_committee_period) + + pubkey = state.validators[validator_index].pubkey + if sync_committee_period == current_sync_committee_period: + return pubkey in state.current_sync_committee.pubkeys + else: # sync_committee_period == next_sync_committee_period + return pubkey in state.next_sync_committee.pubkeys + + +- name: is_attestation_same_slot#gloas + sources: + - file: packages/state-transition/src/util/gloas.ts + search: export function isAttestationSameSlot( + spec: | + + def is_attestation_same_slot(state: BeaconState, data: AttestationData) -> bool: + """ + Check if the attestation is for the block proposed at the attestation slot. + """ + if data.slot == 0: + return True + + blockroot = data.beacon_block_root + slot_blockroot = get_block_root_at_slot(state, data.slot) + prev_blockroot = get_block_root_at_slot(state, Slot(data.slot - 1)) + + return blockroot == slot_blockroot and blockroot != prev_blockroot + + +- name: is_better_update#altair + sources: + - file: packages/light-client/src/spec/isBetterUpdate.ts + search: export function isBetterUpdate( + spec: | + + def is_better_update(new_update: LightClientUpdate, old_update: LightClientUpdate) -> bool: + # Compare supermajority (> 2/3) sync committee participation + max_active_participants = len(new_update.sync_aggregate.sync_committee_bits) + new_num_active_participants = sum(new_update.sync_aggregate.sync_committee_bits) + old_num_active_participants = sum(old_update.sync_aggregate.sync_committee_bits) + new_has_supermajority = new_num_active_participants * 3 >= max_active_participants * 2 + old_has_supermajority = old_num_active_participants * 3 >= max_active_participants * 2 + if new_has_supermajority != old_has_supermajority: + return new_has_supermajority + if not new_has_supermajority and new_num_active_participants != old_num_active_participants: + return new_num_active_participants > old_num_active_participants + + # Compare presence of relevant sync committee + new_has_relevant_sync_committee = is_sync_committee_update(new_update) and ( + compute_sync_committee_period_at_slot(new_update.attested_header.beacon.slot) + == compute_sync_committee_period_at_slot(new_update.signature_slot) + ) + old_has_relevant_sync_committee = is_sync_committee_update(old_update) and ( + compute_sync_committee_period_at_slot(old_update.attested_header.beacon.slot) + == compute_sync_committee_period_at_slot(old_update.signature_slot) + ) + if new_has_relevant_sync_committee != old_has_relevant_sync_committee: + return new_has_relevant_sync_committee + + # Compare indication of any finality + new_has_finality = is_finality_update(new_update) + old_has_finality = is_finality_update(old_update) + if new_has_finality != old_has_finality: + return new_has_finality + + # Compare sync committee finality + if new_has_finality: + new_has_sync_committee_finality = compute_sync_committee_period_at_slot( + new_update.finalized_header.beacon.slot + ) == compute_sync_committee_period_at_slot(new_update.attested_header.beacon.slot) + old_has_sync_committee_finality = compute_sync_committee_period_at_slot( + old_update.finalized_header.beacon.slot + ) == compute_sync_committee_period_at_slot(old_update.attested_header.beacon.slot) + if new_has_sync_committee_finality != old_has_sync_committee_finality: + return new_has_sync_committee_finality + + # Tiebreaker 1: Sync committee participation beyond supermajority + if new_num_active_participants != old_num_active_participants: + return new_num_active_participants > old_num_active_participants + + # Tiebreaker 2: Prefer older data (fewer changes to best) + if new_update.attested_header.beacon.slot != old_update.attested_header.beacon.slot: + return new_update.attested_header.beacon.slot < old_update.attested_header.beacon.slot + + # Tiebreaker 3: Prefer updates with earlier signature slots + return new_update.signature_slot < old_update.signature_slot + + +- name: is_builder_index#gloas + sources: + - file: packages/state-transition/src/util/gloas.ts + search: export function isBuilderIndex( + spec: | + + def is_builder_index(validator_index: ValidatorIndex) -> bool: + return (validator_index & BUILDER_INDEX_FLAG) != 0 + + +- name: is_builder_withdrawal_credential#gloas + sources: + - file: packages/state-transition/src/util/gloas.ts + search: export function isBuilderWithdrawalCredential( + spec: | + + def is_builder_withdrawal_credential(withdrawal_credentials: Bytes32) -> bool: + return withdrawal_credentials[:1] == BUILDER_WITHDRAWAL_PREFIX + + +- name: is_candidate_block#phase0 + sources: [] + spec: | + + def is_candidate_block(block: Eth1Block, period_start: uint64) -> bool: + return ( + block.timestamp + SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE <= period_start + and block.timestamp + SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE * 2 >= period_start + ) + + +- name: is_compounding_withdrawal_credential#electra + sources: + - file: packages/state-transition/src/util/electra.ts + search: export function hasCompoundingWithdrawalCredential( + spec: | + + def is_compounding_withdrawal_credential(withdrawal_credentials: Bytes32) -> bool: + return withdrawal_credentials[:1] == COMPOUNDING_WITHDRAWAL_PREFIX + + +- name: is_data_available#deneb + sources: + - file: packages/beacon-node/src/chain/blocks/verifyBlocksDataAvailability.ts + search: export async function verifyBlocksDataAvailability( + spec: | + + def is_data_available( + beacon_block_root: Root, blob_kzg_commitments: Sequence[KZGCommitment] + ) -> bool: + # `retrieve_blobs_and_proofs` is implementation and context dependent + # It returns all the blobs for the given block root, and raises an exception if not available + # Note: the p2p network does not guarantee sidecar retrieval outside of + # `MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS` + blobs, proofs = retrieve_blobs_and_proofs(beacon_block_root) + + return verify_blob_kzg_proof_batch(blobs, blob_kzg_commitments, proofs) + + +- name: is_data_available#fulu + sources: + - file: packages/beacon-node/src/chain/blocks/verifyBlocksDataAvailability.ts + search: export async function verifyBlocksDataAvailability( + spec: | + + def is_data_available(beacon_block_root: Root) -> bool: + # `retrieve_column_sidecars` is implementation and context dependent, replacing + # `retrieve_blobs_and_proofs`. For the given block root, it returns all column + # sidecars to sample, or raises an exception if they are not available. + # The p2p network does not guarantee sidecar retrieval outside of + # `MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS` epochs. + column_sidecars = retrieve_column_sidecars(beacon_block_root) + return all( + verify_data_column_sidecar(column_sidecar) + and verify_data_column_sidecar_kzg_proofs(column_sidecar) + for column_sidecar in column_sidecars + ) + + +- name: is_eligible_for_activation#phase0 + sources: + - file: packages/state-transition/src/cache/epochTransitionCache.ts + search: "// def is_eligible_for_activation(state: BeaconState, validator: Validator) -> bool:" + spec: | + + def is_eligible_for_activation(state: BeaconState, validator: Validator) -> bool: + """ + Check if ``validator`` is eligible for activation. + """ + return ( + # Placement in queue is finalized + validator.activation_eligibility_epoch <= state.finalized_checkpoint.epoch + # Has not yet been activated + and validator.activation_epoch == FAR_FUTURE_EPOCH + ) + + +- name: is_eligible_for_activation_queue#phase0 + sources: + - file: packages/state-transition/src/cache/epochTransitionCache.ts + search: "// def is_eligible_for_activation_queue(validator: Validator) -> bool:" + spec: | + + def is_eligible_for_activation_queue(validator: Validator) -> bool: + """ + Check if ``validator`` is eligible to be placed into the activation queue. + """ + return ( + validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH + and validator.effective_balance == MAX_EFFECTIVE_BALANCE + ) + + +- name: is_eligible_for_activation_queue#electra + sources: + - file: packages/state-transition/src/cache/epochTransitionCache.ts + search: "// def is_eligible_for_activation_queue(validator: Validator) -> bool:" + spec: | + + def is_eligible_for_activation_queue(validator: Validator) -> bool: + """ + Check if ``validator`` is eligible to be placed into the activation queue. + """ + return ( + validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH + # [Modified in Electra:EIP7251] + and validator.effective_balance >= MIN_ACTIVATION_BALANCE + ) + + +- name: is_eligible_for_partial_withdrawals#electra + sources: [] + spec: | + + def is_eligible_for_partial_withdrawals(validator: Validator, balance: Gwei) -> bool: + """ + Check if ``validator`` can process a pending partial withdrawal. + """ + has_sufficient_effective_balance = validator.effective_balance >= MIN_ACTIVATION_BALANCE + has_excess_balance = balance > MIN_ACTIVATION_BALANCE + return ( + validator.exit_epoch == FAR_FUTURE_EPOCH + and has_sufficient_effective_balance + and has_excess_balance + ) + + +- name: is_execution_block#bellatrix + sources: [] + spec: | + + def is_execution_block(block: BeaconBlock) -> bool: + return block.body.execution_payload != ExecutionPayload() + + +- name: is_execution_enabled#bellatrix + sources: + - file: packages/state-transition/src/util/execution.ts + search: export function isExecutionEnabled( + spec: | + + def is_execution_enabled(state: BeaconState, body: BeaconBlockBody) -> bool: + return is_merge_transition_block(state, body) or is_merge_transition_complete(state) + + +- name: is_ffg_competitive#phase0 + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: "// https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#is_ffg_competitive" + spec: | + + def is_ffg_competitive(store: Store, head_root: Root, parent_root: Root) -> bool: + return ( + store.unrealized_justifications[head_root] == store.unrealized_justifications[parent_root] + ) + + +- name: is_finality_update#altair + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function isFinalityUpdate( + spec: | + + def is_finality_update(update: LightClientUpdate) -> bool: + return update.finality_branch != FinalityBranch() + + +- name: is_finalization_ok#phase0 + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: "// https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#is_finalization_ok" + spec: | + + def is_finalization_ok(store: Store, slot: Slot) -> bool: + epochs_since_finalization = compute_epoch_at_slot(slot) - store.finalized_checkpoint.epoch + return epochs_since_finalization <= REORG_MAX_EPOCHS_SINCE_FINALIZATION + + +- name: is_fully_withdrawable_validator#capella + sources: [] + spec: | + + def is_fully_withdrawable_validator(validator: Validator, balance: Gwei, epoch: Epoch) -> bool: + """ + Check if ``validator`` is fully withdrawable. + """ + return ( + has_eth1_withdrawal_credential(validator) + and validator.withdrawable_epoch <= epoch + and balance > 0 + ) + + +- name: is_fully_withdrawable_validator#electra + sources: [] + spec: | + + def is_fully_withdrawable_validator(validator: Validator, balance: Gwei, epoch: Epoch) -> bool: + """ + Check if ``validator`` is fully withdrawable. + """ + return ( + # [Modified in Electra:EIP7251] + has_execution_withdrawal_credential(validator) + and validator.withdrawable_epoch <= epoch + and balance > 0 + ) + + +- name: is_head_late#phase0 + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: "// https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#is_head_late" + spec: | + + def is_head_late(store: Store, head_root: Root) -> bool: + return not store.block_timeliness[head_root] + + +- name: is_head_late#gloas + sources: [] + spec: | + + def is_head_late(store: Store, head_root: Root) -> bool: + return not store.block_timeliness[head_root][ATTESTATION_TIMELINESS_INDEX] + + +- name: is_head_weak#phase0 + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: "// https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#is_head_weak" + spec: | + + def is_head_weak(store: Store, head_root: Root) -> bool: + justified_state = store.checkpoint_states[store.justified_checkpoint] + reorg_threshold = calculate_committee_fraction(justified_state, REORG_HEAD_WEIGHT_THRESHOLD) + head_weight = get_weight(store, head_root) + return head_weight < reorg_threshold + + +- name: is_head_weak#gloas + sources: [] + spec: | + + def is_head_weak(store: Store, head_root: Root) -> bool: + # Calculate weight threshold for weak head + justified_state = store.checkpoint_states[store.justified_checkpoint] + reorg_threshold = calculate_committee_fraction(justified_state, REORG_HEAD_WEIGHT_THRESHOLD) + + # Compute head weight including equivocations + head_state = store.block_states[head_root] + head_block = store.blocks[head_root] + epoch = compute_epoch_at_slot(head_block.slot) + head_node = ForkChoiceNode(root=head_root, payload_status=PAYLOAD_STATUS_PENDING) + head_weight = get_attestation_score(store, head_node, justified_state) + for index in range(get_committee_count_per_slot(head_state, epoch)): + committee = get_beacon_committee(head_state, head_block.slot, CommitteeIndex(index)) + head_weight += Gwei( + sum( + justified_state.validators[i].effective_balance + for i in committee + if i in store.equivocating_indices + ) + ) + + return head_weight < reorg_threshold + + +- name: is_in_inactivity_leak#phase0 + sources: + - file: packages/state-transition/src/util/finality.ts + search: export function isInInactivityLeak( + spec: | + + def is_in_inactivity_leak(state: BeaconState) -> bool: + return get_finality_delay(state) > MIN_EPOCHS_TO_INACTIVITY_PENALTY + + +- name: is_merge_transition_block#bellatrix + sources: [] + spec: | + + def is_merge_transition_block(state: BeaconState, body: BeaconBlockBody) -> bool: + return not is_merge_transition_complete(state) and body.execution_payload != ExecutionPayload() + + +- name: is_merge_transition_complete#bellatrix + sources: + - file: packages/state-transition/src/util/execution.ts + search: export function isMergeTransitionComplete( + spec: | + + def is_merge_transition_complete(state: BeaconState) -> bool: + return state.latest_execution_payload_header != ExecutionPayloadHeader() + + +- name: is_next_sync_committee_known#altair + sources: [] + spec: | + + def is_next_sync_committee_known(store: LightClientStore) -> bool: + return store.next_sync_committee != SyncCommittee() + + +- name: is_optimistic#bellatrix + sources: [] + spec: | + + def is_optimistic(opt_store: OptimisticStore, block: BeaconBlock) -> bool: + return hash_tree_root(block) in opt_store.optimistic_roots + + +- name: is_optimistic_candidate_block#bellatrix + sources: [] + spec: | + + def is_optimistic_candidate_block( + opt_store: OptimisticStore, current_slot: Slot, block: BeaconBlock + ) -> bool: + if is_execution_block(opt_store.blocks[block.parent_root]): + return True + + if block.slot + SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY <= current_slot: + return True + + return False + + +- name: is_parent_block_full#gloas + sources: + - file: packages/state-transition/src/util/gloas.ts + search: export function isParentBlockFull( + spec: | + + def is_parent_block_full(state: BeaconState) -> bool: + return state.latest_execution_payload_bid.block_hash == state.latest_block_hash + + +- name: is_parent_node_full#gloas + sources: [] + spec: | + + def is_parent_node_full(store: Store, block: BeaconBlock) -> bool: + return get_parent_payload_status(store, block) == PAYLOAD_STATUS_FULL + + +- name: is_parent_strong#phase0 + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: "// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/fork-choice.md#is_parent_strong" + spec: | + + def is_parent_strong(store: Store, root: Root) -> bool: + justified_state = store.checkpoint_states[store.justified_checkpoint] + parent_threshold = calculate_committee_fraction(justified_state, REORG_PARENT_WEIGHT_THRESHOLD) + parent_root = store.blocks[root].parent_root + parent_weight = get_weight(store, parent_root) + return parent_weight > parent_threshold + + +- name: is_parent_strong#gloas + sources: [] + spec: | + + def is_parent_strong(store: Store, root: Root) -> bool: + justified_state = store.checkpoint_states[store.justified_checkpoint] + parent_threshold = calculate_committee_fraction(justified_state, REORG_PARENT_WEIGHT_THRESHOLD) + block = store.blocks[root] + parent_payload_status = get_parent_payload_status(store, block) + parent_node = ForkChoiceNode(root=block.parent_root, payload_status=parent_payload_status) + parent_weight = get_attestation_score(store, parent_node, justified_state) + return parent_weight > parent_threshold + + +- name: is_partially_withdrawable_validator#capella + sources: [] + spec: | + + def is_partially_withdrawable_validator(validator: Validator, balance: Gwei) -> bool: + """ + Check if ``validator`` is partially withdrawable. + """ + has_max_effective_balance = validator.effective_balance == MAX_EFFECTIVE_BALANCE + has_excess_balance = balance > MAX_EFFECTIVE_BALANCE + return ( + has_eth1_withdrawal_credential(validator) + and has_max_effective_balance + and has_excess_balance + ) + + +- name: is_partially_withdrawable_validator#electra + sources: [] + spec: | + + def is_partially_withdrawable_validator(validator: Validator, balance: Gwei) -> bool: + """ + Check if ``validator`` is partially withdrawable. + """ + max_effective_balance = get_max_effective_balance(validator) + # [Modified in Electra:EIP7251] + has_max_effective_balance = validator.effective_balance == max_effective_balance + # [Modified in Electra:EIP7251] + has_excess_balance = balance > max_effective_balance + return ( + # [Modified in Electra:EIP7251] + has_execution_withdrawal_credential(validator) + and has_max_effective_balance + and has_excess_balance + ) + + +- name: is_payload_timely#gloas + sources: [] + spec: | + + def is_payload_timely(store: Store, root: Root) -> bool: + """ + Return whether the execution payload for the beacon block with root ``root`` + was voted as present by the PTC, and was locally determined to be available. + """ + # The beacon block root must be known + assert root in store.ptc_vote + + # If the payload is not locally available, the payload + # is not considered available regardless of the PTC vote + if root not in store.execution_payload_states: + return False + + return sum(store.ptc_vote[root]) > PAYLOAD_TIMELY_THRESHOLD + + +- name: is_proposer#phase0 + sources: [] + spec: | + + def is_proposer(state: BeaconState, validator_index: ValidatorIndex) -> bool: + return get_beacon_proposer_index(state) == validator_index + + +- name: is_proposer_equivocation#phase0 + sources: [] + spec: | + + def is_proposer_equivocation(store: Store, root: Root) -> bool: + block = store.blocks[root] + proposer_index = block.proposer_index + slot = block.slot + # roots from the same slot and proposer + matching_roots = [ + root + for root, block in store.blocks.items() + if (block.proposer_index == proposer_index and block.slot == slot) + ] + return len(matching_roots) > 1 + + +- name: is_proposing_on_time#phase0 + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: "* https://github.com/ethereum/consensus-specs/blob/v1.5.0/specs/phase0/fork-choice.md#is_proposing_on_time" + spec: | + + def is_proposing_on_time(store: Store) -> bool: + seconds_since_genesis = store.time - store.genesis_time + time_into_slot_ms = seconds_to_milliseconds(seconds_since_genesis) % SLOT_DURATION_MS + epoch = get_current_store_epoch(store) + proposer_reorg_cutoff_ms = get_proposer_reorg_cutoff_ms(epoch) + return time_into_slot_ms <= proposer_reorg_cutoff_ms + + +- name: is_shuffling_stable#phase0 + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: "// https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#is_shuffling_stable" + spec: | + + def is_shuffling_stable(slot: Slot) -> bool: + return slot % SLOTS_PER_EPOCH != 0 + + +- name: is_slashable_attestation_data#phase0 + sources: + - file: packages/state-transition/src/util/attestation.ts + search: export function isSlashableAttestationData( + spec: | + + def is_slashable_attestation_data(data_1: AttestationData, data_2: AttestationData) -> bool: + """ + Check if ``data_1`` and ``data_2`` are slashable according to Casper FFG rules. + """ + return ( + # Double vote + (data_1 != data_2 and data_1.target.epoch == data_2.target.epoch) + or + # Surround vote + (data_1.source.epoch < data_2.source.epoch and data_2.target.epoch < data_1.target.epoch) + ) + + +- name: is_slashable_validator#phase0 + sources: + - file: packages/state-transition/src/util/validator.ts + search: export function isSlashableValidator( + spec: | + + def is_slashable_validator(validator: Validator, epoch: Epoch) -> bool: + """ + Check if ``validator`` is slashable. + """ + return (not validator.slashed) and ( + validator.activation_epoch <= epoch < validator.withdrawable_epoch + ) + + +- name: is_supporting_vote#gloas + sources: [] + spec: | + + def is_supporting_vote(store: Store, node: ForkChoiceNode, message: LatestMessage) -> bool: + """ + Returns whether a vote for ``message.root`` supports the chain containing the beacon block ``node.root`` with the + payload contents indicated by ``node.payload_status`` as head during slot ``node.slot``. + """ + block = store.blocks[node.root] + if node.root == message.root: + if node.payload_status == PAYLOAD_STATUS_PENDING: + return True + if message.slot <= block.slot: + return False + if message.payload_present: + return node.payload_status == PAYLOAD_STATUS_FULL + else: + return node.payload_status == PAYLOAD_STATUS_EMPTY + else: + ancestor = get_ancestor(store, message.root, block.slot) + return node.root == ancestor.root and ( + node.payload_status == PAYLOAD_STATUS_PENDING + or node.payload_status == ancestor.payload_status + ) + + +- name: is_sync_committee_aggregator#altair + sources: + - file: packages/state-transition/src/util/aggregator.ts + search: export function isSyncCommitteeAggregator( + spec: | + + def is_sync_committee_aggregator(signature: BLSSignature) -> bool: + modulo = max( + 1, + SYNC_COMMITTEE_SIZE + // SYNC_COMMITTEE_SUBNET_COUNT + // TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE, + ) + return bytes_to_uint64(hash(signature)[0:8]) % modulo == 0 + + +- name: is_sync_committee_update#altair + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function isSyncCommitteeUpdate( + spec: | + + def is_sync_committee_update(update: LightClientUpdate) -> bool: + return update.next_sync_committee_branch != NextSyncCommitteeBranch() + + +- name: is_valid_deposit_signature#electra + sources: + - file: packages/state-transition/src/block/processDeposit.ts + search: export function isValidDepositSignature( + spec: | + + def is_valid_deposit_signature( + pubkey: BLSPubkey, withdrawal_credentials: Bytes32, amount: uint64, signature: BLSSignature + ) -> bool: + deposit_message = DepositMessage( + pubkey=pubkey, + withdrawal_credentials=withdrawal_credentials, + amount=amount, + ) + # Fork-agnostic domain since deposits are valid across forks + domain = compute_domain(DOMAIN_DEPOSIT) + signing_root = compute_signing_root(deposit_message, domain) + return bls.Verify(pubkey, signing_root, signature) + + +- name: is_valid_genesis_state#phase0 + sources: + - file: packages/state-transition/src/util/genesis.ts + search: export function isValidGenesisState( + spec: | + + def is_valid_genesis_state(state: BeaconState) -> bool: + if state.genesis_time < MIN_GENESIS_TIME: + return False + if len(get_active_validator_indices(state, GENESIS_EPOCH)) < MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: + return False + return True + + +- name: is_valid_indexed_attestation#phase0 + sources: + - file: packages/state-transition/src/block/isValidIndexedAttestation.ts + search: export function isValidIndexedAttestation( + spec: | + + def is_valid_indexed_attestation( + state: BeaconState, indexed_attestation: IndexedAttestation + ) -> bool: + """ + Check if ``indexed_attestation`` is not empty, has sorted and unique indices and has a valid aggregate signature. + """ + # Verify indices are sorted and unique + indices = indexed_attestation.attesting_indices + if len(indices) == 0 or not indices == sorted(set(indices)): + return False + # Verify aggregate signature + pubkeys = [state.validators[i].pubkey for i in indices] + domain = get_domain(state, DOMAIN_BEACON_ATTESTER, indexed_attestation.data.target.epoch) + signing_root = compute_signing_root(indexed_attestation.data, domain) + return bls.FastAggregateVerify(pubkeys, signing_root, indexed_attestation.signature) + + +- name: is_valid_indexed_payload_attestation#gloas + sources: + - file: packages/state-transition/src/block/isValidIndexedPayloadAttestation.ts + search: export function isValidIndexedPayloadAttestation( + spec: | + + def is_valid_indexed_payload_attestation( + state: BeaconState, indexed_payload_attestation: IndexedPayloadAttestation + ) -> bool: + """ + Check if ``indexed_payload_attestation`` is non-empty, has sorted indices, and has + a valid aggregate signature. + """ + # Verify indices are non-empty and sorted + indices = indexed_payload_attestation.attesting_indices + if len(indices) == 0 or not indices == sorted(indices): + return False + + # Verify aggregate signature + pubkeys = [state.validators[i].pubkey for i in indices] + domain = get_domain(state, DOMAIN_PTC_ATTESTER, None) + signing_root = compute_signing_root(indexed_payload_attestation.data, domain) + return bls.FastAggregateVerify(pubkeys, signing_root, indexed_payload_attestation.signature) + + +- name: is_valid_light_client_header#altair + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function isValidLightClientHeader( + spec: | + + def is_valid_light_client_header(_header: LightClientHeader) -> bool: + return True + + +- name: is_valid_light_client_header#capella + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function isValidLightClientHeader( + spec: | + + def is_valid_light_client_header(header: LightClientHeader) -> bool: + epoch = compute_epoch_at_slot(header.beacon.slot) + + if epoch < CAPELLA_FORK_EPOCH: + return ( + header.execution == ExecutionPayloadHeader() + and header.execution_branch == ExecutionBranch() + ) + + return is_valid_merkle_branch( + leaf=get_lc_execution_root(header), + branch=header.execution_branch, + depth=floorlog2(EXECUTION_PAYLOAD_GINDEX), + index=get_subtree_index(EXECUTION_PAYLOAD_GINDEX), + root=header.beacon.body_root, + ) + + +- name: is_valid_light_client_header#deneb + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function isValidLightClientHeader( + spec: | + + def is_valid_light_client_header(header: LightClientHeader) -> bool: + epoch = compute_epoch_at_slot(header.beacon.slot) + + # [New in Deneb:EIP4844] + if epoch < DENEB_FORK_EPOCH: + if header.execution.blob_gas_used != uint64(0): + return False + if header.execution.excess_blob_gas != uint64(0): + return False + + if epoch < CAPELLA_FORK_EPOCH: + return ( + header.execution == ExecutionPayloadHeader() + and header.execution_branch == ExecutionBranch() + ) + + return is_valid_merkle_branch( + leaf=get_lc_execution_root(header), + branch=header.execution_branch, + depth=floorlog2(EXECUTION_PAYLOAD_GINDEX), + index=get_subtree_index(EXECUTION_PAYLOAD_GINDEX), + root=header.beacon.body_root, + ) + + +- name: is_valid_merkle_branch#phase0 + sources: + - file: packages/light-client/src/utils/verifyMerkleBranch.ts + search: export function isValidMerkleBranch( + spec: | + + def is_valid_merkle_branch( + leaf: Bytes32, branch: Sequence[Bytes32], depth: uint64, index: uint64, root: Root + ) -> bool: + """ + Check if ``leaf`` at ``index`` verifies against the Merkle ``root`` and ``branch``. + """ + value = leaf + for i in range(depth): + if index // (2**i) % 2: + value = hash(branch[i] + value) + else: + value = hash(value + branch[i]) + return value == root + + +- name: is_valid_normalized_merkle_branch#altair + sources: [] + spec: | + + def is_valid_normalized_merkle_branch( + leaf: Bytes32, branch: Sequence[Bytes32], gindex: GeneralizedIndex, root: Root + ) -> bool: + depth = floorlog2(gindex) + index = get_subtree_index(gindex) + num_extra = len(branch) - depth + for i in range(num_extra): + if branch[i] != Bytes32(): + return False + return is_valid_merkle_branch(leaf, branch[num_extra:], depth, index, root) + + +- name: is_valid_proposal_slot#gloas + sources: [] + spec: | + + def is_valid_proposal_slot(state: BeaconState, preferences: ProposerPreferences) -> bool: + """ + Check if the validator is the proposer for the given slot in the next epoch. + """ + index = SLOTS_PER_EPOCH + preferences.proposal_slot % SLOTS_PER_EPOCH + return state.proposer_lookahead[index] == preferences.validator_index + + +- name: is_valid_switch_to_compounding_request#electra + sources: [] + spec: | + + def is_valid_switch_to_compounding_request( + state: BeaconState, consolidation_request: ConsolidationRequest + ) -> bool: + # Switch to compounding requires source and target be equal + if consolidation_request.source_pubkey != consolidation_request.target_pubkey: + return False + + # Verify pubkey exists + source_pubkey = consolidation_request.source_pubkey + validator_pubkeys = [v.pubkey for v in state.validators] + if source_pubkey not in validator_pubkeys: + return False + + source_validator = state.validators[ValidatorIndex(validator_pubkeys.index(source_pubkey))] + + # Verify request has been authorized + if source_validator.withdrawal_credentials[12:] != consolidation_request.source_address: + return False + + # Verify source withdrawal credentials + if not has_eth1_withdrawal_credential(source_validator): + return False + + # Verify the source is active + current_epoch = get_current_epoch(state) + if not is_active_validator(source_validator, current_epoch): + return False + + # Verify exit for source has not been initiated + if source_validator.exit_epoch != FAR_FUTURE_EPOCH: + return False + + return True + + +- name: is_valid_terminal_pow_block#bellatrix + sources: [] + spec: | + + def is_valid_terminal_pow_block(block: PowBlock, parent: PowBlock) -> bool: + is_total_difficulty_reached = block.total_difficulty >= TERMINAL_TOTAL_DIFFICULTY + is_parent_total_difficulty_valid = parent.total_difficulty < TERMINAL_TOTAL_DIFFICULTY + return is_total_difficulty_reached and is_parent_total_difficulty_valid + + +- name: is_within_weak_subjectivity_period#phase0 + sources: + - file: packages/state-transition/src/util/weakSubjectivity.ts + search: export function isWithinWeakSubjectivityPeriod( + spec: | + + def is_within_weak_subjectivity_period( + store: Store, ws_state: BeaconState, ws_checkpoint: Checkpoint + ) -> bool: + # Clients may choose to validate the input state against the input Weak Subjectivity Checkpoint + assert get_block_root(ws_state, ws_checkpoint.epoch) == ws_checkpoint.root + assert compute_epoch_at_slot(ws_state.slot) == ws_checkpoint.epoch + + ws_period = compute_weak_subjectivity_period(ws_state) + ws_state_epoch = compute_epoch_at_slot(ws_state.slot) + current_epoch = compute_epoch_at_slot(get_current_slot(store)) + return current_epoch <= ws_state_epoch + ws_period + + +- name: is_within_weak_subjectivity_period#electra + sources: + - file: packages/state-transition/src/util/weakSubjectivity.ts + search: export function isWithinWeakSubjectivityPeriod( + spec: | + + def is_within_weak_subjectivity_period( + store: Store, ws_state: BeaconState, ws_checkpoint: Checkpoint + ) -> bool: + # Clients may choose to validate the input state against the input Weak Subjectivity Checkpoint + assert get_block_root(ws_state, ws_checkpoint.epoch) == ws_checkpoint.root + assert compute_epoch_at_slot(ws_state.slot) == ws_checkpoint.epoch + + # [Modified in Electra] + ws_period = compute_weak_subjectivity_period(ws_state) + ws_state_epoch = compute_epoch_at_slot(ws_state.slot) + current_epoch = compute_epoch_at_slot(get_current_slot(store)) + return current_epoch <= ws_state_epoch + ws_period + + +- name: kzg_commitment_to_versioned_hash#deneb + sources: + - file: packages/beacon-node/src/util/blobs.ts + search: export function kzgCommitmentToVersionedHash( + spec: | + + def kzg_commitment_to_versioned_hash(kzg_commitment: KZGCommitment) -> VersionedHash: + return VERSIONED_HASH_VERSION_KZG + hash(kzg_commitment)[1:] + + +- name: latest_verified_ancestor#bellatrix + sources: [] + spec: | + + def latest_verified_ancestor(opt_store: OptimisticStore, block: BeaconBlock) -> BeaconBlock: + # It is assumed that the `block` parameter is never an INVALIDATED block. + while True: + if not is_optimistic(opt_store, block) or block.parent_root == Root(): + return block + block = opt_store.blocks[block.parent_root] + + +- name: max_compressed_len#phase0 + sources: [] + spec: | + + def max_compressed_len(n: uint64) -> uint64: + # Worst-case compressed length for a given payload of size n when using snappy: + # https://github.com/google/snappy/blob/32ded457c0b1fe78ceb8397632c416568d6714a0/snappy.cc#L218C1-L218C47 + return uint64(32 + n + n / 6) + + +- name: max_message_size#phase0 + sources: [] + spec: | + + def max_message_size() -> uint64: + # Allow 1024 bytes for framing and encoding overhead but at least 1MiB in case MAX_PAYLOAD_SIZE is small. + return max(max_compressed_len(MAX_PAYLOAD_SIZE) + 1024, 1024 * 1024) + + +- name: next_sync_committee_gindex_at_slot#altair + sources: [] + spec: | + + def next_sync_committee_gindex_at_slot(_slot: Slot) -> GeneralizedIndex: + return NEXT_SYNC_COMMITTEE_GINDEX + + +- name: next_sync_committee_gindex_at_slot#electra + sources: [] + spec: | + + def next_sync_committee_gindex_at_slot(slot: Slot) -> GeneralizedIndex: + epoch = compute_epoch_at_slot(slot) + + # [Modified in Electra] + if epoch >= ELECTRA_FORK_EPOCH: + return NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA + return NEXT_SYNC_COMMITTEE_GINDEX + + +- name: normalize_merkle_branch#electra + sources: + - file: packages/light-client/src/utils/normalizeMerkleBranch.ts + search: export function normalizeMerkleBranch( + spec: | + + def normalize_merkle_branch( + branch: Sequence[Bytes32], gindex: GeneralizedIndex + ) -> Sequence[Bytes32]: + depth = floorlog2(gindex) + num_extra = depth - len(branch) + return [Bytes32()] * num_extra + [*branch] + + +- name: notify_ptc_messages#gloas + sources: [] + spec: | + + def notify_ptc_messages( + store: Store, state: BeaconState, payload_attestations: Sequence[PayloadAttestation] + ) -> None: + """ + Extracts a list of ``PayloadAttestationMessage`` from ``payload_attestations`` and updates the store with them + These Payload attestations are assumed to be in the beacon block hence signature verification is not needed + """ + if state.slot == 0: + return + for payload_attestation in payload_attestations: + indexed_payload_attestation = get_indexed_payload_attestation(state, payload_attestation) + for idx in indexed_payload_attestation.attesting_indices: + on_payload_attestation_message( + store, + PayloadAttestationMessage( + validator_index=idx, + data=payload_attestation.data, + signature=BLSSignature(), + ), + is_from_block=True, + ) + + +- name: on_attestation#phase0 + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: '^\s+onAttestation\(' + regex: true + spec: | + + def on_attestation(store: Store, attestation: Attestation, is_from_block: bool = False) -> None: + """ + Run ``on_attestation`` upon receiving a new ``attestation`` from either within a block or directly on the wire. + + An ``attestation`` that is asserted as invalid may be valid at a later time, + consider scheduling it for later processing in such case. + """ + validate_on_attestation(store, attestation, is_from_block) + + store_target_checkpoint_state(store, attestation.data.target) + + # Get state at the `target` to fully validate attestation + target_state = store.checkpoint_states[attestation.data.target] + indexed_attestation = get_indexed_attestation(target_state, attestation) + assert is_valid_indexed_attestation(target_state, indexed_attestation) + + # Update latest messages for attesting indices + update_latest_messages(store, indexed_attestation.attesting_indices, attestation) + + +- name: on_attester_slashing#phase0 + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: '^\s+onAttesterSlashing\(' + regex: true + spec: | + + def on_attester_slashing(store: Store, attester_slashing: AttesterSlashing) -> None: + """ + Run ``on_attester_slashing`` immediately upon receiving a new ``AttesterSlashing`` + from either within a block or directly on the wire. + """ + attestation_1 = attester_slashing.attestation_1 + attestation_2 = attester_slashing.attestation_2 + assert is_slashable_attestation_data(attestation_1.data, attestation_2.data) + state = store.block_states[store.justified_checkpoint.root] + assert is_valid_indexed_attestation(state, attestation_1) + assert is_valid_indexed_attestation(state, attestation_2) + + indices = set(attestation_1.attesting_indices).intersection(attestation_2.attesting_indices) + for index in indices: + store.equivocating_indices.add(index) + + +- name: on_block#phase0 + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: '^\s+onBlock\(' + regex: true + spec: | + + def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: + block = signed_block.message + # Parent block must be known + assert block.parent_root in store.block_states + # Make a copy of the state to avoid mutability issues + pre_state = copy(store.block_states[block.parent_root]) + # Blocks cannot be in the future. If they are, their consideration must be delayed until they are in the past. + assert get_current_slot(store) >= block.slot + + # Check that block is later than the finalized epoch slot (optimization to reduce calls to get_ancestor) + finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) + assert block.slot > finalized_slot + # Check block is a descendant of the finalized block at the checkpoint finalized slot + finalized_checkpoint_block = get_checkpoint_block( + store, + block.parent_root, + store.finalized_checkpoint.epoch, + ) + assert store.finalized_checkpoint.root == finalized_checkpoint_block + + # Check the block is valid and compute the post-state + state = pre_state.copy() + block_root = hash_tree_root(block) + state_transition(state, signed_block, True) + # Add new block to the store + store.blocks[block_root] = block + # Add new state for this block to the store + store.block_states[block_root] = state + + record_block_timeliness(store, block_root) + update_proposer_boost_root(store, block_root) + + # Update checkpoints in store if necessary + update_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint) + + # Eagerly compute unrealized justification and finality + compute_pulled_up_tip(store, block_root) + + +- name: on_block#bellatrix + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: '^\s+onBlock\(' + regex: true + spec: | + + def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: + """ + Run ``on_block`` upon receiving a new block. + + A block that is asserted as invalid due to unavailable PoW block may be valid at a later time, + consider scheduling it for later processing in such case. + """ + block = signed_block.message + # Parent block must be known + assert block.parent_root in store.block_states + # Make a copy of the state to avoid mutability issues + pre_state = copy(store.block_states[block.parent_root]) + # Blocks cannot be in the future. If they are, their consideration must be delayed until they are in the past. + assert get_current_slot(store) >= block.slot + + # Check that block is later than the finalized epoch slot (optimization to reduce calls to get_ancestor) + finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) + assert block.slot > finalized_slot + # Check block is a descendant of the finalized block at the checkpoint finalized slot + finalized_checkpoint_block = get_checkpoint_block( + store, + block.parent_root, + store.finalized_checkpoint.epoch, + ) + assert store.finalized_checkpoint.root == finalized_checkpoint_block + + # Check the block is valid and compute the post-state + state = pre_state.copy() + block_root = hash_tree_root(block) + state_transition(state, signed_block, True) + + # [New in Bellatrix] + if is_merge_transition_block(pre_state, block.body): + validate_merge_block(block) + + # Add new block to the store + store.blocks[block_root] = block + # Add new state for this block to the store + store.block_states[block_root] = state + + record_block_timeliness(store, block_root) + update_proposer_boost_root(store, block_root) + + # Update checkpoints in store if necessary + update_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint) + + # Eagerly compute unrealized justification and finality. + compute_pulled_up_tip(store, block_root) + + +- name: on_block#capella + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: '^\s+onBlock\(' + regex: true + spec: | + + def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: + """ + Run ``on_block`` upon receiving a new block. + """ + block = signed_block.message + # Parent block must be known + assert block.parent_root in store.block_states + # Blocks cannot be in the future. If they are, their consideration must be delayed until they are in the past. + assert get_current_slot(store) >= block.slot + + # Check that block is later than the finalized epoch slot (optimization to reduce calls to get_ancestor) + finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) + assert block.slot > finalized_slot + # Check block is a descendant of the finalized block at the checkpoint finalized slot + finalized_checkpoint_block = get_checkpoint_block( + store, + block.parent_root, + store.finalized_checkpoint.epoch, + ) + assert store.finalized_checkpoint.root == finalized_checkpoint_block + + # Check the block is valid and compute the post-state + # Make a copy of the state to avoid mutability issues + state = copy(store.block_states[block.parent_root]) + block_root = hash_tree_root(block) + state_transition(state, signed_block, True) + + # Add new block to the store + store.blocks[block_root] = block + # Add new state for this block to the store + store.block_states[block_root] = state + + record_block_timeliness(store, block_root) + update_proposer_boost_root(store, block_root) + + # Update checkpoints in store if necessary + update_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint) + + # Eagerly compute unrealized justification and finality. + compute_pulled_up_tip(store, block_root) + + +- name: on_block#deneb + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: '^\s+onBlock\(' + regex: true + spec: | + + def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: + """ + Run ``on_block`` upon receiving a new block. + """ + block = signed_block.message + # Parent block must be known + assert block.parent_root in store.block_states + # Blocks cannot be in the future. If they are, their consideration must be delayed until they are in the past. + assert get_current_slot(store) >= block.slot + + # Check that block is later than the finalized epoch slot (optimization to reduce calls to get_ancestor) + finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) + assert block.slot > finalized_slot + # Check block is a descendant of the finalized block at the checkpoint finalized slot + finalized_checkpoint_block = get_checkpoint_block( + store, + block.parent_root, + store.finalized_checkpoint.epoch, + ) + assert store.finalized_checkpoint.root == finalized_checkpoint_block + + # [New in Deneb:EIP4844] + # Check if blob data is available + # If not, this payload MAY be queued and subsequently considered when blob data becomes available + assert is_data_available(hash_tree_root(block), block.body.blob_kzg_commitments) + + # Check the block is valid and compute the post-state + # Make a copy of the state to avoid mutability issues + state = copy(store.block_states[block.parent_root]) + block_root = hash_tree_root(block) + state_transition(state, signed_block, True) + + # Add new block to the store + store.blocks[block_root] = block + # Add new state for this block to the store + store.block_states[block_root] = state + + record_block_timeliness(store, block_root) + update_proposer_boost_root(store, block_root) + + # Update checkpoints in store if necessary + update_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint) + + # Eagerly compute unrealized justification and finality. + compute_pulled_up_tip(store, block_root) + + +- name: on_block#fulu + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: '^\s+onBlock\(' + regex: true + spec: | + + def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: + """ + Run ``on_block`` upon receiving a new block. + """ + block = signed_block.message + # Parent block must be known + assert block.parent_root in store.block_states + # Make a copy of the state to avoid mutability issues + state = copy(store.block_states[block.parent_root]) + # Blocks cannot be in the future. If they are, their consideration must be delayed until they are in the past. + assert get_current_slot(store) >= block.slot + + # Check that block is later than the finalized epoch slot (optimization to reduce calls to get_ancestor) + finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) + assert block.slot > finalized_slot + # Check block is a descendant of the finalized block at the checkpoint finalized slot + finalized_checkpoint_block = get_checkpoint_block( + store, + block.parent_root, + store.finalized_checkpoint.epoch, + ) + assert store.finalized_checkpoint.root == finalized_checkpoint_block + + # [Modified in Fulu:EIP7594] + # Check if blob data is available + # If not, this payload MAY be queued and subsequently considered when blob data becomes available + assert is_data_available(hash_tree_root(block)) + + # Check the block is valid and compute the post-state + block_root = hash_tree_root(block) + state_transition(state, signed_block, True) + + # Add new block to the store + store.blocks[block_root] = block + # Add new state for this block to the store + store.block_states[block_root] = state + + record_block_timeliness(store, block_root) + update_proposer_boost_root(store, block_root) + + # Update checkpoints in store if necessary + update_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint) + + # Eagerly compute unrealized justification and finality. + compute_pulled_up_tip(store, block_root) + + +- name: on_block#gloas + sources: [] + spec: | + + def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: + """ + Run ``on_block`` upon receiving a new block. + """ + block = signed_block.message + # Parent block must be known + assert block.parent_root in store.block_states + + # Check if this blocks builds on empty or full parent block + parent_block = store.blocks[block.parent_root] + bid = block.body.signed_execution_payload_bid.message + parent_bid = parent_block.body.signed_execution_payload_bid.message + # Make a copy of the state to avoid mutability issues + if is_parent_node_full(store, block): + assert block.parent_root in store.execution_payload_states + state = copy(store.execution_payload_states[block.parent_root]) + else: + assert bid.parent_block_hash == parent_bid.parent_block_hash + state = copy(store.block_states[block.parent_root]) + + # Blocks cannot be in the future. If they are, their consideration must be delayed until they are in the past. + current_slot = get_current_slot(store) + assert current_slot >= block.slot + + # Check that block is later than the finalized epoch slot (optimization to reduce calls to get_ancestor) + finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) + assert block.slot > finalized_slot + # Check block is a descendant of the finalized block at the checkpoint finalized slot + finalized_checkpoint_block = get_checkpoint_block( + store, + block.parent_root, + store.finalized_checkpoint.epoch, + ) + assert store.finalized_checkpoint.root == finalized_checkpoint_block + + # Check the block is valid and compute the post-state + block_root = hash_tree_root(block) + state_transition(state, signed_block, True) + + # Add new block to the store + store.blocks[block_root] = block + # Add new state for this block to the store + store.block_states[block_root] = state + # Add a new PTC voting for this block to the store + store.ptc_vote[block_root] = [False] * PTC_SIZE + + # Notify the store about the payload_attestations in the block + notify_ptc_messages(store, state, block.body.payload_attestations) + + record_block_timeliness(store, block_root) + update_proposer_boost_root(store, block_root) + + # Update checkpoints in store if necessary + update_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint) + + # Eagerly compute unrealized justification and finality. + compute_pulled_up_tip(store, block_root) + + +- name: on_execution_payload#gloas + sources: [] + spec: | + + def on_execution_payload(store: Store, signed_envelope: SignedExecutionPayloadEnvelope) -> None: + """ + Run ``on_execution_payload`` upon receiving a new execution payload. + """ + envelope = signed_envelope.message + # The corresponding beacon block root needs to be known + assert envelope.beacon_block_root in store.block_states + + # Check if blob data is available + # If not, this payload MAY be queued and subsequently considered when blob data becomes available + assert is_data_available(envelope.beacon_block_root) + + # Make a copy of the state to avoid mutability issues + state = copy(store.block_states[envelope.beacon_block_root]) + + # Process the execution payload + process_execution_payload(state, signed_envelope, EXECUTION_ENGINE) + + # Add new state for this payload to the store + store.execution_payload_states[envelope.beacon_block_root] = state + + +- name: on_payload_attestation_message#gloas + sources: [] + spec: | + + def on_payload_attestation_message( + store: Store, ptc_message: PayloadAttestationMessage, is_from_block: bool = False + ) -> None: + """ + Run ``on_payload_attestation_message`` upon receiving a new ``ptc_message`` from + either within a block or directly on the wire. + """ + # The beacon block root must be known + data = ptc_message.data + # PTC attestation must be for a known block. If block is unknown, delay consideration until the block is found + state = store.block_states[data.beacon_block_root] + ptc = get_ptc(state, data.slot) + # PTC votes can only change the vote for their assigned beacon block, return early otherwise + if data.slot != state.slot: + return + # Check that the attester is from the PTC + assert ptc_message.validator_index in ptc + + # Verify the signature and check that its for the current slot if it is coming from the wire + if not is_from_block: + # Check that the attestation is for the current slot + assert data.slot == get_current_slot(store) + # Verify the signature + assert is_valid_indexed_payload_attestation( + state, + IndexedPayloadAttestation( + attesting_indices=[ptc_message.validator_index], + data=data, + signature=ptc_message.signature, + ), + ) + # Update the ptc vote for the block + ptc_index = ptc.index(ptc_message.validator_index) + ptc_vote = store.ptc_vote[data.beacon_block_root] + ptc_vote[ptc_index] = data.payload_present + + +- name: on_tick#phase0 + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: '^\s+private onTick\(' + regex: true + spec: | + + def on_tick(store: Store, time: uint64) -> None: + # If the ``store.time`` falls behind, while loop catches up slot by slot + # to ensure that every previous slot is processed with ``on_tick_per_slot`` + tick_slot = (time - store.genesis_time) // SECONDS_PER_SLOT + while get_current_slot(store) < tick_slot: + previous_time = store.genesis_time + (get_current_slot(store) + 1) * SECONDS_PER_SLOT + on_tick_per_slot(store, previous_time) + on_tick_per_slot(store, time) + + +- name: on_tick_per_slot#phase0 + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: '^\s+private onTick\(' + regex: true + spec: | + + def on_tick_per_slot(store: Store, time: uint64) -> None: + previous_slot = get_current_slot(store) + + # Update store time + store.time = time + + current_slot = get_current_slot(store) + + # If this is a new slot, reset store.proposer_boost_root + if current_slot > previous_slot: + store.proposer_boost_root = Root() + + # If a new epoch, pull-up justification and finalization from previous epoch + if current_slot > previous_slot and compute_slots_since_epoch_start(current_slot) == 0: + update_checkpoints( + store, store.unrealized_justified_checkpoint, store.unrealized_finalized_checkpoint + ) + + +- name: prepare_execution_payload#bellatrix + sources: + - file: packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts + search: export async function prepareExecutionPayload( + spec: | + + def prepare_execution_payload( + state: BeaconState, + safe_block_hash: Hash32, + finalized_block_hash: Hash32, + suggested_fee_recipient: ExecutionAddress, + execution_engine: ExecutionEngine, + pow_chain: Optional[Dict[Hash32, PowBlock]] = None, + ) -> Optional[PayloadId]: + if not is_merge_transition_complete(state): + assert pow_chain is not None + is_terminal_block_hash_set = TERMINAL_BLOCK_HASH != Hash32() + is_activation_epoch_reached = ( + get_current_epoch(state) >= TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH + ) + if is_terminal_block_hash_set and not is_activation_epoch_reached: + # Terminal block hash is set but activation epoch is not yet reached, no prepare payload call is needed + return None + + terminal_pow_block = get_terminal_pow_block(pow_chain) + if terminal_pow_block is None: + # Pre-merge, no prepare payload call is needed + return None + # Signify merge via producing on top of the terminal PoW block + parent_hash = terminal_pow_block.block_hash + else: + # Post-merge, normal payload + parent_hash = state.latest_execution_payload_header.block_hash + + # Set the forkchoice head and initiate the payload build process + payload_attributes = PayloadAttributes( + timestamp=compute_time_at_slot(state, state.slot), + prev_randao=get_randao_mix(state, get_current_epoch(state)), + suggested_fee_recipient=suggested_fee_recipient, + ) + return execution_engine.notify_forkchoice_updated( + head_block_hash=parent_hash, + safe_block_hash=safe_block_hash, + finalized_block_hash=finalized_block_hash, + payload_attributes=payload_attributes, + ) + + +- name: prepare_execution_payload#capella + sources: + - file: packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts + search: export async function prepareExecutionPayload( + spec: | + + def prepare_execution_payload( + state: BeaconState, + safe_block_hash: Hash32, + finalized_block_hash: Hash32, + suggested_fee_recipient: ExecutionAddress, + execution_engine: ExecutionEngine, + ) -> Optional[PayloadId]: + # [Modified in Capella] + # Removed `is_merge_transition_complete` check + parent_hash = state.latest_execution_payload_header.block_hash + + # Set the forkchoice head and initiate the payload build process + payload_attributes = PayloadAttributes( + timestamp=compute_time_at_slot(state, state.slot), + prev_randao=get_randao_mix(state, get_current_epoch(state)), + suggested_fee_recipient=suggested_fee_recipient, + # [New in Capella] + withdrawals=get_expected_withdrawals(state).withdrawals, + ) + return execution_engine.notify_forkchoice_updated( + head_block_hash=parent_hash, + safe_block_hash=safe_block_hash, + finalized_block_hash=finalized_block_hash, + payload_attributes=payload_attributes, + ) + + +- name: prepare_execution_payload#deneb + sources: + - file: packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts + search: export async function prepareExecutionPayload( + spec: | + + def prepare_execution_payload( + state: BeaconState, + safe_block_hash: Hash32, + finalized_block_hash: Hash32, + suggested_fee_recipient: ExecutionAddress, + execution_engine: ExecutionEngine, + ) -> Optional[PayloadId]: + # Verify consistency of the parent hash with respect to the previous execution payload header + parent_hash = state.latest_execution_payload_header.block_hash + + # Set the forkchoice head and initiate the payload build process + payload_attributes = PayloadAttributes( + timestamp=compute_time_at_slot(state, state.slot), + prev_randao=get_randao_mix(state, get_current_epoch(state)), + suggested_fee_recipient=suggested_fee_recipient, + withdrawals=get_expected_withdrawals(state).withdrawals, + # [New in Deneb:EIP4788] + parent_beacon_block_root=hash_tree_root(state.latest_block_header), + ) + return execution_engine.notify_forkchoice_updated( + head_block_hash=parent_hash, + safe_block_hash=safe_block_hash, + finalized_block_hash=finalized_block_hash, + payload_attributes=payload_attributes, + ) + + +- name: prepare_execution_payload#electra + sources: + - file: packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts + search: export async function prepareExecutionPayload( + spec: | + + def prepare_execution_payload( + state: BeaconState, + safe_block_hash: Hash32, + finalized_block_hash: Hash32, + suggested_fee_recipient: ExecutionAddress, + execution_engine: ExecutionEngine, + ) -> Optional[PayloadId]: + # Verify consistency of the parent hash with respect to the previous execution payload header + parent_hash = state.latest_execution_payload_header.block_hash + + # Set the forkchoice head and initiate the payload build process + payload_attributes = PayloadAttributes( + timestamp=compute_time_at_slot(state, state.slot), + prev_randao=get_randao_mix(state, get_current_epoch(state)), + suggested_fee_recipient=suggested_fee_recipient, + withdrawals=get_expected_withdrawals(state).withdrawals, + # [New in Deneb:EIP4788] + parent_beacon_block_root=hash_tree_root(state.latest_block_header), + ) + return execution_engine.notify_forkchoice_updated( + head_block_hash=parent_hash, + safe_block_hash=safe_block_hash, + finalized_block_hash=finalized_block_hash, + payload_attributes=payload_attributes, + ) + + +- name: prepare_execution_payload#gloas + sources: [] + spec: | + + def prepare_execution_payload( + state: BeaconState, + safe_block_hash: Hash32, + finalized_block_hash: Hash32, + suggested_fee_recipient: ExecutionAddress, + execution_engine: ExecutionEngine, + ) -> Optional[PayloadId]: + # Verify consistency of the parent hash with respect to the previous execution payload header + parent_hash = state.latest_execution_payload_header.block_hash + + # Set the forkchoice head and initiate the payload build process + payload_attributes = PayloadAttributes( + timestamp=compute_time_at_slot(state, state.slot), + prev_randao=get_randao_mix(state, get_current_epoch(state)), + suggested_fee_recipient=suggested_fee_recipient, + withdrawals=get_expected_withdrawals(state).withdrawals, + # [New in Deneb:EIP4788] + parent_beacon_block_root=hash_tree_root(state.latest_block_header), + ) + return execution_engine.notify_forkchoice_updated( + head_block_hash=parent_hash, + safe_block_hash=safe_block_hash, + finalized_block_hash=finalized_block_hash, + payload_attributes=payload_attributes, + ) + + +- name: process_attestation#phase0 + sources: + - file: packages/state-transition/src/block/processAttestationPhase0.ts + search: export function processAttestationPhase0( + spec: | + + def process_attestation(state: BeaconState, attestation: Attestation) -> None: + data = attestation.data + assert data.target.epoch in (get_previous_epoch(state), get_current_epoch(state)) + assert data.target.epoch == compute_epoch_at_slot(data.slot) + assert data.slot + MIN_ATTESTATION_INCLUSION_DELAY <= state.slot <= data.slot + SLOTS_PER_EPOCH + assert data.index < get_committee_count_per_slot(state, data.target.epoch) + + committee = get_beacon_committee(state, data.slot, data.index) + assert len(attestation.aggregation_bits) == len(committee) + + pending_attestation = PendingAttestation( + aggregation_bits=attestation.aggregation_bits, + data=data, + inclusion_delay=state.slot - data.slot, + proposer_index=get_beacon_proposer_index(state), + ) + + if data.target.epoch == get_current_epoch(state): + assert data.source == state.current_justified_checkpoint + state.current_epoch_attestations.append(pending_attestation) + else: + assert data.source == state.previous_justified_checkpoint + state.previous_epoch_attestations.append(pending_attestation) + + # Verify signature + assert is_valid_indexed_attestation(state, get_indexed_attestation(state, attestation)) + + +- name: process_attestation#altair + sources: + - file: packages/state-transition/src/block/processAttestationsAltair.ts + search: export function processAttestationsAltair( + spec: | + + def process_attestation(state: BeaconState, attestation: Attestation) -> None: + data = attestation.data + assert data.target.epoch in (get_previous_epoch(state), get_current_epoch(state)) + assert data.target.epoch == compute_epoch_at_slot(data.slot) + assert data.slot + MIN_ATTESTATION_INCLUSION_DELAY <= state.slot <= data.slot + SLOTS_PER_EPOCH + assert data.index < get_committee_count_per_slot(state, data.target.epoch) + + committee = get_beacon_committee(state, data.slot, data.index) + assert len(attestation.aggregation_bits) == len(committee) + + # Participation flag indices + participation_flag_indices = get_attestation_participation_flag_indices( + state, data, state.slot - data.slot + ) + + # Verify signature + assert is_valid_indexed_attestation(state, get_indexed_attestation(state, attestation)) + + # Update epoch participation flags + if data.target.epoch == get_current_epoch(state): + epoch_participation = state.current_epoch_participation + else: + epoch_participation = state.previous_epoch_participation + + proposer_reward_numerator = 0 + for index in get_attesting_indices(state, attestation): + for flag_index, weight in enumerate(PARTICIPATION_FLAG_WEIGHTS): + if flag_index in participation_flag_indices and not has_flag( + epoch_participation[index], flag_index + ): + epoch_participation[index] = add_flag(epoch_participation[index], flag_index) + proposer_reward_numerator += get_base_reward(state, index) * weight + + # Reward proposer + proposer_reward_denominator = ( + (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT) * WEIGHT_DENOMINATOR // PROPOSER_WEIGHT + ) + proposer_reward = Gwei(proposer_reward_numerator // proposer_reward_denominator) + increase_balance(state, get_beacon_proposer_index(state), proposer_reward) + + +- name: process_attestation#deneb + sources: + - file: packages/state-transition/src/block/processAttestationsAltair.ts + search: export function processAttestationsAltair( + spec: | + + def process_attestation(state: BeaconState, attestation: Attestation) -> None: + data = attestation.data + assert data.target.epoch in (get_previous_epoch(state), get_current_epoch(state)) + assert data.target.epoch == compute_epoch_at_slot(data.slot) + # [Modified in Deneb:EIP7045] + assert data.slot + MIN_ATTESTATION_INCLUSION_DELAY <= state.slot + assert data.index < get_committee_count_per_slot(state, data.target.epoch) + + committee = get_beacon_committee(state, data.slot, data.index) + assert len(attestation.aggregation_bits) == len(committee) + + # Participation flag indices + participation_flag_indices = get_attestation_participation_flag_indices( + state, data, state.slot - data.slot + ) + + # Verify signature + assert is_valid_indexed_attestation(state, get_indexed_attestation(state, attestation)) + + # Update epoch participation flags + if data.target.epoch == get_current_epoch(state): + epoch_participation = state.current_epoch_participation + else: + epoch_participation = state.previous_epoch_participation + + proposer_reward_numerator = 0 + for index in get_attesting_indices(state, attestation): + for flag_index, weight in enumerate(PARTICIPATION_FLAG_WEIGHTS): + if flag_index in participation_flag_indices and not has_flag( + epoch_participation[index], flag_index + ): + epoch_participation[index] = add_flag(epoch_participation[index], flag_index) + proposer_reward_numerator += get_base_reward(state, index) * weight + + # Reward proposer + proposer_reward_denominator = ( + (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT) * WEIGHT_DENOMINATOR // PROPOSER_WEIGHT + ) + proposer_reward = Gwei(proposer_reward_numerator // proposer_reward_denominator) + increase_balance(state, get_beacon_proposer_index(state), proposer_reward) + + +- name: process_attestation#electra + sources: + - file: packages/state-transition/src/block/processAttestationsAltair.ts + search: export function processAttestationsAltair( + spec: | + + def process_attestation(state: BeaconState, attestation: Attestation) -> None: + data = attestation.data + assert data.target.epoch in (get_previous_epoch(state), get_current_epoch(state)) + assert data.target.epoch == compute_epoch_at_slot(data.slot) + assert data.slot + MIN_ATTESTATION_INCLUSION_DELAY <= state.slot + + # [Modified in Electra:EIP7549] + assert data.index == 0 + committee_indices = get_committee_indices(attestation.committee_bits) + committee_offset = 0 + for committee_index in committee_indices: + assert committee_index < get_committee_count_per_slot(state, data.target.epoch) + committee = get_beacon_committee(state, data.slot, committee_index) + committee_attesters = set( + attester_index + for i, attester_index in enumerate(committee) + if attestation.aggregation_bits[committee_offset + i] + ) + assert len(committee_attesters) > 0 + committee_offset += len(committee) + + # Bitfield length matches total number of participants + assert len(attestation.aggregation_bits) == committee_offset + + # Participation flag indices + participation_flag_indices = get_attestation_participation_flag_indices( + state, data, state.slot - data.slot + ) + + # Verify signature + assert is_valid_indexed_attestation(state, get_indexed_attestation(state, attestation)) + + # Update epoch participation flags + if data.target.epoch == get_current_epoch(state): + epoch_participation = state.current_epoch_participation + else: + epoch_participation = state.previous_epoch_participation + + proposer_reward_numerator = 0 + for index in get_attesting_indices(state, attestation): + for flag_index, weight in enumerate(PARTICIPATION_FLAG_WEIGHTS): + if flag_index in participation_flag_indices and not has_flag( + epoch_participation[index], flag_index + ): + epoch_participation[index] = add_flag(epoch_participation[index], flag_index) + proposer_reward_numerator += get_base_reward(state, index) * weight + + # Reward proposer + proposer_reward_denominator = ( + (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT) * WEIGHT_DENOMINATOR // PROPOSER_WEIGHT + ) + proposer_reward = Gwei(proposer_reward_numerator // proposer_reward_denominator) + increase_balance(state, get_beacon_proposer_index(state), proposer_reward) + + +- name: process_attestation#gloas + sources: [] + spec: | + + def process_attestation(state: BeaconState, attestation: Attestation) -> None: + data = attestation.data + assert data.target.epoch in (get_previous_epoch(state), get_current_epoch(state)) + assert data.target.epoch == compute_epoch_at_slot(data.slot) + assert data.slot + MIN_ATTESTATION_INCLUSION_DELAY <= state.slot + + # [Modified in Gloas:EIP7732] + assert data.index < 2 + committee_indices = get_committee_indices(attestation.committee_bits) + committee_offset = 0 + for committee_index in committee_indices: + assert committee_index < get_committee_count_per_slot(state, data.target.epoch) + committee = get_beacon_committee(state, data.slot, committee_index) + committee_attesters = set( + attester_index + for i, attester_index in enumerate(committee) + if attestation.aggregation_bits[committee_offset + i] + ) + assert len(committee_attesters) > 0 + committee_offset += len(committee) + + # Bitfield length matches total number of participants + assert len(attestation.aggregation_bits) == committee_offset + + # Participation flag indices + participation_flag_indices = get_attestation_participation_flag_indices( + state, data, state.slot - data.slot + ) + + # Verify signature + assert is_valid_indexed_attestation(state, get_indexed_attestation(state, attestation)) + + # [Modified in Gloas:EIP7732] + if data.target.epoch == get_current_epoch(state): + current_epoch_target = True + epoch_participation = state.current_epoch_participation + payment = state.builder_pending_payments[SLOTS_PER_EPOCH + data.slot % SLOTS_PER_EPOCH] + else: + current_epoch_target = False + epoch_participation = state.previous_epoch_participation + payment = state.builder_pending_payments[data.slot % SLOTS_PER_EPOCH] + + proposer_reward_numerator = 0 + for index in get_attesting_indices(state, attestation): + # [New in Gloas:EIP7732] + # For same-slot attestations, check if we are setting any new flags. + # If we are, this validator has not contributed to this slot's quorum yet. + will_set_new_flag = False + + for flag_index, weight in enumerate(PARTICIPATION_FLAG_WEIGHTS): + if flag_index in participation_flag_indices and not has_flag( + epoch_participation[index], flag_index + ): + epoch_participation[index] = add_flag(epoch_participation[index], flag_index) + proposer_reward_numerator += get_base_reward(state, index) * weight + # [New in Gloas:EIP7732] + will_set_new_flag = True + + # [New in Gloas:EIP7732] + # Add weight for same-slot attestations when any new flag is set. + # This ensures each validator contributes exactly once per slot. + if ( + will_set_new_flag + and is_attestation_same_slot(state, data) + and payment.withdrawal.amount > 0 + ): + payment.weight += state.validators[index].effective_balance + + # Reward proposer + proposer_reward_denominator = ( + (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT) * WEIGHT_DENOMINATOR // PROPOSER_WEIGHT + ) + proposer_reward = Gwei(proposer_reward_numerator // proposer_reward_denominator) + increase_balance(state, get_beacon_proposer_index(state), proposer_reward) + + # [New in Gloas:EIP7732] + # Update builder payment weight + if current_epoch_target: + state.builder_pending_payments[SLOTS_PER_EPOCH + data.slot % SLOTS_PER_EPOCH] = payment + else: + state.builder_pending_payments[data.slot % SLOTS_PER_EPOCH] = payment + + +- name: process_attester_slashing#phase0 + sources: + - file: packages/state-transition/src/block/processAttesterSlashing.ts + search: export function processAttesterSlashing( + spec: | + + def process_attester_slashing(state: BeaconState, attester_slashing: AttesterSlashing) -> None: + attestation_1 = attester_slashing.attestation_1 + attestation_2 = attester_slashing.attestation_2 + assert is_slashable_attestation_data(attestation_1.data, attestation_2.data) + assert is_valid_indexed_attestation(state, attestation_1) + assert is_valid_indexed_attestation(state, attestation_2) + + slashed_any = False + indices = set(attestation_1.attesting_indices).intersection(attestation_2.attesting_indices) + for index in sorted(indices): + if is_slashable_validator(state.validators[index], get_current_epoch(state)): + slash_validator(state, index) + slashed_any = True + assert slashed_any + + +- name: process_block#phase0 + sources: + - file: packages/state-transition/src/block/index.ts + search: export function processBlock( + spec: | + + def process_block(state: BeaconState, block: BeaconBlock) -> None: + process_block_header(state, block) + process_randao(state, block.body) + process_eth1_data(state, block.body) + process_operations(state, block.body) + + +- name: process_block#altair + sources: + - file: packages/state-transition/src/block/index.ts + search: export function processBlock( + spec: | + + def process_block(state: BeaconState, block: BeaconBlock) -> None: + process_block_header(state, block) + process_randao(state, block.body) + process_eth1_data(state, block.body) + # [Modified in Altair] + process_operations(state, block.body) + # [New in Altair] + process_sync_aggregate(state, block.body.sync_aggregate) + + +- name: process_block#bellatrix + sources: + - file: packages/state-transition/src/block/index.ts + search: export function processBlock( + spec: | + + def process_block(state: BeaconState, block: BeaconBlock) -> None: + process_block_header(state, block) + if is_execution_enabled(state, block.body): + # [New in Bellatrix] + process_execution_payload(state, block.body, EXECUTION_ENGINE) + process_randao(state, block.body) + process_eth1_data(state, block.body) + process_operations(state, block.body) + process_sync_aggregate(state, block.body.sync_aggregate) + + +- name: process_block#capella + sources: + - file: packages/state-transition/src/block/index.ts + search: export function processBlock( + spec: | + + def process_block(state: BeaconState, block: BeaconBlock) -> None: + process_block_header(state, block) + # [Modified in Capella] + # Removed `is_execution_enabled` call + # [New in Capella] + process_withdrawals(state, block.body.execution_payload) + # [Modified in Capella] + process_execution_payload(state, block.body, EXECUTION_ENGINE) + process_randao(state, block.body) + process_eth1_data(state, block.body) + # [Modified in Capella] + process_operations(state, block.body) + process_sync_aggregate(state, block.body.sync_aggregate) + + +- name: process_block#electra + sources: + - file: packages/state-transition/src/block/index.ts + search: export function processBlock( + spec: | + + def process_block(state: BeaconState, block: BeaconBlock) -> None: + process_block_header(state, block) + # [Modified in Electra:EIP7251] + process_withdrawals(state, block.body.execution_payload) + # [Modified in Electra:EIP6110] + process_execution_payload(state, block.body, EXECUTION_ENGINE) + process_randao(state, block.body) + process_eth1_data(state, block.body) + # [Modified in Electra:EIP6110:EIP7002:EIP7549:EIP7251] + process_operations(state, block.body) + process_sync_aggregate(state, block.body.sync_aggregate) + + +- name: process_block#gloas + sources: [] + spec: | + + def process_block(state: BeaconState, block: BeaconBlock) -> None: + process_block_header(state, block) + # [Modified in Gloas:EIP7732] + process_withdrawals(state) + # [Modified in Gloas:EIP7732] + # Removed `process_execution_payload` + # [New in Gloas:EIP7732] + process_execution_payload_bid(state, block) + process_randao(state, block.body) + process_eth1_data(state, block.body) + # [Modified in Gloas:EIP7732] + process_operations(state, block.body) + process_sync_aggregate(state, block.body.sync_aggregate) + + +- name: process_block_header#phase0 + sources: + - file: packages/state-transition/src/block/processBlockHeader.ts + search: export function processBlockHeader( + spec: | + + def process_block_header(state: BeaconState, block: BeaconBlock) -> None: + # Verify that the slots match + assert block.slot == state.slot + # Verify that the block is newer than latest block header + assert block.slot > state.latest_block_header.slot + # Verify that proposer index is the correct index + assert block.proposer_index == get_beacon_proposer_index(state) + # Verify that the parent matches + assert block.parent_root == hash_tree_root(state.latest_block_header) + # Cache current block as the new latest block + state.latest_block_header = BeaconBlockHeader( + slot=block.slot, + proposer_index=block.proposer_index, + parent_root=block.parent_root, + state_root=Bytes32(), # Overwritten in the next process_slot call + body_root=hash_tree_root(block.body), + ) + + # Verify proposer is not slashed + proposer = state.validators[block.proposer_index] + assert not proposer.slashed + + +- name: process_bls_to_execution_change#capella + sources: + - file: packages/state-transition/src/block/processBlsToExecutionChange.ts + search: export function processBlsToExecutionChange( + spec: | + + def process_bls_to_execution_change( + state: BeaconState, signed_address_change: SignedBLSToExecutionChange + ) -> None: + address_change = signed_address_change.message + + assert address_change.validator_index < len(state.validators) + + validator = state.validators[address_change.validator_index] + + assert validator.withdrawal_credentials[:1] == BLS_WITHDRAWAL_PREFIX + assert validator.withdrawal_credentials[1:] == hash(address_change.from_bls_pubkey)[1:] + + # Fork-agnostic domain since address changes are valid across forks + domain = compute_domain( + DOMAIN_BLS_TO_EXECUTION_CHANGE, genesis_validators_root=state.genesis_validators_root + ) + signing_root = compute_signing_root(address_change, domain) + assert bls.Verify(address_change.from_bls_pubkey, signing_root, signed_address_change.signature) + + validator.withdrawal_credentials = ( + ETH1_ADDRESS_WITHDRAWAL_PREFIX + b"\x00" * 11 + address_change.to_execution_address + ) + + +- name: process_builder_pending_payments#gloas + sources: + - file: packages/state-transition/src/epoch/processBuilderPendingPayments.ts + search: export function processBuilderPendingPayments( + spec: | + + def process_builder_pending_payments(state: BeaconState) -> None: + """ + Processes the builder pending payments from the previous epoch. + """ + quorum = get_builder_payment_quorum_threshold(state) + for payment in state.builder_pending_payments[:SLOTS_PER_EPOCH]: + if payment.weight >= quorum: + state.builder_pending_withdrawals.append(payment.withdrawal) + + old_payments = state.builder_pending_payments[SLOTS_PER_EPOCH:] + new_payments = [BuilderPendingPayment() for _ in range(SLOTS_PER_EPOCH)] + state.builder_pending_payments = old_payments + new_payments + + +- name: process_consolidation_request#electra + sources: + - file: packages/state-transition/src/block/processConsolidationRequest.ts + search: export function processConsolidationRequest( + spec: | + + def process_consolidation_request( + state: BeaconState, consolidation_request: ConsolidationRequest + ) -> None: + if is_valid_switch_to_compounding_request(state, consolidation_request): + validator_pubkeys = [v.pubkey for v in state.validators] + request_source_pubkey = consolidation_request.source_pubkey + source_index = ValidatorIndex(validator_pubkeys.index(request_source_pubkey)) + switch_to_compounding_validator(state, source_index) + return + + # Verify that source != target, so a consolidation cannot be used as an exit + if consolidation_request.source_pubkey == consolidation_request.target_pubkey: + return + # If the pending consolidations queue is full, consolidation requests are ignored + if len(state.pending_consolidations) == PENDING_CONSOLIDATIONS_LIMIT: + return + # If there is too little available consolidation churn limit, consolidation requests are ignored + if get_consolidation_churn_limit(state) <= MIN_ACTIVATION_BALANCE: + return + + validator_pubkeys = [v.pubkey for v in state.validators] + # Verify pubkeys exists + request_source_pubkey = consolidation_request.source_pubkey + request_target_pubkey = consolidation_request.target_pubkey + if request_source_pubkey not in validator_pubkeys: + return + if request_target_pubkey not in validator_pubkeys: + return + source_index = ValidatorIndex(validator_pubkeys.index(request_source_pubkey)) + target_index = ValidatorIndex(validator_pubkeys.index(request_target_pubkey)) + source_validator = state.validators[source_index] + target_validator = state.validators[target_index] + + # Verify source withdrawal credentials + has_correct_credential = has_execution_withdrawal_credential(source_validator) + is_correct_source_address = ( + source_validator.withdrawal_credentials[12:] == consolidation_request.source_address + ) + if not (has_correct_credential and is_correct_source_address): + return + + # Verify that target has compounding withdrawal credentials + if not has_compounding_withdrawal_credential(target_validator): + return + + # Verify the source and the target are active + current_epoch = get_current_epoch(state) + if not is_active_validator(source_validator, current_epoch): + return + if not is_active_validator(target_validator, current_epoch): + return + # Verify exits for source and target have not been initiated + if source_validator.exit_epoch != FAR_FUTURE_EPOCH: + return + if target_validator.exit_epoch != FAR_FUTURE_EPOCH: + return + # Verify the source has been active long enough + if current_epoch < source_validator.activation_epoch + SHARD_COMMITTEE_PERIOD: + return + # Verify the source has no pending withdrawals in the queue + if get_pending_balance_to_withdraw(state, source_index) > 0: + return + + # Initiate source validator exit and append pending consolidation + source_validator.exit_epoch = compute_consolidation_epoch_and_update_churn( + state, source_validator.effective_balance + ) + source_validator.withdrawable_epoch = Epoch( + source_validator.exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY + ) + state.pending_consolidations.append( + PendingConsolidation(source_index=source_index, target_index=target_index) + ) + + +- name: process_deposit#phase0 + sources: + - file: packages/state-transition/src/block/processDeposit.ts + search: export function processDeposit( + spec: | + + def process_deposit(state: BeaconState, deposit: Deposit) -> None: + # Verify the Merkle branch + assert is_valid_merkle_branch( + leaf=hash_tree_root(deposit.data), + branch=deposit.proof, + # Add 1 for the List length mix-in + depth=DEPOSIT_CONTRACT_TREE_DEPTH + 1, + index=state.eth1_deposit_index, + root=state.eth1_data.deposit_root, + ) + + # Deposits must be processed in order + state.eth1_deposit_index += 1 + + apply_deposit( + state=state, + pubkey=deposit.data.pubkey, + withdrawal_credentials=deposit.data.withdrawal_credentials, + amount=deposit.data.amount, + signature=deposit.data.signature, + ) + + +- name: process_deposit#electra + sources: + - file: packages/state-transition/src/block/processDeposit.ts + search: export function processDeposit( + spec: | + + def process_deposit(state: BeaconState, deposit: Deposit) -> None: + # Verify the Merkle branch + assert is_valid_merkle_branch( + leaf=hash_tree_root(deposit.data), + branch=deposit.proof, + # Add 1 for the List length mix-in + depth=DEPOSIT_CONTRACT_TREE_DEPTH + 1, + index=state.eth1_deposit_index, + root=state.eth1_data.deposit_root, + ) + + # Deposits must be processed in order + state.eth1_deposit_index += 1 + + # [Modified in Electra:EIP7251] + apply_deposit( + state=state, + pubkey=deposit.data.pubkey, + withdrawal_credentials=deposit.data.withdrawal_credentials, + amount=deposit.data.amount, + signature=deposit.data.signature, + ) + + +- name: process_deposit_request#electra + sources: + - file: packages/state-transition/src/block/processDepositRequest.ts + search: export function processDepositRequest( + spec: | + + def process_deposit_request(state: BeaconState, deposit_request: DepositRequest) -> None: + # Set deposit request start index + if state.deposit_requests_start_index == UNSET_DEPOSIT_REQUESTS_START_INDEX: + state.deposit_requests_start_index = deposit_request.index + + # Create pending deposit + state.pending_deposits.append( + PendingDeposit( + pubkey=deposit_request.pubkey, + withdrawal_credentials=deposit_request.withdrawal_credentials, + amount=deposit_request.amount, + signature=deposit_request.signature, + slot=state.slot, + ) + ) + + +- name: process_deposit_request#gloas + sources: + - file: packages/state-transition/src/block/processDepositRequest.ts + search: export function processDepositRequest( + spec: | + + def process_deposit_request(state: BeaconState, deposit_request: DepositRequest) -> None: + # [New in Gloas:EIP7732] + builder_pubkeys = [b.pubkey for b in state.builders] + validator_pubkeys = [v.pubkey for v in state.validators] + + # [New in Gloas:EIP7732] + # Regardless of the withdrawal credentials prefix, if a builder/validator + # already exists with this pubkey, apply the deposit to their balance + is_builder = deposit_request.pubkey in builder_pubkeys + is_validator = deposit_request.pubkey in validator_pubkeys + is_builder_prefix = is_builder_withdrawal_credential(deposit_request.withdrawal_credentials) + if is_builder or (is_builder_prefix and not is_validator): + # Apply builder deposits immediately + apply_deposit_for_builder( + state, + deposit_request.pubkey, + deposit_request.withdrawal_credentials, + deposit_request.amount, + deposit_request.signature, + ) + return + + # Add validator deposits to the queue + state.pending_deposits.append( + PendingDeposit( + pubkey=deposit_request.pubkey, + withdrawal_credentials=deposit_request.withdrawal_credentials, + amount=deposit_request.amount, + signature=deposit_request.signature, + slot=state.slot, + ) + ) + + +- name: process_effective_balance_updates#phase0 + sources: + - file: packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts + search: export function processEffectiveBalanceUpdates( + spec: | + + def process_effective_balance_updates(state: BeaconState) -> None: + # Update effective balances with hysteresis + for index, validator in enumerate(state.validators): + balance = state.balances[index] + HYSTERESIS_INCREMENT = uint64(EFFECTIVE_BALANCE_INCREMENT // HYSTERESIS_QUOTIENT) + DOWNWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_DOWNWARD_MULTIPLIER + UPWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_UPWARD_MULTIPLIER + if ( + balance + DOWNWARD_THRESHOLD < validator.effective_balance + or validator.effective_balance + UPWARD_THRESHOLD < balance + ): + validator.effective_balance = min( + balance - balance % EFFECTIVE_BALANCE_INCREMENT, MAX_EFFECTIVE_BALANCE + ) + + +- name: process_effective_balance_updates#electra + sources: + - file: packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts + search: export function processEffectiveBalanceUpdates( + spec: | + + def process_effective_balance_updates(state: BeaconState) -> None: + # Update effective balances with hysteresis + for index, validator in enumerate(state.validators): + balance = state.balances[index] + HYSTERESIS_INCREMENT = uint64(EFFECTIVE_BALANCE_INCREMENT // HYSTERESIS_QUOTIENT) + DOWNWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_DOWNWARD_MULTIPLIER + UPWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_UPWARD_MULTIPLIER + # [Modified in Electra:EIP7251] + max_effective_balance = get_max_effective_balance(validator) + + if ( + balance + DOWNWARD_THRESHOLD < validator.effective_balance + or validator.effective_balance + UPWARD_THRESHOLD < balance + ): + validator.effective_balance = min( + balance - balance % EFFECTIVE_BALANCE_INCREMENT, max_effective_balance + ) + + +- name: process_epoch#phase0 + sources: + - file: packages/state-transition/src/epoch/index.ts + search: export function processEpoch( + spec: | + + def process_epoch(state: BeaconState) -> None: + process_justification_and_finalization(state) + process_rewards_and_penalties(state) + process_registry_updates(state) + process_slashings(state) + process_eth1_data_reset(state) + process_effective_balance_updates(state) + process_slashings_reset(state) + process_randao_mixes_reset(state) + process_historical_roots_update(state) + process_participation_record_updates(state) + + +- name: process_epoch#altair + sources: + - file: packages/state-transition/src/epoch/index.ts + search: export function processEpoch( + spec: | + + def process_epoch(state: BeaconState) -> None: + # [Modified in Altair] + process_justification_and_finalization(state) + # [New in Altair] + process_inactivity_updates(state) + # [Modified in Altair] + process_rewards_and_penalties(state) + process_registry_updates(state) + # [Modified in Altair] + process_slashings(state) + process_eth1_data_reset(state) + process_effective_balance_updates(state) + process_slashings_reset(state) + process_randao_mixes_reset(state) + process_historical_roots_update(state) + # [New in Altair] + process_participation_flag_updates(state) + # [New in Altair] + process_sync_committee_updates(state) + + +- name: process_epoch#capella + sources: + - file: packages/state-transition/src/epoch/index.ts + search: export function processEpoch( + spec: | + + def process_epoch(state: BeaconState) -> None: + process_justification_and_finalization(state) + process_inactivity_updates(state) + process_rewards_and_penalties(state) + process_registry_updates(state) + process_slashings(state) + process_eth1_data_reset(state) + process_effective_balance_updates(state) + process_slashings_reset(state) + process_randao_mixes_reset(state) + # [Modified in Capella] + process_historical_summaries_update(state) + process_participation_flag_updates(state) + process_sync_committee_updates(state) + + +- name: process_epoch#electra + sources: + - file: packages/state-transition/src/epoch/index.ts + search: export function processEpoch( + spec: | + + def process_epoch(state: BeaconState) -> None: + process_justification_and_finalization(state) + process_inactivity_updates(state) + process_rewards_and_penalties(state) + # [Modified in Electra:EIP7251] + process_registry_updates(state) + # [Modified in Electra:EIP7251] + process_slashings(state) + process_eth1_data_reset(state) + # [New in Electra:EIP7251] + process_pending_deposits(state) + # [New in Electra:EIP7251] + process_pending_consolidations(state) + # [Modified in Electra:EIP7251] + process_effective_balance_updates(state) + process_slashings_reset(state) + process_randao_mixes_reset(state) + process_historical_summaries_update(state) + process_participation_flag_updates(state) + process_sync_committee_updates(state) + + +- name: process_epoch#fulu + sources: + - file: packages/state-transition/src/epoch/index.ts + search: export function processEpoch( + spec: | + + def process_epoch(state: BeaconState) -> None: + process_justification_and_finalization(state) + process_inactivity_updates(state) + process_rewards_and_penalties(state) + process_registry_updates(state) + process_slashings(state) + process_eth1_data_reset(state) + process_pending_deposits(state) + process_pending_consolidations(state) + process_effective_balance_updates(state) + process_slashings_reset(state) + process_randao_mixes_reset(state) + process_historical_summaries_update(state) + process_participation_flag_updates(state) + process_sync_committee_updates(state) + # [New in Fulu:EIP7917] + process_proposer_lookahead(state) + + +- name: process_epoch#gloas + sources: [] + spec: | + + def process_epoch(state: BeaconState) -> None: + process_justification_and_finalization(state) + process_inactivity_updates(state) + process_rewards_and_penalties(state) + process_registry_updates(state) + process_slashings(state) + process_eth1_data_reset(state) + process_pending_deposits(state) + process_pending_consolidations(state) + # [New in Gloas:EIP7732] + process_builder_pending_payments(state) + process_effective_balance_updates(state) + process_slashings_reset(state) + process_randao_mixes_reset(state) + process_historical_summaries_update(state) + process_participation_flag_updates(state) + process_sync_committee_updates(state) + process_proposer_lookahead(state) + + +- name: process_eth1_data#phase0 + sources: + - file: packages/state-transition/src/block/processEth1Data.ts + search: export function processEth1Data + spec: | + + def process_eth1_data(state: BeaconState, body: BeaconBlockBody) -> None: + state.eth1_data_votes.append(body.eth1_data) + if ( + state.eth1_data_votes.count(body.eth1_data) * 2 + > EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH + ): + state.eth1_data = body.eth1_data + + +- name: process_eth1_data_reset#phase0 + sources: + - file: packages/state-transition/src/epoch/processEth1DataReset.ts + search: export function processEth1DataReset( + spec: | + + def process_eth1_data_reset(state: BeaconState) -> None: + next_epoch = Epoch(get_current_epoch(state) + 1) + # Reset eth1 data votes + if next_epoch % EPOCHS_PER_ETH1_VOTING_PERIOD == 0: + state.eth1_data_votes = [] + + +- name: process_execution_payload#bellatrix + sources: + - file: packages/state-transition/src/block/processExecutionPayload.ts + search: export function processExecutionPayload( + spec: | + + def process_execution_payload( + state: BeaconState, body: BeaconBlockBody, execution_engine: ExecutionEngine + ) -> None: + payload = body.execution_payload + + # Verify consistency of the parent hash with respect to the previous execution payload header + if is_merge_transition_complete(state): + assert payload.parent_hash == state.latest_execution_payload_header.block_hash + # Verify prev_randao + assert payload.prev_randao == get_randao_mix(state, get_current_epoch(state)) + # Verify timestamp + assert payload.timestamp == compute_time_at_slot(state, state.slot) + # Verify the execution payload is valid + assert execution_engine.verify_and_notify_new_payload( + NewPayloadRequest(execution_payload=payload) + ) + # Cache execution payload header + state.latest_execution_payload_header = ExecutionPayloadHeader( + parent_hash=payload.parent_hash, + fee_recipient=payload.fee_recipient, + state_root=payload.state_root, + receipts_root=payload.receipts_root, + logs_bloom=payload.logs_bloom, + prev_randao=payload.prev_randao, + block_number=payload.block_number, + gas_limit=payload.gas_limit, + gas_used=payload.gas_used, + timestamp=payload.timestamp, + extra_data=payload.extra_data, + base_fee_per_gas=payload.base_fee_per_gas, + block_hash=payload.block_hash, + transactions_root=hash_tree_root(payload.transactions), + ) + + +- name: process_execution_payload#capella + sources: + - file: packages/state-transition/src/block/processExecutionPayload.ts + search: export function processExecutionPayload( + spec: | + + def process_execution_payload( + state: BeaconState, body: BeaconBlockBody, execution_engine: ExecutionEngine + ) -> None: + payload = body.execution_payload + # [Modified in Capella] + # Removed `is_merge_transition_complete` check + # Verify consistency of the parent hash with respect to the previous execution payload header + assert payload.parent_hash == state.latest_execution_payload_header.block_hash + # Verify prev_randao + assert payload.prev_randao == get_randao_mix(state, get_current_epoch(state)) + # Verify timestamp + assert payload.timestamp == compute_time_at_slot(state, state.slot) + # Verify the execution payload is valid + assert execution_engine.verify_and_notify_new_payload( + NewPayloadRequest(execution_payload=payload) + ) + # Cache execution payload header + state.latest_execution_payload_header = ExecutionPayloadHeader( + parent_hash=payload.parent_hash, + fee_recipient=payload.fee_recipient, + state_root=payload.state_root, + receipts_root=payload.receipts_root, + logs_bloom=payload.logs_bloom, + prev_randao=payload.prev_randao, + block_number=payload.block_number, + gas_limit=payload.gas_limit, + gas_used=payload.gas_used, + timestamp=payload.timestamp, + extra_data=payload.extra_data, + base_fee_per_gas=payload.base_fee_per_gas, + block_hash=payload.block_hash, + transactions_root=hash_tree_root(payload.transactions), + # [New in Capella] + withdrawals_root=hash_tree_root(payload.withdrawals), + ) + + +- name: process_execution_payload#deneb + sources: + - file: packages/state-transition/src/block/processExecutionPayload.ts + search: export function processExecutionPayload( + spec: | + + def process_execution_payload( + state: BeaconState, body: BeaconBlockBody, execution_engine: ExecutionEngine + ) -> None: + payload = body.execution_payload + + # Verify consistency of the parent hash with respect to the previous execution payload header + assert payload.parent_hash == state.latest_execution_payload_header.block_hash + # Verify prev_randao + assert payload.prev_randao == get_randao_mix(state, get_current_epoch(state)) + # Verify timestamp + assert payload.timestamp == compute_time_at_slot(state, state.slot) + # [New in Deneb:EIP4844] + # Verify commitments are under limit + assert len(body.blob_kzg_commitments) <= MAX_BLOBS_PER_BLOCK + + # [New in Deneb:EIP4844] + # Compute list of versioned hashes + versioned_hashes = [ + kzg_commitment_to_versioned_hash(commitment) for commitment in body.blob_kzg_commitments + ] + + # Verify the execution payload is valid + assert execution_engine.verify_and_notify_new_payload( + NewPayloadRequest( + execution_payload=payload, + # [New in Deneb:EIP4844] + versioned_hashes=versioned_hashes, + # [New in Deneb:EIP4788] + parent_beacon_block_root=state.latest_block_header.parent_root, + ) + ) + + # Cache execution payload header + state.latest_execution_payload_header = ExecutionPayloadHeader( + parent_hash=payload.parent_hash, + fee_recipient=payload.fee_recipient, + state_root=payload.state_root, + receipts_root=payload.receipts_root, + logs_bloom=payload.logs_bloom, + prev_randao=payload.prev_randao, + block_number=payload.block_number, + gas_limit=payload.gas_limit, + gas_used=payload.gas_used, + timestamp=payload.timestamp, + extra_data=payload.extra_data, + base_fee_per_gas=payload.base_fee_per_gas, + block_hash=payload.block_hash, + transactions_root=hash_tree_root(payload.transactions), + withdrawals_root=hash_tree_root(payload.withdrawals), + # [New in Deneb:EIP4844] + blob_gas_used=payload.blob_gas_used, + # [New in Deneb:EIP4844] + excess_blob_gas=payload.excess_blob_gas, + ) + + +- name: process_execution_payload#electra + sources: + - file: packages/state-transition/src/block/processExecutionPayload.ts + search: export function processExecutionPayload( + spec: | + + def process_execution_payload( + state: BeaconState, body: BeaconBlockBody, execution_engine: ExecutionEngine + ) -> None: + payload = body.execution_payload + + # Verify consistency of the parent hash with respect to the previous execution payload header + assert payload.parent_hash == state.latest_execution_payload_header.block_hash + # Verify prev_randao + assert payload.prev_randao == get_randao_mix(state, get_current_epoch(state)) + # Verify timestamp + assert payload.timestamp == compute_time_at_slot(state, state.slot) + # [Modified in Electra:EIP7691] + # Verify commitments are under limit + assert len(body.blob_kzg_commitments) <= MAX_BLOBS_PER_BLOCK_ELECTRA + + # Compute list of versioned hashes + versioned_hashes = [ + kzg_commitment_to_versioned_hash(commitment) for commitment in body.blob_kzg_commitments + ] + + # Verify the execution payload is valid + assert execution_engine.verify_and_notify_new_payload( + NewPayloadRequest( + execution_payload=payload, + versioned_hashes=versioned_hashes, + parent_beacon_block_root=state.latest_block_header.parent_root, + # [New in Electra] + execution_requests=body.execution_requests, + ) + ) + + # Cache execution payload header + state.latest_execution_payload_header = ExecutionPayloadHeader( + parent_hash=payload.parent_hash, + fee_recipient=payload.fee_recipient, + state_root=payload.state_root, + receipts_root=payload.receipts_root, + logs_bloom=payload.logs_bloom, + prev_randao=payload.prev_randao, + block_number=payload.block_number, + gas_limit=payload.gas_limit, + gas_used=payload.gas_used, + timestamp=payload.timestamp, + extra_data=payload.extra_data, + base_fee_per_gas=payload.base_fee_per_gas, + block_hash=payload.block_hash, + transactions_root=hash_tree_root(payload.transactions), + withdrawals_root=hash_tree_root(payload.withdrawals), + blob_gas_used=payload.blob_gas_used, + excess_blob_gas=payload.excess_blob_gas, + ) + + +- name: process_execution_payload#fulu + sources: + - file: packages/state-transition/src/block/processExecutionPayload.ts + search: export function processExecutionPayload( + spec: | + + def process_execution_payload( + state: BeaconState, body: BeaconBlockBody, execution_engine: ExecutionEngine + ) -> None: + payload = body.execution_payload + + # Verify consistency of the parent hash with respect to the previous execution payload header + assert payload.parent_hash == state.latest_execution_payload_header.block_hash + # Verify prev_randao + assert payload.prev_randao == get_randao_mix(state, get_current_epoch(state)) + # Verify timestamp + assert payload.timestamp == compute_time_at_slot(state, state.slot) + # [Modified in Fulu:EIP7892] + # Verify commitments are under limit + assert ( + len(body.blob_kzg_commitments) + <= get_blob_parameters(get_current_epoch(state)).max_blobs_per_block + ) + + # Compute list of versioned hashes + versioned_hashes = [ + kzg_commitment_to_versioned_hash(commitment) for commitment in body.blob_kzg_commitments + ] + + # Verify the execution payload is valid + assert execution_engine.verify_and_notify_new_payload( + NewPayloadRequest( + execution_payload=payload, + versioned_hashes=versioned_hashes, + parent_beacon_block_root=state.latest_block_header.parent_root, + execution_requests=body.execution_requests, + ) + ) + + # Cache execution payload header + state.latest_execution_payload_header = ExecutionPayloadHeader( + parent_hash=payload.parent_hash, + fee_recipient=payload.fee_recipient, + state_root=payload.state_root, + receipts_root=payload.receipts_root, + logs_bloom=payload.logs_bloom, + prev_randao=payload.prev_randao, + block_number=payload.block_number, + gas_limit=payload.gas_limit, + gas_used=payload.gas_used, + timestamp=payload.timestamp, + extra_data=payload.extra_data, + base_fee_per_gas=payload.base_fee_per_gas, + block_hash=payload.block_hash, + transactions_root=hash_tree_root(payload.transactions), + withdrawals_root=hash_tree_root(payload.withdrawals), + blob_gas_used=payload.blob_gas_used, + excess_blob_gas=payload.excess_blob_gas, + ) + + +- name: process_execution_payload#gloas + sources: [] + spec: | + + def process_execution_payload( + state: BeaconState, + # [Modified in Gloas:EIP7732] + # Removed `body` + # [New in Gloas:EIP7732] + signed_envelope: SignedExecutionPayloadEnvelope, + execution_engine: ExecutionEngine, + # [New in Gloas:EIP7732] + verify: bool = True, + ) -> None: + envelope = signed_envelope.message + payload = envelope.payload + + # Verify signature + if verify: + assert verify_execution_payload_envelope_signature(state, signed_envelope) + + # Cache latest block header state root + previous_state_root = hash_tree_root(state) + if state.latest_block_header.state_root == Root(): + state.latest_block_header.state_root = previous_state_root + + # Verify consistency with the beacon block + assert envelope.beacon_block_root == hash_tree_root(state.latest_block_header) + assert envelope.slot == state.slot + + # Verify consistency with the committed bid + committed_bid = state.latest_execution_payload_bid + assert envelope.builder_index == committed_bid.builder_index + assert committed_bid.blob_kzg_commitments_root == hash_tree_root(envelope.blob_kzg_commitments) + assert committed_bid.prev_randao == payload.prev_randao + + # Verify consistency with expected withdrawals + assert hash_tree_root(payload.withdrawals) == hash_tree_root(state.payload_expected_withdrawals) + + # Verify the gas_limit + assert committed_bid.gas_limit == payload.gas_limit + # Verify the block hash + assert committed_bid.block_hash == payload.block_hash + # Verify consistency of the parent hash with respect to the previous execution payload + assert payload.parent_hash == state.latest_block_hash + # Verify timestamp + assert payload.timestamp == compute_time_at_slot(state, state.slot) + # Verify commitments are under limit + assert ( + len(envelope.blob_kzg_commitments) + <= get_blob_parameters(get_current_epoch(state)).max_blobs_per_block + ) + # Verify the execution payload is valid + versioned_hashes = [ + kzg_commitment_to_versioned_hash(commitment) for commitment in envelope.blob_kzg_commitments + ] + requests = envelope.execution_requests + assert execution_engine.verify_and_notify_new_payload( + NewPayloadRequest( + execution_payload=payload, + versioned_hashes=versioned_hashes, + parent_beacon_block_root=state.latest_block_header.parent_root, + execution_requests=requests, + ) + ) + + def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None: + for operation in operations: + fn(state, operation) + + for_ops(requests.deposits, process_deposit_request) + for_ops(requests.withdrawals, process_withdrawal_request) + for_ops(requests.consolidations, process_consolidation_request) + + # Queue the builder payment + payment = state.builder_pending_payments[SLOTS_PER_EPOCH + state.slot % SLOTS_PER_EPOCH] + amount = payment.withdrawal.amount + if amount > 0: + state.builder_pending_withdrawals.append(payment.withdrawal) + state.builder_pending_payments[SLOTS_PER_EPOCH + state.slot % SLOTS_PER_EPOCH] = ( + BuilderPendingPayment() + ) + + # Cache the execution payload hash + state.execution_payload_availability[state.slot % SLOTS_PER_HISTORICAL_ROOT] = 0b1 + state.latest_block_hash = payload.block_hash + + # Verify the state root + if verify: + assert envelope.state_root == hash_tree_root(state) + + +- name: process_execution_payload_bid#gloas + sources: + - file: packages/state-transition/src/block/processExecutionPayloadBid.ts + search: export function processExecutionPayloadBid( + spec: | + + def process_execution_payload_bid(state: BeaconState, block: BeaconBlock) -> None: + signed_bid = block.body.signed_execution_payload_bid + bid = signed_bid.message + builder_index = bid.builder_index + amount = bid.value + + # For self-builds, amount must be zero regardless of withdrawal credential prefix + if builder_index == BUILDER_INDEX_SELF_BUILD: + assert amount == 0 + assert signed_bid.signature == bls.G2_POINT_AT_INFINITY + else: + # Verify that the builder is active + assert is_active_builder(state, builder_index) + # Verify that the builder has funds to cover the bid + assert can_builder_cover_bid(state, builder_index, amount) + # Verify that the bid signature is valid + assert verify_execution_payload_bid_signature(state, signed_bid) + + # Verify that the bid is for the current slot + assert bid.slot == block.slot + # Verify that the bid is for the right parent block + assert bid.parent_block_hash == state.latest_block_hash + assert bid.parent_block_root == block.parent_root + assert bid.prev_randao == get_randao_mix(state, get_current_epoch(state)) + + # Record the pending payment if there is some payment + if amount > 0: + pending_payment = BuilderPendingPayment( + weight=0, + withdrawal=BuilderPendingWithdrawal( + fee_recipient=bid.fee_recipient, + amount=amount, + builder_index=builder_index, + ), + ) + state.builder_pending_payments[SLOTS_PER_EPOCH + bid.slot % SLOTS_PER_EPOCH] = ( + pending_payment + ) + + # Cache the signed execution payload bid + state.latest_execution_payload_bid = bid + + +- name: process_historical_roots_update#phase0 + sources: + - file: packages/state-transition/src/epoch/processHistoricalRootsUpdate.ts + search: export function processHistoricalRootsUpdate( + spec: | + + def process_historical_roots_update(state: BeaconState) -> None: + # Set historical root accumulator + next_epoch = Epoch(get_current_epoch(state) + 1) + if next_epoch % (SLOTS_PER_HISTORICAL_ROOT // SLOTS_PER_EPOCH) == 0: + historical_batch = HistoricalBatch( + block_roots=state.block_roots, state_roots=state.state_roots + ) + state.historical_roots.append(hash_tree_root(historical_batch)) + + +- name: process_historical_summaries_update#capella + sources: + - file: packages/state-transition/src/epoch/processHistoricalSummariesUpdate.ts + search: export function processHistoricalSummariesUpdate( + spec: | + + def process_historical_summaries_update(state: BeaconState) -> None: + # Set historical block root accumulator. + next_epoch = Epoch(get_current_epoch(state) + 1) + if next_epoch % (SLOTS_PER_HISTORICAL_ROOT // SLOTS_PER_EPOCH) == 0: + historical_summary = HistoricalSummary( + block_summary_root=hash_tree_root(state.block_roots), + state_summary_root=hash_tree_root(state.state_roots), + ) + state.historical_summaries.append(historical_summary) + + +- name: process_inactivity_updates#altair + sources: + - file: packages/state-transition/src/epoch/processInactivityUpdates.ts + search: export function processInactivityUpdates( + spec: | + + def process_inactivity_updates(state: BeaconState) -> None: + # Skip the genesis epoch as score updates are based on the previous epoch participation + if get_current_epoch(state) == GENESIS_EPOCH: + return + + for index in get_eligible_validator_indices(state): + # Increase the inactivity score of inactive validators + if index in get_unslashed_participating_indices( + state, TIMELY_TARGET_FLAG_INDEX, get_previous_epoch(state) + ): + state.inactivity_scores[index] -= min(1, state.inactivity_scores[index]) + else: + state.inactivity_scores[index] += INACTIVITY_SCORE_BIAS + # Decrease the inactivity score of all eligible validators during a leak-free epoch + if not is_in_inactivity_leak(state): + state.inactivity_scores[index] -= min( + INACTIVITY_SCORE_RECOVERY_RATE, state.inactivity_scores[index] + ) + + +- name: process_justification_and_finalization#phase0 + sources: + - file: packages/state-transition/src/epoch/processJustificationAndFinalization.ts + search: export function processJustificationAndFinalization( + spec: | + + def process_justification_and_finalization(state: BeaconState) -> None: + # Initial FFG checkpoint values have a `0x00` stub for `root`. + # Skip FFG updates in the first two epochs to avoid corner cases that might result in modifying this stub. + if get_current_epoch(state) <= GENESIS_EPOCH + 1: + return + previous_attestations = get_matching_target_attestations(state, get_previous_epoch(state)) + current_attestations = get_matching_target_attestations(state, get_current_epoch(state)) + total_active_balance = get_total_active_balance(state) + previous_target_balance = get_attesting_balance(state, previous_attestations) + current_target_balance = get_attesting_balance(state, current_attestations) + weigh_justification_and_finalization( + state, total_active_balance, previous_target_balance, current_target_balance + ) + + +- name: process_justification_and_finalization#altair + sources: + - file: packages/state-transition/src/epoch/processJustificationAndFinalization.ts + search: export function processJustificationAndFinalization( + spec: | + + def process_justification_and_finalization(state: BeaconState) -> None: + # Initial FFG checkpoint values have a `0x00` stub for `root`. + # Skip FFG updates in the first two epochs to avoid corner cases that might result in modifying this stub. + if get_current_epoch(state) <= GENESIS_EPOCH + 1: + return + previous_indices = get_unslashed_participating_indices( + state, TIMELY_TARGET_FLAG_INDEX, get_previous_epoch(state) + ) + current_indices = get_unslashed_participating_indices( + state, TIMELY_TARGET_FLAG_INDEX, get_current_epoch(state) + ) + total_active_balance = get_total_active_balance(state) + previous_target_balance = get_total_balance(state, previous_indices) + current_target_balance = get_total_balance(state, current_indices) + weigh_justification_and_finalization( + state, total_active_balance, previous_target_balance, current_target_balance + ) + + +- name: process_light_client_finality_update#altair + sources: [] + spec: | + + def process_light_client_finality_update( + store: LightClientStore, + finality_update: LightClientFinalityUpdate, + current_slot: Slot, + genesis_validators_root: Root, + ) -> None: + update = LightClientUpdate( + attested_header=finality_update.attested_header, + next_sync_committee=SyncCommittee(), + next_sync_committee_branch=NextSyncCommitteeBranch(), + finalized_header=finality_update.finalized_header, + finality_branch=finality_update.finality_branch, + sync_aggregate=finality_update.sync_aggregate, + signature_slot=finality_update.signature_slot, + ) + process_light_client_update(store, update, current_slot, genesis_validators_root) + + +- name: process_light_client_optimistic_update#altair + sources: [] + spec: | + + def process_light_client_optimistic_update( + store: LightClientStore, + optimistic_update: LightClientOptimisticUpdate, + current_slot: Slot, + genesis_validators_root: Root, + ) -> None: + update = LightClientUpdate( + attested_header=optimistic_update.attested_header, + next_sync_committee=SyncCommittee(), + next_sync_committee_branch=NextSyncCommitteeBranch(), + finalized_header=LightClientHeader(), + finality_branch=FinalityBranch(), + sync_aggregate=optimistic_update.sync_aggregate, + signature_slot=optimistic_update.signature_slot, + ) + process_light_client_update(store, update, current_slot, genesis_validators_root) + + +- name: process_light_client_store_force_update#altair + sources: [] + spec: | + + def process_light_client_store_force_update(store: LightClientStore, current_slot: Slot) -> None: + if ( + current_slot > store.finalized_header.beacon.slot + UPDATE_TIMEOUT + and store.best_valid_update is not None + ): + # Forced best update when the update timeout has elapsed. + # Because the apply logic waits for `finalized_header.beacon.slot` to indicate sync committee finality, + # the `attested_header` may be treated as `finalized_header` in extended periods of non-finality + # to guarantee progression into later sync committee periods according to `is_better_update`. + if ( + store.best_valid_update.finalized_header.beacon.slot + <= store.finalized_header.beacon.slot + ): + store.best_valid_update.finalized_header = store.best_valid_update.attested_header + apply_light_client_update(store, store.best_valid_update) + store.best_valid_update = None + + +- name: process_light_client_update#altair + sources: + - file: packages/light-client/src/spec/processLightClientUpdate.ts + search: export function processLightClientUpdate( + spec: | + + def process_light_client_update( + store: LightClientStore, + update: LightClientUpdate, + current_slot: Slot, + genesis_validators_root: Root, + ) -> None: + validate_light_client_update(store, update, current_slot, genesis_validators_root) + + sync_committee_bits = update.sync_aggregate.sync_committee_bits + + # Update the best update in case we have to force-update to it if the timeout elapses + if store.best_valid_update is None or is_better_update(update, store.best_valid_update): + store.best_valid_update = update + + # Track the maximum number of active participants in the committee signatures + store.current_max_active_participants = max( + store.current_max_active_participants, + sum(sync_committee_bits), + ) + + # Update the optimistic header + if ( + sum(sync_committee_bits) > get_safety_threshold(store) + and update.attested_header.beacon.slot > store.optimistic_header.beacon.slot + ): + store.optimistic_header = update.attested_header + + # Update finalized header + update_has_finalized_next_sync_committee = ( + not is_next_sync_committee_known(store) + and is_sync_committee_update(update) + and is_finality_update(update) + and ( + compute_sync_committee_period_at_slot(update.finalized_header.beacon.slot) + == compute_sync_committee_period_at_slot(update.attested_header.beacon.slot) + ) + ) + if sum(sync_committee_bits) * 3 >= len(sync_committee_bits) * 2 and ( + update.finalized_header.beacon.slot > store.finalized_header.beacon.slot + or update_has_finalized_next_sync_committee + ): + # Normal update through 2/3 threshold + apply_light_client_update(store, update) + store.best_valid_update = None + + +- name: process_operations#phase0 + sources: + - file: packages/state-transition/src/block/processOperations.ts + search: export function processOperations( + spec: | + + def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: + # Verify that outstanding deposits are processed up to the maximum number of deposits + assert len(body.deposits) == min( + MAX_DEPOSITS, state.eth1_data.deposit_count - state.eth1_deposit_index + ) + + def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None: + for operation in operations: + fn(state, operation) + + for_ops(body.proposer_slashings, process_proposer_slashing) + for_ops(body.attester_slashings, process_attester_slashing) + for_ops(body.attestations, process_attestation) + for_ops(body.deposits, process_deposit) + for_ops(body.voluntary_exits, process_voluntary_exit) + + +- name: process_operations#capella + sources: + - file: packages/state-transition/src/block/processOperations.ts + search: export function processOperations( + spec: | + + def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: + # Verify that outstanding deposits are processed up to the maximum number of deposits + assert len(body.deposits) == min( + MAX_DEPOSITS, state.eth1_data.deposit_count - state.eth1_deposit_index + ) + + def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None: + for operation in operations: + fn(state, operation) + + for_ops(body.proposer_slashings, process_proposer_slashing) + for_ops(body.attester_slashings, process_attester_slashing) + for_ops(body.attestations, process_attestation) + for_ops(body.deposits, process_deposit) + for_ops(body.voluntary_exits, process_voluntary_exit) + # [New in Capella] + for_ops(body.bls_to_execution_changes, process_bls_to_execution_change) + + +- name: process_operations#electra + sources: + - file: packages/state-transition/src/block/processOperations.ts + search: export function processOperations( + spec: | + + def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: + # [Modified in Electra:EIP6110] + # Disable former deposit mechanism once all prior deposits are processed + eth1_deposit_index_limit = min( + state.eth1_data.deposit_count, state.deposit_requests_start_index + ) + if state.eth1_deposit_index < eth1_deposit_index_limit: + assert len(body.deposits) == min( + MAX_DEPOSITS, eth1_deposit_index_limit - state.eth1_deposit_index + ) + else: + assert len(body.deposits) == 0 + + def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None: + for operation in operations: + fn(state, operation) + + for_ops(body.proposer_slashings, process_proposer_slashing) + for_ops(body.attester_slashings, process_attester_slashing) + # [Modified in Electra:EIP7549] + for_ops(body.attestations, process_attestation) + for_ops(body.deposits, process_deposit) + # [Modified in Electra:EIP7251] + for_ops(body.voluntary_exits, process_voluntary_exit) + for_ops(body.bls_to_execution_changes, process_bls_to_execution_change) + # [New in Electra:EIP6110] + for_ops(body.execution_requests.deposits, process_deposit_request) + # [New in Electra:EIP7002:EIP7251] + for_ops(body.execution_requests.withdrawals, process_withdrawal_request) + # [New in Electra:EIP7251] + for_ops(body.execution_requests.consolidations, process_consolidation_request) + + +- name: process_operations#gloas + sources: [] + spec: | + + def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: + # Disable former deposit mechanism once all prior deposits are processed + eth1_deposit_index_limit = min( + state.eth1_data.deposit_count, state.deposit_requests_start_index + ) + if state.eth1_deposit_index < eth1_deposit_index_limit: + assert len(body.deposits) == min( + MAX_DEPOSITS, eth1_deposit_index_limit - state.eth1_deposit_index + ) + else: + assert len(body.deposits) == 0 + + def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None: + for operation in operations: + fn(state, operation) + + # [Modified in Gloas:EIP7732] + for_ops(body.proposer_slashings, process_proposer_slashing) + for_ops(body.attester_slashings, process_attester_slashing) + # [Modified in Gloas:EIP7732] + for_ops(body.attestations, process_attestation) + for_ops(body.deposits, process_deposit) + # [Modified in Gloas:EIP7732] + for_ops(body.voluntary_exits, process_voluntary_exit) + for_ops(body.bls_to_execution_changes, process_bls_to_execution_change) + # [Modified in Gloas:EIP7732] + # Removed `process_deposit_request` + # [Modified in Gloas:EIP7732] + # Removed `process_withdrawal_request` + # [Modified in Gloas:EIP7732] + # Removed `process_consolidation_request` + # [New in Gloas:EIP7732] + for_ops(body.payload_attestations, process_payload_attestation) + + +- name: process_participation_flag_updates#altair + sources: + - file: packages/state-transition/src/epoch/processParticipationFlagUpdates.ts + search: export function processParticipationFlagUpdates( + spec: | + + def process_participation_flag_updates(state: BeaconState) -> None: + state.previous_epoch_participation = state.current_epoch_participation + state.current_epoch_participation = [ + ParticipationFlags(0b0000_0000) for _ in range(len(state.validators)) + ] + + +- name: process_participation_record_updates#phase0 + sources: + - file: packages/state-transition/src/epoch/processParticipationRecordUpdates.ts + search: export function processParticipationRecordUpdates( + spec: | + + def process_participation_record_updates(state: BeaconState) -> None: + # Rotate current/previous epoch attestations + state.previous_epoch_attestations = state.current_epoch_attestations + state.current_epoch_attestations = [] + + +- name: process_payload_attestation#gloas + sources: + - file: packages/state-transition/src/block/processPayloadAttestation.ts + search: export function processPayloadAttestation( + spec: | + + def process_payload_attestation( + state: BeaconState, payload_attestation: PayloadAttestation + ) -> None: + data = payload_attestation.data + + # Check that the attestation is for the parent beacon block + assert data.beacon_block_root == state.latest_block_header.parent_root + # Check that the attestation is for the previous slot + assert data.slot + 1 == state.slot + # Verify signature + indexed_payload_attestation = get_indexed_payload_attestation(state, payload_attestation) + assert is_valid_indexed_payload_attestation(state, indexed_payload_attestation) + + +- name: process_pending_consolidations#electra + sources: + - file: packages/state-transition/src/epoch/processPendingConsolidations.ts + search: export function processPendingConsolidations( + spec: | + + def process_pending_consolidations(state: BeaconState) -> None: + next_epoch = Epoch(get_current_epoch(state) + 1) + next_pending_consolidation = 0 + for pending_consolidation in state.pending_consolidations: + source_validator = state.validators[pending_consolidation.source_index] + if source_validator.slashed: + next_pending_consolidation += 1 + continue + if source_validator.withdrawable_epoch > next_epoch: + break + + # Calculate the consolidated balance + source_effective_balance = min( + state.balances[pending_consolidation.source_index], source_validator.effective_balance + ) + + # Move active balance to target. Excess balance is withdrawable. + decrease_balance(state, pending_consolidation.source_index, source_effective_balance) + increase_balance(state, pending_consolidation.target_index, source_effective_balance) + next_pending_consolidation += 1 + + state.pending_consolidations = state.pending_consolidations[next_pending_consolidation:] + + +- name: process_pending_deposits#electra + sources: + - file: packages/state-transition/src/epoch/processPendingDeposits.ts + search: export function processPendingDeposits( + spec: | + + def process_pending_deposits(state: BeaconState) -> None: + next_epoch = Epoch(get_current_epoch(state) + 1) + available_for_processing = state.deposit_balance_to_consume + get_activation_exit_churn_limit( + state + ) + processed_amount = 0 + next_deposit_index = 0 + deposits_to_postpone = [] + is_churn_limit_reached = False + finalized_slot = compute_start_slot_at_epoch(state.finalized_checkpoint.epoch) + + for deposit in state.pending_deposits: + # Do not process deposit requests if Eth1 bridge deposits are not yet applied. + if ( + # Is deposit request + deposit.slot > GENESIS_SLOT + and + # There are pending Eth1 bridge deposits + state.eth1_deposit_index < state.deposit_requests_start_index + ): + break + + # Check if deposit has been finalized, otherwise, stop processing. + if deposit.slot > finalized_slot: + break + + # Check if number of processed deposits has not reached the limit, otherwise, stop processing. + if next_deposit_index >= MAX_PENDING_DEPOSITS_PER_EPOCH: + break + + # Read validator state + is_validator_exited = False + is_validator_withdrawn = False + validator_pubkeys = [v.pubkey for v in state.validators] + if deposit.pubkey in validator_pubkeys: + validator = state.validators[ValidatorIndex(validator_pubkeys.index(deposit.pubkey))] + is_validator_exited = validator.exit_epoch < FAR_FUTURE_EPOCH + is_validator_withdrawn = validator.withdrawable_epoch < next_epoch + + if is_validator_withdrawn: + # Deposited balance will never become active. Increase balance but do not consume churn + apply_pending_deposit(state, deposit) + elif is_validator_exited: + # Validator is exiting, postpone the deposit until after withdrawable epoch + deposits_to_postpone.append(deposit) + else: + # Check if deposit fits in the churn, otherwise, do no more deposit processing in this epoch. + is_churn_limit_reached = processed_amount + deposit.amount > available_for_processing + if is_churn_limit_reached: + break + + # Consume churn and apply deposit. + processed_amount += deposit.amount + apply_pending_deposit(state, deposit) + + # Regardless of how the deposit was handled, we move on in the queue. + next_deposit_index += 1 + + state.pending_deposits = state.pending_deposits[next_deposit_index:] + deposits_to_postpone + + # Accumulate churn only if the churn limit has been hit. + if is_churn_limit_reached: + state.deposit_balance_to_consume = available_for_processing - processed_amount + else: + state.deposit_balance_to_consume = Gwei(0) + + +- name: process_proposer_lookahead#fulu + sources: + - file: packages/state-transition/src/epoch/processProposerLookahead.ts + search: export function processProposerLookahead( + spec: | + + def process_proposer_lookahead(state: BeaconState) -> None: + last_epoch_start = len(state.proposer_lookahead) - SLOTS_PER_EPOCH + # Shift out proposers in the first epoch + state.proposer_lookahead[:last_epoch_start] = state.proposer_lookahead[SLOTS_PER_EPOCH:] + # Fill in the last epoch with new proposer indices + last_epoch_proposers = get_beacon_proposer_indices( + state, Epoch(get_current_epoch(state) + MIN_SEED_LOOKAHEAD + 1) + ) + state.proposer_lookahead[last_epoch_start:] = last_epoch_proposers + + +- name: process_proposer_slashing#phase0 + sources: + - file: packages/state-transition/src/block/processProposerSlashing.ts + search: export function processProposerSlashing( + spec: | + + def process_proposer_slashing(state: BeaconState, proposer_slashing: ProposerSlashing) -> None: + header_1 = proposer_slashing.signed_header_1.message + header_2 = proposer_slashing.signed_header_2.message + + # Verify header slots match + assert header_1.slot == header_2.slot + # Verify header proposer indices match + assert header_1.proposer_index == header_2.proposer_index + # Verify the headers are different + assert header_1 != header_2 + # Verify the proposer is slashable + proposer = state.validators[header_1.proposer_index] + assert is_slashable_validator(proposer, get_current_epoch(state)) + # Verify signatures + for signed_header in (proposer_slashing.signed_header_1, proposer_slashing.signed_header_2): + domain = get_domain( + state, DOMAIN_BEACON_PROPOSER, compute_epoch_at_slot(signed_header.message.slot) + ) + signing_root = compute_signing_root(signed_header.message, domain) + assert bls.Verify(proposer.pubkey, signing_root, signed_header.signature) + + slash_validator(state, header_1.proposer_index) + + +- name: process_proposer_slashing#gloas + sources: [] + spec: | + + def process_proposer_slashing(state: BeaconState, proposer_slashing: ProposerSlashing) -> None: + header_1 = proposer_slashing.signed_header_1.message + header_2 = proposer_slashing.signed_header_2.message + + # Verify header slots match + assert header_1.slot == header_2.slot + # Verify header proposer indices match + assert header_1.proposer_index == header_2.proposer_index + # Verify the headers are different + assert header_1 != header_2 + # Verify the proposer is slashable + proposer = state.validators[header_1.proposer_index] + assert is_slashable_validator(proposer, get_current_epoch(state)) + # Verify signatures + for signed_header in (proposer_slashing.signed_header_1, proposer_slashing.signed_header_2): + domain = get_domain( + state, DOMAIN_BEACON_PROPOSER, compute_epoch_at_slot(signed_header.message.slot) + ) + signing_root = compute_signing_root(signed_header.message, domain) + assert bls.Verify(proposer.pubkey, signing_root, signed_header.signature) + + # [New in Gloas:EIP7732] + # Remove the BuilderPendingPayment corresponding to + # this proposal if it is still in the 2-epoch window. + slot = header_1.slot + proposal_epoch = compute_epoch_at_slot(slot) + if proposal_epoch == get_current_epoch(state): + payment_index = SLOTS_PER_EPOCH + slot % SLOTS_PER_EPOCH + state.builder_pending_payments[payment_index] = BuilderPendingPayment() + elif proposal_epoch == get_previous_epoch(state): + payment_index = slot % SLOTS_PER_EPOCH + state.builder_pending_payments[payment_index] = BuilderPendingPayment() + + slash_validator(state, header_1.proposer_index) + + +- name: process_randao#phase0 + sources: + - file: packages/state-transition/src/block/processRandao.ts + search: export function processRandao( + spec: | + + def process_randao(state: BeaconState, body: BeaconBlockBody) -> None: + epoch = get_current_epoch(state) + # Verify RANDAO reveal + proposer = state.validators[get_beacon_proposer_index(state)] + signing_root = compute_signing_root(epoch, get_domain(state, DOMAIN_RANDAO)) + assert bls.Verify(proposer.pubkey, signing_root, body.randao_reveal) + # Mix in RANDAO reveal + mix = xor(get_randao_mix(state, epoch), hash(body.randao_reveal)) + state.randao_mixes[epoch % EPOCHS_PER_HISTORICAL_VECTOR] = mix + + +- name: process_randao_mixes_reset#phase0 + sources: + - file: packages/state-transition/src/epoch/processRandaoMixesReset.ts + search: export function processRandaoMixesReset( + spec: | + + def process_randao_mixes_reset(state: BeaconState) -> None: + current_epoch = get_current_epoch(state) + next_epoch = Epoch(current_epoch + 1) + # Set randao mix + state.randao_mixes[next_epoch % EPOCHS_PER_HISTORICAL_VECTOR] = get_randao_mix( + state, current_epoch + ) + + +- name: process_registry_updates#phase0 + sources: + - file: packages/state-transition/src/epoch/processRegistryUpdates.ts + search: export function processRegistryUpdates( + spec: | + + def process_registry_updates(state: BeaconState) -> None: + # Process activation eligibility and ejections + for index, validator in enumerate(state.validators): + if is_eligible_for_activation_queue(validator): + validator.activation_eligibility_epoch = get_current_epoch(state) + 1 + + if ( + is_active_validator(validator, get_current_epoch(state)) + and validator.effective_balance <= EJECTION_BALANCE + ): + initiate_validator_exit(state, ValidatorIndex(index)) + + # Queue validators eligible for activation and not yet dequeued for activation + activation_queue = sorted( + [ + index + for index, validator in enumerate(state.validators) + if is_eligible_for_activation(state, validator) + ], + # Order by the sequence of activation_eligibility_epoch setting and then index + key=lambda index: (state.validators[index].activation_eligibility_epoch, index), + ) + # Dequeued validators for activation up to churn limit + for index in activation_queue[: get_validator_churn_limit(state)]: + validator = state.validators[index] + validator.activation_epoch = compute_activation_exit_epoch(get_current_epoch(state)) + + +- name: process_registry_updates#deneb + sources: + - file: packages/state-transition/src/epoch/processRegistryUpdates.ts + search: export function processRegistryUpdates( + spec: | + + def process_registry_updates(state: BeaconState) -> None: + # Process activation eligibility and ejections + for index, validator in enumerate(state.validators): + if is_eligible_for_activation_queue(validator): + validator.activation_eligibility_epoch = get_current_epoch(state) + 1 + + if ( + is_active_validator(validator, get_current_epoch(state)) + and validator.effective_balance <= EJECTION_BALANCE + ): + initiate_validator_exit(state, ValidatorIndex(index)) + + # Queue validators eligible for activation and not yet dequeued for activation + activation_queue = sorted( + [ + index + for index, validator in enumerate(state.validators) + if is_eligible_for_activation(state, validator) + ], + # Order by the sequence of activation_eligibility_epoch setting and then index + key=lambda index: (state.validators[index].activation_eligibility_epoch, index), + ) + # Dequeued validators for activation up to activation churn limit + # [Modified in Deneb:EIP7514] + for index in activation_queue[: get_validator_activation_churn_limit(state)]: + validator = state.validators[index] + validator.activation_epoch = compute_activation_exit_epoch(get_current_epoch(state)) + + +- name: process_registry_updates#electra + sources: + - file: packages/state-transition/src/epoch/processRegistryUpdates.ts + search: export function processRegistryUpdates( + spec: | + + def process_registry_updates(state: BeaconState) -> None: + current_epoch = get_current_epoch(state) + activation_epoch = compute_activation_exit_epoch(current_epoch) + + # Process activation eligibility, ejections, and activations + for index, validator in enumerate(state.validators): + # [Modified in Electra:EIP7251] + if is_eligible_for_activation_queue(validator): + validator.activation_eligibility_epoch = current_epoch + 1 + elif ( + is_active_validator(validator, current_epoch) + and validator.effective_balance <= EJECTION_BALANCE + ): + # [Modified in Electra:EIP7251] + initiate_validator_exit(state, ValidatorIndex(index)) + elif is_eligible_for_activation(state, validator): + validator.activation_epoch = activation_epoch + + +- name: process_rewards_and_penalties#phase0 + sources: + - file: packages/state-transition/src/epoch/processRewardsAndPenalties.ts + search: export function processRewardsAndPenalties( + spec: | + + def process_rewards_and_penalties(state: BeaconState) -> None: + # No rewards are applied at the end of `GENESIS_EPOCH` because rewards are for work done in the previous epoch + if get_current_epoch(state) == GENESIS_EPOCH: + return + + rewards, penalties = get_attestation_deltas(state) + for index in range(len(state.validators)): + increase_balance(state, ValidatorIndex(index), rewards[index]) + decrease_balance(state, ValidatorIndex(index), penalties[index]) + + +- name: process_rewards_and_penalties#altair + sources: + - file: packages/state-transition/src/epoch/processRewardsAndPenalties.ts + search: export function processRewardsAndPenalties( + spec: | + + def process_rewards_and_penalties(state: BeaconState) -> None: + # No rewards are applied at the end of `GENESIS_EPOCH` because rewards are for work done in the previous epoch + if get_current_epoch(state) == GENESIS_EPOCH: + return + + flag_deltas = [ + get_flag_index_deltas(state, flag_index) + for flag_index in range(len(PARTICIPATION_FLAG_WEIGHTS)) + ] + deltas = flag_deltas + [get_inactivity_penalty_deltas(state)] + for rewards, penalties in deltas: + for index in range(len(state.validators)): + increase_balance(state, ValidatorIndex(index), rewards[index]) + decrease_balance(state, ValidatorIndex(index), penalties[index]) + + +- name: process_slashings#phase0 + sources: + - file: packages/state-transition/src/epoch/processSlashings.ts + search: export function processSlashings( + spec: | + + def process_slashings(state: BeaconState) -> None: + epoch = get_current_epoch(state) + total_balance = get_total_active_balance(state) + adjusted_total_slashing_balance = min( + sum(state.slashings) * PROPORTIONAL_SLASHING_MULTIPLIER, total_balance + ) + for index, validator in enumerate(state.validators): + if ( + validator.slashed + and epoch + EPOCHS_PER_SLASHINGS_VECTOR // 2 == validator.withdrawable_epoch + ): + increment = EFFECTIVE_BALANCE_INCREMENT # Factored out from penalty numerator to avoid uint64 overflow + penalty_numerator = ( + validator.effective_balance // increment * adjusted_total_slashing_balance + ) + penalty = penalty_numerator // total_balance * increment + decrease_balance(state, ValidatorIndex(index), penalty) + + +- name: process_slashings#altair + sources: + - file: packages/state-transition/src/epoch/processSlashings.ts + search: export function processSlashings( + spec: | + + def process_slashings(state: BeaconState) -> None: + epoch = get_current_epoch(state) + total_balance = get_total_active_balance(state) + adjusted_total_slashing_balance = min( + sum(state.slashings) * PROPORTIONAL_SLASHING_MULTIPLIER_ALTAIR, total_balance + ) + for index, validator in enumerate(state.validators): + if ( + validator.slashed + and epoch + EPOCHS_PER_SLASHINGS_VECTOR // 2 == validator.withdrawable_epoch + ): + increment = EFFECTIVE_BALANCE_INCREMENT # Factored out from penalty numerator to avoid uint64 overflow + penalty_numerator = ( + validator.effective_balance // increment * adjusted_total_slashing_balance + ) + penalty = penalty_numerator // total_balance * increment + decrease_balance(state, ValidatorIndex(index), penalty) + + +- name: process_slashings#bellatrix + sources: + - file: packages/state-transition/src/epoch/processSlashings.ts + search: export function processSlashings( + spec: | + + def process_slashings(state: BeaconState) -> None: + epoch = get_current_epoch(state) + total_balance = get_total_active_balance(state) + adjusted_total_slashing_balance = min( + sum(state.slashings) + # [Modified in Bellatrix] + * PROPORTIONAL_SLASHING_MULTIPLIER_BELLATRIX, + total_balance, + ) + for index, validator in enumerate(state.validators): + if ( + validator.slashed + and epoch + EPOCHS_PER_SLASHINGS_VECTOR // 2 == validator.withdrawable_epoch + ): + increment = EFFECTIVE_BALANCE_INCREMENT # Factored out from penalty numerator to avoid uint64 overflow + penalty_numerator = ( + validator.effective_balance // increment * adjusted_total_slashing_balance + ) + penalty = penalty_numerator // total_balance * increment + decrease_balance(state, ValidatorIndex(index), penalty) + + +- name: process_slashings#electra + sources: + - file: packages/state-transition/src/epoch/processSlashings.ts + search: export function processSlashings( + spec: | + + def process_slashings(state: BeaconState) -> None: + epoch = get_current_epoch(state) + total_balance = get_total_active_balance(state) + adjusted_total_slashing_balance = min( + sum(state.slashings) * PROPORTIONAL_SLASHING_MULTIPLIER_BELLATRIX, total_balance + ) + increment = ( + EFFECTIVE_BALANCE_INCREMENT # Factored out from total balance to avoid uint64 overflow + ) + penalty_per_effective_balance_increment = adjusted_total_slashing_balance // ( + total_balance // increment + ) + for index, validator in enumerate(state.validators): + if ( + validator.slashed + and epoch + EPOCHS_PER_SLASHINGS_VECTOR // 2 == validator.withdrawable_epoch + ): + effective_balance_increments = validator.effective_balance // increment + # [Modified in Electra:EIP7251] + penalty = penalty_per_effective_balance_increment * effective_balance_increments + decrease_balance(state, ValidatorIndex(index), penalty) + + +- name: process_slashings_reset#phase0 + sources: + - file: packages/state-transition/src/epoch/processSlashingsReset.ts + search: export function processSlashingsReset( + spec: | + + def process_slashings_reset(state: BeaconState) -> None: + next_epoch = Epoch(get_current_epoch(state) + 1) + # Reset slashings + state.slashings[next_epoch % EPOCHS_PER_SLASHINGS_VECTOR] = Gwei(0) + + +- name: process_slot#phase0 + sources: + - file: packages/state-transition/src/slot/index.ts + search: export function processSlot( + spec: | + + def process_slot(state: BeaconState) -> None: + # Cache state root + previous_state_root = hash_tree_root(state) + state.state_roots[state.slot % SLOTS_PER_HISTORICAL_ROOT] = previous_state_root + # Cache latest block header state root + if state.latest_block_header.state_root == Bytes32(): + state.latest_block_header.state_root = previous_state_root + # Cache block root + previous_block_root = hash_tree_root(state.latest_block_header) + state.block_roots[state.slot % SLOTS_PER_HISTORICAL_ROOT] = previous_block_root + + +- name: process_slot#gloas + sources: [] + spec: | + + def process_slot(state: BeaconState) -> None: + # Cache state root + previous_state_root = hash_tree_root(state) + state.state_roots[state.slot % SLOTS_PER_HISTORICAL_ROOT] = previous_state_root + # Cache latest block header state root + if state.latest_block_header.state_root == Bytes32(): + state.latest_block_header.state_root = previous_state_root + # Cache block root + previous_block_root = hash_tree_root(state.latest_block_header) + state.block_roots[state.slot % SLOTS_PER_HISTORICAL_ROOT] = previous_block_root + # [New in Gloas:EIP7732] + # Unset the next payload availability + state.execution_payload_availability[(state.slot + 1) % SLOTS_PER_HISTORICAL_ROOT] = 0b0 + + +- name: process_slots#phase0 + sources: + - file: packages/state-transition/src/stateTransition.ts + search: export function processSlots( + spec: | + + def process_slots(state: BeaconState, slot: Slot) -> None: + assert state.slot < slot + while state.slot < slot: + process_slot(state) + # Process epoch on the start slot of the next epoch + if (state.slot + 1) % SLOTS_PER_EPOCH == 0: + process_epoch(state) + state.slot = Slot(state.slot + 1) + + +- name: process_sync_aggregate#altair + sources: + - file: packages/state-transition/src/block/processSyncCommittee.ts + search: export function processSyncAggregate( + spec: | + + def process_sync_aggregate(state: BeaconState, sync_aggregate: SyncAggregate) -> None: + # Verify sync committee aggregate signature signing over the previous slot block root + committee_pubkeys = state.current_sync_committee.pubkeys + committee_bits = sync_aggregate.sync_committee_bits + if sum(committee_bits) == SYNC_COMMITTEE_SIZE: + # All members participated - use precomputed aggregate key + participant_pubkeys = [state.current_sync_committee.aggregate_pubkey] + elif sum(committee_bits) > SYNC_COMMITTEE_SIZE // 2: + # More than half participated - subtract non-participant keys. + # First determine nonparticipating members + non_participant_pubkeys = [ + pubkey for pubkey, bit in zip(committee_pubkeys, committee_bits) if not bit + ] + # Compute aggregate of non-participants + non_participant_aggregate = eth_aggregate_pubkeys(non_participant_pubkeys) + # Subtract non-participants from the full aggregate + # This is equivalent to: aggregate_pubkey + (-non_participant_aggregate) + participant_pubkey = bls.add( + bls.bytes48_to_G1(state.current_sync_committee.aggregate_pubkey), + bls.neg(bls.bytes48_to_G1(non_participant_aggregate)), + ) + participant_pubkeys = [BLSPubkey(bls.G1_to_bytes48(participant_pubkey))] + else: + # Less than half participated - aggregate participant keys + participant_pubkeys = [ + pubkey + for pubkey, bit in zip(committee_pubkeys, sync_aggregate.sync_committee_bits) + if bit + ] + previous_slot = max(state.slot, Slot(1)) - Slot(1) + domain = get_domain(state, DOMAIN_SYNC_COMMITTEE, compute_epoch_at_slot(previous_slot)) + signing_root = compute_signing_root(get_block_root_at_slot(state, previous_slot), domain) + # Note: eth_fast_aggregate_verify works with a singleton list containing an aggregated key + assert eth_fast_aggregate_verify( + participant_pubkeys, signing_root, sync_aggregate.sync_committee_signature + ) + + # Compute participant and proposer rewards + total_active_increments = get_total_active_balance(state) // EFFECTIVE_BALANCE_INCREMENT + total_base_rewards = Gwei(get_base_reward_per_increment(state) * total_active_increments) + max_participant_rewards = Gwei( + total_base_rewards * SYNC_REWARD_WEIGHT // WEIGHT_DENOMINATOR // SLOTS_PER_EPOCH + ) + participant_reward = Gwei(max_participant_rewards // SYNC_COMMITTEE_SIZE) + proposer_reward = Gwei( + participant_reward * PROPOSER_WEIGHT // (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT) + ) + + # Apply participant and proposer rewards + all_pubkeys = [v.pubkey for v in state.validators] + committee_indices = [ + ValidatorIndex(all_pubkeys.index(pubkey)) for pubkey in state.current_sync_committee.pubkeys + ] + for participant_index, participation_bit in zip( + committee_indices, sync_aggregate.sync_committee_bits + ): + if participation_bit: + increase_balance(state, participant_index, participant_reward) + increase_balance(state, get_beacon_proposer_index(state), proposer_reward) + else: + decrease_balance(state, participant_index, participant_reward) + + +- name: process_sync_committee_contributions#altair + sources: + - file: packages/beacon-node/src/chain/opPools/syncContributionAndProofPool.ts + search: "* This is for producing blocks, the same to process_sync_committee_contributions in the spec." + spec: | + + def process_sync_committee_contributions( + block: BeaconBlock, contributions: Set[SyncCommitteeContribution] + ) -> None: + sync_aggregate = SyncAggregate() + signatures = [] + sync_subcommittee_size = SYNC_COMMITTEE_SIZE // SYNC_COMMITTEE_SUBNET_COUNT + + for contribution in contributions: + subcommittee_index = contribution.subcommittee_index + for index, participated in enumerate(contribution.aggregation_bits): + if participated: + participant_index = sync_subcommittee_size * subcommittee_index + index + sync_aggregate.sync_committee_bits[participant_index] = True + signatures.append(contribution.signature) + + sync_aggregate.sync_committee_signature = bls.Aggregate(signatures) + + block.body.sync_aggregate = sync_aggregate + + +- name: process_sync_committee_updates#altair + sources: + - file: packages/state-transition/src/epoch/processSyncCommitteeUpdates.ts + search: export function processSyncCommitteeUpdates( + spec: | + + def process_sync_committee_updates(state: BeaconState) -> None: + next_epoch = get_current_epoch(state) + Epoch(1) + if next_epoch % EPOCHS_PER_SYNC_COMMITTEE_PERIOD == 0: + state.current_sync_committee = state.next_sync_committee + state.next_sync_committee = get_next_sync_committee(state) + + +- name: process_voluntary_exit#phase0 + sources: + - file: packages/state-transition/src/block/processVoluntaryExit.ts + search: export function processVoluntaryExit( + spec: | + + def process_voluntary_exit(state: BeaconState, signed_voluntary_exit: SignedVoluntaryExit) -> None: + voluntary_exit = signed_voluntary_exit.message + validator = state.validators[voluntary_exit.validator_index] + # Verify the validator is active + assert is_active_validator(validator, get_current_epoch(state)) + # Verify exit has not been initiated + assert validator.exit_epoch == FAR_FUTURE_EPOCH + # Exits must specify an epoch when they become valid; they are not valid before then + assert get_current_epoch(state) >= voluntary_exit.epoch + # Verify the validator has been active long enough + assert get_current_epoch(state) >= validator.activation_epoch + SHARD_COMMITTEE_PERIOD + # Verify signature + domain = get_domain(state, DOMAIN_VOLUNTARY_EXIT, voluntary_exit.epoch) + signing_root = compute_signing_root(voluntary_exit, domain) + assert bls.Verify(validator.pubkey, signing_root, signed_voluntary_exit.signature) + # Initiate exit + initiate_validator_exit(state, voluntary_exit.validator_index) + + +- name: process_voluntary_exit#deneb + sources: + - file: packages/state-transition/src/block/processVoluntaryExit.ts + search: export function processVoluntaryExit( + spec: | + + def process_voluntary_exit(state: BeaconState, signed_voluntary_exit: SignedVoluntaryExit) -> None: + voluntary_exit = signed_voluntary_exit.message + validator = state.validators[voluntary_exit.validator_index] + # Verify the validator is active + assert is_active_validator(validator, get_current_epoch(state)) + # Verify exit has not been initiated + assert validator.exit_epoch == FAR_FUTURE_EPOCH + # Exits must specify an epoch when they become valid; they are not valid before then + assert get_current_epoch(state) >= voluntary_exit.epoch + # Verify the validator has been active long enough + assert get_current_epoch(state) >= validator.activation_epoch + SHARD_COMMITTEE_PERIOD + # Verify signature + # [Modified in Deneb:EIP7044] + domain = compute_domain( + DOMAIN_VOLUNTARY_EXIT, CAPELLA_FORK_VERSION, state.genesis_validators_root + ) + signing_root = compute_signing_root(voluntary_exit, domain) + assert bls.Verify(validator.pubkey, signing_root, signed_voluntary_exit.signature) + # Initiate exit + initiate_validator_exit(state, voluntary_exit.validator_index) + + +- name: process_voluntary_exit#electra + sources: + - file: packages/state-transition/src/block/processVoluntaryExit.ts + search: export function processVoluntaryExit( + spec: | + + def process_voluntary_exit(state: BeaconState, signed_voluntary_exit: SignedVoluntaryExit) -> None: + voluntary_exit = signed_voluntary_exit.message + validator = state.validators[voluntary_exit.validator_index] + # Verify the validator is active + assert is_active_validator(validator, get_current_epoch(state)) + # Verify exit has not been initiated + assert validator.exit_epoch == FAR_FUTURE_EPOCH + # Exits must specify an epoch when they become valid; they are not valid before then + assert get_current_epoch(state) >= voluntary_exit.epoch + # Verify the validator has been active long enough + assert get_current_epoch(state) >= validator.activation_epoch + SHARD_COMMITTEE_PERIOD + # [New in Electra:EIP7251] + # Only exit validator if it has no pending withdrawals in the queue + assert get_pending_balance_to_withdraw(state, voluntary_exit.validator_index) == 0 + # Verify signature + domain = compute_domain( + DOMAIN_VOLUNTARY_EXIT, CAPELLA_FORK_VERSION, state.genesis_validators_root + ) + signing_root = compute_signing_root(voluntary_exit, domain) + assert bls.Verify(validator.pubkey, signing_root, signed_voluntary_exit.signature) + # Initiate exit + initiate_validator_exit(state, voluntary_exit.validator_index) + + +- name: process_voluntary_exit#gloas + sources: + - file: packages/state-transition/src/block/processVoluntaryExit.ts + search: export function processVoluntaryExit( + spec: | + + def process_voluntary_exit(state: BeaconState, signed_voluntary_exit: SignedVoluntaryExit) -> None: + voluntary_exit = signed_voluntary_exit.message + domain = compute_domain( + DOMAIN_VOLUNTARY_EXIT, CAPELLA_FORK_VERSION, state.genesis_validators_root + ) + signing_root = compute_signing_root(voluntary_exit, domain) + + # Exits must specify an epoch when they become valid; they are not valid before then + assert get_current_epoch(state) >= voluntary_exit.epoch + + # [New in Gloas:EIP7732] + if is_builder_index(voluntary_exit.validator_index): + builder_index = convert_validator_index_to_builder_index(voluntary_exit.validator_index) + # Verify the builder is active + assert is_active_builder(state, builder_index) + # Only exit builder if it has no pending withdrawals in the queue + assert get_pending_balance_to_withdraw_for_builder(state, builder_index) == 0 + # Verify signature + pubkey = state.builders[builder_index].pubkey + assert bls.Verify(pubkey, signing_root, signed_voluntary_exit.signature) + # Initiate exit + initiate_builder_exit(state, builder_index) + return + + validator = state.validators[voluntary_exit.validator_index] + # Verify the validator is active + assert is_active_validator(validator, get_current_epoch(state)) + # Verify exit has not been initiated + assert validator.exit_epoch == FAR_FUTURE_EPOCH + # Verify the validator has been active long enough + assert get_current_epoch(state) >= validator.activation_epoch + SHARD_COMMITTEE_PERIOD + # Only exit validator if it has no pending withdrawals in the queue + assert get_pending_balance_to_withdraw(state, voluntary_exit.validator_index) == 0 + # Verify signature + assert bls.Verify(validator.pubkey, signing_root, signed_voluntary_exit.signature) + # Initiate exit + initiate_validator_exit(state, voluntary_exit.validator_index) + + +- name: process_withdrawal_request#electra + sources: + - file: packages/state-transition/src/block/processWithdrawalRequest.ts + search: export function processWithdrawalRequest( + spec: | + + def process_withdrawal_request(state: BeaconState, withdrawal_request: WithdrawalRequest) -> None: + amount = withdrawal_request.amount + is_full_exit_request = amount == FULL_EXIT_REQUEST_AMOUNT + + # If partial withdrawal queue is full, only full exits are processed + if ( + len(state.pending_partial_withdrawals) == PENDING_PARTIAL_WITHDRAWALS_LIMIT + and not is_full_exit_request + ): + return + + validator_pubkeys = [v.pubkey for v in state.validators] + # Verify pubkey exists + request_pubkey = withdrawal_request.validator_pubkey + if request_pubkey not in validator_pubkeys: + return + index = ValidatorIndex(validator_pubkeys.index(request_pubkey)) + validator = state.validators[index] + + # Verify withdrawal credentials + has_correct_credential = has_execution_withdrawal_credential(validator) + is_correct_source_address = ( + validator.withdrawal_credentials[12:] == withdrawal_request.source_address + ) + if not (has_correct_credential and is_correct_source_address): + return + # Verify the validator is active + if not is_active_validator(validator, get_current_epoch(state)): + return + # Verify exit has not been initiated + if validator.exit_epoch != FAR_FUTURE_EPOCH: + return + # Verify the validator has been active long enough + if get_current_epoch(state) < validator.activation_epoch + SHARD_COMMITTEE_PERIOD: + return + + pending_balance_to_withdraw = get_pending_balance_to_withdraw(state, index) + + if is_full_exit_request: + # Only exit validator if it has no pending withdrawals in the queue + if pending_balance_to_withdraw == 0: + initiate_validator_exit(state, index) + return + + has_sufficient_effective_balance = validator.effective_balance >= MIN_ACTIVATION_BALANCE + has_excess_balance = ( + state.balances[index] > MIN_ACTIVATION_BALANCE + pending_balance_to_withdraw + ) + + # Only allow partial withdrawals with compounding withdrawal credentials + if ( + has_compounding_withdrawal_credential(validator) + and has_sufficient_effective_balance + and has_excess_balance + ): + to_withdraw = min( + state.balances[index] - MIN_ACTIVATION_BALANCE - pending_balance_to_withdraw, amount + ) + exit_queue_epoch = compute_exit_epoch_and_update_churn(state, to_withdraw) + withdrawable_epoch = Epoch(exit_queue_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY) + state.pending_partial_withdrawals.append( + PendingPartialWithdrawal( + validator_index=index, + amount=to_withdraw, + withdrawable_epoch=withdrawable_epoch, + ) + ) + + +- name: process_withdrawals#capella + sources: + - file: packages/state-transition/src/block/processWithdrawals.ts + search: export function processWithdrawals( + spec: | + + def process_withdrawals(state: BeaconState, payload: ExecutionPayload) -> None: + # Get expected withdrawals + expected = get_expected_withdrawals(state) + assert payload.withdrawals == expected.withdrawals + + # Apply expected withdrawals + apply_withdrawals(state, expected.withdrawals) + + # Update withdrawals fields in the state + update_next_withdrawal_index(state, expected.withdrawals) + update_next_withdrawal_validator_index(state, expected.withdrawals) + + +- name: process_withdrawals#electra + sources: + - file: packages/state-transition/src/block/processWithdrawals.ts + search: export function processWithdrawals( + spec: | + + def process_withdrawals(state: BeaconState, payload: ExecutionPayload) -> None: + # Get expected withdrawals + expected = get_expected_withdrawals(state) + assert payload.withdrawals == expected.withdrawals + + # Apply expected withdrawals + apply_withdrawals(state, expected.withdrawals) + + # Update withdrawals fields in the state + update_next_withdrawal_index(state, expected.withdrawals) + # [New in Electra:EIP7251] + update_pending_partial_withdrawals(state, expected.processed_partial_withdrawals_count) + update_next_withdrawal_validator_index(state, expected.withdrawals) + + +- name: process_withdrawals#gloas + sources: [] + spec: | + + def process_withdrawals( + state: BeaconState, + # [Modified in Gloas:EIP7732] + # Removed `payload` + ) -> None: + # [New in Gloas:EIP7732] + # Return early if the parent block is empty + if not is_parent_block_full(state): + return + + # Get expected withdrawals + expected = get_expected_withdrawals(state) + + # Apply expected withdrawals + apply_withdrawals(state, expected.withdrawals) + + # Update withdrawals fields in the state + update_next_withdrawal_index(state, expected.withdrawals) + # [New in Gloas:EIP7732] + update_payload_expected_withdrawals(state, expected.withdrawals) + # [New in Gloas:EIP7732] + update_builder_pending_withdrawals(state, expected.processed_builder_withdrawals_count) + update_pending_partial_withdrawals(state, expected.processed_partial_withdrawals_count) + # [New in Gloas:EIP7732] + update_next_withdrawal_builder_index(state, expected.processed_builders_sweep_count) + update_next_withdrawal_validator_index(state, expected.withdrawals) + + +- name: queue_excess_active_balance#electra + sources: + - file: packages/state-transition/src/util/electra.ts + search: export function queueExcessActiveBalance( + spec: | + + def queue_excess_active_balance(state: BeaconState, index: ValidatorIndex) -> None: + balance = state.balances[index] + if balance > MIN_ACTIVATION_BALANCE: + excess_balance = balance - MIN_ACTIVATION_BALANCE + state.balances[index] = MIN_ACTIVATION_BALANCE + validator = state.validators[index] + # Use bls.G2_POINT_AT_INFINITY as a signature field placeholder + # and GENESIS_SLOT to distinguish from a pending deposit request + state.pending_deposits.append( + PendingDeposit( + pubkey=validator.pubkey, + withdrawal_credentials=validator.withdrawal_credentials, + amount=excess_balance, + signature=bls.G2_POINT_AT_INFINITY, + slot=GENESIS_SLOT, + ) + ) + + +- name: record_block_timeliness#phase0 + sources: [] + spec: | + + def record_block_timeliness(store: Store, root: Root) -> None: + block = store.blocks[root] + seconds_since_genesis = store.time - store.genesis_time + time_into_slot_ms = seconds_to_milliseconds(seconds_since_genesis) % SLOT_DURATION_MS + epoch = get_current_store_epoch(store) + attestation_threshold_ms = get_attestation_due_ms(epoch) + is_before_attesting_interval = time_into_slot_ms < attestation_threshold_ms + is_timely = get_current_slot(store) == block.slot and is_before_attesting_interval + store.block_timeliness[root] = is_timely + + +- name: record_block_timeliness#gloas + sources: [] + spec: | + + def record_block_timeliness(store: Store, root: Root) -> None: + block = store.blocks[root] + seconds_since_genesis = store.time - store.genesis_time + time_into_slot_ms = seconds_to_milliseconds(seconds_since_genesis) % SLOT_DURATION_MS + epoch = get_current_store_epoch(store) + attestation_threshold_ms = get_attestation_due_ms(epoch) + # [New in Gloas:EIP7732] + is_current_slot = get_current_slot(store) == block.slot + ptc_threshold_ms = get_payload_attestation_due_ms(epoch) + # [Modified in Gloas:EIP7732] + store.block_timeliness[root] = [ + is_current_slot and time_into_slot_ms < threshold + for threshold in [attestation_threshold_ms, ptc_threshold_ms] + ] + + +- name: recover_matrix#fulu + sources: + - file: packages/beacon-node/src/util/blobs.ts + search: export async function dataColumnMatrixRecovery( + spec: | + + def recover_matrix( + partial_matrix: Sequence[MatrixEntry], blob_count: uint64 + ) -> Sequence[MatrixEntry]: + """ + Recover the full, flattened sequence of matrix entries. + + This helper demonstrates how to apply ``recover_cells_and_kzg_proofs``. + The data structure for storing cells/proofs is implementation-dependent. + """ + matrix = [] + for blob_index in range(blob_count): + cell_indices = [e.column_index for e in partial_matrix if e.row_index == blob_index] + cells = [e.cell for e in partial_matrix if e.row_index == blob_index] + recovered_cells, recovered_proofs = recover_cells_and_kzg_proofs(cell_indices, cells) + for cell_index, (cell, proof) in enumerate(zip(recovered_cells, recovered_proofs)): + matrix.append( + MatrixEntry( + cell=cell, + kzg_proof=proof, + column_index=cell_index, + row_index=blob_index, + ) + ) + return matrix + + +- name: saturating_sub#phase0 + sources: [] + spec: | + + def saturating_sub(a: int, b: int) -> int: + """ + Computes a - b, saturating at numeric bounds. + """ + return a - b if a > b else 0 + + +- name: seconds_to_milliseconds#phase0 + sources: [] + spec: | + + def seconds_to_milliseconds(seconds: uint64) -> uint64: + """ + Convert seconds to milliseconds with overflow protection. + Returns ``UINT64_MAX`` if the result would overflow. + """ + if seconds > UINT64_MAX // 1000: + return UINT64_MAX + return seconds * 1000 + + +- name: set_or_append_list#altair + sources: [] + spec: | + + def set_or_append_list(list: List, index: ValidatorIndex, value: Any) -> None: + if index == len(list): + list.append(value) + else: + list[index] = value + + +- name: should_apply_proposer_boost#gloas + sources: [] + spec: | + + def should_apply_proposer_boost(store: Store) -> bool: + if store.proposer_boost_root == Root(): + return False + + block = store.blocks[store.proposer_boost_root] + parent_root = block.parent_root + parent = store.blocks[parent_root] + slot = block.slot + + # Apply proposer boost if `parent` is not from the previous slot + if parent.slot + 1 < slot: + return True + + # Apply proposer boost if `parent` is not weak + if not is_head_weak(store, parent_root): + return True + + # If `parent` is weak and from the previous slot, apply + # proposer boost if there are no early equivocations + equivocations = [ + root + for root, block in store.blocks.items() + if ( + store.block_timeliness[root][PTC_TIMELINESS_INDEX] + and block.proposer_index == parent.proposer_index + and block.slot + 1 == slot + and root != parent_root + ) + ] + + return len(equivocations) == 0 + + +- name: should_extend_payload#gloas + sources: [] + spec: | + + def should_extend_payload(store: Store, root: Root) -> bool: + proposer_root = store.proposer_boost_root + return ( + is_payload_timely(store, root) + or proposer_root == Root() + or store.blocks[proposer_root].parent_root != root + or is_parent_node_full(store, store.blocks[proposer_root]) + ) + + +- name: should_override_forkchoice_update#bellatrix + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: "// See https://github.com/ethereum/consensus-specs/blob/v1.5.0/specs/bellatrix/fork-choice.md#should_override_forkchoice_update" + spec: | + + def should_override_forkchoice_update(store: Store, head_root: Root) -> bool: + head_block = store.blocks[head_root] + parent_root = head_block.parent_root + parent_block = store.blocks[parent_root] + current_slot = get_current_slot(store) + proposal_slot = head_block.slot + Slot(1) + + # Only re-org the head_block block if it arrived later than the attestation deadline. + head_late = is_head_late(store, head_root) + + # Shuffling stable. + shuffling_stable = is_shuffling_stable(proposal_slot) + + # FFG information of the new head_block will be competitive with the current head. + ffg_competitive = is_ffg_competitive(store, head_root, parent_root) + + # Do not re-org if the chain is not finalizing with acceptable frequency. + finalization_ok = is_finalization_ok(store, proposal_slot) + + # Only suppress the fork choice update if we are confident that we will propose the next block. + parent_state_advanced = store.block_states[parent_root].copy() + process_slots(parent_state_advanced, proposal_slot) + proposer_index = get_beacon_proposer_index(parent_state_advanced) + proposing_reorg_slot = validator_is_connected(proposer_index) + + # Single slot re-org. + parent_slot_ok = parent_block.slot + 1 == head_block.slot + proposing_on_time = is_proposing_on_time(store) + + # Note that this condition is different from `get_proposer_head` + current_time_ok = head_block.slot == current_slot or ( + proposal_slot == current_slot and proposing_on_time + ) + single_slot_reorg = parent_slot_ok and current_time_ok + + # Check the head weight only if the attestations from the head slot have already been applied. + # Implementations may want to do this in different ways, e.g. by advancing + # `store.time` early, or by counting queued attestations during the head block's slot. + if current_slot > head_block.slot: + head_weak = is_head_weak(store, head_root) + parent_strong = is_parent_strong(store, head_root) + else: + head_weak = True + parent_strong = True + + return all( + [ + head_late, + shuffling_stable, + ffg_competitive, + finalization_ok, + proposing_reorg_slot, + single_slot_reorg, + head_weak, + parent_strong, + ] + ) + + +- name: slash_validator#phase0 + sources: + - file: packages/state-transition/src/block/slashValidator.ts + search: export function slashValidator( + spec: | + + def slash_validator( + state: BeaconState, slashed_index: ValidatorIndex, whistleblower_index: ValidatorIndex = None + ) -> None: + """ + Slash the validator with index ``slashed_index``. + """ + epoch = get_current_epoch(state) + initiate_validator_exit(state, slashed_index) + validator = state.validators[slashed_index] + validator.slashed = True + validator.withdrawable_epoch = max( + validator.withdrawable_epoch, Epoch(epoch + EPOCHS_PER_SLASHINGS_VECTOR) + ) + state.slashings[epoch % EPOCHS_PER_SLASHINGS_VECTOR] += validator.effective_balance + decrease_balance( + state, slashed_index, validator.effective_balance // MIN_SLASHING_PENALTY_QUOTIENT + ) + + # Apply proposer and whistleblower rewards + proposer_index = get_beacon_proposer_index(state) + if whistleblower_index is None: + whistleblower_index = proposer_index + whistleblower_reward = Gwei(validator.effective_balance // WHISTLEBLOWER_REWARD_QUOTIENT) + proposer_reward = Gwei(whistleblower_reward // PROPOSER_REWARD_QUOTIENT) + increase_balance(state, proposer_index, proposer_reward) + increase_balance(state, whistleblower_index, Gwei(whistleblower_reward - proposer_reward)) + + +- name: slash_validator#altair + sources: + - file: packages/state-transition/src/block/slashValidator.ts + search: export function slashValidator( + spec: | + + def slash_validator( + state: BeaconState, slashed_index: ValidatorIndex, whistleblower_index: ValidatorIndex = None + ) -> None: + """ + Slash the validator with index ``slashed_index``. + """ + epoch = get_current_epoch(state) + initiate_validator_exit(state, slashed_index) + validator = state.validators[slashed_index] + validator.slashed = True + validator.withdrawable_epoch = max( + validator.withdrawable_epoch, Epoch(epoch + EPOCHS_PER_SLASHINGS_VECTOR) + ) + state.slashings[epoch % EPOCHS_PER_SLASHINGS_VECTOR] += validator.effective_balance + decrease_balance( + state, slashed_index, validator.effective_balance // MIN_SLASHING_PENALTY_QUOTIENT_ALTAIR + ) + + # Apply proposer and whistleblower rewards + proposer_index = get_beacon_proposer_index(state) + if whistleblower_index is None: + whistleblower_index = proposer_index + whistleblower_reward = Gwei(validator.effective_balance // WHISTLEBLOWER_REWARD_QUOTIENT) + proposer_reward = Gwei(whistleblower_reward * PROPOSER_WEIGHT // WEIGHT_DENOMINATOR) + increase_balance(state, proposer_index, proposer_reward) + increase_balance(state, whistleblower_index, Gwei(whistleblower_reward - proposer_reward)) + + +- name: slash_validator#bellatrix + sources: + - file: packages/state-transition/src/block/slashValidator.ts + search: export function slashValidator( + spec: | + + def slash_validator( + state: BeaconState, slashed_index: ValidatorIndex, whistleblower_index: ValidatorIndex = None + ) -> None: + """ + Slash the validator with index ``slashed_index``. + """ + epoch = get_current_epoch(state) + initiate_validator_exit(state, slashed_index) + validator = state.validators[slashed_index] + validator.slashed = True + validator.withdrawable_epoch = max( + validator.withdrawable_epoch, Epoch(epoch + EPOCHS_PER_SLASHINGS_VECTOR) + ) + state.slashings[epoch % EPOCHS_PER_SLASHINGS_VECTOR] += validator.effective_balance + # [Modified in Bellatrix] + slashing_penalty = validator.effective_balance // MIN_SLASHING_PENALTY_QUOTIENT_BELLATRIX + decrease_balance(state, slashed_index, slashing_penalty) + + # Apply proposer and whistleblower rewards + proposer_index = get_beacon_proposer_index(state) + if whistleblower_index is None: + whistleblower_index = proposer_index + whistleblower_reward = Gwei(validator.effective_balance // WHISTLEBLOWER_REWARD_QUOTIENT) + proposer_reward = Gwei(whistleblower_reward * PROPOSER_WEIGHT // WEIGHT_DENOMINATOR) + increase_balance(state, proposer_index, proposer_reward) + increase_balance(state, whistleblower_index, Gwei(whistleblower_reward - proposer_reward)) + + +- name: slash_validator#electra + sources: + - file: packages/state-transition/src/block/slashValidator.ts + search: export function slashValidator( + spec: | + + def slash_validator( + state: BeaconState, slashed_index: ValidatorIndex, whistleblower_index: ValidatorIndex = None + ) -> None: + """ + Slash the validator with index ``slashed_index``. + """ + epoch = get_current_epoch(state) + initiate_validator_exit(state, slashed_index) + validator = state.validators[slashed_index] + validator.slashed = True + validator.withdrawable_epoch = max( + validator.withdrawable_epoch, Epoch(epoch + EPOCHS_PER_SLASHINGS_VECTOR) + ) + state.slashings[epoch % EPOCHS_PER_SLASHINGS_VECTOR] += validator.effective_balance + # [Modified in Electra:EIP7251] + slashing_penalty = validator.effective_balance // MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA + decrease_balance(state, slashed_index, slashing_penalty) + + # Apply proposer and whistleblower rewards + proposer_index = get_beacon_proposer_index(state) + if whistleblower_index is None: + whistleblower_index = proposer_index + # [Modified in Electra:EIP7251] + whistleblower_reward = Gwei( + validator.effective_balance // WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA + ) + proposer_reward = Gwei(whistleblower_reward * PROPOSER_WEIGHT // WEIGHT_DENOMINATOR) + increase_balance(state, proposer_index, proposer_reward) + increase_balance(state, whistleblower_index, Gwei(whistleblower_reward - proposer_reward)) + + +- name: state_transition#phase0 + sources: + - file: packages/state-transition/src/stateTransition.ts + search: export function stateTransition( + spec: | + + def state_transition( + state: BeaconState, signed_block: SignedBeaconBlock, validate_result: bool = True + ) -> None: + block = signed_block.message + # Process slots (including those with no blocks) since block + process_slots(state, block.slot) + # Verify signature + if validate_result: + assert verify_block_signature(state, signed_block) + # Process block + process_block(state, block) + # Verify state root + if validate_result: + assert block.state_root == hash_tree_root(state) + + +- name: store_target_checkpoint_state#phase0 + sources: [] + spec: | + + def store_target_checkpoint_state(store: Store, target: Checkpoint) -> None: + # Store target checkpoint state if not yet seen + if target not in store.checkpoint_states: + base_state = copy(store.block_states[target.root]) + if base_state.slot < compute_start_slot_at_epoch(target.epoch): + process_slots(base_state, compute_start_slot_at_epoch(target.epoch)) + store.checkpoint_states[target] = base_state + + +- name: switch_to_compounding_validator#electra + sources: + - file: packages/state-transition/src/util/electra.ts + search: export function switchToCompoundingValidator( + spec: | + + def switch_to_compounding_validator(state: BeaconState, index: ValidatorIndex) -> None: + validator = state.validators[index] + validator.withdrawal_credentials = ( + COMPOUNDING_WITHDRAWAL_PREFIX + validator.withdrawal_credentials[1:] + ) + queue_excess_active_balance(state, index) + + +- name: translate_participation#altair + sources: + - file: packages/state-transition/src/slot/upgradeStateToAltair.ts + search: function translateParticipation( + spec: | + + def translate_participation( + state: BeaconState, pending_attestations: Sequence[phase0.PendingAttestation] + ) -> None: + for attestation in pending_attestations: + data = attestation.data + inclusion_delay = attestation.inclusion_delay + # Translate attestation inclusion info to flag indices + participation_flag_indices = get_attestation_participation_flag_indices( + state, data, inclusion_delay + ) + + # Apply flags to all attesting validators + epoch_participation = state.previous_epoch_participation + for index in get_attesting_indices(state, attestation): + for flag_index in participation_flag_indices: + epoch_participation[index] = add_flag(epoch_participation[index], flag_index) + + +- name: update_builder_pending_withdrawals#gloas + sources: [] + spec: | + + def update_builder_pending_withdrawals( + state: BeaconState, processed_builder_withdrawals_count: uint64 + ) -> None: + state.builder_pending_withdrawals = state.builder_pending_withdrawals[ + processed_builder_withdrawals_count: + ] + + +- name: update_checkpoints#phase0 + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: '^\s+private updateCheckpoints\(' + regex: true + spec: | + + def update_checkpoints( + store: Store, justified_checkpoint: Checkpoint, finalized_checkpoint: Checkpoint + ) -> None: + """ + Update checkpoints in store if necessary + """ + # Update justified checkpoint + if justified_checkpoint.epoch > store.justified_checkpoint.epoch: + store.justified_checkpoint = justified_checkpoint + + # Update finalized checkpoint + if finalized_checkpoint.epoch > store.finalized_checkpoint.epoch: + store.finalized_checkpoint = finalized_checkpoint + + +- name: update_latest_messages#phase0 + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: private addLatestMessage( + spec: | + + def update_latest_messages( + store: Store, attesting_indices: Sequence[ValidatorIndex], attestation: Attestation + ) -> None: + target = attestation.data.target + beacon_block_root = attestation.data.beacon_block_root + non_equivocating_attesting_indices = [ + i for i in attesting_indices if i not in store.equivocating_indices + ] + for i in non_equivocating_attesting_indices: + if i not in store.latest_messages or target.epoch > store.latest_messages[i].epoch: + store.latest_messages[i] = LatestMessage(epoch=target.epoch, root=beacon_block_root) + + +- name: update_latest_messages#gloas + sources: [] + spec: | + + def update_latest_messages( + store: Store, attesting_indices: Sequence[ValidatorIndex], attestation: Attestation + ) -> None: + slot = attestation.data.slot + beacon_block_root = attestation.data.beacon_block_root + payload_present = attestation.data.index == 1 + non_equivocating_attesting_indices = [ + i for i in attesting_indices if i not in store.equivocating_indices + ] + for i in non_equivocating_attesting_indices: + if i not in store.latest_messages or slot > store.latest_messages[i].slot: + store.latest_messages[i] = LatestMessage( + slot=slot, root=beacon_block_root, payload_present=payload_present + ) + + +- name: update_next_withdrawal_builder_index#gloas + sources: [] + spec: | + + def update_next_withdrawal_builder_index( + state: BeaconState, processed_builders_sweep_count: uint64 + ) -> None: + if len(state.builders) > 0: + # Update the next builder index to start the next withdrawal sweep + next_index = state.next_withdrawal_builder_index + processed_builders_sweep_count + next_builder_index = BuilderIndex(next_index % len(state.builders)) + state.next_withdrawal_builder_index = next_builder_index + + +- name: update_next_withdrawal_index#capella + sources: [] + spec: | + + def update_next_withdrawal_index(state: BeaconState, withdrawals: Sequence[Withdrawal]) -> None: + # Update the next withdrawal index if this block contained withdrawals + if len(withdrawals) != 0: + latest_withdrawal = withdrawals[-1] + state.next_withdrawal_index = WithdrawalIndex(latest_withdrawal.index + 1) + + +- name: update_next_withdrawal_validator_index#capella + sources: [] + spec: | + + def update_next_withdrawal_validator_index( + state: BeaconState, withdrawals: Sequence[Withdrawal] + ) -> None: + # Update the next validator index to start the next withdrawal sweep + if len(withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD: + # Next sweep starts after the latest withdrawal's validator index + next_validator_index = ValidatorIndex( + (withdrawals[-1].validator_index + 1) % len(state.validators) + ) + state.next_withdrawal_validator_index = next_validator_index + else: + # Advance sweep by the max length of the sweep if there was not a full set of withdrawals + next_index = state.next_withdrawal_validator_index + MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP + next_validator_index = ValidatorIndex(next_index % len(state.validators)) + state.next_withdrawal_validator_index = next_validator_index + + +- name: update_payload_expected_withdrawals#gloas + sources: [] + spec: | + + def update_payload_expected_withdrawals( + state: BeaconState, withdrawals: Sequence[Withdrawal] + ) -> None: + state.payload_expected_withdrawals = List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD](withdrawals) + + +- name: update_pending_partial_withdrawals#electra + sources: [] + spec: | + + def update_pending_partial_withdrawals( + state: BeaconState, processed_partial_withdrawals_count: uint64 + ) -> None: + state.pending_partial_withdrawals = state.pending_partial_withdrawals[ + processed_partial_withdrawals_count: + ] + + +- name: update_proposer_boost_root#phase0 + sources: [] + spec: | + + def update_proposer_boost_root(store: Store, root: Root) -> None: + is_first_block = store.proposer_boost_root == Root() + is_timely = store.block_timeliness[root] + + # Add proposer score boost if the block is timely, not conflicting with an + # existing block, with the same the proposer as the canonical chain. + if is_timely and is_first_block: + head_state = copy(store.block_states[get_head(store)]) + slot = get_current_slot(store) + if head_state.slot < slot: + process_slots(head_state, slot) + block = store.blocks[root] + # Only update if the proposer is the same as on the canonical chain + if block.proposer_index == get_beacon_proposer_index(head_state): + store.proposer_boost_root = root + + +- name: update_proposer_boost_root#gloas + sources: [] + spec: | + + def update_proposer_boost_root(store: Store, root: Root) -> None: + is_first_block = store.proposer_boost_root == Root() + # [Modified in Gloas:EIP7732] + is_timely = store.block_timeliness[root][ATTESTATION_TIMELINESS_INDEX] + + # Add proposer score boost if the block is the first timely block + # for this slot, with the same proposer as the canonical chain. + if is_timely and is_first_block: + head_state = copy(store.block_states[get_head(store).root]) + slot = get_current_slot(store) + if head_state.slot < slot: + process_slots(head_state, slot) + block = store.blocks[root] + # Only update if the proposer is the same as on the canonical chain + if block.proposer_index == get_beacon_proposer_index(head_state): + store.proposer_boost_root = root + + +- name: update_unrealized_checkpoints#phase0 + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: '^\s+private updateUnrealizedCheckpoints\(' + regex: true + spec: | + + def update_unrealized_checkpoints( + store: Store, + unrealized_justified_checkpoint: Checkpoint, + unrealized_finalized_checkpoint: Checkpoint, + ) -> None: + """ + Update unrealized checkpoints in store if necessary + """ + # Update unrealized justified checkpoint + if unrealized_justified_checkpoint.epoch > store.unrealized_justified_checkpoint.epoch: + store.unrealized_justified_checkpoint = unrealized_justified_checkpoint + + # Update unrealized finalized checkpoint + if unrealized_finalized_checkpoint.epoch > store.unrealized_finalized_checkpoint.epoch: + store.unrealized_finalized_checkpoint = unrealized_finalized_checkpoint + + +- name: upgrade_lc_bootstrap_to_capella#capella + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function upgradeLightClientHeader( + spec: | + + def upgrade_lc_bootstrap_to_capella(pre: altair.LightClientBootstrap) -> LightClientBootstrap: + return LightClientBootstrap( + header=upgrade_lc_header_to_capella(pre.header), + current_sync_committee=pre.current_sync_committee, + current_sync_committee_branch=pre.current_sync_committee_branch, + ) + + +- name: upgrade_lc_bootstrap_to_deneb#deneb + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function upgradeLightClientHeader( + spec: | + + def upgrade_lc_bootstrap_to_deneb(pre: capella.LightClientBootstrap) -> LightClientBootstrap: + return LightClientBootstrap( + header=upgrade_lc_header_to_deneb(pre.header), + current_sync_committee=pre.current_sync_committee, + current_sync_committee_branch=pre.current_sync_committee_branch, + ) + + +- name: upgrade_lc_bootstrap_to_electra#electra + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function upgradeLightClientHeader( + spec: | + + def upgrade_lc_bootstrap_to_electra(pre: deneb.LightClientBootstrap) -> LightClientBootstrap: + return LightClientBootstrap( + header=upgrade_lc_header_to_electra(pre.header), + current_sync_committee=pre.current_sync_committee, + current_sync_committee_branch=normalize_merkle_branch( + pre.current_sync_committee_branch, CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA + ), + ) + + +- name: upgrade_lc_finality_update_to_capella#capella + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function upgradeLightClientFinalityUpdate( + spec: | + + def upgrade_lc_finality_update_to_capella( + pre: altair.LightClientFinalityUpdate, + ) -> LightClientFinalityUpdate: + return LightClientFinalityUpdate( + attested_header=upgrade_lc_header_to_capella(pre.attested_header), + finalized_header=upgrade_lc_header_to_capella(pre.finalized_header), + finality_branch=pre.finality_branch, + sync_aggregate=pre.sync_aggregate, + signature_slot=pre.signature_slot, + ) + + +- name: upgrade_lc_finality_update_to_deneb#deneb + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function upgradeLightClientFinalityUpdate( + spec: | + + def upgrade_lc_finality_update_to_deneb( + pre: capella.LightClientFinalityUpdate, + ) -> LightClientFinalityUpdate: + return LightClientFinalityUpdate( + attested_header=upgrade_lc_header_to_deneb(pre.attested_header), + finalized_header=upgrade_lc_header_to_deneb(pre.finalized_header), + finality_branch=pre.finality_branch, + sync_aggregate=pre.sync_aggregate, + signature_slot=pre.signature_slot, + ) + + +- name: upgrade_lc_finality_update_to_electra#electra + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function upgradeLightClientFinalityUpdate( + spec: | + + def upgrade_lc_finality_update_to_electra( + pre: deneb.LightClientFinalityUpdate, + ) -> LightClientFinalityUpdate: + return LightClientFinalityUpdate( + attested_header=upgrade_lc_header_to_electra(pre.attested_header), + finalized_header=upgrade_lc_header_to_electra(pre.finalized_header), + finality_branch=normalize_merkle_branch(pre.finality_branch, FINALIZED_ROOT_GINDEX_ELECTRA), + sync_aggregate=pre.sync_aggregate, + signature_slot=pre.signature_slot, + ) + + +- name: upgrade_lc_header_to_capella#capella + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function upgradeLightClientHeader( + spec: | + + def upgrade_lc_header_to_capella(pre: altair.LightClientHeader) -> LightClientHeader: + return LightClientHeader( + beacon=pre.beacon, + execution=ExecutionPayloadHeader(), + execution_branch=ExecutionBranch(), + ) + + +- name: upgrade_lc_header_to_deneb#deneb + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function upgradeLightClientHeader( + spec: | + + def upgrade_lc_header_to_deneb(pre: capella.LightClientHeader) -> LightClientHeader: + return LightClientHeader( + beacon=pre.beacon, + execution=ExecutionPayloadHeader( + parent_hash=pre.execution.parent_hash, + fee_recipient=pre.execution.fee_recipient, + state_root=pre.execution.state_root, + receipts_root=pre.execution.receipts_root, + logs_bloom=pre.execution.logs_bloom, + prev_randao=pre.execution.prev_randao, + block_number=pre.execution.block_number, + gas_limit=pre.execution.gas_limit, + gas_used=pre.execution.gas_used, + timestamp=pre.execution.timestamp, + extra_data=pre.execution.extra_data, + base_fee_per_gas=pre.execution.base_fee_per_gas, + block_hash=pre.execution.block_hash, + transactions_root=pre.execution.transactions_root, + withdrawals_root=pre.execution.withdrawals_root, + # [New in Deneb:EIP4844] + blob_gas_used=uint64(0), + # [New in Deneb:EIP4844] + excess_blob_gas=uint64(0), + ), + execution_branch=pre.execution_branch, + ) + + +- name: upgrade_lc_header_to_electra#electra + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function upgradeLightClientHeader( + spec: | + + def upgrade_lc_header_to_electra(pre: deneb.LightClientHeader) -> LightClientHeader: + return LightClientHeader( + beacon=pre.beacon, + execution=pre.execution, + execution_branch=pre.execution_branch, + ) + + +- name: upgrade_lc_optimistic_update_to_capella#capella + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function upgradeLightClientOptimisticUpdate( + spec: | + + def upgrade_lc_optimistic_update_to_capella( + pre: altair.LightClientOptimisticUpdate, + ) -> LightClientOptimisticUpdate: + return LightClientOptimisticUpdate( + attested_header=upgrade_lc_header_to_capella(pre.attested_header), + sync_aggregate=pre.sync_aggregate, + signature_slot=pre.signature_slot, + ) + + +- name: upgrade_lc_optimistic_update_to_deneb#deneb + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function upgradeLightClientOptimisticUpdate( + spec: | + + def upgrade_lc_optimistic_update_to_deneb( + pre: capella.LightClientOptimisticUpdate, + ) -> LightClientOptimisticUpdate: + return LightClientOptimisticUpdate( + attested_header=upgrade_lc_header_to_deneb(pre.attested_header), + sync_aggregate=pre.sync_aggregate, + signature_slot=pre.signature_slot, + ) + + +- name: upgrade_lc_optimistic_update_to_electra#electra + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function upgradeLightClientOptimisticUpdate( + spec: | + + def upgrade_lc_optimistic_update_to_electra( + pre: deneb.LightClientOptimisticUpdate, + ) -> LightClientOptimisticUpdate: + return LightClientOptimisticUpdate( + attested_header=upgrade_lc_header_to_electra(pre.attested_header), + sync_aggregate=pre.sync_aggregate, + signature_slot=pre.signature_slot, + ) + + +- name: upgrade_lc_store_to_capella#capella + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function upgradeLightClientStore( + spec: | + + def upgrade_lc_store_to_capella(pre: altair.LightClientStore) -> LightClientStore: + if pre.best_valid_update is None: + best_valid_update = None + else: + best_valid_update = upgrade_lc_update_to_capella(pre.best_valid_update) + return LightClientStore( + finalized_header=upgrade_lc_header_to_capella(pre.finalized_header), + current_sync_committee=pre.current_sync_committee, + next_sync_committee=pre.next_sync_committee, + best_valid_update=best_valid_update, + optimistic_header=upgrade_lc_header_to_capella(pre.optimistic_header), + previous_max_active_participants=pre.previous_max_active_participants, + current_max_active_participants=pre.current_max_active_participants, + ) + + +- name: upgrade_lc_store_to_deneb#deneb + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function upgradeLightClientStore( + spec: | + + def upgrade_lc_store_to_deneb(pre: capella.LightClientStore) -> LightClientStore: + if pre.best_valid_update is None: + best_valid_update = None + else: + best_valid_update = upgrade_lc_update_to_deneb(pre.best_valid_update) + return LightClientStore( + finalized_header=upgrade_lc_header_to_deneb(pre.finalized_header), + current_sync_committee=pre.current_sync_committee, + next_sync_committee=pre.next_sync_committee, + best_valid_update=best_valid_update, + optimistic_header=upgrade_lc_header_to_deneb(pre.optimistic_header), + previous_max_active_participants=pre.previous_max_active_participants, + current_max_active_participants=pre.current_max_active_participants, + ) + + +- name: upgrade_lc_store_to_electra#electra + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function upgradeLightClientStore( + spec: | + + def upgrade_lc_store_to_electra(pre: deneb.LightClientStore) -> LightClientStore: + if pre.best_valid_update is None: + best_valid_update = None + else: + best_valid_update = upgrade_lc_update_to_electra(pre.best_valid_update) + return LightClientStore( + finalized_header=upgrade_lc_header_to_electra(pre.finalized_header), + current_sync_committee=pre.current_sync_committee, + next_sync_committee=pre.next_sync_committee, + best_valid_update=best_valid_update, + optimistic_header=upgrade_lc_header_to_electra(pre.optimistic_header), + previous_max_active_participants=pre.previous_max_active_participants, + current_max_active_participants=pre.current_max_active_participants, + ) + + +- name: upgrade_lc_update_to_capella#capella + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function upgradeLightClientUpdate( + spec: | + + def upgrade_lc_update_to_capella(pre: altair.LightClientUpdate) -> LightClientUpdate: + return LightClientUpdate( + attested_header=upgrade_lc_header_to_capella(pre.attested_header), + next_sync_committee=pre.next_sync_committee, + next_sync_committee_branch=pre.next_sync_committee_branch, + finalized_header=upgrade_lc_header_to_capella(pre.finalized_header), + finality_branch=pre.finality_branch, + sync_aggregate=pre.sync_aggregate, + signature_slot=pre.signature_slot, + ) + + +- name: upgrade_lc_update_to_deneb#deneb + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function upgradeLightClientUpdate( + spec: | + + def upgrade_lc_update_to_deneb(pre: capella.LightClientUpdate) -> LightClientUpdate: + return LightClientUpdate( + attested_header=upgrade_lc_header_to_deneb(pre.attested_header), + next_sync_committee=pre.next_sync_committee, + next_sync_committee_branch=pre.next_sync_committee_branch, + finalized_header=upgrade_lc_header_to_deneb(pre.finalized_header), + finality_branch=pre.finality_branch, + sync_aggregate=pre.sync_aggregate, + signature_slot=pre.signature_slot, + ) + + +- name: upgrade_lc_update_to_electra#electra + sources: + - file: packages/light-client/src/spec/utils.ts + search: export function upgradeLightClientUpdate( + spec: | + + def upgrade_lc_update_to_electra(pre: deneb.LightClientUpdate) -> LightClientUpdate: + return LightClientUpdate( + attested_header=upgrade_lc_header_to_electra(pre.attested_header), + next_sync_committee=pre.next_sync_committee, + next_sync_committee_branch=normalize_merkle_branch( + pre.next_sync_committee_branch, NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA + ), + finalized_header=upgrade_lc_header_to_electra(pre.finalized_header), + finality_branch=normalize_merkle_branch(pre.finality_branch, FINALIZED_ROOT_GINDEX_ELECTRA), + sync_aggregate=pre.sync_aggregate, + signature_slot=pre.signature_slot, + ) + + +- name: upgrade_to_altair#altair + sources: + - file: packages/state-transition/src/slot/upgradeStateToAltair.ts + search: export function upgradeStateToAltair( + spec: | + + def upgrade_to_altair(pre: phase0.BeaconState) -> BeaconState: + epoch = phase0.get_current_epoch(pre) + post = BeaconState( + genesis_time=pre.genesis_time, + genesis_validators_root=pre.genesis_validators_root, + slot=pre.slot, + fork=Fork( + previous_version=pre.fork.current_version, + current_version=ALTAIR_FORK_VERSION, + epoch=epoch, + ), + latest_block_header=pre.latest_block_header, + block_roots=pre.block_roots, + state_roots=pre.state_roots, + historical_roots=pre.historical_roots, + eth1_data=pre.eth1_data, + eth1_data_votes=pre.eth1_data_votes, + eth1_deposit_index=pre.eth1_deposit_index, + validators=pre.validators, + balances=pre.balances, + randao_mixes=pre.randao_mixes, + slashings=pre.slashings, + previous_epoch_participation=[ + ParticipationFlags(0b0000_0000) for _ in range(len(pre.validators)) + ], + current_epoch_participation=[ + ParticipationFlags(0b0000_0000) for _ in range(len(pre.validators)) + ], + justification_bits=pre.justification_bits, + previous_justified_checkpoint=pre.previous_justified_checkpoint, + current_justified_checkpoint=pre.current_justified_checkpoint, + finalized_checkpoint=pre.finalized_checkpoint, + inactivity_scores=[uint64(0) for _ in range(len(pre.validators))], + ) + # Fill in previous epoch participation from the pre state's pending attestations + translate_participation(post, pre.previous_epoch_attestations) + + # Fill in sync committees + # Note: A duplicate committee is assigned for the current and next committee at the fork boundary + post.current_sync_committee = get_next_sync_committee(post) + post.next_sync_committee = get_next_sync_committee(post) + return post + + +- name: upgrade_to_bellatrix#bellatrix + sources: + - file: packages/state-transition/src/slot/upgradeStateToBellatrix.ts + search: export function upgradeStateToBellatrix( + spec: | + + def upgrade_to_bellatrix(pre: altair.BeaconState) -> BeaconState: + epoch = altair.get_current_epoch(pre) + post = BeaconState( + genesis_time=pre.genesis_time, + genesis_validators_root=pre.genesis_validators_root, + slot=pre.slot, + fork=Fork( + previous_version=pre.fork.current_version, + # [New in Bellatrix] + current_version=BELLATRIX_FORK_VERSION, + epoch=epoch, + ), + latest_block_header=pre.latest_block_header, + block_roots=pre.block_roots, + state_roots=pre.state_roots, + historical_roots=pre.historical_roots, + eth1_data=pre.eth1_data, + eth1_data_votes=pre.eth1_data_votes, + eth1_deposit_index=pre.eth1_deposit_index, + validators=pre.validators, + balances=pre.balances, + randao_mixes=pre.randao_mixes, + slashings=pre.slashings, + previous_epoch_participation=pre.previous_epoch_participation, + current_epoch_participation=pre.current_epoch_participation, + justification_bits=pre.justification_bits, + previous_justified_checkpoint=pre.previous_justified_checkpoint, + current_justified_checkpoint=pre.current_justified_checkpoint, + finalized_checkpoint=pre.finalized_checkpoint, + inactivity_scores=pre.inactivity_scores, + current_sync_committee=pre.current_sync_committee, + next_sync_committee=pre.next_sync_committee, + # [New in Bellatrix] + latest_execution_payload_header=ExecutionPayloadHeader(), + ) + + return post + + +- name: upgrade_to_capella#capella + sources: + - file: packages/state-transition/src/slot/upgradeStateToCapella.ts + search: export function upgradeStateToCapella( + spec: | + + def upgrade_to_capella(pre: bellatrix.BeaconState) -> BeaconState: + epoch = bellatrix.get_current_epoch(pre) + latest_execution_payload_header = ExecutionPayloadHeader( + parent_hash=pre.latest_execution_payload_header.parent_hash, + fee_recipient=pre.latest_execution_payload_header.fee_recipient, + state_root=pre.latest_execution_payload_header.state_root, + receipts_root=pre.latest_execution_payload_header.receipts_root, + logs_bloom=pre.latest_execution_payload_header.logs_bloom, + prev_randao=pre.latest_execution_payload_header.prev_randao, + block_number=pre.latest_execution_payload_header.block_number, + gas_limit=pre.latest_execution_payload_header.gas_limit, + gas_used=pre.latest_execution_payload_header.gas_used, + timestamp=pre.latest_execution_payload_header.timestamp, + extra_data=pre.latest_execution_payload_header.extra_data, + base_fee_per_gas=pre.latest_execution_payload_header.base_fee_per_gas, + block_hash=pre.latest_execution_payload_header.block_hash, + transactions_root=pre.latest_execution_payload_header.transactions_root, + # [New in Capella] + withdrawals_root=Root(), + ) + post = BeaconState( + genesis_time=pre.genesis_time, + genesis_validators_root=pre.genesis_validators_root, + slot=pre.slot, + fork=Fork( + previous_version=pre.fork.current_version, + current_version=CAPELLA_FORK_VERSION, + epoch=epoch, + ), + latest_block_header=pre.latest_block_header, + block_roots=pre.block_roots, + state_roots=pre.state_roots, + historical_roots=pre.historical_roots, + eth1_data=pre.eth1_data, + eth1_data_votes=pre.eth1_data_votes, + eth1_deposit_index=pre.eth1_deposit_index, + validators=pre.validators, + balances=pre.balances, + randao_mixes=pre.randao_mixes, + slashings=pre.slashings, + previous_epoch_participation=pre.previous_epoch_participation, + current_epoch_participation=pre.current_epoch_participation, + justification_bits=pre.justification_bits, + previous_justified_checkpoint=pre.previous_justified_checkpoint, + current_justified_checkpoint=pre.current_justified_checkpoint, + finalized_checkpoint=pre.finalized_checkpoint, + inactivity_scores=pre.inactivity_scores, + current_sync_committee=pre.current_sync_committee, + next_sync_committee=pre.next_sync_committee, + latest_execution_payload_header=latest_execution_payload_header, + # [New in Capella] + next_withdrawal_index=WithdrawalIndex(0), + # [New in Capella] + next_withdrawal_validator_index=ValidatorIndex(0), + # [New in Capella] + historical_summaries=List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT]([]), + ) + + return post + + +- name: upgrade_to_deneb#deneb + sources: + - file: packages/state-transition/src/slot/upgradeStateToDeneb.ts + search: export function upgradeStateToDeneb( + spec: | + + def upgrade_to_deneb(pre: capella.BeaconState) -> BeaconState: + epoch = capella.get_current_epoch(pre) + latest_execution_payload_header = ExecutionPayloadHeader( + parent_hash=pre.latest_execution_payload_header.parent_hash, + fee_recipient=pre.latest_execution_payload_header.fee_recipient, + state_root=pre.latest_execution_payload_header.state_root, + receipts_root=pre.latest_execution_payload_header.receipts_root, + logs_bloom=pre.latest_execution_payload_header.logs_bloom, + prev_randao=pre.latest_execution_payload_header.prev_randao, + block_number=pre.latest_execution_payload_header.block_number, + gas_limit=pre.latest_execution_payload_header.gas_limit, + gas_used=pre.latest_execution_payload_header.gas_used, + timestamp=pre.latest_execution_payload_header.timestamp, + extra_data=pre.latest_execution_payload_header.extra_data, + base_fee_per_gas=pre.latest_execution_payload_header.base_fee_per_gas, + block_hash=pre.latest_execution_payload_header.block_hash, + transactions_root=pre.latest_execution_payload_header.transactions_root, + withdrawals_root=pre.latest_execution_payload_header.withdrawals_root, + # [New in Deneb:EIP4844] + blob_gas_used=uint64(0), + # [New in Deneb:EIP4844] + excess_blob_gas=uint64(0), + ) + post = BeaconState( + genesis_time=pre.genesis_time, + genesis_validators_root=pre.genesis_validators_root, + slot=pre.slot, + fork=Fork( + previous_version=pre.fork.current_version, + # [Modified in Deneb] + current_version=DENEB_FORK_VERSION, + epoch=epoch, + ), + latest_block_header=pre.latest_block_header, + block_roots=pre.block_roots, + state_roots=pre.state_roots, + historical_roots=pre.historical_roots, + eth1_data=pre.eth1_data, + eth1_data_votes=pre.eth1_data_votes, + eth1_deposit_index=pre.eth1_deposit_index, + validators=pre.validators, + balances=pre.balances, + randao_mixes=pre.randao_mixes, + slashings=pre.slashings, + previous_epoch_participation=pre.previous_epoch_participation, + current_epoch_participation=pre.current_epoch_participation, + justification_bits=pre.justification_bits, + previous_justified_checkpoint=pre.previous_justified_checkpoint, + current_justified_checkpoint=pre.current_justified_checkpoint, + finalized_checkpoint=pre.finalized_checkpoint, + inactivity_scores=pre.inactivity_scores, + current_sync_committee=pre.current_sync_committee, + next_sync_committee=pre.next_sync_committee, + # [Modified in Deneb:EIP4844] + latest_execution_payload_header=latest_execution_payload_header, + next_withdrawal_index=pre.next_withdrawal_index, + next_withdrawal_validator_index=pre.next_withdrawal_validator_index, + historical_summaries=pre.historical_summaries, + ) + + return post + + +- name: upgrade_to_electra#electra + sources: + - file: packages/state-transition/src/slot/upgradeStateToElectra.ts + search: export function upgradeStateToElectra( + spec: | + + def upgrade_to_electra(pre: deneb.BeaconState) -> BeaconState: + epoch = deneb.get_current_epoch(pre) + + earliest_exit_epoch = compute_activation_exit_epoch(get_current_epoch(pre)) + for validator in pre.validators: + if validator.exit_epoch != FAR_FUTURE_EPOCH: + if validator.exit_epoch > earliest_exit_epoch: + earliest_exit_epoch = validator.exit_epoch + earliest_exit_epoch += Epoch(1) + + post = BeaconState( + genesis_time=pre.genesis_time, + genesis_validators_root=pre.genesis_validators_root, + slot=pre.slot, + fork=Fork( + previous_version=pre.fork.current_version, + # [Modified in Electra] + current_version=ELECTRA_FORK_VERSION, + epoch=epoch, + ), + latest_block_header=pre.latest_block_header, + block_roots=pre.block_roots, + state_roots=pre.state_roots, + historical_roots=pre.historical_roots, + eth1_data=pre.eth1_data, + eth1_data_votes=pre.eth1_data_votes, + eth1_deposit_index=pre.eth1_deposit_index, + validators=pre.validators, + balances=pre.balances, + randao_mixes=pre.randao_mixes, + slashings=pre.slashings, + previous_epoch_participation=pre.previous_epoch_participation, + current_epoch_participation=pre.current_epoch_participation, + justification_bits=pre.justification_bits, + previous_justified_checkpoint=pre.previous_justified_checkpoint, + current_justified_checkpoint=pre.current_justified_checkpoint, + finalized_checkpoint=pre.finalized_checkpoint, + inactivity_scores=pre.inactivity_scores, + current_sync_committee=pre.current_sync_committee, + next_sync_committee=pre.next_sync_committee, + latest_execution_payload_header=pre.latest_execution_payload_header, + next_withdrawal_index=pre.next_withdrawal_index, + next_withdrawal_validator_index=pre.next_withdrawal_validator_index, + historical_summaries=pre.historical_summaries, + # [New in Electra:EIP6110] + deposit_requests_start_index=UNSET_DEPOSIT_REQUESTS_START_INDEX, + # [New in Electra:EIP7251] + deposit_balance_to_consume=0, + # [New in Electra:EIP7251] + exit_balance_to_consume=0, + # [New in Electra:EIP7251] + earliest_exit_epoch=earliest_exit_epoch, + # [New in Electra:EIP7251] + consolidation_balance_to_consume=0, + # [New in Electra:EIP7251] + earliest_consolidation_epoch=compute_activation_exit_epoch(get_current_epoch(pre)), + # [New in Electra:EIP7251] + pending_deposits=[], + # [New in Electra:EIP7251] + pending_partial_withdrawals=[], + # [New in Electra:EIP7251] + pending_consolidations=[], + ) + + post.exit_balance_to_consume = get_activation_exit_churn_limit(post) + post.consolidation_balance_to_consume = get_consolidation_churn_limit(post) + + # [New in Electra:EIP7251] + # add validators that are not yet active to pending balance deposits + pre_activation = sorted( + [ + index + for index, validator in enumerate(post.validators) + if validator.activation_epoch == FAR_FUTURE_EPOCH + ], + key=lambda index: (post.validators[index].activation_eligibility_epoch, index), + ) + + for index in pre_activation: + balance = post.balances[index] + post.balances[index] = 0 + validator = post.validators[index] + validator.effective_balance = 0 + validator.activation_eligibility_epoch = FAR_FUTURE_EPOCH + # Use bls.G2_POINT_AT_INFINITY as a signature field placeholder + # and GENESIS_SLOT to distinguish from a pending deposit request + post.pending_deposits.append( + PendingDeposit( + pubkey=validator.pubkey, + withdrawal_credentials=validator.withdrawal_credentials, + amount=balance, + signature=bls.G2_POINT_AT_INFINITY, + slot=GENESIS_SLOT, + ) + ) + + # Ensure early adopters of compounding credentials go through the activation churn + for index, validator in enumerate(post.validators): + if has_compounding_withdrawal_credential(validator): + queue_excess_active_balance(post, ValidatorIndex(index)) + + return post + + +- name: upgrade_to_fulu#fulu + sources: + - file: packages/state-transition/src/slot/upgradeStateToFulu.ts + search: export function upgradeStateToFulu( + spec: | + + def upgrade_to_fulu(pre: electra.BeaconState) -> BeaconState: + epoch = electra.get_current_epoch(pre) + post = BeaconState( + genesis_time=pre.genesis_time, + genesis_validators_root=pre.genesis_validators_root, + slot=pre.slot, + fork=Fork( + previous_version=pre.fork.current_version, + # [Modified in Fulu] + current_version=FULU_FORK_VERSION, + epoch=epoch, + ), + latest_block_header=pre.latest_block_header, + block_roots=pre.block_roots, + state_roots=pre.state_roots, + historical_roots=pre.historical_roots, + eth1_data=pre.eth1_data, + eth1_data_votes=pre.eth1_data_votes, + eth1_deposit_index=pre.eth1_deposit_index, + validators=pre.validators, + balances=pre.balances, + randao_mixes=pre.randao_mixes, + slashings=pre.slashings, + previous_epoch_participation=pre.previous_epoch_participation, + current_epoch_participation=pre.current_epoch_participation, + justification_bits=pre.justification_bits, + previous_justified_checkpoint=pre.previous_justified_checkpoint, + current_justified_checkpoint=pre.current_justified_checkpoint, + finalized_checkpoint=pre.finalized_checkpoint, + inactivity_scores=pre.inactivity_scores, + current_sync_committee=pre.current_sync_committee, + next_sync_committee=pre.next_sync_committee, + latest_execution_payload_header=pre.latest_execution_payload_header, + next_withdrawal_index=pre.next_withdrawal_index, + next_withdrawal_validator_index=pre.next_withdrawal_validator_index, + historical_summaries=pre.historical_summaries, + deposit_requests_start_index=pre.deposit_requests_start_index, + deposit_balance_to_consume=pre.deposit_balance_to_consume, + exit_balance_to_consume=pre.exit_balance_to_consume, + earliest_exit_epoch=pre.earliest_exit_epoch, + consolidation_balance_to_consume=pre.consolidation_balance_to_consume, + earliest_consolidation_epoch=pre.earliest_consolidation_epoch, + pending_deposits=pre.pending_deposits, + pending_partial_withdrawals=pre.pending_partial_withdrawals, + pending_consolidations=pre.pending_consolidations, + # [New in Fulu:EIP7917] + proposer_lookahead=initialize_proposer_lookahead(pre), + ) + + return post + + +- name: upgrade_to_gloas#gloas + sources: + - file: packages/state-transition/src/slot/upgradeStateToGloas.ts + search: export function upgradeStateToGloas( + spec: | + + def upgrade_to_gloas(pre: fulu.BeaconState) -> BeaconState: + epoch = fulu.get_current_epoch(pre) + + post = BeaconState( + genesis_time=pre.genesis_time, + genesis_validators_root=pre.genesis_validators_root, + slot=pre.slot, + fork=Fork( + previous_version=pre.fork.current_version, + # [Modified in Gloas:EIP7732] + current_version=GLOAS_FORK_VERSION, + epoch=epoch, + ), + latest_block_header=pre.latest_block_header, + block_roots=pre.block_roots, + state_roots=pre.state_roots, + historical_roots=pre.historical_roots, + eth1_data=pre.eth1_data, + eth1_data_votes=pre.eth1_data_votes, + eth1_deposit_index=pre.eth1_deposit_index, + validators=pre.validators, + balances=pre.balances, + randao_mixes=pre.randao_mixes, + slashings=pre.slashings, + previous_epoch_participation=pre.previous_epoch_participation, + current_epoch_participation=pre.current_epoch_participation, + justification_bits=pre.justification_bits, + previous_justified_checkpoint=pre.previous_justified_checkpoint, + current_justified_checkpoint=pre.current_justified_checkpoint, + finalized_checkpoint=pre.finalized_checkpoint, + inactivity_scores=pre.inactivity_scores, + current_sync_committee=pre.current_sync_committee, + next_sync_committee=pre.next_sync_committee, + # [Modified in Gloas:EIP7732] + # Removed `latest_execution_payload_header` + # [New in Gloas:EIP7732] + latest_execution_payload_bid=ExecutionPayloadBid( + block_hash=pre.latest_execution_payload_header.block_hash, + ), + next_withdrawal_index=pre.next_withdrawal_index, + next_withdrawal_validator_index=pre.next_withdrawal_validator_index, + historical_summaries=pre.historical_summaries, + deposit_requests_start_index=pre.deposit_requests_start_index, + deposit_balance_to_consume=pre.deposit_balance_to_consume, + exit_balance_to_consume=pre.exit_balance_to_consume, + earliest_exit_epoch=pre.earliest_exit_epoch, + consolidation_balance_to_consume=pre.consolidation_balance_to_consume, + earliest_consolidation_epoch=pre.earliest_consolidation_epoch, + pending_deposits=pre.pending_deposits, + pending_partial_withdrawals=pre.pending_partial_withdrawals, + pending_consolidations=pre.pending_consolidations, + proposer_lookahead=pre.proposer_lookahead, + # [New in Gloas:EIP7732] + builders=[], + # [New in Gloas:EIP7732] + next_withdrawal_builder_index=BuilderIndex(0), + # [New in Gloas:EIP7732] + execution_payload_availability=[0b1 for _ in range(SLOTS_PER_HISTORICAL_ROOT)], + # [New in Gloas:EIP7732] + builder_pending_payments=[BuilderPendingPayment() for _ in range(2 * SLOTS_PER_EPOCH)], + # [New in Gloas:EIP7732] + builder_pending_withdrawals=[], + # [New in Gloas:EIP7732] + latest_block_hash=pre.latest_execution_payload_header.block_hash, + # [New in Gloas:EIP7732] + payload_expected_withdrawals=[], + ) + + return post + + +- name: validate_light_client_update#altair + sources: + - file: packages/light-client/src/spec/validateLightClientUpdate.ts + search: export function validateLightClientUpdate( + spec: | + + def validate_light_client_update( + store: LightClientStore, + update: LightClientUpdate, + current_slot: Slot, + genesis_validators_root: Root, + ) -> None: + # Verify sync committee has sufficient participants + sync_aggregate = update.sync_aggregate + assert sum(sync_aggregate.sync_committee_bits) >= MIN_SYNC_COMMITTEE_PARTICIPANTS + + # Verify update does not skip a sync committee period + assert is_valid_light_client_header(update.attested_header) + update_attested_slot = update.attested_header.beacon.slot + update_finalized_slot = update.finalized_header.beacon.slot + assert current_slot >= update.signature_slot > update_attested_slot >= update_finalized_slot + store_period = compute_sync_committee_period_at_slot(store.finalized_header.beacon.slot) + update_signature_period = compute_sync_committee_period_at_slot(update.signature_slot) + if is_next_sync_committee_known(store): + assert update_signature_period in (store_period, store_period + 1) + else: + assert update_signature_period == store_period + + # Verify update is relevant + update_attested_period = compute_sync_committee_period_at_slot(update_attested_slot) + update_has_next_sync_committee = not is_next_sync_committee_known(store) and ( + is_sync_committee_update(update) and update_attested_period == store_period + ) + assert ( + update_attested_slot > store.finalized_header.beacon.slot or update_has_next_sync_committee + ) + + # Verify that the `finality_branch`, if present, confirms `finalized_header` + # to match the finalized checkpoint root saved in the state of `attested_header`. + # Note that the genesis finalized checkpoint root is represented as a zero hash. + if not is_finality_update(update): + assert update.finalized_header == LightClientHeader() + else: + if update_finalized_slot == GENESIS_SLOT: + assert update.finalized_header == LightClientHeader() + finalized_root = Bytes32() + else: + assert is_valid_light_client_header(update.finalized_header) + finalized_root = hash_tree_root(update.finalized_header.beacon) + assert is_valid_normalized_merkle_branch( + leaf=finalized_root, + branch=update.finality_branch, + gindex=finalized_root_gindex_at_slot(update.attested_header.beacon.slot), + root=update.attested_header.beacon.state_root, + ) + + # Verify that the `next_sync_committee`, if present, actually is the next sync committee saved in the + # state of the `attested_header` + if not is_sync_committee_update(update): + assert update.next_sync_committee == SyncCommittee() + else: + if update_attested_period == store_period and is_next_sync_committee_known(store): + assert update.next_sync_committee == store.next_sync_committee + assert is_valid_normalized_merkle_branch( + leaf=hash_tree_root(update.next_sync_committee), + branch=update.next_sync_committee_branch, + gindex=next_sync_committee_gindex_at_slot(update.attested_header.beacon.slot), + root=update.attested_header.beacon.state_root, + ) + + # Verify sync committee aggregate signature + if update_signature_period == store_period: + sync_committee = store.current_sync_committee + else: + sync_committee = store.next_sync_committee + participant_pubkeys = [ + pubkey + for (bit, pubkey) in zip(sync_aggregate.sync_committee_bits, sync_committee.pubkeys) + if bit + ] + fork_version_slot = max(update.signature_slot, Slot(1)) - Slot(1) + fork_version = compute_fork_version(compute_epoch_at_slot(fork_version_slot)) + domain = compute_domain(DOMAIN_SYNC_COMMITTEE, fork_version, genesis_validators_root) + signing_root = compute_signing_root(update.attested_header.beacon, domain) + assert bls.FastAggregateVerify( + participant_pubkeys, signing_root, sync_aggregate.sync_committee_signature + ) + + +- name: validate_merge_block#bellatrix + sources: [] + spec: | + + def validate_merge_block(block: BeaconBlock) -> None: + """ + Check the parent PoW block of execution payload is a valid terminal PoW block. + + Note: Unavailable PoW block(s) may later become available, + and a client software MAY delay a call to ``validate_merge_block`` + until the PoW block(s) become available. + """ + if TERMINAL_BLOCK_HASH != Hash32(): + # If `TERMINAL_BLOCK_HASH` is used as an override, the activation epoch must be reached. + assert compute_epoch_at_slot(block.slot) >= TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH + assert block.body.execution_payload.parent_hash == TERMINAL_BLOCK_HASH + return + + pow_block = get_pow_block(block.body.execution_payload.parent_hash) + # Check if `pow_block` is available + assert pow_block is not None + pow_parent = get_pow_block(pow_block.parent_hash) + # Check if `pow_parent` is available + assert pow_parent is not None + # Check if `pow_block` is a valid terminal PoW block + assert is_valid_terminal_pow_block(pow_block, pow_parent) + + +- name: validate_merge_block#gloas + sources: [] + spec: | + + def validate_merge_block(block: BeaconBlock) -> None: + """ + Check the parent PoW block of execution payload is a valid terminal PoW block. + + Note: Unavailable PoW block(s) may later become available, + and a client software MAY delay a call to ``validate_merge_block`` + until the PoW block(s) become available. + """ + if TERMINAL_BLOCK_HASH != Hash32(): + # If `TERMINAL_BLOCK_HASH` is used as an override, the activation epoch must be reached. + assert compute_epoch_at_slot(block.slot) >= TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH + assert block.body.execution_payload.parent_hash == TERMINAL_BLOCK_HASH + return + + pow_block = get_pow_block(block.body.execution_payload.parent_hash) + # Check if `pow_block` is available + assert pow_block is not None + pow_parent = get_pow_block(pow_block.parent_hash) + # Check if `pow_parent` is available + assert pow_parent is not None + # Check if `pow_block` is a valid terminal PoW block + assert is_valid_terminal_pow_block(pow_block, pow_parent) + + +- name: validate_on_attestation#phase0 + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: '^\s+private validateOnAttestation\(' + regex: true + spec: | + + def validate_on_attestation(store: Store, attestation: Attestation, is_from_block: bool) -> None: + target = attestation.data.target + + # If the given attestation is not from a beacon block message, we have to check the target epoch scope. + if not is_from_block: + validate_target_epoch_against_current_time(store, attestation) + + # Check that the epoch number and slot number are matching + assert target.epoch == compute_epoch_at_slot(attestation.data.slot) + + # Attestation target must be for a known block. If target block is unknown, delay consideration until block is found + assert target.root in store.blocks + + # Attestations must be for a known block. If block is unknown, delay consideration until the block is found + assert attestation.data.beacon_block_root in store.blocks + # Attestations must not be for blocks in the future. If not, the attestation should not be considered + assert store.blocks[attestation.data.beacon_block_root].slot <= attestation.data.slot + + # LMD vote must be consistent with FFG vote target + assert target.root == get_checkpoint_block( + store, attestation.data.beacon_block_root, target.epoch + ) + + # Attestations can only affect the fork choice of subsequent slots. + # Delay consideration in the fork choice until their slot is in the past. + assert get_current_slot(store) >= attestation.data.slot + 1 + + +- name: validate_on_attestation#gloas + sources: [] + spec: | + + def validate_on_attestation(store: Store, attestation: Attestation, is_from_block: bool) -> None: + target = attestation.data.target + + # If the given attestation is not from a beacon block message, + # we have to check the target epoch scope. + if not is_from_block: + validate_target_epoch_against_current_time(store, attestation) + + # Check that the epoch number and slot number are matching. + assert target.epoch == compute_epoch_at_slot(attestation.data.slot) + + # Attestation target must be for a known block. If target block + # is unknown, delay consideration until block is found. + assert target.root in store.blocks + + # Attestations must be for a known block. If block + # is unknown, delay consideration until the block is found. + assert attestation.data.beacon_block_root in store.blocks + # Attestations must not be for blocks in the future. + # If not, the attestation should not be considered. + block_slot = store.blocks[attestation.data.beacon_block_root].slot + assert block_slot <= attestation.data.slot + + # [New in Gloas:EIP7732] + assert attestation.data.index in [0, 1] + if block_slot == attestation.data.slot: + assert attestation.data.index == 0 + + # LMD vote must be consistent with FFG vote target + assert target.root == get_checkpoint_block( + store, attestation.data.beacon_block_root, target.epoch + ) + + # Attestations can only affect the fork-choice of subsequent slots. + # Delay consideration in the fork-choice until their slot is in the past. + assert get_current_slot(store) >= attestation.data.slot + 1 + + +- name: validate_target_epoch_against_current_time#phase0 + sources: [] + spec: | + + def validate_target_epoch_against_current_time(store: Store, attestation: Attestation) -> None: + target = attestation.data.target + + # Attestations must be from the current or previous epoch + current_epoch = get_current_store_epoch(store) + # Use GENESIS_EPOCH for previous when genesis to avoid underflow + previous_epoch = current_epoch - 1 if current_epoch > GENESIS_EPOCH else GENESIS_EPOCH + # If attestation target is from a future epoch, delay consideration until the epoch arrives + assert target.epoch in [current_epoch, previous_epoch] + + +- name: verify_blob_sidecar_inclusion_proof#deneb + sources: + - file: packages/beacon-node/src/chain/validation/blobSidecar.ts + search: export function validateBlobSidecarInclusionProof( + spec: | + + def verify_blob_sidecar_inclusion_proof(blob_sidecar: BlobSidecar) -> bool: + gindex = get_subtree_index( + get_generalized_index(BeaconBlockBody, "blob_kzg_commitments", blob_sidecar.index) + ) + return is_valid_merkle_branch( + leaf=blob_sidecar.kzg_commitment.hash_tree_root(), + branch=blob_sidecar.kzg_commitment_inclusion_proof, + depth=KZG_COMMITMENT_INCLUSION_PROOF_DEPTH, + index=gindex, + root=blob_sidecar.signed_block_header.message.body_root, + ) + + +- name: verify_block_signature#phase0 + sources: + - file: packages/state-transition/src/signatureSets/proposer.ts + search: export function verifyProposerSignature( + spec: | + + def verify_block_signature(state: BeaconState, signed_block: SignedBeaconBlock) -> bool: + proposer = state.validators[signed_block.message.proposer_index] + signing_root = compute_signing_root( + signed_block.message, get_domain(state, DOMAIN_BEACON_PROPOSER) + ) + return bls.Verify(proposer.pubkey, signing_root, signed_block.signature) + + +- name: verify_data_column_sidecar#fulu + sources: + - file: packages/beacon-node/src/chain/validation/dataColumnSidecar.ts + search: function verifyDataColumnSidecar( + spec: | + + def verify_data_column_sidecar(sidecar: DataColumnSidecar) -> bool: + """ + Verify if the data column sidecar is valid. + """ + # The sidecar index must be within the valid range + if sidecar.index >= NUMBER_OF_COLUMNS: + return False + + # A sidecar for zero blobs is invalid + if len(sidecar.kzg_commitments) == 0: + return False + + # Check that the sidecar respects the blob limit + epoch = compute_epoch_at_slot(sidecar.signed_block_header.message.slot) + if len(sidecar.kzg_commitments) > get_blob_parameters(epoch).max_blobs_per_block: + return False + + # The column length must be equal to the number of commitments/proofs + if len(sidecar.column) != len(sidecar.kzg_commitments) or len(sidecar.column) != len( + sidecar.kzg_proofs + ): + return False + + return True + + +- name: verify_data_column_sidecar#gloas + sources: [] + spec: | + + def verify_data_column_sidecar(sidecar: DataColumnSidecar) -> bool: + """ + Verify if the data column sidecar is valid. + """ + # The sidecar index must be within the valid range + if sidecar.index >= NUMBER_OF_COLUMNS: + return False + + # A sidecar for zero blobs is invalid + if len(sidecar.kzg_commitments) == 0: + return False + + # [Modified in Gloas:EIP7732] + # Check that the sidecar respects the blob limit + epoch = compute_epoch_at_slot(sidecar.slot) + if len(sidecar.kzg_commitments) > get_blob_parameters(epoch).max_blobs_per_block: + return False + + # The column length must be equal to the number of commitments/proofs + if len(sidecar.column) != len(sidecar.kzg_commitments) or len(sidecar.column) != len( + sidecar.kzg_proofs + ): + return False + + return True + + +- name: verify_data_column_sidecar_inclusion_proof#fulu + sources: + - file: packages/beacon-node/src/chain/validation/dataColumnSidecar.ts + search: export function verifyDataColumnSidecarInclusionProof( + spec: | + + def verify_data_column_sidecar_inclusion_proof(sidecar: DataColumnSidecar) -> bool: + """ + Verify if the given KZG commitments included in the given beacon block. + """ + return is_valid_merkle_branch( + leaf=hash_tree_root(sidecar.kzg_commitments), + branch=sidecar.kzg_commitments_inclusion_proof, + depth=KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH, + index=get_subtree_index(get_generalized_index(BeaconBlockBody, "blob_kzg_commitments")), + root=sidecar.signed_block_header.message.body_root, + ) + + +- name: verify_data_column_sidecar_kzg_proofs#fulu + sources: + - file: packages/beacon-node/src/chain/validation/dataColumnSidecar.ts + search: export async function verifyDataColumnSidecarKzgProofs( + spec: | + + def verify_data_column_sidecar_kzg_proofs(sidecar: DataColumnSidecar) -> bool: + """ + Verify if the KZG proofs are correct. + """ + # The column index also represents the cell index + cell_indices = [CellIndex(sidecar.index)] * len(sidecar.column) + + # Batch verify that the cells match the corresponding commitments and proofs + return verify_cell_kzg_proof_batch( + commitments_bytes=sidecar.kzg_commitments, + cell_indices=cell_indices, + cells=sidecar.column, + proofs_bytes=sidecar.kzg_proofs, + ) + + +- name: verify_execution_payload_bid_signature#gloas + sources: + - file: packages/state-transition/src/block/processExecutionPayloadBid.ts + search: function verifyExecutionPayloadBidSignature( + spec: | + + def verify_execution_payload_bid_signature( + state: BeaconState, signed_bid: SignedExecutionPayloadBid + ) -> bool: + builder = state.builders[signed_bid.message.builder_index] + signing_root = compute_signing_root( + signed_bid.message, get_domain(state, DOMAIN_BEACON_BUILDER) + ) + return bls.Verify(builder.pubkey, signing_root, signed_bid.signature) + + +- name: verify_execution_payload_envelope_signature#gloas + sources: + - file: packages/state-transition/src/block/processExecutionPayloadEnvelope.ts + search: function verifyExecutionPayloadEnvelopeSignature( + spec: | + + def verify_execution_payload_envelope_signature( + state: BeaconState, signed_envelope: SignedExecutionPayloadEnvelope + ) -> bool: + builder_index = signed_envelope.message.builder_index + if builder_index == BUILDER_INDEX_SELF_BUILD: + validator_index = state.latest_block_header.proposer_index + pubkey = state.validators[validator_index].pubkey + else: + pubkey = state.builders[builder_index].pubkey + + signing_root = compute_signing_root( + signed_envelope.message, get_domain(state, DOMAIN_BEACON_BUILDER) + ) + return bls.Verify(pubkey, signing_root, signed_envelope.signature) + + +- name: voting_period_start_time#phase0 + sources: [] + spec: | + + def voting_period_start_time(state: BeaconState) -> uint64: + eth1_voting_period_start_slot = Slot( + state.slot - state.slot % (EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH) + ) + return compute_time_at_slot(state, eth1_voting_period_start_slot) + + +- name: weigh_justification_and_finalization#phase0 + sources: + - file: packages/state-transition/src/epoch/processJustificationAndFinalization.ts + search: export function weighJustificationAndFinalization( + spec: | + + def weigh_justification_and_finalization( + state: BeaconState, + total_active_balance: Gwei, + previous_epoch_target_balance: Gwei, + current_epoch_target_balance: Gwei, + ) -> None: + previous_epoch = get_previous_epoch(state) + current_epoch = get_current_epoch(state) + old_previous_justified_checkpoint = state.previous_justified_checkpoint + old_current_justified_checkpoint = state.current_justified_checkpoint + + # Process justifications + state.previous_justified_checkpoint = state.current_justified_checkpoint + state.justification_bits[1:] = state.justification_bits[: JUSTIFICATION_BITS_LENGTH - 1] + state.justification_bits[0] = 0b0 + if previous_epoch_target_balance * 3 >= total_active_balance * 2: + state.current_justified_checkpoint = Checkpoint( + epoch=previous_epoch, root=get_block_root(state, previous_epoch) + ) + state.justification_bits[1] = 0b1 + if current_epoch_target_balance * 3 >= total_active_balance * 2: + state.current_justified_checkpoint = Checkpoint( + epoch=current_epoch, root=get_block_root(state, current_epoch) + ) + state.justification_bits[0] = 0b1 + + # Process finalizations + bits = state.justification_bits + # The 2nd/3rd/4th most recent epochs are justified, the 2nd using the 4th as source + if all(bits[1:4]) and old_previous_justified_checkpoint.epoch + 3 == current_epoch: + state.finalized_checkpoint = old_previous_justified_checkpoint + # The 2nd/3rd most recent epochs are justified, the 2nd using the 3rd as source + if all(bits[1:3]) and old_previous_justified_checkpoint.epoch + 2 == current_epoch: + state.finalized_checkpoint = old_previous_justified_checkpoint + # The 1st/2nd/3rd most recent epochs are justified, the 1st using the 3rd as source + if all(bits[0:3]) and old_current_justified_checkpoint.epoch + 2 == current_epoch: + state.finalized_checkpoint = old_current_justified_checkpoint + # The 1st/2nd most recent epochs are justified, the 1st using the 2nd as source + if all(bits[0:2]) and old_current_justified_checkpoint.epoch + 1 == current_epoch: + state.finalized_checkpoint = old_current_justified_checkpoint + + +- name: xor#phase0 + sources: + - file: packages/utils/src/bytes/browser.ts + search: export function xor( + spec: | + + def xor(bytes_1: Bytes32, bytes_2: Bytes32) -> Bytes32: + """ + Return the exclusive-or of two 32-byte strings. + """ + return Bytes32(a ^ b for a, b in zip(bytes_1, bytes_2)) + diff --git a/specrefs/presets.yml b/specrefs/presets.yml new file mode 100644 index 000000000000..d936edd1bfbb --- /dev/null +++ b/specrefs/presets.yml @@ -0,0 +1,683 @@ +- name: BASE_REWARD_FACTOR#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "BASE_REWARD_FACTOR:" + spec: | + + BASE_REWARD_FACTOR: uint64 = 64 + + +- name: BUILDER_PENDING_WITHDRAWALS_LIMIT#gloas + sources: + - file: packages/params/src/presets/mainnet.ts + search: "BUILDER_PENDING_WITHDRAWALS_LIMIT:" + spec: | + + BUILDER_PENDING_WITHDRAWALS_LIMIT: uint64 = 1048576 + + +- name: BUILDER_REGISTRY_LIMIT#gloas + sources: + - file: packages/params/src/presets/mainnet.ts + search: "BUILDER_REGISTRY_LIMIT:" + spec: | + + BUILDER_REGISTRY_LIMIT: uint64 = 1099511627776 + + +- name: BYTES_PER_LOGS_BLOOM#bellatrix + sources: + - file: packages/params/src/presets/mainnet.ts + search: "BYTES_PER_LOGS_BLOOM:" + spec: | + + BYTES_PER_LOGS_BLOOM: uint64 = 256 + + +- name: CELLS_PER_EXT_BLOB#fulu + sources: + - file: packages/params/src/presets/mainnet.ts + search: "CELLS_PER_EXT_BLOB:" + spec: | + + CELLS_PER_EXT_BLOB = 128 + + +- name: EFFECTIVE_BALANCE_INCREMENT#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "EFFECTIVE_BALANCE_INCREMENT:" + spec: | + + EFFECTIVE_BALANCE_INCREMENT: Gwei = 1000000000 + + +- name: EPOCHS_PER_ETH1_VOTING_PERIOD#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "EPOCHS_PER_ETH1_VOTING_PERIOD:" + spec: | + + EPOCHS_PER_ETH1_VOTING_PERIOD: uint64 = 64 + + +- name: EPOCHS_PER_HISTORICAL_VECTOR#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "EPOCHS_PER_HISTORICAL_VECTOR:" + spec: | + + EPOCHS_PER_HISTORICAL_VECTOR: uint64 = 65536 + + +- name: EPOCHS_PER_SLASHINGS_VECTOR#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "EPOCHS_PER_SLASHINGS_VECTOR:" + spec: | + + EPOCHS_PER_SLASHINGS_VECTOR: uint64 = 8192 + + +- name: EPOCHS_PER_SYNC_COMMITTEE_PERIOD#altair + sources: + - file: packages/params/src/presets/mainnet.ts + search: "EPOCHS_PER_SYNC_COMMITTEE_PERIOD:" + spec: | + + EPOCHS_PER_SYNC_COMMITTEE_PERIOD: uint64 = 256 + + +- name: FIELD_ELEMENTS_PER_BLOB#deneb + sources: + - file: packages/params/src/presets/mainnet.ts + search: "FIELD_ELEMENTS_PER_BLOB:" + spec: | + + FIELD_ELEMENTS_PER_BLOB: uint64 = 4096 + + +- name: FIELD_ELEMENTS_PER_CELL#fulu + sources: + - file: packages/params/src/presets/mainnet.ts + search: "FIELD_ELEMENTS_PER_CELL:" + spec: | + + FIELD_ELEMENTS_PER_CELL: uint64 = 64 + + +- name: FIELD_ELEMENTS_PER_EXT_BLOB#fulu + sources: + - file: packages/params/src/presets/mainnet.ts + search: "FIELD_ELEMENTS_PER_EXT_BLOB:" + spec: | + + FIELD_ELEMENTS_PER_EXT_BLOB = 8192 + + +- name: HISTORICAL_ROOTS_LIMIT#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "HISTORICAL_ROOTS_LIMIT:" + spec: | + + HISTORICAL_ROOTS_LIMIT: uint64 = 16777216 + + +- name: HYSTERESIS_DOWNWARD_MULTIPLIER#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "HYSTERESIS_DOWNWARD_MULTIPLIER:" + spec: | + + HYSTERESIS_DOWNWARD_MULTIPLIER: uint64 = 1 + + +- name: HYSTERESIS_QUOTIENT#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "HYSTERESIS_QUOTIENT:" + spec: | + + HYSTERESIS_QUOTIENT: uint64 = 4 + + +- name: HYSTERESIS_UPWARD_MULTIPLIER#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "HYSTERESIS_UPWARD_MULTIPLIER:" + spec: | + + HYSTERESIS_UPWARD_MULTIPLIER: uint64 = 5 + + +- name: INACTIVITY_PENALTY_QUOTIENT#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "INACTIVITY_PENALTY_QUOTIENT:" + spec: | + + INACTIVITY_PENALTY_QUOTIENT: uint64 = 67108864 + + +- name: INACTIVITY_PENALTY_QUOTIENT_ALTAIR#altair + sources: + - file: packages/params/src/presets/mainnet.ts + search: "INACTIVITY_PENALTY_QUOTIENT_ALTAIR:" + spec: | + + INACTIVITY_PENALTY_QUOTIENT_ALTAIR: uint64 = 50331648 + + +- name: INACTIVITY_PENALTY_QUOTIENT_BELLATRIX#bellatrix + sources: + - file: packages/params/src/presets/mainnet.ts + search: "INACTIVITY_PENALTY_QUOTIENT_BELLATRIX:" + spec: | + + INACTIVITY_PENALTY_QUOTIENT_BELLATRIX: uint64 = 16777216 + + +- name: KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH#fulu + sources: + - file: packages/params/src/presets/mainnet.ts + search: "KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH:" + spec: | + + KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH: uint64 = 4 + + +- name: KZG_COMMITMENT_INCLUSION_PROOF_DEPTH#deneb + sources: + - file: packages/params/src/presets/mainnet.ts + search: "KZG_COMMITMENT_INCLUSION_PROOF_DEPTH:" + spec: | + + KZG_COMMITMENT_INCLUSION_PROOF_DEPTH: uint64 = 17 + + +- name: MAX_ATTESTATIONS#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_ATTESTATIONS:" + spec: | + + MAX_ATTESTATIONS = 128 + + +- name: MAX_ATTESTATIONS_ELECTRA#electra + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_ATTESTATIONS_ELECTRA:" + spec: | + + MAX_ATTESTATIONS_ELECTRA = 8 + + +- name: MAX_ATTESTER_SLASHINGS#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_ATTESTER_SLASHINGS:" + spec: | + + MAX_ATTESTER_SLASHINGS = 2 + + +- name: MAX_ATTESTER_SLASHINGS_ELECTRA#electra + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_ATTESTER_SLASHINGS_ELECTRA:" + spec: | + + MAX_ATTESTER_SLASHINGS_ELECTRA = 1 + + +- name: MAX_BLOB_COMMITMENTS_PER_BLOCK#deneb + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_BLOB_COMMITMENTS_PER_BLOCK:" + spec: | + + MAX_BLOB_COMMITMENTS_PER_BLOCK: uint64 = 4096 + + +- name: MAX_BLS_TO_EXECUTION_CHANGES#capella + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_BLS_TO_EXECUTION_CHANGES:" + spec: | + + MAX_BLS_TO_EXECUTION_CHANGES = 16 + + +- name: MAX_BUILDERS_PER_WITHDRAWALS_SWEEP#gloas + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_BUILDERS_PER_WITHDRAWALS_SWEEP:" + spec: | + + MAX_BUILDERS_PER_WITHDRAWALS_SWEEP = 16384 + + +- name: MAX_BYTES_PER_TRANSACTION#bellatrix + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_BYTES_PER_TRANSACTION:" + spec: | + + MAX_BYTES_PER_TRANSACTION: uint64 = 1073741824 + + +- name: MAX_COMMITTEES_PER_SLOT#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_COMMITTEES_PER_SLOT:" + spec: | + + MAX_COMMITTEES_PER_SLOT: uint64 = 64 + + +- name: MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD#electra + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD:" + spec: | + + MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: uint64 = 2 + + +- name: MAX_DEPOSITS#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_DEPOSITS:" + spec: | + + MAX_DEPOSITS = 16 + + +- name: MAX_DEPOSIT_REQUESTS_PER_PAYLOAD#electra + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_DEPOSIT_REQUESTS_PER_PAYLOAD:" + spec: | + + MAX_DEPOSIT_REQUESTS_PER_PAYLOAD: uint64 = 8192 + + +- name: MAX_EFFECTIVE_BALANCE#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_EFFECTIVE_BALANCE:" + spec: | + + MAX_EFFECTIVE_BALANCE: Gwei = 32000000000 + + +- name: MAX_EFFECTIVE_BALANCE_ELECTRA#electra + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_EFFECTIVE_BALANCE_ELECTRA:" + spec: | + + MAX_EFFECTIVE_BALANCE_ELECTRA: Gwei = 2048000000000 + + +- name: MAX_EXTRA_DATA_BYTES#bellatrix + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_EXTRA_DATA_BYTES:" + spec: | + + MAX_EXTRA_DATA_BYTES = 32 + + +- name: MAX_PAYLOAD_ATTESTATIONS#gloas + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_PAYLOAD_ATTESTATIONS:" + spec: | + + MAX_PAYLOAD_ATTESTATIONS = 4 + + +- name: MAX_PENDING_DEPOSITS_PER_EPOCH#electra + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_PENDING_DEPOSITS_PER_EPOCH:" + spec: | + + MAX_PENDING_DEPOSITS_PER_EPOCH: uint64 = 16 + + +- name: MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP#electra + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP:" + spec: | + + MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: uint64 = 8 + + +- name: MAX_PROPOSER_SLASHINGS#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_PROPOSER_SLASHINGS:" + spec: | + + MAX_PROPOSER_SLASHINGS = 16 + + +- name: MAX_SEED_LOOKAHEAD#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_SEED_LOOKAHEAD:" + spec: | + + MAX_SEED_LOOKAHEAD: uint64 = 4 + + +- name: MAX_TRANSACTIONS_PER_PAYLOAD#bellatrix + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_TRANSACTIONS_PER_PAYLOAD:" + spec: | + + MAX_TRANSACTIONS_PER_PAYLOAD: uint64 = 1048576 + + +- name: MAX_VALIDATORS_PER_COMMITTEE#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_VALIDATORS_PER_COMMITTEE:" + spec: | + + MAX_VALIDATORS_PER_COMMITTEE: uint64 = 2048 + + +- name: MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP#capella + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP:" + spec: | + + MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP = 16384 + + +- name: MAX_VOLUNTARY_EXITS#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_VOLUNTARY_EXITS:" + spec: | + + MAX_VOLUNTARY_EXITS = 16 + + +- name: MAX_WITHDRAWALS_PER_PAYLOAD#capella + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_WITHDRAWALS_PER_PAYLOAD:" + spec: | + + MAX_WITHDRAWALS_PER_PAYLOAD: uint64 = 16 + + +- name: MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD#electra + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD:" + spec: | + + MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: uint64 = 16 + + +- name: MIN_ACTIVATION_BALANCE#electra + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MIN_ACTIVATION_BALANCE:" + spec: | + + MIN_ACTIVATION_BALANCE: Gwei = 32000000000 + + +- name: MIN_ATTESTATION_INCLUSION_DELAY#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MIN_ATTESTATION_INCLUSION_DELAY:" + spec: | + + MIN_ATTESTATION_INCLUSION_DELAY: uint64 = 1 + + +- name: MIN_DEPOSIT_AMOUNT#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MIN_DEPOSIT_AMOUNT:" + spec: | + + MIN_DEPOSIT_AMOUNT: Gwei = 1000000000 + + +- name: MIN_EPOCHS_TO_INACTIVITY_PENALTY#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MIN_EPOCHS_TO_INACTIVITY_PENALTY:" + spec: | + + MIN_EPOCHS_TO_INACTIVITY_PENALTY: uint64 = 4 + + +- name: MIN_SEED_LOOKAHEAD#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MIN_SEED_LOOKAHEAD:" + spec: | + + MIN_SEED_LOOKAHEAD: uint64 = 1 + + +- name: MIN_SLASHING_PENALTY_QUOTIENT#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MIN_SLASHING_PENALTY_QUOTIENT:" + spec: | + + MIN_SLASHING_PENALTY_QUOTIENT: uint64 = 128 + + +- name: MIN_SLASHING_PENALTY_QUOTIENT_ALTAIR#altair + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MIN_SLASHING_PENALTY_QUOTIENT_ALTAIR:" + spec: | + + MIN_SLASHING_PENALTY_QUOTIENT_ALTAIR: uint64 = 64 + + +- name: MIN_SLASHING_PENALTY_QUOTIENT_BELLATRIX#bellatrix + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MIN_SLASHING_PENALTY_QUOTIENT_BELLATRIX:" + spec: | + + MIN_SLASHING_PENALTY_QUOTIENT_BELLATRIX: uint64 = 32 + + +- name: MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA#electra + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA:" + spec: | + + MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA: uint64 = 4096 + + +- name: MIN_SYNC_COMMITTEE_PARTICIPANTS#altair + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MIN_SYNC_COMMITTEE_PARTICIPANTS:" + spec: | + + MIN_SYNC_COMMITTEE_PARTICIPANTS = 1 + + +- name: NUMBER_OF_COLUMNS#fulu + sources: + - file: packages/params/src/presets/mainnet.ts + search: "NUMBER_OF_COLUMNS:" + spec: | + + NUMBER_OF_COLUMNS: uint64 = 128 + + +- name: PENDING_CONSOLIDATIONS_LIMIT#electra + sources: + - file: packages/params/src/presets/mainnet.ts + search: "PENDING_CONSOLIDATIONS_LIMIT:" + spec: | + + PENDING_CONSOLIDATIONS_LIMIT: uint64 = 262144 + + +- name: PENDING_DEPOSITS_LIMIT#electra + sources: + - file: packages/params/src/presets/mainnet.ts + search: "PENDING_DEPOSITS_LIMIT:" + spec: | + + PENDING_DEPOSITS_LIMIT: uint64 = 134217728 + + +- name: PENDING_PARTIAL_WITHDRAWALS_LIMIT#electra + sources: + - file: packages/params/src/presets/mainnet.ts + search: "PENDING_PARTIAL_WITHDRAWALS_LIMIT:" + spec: | + + PENDING_PARTIAL_WITHDRAWALS_LIMIT: uint64 = 134217728 + + +- name: PROPORTIONAL_SLASHING_MULTIPLIER#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "PROPORTIONAL_SLASHING_MULTIPLIER:" + spec: | + + PROPORTIONAL_SLASHING_MULTIPLIER: uint64 = 1 + + +- name: PROPORTIONAL_SLASHING_MULTIPLIER_ALTAIR#altair + sources: + - file: packages/params/src/presets/mainnet.ts + search: "PROPORTIONAL_SLASHING_MULTIPLIER_ALTAIR:" + spec: | + + PROPORTIONAL_SLASHING_MULTIPLIER_ALTAIR: uint64 = 2 + + +- name: PROPORTIONAL_SLASHING_MULTIPLIER_BELLATRIX#bellatrix + sources: + - file: packages/params/src/presets/mainnet.ts + search: "PROPORTIONAL_SLASHING_MULTIPLIER_BELLATRIX:" + spec: | + + PROPORTIONAL_SLASHING_MULTIPLIER_BELLATRIX: uint64 = 3 + + +- name: PROPOSER_REWARD_QUOTIENT#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "PROPOSER_REWARD_QUOTIENT:" + spec: | + + PROPOSER_REWARD_QUOTIENT: uint64 = 8 + + +- name: PTC_SIZE#gloas + sources: + - file: packages/params/src/presets/mainnet.ts + search: "PTC_SIZE:" + spec: | + + PTC_SIZE: uint64 = 512 + + +- name: SHUFFLE_ROUND_COUNT#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "SHUFFLE_ROUND_COUNT:" + spec: | + + SHUFFLE_ROUND_COUNT: uint64 = 90 + + +- name: SLOTS_PER_EPOCH#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "SLOTS_PER_EPOCH:" + spec: | + + SLOTS_PER_EPOCH: uint64 = 32 + + +- name: SLOTS_PER_HISTORICAL_ROOT#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "SLOTS_PER_HISTORICAL_ROOT:" + spec: | + + SLOTS_PER_HISTORICAL_ROOT: uint64 = 8192 + + +- name: SYNC_COMMITTEE_SIZE#altair + sources: + - file: packages/params/src/presets/mainnet.ts + search: "SYNC_COMMITTEE_SIZE:" + spec: | + + SYNC_COMMITTEE_SIZE: uint64 = 512 + + +- name: TARGET_COMMITTEE_SIZE#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "TARGET_COMMITTEE_SIZE:" + spec: | + + TARGET_COMMITTEE_SIZE: uint64 = 128 + + +- name: UPDATE_TIMEOUT#altair + sources: + - file: packages/params/src/presets/mainnet.ts + search: "UPDATE_TIMEOUT:" + spec: | + + UPDATE_TIMEOUT = 8192 + + +- name: VALIDATOR_REGISTRY_LIMIT#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "VALIDATOR_REGISTRY_LIMIT:" + spec: | + + VALIDATOR_REGISTRY_LIMIT: uint64 = 1099511627776 + + +- name: WHISTLEBLOWER_REWARD_QUOTIENT#phase0 + sources: + - file: packages/params/src/presets/mainnet.ts + search: "WHISTLEBLOWER_REWARD_QUOTIENT:" + spec: | + + WHISTLEBLOWER_REWARD_QUOTIENT: uint64 = 512 + + +- name: WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA#electra + sources: + - file: packages/params/src/presets/mainnet.ts + search: "WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA:" + spec: | + + WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA: uint64 = 4096 + diff --git a/specrefs/types.yml b/specrefs/types.yml new file mode 100644 index 000000000000..c5d28cf5881d --- /dev/null +++ b/specrefs/types.yml @@ -0,0 +1,370 @@ +- name: BLSPubkey#phase0 + sources: + - file: packages/types/src/primitive/sszTypes.ts + search: export const BLSPubkey = + spec: | + + BLSPubkey = Bytes48 + + +- name: BLSSignature#phase0 + sources: + - file: packages/types/src/primitive/sszTypes.ts + search: export const BLSSignature = + spec: | + + BLSSignature = Bytes96 + + +- name: Blob#deneb + sources: + - file: packages/types/src/deneb/sszTypes.ts + search: export const Blob = + spec: | + + Blob = ByteVector[BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_BLOB] + + +- name: BlobIndex#deneb + sources: + - file: packages/types/src/primitive/sszTypes.ts + search: export const BlobIndex = + spec: | + + BlobIndex = uint64 + + +- name: BuilderIndex#gloas + sources: + - file: packages/types/src/primitive/sszTypes.ts + search: export const BuilderIndex = + spec: | + + BuilderIndex = uint64 + + +- name: Cell#fulu + sources: + - file: packages/types/src/fulu/sszTypes.ts + search: export const Cell = + spec: | + + Cell = ByteVector[BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_CELL] + + +- name: CellIndex#fulu + sources: [] + spec: | + + CellIndex = uint64 + + +- name: ColumnIndex#fulu + sources: + - file: packages/types/src/primitive/sszTypes.ts + search: export const ColumnIndex = + spec: | + + ColumnIndex = uint64 + + +- name: CommitmentIndex#fulu + sources: [] + spec: | + + CommitmentIndex = uint64 + + +- name: CommitteeIndex#phase0 + sources: + - file: packages/types/src/primitive/sszTypes.ts + search: export const CommitteeIndex = + spec: | + + CommitteeIndex = uint64 + + +- name: CurrentSyncCommitteeBranch#altair + sources: + - file: packages/types/src/altair/sszTypes.ts + search: export const CurrentSyncCommitteeBranch = + spec: | + + CurrentSyncCommitteeBranch = Vector[Bytes32, floorlog2(CURRENT_SYNC_COMMITTEE_GINDEX)] + + +- name: CurrentSyncCommitteeBranch#electra + sources: + - file: packages/types/src/electra/sszTypes.ts + search: export const CurrentSyncCommitteeBranch = + spec: | + + CurrentSyncCommitteeBranch = Vector[Bytes32, floorlog2(CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA)] + + +- name: CustodyIndex#fulu + sources: + - file: packages/types/src/primitive/sszTypes.ts + search: export const CustodyIndex = + spec: | + + CustodyIndex = uint64 + + +- name: Domain#phase0 + sources: + - file: packages/types/src/primitive/sszTypes.ts + search: export const Domain = + spec: | + + Domain = Bytes32 + + +- name: DomainType#phase0 + sources: + - file: packages/types/src/primitive/sszTypes.ts + search: export const DomainType = + spec: | + + DomainType = Bytes4 + + +- name: Epoch#phase0 + sources: + - file: packages/types/src/primitive/sszTypes.ts + search: export const Epoch = + spec: | + + Epoch = uint64 + + +- name: Ether#phase0 + sources: [] + spec: | + + Ether = uint64 + + +- name: ExecutionAddress#bellatrix + sources: + - file: packages/types/src/primitive/sszTypes.ts + search: export const ExecutionAddress = + spec: | + + ExecutionAddress = Bytes20 + + +- name: ExecutionBranch#capella + sources: + - file: packages/types/src/capella/sszTypes.ts + search: export const ExecutionBranch = + spec: | + + ExecutionBranch = Vector[Bytes32, floorlog2(EXECUTION_PAYLOAD_GINDEX)] + + +- name: FinalityBranch#altair + sources: + - file: packages/types/src/altair/sszTypes.ts + search: export const FinalityBranch = + spec: | + + FinalityBranch = Vector[Bytes32, floorlog2(FINALIZED_ROOT_GINDEX)] + + +- name: FinalityBranch#electra + sources: + - file: packages/types/src/electra/sszTypes.ts + search: export const FinalityBranch = + spec: | + + FinalityBranch = Vector[Bytes32, floorlog2(FINALIZED_ROOT_GINDEX_ELECTRA)] + + +- name: ForkDigest#phase0 + sources: + - file: packages/types/src/primitive/sszTypes.ts + search: export const ForkDigest = + spec: | + + ForkDigest = Bytes4 + + +- name: G1Point#deneb + sources: + - file: packages/types/src/deneb/sszTypes.ts + search: export const G1Point = + spec: | + + G1Point = Bytes48 + + +- name: G2Point#deneb + sources: + - file: packages/types/src/deneb/sszTypes.ts + search: export const G2Point = + spec: | + + G2Point = Bytes96 + + +- name: Gwei#phase0 + sources: + - file: packages/types/src/primitive/sszTypes.ts + search: export const Gwei = + spec: | + + Gwei = uint64 + + +- name: Hash32#phase0 + sources: [] + spec: | + + Hash32 = Bytes32 + + +- name: KZGCommitment#deneb + sources: + - file: packages/types/src/deneb/sszTypes.ts + search: export const KZGCommitment = + spec: | + + KZGCommitment = Bytes48 + + +- name: KZGProof#deneb + sources: + - file: packages/types/src/deneb/sszTypes.ts + search: export const KZGProof = + spec: | + + KZGProof = Bytes48 + + +- name: NextSyncCommitteeBranch#altair + sources: + - file: packages/types/src/altair/sszTypes.ts + search: export const NextSyncCommitteeBranch = + spec: | + + NextSyncCommitteeBranch = Vector[Bytes32, floorlog2(NEXT_SYNC_COMMITTEE_GINDEX)] + + +- name: NextSyncCommitteeBranch#electra + sources: + - file: packages/types/src/electra/sszTypes.ts + search: export const NextSyncCommitteeBranch = + spec: | + + NextSyncCommitteeBranch = Vector[Bytes32, floorlog2(NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA)] + + +- name: NodeID#phase0 + sources: [] + spec: | + + NodeID = uint256 + + +- name: ParticipationFlags#altair + sources: + - file: packages/types/src/primitive/sszTypes.ts + search: export const ParticipationFlags = + spec: | + + ParticipationFlags = uint8 + + +- name: PayloadId#bellatrix + sources: [] + spec: | + + PayloadId = Bytes8 + + +- name: PayloadStatus#gloas + sources: [] + spec: | + + PayloadStatus = uint8 + + +- name: Root#phase0 + sources: + - file: packages/types/src/primitive/sszTypes.ts + search: export const Root = + spec: | + + Root = Bytes32 + + +- name: RowIndex#fulu + sources: + - file: packages/types/src/primitive/sszTypes.ts + search: export const RowIndex = + spec: | + + RowIndex = uint64 + + +- name: Slot#phase0 + sources: + - file: packages/types/src/primitive/sszTypes.ts + search: export const Slot = + spec: | + + Slot = uint64 + + +- name: SubnetID#phase0 + sources: [] + spec: | + + SubnetID = uint64 + + +- name: Transaction#bellatrix + sources: + - file: packages/types/src/bellatrix/sszTypes.ts + search: export const Transaction = + spec: | + + Transaction = ByteList[MAX_BYTES_PER_TRANSACTION] + + +- name: ValidatorIndex#phase0 + sources: + - file: packages/types/src/primitive/sszTypes.ts + search: export const ValidatorIndex = + spec: | + + ValidatorIndex = uint64 + + +- name: Version#phase0 + sources: + - file: packages/types/src/primitive/sszTypes.ts + search: export const Version = + spec: | + + Version = Bytes4 + + +- name: VersionedHash#deneb + sources: + - file: packages/types/src/deneb/sszTypes.ts + search: export const VersionedHash = + spec: | + + VersionedHash = Bytes32 + + +- name: WithdrawalIndex#capella + sources: + - file: packages/types/src/primitive/sszTypes.ts + search: export const WithdrawalIndex = + spec: | + + WithdrawalIndex = uint64 +