Compare commits

...

195 Commits

Author SHA1 Message Date
nisdas
ebcc2aeaf7 Eliminate per-validator heap allocations in epoch processing
Replace ReadOnlyValidator interface boxing with direct CompactValidator field
access throughout epoch processing, removing ~23% of all heap allocations
(12.95 GB cumulative per profiling session). Key changes:

- Add ForEachValidator and ApplyToEveryCompactValidator for zero-alloc
  validator iteration (read-only and read-write paths respectively)
- Add EffectiveBalanceAtIndex for direct effective balance reads without
  interface boxing
- Rewrite ProcessEffectiveBalanceUpdates (pre-Electra and Electra+) to
  use ApplyToEveryCompactValidator with two-pass collect-then-mutate
- Rewrite BaseRewardWithTotalBalance to use EffectiveBalanceAtIndex
- Convert InitializePrecomputeValidators, ProcessSlashingsPrecompute,
  ProcessRegistryUpdates and related helpers to use ForEachValidator

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 13:32:08 +01:00
Manu NALEPA
39b027c6c2 CoW merkleLayers via ref-counted sharedMerkleLayers
Replace deep-copy triple-nested loop in Copy() with ref-counted
sharedMerkleLayers. The layers are materialized only when
recomputeDirtyFields actually mutates, saving significant allocation
overhead during state copies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 13:26:45 +01:00
Manu NALEPA
76d0b5706e Add changelog 2026-03-17 22:52:17 +01:00
Manu NALEPA
7df70bdffe getVerifyingState: Check in cache before replaying state. 2026-03-17 22:48:33 +01:00
Manu NALEPA
33143139d4 getVerifyingState: optimize state replay for concurrent data column verification. 2026-03-17 22:23:28 +01:00
Manu NALEPA
14d801a4da Add useful logs 2026-03-17 22:23:28 +01:00
Manu NALEPA
be4850ebba Add changelog 2026-03-17 17:24:55 +01:00
Manu NALEPA
cf75e1bc6b StateByRootIfCachedNoCopy: Check also in the epoch boundary state cache. 2026-03-17 17:24:55 +01:00
Manu NALEPA
47d4c915d3 Use state.ReadOnlyBeaconState instead of state.BeaconState when possible.
It avoids useless state copies.
2026-03-17 17:24:52 +01:00
satushh
93f7214b32 Voluntary exit gloas (#16527)
**What type of PR is this?**

Feature

**What does this PR do? Why is it needed?**

This PR implements voluntary exit specific logic for gloas builder,
specifically the following ones:

-
[get_pending_balance_to_withdraw_for_builder](https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#new-get_pending_balance_to_withdraw_for_builder)
-
[process_voluntary_exit](https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#modified-process_voluntary_exit)
-
[initiate_builder_exit](https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#new-initiate_builder_exit)
-
[MIN_BUILDER_WITHDRAWABILITY_DELAY](https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#time-parameters)

---------

Co-authored-by: Potuz <potuz@prysmaticlabs.com>
2026-03-17 15:33:09 +00:00
Manu NALEPA
1bffcc84f4 Beacon state: Replace *ethpb.Validator by CompactValidator (#16535)
**What type of PR is this?**
Optimisation (@nisdas)

**What does this PR do? Why is it needed?**
The beacon state contains the list of all validators that have been at
least once deposited on the network.

| Network  | Active       | Total                | Ratio      |
|----------|-----------|---------------|---------|
| Hoodi      | 1,224,855 | **1,294,521**  | 94.1%   |
| Mainnet  | 948,883   | **2,229,313** | 43.56% |

Currently, the validators are stored in the beacon state like this:
```go
type BeaconState struct {
...
	validatorsMultiValue                *MultiValueValidators
...
}

type MultiValueValidators = multi_value_slice.Slice[*ethpb.Validator]

type Validator struct {
	state                      protoimpl.MessageState                                            `protogen:"open.v1"`
	PublicKey                  []byte                                                            `protobuf:"bytes,1,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty" spec-name:"pubkey" ssz-size:"48"`
	WithdrawalCredentials      []byte                                                            `protobuf:"bytes,2,opt,name=withdrawal_credentials,json=withdrawalCredentials,proto3" json:"withdrawal_credentials,omitempty" ssz-size:"32"`
	EffectiveBalance           uint64                                                            `protobuf:"varint,3,opt,name=effective_balance,json=effectiveBalance,proto3" json:"effective_balance,omitempty"`
	Slashed                    bool                                                              `protobuf:"varint,4,opt,name=slashed,proto3" json:"slashed,omitempty"`
	ActivationEligibilityEpoch github_com_OffchainLabs_prysm_v7_consensus_types_primitives.Epoch `protobuf:"varint,5,opt,name=activation_eligibility_epoch,json=activationEligibilityEpoch,proto3" json:"activation_eligibility_epoch,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/primitives.Epoch"`
	ActivationEpoch            github_com_OffchainLabs_prysm_v7_consensus_types_primitives.Epoch `protobuf:"varint,6,opt,name=activation_epoch,json=activationEpoch,proto3" json:"activation_epoch,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/primitives.Epoch"`
	ExitEpoch                  github_com_OffchainLabs_prysm_v7_consensus_types_primitives.Epoch `protobuf:"varint,7,opt,name=exit_epoch,json=exitEpoch,proto3" json:"exit_epoch,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/primitives.Epoch"`
	WithdrawableEpoch          github_com_OffchainLabs_prysm_v7_consensus_types_primitives.Epoch `protobuf:"varint,8,opt,name=withdrawable_epoch,json=withdrawableEpoch,proto3" json:"withdrawable_epoch,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/primitives.Epoch"`
	unknownFields              protoimpl.UnknownFields
	sizeCache                  protoimpl.SizeCache
}
```

Some fields, used only by protobuf (`state`, `unknownFields`,
`sizeCache`) are not used by the beacon node.
Some other fields, stored as slices (`PublicKey`,
`WithdrawalCredentials`) need extra memory (`ptr+len+cap`) while they
could be stored as arrays because their sizes are fixed and known in
advance.

We define a new custom type called `CompactValidator`, which is a
fixed-size, pointer-free representation of a validator:

```go
type CompactValidator struct {
	PublicKey                  [fieldparams.BLSPubkeyLength]byte // 48 bytes
	WithdrawalCredentials      [32]byte                          // 32 bytes
	EffectiveBalance           uint64                            // 8 bytes
	Slashed                    bool                              // 1 byte
	ActivationEligibilityEpoch primitives.Epoch                  // 8 bytes
	ActivationEpoch            primitives.Epoch                  // 8 bytes
	ExitEpoch                  primitives.Epoch                  // 8 bytes
	WithdrawableEpoch          primitives.Epoch                  // 8 bytes
}
```

Here is the comparison, per validator, between storing a
`*ethpb.Validator` and a `CompactValidator`.

| Component | `*ethpb.Validator` | `CompactValidator` | Wasted |

|------------------------------------------|--------------------|--------------------|---------|
| Pointer to struct | 8 B | 0 B | 8 B |
| `state` (protoimpl.MessageState) | 8 B | 0 B | 8 B |
| `PublicKey` slice header (ptr+len+cap) | 24 B | 0 B | 24 B |
| `PublicKey` backing array (heap) | 48 B | 48 B (inline) | 0 B |
| `PublicKey` malloc overhead | ~16 B | 0 B | ~16 B |
| `WithdrawalCredentials` slice header | 24 B | 0 B | 24 B |
| `WithdrawalCredentials` backing array | 32 B | 32 B (inline) | 0 B |
| `WithdrawalCredentials` malloc overhead | ~16 B | 0 B | ~16 B |
| `EffectiveBalance` (uint64) | 8 B | 8 B | 0 B |
| `Slashed` (bool + padding) | 8 B | 8 B | 0 B |
| `ActivationEligibilityEpoch` | 8 B | 8 B | 0 B |
| `ActivationEpoch` | 8 B | 8 B | 0 B |
| `ExitEpoch` | 8 B | 8 B | 0 B |
| `WithdrawableEpoch` | 8 B | 8 B | 0 B |
| `unknownFields` ([]byte header) | 24 B | 0 B | 24 B |
| `sizeCache` (int32 + padding) | 8 B | 0 B | 8 B |
| Struct malloc overhead | ~16 B | 0 B | ~16 B |
| **Total** | **~264 B** | **128 B** | **~136 B (52%)** |

We waste `~136 B` per validator.
With a total of `2,229,313` validators (mainnet), it represents `~290
MB`.
Storing `CompactValidator` instead of `*ethpb.Validator` also reduces
the garbage collection pressure.

The following graph represents, for a Hoodi supernode (200 validators
managed):
1. The heap memory usage (`go_memstats_alloc_bytes`) without this PR
(`ref`) and with this PR (`current`)
2. `1.`, but averaged over the latest four hours.
3. The diff of the graphs in `2`. (The bigger, the better)

> [!NOTE]
> The improvement is, after stabilisation, **~300MB-450MB**,
representing **~8%-11%**.
>
> <img width="1028" height="917" alt="image"
src="https://github.com/user-attachments/assets/9179d9b1-bc50-4248-9f47-82a7646d58ab"
/>
>
> For some reasons, the gain is higher than initialy expected.

**For the reviewers:**
This PR should be reviewed commit by commit:
Commits 1 and 2 are independent and unrelated to this PR
Commit 3 is an oversight of:
- https://github.com/OffchainLabs/prysm/pull/16420

Commit 4 implements the `CompactValidator`
Commit 5 uses the `CompactValidator`. The key change is:
```go
// MultiValueValidators is a multi-value slice of compact validators.
type MultiValueValidators = multi_value_slice.Slice[stateutil.CompactValidator]
```

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).

---------

Co-authored-by: Bastin <43618253+Inspector-Butters@users.noreply.github.com>
2026-03-17 13:38:58 +00:00
terence
7728ad4aa2 p2p: add signed proposer preferences gossip topic and verification (#16537)
This PR Implements the `signed_proposer_preferences` gossip topic from
the Gloas spec. Proposers broadcast their `fee_recipient` and `gas_limit
preferences` for upcoming slots, which are cached for downstream bid
validation.

- Gossip topic registration, mapping, and subscriber for
`proposer_preferences`
- Verification: next-epoch check, valid proposal slot, signature
validation
- Slot-keyed `ProposerPreferencesCache` with pruning in the fork watcher
  - Protobuf `ProposerPreferences` and `SignedProposerPreferences`

  > **Note:** This is PR 1 of 3 in a stack. Please review in order:
  > 1. **This PR** — proposer preferences P2P
  > 2. Proposer preferences RPC endpoint
  > 3. Execution payload bid P2P
2026-03-17 00:12:37 +00:00
terence
1362654669 proposer: use correct access root to get pre state (#16496) 2026-03-16 18:06:31 +00:00
dkutzmarks-rgb
3cec3997f8 ci: add SBOM export workflow (#16531)
## Summary
- Adds CycloneDX SBOM generation via cdxgen and uploads to Dependency
Track
- Runs on push to the default branch and weekly (randomized schedule)

## Details
- SBOM format: CycloneDX 1.6 (required by Dependency Track)
- Generator: cdxgen v12.1.1 (Docker image)
- Runner: `ubuntu-latest`
- Skips SBOM generation if no commits in the last 7 days

---------

Co-authored-by: Preston Van Loon <preston@pvl.dev>
2026-03-16 15:46:59 +00:00
terence
1236519810 fix: guard against fork boundary IsParentBlockFull false positive (#16510)
`UpgradeToGloas` initializes both `latestExecutionPayloadBid.BlockHash`
and `latestBlockHash` to the last Fulu EL hash. If the first Gloas block
(or any subsequent block in a run of empty-variant slots) has no bid,
`process_execution_payload_bid` never runs, `bid.Slot` stays `0`, and
the two fields remain equal

`IsParentBlockFull() = true`. `saveHead` and `lateBlockTasks` both used
this result to pick EL block hash as the state access key, but the state
was only ever saved under the beacon block root
2026-03-16 14:55:40 +00:00
Manu NALEPA
36052ed1bb Reduce log noise by returning nil error when ignoring already-seen data column sidecars during gossip validation (#16536)
**What type of PR is this?**
Other

**What does this PR do? Why is it needed?**
Starting at:
- https://github.com/OffchainLabs/prysm/pull/16515

For a super node, the
> data column sidecar already seen

log takes almost **50%** of the whole debug log volume.

The reason is, previously, if a given data column sidecar was already
seen, the message was ignored with the corresponding `err == nil`.
Starting at this PR, the message is still ignored but `err != nil`.
The direct consequence is the corresponding error is printed on the
console.

This present pull request sets back to `nil` the corresponding error.
The rationale is receiving multiple times the same sidecar from
different peers is an expected and normal behavior.
==> We don't need to consider this event as an error.

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-03-14 22:42:59 +00:00
terence
458d4ebe54 Use read only validator accessor in IsPayloadTimelyCommittee (#16530)
This PR replaces `ValidatorAtIndex` with `ValidatorAtIndexReadOnly` in
`IsPayloadTimelyCommittee` to avoid copying the entire validator on each
call

CPU and heap profiling of ePBS devnet nodes shows `PayloadCommittee` →
`CopyValidator` as the single largest hotspot, consuming ~18-20% of CPU
time across all servers. `IsPayloadTimelyCommittee` calls
`ValidatorAtIndex` in a loop over candidate committee members, and each
call deep-copies the full validator struct (~1 KB) only to read
`EffectiveBalance`

`ValidatorAtIndexReadOnly` returns a read-only reference to the
underlying validator, eliminating the copy. The only field accessed is
`EffectiveBalance()`, which is available on the read-only interface
2026-03-13 20:23:10 +00:00
james-prysm
8680f3f8bb adding grpc endpoints for attester, proposer, sync duties, and ptc duties (#16416)
<!-- Thanks for sending a PR! Before submitting:

1. If this is your first PR, check out our contribution guide here
https://docs.prylabs.network/docs/contribute/contribution-guidelines
You will then need to sign our Contributor License Agreement (CLA),
which will show up as a comment from a bot in this pull request after
you open it. We cannot review code without a signed CLA.
2. Please file an associated tracking issue if this pull request is
non-trivial and requires context for our team to understand. All
features and most bug fixes should have
an associated issue with a design discussed and decided upon. Small bug
   fixes and documentation improvements don't need issues.
3. New features and bug fixes must have tests. Documentation may need to
be updated. If you're unsure what to update, send the PR, and we'll
discuss
   in review.
4. Note that PRs updating dependencies and new Go versions are not
accepted.
   Please file an issue instead.
5. A changelog entry is required for user facing issues.
-->

**What type of PR is this?**

Other

**What does this PR do? Why is it needed?**

~~DEPENDS ON https://github.com/OffchainLabs/prysm/pull/16402~~

Adding grpc endpoints for attester, proposer, and sync duties. This pr
doesn't utilize the apis.
In a future pr we will include a transition from using duties v2
endpoint to the split duties endpoints for the fork.

TESTING

Both the GetDutiesV2 endpoint and the new ones added (
GetAttesterDuties, GetProposerDutiesV2, GetSyncCommitteeDuties,
GetPTCDuties) use the same underlying helper function under
coreservice.duty function, so all core duties should be the same. Note
that the new gRPC endpoints use validator indices instead of pubkeys to
match REST api endpoints.

Using grpcurl (install with brew install grpcurl):
```
  # GetDutiesV2 - composite endpoint
  # Note: public_keys are base64-encoded BLS pubkeys
  grpcurl -plaintext -d '{
    "epoch": '$EPOCH',
    "public_keys": ["<base64_pubkey_1>", "<base64_pubkey_2>"]
  }' localhost:4000
  ethereum.eth.v1alpha1.BeaconNodeValidator/GetDutiesV2
```

```
  # GetAttesterDuties
  grpcurl -plaintext -d '{
    "epoch": '$EPOCH',
    "validator_indices": [0, 1, 2, 3, 4]
  }' localhost:4000
  ethereum.eth.v1alpha1.BeaconNodeValidator/GetAttesterDuties
```

```
  # GetProposerDutiesV2
  grpcurl -plaintext -d '{
    "epoch": '$EPOCH'
  }' localhost:4000
  ethereum.eth.v1alpha1.BeaconNodeValidator/GetProposerDutiesV2
```

```
  # GetSyncCommitteeDuties
  grpcurl -plaintext -d '{
    "epoch": '$EPOCH',
    "validator_indices": [0, 1, 2, 3, 4]
  }' localhost:4000 ethereum.eth.v1alpha1.BeaconNodeValidator/Get
  SyncCommitteeDuties
```

```
  # GetPTCDuties (Gloas+ only)
  grpcurl -plaintext -d '{
    "epoch": '$EPOCH',
    "validator_indices": [0, 1, 2, 3, 4]
  }' localhost:4000
  ethereum.eth.v1alpha1.BeaconNodeValidator/GetPTCDuties
```

  Verification Checklist

  1. Attester duty data match: For each validator in
  GetDutiesV2.CurrentEpochDuties, confirm attester_slot,
  committee_index, committee_length, committees_at_slot,
  validator_committee_index match the corresponding AttesterDuty
  from GetAttesterDuties

  2. Proposer duty data match: For each validator with non-empty
  proposer_slots in GetDutiesV2, confirm those slots appear in
  GetProposerDutiesV2 response for that validator index
  3. Sync committee flag match: For each validator where
  is_sync_committee=true in GetDutiesV2, confirm that validator
  appears in GetSyncCommitteeDuties response

  4. PTC duty data match: For each validator with non-empty
  ptc_slots in GetDutiesV2, confirm those slots appear in
  GetPTCDuties response for that validator index

  5. Dependent root — attester:
  GetDutiesV2.PreviousDutyDependentRoot ==
  GetAttesterDuties.dependent_root (for the same epoch)

  6. Dependent root — proposer pre-Fulu:
  GetDutiesV2.CurrentDutyDependentRoot ==
  GetProposerDutiesV2.dependent_root

7. Dependent root — proposer post-Fulu: ( DutiesV2 still has a bug that
doesn't take into account the proposer look ahead)
  GetDutiesV2.CurrentDutyDependentRoot ≠
  GetProposerDutiesV2.dependent_root. The V2 value should match
  GetAttesterDuties.dependent_root instead (both use (E-1)_start - 1)
  
  8. Dependent root — PTC: GetPTCDuties.dependent_root ==
  GetAttesterDuties.dependent_root (both use
  AttestationDependentRoot)

  9. Next epoch: Repeat checks 1-4 using
  GetDutiesV2.NextEpochDuties vs individual endpoints queried
  with epoch+1

  10. Edge cases: Epoch 0, epoch 1, sync committee period
  boundary, pre-Gloas epoch for PTC (should error)


**Which issues(s) does this PR fix?**

Fixes #

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-03-13 19:36:21 +00:00
terence
416c49e6d5 metrics: add initial gloas metric (#16519)
This PR adds an initial gloas metric

**Forkchoice**
- Add `forkchoice_payload_inserted_count`
- Add `forkchoice_payload_empty_node_count`
- Add `forkchoice_payload_full_node_count`
- Add `forkchoice_ptc_vote_count`

These metrics show how forkchoice evolves under Gloas:
- how many payload nodes are inserted
- how many currently tracked nodes represent empty payloads vs full
payloads
- how many PTC votes forkchoice receives

**Blockchain**
- Add `beacon_execution_payload_envelope_valid_total`
- Add `beacon_execution_payload_envelope_invalid_total`
- Add `beacon_execution_payload_envelope_processing_duration_seconds`
- Add `beacon_late_payload_task_triggered_total`

These metrics track the beacon node’s envelope handling:
- whether received execution payload envelopes are accepted or rejected
- how long envelope processing takes end-to-end
- how often the late payload task fires during the slot

**Sync**
- Add `sync_payload_attestation_arrival_delay_seconds`
- Add `sync_execution_payload_envelope_arrival_delay_seconds`
- Add `sync_payload_envelope_by_range_served_total`
- Add `sync_payload_envelope_by_root_served_total`
- Add `gloas_execution_payload_envelopes_rpc_requests_total{rpc,result}`

These metrics focus on network timing and RPC serving:
- how long after slot start payload attestations and payload envelopes
arrive over gossip
- how often payload envelope RPC requests are served
- how payload-envelope RPC requests break down by method and result

For gossip validation, only arrival delay remains:
- payload attestation
- execution payload envelope
- data column sidecar continues to use the existing generic arrival
metric

Custom sync-side gossip result counters were removed because libp2p
already accounts for validation outcomes.

**Payload Attestation Pool**
- Add `payload_attestation_pool_size`

This metric shows the current size of the payload attestation pool,
which is useful for seeing whether attestations are being accumulated or
drained as expected.

**Validator RPC**
- Add `gloas_payload_id_cache_total{result}`

This metric shows Gloas proposer payload ID cache behavior:
- cache hits
- cache misses

That helps explain whether proposers are reusing cached EL payload IDs
or falling back to fresh forkchoice updates.

**Validator Client**
- Add `validator_payload_attestation_submission_total{result}`
- Add `validator_self_build_envelope_submission_total{result}`

These metrics show validator-side Gloas behavior:
- whether payload attestation submissions succeed or fail
- whether self-build envelope submissions succeed or fail

**Core Gloas**
- Add `gloas_builder_pending_payments_processed_total`
- Add `gloas_builder_deposits_processed_total`

These metrics show builder-economics state transitions:
- how many pending builder payments are promoted into pending
withdrawals
- how many builder-related deposit requests are processed

**State Native**
- Add `gloas_execution_payload_availability_ratio`
- Add `gloas_builder_pending_withdrawals_count`
- Add `gloas_builder_pending_withdrawals_gwei`
- Add `gloas_payload_expected_withdrawals_count`
- Add `gloas_active_builders_count`
- Add `gloas_active_builders_balance_gwei`

These metrics expose the current Gloas state snapshot:
- how much of the payload availability window is marked available
- how many builder pending withdrawals exist and their total value
- how many expected withdrawals are cached for the next payload
- how many builders are currently active and their aggregate balance

These are emitted from `RecordStateMetrics()`
2026-03-13 16:07:01 +00:00
terence
6605dfbd50 forkchoice: fix forkchoice balance underflow when att slot change (#16520)
In ePBS, `resolveVoteNode` routes validator balances to two different
accumulators based on the attestation slot:

When a validator re-attests for the **same block** in a **new epoch**
(assigned to a different slot), the root and payload status are
unchanged. The trigger condition:

  ```go
if vote.currentRoot != vote.nextRoot || oldBalance != newBalance ||
vote.currentPayloadStatus != vote.nextPayloadStatus {
```
  ...does not fire, so the balance is never moved. But the vote rotation silently updates currentSlot to the new value. On the next vote change, the subtraction targets the wrong accumulator (which has balance=0), causing the underflow.

  Examples:
  1. Epoch 2: Validator attests at slot 95 for block B (slot 95).
  pending = (95 == 95) = true → 32 ETH added to `Node.balance`
  2. Epoch 3: Validator re-attests at slot 96 for same block `B.Root/payloadStatus` went from pending to empty → not reprocessed. And currentSlot rotated to 96.
  3. Epoch 4: Validator attests for block C. Subtract from B that's no longer pending but has zero balance which triggers underflow
2026-03-13 15:32:25 +00:00
Manu NALEPA
84993fdd68 Fix TestProcessPendingDepositsMultiplesSameDeposits to properly test that duplicate pending deposits for the same key top up a single validator instead of creating duplicates. (#16529)
**What type of PR is this?**
Bug fix (in tests)

**What does this PR do? Why is it needed?**
Fix `TestProcessPendingDepositsMultiplesSameDeposits` to properly test
that duplicate pending deposits for the same key top up a single
validator instead of creating duplicates.

Previously, this test used 2 validators with the same pubkey.

**Acknowledgements**
- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-03-13 15:01:19 +00:00
james-prysm
1e916418f2 changed /eth/v1/beacon/execution_payload_envelope/{block_root} to /eth/v1/beacon/execution_payload_envelope/{block_id} defined in beacon apis (#16521)
**What type of PR is this?**

 Bug fix

**What does this PR do? Why is it needed?**

missed this in review

we should have /eth/v1/beacon/execution_payload_envelope/{block_root} as
/eth/v1/beacon/execution_payload_envelope/{block_id} defined in beacon
apis


https://github.com/ethereum/beacon-APIs/blob/master/apis/beacon/execution_payload/envelope_get.yaml

missed in https://github.com/OffchainLabs/prysm/pull/16441

**Which issues(s) does this PR fix?**

Fixes #

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-03-13 14:12:42 +00:00
terence
8d5d584cf8 sync,verification: add Gloas data column gossip validation path (#16515)
This PR adds a dedicated Gloas `data_column_sidecar` gossip validation
path and aligns it with the EIP-7732 spec.

The validation flow is now split cleanly:
- Fulu stays in the existing `validate_data_column` path
- Gloas moves into `validate_data_column_gloas`
- Gloas-specific checks live in a dedicated `GloasDataColumnVerifier`

**What Changed**
- Add a separate Gloas data column gossip validator in sync
- Add `GloasDataColumnVerifier` in `verification`
- Validate Gloas sidecars against
`block.body.signed_execution_payload_bid.message.blob_kzg_commitments`
- Refactor KZG proof verification to accept commitments explicitly
instead of rewriting the sidecar
- Use Gloas dedupe semantics: `(beacon_block_root, index)`
- Add spec-mapped comments next to the Gloas checks
- Add a TODO for deferred queue/revalidation when the block has not yet
been seen

**Spec Mapping**

The Gloas path now follows this order:
1. `IGNORE` if the block for `sidecar.beacon_block_root` has not been
seen
2. `REJECT` if `sidecar.slot != block.slot`
3. `REJECT` if `verify_data_column_sidecar(sidecar,
bid.blob_kzg_commitments)` fails
4. `REJECT` if the sidecar is on the wrong subnet
5. `REJECT` if `verify_data_column_sidecar_kzg_proofs(sidecar,
bid.blob_kzg_commitments)` fails
6. `IGNORE` if `(sidecar.beacon_block_root, sidecar.index)` has already
been seen

---------

Co-authored-by: Manu NALEPA <enalepa@offchainlabs.com>
2026-03-12 22:50:26 +00:00
satushh
598509ffa8 Remove next epoch lookahead from PTC duties (#16517)
**What does this PR do? Why is it needed?**

Implemented the spec change:
https://github.com/ethereum/beacon-APIs/pull/586

(Removed next epoch lookahead from PTC duties)
2026-03-12 16:43:32 +00:00
terence
0fbd643c02 forkchoice: fix head change on every attestation tick pre gloas (#16508)
`store.go` creates a `fullNodeByRoot` entry with `full=true` for all
fulu blocks because pre-Ggoas blocks embed their EL payload inline and
need optimistic validation tracking. `FullHead` was surfacing this
internal `full=true` to callers without distinguishing whether the node
was actually post-gloas.

`UpdateHead` called `FullHead` which returned `full=true` for fulu
blocks. Meanwhile `saveHead` always stored `s.head.full=false`for
pre-gloas states. The mismatch caused `isNewHead` to fire on ticker
interval.

I liked the option to fix in `FullHead`, it's a simple change. The con
is `CanonicalNodeAtSlot` is a different path that also returns `pn.full`
for pre-gloas, t's already call-site guarded by `GloasForkEpoch` so it's
fine, but the internal inconsistency remains.

**Option 2** is Fix in `saveHead` for fulu
```go
if headState.Version() >= version.Gloas {
} else {
    full = true
}
```
This aligns `s.head.full` with what `FullHead` returns

**Option 3** is Fix in `isNewHead` (ignore full pre-gloas)
```go
    postGloas := slots.ToEpoch(s.CurrentSlot()) >= params.BeaconConfig().GloasForkEpoch
    fullChanged := postGloas && full != currentFull
    return r != currentHeadRoot || fullChanged || r == [32]byte{}
```

**Option 4** is Fix in `store.go` to not expose full=true pre gloas,
which change `choosePayloadContent` to return the empty node for pre
gloas. I haven't analyzed this one deeply to understand its downstream
effect
```go
func (s *Store) choosePayloadContent(n *Node) *PayloadNode {
    fn := s.fullNodeByRoot[n.root]
    en := s.emptyNodeByRoot[n.root]
    if fn == nil {
        return en
    }
    if fn.full && slots.ToEpoch(n.slot) < params.BeaconConfig().GloasForkEpoch {
        return en
    }
    ...
```
2026-03-11 23:29:31 +00:00
james-prysm
0e4f3231d2 adding payload_attestation_message event (#16506)
**What type of PR is this?**

Feature

**What does this PR do? Why is it needed?**

adds payload_attestation_message event triggered via grpc and gossip
message received, introduced in
https://github.com/ethereum/beacon-APIs/pull/552

**Which issues(s) does this PR fix?**

Fixes #

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [ ] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-03-11 16:24:53 +00:00
james-prysm
b17f2752ab adding payload attestation apis connection from validator client (#16504)
pr replaces grpc connection stub with actual call to api endpoints
2026-03-10 18:52:50 +00:00
Potuz
932e5eb7d8 Use execution block hash as state access key post-Gloas (#16459)
Post-Gloas, SaveState and the next-slot cache are keyed by execution
block hash as well of beacon block root. Thread an accessRoot through
getStateAndBlock (for StateByRoot) and getPayloadAttribute (for
ProcessSlotsUsingNextSlotCache) so each caller can supply the right
key.

Add FullHead to forkchoice: it returns the head root, the block hash
of the nearest full payload (walking up to an ancestor when the head
is empty), and whether the head itself is full. This gives callers
enough information to derive the correct accessRoot.

Rework UpdateHead to call FullHead and branch on post-Gloas: the
Gloas path computes accessRoot from the full block hash and sends FCU
via notifyForkchoiceUpdateGloas; the legacy path keeps the existing
forkchoiceUpdateWithExecution flow. saveHead and pruneAttsFromPool
now use local variables instead of the removed shared fcuArgs.

Flatten the else branch in lateBlockTasks so the Gloas path returns
early and the pre-Gloas path runs at the top indentation level.

In postPayloadHeadUpdate pass the envelope's block hash as accessRoot
since the state was just saved under that key.
2026-03-10 17:33:18 +00:00
Jun Song
de233438f1 Remove unnecessary memory allocation while encoding in JSON at GetBeaconStateV2 (#16485)
**What type of PR is this?**

> Other: Optimization

**What does this PR do? Why is it needed?**

In `GetBeaconStateV2`, an extra `json.Marshal` call is used for type
checking, which allocates `[]byte` **twice** so the total memory usage
is increased by the size of `BeaconState`. Currently, the `BeaconState`
in mainnet exceeds over 250MB, so I think this can be a good
optimization for nodes who serve `BeaconState` periodically.

**Which issues(s) does this PR fix?**

N/A

**Other notes for review**

I used [this benchmark
script](https://gist.github.com/syjn99/499eaae60e37447297bb568280ab2b64).

Before:

```
Running tool: /opt/homebrew/bin/go test -test.fullpath=true -benchmem -run=^$ -bench ^BenchmarkGetBeaconStateV2$ github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/eth/debug

goos: darwin
goarch: arm64
pkg: github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/eth/debug
cpu: Apple M4 Pro
BenchmarkGetBeaconStateV2/validators_64-12         	      49	  24245383 ns/op	42510257 B/op	  250511 allocs/op
BenchmarkGetBeaconStateV2/validators_100000-12     	       5	 225380608 ns/op	347510451 B/op	 1849520 allocs/op
BenchmarkGetBeaconStateV2/validators_1000000-12    	       1	2437710417 ns/op	2901637784 B/op	16249530 allocs/op
PASS
ok  	github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/eth/debug	7.431s
```

After:
```
Running tool: /opt/homebrew/bin/go test -test.fullpath=true -benchmem -run=^$ -bench ^BenchmarkGetBeaconStateV2$ github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/eth/debug

goos: darwin
goarch: arm64
pkg: github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/eth/debug
cpu: Apple M4 Pro
BenchmarkGetBeaconStateV2/validators_64-12         	     129	   8947429 ns/op	34877098 B/op	  250497 allocs/op
BenchmarkGetBeaconStateV2/validators_100000-12     	      12	  90652486 ns/op	254957336 B/op	 1849493 allocs/op
BenchmarkGetBeaconStateV2/validators_1000000-12    	       1	1182659125 ns/op	2475831984 B/op	16249524 allocs/op
PASS
ok  	github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/eth/debug	5.816s
```

Summary:
```
validators_64 ->
Time: 24.2ms → 8.9ms (2.71x faster, −63%)
Memory: 42.5MB → 34.9MB (−18%)

validators_100,000 ->
Time: 225ms → 91ms (2.49x faster, −60%)
Memory: 347.5MB → 255.0MB (−26.6%)

validators_1,000,000 ->
Time: 2,438ms → 1,183ms (2.06x faster, −51%)
Memory: 2,901MB → 2,476MB (−14.7%)
```

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).

---------

Co-authored-by: james-prysm <90280386+james-prysm@users.noreply.github.com>
2026-03-10 15:37:04 +00:00
Potuz
e35f6c351a Handle missing Payloads (#16460)
In the event of a late missing Payload, we add a helper that updates the
beacon block post state in the NSC as well as handling the boundary
caches and calling FCU if we are proposing next slot.
2026-03-10 13:59:50 +00:00
terence
4da5ed072c proposer: add payload attestations packing (#16493)
This PR adds packing for payload attestations for a beacon block. It
filters payload attestations for slot and beacon root checks
2026-03-10 02:27:03 +00:00
james-prysm
dec4b43b3e fix to attestation pool to only consume on insert (#16503)
**What type of PR is this?**
Bug fix

**What does this PR do? Why is it needed?**

fixes bug on payload attestation pool consuming on reads, when we should
only prune on insert

**Which issues(s) does this PR fix?**

Fixes #

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-03-10 00:02:52 +00:00
james-prysm
17ea45a011 adding the duties stores for validator duties processing (#16479)
**What type of PR is this?**

Other

**What does this PR do? Why is it needed?**

This PR refactors the way we store different validator duties into a
duties store for easier splitting of tasks and in a future pr processing
duties for split endpoints. This PR will reduce the number of changes
when we start calling the different endpoints introduced in
https://github.com/OffchainLabs/prysm/pull/16416

pr is part of https://github.com/OffchainLabs/prysm/pull/16421

**Which issues(s) does this PR fix?**

Fixes #

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).

---------

Co-authored-by: Preston Van Loon <pvanloon@offchainlabs.com>
2026-03-09 21:44:14 +00:00
terence
1934afac73 sync: wire payload attestations into pending pool (#16492)
This PR wires payload attestations into the pending pool arrived over
sync gossip pipeline
2026-03-09 18:32:10 +00:00
james-prysm
d0c9a31657 Validator client ptc actions (#16407)
**What type of PR is this?**

Feature

**What does this PR do? Why is it needed?**

This PR introduces PTC attestations including getting the attestation
data and signing and submitting, the getting and submitting are stubbed
out for now as the APIs are not implemented /merged yet.

Tested in kurtosis to verify no regressions 

**Which issues(s) does this PR fix?**

Fixes #

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [ ] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-03-09 16:39:47 +00:00
terence
66c70200ee duties: wire PTC slot assignments into validator duty pipeline (#16489)
This PR adds end-to-end PTC support for validator duties by adding PTC
duty support in duties v2:
  - introduced repeated PTC slot assignments per validator
- cpmputed assignments by scanning epoch slots and checking
`gloas.PayloadCommittee`
  - propagated PTC slots through server/adapter duty responses
2026-03-09 14:20:13 +00:00
Bastin
928a874e4a add log.go files to runtime (#16491)
**What does this PR do? Why is it needed?**
This PR adds the `runtime` package and it's subpackages (except
`runtime/logging`) to the log generation process. which means they get
an additional `log.go` file with the package path as a field.

**Other notes for review**
- `runtime/logging` is an exception because adding it to the log gen
process messes with the internals of the package, since it's handling
the logging formats, etc...

- to test this, you can look at the logs emitted from these packages
before and after this PR. they will have a field `prefix` before this
PR, and no `prefix` after. The most obvious one is the runtime
(`registry`) package itself which usually emits a log at the start and
end of beacon node.

This is as part of my goal to remove all `prefix` logs and move them to
the new `package_path` format.
2026-03-08 21:09:40 +00:00
Bastin
ed8a3351aa support for calling debug/states/{id} with pre-payload roots (#16466)
in `rpc/lookup/stater`, when trying to fetch a state using a state root,
we only look in the `state.StateRoots` .
in gloas that would only account for post-payload roots. 

This PR adds a non-optimal way to support fetching states using
pre-payload state roots (dora does this).

I'm running a local devnet so I can see how long it takes for the stater
to respond in the worst case. but I think it should be fine for beacon
API.
2026-03-08 19:51:44 +00:00
terence
5b95d11c5e rpc: allow execution payload envelope lookup by block ID (#16495)
fixes #[16494](https://github.com/OffchainLabs/prysm/issues/16494)
2026-03-08 14:00:52 +00:00
terence
7a4bea0e44 rpc/validator: implement PayloadAttestationData and SubmitPayloadAttestation gRPC endpoints (#16487)
- This PR adds the payload attestation prototype flow. 
- On the proto side it introduces `PayloadAttestationDataRequest` and
two RPC methods on `BeaconNodeValidator`: `PayloadAttestationData` and
`SubmitPayloadAttestation`.
- The `ForkchoiceFetcher` interface is extended with
`HighestReceivedBlockRoot() [32]byte`, which returns the beacon block
root at the highest received slot and is used to determine which block
PTC members attest to, and `HasFullNode([32]byte) bool`, which indicates
whether a full payload node exists for a given root and determines the
`PayloadPresent` field in the response.
- The RPC server implementation lives in
`beacon-chain/rpc/prysm/v1alpha1/validator/payload_attestation.go`.
`PayloadAttestationData` validates the current slot, checks sync status,
and returns attestation data using the forkchoice helpers, while
`SubmitPayloadAttestation` validates the request, broadcasts it over
P2P, applies it locally through `ReceivePayloadAttestationMessage`, and
inserts it into the payload attestation pool. Wiring adds
`PayloadAttestationPool` and `PayloadAttestationReceiver` to `Server`,
`rpc.Config`, and `BeaconNode`, with the pool instantiated in
`node.New()`.
- Both RPCs are marked `deprecated = true` since this prototype will
eventually move to the beacon REST API.
2026-03-07 04:57:28 +00:00
terence
e899003973 operations/payloadattestation: add PTC attestation pool (#16486)
Introduces the `payloadattestation` pool package, a thread-safe store
for aggregating `PayloadAttestationMessage`s from PTC members before
they are included in blocks by the `slot N+1` proposer.

PTC validators broadcast `PayloadAttestationMessage`s during slot N
attesting whether the execution payload arrived on time
(`PayloadPresent`) and whether blob data is available
(`BlobDataAvailable`). The slot N+1 proposer collects these and includes
aggregated `PayloadAttestation`s in the block body.

The pool:
- Keys entries by struct — one entry per distinct `(block_root, slot,
payload_present, blob_data_available)` tuple
- Aggregates BLS signatures and bit vectors as messages arrive
- Exposes `PendingPayloadAttestations(slot)` for proposer block
construction. Slot and lower are automatically pruned
2026-03-06 22:50:07 +00:00
james-prysm
e751a74c64 gloas attestation (#16391)
**What type of PR is this?**

Feature

**What does this PR do? Why is it needed?**

Adds basic changes for gloas attestations to have updated index value
based on payload availability.

**Which issues(s) does this PR fix?**

Fixes #

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [ ] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).

---------

Co-authored-by: Potuz <potuz@prysmaticlabs.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 19:44:21 +00:00
satushh
6826e77539 Use copy() instead of byte-by-byte loop in MarshalSSZ (#16483)
**What type of PR is this?**

Optimisation

**What does this PR do? Why is it needed?**

Use copy() instead of byte-by-byte loop in MarshalSSZ
Similar to https://github.com/OffchainLabs/prysm/pull/16222
2026-03-06 18:01:55 +00:00
Jun Song
15c178ef0c Add Close() call for resources (http response, file descriptor) in e2e package (#16481)
**What type of PR is this?**

> Bug fix

**What does this PR do? Why is it needed?**

Some resources in e2e package didn't get closed after usage.

**Which issues(s) does this PR fix?**

N/A

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [ ] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).

---------

Co-authored-by: james-prysm <90280386+james-prysm@users.noreply.github.com>
2026-03-06 17:39:25 +00:00
Jun Song
e115137591 Fix file descriptor leaks in CopyFile (#16480)
**What type of PR is this?**

> Bug fix

**What does this PR do? Why is it needed?**

Close file descriptors for both source/destination files in `CopyFile`
function.

**Which issues(s) does this PR fix?**

N/A

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [ ] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 17:33:58 +00:00
Manu NALEPA
dc62271ebb Use InitializeFromProtoUnsafeXXX instead of InitializeFromProtoXXX when possible. (Nishant's optimization) (#16420)
**What type of PR is this?**
Optimisation

**What does this PR do? Why is it needed?**
Use `InitializeFromProtoUnsafeXXX` instead of `InitializeFromProtoXXX`
when possible. (@nisdas' change)
This change avoids useless copies.

The difference between these two methods is the latest copies the input
state. However, if the input state is guaranteed not to be modified
after the use of this function (and, in particular, if the input state
is not used at all after the use of this function), then copying it is
unnecessarry. ==> Using the "unsafe" method is fine in this case.

**Other notes for review**
As a reviewer, you should ensure that when the unsafe method is used,
then the input state is never modified after the use of the function.

This PR is much easier to review directly in VSCode, because it displays
more context by default.

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-03-06 15:46:34 +00:00
Manu NALEPA
3ec505bc22 Periodically evict finalized states in checkpoint states cache (#16458)
**What type of PR is this?**
Bug fix

**What does this PR do? Why is it needed?**
This PR evicts finalized states in checkpoint states cache.

States are efficiently stored in caches, especially thanks to multi
value slices. If 1 state takes 300 MB and 2 states that are really
similar are stored in a cache, then these 2 states could only need let's
say 310MB of cache memory (instead of 300 MB x 2 = 600 MB).

**Before the commit creating the memory issue**, new states were
regularly stored in the `CheckpointStateCache`. This cache has 10 slots.
After this cache is full, oldest values (not quite exactly because it's
a LRU cache) are pruned.

**After the commit creating the memory issue**, new states are quite
rarely inserted into this cache. For example, on a run, almost 5H (!)
were needed before the first value was evicted from this cache. This
mean this cache contains multiple states that do not share a lot of
values with other states in all other caches/head.
==> A lot of fields stay in memory that are exclusively needed for the
(old) states only present in this cache.

The beacon node now evicts finalized states from the cache.

**Which issues(s) does this PR fix?**
- https://github.com/OffchainLabs/prysm/issues/16376

<img width="1022" height="914" alt="image"
src="https://github.com/user-attachments/assets/98886364-001a-48fc-a952-5c6a7e80bf88"
/>

**Acknowledgements**
- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-03-06 09:39:37 +00:00
Potuz
6be77c0194 Do not reject on blocks unless they are provably bad (#16471)
Co-authored-by: terence <terence@prysmaticlabs.com>
2026-03-06 02:20:11 +00:00
Potuz
03fa7042cb Dont fail seting fc (#16478)
Do not fail when setting up the forkchoice checkpoints. Balances will be
wrong, but they self-heal on 2 epochs.

---------

Co-authored-by: terence <terence@prysmaticlabs.com>
2026-03-06 00:13:40 +00:00
terence
c620f29aab fix(sync): do not fail block processing on hot state DB cleanup error (#16473)
During initial sync, `checkSaveHotStateDB` can fail when it tries to
disable hot-state-saving mode and clean up previously saved states. If
one of those states has since become the finalized checkpoint,
`DeleteStates` returns `ErrDeleteJustifiedAndFinalized`, which is
correct behavior from the DB layer, but should not kill block
processing.
This was observed on epbs-devnet-0 at slot ~3742 where the error
cascaded as:
```
Skip processing Gloas blocks error=receive Gloas block at slot 3742: check save hot state db: cannot delete finalized block or state
```
The fix downgrades the error to a log, matching the pattern already used
by `prunePostBlockOperationPools` two lines above. In the long run, we
should remove such scheme saving unfinalized states in DB when we dont
finalize after some epochs this was a work around for medella incident
2026-03-05 18:42:07 +00:00
terence
7a652d7ec6 fix(sync): preserve the full signed execution payload envelope (#16474)
fix(sync): preserve the full signed execution payload envelope in pubsub
validations
2026-03-05 15:24:23 +00:00
Potuz
b59a830dce Gloas/safer pending signature (#16470)
This PR hardens the pending payload structure on a few ways:

- It quickly ignores before signature verification if we already have
many pending payloads
- It ignores rather than reject payloads for the self-build block if
they fail verification, this is because we may get as in devnet-zero
from gossip payloads from unknown branches.
- It adds a counter to prevent being DOS by the previous feature. Since
we are ignoring instead of rejecting those payloads, it only allows 3
such payloads to come. This counter is pruned on finalization.

---------

Co-authored-by: terence <terence@prysmaticlabs.com>
2026-03-05 04:36:57 +00:00
Potuz
6999943c3d Gloas/late block tasks right fcu (#16454)
This PR changes how we update the next slot cache on late blocks. 

- When the latest saved state in the NSC agrees with the head root, then
we update this latest state because it will be useful. Otherwise we
instead update the headstate and put it as latest.
- This also keys the insertion in the NSC post-Gloas with the right
blockhash instead of blockroot if the headstate is based on full. This
exploits a dangerous feature of `IsParentBlockFull` documented in its
godoc.

This PR also updates the cache and the epoch boundary when we process
the payload.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 00:55:07 +00:00
Potuz
90064edd54 Do not request blocks that are being synced (#16468)
When adding a payload to the pending queue, do not send a batch request
if the block is already in forkchoice or being synced.

Similar when processing pending attestations.

---------

Co-authored-by: terence <terence@prysmaticlabs.com>
2026-03-05 00:06:40 +00:00
james-prysm
393eb1e83c Reorganize duties functions (#16457)
**What type of PR is this?**

Other

**What does this PR do? Why is it needed?**

just moving some functions around to reduce duties split pr. part of
https://github.com/OffchainLabs/prysm/pull/16421

**Which issues(s) does this PR fix?**

Fixes #

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-03-04 20:47:22 +00:00
Potuz
d2bcf75c50 Gloas/pending payload dependent root (#16461)
Only verify the envelope's signature before putting in the queue if
non-self-building or the proposer is in the lookahead. Otherwise just
ignore.

This still can cause a false rejection in a very edgy case of a builder
that is chosen exactly after being active and we are syncing from afar.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 18:40:11 +00:00
terence
89d3a6c66f Preserve canonical order in writeBlockBatchToStream (#16463)
Summary
- update `writeBlockBatchToStream` to preserve canonical slot order when
a batch contains both blinded and full blocks
- reconstruct blinded blocks before streaming and substitute them by
slot during canonical write pass
2026-03-04 18:17:25 +00:00
Bastin
bf2485eb71 gloas support for debug/beacon/state/ api (#16296)
**What does this PR do? Why is it needed?**
gloas support for debug/beacon/state/ api
2026-03-04 16:41:32 +00:00
terence
a88f60f1fa Apply gloas payload state mutations during stategen replay (#16464)
State replay in gloas needs to reflect post-payload state even when
replay input is a blinded envelope format. Without applying these
mutations during stategen replay, reconstructed states can diverge from
the canonical post-payload state. At the same time, not every block has
an envelope, so replay must remain robust when envelope data is absent

Summary
- apply Gloas execution payload envelope state mutations during stategen
replay when a blinded envelope is available
- extract reusable payload-mutation logic into
`ApplyExecutionPayloadStateMutations` and use it from both normal
payload processing and stategen replay
2026-03-04 16:26:04 +00:00
Potuz
33f899506f Gloas/request parent payload (#16418)
This PR adds sync paths to request by RPC the parent Payload in the
event that we know the incoming block is based on full and we do not
have the payload.

It also adds an invalid payload cache and keeps track of some invalid
ones that we have seen on the blockchain package.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 15:14:36 +00:00
Potuz
f7ead02e6e Make Dora Happy (#16441)
- This PR adds the execution_payload_envelope Beacon API endpoint
supporting both JSON and SSZ
- It add the execution_payload_available event stream 

it also adds as a stub without a trigger the execution_payload_bid event
stream.

These are the minimum necessary to get Dora happy on Kurtosis. Haven't
reviewed this PR yet, so opening it as draft. Only check was in Kurtosis
against Dora.

<img width="1555" height="1177" alt="Screenshot 2026-02-27 at 7 18
01 PM"
src="https://github.com/user-attachments/assets/e888ccf3-68fe-42c0-9f9a-f911438438d1"
/>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 01:12:58 +00:00
james-prysm
0abf17f6cd refactor duties internal into core helper functions (#16402)
**What type of PR is this?**

Other

**What does this PR do? Why is it needed?**

This PR is a small component of
https://github.com/OffchainLabs/prysm/pull/16377 with the goal of
refactoring duties into separate endpoints for gloas. This first step
breaks out helper functions that will be useful and important in
breaking out the apis.

did kurtosis testing and basic bench marks to make sure there's no bad
regression

**Which issues(s) does this PR fix?**

Fixes #

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).

---------

Co-authored-by: satushh <satushh@gmail.com>
2026-03-03 21:09:23 +00:00
Bastin
8307ee1098 Make committeeIndex parameter optional for GetAttestationData beacon API post Gloas (#16436)
after gloas the parameter `committeeIndex` of the beacon API
`GetAttestationData` is optional.

[ethereum/beacon-APIs#578](https://github.com/ethereum/beacon-APIs/pull/578)
2026-03-03 20:00:07 +00:00
Bastin
7e4a039a7f Gloas API state structs (#16276)
**What does this PR do? Why is it needed?**
Adds Gloas API structs, and conversion functions
(`BeaconStateGloasFromConsensus()`)
2026-03-03 17:56:35 +00:00
Potuz
d46621eea3 Add Payload Envelope pending queue (#16414)
Adds a queue to keep payload envelopes for which their block hasn't been
seen pending. When process the block we send a call to process the
corresponding payload. Elements from the queue are removed on
finalization. Only one payload is kept per root.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 05:22:31 +00:00
Potuz
8b25ffaa45 Gloas/weights logs (#16456)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 02:47:03 +00:00
Potuz
17f1b78494 Better usage of the Next Slot cache in Gloas (#16439)
This PR exposes a function from the blockchain package to obtain a given
blocks's parent state.

It also fetches the correct state before processing slots to obtain the
prestate of a block.
2026-03-03 01:50:30 +00:00
terence
5874226067 Fix Gloas fork version using MetaDataV1 instead of MetaDataV2 (#16448)
Fix Gloas metadata version negotiation (MetaDataV1 → MetaDataV2)
2026-03-02 23:01:00 +00:00
Barnabas Busa
544bc3eb45 Fix nil pointer dereference in validator client with non-Prysm beacon-nodes (#16449)
1. When connecting to a REST-only beacon node (e.g. Teku, Lighthouse),
the gRPC connection provider is nil. The grpcClientManager was
unconditionally calling ConnectionCounter() on the provider in both
newGrpcClientManager and getClient, causing a SIGSEGV panic on startup.
2. runner.go — Made initial PushProposerSettings non-fatal. Lighthouse
returns 500: UnableToReadSlot on
/eth/v1/validator/prepare_beacon_proposer at genesis (slot 0) before any
blocks exist. The run loop already handles this as a warning — now the
initial call does too, and it retries on the next slot.


to repro the bug:
```yaml
participants:
  - el_type: geth
    el_image: ethpandaops/geth:master
    cl_type: prysm
    cl_image: ethpandaops/prysm-beacon-chain:develop
    vc_type: prysm
    vc_image: ethpandaops/prysm-validator:develop
    count: 1
  - el_type: geth
    el_image: ethpandaops/geth:master
    cl_type: teku
    cl_image: ethpandaops/teku:master
    vc_type: prysm
    vc_image: ethpandaops/prysm-validator:develop
    count: 1
additional_services:
  - dora
```

To repro the fix: 
```yaml
participants:
  - el_type: geth
    el_image: ethpandaops/geth:master
    cl_type: prysm
    cl_image: ethpandaops/prysm-beacon-chain:develop
    vc_type: prysm
    vc_image: ethpandaops/prysm-validator:develop
    count: 1
  - el_type: geth
    el_image: ethpandaops/geth:master
    cl_type: teku
    cl_image: ethpandaops/teku:master
    vc_type: prysm
    vc_image: ethpandaops/prysm-validator:barnabasbusa-bbusa-vc-fix
    count: 1
  - el_type: geth
    el_image: ethpandaops/geth:master
    cl_type: lighthouse
    cl_image: ethpandaops/lighthouse:unstable
    vc_type: prysm
    vc_image: ethpandaops/prysm-validator:barnabasbusa-bbusa-vc-fix
    count: 1


additional_services:
  - dora

```



**What type of PR is this?**

> Uncomment one line below and remove others.
>
> Bug fix
> Feature
> Documentation
> Other

**What does this PR do? Why is it needed?**

**Which issues(s) does this PR fix?**

Fixes #

**Other notes for review**

**Acknowledgements**

- [X] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [X] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [X] I have added a description with sufficient context for reviewers
to understand this PR.
- [X] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 21:46:22 +00:00
Potuz
de6d9947d7 remove spurious addition from gitignore (#16455) 2026-03-02 19:32:50 +00:00
terence
e5382d95dd Add ExecutionPayloadEnvelopesByRange RPC handler for Gloas (#16444)
- Adds the P2P RPC method `execution_payload_envelopes_by_range/1` for
both server handler + client requester
- Server validates range, clamps to Gloas fork start, streams blinded
envelopes from DB with a placeholder unblinded payload (EL
reconstruction is a future TODO)
- Client validates response slots are monotonically increasing and
within the requested range
- Registers a dedicated rate limiter bucket (`envelopeCollector`) for
the new topic
2026-03-02 19:19:57 +00:00
satushh
6d8445d440 Implementation of ExecutionPayloadEnvelopesByRoot v1 (#16394)
**What type of PR is this?**

Feature

**What does this PR do? Why is it needed?**

Implements the `ExecutionPayloadEnvelopesByRoot v1` RPC method as
specified in the
https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/p2p-interface.md#executionpayloadenvelopesbyroot-v1.

This allows peers to request signed execution payload envelopes by their
beacon block root.

This is a cache-only initial implementation. 
TODO in follow up work: DB storage 

**Which issues(s) does this PR fix?**

Fixes #

**Other notes for review**

**Acknowledgements**

- [ ] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [ ] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [ ] I have added a description with sufficient context for reviewers
to understand this PR.
- [ ] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-03-02 15:03:31 +00:00
Potuz
7b16c34af3 Fixed topic name for Gloas (#16443) 2026-03-01 00:29:21 +00:00
Potuz
91577760b6 better gloas logs (#16435)
Co-authored-by: terence <terence@prysmaticlabs.com>
2026-02-28 05:09:44 +00:00
terence
9ced048510 Add gloas beacon state marshal/unmarshal support (#16437) 2026-02-28 03:53:51 +00:00
Potuz
0439151373 Save head on gloas blocks (#16434)
Alternative to #16425
2026-02-27 22:41:34 +00:00
terence
5f050566f4 Update consensus node weight to return both empty and full (#16430)
This PR separates two different weight semantics that were previously
conflated in `Weight`
`Weight(root)` now consistently returns payload-node weight
`ConsensusNodeWeight(root)` is added for consensus-node weight (includes
empty + full payload effects in Gloas).
The "Head block is not the received block" log path now uses
`ConsensusNodeWeight`, so log output matches intended interpretation
2026-02-27 20:14:55 +00:00
terence
8a43513f5a Add receive payload to PublishExecutionPayloadEnvelope (#16429) 2026-02-27 19:32:21 +00:00
terence
6c8504ef71 Get parent root return parent root if parent is older than gloas (#16424)
This PR makes sure gloas parent pre‑state lookup falls back to the
parent root when the parent slot is pre‑fork.

Example:
- GloasForkEpoch = 1, 8 slots per epoch, and we process a block at slot
9 with parent at slot 8
- Slot 8 is still pre‑Gloas. The pre‑state for slot 9 must be fetched by
parent root, not by hash
2026-02-27 17:47:01 +00:00
terence
e99f86ecc5 Add gloas p2p support for gossip topics and scoring (#16423) 2026-02-27 15:33:43 +00:00
terence
0c36416677 Add generic signed beacon block gloas to factory (#16428) 2026-02-27 14:59:57 +00:00
terence
f2db0faea8 Fix use latest block hash for gloas parent hash (#16427)
Fix parent block hash look up in Gloas case, which we should be using
state's latest block hash instead of normal capella path
2026-02-27 14:30:58 +00:00
Jun Song
e31f59e04e Fix clang-format.yml workflow (#16412)
**What type of PR is this?**

> Bug fix

**What does this PR do? Why is it needed?**

CI - Protobuf Format actually always return **0**, so the formatting
check always become no-op.

```
/entrypoint.sh: line 44: local: `proto/testing/test.proto': invalid variable name for name reference
```

This corresponds to the actual [source
code](27cc2adf6e/entrypoint.sh (L44)).
I think it's better to use `clang-format` natively rather than fixing
the upstream dependency.

This PR also formats `*.proto` files with LLVM style.

**Which issues(s) does this PR fix?**

N/A

**Other notes for review**

N/A

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [ ] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-02-27 01:04:51 +00:00
Potuz
83a5210549 Add helper to get canonical node information (#16419)
This adds a new helper to obtain head information from forkchoice. 

⚠️ It does not recompute Head as that is expensive. It uses the cached
head node from forkchoice.

An attester calls it by passing the attestation slot. It will return the
right combination of the beacon block root and the payload content.
Attester needs to set committee index to 0 if false and 1 if true.

A proposer calls it by passing the current slot. It will tell it whether
to build on full or empty.
2026-02-26 20:34:22 +00:00
Preston Van Loon
0aa4a08b7b sync: remove unnecessary SignatureBatch.Copy() in batch verifier (#16398)
**What type of PR is this?**

Other

**What does this PR do? Why is it needed?**

This copy is unnecessary. There is no code path which mutates or would
mutate the signature set. Some local profiling indicates this would
reduce GC pressure somewhat. It was about 8.5% of the allocations on my
mainnet node.

**Which issues(s) does this PR fix?**

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-02-26 19:42:25 +00:00
Manu NALEPA
9c5d4a1767 Update c-kzg-4844 to v2.1.6 (#16417)
**What type of PR is this?**
Other

**What does this PR do? Why is it needed?**
Update c-kzg-4844 to v2.1.6

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-02-26 19:20:58 +00:00
Preston Van Loon
dd1ede572d api: reduce gzip compression level (#16399)
**What type of PR is this?**

Other

**What does this PR do? Why is it needed?**

This is a small optimization on the GetValidators endpoint for the
beacon API. The default compression level was [level
6](https://cs.opensource.google/go/go/+/refs/tags/go1.26.0:src/compress/flate/deflate.go;l=591)
and best speed is level 1. When serving this endpoint, gzip was almost
40% of my CPU profile.

**Which issues(s) does this PR fix?**

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-02-26 18:57:05 +00:00
Potuz
ec7eb97d41 Refactor areDataColumnsAvailable to accept commitments directly (#16405)
Alternative to #16404

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 17:43:12 +00:00
Potuz
0c893bd1a6 Send PTC attestations from block to forkchoice (#16415)
Send PTC attestations from the block to forkchoice.
2026-02-26 16:14:46 +00:00
Potuz
9a7cae9e5c Adapt lateBlockTasks for Gloas fork (#16403)
Switch the late block ticker from SecondsPerSlot/3 to
SlotComponentDuration(AttestationDueBPS), and replace it with
AttestationDueBPSGloas at the Gloas fork epoch. In lateBlockTasks,
branch FCU calls so Gloas+ states use notifyForkchoiceUpdateGloas with
LatestBlockHash instead of the pre-Gloas notifyForkchoiceUpdate path.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 15:25:01 +00:00
terence
c114bc57d9 Implement gloas state transition and more spec tests (#16406) 2026-02-26 15:36:23 +00:00
Potuz
a9aea0ba25 Receive PTC attestation (#16413)
wire up receival of PTC attestations and send to forkchoice.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 12:49:04 +00:00
terence
9f2ade53ff Fix Gloas builders sweep when builders list is empty (#16411)
This PR fixes builders sweep withdrawals to no‑op when the builders list
is empty, preventing out‑of‑range errors during gloas transitions and
aligning behavior with the spec’s zero‑builders case


### Spec reference
```
builders_limit = min(len(state.builders), MAX_BUILDERS_PER_WITHDRAWALS_SWEEP)
...
for _ in range(builders_limit):
    builder = state.builders[builder_index]
```
When `len(state.builders) == 0`, `builders_limit == 0`, so the loop does
not run and no indexing should occur.
2026-02-26 02:25:42 +00:00
terence
5b19916067 gloas: implement modified process withdrawals (#16310)
this PR implements [process
withdrawals](https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#modified-process_withdrawals)for
gloas. In particular

* `process_withdrawals` signature changed from the last fork, moving
from `(state, payload)` to `(state)`. It now adds builder pending
withdrawals, sweeping withdrawals, and index updates, and removes the
payload check. Because of these drastic changes, I think it is more
beneficial to implement gloas process withdrawals from scratch rather
than overloading the electra function

* previously, for every withdrawal, we would lock and unlock state to
deduct validator balance. In this fork, when a withdrawal is for a
builder, we also decrease the builder balance. I improved this by encap
the whole logic under a state setter
`st.DecreaseWithdrawalBalances(expected.Withdrawals)`, which only locks
and unlocks once

* what's new are the following (in code order).. In
`process_withdrawals`

  * `IsParentBlockFull`
  * `ExpectedWithdrawalsGloas`
    * `appendBuilderWithdrawals`
    * `appendBuildersSweepWithdrawals`
  * `DecreaseWithdrawalBalances`
  * `SetPayloadExpectedWithdrawals`
  * `DequeueBuilderPendingWithdrawals`
  * `SetNextWithdrawalBuilderIndex`

* i passed withdrawals by reference as `*[]*enginev1.Withdrawal`. This
results in two slice header copies of 24 bytes. I find the cost
non-matter and the code more readable, but this can be changed if anyone
has a stronger opinion
2026-02-25 18:10:15 +00:00
Potuz
e4cbb34c2f Gloas/forkchoice ptc (#16392)
This PR appplies proposer boost as per Gloas spec and deals with
decisions between full and empty children by taking PTC and DA vote into
account. It **does not** yet deal with the new equivocating conditions
to avoid applying proposer boost on those cases.

Missing tests still, opening for review early, but the bulk of Gloas
forkchoice unit tests will come in this PR later

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:32:34 +00:00
Potuz
c9a2a0dd86 Gloas Changes to ReceiveBlock (#16396)
This PR removes any execution traces from ReceiveBlock after Gloas. It
sets daWaitTime to zero and changes the logs to report on the important
fields in the bid.

Co-authored-by: james-prysm <90280386+james-prysm@users.noreply.github.com>
2026-02-24 20:43:38 +00:00
james-prysm
fdd04a5466 gloas grpc proposer (#16336)
<!-- Thanks for sending a PR! Before submitting:

1. If this is your first PR, check out our contribution guide here
https://docs.prylabs.network/docs/contribute/contribution-guidelines
You will then need to sign our Contributor License Agreement (CLA),
which will show up as a comment from a bot in this pull request after
you open it. We cannot review code without a signed CLA.
2. Please file an associated tracking issue if this pull request is
non-trivial and requires context for our team to understand. All
features and most bug fixes should have
an associated issue with a design discussed and decided upon. Small bug
   fixes and documentation improvements don't need issues.
3. New features and bug fixes must have tests. Documentation may need to
be updated. If you're unsure what to update, send the PR, and we'll
discuss
   in review.
4. Note that PRs updating dependencies and new Go versions are not
accepted.
   Please file an issue instead.
5. A changelog entry is required for user facing issues.
-->

**What type of PR is this?**

Feature

**What does this PR do? Why is it needed?**

This PR defines the validator proposer paths for Gloas, namely adding
gloas fork support on the validator client for propose function, and
adding the following gRPC endpoint support: get Gloas Block, publish
Gloas Block, Get Payload Envelope, Publish Payload envelope.

we propose in this order

1. Get block (cache payload)
2. Submit block
3. Get payload (get post state of 2 via chain info getter) // needs the
block to be recieved and processed
4. Submit payload

There are several things still missing in the PR depending on
https://github.com/OffchainLabs/prysm/pull/15656 as well as future ones,
for now I have left those areas as TODO remarks

**Which issues(s) does this PR fix?**

Fixes #

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [ ] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).

---------

Co-authored-by: terence <terence@prysmaticlabs.com>
2026-02-24 19:16:45 +00:00
Potuz
a199580672 Receive payload envelope (#16386)
This PR wires execution payload envelope processing on the blockchain
package for Gloas.

It **does not** deal with DA yet.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 16:56:32 +00:00
Potuz
b5bdd65f64 Remove head state from new head event (#16388)
The headstate parameter is not used.
2026-02-23 19:06:55 +00:00
Potuz
ce4e653261 Handle Gloas pre-state fetching (#16382)
- a new getter to forkchoice that returns the block hash for the given
blockroot
- a new blockchain getter getLookupParentRoot that returns the root that
serves as key for stategen to fetch the parent state, pre-gloas is not
modified and always returns the blockroot
- Changes the signature of getBlockPreState to receive a ROBlock
2026-02-23 18:11:47 +00:00
Potuz
6f87a0cb95 Remove num active validators (#16390)
Remove unused field in forkchoice

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 18:03:55 +00:00
terence
d262c9f927 Implement upgrade gloas (#16378)
This PR ismplement Gloas fork upgrade. There are two main parts
- The first part is upgrade fulu state to gloas and including new gloas
fields
- The second part on board builder at the fork, which is a state setter
under one block. For this, we had to refactor a few things to be lock
free
- We also added spec tests for it to pass
2026-02-23 17:44:29 +00:00
Potuz
7865bd5f32 Gloas/forkchoice gloas weights (#16357)
This PR changes attestation weight counting so that they are compatible
with Gloas.

Same slot attestations are added to the pending node, otherwise
attestations are added to the full payload status node. This PR also
deals with updating the balances and weight transfer. Notice that due to
our current limitations on how we account for weights, we will mimic the
spec status as if https://github.com/ethereum/consensus-specs/pull/4918
were merged. Counting for these attestations would be quite difficult
otherwise in Prysm.
2026-02-23 16:33:21 +00:00
terence
7f2593e496 e2e: wait for all nodes to reach mid-epoch before head compare (#16379)
`AllNodesHaveSameHead` only waits for mid‑epoch on node 0. In
`testCheckpointSync`, the checkpoint‑sync node can still be finishing
syncing while node 0 advances, so the evaluator compares heads across
different epochs and flakes. We should wait for all nodes to reach the
same point before comparing head epochs and roots

I also added better handling for context timeout per feedback from
@Inspector-Butters
2026-02-23 15:12:01 +00:00
satushh
55e2f0a7d2 Ptc duty endpoint /eth/v1/validator/duties/ptc/{epoch} (#16326)
**What type of PR is this?**

Feature

**What does this PR do? Why is it needed?**

Implement the endpoint `/eth/v1/validator/duties/ptc/{epoch}` for gloas.
Relevant link:
https://github.com/ethereum/beacon-APIs/pull/552/files#diff-ea5c1a400bd02986042f033c8d62441c11077868b0a325c6e24297875fd35c38

**Which issues(s) does this PR fix?**

Fixes #

**Other notes for review**

**Acknowledgements**

- [ ] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [ ] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [ ] I have added a description with sufficient context for reviewers
to understand this PR.
- [ ] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-02-21 03:58:34 +00:00
Potuz
bf11ac0d64 Only call FCU pre-Gloas (#16373)
Also this PR removes the unnecessary pointer parameter fcuArgs since it
is no longer used by the caller.

Co-authored-by: james-prysm <90280386+james-prysm@users.noreply.github.com>
2026-02-19 15:47:21 +00:00
Potuz
dfb918432f Gloas/forkchoice payload envelope (#16351)
Adds a method to insert full nodes after Gloas. Requires #16338
2026-02-19 10:10:55 +00:00
terence
a28c6c8145 Add gloas block gossip changes (#16368)
This updates beacon-block gossip validation for Gloas to use the signed
execution payload bid instead of the execution payload. It removes
execution‑payload‑based checks and introduces bid‑based checks, plus
stubs for parent‑payload checks pending blockchain package support

Reference:
https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/p2p-interface.md#beacon_block

fixes #https://github.com/OffchainLabs/prysm/issues/16372
2026-02-18 20:14:21 +00:00
Bastin
045a3cfe4e GetVersionV2 endpoint (#16347)
**What does this PR do? Why is it needed?**
This PR adds the implementation for `GetVersionV2` based on
[ethereum/beacon-APIs#568](https://github.com/ethereum/beacon-APIs/pull/568).

Q: is there anything to be done for deprecating `GetVersionV1`?
2026-02-18 10:02:06 +00:00
terence
8ee28394ab Add Gloas attestation committee index validation (#16359)
Adds Gloas fork attestation validation rules for gossip processing. This
implements the new committee index validation requirements introduced in
the Gloas fork.

## Changes
- Uses attestation epoch to determine if Gloas rules apply
- **Committee index validation**: 
  - Committee index must be < 2 (0 or 1 only)
- Same-slot attestations (where `attestation.data.slot == block.slot`)
must use committee index 0
  - Different-slot attestations can use either committee index 0 or 1
2026-02-17 18:00:32 +00:00
kasey
b31e2ffe51 avoid copying the finalized state when computing cgc (#16355)
Reviewing some (unrelated) sync code today I noticed that we are using a
stategen accessor for the finalized state which copies the entire state
object to look up validator balances to compute the custody_group_count.
This excess memory allocation is likely causing GC pressure and
increasing memory utilization.

This PR avoids state copying for this purpose by making the following
changes:
- Adds a new method to the `ReadOnlyBalances` state interface:
`EffectiveBalances([]primitives.ValidatorIndex) (uint64, []uint64,
error)`. This method computes returns the sum of the effective balances
of the given list of validator indices, a list with the individual
effective balance of each requested index (where the i-th element in the
return corresponds to the i-th element of the parameter), and an error -
which is necessary due to index bounds checks and quirks of multi-value
slice that can apparently result in the state being unusable for such
lookups if not correctly initialized.
- Adds a new method to the stategen interface
`FinalizedReadOnlyBalances`, which returns the finalized state asserted
to the `ReadOnlyBalances` interface.
- Switches the peerdas code to use the sum given by `EffectiveBalances`.

There was some existing nil checking code in the peerdas package that I
didn't want to modify, so I added a new compound interface in stategen
to allow the returned state to also expose the `IsNil` method.

fixes https://github.com/OffchainLabs/prysm/issues/16354

---------

Co-authored-by: Kasey Kirkham <kasey@users.noreply.github.com>
2026-02-17 08:50:58 +00:00
terence
22e77add54 Refactor function ProcessExecutionPayload with ApplyExecutionPayload (#16356)
This PR refactors `ProcessExecutionPayload` with `ApplyExecutionPayload`
so caller can use Apply method to calculate post state root. Note that
validations are not required for Apply function. We really need the
state mutation lines that's:
```
1. Ensure latest_block_header.state_root is set (if zero, set it to the pre‑payload HashTreeRoot)...
2. processExecutionRequests()
3. QueueBuilderPayment()
4. SetExecutionPayloadAvailability(state.Slot(), true)
5. SetLatestBlockHash(payload.BlockHash())
```
I decided to keep them there because a. it's cheap b. it makes refactor
cleaner to reason c. API/caller may want to validate envelope and bid
consistency (ex: beacon api has option to validate consensus)
2026-02-13 15:51:22 +00:00
terence
7f9983386e gloas: add new execution payload envelope processing (#15656)
This PR implements
[process_execution_payload](https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#new-process_execution_payload)
and spec tests.
2026-02-12 21:56:41 +00:00
Potuz
935acf6b9d Gloas/forkchoice (#16338)
Gloas Initial forkchoice implementation

This is the bare minimum to have the **current fork** logic being
compatible with having full/empty slots in forkchoice. That is,
forkchoice is designed as if we had the option to have full/empty slots,
but we still create the full node when we import a block pre-gloas.

No PTC and no ordering between empty/full is yet implemented, it just
always choses the full child between the two.

The *Node structure corresponds to the `PAYLOAD_PENDING_STATUS` in the
spec, while the enclosing *PayloadNode structures are the full
forkchoice nodes that either have full or emtpy status.

The most complicated path to check in this PR is the working of
`SetOptimisticToInvalid` that handles invalid block insertion from the
EL. There are many options, either the passed root exits or not in
forkchoice and wether this invalid node built on top of full or empty,
which is particularly ugly because if the node is not in forkchoice we
still need to find out if its parent was full or empty.

-[x] compiling beacon chain
-[x] fix tests
2026-02-12 19:39:56 +00:00
terence
63b69a63b1 Add gossip for payload envelope (#16349)
This PR implements gossip for execution payload envelope as outlined in
the following spec:

https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/p2p-interface.md#execution_payload
2026-02-12 16:52:13 +00:00
satushh
cf63d112be Graffiti implementation (#16089)
<!-- Thanks for sending a PR! Before submitting:

1. If this is your first PR, check out our contribution guide here
https://docs.prylabs.network/docs/contribute/contribution-guidelines
You will then need to sign our Contributor License Agreement (CLA),
which will show up as a comment from a bot in this pull request after
you open it. We cannot review code without a signed CLA.
2. Please file an associated tracking issue if this pull request is
non-trivial and requires context for our team to understand. All
features and most bug fixes should have
an associated issue with a design discussed and decided upon. Small bug
   fixes and documentation improvements don't need issues.
3. New features and bug fixes must have tests. Documentation may need to
be updated. If you're unsure what to update, send the PR, and we'll
discuss
   in review.
4. Note that PRs updating dependencies and new Go versions are not
accepted.
   Please file an issue instead.
5. A changelog entry is required for user facing issues.
-->

**What type of PR is this?**

Feature

**What does this PR do? Why is it needed?**

This PR implements graffiti as described in the corresponding spec doc
`graffiti-proposal-brief.md `

**Which issues(s) does this PR fix?**

- https://github.com/OffchainLabs/prysm/issues/13558

**Other notes for review**

**Acknowledgements**

- [ ] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [ ] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [ ] I have added a description to this PR with sufficient context for
reviewers to understand this PR.
2026-02-12 12:35:15 +00:00
terence
6c045083a6 gloas: add modified attestation processing (#15736)
This PR implements
[process_attestation](https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#modified-process_attestation)
alongside spec tests

---------

Co-authored-by: james-prysm <90280386+james-prysm@users.noreply.github.com>
2026-02-11 23:22:42 +00:00
james-prysm
09d0338aa9 adding db functions for saving gloas block and payload (#16301)
<!-- Thanks for sending a PR! Before submitting:

1. If this is your first PR, check out our contribution guide here
https://docs.prylabs.network/docs/contribute/contribution-guidelines
You will then need to sign our Contributor License Agreement (CLA),
which will show up as a comment from a bot in this pull request after
you open it. We cannot review code without a signed CLA.
2. Please file an associated tracking issue if this pull request is
non-trivial and requires context for our team to understand. All
features and most bug fixes should have
an associated issue with a design discussed and decided upon. Small bug
   fixes and documentation improvements don't need issues.
3. New features and bug fixes must have tests. Documentation may need to
be updated. If you're unsure what to update, send the PR, and we'll
discuss
   in review.
4. Note that PRs updating dependencies and new Go versions are not
accepted.
   Please file an issue instead.
5. A changelog entry is required for user facing issues.
-->

**What type of PR is this?**

Feature

**What does this PR do? Why is it needed?**

gloas doesn't have the concept of blinded block anymore so instead we
save the full gloas block. that being said the full block does not
contain the payload envelope so there are separate functions for saving
those. this pr introduces these types and functions, the payload
envelope doesn't actually get saved yet in this pr.

a TODO comment is added for pruning as well

references epbs branch

**Which issues(s) does this PR fix?**

Fixes #

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [ ] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-02-11 17:59:25 +00:00
james-prysm
eaf3aa3e8e simplify get and post block parsing for REST (#16307)
**What type of PR is this?**

 Feature


**What does this PR do? Why is it needed?**

PR is attempts to remove code duplication and process through a map of
configurations for get and post block apis. this should simplify
maintainability.

**Which issues(s) does this PR fix?**

Fixes #

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-02-10 18:28:55 +00:00
terence
9c49bb484c Add gossip for payload attestation (#16333)
This PR implements gossip validation and subscription for payload
attestation
2026-02-10 16:17:22 +00:00
terence
bb0f70ad60 gloas: add read only wrapper for payload envelope (#16339)
This PR adds read only wrapper for execution payload envelope
2026-02-09 17:23:12 +00:00
satushh
dc66f8872d Close libp2p host (#16313)
**What type of PR is this?**

 Other

**What does this PR do? Why is it needed?**

Close host to save resource

**Which issues(s) does this PR fix?**

Fixes #

**Other notes for review**

**Acknowledgements**

- [ ] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [ ] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [ ] I have added a description with sufficient context for reviewers
to understand this PR.
- [ ] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).

---------

Co-authored-by: Bastin <43618253+Inspector-Butters@users.noreply.github.com>
2026-02-09 14:40:07 +00:00
Preston Van Loon
db2bb5505c db: Copy byte slices that live outside of the view transaction (#16332)
**What type of PR is this?**

Bug fix

**What does this PR do? Why is it needed?**

The bbolt documentation suggests that the byte slices used during the
View transaction should not be used outside of the transaction and
mutating those slices could break things.

**Which issues(s) does this PR fix?**

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-02-07 14:10:07 +00:00
terence
14f01bbc6c gloas: move kzg commitments to bid (#16309)
This PR moves kzg commitments to bid. The rationale behind is captured
in this [issue](https://github.com/ethereum/consensus-specs/issues/4870)
* Moves blob KZG commitments to the earlier point where builder intent
is known
* Removes duplicated commitments from data column sidecars which saves
descent b/w per slot
* Enables nodes to start fetching blobs via getBlobs as soon as the bid
is received
* Slightly increases bid size and may add minor bidding channel latency
but the tradeoff favors lower network load and simpler DA handling
2026-02-06 23:07:54 +00:00
Potuz
c3e74e4a5d Remove unused method in forkchoice (#16337) 2026-02-06 19:21:58 +00:00
Potuz
e7ae6a004b Remove unused method in forkchoice (#16331) 2026-02-06 15:30:50 +00:00
Bastin
862fb2eb4a Fix gen-logs.sh - gitignore bug (#16328)
**What does this PR do? Why is it needed?**
`gen-logs.sh` was skipping `cmd/beacon-chain/execution/` due to a rule
in `.gitignore`.
Added a fix in `gen-logs.sh` to ignore `.gitignore` entries by
specification.
2026-02-05 14:06:42 +00:00
Potuz
bb80a9c832 Remove unused map in forkchoice (#16329)
nodeByPayload was not being used.
2026-02-05 13:25:06 +00:00
Bastin
c1b668a50a Fix logging issue (#16322)
**What does this PR do? Why is it needed?**
This PR, in an attempt to fix the logging issue described in #16314,
does the following:
- Adds a new field `Identifier` to the `WriterHook` struct, and filters
out log entries that have the key `log_target` and the value of the
hook's `Identifier`. For now the identifiers are `ephemeral` and `user`,
differentiating between the user facing terminal/log file, and the
debugger facing ephemeral log file.
- Stores the value of the `--verbosity` and `--log.vmodule` flags in
`io/logs`, so it can be accessed by packages that need to know the
verbosity they're logging with. (note that since #16272 each package can
have a different verbosity, so verbosity is now defined per package
instead of globally)
- Improves the calculation of the global logging level by ignoring the
`ephemeralLogFileVerbosity` when the `--disable-ephemeral-log-file` flag
is enabled.
- Uses these added logic to fix the problem in
`logStateTransitionData()` (described in #16314)

Note: since we're saving this new data in `io/logs`, we should refactor
`prefixFormatter` to read the data from here. but that creates a
circular import error. I will try to fix this and refactor the formatter
in a future PR.

---------

Co-authored-by: Manu NALEPA <enalepa@offchainlabs.com>
2026-02-05 10:24:50 +00:00
Justin Traglia
fab687d96d Improve ethspecify integration (#16304)
**What type of PR is this?**

Documentation

**What does this PR do? Why is it needed?**

* Move the ethspecify config from `/specrefs/.ethspecify` to
`/.ethspecify`.
* This allows developers to use inline specrefs (eg spec functions in
godoc comments).
* To do this, simply add a spec tag and run `ethspecify` to populate it.
* Clean up specref exceptions; organize by upgrade & put items in the
correct section.
* Update a few godoc comments to use the new inline specref feature.
* Update check-specrefs GitHub action so that it enforces up-to-date
godocs.
* Standardize specref naming; requiring a `#fork` tag for everything.
* Add new specrefs (which haven't been implemented yet) which were
missing.

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).

---------

Co-authored-by: james-prysm <90280386+james-prysm@users.noreply.github.com>
2026-02-04 18:44:01 +00:00
james-prysm
cf94ccbf72 node fallback cleanup (#16316)
**What type of PR is this?**

 Other

**What does this PR do? Why is it needed?**

Follow up to https://github.com/OffchainLabs/prysm/pull/16215 this pr
improves logging, fixes stuttering in package naming, adds additional
unit tests, and deduplicates fallback node code.

**Which issues(s) does this PR fix?**

fixes a potential race if reconnecting to the same host very quickly
which has a stale connection still.

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-02-04 15:59:42 +00:00
Aarsh Shah
75895c1e0b fix: Set Beacon Node Options after reading the config file (#16320)
**What type of PR is this?**
Bug fix

**What does this PR do? Why is it needed?**
This PR ensures that we set the beacon node options AFTER reading the
config file (if one is given to override the defaults).

**Which issues(s) does this PR fix?**
It fixes the issue that Barnabas reported around the
"MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS" override not being
respected (and potentially other issues resulting from setting the
options before reading the config).

Fixes #

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-02-03 16:51:01 +00:00
Preston Van Loon
d1b9281677 golangci-lint: Remove test exclusion from formatting (#16318)
**What type of PR is this?**

> Other

**What does this PR do? Why is it needed?**

**Which issues(s) does this PR fix?**

Follow up to #16311

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-02-02 17:42:05 +00:00
james-prysm
641d90990d grpc fallback improvements (#16215)
<!-- Thanks for sending a PR! Before submitting:

1. If this is your first PR, check out our contribution guide here
https://docs.prylabs.network/docs/contribute/contribution-guidelines
You will then need to sign our Contributor License Agreement (CLA),
which will show up as a comment from a bot in this pull request after
you open it. We cannot review code without a signed CLA.
2. Please file an associated tracking issue if this pull request is
non-trivial and requires context for our team to understand. All
features and most bug fixes should have
an associated issue with a design discussed and decided upon. Small bug
   fixes and documentation improvements don't need issues.
3. New features and bug fixes must have tests. Documentation may need to
be updated. If you're unsure what to update, send the PR, and we'll
discuss
   in review.
4. Note that PRs updating dependencies and new Go versions are not
accepted.
   Please file an issue instead.
5. A changelog entry is required for user facing issues.
-->

**What type of PR is this?**

## Summary

This PR implements gRPC fallback support for the validator client,
allowing it to automatically switch between multiple beacon node
endpoints when the primary node becomes unavailable or unhealthy.

## Changes

- Added `grpcConnectionProvider` to manage multiple gRPC connections
with circular failover
- Validator automatically detects unhealthy beacon nodes and switches to
the next available endpoint
- Health checks verify both node responsiveness AND sync status before
accepting a node
- Improved logging to only show "Found fully synced beacon node" when an
actual switch occurs (reduces log noise)


I removed the old middleware that uses gRPC's built in load balancer
because:

- gRPC's pick_first load balancer doesn't provide sync-status-aware
failover
- The validator needs to ensure it connects to a fully synced node, not
just a reachable one

## Test Scenario

### Setup
Deployed a 4-node Kurtosis testnet with local validator connecting to 2
beacon nodes:

```yaml
# kurtosis-grpc-fallback-test.yaml
participants:
  - el_type: nethermind
    cl_type: prysm
    validator_count: 128  # Keeps chain advancing
  - el_type: nethermind
    cl_type: prysm
    validator_count: 64
  - el_type: nethermind
    cl_type: prysm
    validator_count: 64   # Keeps chain advancing
  - el_type: nethermind
    cl_type: prysm
    validator_count: 64   # Keeps chain advancing

network_params:
  fulu_fork_epoch: 0
  seconds_per_slot: 6
```

Local validator started with:
```bash
./validator --beacon-rpc-provider=127.0.0.1:33005,127.0.0.1:33012 ...
```

### Test 1: Primary Failover (cl-1 → cl-2)

1. Stopped cl-1 beacon node
2. Validator detected failure and switched to cl-2

**Logs:**
```
WARN  Beacon node is not responding, switching host currentHost=127.0.0.1:33005 nextHost=127.0.0.1:33012
DEBUG Trying gRPC endpoint newHost=127.0.0.1:33012 previousHost=127.0.0.1:33005
INFO  Failover succeeded: connected to healthy beacon node failedAttempts=[127.0.0.1:33005] newHost=127.0.0.1:33012 previousHost=127.0.0.1:33005
```

**Result:**  PASSED - Validator continued submitting attestations on
cl-2

### Test 2: Circular Failover (cl-2 → cl-1)

1. Restarted cl-1, stopped cl-2
2. Validator detected failure and switched back to cl-1

**Logs:**
```
WARN  Beacon node is not responding, switching host currentHost=127.0.0.1:33012 nextHost=127.0.0.1:33005
DEBUG Trying gRPC endpoint newHost=127.0.0.1:33005 previousHost=127.0.0.1:33012
INFO  Failover succeeded: connected to healthy beacon node failedAttempts=[127.0.0.1:33012] newHost=127.0.0.1:33005 previousHost=127.0.0.1:33012
```

**Result:**  PASSED - Circular fallback works correctly

## Key Log Messages

| Log Level | Message | Source |
|-----------|---------|--------|
| WARN | "Beacon node is not responding, switching host" |
`changeHost()` in validator.go |
| INFO | "Switched gRPC endpoint" | `SetHost()` in
grpc_connection_provider.go |
| INFO | "Found fully synced beacon node" | `FindHealthyHost()` in
validator.go (only on actual switch) |

## Test Plan

- [x] Verify primary failover (cl-1 → cl-2)
- [x] Verify circular failover (cl-2 → cl-1)
- [x] Verify validator continues producing attestations after switch
- [x] Verify "Found fully synced beacon node" only logs on actual switch
(not every health check)

**What does this PR do? Why is it needed?**

**Which issues(s) does this PR fix?**

Fixes # https://github.com/OffchainLabs/prysm/pull/7133


**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).

---------

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Radosław Kapka <rkapka@wp.pl>
Co-authored-by: Manu NALEPA <enalepa@offchainlabs.com>
2026-02-02 14:51:56 +00:00
terence
d2fc250f34 Run go fmt (#16311)
Co-authored-by: Manu NALEPA <enalepa@offchainlabs.com>
2026-02-02 14:19:15 +00:00
Jun Song
571c6f39aa Add docs for SSZ Query package (#16299)
**What type of PR is this?**

Documentation

**What does this PR do? Why is it needed?**

Although godoc and comments are well-written in `encoding/ssz/query`
package, we (@rkapka, @fernantho, @syjn99)
[agreed](https://discord.com/channels/476244492043812875/1387734369527136297/1466075406523174944)
that it would be great to have human-readable documentation.

**Which issues(s) does this PR fix?**

Part of  #15587 & #15598 

**Other notes for review**

This documentation is first drafted by Claude Code, and then has a few
rounds of self-review.

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [ ] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).

---------

Co-authored-by: fernantho <fernantho1@gmail.com>
Co-authored-by: Radosław Kapka <radoslaw.kapka@gmail.com>
2026-02-01 03:39:53 +00:00
Justin Traglia
55fe85c887 Add ability to download nightly tests from a specific night (#16298)
**What type of PR is this?**

Feature

**What does this PR do? Why is it needed?**

This PR allows devs to test against a specific run of the nightly
reference test generator.

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-01-29 21:38:13 +00:00
Justin Traglia
31f77567dd Add a README for specrefs (#16302)
**What type of PR is this?**

Documentation

**What does this PR do? Why is it needed?**

This PR adds a basic README for the specrefs.


**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-01-29 20:36:29 +00:00
terence
a7fdd11777 gloas: sample PTC per committee (#16293)
This PR updates `get_ptc` construction to sample ptc
committee-by-committee instead of concatenating all beacon committees
into a large slice. No functional changes to payload attestation
verification
2026-01-29 14:21:54 +00:00
james-prysm
919bd5d6aa Update health endpoint to include sync and optimistic checks (#16294)
**What type of PR is this?**
Other

**What does this PR do? Why is it needed?**

**Which issues(s) does this PR fix?**

Fixes #

**Other notes for review**

a node that is in syncing or optimistic status isn't fully ready yet. we
don't have a is ready endpoint, but I think having the gRPC match more
closely to
[/eth/v1/node/health](https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Node/getHealth)
would be good. This endpoint is only used internally as far as I can
tell.

this is prerequisite to https://github.com/OffchainLabs/prysm/pull/16215

tested via grpcurl against a syncing hoodi node

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-01-28 21:13:30 +00:00
fernantho
0476eeda57 SSZ-QL: custom Generic Merkle Proofs building the tree and collecting the hashes in one sweep (#16177)
<!-- Thanks for sending a PR! Before submitting:

1. If this is your first PR, check out our contribution guide here
https://docs.prylabs.network/docs/contribute/contribution-guidelines
You will then need to sign our Contributor License Agreement (CLA),
which will show up as a comment from a bot in this pull request after
you open it. We cannot review code without a signed CLA.
2. Please file an associated tracking issue if this pull request is
non-trivial and requires context for our team to understand. All
features and most bug fixes should have
an associated issue with a design discussed and decided upon. Small bug
   fixes and documentation improvements don't need issues.
3. New features and bug fixes must have tests. Documentation may need to
be updated. If you're unsure what to update, send the PR, and we'll
discuss
   in review.
4. Note that PRs updating dependencies and new Go versions are not
accepted.
   Please file an issue instead.
5. A changelog entry is required for user facing issues.
-->

**What type of PR is this?**
Feature

**What does this PR do? Why is it needed?**
This PR replaces the previous PR
https://github.com/OffchainLabs/prysm/pull/16121, which built the entire
Merkle tree and generated proofs only after the tree was complete. In
this PR, the Merkle proof is produced by collecting hashes while the
Merkle tree is being built. This approach has proven to be more
efficient than the one in
https://github.com/OffchainLabs/prysm/pull/16121.

- **ProofCollector**: 
- New `ProofCollector` type in `encoding/ssz/query/proof_collector.go`:
Collects sibling hashes and leaves needed for Merkle proofs during
merkleization.
- Multiproof-ready design with `requiredSiblings`/`requiredLeaves` maps
for registering target gindices before merkleization.
- Thread-safe: read-only required maps during merkleization,
mutex-protected writes to `siblings`/`leaves`.
- `AddTarget(gindex)` registers a target leaf and computes all required
sibling gindices along the path to root.
- `toProof()` converts collected data into `fastssz.Proof` structure.
- Parallel execution in `merkleizeVectorBody` for composite elements
with worker pool pattern.
- Optimized container hashing: Generalized
`stateutil.OptimizedValidatorRoots` pattern for any SSZ container type:
- `optimizedContainerRoots`: Parallelized field root computation +
level-by-level vectorized hashing via `VectorizedSha256`.
- `hashContainerHelper`: Worker goroutine for processing container
subsets.
- `containerFieldRoots`: Computes field roots for a single container
using reflection and SszInfo metadata.

- **`Prove(gindex)` method** in `encoding/ssz/query/merkle_proof.go`:
Entry point for generating SSZ Merkle proofs for a given generalized
index.

- **Testing**
- Added `merkle_proof_test.go` and `proof_collector_test.go` to test and
benchmark this feature.

The main outcomes of the optimizations are here:
```
❯ go test ./encoding/ssz/query -run=^$ -bench='Benchmark(OptimizedContainerRoots|OptimizedValidatorRoots|ProofCollectorMerkleize)$' -benchmem
goos: darwin
goarch: arm64
pkg: github.com/OffchainLabs/prysm/v7/encoding/ssz/query
cpu: Apple M2 Pro
BenchmarkOptimizedValidatorRoots-10         3237            361029 ns/op          956858 B/op       6024 allocs/op
BenchmarkOptimizedContainerRoots-10         1138            969002 ns/op         3245223 B/op      11024 allocs/op
BenchmarkProofCollectorMerkleize-10          522           2262066 ns/op         3216000 B/op      19000 allocs/op
PASS
ok      github.com/OffchainLabs/prysm/v7/encoding/ssz/query     4.619s
```
Knowing that `OptimizedValidatorRoots` implements very effective
optimizations, `OptimizedContainerRoots` mimics them.
In the benchmark we can see that `OptimizedValidatorRoots` remain as the
most performant and tit the baseline here:
- `ProofCollectorMerkleize` is **~6.3× slower**, uses **~3.4× more
memory** (B/op), and performs **~3.2× more allocations**.
- `OptimizedContainerRoots` sits in between: it’s **~2.7× slower** than
`OptimizedValidatorRoots` (and **~3.4× higher B/op**, **~1.8× more
allocations**), but it is a clear win over `ProofCollectorMerkleize` for
lists/vectors: **~2.3× faster** with **~1.7× fewer allocations** (and
essentially the same memory footprint).

The main drawback is that `OptimizedContainerRoots` can only be applied
to vector/list subtrees where we don’t need to collect any sibling/leaf
data (i.e., no proof targets within that subtree); integrating it into
the recursive merkleize(...) flow when targets are outside the subtree
is expected to land in a follow-up PR.

**Which issues(s) does this PR fix?**
Partially https://github.com/OffchainLabs/prysm/issues/15598

**Other notes for review**
In this [write-up](https://hackmd.io/@fernantho/BJbZ1xmmbg), I depict
the process to come up with this solution.

Future improvements:
- Defensive check that the gindex is not too big, depicted [here](
https://github.com/OffchainLabs/prysm/pull/16177#discussion_r2671684100).
- Integrate optimizedContainerRoots into the recursive merkleize(...)
flow when proof targets are not within the subtree (skip full traversal
for container lists).
- Add multiproofs.
- Connect `proofCollector` to SSZ-QL endpoints (direct integration of
`proofCollector` for BeaconBlock endpoint and "hybrid" approach for
BeaconState endpoint).

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).

---------

Co-authored-by: Radosław Kapka <radoslaw.kapka@gmail.com>
Co-authored-by: Jun Song <87601811+syjn99@users.noreply.github.com>
2026-01-28 13:01:22 +00:00
Victor Farazdagi
1c65c8866a fix: bazel build failure on macOS (hashtree patch) (#16281)
**What type of PR is this?**


Bug fix

**What does this PR do? Why is it needed?**
  
It appears that #16216 introduced hashtree integration but broke builds
on macOS Intel (darwin_amd64).

```                                                                                                                                                                                                                                                                          
  Error:                                                                                                                                                                                                                                                                  
  Undefined symbols for architecture x86_64:                                                                                                                                                                                                                              
    "_github.com/OffchainLabs/hashtree.HashtreeHash"                                                                                                                                                                                                                      
```

The Bazel patch for hashtree was missing `wrapper_darwin_amd64.s`. So,
`//go:noescape` in `bindings.go` assumes that symbols are available
elsewhere, and while on other platforms optimized version is used, on
Darwin we have stub (symbol still must be available), which needs to be
referenced -- hence, this PR.

**Other notes for review**

I've re-checked using `bazel clean && bazel build //cmd/beacon-chain` --
it was failing before, works now.

cc @potuz as original patch author

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).

Co-authored-by: Potuz <potuz@prysmaticlabs.com>
2026-01-27 16:40:29 +00:00
terence
14a4b97d57 gloas: add duty timing BPS and fork epoch gating (#16291)
This PR adds gloas specific time basis points for validator duties
(attestation, aggregation, and sync contribution) and wires the
validator client to use them once `GloasForkEpoch` is active. For now,
`GloasForkEpoch` is set to far-future so behavior is unchanged until the
fork epoch is configured

Note to the rev: today sync message duty uses `waitOneThirdOrValidBlock`
which shares with attestation dead line, that's why sync message bps is
not used
2026-01-27 15:29:37 +00:00
Potuz
0e537694c3 Hdiff start database (#16203)
Initialize state-diff database on new node startup

When starting a node with --enable-state-diff feature flag, the database
is now initialized appropriately based on the startup scenario:

- New database + checkpoint sync: Set offset to checkpoint state slot,
save checkpoint state as full snapshot
- New database + genesis sync: Set offset to 0, save genesis state as
full snapshot
- Existing database without state-diff: Log warning that feature is
ignored due to old schema
- Existing database with state-diff: Return error (restarts will be
handled in a future PR)

Review after #16049 has merged

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Bastin <43618253+Inspector-Butters@users.noreply.github.com>
2026-01-27 13:30:52 +00:00
Bastin
1b190e966e add path field for ephemeral log file initialization (#16289)
**What does this PR do? Why is it needed?**
Add a field `path` to the initialization log of ephemeral-logfile
feature:

before:
```
[2026-01-26 19:49:34.64] DEBUG Ephemeral log file initialized
```

after:
```
[2026-01-26 19:49:34.64] DEBUG Ephemeral log file initialized path=../eth/datadir/logs/beacon-chain.log
```
2026-01-27 10:37:23 +00:00
terence
2cbb743606 gloas: add new payload attestation processing (#15650)
This PR implements
[process_payload_attestation](https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#new-process_payload_attestation)
and spec tests
2026-01-27 03:34:12 +00:00
james-prysm
ce9c968dcc adding gloas block support to /eth/v2/beacon/blocks/{block_id} and /eth/v1/beacon/blocks/{block_id}/root (#16278)
**What type of PR is this?**
Feature

**What does this PR do? Why is it needed?**

adding gloas block return on `/eth/v2/beacon/blocks/{block_id}` endpoint

-
[/eth/v1/beacon/blocks/{block_id}/root](https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getBlockRoot)
should inherit this as well

reference
https://github.com/ethereum/beacon-APIs/pull/552/files#r2722805266

**Which issues(s) does this PR fix?**

Fixes #

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-01-27 00:02:16 +00:00
Potuz
60e5c29b92 Fix arm64 hashtree builds (#16288)
This PR fixes release builds that fail because of hashtree's patch.
Should be in conjunction with #16281

---------

Co-authored-by: Preston Van Loon <preston@pvl.dev>
2026-01-26 20:20:57 +00:00
Bastin
e27445bae2 Add cmd/* to gen-logs.sh (#16285)
**What does this PR do? Why is it needed?**
This PR adds the `cmd` directory and all it's subcategories to the
`hack/gen-logs.sh`. meaning that they will now have a `log.go` file and
a `package` field. see the list of added packages below. some of them
had different names, which has been added to the `prefixReplacements`
map.

packages added:
"cmd"
"cmd/validator/web"
"cmd/validator/wallet"
"cmd/validator/slashing-protection"
"cmd/validator/db"
"cmd/validator/accounts"
"cmd/prysmctl"
"cmd/prysmctl/weaksubjectivity"
"cmd/prysmctl/validator"
"cmd/prysmctl/testnet"
"cmd/prysmctl/p2p"
"cmd/prysmctl/db"
"cmd/prysmctl/checkpointsync"
"cmd/client-stats"
"cmd/beacon-chain"
"cmd/beacon-chain/storage"
"cmd/beacon-chain/jwt"
"cmd/beacon-chain/genesis"
"cmd/beacon-chain/flags"
"cmd/beacon-chain/db"
2026-01-26 17:49:52 +00:00
terence
42e5417a7b core: move EL requests to core/requests (#16280)
This PR moves consolidation and withdrawal request processing into
neutral package: `beacon-chain/core/requests`, such that they can be
shared between fork

Why?
Today our core packages are mixed between two workflows: older forks
calling into newer forks, and newer work calling back into older fork
code. No matter which direction we settle on long-term, a clear first
step is to move shared cross-fork functionality into a neutral place so
we can reuse it later such that if fork A and fork B interleave each
other, they can become clear abstractions

Why git mv wasn’t used for consolidations?
Withdrawals lived in a clean, dedicated file so it was moved with git
mv. Consolidation logic was interleaved with other Electra code and
needed extraction/refactor, so a pure git mv would not have preserved
meaningful history
2026-01-26 16:31:47 +00:00
Bastin
c8012b41f9 Fix bug in check-logs (#16287)
**What does this PR do? Why is it needed?**
Fixing a bug in `check-logs.sh` where untracked files were ignored. 
for example PR #16216 passed the check while it shouldn't have. this
change fixes the problem.
2026-01-26 16:28:46 +00:00
Bastin
f35074a78f Add startup version log (#16283)
**What does this PR do? Why is it needed?**
Adds a log mentioning the name and the version of the app: 

Beacon-chain:
```
[2026-01-26 12:03:12.91]  INFO main: Prysm Beacon Chain started. [Prysm/Unknown/Local build. Built at: Moments ago] prefix=main
```
Validator:
```
[2026-01-26 12:00:07.38]  INFO main: Prysm Validator started. [Prysm/v7.1.2/7950a249266a692551e5a910adb9a82a02c92040. Built at: 2026-01-06 18:47:23+00:00] prefix=main
```

**Note:** I've removed the previous two logs that mentioned version in
favor of these new ones. I've also moved the initialization logs of
persistent log file and ephemeral log file to DEBUG.

**Which issues(s) does this PR fix?**

Fixes #16259
2026-01-26 16:10:46 +00:00
james-prysm
520733ba55 removing duplicate logic in /eth/v1/beacon/blocks/{block_id}/root by using blocker lookup (#16279)
<!-- Thanks for sending a PR! Before submitting:

1. If this is your first PR, check out our contribution guide here
https://docs.prylabs.network/docs/contribute/contribution-guidelines
You will then need to sign our Contributor License Agreement (CLA),
which will show up as a comment from a bot in this pull request after
you open it. We cannot review code without a signed CLA.
2. Please file an associated tracking issue if this pull request is
non-trivial and requires context for our team to understand. All
features and most bug fixes should have
an associated issue with a design discussed and decided upon. Small bug
   fixes and documentation improvements don't need issues.
3. New features and bug fixes must have tests. Documentation may need to
be updated. If you're unsure what to update, send the PR, and we'll
discuss
   in review.
4. Note that PRs updating dependencies and new Go versions are not
accepted.
   Please file an issue instead.
5. A changelog entry is required for user facing issues.
-->

**What type of PR is this?**

Other

**What does this PR do? Why is it needed?**


[/eth/v1/beacon/blocks/{block_id}/root](https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getBlockRoot)
updated logic so that it uses the blocker lookup instead of mostly
duplicated logic

- The new implementation returns an error if no canonical block exists
at a slot, while the old one would return a
   non-canonical block's root
- adds justified as an option for block id

**Which issues(s) does this PR fix?**

Fixes #

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).

---------

Co-authored-by: Radosław Kapka <rkapka@wp.pl>
2026-01-26 15:07:38 +00:00
terence
bcf060619b Refactor expected withdrawals helpers (#16282)
This refactors `ExpectedWithdrawals` to use shared helpers for pending
partial withdrawals and the validator sweep. The goal is to keep the
logic spec-aligned while making it easier to reuse across future forks.
```
**Example (Gloas expected withdrawals order):**
builder withdrawals
pending partial withdrawals
builder sweep withdrawals
validator sweep withdrawals
```

Note to reviewers: the helpers take a `*[]*enginev1.Withdrawal` so we
can append efficiently without returning/re-appending slices.
2026-01-26 14:02:05 +00:00
terence
37b27fdd3c Move deposit helpers out of blocks to break blocks <-> gloas cycle (#16277)
- moves deposit-related helpers (deposit signature verification, batch
verification, merkle proof verification, and activation helper) from
`beacon-chain/core/blocks` into `beacon-chain/core/helpers`
- updates call sites (Altair/Electra) to use helpers

Why?
- In gloas, the blocks package needs to call into gloas logic (e.g.
clearing builder pending payments/withdrawals on proposer slashing)
- gloas also introduces deposit-request processing which needs deposit
signature verification previously located in blocks.
That creates a Bazel/Go dependency cycle (blocks -> gloas -> blocks)
- the natural layering is for blocks and fork logic to depend on a lower
level util package for deposit verification, so moving deposit helpers
to core/helpers breaks the cycle
2026-01-23 21:33:28 +00:00
Bastin
6a9bcbab3a logging: per package verbosity (#16272)
**What type of PR is this?**
Feature

**What does this PR do? Why is it needed?**
This PR adds a `--log.vmodule` flag to the beacon-chain and validator
apps, that allows setting a different verbosity for every* package.

*: not every package, but most packages. (all packages that define their
logger variable with a `package` field)

Combined with the `--verbosity` flag this allows users to control
exactly what they see.

This affects both the terminal and the log file (`--log-file`), but not
the ephemeral debug log file.

example usage: 
```
./beacon-chain --log.vmodule=beacon-chain/p2p=info,beacon-chain/sync=error,beacon-chain/sync/initial-sync=debug
```

There are improvements to be done later, like accepting just the package
name instead of the full path, etc.
2026-01-23 19:22:20 +00:00
Bastin
1b2524b0fe add NewBeaconStateGloas() (#16275)
**What does this PR do? Why is it needed?**
adds `util.NewBeaconStateGloas()`

also adds a missing fulu test.
2026-01-23 18:39:05 +00:00
satushh
67d11607ea Close opened file in defer in data_column.go (#16274)
**What type of PR is this?**

Bug fix

**What does this PR do? Why is it needed?**

Closes an opened file which was left open. 

**Which issues(s) does this PR fix?**

Fixes #

**Other notes for review**

**Acknowledgements**

- [ ] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [ ] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [ ] I have added a description with sufficient context for reviewers
to understand this PR.
- [ ] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-01-23 15:45:03 +00:00
satushh
4ff15fa988 Add missing fulu presets to beacon config (#16170)
<!-- Thanks for sending a PR! Before submitting:

1. If this is your first PR, check out our contribution guide here
https://docs.prylabs.network/docs/contribute/contribution-guidelines
You will then need to sign our Contributor License Agreement (CLA),
which will show up as a comment from a bot in this pull request after
you open it. We cannot review code without a signed CLA.
2. Please file an associated tracking issue if this pull request is
non-trivial and requires context for our team to understand. All
features and most bug fixes should have
an associated issue with a design discussed and decided upon. Small bug
   fixes and documentation improvements don't need issues.
3. New features and bug fixes must have tests. Documentation may need to
be updated. If you're unsure what to update, send the PR, and we'll
discuss
   in review.
4. Note that PRs updating dependencies and new Go versions are not
accepted.
   Please file an issue instead.
5. A changelog entry is required for user facing issues.
-->

**What type of PR is this?**

Feature

**What does this PR do? Why is it needed?**

Added some missing constants of fulu to beacon config so that beacon api
returns expected values

**Which issues(s) does this PR fix?**

Fixes https://github.com/OffchainLabs/prysm/issues/16138

**Other notes for review**

**Acknowledgements**

- [ ] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [ ] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [ ] I have added a description with sufficient context for reviewers
to understand this PR.
- [ ] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-01-23 15:20:22 +00:00
james-prysm
b37b3e26ba constants update for ethspecify phase 0 (#16273)
**What type of PR is this?**

Other

**What does this PR do? Why is it needed?**

mapping ethspecify items that we have implemented but missing from
ethspecify for constants

**Which issues(s) does this PR fix?**
follow up on https://github.com/OffchainLabs/prysm/pull/16194

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-01-22 21:01:23 +00:00
Manu NALEPA
4ff9eb067c Allow a flag to be hidden and hide the --disable-get-blobs-v2 flag. (#16265)
**What type of PR is this?**
Other

**What does this PR do? Why is it needed?**
Allow a flag to be hidden and hide the `--disable-get-blobs-v2` flag.

This flag is still usable, but does not show up any more in the help.
This flag is used for internal purpose only, so no need to expose it
publicly.

**Why do we need to modify the `cli.HelpPrinter` function whereas other
flags like `--aggregate-first-interval` are hidden just by using the
`Hidden: true` property?**

The `Hidden: true` property on the flag definition doesn't work by
itself because `usage.go` uses a custom help template that bypasses the
standard urfave/cli help rendering.

**Acknowledgements**
- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).

---------

Co-authored-by: Bastin <bastin.m@proton.me>
2026-01-22 16:35:24 +00:00
terence
d440aafacf gloas: add modified proposer slashing processing (#16212)
This PR implements
[process_proposer_slashing](https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#modified-process_proposer_slashing)
alongside spec tests
2026-01-22 15:38:55 +00:00
james-prysm
e336f7fe59 adding in mid epoch timeout for e2e head synced evaluator (#16268)
**What type of PR is this?**

tests

**What does this PR do? Why is it needed?**

reduce e2e flakes by adding a mid epoch check for headslot sync

**Which issues(s) does this PR fix?**

Fixes #

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-01-22 14:07:59 +00:00
terence
fde63a217a gloas: add modified slot processing (#15730)
This PR implements
[process_slot](https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#modified-process_slot)
and spec tests
2026-01-20 22:44:09 +00:00
Luca | Serenita
055c6eb784 fix: typo in AggregateDueBPS (#16194)
<!-- Thanks for sending a PR! Before submitting:

1. If this is your first PR, check out our contribution guide here
https://docs.prylabs.network/docs/contribute/contribution-guidelines
You will then need to sign our Contributor License Agreement (CLA),
which will show up as a comment from a bot in this pull request after
you open it. We cannot review code without a signed CLA.
2. Please file an associated tracking issue if this pull request is
non-trivial and requires context for our team to understand. All
features and most bug fixes should have
an associated issue with a design discussed and decided upon. Small bug
   fixes and documentation improvements don't need issues.
3. New features and bug fixes must have tests. Documentation may need to
be updated. If you're unsure what to update, send the PR, and we'll
discuss
   in review.
4. Note that PRs updating dependencies and new Go versions are not
accepted.
   Please file an issue instead.
5. A changelog entry is required for user facing issues.
-->

**What type of PR is this?**

Bug fix

**What does this PR do? Why is it needed?**

This PR fixes a typo which resulted in a wrong variable name to be
returned on the Beacon API `/eth/v1/config/spec` endpoint:

```
curl http://127.0.0.1:49183/eth/v1/config/spec
{"data":{"AGGREGRATE_DUE_BPS":"6667", [...]
```

I discovered the discrepancy while testing the change to these "BPS"
values in the Vero VC which checks spec values against the ones it ships
with.

**Which issues(s) does this PR fix?**

N/A

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).

---------

Co-authored-by: james-prysm <90280386+james-prysm@users.noreply.github.com>
2026-01-20 15:16:13 +00:00
terence
d33389fb54 gloas: add new pending payment processing (#15655)
This PR implements
[process_builder_pending_payments](https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#new-process_builder_pending_payments)
and spec tests.
2026-01-16 21:19:06 +00:00
Maxim Evtush
ce72deb3c0 Fix authentication bypass for direct /v2/validator/* endpoints (#16226)
This PR fixes a security vulnerability where authenticated endpoints
could be accessed without authorization by using direct
`/v2/validator/*` paths instead of `/api/v2/validator/*`.

The `AuthTokenHandler` middleware only checked for authentication on
requests containing `/api/v2/validator/` or `/eth/v1` prefixes, but the
same handlers are also registered for direct `/v2/validator/*` routes.
This allowed attackers to bypass authentication by simply removing the
`/api` prefix from the URL.

---------

Co-authored-by: james-prysm <90280386+james-prysm@users.noreply.github.com>
2026-01-16 19:43:27 +00:00
Manu NALEPA
ec48e6340c Stop batching of KZG verification for incoming via gossip data column sidecars (#16240)
**What type of PR is this?**
Optimisation

**What does this PR do? Why is it needed?**
This is an alternate take of:
- https://github.com/OffchainLabs/prysm/pull/16220


**Test configuration:**
- Using the `--disable-get-blobs-v2` and `--supernode` flags
- On [VPS 3000 G11](https://www.netcup.com/en/server/vps)

**4H average**
| Impl. | CPU usage| Sidecar gossip verif. dur. | DA waiting time |
Chain service proc. time | Total |
|--------|--------|--------|--------|--------|--------|
| `develop` | 132% | 185 ms | 82.2 ms | **457 ms** | 539 ms |
| https://github.com/OffchainLabs/prysm/pull/16220 | 144% | 76.5 ms |
21.7 ms | 473 ms | **495 ms** |
| This PR  | **117%** | **26 ms** | **16.3 ms** | 479 ms | **495 ms** |

 
**Before this PR:**
<img width="950" height="1296" alt="image"
src="https://github.com/user-attachments/assets/1fb45282-a9c8-4543-adb3-39b04b79eab2"
/>

**With this PR:**
<img width="950" height="1301" alt="image"
src="https://github.com/user-attachments/assets/993feb49-ef38-4052-9cb4-aebe93456eba"
/>

Metrics:
- `beacon_data_column_sidecar_gossip_verification_milliseconds`
- `da_waited_time_milliseconds`

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-01-16 18:53:45 +00:00
Preston Van Loon
a135a336c3 Fix issue : Prevent makeslice panic from invalid Count values (#16227)
**What type of PR is this?**

Bug fix

**What does this PR do? Why is it needed?**

Add defensive checks to prevent panic from large Count values that could
result from unsigned integer underflow:

1. In batch.blockRequest() and batch.blobRequest(): Return Count=0 when
end <= begin, preventing the underflow at the source.

2. In SendBeaconBlocksByRangeRequest(): Cap slice capacity to
MaxRequestBlock before allocation to prevent panic even if upstream code
produces invalid values.


**Which issues(s) does this PR fix?**

Fixes #16223

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-01-16 14:25:04 +00:00
Manu NALEPA
5f189f002e Remove unused delay parameter from fetchOriginDataColumnSidecars function. (#16262)
**What type of PR is this?**
Other

**What does this PR do? Why is it needed?**
Remove unused delay parameter from `fetchOriginDataColumnSidecars`
function.

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-01-16 14:04:42 +00:00
willian.eth
bca6166e82 Add shell completion for beacon-chain and validator CLI (#16245)
**What type of PR is this?**

Feature

**What does this PR do? Why is it needed?**

Introduces a `completion` subcommand to `beacon-chain` and `validator`
that outputs shell completion scripts. Supports Bash, Zsh, and Fish
shells.

```bash
# Load completions in current session
source <(beacon-chain completion bash)

# Persist for future sessions
beacon-chain completion zsh > "${fpath[1]}/_beacon-chain"
validator completion fish > ~/.config/fish/completions/validator.fish
```

Once loaded, users can press TAB to complete subcommands, nested
commands, and flags. Flag completion supports prefix matching (e.g.,
typing `--exec<TAB>` suggests `--execution-endpoint`,
`--execution-headers`).

**Which issues(s) does this PR fix?**

Fixes #16244

**Other notes for review**

The implementation adds three files to the existing `cmd` package:
- `completion.go` - Defines `CompletionCommand()` returning a
`*cli.Command` with `bash`, `zsh`, `fish` subcommands
- `completion_scripts.go` - Contains the shell script templates
- `completion_test.go` - Unit tests for command structure and script
content

Changes to `beacon-chain` and `validator`:
- Import `cmd.CompletionCommand("binary-name")` in the Commands slice
- Set `EnableBashCompletion: true` on the cli.App to activate
urfave/cli's `--generate-bash-completion` hidden flag

The shell scripts call the binary with `--generate-bash-completion`
appended to get context-aware suggestions. This means completions
automatically reflect the current binary's flags and commands.

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).

Signed-off-by: Willian Paixao <willian@ufpa.br>
2026-01-15 20:07:11 +00:00
Bastin
b6818853b4 state-diff small changes (#16260)
**What does this PR do?**
small touch ups on state diff code.
2026-01-15 18:47:29 +00:00
satushh
5a56bfcf98 Print commitments instead of indices (#16258)
**What type of PR is this?**

Other

**What does this PR do? Why is it needed?**

Print commitments instead of indices in `missingCommitError` function

**Which issues(s) does this PR fix?**

Fixes #

**Other notes for review**

**Acknowledgements**

- [ ] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [ ] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [ ] I have added a description with sufficient context for reviewers
to understand this PR.
- [ ] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-01-15 15:37:32 +00:00
terence
a08f185170 gloas: add new execution payload bid processing (#15638)
This PR implements
[process_execution_payload_bid](https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#new-process_execution_payload_bid)
and spec tests
2026-01-15 10:52:19 +00:00
Preston Van Loon
15b1d68249 CI: Add gazelle update-repos check (#16257)
**What type of PR is this?**

Other

**What does this PR do? Why is it needed?**

This wont pass til #16252 merges

**Which issues(s) does this PR fix?**

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-01-15 03:47:34 +00:00
Victor Farazdagi
885d9cc478 fix: use input genesis.json timestamp in prysmctl (#16239)
**What type of PR is this?**

Bug fix


**What does this PR do? Why is it needed?**

- When no `--genesis-time` is provided we default to `now()`, instead of
using `timestamp` from provided `--geth-genesis-json-in` input file
- This results in inconsistencies, especially, if the input file is not
overwritten using `--geth-genesis-json-out` (say, the generator is used
to produce the `genesis.ssz` file only, as described in the original
issue)

**Which issues(s) does this PR fix?**

Fixes #16002

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-01-15 03:47:06 +00:00
Victor Farazdagi
511248213c refactor: remove gometalinter refs (#16229)
**What type of PR is this?**

Refactor/Cleanup

**What does this PR do? Why is it needed?**

- [gometalinter](https://github.com/alecthomas/gometalinter) project is
deprecated (has been forever so)
- in #2100 it has been removed from the Prysm project
- while not being used `gometalinter` references still existed in Bazel
config. Since `gometalinter.json` was removed in PR-2100, the `bazel run
//:gometalinter` will err unless config file re-created, hence it
appears we have a dead code.

**Which issues(s) does this PR fix?**

No issue, simple cleanup

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-01-15 03:46:13 +00:00
Preston Van Loon
1a936e2ffa Update go-ethereum to v1.16.8 (security fix release) (#16252)
**What type of PR is this?**

Other

**What does this PR do? Why is it needed?**

Updating our internal geth dependency after today's release.

**Which issues(s) does this PR fix?**

**Other notes for review**

No known security issues in Prysm. 

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-01-14 20:48:44 +00:00
Galoretka
6027518ad5 fix: stop SlotIntervalTicker goroutine leaks (#16241)
`SlotIntervalTicker` was created in `prepareForkChoiceAtts` and in the
fork-choice attestation processing routine without ever calling `Done()`
when the service context was cancelled. Once the consuming goroutine
exits on `ctx.Done()`, the ticker keeps running and eventually blocks on
sending to its channel, leaving a leaked goroutine behind.

This change wires the lifetime of the `SlotIntervalTicker` to the
corresponding service contexts by calling `ticker.Done()` on
`ctx.Done()` in both call sites. This keeps the behaviour of the
routines unchanged while ensuring the ticker goroutines exit cleanly on
shutdown, consistent with how regular `SlotTicker` is handled elsewhere
in the codebase.
2026-01-14 17:14:17 +00:00
Manu NALEPA
e4a6bc7065 Add metrics monitoring blob count per block (#16254)
**What type of PR is this?**
Feature

**What does this PR do? Why is it needed?**
Add metrics monitoring blob count per block for block received via
gossip

<img width="954" height="659" alt="image"
src="https://github.com/user-attachments/assets/ae9ff9ed-06ae-473b-bb4e-f162cf17702b"
/>

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-01-14 13:18:59 +00:00
satushh
a2982f0807 Avoid unnecessary heap allocation (#16251)
**What type of PR is this?**

Micro optimisation

**What does this PR do? Why is it needed?**
Avoids unnecessary allocation 

**Acknowledgements**

- [ ] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [ ] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [ ] I have added a description with sufficient context for reviewers
to understand this PR.
- [ ] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-01-14 10:08:47 +00:00
james-prysm
73e9d6e0ce adding cache for attestation data so we don't call it multiple times (#16236)
**What type of PR is this?**

Other

**What does this PR do? Why is it needed?**

post electra attestation data calls are the same in a slot because
committee information is 0, we can save some api calls by caching the
attestation data per slot.

**Which issues(s) does this PR fix?**

Fixes # https://github.com/OffchainLabs/prysm/issues/16228

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-01-14 02:30:35 +00:00
Potuz
2e43d50364 Use dependent root when validating data column (#16250)
This PR uses the head state to validate data column in more places than
we currently do. When the parent state is from the previous epoch and is
the head (for example at slot 0) instead of replaying slot and doing an
epoch transition, we use the head state directly.

Another change is that instead of replaying until the parent state in
the case of a head miss, we only replay until the target checkpoint
state, which is more likely to be a checkpoint state in the epoch
boundary cache.

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 18:19:41 +00:00
Nina
71e7b526d2 docs: fix broken links (#15856)
<!-- Thanks for sending a PR! Before submitting:

1. If this is your first PR, check out our contribution guide here
https://docs.prylabs.network/docs/contribute/contribution-guidelines
You will then need to sign our Contributor License Agreement (CLA),
which will show up as a comment from a bot in this pull request after
you open it. We cannot review code without a signed CLA.
2. Please file an associated tracking issue if this pull request is
non-trivial and requires context for our team to understand. All
features and most bug fixes should have
an associated issue with a design discussed and decided upon. Small bug
   fixes and documentation improvements don't need issues.
3. New features and bug fixes must have tests. Documentation may need to
be updated. If you're unsure what to update, send the PR, and we'll
discuss
   in review.
4. Note that PRs updating dependencies and new Go versions are not
accepted.
   Please file an issue instead.
5. A changelog entry is required for user facing issues.
-->

**What type of PR is this?**

 Documentation

**What does this PR do? Why is it needed?**

Fixes broken and old links in documentation following code standards and
to avoid possible future problems with redirects



**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description to this PR with sufficient context for
reviewers to understand this PR.

---------

Co-authored-by: james-prysm <90280386+james-prysm@users.noreply.github.com>
2026-01-13 16:55:50 +00:00
james-prysm
ea8baab7b0 fulu e2e (#15640)
<!-- Thanks for sending a PR! Before submitting:

1. If this is your first PR, check out our contribution guide here
https://docs.prylabs.network/docs/contribute/contribution-guidelines
You will then need to sign our Contributor License Agreement (CLA),
which will show up as a comment from a bot in this pull request after
you open it. We cannot review code without a signed CLA.
2. Please file an associated tracking issue if this pull request is
non-trivial and requires context for our team to understand. All
features and most bug fixes should have
an associated issue with a design discussed and decided upon. Small bug
   fixes and documentation improvements don't need issues.
3. New features and bug fixes must have tests. Documentation may need to
be updated. If you're unsure what to update, send the PR, and we'll
discuss
   in review.
4. Note that PRs updating dependencies and new Go versions are not
accepted.
   Please file an issue instead.
5. A changelog entry is required for user facing issues.
-->

**What type of PR is this?**

Feature

**What does this PR do? Why is it needed?**

- upgrading go ethereum to 1.16.7
- enabling fulu e2e 
- added new e2e field params and build option
- removes github.com/MariusVanDerWijden/FuzzyVM
v0.0.0-20240516070431-7828990cad7d and
github.com/MariusVanDerWijden/tx-fuzz v1.4.0

what changed
- e2e config on slots per epoch increased to match minimum 6->8 a 33%
increase in run time ( this is needed because field params only have
minimum presets and proposer look ahead feature uses it, so if it
doesn't match it fails)
- reduce presubmit epochs from 18 -> 10 and only run for electra -> fulu
- moves bellatrix -> fulu post merge test to post submit

**Which issues(s) does this PR fix?**

Fixes #

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description to this PR with sufficient context for
reviewers to understand this PR.

---------

Co-authored-by: Preston Van Loon <preston@pvl.dev>
Co-authored-by: Manu NALEPA <enalepa@offchainlabs.com>
Co-authored-by: Preston Van Loon <pvanloon@offchainlabs.com>
Co-authored-by: Radosław Kapka <rkapka@wp.pl>
Co-authored-by: Potuz <potuz@prysmaticlabs.com>
2026-01-13 02:29:51 +00:00
Potuz
37c5178fa8 Migrate to cold with state diffs (#16049)
This PR adds the logic to migrate to cold when the database has the
hdiff feature. The main difference is that the boundary states have to
have the right slot therefore they need to be advanced and aren't
necessarily the post-state of a given beacon block root.

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 18:56:18 +00:00
Potuz
13f8e7b47f Hashtree from source (#16216)
A try to #14524 without using the syso files

---------

Co-authored-by: Preston Van Loon <pvanloon@offchainlabs.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 17:25:14 +00:00
Manu NALEPA
124eadd56e Earliest available slot at node start: Use the justified checkpoint. (#16230)
**What type of PR is this?**
Bug fix

**What does this PR do? Why is it needed?**
**Before this PR:**
When starting the node with the `--[semi-]supernode` flag with an
already existing DB, the new value of the earliest available slot was
set to the slot of the latest finalized checkpoint.

==> Between the latest finalized checkpoint and the slot where Prysm
starts to actually sync (the justified checkpoint) after the reboot,
Prysm advertises a higher `cgc` than it should.

**With this PR:**
If, at node start, the node needs to increase its `cgc`, then it uses
the latest justified checkpoint (+ 1) for the new `eas`.

**Which issues(s) does this PR fix?**
- https://github.com/OffchainLabs/prysm/issues/16066

**Example of a test case:**
1. Start the node with an empty DB, without any validator connected.
```
[2026-01-09 13:38:21.77] DEBUG db: Custody info earliestAvailableSlot=2145952 groupCount=4
```

2. Try: 
```
curl http://localhost:3500/eth/v1/beacon/blobs/2145952 | jq
{
  "message": "Not found: the node does not custody enough data columns to reconstruct blobs - please start the beacon node with the `--semi-supernode` flag to ensure this call to succeed",
  "code": 404
}
```

==> This is expected, since `cgc=4 < 64`

3. After a few epochs, add a few validators (< 64):
```
[2026-01-09 13:43:21.77] DEBUG db: Custody info earliestAvailableSlot=2146066 groupCount=10
```

4. Try:
```
curl http://localhost:3500/eth/v1/beacon/blobs/2146066 | jq
{
  "message": "Not found: the node does not custody enough data columns to reconstruct blobs - please start the beacon node with the `--semi-supernode` flag to ensure this call to succeed",
  "code": 404
}
```

==> This is expected, since `cgc=10 < 64`


5. After a few epochs, restart the node:
```
[2026-01-09 13:46:44.09] DEBUG db: Custody info earliestAvailableSlot=2146066 groupCount=10
```

==> OK (No change)

6. Restart the node with the `--semi-supernode` flag.
```
[2026-01-09 13:49:26.14] DEBUG db: Custody info earliestAvailableSlot=2146049 groupCount=64
```

The `eas` goes backward which is expected, since the node restarts
syncing from the latest justified checkpoint, which in this case is
lower than the slot where we added validators during step `3.`.

Try:
```
curl http://localhost:3500/eth/v1/beacon/blobs/2146049 | jq
==> OK
```




The whole `eas/cgc` advertisement management should probably be
re-thinked, for example by using what the node actually has in its DB to
decide what `eas/cgc` should be advertised.
(With a particular attention to the full node case until
https://github.com/OffchainLabs/prysm/issues/16065 is fixed.)

However, this PR fixes the linked issue, so it's a good fix until a
deeper redesign is done.

**Acknowledgements**
- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-01-12 14:25:12 +00:00
Potuz
76420d9428 Update Spectests to v1.7.0.alpha-1 (#16246) 2026-01-11 12:54:01 +00:00
Jun Song
21366e11ca feat: generalize proof generation for beacon state fields (#15443)
**What type of PR is this?**

Feature

**What does this PR do? Why is it needed?**

**Which issues(s) does this PR fix?**

This PR is the part of EPF6 project: [Merkle Proofs of
Everything](https://github.com/eth-protocol-fellows/cohort-six/blob/master/projects/project-ideas.md#prysm-merkle-proofs-of-everything).

**Other notes for review**

You can see some rationale and future TODOs in [my HackMD
post](https://hackmd.io/@junsong/SJGze5cNxg). The main next task is
following: (excerpted from the post)

> More generally speaking, we should find a way to prove for an item
with generalized index more than 128.

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description to this PR with sufficient context for
reviewers to understand this PR.

---------

Co-authored-by: Radosław Kapka <rkapka@wp.pl>
2026-01-10 13:15:25 +00:00
Potuz
7ed4d496dd Add feature flag to verify signatures before proposing (#15920)
Adds a feature flag `--enable-proposer-preprocessing` to verify
individual signatures in the block right before proposing to fallback to
empty fields in case of failure.

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 18:35:40 +00:00
Potuz
158c09ca8c Add --low-valcount-sweep feature flag for withdrawal sweep bound (#16231)
Gate the withdrawal sweep optimization (using min of validator count and
MaxValidatorsPerWithdrawalsSweep) behind a hidden feature flag that
defaults to false. Enable the flag for spectests to match consensus
spec.

The backported changes were from
[4788](https://github.com/ethereum/consensus-specs/pull/4788)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 16:14:03 +00:00
Bastin
17245f4fac Ephemeral debug logfile (#16108)
**What type of PR is this?**
Feature

**What does this PR do? Why is it needed?**
This PR introduces an ephemeral log file that captures debug logs for 24
hours.

- it captures debug logs regardless of the user provided (or
non-provided) `--verbosity` flag.
- it allows a maximum of 250MB for each log file. 
- it keeps 1 backup logfile in case of size-based rotations. (as opposed
to time-based)
- this is enabled by default for beacon and validator nodes.
- the log files live in `datadir/logs/` directory under the names of
`beacon-chain.log` and `validator.log`. backups have a timestamp in
their name as well.
- the feature can be disabled using the `--disable-ephemeral-log-file`
flag.
2026-01-08 14:16:22 +00:00
Bastin
53b0a574ab Set logging verbosity per writer hook instead of globally (#16106)
**What type of PR is this?**
Feature

**What does this PR do? Why is it needed?**
This PR sets the logging verbosity level per writer hook (per output:
terminal, log file, etc) rather than setting a global logrus level which
limits customizing each output.

it set the terminal and log file output to be the same as the user
provided `--verbosity` flag. so nothing changes in reality.

it also introduces a `SetLoggingLevel()` to be used instead of
`logrus.SetLeveL()` in order for us to be able to set a different
baseline level later on if needed. (my next PR will use this).

I'm only making this change in the `beacon-chain` and `validator` apps,
skipping tools like `bootnode` and `client-stats`.
2026-01-08 12:19:16 +00:00
terence
c96d188468 gloas: add builders registry and update state fields (#16164)
This pr implements the Gloas builder registry and related beacon state
fields per the spec, including proto/SSZ updates and state-native wiring
for builders, payload availability, pending payments/withdrawals, and
expected withdrawals. This aligns BeaconState with the Gloas container
changes and adds supporting hashing/copy helpers.

Spec ref:
https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md
2026-01-07 21:51:56 +00:00
1144 changed files with 60820 additions and 12573 deletions

View File

@@ -1,29 +1,39 @@
version: v1.7.0-alpha.0
version: v1.7.0-alpha.2
style: full
specrefs:
search_root: ..
search_root: .
auto_standardize_names: true
auto_add_missing_entries: true
require_exceptions_have_fork: true
files:
- configs.yml
- constants.yml
- containers.yml
- dataclasses.yml
- functions.yml
- presets.yml
- specrefs/configs.yml
- specrefs/constants.yml
- specrefs/containers.yml
- specrefs/dataclasses.yml
- specrefs/functions.yml
- specrefs/presets.yml
exceptions:
presets:
# Not implemented
- CELLS_PER_EXT_BLOB#fulu
- UPDATE_TIMEOUT#altair
# Not implemented: gloas (future fork)
# gloas
- BUILDER_PENDING_WITHDRAWALS_LIMIT#gloas
- MAX_PAYLOAD_ATTESTATIONS#gloas
- PTC_SIZE#gloas
constants:
# Constants in the KZG library
# phase0
- BASIS_POINTS#phase0
- ENDIANNESS#phase0
- MAX_CONCURRENT_REQUESTS#phase0
- UINT64_MAX#phase0
- UINT64_MAX_SQRT#phase0
# altair
- PARTICIPATION_FLAG_WEIGHTS#altair
# bellatrix
- SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY#bellatrix
# deneb
- BLS_MODULUS#deneb
- BYTES_PER_COMMITMENT#deneb
- BYTES_PER_FIELD_ELEMENT#deneb
@@ -37,18 +47,9 @@ exceptions:
- PRIMITIVE_ROOT_OF_UNITY#deneb
- RANDOM_CHALLENGE_KZG_BATCH_DOMAIN#deneb
- RANDOM_CHALLENGE_KZG_CELL_BATCH_DOMAIN#fulu
# Not implemented
- BASIS_POINTS#phase0
- ENDIANNESS#phase0
- MAX_CONCURRENT_REQUESTS#phase0
- PARTICIPATION_FLAG_WEIGHTS#altair
- SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY#bellatrix
# fulu
- UINT256_MAX#fulu
- UINT64_MAX#phase0
- UINT64_MAX_SQRT#phase0
# Not implemented: gloas (future fork)
# gloas
- BUILDER_PAYMENT_THRESHOLD_DENOMINATOR#gloas
- BUILDER_PAYMENT_THRESHOLD_NUMERATOR#gloas
- BUILDER_WITHDRAWAL_PREFIX#gloas
@@ -65,67 +66,60 @@ exceptions:
- PTC_TIMELINESS_INDEX#gloas
configs:
# Not implemented (placeholders)
- AGGREGATE_DUE_BPS#phase0
- ATTESTATION_DUE_BPS#phase0
- CONTRIBUTION_DUE_BPS#altair
- PROPOSER_REORG_CUTOFF_BPS#phase0
- SLOT_DURATION_MS#phase0
- SYNC_MESSAGE_DUE_BPS#altair
# Not implemented: gloas (future fork)
# gloas
- AGGREGATE_DUE_BPS_GLOAS#gloas
- ATTESTATION_DUE_BPS_GLOAS#gloas
- CONTRIBUTION_DUE_BPS_GLOAS#gloas
- GLOAS_FORK_EPOCH#gloas
- GLOAS_FORK_VERSION#gloas
- MAX_REQUEST_PAYLOADS#gloas
- PAYLOAD_ATTESTATION_DUE_BPS#gloas
- SYNC_MESSAGE_DUE_BPS_GLOAS#gloas
- MIN_BUILDER_WITHDRAWABILITY_DELAY#gloas
ssz_objects:
# Not implemented
# phase0
- Eth1Block#phase0
- MatrixEntry#fulu
# Not implemented: capella
# capella
- LightClientBootstrap#capella
- LightClientFinalityUpdate#capella
- LightClientOptimisticUpdate#capella
- LightClientUpdate#capella
# Not implemented: gloas (future fork)
# fulu
- MatrixEntry#fulu
# gloas
- BeaconBlockBody#gloas
- BeaconState#gloas
- Builder#gloas
- BuilderPendingPayment#gloas
- BuilderPendingWithdrawal#gloas
- DataColumnSidecar#gloas
- ExecutionPayloadEnvelope#gloas
- ExecutionPayloadBid#gloas
- ExecutionPayloadEnvelope#gloas
- ForkChoiceNode#gloas
- IndexedPayloadAttestation#gloas
- PayloadAttestation#gloas
- PayloadAttestationData#gloas
- PayloadAttestationMessage#gloas
- SignedExecutionPayloadEnvelope#gloas
- SignedExecutionPayloadBid#gloas
- Builder#gloas
- ProposerPreferences#gloas
- SignedExecutionPayloadBid#gloas
- SignedExecutionPayloadEnvelope#gloas
- SignedProposerPreferences#gloas
dataclasses:
# Not implemented
- BlobParameters#fulu
# phase0
- LatestMessage#phase0
- LightClientStore#altair
- OptimisticStore#bellatrix
- Store#phase0
# Not implemented: capella
# altair
- LightClientStore#altair
# bellatrix
- OptimisticStore#bellatrix
# capella
- ExpectedWithdrawals#capella
- LightClientStore#capella
# Not implemented: gloas (future fork)
# electra
- ExpectedWithdrawals#electra
# fulu
- BlobParameters#fulu
# gloas
- ExpectedWithdrawals#gloas
- LatestMessage#gloas
- Store#gloas
@@ -149,7 +143,6 @@ exceptions:
- g1_lincomb#deneb
- hash_to_bls_field#deneb
- is_power_of_two#deneb
- multi_exp#deneb
- reverse_bits#deneb
- validate_kzg_g1#deneb
- verify_blob_kzg_proof#deneb
@@ -184,7 +177,12 @@ exceptions:
- verify_cell_kzg_proof_batch#fulu
- verify_cell_kzg_proof_batch_impl#fulu
# Not implemented: phase0
# phase0
- update_proposer_boost_root#phase0
- is_proposer_equivocation#phase0
- record_block_timeliness#phase0
- compute_proposer_score#phase0
- get_attestation_score#phase0
- calculate_committee_fraction#phase0
- compute_fork_version#phase0
- compute_pulled_up_tip#phase0
@@ -230,8 +228,7 @@ exceptions:
- validate_on_attestation#phase0
- validate_target_epoch_against_current_time#phase0
- xor#phase0
# Not implemented: altair
# altair
- compute_merkle_proof#altair
- compute_sync_committee_period_at_slot#altair
- get_contribution_and_proof#altair
@@ -253,27 +250,29 @@ exceptions:
- process_sync_committee_contributions#altair
- set_or_append_list#altair
- validate_light_client_update#altair
# Not implemented: bellatrix
# bellatrix
- get_execution_payload#bellatrix
- is_merge_transition_block#bellatrix
- is_optimistic_candidate_block#bellatrix
- latest_verified_ancestor#bellatrix
- prepare_execution_payload#bellatrix
# Not implemented: capella
# capella
- apply_withdrawals#capella
- get_balance_after_withdrawals#capella
- get_lc_execution_root#capella
- get_validators_sweep_withdrawals#capella
- is_valid_light_client_header#capella
- prepare_execution_payload#capella
- process_epoch#capella
- update_next_withdrawal_index#capella
- update_next_withdrawal_validator_index#capella
- upgrade_lc_bootstrap_to_capella#capella
- upgrade_lc_finality_update_to_capella#capella
- upgrade_lc_header_to_capella#capella
- upgrade_lc_optimistic_update_to_capella#capella
- upgrade_lc_store_to_capella#capella
- upgrade_lc_update_to_capella#capella
# Not implemented: deneb
# deneb
- get_lc_execution_root#deneb
- is_valid_light_client_header#deneb
- prepare_execution_payload#deneb
@@ -283,33 +282,34 @@ exceptions:
- upgrade_lc_optimistic_update_to_deneb#deneb
- upgrade_lc_store_to_deneb#deneb
- upgrade_lc_update_to_deneb#deneb
# Not implemented: electra
# electra
- compute_weak_subjectivity_period#electra
- current_sync_committee_gindex_at_slot#electra
- finalized_root_gindex_at_slot#electra
- get_eth1_vote#electra
- get_lc_execution_root#electra
- get_pending_partial_withdrawals#electra
- get_validators_sweep_withdrawals#electra
- is_compounding_withdrawal_credential#electra
- is_eligible_for_partial_withdrawals#electra
- is_within_weak_subjectivity_period#electra
- next_sync_committee_gindex_at_slot#electra
- normalize_merkle_branch#electra
- prepare_execution_payload#electra
- update_pending_partial_withdrawals#electra
- upgrade_lc_bootstrap_to_electra#electra
- upgrade_lc_finality_update_to_electra#electra
- upgrade_lc_header_to_electra#electra
- upgrade_lc_optimistic_update_to_electra#electra
- upgrade_lc_store_to_electra#electra
- upgrade_lc_update_to_electra#electra
# Not implemented: fulu
# fulu
- compute_matrix#fulu
- get_blob_parameters#fulu
- get_data_column_sidecars_from_block#fulu
- get_data_column_sidecars_from_column_sidecar#fulu
- recover_matrix#fulu
# Not implemented: gloas (future fork)
# gloas
- compute_balance_weighted_acceptance#gloas
- compute_balance_weighted_selection#gloas
- compute_fork_version#gloas
@@ -377,53 +377,41 @@ exceptions:
- verify_execution_payload_bid_signature#gloas
- add_builder_to_registry#gloas
- apply_deposit_for_builder#gloas
- apply_withdrawals#capella
- apply_withdrawals#gloas
- can_builder_cover_bid#gloas
- compute_proposer_score#phase0
- convert_builder_index_to_validator_index#gloas
- convert_validator_index_to_builder_index#gloas
- get_attestation_score#gloas
- get_attestation_score#phase0
- get_balance_after_withdrawals#capella
- get_builder_from_deposit#gloas
- get_builder_withdrawals#gloas
- get_builders_sweep_withdrawals#gloas
- get_index_for_new_builder#gloas
- get_pending_balance_to_withdraw_for_builder#gloas
- get_pending_partial_withdrawals#electra
- get_proposer_preferences_signature#gloas
- get_upcoming_proposal_slots#gloas
- get_validators_sweep_withdrawals#capella
- get_validators_sweep_withdrawals#electra
- initiate_builder_exit#gloas
- is_active_builder#gloas
- is_builder_index#gloas
- is_data_available#gloas
- is_eligible_for_partial_withdrawals#electra
- is_head_late#gloas
- is_head_weak#gloas
- is_parent_strong#gloas
- is_proposer_equivocation#phase0
- is_valid_proposal_slot#gloas
- onboard_builders_from_pending_deposits#gloas
- process_deposit_request#gloas
- process_voluntary_exit#gloas
- record_block_timeliness#gloas
- record_block_timeliness#phase0
- verify_data_column_sidecar_kzg_proofs#gloas
- should_apply_proposer_boost#gloas
- update_builder_pending_withdrawals#gloas
- update_next_withdrawal_builder_index#gloas
- update_next_withdrawal_index#capella
- update_next_withdrawal_validator_index#capella
- update_payload_expected_withdrawals#gloas
- update_pending_partial_withdrawals#electra
- update_proposer_boost_root#gloas
- update_proposer_boost_root#phase0
presets:
- CELLS_PER_EXT_BLOB#fulu
# gloas
- BUILDER_PENDING_WITHDRAWALS_LIMIT#gloas
- BUILDER_REGISTRY_LIMIT#gloas
- MAX_BUILDERS_PER_WITHDRAWALS_SWEEP#gloas
- MAX_PAYLOAD_ATTESTATIONS#gloas
- PTC_SIZE#gloas
- UPDATE_TIMEOUT#altair

View File

@@ -12,11 +12,11 @@ jobs:
- name: Check version consistency
run: |
WORKSPACE_VERSION=$(grep 'consensus_spec_version = ' WORKSPACE | sed 's/.*"\(.*\)"/\1/')
ETHSPECIFY_VERSION=$(grep '^version:' specrefs/.ethspecify.yml | sed 's/version: //')
ETHSPECIFY_VERSION=$(grep '^version:' .ethspecify.yml | sed 's/version: //')
if [ "$WORKSPACE_VERSION" != "$ETHSPECIFY_VERSION" ]; then
echo "Version mismatch between WORKSPACE and ethspecify"
echo " WORKSPACE: $WORKSPACE_VERSION"
echo " specrefs/.ethspecify.yml: $ETHSPECIFY_VERSION"
echo " .ethspecify.yml: $ETHSPECIFY_VERSION"
exit 1
else
echo "Versions match: $WORKSPACE_VERSION"
@@ -26,7 +26,7 @@ jobs:
run: python3 -mpip install ethspecify
- name: Update spec references
run: ethspecify process --path=specrefs
run: ethspecify
- name: Check for differences
run: |
@@ -40,4 +40,4 @@ jobs:
fi
- name: Check spec references
run: ethspecify check --path=specrefs
run: ethspecify check

View File

@@ -1,6 +1,6 @@
name: Protobuf Format
on:
on:
push:
branches: [ '*' ]
pull_request:
@@ -12,10 +12,14 @@ jobs:
clang-format-checking:
runs-on: ubuntu-4
steps:
- uses: actions/checkout@v2
# Is this step failing for you?
- uses: actions/checkout@v6
# Is this step failing for you?
# Run: clang-format -i proto/**/*.proto
# See: https://clang.llvm.org/docs/ClangFormat.html
- uses: RafikFarhad/clang-format-github-action@v3
with:
sources: "proto/**/*.proto"
- name: Install clang-format
run: |
sudo apt-get update -qq
sudo apt-get install -y clang-format
- name: Check protobuf formatting
run: |
clang-format --style=LLVM --dry-run --Werror proto/**/*.proto

View File

@@ -2,7 +2,7 @@ name: Go
on:
push:
branches: [ master ]
branches: [ master, develop ]
pull_request:
branches: [ '*' ]
merge_group:

67
.github/workflows/sbom-export.yaml vendored Normal file
View File

@@ -0,0 +1,67 @@
name: SBOM Export & Centralize
on:
push:
branches: [ "develop" ]
schedule:
- cron: '50 21 * * 2'
permissions:
contents: read
jobs:
generate-and-upload:
runs-on: ubuntu-latest
steps:
- name: Checkout Source Code
uses: actions/checkout@v6
- name: Check for recent changes
id: check
run: |
if [ -z "$(git log --since='7 days ago' --oneline | head -1)" ]; then
echo "No commits in the last 7 days, skipping SBOM generation."
echo "skip=true" >> "$GITHUB_OUTPUT"
fi
- name: Generate CycloneDX SBOM via cdxgen
if: steps.check.outputs.skip != 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
docker run --rm \
--user "$(id -u):$(id -g)" \
-v /tmp:/tmp \
-v "${{ github.workspace }}:/app:rw" \
-e FETCH_LICENSE=true \
-e GITHUB_TOKEN \
ghcr.io/cdxgen/cdxgen:v12.1.1 \
-r /app \
-o /app/sbom.cdx.json \
--no-install-deps \
--spec-version 1.6
if [ ! -s sbom.cdx.json ]; then
echo "::error::cdxgen SBOM generation failed or returned empty."
exit 1
fi
echo "SBOM generated successfully:"
ls -lh sbom.cdx.json
- name: Upload SBOM to Dependency Track
if: steps.check.outputs.skip != 'true'
env:
DT_API_KEY: ${{ secrets.DEPENDENCY_TRACK_API_KEY }}
DT_URL: ${{ secrets.DEPENDENCY_TRACK_URL }}
run: |
REPO_NAME=${GITHUB_REPOSITORY##*/}
curl -sf -X POST "${DT_URL}/api/v1/bom" \
-H "X-Api-Key: ${DT_API_KEY}" \
-F "autoCreate=true" \
-F "projectName=${REPO_NAME}" \
-F "projectVersion=${{ github.ref_name }}" \
-F "bom=@sbom.cdx.json"
echo "SBOM uploaded to Dependency Track for ${REPO_NAME}@${{ github.ref_name }}"

3
.gitignore vendored
View File

@@ -44,6 +44,3 @@ tmp
# spectest coverage reports
report.txt
# execution client data
execution/

View File

@@ -33,9 +33,8 @@ formatters:
generated: lax
paths:
- validator/web/site_data.go
- .*_test.go
- proto
- tools/analyzers
- third_party$
- builtin$
- examples$
- examples$

View File

@@ -1,5 +1,4 @@
load("@bazel_gazelle//:def.bzl", "gazelle")
load("@com_github_atlassian_bazel_tools//gometalinter:def.bzl", "gometalinter")
load("@com_github_atlassian_bazel_tools//goimports:def.bzl", "goimports")
load("@io_kubernetes_build//defs:run_in_workspace.bzl", "workspace_binary")
load("@io_bazel_rules_go//go:def.bzl", "nogo")
@@ -55,15 +54,6 @@ alias(
visibility = ["//visibility:public"],
)
gometalinter(
name = "gometalinter",
config = "//:.gometalinter.json",
paths = [
"./...",
],
prefix = prefix,
)
goimports(
name = "goimports",
display_diffs = True,

View File

@@ -72,7 +72,7 @@ Do NOT add new `go_repository` to the WORKSPACE file. All dependencies should li
To enable conditional compilation and custom configuration for tests (where compiled code has more
debug info, while not being completely optimized), we rely on Go's build tags/constraints mechanism
(see official docs on [build constraints](https://golang.org/pkg/go/build/#hdr-Build_Constraints)).
(see official docs on [build constraints](https://pkg.go.dev/go/build#hdr-Build_Constraints)).
Therefore, whenever using `go test`, do not forget to pass in extra build tag, eg:
```bash

View File

@@ -9,7 +9,7 @@ This README details how to setup Prysm for interop testing for usage with other
## Installation & Setup
1. Install [Bazel](https://docs.bazel.build/versions/master/install.html) **(Recommended)**
1. Install [Bazel](https://bazel.build/install) **(Recommended)**
2. `git clone https://github.com/OffchainLabs/prysm && cd prysm`
3. `bazel build //cmd/...`

View File

@@ -15,7 +15,7 @@
## 📖 Overview
This is the core repository for Prysm, a [Golang](https://golang.org/) implementation of the [Ethereum Consensus](https://ethereum.org/en/developers/docs/consensus-mechanisms/#proof-of-stake) [specification](https://github.com/ethereum/consensus-specs), developed by [Offchain Labs](https://www.offchainlabs.com).
This is the core repository for Prysm, a [Golang](https://go.dev/) implementation of the [Ethereum Consensus](https://ethereum.org/en/developers/docs/consensus-mechanisms/#proof-of-stake) [specification](https://github.com/ethereum/consensus-specs), developed by [Offchain Labs](https://www.offchainlabs.com).
See the [Changelog](https://github.com/OffchainLabs/prysm/releases) for details of the latest releases and upcoming breaking changes.
@@ -23,7 +23,7 @@ See the [Changelog](https://github.com/OffchainLabs/prysm/releases) for details
## 🚀 Getting Started
A detailed set of installation and usage instructions as well as breakdowns of each individual component are available in the **[official documentation portal](https://docs.prylabs.network)**.
A detailed set of installation and usage instructions as well as breakdowns of each individual component are available in the **[official documentation portal](https://prysm.offchainlabs.com/docs/)**.
💬 **Need help?** Join our **[Discord Community](https://discord.gg/prysm)** for support.
@@ -51,7 +51,7 @@ Prysm maintains two permanent branches:
### 🛠 Contribution Guide
Want to get involved? Check out our **[Contribution Guide](https://docs.prylabs.network/docs/contribute/contribution-guidelines/)** to learn more!
Want to get involved? Check out our **[Contribution Guide](https://prysm.offchainlabs.com/docs/contribute/contribution-guidelines/)** to learn more!
---

View File

@@ -273,16 +273,16 @@ filegroup(
url = "https://github.com/ethereum/EIPs/archive/5480440fe51742ed23342b68cf106cefd427e39d.tar.gz",
)
consensus_spec_version = "v1.7.0-alpha.0"
consensus_spec_version = "v1.7.0-alpha.2"
load("@prysm//tools:download_spectests.bzl", "consensus_spec_tests")
consensus_spec_tests(
name = "consensus_spec_tests",
flavors = {
"general": "sha256-b+rJOuVqq+Dy53quPcNYcQwPFoMU7Wp7tdUVe7n0g8w=",
"minimal": "sha256-qxRIxtjPxVsVCY90WsBJKhk0027XDSmhjnRvRN14V1c=",
"mainnet": "sha256-NsuOQG3LzeiEE1TrWuvQ6vu6BboHv7h7f/RTS0pWkCs=",
"general": "sha256-iGQsGZ1cHah+2CSod9jC3kN8Ku4n6KO0hIwfINrn/po=",
"minimal": "sha256-TgcYt8N8sXSttdHTGvOa+exUZ1zn1UzlAMz0V7i37xc=",
"mainnet": "sha256-LnXyiLoJtrvEvbqLDSAAqpLMdN/lXv92SAgYG8fNjCs=",
},
version = consensus_spec_version,
)
@@ -298,7 +298,7 @@ filegroup(
visibility = ["//visibility:public"],
)
""",
integrity = "sha256-hwNdUBgdBrkk6pWIpNYbzbwswUuOu6AMD2exN8uv+QQ=",
integrity = "sha256-Y/67Dg393PksZj5rTFNLntiJ6hNdB7Rxbu5gZE2gebY=",
strip_prefix = "consensus-specs-" + consensus_spec_version[1:],
url = "https://github.com/ethereum/consensus-specs/archive/refs/tags/%s.tar.gz" % consensus_spec_version,
)
@@ -423,10 +423,6 @@ load("@prysm//testing/endtoend:deps.bzl", "e2e_deps")
e2e_deps()
load("@com_github_atlassian_bazel_tools//gometalinter:deps.bzl", "gometalinter_dependencies")
gometalinter_dependencies()
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
gazelle_dependencies(go_sdk = "go_sdk")

19
api/fallback/BUILD.bazel Normal file
View File

@@ -0,0 +1,19 @@
load("@prysm//tools/go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"fallback.go",
"log.go",
],
importpath = "github.com/OffchainLabs/prysm/v7/api/fallback",
visibility = ["//visibility:public"],
deps = ["@com_github_sirupsen_logrus//:go_default_library"],
)
go_test(
name = "go_default_test",
srcs = ["fallback_test.go"],
embed = [":go_default_library"],
deps = ["//testing/assert:go_default_library"],
)

66
api/fallback/fallback.go Normal file
View File

@@ -0,0 +1,66 @@
package fallback
import (
"context"
"github.com/sirupsen/logrus"
)
// HostProvider is the subset of connection-provider methods that EnsureReady
// needs. Both grpc.GrpcConnectionProvider and rest.RestConnectionProvider
// satisfy this interface.
type HostProvider interface {
Hosts() []string
CurrentHost() string
SwitchHost(index int) error
}
// ReadyChecker can report whether the current endpoint is ready.
// iface.NodeClient satisfies this implicitly.
type ReadyChecker interface {
IsReady(ctx context.Context) bool
}
// EnsureReady iterates through the configured hosts and returns true as soon as
// one responds as ready. It starts from the provider's current host and wraps
// around using modular arithmetic, performing failover when a host is not ready.
func EnsureReady(ctx context.Context, provider HostProvider, checker ReadyChecker) bool {
hosts := provider.Hosts()
numHosts := len(hosts)
startingHost := provider.CurrentHost()
var attemptedHosts []string
// Find current index
currentIdx := 0
for i, h := range hosts {
if h == startingHost {
currentIdx = i
break
}
}
for i := range numHosts {
if checker.IsReady(ctx) {
if len(attemptedHosts) > 0 {
log.WithFields(logrus.Fields{
"previous": startingHost,
"current": provider.CurrentHost(),
"tried": attemptedHosts,
}).Info("Switched to responsive beacon node")
}
return true
}
attemptedHosts = append(attemptedHosts, provider.CurrentHost())
// Try next host if not the last iteration
if i < numHosts-1 {
nextIdx := (currentIdx + i + 1) % numHosts
if err := provider.SwitchHost(nextIdx); err != nil {
log.WithError(err).Error("Failed to switch host")
}
}
}
log.WithField("tried", attemptedHosts).Warn("No responsive beacon node found")
return false
}

View File

@@ -0,0 +1,94 @@
package fallback
import (
"context"
"testing"
"github.com/OffchainLabs/prysm/v7/testing/assert"
)
// mockHostProvider is a minimal HostProvider for unit tests.
type mockHostProvider struct {
hosts []string
hostIndex int
}
func (m *mockHostProvider) Hosts() []string { return m.hosts }
func (m *mockHostProvider) CurrentHost() string {
return m.hosts[m.hostIndex%len(m.hosts)]
}
func (m *mockHostProvider) SwitchHost(index int) error { m.hostIndex = index; return nil }
// mockReadyChecker records per-call IsReady results in sequence.
type mockReadyChecker struct {
results []bool
idx int
}
func (m *mockReadyChecker) IsReady(_ context.Context) bool {
if m.idx >= len(m.results) {
return false
}
r := m.results[m.idx]
m.idx++
return r
}
func TestEnsureReady_SingleHostReady(t *testing.T) {
provider := &mockHostProvider{hosts: []string{"http://host1:3500"}, hostIndex: 0}
checker := &mockReadyChecker{results: []bool{true}}
assert.Equal(t, true, EnsureReady(t.Context(), provider, checker))
assert.Equal(t, 0, provider.hostIndex)
}
func TestEnsureReady_SingleHostNotReady(t *testing.T) {
provider := &mockHostProvider{hosts: []string{"http://host1:3500"}, hostIndex: 0}
checker := &mockReadyChecker{results: []bool{false}}
assert.Equal(t, false, EnsureReady(t.Context(), provider, checker))
}
func TestEnsureReady_SingleHostError(t *testing.T) {
provider := &mockHostProvider{hosts: []string{"http://host1:3500"}, hostIndex: 0}
checker := &mockReadyChecker{results: []bool{false}}
assert.Equal(t, false, EnsureReady(t.Context(), provider, checker))
}
func TestEnsureReady_MultipleHostsFirstReady(t *testing.T) {
provider := &mockHostProvider{
hosts: []string{"http://host1:3500", "http://host2:3500"},
hostIndex: 0,
}
checker := &mockReadyChecker{results: []bool{true}}
assert.Equal(t, true, EnsureReady(t.Context(), provider, checker))
assert.Equal(t, 0, provider.hostIndex)
}
func TestEnsureReady_MultipleHostsFailoverToSecond(t *testing.T) {
provider := &mockHostProvider{
hosts: []string{"http://host1:3500", "http://host2:3500"},
hostIndex: 0,
}
checker := &mockReadyChecker{results: []bool{false, true}}
assert.Equal(t, true, EnsureReady(t.Context(), provider, checker))
assert.Equal(t, 1, provider.hostIndex)
}
func TestEnsureReady_MultipleHostsNoneReady(t *testing.T) {
provider := &mockHostProvider{
hosts: []string{"http://host1:3500", "http://host2:3500", "http://host3:3500"},
hostIndex: 0,
}
checker := &mockReadyChecker{results: []bool{false, false, false}}
assert.Equal(t, false, EnsureReady(t.Context(), provider, checker))
}
func TestEnsureReady_WrapAroundFromNonZeroIndex(t *testing.T) {
provider := &mockHostProvider{
hosts: []string{"http://host0:3500", "http://host1:3500", "http://host2:3500"},
hostIndex: 1,
}
// host1 (start) fails, host2 fails, host0 succeeds
checker := &mockReadyChecker{results: []bool{false, false, true}}
assert.Equal(t, true, EnsureReady(t.Context(), provider, checker))
assert.Equal(t, 0, provider.hostIndex)
}

9
api/fallback/log.go Normal file
View File

@@ -0,0 +1,9 @@
// Code generated by hack/gen-logs.sh; DO NOT EDIT.
// This file is created and regenerated automatically. Anything added here might get removed.
package fallback
import "github.com/sirupsen/logrus"
// The prefix for logs from this package will be the text after the last slash in the package path.
// If you wish to change this, you should add your desired name in the runtime/logging/logrus-prefixed-formatter/prefix-replacement.go file.
var log = logrus.WithField("package", "api/fallback")

View File

@@ -3,13 +3,16 @@ load("@prysm//tools/go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"grpc_connection_provider.go",
"grpcutils.go",
"log.go",
"mock_grpc_provider.go",
"parameters.go",
],
importpath = "github.com/OffchainLabs/prysm/v7/api/grpc",
visibility = ["//visibility:public"],
deps = [
"@com_github_pkg_errors//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_google_grpc//metadata:go_default_library",
@@ -18,12 +21,17 @@ go_library(
go_test(
name = "go_default_test",
srcs = ["grpcutils_test.go"],
srcs = [
"grpc_connection_provider_test.go",
"grpcutils_test.go",
],
embed = [":go_default_library"],
deps = [
"//testing/assert:go_default_library",
"//testing/require:go_default_library",
"@com_github_sirupsen_logrus//hooks/test:go_default_library",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_google_grpc//credentials/insecure:go_default_library",
"@org_golang_google_grpc//metadata:go_default_library",
],
)

View File

@@ -0,0 +1,186 @@
package grpc
import (
"context"
"strings"
"sync"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"google.golang.org/grpc"
)
// GrpcConnectionProvider manages gRPC connections for failover support.
// It allows switching between different beacon node endpoints when the current one becomes unavailable.
// Only one connection is maintained at a time - when switching hosts, the old connection is closed.
type GrpcConnectionProvider interface {
// CurrentConn returns the currently active gRPC connection.
// The connection is created lazily on first call.
// Returns nil if the provider has been closed.
CurrentConn() *grpc.ClientConn
// CurrentHost returns the address of the currently active endpoint.
CurrentHost() string
// Hosts returns all configured endpoint addresses.
Hosts() []string
// SwitchHost switches to the endpoint at the given index.
// The new connection is created lazily on next CurrentConn() call.
SwitchHost(index int) error
// ConnectionCounter returns a monotonically increasing counter that increments
// each time SwitchHost changes the active endpoint. This allows consumers to
// detect connection changes even when the host string returns to a previous value
// (e.g., host0 → host1 → host0).
ConnectionCounter() uint64
// Close closes the current connection.
Close()
}
type grpcConnectionProvider struct {
// Immutable after construction - no lock needed for reads
endpoints []string
ctx context.Context
dialOpts []grpc.DialOption
// Current connection state (protected by mutex)
currentIndex uint64
conn *grpc.ClientConn
connCounter uint64
mu sync.Mutex
closed bool
}
// NewGrpcConnectionProvider creates a new connection provider that manages gRPC connections.
// The endpoint parameter can be a comma-separated list of addresses (e.g., "host1:4000,host2:4000").
// Only one connection is maintained at a time, created lazily on first use.
func NewGrpcConnectionProvider(
ctx context.Context,
endpoint string,
dialOpts []grpc.DialOption,
) (GrpcConnectionProvider, error) {
endpoints := parseEndpoints(endpoint)
if len(endpoints) == 0 {
return nil, errors.New("no gRPC endpoints provided")
}
log.WithFields(logrus.Fields{
"endpoints": endpoints,
"count": len(endpoints),
}).Info("Initialized gRPC connection provider")
return &grpcConnectionProvider{
endpoints: endpoints,
ctx: ctx,
dialOpts: dialOpts,
}, nil
}
// parseEndpoints splits a comma-separated endpoint string into individual endpoints.
func parseEndpoints(endpoint string) []string {
if endpoint == "" {
return nil
}
endpoints := make([]string, 0, 1)
for p := range strings.SplitSeq(endpoint, ",") {
if p = strings.TrimSpace(p); p != "" {
endpoints = append(endpoints, p)
}
}
return endpoints
}
func (p *grpcConnectionProvider) CurrentConn() *grpc.ClientConn {
p.mu.Lock()
defer p.mu.Unlock()
if p.closed {
return nil
}
// Return existing connection if available
if p.conn != nil {
return p.conn
}
// Create connection lazily
ep := p.endpoints[p.currentIndex]
conn, err := grpc.DialContext(p.ctx, ep, p.dialOpts...)
if err != nil {
log.WithError(err).WithField("endpoint", ep).Error("Failed to create gRPC connection")
return nil
}
p.conn = conn
log.WithField("endpoint", ep).Debug("Created gRPC connection")
return conn
}
func (p *grpcConnectionProvider) CurrentHost() string {
p.mu.Lock()
defer p.mu.Unlock()
return p.endpoints[p.currentIndex]
}
func (p *grpcConnectionProvider) Hosts() []string {
// Return a copy to maintain immutability
hosts := make([]string, len(p.endpoints))
copy(hosts, p.endpoints)
return hosts
}
func (p *grpcConnectionProvider) SwitchHost(index int) error {
if index < 0 || index >= len(p.endpoints) {
return errors.Errorf("invalid host index %d, must be between 0 and %d", index, len(p.endpoints)-1)
}
p.mu.Lock()
defer p.mu.Unlock()
if uint64(index) == p.currentIndex {
return nil // Already on this host
}
oldHost := p.endpoints[p.currentIndex]
oldConn := p.conn
p.conn = nil // Clear immediately - new connection created lazily
p.currentIndex = uint64(index)
p.connCounter++
// Close old connection asynchronously to avoid blocking the caller
if oldConn != nil {
go func() {
if err := oldConn.Close(); err != nil {
log.WithError(err).WithField("endpoint", oldHost).Debug("Failed to close previous connection")
}
}()
}
log.WithFields(logrus.Fields{
"previousHost": oldHost,
"newHost": p.endpoints[index],
}).Debug("Switched gRPC endpoint")
return nil
}
func (p *grpcConnectionProvider) ConnectionCounter() uint64 {
p.mu.Lock()
defer p.mu.Unlock()
return p.connCounter
}
func (p *grpcConnectionProvider) Close() {
p.mu.Lock()
defer p.mu.Unlock()
if p.closed {
return
}
p.closed = true
if p.conn != nil {
if err := p.conn.Close(); err != nil {
log.WithError(err).WithField("endpoint", p.endpoints[p.currentIndex]).Debug("Failed to close gRPC connection")
}
p.conn = nil
}
}

View File

@@ -0,0 +1,207 @@
package grpc
import (
"context"
"net"
"reflect"
"strings"
"testing"
"github.com/OffchainLabs/prysm/v7/testing/assert"
"github.com/OffchainLabs/prysm/v7/testing/require"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func TestParseEndpoints(t *testing.T) {
tests := []struct {
name string
input string
expected []string
}{
{"single endpoint", "localhost:4000", []string{"localhost:4000"}},
{"multiple endpoints", "host1:4000,host2:4000,host3:4000", []string{"host1:4000", "host2:4000", "host3:4000"}},
{"endpoints with spaces", "host1:4000, host2:4000 , host3:4000", []string{"host1:4000", "host2:4000", "host3:4000"}},
{"empty string", "", nil},
{"only commas", ",,,", []string{}},
{"trailing comma", "host1:4000,host2:4000,", []string{"host1:4000", "host2:4000"}},
{"leading comma", ",host1:4000,host2:4000", []string{"host1:4000", "host2:4000"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseEndpoints(tt.input)
if !reflect.DeepEqual(tt.expected, got) {
t.Errorf("parseEndpoints(%q) = %v, want %v", tt.input, got, tt.expected)
}
})
}
}
func TestNewGrpcConnectionProvider_Errors(t *testing.T) {
t.Run("no endpoints", func(t *testing.T) {
dialOpts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
_, err := NewGrpcConnectionProvider(context.Background(), "", dialOpts)
require.ErrorContains(t, "no gRPC endpoints provided", err)
})
}
func TestGrpcConnectionProvider_LazyConnection(t *testing.T) {
// Start only one server but configure provider with two endpoints
lis, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
server := grpc.NewServer()
go func() { _ = server.Serve(lis) }()
defer server.Stop()
validAddr := lis.Addr().String()
invalidAddr := "127.0.0.1:1" // Port 1 is unlikely to be listening
// Provider should succeed even though second endpoint is invalid (lazy connections)
endpoint := validAddr + "," + invalidAddr
ctx := context.Background()
dialOpts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
provider, err := NewGrpcConnectionProvider(ctx, endpoint, dialOpts)
require.NoError(t, err, "Provider creation should succeed with lazy connections")
defer func() { provider.Close() }()
// First endpoint should work
conn := provider.CurrentConn()
assert.NotNil(t, conn, "First connection should be created lazily")
}
func TestGrpcConnectionProvider_SingleConnectionModel(t *testing.T) {
// Create provider with 3 endpoints
var addrs []string
var servers []*grpc.Server
for range 3 {
lis, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
server := grpc.NewServer()
go func() { _ = server.Serve(lis) }()
addrs = append(addrs, lis.Addr().String())
servers = append(servers, server)
}
defer func() {
for _, s := range servers {
s.Stop()
}
}()
endpoint := strings.Join(addrs, ",")
ctx := context.Background()
dialOpts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
provider, err := NewGrpcConnectionProvider(ctx, endpoint, dialOpts)
require.NoError(t, err)
defer func() { provider.Close() }()
// Access the internal state to verify single connection behavior
p := provider.(*grpcConnectionProvider)
// Initially no connection
p.mu.Lock()
assert.Equal(t, (*grpc.ClientConn)(nil), p.conn, "Connection should be nil before access")
p.mu.Unlock()
// Access connection - should create one
conn0 := provider.CurrentConn()
assert.NotNil(t, conn0)
p.mu.Lock()
assert.NotNil(t, p.conn, "Connection should be created after CurrentConn()")
firstConn := p.conn
p.mu.Unlock()
// Call CurrentConn again - should return same connection
conn0Again := provider.CurrentConn()
assert.Equal(t, conn0, conn0Again, "Should return same connection")
// Switch to different host - old connection should be closed, new one created lazily
require.NoError(t, provider.SwitchHost(1))
p.mu.Lock()
assert.Equal(t, (*grpc.ClientConn)(nil), p.conn, "Connection should be nil after SwitchHost (lazy)")
p.mu.Unlock()
// Get new connection
conn1 := provider.CurrentConn()
assert.NotNil(t, conn1)
assert.NotEqual(t, firstConn, conn1, "Should be a different connection after switching hosts")
}
// testProvider creates a provider with n test servers and returns cleanup function.
func testProvider(t *testing.T, n int) (GrpcConnectionProvider, []string, func()) {
var addrs []string
var cleanups []func()
for range n {
lis, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
server := grpc.NewServer()
go func() { _ = server.Serve(lis) }()
addrs = append(addrs, lis.Addr().String())
cleanups = append(cleanups, server.Stop)
}
endpoint := strings.Join(addrs, ",")
ctx := context.Background()
dialOpts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
provider, err := NewGrpcConnectionProvider(ctx, endpoint, dialOpts)
require.NoError(t, err)
cleanup := func() {
provider.Close()
for _, c := range cleanups {
c()
}
}
return provider, addrs, cleanup
}
func TestGrpcConnectionProvider(t *testing.T) {
provider, addrs, cleanup := testProvider(t, 3)
defer cleanup()
t.Run("initial state", func(t *testing.T) {
assert.Equal(t, 3, len(provider.Hosts()))
assert.Equal(t, addrs[0], provider.CurrentHost())
assert.NotNil(t, provider.CurrentConn())
})
t.Run("SwitchHost", func(t *testing.T) {
require.NoError(t, provider.SwitchHost(1))
assert.Equal(t, addrs[1], provider.CurrentHost())
assert.NotNil(t, provider.CurrentConn()) // New connection created lazily
require.NoError(t, provider.SwitchHost(0))
assert.Equal(t, addrs[0], provider.CurrentHost())
require.ErrorContains(t, "invalid host index", provider.SwitchHost(-1))
require.ErrorContains(t, "invalid host index", provider.SwitchHost(3))
})
t.Run("SwitchHost circular", func(t *testing.T) {
// Test round-robin style switching using SwitchHost with manual index
indices := []int{1, 2, 0, 1} // Simulate circular switching
for i, idx := range indices {
require.NoError(t, provider.SwitchHost(idx))
assert.Equal(t, addrs[idx], provider.CurrentHost(), "iteration %d", i)
}
})
t.Run("Hosts returns copy", func(t *testing.T) {
hosts := provider.Hosts()
original := hosts[0]
hosts[0] = "modified"
assert.Equal(t, original, provider.Hosts()[0])
})
}
func TestGrpcConnectionProvider_Close(t *testing.T) {
provider, _, cleanup := testProvider(t, 1)
defer cleanup()
assert.NotNil(t, provider.CurrentConn())
provider.Close()
assert.Equal(t, (*grpc.ClientConn)(nil), provider.CurrentConn())
provider.Close() // Double close is safe
}

View File

@@ -0,0 +1,27 @@
package grpc
import "google.golang.org/grpc"
// MockGrpcProvider implements GrpcConnectionProvider for testing.
type MockGrpcProvider struct {
MockConn *grpc.ClientConn
MockHosts []string
CurrentIndex int
ConnCounter uint64
}
func (m *MockGrpcProvider) CurrentConn() *grpc.ClientConn { return m.MockConn }
func (m *MockGrpcProvider) CurrentHost() string {
if len(m.MockHosts) > 0 {
return m.MockHosts[m.CurrentIndex]
}
return ""
}
func (m *MockGrpcProvider) Hosts() []string { return m.MockHosts }
func (m *MockGrpcProvider) SwitchHost(idx int) error {
m.CurrentIndex = idx
m.ConnCounter++
return nil
}
func (m *MockGrpcProvider) ConnectionCounter() uint64 { return m.ConnCounter }
func (m *MockGrpcProvider) Close() {}

34
api/rest/BUILD.bazel Normal file
View File

@@ -0,0 +1,34 @@
load("@prysm//tools/go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"log.go",
"mock_rest_provider.go",
"rest_connection_provider.go",
"rest_handler.go",
],
importpath = "github.com/OffchainLabs/prysm/v7/api/rest",
visibility = ["//visibility:public"],
deps = [
"//api:go_default_library",
"//api/apiutil:go_default_library",
"//api/client:go_default_library",
"//config/params:go_default_library",
"//network/httputil:go_default_library",
"//runtime/version:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@io_opentelemetry_go_contrib_instrumentation_net_http_otelhttp//:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["rest_connection_provider_test.go"],
embed = [":go_default_library"],
deps = [
"//testing/assert:go_default_library",
"//testing/require:go_default_library",
],
)

9
api/rest/log.go Normal file
View File

@@ -0,0 +1,9 @@
// Code generated by hack/gen-logs.sh; DO NOT EDIT.
// This file is created and regenerated automatically. Anything added here might get removed.
package rest
import "github.com/sirupsen/logrus"
// The prefix for logs from this package will be the text after the last slash in the package path.
// If you wish to change this, you should add your desired name in the runtime/logging/logrus-prefixed-formatter/prefix-replacement.go file.
var log = logrus.WithField("package", "api/rest")

View File

@@ -0,0 +1,46 @@
package rest
import (
"bytes"
"context"
"net/http"
)
// MockRestProvider implements RestConnectionProvider for testing.
type MockRestProvider struct {
MockClient *http.Client
MockHandler Handler
MockHosts []string
HostIndex int
}
func (m *MockRestProvider) HttpClient() *http.Client { return m.MockClient }
func (m *MockRestProvider) Handler() Handler { return m.MockHandler }
func (m *MockRestProvider) CurrentHost() string {
if len(m.MockHosts) > 0 {
return m.MockHosts[m.HostIndex%len(m.MockHosts)]
}
return ""
}
func (m *MockRestProvider) Hosts() []string { return m.MockHosts }
func (m *MockRestProvider) SwitchHost(index int) error { m.HostIndex = index; return nil }
// MockHandler implements Handler for testing.
type MockHandler struct {
MockHost string
}
func (m *MockHandler) Get(_ context.Context, _ string, _ any) error { return nil }
func (m *MockHandler) GetStatusCode(_ context.Context, _ string) (int, error) {
return http.StatusOK, nil
}
func (m *MockHandler) GetSSZ(_ context.Context, _ string) ([]byte, http.Header, error) {
return nil, nil, nil
}
func (m *MockHandler) Post(_ context.Context, _ string, _ map[string]string, _ *bytes.Buffer, _ any) error {
return nil
}
func (m *MockHandler) PostSSZ(_ context.Context, _ string, _ map[string]string, _ *bytes.Buffer) ([]byte, http.Header, error) {
return nil, nil, nil
}
func (m *MockHandler) Host() string { return m.MockHost }

View File

@@ -0,0 +1,158 @@
package rest
import (
"net/http"
"strings"
"sync/atomic"
"time"
"github.com/OffchainLabs/prysm/v7/api/client"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
// RestConnectionProvider manages HTTP client configuration for REST API with failover support.
// It allows switching between different beacon node REST endpoints when the current one becomes unavailable.
type RestConnectionProvider interface {
// HttpClient returns the configured HTTP client with headers, timeout, and optional tracing.
HttpClient() *http.Client
// Handler returns the REST handler for making API requests.
Handler() Handler
// CurrentHost returns the current REST API endpoint URL.
CurrentHost() string
// Hosts returns all configured REST API endpoint URLs.
Hosts() []string
// SwitchHost switches to the endpoint at the given index.
SwitchHost(index int) error
}
// RestConnectionProviderOption is a functional option for configuring the REST connection provider.
type RestConnectionProviderOption func(*restConnectionProvider)
// WithHttpTimeout sets the HTTP client timeout.
func WithHttpTimeout(timeout time.Duration) RestConnectionProviderOption {
return func(p *restConnectionProvider) {
p.timeout = timeout
}
}
// WithHttpHeaders sets custom HTTP headers to include in all requests.
func WithHttpHeaders(headers map[string][]string) RestConnectionProviderOption {
return func(p *restConnectionProvider) {
p.headers = headers
}
}
// WithTracing enables OpenTelemetry tracing for HTTP requests.
func WithTracing() RestConnectionProviderOption {
return func(p *restConnectionProvider) {
p.enableTracing = true
}
}
type restConnectionProvider struct {
endpoints []string
httpClient *http.Client
restHandler *handler
currentIndex atomic.Uint64
timeout time.Duration
headers map[string][]string
enableTracing bool
}
// NewRestConnectionProvider creates a new REST connection provider that manages HTTP client configuration.
// The endpoint parameter can be a comma-separated list of URLs (e.g., "http://host1:3500,http://host2:3500").
func NewRestConnectionProvider(endpoint string, opts ...RestConnectionProviderOption) (RestConnectionProvider, error) {
endpoints := parseEndpoints(endpoint)
if len(endpoints) == 0 {
return nil, errors.New("no REST API endpoints provided")
}
p := &restConnectionProvider{
endpoints: endpoints,
}
for _, opt := range opts {
opt(p)
}
// Build the HTTP transport chain
var transport http.RoundTripper = http.DefaultTransport
// Add custom headers if configured
if len(p.headers) > 0 {
transport = client.NewCustomHeadersTransport(transport, p.headers)
}
// Add tracing if enabled
if p.enableTracing {
transport = otelhttp.NewTransport(transport)
}
p.httpClient = &http.Client{
Timeout: p.timeout,
Transport: transport,
}
// Create the REST handler with the HTTP client and initial host
p.restHandler = newHandler(*p.httpClient, endpoints[0])
log.WithFields(logrus.Fields{
"endpoints": endpoints,
"count": len(endpoints),
}).Info("Initialized REST connection provider")
return p, nil
}
// parseEndpoints splits a comma-separated endpoint string into individual endpoints.
func parseEndpoints(endpoint string) []string {
if endpoint == "" {
return nil
}
endpoints := make([]string, 0, 1)
for p := range strings.SplitSeq(endpoint, ",") {
if p = strings.TrimSpace(p); p != "" {
endpoints = append(endpoints, p)
}
}
return endpoints
}
func (p *restConnectionProvider) HttpClient() *http.Client {
return p.httpClient
}
func (p *restConnectionProvider) Handler() Handler {
return p.restHandler
}
func (p *restConnectionProvider) CurrentHost() string {
return p.endpoints[p.currentIndex.Load()]
}
func (p *restConnectionProvider) Hosts() []string {
// Return a copy to maintain immutability
hosts := make([]string, len(p.endpoints))
copy(hosts, p.endpoints)
return hosts
}
func (p *restConnectionProvider) SwitchHost(index int) error {
if index < 0 || index >= len(p.endpoints) {
return errors.Errorf("invalid host index %d, must be between 0 and %d", index, len(p.endpoints)-1)
}
oldIdx := p.currentIndex.Load()
p.currentIndex.Store(uint64(index))
// Update the rest handler's host
p.restHandler.SwitchHost(p.endpoints[index])
log.WithFields(logrus.Fields{
"previousHost": p.endpoints[oldIdx],
"newHost": p.endpoints[index],
}).Debug("Switched REST endpoint")
return nil
}

View File

@@ -0,0 +1,80 @@
package rest
import (
"reflect"
"testing"
"github.com/OffchainLabs/prysm/v7/testing/assert"
"github.com/OffchainLabs/prysm/v7/testing/require"
)
func TestParseEndpoints(t *testing.T) {
tests := []struct {
name string
input string
expected []string
}{
{"single endpoint", "http://localhost:3500", []string{"http://localhost:3500"}},
{"multiple endpoints", "http://host1:3500,http://host2:3500,http://host3:3500", []string{"http://host1:3500", "http://host2:3500", "http://host3:3500"}},
{"endpoints with spaces", "http://host1:3500, http://host2:3500 , http://host3:3500", []string{"http://host1:3500", "http://host2:3500", "http://host3:3500"}},
{"empty string", "", nil},
{"only commas", ",,,", []string{}},
{"trailing comma", "http://host1:3500,http://host2:3500,", []string{"http://host1:3500", "http://host2:3500"}},
{"leading comma", ",http://host1:3500,http://host2:3500", []string{"http://host1:3500", "http://host2:3500"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseEndpoints(tt.input)
if !reflect.DeepEqual(tt.expected, got) {
t.Errorf("parseEndpoints(%q) = %v, want %v", tt.input, got, tt.expected)
}
})
}
}
func TestNewRestConnectionProvider_Errors(t *testing.T) {
t.Run("no endpoints", func(t *testing.T) {
_, err := NewRestConnectionProvider("")
require.ErrorContains(t, "no REST API endpoints provided", err)
})
}
func TestRestConnectionProvider(t *testing.T) {
provider, err := NewRestConnectionProvider("http://host1:3500,http://host2:3500,http://host3:3500")
require.NoError(t, err)
t.Run("initial state", func(t *testing.T) {
assert.Equal(t, 3, len(provider.Hosts()))
assert.Equal(t, "http://host1:3500", provider.CurrentHost())
assert.NotNil(t, provider.HttpClient())
})
t.Run("SwitchHost", func(t *testing.T) {
require.NoError(t, provider.SwitchHost(1))
assert.Equal(t, "http://host2:3500", provider.CurrentHost())
require.NoError(t, provider.SwitchHost(0))
assert.Equal(t, "http://host1:3500", provider.CurrentHost())
require.ErrorContains(t, "invalid host index", provider.SwitchHost(-1))
require.ErrorContains(t, "invalid host index", provider.SwitchHost(3))
})
t.Run("Hosts returns copy", func(t *testing.T) {
hosts := provider.Hosts()
original := hosts[0]
hosts[0] = "modified"
assert.Equal(t, original, provider.Hosts()[0])
})
}
func TestRestConnectionProvider_WithOptions(t *testing.T) {
headers := map[string][]string{"Authorization": {"Bearer token"}}
provider, err := NewRestConnectionProvider(
"http://localhost:3500",
WithHttpHeaders(headers),
WithHttpTimeout(30000000000), // 30 seconds in nanoseconds
WithTracing(),
)
require.NoError(t, err)
assert.NotNil(t, provider.HttpClient())
assert.Equal(t, "http://localhost:3500", provider.CurrentHost())
}

View File

@@ -1,4 +1,4 @@
package beacon_api
package rest
import (
"bytes"
@@ -21,37 +21,46 @@ import (
type reqOption func(*http.Request)
type RestHandler interface {
// Handler defines the interface for making REST API requests.
type Handler interface {
Get(ctx context.Context, endpoint string, resp any) error
GetStatusCode(ctx context.Context, endpoint string) (int, error)
GetSSZ(ctx context.Context, endpoint string) ([]byte, http.Header, error)
Post(ctx context.Context, endpoint string, headers map[string]string, data *bytes.Buffer, resp any) error
PostSSZ(ctx context.Context, endpoint string, headers map[string]string, data *bytes.Buffer) ([]byte, http.Header, error)
HttpClient() *http.Client
Host() string
SetHost(host string)
}
type BeaconApiRestHandler struct {
type handler struct {
client http.Client
host string
reqOverrides []reqOption
}
// NewBeaconApiRestHandler returns a RestHandler
func NewBeaconApiRestHandler(client http.Client, host string) RestHandler {
brh := &BeaconApiRestHandler{
// newHandler returns a *handler for internal use within the rest package.
func newHandler(client http.Client, host string) *handler {
rh := &handler{
client: client,
host: host,
}
brh.appendAcceptOverride()
return brh
rh.appendAcceptOverride()
return rh
}
// NewHandler returns a Handler
func NewHandler(client http.Client, host string) Handler {
rh := &handler{
client: client,
host: host,
}
rh.appendAcceptOverride()
return rh
}
// appendAcceptOverride enables the Accept header to be customized at runtime via an environment variable.
// This is specified as an env var because it is a niche option that prysm may use for performance testing or debugging
// bug which users are unlikely to need. Using an env var keeps the set of user-facing flags cleaner.
func (c *BeaconApiRestHandler) appendAcceptOverride() {
func (c *handler) appendAcceptOverride() {
if accept := os.Getenv(params.EnvNameOverrideAccept); accept != "" {
c.reqOverrides = append(c.reqOverrides, func(req *http.Request) {
req.Header.Set("Accept", accept)
@@ -60,18 +69,18 @@ func (c *BeaconApiRestHandler) appendAcceptOverride() {
}
// HttpClient returns the underlying HTTP client of the handler
func (c *BeaconApiRestHandler) HttpClient() *http.Client {
func (c *handler) HttpClient() *http.Client {
return &c.client
}
// Host returns the underlying HTTP host
func (c *BeaconApiRestHandler) Host() string {
func (c *handler) Host() string {
return c.host
}
// Get sends a GET request and decodes the response body as a JSON object into the passed in object.
// If an HTTP error is returned, the body is decoded as a DefaultJsonError JSON object and returned as the first return value.
func (c *BeaconApiRestHandler) Get(ctx context.Context, endpoint string, resp any) error {
func (c *handler) Get(ctx context.Context, endpoint string, resp any) error {
url := c.host + endpoint
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
@@ -94,7 +103,7 @@ func (c *BeaconApiRestHandler) Get(ctx context.Context, endpoint string, resp an
// GetStatusCode sends a GET request and returns only the HTTP status code.
// This is useful for endpoints like /eth/v1/node/health that communicate status via HTTP codes
// (200 = ready, 206 = syncing, 503 = unavailable) rather than response bodies.
func (c *BeaconApiRestHandler) GetStatusCode(ctx context.Context, endpoint string) (int, error) {
func (c *handler) GetStatusCode(ctx context.Context, endpoint string) (int, error) {
url := c.host + endpoint
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
@@ -113,7 +122,7 @@ func (c *BeaconApiRestHandler) GetStatusCode(ctx context.Context, endpoint strin
return httpResp.StatusCode, nil
}
func (c *BeaconApiRestHandler) GetSSZ(ctx context.Context, endpoint string) ([]byte, http.Header, error) {
func (c *handler) GetSSZ(ctx context.Context, endpoint string) ([]byte, http.Header, error) {
url := c.host + endpoint
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
@@ -168,7 +177,7 @@ func (c *BeaconApiRestHandler) GetSSZ(ctx context.Context, endpoint string) ([]b
// Post sends a POST request and decodes the response body as a JSON object into the passed in object.
// If an HTTP error is returned, the body is decoded as a DefaultJsonError JSON object and returned as the first return value.
func (c *BeaconApiRestHandler) Post(
func (c *handler) Post(
ctx context.Context,
apiEndpoint string,
headers map[string]string,
@@ -204,7 +213,7 @@ func (c *BeaconApiRestHandler) Post(
}
// PostSSZ sends a POST request and prefers an SSZ (application/octet-stream) response body.
func (c *BeaconApiRestHandler) PostSSZ(
func (c *handler) PostSSZ(
ctx context.Context,
apiEndpoint string,
headers map[string]string,
@@ -305,6 +314,6 @@ func decodeResp(httpResp *http.Response, resp any) error {
return nil
}
func (c *BeaconApiRestHandler) SetHost(host string) {
func (c *handler) SwitchHost(host string) {
c.host = host
}

View File

@@ -92,7 +92,11 @@ func AcceptEncodingHeaderHandler() Middleware {
return
}
gz := gzip.NewWriter(w)
gz, err := gzip.NewWriterLevel(w, gzip.BestSpeed)
if err != nil {
next.ServeHTTP(w, r)
return
}
gzipRW := &gzipResponseWriter{gz: gz, ResponseWriter: w}
defer func() {
if !gzipRW.zip {

View File

@@ -9,6 +9,7 @@ go_library(
"conversions_blob.go",
"conversions_block.go",
"conversions_block_execution.go",
"conversions_gloas.go",
"conversions_lightclient.go",
"conversions_state.go",
"endpoints_beacon.go",
@@ -57,10 +58,12 @@ go_test(
],
embed = [":go_default_library"],
deps = [
"//consensus-types/blocks:go_default_library",
"//proto/engine/v1:go_default_library",
"//proto/prysm/v1alpha1:go_default_library",
"//testing/assert:go_default_library",
"//testing/require:go_default_library",
"//testing/util:go_default_library",
"@com_github_ethereum_go_ethereum//common:go_default_library",
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",
],

View File

@@ -503,3 +503,97 @@ func (s *SignedBlindedBeaconBlockFulu) MessageRawJson() ([]byte, error) {
func (s *SignedBlindedBeaconBlockFulu) SigString() string {
return s.Signature
}
// ----------------------------------------------------------------------------
// Gloas
// ----------------------------------------------------------------------------
type ExecutionPayloadBid struct {
ParentBlockHash string `json:"parent_block_hash"`
ParentBlockRoot string `json:"parent_block_root"`
BlockHash string `json:"block_hash"`
PrevRandao string `json:"prev_randao"`
FeeRecipient string `json:"fee_recipient"`
GasLimit string `json:"gas_limit"`
BuilderIndex string `json:"builder_index"`
Slot string `json:"slot"`
Value string `json:"value"`
ExecutionPayment string `json:"execution_payment"`
BlobKzgCommitments []string `json:"blob_kzg_commitments"`
}
type SignedExecutionPayloadBid struct {
Message *ExecutionPayloadBid `json:"message"`
Signature string `json:"signature"`
}
type PayloadAttestationData struct {
BeaconBlockRoot string `json:"beacon_block_root"`
Slot string `json:"slot"`
PayloadPresent bool `json:"payload_present"`
BlobDataAvailable bool `json:"blob_data_available"`
}
type PayloadAttestation struct {
AggregationBits string `json:"aggregation_bits"`
Data *PayloadAttestationData `json:"data"`
Signature string `json:"signature"`
}
type PayloadAttestationMessage struct {
ValidatorIndex string `json:"validator_index"`
Data *PayloadAttestationData `json:"data"`
Signature string `json:"signature"`
}
type BeaconBlockBodyGloas struct {
RandaoReveal string `json:"randao_reveal"`
Eth1Data *Eth1Data `json:"eth1_data"`
Graffiti string `json:"graffiti"`
ProposerSlashings []*ProposerSlashing `json:"proposer_slashings"`
AttesterSlashings []*AttesterSlashingElectra `json:"attester_slashings"`
Attestations []*AttestationElectra `json:"attestations"`
Deposits []*Deposit `json:"deposits"`
VoluntaryExits []*SignedVoluntaryExit `json:"voluntary_exits"`
SyncAggregate *SyncAggregate `json:"sync_aggregate"`
BLSToExecutionChanges []*SignedBLSToExecutionChange `json:"bls_to_execution_changes"`
SignedExecutionPayloadBid *SignedExecutionPayloadBid `json:"signed_execution_payload_bid"`
PayloadAttestations []*PayloadAttestation `json:"payload_attestations"`
}
type BeaconBlockGloas struct {
Slot string `json:"slot"`
ProposerIndex string `json:"proposer_index"`
ParentRoot string `json:"parent_root"`
StateRoot string `json:"state_root"`
Body *BeaconBlockBodyGloas `json:"body"`
}
type SignedBeaconBlockGloas struct {
Message *BeaconBlockGloas `json:"message"`
Signature string `json:"signature"`
}
var _ SignedMessageJsoner = &SignedBeaconBlockGloas{}
func (s *SignedBeaconBlockGloas) MessageRawJson() ([]byte, error) {
return json.Marshal(s.Message)
}
func (s *SignedBeaconBlockGloas) SigString() string {
return s.Signature
}
type ExecutionPayloadEnvelope struct {
Payload *ExecutionPayloadDeneb `json:"payload"`
ExecutionRequests *ExecutionRequests `json:"execution_requests"`
BuilderIndex string `json:"builder_index"`
BeaconBlockRoot string `json:"beacon_block_root"`
Slot string `json:"slot"`
StateRoot string `json:"state_root"`
}
type SignedExecutionPayloadEnvelope struct {
Message *ExecutionPayloadEnvelope `json:"message"`
Signature string `json:"signature"`
}

View File

@@ -268,6 +268,8 @@ func SignedBeaconBlockMessageJsoner(block interfaces.ReadOnlySignedBeaconBlock)
return SignedBlindedBeaconBlockFuluFromConsensus(pbStruct)
case *eth.SignedBeaconBlockFulu:
return SignedBeaconBlockFuluFromConsensus(pbStruct)
case *eth.SignedBeaconBlockGloas:
return SignedBeaconBlockGloasFromConsensus(pbStruct)
default:
return nil, ErrUnsupportedConversion
}
@@ -2885,3 +2887,422 @@ func SignedBeaconBlockFuluFromConsensus(b *eth.SignedBeaconBlockFulu) (*SignedBe
Signature: hexutil.Encode(b.Signature),
}, nil
}
// ----------------------------------------------------------------------------
// Gloas
// ----------------------------------------------------------------------------
func SignedBeaconBlockGloasFromConsensus(b *eth.SignedBeaconBlockGloas) (*SignedBeaconBlockGloas, error) {
block, err := BeaconBlockGloasFromConsensus(b.Block)
if err != nil {
return nil, err
}
return &SignedBeaconBlockGloas{
Message: block,
Signature: hexutil.Encode(b.Signature),
}, nil
}
func BeaconBlockGloasFromConsensus(b *eth.BeaconBlockGloas) (*BeaconBlockGloas, error) {
payloadAttestations := make([]*PayloadAttestation, len(b.Body.PayloadAttestations))
for i, pa := range b.Body.PayloadAttestations {
payloadAttestations[i] = PayloadAttestationFromConsensus(pa)
}
return &BeaconBlockGloas{
Slot: fmt.Sprintf("%d", b.Slot),
ProposerIndex: fmt.Sprintf("%d", b.ProposerIndex),
ParentRoot: hexutil.Encode(b.ParentRoot),
StateRoot: hexutil.Encode(b.StateRoot),
Body: &BeaconBlockBodyGloas{
RandaoReveal: hexutil.Encode(b.Body.RandaoReveal),
Eth1Data: Eth1DataFromConsensus(b.Body.Eth1Data),
Graffiti: hexutil.Encode(b.Body.Graffiti),
ProposerSlashings: ProposerSlashingsFromConsensus(b.Body.ProposerSlashings),
AttesterSlashings: AttesterSlashingsElectraFromConsensus(b.Body.AttesterSlashings),
Attestations: AttsElectraFromConsensus(b.Body.Attestations),
Deposits: DepositsFromConsensus(b.Body.Deposits),
VoluntaryExits: SignedExitsFromConsensus(b.Body.VoluntaryExits),
SyncAggregate: SyncAggregateFromConsensus(b.Body.SyncAggregate),
BLSToExecutionChanges: SignedBLSChangesFromConsensus(b.Body.BlsToExecutionChanges),
SignedExecutionPayloadBid: SignedExecutionPayloadBidFromConsensus(b.Body.SignedExecutionPayloadBid),
PayloadAttestations: payloadAttestations,
},
}, nil
}
func SignedExecutionPayloadBidFromConsensus(b *eth.SignedExecutionPayloadBid) *SignedExecutionPayloadBid {
return &SignedExecutionPayloadBid{
Message: ExecutionPayloadBidFromConsensus(b.Message),
Signature: hexutil.Encode(b.Signature),
}
}
func ExecutionPayloadBidFromConsensus(b *eth.ExecutionPayloadBid) *ExecutionPayloadBid {
blobKzgCommitments := make([]string, len(b.BlobKzgCommitments))
for i := range b.BlobKzgCommitments {
blobKzgCommitments[i] = hexutil.Encode(b.BlobKzgCommitments[i])
}
return &ExecutionPayloadBid{
ParentBlockHash: hexutil.Encode(b.ParentBlockHash),
ParentBlockRoot: hexutil.Encode(b.ParentBlockRoot),
BlockHash: hexutil.Encode(b.BlockHash),
PrevRandao: hexutil.Encode(b.PrevRandao),
FeeRecipient: hexutil.Encode(b.FeeRecipient),
GasLimit: fmt.Sprintf("%d", b.GasLimit),
BuilderIndex: fmt.Sprintf("%d", b.BuilderIndex),
Slot: fmt.Sprintf("%d", b.Slot),
Value: fmt.Sprintf("%d", b.Value),
ExecutionPayment: fmt.Sprintf("%d", b.ExecutionPayment),
BlobKzgCommitments: blobKzgCommitments,
}
}
func PayloadAttestationFromConsensus(pa *eth.PayloadAttestation) *PayloadAttestation {
return &PayloadAttestation{
AggregationBits: hexutil.Encode(pa.AggregationBits),
Data: PayloadAttestationDataFromConsensus(pa.Data),
Signature: hexutil.Encode(pa.Signature),
}
}
func PayloadAttestationMessageFromConsensus(m *eth.PayloadAttestationMessage) *PayloadAttestationMessage {
return &PayloadAttestationMessage{
ValidatorIndex: fmt.Sprintf("%d", m.ValidatorIndex),
Data: PayloadAttestationDataFromConsensus(m.Data),
Signature: hexutil.Encode(m.Signature),
}
}
func PayloadAttestationDataFromConsensus(d *eth.PayloadAttestationData) *PayloadAttestationData {
return &PayloadAttestationData{
BeaconBlockRoot: hexutil.Encode(d.BeaconBlockRoot),
Slot: fmt.Sprintf("%d", d.Slot),
PayloadPresent: d.PayloadPresent,
BlobDataAvailable: d.BlobDataAvailable,
}
}
func (b *SignedBeaconBlockGloas) ToConsensus() (*eth.SignedBeaconBlockGloas, error) {
if b == nil {
return nil, errNilValue
}
sig, err := bytesutil.DecodeHexWithLength(b.Signature, fieldparams.BLSSignatureLength)
if err != nil {
return nil, server.NewDecodeError(err, "Signature")
}
block, err := b.Message.ToConsensus()
if err != nil {
return nil, server.NewDecodeError(err, "Message")
}
return &eth.SignedBeaconBlockGloas{
Block: block,
Signature: sig,
}, nil
}
func (b *BeaconBlockGloas) ToConsensus() (*eth.BeaconBlockGloas, error) {
if b == nil {
return nil, errNilValue
}
if b.Body == nil {
return nil, server.NewDecodeError(errNilValue, "Body")
}
if b.Body.Eth1Data == nil {
return nil, server.NewDecodeError(errNilValue, "Body.Eth1Data")
}
if b.Body.SyncAggregate == nil {
return nil, server.NewDecodeError(errNilValue, "Body.SyncAggregate")
}
if b.Body.SignedExecutionPayloadBid == nil {
return nil, server.NewDecodeError(errNilValue, "Body.SignedExecutionPayloadBid")
}
slot, err := strconv.ParseUint(b.Slot, 10, 64)
if err != nil {
return nil, server.NewDecodeError(err, "Slot")
}
proposerIndex, err := strconv.ParseUint(b.ProposerIndex, 10, 64)
if err != nil {
return nil, server.NewDecodeError(err, "ProposerIndex")
}
parentRoot, err := bytesutil.DecodeHexWithLength(b.ParentRoot, fieldparams.RootLength)
if err != nil {
return nil, server.NewDecodeError(err, "ParentRoot")
}
stateRoot, err := bytesutil.DecodeHexWithLength(b.StateRoot, fieldparams.RootLength)
if err != nil {
return nil, server.NewDecodeError(err, "StateRoot")
}
body, err := b.Body.ToConsensus()
if err != nil {
return nil, server.NewDecodeError(err, "Body")
}
return &eth.BeaconBlockGloas{
Slot: primitives.Slot(slot),
ProposerIndex: primitives.ValidatorIndex(proposerIndex),
ParentRoot: parentRoot,
StateRoot: stateRoot,
Body: body,
}, nil
}
func (b *BeaconBlockBodyGloas) ToConsensus() (*eth.BeaconBlockBodyGloas, error) {
if b == nil {
return nil, errNilValue
}
randaoReveal, err := bytesutil.DecodeHexWithLength(b.RandaoReveal, fieldparams.BLSSignatureLength)
if err != nil {
return nil, server.NewDecodeError(err, "RandaoReveal")
}
depositRoot, err := bytesutil.DecodeHexWithLength(b.Eth1Data.DepositRoot, fieldparams.RootLength)
if err != nil {
return nil, server.NewDecodeError(err, "Eth1Data.DepositRoot")
}
depositCount, err := strconv.ParseUint(b.Eth1Data.DepositCount, 10, 64)
if err != nil {
return nil, server.NewDecodeError(err, "Eth1Data.DepositCount")
}
blockHash, err := bytesutil.DecodeHexWithLength(b.Eth1Data.BlockHash, fieldparams.RootLength)
if err != nil {
return nil, server.NewDecodeError(err, "Eth1Data.BlockHash")
}
graffiti, err := bytesutil.DecodeHexWithLength(b.Graffiti, fieldparams.RootLength)
if err != nil {
return nil, server.NewDecodeError(err, "Graffiti")
}
proposerSlashings, err := ProposerSlashingsToConsensus(b.ProposerSlashings)
if err != nil {
return nil, server.NewDecodeError(err, "ProposerSlashings")
}
attesterSlashings, err := AttesterSlashingsElectraToConsensus(b.AttesterSlashings)
if err != nil {
return nil, server.NewDecodeError(err, "AttesterSlashings")
}
atts, err := AttsElectraToConsensus(b.Attestations)
if err != nil {
return nil, server.NewDecodeError(err, "Attestations")
}
deposits, err := DepositsToConsensus(b.Deposits)
if err != nil {
return nil, server.NewDecodeError(err, "Deposits")
}
exits, err := SignedExitsToConsensus(b.VoluntaryExits)
if err != nil {
return nil, server.NewDecodeError(err, "VoluntaryExits")
}
syncCommitteeBits, err := bytesutil.DecodeHexWithLength(b.SyncAggregate.SyncCommitteeBits, fieldparams.SyncAggregateSyncCommitteeBytesLength)
if err != nil {
return nil, server.NewDecodeError(err, "SyncAggregate.SyncCommitteeBits")
}
syncCommitteeSig, err := bytesutil.DecodeHexWithLength(b.SyncAggregate.SyncCommitteeSignature, fieldparams.BLSSignatureLength)
if err != nil {
return nil, server.NewDecodeError(err, "SyncAggregate.SyncCommitteeSignature")
}
blsChanges, err := SignedBLSChangesToConsensus(b.BLSToExecutionChanges)
if err != nil {
return nil, server.NewDecodeError(err, "BLSToExecutionChanges")
}
signedBid, err := b.SignedExecutionPayloadBid.ToConsensus()
if err != nil {
return nil, server.NewDecodeError(err, "SignedExecutionPayloadBid")
}
payloadAttestations, err := PayloadAttestationsToConsensus(b.PayloadAttestations)
if err != nil {
return nil, server.NewDecodeError(err, "PayloadAttestations")
}
return &eth.BeaconBlockBodyGloas{
RandaoReveal: randaoReveal,
Eth1Data: &eth.Eth1Data{
DepositRoot: depositRoot,
DepositCount: depositCount,
BlockHash: blockHash,
},
Graffiti: graffiti,
ProposerSlashings: proposerSlashings,
AttesterSlashings: attesterSlashings,
Attestations: atts,
Deposits: deposits,
VoluntaryExits: exits,
SyncAggregate: &eth.SyncAggregate{
SyncCommitteeBits: syncCommitteeBits,
SyncCommitteeSignature: syncCommitteeSig,
},
BlsToExecutionChanges: blsChanges,
SignedExecutionPayloadBid: signedBid,
PayloadAttestations: payloadAttestations,
}, nil
}
func (b *SignedExecutionPayloadBid) ToConsensus() (*eth.SignedExecutionPayloadBid, error) {
if b == nil {
return nil, errNilValue
}
sig, err := bytesutil.DecodeHexWithLength(b.Signature, fieldparams.BLSSignatureLength)
if err != nil {
return nil, server.NewDecodeError(err, "Signature")
}
message, err := b.Message.ToConsensus()
if err != nil {
return nil, server.NewDecodeError(err, "Message")
}
return &eth.SignedExecutionPayloadBid{
Message: message,
Signature: sig,
}, nil
}
func (b *ExecutionPayloadBid) ToConsensus() (*eth.ExecutionPayloadBid, error) {
if b == nil {
return nil, errNilValue
}
parentBlockHash, err := bytesutil.DecodeHexWithLength(b.ParentBlockHash, fieldparams.RootLength)
if err != nil {
return nil, server.NewDecodeError(err, "ParentBlockHash")
}
parentBlockRoot, err := bytesutil.DecodeHexWithLength(b.ParentBlockRoot, fieldparams.RootLength)
if err != nil {
return nil, server.NewDecodeError(err, "ParentBlockRoot")
}
blockHash, err := bytesutil.DecodeHexWithLength(b.BlockHash, fieldparams.RootLength)
if err != nil {
return nil, server.NewDecodeError(err, "BlockHash")
}
prevRandao, err := bytesutil.DecodeHexWithLength(b.PrevRandao, fieldparams.RootLength)
if err != nil {
return nil, server.NewDecodeError(err, "PrevRandao")
}
feeRecipient, err := bytesutil.DecodeHexWithLength(b.FeeRecipient, fieldparams.FeeRecipientLength)
if err != nil {
return nil, server.NewDecodeError(err, "FeeRecipient")
}
gasLimit, err := strconv.ParseUint(b.GasLimit, 10, 64)
if err != nil {
return nil, server.NewDecodeError(err, "GasLimit")
}
builderIndex, err := strconv.ParseUint(b.BuilderIndex, 10, 64)
if err != nil {
return nil, server.NewDecodeError(err, "BuilderIndex")
}
slot, err := strconv.ParseUint(b.Slot, 10, 64)
if err != nil {
return nil, server.NewDecodeError(err, "Slot")
}
value, err := strconv.ParseUint(b.Value, 10, 64)
if err != nil {
return nil, server.NewDecodeError(err, "Value")
}
executionPayment, err := strconv.ParseUint(b.ExecutionPayment, 10, 64)
if err != nil {
return nil, server.NewDecodeError(err, "ExecutionPayment")
}
err = slice.VerifyMaxLength(b.BlobKzgCommitments, fieldparams.MaxBlobCommitmentsPerBlock)
if err != nil {
return nil, server.NewDecodeError(err, "BlobKzgCommitments")
}
blobKzgCommitments := make([][]byte, len(b.BlobKzgCommitments))
for i, commitment := range b.BlobKzgCommitments {
kzg, err := bytesutil.DecodeHexWithLength(commitment, fieldparams.BLSPubkeyLength)
if err != nil {
return nil, server.NewDecodeError(err, fmt.Sprintf("BlobKzgCommitments[%d]", i))
}
blobKzgCommitments[i] = kzg
}
return &eth.ExecutionPayloadBid{
ParentBlockHash: parentBlockHash,
ParentBlockRoot: parentBlockRoot,
BlockHash: blockHash,
PrevRandao: prevRandao,
FeeRecipient: feeRecipient,
GasLimit: gasLimit,
BuilderIndex: primitives.BuilderIndex(builderIndex),
Slot: primitives.Slot(slot),
Value: primitives.Gwei(value),
ExecutionPayment: primitives.Gwei(executionPayment),
BlobKzgCommitments: blobKzgCommitments,
}, nil
}
func PayloadAttestationsToConsensus(pa []*PayloadAttestation) ([]*eth.PayloadAttestation, error) {
if pa == nil {
return nil, errNilValue
}
result := make([]*eth.PayloadAttestation, len(pa))
for i, p := range pa {
converted, err := p.ToConsensus()
if err != nil {
return nil, server.NewDecodeError(err, fmt.Sprintf("[%d]", i))
}
result[i] = converted
}
return result, nil
}
func (p *PayloadAttestation) ToConsensus() (*eth.PayloadAttestation, error) {
if p == nil {
return nil, errNilValue
}
aggregationBits, err := hexutil.Decode(p.AggregationBits)
if err != nil {
return nil, server.NewDecodeError(err, "AggregationBits")
}
data, err := p.Data.ToConsensus()
if err != nil {
return nil, server.NewDecodeError(err, "Data")
}
sig, err := bytesutil.DecodeHexWithLength(p.Signature, fieldparams.BLSSignatureLength)
if err != nil {
return nil, server.NewDecodeError(err, "Signature")
}
return &eth.PayloadAttestation{
AggregationBits: aggregationBits,
Data: data,
Signature: sig,
}, nil
}
func (d *PayloadAttestationData) ToConsensus() (*eth.PayloadAttestationData, error) {
if d == nil {
return nil, errNilValue
}
beaconBlockRoot, err := bytesutil.DecodeHexWithLength(d.BeaconBlockRoot, fieldparams.RootLength)
if err != nil {
return nil, server.NewDecodeError(err, "BeaconBlockRoot")
}
slot, err := strconv.ParseUint(d.Slot, 10, 64)
if err != nil {
return nil, server.NewDecodeError(err, "Slot")
}
return &eth.PayloadAttestationData{
BeaconBlockRoot: beaconBlockRoot,
Slot: primitives.Slot(slot),
PayloadPresent: d.PayloadPresent,
BlobDataAvailable: d.BlobDataAvailable,
}, nil
}
// SignedExecutionPayloadEnvelopeFromConsensus converts a proto envelope to the API struct.
func SignedExecutionPayloadEnvelopeFromConsensus(e *eth.SignedExecutionPayloadEnvelope) (*SignedExecutionPayloadEnvelope, error) {
payload, err := ExecutionPayloadDenebFromConsensus(e.Message.Payload)
if err != nil {
return nil, err
}
var requests *ExecutionRequests
if e.Message.ExecutionRequests != nil {
requests = ExecutionRequestsFromConsensus(e.Message.ExecutionRequests)
}
return &SignedExecutionPayloadEnvelope{
Message: &ExecutionPayloadEnvelope{
Payload: payload,
ExecutionRequests: requests,
BuilderIndex: fmt.Sprintf("%d", e.Message.BuilderIndex),
BeaconBlockRoot: hexutil.Encode(e.Message.BeaconBlockRoot),
Slot: fmt.Sprintf("%d", e.Message.Slot),
StateRoot: hexutil.Encode(e.Message.StateRoot),
},
Signature: hexutil.Encode(e.Signature),
}, nil
}

View File

@@ -0,0 +1,89 @@
package structs
import (
"fmt"
"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/ethereum/go-ethereum/common/hexutil"
)
func ROExecutionPayloadBidFromConsensus(b interfaces.ROExecutionPayloadBid) *ExecutionPayloadBid {
if b == nil {
return nil
}
pbh := b.ParentBlockHash()
pbr := b.ParentBlockRoot()
bh := b.BlockHash()
pr := b.PrevRandao()
fr := b.FeeRecipient()
commitments := b.BlobKzgCommitments()
blobKzgCommitments := make([]string, 0, len(commitments))
for _, commitment := range commitments {
blobKzgCommitments = append(blobKzgCommitments, hexutil.Encode(commitment))
}
return &ExecutionPayloadBid{
ParentBlockHash: hexutil.Encode(pbh[:]),
ParentBlockRoot: hexutil.Encode(pbr[:]),
BlockHash: hexutil.Encode(bh[:]),
PrevRandao: hexutil.Encode(pr[:]),
FeeRecipient: hexutil.Encode(fr[:]),
GasLimit: fmt.Sprintf("%d", b.GasLimit()),
BuilderIndex: fmt.Sprintf("%d", b.BuilderIndex()),
Slot: fmt.Sprintf("%d", b.Slot()),
Value: fmt.Sprintf("%d", b.Value()),
ExecutionPayment: fmt.Sprintf("%d", b.ExecutionPayment()),
BlobKzgCommitments: blobKzgCommitments,
}
}
func BuildersFromConsensus(builders []*ethpb.Builder) []*Builder {
newBuilders := make([]*Builder, len(builders))
for i, b := range builders {
newBuilders[i] = BuilderFromConsensus(b)
}
return newBuilders
}
func BuilderFromConsensus(b *ethpb.Builder) *Builder {
return &Builder{
Pubkey: hexutil.Encode(b.Pubkey),
Version: hexutil.Encode(b.Version),
ExecutionAddress: hexutil.Encode(b.ExecutionAddress),
Balance: fmt.Sprintf("%d", b.Balance),
DepositEpoch: fmt.Sprintf("%d", b.DepositEpoch),
WithdrawableEpoch: fmt.Sprintf("%d", b.WithdrawableEpoch),
}
}
func BuilderPendingPaymentsFromConsensus(payments []*ethpb.BuilderPendingPayment) []*BuilderPendingPayment {
newPayments := make([]*BuilderPendingPayment, len(payments))
for i, p := range payments {
newPayments[i] = BuilderPendingPaymentFromConsensus(p)
}
return newPayments
}
func BuilderPendingPaymentFromConsensus(p *ethpb.BuilderPendingPayment) *BuilderPendingPayment {
return &BuilderPendingPayment{
Weight: fmt.Sprintf("%d", p.Weight),
Withdrawal: BuilderPendingWithdrawalFromConsensus(p.Withdrawal),
}
}
func BuilderPendingWithdrawalsFromConsensus(withdrawals []*ethpb.BuilderPendingWithdrawal) []*BuilderPendingWithdrawal {
newWithdrawals := make([]*BuilderPendingWithdrawal, len(withdrawals))
for i, w := range withdrawals {
newWithdrawals[i] = BuilderPendingWithdrawalFromConsensus(w)
}
return newWithdrawals
}
func BuilderPendingWithdrawalFromConsensus(w *ethpb.BuilderPendingWithdrawal) *BuilderPendingWithdrawal {
return &BuilderPendingWithdrawal{
FeeRecipient: hexutil.Encode(w.FeeRecipient),
Amount: fmt.Sprintf("%d", w.Amount),
BuilderIndex: fmt.Sprintf("%d", w.BuilderIndex),
}
}

View File

@@ -972,3 +972,223 @@ func BeaconStateFuluFromConsensus(st beaconState.BeaconState) (*BeaconStateFulu,
ProposerLookahead: lookahead,
}, nil
}
// ----------------------------------------------------------------------------
// Gloas
// ----------------------------------------------------------------------------
func BeaconStateGloasFromConsensus(st beaconState.BeaconState) (*BeaconStateGloas, error) {
srcBr := st.BlockRoots()
br := make([]string, len(srcBr))
for i, r := range srcBr {
br[i] = hexutil.Encode(r)
}
srcSr := st.StateRoots()
sr := make([]string, len(srcSr))
for i, r := range srcSr {
sr[i] = hexutil.Encode(r)
}
srcHr := st.HistoricalRoots()
hr := make([]string, len(srcHr))
for i, r := range srcHr {
hr[i] = hexutil.Encode(r)
}
srcVotes := st.Eth1DataVotes()
votes := make([]*Eth1Data, len(srcVotes))
for i, e := range srcVotes {
votes[i] = Eth1DataFromConsensus(e)
}
srcVals := st.Validators()
vals := make([]*Validator, len(srcVals))
for i, v := range srcVals {
vals[i] = ValidatorFromConsensus(v)
}
srcBals := st.Balances()
bals := make([]string, len(srcBals))
for i, b := range srcBals {
bals[i] = fmt.Sprintf("%d", b)
}
srcRm := st.RandaoMixes()
rm := make([]string, len(srcRm))
for i, m := range srcRm {
rm[i] = hexutil.Encode(m)
}
srcSlashings := st.Slashings()
slashings := make([]string, len(srcSlashings))
for i, s := range srcSlashings {
slashings[i] = fmt.Sprintf("%d", s)
}
srcPrevPart, err := st.PreviousEpochParticipation()
if err != nil {
return nil, err
}
prevPart := make([]string, len(srcPrevPart))
for i, p := range srcPrevPart {
prevPart[i] = fmt.Sprintf("%d", p)
}
srcCurrPart, err := st.CurrentEpochParticipation()
if err != nil {
return nil, err
}
currPart := make([]string, len(srcCurrPart))
for i, p := range srcCurrPart {
currPart[i] = fmt.Sprintf("%d", p)
}
srcIs, err := st.InactivityScores()
if err != nil {
return nil, err
}
is := make([]string, len(srcIs))
for i, s := range srcIs {
is[i] = fmt.Sprintf("%d", s)
}
currSc, err := st.CurrentSyncCommittee()
if err != nil {
return nil, err
}
nextSc, err := st.NextSyncCommittee()
if err != nil {
return nil, err
}
srcHs, err := st.HistoricalSummaries()
if err != nil {
return nil, err
}
hs := make([]*HistoricalSummary, len(srcHs))
for i, s := range srcHs {
hs[i] = HistoricalSummaryFromConsensus(s)
}
nwi, err := st.NextWithdrawalIndex()
if err != nil {
return nil, err
}
nwvi, err := st.NextWithdrawalValidatorIndex()
if err != nil {
return nil, err
}
drsi, err := st.DepositRequestsStartIndex()
if err != nil {
return nil, err
}
dbtc, err := st.DepositBalanceToConsume()
if err != nil {
return nil, err
}
ebtc, err := st.ExitBalanceToConsume()
if err != nil {
return nil, err
}
eee, err := st.EarliestExitEpoch()
if err != nil {
return nil, err
}
cbtc, err := st.ConsolidationBalanceToConsume()
if err != nil {
return nil, err
}
ece, err := st.EarliestConsolidationEpoch()
if err != nil {
return nil, err
}
pbd, err := st.PendingDeposits()
if err != nil {
return nil, err
}
ppw, err := st.PendingPartialWithdrawals()
if err != nil {
return nil, err
}
pc, err := st.PendingConsolidations()
if err != nil {
return nil, err
}
srcLookahead, err := st.ProposerLookahead()
if err != nil {
return nil, err
}
lookahead := make([]string, len(srcLookahead))
for i, v := range srcLookahead {
lookahead[i] = fmt.Sprintf("%d", uint64(v))
}
// Gloas-specific fields
lepb, err := st.LatestExecutionPayloadBid()
if err != nil {
return nil, err
}
builders, err := st.Builders()
if err != nil {
return nil, err
}
nwbi, err := st.NextWithdrawalBuilderIndex()
if err != nil {
return nil, err
}
epa, err := st.ExecutionPayloadAvailabilityVector()
if err != nil {
return nil, err
}
bpp, err := st.BuilderPendingPayments()
if err != nil {
return nil, err
}
bpw, err := st.BuilderPendingWithdrawals()
if err != nil {
return nil, err
}
lbh, err := st.LatestBlockHash()
if err != nil {
return nil, err
}
pew, err := st.PayloadExpectedWithdrawals()
if err != nil {
return nil, err
}
return &BeaconStateGloas{
GenesisTime: fmt.Sprintf("%d", st.GenesisTime().Unix()),
GenesisValidatorsRoot: hexutil.Encode(st.GenesisValidatorsRoot()),
Slot: fmt.Sprintf("%d", st.Slot()),
Fork: ForkFromConsensus(st.Fork()),
LatestBlockHeader: BeaconBlockHeaderFromConsensus(st.LatestBlockHeader()),
BlockRoots: br,
StateRoots: sr,
HistoricalRoots: hr,
Eth1Data: Eth1DataFromConsensus(st.Eth1Data()),
Eth1DataVotes: votes,
Eth1DepositIndex: fmt.Sprintf("%d", st.Eth1DepositIndex()),
Validators: vals,
Balances: bals,
RandaoMixes: rm,
Slashings: slashings,
PreviousEpochParticipation: prevPart,
CurrentEpochParticipation: currPart,
JustificationBits: hexutil.Encode(st.JustificationBits()),
PreviousJustifiedCheckpoint: CheckpointFromConsensus(st.PreviousJustifiedCheckpoint()),
CurrentJustifiedCheckpoint: CheckpointFromConsensus(st.CurrentJustifiedCheckpoint()),
FinalizedCheckpoint: CheckpointFromConsensus(st.FinalizedCheckpoint()),
InactivityScores: is,
CurrentSyncCommittee: SyncCommitteeFromConsensus(currSc),
NextSyncCommittee: SyncCommitteeFromConsensus(nextSc),
NextWithdrawalIndex: fmt.Sprintf("%d", nwi),
NextWithdrawalValidatorIndex: fmt.Sprintf("%d", nwvi),
HistoricalSummaries: hs,
DepositRequestsStartIndex: fmt.Sprintf("%d", drsi),
DepositBalanceToConsume: fmt.Sprintf("%d", dbtc),
ExitBalanceToConsume: fmt.Sprintf("%d", ebtc),
EarliestExitEpoch: fmt.Sprintf("%d", eee),
ConsolidationBalanceToConsume: fmt.Sprintf("%d", cbtc),
EarliestConsolidationEpoch: fmt.Sprintf("%d", ece),
PendingDeposits: PendingDepositsFromConsensus(pbd),
PendingPartialWithdrawals: PendingPartialWithdrawalsFromConsensus(ppw),
PendingConsolidations: PendingConsolidationsFromConsensus(pc),
ProposerLookahead: lookahead,
LatestExecutionPayloadBid: ROExecutionPayloadBidFromConsensus(lepb),
Builders: BuildersFromConsensus(builders),
NextWithdrawalBuilderIndex: fmt.Sprintf("%d", nwbi),
ExecutionPayloadAvailability: hexutil.Encode(epa),
BuilderPendingPayments: BuilderPendingPaymentsFromConsensus(bpp),
BuilderPendingWithdrawals: BuilderPendingWithdrawalsFromConsensus(bpw),
LatestBlockHash: hexutil.Encode(lbh[:]),
PayloadExpectedWithdrawals: WithdrawalsFromConsensus(pew),
}, nil
}

View File

@@ -1,11 +1,15 @@
package structs
import (
"bytes"
"testing"
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
eth "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/testing/assert"
"github.com/OffchainLabs/prysm/v7/testing/require"
"github.com/OffchainLabs/prysm/v7/testing/util"
"github.com/ethereum/go-ethereum/common/hexutil"
)
@@ -355,3 +359,214 @@ func TestIndexedAttestation_ToConsensus(t *testing.T) {
_, err := a.ToConsensus()
require.ErrorContains(t, errNilValue.Error(), err)
}
func TestROExecutionPayloadBidFromConsensus(t *testing.T) {
t.Run("empty blobkzg commitments", func(t *testing.T) {
bid := &eth.ExecutionPayloadBid{
ParentBlockHash: bytes.Repeat([]byte{0x01}, 32),
ParentBlockRoot: bytes.Repeat([]byte{0x02}, 32),
BlockHash: bytes.Repeat([]byte{0x03}, 32),
PrevRandao: bytes.Repeat([]byte{0x04}, 32),
FeeRecipient: bytes.Repeat([]byte{0x05}, 20),
GasLimit: 100,
BuilderIndex: 7,
Slot: 9,
Value: 11,
ExecutionPayment: 22,
BlobKzgCommitments: [][]byte{},
}
roBid, err := blocks.WrappedROExecutionPayloadBid(bid)
require.NoError(t, err)
got := ROExecutionPayloadBidFromConsensus(roBid)
want := &ExecutionPayloadBid{
ParentBlockHash: hexutil.Encode(bid.ParentBlockHash),
ParentBlockRoot: hexutil.Encode(bid.ParentBlockRoot),
BlockHash: hexutil.Encode(bid.BlockHash),
PrevRandao: hexutil.Encode(bid.PrevRandao),
FeeRecipient: hexutil.Encode(bid.FeeRecipient),
GasLimit: "100",
BuilderIndex: "7",
Slot: "9",
Value: "11",
ExecutionPayment: "22",
BlobKzgCommitments: []string{},
}
assert.DeepEqual(t, want, got)
})
t.Run("default", func(t *testing.T) {
bid := &eth.ExecutionPayloadBid{
ParentBlockHash: bytes.Repeat([]byte{0x01}, 32),
ParentBlockRoot: bytes.Repeat([]byte{0x02}, 32),
BlockHash: bytes.Repeat([]byte{0x03}, 32),
PrevRandao: bytes.Repeat([]byte{0x04}, 32),
FeeRecipient: bytes.Repeat([]byte{0x05}, 20),
GasLimit: 100,
BuilderIndex: 7,
Slot: 9,
Value: 11,
ExecutionPayment: 22,
BlobKzgCommitments: [][]byte{bytes.Repeat([]byte{0x06}, 48)},
}
roBid, err := blocks.WrappedROExecutionPayloadBid(bid)
require.NoError(t, err)
var bkcs []string
for _, commitment := range roBid.BlobKzgCommitments() {
bkcs = append(bkcs, hexutil.Encode(commitment))
}
got := ROExecutionPayloadBidFromConsensus(roBid)
want := &ExecutionPayloadBid{
ParentBlockHash: hexutil.Encode(bid.ParentBlockHash),
ParentBlockRoot: hexutil.Encode(bid.ParentBlockRoot),
BlockHash: hexutil.Encode(bid.BlockHash),
PrevRandao: hexutil.Encode(bid.PrevRandao),
FeeRecipient: hexutil.Encode(bid.FeeRecipient),
GasLimit: "100",
BuilderIndex: "7",
Slot: "9",
Value: "11",
ExecutionPayment: "22",
BlobKzgCommitments: bkcs,
}
assert.DeepEqual(t, want, got)
})
}
func TestBuilderConversionsFromConsensus(t *testing.T) {
builder := &eth.Builder{
Pubkey: bytes.Repeat([]byte{0xAA}, 48),
Version: bytes.Repeat([]byte{0x01}, 4),
ExecutionAddress: bytes.Repeat([]byte{0xBB}, 20),
Balance: 42,
DepositEpoch: 3,
WithdrawableEpoch: 4,
}
wantBuilder := &Builder{
Pubkey: hexutil.Encode(builder.Pubkey),
Version: hexutil.Encode(builder.Version),
ExecutionAddress: hexutil.Encode(builder.ExecutionAddress),
Balance: "42",
DepositEpoch: "3",
WithdrawableEpoch: "4",
}
assert.DeepEqual(t, wantBuilder, BuilderFromConsensus(builder))
assert.DeepEqual(t, []*Builder{wantBuilder}, BuildersFromConsensus([]*eth.Builder{builder}))
}
func TestBuilderPendingPaymentConversionsFromConsensus(t *testing.T) {
withdrawal := &eth.BuilderPendingWithdrawal{
FeeRecipient: bytes.Repeat([]byte{0x10}, 20),
Amount: 15,
BuilderIndex: 2,
}
payment := &eth.BuilderPendingPayment{
Weight: 5,
Withdrawal: withdrawal,
}
wantWithdrawal := &BuilderPendingWithdrawal{
FeeRecipient: hexutil.Encode(withdrawal.FeeRecipient),
Amount: "15",
BuilderIndex: "2",
}
wantPayment := &BuilderPendingPayment{
Weight: "5",
Withdrawal: wantWithdrawal,
}
assert.DeepEqual(t, wantPayment, BuilderPendingPaymentFromConsensus(payment))
assert.DeepEqual(t, []*BuilderPendingPayment{wantPayment}, BuilderPendingPaymentsFromConsensus([]*eth.BuilderPendingPayment{payment}))
assert.DeepEqual(t, wantWithdrawal, BuilderPendingWithdrawalFromConsensus(withdrawal))
assert.DeepEqual(t, []*BuilderPendingWithdrawal{wantWithdrawal}, BuilderPendingWithdrawalsFromConsensus([]*eth.BuilderPendingWithdrawal{withdrawal}))
}
func TestBeaconStateGloasFromConsensus(t *testing.T) {
st, err := util.NewBeaconStateGloas(func(state *eth.BeaconStateGloas) error {
state.GenesisTime = 123
state.GenesisValidatorsRoot = bytes.Repeat([]byte{0x10}, 32)
state.Slot = 5
state.ProposerLookahead = []uint64{1, 2}
state.LatestExecutionPayloadBid = &eth.ExecutionPayloadBid{
ParentBlockHash: bytes.Repeat([]byte{0x11}, 32),
ParentBlockRoot: bytes.Repeat([]byte{0x12}, 32),
BlockHash: bytes.Repeat([]byte{0x13}, 32),
PrevRandao: bytes.Repeat([]byte{0x14}, 32),
FeeRecipient: bytes.Repeat([]byte{0x15}, 20),
GasLimit: 64,
BuilderIndex: 3,
Slot: 5,
Value: 99,
ExecutionPayment: 7,
BlobKzgCommitments: [][]byte{bytes.Repeat([]byte{0x16}, 48)},
}
state.Builders = []*eth.Builder{
{
Pubkey: bytes.Repeat([]byte{0x20}, 48),
Version: bytes.Repeat([]byte{0x21}, 4),
ExecutionAddress: bytes.Repeat([]byte{0x22}, 20),
Balance: 88,
DepositEpoch: 1,
WithdrawableEpoch: 2,
},
}
state.NextWithdrawalBuilderIndex = 9
state.ExecutionPayloadAvailability = []byte{0x01, 0x02}
state.BuilderPendingPayments = []*eth.BuilderPendingPayment{
{
Weight: 3,
Withdrawal: &eth.BuilderPendingWithdrawal{
FeeRecipient: bytes.Repeat([]byte{0x23}, 20),
Amount: 4,
BuilderIndex: 5,
},
},
}
state.BuilderPendingWithdrawals = []*eth.BuilderPendingWithdrawal{
{
FeeRecipient: bytes.Repeat([]byte{0x24}, 20),
Amount: 6,
BuilderIndex: 7,
},
}
state.LatestBlockHash = bytes.Repeat([]byte{0x25}, 32)
state.PayloadExpectedWithdrawals = []*enginev1.Withdrawal{
{Index: 1, ValidatorIndex: 2, Address: bytes.Repeat([]byte{0x26}, 20), Amount: 10},
}
return nil
})
require.NoError(t, err)
got, err := BeaconStateGloasFromConsensus(st)
require.NoError(t, err)
require.Equal(t, "123", got.GenesisTime)
require.Equal(t, hexutil.Encode(bytes.Repeat([]byte{0x10}, 32)), got.GenesisValidatorsRoot)
require.Equal(t, "5", got.Slot)
require.DeepEqual(t, []string{"1", "2"}, got.ProposerLookahead)
require.Equal(t, "9", got.NextWithdrawalBuilderIndex)
require.Equal(t, hexutil.Encode([]byte{0x01, 0x02}), got.ExecutionPayloadAvailability)
require.Equal(t, hexutil.Encode(bytes.Repeat([]byte{0x25}, 32)), got.LatestBlockHash)
require.NotNil(t, got.LatestExecutionPayloadBid)
require.Equal(t, "64", got.LatestExecutionPayloadBid.GasLimit)
require.Equal(t, hexutil.Encode(bytes.Repeat([]byte{0x11}, 32)), got.LatestExecutionPayloadBid.ParentBlockHash)
require.NotNil(t, got.Builders)
require.Equal(t, hexutil.Encode(bytes.Repeat([]byte{0x20}, 48)), got.Builders[0].Pubkey)
require.Equal(t, "88", got.Builders[0].Balance)
require.Equal(t, "3", got.BuilderPendingPayments[0].Weight)
require.Equal(t, "4", got.BuilderPendingPayments[0].Withdrawal.Amount)
require.Equal(t, hexutil.Encode(bytes.Repeat([]byte{0x23}, 20)), got.BuilderPendingPayments[0].Withdrawal.FeeRecipient)
require.Equal(t, "6", got.BuilderPendingWithdrawals[0].Amount)
require.Equal(t, hexutil.Encode(bytes.Repeat([]byte{0x24}, 20)), got.BuilderPendingWithdrawals[0].FeeRecipient)
require.Equal(t, "1", got.PayloadExpectedWithdrawals[0].WithdrawalIndex)
require.Equal(t, "2", got.PayloadExpectedWithdrawals[0].ValidatorIndex)
require.Equal(t, hexutil.Encode(bytes.Repeat([]byte{0x26}, 20)), got.PayloadExpectedWithdrawals[0].ExecutionAddress)
require.Equal(t, "10", got.PayloadExpectedWithdrawals[0].Amount)
}

View File

@@ -285,6 +285,13 @@ type GetBlobsResponse struct {
Data []string `json:"data"` //blobs
}
type GetExecutionPayloadEnvelopeResponse struct {
Version string `json:"version"`
ExecutionOptimistic bool `json:"execution_optimistic"`
Finalized bool `json:"finalized"`
Data *SignedExecutionPayloadEnvelope `json:"data"`
}
type SSZQueryRequest struct {
Query string `json:"query"`
IncludeProof bool `json:"include_proof,omitempty"`

View File

@@ -112,3 +112,8 @@ type LightClientOptimisticUpdateEvent struct {
Version string `json:"version"`
Data *LightClientOptimisticUpdate `json:"data"`
}
type PayloadEvent struct {
Slot string `json:"slot"`
BlockRoot string `json:"block_root"`
}

View File

@@ -63,6 +63,19 @@ type PeerCount struct {
Connected string `json:"connected"`
Disconnecting string `json:"disconnecting"`
}
type GetVersionV2Response struct {
Data *VersionV2 `json:"data"`
}
type VersionV2 struct {
BeaconNode *ClientVersionV1 `json:"beacon_node"`
ExecutionClient *ClientVersionV1 `json:"execution_client,omitempty"`
}
type ClientVersionV1 struct {
Code string `json:"code"`
Name string `json:"name"`
Version string `json:"version"`
Commit string `json:"commit"`
}
type GetVersionResponse struct {
Data *Version `json:"data"`

View File

@@ -74,6 +74,18 @@ type SyncCommitteeDuty struct {
ValidatorSyncCommitteeIndices []string `json:"validator_sync_committee_indices"`
}
type GetPTCDutiesResponse struct {
DependentRoot string `json:"dependent_root"`
ExecutionOptimistic bool `json:"execution_optimistic"`
Data []*PTCDuty `json:"data"`
}
type PTCDuty struct {
Pubkey string `json:"pubkey"`
ValidatorIndex string `json:"validator_index"`
Slot string `json:"slot"`
}
// ProduceBlockV3Response is a wrapper json object for the returned block from the ProduceBlockV3 endpoint
type ProduceBlockV3Response struct {
Version string `json:"version"`

View File

@@ -262,3 +262,23 @@ type PendingConsolidation struct {
SourceIndex string `json:"source_index"`
TargetIndex string `json:"target_index"`
}
type Builder struct {
Pubkey string `json:"pubkey"`
Version string `json:"version"`
ExecutionAddress string `json:"execution_address"`
Balance string `json:"balance"`
DepositEpoch string `json:"deposit_epoch"`
WithdrawableEpoch string `json:"withdrawable_epoch"`
}
type BuilderPendingPayment struct {
Weight string `json:"weight"`
Withdrawal *BuilderPendingWithdrawal `json:"withdrawal"`
}
type BuilderPendingWithdrawal struct {
FeeRecipient string `json:"fee_recipient"`
Amount string `json:"amount"`
BuilderIndex string `json:"builder_index"`
}

View File

@@ -221,3 +221,51 @@ type BeaconStateFulu struct {
PendingConsolidations []*PendingConsolidation `json:"pending_consolidations"`
ProposerLookahead []string `json:"proposer_lookahead"`
}
type BeaconStateGloas struct {
GenesisTime string `json:"genesis_time"`
GenesisValidatorsRoot string `json:"genesis_validators_root"`
Slot string `json:"slot"`
Fork *Fork `json:"fork"`
LatestBlockHeader *BeaconBlockHeader `json:"latest_block_header"`
BlockRoots []string `json:"block_roots"`
StateRoots []string `json:"state_roots"`
HistoricalRoots []string `json:"historical_roots"`
Eth1Data *Eth1Data `json:"eth1_data"`
Eth1DataVotes []*Eth1Data `json:"eth1_data_votes"`
Eth1DepositIndex string `json:"eth1_deposit_index"`
Validators []*Validator `json:"validators"`
Balances []string `json:"balances"`
RandaoMixes []string `json:"randao_mixes"`
Slashings []string `json:"slashings"`
PreviousEpochParticipation []string `json:"previous_epoch_participation"`
CurrentEpochParticipation []string `json:"current_epoch_participation"`
JustificationBits string `json:"justification_bits"`
PreviousJustifiedCheckpoint *Checkpoint `json:"previous_justified_checkpoint"`
CurrentJustifiedCheckpoint *Checkpoint `json:"current_justified_checkpoint"`
FinalizedCheckpoint *Checkpoint `json:"finalized_checkpoint"`
InactivityScores []string `json:"inactivity_scores"`
CurrentSyncCommittee *SyncCommittee `json:"current_sync_committee"`
NextSyncCommittee *SyncCommittee `json:"next_sync_committee"`
NextWithdrawalIndex string `json:"next_withdrawal_index"`
NextWithdrawalValidatorIndex string `json:"next_withdrawal_validator_index"`
HistoricalSummaries []*HistoricalSummary `json:"historical_summaries"`
DepositRequestsStartIndex string `json:"deposit_requests_start_index"`
DepositBalanceToConsume string `json:"deposit_balance_to_consume"`
ExitBalanceToConsume string `json:"exit_balance_to_consume"`
EarliestExitEpoch string `json:"earliest_exit_epoch"`
ConsolidationBalanceToConsume string `json:"consolidation_balance_to_consume"`
EarliestConsolidationEpoch string `json:"earliest_consolidation_epoch"`
PendingDeposits []*PendingDeposit `json:"pending_deposits"`
PendingPartialWithdrawals []*PendingPartialWithdrawal `json:"pending_partial_withdrawals"`
PendingConsolidations []*PendingConsolidation `json:"pending_consolidations"`
ProposerLookahead []string `json:"proposer_lookahead"`
LatestExecutionPayloadBid *ExecutionPayloadBid `json:"latest_execution_payload_bid"`
Builders []*Builder `json:"builders"`
NextWithdrawalBuilderIndex string `json:"next_withdrawal_builder_index"`
ExecutionPayloadAvailability string `json:"execution_payload_availability"`
BuilderPendingPayments []*BuilderPendingPayment `json:"builder_pending_payments"`
BuilderPendingWithdrawals []*BuilderPendingWithdrawal `json:"builder_pending_withdrawals"`
LatestBlockHash string `json:"latest_block_hash"`
PayloadExpectedWithdrawals []*Withdrawal `json:"payload_expected_withdrawals"`
}

View File

@@ -10,6 +10,7 @@ go_library(
"error.go",
"execution_engine.go",
"forkchoice_update_execution.go",
"gloas.go",
"head.go",
"head_sync_committee_info.go",
"init_sync_process_block.go",
@@ -27,6 +28,8 @@ go_library(
"receive_blob.go",
"receive_block.go",
"receive_data_column.go",
"receive_execution_payload_envelope.go",
"receive_payload_attestation_message.go",
"service.go",
"setup_forkchoice.go",
"tracked_proposer.go",
@@ -50,6 +53,7 @@ go_library(
"//beacon-chain/core/epoch/precompute:go_default_library",
"//beacon-chain/core/feed:go_default_library",
"//beacon-chain/core/feed/state:go_default_library",
"//beacon-chain/core/gloas:go_default_library",
"//beacon-chain/core/helpers:go_default_library",
"//beacon-chain/core/peerdas:go_default_library",
"//beacon-chain/core/signing:go_default_library",
@@ -74,7 +78,6 @@ go_library(
"//beacon-chain/state:go_default_library",
"//beacon-chain/state/stategen:go_default_library",
"//beacon-chain/verification:go_default_library",
"//cmd/beacon-chain/flags:go_default_library",
"//config/features:go_default_library",
"//config/fieldparams:go_default_library",
"//config/params:go_default_library",
@@ -86,6 +89,7 @@ go_library(
"//consensus-types/primitives:go_default_library",
"//crypto/bls:go_default_library",
"//encoding/bytesutil:go_default_library",
"//io/logs:go_default_library",
"//math:go_default_library",
"//monitoring/tracing:go_default_library",
"//monitoring/tracing/trace:go_default_library",
@@ -119,6 +123,7 @@ go_test(
"error_test.go",
"execution_engine_test.go",
"forkchoice_update_execution_test.go",
"gloas_test.go",
"head_sync_committee_info_test.go",
"head_test.go",
"init_sync_process_block_test.go",
@@ -131,6 +136,7 @@ go_test(
"process_block_test.go",
"receive_attestation_test.go",
"receive_block_test.go",
"receive_payload_attestation_message_test.go",
"service_norace_test.go",
"service_test.go",
"setup_forkchoice_test.go",
@@ -148,6 +154,7 @@ go_test(
"//beacon-chain/core/altair:go_default_library",
"//beacon-chain/core/blocks:go_default_library",
"//beacon-chain/core/feed/state:go_default_library",
"//beacon-chain/core/gloas:go_default_library",
"//beacon-chain/core/helpers:go_default_library",
"//beacon-chain/core/peerdas:go_default_library",
"//beacon-chain/core/signing:go_default_library",
@@ -174,12 +181,12 @@ go_test(
"//beacon-chain/state/state-native:go_default_library",
"//beacon-chain/state/stategen:go_default_library",
"//beacon-chain/verification:go_default_library",
"//cmd/beacon-chain/flags:go_default_library",
"//config/features:go_default_library",
"//config/fieldparams:go_default_library",
"//config/params:go_default_library",
"//consensus-types/blocks:go_default_library",
"//consensus-types/interfaces:go_default_library",
"//consensus-types/payload-attribute:go_default_library",
"//consensus-types/primitives:go_default_library",
"//container/trie:go_default_library",
"//crypto/bls:go_default_library",

View File

@@ -18,6 +18,7 @@ import (
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
"github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/runtime/version"
"github.com/OffchainLabs/prysm/v7/time/slots"
"github.com/pkg/errors"
)
@@ -44,14 +45,19 @@ type ForkchoiceFetcher interface {
SetForkChoiceGenesisTime(time.Time)
UpdateHead(context.Context, primitives.Slot)
HighestReceivedBlockSlot() primitives.Slot
HighestReceivedBlockRoot() [32]byte
HasFullNode([32]byte) bool
PayloadContentLookup([32]byte) ([32]byte, bool)
ReceivedBlocksLastEpoch() (uint64, error)
InsertNode(context.Context, state.BeaconState, consensus_blocks.ROBlock) error
InsertPayload(interfaces.ROExecutionPayloadEnvelope) error
ForkChoiceDump(context.Context) (*forkchoice.Dump, error)
NewSlot(context.Context, primitives.Slot) error
ProposerBoost() [32]byte
RecentBlockSlot(root [32]byte) (primitives.Slot, error)
IsCanonical(ctx context.Context, blockRoot [32]byte) (bool, error)
DependentRoot(primitives.Epoch) ([32]byte, error)
CanonicalNodeAtSlot(primitives.Slot) ([32]byte, bool)
}
// TimeFetcher retrieves the Ethereum consensus data that's related to time.
@@ -113,6 +119,7 @@ type FinalizationFetcher interface {
FinalizedBlockHash() [32]byte
InForkchoice([32]byte) bool
IsFinalized(ctx context.Context, blockRoot [32]byte) bool
ParentPayloadReady(interfaces.ReadOnlyBeaconBlock) bool
}
// OptimisticModeFetcher retrieves information about optimistic status of the node.
@@ -402,6 +409,32 @@ func (s *Service) InForkchoice(root [32]byte) bool {
return s.cfg.ForkChoiceStore.HasNode(root)
}
// ParentPayloadReady returns true if the block's parent payload is available
// in forkchoice. For pre-Gloas blocks or blocks building on empty, this always
// returns true. For blocks building on full, it checks that the full node
// exists.
func (s *Service) ParentPayloadReady(blk interfaces.ReadOnlyBeaconBlock) bool {
if blk.Version() < version.Gloas {
return true
}
parentRoot := blk.ParentRoot()
s.cfg.ForkChoiceStore.RLock()
defer s.cfg.ForkChoiceStore.RUnlock()
blockHash, err := s.cfg.ForkChoiceStore.BlockHash(parentRoot)
if err != nil {
return false
}
bid, err := blk.Body().SignedExecutionPayloadBid()
if err != nil || bid == nil || bid.Message == nil {
return false
}
parentBlockHash := [32]byte(bid.Message.ParentBlockHash)
if parentBlockHash != blockHash {
return true // builds on empty, no full node needed
}
return s.cfg.ForkChoiceStore.HasFullNode(parentRoot)
}
// IsOptimisticForRoot takes the root as argument instead of the current head
// and returns true if it is optimistic.
func (s *Service) IsOptimisticForRoot(ctx context.Context, root [32]byte) (bool, error) {

View File

@@ -7,6 +7,7 @@ import (
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
consensus_blocks "github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
"github.com/OffchainLabs/prysm/v7/consensus-types/forkchoice"
"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
"github.com/OffchainLabs/prysm/v7/runtime/version"
@@ -41,6 +42,27 @@ func (s *Service) HighestReceivedBlockSlot() primitives.Slot {
return s.cfg.ForkChoiceStore.HighestReceivedBlockSlot()
}
// HighestReceivedBlockRoot returns the corresponding value from forkchoice
func (s *Service) HighestReceivedBlockRoot() [32]byte {
s.cfg.ForkChoiceStore.RLock()
defer s.cfg.ForkChoiceStore.RUnlock()
return s.cfg.ForkChoiceStore.HighestReceivedBlockRoot()
}
// HasFullNode returns the corresponding value from forkchoice
func (s *Service) HasFullNode(root [32]byte) bool {
s.cfg.ForkChoiceStore.RLock()
defer s.cfg.ForkChoiceStore.RUnlock()
return s.cfg.ForkChoiceStore.HasFullNode(root)
}
// PayloadContentLookup returns the preferred payload-content lookup key from forkchoice.
func (s *Service) PayloadContentLookup(root [32]byte) ([32]byte, bool) {
s.cfg.ForkChoiceStore.RLock()
defer s.cfg.ForkChoiceStore.RUnlock()
return s.cfg.ForkChoiceStore.PayloadContentLookup(root)
}
// ReceivedBlocksLastEpoch returns the corresponding value from forkchoice
func (s *Service) ReceivedBlocksLastEpoch() (uint64, error) {
s.cfg.ForkChoiceStore.RLock()
@@ -55,6 +77,13 @@ func (s *Service) InsertNode(ctx context.Context, st state.BeaconState, block co
return s.cfg.ForkChoiceStore.InsertNode(ctx, st, block)
}
// InsertPayload is a wrapper for payload insertion which is self locked
func (s *Service) InsertPayload(pe interfaces.ROExecutionPayloadEnvelope) error {
s.cfg.ForkChoiceStore.Lock()
defer s.cfg.ForkChoiceStore.Unlock()
return s.cfg.ForkChoiceStore.InsertPayload(pe)
}
// ForkChoiceDump returns the corresponding value from forkchoice
func (s *Service) ForkChoiceDump(ctx context.Context) (*forkchoice.Dump, error) {
s.cfg.ForkChoiceStore.RLock()
@@ -128,6 +157,13 @@ func (s *Service) hashForGenesisBlock(ctx context.Context, root [32]byte) ([]byt
return bytesutil.SafeCopyBytes(header.BlockHash()), nil
}
// CanonicalNodeAtSlot wraps the corresponding method in forkchoice
func (s *Service) CanonicalNodeAtSlot(slot primitives.Slot) ([32]byte, bool) {
s.cfg.ForkChoiceStore.RLock()
defer s.cfg.ForkChoiceStore.RUnlock()
return s.cfg.ForkChoiceStore.CanonicalNodeAtSlot(slot)
}
// DependentRoot wraps the corresponding method in forkchoice
func (s *Service) DependentRoot(epoch primitives.Epoch) ([32]byte, error) {
s.cfg.ForkChoiceStore.RLock()

View File

@@ -620,6 +620,121 @@ func TestService_IsFinalized(t *testing.T) {
require.Equal(t, false, c.IsFinalized(ctx, [32]byte{'c'}))
}
func TestParentPayloadReady(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
cfg.GloasForkEpoch = 0
cfg.InitializeForkSchedule()
params.OverrideBeaconConfig(cfg)
service, tr := minimalTestService(t)
ctx := t.Context()
fcs := tr.fcs
parentRoot := [32]byte{1}
parentBlockHash := [32]byte{10}
zeroHash := params.BeaconConfig().ZeroHash
// Insert parent node into forkchoice.
st, parentROBlock, err := prepareGloasForkchoiceState(ctx, 1, parentRoot, zeroHash, parentBlockHash, zeroHash, 0, 0)
require.NoError(t, err)
require.NoError(t, fcs.InsertNode(ctx, st, parentROBlock))
t.Run("pre-Gloas always true", func(t *testing.T) {
blk := util.HydrateSignedBeaconBlockDeneb(&ethpb.SignedBeaconBlockDeneb{
Block: &ethpb.BeaconBlockDeneb{ParentRoot: parentRoot[:]},
})
wsb, err := blocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
require.Equal(t, true, service.ParentPayloadReady(wsb.Block()))
})
t.Run("parent not in forkchoice", func(t *testing.T) {
unknownParent := [32]byte{99}
bid := util.HydrateSignedExecutionPayloadBid(&ethpb.SignedExecutionPayloadBid{
Message: &ethpb.ExecutionPayloadBid{
BlockHash: []byte{20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
ParentBlockHash: parentBlockHash[:],
},
})
blk := util.HydrateSignedBeaconBlockGloas(&ethpb.SignedBeaconBlockGloas{
Block: &ethpb.BeaconBlockGloas{
Slot: 2,
ParentRoot: unknownParent[:],
Body: &ethpb.BeaconBlockBodyGloas{SignedExecutionPayloadBid: bid},
},
})
wsb, err := blocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
require.Equal(t, false, service.ParentPayloadReady(wsb.Block()))
})
t.Run("builds on empty", func(t *testing.T) {
differentHash := [32]byte{99}
bid := util.HydrateSignedExecutionPayloadBid(&ethpb.SignedExecutionPayloadBid{
Message: &ethpb.ExecutionPayloadBid{
BlockHash: []byte{20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
ParentBlockHash: differentHash[:],
},
})
blk := util.HydrateSignedBeaconBlockGloas(&ethpb.SignedBeaconBlockGloas{
Block: &ethpb.BeaconBlockGloas{
Slot: 2,
ParentRoot: parentRoot[:],
Body: &ethpb.BeaconBlockBodyGloas{SignedExecutionPayloadBid: bid},
},
})
wsb, err := blocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
require.Equal(t, true, service.ParentPayloadReady(wsb.Block()))
})
t.Run("builds on full without payload", func(t *testing.T) {
bid := util.HydrateSignedExecutionPayloadBid(&ethpb.SignedExecutionPayloadBid{
Message: &ethpb.ExecutionPayloadBid{
BlockHash: []byte{20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
ParentBlockHash: parentBlockHash[:],
},
})
blk := util.HydrateSignedBeaconBlockGloas(&ethpb.SignedBeaconBlockGloas{
Block: &ethpb.BeaconBlockGloas{
Slot: 2,
ParentRoot: parentRoot[:],
Body: &ethpb.BeaconBlockBodyGloas{SignedExecutionPayloadBid: bid},
},
})
wsb, err := blocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
require.Equal(t, false, service.ParentPayloadReady(wsb.Block()))
})
t.Run("builds on full with payload", func(t *testing.T) {
pe, err := blocks.WrappedROExecutionPayloadEnvelope(&ethpb.ExecutionPayloadEnvelope{
BeaconBlockRoot: parentRoot[:],
Payload: &enginev1.ExecutionPayloadDeneb{},
})
require.NoError(t, err)
require.NoError(t, fcs.InsertPayload(pe))
bid := util.HydrateSignedExecutionPayloadBid(&ethpb.SignedExecutionPayloadBid{
Message: &ethpb.ExecutionPayloadBid{
BlockHash: []byte{20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
ParentBlockHash: parentBlockHash[:],
},
})
blk := util.HydrateSignedBeaconBlockGloas(&ethpb.SignedBeaconBlockGloas{
Block: &ethpb.BeaconBlockGloas{
Slot: 2,
ParentRoot: parentRoot[:],
Body: &ethpb.BeaconBlockBodyGloas{SignedExecutionPayloadBid: bid},
},
})
wsb, err := blocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
require.Equal(t, true, service.ParentPayloadReady(wsb.Block()))
})
}
func Test_hashForGenesisRoot(t *testing.T) {
beaconDB := testDB.SetupDB(t)
ctx := t.Context()

View File

@@ -101,11 +101,16 @@ func (s *Service) notifyForkchoiceUpdate(ctx context.Context, arg *fcuConfig) (*
if len(lastValidHash) == 0 {
lastValidHash = defaultLatestValidHash
}
invalidRoots, err := s.cfg.ForkChoiceStore.SetOptimisticToInvalid(ctx, headRoot, headBlk.ParentRoot(), bytesutil.ToBytes32(lastValidHash))
// this call has guaranteed to have the `headRoot` with its payload in forkchoice.
invalidRoots, err := s.cfg.ForkChoiceStore.SetOptimisticToInvalid(ctx, headRoot, headBlk.ParentRoot(), bytesutil.ToBytes32(headPayload.ParentHash()), bytesutil.ToBytes32(lastValidHash))
if err != nil {
log.WithError(err).Error("Could not set head root to invalid")
return nil, nil
}
// TODO: Gloas, we should not include the head root in this call
if len(invalidRoots) == 0 || invalidRoots[0] != headRoot {
invalidRoots = append([][32]byte{headRoot}, invalidRoots...)
}
if err := s.removeInvalidBlockAndState(ctx, invalidRoots); err != nil {
log.WithError(err).Error("Could not remove invalid block and state")
return nil, nil
@@ -228,6 +233,9 @@ func (s *Service) notifyNewPayload(ctx context.Context, stVersion int, header in
if stVersion < version.Bellatrix {
return true, nil
}
if blk.Version() >= version.Gloas {
return false, nil
}
body := blk.Block().Body()
enabled, err := blocks.IsExecutionEnabledUsingHeader(header, body)
if err != nil {
@@ -290,10 +298,10 @@ func (s *Service) notifyNewPayload(ctx context.Context, stVersion int, header in
return false, errors.WithMessage(ErrUndefinedExecutionEngineError, err.Error())
}
// reportInvalidBlock deals with the event that an invalid block was detected by the execution layer
func (s *Service) pruneInvalidBlock(ctx context.Context, root, parentRoot, lvh [32]byte) error {
// pruneInvalidBlock deals with the event that an invalid block was detected by the execution layer
func (s *Service) pruneInvalidBlock(ctx context.Context, root, parentRoot, parentHash [32]byte, lvh [32]byte) error {
newPayloadInvalidNodeCount.Inc()
invalidRoots, err := s.cfg.ForkChoiceStore.SetOptimisticToInvalid(ctx, root, parentRoot, lvh)
invalidRoots, err := s.cfg.ForkChoiceStore.SetOptimisticToInvalid(ctx, root, parentRoot, parentHash, lvh)
if err != nil {
return err
}
@@ -313,7 +321,7 @@ func (s *Service) pruneInvalidBlock(ctx context.Context, root, parentRoot, lvh [
// getPayloadAttributes returns the payload attributes for the given state and slot.
// The attribute is required to initiate a payload build process in the context of an `engine_forkchoiceUpdated` call.
func (s *Service) getPayloadAttribute(ctx context.Context, st state.BeaconState, slot primitives.Slot, headRoot []byte) payloadattribute.Attributer {
func (s *Service) getPayloadAttribute(ctx context.Context, st state.BeaconState, slot primitives.Slot, headRoot, accessRoot []byte) payloadattribute.Attributer {
emptyAttri := payloadattribute.EmptyWithVersion(st.Version())
// If it is an epoch boundary then process slots to get the right
@@ -335,7 +343,7 @@ func (s *Service) getPayloadAttribute(ctx context.Context, st state.BeaconState,
// right proposer index pre-Fulu, either way we need to copy the state to process it.
st = st.Copy()
var err error
st, err = transition.ProcessSlotsUsingNextSlotCache(ctx, st, headRoot, slot)
st, err = transition.ProcessSlotsUsingNextSlotCache(ctx, st, accessRoot, slot)
if err != nil {
log.WithError(err).Error("Could not process slots to get payload attribute")
return emptyAttri

View File

@@ -429,9 +429,9 @@ func Test_NotifyForkchoiceUpdateRecursive_DoublyLinkedTree(t *testing.T) {
// Insert Attestations to D, F and G so that they have higher weight than D
// Ensure G is head
fcs.ProcessAttestation(ctx, []uint64{0}, brd, 1)
fcs.ProcessAttestation(ctx, []uint64{1}, brf, 1)
fcs.ProcessAttestation(ctx, []uint64{2}, brg, 1)
fcs.ProcessAttestation(ctx, []uint64{0}, brd, params.BeaconConfig().SlotsPerEpoch, true)
fcs.ProcessAttestation(ctx, []uint64{1}, brf, params.BeaconConfig().SlotsPerEpoch, true)
fcs.ProcessAttestation(ctx, []uint64{2}, brg, params.BeaconConfig().SlotsPerEpoch, true)
fcs.SetBalancesByRooter(service.cfg.StateGen.ActiveNonSlashedBalancesByRoot)
jc := &forkchoicetypes.Checkpoint{Epoch: 0, Root: bra}
require.NoError(t, fcs.UpdateJustifiedCheckpoint(ctx, jc))
@@ -465,9 +465,9 @@ func Test_NotifyForkchoiceUpdateRecursive_DoublyLinkedTree(t *testing.T) {
require.NoError(t, err)
require.Equal(t, brd, headRoot)
// Ensure F and G where removed but their parent E wasn't
require.Equal(t, false, fcs.HasNode(brf))
require.Equal(t, false, fcs.HasNode(brg))
// Ensure F and G's full nodes were removed but their empty (consensus) nodes remain, as does E
require.Equal(t, true, fcs.HasNode(brf))
require.Equal(t, true, fcs.HasNode(brg))
require.Equal(t, true, fcs.HasNode(bre))
}
@@ -703,14 +703,13 @@ func Test_reportInvalidBlock(t *testing.T) {
require.NoError(t, fcs.InsertNode(ctx, st, root))
require.NoError(t, fcs.SetOptimisticToValid(ctx, [32]byte{'A'}))
err = service.pruneInvalidBlock(ctx, [32]byte{'D'}, [32]byte{'C'}, [32]byte{'a'})
err = service.pruneInvalidBlock(ctx, [32]byte{'D'}, [32]byte{'C'}, [32]byte{'c'}, [32]byte{'a'})
require.Equal(t, IsInvalidBlock(err), true)
require.Equal(t, InvalidBlockLVH(err), [32]byte{'a'})
invalidRoots := InvalidAncestorRoots(err)
require.Equal(t, 3, len(invalidRoots))
require.Equal(t, 2, len(invalidRoots))
require.Equal(t, [32]byte{'D'}, invalidRoots[0])
require.Equal(t, [32]byte{'C'}, invalidRoots[1])
require.Equal(t, [32]byte{'B'}, invalidRoots[2])
}
func Test_GetPayloadAttribute(t *testing.T) {
@@ -718,14 +717,14 @@ func Test_GetPayloadAttribute(t *testing.T) {
ctx := tr.ctx
st, _ := util.DeterministicGenesisStateBellatrix(t, 1)
attr := service.getPayloadAttribute(ctx, st, 0, []byte{})
attr := service.getPayloadAttribute(ctx, st, 0, []byte{}, []byte{})
require.Equal(t, true, attr.IsEmpty())
service.cfg.TrackedValidatorsCache.Set(cache.TrackedValidator{Active: true, Index: 0})
// Cache hit, advance state, no fee recipient
slot := primitives.Slot(1)
service.cfg.PayloadIDCache.Set(slot, [32]byte{}, [8]byte{})
attr = service.getPayloadAttribute(ctx, st, slot, params.BeaconConfig().ZeroHash[:])
attr = service.getPayloadAttribute(ctx, st, slot, params.BeaconConfig().ZeroHash[:], params.BeaconConfig().ZeroHash[:])
require.Equal(t, false, attr.IsEmpty())
require.Equal(t, params.BeaconConfig().EthBurnAddressHex, common.BytesToAddress(attr.SuggestedFeeRecipient()).String())
@@ -733,7 +732,7 @@ func Test_GetPayloadAttribute(t *testing.T) {
suggestedAddr := common.HexToAddress("123")
service.cfg.TrackedValidatorsCache.Set(cache.TrackedValidator{Active: true, FeeRecipient: primitives.ExecutionAddress(suggestedAddr), Index: 0})
service.cfg.PayloadIDCache.Set(slot, [32]byte{}, [8]byte{})
attr = service.getPayloadAttribute(ctx, st, slot, params.BeaconConfig().ZeroHash[:])
attr = service.getPayloadAttribute(ctx, st, slot, params.BeaconConfig().ZeroHash[:], params.BeaconConfig().ZeroHash[:])
require.Equal(t, false, attr.IsEmpty())
require.Equal(t, suggestedAddr, common.BytesToAddress(attr.SuggestedFeeRecipient()))
}
@@ -748,7 +747,7 @@ func Test_GetPayloadAttribute_PrepareAllPayloads(t *testing.T) {
ctx := tr.ctx
st, _ := util.DeterministicGenesisStateBellatrix(t, 1)
attr := service.getPayloadAttribute(ctx, st, 0, []byte{})
attr := service.getPayloadAttribute(ctx, st, 0, []byte{}, []byte{})
require.Equal(t, false, attr.IsEmpty())
require.Equal(t, params.BeaconConfig().EthBurnAddressHex, common.BytesToAddress(attr.SuggestedFeeRecipient()).String())
}
@@ -758,14 +757,14 @@ func Test_GetPayloadAttributeV2(t *testing.T) {
ctx := tr.ctx
st, _ := util.DeterministicGenesisStateCapella(t, 1)
attr := service.getPayloadAttribute(ctx, st, 0, []byte{})
attr := service.getPayloadAttribute(ctx, st, 0, []byte{}, []byte{})
require.Equal(t, true, attr.IsEmpty())
// Cache hit, advance state, no fee recipient
service.cfg.TrackedValidatorsCache.Set(cache.TrackedValidator{Active: true, Index: 0})
slot := primitives.Slot(1)
service.cfg.PayloadIDCache.Set(slot, [32]byte{}, [8]byte{})
attr = service.getPayloadAttribute(ctx, st, slot, params.BeaconConfig().ZeroHash[:])
attr = service.getPayloadAttribute(ctx, st, slot, params.BeaconConfig().ZeroHash[:], params.BeaconConfig().ZeroHash[:])
require.Equal(t, false, attr.IsEmpty())
require.Equal(t, params.BeaconConfig().EthBurnAddressHex, common.BytesToAddress(attr.SuggestedFeeRecipient()).String())
a, err := attr.Withdrawals()
@@ -776,7 +775,7 @@ func Test_GetPayloadAttributeV2(t *testing.T) {
suggestedAddr := common.HexToAddress("123")
service.cfg.TrackedValidatorsCache.Set(cache.TrackedValidator{Active: true, FeeRecipient: primitives.ExecutionAddress(suggestedAddr), Index: 0})
service.cfg.PayloadIDCache.Set(slot, [32]byte{}, [8]byte{})
attr = service.getPayloadAttribute(ctx, st, slot, params.BeaconConfig().ZeroHash[:])
attr = service.getPayloadAttribute(ctx, st, slot, params.BeaconConfig().ZeroHash[:], params.BeaconConfig().ZeroHash[:])
require.Equal(t, false, attr.IsEmpty())
require.Equal(t, suggestedAddr, common.BytesToAddress(attr.SuggestedFeeRecipient()))
a, err = attr.Withdrawals()
@@ -785,7 +784,7 @@ func Test_GetPayloadAttributeV2(t *testing.T) {
}
func Test_GetPayloadAttributeV3(t *testing.T) {
var testCases = []struct {
testCases := []struct {
name string
st bstate.BeaconState
}{
@@ -810,14 +809,14 @@ func Test_GetPayloadAttributeV3(t *testing.T) {
service, tr := minimalTestService(t, WithPayloadIDCache(cache.NewPayloadIDCache()))
ctx := tr.ctx
attr := service.getPayloadAttribute(ctx, test.st, 0, []byte{})
attr := service.getPayloadAttribute(ctx, test.st, 0, []byte{}, []byte{})
require.Equal(t, true, attr.IsEmpty())
// Cache hit, advance state, no fee recipient
slot := primitives.Slot(1)
service.cfg.TrackedValidatorsCache.Set(cache.TrackedValidator{Active: true, Index: 0})
service.cfg.PayloadIDCache.Set(slot, [32]byte{}, [8]byte{})
attr = service.getPayloadAttribute(ctx, test.st, slot, params.BeaconConfig().ZeroHash[:])
attr = service.getPayloadAttribute(ctx, test.st, slot, params.BeaconConfig().ZeroHash[:], params.BeaconConfig().ZeroHash[:])
require.Equal(t, false, attr.IsEmpty())
require.Equal(t, params.BeaconConfig().EthBurnAddressHex, common.BytesToAddress(attr.SuggestedFeeRecipient()).String())
a, err := attr.Withdrawals()
@@ -828,7 +827,7 @@ func Test_GetPayloadAttributeV3(t *testing.T) {
suggestedAddr := common.HexToAddress("123")
service.cfg.TrackedValidatorsCache.Set(cache.TrackedValidator{Active: true, FeeRecipient: primitives.ExecutionAddress(suggestedAddr), Index: 0})
service.cfg.PayloadIDCache.Set(slot, [32]byte{}, [8]byte{})
attr = service.getPayloadAttribute(ctx, test.st, slot, params.BeaconConfig().ZeroHash[:])
attr = service.getPayloadAttribute(ctx, test.st, slot, params.BeaconConfig().ZeroHash[:], params.BeaconConfig().ZeroHash[:])
require.Equal(t, false, attr.IsEmpty())
require.Equal(t, suggestedAddr, common.BytesToAddress(attr.SuggestedFeeRecipient()))
a, err = attr.Withdrawals()

View File

@@ -18,19 +18,21 @@ import (
"github.com/sirupsen/logrus"
)
func (s *Service) isNewHead(r [32]byte) bool {
func (s *Service) isNewHead(r [32]byte, full bool) bool {
s.headLock.RLock()
defer s.headLock.RUnlock()
currentHeadRoot := s.originBlockRoot
currentFull := false
if s.head != nil {
currentHeadRoot = s.headRoot()
currentFull = s.head.full
}
return r != currentHeadRoot || r == [32]byte{}
return r != currentHeadRoot || full != currentFull || r == [32]byte{}
}
func (s *Service) getStateAndBlock(ctx context.Context, r [32]byte) (state.BeaconState, interfaces.ReadOnlySignedBeaconBlock, error) {
func (s *Service) getStateAndBlock(ctx context.Context, r, h [32]byte) (state.BeaconState, interfaces.ReadOnlySignedBeaconBlock, error) {
if !s.hasBlockInInitSyncOrDB(ctx, r) {
return nil, nil, errors.New("block does not exist")
}
@@ -38,7 +40,7 @@ func (s *Service) getStateAndBlock(ctx context.Context, r [32]byte) (state.Beaco
if err != nil {
return nil, nil, err
}
headState, err := s.cfg.StateGen.StateByRoot(ctx, r)
headState, err := s.cfg.StateGen.StateByRoot(ctx, h)
if err != nil {
return nil, nil, err
}
@@ -56,7 +58,7 @@ type fcuConfig struct {
// sendFCU handles the logic to notify the engine of a forckhoice update
// when processing an incoming block during regular sync. It
// always updates the shuffling caches and handles epoch transitions .
func (s *Service) sendFCU(cfg *postBlockProcessConfig, fcuArgs *fcuConfig) {
func (s *Service) sendFCU(cfg *postBlockProcessConfig) {
if cfg.postState.Version() < version.Fulu {
// update the caches to compute the right proposer index
// this function is called under a forkchoice lock which we need to release.
@@ -64,12 +66,13 @@ func (s *Service) sendFCU(cfg *postBlockProcessConfig, fcuArgs *fcuConfig) {
s.updateCachesPostBlockProcessing(cfg)
s.ForkChoicer().Lock()
}
if err := s.getFCUArgs(cfg, fcuArgs); err != nil {
fcuArgs, err := s.getFCUArgs(cfg)
if err != nil {
log.WithError(err).Error("Could not get forkchoice update argument")
return
}
// If head has not been updated and attributes are nil, we can skip the FCU.
if !s.isNewHead(cfg.headRoot) && (fcuArgs.attributes == nil || fcuArgs.attributes.IsEmpty()) {
if !s.isNewHead(cfg.headRoot, false) && (fcuArgs.attributes == nil || fcuArgs.attributes.IsEmpty()) {
return
}
// If we are proposing and we aim to reorg the block, we have already sent FCU with attributes on lateBlockTasks
@@ -80,7 +83,7 @@ func (s *Service) sendFCU(cfg *postBlockProcessConfig, fcuArgs *fcuConfig) {
go s.forkchoiceUpdateWithExecution(cfg.ctx, fcuArgs)
}
if s.isNewHead(fcuArgs.headRoot) {
if s.isNewHead(fcuArgs.headRoot, false) {
if err := s.saveHead(cfg.ctx, fcuArgs.headRoot, fcuArgs.headBlock, fcuArgs.headState); err != nil {
log.WithError(err).Error("Could not save head")
}

View File

@@ -19,23 +19,42 @@ import (
func TestService_isNewHead(t *testing.T) {
beaconDB := testDB.SetupDB(t)
service := setupBeaconChain(t, beaconDB)
require.Equal(t, true, service.isNewHead([32]byte{}))
// Zero root is always a new head
require.Equal(t, true, service.isNewHead([32]byte{}, false))
require.Equal(t, true, service.isNewHead([32]byte{}, true))
// Different root is a new head
service.head = &head{root: [32]byte{1}}
require.Equal(t, true, service.isNewHead([32]byte{2}))
require.Equal(t, false, service.isNewHead([32]byte{1}))
require.Equal(t, true, service.isNewHead([32]byte{2}, false))
// Same root and same full status is not a new head
require.Equal(t, false, service.isNewHead([32]byte{1}, false))
// Same root but different full status is a new head
require.Equal(t, true, service.isNewHead([32]byte{1}, true))
// Same root and both full is not a new head
service.head = &head{root: [32]byte{1}, full: true}
require.Equal(t, false, service.isNewHead([32]byte{1}, true))
// Same root, head is full but incoming is not full, is a new head
require.Equal(t, true, service.isNewHead([32]byte{1}, false))
// Nil head should use origin root
service.head = nil
service.originBlockRoot = [32]byte{3}
require.Equal(t, true, service.isNewHead([32]byte{2}))
require.Equal(t, false, service.isNewHead([32]byte{3}))
require.Equal(t, true, service.isNewHead([32]byte{2}, false))
require.Equal(t, false, service.isNewHead([32]byte{3}, false))
// Nil head with full=true is always a new head (originBlockRoot has full=false)
require.Equal(t, true, service.isNewHead([32]byte{3}, true))
}
func TestService_getHeadStateAndBlock(t *testing.T) {
beaconDB := testDB.SetupDB(t)
service := setupBeaconChain(t, beaconDB)
_, _, err := service.getStateAndBlock(t.Context(), [32]byte{})
_, _, err := service.getStateAndBlock(t.Context(), [32]byte{}, [32]byte{})
require.ErrorContains(t, "block does not exist", err)
blk, err := blocks.NewSignedBeaconBlock(util.HydrateSignedBeaconBlock(&ethpb.SignedBeaconBlock{Signature: []byte{1}}))

View File

@@ -0,0 +1,213 @@
package blockchain
import (
"context"
"math"
"github.com/OffchainLabs/prysm/v7/beacon-chain/cache"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/time"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/transition"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"github.com/OffchainLabs/prysm/v7/config/params"
consensus_blocks "github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
payloadattribute "github.com/OffchainLabs/prysm/v7/consensus-types/payload-attribute"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
"github.com/OffchainLabs/prysm/v7/runtime/version"
"github.com/OffchainLabs/prysm/v7/time/slots"
"github.com/pkg/errors"
)
func (s *Service) waitUntilEpoch(target primitives.Epoch, secondsPerSlot uint64) error {
if slots.ToEpoch(s.CurrentSlot()) >= target {
return nil
}
ticker := slots.NewSlotTicker(s.genesisTime, secondsPerSlot)
defer ticker.Done()
for {
select {
case slot := <-ticker.C():
if slots.ToEpoch(slot) >= target {
return nil
}
case <-s.ctx.Done():
return s.ctx.Err()
}
}
}
// getLookupParentRoot returns the root that serves as key to generate the parent state for the passed beacon block.
// if it is based on empty or it is pre-Gloas, it is the parent root of the block, otherwise if it is based on full it is
// the parent hash.
// The caller of this function should not hold a lock on forkchoice.
func (s *Service) getLookupParentRoot(b consensus_blocks.ROBlock) ([32]byte, error) {
bl := b.Block()
parentRoot := bl.ParentRoot()
if b.Version() < version.Gloas {
return parentRoot, nil
}
parentSlot, err := s.cfg.ForkChoiceStore.Slot(parentRoot)
if err != nil {
return [32]byte{}, errors.Wrap(err, "failed to get slot for parent root")
}
if slots.ToEpoch(parentSlot) < params.BeaconConfig().GloasForkEpoch {
return parentRoot, nil
}
blockHash, err := s.cfg.ForkChoiceStore.BlockHash(parentRoot)
if err != nil {
return [32]byte{}, errors.Wrap(err, "failed to get block hash for parent root")
}
bid, err := bl.Body().SignedExecutionPayloadBid()
if err != nil {
return [32]byte{}, errors.Wrap(err, "failed to get signed execution payload bid from block body")
}
if bid == nil || bid.Message == nil || len(bid.Message.ParentBlockHash) != 32 {
return [32]byte{}, errors.New("invalid signed execution payload bid message")
}
parentHash := [32]byte(bid.Message.ParentBlockHash)
if blockHash == parentHash {
return parentHash, nil
}
return parentRoot, nil
}
func (s *Service) runLatePayloadTasks() {
if err := s.waitForSync(); err != nil {
log.WithError(err).Error("Failed to wait for initial sync")
return
}
cfg := params.BeaconConfig()
if cfg.GloasForkEpoch == math.MaxUint64 {
return
}
if err := s.waitUntilEpoch(cfg.GloasForkEpoch, cfg.SecondsPerSlot); err != nil {
return
}
offset := cfg.SlotComponentDuration(cfg.PayloadAttestationDueBPS)
ticker := slots.NewSlotTickerWithOffset(s.genesisTime, offset, cfg.SecondsPerSlot)
defer ticker.Done()
for {
select {
case <-ticker.C():
s.latePayloadTasks(s.ctx)
case <-s.ctx.Done():
log.Debug("Context closed, exiting late payload tasks routine")
return
}
}
}
func (s *Service) checkIfProposing(st state.ReadOnlyBeaconState, slot primitives.Slot) (cache.TrackedValidator, bool) {
e := slots.ToEpoch(slot)
stateEpoch := slots.ToEpoch(st.Slot())
fuluAndNextEpoch := st.Version() >= version.Fulu && e == stateEpoch+1
if e == stateEpoch || fuluAndNextEpoch {
return s.trackedProposer(st, slot)
}
return cache.TrackedValidator{}, false
}
// This is a Gloas version of getPayloadAttribute that avoids all the clutter that was originally due to the proposer Index.
// It is guaranteed to be called for the current slot + 1 and the head state to have been advanced to at least the current epoch.
func (s *Service) getPayloadAttributeGloas(ctx context.Context, st state.ReadOnlyBeaconState, slot primitives.Slot, headRoot, accessRoot []byte) payloadattribute.Attributer {
emptyAttri := payloadattribute.EmptyWithVersion(st.Version())
val, proposing := s.checkIfProposing(st, slot)
if !proposing {
return emptyAttri
}
st, err := transition.ProcessSlotsIfNeeded(ctx, st, accessRoot, slot)
if err != nil {
log.WithError(err).Error("Could not process slots to get payload attribute")
return emptyAttri
}
// Get previous randao.
prevRando, err := helpers.RandaoMix(st, time.CurrentEpoch(st))
if err != nil {
log.WithError(err).Error("Could not get randao mix to get payload attribute")
return emptyAttri
}
// Get timestamp.
t, err := slots.StartTime(s.genesisTime, slot)
if err != nil {
log.WithError(err).Error("Could not get timestamp to get payload attribute")
return emptyAttri
}
withdrawals, _, err := st.ExpectedWithdrawals()
if err != nil {
log.WithError(err).Error("Could not get expected withdrawals to get payload attribute")
return emptyAttri
}
attr, err := payloadattribute.New(&enginev1.PayloadAttributesV3{
Timestamp: uint64(t.Unix()),
PrevRandao: prevRando,
SuggestedFeeRecipient: val.FeeRecipient[:],
Withdrawals: withdrawals,
ParentBeaconBlockRoot: headRoot,
})
if err != nil {
log.WithError(err).Error("Could not get payload attribute")
return emptyAttri
}
return attr
}
// latePayloadTasks updates the NSC and epoch boundary caches when there is no payload in the current slot (and there is a block)
// The case where the block was also missing would have been dealt by lateBlockTasks already.
// We call FCU only if we are proposing next slot, as the execution head is assumed to not have changed.
func (s *Service) latePayloadTasks(ctx context.Context) {
currentSlot := s.CurrentSlot()
if currentSlot != s.HeadSlot() {
// We must've already sent a FCU and updated the caches in lateBlockTaks.
return
}
r, err := s.HeadRoot(ctx)
if err != nil {
log.WithError(err).Error("Failed to get head root")
return
}
hr := [32]byte(r)
if s.payloadBeingSynced.isSyncing(hr) {
return
}
if s.HasFullNode(hr) {
return
}
st, err := s.HeadStateReadOnly(ctx)
if err != nil {
log.WithError(err).Error("Failed to get head state")
return
}
if !s.inRegularSync() {
return
}
attr := s.getPayloadAttributeGloas(ctx, st, currentSlot+1, r, r)
if attr == nil || attr.IsEmpty() {
return
}
beaconLatePayloadTaskTriggeredTotal.Inc()
// Head is the empty block.
bh, err := st.LatestBlockHash()
if err != nil {
log.WithError(err).Error("Could not get latest block hash to notify engine")
return
}
pid, err := s.notifyForkchoiceUpdateGloas(ctx, bh, attr)
if err != nil {
log.WithError(err).Error("Could not notify forkchoice update")
return
}
if pid == nil {
log.Warn("Received nil payload ID from forkchoice update.")
return
}
var pId [8]byte
copy(pId[:], pid[:])
s.cfg.PayloadIDCache.Set(currentSlot+1, hr, pId)
}

View File

@@ -0,0 +1,940 @@
package blockchain
import (
"context"
"testing"
"time"
"github.com/OffchainLabs/prysm/v7/beacon-chain/cache"
"github.com/OffchainLabs/prysm/v7/beacon-chain/execution"
mockExecution "github.com/OffchainLabs/prysm/v7/beacon-chain/execution/testing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
state_native "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native"
"github.com/OffchainLabs/prysm/v7/config/features"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
payloadattribute "github.com/OffchainLabs/prysm/v7/consensus-types/payload-attribute"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/runtime/version"
"github.com/OffchainLabs/prysm/v7/testing/require"
"github.com/OffchainLabs/prysm/v7/testing/util"
"github.com/OffchainLabs/prysm/v7/time/slots"
logTest "github.com/sirupsen/logrus/hooks/test"
)
func prepareGloasForkchoiceState(
_ context.Context,
slot primitives.Slot,
blockRoot [32]byte,
parentRoot [32]byte,
blockHash [32]byte,
parentBlockHash [32]byte,
justifiedEpoch primitives.Epoch,
finalizedEpoch primitives.Epoch,
) (state.BeaconState, blocks.ROBlock, error) {
blockHeader := &ethpb.BeaconBlockHeader{
ParentRoot: parentRoot[:],
}
justifiedCheckpoint := &ethpb.Checkpoint{
Epoch: justifiedEpoch,
}
finalizedCheckpoint := &ethpb.Checkpoint{
Epoch: finalizedEpoch,
}
builderPendingPayments := make([]*ethpb.BuilderPendingPayment, 64)
for i := range builderPendingPayments {
builderPendingPayments[i] = &ethpb.BuilderPendingPayment{
Withdrawal: &ethpb.BuilderPendingWithdrawal{
FeeRecipient: make([]byte, 20),
},
}
}
base := &ethpb.BeaconStateGloas{
Slot: slot,
RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector),
CurrentJustifiedCheckpoint: justifiedCheckpoint,
FinalizedCheckpoint: finalizedCheckpoint,
LatestBlockHeader: blockHeader,
LatestExecutionPayloadBid: &ethpb.ExecutionPayloadBid{
BlockHash: blockHash[:],
ParentBlockHash: parentBlockHash[:],
ParentBlockRoot: make([]byte, 32),
PrevRandao: make([]byte, 32),
FeeRecipient: make([]byte, 20),
BlobKzgCommitments: [][]byte{make([]byte, 48)},
},
Builders: make([]*ethpb.Builder, 0),
BuilderPendingPayments: builderPendingPayments,
ExecutionPayloadAvailability: make([]byte, 1024),
LatestBlockHash: make([]byte, 32),
PayloadExpectedWithdrawals: make([]*enginev1.Withdrawal, 0),
ProposerLookahead: make([]uint64, 64),
}
st, err := state_native.InitializeFromProtoUnsafeGloas(base)
if err != nil {
return nil, blocks.ROBlock{}, err
}
bid := util.HydrateSignedExecutionPayloadBid(&ethpb.SignedExecutionPayloadBid{
Message: &ethpb.ExecutionPayloadBid{
BlockHash: blockHash[:],
ParentBlockHash: parentBlockHash[:],
},
})
blk := util.HydrateSignedBeaconBlockGloas(&ethpb.SignedBeaconBlockGloas{
Block: &ethpb.BeaconBlockGloas{
Slot: slot,
ParentRoot: parentRoot[:],
Body: &ethpb.BeaconBlockBodyGloas{
SignedExecutionPayloadBid: bid,
},
},
})
signed, err := blocks.NewSignedBeaconBlock(blk)
if err != nil {
return nil, blocks.ROBlock{}, err
}
roblock, err := blocks.NewROBlockWithRoot(signed, blockRoot)
return st, roblock, err
}
func testGloasState(t *testing.T, slot primitives.Slot, parentRoot [32]byte, blockHash [32]byte) (*ethpb.BeaconStateGloas, *ethpb.SignedBeaconBlockGloas) {
t.Helper()
builderPendingPayments := make([]*ethpb.BuilderPendingPayment, 64)
for i := range builderPendingPayments {
builderPendingPayments[i] = &ethpb.BuilderPendingPayment{
Withdrawal: &ethpb.BuilderPendingWithdrawal{FeeRecipient: make([]byte, 20)},
}
}
base := &ethpb.BeaconStateGloas{
Slot: slot,
RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector),
BlockRoots: make([][]byte, params.BeaconConfig().SlotsPerHistoricalRoot),
StateRoots: make([][]byte, params.BeaconConfig().SlotsPerHistoricalRoot),
Slashings: make([]uint64, params.BeaconConfig().EpochsPerSlashingsVector),
CurrentJustifiedCheckpoint: &ethpb.Checkpoint{Root: make([]byte, 32)},
FinalizedCheckpoint: &ethpb.Checkpoint{Root: make([]byte, 32)},
LatestBlockHeader: &ethpb.BeaconBlockHeader{
ParentRoot: parentRoot[:],
StateRoot: make([]byte, 32),
BodyRoot: make([]byte, 32),
},
Eth1Data: &ethpb.Eth1Data{
DepositRoot: make([]byte, 32),
BlockHash: make([]byte, 32),
},
LatestExecutionPayloadBid: &ethpb.ExecutionPayloadBid{
BlockHash: blockHash[:],
ParentBlockHash: make([]byte, 32),
ParentBlockRoot: make([]byte, 32),
PrevRandao: make([]byte, 32),
FeeRecipient: make([]byte, 20),
BlobKzgCommitments: [][]byte{make([]byte, 48)},
},
Builders: make([]*ethpb.Builder, 0),
BuilderPendingPayments: builderPendingPayments,
ExecutionPayloadAvailability: make([]byte, 1024),
LatestBlockHash: make([]byte, 32),
PayloadExpectedWithdrawals: make([]*enginev1.Withdrawal, 0),
ProposerLookahead: make([]uint64, 64),
}
bid := util.HydrateSignedExecutionPayloadBid(&ethpb.SignedExecutionPayloadBid{
Message: &ethpb.ExecutionPayloadBid{
BlockHash: blockHash[:],
ParentBlockHash: make([]byte, 32),
},
})
blk := util.HydrateSignedBeaconBlockGloas(&ethpb.SignedBeaconBlockGloas{
Block: &ethpb.BeaconBlockGloas{
Slot: slot,
ParentRoot: parentRoot[:],
Body: &ethpb.BeaconBlockBodyGloas{SignedExecutionPayloadBid: bid},
},
})
return base, blk
}
func testSignedEnvelope(t *testing.T, blockRoot [32]byte, slot primitives.Slot, blockHash []byte) *ethpb.SignedExecutionPayloadEnvelope {
t.Helper()
return &ethpb.SignedExecutionPayloadEnvelope{
Message: &ethpb.ExecutionPayloadEnvelope{
Payload: &enginev1.ExecutionPayloadDeneb{
ParentHash: make([]byte, 32),
FeeRecipient: make([]byte, 20),
StateRoot: make([]byte, 32),
ReceiptsRoot: make([]byte, 32),
LogsBloom: make([]byte, 256),
PrevRandao: make([]byte, 32),
BaseFeePerGas: make([]byte, 32),
BlockHash: blockHash,
Transactions: [][]byte{},
Withdrawals: []*enginev1.Withdrawal{},
},
ExecutionRequests: &enginev1.ExecutionRequests{},
BuilderIndex: 0,
BeaconBlockRoot: blockRoot[:],
Slot: slot,
StateRoot: make([]byte, 32),
},
Signature: make([]byte, 96),
}
}
func setupGloasService(t *testing.T, engineClient *mockExecution.EngineClient) (*Service, *testServiceRequirements) {
t.Helper()
return minimalTestService(t,
WithPayloadIDCache(cache.NewPayloadIDCache()),
WithExecutionEngineCaller(engineClient),
)
}
func insertGloasBlock(t *testing.T, s *Service, base *ethpb.BeaconStateGloas, blk *ethpb.SignedBeaconBlockGloas, blockRoot [32]byte) {
t.Helper()
ctx := t.Context()
st, err := state_native.InitializeFromProtoUnsafeGloas(base)
require.NoError(t, err)
signed, err := blocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
roblock, err := blocks.NewROBlockWithRoot(signed, blockRoot)
require.NoError(t, err)
require.NoError(t, s.cfg.BeaconDB.SaveBlock(ctx, signed))
require.NoError(t, s.cfg.BeaconDB.SaveStateSummary(ctx, &ethpb.StateSummary{Root: blockRoot[:], Slot: blk.Block.Slot}))
require.NoError(t, s.cfg.StateGen.SaveState(ctx, blockRoot, st))
require.NoError(t, s.InsertNode(ctx, st, roblock))
}
func TestGetPayloadEnvelopePrestate_UnknownRoot(t *testing.T) {
s, _ := setupGloasService(t, &mockExecution.EngineClient{})
ctx := t.Context()
unknownRoot := bytesutil.ToBytes32([]byte("unknown"))
env := &ethpb.ExecutionPayloadEnvelope{
BeaconBlockRoot: unknownRoot[:],
Payload: &enginev1.ExecutionPayloadDeneb{},
}
envelope, err := blocks.WrappedROExecutionPayloadEnvelope(env)
require.NoError(t, err)
_, err = s.getPayloadEnvelopePrestate(ctx, envelope)
require.ErrorContains(t, "not found in forkchoice", err)
}
func TestGetPayloadEnvelopePrestate_OK(t *testing.T) {
s, _ := setupGloasService(t, &mockExecution.EngineClient{})
ctx := t.Context()
blockRoot := bytesutil.ToBytes32([]byte("root1"))
parentRoot := params.BeaconConfig().ZeroHash
blockHash := bytesutil.ToBytes32([]byte("hash1"))
base, blk := testGloasState(t, 1, parentRoot, blockHash)
insertGloasBlock(t, s, base, blk, blockRoot)
env := &ethpb.ExecutionPayloadEnvelope{
BeaconBlockRoot: blockRoot[:],
Payload: &enginev1.ExecutionPayloadDeneb{},
}
envelope, err := blocks.WrappedROExecutionPayloadEnvelope(env)
require.NoError(t, err)
st, err := s.getPayloadEnvelopePrestate(ctx, envelope)
require.NoError(t, err)
require.Equal(t, primitives.Slot(1), st.Slot())
}
func TestNotifyNewEnvelope_Valid(t *testing.T) {
s, _ := setupGloasService(t, &mockExecution.EngineClient{})
ctx := t.Context()
blockRoot := bytesutil.ToBytes32([]byte("root1"))
parentRoot := params.BeaconConfig().ZeroHash
blockHash := bytesutil.ToBytes32([]byte("hash1"))
base, _ := testGloasState(t, 1, parentRoot, blockHash)
st, err := state_native.InitializeFromProtoUnsafeGloas(base)
require.NoError(t, err)
env := &ethpb.ExecutionPayloadEnvelope{
BeaconBlockRoot: blockRoot[:],
Payload: &enginev1.ExecutionPayloadDeneb{BlockHash: blockHash[:]},
ExecutionRequests: &enginev1.ExecutionRequests{},
Slot: 1,
}
envelope, err := blocks.WrappedROExecutionPayloadEnvelope(env)
require.NoError(t, err)
isValid, err := s.notifyNewEnvelope(ctx, st, envelope)
require.NoError(t, err)
require.Equal(t, true, isValid)
}
func TestNotifyNewEnvelope_Syncing(t *testing.T) {
s, _ := setupGloasService(t, &mockExecution.EngineClient{
ErrNewPayload: execution.ErrAcceptedSyncingPayloadStatus,
})
ctx := t.Context()
blockRoot := bytesutil.ToBytes32([]byte("root1"))
parentRoot := params.BeaconConfig().ZeroHash
blockHash := bytesutil.ToBytes32([]byte("hash1"))
base, _ := testGloasState(t, 1, parentRoot, blockHash)
st, err := state_native.InitializeFromProtoUnsafeGloas(base)
require.NoError(t, err)
env := &ethpb.ExecutionPayloadEnvelope{
BeaconBlockRoot: blockRoot[:],
Payload: &enginev1.ExecutionPayloadDeneb{BlockHash: blockHash[:]},
ExecutionRequests: &enginev1.ExecutionRequests{},
Slot: 1,
}
envelope, err := blocks.WrappedROExecutionPayloadEnvelope(env)
require.NoError(t, err)
isValid, err := s.notifyNewEnvelope(ctx, st, envelope)
require.NoError(t, err)
require.Equal(t, false, isValid)
}
func TestNotifyNewEnvelope_Invalid(t *testing.T) {
s, _ := setupGloasService(t, &mockExecution.EngineClient{
ErrNewPayload: execution.ErrInvalidPayloadStatus,
})
ctx := t.Context()
blockRoot := bytesutil.ToBytes32([]byte("root1"))
parentRoot := params.BeaconConfig().ZeroHash
blockHash := bytesutil.ToBytes32([]byte("hash1"))
base, _ := testGloasState(t, 1, parentRoot, blockHash)
st, err := state_native.InitializeFromProtoUnsafeGloas(base)
require.NoError(t, err)
env := &ethpb.ExecutionPayloadEnvelope{
BeaconBlockRoot: blockRoot[:],
Payload: &enginev1.ExecutionPayloadDeneb{BlockHash: blockHash[:]},
ExecutionRequests: &enginev1.ExecutionRequests{},
Slot: 1,
}
envelope, err := blocks.WrappedROExecutionPayloadEnvelope(env)
require.NoError(t, err)
_, err = s.notifyNewEnvelope(ctx, st, envelope)
require.Equal(t, true, IsInvalidBlock(err))
}
func TestNotifyForkchoiceUpdateGloas_Valid(t *testing.T) {
pid := &enginev1.PayloadIDBytes{1, 2, 3, 4, 5, 6, 7, 8}
s, _ := setupGloasService(t, &mockExecution.EngineClient{PayloadIDBytes: pid})
ctx := t.Context()
blockHash := bytesutil.ToBytes32([]byte("hash1"))
attr := payloadattribute.EmptyWithVersion(version.Gloas)
retPid, err := s.notifyForkchoiceUpdateGloas(ctx, blockHash, attr)
require.NoError(t, err)
require.DeepEqual(t, pid, retPid)
}
func TestNotifyForkchoiceUpdateGloas_Syncing(t *testing.T) {
s, _ := setupGloasService(t, &mockExecution.EngineClient{
ErrForkchoiceUpdated: execution.ErrAcceptedSyncingPayloadStatus,
})
ctx := t.Context()
blockHash := bytesutil.ToBytes32([]byte("hash1"))
_, err := s.notifyForkchoiceUpdateGloas(ctx, blockHash, nil)
require.NoError(t, err)
}
func TestNotifyForkchoiceUpdateGloas_Invalid(t *testing.T) {
s, _ := setupGloasService(t, &mockExecution.EngineClient{
ErrForkchoiceUpdated: execution.ErrInvalidPayloadStatus,
})
ctx := t.Context()
blockHash := bytesutil.ToBytes32([]byte("hash1"))
_, err := s.notifyForkchoiceUpdateGloas(ctx, blockHash, nil)
require.Equal(t, true, IsInvalidBlock(err))
}
func TestNotifyForkchoiceUpdateGloas_NilAttributes(t *testing.T) {
s, _ := setupGloasService(t, &mockExecution.EngineClient{})
ctx := t.Context()
blockHash := bytesutil.ToBytes32([]byte("hash1"))
_, err := s.notifyForkchoiceUpdateGloas(ctx, blockHash, nil)
require.NoError(t, err)
}
func TestSavePostPayload(t *testing.T) {
s, _ := setupGloasService(t, &mockExecution.EngineClient{})
ctx := t.Context()
blockRoot := bytesutil.ToBytes32([]byte("root1"))
blockHash := bytesutil.ToBytes32([]byte("hash1"))
base, _ := testGloasState(t, 1, params.BeaconConfig().ZeroHash, blockHash)
st, err := state_native.InitializeFromProtoUnsafeGloas(base)
require.NoError(t, err)
protoEnv := testSignedEnvelope(t, blockRoot, 1, blockHash[:])
signed, err := blocks.WrappedROSignedExecutionPayloadEnvelope(protoEnv)
require.NoError(t, err)
require.NoError(t, s.savePostPayload(ctx, signed, st))
// Verify the envelope was saved in the DB.
require.Equal(t, true, s.cfg.BeaconDB.HasExecutionPayloadEnvelope(ctx, blockRoot))
}
func TestValidateExecutionOnEnvelope_Valid(t *testing.T) {
s, _ := setupGloasService(t, &mockExecution.EngineClient{})
ctx := t.Context()
blockRoot := bytesutil.ToBytes32([]byte("root1"))
parentRoot := params.BeaconConfig().ZeroHash
blockHash := bytesutil.ToBytes32([]byte("hash1"))
base, _ := testGloasState(t, 1, parentRoot, blockHash)
st, err := state_native.InitializeFromProtoUnsafeGloas(base)
require.NoError(t, err)
env := &ethpb.ExecutionPayloadEnvelope{
BeaconBlockRoot: blockRoot[:],
Payload: &enginev1.ExecutionPayloadDeneb{BlockHash: blockHash[:], ParentHash: make([]byte, 32)},
ExecutionRequests: &enginev1.ExecutionRequests{},
Slot: 1,
}
envelope, err := blocks.WrappedROExecutionPayloadEnvelope(env)
require.NoError(t, err)
isValid, err := s.validateExecutionOnEnvelope(ctx, st, envelope)
require.NoError(t, err)
require.Equal(t, true, isValid)
}
func TestPostPayloadHeadUpdate_NotHead(t *testing.T) {
s, _ := setupGloasService(t, &mockExecution.EngineClient{})
ctx := t.Context()
root := bytesutil.ToBytes32([]byte("root1"))
headRoot := bytesutil.ToBytes32([]byte("different"))
blockHash := bytesutil.ToBytes32([]byte("hash1"))
base, _ := testGloasState(t, 1, params.BeaconConfig().ZeroHash, blockHash)
st, err := state_native.InitializeFromProtoUnsafeGloas(base)
require.NoError(t, err)
env := &ethpb.ExecutionPayloadEnvelope{
BeaconBlockRoot: root[:],
Payload: &enginev1.ExecutionPayloadDeneb{BlockHash: blockHash[:]},
Slot: 1,
}
envelope, err := blocks.WrappedROExecutionPayloadEnvelope(env)
require.NoError(t, err)
require.NoError(t, s.postPayloadHeadUpdate(ctx, envelope, st, root, headRoot[:]))
}
func TestPostPayloadHeadUpdate_SetsHeadFull(t *testing.T) {
s, _ := setupGloasService(t, &mockExecution.EngineClient{})
ctx := t.Context()
root := bytesutil.ToBytes32([]byte("root1"))
blockHash := bytesutil.ToBytes32([]byte("hash1"))
base, blk := testGloasState(t, 1, params.BeaconConfig().ZeroHash, blockHash)
st, err := state_native.InitializeFromProtoUnsafeGloas(base)
require.NoError(t, err)
signed, err := blocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
s.head = &head{root: root, block: signed, state: st, slot: 1}
require.Equal(t, false, s.head.full)
env := &ethpb.ExecutionPayloadEnvelope{
BeaconBlockRoot: root[:],
Payload: &enginev1.ExecutionPayloadDeneb{BlockHash: blockHash[:], ParentHash: make([]byte, 32)},
Slot: 1,
}
envelope, err := blocks.WrappedROExecutionPayloadEnvelope(env)
require.NoError(t, err)
require.NoError(t, s.postPayloadHeadUpdate(ctx, envelope, st, root, root[:]))
s.headLock.RLock()
require.Equal(t, true, s.head.full)
s.headLock.RUnlock()
}
func TestGetLookupParentRoot_PreGloas(t *testing.T) {
service, _ := minimalTestService(t)
parentRoot := [32]byte{1}
blk := util.HydrateSignedBeaconBlockDeneb(&ethpb.SignedBeaconBlockDeneb{
Block: &ethpb.BeaconBlockDeneb{
ParentRoot: parentRoot[:],
},
})
wsb, err := blocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
roblock, err := blocks.NewROBlock(wsb)
require.NoError(t, err)
got, err := service.getLookupParentRoot(roblock)
require.NoError(t, err)
require.Equal(t, parentRoot, got)
}
func TestGetLookupParentRoot_GloasBuildsOnEmpty(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
cfg.GloasForkEpoch = 0
cfg.InitializeForkSchedule()
params.OverrideBeaconConfig(cfg)
service, req := minimalTestService(t)
ctx := t.Context()
parentRoot := [32]byte{1}
parentBlockHash := [32]byte{20}
parentNodeBlockHash := [32]byte{99} // Different from parentBlockHash => builds on empty
// Insert a Gloas node for the parent so BlockHash works.
st, parentROBlock, err := prepareGloasForkchoiceState(ctx, 1, parentRoot, params.BeaconConfig().ZeroHash, parentNodeBlockHash, params.BeaconConfig().ZeroHash, 0, 0)
require.NoError(t, err)
require.NoError(t, req.fcs.InsertNode(ctx, st, parentROBlock))
blockHash := [32]byte{10}
bid := util.HydrateSignedExecutionPayloadBid(&ethpb.SignedExecutionPayloadBid{
Message: &ethpb.ExecutionPayloadBid{
BlockHash: blockHash[:],
ParentBlockHash: parentBlockHash[:],
},
})
blk := util.HydrateSignedBeaconBlockGloas(&ethpb.SignedBeaconBlockGloas{
Block: &ethpb.BeaconBlockGloas{
Slot: 2,
ParentRoot: parentRoot[:],
Body: &ethpb.BeaconBlockBodyGloas{
SignedExecutionPayloadBid: bid,
},
},
})
wsb, err := blocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
roblock, err := blocks.NewROBlock(wsb)
require.NoError(t, err)
got, err := service.getLookupParentRoot(roblock)
require.NoError(t, err)
// parentBlockHash != parentNodeBlockHash, so it builds on empty => returns parentRoot
require.Equal(t, parentRoot, got)
}
func TestGetLookupParentRoot_GloasBuildsOnFull(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
cfg.GloasForkEpoch = 0
cfg.InitializeForkSchedule()
params.OverrideBeaconConfig(cfg)
service, req := minimalTestService(t)
ctx := t.Context()
parentRoot := [32]byte{1}
parentNodeBlockHash := [32]byte{10}
// Insert a Gloas node for the parent so BlockHash works.
st, parentROBlock, err := prepareGloasForkchoiceState(ctx, 1, parentRoot, params.BeaconConfig().ZeroHash, parentNodeBlockHash, params.BeaconConfig().ZeroHash, 0, 0)
require.NoError(t, err)
require.NoError(t, req.fcs.InsertNode(ctx, st, parentROBlock))
// Set parentBlockHash in the bid to match the parent's blockHash.
blockHash := [32]byte{20}
bid := util.HydrateSignedExecutionPayloadBid(&ethpb.SignedExecutionPayloadBid{
Message: &ethpb.ExecutionPayloadBid{
BlockHash: blockHash[:],
ParentBlockHash: parentNodeBlockHash[:],
},
})
blk := util.HydrateSignedBeaconBlockGloas(&ethpb.SignedBeaconBlockGloas{
Block: &ethpb.BeaconBlockGloas{
Slot: 2,
ParentRoot: parentRoot[:],
Body: &ethpb.BeaconBlockBodyGloas{
SignedExecutionPayloadBid: bid,
},
},
})
wsb, err := blocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
roblock, err := blocks.NewROBlock(wsb)
require.NoError(t, err)
got, err := service.getLookupParentRoot(roblock)
require.NoError(t, err)
// parentBlockHash == parentNodeBlockHash, so it builds on full => returns parentBlockHash
require.Equal(t, parentNodeBlockHash, got)
}
func TestGetLookupParentRoot_GloasParentPreForkEpoch(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
cfg.GloasForkEpoch = 2
cfg.InitializeForkSchedule()
params.OverrideBeaconConfig(cfg)
service, req := minimalTestService(t)
ctx := t.Context()
parentRoot := [32]byte{1}
parentNodeBlockHash := [32]byte{10}
parentSlot, err := slots.EpochStart(params.BeaconConfig().GloasForkEpoch)
require.NoError(t, err)
parentSlot = parentSlot - 1
st, parentROBlock, err := prepareGloasForkchoiceState(
ctx,
parentSlot,
parentRoot,
params.BeaconConfig().ZeroHash,
parentNodeBlockHash,
params.BeaconConfig().ZeroHash,
0,
0,
)
require.NoError(t, err)
require.NoError(t, req.fcs.InsertNode(ctx, st, parentROBlock))
blockHash := [32]byte{20}
bid := util.HydrateSignedExecutionPayloadBid(&ethpb.SignedExecutionPayloadBid{
Message: &ethpb.ExecutionPayloadBid{
BlockHash: blockHash[:],
ParentBlockHash: parentNodeBlockHash[:],
},
})
blk := util.HydrateSignedBeaconBlockGloas(&ethpb.SignedBeaconBlockGloas{
Block: &ethpb.BeaconBlockGloas{
Slot: parentSlot + 1,
ParentRoot: parentRoot[:],
Body: &ethpb.BeaconBlockBodyGloas{
SignedExecutionPayloadBid: bid,
},
},
})
wsb, err := blocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
roblock, err := blocks.NewROBlock(wsb)
require.NoError(t, err)
got, err := service.getLookupParentRoot(roblock)
require.NoError(t, err)
// Parent slot is pre-fork, so always return parentRoot.
require.Equal(t, parentRoot, got)
}
func TestLatePayloadTasks_ReturnsEarlyWhenBlockLate(t *testing.T) {
logHook := logTest.NewGlobal()
service, tr := setupGloasService(t, &mockExecution.EngineClient{})
blockHash := bytesutil.ToBytes32([]byte("hash1"))
base, _ := testGloasState(t, 1, params.BeaconConfig().ZeroHash, blockHash)
base.LatestBlockHash = blockHash[:]
st, err := state_native.InitializeFromProtoUnsafeGloas(base)
require.NoError(t, err)
headRoot := bytesutil.ToBytes32([]byte("headroot"))
service.head = &head{
root: headRoot,
state: st,
slot: 1,
}
// Set genesis time so CurrentSlot > HeadSlot.
service.SetGenesisTime(time.Now().Add(-2 * time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Second))
service.latePayloadTasks(tr.ctx)
require.LogsDoNotContain(t, logHook, "Could not notify forkchoice update")
// No payload ID should have been cached.
_, has := service.cfg.PayloadIDCache.PayloadID(service.CurrentSlot()+1, headRoot)
require.Equal(t, false, has)
}
func TestLatePayloadTasks_SendsFCU(t *testing.T) {
logHook := logTest.NewGlobal()
resetCfg := features.InitWithReset(&features.Flags{
PrepareAllPayloads: true,
})
defer resetCfg()
pid := &enginev1.PayloadIDBytes{1, 2, 3, 4, 5, 6, 7, 8}
service, tr := setupGloasService(t, &mockExecution.EngineClient{PayloadIDBytes: pid})
blockHash := bytesutil.ToBytes32([]byte("hash1"))
base, blk := testGloasState(t, 1, params.BeaconConfig().ZeroHash, blockHash)
base.LatestBlockHash = blockHash[:]
st, err := state_native.InitializeFromProtoUnsafeGloas(base)
require.NoError(t, err)
signed, err := blocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
headRoot := bytesutil.ToBytes32([]byte("headroot"))
service.head = &head{
root: headRoot,
block: signed,
state: st,
slot: 1,
}
// CurrentSlot == HeadSlot == 1: place genesis 1.5 slots ago so we're solidly in slot 1.
service.SetGenesisTime(time.Now().Add(-3 * time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Second / 2))
service.SetForkChoiceGenesisTime(service.genesisTime)
service.latePayloadTasks(tr.ctx)
require.LogsDoNotContain(t, logHook, "Could not notify forkchoice update")
require.LogsDoNotContain(t, logHook, "Could not get")
// Payload ID should have been cached.
cachedPid, has := service.cfg.PayloadIDCache.PayloadID(service.CurrentSlot()+1, headRoot)
require.Equal(t, true, has)
require.Equal(t, primitives.PayloadID(pid[:]), cachedPid)
}
func TestLateBlockTasks_GloasFCU(t *testing.T) {
logHook := logTest.NewGlobal()
resetCfg := features.InitWithReset(&features.Flags{
PrepareAllPayloads: true,
})
defer resetCfg()
pid := &enginev1.PayloadIDBytes{1, 2, 3, 4, 5, 6, 7, 8}
service, tr := setupGloasService(t, &mockExecution.EngineClient{PayloadIDBytes: pid})
blockHash := bytesutil.ToBytes32([]byte("hash1"))
base, _ := testGloasState(t, 1, params.BeaconConfig().ZeroHash, blockHash)
base.LatestBlockHash = blockHash[:]
st, err := state_native.InitializeFromProtoUnsafeGloas(base)
require.NoError(t, err)
headRoot := bytesutil.ToBytes32([]byte("headroot"))
service.head = &head{
root: headRoot,
state: st,
slot: 1,
}
// Set genesis time so CurrentSlot > HeadSlot, triggering late block logic.
service.SetGenesisTime(time.Now().Add(-2 * time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Second))
service.SetForkChoiceGenesisTime(service.genesisTime)
service.lateBlockTasks(tr.ctx)
require.LogsDoNotContain(t, logHook, "could not perform late block tasks")
}
// TestSaveHead_GloasForkBoundary_PreforkBidForcesEmptyHead verifies that saveHead does not
// treat the head as "full" when the latest execution payload bid was issued in a pre-fork epoch.
// This guards against the Fulu->Gloas upgrade-seeded bid (bid.BlockHash == latestBlockHash,
// bid.Slot == 0) causing a spurious full=true head before any real Gloas bid has been processed.
func TestSaveHead_GloasForkBoundary_PreforkBidForcesEmptyHead(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
cfg.GloasForkEpoch = 1
cfg.InitializeForkSchedule()
params.OverrideBeaconConfig(cfg)
service, _ := setupGloasService(t, &mockExecution.EngineClient{})
ctx := t.Context()
blockRoot := bytesutil.ToBytes32([]byte("root1"))
parentRoot := params.BeaconConfig().ZeroHash
blockHash := bytesutil.ToBytes32([]byte("hash1"))
// Create a Gloas state where IsParentBlockFull()==true (bid.BlockHash == LatestBlockHash)
// but bid.Slot is 0 (epoch 0, pre-fork). This mimics the upgrade-seeded state.
base, blk := testGloasState(t, 1, parentRoot, blockHash)
base.LatestBlockHash = blockHash[:]
// bid.Slot defaults to 0, which is before GloasForkEpoch=1.
// Set a valid initial head so saveHead's headBlock() call does not panic.
// We do NOT insert the old block into forkchoice because insertGloasBlock
// would claim the tree root slot; the target block (parentRoot=ZeroHash) must
// be the first node inserted so it can become the tree root.
oldBlk := util.HydrateSignedBeaconBlockGloas(&ethpb.SignedBeaconBlockGloas{})
oldSigned, err2 := blocks.NewSignedBeaconBlock(oldBlk)
require.NoError(t, err2)
oldSt, err2 := state_native.InitializeFromProtoUnsafeGloas(&ethpb.BeaconStateGloas{
Slot: 0,
RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector),
CurrentJustifiedCheckpoint: &ethpb.Checkpoint{Root: make([]byte, 32)},
FinalizedCheckpoint: &ethpb.Checkpoint{Root: make([]byte, 32)},
LatestBlockHeader: &ethpb.BeaconBlockHeader{ParentRoot: make([]byte, 32), StateRoot: make([]byte, 32), BodyRoot: make([]byte, 32)},
Eth1Data: &ethpb.Eth1Data{DepositRoot: make([]byte, 32), BlockHash: make([]byte, 32)},
LatestExecutionPayloadBid: &ethpb.ExecutionPayloadBid{BlockHash: make([]byte, 32), ParentBlockHash: make([]byte, 32), ParentBlockRoot: make([]byte, 32), PrevRandao: make([]byte, 32), FeeRecipient: make([]byte, 20), BlobKzgCommitments: [][]byte{make([]byte, 48)}},
BuilderPendingPayments: func() []*ethpb.BuilderPendingPayment { pp := make([]*ethpb.BuilderPendingPayment, 64); for i := range pp { pp[i] = &ethpb.BuilderPendingPayment{Withdrawal: &ethpb.BuilderPendingWithdrawal{FeeRecipient: make([]byte, 20)}} }; return pp }(),
ExecutionPayloadAvailability: make([]byte, 1024),
LatestBlockHash: make([]byte, 32),
PayloadExpectedWithdrawals: make([]*enginev1.Withdrawal, 0),
ProposerLookahead: make([]uint64, 64),
})
require.NoError(t, err2)
oldRoot := bytesutil.ToBytes32([]byte("oldroot1"))
service.head = &head{root: oldRoot, block: oldSigned, state: oldSt, slot: 0}
insertGloasBlock(t, service, base, blk, blockRoot)
st, err := state_native.InitializeFromProtoUnsafeGloas(base)
require.NoError(t, err)
// Verify precondition: IsParentBlockFull() is true.
full, err := st.IsParentBlockFull()
require.NoError(t, err)
require.Equal(t, true, full, "precondition: IsParentBlockFull must be true")
// Verify guard precondition: bid.Slot is pre-fork.
bid, err := st.LatestExecutionPayloadBid()
require.NoError(t, err)
isPrefork := slots.ToEpoch(bid.Slot()) < params.BeaconConfig().GloasForkEpoch
require.Equal(t, true, isPrefork, "precondition: bid.Slot must be pre-fork")
ssigned, err := blocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
// saveHead should NOT mark the head as full because bid.Slot < GloasForkEpoch.
require.NoError(t, service.saveHead(ctx, blockRoot, ssigned, st))
service.headLock.RLock()
headFull := service.head.full
service.headLock.RUnlock()
require.Equal(t, false, headFull, "head must not be full for upgrade-seeded bid")
}
// TestSaveHead_GloasForkBoundary_PostforkBidSetsFullHead verifies that saveHead correctly
// marks the head as full when the latest bid is from a post-fork epoch.
func TestSaveHead_GloasForkBoundary_PostforkBidSetsFullHead(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
cfg.GloasForkEpoch = 1
cfg.InitializeForkSchedule()
params.OverrideBeaconConfig(cfg)
service, _ := setupGloasService(t, &mockExecution.EngineClient{})
ctx := t.Context()
forkSlot, err := slots.EpochStart(params.BeaconConfig().GloasForkEpoch)
require.NoError(t, err)
blockRoot := bytesutil.ToBytes32([]byte("root1"))
parentRoot := params.BeaconConfig().ZeroHash
blockHash := bytesutil.ToBytes32([]byte("hash1"))
// Set a valid initial head so saveHead's headBlock() call does not panic.
// Do NOT use insertGloasBlock for the old block — the target block must be
// the first node inserted so it can claim the tree root (parentRoot=ZeroHash).
oldBlk2 := util.HydrateSignedBeaconBlockGloas(&ethpb.SignedBeaconBlockGloas{})
oldSigned2, err2 := blocks.NewSignedBeaconBlock(oldBlk2)
require.NoError(t, err2)
oldSt2, err2 := state_native.InitializeFromProtoUnsafeGloas(&ethpb.BeaconStateGloas{
Slot: 0,
RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector),
CurrentJustifiedCheckpoint: &ethpb.Checkpoint{Root: make([]byte, 32)},
FinalizedCheckpoint: &ethpb.Checkpoint{Root: make([]byte, 32)},
LatestBlockHeader: &ethpb.BeaconBlockHeader{ParentRoot: make([]byte, 32), StateRoot: make([]byte, 32), BodyRoot: make([]byte, 32)},
Eth1Data: &ethpb.Eth1Data{DepositRoot: make([]byte, 32), BlockHash: make([]byte, 32)},
LatestExecutionPayloadBid: &ethpb.ExecutionPayloadBid{BlockHash: make([]byte, 32), ParentBlockHash: make([]byte, 32), ParentBlockRoot: make([]byte, 32), PrevRandao: make([]byte, 32), FeeRecipient: make([]byte, 20), BlobKzgCommitments: [][]byte{make([]byte, 48)}},
BuilderPendingPayments: func() []*ethpb.BuilderPendingPayment { pp := make([]*ethpb.BuilderPendingPayment, 64); for i := range pp { pp[i] = &ethpb.BuilderPendingPayment{Withdrawal: &ethpb.BuilderPendingWithdrawal{FeeRecipient: make([]byte, 20)}} }; return pp }(),
ExecutionPayloadAvailability: make([]byte, 1024),
LatestBlockHash: make([]byte, 32),
PayloadExpectedWithdrawals: make([]*enginev1.Withdrawal, 0),
ProposerLookahead: make([]uint64, 64),
})
require.NoError(t, err2)
oldRoot2 := bytesutil.ToBytes32([]byte("oldroot2"))
service.head = &head{root: oldRoot2, block: oldSigned2, state: oldSt2, slot: 0}
base, blk := testGloasState(t, forkSlot+1, parentRoot, blockHash)
base.LatestBlockHash = blockHash[:]
// Set bid.Slot to a post-fork epoch slot.
base.LatestExecutionPayloadBid.Slot = forkSlot + 1
insertGloasBlock(t, service, base, blk, blockRoot)
st, err := state_native.InitializeFromProtoUnsafeGloas(base)
require.NoError(t, err)
// Verify preconditions.
full, err := st.IsParentBlockFull()
require.NoError(t, err)
require.Equal(t, true, full, "precondition: IsParentBlockFull must be true")
bid, err := st.LatestExecutionPayloadBid()
require.NoError(t, err)
isPostfork := slots.ToEpoch(bid.Slot()) >= params.BeaconConfig().GloasForkEpoch
require.Equal(t, true, isPostfork, "precondition: bid.Slot must be post-fork")
ssigned, err := blocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
// saveHead SHOULD mark the head as full because bid.Slot >= GloasForkEpoch.
require.NoError(t, service.saveHead(ctx, blockRoot, ssigned, st))
service.headLock.RLock()
headFull := service.head.full
service.headLock.RUnlock()
require.Equal(t, true, headFull, "head must be full for real post-fork bid")
}
// TestLateBlockTasks_GloasForkBoundary_PreforkBidUsesHeadRoot verifies that lateBlockTasks
// uses headRoot (not LatestBlockHash) as the accessRoot when the bid is pre-fork epoch.
// Without this guard, the upgrade-seeded bid would cause lateBlockTasks to use the wrong
// access root for the next-slot cache.
func TestLateBlockTasks_GloasForkBoundary_PreforkBidUsesHeadRoot(t *testing.T) {
logHook := logTest.NewGlobal()
resetCfg := features.InitWithReset(&features.Flags{
PrepareAllPayloads: true,
})
defer resetCfg()
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
cfg.GloasForkEpoch = 1
cfg.InitializeForkSchedule()
params.OverrideBeaconConfig(cfg)
pid := &enginev1.PayloadIDBytes{1, 2, 3, 4, 5, 6, 7, 8}
service, tr := setupGloasService(t, &mockExecution.EngineClient{PayloadIDBytes: pid})
blockHash := bytesutil.ToBytes32([]byte("hash1"))
base, _ := testGloasState(t, 1, params.BeaconConfig().ZeroHash, blockHash)
// Make IsParentBlockFull() true: bid.BlockHash == LatestBlockHash.
base.LatestBlockHash = blockHash[:]
// bid.Slot is 0 (pre-fork epoch): the epoch guard should prevent using LatestBlockHash as accessRoot.
st, err := state_native.InitializeFromProtoUnsafeGloas(base)
require.NoError(t, err)
headRoot := bytesutil.ToBytes32([]byte("headroot"))
service.head = &head{
root: headRoot,
state: st,
slot: 1,
}
// Trigger late block logic: CurrentSlot > HeadSlot.
service.SetGenesisTime(time.Now().Add(-2 * time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Second))
service.SetForkChoiceGenesisTime(service.genesisTime)
service.lateBlockTasks(tr.ctx)
require.LogsDoNotContain(t, logHook, "could not perform late block tasks")
}

View File

@@ -50,6 +50,7 @@ type head struct {
block interfaces.ReadOnlySignedBeaconBlock // current head block.
state state.BeaconState // current head state.
slot primitives.Slot // the head block slot number
full bool // whether the head is post-CL or post-EL after Gloas
optimistic bool // optimistic status when saved head
}
@@ -60,8 +61,24 @@ func (s *Service) saveHead(ctx context.Context, newHeadRoot [32]byte, headBlock
ctx, span := trace.StartSpan(ctx, "blockChain.saveHead")
defer span.End()
// Pre-Gloas we use empty for head because we still key states by blockroot
var full bool
var err error
if headState.Version() >= version.Gloas {
gloasFirstSlot, err := slots.EpochStart(params.BeaconConfig().GloasForkEpoch)
if err != nil {
return errors.Wrap(err, "could not compute gloas first slot")
}
if headState.Slot() > gloasFirstSlot {
full, err = headState.IsParentBlockFull()
if err != nil {
return errors.Wrap(err, "could not determine if head is full or not")
}
}
}
// Do nothing if head hasn't changed.
if !s.isNewHead(newHeadRoot) {
if !s.isNewHead(newHeadRoot, full) {
return nil
}
@@ -157,6 +174,7 @@ func (s *Service) saveHead(ctx context.Context, newHeadRoot [32]byte, headBlock
state: headState,
optimistic: isOptimistic,
slot: headBlock.Block().Slot(),
full: full,
}
if err := s.setHead(newHead); err != nil {
return errors.Wrap(err, "could not set head")
@@ -170,7 +188,7 @@ func (s *Service) saveHead(ctx context.Context, newHeadRoot [32]byte, headBlock
// Forward an event capturing a new chain head over a common event feed
// done in a goroutine to avoid blocking the critical runtime main routine.
go func() {
if err := s.notifyNewHeadEvent(ctx, newHeadSlot, headState, newStateRoot[:], newHeadRoot[:]); err != nil {
if err := s.notifyNewHeadEvent(ctx, newHeadSlot, newStateRoot[:], newHeadRoot[:]); err != nil {
log.WithError(err).Error("Could not notify event feed of new chain head")
}
}()
@@ -217,6 +235,7 @@ func (s *Service) setHead(newHead *head) error {
root: newHead.root,
block: bCp,
state: newHead.state.Copy(),
full: newHead.full,
optimistic: newHead.optimistic,
slot: newHead.slot,
}
@@ -322,7 +341,6 @@ func (s *Service) hasHeadState() bool {
func (s *Service) notifyNewHeadEvent(
ctx context.Context,
newHeadSlot primitives.Slot,
newHeadState state.BeaconState,
newHeadStateRoot,
newHeadRoot []byte,
) error {

View File

@@ -152,7 +152,6 @@ func TestSaveHead_Different_Reorg(t *testing.T) {
func Test_notifyNewHeadEvent(t *testing.T) {
t.Run("genesis_state_root", func(t *testing.T) {
bState, _ := util.DeterministicGenesisState(t, 10)
srv := testServiceWithDB(t)
srv.SetGenesisTime(time.Now())
notifier := srv.cfg.StateNotifier.(*mock.MockStateNotifier)
@@ -165,7 +164,7 @@ func Test_notifyNewHeadEvent(t *testing.T) {
st, blk, err = prepareForkchoiceState(t.Context(), 1, newHeadRoot, [32]byte{}, [32]byte{}, &ethpb.Checkpoint{}, &ethpb.Checkpoint{})
require.NoError(t, err)
require.NoError(t, srv.cfg.ForkChoiceStore.InsertNode(t.Context(), st, blk))
require.NoError(t, srv.notifyNewHeadEvent(t.Context(), 1, bState, newHeadStateRoot[:], newHeadRoot[:]))
require.NoError(t, srv.notifyNewHeadEvent(t.Context(), 1, newHeadStateRoot[:], newHeadRoot[:]))
events := notifier.ReceivedEvents()
require.Equal(t, 1, len(events))
@@ -202,7 +201,7 @@ func Test_notifyNewHeadEvent(t *testing.T) {
st, blk, err = prepareForkchoiceState(t.Context(), 0, newHeadRoot, [32]byte{}, [32]byte{}, &ethpb.Checkpoint{}, &ethpb.Checkpoint{})
require.NoError(t, err)
require.NoError(t, srv.cfg.ForkChoiceStore.InsertNode(t.Context(), st, blk))
err = srv.notifyNewHeadEvent(t.Context(), epoch2Start, bState, newHeadStateRoot[:], newHeadRoot[:])
err = srv.notifyNewHeadEvent(t.Context(), epoch2Start, newHeadStateRoot[:], newHeadRoot[:])
require.NoError(t, err)
events := notifier.ReceivedEvents()
require.Equal(t, 1, len(events))
@@ -220,7 +219,6 @@ func Test_notifyNewHeadEvent(t *testing.T) {
require.DeepSSZEqual(t, wanted, eventHead)
})
t.Run("epoch transition", func(t *testing.T) {
bState, _ := util.DeterministicGenesisState(t, 10)
srv := testServiceWithDB(t)
srv.SetGenesisTime(time.Now())
notifier := srv.cfg.StateNotifier.(*mock.MockStateNotifier)
@@ -234,7 +232,7 @@ func Test_notifyNewHeadEvent(t *testing.T) {
require.NoError(t, err)
require.NoError(t, srv.cfg.ForkChoiceStore.InsertNode(t.Context(), st, blk))
newHeadSlot := params.BeaconConfig().SlotsPerEpoch
require.NoError(t, srv.notifyNewHeadEvent(t.Context(), newHeadSlot, bState, newHeadStateRoot[:], newHeadRoot[:]))
require.NoError(t, srv.notifyNewHeadEvent(t.Context(), newHeadSlot, newHeadStateRoot[:], newHeadRoot[:]))
events := notifier.ReceivedEvents()
require.Equal(t, 1, len(events))

View File

@@ -13,7 +13,7 @@ go_library(
deps = [
"//consensus-types/blocks:go_default_library",
"@com_github_crate_crypto_go_kzg_4844//:go_default_library",
"@com_github_ethereum_c_kzg_4844//bindings/go:go_default_library",
"@com_github_ethereum_c_kzg_4844_v2//bindings/go:go_default_library",
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",
"@com_github_ethereum_go_ethereum//crypto/kzg4844:go_default_library",
"@com_github_pkg_errors//:go_default_library",

View File

@@ -5,11 +5,11 @@ import (
"fmt"
"time"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/blocks"
"github.com/OffchainLabs/prysm/v7/config/params"
consensus_types "github.com/OffchainLabs/prysm/v7/consensus-types"
"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
"github.com/OffchainLabs/prysm/v7/io/logs"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/runtime/version"
prysmTime "github.com/OffchainLabs/prysm/v7/time"
@@ -40,7 +40,7 @@ func logStateTransitionData(b interfaces.ReadOnlyBeaconBlock) error {
}
log = log.WithField("syncBitsCount", agg.SyncCommitteeBits.Count())
}
if b.Version() >= version.Bellatrix {
if b.Version() >= version.Bellatrix && b.Version() < version.Gloas {
p, err := b.Body().Execution()
if err != nil {
return err
@@ -56,7 +56,7 @@ func logStateTransitionData(b interfaces.ReadOnlyBeaconBlock) error {
txsPerSlotCount.Set(float64(len(txs)))
}
}
if b.Version() >= version.Deneb {
if b.Version() >= version.Deneb && b.Version() < version.Gloas {
kzgs, err := b.Body().BlobKzgCommitments()
if err != nil {
log.WithError(err).Error("Failed to get blob KZG commitments")
@@ -64,7 +64,7 @@ func logStateTransitionData(b interfaces.ReadOnlyBeaconBlock) error {
log = log.WithField("kzgCommitmentCount", len(kzgs))
}
}
if b.Version() >= version.Electra {
if b.Version() >= version.Electra && b.Version() < version.Gloas {
eReqs, err := b.Body().ExecutionRequests()
if err != nil {
log.WithError(err).Error("Failed to get execution requests")
@@ -80,6 +80,20 @@ func logStateTransitionData(b interfaces.ReadOnlyBeaconBlock) error {
}
}
}
if b.Version() >= version.Gloas {
signedBid, err := b.Body().SignedExecutionPayloadBid()
if err != nil {
log.WithError(err).Error("Failed to get signed execution payload bid")
} else {
bid := signedBid.Message
log = log.WithFields(logrus.Fields{
"blobKzgCommitmentCount": len(bid.BlobKzgCommitments),
"payloadHash": fmt.Sprintf("%#x", bytesutil.Trunc(bid.BlockHash)),
"parentHash": fmt.Sprintf("%#x", bytesutil.Trunc(bid.ParentBlockHash)),
"builderIndex": bid.BuilderIndex,
})
}
}
log.Info("Finished applying state transition")
return nil
}
@@ -87,46 +101,62 @@ func logStateTransitionData(b interfaces.ReadOnlyBeaconBlock) error {
func logBlockSyncStatus(block interfaces.ReadOnlyBeaconBlock, blockRoot [32]byte, justified, finalized *ethpb.Checkpoint, receivedTime time.Time, genesis time.Time, daWaitedTime time.Duration) error {
startTime, err := slots.StartTime(genesis, block.Slot())
if err != nil {
return err
return errors.Wrap(err, "failed to get slot start time")
}
level := log.Logger.GetLevel()
if level >= logrus.DebugLevel {
parentRoot := block.ParentRoot()
lf := logrus.Fields{
"slot": block.Slot(),
"slotInEpoch": block.Slot() % params.BeaconConfig().SlotsPerEpoch,
"block": fmt.Sprintf("0x%s...", hex.EncodeToString(blockRoot[:])[:8]),
"epoch": slots.ToEpoch(block.Slot()),
"justifiedEpoch": justified.Epoch,
"justifiedRoot": fmt.Sprintf("0x%s...", hex.EncodeToString(justified.Root)[:8]),
"finalizedEpoch": finalized.Epoch,
"finalizedRoot": fmt.Sprintf("0x%s...", hex.EncodeToString(finalized.Root)[:8]),
"parentRoot": fmt.Sprintf("0x%s...", hex.EncodeToString(parentRoot[:])[:8]),
"version": version.String(block.Version()),
"sinceSlotStartTime": prysmTime.Now().Sub(startTime),
"chainServiceProcessedTime": prysmTime.Now().Sub(receivedTime) - daWaitedTime,
"dataAvailabilityWaitedTime": daWaitedTime,
}
log.WithFields(lf).Debug("Synced new block")
parentRoot := block.ParentRoot()
blkRoot := fmt.Sprintf("0x%s...", hex.EncodeToString(blockRoot[:])[:8])
finalizedRoot := fmt.Sprintf("0x%s...", hex.EncodeToString(finalized.Root)[:8])
sinceSlotStartTime := prysmTime.Now().Sub(startTime)
lessFields := logrus.Fields{
"slot": block.Slot(),
"block": blkRoot,
"finalizedEpoch": finalized.Epoch,
"finalizedRoot": finalizedRoot,
"epoch": slots.ToEpoch(block.Slot()),
"sinceSlotStartTime": sinceSlotStartTime,
}
moreFields := logrus.Fields{
"slot": block.Slot(),
"slotInEpoch": block.Slot() % params.BeaconConfig().SlotsPerEpoch,
"block": blkRoot,
"epoch": slots.ToEpoch(block.Slot()),
"justifiedEpoch": justified.Epoch,
"justifiedRoot": fmt.Sprintf("0x%s...", hex.EncodeToString(justified.Root)[:8]),
"finalizedEpoch": finalized.Epoch,
"finalizedRoot": finalizedRoot,
"parentRoot": fmt.Sprintf("0x%s...", hex.EncodeToString(parentRoot[:])[:8]),
"version": version.String(block.Version()),
"sinceSlotStartTime": sinceSlotStartTime,
"chainServiceProcessedTime": prysmTime.Now().Sub(receivedTime) - daWaitedTime,
}
if block.Version() < version.Gloas {
moreFields["dataAvailabilityWaitedTime"] = daWaitedTime
} else {
log.WithFields(logrus.Fields{
"slot": block.Slot(),
"block": fmt.Sprintf("0x%s...", hex.EncodeToString(blockRoot[:])[:8]),
"finalizedEpoch": finalized.Epoch,
"finalizedRoot": fmt.Sprintf("0x%s...", hex.EncodeToString(finalized.Root)[:8]),
"epoch": slots.ToEpoch(block.Slot()),
}).Info("Synced new block")
signedBid, err := block.Body().SignedExecutionPayloadBid()
if err != nil {
log.WithError(err).Error("Failed to get signed execution payload bid for logging")
} else {
moreFields["blockHash"] = fmt.Sprintf("%#x", bytesutil.Trunc(signedBid.Message.BlockHash))
moreFields["parentHash"] = fmt.Sprintf("%#x", bytesutil.Trunc(signedBid.Message.ParentBlockHash))
moreFields["builderIndex"] = signedBid.Message.BuilderIndex
}
}
level := logs.PackageVerbosity("beacon-chain/blockchain")
if level >= logrus.DebugLevel {
log.WithFields(moreFields).Info("Synced new block")
return nil
}
log.WithFields(lessFields).WithField(logs.LogTargetField, logs.LogTargetUser).Info("Synced new block")
log.WithFields(moreFields).WithField(logs.LogTargetField, logs.LogTargetEphemeral).Info("Synced new block")
return nil
}
// logs payload related data every slot.
func logPayload(block interfaces.ReadOnlyBeaconBlock) error {
isExecutionBlk, err := blocks.IsExecutionBlock(block.Body())
if err != nil {
return errors.Wrap(err, "could not determine if block is execution block")
}
if !isExecutionBlk {
if block.Version() < version.Bellatrix || block.Version() >= version.Gloas {
return nil
}
payload, err := block.Body().Execution()

View File

@@ -221,6 +221,38 @@ var (
Buckets: []float64{1, 2, 4, 8, 16, 32},
},
)
commitmentCount = promauto.NewHistogram(
prometheus.HistogramOpts{
Name: "commitment_count_max_21",
Help: "The number of blob KZG commitments per block.",
Buckets: []float64{1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21},
},
)
maxBlobsPerBlock = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "max_blobs_per_block",
Help: "The maximum number of blobs allowed in a block.",
},
)
beaconExecutionPayloadEnvelopeValidTotal = promauto.NewCounter(prometheus.CounterOpts{
Name: "beacon_execution_payload_envelope_valid_total",
Help: "Count the number of execution payload envelopes that were processed successfully.",
})
beaconExecutionPayloadEnvelopeInvalidTotal = promauto.NewCounter(prometheus.CounterOpts{
Name: "beacon_execution_payload_envelope_invalid_total",
Help: "Count the number of execution payload envelopes that failed processing.",
})
beaconExecutionPayloadEnvelopeProcessingDurationSeconds = promauto.NewHistogram(
prometheus.HistogramOpts{
Name: "beacon_execution_payload_envelope_processing_duration_seconds",
Help: "Captures end-to-end processing time for execution payload envelopes.",
Buckets: prometheus.DefBuckets,
},
)
beaconLatePayloadTaskTriggeredTotal = promauto.NewCounter(prometheus.CounterOpts{
Name: "beacon_late_payload_task_triggered_total",
Help: "Count the number of times late payload tasks fired.",
})
)
// reportSlotMetrics reports slot related metrics.

View File

@@ -2,6 +2,7 @@ package blockchain
import (
"context"
"fmt"
"time"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
@@ -57,7 +58,7 @@ func (s *Service) OnAttestation(ctx context.Context, a ethpb.Att, disparity time
// save it to the cache.
baseState, err := s.getAttPreState(ctx, tgt)
if err != nil {
return err
return fmt.Errorf("get att pre state for att root %s: %w", fmt.Sprintf("%#x", a.GetData().BeaconBlockRoot), err)
}
// Verify attestation target is from current epoch or previous epoch.
@@ -96,7 +97,11 @@ func (s *Service) OnAttestation(ctx context.Context, a ethpb.Att, disparity time
// We assume trusted attestation in this function has verified signature.
// Update forkchoice store with the new attestation for updating weight.
s.cfg.ForkChoiceStore.ProcessAttestation(ctx, indexedAtt.GetAttestingIndices(), bytesutil.ToBytes32(a.GetData().BeaconBlockRoot), a.GetData().Target.Epoch)
attData := a.GetData()
payloadStatus := true
if attData.Target.Epoch >= params.BeaconConfig().GloasForkEpoch {
payloadStatus = attData.CommitteeIndex == 1
}
s.cfg.ForkChoiceStore.ProcessAttestation(ctx, indexedAtt.GetAttestingIndices(), bytesutil.ToBytes32(attData.BeaconBlockRoot), attData.Slot, payloadStatus)
return nil
}

View File

@@ -1,12 +1,14 @@
package blockchain
import (
"bytes"
"context"
"fmt"
"time"
"github.com/OffchainLabs/go-bitfield"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/blocks"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/gloas"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/peerdas"
coreTime "github.com/OffchainLabs/prysm/v7/beacon-chain/core/time"
@@ -64,7 +66,6 @@ func (s *Service) postBlockProcess(cfg *postBlockProcessConfig) error {
return invalidBlock{error: err}
}
startTime := time.Now()
fcuArgs := &fcuConfig{}
if features.Get().EnableLightClient && slots.ToEpoch(s.CurrentSlot()) >= params.BeaconConfig().AltairForkEpoch {
defer s.processLightClientUpdates(cfg)
@@ -83,6 +84,9 @@ func (s *Service) postBlockProcess(cfg *postBlockProcessConfig) error {
if err := s.handleBlockAttestations(ctx, cfg.roblock.Block(), cfg.postState); err != nil {
return errors.Wrap(err, "could not handle block's attestations")
}
if err := s.handleBlockPayloadAttestations(ctx, cfg.roblock.Block(), cfg.postState); err != nil {
return errors.Wrap(err, "could not handle block's payload attestations")
}
s.InsertSlashingsToForkChoiceStore(ctx, cfg.roblock.Block().Body().AttesterSlashings())
if cfg.isValidPayload {
@@ -102,7 +106,14 @@ func (s *Service) postBlockProcess(cfg *postBlockProcessConfig) error {
s.logNonCanonicalBlockReceived(cfg.roblock.Root(), cfg.headRoot)
return nil
}
s.sendFCU(cfg, fcuArgs)
if cfg.roblock.Version() < version.Gloas {
s.sendFCU(cfg)
} else if s.isNewHead(cfg.headRoot, false) { // We reach this only when the incoming block is head.
if err := s.saveHead(ctx, cfg.headRoot, cfg.roblock, cfg.postState); err != nil {
log.WithError(err).Error("Could not save head")
}
s.pruneAttsFromPool(ctx, cfg.postState, cfg.roblock)
}
// Pre-Fulu the caches are updated when computing the payload attributes
if cfg.postState.Version() >= version.Fulu {
@@ -232,7 +243,8 @@ func (s *Service) onBlockBatch(ctx context.Context, blks []consensusblocks.ROBlo
postVersionAndHeaders[i].version,
postVersionAndHeaders[i].header, b)
if err != nil {
return s.handleInvalidExecutionError(ctx, err, root, b.Block().ParentRoot())
// this call does not have the root in forkchoice yet.
return s.handleInvalidExecutionError(ctx, err, root, b.Block().ParentRoot(), [32]byte(postVersionAndHeaders[i].header.ParentHash()))
}
if isValidPayload {
if err := s.validateMergeTransitionBlock(ctx, preVersionAndHeaders[i].version,
@@ -304,7 +316,18 @@ func (s *Service) areSidecarsAvailable(ctx context.Context, avs das.Availability
slot := block.Slot()
if blockVersion >= version.Fulu {
if err := s.areDataColumnsAvailable(ctx, roBlock.Root(), block); err != nil {
body := block.Body()
if body == nil {
return errors.New("invalid nil beacon block body")
}
kzgCommitments, err := body.BlobKzgCommitments()
if err != nil {
return errors.Wrap(err, "blob KZG commitments")
}
if len(kzgCommitments) == 0 {
return nil
}
if err := s.areDataColumnsAvailable(ctx, roBlock.Root(), slot); err != nil {
return errors.Wrapf(err, "are data columns available for block %#x with slot %d", roBlock.Root(), slot)
}
@@ -366,9 +389,49 @@ func (s *Service) updateEpochBoundaryCaches(ctx context.Context, st state.Beacon
return nil
}
// refreshCaches updates the next slot state cache and epoch boundary caches.
// Before Fulu this is done synchronously, after Fulu it is deferred to a goroutine.
func (s *Service) refreshCaches(ctx context.Context, currentSlot primitives.Slot, headRoot [32]byte, headState state.BeaconState, accessRoot [32]byte) {
lastRoot, lastState := transition.LastCachedState()
if lastState == nil {
lastRoot, lastState = headRoot[:], headState
}
if lastState.Version() < version.Fulu {
s.updateCachesAndEpochBoundary(ctx, currentSlot, headState, accessRoot, lastRoot, lastState)
} else {
go func() {
ctx, cancel := context.WithTimeout(s.ctx, slotDeadline)
defer cancel()
s.updateCachesAndEpochBoundary(ctx, currentSlot, headState, accessRoot, lastRoot, lastState)
}()
}
}
// updateCachesAndEpochBoundary updates the next slot state cache and handles
// epoch boundary processing. If the lastRoot matches accessRoot, the cached
// last state is reused; otherwise, the head state is advanced instead.
func (s *Service) updateCachesAndEpochBoundary(ctx context.Context, currentSlot primitives.Slot, headState state.BeaconState, accessRoot [32]byte, lastRoot []byte, lastState state.BeaconState) {
if bytes.Equal(lastRoot, accessRoot[:]) {
// Happy case, the last advanced state is head, we thus keep it
lastState.CopyAllTries()
if err := transition.UpdateNextSlotCache(ctx, lastRoot, lastState); err != nil {
log.WithError(err).Debug("Could not update next slot state cache")
}
} else {
// Last advanced state was not head, we do not advance this but rather use headstate
headState.CopyAllTries()
if err := transition.UpdateNextSlotCache(ctx, accessRoot[:], headState); err != nil {
log.WithError(err).Debug("Could not update next slot state cache")
}
}
if err := s.handleEpochBoundary(ctx, currentSlot, headState, accessRoot[:]); err != nil {
log.WithError(err).Error("Could not update epoch boundary caches")
}
}
// Epoch boundary tasks: it copies the headState and updates the epoch boundary
// caches. The caller of this function must not hold a lock in forkchoice store.
func (s *Service) handleEpochBoundary(ctx context.Context, slot primitives.Slot, headState state.BeaconState, blockRoot []byte) error {
func (s *Service) handleEpochBoundary(ctx context.Context, slot primitives.Slot, headState state.ReadOnlyBeaconState, blockRoot []byte) error {
ctx, span := trace.StartSpan(ctx, "blockChain.handleEpochBoundary")
defer span.End()
// return early if we are advancing to a past epoch
@@ -401,7 +464,11 @@ func (s *Service) handleBlockAttestations(ctx context.Context, blk interfaces.Re
}
r := bytesutil.ToBytes32(a.GetData().BeaconBlockRoot)
if s.cfg.ForkChoiceStore.HasNode(r) {
s.cfg.ForkChoiceStore.ProcessAttestation(ctx, indices, r, a.GetData().Target.Epoch)
payloadStatus := true
if a.GetData().Target.Epoch >= params.BeaconConfig().GloasForkEpoch {
payloadStatus = a.GetData().CommitteeIndex == 1
}
s.cfg.ForkChoiceStore.ProcessAttestation(ctx, indices, r, a.GetData().Slot, payloadStatus)
} else if features.Get().EnableExperimentalAttestationPool {
if err = s.cfg.AttestationCache.Add(a); err != nil {
return err
@@ -413,6 +480,36 @@ func (s *Service) handleBlockAttestations(ctx context.Context, blk interfaces.Re
return nil
}
// handleBlockPayloadAttestations feeds payload attestations included in a Gloas block into forkchoice.
func (s *Service) handleBlockPayloadAttestations(ctx context.Context, blk interfaces.ReadOnlyBeaconBlock, st state.BeaconState) error {
if blk.Version() < version.Gloas {
return nil
}
atts, err := blk.Body().PayloadAttestations()
if err != nil {
return err
}
if len(atts) == 0 {
return nil
}
committee, err := gloas.PayloadCommittee(ctx, st, blk.Slot()-1)
if err != nil {
return err
}
for _, att := range atts {
root := bytesutil.ToBytes32(att.Data.BeaconBlockRoot)
if !s.cfg.ForkChoiceStore.HasNode(root) {
continue
}
for i := range committee {
if att.AggregationBits.BitAt(uint64(i)) {
s.cfg.ForkChoiceStore.SetPTCVote(root, uint64(i), att.Data.PayloadPresent, att.Data.BlobDataAvailable)
}
}
}
return nil
}
// InsertSlashingsToForkChoiceStore inserts attester slashing indices to fork choice store.
// To call this function, it's caller's responsibility to ensure the slashing object is valid.
// This function requires a write lock on forkchoice.
@@ -543,6 +640,9 @@ func (s *Service) validateMergeTransitionBlock(ctx context.Context, stateVersion
if blocks.IsPreBellatrixVersion(blk.Block().Version()) {
return nil
}
if blk.Block().Version() >= version.Gloas {
return nil
}
// Skip validation if block has an empty payload.
payload, err := blk.Block().Body().Execution()
@@ -583,11 +683,22 @@ func (s *Service) runLateBlockTasks() {
return
}
attThreshold := params.BeaconConfig().SecondsPerSlot / 3
ticker := slots.NewSlotTickerWithOffset(s.genesisTime, time.Duration(attThreshold)*time.Second, params.BeaconConfig().SecondsPerSlot)
cfg := params.BeaconConfig()
attDueBPS := cfg.AttestationDueBPS
if slots.ToEpoch(s.CurrentSlot()) >= cfg.GloasForkEpoch {
attDueBPS = cfg.AttestationDueBPSGloas
}
attThreshold := cfg.SlotComponentDuration(attDueBPS)
ticker := slots.NewSlotTickerWithOffset(s.genesisTime, attThreshold, cfg.SecondsPerSlot)
for {
select {
case <-ticker.C():
case slot := <-ticker.C():
if attDueBPS != cfg.AttestationDueBPSGloas && slots.ToEpoch(slot) >= cfg.GloasForkEpoch {
ticker.Done()
attDueBPS = cfg.AttestationDueBPSGloas
attThreshold = cfg.SlotComponentDuration(attDueBPS)
ticker = slots.NewSlotTickerWithOffset(s.genesisTime, attThreshold, cfg.SecondsPerSlot)
}
s.lateBlockTasks(s.ctx)
case <-s.ctx.Done():
log.Debug("Context closed, exiting routine")
@@ -664,7 +775,18 @@ func (s *Service) isDataAvailable(
root := roBlock.Root()
blockVersion := block.Version()
if blockVersion >= version.Fulu {
return s.areDataColumnsAvailable(ctx, root, block)
body := block.Body()
if body == nil {
return errors.New("invalid nil beacon block body")
}
kzgCommitments, err := body.BlobKzgCommitments()
if err != nil {
return errors.Wrap(err, "blob KZG commitments")
}
if len(kzgCommitments) == 0 {
return nil
}
return s.areDataColumnsAvailable(ctx, root, block.Slot())
}
if blockVersion >= version.Deneb {
@@ -679,30 +801,15 @@ func (s *Service) isDataAvailable(
func (s *Service) areDataColumnsAvailable(
ctx context.Context,
root [fieldparams.RootLength]byte,
block interfaces.ReadOnlyBeaconBlock,
slot primitives.Slot,
) error {
// We are only required to check within MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS
blockSlot, currentSlot := block.Slot(), s.CurrentSlot()
blockEpoch, currentEpoch := slots.ToEpoch(blockSlot), slots.ToEpoch(currentSlot)
currentSlot := s.CurrentSlot()
blockEpoch, currentEpoch := slots.ToEpoch(slot), slots.ToEpoch(currentSlot)
if !params.WithinDAPeriod(blockEpoch, currentEpoch) {
return nil
}
body := block.Body()
if body == nil {
return errors.New("invalid nil beacon block body")
}
kzgCommitments, err := body.BlobKzgCommitments()
if err != nil {
return errors.Wrap(err, "blob KZG commitments")
}
// If block has not commitments there is nothing to wait for.
if len(kzgCommitments) == 0 {
return nil
}
// All columns to sample need to be available for the block to be considered available.
nodeID := s.cfg.P2P.NodeID()
@@ -756,7 +863,7 @@ func (s *Service) areDataColumnsAvailable(
}
// Log for DA checks that cross over into the next slot; helpful for debugging.
nextSlot, err := slots.StartTime(s.genesisTime, block.Slot()+1)
nextSlot, err := slots.StartTime(s.genesisTime, slot+1)
if err != nil {
return fmt.Errorf("unable to determine slot start time: %w", err)
}
@@ -771,7 +878,7 @@ func (s *Service) areDataColumnsAvailable(
}
log.WithFields(logrus.Fields{
"slot": block.Slot(),
"slot": slot,
"root": fmt.Sprintf("%#x", root),
"columnsExpected": helpers.SortedPrettySliceFromMap(peerInfo.CustodyColumns),
"columnsWaiting": helpers.SortedPrettySliceFromMap(missing),
@@ -817,7 +924,7 @@ func (s *Service) areDataColumnsAvailable(
missingIndices = helpers.SortedPrettySliceFromMap(missing)
}
return errors.Wrapf(ctx.Err(), "data column sidecars slot: %d, BlockRoot: %#x, missing: %v", block.Slot(), root, missingIndices)
return errors.Wrapf(ctx.Err(), "data column sidecars slot: %d, BlockRoot: %#x, missing: %v", slot, root, missingIndices)
}
}
}
@@ -915,37 +1022,20 @@ func (s *Service) lateBlockTasks(ctx context.Context) {
headRoot := s.headRoot()
headState := s.headState(ctx)
s.headLock.RUnlock()
lastRoot, lastState := transition.LastCachedState()
if lastState == nil {
lastRoot, lastState = headRoot[:], headState
}
// Before Fulu we need to process the next slot to find out if we are proposing.
if lastState.Version() < version.Fulu {
// Copy all the field tries in our cached state in the event of late
// blocks.
lastState.CopyAllTries()
if err := transition.UpdateNextSlotCache(ctx, lastRoot, lastState); err != nil {
log.WithError(err).Debug("Could not update next slot state cache")
}
if err := s.handleEpochBoundary(ctx, currentSlot, headState, headRoot[:]); err != nil {
log.WithError(err).Error("Could not update epoch boundary caches")
}
var accessRoot [32]byte
isFull, err := headState.IsParentBlockFull()
gloasFirstSlot, _ := slots.EpochStart(params.BeaconConfig().GloasForkEpoch)
if err != nil || !isFull || headState.Slot() <= gloasFirstSlot {
accessRoot = headRoot
} else {
// After Fulu, we can update the caches asynchronously after sending FCU to the engine
defer func() {
go func() {
ctx, cancel := context.WithTimeout(s.ctx, slotDeadline)
defer cancel()
lastState.CopyAllTries()
if err := transition.UpdateNextSlotCache(ctx, lastRoot, lastState); err != nil {
log.WithError(err).Debug("Could not update next slot state cache")
}
if err := s.handleEpochBoundary(ctx, currentSlot, headState, headRoot[:]); err != nil {
log.WithError(err).Error("Could not update epoch boundary caches")
}
}()
}()
accessRoot, err = headState.LatestBlockHash()
if err != nil {
log.WithError(err).Debug("could not perform late block tasks: failed to retrieve latest block hash, using head root as access root")
accessRoot = headRoot
}
}
s.refreshCaches(ctx, currentSlot, headRoot, headState, accessRoot)
// return early if we already started building a block for the current
// head root
_, has := s.cfg.PayloadIDCache.PayloadID(s.CurrentSlot()+1, headRoot)
@@ -953,12 +1043,24 @@ func (s *Service) lateBlockTasks(ctx context.Context) {
return
}
attribute := s.getPayloadAttribute(ctx, headState, s.CurrentSlot()+1, headRoot[:])
attribute := s.getPayloadAttribute(ctx, headState, s.CurrentSlot()+1, headRoot[:], accessRoot[:])
// return early if we are not proposing next slot
if attribute.IsEmpty() {
return
}
if headState.Version() >= version.Gloas {
bh, err := headState.LatestBlockHash()
if err != nil {
log.WithError(err).Debug("could not perform late block tasks: failed to retrieve latest block hash")
return
}
_, err = s.notifyForkchoiceUpdateGloas(ctx, bh, attribute)
if err != nil {
log.WithError(err).Debug("could not perform late block tasks: failed to update forkchoice with engine")
}
return
}
s.headLock.RLock()
headBlock, err := s.headBlock()
if err != nil {
@@ -992,9 +1094,10 @@ func (s *Service) waitForSync() error {
}
}
func (s *Service) handleInvalidExecutionError(ctx context.Context, err error, blockRoot, parentRoot [fieldparams.RootLength]byte) error {
// the caller of this function must hold a write lock in forkchoice store.
func (s *Service) handleInvalidExecutionError(ctx context.Context, err error, blockRoot, parentRoot [32]byte, parentHash [32]byte) error {
if IsInvalidBlock(err) && InvalidBlockLVH(err) != [32]byte{} {
return s.pruneInvalidBlock(ctx, blockRoot, parentRoot, InvalidBlockLVH(err))
return s.pruneInvalidBlock(ctx, blockRoot, parentRoot, parentHash, InvalidBlockLVH(err))
}
return err
}

View File

@@ -38,57 +38,67 @@ func (s *Service) CurrentSlot() primitives.Slot {
}
// getFCUArgs returns the arguments to call forkchoice update
func (s *Service) getFCUArgs(cfg *postBlockProcessConfig, fcuArgs *fcuConfig) error {
if err := s.getFCUArgsEarlyBlock(cfg, fcuArgs); err != nil {
return err
func (s *Service) getFCUArgs(cfg *postBlockProcessConfig) (*fcuConfig, error) {
fcuArgs, err := s.getFCUArgsEarlyBlock(cfg)
if err != nil {
return nil, err
}
fcuArgs.attributes = s.getPayloadAttribute(cfg.ctx, fcuArgs.headState, fcuArgs.proposingSlot, cfg.headRoot[:])
return nil
fcuArgs.attributes = s.getPayloadAttribute(cfg.ctx, fcuArgs.headState, fcuArgs.proposingSlot, cfg.headRoot[:], cfg.headRoot[:])
return fcuArgs, nil
}
func (s *Service) getFCUArgsEarlyBlock(cfg *postBlockProcessConfig, fcuArgs *fcuConfig) error {
func (s *Service) getFCUArgsEarlyBlock(cfg *postBlockProcessConfig) (*fcuConfig, error) {
if cfg.roblock.Root() == cfg.headRoot {
fcuArgs.headState = cfg.postState
fcuArgs.headBlock = cfg.roblock
fcuArgs.headRoot = cfg.headRoot
fcuArgs.proposingSlot = s.CurrentSlot() + 1
return nil
return &fcuConfig{
headState: cfg.postState,
headBlock: cfg.roblock,
headRoot: cfg.headRoot,
proposingSlot: s.CurrentSlot() + 1,
}, nil
}
return s.fcuArgsNonCanonicalBlock(cfg, fcuArgs)
return s.fcuArgsNonCanonicalBlock(cfg)
}
// logNonCanonicalBlockReceived prints a message informing that the received
// block is not the head of the chain. It requires the caller holds a lock on
// Forkchoice.
func (s *Service) logNonCanonicalBlockReceived(blockRoot [32]byte, headRoot [32]byte) {
receivedWeight, err := s.cfg.ForkChoiceStore.Weight(blockRoot)
receivedWeight, err := s.cfg.ForkChoiceStore.ConsensusNodeWeight(blockRoot)
if err != nil {
log.WithField("root", fmt.Sprintf("%#x", blockRoot)).Warn("Could not determine node weight")
}
headWeight, err := s.cfg.ForkChoiceStore.Weight(headRoot)
headWeight, err := s.cfg.ForkChoiceStore.ConsensusNodeWeight(headRoot)
if err != nil {
log.WithField("root", fmt.Sprintf("%#x", headRoot)).Warn("Could not determine node weight")
}
log.WithFields(logrus.Fields{
fields := logrus.Fields{
"receivedRoot": fmt.Sprintf("%#x", blockRoot),
"receivedWeight": receivedWeight,
"headRoot": fmt.Sprintf("%#x", headRoot),
"headWeight": headWeight,
}).Debug("Head block is not the received block")
}
headEmpty, headFull, err := s.cfg.ForkChoiceStore.PayloadWeights(headRoot)
if err == nil {
fields["headEmptyWeight"] = headEmpty
fields["headFullWeight"] = headFull
}
log.WithFields(fields).Debug("Head block is not the received block")
}
// fcuArgsNonCanonicalBlock returns the arguments to the FCU call when the
// incoming block is non-canonical, that is, based on the head root.
func (s *Service) fcuArgsNonCanonicalBlock(cfg *postBlockProcessConfig, fcuArgs *fcuConfig) error {
headState, headBlock, err := s.getStateAndBlock(cfg.ctx, cfg.headRoot)
func (s *Service) fcuArgsNonCanonicalBlock(cfg *postBlockProcessConfig) (*fcuConfig, error) {
headState, headBlock, err := s.getStateAndBlock(cfg.ctx, cfg.headRoot, cfg.headRoot)
if err != nil {
return err
return nil, err
}
fcuArgs.headState = headState
fcuArgs.headBlock = headBlock
fcuArgs.headRoot = cfg.headRoot
fcuArgs.proposingSlot = s.CurrentSlot() + 1
return nil
return &fcuConfig{
headState: headState,
headBlock: headBlock,
headRoot: cfg.headRoot,
proposingSlot: s.CurrentSlot() + 1,
}, nil
}
// sendStateFeedOnBlock sends an event that a new block has been synced
@@ -189,36 +199,62 @@ func reportProcessingTime(startTime time.Time) {
onBlockProcessingTime.Observe(float64(time.Since(startTime).Milliseconds()))
}
// getBlockPreState returns the pre state of an incoming block. It uses the parent root of the block
// GetPrestateToPropose returns the pre-state for a proposer to base its block on.
// It is similar to GetBlockPreState but it lacks unnecessary verifications.
func (s *Service) GetPrestateToPropose(ctx context.Context, b consensus_blocks.ROBlock) (state.BeaconState, error) {
ctx, span := trace.StartSpan(ctx, "blockChain.GetPreStateToPropose")
defer span.End()
accessRoot, err := s.getLookupParentRoot(b)
if err != nil {
return nil, errors.Wrap(err, "could not get lookup parent root")
}
bl := b.Block()
preState, err := s.cfg.StateGen.StateByRoot(ctx, accessRoot)
if err != nil {
return nil, errors.Wrapf(err, "could not get pre state for slot %d", bl.Slot())
}
if preState == nil || preState.IsNil() {
return nil, errors.Wrapf(err, "nil pre state for slot %d", bl.Slot())
}
return preState, nil
}
// GetBlockPreState returns the pre state of an incoming block. It uses the parent root of the block
// to retrieve the state in DB. It verifies the pre state's validity and the incoming block
// is in the correct time window.
func (s *Service) getBlockPreState(ctx context.Context, b interfaces.ReadOnlyBeaconBlock) (state.BeaconState, error) {
func (s *Service) GetBlockPreState(ctx context.Context, b consensus_blocks.ROBlock) (state.BeaconState, error) {
ctx, span := trace.StartSpan(ctx, "blockChain.getBlockPreState")
defer span.End()
accessRoot, err := s.getLookupParentRoot(b)
if err != nil {
return nil, errors.Wrap(err, "could not get lookup parent root")
}
// Verify incoming block has a valid pre state.
if err := s.verifyBlkPreState(ctx, b.ParentRoot()); err != nil {
if err := s.verifyBlkPreState(ctx, accessRoot); err != nil {
return nil, err
}
preState, err := s.cfg.StateGen.StateByRoot(ctx, b.ParentRoot())
bl := b.Block()
preState, err := s.cfg.StateGen.StateByRoot(ctx, accessRoot)
if err != nil {
return nil, errors.Wrapf(err, "could not get pre state for slot %d", b.Slot())
return nil, errors.Wrapf(err, "could not get pre state for slot %d", bl.Slot())
}
if preState == nil || preState.IsNil() {
return nil, errors.Wrapf(err, "nil pre state for slot %d", b.Slot())
return nil, errors.Wrapf(err, "nil pre state for slot %d", bl.Slot())
}
// Verify block slot time is not from the future.
if err := slots.VerifyTime(s.genesisTime, b.Slot(), params.BeaconConfig().MaximumGossipClockDisparityDuration()); err != nil {
if err := slots.VerifyTime(s.genesisTime, bl.Slot(), params.BeaconConfig().MaximumGossipClockDisparityDuration()); err != nil {
return nil, err
}
// Verify block is later than the finalized epoch slot.
if err := s.verifyBlkFinalizedSlot(b); err != nil {
if err := s.verifyBlkFinalizedSlot(bl); err != nil {
return nil, err
}
return preState, nil
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/OffchainLabs/prysm/v7/beacon-chain/cache"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/blocks"
statefeed "github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed/state"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/gloas"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/peerdas"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/signing"
@@ -731,13 +732,13 @@ func TestOnBlock_CanFinalize_WithOnTick(t *testing.T) {
currStoreJustifiedEpoch := service.CurrentJustifiedCheckpt().Epoch
currStoreFinalizedEpoch := service.FinalizedCheckpt().Epoch
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, r)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, r, wsb, postState))
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, r)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
require.NoError(t, service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, true}))
service.cfg.ForkChoiceStore.Unlock()
@@ -783,13 +784,13 @@ func TestOnBlock_CanFinalize(t *testing.T) {
currStoreJustifiedEpoch := service.CurrentJustifiedCheckpt().Epoch
currStoreFinalizedEpoch := service.FinalizedCheckpt().Epoch
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, r)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, r, wsb, postState))
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, r)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
require.NoError(t, service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, true}))
service.cfg.ForkChoiceStore.Unlock()
@@ -847,13 +848,13 @@ func TestOnBlock_CallNewPayloadAndForkchoiceUpdated(t *testing.T) {
wsb, err := consensusblocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, r)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, r, wsb, postState))
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, r)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
require.NoError(t, service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, false}))
service.cfg.ForkChoiceStore.Unlock()
@@ -1322,13 +1323,13 @@ func TestOnBlock_ProcessBlocksParallel(t *testing.T) {
wg.Add(4)
var lock sync.Mutex
go func() {
preState, err := service.getBlockPreState(ctx, wsb1.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb1, r1)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb1)
require.NoError(t, err)
lock.Lock()
roblock, err := consensusblocks.NewROBlockWithRoot(wsb1, r1)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
require.NoError(t, service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, true}))
service.cfg.ForkChoiceStore.Unlock()
@@ -1336,13 +1337,13 @@ func TestOnBlock_ProcessBlocksParallel(t *testing.T) {
wg.Done()
}()
go func() {
preState, err := service.getBlockPreState(ctx, wsb2.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb2, r2)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb2)
require.NoError(t, err)
lock.Lock()
roblock, err := consensusblocks.NewROBlockWithRoot(wsb2, r2)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
require.NoError(t, service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, true}))
service.cfg.ForkChoiceStore.Unlock()
@@ -1350,13 +1351,13 @@ func TestOnBlock_ProcessBlocksParallel(t *testing.T) {
wg.Done()
}()
go func() {
preState, err := service.getBlockPreState(ctx, wsb3.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb3, r3)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb3)
require.NoError(t, err)
lock.Lock()
roblock, err := consensusblocks.NewROBlockWithRoot(wsb3, r3)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
require.NoError(t, service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, true}))
service.cfg.ForkChoiceStore.Unlock()
@@ -1364,13 +1365,13 @@ func TestOnBlock_ProcessBlocksParallel(t *testing.T) {
wg.Done()
}()
go func() {
preState, err := service.getBlockPreState(ctx, wsb4.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb4, r4)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb4)
require.NoError(t, err)
lock.Lock()
roblock, err := consensusblocks.NewROBlockWithRoot(wsb4, r4)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
require.NoError(t, service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, true}))
service.cfg.ForkChoiceStore.Unlock()
@@ -1442,13 +1443,13 @@ func TestStore_NoViableHead_NewPayload(t *testing.T) {
require.NoError(t, err)
root, err := b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, root, wsb, postState))
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
require.NoError(t, service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, false}))
service.cfg.ForkChoiceStore.Unlock()
@@ -1464,13 +1465,13 @@ func TestStore_NoViableHead_NewPayload(t *testing.T) {
require.NoError(t, err)
root, err := b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, root, wsb, postState))
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
require.NoError(t, service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, false}))
service.cfg.ForkChoiceStore.Unlock()
@@ -1487,13 +1488,13 @@ func TestStore_NoViableHead_NewPayload(t *testing.T) {
root, err := b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, root, wsb, postState))
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
require.NoError(t, service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, false}))
service.cfg.ForkChoiceStore.Unlock()
@@ -1512,13 +1513,13 @@ func TestStore_NoViableHead_NewPayload(t *testing.T) {
require.NoError(t, err)
firstInvalidRoot, err := b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, firstInvalidRoot)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, firstInvalidRoot, wsb, postState))
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, firstInvalidRoot)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
err = service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, false})
service.cfg.ForkChoiceStore.Unlock()
@@ -1549,12 +1550,12 @@ func TestStore_NoViableHead_NewPayload(t *testing.T) {
root, err := b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err = service.getBlockPreState(ctx, wsb.Block())
rowsb, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
preState, err = service.GetBlockPreState(ctx, rowsb)
require.NoError(t, err)
preStateVersion, preStateHeader, err := getStateVersionAndPayload(preState)
require.NoError(t, err)
rowsb, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
_, err = service.validateExecutionOnBlock(ctx, preStateVersion, preStateHeader, rowsb)
require.ErrorContains(t, "received an INVALID payload from execution engine", err)
// Check that forkchoice's head and store's headroot are the previous head (since the invalid block did
@@ -1578,13 +1579,13 @@ func TestStore_NoViableHead_NewPayload(t *testing.T) {
require.NoError(t, err)
root, err = b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err = service.getBlockPreState(ctx, wsb.Block())
roblock, err = consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
preState, err = service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err = service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, root, wsb, postState))
roblock, err = consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
err = service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, true})
service.cfg.ForkChoiceStore.Unlock()
@@ -1647,13 +1648,13 @@ func TestStore_NoViableHead_Liveness(t *testing.T) {
root, err := b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, root, wsb, postState))
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
require.NoError(t, service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, false}))
service.cfg.ForkChoiceStore.Unlock()
@@ -1670,13 +1671,13 @@ func TestStore_NoViableHead_Liveness(t *testing.T) {
root, err := b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, root, wsb, postState))
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
require.NoError(t, service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, false}))
service.cfg.ForkChoiceStore.Unlock()
@@ -1692,13 +1693,13 @@ func TestStore_NoViableHead_Liveness(t *testing.T) {
require.NoError(t, err)
lastValidRoot, err := b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, lastValidRoot)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, lastValidRoot, wsb, postState))
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, lastValidRoot)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
err = service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, false})
service.cfg.ForkChoiceStore.Unlock()
@@ -1723,13 +1724,13 @@ func TestStore_NoViableHead_Liveness(t *testing.T) {
require.NoError(t, err)
invalidRoots[i-13], err = b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, invalidRoots[i-13])
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, invalidRoots[i-13], wsb, postState))
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, invalidRoots[i-13])
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
require.NoError(t, service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, false}))
service.cfg.ForkChoiceStore.Unlock()
@@ -1753,12 +1754,12 @@ func TestStore_NoViableHead_Liveness(t *testing.T) {
root, err := b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err = service.getBlockPreState(ctx, wsb.Block())
rowsb, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
preState, err = service.GetBlockPreState(ctx, rowsb)
require.NoError(t, err)
preStateVersion, preStateHeader, err := getStateVersionAndPayload(preState)
require.NoError(t, err)
rowsb, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
_, err = service.validateExecutionOnBlock(ctx, preStateVersion, preStateHeader, rowsb)
require.ErrorContains(t, "received an INVALID payload from execution engine", err)
@@ -1793,13 +1794,13 @@ func TestStore_NoViableHead_Liveness(t *testing.T) {
require.NoError(t, err)
root, err = b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err = service.getBlockPreState(ctx, wsb.Block())
roblock, err = consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
preState, err = service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err = service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, root, wsb, postState))
roblock, err = consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
require.NoError(t, service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, true}))
service.cfg.ForkChoiceStore.Unlock()
@@ -1820,13 +1821,13 @@ func TestStore_NoViableHead_Liveness(t *testing.T) {
require.NoError(t, err)
root, err := b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, root, wsb, postState))
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
err = service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, true})
service.cfg.ForkChoiceStore.Unlock()
@@ -1850,13 +1851,13 @@ func TestStore_NoViableHead_Liveness(t *testing.T) {
root, err = b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err = service.getBlockPreState(ctx, wsb.Block())
roblock, err = consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
preState, err = service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err = service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, root, wsb, postState))
roblock, err = consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
err = service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, true})
service.cfg.ForkChoiceStore.Unlock()
@@ -1912,13 +1913,13 @@ func TestNoViableHead_Reboot(t *testing.T) {
require.NoError(t, err)
root, err := b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, root, wsb, postState))
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
require.NoError(t, service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, false}))
service.cfg.ForkChoiceStore.Unlock()
@@ -1934,13 +1935,13 @@ func TestNoViableHead_Reboot(t *testing.T) {
require.NoError(t, err)
root, err := b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, root, wsb, postState))
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
require.NoError(t, service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, false}))
service.cfg.ForkChoiceStore.Unlock()
@@ -1956,13 +1957,13 @@ func TestNoViableHead_Reboot(t *testing.T) {
require.NoError(t, err)
lastValidRoot, err := b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, lastValidRoot)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, lastValidRoot, wsb, postState))
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, lastValidRoot)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
err = service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, false})
service.cfg.ForkChoiceStore.Unlock()
@@ -1989,13 +1990,13 @@ func TestNoViableHead_Reboot(t *testing.T) {
// Save current justified and finalized epochs for future use.
currStoreJustifiedEpoch := service.CurrentJustifiedCheckpt().Epoch
currStoreFinalizedEpoch := service.FinalizedCheckpt().Epoch
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, root, wsb, postState))
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
require.NoError(t, service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, false}))
service.cfg.ForkChoiceStore.Unlock()
@@ -2006,6 +2007,7 @@ func TestNoViableHead_Reboot(t *testing.T) {
// Check that we have justified the second epoch
jc := service.cfg.ForkChoiceStore.JustifiedCheckpoint()
require.Equal(t, primitives.Epoch(2), jc.Epoch)
time.Sleep(20 * time.Millisecond) // wait for async forkchoice update to be processed
// import block 19 to find out that the whole chain 13--18 was in fact
// invalid
@@ -2020,12 +2022,12 @@ func TestNoViableHead_Reboot(t *testing.T) {
require.NoError(t, err)
root, err := b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err = service.getBlockPreState(ctx, wsb.Block())
rowsb, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
preState, err = service.GetBlockPreState(ctx, rowsb)
require.NoError(t, err)
preStateVersion, preStateHeader, err := getStateVersionAndPayload(preState)
require.NoError(t, err)
rowsb, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
_, err = service.validateExecutionOnBlock(ctx, preStateVersion, preStateHeader, rowsb)
require.ErrorContains(t, "received an INVALID payload from execution engine", err)
@@ -2112,13 +2114,13 @@ func TestOnBlock_HandleBlockAttestations(t *testing.T) {
require.NoError(t, err)
root, err := b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, root, wsb, postState))
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
require.NoError(t, service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, false}))
service.cfg.ForkChoiceStore.Unlock()
@@ -2180,13 +2182,13 @@ func TestOnBlock_HandleBlockAttestations(t *testing.T) {
require.NoError(t, err)
root, err := b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, root, wsb, postState))
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
require.NoError(t, service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, false}))
service.cfg.ForkChoiceStore.Unlock()
@@ -2416,14 +2418,12 @@ func Test_getFCUArgs(t *testing.T) {
isValidPayload: true,
}
// error branch
fcuArgs := &fcuConfig{}
err = s.getFCUArgs(cfg, fcuArgs)
_, err = s.getFCUArgs(cfg)
require.ErrorContains(t, "block does not exist", err)
// canonical branch
cfg.headRoot = cfg.roblock.Root()
fcuArgs = &fcuConfig{}
err = s.getFCUArgs(cfg, fcuArgs)
fcuArgs, err := s.getFCUArgs(cfg)
require.NoError(t, err)
require.Equal(t, cfg.roblock.Root(), fcuArgs.headRoot)
}
@@ -2455,7 +2455,9 @@ func TestRollbackBlock(t *testing.T) {
require.NoError(t, err)
root, err := b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
@@ -2468,7 +2470,7 @@ func TestRollbackBlock(t *testing.T) {
// Set invalid parent root to trigger forkchoice error.
wsb.SetParentRoot([]byte("bad"))
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
roblock, err = consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
// Rollback block insertion into db and caches.
@@ -2513,7 +2515,9 @@ func TestRollbackBlock_SavePostStateInfo_ContextDeadline(t *testing.T) {
require.NoError(t, err)
root, err := b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
@@ -2569,13 +2573,13 @@ func TestRollbackBlock_ContextDeadline(t *testing.T) {
require.NoError(t, err)
root, err := b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, root, wsb, postState))
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
require.NoError(t, service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, false}))
service.cfg.ForkChoiceStore.Unlock()
@@ -2586,7 +2590,9 @@ func TestRollbackBlock_ContextDeadline(t *testing.T) {
require.NoError(t, err)
root, err = b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err = service.getBlockPreState(ctx, wsb.Block())
roblock, err = consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
preState, err = service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err = service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
@@ -2600,8 +2606,6 @@ func TestRollbackBlock_ContextDeadline(t *testing.T) {
// Set deadlined context when processing the block
cancCtx, canc := context.WithCancel(t.Context())
canc()
roblock, err = consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
parentRoot = roblock.Block().ParentRoot()
@@ -3477,3 +3481,219 @@ func TestProcessLightClientFinalityUpdate(t *testing.T) {
}
}
}
func TestHandleBlockPayloadAttestations(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig()
cfg.GloasForkEpoch = 0
params.OverrideBeaconConfig(cfg)
t.Run("pre-Gloas block is no-op", func(t *testing.T) {
s, _ := setupGloasService(t, &mockExecution.EngineClient{})
blk := util.NewBeaconBlockElectra()
wsb, err := consensusblocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
st, err := util.NewBeaconStateElectra()
require.NoError(t, err)
require.NoError(t, s.handleBlockPayloadAttestations(t.Context(), wsb.Block(), st))
})
t.Run("empty payload attestations", func(t *testing.T) {
s, _ := setupGloasService(t, &mockExecution.EngineClient{})
blk := util.NewBeaconBlockGloas()
wsb, err := consensusblocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
st, err := util.NewBeaconStateGloas()
require.NoError(t, err)
require.NoError(t, s.handleBlockPayloadAttestations(t.Context(), wsb.Block(), st))
})
t.Run("unknown root is skipped", func(t *testing.T) {
s, _ := setupGloasService(t, &mockExecution.EngineClient{})
ctx := t.Context()
numVals := 2048
headState := gloasStateWithValidators(t, 2, numVals)
unknownRoot := bytesutil.ToBytes32([]byte("unknown"))
bits := bitfield.NewBitvector512()
bits.SetBitAt(0, true)
blk := util.HydrateSignedBeaconBlockGloas(&ethpb.SignedBeaconBlockGloas{
Block: &ethpb.BeaconBlockGloas{
Slot: 2,
Body: &ethpb.BeaconBlockBodyGloas{
PayloadAttestations: []*ethpb.PayloadAttestation{
{
AggregationBits: bits,
Data: &ethpb.PayloadAttestationData{
BeaconBlockRoot: unknownRoot[:],
Slot: 1,
PayloadPresent: true,
BlobDataAvailable: true,
},
Signature: make([]byte, 96),
},
},
},
},
})
wsb, err := consensusblocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
require.NoError(t, s.handleBlockPayloadAttestations(ctx, wsb.Block(), headState))
})
t.Run("known root sets PTC votes", func(t *testing.T) {
s, _ := setupGloasService(t, &mockExecution.EngineClient{})
ctx := t.Context()
blockRoot := bytesutil.ToBytes32([]byte("root1"))
parentRoot := params.BeaconConfig().ZeroHash
blockHash := bytesutil.ToBytes32([]byte("hash1"))
numVals := 2048
headState := gloasStateWithValidators(t, 2, numVals)
base, insertBlk := testGloasState(t, 1, parentRoot, blockHash)
insertGloasBlock(t, s, base, insertBlk, blockRoot)
ptc, err := gloas.PayloadCommittee(ctx, headState, 1)
require.NoError(t, err)
require.NotEqual(t, 0, len(ptc))
bits := bitfield.NewBitvector512()
bits.SetBitAt(0, true)
bits.SetBitAt(2, true)
blk := util.HydrateSignedBeaconBlockGloas(&ethpb.SignedBeaconBlockGloas{
Block: &ethpb.BeaconBlockGloas{
Slot: 2,
Body: &ethpb.BeaconBlockBodyGloas{
PayloadAttestations: []*ethpb.PayloadAttestation{
{
AggregationBits: bits,
Data: &ethpb.PayloadAttestationData{
BeaconBlockRoot: blockRoot[:],
Slot: 1,
PayloadPresent: true,
BlobDataAvailable: true,
},
Signature: make([]byte, 96),
},
},
},
},
})
wsb, err := consensusblocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
require.NoError(t, s.handleBlockPayloadAttestations(ctx, wsb.Block(), headState))
})
t.Run("multiple attestations", func(t *testing.T) {
s, _ := setupGloasService(t, &mockExecution.EngineClient{})
ctx := t.Context()
blockRoot := bytesutil.ToBytes32([]byte("root1"))
parentRoot := params.BeaconConfig().ZeroHash
blockHash := bytesutil.ToBytes32([]byte("hash1"))
numVals := 2048
headState := gloasStateWithValidators(t, 2, numVals)
base, insertBlk := testGloasState(t, 1, parentRoot, blockHash)
insertGloasBlock(t, s, base, insertBlk, blockRoot)
bits1 := bitfield.NewBitvector512()
bits1.SetBitAt(0, true)
bits2 := bitfield.NewBitvector512()
bits2.SetBitAt(1, true)
blk := util.HydrateSignedBeaconBlockGloas(&ethpb.SignedBeaconBlockGloas{
Block: &ethpb.BeaconBlockGloas{
Slot: 2,
Body: &ethpb.BeaconBlockBodyGloas{
PayloadAttestations: []*ethpb.PayloadAttestation{
{
AggregationBits: bits1,
Data: &ethpb.PayloadAttestationData{
BeaconBlockRoot: blockRoot[:],
Slot: 1,
PayloadPresent: true,
BlobDataAvailable: false,
},
Signature: make([]byte, 96),
},
{
AggregationBits: bits2,
Data: &ethpb.PayloadAttestationData{
BeaconBlockRoot: blockRoot[:],
Slot: 1,
PayloadPresent: false,
BlobDataAvailable: true,
},
Signature: make([]byte, 96),
},
},
},
},
})
wsb, err := consensusblocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
require.NoError(t, s.handleBlockPayloadAttestations(ctx, wsb.Block(), headState))
})
}
func TestUpdateCachesAndEpochBoundary_MatchingRoots(t *testing.T) {
service := testServiceNoDB(t)
st, _ := util.DeterministicGenesisState(t, 1)
accessRoot := [32]byte{'a'}
service.updateCachesAndEpochBoundary(t.Context(), 1, st, accessRoot, accessRoot[:], st)
cached := transition.NextSlotState(accessRoot[:], 1)
require.NotNil(t, cached)
require.Equal(t, primitives.Slot(1), cached.Slot())
}
func TestUpdateCachesAndEpochBoundary_DifferentRoots(t *testing.T) {
service := testServiceNoDB(t)
headState, _ := util.DeterministicGenesisState(t, 1)
lastState, _ := util.DeterministicGenesisState(t, 1)
accessRoot := [32]byte{'a'}
lastRoot := [32]byte{'b'}
service.updateCachesAndEpochBoundary(t.Context(), 1, headState, accessRoot, lastRoot[:], lastState)
// Cache should be keyed by accessRoot, not lastRoot.
cached := transition.NextSlotState(accessRoot[:], 1)
require.NotNil(t, cached)
require.Equal(t, primitives.Slot(1), cached.Slot())
cached = transition.NextSlotState(lastRoot[:], 1)
require.Equal(t, true, cached == nil)
}
func TestRefreshCaches_NoCachedState(t *testing.T) {
service := testServiceNoDB(t)
st, _ := util.DeterministicGenesisState(t, 1)
headRoot := [32]byte{'h'}
service.refreshCaches(t.Context(), 1, headRoot, st, headRoot)
cached := transition.NextSlotState(headRoot[:], 1)
require.NotNil(t, cached)
require.Equal(t, primitives.Slot(1), cached.Slot())
}
func TestRefreshCaches_CachedStateMatchesAccessRoot(t *testing.T) {
service := testServiceNoDB(t)
st, _ := util.DeterministicGenesisState(t, 1)
accessRoot := [32]byte{'a'}
headRoot := [32]byte{'h'}
// Pre-populate the cache with accessRoot.
require.NoError(t, transition.UpdateNextSlotCache(t.Context(), accessRoot[:], st))
service.refreshCaches(t.Context(), 1, headRoot, st, accessRoot)
cached := transition.NextSlotState(accessRoot[:], 1)
require.NotNil(t, cached)
require.Equal(t, primitives.Slot(1), cached.Slot())
}

View File

@@ -49,7 +49,13 @@ func (s *Service) AttestationTargetState(ctx context.Context, target *ethpb.Chec
// We acquire the lock here instead than on gettAttPreState because that function gets called from UpdateHead that holds a write lock
s.cfg.ForkChoiceStore.RLock()
defer s.cfg.ForkChoiceStore.RUnlock()
return s.getAttPreState(ctx, target)
preState, err := s.getAttPreState(ctx, target)
if err != nil {
return nil, fmt.Errorf("get att pre state: %w", err)
}
return preState, nil
}
// VerifyLmdFfgConsistency verifies that attestation's LMD and FFG votes are consistency to each other.
@@ -94,6 +100,7 @@ func (s *Service) spawnProcessAttestationsRoutine() {
for {
select {
case <-s.ctx.Done():
ticker.Done()
return
case slotInterval := <-ticker.C():
if slotInterval.Interval > 0 {
@@ -133,38 +140,64 @@ func (s *Service) UpdateHead(ctx context.Context, proposingSlot primitives.Slot)
start = time.Now()
// return early if we haven't changed head
newHeadRoot, err := s.cfg.ForkChoiceStore.Head(ctx)
newHeadRoot, newHeadBlockHash, full, err := s.cfg.ForkChoiceStore.FullHead(ctx)
if err != nil {
log.WithError(err).Error("Could not compute head from new attestations")
return
}
if !s.isNewHead(newHeadRoot) {
if !s.isNewHead(newHeadRoot, full) {
return
}
log.WithField("newHeadRoot", fmt.Sprintf("%#x", newHeadRoot)).Debug("Head changed due to attestations")
headState, headBlock, err := s.getStateAndBlock(ctx, newHeadRoot)
var accessRoot [32]byte
postGloas := slots.ToEpoch(proposingSlot) >= params.BeaconConfig().GloasForkEpoch
if full && postGloas {
accessRoot = newHeadBlockHash
} else {
accessRoot = newHeadRoot
}
headState, headBlock, err := s.getStateAndBlock(ctx, newHeadRoot, accessRoot)
if err != nil {
log.WithError(err).Error("Could not get head block")
log.WithError(err).Error("Could not get head block and state")
return
}
newAttHeadElapsedTime.Observe(float64(time.Since(start).Milliseconds()))
fcuArgs := &fcuConfig{
headState: headState,
headRoot: newHeadRoot,
headBlock: headBlock,
proposingSlot: proposingSlot,
}
if s.inRegularSync() {
fcuArgs.attributes = s.getPayloadAttribute(ctx, headState, proposingSlot, newHeadRoot[:])
if fcuArgs.attributes != nil && s.shouldOverrideFCU(newHeadRoot, proposingSlot) {
attr := s.getPayloadAttribute(ctx, headState, proposingSlot, newHeadRoot[:], accessRoot[:])
if attr != nil && s.shouldOverrideFCU(newHeadRoot, proposingSlot) {
return
}
go s.forkchoiceUpdateWithExecution(s.ctx, fcuArgs)
if postGloas {
go func() {
pid, err := s.notifyForkchoiceUpdateGloas(s.ctx, newHeadBlockHash, attr)
if err != nil {
log.WithError(err).Error("Could not update forkchoice with engine")
}
if pid == nil {
if attr != nil {
log.Warn("Engine did not return a payload ID for the fork choice update with attributes")
}
return
}
var pId [8]byte
copy(pId[:], pid[:])
s.cfg.PayloadIDCache.Set(proposingSlot, newHeadRoot, pId)
}()
} else {
fcuArgs := &fcuConfig{
headState: headState,
headRoot: newHeadRoot,
headBlock: headBlock,
proposingSlot: proposingSlot,
attributes: attr,
}
go s.forkchoiceUpdateWithExecution(s.ctx, fcuArgs)
}
}
if err := s.saveHead(s.ctx, fcuArgs.headRoot, fcuArgs.headBlock, fcuArgs.headState); err != nil {
if err := s.saveHead(s.ctx, newHeadRoot, headBlock, headState); err != nil {
log.WithError(err).Error("Could not save head")
}
s.pruneAttsFromPool(s.ctx, fcuArgs.headState, fcuArgs.headBlock)
s.pruneAttsFromPool(s.ctx, headState, headBlock)
}
// This processes fork choice attestations from the pool to account for validator votes and fork choice.

View File

@@ -110,13 +110,13 @@ func TestService_ProcessAttestationsAndUpdateHead(t *testing.T) {
wsb, err := blocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := blocks.NewROBlockWithRoot(wsb, tRoot)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, tRoot, wsb, postState))
roblock, err := blocks.NewROBlockWithRoot(wsb, tRoot)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
require.NoError(t, service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, false}))
service.cfg.ForkChoiceStore.Unlock()
@@ -172,13 +172,13 @@ func TestService_UpdateHead_NoAtts(t *testing.T) {
wsb, err := blocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := blocks.NewROBlockWithRoot(wsb, tRoot)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)
require.NoError(t, service.savePostStateInfo(ctx, tRoot, wsb, postState))
roblock, err := blocks.NewROBlockWithRoot(wsb, tRoot)
require.NoError(t, err)
service.cfg.ForkChoiceStore.Lock()
require.NoError(t, service.postBlockProcess(&postBlockProcessConfig{ctx, roblock, [32]byte{}, postState, false}))
service.cfg.ForkChoiceStore.Unlock()

View File

@@ -16,6 +16,7 @@ import (
"github.com/OffchainLabs/prysm/v7/beacon-chain/slasher/types"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"github.com/OffchainLabs/prysm/v7/config/features"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
@@ -44,6 +45,8 @@ type BlockReceiver interface {
HasBlock(ctx context.Context, root [32]byte) bool
RecentBlockSlot(root [32]byte) (primitives.Slot, error)
BlockBeingSynced([32]byte) bool
GetBlockPreState(ctx context.Context, b blocks.ROBlock) (state.BeaconState, error)
GetPrestateToPropose(ctx context.Context, b blocks.ROBlock) (state.BeaconState, error)
}
// BlobReceiver interface defines the methods of chain service for receiving new
@@ -94,18 +97,17 @@ func (s *Service) ReceiveBlock(ctx context.Context, block interfaces.ReadOnlySig
if err != nil {
return errors.Wrap(err, "block copy")
}
preState, err := s.getBlockPreState(ctx, blockCopy.Block())
if err != nil {
return errors.Wrap(err, "could not get block's prestate")
}
currentCheckpoints := s.saveCurrentCheckpoints(preState)
roblock, err := blocks.NewROBlockWithRoot(blockCopy, blockRoot)
if err != nil {
return errors.Wrap(err, "new ro block with root")
}
preState, err := s.GetBlockPreState(ctx, roblock)
if err != nil {
return errors.Wrap(err, "could not get block's prestate")
}
currentCheckpoints := s.saveCurrentCheckpoints(preState)
postState, isValidPayload, err := s.validateExecutionAndConsensus(ctx, preState, roblock)
if err != nil {
return errors.Wrap(err, "validator execution and consensus")
@@ -151,7 +153,7 @@ func (s *Service) ReceiveBlock(ctx context.Context, block interfaces.ReadOnlySig
// Have we been finalizing? Should we start saving hot states to db?
if err := s.checkSaveHotStateDB(ctx); err != nil {
return errors.Wrap(err, "check save hot state db")
log.WithError(err).Error("Could not check save hot state DB")
}
// We apply the same heuristic to some of our more important caches.
@@ -210,6 +212,16 @@ func (s *Service) validateExecutionAndConsensus(
preState state.BeaconState,
block blocks.ROBlock,
) (state.BeaconState, bool, error) {
if block.Version() >= version.Gloas {
postState, err := s.validateStateTransition(ctx, preState, block)
if errors.Is(err, ErrNotDescendantOfFinalized) {
return nil, false, invalidBlock{error: err, root: block.Root()}
}
if err != nil {
return nil, false, errors.Wrap(err, "failed to validate consensus state transition function")
}
return postState, false, nil
}
preStateVersion, preStateHeader, err := getStateVersionAndPayload(preState)
if err != nil {
return nil, false, err
@@ -243,6 +255,10 @@ func (s *Service) validateExecutionAndConsensus(
}
func (s *Service) handleDA(ctx context.Context, avs das.AvailabilityChecker, block blocks.ROBlock) (time.Duration, error) {
// Gloas DA is handled on the payload enevelope.
if block.Version() >= version.Gloas {
return 0, nil
}
var err error
start := time.Now()
if avs != nil {
@@ -258,32 +274,55 @@ func (s *Service) handleDA(ctx context.Context, avs das.AvailabilityChecker, blo
}
func (s *Service) reportPostBlockProcessing(
block interfaces.SignedBeaconBlock,
signedBlock interfaces.SignedBeaconBlock,
blockRoot [32]byte,
receivedTime time.Time,
daWaitedTime time.Duration,
) {
block := signedBlock.Block()
if block == nil {
log.WithField("blockRoot", blockRoot).Error("Nil block")
return
}
// Reports on block and fork choice metrics.
cp := s.cfg.ForkChoiceStore.FinalizedCheckpoint()
finalized := &ethpb.Checkpoint{Epoch: cp.Epoch, Root: bytesutil.SafeCopyBytes(cp.Root[:])}
reportSlotMetrics(block.Block().Slot(), s.HeadSlot(), s.CurrentSlot(), finalized)
reportSlotMetrics(block.Slot(), s.HeadSlot(), s.CurrentSlot(), finalized)
// Log block sync status.
cp = s.cfg.ForkChoiceStore.JustifiedCheckpoint()
justified := &ethpb.Checkpoint{Epoch: cp.Epoch, Root: bytesutil.SafeCopyBytes(cp.Root[:])}
if err := logBlockSyncStatus(block.Block(), blockRoot, justified, finalized, receivedTime, s.genesisTime, daWaitedTime); err != nil {
if err := logBlockSyncStatus(block, blockRoot, justified, finalized, receivedTime, s.genesisTime, daWaitedTime); err != nil {
log.WithError(err).Error("Unable to log block sync status")
}
// Log payload data
if err := logPayload(block.Block()); err != nil {
if err := logPayload(block); err != nil {
log.WithError(err).Error("Unable to log debug block payload data")
}
// Log state transition data.
if err := logStateTransitionData(block.Block()); err != nil {
if err := logStateTransitionData(block); err != nil {
log.WithError(err).Error("Unable to log state transition data")
}
timeWithoutDaWait := time.Since(receivedTime) - daWaitedTime
chainServiceProcessingTime.Observe(float64(timeWithoutDaWait.Milliseconds()))
body := block.Body()
if body == nil {
log.WithField("blockRoot", blockRoot).Error("Nil block body")
return
}
commitments, err := body.BlobKzgCommitments()
if err != nil {
log.WithError(err).Error("Unable to get blob KZG commitments")
}
commitmentCount.Observe(float64(len(commitments)))
maxBlobsPerBlock.Set(float64(params.BeaconConfig().MaxBlobsPerBlock(block.Slot())))
}
func (s *Service) executePostFinalizationTasks(ctx context.Context, finalizedState state.BeaconState) {
@@ -327,6 +366,8 @@ func (s *Service) executePostFinalizationTasks(ctx context.Context, finalizedSta
}
}()
}
go s.checkpointStateCache.EvictUpTo(finalized.Epoch)
}
// ReceiveBlockBatch processes the whole block batch at once, assuming the block batch is linear ,transitioning
@@ -609,7 +650,7 @@ func (s *Service) validateExecutionOnBlock(ctx context.Context, ver int, header
isValidPayload, err := s.notifyNewPayload(ctx, ver, header, block)
if err != nil {
s.cfg.ForkChoiceStore.Lock()
err = s.handleInvalidExecutionError(ctx, err, block.Root(), block.Block().ParentRoot())
err = s.handleInvalidExecutionError(ctx, err, block.Root(), block.Block().ParentRoot(), [32]byte(header.BlockHash()))
s.cfg.ForkChoiceStore.Unlock()
return false, err
}

View File

@@ -0,0 +1,330 @@
package blockchain
import (
"bytes"
"context"
"fmt"
"time"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed"
statefeed "github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed/state"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/gloas"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/transition"
"github.com/OffchainLabs/prysm/v7/beacon-chain/execution"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
payloadattribute "github.com/OffchainLabs/prysm/v7/consensus-types/payload-attribute"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
"github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace"
enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/runtime/version"
"github.com/ethereum/go-ethereum/common"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
)
// ExecutionPayloadEnvelopeReceiver defines the methods for receiving execution payload envelopes.
type ExecutionPayloadEnvelopeReceiver interface {
ReceiveExecutionPayloadEnvelope(context.Context, interfaces.ROSignedExecutionPayloadEnvelope) error
}
// ReceiveExecutionPayloadEnvelope processes a signed execution payload envelope for the Gloas fork.
func (s *Service) ReceiveExecutionPayloadEnvelope(ctx context.Context, signed interfaces.ROSignedExecutionPayloadEnvelope) (err error) {
ctx, span := trace.StartSpan(ctx, "blockChain.ReceiveExecutionPayloadEnvelope")
defer span.End()
start := time.Now()
defer func() {
beaconExecutionPayloadEnvelopeProcessingDurationSeconds.Observe(time.Since(start).Seconds())
if err != nil {
beaconExecutionPayloadEnvelopeInvalidTotal.Inc()
return
}
beaconExecutionPayloadEnvelopeValidTotal.Inc()
}()
envelope, err := signed.Envelope()
if err != nil {
return errors.Wrap(err, "could not get envelope")
}
root := envelope.BeaconBlockRoot()
err = s.payloadBeingSynced.set(root)
if errors.Is(err, errBlockBeingSynced) {
log.WithField("blockRoot", fmt.Sprintf("%#x", root)).Debug("Ignoring payload envelope currently being synced")
return nil
}
defer s.payloadBeingSynced.unset(root)
preState, err := s.getPayloadEnvelopePrestate(ctx, envelope)
if err != nil {
return err
}
var isValidPayload bool
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
return gloas.ProcessExecutionPayload(gCtx, preState, signed)
})
g.Go(func() error {
var elErr error
isValidPayload, elErr = s.validateExecutionOnEnvelope(gCtx, preState, envelope)
return elErr
})
if err := g.Wait(); err != nil {
return err
}
// DA check: verify data columns are available before inserting payload.
bid, err := preState.LatestExecutionPayloadBid()
if err != nil {
return errors.Wrap(err, "could not get latest execution payload bid")
}
if len(bid.BlobKzgCommitments()) > 0 {
if err := s.areDataColumnsAvailable(ctx, root, envelope.Slot()); err != nil {
return errors.Wrap(err, "data availability check failed for payload envelope")
}
}
if err := s.savePostPayload(ctx, signed, preState); err != nil {
return err
}
if err := s.InsertPayload(envelope); err != nil {
return errors.Wrap(err, "could not insert payload into forkchoice")
}
if isValidPayload {
s.cfg.ForkChoiceStore.Lock()
if err := s.cfg.ForkChoiceStore.SetOptimisticToValid(ctx, root); err != nil {
log.WithError(err).Error("Could not set optimistic to valid")
}
s.cfg.ForkChoiceStore.Unlock()
}
headRoot, err := s.HeadRoot(ctx)
if err != nil {
log.WithError(err).Error("Could not get head root")
return nil
}
if err := s.postPayloadHeadUpdate(ctx, envelope, preState, root, headRoot); err != nil {
return err
}
s.cfg.StateNotifier.StateFeed().Send(&feed.Event{
Type: statefeed.PayloadProcessed,
Data: &statefeed.PayloadProcessedData{
Slot: envelope.Slot(),
BlockRoot: root,
},
})
execution, err := envelope.Execution()
if err != nil {
log.WithError(err).Error("Could not get execution payload from envelope for logging")
return nil
}
log.WithFields(logrus.Fields{
"slot": envelope.Slot(),
"blockRoot": fmt.Sprintf("%#x", bytesutil.Trunc(root[:])),
"blockHash": fmt.Sprintf("%#x", bytesutil.Trunc(execution.BlockHash())),
"parentHash": fmt.Sprintf("%#x", bytesutil.Trunc(execution.ParentHash())),
}).Info("Processed execution payload envelope")
return nil
}
func (s *Service) postPayloadHeadUpdate(ctx context.Context, envelope interfaces.ROExecutionPayloadEnvelope, st state.BeaconState, root [32]byte, headRoot []byte) error {
if !bytes.Equal(headRoot, root[:]) {
return nil
}
payload, err := envelope.Execution()
if err != nil {
return errors.Wrap(err, "could not get execution payload from envelope")
}
blockHash := bytesutil.ToBytes32(payload.BlockHash())
s.headLock.Lock()
s.head.state = st
s.head.full = true
s.headLock.Unlock()
go func() {
ctx, cancel := context.WithTimeout(s.ctx, slotDeadline)
defer cancel()
if err := transition.UpdateNextSlotCache(ctx, blockHash[:], st); err != nil {
log.WithError(err).Error("Could not update next slot cache")
}
if err := s.handleEpochBoundary(ctx, envelope.Slot(), st, blockHash[:]); err != nil {
log.WithError(err).Error("Could not handle epoch boundary")
}
}()
attr := s.getPayloadAttribute(ctx, st, envelope.Slot()+1, headRoot, blockHash[:])
if s.inRegularSync() {
go func() {
pid, err := s.notifyForkchoiceUpdateGloas(s.ctx, blockHash, attr)
if err != nil {
log.WithError(err).Error("Could not notify forkchoice update")
return
}
if attr != nil && !attr.IsEmpty() && pid != nil {
var pId [8]byte
copy(pId[:], pid[:])
s.cfg.PayloadIDCache.Set(envelope.Slot()+1, root, pId)
}
}()
}
return nil
}
func (s *Service) getPayloadEnvelopePrestate(ctx context.Context, envelope interfaces.ROExecutionPayloadEnvelope) (state.BeaconState, error) {
ctx, span := trace.StartSpan(ctx, "blockChain.getPayloadEnvelopePrestate")
defer span.End()
root := envelope.BeaconBlockRoot()
if !s.InForkchoice(root) {
return nil, fmt.Errorf("beacon block root %#x not found in forkchoice", root)
}
if err := s.verifyBlkPreState(ctx, root); err != nil {
return nil, errors.Wrap(err, "could not verify pre-state")
}
preState, err := s.cfg.StateGen.StateByRoot(ctx, root)
if err != nil {
return nil, errors.Wrap(err, "could not get pre-state by root")
}
if preState == nil || preState.IsNil() {
return nil, fmt.Errorf("nil pre-state for beacon block root %#x", root)
}
return preState, nil
}
// The returned boolean indicates whether the payload was valid or if it was accepted as syncing (optimistic).
func (s *Service) notifyNewEnvelope(ctx context.Context, st state.BeaconState, envelope interfaces.ROExecutionPayloadEnvelope) (bool, error) {
ctx, span := trace.StartSpan(ctx, "blockChain.notifyNewEnvelope")
defer span.End()
payload, err := envelope.Execution()
if err != nil {
return false, errors.Wrap(err, "could not get execution payload from envelope")
}
latestBid, err := st.LatestExecutionPayloadBid()
if err != nil {
return false, errors.Wrap(err, "could not get latest execution payload bid")
}
commitments := latestBid.BlobKzgCommitments()
versionedHashes := make([]common.Hash, len(commitments))
for i, c := range commitments {
versionedHashes[i] = primitives.ConvertKzgCommitmentToVersionedHash(c)
}
parentRoot := common.Hash(bytesutil.ToBytes32(st.LatestBlockHeader().ParentRoot))
requests := envelope.ExecutionRequests()
_, err = s.cfg.ExecutionEngineCaller.NewPayload(ctx, payload, versionedHashes, &parentRoot, requests)
if err == nil {
return true, nil
}
if errors.Is(err, execution.ErrAcceptedSyncingPayloadStatus) {
log.WithFields(logrus.Fields{
"slot": envelope.Slot(),
"payloadBlockHash": fmt.Sprintf("%#x", bytesutil.Trunc(payload.BlockHash())),
}).Info("Called new payload with optimistic envelope")
return false, nil
}
if errors.Is(err, execution.ErrInvalidPayloadStatus) {
return false, invalidBlock{error: ErrInvalidPayload}
}
return false, errors.WithMessage(ErrUndefinedExecutionEngineError, err.Error())
}
func (s *Service) validateExecutionOnEnvelope(ctx context.Context, st state.BeaconState, envelope interfaces.ROExecutionPayloadEnvelope) (bool, error) {
isValid, err := s.notifyNewEnvelope(ctx, st, envelope)
if err == nil {
return isValid, nil
}
blockRoot := envelope.BeaconBlockRoot()
parentRoot := bytesutil.ToBytes32(st.LatestBlockHeader().ParentRoot)
payload, payloadErr := envelope.Execution()
if payloadErr != nil {
return false, errors.Wrap(payloadErr, "could not get execution payload from envelope")
}
parentHash := bytesutil.ToBytes32(payload.ParentHash())
s.cfg.ForkChoiceStore.Lock()
defer s.cfg.ForkChoiceStore.Unlock()
return false, s.handleInvalidExecutionError(ctx, err, blockRoot, parentRoot, parentHash)
}
func (s *Service) savePostPayload(ctx context.Context, signed interfaces.ROSignedExecutionPayloadEnvelope, st state.BeaconState) error {
ctx, span := trace.StartSpan(ctx, "blockChain.savePostPayload")
defer span.End()
protoEnv, ok := signed.Proto().(*ethpb.SignedExecutionPayloadEnvelope)
if !ok {
return errors.New("could not type assert signed envelope to proto")
}
if err := s.cfg.BeaconDB.SaveExecutionPayloadEnvelope(ctx, protoEnv); err != nil {
return errors.Wrap(err, "could not save execution payload envelope")
}
envelope, err := signed.Envelope()
if err != nil {
return errors.Wrap(err, "could not get envelope")
}
payload, err := envelope.Execution()
if err != nil {
return errors.Wrap(err, "could not get execution payload from envelope")
}
return s.cfg.StateGen.SaveState(ctx, bytesutil.ToBytes32(payload.BlockHash()), st)
}
// notifyForkchoiceUpdateGloas takes the block hash directly because Gloas
// blocks don't carry an execution payload in the body.
func (s *Service) notifyForkchoiceUpdateGloas(ctx context.Context, blockHash [32]byte, attributes payloadattribute.Attributer) (*enginev1.PayloadIDBytes, error) {
ctx, span := trace.StartSpan(ctx, "blockChain.notifyForkchoiceUpdateGloas")
defer span.End()
s.cfg.ForkChoiceStore.RLock()
finalizedHash := s.cfg.ForkChoiceStore.FinalizedPayloadBlockHash()
justifiedHash := s.cfg.ForkChoiceStore.UnrealizedJustifiedPayloadBlockHash()
s.cfg.ForkChoiceStore.RUnlock()
fcs := &enginev1.ForkchoiceState{
HeadBlockHash: blockHash[:],
SafeBlockHash: justifiedHash[:],
FinalizedBlockHash: finalizedHash[:],
}
if attributes == nil {
attributes = payloadattribute.EmptyWithVersion(version.Gloas)
}
payloadID, lastValidHash, err := s.cfg.ExecutionEngineCaller.ForkchoiceUpdated(ctx, fcs, attributes)
if err == nil {
return payloadID, nil
}
switch {
case errors.Is(err, execution.ErrAcceptedSyncingPayloadStatus):
log.WithFields(logrus.Fields{
"headBlockHash": fmt.Sprintf("%#x", bytesutil.Trunc(blockHash[:])),
"finalizedPayloadBlockHash": fmt.Sprintf("%#x", bytesutil.Trunc(finalizedHash[:])),
}).Info("Called forkchoice updated with optimistic block (Gloas)")
return payloadID, nil
case errors.Is(err, execution.ErrInvalidPayloadStatus):
if len(lastValidHash) == 0 {
lastValidHash = defaultLatestValidHash
}
return nil, invalidBlock{
error: ErrInvalidPayload,
lastValidHash: bytesutil.ToBytes32(lastValidHash),
}
default:
log.WithError(err).Error(ErrUndefinedExecutionEngineError)
return nil, nil
}
}

View File

@@ -0,0 +1,39 @@
package blockchain
import (
"context"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/gloas"
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
"github.com/pkg/errors"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
)
// PayloadAttestationReceiver interface defines the methods of chain service for receiving
// validated payload attestation messages.
type PayloadAttestationReceiver interface {
ReceivePayloadAttestationMessage(context.Context, *ethpb.PayloadAttestationMessage) error
}
// ReceivePayloadAttestationMessage accepts a payload attestation message and updates the
// forkchoice PTC vote bitvectors for the referenced beacon block.
func (s *Service) ReceivePayloadAttestationMessage(ctx context.Context, a *ethpb.PayloadAttestationMessage) error {
if a == nil || a.Data == nil {
return errors.New("nil payload attestation message")
}
root := bytesutil.ToBytes32(a.Data.BeaconBlockRoot)
st, err := s.HeadStateReadOnly(ctx)
if err != nil {
return err
}
idx, err := gloas.PayloadCommitteeIndex(ctx, st, a.Data.Slot, a.ValidatorIndex)
if err != nil {
return err
}
s.cfg.ForkChoiceStore.Lock()
defer s.cfg.ForkChoiceStore.Unlock()
s.cfg.ForkChoiceStore.SetPTCVote(root, idx, a.Data.PayloadPresent, a.Data.BlobDataAvailable)
return nil
}

View File

@@ -0,0 +1,143 @@
package blockchain
import (
"testing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/gloas"
mockExecution "github.com/OffchainLabs/prysm/v7/beacon-chain/execution/testing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/testing/require"
"github.com/OffchainLabs/prysm/v7/testing/util"
)
func TestReceivePayloadAttestationMessage_NilMessage(t *testing.T) {
s, _ := setupGloasService(t, &mockExecution.EngineClient{})
err := s.ReceivePayloadAttestationMessage(t.Context(), nil)
require.ErrorContains(t, "nil payload attestation message", err)
}
func TestReceivePayloadAttestationMessage_NilData(t *testing.T) {
s, _ := setupGloasService(t, &mockExecution.EngineClient{})
msg := &ethpb.PayloadAttestationMessage{}
err := s.ReceivePayloadAttestationMessage(t.Context(), msg)
require.ErrorContains(t, "nil payload attestation message", err)
}
func TestReceivePayloadAttestationMessage_ValidatorNotInPTC(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig()
cfg.GloasForkEpoch = 0
params.OverrideBeaconConfig(cfg)
s, _ := setupGloasService(t, &mockExecution.EngineClient{})
ctx := t.Context()
blockRoot := bytesutil.ToBytes32([]byte("root1"))
parentRoot := params.BeaconConfig().ZeroHash
blockHash := bytesutil.ToBytes32([]byte("hash1"))
numVals := 2048
headState := gloasStateWithValidators(t, 1, numVals)
base, blk := testGloasState(t, 1, parentRoot, blockHash)
insertGloasBlock(t, s, base, blk, blockRoot)
wsb, err := blocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
s.head = &head{root: blockRoot, block: wsb, state: headState, slot: 1}
ptc, err := gloas.PayloadCommittee(ctx, headState, 1)
require.NoError(t, err)
// Pick a validator index not in the PTC.
inPTC := make(map[primitives.ValidatorIndex]bool)
for _, idx := range ptc {
inPTC[idx] = true
}
var notInPTC primitives.ValidatorIndex
for i := primitives.ValidatorIndex(0); int(i) < numVals; i++ {
if !inPTC[i] {
notInPTC = i
break
}
}
msg := &ethpb.PayloadAttestationMessage{
ValidatorIndex: notInPTC,
Data: &ethpb.PayloadAttestationData{
BeaconBlockRoot: blockRoot[:],
Slot: 1,
},
}
err = s.ReceivePayloadAttestationMessage(ctx, msg)
require.ErrorContains(t, "validator not in PTC", err)
}
func TestReceivePayloadAttestationMessage_OK(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig()
cfg.GloasForkEpoch = 0
params.OverrideBeaconConfig(cfg)
s, _ := setupGloasService(t, &mockExecution.EngineClient{})
ctx := t.Context()
blockRoot := bytesutil.ToBytes32([]byte("root1"))
parentRoot := params.BeaconConfig().ZeroHash
blockHash := bytesutil.ToBytes32([]byte("hash1"))
headState := gloasStateWithValidators(t, 1, 2048)
base, blk := testGloasState(t, 1, parentRoot, blockHash)
insertGloasBlock(t, s, base, blk, blockRoot)
wsb, err := blocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
s.head = &head{root: blockRoot, block: wsb, state: headState, slot: 1}
ptc, err := gloas.PayloadCommittee(ctx, headState, 1)
require.NoError(t, err)
require.NotEqual(t, 0, len(ptc))
msg := &ethpb.PayloadAttestationMessage{
ValidatorIndex: ptc[0],
Data: &ethpb.PayloadAttestationData{
BeaconBlockRoot: blockRoot[:],
Slot: 1,
PayloadPresent: true,
BlobDataAvailable: true,
},
}
require.NoError(t, s.ReceivePayloadAttestationMessage(ctx, msg))
}
// gloasStateWithValidators returns a Gloas beacon state with active validators
// for PTC committee computation.
func gloasStateWithValidators(t *testing.T, slot primitives.Slot, numVals int) state.BeaconState {
t.Helper()
validators := make([]*ethpb.Validator, numVals)
balances := make([]uint64, numVals)
for i := range validators {
validators[i] = &ethpb.Validator{
PublicKey: make([]byte, 48),
WithdrawalCredentials: make([]byte, 32),
EffectiveBalance: params.BeaconConfig().MaxEffectiveBalanceElectra,
ActivationEpoch: 0,
ExitEpoch: params.BeaconConfig().FarFutureEpoch,
}
balances[i] = params.BeaconConfig().MaxEffectiveBalanceElectra
}
st, err := util.NewBeaconStateGloas(func(s *ethpb.BeaconStateGloas) error {
s.Slot = slot
s.Validators = validators
s.Balances = balances
return nil
})
require.NoError(t, err)
return st
}

View File

@@ -14,7 +14,6 @@ import (
"github.com/OffchainLabs/prysm/v7/beacon-chain/cache"
statefeed "github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed/state"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/peerdas"
coreTime "github.com/OffchainLabs/prysm/v7/beacon-chain/core/time"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/transition"
"github.com/OffchainLabs/prysm/v7/beacon-chain/db"
@@ -30,7 +29,6 @@ import (
"github.com/OffchainLabs/prysm/v7/beacon-chain/startup"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state/stategen"
"github.com/OffchainLabs/prysm/v7/cmd/beacon-chain/flags"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
@@ -64,6 +62,7 @@ type Service struct {
syncComplete chan struct{}
blobNotifiers *blobNotifierMap
blockBeingSynced *currentlySyncingBlock
payloadBeingSynced *currentlySyncingBlock
blobStorage *filesystem.BlobStorage
dataColumnStorage *filesystem.DataColumnStorage
slasherEnabled bool
@@ -188,6 +187,7 @@ func NewService(ctx context.Context, opts ...Option) (*Service, error) {
blobNotifiers: bn,
cfg: &config{},
blockBeingSynced: &currentlySyncingBlock{roots: make(map[[32]byte]struct{})},
payloadBeingSynced: &currentlySyncingBlock{roots: make(map[[32]byte]struct{})},
syncCommitteeHeadState: cache.NewSyncCommitteeHeadState(),
}
for _, opt := range opts {
@@ -213,6 +213,7 @@ func (s *Service) Start() {
}
s.spawnProcessAttestationsRoutine()
go s.runLateBlockTasks()
go s.runLatePayloadTasks()
}
// Stop the blockchain service's main event loop and associated goroutines.
@@ -291,19 +292,6 @@ func (s *Service) StartFromSavedState(saved state.BeaconState) error {
return errors.Wrap(err, "failed to initialize blockchain service")
}
if !params.FuluEnabled() {
return nil
}
earliestAvailableSlot, custodySubnetCount, err := s.updateCustodyInfoInDB(saved.Slot())
if err != nil {
return errors.Wrap(err, "could not get and save custody group count")
}
if _, _, err := s.cfg.P2P.UpdateCustodyInfo(earliestAvailableSlot, custodySubnetCount); err != nil {
return errors.Wrap(err, "update custody info")
}
return nil
}
@@ -356,7 +344,7 @@ func (s *Service) initializeHead(ctx context.Context, st state.BeaconState) erro
return errors.Wrap(err, "could not get head state")
}
}
if err := s.setHead(&head{root, blk, st, blk.Block().Slot(), false}); err != nil {
if err := s.setHead(&head{root, blk, st, blk.Block().Slot(), false, false}); err != nil {
return errors.Wrap(err, "could not set head")
}
log.WithFields(logrus.Fields{
@@ -445,6 +433,7 @@ func (s *Service) saveGenesisData(ctx context.Context, genesisState state.Beacon
genesisState,
genesisBlk.Block().Slot(),
false,
false,
}); err != nil {
log.WithError(err).Fatal("Could not set head")
}
@@ -468,73 +457,6 @@ func (s *Service) removeStartupState() {
s.cfg.FinalizedStateAtStartUp = nil
}
// UpdateCustodyInfoInDB updates the custody information in the database.
// It returns the (potentially updated) custody group count and the earliest available slot.
func (s *Service) updateCustodyInfoInDB(slot primitives.Slot) (primitives.Slot, uint64, error) {
isSupernode := flags.Get().Supernode
isSemiSupernode := flags.Get().SemiSupernode
cfg := params.BeaconConfig()
custodyRequirement := cfg.CustodyRequirement
// Check if the node was previously subscribed to all data subnets, and if so,
// store the new status accordingly.
wasSupernode, err := s.cfg.BeaconDB.UpdateSubscribedToAllDataSubnets(s.ctx, isSupernode)
if err != nil {
return 0, 0, errors.Wrap(err, "update subscribed to all data subnets")
}
// Compute the target custody group count based on current flag configuration.
targetCustodyGroupCount := custodyRequirement
// Supernode: custody all groups (either currently set or previously enabled)
if isSupernode {
targetCustodyGroupCount = cfg.NumberOfCustodyGroups
}
// Semi-supernode: custody minimum needed for reconstruction, or custody requirement if higher
if isSemiSupernode {
semiSupernodeCustody, err := peerdas.MinimumCustodyGroupCountToReconstruct()
if err != nil {
return 0, 0, errors.Wrap(err, "minimum custody group count")
}
targetCustodyGroupCount = max(custodyRequirement, semiSupernodeCustody)
}
// Safely compute the fulu fork slot.
fuluForkSlot, err := fuluForkSlot()
if err != nil {
return 0, 0, errors.Wrap(err, "fulu fork slot")
}
// If slot is before the fulu fork slot, then use the earliest stored slot as the reference slot.
if slot < fuluForkSlot {
slot, err = s.cfg.BeaconDB.EarliestSlot(s.ctx)
if err != nil {
return 0, 0, errors.Wrap(err, "earliest slot")
}
}
earliestAvailableSlot, actualCustodyGroupCount, err := s.cfg.BeaconDB.UpdateCustodyInfo(s.ctx, slot, targetCustodyGroupCount)
if err != nil {
return 0, 0, errors.Wrap(err, "update custody info")
}
if isSupernode {
log.WithFields(logrus.Fields{
"current": actualCustodyGroupCount,
"target": cfg.NumberOfCustodyGroups,
}).Info("Supernode mode enabled. Will custody all data columns going forward.")
}
if wasSupernode && !isSupernode {
log.Warningf("Because the `--%s` flag was previously used, the node will continue to act as a super node.", flags.Supernode.Name)
}
return earliestAvailableSlot, actualCustodyGroupCount, nil
}
func spawnCountdownIfPreGenesis(ctx context.Context, genesisTime time.Time, db db.HeadAccessDatabase) {
currentTime := prysmTime.Now()
if currentTime.After(genesisTime) {
@@ -551,19 +473,3 @@ func spawnCountdownIfPreGenesis(ctx context.Context, genesisTime time.Time, db d
}
go slots.CountdownToGenesis(ctx, genesisTime, uint64(gState.NumValidators()), gRoot)
}
func fuluForkSlot() (primitives.Slot, error) {
cfg := params.BeaconConfig()
fuluForkEpoch := cfg.FuluForkEpoch
if fuluForkEpoch == cfg.FarFutureEpoch {
return cfg.FarFutureSlot, nil
}
forkFuluSlot, err := slots.EpochStart(fuluForkEpoch)
if err != nil {
return 0, errors.Wrap(err, "epoch start")
}
return forkFuluSlot, nil
}

View File

@@ -23,11 +23,9 @@ import (
"github.com/OffchainLabs/prysm/v7/beacon-chain/startup"
state_native "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state/stategen"
"github.com/OffchainLabs/prysm/v7/cmd/beacon-chain/flags"
"github.com/OffchainLabs/prysm/v7/config/features"
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
consensusblocks "github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
@@ -596,218 +594,3 @@ func TestNotifyIndex(t *testing.T) {
t.Errorf("Notifier channel did not receive the index")
}
}
func TestUpdateCustodyInfoInDB(t *testing.T) {
const (
fuluForkEpoch = 10
custodyRequirement = uint64(4)
earliestStoredSlot = primitives.Slot(12)
numberOfCustodyGroups = uint64(64)
)
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig()
cfg.FuluForkEpoch = fuluForkEpoch
cfg.CustodyRequirement = custodyRequirement
cfg.NumberOfCustodyGroups = numberOfCustodyGroups
params.OverrideBeaconConfig(cfg)
ctx := t.Context()
pbBlock := util.NewBeaconBlock()
pbBlock.Block.Slot = 12
signedBeaconBlock, err := blocks.NewSignedBeaconBlock(pbBlock)
require.NoError(t, err)
roBlock, err := blocks.NewROBlock(signedBeaconBlock)
require.NoError(t, err)
t.Run("CGC increases before fulu", func(t *testing.T) {
service, requirements := minimalTestService(t)
err = requirements.db.SaveBlock(ctx, roBlock)
require.NoError(t, err)
// Before Fulu
// -----------
actualEas, actualCgc, err := service.updateCustodyInfoInDB(15)
require.NoError(t, err)
require.Equal(t, earliestStoredSlot, actualEas)
require.Equal(t, custodyRequirement, actualCgc)
actualEas, actualCgc, err = service.updateCustodyInfoInDB(17)
require.NoError(t, err)
require.Equal(t, earliestStoredSlot, actualEas)
require.Equal(t, custodyRequirement, actualCgc)
resetFlags := flags.Get()
gFlags := new(flags.GlobalFlags)
gFlags.Supernode = true
flags.Init(gFlags)
defer flags.Init(resetFlags)
actualEas, actualCgc, err = service.updateCustodyInfoInDB(19)
require.NoError(t, err)
require.Equal(t, earliestStoredSlot, actualEas)
require.Equal(t, numberOfCustodyGroups, actualCgc)
// After Fulu
// ----------
actualEas, actualCgc, err = service.updateCustodyInfoInDB(fuluForkEpoch*primitives.Slot(cfg.SlotsPerEpoch) + 1)
require.NoError(t, err)
require.Equal(t, earliestStoredSlot, actualEas)
require.Equal(t, numberOfCustodyGroups, actualCgc)
})
t.Run("CGC increases after fulu", func(t *testing.T) {
service, requirements := minimalTestService(t)
err = requirements.db.SaveBlock(ctx, roBlock)
require.NoError(t, err)
// Before Fulu
// -----------
actualEas, actualCgc, err := service.updateCustodyInfoInDB(15)
require.NoError(t, err)
require.Equal(t, earliestStoredSlot, actualEas)
require.Equal(t, custodyRequirement, actualCgc)
actualEas, actualCgc, err = service.updateCustodyInfoInDB(17)
require.NoError(t, err)
require.Equal(t, earliestStoredSlot, actualEas)
require.Equal(t, custodyRequirement, actualCgc)
// After Fulu
// ----------
resetFlags := flags.Get()
gFlags := new(flags.GlobalFlags)
gFlags.Supernode = true
flags.Init(gFlags)
defer flags.Init(resetFlags)
slot := fuluForkEpoch*primitives.Slot(cfg.SlotsPerEpoch) + 1
actualEas, actualCgc, err = service.updateCustodyInfoInDB(slot)
require.NoError(t, err)
require.Equal(t, slot, actualEas)
require.Equal(t, numberOfCustodyGroups, actualCgc)
actualEas, actualCgc, err = service.updateCustodyInfoInDB(slot + 2)
require.NoError(t, err)
require.Equal(t, slot, actualEas)
require.Equal(t, numberOfCustodyGroups, actualCgc)
})
t.Run("Supernode downgrade prevented", func(t *testing.T) {
service, requirements := minimalTestService(t)
err = requirements.db.SaveBlock(ctx, roBlock)
require.NoError(t, err)
// Enable supernode
resetFlags := flags.Get()
gFlags := new(flags.GlobalFlags)
gFlags.Supernode = true
flags.Init(gFlags)
slot := fuluForkEpoch*primitives.Slot(cfg.SlotsPerEpoch) + 1
actualEas, actualCgc, err := service.updateCustodyInfoInDB(slot)
require.NoError(t, err)
require.Equal(t, slot, actualEas)
require.Equal(t, numberOfCustodyGroups, actualCgc)
// Try to downgrade by removing flag
gFlags.Supernode = false
flags.Init(gFlags)
defer flags.Init(resetFlags)
// Should still be supernode
actualEas, actualCgc, err = service.updateCustodyInfoInDB(slot + 2)
require.NoError(t, err)
require.Equal(t, slot, actualEas)
require.Equal(t, numberOfCustodyGroups, actualCgc) // Still 64, not downgraded
})
t.Run("Semi-supernode downgrade prevented", func(t *testing.T) {
service, requirements := minimalTestService(t)
err = requirements.db.SaveBlock(ctx, roBlock)
require.NoError(t, err)
// Enable semi-supernode
resetFlags := flags.Get()
gFlags := new(flags.GlobalFlags)
gFlags.SemiSupernode = true
flags.Init(gFlags)
slot := fuluForkEpoch*primitives.Slot(cfg.SlotsPerEpoch) + 1
actualEas, actualCgc, err := service.updateCustodyInfoInDB(slot)
require.NoError(t, err)
require.Equal(t, slot, actualEas)
semiSupernodeCustody := numberOfCustodyGroups / 2 // 64
require.Equal(t, semiSupernodeCustody, actualCgc) // Semi-supernode custodies 64 groups
// Try to downgrade by removing flag
gFlags.SemiSupernode = false
flags.Init(gFlags)
defer flags.Init(resetFlags)
// UpdateCustodyInfo should prevent downgrade - custody count should remain at 64
actualEas, actualCgc, err = service.updateCustodyInfoInDB(slot + 2)
require.NoError(t, err)
require.Equal(t, slot, actualEas)
require.Equal(t, semiSupernodeCustody, actualCgc) // Still 64 due to downgrade prevention by UpdateCustodyInfo
})
t.Run("Semi-supernode to supernode upgrade allowed", func(t *testing.T) {
service, requirements := minimalTestService(t)
err = requirements.db.SaveBlock(ctx, roBlock)
require.NoError(t, err)
// Start with semi-supernode
resetFlags := flags.Get()
gFlags := new(flags.GlobalFlags)
gFlags.SemiSupernode = true
flags.Init(gFlags)
slot := fuluForkEpoch*primitives.Slot(cfg.SlotsPerEpoch) + 1
actualEas, actualCgc, err := service.updateCustodyInfoInDB(slot)
require.NoError(t, err)
require.Equal(t, slot, actualEas)
semiSupernodeCustody := numberOfCustodyGroups / 2 // 64
require.Equal(t, semiSupernodeCustody, actualCgc) // Semi-supernode custodies 64 groups
// Upgrade to full supernode
gFlags.SemiSupernode = false
gFlags.Supernode = true
flags.Init(gFlags)
defer flags.Init(resetFlags)
// Should upgrade to full supernode
upgradeSlot := slot + 2
actualEas, actualCgc, err = service.updateCustodyInfoInDB(upgradeSlot)
require.NoError(t, err)
require.Equal(t, upgradeSlot, actualEas) // Earliest slot updates when upgrading
require.Equal(t, numberOfCustodyGroups, actualCgc) // Upgraded to 128
})
t.Run("Semi-supernode with high validator requirements uses higher custody", func(t *testing.T) {
service, requirements := minimalTestService(t)
err = requirements.db.SaveBlock(ctx, roBlock)
require.NoError(t, err)
// Enable semi-supernode
resetFlags := flags.Get()
gFlags := new(flags.GlobalFlags)
gFlags.SemiSupernode = true
flags.Init(gFlags)
defer flags.Init(resetFlags)
// Mock a high custody requirement (simulating many validators)
// We need to override the custody requirement calculation
// For this test, we'll verify the logic by checking if custodyRequirement > 64
// Since custodyRequirement in minimalTestService is 4, we can't test the high case here
// This would require a different test setup with actual validators
slot := fuluForkEpoch*primitives.Slot(cfg.SlotsPerEpoch) + 1
actualEas, actualCgc, err := service.updateCustodyInfoInDB(slot)
require.NoError(t, err)
require.Equal(t, slot, actualEas)
semiSupernodeCustody := numberOfCustodyGroups / 2 // 64
// With low validator requirements (4), should use semi-supernode minimum (64)
require.Equal(t, semiSupernodeCustody, actualCgc)
})
}

View File

@@ -166,11 +166,11 @@ func (s *Service) setupForkchoiceCheckpoints() error {
defer s.cfg.ForkChoiceStore.Unlock()
if err := s.cfg.ForkChoiceStore.UpdateJustifiedCheckpoint(s.ctx, &forkchoicetypes.Checkpoint{Epoch: justified.Epoch,
Root: bytesutil.ToBytes32(justified.Root)}); err != nil {
return errors.Wrap(err, "could not update forkchoice's justified checkpoint")
log.WithError(err).Error("Could not update forkchoice's justified checkpoint, trying to update finalized checkpoint anyway")
}
if err := s.cfg.ForkChoiceStore.UpdateFinalizedCheckpoint(&forkchoicetypes.Checkpoint{Epoch: finalized.Epoch,
Root: fRoot}); err != nil {
return errors.Wrap(err, "could not update forkchoice's finalized checkpoint")
log.WithError(err).Error("Could not update forkchoice's finalized checkpoint")
}
s.cfg.ForkChoiceStore.SetGenesisTime(s.genesisTime)
return nil

View File

@@ -104,7 +104,9 @@ func Test_setupForkchoiceTree_Head(t *testing.T) {
require.NoError(t, err)
root, err := b.Block.HashTreeRoot()
require.NoError(t, err)
preState, err := service.getBlockPreState(ctx, wsb.Block())
roblock, err := consensusblocks.NewROBlockWithRoot(wsb, root)
require.NoError(t, err)
preState, err := service.GetBlockPreState(ctx, roblock)
require.NoError(t, err)
postState, err := service.validateStateTransition(ctx, preState, wsb)
require.NoError(t, err)

View File

@@ -77,6 +77,12 @@ type ChainService struct {
DataColumns []blocks.VerifiedRODataColumn
TargetRoot [32]byte
MockHeadSlot *primitives.Slot
MockCanonicalRoots map[primitives.Slot][32]byte
MockCanonicalFull map[primitives.Slot]bool
MockPayloadContentLookup map[[32]byte][32]byte
MockPayloadContentIsFull map[[32]byte]bool
ParentPayloadReadyVal *bool
ForkchoiceRoots map[[32]byte]bool
}
func (s *ChainService) Ancestor(ctx context.Context, root []byte, slot primitives.Slot) ([]byte, error) {
@@ -334,6 +340,16 @@ func (s *ChainService) ReceiveBlock(ctx context.Context, block interfaces.ReadOn
return nil
}
// GetBlockPreState mocks the same method in the chain service.
func (s *ChainService) GetBlockPreState(_ context.Context, _ blocks.ROBlock) (state.BeaconState, error) {
return s.State, nil
}
// GetPrestateToPropose mocks the same method in the chain service.
func (s *ChainService) GetPrestateToPropose(_ context.Context, _ blocks.ROBlock) (state.BeaconState, error) {
return s.State.Copy(), nil
}
// HeadSlot mocks HeadSlot method in chain service.
func (s *ChainService) HeadSlot() primitives.Slot {
if s.MockHeadSlot != nil {
@@ -569,7 +585,10 @@ func (s *ChainService) IsOptimistic(_ context.Context) (bool, error) {
}
// InForkchoice mocks the same method in the chain service
func (s *ChainService) InForkchoice(_ [32]byte) bool {
func (s *ChainService) InForkchoice(root [32]byte) bool {
if s.ForkchoiceRoots != nil {
return s.ForkchoiceRoots[root]
}
return !s.NotFinalized
}
@@ -630,7 +649,7 @@ func prepareForkchoiceState(
}
base.BlockRoots[0] = append(base.BlockRoots[0], blockRoot[:]...)
st, err := state_native.InitializeFromProtoBellatrix(base)
st, err := state_native.InitializeFromProtoUnsafeBellatrix(base)
if err != nil {
return nil, blocks.ROBlock{}, err
}
@@ -689,7 +708,55 @@ func (s *ChainService) HighestReceivedBlockSlot() primitives.Slot {
if s.ForkChoiceStore != nil {
return s.ForkChoiceStore.HighestReceivedBlockSlot()
}
return 0
if s.Slot != nil {
return *s.Slot
}
return s.BlockSlot
}
// HighestReceivedBlockRoot mocks the same method in the chain service
func (s *ChainService) HighestReceivedBlockRoot() [32]byte {
if s.ForkChoiceStore != nil {
return s.ForkChoiceStore.HighestReceivedBlockRoot()
}
if s.Slot != nil && s.MockCanonicalRoots != nil {
if root, ok := s.MockCanonicalRoots[*s.Slot]; ok {
return root
}
}
if len(s.Root) == 32 {
return bytesutil.ToBytes32(s.Root)
}
return [32]byte{}
}
// HasFullNode mocks the same method in the chain service
func (s *ChainService) HasFullNode(root [32]byte) bool {
if s.ForkChoiceStore != nil {
return s.ForkChoiceStore.HasFullNode(root)
}
if s.Slot != nil && s.MockCanonicalRoots != nil && s.MockCanonicalFull != nil {
if r, ok := s.MockCanonicalRoots[*s.Slot]; ok && r == root {
return s.MockCanonicalFull[*s.Slot]
}
}
if s.ForkchoiceRoots != nil {
return s.ForkchoiceRoots[root]
}
return false
}
// PayloadContentLookup mocks the same method in the chain service.
func (s *ChainService) PayloadContentLookup(root [32]byte) ([32]byte, bool) {
if s.ForkChoiceStore != nil {
return s.ForkChoiceStore.PayloadContentLookup(root)
}
if s.MockPayloadContentLookup != nil {
if value, ok := s.MockPayloadContentLookup[root]; ok {
return value, s.MockPayloadContentIsFull[root]
}
}
return root, false
}
// InsertNode mocks the same method in the chain service
@@ -700,6 +767,14 @@ func (s *ChainService) InsertNode(ctx context.Context, st state.BeaconState, blo
return nil
}
// InsertPayload mocks the same method in the chain service
func (s *ChainService) InsertPayload(pe interfaces.ROExecutionPayloadEnvelope) error {
if s.ForkChoiceStore != nil {
return s.ForkChoiceStore.InsertPayload(pe)
}
return nil
}
// ForkChoiceDump mocks the same method in the chain service
func (s *ChainService) ForkChoiceDump(ctx context.Context) (*forkchoice2.Dump, error) {
if s.ForkChoiceStore != nil {
@@ -757,6 +832,24 @@ func (c *ChainService) ReceiveDataColumns(dcs []blocks.VerifiedRODataColumn) err
return nil
}
// ReceivePayloadAttestationMessage implements the same method in the chain service.
func (c *ChainService) ReceivePayloadAttestationMessage(_ context.Context, _ *ethpb.PayloadAttestationMessage) error {
return nil
}
// ReceiveExecutionPayloadEnvelope implements the same method in the chain service.
func (c *ChainService) ReceiveExecutionPayloadEnvelope(_ context.Context, _ interfaces.ROSignedExecutionPayloadEnvelope) error {
return nil
}
// ParentPayloadReady mocks the same method in the chain service.
func (s *ChainService) ParentPayloadReady(_ interfaces.ReadOnlyBeaconBlock) bool {
if s.ParentPayloadReadyVal != nil {
return *s.ParentPayloadReadyVal
}
return true
}
// DependentRootForEpoch mocks the same method in the chain service
func (c *ChainService) DependentRootForEpoch(_ [32]byte, _ primitives.Epoch) ([32]byte, error) {
return c.TargetRoot, nil
@@ -767,6 +860,17 @@ func (c *ChainService) TargetRootForEpoch(_ [32]byte, _ primitives.Epoch) ([32]b
return c.TargetRoot, nil
}
func (c *ChainService) CanonicalNodeAtSlot(slot primitives.Slot) ([32]byte, bool) {
var root [32]byte
if c.MockCanonicalRoots != nil {
root = c.MockCanonicalRoots[slot]
}
if c.MockCanonicalFull != nil {
return root, c.MockCanonicalFull[slot]
}
return root, false
}
// MockSyncChecker is a mock implementation of blockchain.Checker.
// We can't make an assertion here that this is true because that would create a circular dependency.
type MockSyncChecker struct {

View File

@@ -17,10 +17,12 @@ go_library(
"error.go",
"interfaces.go",
"log.go",
"payload_attestation.go",
"payload_id.go",
"proposer_indices.go",
"proposer_indices_disabled.go", # keep
"proposer_indices_type.go",
"proposer_preferences.go",
"registration.go",
"skip_slot_cache.go",
"subnet_ids.go",
@@ -54,6 +56,7 @@ go_library(
"//proto/prysm/v1alpha1:go_default_library",
"//proto/prysm/v1alpha1/attestation:go_default_library",
"//runtime/version:go_default_library",
"//time/slots:go_default_library",
"@com_github_ethereum_go_ethereum//common:go_default_library",
"@com_github_hashicorp_golang_lru//:go_default_library",
"@com_github_patrickmn_go_cache//:go_default_library",
@@ -76,9 +79,11 @@ go_test(
"checkpoint_state_test.go",
"committee_fuzz_test.go",
"committee_test.go",
"payload_attestation_test.go",
"payload_id_test.go",
"private_access_test.go",
"proposer_indices_test.go",
"proposer_preferences_test.go",
"registration_test.go",
"skip_slot_cache_test.go",
"subnet_ids_test.go",

View File

@@ -9,10 +9,11 @@ import (
)
type AttestationConsensusData struct {
Slot primitives.Slot
HeadRoot []byte
Target forkchoicetypes.Checkpoint
Source forkchoicetypes.Checkpoint
Slot primitives.Slot
HeadRoot []byte
Target forkchoicetypes.Checkpoint
Source forkchoicetypes.Checkpoint
IsPayloadFull bool
}
// AttestationDataCache stores cached results of AttestationData requests.

View File

@@ -3,8 +3,10 @@ package cache
import (
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
lruwrpr "github.com/OffchainLabs/prysm/v7/cache/lru"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/crypto/hash"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/time/slots"
lru "github.com/hashicorp/golang-lru"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
@@ -25,6 +27,14 @@ var (
Name: "check_point_state_cache_hit",
Help: "The number of check point state requests that are present in the cache.",
})
checkpointStateSize = promauto.NewGauge(prometheus.GaugeOpts{
Name: "check_point_state_cache_size",
Help: "The number of entries in the check point state cache.",
})
checkpointStateEvicted = promauto.NewCounter(prometheus.CounterOpts{
Name: "check_point_state_cache_evicted_total",
Help: "The number of entries evicted from the check point state cache.",
})
)
// CheckpointStateCache is a struct with 1 queue for looking up state by checkpoint.
@@ -49,14 +59,14 @@ func (c *CheckpointStateCache) StateByCheckpoint(cp *ethpb.Checkpoint) (state.Be
item, exists := c.cache.Get(h)
if exists && item != nil {
checkpointStateHit.Inc()
// Copy here is unnecessary since the return will only be used to verify attestation signature.
return item.(state.BeaconState), nil
if !exists || item == nil {
checkpointStateMiss.Inc()
return nil, nil
}
checkpointStateMiss.Inc()
return nil, nil
checkpointStateHit.Inc()
// Copy here is unnecessary since the return will only be used to verify attestation signature.
return item.(state.BeaconState), nil
}
// AddCheckpointState adds CheckpointState object to the cache. This method also trims the least
@@ -66,6 +76,35 @@ func (c *CheckpointStateCache) AddCheckpointState(cp *ethpb.Checkpoint, s state.
if err != nil {
return err
}
c.cache.Add(h, s)
checkpointStateSize.Set(float64(c.cache.Len()))
return nil
}
// EvictUpTo removes all entries from the cache whose state epoch is at
// or before the given epoch. Returns the number of evicted entries.
func (c *CheckpointStateCache) EvictUpTo(epoch primitives.Epoch) int {
evicted := 0
for _, key := range c.cache.Keys() {
// Peek is used here to avoid updating the recency of the entry,
// as we are only checking for eviction.
v, ok := c.cache.Peek(key)
if !ok {
continue
}
st := v.(state.ReadOnlyBeaconState)
if slots.ToEpoch(st.Slot()) <= epoch {
c.cache.Remove(key)
evicted++
}
}
if evicted > 0 {
checkpointStateSize.Set(float64(c.cache.Len()))
checkpointStateEvicted.Add(float64(evicted))
}
return evicted
}

View File

@@ -72,3 +72,76 @@ func TestCheckpointStateCache_MaxSize(t *testing.T) {
assert.Equal(t, cache.MaxCheckpointStateSize(), len(c.Cache().Keys()))
}
func TestCheckpointStateCache_EvictFinalized_FinalizedEntry(t *testing.T) {
c := cache.NewCheckpointStateCache()
cp := &ethpb.Checkpoint{Epoch: 1, Root: bytesutil.PadTo([]byte{'A'}, 32)}
st, err := state_native.InitializeFromProtoPhase0(&ethpb.BeaconState{Slot: 32})
require.NoError(t, err)
require.NoError(t, c.AddCheckpointState(cp, st))
evicted := c.EvictUpTo(1)
assert.Equal(t, 1, evicted, "expected finalized entry to be evicted")
s, err := c.StateByCheckpoint(cp)
require.NoError(t, err)
assert.Equal(t, state.BeaconState(nil), s, "expected cache to be empty after eviction")
}
func TestCheckpointStateCache_EvictFinalized_NotFinalizedEntry(t *testing.T) {
c := cache.NewCheckpointStateCache()
cp := &ethpb.Checkpoint{Epoch: 5, Root: bytesutil.PadTo([]byte{'A'}, 32)}
st, err := state_native.InitializeFromProtoPhase0(&ethpb.BeaconState{Slot: 160})
require.NoError(t, err)
require.NoError(t, c.AddCheckpointState(cp, st))
evicted := c.EvictUpTo(3)
assert.Equal(t, 0, evicted, "expected non-finalized entry NOT to be evicted")
s, err := c.StateByCheckpoint(cp)
require.NoError(t, err)
assert.NotNil(t, s, "expected entry to still be in cache")
}
func TestCheckpointStateCache_EvictFinalized_Mixed(t *testing.T) {
c := cache.NewCheckpointStateCache()
cp1 := &ethpb.Checkpoint{Epoch: 1, Root: bytesutil.PadTo([]byte{'A'}, 32)}
st1, err := state_native.InitializeFromProtoPhase0(&ethpb.BeaconState{Slot: 32})
require.NoError(t, err)
cp2 := &ethpb.Checkpoint{Epoch: 2, Root: bytesutil.PadTo([]byte{'B'}, 32)}
st2, err := state_native.InitializeFromProtoPhase0(&ethpb.BeaconState{Slot: 64})
require.NoError(t, err)
cp5 := &ethpb.Checkpoint{Epoch: 5, Root: bytesutil.PadTo([]byte{'C'}, 32)}
st5, err := state_native.InitializeFromProtoPhase0(&ethpb.BeaconState{Slot: 160})
require.NoError(t, err)
require.NoError(t, c.AddCheckpointState(cp1, st1))
require.NoError(t, c.AddCheckpointState(cp2, st2))
require.NoError(t, c.AddCheckpointState(cp5, st5))
evicted := c.EvictUpTo(3)
assert.Equal(t, 2, evicted, "expected epochs 1 and 2 to be evicted")
s, err := c.StateByCheckpoint(cp1)
require.NoError(t, err)
assert.Equal(t, state.BeaconState(nil), s, "expected cp1 to be evicted")
s, err = c.StateByCheckpoint(cp2)
require.NoError(t, err)
assert.Equal(t, state.BeaconState(nil), s, "expected cp2 to be evicted")
s, err = c.StateByCheckpoint(cp5)
require.NoError(t, err)
assert.NotNil(t, s, "expected cp5 to still be in cache")
}
func TestCheckpointStateCache_EvictFinalized_EmptyCache(t *testing.T) {
c := cache.NewCheckpointStateCache()
evicted := c.EvictUpTo(0)
assert.Equal(t, 0, evicted, "expected no eviction from empty cache")
}

View File

@@ -0,0 +1,53 @@
package cache
import (
"sync"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
)
// PayloadAttestationCache tracks seen payload attestation messages for a single slot.
type PayloadAttestationCache struct {
slot primitives.Slot
seen map[primitives.ValidatorIndex]struct{}
mu sync.RWMutex
}
// Seen returns true if a vote for the given slot has already been
// processed for this validator index.
func (p *PayloadAttestationCache) Seen(slot primitives.Slot, idx primitives.ValidatorIndex) bool {
p.mu.RLock()
defer p.mu.RUnlock()
if p.slot != slot {
return false
}
if p.seen == nil {
return false
}
_, ok := p.seen[idx]
return ok
}
// Add marks the given slot and validator index as seen.
// This function assumes that the message has already been validated.
func (p *PayloadAttestationCache) Add(slot primitives.Slot, idx primitives.ValidatorIndex) error {
p.mu.Lock()
defer p.mu.Unlock()
if p.slot != slot {
p.slot = slot
p.seen = make(map[primitives.ValidatorIndex]struct{})
}
if p.seen == nil {
p.seen = make(map[primitives.ValidatorIndex]struct{})
}
p.seen[idx] = struct{}{}
return nil
}
// Clear clears the internal cache.
func (p *PayloadAttestationCache) Clear() {
p.mu.Lock()
defer p.mu.Unlock()
p.slot = 0
p.seen = nil
}

View File

@@ -0,0 +1,48 @@
package cache_test
import (
"testing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/cache"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/stretchr/testify/require"
)
func TestPayloadAttestationCache_SeenAndAdd(t *testing.T) {
var c cache.PayloadAttestationCache
slot1 := primitives.Slot(1)
slot2 := primitives.Slot(2)
idx1 := primitives.ValidatorIndex(3)
idx2 := primitives.ValidatorIndex(4)
require.False(t, c.Seen(slot1, idx1))
require.NoError(t, c.Add(slot1, idx1))
require.True(t, c.Seen(slot1, idx1))
require.False(t, c.Seen(slot1, idx2))
require.False(t, c.Seen(slot2, idx1))
require.NoError(t, c.Add(slot1, idx2))
require.True(t, c.Seen(slot1, idx1))
require.True(t, c.Seen(slot1, idx2))
require.NoError(t, c.Add(slot2, idx1))
require.True(t, c.Seen(slot2, idx1))
require.False(t, c.Seen(slot1, idx1))
require.False(t, c.Seen(slot1, idx2))
}
func TestPayloadAttestationCache_Clear(t *testing.T) {
var c cache.PayloadAttestationCache
slot := primitives.Slot(10)
idx := primitives.ValidatorIndex(42)
require.NoError(t, c.Add(slot, idx))
require.True(t, c.Seen(slot, idx))
c.Clear()
require.False(t, c.Seen(slot, idx))
require.NoError(t, c.Add(slot, idx))
require.True(t, c.Seen(slot, idx))
}

View File

@@ -0,0 +1,87 @@
package cache
import (
"sync"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
)
// ProposerPreference stores the proposer fee recipient and gas limit for a slot.
type ProposerPreference struct {
FeeRecipient []byte
GasLimit uint64
}
// ProposerPreferencesCache stores proposer preferences by slot.
type ProposerPreferencesCache struct {
slotToPreferences map[primitives.Slot]ProposerPreference
lock sync.RWMutex
}
// NewProposerPreferencesCache initializes a proposer preferences cache.
func NewProposerPreferencesCache() *ProposerPreferencesCache {
return &ProposerPreferencesCache{
slotToPreferences: make(map[primitives.Slot]ProposerPreference),
}
}
// Add stores proposer preferences for a slot. If the slot already exists, the
// existing value is kept and false is returned.
func (c *ProposerPreferencesCache) Add(slot primitives.Slot, feeRecipient []byte, gasLimit uint64) bool {
c.lock.Lock()
defer c.lock.Unlock()
if _, ok := c.slotToPreferences[slot]; ok {
return false
}
// FeeRecipient comes from validated SSZ-decoded proposer preferences, so
// retaining the slice reference here is intentional.
c.slotToPreferences[slot] = ProposerPreference{
FeeRecipient: feeRecipient,
GasLimit: gasLimit,
}
return true
}
// Get returns proposer preferences for a slot.
func (c *ProposerPreferencesCache) Get(slot primitives.Slot) (ProposerPreference, bool) {
c.lock.RLock()
defer c.lock.RUnlock()
pref, ok := c.slotToPreferences[slot]
if !ok {
return ProposerPreference{}, false
}
return pref, true
}
// Has returns true if proposer preferences for the slot already exist.
func (c *ProposerPreferencesCache) Has(slot primitives.Slot) bool {
c.lock.RLock()
defer c.lock.RUnlock()
_, ok := c.slotToPreferences[slot]
return ok
}
// PruneBefore removes all proposer preferences for slots before the provided slot.
func (c *ProposerPreferencesCache) PruneBefore(slot primitives.Slot) {
c.lock.Lock()
defer c.lock.Unlock()
for cachedSlot := range c.slotToPreferences {
if cachedSlot < slot {
delete(c.slotToPreferences, cachedSlot)
}
}
}
// Clear removes all cached proposer preferences.
func (c *ProposerPreferencesCache) Clear() {
c.lock.Lock()
defer c.lock.Unlock()
c.slotToPreferences = make(map[primitives.Slot]ProposerPreference)
}

View File

@@ -0,0 +1,63 @@
package cache
import (
"testing"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/testing/require"
)
func TestProposerPreferencesCache_AddGetHas(t *testing.T) {
c := NewProposerPreferencesCache()
slot := primitives.Slot(123)
feeRecipient := []byte{1, 2, 3, 4}
require.Equal(t, false, c.Has(slot))
added := c.Add(slot, feeRecipient, 42)
require.Equal(t, true, added)
require.Equal(t, true, c.Has(slot))
pref, ok := c.Get(slot)
require.Equal(t, true, ok)
require.DeepEqual(t, feeRecipient, pref.FeeRecipient)
require.Equal(t, uint64(42), pref.GasLimit)
}
func TestProposerPreferencesCache_AddDuplicateSlot(t *testing.T) {
c := NewProposerPreferencesCache()
slot := primitives.Slot(456)
require.Equal(t, true, c.Add(slot, []byte{1}, 10))
require.Equal(t, false, c.Add(slot, []byte{2}, 20))
pref, ok := c.Get(slot)
require.Equal(t, true, ok)
require.DeepEqual(t, []byte{1}, pref.FeeRecipient)
require.Equal(t, uint64(10), pref.GasLimit)
}
func TestProposerPreferencesCache_Clear(t *testing.T) {
c := NewProposerPreferencesCache()
slot := primitives.Slot(789)
require.Equal(t, true, c.Add(slot, []byte{1}, 10))
c.Clear()
require.Equal(t, false, c.Has(slot))
_, ok := c.Get(slot)
require.Equal(t, false, ok)
}
func TestProposerPreferencesCache_PruneBefore(t *testing.T) {
c := NewProposerPreferencesCache()
require.Equal(t, true, c.Add(10, []byte{1}, 10))
require.Equal(t, true, c.Add(11, []byte{2}, 11))
require.Equal(t, true, c.Add(12, []byte{3}, 12))
c.PruneBefore(11)
require.Equal(t, false, c.Has(10))
require.Equal(t, true, c.Has(11))
require.Equal(t, true, c.Has(12))
}

View File

@@ -172,7 +172,7 @@ func (s *SyncCommitteeCache) idxPositionInCommittee(
// UpdatePositionsInCommittee updates caching of validators position in sync committee in respect to
// current epoch and next epoch. This should be called when `current_sync_committee` and `next_sync_committee`
// change and that happens every `EPOCHS_PER_SYNC_COMMITTEE_PERIOD`.
func (s *SyncCommitteeCache) UpdatePositionsInCommittee(syncCommitteeBoundaryRoot [32]byte, st state.BeaconState) error {
func (s *SyncCommitteeCache) UpdatePositionsInCommittee(syncCommitteeBoundaryRoot [32]byte, st state.ReadOnlyBeaconState) error {
// since we call UpdatePositionsInCommittee asynchronously, keep track of the cache value
// seen at the beginning of the routine and compare at the end before updating. If the underlying value has been
// cycled (new address), don't update it.

View File

@@ -32,7 +32,7 @@ func (s *FakeSyncCommitteeCache) NextPeriodIndexPosition(root [32]byte, valIdx p
}
// UpdatePositionsInCommittee -- fake.
func (s *FakeSyncCommitteeCache) UpdatePositionsInCommittee(syncCommitteeBoundaryRoot [32]byte, state state.BeaconState) error {
func (s *FakeSyncCommitteeCache) UpdatePositionsInCommittee(syncCommitteeBoundaryRoot [32]byte, state state.ReadOnlyBeaconState) error {
return nil
}

View File

@@ -20,12 +20,14 @@ go_library(
"//beacon-chain/core/blocks:go_default_library",
"//beacon-chain/core/epoch:go_default_library",
"//beacon-chain/core/epoch/precompute:go_default_library",
"//beacon-chain/core/gloas:go_default_library",
"//beacon-chain/core/helpers:go_default_library",
"//beacon-chain/core/signing:go_default_library",
"//beacon-chain/core/time:go_default_library",
"//beacon-chain/p2p/types:go_default_library",
"//beacon-chain/state:go_default_library",
"//beacon-chain/state/state-native:go_default_library",
"//beacon-chain/state/stateutil:go_default_library",
"//config/fieldparams:go_default_library",
"//config/params:go_default_library",
"//consensus-types/blocks:go_default_library",

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/blocks"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/gloas"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/time"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
@@ -75,7 +76,11 @@ func ProcessAttestationNoVerifySignature(
return nil, err
}
return SetParticipationAndRewardProposer(ctx, beaconState, att.GetData().Target.Epoch, indices, participatedFlags, totalBalance)
if err := beaconState.UpdatePendingPaymentWeight(att, indices, participatedFlags); err != nil {
return nil, errors.Wrap(err, "failed to update pending payment weight")
}
return SetParticipationAndRewardProposer(ctx, beaconState, att.GetData().Target.Epoch, indices, participatedFlags, totalBalance, att)
}
// SetParticipationAndRewardProposer retrieves and sets the epoch participation bits in state. Based on the epoch participation, it rewards
@@ -105,7 +110,9 @@ func SetParticipationAndRewardProposer(
beaconState state.BeaconState,
targetEpoch primitives.Epoch,
indices []uint64,
participatedFlags map[uint8]bool, totalBalance uint64) (state.BeaconState, error) {
participatedFlags map[uint8]bool,
totalBalance uint64,
att ethpb.Att) (state.BeaconState, error) {
var proposerRewardNumerator uint64
currentEpoch := time.CurrentEpoch(beaconState)
var stateErr error
@@ -299,6 +306,19 @@ func AttestationParticipationFlagIndices(beaconState state.ReadOnlyBeaconState,
participatedFlags[targetFlagIndex] = true
}
matchedSrcTgtHead := matchedHead && matchedSrcTgt
var beaconBlockRoot [32]byte
copy(beaconBlockRoot[:], data.BeaconBlockRoot)
matchingPayload, err := gloas.MatchingPayload(
beaconState,
beaconBlockRoot,
data.Slot,
uint64(data.CommitteeIndex),
)
if err != nil {
return nil, err
}
matchedSrcTgtHead = matchedSrcTgtHead && matchingPayload
if matchedSrcTgtHead && delay == cfg.MinAttestationInclusionDelay {
participatedFlags[headFlagIndex] = true
}

View File

@@ -1,7 +1,9 @@
package altair_test
import (
"bytes"
"fmt"
"reflect"
"testing"
"github.com/OffchainLabs/go-bitfield"
@@ -556,7 +558,7 @@ func TestSetParticipationAndRewardProposer(t *testing.T) {
b, err := helpers.TotalActiveBalance(beaconState)
require.NoError(t, err)
st, err := altair.SetParticipationAndRewardProposer(t.Context(), beaconState, test.epoch, test.indices, test.participatedFlags, b)
st, err := altair.SetParticipationAndRewardProposer(t.Context(), beaconState, test.epoch, test.indices, test.participatedFlags, b, &ethpb.Attestation{})
require.NoError(t, err)
i, err := helpers.BeaconProposerIndex(t.Context(), st)
@@ -775,11 +777,67 @@ func TestAttestationParticipationFlagIndices(t *testing.T) {
headFlagIndex: true,
},
},
{
name: "gloas same-slot committee index non-zero errors",
inputState: func() state.BeaconState {
stateSlot := primitives.Slot(5)
slot := primitives.Slot(3)
targetRoot := bytes.Repeat([]byte{0xAA}, 32)
headRoot := bytes.Repeat([]byte{0xBB}, 32)
prevRoot := bytes.Repeat([]byte{0xCC}, 32)
return buildGloasStateForFlags(t, stateSlot, slot, targetRoot, headRoot, prevRoot, 0, 0)
}(),
inputData: &ethpb.AttestationData{
Slot: 3,
CommitteeIndex: 1, // invalid for same-slot
BeaconBlockRoot: bytes.Repeat([]byte{0xBB}, 32),
Source: &ethpb.Checkpoint{Root: bytes.Repeat([]byte{0xDD}, 32)},
Target: &ethpb.Checkpoint{
Epoch: 0,
Root: bytes.Repeat([]byte{0xAA}, 32),
},
},
inputDelay: 1,
participationIndices: nil,
},
{
name: "gloas payload availability matches committee index",
inputState: func() state.BeaconState {
stateSlot := primitives.Slot(5)
slot := primitives.Slot(3)
targetRoot := bytes.Repeat([]byte{0xAA}, 32)
headRoot := bytes.Repeat([]byte{0xBB}, 32)
// Same prev root to make SameSlotAttestation false and use payload availability.
return buildGloasStateForFlags(t, stateSlot, slot, targetRoot, headRoot, headRoot, 1, slot)
}(),
inputData: &ethpb.AttestationData{
Slot: 3,
CommitteeIndex: 1,
BeaconBlockRoot: bytes.Repeat([]byte{0xBB}, 32),
Source: &ethpb.Checkpoint{Root: bytes.Repeat([]byte{0xDD}, 32)},
Target: &ethpb.Checkpoint{
Epoch: 0,
Root: bytes.Repeat([]byte{0xAA}, 32),
},
},
inputDelay: 1,
participationIndices: map[uint8]bool{
sourceFlagIndex: true,
targetFlagIndex: true,
headFlagIndex: true,
},
},
}
for _, test := range tests {
flagIndices, err := altair.AttestationParticipationFlagIndices(test.inputState, test.inputData, test.inputDelay)
if test.participationIndices == nil {
require.ErrorContains(t, "committee index", err)
continue
}
require.NoError(t, err)
require.DeepEqual(t, test.participationIndices, flagIndices)
if !reflect.DeepEqual(test.participationIndices, flagIndices) {
t.Fatalf("unexpected participation indices: got %v want %v", flagIndices, test.participationIndices)
}
}
}
@@ -858,3 +916,61 @@ func TestMatchingStatus(t *testing.T) {
require.Equal(t, test.matchedHead, head)
}
}
func buildGloasStateForFlags(t *testing.T, stateSlot, slot primitives.Slot, targetRoot, headRoot, prevRoot []byte, availabilityBit uint8, availabilitySlot primitives.Slot) state.BeaconState {
t.Helper()
cfg := params.BeaconConfig()
blockRoots := make([][]byte, cfg.SlotsPerHistoricalRoot)
blockRoots[0] = targetRoot
blockRoots[slot%cfg.SlotsPerHistoricalRoot] = headRoot
blockRoots[(slot-1)%cfg.SlotsPerHistoricalRoot] = prevRoot
stateRoots := make([][]byte, cfg.SlotsPerHistoricalRoot)
for i := range stateRoots {
stateRoots[i] = make([]byte, fieldparams.RootLength)
}
randaoMixes := make([][]byte, cfg.EpochsPerHistoricalVector)
for i := range randaoMixes {
randaoMixes[i] = make([]byte, fieldparams.RootLength)
}
execPayloadAvailability := make([]byte, cfg.SlotsPerHistoricalRoot/8)
idx := availabilitySlot % cfg.SlotsPerHistoricalRoot
byteIndex := idx / 8
bitIndex := idx % 8
if availabilityBit == 1 {
execPayloadAvailability[byteIndex] |= 1 << bitIndex
}
checkpointRoot := bytes.Repeat([]byte{0xDD}, fieldparams.RootLength)
justified := &ethpb.Checkpoint{Root: checkpointRoot}
stProto := &ethpb.BeaconStateGloas{
Slot: stateSlot,
GenesisValidatorsRoot: bytes.Repeat([]byte{0x11}, fieldparams.RootLength),
BlockRoots: blockRoots,
StateRoots: stateRoots,
RandaoMixes: randaoMixes,
ExecutionPayloadAvailability: execPayloadAvailability,
CurrentJustifiedCheckpoint: justified,
PreviousJustifiedCheckpoint: justified,
Validators: []*ethpb.Validator{
{
EffectiveBalance: cfg.MinActivationBalance,
WithdrawalCredentials: append([]byte{cfg.ETH1AddressWithdrawalPrefixByte}, bytes.Repeat([]byte{0x01}, 31)...),
},
},
Balances: []uint64{cfg.MinActivationBalance},
BuilderPendingPayments: make([]*ethpb.BuilderPendingPayment, cfg.SlotsPerEpoch*2),
Fork: &ethpb.Fork{
CurrentVersion: bytes.Repeat([]byte{0x01}, 4),
PreviousVersion: bytes.Repeat([]byte{0x01}, 4),
Epoch: 0,
},
}
beaconState, err := state_native.InitializeFromProtoGloas(stProto)
require.NoError(t, err)
return beaconState
}

View File

@@ -3,7 +3,6 @@ package altair
import (
"context"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/blocks"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"github.com/OffchainLabs/prysm/v7/config/params"
@@ -24,7 +23,7 @@ func ProcessPreGenesisDeposits(
if err != nil {
return nil, errors.Wrap(err, "could not process deposit")
}
beaconState, err = blocks.ActivateValidatorWithEffectiveBalance(beaconState, deposits)
beaconState, err = helpers.ActivateValidatorWithEffectiveBalance(beaconState, deposits)
if err != nil {
return nil, err
}
@@ -37,7 +36,7 @@ func ProcessDeposits(
beaconState state.BeaconState,
deposits []*ethpb.Deposit,
) (state.BeaconState, error) {
allSignaturesVerified, err := blocks.BatchVerifyDepositsSignatures(ctx, deposits)
allSignaturesVerified, err := helpers.BatchVerifyDepositsSignatures(ctx, deposits)
if err != nil {
return nil, err
}
@@ -82,7 +81,7 @@ func ProcessDeposits(
// signature=deposit.data.signature,
// )
func ProcessDeposit(beaconState state.BeaconState, deposit *ethpb.Deposit, allSignaturesVerified bool) (state.BeaconState, error) {
if err := blocks.VerifyDeposit(beaconState, deposit); err != nil {
if err := helpers.VerifyDeposit(beaconState, deposit); err != nil {
if deposit == nil || deposit.Data == nil {
return nil, err
}
@@ -122,7 +121,7 @@ func ApplyDeposit(beaconState state.BeaconState, data *ethpb.Deposit_Data, allSi
index, ok := beaconState.ValidatorIndexByPubkey(bytesutil.ToBytes48(pubKey))
if !ok {
if !allSignaturesVerified {
valid, err := blocks.IsValidDepositSignature(data)
valid, err := helpers.IsValidDepositSignature(data)
if err != nil {
return nil, err
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/time"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state/stateutil"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/math"
"github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace"
@@ -41,26 +42,26 @@ func InitializePrecomputeValidators(ctx context.Context, beaconState state.Beaco
if beaconState.NumValidators() != len(inactivityScores) {
return nil, nil, errors.New("num of validators is different than num of inactivity scores")
}
if err := beaconState.ReadFromEveryValidator(func(idx int, val state.ReadOnlyValidator) error {
if err := beaconState.ForEachValidator(func(idx int, val *stateutil.CompactValidator) error {
// Set validator's balance, inactivity score and slashed/withdrawable status.
v := &precompute.Validator{
CurrentEpochEffectiveBalance: val.EffectiveBalance(),
CurrentEpochEffectiveBalance: val.EffectiveBalance,
InactivityScore: inactivityScores[idx],
IsSlashed: val.Slashed(),
IsWithdrawableCurrentEpoch: currentEpoch >= val.WithdrawableEpoch(),
IsSlashed: val.Slashed,
IsWithdrawableCurrentEpoch: currentEpoch >= val.WithdrawableEpoch,
}
// Set validator's active status for current epoch.
if helpers.IsActiveValidatorUsingTrie(val, currentEpoch) {
if helpers.IsActiveCompactValidator(val, currentEpoch) {
v.IsActiveCurrentEpoch = true
bal.ActiveCurrentEpoch, err = math.Add64(bal.ActiveCurrentEpoch, val.EffectiveBalance())
bal.ActiveCurrentEpoch, err = math.Add64(bal.ActiveCurrentEpoch, val.EffectiveBalance)
if err != nil {
return err
}
}
// Set validator's active status for previous epoch.
if helpers.IsActiveValidatorUsingTrie(val, prevEpoch) {
if helpers.IsActiveCompactValidator(val, prevEpoch) {
v.IsActivePrevEpoch = true
bal.ActivePrevEpoch, err = math.Add64(bal.ActivePrevEpoch, val.EffectiveBalance())
bal.ActivePrevEpoch, err = math.Add64(bal.ActivePrevEpoch, val.EffectiveBalance)
if err != nil {
return err
}

View File

@@ -34,12 +34,12 @@ func BaseReward(s state.ReadOnlyBeaconState, index primitives.ValidatorIndex) (u
// BaseRewardWithTotalBalance calculates the base reward with the provided total balance.
func BaseRewardWithTotalBalance(s state.ReadOnlyBeaconState, index primitives.ValidatorIndex, totalBalance uint64) (uint64, error) {
val, err := s.ValidatorAtIndexReadOnly(index)
effBal, err := s.EffectiveBalanceAtIndex(index)
if err != nil {
return 0, err
}
cfg := params.BeaconConfig()
increments := val.EffectiveBalance() / cfg.EffectiveBalanceIncrement
increments := effBal / cfg.EffectiveBalanceIncrement
baseRewardPerInc, err := BaseRewardPerIncrement(totalBalance)
if err != nil {
return 0, err

View File

@@ -5,7 +5,6 @@ go_library(
srcs = [
"attestation.go",
"attester_slashing.go",
"deposit.go",
"error.go",
"eth1_data.go",
"exit.go",
@@ -21,6 +20,7 @@ go_library(
importpath = "github.com/OffchainLabs/prysm/v7/beacon-chain/core/blocks",
visibility = ["//visibility:public"],
deps = [
"//beacon-chain/core/gloas:go_default_library",
"//beacon-chain/core/helpers:go_default_library",
"//beacon-chain/core/signing:go_default_library",
"//beacon-chain/core/time:go_default_library",
@@ -33,8 +33,6 @@ go_library(
"//consensus-types/interfaces:go_default_library",
"//consensus-types/primitives:go_default_library",
"//container/slice:go_default_library",
"//container/trie:go_default_library",
"//contracts/deposit:go_default_library",
"//crypto/bls:go_default_library",
"//crypto/hash:go_default_library",
"//encoding/bytesutil:go_default_library",
@@ -61,8 +59,8 @@ go_test(
"attester_slashing_test.go",
"block_operations_fuzz_test.go",
"block_regression_test.go",
"deposit_test.go",
"eth1_data_test.go",
"exit_builder_test.go",
"exit_test.go",
"exports_test.go",
"genesis_test.go",
@@ -90,7 +88,6 @@ go_test(
"//consensus-types/blocks:go_default_library",
"//consensus-types/interfaces:go_default_library",
"//consensus-types/primitives:go_default_library",
"//container/trie:go_default_library",
"//crypto/bls:go_default_library",
"//crypto/bls/common:go_default_library",
"//crypto/hash:go_default_library",

View File

@@ -111,10 +111,21 @@ func VerifyAttestationNoVerifySignature(
var indexedAtt ethpb.IndexedAtt
if att.Version() >= version.Electra {
if att.GetData().CommitteeIndex != 0 {
return errors.New("committee index must be 0 post-Electra")
ci := att.GetData().CommitteeIndex
// Spec v1.7.0-alpha pseudocode:
//
// # [Modified in Gloas:EIP7732]
// assert data.index < 2
//
if beaconState.Version() >= version.Gloas {
if ci >= 2 {
return fmt.Errorf("incorrect committee index %d", ci)
}
} else {
if ci != 0 {
return errors.New("committee index must be 0 between Electra and Gloas forks")
}
}
aggBits := att.GetAggregationBits()
committeeIndices := att.CommitteeBitsVal().BitIndices()
committees := make([][]primitives.ValidatorIndex, len(committeeIndices))

View File

@@ -1,6 +1,7 @@
package blocks_test
import (
"bytes"
"context"
"testing"
@@ -262,7 +263,7 @@ func TestVerifyAttestationNoVerifySignature_Electra(t *testing.T) {
CommitteeBits: bitfield.NewBitvector64(),
}
err = blocks.VerifyAttestationNoVerifySignature(context.TODO(), beaconState, att)
assert.ErrorContains(t, "committee index must be 0 post-Electra", err)
assert.ErrorContains(t, "committee index must be 0", err)
})
t.Run("index of committee too big", func(t *testing.T) {
aggBits := bitfield.NewBitlist(3)
@@ -314,6 +315,75 @@ func TestVerifyAttestationNoVerifySignature_Electra(t *testing.T) {
})
}
func TestVerifyAttestationNoVerifySignature_GloasCommitteeIndexLimit(t *testing.T) {
cfg := params.BeaconConfig()
stateSlot := cfg.MinAttestationInclusionDelay + 1
blockRoots := make([][]byte, cfg.SlotsPerHistoricalRoot)
for i := range blockRoots {
blockRoots[i] = make([]byte, fieldparams.RootLength)
}
stateRoots := make([][]byte, cfg.SlotsPerHistoricalRoot)
for i := range stateRoots {
stateRoots[i] = make([]byte, fieldparams.RootLength)
}
randaoMixes := make([][]byte, cfg.EpochsPerHistoricalVector)
for i := range randaoMixes {
randaoMixes[i] = make([]byte, fieldparams.RootLength)
}
checkpointRoot := bytes.Repeat([]byte{0xAA}, fieldparams.RootLength)
justified := &ethpb.Checkpoint{Epoch: 0, Root: checkpointRoot}
gloasStateProto := &ethpb.BeaconStateGloas{
Slot: stateSlot,
GenesisValidatorsRoot: bytes.Repeat([]byte{0x11}, fieldparams.RootLength),
BlockRoots: blockRoots,
StateRoots: stateRoots,
RandaoMixes: randaoMixes,
ExecutionPayloadAvailability: make([]byte, cfg.SlotsPerHistoricalRoot/8),
CurrentJustifiedCheckpoint: justified,
PreviousJustifiedCheckpoint: justified,
Validators: []*ethpb.Validator{
{
EffectiveBalance: cfg.MinActivationBalance,
WithdrawalCredentials: append([]byte{cfg.ETH1AddressWithdrawalPrefixByte}, bytes.Repeat([]byte{0x01}, 31)...),
},
},
Balances: []uint64{cfg.MinActivationBalance},
BuilderPendingPayments: make([]*ethpb.BuilderPendingPayment, cfg.SlotsPerEpoch*2),
Fork: &ethpb.Fork{
CurrentVersion: bytes.Repeat([]byte{0x01}, 4),
PreviousVersion: bytes.Repeat([]byte{0x01}, 4),
Epoch: 0,
},
}
beaconState, err := state_native.InitializeFromProtoGloas(gloasStateProto)
require.NoError(t, err)
committeeBits := bitfield.NewBitvector64()
committeeBits.SetBitAt(0, true)
aggBits := bitfield.NewBitlist(1)
aggBits.SetBitAt(0, true)
att := &ethpb.AttestationElectra{
Data: &ethpb.AttestationData{
Slot: 0,
CommitteeIndex: 2, // invalid for Gloas (must be <2)
BeaconBlockRoot: blockRoots[0],
Source: justified,
Target: justified,
},
AggregationBits: aggBits,
CommitteeBits: committeeBits,
Signature: bytes.Repeat([]byte{0x00}, fieldparams.BLSSignatureLength),
}
err = blocks.VerifyAttestationNoVerifySignature(context.TODO(), beaconState, att)
assert.ErrorContains(t, "incorrect committee index 2", err)
}
func TestConvertToIndexed_OK(t *testing.T) {
helpers.ClearCache()
validators := make([]*ethpb.Validator, 2*params.BeaconConfig().SlotsPerEpoch)
@@ -583,6 +653,7 @@ func TestVerifyAttestations_HandlesPlannedFork(t *testing.T) {
}
func TestRetrieveAttestationSignatureSet_VerifiesMultipleAttestations(t *testing.T) {
helpers.ClearCache()
ctx := t.Context()
numOfValidators := uint64(params.BeaconConfig().SlotsPerEpoch.Mul(4))
validators := make([]*ethpb.Validator, numOfValidators)

View File

@@ -3,6 +3,7 @@ package blocks
import (
"testing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
v "github.com/OffchainLabs/prysm/v7/beacon-chain/core/validators"
state_native "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native"
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
@@ -318,7 +319,7 @@ func TestFuzzverifyDeposit_10000(t *testing.T) {
fuzzer.Fuzz(deposit)
s, err := state_native.InitializeFromProtoUnsafePhase0(state)
require.NoError(t, err)
err = VerifyDeposit(s, deposit)
err = helpers.VerifyDeposit(s, deposit)
_ = err
fuzz.FreeMemory(i)
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/gloas"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/signing"
v "github.com/OffchainLabs/prysm/v7/beacon-chain/core/validators"
@@ -62,6 +63,16 @@ func ProcessVoluntaryExits(
if exit == nil || exit.Exit == nil {
return nil, errors.New("nil voluntary exit in block body")
}
// [New in Gloas:EIP7732] Builder exits are identified by the builder index flag.
if beaconState.Version() >= version.Gloas && exit.Exit.ValidatorIndex.IsBuilderIndex() {
if err := verifyBuilderExitAndSignature(beaconState, exit); err != nil {
return nil, errors.Wrapf(err, "could not verify builder exit %d", idx)
}
if err := gloas.InitiateBuilderExit(beaconState, exit.Exit.ValidatorIndex.ToBuilderIndex()); err != nil {
return nil, err
}
continue
}
val, err := beaconState.ValidatorAtIndexReadOnly(exit.Exit.ValidatorIndex)
if err != nil {
return nil, err
@@ -102,19 +113,24 @@ func ProcessVoluntaryExits(
// initiate_validator_exit(state, voluntary_exit.validator_index)
func VerifyExitAndSignature(
validator state.ReadOnlyValidator,
state state.ReadOnlyBeaconState,
st state.ReadOnlyBeaconState,
signed *ethpb.SignedVoluntaryExit,
) error {
if signed == nil || signed.Exit == nil {
return errors.New("nil exit")
}
fork := state.Fork()
genesisRoot := state.GenesisValidatorsRoot()
// [New in Gloas:EIP7732] Builder exits are verified separately.
if st.Version() >= version.Gloas && signed.Exit.ValidatorIndex.IsBuilderIndex() {
return verifyBuilderExitAndSignature(st, signed)
}
fork := st.Fork()
genesisRoot := st.GenesisValidatorsRoot()
// EIP-7044: Beginning in Deneb, fix the fork version to Capella.
// This allows for signed validator exits to be valid forever.
if state.Version() >= version.Deneb {
if st.Version() >= version.Deneb {
fork = &ethpb.Fork{
PreviousVersion: params.BeaconConfig().CapellaForkVersion,
CurrentVersion: params.BeaconConfig().CapellaForkVersion,
@@ -123,7 +139,7 @@ func VerifyExitAndSignature(
}
exit := signed.Exit
if err := verifyExitConditions(state, validator, exit); err != nil {
if err := verifyExitConditions(st, validator, exit); err != nil {
return err
}
domain, err := signing.Domain(fork, exit.Epoch, params.BeaconConfig().DomainVoluntaryExit, genesisRoot)
@@ -198,3 +214,57 @@ func verifyExitConditions(st state.ReadOnlyBeaconState, validator state.ReadOnly
return nil
}
// verifyBuilderExitAndSignature validates a builder voluntary exit.
// [New in Gloas:EIP7732]
func verifyBuilderExitAndSignature(st state.ReadOnlyBeaconState, signed *ethpb.SignedVoluntaryExit) error {
if signed == nil || signed.Exit == nil {
return errors.New("nil exit")
}
exit := signed.Exit
builderIndex := exit.ValidatorIndex.ToBuilderIndex()
// Exits must specify an epoch when they become valid; they are not valid before then.
currentEpoch := slots.ToEpoch(st.Slot())
if currentEpoch < exit.Epoch {
return fmt.Errorf("expected current epoch >= exit epoch, received %d < %d", currentEpoch, exit.Epoch)
}
// Verify the builder is active.
active, err := st.IsActiveBuilder(builderIndex)
if err != nil {
return errors.Wrap(err, "could not check if builder is active")
}
if !active {
return fmt.Errorf("builder %d is not active", builderIndex)
}
// Only exit builder if it has no pending balance to withdraw.
pendingBalance, err := st.BuilderPendingBalanceToWithdraw(builderIndex)
if err != nil {
return errors.Wrap(err, "could not get builder pending balance to withdraw")
}
if pendingBalance != 0 {
return fmt.Errorf("builder %d has pending balance to withdraw: %d", builderIndex, pendingBalance)
}
// Verify signature using builder pubkey with Capella fork version (EIP-7044).
pubkey, err := st.BuilderPubkey(builderIndex)
if err != nil {
return errors.Wrap(err, "could not get builder pubkey")
}
fork := &ethpb.Fork{
PreviousVersion: params.BeaconConfig().CapellaForkVersion,
CurrentVersion: params.BeaconConfig().CapellaForkVersion,
Epoch: params.BeaconConfig().CapellaForkEpoch,
}
genesisRoot := st.GenesisValidatorsRoot()
domain, err := signing.Domain(fork, exit.Epoch, params.BeaconConfig().DomainVoluntaryExit, genesisRoot)
if err != nil {
return err
}
if err := signing.VerifySigningRoot(exit, pubkey[:], signed.Signature, domain); err != nil {
return signing.ErrSigFailedToVerify
}
return nil
}

View File

@@ -0,0 +1,307 @@
package blocks_test
import (
"testing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/blocks"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/signing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/validators"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
state_native "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/crypto/bls"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/testing/assert"
"github.com/OffchainLabs/prysm/v7/testing/require"
)
// setGloasTestConfig sets fork epochs so Gloas is active at epoch 5.
func setGloasTestConfig(t *testing.T) {
t.Helper()
cfg := params.BeaconConfig().Copy()
cfg.CapellaForkEpoch = 1
cfg.DenebForkEpoch = 2
cfg.ElectraForkEpoch = 3
cfg.FuluForkEpoch = 4
cfg.GloasForkEpoch = 5
params.SetActiveTestCleanup(t, cfg)
}
// newGloasStateWithBuilder creates a minimal Gloas beacon state with one active builder
// and returns the state along with the builder's BLS private key.
func newGloasStateWithBuilder(t *testing.T, builderIndex primitives.BuilderIndex, epoch primitives.Epoch) (state.BeaconState, bls.SecretKey) {
t.Helper()
priv, err := bls.RandKey()
require.NoError(t, err)
cfg := params.BeaconConfig()
builder := &ethpb.Builder{
Pubkey: priv.PublicKey().Marshal(),
WithdrawableEpoch: cfg.FarFutureEpoch,
DepositEpoch: 0,
Balance: 32_000_000_000,
ExecutionAddress: make([]byte, 20),
}
builders := make([]*ethpb.Builder, int(builderIndex)+1)
for i := range builders {
if primitives.BuilderIndex(i) == builderIndex {
builders[i] = builder
} else {
builders[i] = &ethpb.Builder{
Pubkey: make([]byte, 48),
WithdrawableEpoch: cfg.FarFutureEpoch,
DepositEpoch: 0,
ExecutionAddress: make([]byte, 20),
}
}
}
stProto := &ethpb.BeaconStateGloas{
Slot: cfg.SlotsPerEpoch * primitives.Slot(epoch),
Fork: &ethpb.Fork{
PreviousVersion: cfg.FuluForkVersion,
CurrentVersion: cfg.GloasForkVersion,
Epoch: cfg.GloasForkEpoch,
},
GenesisValidatorsRoot: make([]byte, 32),
FinalizedCheckpoint: &ethpb.Checkpoint{
Epoch: epoch - 1,
Root: make([]byte, 32),
},
CurrentJustifiedCheckpoint: &ethpb.Checkpoint{Root: make([]byte, 32)},
PreviousJustifiedCheckpoint: &ethpb.Checkpoint{Root: make([]byte, 32)},
Builders: builders,
Validators: []*ethpb.Validator{
{
ExitEpoch: cfg.FarFutureEpoch,
ActivationEpoch: 0,
PublicKey: make([]byte, 48),
},
},
Balances: []uint64{32_000_000_000},
BlockRoots: make([][]byte, cfg.SlotsPerHistoricalRoot),
StateRoots: make([][]byte, cfg.SlotsPerHistoricalRoot),
RandaoMixes: make([][]byte, cfg.EpochsPerHistoricalVector),
Slashings: make([]uint64, cfg.EpochsPerSlashingsVector),
ExecutionPayloadAvailability: make([]byte, cfg.SlotsPerHistoricalRoot/8),
}
for i := range stProto.BlockRoots {
stProto.BlockRoots[i] = make([]byte, 32)
}
for i := range stProto.StateRoots {
stProto.StateRoots[i] = make([]byte, 32)
}
for i := range stProto.RandaoMixes {
stProto.RandaoMixes[i] = make([]byte, 32)
}
st, err := state_native.InitializeFromProtoUnsafeGloas(stProto)
require.NoError(t, err)
return st, priv
}
func signBuilderExit(t *testing.T, st state.ReadOnlyBeaconState, exit *ethpb.VoluntaryExit, priv bls.SecretKey) *ethpb.SignedVoluntaryExit {
t.Helper()
sb, err := signing.ComputeDomainAndSign(st, exit.Epoch, exit, params.BeaconConfig().DomainVoluntaryExit, priv)
require.NoError(t, err)
sig, err := bls.SignatureFromBytes(sb)
require.NoError(t, err)
return &ethpb.SignedVoluntaryExit{
Exit: exit,
Signature: sig.Marshal(),
}
}
func TestVerifyExitAndSignature_BuilderExit_HappyPath(t *testing.T) {
setGloasTestConfig(t)
builderIndex := primitives.BuilderIndex(0)
epoch := primitives.Epoch(10)
st, priv := newGloasStateWithBuilder(t, builderIndex, epoch)
exit := &ethpb.VoluntaryExit{
ValidatorIndex: builderIndex.ToValidatorIndex(),
Epoch: epoch,
}
signed := signBuilderExit(t, st, exit, priv)
err := blocks.VerifyExitAndSignature(nil, st, signed)
require.NoError(t, err)
}
func TestVerifyExitAndSignature_BuilderNotActive(t *testing.T) {
setGloasTestConfig(t)
builderIndex := primitives.BuilderIndex(0)
epoch := primitives.Epoch(10)
st, priv := newGloasStateWithBuilder(t, builderIndex, epoch)
// Make builder not active by setting withdrawable epoch (already initiated exit).
builder, err := st.Builder(builderIndex)
require.NoError(t, err)
builder.WithdrawableEpoch = 5
require.NoError(t, st.UpdateBuilderAtIndex(builderIndex, builder))
exit := &ethpb.VoluntaryExit{
ValidatorIndex: builderIndex.ToValidatorIndex(),
Epoch: epoch,
}
signed := signBuilderExit(t, st, exit, priv)
err = blocks.VerifyExitAndSignature(nil, st, signed)
assert.ErrorContains(t, "is not active", err)
}
func TestVerifyExitAndSignature_BuilderPendingWithdrawal(t *testing.T) {
setGloasTestConfig(t)
builderIndex := primitives.BuilderIndex(0)
epoch := primitives.Epoch(10)
st, priv := newGloasStateWithBuilder(t, builderIndex, epoch)
// Give the builder a pending withdrawal.
require.NoError(t, st.AppendBuilderPendingWithdrawals([]*ethpb.BuilderPendingWithdrawal{
{
BuilderIndex: builderIndex,
Amount: 1000,
FeeRecipient: make([]byte, 20),
},
}))
exit := &ethpb.VoluntaryExit{
ValidatorIndex: builderIndex.ToValidatorIndex(),
Epoch: epoch,
}
signed := signBuilderExit(t, st, exit, priv)
err := blocks.VerifyExitAndSignature(nil, st, signed)
assert.ErrorContains(t, "pending balance to withdraw", err)
}
func TestVerifyExitAndSignature_BuilderBadSignature(t *testing.T) {
setGloasTestConfig(t)
builderIndex := primitives.BuilderIndex(0)
epoch := primitives.Epoch(10)
st, _ := newGloasStateWithBuilder(t, builderIndex, epoch)
wrongKey, err := bls.RandKey()
require.NoError(t, err)
exit := &ethpb.VoluntaryExit{
ValidatorIndex: builderIndex.ToValidatorIndex(),
Epoch: epoch,
}
signed := signBuilderExit(t, st, exit, wrongKey)
err = blocks.VerifyExitAndSignature(nil, st, signed)
assert.ErrorContains(t, "signature did not verify", err)
}
func TestVerifyExitAndSignature_BuilderExitInFuture(t *testing.T) {
setGloasTestConfig(t)
builderIndex := primitives.BuilderIndex(0)
epoch := primitives.Epoch(10)
st, priv := newGloasStateWithBuilder(t, builderIndex, epoch)
exit := &ethpb.VoluntaryExit{
ValidatorIndex: builderIndex.ToValidatorIndex(),
Epoch: epoch + 1, // Future epoch.
}
signed := signBuilderExit(t, st, exit, priv)
err := blocks.VerifyExitAndSignature(nil, st, signed)
assert.ErrorContains(t, "expected current epoch >= exit epoch", err)
}
func TestProcessVoluntaryExits_BuilderExit(t *testing.T) {
setGloasTestConfig(t)
builderIndex := primitives.BuilderIndex(0)
epoch := primitives.Epoch(10)
st, priv := newGloasStateWithBuilder(t, builderIndex, epoch)
exit := &ethpb.VoluntaryExit{
ValidatorIndex: builderIndex.ToValidatorIndex(),
Epoch: epoch,
}
signed := signBuilderExit(t, st, exit, priv)
newState, err := blocks.ProcessVoluntaryExits(t.Context(), st, []*ethpb.SignedVoluntaryExit{signed}, validators.ExitInformation(st))
require.NoError(t, err)
// Verify builder's withdrawable epoch was set.
builder, err := newState.Builder(builderIndex)
require.NoError(t, err)
cfg := params.BeaconConfig()
expectedWithdrawableEpoch := epoch + cfg.MinBuilderWithdrawabilityDelay
assert.Equal(t, expectedWithdrawableEpoch, builder.WithdrawableEpoch)
}
func TestProcessVoluntaryExits_BuilderExitPreGloas(t *testing.T) {
cfg := params.BeaconConfig().Copy()
cfg.CapellaForkEpoch = 1
cfg.DenebForkEpoch = 2
cfg.ElectraForkEpoch = 3
cfg.FuluForkEpoch = 4
cfg.GloasForkEpoch = 100 // Gloas not yet active.
params.SetActiveTestCleanup(t, cfg)
epoch := primitives.Epoch(10)
builderIndex := primitives.BuilderIndex(0)
stProto := &ethpb.BeaconStateFulu{
Slot: cfg.SlotsPerEpoch * primitives.Slot(epoch),
Fork: &ethpb.Fork{
PreviousVersion: cfg.DenebForkVersion,
CurrentVersion: cfg.FuluForkVersion,
Epoch: cfg.FuluForkEpoch,
},
GenesisValidatorsRoot: make([]byte, 32),
FinalizedCheckpoint: &ethpb.Checkpoint{Root: make([]byte, 32)},
CurrentJustifiedCheckpoint: &ethpb.Checkpoint{Root: make([]byte, 32)},
PreviousJustifiedCheckpoint: &ethpb.Checkpoint{Root: make([]byte, 32)},
Validators: []*ethpb.Validator{
{ExitEpoch: cfg.FarFutureEpoch, ActivationEpoch: 0, PublicKey: make([]byte, 48)},
},
Balances: []uint64{32_000_000_000},
BlockRoots: make([][]byte, cfg.SlotsPerHistoricalRoot),
StateRoots: make([][]byte, cfg.SlotsPerHistoricalRoot),
RandaoMixes: make([][]byte, cfg.EpochsPerHistoricalVector),
Slashings: make([]uint64, cfg.EpochsPerSlashingsVector),
}
for i := range stProto.BlockRoots {
stProto.BlockRoots[i] = make([]byte, 32)
}
for i := range stProto.StateRoots {
stProto.StateRoots[i] = make([]byte, 32)
}
for i := range stProto.RandaoMixes {
stProto.RandaoMixes[i] = make([]byte, 32)
}
st, err := state_native.InitializeFromProtoUnsafeFulu(stProto)
require.NoError(t, err)
signed := &ethpb.SignedVoluntaryExit{
Exit: &ethpb.VoluntaryExit{
ValidatorIndex: builderIndex.ToValidatorIndex(),
Epoch: epoch,
},
Signature: make([]byte, 96),
}
// On pre-Gloas state, builder-flagged exits are not routed to the builder path.
// ProcessVoluntaryExits treats the builder-flagged index as a regular validator index,
// which fails because no such validator exists.
_, err = blocks.ProcessVoluntaryExits(t.Context(), st, []*ethpb.SignedVoluntaryExit{signed}, validators.ExitInformation(st))
require.ErrorContains(t, "out of bounds", err)
}

View File

@@ -2,4 +2,5 @@ package blocks
var ProcessBLSToExecutionChange = processBLSToExecutionChange
var ErrInvalidBLSPrefix = errInvalidBLSPrefix
var ErrInvalidWithdrawalCredentials = errInvalidWithdrawalCredentials
var VerifyBlobCommitmentCount = verifyBlobCommitmentCount

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/gloas"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/signing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/time"
@@ -11,6 +12,7 @@ import (
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"github.com/OffchainLabs/prysm/v7/config/params"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/runtime/version"
"github.com/OffchainLabs/prysm/v7/time/slots"
"github.com/pkg/errors"
"google.golang.org/protobuf/proto"
@@ -126,7 +128,16 @@ func processProposerSlashing(
if exitInfo == nil {
return nil, errors.New("exit info is required to process proposer slashing")
}
var err error
// [New in Gloas:EIP7732]: remove the BuilderPendingPayment corresponding to the slashed proposer within 2 epoch window
if beaconState.Version() >= version.Gloas {
err = gloas.RemoveBuilderPendingPayment(beaconState, slashing.Header_1.Header)
if err != nil {
return nil, err
}
}
beaconState, err = validators.SlashValidator(ctx, beaconState, slashing.Header_1.Header.ProposerIndex, exitInfo)
if err != nil {
return nil, errors.Wrapf(err, "could not slash proposer index %d", slashing.Header_1.Header.ProposerIndex)

View File

@@ -212,8 +212,7 @@ func ProcessWithdrawals(st state.BeaconState, executionData interfaces.Execution
if err != nil {
return nil, errors.Wrap(err, "could not get next withdrawal validator index")
}
bound := min(uint64(st.NumValidators()), params.BeaconConfig().MaxValidatorsPerWithdrawalsSweep)
nextValidatorIndex += primitives.ValidatorIndex(bound)
nextValidatorIndex += primitives.ValidatorIndex(params.BeaconConfig().MaxValidatorsPerWithdrawalsSweep)
nextValidatorIndex = nextValidatorIndex % primitives.ValidatorIndex(st.NumValidators())
} else {
nextValidatorIndex = expectedWithdrawals[len(expectedWithdrawals)-1].ValidatorIndex + 1

View File

@@ -147,7 +147,7 @@ func TestProcessBLSToExecutionChange(t *testing.T) {
_, err = blocks.ValidateBLSToExecutionChange(st, signed)
// The state should return an empty validator, even when the validator object in the registry is
// nil. This error should return when the withdrawal credentials are invalid or too short.
require.ErrorIs(t, err, blocks.ErrInvalidBLSPrefix)
require.ErrorIs(t, err, blocks.ErrInvalidWithdrawalCredentials)
})
t.Run("non-existent validator", func(t *testing.T) {
priv, err := bls.RandKey()

View File

@@ -12,16 +12,13 @@ go_library(
"log.go",
"registry_updates.go",
"transition.go",
"transition_no_verify_sig.go",
"upgrade.go",
"validator.go",
"withdrawals.go",
],
importpath = "github.com/OffchainLabs/prysm/v7/beacon-chain/core/electra",
visibility = ["//visibility:public"],
deps = [
"//beacon-chain/core/altair:go_default_library",
"//beacon-chain/core/blocks:go_default_library",
"//beacon-chain/core/epoch:go_default_library",
"//beacon-chain/core/epoch/precompute:go_default_library",
"//beacon-chain/core/helpers:go_default_library",
@@ -31,6 +28,7 @@ go_library(
"//beacon-chain/state:go_default_library",
"//beacon-chain/state/state-native:go_default_library",
"//beacon-chain/state/state-native/custom-types:go_default_library",
"//beacon-chain/state/stateutil:go_default_library",
"//config/params:go_default_library",
"//consensus-types/interfaces:go_default_library",
"//consensus-types/primitives:go_default_library",
@@ -44,8 +42,6 @@ go_library(
"//proto/prysm/v1alpha1/attestation:go_default_library",
"//runtime/version:go_default_library",
"//time/slots:go_default_library",
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",
"@com_github_ethereum_go_ethereum//common/math:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
],
@@ -62,17 +58,14 @@ go_test(
"error_test.go",
"export_test.go",
"registry_updates_test.go",
"transition_no_verify_sig_test.go",
"transition_test.go",
"upgrade_test.go",
"validator_test.go",
"withdrawals_test.go",
],
data = glob(["testdata/**"]),
embed = [":go_default_library"],
deps = [
"//beacon-chain/core/helpers:go_default_library",
"//beacon-chain/core/signing:go_default_library",
"//beacon-chain/core/time:go_default_library",
"//beacon-chain/state:go_default_library",
"//beacon-chain/state/state-native:go_default_library",
@@ -85,16 +78,12 @@ go_test(
"//encoding/bytesutil:go_default_library",
"//proto/engine/v1:go_default_library",
"//proto/prysm/v1alpha1:go_default_library",
"//testing/assert:go_default_library",
"//testing/fuzz:go_default_library",
"//testing/require:go_default_library",
"//testing/util:go_default_library",
"//time/slots:go_default_library",
"@com_github_ethereum_go_ethereum//common:go_default_library",
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",
"@com_github_google_gofuzz//:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@com_github_sirupsen_logrus//hooks/test:go_default_library",
],
)

View File

@@ -3,19 +3,14 @@ package electra
import (
"bytes"
"context"
"fmt"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
state_native "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
"github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace"
enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
eth "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/time/slots"
"github.com/ethereum/go-ethereum/common/math"
"github.com/pkg/errors"
)
@@ -95,217 +90,6 @@ func ProcessPendingConsolidations(ctx context.Context, st state.BeaconState) err
return nil
}
// ProcessConsolidationRequests implements the spec definition below. This method makes mutating
// calls to the beacon state.
//
// def process_consolidation_request(
// state: BeaconState,
// consolidation_request: ConsolidationRequest
// ) -> None:
// if is_valid_switch_to_compounding_request(state, consolidation_request):
// validator_pubkeys = [v.pubkey for v in state.validators]
// request_source_pubkey = consolidation_request.source_pubkey
// source_index = ValidatorIndex(validator_pubkeys.index(request_source_pubkey))
// switch_to_compounding_validator(state, source_index)
// return
//
// # Verify that source != target, so a consolidation cannot be used as an exit.
// if consolidation_request.source_pubkey == consolidation_request.target_pubkey:
// return
// # If the pending consolidations queue is full, consolidation requests are ignored
// if len(state.pending_consolidations) == PENDING_CONSOLIDATIONS_LIMIT:
// return
// # If there is too little available consolidation churn limit, consolidation requests are ignored
// if get_consolidation_churn_limit(state) <= MIN_ACTIVATION_BALANCE:
// return
//
// validator_pubkeys = [v.pubkey for v in state.validators]
// # Verify pubkeys exists
// request_source_pubkey = consolidation_request.source_pubkey
// request_target_pubkey = consolidation_request.target_pubkey
// if request_source_pubkey not in validator_pubkeys:
// return
// if request_target_pubkey not in validator_pubkeys:
// return
// source_index = ValidatorIndex(validator_pubkeys.index(request_source_pubkey))
// target_index = ValidatorIndex(validator_pubkeys.index(request_target_pubkey))
// source_validator = state.validators[source_index]
// target_validator = state.validators[target_index]
//
// # Verify source withdrawal credentials
// has_correct_credential = has_execution_withdrawal_credential(source_validator)
// is_correct_source_address = (
// source_validator.withdrawal_credentials[12:] == consolidation_request.source_address
// )
// if not (has_correct_credential and is_correct_source_address):
// return
//
// # Verify that target has compounding withdrawal credentials
// if not has_compounding_withdrawal_credential(target_validator):
// return
//
// # Verify the source and the target are active
// current_epoch = get_current_epoch(state)
// if not is_active_validator(source_validator, current_epoch):
// return
// if not is_active_validator(target_validator, current_epoch):
// return
// # Verify exits for source and target have not been initiated
// if source_validator.exit_epoch != FAR_FUTURE_EPOCH:
// return
// if target_validator.exit_epoch != FAR_FUTURE_EPOCH:
// return
//
// # Verify the source has been active long enough
// if current_epoch < source_validator.activation_epoch + SHARD_COMMITTEE_PERIOD:
// return
//
// # Verify the source has no pending withdrawals in the queue
// if get_pending_balance_to_withdraw(state, source_index) > 0:
// return
// # Initiate source validator exit and append pending consolidation
// source_validator.exit_epoch = compute_consolidation_epoch_and_update_churn(
// state, source_validator.effective_balance
// )
// source_validator.withdrawable_epoch = Epoch(
// source_validator.exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY
// )
// state.pending_consolidations.append(PendingConsolidation(
// source_index=source_index,
// target_index=target_index
// ))
func ProcessConsolidationRequests(ctx context.Context, st state.BeaconState, reqs []*enginev1.ConsolidationRequest) error {
if len(reqs) == 0 || st == nil {
return nil
}
curEpoch := slots.ToEpoch(st.Slot())
ffe := params.BeaconConfig().FarFutureEpoch
minValWithdrawDelay := params.BeaconConfig().MinValidatorWithdrawabilityDelay
pcLimit := params.BeaconConfig().PendingConsolidationsLimit
for _, cr := range reqs {
if cr == nil {
return errors.New("nil consolidation request")
}
if ctx.Err() != nil {
return fmt.Errorf("cannot process consolidation requests: %w", ctx.Err())
}
if IsValidSwitchToCompoundingRequest(st, cr) {
srcIdx, ok := st.ValidatorIndexByPubkey(bytesutil.ToBytes48(cr.SourcePubkey))
if !ok {
log.Error("Failed to find source validator index")
continue
}
if err := SwitchToCompoundingValidator(st, srcIdx); err != nil {
log.WithError(err).Error("Failed to switch to compounding validator")
}
continue
}
sourcePubkey := bytesutil.ToBytes48(cr.SourcePubkey)
targetPubkey := bytesutil.ToBytes48(cr.TargetPubkey)
if sourcePubkey == targetPubkey {
continue
}
if npc, err := st.NumPendingConsolidations(); err != nil {
return fmt.Errorf("failed to fetch number of pending consolidations: %w", err) // This should never happen.
} else if npc >= pcLimit {
continue
}
activeBal, err := helpers.TotalActiveBalance(st)
if err != nil {
return err
}
churnLimit := helpers.ConsolidationChurnLimit(primitives.Gwei(activeBal))
if churnLimit <= primitives.Gwei(params.BeaconConfig().MinActivationBalance) {
continue
}
srcIdx, ok := st.ValidatorIndexByPubkey(sourcePubkey)
if !ok {
continue
}
tgtIdx, ok := st.ValidatorIndexByPubkey(targetPubkey)
if !ok {
continue
}
srcV, err := st.ValidatorAtIndex(srcIdx)
if err != nil {
return fmt.Errorf("failed to fetch source validator: %w", err) // This should never happen.
}
roSrcV, err := state_native.NewValidator(srcV)
if err != nil {
return err
}
tgtV, err := st.ValidatorAtIndexReadOnly(tgtIdx)
if err != nil {
return fmt.Errorf("failed to fetch target validator: %w", err) // This should never happen.
}
// Verify source withdrawal credentials
if !roSrcV.HasExecutionWithdrawalCredentials() {
continue
}
// Confirm source_validator.withdrawal_credentials[12:] == consolidation_request.source_address
if len(srcV.WithdrawalCredentials) != 32 || len(cr.SourceAddress) != 20 || !bytes.HasSuffix(srcV.WithdrawalCredentials, cr.SourceAddress) {
continue
}
// Target validator must have their withdrawal credentials set appropriately.
if !tgtV.HasCompoundingWithdrawalCredentials() {
continue
}
// Both validators must be active.
if !helpers.IsActiveValidator(srcV, curEpoch) || !helpers.IsActiveValidatorUsingTrie(tgtV, curEpoch) {
continue
}
// Neither validator is exiting.
if srcV.ExitEpoch != ffe || tgtV.ExitEpoch() != ffe {
continue
}
e, overflow := math.SafeAdd(uint64(srcV.ActivationEpoch), uint64(params.BeaconConfig().ShardCommitteePeriod))
if overflow {
log.Error("Overflow when adding activation epoch and shard committee period")
continue
}
if uint64(curEpoch) < e {
continue
}
hasBal, err := st.HasPendingBalanceToWithdraw(srcIdx)
if err != nil {
log.WithError(err).Error("Failed to fetch pending balance to withdraw")
continue
}
if hasBal {
continue
}
// Initiate the exit of the source validator.
exitEpoch, err := ComputeConsolidationEpochAndUpdateChurn(ctx, st, primitives.Gwei(srcV.EffectiveBalance))
if err != nil {
log.WithError(err).Error("Failed to compute consolidation epoch")
continue
}
srcV.ExitEpoch = exitEpoch
srcV.WithdrawableEpoch = exitEpoch + minValWithdrawDelay
if err := st.UpdateValidatorAtIndex(srcIdx, srcV); err != nil {
return fmt.Errorf("failed to update validator: %w", err) // This should never happen.
}
if err := st.AppendPendingConsolidation(&eth.PendingConsolidation{SourceIndex: srcIdx, TargetIndex: tgtIdx}); err != nil {
return fmt.Errorf("failed to append pending consolidation: %w", err) // This should never happen.
}
}
return nil
}
// IsValidSwitchToCompoundingRequest returns true if the given consolidation request is valid for switching to compounding.
//
// Spec code:

View File

@@ -8,8 +8,6 @@ import (
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
state_native "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
eth "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/testing/require"
@@ -203,275 +201,6 @@ func TestProcessPendingConsolidations(t *testing.T) {
}
}
func TestProcessConsolidationRequests(t *testing.T) {
tests := []struct {
name string
state state.BeaconState
reqs []*enginev1.ConsolidationRequest
validate func(*testing.T, state.BeaconState)
wantErr bool
}{
{
name: "nil request",
state: func() state.BeaconState {
st := &eth.BeaconStateElectra{}
s, err := state_native.InitializeFromProtoElectra(st)
require.NoError(t, err)
return s
}(),
reqs: []*enginev1.ConsolidationRequest{nil},
validate: func(t *testing.T, st state.BeaconState) {
require.DeepEqual(t, st, st)
},
wantErr: true,
},
{
name: "one valid request",
state: func() state.BeaconState {
st := &eth.BeaconStateElectra{
Slot: params.BeaconConfig().SlotsPerEpoch.Mul(uint64(params.BeaconConfig().ShardCommitteePeriod)),
Validators: createValidatorsWithTotalActiveBalance(32000000000000000), // 32M ETH
}
// Validator scenario setup. See comments in reqs section.
st.Validators[3].WithdrawalCredentials = bytesutil.Bytes32(0)
st.Validators[8].WithdrawalCredentials = bytesutil.Bytes32(1)
st.Validators[9].ActivationEpoch = params.BeaconConfig().FarFutureEpoch
st.Validators[12].ActivationEpoch = params.BeaconConfig().FarFutureEpoch
st.Validators[13].ExitEpoch = 10
st.Validators[16].ExitEpoch = 10
st.PendingPartialWithdrawals = []*eth.PendingPartialWithdrawal{
{
Index: 17,
Amount: 100,
},
}
s, err := state_native.InitializeFromProtoElectra(st)
require.NoError(t, err)
return s
}(),
reqs: []*enginev1.ConsolidationRequest{
// Source doesn't have withdrawal credentials.
{
SourceAddress: append(bytesutil.PadTo(nil, 19), byte(1)),
SourcePubkey: []byte("val_3"),
TargetPubkey: []byte("val_4"),
},
// Source withdrawal credentials don't match the consolidation address.
{
SourceAddress: append(bytesutil.PadTo(nil, 19), byte(0)), // Should be 5
SourcePubkey: []byte("val_5"),
TargetPubkey: []byte("val_6"),
},
// Target does not have their withdrawal credentials set appropriately. (Using eth1 address prefix)
{
SourceAddress: append(bytesutil.PadTo(nil, 19), byte(7)),
SourcePubkey: []byte("val_7"),
TargetPubkey: []byte("val_8"),
},
// Source is inactive.
{
SourceAddress: append(bytesutil.PadTo(nil, 19), byte(9)),
SourcePubkey: []byte("val_9"),
TargetPubkey: []byte("val_10"),
},
// Target is inactive.
{
SourceAddress: append(bytesutil.PadTo(nil, 19), byte(11)),
SourcePubkey: []byte("val_11"),
TargetPubkey: []byte("val_12"),
},
// Source is exiting.
{
SourceAddress: append(bytesutil.PadTo(nil, 19), byte(13)),
SourcePubkey: []byte("val_13"),
TargetPubkey: []byte("val_14"),
},
// Target is exiting.
{
SourceAddress: append(bytesutil.PadTo(nil, 19), byte(15)),
SourcePubkey: []byte("val_15"),
TargetPubkey: []byte("val_16"),
},
// Source doesn't exist
{
SourceAddress: append(bytesutil.PadTo(nil, 19), byte(0)),
SourcePubkey: []byte("INVALID"),
TargetPubkey: []byte("val_0"),
},
// Target doesn't exist
{
SourceAddress: append(bytesutil.PadTo(nil, 19), byte(0)),
SourcePubkey: []byte("val_0"),
TargetPubkey: []byte("INVALID"),
},
// Source == target
{
SourceAddress: append(bytesutil.PadTo(nil, 19), byte(0)),
SourcePubkey: []byte("val_0"),
TargetPubkey: []byte("val_0"),
},
// Has pending partial withdrawal
{
SourceAddress: append(bytesutil.PadTo(nil, 19), byte(0)),
SourcePubkey: []byte("val_17"),
TargetPubkey: []byte("val_1"),
},
// Valid consolidation request. This should be last to ensure invalid requests do
// not end the processing early.
{
SourceAddress: append(bytesutil.PadTo(nil, 19), byte(1)),
SourcePubkey: []byte("val_1"),
TargetPubkey: []byte("val_2"),
},
},
validate: func(t *testing.T, st state.BeaconState) {
// Verify a pending consolidation is created.
numPC, err := st.NumPendingConsolidations()
require.NoError(t, err)
require.Equal(t, uint64(1), numPC)
pcs, err := st.PendingConsolidations()
require.NoError(t, err)
require.Equal(t, primitives.ValidatorIndex(1), pcs[0].SourceIndex)
require.Equal(t, primitives.ValidatorIndex(2), pcs[0].TargetIndex)
// Verify the source validator is exiting.
src, err := st.ValidatorAtIndex(1)
require.NoError(t, err)
require.NotEqual(t, params.BeaconConfig().FarFutureEpoch, src.ExitEpoch, "source validator exit epoch not updated")
require.Equal(t, params.BeaconConfig().MinValidatorWithdrawabilityDelay, src.WithdrawableEpoch-src.ExitEpoch, "source validator withdrawable epoch not set correctly")
},
},
{
name: "pending consolidations limit reached",
state: func() state.BeaconState {
st := &eth.BeaconStateElectra{
Validators: createValidatorsWithTotalActiveBalance(32000000000000000), // 32M ETH
PendingConsolidations: make([]*eth.PendingConsolidation, params.BeaconConfig().PendingConsolidationsLimit),
}
s, err := state_native.InitializeFromProtoElectra(st)
require.NoError(t, err)
return s
}(),
reqs: []*enginev1.ConsolidationRequest{
{
SourceAddress: append(bytesutil.PadTo(nil, 19), byte(1)),
SourcePubkey: []byte("val_1"),
TargetPubkey: []byte("val_2"),
},
},
validate: func(t *testing.T, st state.BeaconState) {
// Verify no pending consolidation is created.
numPC, err := st.NumPendingConsolidations()
require.NoError(t, err)
require.Equal(t, params.BeaconConfig().PendingConsolidationsLimit, numPC)
// Verify the source validator is not exiting.
src, err := st.ValidatorAtIndex(1)
require.NoError(t, err)
require.Equal(t, params.BeaconConfig().FarFutureEpoch, src.ExitEpoch, "source validator exit epoch should not be updated")
require.Equal(t, params.BeaconConfig().FarFutureEpoch, src.WithdrawableEpoch, "source validator withdrawable epoch should not be updated")
},
},
{
name: "pending consolidations limit reached during processing",
state: func() state.BeaconState {
st := &eth.BeaconStateElectra{
Slot: params.BeaconConfig().SlotsPerEpoch.Mul(uint64(params.BeaconConfig().ShardCommitteePeriod)),
Validators: createValidatorsWithTotalActiveBalance(32000000000000000), // 32M ETH
PendingConsolidations: make([]*eth.PendingConsolidation, params.BeaconConfig().PendingConsolidationsLimit-1),
}
s, err := state_native.InitializeFromProtoElectra(st)
require.NoError(t, err)
return s
}(),
reqs: []*enginev1.ConsolidationRequest{
{
SourceAddress: append(bytesutil.PadTo(nil, 19), byte(1)),
SourcePubkey: []byte("val_1"),
TargetPubkey: []byte("val_2"),
},
{
SourceAddress: append(bytesutil.PadTo(nil, 19), byte(3)),
SourcePubkey: []byte("val_3"),
TargetPubkey: []byte("val_4"),
},
},
validate: func(t *testing.T, st state.BeaconState) {
// Verify a pending consolidation is created.
numPC, err := st.NumPendingConsolidations()
require.NoError(t, err)
require.Equal(t, params.BeaconConfig().PendingConsolidationsLimit, numPC)
// The first consolidation was appended.
pcs, err := st.PendingConsolidations()
require.NoError(t, err)
require.Equal(t, primitives.ValidatorIndex(1), pcs[params.BeaconConfig().PendingConsolidationsLimit-1].SourceIndex)
require.Equal(t, primitives.ValidatorIndex(2), pcs[params.BeaconConfig().PendingConsolidationsLimit-1].TargetIndex)
// Verify the second source validator is not exiting.
src, err := st.ValidatorAtIndex(3)
require.NoError(t, err)
require.Equal(t, params.BeaconConfig().FarFutureEpoch, src.ExitEpoch, "source validator exit epoch should not be updated")
require.Equal(t, params.BeaconConfig().FarFutureEpoch, src.WithdrawableEpoch, "source validator withdrawable epoch should not be updated")
},
},
{
name: "pending consolidations limit reached and compounded consolidation after",
state: func() state.BeaconState {
st := &eth.BeaconStateElectra{
Slot: params.BeaconConfig().SlotsPerEpoch.Mul(uint64(params.BeaconConfig().ShardCommitteePeriod)),
Validators: createValidatorsWithTotalActiveBalance(32000000000000000), // 32M ETH
PendingConsolidations: make([]*eth.PendingConsolidation, params.BeaconConfig().PendingConsolidationsLimit),
}
// To allow compounding consolidation requests.
st.Validators[3].WithdrawalCredentials[0] = params.BeaconConfig().ETH1AddressWithdrawalPrefixByte
s, err := state_native.InitializeFromProtoElectra(st)
require.NoError(t, err)
return s
}(),
reqs: []*enginev1.ConsolidationRequest{
{
SourceAddress: append(bytesutil.PadTo(nil, 19), byte(1)),
SourcePubkey: []byte("val_1"),
TargetPubkey: []byte("val_2"),
},
{
SourceAddress: append(bytesutil.PadTo(nil, 19), byte(3)),
SourcePubkey: []byte("val_3"),
TargetPubkey: []byte("val_3"),
},
},
validate: func(t *testing.T, st state.BeaconState) {
// Verify a pending consolidation is created.
numPC, err := st.NumPendingConsolidations()
require.NoError(t, err)
require.Equal(t, params.BeaconConfig().PendingConsolidationsLimit, numPC)
// Verify that the last consolidation was included
src, err := st.ValidatorAtIndex(3)
require.NoError(t, err)
require.Equal(t, params.BeaconConfig().CompoundingWithdrawalPrefixByte, src.WithdrawalCredentials[0], "source validator was not compounded")
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := electra.ProcessConsolidationRequests(context.TODO(), tt.state, tt.reqs)
if (err != nil) != tt.wantErr {
t.Errorf("ProcessWithdrawalRequests() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
require.NoError(t, err)
}
if tt.validate != nil {
tt.validate(t, tt.state)
}
})
}
}
func TestIsValidSwitchToCompoundingRequest(t *testing.T) {
st, _ := util.DeterministicGenesisStateElectra(t, 1)
t.Run("nil source pubkey", func(t *testing.T) {

View File

@@ -3,7 +3,6 @@ package electra
import (
"context"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/blocks"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/signing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
@@ -13,7 +12,6 @@ import (
"github.com/OffchainLabs/prysm/v7/contracts/deposit"
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
"github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace"
enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/runtime/version"
"github.com/OffchainLabs/prysm/v7/time/slots"
@@ -37,7 +35,7 @@ func ProcessDeposits(
defer span.End()
// Attempt to verify all deposit signatures at once, if this fails then fall back to processing
// individual deposits with signature verification enabled.
allSignaturesVerified, err := blocks.BatchVerifyDepositsSignatures(ctx, deposits)
allSignaturesVerified, err := helpers.BatchVerifyDepositsSignatures(ctx, deposits)
if err != nil {
return nil, errors.Wrap(err, "could not verify deposit signatures in batch")
}
@@ -82,7 +80,7 @@ func ProcessDeposits(
// signature=deposit.data.signature,
// )
func ProcessDeposit(beaconState state.BeaconState, deposit *ethpb.Deposit, allSignaturesVerified bool) (state.BeaconState, error) {
if err := blocks.VerifyDeposit(beaconState, deposit); err != nil {
if err := helpers.VerifyDeposit(beaconState, deposit); err != nil {
if deposit == nil || deposit.Data == nil {
return nil, err
}
@@ -377,7 +375,7 @@ func batchProcessNewPendingDeposits(ctx context.Context, state state.BeaconState
return nil
}
allSignaturesVerified, err := blocks.BatchVerifyPendingDepositsSignatures(ctx, pendingDeposits)
allSignaturesVerified, err := helpers.BatchVerifyPendingDepositsSignatures(ctx, pendingDeposits)
if err != nil {
return errors.Wrap(err, "batch signature verification failed")
}
@@ -386,7 +384,7 @@ func batchProcessNewPendingDeposits(ctx context.Context, state state.BeaconState
validSig := allSignaturesVerified
if !allSignaturesVerified {
validSig, err = blocks.IsValidDepositSignature(&ethpb.Deposit_Data{
validSig, err = helpers.IsValidDepositSignature(&ethpb.Deposit_Data{
PublicKey: bytesutil.SafeCopyBytes(pd.PublicKey),
WithdrawalCredentials: bytesutil.SafeCopyBytes(pd.WithdrawalCredentials),
Amount: pd.Amount,
@@ -441,7 +439,7 @@ func ApplyPendingDeposit(ctx context.Context, st state.BeaconState, deposit *eth
defer span.End()
index, ok := st.ValidatorIndexByPubkey(bytesutil.ToBytes48(deposit.PublicKey))
if !ok {
verified, err := blocks.IsValidDepositSignature(&ethpb.Deposit_Data{
verified, err := helpers.IsValidDepositSignature(&ethpb.Deposit_Data{
PublicKey: bytesutil.SafeCopyBytes(deposit.PublicKey),
WithdrawalCredentials: bytesutil.SafeCopyBytes(deposit.WithdrawalCredentials),
Amount: deposit.Amount,
@@ -537,62 +535,3 @@ func GetValidatorFromDeposit(pubKey []byte, withdrawalCredentials []byte, amount
validator.EffectiveBalance = min(amount-(amount%params.BeaconConfig().EffectiveBalanceIncrement), maxEffectiveBalance)
return validator, nil
}
// ProcessDepositRequests is a function as part of electra to process execution layer deposits
func ProcessDepositRequests(ctx context.Context, beaconState state.BeaconState, requests []*enginev1.DepositRequest) (state.BeaconState, error) {
_, span := trace.StartSpan(ctx, "electra.ProcessDepositRequests")
defer span.End()
if len(requests) == 0 {
return beaconState, nil
}
var err error
for _, receipt := range requests {
beaconState, err = processDepositRequest(beaconState, receipt)
if err != nil {
return nil, errors.Wrap(err, "could not apply deposit request")
}
}
return beaconState, nil
}
// processDepositRequest processes the specific deposit request
// def process_deposit_request(state: BeaconState, deposit_request: DepositRequest) -> None:
//
// # Set deposit request start index
// if state.deposit_requests_start_index == UNSET_DEPOSIT_REQUESTS_START_INDEX:
// state.deposit_requests_start_index = deposit_request.index
//
// # Create pending deposit
// state.pending_deposits.append(PendingDeposit(
// pubkey=deposit_request.pubkey,
// withdrawal_credentials=deposit_request.withdrawal_credentials,
// amount=deposit_request.amount,
// signature=deposit_request.signature,
// slot=state.slot,
// ))
func processDepositRequest(beaconState state.BeaconState, request *enginev1.DepositRequest) (state.BeaconState, error) {
requestsStartIndex, err := beaconState.DepositRequestsStartIndex()
if err != nil {
return nil, errors.Wrap(err, "could not get deposit requests start index")
}
if request == nil {
return nil, errors.New("nil deposit request")
}
if requestsStartIndex == params.BeaconConfig().UnsetDepositRequestsStartIndex {
if err := beaconState.SetDepositRequestsStartIndex(request.Index); err != nil {
return nil, errors.Wrap(err, "could not set deposit requests start index")
}
}
if err := beaconState.AppendPendingDeposit(&ethpb.PendingDeposit{
PublicKey: bytesutil.SafeCopyBytes(request.Pubkey),
WithdrawalCredentials: bytesutil.SafeCopyBytes(request.WithdrawalCredentials),
Amount: request.Amount,
Signature: bytesutil.SafeCopyBytes(request.Signature),
Slot: beaconState.Slot(),
}); err != nil {
return nil, errors.Wrap(err, "could not append deposit request")
}
return beaconState, nil
}

View File

@@ -6,7 +6,6 @@ import (
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/electra"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/signing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
state_native "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native"
stateTesting "github.com/OffchainLabs/prysm/v7/beacon-chain/state/testing"
@@ -15,44 +14,52 @@ import (
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/crypto/bls"
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
eth "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/testing/require"
"github.com/OffchainLabs/prysm/v7/testing/util"
)
func TestProcessPendingDepositsMultiplesSameDeposits(t *testing.T) {
st := stateWithActiveBalanceETH(t, 1000)
deps := make([]*eth.PendingDeposit, 2) // Make same deposit twice
validators := st.Validators()
sk, err := bls.RandKey()
require.NoError(t, err)
for i := 0; i < len(deps); i += 1 {
wc := make([]byte, 32)
wc[0] = params.BeaconConfig().ETH1AddressWithdrawalPrefixByte
wc[31] = byte(i)
validators[i].PublicKey = sk.PublicKey().Marshal()
validators[i].WithdrawalCredentials = wc
deps[i] = stateTesting.GeneratePendingDeposit(t, sk, 32, bytesutil.ToBytes32(wc), 0)
}
require.NoError(t, st.SetPendingDeposits(deps))
const (
depositCount = uint64(2)
amountETH = uint64(32)
slot = 0
activeBalanceGwei = 10_000
)
err = electra.ProcessPendingDeposits(context.TODO(), st, 10000)
state := stateWithActiveBalanceETH(t, 0)
secretKey, err := bls.RandKey()
require.NoError(t, err)
val := st.Validators()
seenPubkeys := make(map[string]struct{})
for i := 0; i < len(val); i += 1 {
if len(val[i].PublicKey) == 0 {
continue
}
_, ok := seenPubkeys[string(val[i].PublicKey)]
if ok {
t.Fatalf("duplicated pubkeys")
} else {
seenPubkeys[string(val[i].PublicKey)] = struct{}{}
}
withdrawalCredentialsBytes := make([]byte, 32)
withdrawalCredentialsBytes[0] = params.BeaconConfig().ETH1AddressWithdrawalPrefixByte
withdrawalCredentials := bytesutil.ToBytes32(withdrawalCredentialsBytes)
validators := state.Validators()
require.Equal(t, 0, len(validators))
deposits := make([]*eth.PendingDeposit, 0, depositCount)
for range depositCount {
deposit := stateTesting.GeneratePendingDeposit(t, secretKey, amountETH, withdrawalCredentials, slot)
deposits = append(deposits, deposit)
}
err = state.SetPendingDeposits(deposits)
require.NoError(t, err)
err = electra.ProcessPendingDeposits(t.Context(), state, activeBalanceGwei)
require.NoError(t, err)
// The first deposit should create a new validator,
// and the second deposit should top up the same validator
// We should have 1 validator with balance of 64 ETH.
validators = state.Validators()
require.Equal(t, 1, len(validators))
balance, err := state.BalanceAtIndex(0)
require.NoError(t, err)
require.Equal(t, depositCount*amountETH, balance)
}
func TestProcessPendingDeposits(t *testing.T) {
@@ -361,60 +368,6 @@ func TestBatchProcessNewPendingDeposits(t *testing.T) {
})
}
func TestProcessDepositRequests(t *testing.T) {
st, _ := util.DeterministicGenesisStateElectra(t, 1)
sk, err := bls.RandKey()
require.NoError(t, err)
require.NoError(t, st.SetDepositRequestsStartIndex(1))
t.Run("empty requests continues", func(t *testing.T) {
newSt, err := electra.ProcessDepositRequests(t.Context(), st, []*enginev1.DepositRequest{})
require.NoError(t, err)
require.DeepEqual(t, newSt, st)
})
t.Run("nil request errors", func(t *testing.T) {
_, err = electra.ProcessDepositRequests(t.Context(), st, []*enginev1.DepositRequest{nil})
require.ErrorContains(t, "nil deposit request", err)
})
vals := st.Validators()
vals[0].PublicKey = sk.PublicKey().Marshal()
vals[0].WithdrawalCredentials[0] = params.BeaconConfig().ETH1AddressWithdrawalPrefixByte
require.NoError(t, st.SetValidators(vals))
bals := st.Balances()
bals[0] = params.BeaconConfig().MinActivationBalance + 2000
require.NoError(t, st.SetBalances(bals))
require.NoError(t, st.SetPendingDeposits(make([]*eth.PendingDeposit, 0))) // reset pbd as the determinitstic state populates this already
withdrawalCred := make([]byte, 32)
withdrawalCred[0] = params.BeaconConfig().CompoundingWithdrawalPrefixByte
depositMessage := &eth.DepositMessage{
PublicKey: sk.PublicKey().Marshal(),
Amount: 1000,
WithdrawalCredentials: withdrawalCred,
}
domain, err := signing.ComputeDomain(params.BeaconConfig().DomainDeposit, nil, nil)
require.NoError(t, err)
sr, err := signing.ComputeSigningRoot(depositMessage, domain)
require.NoError(t, err)
sig := sk.Sign(sr[:])
requests := []*enginev1.DepositRequest{
{
Pubkey: depositMessage.PublicKey,
Index: 0,
WithdrawalCredentials: depositMessage.WithdrawalCredentials,
Amount: depositMessage.Amount,
Signature: sig.Marshal(),
},
}
st, err = electra.ProcessDepositRequests(t.Context(), st, requests)
require.NoError(t, err)
pbd, err := st.PendingDeposits()
require.NoError(t, err)
require.Equal(t, 1, len(pbd))
require.Equal(t, uint64(1000), pbd[0].Amount)
}
func TestProcessDeposit_Electra_Simple(t *testing.T) {
deps, _, err := util.DeterministicDepositsAndKeysSameValidator(3)
require.NoError(t, err)

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