diff --git a/.github/workflows/kurtosis-eip8025.yml b/.github/workflows/kurtosis-eip8025.yml new file mode 100644 index 00000000000..87c0e949236 --- /dev/null +++ b/.github/workflows/kurtosis-eip8025.yml @@ -0,0 +1,139 @@ +# Test that the EIP-8025 Kurtosis testnet starts and the proof engine integrates +# correctly with real zkboost-server backends. +name: kurtosis eip8025 + +on: + push: + branches: + - unstable + pull_request: + merge_group: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + run-eip8025-testnet: + runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x' || 'ubuntu-latest' }} + steps: + - uses: actions/checkout@v5 + + - name: Install Kurtosis + run: | + echo "deb [trusted=yes] https://sdk.kurtosis.com/kurtosis-cli-release-artifacts/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list + sudo apt update + sudo apt install -y kurtosis-cli + kurtosis analytics disable + + - name: Install yq + run: | + sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 + sudo chmod +x /usr/local/bin/yq + + - name: Start EIP-8025 zkboost testnet + run: ./start_eip8025_testnet.sh -e eip8025-zkboost -n network_params_eip8025_zkboost.yaml + working-directory: scripts/local_testnet + + - name: Wait for chain liveness + run: | + BEACON_URL=$(kurtosis port print eip8025-zkboost cl-1-lighthouse-reth http) + echo "Polling $BEACON_URL for head slot > 10..." + for i in $(seq 1 20); do + SLOT=$(curl -sf "$BEACON_URL/eth/v1/beacon/headers/head" \ + | jq -r '.data.header.message.slot' 2>/dev/null || echo 0) + echo " attempt $i: head slot = $SLOT" + if [ "$SLOT" -gt 10 ]; then + echo "Chain is live at slot $SLOT" + break + fi + if [ "$i" -eq 20 ]; then + echo "Timed out waiting for head slot > 10" + exit 1 + fi + sleep 30 + done + + - name: Inspect beacon node state + run: | + set -euo pipefail + ENCLAVE=eip8025-zkboost + SERVICES=(cl-1-lighthouse-reth cl-2-lighthouse-reth cl-3-lighthouse-reth cl-4-lighthouse-reth) + FAILED=0 + + for SVC in "${SERVICES[@]}"; do + URL=$(kurtosis port print "$ENCLAVE" "$SVC" http) + echo "=== $SVC ($URL) ===" + + # Syncing status — must not be syncing + SYNCING=$(curl -sf "$URL/eth/v1/node/syncing" | jq -r '.data.is_syncing') + echo " is_syncing: $SYNCING" + if [ "$SYNCING" != "false" ]; then + echo " FAIL: $SVC is still syncing" + FAILED=1 + fi + + # Peer count — must have at least one connected peer + PEERS=$(curl -sf "$URL/eth/v1/node/peer_count" | jq -r '.data.connected') + echo " connected peers: $PEERS" + if [ "${PEERS:-0}" -lt 1 ]; then + echo " FAIL: $SVC has no connected peers" + FAILED=1 + fi + + # Head slot — must be non-zero + SLOT=$(curl -sf "$URL/eth/v1/beacon/headers/head" | jq -r '.data.header.message.slot') + echo " head slot: $SLOT" + if [ "${SLOT:-0}" -lt 1 ]; then + echo " FAIL: $SVC head slot is 0" + FAILED=1 + fi + + # Finality checkpoints — informational, log the finalized epoch + FINALIZED=$(curl -sf "$URL/eth/v1/beacon/states/head/finality_checkpoints" \ + | jq -r '.data.finalized.epoch') + echo " finalized epoch: $FINALIZED" + done + + exit "$FAILED" + + - name: Check zkboost sidecars are running and generating proofs + run: | + ENCLAVE=eip8025-zkboost + + # Both zkboost services must be in RUNNING state + kurtosis enclave inspect "$ENCLAVE" | grep -E "zkboost-[12].*RUNNING" \ + || { echo "FAIL: one or more zkboost services not in RUNNING state"; exit 1; } + + # Each zkboost sidecar must have generated at least one proof. + # Check via the Prometheus metrics endpoint (zkboost_prove_total) rather than + # log scraping — kurtosis service logs may not be available in all CI environments. + for SVC in zkboost-1 zkboost-2; do + URL=$(kurtosis port print "$ENCLAVE" "$SVC" http) + COUNT=$(curl -sf "$URL/metrics" \ + | awk '/^zkboost_prove_total\{/ {sum += $2} END {print int(sum)}') + echo "$SVC: $COUNT proofs generated" + if [ "${COUNT:-0}" -lt 1 ]; then + echo "FAIL: $SVC has not generated any proofs" + exit 1 + fi + done + + - name: Stop testnet and collect logs + if: always() + run: | + mkdir -p scripts/local_testnet/logs + ENCLAVE=eip8025-zkboost + for SVC in cl-1-lighthouse-reth cl-2-lighthouse-reth cl-3-lighthouse-reth cl-4-lighthouse-reth \ + zkboost-1 zkboost-2; do + kurtosis service logs "$ENCLAVE" "$SVC" > "scripts/local_testnet/logs/${SVC}.log" 2>&1 || true + done + kurtosis enclave rm -f "$ENCLAVE" || true + + - name: Upload logs artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: logs-kurtosis-eip8025 + path: scripts/local_testnet/logs + retention-days: 3 diff --git a/.github/workflows/nightly-tests.yml b/.github/workflows/nightly-tests.yml index 636d0ea0dd9..2f9506326a8 100644 --- a/.github/workflows/nightly-tests.yml +++ b/.github/workflows/nightly-tests.yml @@ -24,6 +24,8 @@ env: LIGHTHOUSE_GITHUB_TOKEN: ${{ secrets.LIGHTHOUSE_GITHUB_TOKEN }} # Disable incremental compilation CARGO_INCREMENTAL: 0 + # Keep optional-proofs branch-family caches separate while their dependency graphs diverge. + RUST_CACHE_EXTRA_IDENTIFIER: ${{ contains(github.head_ref || github.ref_name, 'optional-proofs-unstable') && 'optional-proofs-unstable' || contains(github.head_ref || github.ref_name, 'optional-proofs') && 'optional-proofs' || '' }} # Enable portable to prevent issues with caching `blst` for the wrong CPU type TEST_FEATURES: portable @@ -58,6 +60,7 @@ jobs: uses: moonrepo/setup-rust@v1 with: channel: stable + cache-extra-identifier: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} cache-target: release bins: cargo-nextest - name: Run beacon_chain tests for ${{ matrix.fork }} @@ -82,6 +85,7 @@ jobs: uses: moonrepo/setup-rust@v1 with: channel: stable + cache-extra-identifier: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} cache-target: release bins: cargo-nextest - name: Run operation_pool tests for ${{ matrix.fork }} @@ -106,6 +110,7 @@ jobs: uses: moonrepo/setup-rust@v1 with: channel: stable + cache-extra-identifier: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} cache-target: release bins: cargo-nextest - name: Create CI logger dir diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 1d66bd30e78..c5f38f45337 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -26,6 +26,8 @@ env: CARGO_INCREMENTAL: 0 # Enable portable to prevent issues with caching `blst` for the wrong CPU type TEST_FEATURES: portable + # Keep optional-proofs branch-family caches separate while their dependency graphs diverge. + RUST_CACHE_EXTRA_IDENTIFIER: ${{ contains(github.head_ref || github.ref_name, 'optional-proofs-unstable') && 'optional-proofs-unstable' || contains(github.head_ref || github.ref_name, 'optional-proofs') && 'optional-proofs' || '' }} jobs: check-labels: runs-on: ubuntu-latest @@ -112,6 +114,7 @@ jobs: uses: moonrepo/setup-rust@v1 with: channel: stable + cache-extra-identifier: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} cache-target: release bins: cargo-nextest env: @@ -120,12 +123,37 @@ jobs: uses: Swatinem/rust-cache@v2 with: cache-provider: warpbuild + key: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} - name: Run tests in release run: make test-release - name: Show cache stats if: env.SELF_HOSTED_RUNNERS == 'true' continue-on-error: true run: sccache --show-stats + proof-engine-tests: + name: proof-engine-tests + needs: [check-labels] + if: needs.check-labels.outputs.skip_ci != 'true' + runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-4x;snapshot.key=lighthouse-ubuntu-latest-v1' || 'ubuntu-latest' }} + steps: + - uses: actions/checkout@v5 + - if: github.repository != 'sigp/lighthouse' + name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: + channel: stable + cache-extra-identifier: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} + cache-target: release + bins: cargo-nextest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - if: github.repository == 'sigp/lighthouse' + uses: Swatinem/rust-cache@v2 + with: + cache-provider: warpbuild + key: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} + - name: Run proof engine tests sequentially + run: make test-proof-engine beacon-chain-tests: name: beacon-chain-tests needs: [check-labels] @@ -140,12 +168,14 @@ jobs: uses: moonrepo/setup-rust@v1 with: channel: stable + cache-extra-identifier: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} cache-target: release bins: cargo-nextest - if: github.repository == 'sigp/lighthouse' uses: Swatinem/rust-cache@v2 with: cache-provider: warpbuild + key: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} - name: Run beacon_chain tests for all known forks run: make test-beacon-chain http-api-tests: @@ -162,12 +192,14 @@ jobs: uses: moonrepo/setup-rust@v1 with: channel: stable + cache-extra-identifier: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} cache-target: release bins: cargo-nextest - if: github.repository == 'sigp/lighthouse' uses: Swatinem/rust-cache@v2 with: cache-provider: warpbuild + key: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} - name: Run http_api tests for all recent forks run: make test-http-api op-pool-tests: @@ -183,6 +215,7 @@ jobs: uses: moonrepo/setup-rust@v1 with: channel: stable + cache-extra-identifier: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} cache-target: release bins: cargo-nextest - name: Run operation_pool tests for all known forks @@ -200,6 +233,7 @@ jobs: uses: moonrepo/setup-rust@v1 with: channel: stable + cache-extra-identifier: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} cache-target: release bins: cargo-nextest - name: Create CI logger dir @@ -229,6 +263,7 @@ jobs: uses: moonrepo/setup-rust@v1 with: channel: stable + cache-extra-identifier: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} cache-target: release bins: cargo-nextest - name: Run slasher tests for all supported backends @@ -247,11 +282,13 @@ jobs: uses: moonrepo/setup-rust@v1 with: channel: stable + cache-extra-identifier: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} bins: cargo-nextest - if: github.repository == 'sigp/lighthouse' uses: Swatinem/rust-cache@v2 with: cache-provider: warpbuild + key: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} - name: Run tests in debug run: make test-debug state-transition-vectors-ubuntu: @@ -265,6 +302,7 @@ jobs: uses: moonrepo/setup-rust@v1 with: channel: stable + cache-extra-identifier: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} cache-target: release - name: Run state_transition_vectors in release. run: make run-state-transition-tests @@ -282,12 +320,14 @@ jobs: uses: moonrepo/setup-rust@v1 with: channel: stable + cache-extra-identifier: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} cache-target: release bins: cargo-nextest - if: github.repository == 'sigp/lighthouse' uses: Swatinem/rust-cache@v2 with: cache-provider: warpbuild + key: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} - name: Run consensus-spec-tests with blst and fake_crypto run: make test-ef basic-simulator-ubuntu: @@ -301,10 +341,11 @@ jobs: uses: moonrepo/setup-rust@v1 with: channel: stable + cache-extra-identifier: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} cache-target: release - name: Create log dir run: mkdir ${{ runner.temp }}/basic_simulator_logs - - name: Run a basic beacon chain sim that starts from Deneb + - name: Run a basic beacon chain sim run: cargo run --release --bin simulator basic-sim --disable-stdout-logging --log-dir ${{ runner.temp }}/basic_simulator_logs - name: Upload logs if: always() @@ -323,6 +364,7 @@ jobs: uses: moonrepo/setup-rust@v1 with: channel: stable + cache-extra-identifier: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} cache-target: release - name: Create log dir run: mkdir ${{ runner.temp }}/fallback_simulator_logs @@ -346,6 +388,7 @@ jobs: uses: moonrepo/setup-rust@v1 with: channel: stable + cache-extra-identifier: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} cache: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -362,6 +405,7 @@ jobs: uses: moonrepo/setup-rust@v1 with: channel: stable + cache-extra-identifier: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} cache-target: release components: rustfmt,clippy bins: cargo-audit,cargo-deny @@ -410,6 +454,7 @@ jobs: uses: moonrepo/setup-rust@v1 with: channel: nightly + cache-extra-identifier: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} bins: cargo-udeps cache: false env: @@ -447,6 +492,7 @@ jobs: uses: moonrepo/setup-rust@v1 with: channel: stable + cache-extra-identifier: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} cache-target: release - name: Run Makefile to trigger the bash script run: make cli-local @@ -461,6 +507,7 @@ jobs: uses: moonrepo/setup-rust@v1 with: channel: stable + cache-extra-identifier: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} - uses: taiki-e/install-action@cargo-hack - name: Check types feature powerset run: cargo hack check -p types --feature-powerset --no-dev-deps --exclude-features arbitrary-fuzz,portable @@ -477,6 +524,7 @@ jobs: uses: moonrepo/setup-rust@v1 with: channel: stable + cache-extra-identifier: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} cache-target: release bins: cargo-sort - name: Run cargo sort to check if Cargo.toml files are sorted diff --git a/.github/workflows/zkboost-tests.yml b/.github/workflows/zkboost-tests.yml new file mode 100644 index 00000000000..bad0f497bbf --- /dev/null +++ b/.github/workflows/zkboost-tests.yml @@ -0,0 +1,76 @@ +name: zkboost-tests + +on: + push: + branches: + - stable + - staging + - trying + - 'pr/*' + pull_request: + merge_group: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + RUSTFLAGS: "-D warnings -C debuginfo=0" + CARGO_INCREMENTAL: 0 + TEST_FEATURES: portable + # Keep optional-proofs branch-family caches separate while their dependency graphs diverge. + RUST_CACHE_EXTRA_IDENTIFIER: ${{ contains(github.head_ref || github.ref_name, 'optional-proofs-unstable') && 'optional-proofs-unstable' || contains(github.head_ref || github.ref_name, 'optional-proofs') && 'optional-proofs' || '' }} + # Use Clang for C/C++ compilation. Required because leveldb-sys uses + # -Wthread-safety which is a Clang-only flag unsupported by GCC. + CC: clang + CXX: clang++ + # leveldb-1.22's doc/bench/db_bench_sqlite3.cc uses time()/ctime() without + # including ; newer libc++ no longer pulls it in transitively. + CXXFLAGS: "-include ctime" + +jobs: + check-labels: + runs-on: ubuntu-latest + name: Check for 'skip-ci' label + outputs: + skip_ci: ${{ steps.set-output.outputs.SKIP_CI }} + steps: + - name: check for skip-ci label + id: set-output + env: + LABELS: ${{ toJson(github.event.pull_request.labels) }} + run: | + SKIP_CI="false" + if [ -z "${LABELS}" ] || [ "${LABELS}" = "null" ]; then + LABELS="none"; + else + LABELS=$(echo ${LABELS} | jq -r '.[].name') + fi + for label in ${LABELS}; do + if [ "$label" = "skip-ci" ]; then + SKIP_CI="true" + break + fi + done + echo "skip_ci=$SKIP_CI" >> $GITHUB_OUTPUT + + zkboost-tests: + name: zkboost-tests + needs: [check-labels] + if: needs.check-labels.outputs.skip_ci != 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Install dependencies + run: sudo apt update && sudo apt install -y git gcc g++ make cmake pkg-config llvm-dev libclang-dev clang + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: + channel: stable + cache-extra-identifier: ${{ env.RUST_CACHE_EXTRA_IDENTIFIER }} + cache-target: release + bins: cargo-nextest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Run proof_engine_zkboost integration tests + run: make test-zkboost diff --git a/Cargo.lock b/Cargo.lock index 129be32fcdd..001f12fa763 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2855,8 +2855,10 @@ dependencies = [ "fs2", "hex", "kzg", + "lighthouse_network", "logging", "milhouse", + "network", "proto_array", "rayon", "serde", @@ -3386,14 +3388,18 @@ dependencies = [ "alloy-rlp", "alloy-rpc-types-eth", "arc-swap", + "async-stream", + "async-trait", "bls", "builder_client", "bytes", "eth2", "ethereum_serde_utils", "ethereum_ssz", + "ethereum_ssz_derive", "fixed_bytes", "fork_choice", + "futures", "hash-db", "hash256-std-hasher", "hex", @@ -3408,6 +3414,7 @@ dependencies = [ "pretty_reqwest_error", "rand 0.9.2", "reqwest", + "reqwest-eventsource", "sensitive_url", "serde", "serde_json", @@ -6952,6 +6959,22 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "proof_engine_test" +version = "8.1.3" +dependencies = [ + "anyhow", + "beacon_chain", + "execution_layer", + "futures", + "network", + "simulator", + "task_executor", + "tokio", + "tracing", + "types", +] + [[package]] name = "proptest" version = "1.9.0" @@ -7018,6 +7041,7 @@ dependencies = [ name = "proto_array" version = "0.2.0" dependencies = [ + "criterion", "ethereum_ssz", "ethereum_ssz_derive", "fixed_bytes", @@ -8200,22 +8224,29 @@ dependencies = [ name = "simulator" version = "0.2.0" dependencies = [ + "anyhow", + "beacon_chain", "clap", "environment", + "eth2", "execution_layer", "futures", "kzg", + "lighthouse_network", "logging", + "network_utils", "node_test_rig", "parking_lot", "rayon", "sensitive_url", "serde_json", + "task_executor", "tokio", "tracing", "tracing-subscriber", "typenum", "types", + "validator_http_api", ] [[package]] @@ -9521,6 +9552,7 @@ dependencies = [ "doppelganger_service", "environment", "eth2", + "execution_layer", "fdlimit", "graffiti_file", "hyper 1.8.1", @@ -9536,6 +9568,7 @@ dependencies = [ "slot_clock", "tokio", "tracing", + "typenum", "types", "validator_http_api", "validator_http_metrics", @@ -9677,6 +9710,7 @@ dependencies = [ "bls", "either", "eth2", + "execution_layer", "futures", "graffiti_file", "logging", diff --git a/Cargo.toml b/Cargo.toml index 71398530fe4..6560e810dfd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ members = [ "testing/ef_tests", "testing/execution_engine_integration", "testing/node_test_rig", + "testing/proof_engine", "testing/simulator", "testing/state_transition_vectors", "testing/validator_test_rig", @@ -141,7 +142,7 @@ eth2_network_config = { path = "common/eth2_network_config" } eth2_wallet = { path = "crypto/eth2_wallet" } ethereum_hashing = "0.8.0" ethereum_serde_utils = "0.8.0" -ethereum_ssz = { version = "0.10.0", features = ["context_deserialize"] } +ethereum_ssz = { version = "0.10.4", features = ["context_deserialize"] } ethereum_ssz_derive = "0.10.0" execution_layer = { path = "beacon_node/execution_layer" } filesystem = { path = "common/filesystem" } @@ -219,7 +220,7 @@ slashing_protection = { path = "validator_client/slashing_protection" } slot_clock = { path = "common/slot_clock" } smallvec = "1" snap = "1" -ssz_types = { version = "0.14.0", features = ["context_deserialize", "runtime_types"] } +ssz_types = { version = "0.14.1", features = ["context_deserialize", "runtime_types"] } state_processing = { path = "consensus/state_processing" } store = { path = "beacon_node/store" } strum = { version = "0.27", features = ["derive"] } @@ -275,4 +276,3 @@ debug = true [patch.crates-io] quick-protobuf = { git = "https://github.com/sigp/quick-protobuf.git", rev = "87c4ccb9bb2af494de375f5f6c62850badd26304" } - diff --git a/Makefile b/Makefile index dd57bb038e8..c75c7a647ba 100644 --- a/Makefile +++ b/Makefile @@ -33,11 +33,11 @@ PROFILE ?= release # List of all hard forks up to gloas. This list is used to set env variables for several tests so that # they run for different forks. # TODO(EIP-7732) Remove this once we extend network tests to support gloas and use RECENT_FORKS instead -RECENT_FORKS_BEFORE_GLOAS=electra fulu +RECENT_FORKS_BEFORE_GLOAS=fulu # List of all recent hard forks. This list is used to set env variables for http_api tests # Include phase0 to test the code paths in sync that are pre blobs -RECENT_FORKS=electra fulu gloas +RECENT_FORKS=fulu gloas # For network tests include phase0 to cover genesis syncing (blocks without blobs or columns) TEST_NETWORK_FORKS=phase0 $(RECENT_FORKS_BEFORE_GLOAS) @@ -181,7 +181,12 @@ build-release-tarballs: test-release: cargo nextest run --workspace --release --features "$(TEST_FEATURES)" \ --exclude ef_tests --exclude beacon_chain --exclude slasher --exclude network \ - --exclude http_api + --exclude http_api --exclude proof_engine_test + +# Runs the proof engine integration tests sequentially. Each test spawns multiple +# beacon nodes and is sensitive to slot timing, so dedicated execution is required. +test-proof-engine: + cargo nextest run -p proof_engine_test --release --test-threads 1 # Runs the full workspace tests in **debug**, without downloading any additional test @@ -190,6 +195,10 @@ test-debug: cargo nextest run --workspace --features "$(TEST_FEATURES)" \ --exclude ef_tests --exclude beacon_chain --exclude network --exclude http_api +# Runs the proof_engine_zkboost integration tests against a real mock-backend zkBoost server. +test-zkboost: + cargo nextest run --manifest-path testing/proof_engine_zkboost/Cargo.toml --release + # Runs cargo-fmt (linter). cargo-fmt: cargo fmt --all -- --check diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index b3d258a2fb0..fdae204d782 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -5,7 +5,6 @@ use crate::attestation_verification::{ }; use crate::beacon_block_streamer::{BeaconBlockStreamer, CheckCaches}; use crate::beacon_proposer_cache::{BeaconProposerCache, EpochBlockProposers}; -use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use crate::block_times_cache::BlockTimesCache; use crate::block_verification::{ BlockError, ExecutionPendingBlock, GossipVerifiedBlock, IntoExecutionPendingBlock, @@ -26,16 +25,16 @@ use crate::data_availability_checker::{ use crate::data_availability_checker::DataAvailabilityChecker; use crate::data_column_verification::{ GossipDataColumnError, GossipPartialDataColumnError, GossipVerifiedDataColumn, - GossipVerifiedPartialDataColumnHeader, KzgVerifiedCustodyPartialDataColumn, - KzgVerifiedPartialDataColumn, PartialColumnVerificationResult, - validate_partial_data_column_sidecar_for_gossip, + GossipVerifiedPartialDataColumnHeader, KzgVerifiedCustodyDataColumn, + KzgVerifiedCustodyPartialDataColumn, KzgVerifiedPartialDataColumn, + PartialColumnVerificationResult, validate_partial_data_column_sidecar_for_gossip, }; use crate::early_attester_cache::EarlyAttesterCache; +use crate::eip8025::ExecutionProofStatusCache; use crate::envelope_times_cache::EnvelopeTimesCache; use crate::errors::{BeaconChainError as Error, BlockProductionError}; use crate::events::ServerSentEventHandler; use crate::execution_payload::{NotifyExecutionLayer, PreparePayloadHandle, get_execution_payload}; -use crate::fetch_blobs::EngineGetBlobsOutput; use crate::fork_choice_signal::{ForkChoiceSignalRx, ForkChoiceSignalTx}; use crate::graffiti_calculator::{GraffitiCalculator, GraffitiSettings}; use crate::light_client_finality_update_verification::{ @@ -59,6 +58,7 @@ use crate::observed_attesters::{ }; use crate::observed_block_producers::ObservedBlockProducers; use crate::observed_data_sidecars::ObservedDataSidecars; +use crate::observed_execution_proofs::ObservedExecutionProofs; use crate::observed_operations::{ObservationOutcome, ObservedOperations}; use crate::observed_slashable::ObservedSlashable; use crate::partial_data_column_assembler::PartialMergeResult; @@ -431,11 +431,17 @@ pub struct BeaconChain { pub(crate) observed_payload_attesters: RwLock>, /// Maintains a record of which validators have proposed blocks for each slot. pub observed_block_producers: RwLock>, - /// Maintains a record of blob sidecars seen over the gossip network. - pub observed_blob_sidecars: RwLock, T::EthSpec>>, /// Maintains a record of column sidecars seen over the gossip network. pub observed_column_sidecars: RwLock, T::EthSpec>>, + /// Maintains proof-gossip deduplication state without storing proof bytes. + pub observed_execution_proofs: RwLock, + /// Maintains EIP-8025 proof-status metadata and bounded request-root mappings. + pub execution_proof_statuses: RwLock, + /// Lazily initialized event bus used by execution-proof integration tests. + pub internal_event_tx: std::sync::OnceLock< + tokio::sync::broadcast::Sender, + >, /// Maintains a record of slashable message seen over the gossip network or RPC. pub observed_slashable: RwLock>, /// Cache of pending execution payload envelopes for local block building. @@ -2201,8 +2207,11 @@ impl BeaconChain { slot_start.is_some_and(|start| observed.saturating_sub(start) < payload_due) }); - // TODO(EIP-7732): Check blob data availability. For now, default to true. - let blob_data_available = true; + // A payload is only imported into fork choice if its data was available. + let blob_data_available = self + .canonical_head + .fork_choice_read_lock() + .is_payload_received(&beacon_block_root); Ok(PayloadAttestationData { beacon_block_root, @@ -2453,19 +2462,6 @@ impl BeaconChain { ret } - #[instrument(skip_all, level = "trace")] - pub fn verify_blob_sidecar_for_gossip( - self: &Arc, - blob_sidecar: Arc>, - subnet_id: u64, - ) -> Result, GossipBlobError> { - metrics::inc_counter(&metrics::BLOBS_SIDECAR_PROCESSING_REQUESTS); - let _timer = metrics::start_timer(&metrics::BLOBS_SIDECAR_GOSSIP_VERIFICATION_TIMES); - GossipVerifiedBlob::new(blob_sidecar, subnet_id, self).inspect(|_| { - metrics::inc_counter(&metrics::BLOBS_SIDECAR_PROCESSING_SUCCESSES); - }) - } - /// Accepts some 'LightClientOptimisticUpdate' from the network and attempts to verify it pub fn verify_optimistic_update_for_gossip( self: &Arc, @@ -3253,35 +3249,6 @@ impl BeaconChain { .map_err(BeaconChainError::TokioJoin)? } - /// Cache the blob in the processing cache, process it, then evict it from the cache if it was - /// imported or errors. - #[instrument(skip_all, level = "debug")] - pub async fn process_gossip_blob( - self: &Arc, - blob: GossipVerifiedBlob, - ) -> Result { - let block_root = blob.block_root(); - - // If this block has already been imported to forkchoice it must have been available, so - // we don't need to process its blobs again. - if self - .canonical_head - .fork_choice_read_lock() - .contains_block(&block_root) - { - return Err(BlockError::DuplicateFullyImported(blob.block_root())); - } - - // No need to process and import blobs beyond the PeerDAS epoch. - if self.spec.is_peer_das_enabled_for_epoch(blob.epoch()) { - return Err(BlockError::BlobNotRequired(blob.slot())); - } - - self.emit_sse_blob_sidecar_events(&block_root, std::iter::once(blob.as_blob())); - - self.check_gossip_blob_availability_and_import(blob).await - } - /// Cache the data columns in the processing cache, process it, then evict it from the cache if it was /// imported or errors. /// Only accepts full columns. Partials are handled via PartialDataColumnAssembler. @@ -3428,19 +3395,21 @@ impl BeaconChain { return Err(BlockError::DuplicateFullyImported(block_root)); } - // Reject RPC blobs referencing unknown parents. Otherwise we allow potentially invalid data - // into the da_checker, where invalid = descendant of invalid blocks. - // Note: blobs should have at least one item and all items have the same parent root. - if let Some(parent_root) = blobs - .iter() - .filter_map(|b| b.as_ref().map(|b| b.block_parent_root())) - .next() - && !self - .canonical_head - .fork_choice_read_lock() - .contains_block(&parent_root) - { - return Err(BlockError::ParentUnknown { parent_root }); + for blob in &blobs { + if let Some(blob) = blob.as_ref() { + // Reject RPC blobs referencing unknown parents. Otherwise we allow potentially invalid data + // into the da_checker, where invalid = descendant of invalid blocks. + // Note: blobs should have at least one item and all items have the same parent root. + if !self + .canonical_head + .fork_choice_read_lock() + .contains_block(&blob.block_parent_root()) + { + return Err(BlockError::ParentUnknown { + parent_root: blob.block_parent_root(), + }); + } + } } self.emit_sse_blob_sidecar_events(&block_root, blobs.iter().flatten().map(Arc::as_ref)); @@ -3454,7 +3423,7 @@ impl BeaconChain { self: &Arc, slot: Slot, block_root: Hash256, - engine_get_blobs_output: EngineGetBlobsOutput, + engine_get_blobs_output: Vec>, ) -> Result { // If this block has already been imported to forkchoice it must have been available, so // we don't need to process its blobs again. @@ -3466,17 +3435,12 @@ impl BeaconChain { return Err(BlockError::DuplicateFullyImported(block_root)); } - match &engine_get_blobs_output { - EngineGetBlobsOutput::Blobs(blobs) => { - self.emit_sse_blob_sidecar_events(&block_root, blobs.iter().map(|b| b.as_blob())); - } - EngineGetBlobsOutput::CustodyColumns(columns) => { - self.emit_sse_data_column_sidecar_events( - &block_root, - columns.iter().map(|column| column.as_data_column()), - ); - } - } + self.emit_sse_data_column_sidecar_events( + &block_root, + engine_get_blobs_output + .iter() + .map(|column| column.as_data_column()), + ); self.check_engine_blobs_availability_and_import(slot, block_root, engine_get_blobs_output) .await @@ -3915,24 +3879,6 @@ impl BeaconChain { .await } - /// Checks if the provided blob can make any cached blocks available, and imports immediately - /// if so, otherwise caches the blob in the data availability checker. - async fn check_gossip_blob_availability_and_import( - self: &Arc, - blob: GossipVerifiedBlob, - ) -> Result { - let slot = blob.slot(); - if let Some(slasher) = self.slasher.as_ref() { - slasher.accept_block_header(blob.signed_block_header()); - } - let availability = self - .data_availability_checker - .put_gossip_verified_blobs(blob.block_root(), std::iter::once(blob))?; - - self.process_availability(slot, availability, || Ok(())) - .await - } - /// Checks if the provided data column can make any cached blocks available, and imports immediately /// if so, otherwise caches the data column in the data availability checker. /// Check gossip data columns for availability and import. Only accepts full columns. @@ -4015,7 +3961,7 @@ impl BeaconChain { ) -> Result { self.check_blob_header_signature_and_slashability( block_root, - blobs.iter().flatten().map(Arc::as_ref), + blobs.iter().flatten().map(|b| b.as_ref()), )?; let availability = self .data_availability_checker @@ -4030,56 +3976,36 @@ impl BeaconChain { self: &Arc, slot: Slot, block_root: Hash256, - engine_get_blobs_output: EngineGetBlobsOutput, + engine_get_blobs_output: Vec>, ) -> Result { - match engine_get_blobs_output { - EngineGetBlobsOutput::Blobs(blobs) => { - self.check_blob_header_signature_and_slashability( - block_root, - blobs.iter().map(|b| b.as_blob()), - )?; - let availability = self - .data_availability_checker - .put_kzg_verified_blobs(block_root, blobs) - .map_err(BlockError::from)?; - - Ok(self - .process_availability(slot, availability, || Ok(())) - .await?) - } - EngineGetBlobsOutput::CustodyColumns(data_columns) => { - // TODO(gloas) verify that this check is no longer relevant for gloas - self.check_data_column_sidecar_header_signature_and_slashability( - block_root, - data_columns - .iter() - .filter_map(|c| match c.as_data_column() { - DataColumnSidecar::Fulu(column) => Some(column), - _ => None, - }), - )?; - if self - .spec - .fork_name_at_slot::(slot) - .gloas_enabled() - { - let availability = self - .pending_payload_cache - .put_kzg_verified_custody_data_columns(block_root, &data_columns) - .map_err(BlockError::from)?; - Ok(self - .process_payload_envelope_availability(slot, availability, || Ok(())) - .await?) - } else { - let availability = self - .data_availability_checker - .put_kzg_verified_custody_data_columns(block_root, data_columns) - .map_err(BlockError::from)?; - Ok(self - .process_availability(slot, availability, || Ok(())) - .await?) - } - } + // TODO(gloas) verify that this check is no longer relevant for gloas + self.check_data_column_sidecar_header_signature_and_slashability( + block_root, + engine_get_blobs_output + .iter() + .filter_map(|c| match c.as_data_column() { + DataColumnSidecar::Fulu(column) => Some(column), + _ => None, + }), + )?; + if self + .spec + .fork_name_at_slot::(slot) + .gloas_enabled() + { + let availability = self + .pending_payload_cache + .put_kzg_verified_custody_data_columns(block_root, &engine_get_blobs_output) + .map_err(BlockError::from)?; + self.process_payload_envelope_availability(slot, availability, || Ok(())) + .await + } else { + let availability = self + .data_availability_checker + .put_kzg_verified_custody_data_columns(block_root, engine_get_blobs_output) + .map_err(BlockError::from)?; + self.process_availability(slot, availability, || Ok(())) + .await } } @@ -4511,6 +4437,16 @@ impl BeaconChain { // This prevents inconsistency between the two at the expense of concurrency. drop(fork_choice); + if let Ok(new_payload_request) = + TryInto::>::try_into(block) + { + self.execution_proof_statuses.write().register_request_root( + block_root, + new_payload_request.request_root(), + block.slot(), + ); + } + // We're declaring the block "imported" at this point, since fork choice and the DB know // about it. let block_time_imported = self.slot_clock.now_duration().unwrap_or(Duration::MAX); @@ -7525,6 +7461,32 @@ impl BeaconChain { self.dump_as_dot(&mut file); } + pub fn subscribe_internal_events( + &self, + ) -> tokio::sync::broadcast::Receiver { + self.internal_event_tx + .get_or_init(|| { + let (tx, _rx) = tokio::sync::broadcast::channel( + crate::internal_events::INTERNAL_EVENT_CHANNEL_CAPACITY, + ); + tx + }) + .subscribe() + } + + pub fn internal_event_sender( + &self, + ) -> Option<&tokio::sync::broadcast::Sender> + { + self.internal_event_tx.get() + } + + pub fn emit_internal_event(&self, event: crate::internal_events::InternalBeaconNodeEvent) { + if let Some(tx) = self.internal_event_tx.get() { + let _ = tx.send(event); + } + } + /// Checks if attestations have been seen from the given `validator_index` at the /// given `epoch`. pub fn validator_seen_at_epoch(&self, validator_index: usize, epoch: Epoch) -> bool { diff --git a/beacon_node/beacon_chain/src/blob_verification.rs b/beacon_node/beacon_chain/src/blob_verification.rs index e557a243694..79b29696458 100644 --- a/beacon_node/beacon_chain/src/blob_verification.rs +++ b/beacon_node/beacon_chain/src/blob_verification.rs @@ -1,249 +1,11 @@ -use educe::Educe; -use slot_clock::SlotClock; -use std::marker::PhantomData; -use std::sync::Arc; - -use crate::beacon_chain::{BeaconChain, BeaconChainTypes}; -use crate::block_verification::{ - BlockSlashInfo, get_validator_pubkey_cache, process_block_slash_info, -}; use crate::kzg_utils::{validate_blob, validate_blobs}; -use crate::observed_data_sidecars::{ - Error as ObservedDataSidecarsError, ObservationStrategy, Observe, -}; -use crate::{BeaconChainError, metrics}; +use educe::Educe; use kzg::{Error as KzgError, Kzg, KzgCommitment}; use ssz_derive::{Decode, Encode}; +use std::sync::Arc; use std::time::Duration; -use tracing::{debug, instrument}; -use tree_hash::TreeHash; -use types::data::BlobIdentifier; -use types::{ - BeaconStateError, BlobSidecar, Epoch, EthSpec, Hash256, SignedBeaconBlockHeader, Slot, -}; - -/// An error occurred while validating a gossip blob. -#[derive(Debug)] -pub enum GossipBlobError { - /// The blob sidecar is from a slot that is later than the current slot (with respect to the - /// gossip clock disparity). - /// - /// ## Peer scoring - /// - /// Assuming the local clock is correct, the peer has sent an invalid message. - FutureSlot { - message_slot: Slot, - latest_permissible_slot: Slot, - }, - - /// There was an error whilst processing the blob. It is not known if it is - /// valid or invalid. - /// - /// ## Peer scoring - /// - /// We were unable to process this blob due to an internal error. It's - /// unclear if the blob is valid. - BeaconChainError(Box), - - /// The `BlobSidecar` was gossiped over an incorrect subnet. - /// - /// ## Peer scoring - /// - /// The blob is invalid or the peer is faulty. - InvalidSubnet { expected: u64, received: u64 }, - - /// The sidecar corresponds to a slot older than the finalized head slot. - /// - /// ## Peer scoring - /// - /// It's unclear if this blob is valid, but this blob is for a finalized slot and is - /// therefore useless to us. - PastFinalizedSlot { - blob_slot: Slot, - finalized_slot: Slot, - }, - - /// The proposer index specified in the sidecar does not match the locally computed - /// proposer index. - /// - /// ## Peer scoring - /// - /// The blob is invalid and the peer is faulty. - ProposerIndexMismatch { sidecar: usize, local: usize }, - - /// The proposal signature in invalid. - /// - /// ## Peer scoring - /// - /// The blob is invalid and the peer is faulty. - ProposalSignatureInvalid, - - /// The proposal_index corresponding to blob.beacon_block_root is not known. - /// - /// ## Peer scoring - /// - /// The blob is invalid and the peer is faulty. - UnknownValidator(u64), - - /// The provided blob is not from a later slot than its parent. - /// - /// ## Peer scoring - /// - /// The blob is invalid and the peer is faulty. - BlobIsNotLaterThanParent { blob_slot: Slot, parent_slot: Slot }, - - /// The provided blob's parent block is unknown. - /// - /// ## Peer scoring - /// - /// We cannot process the blob without validating its parent, the peer isn't necessarily faulty. - ParentUnknown { parent_root: Hash256 }, - - /// Invalid kzg commitment inclusion proof - /// ## Peer scoring - /// - /// The blob sidecar is invalid and the peer is faulty - InvalidInclusionProof, - - /// A blob has already been seen for the given `(sidecar.block_root, sidecar.index)` tuple - /// over gossip or no gossip sources. - /// - /// ## Peer scoring - /// - /// The peer isn't faulty, but we do not forward it over gossip. - RepeatBlob { - proposer: u64, - slot: Slot, - index: u64, - }, - - /// The kzg verification failed. - /// - /// ## Peer scoring - /// - /// The blob sidecar is invalid and the peer is faulty. - KzgError(kzg::Error), - - /// The pubkey cache timed out. - /// - /// ## Peer scoring - /// - /// The blob sidecar may be valid, this is an internal error. - PubkeyCacheTimeout, - - /// The block conflicts with finalization, no need to propagate. - /// - /// ## Peer scoring - /// - /// It's unclear if this block is valid, but it conflicts with finality and shouldn't be - /// imported. - NotFinalizedDescendant { block_parent_root: Hash256 }, -} - -impl std::fmt::Display for GossipBlobError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) - } -} - -impl From for GossipBlobError { - fn from(e: BeaconChainError) -> Self { - GossipBlobError::BeaconChainError(e.into()) - } -} - -impl From for GossipBlobError { - fn from(e: BeaconStateError) -> Self { - GossipBlobError::BeaconChainError(BeaconChainError::BeaconStateError(e).into()) - } -} - -/// A wrapper around a `BlobSidecar` that indicates it has been approved for re-gossiping on -/// the p2p network. -#[derive(Debug)] -pub struct GossipVerifiedBlob { - block_root: Hash256, - blob: KzgVerifiedBlob, - _phantom: PhantomData, -} - -impl Clone for GossipVerifiedBlob { - fn clone(&self) -> Self { - Self { - block_root: self.block_root, - blob: self.blob.clone(), - _phantom: PhantomData, - } - } -} - -impl GossipVerifiedBlob { - pub fn new( - blob: Arc>, - subnet_id: u64, - chain: &BeaconChain, - ) -> Result { - let header = blob.signed_block_header.clone(); - // We only process slashing info if the gossip verification failed - // since we do not process the blob any further in that case. - validate_blob_sidecar_for_gossip::(blob, subnet_id, chain).map_err(|e| { - process_block_slash_info::<_, GossipBlobError>( - chain, - BlockSlashInfo::from_early_error_blob(header, e), - ) - }) - } - /// Construct a `GossipVerifiedBlob` that is assumed to be valid. - /// - /// This should ONLY be used for testing. - pub fn __assumed_valid(blob: Arc>) -> Self { - Self { - block_root: blob.block_root(), - blob: KzgVerifiedBlob { - blob, - seen_timestamp: Duration::from_secs(0), - }, - _phantom: PhantomData, - } - } - pub fn id(&self) -> BlobIdentifier { - BlobIdentifier { - block_root: self.block_root, - index: self.blob.blob_index(), - } - } - pub fn block_root(&self) -> Hash256 { - self.block_root - } - pub fn slot(&self) -> Slot { - self.blob.blob.slot() - } - pub fn epoch(&self) -> Epoch { - self.blob.blob.epoch() - } - pub fn index(&self) -> u64 { - self.blob.blob.index - } - pub fn kzg_commitment(&self) -> KzgCommitment { - self.blob.blob.kzg_commitment - } - pub fn signed_block_header(&self) -> SignedBeaconBlockHeader { - self.blob.blob.signed_block_header.clone() - } - pub fn block_proposer_index(&self) -> u64 { - self.blob.blob.block_proposer_index() - } - pub fn into_inner(self) -> KzgVerifiedBlob { - self.blob - } - pub fn as_blob(&self) -> &BlobSidecar { - self.blob.as_blob() - } - /// This is cheap as we're calling clone on an Arc - pub fn clone_blob(&self) -> Arc> { - self.blob.clone_blob() - } -} +use tracing::instrument; +use types::{BlobSidecar, EthSpec}; /// Wrapper over a `BlobSidecar` for which we have completed kzg verification. /// i.e. `verify_blob_kzg_proof(blob, commitment, proof) == true`. @@ -388,239 +150,3 @@ where .unzip(); validate_blobs::(kzg, commitments.as_slice(), blobs, proofs.as_slice()) } - -pub fn validate_blob_sidecar_for_gossip( - blob_sidecar: Arc>, - subnet: u64, - chain: &BeaconChain, -) -> Result, GossipBlobError> { - let blob_slot = blob_sidecar.slot(); - let blob_index = blob_sidecar.index; - let block_parent_root = blob_sidecar.block_parent_root(); - let blob_proposer_index = blob_sidecar.block_proposer_index(); - let block_root = blob_sidecar.block_root(); - let blob_epoch = blob_slot.epoch(T::EthSpec::slots_per_epoch()); - let signed_block_header = &blob_sidecar.signed_block_header; - - let seen_timestamp = chain.slot_clock.now_duration().unwrap_or_default(); - - // This condition is not possible if we have received the blob from the network - // since we only subscribe to `MaxBlobsPerBlock` subnets over gossip network. - // We include this check only for completeness. - // Getting this error would imply something very wrong with our networking decoding logic. - if blob_index >= chain.spec.max_blobs_per_block(blob_epoch) { - return Err(GossipBlobError::InvalidSubnet { - expected: subnet, - received: blob_index, - }); - } - - // Verify that the blob_sidecar was received on the correct subnet. - if blob_index != subnet { - return Err(GossipBlobError::InvalidSubnet { - expected: subnet, - received: blob_index, - }); - } - - // Verify that the sidecar is not from a future slot. - let latest_permissible_slot = chain - .slot_clock - .now_with_future_tolerance(chain.spec.maximum_gossip_clock_disparity()) - .ok_or(BeaconChainError::UnableToReadSlot)?; - if blob_slot > latest_permissible_slot { - return Err(GossipBlobError::FutureSlot { - message_slot: blob_slot, - latest_permissible_slot, - }); - } - - // Verify that the sidecar slot is greater than the latest finalized slot - let latest_finalized_slot = chain - .head() - .finalized_checkpoint() - .epoch - .start_slot(T::EthSpec::slots_per_epoch()); - if blob_slot <= latest_finalized_slot { - return Err(GossipBlobError::PastFinalizedSlot { - blob_slot, - finalized_slot: latest_finalized_slot, - }); - } - - // Verify that this is the first blob sidecar received for the tuple: - // (block_header.slot, block_header.proposer_index, blob_sidecar.index) - if chain - .observed_blob_sidecars - .read() - .observation_key_is_known(&blob_sidecar) - .map_err(|e| GossipBlobError::BeaconChainError(Box::new(e.into())))? - .is_some() - { - return Err(GossipBlobError::RepeatBlob { - proposer: blob_proposer_index, - slot: blob_slot, - index: blob_index, - }); - } - - // Verify the inclusion proof in the sidecar - let _timer = metrics::start_timer(&metrics::BLOB_SIDECAR_INCLUSION_PROOF_VERIFICATION); - if !blob_sidecar.verify_blob_sidecar_inclusion_proof() { - return Err(GossipBlobError::InvalidInclusionProof); - } - drop(_timer); - - let fork_choice = chain.canonical_head.fork_choice_read_lock(); - - // We have already verified that the blob is past finalization, so we can - // just check fork choice for the block's parent. - let Some(parent_block) = fork_choice.get_block(&block_parent_root) else { - return Err(GossipBlobError::ParentUnknown { - parent_root: block_parent_root, - }); - }; - - // Do not process a blob that does not descend from the finalized root. - // We just loaded the parent_block, so we can be sure that it exists in fork choice. - if !fork_choice.is_finalized_checkpoint_or_descendant(block_parent_root) { - return Err(GossipBlobError::NotFinalizedDescendant { block_parent_root }); - } - drop(fork_choice); - - if parent_block.slot >= blob_slot { - return Err(GossipBlobError::BlobIsNotLaterThanParent { - blob_slot, - parent_slot: parent_block.slot, - }); - } - - let proposer_shuffling_root = - parent_block.proposer_shuffling_root_for_child_block(blob_epoch, &chain.spec); - - let proposer = chain.with_proposer_cache( - proposer_shuffling_root, - blob_epoch, - |proposers| proposers.get_slot::(blob_slot), - || { - debug!( - %block_root, - index = %blob_index, - "Proposer shuffling cache miss for blob verification" - ); - // Blob verification is only relevant pre-Fulu and pre-Gloas, so `Pending` payload - // status is sufficient. - chain - .store - .get_advanced_hot_state(block_parent_root, blob_slot, parent_block.state_root) - .map_err(|e| GossipBlobError::BeaconChainError(Box::new(e.into())))? - .ok_or_else(|| { - GossipBlobError::BeaconChainError(Box::new(BeaconChainError::DBInconsistent( - format!("Missing state for parent block {block_parent_root:?}",), - ))) - }) - }, - )?; - let proposer_index = proposer.index; - let fork = proposer.fork; - - // Signature verify the signed block header. - let signature_is_valid = { - let pubkey_cache = - get_validator_pubkey_cache(chain).map_err(|_| GossipBlobError::PubkeyCacheTimeout)?; - - let pubkey = pubkey_cache - .get(proposer_index) - .ok_or_else(|| GossipBlobError::UnknownValidator(proposer_index as u64))?; - signed_block_header.verify_signature::( - pubkey, - &fork, - chain.genesis_validators_root, - &chain.spec, - ) - }; - - if !signature_is_valid { - return Err(GossipBlobError::ProposalSignatureInvalid); - } - - if proposer_index != blob_proposer_index as usize { - return Err(GossipBlobError::ProposerIndexMismatch { - sidecar: blob_proposer_index as usize, - local: proposer_index, - }); - } - - // Kzg verification for gossip blob sidecar - let kzg = chain.kzg.as_ref(); - - let kzg_verified_blob = KzgVerifiedBlob::new(blob_sidecar.clone(), kzg, seen_timestamp) - .map_err(GossipBlobError::KzgError)?; - let blob_sidecar = &kzg_verified_blob.blob; - - chain - .observed_slashable - .write() - .observe_slashable( - blob_sidecar.slot(), - blob_sidecar.block_proposer_index(), - block_root, - ) - .map_err(|e| GossipBlobError::BeaconChainError(Box::new(e.into())))?; - - if O::observe() { - observe_gossip_blob(&kzg_verified_blob.blob, chain)?; - } - - Ok(GossipVerifiedBlob { - block_root, - blob: kzg_verified_blob, - _phantom: PhantomData, - }) -} - -pub fn observe_gossip_blob( - blob_sidecar: &BlobSidecar, - chain: &BeaconChain, -) -> Result<(), GossipBlobError> { - // Now the signature is valid, store the proposal so we don't accept another blob sidecar - // with the same `BlobIdentifier`. It's important to double-check that the proposer still - // hasn't been observed so we don't have a race-condition when verifying two blocks - // simultaneously. - // - // Note: If this BlobSidecar goes on to fail full verification, we do not evict it from the - // seen_cache as alternate blob_sidecars for the same identifier can still be retrieved over - // rpc. Evicting them from this cache would allow faster propagation over gossip. So we - // allow retrieval of potentially valid blocks over rpc, but try to punish the proposer for - // signing invalid messages. Issue for more background - // https://github.com/ethereum/consensus-specs/issues/3261 - if chain - .observed_blob_sidecars - .write() - .observe_sidecar(blob_sidecar) - .map_err(|e: ObservedDataSidecarsError| { - GossipBlobError::BeaconChainError(Box::new(e.into())) - })? - .is_some() - { - return Err(GossipBlobError::RepeatBlob { - proposer: blob_sidecar.block_proposer_index(), - slot: blob_sidecar.slot(), - index: blob_sidecar.index, - }); - } - Ok(()) -} - -/// Returns the canonical root of the given `blob`. -/// -/// Use this function to ensure that we report the blob hashing time Prometheus metric. -pub fn get_blob_root(blob: &BlobSidecar) -> Hash256 { - let blob_root_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_BLOB_ROOT); - - let blob_root = blob.tree_hash_root(); - - metrics::stop_timer(blob_root_timer); - - blob_root -} diff --git a/beacon_node/beacon_chain/src/block_production/mod.rs b/beacon_node/beacon_chain/src/block_production/mod.rs index a94bc697b94..17fa34ce02d 100644 --- a/beacon_node/beacon_chain/src/block_production/mod.rs +++ b/beacon_node/beacon_chain/src/block_production/mod.rs @@ -179,14 +179,6 @@ impl BeaconChain { let re_org_max_epochs_since_finalization = Epoch::new(self.spec.reorg_max_epochs_since_finalization); - if self.spec.proposer_score_boost.is_none() { - warn!( - reason = "this network does not have proposer boost enabled", - "Ignoring proposer re-org configuration" - ); - return None; - } - let slot_delay = self .slot_clock .seconds_from_current_slot_start() diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 24f971f736f..de592e8dae1 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -49,7 +49,6 @@ #![allow(clippy::result_large_err)] use crate::beacon_snapshot::PreProcessingSnapshot; -use crate::blob_verification::GossipBlobError; use crate::block_verification_types::{AsBlock, BlockImportData, LookupBlock, RangeSyncBlock}; use crate::data_availability_checker::{ AvailabilityCheckError, AvailableBlock, AvailableBlockData, MaybeAvailableBlock, @@ -71,7 +70,7 @@ use bls::{PublicKey, PublicKeyBytes}; use educe::Educe; use eth2::types::{BlockGossip, EventKind}; use execution_layer::PayloadStatus; -pub use fork_choice::{AttestationFromBlock, PayloadVerificationStatus}; +pub use fork_choice::{AttestationFromBlock, ParentImportStatus, PayloadVerificationStatus}; use metrics::TryExt; use parking_lot::RwLockReadGuard; use proto_array::Block as ProtoBlock; @@ -93,7 +92,7 @@ use std::fs; use std::io::Write; use std::sync::Arc; use store::{Error as DBError, KeyValueStore}; -use strum::AsRefStr; +use strum::{AsRefStr, IntoStaticStr}; use task_executor::JoinHandle; use tracing::{Instrument, Span, debug, debug_span, error, info_span, instrument}; use types::{ @@ -115,7 +114,7 @@ const WRITE_BLOCK_PROCESSING_SSZ: bool = cfg!(feature = "write_ssz_files"); /// /// - The block is malformed/invalid (indicated by all results other than `BeaconChainError`. /// - We encountered an error whilst trying to verify the block (a `BeaconChainError`). -#[derive(Debug, AsRefStr)] +#[derive(Debug, AsRefStr, IntoStaticStr)] pub enum BlockError { /// The parent block was unknown. /// @@ -290,14 +289,6 @@ pub enum BlockError { EnvelopeBlockRootUnknown(Hash256), /// Optimistic sync is not supported for Gloas payload envelopes. OptimisticSyncNotSupported { block_root: Hash256 }, - /// A Blob with a slot after PeerDAS is received and is not required to be imported. - /// This can happen because we stay subscribed to the blob subnet after 2 epochs, as we could - /// still receive valid blobs from a Deneb epoch after PeerDAS is activated. - /// - /// ## Peer scoring - /// - /// This indicates the peer is sending an unexpected gossip blob and should be penalised. - BlobNotRequired(Slot), /// An internal error has occurred when processing the block or sidecars. /// /// ## Peer scoring @@ -345,7 +336,7 @@ impl From for BlockError { /// Returned when block validation failed due to some issue verifying /// the execution payload. -#[derive(Debug)] +#[derive(Debug, IntoStaticStr)] pub enum ExecutionPayloadError { /// There's no eth1 connection (mandatory after merge) /// @@ -520,17 +511,6 @@ impl BlockSlashInfo { } } -impl BlockSlashInfo { - pub fn from_early_error_blob(header: SignedBeaconBlockHeader, e: GossipBlobError) -> Self { - match e { - GossipBlobError::ProposalSignatureInvalid => BlockSlashInfo::SignatureInvalid(e), - // `InvalidSignature` could indicate any signature in the block, so we want - // to recheck the proposer signature alone. - _ => BlockSlashInfo::SignatureNotChecked(header, e), - } - } -} - impl BlockSlashInfo { pub fn from_early_error_data_column( header: SignedBeaconBlockHeader, @@ -890,7 +870,7 @@ impl GossipVerifiedBlock { let block_epoch = block.slot().epoch(T::EthSpec::slots_per_epoch()); let (parent_block, block) = - verify_parent_block_is_known::(&fork_choice_read_lock, block)?; + verify_parent_block_and_envelope_are_known::(&fork_choice_read_lock, block)?; // [New in Gloas]: Verify bid.parent_block_root matches block.parent_root. if let Ok(bid) = block.message().body().signed_execution_payload_bid() @@ -902,13 +882,6 @@ impl GossipVerifiedBlock { }); } - // TODO(gloas) The following validation can only be completed once fork choice has been implemented: - // The block's parent execution payload (defined by bid.parent_block_hash) has been seen - // (via gossip or non-gossip sources) (a client MAY queue blocks for processing - // once the parent payload is retrieved). If execution_payload verification of block's execution - // payload parent by an execution node is complete, verify the block's execution payload - // parent (defined by bid.parent_block_hash) passes all validation. - drop(fork_choice_read_lock); // Track the number of skip slots between the block and its parent. @@ -1401,32 +1374,23 @@ impl ExecutionPendingBlock { .observe_proposal(block_root, block.message()) .map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))?; - if let Some(parent) = chain + match chain .canonical_head .fork_choice_read_lock() - .get_block(&block.parent_root()) + .get_parent_import_status(block.as_block()) { - // Reject any block where the parent has an invalid payload. It's impossible for a valid - // block to descend from an invalid parent. - if parent.execution_status.is_invalid() { - return Err(BlockError::ParentExecutionPayloadInvalid { + ParentImportStatus::Imported(parent) => { + if parent.execution_status.is_invalid() { + return Err(BlockError::ParentExecutionPayloadInvalid { + parent_root: block.parent_root(), + }); + } + } + ParentImportStatus::UnknownBlock | ParentImportStatus::UnknownPayload => { + return Err(BlockError::ParentUnknown { parent_root: block.parent_root(), }); } - } else { - // Reject any block if its parent is not known to fork choice. - // - // A block that is not in fork choice is either: - // - // - Not yet imported: we should reject this block because we should only import a child - // after its parent has been fully imported. - // - Pre-finalized: if the parent block is _prior_ to finalization, we should ignore it - // because it will revert finalization. Note that the finalized block is stored in fork - // choice, so we will not reject any child of the finalized block (this is relevant during - // genesis). - return Err(BlockError::ParentUnknown { - parent_root: block.parent_root(), - }); } /* @@ -1882,19 +1846,20 @@ pub fn get_block_header_root(block_header: &SignedBeaconBlockHeader) -> Hash256 block_root } -/// Verify the parent of `block` is known, returning some information about the parent block from -/// fork choice. +/// Verify the parent block — and, for a post-Gloas FULL child, the parent payload — are known to +/// fork choice; both missing cases return `ParentUnknown`. #[allow(clippy::type_complexity)] -fn verify_parent_block_is_known( +fn verify_parent_block_and_envelope_are_known( fork_choice_read_lock: &RwLockReadGuard>, block: Arc>, ) -> Result<(ProtoBlock, Arc>), BlockError> { - if let Some(proto_block) = fork_choice_read_lock.get_block(&block.parent_root()) { - Ok((proto_block, block)) - } else { - Err(BlockError::ParentUnknown { - parent_root: block.parent_root(), - }) + match fork_choice_read_lock.get_parent_import_status(&block) { + ParentImportStatus::Imported(parent) => Ok((parent, block)), + ParentImportStatus::UnknownBlock | ParentImportStatus::UnknownPayload => { + Err(BlockError::ParentUnknown { + parent_root: block.parent_root(), + }) + } } } @@ -1921,7 +1886,7 @@ fn load_parent>( if !chain .canonical_head .fork_choice_read_lock() - .contains_block(&block.parent_root()) + .is_parent_imported(block.as_block()) { return Err(BlockError::ParentUnknown { parent_root: block.parent_root(), @@ -2038,23 +2003,6 @@ impl BlockBlobError for BlockError { } } -impl BlockBlobError for GossipBlobError { - fn not_later_than_parent_error(blob_slot: Slot, parent_slot: Slot) -> Self { - GossipBlobError::BlobIsNotLaterThanParent { - blob_slot, - parent_slot, - } - } - - fn unknown_validator_error(validator_index: u64) -> Self { - GossipBlobError::UnknownValidator(validator_index) - } - - fn proposer_signature_invalid() -> Self { - GossipBlobError::ProposalSignatureInvalid - } -} - impl BlockBlobError for GossipDataColumnError { fn not_later_than_parent_error(data_column_slot: Slot, parent_slot: Slot) -> Self { GossipDataColumnError::IsNotLaterThanParent { diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index b8da2bcdedc..f4aa598a6cb 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -21,6 +21,9 @@ use crate::validator_pubkey_cache::ValidatorPubkeyCache; use crate::{ BeaconChain, BeaconChainTypes, BeaconForkChoiceStore, BeaconSnapshot, ServerSentEventHandler, }; +use crate::{ + eip8025::ExecutionProofStatusCache, observed_execution_proofs::ObservedExecutionProofs, +}; use bls::Signature; use execution_layer::ExecutionLayer; use fixed_bytes::FixedBytesExtended; @@ -1007,7 +1010,9 @@ where // TODO: allow for persisting and loading the pool from disk. observed_block_producers: <_>::default(), observed_column_sidecars: RwLock::new(ObservedDataSidecars::new(self.spec.clone())), - observed_blob_sidecars: RwLock::new(ObservedDataSidecars::new(self.spec.clone())), + observed_execution_proofs: RwLock::new(ObservedExecutionProofs::default()), + execution_proof_statuses: RwLock::new(ExecutionProofStatusCache::default()), + internal_event_tx: std::sync::OnceLock::new(), observed_slashable: <_>::default(), pending_payload_envelopes: <_>::default(), observed_voluntary_exits: <_>::default(), diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index b3ab2e69756..1eab7ccf7ac 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -967,13 +967,6 @@ impl BeaconChain { .start_slot(T::EthSpec::slots_per_epoch()), ); - self.observed_blob_sidecars.write().prune( - new_view - .finalized_checkpoint - .epoch - .start_slot(T::EthSpec::slots_per_epoch()), - ); - self.observed_column_sidecars.write().prune( new_view .finalized_checkpoint diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index dde09bf1057..81f8fc0a83f 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -3,7 +3,7 @@ pub use proto_array::DisallowedReOrgOffsets; use serde::{Deserialize, Serialize}; use std::str::FromStr; use std::{collections::HashSet, sync::LazyLock, time::Duration}; -use types::{Checkpoint, Hash256}; +use types::{Checkpoint, Hash256, MIN_REQUIRED_EXECUTION_PROOFS}; pub const DEFAULT_FORK_CHOICE_BEFORE_PROPOSAL_TIMEOUT: u64 = 250; @@ -114,6 +114,35 @@ pub struct ChainConfig { pub node_custody_type: NodeCustodyType, /// Disable proposer re-org pub disable_proposer_reorg: bool, + /// Non-default EIP-8025 proof quorum configuration. + /// + /// When disabled, valid execution proofs are tracked as proof metadata only and never change + /// payload/fork-choice validity. + #[serde(default)] + pub execution_proof_quorum: ExecutionProofQuorumConfig, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +pub struct ExecutionProofQuorumConfig { + /// Allow proof validity to mark a Gloas payload envelope as received in fork choice. + pub enabled: bool, + /// Required number of distinct valid proof types for the same new-payload request root. + pub min_valid_proof_types: usize, +} + +impl Default for ExecutionProofQuorumConfig { + fn default() -> Self { + Self { + enabled: false, + min_valid_proof_types: MIN_REQUIRED_EXECUTION_PROOFS, + } + } +} + +impl ExecutionProofQuorumConfig { + pub fn threshold(&self) -> Option { + (self.enabled && self.min_valid_proof_types > 0).then_some(self.min_valid_proof_types) + } } impl Default for ChainConfig { @@ -154,6 +183,7 @@ impl Default for ChainConfig { enable_partial_columns: false, node_custody_type: NodeCustodyType::Fullnode, disable_proposer_reorg: false, + execution_proof_quorum: ExecutionProofQuorumConfig::default(), } } } diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index cfd8ee7d34a..9829db0f1d0 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -1,6 +1,4 @@ -use crate::blob_verification::{ - GossipVerifiedBlob, KzgVerifiedBlob, KzgVerifiedBlobList, verify_kzg_for_blob_list, -}; +use crate::blob_verification::{KzgVerifiedBlob, KzgVerifiedBlobList, verify_kzg_for_blob_list}; use crate::block_verification_types::{AvailabilityPendingExecutedBlock, AvailableExecutedBlock}; use crate::data_availability_checker::overflow_lru_cache::{ DataAvailabilityCheckerInner, ReconstructColumnsDecision, @@ -364,24 +362,6 @@ impl DataAvailabilityChecker { .put_kzg_verified_data_columns(block_root, verified_custody_columns) } - /// Check if we've cached other blobs for this block. If it completes a set and we also - /// have a block cached, return the `Availability` variant triggering block import. - /// Otherwise cache the blob sidecar. - /// - /// This should only accept gossip verified blobs, so we should not have to worry about dupes. - #[instrument(skip_all, level = "trace")] - pub fn put_gossip_verified_blobs< - I: IntoIterator>, - O: ObservationStrategy, - >( - &self, - block_root: Hash256, - blobs: I, - ) -> Result, AvailabilityCheckError> { - self.availability_cache - .put_kzg_verified_blobs(block_root, blobs.into_iter().map(|b| b.into_inner())) - } - #[instrument(skip_all, level = "trace")] pub fn put_kzg_verified_blobs>>( &self, @@ -881,8 +861,6 @@ pub struct AvailableBlock { #[educe(Hash(ignore))] /// Timestamp at which this block first became available (UNIX timestamp, time since 1970). blobs_available_timestamp: Option, - #[educe(Hash(ignore))] - pub spec: Arc, } impl AvailableBlock { @@ -915,12 +893,8 @@ impl AvailableBlock { match &block_data { AvailableBlockData::NoData => { // For Gloas, DA is checked for the PayloadEnvelope, not for the block. - if !block.fork_name_unchecked().gloas_enabled() { - if columns_required { - return Err(AvailabilityCheckError::MissingCustodyColumns); - } else if blobs_required { - return Err(AvailabilityCheckError::MissingBlobs); - } + if !block.fork_name_unchecked().gloas_enabled() && columns_required { + return Err(AvailabilityCheckError::MissingCustodyColumns); } } AvailableBlockData::Blobs(blobs) => { @@ -976,7 +950,6 @@ impl AvailableBlock { block, blob_data: block_data, blobs_available_timestamp: None, - spec: spec.clone(), }) } @@ -1031,7 +1004,6 @@ impl AvailableBlock { } }, blobs_available_timestamp: self.blobs_available_timestamp, - spec: self.spec.clone(), }) } } diff --git a/beacon_node/beacon_chain/src/data_availability_checker/error.rs b/beacon_node/beacon_chain/src/data_availability_checker/error.rs index ab69a629853..2653c848608 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/error.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/error.rs @@ -1,7 +1,8 @@ use kzg::{Error as KzgError, KzgCommitment}; +use strum::IntoStaticStr; use types::{BeaconStateError, ColumnIndex, Hash256}; -#[derive(Debug)] +#[derive(Debug, IntoStaticStr)] pub enum Error { InvalidBlobs(KzgError), MissingBid(Hash256), diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index 8a80f835ab7..2254728850e 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -9,7 +9,7 @@ use crate::data_column_verification::KzgVerifiedCustodyDataColumn; use crate::{BeaconChainTypes, BlockProcessStatus}; use lru::LruCache; use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; -use ssz_types::{RuntimeFixedVector, RuntimeVariableList}; +use ssz_types::RuntimeFixedVector; use std::cmp::Ordering; use std::num::NonZeroUsize; use std::sync::Arc; @@ -200,7 +200,6 @@ impl PendingComponents { /// must be persisted in the DB along with the block. pub fn make_available( &self, - spec: &Arc, num_expected_columns_opt: Option, ) -> Result>, AvailabilityCheckError> { let Some(CachedBlock::Executed(block)) = &self.block else { @@ -235,36 +234,7 @@ impl PendingComponents { } } } else { - // Before PeerDAS, blobs - let num_received_blobs = self.verified_blobs.iter().flatten().count(); - match num_received_blobs.cmp(&num_expected_blobs) { - Ordering::Greater => { - // Should never happen - return Err(AvailabilityCheckError::Unexpected(format!( - "too many blobs got {num_received_blobs} expected {num_expected_blobs}" - ))); - } - Ordering::Equal => { - let max_blobs = spec.max_blobs_per_block(block.block.epoch()) as usize; - let blobs_vec = self - .verified_blobs - .iter() - .flatten() - .map(|blob| blob.clone().to_blob()) - .collect::>(); - let blobs_len = blobs_vec.len(); - let blobs = RuntimeVariableList::new(blobs_vec, max_blobs).map_err(|_| { - AvailabilityCheckError::Unexpected(format!( - "over max_blobs len {blobs_len} max {max_blobs}" - )) - })?; - Some(AvailableBlockData::Blobs(blobs)) - } - Ordering::Less => { - // Not enough blobs received yet - None - } - } + Some(AvailableBlockData::NoData) }; // Block's data not available yet @@ -300,7 +270,6 @@ impl PendingComponents { block: block.clone(), blob_data, blobs_available_timestamp, - spec: spec.clone(), }; self.span.in_scope(|| { @@ -558,7 +527,7 @@ impl DataAvailabilityCheckerInner { num_expected_columns_opt: Option, ) -> Result, AvailabilityCheckError> { if let Some(available_block) = - pending_components.make_available(&self.spec, num_expected_columns_opt)? + pending_components.make_available(num_expected_columns_opt)? { // Explicitly drop read lock before acquiring write lock drop(pending_components); @@ -780,9 +749,11 @@ impl DataAvailabilityCheckerInner { mod test { use super::*; - use crate::test_utils::generate_data_column_indices_rand_order; + use crate::data_column_verification::{GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn}; + use crate::test_utils::{ + generate_data_column_indices_rand_order, generate_data_column_sidecars_from_block, + }; use crate::{ - blob_verification::GossipVerifiedBlob, block_verification::PayloadVerificationOutcome, block_verification_types::{AsBlock, BlockImportData}, custody_context::NodeCustodyType, @@ -794,8 +765,8 @@ mod test { use store::{HotColdDB, ItemStore, StoreConfig, database::interface::BeaconNodeBackend}; use tempfile::{TempDir, tempdir}; use tracing::info; - use types::MinimalEthSpec; use types::new_non_zero_usize; + use types::{DataColumnSubnetId, MinimalEthSpec}; const LOW_VALIDATOR_COUNT: usize = 32; @@ -819,21 +790,25 @@ mod test { .expect("disk store should initialize") } - // get a beacon chain harness advanced to just before deneb fork - async fn get_deneb_chain( + // get a beacon chain harness advanced to just before fulu fork + async fn get_fulu_chain( db_path: &TempDir, ) -> BeaconChainHarness> { let altair_fork_epoch = Epoch::new(0); let bellatrix_fork_epoch = Epoch::new(0); let capella_fork_epoch = Epoch::new(3); let deneb_fork_epoch = Epoch::new(4); - let deneb_fork_slot = deneb_fork_epoch.start_slot(E::slots_per_epoch()); + let electra_fork_epoch = Epoch::new(5); + let fulu_fork_epoch = Epoch::new(6); + let fulu_fork_slot = fulu_fork_epoch.start_slot(E::slots_per_epoch()); let mut spec = E::default_spec(); spec.altair_fork_epoch = Some(altair_fork_epoch); spec.bellatrix_fork_epoch = Some(bellatrix_fork_epoch); spec.capella_fork_epoch = Some(capella_fork_epoch); spec.deneb_fork_epoch = Some(deneb_fork_epoch); + spec.electra_fork_epoch = Some(electra_fork_epoch); + spec.fulu_fork_epoch = Some(fulu_fork_epoch); let spec = Arc::new(spec); let chain_store = get_store_with_spec::(db_path, spec.clone()); @@ -846,8 +821,10 @@ mod test { .mock_execution_layer() .build(); - // go right before deneb slot - harness.extend_to_slot(deneb_fork_slot - 1).await; + harness.execution_block_generator().set_min_blob_count(1); + + // go right before fulu slot + harness.extend_to_slot(fulu_fork_slot - 1).await; harness } @@ -856,7 +833,7 @@ mod test { harness: &BeaconChainHarness>, ) -> ( AvailabilityPendingExecutedBlock, - Vec>>, + Vec>>, ) where E: EthSpec, @@ -874,7 +851,7 @@ mod test { .expect("should get block") .expect("should have block"); - let (signed_beacon_block_hash, (block, maybe_blobs), state) = harness + let (signed_beacon_block_hash, (block, _maybe_blobs), state) = harness .add_block_at_slot(target_slot, parent_state) .await .expect("should add block"); @@ -892,27 +869,25 @@ mod test { .message() .body() .blob_kzg_commitments() - .expect("should be deneb fork") + .expect("should be fulu fork") .clone(), ) { info!(commitment = ?comm, "kzg commitment"); } info!("done printing kzg commitments"); - let gossip_verified_blobs = if let Some((kzg_proofs, blobs)) = maybe_blobs { - let sidecars = - BlobSidecar::build_sidecars(blobs, &block, kzg_proofs, &chain.spec).unwrap(); - Vec::from(sidecars) - .into_iter() - .map(|sidecar| { - let subnet = sidecar.index; - GossipVerifiedBlob::new(sidecar, subnet, &harness.chain) - .expect("should validate blob") - }) - .collect() - } else { - vec![] - }; + // Generate data columns from the block + let data_columns = generate_data_column_sidecars_from_block(&block, &harness.spec); + + let gossip_verified_columns: Vec<_> = data_columns + .into_iter() + .map(|sidecar| { + let subnet_id = + DataColumnSubnetId::from_column_index(*sidecar.index(), &harness.spec); + GossipVerifiedDataColumn::new(sidecar, subnet_id, &harness.chain) + .expect("should validate data column") + }) + .collect(); let slot = block.slot(); let consensus_context = ConsensusContext::::new(slot); @@ -933,7 +908,7 @@ mod test { payload_verification_outcome, }; - (availability_pending_block, gossip_verified_blobs) + (availability_pending_block, gossip_verified_columns) } async fn setup_harness_and_cache( @@ -953,7 +928,7 @@ mod test { { create_test_tracing_subscriber(); let chain_db_path = tempdir().expect("should get temp dir"); - let harness = get_deneb_chain(&chain_db_path).await; + let harness = get_fulu_chain(&chain_db_path).await; let spec = harness.spec.clone(); let capacity_non_zero = new_non_zero_usize(capacity); let custody_context = Arc::new(CustodyContext::new( @@ -979,20 +954,27 @@ mod test { let capacity = 4; let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; - let (pending_block, blobs) = availability_pending_block(&harness).await; + let (pending_block, columns) = availability_pending_block(&harness).await; let root = pending_block.import_data.block_root; + let epoch = pending_block.block.epoch(); + + let num_blobs_expected = pending_block.num_blobs_expected(); + let columns_expected = cache + .custody_context + .num_of_data_columns_to_sample(epoch, &harness.spec); - let blobs_expected = pending_block.num_blobs_expected(); + // All columns are returned from availability_pending_block (E::number_of_columns()) + // but we only need custody columns assert_eq!( - blobs.len(), - blobs_expected, - "should have expected number of blobs" + columns.len(), + E::number_of_columns(), + "should have all data columns from block" ); assert!(cache.critical.read().is_empty(), "cache should be empty"); let availability = cache .put_executed_block(pending_block) .expect("should put block"); - if blobs_expected == 0 { + if num_blobs_expected == 0 { assert!( matches!(availability, Availability::Available(_)), "block doesn't have blobs, should be available" @@ -1005,7 +987,7 @@ mod test { } else { assert!( matches!(availability, Availability::MissingComponents(_)), - "should be pending blobs" + "should be pending columns" ); assert_eq!( cache.critical.read().len(), @@ -1018,13 +1000,26 @@ mod test { ); } - let mut kzg_verified_blobs = Vec::new(); - for (blob_index, gossip_blob) in blobs.into_iter().enumerate() { - kzg_verified_blobs.push(gossip_blob.into_inner()); + // Get sampling column indices for this epoch + let sampling_column_indices = cache + .custody_context + .sampling_columns_for_epoch(epoch, &harness.spec); + + // Filter to only sampling columns + let sampling_columns: Vec<_> = columns + .into_iter() + .filter(|col| sampling_column_indices.contains(&col.index())) + .collect(); + + let mut kzg_verified_columns = Vec::new(); + for (col_index, gossip_column) in sampling_columns.into_iter().enumerate() { + kzg_verified_columns.push(KzgVerifiedCustodyDataColumn::from_asserted_custody( + gossip_column.into_inner(), + )); let availability = cache - .put_kzg_verified_blobs(root, kzg_verified_blobs.clone()) - .expect("should put blob"); - if blob_index == blobs_expected - 1 { + .put_kzg_verified_data_columns(root, kzg_verified_columns.clone()) + .expect("should put column"); + if col_index == columns_expected - 1 { assert!(matches!(availability, Availability::Available(_))); } else { assert!(matches!(availability, Availability::MissingComponents(_))); @@ -1032,20 +1027,36 @@ mod test { } } - let (pending_block, blobs) = availability_pending_block(&harness).await; - let blobs_expected = pending_block.num_blobs_expected(); + let (pending_block, columns) = availability_pending_block(&harness).await; + let _num_blobs_expected = pending_block.num_blobs_expected(); + let epoch = pending_block.block.epoch(); + // All columns returned assert_eq!( - blobs.len(), - blobs_expected, - "should have expected number of blobs" + columns.len(), + E::number_of_columns(), + "should have all data columns" ); let root = pending_block.import_data.block_root; - let mut kzg_verified_blobs = vec![]; - for gossip_blob in blobs { - kzg_verified_blobs.push(gossip_blob.into_inner()); + + // Get sampling column indices for this epoch + let sampling_column_indices = cache + .custody_context + .sampling_columns_for_epoch(epoch, &harness.spec); + + // Filter to only sampling columns + let sampling_columns: Vec<_> = columns + .into_iter() + .filter(|col| sampling_column_indices.contains(&col.index())) + .collect(); + + let mut kzg_verified_columns = vec![]; + for gossip_column in sampling_columns { + kzg_verified_columns.push(KzgVerifiedCustodyDataColumn::from_asserted_custody( + gossip_column.into_inner(), + )); let availability = cache - .put_kzg_verified_blobs(root, kzg_verified_blobs.clone()) - .expect("should put blob"); + .put_kzg_verified_data_columns(root, kzg_verified_columns.clone()) + .expect("should put column"); assert!( matches!(availability, Availability::MissingComponents(_)), "should be pending block" diff --git a/beacon_node/beacon_chain/src/eip8025/mod.rs b/beacon_node/beacon_chain/src/eip8025/mod.rs new file mode 100644 index 00000000000..23ed5c16691 --- /dev/null +++ b/beacon_node/beacon_chain/src/eip8025/mod.rs @@ -0,0 +1,18 @@ +//! EIP-8025: Optional Execution Proofs +//! +//! This module provides beacon chain integration for EIP-8025 optional execution proofs. +//! It includes: +//! - Proof verification logic using validator signatures +//! - TODO: integrate into proof engine + +pub mod proof_status; +pub mod proof_verification; + +pub use proof_status::{ + ExecutionProofBlockStatus, ExecutionProofObservation, ExecutionProofStatusCache, + ExecutionProofStatusSummary, MissingExecutionProofInfo, +}; +pub use proof_verification::{ + ExecutionProofError, compute_execution_proof_domain, compute_signing_root, + verify_signed_execution_proof_signature, +}; diff --git a/beacon_node/beacon_chain/src/eip8025/proof_status.rs b/beacon_node/beacon_chain/src/eip8025/proof_status.rs new file mode 100644 index 00000000000..789071a9c85 --- /dev/null +++ b/beacon_node/beacon_chain/src/eip8025/proof_status.rs @@ -0,0 +1,682 @@ +use super::proof_verification::{ExecutionProofError, verify_signed_execution_proof_signature}; +use crate::observed_execution_proofs::ProofObservation; +use crate::{BeaconChain, BeaconChainError, BeaconChainTypes, ForkChoiceError}; +use execution_layer::{NewPayloadRequest, NewPayloadRequestGloas}; +use lru::LruCache; +use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash; +use std::collections::{HashMap, HashSet}; +use std::num::NonZeroUsize; +use std::sync::Arc; +use store::DatabaseBlock; +use types::{ + EthSpec, Hash256, ProofStatus, ProofType, SignedBlindedBeaconBlock, + SignedExecutionPayloadEnvelope, SignedExecutionProof, Slot, +}; + +const DEFAULT_REQUEST_ROOT_CACHE_SIZE: usize = 8192; +const DEFAULT_PROOF_CACHE_SIZE: usize = 8192; + +/// Proof metadata for one beacon block / `engine_newPayload` request. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExecutionProofBlockStatus { + pub block_root: Hash256, + pub request_root: Hash256, + pub slot: Slot, + valid_proof_types: HashSet, +} + +impl ExecutionProofBlockStatus { + fn new(block_root: Hash256, request_root: Hash256, slot: Slot) -> Self { + Self { + block_root, + request_root, + slot, + valid_proof_types: HashSet::new(), + } + } + + pub fn valid_proof_type_count(&self) -> usize { + self.valid_proof_types.len() + } + + pub fn valid_proof_types(&self) -> impl Iterator + '_ { + self.valid_proof_types.iter().copied() + } +} + +/// Bounded request-root ingress cache plus proof-status metadata. +/// +/// This deliberately stores proof status only. Unfinalized proof bytes remain hot/prunable and are +/// not durably tracked here. +#[derive(Debug)] +pub struct ExecutionProofStatusCache { + request_root_to_block_root: LruCache, + block_root_to_request_root: LruCache, + proofs_by_block_and_type: LruCache<(Hash256, ProofType), Arc>, + statuses_by_block_root: HashMap, +} + +impl Default for ExecutionProofStatusCache { + fn default() -> Self { + let request_root_capacity = NonZeroUsize::new(DEFAULT_REQUEST_ROOT_CACHE_SIZE) + .expect("default request-root cache size is non-zero"); + let proof_capacity = NonZeroUsize::new(DEFAULT_PROOF_CACHE_SIZE) + .expect("default proof cache size is non-zero"); + Self { + request_root_to_block_root: LruCache::new(request_root_capacity), + block_root_to_request_root: LruCache::new(request_root_capacity), + proofs_by_block_and_type: LruCache::new(proof_capacity), + statuses_by_block_root: HashMap::new(), + } + } +} + +impl ExecutionProofStatusCache { + pub fn register_request_root( + &mut self, + block_root: Hash256, + request_root: Hash256, + slot: Slot, + ) { + self.request_root_to_block_root + .put(request_root, block_root); + self.block_root_to_request_root + .put(block_root, request_root); + self.statuses_by_block_root + .entry(block_root) + .or_insert_with(|| ExecutionProofBlockStatus::new(block_root, request_root, slot)); + } + + pub fn block_root_for_request_root(&self, request_root: &Hash256) -> Option { + self.request_root_to_block_root.peek(request_root).copied() + } + + pub fn request_root_for_block_root(&self, block_root: &Hash256) -> Option { + self.block_root_to_request_root.peek(block_root).copied() + } + + pub fn block_context_for_request_root( + &self, + request_root: &Hash256, + ) -> Option<(Hash256, Slot)> { + let block_root = self.request_root_to_block_root.peek(request_root)?; + self.statuses_by_block_root + .get(block_root) + .map(|status| (status.block_root, status.slot)) + } + + pub fn observe_valid_proof( + &mut self, + block_root: Hash256, + request_root: Hash256, + slot: Slot, + proof: Arc, + ) -> ExecutionProofStatusSummary { + let proof_type = proof.proof_type(); + self.register_request_root(block_root, request_root, slot); + let status = self + .statuses_by_block_root + .entry(block_root) + .or_insert_with(|| ExecutionProofBlockStatus::new(block_root, request_root, slot)); + let newly_observed = status.valid_proof_types.insert(proof_type); + self.proofs_by_block_and_type + .put((block_root, proof_type), proof); + + ExecutionProofStatusSummary { + block_root, + request_root, + slot, + newly_observed, + valid_proof_type_count: status.valid_proof_type_count(), + } + } + + pub fn status_by_block_root(&self, block_root: &Hash256) -> Option<&ExecutionProofBlockStatus> { + self.statuses_by_block_root.get(block_root) + } + + pub fn latest_status_with_valid_proofs( + &self, + configured_proof_types: &[ProofType], + ) -> Option { + self.statuses_by_block_root + .values() + .filter(|status| { + configured_proof_types + .iter() + .any(|proof_type| status.valid_proof_types.contains(proof_type)) + }) + .max_by_key(|status| status.slot) + .cloned() + } + + pub fn proof_by_block_root_and_type( + &mut self, + block_root: Hash256, + proof_type: ProofType, + ) -> Option> { + self.proofs_by_block_and_type + .get(&(block_root, proof_type)) + .cloned() + } + + pub fn missing_execution_proofs( + &self, + configured_proof_types: &[ProofType], + ) -> Vec { + self.statuses_by_block_root + .values() + .filter_map(|status| { + let missing_any = configured_proof_types + .iter() + .any(|proof_type| !status.valid_proof_types.contains(proof_type)); + missing_any.then(|| MissingExecutionProofInfo { + root: status.block_root, + slot: status.slot, + existing_proof_types: status.valid_proof_types.clone(), + }) + }) + .collect() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExecutionProofStatusSummary { + pub block_root: Hash256, + pub request_root: Hash256, + pub slot: Slot, + pub newly_observed: bool, + pub valid_proof_type_count: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MissingExecutionProofInfo { + pub root: Hash256, + pub slot: Slot, + pub existing_proof_types: HashSet, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExecutionProofObservation { + pub status: ProofStatus, + pub block_root: Option, + pub request_root: Hash256, + pub valid_proof_type_count: usize, + pub quorum_threshold: Option, + pub proof_backed_payload_promotion: bool, +} + +impl ExecutionProofObservation { + fn syncing(request_root: Hash256, quorum_threshold: Option) -> Self { + Self { + status: ProofStatus::Syncing, + block_root: None, + request_root, + valid_proof_type_count: 0, + quorum_threshold, + proof_backed_payload_promotion: false, + } + } +} + +impl BeaconChain { + /// Compute and cache the EIP-8025 new-payload request root for a known Gloas block root. + pub fn register_execution_payload_request_root( + &self, + block_root: Hash256, + ) -> Result { + let (request_root, slot) = self.execution_payload_request_context(block_root)?; + self.execution_proof_statuses + .write() + .register_request_root(block_root, request_root, slot); + Ok(request_root) + } + + /// Return the cached block root for an EIP-8025 new-payload request root. + pub fn block_root_for_execution_proof_request( + &self, + request_root: &Hash256, + ) -> Option { + self.execution_proof_statuses + .read() + .block_root_for_request_root(request_root) + } + + /// Record one externally-validated proof and optionally apply the non-default proof quorum. + /// + /// This function assumes BLS signature checks and proof-engine verification have already + /// succeeded. Invalid proofs must not call this path. + pub fn observe_valid_execution_proof( + &self, + proof: &SignedExecutionProof, + block_root_hint: Option, + ) -> Result { + let request_root = proof.request_root(); + let quorum_threshold = self.config.execution_proof_quorum.threshold(); + + let Some((block_root, slot)) = + self.resolve_execution_proof_block_root(request_root, block_root_hint)? + else { + return Ok(ExecutionProofObservation::syncing( + request_root, + quorum_threshold, + )); + }; + + let summary = self.execution_proof_statuses.write().observe_valid_proof( + block_root, + request_root, + slot, + Arc::new(proof.clone()), + ); + + self.observed_execution_proofs.write().observe_valid_proof( + request_root, + proof.proof_type(), + slot, + ); + + let proof_backed_payload_promotion = if quorum_threshold + .is_some_and(|threshold| summary.valid_proof_type_count >= threshold) + { + self.try_mark_proof_backed_payload_valid(block_root)? + } else { + false + }; + + Ok(ExecutionProofObservation { + status: if proof_backed_payload_promotion { + ProofStatus::Valid + } else { + ProofStatus::Accepted + }, + block_root: Some(block_root), + request_root, + valid_proof_type_count: summary.valid_proof_type_count, + quorum_threshold, + proof_backed_payload_promotion, + }) + } + + /// Verify a signed execution proof and record proof metadata if it is valid. + /// + /// This path keeps proof validity optional: invalid proofs never invalidate the payload and + /// valid proofs only affect fork choice when `execution_proof_quorum` is explicitly enabled. + pub async fn verify_and_observe_execution_proof( + &self, + proof: &SignedExecutionProof, + block_root_hint: Option, + ) -> Result { + let request_root = proof.request_root(); + let proof_type = proof.proof_type(); + let quorum_threshold = self.config.execution_proof_quorum.threshold(); + + match self.observed_execution_proofs.read().check( + request_root, + proof_type, + proof.proof_data(), + proof.validator_index(), + ) { + ProofObservation::AlreadyRejectedProof => { + return Ok(ExecutionProofObservation { + status: ProofStatus::Invalid, + block_root: None, + request_root, + valid_proof_type_count: 0, + quorum_threshold, + proof_backed_payload_promotion: false, + }); + } + ProofObservation::AlreadyHaveValidProof | ProofObservation::DuplicateFromValidator => { + return Ok(ExecutionProofObservation { + status: ProofStatus::Accepted, + block_root: self.block_root_for_execution_proof_request(&request_root), + request_root, + valid_proof_type_count: 0, + quorum_threshold, + proof_backed_payload_promotion: false, + }); + } + ProofObservation::New => {} + } + + let Some((_, slot)) = + self.resolve_execution_proof_block_root(request_root, block_root_hint)? + else { + return Ok(ExecutionProofObservation::syncing( + request_root, + quorum_threshold, + )); + }; + + self.observed_execution_proofs + .write() + .observe_verification_attempt(request_root, proof_type, proof.validator_index()); + + let validator_index = usize::try_from(proof.validator_index()) + .map_err(|_| ExecutionProofError::InvalidValidatorIndex)?; + let validator_pubkey = self + .validator_pubkey_bytes(validator_index)? + .ok_or(ExecutionProofError::InvalidValidatorIndex)?; + let fork_name = self.spec.fork_name_at_slot::(slot); + + verify_signed_execution_proof_signature::( + proof, + &validator_pubkey, + fork_name, + self.genesis_validators_root, + &self.spec, + )?; + + let proof_engine = self + .execution_layer + .as_ref() + .and_then(|execution_layer| execution_layer.proof_engine()) + .ok_or(ExecutionProofError::NoExecutionLayer)?; + + match proof_engine.verify_execution_proof(proof).await? { + ProofStatus::Valid => self.observe_valid_execution_proof(proof, block_root_hint), + ProofStatus::Invalid => { + self.observed_execution_proofs + .write() + .observe_invalid_proof(proof_type, proof.proof_data()); + Ok(ExecutionProofObservation { + status: ProofStatus::Invalid, + block_root: self.block_root_for_execution_proof_request(&request_root), + request_root, + valid_proof_type_count: 0, + quorum_threshold, + proof_backed_payload_promotion: false, + }) + } + status => Ok(ExecutionProofObservation { + status, + block_root: self.block_root_for_execution_proof_request(&request_root), + request_root, + valid_proof_type_count: 0, + quorum_threshold, + proof_backed_payload_promotion: false, + }), + } + } + + fn resolve_execution_proof_block_root( + &self, + request_root: Hash256, + block_root_hint: Option, + ) -> Result, BeaconChainError> { + if let Some(block_root) = block_root_hint { + let (computed_request_root, slot) = + self.execution_payload_request_context(block_root)?; + if computed_request_root != request_root { + return Err(BeaconChainError::ExecutionProofError( + super::proof_verification::ExecutionProofError::UnknownRequestRoot( + request_root, + ), + )); + } + self.execution_proof_statuses.write().register_request_root( + block_root, + request_root, + slot, + ); + return Ok(Some((block_root, slot))); + } + + let Some((block_root, slot)) = self + .execution_proof_statuses + .read() + .block_context_for_request_root(&request_root) + else { + return Ok(None); + }; + + Ok(Some((block_root, slot))) + } + + fn execution_payload_request_context( + &self, + block_root: Hash256, + ) -> Result<(Hash256, Slot), BeaconChainError> { + if let Some(DatabaseBlock::Full(block)) = self.store.try_get_full_block(&block_root)? + && !block.fork_name_unchecked().gloas_enabled() + { + let slot = block.slot(); + let request = NewPayloadRequest::try_from(block.message()) + .map_err(BeaconChainError::BeaconStateError)?; + return Ok((request.request_root(), slot)); + } + + let block = self + .get_blinded_block(&block_root)? + .ok_or(BeaconChainError::MissingBeaconBlock(block_root))?; + let envelope = self.get_payload_envelope(&block_root)?.ok_or( + BeaconChainError::MissingExecutionPayloadEnvelope(block_root), + )?; + let slot = block.slot(); + let request = build_gloas_new_payload_request(&block, &envelope)?; + + Ok((request.request_root(), slot)) + } + + fn try_mark_proof_backed_payload_valid( + &self, + block_root: Hash256, + ) -> Result { + let block = self + .get_blinded_block(&block_root)? + .ok_or(BeaconChainError::MissingBeaconBlock(block_root))?; + let is_gloas = block.fork_name_unchecked().gloas_enabled(); + if is_gloas && self.get_payload_envelope(&block_root)?.is_none() { + return Ok(false); + } + + let mut fork_choice = self.canonical_head.fork_choice_write_lock(); + if is_gloas { + fork_choice + .on_valid_payload_envelope_received(block_root) + .map_err(map_fork_choice_error)?; + } else { + fork_choice + .on_valid_execution_payload(block_root) + .map_err(map_fork_choice_error)?; + } + + Ok(true) + } + + pub fn execution_proof_by_block_root_and_type( + &self, + block_root: Hash256, + proof_type: ProofType, + ) -> Option> { + self.execution_proof_statuses + .write() + .proof_by_block_root_and_type(block_root, proof_type) + } + + pub fn execution_proofs_by_block_root( + &self, + block_root: Hash256, + proof_types: &[ProofType], + ) -> Vec> { + proof_types + .iter() + .filter_map(|proof_type| { + self.execution_proof_by_block_root_and_type(block_root, *proof_type) + }) + .collect() + } + + pub fn execution_proofs_by_range( + &self, + start_slot: Slot, + count: u64, + proof_types: &[ProofType], + ) -> Result>, BeaconChainError> { + let mut proofs = vec![]; + for offset in 0..count { + let Some(slot) = start_slot.as_u64().checked_add(offset).map(Slot::new) else { + break; + }; + let Some(block_root) = self.block_root_at_slot(slot, crate::WhenSlotSkipped::None)? + else { + continue; + }; + proofs.extend(self.execution_proofs_by_block_root(block_root, proof_types)); + } + Ok(proofs) + } + + pub fn missing_execution_proofs( + &self, + proof_types: &[ProofType], + ) -> Vec { + self.register_execution_proof_request_window(); + self.execution_proof_statuses + .read() + .missing_execution_proofs(proof_types) + } + + pub fn latest_execution_proof_status( + &self, + proof_types: &[ProofType], + ) -> Option { + self.execution_proof_statuses + .read() + .latest_status_with_valid_proofs(proof_types) + } + + fn register_execution_proof_request_window(&self) { + let head = self.canonical_head.cached_head(); + let start_slot = head + .finalized_checkpoint() + .epoch + .start_slot(T::EthSpec::slots_per_epoch()); + let end_slot = head.head_slot(); + + for slot in start_slot.as_u64()..=end_slot.as_u64() { + let slot = Slot::new(slot); + let Ok(Some(block_root)) = self.block_root_at_slot(slot, crate::WhenSlotSkipped::None) + else { + continue; + }; + + if self + .execution_proof_statuses + .read() + .request_root_for_block_root(&block_root) + .is_some() + { + continue; + } + + let Ok((request_root, request_slot)) = + self.execution_payload_request_context(block_root) + else { + continue; + }; + + self.execution_proof_statuses.write().register_request_root( + block_root, + request_root, + request_slot, + ); + } + } +} + +fn build_gloas_new_payload_request<'a, E: types::EthSpec>( + block: &'a SignedBlindedBeaconBlock, + envelope: &'a SignedExecutionPayloadEnvelope, +) -> Result, BeaconChainError> { + let bid = &block + .message() + .body() + .signed_execution_payload_bid() + .map_err(BeaconChainError::BeaconStateError)? + .message; + + let versioned_hashes = bid + .blob_kzg_commitments + .iter() + .map(kzg_commitment_to_versioned_hash) + .collect::>() + .try_into() + .map_err(BeaconChainError::SszTypesError)?; + + Ok(NewPayloadRequest::Gloas(NewPayloadRequestGloas { + execution_payload: &envelope.message.payload, + versioned_hashes, + parent_beacon_block_root: envelope.message.parent_beacon_block_root, + execution_requests: &envelope.message.execution_requests, + })) +} + +fn map_fork_choice_error(error: ForkChoiceError) -> BeaconChainError { + BeaconChainError::ForkChoiceError(error) +} + +#[cfg(test)] +mod tests { + use super::*; + use bls::SignatureBytes; + use ssz_types::VariableList; + use types::{ExecutionProof, PublicInput}; + + fn signed_proof(request_root: Hash256, proof_type: ProofType) -> Arc { + Arc::new(SignedExecutionProof { + message: ExecutionProof { + proof_data: VariableList::new(vec![proof_type]).unwrap(), + proof_type, + public_input: PublicInput { + new_payload_request_root: request_root, + }, + }, + validator_index: 0, + signature: SignatureBytes::empty(), + }) + } + + #[test] + fn latest_status_with_valid_proofs_ignores_empty_and_unconfigured_statuses() { + let mut cache = ExecutionProofStatusCache::default(); + let block_root_a = Hash256::repeat_byte(0xaa); + let block_root_b = Hash256::repeat_byte(0xbb); + let block_root_c = Hash256::repeat_byte(0xcc); + let request_root_a = Hash256::repeat_byte(0x0a); + let request_root_b = Hash256::repeat_byte(0x0b); + let request_root_c = Hash256::repeat_byte(0x0c); + + cache.register_request_root(block_root_c, request_root_c, Slot::new(30)); + assert!( + cache.latest_status_with_valid_proofs(&[1]).is_none(), + "request-root-only statuses must not advertise proof availability" + ); + + cache.observe_valid_proof( + block_root_a, + request_root_a, + Slot::new(10), + signed_proof(request_root_a, 1), + ); + cache.observe_valid_proof( + block_root_b, + request_root_b, + Slot::new(20), + signed_proof(request_root_b, 2), + ); + + let status = cache + .latest_status_with_valid_proofs(&[1]) + .expect("configured proof type should be advertised"); + assert_eq!(status.block_root, block_root_a); + assert_eq!(status.slot, Slot::new(10)); + assert_eq!(status.valid_proof_types().collect::>(), vec![1]); + + assert!( + cache.latest_status_with_valid_proofs(&[3]).is_none(), + "unconfigured proof types must not make a peer look useful" + ); + } +} diff --git a/beacon_node/beacon_chain/src/eip8025/proof_verification.rs b/beacon_node/beacon_chain/src/eip8025/proof_verification.rs new file mode 100644 index 00000000000..7dd21bcb121 --- /dev/null +++ b/beacon_node/beacon_chain/src/eip8025/proof_verification.rs @@ -0,0 +1,466 @@ +//! EIP-8025 Proof Verification +//! +//! This module implements the proof verification logic for EIP-8025 optional execution proofs. +//! It provides: +//! - BLS signature verification for validator signatures +//! - Validator index validation against the BeaconState +//! - TODO: integration into proof engine for end-to-end verification + +use crate::BeaconChainError; +use execution_layer::eip8025::ProofEngineError; +use std::fmt; +use tree_hash::TreeHash; +use types::{ + ChainSpec, DOMAIN_EXECUTION_PROOF, EthSpec, ForkName, Hash256, SignedExecutionProof, + SigningData, +}; + +/// Errors that can occur during execution proof verification. +#[derive(Debug)] +pub enum ExecutionProofError { + /// The BLS signature is invalid. + InvalidSignature, + /// The proof data is empty. + EmptyProofData, + /// The validator index is out of range. + InvalidValidatorIndex, + /// Failed to decompress the validator's public key. + InvalidValidatorPubkey, + /// Failed to decompress the signature. + InvalidSignatureFormat, + /// Failed to retrieve beacon state. + StateError(String), + /// No execution layer configured. + NoExecutionLayer, + /// The request root referenced by the proof is not known. + UnknownRequestRoot(Hash256), + /// There was an error in the proof engine during verification. + ProofEngineError(ProofEngineError), +} + +impl fmt::Display for ExecutionProofError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ExecutionProofError::InvalidSignature => { + write!(f, "Invalid BLS signature") + } + ExecutionProofError::EmptyProofData => { + write!(f, "Proof data is empty") + } + ExecutionProofError::InvalidValidatorIndex => { + write!(f, "Validator index out of range") + } + ExecutionProofError::InvalidValidatorPubkey => { + write!(f, "Invalid validator public key format") + } + ExecutionProofError::InvalidSignatureFormat => { + write!(f, "Invalid signature format") + } + ExecutionProofError::StateError(msg) => { + write!(f, "Beacon state error: {}", msg) + } + ExecutionProofError::NoExecutionLayer => { + write!(f, "No execution layer configured") + } + ExecutionProofError::UnknownRequestRoot(root) => { + write!( + f, + "Unknown request root {:?}. Block may not be imported yet or was already finalized.", + root + ) + } + ExecutionProofError::ProofEngineError(engine_error) => { + write!(f, "Proof engine error: {:?}", engine_error) + } + } + } +} + +impl std::error::Error for ExecutionProofError {} + +/// Compute the signing root for an execution proof message. +/// +/// This function is public for use by the validator client when signing proofs. +pub fn compute_signing_root(message: &types::ExecutionProof, domain: Hash256) -> Hash256 { + SigningData { + object_root: message.tree_hash_root(), + domain, + } + .tree_hash_root() +} + +/// Compute the domain for execution proof signing. +/// +/// This function is public for use by the validator client when signing proofs. +pub fn compute_execution_proof_domain( + fork_name: ForkName, + genesis_validators_root: Hash256, + spec: &ChainSpec, +) -> Hash256 { + let fork_version = spec.fork_version_for_name(fork_name); + let fork_data_root = ChainSpec::compute_fork_data_root(fork_version, genesis_validators_root); + + let mut domain = [0; 32]; + domain[0..4].copy_from_slice(&DOMAIN_EXECUTION_PROOF); + domain[4..].copy_from_slice( + fork_data_root + .as_slice() + .get(..28) + .expect("fork data root is 32 bytes so first 28 bytes should exist"), + ); + + Hash256::from(domain) +} + +// TODO: migrate into an impl on BeaconChain +/// Verify a validator's BLS signature over an execution proof. +/// +/// This function: +/// 1. Checks that the fork supports EIP-8025 +/// 2. Checks that proof data is not empty (max proof size should be enforced by ssz deserialization) +/// 3. Verifies the BLS signature over the proof message using the validator's pubkey +/// +/// # Arguments +/// +/// * `signed_proof` - The signed execution proof to verify +/// * `validator_pubkey` - The public key of the validator at the specified index +/// * `fork_name` - The current fork name +/// * `genesis_validators_root` - The genesis validators root for domain computation +/// * `spec` - The chain specification +/// +/// # Returns +/// +/// `Ok(())` if the proof is valid, otherwise an `ExecutionProofError`. +pub fn verify_signed_execution_proof_signature( + signed_proof: &SignedExecutionProof, + validator_pubkey: &bls::PublicKeyBytes, + fork_name: ForkName, + genesis_validators_root: Hash256, + spec: &ChainSpec, +) -> Result<(), BeaconChainError> { + // Check proof data is not empty + if signed_proof.message.proof_data.is_empty() { + Err(ExecutionProofError::EmptyProofData)?; + } + + // Decompress the validator's public key + let pubkey = validator_pubkey + .decompress() + .map_err(|_| ExecutionProofError::InvalidValidatorPubkey)?; + + // Decompress the signature using bls::SignatureBytes::decompress() + let signature = signed_proof + .signature + .decompress() + .map_err(|_| ExecutionProofError::InvalidSignatureFormat)?; + + // Get the domain for execution proof signing + let domain = compute_execution_proof_domain(fork_name, genesis_validators_root, spec); + + // Compute the signing root + let signing_root = compute_signing_root(&signed_proof.message, domain); + + // Verify the signature + if !signature.verify(&pubkey, signing_root) { + Err(ExecutionProofError::InvalidSignature)?; + } + + Ok(()) +} + +impl From for BeaconChainError { + fn from(engine_error: ProofEngineError) -> Self { + BeaconChainError::ExecutionProofError(ExecutionProofError::ProofEngineError(engine_error)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::BeaconChainError; + use bls::{Keypair, SignatureBytes}; + use ssz_types::VariableList; + use types::{ExecutionProof, MainnetEthSpec, PublicInput}; + + fn get_fulu_spec() -> ChainSpec { + ForkName::Fulu.make_genesis_spec(MainnetEthSpec::default_spec()) + } + + fn create_test_proof(proof_data: Vec) -> ExecutionProof { + ExecutionProof { + proof_data: VariableList::new(proof_data).unwrap(), + proof_type: 1, + public_input: PublicInput { + new_payload_request_root: Hash256::repeat_byte(0xab), + }, + } + } + + fn sign_proof( + proof: &ExecutionProof, + keypair: &Keypair, + fork_name: ForkName, + genesis_validators_root: Hash256, + spec: &ChainSpec, + ) -> SignedExecutionProof { + let domain = compute_execution_proof_domain(fork_name, genesis_validators_root, spec); + let signing_root = compute_signing_root(proof, domain); + let signature = keypair.sk.sign(signing_root); + + // Convert signature to bls::SignatureBytes + let sig_bytes = signature.serialize(); + let signature_vec: SignatureBytes = SignatureBytes::deserialize(&sig_bytes).unwrap(); + + SignedExecutionProof { + message: proof.clone(), + validator_index: 0, + signature: signature_vec, + } + } + + #[test] + fn test_verify_valid_signature() { + let keypair = Keypair::random(); + let spec = get_fulu_spec(); + let genesis_validators_root = Hash256::repeat_byte(0xcd); + let proof = create_test_proof(vec![1, 2, 3, 4]); + + let signed = sign_proof( + &proof, + &keypair, + ForkName::Fulu, + genesis_validators_root, + &spec, + ); + + let result = verify_signed_execution_proof_signature::( + &signed, + &keypair.pk.compress(), + ForkName::Fulu, + genesis_validators_root, + &spec, + ); + + assert!(result.is_ok()); + } + + #[test] + fn test_verify_invalid_signature() { + let keypair = Keypair::random(); + let wrong_keypair = Keypair::random(); + let spec = get_fulu_spec(); + let genesis_validators_root = Hash256::repeat_byte(0xcd); + let proof = create_test_proof(vec![1, 2, 3, 4]); + + // Sign with one keypair, verify with another + let signed = sign_proof( + &proof, + &keypair, + ForkName::Fulu, + genesis_validators_root, + &spec, + ); + + let result = verify_signed_execution_proof_signature::( + &signed, + &wrong_keypair.pk.compress(), + ForkName::Fulu, + genesis_validators_root, + &spec, + ); + + assert!(matches!( + result, + Err(BeaconChainError::ExecutionProofError( + ExecutionProofError::InvalidSignature + )) + )); + } + + #[test] + fn test_verify_empty_proof_data() { + let keypair = Keypair::random(); + let spec = get_fulu_spec(); + let genesis_validators_root = Hash256::repeat_byte(0xcd); + let proof = create_test_proof(vec![]); // Empty proof data + + let signed = sign_proof( + &proof, + &keypair, + ForkName::Fulu, + genesis_validators_root, + &spec, + ); + + let result = verify_signed_execution_proof_signature::( + &signed, + &keypair.pk.compress(), + ForkName::Fulu, + genesis_validators_root, + &spec, + ); + + assert!(matches!( + result, + Err(BeaconChainError::ExecutionProofError( + ExecutionProofError::EmptyProofData + )) + )); + } + + #[test] + fn test_verify_invalid_pubkey_format() { + let keypair = Keypair::random(); + let spec = get_fulu_spec(); + let genesis_validators_root = Hash256::repeat_byte(0xcd); + let proof = create_test_proof(vec![1, 2, 3, 4]); + + let signed = sign_proof( + &proof, + &keypair, + ForkName::Fulu, + genesis_validators_root, + &spec, + ); + + // Create invalid pubkey bytes (all zeros is not a valid point on the curve) + let invalid_pubkey = bls::PublicKeyBytes::empty(); + + let result = verify_signed_execution_proof_signature::( + &signed, + &invalid_pubkey, + ForkName::Fulu, + genesis_validators_root, + &spec, + ); + + assert!(matches!( + result, + Err(BeaconChainError::ExecutionProofError( + ExecutionProofError::InvalidValidatorPubkey + )) + )); + } + + #[test] + fn test_verify_invalid_signature_format() { + let keypair = Keypair::random(); + let spec = get_fulu_spec(); + let genesis_validators_root = Hash256::repeat_byte(0xcd); + let proof = create_test_proof(vec![1, 2, 3, 4]); + + // Create a signed proof with invalid signature bytes. + // BLS signatures are G2 points. Bytes 0xff repeated are not a valid + // compressed G2 point representation because they fail deserialization. + let invalid_signature = SignatureBytes::deserialize(&[0xff; 96]).unwrap(); + let signed = SignedExecutionProof { + message: proof, + validator_index: 0, + signature: invalid_signature, + }; + + let result = verify_signed_execution_proof_signature::( + &signed, + &keypair.pk.compress(), + ForkName::Fulu, + genesis_validators_root, + &spec, + ); + + assert!(matches!( + result, + Err(BeaconChainError::ExecutionProofError( + ExecutionProofError::InvalidSignatureFormat + )) + )); + } + + #[test] + fn test_compute_signing_root_deterministic() { + let proof = create_test_proof(vec![1, 2, 3, 4]); + let domain = Hash256::repeat_byte(0xaa); + + let root1 = compute_signing_root(&proof, domain); + let root2 = compute_signing_root(&proof, domain); + + assert_eq!(root1, root2); + } + + #[test] + fn test_compute_signing_root_different_inputs() { + let proof1 = create_test_proof(vec![1, 2, 3, 4]); + let proof2 = create_test_proof(vec![5, 6, 7, 8]); + let domain = Hash256::repeat_byte(0xaa); + + let root1 = compute_signing_root(&proof1, domain); + let root2 = compute_signing_root(&proof2, domain); + + assert_ne!(root1, root2); + } + + #[test] + fn test_compute_signing_root_different_domains() { + let proof = create_test_proof(vec![1, 2, 3, 4]); + let domain1 = Hash256::repeat_byte(0xaa); + let domain2 = Hash256::repeat_byte(0xbb); + + let root1 = compute_signing_root(&proof, domain1); + let root2 = compute_signing_root(&proof, domain2); + + assert_ne!(root1, root2); + } + + #[test] + fn test_compute_execution_proof_domain() { + let spec = get_fulu_spec(); + let genesis_validators_root = Hash256::repeat_byte(0xcd); + + let domain1 = + compute_execution_proof_domain(ForkName::Fulu, genesis_validators_root, &spec); + + // Domain should be deterministic + let domain2 = + compute_execution_proof_domain(ForkName::Fulu, genesis_validators_root, &spec); + assert_eq!(domain1, domain2); + + // Different genesis_validators_root should produce different domain + let different_root = Hash256::repeat_byte(0xef); + let domain3 = compute_execution_proof_domain(ForkName::Fulu, different_root, &spec); + assert_ne!(domain1, domain3); + } + + #[test] + fn test_verify_with_different_genesis_validators_root() { + let keypair = Keypair::random(); + let spec = get_fulu_spec(); + let genesis_validators_root = Hash256::repeat_byte(0xcd); + let different_root = Hash256::repeat_byte(0xef); + let proof = create_test_proof(vec![1, 2, 3, 4]); + + // Sign with one genesis_validators_root + let signed = sign_proof( + &proof, + &keypair, + ForkName::Fulu, + genesis_validators_root, + &spec, + ); + + // Verify with different genesis_validators_root + let result = verify_signed_execution_proof_signature::( + &signed, + &keypair.pk.compress(), + ForkName::Fulu, + different_root, + &spec, + ); + + // Should fail because domain computation uses genesis_validators_root + assert!(matches!( + result, + Err(BeaconChainError::ExecutionProofError( + ExecutionProofError::InvalidSignature + )) + )); + } +} diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 5efe9a3c232..32bb152a700 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -2,6 +2,7 @@ use crate::beacon_block_streamer::Error as BlockStreamerError; use crate::beacon_chain::ForkChoiceError; use crate::beacon_fork_choice_store::Error as ForkChoiceStoreError; use crate::data_availability_checker::AvailabilityCheckError; +use crate::eip8025::proof_verification::ExecutionProofError; use crate::migrate::PruningError; use crate::naive_aggregation_pool::Error as NaiveAggregationError; use crate::observed_aggregates::Error as ObservedAttestationsError; @@ -147,6 +148,7 @@ pub enum BeaconChainError { AltairForkDisabled, BuilderMissing, ExecutionLayerMissing, + ExecutionProofError(ExecutionProofError), BlockVariantLacksExecutionPayload(Hash256), ExecutionLayerErrorPayloadReconstruction(ExecutionBlockHash, Box), EngineGetCapabilititesFailed(Box), @@ -281,6 +283,7 @@ easy_from_to!(BlockSignatureVerifierError, BeaconChainError); easy_from_to!(PruningError, BeaconChainError); easy_from_to!(ArithError, BeaconChainError); easy_from_to!(ForkChoiceStoreError, BeaconChainError); +easy_from_to!(ExecutionProofError, BeaconChainError); easy_from_to!(StateAdvanceError, BeaconChainError); easy_from_to!(BlockReplayError, BeaconChainError); easy_from_to!(InconsistentFork, BeaconChainError); diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index c8976fc6a83..c6486bc959b 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -12,12 +12,13 @@ use crate::{ ExecutionPayloadError, }; use execution_layer::{ - BlockProposalContentsType, BuilderParams, NewPayloadRequest, PayloadAttributes, + BlockProposalContentsType, BuilderParams, ExecutionLayer, NewPayloadRequest, PayloadAttributes, PayloadParameters, PayloadStatus, }; use fork_choice::{InvalidationOperation, PayloadVerificationStatus}; use proto_array::{Block as ProtoBlock, ExecutionStatus}; use slot_clock::SlotClock; +use ssz::Encode; use state_processing::per_block_processing::{ compute_timestamp_at_slot, get_expected_withdrawals, is_execution_enabled, partially_verify_execution_payload, @@ -139,6 +140,8 @@ pub async fn notify_new_payload( .as_ref() .ok_or(ExecutionPayloadError::NoExecutionConnection)?; + request_execution_proofs(chain, execution_layer, &new_payload_request); + let execution_block_hash = new_payload_request.execution_payload_ref().block_hash(); let new_payload_response = execution_layer .notify_new_payload(new_payload_request.clone()) @@ -213,6 +216,41 @@ pub async fn notify_new_payload( } } +fn request_execution_proofs( + chain: &Arc>, + execution_layer: &ExecutionLayer, + new_payload_request: &NewPayloadRequest<'_, T::EthSpec>, +) { + let Some(proof_engine) = execution_layer.proof_engine() else { + return; + }; + + let proof_types = execution_layer + .proof_types() + .iter() + .map(|proof_type| proof_type.to_u8()) + .collect(); + let proof_attributes = ProofAttributes { proof_types }; + let request_body = new_payload_request.as_ssz_bytes(); + let request_root = new_payload_request.request_root(); + + chain.task_executor.spawn( + async move { + if let Err(error) = proof_engine + .request_proofs_ssz(request_body, proof_attributes) + .await + { + warn!( + ?error, + ?request_root, + "Failed to request EIP-8025 execution proofs" + ); + } + }, + "eip8025_proof_request", + ); +} + /// Validate the gossip block's execution_payload according to the checks described here: /// https://github.com/ethereum/consensus-specs/blob/dev/specs/merge/p2p-interface.md#beacon_block pub fn validate_execution_payload_for_gossip( diff --git a/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs b/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs index f5ba647fce8..b75fcdac5c8 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs @@ -1,8 +1,9 @@ -use crate::fetch_blobs::{EngineGetBlobsOutput, FetchEngineBlobError}; +use crate::data_column_verification::KzgVerifiedCustodyDataColumn; +use crate::fetch_blobs::FetchEngineBlobError; use crate::observed_data_sidecars::ObservationKey; use crate::partial_data_column_assembler::PartialDataColumnAssembler; use crate::{AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes}; -use execution_layer::json_structures::{BlobAndProofV1, BlobAndProofV2, BlobAndProofV3}; +use execution_layer::json_structures::{BlobAndProofV2, BlobAndProofV3}; use kzg::Kzg; #[cfg(test)] use mockall::automock; @@ -43,22 +44,6 @@ impl FetchBlobsBeaconAdapter { .cloned() } - pub(crate) async fn get_blobs_v1( - &self, - versioned_hashes: Vec, - ) -> Result>>, FetchEngineBlobError> { - let execution_layer = self - .chain - .execution_layer - .as_ref() - .ok_or(FetchEngineBlobError::ExecutionLayerMissing)?; - - execution_layer - .get_blobs_v1(versioned_hashes) - .await - .map_err(FetchEngineBlobError::RequestFailed) - } - pub(crate) async fn get_blobs_v2( &self, versioned_hashes: Vec, @@ -91,17 +76,6 @@ impl FetchBlobsBeaconAdapter { .map_err(FetchEngineBlobError::RequestFailed) } - pub(crate) fn blobs_known_for_observation_key( - &self, - observation_key: ObservationKey, - ) -> Option> { - self.chain - .observed_blob_sidecars - .read() - .known_for_observation_key(&observation_key) - .cloned() - } - pub(crate) fn data_column_known_for_observation_key( &self, observation_key: ObservationKey, @@ -113,12 +87,6 @@ impl FetchBlobsBeaconAdapter { .cloned() } - pub(crate) fn cached_blob_indexes(&self, block_root: &Hash256) -> Option> { - self.chain - .data_availability_checker - .cached_blob_indexes(block_root) - } - pub(crate) fn cached_data_column_indexes( &self, block_root: &Hash256, @@ -131,7 +99,7 @@ impl FetchBlobsBeaconAdapter { &self, slot: Slot, block_root: Hash256, - blobs: EngineGetBlobsOutput, + blobs: Vec>, ) -> Result { self.chain .process_engine_blobs(slot, block_root, blobs) diff --git a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs index 351e35666a9..158cef00039 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs @@ -12,7 +12,6 @@ mod fetch_blobs_beacon_adapter; #[cfg(test)] mod tests; -use crate::blob_verification::{GossipBlobError, KzgVerifiedBlob}; use crate::data_column_verification::{ KzgVerifiedCustodyDataColumn, KzgVerifiedCustodyPartialDataColumn, KzgVerifiedPartialDataColumn, }; @@ -25,26 +24,15 @@ use crate::{ metrics, }; use execution_layer::Error as ExecutionLayerError; -use execution_layer::json_structures::{BlobAndProofV1, BlobAndProofV2, BlobAndProofV3}; +use execution_layer::json_structures::{BlobAndProofV2, BlobAndProofV3}; use metrics::{TryExt, inc_counter}; #[cfg(test)] use mockall_double::double; -use slot_clock::timestamp_now; use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash; use std::sync::Arc; use tracing::{debug, instrument, warn}; use types::data::{BlobSidecarError, ColumnIndex, DataColumnSidecarError, PartialDataColumnHeader}; -use types::{BeaconStateError, BlobSidecar, EthSpec, Hash256, VersionedHash}; - -/// Result from engine get blobs to be passed onto `DataAvailabilityChecker` and published to the -/// gossip network. The blobs / data columns have not been marked as observed yet, as they may not -/// be published immediately. -#[derive(Debug)] -pub enum EngineGetBlobsOutput { - Blobs(Vec>), - /// A filtered list of custody data columns to be imported into the `DataAvailabilityChecker`. - CustodyColumns(Vec>), -} +use types::{BeaconStateError, EthSpec, Hash256, VersionedHash}; #[derive(Debug)] pub enum FetchEngineBlobError { @@ -55,22 +43,21 @@ pub enum FetchEngineBlobError { DataColumnSidecarError(DataColumnSidecarError), ExecutionLayerMissing, InternalError(String), - GossipBlob(GossipBlobError), KzgError(kzg::Error), RequestFailed(ExecutionLayerError), RuntimeShutdown, TokioJoin(tokio::task::JoinError), } -/// Fetches blobs from the EL mempool and processes them. It also broadcasts unseen blobs or -/// data columns (PeerDAS onwards) to the network, using the supplied `publish_fn`. +/// Fetches blobs from the EL mempool and processes them as data columns. It also broadcasts +/// unseen data columns to the network, using the supplied `publish_fn`. #[instrument(skip_all)] pub async fn fetch_and_process_engine_blobs( chain: Arc>, block_root: Hash256, header: Arc>, custody_columns: &[ColumnIndex], - publish_fn: impl Fn(EngineGetBlobsOutput) + Send + 'static, + publish_fn: impl Fn(Vec>) + Send + 'static, ) -> Result, FetchEngineBlobError> { fetch_and_process_engine_blobs_inner( FetchBlobsBeaconAdapter::new(chain), @@ -89,7 +76,7 @@ async fn fetch_and_process_engine_blobs_inner( block_root: Hash256, header: Arc>, custody_columns: &[ColumnIndex], - publish_fn: impl Fn(EngineGetBlobsOutput) + Send + 'static, + publish_fn: impl Fn(Vec>) + Send + 'static, ) -> Result, FetchEngineBlobError> { let versioned_hashes = header .kzg_commitments @@ -120,104 +107,12 @@ async fn fetch_and_process_engine_blobs_inner( ) .await } else { - fetch_and_process_blobs_v1( - chain_adapter, - block_root, - &header, - versioned_hashes, - publish_fn, - ) - .await + Err(FetchEngineBlobError::InternalError( + "fetch blobs v1 no longer supported".to_owned(), + )) } } -#[instrument(skip_all, level = "debug")] -async fn fetch_and_process_blobs_v1( - chain_adapter: FetchBlobsBeaconAdapter, - block_root: Hash256, - header: &PartialDataColumnHeader, - versioned_hashes: Vec, - publish_fn: impl Fn(EngineGetBlobsOutput) + Send + Sized, -) -> Result, FetchEngineBlobError> { - let num_expected_blobs = versioned_hashes.len(); - metrics::observe(&metrics::BLOBS_FROM_EL_EXPECTED, num_expected_blobs as f64); - debug!(num_expected_blobs, "Fetching blobs from the EL"); - let response = chain_adapter - .get_blobs_v1(versioned_hashes) - .await - .inspect_err(|_| { - inc_counter(&metrics::BLOBS_FROM_EL_ERROR_TOTAL); - })?; - - let num_fetched_blobs = response.iter().filter(|opt| opt.is_some()).count(); - metrics::observe(&metrics::BLOBS_FROM_EL_RECEIVED, num_fetched_blobs as f64); - - if num_fetched_blobs == 0 { - debug!(num_expected_blobs, "No blobs fetched from the EL"); - inc_counter(&metrics::BLOBS_FROM_EL_MISS_TOTAL); - return Ok(None); - } else { - debug!( - num_expected_blobs, - num_fetched_blobs, "Received blobs from the EL" - ); - inc_counter(&metrics::BLOBS_FROM_EL_HIT_TOTAL); - } - - if chain_adapter.fork_choice_contains_block(&block_root) { - // Avoid computing sidecars if the block has already been imported. - debug!( - info = "block has already been imported", - "Ignoring EL blobs response" - ); - return Ok(None); - } - - let mut blob_sidecar_list = build_blob_sidecars(header, response)?; - - let observation_key = ObservationKey::new_proposer_key( - header.signed_block_header.message.proposer_index, - header.slot(), - ); - - if let Some(observed_blobs) = chain_adapter.blobs_known_for_observation_key(observation_key) { - blob_sidecar_list.retain(|blob| !observed_blobs.contains(&blob.blob_index())); - if blob_sidecar_list.is_empty() { - debug!( - info = "blobs have already been seen on gossip", - "Ignoring EL blobs response" - ); - return Ok(None); - } - } - - if let Some(known_blobs) = chain_adapter.cached_blob_indexes(&block_root) { - blob_sidecar_list.retain(|blob| !known_blobs.contains(&blob.blob_index())); - if blob_sidecar_list.is_empty() { - debug!( - info = "blobs have already been imported into data availability checker", - "Ignoring EL blobs response" - ); - return Ok(None); - } - } - - // Up until this point we have not observed the blobs in the gossip cache, which allows them to - // arrive independently while this function is running. In `publish_fn` we will observe them - // and then publish any blobs that had not already been observed. - publish_fn(EngineGetBlobsOutput::Blobs(blob_sidecar_list.clone())); - - let availability_processing_status = chain_adapter - .process_engine_blobs( - header.slot(), - block_root, - EngineGetBlobsOutput::Blobs(blob_sidecar_list), - ) - .await?; - - Ok(Some(availability_processing_status)) -} - #[instrument(skip_all, level = "debug")] async fn fetch_and_process_blobs_v2_or_v3( chain_adapter: FetchBlobsBeaconAdapter, @@ -225,7 +120,7 @@ async fn fetch_and_process_blobs_v2_or_v3( header: Arc>, versioned_hashes: Vec, custody_columns_indices: &[ColumnIndex], - publish_fn: impl Fn(EngineGetBlobsOutput) + Send + 'static, + publish_fn: impl Fn(Vec>) + Send + 'static, ) -> Result, FetchEngineBlobError> { let num_expected_blobs = versioned_hashes.len(); let slot = header.slot(); @@ -354,7 +249,7 @@ async fn fetch_and_process_blobs_v2_or_v3( // Publish complete columns if !full_columns.is_empty() { - publish_fn(EngineGetBlobsOutput::CustodyColumns(full_columns.clone())); + publish_fn(full_columns.clone()); } // We publish all partials at the calling site, regardless of result, as previous publishs // have been blocked, waiting for the results of this call @@ -362,11 +257,7 @@ async fn fetch_and_process_blobs_v2_or_v3( // Process complete columns through DA checker let availability_processing_status = if !full_columns.is_empty() { chain_adapter - .process_engine_blobs( - slot, - block_root, - EngineGetBlobsOutput::CustodyColumns(full_columns), - ) + .process_engine_blobs(slot, block_root, full_columns) .await? } else { // No complete columns yet, still missing components @@ -461,30 +352,3 @@ async fn compute_custody_columns_to_import( .await .map_err(FetchEngineBlobError::TokioJoin)? } - -fn build_blob_sidecars( - header: &PartialDataColumnHeader, - response: Vec>>, -) -> Result>, FetchEngineBlobError> { - let mut sidecars = vec![]; - for (index, blob_and_proof) in response - .into_iter() - .enumerate() - .filter_map(|(index, opt_blob)| Some((index, opt_blob?))) - { - let blob_sidecar = BlobSidecar::new_with_existing_proof( - index, - blob_and_proof.blob, - header.clone(), - blob_and_proof.proof, - ) - .map_err(FetchEngineBlobError::BlobSidecarError)?; - - sidecars.push(KzgVerifiedBlob::from_execution_verified( - Arc::new(blob_sidecar), - timestamp_now(), - )); - } - - Ok(sidecars) -} diff --git a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs index 37d40f3a270..99cb4b5a78c 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs @@ -1,8 +1,7 @@ use crate::AvailabilityProcessingStatus; +use crate::data_column_verification::KzgVerifiedCustodyDataColumn; use crate::fetch_blobs::fetch_blobs_beacon_adapter::MockFetchBlobsBeaconAdapter; -use crate::fetch_blobs::{ - EngineGetBlobsOutput, FetchEngineBlobError, fetch_and_process_engine_blobs_inner, -}; +use crate::fetch_blobs::{FetchEngineBlobError, fetch_and_process_engine_blobs_inner}; use crate::partial_data_column_assembler::PartialDataColumnAssembler; use crate::test_utils::{EphemeralHarnessType, get_kzg}; use bls::Signature; @@ -226,7 +225,7 @@ mod get_blobs_v2 { assert!( matches!( published_columns, - EngineGetBlobsOutput::CustodyColumns(columns) if columns.len() == custody_columns.len() + columns if columns.len() == custody_columns.len() ), "should publish custody columns" ); @@ -251,284 +250,10 @@ mod get_blobs_v2 { } } -mod get_blobs_v1 { - use super::*; - use crate::block_verification_types::AsBlock; - use std::collections::HashSet; - use types::{ColumnIndex, FullPayload, PartialDataColumnHeader}; - - const ELECTRA_FORK: ForkName = ForkName::Electra; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_fetch_blobs_v1_no_blobs_in_block() { - let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false); - let spec = mock_adapter.spec(); - let (publish_fn, _s) = mock_publish_fn(); - let block_no_blobs = SignedBeaconBlock::>::from_block( - BeaconBlock::empty(spec), - Signature::empty(), - ); - let block_root = block_no_blobs.canonical_root(); - - // Expectations: engine fetch blobs should not be triggered - mock_adapter.expect_get_blobs_v1().times(0); - - // WHEN: Trigger fetch blobs on the block - let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; - let processing_status = fetch_and_process_engine_blobs_inner( - mock_adapter, - block_root, - Arc::new(PartialDataColumnHeader::try_from(&block_no_blobs).unwrap()), - &custody_columns, - publish_fn, - ) - .await - .expect("fetch blobs should succeed"); - - // THEN: No blob is processed - assert_eq!(processing_status, None); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_fetch_blobs_v1_no_blobs_returned() { - let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false); - let (publish_fn, _) = mock_publish_fn(); - let (block, _blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2); - let block_root = block.canonical_root(); - - // GIVEN: No blobs in EL response - let expected_blob_count = block.message().body().blob_kzg_commitments().unwrap().len(); - mock_get_blobs_v1_response(&mut mock_adapter, vec![None; expected_blob_count]); - - // WHEN: Trigger fetch blobs on the block - let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; - let processing_status = fetch_and_process_engine_blobs_inner( - mock_adapter, - block_root, - Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()), - &custody_columns, - publish_fn, - ) - .await - .expect("fetch blobs should succeed"); - - // THEN: No blob is processed - assert_eq!(processing_status, None); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_fetch_blobs_v1_partial_blobs_returned() { - let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false); - let (publish_fn, publish_fn_args) = mock_publish_fn(); - let blob_count = 2; - let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, blob_count); - let block_slot = block.slot(); - let block_root = block.canonical_root(); - - // GIVEN: Missing a blob in EL response (remove 1 blob from response) - let mut blob_and_proof_opts = blobs_and_proofs.into_iter().map(Some).collect::>(); - blob_and_proof_opts.first_mut().unwrap().take(); - mock_get_blobs_v1_response(&mut mock_adapter, blob_and_proof_opts); - // AND block is not imported into fork choice - mock_fork_choice_contains_block(&mut mock_adapter, vec![]); - // AND all blobs have not yet been seen - mock_adapter - .expect_cached_blob_indexes() - .returning(|_| None); - mock_adapter - .expect_blobs_known_for_observation_key() - .returning(|_| None); - // Returned blobs should be processed - mock_process_engine_blobs_result( - &mut mock_adapter, - Ok(AvailabilityProcessingStatus::MissingComponents( - block_slot, block_root, - )), - ); - - // WHEN: Trigger fetch blobs on the block - let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; - let processing_status = fetch_and_process_engine_blobs_inner( - mock_adapter, - block_root, - Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()), - &custody_columns, - publish_fn, - ) - .await - .expect("fetch blobs should succeed"); - - // THEN: Returned blobs are processed and published - assert_eq!( - processing_status, - Some(AvailabilityProcessingStatus::MissingComponents( - block_slot, block_root, - )) - ); - assert!( - matches!( - extract_published_blobs(publish_fn_args), - EngineGetBlobsOutput::Blobs(blobs) if blobs.len() == blob_count - 1 - ), - "partial blob results should still be published" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_fetch_blobs_v1_block_imported_after_el_response() { - let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false); - let (publish_fn, publish_fn_args) = mock_publish_fn(); - let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2); - let block_root = block.canonical_root(); - - // GIVEN: All blobs returned, but fork choice already imported the block - let blob_and_proof_opts = blobs_and_proofs.into_iter().map(Some).collect::>(); - mock_get_blobs_v1_response(&mut mock_adapter, blob_and_proof_opts); - mock_fork_choice_contains_block(&mut mock_adapter, vec![block.canonical_root()]); - - // WHEN: Trigger fetch blobs on the block - let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; - let processing_status = fetch_and_process_engine_blobs_inner( - mock_adapter, - block_root, - Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()), - &custody_columns, - publish_fn, - ) - .await - .expect("fetch blobs should succeed"); - - // THEN: Returned blobs should NOT be processed or published. - assert_eq!(processing_status, None); - assert_eq!( - publish_fn_args.lock().unwrap().len(), - 0, - "no blobs should be published" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_fetch_blobs_v1_no_new_blobs_to_import() { - let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false); - let (publish_fn, publish_fn_args) = mock_publish_fn(); - let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2); - let block_root = block.canonical_root(); - - // **GIVEN**: - // All blobs returned - let blob_and_proof_opts = blobs_and_proofs.into_iter().map(Some).collect::>(); - let all_blob_indices = blob_and_proof_opts - .iter() - .enumerate() - .map(|(i, _)| i as u64) - .collect::>(); - - mock_get_blobs_v1_response(&mut mock_adapter, blob_and_proof_opts); - // block not yet imported into fork choice - mock_fork_choice_contains_block(&mut mock_adapter, vec![]); - // All blobs already seen on gossip - mock_adapter - .expect_cached_blob_indexes() - .returning(|_| None); - mock_adapter - .expect_blobs_known_for_observation_key() - .returning(move |_| Some(all_blob_indices.clone())); - - // **WHEN**: Trigger `fetch_blobs` on the block - let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; - let processing_status = fetch_and_process_engine_blobs_inner( - mock_adapter, - block_root, - Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()), - &custody_columns, - publish_fn, - ) - .await - .expect("fetch blobs should succeed"); - - // **THEN**: Should NOT be processed and no blobs should be published. - assert_eq!(processing_status, None); - assert_eq!( - publish_fn_args.lock().unwrap().len(), - 0, - "no blobs should be published" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_fetch_blobs_v1_success() { - let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false); - let (publish_fn, publish_fn_args) = mock_publish_fn(); - let blob_count = 2; - let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, blob_count); - let block_root = block.canonical_root(); - - // All blobs returned, fork choice doesn't contain block - let blob_and_proof_opts = blobs_and_proofs.into_iter().map(Some).collect::>(); - mock_get_blobs_v1_response(&mut mock_adapter, blob_and_proof_opts); - mock_fork_choice_contains_block(&mut mock_adapter, vec![]); - mock_adapter - .expect_cached_blob_indexes() - .returning(|_| None); - mock_adapter - .expect_blobs_known_for_observation_key() - .returning(|_| None); - mock_process_engine_blobs_result( - &mut mock_adapter, - Ok(AvailabilityProcessingStatus::Imported(block_root)), - ); - - // Trigger fetch blobs on the block - let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; - let processing_status = fetch_and_process_engine_blobs_inner( - mock_adapter, - block_root, - Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()), - &custody_columns, - publish_fn, - ) - .await - .expect("fetch blobs should succeed"); - - // THEN all fetched blobs are processed and published - assert_eq!( - processing_status, - Some(AvailabilityProcessingStatus::Imported(block_root)) - ); - - let published_blobs = extract_published_blobs(publish_fn_args); - assert!( - matches!( - published_blobs, - EngineGetBlobsOutput::Blobs(blobs) if blobs.len() == blob_count - ), - "should publish fetched blobs" - ); - } - - fn mock_get_blobs_v1_response( - mock_adapter: &mut MockFetchBlobsBeaconAdapter, - blobs_and_proofs_opt: Vec>>, - ) { - let blobs_and_proofs_v1 = blobs_and_proofs_opt - .into_iter() - .map(|blob_and_proof_opt| { - blob_and_proof_opt.map(|blob_and_proof| match blob_and_proof { - BlobAndProof::V1(inner) => inner, - _ => panic!("BlobAndProofV1 not expected"), - }) - }) - .collect(); - mock_adapter - .expect_get_blobs_v1() - .return_once(move |_| Ok(blobs_and_proofs_v1)); - } -} - -/// Extract the `EngineGetBlobsOutput` passed to the `publish_fn`. +/// Extract the `Vec>` passed to the `publish_fn`. fn extract_published_blobs( - publish_fn_args: Arc>>>, -) -> EngineGetBlobsOutput { + publish_fn_args: Arc>>>>, +) -> Vec> { let mut calls = publish_fn_args.lock().unwrap(); assert_eq!(calls.len(), 1); calls.pop().unwrap() @@ -597,8 +322,8 @@ fn create_test_block_and_blobs( #[allow(clippy::type_complexity)] fn mock_publish_fn() -> ( - impl Fn(EngineGetBlobsOutput) + Send + 'static, - Arc>>>, + impl Fn(Vec>) + Send + 'static, + Arc>>>>, ) { // Keep track of the arguments captured by `publish_fn`. let captured_args = Arc::new(Mutex::new(vec![])); diff --git a/beacon_node/beacon_chain/src/internal_events.rs b/beacon_node/beacon_chain/src/internal_events.rs new file mode 100644 index 00000000000..359eff845bd --- /dev/null +++ b/beacon_node/beacon_chain/src/internal_events.rs @@ -0,0 +1,29 @@ +//! Internal event bus for execution-proof integration tests. + +use std::sync::Arc; +use types::execution::eip8025::{ProofByRootIdentifier, ProofStatus, SignedExecutionProof}; +use types::{Hash256, Slot}; + +pub const INTERNAL_EVENT_CHANNEL_CAPACITY: usize = 16_384; + +#[derive(Debug, Clone)] +pub enum InternalBeaconNodeEvent { + GossipExecutionProof(Arc), + RpcExecutionProof(Arc), + OutboundExecutionProofsByRange { + start_slot: Slot, + count: u64, + }, + OutboundExecutionProofsByRoot { + identifiers: Vec, + }, + ExecutionProofVerified { + request_root: Hash256, + status: ProofStatus, + block: Option<(Hash256, Slot)>, + }, + ExecutionProofVerificationFailed { + request_root: Hash256, + error: String, + }, +} diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 804268a6139..86384f429bc 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -20,6 +20,7 @@ pub mod custody_context; pub mod data_availability_checker; pub mod data_column_verification; mod early_attester_cache; +pub mod eip8025; pub mod envelope_times_cache; mod errors; pub mod events; @@ -29,6 +30,7 @@ pub mod fork_choice_signal; pub mod graffiti_calculator; pub mod historical_blocks; pub mod historical_data_columns; +pub mod internal_events; pub mod invariants; pub mod kzg_utils; pub mod light_client_finality_update_verification; @@ -41,6 +43,7 @@ pub mod observed_aggregates; mod observed_attesters; pub mod observed_block_producers; pub mod observed_data_sidecars; +pub mod observed_execution_proofs; pub mod observed_operations; mod observed_slashable; pub mod partial_data_column_assembler; @@ -85,7 +88,7 @@ pub use beacon_fork_choice_store::{ }; pub use block_verification::{ BlockError, ExecutionPayloadError, ExecutionPendingBlock, GossipVerifiedBlock, - IntoExecutionPendingBlock, IntoGossipVerifiedBlock, InvalidSignature, + IntoExecutionPendingBlock, IntoGossipVerifiedBlock, InvalidSignature, ParentImportStatus, PayloadVerificationOutcome, PayloadVerificationStatus, build_blob_data_column_sidecars, get_block_root, signature_verify_chain_segment, }; diff --git a/beacon_node/beacon_chain/src/observed_execution_proofs.rs b/beacon_node/beacon_chain/src/observed_execution_proofs.rs new file mode 100644 index 00000000000..1484cd15db7 --- /dev/null +++ b/beacon_node/beacon_chain/src/observed_execution_proofs.rs @@ -0,0 +1,284 @@ +//! Deduplication cache for execution proofs received via gossip. +//! +//! Implements gossip IGNORE rules from the EIP-8025 p2p-interface spec: +//! - IGNORE-2: No valid proof already received for `(request_root, proof_type)` +//! - IGNORE-3: First proof from validator for `(request_root, proof_type, validator_index)` +//! +//! Request-root scoped entries are evicted at finalization: proofs for finalized blocks are +//! irrelevant. Invalid proof-data entries are process-local and retained until restart. + +use std::collections::{HashMap, HashSet}; +use tree_hash::TreeHash; +use types::execution::eip8025::ProofData; +use types::{Hash256, ProofType, Slot}; + +/// Gossip deduplication cache for execution proofs. +/// +/// Checked *before* BLS/proof-engine verification to avoid redundant work. +#[derive(Debug, Default)] +pub struct ObservedExecutionProofs { + /// Tracks `(request_root, proof_type)` pairs for which we already have a *valid* proof. + /// Used to implement IGNORE-2. + valid_proofs: HashMap<(Hash256, ProofType), ()>, + + /// Tracks `(request_root, proof_type, validator_index)` triples we have already attempted + /// to verify (regardless of outcome). Used to implement IGNORE-3. + seen_from_validator: HashSet<(Hash256, ProofType, u64)>, + + /// Tracks `(proof_type, hash_tree_root(proof_data))` pairs for proofs already rejected by the + /// proof engine. + invalid_proofs: HashSet<(ProofType, Hash256)>, + + /// Maps slot → set of request roots observed at that slot. Populated when a valid/accepted + /// proof is observed. Used to prune `valid_proofs` and `seen_from_validator` at finalization. + slot_to_request_roots: HashMap>, +} + +/// Result of checking the dedup cache. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProofObservation { + /// We already rejected this `(proof_type, proof_data)` pair. + AlreadyRejectedProof, + /// We already have a valid proof for this `(request_root, proof_type)` — IGNORE-2. + AlreadyHaveValidProof, + /// We already saw a proof from this validator for this `(request_root, proof_type)` — IGNORE-3. + DuplicateFromValidator, + /// First time seeing this proof — proceed with verification. + New, +} + +impl ObservedExecutionProofs { + /// Check whether a proof should be processed or ignored based on the dedup rules. + /// + /// This does *not* insert the proof into the cache; call [`observe_verification_attempt`] + /// and then [`observe_invalid_proof`] or [`observe_valid_proof`] after verification completes. + pub fn check( + &self, + request_root: Hash256, + proof_type: ProofType, + proof_data: &ProofData, + validator_index: u64, + ) -> ProofObservation { + // IGNORE-2: already have a valid proof for this (root, type) + if self.valid_proofs.contains_key(&(request_root, proof_type)) { + return ProofObservation::AlreadyHaveValidProof; + } + + // IGNORE-3: already saw a proof from this validator for this (root, type) + if self + .seen_from_validator + .contains(&(request_root, proof_type, validator_index)) + { + return ProofObservation::DuplicateFromValidator; + } + + if self + .invalid_proofs + .contains(&(proof_type, proof_data.tree_hash_root())) + { + return ProofObservation::AlreadyRejectedProof; + } + + ProofObservation::New + } + + /// Record that we attempted to verify a proof from this validator. + /// Must be called for every verification attempt, regardless of outcome. + pub fn observe_verification_attempt( + &mut self, + request_root: Hash256, + proof_type: ProofType, + validator_index: u64, + ) { + self.seen_from_validator + .insert((request_root, proof_type, validator_index)); + } + + /// Record that the proof engine rejected this `(proof_type, proof_data)` pair. + /// Returns `true` if this is the first rejection recorded for the pair. + pub fn observe_invalid_proof(&mut self, proof_type: ProofType, proof_data: &ProofData) -> bool { + self.invalid_proofs + .insert((proof_type, proof_data.tree_hash_root())) + } + + /// Record that a valid proof was received for `(request_root, proof_type)` at `slot`. + pub fn observe_valid_proof( + &mut self, + request_root: Hash256, + proof_type: ProofType, + slot: Slot, + ) { + self.valid_proofs.insert((request_root, proof_type), ()); + self.slot_to_request_roots + .entry(slot) + .or_default() + .insert(request_root); + } + + /// Prune entries for request roots whose slot is at or below `finalized_slot`. + /// + /// Call at finalization. Any proof for a finalized block will never need dedup again. + /// Entries in `seen_from_validator` without a known slot (e.g. for proofs that failed + /// BLS or engine verification) are retained until restart. + pub fn prune(&mut self, finalized_slot: Slot) { + let pruned_roots: HashSet = self + .slot_to_request_roots + .extract_if(|&slot, _| slot <= finalized_slot) + .flat_map(|(_, roots)| roots) + .collect(); + self.valid_proofs + .retain(|(root, _), _| !pruned_roots.contains(root)); + self.seen_from_validator + .retain(|(root, _, _)| !pruned_roots.contains(root)); + } + + /// Number of valid-proof entries (for metrics / tests). + pub fn valid_proof_count(&self) -> usize { + self.valid_proofs.len() + } + + /// Number of seen-from-validator entries (for metrics / tests). + pub fn seen_from_validator_count(&self) -> usize { + self.seen_from_validator.len() + } + + /// Number of invalid proof-data entries (for metrics / tests). + pub fn invalid_proof_count(&self) -> usize { + self.invalid_proofs.len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_proof_data(bytes: &[u8]) -> ProofData { + ProofData::new(bytes.to_vec()).expect("proof data should fit") + } + + #[test] + fn new_proof_is_observed() { + let cache = ObservedExecutionProofs::default(); + let root = Hash256::repeat_byte(0x01); + let proof_data = make_proof_data(&[1, 2, 3]); + assert_eq!(cache.check(root, 1, &proof_data, 42), ProofObservation::New); + } + + #[test] + fn ignore_2_valid_proof_dedup() { + let mut cache = ObservedExecutionProofs::default(); + let root = Hash256::repeat_byte(0x01); + let proof_data = make_proof_data(&[1, 2, 3]); + + cache.observe_valid_proof(root, 1, Slot::new(1)); + + // Same (root, type) from a different validator → still IGNORE + assert_eq!( + cache.check(root, 1, &proof_data, 99), + ProofObservation::AlreadyHaveValidProof + ); + + // Different type → New + assert_eq!(cache.check(root, 2, &proof_data, 99), ProofObservation::New); + } + + #[test] + fn invalid_proof_data_dedup_uses_type_and_data_root() { + let mut cache = ObservedExecutionProofs::default(); + let root = Hash256::repeat_byte(0x01); + let other_root = Hash256::repeat_byte(0x02); + let proof_data = make_proof_data(&[1, 2, 3]); + let other_proof_data = make_proof_data(&[4, 5, 6]); + + assert!(cache.observe_invalid_proof(1, &proof_data)); + assert!(!cache.observe_invalid_proof(1, &proof_data)); + + assert_eq!( + cache.check(other_root, 1, &proof_data, 99), + ProofObservation::AlreadyRejectedProof + ); + + // Same proof data with a different type is a distinct cache key. + assert_eq!(cache.check(root, 2, &proof_data, 42), ProofObservation::New); + + // Same type with different proof data is a distinct cache key. + assert_eq!( + cache.check(root, 1, &other_proof_data, 42), + ProofObservation::New + ); + + assert_eq!(cache.invalid_proof_count(), 1); + } + + #[test] + fn cheap_dedup_checks_precede_invalid_proof_data_rooting() { + let mut cache = ObservedExecutionProofs::default(); + let root = Hash256::repeat_byte(0x01); + let proof_data = make_proof_data(&[1, 2, 3]); + + cache.observe_valid_proof(root, 1, Slot::new(1)); + cache.observe_invalid_proof(1, &proof_data); + + assert_eq!( + cache.check(root, 1, &proof_data, 42), + ProofObservation::AlreadyHaveValidProof + ); + + let other_root = Hash256::repeat_byte(0x02); + cache.observe_verification_attempt(other_root, 1, 42); + + assert_eq!( + cache.check(other_root, 1, &proof_data, 42), + ProofObservation::DuplicateFromValidator + ); + } + + #[test] + fn ignore_3_validator_dedup() { + let mut cache = ObservedExecutionProofs::default(); + let root = Hash256::repeat_byte(0x01); + let proof_data = make_proof_data(&[1, 2, 3]); + + cache.observe_verification_attempt(root, 1, 42); + + assert_eq!( + cache.check(root, 1, &proof_data, 42), + ProofObservation::DuplicateFromValidator + ); + + // Same validator, different type → New + assert_eq!(cache.check(root, 2, &proof_data, 42), ProofObservation::New); + + // Different validator, same type → New + assert_eq!(cache.check(root, 1, &proof_data, 43), ProofObservation::New); + } + + #[test] + fn prune_removes_finalized_roots() { + let mut cache = ObservedExecutionProofs::default(); + let root_a = Hash256::repeat_byte(0x01); + let root_b = Hash256::repeat_byte(0x02); + let proof_data = make_proof_data(&[1, 2, 3]); + + // root_a at slot 10 (will be finalized), root_b at slot 20 (will be retained). + cache.observe_valid_proof(root_a, 1, Slot::new(10)); + cache.observe_valid_proof(root_b, 1, Slot::new(20)); + cache.observe_verification_attempt(root_a, 1, 42); + cache.observe_verification_attempt(root_b, 1, 43); + + cache.prune(Slot::new(15)); + + assert_eq!(cache.valid_proof_count(), 1); + assert_eq!(cache.seen_from_validator_count(), 1); + // root_b still tracked + assert_eq!( + cache.check(root_b, 1, &proof_data, 99), + ProofObservation::AlreadyHaveValidProof + ); + // root_a gone → New + assert_eq!( + cache.check(root_a, 1, &proof_data, 42), + ProofObservation::New + ); + } +} diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs index 73ddb43273f..8a1b6cadf56 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -318,6 +318,14 @@ impl BeaconChain { // This prevents inconsistency between the two at the expense of concurrency. drop(fork_choice); + if let Err(error) = self.register_execution_payload_request_root(block_root) { + warn!( + ?error, + ?block_root, + "Unable to cache EIP-8025 execution proof request root" + ); + } + // We're declaring the envelope "imported" at this point, since fork choice and the DB know // about it. let envelope_time_imported = self.slot_clock.now_duration().unwrap_or(Duration::MAX); diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs index 0bbe32525aa..6f08e0c4503 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs @@ -2,8 +2,10 @@ use std::sync::Arc; use execution_layer::{NewPayloadRequest, NewPayloadRequestGloas}; use fork_choice::PayloadVerificationStatus; +use ssz::Encode; use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash; use tracing::warn; +use types::ProofAttributes; use types::{SignedBeaconBlock, SignedExecutionPayloadEnvelope}; use crate::{ @@ -64,10 +66,45 @@ impl PayloadNotifier { } else { let parent_root = self.block.message().parent_root(); let request = Self::build_new_payload_request(&self.envelope, &self.block)?; + self.request_execution_proofs(&request); notify_new_payload(&self.chain, self.envelope.slot(), parent_root, request).await } } + fn request_execution_proofs(&self, request: &NewPayloadRequest<'_, T::EthSpec>) { + let Some(execution_layer) = self.chain.execution_layer.as_ref() else { + return; + }; + let Some(proof_engine) = execution_layer.proof_engine() else { + return; + }; + + let proof_types = execution_layer + .proof_types() + .iter() + .map(|proof_type| proof_type.to_u8()) + .collect(); + let proof_attributes = ProofAttributes { proof_types }; + let request_body = request.as_ssz_bytes(); + let request_root = request.request_root(); + + self.chain.task_executor.spawn( + async move { + if let Err(error) = proof_engine + .request_proofs_ssz(request_body, proof_attributes) + .await + { + warn!( + ?error, + ?request_root, + "Failed to request EIP-8025 execution proofs" + ); + } + }, + "eip8025_proof_request", + ); + } + fn build_new_payload_request<'a>( envelope: &'a SignedExecutionPayloadEnvelope, block: &'a SignedBeaconBlock, @@ -83,7 +120,11 @@ impl PayloadNotifier { .blob_kzg_commitments .iter() .map(kzg_commitment_to_versioned_hash) - .collect(); + .collect::>() + .try_into() + .map_err(|e| { + BlockError::BeaconChainError(Box::new(crate::BeaconChainError::SszTypesError(e))) + })?; Ok(NewPayloadRequest::Gloas(NewPayloadRequestGloas { execution_payload: &envelope.message.payload, diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index c2ccad7d8c5..53effddddac 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -1,4 +1,3 @@ -use crate::blob_verification::GossipVerifiedBlob; use crate::block_verification_types::{AsBlock, AvailableBlockData, LookupBlock, RangeSyncBlock}; use crate::custody_context::NodeCustodyType; use crate::data_availability_checker::DataAvailabilityChecker; @@ -1569,6 +1568,7 @@ where beacon_block_root: Hash256, mut state: Cow>, state_root: Hash256, + payload_present_override: Option, ) -> Result, BeaconChainError> { assert_eq!( state.get_latest_block_root(state_root), @@ -1603,12 +1603,17 @@ where *state.get_block_root(target_slot)? }; - let payload_present = state.fork_name_unchecked().gloas_enabled() - && state.latest_block_header().slot != slot - && self - .chain - .canonical_head - .block_has_canonical_payload(&beacon_block_root, &self.spec)?; + let payload_present = match payload_present_override { + Some(payload_present) => payload_present, + None => { + state.fork_name_unchecked().gloas_enabled() + && state.latest_block_header().slot != slot + && self + .chain + .canonical_head + .block_has_canonical_payload(&beacon_block_root, &self.spec)? + } + }; Ok(Attestation::empty_for_signing( index, @@ -1647,7 +1652,11 @@ where state_root, head_block_root, attestation_slot, - MakeAttestationOptions { limit: None, fork }, + MakeAttestationOptions { + limit: None, + fork, + payload_present_override: None, + }, ) .0 } @@ -1674,7 +1683,11 @@ where state_root, head_block_root, attestation_slot, - MakeAttestationOptions { limit: None, fork }, + MakeAttestationOptions { + limit: None, + fork, + payload_present_override: None, + }, ) .0 } @@ -1688,7 +1701,7 @@ where attestation_slot: Slot, opts: MakeAttestationOptions, ) -> (Vec, Vec) { - let MakeAttestationOptions { limit, fork } = opts; + let MakeAttestationOptions { limit, fork, .. } = opts; let committee_count = state.get_committee_count_at_slot(state.slot()).unwrap(); let num_attesters = AtomicUsize::new(0); @@ -1781,7 +1794,11 @@ where attestation_slot: Slot, opts: MakeAttestationOptions, ) -> (Vec>, Vec) { - let MakeAttestationOptions { limit, fork } = opts; + let MakeAttestationOptions { + limit, + fork, + payload_present_override, + } = opts; let committee_count = state.get_committee_count_at_slot(state.slot()).unwrap(); let num_attesters = AtomicUsize::new(0); @@ -1814,6 +1831,7 @@ where head_block_root.into(), Cow::Borrowed(state), state_root, + payload_present_override, ) .unwrap(); @@ -2016,7 +2034,11 @@ where state_root, block_hash, slot, - MakeAttestationOptions { limit, fork }, + MakeAttestationOptions { + limit, + fork, + payload_present_override: None, + }, ) } @@ -2834,7 +2856,9 @@ where .blob_kzg_commitments .iter() .map(kzg_commitment_to_versioned_hash) - .collect(); + .collect::>() + .try_into() + .expect("versioned hashes should fit"); let request = NewPayloadRequest::Gloas(NewPayloadRequestGloas { execution_payload: &signed_envelope.message.payload, @@ -3696,55 +3720,39 @@ where Ok(()) } - /// Simulate some of the blobs / data columns being seen on gossip. - /// Converts the blobs to data columns if the slot is Fulu or later. - pub async fn process_gossip_blobs_or_columns<'a>( + /// Simulate the block's custody data columns (or those in `custody_columns_opt`) being + /// seen on gossip. Panics unless PeerDAS is enabled for the block's epoch. + pub async fn process_gossip_columns( &self, block: &SignedBeaconBlock, - blobs: impl Iterator>, - proofs: impl Iterator, custody_columns_opt: Option>, ) { - let is_peerdas_enabled = self.chain.spec.is_peer_das_enabled_for_epoch(block.epoch()); - if is_peerdas_enabled { - let custody_columns = custody_columns_opt.unwrap_or_else(|| { - let epoch = block.slot().epoch(E::slots_per_epoch()); + assert!(self.chain.spec.is_peer_das_enabled_for_epoch(block.epoch())); + let custody_columns = custody_columns_opt.unwrap_or_else(|| { + let epoch = block.slot().epoch(E::slots_per_epoch()); + self.chain + .sampling_columns_for_epoch(epoch) + .iter() + .copied() + .collect() + }); + + let verified_columns = generate_data_column_sidecars_from_block(block, &self.spec) + .into_iter() + .filter(|c| custody_columns.contains(c.index())) + .map(|sidecar| { + let subnet_id = DataColumnSubnetId::from_column_index(*sidecar.index(), &self.spec); self.chain - .sampling_columns_for_epoch(epoch) - .iter() - .copied() - .collect() - }); + .verify_data_column_sidecar_for_gossip(sidecar, subnet_id) + }) + .collect::, _>>() + .unwrap(); - let verified_columns = generate_data_column_sidecars_from_block(block, &self.spec) - .into_iter() - .filter(|c| custody_columns.contains(c.index())) - .map(|sidecar| { - let subnet_id = - DataColumnSubnetId::from_column_index(*sidecar.index(), &self.spec); - self.chain - .verify_data_column_sidecar_for_gossip(sidecar, subnet_id) - }) - .collect::, _>>() + if !verified_columns.is_empty() { + self.chain + .process_gossip_data_columns(verified_columns, || Ok(())) + .await .unwrap(); - - if !verified_columns.is_empty() { - self.chain - .process_gossip_data_columns(verified_columns, || Ok(())) - .await - .unwrap(); - } - } else { - for (i, (kzg_proof, blob)) in proofs.into_iter().zip(blobs).enumerate() { - let sidecar = - Arc::new(BlobSidecar::new(i, blob.clone(), block, *kzg_proof).unwrap()); - let gossip_blob = GossipVerifiedBlob::new(sidecar, i as u64, &self.chain) - .expect("should obtain gossip verified blob"); - self.chain - .process_gossip_blob(gossip_blob) - .await - .expect("should import valid gossip verified blob"); - } } } } @@ -3761,6 +3769,8 @@ pub struct MakeAttestationOptions { pub limit: Option, /// Fork to use for signing attestations. pub fork: Fork, + /// Override post-Gloas regular attestation payload-present encoding. + pub payload_present_override: Option, } pub enum NumBlobs { diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index 1b87fc041a2..862c2a9fe88 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -8,6 +8,7 @@ use beacon_chain::test_utils::{ use beacon_chain::validator_monitor::UNAGGREGATED_ATTESTATION_LAG_SLOTS; use beacon_chain::{StateSkipConfig, WhenSlotSkipped, metrics}; use bls::{AggregateSignature, Keypair}; +use slot_clock::SlotClock; use std::sync::{Arc, LazyLock}; use tree_hash::TreeHash; use types::{Attestation, EthSpec, MainnetEthSpec, RelativeEpoch, Slot}; @@ -448,3 +449,69 @@ async fn gloas_attestation_index_payload_absent() { "gloas attestation to prior slot without payload should have index=0 (payload_absent)" ); } + +/// Verify that `produce_payload_attestation_data` reports `payload_present = true` but +/// `blob_data_available = false` when the envelope was observed on but not imported +/// because its data was unavailable. +/// +/// Setup: build a chain through slot 2, then at slot 3 import only the beacon block (no +/// envelope) and mark the envelope as observed on time. +#[tokio::test] +async fn gloas_payload_attestation_seen_but_data_unavailable() { + if fork_name_from_env().is_some_and(|f| !f.gloas_enabled()) { + return; + } + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .default_spec() + .keypairs(KEYPAIRS[..].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + let chain = &harness.chain; + + harness.advance_slot(); + harness + .extend_chain( + 2, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Slot 3: import the beacon block but withhold its envelope. + harness.advance_slot(); + let state = harness.get_current_state(); + let (block_contents, _envelope, _new_state) = + harness.make_block_with_envelope(state, Slot::new(3)).await; + let block_root = block_contents.0.canonical_root(); + harness + .process_block(Slot::new(3), block_root, block_contents) + .await + .expect("block should import without envelope"); + + assert_eq!(chain.head_snapshot().beacon_block.slot(), Slot::new(3)); + + // Mark the envelope as observed at the start of the slot, before its deadline. + let slot_start = chain.slot_clock.start_of(Slot::new(3)).unwrap(); + chain.envelope_times_cache.write().set_time_observed( + block_root, + Slot::new(3), + slot_start, + None, + ); + + let pa_data = chain + .produce_payload_attestation_data(Slot::new(3)) + .expect("should produce payload attestation data"); + + assert!( + pa_data.payload_present, + "envelope observed before the deadline should vote payload_present=true" + ); + assert!( + !pa_data.blob_data_available, + "unimported envelope data should vote blob_data_available=false" + ); +} diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs index da7f380e361..03b8ae58ac0 100644 --- a/beacon_node/beacon_chain/tests/attestation_verification.rs +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -1636,6 +1636,7 @@ async fn attestation_verification_use_head_state_fork() { MakeAttestationOptions { fork: capella_fork, limit: None, + payload_present_override: None, }, ) .0 @@ -1667,6 +1668,7 @@ async fn attestation_verification_use_head_state_fork() { MakeAttestationOptions { fork: bellatrix_fork, limit: None, + payload_present_override: None, }, ) .0 @@ -1741,6 +1743,7 @@ async fn aggregated_attestation_verification_use_head_state_fork() { MakeAttestationOptions { fork: capella_fork, limit: None, + payload_present_override: None, }, ) .0 @@ -1768,6 +1771,7 @@ async fn aggregated_attestation_verification_use_head_state_fork() { MakeAttestationOptions { fork: bellatrix_fork, limit: None, + payload_present_override: None, }, ) .0 diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 533ef612197..deadafac367 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -8,7 +8,8 @@ use beacon_chain::{ WhenSlotSkipped, custody_context::NodeCustodyType, test_utils::{ - AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, test_spec, + AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, + MakeAttestationOptions, fork_name_from_env, test_spec, }, }; use beacon_chain::{ @@ -17,6 +18,7 @@ use beacon_chain::{ }; use bls::{AggregateSignature, Keypair, Signature}; use fixed_bytes::FixedBytesExtended; +use fork_choice::PayloadStatus; use logging::create_test_tracing_subscriber; use slasher::{Config as SlasherConfig, Slasher}; use state_processing::{ @@ -357,6 +359,10 @@ fn update_data_column_signed_header( #[tokio::test] async fn chain_segment_full_segment() { + // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. + if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); let (chain_segment, chain_segment_blobs) = get_chain_segment().await; store_envelopes_for_chain_segment(&chain_segment, &harness); @@ -397,6 +403,10 @@ async fn chain_segment_full_segment() { #[tokio::test] async fn chain_segment_varying_chunk_size() { + // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. + if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let (chain_segment, chain_segment_blobs) = get_chain_segment().await; let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); let blocks: Vec> = @@ -677,6 +687,10 @@ async fn get_invalid_sigs_harness( } #[tokio::test] async fn invalid_signature_gossip_block() { + // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. + if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let (chain_segment, chain_segment_blobs) = get_chain_segment().await; for &block_index in BLOCK_INDICES { // Ensure the block will be rejected if imported on its own (without gossip checking). @@ -733,6 +747,10 @@ async fn invalid_signature_gossip_block() { #[tokio::test] async fn invalid_signature_block_proposal() { + // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. + if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let (chain_segment, chain_segment_blobs) = get_chain_segment().await; for &block_index in BLOCK_INDICES { let harness = get_invalid_sigs_harness(&chain_segment).await; @@ -772,6 +790,10 @@ async fn invalid_signature_block_proposal() { #[tokio::test] async fn invalid_signature_randao_reveal() { + // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. + if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; for &block_index in BLOCK_INDICES { let harness = get_invalid_sigs_harness(&chain_segment).await; @@ -800,6 +822,10 @@ async fn invalid_signature_randao_reveal() { #[tokio::test] async fn invalid_signature_proposer_slashing() { + // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. + if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; for &block_index in BLOCK_INDICES { let harness = get_invalid_sigs_harness(&chain_segment).await; @@ -842,6 +868,10 @@ async fn invalid_signature_proposer_slashing() { #[tokio::test] async fn invalid_signature_attester_slashing() { + // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. + if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; for &block_index in BLOCK_INDICES { let harness = get_invalid_sigs_harness(&chain_segment).await; @@ -963,6 +993,10 @@ async fn invalid_signature_attester_slashing() { #[tokio::test] async fn invalid_signature_attestation() { + // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. + if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; let mut checked_attestation = false; @@ -1088,6 +1122,10 @@ async fn invalid_signature_deposit() { #[tokio::test] async fn invalid_signature_exit() { + // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. + if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; for &block_index in BLOCK_INDICES { let harness = get_invalid_sigs_harness(&chain_segment).await; @@ -1456,20 +1494,8 @@ async fn verify_and_process_gossip_data_sidecars( data_sidecars: DataSidecars, ) { match data_sidecars { - DataSidecars::Blobs(blob_sidecars) => { - for blob_sidecar in blob_sidecars { - let blob_index = blob_sidecar.index; - let gossip_verified = harness - .chain - .verify_blob_sidecar_for_gossip(blob_sidecar.clone(), blob_index) - .expect("should obtain gossip verified blob"); - - harness - .chain - .process_gossip_blob(gossip_verified) - .await - .expect("should import valid gossip verified blob"); - } + DataSidecars::Blobs(_blob_sidecars) => { + // Blob gossip is deprecated, blobs are available via RPC. } DataSidecars::DataColumns(column_sidecars) => { let gossip_verified = column_sidecars @@ -1521,14 +1547,9 @@ async fn verify_block_for_gossip_slashing_detection() { let verified_block = harness.chain.verify_block_for_gossip(block1).await.unwrap(); - if let Some((kzg_proofs, blobs)) = blobs1 { + if blobs1.is_some() { harness - .process_gossip_blobs_or_columns( - verified_block.block(), - blobs.iter(), - kzg_proofs.iter(), - None, - ) + .process_gossip_columns(verified_block.block(), None) .await; } harness @@ -1926,6 +1947,153 @@ async fn add_altair_block_to_base_chain() { )); } +// This is a regression test for the bogus `InvalidBestNode` error which was reachable in Gloas +// networks. Previously Lighthouse would return an `InvalidBestNode` error from `get_head` in +// contradiction to the spec, which states that the justified root should be returned when no leaf +// node is viable. +// +// The chain construction in this test is contrived but not impossible: the justified block's full +// branch is what contained the evidence to justify it, but the empty branch is more weighty and +// wins out. +#[tokio::test] +async fn gloas_get_head_can_return_justified_empty_payload_branch() { + let spec = test_spec::(); + if !spec.fork_name_at_epoch(Epoch::new(0)).gloas_enabled() { + return; + } + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec.clone().into()) + .chain_config(ChainConfig { + archive: true, + ..ChainConfig::default() + }) + .keypairs(KEYPAIRS[0..VALIDATOR_COUNT].to_vec()) + .node_custody_type(NodeCustodyType::Supernode) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + harness + .extend_slots(E::slots_per_epoch() as usize * 3) + .await; + + let justified_checkpoint = harness.justified_checkpoint(); + assert_ne!(justified_checkpoint.epoch, Epoch::new(0)); + let justified_root = justified_checkpoint.root; + let justified_block = harness + .chain + .get_blinded_block(&justified_root) + .unwrap() + .unwrap(); + let justified_slot = justified_block.message().slot(); + let justified_state_root = justified_block.message().state_root(); + + harness.advance_slot(); + harness + .extend_chain( + E::slots_per_epoch() as usize * 2, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::SomeValidators(vec![]), + ) + .await; + + let current_slot = harness.get_current_slot(); + let current_epoch = current_slot.epoch(E::slots_per_epoch()); + assert_eq!( + harness + .chain + .canonical_head + .cached_head() + .head_payload_status(), + PayloadStatus::Full + ); + + { + let fork_choice = harness.chain.canonical_head.fork_choice_read_lock(); + assert!(fork_choice.is_payload_received(&justified_root)); + let justified_node = fork_choice.get_block(&justified_root).unwrap(); + let voting_source = justified_node + .unrealized_justified_checkpoint + .unwrap_or(justified_node.justified_checkpoint); + assert!( + voting_source.epoch + 2 < current_epoch, + "the justified node's own voting source must be stale" + ); + } + + let mut attestation_state = harness + .chain + .get_state(&justified_state_root, Some(justified_slot), true) + .unwrap() + .unwrap(); + assert!( + attestation_state + .validators() + .iter() + .all(|validator| !validator.slashed), + "reproducer must not rely on slashed validators" + ); + + let all_validators = harness.get_all_validators(); + let mut validators_with_empty_vote = [false; VALIDATOR_COUNT]; + let attestation_start_slot = (current_epoch - 1).start_slot(E::slots_per_epoch()); + let attestation_slot = current_slot - 1; + assert_eq!( + attestation_start_slot + E::slots_per_epoch() - 1, + attestation_slot + ); + + // Create two epochs worth of attestations with `payload_present=false`, all pointing at the + // justified block. This ensures it's very much the canonical head, instead of the justifying + // chain built off its `Full` branch. + for slot in (attestation_start_slot.as_u64()..current_slot.as_u64()).map(Slot::new) { + while attestation_state.slot() < slot { + per_slot_processing(&mut attestation_state, None, &spec).unwrap(); + } + attestation_state.build_caches(&spec).unwrap(); + let attestation_state_root = attestation_state.update_tree_hash_cache().unwrap(); + assert_eq!( + attestation_state.get_latest_block_root(attestation_state_root), + justified_root + ); + + let fork = spec.fork_at_epoch(slot.epoch(E::slots_per_epoch())); + let (attestations, attesters) = harness.make_attestations_with_opts( + &all_validators, + &attestation_state, + attestation_state_root, + justified_root.into(), + slot, + MakeAttestationOptions { + limit: None, + fork, + payload_present_override: Some(false), + }, + ); + + for validator_index in attesters { + validators_with_empty_vote[validator_index] = true; + } + harness.process_attestations(attestations, &attestation_state); + } + + assert!( + validators_with_empty_vote.iter().all(|attested| *attested), + "all validators should have a latest regular attestation to the justified root" + ); + + let (head_root, payload_status) = harness + .chain + .canonical_head + .fork_choice_write_lock() + .get_head(current_slot, &spec) + .expect("fork choice should return the justified root on the empty payload branch"); + + assert_eq!(head_root, justified_root); + assert_eq!(payload_status, PayloadStatus::Empty); +} + // This is a regression test for this bug: // https://github.com/sigp/lighthouse/issues/4332#issuecomment-1565092279 #[tokio::test] diff --git a/beacon_node/beacon_chain/tests/events.rs b/beacon_node/beacon_chain/tests/events.rs index 29d0e38b93a..baa69753030 100644 --- a/beacon_node/beacon_chain/tests/events.rs +++ b/beacon_node/beacon_chain/tests/events.rs @@ -1,12 +1,9 @@ use arbitrary::Arbitrary; -use beacon_chain::blob_verification::GossipVerifiedBlob; use beacon_chain::data_column_verification::GossipVerifiedDataColumn; use beacon_chain::test_utils::{ BeaconChainHarness, fork_name_from_env, generate_data_column_sidecars_from_block, test_spec, }; use eth2::types::{EventKind, SseBlobSidecar, SseDataColumnSidecar}; -use rand::SeedableRng; -use rand::rngs::StdRng; use std::sync::Arc; use types::data::FixedBlobSidecarList; use types::{ @@ -17,44 +14,6 @@ use types::{ type E = MinimalEthSpec; -/// Verifies that a blob event is emitted when a gossip verified blob is received via gossip or the publish block API. -#[tokio::test] -async fn blob_sidecar_event_on_process_gossip_blob() { - if fork_name_from_env().is_some_and(|f| !f.deneb_enabled() || f.fulu_enabled()) { - return; - }; - - let spec = Arc::new(test_spec::()); - let harness = BeaconChainHarness::builder(E::default()) - .spec(spec) - .deterministic_keypairs(8) - .fresh_ephemeral_store() - .mock_execution_layer() - .build(); - - // subscribe to blob sidecar events - let event_handler = harness.chain.event_handler.as_ref().unwrap(); - let mut blob_event_receiver = event_handler.subscribe_blob_sidecar(); - - // build and process a gossip verified blob - let kzg = harness.chain.kzg.as_ref(); - let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); - let sidecar = BlobSidecar::random_valid(&mut rng, kzg) - .map(Arc::new) - .unwrap(); - let gossip_verified_blob = GossipVerifiedBlob::__assumed_valid(sidecar); - let expected_sse_blobs = SseBlobSidecar::from_blob_sidecar(gossip_verified_blob.as_blob()); - - let _ = harness - .chain - .process_gossip_blob(gossip_verified_blob) - .await - .unwrap(); - - let sidecar_event = blob_event_receiver.try_recv().unwrap(); - assert_eq!(sidecar_event, EventKind::BlobSidecar(expected_sse_blobs)); -} - /// Verifies that a data column event is emitted when a gossip verified data column is received via gossip or the publish block API. #[tokio::test] async fn data_column_sidecar_event_on_process_gossip_data_column() { diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index abf1fe48a67..42a78d740f7 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -6,7 +6,7 @@ use beacon_chain::{ BeaconChainError, BlockError, ChainConfig, ExecutionPayloadError, INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, NotifyExecutionLayer, StateSkipConfig, WhenSlotSkipped, - canonical_head::{CachedHead, CanonicalHead}, + canonical_head::CachedHead, test_utils::{BeaconChainHarness, EphemeralHarnessType, fork_name_from_env, test_spec}, }; use execution_layer::{ @@ -108,10 +108,6 @@ impl InvalidPayloadRig { self.harness.chain.canonical_head.cached_head() } - fn canonical_head(&self) -> &CanonicalHead> { - &self.harness.chain.canonical_head - } - fn previous_forkchoice_update_params(&self) -> (ForkchoiceState, PayloadAttributes) { let mock_execution_layer = self.harness.mock_execution_layer.as_ref().unwrap(); let json = mock_execution_layer @@ -353,19 +349,6 @@ impl InvalidPayloadRig { .await .unwrap(); } - - fn assert_get_head_error_contains(&self, s: &str) { - match self - .harness - .chain - .canonical_head - .fork_choice_write_lock() - .get_head(self.harness.chain.slot().unwrap(), &self.harness.chain.spec) - { - Err(ForkChoiceError::ProtoArrayStringError(e)) if e.contains(s) => (), - other => panic!("expected {} error, got {:?}", s, other), - }; - } } /// Simple test of the different import types. @@ -1297,21 +1280,14 @@ impl InvalidHeadSetup { rig.invalidate_manually(invalid_head.head_block_root()) .await; - // Since our setup ensures that there is only a single, invalid block - // that's viable for head (according to FFG filtering), setting the - // head block as invalid should not result in another head being chosen. - // Rather, it should fail to run fork choice and leave the invalid block as - // the head. - assert!( - rig.canonical_head() - .head_execution_status() - .unwrap() - .is_invalid() - ); - - // Ensure that we're getting the correct error when trying to find a new - // head. - rig.assert_get_head_error_contains("InvalidBestNode"); + // Ensure the justified root is the head. This is the spec-correct choice of head when + // all leaves are ineligible. + let mut fork_choice = rig.harness.chain.canonical_head.fork_choice_write_lock(); + let head = fork_choice + .get_head(rig.harness.chain.slot().unwrap(), &rig.harness.chain.spec) + .unwrap(); + assert_eq!(head.0, fork_choice.justified_checkpoint().root); + drop(fork_choice); Self { rig, diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 0ac77dcfaa0..b70961c499b 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -3148,6 +3148,14 @@ async fn weak_subjectivity_sync_test( .store .put_payload_envelope(&wss_block_root, &envelope) .unwrap(); + + // `from_anchor` doesn't mark the anchor's payload received, so do it here; otherwise the + // first forward block (a FULL child of the anchor) would be rejected with `ParentUnknown`. + beacon_chain + .canonical_head + .fork_choice_write_lock() + .on_valid_payload_envelope_received(wss_block_root) + .unwrap(); } // Apply blocks forward to reach head. diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index ce3851ea54e..d6233ebaf92 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -41,8 +41,8 @@ pub use crate::scheduler::BeaconProcessorQueueLengths; use crate::scheduler::work_queue::WorkQueues; use crate::work_reprocessing_queue::{ - QueuedBackfillBatch, QueuedColumnReconstruction, QueuedGossipBlock, QueuedGossipEnvelope, - ReprocessQueueMessage, + QueuedBackfillBatch, QueuedColumnReconstruction, QueuedGossipBlock, QueuedGossipDataColumn, + QueuedGossipEnvelope, ReprocessQueueMessage, }; use futures::stream::{Stream, StreamExt}; use futures::task::Poll; @@ -304,6 +304,10 @@ impl From for WorkEvent { work: Work::ColumnReconstruction(process_fn), } } + ReadyWork::DataColumn(QueuedGossipDataColumn { process_fn, .. }) => Self { + drop_during_sync: true, + work: Work::UnknownBlockDataColumn { process_fn }, + }, } } } @@ -369,6 +373,9 @@ pub enum Work { UnknownBlockAttestation { process_fn: BlockingFn, }, + UnknownBlockDataColumn { + process_fn: BlockingFn, + }, GossipAttestationBatch { attestations: GossipAttestationBatch, process_batch: Box, @@ -390,7 +397,6 @@ pub enum Work { process_batch: Box>) + Send + Sync>, }, GossipBlock(AsyncFn), - GossipBlobSidecar(AsyncFn), GossipDataColumnSidecar(AsyncFn), GossipPartialDataColumnSidecar(AsyncFn), DelayedImportBlock { @@ -465,13 +471,13 @@ pub enum WorkType { GossipAttestation, GossipAttestationToConvert, UnknownBlockAttestation, + UnknownBlockDataColumn, GossipAttestationBatch, GossipAggregate, UnknownBlockAggregate, UnknownLightClientOptimisticUpdate, GossipAggregateBatch, GossipBlock, - GossipBlobSidecar, GossipDataColumnSidecar, GossipPartialDataColumnSidecar, DelayedImportBlock, @@ -528,7 +534,6 @@ impl Work { Work::GossipAggregate { .. } => WorkType::GossipAggregate, Work::GossipAggregateBatch { .. } => WorkType::GossipAggregateBatch, Work::GossipBlock(_) => WorkType::GossipBlock, - Work::GossipBlobSidecar(_) => WorkType::GossipBlobSidecar, Work::GossipDataColumnSidecar(_) => WorkType::GossipDataColumnSidecar, Work::GossipPartialDataColumnSidecar(_) => WorkType::GossipPartialDataColumnSidecar, Work::DelayedImportBlock { .. } => WorkType::DelayedImportBlock, @@ -572,6 +577,7 @@ impl Work { Work::LightClientFinalityUpdateRequest(_) => WorkType::LightClientFinalityUpdateRequest, Work::LightClientUpdatesByRangeRequest(_) => WorkType::LightClientUpdatesByRangeRequest, Work::UnknownBlockAttestation { .. } => WorkType::UnknownBlockAttestation, + Work::UnknownBlockDataColumn { .. } => WorkType::UnknownBlockDataColumn, Work::UnknownBlockAggregate { .. } => WorkType::UnknownBlockAggregate, Work::UnknownLightClientOptimisticUpdate { .. } => { WorkType::UnknownLightClientOptimisticUpdate @@ -843,10 +849,11 @@ impl BeaconProcessor { } else if let Some(item) = work_queues.gossip_execution_payload_queue.pop() { Some(item) - } else if let Some(item) = work_queues.gossip_blob_queue.pop() { - Some(item) } else if let Some(item) = work_queues.gossip_data_column_queue.pop() { Some(item) + } else if let Some(item) = work_queues.unknown_block_data_column_queue.pop() + { + Some(item) } else if let Some(item) = work_queues.gossip_partial_data_column_queue.pop() { @@ -1157,9 +1164,6 @@ impl BeaconProcessor { Work::GossipBlock { .. } => { work_queues.gossip_block_queue.push(work, work_id) } - Work::GossipBlobSidecar { .. } => { - work_queues.gossip_blob_queue.push(work, work_id) - } Work::GossipDataColumnSidecar { .. } => { work_queues.gossip_data_column_queue.push(work, work_id) } @@ -1246,6 +1250,9 @@ impl BeaconProcessor { Work::UnknownBlockAttestation { .. } => { work_queues.unknown_block_attestation_queue.push(work) } + Work::UnknownBlockDataColumn { .. } => work_queues + .unknown_block_data_column_queue + .push(work, work_id), Work::UnknownBlockAggregate { .. } => { work_queues.unknown_block_aggregate_queue.push(work) } @@ -1296,6 +1303,9 @@ impl BeaconProcessor { WorkType::UnknownBlockAttestation => { work_queues.unknown_block_attestation_queue.len() } + WorkType::UnknownBlockDataColumn => { + work_queues.unknown_block_data_column_queue.len() + } WorkType::GossipAttestationBatch => 0, // No queue WorkType::GossipAggregate => work_queues.aggregate_queue.len(), WorkType::UnknownBlockAggregate => { @@ -1306,7 +1316,6 @@ impl BeaconProcessor { } WorkType::GossipAggregateBatch => 0, // No queue WorkType::GossipBlock => work_queues.gossip_block_queue.len(), - WorkType::GossipBlobSidecar => work_queues.gossip_blob_queue.len(), WorkType::GossipDataColumnSidecar => { work_queues.gossip_data_column_queue.len() } @@ -1513,6 +1522,7 @@ impl BeaconProcessor { }), Work::UnknownBlockAttestation { process_fn } | Work::UnknownBlockAggregate { process_fn } + | Work::UnknownBlockDataColumn { process_fn } | Work::UnknownLightClientOptimisticUpdate { process_fn, .. } => { task_spawner.spawn_blocking(process_fn) } @@ -1536,7 +1546,6 @@ impl BeaconProcessor { | Work::ColumnReconstruction(process_fn) => task_spawner.spawn_async(process_fn), Work::IgnoredRpcBlock { process_fn } => task_spawner.spawn_blocking(process_fn), Work::GossipBlock(work) - | Work::GossipBlobSidecar(work) | Work::GossipDataColumnSidecar(work) | Work::GossipPartialDataColumnSidecar(work) | Work::GossipExecutionPayload(work) => task_spawner.spawn_async(async move { diff --git a/beacon_node/beacon_processor/src/scheduler/work_queue.rs b/beacon_node/beacon_processor/src/scheduler/work_queue.rs index 2fdc15182cd..cc03feac51d 100644 --- a/beacon_node/beacon_processor/src/scheduler/work_queue.rs +++ b/beacon_node/beacon_processor/src/scheduler/work_queue.rs @@ -111,6 +111,7 @@ pub struct BeaconProcessorQueueLengths { attestation_queue: usize, unknown_block_aggregate_queue: usize, unknown_block_attestation_queue: usize, + unknown_block_data_column_queue: usize, sync_message_queue: usize, sync_contribution_queue: usize, gossip_voluntary_exit_queue: usize, @@ -125,7 +126,6 @@ pub struct BeaconProcessorQueueLengths { chain_segment_queue: usize, backfill_chain_segment: usize, gossip_block_queue: usize, - gossip_blob_queue: usize, gossip_data_column_queue: usize, gossip_partial_data_column_queue: usize, delayed_block_queue: usize, @@ -175,6 +175,8 @@ impl BeaconProcessorQueueLengths { Ok(Self { aggregate_queue: 4096, unknown_block_aggregate_queue: 1024, + // Capacity for two slot's worth of data columns for a supernode. + unknown_block_data_column_queue: 256, // Capacity for a full slot's worth of attestations if subscribed to all subnets attestation_queue: std::cmp::max( active_validator_count / slots_per_epoch, @@ -202,7 +204,6 @@ impl BeaconProcessorQueueLengths { chain_segment_queue: 64, backfill_chain_segment: 64, gossip_block_queue: 1024, - gossip_blob_queue: 1024, gossip_data_column_queue: 1024, gossip_partial_data_column_queue: 1024, delayed_block_queue: 1024, @@ -218,7 +219,7 @@ impl BeaconProcessorQueueLengths { payload_envelopes_brange_queue: 1024, payload_envelopes_broots_queue: 1024, gossip_bls_to_execution_change_queue: 16384, - // TODO(EIP-7732): verify 1024 is preferable. I used same value as `gossip_block_queue` and `gossip_blob_queue` + // TODO(EIP-7732): verify 1024 is preferable. gossip_execution_payload_queue: 1024, // TODO(EIP-7732) how big should this queue be? gossip_execution_payload_bid_queue: 1024, @@ -247,6 +248,7 @@ pub struct WorkQueues { pub attestation_debounce: TimeLatch, pub unknown_block_aggregate_queue: LifoQueue>, pub unknown_block_attestation_queue: LifoQueue>, + pub unknown_block_data_column_queue: FifoQueue>, pub sync_message_queue: LifoQueue>, pub sync_contribution_queue: LifoQueue>, pub gossip_voluntary_exit_queue: FifoQueue>, @@ -261,7 +263,6 @@ pub struct WorkQueues { pub chain_segment_queue: FifoQueue>, pub backfill_chain_segment: FifoQueue>, pub gossip_block_queue: FifoQueue>, - pub gossip_blob_queue: FifoQueue>, pub gossip_data_column_queue: FifoQueue>, pub gossip_partial_data_column_queue: FifoQueue>, pub delayed_block_queue: FifoQueue>, @@ -305,6 +306,8 @@ impl WorkQueues { LifoQueue::new(queue_lengths.unknown_block_aggregate_queue); let unknown_block_attestation_queue = LifoQueue::new(queue_lengths.unknown_block_attestation_queue); + let unknown_block_data_column_queue = + FifoQueue::new(queue_lengths.unknown_block_data_column_queue); let sync_message_queue = LifoQueue::new(queue_lengths.sync_message_queue); let sync_contribution_queue = LifoQueue::new(queue_lengths.sync_contribution_queue); @@ -332,7 +335,6 @@ impl WorkQueues { let chain_segment_queue = FifoQueue::new(queue_lengths.chain_segment_queue); let backfill_chain_segment = FifoQueue::new(queue_lengths.backfill_chain_segment); let gossip_block_queue = FifoQueue::new(queue_lengths.gossip_block_queue); - let gossip_blob_queue = FifoQueue::new(queue_lengths.gossip_blob_queue); let gossip_data_column_queue = FifoQueue::new(queue_lengths.gossip_data_column_queue); let gossip_partial_data_column_queue = FifoQueue::new(queue_lengths.gossip_partial_data_column_queue); @@ -387,6 +389,7 @@ impl WorkQueues { attestation_debounce, unknown_block_aggregate_queue, unknown_block_attestation_queue, + unknown_block_data_column_queue, sync_message_queue, sync_contribution_queue, gossip_voluntary_exit_queue, @@ -401,7 +404,6 @@ impl WorkQueues { column_reconstruction_queue, backfill_chain_segment, gossip_block_queue, - gossip_blob_queue, gossip_data_column_queue, gossip_partial_data_column_queue, delayed_block_queue, diff --git a/beacon_node/beacon_processor/src/scheduler/work_reprocessing_queue.rs b/beacon_node/beacon_processor/src/scheduler/work_reprocessing_queue.rs index b1fa56af018..62ed86fbad0 100644 --- a/beacon_node/beacon_processor/src/scheduler/work_reprocessing_queue.rs +++ b/beacon_node/beacon_processor/src/scheduler/work_reprocessing_queue.rs @@ -52,6 +52,10 @@ pub const QUEUED_ATTESTATION_DELAY: Duration = Duration::from_secs(12); /// For how long to queue light client updates for re-processing. pub const QUEUED_LIGHT_CLIENT_UPDATE_DELAY: Duration = Duration::from_secs(12); +/// Data column timeout as a multiplier of slot duration. Columns waiting for their block will be +/// sent for processing after this many slots worth of time, even if the block hasn't arrived. +const QUEUED_DATA_COLUMN_DELAY_SLOTS: u32 = 1; + /// Envelope timeout as a multiplier of slot duration. Envelopes waiting for their block will be /// sent for processing after this many slots worth of time, even if the block hasn't arrived. const QUEUED_ENVELOPE_DELAY_SLOTS: u32 = 1; @@ -76,6 +80,9 @@ const MAXIMUM_QUEUED_ENVELOPES: usize = 16; /// How many attestations we keep before new ones get dropped. const MAXIMUM_QUEUED_ATTESTATIONS: usize = 16_384; +/// How many columns we keep before new ones get dropped. +const MAXIMUM_QUEUED_DATA_COLUMNS: usize = 256; + /// How many light client updates we keep before new ones get dropped. const MAXIMUM_QUEUED_LIGHT_CLIENT_UPDATES: usize = 128; @@ -123,6 +130,8 @@ pub enum ReprocessQueueMessage { UnknownLightClientOptimisticUpdate(QueuedLightClientUpdate), /// A new backfill batch that needs to be scheduled for processing. BackfillSync(QueuedBackfillBatch), + /// A gossip data column that references an unknown block. + UnknownBlockDataColumn(QueuedGossipDataColumn), /// A delayed column reconstruction that needs checking DelayColumnReconstruction(QueuedColumnReconstruction), } @@ -138,6 +147,7 @@ pub enum ReadyWork { LightClientUpdate(QueuedLightClientUpdate), BackfillSync(QueuedBackfillBatch), ColumnReconstruction(QueuedColumnReconstruction), + DataColumn(QueuedGossipDataColumn), } /// An Attestation for which the corresponding block was not seen while processing, queued for @@ -200,6 +210,12 @@ pub struct QueuedColumnReconstruction { pub process_fn: AsyncFn, } +/// A gossip data column that references an unknown block, queued for later reprocessing. +pub struct QueuedGossipDataColumn { + pub beacon_block_root: Hash256, + pub process_fn: BlockingFn, +} + impl TryFrom> for QueuedBackfillBatch { type Error = WorkEvent; @@ -240,6 +256,8 @@ enum InboundEvent { ReadyBackfillSync(QueuedBackfillBatch), /// A column reconstruction that was queued is ready for processing. ReadyColumnReconstruction(QueuedColumnReconstruction), + /// A gossip data column that is ready for re-processing. + ReadyDataColumn(Hash256), /// A message sent to the `ReprocessQueue` Msg(ReprocessQueueMessage), } @@ -264,6 +282,8 @@ struct ReprocessQueue { lc_updates_delay_queue: DelayQueue, /// Queue to manage scheduled column reconstructions. column_reconstructions_delay_queue: DelayQueue, + /// Queue to manage gossip data column timeouts. + data_columns_delay_queue: DelayQueue, /* Queued items */ /// Queued blocks. @@ -284,6 +304,10 @@ struct ReprocessQueue { queued_column_reconstructions: HashMap>, /// Queued backfill batches queued_backfill_batches: Vec, + /// Queued gossip data columns awaiting their block, keyed by block root. + awaiting_data_columns_per_root: HashMap, DelayKey)>, + /// Total number of queued gossip data columns across all roots. + queued_data_columns_count: usize, /* Aux */ /// Next attestation id, used for both aggregated and unaggregated attestations @@ -294,6 +318,7 @@ struct ReprocessQueue { rpc_block_debounce: TimeLatch, attestation_delay_debounce: TimeLatch, lc_update_delay_debounce: TimeLatch, + data_column_delay_debounce: TimeLatch, next_backfill_batch_event: Option>>, slot_clock: Arc, } @@ -387,6 +412,13 @@ impl Stream for ReprocessQueue { Poll::Ready(None) | Poll::Pending => (), } + match self.data_columns_delay_queue.poll_expired(cx) { + Poll::Ready(Some(block_root)) => { + return Poll::Ready(Some(InboundEvent::ReadyDataColumn(block_root.into_inner()))); + } + Poll::Ready(None) | Poll::Pending => (), + } + if let Some(next_backfill_batch_event) = self.next_backfill_batch_event.as_mut() { match next_backfill_batch_event.as_mut().poll(cx) { Poll::Ready(_) => { @@ -455,6 +487,7 @@ impl ReprocessQueue { attestations_delay_queue: DelayQueue::new(), lc_updates_delay_queue: DelayQueue::new(), column_reconstructions_delay_queue: DelayQueue::new(), + data_columns_delay_queue: DelayQueue::new(), queued_gossip_block_roots: HashSet::new(), awaiting_envelopes_per_root: HashMap::new(), queued_lc_updates: FnvHashMap::default(), @@ -464,6 +497,8 @@ impl ReprocessQueue { awaiting_lc_updates_per_parent_root: HashMap::new(), queued_backfill_batches: Vec::new(), queued_column_reconstructions: HashMap::new(), + awaiting_data_columns_per_root: HashMap::new(), + queued_data_columns_count: 0, next_attestation: 0, next_lc_update: 0, early_block_debounce: TimeLatch::default(), @@ -471,6 +506,7 @@ impl ReprocessQueue { rpc_block_debounce: TimeLatch::default(), attestation_delay_debounce: TimeLatch::default(), lc_update_delay_debounce: TimeLatch::default(), + data_column_delay_debounce: TimeLatch::default(), next_backfill_batch_event: None, slot_clock, } @@ -551,22 +587,16 @@ impl ReprocessQueue { return; } - // When the queue is full, evict the oldest entry to make room for newer envelopes. + // When the queue is full, drop the new envelope. if self.awaiting_envelopes_per_root.len() >= MAXIMUM_QUEUED_ENVELOPES { if self.envelope_delay_debounce.elapsed() { warn!( queue_size = MAXIMUM_QUEUED_ENVELOPES, msg = "system resources may be saturated", - "Envelope delay queue is full, evicting oldest entry" + "Envelope delay queue is full, dropping envelope" ); } - if let Some(oldest_root) = - self.awaiting_envelopes_per_root.keys().next().copied() - && let Some((_envelope, delay_key)) = - self.awaiting_envelopes_per_root.remove(&oldest_root) - { - self.envelope_delay_queue.remove(&delay_key); - } + return; } // Register the timeout. @@ -688,6 +718,37 @@ impl ReprocessQueue { self.next_attestation += 1; } + InboundEvent::Msg(UnknownBlockDataColumn(queued_data_column)) => { + let block_root = queued_data_column.beacon_block_root; + + if self.queued_data_columns_count >= MAXIMUM_QUEUED_DATA_COLUMNS { + if self.data_column_delay_debounce.elapsed() { + warn!( + queue_size = MAXIMUM_QUEUED_DATA_COLUMNS, + msg = "system resources may be saturated", + "Data column delay queue is full, dropping column" + ); + } + return; + } + + if let Some((columns, _delay_key)) = + self.awaiting_data_columns_per_root.get_mut(&block_root) + { + // Append to existing entry; the timer for this root is already running. + columns.push(queued_data_column); + } else { + let delay_key = self.data_columns_delay_queue.insert( + block_root, + self.slot_clock.slot_duration() * QUEUED_DATA_COLUMN_DELAY_SLOTS, + ); + + self.awaiting_data_columns_per_root + .insert(block_root, (vec![queued_data_column], delay_key)); + } + + self.queued_data_columns_count += 1; + } InboundEvent::Msg(UnknownLightClientOptimisticUpdate( queued_light_client_optimistic_update, )) => { @@ -800,6 +861,25 @@ impl ReprocessQueue { ); } } + + // Unqueue the data columns we have for this root, if any. + if let Some((data_columns, delay_key)) = + self.awaiting_data_columns_per_root.remove(&block_root) + { + self.data_columns_delay_queue.remove(&delay_key); + self.queued_data_columns_count = self + .queued_data_columns_count + .saturating_sub(data_columns.len()); + for data_column in data_columns { + if self + .ready_work_tx + .try_send(ReadyWork::DataColumn(data_column)) + .is_err() + { + error!(?block_root, "Failed to send data column for reprocessing"); + } + } + } } InboundEvent::Msg(NewLightClientOptimisticUpdate { parent_root }) => { // Unqueue the light client optimistic updates we have for this root, if any. @@ -1053,6 +1133,27 @@ impl ReprocessQueue { ); } } + InboundEvent::ReadyDataColumn(block_root) => { + if let Some((data_columns, _)) = + self.awaiting_data_columns_per_root.remove(&block_root) + { + self.queued_data_columns_count = self + .queued_data_columns_count + .saturating_sub(data_columns.len()); + for data_column in data_columns { + if self + .ready_work_tx + .try_send(ReadyWork::DataColumn(data_column)) + .is_err() + { + error!( + hint = "system may be overloaded", + "Ignored expired gossip data column" + ); + } + } + } + } } metrics::set_gauge_vec( @@ -1581,48 +1682,87 @@ mod tests { assert_eq!(queue.envelope_delay_queue.len(), 1); } + /// Tests that a queued gossip data column is released when its block is imported. #[tokio::test] - async fn envelope_capacity_evicts_oldest() { + async fn data_column_released_on_block_imported() { create_test_tracing_subscriber(); - let mut queue = test_queue(); + let config = BeaconProcessorConfig::default(); + let (ready_work_tx, mut ready_work_rx) = + mpsc::channel::(config.max_scheduled_work_queue_len); + let (_, reprocess_work_rx) = + mpsc::channel::(config.max_scheduled_work_queue_len); + let slot_clock = Arc::new(testing_slot_clock(12)); + let mut queue = ReprocessQueue::new(ready_work_tx, reprocess_work_rx, slot_clock); - // Pause time so it only advances manually tokio::time::pause(); - // Fill the queue to capacity. - for i in 0..MAXIMUM_QUEUED_ENVELOPES { - let block_root = Hash256::repeat_byte(i as u8); - let msg = ReprocessQueueMessage::UnknownBlockForEnvelope(QueuedGossipEnvelope { - beacon_block_slot: Slot::new(1), - beacon_block_root: block_root, - process_fn: Box::pin(async {}), - }); - queue.handle_message(InboundEvent::Msg(msg)); - } - assert_eq!( - queue.awaiting_envelopes_per_root.len(), - MAXIMUM_QUEUED_ENVELOPES - ); + let beacon_block_root = Hash256::repeat_byte(0xbb); - // One more should evict the oldest and insert the new one. - let overflow_root = Hash256::repeat_byte(0xff); - let msg = ReprocessQueueMessage::UnknownBlockForEnvelope(QueuedGossipEnvelope { - beacon_block_slot: Slot::new(1), - beacon_block_root: overflow_root, - process_fn: Box::pin(async {}), + let msg = ReprocessQueueMessage::UnknownBlockDataColumn(QueuedGossipDataColumn { + beacon_block_root, + process_fn: Box::new(|| {}), }); queue.handle_message(InboundEvent::Msg(msg)); - // Queue should still be at capacity, with the new root present. - assert_eq!( - queue.awaiting_envelopes_per_root.len(), - MAXIMUM_QUEUED_ENVELOPES + assert_eq!(queue.awaiting_data_columns_per_root.len(), 1); + assert!( + queue + .awaiting_data_columns_per_root + .contains_key(&beacon_block_root) ); + assert_eq!(queue.data_columns_delay_queue.len(), 1); + + // Simulate block import. + queue.handle_message(InboundEvent::Msg(ReprocessQueueMessage::BlockImported { + block_root: beacon_block_root, + parent_root: Hash256::repeat_byte(0x00), + })); + + // Internal state should be cleaned up. + assert!(queue.awaiting_data_columns_per_root.is_empty()); + assert_eq!(queue.data_columns_delay_queue.len(), 0); + + // The column should have been sent to the ready_work channel. + let ready = ready_work_rx.try_recv().expect("column should be ready"); + assert!(matches!(ready, ReadyWork::DataColumn(_))); + } + + /// Tests that an expired gossip data column is pruned cleanly from all internal state. + #[tokio::test] + async fn prune_awaiting_data_columns_per_root() { + create_test_tracing_subscriber(); + + let mut queue = test_queue(); + + tokio::time::pause(); + + let beacon_block_root = Hash256::repeat_byte(0xcd); + + let msg = ReprocessQueueMessage::UnknownBlockDataColumn(QueuedGossipDataColumn { + beacon_block_root, + process_fn: Box::new(|| {}), + }); + queue.handle_message(InboundEvent::Msg(msg)); + + assert_eq!(queue.awaiting_data_columns_per_root.len(), 1); assert!( queue - .awaiting_envelopes_per_root - .contains_key(&overflow_root) + .awaiting_data_columns_per_root + .contains_key(&beacon_block_root) ); + + // Advance time past the delay so the entry expires. + advance_time( + &queue.slot_clock, + 2 * queue.slot_clock.slot_duration() * QUEUED_DATA_COLUMN_DELAY_SLOTS, + ) + .await; + let ready_msg = queue.next().await.unwrap(); + assert!(matches!(ready_msg, InboundEvent::ReadyDataColumn(_))); + queue.handle_message(ready_msg); + + // All internal state should be cleaned up. + assert!(queue.awaiting_data_columns_per_root.is_empty()); } } diff --git a/beacon_node/execution_layer/Cargo.toml b/beacon_node/execution_layer/Cargo.toml index a23ea948e4e..e37a85f1444 100644 --- a/beacon_node/execution_layer/Cargo.toml +++ b/beacon_node/execution_layer/Cargo.toml @@ -10,14 +10,18 @@ alloy-primitives = { workspace = true } alloy-rlp = { workspace = true } alloy-rpc-types-eth = { workspace = true } arc-swap = "1.6.0" +async-stream = "0.3" +async-trait = "0.1" bls = { workspace = true } builder_client = { path = "../builder_client" } bytes = { workspace = true } eth2 = { workspace = true, features = ["events", "lighthouse", "network"] } ethereum_serde_utils = { workspace = true } ethereum_ssz = { workspace = true } +ethereum_ssz_derive = { workspace = true } fixed_bytes = { workspace = true } fork_choice = { workspace = true } +futures = { workspace = true } hash-db = "0.15.2" hash256-std-hasher = "0.15.2" hex = { workspace = true } @@ -32,6 +36,7 @@ parking_lot = { workspace = true } pretty_reqwest_error = { workspace = true } rand = { workspace = true } reqwest = { workspace = true } +reqwest-eventsource = "0.6" sensitive_url = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/beacon_node/execution_layer/src/eip8025/errors.rs b/beacon_node/execution_layer/src/eip8025/errors.rs new file mode 100644 index 00000000000..f2fca595820 --- /dev/null +++ b/beacon_node/execution_layer/src/eip8025/errors.rs @@ -0,0 +1,92 @@ +use pretty_reqwest_error::PrettyReqwestError; +use std::fmt; + +#[derive(Debug)] +pub enum ProofEngineError { + InvalidProofType(String), + InvalidHeaderFormat(String), + InvalidPayload(String), + ProofGenerationUnavailable(String), + HttpClientError(PrettyReqwestError), + JsonRpcError { code: i64, message: String }, + SerdeError(serde_json::Error), + SszError(ssz_types::Error), + SseError(String), + ForkNotSupported(String), + ProofTypeNotSupported(u8), + Timeout, + EngineUnavailable, +} + +impl ProofEngineError { + pub fn rpc_error_code(&self) -> Option { + match self { + ProofEngineError::JsonRpcError { code, .. } => Some(*code), + _ => None, + } + } + + pub fn is_not_supported(&self) -> bool { + matches!(self, ProofEngineError::ProofTypeNotSupported(_)) + } +} + +pub mod error_codes { + pub const INVALID_HEADER_FORMAT: i64 = -39002; + pub const INVALID_PAYLOAD: i64 = -39003; + pub const PROOF_GENERATION_UNAVAILABLE: i64 = -39004; +} + +impl fmt::Display for ProofEngineError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ProofEngineError::InvalidProofType(msg) => write!(f, "invalid proof type: {msg}"), + ProofEngineError::InvalidHeaderFormat(msg) => { + write!(f, "invalid header format: {msg}") + } + ProofEngineError::InvalidPayload(msg) => write!(f, "invalid payload: {msg}"), + ProofEngineError::ProofGenerationUnavailable(msg) => { + write!(f, "proof generation unavailable: {msg}") + } + ProofEngineError::HttpClientError(err) => write!(f, "HTTP request failed: {err}"), + ProofEngineError::JsonRpcError { code, message } => { + write!(f, "JSON-RPC error ({code}): {message}") + } + ProofEngineError::SerdeError(err) => write!(f, "serialization error: {err}"), + ProofEngineError::SszError(err) => write!(f, "SSZ error: {err}"), + ProofEngineError::SseError(msg) => write!(f, "SSE error: {msg}"), + ProofEngineError::ForkNotSupported(fork) => write!(f, "fork not supported: {fork}"), + ProofEngineError::ProofTypeNotSupported(proof_type) => { + write!(f, "proof type {proof_type} not supported") + } + ProofEngineError::Timeout => write!(f, "proof engine request timed out"), + ProofEngineError::EngineUnavailable => write!(f, "proof engine is unavailable"), + } + } +} + +impl std::error::Error for ProofEngineError {} + +impl From for ProofEngineError { + fn from(e: serde_json::Error) -> Self { + ProofEngineError::SerdeError(e) + } +} + +impl From for ProofEngineError { + fn from(e: ssz_types::Error) -> Self { + ProofEngineError::SszError(e) + } +} + +impl From for ProofEngineError { + fn from(e: reqwest::Error) -> Self { + ProofEngineError::HttpClientError(e.into()) + } +} + +impl From for ProofEngineError { + fn from(e: PrettyReqwestError) -> Self { + ProofEngineError::HttpClientError(e) + } +} diff --git a/beacon_node/execution_layer/src/eip8025/mod.rs b/beacon_node/execution_layer/src/eip8025/mod.rs new file mode 100644 index 00000000000..44857440b72 --- /dev/null +++ b/beacon_node/execution_layer/src/eip8025/mod.rs @@ -0,0 +1,17 @@ +//! EIP-8025 optional execution proof-engine transport. +//! +//! This module intentionally does not act as an execution engine and does not gate fork choice. +//! It provides HTTP helpers for requesting and verifying proofs. Beacon-chain code records proof +//! status separately and only applies proof-backed payload promotion when explicitly configured. + +pub mod errors; +pub mod proof_engine; +pub mod proof_node_client; +pub mod types; + +pub use errors::ProofEngineError; +pub use proof_engine::HttpProofEngine; +pub use proof_node_client::{ + HttpProofNodeClient, PROOF_ENGINE_TIMEOUT, ProofNodeClient, ProofRequestResponse, +}; +pub use types::{FailureReason, ProofComplete, ProofEvent, ProofFailure, ProofType, SseEventParts}; diff --git a/beacon_node/execution_layer/src/eip8025/proof_engine.rs b/beacon_node/execution_layer/src/eip8025/proof_engine.rs new file mode 100644 index 00000000000..1332957839a --- /dev/null +++ b/beacon_node/execution_layer/src/eip8025/proof_engine.rs @@ -0,0 +1,72 @@ +use super::errors::ProofEngineError; +use super::proof_node_client::{HttpProofNodeClient, ProofNodeClient}; +use super::types::ProofEvent; +use bytes::Bytes; +use futures::stream::Stream; +use sensitive_url::SensitiveUrl; +use ssz::Encode; +use std::pin::Pin; +use std::time::Duration; +use types::execution::eip8025::{ProofAttributes, ProofStatus, SignedExecutionProof}; +use types::{EthSpec, Hash256}; + +pub struct HttpProofEngine { + proof_node: Box, +} + +impl HttpProofEngine { + pub fn new(url: SensitiveUrl, timeout: Option) -> Self { + Self::with_proof_node(HttpProofNodeClient::new(url, timeout)) + } + + pub fn with_proof_node(proof_node: impl ProofNodeClient + 'static) -> Self { + Self { + proof_node: Box::new(proof_node), + } + } + + pub async fn verify_execution_proof( + &self, + proof: &SignedExecutionProof, + ) -> Result { + self.proof_node + .verify_proof(proof.request_root(), proof.proof_type(), proof.proof_data()) + .await + } + + pub async fn get_proof( + &self, + new_payload_request_root: Hash256, + proof_type: u8, + ) -> Result { + self.proof_node + .get_proof(new_payload_request_root, proof_type) + .await + } + + pub async fn request_proofs( + &self, + new_payload_request: crate::NewPayloadRequest<'_, E>, + proof_attributes: ProofAttributes, + ) -> Result { + self.request_proofs_ssz(new_payload_request.as_ssz_bytes(), proof_attributes) + .await + } + + pub async fn request_proofs_ssz( + &self, + ssz_body: Vec, + proof_attributes: ProofAttributes, + ) -> Result { + self.proof_node + .request_proofs(ssz_body, proof_attributes) + .await + } + + pub fn subscribe_proof_events( + &self, + filter_root: Option, + ) -> Pin> + Send + '_>> { + self.proof_node.subscribe_proof_events(filter_root) + } +} diff --git a/beacon_node/execution_layer/src/eip8025/proof_node_client.rs b/beacon_node/execution_layer/src/eip8025/proof_node_client.rs new file mode 100644 index 00000000000..c4e88490f24 --- /dev/null +++ b/beacon_node/execution_layer/src/eip8025/proof_node_client.rs @@ -0,0 +1,197 @@ +use super::errors::ProofEngineError; +use super::types::{ProofEvent, ProofType, SseEventParts}; +use bytes::Bytes; +use futures::stream::Stream; +use reqwest::Client; +use reqwest_eventsource::{Event, EventSource}; +use sensitive_url::SensitiveUrl; +use std::pin::Pin; +use std::time::Duration; +use tokio_stream::StreamExt; +use types::Hash256; +use types::execution::eip8025::{ProofAttributes, ProofStatus}; + +pub const PROOF_ENGINE_TIMEOUT: Duration = Duration::from_secs(1); + +const PATH_PROOF_REQUESTS: &str = "/v1/execution_proof_requests"; +const PATH_PROOF_VERIFICATIONS: &str = "/v1/execution_proof_verifications"; +const PATH_PROOFS: &str = "/v1/execution_proofs"; + +const QUERY_PROOF_TYPES: &str = "proof_types"; +const QUERY_NEW_PAYLOAD_REQUEST_ROOT: &str = "new_payload_request_root"; +const QUERY_PROOF_TYPE: &str = "proof_type"; + +const HEADER_CONTENT_TYPE: &str = "content-type"; +const HEADER_VALUE_SSZ: &str = "application/octet-stream"; + +#[async_trait::async_trait] +pub trait ProofNodeClient: Send + Sync { + async fn request_proofs( + &self, + ssz_body: Vec, + proof_attributes: ProofAttributes, + ) -> Result; + + async fn verify_proof( + &self, + root: Hash256, + proof_type: u8, + proof_data: &[u8], + ) -> Result; + + async fn get_proof(&self, root: Hash256, proof_type: u8) -> Result; + + fn subscribe_proof_events( + &self, + filter_root: Option, + ) -> Pin> + Send + '_>>; +} + +#[derive(Debug, Clone, serde::Deserialize)] +pub struct ProofRequestResponse { + pub new_payload_request_root: Hash256, +} + +#[derive(Debug, Clone, serde::Deserialize)] +struct ProofVerificationResponse { + status: ProofVerificationStatus, +} + +#[derive(Debug, Clone, serde::Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +enum ProofVerificationStatus { + Valid, + Invalid, +} + +pub struct HttpProofNodeClient { + client: Client, + url: SensitiveUrl, + timeout: Duration, +} + +impl HttpProofNodeClient { + pub fn new(url: SensitiveUrl, timeout: Option) -> Self { + let client = Client::builder() + .build() + .expect("failed to build proof-engine HTTP client"); + + Self { + client, + url, + timeout: timeout.unwrap_or(PROOF_ENGINE_TIMEOUT), + } + } + + fn url(&self, path: &str) -> reqwest::Url { + let mut url = self.url.expose_full().clone(); + url.set_path(path); + url + } +} + +#[async_trait::async_trait] +impl ProofNodeClient for HttpProofNodeClient { + async fn request_proofs( + &self, + ssz_body: Vec, + proof_attributes: ProofAttributes, + ) -> Result { + let proof_types_csv = proof_attributes + .proof_types + .iter() + .map(|proof_type| ProofType::from_u8(*proof_type).map(|pt| pt.as_str().to_string())) + .collect::, _>>()? + .join(","); + + let response: ProofRequestResponse = self + .client + .post(self.url(PATH_PROOF_REQUESTS)) + .query(&[(QUERY_PROOF_TYPES, &proof_types_csv)]) + .header(HEADER_CONTENT_TYPE, HEADER_VALUE_SSZ) + .body(ssz_body) + .timeout(self.timeout) + .send() + .await? + .error_for_status()? + .json() + .await?; + + Ok(response.new_payload_request_root) + } + + async fn verify_proof( + &self, + root: Hash256, + proof_type: u8, + proof_data: &[u8], + ) -> Result { + let proof_type = ProofType::from_u8(proof_type)?; + let response: ProofVerificationResponse = self + .client + .post(self.url(PATH_PROOF_VERIFICATIONS)) + .query(&[ + (QUERY_NEW_PAYLOAD_REQUEST_ROOT, &root.to_string()), + (QUERY_PROOF_TYPE, &proof_type.to_string()), + ]) + .header(HEADER_CONTENT_TYPE, HEADER_VALUE_SSZ) + .body(proof_data.to_vec()) + .timeout(self.timeout) + .send() + .await? + .error_for_status()? + .json() + .await?; + + match response.status { + ProofVerificationStatus::Valid => Ok(ProofStatus::Valid), + ProofVerificationStatus::Invalid => Ok(ProofStatus::Invalid), + } + } + + async fn get_proof(&self, root: Hash256, proof_type: u8) -> Result { + let proof_type = ProofType::from_u8(proof_type)?; + Ok(self + .client + .get(self.url(&format!("{PATH_PROOFS}/{root}/{proof_type}"))) + .timeout(self.timeout) + .send() + .await? + .error_for_status()? + .bytes() + .await?) + } + + fn subscribe_proof_events( + &self, + filter_root: Option, + ) -> Pin> + Send + '_>> { + let client = self.client.clone(); + let url = self.url(PATH_PROOF_REQUESTS); + + Box::pin(async_stream::try_stream! { + let builder = if let Some(root) = filter_root { + client.get(url).query(&[(QUERY_NEW_PAYLOAD_REQUEST_ROOT, &root.to_string())]) + } else { + client.get(url) + }; + let mut events = EventSource::new(builder) + .map_err(|e| ProofEngineError::SseError( + format!("failed to create proof-engine event source: {e}") + ))?; + + while let Some(event) = events.next().await { + match event { + Ok(Event::Open) => {} + Ok(Event::Message(message)) => { + yield ProofEvent::try_from(SseEventParts(&message.event, &message.data))?; + } + Err(error) => { + events.close(); + Err(ProofEngineError::SseError(error.to_string()))?; + } + } + } + }) + } +} diff --git a/beacon_node/execution_layer/src/eip8025/types.rs b/beacon_node/execution_layer/src/eip8025/types.rs new file mode 100644 index 00000000000..3f09563a180 --- /dev/null +++ b/beacon_node/execution_layer/src/eip8025/types.rs @@ -0,0 +1,269 @@ +use super::errors::ProofEngineError; +use serde::{Deserialize, Deserializer, Serialize}; +use std::fmt; +use std::str::FromStr; +use types::Hash256; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(into = "String", try_from = "String")] +#[repr(u8)] +pub enum ProofType { + EthrexRisc0 = 0, + EthrexSP1 = 1, + EthrexZisk = 2, + RethOpenVM = 3, + RethRisc0 = 4, + RethSP1 = 5, + RethZisk = 6, +} + +impl ProofType { + pub fn as_str(&self) -> &'static str { + match self { + Self::EthrexRisc0 => "ethrex-risc0", + Self::EthrexSP1 => "ethrex-sp1", + Self::EthrexZisk => "ethrex-zisk", + Self::RethOpenVM => "reth-openvm", + Self::RethRisc0 => "reth-risc0", + Self::RethSP1 => "reth-sp1", + Self::RethZisk => "reth-zisk", + } + } + + pub fn from_u8(value: u8) -> Result { + match value { + 0 => Ok(Self::EthrexRisc0), + 1 => Ok(Self::EthrexSP1), + 2 => Ok(Self::EthrexZisk), + 3 => Ok(Self::RethOpenVM), + 4 => Ok(Self::RethRisc0), + 5 => Ok(Self::RethSP1), + 6 => Ok(Self::RethZisk), + _ => Err(ProofEngineError::InvalidProofType(format!( + "no mapping for proof type {value}" + ))), + } + } + + pub fn to_u8(self) -> u8 { + self as u8 + } + + pub fn all() -> &'static [ProofType] { + &[ + Self::EthrexRisc0, + Self::EthrexSP1, + Self::EthrexZisk, + Self::RethOpenVM, + Self::RethRisc0, + Self::RethSP1, + Self::RethZisk, + ] + } +} + +impl FromStr for ProofType { + type Err = ProofEngineError; + + fn from_str(s: &str) -> Result { + match s { + "ethrex-risc0" => Ok(Self::EthrexRisc0), + "ethrex-sp1" => Ok(Self::EthrexSP1), + "ethrex-zisk" => Ok(Self::EthrexZisk), + "reth-openvm" => Ok(Self::RethOpenVM), + "reth-risc0" => Ok(Self::RethRisc0), + "reth-sp1" => Ok(Self::RethSP1), + "reth-zisk" => Ok(Self::RethZisk), + numeric => numeric.parse::().map_or_else( + |_| { + Err(ProofEngineError::InvalidProofType(format!( + "unknown proof type: {s}" + ))) + }, + Self::from_u8, + ), + } + } +} + +#[cfg(test)] +mod tests { + use super::ProofType; + + #[test] + fn proof_type_parses_string_names() { + assert_eq!( + "reth-zisk" + .parse::() + .expect("known proof type should parse"), + ProofType::RethZisk + ); + } + + #[test] + fn proof_type_parses_numeric_ids() { + assert_eq!( + "6".parse::() + .expect("known numeric proof type should parse"), + ProofType::RethZisk + ); + } + + #[test] + fn proof_type_rejects_unknown_names() { + let error = "not-a-proof-type" + .parse::() + .expect_err("unknown proof type should be rejected"); + assert!( + error.to_string().contains("unknown proof type"), + "unexpected error: {error}" + ); + } + + #[test] + fn proof_type_rejects_unknown_numeric_ids() { + let error = "7" + .parse::() + .expect_err("unknown numeric proof type should be rejected"); + assert!( + error.to_string().contains("no mapping for proof type 7"), + "unexpected error: {error}" + ); + } +} + +impl fmt::Display for ProofType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl From for String { + fn from(proof_type: ProofType) -> Self { + proof_type.as_str().to_string() + } +} + +impl TryFrom for ProofType { + type Error = ProofEngineError; + + fn try_from(s: String) -> Result { + s.parse() + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ProofTypes(pub Vec); + +impl Default for ProofTypes { + fn default() -> Self { + Self(vec![ + ProofType::EthrexRisc0, + ProofType::EthrexSP1, + ProofType::EthrexZisk, + ProofType::RethOpenVM, + ]) + } +} + +impl std::ops::Deref for ProofTypes { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From> for ProofTypes { + fn from(proof_types: Vec) -> Self { + Self(proof_types) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ProofEvent { + ProofComplete(ProofComplete), + ProofFailure(ProofFailure), +} + +impl ProofEvent { + pub fn new_payload_request_root(&self) -> Hash256 { + match self { + Self::ProofComplete(complete) => complete.new_payload_request_root, + Self::ProofFailure(failure) => failure.new_payload_request_root, + } + } + + pub fn proof_type(&self) -> u8 { + match self { + Self::ProofComplete(complete) => complete.proof_type, + Self::ProofFailure(failure) => failure.proof_type, + } + } +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct ProofComplete { + pub new_payload_request_root: Hash256, + #[serde(deserialize_with = "deserialize_proof_type")] + pub proof_type: u8, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct ProofFailure { + pub new_payload_request_root: Hash256, + #[serde(deserialize_with = "deserialize_proof_type")] + pub proof_type: u8, + pub reason: FailureReason, + pub error: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum FailureReason { + WitnessTimeout, + ProvingTimeout, + ProvingError, + InternalError, + #[serde(other)] + Unknown, +} + +fn deserialize_proof_type<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum ProofTypeValue { + Number(u8), + String(String), + } + + match ProofTypeValue::deserialize(deserializer)? { + ProofTypeValue::Number(n) => Ok(n), + ProofTypeValue::String(s) => { + if let Ok(proof_type) = s.parse::() { + return Ok(proof_type.to_u8()); + } + s.parse::().map_err(serde::de::Error::custom) + } + } +} + +pub struct SseEventParts<'a>(pub &'a str, pub &'a str); + +impl<'a> TryFrom> for ProofEvent { + type Error = ProofEngineError; + + fn try_from(parts: SseEventParts<'a>) -> Result { + let SseEventParts(name, data) = parts; + match name { + "proof_complete" => Ok(Self::ProofComplete(serde_json::from_str(data)?)), + "proof_failure" => Ok(Self::ProofFailure(serde_json::from_str(data)?)), + other => Err(ProofEngineError::SseError(format!( + "unknown SSE event type: {other}" + ))), + } + } +} diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index 7337a29c8f0..d9dd9aaf4ce 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -1,12 +1,11 @@ use crate::engines::ForkchoiceState; use crate::http::{ ENGINE_FORKCHOICE_UPDATED_V1, ENGINE_FORKCHOICE_UPDATED_V2, ENGINE_FORKCHOICE_UPDATED_V3, - ENGINE_FORKCHOICE_UPDATED_V4, ENGINE_GET_BLOBS_V1, ENGINE_GET_BLOBS_V2, - ENGINE_GET_CLIENT_VERSION_V1, ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, - ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, ENGINE_GET_PAYLOAD_V1, ENGINE_GET_PAYLOAD_V2, - ENGINE_GET_PAYLOAD_V3, ENGINE_GET_PAYLOAD_V4, ENGINE_GET_PAYLOAD_V5, ENGINE_GET_PAYLOAD_V6, - ENGINE_NEW_PAYLOAD_V1, ENGINE_NEW_PAYLOAD_V2, ENGINE_NEW_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V4, - ENGINE_NEW_PAYLOAD_V5, + ENGINE_FORKCHOICE_UPDATED_V4, ENGINE_GET_BLOBS_V2, ENGINE_GET_CLIENT_VERSION_V1, + ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, + ENGINE_GET_PAYLOAD_V1, ENGINE_GET_PAYLOAD_V2, ENGINE_GET_PAYLOAD_V3, ENGINE_GET_PAYLOAD_V4, + ENGINE_GET_PAYLOAD_V5, ENGINE_GET_PAYLOAD_V6, ENGINE_NEW_PAYLOAD_V1, ENGINE_NEW_PAYLOAD_V2, + ENGINE_NEW_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V4, ENGINE_NEW_PAYLOAD_V5, }; use eth2::types::{ BlobsBundle, SsePayloadAttributes, SsePayloadAttributesV1, SsePayloadAttributesV2, @@ -607,7 +606,6 @@ pub struct EngineCapabilities { pub get_payload_v5: bool, pub get_payload_v6: bool, pub get_client_version_v1: bool, - pub get_blobs_v1: bool, pub get_blobs_v2: bool, pub get_blobs_v3: bool, } @@ -669,9 +667,6 @@ impl EngineCapabilities { if self.get_client_version_v1 { response.push(ENGINE_GET_CLIENT_VERSION_V1); } - if self.get_blobs_v1 { - response.push(ENGINE_GET_BLOBS_V1); - } if self.get_blobs_v2 { response.push(ENGINE_GET_BLOBS_V2); } diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index 7c63f78a223..8df7d2a54be 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -62,7 +62,6 @@ pub const ENGINE_EXCHANGE_CAPABILITIES_TIMEOUT: Duration = Duration::from_secs(1 pub const ENGINE_GET_CLIENT_VERSION_V1: &str = "engine_getClientVersionV1"; pub const ENGINE_GET_CLIENT_VERSION_TIMEOUT: Duration = Duration::from_secs(1); -pub const ENGINE_GET_BLOBS_V1: &str = "engine_getBlobsV1"; pub const ENGINE_GET_BLOBS_V2: &str = "engine_getBlobsV2"; pub const ENGINE_GET_BLOBS_V3: &str = "engine_getBlobsV3"; pub const ENGINE_GET_BLOBS_TIMEOUT: Duration = Duration::from_secs(1); @@ -92,7 +91,6 @@ pub static LIGHTHOUSE_CAPABILITIES: &[&str] = &[ ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, ENGINE_GET_CLIENT_VERSION_V1, - ENGINE_GET_BLOBS_V1, ENGINE_GET_BLOBS_V2, ENGINE_GET_BLOBS_V3, ]; @@ -717,20 +715,6 @@ impl HttpJsonRpc { } } - pub async fn get_blobs_v1( - &self, - versioned_hashes: Vec, - ) -> Result>>, Error> { - let params = json!([versioned_hashes]); - - self.rpc_request( - ENGINE_GET_BLOBS_V1, - params, - ENGINE_GET_BLOBS_TIMEOUT * self.execution_timeout_multiplier, - ) - .await - } - pub async fn get_blobs_v2( &self, versioned_hashes: Vec, @@ -1272,7 +1256,6 @@ impl HttpJsonRpc { get_payload_v5: capabilities.contains(ENGINE_GET_PAYLOAD_V5), get_payload_v6: capabilities.contains(ENGINE_GET_PAYLOAD_V6), get_client_version_v1: capabilities.contains(ENGINE_GET_CLIENT_VERSION_V1), - get_blobs_v1: capabilities.contains(ENGINE_GET_BLOBS_V1), get_blobs_v2: capabilities.contains(ENGINE_GET_BLOBS_V2), get_blobs_v3: capabilities.contains(ENGINE_GET_BLOBS_V3), }) diff --git a/beacon_node/execution_layer/src/engine_api/new_payload_request.rs b/beacon_node/execution_layer/src/engine_api/new_payload_request.rs index ba94296b859..b6596fbfc60 100644 --- a/beacon_node/execution_layer/src/engine_api/new_payload_request.rs +++ b/beacon_node/execution_layer/src/engine_api/new_payload_request.rs @@ -1,8 +1,12 @@ use crate::{Error, block_hash::calculate_execution_block_hash, metrics}; use crate::versioned_hashes::verify_versioned_hashes; +use ssz_derive::Encode as SszEncode; +use ssz_types::VariableList; use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash; use superstruct::superstruct; +use tree_hash::TreeHash as _; +use tree_hash_derive::TreeHash; use types::{ BeaconBlockRef, BeaconStateError, EthSpec, ExecutionBlockHash, ExecutionPayload, ExecutionPayloadRef, Hash256, VersionedHash, @@ -14,7 +18,7 @@ use types::{ #[superstruct( variants(Bellatrix, Capella, Deneb, Electra, Fulu, Gloas), - variant_attributes(derive(Clone, Debug, PartialEq),), + variant_attributes(derive(Clone, Debug, PartialEq, SszEncode, TreeHash),), map_into(ExecutionPayload), map_ref_into(ExecutionPayloadRef), cast_error( @@ -26,7 +30,9 @@ use types::{ expr = "BeaconStateError::IncorrectStateVariant" ) )] -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, SszEncode, TreeHash)] +#[tree_hash(enum_behaviour = "transparent")] +#[ssz(enum_behaviour = "transparent")] pub struct NewPayloadRequest<'block, E: EthSpec> { #[superstruct( only(Bellatrix), @@ -44,7 +50,7 @@ pub struct NewPayloadRequest<'block, E: EthSpec> { #[superstruct(only(Gloas), partial_getter(rename = "execution_payload_gloas"))] pub execution_payload: &'block ExecutionPayloadGloas, #[superstruct(only(Deneb, Electra, Fulu, Gloas))] - pub versioned_hashes: Vec, + pub versioned_hashes: VariableList, #[superstruct(only(Deneb, Electra, Fulu, Gloas))] pub parent_beacon_block_root: Hash256, #[superstruct(only(Electra, Fulu, Gloas))] @@ -52,6 +58,11 @@ pub struct NewPayloadRequest<'block, E: EthSpec> { } impl<'block, E: EthSpec> NewPayloadRequest<'block, E> { + /// Root used by EIP-8025 proofs to bind a proof to an `engine_newPayload` request. + pub fn request_root(&self) -> Hash256 { + self.tree_hash_root() + } + pub fn parent_hash(&self) -> ExecutionBlockHash { match self { Self::Bellatrix(payload) => payload.execution_payload.parent_hash, @@ -196,7 +207,8 @@ impl<'a, E: EthSpec> TryFrom> for NewPayloadRequest<'a, E> .blob_kzg_commitments .iter() .map(kzg_commitment_to_versioned_hash) - .collect(), + .collect::>() + .try_into()?, parent_beacon_block_root: block_ref.parent_root, })), BeaconBlockRef::Electra(block_ref) => Ok(Self::Electra(NewPayloadRequestElectra { @@ -206,7 +218,8 @@ impl<'a, E: EthSpec> TryFrom> for NewPayloadRequest<'a, E> .blob_kzg_commitments .iter() .map(kzg_commitment_to_versioned_hash) - .collect(), + .collect::>() + .try_into()?, parent_beacon_block_root: block_ref.parent_root, execution_requests: &block_ref.body.execution_requests, })), @@ -217,7 +230,8 @@ impl<'a, E: EthSpec> TryFrom> for NewPayloadRequest<'a, E> .blob_kzg_commitments .iter() .map(kzg_commitment_to_versioned_hash) - .collect(), + .collect::>() + .try_into()?, parent_beacon_block_root: block_ref.parent_root, execution_requests: &block_ref.body.execution_requests, })), diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index b1b8b0deaaa..378b6275530 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -4,7 +4,7 @@ //! This crate only provides useful functionality for "The Merge", it does not provide any of the //! deposit-contract functionality that the `beacon_node/eth1` crate already provides. -use crate::json_structures::{BlobAndProofV1, BlobAndProofV2, BlobAndProofV3}; +use crate::json_structures::{BlobAndProofV2, BlobAndProofV3}; use crate::payload_cache::PayloadCache; use arc_swap::ArcSwapOption; use auth::{Auth, JwtKey, strip_prefix}; @@ -58,6 +58,7 @@ use types::{ }; mod block_hash; +pub mod eip8025; mod engine_api; pub mod engines; mod keccak; @@ -138,7 +139,9 @@ impl TryFrom> for ProvenancedPayload for Error { } } +impl From for Error { + fn from(e: eip8025::ProofEngineError) -> Self { + Error::ProofEngineError(e) + } +} + pub enum BlockProposalContentsType { Full(BlockProposalContents>), Blinded(BlockProposalContents>), @@ -449,7 +458,7 @@ pub enum SubmitBlindedBlockResponse { type PayloadContentsRefTuple<'a, E> = (ExecutionPayloadRef<'a, E>, Option<&'a BlobsBundle>); struct Inner { - engine: Arc, + engine: Option>, builder: ArcSwapOption, execution_engine_forkchoice_lock: Mutex<()>, suggested_fee_recipient: Option
, @@ -457,6 +466,8 @@ struct Inner { proposers: RwLock>, executor: TaskExecutor, payload_cache: PayloadCache, + proof_engine: Option>, + proof_types: eip8025::types::ProofTypes, /// Track whether the last `newPayload` call errored. /// /// This is used *only* in the informational sync status endpoint, so that a VC using this @@ -468,6 +479,11 @@ struct Inner { pub struct Config { /// Endpoint url for EL nodes that are running the engine api. pub execution_endpoint: Option, + /// Endpoint url for the optional EIP-8025 proof engine. + pub proof_engine_endpoint: Option, + /// Proof types to request from the proof engine when generating proofs. + #[serde(default)] + pub proof_types: eip8025::types::ProofTypes, /// Endpoint urls for services providing the builder api. pub builder_url: Option, /// The timeout value used when making a request to fetch a block header @@ -502,7 +518,9 @@ impl ExecutionLayer { /// Instantiate `Self` with an Execution engine specified in `Config`, using JSON-RPC via HTTP. pub fn from_config(config: Config, executor: TaskExecutor) -> Result { let Config { - execution_endpoint: url, + execution_endpoint, + proof_engine_endpoint, + proof_types, builder_url, builder_user_agent, builder_header_timeout, @@ -515,50 +533,68 @@ impl ExecutionLayer { execution_timeout_multiplier, } = config; - let execution_url = url.ok_or(Error::NoEngine)?; - - // Use the default jwt secret path if not provided via cli. - let secret_file = secret_file.unwrap_or_else(|| default_datadir.join(DEFAULT_JWT_FILE)); - - let jwt_key = if secret_file.exists() { - // Read secret from file if it already exists - std::fs::read_to_string(&secret_file) - .map_err(|e| format!("Failed to read JWT secret file. Error: {:?}", e)) - .and_then(|ref s| { - let secret = JwtKey::from_slice( - &hex::decode(strip_prefix(s.trim_end())) - .map_err(|e| format!("Invalid hex string: {:?}", e))?, - )?; - Ok(secret) - }) - .map_err(Error::InvalidJWTSecret) - } else { - // Create a new file and write a randomly generated secret to it if file does not exist - warn!(path = %secret_file.display(),"No JWT found on disk. Generating"); - std::fs::File::options() - .write(true) - .create_new(true) - .open(&secret_file) - .map_err(|e| format!("Failed to open JWT secret file. Error: {:?}", e)) - .and_then(|mut f| { - let secret = auth::JwtKey::random(); - f.write_all(secret.hex_string().as_bytes()) - .map_err(|e| format!("Failed to write to JWT secret file: {:?}", e))?; - Ok(secret) - }) - .map_err(Error::InvalidJWTSecret) - }?; + if execution_endpoint.is_none() && proof_engine_endpoint.is_none() { + return Err(Error::NoExecutionEndpoint); + } + + let engine = if let Some(execution_url) = execution_endpoint { + // Use the default jwt secret path if not provided via cli. + let secret_file = secret_file.unwrap_or_else(|| default_datadir.join(DEFAULT_JWT_FILE)); + + let jwt_key = if secret_file.exists() { + // Read secret from file if it already exists + std::fs::read_to_string(&secret_file) + .map_err(|e| format!("Failed to read JWT secret file. Error: {:?}", e)) + .and_then(|ref s| { + let secret = JwtKey::from_slice( + &hex::decode(strip_prefix(s.trim_end())) + .map_err(|e| format!("Invalid hex string: {:?}", e))?, + )?; + Ok(secret) + }) + .map_err(Error::InvalidJWTSecret) + } else { + // Create a new file and write a randomly generated secret to it if file does not exist + warn!(path = %secret_file.display(),"No JWT found on disk. Generating"); + std::fs::File::options() + .write(true) + .create_new(true) + .open(&secret_file) + .map_err(|e| format!("Failed to open JWT secret file. Error: {:?}", e)) + .and_then(|mut f| { + let secret = auth::JwtKey::random(); + f.write_all(secret.hex_string().as_bytes()) + .map_err(|e| format!("Failed to write to JWT secret file: {:?}", e))?; + Ok(secret) + }) + .map_err(Error::InvalidJWTSecret) + }?; - let engine: Engine = { let auth = Auth::new(jwt_key, jwt_id, jwt_version); debug!(endpoint = %execution_url, jwt_path = ?secret_file.as_path(),"Loaded execution endpoint"); let api = HttpJsonRpc::new_with_auth(execution_url, auth, execution_timeout_multiplier) .map_err(Error::ApiError)?; - Engine::new(api, executor.clone()) + Some(Arc::new(Engine::new(api, executor.clone()))) + } else { + None }; + let proof_engine = proof_engine_endpoint.map(|url| { + if let Some(idx) = test_utils::parse_mock_index(url.expose_full().as_str()) { + let mock = test_utils::get_mock_proof_engine::(idx).unwrap_or_else(|| { + debug!( + idx, + "No pre-registered mock proof engine; creating one on demand" + ); + test_utils::register_mock_proof_engine::(idx, 0) + }); + Arc::new(eip8025::HttpProofEngine::with_proof_node(mock)) + } else { + Arc::new(eip8025::HttpProofEngine::new(url, None)) + } + }); let inner = Inner { - engine: Arc::new(engine), + engine, builder: ArcSwapOption::empty(), execution_engine_forkchoice_lock: <_>::default(), suggested_fee_recipient, @@ -566,6 +602,8 @@ impl ExecutionLayer { proposers: RwLock::new(HashMap::new()), executor, payload_cache: PayloadCache::default(), + proof_engine, + proof_types, last_new_payload_errored: RwLock::new(false), }; @@ -585,8 +623,16 @@ impl ExecutionLayer { Ok(el) } - fn engine(&self) -> &Arc { - &self.inner.engine + pub fn engine(&self) -> Option<&Arc> { + self.inner.engine.as_ref() + } + + pub fn proof_engine(&self) -> Option> { + self.inner.proof_engine.clone() + } + + pub fn proof_types(&self) -> &eip8025::types::ProofTypes { + &self.inner.proof_types } pub fn builder(&self) -> Option> { @@ -647,20 +693,27 @@ impl ExecutionLayer { /// Get the current difficulty of the PoW chain. pub async fn get_current_difficulty(&self) -> Result, ApiError> { - let block = self - .engine() - .api - .get_block_by_number(BlockByNumberQuery::Tag(LATEST_TAG)) - .await? - .ok_or(ApiError::ExecutionHeadBlockNotFound)?; - Ok(block.total_difficulty) + if let Some(engine) = self.engine() { + let block = engine + .api + .get_block_by_number(BlockByNumberQuery::Tag(LATEST_TAG)) + .await? + .ok_or(ApiError::ExecutionHeadBlockNotFound)?; + Ok(block.total_difficulty) + } else { + Ok(None) + } } /// Gives access to a channel containing if the last engine state is online or not. /// /// This can be called several times. - pub async fn get_responsiveness_watch(&self) -> WatchStream { - self.engine().watch_state().await + pub async fn get_responsiveness_watch(&self) -> Option> { + if let Some(engine) = self.engine() { + Some(engine.watch_state().await) + } else { + None + } } /// Note: this function returns a mutex guard, be careful to avoid deadlocks. @@ -708,7 +761,9 @@ impl ExecutionLayer { /// Performs a single execution of the watchdog routine. pub async fn watchdog_task(&self) { - self.engine().upcheck().await; + if let Some(engine) = self.engine() { + engine.upcheck().await; + } } /// Spawns a routine which cleans the cached proposer data periodically. @@ -751,7 +806,11 @@ impl ExecutionLayer { /// Returns `true` if the execution engine is synced and reachable. pub async fn is_synced(&self) -> bool { - self.engine().is_synced().await + if let Some(engine) = self.engine() { + engine.is_synced().await + } else { + true + } } /// Execution nodes return a "SYNCED" response when they do not have any peers. @@ -762,12 +821,17 @@ impl ExecutionLayer { /// Returns the `Self::is_synced` response if unable to get latest block. pub async fn is_synced_for_notifier(&self, current_slot: Slot) -> bool { let synced = self.is_synced().await; - if synced - && let Ok(Some(block)) = self - .engine() + let block = if let Some(engine) = self.engine() { + engine .api .get_block_by_number(BlockByNumberQuery::Tag(LATEST_TAG)) .await + } else { + Ok(None) + }; + + if synced + && let Ok(Some(block)) = block && block.block_number == 0 && current_slot > 0 { @@ -783,7 +847,12 @@ impl ExecutionLayer { /// be used to give an indication on the HTTP API that the node's execution layer is struggling, /// which can in turn be used by the VC. pub async fn is_offline_or_erroring(&self) -> bool { - self.engine().is_offline().await || *self.inner.last_new_payload_errored.read().await + let engine_offline = if let Some(engine) = self.engine() { + engine.is_offline().await + } else { + false + }; + engine_offline || *self.inner.last_new_payload_errored.read().await } /// Updates the proposer preparation data provided by validators @@ -1311,7 +1380,9 @@ impl ExecutionLayer { .. } = payload_parameters; - self.engine() + let engine = self.engine().ok_or(Error::NoEngine)?; + + engine .request(move |engine| async move { let payload_id = if let Some(id) = engine .get_payload_id(&parent_hash, payload_attributes) @@ -1429,8 +1500,12 @@ impl ExecutionLayer { let block_hash = new_payload_request.block_hash(); let parent_hash = new_payload_request.parent_hash(); - let result = self - .engine() + let Some(engine) = self.engine() else { + *self.inner.last_new_payload_errored.write().await = false; + return Ok(PayloadStatus::Accepted); + }; + + let result = engine .request(|engine| engine.api.new_payload(new_payload_request)) .await; @@ -1458,7 +1533,9 @@ impl ExecutionLayer { /// Update engine sync status. pub async fn upcheck(&self) { - self.engine().upcheck().await; + if let Some(engine) = self.engine() { + engine.upcheck().await; + } } /// Register that the given `validator_index` is going to produce a block at `slot`. @@ -1572,18 +1649,19 @@ impl ExecutionLayer { finalized_block_hash, }; - self.engine() - .set_latest_forkchoice_state(forkchoice_state) - .await; + let result = if let Some(engine) = self.engine() { + engine.set_latest_forkchoice_state(forkchoice_state).await; - let result = self - .engine() - .request(|engine| async move { - engine - .notify_forkchoice_updated(forkchoice_state, payload_attributes) - .await - }) - .await; + engine + .request(|engine| async move { + engine + .notify_forkchoice_updated(forkchoice_state, payload_attributes) + .await + }) + .await + } else { + return Ok(PayloadStatus::Accepted); + }; if let Ok(status) = &result { metrics::inc_counter_vec( @@ -1613,10 +1691,35 @@ impl ExecutionLayer { &self, age_limit: Option, ) -> Result { - self.engine() - .request(|engine| engine.get_engine_capabilities(age_limit)) - .await - .map_err(Into::into) + if let Some(engine) = self.engine() { + engine + .request(|engine| engine.get_engine_capabilities(age_limit)) + .await + .map_err(Into::into) + } else { + Ok(EngineCapabilities { + new_payload_v1: true, + new_payload_v2: true, + new_payload_v3: true, + new_payload_v4: true, + new_payload_v5: true, + forkchoice_updated_v1: true, + forkchoice_updated_v2: true, + forkchoice_updated_v3: true, + forkchoice_updated_v4: true, + get_payload_bodies_by_hash_v1: false, + get_payload_bodies_by_range_v1: false, + get_payload_v1: true, + get_payload_v2: true, + get_payload_v3: true, + get_payload_v4: true, + get_payload_v5: true, + get_payload_v6: true, + get_client_version_v1: false, + get_blobs_v2: false, + get_blobs_v3: false, + }) + } } /// Returns the execution engine version resulting from a call to @@ -1632,27 +1735,33 @@ impl ExecutionLayer { &self, age_limit: Option, ) -> Result, Error> { - let versions = self - .engine() - .request(|engine| engine.get_engine_version(age_limit)) - .await - .map_err(Into::::into)?; - metrics::expose_execution_layer_info(&versions); - - Ok(versions) + if let Some(engine) = self.engine() { + let versions = engine + .request(|engine| engine.get_engine_version(age_limit)) + .await + .map_err(Into::::into)?; + metrics::expose_execution_layer_info(&versions); + Ok(versions) + } else { + Ok(vec![]) + } } pub async fn get_payload_bodies_by_hash( &self, hashes: Vec, ) -> Result>>, Error> { - self.engine() - .request(|engine: &Engine| async move { - engine.api.get_payload_bodies_by_hash_v1(hashes).await - }) - .await - .map_err(Box::new) - .map_err(Error::EngineError) + if let Some(engine) = self.engine() { + engine + .request(|engine: &Engine| async move { + engine.api.get_payload_bodies_by_hash_v1(hashes).await + }) + .await + .map_err(Box::new) + .map_err(Error::EngineError) + } else { + Ok(vec![None; hashes.len()]) + } } pub async fn get_payload_bodies_by_range( @@ -1661,16 +1770,20 @@ impl ExecutionLayer { count: u64, ) -> Result>>, Error> { let _timer = metrics::start_timer(&metrics::EXECUTION_LAYER_GET_PAYLOAD_BODIES_BY_RANGE); - self.engine() - .request(|engine: &Engine| async move { - engine - .api - .get_payload_bodies_by_range_v1(start, count) - .await - }) - .await - .map_err(Box::new) - .map_err(Error::EngineError) + if let Some(engine) = self.engine() { + engine + .request(|engine: &Engine| async move { + engine + .api + .get_payload_bodies_by_range_v1(start, count) + .await + }) + .await + .map_err(Box::new) + .map_err(Error::EngineError) + } else { + Ok(vec![None; count as usize]) + } } /// Fetch a full payload from the execution node. @@ -1722,23 +1835,6 @@ impl ExecutionLayer { } } - pub async fn get_blobs_v1( - &self, - query: Vec, - ) -> Result>>, Error> { - let capabilities = self.get_engine_capabilities(None).await?; - - if capabilities.get_blobs_v1 { - self.engine() - .request(|engine| async move { engine.api.get_blobs_v1(query).await }) - .await - .map_err(Box::new) - .map_err(Error::EngineError) - } else { - Err(Error::GetBlobsNotSupported) - } - } - pub async fn get_blobs_v2( &self, query: Vec, @@ -1747,6 +1843,7 @@ impl ExecutionLayer { if capabilities.get_blobs_v2 { self.engine() + .expect("capabilities only returns get_blobs_v2=true if engine is present") .request(|engine| async move { engine.api.get_blobs_v2(query).await }) .await .map_err(Box::new) @@ -1764,6 +1861,7 @@ impl ExecutionLayer { if capabilities.get_blobs_v3 { self.engine() + .expect("capabilities only returns get_blobs_v3=true if engine is present") .request(|engine| async move { engine.api.get_blobs_v3(query).await }) .await .map_err(Box::new) @@ -1777,11 +1875,15 @@ impl ExecutionLayer { &self, query: BlockByNumberQuery<'_>, ) -> Result, Error> { - self.engine() - .request(|engine| async move { engine.api.get_block_by_number(query).await }) - .await - .map_err(Box::new) - .map_err(Error::EngineError) + if let Some(engine) = self.engine() { + engine + .request(|engine| async move { engine.api.get_block_by_number(query).await }) + .await + .map_err(Box::new) + .map_err(Error::EngineError) + } else { + Ok(None) + } } pub async fn propose_blinded_beacon_block( diff --git a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs index 4a46ce0f880..b05db6e8bdd 100644 --- a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs +++ b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs @@ -565,20 +565,16 @@ impl ExecutionBlockGenerator { self.insert_block(Block::PoS(payload))?; } - // Post-Gloas, the justified and finalized block hashes must be non-zero, since the - // CL always has a known parent_block_hash to reference. - if let Some(head_block) = self.blocks.get(&head_block_hash) - && self - .get_fork_at_timestamp(head_block.timestamp()) - .gloas_enabled() - { + // If Gloas was enabled from genesis, the justified and finalized block hashes must be + // non-zero, since the CL always has a known parent_block_hash to reference. + if self.get_fork_at_timestamp(0).gloas_enabled() { assert!( forkchoice_state.safe_block_hash != ExecutionBlockHash::zero(), - "post-Gloas safe_block_hash must not be zero" + "for Gloas genesis safe_block_hash must not be zero" ); assert!( forkchoice_state.finalized_block_hash != ExecutionBlockHash::zero(), - "post-Gloas finalized_block_hash must not be zero" + "for Gloas genesis finalized_block_hash must not be zero" ); } diff --git a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs index 64eecccc583..9924fbe474c 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -494,20 +494,6 @@ pub async fn handle_rpc( _ => unreachable!(), } } - ENGINE_GET_BLOBS_V1 => { - let versioned_hashes = - get_param::>(params, 0).map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?; - let generator = ctx.execution_block_generator.read(); - // V1: per-element nullable array, positionally matching the request. - let response: Vec>> = versioned_hashes - .iter() - .map(|hash| match generator.get_blob_and_proof(hash) { - Some(BlobAndProof::V1(v1)) => Some(v1), - _ => None, - }) - .collect(); - Ok(serde_json::to_value(response).unwrap()) - } ENGINE_GET_BLOBS_V2 => { let versioned_hashes = get_param::>(params, 0).map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?; diff --git a/beacon_node/execution_layer/src/test_utils/mock_event_stream.rs b/beacon_node/execution_layer/src/test_utils/mock_event_stream.rs new file mode 100644 index 00000000000..400efdc7e2d --- /dev/null +++ b/beacon_node/execution_layer/src/test_utils/mock_event_stream.rs @@ -0,0 +1,116 @@ +//! Assertion helpers for [`MockClientEvent`] broadcast streams. +//! +//! [`MockEventStream`] wraps a `broadcast::Receiver` and provides +//! ergonomic methods for collecting and asserting on events in integration tests, +//! eliminating the `timeout + loop + counter` boilerplate. + +use super::MockClientEvent; +use std::time::Duration; +use tokio::sync::broadcast; + +/// Error type for [`MockEventStream`] assertions. +#[derive(Debug)] +pub struct MockStreamError(String); + +impl std::fmt::Display for MockStreamError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl std::error::Error for MockStreamError {} + +/// Wraps a `broadcast::Receiver` with assertion helpers. +pub struct MockEventStream { + rx: broadcast::Receiver, +} + +impl From> for MockEventStream { + fn from(rx: broadcast::Receiver) -> Self { + Self { rx } + } +} + +impl MockEventStream { + /// Collect `n` events matching `predicate` within `timeout`, or return an error. + pub async fn collect_n( + &mut self, + n: usize, + predicate: impl Fn(&MockClientEvent) -> bool, + timeout: Duration, + ) -> Result, MockStreamError> { + tokio::time::timeout(timeout, async { + let mut collected = Vec::with_capacity(n); + loop { + match self.rx.recv().await { + Ok(event) if predicate(&event) => { + collected.push(event); + if collected.len() >= n { + return Ok(collected); + } + } + Ok(_) => {} + Err(broadcast::error::RecvError::Lagged(skipped)) => { + return Err(MockStreamError(format!( + "event stream lagged, skipped {skipped} events" + ))); + } + Err(broadcast::error::RecvError::Closed) => { + return Err(MockStreamError(format!( + "event stream closed before collecting {n} events (got {})", + collected.len() + ))); + } + } + } + }) + .await + .map_err(|_| { + MockStreamError(format!( + "timed out after {timeout:?} waiting for {n} events" + )) + })? + } + + /// Expect at least `n` `ProofRequested` events within `timeout`. + pub async fn expect_proof_requests( + &mut self, + n: usize, + timeout: Duration, + ) -> Result, MockStreamError> { + self.collect_n( + n, + |e| matches!(e, MockClientEvent::ProofRequested { .. }), + timeout, + ) + .await + } + + /// Expect at least `n` `ProofVerified` events within `timeout`. + pub async fn expect_proof_verified( + &mut self, + n: usize, + timeout: Duration, + ) -> Result, MockStreamError> { + self.collect_n( + n, + |e| matches!(e, MockClientEvent::ProofVerified { .. }), + timeout, + ) + .await + } + + /// Expect at least `n` `ProofFetched` events within `timeout`. + pub async fn expect_proof_fetched( + &mut self, + n: usize, + timeout: Duration, + ) -> Result, MockStreamError> { + self.collect_n( + n, + |e| matches!(e, MockClientEvent::ProofFetched { .. }), + timeout, + ) + .await + } +} diff --git a/beacon_node/execution_layer/src/test_utils/mock_proof_node_client.rs b/beacon_node/execution_layer/src/test_utils/mock_proof_node_client.rs new file mode 100644 index 00000000000..408171d25f6 --- /dev/null +++ b/beacon_node/execution_layer/src/test_utils/mock_proof_node_client.rs @@ -0,0 +1,320 @@ +//! Mock [`ProofNodeClient`] for unit testing [`HttpProofEngine`]. +//! +//! [`MockProofNodeClient`] implements [`ProofNodeClient`] entirely in memory — +//! no HTTP server required. It records received requests, broadcasts proof +//! events after a configurable delay, and always returns `Valid` for verification. +//! +//! [`ProofNodeClient`]: crate::eip8025::ProofNodeClient +//! [`HttpProofEngine`]: crate::eip8025::HttpProofEngine + +use crate::eip8025::errors::ProofEngineError; +use crate::eip8025::proof_node_client::ProofNodeClient; +use crate::eip8025::types::{ProofComplete, ProofEvent}; +use bytes::Bytes; +use futures::stream::Stream; +use parking_lot::Mutex; +use ssz::{Decode, Encode}; +use ssz_derive::{Decode as SszDecode, Encode as SszEncode}; +use ssz_types::VariableList; +use std::collections::HashMap; +use std::marker::PhantomData; +use std::pin::Pin; +use std::sync::{Arc, LazyLock}; +use std::time::Duration; +use superstruct::superstruct; +use tokio::sync::broadcast; +use tokio_stream::StreamExt; +use tokio_stream::wrappers::BroadcastStream; +use tree_hash::TreeHash; +use tree_hash_derive::TreeHash as TreeHashDerive; +use types::execution::eip8025::{ProofAttributes, ProofStatus}; +use types::{ + BeaconStateError, EthSpec, ExecutionPayloadBellatrix, ExecutionPayloadCapella, + ExecutionPayloadDeneb, ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, + ExecutionRequests, Hash256, MainnetEthSpec, VersionedHash, +}; + +/// Owned version of `NewPayloadRequest` used only for SSZ decoding inside the mock. +/// +/// The production `NewPayloadRequest<'block, E>` holds `&'block` references (zero-copy +/// during block processing), which prevents deriving `ssz::Decode`. This local owned +/// superstruct enum mirrors all fork variants with owned fields and is used exclusively +/// to decode the SSZ bytes sent to `request_proofs` and compute `tree_hash_root`. +#[superstruct( + variants(Bellatrix, Capella, Deneb, Electra, Fulu, Gloas), + variant_attributes(derive(SszEncode, SszDecode, TreeHashDerive)), + cast_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ), + partial_getter_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ) +)] +#[derive(SszEncode, SszDecode, TreeHashDerive)] +#[ssz(enum_behaviour = "transparent")] +#[tree_hash(enum_behaviour = "transparent")] +pub struct OwnedNewPayloadRequest { + #[superstruct( + only(Bellatrix), + partial_getter(rename = "execution_payload_bellatrix") + )] + pub execution_payload: ExecutionPayloadBellatrix, + #[superstruct(only(Capella), partial_getter(rename = "execution_payload_capella"))] + pub execution_payload: ExecutionPayloadCapella, + #[superstruct(only(Deneb), partial_getter(rename = "execution_payload_deneb"))] + pub execution_payload: ExecutionPayloadDeneb, + #[superstruct(only(Electra), partial_getter(rename = "execution_payload_electra"))] + pub execution_payload: ExecutionPayloadElectra, + #[superstruct(only(Fulu), partial_getter(rename = "execution_payload_fulu"))] + pub execution_payload: ExecutionPayloadFulu, + #[superstruct(only(Gloas), partial_getter(rename = "execution_payload_gloas"))] + pub execution_payload: ExecutionPayloadGloas, + #[superstruct(only(Deneb, Electra, Fulu, Gloas))] + pub versioned_hashes: VariableList, + #[superstruct(only(Deneb, Electra, Fulu, Gloas))] + pub parent_beacon_block_root: Hash256, + #[superstruct(only(Electra, Fulu, Gloas))] + pub execution_requests: ExecutionRequests, +} + +/// Events emitted by [`MockProofNodeClient`] for each method invocation. +/// +/// Subscribe via [`MockProofNodeClient::subscribe_client_events`] to observe +/// calls in tests without polling shared state. +#[derive(Debug, Clone)] +pub enum MockClientEvent { + /// Emitted when [`ProofNodeClient::request_proofs`] is called. + ProofRequested { + ssz_body: Vec, + proof_attributes: ProofAttributes, + root: Hash256, + }, + /// Emitted when [`ProofNodeClient::verify_proof`] is called. + ProofVerified { root: Hash256, proof_type: u8 }, + /// Emitted when [`ProofNodeClient::get_proof`] is called. + ProofFetched { root: Hash256, proof_type: u8 }, +} + +/// The registry stores a concrete `MockProofNodeClient` as a +/// non-generic stand-in. All fields are Arc-wrapped, so `get_mock_proof_engine` +/// can construct a `MockProofNodeClient` for any `E` by sharing those Arcs. +static MOCK_REGISTRY: LazyLock< + parking_lot::Mutex>>>, +> = LazyLock::new(|| parking_lot::Mutex::new(HashMap::new())); + +/// Register a mock at `index`. Must be called before `ExecutionLayer::from_config`. +/// +/// Stores the mock as `MainnetEthSpec` internally and returns a `MockProofNodeClient` +/// that shares the same Arc-backed state but decodes SSZ using `E`. +pub fn register_mock_proof_engine( + index: usize, + callback_delay_ms: u64, +) -> MockProofNodeClient { + let stored = Arc::new(MockProofNodeClient::::new( + callback_delay_ms, + )); + let typed = MockProofNodeClient:: { + requests: stored.requests.clone(), + event_tx: stored.event_tx.clone(), + call_tx: stored.call_tx.clone(), + proof_generation_delay: stored.proof_generation_delay, + _phantom: PhantomData, + }; + MOCK_REGISTRY.lock().insert(index, stored); + typed +} + +/// Fetch a registered mock by index as a `MockProofNodeClient`. +/// +/// Constructs the typed client by sharing the Arc fields of the stored +/// `MockProofNodeClient`, so all state (requests, events) is shared. +pub fn get_mock_proof_engine(index: usize) -> Option> { + MOCK_REGISTRY + .lock() + .get(&index) + .map(|stored| MockProofNodeClient:: { + requests: stored.requests.clone(), + event_tx: stored.event_tx.clone(), + call_tx: stored.call_tx.clone(), + proof_generation_delay: stored.proof_generation_delay, + _phantom: PhantomData, + }) +} + +/// URL encoding an index: `"http://mock/{n}/"`. +pub fn mock_proof_engine_url(index: usize) -> String { + format!("http://mock/{}/", index) +} + +/// Parse the index from a mock URL. Returns `None` for non-mock URLs. +pub fn parse_mock_index(url: &str) -> Option { + url.strip_prefix("http://mock/").map(|s| { + let s = s.strip_suffix('/').unwrap_or(s); + if s.is_empty() { + 0 + } else { + s.parse().unwrap_or(0) + } + }) +} + +/// Build a test SSZ body encoding a `NewPayloadRequestFulu` with the given +/// parent beacon block root. Returns `(ssz_bytes, expected_tree_hash_root)`. +pub fn make_test_fulu_ssz(parent_root: Hash256) -> (Vec, Hash256) { + let request = OwnedNewPayloadRequestFulu:: { + execution_payload: ExecutionPayloadFulu::default(), + versioned_hashes: VariableList::default(), + parent_beacon_block_root: parent_root, + execution_requests: ExecutionRequests::default(), + }; + let request = OwnedNewPayloadRequest::Fulu(request); + (request.as_ssz_bytes(), request.tree_hash_root()) +} + +/// In-memory proof node client for testing, generic over [`EthSpec`]. +/// +/// Each call to [`request_proofs`] decodes the SSZ body using `E`, records the +/// raw SSZ body, and schedules a [`ProofEvent::ProofComplete`] event for each +/// requested proof type after `callback_delay_ms` milliseconds. +/// +/// Call [`subscribe_client_events`] to receive a [`MockClientEvent`] stream +/// that fires once per method invocation — useful for asserting that the proof +/// engine issues the expected calls without polling shared state. +/// +/// [`request_proofs`]: MockProofNodeClient::request_proofs +/// [`subscribe_client_events`]: MockProofNodeClient::subscribe_client_events +pub struct MockProofNodeClient { + /// Received SSZ request bodies in order of arrival. + requests: Arc>>>, + /// Broadcast channel for in-memory SSE events. + event_tx: broadcast::Sender, + /// Broadcast channel for method-invocation events. + call_tx: broadcast::Sender, + /// Delay in milliseconds before broadcasting proof complete events. + proof_generation_delay: u64, + _phantom: PhantomData, +} + +impl Clone for MockProofNodeClient { + fn clone(&self) -> Self { + Self { + requests: self.requests.clone(), + event_tx: self.event_tx.clone(), + call_tx: self.call_tx.clone(), + proof_generation_delay: self.proof_generation_delay, + _phantom: PhantomData, + } + } +} + +impl MockProofNodeClient { + /// Create a new unregistered mock client. + /// + /// `callback_delay_ms` controls how long after `request_proofs` the + /// proof complete events are broadcast. + pub fn new(callback_delay_ms: u64) -> Self { + let (event_tx, _) = broadcast::channel(256); + let (call_tx, _) = broadcast::channel(256); + Self { + requests: Arc::new(Mutex::new(Vec::new())), + event_tx, + call_tx, + proof_generation_delay: callback_delay_ms, + _phantom: PhantomData, + } + } + + /// Returns the number of proof requests received. + pub fn request_count(&self) -> usize { + self.requests.lock().len() + } + + /// Returns a clone of all received SSZ request bodies. + pub fn received_requests(&self) -> Vec> { + self.requests.lock().clone() + } + + /// Subscribe to method-invocation events. + /// + /// Each call to `request_proofs`, `verify_proof`, or `get_proof` on this + /// client sends one [`MockClientEvent`] to all active receivers. Use this + /// in tests to assert that the proof engine issues the expected calls. + pub fn subscribe_client_events(&self) -> broadcast::Receiver { + self.call_tx.subscribe() + } +} + +#[async_trait::async_trait] +impl ProofNodeClient for MockProofNodeClient { + async fn request_proofs( + &self, + ssz_body: Vec, + proof_attributes: ProofAttributes, + ) -> Result { + let root = OwnedNewPayloadRequest::::from_ssz_bytes(&ssz_body) + .map_err(|e| ProofEngineError::InvalidPayload(format!("SSZ decode failed: {e:?}")))? + .tree_hash_root(); + + self.requests.lock().push(ssz_body.clone()); + + let _ = self.call_tx.send(MockClientEvent::ProofRequested { + ssz_body, + proof_attributes: proof_attributes.clone(), + root, + }); + + let event_tx = self.event_tx.clone(); + let delay = self.proof_generation_delay; + let proof_types = proof_attributes.proof_types.clone(); + + tokio::spawn(async move { + tokio::time::sleep(Duration::from_millis(delay)).await; + for proof_type in proof_types { + let _ = event_tx.send(ProofEvent::ProofComplete(ProofComplete { + new_payload_request_root: root, + proof_type, + })); + } + }); + + Ok(root) + } + + async fn verify_proof( + &self, + root: Hash256, + proof_type: u8, + _proof_data: &[u8], + ) -> Result { + let _ = self + .call_tx + .send(MockClientEvent::ProofVerified { root, proof_type }); + Ok(ProofStatus::Valid) + } + + async fn get_proof(&self, root: Hash256, proof_type: u8) -> Result { + let _ = self + .call_tx + .send(MockClientEvent::ProofFetched { root, proof_type }); + Ok(Bytes::from(vec![0xDE, 0xAD, 0xBE, 0xEF])) + } + + fn subscribe_proof_events( + &self, + filter_root: Option, + ) -> Pin> + Send + '_>> { + let rx = self.event_tx.subscribe(); + let stream = BroadcastStream::new(rx).filter_map(move |result| match result { + Ok(event) => { + if filter_root.is_some_and(|root| event.new_payload_request_root() != root) { + return None; + } + Some(Ok(event)) + } + Err(_) => None, + }); + Box::pin(stream) + } +} diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index 4eb03778f80..f9b32cd7292 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -33,7 +33,12 @@ pub use execution_block_generator::{ }; pub use hook::Hook; pub use mock_builder::{MockBuilder, Operation, mock_builder_extra_data}; +pub use mock_event_stream::{MockEventStream, MockStreamError}; pub use mock_execution_layer::MockExecutionLayer; +pub use mock_proof_node_client::{ + MockClientEvent, MockProofNodeClient, OwnedNewPayloadRequest, get_mock_proof_engine, + make_test_fulu_ssz, mock_proof_engine_url, parse_mock_index, register_mock_proof_engine, +}; pub const DEFAULT_JWT_SECRET: [u8; 32] = [42; 32]; pub const DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI: u128 = 10_000_000_000_000_000; @@ -57,7 +62,6 @@ pub const DEFAULT_ENGINE_CAPABILITIES: EngineCapabilities = EngineCapabilities { get_payload_v5: true, get_payload_v6: true, get_client_version_v1: true, - get_blobs_v1: true, get_blobs_v2: true, get_blobs_v3: true, }; @@ -74,7 +78,9 @@ mod execution_block_generator; mod handle_rpc; mod hook; mod mock_builder; +mod mock_event_stream; mod mock_execution_layer; +pub(crate) mod mock_proof_node_client; /// Configuration for the MockExecutionLayer. #[derive(Clone)] diff --git a/beacon_node/http_api/src/beacon/execution_payload_bid.rs b/beacon_node/http_api/src/beacon/execution_payload_bid.rs new file mode 100644 index 00000000000..f6041b55c8a --- /dev/null +++ b/beacon_node/http_api/src/beacon/execution_payload_bid.rs @@ -0,0 +1,112 @@ +use crate::task_spawner::{Priority, TaskSpawner}; +use crate::utils::{ + ChainFilter, EthV1Filter, NetworkTxFilter, ResponseFilter, TaskSpawnerFilter, + publish_pubsub_message, +}; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use bytes::Bytes; +use lighthouse_network::PubsubMessage; +use network::NetworkMessage; +use ssz::Decode; +use std::sync::Arc; +use tokio::sync::mpsc::UnboundedSender; +use tracing::{debug, warn}; +use types::SignedExecutionPayloadBid; +use warp::{Filter, Rejection, Reply, hyper::Body, hyper::Response}; + +// POST /eth/v1/beacon/execution_payload_bid (SSZ) +pub(crate) fn post_beacon_execution_payload_bid_ssz( + eth_v1: EthV1Filter, + task_spawner_filter: TaskSpawnerFilter, + chain_filter: ChainFilter, + network_tx_filter: NetworkTxFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("beacon")) + .and(warp::path("execution_payload_bid")) + .and(warp::path::end()) + .and(warp::body::bytes()) + .and(task_spawner_filter) + .and(chain_filter) + .and(network_tx_filter) + .then( + |body_bytes: Bytes, + task_spawner: TaskSpawner, + chain: Arc>, + network_tx: UnboundedSender>| { + task_spawner.blocking_response_task(Priority::P0, move || { + let bid = SignedExecutionPayloadBid::::from_ssz_bytes(&body_bytes) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}")) + })?; + publish_execution_payload_bid(bid, &chain, &network_tx) + }) + }, + ) + .boxed() +} + +// POST /eth/v1/beacon/execution_payload_bid +pub(crate) fn post_beacon_execution_payload_bid( + eth_v1: EthV1Filter, + task_spawner_filter: TaskSpawnerFilter, + chain_filter: ChainFilter, + network_tx_filter: NetworkTxFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("beacon")) + .and(warp::path("execution_payload_bid")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(task_spawner_filter) + .and(chain_filter) + .and(network_tx_filter) + .then( + |bid: SignedExecutionPayloadBid, + task_spawner: TaskSpawner, + chain: Arc>, + network_tx: UnboundedSender>| { + task_spawner.blocking_response_task(Priority::P0, move || { + publish_execution_payload_bid(bid, &chain, &network_tx) + }) + }, + ) + .boxed() +} + +pub fn publish_execution_payload_bid( + bid: SignedExecutionPayloadBid, + chain: &Arc>, + network_tx: &UnboundedSender>, +) -> Result, Rejection> { + let slot = bid.slot(); + let builder_index = bid.message.builder_index; + + if !chain.spec.is_gloas_scheduled() { + return Err(warp_utils::reject::custom_bad_request( + "Execution payload bids are not supported before the Gloas fork".into(), + )); + } + + debug!( + %slot, + builder_index, + "Publishing signed execution payload bid to network" + ); + + let gossip_verified_bid = chain + .verify_payload_bid_for_gossip(Arc::new(bid)) + .map_err(|e| { + warn!(%slot, error = ?e, "Execution payload bid failed gossip verification"); + warp_utils::reject::custom_bad_request(format!("bid failed gossip verification: {e}")) + })?; + + let bid_for_gossip = gossip_verified_bid.signed_bid.as_ref().clone(); + + publish_pubsub_message( + network_tx, + PubsubMessage::ExecutionPayloadBid(Box::new(bid_for_gossip)), + )?; + + Ok(warp::reply().into_response()) +} diff --git a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs index 2e7fe693d60..d8813b0db58 100644 --- a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs +++ b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs @@ -11,7 +11,6 @@ use beacon_chain::payload_envelope_verification::EnvelopeError; use beacon_chain::{BeaconChain, BeaconChainTypes, NotifyExecutionLayer}; use bytes::Bytes; use eth2::types as api_types; -use eth2::{CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER}; use lighthouse_network::PubsubMessage; use network::NetworkMessage; use ssz::{Decode, Encode}; @@ -36,10 +35,6 @@ pub(crate) fn post_beacon_execution_payload_envelope_ssz( .and(warp::path("beacon")) .and(warp::path("execution_payload_envelope")) .and(warp::path::end()) - .and(warp::header::exact( - CONTENT_TYPE_HEADER, - SSZ_CONTENT_TYPE_HEADER, - )) .and(warp::body::bytes()) .and(task_spawner_filter) .and(chain_filter) diff --git a/beacon_node/http_api/src/beacon/mod.rs b/beacon_node/http_api/src/beacon/mod.rs index 9ec1c476f61..db0062c14f7 100644 --- a/beacon_node/http_api/src/beacon/mod.rs +++ b/beacon_node/http_api/src/beacon/mod.rs @@ -1,3 +1,4 @@ +pub mod execution_payload_bid; pub mod execution_payload_envelope; pub mod pool; pub mod states; diff --git a/beacon_node/http_api/src/beacon/pool.rs b/beacon_node/http_api/src/beacon/pool.rs index 3525567eb42..1b60d018d5a 100644 --- a/beacon_node/http_api/src/beacon/pool.rs +++ b/beacon_node/http_api/src/beacon/pool.rs @@ -16,6 +16,7 @@ use eth2::types::{AttestationPoolQuery, EndpointVersion, Failure, GenericRespons use lighthouse_network::PubsubMessage; use network::NetworkMessage; use operation_pool::ReceivedPreCapella; +use serde::{Deserialize, Serialize}; use slot_clock::SlotClock; use ssz::{Decode, Encode}; use std::collections::HashSet; @@ -24,8 +25,8 @@ use tokio::sync::mpsc::UnboundedSender; use tracing::{debug, error, info, warn}; use types::{ Attestation, AttestationData, AttesterSlashing, ForkName, PayloadAttestationMessage, - ProposerSlashing, SignedBlsToExecutionChange, SignedVoluntaryExit, SingleAttestation, - SyncCommitteeMessage, + ProofStatus, ProposerSlashing, SignedBlsToExecutionChange, SignedExecutionProof, + SignedVoluntaryExit, SingleAttestation, SyncCommitteeMessage, }; use warp::filters::BoxedFilter; use warp::{Filter, Reply}; @@ -45,6 +46,123 @@ pub type BeaconPoolPathAnyFilter = BoxedFilter<( Arc>, )>; +#[derive(Debug, Deserialize)] +pub struct SubmitExecutionProofsRequest { + pub proofs: Vec, +} + +#[derive(Debug, Serialize)] +pub struct SubmitExecutionProofsResponse { + pub statuses: Vec, +} + +#[derive(Debug, Serialize)] +pub struct SubmitExecutionProofStatus { + pub status: ProofStatus, + pub request_root: types::Hash256, + pub block_root: Option, + pub valid_proof_type_count: usize, + pub proof_backed_payload_promotion: bool, +} + +/// POST beacon/pool/execution_proofs +pub fn post_beacon_pool_execution_proofs( + network_tx_filter: &NetworkTxFilter, + beacon_pool_path: &BeaconPoolPathFilter, +) -> ResponseFilter { + beacon_pool_path + .clone() + .and(warp::path("execution_proofs")) + .and(warp::path::end()) + .and(warp_utils::json::json()) + .and(network_tx_filter.clone()) + .then( + |_task_spawner: TaskSpawner, + chain: Arc>, + request: SubmitExecutionProofsRequest, + network_tx: UnboundedSender>| async move { + convert_rejection( + async move { + if chain + .execution_layer + .as_ref() + .and_then(|execution_layer| execution_layer.proof_engine()) + .is_none() + { + return Err(warp_utils::reject::custom_server_error( + "proof engine not configured".to_string(), + )); + } + + if request.proofs.is_empty() { + return Err(warp_utils::reject::custom_bad_request( + "no execution proofs provided".to_string(), + )); + } + + let mut statuses = Vec::with_capacity(request.proofs.len()); + let mut failures = vec![]; + + for (index, proof) in request.proofs.iter().enumerate() { + match chain.verify_and_observe_execution_proof(proof, None).await { + Ok(observation) => { + if observation.status == ProofStatus::Invalid { + failures.push(Failure::new( + index, + "execution proof is invalid".to_string(), + )); + } else if observation.status == ProofStatus::Valid + || (observation.status == ProofStatus::Accepted + && observation.valid_proof_type_count > 0) + { + utils::publish_pubsub_message( + &network_tx, + PubsubMessage::ExecutionProof(Arc::new(proof.clone())), + )?; + } + statuses.push(SubmitExecutionProofStatus { + status: observation.status, + request_root: observation.request_root, + block_root: observation.block_root, + valid_proof_type_count: observation.valid_proof_type_count, + proof_backed_payload_promotion: observation + .proof_backed_payload_promotion, + }); + } + Err(error) => { + warn!( + ?error, + request_root = ?proof.request_root(), + proof_type = proof.proof_type(), + validator_index = proof.validator_index(), + "Execution proof validation failed" + ); + failures + .push(Failure::new(index, format!("invalid: {error:?}"))); + } + } + } + + if failures.is_empty() { + Ok( + warp::reply::json(&SubmitExecutionProofsResponse { statuses }) + .into_response(), + ) + } else { + Err(warp_utils::reject::indexed_bad_request( + "some execution proofs failed to verify".into(), + failures, + )) + } + } + .await, + ) + .await + }, + ) + .boxed() +} + /// POST beacon/pool/bls_to_execution_changes pub fn post_beacon_pool_bls_to_execution_changes( network_tx_filter: &NetworkTxFilter, diff --git a/beacon_node/http_api/src/block_id.rs b/beacon_node/http_api/src/block_id.rs index ca980b96a4e..8843541c11e 100644 --- a/beacon_node/http_api/src/block_id.rs +++ b/beacon_node/http_api/src/block_id.rs @@ -455,20 +455,18 @@ impl BlockId { warp_utils::reject::custom_not_found(format!("no blobs stored for block {root}")) })?; - let blob_sidecar_list_filtered = match indices { - Some(vec) => { - let list: Vec<_> = vec - .into_iter() - .flat_map(|index| blob_sidecar_list.get(index as usize).cloned()) - .collect(); - - BlobSidecarList::new(list, max_blobs_per_block) - .map_err(|e| warp_utils::reject::custom_server_error(format!("{:?}", e)))? - } + let blob_sidecar_list: Vec<_> = blob_sidecar_list.into_iter().collect(); + + let blob_sidecar_list = match indices { + Some(indices) => indices + .into_iter() + .filter_map(|i| blob_sidecar_list.get(i as usize).cloned()) + .collect(), None => blob_sidecar_list, }; - Ok(blob_sidecar_list_filtered) + BlobSidecarList::new(blob_sidecar_list, max_blobs_per_block) + .map_err(|e| warp_utils::reject::custom_server_error(format!("{:?}", e))) } fn get_blobs_from_data_columns( diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 74bf1ccd764..de8640cdb6e 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -36,6 +36,9 @@ mod validator_inclusion; mod validators; mod version; +use crate::beacon::execution_payload_bid::{ + post_beacon_execution_payload_bid, post_beacon_execution_payload_bid_ssz, +}; use crate::beacon::execution_payload_envelope::{ get_beacon_execution_payload_envelope, post_beacon_execution_payload_envelope, post_beacon_execution_payload_envelope_ssz, @@ -229,6 +232,7 @@ pub fn prometheus_metrics() -> warp::filters::log::Log( let post_beacon_pool_bls_to_execution_changes = post_beacon_pool_bls_to_execution_changes(&network_tx_filter, &beacon_pool_path); + // POST beacon/pool/execution_proofs + let post_beacon_pool_execution_proofs = + post_beacon_pool_execution_proofs(&network_tx_filter, &beacon_pool_path); + // POST validator/proposer_preferences (JSON) let post_validator_proposer_preferences = post_validator_proposer_preferences( eth_v1.clone(), @@ -1555,6 +1563,22 @@ pub fn serve( network_tx_filter.clone(), ); + // POST beacon/execution_payload_bid + let post_beacon_execution_payload_bid = post_beacon_execution_payload_bid( + eth_v1.clone(), + task_spawner_filter.clone(), + chain_filter.clone(), + network_tx_filter.clone(), + ); + + // POST beacon/execution_payload_bid (SSZ) + let post_beacon_execution_payload_bid_ssz = post_beacon_execution_payload_bid_ssz( + eth_v1.clone(), + task_spawner_filter.clone(), + chain_filter.clone(), + network_tx_filter.clone(), + ); + // GET beacon/execution_payload_envelope/{block_id} let get_beacon_execution_payload_envelope = get_beacon_execution_payload_envelope( eth_v1.clone(), @@ -3445,6 +3469,7 @@ pub fn serve( .uor(post_beacon_blinded_blocks_ssz) .uor(post_beacon_blinded_blocks_v2_ssz) .uor(post_beacon_execution_payload_envelope_ssz) + .uor(post_beacon_execution_payload_bid_ssz) .uor(post_beacon_pool_payload_attestations_ssz) .uor(post_validator_proposer_preferences_ssz), ) @@ -3459,8 +3484,10 @@ pub fn serve( .uor(post_beacon_pool_sync_committees) .uor(post_beacon_pool_payload_attestations) .uor(post_beacon_pool_bls_to_execution_changes) + .uor(post_beacon_pool_execution_proofs) .uor(post_validator_proposer_preferences) .uor(post_beacon_execution_payload_envelope) + .uor(post_beacon_execution_payload_bid) .uor(post_beacon_state_validators) .uor(post_beacon_state_validator_balances) .uor(post_beacon_state_validator_identities) diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index ca4ab855245..b46576ddadb 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -1,7 +1,6 @@ use crate::metrics; use std::future::Future; -use beacon_chain::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use beacon_chain::block_verification_types::{AsBlock, LookupBlock}; use beacon_chain::data_column_verification::GossipVerifiedDataColumn; use beacon_chain::validator_monitor::get_block_delay_ms; @@ -26,13 +25,12 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; use tokio::sync::mpsc::UnboundedSender; -use tracing::{Span, debug, debug_span, error, field, info, instrument, warn}; +use tracing::{Span, debug, error, field, info, instrument, warn}; use tree_hash::TreeHash; use types::{ - AbstractExecPayload, BeaconBlockRef, BlobSidecar, BlobsList, BlockImportSource, - DataColumnSidecar, DataColumnSubnetId, EthSpec, ExecPayload, ExecutionBlockHash, ForkName, - FullPayload, FullPayloadBellatrix, Hash256, KzgProofs, SignedBeaconBlock, - SignedBlindedBeaconBlock, + AbstractExecPayload, BeaconBlockRef, BlobsList, BlockImportSource, DataColumnSidecar, + DataColumnSubnetId, EthSpec, ExecPayload, ExecutionBlockHash, ForkName, FullPayload, + FullPayloadBellatrix, Hash256, KzgProofs, SignedBeaconBlock, SignedBlindedBeaconBlock, }; use warp::{Rejection, Reply, reply::Response}; @@ -195,23 +193,8 @@ pub async fn publish_block>( Ok(()) }; - // Wait for blobs/columns to get gossip verified before proceeding further as we need them for import. - let (gossip_verified_blobs, gossip_verified_columns) = build_sidecar_task_handle.await?; - - for blob in gossip_verified_blobs.into_iter().flatten() { - publish_blob_sidecars(network_tx, &blob).map_err(|_| { - warp_utils::reject::custom_server_error("unable to publish blob sidecars".into()) - })?; - if let Err(e) = Box::pin(chain.process_gossip_blob(blob)).await { - let msg = format!("Invalid blob: {e}"); - return if let BroadcastValidation::Gossip = validation_level { - Err(warp_utils::reject::broadcast_without_import(msg)) - } else { - error!(reason = &msg, "Invalid blob provided to HTTP API"); - Err(warp_utils::reject::custom_bad_request(msg)) - }; - } - } + // Wait for columns to get gossip verified before proceeding further as we need them for import. + let gossip_verified_columns = build_sidecar_task_handle.await?; if !gossip_verified_columns.is_empty() { if let Some(data_column_publishing_delay) = data_column_publishing_delay_for_testing { @@ -342,18 +325,9 @@ pub async fn publish_block>( } } -type BuildDataSidecarTaskResult = Result< - ( - Vec>>, - Vec>, - ), - Rejection, ->; +type BuildDataSidecarTaskResult = Result>, Rejection>; -/// Convert blobs to either: -/// -/// 1. Blob sidecars if prior to peer DAS, or -/// 2. Data column sidecars if post peer DAS. +/// Convert blobs to data column sidecars. fn spawn_build_data_sidecar_task( chain: Arc>, block: Arc>>, @@ -365,22 +339,9 @@ fn spawn_build_data_sidecar_task( .spawn_blocking_handle( move || { let Some((kzg_proofs, blobs)) = proofs_and_blobs else { - return Ok((vec![], vec![])); + return Ok(vec![]); }; - let _span = debug_span!("build_data_sidecars").entered(); - - let peer_das_enabled = chain.spec.is_peer_das_enabled_for_epoch(block.epoch()); - if !peer_das_enabled { - // Pre-PeerDAS: construct blob sidecars for the network. - let gossip_verified_blobs = - build_gossip_verified_blobs(&chain, &block, blobs, kzg_proofs)?; - Ok((gossip_verified_blobs, vec![])) - } else { - // Post PeerDAS: construct data columns. - let gossip_verified_data_columns = - build_data_columns(&chain, &block, blobs, kzg_proofs)?; - Ok((vec![], gossip_verified_data_columns)) - } + build_data_columns(&chain, &block, blobs, kzg_proofs) }, "build_data_sidecars", ) @@ -424,76 +385,6 @@ fn build_data_columns( Ok(gossip_verified_data_columns) } -fn build_gossip_verified_blobs( - chain: &BeaconChain, - block: &SignedBeaconBlock>, - blobs: BlobsList, - kzg_proofs: KzgProofs, -) -> Result>>, Rejection> { - let slot = block.slot(); - let gossip_verified_blobs = kzg_proofs - .into_iter() - .zip(blobs) - .enumerate() - .map(|(i, (proof, unverified_blob))| { - let timer = metrics::start_timer( - &beacon_chain::metrics::BLOB_SIDECAR_INCLUSION_PROOF_COMPUTATION, - ); - let blob_sidecar = BlobSidecar::new(i, unverified_blob, block, proof) - .map(Arc::new) - .map_err(|e| { - error!( - error = ?e, - blob_index = i, - %slot, - "Invalid blob - not publishing block" - ); - warp_utils::reject::custom_bad_request(format!("{e:?}")) - })?; - drop(timer); - - let gossip_verified_blob = - GossipVerifiedBlob::new(blob_sidecar.clone(), blob_sidecar.index, chain); - - match gossip_verified_blob { - Ok(blob) => Ok(Some(blob)), - Err(GossipBlobError::RepeatBlob { proposer, .. }) => { - // Log the error but do not abort publication, we may need to publish the block - // or some of the other blobs if the block & blobs are only partially published - // by the other publisher. - debug!( - blob_index = blob_sidecar.index, - %slot, - proposer, - "Blob for publication already known" - ); - Ok(None) - } - Err(e) => { - error!( - blob_index = blob_sidecar.index, - %slot, - error = ?e, - "Blob for publication is gossip-invalid" - ); - Err(warp_utils::reject::custom_bad_request(e.to_string())) - } - } - }) - .collect::, Rejection>>()?; - - Ok(gossip_verified_blobs) -} - -fn publish_blob_sidecars( - sender_clone: &UnboundedSender>, - blob: &GossipVerifiedBlob, -) -> Result<(), BlockError> { - let pubsub_message = PubsubMessage::BlobSidecar(Box::new((blob.index(), blob.clone_blob()))); - crate::utils::publish_pubsub_message(sender_clone, pubsub_message) - .map_err(|_| BlockError::BeaconChainError(Box::new(BeaconChainError::UnableToPublish))) -} - pub(crate) fn publish_column_sidecars( sender_clone: &UnboundedSender>, data_column_sidecars: &[GossipVerifiedDataColumn], diff --git a/beacon_node/http_api/tests/broadcast_validation_tests.rs b/beacon_node/http_api/tests/broadcast_validation_tests.rs index a189be1cfcb..98629a1c5e1 100644 --- a/beacon_node/http_api/tests/broadcast_validation_tests.rs +++ b/beacon_node/http_api/tests/broadcast_validation_tests.rs @@ -1587,7 +1587,7 @@ pub async fn block_seen_on_gossip_without_blobs_or_columns() { let state = tester.harness.get_current_state(); let fork_name = state.fork_name(&tester.harness.spec).unwrap(); // Gloas blocks don't carry blobs (execution data comes via envelopes). - if !fork_name.deneb_enabled() || fork_name.gloas_enabled() { + if !fork_name.fulu_enabled() || fork_name.gloas_enabled() { return; } @@ -1647,7 +1647,7 @@ pub async fn block_seen_on_gossip_without_blobs_or_columns() { /// This test checks that an HTTP POST request with the block & blobs/columns succeeds with a 200 response /// even if the block has already been seen on gossip without all blobs/columns. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -pub async fn block_seen_on_gossip_with_some_blobs_or_columns() { +pub async fn block_seen_on_gossip_with_columns() { let validation_level: Option = Some(BroadcastValidation::Gossip); // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing @@ -1658,7 +1658,7 @@ pub async fn block_seen_on_gossip_with_some_blobs_or_columns() { let state = tester.harness.get_current_state(); let fork_name = state.fork_name(&tester.harness.spec).unwrap(); // Gloas blocks don't carry blobs (execution data comes via envelopes). - if !fork_name.deneb_enabled() || fork_name.gloas_enabled() { + if !fork_name.fulu_enabled() || fork_name.gloas_enabled() { return; } @@ -1690,9 +1690,6 @@ pub async fn block_seen_on_gossip_with_some_blobs_or_columns() { blobs.0.len() ); - let partial_kzg_proofs = [*blobs.0.first().unwrap()]; - let partial_blobs = [blobs.1.first().unwrap().clone()]; - // Simulate the block being seen on gossip. block .clone() @@ -1702,12 +1699,7 @@ pub async fn block_seen_on_gossip_with_some_blobs_or_columns() { // Simulate some of the blobs being seen on gossip. tester .harness - .process_gossip_blobs_or_columns( - &block, - partial_blobs.iter(), - partial_kzg_proofs.iter(), - Some(get_custody_columns(&tester, block.slot())), - ) + .process_gossip_columns(&block, Some(get_custody_columns(&tester, block.slot()))) .await; // It should not yet be added to fork choice because all blobs have not been seen. @@ -1740,7 +1732,7 @@ pub async fn block_seen_on_gossip_with_some_blobs_or_columns() { /// This test checks that an HTTP POST request with the block & blobs/columns succeeds with a 200 response /// even if the blobs/columns have already been seen on gossip. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -pub async fn blobs_or_columns_seen_on_gossip_without_block() { +pub async fn columns_seen_on_gossip_without_block() { let spec = test_spec::(); let validation_level: Option = Some(BroadcastValidation::Gossip); @@ -1752,7 +1744,7 @@ pub async fn blobs_or_columns_seen_on_gossip_without_block() { let state = tester.harness.get_current_state(); let fork_name = state.fork_name(&tester.harness.spec).unwrap(); // Gloas blocks don't carry blobs (execution data comes via envelopes). - if !fork_name.deneb_enabled() || fork_name.gloas_enabled() { + if !fork_name.fulu_enabled() || fork_name.gloas_enabled() { return; } @@ -1778,12 +1770,7 @@ pub async fn blobs_or_columns_seen_on_gossip_without_block() { // Simulate the blobs being seen on gossip. tester .harness - .process_gossip_blobs_or_columns( - &block, - blobs.iter(), - kzg_proofs.iter(), - Some(get_custody_columns(&tester, block.slot())), - ) + .process_gossip_columns(&block, Some(get_custody_columns(&tester, block.slot()))) .await; // It should not yet be added to fork choice because the block has not been seen. @@ -1816,7 +1803,7 @@ pub async fn blobs_or_columns_seen_on_gossip_without_block() { /// This test checks that an HTTP POST request with the block succeeds with a 200 response /// if just the blobs have already been seen on gossip. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn blobs_or_columns_seen_on_gossip_without_block_and_no_http_blobs_or_columns() { +async fn columns_seen_on_gossip_without_block_and_no_http_columns() { let validation_level: Option = Some(BroadcastValidation::Gossip); // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing @@ -1827,7 +1814,7 @@ async fn blobs_or_columns_seen_on_gossip_without_block_and_no_http_blobs_or_colu let state = tester.harness.get_current_state(); let fork_name = state.fork_name(&tester.harness.spec).unwrap(); // Gloas blocks don't carry blobs (execution data comes via envelopes). - if !fork_name.deneb_enabled() || fork_name.gloas_enabled() { + if !fork_name.fulu_enabled() || fork_name.gloas_enabled() { return; } @@ -1848,18 +1835,13 @@ async fn blobs_or_columns_seen_on_gossip_without_block_and_no_http_blobs_or_colu let state_a = tester.harness.get_current_state(); let ((block, blobs), _) = tester.harness.make_block(state_a, slot_b).await; - let (kzg_proofs, blobs) = blobs.expect("should have some blobs"); + let (_, blobs) = blobs.expect("should have some blobs"); assert!(!blobs.is_empty()); // Simulate the blobs being seen on gossip. tester .harness - .process_gossip_blobs_or_columns( - &block, - blobs.iter(), - kzg_proofs.iter(), - Some(get_custody_columns(&tester, block.slot())), - ) + .process_gossip_columns(&block, Some(get_custody_columns(&tester, block.slot()))) .await; // It should not yet be added to fork choice because the block has not been seen. @@ -1893,7 +1875,7 @@ async fn blobs_or_columns_seen_on_gossip_without_block_and_no_http_blobs_or_colu } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn slashable_blobs_or_columns_seen_on_gossip_cause_failure() { +async fn slashable_columns_seen_on_gossip_cause_failure() { let validation_level: Option = Some(BroadcastValidation::ConsensusAndEquivocation); @@ -1905,7 +1887,7 @@ async fn slashable_blobs_or_columns_seen_on_gossip_cause_failure() { let state = tester.harness.get_current_state(); let fork_name = state.fork_name(&tester.harness.spec).unwrap(); // Gloas blocks don't carry blobs (execution data comes via envelopes). - if !fork_name.deneb_enabled() || fork_name.gloas_enabled() { + if !fork_name.fulu_enabled() || fork_name.gloas_enabled() { return; } @@ -1926,19 +1908,13 @@ async fn slashable_blobs_or_columns_seen_on_gossip_cause_failure() { let state_a = tester.harness.get_current_state(); let ((block_a, blobs_a), _) = tester.harness.make_block(state_a.clone(), slot_b).await; - let ((block_b, blobs_b), _) = tester.harness.make_block(state_a, slot_b).await; + let ((block_b, _), _) = tester.harness.make_block(state_a, slot_b).await; let (kzg_proofs_a, blobs_a) = blobs_a.expect("should have some blobs"); - let (kzg_proofs_b, blobs_b) = blobs_b.expect("should have some blobs"); // Simulate the blobs of block B being seen on gossip. tester .harness - .process_gossip_blobs_or_columns( - &block_b, - blobs_b.iter(), - kzg_proofs_b.iter(), - Some(get_custody_columns(&tester, block_b.slot())), - ) + .process_gossip_columns(&block_b, Some(get_custody_columns(&tester, block_b.slot()))) .await; // It should not yet be added to fork choice because block B has not been seen. @@ -1984,7 +1960,7 @@ pub async fn duplicate_block_status_code() { // Gloas blocks don't carry blobs (execution data comes via envelopes). let spec = test_spec::(); let genesis_fork = spec.fork_name_at_slot::(Slot::new(0)); - if !genesis_fork.deneb_enabled() || genesis_fork.gloas_enabled() { + if !genesis_fork.fulu_enabled() || genesis_fork.gloas_enabled() { return; } diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 3da0841a4ef..319229d5f17 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -48,10 +48,10 @@ use tokio::time::Duration; use tree_hash::TreeHash; use types::ApplicationDomain; use types::{ - Address, Domain, EthSpec, ExecutionBlockHash, Hash256, MainnetEthSpec, ProposerPreferences, - RelativeEpoch, SelectionProof, SignedExecutionPayloadEnvelope, SignedProposerPreferences, - SignedRoot, SingleAttestation, Slot, attestation::AttestationBase, - consts::gloas::BUILDER_INDEX_SELF_BUILD, + Address, Domain, EthSpec, ExecutionBlockHash, ExecutionPayloadBid, Hash256, MainnetEthSpec, + ProposerPreferences, RelativeEpoch, SelectionProof, SignedExecutionPayloadBid, + SignedExecutionPayloadEnvelope, SignedProposerPreferences, SignedRoot, SingleAttestation, Slot, + attestation::AttestationBase, consts::gloas::BUILDER_INDEX_SELF_BUILD, }; type E = MainnetEthSpec; @@ -3055,6 +3055,69 @@ impl ApiTester { self } + /// Build a `SignedExecutionPayloadBid` + fn make_signed_execution_payload_bid(&self) -> (SignedExecutionPayloadBid, ForkName) { + let head = self.chain.canonical_head.cached_head(); + let slot = self.chain.slot().unwrap(); + let fork_name = self.chain.spec.fork_name_at_slot::(slot); + + let bid = ExecutionPayloadBid { + parent_block_hash: ExecutionBlockHash::zero(), + parent_block_root: head.head_block_root(), + block_hash: ExecutionBlockHash::zero(), + prev_randao: Hash256::zero(), + fee_recipient: Address::zero(), + gas_limit: 30_000_000, + builder_index: 0, + slot, + value: 100, + execution_payment: 0, + blob_kzg_commitments: Default::default(), + execution_requests_root: Hash256::zero(), + }; + + let signed = SignedExecutionPayloadBid { + message: bid, + signature: bls::Signature::empty(), + }; + + (signed, fork_name) + } + + /// JSON bid with a valid structure reaches gossip verification and is rejected with 400. + pub async fn test_post_beacon_execution_payload_bid_json(self) -> Self { + let (bid, fork_name) = self.make_signed_execution_payload_bid(); + + let result = self + .client + .post_beacon_execution_payload_bid(&bid, fork_name) + .await; + + assert!( + result.is_err(), + "bid should be rejected by gossip verification" + ); + + self + } + + /// SSZ bid with a valid structure reaches gossip verification and is rejected with 400. + pub async fn test_post_beacon_execution_payload_bid_ssz(self) -> Self { + let (bid, fork_name) = self.make_signed_execution_payload_bid(); + + let result = self + .client + .post_beacon_execution_payload_bid_ssz(&bid, fork_name) + .await; + + assert!( + result.is_err(), + "bid (SSZ) should be rejected by gossip verification" + ); + + self + } + pub async fn test_get_config_fork_schedule(self) -> Self { let result = self.client.get_config_fork_schedule().await.unwrap().data; @@ -4907,6 +4970,10 @@ impl ApiTester { "payload attestation should report payload_present=true after publishing \ the envelope via the HTTP API (slot {slot})" ); + assert!( + pa_data.blob_data_available, + "blob_data_available should be true once the envelope is imported (slot {slot})" + ); self.chain.slot_clock.set_slot(slot.as_u64() + 1); } @@ -4914,6 +4981,71 @@ impl ApiTester { self } + /// When a payload hasn't been seen, the payload attestation data + /// must report `payload_present = false` and `blob_data_available = false`. + pub async fn test_payload_attestation_unavailable_without_envelope(self) -> Self { + if !self.chain.spec.is_gloas_scheduled() { + return self; + } + + let fork = self.chain.canonical_head.cached_head().head_fork(); + let genesis_validators_root = self.chain.genesis_validators_root; + + for _ in 0..E::slots_per_epoch() * 3 { + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + let fork_name = self.chain.spec.fork_name_at_slot::(slot); + + if !fork_name.gloas_enabled() { + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + continue; + } + + let (sk, randao_reveal) = self + .proposer_setup(slot, epoch, &fork, genesis_validators_root) + .await; + + // Produce and publish a block, but withhold its envelope. + let (response, _metadata) = self + .client + .get_validator_blocks_v4::(slot, &randao_reveal, None, None, None, None) + .await + .unwrap(); + let block = response.data; + let block_root = block.tree_hash_root(); + + let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); + let signed_block_request = + PublishBlockRequest::try_from(Arc::new(signed_block)).unwrap(); + self.client + .post_beacon_blocks_v2(&signed_block_request, None) + .await + .unwrap(); + + let pa_data = self + .client + .get_validator_payload_attestation_data(slot) + .await + .unwrap() + .expect("expected payload attestation data for slot with block") + .into_data(); + + assert_eq!(pa_data.beacon_block_root, block_root); + assert!( + !pa_data.payload_present, + "payload_present should be false when the envelope is withheld (slot {slot})" + ); + assert!( + !pa_data.blob_data_available, + "blob_data_available should be false when the envelope is not imported (slot {slot})" + ); + + return self; + } + + self + } + pub async fn test_get_validator_payload_attestation_data_pre_gloas(self) -> Self { let slot = self.chain.slot().unwrap(); @@ -8640,6 +8772,14 @@ async fn payload_attestation_present_after_envelope_publish() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn payload_attestation_unavailable_without_envelope() { + ApiTester::new_with_hard_forks() + .await + .test_payload_attestation_unavailable_without_envelope() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn post_beacon_pool_payload_attestations_valid() { if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { @@ -9167,11 +9307,17 @@ async fn builder_works_post_deneb() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn get_blob_sidecars() { - let mut config = ApiTesterConfig::default(); + let mut config = ApiTesterConfig { + retain_historic_states: false, + spec: E::default_spec(), + node_custody_type: NodeCustodyType::Supernode, + }; config.spec.altair_fork_epoch = Some(Epoch::new(0)); config.spec.bellatrix_fork_epoch = Some(Epoch::new(0)); config.spec.capella_fork_epoch = Some(Epoch::new(0)); config.spec.deneb_fork_epoch = Some(Epoch::new(0)); + config.spec.electra_fork_epoch = Some(Epoch::new(0)); + config.spec.fulu_fork_epoch = Some(Epoch::new(0)); ApiTester::new_from_config(config) .await @@ -9410,3 +9556,16 @@ async fn post_validator_proposer_preferences() { .test_post_validator_proposer_preferences_duplicate() .await; } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn post_beacon_execution_payload_bid() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + ApiTester::new_with_hard_forks() + .await + .test_post_beacon_execution_payload_bid_json() + .await + .test_post_beacon_execution_payload_bid_ssz() + .await; +} diff --git a/beacon_node/lighthouse_network/src/config.rs b/beacon_node/lighthouse_network/src/config.rs index db42d0cfa8f..303f4771efa 100644 --- a/beacon_node/lighthouse_network/src/config.rs +++ b/beacon_node/lighthouse_network/src/config.rs @@ -125,6 +125,12 @@ pub struct Config { /// Whether light client protocols should be enabled. pub enable_light_client_server: bool, + /// Whether to enable the deprecated mplex multiplexer alongside yamux. + pub enable_mplex: bool, + + /// Whether optional EIP-8025 execution proof gossip/RPC protocols should be enabled. + pub enable_execution_proof: bool, + /// Configuration for the outbound rate limiter (requests made by this node). pub outbound_rate_limiter_config: Option, @@ -362,6 +368,8 @@ impl Default for Config { proposer_only: false, metrics_enabled: false, enable_light_client_server: true, + enable_mplex: false, + enable_execution_proof: false, outbound_rate_limiter_config: None, invalid_block_storage: None, inbound_rate_limiter_config: None, diff --git a/beacon_node/lighthouse_network/src/discovery/enr.rs b/beacon_node/lighthouse_network/src/discovery/enr.rs index 01a01d55ab5..1faf7320945 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr.rs @@ -29,6 +29,8 @@ pub const ATTESTATION_BITFIELD_ENR_KEY: &str = "attnets"; pub const SYNC_COMMITTEE_BITFIELD_ENR_KEY: &str = "syncnets"; /// The ENR field specifying the peerdas custody group count. pub const PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY: &str = "cgc"; +/// The ENR field indicating execution proof support. +pub const EXECUTION_PROOF_ENR_KEY: &str = "eproof"; /// Extension trait for ENR's within Eth2. pub trait Eth2Enr { @@ -47,6 +49,9 @@ pub trait Eth2Enr { fn next_fork_digest(&self) -> Result<[u8; 4], &'static str>; fn eth2(&self) -> Result; + + /// Whether this node advertises optional execution proof support. + fn execution_proof_enabled(&self) -> bool; } impl Eth2Enr for Enr { @@ -99,6 +104,12 @@ impl Eth2Enr for Enr { EnrForkId::from_ssz_bytes(ð2_bytes).map_err(|_| "Could not decode EnrForkId") } + + fn execution_proof_enabled(&self) -> bool { + self.get_decodable::(EXECUTION_PROOF_ENR_KEY) + .and_then(|r| r.ok()) + .unwrap_or(false) + } } /// Either use the given ENR or load an ENR from file if it exists and matches the current NodeId @@ -296,6 +307,10 @@ pub fn build_enr( builder.add_value(NEXT_FORK_DIGEST_ENR_KEY, &next_fork_digest); } + if config.enable_execution_proof { + builder.add_value(EXECUTION_PROOF_ENR_KEY, &true); + } + builder .build(enr_key) .map_err(|e| format!("Could not build Local ENR: {:?}", e)) @@ -325,6 +340,7 @@ fn compare_enr(local_enr: &Enr, disk_enr: &Enr) -> bool { && local_enr.get_decodable::(ATTESTATION_BITFIELD_ENR_KEY) == disk_enr.get_decodable(ATTESTATION_BITFIELD_ENR_KEY) && local_enr.get_decodable::(SYNC_COMMITTEE_BITFIELD_ENR_KEY) == disk_enr.get_decodable(SYNC_COMMITTEE_BITFIELD_ENR_KEY) && local_enr.get_decodable::(PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY) == disk_enr.get_decodable(PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY) + && local_enr.get_decodable::(EXECUTION_PROOF_ENR_KEY) == disk_enr.get_decodable(EXECUTION_PROOF_ENR_KEY) } /// Loads enr from the given directory diff --git a/beacon_node/lighthouse_network/src/peer_manager/mod.rs b/beacon_node/lighthouse_network/src/peer_manager/mod.rs index 6b5144fa6fd..16125797915 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/mod.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/mod.rs @@ -593,6 +593,9 @@ impl PeerManager { Protocol::BlobsByRange => PeerAction::MidToleranceError, Protocol::PayloadEnvelopesByRange => PeerAction::MidToleranceError, Protocol::PayloadEnvelopesByRoot => PeerAction::MidToleranceError, + Protocol::ExecutionProofsByRange => PeerAction::MidToleranceError, + Protocol::ExecutionProofsByRoot => PeerAction::MidToleranceError, + Protocol::ExecutionProofStatus => PeerAction::MidToleranceError, // Lighthouse does not currently make light client requests; therefore, this // is an unexpected scenario. We do not ban the peer for rate limiting. Protocol::LightClientBootstrap => return, @@ -621,6 +624,9 @@ impl PeerManager { Protocol::BlocksByHead => return, Protocol::PayloadEnvelopesByRange => return, Protocol::PayloadEnvelopesByRoot => return, + Protocol::ExecutionProofsByRange => return, + Protocol::ExecutionProofsByRoot => return, + Protocol::ExecutionProofStatus => return, Protocol::BlobsByRange => return, Protocol::BlobsByRoot => return, Protocol::DataColumnsByRoot => return, @@ -647,6 +653,9 @@ impl PeerManager { Protocol::BlocksByHead => PeerAction::MidToleranceError, Protocol::PayloadEnvelopesByRange => PeerAction::MidToleranceError, Protocol::PayloadEnvelopesByRoot => PeerAction::MidToleranceError, + Protocol::ExecutionProofsByRange => PeerAction::MidToleranceError, + Protocol::ExecutionProofsByRoot => PeerAction::MidToleranceError, + Protocol::ExecutionProofStatus => PeerAction::MidToleranceError, Protocol::BlobsByRange => PeerAction::MidToleranceError, Protocol::BlobsByRoot => PeerAction::MidToleranceError, Protocol::DataColumnsByRoot => PeerAction::MidToleranceError, diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs index 11ce7853507..23f47c67a73 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs @@ -793,12 +793,39 @@ impl PeerDB { ); } - /// Updates the connection state. MUST ONLY BE USED IN TESTS. - pub fn __add_connected_peer_testing_only( + /// Adds a connected peer to the PeerDB and sets the custody subnets. + /// WARNING: This updates the connection state. MUST ONLY BE USED IN TESTS. + pub fn __add_connected_peer_with_custody_subnets( &mut self, supernode: bool, spec: &ChainSpec, enr_key: CombinedKey, + ) -> PeerId { + let peer_id = self.__add_connected_peer(supernode, enr_key, spec); + + let subnets = if supernode { + (0..spec.data_column_sidecar_subnet_count) + .map(|subnet_id| subnet_id.into()) + .collect() + } else { + let node_id = peer_id_to_node_id(&peer_id).expect("convert peer_id to node_id"); + compute_subnets_for_node::(node_id.raw(), spec.custody_requirement, spec) + .expect("should compute custody subnets") + }; + + let peer_info = self.peers.get_mut(&peer_id).expect("peer exists"); + peer_info.set_custody_subnets(subnets); + + peer_id + } + + /// Adds a connected peer to the PeerDB and updates the connection state. + /// MUST ONLY BE USED IN TESTS. + pub fn __add_connected_peer( + &mut self, + supernode: bool, + enr_key: CombinedKey, + spec: &ChainSpec, ) -> PeerId { let mut enr = Enr::builder().build(&enr_key).unwrap(); let peer_id = enr.peer_id(); @@ -835,24 +862,21 @@ impl PeerDB { }, ); - if supernode { - let peer_info = self.peers.get_mut(&peer_id).expect("peer exists"); - let all_subnets = (0..spec.data_column_sidecar_subnet_count) - .map(|subnet_id| subnet_id.into()) - .collect(); - peer_info.set_custody_subnets(all_subnets); - } else { - let peer_info = self.peers.get_mut(&peer_id).expect("peer exists"); - let node_id = peer_id_to_node_id(&peer_id).expect("convert peer_id to node_id"); - let subnets = - compute_subnets_for_node::(node_id.raw(), spec.custody_requirement, spec) - .expect("should compute custody subnets"); - peer_info.set_custody_subnets(subnets); - } - peer_id } + /// MUST ONLY BE USED IN TESTS. + pub fn __set_custody_subnets( + &mut self, + peer_id: &PeerId, + custody_subnets: HashSet, + ) -> Result<(), String> { + self.peers + .get_mut(peer_id) + .map(|info| info.set_custody_subnets(custody_subnets)) + .ok_or_else(|| "Cannot set custody subnets, peer not found".to_string()) + } + /// The connection state of the peer has been changed. Modify the peer in the db to ensure all /// variables are in sync with libp2p. /// Updating the state can lead to a `BanOperation` which needs to be processed via the peer diff --git a/beacon_node/lighthouse_network/src/rpc/codec.rs b/beacon_node/lighthouse_network/src/rpc/codec.rs index ba95fff5e8e..91733dda8e4 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec.rs @@ -15,14 +15,14 @@ use std::io::{Read, Write}; use std::marker::PhantomData; use std::sync::Arc; use tokio_util::codec::{Decoder, Encoder}; -use types::SignedExecutionPayloadEnvelope; use types::{ BlobSidecar, ChainSpec, DataColumnSidecar, DataColumnsByRootIdentifier, EthSpec, ForkContext, ForkName, ForkVersionDecode, Hash256, LightClientBootstrap, LightClientFinalityUpdate, LightClientOptimisticUpdate, LightClientUpdate, SignedBeaconBlock, SignedBeaconBlockAltair, SignedBeaconBlockBase, SignedBeaconBlockBellatrix, SignedBeaconBlockCapella, SignedBeaconBlockDeneb, SignedBeaconBlockElectra, SignedBeaconBlockFulu, - SignedBeaconBlockGloas, + SignedBeaconBlockGloas, SignedExecutionPayloadEnvelope, + execution::eip8025::SignedExecutionProof, }; use unsigned_varint::codec::Uvi; @@ -88,6 +88,9 @@ impl SSZSnappyInboundCodec { RpcSuccessResponse::LightClientOptimisticUpdate(res) => res.as_ssz_bytes(), RpcSuccessResponse::LightClientFinalityUpdate(res) => res.as_ssz_bytes(), RpcSuccessResponse::LightClientUpdatesByRange(res) => res.as_ssz_bytes(), + RpcSuccessResponse::ExecutionProofsByRange(res) => res.as_ssz_bytes(), + RpcSuccessResponse::ExecutionProofsByRoot(res) => res.as_ssz_bytes(), + RpcSuccessResponse::ExecutionProofStatus(res) => res.as_ssz_bytes(), RpcSuccessResponse::Pong(res) => res.data.as_ssz_bytes(), RpcSuccessResponse::MetaData(res) => // Encode the correct version of the MetaData response based on the negotiated version. @@ -370,6 +373,9 @@ impl Encoder> for SSZSnappyOutboundCodec { RequestType::Ping(req) => req.as_ssz_bytes(), RequestType::LightClientBootstrap(req) => req.as_ssz_bytes(), RequestType::LightClientUpdatesByRange(req) => req.as_ssz_bytes(), + RequestType::ExecutionProofsByRange(req) => req.as_ssz_bytes(), + RequestType::ExecutionProofsByRoot(req) => req.identifiers.as_ssz_bytes(), + RequestType::ExecutionProofStatus(req) => req.as_ssz_bytes(), // no metadata to encode RequestType::MetaData(_) | RequestType::LightClientOptimisticUpdate @@ -613,6 +619,22 @@ fn handle_rpc_request( LightClientUpdatesByRangeRequest::from_ssz_bytes(decoded_buffer)?, ))) } + SupportedProtocol::ExecutionProofsByRangeV1 => { + Ok(Some(RequestType::ExecutionProofsByRange( + ExecutionProofsByRangeRequest::from_ssz_bytes(decoded_buffer)?, + ))) + } + SupportedProtocol::ExecutionProofsByRootV1 => Ok(Some(RequestType::ExecutionProofsByRoot( + ExecutionProofsByRootRequest { + identifiers: RuntimeVariableList::from_ssz_bytes( + decoded_buffer, + spec.max_request_blocks(current_fork), + )?, + }, + ))), + SupportedProtocol::ExecutionProofStatusV1 => Ok(Some(RequestType::ExecutionProofStatus( + ExecutionProofStatus::from_ssz_bytes(decoded_buffer)?, + ))), // MetaData requests return early from InboundUpgrade and do not reach the decoder. // Handle this case just for completeness. SupportedProtocol::MetaDataV3 => { @@ -862,6 +884,21 @@ fn handle_rpc_response( ), )), }, + SupportedProtocol::ExecutionProofsByRangeV1 => { + Ok(Some(RpcSuccessResponse::ExecutionProofsByRange(Arc::new( + SignedExecutionProof::from_ssz_bytes(decoded_buffer)?, + )))) + } + SupportedProtocol::ExecutionProofsByRootV1 => { + Ok(Some(RpcSuccessResponse::ExecutionProofsByRoot(Arc::new( + SignedExecutionProof::from_ssz_bytes(decoded_buffer)?, + )))) + } + SupportedProtocol::ExecutionProofStatusV1 => { + Ok(Some(RpcSuccessResponse::ExecutionProofStatus( + ExecutionProofStatus::from_ssz_bytes(decoded_buffer)?, + ))) + } // MetaData V2/V3 responses have no context bytes, so behave similarly to V1 responses SupportedProtocol::MetaDataV3 => Ok(Some(RpcSuccessResponse::MetaData(Arc::new( MetaData::V3(MetaDataV3::from_ssz_bytes(decoded_buffer)?), @@ -1175,6 +1212,40 @@ mod tests { } } + fn eprange_request() -> ExecutionProofsByRangeRequest { + use typenum::Unsigned; + ExecutionProofsByRangeRequest { + start_slot: 5, + count: 10, + proof_types: RuntimeVariableList::new( + vec![0, 2], + types::execution::eip8025::MaxExecutionProofsPerPayload::to_usize(), + ) + .unwrap(), + } + } + + fn eproot_request(fork_name: ForkName, spec: &ChainSpec) -> ExecutionProofsByRootRequest { + ExecutionProofsByRootRequest { + identifiers: RuntimeVariableList::new( + vec![types::execution::eip8025::ProofByRootIdentifier { + block_root: Hash256::zero(), + proof_types: VariableList::try_from(vec![0, 1]).unwrap(), + }], + spec.max_request_blocks(fork_name), + ) + .unwrap(), + } + } + + fn epstatus_request() -> ExecutionProofStatus { + ExecutionProofStatus { + block_root: Hash256::zero(), + slot: 42, + proof_types: VariableList::try_from(vec![0, 1, 2, 3]).unwrap(), + } + } + fn bbroot_request_v1(fork_name: ForkName, spec: &ChainSpec) -> BlocksByRootRequest { BlocksByRootRequest::new_v1(vec![Hash256::zero()], &fork_context(fork_name, spec)).unwrap() } @@ -1376,6 +1447,24 @@ mod tests { RequestType::LightClientUpdatesByRange(light_client_updates_by_range) ) } + RequestType::ExecutionProofsByRange(execution_proofs_by_range) => { + assert_eq!( + decoded, + RequestType::ExecutionProofsByRange(execution_proofs_by_range) + ) + } + RequestType::ExecutionProofsByRoot(execution_proofs_by_root) => { + assert_eq!( + decoded, + RequestType::ExecutionProofsByRoot(execution_proofs_by_root) + ) + } + RequestType::ExecutionProofStatus(execution_proof_status) => { + assert_eq!( + decoded, + RequestType::ExecutionProofStatus(execution_proof_status) + ) + } } } @@ -2112,6 +2201,8 @@ mod tests { beacon_root: Hash256::zero(), count: 32, }), + RequestType::ExecutionProofsByRange(eprange_request()), + RequestType::ExecutionProofStatus(epstatus_request()), ]; for req in requests.iter() { for fork_name in ForkName::list_all() { @@ -2127,6 +2218,7 @@ mod tests { RequestType::BlocksByRoot(bbroot_request_v1(fork_name, &chain_spec)), RequestType::BlocksByRoot(bbroot_request_v2(fork_name, &chain_spec)), RequestType::DataColumnsByRoot(dcbroot_request(fork_name, &chain_spec)), + RequestType::ExecutionProofsByRoot(eproot_request(fork_name, &chain_spec)), ] }; for fork_name in ForkName::list_all() { diff --git a/beacon_node/lighthouse_network/src/rpc/config.rs b/beacon_node/lighthouse_network/src/rpc/config.rs index 59f0b8e9a2f..80628548058 100644 --- a/beacon_node/lighthouse_network/src/rpc/config.rs +++ b/beacon_node/lighthouse_network/src/rpc/config.rs @@ -92,6 +92,9 @@ pub struct RateLimiterConfig { pub(super) blocks_by_head_quota: Quota, pub(super) payload_envelopes_by_range_quota: Quota, pub(super) payload_envelopes_by_root_quota: Quota, + pub(super) execution_proofs_by_range_quota: Quota, + pub(super) execution_proofs_by_root_quota: Quota, + pub(super) execution_proof_status_quota: Quota, pub(super) blobs_by_range_quota: Quota, pub(super) blobs_by_root_quota: Quota, pub(super) data_columns_by_root_quota: Quota, @@ -120,6 +123,12 @@ impl RateLimiterConfig { Quota::n_every(NonZeroU64::new(128).unwrap(), 10); pub const DEFAULT_PAYLOAD_ENVELOPES_BY_ROOT_QUOTA: Quota = Quota::n_every(NonZeroU64::new(128).unwrap(), 10); + pub const DEFAULT_EXECUTION_PROOFS_BY_RANGE_QUOTA: Quota = + Quota::n_every(NonZeroU64::new(128).unwrap(), 10); + pub const DEFAULT_EXECUTION_PROOFS_BY_ROOT_QUOTA: Quota = + Quota::n_every(NonZeroU64::new(128).unwrap(), 10); + pub const DEFAULT_EXECUTION_PROOF_STATUS_QUOTA: Quota = + Quota::n_every(NonZeroU64::new(128).unwrap(), 10); // `DEFAULT_BLOCKS_BY_RANGE_QUOTA` * (target + 1) to account for high usage pub const DEFAULT_BLOBS_BY_RANGE_QUOTA: Quota = Quota::n_every(NonZeroU64::new(896).unwrap(), 10); @@ -149,6 +158,9 @@ impl Default for RateLimiterConfig { blocks_by_head_quota: Self::DEFAULT_BLOCKS_BY_HEAD_QUOTA, payload_envelopes_by_range_quota: Self::DEFAULT_PAYLOAD_ENVELOPES_BY_RANGE_QUOTA, payload_envelopes_by_root_quota: Self::DEFAULT_PAYLOAD_ENVELOPES_BY_ROOT_QUOTA, + execution_proofs_by_range_quota: Self::DEFAULT_EXECUTION_PROOFS_BY_RANGE_QUOTA, + execution_proofs_by_root_quota: Self::DEFAULT_EXECUTION_PROOFS_BY_ROOT_QUOTA, + execution_proof_status_quota: Self::DEFAULT_EXECUTION_PROOF_STATUS_QUOTA, blobs_by_range_quota: Self::DEFAULT_BLOBS_BY_RANGE_QUOTA, blobs_by_root_quota: Self::DEFAULT_BLOBS_BY_ROOT_QUOTA, data_columns_by_root_quota: Self::DEFAULT_DATA_COLUMNS_BY_ROOT_QUOTA, @@ -190,6 +202,18 @@ impl Debug for RateLimiterConfig { "payload_envelopes_by_root", fmt_q!(&self.payload_envelopes_by_root_quota), ) + .field( + "execution_proofs_by_range", + fmt_q!(&self.execution_proofs_by_range_quota), + ) + .field( + "execution_proofs_by_root", + fmt_q!(&self.execution_proofs_by_root_quota), + ) + .field( + "execution_proof_status", + fmt_q!(&self.execution_proof_status_quota), + ) .field("blobs_by_range", fmt_q!(&self.blobs_by_range_quota)) .field("blobs_by_root", fmt_q!(&self.blobs_by_root_quota)) .field( @@ -221,6 +245,9 @@ impl FromStr for RateLimiterConfig { let mut blocks_by_head_quota = None; let mut payload_envelopes_by_range_quota = None; let mut payload_envelopes_by_root_quota = None; + let mut execution_proofs_by_range_quota = None; + let mut execution_proofs_by_root_quota = None; + let mut execution_proof_status_quota = None; let mut blobs_by_range_quota = None; let mut blobs_by_root_quota = None; let mut data_columns_by_root_quota = None; @@ -245,6 +272,15 @@ impl FromStr for RateLimiterConfig { Protocol::PayloadEnvelopesByRoot => { payload_envelopes_by_root_quota = payload_envelopes_by_root_quota.or(quota) } + Protocol::ExecutionProofsByRange => { + execution_proofs_by_range_quota = execution_proofs_by_range_quota.or(quota) + } + Protocol::ExecutionProofsByRoot => { + execution_proofs_by_root_quota = execution_proofs_by_root_quota.or(quota) + } + Protocol::ExecutionProofStatus => { + execution_proof_status_quota = execution_proof_status_quota.or(quota) + } Protocol::BlobsByRange => blobs_by_range_quota = blobs_by_range_quota.or(quota), Protocol::BlobsByRoot => blobs_by_root_quota = blobs_by_root_quota.or(quota), Protocol::DataColumnsByRoot => { @@ -287,6 +323,12 @@ impl FromStr for RateLimiterConfig { .unwrap_or(Self::DEFAULT_PAYLOAD_ENVELOPES_BY_RANGE_QUOTA), payload_envelopes_by_root_quota: payload_envelopes_by_root_quota .unwrap_or(Self::DEFAULT_PAYLOAD_ENVELOPES_BY_ROOT_QUOTA), + execution_proofs_by_range_quota: execution_proofs_by_range_quota + .unwrap_or(Self::DEFAULT_EXECUTION_PROOFS_BY_RANGE_QUOTA), + execution_proofs_by_root_quota: execution_proofs_by_root_quota + .unwrap_or(Self::DEFAULT_EXECUTION_PROOFS_BY_ROOT_QUOTA), + execution_proof_status_quota: execution_proof_status_quota + .unwrap_or(Self::DEFAULT_EXECUTION_PROOF_STATUS_QUOTA), blobs_by_range_quota: blobs_by_range_quota .unwrap_or(Self::DEFAULT_BLOBS_BY_RANGE_QUOTA), blobs_by_root_quota: blobs_by_root_quota.unwrap_or(Self::DEFAULT_BLOBS_BY_ROOT_QUOTA), diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index f3f294d9135..47750b68353 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -12,7 +12,11 @@ use std::ops::Deref; use std::sync::Arc; use strum::IntoStaticStr; use superstruct::superstruct; +use typenum::Unsigned; use types::data::BlobIdentifier; +use types::execution::eip8025::{ + MaxExecutionProofsPerPayload, ProofByRootIdentifier, ProofType, SignedExecutionProof, +}; use types::light_client::consts::MAX_REQUEST_LIGHT_CLIENT_UPDATES; use types::{ BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnsByRootIdentifier, Epoch, @@ -619,6 +623,108 @@ impl LightClientUpdatesByRangeRequest { } } +/// The peer's current execution proof verification status, exchanged via the +/// `ExecutionProofStatus` RPC protocol. +#[derive(Clone, Debug, Default, PartialEq, Encode, Decode)] +pub struct ExecutionProofStatus { + /// The block root of the latest block this peer can use as a proof-sync anchor. + pub block_root: Hash256, + /// The slot of the latest block this peer can use as a proof-sync anchor. + pub slot: u64, + /// Proof types supported by this peer. + pub proof_types: VariableList, +} + +impl ExecutionProofStatus { + pub fn ssz_min_len() -> usize { + 32 + 8 + ssz::BYTES_PER_LENGTH_OFFSET + } + + pub fn ssz_max_len() -> usize { + use typenum::Unsigned; + Self::ssz_min_len() + MaxExecutionProofsPerPayload::USIZE + } +} + +/// Request execution proofs for a slot range from a peer. +#[derive(Clone, Debug, PartialEq)] +pub struct ExecutionProofsByRangeRequest { + /// The starting slot to request execution proofs. + pub start_slot: u64, + /// The number of slots from the start slot. + pub count: u64, + /// Proof types to return across the requested range. Empty list means all known proof types. + pub proof_types: RuntimeVariableList, +} + +impl ExecutionProofsByRangeRequest { + pub fn max_requested(&self) -> u64 { + use typenum::Unsigned; + self.count + .saturating_mul(MaxExecutionProofsPerPayload::to_u64()) + } + + pub fn ssz_min_len() -> usize { + 20 + } + + pub fn ssz_max_len() -> usize { + use typenum::Unsigned; + Self::ssz_min_len() + MaxExecutionProofsPerPayload::USIZE + } + + pub fn from_ssz_bytes(bytes: &[u8]) -> Result { + let mut builder = ssz::SszDecoderBuilder::new(bytes); + builder.register_type::()?; + builder.register_type::()?; + builder.register_type::>()?; + let mut decoder = builder.build()?; + Ok(Self { + start_slot: decoder.decode_next::()?, + count: decoder.decode_next::()?, + proof_types: decoder.decode_next_with(|slice| { + RuntimeVariableList::from_ssz_bytes(slice, MaxExecutionProofsPerPayload::to_usize()) + })?, + }) + } +} + +impl ssz::Encode for ExecutionProofsByRangeRequest { + fn is_ssz_fixed_len() -> bool { + false + } + + fn ssz_append(&self, buf: &mut Vec) { + let mut encoder = ssz::SszEncoder::container(buf, Self::ssz_min_len()); + encoder.append(&self.start_slot); + encoder.append(&self.count); + encoder.append(&self.proof_types); + encoder.finalize(); + } + + fn ssz_bytes_len(&self) -> usize { + Self::ssz_min_len() + self.proof_types.ssz_bytes_len() + } +} + +/// Request execution proofs for specific blocks by root from a peer. +#[derive(Clone, Debug, PartialEq)] +pub struct ExecutionProofsByRootRequest { + /// Each entry identifies a block root and the proof types requested for it. + pub identifiers: RuntimeVariableList, +} + +impl ExecutionProofsByRootRequest { + pub fn new( + identifiers: Vec, + max_request_blocks: usize, + ) -> Result { + let identifiers = RuntimeVariableList::new(identifiers, max_request_blocks) + .map_err(|e| format!("ExecutionProofsByRootRequest too many identifiers: {e:?}"))?; + Ok(Self { identifiers }) + } +} + /* RPC Handling and Grouping */ // Collection of enums and structs used by the Codecs to encode/decode RPC messages @@ -668,6 +774,15 @@ pub enum RpcSuccessResponse { /// A response to a get DATA_COLUMN_SIDECARS_BY_RANGE request. DataColumnsByRange(Arc>), + /// A response to a get EXECUTION_PROOFS_BY_RANGE request. + ExecutionProofsByRange(Arc), + + /// A response to a get EXECUTION_PROOFS_BY_ROOT request. + ExecutionProofsByRoot(Arc), + + /// A response to an EXECUTION_PROOF_STATUS request. + ExecutionProofStatus(ExecutionProofStatus), + /// A PONG response to a PING request. Pong(Ping), @@ -707,6 +822,12 @@ pub enum ResponseTermination { /// Light client updates by range stream termination. LightClientUpdatesByRange, + + /// Execution proofs by range stream termination. + ExecutionProofsByRange, + + /// Execution proofs by root stream termination. + ExecutionProofsByRoot, } impl ResponseTermination { @@ -722,6 +843,8 @@ impl ResponseTermination { ResponseTermination::DataColumnsByRoot => Protocol::DataColumnsByRoot, ResponseTermination::DataColumnsByRange => Protocol::DataColumnsByRange, ResponseTermination::LightClientUpdatesByRange => Protocol::LightClientUpdatesByRange, + ResponseTermination::ExecutionProofsByRange => Protocol::ExecutionProofsByRange, + ResponseTermination::ExecutionProofsByRoot => Protocol::ExecutionProofsByRoot, } } } @@ -827,6 +950,9 @@ impl RpcSuccessResponse { } RpcSuccessResponse::LightClientFinalityUpdate(_) => Protocol::LightClientFinalityUpdate, RpcSuccessResponse::LightClientUpdatesByRange(_) => Protocol::LightClientUpdatesByRange, + RpcSuccessResponse::ExecutionProofsByRange(_) => Protocol::ExecutionProofsByRange, + RpcSuccessResponse::ExecutionProofsByRoot(_) => Protocol::ExecutionProofsByRoot, + RpcSuccessResponse::ExecutionProofStatus(_) => Protocol::ExecutionProofStatus, } } @@ -843,6 +969,9 @@ impl RpcSuccessResponse { Self::LightClientOptimisticUpdate(r) => Some(r.get_slot()), Self::LightClientUpdatesByRange(r) => Some(r.attested_header_slot()), Self::MetaData(_) | Self::Status(_) | Self::Pong(_) => None, + Self::ExecutionProofsByRange(_) + | Self::ExecutionProofsByRoot(_) + | Self::ExecutionProofStatus(_) => None, } } } @@ -947,6 +1076,23 @@ impl std::fmt::Display for RpcSuccessResponse { update.signature_slot(), ) } + RpcSuccessResponse::ExecutionProofsByRange(proof) => { + write!( + f, + "ExecutionProofsByRange: validator_index: {}", + proof.validator_index + ) + } + RpcSuccessResponse::ExecutionProofsByRoot(proof) => { + write!( + f, + "ExecutionProofsByRoot: validator_index: {}", + proof.validator_index + ) + } + RpcSuccessResponse::ExecutionProofStatus(status) => { + write!(f, "ExecutionProofStatus: slot={}", status.slot) + } } } } @@ -1039,3 +1185,25 @@ impl std::fmt::Display for DataColumnsByRootRequest { ) } } + +impl std::fmt::Display for ExecutionProofsByRangeRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Request: ExecutionProofsByRange: Start Slot: {}, Count: {}, Proof Types: {}", + self.start_slot, + self.count, + self.proof_types.len() + ) + } +} + +impl std::fmt::Display for ExecutionProofsByRootRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Request: ExecutionProofsByRoot: Number of Requested Identifiers: {}", + self.identifiers.len() + ) + } +} diff --git a/beacon_node/lighthouse_network/src/rpc/mod.rs b/beacon_node/lighthouse_network/src/rpc/mod.rs index 7c43018af83..6c7b1e2d781 100644 --- a/beacon_node/lighthouse_network/src/rpc/mod.rs +++ b/beacon_node/lighthouse_network/src/rpc/mod.rs @@ -155,6 +155,7 @@ pub struct RPC { events: Vec>, fork_context: Arc, enable_light_client_server: bool, + enable_execution_proof: bool, /// A sequential counter indicating when data gets modified. seq_number: u64, } @@ -163,6 +164,7 @@ impl RPC { pub fn new( fork_context: Arc, enable_light_client_server: bool, + enable_execution_proof: bool, inbound_rate_limiter_config: Option, outbound_rate_limiter_config: Option, seq_number: u64, @@ -184,6 +186,7 @@ impl RPC { events: Vec::new(), fork_context, enable_light_client_server, + enable_execution_proof, seq_number, } } @@ -319,6 +322,7 @@ where fork_context: self.fork_context.clone(), max_rpc_size: self.fork_context.spec.max_payload_size as usize, enable_light_client_server: self.enable_light_client_server, + enable_execution_proof: self.enable_execution_proof, phantom: PhantomData, }, (), @@ -342,6 +346,7 @@ where fork_context: self.fork_context.clone(), max_rpc_size: self.fork_context.spec.max_payload_size as usize, enable_light_client_server: self.enable_light_client_server, + enable_execution_proof: self.enable_execution_proof, phantom: PhantomData, }, (), diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index 056ffc03b85..fd524bd9ac9 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -16,6 +16,8 @@ use tokio_util::{ codec::Framed, compat::{Compat, FuturesAsyncReadCompatExt}, }; +use typenum::Unsigned; +use types::execution::eip8025::{MaxExecutionProofsPerPayload, MaxProofSize}; use types::{ BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BlobSidecar, ChainSpec, DataColumnSidecarFulu, DataColumnSidecarGloas, EmptyBlock, Epoch, EthSpec, EthSpecId, ForkContext, ForkName, @@ -125,6 +127,13 @@ pub static LIGHT_CLIENT_UPDATES_BY_RANGE_DENEB_MAX: LazyLock = pub static LIGHT_CLIENT_UPDATES_BY_RANGE_ELECTRA_MAX: LazyLock = LazyLock::new(|| LightClientUpdate::::ssz_max_len_for_fork(ForkName::Electra)); +/// Minimum SSZ size of a `SignedExecutionProof` with empty proof data. +pub const SIGNED_EXECUTION_PROOF_MIN_SIZE: usize = 4 + 8 + 96 + 37; + +/// Maximum SSZ size of a `SignedExecutionProof`. +pub const SIGNED_EXECUTION_PROOF_MAX_SIZE: usize = + SIGNED_EXECUTION_PROOF_MIN_SIZE + MaxProofSize::USIZE; + /// The protocol prefix the RPC protocol id. const PROTOCOL_PREFIX: &str = "/eth2/beacon_chain/req"; /// The number of seconds to wait for the first bytes of a request once a protocol has been @@ -300,6 +309,15 @@ pub enum Protocol { /// The `LightClientUpdatesByRange` protocol name #[strum(serialize = "light_client_updates_by_range")] LightClientUpdatesByRange, + /// The `ExecutionProofsByRange` protocol name. + #[strum(serialize = "execution_proofs_by_range")] + ExecutionProofsByRange, + /// The `ExecutionProofsByRoot` protocol name. + #[strum(serialize = "execution_proofs_by_root")] + ExecutionProofsByRoot, + /// The `ExecutionProofStatus` protocol name. + #[strum(serialize = "execution_proof_status")] + ExecutionProofStatus, } impl Protocol { @@ -322,6 +340,9 @@ impl Protocol { Protocol::LightClientOptimisticUpdate => None, Protocol::LightClientFinalityUpdate => None, Protocol::LightClientUpdatesByRange => None, + Protocol::ExecutionProofsByRange => Some(ResponseTermination::ExecutionProofsByRange), + Protocol::ExecutionProofsByRoot => Some(ResponseTermination::ExecutionProofsByRoot), + Protocol::ExecutionProofStatus => None, } } } @@ -357,6 +378,9 @@ pub enum SupportedProtocol { LightClientOptimisticUpdateV1, LightClientFinalityUpdateV1, LightClientUpdatesByRangeV1, + ExecutionProofsByRangeV1, + ExecutionProofsByRootV1, + ExecutionProofStatusV1, } impl SupportedProtocol { @@ -384,6 +408,9 @@ impl SupportedProtocol { SupportedProtocol::LightClientOptimisticUpdateV1 => "1", SupportedProtocol::LightClientFinalityUpdateV1 => "1", SupportedProtocol::LightClientUpdatesByRangeV1 => "1", + SupportedProtocol::ExecutionProofsByRangeV1 => "1", + SupportedProtocol::ExecutionProofsByRootV1 => "1", + SupportedProtocol::ExecutionProofStatusV1 => "1", } } @@ -413,6 +440,9 @@ impl SupportedProtocol { } SupportedProtocol::LightClientFinalityUpdateV1 => Protocol::LightClientFinalityUpdate, SupportedProtocol::LightClientUpdatesByRangeV1 => Protocol::LightClientUpdatesByRange, + SupportedProtocol::ExecutionProofsByRangeV1 => Protocol::ExecutionProofsByRange, + SupportedProtocol::ExecutionProofsByRootV1 => Protocol::ExecutionProofsByRoot, + SupportedProtocol::ExecutionProofStatusV1 => Protocol::ExecutionProofStatus, } } @@ -490,6 +520,7 @@ pub struct RPCProtocol { pub fork_context: Arc, pub max_rpc_size: usize, pub enable_light_client_server: bool, + pub enable_execution_proof: bool, pub phantom: PhantomData, } @@ -518,6 +549,20 @@ impl UpgradeInfo for RPCProtocol { Encoding::SSZSnappy, )); } + if self.enable_execution_proof { + supported_protocols.push(ProtocolId::new( + SupportedProtocol::ExecutionProofsByRangeV1, + Encoding::SSZSnappy, + )); + supported_protocols.push(ProtocolId::new( + SupportedProtocol::ExecutionProofsByRootV1, + Encoding::SSZSnappy, + )); + supported_protocols.push(ProtocolId::new( + SupportedProtocol::ExecutionProofStatusV1, + Encoding::SSZSnappy, + )); + } supported_protocols } } @@ -614,6 +659,20 @@ impl ProtocolId { LightClientUpdatesByRangeRequest::ssz_max_len(), ), Protocol::MetaData => RpcLimits::new(0, 0), // Metadata requests are empty + Protocol::ExecutionProofsByRange => RpcLimits::new( + ExecutionProofsByRangeRequest::ssz_min_len(), + ExecutionProofsByRangeRequest::ssz_max_len(), + ), + Protocol::ExecutionProofsByRoot => { + let max = spec + .max_blocks_by_root_request + .saturating_mul(4 + 32 + 4 + MaxExecutionProofsPerPayload::USIZE); + RpcLimits::new(0, max) + } + Protocol::ExecutionProofStatus => RpcLimits::new( + ExecutionProofStatus::ssz_min_len(), + ExecutionProofStatus::ssz_max_len(), + ), } } @@ -658,6 +717,14 @@ impl ProtocolId { Protocol::LightClientUpdatesByRange => { rpc_light_client_updates_by_range_limits_by_fork(fork_context.current_fork_name()) } + Protocol::ExecutionProofsByRange | Protocol::ExecutionProofsByRoot => RpcLimits::new( + SIGNED_EXECUTION_PROOF_MIN_SIZE, + SIGNED_EXECUTION_PROOF_MAX_SIZE, + ), + Protocol::ExecutionProofStatus => RpcLimits::new( + ExecutionProofStatus::ssz_min_len(), + ExecutionProofStatus::ssz_max_len(), + ), } } @@ -686,7 +753,10 @@ impl ProtocolId { | SupportedProtocol::MetaDataV1 | SupportedProtocol::MetaDataV2 | SupportedProtocol::MetaDataV3 - | SupportedProtocol::GoodbyeV1 => false, + | SupportedProtocol::GoodbyeV1 + | SupportedProtocol::ExecutionProofsByRangeV1 + | SupportedProtocol::ExecutionProofsByRootV1 + | SupportedProtocol::ExecutionProofStatusV1 => false, } } } @@ -832,6 +902,9 @@ pub enum RequestType { LightClientOptimisticUpdate, LightClientFinalityUpdate, LightClientUpdatesByRange(LightClientUpdatesByRangeRequest), + ExecutionProofsByRange(ExecutionProofsByRangeRequest), + ExecutionProofsByRoot(ExecutionProofsByRootRequest), + ExecutionProofStatus(ExecutionProofStatus), Ping(Ping), MetaData(MetadataRequest), } @@ -860,6 +933,10 @@ impl RequestType { RequestType::LightClientOptimisticUpdate => 1, RequestType::LightClientFinalityUpdate => 1, RequestType::LightClientUpdatesByRange(req) => req.count, + RequestType::ExecutionProofsByRange(req) => req.max_requested(), + RequestType::ExecutionProofsByRoot(req) => (req.identifiers.len() as u64) + .saturating_mul(MaxExecutionProofsPerPayload::to_u64()), + RequestType::ExecutionProofStatus(_) => 1, } } @@ -902,6 +979,9 @@ impl RequestType { RequestType::LightClientUpdatesByRange(_) => { SupportedProtocol::LightClientUpdatesByRangeV1 } + RequestType::ExecutionProofsByRange(_) => SupportedProtocol::ExecutionProofsByRangeV1, + RequestType::ExecutionProofsByRoot(_) => SupportedProtocol::ExecutionProofsByRootV1, + RequestType::ExecutionProofStatus(_) => SupportedProtocol::ExecutionProofStatusV1, } } @@ -920,6 +1000,8 @@ impl RequestType { RequestType::BlobsByRoot(_) => ResponseTermination::BlobsByRoot, RequestType::DataColumnsByRoot(_) => ResponseTermination::DataColumnsByRoot, RequestType::DataColumnsByRange(_) => ResponseTermination::DataColumnsByRange, + RequestType::ExecutionProofsByRange(_) => ResponseTermination::ExecutionProofsByRange, + RequestType::ExecutionProofsByRoot(_) => ResponseTermination::ExecutionProofsByRoot, RequestType::Status(_) => unreachable!(), RequestType::Goodbye(_) => unreachable!(), RequestType::Ping(_) => unreachable!(), @@ -928,6 +1010,7 @@ impl RequestType { RequestType::LightClientFinalityUpdate => unreachable!(), RequestType::LightClientOptimisticUpdate => unreachable!(), RequestType::LightClientUpdatesByRange(_) => unreachable!(), + RequestType::ExecutionProofStatus(_) => unreachable!(), } } @@ -1003,6 +1086,18 @@ impl RequestType { SupportedProtocol::LightClientUpdatesByRangeV1, Encoding::SSZSnappy, )], + RequestType::ExecutionProofsByRange(_) => vec![ProtocolId::new( + SupportedProtocol::ExecutionProofsByRangeV1, + Encoding::SSZSnappy, + )], + RequestType::ExecutionProofsByRoot(_) => vec![ProtocolId::new( + SupportedProtocol::ExecutionProofsByRootV1, + Encoding::SSZSnappy, + )], + RequestType::ExecutionProofStatus(_) => vec![ProtocolId::new( + SupportedProtocol::ExecutionProofStatusV1, + Encoding::SSZSnappy, + )], } } @@ -1025,6 +1120,9 @@ impl RequestType { RequestType::LightClientOptimisticUpdate => true, RequestType::LightClientFinalityUpdate => true, RequestType::LightClientUpdatesByRange(_) => true, + RequestType::ExecutionProofsByRange(_) => false, + RequestType::ExecutionProofsByRoot(_) => false, + RequestType::ExecutionProofStatus(_) => true, } } } @@ -1153,6 +1251,11 @@ impl std::fmt::Display for RequestType { RequestType::LightClientUpdatesByRange(_) => { write!(f, "Light client updates by range request") } + RequestType::ExecutionProofsByRange(req) => write!(f, "{}", req), + RequestType::ExecutionProofsByRoot(req) => write!(f, "{}", req), + RequestType::ExecutionProofStatus(status) => { + write!(f, "ExecutionProofStatus(slot={})", status.slot) + } } } } @@ -1207,7 +1310,10 @@ mod tests { LightClientBootstrapV1 | LightClientOptimisticUpdateV1 | LightClientFinalityUpdateV1 - | LightClientUpdatesByRangeV1 => false, + | LightClientUpdatesByRangeV1 + | ExecutionProofsByRangeV1 + | ExecutionProofsByRootV1 + | ExecutionProofStatusV1 => false, } } @@ -1244,6 +1350,7 @@ mod tests { fork_context: fork_context.clone(), max_rpc_size: spec.max_payload_size as usize, enable_light_client_server: true, + enable_execution_proof: false, phantom: PhantomData, }; let protocol_info: HashSet = rpc_protocol diff --git a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs index a5c98a4d309..0e6c14115d3 100644 --- a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs @@ -115,6 +115,12 @@ pub struct RPCRateLimiter { envrange_rl: Limiter, /// PayloadEnvelopesByRoot rate limiter. envroots_rl: Limiter, + /// ExecutionProofsByRange rate limiter. + proofrange_rl: Limiter, + /// ExecutionProofsByRoot rate limiter. + proofroots_rl: Limiter, + /// ExecutionProofStatus rate limiter. + proofstatus_rl: Limiter, /// DataColumnsByRoot rate limiter. dcbroot_rl: Limiter, /// DataColumnsByRange rate limiter. @@ -160,6 +166,12 @@ pub struct RPCRateLimiterBuilder { perange_quota: Option, /// Quota for the ExecutionPayloadEnvelopesByRoot protocol. peroots_quota: Option, + /// Quota for the ExecutionProofsByRange protocol. + proofrange_quota: Option, + /// Quota for the ExecutionProofsByRoot protocol. + proofroots_quota: Option, + /// Quota for the ExecutionProofStatus protocol. + proofstatus_quota: Option, /// Quota for the BlobsByRange protocol. blbrange_quota: Option, /// Quota for the BlobsByRoot protocol. @@ -192,6 +204,9 @@ impl RPCRateLimiterBuilder { Protocol::BlocksByHead => self.bbhead_quota = q, Protocol::PayloadEnvelopesByRange => self.perange_quota = q, Protocol::PayloadEnvelopesByRoot => self.peroots_quota = q, + Protocol::ExecutionProofsByRange => self.proofrange_quota = q, + Protocol::ExecutionProofsByRoot => self.proofroots_quota = q, + Protocol::ExecutionProofStatus => self.proofstatus_quota = q, Protocol::BlobsByRange => self.blbrange_quota = q, Protocol::BlobsByRoot => self.blbroot_quota = q, Protocol::DataColumnsByRoot => self.dcbroot_quota = q, @@ -225,6 +240,15 @@ impl RPCRateLimiterBuilder { let peroots_quota = self .peroots_quota .ok_or("PayloadEnvelopesByRoot quota not specified")?; + let proofrange_quota = self + .proofrange_quota + .ok_or("ExecutionProofsByRange quota not specified")?; + let proofroots_quota = self + .proofroots_quota + .ok_or("ExecutionProofsByRoot quota not specified")?; + let proofstatus_quota = self + .proofstatus_quota + .ok_or("ExecutionProofStatus quota not specified")?; let lc_bootstrap_quota = self .lcbootstrap_quota .ok_or("LightClientBootstrap quota not specified")?; @@ -263,6 +287,9 @@ impl RPCRateLimiterBuilder { let bbhead_rl = Limiter::from_quota(bbhead_quota)?; let envrange_rl = Limiter::from_quota(perange_quota)?; let envroots_rl = Limiter::from_quota(peroots_quota)?; + let proofrange_rl = Limiter::from_quota(proofrange_quota)?; + let proofroots_rl = Limiter::from_quota(proofroots_quota)?; + let proofstatus_rl = Limiter::from_quota(proofstatus_quota)?; let blbrange_rl = Limiter::from_quota(blbrange_quota)?; let blbroot_rl = Limiter::from_quota(blbroots_quota)?; let dcbroot_rl = Limiter::from_quota(dcbroot_quota)?; @@ -289,6 +316,9 @@ impl RPCRateLimiterBuilder { bbhead_rl, envrange_rl, envroots_rl, + proofrange_rl, + proofroots_rl, + proofstatus_rl, blbrange_rl, blbroot_rl, dcbroot_rl, @@ -345,6 +375,9 @@ impl RPCRateLimiter { blocks_by_head_quota, payload_envelopes_by_range_quota, payload_envelopes_by_root_quota, + execution_proofs_by_range_quota, + execution_proofs_by_root_quota, + execution_proof_status_quota, blobs_by_range_quota, blobs_by_root_quota, data_columns_by_root_quota, @@ -371,6 +404,15 @@ impl RPCRateLimiter { Protocol::PayloadEnvelopesByRoot, payload_envelopes_by_root_quota, ) + .set_quota( + Protocol::ExecutionProofsByRange, + execution_proofs_by_range_quota, + ) + .set_quota( + Protocol::ExecutionProofsByRoot, + execution_proofs_by_root_quota, + ) + .set_quota(Protocol::ExecutionProofStatus, execution_proof_status_quota) .set_quota(Protocol::BlobsByRange, blobs_by_range_quota) .set_quota(Protocol::BlobsByRoot, blobs_by_root_quota) .set_quota(Protocol::DataColumnsByRoot, data_columns_by_root_quota) @@ -421,6 +463,9 @@ impl RPCRateLimiter { Protocol::BlocksByHead => &mut self.bbhead_rl, Protocol::PayloadEnvelopesByRange => &mut self.envrange_rl, Protocol::PayloadEnvelopesByRoot => &mut self.envroots_rl, + Protocol::ExecutionProofsByRange => &mut self.proofrange_rl, + Protocol::ExecutionProofsByRoot => &mut self.proofroots_rl, + Protocol::ExecutionProofStatus => &mut self.proofstatus_rl, Protocol::BlobsByRange => &mut self.blbrange_rl, Protocol::BlobsByRoot => &mut self.blbroot_rl, Protocol::DataColumnsByRoot => &mut self.dcbroot_rl, @@ -448,6 +493,9 @@ impl RPCRateLimiter { bbhead_rl, envrange_rl, envroots_rl, + proofrange_rl, + proofroots_rl, + proofstatus_rl, blbrange_rl, blbroot_rl, dcbroot_rl, @@ -468,6 +516,9 @@ impl RPCRateLimiter { bbhead_rl.prune(time_since_start); envrange_rl.prune(time_since_start); envroots_rl.prune(time_since_start); + proofrange_rl.prune(time_since_start); + proofroots_rl.prune(time_since_start); + proofstatus_rl.prune(time_since_start); blbrange_rl.prune(time_since_start); blbroot_rl.prune(time_since_start); dcbrange_rl.prune(time_since_start); diff --git a/beacon_node/lighthouse_network/src/service/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index 2429b813e91..8dc98cbadd6 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -1,11 +1,13 @@ -use crate::rpc::methods::{ResponseTermination, RpcResponse, RpcSuccessResponse, StatusMessage}; +use crate::rpc::methods::{ + ExecutionProofStatus, ResponseTermination, RpcResponse, RpcSuccessResponse, StatusMessage, +}; use libp2p::PeerId; use std::fmt::{Display, Formatter}; use std::sync::Arc; use types::{ BlobSidecar, DataColumnSidecar, Epoch, EthSpec, LightClientBootstrap, LightClientFinalityUpdate, LightClientOptimisticUpdate, LightClientUpdate, SignedBeaconBlock, - SignedExecutionPayloadEnvelope, + SignedExecutionPayloadEnvelope, execution::eip8025::SignedExecutionProof, }; pub type Id = u32; @@ -21,8 +23,6 @@ pub struct SingleLookupReqId { pub enum SyncRequestId { /// Request searching for a block given a hash. SingleBlock { id: SingleLookupReqId }, - /// Request searching for a set of blobs given a hash. - SingleBlob { id: SingleLookupReqId }, /// Request searching for a payload envelope given a hash. SinglePayloadEnvelope { id: SingleLookupReqId }, /// Request searching for a set of data columns given a hash and list of column indices. @@ -33,6 +33,12 @@ pub enum SyncRequestId { BlobsByRange(BlobsByRangeRequestId), /// Data columns by range request DataColumnsByRange(DataColumnsByRangeRequestId), + /// Execution proofs by range request + ExecutionProofsByRange(ExecutionProofsByRangeRequestId), + /// Execution proofs by root request + ExecutionProofsByRoot(ExecutionProofsByRootRequestId), + /// Execution proof status request + ExecutionProofStatus(ExecutionProofStatusRequestId), } /// Request ID for data_columns_by_root requests. Block lookups do not issue this request directly. @@ -72,6 +78,24 @@ pub struct DataColumnsByRangeRequestId { pub peer: PeerId, } +/// Request ID for execution_proofs_by_range requests. +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub struct ExecutionProofsByRangeRequestId { + pub id: Id, +} + +/// Request ID for execution_proofs_by_root requests. +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub struct ExecutionProofsByRootRequestId { + pub id: Id, +} + +/// Request ID for execution_proof_status requests. +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub struct ExecutionProofStatusRequestId { + pub id: Id, +} + #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] pub enum DataColumnsByRangeRequester { ComponentsByRange(ComponentsByRangeRequestId), @@ -182,6 +206,12 @@ pub enum Response { LightClientFinalityUpdate(Arc>), /// A response to a LightClientUpdatesByRange request. LightClientUpdatesByRange(Option>>), + /// A response to a get EXECUTION_PROOFS_BY_RANGE request. + ExecutionProofsByRange(Option>), + /// A response to a get EXECUTION_PROOFS_BY_ROOT request. + ExecutionProofsByRoot(Option>), + /// A response to an EXECUTION_PROOF_STATUS request. + ExecutionProofStatus(ExecutionProofStatus), } impl std::convert::From> for RpcResponse { @@ -241,6 +271,21 @@ impl std::convert::From> for RpcResponse { RpcResponse::StreamTermination(ResponseTermination::LightClientUpdatesByRange) } }, + Response::ExecutionProofsByRange(r) => match r { + Some(proof) => { + RpcResponse::Success(RpcSuccessResponse::ExecutionProofsByRange(proof)) + } + None => RpcResponse::StreamTermination(ResponseTermination::ExecutionProofsByRange), + }, + Response::ExecutionProofsByRoot(r) => match r { + Some(proof) => { + RpcResponse::Success(RpcSuccessResponse::ExecutionProofsByRoot(proof)) + } + None => RpcResponse::StreamTermination(ResponseTermination::ExecutionProofsByRoot), + }, + Response::ExecutionProofStatus(status) => { + RpcResponse::Success(RpcSuccessResponse::ExecutionProofStatus(status)) + } } } } @@ -261,6 +306,9 @@ macro_rules! impl_display { impl_display!(BlocksByRangeRequestId, "{}/{}", id, parent_request_id); impl_display!(BlobsByRangeRequestId, "{}/{}", id, parent_request_id); impl_display!(DataColumnsByRangeRequestId, "{}/{}", id, parent_request_id); +impl_display!(ExecutionProofsByRangeRequestId, "ExecProofsByRange/{}", id); +impl_display!(ExecutionProofsByRootRequestId, "ExecProofsByRoot/{}", id); +impl_display!(ExecutionProofStatusRequestId, "ExecProofStatus/{}", id); impl_display!(ComponentsByRangeRequestId, "{}/{}", id, requester); impl_display!(DataColumnsByRootRequestId, "{}/{}", id, requester); impl_display!(SingleLookupReqId, "{}/Lookup/{}", req_id, lookup_id); diff --git a/beacon_node/lighthouse_network/src/service/gossip_cache.rs b/beacon_node/lighthouse_network/src/service/gossip_cache.rs index e9862e3f74e..683ec31b7de 100644 --- a/beacon_node/lighthouse_network/src/service/gossip_cache.rs +++ b/beacon_node/lighthouse_network/src/service/gossip_cache.rs @@ -20,8 +20,6 @@ pub struct GossipCache { topic_msgs: HashMap, Key>>, /// Timeout for blocks. beacon_block: Option, - /// Timeout for blobs. - blob_sidecar: Option, /// Timeout for data columns. data_column_sidecar: Option, /// Timeout for aggregate attestations. @@ -59,8 +57,6 @@ pub struct GossipCacheBuilder { default_timeout: Option, /// Timeout for blocks. beacon_block: Option, - /// Timeout for blob sidecars. - blob_sidecar: Option, /// Timeout for data column sidecars. data_column_sidecar: Option, /// Timeout for aggregate attestations. @@ -195,7 +191,6 @@ impl GossipCacheBuilder { let GossipCacheBuilder { default_timeout, beacon_block, - blob_sidecar, data_column_sidecar, aggregates, attestation, @@ -216,7 +211,6 @@ impl GossipCacheBuilder { expirations: DelayQueue::default(), topic_msgs: HashMap::default(), beacon_block: beacon_block.or(default_timeout), - blob_sidecar: blob_sidecar.or(default_timeout), data_column_sidecar: data_column_sidecar.or(default_timeout), aggregates: aggregates.or(default_timeout), attestation: attestation.or(default_timeout), @@ -247,7 +241,6 @@ impl GossipCache { pub fn insert(&mut self, topic: GossipTopic, data: Vec) { let expire_timeout = match topic.kind() { GossipKind::BeaconBlock => self.beacon_block, - GossipKind::BlobSidecar(_) => self.blob_sidecar, GossipKind::DataColumnSidecar(_) => self.data_column_sidecar, GossipKind::BeaconAggregateAndProof => self.aggregates, GossipKind::Attestation(_) => self.attestation, @@ -261,6 +254,7 @@ impl GossipCache { GossipKind::ExecutionPayloadBid => self.execution_payload_bid, GossipKind::PayloadAttestation => self.payload_attestation, GossipKind::ProposerPreferences => self.proposer_preferences, + GossipKind::ExecutionProof => None, GossipKind::LightClientFinalityUpdate => self.light_client_finality_update, GossipKind::LightClientOptimisticUpdate => self.light_client_optimistic_update, }; diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index 41d937e3245..a8338233bea 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -382,6 +382,7 @@ impl Network { let eth2_rpc = RPC::new( ctx.fork_context.clone(), config.enable_light_client_server, + config.enable_execution_proof, config.inbound_rate_limiter_config.clone(), config.outbound_rate_limiter_config.clone(), seq_number, @@ -466,9 +467,13 @@ impl Network { } }; - // Set up the transport - tcp/quic with noise and mplex - let transport = build_transport(local_keypair.clone(), !config.disable_quic_support) - .map_err(|e| format!("Failed to build transport: {:?}", e))?; + // Set up the transport - tcp/quic with noise and yamux (mplex optional) + let transport = build_transport( + local_keypair.clone(), + !config.disable_quic_support, + config.enable_mplex, + ) + .map_err(|e| format!("Failed to build transport: {:?}", e))?; // use the executor for libp2p struct Executor(task_executor::TaskExecutor); @@ -1721,6 +1726,39 @@ impl Network { request_type, }) } + RequestType::ExecutionProofsByRange(_) => { + metrics::inc_counter_vec( + &metrics::TOTAL_RPC_REQUESTS, + &["execution_proofs_by_range"], + ); + Some(NetworkEvent::RequestReceived { + peer_id, + inbound_request_id, + request_type, + }) + } + RequestType::ExecutionProofsByRoot(_) => { + metrics::inc_counter_vec( + &metrics::TOTAL_RPC_REQUESTS, + &["execution_proofs_by_root"], + ); + Some(NetworkEvent::RequestReceived { + peer_id, + inbound_request_id, + request_type, + }) + } + RequestType::ExecutionProofStatus(_) => { + metrics::inc_counter_vec( + &metrics::TOTAL_RPC_REQUESTS, + &["execution_proof_status"], + ); + Some(NetworkEvent::RequestReceived { + peer_id, + inbound_request_id, + request_type, + }) + } RequestType::BlobsByRange(_) => { metrics::inc_counter_vec(&metrics::TOTAL_RPC_REQUESTS, &["blobs_by_range"]); Some(NetworkEvent::RequestReceived { @@ -1848,6 +1886,19 @@ impl Network { peer_id, Response::PayloadEnvelopesByRoot(Some(resp)), ), + RpcSuccessResponse::ExecutionProofsByRange(resp) => self.build_response( + id, + peer_id, + Response::ExecutionProofsByRange(Some(resp)), + ), + RpcSuccessResponse::ExecutionProofsByRoot(resp) => self.build_response( + id, + peer_id, + Response::ExecutionProofsByRoot(Some(resp)), + ), + RpcSuccessResponse::ExecutionProofStatus(status) => { + self.build_response(id, peer_id, Response::ExecutionProofStatus(status)) + } RpcSuccessResponse::BlobsByRoot(resp) => { self.build_response(id, peer_id, Response::BlobsByRoot(Some(resp))) } @@ -1889,6 +1940,12 @@ impl Network { ResponseTermination::PayloadEnvelopesByRoot => { Response::PayloadEnvelopesByRoot(None) } + ResponseTermination::ExecutionProofsByRange => { + Response::ExecutionProofsByRange(None) + } + ResponseTermination::ExecutionProofsByRoot => { + Response::ExecutionProofsByRoot(None) + } ResponseTermination::BlobsByRange => Response::BlobsByRange(None), ResponseTermination::BlobsByRoot => Response::BlobsByRoot(None), ResponseTermination::DataColumnsByRoot => Response::DataColumnsByRoot(None), diff --git a/beacon_node/lighthouse_network/src/service/utils.rs b/beacon_node/lighthouse_network/src/service/utils.rs index d235e4b28f6..d4df7a7c01f 100644 --- a/beacon_node/lighthouse_network/src/service/utils.rs +++ b/beacon_node/lighthouse_network/src/service/utils.rs @@ -34,27 +34,39 @@ pub struct Context<'a> { type BoxedTransport = Boxed<(PeerId, StreamMuxerBox)>; /// The implementation supports TCP/IP, QUIC (experimental) over UDP, noise as the encryption layer, and -/// mplex/yamux as the multiplexing layer (when using TCP). +/// yamux as the multiplexing layer (when using TCP). Mplex can be optionally enabled. pub fn build_transport( local_private_key: Keypair, quic_support: bool, + enable_mplex: bool, ) -> std::io::Result { - // mplex config - let mut mplex_config = libp2p_mplex::Config::new(); - mplex_config.set_max_buffer_size(256); - mplex_config.set_max_buffer_behaviour(libp2p_mplex::MaxBufferBehaviour::Block); - // yamux config let yamux_config = yamux::Config::default(); + // Creates the TCP transport layer - let tcp = libp2p::tcp::tokio::Transport::new(libp2p::tcp::Config::default().nodelay(true)) - .upgrade(core::upgrade::Version::V1) - .authenticate(generate_noise_config(&local_private_key)) - .multiplex(core::upgrade::SelectUpgrade::new( - yamux_config, - mplex_config, - )) - .timeout(Duration::from_secs(10)); + let tcp: BoxedTransport = if enable_mplex { + // Enable both yamux and mplex. + let mut mplex_config = libp2p_mplex::Config::new(); + mplex_config.set_max_num_streams(32); + mplex_config.set_max_buffer_behaviour(libp2p_mplex::MaxBufferBehaviour::ResetStream); + libp2p::tcp::tokio::Transport::new(libp2p::tcp::Config::default().nodelay(true)) + .upgrade(core::upgrade::Version::V1) + .authenticate(generate_noise_config(&local_private_key)) + .multiplex(core::upgrade::SelectUpgrade::new( + yamux_config, + mplex_config, + )) + .timeout(Duration::from_secs(10)) + .boxed() + } else { + // Yamux only + libp2p::tcp::tokio::Transport::new(libp2p::tcp::Config::default().nodelay(true)) + .upgrade(core::upgrade::Version::V1) + .authenticate(generate_noise_config(&local_private_key)) + .multiplex(yamux_config) + .timeout(Duration::from_secs(10)) + .boxed() + }; let transport = if quic_support { // Enables Quic // The default quic configuration suits us for now. @@ -276,6 +288,7 @@ pub(crate) fn create_whitelist_filter( add(ExecutionPayloadBid); add(PayloadAttestation); add(ProposerPreferences); + add(ExecutionProof); add(LightClientFinalityUpdate); add(LightClientOptimisticUpdate); for id in 0..spec.attestation_subnet_count { @@ -284,10 +297,6 @@ pub(crate) fn create_whitelist_filter( for id in 0..sync_committee_subnet_count { add(SyncCommitteeMessage(SyncSubnetId::new(id))); } - let blob_subnet_count = spec.blob_sidecar_subnet_count_max(); - for id in 0..blob_subnet_count { - add(BlobSidecar(id)); - } for id in 0..spec.data_column_sidecar_subnet_count { add(DataColumnSidecar(DataColumnSubnetId::new(id))); } diff --git a/beacon_node/lighthouse_network/src/types/globals.rs b/beacon_node/lighthouse_network/src/types/globals.rs index df8dbdc559e..0a9642bd826 100644 --- a/beacon_node/lighthouse_network/src/types/globals.rs +++ b/beacon_node/lighthouse_network/src/types/globals.rs @@ -226,6 +226,7 @@ impl NetworkGlobals { pub fn as_topic_config(&self) -> TopicConfig { TopicConfig { enable_light_client_server: self.config.enable_light_client_server, + enable_execution_proof: self.config.enable_execution_proof, subscribe_all_subnets: self.config.subscribe_all_subnets, sampling_subnets: self.sampling_subnets.read().clone(), } diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index e5a703ff1e5..61a9b4b1b0a 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -7,25 +7,23 @@ use ssz::{Decode, Encode}; use std::io::{Error, ErrorKind}; use std::sync::Arc; use types::{ - AttesterSlashing, AttesterSlashingBase, AttesterSlashingElectra, BlobSidecar, - DataColumnSidecar, DataColumnSubnetId, EthSpec, ForkContext, ForkName, Hash256, - LightClientFinalityUpdate, LightClientOptimisticUpdate, PartialDataColumn, - PartialDataColumnSidecar, PayloadAttestationMessage, ProposerSlashing, SignedAggregateAndProof, + AttesterSlashing, AttesterSlashingBase, AttesterSlashingElectra, DataColumnSidecar, + DataColumnSubnetId, EthSpec, ForkContext, ForkName, Hash256, LightClientFinalityUpdate, + LightClientOptimisticUpdate, PartialDataColumn, PartialDataColumnSidecar, + PayloadAttestationMessage, ProposerSlashing, SignedAggregateAndProof, SignedAggregateAndProofBase, SignedAggregateAndProofElectra, SignedBeaconBlock, SignedBeaconBlockAltair, SignedBeaconBlockBase, SignedBeaconBlockBellatrix, SignedBeaconBlockCapella, SignedBeaconBlockDeneb, SignedBeaconBlockElectra, SignedBeaconBlockFulu, SignedBeaconBlockGloas, SignedBlsToExecutionChange, SignedContributionAndProof, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, - SignedProposerPreferences, SignedVoluntaryExit, SingleAttestation, SubnetId, - SyncCommitteeMessage, SyncSubnetId, + SignedExecutionProof, SignedProposerPreferences, SignedVoluntaryExit, SingleAttestation, + SubnetId, SyncCommitteeMessage, SyncSubnetId, }; #[derive(Debug, Clone, PartialEq)] pub enum PubsubMessage { /// Gossipsub message providing notification of a new block. BeaconBlock(Arc>), - /// Gossipsub message providing notification of a [`BlobSidecar`] along with the subnet id where it was received. - BlobSidecar(Box<(u64, Arc>)>), /// Gossipsub message providing notification of a [`DataColumnSidecar`] along with the subnet id where it was received. DataColumnSidecar(Box<(DataColumnSubnetId, Arc>)>), /// Gossipsub message providing notification of a Aggregate attestation and associated proof. @@ -52,6 +50,8 @@ pub enum PubsubMessage { ExecutionPayloadBid(Box>), /// Gossipsub message providing notification of signed proposer preferences. ProposerPreferences(Arc), + /// EIP-8025 signed execution proof. + ExecutionProof(Arc), /// Gossipsub message providing notification of a light client finality update. LightClientFinalityUpdate(Box>), /// Gossipsub message providing notification of a light client optimistic update. @@ -139,9 +139,6 @@ impl PubsubMessage { pub fn kind(&self) -> GossipKind { match self { PubsubMessage::BeaconBlock(_) => GossipKind::BeaconBlock, - PubsubMessage::BlobSidecar(blob_sidecar_data) => { - GossipKind::BlobSidecar(blob_sidecar_data.0) - } PubsubMessage::DataColumnSidecar(column_sidecar_data) => { GossipKind::DataColumnSidecar(column_sidecar_data.0) } @@ -159,6 +156,7 @@ impl PubsubMessage { PubsubMessage::PayloadAttestation(_) => GossipKind::PayloadAttestation, PubsubMessage::ExecutionPayloadBid(_) => GossipKind::ExecutionPayloadBid, PubsubMessage::ProposerPreferences(_) => GossipKind::ProposerPreferences, + PubsubMessage::ExecutionProof(_) => GossipKind::ExecutionProof, PubsubMessage::LightClientFinalityUpdate(_) => GossipKind::LightClientFinalityUpdate, PubsubMessage::LightClientOptimisticUpdate(_) => { GossipKind::LightClientOptimisticUpdate @@ -266,26 +264,6 @@ impl PubsubMessage { }; Ok(PubsubMessage::BeaconBlock(Arc::new(beacon_block))) } - GossipKind::BlobSidecar(blob_index) => { - if let Some(fork_name) = - fork_context.get_fork_from_context_bytes(gossip_topic.fork_digest) - && fork_name.deneb_enabled() - { - let blob_sidecar = Arc::new( - BlobSidecar::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?, - ); - return Ok(PubsubMessage::BlobSidecar(Box::new(( - *blob_index, - blob_sidecar, - )))); - } - - Err(format!( - "beacon_blobs_and_sidecar topic invalid for given fork digest {:?}", - gossip_topic.fork_digest - )) - } GossipKind::DataColumnSidecar(subnet_id) => { match fork_context.get_fork_from_context_bytes(gossip_topic.fork_digest) { Some(fork) if fork.fulu_enabled() => { @@ -392,6 +370,11 @@ impl PubsubMessage { proposer_preferences, ))) } + GossipKind::ExecutionProof => { + let execution_proof = SignedExecutionProof::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?; + Ok(PubsubMessage::ExecutionProof(Arc::new(execution_proof))) + } GossipKind::LightClientFinalityUpdate => { let light_client_finality_update = match fork_context .get_fork_from_context_bytes(gossip_topic.fork_digest) @@ -444,7 +427,6 @@ impl PubsubMessage { // messages for us. match &self { PubsubMessage::BeaconBlock(data) => data.as_ssz_bytes(), - PubsubMessage::BlobSidecar(data) => data.1.as_ssz_bytes(), PubsubMessage::DataColumnSidecar(data) => data.1.as_ssz_bytes(), PubsubMessage::AggregateAndProofAttestation(data) => data.as_ssz_bytes(), PubsubMessage::VoluntaryExit(data) => data.as_ssz_bytes(), @@ -458,6 +440,7 @@ impl PubsubMessage { PubsubMessage::PayloadAttestation(data) => data.as_ssz_bytes(), PubsubMessage::ExecutionPayloadBid(data) => data.as_ssz_bytes(), PubsubMessage::ProposerPreferences(data) => data.as_ssz_bytes(), + PubsubMessage::ExecutionProof(data) => data.as_ssz_bytes(), PubsubMessage::LightClientFinalityUpdate(data) => data.as_ssz_bytes(), PubsubMessage::LightClientOptimisticUpdate(data) => data.as_ssz_bytes(), } @@ -502,12 +485,6 @@ impl std::fmt::Display for PubsubMessage { block.slot(), block.message().proposer_index() ), - PubsubMessage::BlobSidecar(data) => write!( - f, - "BlobSidecar: slot: {}, blob index: {}", - data.1.slot(), - data.1.index, - ), PubsubMessage::DataColumnSidecar(data) => write!( f, "DataColumnSidecar: slot: {}, column index: {}", @@ -574,6 +551,14 @@ impl std::fmt::Display for PubsubMessage { data.message.proposal_slot, data.message.validator_index ) } + PubsubMessage::ExecutionProof(data) => { + write!( + f, + "Execution Proof: request_root: {:?}, proof_type: {}", + data.request_root(), + data.proof_type() + ) + } PubsubMessage::LightClientFinalityUpdate(_data) => { write!(f, "Light CLient Finality Update") } diff --git a/beacon_node/lighthouse_network/src/types/topics.rs b/beacon_node/lighthouse_network/src/types/topics.rs index b51c459a809..14440e7abd4 100644 --- a/beacon_node/lighthouse_network/src/types/topics.rs +++ b/beacon_node/lighthouse_network/src/types/topics.rs @@ -21,7 +21,6 @@ pub const SSZ_SNAPPY_ENCODING_POSTFIX: &str = "ssz_snappy"; pub const BEACON_BLOCK_TOPIC: &str = "beacon_block"; pub const BEACON_AGGREGATE_AND_PROOF_TOPIC: &str = "beacon_aggregate_and_proof"; pub const BEACON_ATTESTATION_PREFIX: &str = "beacon_attestation_"; -pub const BLOB_SIDECAR_PREFIX: &str = "blob_sidecar_"; pub const DATA_COLUMN_SIDECAR_PREFIX: &str = "data_column_sidecar_"; pub const VOLUNTARY_EXIT_TOPIC: &str = "voluntary_exit"; pub const PROPOSER_SLASHING_TOPIC: &str = "proposer_slashing"; @@ -33,12 +32,14 @@ pub const EXECUTION_PAYLOAD: &str = "execution_payload"; pub const EXECUTION_PAYLOAD_BID: &str = "execution_payload_bid"; pub const PAYLOAD_ATTESTATION: &str = "payload_attestation_message"; pub const PROPOSER_PREFERENCES: &str = "proposer_preferences"; +pub const EXECUTION_PROOF_TOPIC: &str = "execution_proof"; pub const LIGHT_CLIENT_FINALITY_UPDATE: &str = "light_client_finality_update"; pub const LIGHT_CLIENT_OPTIMISTIC_UPDATE: &str = "light_client_optimistic_update"; #[derive(Debug)] pub struct TopicConfig { pub enable_light_client_server: bool, + pub enable_execution_proof: bool, pub subscribe_all_subnets: bool, pub sampling_subnets: HashSet, } @@ -82,13 +83,6 @@ pub fn core_topics_to_subscribe( topics.push(GossipKind::BlsToExecutionChange); } - if fork_name.deneb_enabled() && !fork_name.fulu_enabled() { - // All of deneb blob topics are core topics - for i in 0..spec.blob_sidecar_subnet_count(fork_name) { - topics.push(GossipKind::BlobSidecar(i)); - } - } - if fork_name.fulu_enabled() { for subnet in &opts.sampling_subnets { topics.push(GossipKind::DataColumnSidecar(*subnet)); @@ -102,6 +96,10 @@ pub fn core_topics_to_subscribe( topics.push(GossipKind::ProposerPreferences); } + if opts.enable_execution_proof { + topics.push(GossipKind::ExecutionProof); + } + topics } @@ -118,7 +116,6 @@ pub fn is_fork_non_core_topic(topic: &GossipTopic, _fork_name: ForkName) -> bool // All these topics are core-only GossipKind::BeaconBlock | GossipKind::BeaconAggregateAndProof - | GossipKind::BlobSidecar(_) | GossipKind::DataColumnSidecar(_) | GossipKind::VoluntaryExit | GossipKind::ProposerSlashing @@ -129,6 +126,7 @@ pub fn is_fork_non_core_topic(topic: &GossipTopic, _fork_name: ForkName) -> bool | GossipKind::ExecutionPayloadBid | GossipKind::PayloadAttestation | GossipKind::ProposerPreferences + | GossipKind::ExecutionProof | GossipKind::LightClientFinalityUpdate | GossipKind::LightClientOptimisticUpdate => false, } @@ -139,6 +137,7 @@ pub fn all_topics_at_fork(fork: ForkName, spec: &ChainSpec) -> Vec { write!(f, "sync_committee_{}", **subnet_id) } - GossipKind::BlobSidecar(blob_index) => { - write!(f, "{}{}", BLOB_SIDECAR_PREFIX, blob_index) - } GossipKind::DataColumnSidecar(column_subnet_id) => { write!(f, "{}{}", DATA_COLUMN_SIDECAR_PREFIX, **column_subnet_id) } @@ -291,6 +287,7 @@ impl GossipTopic { EXECUTION_PAYLOAD_BID => GossipKind::ExecutionPayloadBid, PAYLOAD_ATTESTATION => GossipKind::PayloadAttestation, PROPOSER_PREFERENCES => GossipKind::ProposerPreferences, + EXECUTION_PROOF_TOPIC => GossipKind::ExecutionProof, LIGHT_CLIENT_FINALITY_UPDATE => GossipKind::LightClientFinalityUpdate, LIGHT_CLIENT_OPTIMISTIC_UPDATE => GossipKind::LightClientOptimisticUpdate, topic => match subnet_topic_index(topic) { @@ -349,9 +346,6 @@ impl std::fmt::Display for GossipTopic { GossipKind::SyncCommitteeMessage(index) => { format!("{}{}", SYNC_COMMITTEE_PREFIX_TOPIC, *index) } - GossipKind::BlobSidecar(blob_index) => { - format!("{}{}", BLOB_SIDECAR_PREFIX, blob_index) - } GossipKind::DataColumnSidecar(column_subnet_id) => { format!("{}{}", DATA_COLUMN_SIDECAR_PREFIX, *column_subnet_id) } @@ -360,6 +354,7 @@ impl std::fmt::Display for GossipTopic { GossipKind::PayloadAttestation => PAYLOAD_ATTESTATION.into(), GossipKind::ExecutionPayloadBid => EXECUTION_PAYLOAD_BID.into(), GossipKind::ProposerPreferences => PROPOSER_PREFERENCES.into(), + GossipKind::ExecutionProof => EXECUTION_PROOF_TOPIC.into(), GossipKind::LightClientFinalityUpdate => LIGHT_CLIENT_FINALITY_UPDATE.into(), GossipKind::LightClientOptimisticUpdate => LIGHT_CLIENT_OPTIMISTIC_UPDATE.into(), }; @@ -401,8 +396,6 @@ fn subnet_topic_index(topic: &str) -> Option { return Some(GossipKind::SyncCommitteeMessage(SyncSubnetId::new( index.parse::().ok()?, ))); - } else if let Some(index) = topic.strip_prefix(BLOB_SIDECAR_PREFIX) { - return Some(GossipKind::BlobSidecar(index.parse::().ok()?)); } else if let Some(index) = topic.strip_prefix(DATA_COLUMN_SIDECAR_PREFIX) { return Some(GossipKind::DataColumnSidecar(DataColumnSubnetId::new( index.parse::().ok()?, @@ -558,6 +551,7 @@ mod tests { fn get_topic_config(sampling_subnets: &HashSet) -> TopicConfig { TopicConfig { enable_light_client_server: false, + enable_execution_proof: false, subscribe_all_subnets: false, sampling_subnets: sampling_subnets.clone(), } @@ -576,17 +570,6 @@ mod tests { } } - #[test] - fn blobs_are_not_subscribed_in_peerdas() { - let spec = get_spec(); - let s = get_sampling_subnets(); - let topic_config = get_topic_config(&s); - assert!( - !core_topics_to_subscribe::(ForkName::Fulu, &topic_config, &spec,) - .contains(&GossipKind::BlobSidecar(0)) - ); - } - #[test] fn columns_are_subscribed_in_peerdas() { let spec = get_spec(); diff --git a/beacon_node/network/src/lib.rs b/beacon_node/network/src/lib.rs index 2a7fedb53e9..dc45f53c705 100644 --- a/beacon_node/network/src/lib.rs +++ b/beacon_node/network/src/lib.rs @@ -11,6 +11,7 @@ mod subnet_service; mod sync; pub use lighthouse_network::NetworkConfig; +pub use network_beacon_processor::NetworkBeaconProcessor; pub use service::{ NetworkMessage, NetworkReceivers, NetworkSenders, NetworkService, ValidatorSubscriptionMessage, }; diff --git a/beacon_node/network/src/metrics.rs b/beacon_node/network/src/metrics.rs index 4b34d7bfc0b..c043133cee9 100644 --- a/beacon_node/network/src/metrics.rs +++ b/beacon_node/network/src/metrics.rs @@ -128,13 +128,6 @@ pub static BEACON_PROCESSOR_GOSSIP_BLOCK_EARLY_SECONDS: LazyLock> = - LazyLock::new(|| { - try_create_int_counter( - "beacon_processor_gossip_blob_verified_total", - "Total number of gossip blob verified for propagation.", - ) - }); pub static BEACON_PROCESSOR_GOSSIP_DATA_COLUMN_SIDECAR_VERIFIED_TOTAL: LazyLock< Result, > = LazyLock::new(|| { @@ -600,12 +593,6 @@ pub static BEACON_BLOCK_DELAY_GOSSIP_ARRIVED_LATE_TOTAL: LazyLock> = LazyLock::new(|| { - try_create_int_gauge( - "beacon_blob_delay_gossip_last_delay", - "The first time we see this blob as a delay from the start of the slot", - ) -}); pub static BEACON_DATA_COLUMN_GOSSIP_PROPAGATION_VERIFICATION_DELAY_TIME: LazyLock< Result, @@ -664,14 +651,6 @@ pub static BEACON_USEFUL_FULL_COLUMNS_RECEIVED_TOTAL: LazyLock> = LazyLock::new( - || { - try_create_int_gauge( - "beacon_blob_delay_gossip_verification", - "Keeps track of the time delay from the start of the slot to the point we propagate the blob", - ) - }, -); pub static BEACON_BLOB_DELAY_FULL_VERIFICATION: LazyLock> = LazyLock::new(|| { try_create_int_gauge( "beacon_blob_last_full_verification_delay", @@ -695,15 +674,6 @@ pub static BEACON_BLOB_RPC_SLOT_START_DELAY_TIME: LazyLock> = }, ); -pub static BEACON_BLOB_GOSSIP_ARRIVED_LATE_TOTAL: LazyLock> = LazyLock::new( - || { - try_create_int_counter( - "beacon_blob_gossip_arrived_late_total", - "Count of times when a gossip blob arrived from the network later than the attestation deadline.", - ) - }, -); - /* * Light client update reprocessing queue metrics. */ diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index 14cda1b4836..f62a2eb793b 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -10,7 +10,11 @@ use beacon_chain::data_column_verification::{ GossipVerifiedPartialDataColumnHeader, KzgVerifiedPartialDataColumn, PartialColumnVerificationResult, }; +use beacon_chain::internal_events::InternalBeaconNodeEvent; use beacon_chain::payload_bid_verification::PayloadBidError; +use beacon_chain::payload_envelope_verification::{ + EnvelopeError, gossip_verified_envelope::GossipVerifiedEnvelope, +}; use beacon_chain::proposer_preferences_verification::ProposerPreferencesError; use beacon_chain::store::Error; use beacon_chain::{ @@ -18,6 +22,7 @@ use beacon_chain::{ GossipVerifiedBlock, NotifyExecutionLayer, attestation_verification::{self, Error as AttnError, VerifiedAttestation}, data_availability_checker::AvailabilityCheckErrorCategory, + eip8025::ExecutionProofError, light_client_finality_update_verification::Error as LightClientFinalityUpdateError, light_client_optimistic_update_verification::Error as LightClientOptimisticUpdateError, observed_operations::ObservationOutcome, @@ -27,12 +32,6 @@ use beacon_chain::{ sync_committee_verification::{self, Error as SyncCommitteeError}, validator_monitor::{get_block_delay_ms, get_slot_delay_ms}, }; -use beacon_chain::{ - blob_verification::{GossipBlobError, GossipVerifiedBlob}, - payload_envelope_verification::{ - EnvelopeError, gossip_verified_envelope::GossipVerifiedEnvelope, - }, -}; use beacon_processor::{Work, WorkEvent}; use lighthouse_network::{ Client, GossipTopic, MessageAcceptance, MessageId, PeerAction, PeerId, PubsubMessage, @@ -50,22 +49,22 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use store::hot_cold_store::HotColdDBError; use tracing::{Instrument, Span, debug, error, info, instrument, trace, warn}; use types::{ - Attestation, AttestationData, AttestationRef, AttesterSlashing, BlobSidecar, ColumnIndex, - DataColumnSidecar, DataColumnSubnetId, EthSpec, Hash256, IndexedAttestation, - LightClientFinalityUpdate, LightClientOptimisticUpdate, PartialDataColumn, - PartialDataColumnHeader, PayloadAttestationMessage, ProposerSlashing, SignedAggregateAndProof, + Attestation, AttestationData, AttestationRef, AttesterSlashing, ColumnIndex, DataColumnSidecar, + DataColumnSubnetId, EthSpec, Hash256, IndexedAttestation, LightClientFinalityUpdate, + LightClientOptimisticUpdate, PartialDataColumn, PartialDataColumnHeader, + PayloadAttestationMessage, ProofStatus, ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, SignedBlsToExecutionChange, SignedContributionAndProof, - SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, SignedProposerPreferences, - SignedVoluntaryExit, SingleAttestation, Slot, SubnetId, SyncCommitteeMessage, SyncSubnetId, - block::BlockImportSource, + SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, SignedExecutionProof, + SignedProposerPreferences, SignedVoluntaryExit, SingleAttestation, Slot, SubnetId, + SyncCommitteeMessage, SyncSubnetId, block::BlockImportSource, }; use beacon_processor::work_reprocessing_queue::QueuedColumnReconstruction; use beacon_processor::{ DuplicateCache, GossipAggregatePackage, GossipAttestationBatch, work_reprocessing_queue::{ - QueuedAggregate, QueuedGossipBlock, QueuedGossipEnvelope, QueuedLightClientUpdate, - QueuedUnaggregate, ReprocessQueueMessage, + QueuedAggregate, QueuedGossipBlock, QueuedGossipDataColumn, QueuedGossipEnvelope, + QueuedLightClientUpdate, QueuedUnaggregate, ReprocessQueueMessage, }, }; @@ -174,6 +173,30 @@ impl FailedAtt { } } +/// `MessageAcceptance` doesn't implement clone so we do a manual match here. +/// TODO: remove this once `Clone` is available on this type: +/// https://github.com/libp2p/rust-libp2p/pull/6445 +fn clone_message_acceptance(a: &MessageAcceptance) -> MessageAcceptance { + match a { + MessageAcceptance::Accept => MessageAcceptance::Accept, + MessageAcceptance::Reject => MessageAcceptance::Reject, + MessageAcceptance::Ignore => MessageAcceptance::Ignore, + } +} + +fn is_invalid_execution_proof_error(error: &BeaconChainError) -> bool { + matches!( + error, + BeaconChainError::ExecutionProofError( + ExecutionProofError::InvalidSignature + | ExecutionProofError::EmptyProofData + | ExecutionProofError::InvalidValidatorIndex + | ExecutionProofError::InvalidValidatorPubkey + | ExecutionProofError::InvalidSignatureFormat + ) + ) +} + impl NetworkBeaconProcessor { /* Auxiliary functions */ @@ -206,6 +229,147 @@ impl NetworkBeaconProcessor { }) } + pub async fn process_gossip_execution_proof( + self: Arc, + message_id: MessageId, + peer_id: PeerId, + execution_proof: Arc, + ) { + if self.chain.internal_event_sender().is_some() { + self.chain + .emit_internal_event(InternalBeaconNodeEvent::GossipExecutionProof( + execution_proof.clone(), + )); + } + + let request_root = execution_proof.request_root(); + let verification_result = self + .chain + .verify_and_observe_execution_proof(&execution_proof, None) + .await; + + if self.chain.internal_event_sender().is_some() { + self.chain.emit_internal_event(match &verification_result { + Ok(observation) => InternalBeaconNodeEvent::ExecutionProofVerified { + request_root, + status: observation.status, + block: None, + }, + Err(error) => InternalBeaconNodeEvent::ExecutionProofVerificationFailed { + request_root, + error: format!("{error:?}"), + }, + }); + } + + match verification_result { + Ok(observation) => match observation.status { + ProofStatus::Valid | ProofStatus::Accepted => { + self.propagate_validation_result( + message_id, + peer_id, + MessageAcceptance::Accept, + ); + } + ProofStatus::Syncing | ProofStatus::NotSupported => { + self.propagate_validation_result( + message_id, + peer_id, + MessageAcceptance::Ignore, + ); + } + ProofStatus::Invalid => { + self.gossip_penalize_peer( + peer_id, + PeerAction::LowToleranceError, + "invalid execution proof", + ); + self.propagate_validation_result( + message_id, + peer_id, + MessageAcceptance::Reject, + ); + } + }, + Err(error) if is_invalid_execution_proof_error(&error) => { + self.gossip_penalize_peer( + peer_id, + PeerAction::LowToleranceError, + "invalid execution proof", + ); + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); + } + Err(error) => { + debug!(?error, %peer_id, "Could not verify gossip execution proof"); + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); + } + } + } + + pub async fn process_rpc_execution_proof( + self: Arc, + peer_id: PeerId, + execution_proof: Arc, + ) { + if self.chain.internal_event_sender().is_some() { + self.chain + .emit_internal_event(InternalBeaconNodeEvent::RpcExecutionProof( + execution_proof.clone(), + )); + } + + let request_root = execution_proof.request_root(); + let verification_result = self + .chain + .verify_and_observe_execution_proof(&execution_proof, None) + .await; + + if self.chain.internal_event_sender().is_some() { + self.chain.emit_internal_event(match &verification_result { + Ok(observation) => InternalBeaconNodeEvent::ExecutionProofVerified { + request_root, + status: observation.status, + block: None, + }, + Err(error) => InternalBeaconNodeEvent::ExecutionProofVerificationFailed { + request_root, + error: format!("{error:?}"), + }, + }); + } + + match verification_result { + Ok(observation) if observation.status == ProofStatus::Invalid => { + self.send_network_message(NetworkMessage::ReportPeer { + peer_id, + action: PeerAction::LowToleranceError, + source: ReportSource::SyncService, + msg: "invalid execution proof", + }); + } + Ok(observation) => { + debug!( + %peer_id, + status = %observation.status, + request_root = %observation.request_root, + block_root = ?observation.block_root, + "Observed RPC execution proof" + ); + } + Err(error) if is_invalid_execution_proof_error(&error) => { + self.send_network_message(NetworkMessage::ReportPeer { + peer_id, + action: PeerAction::LowToleranceError, + source: ReportSource::SyncService, + msg: "invalid execution proof", + }); + } + Err(error) => { + debug!(?error, %peer_id, "Could not verify RPC execution proof"); + } + } + } + /// Send a message on `message_tx` that `peer_id` has sent an invalid partial message and should /// be penalized. pub(crate) fn propagate_partial_validation_failure( @@ -649,6 +813,7 @@ impl NetworkBeaconProcessor { subnet_id: DataColumnSubnetId, column_sidecar: Arc>, seen_duration: Duration, + allow_reprocess: bool, ) { let slot = column_sidecar.slot(); let block_root = column_sidecar.block_root(); @@ -711,36 +876,67 @@ impl NetworkBeaconProcessor { MessageAcceptance::Accept, ); } - GossipDataColumnError::ParentUnknown { parent_root, .. } => { + GossipDataColumnError::ParentUnknown { parent_root, slot } => { debug!( action = "requesting parent", %block_root, %parent_root, "Unknown parent hash for column" ); - self.send_sync_message(SyncMessage::UnknownParentDataColumn( + self.send_sync_message(SyncMessage::UnknownParentSidecarHeader { peer_id, - column_sidecar, - )); + block_root, + parent_root, + slot, + }); } GossipDataColumnError::BlockRootUnknown { block_root: unknown_block_root, .. } => { debug!( - action = "ignoring", + action = "queuing for reprocessing", %unknown_block_root, "Unknown block root for column" ); - // TODO(gloas): wire this into proper lookup sync. Sending - // `UnknownBlockHashFromAttestation` here is a Fulu-shaped fallback that - // mixes column processing with the attestation lookup path and is not - // the right primitive for Gloas column lookups. self.propagate_validation_result( - message_id, + message_id.clone(), peer_id, MessageAcceptance::Ignore, ); + + if allow_reprocess { + // Queue the column for reprocessing when the block arrives. + let processor = self.clone(); + let reprocess_msg = ReprocessQueueMessage::UnknownBlockDataColumn( + QueuedGossipDataColumn { + beacon_block_root: unknown_block_root, + process_fn: Box::new(move || { + let _ = processor.send_gossip_data_column_sidecar( + message_id, + peer_id, + subnet_id, + column_sidecar, + seen_duration, + false, // Do not reprocess this message again. + ); + }), + }, + ); + if self + .beacon_processor_send + .try_send(WorkEvent { + drop_during_sync: false, + work: Work::Reprocess(reprocess_msg), + }) + .is_err() + { + debug!( + %unknown_block_root, + "Failed to queue data column for reprocessing" + ); + } + } } GossipDataColumnError::InvalidVariant | GossipDataColumnError::PubkeyCacheTimeout @@ -833,13 +1029,109 @@ impl NetworkBeaconProcessor { } } - #[instrument( - name = "lh_process_gossip_partial_data_column", - parent = None, - level = "debug", - skip_all, - fields(block_root = ?column.block_root, index = column.index), - )] + async fn process_gossip_verified_data_column( + self: &Arc, + peer_id: PeerId, + verified_data_column: GossipVerifiedDataColumn, + // This value is not used presently, but it might come in handy for debugging. + _seen_duration: Duration, + ) { + let processing_start_time = Instant::now(); + let block_root = verified_data_column.block_root(); + let data_column_slot = verified_data_column.slot(); + let data_column_index = verified_data_column.index(); + + // TODO(gloas): implement partial messages + if let DataColumnSidecar::Fulu(col) = verified_data_column.as_data_column() + && self + .chain + .data_availability_checker + .partial_assembler() + .is_some_and(|a| !a.is_complete(block_root, verified_data_column.index())) + { + metrics::inc_counter_vec( + &metrics::BEACON_USEFUL_FULL_COLUMNS_RECEIVED_TOTAL, + &[&data_column_index.to_string()], + ); + + match col.to_partial() { + Ok(mut column) => { + let header = column.sidecar.header.take(); + if let Some(header) = header { + self.send_network_message(NetworkMessage::PublishPartialColumns { + columns: vec![Arc::new(column)], + header: Arc::new(header), + }); + } else { + crit!("Converting from full to partial yielded headerless partial") + }; + } + Err(err) => crit!(?err, "Could not convert from full to partial"), + } + } + + let result = self + .chain + .process_gossip_data_columns(vec![verified_data_column], || Ok(())) + .await; + register_process_result_metrics(&result, metrics::BlockSource::Gossip, "data_column"); + + match result { + Ok(availability) => match availability { + AvailabilityProcessingStatus::Imported(block_root) => { + debug!( + %block_root, + "Gossipsub data column processed, imported fully available block" + ); + self.chain.recompute_head_at_current_slot().await; + + metrics::set_gauge( + &metrics::BEACON_BLOB_DELAY_FULL_VERIFICATION, + processing_start_time.elapsed().as_millis() as i64, + ); + + // If a block is in the da_checker, sync maybe awaiting for an event when block is finally + // imported. A block can become imported both after processing a block or data column. If + // importing a block results in `Imported`, notify. Do not notify of data column errors. + self.send_sync_message(SyncMessage::GossipBlockProcessResult { + block_root, + imported: true, + }); + } + AvailabilityProcessingStatus::MissingComponents(slot, block_root) => { + trace!( + %slot, + %data_column_index, + %block_root, + "Processed data column, waiting for other components" + ); + + self.check_reconstruction_trigger(slot, &block_root).await; + } + }, + Err(BlockError::DuplicateFullyImported(_)) => { + debug!( + ?block_root, + data_column_index, "Ignoring gossip column already imported" + ); + } + Err(err) => { + debug!( + outcome = ?err, + ?block_root, + block_slot = %data_column_slot, + data_column_index, + "Invalid gossip data column" + ); + self.gossip_penalize_peer( + peer_id, + PeerAction::MidToleranceError, + "bad_gossip_data_column_ssz", + ); + } + } + } + pub async fn process_gossip_partial_data_column_sidecar( self: &Arc, peer_id: PeerId, @@ -943,7 +1235,7 @@ impl NetworkBeaconProcessor { %parent_root, "Unknown parent hash for partial column" ); - self.send_sync_message(SyncMessage::UnknownParentPartialDataColumn { + self.send_sync_message(SyncMessage::UnknownParentSidecarHeader { peer_id, block_root, parent_root, @@ -997,7 +1289,6 @@ impl NetworkBeaconProcessor { %index, "Could not verify partial column for gossip. Rejecting the column sidecar" ); - // Prevent recurring behaviour by penalizing the peer slightly. self.gossip_penalize_peer( peer_id, PeerAction::LowToleranceError, @@ -1006,9 +1297,6 @@ impl NetworkBeaconProcessor { self.propagate_partial_validation_failure(peer_id, topic); } GossipDataColumnError::PriorKnown { .. } => { - // Data column is available via either the EL or reconstruction. - // Do not penalise the peer. - // Gossip filter should filter any duplicates received after this. debug!( %block_root, %index, @@ -1023,7 +1311,6 @@ impl NetworkBeaconProcessor { %index, "Could not verify column sidecar for gossip. Ignoring the partial column sidecar" ); - // Prevent recurring behaviour by penalizing the peer slightly. self.gossip_penalize_peer( peer_id, PeerAction::HighToleranceError, @@ -1108,382 +1395,31 @@ impl NetworkBeaconProcessor { } } - #[allow(clippy::too_many_arguments)] - #[instrument( - name = "lh_process_gossip_blob", - parent = None, - level = "debug", - skip_all, - fields( - slot = ?blob_sidecar.slot(), - block_root = ?blob_sidecar.block_root(), - index = blob_sidecar.index), - )] - pub async fn process_gossip_blob( + /// Process a gossip-verified partial data column by merging it in the assembler + async fn process_gossip_verified_partial_data_column( self: &Arc, - message_id: MessageId, - peer_id: PeerId, - _peer_client: Client, - blob_index: u64, - blob_sidecar: Arc>, - seen_duration: Duration, + _peer_id: PeerId, + verified_partial: KzgVerifiedPartialDataColumn, + verified_header: GossipVerifiedPartialDataColumnHeader, + slot: Slot, ) { - let slot = blob_sidecar.slot(); - let root = blob_sidecar.block_root(); - let index = blob_sidecar.index; - let commitment = blob_sidecar.kzg_commitment; - let delay = get_slot_delay_ms(seen_duration, slot, &self.chain.slot_clock); - // Log metrics to track delay from other nodes on the network. - metrics::set_gauge(&metrics::BEACON_BLOB_DELAY_GOSSIP, delay.as_millis() as i64); - match self + let processing_start_time = Instant::now(); + let block_root = verified_partial.block_root(); + let data_column_index = verified_partial.index(); + + let result = self .chain - .verify_blob_sidecar_for_gossip(blob_sidecar.clone(), blob_index) - { - Ok(gossip_verified_blob) => { - metrics::inc_counter(&metrics::BEACON_PROCESSOR_GOSSIP_BLOB_VERIFIED_TOTAL); + .process_gossip_partial_data_column(verified_partial, verified_header.clone(), slot) + .await; - if delay >= self.chain.spec.get_unaggregated_attestation_due() { - metrics::inc_counter(&metrics::BEACON_BLOB_GOSSIP_ARRIVED_LATE_TOTAL); + // First, handle merge results (if any) + let result = match result { + Ok(Some((avail, merge_result))) => { + if !merge_result.full_columns.is_empty() { debug!( - block_root = ?gossip_verified_blob.block_root(), - proposer_index = gossip_verified_blob.block_proposer_index(), - slot = %gossip_verified_blob.slot(), - delay = ?delay, - commitment = %gossip_verified_blob.kzg_commitment(), - "Gossip blob arrived late" - ); - } - - debug!( - %slot, - %root, - %index, - commitment = %gossip_verified_blob.kzg_commitment(), - "Successfully verified gossip blob" - ); - - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); - - // Log metrics to keep track of propagation delay times. - if let Some(duration) = SystemTime::now() - .duration_since(UNIX_EPOCH) - .ok() - .and_then(|now| now.checked_sub(seen_duration)) - { - metrics::set_gauge( - &metrics::BEACON_BLOB_DELAY_GOSSIP_VERIFICATION, - duration.as_millis() as i64, - ); - } - self.process_gossip_verified_blob(peer_id, gossip_verified_blob, seen_duration) - .await - } - Err(err) => { - match err { - GossipBlobError::ParentUnknown { parent_root } => { - debug!( - action = "requesting parent", - block_root = %root, - parent_root = %parent_root, - %commitment, - "Unknown parent hash for blob" - ); - self.send_sync_message(SyncMessage::UnknownParentBlob( - peer_id, - blob_sidecar, - )); - } - GossipBlobError::PubkeyCacheTimeout | GossipBlobError::BeaconChainError(_) => { - crit!( - error = ?err, - "Internal error when verifying blob sidecar" - ) - } - GossipBlobError::ProposalSignatureInvalid - | GossipBlobError::UnknownValidator(_) - | GossipBlobError::ProposerIndexMismatch { .. } - | GossipBlobError::BlobIsNotLaterThanParent { .. } - | GossipBlobError::InvalidSubnet { .. } - | GossipBlobError::InvalidInclusionProof - | GossipBlobError::KzgError(_) - | GossipBlobError::NotFinalizedDescendant { .. } => { - warn!( - error = ?err, - %slot, - %root, - %index, - %commitment, - "Could not verify blob sidecar for gossip. Rejecting the blob sidecar" - ); - // Prevent recurring behaviour by penalizing the peer. - self.gossip_penalize_peer( - peer_id, - PeerAction::LowToleranceError, - "gossip_blob_low", - ); - self.propagate_validation_result( - message_id, - peer_id, - MessageAcceptance::Reject, - ); - } - GossipBlobError::RepeatBlob { .. } => { - // We may have received the blob from the EL. Do not penalise the peer. - // Gossip filter should filter any duplicates received after this. - debug!( - %slot, - %root, - %index, - "Received already available blob sidecar. Ignoring the blob sidecar" - ) - } - GossipBlobError::FutureSlot { .. } => { - debug!( - error = ?err, - %slot, - %root, - %index, - %commitment, - "Could not verify blob sidecar for gossip. Ignoring the blob sidecar" - ); - // Prevent recurring behaviour by penalizing the peer slightly. - self.gossip_penalize_peer( - peer_id, - PeerAction::HighToleranceError, - "gossip_blob_high", - ); - self.propagate_validation_result( - message_id, - peer_id, - MessageAcceptance::Ignore, - ); - } - GossipBlobError::PastFinalizedSlot { .. } => { - debug!( - error = ?err, - %slot, - %root, - %index, - %commitment, - "Could not verify blob sidecar for gossip. Ignoring the blob sidecar" - ); - // Prevent recurring behaviour by penalizing the peer. A low-tolerance - // error is fine because there's no reason for peers to be propagating old - // blobs on gossip, even if their view of finality is lagging. - self.gossip_penalize_peer( - peer_id, - PeerAction::LowToleranceError, - "gossip_blob_low", - ); - self.propagate_validation_result( - message_id, - peer_id, - MessageAcceptance::Ignore, - ); - } - } - } - } - } - - async fn process_gossip_verified_blob( - self: &Arc, - peer_id: PeerId, - verified_blob: GossipVerifiedBlob, - _seen_duration: Duration, - ) { - let processing_start_time = Instant::now(); - let block_root = verified_blob.block_root(); - let blob_slot = verified_blob.slot(); - let blob_index = verified_blob.id().index; - - let result = self.chain.process_gossip_blob(verified_blob).await; - register_process_result_metrics(&result, metrics::BlockSource::Gossip, "blob"); - - match &result { - Ok(AvailabilityProcessingStatus::Imported(block_root)) => { - debug!( - %block_root, - "Gossipsub blob processed - imported fully available block" - ); - self.chain.recompute_head_at_current_slot().await; - - metrics::set_gauge( - &metrics::BEACON_BLOB_DELAY_FULL_VERIFICATION, - processing_start_time.elapsed().as_millis() as i64, - ); - } - Ok(AvailabilityProcessingStatus::MissingComponents(slot, block_root)) => { - debug!( - %slot, - %blob_index, - %block_root, - "Processed gossip blob - waiting for other components" - ); - } - Err(BlockError::DuplicateFullyImported(_)) => { - debug!( - ?block_root, - blob_index, "Ignoring gossip blob already imported" - ); - } - Err(err) => { - debug!( - outcome = ?err, - ?block_root, - %blob_slot, - blob_index, - "Invalid gossip blob" - ); - self.gossip_penalize_peer( - peer_id, - PeerAction::MidToleranceError, - "bad_gossip_blob_ssz", - ); - } - } - - // If a block is in the da_checker, sync maybe awaiting for an event when block is finally - // imported. A block can become imported both after processing a block or blob. If a - // importing a block results in `Imported`, notify. Do not notify of blob errors. - if matches!(result, Ok(AvailabilityProcessingStatus::Imported(_))) { - self.send_sync_message(SyncMessage::GossipBlockProcessResult { - block_root, - imported: true, - }); - } - } - - /// Process a gossip-verified full data column (not partial). - /// Partials are handled by process_gossip_verified_partial_data_column. - async fn process_gossip_verified_data_column( - self: &Arc, - peer_id: PeerId, - verified_data_column: GossipVerifiedDataColumn, - // This value is not used presently, but it might come in handy for debugging. - _seen_duration: Duration, - ) { - let processing_start_time = Instant::now(); - let block_root = verified_data_column.block_root(); - let data_column_slot = verified_data_column.slot(); - let data_column_index = verified_data_column.index(); - - // TODO(gloas): implement partial messages - if let DataColumnSidecar::Fulu(col) = verified_data_column.as_data_column() - && self - .chain - .data_availability_checker - .partial_assembler() - .is_some_and(|a| !a.is_complete(block_root, verified_data_column.index())) - { - metrics::inc_counter_vec( - &metrics::BEACON_USEFUL_FULL_COLUMNS_RECEIVED_TOTAL, - &[&data_column_index.to_string()], - ); - - match col.to_partial() { - Ok(mut column) => { - let header = column.sidecar.header.take(); - if let Some(header) = header { - self.send_network_message(NetworkMessage::PublishPartialColumns { - columns: vec![Arc::new(column)], - header: Arc::new(header), - }); - } else { - crit!("Converting from full to partial yielded headerless partial") - }; - } - Err(err) => crit!(?err, "Could not convert from full to partial"), - } - } - - let result = self - .chain - .process_gossip_data_columns(vec![verified_data_column], || Ok(())) - .await; - register_process_result_metrics(&result, metrics::BlockSource::Gossip, "data_column"); - - match result { - Ok(availability) => match availability { - AvailabilityProcessingStatus::Imported(block_root) => { - debug!( - %block_root, - "Gossipsub data column processed, imported fully available block" - ); - self.chain.recompute_head_at_current_slot().await; - - metrics::set_gauge( - &metrics::BEACON_BLOB_DELAY_FULL_VERIFICATION, - processing_start_time.elapsed().as_millis() as i64, - ); - - // If a block is in the da_checker, sync maybe awaiting for an event when block is finally - // imported. A block can become imported both after processing a block or data column. If - // importing a block results in `Imported`, notify. Do not notify of data column errors. - self.send_sync_message(SyncMessage::GossipBlockProcessResult { - block_root, - imported: true, - }); - } - AvailabilityProcessingStatus::MissingComponents(slot, block_root) => { - trace!( - %slot, - %data_column_index, - %block_root, - "Processed data column, waiting for other components" - ); - - self.check_reconstruction_trigger(slot, &block_root).await; - } - }, - Err(BlockError::DuplicateFullyImported(_)) => { - debug!( - ?block_root, - data_column_index, "Ignoring gossip column already imported" - ); - } - Err(err) => { - debug!( - outcome = ?err, - ?block_root, - block_slot = %data_column_slot, - data_column_index, - "Invalid gossip data column" - ); - self.gossip_penalize_peer( - peer_id, - PeerAction::MidToleranceError, - "bad_gossip_data_column_ssz", - ); - } - } - } - - /// Process a gossip-verified partial data column by merging it in the assembler - async fn process_gossip_verified_partial_data_column( - self: &Arc, - _peer_id: PeerId, - verified_partial: KzgVerifiedPartialDataColumn, - verified_header: GossipVerifiedPartialDataColumnHeader, - slot: Slot, - ) { - let processing_start_time = Instant::now(); - let block_root = verified_partial.block_root(); - let data_column_index = verified_partial.index(); - - let result = self - .chain - .process_gossip_partial_data_column(verified_partial, verified_header.clone(), slot) - .await; - - // First, handle merge results (if any) - let result = match result { - Ok(Some((avail, merge_result))) => { - if !merge_result.full_columns.is_empty() { - debug!( - %block_root, - index = data_column_index, - "Partial data column completed to full column" + %block_root, + index = data_column_index, + "Partial data column completed to full column" ); self.send_network_message(NetworkMessage::Publish { @@ -1874,9 +1810,7 @@ impl NetworkBeaconProcessor { crit!(error = %e, "Internal block gossip validation error. Availability check during gossip validation"); return None; } - // BlobNotRequired is unreachable. Only constructed in `process_gossip_blob` Err(e @ BlockError::InternalError(_)) - | Err(e @ BlockError::BlobNotRequired(_)) | Err(e @ BlockError::EnvelopeBlockRootUnknown(_)) | Err(e @ BlockError::OptimisticSyncNotSupported { .. }) => { error!(error = %e, "Internal block gossip validation error"); @@ -2194,14 +2128,14 @@ impl NetworkBeaconProcessor { message_id: MessageId, peer_id: PeerId, proposer_slashing: ProposerSlashing, - ) { + ) -> MessageAcceptance { let validator_index = proposer_slashing.signed_header_1.message.proposer_index; - let slashing = match self + let (validation_result, verified_slashing_opt) = match self .chain .verify_proposer_slashing_for_gossip(proposer_slashing) { - Ok(ObservationOutcome::New(slashing)) => slashing, + Ok(ObservationOutcome::New(slashing)) => (MessageAcceptance::Accept, Some(slashing)), Ok(ObservationOutcome::AlreadyKnown) => { debug!( reason = "Already seen a proposer slashing for that validator", @@ -2209,44 +2143,54 @@ impl NetworkBeaconProcessor { peer = %peer_id, "Dropping proposer slashing" ); - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); - return; + (MessageAcceptance::Ignore, None) } Err(e) => { - // This is likely a fault with the beacon chain and not necessarily a - // malicious message from the peer. debug!( validator_index, %peer_id, error = ?e, - "Dropping invalid proposer slashing" + "Dropping proposer slashing due to an error" ); - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); - // Penalize peer slightly for invalids. - self.gossip_penalize_peer( - peer_id, - PeerAction::HighToleranceError, - "invalid_gossip_proposer_slashing", - ); - return; + if matches!(e, BeaconChainError::ProposerSlashingValidationError(_)) { + // Penalize peer slightly for invalids. + self.gossip_penalize_peer( + peer_id, + PeerAction::HighToleranceError, + "invalid_gossip_proposer_slashing", + ); + (MessageAcceptance::Reject, None) + } else { + // This is likely a fault with the beacon chain and not necessarily a + // malicious message from the peer. + (MessageAcceptance::Ignore, None) + } } }; - metrics::inc_counter(&metrics::BEACON_PROCESSOR_PROPOSER_SLASHING_VERIFIED_TOTAL); + self.propagate_validation_result( + message_id, + peer_id, + clone_message_acceptance(&validation_result), + ); - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); + if let Some(slashing) = verified_slashing_opt { + metrics::inc_counter(&metrics::BEACON_PROCESSOR_PROPOSER_SLASHING_VERIFIED_TOTAL); - // Register the slashing with any monitored validators. - self.chain - .validator_monitor - .read() - .register_gossip_proposer_slashing(slashing.as_inner()); + // Register the slashing with any monitored validators. + self.chain + .validator_monitor + .read() + .register_gossip_proposer_slashing(slashing.as_inner()); - self.chain.import_proposer_slashing(slashing); - debug!("Successfully imported proposer slashing"); + self.chain.import_proposer_slashing(slashing); + debug!("Successfully imported proposer slashing"); - metrics::inc_counter(&metrics::BEACON_PROCESSOR_PROPOSER_SLASHING_IMPORTED_TOTAL); + metrics::inc_counter(&metrics::BEACON_PROCESSOR_PROPOSER_SLASHING_IMPORTED_TOTAL); + } + + validation_result } pub fn process_gossip_attester_slashing( @@ -2254,51 +2198,64 @@ impl NetworkBeaconProcessor { message_id: MessageId, peer_id: PeerId, attester_slashing: AttesterSlashing, - ) { - let slashing = match self + ) -> MessageAcceptance { + let (validation_result, verified_slashing_opt) = match self .chain .verify_attester_slashing_for_gossip(attester_slashing) { - Ok(ObservationOutcome::New(slashing)) => slashing, + Ok(ObservationOutcome::New(slashing)) => (MessageAcceptance::Accept, Some(slashing)), Ok(ObservationOutcome::AlreadyKnown) => { debug!( reason = "Slashings already known for all slashed validators", peer = %peer_id, "Dropping attester slashing" ); - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); - return; + (MessageAcceptance::Ignore, None) } Err(e) => { debug!( %peer_id, error = ?e, - "Dropping invalid attester slashing" + "Dropping attester slashing due to an error" ); - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); - // Penalize peer slightly for invalids. - self.gossip_penalize_peer( - peer_id, - PeerAction::HighToleranceError, - "invalid_gossip_attester_slashing", - ); - return; + + if matches!(e, BeaconChainError::AttesterSlashingValidationError(_)) { + // Penalize peer slightly for invalids. + self.gossip_penalize_peer( + peer_id, + PeerAction::HighToleranceError, + "invalid_gossip_attester_slashing", + ); + (MessageAcceptance::Reject, None) + } else { + // This is likely a fault with the beacon chain and not necessarily a + // malicious message from the peer. + (MessageAcceptance::Ignore, None) + } } }; - metrics::inc_counter(&metrics::BEACON_PROCESSOR_ATTESTER_SLASHING_VERIFIED_TOTAL); + self.propagate_validation_result( + message_id, + peer_id, + clone_message_acceptance(&validation_result), + ); - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); + if let Some(slashing) = verified_slashing_opt { + metrics::inc_counter(&metrics::BEACON_PROCESSOR_ATTESTER_SLASHING_VERIFIED_TOTAL); - // Register the slashing with any monitored validators. - self.chain - .validator_monitor - .read() - .register_gossip_attester_slashing(slashing.as_inner().to_ref()); + // Register the slashing with any monitored validators. + self.chain + .validator_monitor + .read() + .register_gossip_attester_slashing(slashing.as_inner().to_ref()); + + self.chain.import_attester_slashing(slashing); + debug!("Successfully imported attester slashing"); + metrics::inc_counter(&metrics::BEACON_PROCESSOR_ATTESTER_SLASHING_IMPORTED_TOTAL); + } - self.chain.import_attester_slashing(slashing); - debug!("Successfully imported attester slashing"); - metrics::inc_counter(&metrics::BEACON_PROCESSOR_ATTESTER_SLASHING_IMPORTED_TOTAL); + validation_result } pub fn process_gossip_bls_to_execution_change( diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 434f7ecc8b3..cee3094e791 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -1,12 +1,12 @@ use crate::sync::manager::BlockProcessType; use crate::{service::NetworkMessage, sync::manager::SyncMessage}; -use beacon_chain::blob_verification::{GossipBlobError, observe_gossip_blob}; use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::block_verification_types::RangeSyncBlock; -use beacon_chain::data_column_verification::{GossipDataColumnError, observe_gossip_data_column}; -use beacon_chain::fetch_blobs::{ - EngineGetBlobsOutput, FetchEngineBlobError, fetch_and_process_engine_blobs, +use beacon_chain::data_column_verification::{ + GossipDataColumnError, KzgVerifiedCustodyDataColumn, observe_gossip_data_column, }; +use beacon_chain::fetch_blobs::{FetchEngineBlobError, fetch_and_process_engine_blobs}; +use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; use beacon_chain::{AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError}; use beacon_processor::{ BeaconProcessorSend, DuplicateCache, GossipAggregatePackage, GossipAttestationPackage, Work, @@ -15,12 +15,13 @@ use beacon_processor::{ use lighthouse_network::rpc::InboundRequestId; use lighthouse_network::rpc::methods::{ BlobsByRangeRequest, BlobsByRootRequest, BlocksByHeadRequest, DataColumnsByRangeRequest, - DataColumnsByRootRequest, LightClientUpdatesByRangeRequest, PayloadEnvelopesByRangeRequest, + DataColumnsByRootRequest, ExecutionProofsByRangeRequest, ExecutionProofsByRootRequest, + LightClientUpdatesByRangeRequest, PayloadEnvelopesByRangeRequest, PayloadEnvelopesByRootRequest, }; use lighthouse_network::service::api_types::CustodyBackfillBatchId; use lighthouse_network::{ - Client, GossipTopic, MessageId, NetworkGlobals, PeerId, PubsubMessage, + Client, GossipTopic, MessageId, NetworkConfig, NetworkGlobals, PeerId, PubsubMessage, rpc::{BlocksByRangeRequest, BlocksByRootRequest, LightClientBootstrapRequest, StatusMessage}, }; use rand::prelude::SliceRandom; @@ -31,15 +32,18 @@ use task_executor::TaskExecutor; use tokio::sync::mpsc::{self, error::TrySendError}; use tracing::{debug, error, instrument, trace, warn}; use types::*; +use { + beacon_chain::builder::Witness, beacon_processor::BeaconProcessorChannels, + slot_clock::ManualSlotClock, store::MemoryStore, tokio::sync::mpsc::UnboundedSender, +}; -pub use sync_methods::ChainSegmentProcessId; -use types::data::FixedBlobSidecarList; +pub use sync_methods::{BlockProcessingResult, ChainSegmentProcessId}; pub type Error = TrySendError>; mod gossip_methods; mod rpc_methods; -mod sync_methods; +pub(crate) mod sync_methods; mod tests; pub(crate) const FUTURE_SLOT_TOLERANCE: u64 = 1; @@ -65,9 +69,6 @@ pub struct NetworkBeaconProcessor { pub executor: TaskExecutor, } -// Publish blobs in batches of exponentially increasing size. -const BLOB_PUBLICATION_EXP_FACTOR: usize = 2; - impl NetworkBeaconProcessor { fn try_send(&self, event: BeaconWorkEvent) -> Result<(), Error> { self.beacon_processor_send.try_send(event) @@ -193,36 +194,6 @@ impl NetworkBeaconProcessor { }) } - /// Create a new `Work` event for some blob sidecar. - pub fn send_gossip_blob_sidecar( - self: &Arc, - message_id: MessageId, - peer_id: PeerId, - peer_client: Client, - blob_index: u64, - blob_sidecar: Arc>, - seen_timestamp: Duration, - ) -> Result<(), Error> { - let processor = self.clone(); - let process_fn = async move { - processor - .process_gossip_blob( - message_id, - peer_id, - peer_client, - blob_index, - blob_sidecar, - seen_timestamp, - ) - .await - }; - - self.try_send(BeaconWorkEvent { - drop_during_sync: false, - work: Work::GossipBlobSidecar(Box::pin(process_fn)), - }) - } - /// Create a new `Work` event for some data column sidecar. pub fn send_gossip_data_column_sidecar( self: &Arc, @@ -231,6 +202,7 @@ impl NetworkBeaconProcessor { subnet_id: DataColumnSubnetId, column_sidecar: Arc>, seen_timestamp: Duration, + allow_reprocess: bool, ) -> Result<(), Error> { let processor = self.clone(); let process_fn = async move { @@ -241,6 +213,7 @@ impl NetworkBeaconProcessor { subnet_id, column_sidecar, seen_timestamp, + allow_reprocess, ) .await }; @@ -353,7 +326,7 @@ impl NetworkBeaconProcessor { ) -> Result<(), Error> { let processor = self.clone(); let process_fn = move || { - processor.process_gossip_proposer_slashing(message_id, peer_id, *proposer_slashing) + processor.process_gossip_proposer_slashing(message_id, peer_id, *proposer_slashing); }; self.try_send(BeaconWorkEvent { @@ -420,7 +393,7 @@ impl NetworkBeaconProcessor { ) -> Result<(), Error> { let processor = self.clone(); let process_fn = move || { - processor.process_gossip_attester_slashing(message_id, peer_id, *attester_slashing) + processor.process_gossip_attester_slashing(message_id, peer_id, *attester_slashing); }; self.try_send(BeaconWorkEvent { @@ -451,6 +424,43 @@ impl NetworkBeaconProcessor { }) } + /// Process an execution proof received over gossip. + pub fn send_gossip_execution_proof( + self: &Arc, + message_id: MessageId, + peer_id: PeerId, + execution_proof: Arc, + ) -> Result<(), Error> { + let processor = self.clone(); + self.executor.spawn( + async move { + processor + .process_gossip_execution_proof(message_id, peer_id, execution_proof) + .await; + }, + "gossip_execution_proof", + ); + Ok(()) + } + + /// Verify an execution proof received over RPC. + pub fn send_rpc_execution_proof( + self: &Arc, + peer_id: PeerId, + execution_proof: Arc, + ) -> Result<(), Error> { + let processor = self.clone(); + self.executor.spawn( + async move { + processor + .process_rpc_execution_proof(peer_id, execution_proof) + .await; + }, + "rpc_execution_proof", + ); + Ok(()) + } + /// Create a new `Work` event for some execution payload envelope. pub fn send_gossip_execution_payload( self: &Arc, @@ -563,31 +573,6 @@ impl NetworkBeaconProcessor { }) } - /// Create a new `Work` event for some blobs, where the result from computation (if any) is - /// sent to the other side of `result_tx`. - pub fn send_rpc_blobs( - self: &Arc, - block_root: Hash256, - blobs: FixedBlobSidecarList, - seen_timestamp: Duration, - process_type: BlockProcessType, - ) -> Result<(), Error> { - let blob_count = blobs.iter().filter(|b| b.is_some()).count(); - if blob_count == 0 { - return Ok(()); - } - let process_fn = self.clone().generate_rpc_blobs_process_fn( - block_root, - blobs, - seen_timestamp, - process_type, - ); - self.try_send(BeaconWorkEvent { - drop_during_sync: false, - work: Work::RpcBlobs { process_fn }, - }) - } - /// Create a new `Work` event for an RPC-fetched payload envelope. `process_lookup_envelope` /// reports the result back to sync. pub fn send_lookup_envelope( @@ -868,6 +853,48 @@ impl NetworkBeaconProcessor { }) } + /// Serve an `ExecutionProofsByRange` RPC request. + pub fn send_execution_proofs_by_range_request( + self: &Arc, + peer_id: PeerId, + inbound_request_id: InboundRequestId, + request: ExecutionProofsByRangeRequest, + ) -> Result<(), Error> { + let processor = self.clone(); + self.executor.spawn_blocking( + move || { + processor.handle_execution_proofs_by_range_request( + peer_id, + inbound_request_id, + request, + ); + }, + "execution_proofs_by_range_request", + ); + Ok(()) + } + + /// Serve an `ExecutionProofsByRoot` RPC request. + pub fn send_execution_proofs_by_root_request( + self: &Arc, + peer_id: PeerId, + inbound_request_id: InboundRequestId, + request: ExecutionProofsByRootRequest, + ) -> Result<(), Error> { + let processor = self.clone(); + self.executor.spawn_blocking( + move || { + processor.handle_execution_proofs_by_root_request( + peer_id, + inbound_request_id, + request, + ); + }, + "execution_proofs_by_root_request", + ); + Ok(()) + } + /// Create a new work event to process `LightClientBootstrap`s from the RPC network. pub fn send_light_client_bootstrap_request( self: &Arc, @@ -953,6 +980,50 @@ impl NetworkBeaconProcessor { }); } + pub fn local_execution_proof_status( + &self, + ) -> lighthouse_network::rpc::methods::ExecutionProofStatus { + let head = self.chain.canonical_head.cached_head(); + let configured_proof_types = self + .chain + .execution_layer + .as_ref() + .map(|execution_layer| { + execution_layer + .proof_types() + .iter() + .map(|proof_type| proof_type.to_u8()) + .collect::>() + }) + .unwrap_or_default(); + let (block_root, slot, mut available_proof_types) = self + .chain + .latest_execution_proof_status(&configured_proof_types) + .map(|status| { + let proof_types = status + .valid_proof_types() + .filter(|proof_type| configured_proof_types.contains(proof_type)) + .collect::>(); + (status.block_root, status.slot.as_u64(), proof_types) + }) + .unwrap_or_else(|| (head.head_block_root(), head.head_slot().as_u64(), vec![])); + available_proof_types.sort_unstable(); + + let proof_types = ssz_types::VariableList::::new( + available_proof_types, + ) + .unwrap_or_else(|error| { + debug!(?error, "Local execution proof types exceed status limit"); + ssz_types::VariableList::default() + }); + + lighthouse_network::rpc::methods::ExecutionProofStatus { + block_root, + slot, + proof_types, + } + } + pub async fn fetch_engine_blobs_and_publish( self: &Arc, header: Arc>, @@ -965,22 +1036,12 @@ impl NetworkBeaconProcessor { let epoch = header.slot().epoch(T::EthSpec::slots_per_epoch()); let custody_columns = self.chain.sampling_columns_for_epoch(epoch); let self_cloned = self.clone(); - let publish_fn = move |blobs_or_data_column| { + let publish_fn = move |columns: Vec>| { if publish_blobs { - match blobs_or_data_column { - EngineGetBlobsOutput::Blobs(blobs) => { - self_cloned.publish_blobs_gradually( - blobs.into_iter().map(|b| b.to_blob()).collect(), - block_root, - ); - } - EngineGetBlobsOutput::CustodyColumns(columns) => { - self_cloned.publish_data_columns_gradually( - columns.into_iter().map(|c| c.clone_arc()).collect(), - block_root, - ); - } - }; + self_cloned.publish_data_columns_gradually( + columns.into_iter().map(|c| c.clone_arc()).collect(), + block_root, + ); } }; @@ -1098,84 +1159,6 @@ impl NetworkBeaconProcessor { } } - /// This function gradually publishes blobs to the network in randomised batches. - /// - /// This is an optimisation to reduce outbound bandwidth and ensures each blob is published - /// by some nodes on the network as soon as possible. Our hope is that some blobs arrive from - /// other nodes in the meantime, obviating the need for us to publish them. If no other - /// publisher exists for a blob, it will eventually get published here. - fn publish_blobs_gradually( - self: &Arc, - mut blobs: Vec>>, - block_root: Hash256, - ) { - let self_clone = self.clone(); - - self.executor.spawn( - async move { - let chain = self_clone.chain.clone(); - let publish_fn = |blobs: Vec>>| { - self_clone.send_network_message(NetworkMessage::Publish { - messages: blobs - .into_iter() - .map(|blob| PubsubMessage::BlobSidecar(Box::new((blob.index, blob)))) - .collect(), - }); - }; - - // Permute the blobs and split them into batches. - // The hope is that we won't need to publish some blobs because we will receive them - // on gossip from other nodes. - blobs.shuffle(&mut rand::rng()); - - let blob_publication_batch_interval = chain.config.blob_publication_batch_interval; - let mut publish_count = 0usize; - let blob_count = blobs.len(); - let mut blobs_iter = blobs.into_iter().peekable(); - let mut batch_size = 1usize; - - while blobs_iter.peek().is_some() { - let batch = blobs_iter.by_ref().take(batch_size); - let publishable = batch - .filter_map(|blob| match observe_gossip_blob(&blob, &chain) { - Ok(()) => Some(blob), - Err(GossipBlobError::RepeatBlob { .. }) => None, - Err(e) => { - warn!( - error = ?e, - "Previously verified blob is invalid" - ); - None - } - }) - .collect::>(); - - if !publishable.is_empty() { - debug!( - publish_count = publishable.len(), - ?block_root, - "Publishing blob batch" - ); - publish_count += publishable.len(); - publish_fn(publishable); - } - - tokio::time::sleep(blob_publication_batch_interval).await; - batch_size *= BLOB_PUBLICATION_EXP_FACTOR; - } - - debug!( - batch_interval = blob_publication_batch_interval.as_millis(), - blob_count, - publish_count, - ?block_root, - "Batch blob publication complete" - ) - }, - "gradual_blob_publication", - ); - } - /// This function gradually publishes data columns to the network in randomised batches. /// /// This is an optimisation to reduce outbound bandwidth and ensures each column is published @@ -1260,16 +1243,8 @@ impl NetworkBeaconProcessor { } } -#[cfg(test)] -use { - beacon_chain::builder::Witness, beacon_processor::BeaconProcessorChannels, - slot_clock::ManualSlotClock, store::MemoryStore, tokio::sync::mpsc::UnboundedSender, -}; - -#[cfg(test)] pub(crate) type TestBeaconChainType = Witness; -#[cfg(test)] impl NetworkBeaconProcessor> { // Instantiates a mostly non-functional version of `Self` and returns the // event receiver that would normally go to the beacon processor. This is @@ -1301,4 +1276,22 @@ impl NetworkBeaconProcessor> { (network_beacon_processor, beacon_processor_rx) } + + /// Constructs a mostly non-functional `NetworkBeaconProcessor` from a test harness, + /// suitable for directly calling gossip processing methods in tests. + pub fn null_from_harness(harness: &BeaconChainHarness>) -> Self { + let network_globals = NetworkGlobals::new_test_globals( + vec![], + Arc::new(NetworkConfig::default()), + harness.spec.clone(), + ); + + Self::null_for_testing( + Arc::new(network_globals), + mpsc::unbounded_channel().0, + harness.chain.clone(), + harness.runtime.task_executor.clone(), + ) + .0 + } } diff --git a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs index 37a6f3779ae..f48de45b12a 100644 --- a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs @@ -8,7 +8,8 @@ use beacon_chain::{BeaconChainError, BeaconChainTypes, BlockProcessStatus, WhenS use itertools::{Itertools, process_results}; use lighthouse_network::rpc::methods::{ BlobsByRangeRequest, BlobsByRootRequest, BlocksByHeadRequest, DataColumnsByRangeRequest, - DataColumnsByRootRequest, PayloadEnvelopesByRangeRequest, PayloadEnvelopesByRootRequest, + DataColumnsByRootRequest, ExecutionProofsByRangeRequest, ExecutionProofsByRootRequest, + PayloadEnvelopesByRangeRequest, PayloadEnvelopesByRootRequest, }; use lighthouse_network::rpc::*; use lighthouse_network::{PeerId, ReportSource, Response, SyncInfo}; @@ -19,7 +20,7 @@ use std::sync::Arc; use tokio_stream::StreamExt; use tracing::{Span, debug, error, field, instrument, trace, warn}; use types::data::BlobIdentifier; -use types::{ColumnIndex, Epoch, EthSpec, Hash256, Slot}; +use types::{ColumnIndex, Epoch, EthSpec, Hash256, ProofType, Slot}; impl NetworkBeaconProcessor { /* Auxiliary functions */ @@ -1862,6 +1863,261 @@ impl NetworkBeaconProcessor { Ok(()) } + /// Handle an `ExecutionProofsByRange` request from the peer. + pub fn handle_execution_proofs_by_range_request( + &self, + peer_id: PeerId, + inbound_request_id: InboundRequestId, + req: ExecutionProofsByRangeRequest, + ) { + self.terminate_response_stream( + peer_id, + inbound_request_id, + self.handle_execution_proofs_by_range_request_inner(peer_id, inbound_request_id, req), + Response::ExecutionProofsByRange, + ); + } + + fn handle_execution_proofs_by_range_request_inner( + &self, + peer_id: PeerId, + inbound_request_id: InboundRequestId, + req: ExecutionProofsByRangeRequest, + ) -> Result<(), (RpcErrorResponse, &'static str)> { + debug!( + %peer_id, + start_slot = req.start_slot, + count = req.count, + num_proof_types = req.proof_types.len(), + "Received ExecutionProofsByRange Request" + ); + + self.check_execution_proofs_by_range_window(req.start_slot, req.count)?; + let proof_types = self.requested_or_configured_proof_types(req.proof_types.iter().copied()); + let proofs = self + .chain + .execution_proofs_by_range(Slot::new(req.start_slot), req.count, &proof_types) + .map_err(|error| { + debug!(?error, %peer_id, "Error getting execution proofs by range"); + ( + RpcErrorResponse::ServerError, + "Error getting execution proofs by range", + ) + })?; + + let proofs_sent = proofs.len(); + for proof in proofs { + self.send_response( + peer_id, + inbound_request_id, + Response::ExecutionProofsByRange(Some(proof)), + ); + } + + debug!( + %peer_id, + start_slot = req.start_slot, + count = req.count, + returned = proofs_sent, + "ExecutionProofsByRange Response processed" + ); + + Ok(()) + } + + fn proof_serve_range(&self) -> (Slot, Slot) { + let finalized_slot = self + .chain + .canonical_head + .cached_head() + .finalized_checkpoint() + .epoch + .start_slot(T::EthSpec::slots_per_epoch()); + let current_slot = self + .chain + .slot() + .unwrap_or_else(|_| self.chain.slot_clock.genesis_slot()); + (finalized_slot, current_slot) + } + + fn check_execution_proofs_by_range_window( + &self, + start_slot: u64, + count: u64, + ) -> Result<(), (RpcErrorResponse, &'static str)> { + if count == 0 { + return Ok(()); + } + + let Some(end_slot) = start_slot.checked_add(count.saturating_sub(1)) else { + return Err(( + RpcErrorResponse::InvalidRequest, + "ExecutionProofsByRange range overflows", + )); + }; + + let (serve_start_slot, current_slot) = self.proof_serve_range(); + if Slot::new(start_slot) < serve_start_slot || Slot::new(end_slot) > current_slot { + debug!( + start_slot, + end_slot, + %serve_start_slot, + %current_slot, + "ExecutionProofsByRange outside proof serve range" + ); + return Err(( + RpcErrorResponse::ResourceUnavailable, + "ExecutionProofsByRange outside proof serve range", + )); + } + + Ok(()) + } + + fn canonical_block_slot( + &self, + block_root: Hash256, + ) -> Result, (RpcErrorResponse, &'static str)> { + let Some(block) = self.chain.get_blinded_block(&block_root).map_err(|error| { + error!( + ?block_root, + ?error, + "Error loading block for ExecutionProofsByRoot proof range check" + ); + ( + RpcErrorResponse::ServerError, + "Failed loading block for ExecutionProofsByRoot", + ) + })? + else { + return Ok(None); + }; + + let slot = block.slot(); + let canonical_root = self + .chain + .block_root_at_slot(slot, WhenSlotSkipped::None) + .map_err(|error| { + error!( + ?block_root, + %slot, + ?error, + "Error checking canonical block root for ExecutionProofsByRoot" + ); + ( + RpcErrorResponse::ServerError, + "Failed checking canonical block root for ExecutionProofsByRoot", + ) + })?; + + Ok((canonical_root == Some(block_root)).then_some(slot)) + } + + /// Handle an `ExecutionProofsByRoot` request from the peer. + pub fn handle_execution_proofs_by_root_request( + &self, + peer_id: PeerId, + inbound_request_id: InboundRequestId, + req: ExecutionProofsByRootRequest, + ) { + self.terminate_response_stream( + peer_id, + inbound_request_id, + self.handle_execution_proofs_by_root_request_inner(peer_id, inbound_request_id, req), + Response::ExecutionProofsByRoot, + ); + } + + fn handle_execution_proofs_by_root_request_inner( + &self, + peer_id: PeerId, + inbound_request_id: InboundRequestId, + req: ExecutionProofsByRootRequest, + ) -> Result<(), (RpcErrorResponse, &'static str)> { + debug!( + %peer_id, + num_identifiers = req.identifiers.len(), + "Received ExecutionProofsByRoot Request" + ); + + let (serve_start_slot, current_slot) = self.proof_serve_range(); + let mut has_canonical_requested_root = false; + let mut in_range_roots = HashSet::new(); + for identifier in req.identifiers.iter() { + let Some(slot) = self.canonical_block_slot(identifier.block_root)? else { + continue; + }; + has_canonical_requested_root = true; + if slot >= serve_start_slot && slot <= current_slot { + in_range_roots.insert(identifier.block_root); + } + } + + if has_canonical_requested_root && in_range_roots.is_empty() { + debug!( + %serve_start_slot, + %current_slot, + num_identifiers = req.identifiers.len(), + "ExecutionProofsByRoot outside proof serve range" + ); + return Err(( + RpcErrorResponse::ResourceUnavailable, + "ExecutionProofsByRoot outside proof serve range", + )); + } + + let mut proofs_sent = 0usize; + for identifier in req.identifiers.iter() { + if has_canonical_requested_root && !in_range_roots.contains(&identifier.block_root) { + continue; + } + let proof_types = + self.requested_or_configured_proof_types(identifier.proof_types.iter().copied()); + let proofs = self + .chain + .execution_proofs_by_block_root(identifier.block_root, &proof_types); + proofs_sent += proofs.len(); + for proof in proofs { + self.send_response( + peer_id, + inbound_request_id, + Response::ExecutionProofsByRoot(Some(proof)), + ); + } + } + + debug!( + %peer_id, + num_identifiers = req.identifiers.len(), + returned = proofs_sent, + "ExecutionProofsByRoot Response processed" + ); + + Ok(()) + } + + fn requested_or_configured_proof_types(&self, requested: I) -> Vec + where + I: IntoIterator, + { + let requested = requested.into_iter().collect::>(); + if !requested.is_empty() { + return requested; + } + + self.chain + .execution_layer + .as_ref() + .map(|execution_layer| { + execution_layer + .proof_types() + .iter() + .map(|proof_type| proof_type.to_u8()) + .collect() + }) + .unwrap_or_default() + } + /// Helper function to ensure single item protocol always end with either a single chunk or an /// error fn terminate_response_single_item Response>( diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index e3ba6fb3c40..87d11946bd5 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -3,12 +3,14 @@ use crate::network_beacon_processor::{FUTURE_SLOT_TOLERANCE, NetworkBeaconProces use crate::sync::BatchProcessResult; use crate::sync::manager::CustodyBatchProcessResult; use crate::sync::{ - ChainId, + ChainId, PeerGroup, SyncNetworkContext, manager::{BlockProcessType, SyncMessage}, }; use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::block_verification_types::{AsBlock, RangeSyncBlock}; -use beacon_chain::data_availability_checker::AvailabilityCheckError; +use beacon_chain::data_availability_checker::{ + AvailabilityCheckError, AvailabilityCheckErrorCategory, +}; use beacon_chain::historical_data_columns::HistoricalDataColumnError; use beacon_chain::{ AvailabilityProcessingStatus, BeaconChainTypes, BlockError, ChainSegmentResult, @@ -20,14 +22,12 @@ use beacon_processor::{ }; use beacon_processor::{Work, WorkEvent}; use lighthouse_network::PeerAction; +use lighthouse_network::PeerId; use lighthouse_network::service::api_types::CustodyBackfillBatchId; use logging::crit; use std::sync::Arc; use std::time::Duration; -use store::KzgCommitment; use tracing::{debug, debug_span, error, info, instrument, warn}; -use types::data::FixedBlobSidecarList; -use types::kzg_ext::format_kzg_commitments; use types::{BlockImportSource, DataColumnSidecarList, Epoch, Hash256}; /// Id associated to a batch processing request, either a sync batch or a parent lookup. @@ -90,10 +90,17 @@ impl NetworkBeaconProcessor { ); // A closure which will ignore the block. let ignore_fn = move || { + warn!( + ?process_type, + "Block processing task dropped, cpu might be overloaded" + ); // Sync handles these results self.send_sync_message(SyncMessage::BlockComponentProcessed { process_type, - result: crate::sync::manager::BlockProcessingResult::Ignored, + result: BlockProcessingResult::Error { + penalty: None, + reason: "ignored_processor_overloaded".to_string(), + }, }); }; (process_fn, Box::new(ignore_fn)) @@ -241,114 +248,6 @@ impl NetworkBeaconProcessor { drop(handle); } - /// Returns an async closure which processes a list of blobs received via RPC. - /// - /// This separate function was required to prevent a cycle during compiler - /// type checking. - pub fn generate_rpc_blobs_process_fn( - self: Arc, - block_root: Hash256, - blobs: FixedBlobSidecarList, - seen_timestamp: Duration, - process_type: BlockProcessType, - ) -> AsyncFn { - let process_fn = async move { - self.clone() - .process_rpc_blobs(block_root, blobs, seen_timestamp, process_type) - .await; - }; - Box::pin(process_fn) - } - - /// Attempt to process a list of blobs received from a direct RPC request. - #[instrument( - name = "lh_process_rpc_blobs", - parent = None, - level = "debug", - skip_all, - fields(?block_root), - )] - pub async fn process_rpc_blobs( - self: Arc>, - block_root: Hash256, - blobs: FixedBlobSidecarList, - seen_timestamp: Duration, - process_type: BlockProcessType, - ) { - let Some(slot) = blobs - .iter() - .find_map(|blob| blob.as_ref().map(|blob| blob.slot())) - else { - return; - }; - - let (indices, commitments): (Vec, Vec) = blobs - .iter() - .filter_map(|blob_opt| { - blob_opt - .as_ref() - .map(|blob| (blob.index, blob.kzg_commitment)) - }) - .unzip(); - let commitments = format_kzg_commitments(&commitments); - - debug!( - ?indices, - %block_root, - %slot, - commitments, - "RPC blobs received" - ); - - if let Ok(current_slot) = self.chain.slot() - && current_slot == slot - { - // Note: this metric is useful to gauge how long it takes to receive blobs requested - // over rpc. Since we always send the request for block components at `get_unaggregated_attestation_due() / 2` - // we can use that as a baseline to measure against. - let delay = get_slot_delay_ms(seen_timestamp, slot, &self.chain.slot_clock); - - metrics::observe_duration(&metrics::BEACON_BLOB_RPC_SLOT_START_DELAY_TIME, delay); - } - - let result = self.chain.process_rpc_blobs(slot, block_root, blobs).await; - register_process_result_metrics(&result, metrics::BlockSource::Rpc, "blobs"); - - match &result { - Ok(AvailabilityProcessingStatus::Imported(hash)) => { - debug!( - result = "imported block and blobs", - %slot, - block_hash = %hash, - "Block components retrieved" - ); - self.chain.recompute_head_at_current_slot().await; - } - Ok(AvailabilityProcessingStatus::MissingComponents(_, _)) => { - debug!( - block_hash = %block_root, - %slot, - "Missing components over rpc" - ); - } - Err(BlockError::DuplicateFullyImported(_)) => { - debug!( - block_hash = %block_root, - %slot, - "Blobs have already been imported" - ); - } - // Errors are handled and logged in `block_lookups` - Err(_) => {} - } - - // Sync handles these results - self.send_sync_message(SyncMessage::BlockComponentProcessed { - process_type, - result: result.into(), - }); - } - #[instrument( name = "lh_process_rpc_custody_columns", parent = None, @@ -1060,3 +959,130 @@ impl NetworkBeaconProcessor { } } } + +/// The classified outcome of submitting a block / blob / column for processing, ready for the +/// lookup state machine to act on without re-inspecting `BlockError`. +#[derive(Debug)] +pub enum BlockProcessingResult { + /// `fully_imported` is true if the lookup is complete; false if `MissingComponents` (the + /// lookup must keep fetching). `info` is a stable label for logs / metrics. + Imported(bool, &'static str), + ParentUnknown { + parent_root: Hash256, + }, + /// Processing failed. `penalty` is `Some` when an attributable peer should be downscored; + /// the third tuple element is the `report_peer` telemetry msg. `reason` is for logs only. + Error { + penalty: Option<(PeerAction, WhichPeerToPenalize, &'static str)>, + reason: String, + }, +} + +impl From> for BlockProcessingResult { + fn from(result: Result) -> Self { + fn block_peer_penalty>( + err: E, + ) -> Option<(PeerAction, WhichPeerToPenalize, &'static str)> { + Some(( + PeerAction::MidToleranceError, + WhichPeerToPenalize::BlockPeer, + err.into(), + )) + } + match result { + Ok(AvailabilityProcessingStatus::Imported(_)) => Self::Imported(true, "imported"), + Ok(AvailabilityProcessingStatus::MissingComponents(_, _)) => { + Self::Imported(false, "missing_components") + } + Err(e) => { + let penalty = match &e { + BlockError::DuplicateFullyImported(_) => { + return Self::Imported(true, "duplicate"); + } + BlockError::GenesisBlock => return Self::Imported(true, "genesis"), + BlockError::ParentUnknown { parent_root, .. } => { + return Self::ParentUnknown { + parent_root: *parent_root, + }; + } + BlockError::BeaconChainError(_) | BlockError::InternalError(_) => None, + BlockError::DuplicateImportStatusUnknown(_) => None, + BlockError::AvailabilityCheck(inner) => match inner { + AvailabilityCheckError::InvalidColumn((Some(idx), _)) => Some(( + PeerAction::MidToleranceError, + WhichPeerToPenalize::CustodyPeerForColumn(*idx), + (&e).into(), + )), + inner => match inner.category() { + AvailabilityCheckErrorCategory::Internal => None, + AvailabilityCheckErrorCategory::Malicious => block_peer_penalty(inner), + }, + }, + BlockError::ExecutionPayloadError(epe) => { + if epe.penalize_peer() { + block_peer_penalty(epe) + } else { + None + } + } + // Remaining invalid blocks: penalize the block peer. Listed explicitly so a + // new `BlockError` variant forces a compile error here. + BlockError::FutureSlot { .. } + | BlockError::StateRootMismatch { .. } + | BlockError::WouldRevertFinalizedSlot { .. } + | BlockError::NotFinalizedDescendant { .. } + | BlockError::BlockSlotLimitReached + | BlockError::IncorrectBlockProposer { .. } + | BlockError::UnknownValidator(_) + | BlockError::InvalidSignature(_) + | BlockError::BlockIsNotLaterThanParent { .. } + | BlockError::NonLinearParentRoots + | BlockError::NonLinearSlots + | BlockError::PerBlockProcessingError(_) + | BlockError::WeakSubjectivityConflict + | BlockError::InconsistentFork(_) + | BlockError::ParentExecutionPayloadInvalid { .. } + | BlockError::KnownInvalidExecutionPayload(_) + | BlockError::Slashable + | BlockError::EnvelopeBlockRootUnknown(_) + | BlockError::OptimisticSyncNotSupported { .. } + | BlockError::InvalidBlobCount { .. } + | BlockError::BidParentRootMismatch { .. } => block_peer_penalty(&e), + }; + Self::Error { + penalty, + reason: format!("{e:?}"), + } + } + } + } +} + +/// Selector for which peer(s) in a `PeerGroup` to downscore. +#[derive(Debug, Clone, Copy)] +pub enum WhichPeerToPenalize { + /// All peers in the group (block peer, or all data peers). + BlockPeer, + /// Only the peer(s) that served the given column index. + CustodyPeerForColumn(u64), +} + +impl WhichPeerToPenalize { + pub fn apply( + self, + action: PeerAction, + peer_group: &PeerGroup, + msg: &'static str, + cx: &mut SyncNetworkContext, + ) { + let peers: Vec = match self { + WhichPeerToPenalize::BlockPeer => peer_group.all().copied().collect(), + WhichPeerToPenalize::CustodyPeerForColumn(idx) => { + peer_group.of_index(idx as usize).copied().collect() + } + }; + for peer in peers { + cx.report_peer(peer, action, msg); + } + } +} diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index 18d34b40b3b..ad988515328 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -41,15 +41,12 @@ use std::iter::Iterator; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; +use types::data::BlobIdentifier; use types::{ - AttesterSlashing, BlobSidecar, ChainSpec, DataColumnSidecarList, DataColumnSubnetId, Epoch, - EthSpec, ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, Hash256, - MainnetEthSpec, ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, - SignedExecutionPayloadEnvelope, SignedVoluntaryExit, SingleAttestation, Slot, SubnetId, -}; -use types::{ - BlobSidecarList, - data::{BlobIdentifier, FixedBlobSidecarList}, + AttesterSlashing, ChainSpec, DataColumnSidecarList, DataColumnSubnetId, Epoch, EthSpec, + ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, Hash256, MainnetEthSpec, + ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, SignedExecutionPayloadEnvelope, + SignedVoluntaryExit, SingleAttestation, Slot, SubnetId, }; type E = MainnetEthSpec; @@ -69,7 +66,6 @@ const STANDARD_TIMEOUT: Duration = Duration::from_secs(10); struct TestRig { chain: Arc>, next_block: Arc>, - next_blobs: Option>, next_data_columns: Option>, attestations: Vec<(SingleAttestation, SubnetId)>, next_block_attestations: Vec<(SingleAttestation, SubnetId)>, @@ -341,7 +337,7 @@ impl TestRig { assert!(beacon_processor.is_ok()); let block = next_block_tuple.0; - let (blob_sidecars, data_columns) = if let Some((kzg_proofs, blobs)) = next_block_tuple.1 { + let data_columns = if let Some((kzg_proofs, blobs)) = next_block_tuple.1 { if chain.spec.is_peer_das_enabled_for_epoch(block.epoch()) { let kzg = get_kzg(&chain.spec); let epoch = block.slot().epoch(E::slots_per_epoch()); @@ -358,20 +354,17 @@ impl TestRig { .filter(|c| sampling_indices.contains(c.index())) .collect::>(); - (None, Some(custody_columns)) + Some(custody_columns) } else { - let blob_sidecars = - BlobSidecar::build_sidecars(blobs, &block, kzg_proofs, &chain.spec).unwrap(); - (Some(blob_sidecars), None) + None } } else { - (None, None) + None }; Self { chain, next_block: block, - next_blobs: blob_sidecars, next_data_columns: data_columns, attestations, next_block_attestations, @@ -409,22 +402,6 @@ impl TestRig { .unwrap(); } - pub fn enqueue_gossip_blob(&self, blob_index: usize) { - if let Some(blobs) = self.next_blobs.as_ref() { - let blob = blobs.get(blob_index).unwrap(); - self.network_beacon_processor - .send_gossip_blob_sidecar( - junk_message_id(), - junk_peer_id(), - Client::default(), - blob.index, - blob.clone(), - Duration::from_secs(0), - ) - .unwrap(); - } - } - pub fn enqueue_gossip_data_columns(&self, col_index: usize) { if let Some(data_columns) = self.next_data_columns.as_ref() { let data_column = data_columns.get(col_index).unwrap(); @@ -435,6 +412,7 @@ impl TestRig { DataColumnSubnetId::from_column_index(*data_column.index(), &self.chain.spec), data_column.clone(), Duration::from_secs(0), + true, ) .unwrap(); } @@ -464,20 +442,6 @@ impl TestRig { .unwrap(); } - pub fn enqueue_single_lookup_rpc_blobs(&self) { - if let Some(blobs) = self.next_blobs.clone() { - let blobs = FixedBlobSidecarList::new(blobs.into_iter().map(Some).collect::>()); - self.network_beacon_processor - .send_rpc_blobs( - self.next_block.canonical_root(), - blobs, - std::time::Duration::default(), - BlockProcessType::SingleBlob { id: 1 }, - ) - .unwrap(); - } - } - pub fn enqueue_single_lookup_rpc_data_columns(&self) { if let Some(data_columns) = self.next_data_columns.clone() { self.network_beacon_processor @@ -1101,13 +1065,6 @@ async fn import_gossip_block_acceptably_early() { rig.assert_event_journal_completes(&[WorkType::GossipBlock]) .await; - let num_blobs = rig.next_blobs.as_ref().map(|b| b.len()).unwrap_or(0); - for i in 0..num_blobs { - rig.enqueue_gossip_blob(i); - rig.assert_event_journal_completes(&[WorkType::GossipBlobSidecar]) - .await; - } - let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); for i in 0..num_data_columns { rig.enqueue_gossip_data_columns(i); @@ -1242,13 +1199,6 @@ async fn import_gossip_block_at_current_slot() { rig.assert_event_journal_completes(&[WorkType::GossipBlock]) .await; - let num_blobs = rig.next_blobs.as_ref().map(|b| b.len()).unwrap_or(0); - for i in 0..num_blobs { - rig.enqueue_gossip_blob(i); - rig.assert_event_journal_completes(&[WorkType::GossipBlobSidecar]) - .await; - } - let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); for i in 0..num_data_columns { rig.enqueue_gossip_data_columns(i); @@ -1308,17 +1258,12 @@ async fn attestation_to_unknown_block_processed(import_method: BlockImportMethod ); // Send the block and ensure that the attestation is received back and imported. - let num_blobs = rig.next_blobs.as_ref().map(|b| b.len()).unwrap_or(0); let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); let mut events = vec![]; match import_method { BlockImportMethod::Gossip => { rig.enqueue_gossip_block(); events.push(WorkType::GossipBlock); - for i in 0..num_blobs { - rig.enqueue_gossip_blob(i); - events.push(WorkType::GossipBlobSidecar); - } for i in 0..num_data_columns { rig.enqueue_gossip_data_columns(i); events.push(WorkType::GossipDataColumnSidecar); @@ -1327,10 +1272,6 @@ async fn attestation_to_unknown_block_processed(import_method: BlockImportMethod BlockImportMethod::Rpc => { rig.enqueue_lookup_block(); events.push(WorkType::RpcBlock); - if num_blobs > 0 { - rig.enqueue_single_lookup_rpc_blobs(); - events.push(WorkType::RpcBlobs); - } if num_data_columns > 0 { rig.enqueue_single_lookup_rpc_data_columns(); events.push(WorkType::RpcCustodyColumn); @@ -1394,17 +1335,12 @@ async fn aggregate_attestation_to_unknown_block(import_method: BlockImportMethod ); // Send the block and ensure that the attestation is received back and imported. - let num_blobs = rig.next_blobs.as_ref().map(|b| b.len()).unwrap_or(0); let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); let mut events = vec![]; match import_method { BlockImportMethod::Gossip => { rig.enqueue_gossip_block(); events.push(WorkType::GossipBlock); - for i in 0..num_blobs { - rig.enqueue_gossip_blob(i); - events.push(WorkType::GossipBlobSidecar); - } for i in 0..num_data_columns { rig.enqueue_gossip_data_columns(i); events.push(WorkType::GossipDataColumnSidecar) @@ -1413,10 +1349,6 @@ async fn aggregate_attestation_to_unknown_block(import_method: BlockImportMethod BlockImportMethod::Rpc => { rig.enqueue_lookup_block(); events.push(WorkType::RpcBlock); - if num_blobs > 0 { - rig.enqueue_single_lookup_rpc_blobs(); - events.push(WorkType::RpcBlobs); - } if num_data_columns > 0 { rig.enqueue_single_lookup_rpc_data_columns(); events.push(WorkType::RpcCustodyColumn); @@ -1603,19 +1535,13 @@ async fn import_misc_gossip_ops() { async fn test_rpc_block_reprocessing() { let mut rig = TestRig::new(SMALL_CHAIN).await; let next_block_root = rig.next_block.canonical_root(); + // Insert the next block into the duplicate cache manually let handle = rig.duplicate_cache.check_and_insert(next_block_root); rig.enqueue_single_lookup_block(); rig.assert_event_journal_completes(&[WorkType::RpcBlock]) .await; - let num_blobs = rig.next_blobs.as_ref().map(|b| b.len()).unwrap_or(0); - if num_blobs > 0 { - rig.enqueue_single_lookup_rpc_blobs(); - rig.assert_event_journal_completes(&[WorkType::RpcBlobs]) - .await; - } - let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); if num_data_columns > 0 { rig.enqueue_single_lookup_rpc_data_columns(); diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index 35939c6f396..0688de07aa0 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -12,6 +12,7 @@ use crate::sync::SyncMessage; use beacon_chain::{BeaconChain, BeaconChainTypes}; use beacon_processor::{BeaconProcessorSend, DuplicateCache}; use futures::prelude::*; +use lighthouse_network::rpc::methods::ExecutionProofStatus; use lighthouse_network::rpc::*; use lighthouse_network::{ GossipTopic, MessageId, NetworkGlobals, PeerId, PubsubMessage, Response, @@ -26,7 +27,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream; use tracing::{debug, error, trace, warn}; use types::{ BlobSidecar, DataColumnSidecar, EthSpec, ForkContext, PartialDataColumn, SignedBeaconBlock, - SignedExecutionPayloadEnvelope, + SignedExecutionPayloadEnvelope, SignedExecutionProof, }; /// Handles messages from the network and routes them to the appropriate service to be handled. @@ -312,6 +313,38 @@ impl Router { request, ), ), + RequestType::ExecutionProofsByRange(request) => self + .handle_beacon_processor_send_result( + self.network_beacon_processor + .send_execution_proofs_by_range_request( + peer_id, + inbound_request_id, + request, + ), + ), + RequestType::ExecutionProofsByRoot(request) => self + .handle_beacon_processor_send_result( + self.network_beacon_processor + .send_execution_proofs_by_root_request( + peer_id, + inbound_request_id, + request, + ), + ), + RequestType::ExecutionProofStatus(status) => { + self.network.send_response( + peer_id, + inbound_request_id, + Response::ExecutionProofStatus( + self.network_beacon_processor.local_execution_proof_status(), + ), + ); + self.send_to_sync(SyncMessage::RpcExecutionProofStatus { + peer_id, + request_id: None, + status, + }); + } _ => {} } } @@ -340,8 +373,8 @@ impl Router { Response::BlobsByRange(blob) => { self.on_blobs_by_range_response(peer_id, app_request_id, blob); } - Response::BlobsByRoot(blob) => { - self.on_blobs_by_root_response(peer_id, app_request_id, blob); + Response::BlobsByRoot(_) => { + crit!(%peer_id, "Unexpected BlobsByRoot response; lookup blob requests removed"); } Response::DataColumnsByRoot(data_column) => { self.on_data_columns_by_root_response(peer_id, app_request_id, data_column); @@ -357,6 +390,19 @@ impl Router { Response::PayloadEnvelopesByRange(_) => { debug!("Requesting envelopes by range not supported yet"); } + Response::ExecutionProofsByRange(execution_proof) => { + self.on_execution_proofs_by_range_response( + peer_id, + app_request_id, + execution_proof, + ); + } + Response::ExecutionProofsByRoot(execution_proof) => { + self.on_execution_proofs_by_root_response(peer_id, app_request_id, execution_proof); + } + Response::ExecutionProofStatus(status) => { + self.on_execution_proof_status_response(peer_id, app_request_id, status); + } // Lighthouse currently only serves BlocksByHead and does not issue it as a client, // so receiving a response is unexpected. Drop it without crashing. Response::BlocksByHead(_) => { @@ -412,19 +458,6 @@ impl Router { seen_timestamp, ), ), - PubsubMessage::BlobSidecar(data) => { - let (blob_index, blob_sidecar) = *data; - self.handle_beacon_processor_send_result( - self.network_beacon_processor.send_gossip_blob_sidecar( - message_id, - peer_id, - self.network_globals.client(&peer_id), - blob_index, - blob_sidecar, - seen_timestamp, - ), - ) - } PubsubMessage::DataColumnSidecar(data) => { let (subnet_id, column_sidecar) = *data; self.handle_beacon_processor_send_result( @@ -435,6 +468,7 @@ impl Router { subnet_id, column_sidecar, seen_timestamp, + true, ), ) } @@ -584,6 +618,16 @@ impl Router { ), ) } + PubsubMessage::ExecutionProof(execution_proof) => { + trace!(%peer_id, "Received execution proof"); + self.handle_beacon_processor_send_result( + self.network_beacon_processor.send_gossip_execution_proof( + message_id, + peer_id, + execution_proof, + ), + ) + } } } @@ -734,40 +778,6 @@ impl Router { }); } - /// Handle a `BlobsByRoot` response from the peer. - pub fn on_blobs_by_root_response( - &mut self, - peer_id: PeerId, - app_request_id: AppRequestId, - blob_sidecar: Option>>, - ) { - let sync_request_id = match app_request_id { - AppRequestId::Sync(sync_id) => match sync_id { - id @ SyncRequestId::SingleBlob { .. } => id, - other => { - crit!(request = ?other, "BlobsByRoot response on incorrect request"); - return; - } - }, - AppRequestId::Router => { - crit!(%peer_id, "All BlobsByRoot requests belong to sync"); - return; - } - AppRequestId::Internal => unreachable!("Handled internally"), - }; - - trace!( - %peer_id, - "Received BlobsByRoot Response" - ); - self.send_to_sync(SyncMessage::RpcBlob { - sync_request_id, - peer_id, - blob_sidecar, - seen_timestamp: self.chain.slot_clock.now_duration().unwrap_or_default(), - }); - } - /// Handle a `DataColumnsByRoot` response from the peer. pub fn on_data_columns_by_root_response( &mut self, @@ -848,6 +858,60 @@ impl Router { }); } + pub fn on_execution_proofs_by_range_response( + &mut self, + peer_id: PeerId, + app_request_id: AppRequestId, + execution_proof: Option>, + ) { + trace!(%peer_id, "Received ExecutionProofsByRange Response"); + if let AppRequestId::Sync(sync_request_id) = app_request_id { + self.send_to_sync(SyncMessage::RpcExecutionProof { + peer_id, + sync_request_id, + execution_proof, + }); + } else { + crit!("All execution proofs by range responses should belong to sync"); + } + } + + pub fn on_execution_proofs_by_root_response( + &mut self, + peer_id: PeerId, + app_request_id: AppRequestId, + execution_proof: Option>, + ) { + trace!(%peer_id, "Received ExecutionProofsByRoot Response"); + if let AppRequestId::Sync(sync_request_id) = app_request_id { + self.send_to_sync(SyncMessage::RpcExecutionProof { + peer_id, + sync_request_id, + execution_proof, + }); + } else { + crit!("All execution proofs by root responses should belong to sync"); + } + } + + fn on_execution_proof_status_response( + &mut self, + peer_id: PeerId, + app_request_id: AppRequestId, + status: ExecutionProofStatus, + ) { + if let AppRequestId::Sync(SyncRequestId::ExecutionProofStatus(request_id)) = app_request_id + { + self.send_to_sync(SyncMessage::RpcExecutionProofStatus { + peer_id, + request_id: Some(request_id), + status, + }); + } else { + debug!(%peer_id, "ExecutionProofStatus response with unexpected request id"); + } + } + fn handle_beacon_processor_send_result( &mut self, result: Result<(), crate::network_beacon_processor::Error>, diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index 0f80138d240..16d5011ada8 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -81,6 +81,7 @@ impl BatchConfig for BackFillBatchConfig { } /// Return type when attempting to start the backfill sync process. +#[cfg_attr(feature = "disable-backfill", allow(dead_code))] pub enum SyncStart { /// The chain started syncing or is already syncing. Syncing { @@ -159,6 +160,7 @@ pub struct BackFillSync { network_globals: Arc>, } +#[cfg_attr(feature = "disable-backfill", allow(dead_code))] impl BackFillSync { pub fn new( beacon_chain: Arc>, @@ -1210,6 +1212,7 @@ impl BackFillSync { } /// Error kind for attempting to restart the sync from beacon chain parameters. +#[cfg_attr(feature = "disable-backfill", allow(dead_code))] enum ResetEpochError { /// The chain has already completed. SyncCompleted, @@ -1247,7 +1250,7 @@ mod tests { let peer_id = network_globals .peers .write() - .__add_connected_peer_testing_only( + .__add_connected_peer_with_custody_subnets( true, &beacon_chain.spec, k256::ecdsa::SigningKey::random(&mut rng).into(), diff --git a/beacon_node/network/src/sync/block_lookups/common.rs b/beacon_node/network/src/sync/block_lookups/common.rs deleted file mode 100644 index edd99345b43..00000000000 --- a/beacon_node/network/src/sync/block_lookups/common.rs +++ /dev/null @@ -1,217 +0,0 @@ -use crate::sync::block_lookups::single_block_lookup::{ - LookupRequestError, SingleBlockLookup, SingleLookupRequestState, -}; -use crate::sync::block_lookups::{ - BlobRequestState, BlockRequestState, CustodyRequestState, PeerId, -}; -use crate::sync::manager::BlockProcessType; -use crate::sync::network_context::{LookupRequestResult, SyncNetworkContext}; -use beacon_chain::BeaconChainTypes; -use lighthouse_network::service::api_types::Id; -use parking_lot::RwLock; -use std::collections::HashSet; -use std::sync::Arc; -use types::data::FixedBlobSidecarList; -use types::{DataColumnSidecarList, SignedBeaconBlock}; - -use super::SingleLookupId; -use super::single_block_lookup::{ComponentRequests, DownloadResult}; - -#[derive(Debug, Copy, Clone)] -pub enum ResponseType { - Block, - Blob, - CustodyColumn, -} - -/// This trait unifies common single block lookup functionality across blocks and blobs. This -/// includes making requests, verifying responses, and handling processing results. A -/// `SingleBlockLookup` includes both a `BlockRequestState` and a `BlobRequestState`, this trait is -/// implemented for each. -/// -/// The use of the `ResponseType` associated type gives us a degree of type -/// safety when handling a block/blob response ensuring we only mutate the correct corresponding -/// state. -pub trait RequestState { - /// The type created after validation. - type VerifiedResponseType: Clone; - - /// Request the network context to prepare a request of a component of `block_root`. If the - /// request is not necessary because the component is already known / processed, return false. - /// Return true if it sent a request and we can expect an event back from the network. - fn make_request( - &self, - id: Id, - lookup_peers: Arc>>, - expected_blobs: usize, - cx: &mut SyncNetworkContext, - ) -> Result; - - /* Response handling methods */ - - /// Send the response to the beacon processor. - fn send_for_processing( - id: Id, - result: DownloadResult, - cx: &SyncNetworkContext, - ) -> Result<(), LookupRequestError>; - - /* Utility methods */ - - /// Returns the `ResponseType` associated with this trait implementation. Useful in logging. - fn response_type() -> ResponseType; - - /// A getter for the `BlockRequestState` or `BlobRequestState` associated with this trait. - fn request_state_mut(request: &mut SingleBlockLookup) -> Result<&mut Self, &'static str>; - - /// A getter for a reference to the `SingleLookupRequestState` associated with this trait. - fn get_state(&self) -> &SingleLookupRequestState; - - /// A getter for a mutable reference to the SingleLookupRequestState associated with this trait. - fn get_state_mut(&mut self) -> &mut SingleLookupRequestState; -} - -impl RequestState for BlockRequestState { - type VerifiedResponseType = Arc>; - - fn make_request( - &self, - id: SingleLookupId, - lookup_peers: Arc>>, - _: usize, - cx: &mut SyncNetworkContext, - ) -> Result { - cx.block_lookup_request(id, lookup_peers, self.requested_block_root) - .map_err(LookupRequestError::SendFailedNetwork) - } - - fn send_for_processing( - id: SingleLookupId, - download_result: DownloadResult, - cx: &SyncNetworkContext, - ) -> Result<(), LookupRequestError> { - let DownloadResult { - value, - block_root, - seen_timestamp, - .. - } = download_result; - cx.send_block_for_processing(id, block_root, value, seen_timestamp) - .map_err(LookupRequestError::SendFailedProcessor) - } - - fn response_type() -> ResponseType { - ResponseType::Block - } - fn request_state_mut(request: &mut SingleBlockLookup) -> Result<&mut Self, &'static str> { - Ok(&mut request.block_request_state) - } - fn get_state(&self) -> &SingleLookupRequestState { - &self.state - } - fn get_state_mut(&mut self) -> &mut SingleLookupRequestState { - &mut self.state - } -} - -impl RequestState for BlobRequestState { - type VerifiedResponseType = FixedBlobSidecarList; - - fn make_request( - &self, - id: Id, - lookup_peers: Arc>>, - expected_blobs: usize, - cx: &mut SyncNetworkContext, - ) -> Result { - cx.blob_lookup_request(id, lookup_peers, self.block_root, expected_blobs) - .map_err(LookupRequestError::SendFailedNetwork) - } - - fn send_for_processing( - id: Id, - download_result: DownloadResult, - cx: &SyncNetworkContext, - ) -> Result<(), LookupRequestError> { - let DownloadResult { - value, - block_root, - seen_timestamp, - .. - } = download_result; - cx.send_blobs_for_processing(id, block_root, value, seen_timestamp) - .map_err(LookupRequestError::SendFailedProcessor) - } - - fn response_type() -> ResponseType { - ResponseType::Blob - } - fn request_state_mut(request: &mut SingleBlockLookup) -> Result<&mut Self, &'static str> { - match &mut request.component_requests { - ComponentRequests::WaitingForBlock => Err("waiting for block"), - ComponentRequests::ActiveBlobRequest(request, _) => Ok(request), - ComponentRequests::ActiveCustodyRequest { .. } => Err("expecting custody request"), - ComponentRequests::NotNeeded { .. } => Err("not needed"), - } - } - fn get_state(&self) -> &SingleLookupRequestState { - &self.state - } - fn get_state_mut(&mut self) -> &mut SingleLookupRequestState { - &mut self.state - } -} - -impl RequestState for CustodyRequestState { - type VerifiedResponseType = DataColumnSidecarList; - - fn make_request( - &self, - id: Id, - lookup_peers: Arc>>, - _: usize, - cx: &mut SyncNetworkContext, - ) -> Result { - cx.custody_lookup_request(id, self.block_root, lookup_peers) - .map_err(LookupRequestError::SendFailedNetwork) - } - - fn send_for_processing( - id: Id, - download_result: DownloadResult, - cx: &SyncNetworkContext, - ) -> Result<(), LookupRequestError> { - let DownloadResult { - value, - block_root, - seen_timestamp, - .. - } = download_result; - cx.send_custody_columns_for_processing( - id, - block_root, - value, - seen_timestamp, - BlockProcessType::SingleCustodyColumn(id), - ) - .map_err(LookupRequestError::SendFailedProcessor) - } - - fn response_type() -> ResponseType { - ResponseType::CustodyColumn - } - fn request_state_mut(request: &mut SingleBlockLookup) -> Result<&mut Self, &'static str> { - match &mut request.component_requests { - ComponentRequests::WaitingForBlock => Err("waiting for block"), - ComponentRequests::ActiveBlobRequest { .. } => Err("expecting blob request"), - ComponentRequests::ActiveCustodyRequest(request) => Ok(request), - ComponentRequests::NotNeeded { .. } => Err("not needed"), - } - } - fn get_state(&self) -> &SingleLookupRequestState { - &self.state - } - fn get_state_mut(&mut self) -> &mut SingleLookupRequestState { - &mut self.state - } -} diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index f10610c751f..a265373e3fc 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -23,31 +23,24 @@ use self::parent_chain::{NodeChain, compute_parent_chains}; pub use self::single_block_lookup::DownloadResult; use self::single_block_lookup::{LookupRequestError, LookupResult, SingleBlockLookup}; -use super::manager::{BlockProcessType, BlockProcessingResult, SLOT_IMPORT_TOLERANCE}; -use super::network_context::{PeerGroup, RpcResponseError, SyncNetworkContext}; +use super::manager::{BlockProcessType, SLOT_IMPORT_TOLERANCE}; +use super::network_context::{RpcResponseError, SyncNetworkContext}; use crate::metrics; +use crate::network_beacon_processor::BlockProcessingResult; use crate::sync::SyncMessage; -use crate::sync::block_lookups::common::ResponseType; use crate::sync::block_lookups::parent_chain::find_oldest_fork_ancestor; -use beacon_chain::block_verification_types::AsBlock; -use beacon_chain::data_availability_checker::{ - AvailabilityCheckError, AvailabilityCheckErrorCategory, -}; -use beacon_chain::{AvailabilityProcessingStatus, BeaconChainTypes, BlockError}; -pub use common::RequestState; +use beacon_chain::BeaconChainTypes; use fnv::FnvHashMap; +use lighthouse_network::PeerId; use lighthouse_network::service::api_types::SingleLookupReqId; -use lighthouse_network::{PeerAction, PeerId}; use lru_cache::LRUTimeCache; -pub use single_block_lookup::{BlobRequestState, BlockRequestState, CustodyRequestState}; use std::collections::hash_map::Entry; use std::sync::Arc; use std::time::Duration; use store::Hash256; use tracing::{debug, error, warn}; -use types::{EthSpec, SignedBeaconBlock}; +use types::{DataColumnSidecarList, EthSpec, SignedBeaconBlock}; -pub mod common; pub mod parent_chain; mod single_block_lookup; @@ -77,42 +70,17 @@ const LOOKUP_MAX_DURATION_NO_PEERS_SECS: u64 = 10; /// take at most 2 GB. 200 lookups allow 3 parallel chains of depth 64 (current maximum). const MAX_LOOKUPS: usize = 200; -/// The values for `Blob`, `DataColumn` and `PartialDataColumn` is the parent root of the column. +type BlockDownloadResponse = Result>>, RpcResponseError>; +type CustodyDownloadResponse = + Result>, RpcResponseError>; + pub enum BlockComponent { Block(DownloadResult>>), - Blob(DownloadResult), - DataColumn(DownloadResult), - PartialDataColumn(DownloadResult), -} - -impl BlockComponent { - fn parent_root(&self) -> Hash256 { - match self { - BlockComponent::Block(block) => block.value.parent_root(), - BlockComponent::Blob(parent_root) - | BlockComponent::DataColumn(parent_root) - | BlockComponent::PartialDataColumn(parent_root) => parent_root.value, - } - } - fn get_type(&self) -> &'static str { - match self { - BlockComponent::Block(_) => "block", - BlockComponent::Blob(_) => "blob", - BlockComponent::DataColumn(_) => "data_column", - BlockComponent::PartialDataColumn(_) => "partial_data_column", - } - } + Sidecar, } pub type SingleLookupId = u32; -enum Action { - Retry, - ParentUnknown { parent_root: Hash256 }, - Drop(/* reason: */ String), - Continue, -} - pub struct BlockLookups { /// A cache of block roots that must be ignored for some time to prevent useless searches. For /// example if a chain is too long, its lookup chain is dropped, and range sync is expected to @@ -200,11 +168,10 @@ impl BlockLookups { &mut self, block_root: Hash256, block_component: BlockComponent, + parent_root: Hash256, peer_id: PeerId, cx: &mut SyncNetworkContext, ) -> bool { - let parent_root = block_component.parent_root(); - let parent_lookup_exists = self.search_parent_of_child(parent_root, block_root, &[peer_id], cx); // Only create the child lookup if the parent exists @@ -214,9 +181,9 @@ impl BlockLookups { block_root, Some(block_component), Some(parent_root), - // On a `UnknownParentBlock` or `UnknownParentBlob` event the peer is not required - // to have the rest of the block components (refer to decoupled blob gossip). Create - // the lookup with zero peers to house the block components. + // On a `UnknownParentBlock` or `UnknownParentSidecarHeader` event the peer is not + // required to have the rest of the block components. Create the lookup with zero + // peers to house the block components. &[], cx, ) @@ -225,7 +192,7 @@ impl BlockLookups { } } - /// Seach a block whose parent root is unknown. + /// Search a block whose parent root is unknown. /// /// Returns true if the lookup is created or already exists #[must_use = "only reference the new lookup if returns true"] @@ -368,13 +335,9 @@ impl BlockLookups { .find(|(_id, lookup)| lookup.is_for_block(block_root)) { if let Some(block_component) = block_component { - let component_type = block_component.get_type(); let imported = lookup.add_child_components(block_component); if !imported { - debug!( - ?block_root, - component_type, "Lookup child component ignored" - ); + debug!(?block_root, "Lookup child component ignored"); } } @@ -446,88 +409,33 @@ impl BlockLookups { /* Lookup responses */ - /// Process a block or blob response received from a single lookup request. - pub fn on_download_response>( + /// Process a block response received from a single lookup request. + pub fn on_block_download_response( &mut self, id: SingleLookupReqId, - response: Result<(R::VerifiedResponseType, PeerGroup, Duration), RpcResponseError>, + response: BlockDownloadResponse, cx: &mut SyncNetworkContext, ) { - let result = self.on_download_response_inner::(id, response, cx); - self.on_lookup_result(id.lookup_id, result, "download_response", cx); + let Some(lookup) = self.single_block_lookups.get_mut(&id.lookup_id) else { + debug!(?id, "Block returned for single block lookup not present"); + return; + }; + let result = lookup.on_block_download_response(id.req_id, response, cx); + self.on_lookup_result(id.lookup_id, result, "block_download_response", cx); } - /// Process a block or blob response received from a single lookup request. - pub fn on_download_response_inner>( + pub fn on_custody_download_response( &mut self, id: SingleLookupReqId, - response: Result<(R::VerifiedResponseType, PeerGroup, Duration), RpcResponseError>, + response: CustodyDownloadResponse, cx: &mut SyncNetworkContext, - ) -> Result { - // Note: no need to downscore peers here, already downscored on network context - - let response_type = R::response_type(); + ) { let Some(lookup) = self.single_block_lookups.get_mut(&id.lookup_id) else { - // We don't have the ability to cancel in-flight RPC requests. So this can happen - // if we started this RPC request, and later saw the block/blobs via gossip. - debug!(?id, "Block returned for single block lookup not present"); - return Err(LookupRequestError::UnknownLookup); + debug!(?id, "Custody returned for single block lookup not present"); + return; }; - - let block_root = lookup.block_root(); - let request_state = R::request_state_mut(lookup) - .map_err(|e| LookupRequestError::BadState(e.to_owned()))? - .get_state_mut(); - - match response { - Ok((response, peer_group, seen_timestamp)) => { - debug!( - ?block_root, - ?id, - ?peer_group, - ?response_type, - "Received lookup download success" - ); - - // Here we could check if response extends a parent chain beyond its max length. - // However we defer that check to the handling of a processing error ParentUnknown. - // - // Here we could check if there's already a lookup for parent_root of `response`. In - // that case we know that sending the response for processing will likely result in - // a `ParentUnknown` error. However, for simplicity we choose to not implement this - // optimization. - - // Register the download peer here. Once we have received some data over the wire we - // attribute it to this peer for scoring latter regardless of how the request was - // done. - request_state.on_download_success( - id.req_id, - DownloadResult { - value: response, - block_root, - seen_timestamp, - peer_group, - }, - )?; - // continue_request will send for processing as the request state is AwaitingProcessing - } - Err(e) => { - // No need to log peer source here. When sending a DataColumnsByRoot request we log - // the peer and the request ID which is linked to this `id` value here. - debug!( - ?block_root, - ?id, - ?response_type, - error = ?e, - "Received lookup download failure" - ); - - request_state.on_download_failure(id.req_id)?; - // continue_request will retry a download as the request state is AwaitingDownload - } - } - - lookup.continue_requests(cx) + let result = lookup.on_custody_download_response(id.req_id, response, cx); + self.on_lookup_result(id.lookup_id, result, "custody_download_response", cx); } /* Error responses */ @@ -549,210 +457,29 @@ impl BlockLookups { result: BlockProcessingResult, cx: &mut SyncNetworkContext, ) { - let lookup_result = match process_type { - BlockProcessType::SingleBlock { id } => { - self.on_processing_result_inner::>(id, result, cx) - } - BlockProcessType::SingleBlob { id } => { - self.on_processing_result_inner::>(id, result, cx) - } - BlockProcessType::SingleCustodyColumn(id) => { - self.on_processing_result_inner::>(id, result, cx) - } - // TODO(gloas): route into the payload envelope lookup state machine. - BlockProcessType::SinglePayloadEnvelope(_) => Ok(LookupResult::Pending), - }; - self.on_lookup_result(process_type.id(), lookup_result, "processing_result", cx); - } - - pub fn on_processing_result_inner>( - &mut self, - lookup_id: SingleLookupId, - result: BlockProcessingResult, - cx: &mut SyncNetworkContext, - ) -> Result { + let lookup_id = process_type.id(); let Some(lookup) = self.single_block_lookups.get_mut(&lookup_id) else { debug!(id = lookup_id, "Unknown single block lookup"); - return Err(LookupRequestError::UnknownLookup); + return; }; - let block_root = lookup.block_root(); - let request_state = R::request_state_mut(lookup) - .map_err(|e| LookupRequestError::BadState(e.to_owned()))? - .get_state_mut(); - debug!( - component = ?R::response_type(), - ?block_root, + block_root = ?lookup.block_root(), id = lookup_id, + ?process_type, ?result, "Received lookup processing result" ); - let action = match result { - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(_)) - | BlockProcessingResult::Err(BlockError::DuplicateFullyImported(..)) - | BlockProcessingResult::Err(BlockError::GenesisBlock) => { - // Successfully imported - request_state.on_processing_success()?; - Action::Continue - } - - BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents { - .. - }) => { - // `on_processing_success` is called here to ensure the request state is updated prior to checking - // if both components have been processed. - request_state.on_processing_success()?; - - if lookup.all_components_processed() { - // We don't request for other block components until being sure that the block has - // data. If we request blobs / columns to a peer we are sure those must exist. - // Therefore if all components are processed and we still receive `MissingComponents` - // it indicates an internal bug. - return Err(LookupRequestError::MissingComponentsAfterAllProcessed); - } else { - // Continue request, potentially request blobs - Action::Retry - } - } - BlockProcessingResult::Err(BlockError::DuplicateImportStatusUnknown(..)) => { - // This is unreachable because RPC blocks do not undergo gossip verification, and - // this error can *only* come from gossip verification. - error!(?block_root, "Single block lookup hit unreachable condition"); - Action::Drop("DuplicateImportStatusUnknown".to_owned()) - } - BlockProcessingResult::Ignored => { - // Beacon processor signalled to ignore the block processing result. - // This implies that the cpu is overloaded. Drop the request. - warn!( - component = ?R::response_type(), - "Lookup component processing ignored, cpu might be overloaded" - ); - Action::Drop("Block processing ignored".to_owned()) - } - BlockProcessingResult::Err(e) => { - match e { - BlockError::BeaconChainError(e) => { - // Internal error - error!(%block_root, error = ?e, "Beacon chain error processing lookup component"); - Action::Drop(format!("{e:?}")) - } - BlockError::ParentUnknown { parent_root, .. } => { - // Reverts the status of this request to `AwaitingProcessing` holding the - // downloaded data. A future call to `continue_requests` will re-submit it - // once there are no pending parent requests. - // Note: `BlockError::ParentUnknown` is only returned when processing - // blocks, not blobs. - request_state.revert_to_awaiting_processing()?; - Action::ParentUnknown { parent_root } - } - ref e @ BlockError::ExecutionPayloadError(ref epe) if !epe.penalize_peer() => { - // These errors indicate that the execution layer is offline - // and failed to validate the execution payload. Do not downscore peer. - debug!( - ?block_root, - error = ?e, - "Single block lookup failed. Execution layer is offline / unsynced / misconfigured" - ); - Action::Drop(format!("{e:?}")) - } - BlockError::AvailabilityCheck(e) - if e.category() == AvailabilityCheckErrorCategory::Internal => - { - // There errors indicate internal problems and should not downscore the peer - warn!(?block_root, error = ?e, "Internal availability check failure"); - - // Here we choose *not* to call `on_processing_failure` because this could result in a bad - // lookup state transition. This error invalidates both blob and block requests, and we don't know the - // state of both requests. Blobs may have already successfullly processed for example. - // We opt to drop the lookup instead. - Action::Drop(format!("{e:?}")) - } - other => { - debug!( - ?block_root, - component = ?R::response_type(), - error = ?other, - "Invalid lookup component" - ); - let peer_group = request_state.on_processing_failure()?; - let peers_to_penalize: Vec<_> = match other { - // Note: currenlty only InvalidColumn errors have index granularity, - // but future errors may follow the same pattern. Generalize this - // pattern with https://github.com/sigp/lighthouse/pull/6321 - BlockError::AvailabilityCheck( - AvailabilityCheckError::InvalidColumn((index_opt, _)), - ) => { - match index_opt { - Some(index) => peer_group.of_index(index as usize).collect(), - // If no index supplied this is an un-attributable fault. In practice - // this should never happen. - None => vec![], - } - } - _ => peer_group.all().collect(), - }; - for peer in peers_to_penalize { - cx.report_peer( - *peer, - PeerAction::MidToleranceError, - match R::response_type() { - ResponseType::Block => "lookup_block_processing_failure", - ResponseType::Blob => "lookup_blobs_processing_failure", - ResponseType::CustodyColumn => { - "lookup_custody_column_processing_failure" - } - }, - ); - } - - Action::Retry - } - } + let lookup_result = match process_type { + BlockProcessType::SingleBlock { .. } => lookup.on_block_processing_result(result, cx), + BlockProcessType::SingleCustodyColumn(_) => { + lookup.on_data_processing_result(result, cx) } + // TODO(gloas): route into the payload envelope lookup state machine. + BlockProcessType::SinglePayloadEnvelope(_) => Ok(LookupResult::Pending), }; - - match action { - Action::Retry => { - // Trigger download for all components in case `MissingComponents` failed the blob - // request. Also if blobs are `AwaitingProcessing` and need to be progressed - lookup.continue_requests(cx) - } - Action::ParentUnknown { parent_root } => { - let peers = lookup.all_peers(); - // Mark lookup as awaiting **before** creating the parent lookup. At this point the - // lookup maybe inconsistent. - lookup.set_awaiting_parent(parent_root); - let parent_lookup_exists = - self.search_parent_of_child(parent_root, block_root, &peers, cx); - if parent_lookup_exists { - // The parent lookup exist or has been created. It's safe for `lookup` to - // reference the parent as awaiting. - debug!( - id = lookup_id, - ?block_root, - ?parent_root, - "Marking lookup as awaiting parent" - ); - Ok(LookupResult::Pending) - } else { - // The parent lookup is faulty and was not created, we must drop the `lookup` as - // it's in an inconsistent state. We must drop all of its children too. - Err(LookupRequestError::Failed(format!( - "Parent lookup is faulty {parent_root:?}" - ))) - } - } - Action::Drop(reason) => { - // Drop with noop - Err(LookupRequestError::Failed(reason)) - } - Action::Continue => { - // Drop this completed lookup only - Ok(LookupResult::Completed) - } - } + self.on_lookup_result(lookup_id, lookup_result, "processing_result", cx); } pub fn on_external_processing_result( @@ -849,7 +576,20 @@ impl BlockLookups { cx: &mut SyncNetworkContext, ) -> bool { match result { - Ok(LookupResult::Pending) => true, // no action + Ok(LookupResult::Pending) => true, + Ok(LookupResult::ParentUnknown { + parent_root, + block_root, + peers, + }) => { + if self.search_parent_of_child(parent_root, block_root, &peers, cx) { + true + } else { + self.drop_lookup_and_children(id, "Failed"); + self.update_metrics(); + false + } + } Ok(LookupResult::Completed) => { if let Some(lookup) = self.single_block_lookups.remove(&id) { debug!( @@ -1015,6 +755,7 @@ impl BlockLookups { } /// Adds peers to a lookup and its ancestors recursively. + /// /// Note: Takes a `lookup_id` as argument to allow recursion on mutable lookups, without having /// to duplicate the code to add peers to a lookup fn add_peers_to_lookup_and_ancestors( @@ -1041,12 +782,12 @@ impl BlockLookups { } if let Some(parent_root) = lookup.awaiting_parent() { - if let Some((&child_id, _)) = self + if let Some((&parent_id, _)) = self .single_block_lookups .iter() .find(|(_, l)| l.block_root() == parent_root) { - self.add_peers_to_lookup_and_ancestors(child_id, peers, cx) + self.add_peers_to_lookup_and_ancestors(parent_id, peers, cx) } else { Err(format!("Lookup references unknown parent {parent_root:?}")) } diff --git a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs index 23bfd531f0f..8eb58da4e6e 100644 --- a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs +++ b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs @@ -1,21 +1,22 @@ use super::{BlockComponent, PeerId, SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS}; -use crate::sync::block_lookups::common::RequestState; +use crate::network_beacon_processor::BlockProcessingResult; +use crate::sync::block_lookups::{BlockDownloadResponse, CustodyDownloadResponse}; +use crate::sync::manager::BlockProcessType; use crate::sync::network_context::{ - LookupRequestResult, PeerGroup, ReqId, RpcRequestSendError, SendErrorProcessor, - SyncNetworkContext, + LookupRequestResult, PeerGroup, ReqId, RpcRequestSendError, RpcResponseError, + SendErrorProcessor, SyncNetworkContext, }; -use beacon_chain::{BeaconChainTypes, BlockProcessStatus}; +use beacon_chain::BeaconChainTypes; +use beacon_chain::block_verification_types::AsBlock; use educe::Educe; use lighthouse_network::service::api_types::Id; use parking_lot::RwLock; use std::collections::HashSet; -use std::fmt::Debug; use std::sync::Arc; use std::time::{Duration, Instant}; use store::Hash256; use strum::IntoStaticStr; use tracing::{Span, debug_span}; -use types::data::FixedBlobSidecarList; use types::{DataColumnSidecarList, EthSpec, SignedBeaconBlock, Slot}; // Dedicated enum for LookupResult to force its usage @@ -25,15 +26,18 @@ pub enum LookupResult { Completed, /// Lookup is expecting some future event from the network Pending, + /// Block's parent is not known to fork-choice, a parent lookup is needed + ParentUnknown { + parent_root: Hash256, + block_root: Hash256, + peers: Vec, + }, } #[derive(Debug, PartialEq, Eq, IntoStaticStr)] pub enum LookupRequestError { /// Too many failed attempts - TooManyAttempts { - /// The failed attempts were primarily due to processing failures. - cannot_process: bool, - }, + TooManyAttempts, /// Error sending event to network SendFailedNetwork(RpcRequestSendError), /// Error sending event to processor @@ -42,9 +46,6 @@ pub enum LookupRequestError { BadState(String), /// Lookup failed for some other reason and should be dropped Failed(/* reason: */ String), - /// Received MissingComponents when all components have been processed. This should never - /// happen, and indicates some internal bug - MissingComponentsAfterAllProcessed, /// Attempted to retrieve a not known lookup id UnknownLookup, /// Received a download result for a different request id than the in-flight request. @@ -56,34 +57,63 @@ pub enum LookupRequestError { }, } +#[derive(Debug)] +struct BlockRequest { + state: SingleLookupRequestState>>, +} + +impl BlockRequest { + fn new() -> Self { + Self { + state: SingleLookupRequestState::new(), + } + } + + fn is_complete(&self) -> bool { + self.state.is_processed() + } +} + +#[derive(Debug)] +enum DataRequest { + WaitingForBlock, + Request { + slot: Slot, + state: SingleLookupRequestState>, + }, + NoData, +} + +impl DataRequest { + fn is_complete(&self) -> bool { + match &self { + DataRequest::WaitingForBlock => false, + DataRequest::Request { state, .. } => state.is_processed(), + DataRequest::NoData => true, + } + } +} + +type PeerSet = Arc>>; + #[derive(Educe)] #[educe(Debug(bound(T: BeaconChainTypes)))] pub struct SingleBlockLookup { pub id: Id, - pub block_request_state: BlockRequestState, - pub component_requests: ComponentRequests, + block_root: Hash256, + block_request: BlockRequest, + data_request: DataRequest, /// Peers that claim to have imported this set of block components. This state is shared with /// the custody request to have an updated view of the peers that claim to have imported the /// block associated with this lookup. The peer set of a lookup can change rapidly, and faster /// than the lifetime of a custody request. #[educe(Debug(method(fmt_peer_set_as_len)))] - peers: Arc>>, - block_root: Hash256, + peers: PeerSet, awaiting_parent: Option, created: Instant, pub(crate) span: Span, } -#[derive(Debug)] -pub(crate) enum ComponentRequests { - WaitingForBlock, - ActiveBlobRequest(BlobRequestState, usize), - ActiveCustodyRequest(CustodyRequestState), - // When printing in debug this state display the reason why it's not needed - #[allow(dead_code)] - NotNeeded(&'static str), -} - impl SingleBlockLookup { pub fn new( requested_block_root: Hash256, @@ -99,25 +129,25 @@ impl SingleBlockLookup { Self { id, - block_request_state: BlockRequestState::new(requested_block_root), - component_requests: ComponentRequests::WaitingForBlock, - peers: Arc::new(RwLock::new(HashSet::from_iter(peers.iter().copied()))), block_root: requested_block_root, + block_request: BlockRequest::new(), + data_request: DataRequest::WaitingForBlock, + peers: Arc::new(RwLock::new(peers.iter().copied().collect())), awaiting_parent, created: Instant::now(), span: lookup_span, } } - /// Reset the status of all internal requests + /// Reset the status of all requests (used on block processing failure) pub fn reset_requests(&mut self) { - self.block_request_state = BlockRequestState::new(self.block_root); - self.component_requests = ComponentRequests::WaitingForBlock; + self.block_request = BlockRequest::new(); + self.data_request = DataRequest::WaitingForBlock; } - /// Return the slot of this lookup's block if it's currently cached as `AwaitingProcessing` + /// Return the slot of this lookup's block if it's currently cached pub fn peek_downloaded_block_slot(&self) -> Option { - self.block_request_state + self.block_request .state .peek_downloaded_data() .map(|block| block.slot()) @@ -152,17 +182,12 @@ impl SingleBlockLookup { /// Maybe insert a verified response into this lookup. Returns true if imported pub fn add_child_components(&mut self, block_component: BlockComponent) -> bool { match block_component { - BlockComponent::Block(block) => self - .block_request_state - .state - .insert_verified_response(block), - BlockComponent::Blob(_) - | BlockComponent::DataColumn(_) - | BlockComponent::PartialDataColumn(_) => { - // For now ignore single blobs and columns, as the blob request state assumes all blobs are - // attributed to the same peer = the peer serving the remaining blobs. Ignoring this - // block component has a minor effect, causing the node to re-request this blob - // once the parent chain is successfully resolved + BlockComponent::Block(block) => { + self.block_request.state.insert_verified_response(block) + } + BlockComponent::Sidecar => { + // There's nothing to do here, there's no component to insert. The lookup downloads + // its required data columns itself once it has the block. false } } @@ -173,33 +198,14 @@ impl SingleBlockLookup { self.block_root() == block_root } - /// Returns true if the block has already been downloaded. - pub fn all_components_processed(&self) -> bool { - self.block_request_state.state.is_processed() - && match &self.component_requests { - ComponentRequests::WaitingForBlock => false, - ComponentRequests::ActiveBlobRequest(request, _) => request.state.is_processed(), - ComponentRequests::ActiveCustodyRequest(request) => request.state.is_processed(), - ComponentRequests::NotNeeded { .. } => true, - } - } - /// Returns true if this request is expecting some event to make progress pub fn is_awaiting_event(&self) -> bool { self.awaiting_parent.is_some() - || self.block_request_state.state.is_awaiting_event() - || match &self.component_requests { - // If components are waiting for the block request to complete, here we should - // check if the`block_request_state.state.is_awaiting_event(). However we already - // checked that above, so `WaitingForBlock => false` is equivalent. - ComponentRequests::WaitingForBlock => false, - ComponentRequests::ActiveBlobRequest(request, _) => { - request.state.is_awaiting_event() - } - ComponentRequests::ActiveCustodyRequest(request) => { - request.state.is_awaiting_event() - } - ComponentRequests::NotNeeded { .. } => false, + || self.block_request.state.is_awaiting_event() + || match &self.data_request { + DataRequest::WaitingForBlock => true, + DataRequest::Request { state, .. } => state.is_awaiting_event(), + DataRequest::NoData => false, } } @@ -210,147 +216,167 @@ impl SingleBlockLookup { cx: &mut SyncNetworkContext, ) -> Result { let _guard = self.span.clone().entered(); - // TODO: Check what's necessary to download, specially for blobs - self.continue_request::>(cx, 0)?; - - if let ComponentRequests::WaitingForBlock = self.component_requests { - let downloaded_block = self - .block_request_state - .state - .peek_downloaded_data() - .cloned(); - - if let Some(block) = downloaded_block.or_else(|| { - // If the block is already being processed or fully validated, retrieve how many blobs - // it expects. Consider any stage of the block. If the block root has been validated, we - // can assert that this is the correct value of `blob_kzg_commitments_count`. - match cx.chain.get_block_process_status(&self.block_root) { - BlockProcessStatus::Unknown => None, - BlockProcessStatus::NotValidated(block, _) - | BlockProcessStatus::ExecutionValidated(block) => Some(block.clone()), - } - }) { - let expected_blobs = block.num_expected_blobs(); - let block_epoch = block.slot().epoch(T::EthSpec::slots_per_epoch()); - if expected_blobs == 0 { - self.component_requests = ComponentRequests::NotNeeded("no data"); - } else if cx.chain.should_fetch_blobs(block_epoch) { - self.component_requests = ComponentRequests::ActiveBlobRequest( - BlobRequestState::new(self.block_root), - expected_blobs, - ); - } else if cx.chain.should_fetch_custody_columns(block_epoch) { - self.component_requests = ComponentRequests::ActiveCustodyRequest( - CustodyRequestState::new(self.block_root), - ); - } else { - self.component_requests = ComponentRequests::NotNeeded("outside da window"); - } - } else { - // Wait to download the block before downloading blobs. Then we can be sure that the - // block has data, so there's no need to do "blind" requests for all possible blobs and - // latter handle the case where if the peer sent no blobs, penalize. - // - // Lookup sync event safety: Reaching this code means that a block is not in any pre-import - // cache nor in the request state of this lookup. Therefore, the block must either: (1) not - // be downloaded yet or (2) the block is already imported into the fork-choice. - // In case (1) the lookup must either successfully download the block or get dropped. - // In case (2) the block will be downloaded, processed, reach `DuplicateFullyImported` - // and get dropped as completed. - } + + // === Block request === + self.block_request.state.maybe_start_downloading(|| { + cx.block_lookup_request(self.id, self.peers.clone(), self.block_root) + })?; + if self.awaiting_parent.is_none() + && let Some(data) = self.block_request.state.maybe_start_processing() + { + cx.send_block_for_processing(self.id, self.block_root, data.value, data.seen_timestamp) + .map_err(LookupRequestError::SendFailedProcessor)?; } - match &self.component_requests { - ComponentRequests::WaitingForBlock => {} // do nothing - ComponentRequests::ActiveBlobRequest(_, expected_blobs) => { - self.continue_request::>(cx, *expected_blobs)? - } - ComponentRequests::ActiveCustodyRequest(_) => { - self.continue_request::>(cx, 0)? + // === Data request === + loop { + match &mut self.data_request { + DataRequest::WaitingForBlock => { + if let Some(block) = self.block_request.state.peek_downloaded_data() { + let block_epoch = block + .slot() + .epoch(::EthSpec::slots_per_epoch()); + self.data_request = if block.num_expected_blobs() == 0 { + DataRequest::NoData + } else if cx.chain.should_fetch_custody_columns(block_epoch) { + DataRequest::Request { + slot: block.slot(), + state: SingleLookupRequestState::new(), + } + } else { + DataRequest::NoData + }; + } else { + break; + } + } + DataRequest::Request { slot, state } => { + state.maybe_start_downloading(|| { + cx.custody_lookup_request( + self.id, + self.block_root, + *slot, + self.peers.clone(), + ) + })?; + // Wait for the parent to be imported, data column processing result handle does + // not support `ParentUnknown`. + if self.awaiting_parent.is_none() + && let Some(data) = state.maybe_start_processing() + { + cx.send_custody_columns_for_processing( + self.id, + self.block_root, + data.value, + data.seen_timestamp, + BlockProcessType::SingleCustodyColumn(self.id), + ) + .map_err(LookupRequestError::SendFailedProcessor)?; + } + break; + } + DataRequest::NoData => break, } - ComponentRequests::NotNeeded { .. } => {} // do nothing } // If all components of this lookup are already processed, there will be no future events // that can make progress so it must be dropped. Consider the lookup completed. // This case can happen if we receive the components from gossip during a retry. - if self.all_components_processed() { - self.span = Span::none(); - Ok(LookupResult::Completed) - } else { - Ok(LookupResult::Pending) + if self.block_request.is_complete() && self.data_request.is_complete() { + return Ok(LookupResult::Completed); } + + Ok(LookupResult::Pending) } - /// Potentially makes progress on this request if it's in a progress-able state - fn continue_request>( + /// Handle block processing result. Advances the lookup state machine. + pub fn on_block_processing_result( &mut self, + result: BlockProcessingResult, cx: &mut SyncNetworkContext, - expected_blobs: usize, - ) -> Result<(), LookupRequestError> { - let id = self.id; - let awaiting_parent = self.awaiting_parent.is_some(); - let request = - R::request_state_mut(self).map_err(|e| LookupRequestError::BadState(e.to_owned()))?; - - // Attempt to progress awaiting downloads - if request.get_state().is_awaiting_download() { - // Verify the current request has not exceeded the maximum number of attempts. - let request_state = request.get_state(); - if request_state.failed_attempts() >= SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS { - let cannot_process = request_state.more_failed_processing_attempts(); - return Err(LookupRequestError::TooManyAttempts { cannot_process }); + ) -> Result { + match result { + BlockProcessingResult::Imported(_fully_imported, _info) => { + self.block_request.state.on_processing_success()?; } - - let peers = self.peers.clone(); - let request = R::request_state_mut(self) - .map_err(|e| LookupRequestError::BadState(e.to_owned()))?; - - match request.make_request(id, peers, expected_blobs, cx)? { - LookupRequestResult::RequestSent(req_id) => { - // Lookup sync event safety: If make_request returns `RequestSent`, we are - // guaranteed that `BlockLookups::on_download_response` will be called exactly - // with this `req_id`. - request.get_state_mut().on_download_start(req_id)? - } - LookupRequestResult::NoRequestNeeded(reason) => { - // Lookup sync event safety: Advances this request to the terminal `Processed` - // state. If all requests reach this state, the request is marked as completed - // in `Self::continue_requests`. - request.get_state_mut().on_completed_request(reason)? - } - // Sync will receive a future event to make progress on the request, do nothing now - LookupRequestResult::Pending(reason) => { - // Lookup sync event safety: Refer to the code paths constructing - // `LookupRequestResult::Pending` - request - .get_state_mut() - .update_awaiting_download_status(reason); - return Ok(()); + BlockProcessingResult::ParentUnknown { parent_root } => { + // `BlockError::ParentUnknown` is only returned when processing blocks. Revert the + // block request to `Downloaded` and park this lookup until the parent resolves; a + // future call to `continue_requests` will re-submit the block for processing once + // the parent lookup completes. + self.block_request.state.revert_to_awaiting_processing()?; + self.set_awaiting_parent(parent_root); + return Ok(LookupResult::ParentUnknown { + parent_root, + block_root: self.block_root, + peers: self.all_peers(), + }); + } + BlockProcessingResult::Error { penalty, .. } => { + let peers = self.block_request.state.on_processing_failure()?; + if let Some((action, whom, msg)) = penalty { + whom.apply(action, &peers, msg, cx); } } + } + self.continue_requests(cx) + } + + /// Handle data processing result + pub fn on_data_processing_result( + &mut self, + result: BlockProcessingResult, + cx: &mut SyncNetworkContext, + ) -> Result { + let DataRequest::Request { state, .. } = &mut self.data_request else { + return Err(LookupRequestError::BadState("no data_request".to_owned())); + }; - // Otherwise, attempt to progress awaiting processing - // If this request is awaiting a parent lookup to be processed, do not send for processing. - // The request will be rejected with unknown parent error. - } else if !awaiting_parent { - // maybe_start_processing returns Some if state == AwaitingProcess. This pattern is - // useful to conditionally access the result data. - if let Some(result) = request.get_state_mut().maybe_start_processing() { - // Lookup sync event safety: If `send_for_processing` returns Ok() we are guaranteed - // that `BlockLookups::on_processing_result` will be called exactly once with this - // lookup_id - return R::send_for_processing(id, result, cx); + match result { + BlockProcessingResult::Imported(_fully_imported, _info) => { + state.on_processing_success()?; + } + BlockProcessingResult::ParentUnknown { .. } => { + return Err(LookupRequestError::BadState( + "data processing returned ParentUnknown".to_owned(), + )); + } + BlockProcessingResult::Error { penalty, .. } => { + let peers = state.on_processing_failure()?; + if let Some((action, whom, msg)) = penalty { + whom.apply(action, &peers, msg, cx); + } } - // Lookup sync event safety: If the request is not in `AwaitingDownload` or - // `AwaitingProcessing` state it is guaranteed to receive some event to make progress. } + self.continue_requests(cx) + } - // Lookup sync event safety: If a lookup is awaiting a parent we are guaranteed to either: - // (1) attempt to make progress with `BlockLookups::continue_child_lookups` if the parent - // lookup completes, or (2) get dropped if the parent fails and is dropped. + /// Handle a block download response. Updates download state and advances the lookup. + pub fn on_block_download_response( + &mut self, + req_id: ReqId, + result: BlockDownloadResponse, + cx: &mut SyncNetworkContext, + ) -> Result { + self.block_request + .state + .on_download_response(req_id, result)?; + self.continue_requests(cx) + } - Ok(()) + /// Handle a custody columns download response. Updates download state and advances the lookup. + pub fn on_custody_download_response( + &mut self, + req_id: ReqId, + result: CustodyDownloadResponse, + cx: &mut SyncNetworkContext, + ) -> Result { + let DataRequest::Request { state, .. } = &mut self.data_request else { + return Err(LookupRequestError::BadState("no data_request".to_owned())); + }; + + state.on_download_response(req_id, result)?; + self.continue_requests(cx) } /// Get all unique peers that claim to have imported this set of block components @@ -359,7 +385,7 @@ impl SingleBlockLookup { } /// Add peer to all request states. The peer must be able to serve this request. - /// Returns true if the peer was newly inserted into some request state. + /// Returns true if the peer was newly inserted into any peer set. pub fn add_peer(&mut self, peer_id: PeerId) -> bool { self.peers.write().insert(peer_id) } @@ -375,68 +401,23 @@ impl SingleBlockLookup { } } -/// The state of the blob request component of a `SingleBlockLookup`. -#[derive(Educe)] -#[educe(Debug)] -pub struct BlobRequestState { - #[educe(Debug(ignore))] - pub block_root: Hash256, - pub state: SingleLookupRequestState>, -} - -impl BlobRequestState { - pub fn new(block_root: Hash256) -> Self { - Self { - block_root, - state: SingleLookupRequestState::new(), - } - } -} - -/// The state of the custody request component of a `SingleBlockLookup`. -#[derive(Educe)] -#[educe(Debug)] -pub struct CustodyRequestState { - #[educe(Debug(ignore))] - pub block_root: Hash256, - pub state: SingleLookupRequestState>, -} - -impl CustodyRequestState { - pub fn new(block_root: Hash256) -> Self { - Self { - block_root, - state: SingleLookupRequestState::new(), - } - } -} - -/// The state of the block request component of a `SingleBlockLookup`. -#[derive(Educe)] -#[educe(Debug)] -pub struct BlockRequestState { - #[educe(Debug(ignore))] - pub requested_block_root: Hash256, - pub state: SingleLookupRequestState>>, -} - -impl BlockRequestState { - pub fn new(block_root: Hash256) -> Self { - Self { - requested_block_root: block_root, - state: SingleLookupRequestState::new(), - } - } -} - #[derive(Debug, Clone)] pub struct DownloadResult { pub value: T, - pub block_root: Hash256, pub seen_timestamp: Duration, pub peer_group: PeerGroup, } +impl DownloadResult { + pub fn new(value: T, peer_group: PeerGroup, seen_timestamp: Duration) -> Self { + Self { + value, + seen_timestamp, + peer_group, + } + } +} + #[derive(IntoStaticStr)] pub enum State { AwaitingDownload(/* reason */ &'static str), @@ -445,7 +426,7 @@ pub enum State { /// Request is processing, sent by lookup sync Processing(DownloadResult), /// Request is processed - Processed(/* reason */ &'static str), + Processed(/* reason */ &'static str, T), } /// Object representing the state of a single block or blob lookup request. @@ -512,8 +493,27 @@ impl SingleLookupRequestState { State::Downloading { .. } => None, State::AwaitingProcess(result) => Some(&result.value), State::Processing(result) => Some(&result.value), - State::Processed { .. } => None, + State::Processed(_, value) => Some(value), + } + } + + /// Drive download: check max attempts, issue request, handle result. + fn maybe_start_downloading( + &mut self, + request_fn: impl FnOnce() -> Result, RpcRequestSendError>, + ) -> Result<(), LookupRequestError> { + if self.is_awaiting_download() { + match request_fn().map_err(LookupRequestError::SendFailedNetwork)? { + LookupRequestResult::RequestSent(req_id) => self.on_download_start(req_id)?, + LookupRequestResult::NoRequestNeeded(reason, value) => { + self.on_completed_request(reason, value)? + } + LookupRequestResult::Pending(reason) => { + self.update_awaiting_download_status(reason) + } + } } + Ok(()) } /// Switch to `AwaitingProcessing` if the request is in `AwaitingDownload` state, otherwise @@ -548,6 +548,17 @@ impl SingleLookupRequestState { } } + pub fn on_download_response( + &mut self, + req_id: ReqId, + result: Result, RpcResponseError>, + ) -> Result<(), LookupRequestError> { + match result { + Ok(result) => self.on_download_success(req_id, result), + Err(_) => self.on_download_failure(req_id), + } + } + /// Registers a failure in downloading a block. This might be a peer disconnection or a wrong /// block. pub fn on_download_failure(&mut self, req_id: ReqId) -> Result<(), LookupRequestError> { @@ -560,6 +571,10 @@ impl SingleLookupRequestState { }); } self.failed_downloading = self.failed_downloading.saturating_add(1); + if self.failed_downloading >= SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS { + return Err(LookupRequestError::TooManyAttempts); + } + self.state = State::AwaitingDownload("not started"); Ok(()) } @@ -624,6 +639,9 @@ impl SingleLookupRequestState { State::Processing(result) => { let peers_source = result.peer_group.clone(); self.failed_processing = self.failed_processing.saturating_add(1); + if self.failed_processing >= SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS { + return Err(LookupRequestError::TooManyAttempts); + } self.state = State::AwaitingDownload("not started"); Ok(peers_source) } @@ -635,8 +653,8 @@ impl SingleLookupRequestState { pub fn on_processing_success(&mut self) -> Result<(), LookupRequestError> { match &self.state { - State::Processing(_) => { - self.state = State::Processed("processing success"); + State::Processing(data) => { + self.state = State::Processed("processing success", data.value.clone()); Ok(()) } other => Err(LookupRequestError::BadState(format!( @@ -646,10 +664,14 @@ impl SingleLookupRequestState { } /// Mark a request as complete without any download or processing - pub fn on_completed_request(&mut self, reason: &'static str) -> Result<(), LookupRequestError> { + pub fn on_completed_request( + &mut self, + reason: &'static str, + value: T, + ) -> Result<(), LookupRequestError> { match &self.state { State::AwaitingDownload { .. } => { - self.state = State::Processed(reason); + self.state = State::Processed(reason, value); Ok(()) } other => Err(LookupRequestError::BadState(format!( @@ -657,15 +679,6 @@ impl SingleLookupRequestState { ))), } } - - /// The total number of failures, whether it be processing or downloading. - pub fn failed_attempts(&self) -> u8 { - self.failed_processing + self.failed_downloading - } - - pub fn more_failed_processing_attempts(&self) -> bool { - self.failed_processing >= self.failed_downloading - } } // Display is used in the BadState assertions above @@ -682,15 +695,15 @@ impl std::fmt::Debug for State { match self { Self::AwaitingDownload(reason) => write!(f, "AwaitingDownload({})", reason), Self::Downloading(req_id) => write!(f, "Downloading({:?})", req_id), - Self::AwaitingProcess(d) => write!(f, "AwaitingProcess({:?})", d.peer_group), - Self::Processing(d) => write!(f, "Processing({:?})", d.peer_group), - Self::Processed(reason) => write!(f, "Processed({})", reason), + Self::AwaitingProcess(_) => write!(f, "AwaitingProcess"), + Self::Processing(_) => write!(f, "Processing"), + Self::Processed(reason, _) => write!(f, "Processed({})", reason), } } } fn fmt_peer_set_as_len( - peer_set: &Arc>>, + peer_set: &PeerSet, f: &mut std::fmt::Formatter, ) -> Result<(), std::fmt::Error> { write!(f, "{}", peer_set.read().len()) diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index bb43396473f..5ec45c8fea6 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -62,7 +62,7 @@ enum RangeBlockDataRequest { } #[derive(Debug)] -pub(crate) enum CouplingError { +pub enum CouplingError { InternalError(String), /// The peer we requested the columns from was faulty/malicious DataColumnPeerFailure { @@ -607,11 +607,14 @@ mod tests { let mut spec = test_spec::(); spec.deneb_fork_epoch = Some(Epoch::new(0)); + // Pin to pre-PeerDAS so this exercises the blob (not custody-column) path under any + // FORK_NAME. + spec.fulu_fork_epoch = None; let spec = Arc::new(spec); let da_checker = Arc::new(test_da_checker(spec.clone(), NodeCustodyType::Fullnode)); - // Assert response is finished and RpcBlocks cannot be constructed, because blobs weren't returned. + // Blobs are no longer required for availability, so the response succeeds without them. let result = info.responses(da_checker, spec).unwrap(); - assert!(result.is_err()) + assert!(result.is_ok()) } #[test] diff --git a/beacon_node/network/src/sync/custody_backfill_sync/mod.rs b/beacon_node/network/src/sync/custody_backfill_sync/mod.rs index c85610613c6..74318006d19 100644 --- a/beacon_node/network/src/sync/custody_backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/custody_backfill_sync/mod.rs @@ -126,6 +126,7 @@ pub struct CustodyBackFillSync { network_globals: Arc>, } +#[cfg_attr(feature = "disable-backfill", allow(dead_code))] impl CustodyBackFillSync { pub fn new( beacon_chain: Arc>, diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 14a38f0e72d..e3f6fbc0f41 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -33,33 +33,36 @@ //! needs to be searched for (i.e if an attestation references an unknown block) this manager can //! search for the block and subsequently search for parents if needed. -use super::backfill_sync::{BackFillSync, ProcessResult, SyncStart}; +#[cfg(not(feature = "disable-backfill"))] +use super::backfill_sync::SyncStart; +use super::backfill_sync::{BackFillSync, ProcessResult}; use super::block_lookups::BlockLookups; use super::network_context::{ CustodyByRootResult, RangeBlockComponent, RangeRequestId, RpcEvent, SyncNetworkContext, }; use super::peer_sync_info::{PeerSyncType, remote_sync_type}; +use super::proof_sync::ProofSync; use super::range_sync::{EPOCHS_PER_BATCH, RangeSync, RangeSyncType}; -use crate::network_beacon_processor::{ChainSegmentProcessId, NetworkBeaconProcessor}; +use crate::network_beacon_processor::{ + BlockProcessingResult, ChainSegmentProcessId, NetworkBeaconProcessor, +}; use crate::service::NetworkMessage; use crate::status::ToStatusMessage; -use crate::sync::block_lookups::{ - BlobRequestState, BlockComponent, BlockRequestState, CustodyRequestState, DownloadResult, -}; +use crate::sync::block_lookups::{BlockComponent, DownloadResult}; use crate::sync::custody_backfill_sync::CustodyBackFillSync; use crate::sync::network_context::{PeerGroup, RpcResponseResult}; use beacon_chain::block_verification_types::AsBlock; -use beacon_chain::{ - AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError, EngineState, -}; +use beacon_chain::{BeaconChain, BeaconChainTypes, EngineState}; use futures::StreamExt; use lighthouse_network::SyncInfo; use lighthouse_network::rpc::RPCError; +use lighthouse_network::rpc::methods::ExecutionProofStatus; use lighthouse_network::service::api_types::{ BlobsByRangeRequestId, BlocksByRangeRequestId, ComponentsByRangeRequestId, CustodyBackFillBatchRequestId, CustodyBackfillBatchId, CustodyRequester, DataColumnsByRangeRequestId, DataColumnsByRangeRequester, DataColumnsByRootRequestId, - DataColumnsByRootRequester, Id, SingleLookupReqId, SyncRequestId, + DataColumnsByRootRequester, ExecutionProofStatusRequestId, Id, SingleLookupReqId, + SyncRequestId, }; use lighthouse_network::types::{NetworkGlobals, SyncState}; use lighthouse_network::{PeerAction, PeerId}; @@ -74,7 +77,7 @@ use tokio::sync::mpsc; use tracing::{debug, error, info, trace}; use types::{ BlobSidecar, DataColumnSidecar, EthSpec, ForkContext, Hash256, SignedBeaconBlock, - SignedExecutionPayloadEnvelope, Slot, + SignedExecutionPayloadEnvelope, SignedExecutionProof, Slot, }; /// The number of slots ahead of us that is allowed before requesting a long-range (batch) Sync @@ -141,17 +144,27 @@ pub enum SyncMessage { seen_timestamp: Duration, }, - /// A block with an unknown parent has been received. - UnknownParentBlock(PeerId, Arc>, Hash256), + /// An execution proof has been received from the RPC. + RpcExecutionProof { + sync_request_id: SyncRequestId, + peer_id: PeerId, + execution_proof: Option>, + }, - /// A blob with an unknown parent has been received. - UnknownParentBlob(PeerId, Arc>), + /// An ExecutionProofStatus response has been received from the RPC, or a peer sent us its + /// status as an inbound request body. + RpcExecutionProofStatus { + peer_id: PeerId, + request_id: Option, + status: ExecutionProofStatus, + }, - /// A data column with an unknown parent has been received. - UnknownParentDataColumn(PeerId, Arc>), + /// A block with an unknown parent has been received. + UnknownParentBlock(PeerId, Arc>, Hash256), - /// A partial data column with an unknown parent has been received. - UnknownParentPartialDataColumn { + /// A sidecar (full/partial data column) with an unknown parent has been received. Carries only the header + /// info needed to trigger a parent lookup, decoupled from the concrete sidecar type. + UnknownParentSidecarHeader { peer_id: PeerId, block_root: Hash256, parent_root: Hash256, @@ -200,7 +213,6 @@ pub enum SyncMessage { #[derive(Debug, Clone)] pub enum BlockProcessType { SingleBlock { id: Id }, - SingleBlob { id: Id }, SingleCustodyColumn(Id), SinglePayloadEnvelope(Id), } @@ -209,20 +221,12 @@ impl BlockProcessType { pub fn id(&self) -> Id { match self { BlockProcessType::SingleBlock { id } - | BlockProcessType::SingleBlob { id } | BlockProcessType::SingleCustodyColumn(id) | BlockProcessType::SinglePayloadEnvelope(id) => *id, } } } -#[derive(Debug)] -pub enum BlockProcessingResult { - Ok(AvailabilityProcessingStatus), - Err(BlockError), - Ignored, -} - /// The result of processing multiple blocks (a chain segment). #[derive(Debug)] pub enum BatchProcessResult { @@ -269,6 +273,9 @@ pub struct SyncManager { /// The object handling long-range batch load-balanced syncing. range_sync: RangeSync, + /// Catch-up mechanism for missing optional execution proofs. + proof_sync: ProofSync, + /// Backfill syncing. backfill_sync: BackFillSync, @@ -324,6 +331,11 @@ impl SyncManager { fork_context: Arc, ) -> Self { let network_globals = beacon_processor.network_globals.clone(); + let proof_types = beacon_chain + .execution_layer + .as_ref() + .map(|execution_layer| execution_layer.proof_types().clone()) + .unwrap_or_default(); Self { chain: beacon_chain.clone(), input_channel: sync_recv, @@ -332,8 +344,10 @@ impl SyncManager { beacon_processor.clone(), beacon_chain.clone(), fork_context.clone(), + proof_types, ), range_sync: RangeSync::new(beacon_chain.clone()), + proof_sync: ProofSync::new(beacon_chain.clone()), backfill_sync: BackFillSync::new(beacon_chain.clone(), network_globals.clone()), custody_backfill_sync: CustodyBackFillSync::new(beacon_chain.clone(), network_globals), block_lookups: BlockLookups::new(), @@ -445,6 +459,12 @@ impl SyncManager { } } + if self.network_globals().config.enable_execution_proof + && self.network.is_proof_capable_peer(&peer_id) + { + self.proof_sync.add_peer(peer_id, &mut self.network); + } + self.update_sync_state(); // Try to make progress on custody requests that are waiting for peers @@ -510,9 +530,6 @@ impl SyncManager { SyncRequestId::SingleBlock { id } => { self.on_single_block_response(id, peer_id, RpcEvent::RPCError(error)) } - SyncRequestId::SingleBlob { id } => { - self.on_single_blob_response(id, peer_id, RpcEvent::RPCError(error)) - } SyncRequestId::SinglePayloadEnvelope { id } => { self.on_single_payload_envelope_response(id, peer_id, RpcEvent::RPCError(error)) } @@ -528,6 +545,18 @@ impl SyncManager { SyncRequestId::DataColumnsByRange(req_id) => { self.on_data_columns_by_range_response(req_id, peer_id, RpcEvent::RPCError(error)) } + SyncRequestId::ExecutionProofsByRange(req_id) => { + debug!(%peer_id, ?req_id, "Execution proofs by range request failed"); + self.proof_sync.on_range_request_error(&req_id); + } + SyncRequestId::ExecutionProofsByRoot(req_id) => { + debug!(%peer_id, ?req_id, "Execution proofs by root request failed"); + self.proof_sync.on_root_request_error(&req_id); + } + SyncRequestId::ExecutionProofStatus(req_id) => { + self.proof_sync + .on_peer_execution_proof_status_error(peer_id, req_id); + } } } @@ -541,6 +570,7 @@ impl SyncManager { self.range_sync.peer_disconnect(&mut self.network, peer_id); let _ = self.backfill_sync.peer_disconnected(peer_id); self.block_lookups.peer_disconnected(peer_id); + self.proof_sync.on_proof_capable_peer_disconnected(peer_id); // Inject a Disconnected error on all requests associated with the disconnected peer // to retry all batches/lookups. Only after removing the peer from the data structures to @@ -637,6 +667,7 @@ impl SyncManager { // If we synced a peer between status messages, most likely the peer has // advanced and will produce a head chain on re-status. Otherwise it will shift // to being synced + #[cfg(not(feature = "disable-backfill"))] let mut sync_state = { let head = self.chain.best_slot(); let current_slot = self.chain.slot().unwrap_or_else(|_| Slot::new(0)); @@ -658,6 +689,28 @@ impl SyncManager { } }; + #[cfg(feature = "disable-backfill")] + let sync_state = { + let head = self.chain.best_slot(); + let current_slot = self.chain.slot().unwrap_or_else(|_| Slot::new(0)); + + let peers = self.network_globals().peers.read(); + if current_slot >= head + && current_slot.sub(head) <= (SLOT_IMPORT_TOLERANCE as u64) + && head > 0 + { + SyncState::Synced + } else if peers.advanced_peers().next().is_some() { + SyncState::SyncTransition + } else if peers.synced_peers().next().is_none() { + SyncState::Stalled + } else { + // There are no peers that require syncing and we have at least one synced + // peer + SyncState::Synced + } + }; + // If we would otherwise be synced, first check if we need to perform or // complete a backfill sync. #[cfg(not(feature = "disable-backfill"))] @@ -723,6 +776,7 @@ impl SyncManager { self.backfill_sync.pause(); self.custody_backfill_sync .pause("Range sync in progress".to_string()); + self.proof_sync.pause(); SyncState::SyncingFinalized { start_slot, @@ -736,6 +790,7 @@ impl SyncManager { self.backfill_sync.pause(); self.custody_backfill_sync .pause("Range sync in progress".to_string()); + self.proof_sync.pause(); SyncState::SyncingHead { start_slot, @@ -761,6 +816,9 @@ impl SyncManager { ) { self.network.subscribe_core_topics(); + if self.network_globals().config.enable_execution_proof { + self.proof_sync.start(&mut self.network); + } } } } @@ -776,7 +834,10 @@ impl SyncManager { .as_ref() .map(|el| el.get_responsiveness_watch()) .into(); - futures::stream::iter(ee_responsiveness_watch.await).flatten() + match ee_responsiveness_watch.await.flatten() { + Some(watch) => watch.left_stream(), + None => futures::stream::empty().right_stream(), + } }; // min(LOOKUP_MAX_DURATION_*) is 15 seconds. The cost of calling prune_lookups more often is @@ -792,6 +853,7 @@ impl SyncManager { let epoch_duration = self.chain.slot_clock.slot_duration().as_secs() * T::EthSpec::slots_per_epoch(); let mut epoch_interval = tokio::time::interval(Duration::from_secs(epoch_duration)); + let mut proof_sync_interval = tokio::time::interval(self.chain.slot_clock.slot_duration()); // process any inbound messages loop { @@ -817,6 +879,9 @@ impl SyncManager { _ = epoch_interval.tick() => { self.update_sync_state(); } + _ = proof_sync_interval.tick(), if self.network_globals().config.enable_execution_proof => { + self.proof_sync.poll(&mut self.network); + } } } } @@ -873,6 +938,18 @@ impl SyncManager { envelope, seen_timestamp, ), + SyncMessage::RpcExecutionProof { + sync_request_id, + peer_id, + execution_proof, + } => self.rpc_execution_proof_received(sync_request_id, peer_id, execution_proof), + SyncMessage::RpcExecutionProofStatus { + peer_id, + request_id, + status, + } => self + .proof_sync + .on_peer_execution_proof_status(peer_id, request_id, status), SyncMessage::UnknownParentBlock(peer_id, block, block_root) => { let block_slot = block.slot(); let parent_root = block.parent_root(); @@ -884,82 +961,24 @@ impl SyncManager { block_slot, BlockComponent::Block(DownloadResult { value: block.block_cloned(), - block_root, - seen_timestamp: self.chain.slot_clock.now_duration().unwrap_or_default(), - peer_group: PeerGroup::from_single(peer_id), - }), - ); - } - SyncMessage::UnknownParentBlob(peer_id, blob) => { - let blob_slot = blob.slot(); - let block_root = blob.block_root(); - let parent_root = blob.block_parent_root(); - debug!(%block_root, %parent_root, "Received unknown parent blob message"); - self.handle_unknown_parent( - peer_id, - block_root, - parent_root, - blob_slot, - BlockComponent::Blob(DownloadResult { - value: parent_root, - block_root, seen_timestamp: self.chain.slot_clock.now_duration().unwrap_or_default(), peer_group: PeerGroup::from_single(peer_id), }), ); } - SyncMessage::UnknownParentDataColumn(peer_id, data_column) => { - let data_column_slot = data_column.slot(); - let block_root = data_column.block_root(); - match data_column.as_ref() { - DataColumnSidecar::Fulu(column) => { - let parent_root = column.block_parent_root(); - debug!(%block_root, %parent_root, "Received unknown parent data column message"); - self.handle_unknown_parent( - peer_id, - block_root, - parent_root, - data_column_slot, - BlockComponent::DataColumn(DownloadResult { - value: parent_root, - block_root, - seen_timestamp: self - .chain - .slot_clock - .now_duration() - .unwrap_or_default(), - peer_group: PeerGroup::from_single(peer_id), - }), - ); - } - DataColumnSidecar::Gloas(_) => { - // TODO(gloas): proper lookup sync for Gloas. Routing into - // `handle_unknown_block_root` here mixes column processing with the - // single-block-lookup path; the Gloas column-arrives-before-block - // case wants its own queue/wakeup. - debug!(%block_root, "Received unknown block data column message"); - self.handle_unknown_block_root(peer_id, block_root); - } - } - } - SyncMessage::UnknownParentPartialDataColumn { + SyncMessage::UnknownParentSidecarHeader { peer_id, block_root, parent_root, slot, } => { - debug!(%block_root, %parent_root, "Received unknown parent partial column message"); + debug!(%block_root, %parent_root, "Received unknown parent sidecar header message"); self.handle_unknown_parent( peer_id, block_root, parent_root, slot, - BlockComponent::PartialDataColumn(DownloadResult { - value: parent_root, - block_root, - seen_timestamp: self.chain.slot_clock.now_duration().unwrap_or_default(), - peer_group: PeerGroup::from_single(peer_id), - }), + BlockComponent::Sidecar, ); } SyncMessage::UnknownBlockHashFromAttestation(peer_id, block_root) => { @@ -1049,6 +1068,7 @@ impl SyncManager { if self.block_lookups.search_child_and_parent( block_root, block_component, + parent_root, peer_id, &mut self.network, ) { @@ -1199,14 +1219,13 @@ impl SyncManager { block: RpcEvent>>, ) { if let Some(resp) = self.network.on_single_block_response(id, peer_id, block) { - self.block_lookups - .on_download_response::>( - id, - resp.map(|(value, seen_timestamp)| { - (value, PeerGroup::from_single(peer_id), seen_timestamp) - }), - &mut self.network, - ) + self.block_lookups.on_block_download_response( + id, + resp.map(|(value, seen_timestamp)| { + DownloadResult::new(value, PeerGroup::from_single(peer_id), seen_timestamp) + }), + &mut self.network, + ) } } @@ -1218,11 +1237,6 @@ impl SyncManager { seen_timestamp: Duration, ) { match sync_request_id { - SyncRequestId::SingleBlob { id } => self.on_single_blob_response( - id, - peer_id, - RpcEvent::from_chunk(blob, seen_timestamp), - ), SyncRequestId::BlobsByRange(id) => self.on_blobs_by_range_response( id, peer_id, @@ -1283,6 +1297,36 @@ impl SyncManager { } } + fn rpc_execution_proof_received( + &mut self, + sync_request_id: SyncRequestId, + peer_id: PeerId, + execution_proof: Option>, + ) { + let Some(proof) = execution_proof else { + match &sync_request_id { + SyncRequestId::ExecutionProofsByRange(id) => { + self.proof_sync.on_range_request_terminated(id); + } + SyncRequestId::ExecutionProofsByRoot(id) => { + self.proof_sync.on_root_request_terminated(id); + } + other => { + debug!(%peer_id, ?other, "Unexpected execution proof stream termination"); + } + } + return; + }; + + if let Err(error) = self + .network + .beacon_processor() + .send_rpc_execution_proof(peer_id, proof) + { + debug!(%peer_id, ?error, "Failed to send RPC execution proof to beacon processor"); + } + } + fn on_single_payload_envelope_response( &mut self, id: SingleLookupReqId, @@ -1299,24 +1343,6 @@ impl SyncManager { } } - fn on_single_blob_response( - &mut self, - id: SingleLookupReqId, - peer_id: PeerId, - blob: RpcEvent>>, - ) { - if let Some(resp) = self.network.on_single_blob_response(id, peer_id, blob) { - self.block_lookups - .on_download_response::>( - id, - resp.map(|(value, seen_timestamp)| { - (value, PeerGroup::from_single(peer_id), seen_timestamp) - }), - &mut self.network, - ) - } - } - fn on_data_columns_by_root_response( &mut self, req_id: DataColumnsByRootRequestId, @@ -1405,11 +1431,7 @@ impl SyncManager { response: CustodyByRootResult, ) { self.block_lookups - .on_download_response::>( - requester.0, - response, - &mut self.network, - ); + .on_custody_download_response(requester.0, response, &mut self.network); } /// Handles receiving a response for a range sync request that should have both blocks and @@ -1516,18 +1538,3 @@ impl SyncManager { } } } - -impl From> for BlockProcessingResult { - fn from(result: Result) -> Self { - match result { - Ok(status) => BlockProcessingResult::Ok(status), - Err(e) => BlockProcessingResult::Err(e), - } - } -} - -impl From for BlockProcessingResult { - fn from(e: BlockError) -> Self { - BlockProcessingResult::Err(e) - } -} diff --git a/beacon_node/network/src/sync/mod.rs b/beacon_node/network/src/sync/mod.rs index 054bab654c2..7c22d1027b8 100644 --- a/beacon_node/network/src/sync/mod.rs +++ b/beacon_node/network/src/sync/mod.rs @@ -9,10 +9,12 @@ mod custody_backfill_sync; pub mod manager; mod network_context; mod peer_sync_info; +mod proof_sync; mod range_data_column_batch_request; mod range_sync; #[cfg(test)] mod tests; pub use manager::{BatchProcessResult, SyncMessage}; +pub use network_context::{PeerGroup, SyncNetworkContext}; pub use range_sync::ChainId; diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 9d5ac40c0a3..f56639f8056 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -16,34 +16,41 @@ use crate::network_beacon_processor::TestBeaconChainType; use crate::service::NetworkMessage; use crate::status::ToStatusMessage; use crate::sync::batch::ByRangeRequestType; -use crate::sync::block_lookups::SingleLookupId; +use crate::sync::block_lookups::{DownloadResult, SingleLookupId}; use crate::sync::block_sidecar_coupling::CouplingError; -use crate::sync::network_context::requests::BlobsByRootSingleBlockRequest; use crate::sync::range_data_column_batch_request::RangeDataColumnBatchRequest; use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::block_verification_types::{AsBlock, RangeSyncBlock}; +use beacon_chain::eip8025::MissingExecutionProofInfo; +use beacon_chain::internal_events::InternalBeaconNodeEvent; use beacon_chain::{BeaconChain, BeaconChainTypes, BlockProcessStatus, EngineState}; use custody::CustodyRequestResult; +use execution_layer::eip8025::types::ProofTypes; use fnv::FnvHashMap; -use lighthouse_network::rpc::methods::{BlobsByRangeRequest, DataColumnsByRangeRequest}; +use lighthouse_network::rpc::methods::{ + BlobsByRangeRequest, DataColumnsByRangeRequest, ExecutionProofStatus, + ExecutionProofsByRangeRequest, ExecutionProofsByRootRequest, +}; use lighthouse_network::rpc::{BlocksByRangeRequest, GoodbyeReason, RPCError, RequestType}; pub use lighthouse_network::service::api_types::RangeRequestId; use lighthouse_network::service::api_types::{ AppRequestId, BlobsByRangeRequestId, BlocksByRangeRequestId, ComponentsByRangeRequestId, CustodyBackFillBatchRequestId, CustodyBackfillBatchId, CustodyId, CustodyRequester, DataColumnsByRangeRequestId, DataColumnsByRangeRequester, DataColumnsByRootRequestId, - DataColumnsByRootRequester, Id, SingleLookupReqId, SyncRequestId, + DataColumnsByRootRequester, ExecutionProofStatusRequestId, ExecutionProofsByRangeRequestId, + ExecutionProofsByRootRequestId, Id, SingleLookupReqId, SyncRequestId, }; -use lighthouse_network::{Client, NetworkGlobals, PeerAction, PeerId, ReportSource}; +use lighthouse_network::{Client, Eth2Enr, NetworkGlobals, PeerAction, PeerId, ReportSource}; use parking_lot::RwLock; pub use requests::LookupVerifyError; use requests::{ - ActiveRequests, BlobsByRangeRequestItems, BlobsByRootRequestItems, BlocksByRangeRequestItems, - BlocksByRootRequestItems, DataColumnsByRangeRequestItems, DataColumnsByRootRequestItems, + ActiveRequests, BlobsByRangeRequestItems, BlocksByRangeRequestItems, BlocksByRootRequestItems, + DataColumnsByRangeRequestItems, DataColumnsByRootRequestItems, PayloadEnvelopesByRootRequestItems, }; #[cfg(test)] use slot_clock::SlotClock; +use ssz_types::{RuntimeVariableList, VariableList, typenum::Unsigned}; use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; use std::fmt::Debug; @@ -53,10 +60,10 @@ use std::time::Duration; use task_executor::TaskExecutor; use tokio::sync::mpsc; use tracing::{Span, debug, debug_span, error, warn}; -use types::data::FixedBlobSidecarList; use types::{ BlobSidecar, BlockImportSource, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, ForkContext, Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, + execution::eip8025::{MaxExecutionProofsPerPayload, ProofByRootIdentifier, ProofType}, }; pub mod custody; @@ -97,7 +104,7 @@ pub type RpcResponseResult = Result<(T, Duration), RpcResponseError>; /// Duration = latest seen timestamp of all received data columns pub type CustodyByRootResult = - Result<(DataColumnSidecarList, PeerGroup, Duration), RpcResponseError>; + Result>, RpcResponseError>; #[derive(Debug)] pub enum RpcResponseError { @@ -121,6 +128,25 @@ pub enum RpcRequestSendError { pub enum NoPeerError { BlockPeer, CustodyPeer(ColumnIndex), + /// No connected peer with execution proof support advertised in its ENR. + ProofPeer, +} + +/// Age threshold for considering a cached `ExecutionProofStatus` stale enough to re-query. +pub const EXECUTION_PROOF_STATUS_REFRESH_THRESHOLD: std::time::Duration = + std::time::Duration::from_secs(300); + +/// A peer's `ExecutionProofStatus`, plus freshness and whether its anchor was verified locally. +pub struct CachedExecutionProofStatus { + pub status: ExecutionProofStatus, + pub timestamp: std::time::Instant, + pub verified: bool, +} + +impl CachedExecutionProofStatus { + pub fn needs_refresh(&self) -> bool { + !self.verified || self.timestamp.elapsed() > EXECUTION_PROOF_STATUS_REFRESH_THRESHOLD + } } #[derive(Debug, PartialEq, Eq)] @@ -178,13 +204,13 @@ impl PeerGroup { /// Sequential ID that uniquely identifies ReqResp outgoing requests pub type ReqId = u32; -pub enum LookupRequestResult { +pub enum LookupRequestResult { /// A request is sent. Sync MUST receive an event from the network in the future for either: /// completed response or failed request RequestSent(I), /// No request is sent, and no further action is necessary to consider this request completed. /// Includes a reason why this request is not needed. - NoRequestNeeded(&'static str), + NoRequestNeeded(&'static str, T), /// No request is sent, but the request is not completed. Sync MUST receive some future event /// that makes progress on the request. For example: request is processing from a different /// source (i.e. block received from gossip) and sync MUST receive an event with that processing @@ -203,8 +229,6 @@ pub struct SyncNetworkContext { /// A mapping of active BlocksByRoot requests, including both current slot and parent lookups. blocks_by_root_requests: ActiveRequests>, - /// A mapping of active BlobsByRoot requests, including both current slot and parent lookups. - blobs_by_root_requests: ActiveRequests>, /// A mapping of active PayloadEnvelopesByRoot requests payload_envelopes_by_root_requests: ActiveRequests>, @@ -241,6 +265,9 @@ pub struct SyncNetworkContext { pub chain: Arc>, fork_context: Arc, + + /// Proof types to request from peers. + proof_types: ProofTypes, } /// Small enumeration to make dealing with block and blob requests easier. @@ -284,6 +311,7 @@ impl SyncNetworkContext> { Arc::new(beacon_processor), beacon_chain, fork_context, + ProofTypes::default(), ) } } @@ -294,13 +322,13 @@ impl SyncNetworkContext { network_beacon_processor: Arc>, chain: Arc>, fork_context: Arc, + proof_types: ProofTypes, ) -> Self { SyncNetworkContext { network_send, execution_engine_state: EngineState::Online, // always assume `Online` at the start request_id: 1, blocks_by_root_requests: ActiveRequests::new("blocks_by_root"), - blobs_by_root_requests: ActiveRequests::new("blobs_by_root"), payload_envelopes_by_root_requests: ActiveRequests::new("payload_envelopes_by_root"), data_columns_by_root_requests: ActiveRequests::new("data_columns_by_root"), blocks_by_range_requests: ActiveRequests::new("blocks_by_range"), @@ -312,6 +340,7 @@ impl SyncNetworkContext { network_beacon_processor, chain, fork_context, + proof_types, } } @@ -329,7 +358,6 @@ impl SyncNetworkContext { network_send: _, request_id: _, blocks_by_root_requests, - blobs_by_root_requests, payload_envelopes_by_root_requests, data_columns_by_root_requests, blocks_by_range_requests, @@ -344,16 +372,13 @@ impl SyncNetworkContext { network_beacon_processor: _, chain: _, fork_context: _, + proof_types: _, } = self; let blocks_by_root_ids = blocks_by_root_requests .active_requests_of_peer(peer_id) .into_iter() .map(|id| SyncRequestId::SingleBlock { id: *id }); - let blobs_by_root_ids = blobs_by_root_requests - .active_requests_of_peer(peer_id) - .into_iter() - .map(|id| SyncRequestId::SingleBlob { id: *id }); let payload_envelopes_by_root_ids = payload_envelopes_by_root_requests .active_requests_of_peer(peer_id) .into_iter() @@ -375,7 +400,6 @@ impl SyncNetworkContext { .into_iter() .map(|req_id| SyncRequestId::DataColumnsByRange(*req_id)); blocks_by_root_ids - .chain(blobs_by_root_ids) .chain(payload_envelopes_by_root_ids) .chain(data_column_by_root_ids) .chain(blocks_by_range_ids) @@ -389,6 +413,168 @@ impl SyncNetworkContext { .custody_peers_for_column(column_index) } + /// Send an `ExecutionProofsByRange` request to the given proof-capable peer. + pub fn request_execution_proofs_by_range( + &mut self, + peer_id: PeerId, + start_slot: Slot, + count: u64, + ) -> Result { + let id = ExecutionProofsByRangeRequestId { id: self.next_id() }; + let proof_types = RuntimeVariableList::new( + self.configured_proof_types().collect(), + MaxExecutionProofsPerPayload::to_usize(), + ) + .map_err(|e| RpcRequestSendError::InternalError(format!("proof_types: {e:?}")))?; + + self.send_network_msg(NetworkMessage::SendRequest { + peer_id, + request: RequestType::ExecutionProofsByRange(ExecutionProofsByRangeRequest { + start_slot: start_slot.as_u64(), + count, + proof_types, + }), + app_request_id: AppRequestId::Sync(SyncRequestId::ExecutionProofsByRange(id)), + }) + .map_err(|e| RpcRequestSendError::InternalError(e.to_owned()))?; + self.chain + .emit_internal_event(InternalBeaconNodeEvent::OutboundExecutionProofsByRange { + start_slot, + count, + }); + + debug!( + method = "ExecutionProofsByRange", + %start_slot, + count, + peer = %peer_id, + %id, + "Sync RPC request sent" + ); + Ok(id) + } + + /// Send an `ExecutionProofsByRoot` request for all missing proofs to `peer_id`. + pub fn request_execution_proofs_by_root( + &mut self, + peer_id: PeerId, + missing: &[MissingExecutionProofInfo], + ) -> Result { + let mut identifiers = Vec::with_capacity(missing.len()); + for info in missing { + let needed = self + .configured_proof_types() + .filter(|proof_type| !info.existing_proof_types.contains(proof_type)) + .collect::>(); + let proof_types = VariableList::new(needed) + .map_err(|e| RpcRequestSendError::InternalError(format!("proof_types: {e:?}")))?; + identifiers.push(ProofByRootIdentifier { + block_root: info.root, + proof_types, + }); + } + + let max_request_blocks = self + .chain + .spec + .max_request_blocks(self.fork_context.current_fork_name()); + let request = ExecutionProofsByRootRequest::new(identifiers, max_request_blocks) + .map_err(RpcRequestSendError::InternalError)?; + let event_identifiers = request.identifiers.to_vec(); + let id = ExecutionProofsByRootRequestId { id: self.next_id() }; + + self.send_network_msg(NetworkMessage::SendRequest { + peer_id, + request: RequestType::ExecutionProofsByRoot(request), + app_request_id: AppRequestId::Sync(SyncRequestId::ExecutionProofsByRoot(id)), + }) + .map_err(|e| RpcRequestSendError::InternalError(e.to_owned()))?; + self.chain + .emit_internal_event(InternalBeaconNodeEvent::OutboundExecutionProofsByRoot { + identifiers: event_identifiers, + }); + + debug!( + method = "ExecutionProofsByRoot", + num_roots = missing.len(), + peer = %peer_id, + %id, + "Sync RPC request sent" + ); + Ok(id) + } + + /// Send an `ExecutionProofStatus` request to `peer_id`. + pub fn request_execution_proof_status( + &mut self, + peer_id: PeerId, + ) -> Result { + let id = ExecutionProofStatusRequestId { id: self.next_id() }; + let local_status = self.local_execution_proof_status(); + self.send_network_msg(NetworkMessage::SendRequest { + peer_id, + request: RequestType::ExecutionProofStatus(local_status), + app_request_id: AppRequestId::Sync(SyncRequestId::ExecutionProofStatus(id)), + }) + .map_err(|e| RpcRequestSendError::InternalError(e.to_owned()))?; + + debug!( + method = "ExecutionProofStatus", + peer = %peer_id, + %id, + "Sync RPC request sent" + ); + Ok(id) + } + + pub fn local_execution_proof_status(&self) -> ExecutionProofStatus { + let head = self.chain.canonical_head.cached_head(); + let configured_proof_types = self.configured_proof_types_vec(); + let (block_root, slot, mut available_proof_types) = self + .chain + .latest_execution_proof_status(&configured_proof_types) + .map(|status| { + let proof_types = status + .valid_proof_types() + .filter(|proof_type| configured_proof_types.contains(proof_type)) + .collect::>(); + (status.block_root, status.slot.as_u64(), proof_types) + }) + .unwrap_or_else(|| (head.head_block_root(), head.head_slot().as_u64(), vec![])); + available_proof_types.sort_unstable(); + + let proof_types = VariableList::new(available_proof_types).unwrap_or_else(|error| { + debug!(?error, "Local execution proof types exceed status limit"); + VariableList::default() + }); + ExecutionProofStatus { + block_root, + slot, + proof_types, + } + } + + pub fn configured_proof_types(&self) -> impl Iterator + '_ { + self.proof_types.iter().map(|proof_type| proof_type.to_u8()) + } + + pub fn configured_proof_types_vec(&self) -> Vec { + self.configured_proof_types().collect() + } + + /// Returns `true` if the peer has execution proof support in its ENR. + pub fn is_proof_capable_peer(&self, peer_id: &PeerId) -> bool { + self.network_globals() + .peers + .read() + .peer_info(peer_id) + .is_some_and(|info| { + info.enr() + .map(|enr| enr.execution_proof_enabled()) + .unwrap_or(false) + }) + } + pub fn network_globals(&self) -> &NetworkGlobals { &self.network_beacon_processor.network_globals } @@ -432,7 +618,6 @@ impl SyncNetworkContext { network_send: _, request_id: _, blocks_by_root_requests, - blobs_by_root_requests, payload_envelopes_by_root_requests, data_columns_by_root_requests, blocks_by_range_requests, @@ -447,6 +632,7 @@ impl SyncNetworkContext { network_beacon_processor: _, chain: _, fork_context: _, + proof_types: _, // Don't use a fallback match. We want to be sure that all requests are considered when // adding new ones } = self; @@ -455,7 +641,6 @@ impl SyncNetworkContext { for peer_id in blocks_by_root_requests .iter_request_peers() - .chain(blobs_by_root_requests.iter_request_peers()) .chain(payload_envelopes_by_root_requests.iter_request_peers()) .chain(data_columns_by_root_requests.iter_request_peers()) .chain(blocks_by_range_requests.iter_request_peers()) @@ -833,7 +1018,7 @@ impl SyncNetworkContext { lookup_id: SingleLookupId, lookup_peers: Arc>>, block_root: Hash256, - ) -> Result { + ) -> Result>>, RpcRequestSendError> { let active_request_count_by_peer = self.active_request_count_by_peer(); let Some(peer_id) = lookup_peers .read() @@ -884,9 +1069,10 @@ impl SyncNetworkContext { }, // Block is fully validated. If it's not yet imported it's waiting for missing block // components. Consider this request completed and do nothing. - BlockProcessStatus::ExecutionValidated { .. } => { + BlockProcessStatus::ExecutionValidated(block) => { return Ok(LookupRequestResult::NoRequestNeeded( "block execution validated", + block, )); } } @@ -950,12 +1136,13 @@ impl SyncNetworkContext { lookup_id: SingleLookupId, lookup_peers: Arc>>, block_root: Hash256, - ) -> Result { + ) -> Result, RpcRequestSendError> { // Skip the download if fork-choice already saw this envelope (e.g. imported via gossip // before the lookup got here). if self.chain.envelope_is_known_to_fork_choice(&block_root) { return Ok(LookupRequestResult::NoRequestNeeded( "envelope already known to fork-choice", + (), )); } @@ -1017,109 +1204,6 @@ impl SyncNetworkContext { Ok(LookupRequestResult::RequestSent(id.req_id)) } - - /// Request necessary blobs for `block_root`. Requests only the necessary blobs by checking: - /// - If we have a downloaded but not yet processed block - /// - If the da_checker has a pending block - /// - If the da_checker has pending blobs from gossip - /// - /// Returns false if no request was made, because we don't need to import (more) blobs. - pub fn blob_lookup_request( - &mut self, - lookup_id: SingleLookupId, - lookup_peers: Arc>>, - block_root: Hash256, - expected_blobs: usize, - ) -> Result { - let active_request_count_by_peer = self.active_request_count_by_peer(); - let Some(peer_id) = lookup_peers - .read() - .iter() - .map(|peer| { - ( - // Prefer peers with less overall requests - active_request_count_by_peer.get(peer).copied().unwrap_or(0), - // Random factor to break ties, otherwise the PeerID breaks ties - rand::random::(), - peer, - ) - }) - .min() - .map(|(_, _, peer)| *peer) - else { - // Allow lookup to not have any peers and do nothing. This is an optimization to not - // lose progress of lookups created from a block with unknown parent before we receive - // attestations for said block. - // Lookup sync event safety: If a lookup requires peers to make progress, and does - // not receive any new peers for some time it will be dropped. If it receives a new - // peer it must attempt to make progress. - return Ok(LookupRequestResult::Pending("no peers")); - }; - - let imported_blob_indexes = self - .chain - .data_availability_checker - .cached_blob_indexes(&block_root) - .unwrap_or_default(); - // Include only the blob indexes not yet imported (received through gossip) - let indices = (0..expected_blobs as u64) - .filter(|index| !imported_blob_indexes.contains(index)) - .collect::>(); - - if indices.is_empty() { - // No blobs required, do not issue any request - return Ok(LookupRequestResult::NoRequestNeeded("no indices to fetch")); - } - - let id = SingleLookupReqId { - lookup_id, - req_id: self.next_id(), - }; - - let request = BlobsByRootSingleBlockRequest { - block_root, - indices: indices.clone(), - }; - - // Lookup sync event safety: Refer to `Self::block_lookup_request` `network_send.send` call - let network_request = RequestType::BlobsByRoot( - request - .clone() - .into_request(&self.fork_context) - .map_err(RpcRequestSendError::InternalError)?, - ); - self.network_send - .send(NetworkMessage::SendRequest { - peer_id, - request: network_request, - app_request_id: AppRequestId::Sync(SyncRequestId::SingleBlob { id }), - }) - .map_err(|_| RpcRequestSendError::InternalError("network send error".to_owned()))?; - - debug!( - method = "BlobsByRoot", - ?block_root, - blob_indices = ?indices, - peer = %peer_id, - %id, - "Sync RPC request sent" - ); - - self.blobs_by_root_requests.insert( - id, - peer_id, - // true = enforce max_requests are returned for blobs_by_root. We only issue requests for - // blocks after we know the block has data, and only request peers after they claim to - // have imported the block+blobs. - true, - BlobsByRootRequestItems::new(request), - // Not implemented - Span::none(), - ); - - Ok(LookupRequestResult::RequestSent(id.req_id)) - } - /// Request to send a single `data_columns_by_root` request to the network. pub fn data_column_lookup_request( &mut self, @@ -1127,7 +1211,7 @@ impl SyncNetworkContext { peer_id: PeerId, request: DataColumnsByRootSingleBlockRequest, expect_max_responses: bool, - ) -> Result, &'static str> { + ) -> Result, &'static str> { let id = DataColumnsByRootRequestId { id: self.next_id(), requester, @@ -1174,34 +1258,18 @@ impl SyncNetworkContext { &mut self, lookup_id: SingleLookupId, block_root: Hash256, + block_slot: Slot, lookup_peers: Arc>>, - ) -> Result { - let slot = self - .chain - .canonical_head - .fork_choice_read_lock() - .get_block(&block_root) - .map(|block| block.slot) - .or_else(|| self.chain.slot().ok()) - .ok_or_else(|| { - RpcRequestSendError::InternalError(format!( - "Unable to determine slot for block {block_root:?}" - )) - })?; - + ) -> Result>, RpcRequestSendError> { let custody_indexes_imported = self .chain - .cached_data_column_indexes(&block_root, slot) + .cached_data_column_indexes(&block_root, block_slot) .unwrap_or_default(); - let current_epoch = self.chain.epoch().map_err(|e| { - RpcRequestSendError::InternalError(format!("Unable to read slot clock {:?}", e)) - })?; - // Include only the blob indexes not yet imported (received through gossip) let mut custody_indexes_to_fetch = self .chain - .sampling_columns_for_epoch(current_epoch) + .sampling_columns_for_epoch(block_slot.epoch(T::EthSpec::slots_per_epoch())) .iter() .copied() .filter(|index| !custody_indexes_imported.contains(index)) @@ -1210,7 +1278,10 @@ impl SyncNetworkContext { if custody_indexes_to_fetch.is_empty() { // No indexes required, do not issue any request - return Ok(LookupRequestResult::NoRequestNeeded("no indices to fetch")); + return Ok(LookupRequestResult::NoRequestNeeded( + "no indices to fetch", + vec![], + )); } let id = SingleLookupReqId { @@ -1538,35 +1609,6 @@ impl SyncNetworkContext { self.on_rpc_response_result(resp, peer_id) } - pub(crate) fn on_single_blob_response( - &mut self, - id: SingleLookupReqId, - peer_id: PeerId, - rpc_event: RpcEvent>>, - ) -> Option>> { - let resp = self.blobs_by_root_requests.on_response(id, rpc_event); - let resp = resp.map(|res| { - res.and_then(|(blobs, seen_timestamp)| { - if let Some(max_len) = blobs - .first() - .map(|blob| self.chain.spec.max_blobs_per_block(blob.epoch()) as usize) - { - match to_fixed_blob_sidecar_list(blobs, max_len) { - Ok(blobs) => Ok((blobs, seen_timestamp)), - Err(e) => Err(e.into()), - } - } else { - Err(RpcResponseError::VerifyError( - LookupVerifyError::InternalError( - "Requested blobs for a block that has no blobs".to_string(), - ), - )) - } - }) - }); - self.on_rpc_response_result(resp, peer_id) - } - pub(crate) fn on_single_payload_envelope_response( &mut self, id: SingleLookupReqId, @@ -1689,8 +1731,8 @@ impl SyncNetworkContext { // Convert a result from internal format of `ActiveCustodyRequest` (error first to use ?) to // an Option first to use in an `if let Some() { act on result }` block. match result.as_ref() { - Some(Ok((columns, peer_group, _))) => { - debug!(?id, count = columns.len(), peers = ?peer_group, "Custody request success, removing") + Some(Ok(data)) => { + debug!(?id, count = data.value.len(), peers = ?data.peer_group, "Custody request success, removing") } Some(Err(e)) => { debug!(?id, error = ?e, "Custody request failure, removing" ) @@ -1734,36 +1776,6 @@ impl SyncNetworkContext { }) } - pub fn send_blobs_for_processing( - &self, - id: Id, - block_root: Hash256, - blobs: FixedBlobSidecarList, - seen_timestamp: Duration, - ) -> Result<(), SendErrorProcessor> { - let beacon_processor = self - .beacon_processor_if_enabled() - .ok_or(SendErrorProcessor::ProcessorNotAvailable)?; - - debug!(?block_root, ?id, "Sending blobs for processing"); - // Lookup sync event safety: If `beacon_processor.send_rpc_blobs` returns Ok() sync - // must receive a single `SyncMessage::BlockComponentProcessed` event with this process type - beacon_processor - .send_rpc_blobs( - block_root, - blobs, - seen_timestamp, - BlockProcessType::SingleBlob { id }, - ) - .map_err(|e| { - error!( - error = ?e, - "Failed to send sync blobs to processor" - ); - SendErrorProcessor::SendError - }) - } - #[allow(dead_code)] pub fn send_payload_for_processing( &self, @@ -1930,7 +1942,6 @@ impl SyncNetworkContext { pub(crate) fn register_metrics(&self) { for (id, count) in [ ("blocks_by_root", self.blocks_by_root_requests.len()), - ("blobs_by_root", self.blobs_by_root_requests.len()), ( "data_columns_by_root", self.data_columns_by_root_requests.len(), @@ -1951,17 +1962,3 @@ impl SyncNetworkContext { } } } - -fn to_fixed_blob_sidecar_list( - blobs: Vec>>, - max_len: usize, -) -> Result, LookupVerifyError> { - let mut fixed_list = FixedBlobSidecarList::new(vec![None; max_len]); - for blob in blobs.into_iter() { - let index = blob.index as usize; - *fixed_list - .get_mut(index) - .ok_or(LookupVerifyError::UnrequestedIndex(index as u64))? = Some(blob) - } - Ok(fixed_list) -} diff --git a/beacon_node/network/src/sync/network_context/custody.rs b/beacon_node/network/src/sync/network_context/custody.rs index 2b96800e37a..e74b74ec08e 100644 --- a/beacon_node/network/src/sync/network_context/custody.rs +++ b/beacon_node/network/src/sync/network_context/custody.rs @@ -1,3 +1,4 @@ +use crate::sync::block_lookups::DownloadResult; use crate::sync::network_context::{ DataColumnsByRootRequestId, DataColumnsByRootSingleBlockRequest, }; @@ -56,8 +57,7 @@ struct ActiveBatchColumnsRequest { span: Span, } -pub type CustodyRequestResult = - Result, PeerGroup, Duration)>, Error>; +pub type CustodyRequestResult = Result>>, Error>; impl ActiveCustodyRequest { pub(crate) fn new( @@ -227,7 +227,11 @@ impl ActiveCustodyRequest { .into_iter() .max() .unwrap_or_else(|| cx.chain.slot_clock.now_duration().unwrap_or_default()); - return Ok(Some((columns, peer_group, max_seen_timestamp))); + return Ok(Some(DownloadResult::new( + columns, + peer_group, + max_seen_timestamp, + ))); } let active_request_count_by_peer = cx.active_request_count_by_peer(); @@ -343,7 +347,7 @@ impl ActiveCustodyRequest { }, ); } - LookupRequestResult::NoRequestNeeded(_) => unreachable!(), + LookupRequestResult::NoRequestNeeded(..) => unreachable!(), LookupRequestResult::Pending(_) => unreachable!(), } } diff --git a/beacon_node/network/src/sync/network_context/requests.rs b/beacon_node/network/src/sync/network_context/requests.rs index 8c091eca807..72dd2c22d09 100644 --- a/beacon_node/network/src/sync/network_context/requests.rs +++ b/beacon_node/network/src/sync/network_context/requests.rs @@ -9,7 +9,6 @@ use tracing::{Span, debug}; use types::{Hash256, Slot}; pub use blobs_by_range::BlobsByRangeRequestItems; -pub use blobs_by_root::{BlobsByRootRequestItems, BlobsByRootSingleBlockRequest}; pub use blocks_by_range::BlocksByRangeRequestItems; pub use blocks_by_root::{BlocksByRootRequestItems, BlocksByRootSingleRequest}; pub use data_columns_by_range::DataColumnsByRangeRequestItems; @@ -25,7 +24,6 @@ use crate::metrics; use super::{RpcEvent, RpcResponseError, RpcResponseResult}; mod blobs_by_range; -mod blobs_by_root; mod blocks_by_range; mod blocks_by_root; mod data_columns_by_range; diff --git a/beacon_node/network/src/sync/network_context/requests/blobs_by_range.rs b/beacon_node/network/src/sync/network_context/requests/blobs_by_range.rs index 9c6f516199c..1da0fb52f74 100644 --- a/beacon_node/network/src/sync/network_context/requests/blobs_by_range.rs +++ b/beacon_node/network/src/sync/network_context/requests/blobs_by_range.rs @@ -33,6 +33,7 @@ impl ActiveRequestItems for BlobsByRangeRequestItems { if blob.index >= self.max_blobs_per_block { return Err(LookupVerifyError::UnrequestedIndex(blob.index)); } + if !blob.verify_blob_sidecar_inclusion_proof() { return Err(LookupVerifyError::InvalidInclusionProof); } diff --git a/beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs b/beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs deleted file mode 100644 index 556985c2b48..00000000000 --- a/beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs +++ /dev/null @@ -1,71 +0,0 @@ -use lighthouse_network::rpc::methods::BlobsByRootRequest; -use std::sync::Arc; -use types::{BlobSidecar, EthSpec, ForkContext, Hash256, data::BlobIdentifier}; - -use super::{ActiveRequestItems, LookupVerifyError}; - -#[derive(Debug, Clone)] -pub struct BlobsByRootSingleBlockRequest { - pub block_root: Hash256, - pub indices: Vec, -} - -impl BlobsByRootSingleBlockRequest { - pub fn into_request(self, spec: &ForkContext) -> Result { - BlobsByRootRequest::new( - self.indices - .into_iter() - .map(|index| BlobIdentifier { - block_root: self.block_root, - index, - }) - .collect(), - spec, - ) - } -} - -pub struct BlobsByRootRequestItems { - request: BlobsByRootSingleBlockRequest, - items: Vec>>, -} - -impl BlobsByRootRequestItems { - pub fn new(request: BlobsByRootSingleBlockRequest) -> Self { - Self { - request, - items: vec![], - } - } -} - -impl ActiveRequestItems for BlobsByRootRequestItems { - type Item = Arc>; - - /// Appends a chunk to this multi-item request. If all expected chunks are received, this - /// method returns `Some`, resolving the request before the stream terminator. - /// The active request SHOULD be dropped after `add_response` returns an error - fn add(&mut self, blob: Self::Item) -> Result { - let block_root = blob.block_root(); - if self.request.block_root != block_root { - return Err(LookupVerifyError::UnrequestedBlockRoot(block_root)); - } - if !blob.verify_blob_sidecar_inclusion_proof() { - return Err(LookupVerifyError::InvalidInclusionProof); - } - if !self.request.indices.contains(&blob.index) { - return Err(LookupVerifyError::UnrequestedIndex(blob.index)); - } - if self.items.iter().any(|b| b.index == blob.index) { - return Err(LookupVerifyError::DuplicatedData(blob.slot(), blob.index)); - } - - self.items.push(blob); - - Ok(self.items.len() >= self.request.indices.len()) - } - - fn consume(&mut self) -> Vec { - std::mem::take(&mut self.items) - } -} diff --git a/beacon_node/network/src/sync/proof_sync.rs b/beacon_node/network/src/sync/proof_sync.rs new file mode 100644 index 00000000000..a0cbc676440 --- /dev/null +++ b/beacon_node/network/src/sync/proof_sync.rs @@ -0,0 +1,452 @@ +//! Catch-up mechanism for optional EIP-8025 execution proofs. + +use super::network_context::{CachedExecutionProofStatus, SyncNetworkContext}; +use beacon_chain::{BeaconChain, BeaconChainTypes, WhenSlotSkipped}; +use lighthouse_network::PeerId; +use lighthouse_network::rpc::methods::ExecutionProofStatus; +use lighthouse_network::service::api_types::{ + ExecutionProofStatusRequestId, ExecutionProofsByRangeRequestId, ExecutionProofsByRootRequestId, +}; +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; +use std::time::Instant; +use tracing::{debug, info}; +use types::{EthSpec, ProofType, Slot}; + +use beacon_chain::eip8025::MissingExecutionProofInfo; + +pub(crate) struct ByRangeRequest { + pub(crate) id: ExecutionProofsByRangeRequestId, + pub(crate) peer_id: PeerId, +} + +pub(crate) struct ByRootRequest { + pub(crate) id: ExecutionProofsByRootRequestId, + pub(crate) peer_id: PeerId, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ProofSyncState { + Idle, + Syncing, +} + +const POST_REQUEST_COOLDOWN_SLOTS: u64 = 1; + +pub struct ProofSync { + chain: Arc>, + state: ProofSyncState, + range_request: Option, + root_request: Option, + post_request_cooldown: u64, + peer_statuses: HashMap, + status_in_flight: HashMap, + logged_no_peer: bool, +} + +impl ProofSync { + pub fn new(chain: Arc>) -> Self { + Self { + chain, + state: ProofSyncState::Idle, + range_request: None, + root_request: None, + post_request_cooldown: 0, + peer_statuses: HashMap::default(), + status_in_flight: HashMap::default(), + logged_no_peer: false, + } + } + + pub fn start(&mut self, cx: &mut SyncNetworkContext) { + if self.state == ProofSyncState::Syncing { + return; + } + info!("Proof sync starting"); + self.post_request_cooldown = 0; + self.refresh_peer_statuses(cx); + self.state = ProofSyncState::Syncing; + } + + pub fn pause(&mut self) { + if self.state == ProofSyncState::Idle { + return; + } + debug!("Proof sync pausing"); + self.state = ProofSyncState::Idle; + } + + pub fn poll(&mut self, cx: &mut SyncNetworkContext) { + if self.state == ProofSyncState::Idle { + return; + } + + if self.post_request_cooldown > 0 { + self.post_request_cooldown = self.post_request_cooldown.saturating_sub(1); + return; + } + + if self.range_request.is_some() || self.root_request.is_some() { + return; + } + + let configured_proof_types = cx.configured_proof_types_vec(); + let missing = self.chain.missing_execution_proofs(&configured_proof_types); + if missing.is_empty() { + return; + } + + let needed_types: HashSet = missing + .iter() + .flat_map(|info| { + configured_proof_types + .iter() + .copied() + .filter(|proof_type| !info.existing_proof_types.contains(proof_type)) + }) + .collect(); + if needed_types.is_empty() { + return; + } + + let Some((peer_id, peer_slot)) = self.best_peer(cx, &needed_types) else { + return; + }; + + let finalized_slot = finalized_request_start_slot(&self.chain); + let missing = + servable_missing_proofs(missing, peer_slot, finalized_slot, &configured_proof_types); + + if missing.is_empty() { + return; + } + + let range_bytes = by_range_request_size(configured_proof_types.len()); + let root_bytes = by_root_request_size(&missing, configured_proof_types.len()); + let start_slot = missing[0].slot; + let Some(count) = missing + .last() + .and_then(|last| last.slot.as_u64().checked_sub(start_slot.as_u64())) + .and_then(|delta| delta.checked_add(1)) + else { + return; + }; + let dense_enough = (count as usize) <= missing.len().saturating_mul(2); + + if dense_enough && range_bytes < root_bytes { + match cx.request_execution_proofs_by_range(peer_id, start_slot, count) { + Ok(id) => { + debug!( + %start_slot, + count, + range_bytes, + root_bytes, + "Proof sync range request sent" + ); + self.range_request = Some(ByRangeRequest { id, peer_id }); + } + Err(error) => { + debug!(?error, "Proof sync range request failed"); + } + } + return; + } + + match cx.request_execution_proofs_by_root(peer_id, &missing) { + Ok(id) => { + debug!( + num_roots = missing.len(), + root_bytes, range_bytes, "Proof sync by-root request sent" + ); + self.root_request = Some(ByRootRequest { id, peer_id }); + } + Err(error) => { + debug!(?error, "Proof sync by-root request failed"); + } + } + } + + pub fn on_range_request_terminated(&mut self, id: &ExecutionProofsByRangeRequestId) { + if self.range_request.as_ref().map(|request| &request.id) == Some(id) { + self.range_request = None; + self.post_request_cooldown = POST_REQUEST_COOLDOWN_SLOTS; + } + } + + pub fn on_root_request_terminated(&mut self, id: &ExecutionProofsByRootRequestId) { + if self.root_request.as_ref().map(|request| &request.id) == Some(id) { + self.root_request = None; + self.post_request_cooldown = POST_REQUEST_COOLDOWN_SLOTS; + } + } + + pub fn on_range_request_error(&mut self, id: &ExecutionProofsByRangeRequestId) { + if self.range_request.as_ref().map(|request| &request.id) == Some(id) { + self.range_request = None; + } + } + + pub fn on_root_request_error(&mut self, id: &ExecutionProofsByRootRequestId) { + if self.root_request.as_ref().map(|request| &request.id) == Some(id) { + self.root_request = None; + } + } + + pub fn add_peer(&mut self, peer_id: PeerId, cx: &mut SyncNetworkContext) { + match cx.request_execution_proof_status(peer_id) { + Ok(id) => { + self.status_in_flight.insert(peer_id, id); + } + Err(error) => { + debug!(?error, %peer_id, "Proof sync status request failed"); + } + } + } + + pub fn on_proof_capable_peer_disconnected(&mut self, peer_id: &PeerId) { + self.peer_statuses.remove(peer_id); + self.status_in_flight.remove(peer_id); + if self + .range_request + .as_ref() + .is_some_and(|request| &request.peer_id == peer_id) + { + self.range_request = None; + } + if self + .root_request + .as_ref() + .is_some_and(|request| &request.peer_id == peer_id) + { + self.root_request = None; + } + } + + pub fn on_peer_execution_proof_status( + &mut self, + peer_id: PeerId, + _request_id: Option, + status: ExecutionProofStatus, + ) { + let best_slot = self.chain.best_slot(); + let verified = if status.slot <= best_slot.as_u64() { + match self + .chain + .block_root_at_slot(Slot::new(status.slot), WhenSlotSkipped::None) + { + Ok(Some(root)) if root == status.block_root => true, + _ => { + debug!( + %peer_id, + slot = status.slot, + claimed_root = %status.block_root, + "Ignoring mismatched execution proof status" + ); + self.on_peer_status_failed(peer_id); + return; + } + } + } else { + false + }; + + self.status_in_flight.remove(&peer_id); + self.peer_statuses.insert( + peer_id, + CachedExecutionProofStatus { + status, + timestamp: Instant::now(), + verified, + }, + ); + } + + pub fn on_peer_execution_proof_status_error( + &mut self, + peer_id: PeerId, + _request_id: ExecutionProofStatusRequestId, + ) { + self.on_peer_status_failed(peer_id); + } + + fn on_peer_status_failed(&mut self, peer_id: PeerId) { + self.status_in_flight.remove(&peer_id); + self.peer_statuses + .entry(peer_id) + .and_modify(|entry| entry.timestamp = Instant::now()) + .or_insert_with(|| CachedExecutionProofStatus { + status: ExecutionProofStatus::default(), + timestamp: Instant::now(), + verified: false, + }); + } + + fn refresh_peer_statuses(&mut self, cx: &mut SyncNetworkContext) { + for (peer_id, status) in self.peer_statuses.iter() { + if status.needs_refresh() && !self.status_in_flight.contains_key(peer_id) { + match cx.request_execution_proof_status(*peer_id) { + Ok(id) => { + self.status_in_flight.insert(*peer_id, id); + } + Err(error) => { + debug!(?error, %peer_id, "Proof sync status refresh failed"); + } + } + } + } + } + + fn best_peer( + &mut self, + cx: &mut SyncNetworkContext, + needed_types: &HashSet, + ) -> Option<(PeerId, Slot)> { + self.refresh_peer_statuses(cx); + + let result = self + .peer_statuses + .iter() + .filter(|(_, cached)| { + cached + .status + .proof_types + .iter() + .any(|proof_type| needed_types.contains(proof_type)) + }) + .max_by_key(|(_, cached)| { + let supported_needed_types = cached + .status + .proof_types + .iter() + .filter(|proof_type| needed_types.contains(proof_type)) + .count(); + (cached.verified, supported_needed_types, cached.status.slot) + }) + .map(|(peer_id, cached)| (*peer_id, Slot::new(cached.status.slot))); + + match result { + None if !self.logged_no_peer => { + debug!("Proof sync has no proof-capable peer"); + self.logged_no_peer = true; + } + Some(_) => { + self.logged_no_peer = false; + } + _ => {} + } + + result + } +} + +fn finalized_request_start_slot(chain: &BeaconChain) -> Slot { + chain + .canonical_head + .cached_head() + .finalized_checkpoint() + .epoch + .start_slot(T::EthSpec::slots_per_epoch()) +} + +fn servable_missing_proofs( + missing: Vec, + peer_slot: Slot, + finalized_slot: Slot, + configured_proof_types: &[ProofType], +) -> Vec { + let mut missing = missing + .into_iter() + .filter(|info| { + if info.slot < finalized_slot { + debug!( + block_root = %info.root, + slot = %info.slot, + %finalized_slot, + "Proof sync skipping missing proof before finalized request window" + ); + false + } else if peer_slot < info.slot { + debug!( + block_root = %info.root, + slot = %info.slot, + %peer_slot, + "Proof sync peer is behind missing proof block" + ); + false + } else { + configured_proof_types + .iter() + .any(|proof_type| !info.existing_proof_types.contains(proof_type)) + } + }) + .collect::>(); + missing.sort_unstable_by_key(|info| info.slot); + missing +} + +fn per_identifier_ssz_bytes( + info: &MissingExecutionProofInfo, + num_configured_types: usize, +) -> usize { + let needed = num_configured_types.saturating_sub(info.existing_proof_types.len()); + 4 + 32 + 4 + needed +} + +fn by_root_request_size( + missing: &[MissingExecutionProofInfo], + num_configured_types: usize, +) -> usize { + missing + .iter() + .map(|info| per_identifier_ssz_bytes(info, num_configured_types)) + .sum() +} + +fn by_range_request_size(num_configured_types: usize) -> usize { + 20 + num_configured_types +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + use types::Hash256; + + fn missing_at( + slot: u64, + existing_proof_types: impl IntoIterator, + ) -> MissingExecutionProofInfo { + MissingExecutionProofInfo { + root: Hash256::with_last_byte((slot & 0xff) as u8), + slot: Slot::new(slot), + existing_proof_types: existing_proof_types.into_iter().collect::>(), + } + } + + #[test] + fn servable_missing_proofs_starts_at_finalized_slot() { + let configured_proof_types = vec![0, 1]; + let missing = vec![ + missing_at(7, []), + missing_at(8, []), + missing_at(9, [0, 1]), + missing_at(10, [0]), + missing_at(11, []), + ]; + + let servable = servable_missing_proofs( + missing, + Slot::new(10), + Slot::new(8), + &configured_proof_types, + ); + + assert_eq!( + servable + .iter() + .map(|info| info.slot.as_u64()) + .collect::>(), + vec![8, 10], + ); + } +} diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index c1b2793491f..5642f7846a6 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -1,20 +1,20 @@ use super::*; use crate::NetworkMessage; +use crate::network_beacon_processor::BlockProcessingResult; +use crate::network_beacon_processor::sync_methods::WhichPeerToPenalize; use crate::network_beacon_processor::{ ChainSegmentProcessId, InvalidBlockStorage, NetworkBeaconProcessor, }; use crate::sync::block_lookups::{BlockLookupSummary, PARENT_DEPTH_TOLERANCE}; use crate::sync::{ SyncMessage, - manager::{BatchProcessResult, BlockProcessType, BlockProcessingResult, SyncManager}, + manager::{BatchProcessResult, BlockProcessType, SyncManager}, }; -use beacon_chain::blob_verification::KzgVerifiedBlob; use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::custody_context::NodeCustodyType; use beacon_chain::{ - AvailabilityProcessingStatus, BlockError, EngineState, NotifyExecutionLayer, + AvailabilityProcessingStatus, EngineState, NotifyExecutionLayer, block_verification_types::{AsBlock, AvailableBlockData}, - data_availability_checker::Availability, test_utils::{ AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, NumBlobs, generate_rand_block_and_blobs, test_spec, @@ -31,13 +31,14 @@ use lighthouse_network::{ types::SyncState, }; use slot_clock::{SlotClock, TestingSlotClock}; +use std::collections::HashSet; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; use tracing::info; use types::{ - BlobSidecar, BlockImportSource, ColumnIndex, DataColumnSidecar, EthSpec, ForkContext, ForkName, - Hash256, MinimalEthSpec as E, SignedBeaconBlock, Slot, + BlobSidecar, BlockImportSource, ColumnIndex, DataColumnSidecar, DataColumnSubnetId, + ForkContext, ForkName, Hash256, MinimalEthSpec as E, SignedBeaconBlock, Slot, }; const D: Duration = Duration::new(0, 0); @@ -549,52 +550,6 @@ impl TestRig { self.send_rpc_blocks_response(req_id, peer_id, &blocks); } - (RequestType::BlobsByRoot(req), AppRequestId::Sync(req_id)) => { - if self.complete_strategy.return_no_data_n_times > 0 { - self.complete_strategy.return_no_data_n_times -= 1; - return self.send_rpc_blobs_response(req_id, peer_id, &[]); - } - - let mut blobs = req - .blob_ids - .iter() - .map(|id| { - self.network_blocks_by_root - .get(&id.block_root) - .unwrap_or_else(|| { - panic!("Test consumer requested unknown block: {id:?}") - }) - .block_data() - .blobs() - .unwrap_or_else(|| panic!("Block {id:?} has no blobs")) - .iter() - .find(|blob| blob.index == id.index) - .unwrap_or_else(|| panic!("Blob id {id:?} not avail")) - .clone() - }) - .collect::>(); - - if self.complete_strategy.return_too_few_data_n_times > 0 { - self.complete_strategy.return_too_few_data_n_times -= 1; - blobs.pop(); - } - - if self - .complete_strategy - .return_wrong_sidecar_for_block_n_times - > 0 - { - self.complete_strategy - .return_wrong_sidecar_for_block_n_times -= 1; - let first = blobs.first_mut().expect("empty blobs"); - let mut blob = Arc::make_mut(first).clone(); - blob.signed_block_header.message.body_root = Hash256::ZERO; - *first = Arc::new(blob); - } - - self.send_rpc_blobs_response(req_id, peer_id, &blobs); - } - (RequestType::DataColumnsByRoot(req), AppRequestId::Sync(req_id)) => { if self.complete_strategy.return_no_data_n_times > 0 { self.complete_strategy.return_no_data_n_times -= 1; @@ -1006,48 +961,6 @@ impl TestRig { keypair.sk.sign(msg) } - fn corrupt_last_blob_proposer_signature(&mut self) { - let range_sync_block = self.get_last_block().clone(); - let block = range_sync_block.block_cloned(); - let mut blobs = range_sync_block - .block_data() - .blobs() - .expect("no blobs") - .into_iter() - .collect::>(); - let columns = range_sync_block.block_data().data_columns(); - let first = blobs.first_mut().expect("empty blobs"); - Arc::make_mut(first).signed_block_header.signature = self.valid_signature(); - let max_blobs = - self.harness - .spec - .max_blobs_per_block(block.slot().epoch(E::slots_per_epoch())) as usize; - let blobs = - types::BlobSidecarList::new(blobs, max_blobs).expect("invalid blob sidecar list"); - self.re_insert_block(block, Some(blobs), columns); - } - - fn corrupt_last_blob_kzg_proof(&mut self) { - let range_sync_block = self.get_last_block().clone(); - let block = range_sync_block.block_cloned(); - let mut blobs = range_sync_block - .block_data() - .blobs() - .expect("no blobs") - .into_iter() - .collect::>(); - let columns = range_sync_block.block_data().data_columns(); - let first = blobs.first_mut().expect("empty blobs"); - Arc::make_mut(first).kzg_proof = kzg::KzgProof::empty(); - let max_blobs = - self.harness - .spec - .max_blobs_per_block(block.slot().epoch(E::slots_per_epoch())) as usize; - let blobs = - types::BlobSidecarList::new(blobs, max_blobs).expect("invalid blob sidecar list"); - self.re_insert_block(block, Some(blobs), columns); - } - fn corrupt_last_column_proposer_signature(&mut self) { let range_sync_block = self.get_last_block().clone(); let block = range_sync_block.block_cloned(); @@ -1205,17 +1118,6 @@ impl TestRig { self.trigger_unknown_parent_block(peer_id, last_block); } - fn trigger_with_last_unknown_blob_parent(&mut self) { - let peer_id = self.new_connected_supernode_peer(); - let blobs = self - .get_last_block() - .block_data() - .blobs() - .expect("no blobs"); - let blob = blobs.first().expect("empty blobs"); - self.trigger_unknown_parent_blob(peer_id, blob.clone()); - } - fn trigger_with_last_unknown_data_column_parent(&mut self) { let peer_id = self.new_connected_supernode_peer(); let columns = self @@ -1224,7 +1126,7 @@ impl TestRig { .data_columns() .expect("No data columns"); let column = columns.first().expect("empty columns"); - self.trigger_unknown_parent_column(peer_id, column.clone()); + self.trigger_unknown_parent_data_column(peer_id, column.clone()); } // Post-test assertions @@ -1424,8 +1326,8 @@ impl TestRig { // Test setup - fn new_after_deneb() -> Option { - genesis_fork().deneb_enabled().then(Self::default) + fn new_after_fulu() -> Option { + genesis_fork().fulu_enabled().then(Self::default) } fn new_after_deneb_before_fulu() -> Option { @@ -1450,10 +1352,6 @@ impl TestRig { info!(msg, "TEST_RIG"); } - pub fn is_after_deneb(&self) -> bool { - self.fork_name.deneb_enabled() - } - pub fn is_after_fulu(&self) -> bool { self.fork_name.fulu_enabled() } @@ -1463,16 +1361,23 @@ impl TestRig { self.send_sync_message(SyncMessage::UnknownParentBlock(peer_id, block, block_root)) } - fn trigger_unknown_parent_blob(&mut self, peer_id: PeerId, blob: Arc>) { - self.send_sync_message(SyncMessage::UnknownParentBlob(peer_id, blob)); - } - - fn trigger_unknown_parent_column( + fn trigger_unknown_parent_data_column( &mut self, peer_id: PeerId, - column: Arc>, + data_column: Arc>, ) { - self.send_sync_message(SyncMessage::UnknownParentDataColumn(peer_id, column)); + let block_root = data_column.block_root(); + let slot = data_column.slot(); + let parent_root = match data_column.as_ref() { + DataColumnSidecar::Fulu(column) => column.block_parent_root(), + DataColumnSidecar::Gloas(_) => panic!("Gloas data column not supported in this test"), + }; + self.send_sync_message(SyncMessage::UnknownParentSidecarHeader { + peer_id, + block_root, + parent_root, + slot, + }); } fn trigger_unknown_block_from_attestation(&mut self, block_root: Hash256, peer_id: PeerId) { @@ -1550,7 +1455,7 @@ impl TestRig { .network_globals .peers .write() - .__add_connected_peer_testing_only(false, &self.harness.spec, key); + .__add_connected_peer_with_custody_subnets(false, &self.harness.spec, key); // Assumes custody subnet count == column count let custody_subnets = self @@ -1581,13 +1486,38 @@ impl TestRig { .network_globals .peers .write() - .__add_connected_peer_testing_only(true, &self.harness.spec, key); + .__add_connected_peer_with_custody_subnets(true, &self.harness.spec, key); self.log(&format!( "Added new peer for testing {peer_id:?}, custody: supernode" )); peer_id } + /// Add a connected supernode peer, but without setting the peers' custody subnet. + /// This is to simulate the real behaviour where metadata is only received some time after + /// a connection is established. + pub fn new_connected_supernode_peer_no_metadata_custody_subnet(&mut self) -> PeerId { + let key = self.determinstic_key(); + self.network_globals + .peers + .write() + .__add_connected_peer(true, key, &self.harness.spec) + } + + /// Update the peer's custody subnet in PeerDB and send a `UpdatedPeerCgc` message to sync. + pub fn send_peer_cgc_update_to_sync( + &mut self, + peer_id: &PeerId, + subnets: HashSet, + ) { + self.network_globals + .peers + .write() + .__set_custody_subnets(peer_id, subnets) + .unwrap(); + self.send_sync_message(SyncMessage::UpdatedPeerCgc(*peer_id)) + } + fn determinstic_key(&mut self) -> CombinedKey { k256::ecdsa::SigningKey::random(&mut self.rng_08).into() } @@ -1743,27 +1673,6 @@ impl TestRig { } } - fn insert_blob_to_da_checker(&mut self, blob: Arc>) { - match self - .harness - .chain - .data_availability_checker - .put_kzg_verified_blobs( - blob.block_root(), - std::iter::once( - KzgVerifiedBlob::new(blob, &self.harness.chain.kzg, Duration::new(0, 0)) - .expect("Invalid blob"), - ), - ) - .unwrap() - { - Availability::Available(_) => panic!("blob removed from da_checker, available"), - Availability::MissingComponents(block_root) => { - self.log(&format!("inserted blob to da_checker {block_root:?}")) - } - }; - } - fn insert_block_to_da_checker_as_pre_execution(&mut self, block: Arc>) { self.log(&format!( "Inserting block to availability_cache as pre_execution_block {:?}", @@ -1930,49 +1839,39 @@ async fn happy_path_unknown_block_parent(depth: usize) { r.build_chain(depth).await; r.trigger_with_last_unknown_block_parent(); r.simulate(SimulateConfig::happy_path()).await; - // All lookups should NOT complete on this test, however note the following for the tip lookup, - // it's the lookup for the tip block which has 0 peers and a block cached: + // Note the following for the tip lookup, it's the lookup for the tip block which has 0 peers + // and a block cached: // - before deneb the block is cached, so it's sent for processing, and success - // - before fulu the block is cached, but we can't fetch blobs so it's stuck + // - deneb/electra the block is cached, so it's sent for processing, and success // - after fulu the block is cached, we start a custody request and since we use the global pool // of peers we DO have 1 connected synced supernode peer, which gives us the columns and the // lookup succeeds - if r.is_after_deneb() && !r.is_after_fulu() { - r.assert_successful_lookup_sync_parent_trigger() - } else { - r.assert_successful_lookup_sync(); - } + r.assert_successful_lookup_sync(); } -/// Assert that sync completes from a GossipUnknownParentBlob / UnknownDataColumnParent +/// Assert that sync completes from an UnknownDataColumnParent async fn happy_path_unknown_data_parent(depth: usize) { - let Some(mut r) = TestRig::new_after_deneb() else { + let Some(mut r) = TestRig::new_after_fulu() else { return; }; r.build_chain(depth).await; - if r.is_after_fulu() { - r.trigger_with_last_unknown_data_column_parent(); - } else if r.is_after_deneb() { - r.trigger_with_last_unknown_blob_parent(); - } + r.trigger_with_last_unknown_data_column_parent(); r.simulate(SimulateConfig::happy_path()).await; r.assert_successful_lookup_sync_parent_trigger(); } /// Assert that multiple trigger types don't create extra lookups async fn happy_path_multiple_triggers(depth: usize) { - let mut r = TestRig::default(); + let Some(mut r) = TestRig::new_after_fulu() else { + return; + }; // + 1, because the unknown parent trigger needs two new blocks r.build_chain(depth + 1).await; r.trigger_with_last_block(); r.trigger_with_last_block(); r.trigger_with_last_unknown_block_parent(); r.trigger_with_last_unknown_block_parent(); - if r.is_after_fulu() { - r.trigger_with_last_unknown_data_column_parent(); - } else if r.is_after_deneb() { - r.trigger_with_last_unknown_blob_parent(); - } + r.trigger_with_last_unknown_data_column_parent(); r.simulate(SimulateConfig::happy_path()).await; assert_eq!(r.created_lookups(), depth + 1, "Don't create extra lookups"); r.assert_successful_lookup_sync(); @@ -1995,9 +1894,9 @@ async fn bad_peer_empty_block_response(depth: usize) { // TODO(tree-sync) Assert that a single lookup is created (no drops) } -/// Assert that if peer responds with no blobs / columns, we downscore, and retry the same lookup +/// Assert that if peer responds with no columns, we downscore, and retry the same lookup. async fn bad_peer_empty_data_response(depth: usize) { - let Some(mut r) = TestRig::new_after_deneb() else { + let Some(mut r) = TestRig::new_after_fulu() else { return; }; r.build_chain_and_trigger_last_block(depth).await; @@ -2009,10 +1908,10 @@ async fn bad_peer_empty_data_response(depth: usize) { // TODO(tree-sync) Assert that a single lookup is created (no drops) } -/// Assert that if peer responds with not enough blobs / columns, we downscore, and retry the same -/// lookup +/// Assert that if peer responds with not enough columns, we downscore, and retry the same +/// lookup. async fn bad_peer_too_few_data_response(depth: usize) { - let Some(mut r) = TestRig::new_after_deneb() else { + let Some(mut r) = TestRig::new_after_fulu() else { return; }; r.build_chain_and_trigger_last_block(depth).await; @@ -2036,9 +1935,9 @@ async fn bad_peer_wrong_block_response(depth: usize) { // TODO(tree-sync) Assert that a single lookup is created (no drops) } -/// Assert that if peer responds with bad blobs / columns, we downscore, and retry the same lookup +/// Assert that if peer responds with bad columns, we downscore, and retry the same lookup. async fn bad_peer_wrong_data_response(depth: usize) { - let Some(mut r) = TestRig::new_after_deneb() else { + let Some(mut r) = TestRig::new_after_fulu() else { return; }; r.build_chain_and_trigger_last_block(depth).await; @@ -2087,7 +1986,14 @@ async fn too_many_processing_failures(depth: usize) { r.build_chain_and_trigger_last_block(depth).await; // Simulate that a peer always returns empty r.simulate( - SimulateConfig::new().with_process_result(|| BlockError::BlockSlotLimitReached.into()), + SimulateConfig::new().with_process_result(|| BlockProcessingResult::Error { + penalty: Some(( + PeerAction::MidToleranceError, + WhichPeerToPenalize::BlockPeer, + "lookup_block_processing_failure", + )), + reason: "lookup_block_processing_failure".to_string(), + }), ) .await; // We register multiple penalties, the lookup fails and sync does not progress @@ -2105,18 +2011,14 @@ async fn too_many_processing_failures(depth: usize) { #[tokio::test] /// Assert that multiple trigger types don't create extra lookups async fn unknown_parent_does_not_add_peers_to_itself() { - let Some(mut r) = TestRig::new_after_deneb() else { + let Some(mut r) = TestRig::new_after_fulu() else { return; }; // 2, because the unknown parent trigger needs two new blocks r.build_chain(2).await; r.trigger_with_last_unknown_block_parent(); r.trigger_with_last_unknown_block_parent(); - if r.is_after_fulu() { - r.trigger_with_last_unknown_data_column_parent(); - } else if r.is_after_deneb() { - r.trigger_with_last_unknown_blob_parent(); - } + r.trigger_with_last_unknown_data_column_parent(); r.simulate(SimulateConfig::happy_path()).await; r.assert_peers_at_lookup_of_slot(2, 0); r.assert_peers_at_lookup_of_slot(1, 3); @@ -2135,15 +2037,21 @@ async fn unknown_parent_does_not_add_peers_to_itself() { } #[tokio::test] -/// Assert that if the beacon processor returns Ignored, the lookup is dropped +/// Assert that a non-attributable processing error (e.g. processor overloaded) is retried up to +/// `SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS`, no peer is penalized, and the lookup is then dropped. async fn test_single_block_lookup_ignored_response() { let mut r = TestRig::default(); r.build_chain_and_trigger_last_block(1).await; - // Send an Ignored response, the request should be dropped - r.simulate(SimulateConfig::new().with_process_result(|| BlockProcessingResult::Ignored)) - .await; + r.simulate( + SimulateConfig::new().with_process_result(|| BlockProcessingResult::Error { + penalty: None, + reason: "processor_overloaded".to_string(), + }), + ) + .await; // The block was not actually imported r.assert_head_slot(0); + r.assert_no_penalties(); assert_eq!(r.created_lookups(), 1, "no created lookups"); assert_eq!(r.dropped_lookups(), 1, "no dropped lookups"); assert_eq!(r.completed_lookups(), 0, "some completed lookups"); @@ -2157,7 +2065,7 @@ async fn test_single_block_lookup_duplicate_response() { // Send a DuplicateFullyImported response, the lookup should complete successfully r.simulate( SimulateConfig::new() - .with_process_result(|| BlockError::DuplicateFullyImported(Hash256::ZERO).into()), + .with_process_result(|| BlockProcessingResult::Imported(true, "duplicate")), ) .await; // The block was not actually imported @@ -2363,8 +2271,8 @@ async fn test_same_chain_race_condition() { #[tokio::test] /// Assert that if the lookup's block is in the da_checker we don't download it again async fn block_in_da_checker_skips_download() { - // Only in Deneb, as the block needs blobs to remain in the da_checker - let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { + // Only post-Fulu, as the block needs custody columns to remain in the da_checker + let Some(mut r) = TestRig::new_after_fulu() else { return; }; // Add block to da_checker @@ -2428,32 +2336,6 @@ async fn block_in_processing_cache_becomes_valid_imported() { r.assert_no_active_lookups(); } -// IGNORE: wait for change that delays blob fetching to knowing the block -#[tokio::test] -async fn blobs_in_da_checker_skip_download() { - let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { - return; - }; - r.build_chain(1).await; - let block = r.get_last_block().clone(); - let blobs = block.block_data().blobs().expect("block with no blobs"); - for blob in &blobs { - r.insert_blob_to_da_checker(blob.clone()); - } - r.trigger_with_last_block(); - r.simulate(SimulateConfig::happy_path()).await; - - r.assert_successful_lookup_sync(); - assert_eq!( - r.requests - .iter() - .filter(|(request, _)| matches!(request, RequestType::BlobsByRoot(_))) - .collect::>(), - Vec::<&(RequestType, AppRequestId)>::new(), - "There should be no blob requests" - ); -} - macro_rules! fulu_peer_matrix_tests { ( [$($name:ident => $variant:expr),+ $(,)?] @@ -2562,43 +2444,7 @@ async fn crypto_on_fail_with_invalid_block_signature() { r.assert_no_penalties(); } else { r.assert_failed_lookup_sync(); - r.assert_penalties_of_type("lookup_block_processing_failure"); - } -} - -#[tokio::test] -async fn crypto_on_fail_with_bad_blob_proposer_signature() { - let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { - return; - }; - r.build_chain(1).await; - r.corrupt_last_blob_proposer_signature(); - r.trigger_with_last_block(); - r.simulate(SimulateConfig::happy_path()).await; - if cfg!(feature = "fake_crypto") { - r.assert_successful_lookup_sync(); - r.assert_no_penalties(); - } else { - r.assert_failed_lookup_sync(); - r.assert_penalties_of_type("lookup_blobs_processing_failure"); - } -} - -#[tokio::test] -async fn crypto_on_fail_with_bad_blob_kzg_proof() { - let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { - return; - }; - r.build_chain(1).await; - r.corrupt_last_blob_kzg_proof(); - r.trigger_with_last_block(); - r.simulate(SimulateConfig::happy_path()).await; - if cfg!(feature = "fake_crypto") { - r.assert_successful_lookup_sync(); - r.assert_no_penalties(); - } else { - r.assert_failed_lookup_sync(); - r.assert_penalties_of_type("lookup_blobs_processing_failure"); + r.assert_penalties_of_type("InvalidSignature"); } } @@ -2616,7 +2462,7 @@ async fn crypto_on_fail_with_bad_column_proposer_signature() { r.assert_no_penalties(); } else { r.assert_failed_lookup_sync(); - r.assert_penalties_of_type("lookup_custody_column_processing_failure"); + r.assert_penalties_of_type("InvalidSignature"); } } @@ -2634,6 +2480,6 @@ async fn crypto_on_fail_with_bad_column_kzg_proof() { r.assert_no_penalties(); } else { r.assert_failed_lookup_sync(); - r.assert_penalties_of_type("lookup_custody_column_processing_failure"); + r.assert_penalties_of_type("AvailabilityCheck"); } } diff --git a/beacon_node/network/src/sync/tests/range.rs b/beacon_node/network/src/sync/tests/range.rs index 891d9d1e978..1499ae5016e 100644 --- a/beacon_node/network/src/sync/tests/range.rs +++ b/beacon_node/network/src/sync/tests/range.rs @@ -27,6 +27,7 @@ use crate::sync::range_sync::RangeSyncType; use lighthouse_network::rpc::RPCError; use lighthouse_network::rpc::methods::StatusMessageV2; use lighthouse_network::{PeerId, SyncInfo}; +use std::collections::HashSet; use types::{Epoch, EthSpec, Hash256, MinimalEthSpec as E, Slot}; /// MinimalEthSpec has 8 slots per epoch @@ -50,7 +51,7 @@ impl TestRig { finalized_root: Hash256::random(), head_slot: finalized_epoch.start_slot(E::slots_per_epoch()), head_root: Hash256::random(), - earliest_available_slot: None, + earliest_available_slot: Some(Slot::new(0)), } } @@ -476,3 +477,76 @@ async fn not_enough_custody_peers_then_peers_arrive() { r.simulate(SimulateConfig::happy_path()).await; r.assert_range_sync_completed(); } + +/// This is a regression test for the following race condition scenario: +/// 1. A node is connected to 3 supernode peers: peer 1 is synced, & peer 2 and 3 are advanced. +/// 2. No metadata has been received yet (i.e. no custody info), so the node cannot start data +/// column range sync yet. +/// 3. Now peer 1 sends the CGC via metadata response, we now have one peer on all custody subnets, +/// BUT not on the finalized syncing chain. +/// 4. The node tries to `send_batch` but fails repeatedly with `NoPeers`, as there's no peer +/// that is able to serve columns for the advanced epochs. The chain is removed after 5 failed attempts. +/// 5. Now peer 2 & 3 send CGC updates, BUT because there's no syncing chain, nothing happens - +/// sync is stuck until finding new peers. +/// +/// The expected behaviour in this scenario should be: +/// 4. not finding suitable peers, chain is kept and batch remains in AwaitingDownload +/// 5. finalized sync should resume as soon as CGC updates are received from peer 2 or 3. +#[tokio::test] +async fn finalized_sync_not_enough_custody_peers_resume_after_peer_cgc_update() { + let mut r = TestRig::default(); + if !r.fork_name.fulu_enabled() { + return; + } + + // GIVEN: the node is connected to 3 supernode peers: + let advanced_epochs: usize = 2; + let sync_epochs = advanced_epochs + 3; + let sync_slots = sync_epochs * SLOTS_PER_EPOCH - 1; + r.build_chain(sync_slots).await; + r.harness.set_current_slot(Slot::new(sync_slots as u64 + 1)); + + // Peer 1 is synced (same finalized epoch), but its earliest available slot means it + // cannot serve the batches needed for this sync. + let peer_1 = r.new_connected_supernode_peer_no_metadata_custody_subnet(); + let mut remote_info = r.local_info().clone(); + remote_info.earliest_available_slot = Some(Slot::new(sync_slots as u64)); + r.send_sync_message(SyncMessage::AddPeer(peer_1, remote_info)); + + // Peer 2 is advanced (local finalized epoch + 2) + let peer_2 = r.new_connected_supernode_peer_no_metadata_custody_subnet(); + let remote_info = r.finalized_remote_info_advanced_by((advanced_epochs as u64).into()); + r.send_sync_message(SyncMessage::AddPeer(peer_2, remote_info.clone())); + // We expect a finalized chain to be created with peer 2, but no requests sent out yet due to missing custody info. + r.assert_state(RangeSyncType::Finalized); + r.assert_empty_network(); + + // Peer 3 is connected and advanced + let peer_3 = r.new_connected_supernode_peer_no_metadata_custody_subnet(); + r.send_sync_message(SyncMessage::AddPeer(peer_3, remote_info)); + // We are still in finalized sync state (now with peer 3 added) + r.assert_state(RangeSyncType::Finalized); + + for (i, p) in [peer_1, peer_2, peer_3].iter().enumerate() { + let peer_idx = i + 1; + r.log(&format!("Peer {peer_idx}: {p:?}")); + } + + // WHEN: peer 1 sends its CGC via metadata response + let all_custody_subnets = (0..r.harness.spec.data_column_sidecar_subnet_count) + .map(|i| i.into()) + .collect::>(); + r.send_peer_cgc_update_to_sync(&peer_1, all_custody_subnets.clone()); + + // We still don't have any peers on the syncing chain with custody columns (only peer 1) + // The node won't send the batch and will remain in the finalized sync state (this was failing before!) + r.assert_state(RangeSyncType::Finalized); + r.assert_empty_network(); + + // Now we receive peer 2 & 3's CGC updates, the node will resume syncing from these two peers + r.send_peer_cgc_update_to_sync(&peer_2, all_custody_subnets.clone()); + r.send_peer_cgc_update_to_sync(&peer_3, all_custody_subnets); + + r.simulate(SimulateConfig::happy_path()).await; + r.assert_range_sync_completed(); +} diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 647b5858cb1..a18f4b5e07a 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -387,6 +387,14 @@ pub fn cli_app() -> Command { .help("Disables the quic transport. The node will rely solely on the TCP transport for libp2p connections.") .display_order(0) ) + .arg( + Arg::new("enable-mplex") + .long("enable-mplex") + .action(ArgAction::SetTrue) + .help_heading(FLAG_HEADER) + .help("Enables mplex multiplexer alongside yamux. Yamux is preferred when both are available.") + .display_order(0) + ) .arg( Arg::new("disable-peer-scoring") .long("disable-peer-scoring") @@ -919,6 +927,24 @@ pub fn cli_app() -> Command { .action(ArgAction::Set) .display_order(0) ) + .arg( + Arg::new("proof-engine-endpoint") + .long("proof-engine-endpoint") + .value_name("PROOF-ENGINE-ENDPOINT") + .help("Server endpoint for the optional EIP-8025 proof engine HTTP API.") + .requires("execution-endpoint") + .action(ArgAction::Set) + .display_order(0) + ) + .arg( + Arg::new("proof-types") + .long("proof-types") + .value_name("PROOF-TYPES") + .help("Comma-separated EIP-8025 proof types to request from the proof engine.") + .requires("proof-engine-endpoint") + .action(ArgAction::Set) + .display_order(0) + ) .arg( Arg::new("disable-get-blobs") .long("disable-get-blobs") @@ -1515,6 +1541,15 @@ pub fn cli_app() -> Command { Lighthouse and only passed to the EL if initial verification fails.") .display_order(0) ) + .arg( + Arg::new("execution-proof-quorum") + .long("execution-proof-quorum") + .value_name("K") + .help("Non-default: mark a Gloas payload envelope as proof-valid after K distinct valid EIP-8025 proof types for the same new-payload request root.") + .requires("proof-engine-endpoint") + .action(ArgAction::Set) + .display_order(0) + ) .arg( Arg::new("disable-light-client-server") .long("disable-light-client-server") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 045b432dc97..8cef79c21ed 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -359,6 +359,29 @@ pub fn get_config( clap_utils::parse_required(cli_args, "execution-timeout-multiplier")?; el_config.execution_timeout_multiplier = Some(execution_timeout_multiplier); + if let Some(endpoint) = cli_args.get_one::("proof-engine-endpoint") { + el_config.proof_engine_endpoint = Some(parse_only_one_value( + endpoint, + SensitiveUrl::parse, + "--proof-engine-endpoint", + )?); + client_config.network.enable_execution_proof = true; + } + + if let Some(proof_types) = cli_args.get_one::("proof-types") { + let proof_types = proof_types + .split(',') + .filter(|proof_type| !proof_type.trim().is_empty()) + .map(|proof_type| { + proof_type + .trim() + .parse::() + .map_err(|e| format!("Invalid --proof-types value: {e}")) + }) + .collect::, _>>()?; + el_config.proof_types = execution_layer::eip8025::types::ProofTypes::from(proof_types); + } + // Store the EL config in the client config. client_config.execution_layer = Some(el_config); @@ -829,6 +852,14 @@ pub fn get_config( client_config.chain.optimistic_finalized_sync = !cli_args.get_flag("disable-optimistic-finalized-sync"); + if let Some(quorum) = clap_utils::parse_optional::(cli_args, "execution-proof-quorum")? { + client_config.chain.execution_proof_quorum.enabled = true; + client_config + .chain + .execution_proof_quorum + .min_valid_proof_types = quorum; + } + if cli_args.get_flag("genesis-backfill") { client_config.chain.genesis_backfill = true; } @@ -1443,6 +1474,10 @@ pub fn set_network_config( config.disable_quic_support = true; } + if parse_flag(cli_args, "enable-mplex") { + config.enable_mplex = true; + } + if parse_flag(cli_args, "disable-upnp") { config.upnp_enabled = false; } diff --git a/book/src/help_bn.md b/book/src/help_bn.md index 30163f1f0cd..ea924ba0058 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -139,6 +139,10 @@ Options: Used by the beacon node to communicate a client version to execution nodes during JWT authentication. It corresponds to the 'clv' field in the JWT claims object.Set to empty by default + --execution-proof-quorum + Non-default: mark a Gloas payload envelope as proof-valid after K + distinct valid EIP-8025 proof types for the same new-payload request + root. --execution-timeout-multiplier Unsigned integer to multiply the default execution timeouts by. [default: 1] @@ -305,6 +309,10 @@ Options: which don't improve their payload after the first call, and high values are useful for ensuring the EL is given ample notice. Default: 1/3 of a slot. + --proof-engine-endpoint + Server endpoint for the optional EIP-8025 proof engine HTTP API. + --proof-types + Comma-separated EIP-8025 proof types to request from the proof engine. --proposer-reorg-cutoff DEPRECATED. This flag has no effect. --proposer-reorg-disallowed-offsets @@ -494,6 +502,9 @@ Flags: Sets the local ENR IP address and port to match those set for lighthouse. Specifically, the IP address will be the value of --listen-address and the UDP port will be --discovery-port. + --enable-mplex + Enables mplex multiplexer alongside yamux. Yamux is preferred when + both are available. --enable-partial-columns Enable partial messages for data columns. This can reduce the amount of data sent over the network. Enabled by default on Hoodi and diff --git a/book/src/help_vc.md b/book/src/help_vc.md index 4647780ea8c..443b04b4ce2 100644 --- a/book/src/help_vc.md +++ b/book/src/help_vc.md @@ -115,6 +115,15 @@ Options: --network Name of the Eth2 chain Lighthouse will sync and follow. [possible values: mainnet, gnosis, chiado, sepolia, holesky, hoodi] + --proof-engine-endpoint + URL of the proof engine HTTP JSON-RPC endpoint for EIP-8025 execution + proofs. When set, the validator client will monitor new blocks, + request proofs from this endpoint, sign completed proofs, and submit + them to the beacon node. + --proof-types + Comma-separated list of proof type identifiers (u8) to request from + the proof engine (e.g., 0,1,2). If not specified, defaults to + '0,1,2,3' (EthrexRisc0, EthrexSP1, EthrexZisk, RethOpenVM). --proposer-nodes Comma-separated addresses to one or more beacon node HTTP APIs. These specify nodes that are used to send beacon block proposals. A failure diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index e9fb44209ba..a376a82c2ba 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -46,7 +46,10 @@ use ssz::{Decode, Encode}; use std::fmt; use std::future::Future; use std::time::Duration; -use types::{PayloadAttestationData, PayloadAttestationMessage, SignedProposerPreferences}; +use types::{ + PayloadAttestationData, PayloadAttestationMessage, SignedExecutionPayloadBid, + SignedExecutionProof, SignedProposerPreferences, +}; pub const V1: EndpointVersion = EndpointVersion(1); pub const V2: EndpointVersion = EndpointVersion(2); @@ -1832,6 +1835,30 @@ impl BeaconNodeHttpClient { Ok(()) } + /// `POST beacon/pool/execution_proofs` + pub async fn post_beacon_pool_execution_proofs( + &self, + proofs: &[SignedExecutionProof], + ) -> Result<(), Error> { + #[derive(Serialize)] + struct SubmitExecutionProofsRequest<'a> { + proofs: &'a [SignedExecutionProof], + } + + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("pool") + .push("execution_proofs"); + + self.post(path, &SubmitExecutionProofsRequest { proofs }) + .await?; + + Ok(()) + } + /// `POST beacon/pool/bls_to_execution_changes` pub async fn post_beacon_pool_bls_to_execution_changes( &self, @@ -2838,6 +2865,54 @@ impl BeaconNodeHttpClient { Ok(()) } + /// `POST v1/beacon/execution_payload_bid` + pub async fn post_beacon_execution_payload_bid( + &self, + bid: &SignedExecutionPayloadBid, + fork_name: ForkName, + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("execution_payload_bid"); + + self.post_generic_with_consensus_version( + path, + bid, + Some(self.timeouts.proposal), + fork_name, + ) + .await?; + + Ok(()) + } + + /// `POST v1/beacon/execution_payload_bid` in SSZ format + pub async fn post_beacon_execution_payload_bid_ssz( + &self, + bid: &SignedExecutionPayloadBid, + fork_name: ForkName, + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("execution_payload_bid"); + + self.post_generic_with_consensus_version_and_ssz_body( + path, + bid.as_ssz_bytes(), + Some(self.timeouts.proposal), + fork_name, + ) + .await?; + + Ok(()) + } + /// Path for `v1/beacon/execution_payload_envelope/{block_id}` pub fn get_beacon_execution_payload_envelope_path( &self, diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 2de8ce7d817..edced9b2467 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -207,6 +207,18 @@ pub enum InvalidPayloadAttestation { }, } +/// The import status of a block's parent, as seen by fork choice. +#[allow(clippy::large_enum_variant)] +pub enum ParentImportStatus { + /// The parent block is imported and the child's bid commits to a parent payload known to fork + /// choice. + Imported(ProtoBlock), + /// The parent block is not known to fork choice. + UnknownBlock, + /// The parent block is known, but the child's bid commits to a payload not known to fork choice. + UnknownPayload, +} + impl From for Error { fn from(e: String) -> Self { Error::ProtoArrayStringError(e) @@ -1537,6 +1549,37 @@ where && self.is_finalized_checkpoint_or_descendant(*block_root) } + /// Returns `true` if the block's parent is imported (and, for a post-Gloas FULL child, its + /// parent's payload is imported too). See [`Self::get_parent_import_status`]. + pub fn is_parent_imported(&self, block: &SignedBeaconBlock) -> bool { + matches!( + self.get_parent_import_status(block), + ParentImportStatus::Imported(_) + ) + } + + /// Returns the import status of the parent of `block`. + /// + /// A post-Gloas FULL child also requires the parent's payload (committed to by the child's bid) + /// to have been received by fork choice. + pub fn get_parent_import_status(&self, block: &SignedBeaconBlock) -> ParentImportStatus { + if let Some(parent_block) = self.get_block(&block.parent_root()) { + let Some(parent_block_hash) = parent_block.execution_payload_block_hash else { + // Pre-Gloas parent: payload is embedded in the block, so treat as imported. + return ParentImportStatus::Imported(parent_block); + }; + if block.is_parent_block_full(parent_block_hash) + && !self.is_payload_received(&block.parent_root()) + { + ParentImportStatus::UnknownPayload + } else { + ParentImportStatus::Imported(parent_block) + } + } else { + ParentImportStatus::UnknownBlock + } + } + /// Called by the proposer to decide whether to build on the full or empty parent. pub fn should_build_on_full( &self, diff --git a/consensus/fork_choice/src/lib.rs b/consensus/fork_choice/src/lib.rs index 159eab0ec05..dcc499547b5 100644 --- a/consensus/fork_choice/src/lib.rs +++ b/consensus/fork_choice/src/lib.rs @@ -4,9 +4,9 @@ mod metrics; pub use crate::fork_choice::{ AttestationFromBlock, Error, ForkChoice, ForkChoiceView, ForkchoiceUpdateParameters, - InvalidAttestation, InvalidBlock, InvalidPayloadAttestation, PayloadVerificationStatus, - PersistedForkChoice, PersistedForkChoiceV28, PersistedForkChoiceV29, QueuedAttestation, - ResetPayloadStatuses, + InvalidAttestation, InvalidBlock, InvalidPayloadAttestation, ParentImportStatus, + PayloadVerificationStatus, PersistedForkChoice, PersistedForkChoiceV28, PersistedForkChoiceV29, + QueuedAttestation, ResetPayloadStatuses, }; pub use fork_choice_store::ForkChoiceStore; pub use proto_array::{ diff --git a/consensus/proto_array/Cargo.toml b/consensus/proto_array/Cargo.toml index ee86277f9cf..c424c01f6c3 100644 --- a/consensus/proto_array/Cargo.toml +++ b/consensus/proto_array/Cargo.toml @@ -19,3 +19,11 @@ superstruct = { workspace = true } typenum = { workspace = true } types = { workspace = true } yaml_serde = { workspace = true } + +[dev-dependencies] +criterion = { workspace = true } +fixed_bytes = { workspace = true } + +[[bench]] +name = "find_head" +harness = false diff --git a/consensus/proto_array/benches/find_head.rs b/consensus/proto_array/benches/find_head.rs new file mode 100644 index 00000000000..98077a7f977 --- /dev/null +++ b/consensus/proto_array/benches/find_head.rs @@ -0,0 +1,118 @@ +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use fixed_bytes::FixedBytesExtended; +use proto_array::{Block, ExecutionStatus, JustifiedBalances, ProtoArrayForkChoice}; +use std::collections::BTreeSet; +use std::time::Duration; +use types::{ + AttestationShufflingId, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, Hash256, + MainnetEthSpec, Slot, +}; + +fn get_root(i: u64) -> Hash256 { + Hash256::from_low_u64_be(i) +} + +fn get_hash(i: u64) -> ExecutionBlockHash { + ExecutionBlockHash::from_root(get_root(i)) +} + +/// Build a linear chain of `num_blocks` blocks. +fn build_chain(num_blocks: u64, gloas: bool) -> (ProtoArrayForkChoice, types::ChainSpec) { + let mut spec = MainnetEthSpec::default_spec(); + let gloas_fork_slot = 32; + if gloas { + spec.gloas_fork_epoch = Some(Epoch::new(1)); + } + + let finalized_checkpoint = Checkpoint { + epoch: Epoch::new(0), + root: get_root(0), + }; + let junk_shuffling_id = AttestationShufflingId::from_components(Epoch::new(0), Hash256::zero()); + + let mut fork_choice = ProtoArrayForkChoice::new::( + Slot::new(0), + Slot::new(0), + Hash256::zero(), + finalized_checkpoint, + finalized_checkpoint, + junk_shuffling_id.clone(), + junk_shuffling_id.clone(), + ExecutionStatus::Optimistic(ExecutionBlockHash::zero()), + None, + None, + 0, + &spec, + ) + .expect("should create fork choice"); + + for i in 1..=num_blocks { + let is_gloas = gloas && i >= gloas_fork_slot; + let block = Block { + slot: Slot::new(i), + root: get_root(i), + parent_root: Some(get_root(i - 1)), + state_root: Hash256::zero(), + target_root: get_root(0), + current_epoch_shuffling_id: junk_shuffling_id.clone(), + next_epoch_shuffling_id: junk_shuffling_id.clone(), + justified_checkpoint: finalized_checkpoint, + finalized_checkpoint, + execution_status: ExecutionStatus::Optimistic(ExecutionBlockHash::zero()), + unrealized_justified_checkpoint: Some(finalized_checkpoint), + unrealized_finalized_checkpoint: Some(finalized_checkpoint), + execution_payload_parent_hash: if is_gloas { + Some(get_hash(i - 1)) + } else { + None + }, + execution_payload_block_hash: if is_gloas { Some(get_hash(i)) } else { None }, + proposer_index: Some(0), + }; + + fork_choice + .process_block::(block, Slot::new(i), &spec, Duration::ZERO) + .expect("should process block"); + } + + (fork_choice, spec) +} + +fn bench_find_head(c: &mut Criterion) { + let mut group = c.benchmark_group("find_head"); + let equivocating_indices = BTreeSet::new(); + let finalized_checkpoint = Checkpoint { + epoch: Epoch::new(0), + root: get_root(0), + }; + let balances = JustifiedBalances::from_effective_balances(vec![1; 64]).unwrap(); + + // 216k = ~1 month non-finality mainnet, 518k = ~1 month non-finality Gnosis. + // Must survive extended non-finality (500k+ blocks). + for (label, gloas) in [("pre_gloas", false), ("gloas", true)] { + for &num_blocks in &[100, 1_000, 10_000, 50_000, 216_000, 518_000] { + let (mut fork_choice, spec) = build_chain(num_blocks, gloas); + + group.bench_function(BenchmarkId::new(label, num_blocks), |b| { + b.iter(|| { + fork_choice + .find_head::( + finalized_checkpoint, + finalized_checkpoint, + &balances, + Hash256::zero(), + &equivocating_indices, + Slot::new(num_blocks), + &spec, + ) + .expect("should find head") + }); + }); + } + } + + group.finish(); +} + +criterion_group!(benches, bench_find_head); +criterion_main!(benches); diff --git a/consensus/proto_array/src/error.rs b/consensus/proto_array/src/error.rs index d185ed371cb..eb0f30cc87e 100644 --- a/consensus/proto_array/src/error.rs +++ b/consensus/proto_array/src/error.rs @@ -1,6 +1,6 @@ use crate::PayloadStatus; use safe_arith::ArithError; -use types::{Checkpoint, Epoch, ExecutionBlockHash, Hash256, Slot}; +use types::{Epoch, ExecutionBlockHash, Hash256}; #[derive(Clone, PartialEq, Debug)] pub enum Error { @@ -9,8 +9,6 @@ pub enum Error { NodeUnknown(Hash256), InvalidFinalizedRootChange, InvalidNodeIndex(usize), - InvalidParentIndex(usize), - InvalidBestChildIndex(usize), InvalidJustifiedIndex(usize), InvalidBestDescendant(usize), InvalidParentDelta(usize), @@ -30,7 +28,6 @@ pub enum Error { current_finalized_epoch: Epoch, new_finalized_epoch: Epoch, }, - InvalidBestNode(Box), InvalidAncestorOfValidPayload { ancestor_block_root: Hash256, ancestor_payload_block_hash: ExecutionBlockHash, @@ -74,14 +71,3 @@ impl From for Error { Error::Arith(e) } } - -#[derive(Clone, PartialEq, Debug)] -pub struct InvalidBestNodeInfo { - pub current_slot: Slot, - pub start_root: Hash256, - pub justified_checkpoint: Checkpoint, - pub finalized_checkpoint: Checkpoint, - pub head_root: Hash256, - pub head_justified_checkpoint: Checkpoint, - pub head_finalized_checkpoint: Checkpoint, -} diff --git a/consensus/proto_array/src/fork_choice_test_definition.rs b/consensus/proto_array/src/fork_choice_test_definition.rs index 43b76ec7cb7..3dc5406212c 100644 --- a/consensus/proto_array/src/fork_choice_test_definition.rs +++ b/consensus/proto_array/src/fork_choice_test_definition.rs @@ -144,7 +144,7 @@ impl ForkChoiceTestDefinition { pub fn run(self) { let spec = self.spec.unwrap_or_else(|| { let mut spec = MainnetEthSpec::default_spec(); - spec.proposer_score_boost = Some(50); + spec.proposer_score_boost = 50; // Legacy test definitions target pre-Gloas behaviour unless explicitly overridden. spec.gloas_fork_epoch = None; spec diff --git a/consensus/proto_array/src/fork_choice_test_definition/gloas_payload.rs b/consensus/proto_array/src/fork_choice_test_definition/gloas_payload.rs index ac4f8992c41..bf79a0170fc 100644 --- a/consensus/proto_array/src/fork_choice_test_definition/gloas_payload.rs +++ b/consensus/proto_array/src/fork_choice_test_definition/gloas_payload.rs @@ -2,7 +2,7 @@ use super::*; fn gloas_spec() -> ChainSpec { let mut spec = MainnetEthSpec::default_spec(); - spec.proposer_score_boost = Some(50); + spec.proposer_score_boost = 50; spec.gloas_fork_epoch = Some(Epoch::new(0)); spec } @@ -977,7 +977,7 @@ mod tests { fn gloas_fork_boundary_spec() -> ChainSpec { let mut spec = MainnetEthSpec::default_spec(); - spec.proposer_score_boost = Some(50); + spec.proposer_score_boost = 50; spec.gloas_fork_epoch = Some(Epoch::new(1)); spec } diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 6ff5eabb045..bd15bb4599e 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -1,4 +1,3 @@ -use crate::error::InvalidBestNodeInfo; use crate::proto_array_fork_choice::IndexedForkChoiceNode; use crate::{ Block, ExecutionStatus, JustifiedBalances, LatestMessage, PayloadStatus, error::Error, @@ -392,6 +391,10 @@ pub struct ProtoArray { pub prune_threshold: usize, pub nodes: Vec, pub indices: HashMap, + /// Cached parent→children index. `children[i]` holds the node indices of all children of + /// node `i`. Maintained incrementally by `on_block` and `maybe_prune`. + #[serde(skip)] + pub children: Vec>, } impl ProtoArray { @@ -674,6 +677,16 @@ impl ProtoArray { self.indices.insert(node.root(), node_index); self.nodes.push(node.clone()); + // Maintain cached children index. `parent_index` is already bounds-checked above + // against `self.nodes`, and `self.children` is kept in lockstep with `self.nodes`. + self.children.push(Vec::new()); + if let Some(parent_index) = node.parent() { + self.children + .get_mut(parent_index) + .ok_or(Error::InvalidNodeIndex(parent_index))? + .push(node_index); + } + if let Some(parent_index) = node.parent() && matches!(block.execution_status, ExecutionStatus::Valid(_)) { @@ -1093,31 +1106,25 @@ impl ProtoArray { spec, )?; - // Perform a sanity check that the node is indeed valid to be the head. - let best_node = self - .nodes - .get(best_fc_node.proto_node_index) - .ok_or(Error::InvalidNodeIndex(best_fc_node.proto_node_index))?; - if !self.node_is_viable_for_head::( - best_node, - current_slot, - best_justified_checkpoint, - best_finalized_checkpoint, - ) { - return Err(Error::InvalidBestNode(Box::new(InvalidBestNodeInfo { - current_slot, - start_root: *justified_root, - justified_checkpoint: best_justified_checkpoint, - finalized_checkpoint: best_finalized_checkpoint, - head_root: best_node.root(), - head_justified_checkpoint: *best_node.justified_checkpoint(), - head_finalized_checkpoint: *best_node.finalized_checkpoint(), - }))); - } - Ok((best_fc_node.root, best_fc_node.payload_status)) } + /// Rebuild the cached `self.children` index from `self.nodes`. Called once after + /// deserialization to populate the transient field. + pub fn rebuild_children_index(&mut self) -> Result<(), Error> { + let mut children = vec![Vec::new(); self.nodes.len()]; + for (i, node) in self.nodes.iter().enumerate() { + if let Some(parent_idx) = node.parent() { + children + .get_mut(parent_idx) + .ok_or(Error::InvalidNodeIndex(parent_idx))? + .push(i); + } + } + self.children = children; + Ok(()) + } + /// Spec: `get_filtered_block_tree`. /// /// Returns the set of node indices on viable branches — those with at least @@ -1128,7 +1135,7 @@ impl ProtoArray { current_slot: Slot, best_justified_checkpoint: Checkpoint, best_finalized_checkpoint: Checkpoint, - ) -> HashSet { + ) -> Result, Error> { let mut viable = HashSet::new(); self.filter_block_tree::( start_index, @@ -1136,71 +1143,88 @@ impl ProtoArray { best_justified_checkpoint, best_finalized_checkpoint, &mut viable, - ); - viable + )?; + Ok(viable) } /// Spec: `filter_block_tree`. + /// + /// Proto_array stores nodes in insertion order — children always have higher + /// indices than their parents. A single reverse pass therefore processes every + /// child before its parent, matching the spec's recursive post-order semantics + /// without recursion (required to survive 500k+ blocks of non-finality). + /// + /// The spec removes execution-invalid blocks (and their entire subtrees) from + /// `store.blocks` before running. We replicate that here with a forward pass + /// propagating `excluded` from parent to child — V29 children of an invalidated + /// V17 ancestor are excluded transitively, since V29 nodes carry no + /// `execution_status` of their own. fn filter_block_tree( &self, - node_index: usize, + start_index: usize, current_slot: Slot, best_justified_checkpoint: Checkpoint, best_finalized_checkpoint: Checkpoint, viable: &mut HashSet, - ) -> bool { - let Some(node) = self.nodes.get(node_index) else { - return false; - }; + ) -> Result<(), Error> { + // Forward pass: a node is "excluded" if it (or any ancestor down to + // `start_index`) has an invalid execution status. + let mut excluded = vec![false; self.nodes.len()]; + for i in (start_index + 1)..self.nodes.len() { + let node = self.nodes.get(i).ok_or(Error::InvalidNodeIndex(i))?; + let parent_excluded = match node.parent() { + Some(p) => *excluded.get(p).ok_or(Error::InvalidNodeIndex(p))?, + None => false, + }; + let self_invalid = node.execution_status().is_ok_and(|s| s.is_invalid()); + excluded[i] = parent_excluded || self_invalid; + } - // Skip invalid children — they aren't in store.blocks in the spec. - let children: Vec = self - .nodes - .iter() - .enumerate() - .filter(|(_, child)| { - child.parent() == Some(node_index) - && !child - .execution_status() - .is_ok_and(|status| status.is_invalid()) - }) - .map(|(i, _)| i) - .collect(); + for node_index in (start_index..self.nodes.len()).rev() { + // Spec: invalid subtree removed from `store.blocks` — skip entirely. + if *excluded + .get(node_index) + .ok_or(Error::InvalidNodeIndex(node_index))? + { + continue; + } + let node = self + .nodes + .get(node_index) + .ok_or(Error::InvalidNodeIndex(node_index))?; - if !children.is_empty() { - // Evaluate ALL children (no short-circuit) to mark all viable branches. - let any_viable = children + // Spec: children = [root for root in blocks if blocks[root].parent_root == block_root] + let valid_children: Vec = self + .children + .get(node_index) + .ok_or(Error::InvalidNodeIndex(node_index))? .iter() - .map(|&child_index| { - self.filter_block_tree::( - child_index, - current_slot, - best_justified_checkpoint, - best_finalized_checkpoint, - viable, - ) + .copied() + .filter_map(|i| match excluded.get(i) { + Some(false) => Some(Ok(i)), + Some(true) => None, + None => Some(Err(Error::InvalidNodeIndex(i))), }) - .collect::>() - .into_iter() - .any(|v| v); - if any_viable { - viable.insert(node_index); - return true; - } - return false; - } + .collect::>()?; - // Leaf node: check viability. - if self.node_is_viable_for_head::( - node, - current_slot, - best_justified_checkpoint, - best_finalized_checkpoint, - ) { - viable.insert(node_index); - return true; + if !valid_children.is_empty() { + // Spec: if any(children): if any(filter_block_tree_result): blocks[block_root] = block + if valid_children.iter().any(|c| viable.contains(c)) { + viable.insert(node_index); + } + } else { + // Spec: leaf — check correct_justified and correct_finalized + if self.node_is_viable_for_head::( + node, + current_slot, + best_justified_checkpoint, + best_finalized_checkpoint, + ) { + viable.insert(node_index); + } + } } - false + Ok(()) } /// Spec: `get_head`. @@ -1227,7 +1251,7 @@ impl ProtoArray { current_slot, best_justified_checkpoint, best_finalized_checkpoint, - ); + )?; // Compute once rather than per-child per-level. let apply_proposer_boost = @@ -1491,25 +1515,35 @@ impl ProtoArray { } Ok(children) } else { - Ok(self - .nodes + // Spec: [root for root in blocks.keys() if blocks[root].parent_root == node.root ...] + // (cached `self.children[i]` is the same set as the spec's filtered scan). + let indices = self + .children + .get(node.proto_node_index) + .ok_or(Error::InvalidNodeIndex(node.proto_node_index))?; + indices .iter() - .enumerate() - .filter(|(_, child_node)| { - child_node.parent() == Some(node.proto_node_index) - && child_node.get_parent_payload_status() == node.payload_status - }) - .map(|(child_index, child_node)| { - ( - IndexedForkChoiceNode { - root: child_node.root(), - proto_node_index: child_index, - payload_status: PayloadStatus::Pending, - }, - child_node.clone(), - ) + .copied() + .filter_map(|i| { + self.nodes + .get(i) + .ok_or(Error::InvalidNodeIndex(i)) + .map(|child| { + // Spec: node.payload_status == get_parent_payload_status(store, blocks[root]) + (child.get_parent_payload_status() == node.payload_status).then(|| { + ( + IndexedForkChoiceNode { + root: child.root(), + proto_node_index: i, + payload_status: PayloadStatus::Pending, + }, + child.clone(), + ) + }) + }) + .transpose() }) - .collect()) + .collect() } } @@ -1640,6 +1674,19 @@ impl ProtoArray { // Drop all the nodes prior to finalization. self.nodes = self.nodes.split_off(finalized_index); + // Drop pruned entries from children index and shift all remaining indices down. + // Invariant: child_index > parent_index, and all parents we kept have + // index >= finalized_index, so every remaining child_index is also + // >= finalized_index. + self.children = self.children.split_off(finalized_index); + for children in self.children.iter_mut() { + for child_index in children.iter_mut() { + *child_index = child_index + .checked_sub(finalized_index) + .ok_or(Error::IndexOverflow("children"))?; + } + } + // Adjust the indices map. for (_root, index) in self.indices.iter_mut() { *index = index @@ -1884,10 +1931,7 @@ fn get_proposer_score( justified_balances: &JustifiedBalances, spec: &ChainSpec, ) -> Result { - let Some(proposer_score_boost) = spec.proposer_score_boost else { - return Ok(0); - }; - calculate_committee_fraction::(justified_balances, proposer_score_boost) + calculate_committee_fraction::(justified_balances, spec.proposer_score_boost) .ok_or(Error::ProposerBoostOverflow(0)) } diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index 96d23022666..2c1195b4913 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -514,6 +514,7 @@ impl ProtoArrayForkChoice { prune_threshold: DEFAULT_PRUNE_THRESHOLD, nodes: Vec::with_capacity(1), indices: HashMap::with_capacity(1), + children: Vec::with_capacity(1), }; let block = Block { diff --git a/consensus/proto_array/src/ssz_container.rs b/consensus/proto_array/src/ssz_container.rs index 69efb35027c..ec70e88a730 100644 --- a/consensus/proto_array/src/ssz_container.rs +++ b/consensus/proto_array/src/ssz_container.rs @@ -59,11 +59,13 @@ impl TryFrom<(SszContainerV29, JustifiedBalances)> for ProtoArrayForkChoice { type Error = Error; fn try_from((from, balances): (SszContainerV29, JustifiedBalances)) -> Result { - let proto_array = ProtoArray { + let mut proto_array = ProtoArray { prune_threshold: from.prune_threshold, nodes: from.nodes, indices: from.indices.into_iter().collect::>(), + children: Vec::new(), }; + proto_array.rebuild_children_index()?; Ok(Self { proto_array, diff --git a/consensus/types/src/core/chain_spec.rs b/consensus/types/src/core/chain_spec.rs index 25dcb4ba06c..31cd6d296b9 100644 --- a/consensus/types/src/core/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -37,6 +37,7 @@ pub enum Domain { BeaconBuilder, PTCAttester, ProposerPreferences, + ExecutionProof, ApplicationMask(ApplicationDomain), } @@ -147,11 +148,12 @@ pub struct ChainSpec { pub(crate) domain_beacon_builder: u32, pub(crate) domain_ptc_attester: u32, pub(crate) domain_proposer_preferences: u32, + pub(crate) domain_execution_proof: u32, /* * Fork choice */ - pub proposer_score_boost: Option, + pub proposer_score_boost: u64, pub reorg_head_weight_threshold: u64, pub reorg_parent_weight_threshold: u64, pub reorg_max_epochs_since_finalization: u64, @@ -525,6 +527,7 @@ impl ChainSpec { Domain::BeaconBuilder => self.domain_beacon_builder, Domain::PTCAttester => self.domain_ptc_attester, Domain::ProposerPreferences => self.domain_proposer_preferences, + Domain::ExecutionProof => self.domain_execution_proof, Domain::SyncCommittee => self.domain_sync_committee, Domain::ContributionAndProof => self.domain_contribution_and_proof, Domain::SyncCommitteeSelectionProof => self.domain_sync_committee_selection_proof, @@ -1158,11 +1161,12 @@ impl ChainSpec { domain_beacon_builder: 0x0B, domain_ptc_attester: 0x0C, domain_proposer_preferences: 0x0D, + domain_execution_proof: 0x0D, /* * Fork choice */ - proposer_score_boost: Some(40), + proposer_score_boost: 40, reorg_head_weight_threshold: 20, reorg_parent_weight_threshold: 160, reorg_max_epochs_since_finalization: 2, @@ -1583,11 +1587,12 @@ impl ChainSpec { domain_beacon_builder: 0x0B, domain_ptc_attester: 0x0C, domain_proposer_preferences: 0x0D, + domain_execution_proof: 0x0D, /* * Fork choice */ - proposer_score_boost: Some(40), + proposer_score_boost: 40, reorg_head_weight_threshold: 20, reorg_parent_weight_threshold: 160, reorg_max_epochs_since_finalization: 2, @@ -2640,7 +2645,9 @@ impl Config { min_per_epoch_churn_limit: spec.min_per_epoch_churn_limit, max_per_epoch_activation_churn_limit: spec.max_per_epoch_activation_churn_limit, - proposer_score_boost: spec.proposer_score_boost.map(|value| MaybeQuoted { value }), + proposer_score_boost: Some(MaybeQuoted { + value: spec.proposer_score_boost, + }), reorg_head_weight_threshold: spec.reorg_head_weight_threshold, reorg_parent_weight_threshold: spec.reorg_parent_weight_threshold, reorg_max_epochs_since_finalization: spec.reorg_max_epochs_since_finalization, @@ -2854,7 +2861,9 @@ impl Config { min_per_epoch_churn_limit, max_per_epoch_activation_churn_limit, churn_limit_quotient, - proposer_score_boost: proposer_score_boost.map(|q| q.value), + proposer_score_boost: proposer_score_boost + .map(|q| q.value) + .unwrap_or(chain_spec.proposer_score_boost), reorg_head_weight_threshold, reorg_parent_weight_threshold, reorg_max_epochs_since_finalization, @@ -2978,6 +2987,7 @@ mod tests { test_domain(Domain::SyncCommittee, spec.domain_sync_committee, &spec); test_domain(Domain::BeaconBuilder, spec.domain_beacon_builder, &spec); test_domain(Domain::PTCAttester, spec.domain_ptc_attester, &spec); + test_domain(Domain::ExecutionProof, spec.domain_execution_proof, &spec); // The builder domain index is zero let builder_domain_pre_mask = [0; 4]; diff --git a/consensus/types/src/core/config_and_preset.rs b/consensus/types/src/core/config_and_preset.rs index 02f9867fcba..acfbd70a3a8 100644 --- a/consensus/types/src/core/config_and_preset.rs +++ b/consensus/types/src/core/config_and_preset.rs @@ -136,6 +136,7 @@ pub fn get_extra_fields(spec: &ChainSpec) -> HashMap { "domain_beacon_builder".to_uppercase() => u32_hex(spec.domain_beacon_builder), "domain_ptc_attester".to_uppercase() => u32_hex(spec.domain_ptc_attester), "domain_proposer_preferences".to_uppercase() => u32_hex(spec.domain_proposer_preferences), + "domain_execution_proof".to_uppercase() => u32_hex(spec.domain_execution_proof), "sync_committee_subnet_count".to_uppercase() => consts::altair::SYNC_COMMITTEE_SUBNET_COUNT.to_string().into(), "target_aggregators_per_sync_subcommittee".to_uppercase() => diff --git a/consensus/types/src/data/partial_data_column_sidecar.rs b/consensus/types/src/data/partial_data_column_sidecar.rs index c0e713b4b81..e70901d76ee 100644 --- a/consensus/types/src/data/partial_data_column_sidecar.rs +++ b/consensus/types/src/data/partial_data_column_sidecar.rs @@ -69,7 +69,7 @@ impl PartialDataColumnSidecar { .count(); self.column .get(storage_idx) - .and_then(|cell| self.kzg_proofs.get(storage_idx).map(|proof| (cell, proof))) + .zip(self.kzg_proofs.get(storage_idx)) } /// Creates a reference to this sidecar containing only the blob indices for which the passed diff --git a/consensus/types/src/execution/eip8025.rs b/consensus/types/src/execution/eip8025.rs new file mode 100644 index 00000000000..2ac729426c8 --- /dev/null +++ b/consensus/types/src/execution/eip8025.rs @@ -0,0 +1,354 @@ +//! EIP-8025: Optional Execution Proofs +//! +//! This module contains types for the EIP-8025 optional execution proofs feature. +//! See: https://eips.ethereum.org/EIPS/eip-8025 + +use crate::core::{Hash256, SignedRoot}; +use bls::SignatureBytes; +use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; +use ssz_types::VariableList; +use tree_hash_derive::TreeHash; + +#[cfg(feature = "arbitrary")] +use arbitrary::Arbitrary; + +/// Maximum proof size: 1344 KiB (1,376,256 bytes). +/// +/// Product of (U21 * U64) * U1024. +pub type MaxProofSize = typenum::Prod; + +/// Maximum proof size in KiB. +pub type MaxProofSizeKiB = typenum::Prod; + +/// Proof data type +/// +/// VariableList of bytes with max length [`MaxProofSize`]. +pub type ProofData = VariableList; + +/// Maximum execution proofs per payload +pub type MaxExecutionProofsPerPayload = typenum::U4; + +/// Proof type identifier +pub type ProofType = u8; + +/// List of execution proofs per payload +pub type ExecutionProofList = VariableList; + +/// Domain type for execution proof signatures (0x0D000000) +pub const DOMAIN_EXECUTION_PROOF: [u8; 4] = [0x0D, 0x00, 0x00, 0x00]; + +/// Minimum required execution proofs for payload verification +pub const MIN_REQUIRED_EXECUTION_PROOFS: usize = 1; + +/// Public input of an [`ExecutionProof`]. +/// +/// Contains the tree hash root of the new payload request that the proof is associated with. +#[derive( + Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Encode, Decode, TreeHash, +)] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] +pub struct PublicInput { + /// The tree hash root of the NewPayloadRequest associated with the proof. + pub new_payload_request_root: Hash256, +} + +/// The type of an execution proof. +/// +/// Contains the proof data, type, and public input that links it to a specific new payload request. +#[derive( + Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Encode, Decode, TreeHash, +)] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] +pub struct ExecutionProof { + /// The proof data. + #[serde(with = "ssz_types::serde_utils::hex_var_list")] + pub proof_data: ProofData, + /// The type of proof. + pub proof_type: ProofType, + /// Public input linking the proof to a specific new payload request. + pub public_input: PublicInput, +} + +impl SignedRoot for ExecutionProof {} + +/// A signed execution proof from a validator. +/// +/// Contains the execution proof, the validator's index, and their BLS signature. +#[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize, Encode, Decode, TreeHash)] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] +pub struct SignedExecutionProof { + /// The execution proof message + pub message: ExecutionProof, + /// Index of the validator who signed this proof + #[serde(with = "serde_utils::quoted_u64")] + pub validator_index: u64, + /// BLS signature over the execution proof + pub signature: SignatureBytes, +} + +/// Identifies a block root and the proof types being requested for it. +/// +/// Matches the `ProofByRootIdentifier` container in the EIP-8025 p2p spec. +#[derive( + Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Encode, Decode, TreeHash, +)] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] +pub struct ProofByRootIdentifier { + /// The beacon block root whose execution proofs are being requested. + pub block_root: Hash256, + /// Proof types the requester still needs for this block root. + pub proof_types: VariableList, +} + +/// Proof attributes for requesting proof generation. +/// +/// Specifies which types of proofs should be generated for a payload. +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ProofAttributes { + /// List of proof types to generate + pub proof_types: Vec, +} + +// ============================================================================= +// Status Types +// ============================================================================= + +/// Status returned from proof verification operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ProofStatus { + /// The proof is valid. + Valid, + /// The proof/header verification failed. + Invalid, + /// The proof is valid but does not change the canonical head. + Accepted, + /// The proof type is not supported by this client. + NotSupported, + /// The request root that the proof is associated with is not yet known. + Syncing, +} + +impl ProofStatus { + /// Returns true if the status indicates successful verification. + pub fn is_valid(&self) -> bool { + matches!(self, ProofStatus::Valid) + } + + /// Returns true if the status indicates the node is still syncing proofs. + pub fn is_syncing(&self) -> bool { + matches!(self, ProofStatus::Syncing) + } + + /// Returns true if the status indicates the node has accepted the proof. + pub fn is_accepted(&self) -> bool { + matches!(self, ProofStatus::Accepted) + } +} + +impl std::fmt::Display for ProofStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ProofStatus::Valid => { + write!(f, "VALID") + } + ProofStatus::Invalid => write!(f, "INVALID"), + ProofStatus::Accepted => write!(f, "ACCEPTED"), + ProofStatus::NotSupported => write!(f, "NOT_SUPPORTED"), + ProofStatus::Syncing => write!(f, "SYNCING"), + } + } +} + +// ============================================================================= +// Utility Implementations +// ============================================================================= + +impl ExecutionProof { + /// Returns true if the proof data is empty. + pub fn is_empty(&self) -> bool { + self.proof_data.is_empty() + } + + /// Returns the size of the proof data in bytes. + pub fn proof_size(&self) -> usize { + self.proof_data.len() + } + + /// Returns the hash tree root of this execution proof. + pub fn hash_tree_root(&self) -> Hash256 { + tree_hash::TreeHash::tree_hash_root(self) + } +} + +impl SignedExecutionProof { + /// Returns a reference to the underlying execution proof. + pub fn proof(&self) -> &ExecutionProof { + &self.message + } + + /// Returns the proof data of the underlying execution proof. + pub fn proof_data(&self) -> &ProofData { + &self.message.proof_data + } + + /// Returns the new payload request root this proof validates. + pub fn request_root(&self) -> Hash256 { + self.message.public_input.new_payload_request_root + } + + /// Returns the proof type. + pub fn proof_type(&self) -> ProofType { + self.message.proof_type + } + + /// Returns the validator index that signed this proof. + pub fn validator_index(&self) -> u64 { + self.validator_index + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ssz::{Decode, Encode}; + + #[test] + fn public_input_round_trip() { + let input = PublicInput { + new_payload_request_root: Hash256::repeat_byte(0xab), + }; + let encoded = input.as_ssz_bytes(); + let decoded = PublicInput::from_ssz_bytes(&encoded).unwrap(); + assert_eq!(input, decoded); + } + + #[test] + fn execution_proof_round_trip() { + let proof = ExecutionProof { + proof_data: VariableList::new(vec![1u8, 2, 3, 4]).unwrap(), + proof_type: 1, + public_input: PublicInput { + new_payload_request_root: Hash256::repeat_byte(0xcd), + }, + }; + let encoded = proof.as_ssz_bytes(); + let decoded = ExecutionProof::from_ssz_bytes(&encoded).unwrap(); + assert_eq!(proof, decoded); + } + + #[test] + fn signed_execution_proof_round_trip() { + let signed_proof = SignedExecutionProof { + message: ExecutionProof { + proof_data: VariableList::new(vec![5u8, 6, 7, 8]).unwrap(), + proof_type: 2, + public_input: PublicInput { + new_payload_request_root: Hash256::repeat_byte(0xef), + }, + }, + validator_index: 42, + signature: SignatureBytes::empty(), + }; + let encoded = signed_proof.as_ssz_bytes(); + let decoded = SignedExecutionProof::from_ssz_bytes(&encoded).unwrap(); + assert_eq!(signed_proof, decoded); + } + + #[test] + fn execution_proof_is_empty() { + let empty_proof = ExecutionProof { + proof_data: VariableList::new(vec![]).unwrap(), + proof_type: 1, + public_input: PublicInput { + new_payload_request_root: Hash256::ZERO, + }, + }; + assert!(empty_proof.is_empty()); + + let non_empty_proof = ExecutionProof { + proof_data: VariableList::new(vec![1u8, 2, 3]).unwrap(), + proof_type: 1, + public_input: PublicInput { + new_payload_request_root: Hash256::ZERO, + }, + }; + assert!(!non_empty_proof.is_empty()); + } + + #[test] + fn execution_proof_size() { + let proof = ExecutionProof { + proof_data: VariableList::new(vec![1u8, 2, 3, 4, 5]).unwrap(), + proof_type: 1, + public_input: PublicInput { + new_payload_request_root: Hash256::ZERO, + }, + }; + assert_eq!(proof.proof_size(), 5); + + let empty_proof = ExecutionProof::default(); + assert_eq!(empty_proof.proof_size(), 0); + } + + #[test] + fn signed_execution_proof_accessors() { + let request_root = Hash256::repeat_byte(0xab); + let proof_type = 42u8; + let validator_index = 123u64; + + let signed_proof = SignedExecutionProof { + message: ExecutionProof { + proof_data: VariableList::new(vec![1u8, 2, 3]).unwrap(), + proof_type, + public_input: PublicInput { + new_payload_request_root: request_root, + }, + }, + validator_index, + signature: SignatureBytes::empty(), + }; + + assert_eq!(signed_proof.request_root(), request_root); + assert_eq!(signed_proof.proof_type(), proof_type); + assert_eq!(signed_proof.validator_index(), validator_index); + assert_eq!(signed_proof.proof().proof_type, proof_type); + } + + #[test] + fn proof_status_is_valid() { + assert!(ProofStatus::Valid.is_valid()); + assert!(!ProofStatus::Invalid.is_valid()); + assert!(!ProofStatus::Accepted.is_valid()); + assert!(!ProofStatus::NotSupported.is_valid()); + } + + #[test] + fn proof_status_is_syncing() { + assert!(ProofStatus::Syncing.is_syncing()); + assert!(!ProofStatus::Accepted.is_syncing()); + assert!(!ProofStatus::Valid.is_syncing()); + assert!(!ProofStatus::Invalid.is_syncing()); + assert!(!ProofStatus::NotSupported.is_syncing()); + } + + #[test] + fn proof_attributes_default() { + let attrs = ProofAttributes::default(); + assert!(attrs.proof_types.is_empty()); + + let attrs_with_types = ProofAttributes { + proof_types: vec![1, 2, 3], + }; + assert_eq!(attrs_with_types.proof_types.len(), 3); + } + + #[test] + fn max_proof_size_is_1344_kib() { + use typenum::Unsigned; + + assert_eq!(MaxProofSizeKiB::USIZE, 1344); + assert_eq!(MaxProofSize::USIZE, 1_376_256); + } +} diff --git a/consensus/types/src/execution/mod.rs b/consensus/types/src/execution/mod.rs index a3d4ed87301..2c2729bd083 100644 --- a/consensus/types/src/execution/mod.rs +++ b/consensus/types/src/execution/mod.rs @@ -1,3 +1,4 @@ +pub mod eip8025; mod eth1_data; mod execution_block_header; #[macro_use] @@ -14,6 +15,11 @@ mod signed_execution_payload_bid; mod signed_execution_payload_envelope; pub use bls_to_execution_change::BlsToExecutionChange; +pub use eip8025::{ + DOMAIN_EXECUTION_PROOF, ExecutionProof, ExecutionProofList, MIN_REQUIRED_EXECUTION_PROOFS, + MaxExecutionProofsPerPayload, ProofAttributes, ProofByRootIdentifier, ProofData, ProofStatus, + ProofType, PublicInput, SignedExecutionProof, +}; pub use eth1_data::Eth1Data; pub use execution_block_header::{EncodableExecutionBlockHeader, ExecutionBlockHeader}; pub use execution_payload::{ diff --git a/lighthouse/environment/Cargo.toml b/lighthouse/environment/Cargo.toml index 6d6ffa1725f..669bd0db278 100644 --- a/lighthouse/environment/Cargo.toml +++ b/lighthouse/environment/Cargo.toml @@ -4,6 +4,9 @@ version = "0.1.2" authors = ["Paul Hauner "] edition = { workspace = true } +[features] +test-utils = [] + [dependencies] async-channel = { workspace = true } clap = { workspace = true } diff --git a/lighthouse/environment/src/lib.rs b/lighthouse/environment/src/lib.rs index 1431b03f453..d801cf28b64 100644 --- a/lighthouse/environment/src/lib.rs +++ b/lighthouse/environment/src/lib.rs @@ -33,6 +33,9 @@ use { #[cfg(not(target_family = "unix"))] use {futures::channel::oneshot, std::cell::RefCell}; +#[cfg(feature = "test-utils")] +pub mod test_utils; + pub mod tracing_common; pub const SSE_LOG_CHANNEL_SIZE: usize = 2048; @@ -284,6 +287,25 @@ impl EnvironmentBuilder { Ok(self) } + #[cfg(feature = "test-utils")] + pub fn build_test_environment(self) -> Result, String> { + let (signal, exit) = async_channel::bounded(1); + let (signal_tx, signal_rx) = channel(1); + Ok(test_utils::TestEnvironment { + executor: TaskExecutor::new( + tokio::runtime::Handle::try_current().map_err(|e| e.to_string())?, + exit.clone(), + signal_tx, + ), + signal_rx: Some(signal_rx), + signal: Some(signal), + sse_logging_components: self.sse_logging_components, + eth_spec_instance: self.eth_spec_instance, + eth2_config: self.eth2_config, + eth2_network_config: self.eth2_network_config.map(Arc::new), + }) + } + /// Consumes the builder, returning an `Environment`. pub fn build(self) -> Result, String> { let (signal, exit) = async_channel::bounded(1); diff --git a/lighthouse/environment/src/test_utils.rs b/lighthouse/environment/src/test_utils.rs new file mode 100644 index 00000000000..c12fb476574 --- /dev/null +++ b/lighthouse/environment/src/test_utils.rs @@ -0,0 +1,24 @@ +use crate::*; +use task_executor::TaskExecutor; + +pub struct TestEnvironment { + pub executor: TaskExecutor, + pub signal_rx: Option>, + pub signal: Option>, + pub sse_logging_components: Option, + pub eth_spec_instance: E, + pub eth2_config: Eth2Config, + pub eth2_network_config: Option>, +} + +impl TestEnvironment { + pub fn core_context(&self) -> RuntimeContext { + RuntimeContext { + executor: self.executor.clone(), + eth_spec_instance: self.eth_spec_instance.clone(), + eth2_config: self.eth2_config.clone(), + eth2_network_config: self.eth2_network_config.clone(), + sse_logging_components: self.sse_logging_components.clone(), + } + } +} diff --git a/scripts/local_testnet/network_params_eip8025.yaml b/scripts/local_testnet/network_params_eip8025.yaml new file mode 100644 index 00000000000..8ae664ffed2 --- /dev/null +++ b/scripts/local_testnet/network_params_eip8025.yaml @@ -0,0 +1,40 @@ +# EIP-8025 multi-node testnet configuration. +# +# Uses MockProofNodeClient via the http://mock/{n}/ URL pattern. +# See start_eip8025_testnet.sh for usage. +# +# Full configuration reference: https://github.com/ethpandaops/ethereum-package#configuration +participants: + # Supernode participants with proof engine enabled + - cl_type: lighthouse + cl_image: ethpandaops/lighthouse:eth-act-optional-proofs + el_type: geth + el_image: ethereum/client-go:latest + supernode: true + cl_extra_params: + - --target-peers=3 + - --proof-engine-endpoint=http://mock/0/ + vc_extra_params: + - --proof-engine-endpoint=http://mock/0/ + count: 2 + # Non-supernode participants with proof engine enabled + - cl_type: lighthouse + cl_image: ethpandaops/lighthouse:eth-act-optional-proofs + el_type: geth + el_image: ethereum/client-go:latest + supernode: false + cl_extra_params: + - --target-peers=3 + - --proof-engine-endpoint=http://mock/0/ + vc_extra_params: + - --proof-engine-endpoint=http://mock/0/ + count: 2 +network_params: + fulu_fork_epoch: 0 + seconds_per_slot: 6 +snooper_enabled: false +global_log_level: debug +additional_services: + - dora + - prometheus + - grafana diff --git a/scripts/local_testnet/network_params_eip8025_zkboost.yaml b/scripts/local_testnet/network_params_eip8025_zkboost.yaml new file mode 100644 index 00000000000..0a66d9ed3f1 --- /dev/null +++ b/scripts/local_testnet/network_params_eip8025_zkboost.yaml @@ -0,0 +1,73 @@ +# EIP-8025 testnet with zkboost backends via native ethereum-package integration. +# +# Run with: +# kurtosis run --enclave eip8025-zkboost \ +# github.com/ethpandaops/ethereum-package \ +# --args-file scripts/local_testnet/network_params_eip8025_zkboost.yaml +# +# For the mock-only path (no zkboost sidecar), use network_params_eip8025.yaml instead. + +# ── Ethereum package participants ──────────────────────────────────────────── +participants: + # Supernode participants — proof engine points to zkboost-1 + - cl_type: lighthouse + cl_image: ethpandaops/lighthouse:eth-act-optional-proofs + el_type: reth + el_image: ghcr.io/paradigmxyz/reth + supernode: true + cl_extra_params: + - --target-peers=3 + - --proof-engine-endpoint=http://zkboost-1:3000 + - --proof-types=6 + vc_extra_params: + - --proof-engine-endpoint=http://zkboost-1:3000 + - --proof-types=6 + count: 2 + # Non-supernode participants — proof engine points to zkboost-2 + - cl_type: lighthouse + cl_image: ethpandaops/lighthouse:eth-act-optional-proofs + el_type: reth + el_image: ghcr.io/paradigmxyz/reth + supernode: false + cl_extra_params: + - --target-peers=3 + - --proof-engine-endpoint=http://zkboost-2:3000 + - --proof-types=6 + vc_extra_params: + - --proof-engine-endpoint=http://zkboost-2:3000 + - --proof-types=6 + count: 2 + +network_params: + fulu_fork_epoch: 0 + seconds_per_slot: 6 + +snooper_enabled: false +global_log_level: debug + +additional_services: + - zkboost + - dora + - prometheus + - grafana + +# ── zkboost configuration ───────────────────────────────────────────────────── +# Processed natively by ethereum-package; see src/zkboost/zkboost_launcher.star. +zkboost_params: + image: ghcr.io/eth-act/zkboost/zkboost:0.5.0 + env: + RUST_LOG: info,zkboost=debug + # Two instances: each connected to its own EL, serving one group of participants. + # el_participant_index is 0-based into the flat participant list after count expansion: + # 0 = el-1-reth-lighthouse (first supernode) + # 1 = el-2-reth-lighthouse (second supernode) + instances: + - name: zkboost-1 + el_participant_index: 0 + - name: zkboost-2 + el_participant_index: 1 + zkvms: + - kind: mock + proof_type: reth-zisk + mock_proving_time: { kind: constant, ms: 300 } + mock_proof_size: 1024 diff --git a/scripts/local_testnet/network_params_eip8025_zkboost_gpu.yaml b/scripts/local_testnet/network_params_eip8025_zkboost_gpu.yaml new file mode 100644 index 00000000000..58b113e0937 --- /dev/null +++ b/scripts/local_testnet/network_params_eip8025_zkboost_gpu.yaml @@ -0,0 +1,95 @@ +# EIP-8025 testnet with zkboost backed by real GPU (ZisK) provers. +# +# Run with: +# kurtosis run --enclave eip8025-zkboost-gpu \ +# github.com/ethpandaops/ethereum-package \ +# --args-file scripts/local_testnet/network_params_eip8025_zkboost_gpu.yaml +# +# Prerequisites: +# - NVIDIA GPUs with drivers installed (≥8 GPUs recommended: 4 per prover type) +# - NVIDIA Container Toolkit configured for Docker +# - ~5-10 min startup time for ZisK setup on first run + +# ── Ethereum package participants ──────────────────────────────────────────── +participants: + # Supernode participants + - cl_type: lighthouse + cl_image: ethpandaops/lighthouse:eth-act-optional-proofs + el_type: reth + el_image: ghcr.io/paradigmxyz/reth + supernode: true + cl_extra_params: + - --target-peers=3 + - --proof-engine-endpoint=http://zkboost-1:3000 + - --proof-types=6 + vc_extra_params: + - --proof-engine-endpoint=http://zkboost-1:3000 + - --proof-types=6 + count: 2 + # Non-supernode participants + - cl_type: lighthouse + cl_image: ethpandaops/lighthouse:eth-act-optional-proofs + el_type: reth + el_image: ghcr.io/paradigmxyz/reth + supernode: false + cl_extra_params: + - --target-peers=3 + - --proof-engine-endpoint=http://zkboost-1:3000 + - --proof-types=6 + vc_extra_params: + - --proof-engine-endpoint=http://zkboost-1:3000 + - --proof-types=6 + count: 2 + +network_params: + fulu_fork_epoch: 0 + seconds_per_slot: 6 + +snooper_enabled: false +global_log_level: debug + +additional_services: + - zkboost + - dora + - prometheus + - grafana + +# ── zkboost configuration ───────────────────────────────────────────────────── +zkboost_params: + image: ghcr.io/eth-act/zkboost/zkboost:0.5.0 + env: + RUST_LOG: info,zkboost=debug + instances: + - name: zkboost-1 + el_participant_index: 0 + zkvms: + # reth-zisk GPU prover — uses GPUs 0-3 on the host + - kind: ere + proof_type: reth-zisk + image: ghcr.io/eth-act/ere/ere-server-zisk:0.6.1-cuda + program_url: https://github.com/eth-act/ere-guests/releases/download/v0.7.0/stateless-validator-reth-zisk + port: 3000 + gpu: + device_ids: ["0", "1", "2", "3"] + shm_size: 32768 # 32 GiB — ZisK requires large shared memory for GPU proving + ulimits: + memlock: -1 # unlimited memory lock — required for CUDA unified memory + env: + RUST_LOG: info + ERE_ZISK_SETUP_ON_INIT: "1" + ERE_ZISK_START_SERVER_TIMEOUT_SEC: "600" + # ethrex-zisk GPU prover — uses GPUs 4-7 on the host + - kind: ere + proof_type: ethrex-zisk + image: ghcr.io/eth-act/ere/ere-server-zisk:0.6.1-cuda + program_url: https://github.com/eth-act/ere-guests/releases/download/v0.7.0/stateless-validator-ethrex-zisk + port: 3000 + gpu: + device_ids: ["4", "5", "6", "7"] + shm_size: 32768 + ulimits: + memlock: -1 + env: + RUST_LOG: info + ERE_ZISK_SETUP_ON_INIT: "1" + ERE_ZISK_START_SERVER_TIMEOUT_SEC: "600" diff --git a/scripts/local_testnet/start_eip8025_testnet.sh b/scripts/local_testnet/start_eip8025_testnet.sh new file mode 100755 index 00000000000..978ecacd5a8 --- /dev/null +++ b/scripts/local_testnet/start_eip8025_testnet.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash + +# Start a local EIP-8025 testnet using Kurtosis. +# +# Requires: docker, kurtosis +# +# This script builds Lighthouse (optional) and launches a Kurtosis enclave via +# the ethereum-package. The network params file selects the topology: +# network_params_eip8025.yaml — mock proof engines (no zkboost) +# network_params_eip8025_zkboost.yaml — zkboost backends (mock zkVM) +# network_params_eip8025_zkboost_gpu.yaml — zkboost backends (GPU provers) + +set -Eeuo pipefail + +SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +ROOT_DIR="$SCRIPT_DIR/../.." +ENCLAVE_NAME=eip8025-testnet +NETWORK_PARAMS_FILE=$SCRIPT_DIR/network_params_eip8025.yaml +ETHEREUM_PKG=github.com/ethpandaops/ethereum-package +# Must match the `cl_image` in the network params yaml so a local build is +# picked up by Kurtosis instead of pulling the remote image. +LH_IMAGE_NAME=ethpandaops/lighthouse:eth-act-optional-proofs + +BUILD_IMAGE=true +KEEP_ENCLAVE=false + +# Get options +while getopts "e:n:p:bkh" flag; do + case "${flag}" in + e) ENCLAVE_NAME=${OPTARG};; + n) NETWORK_PARAMS_FILE=${OPTARG};; + p) ETHEREUM_PKG=${OPTARG};; + b) BUILD_IMAGE=false;; + k) KEEP_ENCLAVE=true;; + h) + echo "Start a local EIP-8025 testnet with Kurtosis." + echo + echo "usage: $0 " + echo + echo "Options:" + echo " -e: enclave name default: $ENCLAVE_NAME" + echo " -n: kurtosis network params file path default: $NETWORK_PARAMS_FILE" + echo " -p: ethereum-package path or GitHub ref default: $ETHEREUM_PKG" + echo " -b: skip building Lighthouse docker image" + echo " -k: keep existing enclave (don't destroy first)" + echo " -h: this help" + exit + ;; + esac +done + +for cmd in docker kurtosis; do + if ! command -v "$cmd" &> /dev/null; then + echo "$cmd is not installed. Please install $cmd and try again." + exit 1 + fi +done + +if [ "$KEEP_ENCLAVE" = false ]; then + kurtosis enclave rm -f "$ENCLAVE_NAME" 2>/dev/null || true +fi + +if [ "$BUILD_IMAGE" = true ]; then + echo "Building Lighthouse Docker image ($LH_IMAGE_NAME)." + docker build \ + --build-arg FEATURES=portable,spec-minimal \ + -f "$ROOT_DIR/Dockerfile" \ + -t "$LH_IMAGE_NAME" \ + "$ROOT_DIR" +else + echo "Skipping Lighthouse Docker image build." +fi + +echo "Starting EIP-8025 testnet enclave: $ENCLAVE_NAME" +echo " network params: $NETWORK_PARAMS_FILE" +echo " ethereum-package: $ETHEREUM_PKG" +kurtosis run --enclave "$ENCLAVE_NAME" \ + "$ETHEREUM_PKG" \ + --args-file "$NETWORK_PARAMS_FILE" + +echo +echo "EIP-8025 testnet started!" +echo +echo "Useful commands:" +echo " kurtosis enclave inspect $ENCLAVE_NAME" +echo " kurtosis service logs $ENCLAVE_NAME " +echo " kurtosis enclave rm -f $ENCLAVE_NAME" diff --git a/testing/ef_tests/Cargo.toml b/testing/ef_tests/Cargo.toml index ac51e827ad6..bb7cba0b100 100644 --- a/testing/ef_tests/Cargo.toml +++ b/testing/ef_tests/Cargo.toml @@ -26,8 +26,10 @@ fork_choice = { workspace = true } fs2 = { workspace = true } hex = { workspace = true } kzg = { workspace = true } +lighthouse_network = { workspace = true } logging = { workspace = true } milhouse = { workspace = true } +network = { workspace = true } proto_array = { workspace = true } rayon = { workspace = true } serde = { workspace = true } diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index 53fb626e7e6..723c5e7e9e8 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -75,8 +75,15 @@ "tests/.*/compute_challenge/.*", # We don't need these manifest files at the moment. "tests/.*/manifest.yaml", - # TODO: gossip condition tests not implemented yet - "tests/.*/.*/networking/.*", + # TODO: Remaining gossip validation topics not yet implemented + "tests/.*/.*/networking/gossip_beacon_block/.*", + "tests/.*/.*/networking/gossip_beacon_attestation/.*", + "tests/.*/.*/networking/gossip_beacon_aggregate_and_proof/.*", + "tests/.*/.*/networking/gossip_voluntary_exit/.*", + "tests/.*/.*/networking/gossip_bls_to_execution_change/.*", + "tests/.*/.*/networking/gossip_sync_committee_message/.*", + "tests/.*/.*/networking/gossip_sync_committee_contribution_and_proof/.*", + "tests/.*/.*/networking/gossip_blob_sidecar/.*", # TODO: fast confirmation rule not merged yet "tests/.*/.*/fast_confirmation", ] diff --git a/testing/ef_tests/src/cases.rs b/testing/ef_tests/src/cases.rs index b2e02763539..b2386f6fa50 100644 --- a/testing/ef_tests/src/cases.rs +++ b/testing/ef_tests/src/cases.rs @@ -20,6 +20,7 @@ mod fork_choice; mod genesis_initialization; mod genesis_validity; mod get_custody_groups; +mod gossip_validation; mod kzg_blob_to_kzg_commitment; mod kzg_compute_blob_kzg_proof; mod kzg_compute_cells; @@ -57,6 +58,7 @@ pub use fork::ForkTest; pub use genesis_initialization::*; pub use genesis_validity::*; pub use get_custody_groups::*; +pub use gossip_validation::*; pub use kzg_blob_to_kzg_commitment::*; pub use kzg_compute_blob_kzg_proof::*; pub use kzg_compute_cells::*; diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 2954ee7eb40..f6405831894 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -2,7 +2,6 @@ use super::*; use crate::decode::{ssz_decode_file, ssz_decode_file_with, ssz_decode_state, yaml_decode_file}; use ::fork_choice::{AttestationFromBlock, PayloadVerificationStatus, ProposerHeadError}; use beacon_chain::beacon_proposer_cache::compute_proposer_duties_from_head; -use beacon_chain::blob_verification::GossipBlobError; use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::chain_config::DisallowedReOrgOffsets; use beacon_chain::data_column_verification::GossipVerifiedDataColumn; @@ -12,7 +11,7 @@ use beacon_chain::{ attestation_verification::{ VerifiedAttestation, obtain_indexed_attestation_and_committees_per_slot, }, - blob_verification::GossipVerifiedBlob, + blob_verification::KzgVerifiedBlob, custody_context::NodeCustodyType, test_utils::{BeaconChainHarness, EphemeralHarnessType}, }; @@ -696,7 +695,6 @@ impl Tester { let mut blob_success = true; - // Convert blobs and kzg_proofs into sidecars, then plumb them into the availability tracker if let Some(blobs) = blobs.clone() { let proofs = kzg_proofs.unwrap(); let commitments = block @@ -709,37 +707,51 @@ impl Tester { // Zipping will stop when any of the zipped lists runs out, which is what we want. Some // of the tests don't provide enough proofs/blobs, and should fail the availability // check. - for (i, ((blob, kzg_proof), kzg_commitment)) in - blobs.into_iter().zip(proofs).zip(commitments).enumerate() - { - let blob_sidecar = Arc::new(BlobSidecar { - index: i as u64, - blob, - kzg_commitment, - kzg_proof, - signed_block_header: block.signed_block_header(), - kzg_commitment_inclusion_proof: block - .message() - .body() - .kzg_commitment_merkle_proof(i) - .unwrap(), - }); - - let chain = self.harness.chain.clone(); - let blob = - match GossipVerifiedBlob::new(blob_sidecar.clone(), blob_sidecar.index, &chain) - { - Ok(gossip_verified_blob) => gossip_verified_blob, - Err(GossipBlobError::KzgError(_)) => { + let verified_blobs: Vec> = blobs + .into_iter() + .zip(proofs) + .zip(commitments) + .enumerate() + .filter_map(|(i, ((blob, kzg_proof), kzg_commitment))| { + let blob_sidecar = Arc::new(BlobSidecar { + index: i as u64, + blob, + kzg_commitment, + kzg_proof, + signed_block_header: block.signed_block_header(), + kzg_commitment_inclusion_proof: block + .message() + .body() + .kzg_commitment_merkle_proof(i) + .unwrap(), + }); + + match KzgVerifiedBlob::new( + blob_sidecar.clone(), + &self.harness.chain.kzg, + Duration::default(), + ) { + Ok(verified) => Some(verified), + Err(_) => { blob_success = false; - GossipVerifiedBlob::__assumed_valid(blob_sidecar) + None } - Err(_) => GossipVerifiedBlob::__assumed_valid(blob_sidecar), - }; - let result = - self.block_on_dangerous(self.harness.chain.process_gossip_blob(blob))?; + } + }) + .collect(); + + if !verified_blobs.is_empty() { + let result = self + .harness + .chain + .data_availability_checker + .put_kzg_verified_blobs(block_root, verified_blobs); if valid { - assert!(result.is_ok()); + assert!( + result.is_ok(), + "put_kzg_verified_blobs failed: {:?}", + result + ); } } }; @@ -755,7 +767,8 @@ impl Tester { ))? .map(|avail: AvailabilityProcessingStatus| avail.try_into()); let success = blob_success && result.as_ref().is_ok_and(|inner| inner.is_ok()); - if success != valid { + // Only assert valid blocks import; blob-DA failure cases are expected to import now. + if valid && !success { return Err(Error::DidntFail(format!( "block with root {} was valid={} whilst test expects valid={}. result: {:?}", block_root, diff --git a/testing/ef_tests/src/cases/gossip_validation.rs b/testing/ef_tests/src/cases/gossip_validation.rs new file mode 100644 index 00000000000..3dbbcae5a72 --- /dev/null +++ b/testing/ef_tests/src/cases/gossip_validation.rs @@ -0,0 +1,206 @@ +use super::*; +use crate::bls_setting::BlsSetting; +use crate::decode::{ssz_decode_file, ssz_decode_state, yaml_decode_file}; +use crate::type_name::TypeName; +use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; +use lighthouse_network::{MessageAcceptance, MessageId, PeerId}; +use network::NetworkBeaconProcessor; +use serde::Deserialize; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use types::{AttesterSlashing, BeaconState, EthSpec, ForkName, ProposerSlashing}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "snake_case")] +enum ExpectedOutcome { + Valid, + Ignore, + Reject, +} + +impl PartialEq for ExpectedOutcome { + fn eq(&self, other: &MessageAcceptance) -> bool { + matches!( + (self, other), + (Self::Valid, MessageAcceptance::Accept) + | (Self::Ignore, MessageAcceptance::Ignore) + | (Self::Reject, MessageAcceptance::Reject) + ) + } +} + +#[derive(Debug, Clone, Deserialize)] +struct Meta { + topic: Topic, + #[serde(default)] + messages: Vec, + #[serde(default)] + bls_setting: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(deny_unknown_fields)] +struct MessageMeta { + message: String, + expected: ExpectedOutcome, + #[serde(default)] + reason: Option, + #[serde(default)] + #[allow(dead_code)] + subnet_id: Option, + #[serde(default)] + #[allow(dead_code)] + offset_ms: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "snake_case")] +enum Topic { + ProposerSlashing, + AttesterSlashing, + // TODO: add support for these topics + // VoluntaryExit, + // BlsToExecutionChange, + // SyncCommittee, + // SyncCommitteeContributionAndProof, + // BeaconBlock, + // BeaconAttestation, + // BeaconAggregateAndProof, +} + +#[derive(Debug)] +pub struct GossipValidation { + path: PathBuf, + meta: Meta, + state: BeaconState, +} + +impl LoadCase for GossipValidation { + fn load_from_dir(path: &Path, fork_name: ForkName) -> Result { + let meta: Meta = yaml_decode_file(&path.join("meta.yaml"))?; + let spec = &testing_spec::(fork_name); + let state = ssz_decode_state(&path.join("state.ssz_snappy"), spec)?; + + Ok(Self { + path: path.to_path_buf(), + meta, + state, + }) + } +} + +impl Case for GossipValidation { + fn description(&self) -> String { + self.path + .iter() + .next_back() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_default() + } + + fn result(&self, _case_index: usize, fork_name: ForkName) -> Result<(), Error> { + if let Some(bls_setting) = self.meta.bls_setting { + bls_setting.check()?; + } + + let spec = testing_spec::(fork_name); + let tester = GossipTester::new(self, spec)?; + + for message_meta in &self.meta.messages { + let actual = + tester.validate_message(&self.path, &self.meta.topic, message_meta, fork_name)?; + + if message_meta.expected != actual { + return Err(Error::NotEqual(format!( + "{}: expected {:?}, got {:?}{}", + self.path.display(), + message_meta.expected, + actual, + message_meta + .reason + .as_ref() + .map(|r| format!(" ({r})")) + .unwrap_or_default() + ))); + } + } + + Ok(()) + } +} + +struct GossipTester { + network_beacon_processor: Arc>>, +} + +impl GossipTester { + fn new(case: &GossipValidation, spec: ChainSpec) -> Result { + let genesis_time = case.state.genesis_time(); + let spec = Arc::new(spec); + + let harness = BeaconChainHarness::>::builder(E::default()) + .spec(spec.clone()) + .keypairs(vec![]) + .genesis_state_ephemeral_store(case.state.clone()) + .mock_execution_layer() + .recalculate_fork_times_with_genesis(genesis_time) + .mock_execution_layer_all_payloads_valid() + .build(); + + let network_beacon_processor = NetworkBeaconProcessor::null_from_harness(&harness); + + Ok(Self { + network_beacon_processor: Arc::new(network_beacon_processor), + }) + } + + fn validate_message( + &self, + path: &Path, + topic: &Topic, + message_meta: &MessageMeta, + fork_name: ForkName, + ) -> Result { + match topic { + Topic::ProposerSlashing => self.validate_proposer_slashing(path, message_meta), + Topic::AttesterSlashing => { + self.validate_attester_slashing(path, message_meta, fork_name) + } + } + } + + fn validate_proposer_slashing( + &self, + path: &Path, + message_meta: &MessageMeta, + ) -> Result { + let slashing: ProposerSlashing = + ssz_decode_file(&path.join(format!("{}.ssz_snappy", message_meta.message)))?; + + let message_id = MessageId::new(&[]); + let peer_id = PeerId::random(); + Ok(self + .network_beacon_processor + .process_gossip_proposer_slashing(message_id, peer_id, slashing)) + } + + fn validate_attester_slashing( + &self, + path: &Path, + message_meta: &MessageMeta, + fork_name: ForkName, + ) -> Result { + let ssz_path = path.join(format!("{}.ssz_snappy", message_meta.message)); + let slashing: AttesterSlashing = if fork_name.electra_enabled() { + ssz_decode_file(&ssz_path).map(AttesterSlashing::Electra)? + } else { + ssz_decode_file(&ssz_path).map(AttesterSlashing::Base)? + }; + + let message_id = MessageId::new(&[]); + let peer_id = PeerId::random(); + Ok(self + .network_beacon_processor + .process_gossip_attester_slashing(message_id, peer_id, slashing)) + } +} diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index 52cc5d57aee..df1ece49dd4 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -979,6 +979,36 @@ impl Handler for ComputeColumnsForCustodyGroupHandler } } +pub struct GossipValidationHandler { + handler_name: &'static str, + _phantom: PhantomData, +} + +impl GossipValidationHandler { + pub const fn new(handler_name: &'static str) -> Self { + Self { + handler_name, + _phantom: PhantomData, + } + } +} + +impl Handler for GossipValidationHandler { + type Case = cases::GossipValidation; + + fn config_name() -> &'static str { + E::name() + } + + fn runner_name() -> &'static str { + "networking" + } + + fn handler_name(&self) -> String { + self.handler_name.into() + } +} + #[derive(Educe)] #[educe(Default)] pub struct KZGComputeCellsHandler(PhantomData); diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 0ff854bd21b..6e1c4fdc10c 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -1189,3 +1189,15 @@ fn compute_columns_for_custody_group() { ComputeColumnsForCustodyGroupHandler::::default().run(); ComputeColumnsForCustodyGroupHandler::::default().run(); } + +#[test] +fn gossip_proposer_slashing() { + GossipValidationHandler::::new("gossip_proposer_slashing").run(); + GossipValidationHandler::::new("gossip_proposer_slashing").run(); +} + +#[test] +fn gossip_attester_slashing() { + GossipValidationHandler::::new("gossip_attester_slashing").run(); + GossipValidationHandler::::new("gossip_attester_slashing").run(); +} diff --git a/testing/proof_engine/Cargo.toml b/testing/proof_engine/Cargo.toml new file mode 100644 index 00000000000..235cc15e895 --- /dev/null +++ b/testing/proof_engine/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "proof_engine_test" +edition.workspace = true +version.workspace = true + +[dependencies] +anyhow = { workspace = true } +beacon_chain = { workspace = true } +execution_layer = { workspace = true } +futures = { workspace = true } +network = { workspace = true, features = ["disable-backfill"] } +simulator = { path = "../simulator", features = ["test-utils"] } +task_executor = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +types = { workspace = true } diff --git a/testing/proof_engine/src/lib.rs b/testing/proof_engine/src/lib.rs new file mode 100644 index 00000000000..575f0456573 --- /dev/null +++ b/testing/proof_engine/src/lib.rs @@ -0,0 +1,301 @@ +//! Integration tests for the EIP-8025 proof engine, using [`ProofEngineTestRig`]. + +mod rig; +pub use rig::ProofEngineTestRig; + +#[cfg(test)] +mod test { + use std::time::Duration; + + use futures::try_join; + use simulator::test_utils::{BeaconNodeHttpClient, Epoch, InternalBeaconNodeEvent, StateId}; + + use super::ProofEngineTestRig; + + async fn wait_for_finalized_epoch( + node: BeaconNodeHttpClient, + min_epoch: Epoch, + timeout: Duration, + ) -> anyhow::Result<()> { + tokio::time::timeout(timeout, async move { + loop { + let checkpoint = node + .get_beacon_states_finality_checkpoints(StateId::Head) + .await + .map_err(|e| anyhow::anyhow!("{e:?}"))? + .ok_or_else(|| anyhow::anyhow!("no finality checkpoint response"))? + .data + .finalized; + if checkpoint.epoch >= min_epoch { + return Ok(()); + } + tokio::time::sleep(Duration::from_secs(1)).await; + } + }) + .await + .map_err(|_| anyhow::anyhow!("timed out waiting for finalized epoch {min_epoch}"))? + } + + #[tokio::test] + #[cfg_attr(debug_assertions, ignore = "too slow in debug mode")] + async fn test_proof_engine_basic() -> anyhow::Result<()> { + let mut rig = ProofEngineTestRig::standard().await?; + rig.fixture.payloads_valid(); + rig.fixture.wait_for_genesis().await?; + + let mut gen_events = rig.proof_generator_events(0)?; + let mut verifier_chain = rig.proof_verifier_chain_events(0)?; + + rig.sign_and_submit_next_generator_proof(0, &mut gen_events) + .await?; + + verifier_chain + .collect_n( + 1, + |e| matches!(e, InternalBeaconNodeEvent::GossipExecutionProof(_)), + Duration::from_secs(60), + ) + .await?; + verifier_chain + .collect_n( + 1, + |e| matches!(e, InternalBeaconNodeEvent::ExecutionProofVerified { .. }), + Duration::from_secs(30), + ) + .await?; + + Ok(()) + } + + /// Assert that the validator client's proof service requests completed proof bytes from the + /// proof node, signs them, submits them to its beacon node, and that the proof reaches a + /// verifier through the normal gossip/verification path. + #[tokio::test] + #[cfg_attr(debug_assertions, ignore = "too slow in debug mode")] + async fn test_validator_client_proof_service_signs_and_submits_proofs() -> anyhow::Result<()> { + let mut rig = ProofEngineTestRig::standard().await?; + rig.fixture.payloads_valid(); + rig.fixture.wait_for_genesis().await?; + + let mut gen_events = rig.proof_generator_events(0)?; + let mut verifier_chain = rig.proof_verifier_chain_events(0)?; + + gen_events + .expect_proof_requests(1, Duration::from_secs(60)) + .await?; + gen_events + .expect_proof_fetched(1, Duration::from_secs(60)) + .await?; + + verifier_chain + .collect_n( + 1, + |e| matches!(e, InternalBeaconNodeEvent::GossipExecutionProof(_)), + Duration::from_secs(60), + ) + .await?; + verifier_chain + .collect_n( + 1, + |e| { + matches!( + e, + InternalBeaconNodeEvent::ExecutionProofVerified { status, .. } + if status.is_valid() || status.is_accepted() + ) + }, + Duration::from_secs(30), + ) + .await?; + + Ok(()) + } + + #[tokio::test] + #[ignore = "late-joining verifier cannot reliably discover the proof-capable peer yet; \ + proof-sync peer selection needs rework"] + async fn test_proof_engine_sync() -> anyhow::Result<()> { + let mut rig = ProofEngineTestRig::sync_topology().await?; + rig.fixture.payloads_valid(); + rig.fixture.wait_for_genesis().await?; + + wait_for_finalized_epoch( + rig.proof_generator_node(0)?, + Epoch::new(2), + Duration::from_secs(90), + ) + .await?; + + // Create a proof inside the generator's current finalized-to-head request window, then add + // a verifier. The generator should see the verifier's proof-capable ENR and initiate the + // proof-status exchange that lets the verifier request the missed proof by RPC. + let (block_root, _slot, request_root) = rig.latest_generator_payload_request(0).await?; + let proof = rig.sign_execution_proof(request_root, 0, 0)?; + rig.observe_valid_generator_proof(0, block_root, &proof)?; + let (_mock_events, mut verifier_chain) = rig.add_proof_verifier_and_subscribe().await?; + + // The late-joining verifier must issue at least one outbound RPC proof request for missing + // proofs in its finalized-to-head window. + verifier_chain + .collect_n( + 1, + |e| { + matches!( + e, + InternalBeaconNodeEvent::OutboundExecutionProofsByRange { .. } + | InternalBeaconNodeEvent::OutboundExecutionProofsByRoot { .. } + ) + }, + Duration::from_secs(120), + ) + .await?; + + // It must then receive proof data by RPC and verify it. `Accepted` means the proof content + // is verified without flipping execution optimism, which is the default proof policy. + verifier_chain + .collect_n( + 1, + |e| matches!(e, InternalBeaconNodeEvent::RpcExecutionProof(_)), + Duration::from_secs(120), + ) + .await?; + verifier_chain + .collect_n( + 1, + |e| { + matches!( + e, + InternalBeaconNodeEvent::ExecutionProofVerified { status, .. } + if status.is_valid() || status.is_accepted() + ) + }, + Duration::from_secs(30), + ) + .await?; + + Ok(()) + } + + /// Assert that the proof verifier receives gossip proofs from the generator and that the + /// full pipeline — gossip arrival → chain verification — completes successfully. + #[tokio::test] + #[cfg_attr(debug_assertions, ignore = "too slow in debug mode")] + async fn test_proof_verifier_receives_proofs() -> anyhow::Result<()> { + let mut rig = ProofEngineTestRig::standard().await?; + rig.fixture.payloads_valid(); + rig.fixture.wait_for_genesis().await?; + + // Subscribe to both streams before proofs start flowing so no events are missed. + let mut mock_events = rig.proof_verifier_events(0)?; + let mut chain_events = rig.proof_verifier_chain_events(0)?; + let mut gen_events = rig.proof_generator_events(0)?; + + rig.sign_and_submit_next_generator_proof(0, &mut gen_events) + .await?; + + // Mock engine confirms the received proof was verified by the verifier's EL. + mock_events + .expect_proof_verified(1, Duration::from_secs(60)) + .await?; + + // Chain events confirm the full gossip pipeline: arrival then on-chain verification. + // Events are buffered since subscription, so these complete immediately. + chain_events + .collect_n( + 1, + |e| matches!(e, InternalBeaconNodeEvent::GossipExecutionProof(_)), + Duration::from_secs(60), + ) + .await?; + chain_events + .collect_n( + 1, + |e| { + matches!( + e, + InternalBeaconNodeEvent::ExecutionProofVerified { status, .. } + // Quorum-based payload promotion is disabled in this network, so newly + // verified proofs surface as `Accepted` rather than `Valid`. + if status.is_valid() || status.is_accepted() + ) + }, + Duration::from_secs(30), + ) + .await?; + + Ok(()) + } + + /// Assert that two independent proof generators each receive proof requests, and that the + /// verifier receives gossip proofs from the network. + #[tokio::test] + #[cfg_attr(debug_assertions, ignore = "too slow in debug mode")] + async fn test_multi_generator_proof_requests() -> anyhow::Result<()> { + let mut rig = ProofEngineTestRig::multi_generator().await?; + rig.fixture.payloads_valid(); + rig.fixture.wait_for_genesis().await?; + + let mut gen0 = rig.proof_generator_events(0)?; + let mut gen1 = rig.proof_generator_events(1)?; + let mut verifier_chain = rig.proof_verifier_chain_events(0)?; + + // Both generators must independently receive proof requests from their EL. Submit the + // first generator's proof once requested so the verifier also exercises gossip. + let (_, _) = try_join!( + rig.sign_and_submit_next_generator_proof(0, &mut gen0), + async { + gen1.expect_proof_requests(1, Duration::from_secs(60)) + .await + .map_err(anyhow::Error::new) + }, + )?; + + verifier_chain + .collect_n( + 1, + |e| matches!(e, InternalBeaconNodeEvent::GossipExecutionProof(_)), + Duration::from_secs(60), + ) + .await?; + verifier_chain + .collect_n( + 1, + |e| matches!(e, InternalBeaconNodeEvent::ExecutionProofVerified { .. }), + Duration::from_secs(30), + ) + .await?; + + Ok(()) + } + + /// Assert that the network reaches finality (epoch ≥ 2) while the proof engine is running. + #[tokio::test] + #[cfg_attr(debug_assertions, ignore = "too slow in debug mode")] + async fn test_network_finalizes_with_proofs() -> anyhow::Result<()> { + let mut rig = ProofEngineTestRig::standard().await?; + rig.fixture.payloads_valid(); + rig.fixture.wait_for_genesis().await?; + + // MinimalEthSpec: 8 slots/epoch. Finality of epoch 2 requires epochs 3-4 to elapse. + // 4 epochs * 8 slots * 1s = 32s minimum; use 45s for margin. + tokio::time::sleep(Duration::from_secs(45)).await; + + // Check finality on the default node and the proof generator independently. + for node in [rig.default_node(0)?, rig.proof_generator_node(0)?] { + let checkpoint = node + .get_beacon_states_finality_checkpoints(StateId::Head) + .await + .map_err(|e| anyhow::anyhow!("{e:?}"))? + .ok_or_else(|| anyhow::anyhow!("no finality checkpoint response"))? + .data + .finalized; + assert!( + checkpoint.epoch >= Epoch::new(2), + "expected finality at epoch ≥ 2, got {}", + checkpoint.epoch + ); + } + + Ok(()) + } +} diff --git a/testing/proof_engine/src/rig.rs b/testing/proof_engine/src/rig.rs new file mode 100644 index 00000000000..dc8d0a289d3 --- /dev/null +++ b/testing/proof_engine/src/rig.rs @@ -0,0 +1,384 @@ +//! [`ProofEngineTestRig`] — a thin wrapper over [`TestNetworkFixture`] for EIP-8025 tests. +//! +//! Provides a clean API for building standard proof engine test topologies and asserting +//! on mock proof engine events, insulating individual tests from `LocalNetwork` internals. + +use anyhow::anyhow; +use beacon_chain::WhenSlotSkipped; +use beacon_chain::eip8025::{compute_execution_proof_domain, compute_signing_root}; +use execution_layer::NewPayloadRequest; +use execution_layer::test_utils::MockClientEvent; +use simulator::test_utils::{ + BeaconNodeHttpClient, Epoch, EventStream, InternalBeaconNodeEvent, LocalNetworkParams, + NodeType, TestNetworkFixture, TestNetworkFixtureBuilder, +}; +use types::test_utils::generate_deterministic_keypair; +use types::{ + EthSpec, ExecutionProof, Hash256, MinimalEthSpec, ProofData, PublicInput, SignedExecutionProof, + Slot, +}; + +pub use simulator::test_utils::MockEventStream; + +pub type E = MinimalEthSpec; + +/// Test harness for EIP-8025 proof engine integration tests. +pub struct ProofEngineTestRig { + pub fixture: TestNetworkFixture, +} + +impl ProofEngineTestRig { + /// Wrap a fixture directly. + pub fn new(fixture: TestNetworkFixture) -> Self { + Self { fixture } + } + + /// Standard topology: 1 vanilla node + 1 proof generator + 1 proof verifier. + /// All forks activate at genesis, 1-second slots. + pub async fn standard() -> anyhow::Result { + Ok(Self::new(base_builder().build().await?)) + } + + /// Sync topology: 1 vanilla node + 1 proof generator, no verifier, 1 delayed node slot. + /// Used for testing late-joining proof verifier sync recovery. + pub async fn sync_topology() -> anyhow::Result { + Ok(Self::new( + base_builder() + .map_spec(|spec| { + // Collapse all columns onto a single subnet so the small network can cover them. + spec.data_column_sidecar_subnet_count = 1; + spec.number_of_custody_groups = 8; + }) + .map_network_params(|params| { + params.proof_verifier_nodes = 0; + params.delayed_nodes = 1; + }) + .build() + .await?, + )) + } + + /// Multi-generator topology: 1 vanilla node + 2 proof generators + 1 proof verifier. + /// Used for testing that each generator is independently wired. + pub async fn multi_generator() -> anyhow::Result { + Ok(Self::new( + base_builder() + .map_network_params(|params| { + params.proof_generator_nodes = 2; + }) + .build() + .await?, + )) + } + + /// Subscribe to the nth proof generator node's event stream (0-indexed). + pub fn proof_generator_events(&self, n: usize) -> anyhow::Result { + let idx = self.fixture.config.network_params.node_count + n; + self.fixture + .network + .node_subscribe_client_events(idx) + .map(MockEventStream::from) + .ok_or_else(|| anyhow!("no proof generator at index {n}")) + } + + /// Subscribe to the nth proof verifier node's event stream (0-indexed). + pub fn proof_verifier_events(&self, n: usize) -> anyhow::Result { + let params = &self.fixture.config.network_params; + let idx = params.node_count + params.proof_generator_nodes + n; + self.fixture + .network + .node_subscribe_client_events(idx) + .map(MockEventStream::from) + .ok_or_else(|| anyhow!("no proof verifier at index {n}")) + } + + /// Subscribe to the internal event bus for the nth default node (0-indexed). + pub fn default_node_chain_events( + &self, + n: usize, + ) -> anyhow::Result> { + self.fixture + .network + .node_subscribe_internal_events(n) + .map(EventStream::from) + .ok_or_else(|| anyhow!("no default node at index {n}")) + } + + /// Subscribe to the internal event bus for the nth proof generator node (0-indexed). + pub fn proof_generator_chain_events( + &self, + n: usize, + ) -> anyhow::Result> { + let idx = self.fixture.config.network_params.node_count + n; + self.fixture + .network + .node_subscribe_internal_events(idx) + .map(EventStream::from) + .ok_or_else(|| anyhow!("no proof generator at index {n}")) + } + + /// Subscribe to the internal event bus for the nth proof verifier node (0-indexed). + pub fn proof_verifier_chain_events( + &self, + n: usize, + ) -> anyhow::Result> { + let params = &self.fixture.config.network_params; + let idx = params.node_count + params.proof_generator_nodes + n; + self.fixture + .network + .node_subscribe_internal_events(idx) + .map(EventStream::from) + .ok_or_else(|| anyhow!("no proof verifier at index {n}")) + } + + /// Return HTTP clients for all beacon nodes in the network. + pub fn remote_nodes(&self) -> anyhow::Result> { + self.fixture + .network + .remote_nodes() + .map_err(anyhow::Error::msg) + } + + /// Return an HTTP client for the nth default node (0-indexed). + pub fn default_node(&self, n: usize) -> anyhow::Result { + let idx = n; + self.fixture + .network + .remote_node(idx) + .ok_or_else(|| anyhow!("no default node at index {n}")) + } + + /// Return an HTTP client for the nth proof generator node (0-indexed). + pub fn proof_generator_node(&self, n: usize) -> anyhow::Result { + let idx = self.fixture.config.network_params.node_count + n; + self.fixture + .network + .remote_node(idx) + .ok_or_else(|| anyhow!("no proof generator node at index {n}")) + } + + /// Return an HTTP client for the nth proof verifier node (0-indexed). + pub fn proof_verifier_node(&self, n: usize) -> anyhow::Result { + let params = &self.fixture.config.network_params; + let idx = params.node_count + params.proof_generator_nodes + n; + self.fixture + .network + .remote_node(idx) + .ok_or_else(|| anyhow!("no proof verifier node at index {n}")) + } + + /// Return the most recent canonical execution payload request in the current + /// finalized-to-head window for the selected proof generator. + pub async fn latest_generator_payload_request( + &self, + generator_index: usize, + ) -> anyhow::Result<(Hash256, Slot, Hash256)> { + let idx = self.fixture.config.network_params.node_count + generator_index; + let chain = self + .fixture + .network + .beacon_nodes + .read() + .get(idx) + .and_then(|node| node.client.beacon_chain()) + .ok_or_else(|| anyhow!("no proof generator chain at index {generator_index}"))?; + + let head = chain.canonical_head.cached_head(); + let start_slot = head + .finalized_checkpoint() + .epoch + .start_slot(E::slots_per_epoch()); + let end_slot = head.head_slot(); + + for slot in (start_slot.as_u64()..=end_slot.as_u64()).rev() { + let slot = Slot::new(slot); + let Some(block_root) = chain + .block_root_at_slot(slot, WhenSlotSkipped::None) + .map_err(|error| anyhow!("{error:?}"))? + else { + continue; + }; + let Some(block) = chain + .get_block(&block_root) + .await + .map_err(|error| anyhow!("{error:?}"))? + else { + continue; + }; + let request = NewPayloadRequest::try_from(block.message()) + .map_err(|error| anyhow!("{error:?}"))?; + return Ok((block_root, slot, request.request_root())); + } + + Err(anyhow!( + "no canonical execution payload request in finalized-to-head window" + )) + } + + /// Store a valid proof on the selected generator without publishing it through gossip. + pub fn observe_valid_generator_proof( + &self, + generator_index: usize, + block_root: Hash256, + proof: &SignedExecutionProof, + ) -> anyhow::Result<()> { + let idx = self.fixture.config.network_params.node_count + generator_index; + let chain = self + .fixture + .network + .beacon_nodes + .read() + .get(idx) + .and_then(|node| node.client.beacon_chain()) + .ok_or_else(|| anyhow!("no proof generator chain at index {generator_index}"))?; + + let observation = chain + .observe_valid_execution_proof(proof, Some(block_root)) + .map_err(|error| anyhow!("{error:?}"))?; + anyhow::ensure!( + observation.block_root == Some(block_root), + "proof was not associated with the expected block root" + ); + Ok(()) + } + + /// Add a proof verifier node dynamically and return its mock and internal event streams. + pub async fn add_proof_verifier_and_subscribe( + &self, + ) -> anyhow::Result<(MockEventStream, EventStream)> { + let client_config = self.fixture.config.client.clone(); + let exec_config = self.fixture.config.execution.clone(); + + // Await the node start so we know its index in beacon_nodes before subscribing. + // Spawning + sleeping is unreliable on slow CI runners where node startup takes + // longer than the fixed sleep duration. + self.fixture + .network + .add_beacon_node(client_config, exec_config, NodeType::ProofVerifier) + .await + .map_err(anyhow::Error::msg)?; + + // The new verifier is the last beacon node; subscribe to both event streams. + let idx = self + .fixture + .network + .beacon_nodes + .read() + .len() + .saturating_sub(1); + let mock = self + .fixture + .network + .node_subscribe_client_events(idx) + .map(MockEventStream::from) + .ok_or_else(|| anyhow!("newly added verifier node has no mock event stream"))?; + let chain = self + .fixture + .network + .node_subscribe_internal_events(idx) + .map(EventStream::from) + .ok_or_else(|| anyhow!("newly added verifier node has no beacon chain"))?; + + Ok((mock, chain)) + } + + /// Wait for a proof request from the selected generator, sign a matching proof, submit it to + /// that generator's beacon node HTTP API, and return the signed proof. + pub async fn sign_and_submit_next_generator_proof( + &self, + generator_index: usize, + events: &mut MockEventStream, + ) -> anyhow::Result { + let request = events + .expect_proof_requests(1, std::time::Duration::from_secs(60)) + .await? + .into_iter() + .next() + .ok_or_else(|| anyhow!("expected one proof request event"))?; + + let MockClientEvent::ProofRequested { + root, + proof_attributes, + .. + } = request + else { + return Err(anyhow!("unexpected mock proof event")); + }; + let proof_type = proof_attributes + .proof_types + .first() + .copied() + .ok_or_else(|| anyhow!("proof request did not include any proof types"))?; + + let proof = self.sign_execution_proof(root, proof_type, 0)?; + self.proof_generator_node(generator_index)? + .post_beacon_pool_execution_proofs(std::slice::from_ref(&proof)) + .await + .map_err(|error| anyhow!("{error:?}"))?; + Ok(proof) + } + + pub fn sign_execution_proof( + &self, + request_root: Hash256, + proof_type: u8, + validator_index: u64, + ) -> anyhow::Result { + let chain = self + .fixture + .network + .beacon_nodes + .read() + .first() + .and_then(|node| node.client.beacon_chain()) + .ok_or_else(|| anyhow!("network has no beacon chain"))?; + let fork_name = chain.spec.fork_name_at_slot::(Slot::new(0)); + let keypair = generate_deterministic_keypair(validator_index as usize); + let proof = ExecutionProof { + proof_data: ProofData::new(vec![0xDE, 0xAD, 0xBE, 0xEF])?, + proof_type, + public_input: PublicInput { + new_payload_request_root: request_root, + }, + }; + let domain = + compute_execution_proof_domain(fork_name, chain.genesis_validators_root, &chain.spec); + let signing_root = compute_signing_root(&proof, domain); + + Ok(SignedExecutionProof { + message: proof, + validator_index, + signature: keypair.sk.sign(signing_root).into(), + }) + } + + /// Builder escape hatch for custom topologies. + pub fn builder() -> TestNetworkFixtureBuilder { + base_builder() + } +} + +/// Base builder shared by all standard topologies. +fn base_builder() -> TestNetworkFixtureBuilder { + TestNetworkFixture::builder() + .map_spec(|spec| { + *spec = spec.clone().set_slot_duration_ms::(1000); + spec.min_genesis_time = 0; + spec.altair_fork_epoch = Some(Epoch::new(0)); + spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + spec.capella_fork_epoch = Some(Epoch::new(0)); + spec.deneb_fork_epoch = Some(Epoch::new(0)); + spec.electra_fork_epoch = Some(Epoch::new(0)); + spec.fulu_fork_epoch = Some(Epoch::new(0)); + }) + .with_network_params(LocalNetworkParams { + validator_count: 4, + node_count: 1, + proposer_nodes: 0, + extra_nodes: 0, + proof_generator_nodes: 1, + proof_verifier_nodes: 1, + delayed_nodes: 0, + genesis_delay: 40, + }) +} diff --git a/testing/proof_engine_zkboost/Cargo.lock b/testing/proof_engine_zkboost/Cargo.lock new file mode 100644 index 00000000000..e4070c16879 --- /dev/null +++ b/testing/proof_engine_zkboost/Cargo.lock @@ -0,0 +1,9908 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addchain" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e33f6a175ec6a9e0aca777567f9ff7c3deefc255660df887e7fa3585e9801d8" +dependencies = [ + "num-bigint 0.3.3", + "num-integer", + "num-traits", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if 1.0.4", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if 1.0.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "alloy-chains" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e9e31d834fe25fe991b8884e4b9f0e59db4a97d86e05d1464d6899c013cd62" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "num_enum", + "serde", + "strum", +] + +[[package]] +name = "alloy-consensus" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20d33348e61388eb90da06e176030abf496120e54795273210eeea2cd76339c4" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "alloy-trie 0.9.5", + "alloy-tx-macros", + "auto_impl", + "borsh", + "c-kzg", + "derive_more 2.1.1", + "either", + "k256", + "once_cell", + "rand 0.8.5", + "secp256k1", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-consensus-any" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d145b64057ea2b66c6146817bee0ffc3adfb3f51c2b60f282f5200b23a6b4bfa" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-eip2124" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "741bdd7499908b3aa0b159bba11e71c8cddd009a2c2eb7a06e825f1ec87900a5" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "crc", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-eip2930" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9441120fa82df73e8959ae0e4ab8ade03de2aaae61be313fbf5746277847ce25" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "serde", +] + +[[package]] +name = "alloy-eip7702" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2919c5a56a1007492da313e7a3b6d45ef5edc5d33416fdec63c0d7a2702a0d20" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "k256", + "serde", + "serde_with", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-eip7928" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8222b1d88f9a6d03be84b0f5e76bb60cd83991b43ad8ab6477f0e4a7809b98d" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "serde", +] + +[[package]] +name = "alloy-eips" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5194943ebbbf25d308e13797b275dd1bf41487f22156dd2c8e17d24726a03888" +dependencies = [ + "alloy-eip2124", + "alloy-eip2930", + "alloy-eip7702", + "alloy-eip7928", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "auto_impl", + "borsh", + "c-kzg", + "derive_more 2.1.1", + "either", + "serde", + "serde_with", + "sha2", +] + +[[package]] +name = "alloy-evm" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b99ba7b74a87176f31ee1cd26768f7155b0eeff61ed925f59b13085ffe5f891" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-hardforks", + "alloy-primitives", + "alloy-sol-types", + "auto_impl", + "derive_more 2.1.1", + "revm", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-genesis" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3de7e0b146edaf966a1d2b5320903ce4a3e4e8452e4710585c0a22967216366" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-serde", + "alloy-trie 0.9.5", + "borsh", + "serde", + "serde_with", +] + +[[package]] +name = "alloy-hardforks" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83ba208044232d14d4adbfa77e57d6329f51bc1acc21f5667bb7db72d88a0831" +dependencies = [ + "alloy-chains", + "alloy-eip2124", + "alloy-primitives", + "auto_impl", + "dyn-clone", +] + +[[package]] +name = "alloy-json-abi" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9dbe713da0c737d9e5e387b0ba790eb98b14dd207fe53eef50e19a5a8ec3dac" +dependencies = [ + "alloy-primitives", + "alloy-sol-type-parser", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-network-primitives" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6e4eef41e348207945ae12d445d1d168ade6ada09161b0c9b05169f47ca81f" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-primitives" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3b431b4e72cd8bd0ec7a50b4be18e73dab74de0dba180eef171055e5d5926e" +dependencies = [ + "alloy-rlp", + "bytes", + "cfg-if 1.0.4", + "const-hex", + "derive_more 2.1.1", + "foldhash 0.2.0", + "getrandom 0.4.2", + "hashbrown 0.16.1", + "indexmap 2.13.0", + "itoa", + "k256", + "keccak-asm", + "paste", + "proptest", + "rand 0.9.2", + "rapidhash", + "ruint", + "rustc-hash", + "serde", + "sha3", +] + +[[package]] +name = "alloy-rlp" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93e50f64a77ad9c5470bf2ad0ca02f228da70c792a8f06634801e202579f35e" +dependencies = [ + "alloy-rlp-derive", + "arrayvec", + "bytes", +] + +[[package]] +name = "alloy-rlp-derive" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8849c74c9ca0f5a03da1c865e3eb6f768df816e67dd3721a398a8a7e398011" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "alloy-rpc-types-debug" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d701b7e29f69634e6cd5111e7cee333278557f7bf5d5e4764026e56adec75e1e" +dependencies = [ + "alloy-primitives", + "derive_more 2.1.1", + "serde", + "serde_with", +] + +[[package]] +name = "alloy-rpc-types-engine" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0bb761c5b208dcb938badf0b1243f623b1b80c15282f85fbeb45efa18f1120" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "derive_more 2.1.1", + "jsonwebtoken", + "rand 0.8.5", + "serde", + "strum", +] + +[[package]] +name = "alloy-rpc-types-eth" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f71d86495861c7b6cff4ed7e0c114f13c8d12c5406f03e88981b834c18715aa8" +dependencies = [ + "alloy-consensus", + "alloy-consensus-any", + "alloy-eips", + "alloy-network-primitives", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "alloy-sol-types", + "itertools 0.14.0", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-serde" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54d6afd8bd1ec6d34a01d03d3a6c547cfcb197dd631cc8908d183da69b7c4c92" +dependencies = [ + "alloy-primitives", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-sol-macro" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab81bab693da9bb79f7a95b64b394718259fdd7e41dceeced4cad57cb71c4f6a" +dependencies = [ + "alloy-sol-macro-expander", + "alloy-sol-macro-input", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "alloy-sol-macro-expander" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489f1620bb7e2483fb5819ed01ab6edc1d2f93939dce35a5695085a1afd1d699" +dependencies = [ + "alloy-sol-macro-input", + "const-hex", + "heck", + "indexmap 2.13.0", + "proc-macro-error2", + "proc-macro2", + "quote", + "sha3", + "syn 2.0.117", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-macro-input" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56cef806ad22d4392c5fc83cf8f2089f988eb99c7067b4e0c6f1971fc1cca318" +dependencies = [ + "const-hex", + "dunce", + "heck", + "macro-string", + "proc-macro2", + "quote", + "syn 2.0.117", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-type-parser" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6df77fea9d6a2a75c0ef8d2acbdfd92286cc599983d3175ccdc170d3433d249" +dependencies = [ + "serde", + "winnow 0.7.15", +] + +[[package]] +name = "alloy-sol-types" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64612d29379782a5dde6f4b6570d9c756d734d760c0c94c254d361e678a6591f" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "alloy-sol-macro", + "serde", +] + +[[package]] +name = "alloy-trie" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "983d99aa81f586cef9dae38443245e585840fcf0fc58b09aee0b1f27aed1d500" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "arrayvec", + "derive_more 2.1.1", + "nybbles 0.3.4", + "smallvec", + "tracing", +] + +[[package]] +name = "alloy-trie" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f14b5d9b2c2173980202c6ff470d96e7c5e202c65a9f67884ad565226df7fbb" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "derive_more 2.1.1", + "nybbles 0.4.8", + "serde", + "smallvec", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "alloy-tx-macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7a6b9edd1f6ea02b4f131d76b1aaf01b6ae53fbf91edec97a4e18ef60c1a0a" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arc-swap" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" +dependencies = [ + "rustversion", +] + +[[package]] +name = "archery" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a8da9bc4c4053ee067669762bcaeea6e241841295a2b6c948312dad6ef4cc02" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "ark-bls12-381" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df4dcc01ff89867cd86b0da835f23c3f02738353aaee7dde7495af71363b8d5" +dependencies = [ + "ark-ec", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", +] + +[[package]] +name = "ark-bn254" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" +dependencies = [ + "ark-ec", + "ark-ff 0.5.0", + "ark-std 0.5.0", +] + +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash", + "ark-ff 0.5.0", + "ark-poly", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe", + "fnv", + "hashbrown 0.15.5", + "itertools 0.13.0", + "num-bigint 0.4.6", + "num-integer", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" +dependencies = [ + "ark-ff-asm 0.3.0", + "ark-ff-macros 0.3.0", + "ark-serialize 0.3.0", + "ark-std 0.3.0", + "derivative", + "num-bigint 0.4.6", + "num-traits", + "paste", + "rustc_version 0.3.3", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm 0.4.2", + "ark-ff-macros 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint 0.4.6", + "num-traits", + "paste", + "rustc_version 0.4.1", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm 0.5.0", + "ark-ff-macros 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "educe", + "itertools 0.13.0", + "num-bigint 0.4.6", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-ff-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" +dependencies = [ + "num-bigint 0.4.6", + "num-traits", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint 0.4.6", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint 0.4.6", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe", + "fnv", + "hashbrown 0.15.5", +] + +[[package]] +name = "ark-serialize" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" +dependencies = [ + "ark-std 0.3.0", + "digest 0.9.0", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-std 0.4.0", + "digest 0.10.7", + "num-bigint 0.4.6", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "num-bigint 0.4.6", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-std" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "aurora-engine-modexp" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "518bc5745a6264b5fd7b09dffb9667e400ee9e2bbe18555fac75e1fe9afa0df9" +dependencies = [ + "hex", + "num", +] + +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core 0.4.5", + "bytes", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "itoa", + "matchit 0.7.3", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core 0.5.6", + "axum-macros", + "base64 0.22.1", + "bytes", + "form_urlencoded", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "itoa", + "matchit 0.8.4", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" +dependencies = [ + "axum 0.8.8", + "axum-core 0.5.6", + "bytes", + "futures-util", + "headers 0.4.1", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde_core", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base256emoji" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" +dependencies = [ + "const-str", + "match-lookup", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "serde", + "unty", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitcoin-io" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" + +[[package]] +name = "bitcoin_hashes" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +dependencies = [ + "bitcoin-io", + "hex-conservative", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "serde", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "blake3" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if 1.0.4", + "constant_time_eq", + "cpufeatures", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bls" +version = "0.2.0" +dependencies = [ + "alloy-primitives", + "blst", + "ethereum_hashing 0.8.0", + "ethereum_serde_utils", + "ethereum_ssz 0.10.4", + "fixed_bytes 0.1.0", + "hex", + "rand 0.9.2", + "safe_arith", + "serde", + "tree_hash 0.12.1", + "zeroize", +] + +[[package]] +name = "bls" +version = "0.2.0" +source = "git+https://github.com/sigp/lighthouse?branch=unstable#c7055b604f9958db410b2e42023763cb19dd7138" +dependencies = [ + "alloy-primitives", + "blst", + "ethereum_hashing 0.8.0", + "ethereum_serde_utils", + "ethereum_ssz 0.10.4", + "fixed_bytes 0.1.0 (git+https://github.com/sigp/lighthouse?branch=unstable)", + "hex", + "rand 0.9.2", + "safe_arith", + "serde", + "tree_hash 0.12.1", + "zeroize", +] + +[[package]] +name = "bls12_381" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3c196a77437e7cc2fb515ce413a6401291578b5afc8ecb29a3c7ab957f05941" +dependencies = [ + "ff 0.12.1", + "group 0.12.1", + "pairing 0.22.0", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "bls12_381" +version = "0.8.0" +source = "git+https://github.com/lambdaclass/bls12_381?branch=expose-fp-struct#219174187bd78154cec35b0809799fc2c991a579" +dependencies = [ + "digest 0.10.7", + "ff 0.13.1", + "group 0.13.0", + "pairing 0.23.0", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "blst" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + +[[package]] +name = "blstrs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a8a8ed6fefbeef4a8c7b460e4110e12c5e22a5b7cf32621aae6ad650c4dcf29" +dependencies = [ + "blst", + "byte-slice-cast", + "ff 0.13.1", + "group 0.13.0", + "pairing 0.23.0", + "rand_core 0.6.4", + "serde", + "subtle", +] + +[[package]] +name = "borsh" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "builder_client" +version = "0.1.0" +dependencies = [ + "bls 0.2.0", + "context_deserialize", + "eth2", + "ethereum_ssz 0.10.4", + "lighthouse_version", + "reqwest", + "sensitive_url", + "serde", + "serde_json", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + +[[package]] +name = "bytecheck" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0caa33a2c0edca0419d15ac723dff03f1956f7978329b1e3b5fdaaaed9d3ca8b" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "rancor", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "c-kzg" +version = "2.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6648ed1e4ea8e8a1a4a2c78e1cda29a3fd500bc622899c340d8525ea9a76b24a" +dependencies = [ + "blst", + "cc", + "glob", + "hex", + "libc", + "once_cell", + "serde", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver 1.0.27", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cc" +version = "1.2.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "compare_fields" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f45d0b4d61b582303179fb7a1a142bc9d647b7583db3b0d5f25a21d286fab9" +dependencies = [ + "compare_fields_derive", + "itertools 0.14.0", +] + +[[package]] +name = "compare_fields_derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ff1dbbda10d495b2c92749c002b2025e0be98f42d1741ecc9ff820d2f04dce" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "concat-kdf" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d72c1252426a83be2092dd5884a5f6e3b8e7180f6891b6263d2c21b92ec8816" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils 0.8.21", +] + +[[package]] +name = "const-hex" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531185e432bb31db1ecda541e9e7ab21468d4d844ad7505e0546a49b4945d49b" +dependencies = [ + "cfg-if 1.0.4", + "cpufeatures", + "proptest", + "serde_core", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-str" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" + +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "context_deserialize" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c523eea4af094b5970c321f4604abc42c5549d3cbae332e98325403fbbdbf70" +dependencies = [ + "context_deserialize_derive", + "serde", +] + +[[package]] +name = "context_deserialize_derive" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7bf98c48ffa511b14bb3c76202c24a8742cea1efa9570391c5d41373419a09" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if 1.0.4", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69323bff1fb41c635347b8ead484a5ca6c3f11914d784170b158d8449ab07f8e" +dependencies = [ + "cfg-if 0.1.10", + "crossbeam-channel 0.4.4", + "crossbeam-deque 0.7.4", + "crossbeam-epoch 0.8.2", + "crossbeam-queue 0.2.3", + "crossbeam-utils 0.7.2", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel 0.5.15", + "crossbeam-deque 0.8.6", + "crossbeam-epoch 0.9.18", + "crossbeam-queue 0.3.12", + "crossbeam-utils 0.8.21", +] + +[[package]] +name = "crossbeam-channel" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" +dependencies = [ + "crossbeam-utils 0.7.2", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils 0.8.21", +] + +[[package]] +name = "crossbeam-deque" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20ff29ded3204c5106278a81a38f4b482636ed4fa1e6cfbeef193291beb29ed" +dependencies = [ + "crossbeam-epoch 0.8.2", + "crossbeam-utils 0.7.2", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch 0.9.18", + "crossbeam-utils 0.8.21", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" +dependencies = [ + "autocfg", + "cfg-if 0.1.10", + "crossbeam-utils 0.7.2", + "lazy_static", + "maybe-uninit", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils 0.8.21", +] + +[[package]] +name = "crossbeam-queue" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" +dependencies = [ + "cfg-if 0.1.10", + "crossbeam-utils 0.7.2", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils 0.8.21", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if 0.1.10", + "lazy_static", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if 1.0.4", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version 0.4.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "serde", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "data-encoding-macro" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8142a83c17aa9461d637e649271eae18bf2edd00e91f2e105df36c3c16355bdb" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" +dependencies = [ + "data-encoding", + "syn 2.0.117", +] + +[[package]] +name = "datatest-stable" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833306ca7eec4d95844e65f0d7502db43888c5c1006c6c517e8cf51a27d15431" +dependencies = [ + "camino", + "fancy-regex", + "libtest-mimic", + "walkdir", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive-where" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl 1.0.0", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl 2.1.1", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "convert_case 0.6.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "unicode-xid", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "syn 2.0.117", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "serdect", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "eip4844" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82ab45fc63db6bbe5c3eb7c79303b2aff7ee529c991b2111c46879d1ea38407e" +dependencies = [ + "ekzg-bls12-381", + "ekzg-maybe-rayon", + "ekzg-polynomial", + "ekzg-serialization", + "ekzg-single-open", + "ekzg-trusted-setup", + "hex", + "itertools 0.14.0", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "eip_3076" +version = "0.1.0" +dependencies = [ + "bls 0.2.0", + "ethereum_serde_utils", + "fixed_bytes 0.1.0", + "serde", + "types 0.2.1", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "ekzg-bls12-381" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c599a59deba6188afd9f783507e4d89efc997f0fa340a758f0d0992b322416" +dependencies = [ + "blst", + "blstrs", + "ff 0.13.1", + "group 0.13.0", + "pairing 0.23.0", + "subtle", +] + +[[package]] +name = "ekzg-erasure-codes" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8474a41a30ddd2b651798b1aa9ce92011207c3667186fe9044184683250109e7" +dependencies = [ + "ekzg-bls12-381", + "ekzg-polynomial", +] + +[[package]] +name = "ekzg-maybe-rayon" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf94d1385185c1f7caef4973be49702c7d9ffdeaf832d126dbb9ed6efe09d40" + +[[package]] +name = "ekzg-multi-open" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d37456a32cf79bdbddd6685a2adec73210e2d60332370bc0e9a502b6d93beb" +dependencies = [ + "ekzg-bls12-381", + "ekzg-maybe-rayon", + "ekzg-polynomial", + "sha2", +] + +[[package]] +name = "ekzg-polynomial" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704751bac85af4754bb8a14457ef24d820738062d0b6f3763534d0980b1a1e81" +dependencies = [ + "ekzg-bls12-381", + "ekzg-maybe-rayon", +] + +[[package]] +name = "ekzg-serialization" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cb983d9f75b2804c00246def8d52c01cf05f70c22593b8d314fbcf0cf89042b" +dependencies = [ + "ekzg-bls12-381", + "hex", +] + +[[package]] +name = "ekzg-single-open" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799d5806d51e1453fa0f528d6acf4127e2a89e98312c826151ebc24ee3448ec3" +dependencies = [ + "ekzg-bls12-381", + "ekzg-polynomial", + "itertools 0.14.0", +] + +[[package]] +name = "ekzg-trusted-setup" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85314d56718dc2c6dd77c3b3630f1839defcb6f47d9c20195608a0f7976095ab" +dependencies = [ + "ekzg-bls12-381", + "ekzg-serialization", + "hex", + "serde", + "serde_json", +] + +[[package]] +name = "elf" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4445909572dbd556c457c849c4ca58623d84b27c8fff1e74b0b4227d8b90d17b" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff 0.13.1", + "generic-array", + "group 0.13.0", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if 1.0.4", +] + +[[package]] +name = "enr" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "851bd664a3d3a3c175cff92b2f0df02df3c541b4895d0ae307611827aae46152" +dependencies = [ + "alloy-rlp", + "base64 0.22.1", + "bytes", + "ed25519-dalek", + "hex", + "k256", + "log", + "rand 0.8.5", + "serde", + "sha3", + "zeroize", +] + +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "envy" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" +dependencies = [ + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "ere-io" +version = "0.3.0" +source = "git+https://github.com/eth-act/ere?tag=v0.3.0#ffc0e230cf4387fb22eb755f0fb323b38294520f" +dependencies = [ + "bincode 2.0.1", + "rkyv", + "serde", +] + +[[package]] +name = "ere-platform-trait" +version = "0.3.0" +source = "git+https://github.com/eth-act/ere?tag=v0.3.0#ffc0e230cf4387fb22eb755f0fb323b38294520f" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "ere-server" +version = "0.3.0" +source = "git+https://github.com/eth-act/ere?tag=v0.3.0#ffc0e230cf4387fb22eb755f0fb323b38294520f" +dependencies = [ + "anyhow", + "bincode 2.0.1", + "ere-zkvm-interface", + "prost", + "serde", + "thiserror 2.0.18", + "tokio", + "twirp", +] + +[[package]] +name = "ere-zkvm-interface" +version = "0.3.0" +source = "git+https://github.com/eth-act/ere?tag=v0.3.0#ffc0e230cf4387fb22eb755f0fb323b38294520f" +dependencies = [ + "anyhow", + "auto_impl", + "bincode 2.0.1", + "clap", + "indexmap 2.13.0", + "serde", + "strum", + "thiserror 2.0.18", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "escape8259" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5692dd7b5a1978a5aeb0ce83b7655c58ca8efdcb79d21036ea249da95afec2c6" + +[[package]] +name = "eth2" +version = "0.1.0" +dependencies = [ + "bls 0.2.0", + "context_deserialize", + "educe", + "eip_3076", + "enr", + "eth2_keystore", + "ethereum_serde_utils", + "ethereum_ssz 0.10.4", + "ethereum_ssz_derive 0.10.1", + "futures", + "futures-util", + "libp2p-identity", + "mediatype", + "multiaddr", + "pretty_reqwest_error", + "proto_array", + "reqwest", + "reqwest-eventsource", + "sensitive_url", + "serde", + "serde_json", + "ssz_types 0.14.1", + "superstruct", + "types 0.2.1", + "zeroize", +] + +[[package]] +name = "eth2_interop_keypairs" +version = "0.2.0" +dependencies = [ + "bls 0.2.0", + "ethereum_hashing 0.8.0", + "hex", + "num-bigint 0.4.6", + "serde", + "yaml_serde", +] + +[[package]] +name = "eth2_interop_keypairs" +version = "0.2.0" +source = "git+https://github.com/sigp/lighthouse?branch=unstable#c7055b604f9958db410b2e42023763cb19dd7138" +dependencies = [ + "bls 0.2.0 (git+https://github.com/sigp/lighthouse?branch=unstable)", + "ethereum_hashing 0.8.0", + "hex", + "num-bigint 0.4.6", + "serde", + "serde_yaml", +] + +[[package]] +name = "eth2_key_derivation" +version = "0.1.0" +dependencies = [ + "bls 0.2.0", + "num-bigint-dig", + "ring", + "sha2", + "zeroize", +] + +[[package]] +name = "eth2_keystore" +version = "0.1.0" +dependencies = [ + "aes", + "bls 0.2.0", + "cipher", + "ctr", + "eth2_key_derivation", + "hex", + "hmac", + "pbkdf2", + "rand 0.9.2", + "scrypt", + "serde", + "serde_json", + "serde_repr", + "sha2", + "unicode-normalization", + "uuid", + "zeroize", +] + +[[package]] +name = "ethbloom" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c321610643004cf908ec0f5f2aa0d8f1f8e14b540562a2887a1111ff1ecbf7b" +dependencies = [ + "crunchy", + "fixed-hash", + "impl-rlp", + "impl-serde", + "tiny-keccak", +] + +[[package]] +name = "ethereum-types" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab15ed80916029f878e0267c3a9f92b67df55e79af370bf66199059ae2b4ee3" +dependencies = [ + "ethbloom", + "fixed-hash", + "impl-rlp", + "impl-serde", + "primitive-types 0.13.1", + "uint 0.10.0", +] + +[[package]] +name = "ethereum_hashing" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c853bd72c9e5787f8aafc3df2907c2ed03cff3150c3acd94e2e53a98ab70a8ab" +dependencies = [ + "cpufeatures", + "ring", + "sha2", +] + +[[package]] +name = "ethereum_hashing" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa93f58bb1eb3d1e556e4f408ef1dac130bad01ac37db4e7ade45de40d1c86a" +dependencies = [ + "cpufeatures", + "ring", + "sha2", +] + +[[package]] +name = "ethereum_serde_utils" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dc1355dbb41fbbd34ec28d4fb2a57d9a70c67ac3c19f6a5ca4d4a176b9e997a" +dependencies = [ + "alloy-primitives", + "hex", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "ethereum_ssz" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dcddb2554d19cde19b099fadddde576929d7a4d0c1cd3512d1fd95cf174375c" +dependencies = [ + "alloy-primitives", + "ethereum_serde_utils", + "itertools 0.13.0", + "serde", + "serde_derive", + "smallvec", + "typenum", +] + +[[package]] +name = "ethereum_ssz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e462875ad8693755ea8913d6e905715c76ea4836e2254e18c9cf0f7a8f8c2a13" +dependencies = [ + "alloy-primitives", + "context_deserialize", + "ethereum_serde_utils", + "itertools 0.14.0", + "serde", + "serde_derive", + "smallvec", + "typenum", +] + +[[package]] +name = "ethereum_ssz_derive" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a657b6b3b7e153637dc6bdc6566ad9279d9ee11a15b12cfb24a2e04360637e9f" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ethereum_ssz_derive" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd596f91cff004fc8d02be44c21c0f9b93140a04b66027ae052f5f8e05b48eba" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ethrex-blockchain" +version = "9.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?tag=v9.0.0#e88175e2d49f1192cc9f2fdeae6fde1392d0759d" +dependencies = [ + "bytes", + "ethrex-common", + "ethrex-crypto", + "ethrex-metrics", + "ethrex-rlp", + "ethrex-storage", + "ethrex-trie", + "ethrex-vm", + "hex", + "rustc-hash", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "ethrex-common" +version = "9.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?tag=v9.0.0#e88175e2d49f1192cc9f2fdeae6fde1392d0759d" +dependencies = [ + "bytes", + "crc32fast", + "ethereum-types", + "ethrex-crypto", + "ethrex-rlp", + "ethrex-trie", + "hex", + "hex-literal", + "k256", + "kzg-rs", + "lazy_static", + "libc", + "once_cell", + "rayon", + "rkyv", + "rustc-hash", + "serde", + "serde_json", + "sha2", + "sha3", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "url", +] + +[[package]] +name = "ethrex-crypto" +version = "9.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?tag=v9.0.0#e88175e2d49f1192cc9f2fdeae6fde1392d0759d" +dependencies = [ + "c-kzg", + "kzg-rs", + "thiserror 2.0.18", + "tiny-keccak", +] + +[[package]] +name = "ethrex-l2-common" +version = "9.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?tag=v9.0.0#e88175e2d49f1192cc9f2fdeae6fde1392d0759d" +dependencies = [ + "bytes", + "ethereum-types", + "ethrex-common", + "ethrex-crypto", + "ethrex-rlp", + "ethrex-storage", + "ethrex-trie", + "ethrex-vm", + "hex", + "k256", + "lambdaworks-crypto", + "rkyv", + "serde", + "serde_with", + "sha3", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "ethrex-levm" +version = "9.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?tag=v9.0.0#e88175e2d49f1192cc9f2fdeae6fde1392d0759d" +dependencies = [ + "ark-bn254", + "ark-ec", + "ark-ff 0.5.0", + "bitvec", + "bls12_381 0.8.0", + "bytes", + "datatest-stable", + "derive_more 1.0.0", + "ethrex-common", + "ethrex-crypto", + "ethrex-rlp", + "k256", + "lambdaworks-math", + "lazy_static", + "malachite", + "p256", + "ripemd", + "rustc-hash", + "serde", + "serde_json", + "sha2", + "sha3", + "strum", + "thiserror 2.0.18", + "walkdir", +] + +[[package]] +name = "ethrex-metrics" +version = "9.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?tag=v9.0.0#e88175e2d49f1192cc9f2fdeae6fde1392d0759d" +dependencies = [ + "axum 0.8.8", + "ethrex-common", + "prometheus 0.13.4", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "ethrex-p2p" +version = "9.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?tag=v9.0.0#e88175e2d49f1192cc9f2fdeae6fde1392d0759d" +dependencies = [ + "aes", + "async-trait", + "bytes", + "concat-kdf", + "crossbeam 0.8.4", + "ctr", + "ethereum-types", + "ethrex-blockchain", + "ethrex-common", + "ethrex-crypto", + "ethrex-rlp", + "ethrex-storage", + "ethrex-threadpool", + "ethrex-trie", + "futures", + "hex", + "hmac", + "indexmap 2.13.0", + "lazy_static", + "prometheus 0.14.0", + "rand 0.8.5", + "rayon", + "rustc-hash", + "secp256k1", + "serde", + "serde_json", + "sha2", + "snap", + "spawned-concurrency", + "spawned-rt", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", +] + +[[package]] +name = "ethrex-rlp" +version = "9.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?tag=v9.0.0#e88175e2d49f1192cc9f2fdeae6fde1392d0759d" +dependencies = [ + "bytes", + "ethereum-types", + "hex", + "lazy_static", + "snap", + "thiserror 2.0.18", + "tinyvec", +] + +[[package]] +name = "ethrex-rpc" +version = "9.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?tag=v9.0.0#e88175e2d49f1192cc9f2fdeae6fde1392d0759d" +dependencies = [ + "axum 0.8.8", + "axum-extra", + "bytes", + "envy", + "ethereum-types", + "ethrex-blockchain", + "ethrex-common", + "ethrex-crypto", + "ethrex-metrics", + "ethrex-p2p", + "ethrex-rlp", + "ethrex-storage", + "ethrex-trie", + "ethrex-vm", + "hex", + "hex-literal", + "jsonwebtoken", + "rand 0.8.5", + "reqwest", + "secp256k1", + "serde", + "serde_json", + "sha2", + "spawned-concurrency", + "spawned-rt", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tower-http", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "ethrex-storage" +version = "9.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?tag=v9.0.0#e88175e2d49f1192cc9f2fdeae6fde1392d0759d" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "ethereum-types", + "ethrex-common", + "ethrex-crypto", + "ethrex-rlp", + "ethrex-trie", + "hex", + "lru 0.16.3", + "qfilter", + "rayon", + "rustc-hash", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "ethrex-threadpool" +version = "9.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?tag=v9.0.0#e88175e2d49f1192cc9f2fdeae6fde1392d0759d" +dependencies = [ + "crossbeam 0.8.4", +] + +[[package]] +name = "ethrex-trie" +version = "9.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?tag=v9.0.0#e88175e2d49f1192cc9f2fdeae6fde1392d0759d" +dependencies = [ + "anyhow", + "bytes", + "crossbeam 0.8.4", + "digest 0.10.7", + "ethereum-types", + "ethrex-crypto", + "ethrex-rlp", + "ethrex-threadpool", + "hex", + "lazy_static", + "rkyv", + "rustc-hash", + "serde", + "serde_json", + "smallvec", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "ethrex-vm" +version = "9.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?tag=v9.0.0#e88175e2d49f1192cc9f2fdeae6fde1392d0759d" +dependencies = [ + "bincode 1.3.3", + "bytes", + "derive_more 1.0.0", + "dyn-clone", + "ethereum-types", + "ethrex-common", + "ethrex-crypto", + "ethrex-levm", + "ethrex-rlp", + "ethrex-trie", + "lazy_static", + "rkyv", + "serde", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "eventsource-stream" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" +dependencies = [ + "futures-core", + "nom", + "pin-project-lite", +] + +[[package]] +name = "execution_layer" +version = "0.1.0" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-eth", + "arc-swap", + "async-stream", + "async-trait", + "bls 0.2.0", + "builder_client", + "bytes", + "eth2", + "ethereum_serde_utils", + "ethereum_ssz 0.10.4", + "ethereum_ssz_derive 0.10.1", + "fixed_bytes 0.1.0", + "fork_choice", + "futures", + "hash-db", + "hash256-std-hasher", + "hex", + "jsonwebtoken", + "keccak-hash", + "kzg 0.1.0", + "lighthouse_version", + "logging", + "lru 0.12.5", + "metrics 0.2.0", + "parking_lot", + "pretty_reqwest_error", + "rand 0.9.2", + "reqwest", + "reqwest-eventsource", + "sensitive_url", + "serde", + "serde_json", + "sha2", + "slot_clock", + "ssz_types 0.14.1", + "state_processing", + "strum", + "superstruct", + "task_executor", + "tempfile", + "tokio", + "tokio-stream", + "tracing", + "tree_hash 0.12.1", + "tree_hash_derive 0.12.1", + "triehash", + "typenum", + "types 0.2.1", + "warp", + "zeroize", +] + +[[package]] +name = "fancy-regex" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fastrlp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + +[[package]] +name = "fastrlp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "bitvec", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "bitvec", + "byteorder", + "ff_derive", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "ff_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f10d12652036b0e99197587c6ba87a8fc3031986499973c030d8b44fcc151b60" +dependencies = [ + "addchain", + "num-bigint 0.3.3", + "num-integer", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand 0.8.5", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "fixed-map" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ed19add84e8cb9e8cc5f7074de0324247149ffef0b851e215fb0edc50c229b" +dependencies = [ + "fixed-map-derive", +] + +[[package]] +name = "fixed-map-derive" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dc7a9cb3326bafb80642c5ce99b39a2c0702d4bfa8ee8a3e773791a6cbe2407" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "fixed_bytes" +version = "0.1.0" +dependencies = [ + "alloy-primitives", + "safe_arith", +] + +[[package]] +name = "fixed_bytes" +version = "0.1.0" +source = "git+https://github.com/sigp/lighthouse?branch=unstable#c7055b604f9958db410b2e42023763cb19dd7138" +dependencies = [ + "alloy-primitives", + "safe_arith", +] + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "fork_choice" +version = "0.1.0" +dependencies = [ + "ethereum_ssz 0.10.4", + "ethereum_ssz_derive 0.10.1", + "fixed_bytes 0.1.0", + "logging", + "metrics 0.2.0", + "proto_array", + "state_processing", + "superstruct", + "tracing", + "types 0.2.1", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gcd" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if 1.0.4", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if 1.0.4", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff 0.12.1", + "memuse", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff 0.13.1", + "rand 0.8.5", + "rand_core 0.6.4", + "rand_xorshift 0.3.0", + "subtle", +] + +[[package]] +name = "guest" +version = "0.5.0" +source = "git+https://github.com/eth-act/ere-guests?tag=v0.6.0#64c94bb3da631101a6cb2f276c89392cb7c3426f" +dependencies = [ + "ere-io", + "ere-platform-trait", + "sha2", +] + +[[package]] +name = "guest_program" +version = "9.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?tag=v9.0.0#e88175e2d49f1192cc9f2fdeae6fde1392d0759d" +dependencies = [ + "bincode 1.3.3", + "bytes", + "ethrex-blockchain", + "ethrex-common", + "ethrex-crypto", + "ethrex-l2-common", + "ethrex-rlp", + "ethrex-storage", + "ethrex-trie", + "ethrex-vm", + "hex", + "rkyv", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.18", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "halo2" +version = "0.1.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a23c779b38253fe1538102da44ad5bd5378495a61d2c4ee18d64eaa61ae5995" +dependencies = [ + "halo2_proofs", +] + +[[package]] +name = "halo2_proofs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e925780549adee8364c7f2b685c753f6f3df23bde520c67416e93bf615933760" +dependencies = [ + "blake2b_simd", + "ff 0.12.1", + "group 0.12.1", + "pasta_curves 0.4.1", + "rand_core 0.6.4", + "rayon", +] + +[[package]] +name = "hash-db" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d23bd4e7b5eda0d0f3a307e8b381fdc8ba9000f26fbe912250c0a4cc3956364a" + +[[package]] +name = "hash256-std-hasher" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92c171d55b98633f4ed3860808f004099b36c1cc29c42cfc53aa8591b21efcf2" +dependencies = [ + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", + "serde", + "serde_core", +] + +[[package]] +name = "headers" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +dependencies = [ + "base64 0.21.7", + "bytes", + "headers-core 0.2.0", + "http 0.2.12", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64 0.22.1", + "bytes", + "headers-core 0.3.0", + "http 1.4.0", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http 0.2.12", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http 1.4.0", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2 0.4.13", + "http 1.4.0", + "http-body 1.0.1", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.4.0", + "hyper 1.8.1", + "hyper-util", + "rustls 0.23.37", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "hyper 1.8.1", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.3", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-codec" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d40b9d5e17727407e55028eafc22b2dc68781786e6d7eb8a21103f5058e3a14" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-rlp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54ed8ad1f3877f7e775b8cbf30ed1bd3209a95401817f19a0eb4402d13f8cf90" +dependencies = [ + "rlp 0.6.1", +] + +[[package]] +name = "impl-serde" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a143eada6a1ec4aefa5049037a26a6d597bfd64f8c026d07b77133e02b7dd0b" +dependencies = [ + "serde", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "int_to_bytes" +version = "0.2.0" +dependencies = [ + "bytes", +] + +[[package]] +name = "int_to_bytes" +version = "0.2.0" +source = "git+https://github.com/sigp/lighthouse?branch=unstable#c7055b604f9958db410b2e42023763cb19dd7138" +dependencies = [ + "bytes", +] + +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "jubjub" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a575df5f985fe1cd5b2b05664ff6accfc46559032b954529fd225a2168d27b0f" +dependencies = [ + "bitvec", + "bls12_381 0.7.1", + "ff 0.12.1", + "group 0.12.1", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if 1.0.4", + "ecdsa", + "elliptic-curve", + "once_cell", + "serdect", + "sha2", + "signature", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "keccak-asm" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b646a74e746cd25045aa0fd42f4f7f78aa6d119380182c7e63a5593c4ab8df6f" +dependencies = [ + "digest 0.10.7", + "sha3-asm", +] + +[[package]] +name = "keccak-hash" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b286e6b663fb926e1eeb68528e69cb70ed46c6d65871a21b2215ae8154c6d3c" +dependencies = [ + "primitive-types 0.12.2", + "tiny-keccak", +] + +[[package]] +name = "kzg" +version = "0.1.0" +dependencies = [ + "educe", + "ethereum_hashing 0.8.0", + "ethereum_serde_utils", + "ethereum_ssz 0.10.4", + "ethereum_ssz_derive 0.10.1", + "hex", + "rayon", + "rust_eth_kzg", + "serde", + "serde_json", + "tracing", + "tree_hash 0.12.1", +] + +[[package]] +name = "kzg" +version = "0.1.0" +source = "git+https://github.com/sigp/lighthouse?branch=unstable#c7055b604f9958db410b2e42023763cb19dd7138" +dependencies = [ + "educe", + "ethereum_hashing 0.8.0", + "ethereum_serde_utils", + "ethereum_ssz 0.10.4", + "ethereum_ssz_derive 0.10.1", + "hex", + "rayon", + "rust_eth_kzg", + "serde", + "serde_json", + "tracing", + "tree_hash 0.12.1", +] + +[[package]] +name = "kzg-rs" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8b4f55c3dedcfaa8668de1dfc8469e7a32d441c28edf225ed1f566fb32977d" +dependencies = [ + "ff 0.13.1", + "hex", + "serde_arrays", + "sha2", + "sp1_bls12_381", + "spin", +] + +[[package]] +name = "lambdaworks-crypto" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58b1a1c1102a5a7fbbda117b79fb3a01e033459c738a3c1642269603484fd1c1" +dependencies = [ + "lambdaworks-math", + "rand 0.8.5", + "rand_chacha 0.3.1", + "serde", + "sha2", + "sha3", +] + +[[package]] +name = "lambdaworks-math" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "018a95aa873eb49896a858dee0d925c33f3978d073c64b08dd4f2c9b35a017c6" +dependencies = [ + "getrandom 0.2.17", + "num-bigint 0.4.6", + "num-traits", + "rand 0.8.5", + "rayon", + "serde", + "serde_json", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libp2p-identity" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c7892c221730ba55f7196e98b0b8ba5e04b4155651736036628e9f73ed6fc3" +dependencies = [ + "bs58", + "hkdf", + "multihash", + "sha2", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "libtest-mimic" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e6ba06f0ade6e504aff834d7c34298e5155c6baca353cc6a4aaff2f9fd7f33" +dependencies = [ + "anstream", + "anstyle", + "clap", + "escape8259", +] + +[[package]] +name = "libyaml-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e126dda6f34391ab7b444f9922055facc83c07a910da3eb16f1e4d9c45dc777" + +[[package]] +name = "lighthouse_version" +version = "8.1.3" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "logging" +version = "0.2.0" +dependencies = [ + "chrono", + "logroller", + "metrics 0.2.0", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-appender", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "workspace_members", +] + +[[package]] +name = "logroller" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83db12bbf439ebe64c0b0e4402f435b6f866db498fc1ae17e1b5d1a01625e2be" +dependencies = [ + "chrono", + "flate2", + "regex", + "thiserror 1.0.69", +] + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "macro-string" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "malachite" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec410515e231332b14cd986a475d1c3323bcfa4c7efc038bfa1d5b410b1c57e4" +dependencies = [ + "malachite-base", + "malachite-nz", + "malachite-q", +] + +[[package]] +name = "malachite-base" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c738d3789301e957a8f7519318fcbb1b92bb95863b28f6938ae5a05be6259f34" +dependencies = [ + "hashbrown 0.15.5", + "itertools 0.14.0", + "libm", + "ryu", +] + +[[package]] +name = "malachite-nz" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1707c9a1fa36ce21749b35972bfad17bbf34cf5a7c96897c0491da321e387d3b" +dependencies = [ + "itertools 0.14.0", + "libm", + "malachite-base", + "wide", +] + +[[package]] +name = "malachite-q" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d764801aa4e96bbb69b389dcd03b50075345131cd63ca2e380bca71cc37a3675" +dependencies = [ + "itertools 0.14.0", + "malachite-base", + "malachite-nz", +] + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "match-lookup" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + +[[package]] +name = "mediatype" +version = "0.19.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33746aadcb41349ec291e7f2f0a3aa6834d1d7c58066fb4b01f68efc4c4b7631" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memuse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d97bbf43eb4f088f8ca469930cde17fa036207c9a5e02ccc5107c4e8b17c964" + +[[package]] +name = "merkle_proof" +version = "0.2.0" +dependencies = [ + "alloy-primitives", + "ethereum_hashing 0.8.0", + "fixed_bytes 0.1.0", + "safe_arith", +] + +[[package]] +name = "merkle_proof" +version = "0.2.0" +source = "git+https://github.com/sigp/lighthouse?branch=unstable#c7055b604f9958db410b2e42023763cb19dd7138" +dependencies = [ + "alloy-primitives", + "ethereum_hashing 0.8.0", + "fixed_bytes 0.1.0 (git+https://github.com/sigp/lighthouse?branch=unstable)", + "safe_arith", +] + +[[package]] +name = "metastruct" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "969a1be9bd80794bdf93b23ab552c2ec6f3e83b33164824553fd996cdad513b8" +dependencies = [ + "metastruct_macro", +] + +[[package]] +name = "metastruct_macro" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de9164f767d73a507c19205868c84da411dc7795f4bdabf497d3dd93cfef9930" +dependencies = [ + "darling 0.23.0", + "itertools 0.14.0", + "proc-macro2", + "quote", + "smallvec", + "syn 2.0.117", +] + +[[package]] +name = "metrics" +version = "0.2.0" +dependencies = [ + "prometheus 0.13.4", +] + +[[package]] +name = "metrics" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8" +dependencies = [ + "ahash", + "portable-atomic", +] + +[[package]] +name = "metrics-exporter-prometheus" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7399781913e5393588a8d8c6a2867bf85fb38eaf2502fdce465aad2dc6f034" +dependencies = [ + "base64 0.22.1", + "http-body-util", + "hyper 1.8.1", + "hyper-rustls", + "hyper-util", + "indexmap 2.13.0", + "ipnet", + "metrics 0.24.3", + "metrics-util", + "quanta", + "thiserror 1.0.69", + "tokio", + "tracing", +] + +[[package]] +name = "metrics-util" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8496cc523d1f94c1385dd8f0f0c2c480b2b8aeccb5b7e4485ad6365523ae376" +dependencies = [ + "crossbeam-epoch 0.9.18", + "crossbeam-utils 0.8.21", + "hashbrown 0.15.5", + "metrics 0.24.3", + "quanta", + "rand 0.9.2", + "rand_xoshiro", + "sketches-ddsketch", +] + +[[package]] +name = "milhouse" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "259dd9da2ae5e0278b95da0b7ecef9c18c309d0a2d9e6db57ed33b9e8910c5e7" +dependencies = [ + "alloy-primitives", + "context_deserialize", + "educe", + "ethereum_hashing 0.8.0", + "ethereum_ssz 0.10.4", + "ethereum_ssz_derive 0.10.1", + "itertools 0.13.0", + "parking_lot", + "rayon", + "serde", + "smallvec", + "tree_hash 0.12.1", + "triomphe", + "typenum", + "vec_map", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "modular-bitfield" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a53d79ba8304ac1c4f9eb3b9d281f21f7be9d4626f72ce7df4ad8fbde4f38a74" +dependencies = [ + "modular-bitfield-impl", + "static_assertions", +] + +[[package]] +name = "modular-bitfield-impl" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a7d5f7076603ebc68de2dc6a650ec331a062a13abaa346975be747bbfa4b789" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "mpt" +version = "0.1.0" +source = "git+https://github.com/eth-act/zkvm-ethereum-mpt.git?rev=a1e44638c49c4e16751a0b915593fce98ab6bdef#a1e44638c49c4e16751a0b915593fce98ab6bdef" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "alloy-trie 0.8.1", + "arrayvec", +] + +[[package]] +name = "multiaddr" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6351f60b488e04c1d21bc69e56b89cb3f5e8f5d22557d6e8031bdfd79b6961" +dependencies = [ + "arrayref", + "byteorder", + "data-encoding", + "libp2p-identity", + "multibase", + "multihash", + "percent-encoding", + "serde", + "static_assertions", + "unsigned-varint", + "url", +] + +[[package]] +name = "multibase" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" +dependencies = [ + "base-x", + "base256emoji", + "data-encoding", + "data-encoding-macro", +] + +[[package]] +name = "multihash" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" +dependencies = [ + "core2", + "unsigned-varint", +] + +[[package]] +name = "munge" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint 0.4.6", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "serde", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint 0.4.6", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "nybbles" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983bb634df7248924ee0c4c3a749609b5abcb082c28fffe3254b3eb3602b307" +dependencies = [ + "const-hex", + "smallvec", +] + +[[package]] +name = "nybbles" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d49ff0c0d00d4a502b39df9af3a525e1efeb14b9dabb5bb83335284c1309210" +dependencies = [ + "alloy-rlp", + "cfg-if 1.0.4", + "proptest", + "ruint", + "serde", + "smallvec", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "op-alloy-consensus" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736381a95471d23e267263cfcee9e1d96d30b9754a94a2819148f83379de8a86" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "derive_more 2.1.1", + "serde", + "serde_with", + "thiserror 2.0.18", +] + +[[package]] +name = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags", + "cfg-if 1.0.4", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p3-bn254-fr" +version = "0.3.2-succinct" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9abf208fbfe540d6e2a6caaa2a9a345b1c8cb23ffdcdfcc6987244525d4fc821" +dependencies = [ + "ff 0.13.1", + "num-bigint 0.4.6", + "p3-field", + "p3-poseidon2", + "p3-symmetric", + "rand 0.8.5", + "serde", +] + +[[package]] +name = "p3-challenger" +version = "0.3.2-succinct" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b725b453bbb35117a1abf0ddfd900b0676063d6e4231e0fa6bb0d76018d8ad" +dependencies = [ + "p3-field", + "p3-maybe-rayon", + "p3-symmetric", + "p3-util", + "serde", + "tracing", +] + +[[package]] +name = "p3-dft" +version = "0.3.2-succinct" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56a1f81101bff744b7ebba7f4497e917a2c6716d6e62736e4a56e555a2d98cb7" +dependencies = [ + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-util", + "tracing", +] + +[[package]] +name = "p3-field" +version = "0.3.2-succinct" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36459d4acb03d08097d713f336c7393990bb489ab19920d4f68658c7a5c10968" +dependencies = [ + "itertools 0.12.1", + "num-bigint 0.4.6", + "num-traits", + "p3-util", + "rand 0.8.5", + "serde", +] + +[[package]] +name = "p3-koala-bear" +version = "0.3.2-succinct" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1f52bcb6be38bdc8fa6b38b3434d4eedd511f361d4249fd798c6a5ef817b40" +dependencies = [ + "num-bigint 0.4.6", + "p3-field", + "p3-mds", + "p3-poseidon2", + "p3-symmetric", + "rand 0.8.5", + "serde", +] + +[[package]] +name = "p3-matrix" +version = "0.3.2-succinct" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5583e9cd136a4095a25c41a9edfdcce2dfae58ef01639317813bdbbd5b55c583" +dependencies = [ + "itertools 0.12.1", + "p3-field", + "p3-maybe-rayon", + "p3-util", + "rand 0.8.5", + "serde", + "tracing", +] + +[[package]] +name = "p3-maybe-rayon" +version = "0.3.2-succinct" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e524d47a49fb4265611303339c4ef970d892817b006cc330dad18afb91e411b1" + +[[package]] +name = "p3-mds" +version = "0.3.2-succinct" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f6cb8edcb276033d43769a3725570c340d2ed6f35c3cca4cddeee07718fa376" +dependencies = [ + "itertools 0.12.1", + "p3-dft", + "p3-field", + "p3-matrix", + "p3-symmetric", + "p3-util", + "rand 0.8.5", +] + +[[package]] +name = "p3-poseidon2" +version = "0.3.2-succinct" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a26197df2097b98ab7038d59a01e1fe1a0f545e7e04aa9436b2454b1836654f" +dependencies = [ + "gcd", + "p3-field", + "p3-mds", + "p3-symmetric", + "rand 0.8.5", + "serde", +] + +[[package]] +name = "p3-symmetric" +version = "0.3.2-succinct" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1d3b5202096bca57cde912fbbb9cbaedaf5ac7c42a924c7166b98709d64d21" +dependencies = [ + "itertools 0.12.1", + "p3-field", + "serde", +] + +[[package]] +name = "p3-util" +version = "0.3.2-succinct" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec5f0388aa6d935ca3a17444086120f393f0b2f0816010b5ff95998c1c4095e3" +dependencies = [ + "serde", +] + +[[package]] +name = "pairing" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135590d8bdba2b31346f9cd1fb2a912329f5135e832a4f422942eb6ead8b6b3b" +dependencies = [ + "group 0.12.1", +] + +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group 0.13.0", +] + +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pasta_curves" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc65faf8e7313b4b1fbaa9f7ca917a0eed499a9663be71477f87993604341d8" +dependencies = [ + "blake2b_simd", + "ff 0.12.1", + "group 0.12.1", + "lazy_static", + "rand 0.8.5", + "static_assertions", + "subtle", +] + +[[package]] +name = "pasta_curves" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e57598f73cc7e1b2ac63c79c517b31a0877cd7c402cdcaa311b5208de7a095" +dependencies = [ + "blake2b_simd", + "ff 0.13.1", + "group 0.13.0", + "lazy_static", + "rand 0.8.5", + "static_assertions", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_reqwest_error" +version = "0.1.0" +dependencies = [ + "reqwest", + "sensitive_url", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "primitive-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" +dependencies = [ + "fixed-hash", + "impl-codec 0.6.0", + "uint 0.9.5", +] + +[[package]] +name = "primitive-types" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15600a7d856470b7d278b3fe0e311fe28c2526348549f8ef2ff7db3299c87f5" +dependencies = [ + "fixed-hash", + "impl-codec 0.7.1", + "impl-rlp", + "impl-serde", + "uint 0.10.0", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.8+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "procfs" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "731e0d9356b0c25f16f33b5be79b1c57b562f141ebfcdb0ad8ac2c13a24293b4" +dependencies = [ + "bitflags", + "hex", + "lazy_static", + "procfs-core", + "rustix 0.38.44", +] + +[[package]] +name = "procfs-core" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" +dependencies = [ + "bitflags", + "hex", +] + +[[package]] +name = "prometheus" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1" +dependencies = [ + "cfg-if 1.0.4", + "fnv", + "lazy_static", + "libc", + "memchr", + "parking_lot", + "procfs", + "protobuf 2.28.0", + "thiserror 1.0.69", +] + +[[package]] +name = "prometheus" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" +dependencies = [ + "cfg-if 1.0.4", + "fnv", + "lazy_static", + "memchr", + "parking_lot", + "protobuf 3.7.2", + "thiserror 2.0.18", +] + +[[package]] +name = "proof_engine_zkboost_test" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum 0.7.9", + "bytes", + "ethereum_ssz 0.10.4", + "execution_layer", + "futures", + "metrics-exporter-prometheus", + "reqwest", + "sensitive_url", + "serde", + "serde_json", + "strum", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "tree_hash 0.12.1", + "types 0.2.1", + "url", + "zkboost-server", + "zkboost-types", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift 0.4.0", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "proto_array" +version = "0.2.0" +dependencies = [ + "ethereum_ssz 0.10.4", + "ethereum_ssz_derive 0.10.1", + "fixed_bytes 0.1.0", + "safe_arith", + "serde", + "smallvec", + "superstruct", + "typenum", + "types 0.2.1", + "yaml_serde", +] + +[[package]] +name = "protobuf" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" + +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", +] + +[[package]] +name = "ptr_meta" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "qfilter" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "746341cd2357c9a4df2d951522b4a8dd1ef553e543119899ad7bf87e938c8fbe" +dependencies = [ + "xxhash-rust", +] + +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils 0.8.21", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.37", + "socket2 0.6.3", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls 0.23.37", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.3", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rancor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" +dependencies = [ + "ptr_meta", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "serde", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", + "serde", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", + "serde", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rapidhash" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" +dependencies = [ + "rustversion", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque 0.8.6", + "crossbeam-utils 0.8.21", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rend" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.4.13", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.37", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls 0.26.4", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "reqwest-eventsource" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632c55746dbb44275691640e7b40c907c16a2dc1a5842aa98aaec90da6ec6bde" +dependencies = [ + "eventsource-stream", + "futures-core", + "futures-timer", + "mime", + "nom", + "pin-project-lite", + "reqwest", + "thiserror 1.0.69", +] + +[[package]] +name = "reth-chainspec" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-chains", + "alloy-consensus", + "alloy-eips", + "alloy-evm", + "alloy-genesis", + "alloy-primitives", + "alloy-trie 0.9.5", + "auto_impl", + "derive_more 2.1.1", + "reth-ethereum-forks", + "reth-network-peers", + "reth-primitives-traits", + "serde_json", +] + +[[package]] +name = "reth-codecs" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-genesis", + "alloy-primitives", + "alloy-trie 0.9.5", + "bytes", + "modular-bitfield", + "op-alloy-consensus", + "reth-codecs-derive", + "reth-zstd-compressors", + "serde", +] + +[[package]] +name = "reth-codecs-derive" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "reth-consensus" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "auto_impl", + "reth-execution-types", + "reth-primitives-traits", + "thiserror 2.0.18", +] + +[[package]] +name = "reth-consensus-common" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "reth-chainspec", + "reth-consensus", + "reth-primitives-traits", +] + +[[package]] +name = "reth-db-models" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "reth-primitives-traits", +] + +[[package]] +name = "reth-errors" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "reth-consensus", + "reth-execution-errors", + "reth-storage-errors", + "thiserror 2.0.18", +] + +[[package]] +name = "reth-ethereum-consensus" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "reth-chainspec", + "reth-consensus", + "reth-consensus-common", + "reth-execution-types", + "reth-primitives-traits", + "tracing", +] + +[[package]] +name = "reth-ethereum-forks" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-eip2124", + "alloy-hardforks", + "alloy-primitives", + "auto_impl", + "once_cell", +] + +[[package]] +name = "reth-ethereum-primitives" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-eth", + "alloy-serde", + "reth-codecs", + "reth-primitives-traits", + "serde", + "serde_with", +] + +[[package]] +name = "reth-evm" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-evm", + "alloy-primitives", + "auto_impl", + "derive_more 2.1.1", + "futures-util", + "reth-execution-errors", + "reth-execution-types", + "reth-primitives-traits", + "reth-storage-api", + "reth-storage-errors", + "reth-trie-common", + "revm", +] + +[[package]] +name = "reth-evm-ethereum" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-evm", + "alloy-primitives", + "alloy-rpc-types-engine", + "reth-chainspec", + "reth-ethereum-forks", + "reth-ethereum-primitives", + "reth-evm", + "reth-execution-types", + "reth-primitives-traits", + "reth-storage-errors", + "revm", +] + +[[package]] +name = "reth-execution-errors" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-evm", + "alloy-primitives", + "alloy-rlp", + "nybbles 0.4.8", + "reth-storage-errors", + "thiserror 2.0.18", +] + +[[package]] +name = "reth-execution-types" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-evm", + "alloy-primitives", + "derive_more 2.1.1", + "reth-ethereum-primitives", + "reth-primitives-traits", + "reth-trie-common", + "revm", +] + +[[package]] +name = "reth-network-peers" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "serde_with", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "reth-payload-validator" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-consensus", + "alloy-rpc-types-engine", + "reth-primitives-traits", +] + +[[package]] +name = "reth-primitives-traits" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-genesis", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-eth", + "alloy-trie 0.9.5", + "auto_impl", + "bytes", + "derive_more 2.1.1", + "once_cell", + "op-alloy-consensus", + "reth-codecs", + "revm-bytecode", + "revm-primitives", + "revm-state", + "secp256k1", + "serde", + "serde_with", + "thiserror 2.0.18", +] + +[[package]] +name = "reth-prune-types" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-primitives", + "derive_more 2.1.1", + "strum", + "thiserror 2.0.18", +] + +[[package]] +name = "reth-revm" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-primitives", + "reth-primitives-traits", + "reth-storage-api", + "reth-storage-errors", + "revm", +] + +[[package]] +name = "reth-stages-types" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-primitives", + "reth-trie-common", +] + +[[package]] +name = "reth-stateless" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-consensus", + "alloy-genesis", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-debug", + "alloy-trie 0.9.5", + "itertools 0.14.0", + "k256", + "reth-chainspec", + "reth-consensus", + "reth-errors", + "reth-ethereum-consensus", + "reth-ethereum-primitives", + "reth-evm", + "reth-primitives-traits", + "reth-revm", + "reth-trie-common", + "reth-trie-sparse", + "serde", + "serde_with", + "thiserror 2.0.18", +] + +[[package]] +name = "reth-static-file-types" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-primitives", + "derive_more 2.1.1", + "fixed-map", + "serde", + "strum", +] + +[[package]] +name = "reth-storage-api" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rpc-types-engine", + "auto_impl", + "reth-chainspec", + "reth-db-models", + "reth-ethereum-primitives", + "reth-execution-types", + "reth-primitives-traits", + "reth-prune-types", + "reth-stages-types", + "reth-storage-errors", + "reth-trie-common", + "revm-database", +] + +[[package]] +name = "reth-storage-errors" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "derive_more 2.1.1", + "reth-primitives-traits", + "reth-prune-types", + "reth-static-file-types", + "revm-database-interface", + "revm-state", + "thiserror 2.0.18", +] + +[[package]] +name = "reth-trie-common" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "alloy-rlp", + "alloy-trie 0.9.5", + "derive_more 2.1.1", + "itertools 0.14.0", + "nybbles 0.4.8", + "reth-primitives-traits", + "revm-database", +] + +[[package]] +name = "reth-trie-sparse" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "alloy-trie 0.9.5", + "auto_impl", + "reth-execution-errors", + "reth-primitives-traits", + "reth-trie-common", + "smallvec", + "tracing", +] + +[[package]] +name = "reth-zstd-compressors" +version = "1.10.2" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.10.2#8e3b5e6a99439561b73c5dd31bd3eced2e994d60" +dependencies = [ + "zstd", +] + +[[package]] +name = "revm" +version = "34.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2aabdebaa535b3575231a88d72b642897ae8106cf6b0d12eafc6bfdf50abfc7" +dependencies = [ + "revm-bytecode", + "revm-context", + "revm-context-interface", + "revm-database", + "revm-database-interface", + "revm-handler", + "revm-inspector", + "revm-interpreter", + "revm-precompile", + "revm-primitives", + "revm-state", +] + +[[package]] +name = "revm-bytecode" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d1e5c1eaa44d39d537f668bc5c3409dc01e5c8be954da6c83370bbdf006457" +dependencies = [ + "bitvec", + "phf", + "revm-primitives", + "serde", +] + +[[package]] +name = "revm-context" +version = "13.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "892ff3e6a566cf8d72ffb627fdced3becebbd9ba64089c25975b9b028af326a5" +dependencies = [ + "bitvec", + "cfg-if 1.0.4", + "derive-where", + "revm-bytecode", + "revm-context-interface", + "revm-database-interface", + "revm-primitives", + "revm-state", +] + +[[package]] +name = "revm-context-interface" +version = "14.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f61cc6d23678c4840af895b19f8acfbbd546142ec8028b6526c53cc1c16c98" +dependencies = [ + "alloy-eip2930", + "alloy-eip7702", + "auto_impl", + "either", + "revm-database-interface", + "revm-primitives", + "revm-state", +] + +[[package]] +name = "revm-database" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "529528d0b05fe646be86223032c3e77aa8b05caa2a35447d538c55965956a511" +dependencies = [ + "revm-bytecode", + "revm-database-interface", + "revm-primitives", + "revm-state", +] + +[[package]] +name = "revm-database-interface" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7bf93ac5b91347c057610c0d96e923db8c62807e03f036762d03e981feddc1d" +dependencies = [ + "auto_impl", + "either", + "revm-primitives", + "revm-state", + "thiserror 2.0.18", +] + +[[package]] +name = "revm-handler" +version = "15.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cd0e43e815a85eded249df886c4badec869195e70cdd808a13cfca2794622d2" +dependencies = [ + "auto_impl", + "derive-where", + "revm-bytecode", + "revm-context", + "revm-context-interface", + "revm-database-interface", + "revm-interpreter", + "revm-precompile", + "revm-primitives", + "revm-state", +] + +[[package]] +name = "revm-inspector" +version = "15.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3ccad59db91ef93696536a0dbaf2f6f17cfe20d4d8843ae118edb7e97947ef" +dependencies = [ + "auto_impl", + "either", + "revm-context", + "revm-database-interface", + "revm-handler", + "revm-interpreter", + "revm-primitives", + "revm-state", +] + +[[package]] +name = "revm-interpreter" +version = "32.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11406408597bc249392d39295831c4b641b3a6f5c471a7c41104a7a1e3564c07" +dependencies = [ + "revm-bytecode", + "revm-context-interface", + "revm-primitives", + "revm-state", +] + +[[package]] +name = "revm-precompile" +version = "32.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ec11f45deec71e4945e1809736bb20d454285f9167ab53c5159dae1deb603f" +dependencies = [ + "ark-bls12-381", + "ark-bn254", + "ark-ec", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "arrayref", + "aurora-engine-modexp", + "cfg-if 1.0.4", + "k256", + "p256", + "revm-primitives", + "ripemd", + "sha2", +] + +[[package]] +name = "revm-primitives" +version = "22.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfb5ce6cf18b118932bcdb7da05cd9c250f2cb9f64131396b55f3fe3537c35" +dependencies = [ + "alloy-primitives", + "num_enum", + "once_cell", + "serde", +] + +[[package]] +name = "revm-state" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "311720d4f0f239b041375e7ddafdbd20032a33b7bae718562ea188e188ed9fd3" +dependencies = [ + "alloy-eip7928", + "bitflags", + "revm-bytecode", + "revm-primitives", + "serde", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if 1.0.4", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "rkyv" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a30e631b7f4a03dee9056b8ef6982e8ba371dd5bedb74d3ec86df4499132c70" +dependencies = [ + "bytecheck", + "bytes", + "hashbrown 0.16.1", + "indexmap 2.13.0", + "munge", + "ptr_meta", + "rancor", + "rend", + "rkyv_derive", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8100bb34c0a1d0f907143db3149e6b4eea3c33b9ee8b189720168e818303986f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rustc-hex", +] + +[[package]] +name = "rlp" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa24e92bb2a83198bb76d661a71df9f7076b8c420b8696e4d3d97d50d94479e3" +dependencies = [ + "bytes", + "rustc-hex", +] + +[[package]] +name = "rpds" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ef5140bcb576bfd6d56cd2de709a7d17851ac1f3805e67fe9d99e42a11821f" +dependencies = [ + "archery", +] + +[[package]] +name = "ruint" +version = "1.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a" +dependencies = [ + "alloy-rlp", + "ark-ff 0.3.0", + "ark-ff 0.4.2", + "ark-ff 0.5.0", + "bytes", + "fastrlp 0.3.1", + "fastrlp 0.4.0", + "num-bigint 0.4.6", + "num-integer", + "num-traits", + "parity-scale-codec", + "primitive-types 0.12.2", + "proptest", + "rand 0.8.5", + "rand 0.9.2", + "rlp 0.5.2", + "ruint-macro", + "serde_core", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + +[[package]] +name = "rust_eth_kzg" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1522b7a740cd7f5bc52ea49863618511c8de138dcdf3f8a80b15b3f764942a5b" +dependencies = [ + "eip4844", + "ekzg-bls12-381", + "ekzg-erasure-codes", + "ekzg-multi-open", + "ekzg-serialization", + "ekzg-trusted-setup", + "hex", + "serde", + "serde_json", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver 0.11.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver 1.0.27", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "aws-lc-rs", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.103.10", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "safe_arith" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b147bb6111014916d3ef9d4c85173124a8e12193a67f6176d67244afd558d6c1" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.5", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "semver-parser" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" +dependencies = [ + "pest", +] + +[[package]] +name = "sensitive_url" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7b0221fa9905eec4163dbf7660b1876cc95663af1deddc3e19ebe49167c58c" +dependencies = [ + "serde", + "url", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_arrays" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a16b99c5ea4fe3daccd14853ad260ec00ea043b2708d1fd1da3106dcd8d9df" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_spanned" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.13.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if 1.0.4", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if 1.0.4", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "sha3-asm" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b31139435f327c93c6038ed350ae4588e2c70a13d50599509fee6349967ba35a" +dependencies = [ + "cc", + "cfg-if 1.0.4", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint 0.4.6", + "num-traits", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "sketches-ddsketch" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slop-algebra" +version = "6.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "691beea96fd18d4881f9ca1cb4e58194dac6366f24956a2fdae00c8ee382a0c9" +dependencies = [ + "itertools 0.14.0", + "p3-field", + "serde", +] + +[[package]] +name = "slop-bn254" +version = "6.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1852499c245f7f3dec23408b4930b3ea7570ae914b9c31f12950ac539d85ee" +dependencies = [ + "ff 0.13.1", + "p3-bn254-fr", + "serde", + "slop-algebra", + "slop-challenger", + "slop-poseidon2", + "slop-symmetric", + "zkhash", +] + +[[package]] +name = "slop-challenger" +version = "6.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4349af93602f3876a3eda948a74d9d16d774c401dfe25f41a45ffd84f230bc1" +dependencies = [ + "futures", + "p3-challenger", + "serde", + "slop-algebra", + "slop-symmetric", +] + +[[package]] +name = "slop-koala-bear" +version = "6.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "574784c044d11cf9d8238dc18bce9b897bc34d0fb1daaceafd75ebb400084016" +dependencies = [ + "lazy_static", + "p3-koala-bear", + "serde", + "slop-algebra", + "slop-challenger", + "slop-poseidon2", + "slop-symmetric", +] + +[[package]] +name = "slop-poseidon2" +version = "6.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5af617970b63e8d7199204bc02996745b6c35c39f2b513a118c62c7b1a0b2f1b" +dependencies = [ + "p3-poseidon2", +] + +[[package]] +name = "slop-primitives" +version = "6.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58d82c53508f3ebff8acdabb5db2584f37686257a2549a17c977cf30cd9e24e6" +dependencies = [ + "slop-algebra", +] + +[[package]] +name = "slop-symmetric" +version = "6.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15acfa7f567ffa4f36de134492632a397c33fa6af2e48894e50978b52eeeb871" +dependencies = [ + "p3-symmetric", +] + +[[package]] +name = "slot_clock" +version = "0.2.0" +dependencies = [ + "metrics 0.2.0", + "parking_lot", + "types 0.2.1", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "snap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "sp1-lib" +version = "6.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "517e820776910468611149dda66791bdb700c1b7d68b96f0ea2e604f00ad8771" +dependencies = [ + "bincode 1.3.3", + "serde", + "sp1-primitives", +] + +[[package]] +name = "sp1-primitives" +version = "6.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f395525b4fc46d37136f45be264c81718a67f4409c14c547ff491a263e019e7" +dependencies = [ + "bincode 1.3.3", + "blake3", + "elf", + "hex", + "itertools 0.14.0", + "lazy_static", + "num-bigint 0.4.6", + "serde", + "sha2", + "slop-algebra", + "slop-bn254", + "slop-challenger", + "slop-koala-bear", + "slop-poseidon2", + "slop-primitives", + "slop-symmetric", +] + +[[package]] +name = "sp1_bls12_381" +version = "0.8.0-sp1-6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f23e41cd36168cc2e51e5d3e35ff0c34b204d945769a65591a76286d04b51e43" +dependencies = [ + "cfg-if 1.0.4", + "ff 0.13.1", + "group 0.13.0", + "pairing 0.23.0", + "rand_core 0.6.4", + "sp1-lib", + "subtle", +] + +[[package]] +name = "sparsestate" +version = "0.1.0" +source = "git+https://github.com/eth-act/zkvm-ethereum-mpt.git?rev=a1e44638c49c4e16751a0b915593fce98ab6bdef#a1e44638c49c4e16751a0b915593fce98ab6bdef" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "alloy-trie 0.9.5", + "mpt", + "reth-errors", + "reth-revm", + "reth-stateless", + "reth-trie-common", +] + +[[package]] +name = "spawned-concurrency" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3ec6b3c003075f7d1c4c6475308243e853c9a78149b84b1f8b64d5bed49d49" +dependencies = [ + "futures", + "pin-project-lite", + "spawned-rt", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "spawned-rt" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cca60c56b1c60b94dd314edce5ea1a98b6037cca3b44d73828e647bad4dae46c" +dependencies = [ + "crossbeam 0.7.3", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "ssz_types" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b55bedc9a18ed2860a46d6beb4f4082416ee1d60be0cc364cebdcdddc7afd4" +dependencies = [ + "ethereum_serde_utils", + "ethereum_ssz 0.9.1", + "itertools 0.13.0", + "serde", + "serde_derive", + "smallvec", + "tree_hash 0.10.0", + "typenum", +] + +[[package]] +name = "ssz_types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d625e4de8e0057eefe7e0b1510ba1dd7adf10cd375fad6cc7fcceac7c39623c9" +dependencies = [ + "context_deserialize", + "educe", + "ethereum_serde_utils", + "ethereum_ssz 0.10.4", + "itertools 0.14.0", + "serde", + "serde_derive", + "smallvec", + "tree_hash 0.12.1", + "typenum", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "state_processing" +version = "0.2.0" +dependencies = [ + "bls 0.2.0", + "educe", + "ethereum_hashing 0.8.0", + "ethereum_ssz 0.10.4", + "ethereum_ssz_derive 0.10.1", + "fixed_bytes 0.1.0", + "int_to_bytes 0.2.0", + "integer-sqrt", + "itertools 0.14.0", + "merkle_proof 0.2.0", + "metrics 0.2.0", + "milhouse", + "rand 0.9.2", + "rayon", + "safe_arith", + "smallvec", + "ssz_types 0.14.1", + "tracing", + "tree_hash 0.12.1", + "typenum", + "types 0.2.1", +] + +[[package]] +name = "stateless-validator-common" +version = "0.5.0" +source = "git+https://github.com/eth-act/ere-guests?tag=v0.6.0#64c94bb3da631101a6cb2f276c89392cb7c3426f" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "anyhow", + "ethereum_ssz 0.9.1", + "ethereum_ssz_derive 0.9.1", + "rkyv", + "serde", + "serde_with", + "sha2", + "ssz_types 0.11.0", + "tree_hash 0.10.0", + "tree_hash_derive 0.10.0", + "typenum", +] + +[[package]] +name = "stateless-validator-ethrex" +version = "0.5.0" +source = "git+https://github.com/eth-act/ere-guests?tag=v0.6.0#64c94bb3da631101a6cb2f276c89392cb7c3426f" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-genesis", + "alloy-rlp", + "anyhow", + "bytes", + "ere-io", + "ere-zkvm-interface", + "ethrex-common", + "ethrex-rlp", + "ethrex-rpc", + "ethrex-vm", + "guest", + "guest_program", + "reth-stateless", + "rkyv", + "stateless-validator-common", + "stateless-validator-reth", +] + +[[package]] +name = "stateless-validator-reth" +version = "0.5.0" +source = "git+https://github.com/eth-act/ere-guests?tag=v0.6.0#64c94bb3da631101a6cb2f276c89392cb7c3426f" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-genesis", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-engine", + "anyhow", + "ere-io", + "ere-zkvm-interface", + "ethereum_ssz 0.9.1", + "guest", + "once_cell", + "reth-chainspec", + "reth-ethereum-primitives", + "reth-evm-ethereum", + "reth-payload-validator", + "reth-primitives-traits", + "reth-stateless", + "serde", + "serde_with", + "sha2", + "sparsestate", + "ssz_types 0.11.0", + "stateless-validator-common", + "tree_hash 0.10.0", + "tree_hash_derive 0.10.0", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "superstruct" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae4a9ccd7882533c1f210e400763ec6ee64c390fc12248c238276281863719e" +dependencies = [ + "darling 0.23.0", + "itertools 0.14.0", + "proc-macro2", + "quote", + "smallvec", + "syn 2.0.117", +] + +[[package]] +name = "swap_or_not_shuffle" +version = "0.2.0" +dependencies = [ + "alloy-primitives", + "ethereum_hashing 0.8.0", + "fixed_bytes 0.1.0", +] + +[[package]] +name = "swap_or_not_shuffle" +version = "0.2.0" +source = "git+https://github.com/sigp/lighthouse?branch=unstable#c7055b604f9958db410b2e42023763cb19dd7138" +dependencies = [ + "alloy-primitives", + "ethereum_hashing 0.8.0", + "fixed_bytes 0.1.0 (git+https://github.com/sigp/lighthouse?branch=unstable)", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn-solidity" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53f425ae0b12e2f5ae65542e00898d500d4d318b4baf09f40fd0d410454e9947" +dependencies = [ + "paste", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "task_executor" +version = "0.1.0" +dependencies = [ + "async-channel", + "futures", + "metrics 0.2.0", + "num_cpus", + "rayon", + "tokio", + "tracing", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "test_random_derive" +version = "0.2.0" +source = "git+https://github.com/sigp/lighthouse?branch=unstable#c7055b604f9958db410b2e42023763cb19dd7138" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if 1.0.4", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.3", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.37", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "futures-util", + "pin-project-lite", + "slab", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.24.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01f2eadbbc6b377a847be05f60791ef1058d9f696ecb51d2c07fe911d8569d8e" +dependencies = [ + "indexmap 2.13.0", + "serde_core", + "serde_spanned", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 1.1.0+spec-1.1.0", + "toml_parser", + "winnow 1.0.0", +] + +[[package]] +name = "toml_parser" +version = "1.1.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +dependencies = [ + "winnow 1.0.0", +] + +[[package]] +name = "toml_writer" +version = "1.1.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" +dependencies = [ + "crossbeam-channel 0.5.15", + "thiserror 2.0.18", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "tree_hash" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee44f4cef85f88b4dea21c0b1f58320bdf35715cf56d840969487cff00613321" +dependencies = [ + "alloy-primitives", + "ethereum_hashing 0.7.0", + "ethereum_ssz 0.9.1", + "smallvec", + "typenum", +] + +[[package]] +name = "tree_hash" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fd51aa83d2eb83b04570808430808b5d24fdbf479a4d5ac5dee4a2e2dd2be4" +dependencies = [ + "alloy-primitives", + "ethereum_hashing 0.8.0", + "ethereum_ssz 0.10.4", + "smallvec", + "typenum", +] + +[[package]] +name = "tree_hash_derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bee2ea1551f90040ab0e34b6fb7f2fa3bad8acc925837ac654f2c78a13e3089" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tree_hash_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8840ad4d852e325d3afa7fde8a50b2412f89dce47d7eb291c0cc7f87cd040f38" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "triehash" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1631b201eb031b563d2e85ca18ec8092508e262a3196ce9bd10a67ec87b9f5c" +dependencies = [ + "hash-db", + "rlp 0.5.2", +] + +[[package]] +name = "triomphe" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" +dependencies = [ + "serde", + "stable_deref_trait", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http 1.4.0", + "httparse", + "log", + "rand 0.9.2", + "sha1", + "thiserror 2.0.18", + "utf-8", +] + +[[package]] +name = "twirp" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c52cc4e4423b6b3e2e2659523c8c9e19af514a06422fe77a95d86f6bf3478a" +dependencies = [ + "anyhow", + "async-trait", + "axum 0.8.8", + "futures", + "http 1.4.0", + "http-body-util", + "hyper 1.8.1", + "prost", + "reqwest", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tower", + "url", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "types" +version = "0.2.1" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "bls 0.2.0", + "compare_fields", + "context_deserialize", + "educe", + "eth2_interop_keypairs 0.2.0", + "ethereum_hashing 0.8.0", + "ethereum_serde_utils", + "ethereum_ssz 0.10.4", + "ethereum_ssz_derive 0.10.1", + "fixed_bytes 0.1.0", + "hex", + "int_to_bytes 0.2.0", + "itertools 0.14.0", + "kzg 0.1.0", + "maplit", + "merkle_proof 0.2.0", + "metastruct", + "milhouse", + "parking_lot", + "paste", + "rand 0.9.2", + "rand_xorshift 0.4.0", + "rayon", + "regex", + "rpds", + "safe_arith", + "serde", + "serde_json", + "smallvec", + "ssz_types 0.14.1", + "superstruct", + "swap_or_not_shuffle 0.2.0", + "tempfile", + "tracing", + "tree_hash 0.12.1", + "tree_hash_derive 0.12.1", + "typenum", + "yaml_serde", +] + +[[package]] +name = "types" +version = "0.2.1" +source = "git+https://github.com/sigp/lighthouse?branch=unstable#c7055b604f9958db410b2e42023763cb19dd7138" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "bls 0.2.0 (git+https://github.com/sigp/lighthouse?branch=unstable)", + "compare_fields", + "context_deserialize", + "educe", + "eth2_interop_keypairs 0.2.0 (git+https://github.com/sigp/lighthouse?branch=unstable)", + "ethereum_hashing 0.8.0", + "ethereum_serde_utils", + "ethereum_ssz 0.10.4", + "ethereum_ssz_derive 0.10.1", + "fixed_bytes 0.1.0 (git+https://github.com/sigp/lighthouse?branch=unstable)", + "hex", + "int_to_bytes 0.2.0 (git+https://github.com/sigp/lighthouse?branch=unstable)", + "itertools 0.14.0", + "kzg 0.1.0 (git+https://github.com/sigp/lighthouse?branch=unstable)", + "maplit", + "merkle_proof 0.2.0 (git+https://github.com/sigp/lighthouse?branch=unstable)", + "metastruct", + "milhouse", + "parking_lot", + "rand 0.9.2", + "rand_xorshift 0.4.0", + "rayon", + "regex", + "rpds", + "safe_arith", + "serde", + "serde_json", + "serde_yaml", + "smallvec", + "ssz_types 0.14.1", + "superstruct", + "swap_or_not_shuffle 0.2.0 (git+https://github.com/sigp/lighthouse?branch=unstable)", + "tempfile", + "test_random_derive", + "tracing", + "tree_hash 0.12.1", + "tree_hash_derive 0.12.1", + "typenum", +] + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "uint" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da36089a805484bcccfffe0739803392c8298778a2d2f09febf76fac5ad9025b" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "unsigned-varint" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "warp" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4378d202ff965b011c64817db11d5829506d3404edeadb61f190d111da3f231c" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "headers 0.3.9", + "http 0.2.12", + "hyper 0.14.32", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project", + "rustls-pemfile", + "scoped-tls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-rustls 0.25.0", + "tokio-util", + "tower-service", + "tracing", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if 1.0.4", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if 1.0.4", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver 1.0.27", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "wide" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver 1.0.27", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "workspace_members" +version = "0.1.0" +dependencies = [ + "cargo_metadata", + "quote", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + +[[package]] +name = "yaml_serde" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c7c1b1a6a7c8a6b2741a6c21a4f8918e51899b111cfa08d1288202656e3975" +dependencies = [ + "indexmap 2.13.0", + "itoa", + "libyaml-rs", + "ryu", + "serde", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "serde", + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zkboost-server" +version = "0.1.0" +source = "git+https://github.com/eth-act/zkboost?branch=master#cbeae7023bf32b4441751c76fc5d2f400524153a" +dependencies = [ + "alloy-eips", + "alloy-genesis", + "alloy-primitives", + "alloy-rpc-types-engine", + "alloy-rpc-types-eth", + "anyhow", + "axum 0.8.8", + "bincode 1.3.3", + "bytes", + "clap", + "ere-server", + "ere-zkvm-interface", + "lru 0.12.5", + "metrics 0.24.3", + "metrics-exporter-prometheus", + "rand 0.9.2", + "reqwest", + "reth-ethereum-primitives", + "reth-stateless", + "serde", + "serde_json", + "sha2", + "stateless-validator-ethrex", + "stateless-validator-reth", + "strum", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tokio-util", + "toml_edit 0.24.1+spec-1.1.0", + "tower-http", + "tracing", + "tracing-subscriber", + "url", + "zkboost-types", +] + +[[package]] +name = "zkboost-types" +version = "0.1.0" +source = "git+https://github.com/eth-act/zkboost?branch=master#cbeae7023bf32b4441751c76fc5d2f400524153a" +dependencies = [ + "ethereum_ssz 0.10.4", + "ethereum_ssz_derive 0.10.1", + "serde", + "serde_json", + "ssz_types 0.14.1", + "strum", + "superstruct", + "tree_hash 0.12.1", + "tree_hash_derive 0.12.1", + "types 0.2.1 (git+https://github.com/sigp/lighthouse?branch=unstable)", +] + +[[package]] +name = "zkhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4352d1081da6922701401cdd4cbf29a2723feb4cfabb5771f6fee8e9276da1c7" +dependencies = [ + "ark-ff 0.4.2", + "ark-std 0.4.0", + "bitvec", + "blake2", + "bls12_381 0.7.1", + "byteorder", + "cfg-if 1.0.4", + "group 0.12.1", + "group 0.13.0", + "halo2", + "hex", + "jubjub", + "lazy_static", + "pasta_curves 0.5.1", + "rand 0.8.5", + "serde", + "sha2", + "sha3", + "subtle", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/testing/proof_engine_zkboost/Cargo.toml b/testing/proof_engine_zkboost/Cargo.toml new file mode 100644 index 00000000000..1201889070f --- /dev/null +++ b/testing/proof_engine_zkboost/Cargo.toml @@ -0,0 +1,38 @@ +[workspace] +members = ["."] +resolver = "2" + +[workspace.dependencies] +zkboost-server = { git = "https://github.com/eth-act/zkboost", branch = "master" } +zkboost-types = { git = "https://github.com/eth-act/zkboost", branch = "master" } + +[package] +name = "proof_engine_zkboost_test" +version = "0.1.0" +edition = "2024" + +[features] +portable = ["types/portable"] + +[dependencies] +anyhow = "1" +axum = "0.7" +bytes = "1" +ethereum_ssz = { version = "0.10.4", features = ["context_deserialize"] } +execution_layer = { path = "../../beacon_node/execution_layer" } +futures = "0.3" +metrics-exporter-prometheus = "0.16" +reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "stream", "rustls-tls"] } +sensitive_url = { version = "0.1", features = ["serde"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +strum = { version = "0.27", features = ["derive"] } +tokio = { version = "1", features = ["rt-multi-thread", "sync", "signal", "macros"] } +tokio-stream = { version = "0.1", features = ["sync"] } +tokio-util = { version = "0.7", features = ["codec", "compat", "time"] } +tracing = "0.1" +tree_hash = "0.12.0" +types = { path = "../../consensus/types" } +url = "2" +zkboost-server = { workspace = true } +zkboost-types = { workspace = true } diff --git a/testing/proof_engine_zkboost/src/lib.rs b/testing/proof_engine_zkboost/src/lib.rs new file mode 100644 index 00000000000..2ba7f4e985d --- /dev/null +++ b/testing/proof_engine_zkboost/src/lib.rs @@ -0,0 +1,306 @@ +//! Integration tests verifying wire-level compatibility between Lighthouse's +//! [`HttpProofNodeClient`] and the **real** zkBoost server. +//! +//! ## Architecture +//! +//! This test crate starts the real `zkBoostServer` (from the `zkboost-server` crate) +//! with mock zkVM backends, and validates that Lighthouse's `HttpProofNodeClient` +//! speaks the correct wire protocol against it. +//! +//! A lightweight mock Execution Layer serves fixture data (chain config + +//! execution witness) so the server can generate witnesses without a real node. +//! +//! ## What is validated +//! +//! - Lighthouse sends zkBoost string proof types in query params, URL paths, SSE +//! - The real server accepts Lighthouse's requests and returns valid responses +//! - SSE events with string `proof_type` values are correctly deserialized to u8 +//! - Full lifecycle: request → SSE event → proof download → verification + +pub mod zkboost_harness; + +#[cfg(test)] +mod tests { + use crate::zkboost_harness::{FIXTURE_NEW_PAYLOAD_REQUEST, ZkboostTestHarness}; + use execution_layer::eip8025::{HttpProofNodeClient, ProofNodeClient, ProofType}; + use execution_layer::test_utils::OwnedNewPayloadRequest; + use futures::StreamExt; + use sensitive_url::SensitiveUrl; + use ssz::Decode; + use std::time::Duration; + use tokio::time::timeout; + use tree_hash::TreeHash; + use types::MainnetEthSpec; + use types::execution::eip8025::ProofAttributes; + use zkboost_types::ProofType as ZkBoostProofType; + + /// Helper: create an `HttpProofNodeClient` pointing at the test server. + fn client_for(url: &str) -> HttpProofNodeClient { + let sensitive_url = SensitiveUrl::parse(url).expect("server URL should be valid"); + HttpProofNodeClient::new(sensitive_url, None) + } + + /// The u8 value for `EthrexZisk` (our default test proof type). + fn ethrex_zisk_u8() -> u8 { + ProofType::EthrexZisk.to_u8() + } + + // ─── Test 1: request_proofs succeeds against real server ───────────────── + + /// Verifies that `HttpProofNodeClient::request_proofs` sends the correct + /// wire format (string proof types in query param, SSZ body) and the real + /// zkBoost server accepts it and returns a root. + #[tokio::test] + async fn test_request_proofs_accepted_by_real_server() { + let harness = ZkboostTestHarness::start(3000).await; + let client = client_for(&harness.url()); + + let attrs = ProofAttributes { + proof_types: vec![ethrex_zisk_u8()], + }; + + let root = client + .request_proofs(FIXTURE_NEW_PAYLOAD_REQUEST.to_vec(), attrs) + .await + .expect("request_proofs should succeed against real server"); + + let expected_root = + OwnedNewPayloadRequest::::from_ssz_bytes(FIXTURE_NEW_PAYLOAD_REQUEST) + .expect("fixture SSZ should decode to a valid NewPayloadRequest") + .tree_hash_root(); + + assert_eq!( + root, expected_root, + "server root should match tree_hash_root of fixture payload" + ); + } + + // ─── Test 2: SSE events from real server are parsed correctly ──────────── + + /// Verifies that SSE events from the real zkBoost server (which use string + /// proof types like `"ethrex-zisk"`) are correctly deserialized by + /// Lighthouse's client back to u8 values. + #[tokio::test] + async fn test_sse_events_from_real_server() { + let harness = ZkboostTestHarness::start(1000).await; + let client = client_for(&harness.url()); + + let attrs = ProofAttributes { + proof_types: vec![ethrex_zisk_u8()], + }; + + // Subscribe to events before requesting proofs. + let mut event_stream = client.subscribe_proof_events(None); + + let root = client + .request_proofs(FIXTURE_NEW_PAYLOAD_REQUEST.to_vec(), attrs) + .await + .expect("request_proofs should succeed"); + + // Wait for a proof event from the real server. + let event = timeout(Duration::from_secs(30), event_stream.next()) + .await + .expect("timed out waiting for SSE event") + .expect("stream ended") + .expect("stream error"); + + assert_eq!(event.new_payload_request_root(), root); + assert_eq!( + event.proof_type(), + ethrex_zisk_u8(), + "string 'ethrex-zisk' from real server should deserialize to u8 {}", + ethrex_zisk_u8() + ); + } + + // ─── Test 3: get_proof downloads proof from real server ────────────────── + + /// Verifies that `get_proof` uses the string proof type in the URL path + /// and successfully downloads a proof from the real server after completion. + #[tokio::test] + async fn test_get_proof_from_real_server() { + let harness = ZkboostTestHarness::start(1000).await; + let client = client_for(&harness.url()); + + let attrs = ProofAttributes { + proof_types: vec![ethrex_zisk_u8()], + }; + + // Subscribe and wait for proof completion. + let mut events = client.subscribe_proof_events(None); + + let root = client + .request_proofs(FIXTURE_NEW_PAYLOAD_REQUEST.to_vec(), attrs) + .await + .expect("request should succeed"); + + // Wait for proof_complete event. + let _event = timeout(Duration::from_secs(30), events.next()) + .await + .expect("timed out waiting for event") + .expect("stream ended") + .expect("stream error"); + + // Download the proof using string proof type in URL path. + let proof_bytes = client + .get_proof(root, ethrex_zisk_u8()) + .await + .expect("get_proof should succeed with string proof type in URL"); + + assert!(!proof_bytes.is_empty(), "proof should not be empty"); + } + + // ─── Test 4: verify_proof against real server ──────────────────────────── + + /// Verifies that `verify_proof` sends the string proof type in query params + /// and the real server accepts the verification request. + #[tokio::test] + async fn test_verify_proof_against_real_server() { + let harness = ZkboostTestHarness::start(1000).await; + let client = client_for(&harness.url()); + + let attrs = ProofAttributes { + proof_types: vec![ethrex_zisk_u8()], + }; + + let mut events = client.subscribe_proof_events(None); + + let root = client + .request_proofs(FIXTURE_NEW_PAYLOAD_REQUEST.to_vec(), attrs) + .await + .expect("request should succeed"); + + // Wait for completion. + let _event = timeout(Duration::from_secs(30), events.next()) + .await + .expect("timed out") + .expect("stream ended") + .expect("stream error"); + + // Download proof. + let proof = client + .get_proof(root, ethrex_zisk_u8()) + .await + .expect("get_proof should succeed"); + + // Verify proof. + let status = client + .verify_proof(root, ethrex_zisk_u8(), &proof) + .await + .expect("verify_proof should succeed against real server"); + + assert_eq!(status, types::execution::eip8025::ProofStatus::Valid); + } + + // ─── Test 5: invalid u8 proof type is rejected by client ───────────────── + + /// Verifies that an unmapped u8 value (e.g. 99) fails at the Lighthouse + /// client level before even reaching the server. + #[tokio::test] + async fn test_invalid_proof_type_rejected_by_client() { + let harness = ZkboostTestHarness::start(0).await; + let client = client_for(&harness.url()); + + let result = client + .get_proof(types::Hash256::repeat_byte(0xAA), 99) + .await; + assert!( + result.is_err(), + "u8 value 99 has no zkBoost mapping — should error at client level" + ); + } + + // ─── Test 6: ZkBoostProofType matches zkboost-types::ProofType ────────── + + /// Validates that Lighthouse's `ZkBoostProofType` enum covers all known + /// zkBoost proof types with matching string representations. + #[tokio::test] + async fn test_zkboost_proof_type_matches_upstream() { + // Collect all upstream ProofType variants. + let upstream: Vec<(String, usize)> = ProofType::all() + .iter() + .enumerate() + .map(|(i, pt)| (pt.as_str().to_string(), i)) + .collect(); + + // Verify Lighthouse's ZkBoostProofType has matching variants. + for (s, i) in &upstream { + let pt: ZkBoostProofType = s + .parse() + .unwrap_or_else(|_| panic!("'{s}' should parse as ZkBoostProofType")); + assert_eq!( + pt.as_str(), + s.as_str(), + "string representation should match upstream" + ); + assert_eq!( + pt as u8, *i as u8, + "u8 mapping for '{s}' should match upstream ordinal {i}" + ); + } + + // Verify all Lighthouse variants are in the upstream list. + let upstream_strs: Vec<&str> = upstream.iter().map(|(s, _)| s.as_str()).collect(); + for pt in ProofType::all() { + assert!( + upstream_strs.contains(&pt.as_str()), + "Lighthouse variant {:?} should exist in upstream zkBoost", + pt + ); + } + + // Counts should match. + assert_eq!( + ProofType::all().len(), + upstream.len(), + "variant count should match between Lighthouse and zkBoost" + ); + } + + // ─── Test 7: full lifecycle (request → SSE → download → verify) ───────── + + /// End-to-end lifecycle against the real zkBoost server. + #[tokio::test] + async fn test_full_lifecycle_against_real_server() { + let harness = ZkboostTestHarness::start(1000).await; + let client = client_for(&harness.url()); + + let attrs = ProofAttributes { + proof_types: vec![ethrex_zisk_u8()], + }; + + let mut events = client.subscribe_proof_events(None); + + // Step 1: Request proof. + let root = client + .request_proofs(FIXTURE_NEW_PAYLOAD_REQUEST.to_vec(), attrs) + .await + .expect("request should succeed"); + + assert!(!root.is_zero()); + + // Step 2: Wait for SSE proof_complete event. + let event = timeout(Duration::from_secs(30), events.next()) + .await + .expect("timed out waiting for event") + .expect("stream ended") + .expect("stream error"); + + assert_eq!(event.new_payload_request_root(), root); + assert_eq!(event.proof_type(), ethrex_zisk_u8()); + + // Step 3: Download proof. + let proof = client + .get_proof(root, ethrex_zisk_u8()) + .await + .expect("get_proof should succeed"); + assert!(!proof.is_empty()); + + // Step 4: Verify proof. + let status = client + .verify_proof(root, ethrex_zisk_u8(), &proof) + .await + .expect("verify_proof should succeed"); + assert_eq!(status, types::execution::eip8025::ProofStatus::Valid); + } +} diff --git a/testing/proof_engine_zkboost/src/zkboost_harness.rs b/testing/proof_engine_zkboost/src/zkboost_harness.rs new file mode 100644 index 00000000000..7e37fb2ca57 --- /dev/null +++ b/testing/proof_engine_zkboost/src/zkboost_harness.rs @@ -0,0 +1,173 @@ +//! Test harness that starts the **real** zkBoost server with mock zkVM backends. +//! +//! This validates that Lighthouse's [`HttpProofNodeClient`] speaks the correct +//! wire protocol by testing it against the actual zkBoost server implementation. +//! +//! ## Architecture +//! +//! 1. A lightweight mock Execution Layer (EL) that serves fixture data for +//! `debug_chainConfig` and `debug_executionWitnessByBlockHash` JSON-RPC methods. +//! 2. The real `zkBoostServer` configured with `zkVMConfig::Mock` backends. +//! 3. Lighthouse's `HttpProofNodeClient` as the system under test. + +use axum::{Json, extract::State, routing::post}; +use bytes::Bytes; +use metrics_exporter_prometheus::PrometheusBuilder; +use serde_json::Value; +use std::net::Ipv4Addr; +use std::sync::Arc; +use tokio::net::TcpListener; +use tokio_util::sync::CancellationToken; +use zkboost_server::{ + config::{Config, zkVMConfig}, + server::zkBoostServer, +}; +use zkboost_types::ProofType; + +// ─── Fixture Data ──────────────────────────────────────────────────────────── + +/// SSZ-encoded NewPayloadRequest from zkBoost's test fixture. +pub const FIXTURE_NEW_PAYLOAD_REQUEST: &[u8] = + include_bytes!("../tests/fixture/new_payload_request.ssz"); + +/// Chain config JSON from zkBoost's test fixture. +const FIXTURE_CHAIN_CONFIG: &str = include_str!("../tests/fixture/chain_config.json"); + +/// Execution witness JSON from zkBoost's test fixture. +const FIXTURE_EXECUTION_WITNESS: &str = include_str!("../tests/fixture/execution_witness.json"); + +// ─── Mock Execution Layer ──────────────────────────────────────────────────── + +struct MockElState { + chain_config: Value, + witness: Value, +} + +/// Mock EL handler that responds to JSON-RPC requests with fixture data. +async fn mock_el_handler(State(state): State>, body: Bytes) -> Json { + let request: Value = serde_json::from_slice(&body).unwrap_or_default(); + let method = request["method"].as_str().unwrap_or(""); + + let result = match method { + "debug_chainConfig" => state.chain_config.clone(), + "debug_executionWitnessByBlockHash" => state.witness.clone(), + _ => Value::Null, + }; + + Json(serde_json::json!({ + "jsonrpc": "2.0", + "result": result, + "id": request["id"], + })) +} + +/// Start a mock execution layer server that serves fixture data. +async fn start_mock_el() -> url::Url { + let chain_config: Value = serde_json::from_str(FIXTURE_CHAIN_CONFIG) + .expect("fixture chain_config.json should be valid JSON"); + let witness: Value = serde_json::from_str(FIXTURE_EXECUTION_WITNESS) + .expect("fixture execution_witness.json should be valid JSON"); + + let state = Arc::new(MockElState { + chain_config, + witness, + }); + + let app = axum::Router::new() + .route("/", post(mock_el_handler)) + .with_state(state); + + let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)) + .await + .expect("failed to bind mock EL"); + let port = listener.local_addr().expect("no local addr").port(); + + tokio::spawn(async move { axum::serve(listener, app).await }); + + format!("http://127.0.0.1:{port}").parse().unwrap() +} + +// ─── Real zkBoost Server ───────────────────────────────────────────────────── + +/// Start the real zkBoost server with mock zkVM backends. +async fn start_zkboost_server( + el_endpoint: url::Url, + zkvm_configs: Vec, +) -> (url::Url, CancellationToken) { + let config = Config { + port: 0, + el_endpoint, + chain_config_path: None, + witness_timeout_secs: 120, + proof_timeout_secs: 120, + proof_cache_size: 128, + witness_cache_size: 128, + zkvm: zkvm_configs, + }; + + let metrics = PrometheusBuilder::new().build_recorder().handle(); + let shutdown = CancellationToken::new(); + let server = zkBoostServer::new(config, metrics) + .await + .expect("failed to create zkBoost server"); + let (addr, _handles) = server + .run(shutdown.clone()) + .await + .expect("failed to start zkBoost server"); + + let endpoint = format!("http://127.0.0.1:{}", addr.port()).parse().unwrap(); + (endpoint, shutdown) +} + +// ─── Test Harness ──────────────────────────────────────────────────────────── + +/// Test harness that manages a real zkBoost server with mock backends. +pub struct ZkboostTestHarness { + /// Base URL of the running zkBoost server. + pub endpoint: url::Url, + /// The proof type configured for the mock backend. + pub proof_type: ProofType, + /// Cancellation token for graceful shutdown. + shutdown: CancellationToken, +} + +impl ZkboostTestHarness { + /// Start a test harness with a single mock zkVM backend. + /// + /// The mock backend uses `EthrexZisk` by default (same as zkBoost's own + /// integration tests) with a configurable proving delay. + pub async fn start(mock_proving_time_ms: u64) -> Self { + Self::start_with_proof_type(ProofType::EthrexZisk, mock_proving_time_ms).await + } + + /// Start a test harness with a specific proof type. + pub async fn start_with_proof_type(proof_type: ProofType, mock_proving_time_ms: u64) -> Self { + let el_endpoint = start_mock_el().await; + + let zkvm_config = zkVMConfig::Mock { + proof_type, + mock_proving_time_ms, + mock_proof_size: 1024, + mock_failure: false, + }; + + let (endpoint, shutdown) = start_zkboost_server(el_endpoint, vec![zkvm_config]).await; + + Self { + endpoint, + proof_type, + shutdown, + } + } + + /// Return the base URL as a string. + pub fn url(&self) -> String { + self.endpoint.to_string().trim_end_matches('/').to_string() + } +} + +impl Drop for ZkboostTestHarness { + fn drop(&mut self) { + self.shutdown.cancel(); + } +} diff --git a/testing/proof_engine_zkboost/tests/fixture/chain_config.json b/testing/proof_engine_zkboost/tests/fixture/chain_config.json new file mode 100644 index 00000000000..82be0f85904 --- /dev/null +++ b/testing/proof_engine_zkboost/tests/fixture/chain_config.json @@ -0,0 +1,45 @@ +{ + "chainId": 3151908, + "homesteadBlock": 0, + "daoForkSupport": false, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "mergeNetsplitBlock": 0, + "shanghaiTime": 0, + "cancunTime": 0, + "pragueTime": 0, + "osakaTime": 0, + "bpo1Time": 0, + "terminalTotalDifficulty": 0, + "terminalTotalDifficultyPassed": true, + "depositContractAddress": "0x00000000219ab540356cbb839cbe05303d7705fa", + "blobSchedule": { + "bpo1": { + "baseFeeUpdateFraction": 8346193, + "max": 15, + "target": 10 + }, + "cancun": { + "baseFeeUpdateFraction": 3338477, + "max": 6, + "target": 3 + }, + "osaka": { + "baseFeeUpdateFraction": 5007716, + "max": 9, + "target": 6 + }, + "prague": { + "baseFeeUpdateFraction": 5007716, + "max": 9, + "target": 6 + } + } +} \ No newline at end of file diff --git a/testing/proof_engine_zkboost/tests/fixture/execution_witness.json b/testing/proof_engine_zkboost/tests/fixture/execution_witness.json new file mode 100644 index 00000000000..65887064f82 --- /dev/null +++ b/testing/proof_engine_zkboost/tests/fixture/execution_witness.json @@ -0,0 +1,50 @@ +{ + "state": [ + "0xf90171a046a9f1217c365990825b7d161fc23cae5688cfb6b2307efe4b732c723e03795880a0c0e0b54cb105bad41b4b925883507463ddfae71c619ba2e41d6d57da2a28effea0793c9db0e252f8f5c79a9d872efc5385ab632a9dc31217637b3509fcf6f0b010a077c059a2b360e9c967686a1302a40994cd63a81aa80a841991d8f3d7379b68eb80a0386a1e942dbe86342b17e2e8b28a259d6db65df8e05f944951a089bb9f3d989fa0315b6e4145b520b88ff5fb638b922671ee1ecbcb65b57b9a4be650ab1fce1d39a066e01acc8a9826bc3d5f5286819fc5883dfa30943331f1e7ff2968bfc57ea2d0a00f7041c0b666de2c820d816b27347738f0e8e2d4d7e1e94e2908b88bc3665a338080a012794aea34d39f220863a2977506ebe5555c2b6488a9469fed918b744f67d6d9a0ace6b45485050162428ffe70f5214d2350ed4890b94322bda3ba63a17342983aa0e20d629ffd2bee3848106f86b98c50a9de755283203bb778c19fa269c8ddb2e38080", + "0xe214a0daadd0f2cf85d5b6a644144de38d5eea115a4546c5efc75c3aee9934f46754a0", + "0xf90131a0e9355b99a40b0e92cc489d34c25f68648461fe0dfcefe3c861f1042ae7cbd522a0766a5ea5b9545a72a463a0fc6151efd1ea0e13f7bd151789dcbff75a1e73cd7180a06e27501c46d61120352d54c49863fcf0eaafcfbffdab9e9e09847d62beef79d88080a0731b30b1211ab24c3e719ad1774d6d450379c926217e248edc5c2a6812e0169480a02978c23bea458e7f47cdc57a9938245e2c763f556847e7e320f7f1bd844127628080a047b4dd8c12aa7dec12a56beb24dff26bd425669991ba54e9ec3225fe6293da24a039f39136138de3527d38db1831c98f8897eecd0a75a77129f6386184f28c779ba0fa149b424c332acc1c6967d908eab8e4922f27e00499ed6a1aca3b9975d87ef580a09f297d5e53d34bc2096cce66773c42bf5b177714e3bb9f180521045b34f7127d80", + "0xf90211a0fcf8a530a63eb8575eb9a70c95332fc1047b567be3d1da03a21c9917d92b14a6a006d47616df479b46b302f2a8b7ed03cb537f6cf7c551c15421c65db4e00fa97fa038e34f9e0e4830343ba24f5fcf0eba28d79cb86397adfb16a0169ee7f0180036a004f7ae295715850712c9dc7f1b9f973797b89ef0f46991203ac789210330a517a0dd3420839babaee761e7eaa38ad5f596b1a9b8716e7e9b9261949a964a5a7d61a0eea64374052ac460957bbc34a071cb8b25dcdf44d96785a55b34242914c83f9fa008763a217b516bcfeadc7f6849e812b392643f50a1d25f002ceec6c2ca0adcafa083c6979e463c02818ffeadaeeb8abc9f2f51e767fb9151a7fc89989eb40b57aca0cbcdc1d226a540c50cb1e615e7af99f171d4365b45734940e22d47ec4aa23a14a0be88e4724326382a8b56e2328eeef0ad51f18d5bae0e84296afe14c4028c4af9a018e0f191e57d4186717e0f3c9379d2438cec0babd12d3903a4ad560f017331bfa01796617427e67ed10cdf8a72b02689a700ba71eb93186a1b120c9ad0b0e56eaea0ad0bb86b47186c04223e85a9c33dd1c87dd6e5c17f753f4fd0a56772d8a78399a065fb94808e31ca248fb2d9de329b81735b22f75d109f389678c9965418bf1f16a06a2b50671c3f299bfd4b6cf43d6e5d6aafd4d3677c38a8af52a0cd7680de2b94a037ff00fbe2105bce0e6ed9ea80a1d67b8a476b1ff3d177ac9597a53241e47aa780", + "0xf901b1a027db720cbe694541a361e08b5450894ddce39b11113fe952080ad5f54ada6f4a80a0d2e57f615a47508c6e60935353428b9fc1cc75677a3eb8f5f73d61dd0aaff5f5a0ca976997ddaf06f18992f6207e4f6a05979d07acead96568058789017cc6d06ba04d78166b48044fdc28ed22d2fd39c8df6f8aaa04cb71d3a17286856f6893ff8380a0fc3b71c33e2e6b77c5e494c1db7fdbb447473f003daf378c7a63ba9bf3f0049da0a9c8e462df1860757a204a01fccc87b873837b0a32cbcc645fb663f3eb12a705a07b8e7a21c1178d28074f157b50fca85ee25c12568ff8e9706dcbcdacb77bf854a0973274526811393ea0bf4811ca9077531db00d06b86237a2ecd683f55ba4bcb0a091d9c76bfbc066e84f0b415c737ab8c477498701d920526db41690050cfade99a06aa67101d011d1c22fe739ef83b04b5214a3e2f8e1a2625d8bfdb116b447e86fa0244e4282dfec33c9bb765162ceee4f2e6390033a94b620d50a2fc6943ebd82fca0f3b039a4f32349e85c782d1164c1890e5bf16badc9ee4cf827db6afd2229dde6a0d9240a9d2d5851d05a97ff3305334dfdb0101e1e321fc279d2bb3cad6afa8fc88080", + "0xf90171a06664dd6bcbb08b83f84324db8cbaf2ceb221e49e66971369dd2257e947a3b13d80a0f4ec365c37413b5f9e7d38c3c6409922fa2a593757ef6176b7291ede5ae2b2d780a0614ab7fe84bea831a68e5e39c6e2d339db432b94dcd29ac75de694cfc6641496a036750a0cdda09ef53dc4a7510eb69e87fbafb1739f51d52c60214b7e0d276ddda04eb05cc2337a47e5d315fc9e2972f88b2282caecf7b79cb486ccf4e64ddf54cd80a0044dadb95a10fad8f922e38449d128807ed6c4b3e6af52d0faa865be8cb8847480a0d53e862eebd81f90452eada8434dfdd03a7ef3d06d6db3e68cbc7d05dff81ec0a0eb47388255e7ca68b42fa56180019c61e2dd301bfe20226d6a74d795f6b016a6a0c522d5defc176e5fa5fc0f16d95ad335f25668067c2c9a55db7d901fd8ac04c6a06c457c05a87c557f84f6d98cfb3754a20c1ded0550ef405433d3514f332c77df80a0d5758f21c6c63a45c81d16ecca352c41af637c1729f8866900efcf731dc10db280", + "0xf901518080a0f1a60e8881cfcb2dc50ba58c326ccc9a6da8287c1e5f56d2017563be700058c4a0616362468a3391221e3782da42e2d6fb8ea41da6bdd2d679e20bf0375c06158680a0ed2fba131fadeadeb1082f565fff16ceb008f693056e3140204716c0739cf1e08080a0cfcecd85b5b3b2b03c196589d3d3b9bcd0ddfc01f000cde9fe3cab41dc6a0a16a0b2a5565ce39d8b7fabb242f087f05b7273aef44094f4166046cddd978751c4bea06234ead07239df2c23d50d21d2e045332bb3e2fb0a402aae5780b823e7d5308680a0ebe51b14fea6aaa5c097f2506874e990813c36cd31399ee3d72666de2dde3fcca051eac0e6e8747ed945c8119613a8359cb76220e714610cf783388ce900153208a0e16e6773b65ff27c428b07407a2d2e479712166515a4a43ecc3c4444d77d4f34a0105bafd3bfbb01dd5f28afe06b314ccf6d5f1bddd1e2135dcc010cb3aedd1d4380", + "0xf869a02086c581c7d7b44eecbb92fd9e5867945ec1acdc0ea5bbabda21d17dddf06473b846f8440180a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a00345a365d2f4c5975b9f1599abe0a2ee76b7a3a731bc68781bd04c84e4858f50", + "0xf8518080a0a63eaef203909ce313085e71f47b6855ccd4fffe444fba1ec1efdab787203351808080a079af4179331361fad570767001c2b705c100b691c849c2bda966d070709c4bf880808080808080808080", + "0xf8f18080a0936cc4aad97b5838a8bc0dfa95a5ad0a0fe2c4681ffe209a0228098aad0c619080a0985a93e071c1474beffb3b3feefcf343ea7e4e002d8c6c7675de86cc9ebc27c0a022652c87d9810e05a254c5942729b67343e1069440f4bee452c8c7bb88d193e8a0975e4f968cf4537118047c31c634177fbec68949fd40003601aab2ff822bcc5580a04b66d0874dc47dba19ba179183342befb18cd80e6d4f85516123426f86e0d7afa081ce69bcb065377bda0ac34c8b05086357838440690dbcccba20f3e8cab1b88680808080a0dc281265feb5bcd82bc162628f62f92dc649b77c80a3ac5d14bdf3d367c495238080", + "0xf90211a040da929897ecf8fb0ecdda44d1c6aa37c7b5d19d0a6f1255c3aaac43b77f2d4ca05dbd3e7becc744398948292f4810e753b166b91cc1a763b214b24718e2bc432aa0d8222bc84a44e1d1d03bdae69910ff3b244c815fa99ef1a9aa6bb568cad6b35ca050e569e8e5e77ab130db2842f7598cee50d0b42cc2504a9df287175c307b23c3a01fd4c856668574229bec8b57377eb317351e0695f8c7c8239aa1016a73001b16a0e0cf581054d8c2bab1359dddde660c659c0e5d70ca2c03e667419d1bc9e45f05a0ebe2e281fc5af1d9bc149c1bf210d264f9b283a2c1760abf0ad5f48e08499ac8a08be4370ed1686f92ab5478848e85a1abab751c9c80e7f8d68daa8c3d8232356aa02fb840ef5765a4ceb26d610badea7ea799c28544f3b329b986c400ff272967d2a0e8393cb9738eea3a5031110dd9c2043e360267072374de576cdc9bc4fa015d39a046ff1faf6df6476a5a4d8f6ba32c8f38582b3a7bd4e12893c1712894ea39c017a008afbb10c9064b061ba3a17cfaf8c083b376a402c60704bc0afaa7a55d27f5c9a0582d0c27b5152cb3f3247a8752888739769fb2b6e3f7842298bc26b616773b88a0db4a8cc49ce3a0fefe00143359d4f0fa86026559ea073bb061b7aacd217ac037a0f31e8aa4efb4024c99d873f31485f1c496f484c345b1ec664f4ba723499e03f8a0672f74dceafee2ee98a97fb19f4afdb991ba8c1ee019438f15b809da4b427b5a80", + "0xf90211a04efbc90ce3b15216a559cdb50fb788b0af3916ef1777a585e7093e27cf4bc16da0047b79502e6ba90c8c1b4863e8380b3e6cc23da1c208f8e39c348a936af31ea5a03db8dd4c19ae2b67a736b757995cb7b57ed55ccdd34fb0ebe979a2dee0c66339a0471db2263571236146b863a32d0d1abe6e21a984998b2d7c0376b4243dca42d2a024e8f92fe5bdde58f4954f534b6f91659a8c0f889abdbf7eec9ab77a26478072a0f8afbd19dafaf176fc835595483ea85f554b1b840e8709b3c2a07715411ecb08a034cb0ac81dbce62a5c9855fe0311bd6827fecbb9aa741a7c8e8b7427f73b8716a0a2a9a28a4324e79e625b104a232620f515ff4a3428c78257bbec3621343ec11aa0b030f3e6c8e7b40bc5bea3da238bcf7546c521b7d6b72dbd98e3fbffb0d604ada0de4bf15b56b7a96707c9c6072d1f413322e563f04ec3c3f9fcf7719b073ed285a0c641efacb85f02a412724d2ba1a107c767d66f5258ae33c9c64bd1bcc4a64540a02e14db6c4900768cf91528d8e1b746f9ab032f277077459f5cc79d16b6be0dc3a006c495fb6961358f4bde6c279838bbc557f9927391b42070bd44b30ab824430fa07415c94beb78124e62f7f63ad7a64076cf7b004809565b8a63dedcacc1434ac5a092712479fda69c5e14b2085716b5e5ab229494f395740b941280432b831ed221a0176a9fce68e6fd07098e5bd0e742a828173ba4a7feed5b6455794caad04462a880", + "0xf869a0209d57be05dd69371c4dd2e871bce6e9f4124236825bb612ee18a45e5675be51b846f8440180a0a247228347f628c6463d5f2932202f269bcabe3dbc08a56392c2dc88e7e04249a06e49e66782037c0555897870e29fa5e552daf4719552131a0abce779daec0a5d", + "0xf90211a0e66e395bd17cd8e5cea8b1c1aad2bb861eeb8a2bb096ea6eddeb34422497bdaaa04c03fe869c8cde143d01ab6bfc09226ea42d9ad99a53263f69716a7186c0bf0aa077e46fe2af85fe2ea2de398481c148651e7ee82f27176160eb18b3a802661798a0c52146e012e5094a13d00fc9dbe596a0639c59e2587b7ac55038d3e52d4f4936a044cb808faa3a8e993889588681b030c9a97babe7e15fdb71be950e9a88a7e402a03deea8359c1b0971aa68d701e9cd18016134f5310b0e4a7d9833247db460a1f1a02cedc09ae6f35f5e75e4a65cee5fc753b113311d912b25fc289a872885415a8ca080b9f7d63a5ea0d7b20ace0018da20977a795543c0ab2d4035b60885e5d60828a0b8f2aa8b6816e39e58f9193d23f9573f75e4c0dea753b325da153a6fbdbaabd2a05126fa3c18c632812536718c92ed0747e4a610c245ea1234acbca7533f1506f2a014116df18532e1f44477d3cf371240e82d2cf7c02542d6da6ab56861626a0c24a0ad7eb60b7242bb4abab99f42056bcc64ae2de2b6182550cb6864c404b059fb3fa04e222b8402af16d6151aba0426b59a029db34ba31592f254ba8d6f64e59e07eba0eae43e73dfb5784c88f6424e4da4ac7aae2aa29f09cd528aab89a4003e3a4da7a023fc581b6065c3d34578d7119f3385df16ae9a24aab09a98877d36fd844f2933a0a4cb53144ee264a09401aabbebb43c80264ebfce063a70c28595e1b0c52fdd9c80", + "0xf90211a0f53fd45e8a28bfc7c92543aac0f242249bd15dc550b8d1d43defabfe1ff4622ba072d67f642876a04c9733ce298d4bb2fdc2eb041b6760ed0a3be006785b0705e0a0a86c39e9a32652492ee5240d1715c6a63537351d350754b62952760d8e1f944ba0e79513901b1f313c826300a31dff17f6adf9e2aeb895f730dbb93f0a96a86d9fa031c4646963f14566afb0e50a6c400d69c834c3b1fdb3909677856cbb576db4e5a09cdcc334e9d1c6451e5f5230efdd07ac62f48223d3a71b7082d1c9f3faee6af1a0f5ea37b375d1f04089104149dd9204aa0ff3c90167f7aca7da201905594300e4a076972cc63f4fabea810e87083ec1899b687d8748d26fe16fd4b6a13ae3e303f1a04ff31ed8ee553088b2e578f36bf3ed50d5cbd58611261be37633294dc61acca9a028b05d809456d53fc06c9b102d216cff567a7aca7c9d1cb4cdca67965f0ef4cda01556f03106eeb9fc5a473e8f7f042e57d827b78b76a5f7a8f5b187f8d897515da0f762ae6fe61a92321fb8d528b2f5f4b1b97a94ebf2d5ec0899e8f703fba9790da036affb194c9227b46dece3bc3e1e5ef56403db6c8e34fe1b8bb3ae197158b5d6a0db08702017c418fb841716b9c2454676fa632f607d5b261f55c7434dbc69c4d6a0d4da88e24a26de50f4f0d35a348e12da471480c6e612dacccbc594a61f58d74ca0aaba74a722fd0645b8b7a8886d0e891e04c4e57914480568f5334d7514391f6680", + "0x80", + "0xf8429f37d5f9b51ca71bda3c02250aa5ededabaa712e18e5f1714fde16280d94a4a3a1a0d5848dbf659bcc407318ddcac1ad62fb7b58c53df808ed0a560c8d4a94ac3e6f", + "0xf8689f3aea581b220579a2b99819299dd32c7c28a420018ecb0bde93af007ad89a31b846f8440180a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a078c6cb5202685228bbcbfb992b1c4e116c7ec5ef11e25b8e92716cfc628ddd60", + "0xf869a020d65eaa92c6bc4c13a5ec45527f0c18ea8932588728769ec7aecfe6d9f32e42b846f8440180a0a52d06d7443bb469a8ddfecb744e9750fa7284a237b31f7168562123b84c3547a0f57acd40259872606d76197ef052f3d35588dadf919ee1f0e3cb9b62d3f4b02c", + "0xf90211a065cb9654d83c2c587ff35d995153e55908ccc8d12f99cec6f0fca2174d0d4887a072c2cce9f8770d341a4cb7c7cdc53d75d6308b55e9f991bc8ba67b29434b61dfa0f2b29241a79b4cf67be8c19e0fc49894bbc908bdfaa864f313e640a9656271cca0a4f08ea6851799ebadce763bdb22c8511a37106f2b1f1a2e1da77743588a4751a0e473037e78e7f6b59faf7c818971524734244419165e3b52fd6747e4acbb3235a09f871e9dc9ad7e80a33f12dfb19ec657a944edd24ecd975367a4675d7a2760a0a0f3e41d9e7b89a679eba0c449b24e2f6b074dd4e65abc10fee304b97893689673a0ba956ceecf3546a048edbdb0e93c6bd5f9437ee2bc2eb547d95cad86e16e791ba0be49e1efa56a6325758e40aa25985c3f71f2d20888daa9efd8e2e9cd0d70826ca05d4d0edd678514b0b449d8689f7971252fd7b86378a102395d5ee769d709c2a1a0fcacd3004b2d9f8c601c667041baea5c7ad53bde430303ab3d2f5c765804cd82a0b7195c41d29afbb5b45413885333d6a19b0679d3a92a9f1198ab04689ac0518ca06675b419aca5f5ab938080fd8245ae9c388c144521ad7d4a57e8f36212e218a2a0ddfccdcd7960367614d844e7fca5cb92573ace5ca42ad9381dfc2c69e7f0f890a04651f6d80d233d28e5cda8940d11319698f604ee414041a9374a5ee3d7305b1fa0da847328820b77fcc53e716178f77359797b68b90e53117251c9115ee6fc428880" + ], + "codes": [ + "0x3373fffffffffffffffffffffffffffffffffffffffe1460cb5760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146101f457600182026001905f5b5f82111560685781019083028483029004916001019190604d565b909390049250505036603814608857366101f457346101f4575f5260205ff35b34106101f457600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260385f601437604c5fa0600101600355005b6003546002548082038060101160df575060105b5f5b8181146101835782810160030260040181604c02815460601b8152601401816001015481526020019060020154807fffffffffffffffffffffffffffffffff00000000000000000000000000000000168252906010019060401c908160381c81600701538160301c81600601538160281c81600501538160201c81600401538160181c81600301538160101c81600201538160081c81600101535360010160e1565b910180921461019557906002556101a0565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff14156101cd57505f5b6001546002828201116101e25750505f6101e8565b01600290035b5f555f600155604c025ff35b5f5ffd", + "0x3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500", + "0x3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500", + "0x3373fffffffffffffffffffffffffffffffffffffffe1460d35760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461019a57600182026001905f5b5f82111560685781019083028483029004916001019190604d565b9093900492505050366060146088573661019a573461019a575f5260205ff35b341061019a57600154600101600155600354806004026004013381556001015f358155600101602035815560010160403590553360601b5f5260605f60143760745fa0600101600355005b6003546002548082038060021160e7575060025b5f5b8181146101295782810160040260040181607402815460601b815260140181600101548152602001816002015481526020019060030154905260010160e9565b910180921461013b5790600255610146565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561017357505f5b6001546001828201116101885750505f61018e565b01600190035b5f555f6001556074025ff35b5f5ffd" + ], + "keys": [ + "0x000f3df6d732807ef1319fb7b8bb8522d0beac02", + "0x0000000000000000000000000000000000000000000000000000000000003808", + "0x0000000000000000000000000000000000000000000000000000000000001809", + "0x00000961ef480eb55e80d19ad83579a64c007002", + "0x0000000000000000000000000000000000000000000000000000000000000002", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000003", + "0x0000f90827f1c53a10cb7a02335b175320002935", + "0x00000000000000000000000000000000000000000000000000000000000004af", + "0x0000bbddc7ce488642fb579f8b00f3a590007251", + "0x0000000000000000000000000000000000000000000000000000000000000002", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000003" + ], + "headers": [ + "0xf9026fa084a5904e068368b6581e5afa05f96e3912068ab8ceee08ca76bdb9719bd1c090a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347948943545177806ed17b9f23f0a21ee5948ecaa776a03bb7c2e1c292bc41a27064b9160eb131723e6c345851ee0c386f09115da5fae6a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000808204af8402255100808469aeca6d92726574682f76312e31302e312f6c696e7578a0f2940bf2aad7139113b79fcd654cb699530e993a33dc05a31ebfcf017643b55888000000000000000007a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b4218080a037afc7de70547b71e752341e78303f688e6f5b87e47367b747947d5d34af77a0a0e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ] +} \ No newline at end of file diff --git a/testing/proof_engine_zkboost/tests/fixture/new_payload_request.ssz b/testing/proof_engine_zkboost/tests/fixture/new_payload_request.ssz new file mode 100644 index 00000000000..6ffe35cc644 Binary files /dev/null and b/testing/proof_engine_zkboost/tests/fixture/new_payload_request.ssz differ diff --git a/testing/simulator/Cargo.toml b/testing/simulator/Cargo.toml index a1b1b6f95d2..1a8a9427195 100644 --- a/testing/simulator/Cargo.toml +++ b/testing/simulator/Cargo.toml @@ -3,22 +3,32 @@ name = "simulator" version = "0.2.0" authors = ["Paul Hauner "] edition = { workspace = true } + +[features] +test-utils = [] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = { workspace = true } +beacon_chain = { workspace = true } clap = { workspace = true } -environment = { workspace = true } +environment = { workspace = true, features = ["test-utils"] } +eth2 = { workspace = true, features = ["events"] } execution_layer = { workspace = true } futures = { workspace = true } kzg = { workspace = true } +lighthouse_network = { workspace = true } logging = { workspace = true } +network_utils = { workspace = true } node_test_rig = { path = "../node_test_rig" } parking_lot = { workspace = true } rayon = { workspace = true } sensitive_url = { workspace = true } serde_json = { workspace = true } +task_executor = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } typenum = { workspace = true } types = { workspace = true } +validator_http_api = { workspace = true } diff --git a/testing/simulator/src/basic_sim.rs b/testing/simulator/src/basic_sim.rs index 79581ee5299..aa7896e20c8 100644 --- a/testing/simulator/src/basic_sim.rs +++ b/testing/simulator/src/basic_sim.rs @@ -1,4 +1,5 @@ use crate::local_network::LocalNetworkParams; +use crate::local_network::NodeType; use crate::local_network::TERMINAL_BLOCK; use crate::{LocalNetwork, checks}; use clap::ArgMatches; @@ -30,9 +31,10 @@ const ALTAIR_FORK_EPOCH: u64 = 0; const BELLATRIX_FORK_EPOCH: u64 = 0; const CAPELLA_FORK_EPOCH: u64 = 0; const DENEB_FORK_EPOCH: u64 = 0; -const ELECTRA_FORK_EPOCH: u64 = 2; -// const FULU_FORK_EPOCH: u64 = 3; -// const GLOAS_FORK_EPOCH: u64 = 4; +const ELECTRA_FORK_EPOCH: u64 = 0; +const FULU_FORK_EPOCH: u64 = 0; +// TODO(gloas): enable Gloas in simulator, current blocker is lack of data column gossip verification +// const GLOAS_FORK_EPOCH: u64 = 2; const SUGGESTED_FEE_RECIPIENT: [u8; 20] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]; @@ -171,8 +173,8 @@ pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { let genesis_delay = GENESIS_DELAY; // Convenience variables. Update these values when adding a newer fork. - let latest_fork_version = spec.electra_fork_version; - let latest_fork_start_epoch = ELECTRA_FORK_EPOCH; + let latest_fork_version = spec.fulu_fork_version; + let latest_fork_start_epoch = FULU_FORK_EPOCH; let mut slot_duration_ms = spec.get_slot_duration().as_millis() as u64; slot_duration_ms /= speed_up_factor; @@ -187,6 +189,7 @@ pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { spec.capella_fork_epoch = Some(Epoch::new(CAPELLA_FORK_EPOCH)); spec.deneb_fork_epoch = Some(Epoch::new(DENEB_FORK_EPOCH)); spec.electra_fork_epoch = Some(Epoch::new(ELECTRA_FORK_EPOCH)); + spec.fulu_fork_epoch = Some(Epoch::new(FULU_FORK_EPOCH)); let spec = Arc::new(spec); env.eth2_config.spec = spec.clone(); @@ -208,8 +211,11 @@ pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { LocalNetworkParams { validator_count: total_validator_count, node_count, - extra_nodes, + extra_nodes: 0, proposer_nodes, + proof_generator_nodes: 0, + proof_verifier_nodes: 0, + delayed_nodes: extra_nodes, genesis_delay, }, context.clone(), @@ -220,7 +226,11 @@ pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { // Add nodes to the network. for _ in 0..node_count { network - .add_beacon_node(beacon_config.clone(), mock_execution_config.clone(), false) + .add_beacon_node( + beacon_config.clone(), + mock_execution_config.clone(), + NodeType::Default, + ) .await?; } @@ -230,7 +240,11 @@ pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { for _ in 0..proposer_nodes { println!("Adding a proposer node"); network - .add_beacon_node(beacon_config.clone(), mock_execution_config.clone(), true) + .add_beacon_node( + beacon_config.clone(), + mock_execution_config.clone(), + NodeType::Proposer, + ) .await?; } @@ -261,7 +275,7 @@ pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { .await } else { network_1 - .add_validator_client(validator_config, i, files) + .add_validator_client(validator_config, i, files, NodeType::Default) .await } .expect("should add validator"); diff --git a/testing/simulator/src/fallback_sim.rs b/testing/simulator/src/fallback_sim.rs index 06f4478c5e6..3d872c1a2c3 100644 --- a/testing/simulator/src/fallback_sim.rs +++ b/testing/simulator/src/fallback_sim.rs @@ -1,4 +1,4 @@ -use crate::local_network::LocalNetworkParams; +use crate::local_network::{LocalNetworkParams, NodeType}; use crate::{LocalNetwork, checks}; use clap::ArgMatches; @@ -25,11 +25,12 @@ const END_EPOCH: u64 = 16; const GENESIS_DELAY: u64 = 38; const ALTAIR_FORK_EPOCH: u64 = 0; const BELLATRIX_FORK_EPOCH: u64 = 0; -const CAPELLA_FORK_EPOCH: u64 = 1; -const DENEB_FORK_EPOCH: u64 = 2; -// const ELECTRA_FORK_EPOCH: u64 = 3; -// const FULU_FORK_EPOCH: u64 = 4; -// const GLOAS_FORK_EPOCH: u64 = 5; +const CAPELLA_FORK_EPOCH: u64 = 0; +const DENEB_FORK_EPOCH: u64 = 0; +const ELECTRA_FORK_EPOCH: u64 = 0; +const FULU_FORK_EPOCH: u64 = 0; +// TODO(gloas): enable Gloas in simulator, current blocker is lack of data column gossip verification +// const GLOAS_FORK_EPOCH: u64 = 2; // Since simulator tests are non-deterministic and there is a non-zero chance of missed // attestations, define an acceptable network-wide attestation performance. @@ -191,8 +192,8 @@ pub fn run_fallback_sim(matches: &ArgMatches) -> Result<(), String> { spec.bellatrix_fork_epoch = Some(Epoch::new(BELLATRIX_FORK_EPOCH)); spec.capella_fork_epoch = Some(Epoch::new(CAPELLA_FORK_EPOCH)); spec.deneb_fork_epoch = Some(Epoch::new(DENEB_FORK_EPOCH)); - //spec.electra_fork_epoch = Some(Epoch::new(ELECTRA_FORK_EPOCH)); - //spec.fulu_fork_epoch = Some(Epoch::new(FULU_FORK_EPOCH)); + spec.electra_fork_epoch = Some(Epoch::new(ELECTRA_FORK_EPOCH)); + spec.fulu_fork_epoch = Some(Epoch::new(FULU_FORK_EPOCH)); let spec = Arc::new(spec); env.eth2_config.spec = spec.clone(); @@ -218,6 +219,9 @@ pub fn run_fallback_sim(matches: &ArgMatches) -> Result<(), String> { node_count, extra_nodes: 0, proposer_nodes: 0, + proof_generator_nodes: 0, + proof_verifier_nodes: 0, + delayed_nodes: 0, genesis_delay, }, context.clone(), @@ -228,7 +232,11 @@ pub fn run_fallback_sim(matches: &ArgMatches) -> Result<(), String> { // Add nodes to the network. for _ in 0..node_count { network - .add_beacon_node(beacon_config.clone(), mock_execution_config.clone(), false) + .add_beacon_node( + beacon_config.clone(), + mock_execution_config.clone(), + NodeType::Default, + ) .await?; } diff --git a/testing/simulator/src/lib.rs b/testing/simulator/src/lib.rs new file mode 100644 index 00000000000..b6c70d44969 --- /dev/null +++ b/testing/simulator/src/lib.rs @@ -0,0 +1,26 @@ +//! This crate provides various simulations that create both beacon nodes and validator clients, +//! each with `v` validators. +//! +//! When a simulation runs, there are checks made to ensure that all components are operating +//! as expected. If any of these checks fail, the simulation will exit immediately. +//! +//! ## Future works +//! +//! Presently all the beacon nodes and validator clients all log to stdout. Additionally, the +//! simulation uses `println` to communicate some info. It might be nice if the nodes logged to +//! easy-to-find files and stdout only contained info from the simulation. +//! +pub mod basic_sim; +pub mod checks; +pub mod cli; +pub mod fallback_sim; +pub mod local_network; +pub mod retry; + +pub use local_network::LocalNetwork; +pub use types::MinimalEthSpec; + +pub type E = MinimalEthSpec; + +#[cfg(feature = "test-utils")] +pub mod test_utils; diff --git a/testing/simulator/src/local_network.rs b/testing/simulator/src/local_network.rs index 2beb9c0efcf..1fbdd7661f0 100644 --- a/testing/simulator/src/local_network.rs +++ b/testing/simulator/src/local_network.rs @@ -1,8 +1,11 @@ use crate::checks::epoch_delay; +use beacon_chain::custody_context::NodeCustodyType; use kzg::trusted_setup::get_trusted_setup; +use lighthouse_network::types::Enr; +use network_utils::listen_addr::ListenAddress; use node_test_rig::{ ClientConfig, ClientGenesis, LocalBeaconNode, LocalExecutionNode, LocalValidatorClient, - MockExecutionConfig, MockServerConfig, ValidatorConfig, ValidatorFiles, + MockExecutionConfig, ValidatorConfig, ValidatorFiles, environment::RuntimeContext, eth2::{BeaconNodeHttpClient, types::StateId}, testing_client_config, @@ -15,23 +18,83 @@ use std::{ sync::Arc, time::{Duration, SystemTime, UNIX_EPOCH}, }; +use task_executor::TaskExecutor; use types::{ChainSpec, Epoch, EthSpec}; +use validator_http_api::{Config as ValidatorHttpConfig, PK_FILENAME}; -const BOOTNODE_PORT: u16 = 42424; -const QUIC_PORT: u16 = 43424; +pub const TERMINAL_BLOCK: u64 = 0; -pub const EXECUTION_PORT: u16 = 4000; +// Proof node types and several helpers are only exercised by the proof engine +// integration tests; allow dead code so the simulator binary builds with +// `-D warnings`. +#[allow(dead_code)] +#[derive(Debug, Copy, Clone)] +pub enum NodeType { + Default, + Proposer, + ProofVerifier, + ProofGenerator, +} -pub const TERMINAL_BLOCK: u64 = 0; +impl NodeType { + pub fn is_proposer(self) -> bool { + matches!(self, NodeType::Proposer) + } + + pub fn is_proof_verifier(self) -> bool { + matches!(self, NodeType::ProofVerifier) + } + + pub fn is_proof_generator(self) -> bool { + matches!(self, NodeType::ProofGenerator) + } + + pub fn requires_proof_node(self) -> bool { + matches!(self, NodeType::ProofVerifier | NodeType::ProofGenerator) + } + pub fn requires_execution_node(self) -> bool { + matches!( + self, + NodeType::Default | NodeType::Proposer | NodeType::ProofGenerator + ) + } +} + +#[derive(Debug, Clone)] pub struct LocalNetworkParams { pub validator_count: usize, pub node_count: usize, pub proposer_nodes: usize, + pub proof_generator_nodes: usize, + pub proof_verifier_nodes: usize, pub extra_nodes: usize, + pub delayed_nodes: usize, pub genesis_delay: u64, } +impl LocalNetworkParams { + #[allow(dead_code)] + pub fn node_type(&self, node_idx: usize) -> NodeType { + if node_idx < self.node_count { + NodeType::Default + } else if node_idx < self.node_count + self.proposer_nodes { + NodeType::Proposer + } else if node_idx < self.node_count + self.proposer_nodes + self.proof_generator_nodes { + NodeType::ProofGenerator + } else if node_idx + < self.node_count + + self.proposer_nodes + + self.proof_generator_nodes + + self.proof_verifier_nodes + { + NodeType::ProofVerifier + } else { + panic!("Invalid node index: {}", node_idx); + } + } +} + fn default_client_config(network_params: LocalNetworkParams, genesis_time: u64) -> ClientConfig { let mut beacon_config = testing_client_config(); @@ -39,22 +102,20 @@ fn default_client_config(network_params: LocalNetworkParams, genesis_time: u64) validator_count: network_params.validator_count, genesis_time, }; - beacon_config.network.target_peers = - network_params.node_count + network_params.proposer_nodes + network_params.extra_nodes - 1; + beacon_config.network.target_peers = network_params.node_count + + network_params.proposer_nodes + + network_params.proof_generator_nodes + + network_params.proof_verifier_nodes + + network_params.extra_nodes + + network_params.delayed_nodes + - 1; beacon_config.network.enr_address = (Some(Ipv4Addr::LOCALHOST), None); beacon_config.network.enable_light_client_server = true; beacon_config.network.discv5_config.enable_packet_filter = false; beacon_config.chain.enable_light_client_server = true; beacon_config.chain.optimistic_finalized_sync = false; + beacon_config.chain.node_custody_type = NodeCustodyType::Supernode; beacon_config.trusted_setup = get_trusted_setup(); - - let el_config = execution_layer::Config { - execution_endpoint: Some( - SensitiveUrl::parse(&format!("http://localhost:{}", EXECUTION_PORT)).unwrap(), - ), - ..Default::default() - }; - beacon_config.execution_layer = Some(el_config); beacon_config } @@ -62,13 +123,7 @@ fn default_mock_execution_config( spec: &ChainSpec, genesis_time: u64, ) -> MockExecutionConfig { - let mut mock_execution_config = MockExecutionConfig { - server_config: MockServerConfig { - listen_port: EXECUTION_PORT, - ..Default::default() - }, - ..Default::default() - }; + let mut mock_execution_config = MockExecutionConfig::default(); if let Some(capella_fork_epoch) = spec.capella_fork_epoch { mock_execution_config.shanghai_time = Some( @@ -103,6 +158,15 @@ fn default_mock_execution_config( ) } + if let Some(gloas_fork_epoch) = spec.gloas_fork_epoch { + mock_execution_config.amsterdam_time = Some( + genesis_time + + (spec.get_slot_duration().as_secs()) + * E::slots_per_epoch() + * gloas_fork_epoch.as_u64(), + ) + } + mock_execution_config } @@ -200,21 +264,29 @@ impl LocalNetwork { self.validator_clients.read().len() } + #[allow(dead_code)] + pub fn executor(&self) -> &TaskExecutor { + &self.context.executor + } + async fn construct_boot_node( &self, mut beacon_config: ClientConfig, mock_execution_config: MockExecutionConfig, ) -> Result<(LocalBeaconNode, LocalExecutionNode), String> { + let listen = ListenAddress::unused_v4_ports(); + let v4 = listen.v4().expect("unused_v4_ports always returns V4"); beacon_config.network.set_ipv4_listening_address( - std::net::Ipv4Addr::UNSPECIFIED, - BOOTNODE_PORT, - BOOTNODE_PORT, - QUIC_PORT, + Ipv4Addr::UNSPECIFIED, + v4.tcp_port, + v4.disc_port, + v4.quic_port, ); - - beacon_config.network.enr_udp4_port = Some(BOOTNODE_PORT.try_into().expect("non zero")); - beacon_config.network.enr_tcp4_port = Some(BOOTNODE_PORT.try_into().expect("non zero")); + beacon_config.network.enr_udp4_port = std::num::NonZeroU16::new(v4.disc_port); + beacon_config.network.enr_tcp4_port = std::num::NonZeroU16::new(v4.tcp_port); + beacon_config.network.enr_quic4_port = std::num::NonZeroU16::new(v4.quic_port); beacon_config.network.discv5_config.table_filter = |_| true; + beacon_config.chain.node_custody_type = NodeCustodyType::Supernode; let execution_node = LocalExecutionNode::new(self.context.clone(), mock_execution_config); @@ -233,37 +305,62 @@ impl LocalNetwork { async fn construct_beacon_node( &self, mut beacon_config: ClientConfig, - mut mock_execution_config: MockExecutionConfig, - is_proposer: bool, - ) -> Result<(LocalBeaconNode, LocalExecutionNode), String> { - let count = (self.beacon_node_count() + self.proposer_node_count()) as u16; - - // Set config. - let libp2p_tcp_port = BOOTNODE_PORT + count; - let discv5_port = BOOTNODE_PORT + count; + mock_execution_config: MockExecutionConfig, + node_type: NodeType, + ) -> Result<(LocalBeaconNode, Option>), String> { + let listen = ListenAddress::unused_v4_ports(); + let v4 = listen.v4().expect("unused_v4_ports always returns V4"); beacon_config.network.set_ipv4_listening_address( - std::net::Ipv4Addr::UNSPECIFIED, - libp2p_tcp_port, - discv5_port, - QUIC_PORT + count, + Ipv4Addr::UNSPECIFIED, + v4.tcp_port, + v4.disc_port, + v4.quic_port, ); - beacon_config.network.enr_udp4_port = Some(discv5_port.try_into().unwrap()); - beacon_config.network.enr_tcp4_port = Some(libp2p_tcp_port.try_into().unwrap()); + beacon_config.network.enr_udp4_port = std::num::NonZeroU16::new(v4.disc_port); + beacon_config.network.enr_tcp4_port = std::num::NonZeroU16::new(v4.tcp_port); + beacon_config.network.enr_quic4_port = std::num::NonZeroU16::new(v4.quic_port); beacon_config.network.discv5_config.table_filter = |_| true; - beacon_config.network.proposer_only = is_proposer; - - mock_execution_config.server_config.listen_port = EXECUTION_PORT + count; + beacon_config.network.proposer_only = node_type.is_proposer(); + + let execution_node = if node_type.requires_execution_node() { + let execution_node = + LocalExecutionNode::new(self.context.clone(), mock_execution_config); + + beacon_config.execution_layer = Some(execution_layer::Config { + execution_endpoint: Some( + SensitiveUrl::parse(&execution_node.server.url()).unwrap(), + ), + default_datadir: execution_node.datadir.path().to_path_buf(), + secret_file: Some(execution_node.datadir.path().join("jwt.hex")), + ..Default::default() + }); + Some(execution_node) + } else { + beacon_config.execution_layer = None; + None + }; - // Construct execution node. - let execution_node = LocalExecutionNode::new(self.context.clone(), mock_execution_config); + if node_type.requires_proof_node() { + beacon_config.network.enable_execution_proof = true; + let bn_idx = self.beacon_nodes.read().len(); + let _: execution_layer::test_utils::MockProofNodeClient = + execution_layer::test_utils::register_mock_proof_engine(bn_idx, 400); + let mock_url = + SensitiveUrl::parse(&execution_layer::test_utils::mock_proof_engine_url(bn_idx)) + .expect("mock URL is valid"); + if let Some(el_config) = beacon_config.execution_layer.as_mut() { + el_config.proof_engine_endpoint = Some(mock_url); + } else { + beacon_config.execution_layer = Some(execution_layer::Config { + proof_engine_endpoint: Some(mock_url), + ..Default::default() + }); + } + } - // Pair the beacon node and execution node. - beacon_config.execution_layer = Some(execution_layer::Config { - execution_endpoint: Some(SensitiveUrl::parse(&execution_node.server.url()).unwrap()), - default_datadir: execution_node.datadir.path().to_path_buf(), - secret_file: Some(execution_node.datadir.path().join("jwt.hex")), - ..Default::default() - }); + if node_type.is_proof_verifier() { + beacon_config.chain.optimistic_finalized_sync = true; + } // Construct beacon node using the config, let beacon_node = LocalBeaconNode::production(self.context.clone(), beacon_config).await?; @@ -271,44 +368,55 @@ impl LocalNetwork { Ok((beacon_node, execution_node)) } + async fn boot_node_enr(&self) -> Result, String> { + if self.beacon_nodes.read().is_empty() { + return Ok(None); + } + + for _ in 0..100 { + if let Some(enr) = self + .beacon_nodes + .read() + .first() + .and_then(|bn| bn.client.enr()) + .filter(|e| e.tcp4().is_some_and(|p| p != 0) && e.udp4().is_some_and(|p| p != 0)) + { + return Ok(Some(enr)); + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + Err("Boot node ENR did not get valid TCP and UDP ports within 10 seconds".to_string()) + } + /// Adds a beacon node to the network, connecting to the 0'th beacon node via ENR. pub async fn add_beacon_node( &self, mut beacon_config: ClientConfig, mock_execution_config: MockExecutionConfig, - is_proposer: bool, + node_type: NodeType, ) -> Result<(), String> { - let first_bn_exists: bool; - { - let read_lock = self.beacon_nodes.read(); - let boot_node = read_lock.first(); - first_bn_exists = boot_node.is_some(); - - if let Some(boot_node) = boot_node { - // Modify beacon_config to add boot node details. - beacon_config.network.boot_nodes_enr.push( - boot_node - .client - .enr() - .expect("Bootnode must have a network."), - ); - } - } - let (beacon_node, execution_node) = if first_bn_exists { - // Network already exists. We construct a new node. - self.construct_beacon_node(beacon_config, mock_execution_config, is_proposer) + let (beacon_node, execution_node) = if let Some(boot_node) = self.boot_node_enr().await? { + beacon_config.network.boot_nodes_enr.push(boot_node); + self.construct_beacon_node(beacon_config, mock_execution_config, node_type) .await? } else { // Network does not exist. We construct a boot node. - self.construct_boot_node(beacon_config, mock_execution_config) - .await? + let (bn, en) = self + .construct_boot_node(beacon_config, mock_execution_config) + .await?; + (bn, Some(en)) }; // Add nodes to the network. - self.execution_nodes.write().push(execution_node); - if is_proposer { - self.proposer_nodes.write().push(beacon_node); - } else { - self.beacon_nodes.write().push(beacon_node); + if let Some(execution_node) = execution_node { + self.execution_nodes.write().push(execution_node); + } + match node_type { + NodeType::Proposer => { + self.proposer_nodes.write().push(beacon_node); + } + _ => { + self.beacon_nodes.write().push(beacon_node); + } } Ok(()) } @@ -325,7 +433,7 @@ impl LocalNetwork { ) -> Result<(), String> { epoch_delay(Epoch::new(wait_until_epoch), slot_duration, slots_per_epoch).await; - self.add_beacon_node(beacon_config, mock_execution_config, false) + self.add_beacon_node(beacon_config, mock_execution_config, NodeType::Default) .await?; Ok(()) @@ -338,7 +446,9 @@ impl LocalNetwork { mut validator_config: ValidatorConfig, beacon_node: usize, validator_files: ValidatorFiles, + node_type: NodeType, ) -> Result<(), String> { + let beacon_node_idx = beacon_node; let context = self.context.clone(); let self_1 = self.clone(); let socket_addr = { @@ -368,6 +478,36 @@ impl LocalNetwork { .unwrap(); validator_config.beacon_nodes = vec![beacon_node]; + if node_type.is_proof_generator() { + let token_dir = std::env::temp_dir().join(format!( + "lighthouse-vc-proof-token-{}-{}", + std::process::id(), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| "should get system time")? + .as_nanos() + )); + std::fs::create_dir_all(&token_dir) + .map_err(|e| format!("Unable to create validator API token dir: {e}"))?; + let token_path = token_dir.join(PK_FILENAME); + validator_config.http_api = ValidatorHttpConfig { + enabled: true, + listen_addr: Ipv4Addr::LOCALHOST.into(), + listen_port: 0, + allow_origin: None, + allow_keystore_export: true, + store_passwords_in_secrets_dir: false, + http_token_path: token_path, + bn_long_timeouts: false, + }; + validator_config.proof_engine_endpoint = Some( + SensitiveUrl::parse(&execution_layer::test_utils::mock_proof_engine_url( + beacon_node_idx, + )) + .expect("mock URL is valid"), + ); + } + // If we have a proposer node established, use it. if let Some(proposer_socket_addr) = proposer_socket_addr { let url = SensitiveUrl::parse( @@ -431,6 +571,15 @@ impl LocalNetwork { Ok(()) } + /// Return a HTTP client for the beacon node at `index`. + #[allow(dead_code)] + pub fn remote_node(&self, index: usize) -> Option { + self.beacon_nodes + .read() + .get(index) + .and_then(|n| n.remote_node().ok()) + } + /// For all beacon nodes in `Self`, return a HTTP client to access each nodes HTTP API. pub fn remote_nodes(&self) -> Result, String> { let beacon_nodes = self.beacon_nodes.read(); @@ -443,6 +592,32 @@ impl LocalNetwork { .collect() } + /// Subscribe to mock proof-client events for a beacon node at a specific index. + #[allow(dead_code)] + pub fn node_subscribe_client_events( + &self, + index: usize, + ) -> Option> + { + execution_layer::test_utils::get_mock_proof_engine::(index) + .map(|mock| mock.subscribe_client_events()) + } + + /// Subscribe to the internal event bus for a beacon node at a specific index. + #[allow(dead_code)] + pub fn node_subscribe_internal_events( + &self, + index: usize, + ) -> Option< + tokio::sync::broadcast::Receiver, + > { + self.beacon_nodes.read().get(index).and_then(|bn| { + bn.client + .beacon_chain() + .map(|chain| chain.subscribe_internal_events()) + }) + } + /// Return current epoch of bootnode. pub async fn _bootnode_epoch(&self) -> Result { let nodes = self.remote_nodes().expect("Failed to get remote nodes"); diff --git a/testing/simulator/src/test_utils/builder.rs b/testing/simulator/src/test_utils/builder.rs new file mode 100644 index 00000000000..c2892235eee --- /dev/null +++ b/testing/simulator/src/test_utils/builder.rs @@ -0,0 +1,341 @@ +use crate::local_network::NodeType; + +use super::*; + +type ClientConfigTransform = Box; +type SpecTransform = Box; + +/// Builder for creating test networks with configurable parameters. +pub struct TestNetworkFixtureBuilder { + env: EnvironmentBuilder, + network_params: LocalNetworkParams, + logger_config: LoggerConfig, + disable_stdout: bool, + client_config_transform: Option, + spec_transform: Option, +} + +impl Default for TestNetworkFixtureBuilder { + fn default() -> Self { + Self { + env: EnvironmentBuilder::minimal(), + network_params: LocalNetworkParams { + validator_count: 4, + node_count: 2, + proposer_nodes: 0, + proof_generator_nodes: 0, + proof_verifier_nodes: 0, + extra_nodes: 0, + delayed_nodes: 0, + genesis_delay: 38, + }, + logger_config: LoggerConfig::default(), + disable_stdout: false, + client_config_transform: None, + spec_transform: None, + } + } +} + +impl TestNetworkFixtureBuilder { + /// Set the `EnvironmentBuilder` to use for the network. + pub fn with_env(mut self, env: EnvironmentBuilder) -> Self { + self.env = env; + self + } + + /// Apply an arbitrary modification to the `EnvironmentBuilder` used for the network. + pub fn map_env(mut self, f: impl FnOnce(&mut EnvironmentBuilder)) -> Self { + f(&mut self.env); + self + } + + /// Apply an arbitrary modification to the `ChainSpec` used for the network. + pub fn map_spec(mut self, f: impl FnOnce(&mut ChainSpec) + Send + 'static) -> Self { + self.spec_transform = Some(match self.spec_transform.take() { + None => Box::new(f), + Some(prev) => Box::new(move |spec| { + prev(spec); + f(spec); + }), + }); + self + } + + /// Set the log level. + pub fn with_log_level(mut self, level: LevelFilter) -> Self { + self.logger_config.debug_level = level; + self.logger_config.logfile_debug_level = level; + self + } + + /// Set the log directory. + pub fn with_log_dir(mut self, log_dir: PathBuf) -> Self { + self.logger_config.path = Some(log_dir); + self + } + + /// Apply an arbitrary modification to the `LoggerConfig` used for the network. + pub fn map_logger_config(mut self, f: impl FnOnce(&mut LoggerConfig)) -> Self { + f(&mut self.logger_config); + self + } + + /// Set the network params. + pub fn with_network_params(mut self, network_params: LocalNetworkParams) -> Self { + self.network_params = network_params; + self + } + + /// Apply an arbitrary modification to the `LocalNetworkParams` used for the network. + pub fn map_network_params(mut self, f: impl FnOnce(&mut LocalNetworkParams)) -> Self { + f(&mut self.network_params); + self + } + + /// Apply an arbitrary modification to the `ClientConfig` used for all beacon nodes. + /// + /// Multiple calls are composed in order: the first registered transform runs first. + pub fn map_client_config(mut self, f: impl FnOnce(&mut ClientConfig) + Send + 'static) -> Self { + self.client_config_transform = Some(match self.client_config_transform.take() { + None => Box::new(f), + Some(prev) => Box::new(move |config| { + prev(config); + f(config); + }), + }); + self + } + + /// Build the test network fixture with the specified configuration. + pub async fn build(self) -> anyhow::Result> { + info!(target: "simulator", "Building test network fixture"); + + // initialize the network + let (env, network_params, network, beacon_config, mock_execution_config) = + self.init_network().await?; + + // Initialize beacon nodes + Self::init_beacon_nodes( + &network, + &network_params, + &beacon_config, + &mock_execution_config, + ) + .await?; + + // Initialize validator clients + Self::init_validators(&network, &network_params).await?; + + Ok(TestNetworkFixture { + env, + network, + config: TestConfig { + client: beacon_config, + execution: mock_execution_config, + network_params, + }, + }) + } + + async fn init_validators( + network: &LocalNetwork, + network_params: &LocalNetworkParams, + ) -> anyhow::Result<()> { + info!(target: "simulator", "Building validator clients for {} validators", network_params.validator_count); + let network_params = network_params.clone(); + let task_executor = network.executor(); + + // Generate validator keystores in parallel to speed up setup time + let validator_files = task_executor + .spawn_blocking_handle( + move || -> anyhow::Result> { + let num_beacon_nodes = + network_params.node_count + network_params.proof_generator_nodes; + let validators_per_node = network_params.validator_count / num_beacon_nodes; + + (0..num_beacon_nodes) + .into_par_iter() + .map(|i| -> anyhow::Result { + info!(target: "simulator", + "Generating keystores for validator {} of {}", + i + 1, + num_beacon_nodes + ); + + let indices = (i * validators_per_node..(i + 1) * validators_per_node) + .collect::>(); + + ValidatorFiles::with_keystores(&indices).map_err(anyhow::Error::msg) + }) + .collect::>>() + }, + "validator_keystore_generation", + ) + .ok_or_else(|| anyhow::anyhow!("Failed to spawn blocking task"))? + .await??; + + for (i, files) in validator_files.into_iter().enumerate() { + let network = network.clone(); + let network_params = network_params.clone(); + + task_executor.spawn( + async move { + let mut validator_config = testing_validator_config(); + validator_config.validator_store.fee_recipient = + Some(Into::
::into(SUGGESTED_FEE_RECIPIENT)); + + // Enable broadcast on every 2nd node. + // TODO: do we need this? + if i % 4 == 0 { + validator_config.broadcast_topics = ApiTopic::all(); + let beacon_nodes = vec![i, (i + 1) % network_params.node_count]; + network + .add_validator_client_with_fallbacks( + validator_config, + beacon_nodes, + files, + ) + .await + } else { + let node_type = network_params.node_type(i); + network + .add_validator_client(validator_config, i, files, node_type) + .await + } + .expect("should add validator"); + }, + "validator_client_setup", + ) + } + + Ok(()) + } + + async fn init_beacon_nodes( + network: &LocalNetwork, + network_params: &LocalNetworkParams, + beacon_config: &ClientConfig, + mock_execution_config: &MockExecutionConfig, + ) -> anyhow::Result<()> { + // Build the full list of (NodeType, count) pairs, then spawn all nodes concurrently. + let node_types = [ + (NodeType::Default, network_params.node_count), + (NodeType::Proposer, network_params.proposer_nodes), + ( + NodeType::ProofGenerator, + network_params.proof_generator_nodes, + ), + (NodeType::ProofVerifier, network_params.proof_verifier_nodes), + ]; + + let total: usize = node_types.iter().map(|(_, n)| n).sum(); + info!(target: "simulator", "Spawning {total} beacon nodes"); + + for (node_type, count) in node_types { + for _ in 0..count { + network + .add_beacon_node( + beacon_config.clone(), + mock_execution_config.clone(), + node_type, + ) + .await + .map_err(anyhow::Error::msg)?; + } + } + + Ok(()) + } + + /// Initialize the network environment and create the local network instance. + async fn init_network( + self, + ) -> anyhow::Result<( + TestEnvironment, + LocalNetworkParams, + LocalNetwork, + ClientConfig, + MockExecutionConfig, + )> { + info!(target: "simulator", "Initializing test network environment and local network"); + let Self { + env, + network_params, + logger_config, + disable_stdout, + client_config_transform, + spec_transform, + } = self; + + // Initialize logging + info!(target: "simulator", "Initializing logging with config: {:?}", logger_config); + + let file_mode = if logger_config.is_restricted { + 0o600 + } else { + 0o644 + }; + let (env, stdout_logging_layer, file_logging_layer, _see_logging_layer) = + env.init_tracing(logger_config.clone(), "lighthouse", file_mode); + + //TODO: optionally add discv5 logging layer for network tests + // Instantiate logging layers + let filters = build_workspace_filter().expect("should build workspace filter"); + let mut layers = vec![]; + + if let Some(layer) = (!disable_stdout).then(|| { + stdout_logging_layer + .with_filter(logger_config.debug_level) + .with_filter(filters.clone()) + .boxed() + }) { + layers.push(layer); + } + if let Some(file_logging_layer) = file_logging_layer { + layers.push( + file_logging_layer + .with_filter(logger_config.logfile_debug_level) + .with_filter(filters.clone()) + .boxed(), + ); + } + // Initialize the subscriber with the configured layers + tracing_subscriber::registry().with(layers).try_init()?; + + // Instantiate the environment. + let mut env = env.build_test_environment().map_err(anyhow::Error::msg)?; + + let mut spec = (*env.eth2_config.spec).clone(); + spec.genesis_delay = network_params.genesis_delay; + spec.min_genesis_active_validator_count = network_params.validator_count as u64; + if let Some(transform) = spec_transform { + transform(&mut spec); + } + env.eth2_config.spec = std::sync::Arc::new(spec); + + // Instantiate the local network + info!(target: "simulator", "Initializing local network with params: {:?}", network_params); + let (network, mut beacon_config, mock_execution_config) = + Box::pin(LocalNetwork::create_local_network( + None, + None, + network_params.clone(), + env.core_context(), + )) + .await + .map_err(anyhow::Error::msg)?; + + if let Some(transform) = client_config_transform { + transform(&mut beacon_config); + } + + Ok(( + env, + network_params, + network, + beacon_config, + mock_execution_config, + )) + } +} diff --git a/testing/simulator/src/test_utils/event_stream.rs b/testing/simulator/src/test_utils/event_stream.rs new file mode 100644 index 00000000000..ddd92835f2a --- /dev/null +++ b/testing/simulator/src/test_utils/event_stream.rs @@ -0,0 +1,56 @@ +//! Generic event stream wrapper for broadcast channels. +//! +//! [`EventStream`] wraps a `broadcast::Receiver` and provides ergonomic timeout-based +//! collection helpers used across simulator integration tests. + +use std::time::Duration; +use tokio::sync::broadcast; + +/// Wraps a `broadcast::Receiver` with assertion helpers. +pub struct EventStream { + rx: broadcast::Receiver, +} + +impl From> for EventStream { + fn from(rx: broadcast::Receiver) -> Self { + Self { rx } + } +} + +impl EventStream { + /// Collect `n` events matching `predicate` within `timeout`, or return an error. + pub async fn collect_n( + &mut self, + n: usize, + predicate: impl Fn(&E) -> bool, + timeout: Duration, + ) -> anyhow::Result> { + tokio::time::timeout(timeout, async { + let mut collected = Vec::with_capacity(n); + loop { + match self.rx.recv().await { + Ok(event) if predicate(&event) => { + collected.push(event); + if collected.len() >= n { + return Ok(collected); + } + } + Ok(_) => {} + Err(broadcast::error::RecvError::Lagged(skipped)) => { + return Err(anyhow::anyhow!( + "event stream lagged, skipped {skipped} events" + )); + } + Err(broadcast::error::RecvError::Closed) => { + return Err(anyhow::anyhow!( + "event stream closed before collecting {n} events (got {})", + collected.len() + )); + } + } + } + }) + .await + .map_err(|_| anyhow::anyhow!("timed out after {timeout:?} waiting for {n} events"))? + } +} diff --git a/testing/simulator/src/test_utils/mod.rs b/testing/simulator/src/test_utils/mod.rs new file mode 100644 index 00000000000..6083e9d392d --- /dev/null +++ b/testing/simulator/src/test_utils/mod.rs @@ -0,0 +1,98 @@ +//! Test network builder for creating local beacon node networks. +//! +//! Provides a builder pattern for setting up test networks with beacon nodes, +//! validator clients, and execution nodes. Used by simulator tests like +//! `basic_sim` and `proof_service_sim`. + +pub use crate::local_network::{LocalNetwork, LocalNetworkParams, NodeType}; +pub use beacon_chain::internal_events::InternalBeaconNodeEvent; +pub use environment::{LoggerConfig, test_utils::TestEnvironment}; +pub use eth2::{BeaconNodeHttpClient, types::StateId}; +pub use execution_layer::test_utils::{MockClientEvent, MockEventStream}; +mod event_stream; +pub use event_stream::EventStream; +pub use logging::build_workspace_filter; +pub use node_test_rig::ApiTopic; +pub use node_test_rig::{ + ClientConfig, MockExecutionConfig, ValidatorFiles, environment::EnvironmentBuilder, + testing_validator_config, +}; +use rayon::iter::{IntoParallelIterator, ParallelIterator}; +use std::path::PathBuf; +pub use tracing::{info, level_filters::LevelFilter}; +use tracing_subscriber::{Layer, layer::SubscriberExt, util::SubscriberInitExt}; +pub use types::{Address, ChainSpec, Epoch, EthSpec, MinimalEthSpec}; + +const SUGGESTED_FEE_RECIPIENT: [u8; 20] = + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]; + +mod builder; +pub use builder::TestNetworkFixtureBuilder; + +pub struct TestNetworkFixture { + pub env: TestEnvironment, + pub network: LocalNetwork, + pub config: TestConfig, +} + +pub struct TestConfig { + pub client: ClientConfig, + pub execution: MockExecutionConfig, + pub network_params: LocalNetworkParams, +} + +impl TestNetworkFixture { + pub fn builder() -> TestNetworkFixtureBuilder { + TestNetworkFixtureBuilder::default() + } + + /// Mark all payloads as valid on execution nodes. + pub fn payloads_valid(&mut self) { + self.network + .execution_nodes + .write() + .iter() + .for_each(|node| { + node.server.all_payloads_valid(); + }); + } + + /// Wait for the network to reach genesis by sleeping until the genesis time. + /// If genesis has already passed (late-joining node), returns immediately. + pub async fn wait_for_genesis(&self) -> anyhow::Result<()> { + if let Ok(duration) = self.network.duration_to_genesis().await { + tokio::time::sleep(duration).await; + } + Ok(()) + } +} + +// Ignore this for now because it conflicts with the `proof_engine` testing crate. +// We should migrate to defaulting to unused ports assigned by the OS instead of hardcoding ports. +#[tokio::test] +#[ignore] +async fn test_network_fixture_build() -> anyhow::Result<()> { + let mut fixture = TestNetworkFixtureBuilder::default() + .map_network_params(|params| { + params.genesis_delay = 20; + }) + .map_spec(|spec| { + *spec = spec.clone().set_slot_duration_ms::(1000); + spec.min_genesis_time = 0; + spec.altair_fork_epoch = Some(Epoch::new(0)); + spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + spec.capella_fork_epoch = Some(Epoch::new(0)); + spec.deneb_fork_epoch = Some(Epoch::new(0)); + spec.electra_fork_epoch = Some(Epoch::new(0)); + spec.fulu_fork_epoch = Some(Epoch::new(2)); + }) + .build() + .await?; + fixture.payloads_valid(); + + fixture.wait_for_genesis().await?; + + tokio::time::sleep(std::time::Duration::from_secs(60)).await; + + Ok(()) +} diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index 6990a2f61a7..5d03314506a 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -18,6 +18,7 @@ dirs = { workspace = true } doppelganger_service = { workspace = true } environment = { workspace = true } eth2 = { workspace = true } +execution_layer = { workspace = true } fdlimit = "0.3.0" graffiti_file = { workspace = true } hyper = { workspace = true } @@ -33,6 +34,7 @@ slashing_protection = { workspace = true } slot_clock = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } +typenum = { workspace = true } types = { workspace = true } validator_http_api = { workspace = true } validator_http_metrics = { workspace = true } diff --git a/validator_client/http_api/src/lib.rs b/validator_client/http_api/src/lib.rs index 8e9c077e57b..7ab2ae40290 100644 --- a/validator_client/http_api/src/lib.rs +++ b/validator_client/http_api/src/lib.rs @@ -58,6 +58,7 @@ use tracing::{info, warn}; use types::{ChainSpec, ConfigAndPreset, EthSpec}; use validator_dir::Builder as ValidatorDirBuilder; use validator_services::block_service::BlockService; +use validator_services::proof_service::ProofService; use warp::{Filter, reply::Response, sse::Event}; use warp_utils::reject::convert_rejection; use warp_utils::task::blocking_json_task; @@ -83,7 +84,7 @@ impl From for Error { /// A wrapper around all the items required to spawn the HTTP server. /// /// The server will gracefully handle the case where any fields are `None`. -pub struct Context { +pub struct Context { pub task_executor: TaskExecutor, pub api_secret: ApiSecret, pub block_service: Option, T>>, @@ -96,6 +97,7 @@ pub struct Context { pub config: Config, pub sse_logging_components: Option, pub slot_clock: T, + pub proof_service: Option, T>>>, } /// Configuration for the HTTP server. diff --git a/validator_client/http_api/src/test_utils.rs b/validator_client/http_api/src/test_utils.rs index f83d9f4d526..42e6b00ca90 100644 --- a/validator_client/http_api/src/test_utils.rs +++ b/validator_client/http_api/src/test_utils.rs @@ -142,6 +142,7 @@ impl ApiTester { config: http_config, sse_logging_components: None, slot_clock, + proof_service: None, }); let ctx = context; let (shutdown_tx, shutdown_rx) = oneshot::channel(); diff --git a/validator_client/http_api/src/tests.rs b/validator_client/http_api/src/tests.rs index 5cb631983cc..404fc6c0f81 100644 --- a/validator_client/http_api/src/tests.rs +++ b/validator_client/http_api/src/tests.rs @@ -132,6 +132,7 @@ impl ApiTester { }, sse_logging_components: None, slot_clock: slot_clock.clone(), + proof_service: None, }); let ctx = context.clone(); let (listening_socket, server) = diff --git a/validator_client/lighthouse_validator_store/src/lib.rs b/validator_client/lighthouse_validator_store/src/lib.rs index cc9729b44d9..38700870b8d 100644 --- a/validator_client/lighthouse_validator_store/src/lib.rs +++ b/validator_client/lighthouse_validator_store/src/lib.rs @@ -1,5 +1,5 @@ use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition}; -use bls::{PublicKeyBytes, Signature}; +use bls::{PublicKeyBytes, Signature, SignatureBytes}; use doppelganger_service::DoppelgangerService; use eth2::types::PublishBlockRequest; use futures::{Stream, future::join_all, stream}; @@ -20,14 +20,14 @@ use task_executor::TaskExecutor; use tracing::{Instrument, debug, error, info, info_span, instrument, warn}; use types::{ AbstractExecPayload, Address, AggregateAndProof, Attestation, BeaconBlock, BlindedPayload, - ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, ExecutionPayloadEnvelope, Fork, - FullPayload, Graffiti, Hash256, PayloadAttestationData, PayloadAttestationMessage, - ProposerPreferences, SelectionProof, SignedAggregateAndProof, SignedBeaconBlock, - SignedContributionAndProof, SignedExecutionPayloadEnvelope, SignedProposerPreferences, - SignedRoot, SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, - SyncAggregatorSelectionData, SyncCommitteeContribution, SyncCommitteeMessage, - SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, VoluntaryExit, - graffiti::GraffitiString, + ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, ExecutionPayloadEnvelope, + ExecutionProof, Fork, FullPayload, Graffiti, Hash256, PayloadAttestationData, + PayloadAttestationMessage, ProposerPreferences, SelectionProof, SignedAggregateAndProof, + SignedBeaconBlock, SignedContributionAndProof, SignedExecutionPayloadEnvelope, + SignedExecutionProof, SignedProposerPreferences, SignedRoot, SignedValidatorRegistrationData, + SignedVoluntaryExit, Slot, SyncAggregatorSelectionData, SyncCommitteeContribution, + SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, + VoluntaryExit, graffiti::GraffitiString, }; use validator_store::{ AggregateToSign, AttestationToSign, ContributionToSign, DoppelgangerStatus, @@ -1348,6 +1348,43 @@ impl ValidatorStore for LighthouseValidatorS }) } + async fn sign_execution_proof( + &self, + validator_pubkey: PublicKeyBytes, + execution_proof: ExecutionProof, + signing_epoch: Epoch, + ) -> Result { + let signing_context = self.signing_context(Domain::ExecutionProof, signing_epoch); + let signing_method = self.doppelganger_bypassed_signing_method(validator_pubkey)?; + + let signature = signing_method + .get_signature::>( + SignableMessage::ExecutionProof(&execution_proof), + signing_context, + &self.spec, + &self.task_executor, + ) + .await?; + + let validator_index = self + .validator_index(&validator_pubkey) + .ok_or(ValidatorStoreError::UnknownPubkey(validator_pubkey))?; + + let signature = SignatureBytes::deserialize(&signature.serialize()) + .map_err(|_| Error::Middleware("Failed to serialize signature".to_string()))?; + + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_EXECUTION_PROOFS_TOTAL, + &[validator_metrics::SUCCESS], + ); + + Ok(SignedExecutionProof { + message: execution_proof, + validator_index, + signature, + }) + } + /// Prune the slashing protection database so that it remains performant. /// /// This function will only do actual pruning periodically, so it should usually be diff --git a/validator_client/signing_method/src/lib.rs b/validator_client/signing_method/src/lib.rs index 0dfde989464..932ca46b31f 100644 --- a/validator_client/signing_method/src/lib.rs +++ b/validator_client/signing_method/src/lib.rs @@ -49,6 +49,7 @@ pub enum SignableMessage<'a, E: EthSpec, Payload: AbstractExecPayload = FullP SignedContributionAndProof(&'a ContributionAndProof), ValidatorRegistration(&'a ValidatorRegistrationData), VoluntaryExit(&'a VoluntaryExit), + ExecutionProof(&'a ExecutionProof), ExecutionPayloadEnvelope(&'a ExecutionPayloadEnvelope), PayloadAttestationData(&'a PayloadAttestationData), ProposerPreferences(&'a ProposerPreferences), @@ -73,6 +74,7 @@ impl> SignableMessage<'_, E, Payload SignableMessage::SignedContributionAndProof(c) => c.signing_root(domain), SignableMessage::ValidatorRegistration(v) => v.signing_root(domain), SignableMessage::VoluntaryExit(exit) => exit.signing_root(domain), + SignableMessage::ExecutionProof(proof) => proof.signing_root(domain), SignableMessage::ExecutionPayloadEnvelope(e) => e.signing_root(domain), SignableMessage::PayloadAttestationData(d) => d.signing_root(domain), SignableMessage::ProposerPreferences(p) => p.signing_root(domain), @@ -239,6 +241,7 @@ impl SigningMethod { Web3SignerObject::ValidatorRegistration(v) } SignableMessage::VoluntaryExit(e) => Web3SignerObject::VoluntaryExit(e), + SignableMessage::ExecutionProof(p) => Web3SignerObject::ExecutionProof(p), SignableMessage::ExecutionPayloadEnvelope(e) => { Web3SignerObject::ExecutionPayloadEnvelope(e) } diff --git a/validator_client/signing_method/src/web3signer.rs b/validator_client/signing_method/src/web3signer.rs index baabb379479..2ac3e722105 100644 --- a/validator_client/signing_method/src/web3signer.rs +++ b/validator_client/signing_method/src/web3signer.rs @@ -19,6 +19,7 @@ pub enum MessageType { SyncCommitteeSelectionProof, SyncCommitteeContributionAndProof, ValidatorRegistration, + ExecutionProof, // TODO(gloas) verify w/ web3signer specs ExecutionPayloadEnvelope, PayloadAttestation, @@ -79,6 +80,7 @@ pub enum Web3SignerObject<'a, E: EthSpec, Payload: AbstractExecPayload> { SyncAggregatorSelectionData(&'a SyncAggregatorSelectionData), ContributionAndProof(&'a ContributionAndProof), ValidatorRegistration(&'a ValidatorRegistrationData), + ExecutionProof(&'a ExecutionProof), ExecutionPayloadEnvelope(&'a ExecutionPayloadEnvelope), PayloadAttestationData(&'a PayloadAttestationData), ProposerPreferences(&'a ProposerPreferences), @@ -147,6 +149,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, E, Pa MessageType::SyncCommitteeContributionAndProof } Web3SignerObject::ValidatorRegistration(_) => MessageType::ValidatorRegistration, + Web3SignerObject::ExecutionProof(_) => MessageType::ExecutionProof, Web3SignerObject::ExecutionPayloadEnvelope(_) => MessageType::ExecutionPayloadEnvelope, Web3SignerObject::PayloadAttestationData(_) => MessageType::PayloadAttestation, Web3SignerObject::ProposerPreferences(_) => MessageType::ProposerPreferences, diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index 0eb0e9e5dda..46c772d9b7f 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -519,4 +519,26 @@ pub struct ValidatorClient { display_order = 0 )] pub web3_signer_max_idle_connections: Option, + + #[clap( + long, + value_name = "HTTP-JSON-RPC-URL", + help = "URL of the proof engine HTTP JSON-RPC endpoint for EIP-8025 execution proofs. \ + When set, the validator client will monitor new blocks, request proofs from this \ + endpoint, sign completed proofs, and submit them to the beacon node.", + display_order = 0 + )] + pub proof_engine_endpoint: Option, + + #[clap( + long, + value_name = "TYPES", + value_delimiter = ',', + requires = "proof_engine_endpoint", + help = "Comma-separated list of proof type identifiers (u8) to request from the proof engine \ + (e.g., 0,1,2). If not specified, defaults to '0,1,2,3' \ + (EthrexRisc0, EthrexSP1, EthrexZisk, RethOpenVM).", + display_order = 0 + )] + pub proof_types: Option>, } diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index d68a78b705f..2dc8878ef31 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -8,6 +8,7 @@ use directory::{ get_network_dir, }; use eth2::types::{Graffiti, GraffitiPolicy}; +use execution_layer::eip8025::types::ProofTypes; use graffiti_file::GraffitiFile; use initialized_validators::Config as InitializedValidatorsConfig; use lighthouse_validator_store::Config as ValidatorStoreConfig; @@ -18,7 +19,9 @@ use std::net::IpAddr; use std::path::PathBuf; use std::time::Duration; use tracing::{info, warn}; +use typenum::Unsigned; use types::GRAFFITI_BYTES_LEN; +use types::execution::eip8025::MaxExecutionProofsPerPayload; use validator_http_api::{self, PK_FILENAME}; use validator_http_metrics; @@ -92,6 +95,11 @@ pub struct Config { #[serde(flatten)] pub initialized_validators: InitializedValidatorsConfig, pub disable_attesting: bool, + /// URL of the proof engine HTTP JSON-RPC endpoint for EIP-8025 execution proofs. + pub proof_engine_endpoint: Option, + /// Proof types to request from the proof engine. + #[serde(default)] + pub proof_types: ProofTypes, } impl Default for Config { @@ -139,6 +147,8 @@ impl Default for Config { distributed: false, initialized_validators: <_>::default(), disable_attesting: false, + proof_engine_endpoint: None, + proof_types: ProofTypes::default(), } } } @@ -284,6 +294,36 @@ impl Config { .web3_signer_max_idle_connections = Some(n); } + /* + * Proof Engine (EIP-8025) + */ + if let Some(proof_engine_endpoint) = validator_client_config.proof_engine_endpoint.as_ref() + { + config.proof_engine_endpoint = Some( + SensitiveUrl::parse(proof_engine_endpoint) + .map_err(|e| format!("Unable to parse proof engine URL: {:?}", e))?, + ); + } + + config.proof_types = if let Some(vals) = &validator_client_config.proof_types { + use execution_layer::eip8025::types::ProofType; + let proof_types = vals + .iter() + .copied() + .map(ProofType::from_u8) + .collect::, _>>() + .map_err(|e| format!("Invalid --proof-types value: {e:?}"))?; + if proof_types.len() > MaxExecutionProofsPerPayload::to_usize() { + return Err(format!( + "--proof-types supports at most {} values", + MaxExecutionProofsPerPayload::to_usize() + )); + } + ProofTypes::from(proof_types) + } else { + ProofTypes::default() + }; + /* * Http API server */ diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 71d93334935..7e7556c66f1 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -47,6 +47,7 @@ use validator_services::{ latency_service, payload_attestation_service::PayloadAttestationService, preparation_service::{PreparationService, PreparationServiceBuilder}, + proof_service::ProofService, proposer_preferences_service::ProposerPreferencesService, sync_committee_service::SyncCommitteeService, }; @@ -95,6 +96,7 @@ pub struct ProductionValidatorClient { http_api_listen_addr: Option, config: Config, genesis_time: u64, + proof_service: Option, SystemTimeSlotClock>>>, } impl ProductionValidatorClient { @@ -575,6 +577,35 @@ impl ProductionValidatorClient { context.eth2_config.spec.clone(), ); + let proof_service = config.proof_engine_endpoint.as_ref().map(|endpoint| { + info!(endpoint = %endpoint, "Initializing proof engine client"); + let url_str = endpoint.expose_full(); + let proof_engine_client = Arc::new( + if let Some(idx) = execution_layer::test_utils::parse_mock_index(url_str.as_str()) { + let mock = execution_layer::test_utils::get_mock_proof_engine::(idx) + .unwrap_or_else(|| { + debug!( + idx, + "No pre-registered mock; creating MockProofNodeClient on the fly" + ); + execution_layer::test_utils::register_mock_proof_engine::(idx, 0) + }); + execution_layer::eip8025::HttpProofEngine::with_proof_node(mock) + } else { + execution_layer::eip8025::HttpProofEngine::new(endpoint.clone(), None) + }, + ); + + Arc::new(ProofService::new( + validator_store.clone(), + beacon_nodes.clone(), + proof_engine_client, + slot_clock.clone(), + context.executor.clone(), + config.proof_types.clone(), + )) + }); + Ok(Self { context, duties_service, @@ -590,6 +621,7 @@ impl ProductionValidatorClient { slot_clock, http_api_listen_addr: None, genesis_time, + proof_service, }) } @@ -616,6 +648,7 @@ impl ProductionValidatorClient { config: self.config.http_api.clone(), sse_logging_components: self.context.sse_logging_components.clone(), slot_clock: self.slot_clock.clone(), + proof_service: self.proof_service.clone(), }); let exit = self.context.executor.exit(); @@ -684,6 +717,13 @@ impl ProductionValidatorClient { info!("Doppelganger protection disabled.") } + if let Some(proof_service) = &self.proof_service { + proof_service + .clone() + .start_service() + .map_err(|e| format!("Unable to start proof service: {}", e))?; + } + let context = self.context.clone(); spawn_notifier( self.duties_service.clone(), diff --git a/validator_client/validator_metrics/src/lib.rs b/validator_client/validator_metrics/src/lib.rs index 46a86381f91..0d1fb4a8cfd 100644 --- a/validator_client/validator_metrics/src/lib.rs +++ b/validator_client/validator_metrics/src/lib.rs @@ -126,6 +126,13 @@ pub static SIGNED_VALIDATOR_REGISTRATIONS_TOTAL: LazyLock> &["status"], ) }); +pub static SIGNED_EXECUTION_PROOFS_TOTAL: LazyLock> = LazyLock::new(|| { + try_create_int_counter_vec( + "vc_signed_execution_proofs_total", + "Total count of ExecutionProof signings", + &["status"], + ) +}); pub static DUTIES_SERVICE_TIMES: LazyLock> = LazyLock::new(|| { try_create_histogram_vec( "vc_duties_service_task_times_seconds", diff --git a/validator_client/validator_services/Cargo.toml b/validator_client/validator_services/Cargo.toml index 25829682655..93ee2dcfb26 100644 --- a/validator_client/validator_services/Cargo.toml +++ b/validator_client/validator_services/Cargo.toml @@ -8,7 +8,8 @@ authors = ["Sigma Prime "] beacon_node_fallback = { workspace = true } bls = { workspace = true } either = { workspace = true } -eth2 = { workspace = true } +eth2 = { workspace = true, features = ["events"] } +execution_layer = { workspace = true } futures = { workspace = true } graffiti_file = { workspace = true } logging = { workspace = true } diff --git a/validator_client/validator_services/src/lib.rs b/validator_client/validator_services/src/lib.rs index c39ef4499b7..23e69faf348 100644 --- a/validator_client/validator_services/src/lib.rs +++ b/validator_client/validator_services/src/lib.rs @@ -5,6 +5,7 @@ pub mod latency_service; pub mod notifier_service; pub mod payload_attestation_service; pub mod preparation_service; +pub mod proof_service; pub mod proposer_preferences_service; pub mod sync; pub mod sync_committee_service; diff --git a/validator_client/validator_services/src/proof_service.rs b/validator_client/validator_services/src/proof_service.rs new file mode 100644 index 00000000000..05dc1c01aaf --- /dev/null +++ b/validator_client/validator_services/src/proof_service.rs @@ -0,0 +1,387 @@ +//! EIP-8025 execution proof service. +//! +//! This service requests execution proofs, signs completed local proof material, +//! and submits signed proofs to the beacon node. + +use beacon_node_fallback::BeaconNodeFallback; +use eth2::types::{BlockId, EventKind, EventTopic}; +use execution_layer::NewPayloadRequest; +use execution_layer::eip8025::types::ProofTypes; +use execution_layer::eip8025::{HttpProofEngine, ProofEvent}; +use futures::StreamExt; +use parking_lot::RwLock; +use slot_clock::SlotClock; +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use task_executor::TaskExecutor; +use tracing::{debug, error, info, warn}; +use types::execution::eip8025::{ProofAttributes, ProofData, PublicInput}; +use types::{EthSpec, ExecutionProof, Hash256}; +use validator_store::{DoppelgangerStatus, ValidatorStore}; + +/// Discard tracking entries older than this. +const PROOF_REQUEST_STALE_TIMEOUT: Duration = Duration::from_secs(300); + +/// An outstanding proof request awaiting completion from the proof engine. +struct OutstandingProofRequest { + /// Proof types we are still waiting for. + pending_proof_types: HashSet, + /// Slot of the block, for epoch derivation during signing. + slot: types::Slot, + /// When the request was made. + requested_at: Instant, +} + +/// Background service for execution proof handling. +pub struct ProofService { + inner: Arc>, +} + +struct Inner { + validator_store: Arc, + beacon_nodes: Arc>, + proof_engine: Arc, + executor: TaskExecutor, + proof_types: Vec, + outstanding_requests: RwLock>, +} + +impl ProofService { + /// Create a new proof service. + pub fn new( + validator_store: Arc, + beacon_nodes: Arc>, + proof_engine: Arc, + _slot_clock: T, + executor: TaskExecutor, + proof_types: ProofTypes, + ) -> Self { + let proof_types = proof_types + .iter() + .map(|proof_type| proof_type.to_u8()) + .collect(); + + Self { + inner: Arc::new(Inner { + validator_store, + beacon_nodes, + proof_engine, + executor, + proof_types, + outstanding_requests: RwLock::new(HashMap::new()), + }), + } + } + + /// Start the proof service background task. + pub fn start_service(self: Arc) -> Result<(), String> { + let inner = self.inner.clone(); + self.inner.executor.spawn( + async move { inner.monitor_task().await }, + "proof_service_monitor", + ); + + info!("Proof service started - monitoring beacon and proof engine events"); + Ok(()) + } +} + +impl Inner { + async fn subscribe_to_events( + &self, + ) -> Result< + impl futures::Stream, eth2::Error>>, + String, + > { + self.beacon_nodes + .first_success( + |node| async move { node.get_events::(&[EventTopic::Block]).await }, + ) + .await + .map_err(|e| format!("All beacon nodes failed to provide event stream: {}", e)) + } + + /// Monitor beacon-node block events and proof-engine events over SSE. + async fn monitor_task(self: Arc) { + info!("Starting proof service event monitoring via SSE"); + + loop { + let mut beacon_stream = match self.subscribe_to_events().await { + Ok(stream) => { + info!("Successfully subscribed to block events"); + stream + } + Err(e) => { + error!(error = %e, "Failed to subscribe to block events, retrying"); + tokio::time::sleep(Duration::from_secs(2)).await; + continue; + } + }; + let mut proof_stream = self.proof_engine.subscribe_proof_events(None); + let mut cleanup_interval = tokio::time::interval(PROOF_REQUEST_STALE_TIMEOUT); + cleanup_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); + cleanup_interval.tick().await; + + loop { + tokio::select! { + event_result = beacon_stream.next() => { + match event_result { + Some(Ok(EventKind::Block(sse_block))) => { + if sse_block.execution_optimistic { + debug!( + slot = sse_block.slot.as_u64(), + "Skipping execution optimistic block" + ); + continue; + } + self.handle_block_event(sse_block.block, sse_block.slot).await; + } + Some(Ok(_)) => {} + Some(Err(e)) => { + warn!(error = %e, "Beacon event stream error, will reconnect"); + break; + } + None => { + warn!("Beacon event stream ended, reconnecting"); + break; + } + } + } + event = proof_stream.next() => { + match event { + Some(Ok(proof_event)) => { + self.handle_proof_engine_event(proof_event).await; + } + Some(Err(e)) => { + warn!(error = %e, "Proof engine SSE error, will reconnect"); + break; + } + None => { + warn!("Proof engine SSE stream ended, reconnecting"); + break; + } + } + } + _ = cleanup_interval.tick() => { + self.cleanup_stale_requests(); + } + } + } + + tokio::time::sleep(Duration::from_secs(2)).await; + } + } + + /// Handle a new block event by fetching the full block via RPC then requesting proofs. + async fn handle_block_event(&self, block_root: Hash256, slot: types::Slot) { + info!( + slot = slot.as_u64(), + block = %block_root, + "New block detected, fetching full block via RPC" + ); + + let signed_block = match self + .beacon_nodes + .first_success(|node| async move { + node.get_beacon_blocks::(BlockId::Root(block_root)) + .await + }) + .await + { + Ok(Some(response)) => response.data().clone(), + Ok(None) => { + warn!(block = %block_root, "Block not found on beacon node"); + return; + } + Err(e) => { + error!(block = %block_root, error = %e, "Failed to fetch block via RPC"); + return; + } + }; + + let new_payload_request = match NewPayloadRequest::try_from(signed_block.message()) { + Ok(req) => req, + Err(e) => { + error!(block = %block_root, error = ?e, "Failed to construct NewPayloadRequest"); + return; + } + }; + + let proof_attributes = ProofAttributes { + proof_types: self.proof_types.clone(), + }; + + match self + .proof_engine + .request_proofs(new_payload_request, proof_attributes) + .await + { + Ok(new_payload_request_root) => { + let pending_proof_types = self.proof_types.iter().copied().collect::>(); + let num_types = pending_proof_types.len(); + self.outstanding_requests.write().insert( + new_payload_request_root, + OutstandingProofRequest { + pending_proof_types, + slot, + requested_at: Instant::now(), + }, + ); + debug!( + root = %new_payload_request_root, + block = %block_root, + num_proof_types = num_types, + "Proof generation requested, tracking for completion" + ); + } + Err(e) => { + error!(block = %block_root, error = ?e, "Failed to request proofs from proof engine"); + } + } + } + + /// Process a single proof-engine SSE event. + async fn handle_proof_engine_event(&self, event: ProofEvent) { + let root = event.new_payload_request_root(); + let proof_type = event.proof_type(); + + let is_tracked = self + .outstanding_requests + .read() + .get(&root) + .map(|req| req.pending_proof_types.contains(&proof_type)) + .unwrap_or(false); + + if !is_tracked { + return; + } + + match event { + ProofEvent::ProofComplete(complete) => { + self.handle_proof_complete(complete.new_payload_request_root, complete.proof_type) + .await; + } + ProofEvent::ProofFailure(failure) => { + warn!( + root = %failure.new_payload_request_root, + proof_type = failure.proof_type, + reason = ?failure.reason, + error = %failure.error, + "Proof generation failed" + ); + self.remove_pending_proof_type( + failure.new_payload_request_root, + failure.proof_type, + ); + } + } + } + + /// Fetch a completed proof from the proof engine, sign it, and submit to the beacon node. + async fn handle_proof_complete(&self, root: Hash256, proof_type: u8) { + let proof_bytes = match self.proof_engine.get_proof(root, proof_type).await { + Ok(bytes) => bytes, + Err(e) => { + error!(root = %root, proof_type, error = ?e, "Failed to fetch completed proof"); + return; + } + }; + + let proof_data = match ProofData::new(proof_bytes.to_vec()) { + Ok(data) => data, + Err(e) => { + error!(root = %root, proof_type, error = ?e, "Proof data exceeds max size"); + return; + } + }; + + let execution_proof = ExecutionProof { + proof_data, + proof_type, + public_input: PublicInput { + new_payload_request_root: root, + }, + }; + + let epoch = self + .outstanding_requests + .read() + .get(&root) + .map(|req| req.slot.epoch(S::E::slots_per_epoch())); + + let Some(epoch) = epoch else { + return; + }; + + let Some(pubkey) = self + .validator_store + .voting_pubkeys::, _>(DoppelgangerStatus::only_safe) + .first() + .cloned() + else { + warn!("No safe validators available to sign completed proof"); + return; + }; + + match self + .validator_store + .sign_execution_proof(pubkey, execution_proof, epoch) + .await + { + Ok(signed_proof) => { + match self + .beacon_nodes + .first_success(move |node| { + let proof = signed_proof.clone(); + async move { node.post_beacon_pool_execution_proofs(&[proof]).await } + }) + .await + { + Ok(_) => { + info!(root = %root, proof_type, ?pubkey, "Completed proof signed and submitted"); + } + Err(e) => { + warn!(root = %root, proof_type, error = %e, "Failed to submit completed proof"); + } + } + } + Err(e) => { + warn!(root = %root, proof_type, error = ?e, "Failed to sign completed proof"); + } + } + + self.remove_pending_proof_type(root, proof_type); + } + + /// Remove a single proof type from an outstanding request. + /// + /// If all requested proof types have been resolved the entry is removed entirely. + fn remove_pending_proof_type(&self, root: Hash256, proof_type: u8) { + let mut requests = self.outstanding_requests.write(); + if let Some(entry) = requests.get_mut(&root) { + entry.pending_proof_types.remove(&proof_type); + if entry.pending_proof_types.is_empty() { + requests.remove(&root); + debug!(root = %root, "All proof types resolved, removing from tracker"); + } + } + } + + /// Remove outstanding requests that have exceeded the stale timeout. + fn cleanup_stale_requests(&self) { + let mut requests = self.outstanding_requests.write(); + let before = requests.len(); + requests.retain(|root, req| { + let stale = req.requested_at.elapsed() > PROOF_REQUEST_STALE_TIMEOUT; + if stale { + warn!(root = %root, "Removing stale proof request"); + } + !stale + }); + let removed = before - requests.len(); + if removed > 0 { + info!(removed, "Cleaned up stale proof requests"); + } + } +} diff --git a/validator_client/validator_store/src/lib.rs b/validator_client/validator_store/src/lib.rs index d40c7994f11..b096df32de7 100644 --- a/validator_client/validator_store/src/lib.rs +++ b/validator_client/validator_store/src/lib.rs @@ -7,11 +7,12 @@ use std::future::Future; use std::sync::Arc; use types::{ Address, Attestation, AttestationError, BlindedBeaconBlock, Epoch, EthSpec, - ExecutionPayloadEnvelope, Graffiti, Hash256, PayloadAttestationData, PayloadAttestationMessage, - ProposerPreferences, SelectionProof, SignedAggregateAndProof, SignedBlindedBeaconBlock, - SignedContributionAndProof, SignedExecutionPayloadEnvelope, SignedProposerPreferences, - SignedValidatorRegistrationData, Slot, SyncCommitteeContribution, SyncCommitteeMessage, - SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, + ExecutionPayloadEnvelope, ExecutionProof, Graffiti, Hash256, PayloadAttestationData, + PayloadAttestationMessage, ProposerPreferences, SelectionProof, SignedAggregateAndProof, + SignedBlindedBeaconBlock, SignedContributionAndProof, SignedExecutionPayloadEnvelope, + SignedExecutionProof, SignedProposerPreferences, SignedValidatorRegistrationData, Slot, + SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, + ValidatorRegistrationData, }; #[derive(Debug, PartialEq, Clone)] @@ -192,6 +193,14 @@ pub trait ValidatorStore: Send + Sync { contributions: Vec>, ) -> impl Stream>, Error>> + Send; + /// Sign an execution proof for EIP-8025 optional execution verification. + fn sign_execution_proof( + &self, + validator_pubkey: PublicKeyBytes, + execution_proof: ExecutionProof, + signing_epoch: Epoch, + ) -> impl Future>> + Send; + /// Prune the slashing protection database so that it remains performant. /// /// This function will only do actual pruning periodically, so it should usually be