Compare commits

...

136 Commits

Author SHA1 Message Date
james-prysm
4a6f470dd2 Fix earliestAvailableSlot going backwards on custody increase 2025-12-16 11:09:57 -06:00
james-prysm
4e0bfada17 Add e2e test for earliestAvailableSlot regression 2025-12-16 11:09:44 -06:00
james-prysm
8a37575407 gaz 2025-12-15 13:03:25 -06:00
james-prysm
0b1d4c485d Merge branch 'develop' into fulu-e2e 2025-12-15 10:57:28 -08:00
james-prysm
0d04013443 more reverals removing e2e build" 2025-12-15 12:56:28 -06:00
james-prysm
2fbcbea550 reverting changes adding e2e field params and custom build 2025-12-15 12:37:25 -06:00
terence
9fcc1a7a77 Guard KZG send with context cancellation (#16144)
Avoid sending KZG verification reqs when the caller context is already
canceled to prevent blocking on the channel
2025-12-15 16:58:51 +00:00
james-prysm
5a49dcabb6 fixing test 2025-12-15 10:12:35 -06:00
james-prysm
d0580debf6 Merge branch 'develop' into fulu-e2e 2025-12-15 06:36:18 -08:00
Potuz
75dea214ac Do not error when indices have been computed (#16142)
If there is a context deadline updating the committee cache, but the
indices have been computed correctly, do not error out but rather return
the indices and log the error.
2025-12-13 17:36:06 +00:00
james-prysm
4374e709cb fixing state replay caused by REST api duties attester and sync committee endpoints (#16136)
<!-- 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?**

s.Stater.StateBySlot may replay if it's the current epoch as it's for
values in the db, if we are in the current we should try to get head
slot and use the cache, proposer duties was doing this already but the
other 2 duties endpoints was not. this pr aligns all 3 and introduces a
new `statebyepoch` that just wraps the approach.

I tested by running this kurtosis config with and without the fix to see
that the replays stop, the blockchain progresses, and the upgraded to
fulu is not printed multiple times

```
participants:
 # Super-nodes
 - el_type: nethermind
   cl_type: prysm
   cl_image: gcr.io/offchainlabs/prysm/beacon-chain:latest
   count: 2
   supernode: true
   cl_extra_params:
     - --subscribe-all-subnets
     - --verbosity=debug
   vc_extra_params:
     - --enable-beacon-rest-api
     - --verbosity=debug

 # Full-nodes
 - el_type: nethermind
   cl_type: prysm
   cl_image: gcr.io/offchainlabs/prysm/beacon-chain:latest
   validator_count: 63
   cl_extra_params:
     - --verbosity=debug
   vc_extra_params:
     - --enable-beacon-rest-api
     - --verbosity=debug

 - el_type: nethermind
   cl_type: prysm
   cl_image: gcr.io/offchainlabs/prysm/beacon-chain:latest
   cl_extra_params:
     - --verbosity=debug
   vc_extra_params:
     - --enable-beacon-rest-api
     - --verbosity=debug
   validator_count: 13

additional_services:
 - dora
 - spamoor

spamoor_params:
 image: ethpandaops/spamoor:master
 max_mem: 4000
 spammers:
   - scenario: eoatx
     config:
       throughput: 200
   - scenario: blobs
     config:
       throughput: 20

network_params:
  fulu_fork_epoch: 2
  bpo_1_epoch: 8
  bpo_1_max_blobs: 21
  withdrawal_type: "0x02"
  preset: mainnet
  seconds_per_slot: 6

global_log_level: debug
```

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

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

**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).
2025-12-12 23:18:22 +00:00
james-prysm
869a6586ff removing e2e field params based on feedback 2025-12-12 16:35:28 -06:00
Radosław Kapka
be300f80bd Static analyzer for httputil.HandleError calls (#16134)
**What type of PR is this?**

Tooling

**What does the PR do?**

Every call to `httputil.HandleError` must be followed by a `return`
statement. It's easy to miss this during reviews, so having a static
analyzer that enforces this will make our life easier.

**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>
2025-12-12 21:38:09 +00:00
terence
096cba5b2d sync: fix KZG batch verifier deadlock on timeout (#16141)
`validateWithKzgBatchVerifier` could timeout (12s) and once it times out
because `resChan` is unbuffered, the verifier will stuck at following
line at `verifyKzgBatch` as its waiting for someone to grab the result
from `resChan`:
```
	for _, verifier := range kzgBatch {
		verifier.resChan <- verificationErr
	}
```
Fix is to make kzg batch verification non blocking on timeouts by
buffering each request’s buffered size 1
2025-12-12 17:17:40 +00:00
SashaMalysehko
d5127233e4 fix: missing return after version header check (#16126)
Ensure SubmitAttesterSlashingsV2 returns immediately when the
Eth-Consensus-Version header is missing. Without this early return the
handler calls version.FromString with an empty value and writes a second
JSON error to the response, producing invalid JSON and duplicating error
output. This change aligns the handler with the error-handling pattern
used in other endpoints that validate the version header.
2025-12-12 17:09:35 +00:00
Radosław Kapka
3d35cc20ec Use WriteStateFetchError in API handlers whenever possible (#16140)
**What type of PR is this?**

Other

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

Calls to `Stater.StateBySlot` and `Stater.State` should be followed by
`shared.WriteStateFetchError` to provide the most robust error handling.

**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).
2025-12-12 16:26:27 +00:00
Aarsh Shah
1e658530a7 revert https://github.com/OffchainLabs/prysm/pull/16100 (#16139)
This PR reverts https://github.com/OffchainLabs/prysm/pull/16100.

**What type of PR is this?**
Bug fix


**What does this PR do? Why is it needed?**
This PR reverts https://github.com/OffchainLabs/prysm/pull/16100 as that
PR deprecates mplex but other implementations only support mplex for
now..


**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).
2025-12-12 14:59:32 +00:00
Preston Van Loon
b360794c9c Update CHANGELOG.md for v7.1.0 release (#16127)
**What type of PR is this?**

Documentation

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

Changelog

**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.
2025-12-11 22:48:33 +00:00
Aarsh Shah
0fc9ab925a feat: add support for detecting and logging per address reachability via libp2p AutoNAT v2 (#16100)
**What type of PR is this?**
Feature

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

This PR adds support for detecting and logging per address reachability
via libp2p AutoNAT v2. See
https://github.com/libp2p/go-libp2p/releases/tag/v0.42.0 for details.
This PR also upgrades Prysm to libp2p v0.42.0

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

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

**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.
2025-12-11 11:56:52 +00:00
satushh
dda5ee3334 Graffiti proposal design doc (#15983)
<!-- 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?**

Design Doc

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

This PR adds a design doc for adding graffiti. The idea is to have it
populated judiciously so that we can get proper information about the
EL, CL and their corresponding version info. At the same time being
flexible enough with the user input.

**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.
2025-12-10 22:57:40 +00:00
Manu NALEPA
14c67376c3 Add test requirement to PULL_REQUEST_TEMPLATE.md (#16123)
**What type of PR is this?**
Other

**What does this PR do? Why is it needed?**
This pull request modifies the `PULL_REQUEST_TEMPLATE.md` to ensure the
developer checked that their PR works as expected.

Some contributors push some changes, without even running the modified
client once to see if their changes work as expected.

Avoidable back-and-forth trips between the contributor and the reviewers
could be prevented thanks to running the modified client.

**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 <pvanloon@offchainlabs.com>
2025-12-10 17:40:29 +00:00
Preston Van Loon
9c8b68a66d Update CHANGELOG.md for v7.0.1 release (#16107)
**What type of PR is this?**

Other

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

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

**Other notes for review**

Did not delete the fragments as they are still needed to generate v7.1.0
release notes. This release is all cherry-picks which would be included
in v7.1.0

**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.
2025-12-10 17:07:38 +00:00
Potuz
a3210157e2 Fix TOCTOU race validating attestations (#16105)
A TOCTOU issue was reported by EF security in which two attestations
being validated at the same time may result in both of them being
forwarded. The spec says that we need to forward only the first one.
2025-12-09 19:26:05 +00:00
satushh
1536d59e30 Remove unnecessary copy in Eth1DataHasEnoughSupport (#16118)
<!-- 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?**

- Remove unnecessary `Copy()` call in `Eth1DataHasEnoughSupport`
- `data.Copy()` was called on every iteration of the vote counting loop,
even though `AreEth1DataEqual` only reads the data and never mutates it.
- Additionally, `Eth1DataVotes()` already returns copies of all votes,
so state is protected regardless.

**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.
2025-12-09 19:02:36 +00:00
satushh
11e46a4560 Optimise for loop of MigrateToCold (#16101)
<!-- 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?**

The for loop in MigrateToCold function was brute force in nature. It
could be improved by just directly jumping by `slotsPerArchivedPoint`
rather than going over every single slot.

```
for slot := oldFSlot; slot < fSlot; slot++ {
  ...
   if slot%s.slotsPerArchivedPoint == 0 && slot != 0 {
```
No need to do the modulo for every single slot.
We could just find the correct starting point and jump by
slotsPerArchivedPoint at a time.

**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: Potuz <potuz@prysmaticlabs.com>
2025-12-09 17:15:52 +00:00
Snezhkko
5a2e51b894 fix(rpc): incorrect constructor return type (#16084)
The constructor `NewStateRootNotFoundError` incorrectly returned
`StateNotFoundError`. This prevented handlers that rely on
errors.As(err, *lookup.StateRootNotFoundError) from matching and mapping
the error to HTTP 404. The function now returns
StateRootNotFoundError and constructs that type, restoring the intended
behavior for “state root not found” cases.

---------

Co-authored-by: Radosław Kapka <rkapka@wp.pl>
2025-12-09 13:56:00 +00:00
Potuz
d20ec4c7a1 Track the dependent root of the latest finalized checkpoint (#16103)
This PR adds the dependent root of the latest finalized checkpoint to
forkchoice since this node will be typically pruned upon finalization.
2025-12-08 16:16:32 +00:00
terence
7a70abbd15 Add --ignore-unviable-attestations and deprecate --disable-last-epoch-targets (#16094)
This PR introduces flag `--ignore-unviable-attestations` (replaces and
deprecates `--disable-last-epoch-targets`) to drop attestations whose
target state is not viable; default remains to process them unless
explicitly enabled.
2025-12-05 15:03:04 +00:00
Potuz
a2b84c9320 Use head state in more cases (#16095)
The head state is guaranteed to have the same shuffling and active
indices if the previous dependent root coincides with the target
checkpoint's in some cases.
2025-12-05 03:44:03 +00:00
terence
edef17e41d Add arrival latency tracking for data column sidecars (#16099)
We have this for blob sidecars but not for data columns
2025-12-04 21:28:02 +00:00
Manu NALEPA
85c5d31b5b blobsDataFromStoredDataColumns: Ask the use to use the --supernode flag and shorten the error mesage. (#16097)
**What type of PR is this?**
Other

**What does this PR do? Why is it needed?**
`blobsDataFromStoredDataColumns`: Ask the use to use the `--supernode`
flag and shorten the error mesage.

**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.
2025-12-04 15:54:13 +00:00
james-prysm
0b47ac51f7 fixing starting post deneb 2025-12-03 12:09:56 -06:00
james-prysm
75916243f2 adding support to start from fulu fork 2025-12-03 09:40:47 -06:00
james-prysm
b499186483 fixing evaluator to match others 2025-12-03 08:50:37 -06:00
Manu NALEPA
fa056c2d21 Move the "Not enough connected peers" (for a given subnet) from WARN to DEBUG (#16087)
**What type of PR is this?**
Other

**What does this PR do? Why is it needed?**
Move the "Not enough connected peers" (for a given subnet) from WARN to
DEBUG

**Rationale:**
The
<img width="1839" height="31" alt="image"
src="https://github.com/user-attachments/assets/44dbdc8d-3e37-42ee-967b-75a7a1fbcafb"
/>
log is (potentially) printed every 5 minutes.
Every 5 minutes, the BN checks if, for a given subnet, the actual count
of peers is at least equal to a minimum one.
If not, this kind of log is printed.

When validators are connected and selected to be an aggregator in the
next epoch, the BN needs to subscribe and find new peers in the
corresponding attestation subnet.
If, right after the beacon is subscribed (but before it had time to find
peers), the "5 min ticker" ticks, then this warning log is displayed,
even if the slot for which the validator is selected as an aggregator is
still minutes away.

For this reason, this log is moved from WARN to DEBUG

**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.
2025-12-03 11:07:24 +00:00
james-prysm
cb28503b5f fixing e2e 2025-12-02 12:25:14 -06:00
kasey
61de11e2c4 Backfill data columns (#15580)
**What type of PR is this?**

Feature

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

Adds data column support to backfill.

**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: Kasey <kasey@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Preston Van Loon <preston@pvl.dev>
2025-12-02 15:19:32 +00:00
james-prysm
48fd9509ef fixing import issue after merge 2025-12-02 08:47:32 -06:00
james-prysm
316f0932ff Merge branch 'develop' into fulu-e2e 2025-12-02 06:36:55 -08:00
james-prysm
7f321a5835 Update changelog/james-prysm_fulu-e2e.md
Co-authored-by: Radosław Kapka <rkapka@wp.pl>
2025-12-02 08:22:24 -06:00
james-prysm
1b302219a8 Update runtime/interop/genesis.go
Co-authored-by: Radosław Kapka <rkapka@wp.pl>
2025-12-02 08:18:53 -06:00
james-prysm
e36c7ec26e Update testing/endtoend/components/eth1/transactions.go
Co-authored-by: Radosław Kapka <rkapka@wp.pl>
2025-12-02 08:18:45 -06:00
james-prysm
8df746cead Update testing/endtoend/components/eth1/transactions.go
Co-authored-by: Radosław Kapka <rkapka@wp.pl>
2025-12-02 08:18:34 -06:00
james-prysm
4294025eed Update ssz_proto_library.bzl
Co-authored-by: Preston Van Loon <pvanloon@offchainlabs.com>
2025-12-01 21:59:53 -08:00
Manu NALEPA
2773bdef89 Remove NUMBER_OF_COLUMNS and MAX_CELLS_IN_EXTENDED_MATRIX configuration. (#16073)
**What type of PR is this?**
Other

**What does this PR do? Why is it needed?**
This pull request removes `NUMBER_OF_COLUMNS` and
`MAX_CELLS_IN_EXTENDED_MATRIX` configuration.

**Other notes for review**
Please read commit by commit, with commit messages.

**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.
2025-11-29 09:30:54 +00:00
Manu NALEPA
2a23dc7f4a Improve logs (#16075)
**What type of PR is this?**
Other

**What does this PR do? Why is it needed?**
- Added log prefix to the `genesis` package.
- Added log prefix to the `params` package.
- `WithGenesisValidatorsRoot`: Use camelCase for log field param.
- Move `Origin checkpoint found in db` log from WARN to INFO, since it
is the expected behaviour.

**Other notes for review**
Please read commit by commit

**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.
2025-11-28 14:34:02 +00:00
james-prysm
f97622b054 e2e support electra forkstart (#16048)
<!-- 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?**

Allows for starting e2e tests from electra or a specific fork of
interest again. doesn't fix missing execution requests tests, nishant
reverted it.

**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: Radosław Kapka <rkapka@wp.pl>
2025-11-26 16:33:24 +00:00
Fibonacci747
08d0f42725 beacon-chain/das: remove dead slot parameter (#16021)
The slot parameter in blobCacheEntry.filter was unused and redundant.
All slot/epoch-sensitive checks happen before filter
(commitmentsToCheck), and disk availability is handled via
BlobStorageSummary (epoch-aware).

Changes:
- Drop slot from blobCacheEntry.filter signature.
- Update call sites in availability_blobs.go and blob_cache_test.go.

Mirrors the data_column_cache.filter API (which does not take slot),
reduces API noise, and removes dead code without changing behavior.
2025-11-26 16:04:05 +00:00
james-prysm
74c8a25354 adding semi-supernode feature (#16029)
<!-- 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?**

| Feature | Semi-Supernode | Supernode |
| ----------------------- | ------------------------- |
------------------------ |
| **Custody Groups** | 64 | 128 |
| **Data Columns** | 64 | 128 |
| **Storage** | ~50% | ~100% |
| **Blob Reconstruction** | Yes (via Reed-Solomon) | No reconstruction
needed |
| **Flag** | `--semi-supernode` | `--supernode` |
| **Can serve all blobs** | Yes (with reconstruction) | Yes (directly) |

**note** if your validator total effective balance results in more
custody than the semi-supernode it will override those those
requirements.

cgc=64 from @nalepae 
Pro:
- We are useful to the network
- Less disconnection likelihood
- Straight forward to implement

Con:
- We cannot revert to a full node
- We have to serve incoming RPC requests corresponding to 64 columns

Tested the following using this kurtosis setup

```
participants:
  # Super-nodes
  - el_type: geth
    el_image: ethpandaops/geth:master
    cl_type: prysm
    vc_image: gcr.io/offchainlabs/prysm/validator:latest
    cl_image: gcr.io/offchainlabs/prysm/beacon-chain:latest
    count: 2
    cl_extra_params:
      - --supernode
    vc_extra_params:
      - --verbosity=debug
  # Full-nodes
  - el_type: geth
    el_image: ethpandaops/geth:master
    cl_type: prysm
    vc_image: gcr.io/offchainlabs/prysm/validator:latest
    cl_image: gcr.io/offchainlabs/prysm/beacon-chain:latest
    count: 2
    validator_count: 1
    cl_extra_params:
      - --semi-supernode
    vc_extra_params:
      - --verbosity=debug

additional_services:
  - dora
  - spamoor

spamoor_params:
  image: ethpandaops/spamoor:master
  max_mem: 4000
  spammers:
    - scenario: eoatx
      config:
        throughput: 200
    - scenario: blobs
      config:
        throughput: 20

network_params:
  fulu_fork_epoch: 0
  withdrawal_type: "0x02"
  preset: mainnet

global_log_level: debug
```

```
curl -H "Accept: application/json" http://127.0.0.1:32961/eth/v1/node/identity
{"data":{"peer_id":"16Uiu2HAm7xzhnGwea8gkcxRSC6fzUkvryP6d9HdWNkoeTkj6RSqw","enr":"enr:-Ni4QIH5u2NQz17_pTe9DcCfUyG8TidDJJjIeBpJRRm4ACQzGBpCJdyUP9eGZzwwZ2HS1TnB9ACxFMQ5LP5njnMDLm-GAZqZEXjih2F0dG5ldHOIAAAAAAAwAACDY2djQIRldGgykLZy_whwAAA4__________-CaWSCdjSCaXCErBAAE4NuZmSEAAAAAIRxdWljgjLIiXNlY3AyNTZrMaECulJrXpSOBmCsQWcGYzQsst7r3-Owlc9iZbEcJTDkB6qIc3luY25ldHMFg3RjcIIyyIN1ZHCCLuA","p2p_addresses":["/ip4/172.16.0.19/tcp/13000/p2p/16Uiu2HAm7xzhnGwea8gkcxRSC6fzUkvryP6d9HdWNkoeTkj6RSqw","/ip4/172.16.0.19/udp/13000/quic-v1/p2p/16Uiu2HAm7xzhnGwea8gkcxRSC6fzUkvryP6d9HdWNkoeTkj6RSqw"],"discovery_addresses":["/ip4/172.16.0.19/udp/12000/p2p/16Uiu2HAm7xzhnGwea8gkcxRSC6fzUkvryP6d9HdWNkoeTkj6RSqw"],"metadata":{"seq_number":"3","attnets":"0x0000000000300000","syncnets":"0x05","custody_group_count":"64"}}}
```

```
curl -s http://127.0.0.1:32961/eth/v1/debug/beacon/data_column_sidecars/head | jq '.data | length'
64
```

```
curl -X 'GET' \
  'http://127.0.0.1:32961/eth/v1/beacon/blobs/head' \
  -H 'accept: application/json'
  ```

**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 <pvanloon@offchainlabs.com>
Co-authored-by: james-prysm <jhe@offchainlabs.com>
Co-authored-by: Manu NALEPA <enalepa@offchainlabs.com>
2025-11-26 15:31:15 +00:00
Potuz
a466c6db9c Fix linter (#16058)
Fix linter, otherwise #16049 fails the linter.
2025-11-26 15:03:26 +00:00
Preston Van Loon
4da6c4291f Fix metrics logging of http_error_count (#16055)
**What type of PR is this?**

Bug fix

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

I am seeing massive metrics cardinality on error cases. 

Example:

```
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682952",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682953",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682954",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682955",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682956",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682957",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682958",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682959",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682960",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682961",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682962",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682966",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682967",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682968",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682969",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682970",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682971",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682972",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682973",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682974",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682975",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682976",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682977",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682978",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682980",method="GET"} 2
http_error_count{code="Not Found",endpoint="/eth/v1/beacon/blob_sidecars/1682983",method="GET"} 2
```

Now it looks like this:

```
# TYPE http_error_count counter
http_error_count{code="Not Found",endpoint="beacon.GetBlockV2",method="GET"} 606
http_error_count{code="Not Found",endpoint="blob.Blobs",method="GET"} 4304
```

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

**Other notes for review**


Other uses of http metrics use the endpoint name rather than the request
URL.

**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.
2025-11-26 04:22:35 +00:00
james-prysm
41e336d373 Merge branch 'develop' into fulu-e2e 2025-11-25 12:43:24 -08:00
Forostovec
2d242a8d09 execution: avoid redundant WithHttpEndpoint when JWT is provided (#16032)
This change ensures FlagOptions in cmd/beacon-chain/execution/options.go
appends only one endpoint option depending on whether a JWT secret is
present. Previously the code always appended WithHttpEndpoint and then
conditionally appended WithHttpEndpointAndJWTSecret which overwrote the
first option, adding unnecessary allocations and cognitive overhead.
Since WithHttpEndpointAndJWTSecret fully configures the endpoint,
including URL and Bearer auth needed by the Engine API, the initial
WithHttpEndpoint is redundant when a JWT is supplied. The refactor
preserves behavior while simplifying option composition and avoiding
redundant state churn.

---------

Co-authored-by: james-prysm <90280386+james-prysm@users.noreply.github.com>
2025-11-25 17:43:38 +00:00
Radosław Kapka
6be1541e57 Initialize the ExecutionRequests field in gossip block map (#16047)
**What type of PR is this?**

Other

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

When unmarshaling a block with fastssz, if the target block's
`ExecutionRequests` field is nil, it will not get populated
```
if b.ExecutionRequests == nil {
	b.ExecutionRequests = new(v1.ExecutionRequests)
}
if err = b.ExecutionRequests.UnmarshalSSZ(buf); err != nil {
	return err
}
```
This is true for other fields and that's why we initialize them in our
gossip data maps. There is no bug at the moment because even if
execution requests are nil, we initialize them in
`consensus-types/blocks/proto.go`
```
er := pb.ExecutionRequests
if er == nil {
	er = &enginev1.ExecutionRequests{}
}
```
However, since we initialize other fields in the data map, it's safer to
do it for execution requests too, to avoid a bug in case the code in
`consensus-types/blocks/proto.go` changes in the future.

**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.
2025-11-25 16:17:44 +00:00
Bastin
b845222ce7 Integrate state-diff into HasState() (#16045)
**What type of PR is this?**
Feature

**What does this PR do? Why is it needed?**
This PR adds integrates state-diff into `HasState()`. 

One thing to note: we are assuming that, for a given block root, that
has either a state summary or a block in db, and also falls in the state
diff tree, then there must exist a state. This function could return
true, even when there is no actual state saved due to any error.
But this is fine, because we have that assumption throughout the whole
state diff feature.
2025-11-25 13:22:57 +00:00
terence
5bbdebee22 Add Gloas consensus type block package (#15618)
Co-authored-by: Bastin <43618253+Inspector-Butters@users.noreply.github.com>
2025-11-25 09:21:19 +00:00
Bastin
26100e074d Refactor slot by blockroot (#16040)
**Review after #16033 is merged**

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

**What does this PR do? Why is it needed?**
This PR refactors the code to find the corresponding slot of a given
block root using state summary or the block itself, into its own
function `SlotByBlockRoot(ctx, blockroot)`.

Note that there exists a function `slotByBlockRoot(ctx context.Context,
tx *bolt.Tx, blockRoot []byte)` immediately below the new function. Also
note that this function has two drawbacks, which led to creation of the
new function:
- the old function requires a boltdb tx, which is not necessarily
available to the caller.
- the old function does NOT make use of the state summary cache. 

edit: 
- the old function also uses the state bucket to retrieve the state and
it's slot. this is not something we want in the state diff feature,
since there is no state bucket.
2025-11-24 19:59:13 +00:00
satushh
768fa0e5a1 Revert "Metrics for eas" (#16043)
Reverts OffchainLabs/prysm#16008
2025-11-24 16:12:30 +00:00
Bastin
11bb8542a4 Integrate state-diff into State() (#16033)
**What type of PR is this?**
Feature

**What does this PR do? Why is it needed?**
This PR integrates the state diff path into the `State()` function from
`db/kv`, which allows reading of states using the state diff db, when
the `EnableStateDiff` flag is enabled.

**Notes for reviewers:**
Files `kv/state_diff_test.go` and `config/features/config.go` only
contain renamings:
- `kv/state_diff_test.go`: rename `setDefaultExponents()` to
`setDefaultStateDiffExponents()` to be less vague.
- `config/features/config.go`: rename `enableStateDiff` to
`EnableStateDiff` to make it public.
2025-11-24 12:42:43 +00:00
Bharath Vedartham
b78c2c354b add a metric to measure the attestation gossip validator (#15785)
This PR adds a Summary metric to measure the p2p topic validation time
for attestations arriving from the network. It times the
`validateCommitteeIndexBeaconAttestation` method.

The metric is called `gossip_attestation_verification_milliseconds`

**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: Kasey Kirkham <kasey@users.noreply.github.com>
2025-11-21 21:57:19 +00:00
Muzry
55e2001a0b Check the JWT secret length (#15939)
<!-- 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?**

Previously, JWT secrets longer than 256 bits could cause client
compatibility issues. For example, Prysm would accept longer secrets
while Geth strictly requires exactly 32 bytes, causing Geth startup
failures when using the same secret file.

This change enforces the Engine API specification requirement that JWT
secrets must be exactly 256 bits (32 bytes), ensuring consistent
behavior across different client implementations.

**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: james-prysm <90280386+james-prysm@users.noreply.github.com>
2025-11-21 21:51:03 +00:00
sashaodessa
c093283b1b Replace fixed sleep delays with active polling in prometheus service test (#15828)
## **Description:**

**What type of PR is this?**

> Bug fix

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

Replaces fixed `time.Sleep(time.Second)` delays in `TestLifecycle` with
active polling to wait for service readiness/shutdown. This improves
test reliability and reduces execution time by eliminating unnecessary
waits when services start/stop faster than expected.

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

N/A - Minor test improvement

**Other notes for review**

- Uses 50ms polling interval with 3s timeout for both startup and
shutdown checks
- Maintains same test logic while making it more efficient and less
flaky
- No functional changes to the service itself

**Acknowledgements**

- [x] 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).
- [x] I have added a description to this PR with sufficient context for
reviewers to understand this PR.
2025-11-21 21:39:24 +00:00
terence
5449fd0352 p2p: wire stategen into service for last finalized state (#16034)
This PR removes the last production usage for: `LastArchivedRoot` by

- extend the P2P config to accept a `StateGen` dependency and wire it up
from the beacon node
- update gossip scoring to read the active validator count via stategen
using last finalized block root

note: i think the correct implementation should process last finalizes
state to current slot, but that's a bigger change i dont want to make in
this PR, i just want to remove usages for `LastArchivedRoot`
2025-11-21 16:13:00 +00:00
Bastin
3d7f7b588b Fix state diff repetitive anchor slot bug (#16037)
**What type of PR is this?**
 Bug fix

**What does this PR do? Why is it needed?**
This PR fixes a bug in the state diff `getBaseAndDiffChain()` method. In
the process of finding the diff chain indices, there could be a scenario
where multiple levels return the same diff slot number not equal to the
base (full snapshot) slot number.
This resulted in multiple diff items being added to the diff chain for
the same slot, but on different levels, which resulted in errors reading
the diff.

Fix: we keep a `lastSeenAnchorSlot` equal to the `BaseAnchorSlot` and
update it every time we see a new anchor slot. We ignore if the current
found anchor slot is equal to the `lastSeenAnchorSlot`.

Scenario example: 
exponents: [20, 14, 10, 7, 5]
offset: 0
slots saved: slot 2^11, and slot (2^11 + 2^5)
slot to read: slot (2^11 + 2^5)
resulting list of anchor slots (diff chain indices): [0, 0, 2^11, 2^11,
2^11 + 2^5]
2025-11-21 15:42:27 +00:00
james-prysm
86cb003b8e removing unneeded change 2025-11-20 09:16:28 -06:00
terence
2f067c4164 Add supported / unsupported version for fork enum (#16030)
* gate unreleased forks

* Preston + Bastin's feedback

* Rename back to all versions

* Clean up, mark PR ready for review

* Changelog
2025-11-20 14:58:41 +00:00
james-prysm
853d64f8f9 fix gosec 2025-11-19 22:46:33 -06:00
james-prysm
6dd11aaa2d found better solution just removing the library, updated changelog and removed transition code 2025-11-19 22:26:46 -06:00
james-prysm
f1d4f6d136 updating third party gosec ignore 2025-11-19 19:40:15 -06:00
james-prysm
c5690a7eb6 Merge branch 'develop' into fulu-e2e 2025-11-19 16:17:12 -08:00
james-prysm
edf60e6e33 gaz 2025-11-19 18:16:53 -06:00
james-prysm
ad022eead9 resolving more build issues 2025-11-19 18:15:59 -06:00
james-prysm
409b48940d gaz 2025-11-19 16:53:06 -06:00
james-prysm
64324211b8 attempting to fix ci 2025-11-19 16:50:51 -06:00
Radosław Kapka
81266f60af Move BlockGossipReceived event to the end of gossip validation. (#16031)
* Move `BlockGossipReceived` event to the end of gossip validation.

* changelog <3

* tests
2025-11-19 22:34:02 +00:00
james-prysm
5531014a75 gaz 2025-11-19 16:33:54 -06:00
james-prysm
80dc3d953d trying to fix gobuild by moving fuzzyvm to third party build 2025-11-19 16:30:13 -06:00
james-prysm
d175bd93ba Merge branch 'develop' into fulu-e2e 2025-11-19 14:05:22 -08:00
james-prysm
4419a36cc2 fixing and updating go ethereum to 1.16.7 2025-11-19 16:04:55 -06:00
Bastin
207f36065a state-diff configs & kv functions (#15903)
* state-diff configs

* state diff kv functions

* potuz's comments

* Update config.go

* fix merge conflicts

* apply bazel's suggestion and fix some bugs

* preston's feedback
2025-11-19 19:27:56 +00:00
terence
eb9feabd6f stop emitting payload attribute events during late block handling (#16026)
* stop emitting payload attribute events during late block handling when we are not proposing the next slot

* Change the behavior to not even enter FCU if we are not proposing next slot
2025-11-19 16:51:46 +00:00
james-prysm
0176706fad Merge branch 'develop' into fulu-e2e 2025-11-19 08:42:42 -08:00
terence
bc0868e232 Add Gloas beacon state package (#15611)
* Add Gloas protobuf definitions with spec tests

Add Gloas state fields to beacon state implementation

* Remove shared field for pending payment

* Radek's feedback

* Potuz feedback

* use slice concat

* Fix comment

* Fix concat

* Fix comment

* Fix correct index
2025-11-18 15:08:31 +00:00
james-prysm
df307544c9 Merge branch 'develop' into fulu-e2e 2025-11-17 13:31:24 -08:00
Chris Berry
35c1ab5e88 Downgrade log level for all validator indices. (#15998)
* Update logging behaviour for updated fee recipient.

* Updated changelog.

* Display validator indices only on TRACE

* Fix tests

---------

Co-authored-by: Manu NALEPA <enalepa@offchainlabs.com>
2025-11-17 16:44:03 +00:00
Radosław Kapka
21bb6f5258 Remove validator cross client from e2e (#16025) 2025-11-17 15:02:06 +00:00
james-prysm
10821dc451 Merge branch 'develop' into fulu-e2e 2025-10-07 14:54:07 -05:00
james-prysm
4706b758dd Merge branch 'develop' into fulu-e2e 2025-10-07 14:48:06 -05:00
james-prysm
9f59b78cec updating dependency 2025-10-07 11:43:46 -05:00
james-prysm
75d8a2bdb6 Merge branch 'develop' into fulu-e2e 2025-10-07 11:35:33 -05:00
james-prysm
ca4ae35f6e fixing comment 2025-09-26 10:50:42 -05:00
james-prysm
3245b884cf Merge branch 'develop' into fulu-e2e 2025-09-26 10:41:31 -05:00
james-prysm
00e66ccab6 more review feedback 2025-09-26 10:40:39 -05:00
james-prysm
ed1885be8a fixing log 2025-09-26 10:35:34 -05:00
james-prysm
fad101c4a0 Update testing/endtoend/components/eth1/transactions.go
Co-authored-by: Manu NALEPA <enalepa@offchainlabs.com>
2025-09-26 10:32:45 -05:00
james-prysm
1c0107812b some review 2025-09-26 09:55:03 -05:00
james-prysm
fd710886c8 Merge branch 'develop' into fulu-e2e 2025-09-26 08:34:36 -05:00
james-prysm
20d6bacfe9 Update testing/endtoend/components/eth1/transactions.go
Co-authored-by: Manu NALEPA <enalepa@offchainlabs.com>
2025-09-26 08:30:11 -05:00
james-prysm
1140141aaa Merge branch 'develop' into fulu-e2e 2025-09-25 13:26:53 -05:00
james-prysm
7402efad2d fixing even more linting 2025-09-25 11:56:26 -05:00
james-prysm
f6cd1d9f7f more linting 2025-09-25 11:43:59 -05:00
james-prysm
a4751b057d even more linting issues 2025-09-25 11:29:21 -05:00
james-prysm
eb5e2a5094 Merge branch 'develop' into fulu-e2e 2025-09-25 10:59:50 -05:00
james-prysm
626830ff9c more linting fixes 2025-09-25 10:59:30 -05:00
james-prysm
6f20ac57d4 attempting to fix linting 2025-09-25 10:07:54 -05:00
james-prysm
cf4095cf3b fixing some linting 2025-09-25 09:19:35 -05:00
james-prysm
39b3dba946 Merge branch 'develop' into fulu-e2e 2025-09-25 08:50:34 -05:00
james-prysm
fa7e0b6e50 Merge branch 'develop' into fulu-e2e 2025-09-24 14:43:28 -05:00
james-prysm
4aed113dea Merge branch 'develop' into fulu-e2e 2025-09-24 13:51:01 -05:00
james-prysm
20ffd4c523 fixing linting 2025-09-24 13:50:51 -05:00
james-prysm
1cd3c3cc2d upgrading size to enormous 2025-09-24 12:55:42 -05:00
james-prysm
15e2e6b85e trying to fix remaining e2e issues 2025-09-24 12:54:36 -05:00
james-prysm
203a098076 adding go ethereum update to changelog 2025-09-23 16:45:35 -05:00
james-prysm
9e5b7b00f3 Merge branch 'develop' into fulu-e2e 2025-09-23 16:30:32 -05:00
james-prysm
1fcf3c7f30 adding new e2e type for build so that we can control the end to end tests 2025-09-23 16:30:11 -05:00
james-prysm
ccf81ed33c fixing test 2025-09-23 14:45:11 -05:00
james-prysm
382934fd30 Merge branch 'develop' into fulu-e2e 2025-09-23 12:56:58 -05:00
james-prysm
c31f0435c6 adjusting transations to avoid execution client issue 2025-09-23 11:06:10 -05:00
james-prysm
59dc0263f2 Merge branch 'develop' into fulu-e2e 2025-09-22 10:20:53 -05:00
james-prysm
de7ea1f72c Merge branch 'develop' into fulu-e2e 2025-09-19 16:37:29 -05:00
james-prysm
2fc7a5af82 Merge branch 'develop' into fulu-e2e 2025-09-19 10:55:29 -05:00
james-prysm
76d137198b Merge branch 'develop' into fulu-e2e 2025-09-18 15:32:51 -05:00
james-prysm
05d8ce15af Merge branch 'develop' into fulu-e2e 2025-09-17 14:07:05 -05:00
james-prysm
2715cd3fc7 Merge branch 'develop' into fulu-e2e 2025-09-11 14:35:58 -05:00
james-prysm
7aa79a420c Merge branch 'develop' into fulu-e2e 2025-09-08 15:43:41 -05:00
james-prysm
b3dc7e4afb changelog 2025-09-08 15:26:44 -05:00
james-prysm
db794db4ee gofmt 2025-09-08 15:22:13 -05:00
james-prysm
f9bc42eed4 go mod tidy 2025-09-08 15:16:00 -05:00
james-prysm
f97082df32 fixing missing fulu from evaluators 2025-09-08 09:14:48 -05:00
james-prysm
0d55b61b3d adding fulu support for validator 2025-09-05 14:10:00 -05:00
Preston Van Loon
d487e5c109 Fixed some build issues for gnark. There may be more but Prysm builds. 2025-09-05 12:10:55 -05:00
james-prysm
7ffbf77b87 updating dependency wip 2025-09-04 15:15:58 -05:00
james-prysm
f103267f10 updating genesis file 2025-09-03 23:29:45 -05:00
james-prysm
6f0ffa2a20 updating goethereum to temp use fusaka devnet 3 and updating genesis helpers 2025-09-03 14:11:15 -05:00
james-prysm
bcb5add346 updating goethereum version 2025-09-03 11:35:36 -05:00
james-prysm
a43fc50015 wip 2025-08-27 13:56:31 -05:00
308 changed files with 19605 additions and 2074 deletions

View File

@@ -34,4 +34,5 @@ Fixes #
- [ ] 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.
- [ ] 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).

View File

@@ -193,6 +193,7 @@ nogo(
"//tools/analyzers/featureconfig:go_default_library",
"//tools/analyzers/gocognit:go_default_library",
"//tools/analyzers/ineffassign:go_default_library",
"//tools/analyzers/httperror:go_default_library",
"//tools/analyzers/interfacechecker:go_default_library",
"//tools/analyzers/logcapitalization:go_default_library",
"//tools/analyzers/logruswitherror:go_default_library",

View File

@@ -4,6 +4,91 @@ All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
## [v7.1.0](https://github.com/prysmaticlabs/prysm/compare/v7.0.0...v7.1.0) - 2025-12-10
This release includes several key features/fixes. If you are running v7.0.0 then you should update to v7.0.1 or later and remove the flag `--disable-last-epoch-targets`.
Release highlights:
- Backfill is now supported in Fulu. Backfill from checkpoint sync now supports data columns. Run with `--enable-backfill` when using checkpoint sync.
- A new node configuration to custody enough data columns to reconstruct blobs. Use flag `--semi-supernode` to custody at least 50% of the data columns.
- Critical fixes in attestation processing.
A post mortem doc with full details on the mainnet attestation processing issue from December 4th is expected in the coming days.
### Added
- add fulu support to light client processing. [[PR]](https://github.com/prysmaticlabs/prysm/pull/15995)
- Record data column gossip KZG batch verification latency in both the pooled worker and fallback paths so the `beacon_kzg_verification_data_column_batch_milliseconds` histogram reflects gossip traffic, annotated with `path` labels to distinguish the sources. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16018)
- Implement Gloas state. [[PR]](https://github.com/prysmaticlabs/prysm/pull/15611)
- Add initial configs for the state-diff feature. [[PR]](https://github.com/prysmaticlabs/prysm/pull/15903)
- Add kv functions for the state-diff feature. [[PR]](https://github.com/prysmaticlabs/prysm/pull/15903)
- Add supported version for fork versions. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16030)
- prometheus metric `gossip_attestation_verification_milliseconds` to track attestation gossip topic validation latency. [[PR]](https://github.com/prysmaticlabs/prysm/pull/15785)
- Integrate state-diff into `State()`. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16033)
- Implement Gloas fork support in consensus-types/blocks with factory methods, getters, setters, and proto handling. [[PR]](https://github.com/prysmaticlabs/prysm/pull/15618)
- Integrate state-diff into `HasState()`. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16045)
- Added `--semi-supernode` flag to custody half of a super node's datacolumn requirements but allowing for reconstruction for blob retrieval. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16029)
- Data column backfill. [[PR]](https://github.com/prysmaticlabs/prysm/pull/15580)
- Backfill metrics for columns: backfill_data_column_sidecar_downloaded, backfill_data_column_sidecar_downloaded_bytes, backfill_batch_columns_download_ms, backfill_batch_columns_verify_ms. [[PR]](https://github.com/prysmaticlabs/prysm/pull/15580)
- prometheus summary `gossip_data_column_sidecar_arrival_milliseconds` to track data column sidecar arrival latency since slot start. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16099)
### Changed
- Improve readability in slashing import and remove duplicated code. [[PR]](https://github.com/prysmaticlabs/prysm/pull/15957)
- Use dependent root instead of target when possible. [[PR]](https://github.com/prysmaticlabs/prysm/pull/15996)
- Changed `--subscribe-all-data-subnets` flag to `--supernode` and aliased `--subscribe-all-data-subnets` for existing users. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16012)
- Use explicit slot component timing configs. [[PR]](https://github.com/prysmaticlabs/prysm/pull/15999)
- Downgraded log level from INFO to DEBUG on PrepareBeaconProposer updated fee recipients. [[PR]](https://github.com/prysmaticlabs/prysm/pull/15998)
- Change the logging behaviour of Updated fee recipients to only log count of validators at Debug level and all validator indices at Trace level. [[PR]](https://github.com/prysmaticlabs/prysm/pull/15998)
- Stop emitting payload attribute events during late block handling when we are not proposing the next slot. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16026)
- Initialize the `ExecutionRequests` field in gossip block map. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16047)
- Avoid redundant WithHttpEndpoint when JWT is provided. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16032)
- Removed dead slot parameter from blobCacheEntry.filter. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16021)
- Added log prefix to the `genesis` package. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16075)
- Added log prefix to the `params` package. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16075)
- `WithGenesisValidatorsRoot`: Use camelCase for log field param. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16075)
- Move `Origin checkpoint found in db` from WARN to INFO, since it is the expected behaviour. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16075)
- backfill metrics that changed name and/or histogram buckets: backfill_batch_time_verify -> backfill_batch_verify_ms, backfill_batch_time_waiting -> backfill_batch_waiting_ms, backfill_batch_time_roundtrip -> backfill_batch_roundtrip_ms, backfill_blocks_bytes_downloaded -> backfill_blocks_downloaded_bytes, backfill_batch_time_verify -> backfill_batch_verify_ms, backfill_batch_blocks_time_download -> backfill_batch_blocks_download_ms, backfill_batch_blobs_time_download -> backfill_batch_blobs_download_ms, backfill_blobs_bytes_downloaded -> backfill_blocks_downloaded_bytes. [[PR]](https://github.com/prysmaticlabs/prysm/pull/15580)
- Move the "Not enough connected peers" (for a given subnet) from WARN to DEBUG. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16087)
- `blobsDataFromStoredDataColumns`: Ask the use to use the `--supernode` flag and shorten the error mesage. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16097)
- Introduced flag `--ignore-unviable-attestations` (replaces and deprecates `--disable-last-epoch-targets`) to drop attestations whose target state is not viable; default remains to process them unless explicitly enabled. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16094)
### Removed
- Remove validator cross-client from end-to-end tests. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16025)
- `NUMBER_OF_COLUMNS` configuration (not in the specification any more, replaced by a preset). [[PR]](https://github.com/prysmaticlabs/prysm/pull/16073)
- `MAX_CELLS_IN_EXTENDED_MATRIX` configuration (not in the specification any more). [[PR]](https://github.com/prysmaticlabs/prysm/pull/16073)
### Fixed
- Nil check for block if it doesn't exist in the DB in fetchOriginSidecars. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16006)
- Fix proposals progress bar count [#16020](https://github.com/OffchainLabs/prysm/pull/16020). [[PR]](https://github.com/prysmaticlabs/prysm/pull/16020)
- Move `BlockGossipReceived` event to the end of gossip validation. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16031)
- Fix state diff repetitive anchor slot bug. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16037)
- Check the JWT secret length is exactly 256 bits (32 bytes) as per Engine API specification. [[PR]](https://github.com/prysmaticlabs/prysm/pull/15939)
- http_error_count now matches the other cases by listing the endpoint name rather than the actual URL requested. This improves metrics cardinality. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16055)
- Fix array out of bounds in static analyzer. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16058)
- fixes E2E tests to be able to start from Electra genesis fork or future forks. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16048)
- Use head state to validate attestations for old blocks if they are compatible. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16095)
## [v7.0.1](https://github.com/prysmaticlabs/prysm/compare/v7.0.0...v7.0.1) - 2025-12-08
This patch release contains 4 cherry-picked changes to address the mainnet attestation processing issue from 2025-12-04. Operators are encouraged to update to this release as soon as practical. As of this release, the feature flag `--disable-last-epoch-targets` has been deprecated and can be safely removed from your node configuration.
A post mortem doc with full details is expected to be published later this week.
### Changed
- Move the "Not enough connected peers" (for a given subnet) from WARN to DEBUG. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16087)
- Use dependent root instead of target when possible. [[PR]](https://github.com/prysmaticlabs/prysm/pull/15996)
- Introduced flag `--ignore-unviable-attestations` (replaces and deprecates `--disable-last-epoch-targets`) to drop attestations whose target state is not viable; default remains to process them unless explicitly enabled. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16094)
### Fixed
- Use head state to validate attestations for old blocks if they are compatible. [[PR]](https://github.com/prysmaticlabs/prysm/pull/16095)
## [v7.0.0](https://github.com/prysmaticlabs/prysm/compare/v6.1.4...v7.0.0) - 2025-11-10
This is our initial mainnet release for the Ethereum mainnet Fulu fork on December 3rd, 2025. All operators MUST update to v7.0.0 or later release prior to the fulu fork epoch `411392`. See the [Ethereum Foundation blog post](https://blog.ethereum.org/2025/11/06/fusaka-mainnet-announcement) for more information on Fulu.

View File

@@ -22,10 +22,7 @@ import (
// The caller of this function must have a lock on forkchoice.
func (s *Service) getRecentPreState(ctx context.Context, c *ethpb.Checkpoint) state.ReadOnlyBeaconState {
headEpoch := slots.ToEpoch(s.HeadSlot())
if c.Epoch < headEpoch {
return nil
}
if !s.cfg.ForkChoiceStore.IsCanonical([32]byte(c.Root)) {
if c.Epoch < headEpoch || c.Epoch == 0 {
return nil
}
// Only use head state if the head state is compatible with the target checkpoint.
@@ -33,11 +30,11 @@ func (s *Service) getRecentPreState(ctx context.Context, c *ethpb.Checkpoint) st
if err != nil {
return nil
}
headDependent, err := s.cfg.ForkChoiceStore.DependentRootForEpoch([32]byte(headRoot), c.Epoch)
headDependent, err := s.cfg.ForkChoiceStore.DependentRootForEpoch([32]byte(headRoot), c.Epoch-1)
if err != nil {
return nil
}
targetDependent, err := s.cfg.ForkChoiceStore.DependentRootForEpoch([32]byte(c.Root), c.Epoch)
targetDependent, err := s.cfg.ForkChoiceStore.DependentRootForEpoch([32]byte(c.Root), c.Epoch-1)
if err != nil {
return nil
}
@@ -53,7 +50,11 @@ func (s *Service) getRecentPreState(ctx context.Context, c *ethpb.Checkpoint) st
}
return st
}
// Otherwise we need to advance the head state to the start of the target epoch.
// At this point we can only have c.Epoch > headEpoch.
if !s.cfg.ForkChoiceStore.IsCanonical([32]byte(c.Root)) {
return nil
}
// Advance the head state to the start of the target epoch.
// This point can only be reached if c.Root == headRoot and c.Epoch > headEpoch.
slot, err := slots.EpochStart(c.Epoch)
if err != nil {

View File

@@ -181,6 +181,123 @@ func TestService_GetRecentPreState(t *testing.T) {
require.NotNil(t, service.getRecentPreState(ctx, &ethpb.Checkpoint{Epoch: 1, Root: ckRoot}))
}
func TestService_GetRecentPreState_Epoch_0(t *testing.T) {
service, _ := minimalTestService(t)
ctx := t.Context()
require.IsNil(t, service.getRecentPreState(ctx, &ethpb.Checkpoint{}))
}
func TestService_GetRecentPreState_Old_Checkpoint(t *testing.T) {
service, _ := minimalTestService(t)
ctx := t.Context()
s, err := util.NewBeaconState()
require.NoError(t, err)
ckRoot := bytesutil.PadTo([]byte{'A'}, fieldparams.RootLength)
cp0 := &ethpb.Checkpoint{Epoch: 0, Root: ckRoot}
err = s.SetFinalizedCheckpoint(cp0)
require.NoError(t, err)
st, root, err := prepareForkchoiceState(ctx, 33, [32]byte(ckRoot), [32]byte{}, [32]byte{'R'}, cp0, cp0)
require.NoError(t, err)
require.NoError(t, service.cfg.ForkChoiceStore.InsertNode(ctx, st, root))
service.head = &head{
root: [32]byte(ckRoot),
state: s,
slot: 33,
}
require.IsNil(t, service.getRecentPreState(ctx, &ethpb.Checkpoint{}))
}
func TestService_GetRecentPreState_Same_DependentRoots(t *testing.T) {
service, _ := minimalTestService(t)
ctx := t.Context()
s, err := util.NewBeaconState()
require.NoError(t, err)
ckRoot := bytesutil.PadTo([]byte{'A'}, fieldparams.RootLength)
cp0 := &ethpb.Checkpoint{Epoch: 0, Root: ckRoot}
// Create a fork 31 <-- 32 <--- 64
// \---------33
// With the same dependent root at epoch 0 for a checkpoint at epoch 2
st, blk, err := prepareForkchoiceState(ctx, 31, [32]byte(ckRoot), [32]byte{}, [32]byte{}, cp0, cp0)
require.NoError(t, err)
require.NoError(t, service.cfg.ForkChoiceStore.InsertNode(ctx, st, blk))
st, blk, err = prepareForkchoiceState(ctx, 32, [32]byte{'S'}, blk.Root(), [32]byte{}, cp0, cp0)
require.NoError(t, err)
require.NoError(t, service.cfg.ForkChoiceStore.InsertNode(ctx, st, blk))
st, blk, err = prepareForkchoiceState(ctx, 64, [32]byte{'T'}, blk.Root(), [32]byte{}, cp0, cp0)
require.NoError(t, err)
require.NoError(t, service.cfg.ForkChoiceStore.InsertNode(ctx, st, blk))
st, blk, err = prepareForkchoiceState(ctx, 33, [32]byte{'U'}, [32]byte(ckRoot), [32]byte{}, cp0, cp0)
require.NoError(t, err)
require.NoError(t, service.cfg.ForkChoiceStore.InsertNode(ctx, st, blk))
cpRoot := blk.Root()
service.head = &head{
root: [32]byte{'T'},
state: s,
slot: 64,
}
require.NotNil(t, service.getRecentPreState(ctx, &ethpb.Checkpoint{Epoch: 2, Root: cpRoot[:]}))
}
func TestService_GetRecentPreState_Different_DependentRoots(t *testing.T) {
service, _ := minimalTestService(t)
ctx := t.Context()
s, err := util.NewBeaconState()
require.NoError(t, err)
ckRoot := bytesutil.PadTo([]byte{'A'}, fieldparams.RootLength)
cp0 := &ethpb.Checkpoint{Epoch: 0, Root: ckRoot}
// Create a fork 30 <-- 31 <-- 32 <--- 64
// \---------33
// With the same dependent root at epoch 0 for a checkpoint at epoch 2
st, blk, err := prepareForkchoiceState(ctx, 30, [32]byte(ckRoot), [32]byte{}, [32]byte{}, cp0, cp0)
require.NoError(t, err)
require.NoError(t, service.cfg.ForkChoiceStore.InsertNode(ctx, st, blk))
st, blk, err = prepareForkchoiceState(ctx, 31, [32]byte{'S'}, blk.Root(), [32]byte{}, cp0, cp0)
require.NoError(t, err)
require.NoError(t, service.cfg.ForkChoiceStore.InsertNode(ctx, st, blk))
st, blk, err = prepareForkchoiceState(ctx, 32, [32]byte{'T'}, blk.Root(), [32]byte{}, cp0, cp0)
require.NoError(t, err)
require.NoError(t, service.cfg.ForkChoiceStore.InsertNode(ctx, st, blk))
st, blk, err = prepareForkchoiceState(ctx, 64, [32]byte{'U'}, blk.Root(), [32]byte{}, cp0, cp0)
require.NoError(t, err)
require.NoError(t, service.cfg.ForkChoiceStore.InsertNode(ctx, st, blk))
st, blk, err = prepareForkchoiceState(ctx, 33, [32]byte{'V'}, [32]byte(ckRoot), [32]byte{}, cp0, cp0)
require.NoError(t, err)
require.NoError(t, service.cfg.ForkChoiceStore.InsertNode(ctx, st, blk))
cpRoot := blk.Root()
service.head = &head{
root: [32]byte{'T'},
state: s,
slot: 64,
}
require.IsNil(t, service.getRecentPreState(ctx, &ethpb.Checkpoint{Epoch: 2, Root: cpRoot[:]}))
}
func TestService_GetRecentPreState_Different(t *testing.T) {
service, _ := minimalTestService(t)
ctx := t.Context()
s, err := util.NewBeaconState()
require.NoError(t, err)
ckRoot := bytesutil.PadTo([]byte{'A'}, fieldparams.RootLength)
cp0 := &ethpb.Checkpoint{Epoch: 0, Root: ckRoot}
err = s.SetFinalizedCheckpoint(cp0)
require.NoError(t, err)
st, root, err := prepareForkchoiceState(ctx, 33, [32]byte(ckRoot), [32]byte{}, [32]byte{'R'}, cp0, cp0)
require.NoError(t, err)
require.NoError(t, service.cfg.ForkChoiceStore.InsertNode(ctx, st, root))
service.head = &head{
root: [32]byte(ckRoot),
state: s,
slot: 33,
}
require.IsNil(t, service.getRecentPreState(ctx, &ethpb.Checkpoint{}))
}
func TestService_GetAttPreState_Concurrency(t *testing.T) {
service, _ := minimalTestService(t)
ctx := t.Context()

View File

@@ -134,7 +134,7 @@ func getStateVersionAndPayload(st state.BeaconState) (int, interfaces.ExecutionD
return preStateVersion, preStateHeader, nil
}
func (s *Service) onBlockBatch(ctx context.Context, blks []consensusblocks.ROBlock, avs das.AvailabilityStore) error {
func (s *Service) onBlockBatch(ctx context.Context, blks []consensusblocks.ROBlock, avs das.AvailabilityChecker) error {
ctx, span := trace.StartSpan(ctx, "blockChain.onBlockBatch")
defer span.End()
@@ -306,7 +306,7 @@ func (s *Service) onBlockBatch(ctx context.Context, blks []consensusblocks.ROBlo
return s.saveHeadNoDB(ctx, lastB, lastBR, preState, !isValidPayload)
}
func (s *Service) areSidecarsAvailable(ctx context.Context, avs das.AvailabilityStore, roBlock consensusblocks.ROBlock) error {
func (s *Service) areSidecarsAvailable(ctx context.Context, avs das.AvailabilityChecker, roBlock consensusblocks.ROBlock) error {
blockVersion := roBlock.Version()
block := roBlock.Block()
slot := block.Slot()
@@ -634,9 +634,7 @@ func missingDataColumnIndices(store *filesystem.DataColumnStorage, root [fieldpa
return nil, nil
}
numberOfColumns := params.BeaconConfig().NumberOfColumns
if uint64(len(expected)) > numberOfColumns {
if len(expected) > fieldparams.NumberOfColumns {
return nil, errMaxDataColumnsExceeded
}
@@ -818,10 +816,9 @@ func (s *Service) areDataColumnsAvailable(
case <-ctx.Done():
var missingIndices any = "all"
numberOfColumns := params.BeaconConfig().NumberOfColumns
missingIndicesCount := uint64(len(missing))
missingIndicesCount := len(missing)
if missingIndicesCount < numberOfColumns {
if missingIndicesCount < fieldparams.NumberOfColumns {
missingIndices = helpers.SortedPrettySliceFromMap(missing)
}
@@ -948,13 +945,6 @@ func (s *Service) lateBlockTasks(ctx context.Context) {
attribute := s.getPayloadAttribute(ctx, headState, s.CurrentSlot()+1, headRoot[:])
// return early if we are not proposing next slot
if attribute.IsEmpty() {
headBlock, err := s.headBlock()
if err != nil {
log.WithError(err).WithField("head_root", headRoot).Error("Unable to retrieve head block to fire payload attributes event")
}
// notifyForkchoiceUpdate fires the payload attribute event. But in this case, we won't
// call notifyForkchoiceUpdate, so the event is fired here.
go s.firePayloadAttributesEvent(s.cfg.StateNotifier.StateFeed(), headBlock, headRoot, s.CurrentSlot()+1)
return
}

View File

@@ -2495,7 +2495,8 @@ func TestMissingBlobIndices(t *testing.T) {
}
func TestMissingDataColumnIndices(t *testing.T) {
countPlusOne := params.BeaconConfig().NumberOfColumns + 1
const countPlusOne = fieldparams.NumberOfColumns + 1
tooManyColumns := make(map[uint64]bool, countPlusOne)
for i := range countPlusOne {
tooManyColumns[uint64(i)] = true
@@ -2805,6 +2806,10 @@ func TestProcessLightClientUpdate(t *testing.T) {
require.NoError(t, s.cfg.BeaconDB.SaveHeadBlockRoot(ctx, [32]byte{1, 2}))
for _, testVersion := range version.All()[1:] {
if testVersion == version.Gloas {
// TODO(16027): Unskip light client tests for Gloas
continue
}
t.Run(version.String(testVersion), func(t *testing.T) {
l := util.NewTestLightClient(t, testVersion)

View File

@@ -39,8 +39,8 @@ var epochsSinceFinalityExpandCache = primitives.Epoch(4)
// BlockReceiver interface defines the methods of chain service for receiving and processing new blocks.
type BlockReceiver interface {
ReceiveBlock(ctx context.Context, block interfaces.ReadOnlySignedBeaconBlock, blockRoot [32]byte, avs das.AvailabilityStore) error
ReceiveBlockBatch(ctx context.Context, blocks []blocks.ROBlock, avs das.AvailabilityStore) error
ReceiveBlock(ctx context.Context, block interfaces.ReadOnlySignedBeaconBlock, blockRoot [32]byte, avs das.AvailabilityChecker) error
ReceiveBlockBatch(ctx context.Context, blocks []blocks.ROBlock, avs das.AvailabilityChecker) error
HasBlock(ctx context.Context, root [32]byte) bool
RecentBlockSlot(root [32]byte) (primitives.Slot, error)
BlockBeingSynced([32]byte) bool
@@ -69,7 +69,7 @@ type SlashingReceiver interface {
// 1. Validate block, apply state transition and update checkpoints
// 2. Apply fork choice to the processed block
// 3. Save latest head info
func (s *Service) ReceiveBlock(ctx context.Context, block interfaces.ReadOnlySignedBeaconBlock, blockRoot [32]byte, avs das.AvailabilityStore) error {
func (s *Service) ReceiveBlock(ctx context.Context, block interfaces.ReadOnlySignedBeaconBlock, blockRoot [32]byte, avs das.AvailabilityChecker) error {
ctx, span := trace.StartSpan(ctx, "blockChain.ReceiveBlock")
defer span.End()
// Return early if the block is blacklisted
@@ -242,7 +242,7 @@ func (s *Service) validateExecutionAndConsensus(
return postState, isValidPayload, nil
}
func (s *Service) handleDA(ctx context.Context, avs das.AvailabilityStore, block blocks.ROBlock) (time.Duration, error) {
func (s *Service) handleDA(ctx context.Context, avs das.AvailabilityChecker, block blocks.ROBlock) (time.Duration, error) {
var err error
start := time.Now()
if avs != nil {
@@ -332,7 +332,7 @@ func (s *Service) executePostFinalizationTasks(ctx context.Context, finalizedSta
// ReceiveBlockBatch processes the whole block batch at once, assuming the block batch is linear ,transitioning
// the state, performing batch verification of all collected signatures and then performing the appropriate
// actions for a block post-transition.
func (s *Service) ReceiveBlockBatch(ctx context.Context, blocks []blocks.ROBlock, avs das.AvailabilityStore) error {
func (s *Service) ReceiveBlockBatch(ctx context.Context, blocks []blocks.ROBlock, avs das.AvailabilityChecker) error {
ctx, span := trace.StartSpan(ctx, "blockChain.ReceiveBlockBatch")
defer span.End()

View File

@@ -14,6 +14,7 @@ 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"
@@ -470,30 +471,35 @@ func (s *Service) removeStartupState() {
// 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) {
isSubscribedToAllDataSubnets := flags.Get().SubscribeAllDataSubnets
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.
wasSubscribedToAllDataSubnets, err := s.cfg.BeaconDB.UpdateSubscribedToAllDataSubnets(s.ctx, isSubscribedToAllDataSubnets)
wasSupernode, err := s.cfg.BeaconDB.UpdateSubscribedToAllDataSubnets(s.ctx, isSupernode)
if err != nil {
log.WithError(err).Error("Could not update subscription status to all data subnets")
return 0, 0, errors.Wrap(err, "update subscribed to all data subnets")
}
// Warn the user if the node was previously subscribed to all data subnets and is not any more.
if wasSubscribedToAllDataSubnets && !isSubscribedToAllDataSubnets {
log.Warnf(
"Because the flag `--%s` was previously used, the node will still subscribe to all data subnets.",
flags.SubscribeAllDataSubnets.Name,
)
// 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
}
// Compute the custody group count.
custodyGroupCount := custodyRequirement
if isSubscribedToAllDataSubnets {
custodyGroupCount = 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.
@@ -510,12 +516,30 @@ func (s *Service) updateCustodyInfoInDB(slot primitives.Slot) (primitives.Slot,
}
}
earliestAvailableSlot, custodyGroupCount, err := s.cfg.BeaconDB.UpdateCustodyInfo(s.ctx, slot, custodyGroupCount)
earliestAvailableSlot, actualCustodyGroupCount, err := s.cfg.BeaconDB.UpdateCustodyInfo(s.ctx, slot, targetCustodyGroupCount)
if err != nil {
return 0, 0, errors.Wrap(err, "update custody info")
}
return earliestAvailableSlot, custodyGroupCount, nil
log.WithFields(logrus.Fields{
"earliestAvailableSlot": earliestAvailableSlot,
"custodyGroupCount": actualCustodyGroupCount,
"inputSlot": slot,
"targetCustodyGroups": targetCustodyGroupCount,
}).Info("Updated custody info in database")
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) {

View File

@@ -603,7 +603,6 @@ func TestUpdateCustodyInfoInDB(t *testing.T) {
custodyRequirement = uint64(4)
earliestStoredSlot = primitives.Slot(12)
numberOfCustodyGroups = uint64(64)
numberOfColumns = uint64(128)
)
params.SetupTestConfigCleanup(t)
@@ -611,7 +610,6 @@ func TestUpdateCustodyInfoInDB(t *testing.T) {
cfg.FuluForkEpoch = fuluForkEpoch
cfg.CustodyRequirement = custodyRequirement
cfg.NumberOfCustodyGroups = numberOfCustodyGroups
cfg.NumberOfColumns = numberOfColumns
params.OverrideBeaconConfig(cfg)
ctx := t.Context()
@@ -642,7 +640,7 @@ func TestUpdateCustodyInfoInDB(t *testing.T) {
resetFlags := flags.Get()
gFlags := new(flags.GlobalFlags)
gFlags.SubscribeAllDataSubnets = true
gFlags.Supernode = true
flags.Init(gFlags)
defer flags.Init(resetFlags)
@@ -680,7 +678,7 @@ func TestUpdateCustodyInfoInDB(t *testing.T) {
// ----------
resetFlags := flags.Get()
gFlags := new(flags.GlobalFlags)
gFlags.SubscribeAllDataSubnets = true
gFlags.Supernode = true
flags.Init(gFlags)
defer flags.Init(resetFlags)
@@ -695,4 +693,121 @@ func TestUpdateCustodyInfoInDB(t *testing.T) {
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

@@ -275,7 +275,7 @@ func (s *ChainService) ReceiveBlockInitialSync(ctx context.Context, block interf
}
// ReceiveBlockBatch processes blocks in batches from initial-sync.
func (s *ChainService) ReceiveBlockBatch(ctx context.Context, blks []blocks.ROBlock, _ das.AvailabilityStore) error {
func (s *ChainService) ReceiveBlockBatch(ctx context.Context, blks []blocks.ROBlock, _ das.AvailabilityChecker) error {
if s.State == nil {
return ErrNilState
}
@@ -305,7 +305,7 @@ func (s *ChainService) ReceiveBlockBatch(ctx context.Context, blks []blocks.ROBl
}
// ReceiveBlock mocks ReceiveBlock method in chain service.
func (s *ChainService) ReceiveBlock(ctx context.Context, block interfaces.ReadOnlySignedBeaconBlock, _ [32]byte, _ das.AvailabilityStore) error {
func (s *ChainService) ReceiveBlock(ctx context.Context, block interfaces.ReadOnlySignedBeaconBlock, _ [32]byte, _ das.AvailabilityChecker) error {
if s.ReceiveBlockMockErr != nil {
return s.ReceiveBlockMockErr
}

View File

@@ -60,7 +60,7 @@ func Eth1DataHasEnoughSupport(beaconState state.ReadOnlyBeaconState, data *ethpb
voteCount := uint64(0)
for _, vote := range beaconState.Eth1DataVotes() {
if AreEth1DataEqual(vote, data.Copy()) {
if AreEth1DataEqual(vote, data) {
voteCount++
}
}

View File

@@ -90,6 +90,9 @@ func IsExecutionEnabled(st state.ReadOnlyBeaconState, body interfaces.ReadOnlyBe
if st == nil || body == nil {
return false, errors.New("nil state or block body")
}
if st.Version() >= version.Capella {
return true, nil
}
if IsPreBellatrixVersion(st.Version()) {
return false, nil
}

View File

@@ -260,11 +260,12 @@ func Test_IsExecutionBlockCapella(t *testing.T) {
func Test_IsExecutionEnabled(t *testing.T) {
tests := []struct {
name string
payload *enginev1.ExecutionPayload
header interfaces.ExecutionData
useAltairSt bool
want bool
name string
payload *enginev1.ExecutionPayload
header interfaces.ExecutionData
useAltairSt bool
useCapellaSt bool
want bool
}{
{
name: "use older than bellatrix state",
@@ -331,6 +332,17 @@ func Test_IsExecutionEnabled(t *testing.T) {
}(),
want: true,
},
{
name: "capella state always enabled",
payload: emptyPayload(),
header: func() interfaces.ExecutionData {
h, err := emptyPayloadHeader()
require.NoError(t, err)
return h
}(),
useCapellaSt: true,
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -342,6 +354,8 @@ func Test_IsExecutionEnabled(t *testing.T) {
require.NoError(t, err)
if tt.useAltairSt {
st, _ = util.DeterministicGenesisStateAltair(t, 1)
} else if tt.useCapellaSt {
st, _ = util.DeterministicGenesisStateCapella(t, 1)
}
got, err := blocks.IsExecutionEnabled(st, body)
require.NoError(t, err)

View File

@@ -152,7 +152,7 @@ func ActiveValidatorIndices(ctx context.Context, s state.ReadOnlyBeaconState, ep
}
if err := UpdateCommitteeCache(ctx, s, epoch); err != nil {
return nil, errors.Wrap(err, "could not update committee cache")
log.WithError(err).Error("Could not update committee cache")
}
return indices, nil

View File

@@ -45,12 +45,13 @@ go_test(
"p2p_interface_test.go",
"reconstruction_helpers_test.go",
"reconstruction_test.go",
"semi_supernode_test.go",
"utils_test.go",
"validator_test.go",
"verification_test.go",
],
embed = [":go_default_library"],
deps = [
":go_default_library",
"//beacon-chain/blockchain/kzg:go_default_library",
"//beacon-chain/state/state-native:go_default_library",
"//config/fieldparams:go_default_library",

View File

@@ -5,6 +5,7 @@ import (
"math"
"slices"
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/crypto/hash"
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
@@ -96,8 +97,7 @@ func ComputeColumnsForCustodyGroup(custodyGroup uint64) ([]uint64, error) {
return nil, ErrCustodyGroupTooLarge
}
numberOfColumns := cfg.NumberOfColumns
numberOfColumns := uint64(fieldparams.NumberOfColumns)
columnsPerGroup := numberOfColumns / numberOfCustodyGroups
columns := make([]uint64, 0, columnsPerGroup)
@@ -112,8 +112,9 @@ func ComputeColumnsForCustodyGroup(custodyGroup uint64) ([]uint64, error) {
// ComputeCustodyGroupForColumn computes the custody group for a given column.
// It is the reciprocal function of ComputeColumnsForCustodyGroup.
func ComputeCustodyGroupForColumn(columnIndex uint64) (uint64, error) {
const numberOfColumns = fieldparams.NumberOfColumns
cfg := params.BeaconConfig()
numberOfColumns := cfg.NumberOfColumns
numberOfCustodyGroups := cfg.NumberOfCustodyGroups
if columnIndex >= numberOfColumns {

View File

@@ -30,7 +30,6 @@ func TestComputeColumnsForCustodyGroup(t *testing.T) {
func TestComputeCustodyGroupForColumn(t *testing.T) {
params.SetupTestConfigCleanup(t)
config := params.BeaconConfig()
config.NumberOfColumns = 128
config.NumberOfCustodyGroups = 64
params.OverrideBeaconConfig(config)

View File

@@ -2,6 +2,7 @@ package peerdas
import (
"encoding/binary"
"maps"
"sync"
"github.com/ethereum/go-ethereum/p2p/enode"
@@ -107,3 +108,102 @@ func computeInfoCacheKey(nodeID enode.ID, custodyGroupCount uint64) [nodeInfoCac
return key
}
// ColumnIndices represents as a set of ColumnIndices. This could be the set of indices that a node is required to custody,
// the set that a peer custodies, missing indices for a given block, indices that are present on disk, etc.
type ColumnIndices map[uint64]struct{}
// Has returns true if the index is present in the ColumnIndices.
func (ci ColumnIndices) Has(index uint64) bool {
_, ok := ci[index]
return ok
}
// Count returns the number of indices present in the ColumnIndices.
func (ci ColumnIndices) Count() int {
return len(ci)
}
// Set sets the index in the ColumnIndices.
func (ci ColumnIndices) Set(index uint64) {
ci[index] = struct{}{}
}
// Unset removes the index from the ColumnIndices.
func (ci ColumnIndices) Unset(index uint64) {
delete(ci, index)
}
// Copy creates a copy of the ColumnIndices.
func (ci ColumnIndices) Copy() ColumnIndices {
newCi := make(ColumnIndices, len(ci))
maps.Copy(newCi, ci)
return newCi
}
// Intersection returns a new ColumnIndices that contains only the indices that are present in both ColumnIndices.
func (ci ColumnIndices) Intersection(other ColumnIndices) ColumnIndices {
result := make(ColumnIndices)
for index := range ci {
if other.Has(index) {
result.Set(index)
}
}
return result
}
// Merge mutates the receiver so that any index that is set in either of
// the two ColumnIndices is set in the receiver after the function finishes.
// It does not mutate the other ColumnIndices given as a function argument.
func (ci ColumnIndices) Merge(other ColumnIndices) {
for index := range other {
ci.Set(index)
}
}
// ToMap converts a ColumnIndices into a map[uint64]struct{}.
// In the future ColumnIndices may be changed to a bit map, so using
// ToMap will ensure forwards-compatibility.
func (ci ColumnIndices) ToMap() map[uint64]struct{} {
return ci.Copy()
}
// ToSlice converts a ColumnIndices into a slice of uint64 indices.
func (ci ColumnIndices) ToSlice() []uint64 {
indices := make([]uint64, 0, len(ci))
for index := range ci {
indices = append(indices, index)
}
return indices
}
// NewColumnIndicesFromSlice creates a ColumnIndices from a slice of uint64.
func NewColumnIndicesFromSlice(indices []uint64) ColumnIndices {
ci := make(ColumnIndices, len(indices))
for _, index := range indices {
ci[index] = struct{}{}
}
return ci
}
// NewColumnIndicesFromMap creates a ColumnIndices from a map[uint64]bool. This kind of map
// is used in several places in peerdas code. Converting from this map type to ColumnIndices
// will allow us to move ColumnIndices underlying type to a bitmap in the future and avoid
// lots of loops for things like intersections/unions or copies.
func NewColumnIndicesFromMap(indices map[uint64]bool) ColumnIndices {
ci := make(ColumnIndices, len(indices))
for index, set := range indices {
if !set {
continue
}
ci[index] = struct{}{}
}
return ci
}
// NewColumnIndices creates an empty ColumnIndices.
// In the future ColumnIndices may change from a reference type to a value type,
// so using this constructor will ensure forwards-compatibility.
func NewColumnIndices() ColumnIndices {
return make(ColumnIndices)
}

View File

@@ -25,3 +25,10 @@ func TestInfo(t *testing.T) {
require.DeepEqual(t, expectedDataColumnsSubnets, actual.DataColumnsSubnets)
}
}
func TestNewColumnIndicesFromMap(t *testing.T) {
t.Run("nil map", func(t *testing.T) {
ci := peerdas.NewColumnIndicesFromMap(nil)
require.Equal(t, 0, ci.Count())
})
}

View File

@@ -33,8 +33,7 @@ func (Cgc) ENRKey() string { return params.BeaconNetworkConfig().CustodyGroupCou
// https://github.com/ethereum/consensus-specs/blob/master/specs/fulu/p2p-interface.md#verify_data_column_sidecar
func VerifyDataColumnSidecar(sidecar blocks.RODataColumn) error {
// The sidecar index must be within the valid range.
numberOfColumns := params.BeaconConfig().NumberOfColumns
if sidecar.Index >= numberOfColumns {
if sidecar.Index >= fieldparams.NumberOfColumns {
return ErrIndexTooLarge
}

View File

@@ -281,8 +281,11 @@ func BenchmarkVerifyDataColumnSidecarKZGProofs_SameCommitments_NoBatch(b *testin
}
func BenchmarkVerifyDataColumnSidecarKZGProofs_DiffCommitments_Batch(b *testing.B) {
const blobCount = 12
numberOfColumns := int64(params.BeaconConfig().NumberOfColumns)
const (
blobCount = 12
numberOfColumns = fieldparams.NumberOfColumns
)
err := kzg.Start()
require.NoError(b, err)

View File

@@ -26,7 +26,40 @@ var (
func MinimumColumnCountToReconstruct() uint64 {
// If the number of columns is odd, then we need total / 2 + 1 columns to reconstruct.
// If the number of columns is even, then we need total / 2 columns to reconstruct.
return (params.BeaconConfig().NumberOfColumns + 1) / 2
return (fieldparams.NumberOfColumns + 1) / 2
}
// MinimumCustodyGroupCountToReconstruct returns the minimum number of custody groups needed to
// custody enough data columns for reconstruction. This accounts for the relationship between
// custody groups and columns, making it future-proof if these values change.
// Returns an error if the configuration values are invalid (zero or would cause division by zero).
func MinimumCustodyGroupCountToReconstruct() (uint64, error) {
const numberOfColumns = fieldparams.NumberOfColumns
cfg := params.BeaconConfig()
// Validate configuration values
if numberOfColumns == 0 {
return 0, errors.New("NumberOfColumns cannot be zero")
}
if cfg.NumberOfCustodyGroups == 0 {
return 0, errors.New("NumberOfCustodyGroups cannot be zero")
}
minimumColumnCount := MinimumColumnCountToReconstruct()
// Calculate how many columns each custody group represents
columnsPerGroup := numberOfColumns / cfg.NumberOfCustodyGroups
// If there are more groups than columns (columnsPerGroup = 0), this is an invalid configuration
// for reconstruction purposes as we cannot determine a meaningful custody group count
if columnsPerGroup == 0 {
return 0, errors.Errorf("invalid configuration: NumberOfCustodyGroups (%d) exceeds NumberOfColumns (%d)",
cfg.NumberOfCustodyGroups, numberOfColumns)
}
// Use ceiling division to ensure we have enough groups to cover the minimum columns
// ceiling(a/b) = (a + b - 1) / b
return (minimumColumnCount + columnsPerGroup - 1) / columnsPerGroup, nil
}
// recoverCellsForBlobs reconstructs cells for specified blobs from the given data column sidecars.
@@ -253,7 +286,8 @@ func ReconstructBlobSidecars(block blocks.ROBlock, verifiedDataColumnSidecars []
// ComputeCellsAndProofsFromFlat computes the cells and proofs from blobs and cell flat proofs.
func ComputeCellsAndProofsFromFlat(blobs [][]byte, cellProofs [][]byte) ([][]kzg.Cell, [][]kzg.Proof, error) {
numberOfColumns := params.BeaconConfig().NumberOfColumns
const numberOfColumns = fieldparams.NumberOfColumns
blobCount := uint64(len(blobs))
cellProofsCount := uint64(len(cellProofs))
@@ -295,8 +329,6 @@ func ComputeCellsAndProofsFromFlat(blobs [][]byte, cellProofs [][]byte) ([][]kzg
// ComputeCellsAndProofsFromStructured computes the cells and proofs from blobs and cell proofs.
func ComputeCellsAndProofsFromStructured(blobsAndProofs []*pb.BlobAndProofV2) ([][]kzg.Cell, [][]kzg.Proof, error) {
numberOfColumns := params.BeaconConfig().NumberOfColumns
cellsPerBlob := make([][]kzg.Cell, 0, len(blobsAndProofs))
proofsPerBlob := make([][]kzg.Proof, 0, len(blobsAndProofs))
for _, blobAndProof := range blobsAndProofs {
@@ -315,7 +347,7 @@ func ComputeCellsAndProofsFromStructured(blobsAndProofs []*pb.BlobAndProofV2) ([
return nil, nil, errors.Wrap(err, "compute cells")
}
kzgProofs := make([]kzg.Proof, 0, numberOfColumns)
kzgProofs := make([]kzg.Proof, 0, fieldparams.NumberOfColumns)
for _, kzgProofBytes := range blobAndProof.KzgProofs {
if len(kzgProofBytes) != kzg.BytesPerProof {
return nil, nil, errors.New("wrong KZG proof size - should never happen")

View File

@@ -17,41 +17,9 @@ import (
)
func TestMinimumColumnsCountToReconstruct(t *testing.T) {
testCases := []struct {
name string
numberOfColumns uint64
expected uint64
}{
{
name: "numberOfColumns=128",
numberOfColumns: 128,
expected: 64,
},
{
name: "numberOfColumns=129",
numberOfColumns: 129,
expected: 65,
},
{
name: "numberOfColumns=130",
numberOfColumns: 130,
expected: 65,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Set the total number of columns.
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
cfg.NumberOfColumns = tc.numberOfColumns
params.OverrideBeaconConfig(cfg)
// Compute the minimum number of columns needed to reconstruct.
actual := peerdas.MinimumColumnCountToReconstruct()
require.Equal(t, tc.expected, actual)
})
}
const expected = uint64(64)
actual := peerdas.MinimumColumnCountToReconstruct()
require.Equal(t, expected, actual)
}
func TestReconstructDataColumnSidecars(t *testing.T) {
@@ -200,7 +168,6 @@ func TestReconstructBlobSidecars(t *testing.T) {
t.Run("nominal", func(t *testing.T) {
const blobCount = 3
numberOfColumns := params.BeaconConfig().NumberOfColumns
roBlock, roBlobSidecars := util.GenerateTestElectraBlockWithSidecar(t, [fieldparams.RootLength]byte{}, 42, blobCount)
@@ -236,7 +203,7 @@ func TestReconstructBlobSidecars(t *testing.T) {
require.NoError(t, err)
// Flatten proofs.
cellProofs := make([][]byte, 0, blobCount*numberOfColumns)
cellProofs := make([][]byte, 0, blobCount*fieldparams.NumberOfColumns)
for _, proofs := range inputProofsPerBlob {
for _, proof := range proofs {
cellProofs = append(cellProofs, proof[:])
@@ -428,13 +395,12 @@ func TestReconstructBlobs(t *testing.T) {
}
func TestComputeCellsAndProofsFromFlat(t *testing.T) {
const numberOfColumns = fieldparams.NumberOfColumns
// Start the trusted setup.
err := kzg.Start()
require.NoError(t, err)
t.Run("mismatched blob and proof counts", func(t *testing.T) {
numberOfColumns := params.BeaconConfig().NumberOfColumns
// Create one blob but proofs for two blobs
blobs := [][]byte{{}}
@@ -447,7 +413,6 @@ func TestComputeCellsAndProofsFromFlat(t *testing.T) {
t.Run("nominal", func(t *testing.T) {
const blobCount = 2
numberOfColumns := params.BeaconConfig().NumberOfColumns
// Generate test blobs
_, roBlobSidecars := util.GenerateTestElectraBlockWithSidecar(t, [fieldparams.RootLength]byte{}, 42, blobCount)

View File

@@ -0,0 +1,137 @@
package peerdas
import (
"testing"
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/testing/require"
"github.com/ethereum/go-ethereum/p2p/enode"
)
func TestSemiSupernodeCustody(t *testing.T) {
const numberOfColumns = fieldparams.NumberOfColumns
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig()
cfg.NumberOfCustodyGroups = 128
params.OverrideBeaconConfig(cfg)
// Create a test node ID
nodeID := enode.ID([32]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32})
t.Run("semi-supernode custodies exactly 64 columns", func(t *testing.T) {
// Semi-supernode uses 64 custody groups (half of 128)
const semiSupernodeCustodyGroupCount = 64
// Get custody groups for semi-supernode
custodyGroups, err := CustodyGroups(nodeID, semiSupernodeCustodyGroupCount)
require.NoError(t, err)
require.Equal(t, semiSupernodeCustodyGroupCount, len(custodyGroups))
// Verify we get exactly 64 custody columns
custodyColumns, err := CustodyColumns(custodyGroups)
require.NoError(t, err)
require.Equal(t, semiSupernodeCustodyGroupCount, len(custodyColumns))
// Verify the columns are valid (within 0-127 range)
for columnIndex := range custodyColumns {
if columnIndex >= numberOfColumns {
t.Fatalf("Invalid column index %d, should be less than %d", columnIndex, numberOfColumns)
}
}
})
t.Run("64 columns is exactly the minimum for reconstruction", func(t *testing.T) {
minimumCount := MinimumColumnCountToReconstruct()
require.Equal(t, uint64(64), minimumCount)
})
t.Run("semi-supernode vs supernode custody", func(t *testing.T) {
// Semi-supernode (64 custody groups)
semiSupernodeGroups, err := CustodyGroups(nodeID, 64)
require.NoError(t, err)
semiSupernodeColumns, err := CustodyColumns(semiSupernodeGroups)
require.NoError(t, err)
// Supernode (128 custody groups = all groups)
supernodeGroups, err := CustodyGroups(nodeID, 128)
require.NoError(t, err)
supernodeColumns, err := CustodyColumns(supernodeGroups)
require.NoError(t, err)
// Verify semi-supernode has exactly half the columns of supernode
require.Equal(t, 64, len(semiSupernodeColumns))
require.Equal(t, 128, len(supernodeColumns))
require.Equal(t, len(supernodeColumns)/2, len(semiSupernodeColumns))
// Verify all semi-supernode columns are a subset of supernode columns
for columnIndex := range semiSupernodeColumns {
if !supernodeColumns[columnIndex] {
t.Fatalf("Semi-supernode column %d not found in supernode columns", columnIndex)
}
}
})
}
func TestMinimumCustodyGroupCountToReconstruct(t *testing.T) {
tests := []struct {
name string
numberOfGroups uint64
expectedResult uint64
}{
{
name: "Standard 1:1 ratio (128 columns, 128 groups)",
numberOfGroups: 128,
expectedResult: 64, // Need half of 128 groups
},
{
name: "2 columns per group (128 columns, 64 groups)",
numberOfGroups: 64,
expectedResult: 32, // Need 64 columns, which is 32 groups (64/2)
},
{
name: "4 columns per group (128 columns, 32 groups)",
numberOfGroups: 32,
expectedResult: 16, // Need 64 columns, which is 16 groups (64/4)
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig()
cfg.NumberOfCustodyGroups = tt.numberOfGroups
params.OverrideBeaconConfig(cfg)
result, err := MinimumCustodyGroupCountToReconstruct()
require.NoError(t, err)
require.Equal(t, tt.expectedResult, result)
})
}
}
func TestMinimumCustodyGroupCountToReconstruct_ErrorCases(t *testing.T) {
t.Run("Returns error when NumberOfCustodyGroups is zero", func(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig()
cfg.NumberOfCustodyGroups = 0
params.OverrideBeaconConfig(cfg)
_, err := MinimumCustodyGroupCountToReconstruct()
require.NotNil(t, err)
require.Equal(t, true, err.Error() == "NumberOfCustodyGroups cannot be zero")
})
t.Run("Returns error when NumberOfCustodyGroups exceeds NumberOfColumns", func(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig()
cfg.NumberOfCustodyGroups = 256
params.OverrideBeaconConfig(cfg)
_, err := MinimumCustodyGroupCountToReconstruct()
require.NotNil(t, err)
// Just check that we got an error about the configuration
require.Equal(t, true, len(err.Error()) > 0)
})
}

View File

@@ -102,11 +102,13 @@ func ValidatorsCustodyRequirement(state beaconState.ReadOnlyBeaconState, validat
// https://github.com/ethereum/consensus-specs/blob/master/specs/fulu/validator.md#get_data_column_sidecars_from_block and
// https://github.com/ethereum/consensus-specs/blob/master/specs/fulu/validator.md#get_data_column_sidecars_from_column_sidecar
func DataColumnSidecars(cellsPerBlob [][]kzg.Cell, proofsPerBlob [][]kzg.Proof, src ConstructionPopulator) ([]blocks.RODataColumn, error) {
const numberOfColumns = uint64(fieldparams.NumberOfColumns)
if len(cellsPerBlob) == 0 {
return nil, nil
}
start := time.Now()
cells, proofs, err := rotateRowsToCols(cellsPerBlob, proofsPerBlob, params.BeaconConfig().NumberOfColumns)
cells, proofs, err := rotateRowsToCols(cellsPerBlob, proofsPerBlob, numberOfColumns)
if err != nil {
return nil, errors.Wrap(err, "rotate cells and proofs")
}
@@ -115,9 +117,8 @@ func DataColumnSidecars(cellsPerBlob [][]kzg.Cell, proofsPerBlob [][]kzg.Proof,
return nil, errors.Wrap(err, "extract block info")
}
maxIdx := params.BeaconConfig().NumberOfColumns
roSidecars := make([]blocks.RODataColumn, 0, maxIdx)
for idx := range maxIdx {
roSidecars := make([]blocks.RODataColumn, 0, numberOfColumns)
for idx := range numberOfColumns {
sidecar := &ethpb.DataColumnSidecar{
Index: idx,
Column: cells[idx],

View File

@@ -6,7 +6,7 @@ import (
"github.com/OffchainLabs/prysm/v7/beacon-chain/blockchain/kzg"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/peerdas"
state_native "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native"
"github.com/OffchainLabs/prysm/v7/config/params"
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
@@ -59,6 +59,8 @@ func TestValidatorsCustodyRequirement(t *testing.T) {
}
func TestDataColumnSidecars(t *testing.T) {
const numberOfColumns = fieldparams.NumberOfColumns
t.Run("sizes mismatch", func(t *testing.T) {
// Create a protobuf signed beacon block.
signedBeaconBlockPb := util.NewBeaconBlockDeneb()
@@ -69,10 +71,10 @@ func TestDataColumnSidecars(t *testing.T) {
// Create cells and proofs.
cellsPerBlob := [][]kzg.Cell{
make([]kzg.Cell, params.BeaconConfig().NumberOfColumns),
make([]kzg.Cell, numberOfColumns),
}
proofsPerBlob := [][]kzg.Proof{
make([]kzg.Proof, params.BeaconConfig().NumberOfColumns),
make([]kzg.Proof, numberOfColumns),
}
rob, err := blocks.NewROBlock(signedBeaconBlock)
@@ -117,7 +119,6 @@ func TestDataColumnSidecars(t *testing.T) {
require.NoError(t, err)
// Create cells and proofs with sufficient cells but insufficient proofs.
numberOfColumns := params.BeaconConfig().NumberOfColumns
cellsPerBlob := [][]kzg.Cell{
make([]kzg.Cell, numberOfColumns),
}
@@ -149,7 +150,6 @@ func TestDataColumnSidecars(t *testing.T) {
require.NoError(t, err)
// Create cells and proofs with correct dimensions.
numberOfColumns := params.BeaconConfig().NumberOfColumns
cellsPerBlob := [][]kzg.Cell{
make([]kzg.Cell, numberOfColumns),
make([]kzg.Cell, numberOfColumns),
@@ -197,6 +197,7 @@ func TestDataColumnSidecars(t *testing.T) {
}
func TestReconstructionSource(t *testing.T) {
const numberOfColumns = fieldparams.NumberOfColumns
// Create a Fulu block with blob commitments.
signedBeaconBlockPb := util.NewBeaconBlockFulu()
commitment1 := make([]byte, 48)
@@ -212,7 +213,6 @@ func TestReconstructionSource(t *testing.T) {
require.NoError(t, err)
// Create cells and proofs with correct dimensions.
numberOfColumns := params.BeaconConfig().NumberOfColumns
cellsPerBlob := [][]kzg.Cell{
make([]kzg.Cell, numberOfColumns),
make([]kzg.Cell, numberOfColumns),

View File

@@ -4,14 +4,19 @@ go_library(
name = "go_default_library",
srcs = [
"availability_blobs.go",
"availability_columns.go",
"bisect.go",
"blob_cache.go",
"data_column_cache.go",
"iface.go",
"log.go",
"mock.go",
"needs.go",
],
importpath = "github.com/OffchainLabs/prysm/v7/beacon-chain/das",
visibility = ["//visibility:public"],
deps = [
"//beacon-chain/core/peerdas:go_default_library",
"//beacon-chain/db/filesystem:go_default_library",
"//beacon-chain/verification:go_default_library",
"//config/fieldparams:go_default_library",
@@ -21,6 +26,7 @@ go_library(
"//runtime/logging:go_default_library",
"//runtime/version:go_default_library",
"//time/slots:go_default_library",
"@com_github_ethereum_go_ethereum//p2p/enode:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
],
@@ -30,11 +36,14 @@ go_test(
name = "go_default_test",
srcs = [
"availability_blobs_test.go",
"availability_columns_test.go",
"blob_cache_test.go",
"data_column_cache_test.go",
"needs_test.go",
],
embed = [":go_default_library"],
deps = [
"//beacon-chain/core/peerdas:go_default_library",
"//beacon-chain/db/filesystem:go_default_library",
"//beacon-chain/verification:go_default_library",
"//config/fieldparams:go_default_library",
@@ -45,6 +54,7 @@ go_test(
"//testing/require:go_default_library",
"//testing/util:go_default_library",
"//time/slots:go_default_library",
"@com_github_ethereum_go_ethereum//p2p/enode:go_default_library",
"@com_github_pkg_errors//:go_default_library",
],
)

View File

@@ -11,9 +11,8 @@ import (
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/runtime/logging"
"github.com/OffchainLabs/prysm/v7/runtime/version"
"github.com/OffchainLabs/prysm/v7/time/slots"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/sirupsen/logrus"
)
var (
@@ -24,12 +23,13 @@ var (
// This implementation will hold any blobs passed to Persist until the IsDataAvailable is called for their
// block, at which time they will undergo full verification and be saved to the disk.
type LazilyPersistentStoreBlob struct {
store *filesystem.BlobStorage
cache *blobCache
verifier BlobBatchVerifier
store *filesystem.BlobStorage
cache *blobCache
verifier BlobBatchVerifier
shouldRetain RetentionChecker
}
var _ AvailabilityStore = &LazilyPersistentStoreBlob{}
var _ AvailabilityChecker = &LazilyPersistentStoreBlob{}
// BlobBatchVerifier enables LazyAvailabilityStore to manage the verification process
// going from ROBlob->VerifiedROBlob, while avoiding the decision of which individual verifications
@@ -42,11 +42,12 @@ type BlobBatchVerifier interface {
// NewLazilyPersistentStore creates a new LazilyPersistentStore. This constructor should always be used
// when creating a LazilyPersistentStore because it needs to initialize the cache under the hood.
func NewLazilyPersistentStore(store *filesystem.BlobStorage, verifier BlobBatchVerifier) *LazilyPersistentStoreBlob {
func NewLazilyPersistentStore(store *filesystem.BlobStorage, verifier BlobBatchVerifier, shouldRetain RetentionChecker) *LazilyPersistentStoreBlob {
return &LazilyPersistentStoreBlob{
store: store,
cache: newBlobCache(),
verifier: verifier,
store: store,
cache: newBlobCache(),
verifier: verifier,
shouldRetain: shouldRetain,
}
}
@@ -66,9 +67,6 @@ func (s *LazilyPersistentStoreBlob) Persist(current primitives.Slot, sidecars ..
}
}
}
if !params.WithinDAPeriod(slots.ToEpoch(sidecars[0].Slot()), slots.ToEpoch(current)) {
return nil
}
key := keyFromSidecar(sidecars[0])
entry := s.cache.ensure(key)
for _, blobSidecar := range sidecars {
@@ -81,8 +79,17 @@ func (s *LazilyPersistentStoreBlob) Persist(current primitives.Slot, sidecars ..
// IsDataAvailable returns nil if all the commitments in the given block are persisted to the db and have been verified.
// BlobSidecars already in the db are assumed to have been previously verified against the block.
func (s *LazilyPersistentStoreBlob) IsDataAvailable(ctx context.Context, current primitives.Slot, b blocks.ROBlock) error {
blockCommitments, err := commitmentsToCheck(b, current)
func (s *LazilyPersistentStoreBlob) IsDataAvailable(ctx context.Context, current primitives.Slot, blks ...blocks.ROBlock) error {
for _, b := range blks {
if err := s.checkOne(ctx, current, b); err != nil {
return err
}
}
return nil
}
func (s *LazilyPersistentStoreBlob) checkOne(ctx context.Context, current primitives.Slot, b blocks.ROBlock) error {
blockCommitments, err := commitmentsToCheck(b, s.shouldRetain)
if err != nil {
return errors.Wrapf(err, "could not check data availability for block %#x", b.Root())
}
@@ -100,7 +107,7 @@ func (s *LazilyPersistentStoreBlob) IsDataAvailable(ctx context.Context, current
// Verify we have all the expected sidecars, and fail fast if any are missing or inconsistent.
// We don't try to salvage problematic batches because this indicates a misbehaving peer and we'd rather
// ignore their response and decrease their peer score.
sidecars, err := entry.filter(root, blockCommitments, b.Block().Slot())
sidecars, err := entry.filter(root, blockCommitments)
if err != nil {
return errors.Wrap(err, "incomplete BlobSidecar batch")
}
@@ -112,7 +119,7 @@ func (s *LazilyPersistentStoreBlob) IsDataAvailable(ctx context.Context, current
ok := errors.As(err, &me)
if ok {
fails := me.Failures()
lf := make(log.Fields, len(fails))
lf := make(logrus.Fields, len(fails))
for i := range fails {
lf[fmt.Sprintf("fail_%d", i)] = fails[i].Error()
}
@@ -131,13 +138,12 @@ func (s *LazilyPersistentStoreBlob) IsDataAvailable(ctx context.Context, current
return nil
}
func commitmentsToCheck(b blocks.ROBlock, current primitives.Slot) ([][]byte, error) {
func commitmentsToCheck(b blocks.ROBlock, shouldRetain RetentionChecker) ([][]byte, error) {
if b.Version() < version.Deneb {
return nil, nil
}
// We are only required to check within MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUEST
if !params.WithinDAPeriod(slots.ToEpoch(b.Block().Slot()), slots.ToEpoch(current)) {
if !shouldRetain(b.Block().Slot()) {
return nil, nil
}

View File

@@ -17,6 +17,10 @@ import (
errors "github.com/pkg/errors"
)
func testShouldRetainAlways(s primitives.Slot) bool {
return true
}
func Test_commitmentsToCheck(t *testing.T) {
params.SetupTestConfigCleanup(t)
params.BeaconConfig().FuluForkEpoch = params.BeaconConfig().ElectraForkEpoch + 4096*2
@@ -30,11 +34,12 @@ func Test_commitmentsToCheck(t *testing.T) {
commits[i] = bytesutil.PadTo([]byte{byte(i)}, 48)
}
cases := []struct {
name string
commits [][]byte
block func(*testing.T) blocks.ROBlock
slot primitives.Slot
err error
name string
commits [][]byte
block func(*testing.T) blocks.ROBlock
slot primitives.Slot
err error
shouldRetain RetentionChecker
}{
{
name: "pre deneb",
@@ -60,6 +65,7 @@ func Test_commitmentsToCheck(t *testing.T) {
require.NoError(t, err)
return rb
},
shouldRetain: testShouldRetainAlways,
commits: func() [][]byte {
mb := params.GetNetworkScheduleEntry(slots.ToEpoch(fulu + 100)).MaxBlobsPerBlock
return commits[:mb]
@@ -79,7 +85,8 @@ func Test_commitmentsToCheck(t *testing.T) {
require.NoError(t, err)
return rb
},
slot: fulu + windowSlots + 1,
shouldRetain: func(s primitives.Slot) bool { return false },
slot: fulu + windowSlots + 1,
},
{
name: "excessive commitments",
@@ -97,14 +104,15 @@ func Test_commitmentsToCheck(t *testing.T) {
require.Equal(t, true, len(c) > params.BeaconConfig().MaxBlobsPerBlock(sb.Block().Slot()))
return rb
},
slot: windowSlots + 1,
err: errIndexOutOfBounds,
shouldRetain: testShouldRetainAlways,
slot: windowSlots + 1,
err: errIndexOutOfBounds,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
b := c.block(t)
co, err := commitmentsToCheck(b, c.slot)
co, err := commitmentsToCheck(b, c.shouldRetain)
if c.err != nil {
require.ErrorIs(t, err, c.err)
} else {
@@ -126,7 +134,7 @@ func TestLazilyPersistent_Missing(t *testing.T) {
blk, blobSidecars := util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, ds, 3)
mbv := &mockBlobBatchVerifier{t: t, scs: blobSidecars}
as := NewLazilyPersistentStore(store, mbv)
as := NewLazilyPersistentStore(store, mbv, testShouldRetainAlways)
// Only one commitment persisted, should return error with other indices
require.NoError(t, as.Persist(ds, blobSidecars[2]))
@@ -153,7 +161,7 @@ func TestLazilyPersistent_Mismatch(t *testing.T) {
mbv := &mockBlobBatchVerifier{t: t, err: errors.New("kzg check should not run")}
blobSidecars[0].KzgCommitment = bytesutil.PadTo([]byte("nope"), 48)
as := NewLazilyPersistentStore(store, mbv)
as := NewLazilyPersistentStore(store, mbv, testShouldRetainAlways)
// Only one commitment persisted, should return error with other indices
require.NoError(t, as.Persist(ds, blobSidecars[0]))
@@ -166,11 +174,11 @@ func TestLazyPersistOnceCommitted(t *testing.T) {
ds := util.SlotAtEpoch(t, params.BeaconConfig().DenebForkEpoch)
_, blobSidecars := util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, ds, 6)
as := NewLazilyPersistentStore(filesystem.NewEphemeralBlobStorage(t), &mockBlobBatchVerifier{})
as := NewLazilyPersistentStore(filesystem.NewEphemeralBlobStorage(t), &mockBlobBatchVerifier{}, testShouldRetainAlways)
// stashes as expected
require.NoError(t, as.Persist(ds, blobSidecars...))
// ignores duplicates
require.ErrorIs(t, as.Persist(ds, blobSidecars...), ErrDuplicateSidecar)
require.ErrorIs(t, as.Persist(ds, blobSidecars...), errDuplicateSidecar)
// ignores index out of bound
blobSidecars[0].Index = 6
@@ -183,7 +191,7 @@ func TestLazyPersistOnceCommitted(t *testing.T) {
require.NoError(t, as.Persist(slotOOB, moreBlobSidecars[0]))
// doesn't ignore new sidecars with a different block root
require.NoError(t, as.Persist(ds, moreBlobSidecars...))
require.NoError(t, as.Persist(ds, moreBlobSidecars[1:]...))
}
type mockBlobBatchVerifier struct {

View File

@@ -0,0 +1,244 @@
package das
import (
"context"
"io"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/peerdas"
"github.com/OffchainLabs/prysm/v7/beacon-chain/db/filesystem"
"github.com/OffchainLabs/prysm/v7/beacon-chain/verification"
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/ethereum/go-ethereum/p2p/enode"
errors "github.com/pkg/errors"
)
// LazilyPersistentStoreColumn is an implementation of AvailabilityStore to be used when batch syncing data columns.
// This implementation will hold any data columns passed to Persist until the IsDataAvailable is called for their
// block, at which time they will undergo full verification and be saved to the disk.
type LazilyPersistentStoreColumn struct {
store *filesystem.DataColumnStorage
cache *dataColumnCache
newDataColumnsVerifier verification.NewDataColumnsVerifier
custody *custodyRequirement
bisector Bisector
shouldRetain RetentionChecker
}
var _ AvailabilityChecker = &LazilyPersistentStoreColumn{}
// DataColumnsVerifier enables LazilyPersistentStoreColumn to manage the verification process
// going from RODataColumn->VerifiedRODataColumn, while avoiding the decision of which individual verifications
// to run and in what order. Since LazilyPersistentStoreColumn always tries to verify and save data columns only when
// they are all available, the interface takes a slice of data column sidecars.
type DataColumnsVerifier interface {
VerifiedRODataColumns(ctx context.Context, blk blocks.ROBlock, scs []blocks.RODataColumn) ([]blocks.VerifiedRODataColumn, error)
}
// NewLazilyPersistentStoreColumn creates a new LazilyPersistentStoreColumn.
// WARNING: The resulting LazilyPersistentStoreColumn is NOT thread-safe.
func NewLazilyPersistentStoreColumn(
store *filesystem.DataColumnStorage,
newDataColumnsVerifier verification.NewDataColumnsVerifier,
nodeID enode.ID,
cgc uint64,
bisector Bisector,
shouldRetain RetentionChecker,
) *LazilyPersistentStoreColumn {
return &LazilyPersistentStoreColumn{
store: store,
cache: newDataColumnCache(),
newDataColumnsVerifier: newDataColumnsVerifier,
custody: &custodyRequirement{nodeID: nodeID, cgc: cgc},
bisector: bisector,
shouldRetain: shouldRetain,
}
}
// PersistColumns adds columns to the working column cache. Columns stored in this cache will be persisted
// for at least as long as the node is running. Once IsDataAvailable succeeds, all columns referenced
// by the given block are guaranteed to be persisted for the remainder of the retention period.
func (s *LazilyPersistentStoreColumn) Persist(_ primitives.Slot, sidecars ...blocks.RODataColumn) error {
for _, sidecar := range sidecars {
if err := s.cache.stash(sidecar); err != nil {
return errors.Wrap(err, "stash DataColumnSidecar")
}
}
return nil
}
// IsDataAvailable returns nil if all the commitments in the given block are persisted to the db and have been verified.
// DataColumnsSidecars already in the db are assumed to have been previously verified against the block.
func (s *LazilyPersistentStoreColumn) IsDataAvailable(ctx context.Context, _ primitives.Slot, blks ...blocks.ROBlock) error {
toVerify := make([]blocks.RODataColumn, 0)
for _, block := range blks {
indices, err := s.required(block)
if err != nil {
return errors.Wrapf(err, "full commitments to check with block root `%#x`", block.Root())
}
if indices.Count() == 0 {
continue
}
key := keyFromBlock(block)
entry := s.cache.entry(key)
toVerify, err = entry.append(toVerify, IndicesNotStored(s.store.Summary(block.Root()), indices))
if err != nil {
return errors.Wrap(err, "entry filter")
}
}
if err := s.verifyAndSave(toVerify); err != nil {
log.Warn("Batch verification failed, bisecting columns by peer")
if err := s.bisectVerification(toVerify); err != nil {
return errors.Wrap(err, "bisect verification")
}
}
s.cache.cleanup(blks)
return nil
}
// required returns the set of column indices to check for a given block.
func (s *LazilyPersistentStoreColumn) required(block blocks.ROBlock) (peerdas.ColumnIndices, error) {
if !s.shouldRetain(block.Block().Slot()) {
return peerdas.NewColumnIndices(), nil
}
// If there are any commitments in the block, there are blobs,
// and if there are blobs, we need the columns bisecting those blobs.
commitments, err := block.Block().Body().BlobKzgCommitments()
if err != nil {
return nil, errors.Wrap(err, "blob KZG commitments")
}
// No DA check needed if the block has no blobs.
if len(commitments) == 0 {
return peerdas.NewColumnIndices(), nil
}
return s.custody.required()
}
// verifyAndSave calls Save on the column store if the columns pass verification.
func (s *LazilyPersistentStoreColumn) verifyAndSave(columns []blocks.RODataColumn) error {
if len(columns) == 0 {
return nil
}
verified, err := s.verifyColumns(columns)
if err != nil {
return errors.Wrap(err, "verify columns")
}
if err := s.store.Save(verified); err != nil {
return errors.Wrap(err, "save data column sidecars")
}
return nil
}
func (s *LazilyPersistentStoreColumn) verifyColumns(columns []blocks.RODataColumn) ([]blocks.VerifiedRODataColumn, error) {
if len(columns) == 0 {
return nil, nil
}
verifier := s.newDataColumnsVerifier(columns, verification.ByRangeRequestDataColumnSidecarRequirements)
if err := verifier.ValidFields(); err != nil {
return nil, errors.Wrap(err, "valid fields")
}
if err := verifier.SidecarInclusionProven(); err != nil {
return nil, errors.Wrap(err, "sidecar inclusion proven")
}
if err := verifier.SidecarKzgProofVerified(); err != nil {
return nil, errors.Wrap(err, "sidecar KZG proof verified")
}
return verifier.VerifiedRODataColumns()
}
// bisectVerification is used when verification of a batch of columns fails. Since the batch could
// span multiple blocks or have been fetched from multiple peers, this pattern enables code using the
// store to break the verification into smaller units and learn the results, in order to plan to retry
// retrieval of the unusable columns.
func (s *LazilyPersistentStoreColumn) bisectVerification(columns []blocks.RODataColumn) error {
if len(columns) == 0 {
return nil
}
if s.bisector == nil {
return errors.New("bisector not initialized")
}
iter, err := s.bisector.Bisect(columns)
if err != nil {
return errors.Wrap(err, "Bisector.Bisect")
}
// It's up to the bisector how to chunk up columns for verification,
// which could be by block, or by peer, or any other strategy.
// For the purposes of range syncing or backfill this will be by peer,
// so that the node can learn which peer is giving us bad data and downscore them.
for columns, err := iter.Next(); columns != nil; columns, err = iter.Next() {
if err != nil {
if !errors.Is(err, io.EOF) {
return errors.Wrap(err, "Bisector.Next")
}
break // io.EOF signals end of iteration
}
// We save the parts of the batch that have been verified successfully even though we don't know
// if all columns for the block will be available until the block is imported.
if err := s.verifyAndSave(s.columnsNotStored(columns)); err != nil {
iter.OnError(err)
continue
}
}
// This should give us a single error representing any unresolved errors seen via onError.
return iter.Error()
}
// columnsNotStored filters the list of ROColumnSidecars to only include those that are not found in the storage summary.
func (s *LazilyPersistentStoreColumn) columnsNotStored(sidecars []blocks.RODataColumn) []blocks.RODataColumn {
// We use this method to filter a set of sidecars that were previously seen to be unavailable on disk. So our base assumption
// is that they are still available and we don't need to copy the list. Instead we make a slice of any indices that are unexpectedly
// stored and only when we find that the storage view has changed do we need to create a new slice.
stored := make(map[int]struct{}, 0)
lastRoot := [32]byte{}
var sum filesystem.DataColumnStorageSummary
for i, sc := range sidecars {
if sc.BlockRoot() != lastRoot {
sum = s.store.Summary(sc.BlockRoot())
lastRoot = sc.BlockRoot()
}
if sum.HasIndex(sc.Index) {
stored[i] = struct{}{}
}
}
// If the view on storage hasn't changed, return the original list.
if len(stored) == 0 {
return sidecars
}
shift := 0
for i := range sidecars {
if _, ok := stored[i]; ok {
// If the index is stored, skip and overwrite it.
// Track how many spaces down to shift unseen sidecars (to overwrite the previously shifted or seen).
shift++
continue
}
if shift > 0 {
// If the index is not stored and we have seen stored indices,
// we need to shift the current index down.
sidecars[i-shift] = sidecars[i]
}
}
return sidecars[:len(sidecars)-shift]
}
type custodyRequirement struct {
nodeID enode.ID
cgc uint64 // custody group count
indices peerdas.ColumnIndices
}
func (c *custodyRequirement) required() (peerdas.ColumnIndices, error) {
peerInfo, _, err := peerdas.Info(c.nodeID, c.cgc)
if err != nil {
return peerdas.NewColumnIndices(), errors.Wrap(err, "peer info")
}
return peerdas.NewColumnIndicesFromMap(peerInfo.CustodyColumns), nil
}

View File

@@ -0,0 +1,908 @@
package das
import (
"context"
"fmt"
"io"
"testing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/db/filesystem"
"github.com/OffchainLabs/prysm/v7/beacon-chain/verification"
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
"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"
"github.com/OffchainLabs/prysm/v7/testing/require"
"github.com/OffchainLabs/prysm/v7/testing/util"
"github.com/OffchainLabs/prysm/v7/time/slots"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/pkg/errors"
)
func mockShouldRetain(current primitives.Epoch) RetentionChecker {
return func(slot primitives.Slot) bool {
return params.WithinDAPeriod(slots.ToEpoch(slot), current)
}
}
var commitments = [][]byte{
bytesutil.PadTo([]byte("a"), 48),
bytesutil.PadTo([]byte("b"), 48),
bytesutil.PadTo([]byte("c"), 48),
bytesutil.PadTo([]byte("d"), 48),
}
func TestPersist(t *testing.T) {
t.Run("no sidecars", func(t *testing.T) {
dataColumnStorage := filesystem.NewEphemeralDataColumnStorage(t)
lazilyPersistentStoreColumns := NewLazilyPersistentStoreColumn(dataColumnStorage, nil, enode.ID{}, 0, nil, mockShouldRetain(0))
err := lazilyPersistentStoreColumns.Persist(0)
require.NoError(t, err)
require.Equal(t, 0, len(lazilyPersistentStoreColumns.cache.entries))
})
t.Run("outside DA period", func(t *testing.T) {
dataColumnStorage := filesystem.NewEphemeralDataColumnStorage(t)
dataColumnParamsByBlockRoot := []util.DataColumnParam{
{Slot: 1, Index: 1},
}
var current primitives.Slot = 1_000_000
sr := mockShouldRetain(slots.ToEpoch(current))
roSidecars, _ := util.CreateTestVerifiedRoDataColumnSidecars(t, dataColumnParamsByBlockRoot)
lazilyPersistentStoreColumns := NewLazilyPersistentStoreColumn(dataColumnStorage, nil, enode.ID{}, 0, nil, sr)
err := lazilyPersistentStoreColumns.Persist(current, roSidecars...)
require.NoError(t, err)
require.Equal(t, len(roSidecars), len(lazilyPersistentStoreColumns.cache.entries))
})
t.Run("nominal", func(t *testing.T) {
const slot = 42
store := filesystem.NewEphemeralDataColumnStorage(t)
dataColumnParamsByBlockRoot := []util.DataColumnParam{
{Slot: slot, Index: 1},
{Slot: slot, Index: 5},
}
roSidecars, roDataColumns := util.CreateTestVerifiedRoDataColumnSidecars(t, dataColumnParamsByBlockRoot)
avs := NewLazilyPersistentStoreColumn(store, nil, enode.ID{}, 0, nil, mockShouldRetain(slots.ToEpoch(slot)))
err := avs.Persist(slot, roSidecars...)
require.NoError(t, err)
require.Equal(t, 1, len(avs.cache.entries))
key := cacheKey{slot: slot, root: roDataColumns[0].BlockRoot()}
entry, ok := avs.cache.entries[key]
require.Equal(t, true, ok)
summary := store.Summary(key.root)
// A call to Persist does NOT save the sidecars to disk.
require.Equal(t, uint64(0), summary.Count())
require.Equal(t, len(roSidecars), len(entry.scs))
idx1 := entry.scs[1]
require.NotNil(t, idx1)
require.DeepSSZEqual(t, roDataColumns[0].BlockRoot(), idx1.BlockRoot())
idx5 := entry.scs[5]
require.NotNil(t, idx5)
require.DeepSSZEqual(t, roDataColumns[1].BlockRoot(), idx5.BlockRoot())
for i, roDataColumn := range entry.scs {
if map[uint64]bool{1: true, 5: true}[i] {
continue
}
require.IsNil(t, roDataColumn)
}
})
}
func TestIsDataAvailable(t *testing.T) {
params.SetupTestConfigCleanup(t)
params.BeaconConfig().FuluForkEpoch = params.BeaconConfig().ElectraForkEpoch + 4096*2
newDataColumnsVerifier := func(dataColumnSidecars []blocks.RODataColumn, _ []verification.Requirement) verification.DataColumnsVerifier {
return &mockDataColumnsVerifier{t: t, dataColumnSidecars: dataColumnSidecars}
}
ctx := t.Context()
t.Run("without commitments", func(t *testing.T) {
signedBeaconBlockFulu := util.NewBeaconBlockFulu()
signedRoBlock := newSignedRoBlock(t, signedBeaconBlockFulu)
dataColumnStorage := filesystem.NewEphemeralDataColumnStorage(t)
lazilyPersistentStoreColumns := NewLazilyPersistentStoreColumn(dataColumnStorage, newDataColumnsVerifier, enode.ID{}, 0, nil, mockShouldRetain(0))
err := lazilyPersistentStoreColumns.IsDataAvailable(ctx, 0, signedRoBlock)
require.NoError(t, err)
})
t.Run("with commitments", func(t *testing.T) {
signedBeaconBlockFulu := util.NewBeaconBlockFulu()
signedBeaconBlockFulu.Block.Slot = primitives.Slot(params.BeaconConfig().FuluForkEpoch) * params.BeaconConfig().SlotsPerEpoch
signedBeaconBlockFulu.Block.Body.BlobKzgCommitments = commitments
signedRoBlock := newSignedRoBlock(t, signedBeaconBlockFulu)
block := signedRoBlock.Block()
slot := block.Slot()
proposerIndex := block.ProposerIndex()
parentRoot := block.ParentRoot()
stateRoot := block.StateRoot()
bodyRoot, err := block.Body().HashTreeRoot()
require.NoError(t, err)
root := signedRoBlock.Root()
storage := filesystem.NewEphemeralDataColumnStorage(t)
indices := []uint64{1, 17, 19, 42, 75, 87, 102, 117}
avs := NewLazilyPersistentStoreColumn(storage, newDataColumnsVerifier, enode.ID{}, uint64(len(indices)), nil, mockShouldRetain(slots.ToEpoch(slot)))
dcparams := make([]util.DataColumnParam, 0, len(indices))
for _, index := range indices {
dataColumnParams := util.DataColumnParam{
Index: index,
KzgCommitments: commitments,
Slot: slot,
ProposerIndex: proposerIndex,
ParentRoot: parentRoot[:],
StateRoot: stateRoot[:],
BodyRoot: bodyRoot[:],
}
dcparams = append(dcparams, dataColumnParams)
}
_, verifiedRoDataColumns := util.CreateTestVerifiedRoDataColumnSidecars(t, dcparams)
key := keyFromBlock(signedRoBlock)
entry := avs.cache.entry(key)
defer avs.cache.delete(key)
for _, verifiedRoDataColumn := range verifiedRoDataColumns {
err := entry.stash(verifiedRoDataColumn.RODataColumn)
require.NoError(t, err)
}
err = avs.IsDataAvailable(ctx, slot, signedRoBlock)
require.NoError(t, err)
actual, err := storage.Get(root, indices)
require.NoError(t, err)
//summary := storage.Summary(root)
require.Equal(t, len(verifiedRoDataColumns), len(actual))
//require.Equal(t, uint64(len(indices)), summary.Count())
//require.DeepSSZEqual(t, verifiedRoDataColumns, actual)
})
}
func TestRetentionWindow(t *testing.T) {
windowSlots, err := slots.EpochEnd(params.BeaconConfig().MinEpochsForDataColumnSidecarsRequest)
require.NoError(t, err)
fuluSlot, err := slots.EpochStart(params.BeaconConfig().FuluForkEpoch)
require.NoError(t, err)
numberOfColumns := fieldparams.NumberOfColumns
testCases := []struct {
name string
commitments [][]byte
block func(*testing.T) blocks.ROBlock
slot primitives.Slot
wantedCols int
}{
{
name: "Pre-Fulu block",
block: func(t *testing.T) blocks.ROBlock {
return newSignedRoBlock(t, util.NewBeaconBlockElectra())
},
},
{
name: "Commitments outside data availability window",
block: func(t *testing.T) blocks.ROBlock {
beaconBlockElectra := util.NewBeaconBlockElectra()
// Block is from slot 0, "current slot" is window size +1 (so outside the window)
beaconBlockElectra.Block.Body.BlobKzgCommitments = commitments
return newSignedRoBlock(t, beaconBlockElectra)
},
slot: fuluSlot + windowSlots,
},
{
name: "Commitments within data availability window",
block: func(t *testing.T) blocks.ROBlock {
signedBeaconBlockFulu := util.NewBeaconBlockFulu()
signedBeaconBlockFulu.Block.Body.BlobKzgCommitments = commitments
signedBeaconBlockFulu.Block.Slot = fuluSlot + windowSlots - 1
return newSignedRoBlock(t, signedBeaconBlockFulu)
},
commitments: commitments,
slot: fuluSlot + windowSlots,
wantedCols: numberOfColumns,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
b := tc.block(t)
s := NewLazilyPersistentStoreColumn(nil, nil, enode.ID{}, uint64(numberOfColumns), nil, mockShouldRetain(slots.ToEpoch(tc.slot)))
indices, err := s.required(b)
require.NoError(t, err)
require.Equal(t, tc.wantedCols, len(indices))
})
}
}
func newSignedRoBlock(t *testing.T, signedBeaconBlock any) blocks.ROBlock {
sb, err := blocks.NewSignedBeaconBlock(signedBeaconBlock)
require.NoError(t, err)
rb, err := blocks.NewROBlock(sb)
require.NoError(t, err)
return rb
}
type mockDataColumnsVerifier struct {
t *testing.T
dataColumnSidecars []blocks.RODataColumn
validCalled, SidecarInclusionProvenCalled, SidecarKzgProofVerifiedCalled bool
}
var _ verification.DataColumnsVerifier = &mockDataColumnsVerifier{}
func (m *mockDataColumnsVerifier) VerifiedRODataColumns() ([]blocks.VerifiedRODataColumn, error) {
require.Equal(m.t, true, m.validCalled && m.SidecarInclusionProvenCalled && m.SidecarKzgProofVerifiedCalled)
verifiedDataColumnSidecars := make([]blocks.VerifiedRODataColumn, 0, len(m.dataColumnSidecars))
for _, dataColumnSidecar := range m.dataColumnSidecars {
verifiedDataColumnSidecar := blocks.NewVerifiedRODataColumn(dataColumnSidecar)
verifiedDataColumnSidecars = append(verifiedDataColumnSidecars, verifiedDataColumnSidecar)
}
return verifiedDataColumnSidecars, nil
}
func (m *mockDataColumnsVerifier) SatisfyRequirement(verification.Requirement) {}
func (m *mockDataColumnsVerifier) ValidFields() error {
m.validCalled = true
return nil
}
func (m *mockDataColumnsVerifier) CorrectSubnet(dataColumnSidecarSubTopic string, expectedTopics []string) error {
return nil
}
func (m *mockDataColumnsVerifier) NotFromFutureSlot() error { return nil }
func (m *mockDataColumnsVerifier) SlotAboveFinalized() error { return nil }
func (m *mockDataColumnsVerifier) ValidProposerSignature(ctx context.Context) error { return nil }
func (m *mockDataColumnsVerifier) SidecarParentSeen(parentSeen func([fieldparams.RootLength]byte) bool) error {
return nil
}
func (m *mockDataColumnsVerifier) SidecarParentValid(badParent func([fieldparams.RootLength]byte) bool) error {
return nil
}
func (m *mockDataColumnsVerifier) SidecarParentSlotLower() error { return nil }
func (m *mockDataColumnsVerifier) SidecarDescendsFromFinalized() error { return nil }
func (m *mockDataColumnsVerifier) SidecarInclusionProven() error {
m.SidecarInclusionProvenCalled = true
return nil
}
func (m *mockDataColumnsVerifier) SidecarKzgProofVerified() error {
m.SidecarKzgProofVerifiedCalled = true
return nil
}
func (m *mockDataColumnsVerifier) SidecarProposerExpected(ctx context.Context) error { return nil }
// Mock implementations for bisectVerification tests
// mockBisectionIterator simulates a BisectionIterator for testing.
type mockBisectionIterator struct {
chunks [][]blocks.RODataColumn
chunkErrors []error
finalError error
chunkIndex int
nextCallCount int
onErrorCallCount int
onErrorErrors []error
}
func (m *mockBisectionIterator) Next() ([]blocks.RODataColumn, error) {
if m.chunkIndex >= len(m.chunks) {
return nil, io.EOF
}
chunk := m.chunks[m.chunkIndex]
var err error
if m.chunkIndex < len(m.chunkErrors) {
err = m.chunkErrors[m.chunkIndex]
}
m.chunkIndex++
m.nextCallCount++
if err != nil {
return chunk, err
}
return chunk, nil
}
func (m *mockBisectionIterator) OnError(err error) {
m.onErrorCallCount++
m.onErrorErrors = append(m.onErrorErrors, err)
}
func (m *mockBisectionIterator) Error() error {
return m.finalError
}
// mockBisector simulates a Bisector for testing.
type mockBisector struct {
shouldError bool
bisectErr error
iterator *mockBisectionIterator
}
func (m *mockBisector) Bisect(columns []blocks.RODataColumn) (BisectionIterator, error) {
if m.shouldError {
return nil, m.bisectErr
}
return m.iterator, nil
}
// testDataColumnsVerifier implements verification.DataColumnsVerifier for testing.
type testDataColumnsVerifier struct {
t *testing.T
shouldFail bool
columns []blocks.RODataColumn
}
func (v *testDataColumnsVerifier) VerifiedRODataColumns() ([]blocks.VerifiedRODataColumn, error) {
verified := make([]blocks.VerifiedRODataColumn, len(v.columns))
for i, col := range v.columns {
verified[i] = blocks.NewVerifiedRODataColumn(col)
}
return verified, nil
}
func (v *testDataColumnsVerifier) SatisfyRequirement(verification.Requirement) {}
func (v *testDataColumnsVerifier) ValidFields() error {
if v.shouldFail {
return errors.New("verification failed")
}
return nil
}
func (v *testDataColumnsVerifier) CorrectSubnet(string, []string) error { return nil }
func (v *testDataColumnsVerifier) NotFromFutureSlot() error { return nil }
func (v *testDataColumnsVerifier) SlotAboveFinalized() error { return nil }
func (v *testDataColumnsVerifier) ValidProposerSignature(context.Context) error { return nil }
func (v *testDataColumnsVerifier) SidecarParentSeen(func([fieldparams.RootLength]byte) bool) error {
return nil
}
func (v *testDataColumnsVerifier) SidecarParentValid(func([fieldparams.RootLength]byte) bool) error {
return nil
}
func (v *testDataColumnsVerifier) SidecarParentSlotLower() error { return nil }
func (v *testDataColumnsVerifier) SidecarDescendsFromFinalized() error { return nil }
func (v *testDataColumnsVerifier) SidecarInclusionProven() error { return nil }
func (v *testDataColumnsVerifier) SidecarKzgProofVerified() error { return nil }
func (v *testDataColumnsVerifier) SidecarProposerExpected(context.Context) error { return nil }
// Helper function to create test data columns
func makeTestDataColumns(t *testing.T, count int, blockRoot [32]byte, startIndex uint64) []blocks.RODataColumn {
columns := make([]blocks.RODataColumn, 0, count)
for i := range count {
params := util.DataColumnParam{
Index: startIndex + uint64(i),
KzgCommitments: commitments,
Slot: primitives.Slot(params.BeaconConfig().FuluForkEpoch) * params.BeaconConfig().SlotsPerEpoch,
}
_, verifiedCols := util.CreateTestVerifiedRoDataColumnSidecars(t, []util.DataColumnParam{params})
if len(verifiedCols) > 0 {
columns = append(columns, verifiedCols[0].RODataColumn)
}
}
return columns
}
// Helper function to create test verifier factory with failure pattern
func makeTestVerifierFactory(failurePattern []bool) verification.NewDataColumnsVerifier {
callIndex := 0
return func(cols []blocks.RODataColumn, _ []verification.Requirement) verification.DataColumnsVerifier {
shouldFail := callIndex < len(failurePattern) && failurePattern[callIndex]
callIndex++
return &testDataColumnsVerifier{
shouldFail: shouldFail,
columns: cols,
}
}
}
// TestBisectVerification tests the bisectVerification method with comprehensive table-driven test cases.
func TestBisectVerification(t *testing.T) {
params.SetupTestConfigCleanup(t)
params.BeaconConfig().FuluForkEpoch = params.BeaconConfig().ElectraForkEpoch + 4096*2
cases := []struct {
expectedError bool
bisectorNil bool
expectedOnErrorCallCount int
expectedNextCallCount int
inputCount int
iteratorFinalError error
bisectorError error
name string
storedColumnIndices []uint64
verificationFailurePattern []bool
chunkErrors []error
chunks [][]blocks.RODataColumn
}{
{
name: "EmptyColumns",
inputCount: 0,
expectedError: false,
expectedNextCallCount: 0,
expectedOnErrorCallCount: 0,
},
{
name: "NilBisector",
inputCount: 3,
bisectorNil: true,
expectedError: true,
expectedNextCallCount: 0,
expectedOnErrorCallCount: 0,
},
{
name: "BisectError",
inputCount: 5,
bisectorError: errors.New("bisect failed"),
expectedError: true,
expectedNextCallCount: 0,
expectedOnErrorCallCount: 0,
},
{
name: "SingleChunkSuccess",
inputCount: 4,
chunks: [][]blocks.RODataColumn{{}},
verificationFailurePattern: []bool{false},
expectedError: false,
expectedNextCallCount: 2,
expectedOnErrorCallCount: 0,
},
{
name: "SingleChunkFails",
inputCount: 4,
chunks: [][]blocks.RODataColumn{{}},
verificationFailurePattern: []bool{true},
iteratorFinalError: errors.New("chunk failed"),
expectedError: true,
expectedNextCallCount: 2,
expectedOnErrorCallCount: 1,
},
{
name: "TwoChunks_BothPass",
inputCount: 8,
chunks: [][]blocks.RODataColumn{{}, {}},
verificationFailurePattern: []bool{false, false},
expectedError: false,
expectedNextCallCount: 3,
expectedOnErrorCallCount: 0,
},
{
name: "TwoChunks_FirstFails",
inputCount: 8,
chunks: [][]blocks.RODataColumn{{}, {}},
verificationFailurePattern: []bool{true, false},
iteratorFinalError: errors.New("first failed"),
expectedError: true,
expectedNextCallCount: 3,
expectedOnErrorCallCount: 1,
},
{
name: "TwoChunks_SecondFails",
inputCount: 8,
chunks: [][]blocks.RODataColumn{{}, {}},
verificationFailurePattern: []bool{false, true},
iteratorFinalError: errors.New("second failed"),
expectedError: true,
expectedNextCallCount: 3,
expectedOnErrorCallCount: 1,
},
{
name: "TwoChunks_BothFail",
inputCount: 8,
chunks: [][]blocks.RODataColumn{{}, {}},
verificationFailurePattern: []bool{true, true},
iteratorFinalError: errors.New("both failed"),
expectedError: true,
expectedNextCallCount: 3,
expectedOnErrorCallCount: 2,
},
{
name: "ManyChunks_AllPass",
inputCount: 16,
chunks: [][]blocks.RODataColumn{{}, {}, {}, {}},
verificationFailurePattern: []bool{false, false, false, false},
expectedError: false,
expectedNextCallCount: 5,
expectedOnErrorCallCount: 0,
},
{
name: "ManyChunks_MixedFail",
inputCount: 16,
chunks: [][]blocks.RODataColumn{{}, {}, {}, {}},
verificationFailurePattern: []bool{false, true, false, true},
iteratorFinalError: errors.New("mixed failures"),
expectedError: true,
expectedNextCallCount: 5,
expectedOnErrorCallCount: 2,
},
{
name: "FilterStoredColumns_PartialFilter",
inputCount: 6,
chunks: [][]blocks.RODataColumn{{}},
verificationFailurePattern: []bool{false},
storedColumnIndices: []uint64{1, 3},
expectedError: false,
expectedNextCallCount: 2,
expectedOnErrorCallCount: 0,
},
{
name: "FilterStoredColumns_AllStored",
inputCount: 6,
chunks: [][]blocks.RODataColumn{{}},
verificationFailurePattern: []bool{false},
storedColumnIndices: []uint64{0, 1, 2, 3, 4, 5},
expectedError: false,
expectedNextCallCount: 2,
expectedOnErrorCallCount: 0,
},
{
name: "FilterStoredColumns_MixedAccess",
inputCount: 10,
chunks: [][]blocks.RODataColumn{{}},
verificationFailurePattern: []bool{false},
storedColumnIndices: []uint64{1, 5, 9},
expectedError: false,
expectedNextCallCount: 2,
expectedOnErrorCallCount: 0,
},
{
name: "IteratorNextError",
inputCount: 4,
chunks: [][]blocks.RODataColumn{{}, {}},
chunkErrors: []error{nil, errors.New("next error")},
verificationFailurePattern: []bool{false},
expectedError: true,
expectedNextCallCount: 2,
expectedOnErrorCallCount: 0,
},
{
name: "IteratorNextEOF",
inputCount: 4,
chunks: [][]blocks.RODataColumn{{}},
verificationFailurePattern: []bool{false},
expectedError: false,
expectedNextCallCount: 2,
expectedOnErrorCallCount: 0,
},
{
name: "LargeChunkSize",
inputCount: 128,
chunks: [][]blocks.RODataColumn{{}},
verificationFailurePattern: []bool{false},
expectedError: false,
expectedNextCallCount: 2,
expectedOnErrorCallCount: 0,
},
{
name: "ManySmallChunks",
inputCount: 32,
chunks: [][]blocks.RODataColumn{{}, {}, {}, {}, {}, {}, {}, {}},
verificationFailurePattern: []bool{false, false, false, false, false, false, false, false},
expectedError: false,
expectedNextCallCount: 9,
expectedOnErrorCallCount: 0,
},
{
name: "ChunkWithSomeStoredColumns",
inputCount: 6,
chunks: [][]blocks.RODataColumn{{}},
verificationFailurePattern: []bool{false},
storedColumnIndices: []uint64{0, 2, 4},
expectedError: false,
expectedNextCallCount: 2,
expectedOnErrorCallCount: 0,
},
{
name: "OnErrorDoesNotStopIteration",
inputCount: 8,
chunks: [][]blocks.RODataColumn{{}, {}},
verificationFailurePattern: []bool{true, false},
iteratorFinalError: errors.New("first failed"),
expectedError: true,
expectedNextCallCount: 3,
expectedOnErrorCallCount: 1,
},
{
name: "VerificationErrorWrapping",
inputCount: 4,
chunks: [][]blocks.RODataColumn{{}},
verificationFailurePattern: []bool{true},
iteratorFinalError: errors.New("verification failed"),
expectedError: true,
expectedNextCallCount: 2,
expectedOnErrorCallCount: 1,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
// Setup storage
var store *filesystem.DataColumnStorage
if len(tc.storedColumnIndices) > 0 {
mocker, s := filesystem.NewEphemeralDataColumnStorageWithMocker(t)
blockRoot := [32]byte{1, 2, 3}
slot := primitives.Slot(params.BeaconConfig().FuluForkEpoch) * params.BeaconConfig().SlotsPerEpoch
require.NoError(t, mocker.CreateFakeIndices(blockRoot, slot, tc.storedColumnIndices...))
store = s
} else {
store = filesystem.NewEphemeralDataColumnStorage(t)
}
// Create test columns
blockRoot := [32]byte{1, 2, 3}
columns := makeTestDataColumns(t, tc.inputCount, blockRoot, 0)
// Setup iterator with chunks
iterator := &mockBisectionIterator{
chunks: tc.chunks,
chunkErrors: tc.chunkErrors,
finalError: tc.iteratorFinalError,
}
// Setup bisector
var bisector Bisector
if tc.bisectorNil || tc.inputCount == 0 {
bisector = nil
} else if tc.bisectorError != nil {
bisector = &mockBisector{
shouldError: true,
bisectErr: tc.bisectorError,
}
} else {
bisector = &mockBisector{
shouldError: false,
iterator: iterator,
}
}
// Create store with verifier
verifierFactory := makeTestVerifierFactory(tc.verificationFailurePattern)
lazilyPersistentStore := &LazilyPersistentStoreColumn{
store: store,
cache: newDataColumnCache(),
newDataColumnsVerifier: verifierFactory,
custody: &custodyRequirement{},
bisector: bisector,
}
// Execute
err := lazilyPersistentStore.bisectVerification(columns)
// Assert
if tc.expectedError {
require.NotNil(t, err)
} else {
require.NoError(t, err)
}
// Verify iterator interactions for non-error cases
if tc.inputCount > 0 && bisector != nil && tc.bisectorError == nil && !tc.expectedError {
require.NotEqual(t, 0, iterator.nextCallCount, "iterator Next() should have been called")
require.Equal(t, tc.expectedOnErrorCallCount, iterator.onErrorCallCount, "OnError() call count mismatch")
}
})
}
}
func allIndicesExcept(total int, excluded []uint64) []uint64 {
excludeMap := make(map[uint64]bool)
for _, idx := range excluded {
excludeMap[idx] = true
}
var result []uint64
for i := range total {
if !excludeMap[uint64(i)] {
result = append(result, uint64(i))
}
}
return result
}
// TestColumnsNotStored tests the columnsNotStored method.
func TestColumnsNotStored(t *testing.T) {
params.SetupTestConfigCleanup(t)
params.BeaconConfig().FuluForkEpoch = params.BeaconConfig().ElectraForkEpoch + 4096*2
cases := []struct {
name string
count int
stored []uint64 // Column indices marked as stored
expected []uint64 // Expected column indices in returned result
}{
// Empty cases
{
name: "EmptyInput",
count: 0,
stored: []uint64{},
expected: []uint64{},
},
// Single element cases
{
name: "SingleElement_NotStored",
count: 1,
stored: []uint64{},
expected: []uint64{0},
},
{
name: "SingleElement_Stored",
count: 1,
stored: []uint64{0},
expected: []uint64{},
},
// All not stored cases
{
name: "AllNotStored_FiveElements",
count: 5,
stored: []uint64{},
expected: []uint64{0, 1, 2, 3, 4},
},
// All stored cases
{
name: "AllStored",
count: 5,
stored: []uint64{0, 1, 2, 3, 4},
expected: []uint64{},
},
// Partial storage - beginning
{
name: "StoredAtBeginning",
count: 5,
stored: []uint64{0, 1},
expected: []uint64{2, 3, 4},
},
// Partial storage - end
{
name: "StoredAtEnd",
count: 5,
stored: []uint64{3, 4},
expected: []uint64{0, 1, 2},
},
// Partial storage - middle
{
name: "StoredInMiddle",
count: 5,
stored: []uint64{2},
expected: []uint64{0, 1, 3, 4},
},
// Partial storage - scattered
{
name: "StoredScattered",
count: 8,
stored: []uint64{1, 3, 5},
expected: []uint64{0, 2, 4, 6, 7},
},
// Alternating pattern
{
name: "AlternatingPattern",
count: 8,
stored: []uint64{0, 2, 4, 6},
expected: []uint64{1, 3, 5, 7},
},
// Consecutive stored
{
name: "ConsecutiveStored",
count: 10,
stored: []uint64{3, 4, 5, 6},
expected: []uint64{0, 1, 2, 7, 8, 9},
},
// Large slice cases
{
name: "LargeSlice_NoStored",
count: 64,
stored: []uint64{},
expected: allIndicesExcept(64, []uint64{}),
},
{
name: "LargeSlice_SingleStored",
count: 64,
stored: []uint64{32},
expected: allIndicesExcept(64, []uint64{32}),
},
}
slot := primitives.Slot(params.BeaconConfig().FuluForkEpoch) * params.BeaconConfig().SlotsPerEpoch
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
// Create test columns first to get the actual block root
var columns []blocks.RODataColumn
if tc.count > 0 {
columns = makeTestDataColumns(t, tc.count, [32]byte{}, 0)
}
// Get the actual block root from the first column (if any)
var blockRoot [32]byte
if len(columns) > 0 {
blockRoot = columns[0].BlockRoot()
}
// Setup storage
var store *filesystem.DataColumnStorage
if len(tc.stored) > 0 {
mocker, s := filesystem.NewEphemeralDataColumnStorageWithMocker(t)
require.NoError(t, mocker.CreateFakeIndices(blockRoot, slot, tc.stored...))
store = s
} else {
store = filesystem.NewEphemeralDataColumnStorage(t)
}
// Create store instance
lazilyPersistentStore := &LazilyPersistentStoreColumn{
store: store,
}
// Execute
result := lazilyPersistentStore.columnsNotStored(columns)
// Assert count
require.Equal(t, len(tc.expected), len(result),
fmt.Sprintf("expected %d columns, got %d", len(tc.expected), len(result)))
// Verify that no stored columns are in the result
if len(tc.stored) > 0 {
resultIndices := make(map[uint64]bool)
for _, col := range result {
resultIndices[col.Index] = true
}
for _, storedIdx := range tc.stored {
require.Equal(t, false, resultIndices[storedIdx],
fmt.Sprintf("stored column index %d should not be in result", storedIdx))
}
}
// If expectedIndices is specified, verify the exact column indices in order
if len(tc.expected) > 0 && len(tc.stored) == 0 {
// Only check exact order for non-stored cases (where we know they stay in same order)
for i, expectedIdx := range tc.expected {
require.Equal(t, columns[expectedIdx].Index, result[i].Index,
fmt.Sprintf("column %d: expected index %d, got %d", i, columns[expectedIdx].Index, result[i].Index))
}
}
// Verify optimization: if nothing stored, should return original slice
if len(tc.stored) == 0 && tc.count > 0 {
require.Equal(t, &columns[0], &result[0],
"when no columns stored, should return original slice (same pointer)")
}
// Verify optimization: if some stored, result should use in-place shifting
if len(tc.stored) > 0 && len(tc.expected) > 0 && tc.count > 0 {
require.Equal(t, cap(columns), cap(result),
"result should be in-place shifted from original (same capacity)")
}
})
}
}

View File

@@ -0,0 +1,40 @@
package das
import (
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
)
// Bisector describes a type that takes a set of RODataColumns via the Bisect method
// and returns a BisectionIterator that returns batches of those columns to be
// verified together.
type Bisector interface {
// Bisect initializes the BisectionIterator and returns the result.
Bisect([]blocks.RODataColumn) (BisectionIterator, error)
}
// BisectionIterator describes an iterator that returns groups of columns to verify.
// It is up to the bisector implementation to decide how to chunk up the columns,
// whether by block, by peer, or any other strategy. For example, backfill implements
// a bisector that keeps track of the source of each sidecar by peer, and groups
// sidecars by peer in the Next method, enabling it to track which peers, out of all
// the peers contributing to a batch, gave us bad data.
// When a batch fails, the OnError method should be used so that the bisector can
// keep track of the failed groups of columns and eg apply that knowledge in peer scoring.
// The same column will be returned multiple times by Next; first as part of a larger batch,
// and again as part of a more fine grained batch if there was an error in the large batch.
// For example, first as part of a batch of all columns spanning peers, and then again
// as part of a batch of columns from a single peer if some column in the larger batch
// failed verification.
type BisectionIterator interface {
// Next returns the next group of columns to verify.
// When the iteration is complete, Next should return (nil, io.EOF).
Next() ([]blocks.RODataColumn, error)
// OnError should be called when verification of a group of columns obtained via Next() fails.
OnError(error)
// Error can be used at the end of the iteration to get a single error result. It will return
// nil if OnError was never called, or an error of the implementers choosing representing the set
// of errors seen during iteration. For instance when bisecting from columns spanning peers to columns
// from a single peer, the broader error could be dropped, and then the more specific error
// (for a single peer's response) returned after bisecting to it.
Error() error
}

View File

@@ -76,7 +76,7 @@ func (e *blobCacheEntry) stash(sc *blocks.ROBlob) error {
e.scs = make([]*blocks.ROBlob, maxBlobsPerBlock)
}
if e.scs[sc.Index] != nil {
return errors.Wrapf(ErrDuplicateSidecar, "root=%#x, index=%d, commitment=%#x", sc.BlockRoot(), sc.Index, sc.KzgCommitment)
return errors.Wrapf(errDuplicateSidecar, "root=%#x, index=%d, commitment=%#x", sc.BlockRoot(), sc.Index, sc.KzgCommitment)
}
e.scs[sc.Index] = sc
return nil
@@ -88,7 +88,7 @@ func (e *blobCacheEntry) stash(sc *blocks.ROBlob) error {
// commitments were found in the cache and the sidecar slice return value can be used
// to perform a DA check against the cached sidecars.
// filter only returns blobs that need to be checked. Blobs already available on disk will be excluded.
func (e *blobCacheEntry) filter(root [32]byte, kc [][]byte, slot primitives.Slot) ([]blocks.ROBlob, error) {
func (e *blobCacheEntry) filter(root [32]byte, kc [][]byte) ([]blocks.ROBlob, error) {
count := len(kc)
if e.diskSummary.AllAvailable(count) {
return nil, nil

View File

@@ -34,7 +34,8 @@ type filterTestCaseSetupFunc func(t *testing.T) (*blobCacheEntry, [][]byte, []bl
func filterTestCaseSetup(slot primitives.Slot, nBlobs int, onDisk []int, numExpected int) filterTestCaseSetupFunc {
return func(t *testing.T) (*blobCacheEntry, [][]byte, []blocks.ROBlob) {
blk, blobs := util.GenerateTestDenebBlockWithSidecar(t, [32]byte{}, slot, nBlobs)
commits, err := commitmentsToCheck(blk, blk.Block().Slot())
shouldRetain := func(s primitives.Slot) bool { return true }
commits, err := commitmentsToCheck(blk, shouldRetain)
require.NoError(t, err)
entry := &blobCacheEntry{}
if len(onDisk) > 0 {
@@ -113,7 +114,7 @@ func TestFilterDiskSummary(t *testing.T) {
t.Run(c.name, func(t *testing.T) {
entry, commits, expected := c.setup(t)
// first (root) argument doesn't matter, it is just for logs
got, err := entry.filter([32]byte{}, commits, 100)
got, err := entry.filter([32]byte{}, commits)
require.NoError(t, err)
require.Equal(t, len(expected), len(got))
})
@@ -195,7 +196,7 @@ func TestFilter(t *testing.T) {
t.Run(c.name, func(t *testing.T) {
entry, commits, expected := c.setup(t)
// first (root) argument doesn't matter, it is just for logs
got, err := entry.filter([32]byte{}, commits, 100)
got, err := entry.filter([32]byte{}, commits)
if c.err != nil {
require.ErrorIs(t, err, c.err)
return

View File

@@ -1,9 +1,7 @@
package das
import (
"bytes"
"slices"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/peerdas"
"github.com/OffchainLabs/prysm/v7/beacon-chain/db/filesystem"
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
@@ -11,9 +9,9 @@ import (
)
var (
ErrDuplicateSidecar = errors.New("duplicate sidecar stashed in AvailabilityStore")
errDuplicateSidecar = errors.New("duplicate sidecar stashed in AvailabilityStore")
errColumnIndexTooHigh = errors.New("column index too high")
errCommitmentMismatch = errors.New("KzgCommitment of sidecar in cache did not match block commitment")
errCommitmentMismatch = errors.New("commitment of sidecar in cache did not match block commitment")
errMissingSidecar = errors.New("no sidecar in cache for block commitment")
)
@@ -25,107 +23,80 @@ func newDataColumnCache() *dataColumnCache {
return &dataColumnCache{entries: make(map[cacheKey]*dataColumnCacheEntry)}
}
// ensure returns the entry for the given key, creating it if it isn't already present.
func (c *dataColumnCache) ensure(key cacheKey) *dataColumnCacheEntry {
// entry returns the entry for the given key, creating it if it isn't already present.
func (c *dataColumnCache) entry(key cacheKey) *dataColumnCacheEntry {
entry, ok := c.entries[key]
if !ok {
entry = &dataColumnCacheEntry{}
entry = newDataColumnCacheEntry(key.root)
c.entries[key] = entry
}
return entry
}
func (c *dataColumnCache) cleanup(blks []blocks.ROBlock) {
for _, block := range blks {
key := cacheKey{slot: block.Block().Slot(), root: block.Root()}
c.delete(key)
}
}
// delete removes the cache entry from the cache.
func (c *dataColumnCache) delete(key cacheKey) {
delete(c.entries, key)
}
// dataColumnCacheEntry holds a fixed-length cache of BlobSidecars.
type dataColumnCacheEntry struct {
scs [fieldparams.NumberOfColumns]*blocks.RODataColumn
diskSummary filesystem.DataColumnStorageSummary
func (c *dataColumnCache) stash(sc blocks.RODataColumn) error {
key := cacheKey{slot: sc.Slot(), root: sc.BlockRoot()}
entry := c.entry(key)
return entry.stash(sc)
}
func (e *dataColumnCacheEntry) setDiskSummary(sum filesystem.DataColumnStorageSummary) {
e.diskSummary = sum
func newDataColumnCacheEntry(root [32]byte) *dataColumnCacheEntry {
return &dataColumnCacheEntry{scs: make(map[uint64]blocks.RODataColumn), root: &root}
}
// dataColumnCacheEntry is the set of RODataColumns for a given block.
type dataColumnCacheEntry struct {
root *[32]byte
scs map[uint64]blocks.RODataColumn
}
// stash adds an item to the in-memory cache of DataColumnSidecars.
// Only the first DataColumnSidecar of a given Index will be kept in the cache.
// stash will return an error if the given data colunn is already in the cache, or if the Index is out of bounds.
func (e *dataColumnCacheEntry) stash(sc *blocks.RODataColumn) error {
// stash will return an error if the given data column Index is out of bounds.
// It will overwrite any existing entry for the same index.
func (e *dataColumnCacheEntry) stash(sc blocks.RODataColumn) error {
if sc.Index >= fieldparams.NumberOfColumns {
return errors.Wrapf(errColumnIndexTooHigh, "index=%d", sc.Index)
}
if e.scs[sc.Index] != nil {
return errors.Wrapf(ErrDuplicateSidecar, "root=%#x, index=%d, commitment=%#x", sc.BlockRoot(), sc.Index, sc.KzgCommitments)
}
e.scs[sc.Index] = sc
return nil
}
func (e *dataColumnCacheEntry) filter(root [32]byte, commitmentsArray *safeCommitmentsArray) ([]blocks.RODataColumn, error) {
nonEmptyIndices := commitmentsArray.nonEmptyIndices()
if e.diskSummary.AllAvailable(nonEmptyIndices) {
return nil, nil
// append appends the requested root and indices from the cache to the given sidecars slice and returns the result.
// If any of the given indices are missing, an error will be returned and the sidecars slice will be unchanged.
func (e *dataColumnCacheEntry) append(sidecars []blocks.RODataColumn, indices peerdas.ColumnIndices) ([]blocks.RODataColumn, error) {
needed := indices.ToMap()
for col := range needed {
_, ok := e.scs[col]
if !ok {
return nil, errors.Wrapf(errMissingSidecar, "root=%#x, index=%#x", e.root, col)
}
}
commitmentsCount := commitmentsArray.count()
sidecars := make([]blocks.RODataColumn, 0, commitmentsCount)
for i := range nonEmptyIndices {
if e.diskSummary.HasIndex(i) {
continue
}
if e.scs[i] == nil {
return nil, errors.Wrapf(errMissingSidecar, "root=%#x, index=%#x", root, i)
}
if !sliceBytesEqual(commitmentsArray[i], e.scs[i].KzgCommitments) {
return nil, errors.Wrapf(errCommitmentMismatch, "root=%#x, index=%#x, commitment=%#x, block commitment=%#x", root, i, e.scs[i].KzgCommitments, commitmentsArray[i])
}
sidecars = append(sidecars, *e.scs[i])
// Loop twice so we can avoid touching the slice if any of the blobs are missing.
for col := range needed {
sidecars = append(sidecars, e.scs[col])
}
return sidecars, nil
}
// safeCommitmentsArray is a fixed size array of commitments.
// This is helpful for avoiding gratuitous bounds checks.
type safeCommitmentsArray [fieldparams.NumberOfColumns][][]byte
// count returns the number of commitments in the array.
func (s *safeCommitmentsArray) count() int {
count := 0
for i := range s {
if s[i] != nil {
count++
// IndicesNotStored filters the list of indices to only include those that are not found in the storage summary.
func IndicesNotStored(sum filesystem.DataColumnStorageSummary, indices peerdas.ColumnIndices) peerdas.ColumnIndices {
indices = indices.Copy()
for col := range indices {
if sum.HasIndex(col) {
indices.Unset(col)
}
}
return count
}
// nonEmptyIndices returns a map of indices that are non-nil in the array.
func (s *safeCommitmentsArray) nonEmptyIndices() map[uint64]bool {
columns := make(map[uint64]bool)
for i := range s {
if s[i] != nil {
columns[uint64(i)] = true
}
}
return columns
}
func sliceBytesEqual(a, b [][]byte) bool {
return slices.EqualFunc(a, b, bytes.Equal)
return indices
}

View File

@@ -1,8 +1,10 @@
package das
import (
"slices"
"testing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/peerdas"
"github.com/OffchainLabs/prysm/v7/beacon-chain/db/filesystem"
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
@@ -13,124 +15,105 @@ import (
func TestEnsureDeleteSetDiskSummary(t *testing.T) {
c := newDataColumnCache()
key := cacheKey{}
entry := c.ensure(key)
require.DeepEqual(t, dataColumnCacheEntry{}, *entry)
entry := c.entry(key)
require.Equal(t, 0, len(entry.scs))
diskSummary := filesystem.NewDataColumnStorageSummary(42, [fieldparams.NumberOfColumns]bool{true})
entry.setDiskSummary(diskSummary)
entry = c.ensure(key)
require.DeepEqual(t, dataColumnCacheEntry{diskSummary: diskSummary}, *entry)
nonDupe := c.entry(key)
require.Equal(t, entry, nonDupe) // same pointer
expect, _ := util.CreateTestVerifiedRoDataColumnSidecars(t, []util.DataColumnParam{{Index: 1}})
require.NoError(t, entry.stash(expect[0]))
require.Equal(t, 1, len(entry.scs))
cols, err := nonDupe.append([]blocks.RODataColumn{}, peerdas.NewColumnIndicesFromSlice([]uint64{expect[0].Index}))
require.NoError(t, err)
require.DeepEqual(t, expect[0], cols[0])
c.delete(key)
entry = c.ensure(key)
require.DeepEqual(t, dataColumnCacheEntry{}, *entry)
entry = c.entry(key)
require.Equal(t, 0, len(entry.scs))
require.NotEqual(t, entry, nonDupe) // different pointer
}
func TestStash(t *testing.T) {
t.Run("Index too high", func(t *testing.T) {
roDataColumns, _ := util.CreateTestVerifiedRoDataColumnSidecars(t, []util.DataColumnParam{{Index: 10_000}})
columns, _ := util.CreateTestVerifiedRoDataColumnSidecars(t, []util.DataColumnParam{{Index: 10_000}})
var entry dataColumnCacheEntry
err := entry.stash(&roDataColumns[0])
err := entry.stash(columns[0])
require.NotNil(t, err)
})
t.Run("Nominal and already existing", func(t *testing.T) {
roDataColumns, _ := util.CreateTestVerifiedRoDataColumnSidecars(t, []util.DataColumnParam{{Index: 1}})
var entry dataColumnCacheEntry
err := entry.stash(&roDataColumns[0])
entry := newDataColumnCacheEntry(roDataColumns[0].BlockRoot())
err := entry.stash(roDataColumns[0])
require.NoError(t, err)
require.DeepEqual(t, roDataColumns[0], entry.scs[1])
err = entry.stash(&roDataColumns[0])
require.NotNil(t, err)
require.NoError(t, entry.stash(roDataColumns[0]))
// stash simply replaces duplicate values now
require.DeepEqual(t, roDataColumns[0], entry.scs[1])
})
}
func TestFilterDataColumns(t *testing.T) {
func TestAppendDataColumns(t *testing.T) {
t.Run("All available", func(t *testing.T) {
commitmentsArray := safeCommitmentsArray{nil, [][]byte{[]byte{1}}, nil, [][]byte{[]byte{3}}}
diskSummary := filesystem.NewDataColumnStorageSummary(42, [fieldparams.NumberOfColumns]bool{false, true, false, true})
dataColumnCacheEntry := dataColumnCacheEntry{diskSummary: diskSummary}
actual, err := dataColumnCacheEntry.filter([fieldparams.RootLength]byte{}, &commitmentsArray)
sum := filesystem.NewDataColumnStorageSummary(42, [fieldparams.NumberOfColumns]bool{false, true, false, true})
notStored := IndicesNotStored(sum, peerdas.NewColumnIndicesFromSlice([]uint64{1, 3}))
actual, err := newDataColumnCacheEntry([32]byte{}).append([]blocks.RODataColumn{}, notStored)
require.NoError(t, err)
require.IsNil(t, actual)
require.Equal(t, 0, len(actual))
})
t.Run("Some scs missing", func(t *testing.T) {
commitmentsArray := safeCommitmentsArray{nil, [][]byte{[]byte{1}}}
sum := filesystem.NewDataColumnStorageSummary(42, [fieldparams.NumberOfColumns]bool{})
diskSummary := filesystem.NewDataColumnStorageSummary(42, [fieldparams.NumberOfColumns]bool{})
dataColumnCacheEntry := dataColumnCacheEntry{diskSummary: diskSummary}
_, err := dataColumnCacheEntry.filter([fieldparams.RootLength]byte{}, &commitmentsArray)
require.NotNil(t, err)
})
t.Run("Commitments not equal", func(t *testing.T) {
commitmentsArray := safeCommitmentsArray{nil, [][]byte{[]byte{1}}}
roDataColumns, _ := util.CreateTestVerifiedRoDataColumnSidecars(t, []util.DataColumnParam{{Index: 1}})
var scs [fieldparams.NumberOfColumns]*blocks.RODataColumn
scs[1] = &roDataColumns[0]
dataColumnCacheEntry := dataColumnCacheEntry{scs: scs}
_, err := dataColumnCacheEntry.filter(roDataColumns[0].BlockRoot(), &commitmentsArray)
notStored := IndicesNotStored(sum, peerdas.NewColumnIndicesFromSlice([]uint64{1}))
actual, err := newDataColumnCacheEntry([32]byte{}).append([]blocks.RODataColumn{}, notStored)
require.Equal(t, 0, len(actual))
require.NotNil(t, err)
})
t.Run("Nominal", func(t *testing.T) {
commitmentsArray := safeCommitmentsArray{nil, [][]byte{[]byte{1}}, nil, [][]byte{[]byte{3}}}
diskSummary := filesystem.NewDataColumnStorageSummary(42, [fieldparams.NumberOfColumns]bool{false, true})
indices := peerdas.NewColumnIndicesFromSlice([]uint64{1, 3})
expected, _ := util.CreateTestVerifiedRoDataColumnSidecars(t, []util.DataColumnParam{{Index: 3, KzgCommitments: [][]byte{[]byte{3}}}})
var scs [fieldparams.NumberOfColumns]*blocks.RODataColumn
scs[3] = &expected[0]
scs := map[uint64]blocks.RODataColumn{
3: expected[0],
}
sum := filesystem.NewDataColumnStorageSummary(42, [fieldparams.NumberOfColumns]bool{false, true})
entry := dataColumnCacheEntry{scs: scs}
dataColumnCacheEntry := dataColumnCacheEntry{scs: scs, diskSummary: diskSummary}
actual, err := dataColumnCacheEntry.filter(expected[0].BlockRoot(), &commitmentsArray)
actual, err := entry.append([]blocks.RODataColumn{}, IndicesNotStored(sum, indices))
require.NoError(t, err)
require.DeepEqual(t, expected, actual)
})
}
func TestCount(t *testing.T) {
s := safeCommitmentsArray{nil, [][]byte{[]byte{1}}, nil, [][]byte{[]byte{3}}}
require.Equal(t, 2, s.count())
}
t.Run("Append does not mutate the input", func(t *testing.T) {
indices := peerdas.NewColumnIndicesFromSlice([]uint64{1, 2})
expected, _ := util.CreateTestVerifiedRoDataColumnSidecars(t, []util.DataColumnParam{
{Index: 0, KzgCommitments: [][]byte{[]byte{1}}},
{Index: 1, KzgCommitments: [][]byte{[]byte{2}}},
{Index: 2, KzgCommitments: [][]byte{[]byte{3}}},
})
func TestNonEmptyIndices(t *testing.T) {
s := safeCommitmentsArray{nil, [][]byte{[]byte{10}}, nil, [][]byte{[]byte{20}}}
actual := s.nonEmptyIndices()
require.DeepEqual(t, map[uint64]bool{1: true, 3: true}, actual)
}
scs := map[uint64]blocks.RODataColumn{
1: expected[1],
2: expected[2],
}
entry := dataColumnCacheEntry{scs: scs}
func TestSliceBytesEqual(t *testing.T) {
t.Run("Different lengths", func(t *testing.T) {
a := [][]byte{[]byte{1, 2, 3}}
b := [][]byte{[]byte{1, 2, 3}, []byte{4, 5, 6}}
require.Equal(t, false, sliceBytesEqual(a, b))
})
t.Run("Same length but different content", func(t *testing.T) {
a := [][]byte{[]byte{1, 2, 3}, []byte{4, 5, 6}}
b := [][]byte{[]byte{1, 2, 3}, []byte{4, 5, 7}}
require.Equal(t, false, sliceBytesEqual(a, b))
})
t.Run("Equal slices", func(t *testing.T) {
a := [][]byte{[]byte{1, 2, 3}, []byte{4, 5, 6}}
b := [][]byte{[]byte{1, 2, 3}, []byte{4, 5, 6}}
require.Equal(t, true, sliceBytesEqual(a, b))
original := []blocks.RODataColumn{expected[0]}
actual, err := entry.append(original, indices)
require.NoError(t, err)
require.Equal(t, len(expected), len(actual))
slices.SortFunc(actual, func(i, j blocks.RODataColumn) int {
return int(i.Index) - int(j.Index)
})
for i := range expected {
require.Equal(t, expected[i].Index, actual[i].Index)
}
require.Equal(t, 1, len(original))
})
}

View File

@@ -7,13 +7,13 @@ import (
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
)
// AvailabilityStore describes a component that can verify and save sidecars for a given block, and confirm previously
// verified and saved sidecars.
// Persist guarantees that the sidecar will be available to perform a DA check
// for the life of the beacon node process.
// IsDataAvailable guarantees that all blobs committed to in the block have been
// durably persisted before returning a non-error value.
type AvailabilityStore interface {
IsDataAvailable(ctx context.Context, current primitives.Slot, b blocks.ROBlock) error
Persist(current primitives.Slot, blobSidecar ...blocks.ROBlob) error
// AvailabilityChecker is the minimum interface needed to check if data is available for a block.
// By convention there is a concept of an AvailabilityStore that implements a method to persist
// blobs or data columns to prepare for Availability checking, but since those methods are different
// for different forms of blob data, they are not included in the interface.
type AvailabilityChecker interface {
IsDataAvailable(ctx context.Context, current primitives.Slot, b ...blocks.ROBlock) error
}
// RetentionChecker is a callback that determines whether blobs at the given slot are within the retention period.
type RetentionChecker func(primitives.Slot) bool

5
beacon-chain/das/log.go Normal file
View File

@@ -0,0 +1,5 @@
package das
import "github.com/sirupsen/logrus"
var log = logrus.WithField("prefix", "das")

View File

@@ -9,16 +9,20 @@ import (
// MockAvailabilityStore is an implementation of AvailabilityStore that can be used by other packages in tests.
type MockAvailabilityStore struct {
VerifyAvailabilityCallback func(ctx context.Context, current primitives.Slot, b blocks.ROBlock) error
VerifyAvailabilityCallback func(ctx context.Context, current primitives.Slot, b ...blocks.ROBlock) error
ErrIsDataAvailable error
PersistBlobsCallback func(current primitives.Slot, blobSidecar ...blocks.ROBlob) error
}
var _ AvailabilityStore = &MockAvailabilityStore{}
var _ AvailabilityChecker = &MockAvailabilityStore{}
// IsDataAvailable satisfies the corresponding method of the AvailabilityStore interface in a way that is useful for tests.
func (m *MockAvailabilityStore) IsDataAvailable(ctx context.Context, current primitives.Slot, b blocks.ROBlock) error {
func (m *MockAvailabilityStore) IsDataAvailable(ctx context.Context, current primitives.Slot, b ...blocks.ROBlock) error {
if m.ErrIsDataAvailable != nil {
return m.ErrIsDataAvailable
}
if m.VerifyAvailabilityCallback != nil {
return m.VerifyAvailabilityCallback(ctx, current, b)
return m.VerifyAvailabilityCallback(ctx, current, b...)
}
return nil
}

135
beacon-chain/das/needs.go Normal file
View File

@@ -0,0 +1,135 @@
package das
import (
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/time/slots"
"github.com/pkg/errors"
)
// NeedSpan represents the need for a resource over a span of slots.
type NeedSpan struct {
Begin primitives.Slot
End primitives.Slot
}
// At returns whether blocks/blobs/columns are needed At the given slot.
func (n NeedSpan) At(slot primitives.Slot) bool {
return slot >= n.Begin && slot < n.End
}
// CurrentNeeds fields can be used to check whether the given resource type is needed
// at a given slot. The values are based on the current slot, so this value shouldn't
// be retained / reused across slots.
type CurrentNeeds struct {
Block NeedSpan
Blob NeedSpan
Col NeedSpan
}
// SyncNeeds holds configuration and state for determining what data is needed
// at any given slot during backfill based on the current slot.
type SyncNeeds struct {
current func() primitives.Slot
deneb primitives.Slot
fulu primitives.Slot
oldestSlotFlagPtr *primitives.Slot
validOldestSlotPtr *primitives.Slot
blockRetention primitives.Epoch
blobRetentionFlag primitives.Epoch
blobRetention primitives.Epoch
colRetention primitives.Epoch
}
type CurrentSlotter func() primitives.Slot
func NewSyncNeeds(current CurrentSlotter, oldestSlotFlagPtr *primitives.Slot, blobRetentionFlag primitives.Epoch) (SyncNeeds, error) {
deneb, err := slots.EpochStart(params.BeaconConfig().DenebForkEpoch)
if err != nil {
return SyncNeeds{}, errors.Wrap(err, "deneb fork slot")
}
fuluBoundary := min(params.BeaconConfig().FuluForkEpoch, slots.MaxSafeEpoch())
fulu, err := slots.EpochStart(fuluBoundary)
if err != nil {
return SyncNeeds{}, errors.Wrap(err, "fulu fork slot")
}
sn := SyncNeeds{
current: func() primitives.Slot { return current() },
deneb: deneb,
fulu: fulu,
blobRetentionFlag: blobRetentionFlag,
}
// We apply the --blob-retention-epochs flag to both blob and column retention.
sn.blobRetention = max(sn.blobRetentionFlag, params.BeaconConfig().MinEpochsForBlobsSidecarsRequest)
sn.colRetention = max(sn.blobRetentionFlag, params.BeaconConfig().MinEpochsForDataColumnSidecarsRequest)
// Override spec minimum block retention with user-provided flag only if it is lower than the spec minimum.
sn.blockRetention = primitives.Epoch(params.BeaconConfig().MinEpochsForBlockRequests)
if oldestSlotFlagPtr != nil {
oldestEpoch := slots.ToEpoch(*oldestSlotFlagPtr)
if oldestEpoch < sn.blockRetention {
sn.validOldestSlotPtr = oldestSlotFlagPtr
} else {
log.WithField("backfill-oldest-slot", *oldestSlotFlagPtr).
WithField("specMinSlot", syncEpochOffset(current(), sn.blockRetention)).
Warn("Ignoring user-specified slot > MIN_EPOCHS_FOR_BLOCK_REQUESTS.")
}
}
return sn, nil
}
// Currently is the main callback given to the different parts of backfill to determine
// what resources are needed at a given slot. It assumes the current instance of SyncNeeds
// is the result of calling initialize.
func (n SyncNeeds) Currently() CurrentNeeds {
current := n.current()
c := CurrentNeeds{
Block: n.blockSpan(current),
Blob: NeedSpan{Begin: syncEpochOffset(current, n.blobRetention), End: n.fulu},
Col: NeedSpan{Begin: syncEpochOffset(current, n.colRetention), End: current},
}
// Adjust the minimums forward to the slots where the sidecar types were introduced
c.Blob.Begin = max(c.Blob.Begin, n.deneb)
c.Col.Begin = max(c.Col.Begin, n.fulu)
return c
}
func (n SyncNeeds) blockSpan(current primitives.Slot) NeedSpan {
if n.validOldestSlotPtr != nil { // assumes validation done in initialize()
return NeedSpan{Begin: *n.validOldestSlotPtr, End: current}
}
return NeedSpan{Begin: syncEpochOffset(current, n.blockRetention), End: current}
}
func (n SyncNeeds) BlobRetentionChecker() RetentionChecker {
return func(slot primitives.Slot) bool {
current := n.Currently()
return current.Blob.At(slot)
}
}
func (n SyncNeeds) DataColumnRetentionChecker() RetentionChecker {
return func(slot primitives.Slot) bool {
current := n.Currently()
return current.Col.At(slot)
}
}
// syncEpochOffset subtracts a number of epochs as slots from the current slot, with underflow checks.
// It returns slot 1 if the result would be 0 or underflow. It doesn't return slot 0 because the
// genesis block needs to be specially synced (it doesn't have a valid signature).
func syncEpochOffset(current primitives.Slot, subtract primitives.Epoch) primitives.Slot {
minEpoch := min(subtract, slots.MaxSafeEpoch())
// compute slot offset - offset is a number of slots to go back from current (not an absolute slot).
offset := slots.UnsafeEpochStart(minEpoch)
// Undeflow protection: slot 0 is the genesis block, therefore the signature in it is invalid.
// To prevent us from rejecting a batch, we restrict the minimum backfill batch till only slot 1
if offset >= current {
return 1
}
return current - offset
}

View File

@@ -0,0 +1,675 @@
package das
import (
"fmt"
"testing"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/testing/require"
"github.com/OffchainLabs/prysm/v7/time/slots"
)
// TestNeedSpanAt tests the needSpan.at() method for range checking.
func TestNeedSpanAt(t *testing.T) {
cases := []struct {
name string
span NeedSpan
slots []primitives.Slot
expected bool
}{
{
name: "within bounds",
span: NeedSpan{Begin: 100, End: 200},
slots: []primitives.Slot{101, 150, 199},
expected: true,
},
{
name: "before begin / at end boundary (exclusive)",
span: NeedSpan{Begin: 100, End: 200},
slots: []primitives.Slot{99, 200, 201},
expected: false,
},
{
name: "empty span (begin == end)",
span: NeedSpan{Begin: 100, End: 100},
slots: []primitives.Slot{100},
expected: false,
},
{
name: "slot 0 with span starting at 0",
span: NeedSpan{Begin: 0, End: 100},
slots: []primitives.Slot{0},
expected: true,
},
}
for _, tc := range cases {
for _, sl := range tc.slots {
t.Run(fmt.Sprintf("%s at slot %d, ", tc.name, sl), func(t *testing.T) {
result := tc.span.At(sl)
require.Equal(t, tc.expected, result)
})
}
}
}
// TestSyncEpochOffset tests the syncEpochOffset helper function.
func TestSyncEpochOffset(t *testing.T) {
slotsPerEpoch := params.BeaconConfig().SlotsPerEpoch
cases := []struct {
name string
current primitives.Slot
subtract primitives.Epoch
expected primitives.Slot
}{
{
name: "typical offset - 5 epochs back",
current: primitives.Slot(10000),
subtract: 5,
expected: primitives.Slot(10000 - 5*slotsPerEpoch),
},
{
name: "zero subtract returns current",
current: primitives.Slot(5000),
subtract: 0,
expected: primitives.Slot(5000),
},
{
name: "subtract 1 epoch from mid-range slot",
current: primitives.Slot(1000),
subtract: 1,
expected: primitives.Slot(1000 - slotsPerEpoch),
},
{
name: "offset equals current - underflow protection",
current: primitives.Slot(slotsPerEpoch),
subtract: 1,
expected: 1,
},
{
name: "offset exceeds current - underflow protection",
current: primitives.Slot(50),
subtract: 1000,
expected: 1,
},
{
name: "current very close to 0",
current: primitives.Slot(10),
subtract: 1,
expected: 1,
},
{
name: "subtract MaxSafeEpoch",
current: primitives.Slot(1000000),
subtract: slots.MaxSafeEpoch(),
expected: 1, // underflow protection
},
{
name: "result exactly at slot 1",
current: primitives.Slot(1 + slotsPerEpoch),
subtract: 1,
expected: 1,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
result := syncEpochOffset(tc.current, tc.subtract)
require.Equal(t, tc.expected, result)
})
}
}
// TestSyncNeedsInitialize tests the syncNeeds.initialize() method.
func TestSyncNeedsInitialize(t *testing.T) {
params.SetupTestConfigCleanup(t)
slotsPerEpoch := params.BeaconConfig().SlotsPerEpoch
minBlobEpochs := params.BeaconConfig().MinEpochsForBlobsSidecarsRequest
minColEpochs := params.BeaconConfig().MinEpochsForDataColumnSidecarsRequest
currentSlot := primitives.Slot(10000)
currentFunc := func() primitives.Slot { return currentSlot }
cases := []struct {
invalidOldestFlag bool
expectValidOldest bool
oldestSlotFlagPtr *primitives.Slot
blobRetentionFlag primitives.Epoch
expectedBlob primitives.Epoch
expectedCol primitives.Epoch
name string
input SyncNeeds
}{
{
name: "basic initialization with no flags",
expectValidOldest: false,
expectedBlob: minBlobEpochs,
expectedCol: minColEpochs,
blobRetentionFlag: 0,
},
{
name: "blob retention flag less than spec minimum",
blobRetentionFlag: minBlobEpochs - 1,
expectValidOldest: false,
expectedBlob: minBlobEpochs,
expectedCol: minColEpochs,
},
{
name: "blob retention flag greater than spec minimum",
blobRetentionFlag: minBlobEpochs + 10,
expectValidOldest: false,
expectedBlob: minBlobEpochs + 10,
expectedCol: minBlobEpochs + 10,
},
{
name: "oldestSlotFlagPtr is nil",
blobRetentionFlag: 0,
oldestSlotFlagPtr: nil,
expectValidOldest: false,
expectedBlob: minBlobEpochs,
expectedCol: minColEpochs,
},
{
name: "valid oldestSlotFlagPtr (earlier than spec minimum)",
blobRetentionFlag: 0,
oldestSlotFlagPtr: func() *primitives.Slot {
slot := primitives.Slot(10)
return &slot
}(),
expectValidOldest: true,
expectedBlob: minBlobEpochs,
expectedCol: minColEpochs,
},
{
name: "invalid oldestSlotFlagPtr (later than spec minimum)",
blobRetentionFlag: 0,
oldestSlotFlagPtr: func() *primitives.Slot {
// Make it way past the spec minimum
slot := currentSlot - primitives.Slot(params.BeaconConfig().MinEpochsForBlockRequests-1)*slotsPerEpoch
return &slot
}(),
expectValidOldest: false,
expectedBlob: minBlobEpochs,
expectedCol: minColEpochs,
invalidOldestFlag: true,
},
{
name: "oldestSlotFlagPtr at boundary (exactly at spec minimum)",
blobRetentionFlag: 0,
oldestSlotFlagPtr: func() *primitives.Slot {
slot := currentSlot - primitives.Slot(params.BeaconConfig().MinEpochsForBlockRequests)*slotsPerEpoch
return &slot
}(),
expectValidOldest: false,
expectedBlob: minBlobEpochs,
expectedCol: minColEpochs,
invalidOldestFlag: true,
},
{
name: "both blob retention flag and oldest slot set",
blobRetentionFlag: minBlobEpochs + 5,
oldestSlotFlagPtr: func() *primitives.Slot {
slot := primitives.Slot(100)
return &slot
}(),
expectValidOldest: true,
expectedBlob: minBlobEpochs + 5,
expectedCol: minBlobEpochs + 5,
},
{
name: "zero blob retention uses spec minimum",
blobRetentionFlag: 0,
expectValidOldest: false,
expectedBlob: minBlobEpochs,
expectedCol: minColEpochs,
},
{
name: "large blob retention value",
blobRetentionFlag: 5000,
expectValidOldest: false,
expectedBlob: 5000,
expectedCol: 5000,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
result, err := NewSyncNeeds(currentFunc, tc.oldestSlotFlagPtr, tc.blobRetentionFlag)
require.NoError(t, err)
// Check that current, deneb, fulu are set correctly
require.Equal(t, currentSlot, result.current())
// Check retention calculations
require.Equal(t, tc.expectedBlob, result.blobRetention)
require.Equal(t, tc.expectedCol, result.colRetention)
if tc.invalidOldestFlag {
require.IsNil(t, result.validOldestSlotPtr)
} else {
require.Equal(t, tc.oldestSlotFlagPtr, result.validOldestSlotPtr)
}
// Check blockRetention is always spec minimum
require.Equal(t, primitives.Epoch(params.BeaconConfig().MinEpochsForBlockRequests), result.blockRetention)
})
}
}
// TestSyncNeedsBlockSpan tests the syncNeeds.blockSpan() method.
func TestSyncNeedsBlockSpan(t *testing.T) {
params.SetupTestConfigCleanup(t)
minBlockEpochs := params.BeaconConfig().MinEpochsForBlockRequests
cases := []struct {
name string
validOldest *primitives.Slot
blockRetention primitives.Epoch
current primitives.Slot
expectedBegin primitives.Slot
expectedEnd primitives.Slot
}{
{
name: "with validOldestSlotPtr set",
validOldest: func() *primitives.Slot { s := primitives.Slot(500); return &s }(),
blockRetention: primitives.Epoch(minBlockEpochs),
current: 10000,
expectedBegin: 500,
expectedEnd: 10000,
},
{
name: "without validOldestSlotPtr (nil)",
validOldest: nil,
blockRetention: primitives.Epoch(minBlockEpochs),
current: 10000,
expectedBegin: syncEpochOffset(10000, primitives.Epoch(minBlockEpochs)),
expectedEnd: 10000,
},
{
name: "very low current slot",
validOldest: nil,
blockRetention: primitives.Epoch(minBlockEpochs),
current: 100,
expectedBegin: 1, // underflow protection
expectedEnd: 100,
},
{
name: "very high current slot",
validOldest: nil,
blockRetention: primitives.Epoch(minBlockEpochs),
current: 1000000,
expectedBegin: syncEpochOffset(1000000, primitives.Epoch(minBlockEpochs)),
expectedEnd: 1000000,
},
{
name: "validOldestSlotPtr at boundary value",
validOldest: func() *primitives.Slot { s := primitives.Slot(1); return &s }(),
blockRetention: primitives.Epoch(minBlockEpochs),
current: 5000,
expectedBegin: 1,
expectedEnd: 5000,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
sn := SyncNeeds{
validOldestSlotPtr: tc.validOldest,
blockRetention: tc.blockRetention,
}
result := sn.blockSpan(tc.current)
require.Equal(t, tc.expectedBegin, result.Begin)
require.Equal(t, tc.expectedEnd, result.End)
})
}
}
// TestSyncNeedsCurrently tests the syncNeeds.currently() method.
func TestSyncNeedsCurrently(t *testing.T) {
params.SetupTestConfigCleanup(t)
slotsPerEpoch := params.BeaconConfig().SlotsPerEpoch
denebSlot := primitives.Slot(1000)
fuluSlot := primitives.Slot(2000)
cases := []struct {
name string
current primitives.Slot
blobRetention primitives.Epoch
colRetention primitives.Epoch
blockRetention primitives.Epoch
validOldest *primitives.Slot
// Expected block span
expectBlockBegin primitives.Slot
expectBlockEnd primitives.Slot
// Expected blob span
expectBlobBegin primitives.Slot
expectBlobEnd primitives.Slot
// Expected column span
expectColBegin primitives.Slot
expectColEnd primitives.Slot
}{
{
name: "pre-Deneb - only blocks needed",
current: 500,
blobRetention: 10,
colRetention: 10,
blockRetention: 5,
validOldest: nil,
expectBlockBegin: syncEpochOffset(500, 5),
expectBlockEnd: 500,
expectBlobBegin: denebSlot, // adjusted to deneb
expectBlobEnd: fuluSlot,
expectColBegin: fuluSlot, // adjusted to fulu
expectColEnd: 500,
},
{
name: "between Deneb and Fulu - blocks and blobs needed",
current: 1500,
blobRetention: 10,
colRetention: 10,
blockRetention: 5,
validOldest: nil,
expectBlockBegin: syncEpochOffset(1500, 5),
expectBlockEnd: 1500,
expectBlobBegin: max(syncEpochOffset(1500, 10), denebSlot),
expectBlobEnd: fuluSlot,
expectColBegin: fuluSlot, // adjusted to fulu
expectColEnd: 1500,
},
{
name: "post-Fulu - all resources needed",
current: 3000,
blobRetention: 10,
colRetention: 10,
blockRetention: 5,
validOldest: nil,
expectBlockBegin: syncEpochOffset(3000, 5),
expectBlockEnd: 3000,
expectBlobBegin: max(syncEpochOffset(3000, 10), denebSlot),
expectBlobEnd: fuluSlot,
expectColBegin: max(syncEpochOffset(3000, 10), fuluSlot),
expectColEnd: 3000,
},
{
name: "exactly at Deneb boundary",
current: denebSlot,
blobRetention: 10,
colRetention: 10,
blockRetention: 5,
validOldest: nil,
expectBlockBegin: syncEpochOffset(denebSlot, 5),
expectBlockEnd: denebSlot,
expectBlobBegin: denebSlot,
expectBlobEnd: fuluSlot,
expectColBegin: fuluSlot,
expectColEnd: denebSlot,
},
{
name: "exactly at Fulu boundary",
current: fuluSlot,
blobRetention: 10,
colRetention: 10,
blockRetention: 5,
validOldest: nil,
expectBlockBegin: syncEpochOffset(fuluSlot, 5),
expectBlockEnd: fuluSlot,
expectBlobBegin: max(syncEpochOffset(fuluSlot, 10), denebSlot),
expectBlobEnd: fuluSlot,
expectColBegin: fuluSlot,
expectColEnd: fuluSlot,
},
{
name: "small retention periods",
current: 5000,
blobRetention: 1,
colRetention: 2,
blockRetention: 1,
validOldest: nil,
expectBlockBegin: syncEpochOffset(5000, 1),
expectBlockEnd: 5000,
expectBlobBegin: max(syncEpochOffset(5000, 1), denebSlot),
expectBlobEnd: fuluSlot,
expectColBegin: max(syncEpochOffset(5000, 2), fuluSlot),
expectColEnd: 5000,
},
{
name: "large retention periods",
current: 10000,
blobRetention: 100,
colRetention: 100,
blockRetention: 50,
validOldest: nil,
expectBlockBegin: syncEpochOffset(10000, 50),
expectBlockEnd: 10000,
expectBlobBegin: max(syncEpochOffset(10000, 100), denebSlot),
expectBlobEnd: fuluSlot,
expectColBegin: max(syncEpochOffset(10000, 100), fuluSlot),
expectColEnd: 10000,
},
{
name: "with validOldestSlotPtr for blocks",
current: 8000,
blobRetention: 10,
colRetention: 10,
blockRetention: 5,
validOldest: func() *primitives.Slot { s := primitives.Slot(100); return &s }(),
expectBlockBegin: 100,
expectBlockEnd: 8000,
expectBlobBegin: max(syncEpochOffset(8000, 10), denebSlot),
expectBlobEnd: fuluSlot,
expectColBegin: max(syncEpochOffset(8000, 10), fuluSlot),
expectColEnd: 8000,
},
{
name: "retention approaching current slot",
current: primitives.Slot(2000 + 5*slotsPerEpoch),
blobRetention: 5,
colRetention: 5,
blockRetention: 3,
validOldest: nil,
expectBlockBegin: syncEpochOffset(primitives.Slot(2000+5*slotsPerEpoch), 3),
expectBlockEnd: primitives.Slot(2000 + 5*slotsPerEpoch),
expectBlobBegin: max(syncEpochOffset(primitives.Slot(2000+5*slotsPerEpoch), 5), denebSlot),
expectBlobEnd: fuluSlot,
expectColBegin: max(syncEpochOffset(primitives.Slot(2000+5*slotsPerEpoch), 5), fuluSlot),
expectColEnd: primitives.Slot(2000 + 5*slotsPerEpoch),
},
{
name: "current just after Deneb",
current: denebSlot + 10,
blobRetention: 10,
colRetention: 10,
blockRetention: 5,
validOldest: nil,
expectBlockBegin: syncEpochOffset(denebSlot+10, 5),
expectBlockEnd: denebSlot + 10,
expectBlobBegin: denebSlot,
expectBlobEnd: fuluSlot,
expectColBegin: fuluSlot,
expectColEnd: denebSlot + 10,
},
{
name: "current just after Fulu",
current: fuluSlot + 10,
blobRetention: 10,
colRetention: 10,
blockRetention: 5,
validOldest: nil,
expectBlockBegin: syncEpochOffset(fuluSlot+10, 5),
expectBlockEnd: fuluSlot + 10,
expectBlobBegin: max(syncEpochOffset(fuluSlot+10, 10), denebSlot),
expectBlobEnd: fuluSlot,
expectColBegin: fuluSlot,
expectColEnd: fuluSlot + 10,
},
{
name: "blob retention would start before Deneb",
current: denebSlot + primitives.Slot(5*slotsPerEpoch),
blobRetention: 100, // very large retention
colRetention: 10,
blockRetention: 5,
validOldest: nil,
expectBlockBegin: syncEpochOffset(denebSlot+primitives.Slot(5*slotsPerEpoch), 5),
expectBlockEnd: denebSlot + primitives.Slot(5*slotsPerEpoch),
expectBlobBegin: denebSlot, // clamped to deneb
expectBlobEnd: fuluSlot,
expectColBegin: fuluSlot,
expectColEnd: denebSlot + primitives.Slot(5*slotsPerEpoch),
},
{
name: "column retention would start before Fulu",
current: fuluSlot + primitives.Slot(5*slotsPerEpoch),
blobRetention: 10,
colRetention: 100, // very large retention
blockRetention: 5,
validOldest: nil,
expectBlockBegin: syncEpochOffset(fuluSlot+primitives.Slot(5*slotsPerEpoch), 5),
expectBlockEnd: fuluSlot + primitives.Slot(5*slotsPerEpoch),
expectBlobBegin: max(syncEpochOffset(fuluSlot+primitives.Slot(5*slotsPerEpoch), 10), denebSlot),
expectBlobEnd: fuluSlot,
expectColBegin: fuluSlot, // clamped to fulu
expectColEnd: fuluSlot + primitives.Slot(5*slotsPerEpoch),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
sn := SyncNeeds{
current: func() primitives.Slot { return tc.current },
deneb: denebSlot,
fulu: fuluSlot,
validOldestSlotPtr: tc.validOldest,
blockRetention: tc.blockRetention,
blobRetention: tc.blobRetention,
colRetention: tc.colRetention,
}
result := sn.Currently()
// Verify block span
require.Equal(t, tc.expectBlockBegin, result.Block.Begin,
"block.begin mismatch")
require.Equal(t, tc.expectBlockEnd, result.Block.End,
"block.end mismatch")
// Verify blob span
require.Equal(t, tc.expectBlobBegin, result.Blob.Begin,
"blob.begin mismatch")
require.Equal(t, tc.expectBlobEnd, result.Blob.End,
"blob.end mismatch")
// Verify column span
require.Equal(t, tc.expectColBegin, result.Col.Begin,
"col.begin mismatch")
require.Equal(t, tc.expectColEnd, result.Col.End,
"col.end mismatch")
})
}
}
// TestCurrentNeedsIntegration verifies the complete currentNeeds workflow.
func TestCurrentNeedsIntegration(t *testing.T) {
params.SetupTestConfigCleanup(t)
denebSlot := primitives.Slot(1000)
fuluSlot := primitives.Slot(2000)
cases := []struct {
name string
current primitives.Slot
blobRetention primitives.Epoch
colRetention primitives.Epoch
testSlots []primitives.Slot
expectBlockAt []bool
expectBlobAt []bool
expectColAt []bool
}{
{
name: "pre-Deneb slot - only blocks",
current: 500,
blobRetention: 10,
colRetention: 10,
testSlots: []primitives.Slot{100, 250, 499, 500, 1000, 2000},
expectBlockAt: []bool{true, true, true, false, false, false},
expectBlobAt: []bool{false, false, false, false, true, false},
expectColAt: []bool{false, false, false, false, false, false},
},
{
name: "between Deneb and Fulu - blocks and blobs",
current: 1500,
blobRetention: 10,
colRetention: 10,
testSlots: []primitives.Slot{500, 1000, 1200, 1499, 1500, 2000},
expectBlockAt: []bool{true, true, true, true, false, false},
expectBlobAt: []bool{false, false, true, true, true, false},
expectColAt: []bool{false, false, false, false, false, false},
},
{
name: "post-Fulu - all resources",
current: 3000,
blobRetention: 10,
colRetention: 10,
testSlots: []primitives.Slot{1000, 1500, 2000, 2500, 2999, 3000},
expectBlockAt: []bool{true, true, true, true, true, false},
expectBlobAt: []bool{false, false, false, false, false, false},
expectColAt: []bool{false, false, false, false, true, false},
},
{
name: "at Deneb boundary",
current: denebSlot,
blobRetention: 5,
colRetention: 5,
testSlots: []primitives.Slot{500, 999, 1000, 1500, 2000},
expectBlockAt: []bool{true, true, false, false, false},
expectBlobAt: []bool{false, false, true, true, false},
expectColAt: []bool{false, false, false, false, false},
},
{
name: "at Fulu boundary",
current: fuluSlot,
blobRetention: 5,
colRetention: 5,
testSlots: []primitives.Slot{1000, 1500, 1999, 2000, 2001},
expectBlockAt: []bool{true, true, true, false, false},
expectBlobAt: []bool{false, false, true, false, false},
expectColAt: []bool{false, false, false, false, false},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
sn := SyncNeeds{
current: func() primitives.Slot { return tc.current },
deneb: denebSlot,
fulu: fuluSlot,
blockRetention: 100,
blobRetention: tc.blobRetention,
colRetention: tc.colRetention,
}
cn := sn.Currently()
// Verify block.end == current
require.Equal(t, tc.current, cn.Block.End, "block.end should equal current")
// Verify blob.end == fulu
require.Equal(t, fuluSlot, cn.Blob.End, "blob.end should equal fulu")
// Verify col.end == current
require.Equal(t, tc.current, cn.Col.End, "col.end should equal current")
// Test each slot
for i, slot := range tc.testSlots {
require.Equal(t, tc.expectBlockAt[i], cn.Block.At(slot),
"block.at(%d) mismatch at index %d", slot, i)
require.Equal(t, tc.expectBlobAt[i], cn.Blob.At(slot),
"blob.at(%d) mismatch at index %d", slot, i)
require.Equal(t, tc.expectColAt[i], cn.Col.At(slot),
"col.at(%d) mismatch at index %d", slot, i)
}
})
}
}

View File

@@ -270,7 +270,7 @@ func (dcs *DataColumnStorage) Save(dataColumnSidecars []blocks.VerifiedRODataCol
// Check the number of columns is the one expected.
// While implementing this, we expect the number of columns won't change.
// If it does, we will need to create a new version of the data column sidecar file.
if params.BeaconConfig().NumberOfColumns != mandatoryNumberOfColumns {
if fieldparams.NumberOfColumns != mandatoryNumberOfColumns {
return errWrongNumberOfColumns
}
@@ -964,8 +964,7 @@ func (si *storageIndices) set(dataColumnIndex uint64, position uint8) error {
// pullChan pulls data column sidecars from the input channel until it is empty.
func pullChan(inputRoDataColumns chan []blocks.VerifiedRODataColumn) []blocks.VerifiedRODataColumn {
numberOfColumns := params.BeaconConfig().NumberOfColumns
dataColumnSidecars := make([]blocks.VerifiedRODataColumn, 0, numberOfColumns)
dataColumnSidecars := make([]blocks.VerifiedRODataColumn, 0, fieldparams.NumberOfColumns)
for {
select {

View File

@@ -117,8 +117,6 @@ func (sc *dataColumnStorageSummaryCache) HighestEpoch() primitives.Epoch {
// set updates the cache.
func (sc *dataColumnStorageSummaryCache) set(dataColumnsIdent DataColumnsIdent) error {
numberOfColumns := params.BeaconConfig().NumberOfColumns
sc.mu.Lock()
defer sc.mu.Unlock()
@@ -127,7 +125,7 @@ func (sc *dataColumnStorageSummaryCache) set(dataColumnsIdent DataColumnsIdent)
count := uint64(0)
for _, index := range dataColumnsIdent.Indices {
if index >= numberOfColumns {
if index >= fieldparams.NumberOfColumns {
return errDataColumnIndexOutOfBounds
}

View File

@@ -7,7 +7,6 @@ import (
"testing"
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
"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/testing/require"
@@ -88,22 +87,6 @@ func TestWarmCache(t *testing.T) {
}
func TestSaveDataColumnsSidecars(t *testing.T) {
t.Run("wrong numbers of columns", func(t *testing.T) {
cfg := params.BeaconConfig().Copy()
cfg.NumberOfColumns = 0
params.OverrideBeaconConfig(cfg)
params.SetupTestConfigCleanup(t)
_, verifiedRoDataColumnSidecars := util.CreateTestVerifiedRoDataColumnSidecars(
t,
[]util.DataColumnParam{{Index: 12}, {Index: 1_000_000}, {Index: 48}},
)
_, dataColumnStorage := NewEphemeralDataColumnStorageAndFs(t)
err := dataColumnStorage.Save(verifiedRoDataColumnSidecars)
require.ErrorIs(t, err, errWrongNumberOfColumns)
})
t.Run("one of the column index is too large", func(t *testing.T) {
_, verifiedRoDataColumnSidecars := util.CreateTestVerifiedRoDataColumnSidecars(
t,

View File

@@ -128,9 +128,9 @@ type NoHeadAccessDatabase interface {
BackfillFinalizedIndex(ctx context.Context, blocks []blocks.ROBlock, finalizedChildRoot [32]byte) error
// Custody operations.
UpdateSubscribedToAllDataSubnets(ctx context.Context, subscribed bool) (bool, error)
UpdateCustodyInfo(ctx context.Context, earliestAvailableSlot primitives.Slot, custodyGroupCount uint64) (primitives.Slot, uint64, error)
UpdateEarliestAvailableSlot(ctx context.Context, earliestAvailableSlot primitives.Slot) error
UpdateSubscribedToAllDataSubnets(ctx context.Context, subscribed bool) (bool, error)
// P2P Metadata operations.
SaveMetadataSeqNum(ctx context.Context, seqNum uint64) error

View File

@@ -27,6 +27,9 @@ go_library(
"p2p.go",
"schema.go",
"state.go",
"state_diff.go",
"state_diff_cache.go",
"state_diff_helpers.go",
"state_summary.go",
"state_summary_cache.go",
"utils.go",
@@ -41,10 +44,12 @@ go_library(
"//beacon-chain/db/iface:go_default_library",
"//beacon-chain/state:go_default_library",
"//beacon-chain/state/state-native: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/hdiff:go_default_library",
"//consensus-types/interfaces:go_default_library",
"//consensus-types/light-client:go_default_library",
"//consensus-types/primitives:go_default_library",
@@ -53,6 +58,7 @@ go_library(
"//encoding/ssz/detect:go_default_library",
"//genesis:go_default_library",
"//io/file:go_default_library",
"//math:go_default_library",
"//monitoring/progress:go_default_library",
"//monitoring/tracing:go_default_library",
"//monitoring/tracing/trace:go_default_library",
@@ -98,6 +104,7 @@ go_test(
"migration_block_slot_index_test.go",
"migration_state_validators_test.go",
"p2p_test.go",
"state_diff_test.go",
"state_summary_test.go",
"state_test.go",
"utils_test.go",
@@ -111,6 +118,7 @@ go_test(
"//beacon-chain/db/iface:go_default_library",
"//beacon-chain/state:go_default_library",
"//beacon-chain/state/state-native: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",
@@ -120,6 +128,7 @@ go_test(
"//consensus-types/primitives:go_default_library",
"//encoding/bytesutil:go_default_library",
"//genesis:go_default_library",
"//math:go_default_library",
"//proto/dbval:go_default_library",
"//proto/engine/v1:go_default_library",
"//proto/prysm/v1alpha1:go_default_library",
@@ -133,6 +142,7 @@ go_test(
"@com_github_golang_snappy//: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",
"@io_bazel_rules_go//go/tools/bazel:go_default_library",
"@io_etcd_go_bbolt//:go_default_library",
"@org_golang_google_protobuf//proto:go_default_library",

View File

@@ -15,7 +15,8 @@ import (
)
// UpdateCustodyInfo atomically updates the custody group count only if it is greater than the stored one.
// In this case, it also updates the earliest available slot with the provided value.
// When the custody group count increases, the earliest available slot is set to the maximum of the
// incoming value and the stored value, ensuring the slot never decreases when increasing custody.
// It returns the (potentially updated) custody group count and earliest available slot.
func (s *Store) UpdateCustodyInfo(ctx context.Context, earliestAvailableSlot primitives.Slot, custodyGroupCount uint64) (primitives.Slot, uint64, error) {
_, span := trace.StartSpan(ctx, "BeaconDB.UpdateCustodyInfo")
@@ -41,25 +42,39 @@ func (s *Store) UpdateCustodyInfo(ctx context.Context, earliestAvailableSlot pri
storedEarliestAvailableSlot = primitives.Slot(bytesutil.BytesToUint64BigEndian(storedEarliestAvailableSlotBytes))
}
log.WithFields(logrus.Fields{
"incomingSlot": earliestAvailableSlot,
"incomingGroupCount": custodyGroupCount,
"storedSlot": storedEarliestAvailableSlot,
"storedGroupCount": storedGroupCount,
"storedSlotBytesLen": len(storedEarliestAvailableSlotBytes),
"storedGroupCountBytesLen": len(storedGroupCountBytes),
}).Debug("UpdateCustodyInfo: comparing incoming vs stored values")
// Exit early if the new custody group count is lower than or equal to the stored one.
if custodyGroupCount <= storedGroupCount {
log.Debug("UpdateCustodyInfo: exiting early, custody group count not increasing")
return nil
}
storedGroupCount, storedEarliestAvailableSlot = custodyGroupCount, earliestAvailableSlot
// Store the earliest available slot.
bytes := bytesutil.Uint64ToBytesBigEndian(uint64(earliestAvailableSlot))
if err := bucket.Put(earliestAvailableSlotKey, bytes); err != nil {
return errors.Wrap(err, "put earliest available slot")
}
// Store the custody group count.
bytes = bytesutil.Uint64ToBytesBigEndian(custodyGroupCount)
// Update the custody group count.
storedGroupCount = custodyGroupCount
bytes := bytesutil.Uint64ToBytesBigEndian(custodyGroupCount)
if err := bucket.Put(groupCountKey, bytes); err != nil {
return errors.Wrap(err, "put custody group count")
}
// Only update earliestAvailableSlot if the incoming value is higher.
// This prevents losing availability for data we already have when switching modes
// (e.g., from normal to semi-supernode or supernode).
if earliestAvailableSlot > storedEarliestAvailableSlot {
storedEarliestAvailableSlot = earliestAvailableSlot
bytes = bytesutil.Uint64ToBytesBigEndian(uint64(earliestAvailableSlot))
if err := bucket.Put(earliestAvailableSlotKey, bytes); err != nil {
return errors.Wrap(err, "put earliest available slot")
}
}
return nil
}); err != nil {
return 0, 0, err
@@ -146,9 +161,9 @@ func (s *Store) UpdateEarliestAvailableSlot(ctx context.Context, earliestAvailab
return nil
}
// UpdateSubscribedToAllDataSubnets updates the "subscribed to all data subnets" status in the database
// only if `subscribed` is `true`.
// It returns the previous subscription status.
// UpdateSubscribedToAllDataSubnets updates whether the node is subscribed to all data subnets (supernode mode).
// This is a one-way flag - once set to true, it cannot be reverted to false.
// Returns the previous state.
func (s *Store) UpdateSubscribedToAllDataSubnets(ctx context.Context, subscribed bool) (bool, error) {
_, span := trace.StartSpan(ctx, "BeaconDB.UpdateSubscribedToAllDataSubnets")
defer span.End()
@@ -156,13 +171,11 @@ func (s *Store) UpdateSubscribedToAllDataSubnets(ctx context.Context, subscribed
result := false
if !subscribed {
if err := s.db.View(func(tx *bolt.Tx) error {
// Retrieve the custody bucket.
bucket := tx.Bucket(custodyBucket)
if bucket == nil {
return nil
}
// Retrieve the subscribe all data subnets flag.
bytes := bucket.Get(subscribeAllDataSubnetsKey)
if len(bytes) == 0 {
return nil
@@ -181,7 +194,6 @@ func (s *Store) UpdateSubscribedToAllDataSubnets(ctx context.Context, subscribed
}
if err := s.db.Update(func(tx *bolt.Tx) error {
// Retrieve the custody bucket.
bucket, err := tx.CreateBucketIfNotExists(custodyBucket)
if err != nil {
return errors.Wrap(err, "create custody bucket")

View File

@@ -67,6 +67,7 @@ func getSubscriptionStatusFromDB(t *testing.T, db *Store) bool {
return subscribed
}
func TestUpdateCustodyInfo(t *testing.T) {
ctx := t.Context()
@@ -88,7 +89,7 @@ func TestUpdateCustodyInfo(t *testing.T) {
require.Equal(t, groupCount, storedCount)
})
t.Run("update with higher group count", func(t *testing.T) {
t.Run("update with higher group count and higher slot", func(t *testing.T) {
const (
initialSlot = primitives.Slot(100)
initialCount = uint64(5)
@@ -111,6 +112,150 @@ func TestUpdateCustodyInfo(t *testing.T) {
require.Equal(t, groupCount, storedCount)
})
t.Run("update with higher group count and lower slot should preserve higher slot", func(t *testing.T) {
// This is the bug scenario: when switching from normal mode to semi-supernode,
// the incoming slot might be lower than the stored slot, but we should preserve
// the higher stored slot to avoid advertising that we can serve data we don't have.
const (
initialSlot = primitives.Slot(1835523) // Higher stored slot
initialCount = uint64(10)
earliestSlot = primitives.Slot(1835456) // Lower incoming slot (e.g., from head slot)
groupCount = uint64(64) // Increasing custody (e.g., semi-supernode)
)
db := setupDB(t)
_, _, err := db.UpdateCustodyInfo(ctx, initialSlot, initialCount)
require.NoError(t, err)
// When custody count increases but slot is lower, the higher slot should be preserved
slot, count, err := db.UpdateCustodyInfo(ctx, earliestSlot, groupCount)
require.NoError(t, err)
require.Equal(t, initialSlot, slot, "earliestAvailableSlot should not decrease when custody group count increases")
require.Equal(t, groupCount, count)
// Verify in the database
storedSlot, storedCount := getCustodyInfoFromDB(t, db)
require.Equal(t, initialSlot, storedSlot, "stored slot should be the higher value")
require.Equal(t, groupCount, storedCount)
})
t.Run("pre-fulu scenario: checkpoint sync before fork, restart with semi-supernode", func(t *testing.T) {
// This test covers the pre-Fulu bug scenario:
// 1. Node starts with checkpoint sync BEFORE Fulu fork - uses EarliestSlot() (checkpoint block slot)
// 2. Validators connect after Fulu activates - maintainCustodyInfo() updates to head slot (higher)
// 3. Node restarts with --semi-supernode - updateCustodyInfoInDB uses EarliestSlot() again
// The bug was that step 3 would overwrite the higher slot from step 2.
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig()
cfg.FuluForkEpoch = 100
params.OverrideBeaconConfig(cfg)
fuluForkSlot, err := slots.EpochStart(cfg.FuluForkEpoch)
require.NoError(t, err)
// Derive slot values relative to Fulu fork
checkpointBlockSlot := fuluForkSlot - 10 // Checkpoint sync happened before Fulu
headSlot := fuluForkSlot + 5 // Head slot after Fulu activates
defaultCustody := cfg.CustodyRequirement // Default custody from config
validatorCustody := cfg.CustodyRequirement + 6 // Custody after validators connect
semiSupernodeCustody := cfg.NumberOfCustodyGroups // Semi-supernode custodies all groups
// Verify our test setup: checkpoint is pre-Fulu, head is post-Fulu
require.Equal(t, true, checkpointBlockSlot < fuluForkSlot, "checkpoint must be before Fulu fork")
require.Equal(t, true, headSlot >= fuluForkSlot, "head must be at or after Fulu fork")
db := setupDB(t)
// Step 1: Node starts with checkpoint sync (pre-Fulu)
// updateCustodyInfoInDB sees saved.Slot() < fuluForkSlot, so uses EarliestSlot()
slot, count, err := db.UpdateCustodyInfo(ctx, checkpointBlockSlot, defaultCustody)
require.NoError(t, err)
require.Equal(t, checkpointBlockSlot, slot)
require.Equal(t, defaultCustody, count)
// Step 2: Validators connect after Fulu activates, maintainCustodyInfo() runs
// Uses headSlot which is higher than checkpointBlockSlot
slot, count, err = db.UpdateCustodyInfo(ctx, headSlot, validatorCustody)
require.NoError(t, err)
require.Equal(t, headSlot, slot, "should update to head slot")
require.Equal(t, validatorCustody, count)
// Verify step 2 stored correctly
storedSlot, storedCount := getCustodyInfoFromDB(t, db)
require.Equal(t, headSlot, storedSlot)
require.Equal(t, validatorCustody, storedCount)
// Step 3: Restart with --semi-supernode
// updateCustodyInfoInDB sees saved.Slot() < fuluForkSlot, so uses EarliestSlot() again
slot, count, err = db.UpdateCustodyInfo(ctx, checkpointBlockSlot, semiSupernodeCustody)
require.NoError(t, err)
require.Equal(t, headSlot, slot, "earliestAvailableSlot should NOT decrease back to checkpoint slot")
require.Equal(t, semiSupernodeCustody, count)
// Verify the database preserved the higher slot
storedSlot, storedCount = getCustodyInfoFromDB(t, db)
require.Equal(t, headSlot, storedSlot, "stored slot should remain at head slot, not checkpoint slot")
require.Equal(t, semiSupernodeCustody, storedCount)
})
t.Run("post-fulu scenario: finalized slot lower than stored head slot", func(t *testing.T) {
// This test covers the post-Fulu bug scenario:
// Post-fork, updateCustodyInfoInDB uses saved.Slot() (finalized slot) directly,
// not EarliestSlot(). But the same bug can occur because:
// - maintainCustodyInfo() stores headSlot (higher)
// - Restart uses finalized slot (lower than head)
// Our fix ensures earliestAvailableSlot never decreases.
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig()
cfg.FuluForkEpoch = 100
params.OverrideBeaconConfig(cfg)
fuluForkSlot, err := slots.EpochStart(cfg.FuluForkEpoch)
require.NoError(t, err)
// Derive slot values relative to Fulu fork - all slots are AFTER Fulu
finalizedSlotAtStart := fuluForkSlot + 100 // Finalized slot at first start (post-Fulu)
headSlot := fuluForkSlot + 200 // Head slot when validators connect
finalizedSlotRestart := fuluForkSlot + 150 // Finalized slot at restart (< headSlot)
defaultCustody := cfg.CustodyRequirement // Default custody from config
validatorCustody := cfg.CustodyRequirement + 6 // Custody after validators connect
semiSupernodeCustody := cfg.NumberOfCustodyGroups // Semi-supernode custodies all groups
// Verify our test setup: all slots are post-Fulu
require.Equal(t, true, finalizedSlotAtStart >= fuluForkSlot, "finalized slot must be at or after Fulu fork")
require.Equal(t, true, headSlot >= fuluForkSlot, "head slot must be at or after Fulu fork")
require.Equal(t, true, finalizedSlotRestart >= fuluForkSlot, "restart finalized slot must be at or after Fulu fork")
require.Equal(t, true, finalizedSlotRestart < headSlot, "restart finalized slot must be less than head slot")
db := setupDB(t)
// Step 1: Node starts post-Fulu
// updateCustodyInfoInDB sees saved.Slot() >= fuluForkSlot, so uses saved.Slot() directly
slot, count, err := db.UpdateCustodyInfo(ctx, finalizedSlotAtStart, defaultCustody)
require.NoError(t, err)
require.Equal(t, finalizedSlotAtStart, slot)
require.Equal(t, defaultCustody, count)
// Step 2: Validators connect, maintainCustodyInfo() uses head slot
slot, count, err = db.UpdateCustodyInfo(ctx, headSlot, validatorCustody)
require.NoError(t, err)
require.Equal(t, headSlot, slot)
require.Equal(t, validatorCustody, count)
// Step 3: Restart with --semi-supernode
// updateCustodyInfoInDB uses finalized slot which is lower than stored head slot
slot, count, err = db.UpdateCustodyInfo(ctx, finalizedSlotRestart, semiSupernodeCustody)
require.NoError(t, err)
require.Equal(t, headSlot, slot, "earliestAvailableSlot should NOT decrease to finalized slot")
require.Equal(t, semiSupernodeCustody, count)
// Verify database preserved the higher slot
storedSlot, storedCount := getCustodyInfoFromDB(t, db)
require.Equal(t, headSlot, storedSlot)
require.Equal(t, semiSupernodeCustody, storedCount)
})
t.Run("update with lower group count should not update", func(t *testing.T) {
const (
initialSlot = primitives.Slot(200)
@@ -274,6 +419,17 @@ func TestUpdateSubscribedToAllDataSubnets(t *testing.T) {
require.Equal(t, false, stored)
})
t.Run("initial update with empty database - set to true", func(t *testing.T) {
db := setupDB(t)
prev, err := db.UpdateSubscribedToAllDataSubnets(ctx, true)
require.NoError(t, err)
require.Equal(t, false, prev)
stored := getSubscriptionStatusFromDB(t, db)
require.Equal(t, true, stored)
})
t.Run("attempt to update from true to false (should not change)", func(t *testing.T) {
db := setupDB(t)
@@ -288,7 +444,7 @@ func TestUpdateSubscribedToAllDataSubnets(t *testing.T) {
require.Equal(t, true, stored)
})
t.Run("attempt to update from true to false (should not change)", func(t *testing.T) {
t.Run("update from true to true (no change)", func(t *testing.T) {
db := setupDB(t)
_, err := db.UpdateSubscribedToAllDataSubnets(ctx, true)

View File

@@ -2,6 +2,13 @@ package kv
import "bytes"
func hasPhase0Key(enc []byte) bool {
if len(phase0Key) >= len(enc) {
return false
}
return bytes.Equal(enc[:len(phase0Key)], phase0Key)
}
// In order for an encoding to be Altair compatible, it must be prefixed with altair key.
func hasAltairKey(enc []byte) bool {
if len(altairKey) >= len(enc) {

View File

@@ -91,6 +91,7 @@ type Store struct {
blockCache *ristretto.Cache[string, interfaces.ReadOnlySignedBeaconBlock]
validatorEntryCache *ristretto.Cache[[]byte, *ethpb.Validator]
stateSummaryCache *stateSummaryCache
stateDiffCache *stateDiffCache
ctx context.Context
}
@@ -112,6 +113,7 @@ var Buckets = [][]byte{
lightClientUpdatesBucket,
lightClientBootstrapBucket,
lightClientSyncCommitteeBucket,
stateDiffBucket,
// Indices buckets.
blockSlotIndicesBucket,
stateSlotIndicesBucket,
@@ -201,6 +203,14 @@ func NewKVStore(ctx context.Context, dirPath string, opts ...KVStoreOption) (*St
return nil, err
}
if features.Get().EnableStateDiff {
sdCache, err := newStateDiffCache(kv)
if err != nil {
return nil, err
}
kv.stateDiffCache = sdCache
}
return kv, nil
}

View File

@@ -216,6 +216,10 @@ func TestStore_LightClientUpdate_CanSaveRetrieve(t *testing.T) {
db := setupDB(t)
ctx := t.Context()
for _, testVersion := range version.All()[1:] {
if testVersion == version.Gloas {
// TODO(16027): Unskip light client tests for Gloas
continue
}
t.Run(version.String(testVersion), func(t *testing.T) {
update, err := createUpdate(t, testVersion)
require.NoError(t, err)
@@ -572,6 +576,10 @@ func TestStore_LightClientBootstrap_CanSaveRetrieve(t *testing.T) {
require.IsNil(t, retrievedBootstrap)
})
for _, testVersion := range version.All()[1:] {
if testVersion == version.Gloas {
// TODO(16027): Unskip light client tests for Gloas
continue
}
t.Run(version.String(testVersion), func(t *testing.T) {
bootstrap, err := createDefaultLightClientBootstrap(primitives.Slot(uint64(params.BeaconConfig().VersionToForkEpochMap()[testVersion]) * uint64(params.BeaconConfig().SlotsPerEpoch)))
require.NoError(t, err)

View File

@@ -16,6 +16,7 @@ var (
stateValidatorsBucket = []byte("state-validators")
feeRecipientBucket = []byte("fee-recipient")
registrationBucket = []byte("registration")
stateDiffBucket = []byte("state-diff")
// Light Client Updates Bucket
lightClientUpdatesBucket = []byte("light-client-updates")
@@ -46,6 +47,7 @@ var (
// Below keys are used to identify objects are to be fork compatible.
// Objects that are only compatible with specific forks should be prefixed with such keys.
phase0Key = []byte("phase0")
altairKey = []byte("altair")
bellatrixKey = []byte("merge")
bellatrixBlindKey = []byte("blind-bellatrix")

View File

@@ -8,7 +8,6 @@ import (
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
statenative "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native"
"github.com/OffchainLabs/prysm/v7/config/features"
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
"github.com/OffchainLabs/prysm/v7/genesis"
@@ -28,6 +27,17 @@ func (s *Store) State(ctx context.Context, blockRoot [32]byte) (state.BeaconStat
ctx, span := trace.StartSpan(ctx, "BeaconDB.State")
defer span.End()
startTime := time.Now()
// If state diff is enabled, we get the state from the state-diff db.
if features.Get().EnableStateDiff {
st, err := s.getStateUsingStateDiff(ctx, blockRoot)
if err != nil {
return nil, err
}
stateReadingTime.Observe(float64(time.Since(startTime).Milliseconds()))
return st, nil
}
enc, err := s.stateBytes(ctx, blockRoot)
if err != nil {
return nil, err
@@ -417,6 +427,16 @@ func (s *Store) storeValidatorEntriesSeparately(ctx context.Context, tx *bolt.Tx
func (s *Store) HasState(ctx context.Context, blockRoot [32]byte) bool {
_, span := trace.StartSpan(ctx, "BeaconDB.HasState")
defer span.End()
if features.Get().EnableStateDiff {
hasState, err := s.hasStateUsingStateDiff(ctx, blockRoot)
if err != nil {
log.WithError(err).Error(fmt.Sprintf("error checking state existence using state-diff"))
return false
}
return hasState
}
hasState := false
err := s.db.View(func(tx *bolt.Tx) error {
bkt := tx.Bucket(stateBucket)
@@ -470,7 +490,7 @@ func (s *Store) DeleteState(ctx context.Context, blockRoot [32]byte) error {
return nil
}
slot, err := s.slotByBlockRoot(ctx, tx, blockRoot[:])
slot, err := s.SlotByBlockRoot(ctx, blockRoot)
if err != nil {
return err
}
@@ -812,50 +832,45 @@ func (s *Store) stateBytes(ctx context.Context, blockRoot [32]byte) ([]byte, err
return dst, err
}
// slotByBlockRoot retrieves the corresponding slot of the input block root.
func (s *Store) slotByBlockRoot(ctx context.Context, tx *bolt.Tx, blockRoot []byte) (primitives.Slot, error) {
ctx, span := trace.StartSpan(ctx, "BeaconDB.slotByBlockRoot")
// SlotByBlockRoot returns the slot of the input block root, based on state summary, block, or state.
// Check for state is only done if state diff feature is not enabled.
func (s *Store) SlotByBlockRoot(ctx context.Context, blockRoot [32]byte) (primitives.Slot, error) {
ctx, span := trace.StartSpan(ctx, "BeaconDB.SlotByBlockRoot")
defer span.End()
bkt := tx.Bucket(stateSummaryBucket)
enc := bkt.Get(blockRoot)
if enc == nil {
// Fall back to check the block.
bkt := tx.Bucket(blocksBucket)
enc := bkt.Get(blockRoot)
if enc == nil {
// Fallback and check the state.
bkt = tx.Bucket(stateBucket)
enc = bkt.Get(blockRoot)
if enc == nil {
return 0, errors.New("state enc can't be nil")
}
// no need to construct the validator entries as it is not used here.
s, err := s.unmarshalState(ctx, enc, nil)
if err != nil {
return 0, errors.Wrap(err, "could not unmarshal state")
}
if s == nil || s.IsNil() {
return 0, errors.New("state can't be nil")
}
return s.Slot(), nil
}
b, err := unmarshalBlock(ctx, enc)
if err != nil {
return 0, errors.Wrap(err, "could not unmarshal block")
}
if err := blocks.BeaconBlockIsNil(b); err != nil {
return 0, err
}
return b.Block().Slot(), nil
// check state summary first
stateSummary, err := s.StateSummary(ctx, blockRoot)
if err != nil {
return 0, err
}
stateSummary := &ethpb.StateSummary{}
if err := decode(ctx, enc, stateSummary); err != nil {
return 0, errors.Wrap(err, "could not unmarshal state summary")
if stateSummary != nil {
return stateSummary.Slot, nil
}
return stateSummary.Slot, nil
// fall back to block if state summary is not found
blk, err := s.Block(ctx, blockRoot)
if err != nil {
return 0, err
}
if blk != nil && !blk.IsNil() {
return blk.Block().Slot(), nil
}
// fall back to state, only if state diff feature is not enabled
if features.Get().EnableStateDiff {
return 0, errors.New("neither state summary nor block found")
}
st, err := s.State(ctx, blockRoot)
if err != nil {
return 0, err
}
if st != nil && !st.IsNil() {
return st.Slot(), nil
}
// neither state summary, block nor state found
return 0, errors.New("neither state summary, block nor state found")
}
// HighestSlotStatesBelow returns the states with the highest slot below the input slot
@@ -1031,3 +1046,30 @@ func (s *Store) isStateValidatorMigrationOver() (bool, error) {
}
return returnFlag, nil
}
func (s *Store) getStateUsingStateDiff(ctx context.Context, blockRoot [32]byte) (state.BeaconState, error) {
slot, err := s.SlotByBlockRoot(ctx, blockRoot)
if err != nil {
return nil, err
}
st, err := s.stateByDiff(ctx, slot)
if err != nil {
return nil, err
}
if st == nil || st.IsNil() {
return nil, errors.New("state not found")
}
return st, nil
}
func (s *Store) hasStateUsingStateDiff(ctx context.Context, blockRoot [32]byte) (bool, error) {
slot, err := s.SlotByBlockRoot(ctx, blockRoot)
if err != nil {
return false, err
}
stateLvl := computeLevel(s.getOffset(), slot)
return stateLvl != -1, nil
}

View File

@@ -0,0 +1,232 @@
package kv
import (
"context"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"github.com/OffchainLabs/prysm/v7/cmd/beacon-chain/flags"
"github.com/OffchainLabs/prysm/v7/consensus-types/hdiff"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace"
"github.com/pkg/errors"
bolt "go.etcd.io/bbolt"
)
const (
stateSuffix = "_s"
validatorSuffix = "_v"
balancesSuffix = "_b"
)
/*
We use a level-based approach to save state diffs. Each level corresponds to an exponent of 2 (exponents[lvl]).
The data at level 0 is saved every 2**exponent[0] slots and always contains a full state snapshot that is used as a base for the delta saved at other levels.
*/
// saveStateByDiff takes a state and decides between saving a full state snapshot or a diff.
func (s *Store) saveStateByDiff(ctx context.Context, st state.ReadOnlyBeaconState) error {
_, span := trace.StartSpan(ctx, "BeaconDB.saveStateByDiff")
defer span.End()
if st == nil {
return errors.New("state is nil")
}
slot := st.Slot()
offset := s.getOffset()
if uint64(slot) < offset {
return ErrSlotBeforeOffset
}
// Find the level to save the state.
lvl := computeLevel(offset, slot)
if lvl == -1 {
return nil
}
// Save full state if level is 0.
if lvl == 0 {
return s.saveFullSnapshot(st)
}
// Get anchor state to compute the diff from.
anchorState, err := s.getAnchorState(offset, lvl, slot)
if err != nil {
return err
}
return s.saveHdiff(lvl, anchorState, st)
}
// stateByDiff retrieves the full state for a given slot.
func (s *Store) stateByDiff(ctx context.Context, slot primitives.Slot) (state.BeaconState, error) {
offset := s.getOffset()
if uint64(slot) < offset {
return nil, ErrSlotBeforeOffset
}
snapshot, diffChain, err := s.getBaseAndDiffChain(offset, slot)
if err != nil {
return nil, err
}
for _, diff := range diffChain {
if err := ctx.Err(); err != nil {
return nil, err
}
snapshot, err = hdiff.ApplyDiff(ctx, snapshot, diff)
if err != nil {
return nil, err
}
}
return snapshot, nil
}
// saveHdiff computes the diff between the anchor state and the current state and saves it to the database.
// This function needs to be called only with the latest finalized state, and in a strictly increasing slot order.
func (s *Store) saveHdiff(lvl int, anchor, st state.ReadOnlyBeaconState) error {
slot := uint64(st.Slot())
key := makeKeyForStateDiffTree(lvl, slot)
diff, err := hdiff.Diff(anchor, st)
if err != nil {
return err
}
err = s.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket(stateDiffBucket)
if bucket == nil {
return bolt.ErrBucketNotFound
}
buf := append(key, stateSuffix...)
if err := bucket.Put(buf, diff.StateDiff); err != nil {
return err
}
buf = append(key, validatorSuffix...)
if err := bucket.Put(buf, diff.ValidatorDiffs); err != nil {
return err
}
buf = append(key, balancesSuffix...)
if err := bucket.Put(buf, diff.BalancesDiff); err != nil {
return err
}
return nil
})
if err != nil {
return err
}
// Save the full state to the cache (if not the last level).
if lvl != len(flags.Get().StateDiffExponents)-1 {
err = s.stateDiffCache.setAnchor(lvl, st)
if err != nil {
return err
}
}
return nil
}
// SaveFullSnapshot saves the full level 0 state snapshot to the database.
func (s *Store) saveFullSnapshot(st state.ReadOnlyBeaconState) error {
slot := uint64(st.Slot())
key := makeKeyForStateDiffTree(0, slot)
stateBytes, err := st.MarshalSSZ()
if err != nil {
return err
}
// add version key to value
enc, err := addKey(st.Version(), stateBytes)
if err != nil {
return err
}
err = s.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket(stateDiffBucket)
if bucket == nil {
return bolt.ErrBucketNotFound
}
if err := bucket.Put(key, enc); err != nil {
return err
}
return nil
})
if err != nil {
return err
}
// Save the full state to the cache, and invalidate other levels.
s.stateDiffCache.clearAnchors()
err = s.stateDiffCache.setAnchor(0, st)
if err != nil {
return err
}
return nil
}
func (s *Store) getDiff(lvl int, slot uint64) (hdiff.HdiffBytes, error) {
key := makeKeyForStateDiffTree(lvl, slot)
var stateDiff []byte
var validatorDiff []byte
var balancesDiff []byte
err := s.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket(stateDiffBucket)
if bucket == nil {
return bolt.ErrBucketNotFound
}
buf := append(key, stateSuffix...)
stateDiff = bucket.Get(buf)
if stateDiff == nil {
return errors.New("state diff not found")
}
buf = append(key, validatorSuffix...)
validatorDiff = bucket.Get(buf)
if validatorDiff == nil {
return errors.New("validator diff not found")
}
buf = append(key, balancesSuffix...)
balancesDiff = bucket.Get(buf)
if balancesDiff == nil {
return errors.New("balances diff not found")
}
return nil
})
if err != nil {
return hdiff.HdiffBytes{}, err
}
return hdiff.HdiffBytes{
StateDiff: stateDiff,
ValidatorDiffs: validatorDiff,
BalancesDiff: balancesDiff,
}, nil
}
func (s *Store) getFullSnapshot(slot uint64) (state.BeaconState, error) {
key := makeKeyForStateDiffTree(0, slot)
var enc []byte
err := s.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket(stateDiffBucket)
if bucket == nil {
return bolt.ErrBucketNotFound
}
enc = bucket.Get(key)
if enc == nil {
return errors.New("state not found")
}
return nil
})
if err != nil {
return nil, err
}
return decodeStateSnapshot(enc)
}

View File

@@ -0,0 +1,77 @@
package kv
import (
"encoding/binary"
"errors"
"sync"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"github.com/OffchainLabs/prysm/v7/cmd/beacon-chain/flags"
"go.etcd.io/bbolt"
)
type stateDiffCache struct {
sync.RWMutex
anchors []state.ReadOnlyBeaconState
offset uint64
}
func newStateDiffCache(s *Store) (*stateDiffCache, error) {
var offset uint64
err := s.db.View(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(stateDiffBucket)
if bucket == nil {
return bbolt.ErrBucketNotFound
}
offsetBytes := bucket.Get([]byte("offset"))
if offsetBytes == nil {
return errors.New("state diff cache: offset not found")
}
offset = binary.LittleEndian.Uint64(offsetBytes)
return nil
})
if err != nil {
return nil, err
}
return &stateDiffCache{
anchors: make([]state.ReadOnlyBeaconState, len(flags.Get().StateDiffExponents)-1), // -1 because last level doesn't need to be cached
offset: offset,
}, nil
}
func (c *stateDiffCache) getAnchor(level int) state.ReadOnlyBeaconState {
c.RLock()
defer c.RUnlock()
return c.anchors[level]
}
func (c *stateDiffCache) setAnchor(level int, anchor state.ReadOnlyBeaconState) error {
c.Lock()
defer c.Unlock()
if level >= len(c.anchors) || level < 0 {
return errors.New("state diff cache: anchor level out of range")
}
c.anchors[level] = anchor
return nil
}
func (c *stateDiffCache) getOffset() uint64 {
c.RLock()
defer c.RUnlock()
return c.offset
}
func (c *stateDiffCache) setOffset(offset uint64) {
c.Lock()
defer c.Unlock()
c.offset = offset
}
func (c *stateDiffCache) clearAnchors() {
c.Lock()
defer c.Unlock()
c.anchors = make([]state.ReadOnlyBeaconState, len(flags.Get().StateDiffExponents)-1) // -1 because last level doesn't need to be cached
}

View File

@@ -0,0 +1,250 @@
package kv
import (
"context"
"encoding/binary"
"errors"
"fmt"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
statenative "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native"
"github.com/OffchainLabs/prysm/v7/cmd/beacon-chain/flags"
"github.com/OffchainLabs/prysm/v7/consensus-types/hdiff"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/math"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/runtime/version"
"go.etcd.io/bbolt"
)
var (
offsetKey = []byte("offset")
ErrSlotBeforeOffset = errors.New("slot is before root offset")
)
func makeKeyForStateDiffTree(level int, slot uint64) []byte {
buf := make([]byte, 16)
buf[0] = byte(level)
binary.LittleEndian.PutUint64(buf[1:], slot)
return buf
}
func (s *Store) getAnchorState(offset uint64, lvl int, slot primitives.Slot) (anchor state.ReadOnlyBeaconState, err error) {
if lvl <= 0 || lvl > len(flags.Get().StateDiffExponents) {
return nil, errors.New("invalid value for level")
}
if uint64(slot) < offset {
return nil, ErrSlotBeforeOffset
}
relSlot := uint64(slot) - offset
prevExp := flags.Get().StateDiffExponents[lvl-1]
if prevExp < 2 || prevExp >= 64 {
return nil, fmt.Errorf("state diff exponent %d out of range for uint64", prevExp)
}
span := math.PowerOf2(uint64(prevExp))
anchorSlot := primitives.Slot(uint64(slot) - relSlot%span)
// anchorLvl can be [0, lvl-1]
anchorLvl := computeLevel(offset, anchorSlot)
if anchorLvl == -1 {
return nil, errors.New("could not compute anchor level")
}
// Check if we have the anchor in cache.
anchor = s.stateDiffCache.getAnchor(anchorLvl)
if anchor != nil {
return anchor, nil
}
// If not, load it from the database.
anchor, err = s.stateByDiff(context.Background(), anchorSlot)
if err != nil {
return nil, err
}
// Save it in the cache.
err = s.stateDiffCache.setAnchor(anchorLvl, anchor)
if err != nil {
return nil, err
}
return anchor, nil
}
// computeLevel computes the level in the diff tree. Returns -1 in case slot should not be in tree.
func computeLevel(offset uint64, slot primitives.Slot) int {
rel := uint64(slot) - offset
for i, exp := range flags.Get().StateDiffExponents {
if exp < 2 || exp >= 64 {
return -1
}
span := math.PowerOf2(uint64(exp))
if rel%span == 0 {
return i
}
}
// If rel isnt on any of the boundaries, we should ignore saving it.
return -1
}
func (s *Store) setOffset(slot primitives.Slot) error {
err := s.db.Update(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(stateDiffBucket)
if bucket == nil {
return bbolt.ErrBucketNotFound
}
offsetBytes := bucket.Get(offsetKey)
if offsetBytes != nil {
return fmt.Errorf("offset already set to %d", binary.LittleEndian.Uint64(offsetBytes))
}
offsetBytes = make([]byte, 8)
binary.LittleEndian.PutUint64(offsetBytes, uint64(slot))
if err := bucket.Put(offsetKey, offsetBytes); err != nil {
return err
}
return nil
})
if err != nil {
return err
}
// Save the offset in the cache.
s.stateDiffCache.setOffset(uint64(slot))
return nil
}
func (s *Store) getOffset() uint64 {
return s.stateDiffCache.getOffset()
}
func keyForSnapshot(v int) ([]byte, error) {
switch v {
case version.Fulu:
return fuluKey, nil
case version.Electra:
return ElectraKey, nil
case version.Deneb:
return denebKey, nil
case version.Capella:
return capellaKey, nil
case version.Bellatrix:
return bellatrixKey, nil
case version.Altair:
return altairKey, nil
case version.Phase0:
return phase0Key, nil
default:
return nil, errors.New("unsupported fork")
}
}
func addKey(v int, bytes []byte) ([]byte, error) {
key, err := keyForSnapshot(v)
if err != nil {
return nil, err
}
enc := make([]byte, len(key)+len(bytes))
copy(enc, key)
copy(enc[len(key):], bytes)
return enc, nil
}
func decodeStateSnapshot(enc []byte) (state.BeaconState, error) {
switch {
case hasFuluKey(enc):
var fuluState ethpb.BeaconStateFulu
if err := fuluState.UnmarshalSSZ(enc[len(fuluKey):]); err != nil {
return nil, err
}
return statenative.InitializeFromProtoUnsafeFulu(&fuluState)
case HasElectraKey(enc):
var electraState ethpb.BeaconStateElectra
if err := electraState.UnmarshalSSZ(enc[len(ElectraKey):]); err != nil {
return nil, err
}
return statenative.InitializeFromProtoUnsafeElectra(&electraState)
case hasDenebKey(enc):
var denebState ethpb.BeaconStateDeneb
if err := denebState.UnmarshalSSZ(enc[len(denebKey):]); err != nil {
return nil, err
}
return statenative.InitializeFromProtoUnsafeDeneb(&denebState)
case hasCapellaKey(enc):
var capellaState ethpb.BeaconStateCapella
if err := capellaState.UnmarshalSSZ(enc[len(capellaKey):]); err != nil {
return nil, err
}
return statenative.InitializeFromProtoUnsafeCapella(&capellaState)
case hasBellatrixKey(enc):
var bellatrixState ethpb.BeaconStateBellatrix
if err := bellatrixState.UnmarshalSSZ(enc[len(bellatrixKey):]); err != nil {
return nil, err
}
return statenative.InitializeFromProtoUnsafeBellatrix(&bellatrixState)
case hasAltairKey(enc):
var altairState ethpb.BeaconStateAltair
if err := altairState.UnmarshalSSZ(enc[len(altairKey):]); err != nil {
return nil, err
}
return statenative.InitializeFromProtoUnsafeAltair(&altairState)
case hasPhase0Key(enc):
var phase0State ethpb.BeaconState
if err := phase0State.UnmarshalSSZ(enc[len(phase0Key):]); err != nil {
return nil, err
}
return statenative.InitializeFromProtoUnsafePhase0(&phase0State)
default:
return nil, errors.New("unsupported fork")
}
}
func (s *Store) getBaseAndDiffChain(offset uint64, slot primitives.Slot) (state.BeaconState, []hdiff.HdiffBytes, error) {
if uint64(slot) < offset {
return nil, nil, ErrSlotBeforeOffset
}
rel := uint64(slot) - offset
lvl := computeLevel(offset, slot)
if lvl == -1 {
return nil, nil, errors.New("slot not in tree")
}
exponents := flags.Get().StateDiffExponents
baseSpan := math.PowerOf2(uint64(exponents[0]))
baseAnchorSlot := uint64(slot) - rel%baseSpan
type diffItem struct {
level int
slot uint64
}
var diffChainItems []diffItem
lastSeenAnchorSlot := baseAnchorSlot
for i, exp := range exponents[1 : lvl+1] {
span := math.PowerOf2(uint64(exp))
diffSlot := rel / span * span
if diffSlot == lastSeenAnchorSlot {
continue
}
diffChainItems = append(diffChainItems, diffItem{level: i + 1, slot: diffSlot + offset})
lastSeenAnchorSlot = diffSlot
}
baseSnapshot, err := s.getFullSnapshot(baseAnchorSlot)
if err != nil {
return nil, nil, err
}
diffChain := make([]hdiff.HdiffBytes, 0, len(diffChainItems))
for _, item := range diffChainItems {
diff, err := s.getDiff(item.level, item.slot)
if err != nil {
return nil, nil, err
}
diffChain = append(diffChain, diff)
}
return baseSnapshot, diffChain, nil
}

View File

@@ -0,0 +1,662 @@
package kv
import (
"context"
"encoding/binary"
"fmt"
"math/rand"
"testing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"github.com/OffchainLabs/prysm/v7/cmd/beacon-chain/flags"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/math"
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"
"go.etcd.io/bbolt"
)
func TestStateDiff_LoadOrInitOffset(t *testing.T) {
setDefaultStateDiffExponents()
db := setupDB(t)
err := setOffsetInDB(db, 10)
require.NoError(t, err)
offset := db.getOffset()
require.Equal(t, uint64(10), offset)
err = db.setOffset(10)
require.ErrorContains(t, "offset already set", err)
offset = db.getOffset()
require.Equal(t, uint64(10), offset)
}
func TestStateDiff_ComputeLevel(t *testing.T) {
db := setupDB(t)
setDefaultStateDiffExponents()
err := setOffsetInDB(db, 0)
require.NoError(t, err)
offset := db.getOffset()
// 2 ** 21
lvl := computeLevel(offset, primitives.Slot(math.PowerOf2(21)))
require.Equal(t, 0, lvl)
// 2 ** 21 * 3
lvl = computeLevel(offset, primitives.Slot(math.PowerOf2(21)*3))
require.Equal(t, 0, lvl)
// 2 ** 18
lvl = computeLevel(offset, primitives.Slot(math.PowerOf2(18)))
require.Equal(t, 1, lvl)
// 2 ** 18 * 3
lvl = computeLevel(offset, primitives.Slot(math.PowerOf2(18)*3))
require.Equal(t, 1, lvl)
// 2 ** 16
lvl = computeLevel(offset, primitives.Slot(math.PowerOf2(16)))
require.Equal(t, 2, lvl)
// 2 ** 16 * 3
lvl = computeLevel(offset, primitives.Slot(math.PowerOf2(16)*3))
require.Equal(t, 2, lvl)
// 2 ** 13
lvl = computeLevel(offset, primitives.Slot(math.PowerOf2(13)))
require.Equal(t, 3, lvl)
// 2 ** 13 * 3
lvl = computeLevel(offset, primitives.Slot(math.PowerOf2(13)*3))
require.Equal(t, 3, lvl)
// 2 ** 11
lvl = computeLevel(offset, primitives.Slot(math.PowerOf2(11)))
require.Equal(t, 4, lvl)
// 2 ** 11 * 3
lvl = computeLevel(offset, primitives.Slot(math.PowerOf2(11)*3))
require.Equal(t, 4, lvl)
// 2 ** 9
lvl = computeLevel(offset, primitives.Slot(math.PowerOf2(9)))
require.Equal(t, 5, lvl)
// 2 ** 9 * 3
lvl = computeLevel(offset, primitives.Slot(math.PowerOf2(9)*3))
require.Equal(t, 5, lvl)
// 2 ** 5
lvl = computeLevel(offset, primitives.Slot(math.PowerOf2(5)))
require.Equal(t, 6, lvl)
// 2 ** 5 * 3
lvl = computeLevel(offset, primitives.Slot(math.PowerOf2(5)*3))
require.Equal(t, 6, lvl)
// 2 ** 7
lvl = computeLevel(offset, primitives.Slot(math.PowerOf2(7)))
require.Equal(t, 6, lvl)
// 2 ** 5 + 1
lvl = computeLevel(offset, primitives.Slot(math.PowerOf2(5)+1))
require.Equal(t, -1, lvl)
// 2 ** 5 + 16
lvl = computeLevel(offset, primitives.Slot(math.PowerOf2(5)+16))
require.Equal(t, -1, lvl)
// 2 ** 5 + 32
lvl = computeLevel(offset, primitives.Slot(math.PowerOf2(5)+32))
require.Equal(t, 6, lvl)
}
func TestStateDiff_SaveFullSnapshot(t *testing.T) {
setDefaultStateDiffExponents()
for v := range version.All() {
t.Run(version.String(v), func(t *testing.T) {
db := setupDB(t)
// Create state with slot 0
st, enc := createState(t, 0, v)
err := setOffsetInDB(db, 0)
require.NoError(t, err)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
err = db.db.View(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(stateDiffBucket)
if bucket == nil {
return bbolt.ErrBucketNotFound
}
s := bucket.Get(makeKeyForStateDiffTree(0, uint64(0)))
if s == nil {
return bbolt.ErrIncompatibleValue
}
require.DeepSSZEqual(t, enc, s)
return nil
})
require.NoError(t, err)
})
}
}
func TestStateDiff_SaveAndReadFullSnapshot(t *testing.T) {
setDefaultStateDiffExponents()
for v := range version.All() {
t.Run(version.String(v), func(t *testing.T) {
db := setupDB(t)
st, _ := createState(t, 0, v)
err := setOffsetInDB(db, 0)
require.NoError(t, err)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
readSt, err := db.stateByDiff(context.Background(), 0)
require.NoError(t, err)
require.NotNil(t, readSt)
stSSZ, err := st.MarshalSSZ()
require.NoError(t, err)
readStSSZ, err := readSt.MarshalSSZ()
require.NoError(t, err)
require.DeepSSZEqual(t, stSSZ, readStSSZ)
})
}
}
func TestStateDiff_SaveDiff(t *testing.T) {
setDefaultStateDiffExponents()
for v := range version.All() {
t.Run(version.String(v), func(t *testing.T) {
db := setupDB(t)
// Create state with slot 2**21
slot := primitives.Slot(math.PowerOf2(21))
st, enc := createState(t, slot, v)
err := setOffsetInDB(db, uint64(slot))
require.NoError(t, err)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
err = db.db.View(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(stateDiffBucket)
if bucket == nil {
return bbolt.ErrBucketNotFound
}
s := bucket.Get(makeKeyForStateDiffTree(0, uint64(slot)))
if s == nil {
return bbolt.ErrIncompatibleValue
}
require.DeepSSZEqual(t, enc, s)
return nil
})
require.NoError(t, err)
// create state with slot 2**18 (+2**21)
slot = primitives.Slot(math.PowerOf2(18) + math.PowerOf2(21))
st, _ = createState(t, slot, v)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
key := makeKeyForStateDiffTree(1, uint64(slot))
err = db.db.View(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(stateDiffBucket)
if bucket == nil {
return bbolt.ErrBucketNotFound
}
buf := append(key, "_s"...)
s := bucket.Get(buf)
if s == nil {
return bbolt.ErrIncompatibleValue
}
buf = append(key, "_v"...)
v := bucket.Get(buf)
if v == nil {
return bbolt.ErrIncompatibleValue
}
buf = append(key, "_b"...)
b := bucket.Get(buf)
if b == nil {
return bbolt.ErrIncompatibleValue
}
return nil
})
require.NoError(t, err)
})
}
}
func TestStateDiff_SaveAndReadDiff(t *testing.T) {
setDefaultStateDiffExponents()
for v := range version.All() {
t.Run(version.String(v), func(t *testing.T) {
db := setupDB(t)
st, _ := createState(t, 0, v)
err := setOffsetInDB(db, 0)
require.NoError(t, err)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
slot := primitives.Slot(math.PowerOf2(5))
st, _ = createState(t, slot, v)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
readSt, err := db.stateByDiff(context.Background(), slot)
require.NoError(t, err)
require.NotNil(t, readSt)
stSSZ, err := st.MarshalSSZ()
require.NoError(t, err)
readStSSZ, err := readSt.MarshalSSZ()
require.NoError(t, err)
require.DeepSSZEqual(t, stSSZ, readStSSZ)
})
}
}
func TestStateDiff_SaveAndReadDiff_WithRepetitiveAnchorSlots(t *testing.T) {
globalFlags := flags.GlobalFlags{
StateDiffExponents: []int{20, 14, 10, 7, 5},
}
flags.Init(&globalFlags)
for v := range version.All() {
t.Run(version.String(v), func(t *testing.T) {
db := setupDB(t)
err := setOffsetInDB(db, 0)
st, _ := createState(t, 0, v)
require.NoError(t, err)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
slot := primitives.Slot(math.PowerOf2(11))
st, _ = createState(t, slot, v)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
slot = primitives.Slot(math.PowerOf2(11) + math.PowerOf2(5))
st, _ = createState(t, slot, v)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
readSt, err := db.stateByDiff(context.Background(), slot)
require.NoError(t, err)
require.NotNil(t, readSt)
stSSZ, err := st.MarshalSSZ()
require.NoError(t, err)
readStSSZ, err := readSt.MarshalSSZ()
require.NoError(t, err)
require.DeepSSZEqual(t, stSSZ, readStSSZ)
})
}
}
func TestStateDiff_SaveAndReadDiff_MultipleLevels(t *testing.T) {
setDefaultStateDiffExponents()
for v := range version.All() {
t.Run(version.String(v), func(t *testing.T) {
db := setupDB(t)
st, _ := createState(t, 0, v)
err := setOffsetInDB(db, 0)
require.NoError(t, err)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
slot := primitives.Slot(math.PowerOf2(11))
st, _ = createState(t, slot, v)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
readSt, err := db.stateByDiff(context.Background(), slot)
require.NoError(t, err)
require.NotNil(t, readSt)
stSSZ, err := st.MarshalSSZ()
require.NoError(t, err)
readStSSZ, err := readSt.MarshalSSZ()
require.NoError(t, err)
require.DeepSSZEqual(t, stSSZ, readStSSZ)
slot = primitives.Slot(math.PowerOf2(11) + math.PowerOf2(9))
st, _ = createState(t, slot, v)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
readSt, err = db.stateByDiff(context.Background(), slot)
require.NoError(t, err)
require.NotNil(t, readSt)
stSSZ, err = st.MarshalSSZ()
require.NoError(t, err)
readStSSZ, err = readSt.MarshalSSZ()
require.NoError(t, err)
require.DeepSSZEqual(t, stSSZ, readStSSZ)
slot = primitives.Slot(math.PowerOf2(11) + math.PowerOf2(9) + math.PowerOf2(5))
st, _ = createState(t, slot, v)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
readSt, err = db.stateByDiff(context.Background(), slot)
require.NoError(t, err)
require.NotNil(t, readSt)
stSSZ, err = st.MarshalSSZ()
require.NoError(t, err)
readStSSZ, err = readSt.MarshalSSZ()
require.NoError(t, err)
require.DeepSSZEqual(t, stSSZ, readStSSZ)
})
}
}
func TestStateDiff_SaveAndReadDiffForkTransition(t *testing.T) {
setDefaultStateDiffExponents()
for v := range version.All()[:len(version.All())-1] {
t.Run(version.String(v), func(t *testing.T) {
db := setupDB(t)
st, _ := createState(t, 0, v)
err := setOffsetInDB(db, 0)
require.NoError(t, err)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
slot := primitives.Slot(math.PowerOf2(5))
st, _ = createState(t, slot, v+1)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
readSt, err := db.stateByDiff(context.Background(), slot)
require.NoError(t, err)
require.NotNil(t, readSt)
stSSZ, err := st.MarshalSSZ()
require.NoError(t, err)
readStSSZ, err := readSt.MarshalSSZ()
require.NoError(t, err)
require.DeepSSZEqual(t, stSSZ, readStSSZ)
})
}
}
func TestStateDiff_OffsetCache(t *testing.T) {
setDefaultStateDiffExponents()
// test for slot numbers 0 and 1 for every version
for slotNum := range 2 {
for v := range version.All() {
t.Run(fmt.Sprintf("slotNum=%d,%s", slotNum, version.String(v)), func(t *testing.T) {
db := setupDB(t)
slot := primitives.Slot(slotNum)
err := setOffsetInDB(db, uint64(slot))
require.NoError(t, err)
st, _ := createState(t, slot, v)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
offset := db.stateDiffCache.getOffset()
require.Equal(t, uint64(slotNum), offset)
slot2 := primitives.Slot(uint64(slotNum) + math.PowerOf2(uint64(flags.Get().StateDiffExponents[0])))
st2, _ := createState(t, slot2, v)
err = db.saveStateByDiff(context.Background(), st2)
require.NoError(t, err)
offset = db.stateDiffCache.getOffset()
require.Equal(t, uint64(slot), offset)
})
}
}
}
func TestStateDiff_AnchorCache(t *testing.T) {
setDefaultStateDiffExponents()
for v := range version.All() {
t.Run(version.String(v), func(t *testing.T) {
exponents := flags.Get().StateDiffExponents
localCache := make([]state.ReadOnlyBeaconState, len(exponents)-1)
db := setupDB(t)
err := setOffsetInDB(db, 0) // lvl 0
require.NoError(t, err)
// at first the cache should be empty
for i := 0; i < len(flags.Get().StateDiffExponents)-1; i++ {
anchor := db.stateDiffCache.getAnchor(i)
require.IsNil(t, anchor)
}
// add level 0
slot := primitives.Slot(0) // offset 0 is already set
st, _ := createState(t, slot, v)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
localCache[0] = st
// level 0 should be the same
require.DeepEqual(t, localCache[0], db.stateDiffCache.getAnchor(0))
// rest of the cache should be nil
for i := 1; i < len(exponents)-1; i++ {
require.IsNil(t, db.stateDiffCache.getAnchor(i))
}
// skip last level as it does not get cached
for i := len(exponents) - 2; i > 0; i-- {
slot = primitives.Slot(math.PowerOf2(uint64(exponents[i])))
st, _ := createState(t, slot, v)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
localCache[i] = st
// anchor cache must match local cache
for i := 0; i < len(exponents)-1; i++ {
if localCache[i] == nil {
require.IsNil(t, db.stateDiffCache.getAnchor(i))
continue
}
localSSZ, err := localCache[i].MarshalSSZ()
require.NoError(t, err)
anchorSSZ, err := db.stateDiffCache.getAnchor(i).MarshalSSZ()
require.NoError(t, err)
require.DeepSSZEqual(t, localSSZ, anchorSSZ)
}
}
// moving to a new tree should invalidate the cache except for level 0
twoTo21 := math.PowerOf2(21)
slot = primitives.Slot(twoTo21)
st, _ = createState(t, slot, v)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
localCache = make([]state.ReadOnlyBeaconState, len(exponents)-1)
localCache[0] = st
// level 0 should be the same
require.DeepEqual(t, localCache[0], db.stateDiffCache.getAnchor(0))
// rest of the cache should be nil
for i := 1; i < len(exponents)-1; i++ {
require.IsNil(t, db.stateDiffCache.getAnchor(i))
}
})
}
}
func TestStateDiff_EncodingAndDecoding(t *testing.T) {
for v := range version.All() {
t.Run(version.String(v), func(t *testing.T) {
st, enc := createState(t, 0, v) // this has addKey called inside
stDecoded, err := decodeStateSnapshot(enc)
require.NoError(t, err)
st1ssz, err := st.MarshalSSZ()
require.NoError(t, err)
st2ssz, err := stDecoded.MarshalSSZ()
require.NoError(t, err)
require.DeepSSZEqual(t, st1ssz, st2ssz)
})
}
}
func createState(t *testing.T, slot primitives.Slot, v int) (state.ReadOnlyBeaconState, []byte) {
p := params.BeaconConfig()
var st state.BeaconState
var err error
switch v {
case version.Phase0:
st, err = util.NewBeaconState()
require.NoError(t, err)
err = st.SetFork(&ethpb.Fork{
PreviousVersion: p.GenesisForkVersion,
CurrentVersion: p.GenesisForkVersion,
Epoch: 0,
})
require.NoError(t, err)
case version.Altair:
st, err = util.NewBeaconStateAltair()
require.NoError(t, err)
err = st.SetFork(&ethpb.Fork{
PreviousVersion: p.GenesisForkVersion,
CurrentVersion: p.AltairForkVersion,
Epoch: p.AltairForkEpoch,
})
require.NoError(t, err)
case version.Bellatrix:
st, err = util.NewBeaconStateBellatrix()
require.NoError(t, err)
err = st.SetFork(&ethpb.Fork{
PreviousVersion: p.AltairForkVersion,
CurrentVersion: p.BellatrixForkVersion,
Epoch: p.BellatrixForkEpoch,
})
require.NoError(t, err)
case version.Capella:
st, err = util.NewBeaconStateCapella()
require.NoError(t, err)
err = st.SetFork(&ethpb.Fork{
PreviousVersion: p.BellatrixForkVersion,
CurrentVersion: p.CapellaForkVersion,
Epoch: p.CapellaForkEpoch,
})
require.NoError(t, err)
case version.Deneb:
st, err = util.NewBeaconStateDeneb()
require.NoError(t, err)
err = st.SetFork(&ethpb.Fork{
PreviousVersion: p.CapellaForkVersion,
CurrentVersion: p.DenebForkVersion,
Epoch: p.DenebForkEpoch,
})
require.NoError(t, err)
case version.Electra:
st, err = util.NewBeaconStateElectra()
require.NoError(t, err)
err = st.SetFork(&ethpb.Fork{
PreviousVersion: p.DenebForkVersion,
CurrentVersion: p.ElectraForkVersion,
Epoch: p.ElectraForkEpoch,
})
require.NoError(t, err)
case version.Fulu:
st, err = util.NewBeaconStateFulu()
require.NoError(t, err)
err = st.SetFork(&ethpb.Fork{
PreviousVersion: p.ElectraForkVersion,
CurrentVersion: p.FuluForkVersion,
Epoch: p.FuluForkEpoch,
})
require.NoError(t, err)
default:
t.Fatalf("unsupported version: %d", v)
}
err = st.SetSlot(slot)
require.NoError(t, err)
slashings := make([]uint64, 8192)
slashings[0] = uint64(rand.Intn(10))
err = st.SetSlashings(slashings)
require.NoError(t, err)
stssz, err := st.MarshalSSZ()
require.NoError(t, err)
enc, err := addKey(v, stssz)
require.NoError(t, err)
return st, enc
}
func setOffsetInDB(s *Store, offset uint64) error {
err := s.db.Update(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(stateDiffBucket)
if bucket == nil {
return bbolt.ErrBucketNotFound
}
offsetBytes := bucket.Get(offsetKey)
if offsetBytes != nil {
return fmt.Errorf("offset already set to %d", binary.LittleEndian.Uint64(offsetBytes))
}
offsetBytes = make([]byte, 8)
binary.LittleEndian.PutUint64(offsetBytes, offset)
if err := bucket.Put(offsetKey, offsetBytes); err != nil {
return err
}
return nil
})
if err != nil {
return err
}
sdCache, err := newStateDiffCache(s)
if err != nil {
return err
}
s.stateDiffCache = sdCache
return nil
}
func setDefaultStateDiffExponents() {
globalFlags := flags.GlobalFlags{
StateDiffExponents: []int{21, 18, 16, 13, 11, 9, 5},
}
flags.Init(&globalFlags)
}

View File

@@ -1,6 +1,7 @@
package kv
import (
"context"
"crypto/rand"
"encoding/binary"
mathRand "math/rand"
@@ -9,6 +10,7 @@ import (
"time"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"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"
@@ -17,11 +19,14 @@ import (
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
"github.com/OffchainLabs/prysm/v7/genesis"
"github.com/OffchainLabs/prysm/v7/math"
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/assert"
"github.com/OffchainLabs/prysm/v7/testing/require"
"github.com/OffchainLabs/prysm/v7/testing/util"
logTest "github.com/sirupsen/logrus/hooks/test"
bolt "go.etcd.io/bbolt"
)
@@ -1329,3 +1334,297 @@ func TestStore_CleanUpDirtyStates_NoOriginRoot(t *testing.T) {
}
}
}
func TestStore_CanSaveRetrieveStateUsingStateDiff(t *testing.T) {
t.Run("No state summary or block", func(t *testing.T) {
db := setupDB(t)
featCfg := &features.Flags{}
featCfg.EnableStateDiff = true
reset := features.InitWithReset(featCfg)
defer reset()
setDefaultStateDiffExponents()
err := setOffsetInDB(db, 0)
require.NoError(t, err)
readSt, err := db.State(context.Background(), [32]byte{'A'})
require.IsNil(t, readSt)
require.ErrorContains(t, "neither state summary nor block found", err)
})
t.Run("Slot not in tree", func(t *testing.T) {
db := setupDB(t)
featCfg := &features.Flags{}
featCfg.EnableStateDiff = true
reset := features.InitWithReset(featCfg)
defer reset()
setDefaultStateDiffExponents()
err := setOffsetInDB(db, 0)
require.NoError(t, err)
r := bytesutil.ToBytes32([]byte{'A'})
ss := &ethpb.StateSummary{Slot: 1, Root: r[:]} // slot 1 not in tree
err = db.SaveStateSummary(context.Background(), ss)
require.NoError(t, err)
readSt, err := db.State(context.Background(), r)
require.ErrorContains(t, "slot not in tree", err)
require.IsNil(t, readSt)
})
t.Run("State not found", func(t *testing.T) {
db := setupDB(t)
featCfg := &features.Flags{}
featCfg.EnableStateDiff = true
reset := features.InitWithReset(featCfg)
defer reset()
setDefaultStateDiffExponents()
err := setOffsetInDB(db, 0)
require.NoError(t, err)
r := bytesutil.ToBytes32([]byte{'A'})
ss := &ethpb.StateSummary{Slot: 32, Root: r[:]} // slot 32 is in tree
err = db.SaveStateSummary(context.Background(), ss)
require.NoError(t, err)
readSt, err := db.State(context.Background(), r)
require.ErrorContains(t, "state not found", err)
require.IsNil(t, readSt)
})
t.Run("Full state snapshot", func(t *testing.T) {
t.Run("using state summary", func(t *testing.T) {
for v := range version.All() {
t.Run(version.String(v), func(t *testing.T) {
db := setupDB(t)
featCfg := &features.Flags{}
featCfg.EnableStateDiff = true
reset := features.InitWithReset(featCfg)
defer reset()
setDefaultStateDiffExponents()
err := setOffsetInDB(db, 0)
require.NoError(t, err)
st, _ := createState(t, 0, v)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
r := bytesutil.ToBytes32([]byte{'A'})
ss := &ethpb.StateSummary{Slot: 0, Root: r[:]}
err = db.SaveStateSummary(context.Background(), ss)
require.NoError(t, err)
readSt, err := db.State(context.Background(), r)
require.NoError(t, err)
require.NotNil(t, readSt)
stSSZ, err := st.MarshalSSZ()
require.NoError(t, err)
readStSSZ, err := readSt.MarshalSSZ()
require.NoError(t, err)
require.DeepSSZEqual(t, stSSZ, readStSSZ)
})
}
})
t.Run("using block", func(t *testing.T) {
for v := range version.All() {
t.Run(version.String(v), func(t *testing.T) {
db := setupDB(t)
featCfg := &features.Flags{}
featCfg.EnableStateDiff = true
reset := features.InitWithReset(featCfg)
defer reset()
setDefaultStateDiffExponents()
err := setOffsetInDB(db, 0)
require.NoError(t, err)
st, _ := createState(t, 0, v)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
blk := util.NewBeaconBlock()
blk.Block.Slot = 0
signedBlk, err := blocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
err = db.SaveBlock(context.Background(), signedBlk)
require.NoError(t, err)
r, err := signedBlk.Block().HashTreeRoot()
require.NoError(t, err)
readSt, err := db.State(context.Background(), r)
require.NoError(t, err)
require.NotNil(t, readSt)
stSSZ, err := st.MarshalSSZ()
require.NoError(t, err)
readStSSZ, err := readSt.MarshalSSZ()
require.NoError(t, err)
require.DeepSSZEqual(t, stSSZ, readStSSZ)
})
}
})
})
t.Run("Diffed state", func(t *testing.T) {
t.Run("using state summary", func(t *testing.T) {
for v := range version.All() {
t.Run(version.String(v), func(t *testing.T) {
db := setupDB(t)
featCfg := &features.Flags{}
featCfg.EnableStateDiff = true
reset := features.InitWithReset(featCfg)
defer reset()
setDefaultStateDiffExponents()
exponents := flags.Get().StateDiffExponents
err := setOffsetInDB(db, 0)
require.NoError(t, err)
st, _ := createState(t, 0, v)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
slot := primitives.Slot(math.PowerOf2(uint64(exponents[len(exponents)-2])))
st, _ = createState(t, slot, v)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
slot = primitives.Slot(math.PowerOf2(uint64(exponents[len(exponents)-2])) + math.PowerOf2(uint64(exponents[len(exponents)-1])))
st, _ = createState(t, slot, v)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
r := bytesutil.ToBytes32([]byte{'A'})
ss := &ethpb.StateSummary{Slot: slot, Root: r[:]}
err = db.SaveStateSummary(context.Background(), ss)
require.NoError(t, err)
readSt, err := db.State(context.Background(), r)
require.NoError(t, err)
require.NotNil(t, readSt)
stSSZ, err := st.MarshalSSZ()
require.NoError(t, err)
readStSSZ, err := readSt.MarshalSSZ()
require.NoError(t, err)
require.DeepSSZEqual(t, stSSZ, readStSSZ)
})
}
})
t.Run("using block", func(t *testing.T) {
for v := range version.All() {
t.Run(version.String(v), func(t *testing.T) {
db := setupDB(t)
featCfg := &features.Flags{}
featCfg.EnableStateDiff = true
reset := features.InitWithReset(featCfg)
defer reset()
setDefaultStateDiffExponents()
exponents := flags.Get().StateDiffExponents
err := setOffsetInDB(db, 0)
require.NoError(t, err)
st, _ := createState(t, 0, v)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
slot := primitives.Slot(math.PowerOf2(uint64(exponents[len(exponents)-2])))
st, _ = createState(t, slot, v)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
slot = primitives.Slot(math.PowerOf2(uint64(exponents[len(exponents)-2])) + math.PowerOf2(uint64(exponents[len(exponents)-1])))
st, _ = createState(t, slot, v)
err = db.saveStateByDiff(context.Background(), st)
require.NoError(t, err)
blk := util.NewBeaconBlock()
blk.Block.Slot = slot
signedBlk, err := blocks.NewSignedBeaconBlock(blk)
require.NoError(t, err)
err = db.SaveBlock(context.Background(), signedBlk)
require.NoError(t, err)
r, err := signedBlk.Block().HashTreeRoot()
require.NoError(t, err)
readSt, err := db.State(context.Background(), r)
require.NoError(t, err)
require.NotNil(t, readSt)
stSSZ, err := st.MarshalSSZ()
require.NoError(t, err)
readStSSZ, err := readSt.MarshalSSZ()
require.NoError(t, err)
require.DeepSSZEqual(t, stSSZ, readStSSZ)
})
}
})
})
}
func TestStore_HasStateUsingStateDiff(t *testing.T) {
t.Run("No state summary or block", func(t *testing.T) {
hook := logTest.NewGlobal()
db := setupDB(t)
featCfg := &features.Flags{}
featCfg.EnableStateDiff = true
reset := features.InitWithReset(featCfg)
defer reset()
setDefaultStateDiffExponents()
err := setOffsetInDB(db, 0)
require.NoError(t, err)
hasSt := db.HasState(t.Context(), [32]byte{'A'})
require.Equal(t, false, hasSt)
require.LogsContain(t, hook, "neither state summary nor block found")
})
t.Run("slot in tree or not", func(t *testing.T) {
db := setupDB(t)
featCfg := &features.Flags{}
featCfg.EnableStateDiff = true
reset := features.InitWithReset(featCfg)
defer reset()
setDefaultStateDiffExponents()
err := setOffsetInDB(db, 0)
require.NoError(t, err)
testCases := []struct {
slot primitives.Slot
expected bool
}{
{slot: 1, expected: false}, // slot 1 not in tree
{slot: 32, expected: true}, // slot 32 in tree
{slot: 0, expected: true}, // slot 0 in tree
{slot: primitives.Slot(math.PowerOf2(21)), expected: true}, // slot in tree
{slot: primitives.Slot(math.PowerOf2(21) - 1), expected: false}, // slot not in tree
{slot: primitives.Slot(math.PowerOf2(22)), expected: true}, // slot in tree
}
for _, tc := range testCases {
r := bytesutil.ToBytes32([]byte{'A'})
ss := &ethpb.StateSummary{Slot: tc.slot, Root: r[:]}
err = db.SaveStateSummary(t.Context(), ss)
require.NoError(t, err)
hasSt := db.HasState(t.Context(), r)
require.Equal(t, tc.expected, hasSt)
}
})
}

View File

@@ -2683,7 +2683,7 @@ func createBlobServerV2(t *testing.T, numBlobs int, blobMasks []bool) *httptest.
Blob: []byte("0xblob"),
KzgProofs: []hexutil.Bytes{},
}
for j := 0; j < int(params.BeaconConfig().NumberOfColumns); j++ {
for range fieldparams.NumberOfColumns {
cellProof := make([]byte, 48)
blobAndCellProofs[i].KzgProofs = append(blobAndCellProofs[i].KzgProofs, cellProof)
}

View File

@@ -240,7 +240,7 @@ func (f *ForkChoice) IsViableForCheckpoint(cp *forkchoicetypes.Checkpoint) (bool
if node.slot == epochStart {
return true, nil
}
if !features.Get().DisableLastEpochTargets {
if !features.Get().IgnoreUnviableAttestations {
// Allow any node from the checkpoint epoch - 1 to be viable.
nodeEpoch := slots.ToEpoch(node.slot)
if nodeEpoch+1 == cp.Epoch {
@@ -642,8 +642,12 @@ func (f *ForkChoice) DependentRootForEpoch(root [32]byte, epoch primitives.Epoch
if !ok || node == nil {
return [32]byte{}, ErrNilNode
}
if slots.ToEpoch(node.slot) >= epoch && node.parent != nil {
node = node.parent
if slots.ToEpoch(node.slot) >= epoch {
if node.parent != nil {
node = node.parent
} else {
return f.store.finalizedDependentRoot, nil
}
}
return node.root, nil
}

View File

@@ -212,6 +212,9 @@ func (s *Store) prune(ctx context.Context) error {
return nil
}
// Save the new finalized dependent root because it will be pruned
s.finalizedDependentRoot = finalizedNode.parent.root
// Prune nodeByRoot starting from root
if err := s.pruneFinalizedNodeByRootMap(ctx, s.treeRootNode, finalizedNode); err != nil {
return err

View File

@@ -465,6 +465,7 @@ func TestStore_TargetRootForEpoch(t *testing.T) {
ctx := t.Context()
f := setup(1, 1)
// Insert a block in slot 32
state, blk, err := prepareForkchoiceState(ctx, params.BeaconConfig().SlotsPerEpoch, [32]byte{'a'}, params.BeaconConfig().ZeroHash, params.BeaconConfig().ZeroHash, 1, 1)
require.NoError(t, err)
require.NoError(t, f.InsertNode(ctx, state, blk))
@@ -475,6 +476,7 @@ func TestStore_TargetRootForEpoch(t *testing.T) {
require.NoError(t, err)
require.Equal(t, dependent, [32]byte{})
// Insert a block in slot 33
state, blk1, err := prepareForkchoiceState(ctx, params.BeaconConfig().SlotsPerEpoch+1, [32]byte{'b'}, blk.Root(), params.BeaconConfig().ZeroHash, 1, 1)
require.NoError(t, err)
require.NoError(t, f.InsertNode(ctx, state, blk1))
@@ -488,7 +490,7 @@ func TestStore_TargetRootForEpoch(t *testing.T) {
require.NoError(t, err)
require.Equal(t, dependent, [32]byte{})
// Insert a block for the next epoch (missed slot 0)
// Insert a block for the next epoch (missed slot 0), slot 65
state, blk2, err := prepareForkchoiceState(ctx, 2*params.BeaconConfig().SlotsPerEpoch+1, [32]byte{'c'}, blk1.Root(), params.BeaconConfig().ZeroHash, 1, 1)
require.NoError(t, err)
@@ -509,6 +511,7 @@ func TestStore_TargetRootForEpoch(t *testing.T) {
require.NoError(t, err)
require.Equal(t, dependent, blk1.Root())
// Insert a block at slot 66
state, blk3, err := prepareForkchoiceState(ctx, 2*params.BeaconConfig().SlotsPerEpoch+2, [32]byte{'d'}, blk2.Root(), params.BeaconConfig().ZeroHash, 1, 1)
require.NoError(t, err)
require.NoError(t, f.InsertNode(ctx, state, blk3))
@@ -533,8 +536,11 @@ func TestStore_TargetRootForEpoch(t *testing.T) {
dependent, err = f.DependentRoot(1)
require.NoError(t, err)
require.Equal(t, [32]byte{}, dependent)
dependent, err = f.DependentRoot(2)
require.NoError(t, err)
require.Equal(t, blk1.Root(), dependent)
// Insert a block for next epoch (slot 0 present)
// Insert a block for the next epoch, slot 96 (descends from finalized at slot 33)
state, blk4, err := prepareForkchoiceState(ctx, 3*params.BeaconConfig().SlotsPerEpoch, [32]byte{'e'}, blk1.Root(), params.BeaconConfig().ZeroHash, 1, 1)
require.NoError(t, err)
require.NoError(t, f.InsertNode(ctx, state, blk4))
@@ -551,6 +557,7 @@ func TestStore_TargetRootForEpoch(t *testing.T) {
require.NoError(t, err)
require.Equal(t, dependent, blk1.Root())
// Insert a block at slot 97
state, blk5, err := prepareForkchoiceState(ctx, 3*params.BeaconConfig().SlotsPerEpoch+1, [32]byte{'f'}, blk4.Root(), params.BeaconConfig().ZeroHash, 1, 1)
require.NoError(t, err)
require.NoError(t, f.InsertNode(ctx, state, blk5))
@@ -600,12 +607,16 @@ func TestStore_TargetRootForEpoch(t *testing.T) {
require.NoError(t, err)
require.Equal(t, target, blk1.Root())
// Prune finalization
// Prune finalization, finalize the block at slot 96
s.finalizedCheckpoint.Root = blk4.Root()
require.NoError(t, s.prune(ctx))
target, err = f.TargetRootForEpoch(blk4.Root(), 3)
require.NoError(t, err)
require.Equal(t, blk4.Root(), target)
// Dependent root for the finalized block should be the root of the pruned block at slot 33
dependent, err = f.DependentRootForEpoch(blk4.Root(), 3)
require.NoError(t, err)
require.Equal(t, blk1.Root(), dependent)
}
func TestStore_DependentRootForEpoch(t *testing.T) {

View File

@@ -31,6 +31,7 @@ type Store struct {
proposerBoostRoot [fieldparams.RootLength]byte // latest block root that was boosted after being received in a timely manner.
previousProposerBoostRoot [fieldparams.RootLength]byte // previous block root that was boosted after being received in a timely manner.
previousProposerBoostScore uint64 // previous proposer boosted root score.
finalizedDependentRoot [fieldparams.RootLength]byte // dependent root at finalized checkpoint.
committeeWeight uint64 // tracks the total active validator balance divided by the number of slots per Epoch.
treeRootNode *Node // the root node of the store tree.
headNode *Node // last head Node

View File

@@ -0,0 +1,95 @@
# Graffiti Version Info Implementation
## Summary
Add automatic EL+CL version info to block graffiti following [ethereum/execution-apis#517](https://github.com/ethereum/execution-apis/pull/517). Uses the [flexible standard](https://hackmd.io/@wmoBhF17RAOH2NZ5bNXJVg/BJX2c9gja) to pack client info into leftover space after user graffiti.
More details: https://github.com/ethereum/execution-apis/blob/main/src/engine/identification.md
## Implementation
### Core Component: GraffitiInfo Struct
Thread-safe struct holding version information:
```go
const clCode = "PR"
type GraffitiInfo struct {
mu sync.RWMutex
userGraffiti string // From --graffiti flag (set once at startup)
clCommit string // From version.GetCommitPrefix() helper function
elCode string // From engine_getClientVersionV1
elCommit string // From engine_getClientVersionV1
}
```
### Flow
1. **Startup**: Parse flags, create GraffitiInfo with user graffiti and CL info.
2. **Wiring**: Pass struct to both execution service and RPC validator server
3. **Runtime**: Execution service goroutine periodically calls `engine_getClientVersionV1` and updates EL fields
4. **Block Proposal**: RPC validator server calls `GenerateGraffiti()` to get formatted graffiti
### Flexible Graffiti Format
Packs as much client info as space allows (after user graffiti):
| Available Space | Format | Example |
|----------------|--------|---------|
| ≥12 bytes | `EL(2)+commit(4)+CL(2)+commit(4)+user` | `GE168dPR63afBob` |
| 8-11 bytes | `EL(2)+commit(2)+CL(2)+commit(2)+user` | `GE16PR63my node here` |
| 4-7 bytes | `EL(2)+CL(2)+user` | `GEPRthis is my graffiti msg` |
| 2-3 bytes | `EL(2)+user` | `GEalmost full graffiti message` |
| <2 bytes | user only | `full 32 byte user graffiti here` |
```go
func (g *GraffitiInfo) GenerateGraffiti() [32]byte {
available := 32 - len(userGraffiti)
if elCode == "" {
elCommit2 = elCommit4 = ""
}
switch {
case available >= 12:
return elCode + elCommit4 + clCode + clCommit4 + userGraffiti
case available >= 8:
return elCode + elCommit2 + clCode + clCommit2 + userGraffiti
case available >= 4:
return elCode + clCode + userGraffiti
case available >= 2:
return elCode + userGraffiti
default:
return userGraffiti
}
}
```
### Update Logic
Single testable function in execution service:
```go
func (s *Service) updateGraffitiInfo() {
versions, err := s.GetClientVersion(ctx)
if err != nil {
return // Keep last good value
}
if len(versions) == 1 {
s.graffitiInfo.UpdateFromEngine(versions[0].Code, versions[0].Commit)
}
}
```
Goroutine calls this on `slot % 8 == 4` timing (4 times per epoch, avoids slot boundaries).
### Files Changes Required
**New:**
- `beacon-chain/execution/graffiti_info.go` - The struct and methods
- `beacon-chain/execution/graffiti_info_test.go` - Unit tests
- `runtime/version/version.go` - Add `GetCommitPrefix()` helper that extracts first 4 hex chars from the git commit injected via Bazel ldflags at build time
**Modified:**
- `beacon-chain/execution/service.go` - Add goroutine + updateGraffitiInfo()
- `beacon-chain/execution/engine_client.go` - Add GetClientVersion() method that does engine call
- `beacon-chain/rpc/.../validator/proposer.go` - Call GenerateGraffiti()
- `beacon-chain/node/node.go` - Wire GraffitiInfo to services
### Testing Strategy
- Unit test GraffitiInfo methods (priority logic, thread safety)
- Unit test updateGraffitiInfo() with mocked engine client

View File

@@ -33,6 +33,10 @@ func TestLightClient_NewLightClientOptimisticUpdateFromBeaconState(t *testing.T)
params.OverrideBeaconConfig(cfg)
for _, testVersion := range version.All()[1:] {
if testVersion == version.Gloas {
// TODO(16027): Unskip light client tests for Gloas
continue
}
t.Run(version.String(testVersion), func(t *testing.T) {
l := util.NewTestLightClient(t, testVersion)

View File

@@ -23,6 +23,7 @@ go_library(
"//beacon-chain/builder:go_default_library",
"//beacon-chain/cache:go_default_library",
"//beacon-chain/cache/depositsnapshot:go_default_library",
"//beacon-chain/das:go_default_library",
"//beacon-chain/db:go_default_library",
"//beacon-chain/db/filesystem:go_default_library",
"//beacon-chain/db/kv:go_default_library",

View File

@@ -26,6 +26,7 @@ import (
"github.com/OffchainLabs/prysm/v7/beacon-chain/builder"
"github.com/OffchainLabs/prysm/v7/beacon-chain/cache"
"github.com/OffchainLabs/prysm/v7/beacon-chain/cache/depositsnapshot"
"github.com/OffchainLabs/prysm/v7/beacon-chain/das"
"github.com/OffchainLabs/prysm/v7/beacon-chain/db"
"github.com/OffchainLabs/prysm/v7/beacon-chain/db/filesystem"
"github.com/OffchainLabs/prysm/v7/beacon-chain/db/kv"
@@ -116,7 +117,7 @@ type BeaconNode struct {
GenesisProviders []genesis.Provider
CheckpointInitializer checkpoint.Initializer
forkChoicer forkchoice.ForkChoicer
clockWaiter startup.ClockWaiter
ClockWaiter startup.ClockWaiter
BackfillOpts []backfill.ServiceOption
initialSyncComplete chan struct{}
BlobStorage *filesystem.BlobStorage
@@ -129,6 +130,7 @@ type BeaconNode struct {
slasherEnabled bool
lcStore *lightclient.Store
ConfigOptions []params.Option
SyncNeedsWaiter func() (das.SyncNeeds, error)
}
// New creates a new node instance, sets up configuration options, and registers
@@ -193,7 +195,7 @@ func New(cliCtx *cli.Context, cancel context.CancelFunc, opts ...Option) (*Beaco
params.LogDigests(params.BeaconConfig())
synchronizer := startup.NewClockSynchronizer()
beacon.clockWaiter = synchronizer
beacon.ClockWaiter = synchronizer
beacon.forkChoicer = doublylinkedtree.New()
depositAddress, err := execution.DepositContractAddress()
@@ -233,12 +235,13 @@ func New(cliCtx *cli.Context, cancel context.CancelFunc, opts ...Option) (*Beaco
beacon.lhsp = &verification.LazyHeadStateProvider{}
beacon.verifyInitWaiter = verification.NewInitializerWaiter(
beacon.clockWaiter, forkchoice.NewROForkChoice(beacon.forkChoicer), beacon.stateGen, beacon.lhsp)
beacon.ClockWaiter, forkchoice.NewROForkChoice(beacon.forkChoicer), beacon.stateGen, beacon.lhsp)
beacon.BackfillOpts = append(
beacon.BackfillOpts,
backfill.WithVerifierWaiter(beacon.verifyInitWaiter),
backfill.WithInitSyncWaiter(initSyncWaiter(ctx, beacon.initialSyncComplete)),
backfill.WithSyncNeedsWaiter(beacon.SyncNeedsWaiter),
)
if err := registerServices(cliCtx, beacon, synchronizer, bfs); err != nil {
@@ -280,7 +283,10 @@ func configureBeacon(cliCtx *cli.Context) error {
return errors.Wrap(err, "could not configure beacon chain")
}
flags.ConfigureGlobalFlags(cliCtx)
err := flags.ConfigureGlobalFlags(cliCtx)
if err != nil {
return errors.Wrap(err, "could not configure global flags")
}
if err := configureChainConfig(cliCtx); err != nil {
return errors.Wrap(err, "could not configure chain config")
@@ -660,7 +666,8 @@ func (b *BeaconNode) registerP2P(cliCtx *cli.Context) error {
EnableUPnP: cliCtx.Bool(cmd.EnableUPnPFlag.Name),
StateNotifier: b,
DB: b.db,
ClockWaiter: b.clockWaiter,
StateGen: b.stateGen,
ClockWaiter: b.ClockWaiter,
})
if err != nil {
return err
@@ -702,7 +709,7 @@ func (b *BeaconNode) registerSlashingPoolService() error {
return err
}
s := slashings.NewPoolService(b.ctx, b.slashingsPool, slashings.WithElectraTimer(b.clockWaiter, chainService.CurrentSlot))
s := slashings.NewPoolService(b.ctx, b.slashingsPool, slashings.WithElectraTimer(b.ClockWaiter, chainService.CurrentSlot))
return b.services.RegisterService(s)
}
@@ -824,7 +831,7 @@ func (b *BeaconNode) registerSyncService(initialSyncComplete chan struct{}, bFil
regularsync.WithSlasherAttestationsFeed(b.slasherAttestationsFeed),
regularsync.WithSlasherBlockHeadersFeed(b.slasherBlockHeadersFeed),
regularsync.WithReconstructor(web3Service),
regularsync.WithClockWaiter(b.clockWaiter),
regularsync.WithClockWaiter(b.ClockWaiter),
regularsync.WithInitialSyncComplete(initialSyncComplete),
regularsync.WithStateNotifier(b),
regularsync.WithBlobStorage(b.BlobStorage),
@@ -855,7 +862,8 @@ func (b *BeaconNode) registerInitialSyncService(complete chan struct{}) error {
P2P: b.fetchP2P(),
StateNotifier: b,
BlockNotifier: b,
ClockWaiter: b.clockWaiter,
ClockWaiter: b.ClockWaiter,
SyncNeedsWaiter: b.SyncNeedsWaiter,
InitialSyncComplete: complete,
BlobStorage: b.BlobStorage,
DataColumnStorage: b.DataColumnStorage,
@@ -886,7 +894,7 @@ func (b *BeaconNode) registerSlasherService() error {
SlashingPoolInserter: b.slashingsPool,
SyncChecker: syncService,
HeadStateFetcher: chainService,
ClockWaiter: b.clockWaiter,
ClockWaiter: b.ClockWaiter,
})
if err != nil {
return err
@@ -979,7 +987,7 @@ func (b *BeaconNode) registerRPCService(router *http.ServeMux) error {
MaxMsgSize: maxMsgSize,
BlockBuilder: b.fetchBuilderService(),
Router: router,
ClockWaiter: b.clockWaiter,
ClockWaiter: b.ClockWaiter,
BlobStorage: b.BlobStorage,
DataColumnStorage: b.DataColumnStorage,
TrackedValidatorsCache: b.trackedValidatorsCache,
@@ -1124,7 +1132,7 @@ func (b *BeaconNode) registerPrunerService(cliCtx *cli.Context) error {
func (b *BeaconNode) RegisterBackfillService(cliCtx *cli.Context, bfs *backfill.Store) error {
pa := peers.NewAssigner(b.fetchP2P().Peers(), b.forkChoicer)
bf, err := backfill.NewService(cliCtx.Context, bfs, b.BlobStorage, b.clockWaiter, b.fetchP2P(), pa, b.BackfillOpts...)
bf, err := backfill.NewService(cliCtx.Context, bfs, b.BlobStorage, b.DataColumnStorage, b.ClockWaiter, b.fetchP2P(), pa, b.BackfillOpts...)
if err != nil {
return errors.Wrap(err, "error initializing backfill service")
}

View File

@@ -56,6 +56,7 @@ go_library(
"//beacon-chain/p2p/peers/scorers:go_default_library",
"//beacon-chain/p2p/types:go_default_library",
"//beacon-chain/startup:go_default_library",
"//beacon-chain/state/stategen:go_default_library",
"//cmd/beacon-chain/flags:go_default_library",
"//config/features:go_default_library",
"//config/fieldparams:go_default_library",
@@ -153,6 +154,7 @@ go_test(
"//beacon-chain/core/helpers:go_default_library",
"//beacon-chain/core/peerdas:go_default_library",
"//beacon-chain/core/signing:go_default_library",
"//beacon-chain/db/iface:go_default_library",
"//beacon-chain/db/testing:go_default_library",
"//beacon-chain/p2p/encoder:go_default_library",
"//beacon-chain/p2p/peers:go_default_library",
@@ -161,6 +163,7 @@ go_test(
"//beacon-chain/p2p/testing:go_default_library",
"//beacon-chain/p2p/types:go_default_library",
"//beacon-chain/startup:go_default_library",
"//beacon-chain/state/stategen/mock:go_default_library",
"//cmd/beacon-chain/flags:go_default_library",
"//config/fieldparams:go_default_library",
"//config/params:go_default_library",

View File

@@ -7,6 +7,7 @@ import (
statefeed "github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed/state"
"github.com/OffchainLabs/prysm/v7/beacon-chain/db"
"github.com/OffchainLabs/prysm/v7/beacon-chain/startup"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state/stategen"
"github.com/sirupsen/logrus"
)
@@ -49,6 +50,7 @@ type Config struct {
IPColocationWhitelist []*net.IPNet
StateNotifier statefeed.Notifier
DB db.ReadOnlyDatabaseWithSeqNum
StateGen stategen.StateManager
ClockWaiter startup.ClockWaiter
}

View File

@@ -156,29 +156,13 @@ func (s *Service) retrieveActiveValidators() (uint64, error) {
if s.activeValidatorCount != 0 {
return s.activeValidatorCount, nil
}
rt := s.cfg.DB.LastArchivedRoot(s.ctx)
if rt == params.BeaconConfig().ZeroHash {
genState, err := s.cfg.DB.GenesisState(s.ctx)
if err != nil {
return 0, err
}
if genState == nil || genState.IsNil() {
return 0, errors.New("no genesis state exists")
}
activeVals, err := helpers.ActiveValidatorCount(context.Background(), genState, coreTime.CurrentEpoch(genState))
if err != nil {
return 0, err
}
// Cache active validator count
s.activeValidatorCount = activeVals
return activeVals, nil
}
bState, err := s.cfg.DB.State(s.ctx, rt)
finalizedCheckpoint, err := s.cfg.DB.FinalizedCheckpoint(s.ctx)
if err != nil {
return 0, err
}
if bState == nil || bState.IsNil() {
return 0, errors.Errorf("no state with root %#x exists", rt)
bState, err := s.cfg.StateGen.StateByRoot(s.ctx, [32]byte(finalizedCheckpoint.Root))
if err != nil {
return 0, err
}
activeVals, err := helpers.ActiveValidatorCount(context.Background(), bState, coreTime.CurrentEpoch(bState))
if err != nil {

View File

@@ -1,10 +1,14 @@
package p2p
import (
"context"
"testing"
iface "github.com/OffchainLabs/prysm/v7/beacon-chain/db/iface"
dbutil "github.com/OffchainLabs/prysm/v7/beacon-chain/db/testing"
mockstategen "github.com/OffchainLabs/prysm/v7/beacon-chain/state/stategen/mock"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/testing/assert"
"github.com/OffchainLabs/prysm/v7/testing/require"
@@ -20,9 +24,11 @@ func TestCorrect_ActiveValidatorsCount(t *testing.T) {
params.OverrideBeaconConfig(cfg)
db := dbutil.SetupDB(t)
wrappedDB := &finalizedCheckpointDB{ReadOnlyDatabaseWithSeqNum: db}
stateGen := mockstategen.NewService()
s := &Service{
ctx: t.Context(),
cfg: &Config{DB: db},
cfg: &Config{DB: wrappedDB, StateGen: stateGen},
}
bState, err := util.NewBeaconState(func(state *ethpb.BeaconState) error {
validators := make([]*ethpb.Validator, params.BeaconConfig().MinGenesisActiveValidatorCount)
@@ -39,6 +45,10 @@ func TestCorrect_ActiveValidatorsCount(t *testing.T) {
})
require.NoError(t, err)
require.NoError(t, db.SaveGenesisData(s.ctx, bState))
checkpoint, err := db.FinalizedCheckpoint(s.ctx)
require.NoError(t, err)
wrappedDB.finalized = checkpoint
stateGen.AddStateForRoot(bState, bytesutil.ToBytes32(checkpoint.Root))
vals, err := s.retrieveActiveValidators()
assert.NoError(t, err, "genesis state not retrieved")
@@ -52,7 +62,10 @@ func TestCorrect_ActiveValidatorsCount(t *testing.T) {
}))
}
require.NoError(t, bState.SetSlot(10000))
require.NoError(t, db.SaveState(s.ctx, bState, [32]byte{'a'}))
rootA := [32]byte{'a'}
require.NoError(t, db.SaveState(s.ctx, bState, rootA))
wrappedDB.finalized = &ethpb.Checkpoint{Root: rootA[:]}
stateGen.AddStateForRoot(bState, rootA)
// Reset count
s.activeValidatorCount = 0
@@ -77,3 +90,15 @@ func TestLoggingParameters(_ *testing.T) {
logGossipParameters("testing", defaultLightClientOptimisticUpdateTopicParams())
logGossipParameters("testing", defaultLightClientFinalityUpdateTopicParams())
}
type finalizedCheckpointDB struct {
iface.ReadOnlyDatabaseWithSeqNum
finalized *ethpb.Checkpoint
}
func (f *finalizedCheckpointDB) FinalizedCheckpoint(ctx context.Context) (*ethpb.Checkpoint, error) {
if f.finalized != nil {
return f.finalized, nil
}
return f.ReadOnlyDatabaseWithSeqNum.FinalizedCheckpoint(ctx)
}

View File

@@ -47,6 +47,7 @@ go_test(
],
embed = [":go_default_library"],
deps = [
"//beacon-chain/forkchoice/types:go_default_library",
"//beacon-chain/p2p/peers/peerdata:go_default_library",
"//beacon-chain/p2p/peers/scorers:go_default_library",
"//cmd/beacon-chain/flags:go_default_library",

View File

@@ -4,11 +4,18 @@ import (
forkchoicetypes "github.com/OffchainLabs/prysm/v7/beacon-chain/forkchoice/types"
"github.com/OffchainLabs/prysm/v7/cmd/beacon-chain/flags"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// StatusProvider describes the minimum capability that Assigner needs from peer status tracking.
// That is, the ability to retrieve the best peers by finalized checkpoint.
type StatusProvider interface {
BestFinalized(ourFinalized primitives.Epoch) (primitives.Epoch, []peer.ID)
}
// FinalizedCheckpointer describes the minimum capability that Assigner needs from forkchoice.
// That is, the ability to retrieve the latest finalized checkpoint to help with peer evaluation.
type FinalizedCheckpointer interface {
@@ -17,9 +24,9 @@ type FinalizedCheckpointer interface {
// NewAssigner assists in the correct construction of an Assigner by code in other packages,
// assuring all the important private member fields are given values.
// The FinalizedCheckpointer is used to retrieve the latest finalized checkpoint each time peers are requested.
// The StatusProvider is used to retrieve best peers, and FinalizedCheckpointer is used to retrieve the latest finalized checkpoint each time peers are requested.
// Peers that report an older finalized checkpoint are filtered out.
func NewAssigner(s *Status, fc FinalizedCheckpointer) *Assigner {
func NewAssigner(s StatusProvider, fc FinalizedCheckpointer) *Assigner {
return &Assigner{
ps: s,
fc: fc,
@@ -28,7 +35,7 @@ func NewAssigner(s *Status, fc FinalizedCheckpointer) *Assigner {
// Assigner uses the "BestFinalized" peer scoring method to pick the next-best peer to receive rpc requests.
type Assigner struct {
ps *Status
ps StatusProvider
fc FinalizedCheckpointer
}
@@ -38,38 +45,42 @@ type Assigner struct {
var ErrInsufficientSuitable = errors.New("no suitable peers")
func (a *Assigner) freshPeers() ([]peer.ID, error) {
required := min(flags.Get().MinimumSyncPeers, params.BeaconConfig().MaxPeersToSync)
_, peers := a.ps.BestFinalized(params.BeaconConfig().MaxPeersToSync, a.fc.FinalizedCheckpoint().Epoch)
required := min(flags.Get().MinimumSyncPeers, min(flags.Get().MinimumSyncPeers, params.BeaconConfig().MaxPeersToSync))
_, peers := a.ps.BestFinalized(a.fc.FinalizedCheckpoint().Epoch)
if len(peers) < required {
log.WithFields(logrus.Fields{
"suitable": len(peers),
"required": required}).Warn("Unable to assign peer while suitable peers < required ")
"required": required}).Trace("Unable to assign peer while suitable peers < required")
return nil, ErrInsufficientSuitable
}
return peers, nil
}
// AssignmentFilter describes a function that takes a list of peer.IDs and returns a filtered subset.
// An example is the NotBusy filter.
type AssignmentFilter func([]peer.ID) []peer.ID
// Assign uses the "BestFinalized" method to select the best peers that agree on a canonical block
// for the configured finalized epoch. At most `n` peers will be returned. The `busy` param can be used
// to filter out peers that we know we don't want to connect to, for instance if we are trying to limit
// the number of outbound requests to each peer from a given component.
func (a *Assigner) Assign(busy map[peer.ID]bool, n int) ([]peer.ID, error) {
func (a *Assigner) Assign(filter AssignmentFilter) ([]peer.ID, error) {
best, err := a.freshPeers()
if err != nil {
return nil, err
}
return pickBest(busy, n, best), nil
return filter(best), nil
}
func pickBest(busy map[peer.ID]bool, n int, best []peer.ID) []peer.ID {
ps := make([]peer.ID, 0, n)
for _, p := range best {
if len(ps) == n {
return ps
}
if !busy[p] {
ps = append(ps, p)
// NotBusy is a filter that returns the list of peer.IDs that are not in the `busy` map.
func NotBusy(busy map[peer.ID]bool) AssignmentFilter {
return func(peers []peer.ID) []peer.ID {
ps := make([]peer.ID, 0, len(peers))
for _, p := range peers {
if !busy[p] {
ps = append(ps, p)
}
}
return ps
}
return ps
}

View File

@@ -5,6 +5,8 @@ import (
"slices"
"testing"
forkchoicetypes "github.com/OffchainLabs/prysm/v7/beacon-chain/forkchoice/types"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/testing/require"
"github.com/libp2p/go-libp2p/core/peer"
)
@@ -14,82 +16,68 @@ func TestPickBest(t *testing.T) {
cases := []struct {
name string
busy map[peer.ID]bool
n int
best []peer.ID
expected []peer.ID
}{
{
name: "",
n: 0,
name: "don't limit",
expected: best,
},
{
name: "none busy",
n: 1,
expected: best[0:1],
expected: best,
},
{
name: "all busy except last",
n: 1,
busy: testBusyMap(best[0 : len(best)-1]),
expected: best[len(best)-1:],
},
{
name: "all busy except i=5",
n: 1,
busy: testBusyMap(slices.Concat(best[0:5], best[6:])),
expected: []peer.ID{best[5]},
},
{
name: "all busy - 0 results",
n: 1,
busy: testBusyMap(best),
},
{
name: "first half busy",
n: 5,
busy: testBusyMap(best[0:5]),
expected: best[5:],
},
{
name: "back half busy",
n: 5,
busy: testBusyMap(best[5:]),
expected: best[0:5],
},
{
name: "pick all ",
n: 10,
expected: best,
},
{
name: "none available",
n: 10,
best: []peer.ID{},
},
{
name: "not enough",
n: 10,
best: best[0:1],
expected: best[0:1],
},
{
name: "not enough, some busy",
n: 10,
best: best[0:6],
busy: testBusyMap(best[0:5]),
expected: best[5:6],
},
}
for _, c := range cases {
name := fmt.Sprintf("n=%d", c.n)
if c.name != "" {
name += " " + c.name
}
t.Run(name, func(t *testing.T) {
t.Run(c.name, func(t *testing.T) {
if c.best == nil {
c.best = best
}
pb := pickBest(c.busy, c.n, c.best)
filt := NotBusy(c.busy)
pb := filt(c.best)
require.Equal(t, len(c.expected), len(pb))
for i := range c.expected {
require.Equal(t, c.expected[i], pb[i])
@@ -113,3 +101,310 @@ func testPeerIds(n int) []peer.ID {
}
return pids
}
// MockStatus is a test mock for the Status interface used in Assigner.
type MockStatus struct {
bestFinalizedEpoch primitives.Epoch
bestPeers []peer.ID
}
func (m *MockStatus) BestFinalized(ourFinalized primitives.Epoch) (primitives.Epoch, []peer.ID) {
return m.bestFinalizedEpoch, m.bestPeers
}
// MockFinalizedCheckpointer is a test mock for FinalizedCheckpointer interface.
type MockFinalizedCheckpointer struct {
checkpoint *forkchoicetypes.Checkpoint
}
func (m *MockFinalizedCheckpointer) FinalizedCheckpoint() *forkchoicetypes.Checkpoint {
return m.checkpoint
}
// TestAssign_HappyPath tests the Assign method with sufficient peers and various filters.
func TestAssign_HappyPath(t *testing.T) {
peers := testPeerIds(10)
cases := []struct {
name string
bestPeers []peer.ID
finalizedEpoch primitives.Epoch
filter AssignmentFilter
expectedCount int
}{
{
name: "sufficient peers with identity filter",
bestPeers: peers,
finalizedEpoch: 10,
filter: func(p []peer.ID) []peer.ID { return p },
expectedCount: 10,
},
{
name: "sufficient peers with NotBusy filter (no busy)",
bestPeers: peers,
finalizedEpoch: 10,
filter: NotBusy(make(map[peer.ID]bool)),
expectedCount: 10,
},
{
name: "sufficient peers with NotBusy filter (some busy)",
bestPeers: peers,
finalizedEpoch: 10,
filter: NotBusy(testBusyMap(peers[0:5])),
expectedCount: 5,
},
{
name: "minimum threshold exactly met",
bestPeers: peers[0:5],
finalizedEpoch: 10,
filter: func(p []peer.ID) []peer.ID { return p },
expectedCount: 5,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
mockStatus := &MockStatus{
bestFinalizedEpoch: tc.finalizedEpoch,
bestPeers: tc.bestPeers,
}
mockCheckpointer := &MockFinalizedCheckpointer{
checkpoint: &forkchoicetypes.Checkpoint{Epoch: tc.finalizedEpoch},
}
assigner := NewAssigner(mockStatus, mockCheckpointer)
result, err := assigner.Assign(tc.filter)
require.NoError(t, err)
require.Equal(t, tc.expectedCount, len(result),
fmt.Sprintf("expected %d peers, got %d", tc.expectedCount, len(result)))
})
}
}
// TestAssign_InsufficientPeers tests error handling when not enough suitable peers are available.
// Note: The actual peer threshold depends on config values MaxPeersToSync and MinimumSyncPeers.
func TestAssign_InsufficientPeers(t *testing.T) {
cases := []struct {
name string
bestPeers []peer.ID
expectedErr error
description string
}{
{
name: "exactly at minimum threshold",
bestPeers: testPeerIds(5),
expectedErr: nil,
description: "5 peers should meet the minimum threshold",
},
{
name: "well above minimum threshold",
bestPeers: testPeerIds(50),
expectedErr: nil,
description: "50 peers should easily meet requirements",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
mockStatus := &MockStatus{
bestFinalizedEpoch: 10,
bestPeers: tc.bestPeers,
}
mockCheckpointer := &MockFinalizedCheckpointer{
checkpoint: &forkchoicetypes.Checkpoint{Epoch: 10},
}
assigner := NewAssigner(mockStatus, mockCheckpointer)
result, err := assigner.Assign(NotBusy(make(map[peer.ID]bool)))
if tc.expectedErr != nil {
require.NotNil(t, err, tc.description)
require.Equal(t, tc.expectedErr, err)
} else {
require.NoError(t, err, tc.description)
require.Equal(t, len(tc.bestPeers), len(result))
}
})
}
}
// TestAssign_FilterApplication verifies that filters are correctly applied to peer lists.
func TestAssign_FilterApplication(t *testing.T) {
peers := testPeerIds(10)
cases := []struct {
name string
bestPeers []peer.ID
filterToApply AssignmentFilter
expectedCount int
description string
}{
{
name: "identity filter returns all peers",
bestPeers: peers,
filterToApply: func(p []peer.ID) []peer.ID { return p },
expectedCount: 10,
description: "identity filter should not change peer list",
},
{
name: "filter removes all peers (all busy)",
bestPeers: peers,
filterToApply: NotBusy(testBusyMap(peers)),
expectedCount: 0,
description: "all peers busy should return empty list",
},
{
name: "filter removes first 5 peers",
bestPeers: peers,
filterToApply: NotBusy(testBusyMap(peers[0:5])),
expectedCount: 5,
description: "should only return non-busy peers",
},
{
name: "filter removes last 5 peers",
bestPeers: peers,
filterToApply: NotBusy(testBusyMap(peers[5:])),
expectedCount: 5,
description: "should only return non-busy peers from beginning",
},
{
name: "custom filter selects every other peer",
bestPeers: peers,
filterToApply: func(p []peer.ID) []peer.ID {
result := make([]peer.ID, 0)
for i := 0; i < len(p); i += 2 {
result = append(result, p[i])
}
return result
},
expectedCount: 5,
description: "custom filter selecting every other peer",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
mockStatus := &MockStatus{
bestFinalizedEpoch: 10,
bestPeers: tc.bestPeers,
}
mockCheckpointer := &MockFinalizedCheckpointer{
checkpoint: &forkchoicetypes.Checkpoint{Epoch: 10},
}
assigner := NewAssigner(mockStatus, mockCheckpointer)
result, err := assigner.Assign(tc.filterToApply)
require.NoError(t, err, fmt.Sprintf("unexpected error: %v", err))
require.Equal(t, tc.expectedCount, len(result),
fmt.Sprintf("%s: expected %d peers, got %d", tc.description, tc.expectedCount, len(result)))
})
}
}
// TestAssign_FinalizedCheckpointUsage verifies that the finalized checkpoint is correctly used.
func TestAssign_FinalizedCheckpointUsage(t *testing.T) {
peers := testPeerIds(10)
cases := []struct {
name string
finalizedEpoch primitives.Epoch
bestPeers []peer.ID
expectedCount int
description string
}{
{
name: "epoch 0",
finalizedEpoch: 0,
bestPeers: peers,
expectedCount: 10,
description: "epoch 0 should work",
},
{
name: "epoch 100",
finalizedEpoch: 100,
bestPeers: peers,
expectedCount: 10,
description: "high epoch number should work",
},
{
name: "epoch changes between calls",
finalizedEpoch: 50,
bestPeers: testPeerIds(5),
expectedCount: 5,
description: "epoch value should be used in checkpoint",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
mockStatus := &MockStatus{
bestFinalizedEpoch: tc.finalizedEpoch,
bestPeers: tc.bestPeers,
}
mockCheckpointer := &MockFinalizedCheckpointer{
checkpoint: &forkchoicetypes.Checkpoint{Epoch: tc.finalizedEpoch},
}
assigner := NewAssigner(mockStatus, mockCheckpointer)
result, err := assigner.Assign(NotBusy(make(map[peer.ID]bool)))
require.NoError(t, err)
require.Equal(t, tc.expectedCount, len(result),
fmt.Sprintf("%s: expected %d peers, got %d", tc.description, tc.expectedCount, len(result)))
})
}
}
// TestAssign_EdgeCases tests boundary conditions and edge cases.
func TestAssign_EdgeCases(t *testing.T) {
cases := []struct {
name string
bestPeers []peer.ID
filter AssignmentFilter
expectedCount int
description string
}{
{
name: "filter returns empty from sufficient peers",
bestPeers: testPeerIds(10),
filter: func(p []peer.ID) []peer.ID { return []peer.ID{} },
expectedCount: 0,
description: "filter can return empty list even if sufficient peers available",
},
{
name: "filter selects subset from sufficient peers",
bestPeers: testPeerIds(10),
filter: func(p []peer.ID) []peer.ID { return p[0:2] },
expectedCount: 2,
description: "filter can return subset of available peers",
},
{
name: "filter selects single peer from many",
bestPeers: testPeerIds(20),
filter: func(p []peer.ID) []peer.ID { return p[0:1] },
expectedCount: 1,
description: "filter can select single peer from many available",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
mockStatus := &MockStatus{
bestFinalizedEpoch: 10,
bestPeers: tc.bestPeers,
}
mockCheckpointer := &MockFinalizedCheckpointer{
checkpoint: &forkchoicetypes.Checkpoint{Epoch: 10},
}
assigner := NewAssigner(mockStatus, mockCheckpointer)
result, err := assigner.Assign(tc.filter)
require.NoError(t, err, fmt.Sprintf("%s: unexpected error: %v", tc.description, err))
require.Equal(t, tc.expectedCount, len(result),
fmt.Sprintf("%s: expected %d peers, got %d", tc.description, tc.expectedCount, len(result)))
})
}
}

View File

@@ -704,76 +704,54 @@ func (p *Status) deprecatedPrune() {
p.tallyIPTracker()
}
// BestFinalized returns the highest finalized epoch equal to or higher than `ourFinalizedEpoch`
// that is agreed upon by the majority of peers, and the peers agreeing on this finalized epoch.
// This method may not return the absolute highest finalized epoch, but the finalized epoch in which
// most peers can serve blocks (plurality voting). Ideally, all peers would be reporting the same
// finalized epoch but some may be behind due to their own latency, or because of their finalized
// epoch at the time we queried them.
func (p *Status) BestFinalized(maxPeers int, ourFinalizedEpoch primitives.Epoch) (primitives.Epoch, []peer.ID) {
// Retrieve all connected peers.
// BestFinalized groups all peers by their last known finalized epoch
// and selects the epoch of the largest group as best.
// Any peer with a finalized epoch < ourFinalized is excluded from consideration.
// In the event of a tie in largest group size, the higher epoch is the tie breaker.
// The selected epoch is returned, along with a list of peers with a finalized epoch >= the selected epoch.
func (p *Status) BestFinalized(ourFinalized primitives.Epoch) (primitives.Epoch, []peer.ID) {
connected := p.Connected()
pids := make([]peer.ID, 0, len(connected))
views := make(map[peer.ID]*pb.StatusV2, len(connected))
// key: finalized epoch, value: number of peers that support this finalized epoch.
finalizedEpochVotes := make(map[primitives.Epoch]uint64)
// key: peer ID, value: finalized epoch of the peer.
pidEpoch := make(map[peer.ID]primitives.Epoch, len(connected))
// key: peer ID, value: head slot of the peer.
pidHead := make(map[peer.ID]primitives.Slot, len(connected))
potentialPIDs := make([]peer.ID, 0, len(connected))
votes := make(map[primitives.Epoch]uint64)
winner := primitives.Epoch(0)
for _, pid := range connected {
peerChainState, err := p.ChainState(pid)
// Skip if the peer's finalized epoch is not defined, or if the peer's finalized epoch is
// lower than ours.
if err != nil || peerChainState == nil || peerChainState.FinalizedEpoch < ourFinalizedEpoch {
view, err := p.ChainState(pid)
if err != nil || view == nil || view.FinalizedEpoch < ourFinalized {
continue
}
pids = append(pids, pid)
views[pid] = view
finalizedEpochVotes[peerChainState.FinalizedEpoch]++
pidEpoch[pid] = peerChainState.FinalizedEpoch
pidHead[pid] = peerChainState.HeadSlot
potentialPIDs = append(potentialPIDs, pid)
}
// Select the target epoch, which is the epoch most peers agree upon.
// If there is a tie, select the highest epoch.
targetEpoch, mostVotes := primitives.Epoch(0), uint64(0)
for epoch, count := range finalizedEpochVotes {
if count > mostVotes || (count == mostVotes && epoch > targetEpoch) {
mostVotes = count
targetEpoch = epoch
votes[view.FinalizedEpoch]++
if winner == 0 {
winner = view.FinalizedEpoch
continue
}
e, v := view.FinalizedEpoch, votes[view.FinalizedEpoch]
if v > votes[winner] || v == votes[winner] && e > winner {
winner = e
}
}
// Sort PIDs by finalized (epoch, head), in decreasing order.
sort.Slice(potentialPIDs, func(i, j int) bool {
if pidEpoch[potentialPIDs[i]] == pidEpoch[potentialPIDs[j]] {
return pidHead[potentialPIDs[i]] > pidHead[potentialPIDs[j]]
// Descending sort by (finalized, head).
sort.Slice(pids, func(i, j int) bool {
iv, jv := views[pids[i]], views[pids[j]]
if iv.FinalizedEpoch == jv.FinalizedEpoch {
return iv.HeadSlot > jv.HeadSlot
}
return pidEpoch[potentialPIDs[i]] > pidEpoch[potentialPIDs[j]]
return iv.FinalizedEpoch > jv.FinalizedEpoch
})
// Trim potential peers to those on or after target epoch.
for i, pid := range potentialPIDs {
if pidEpoch[pid] < targetEpoch {
potentialPIDs = potentialPIDs[:i]
break
}
}
// Find the first peer with finalized epoch < winner, trim and all following (lower) peers.
trim := sort.Search(len(pids), func(i int) bool {
return views[pids[i]].FinalizedEpoch < winner
})
pids = pids[:trim]
// Trim potential peers to at most maxPeers.
if len(potentialPIDs) > maxPeers {
potentialPIDs = potentialPIDs[:maxPeers]
}
return targetEpoch, potentialPIDs
return winner, pids
}
// BestNonFinalized returns the highest known epoch, higher than ours,

View File

@@ -654,9 +654,10 @@ func TestTrimmedOrderedPeers(t *testing.T) {
FinalizedRoot: mockroot2[:],
})
target, pids := p.BestFinalized(maxPeers, 0)
target, pids := p.BestFinalized(0)
assert.Equal(t, expectedTarget, target, "Incorrect target epoch retrieved")
assert.Equal(t, maxPeers, len(pids), "Incorrect number of peers retrieved")
// addPeer called 5 times above
assert.Equal(t, 5, len(pids), "Incorrect number of peers retrieved")
// Expect the returned list to be ordered by finalized epoch and trimmed to max peers.
assert.Equal(t, pid3, pids[0], "Incorrect first peer")
@@ -1017,7 +1018,10 @@ func TestStatus_BestPeer(t *testing.T) {
HeadSlot: peerConfig.headSlot,
})
}
epoch, pids := p.BestFinalized(tt.limitPeers, tt.ourFinalizedEpoch)
epoch, pids := p.BestFinalized(tt.ourFinalizedEpoch)
if len(pids) > tt.limitPeers {
pids = pids[:tt.limitPeers]
}
assert.Equal(t, tt.targetEpoch, epoch, "Unexpected epoch retrieved")
assert.Equal(t, tt.targetEpochSupport, len(pids), "Unexpected number of peers supporting retrieved epoch")
})
@@ -1044,7 +1048,10 @@ func TestBestFinalized_returnsMaxValue(t *testing.T) {
})
}
_, pids := p.BestFinalized(maxPeers, 0)
_, pids := p.BestFinalized(0)
if len(pids) > maxPeers {
pids = pids[:maxPeers]
}
assert.Equal(t, maxPeers, len(pids), "Wrong number of peers returned")
}

View File

@@ -77,12 +77,12 @@ func InitializeDataMaps() {
},
bytesutil.ToBytes4(params.BeaconConfig().ElectraForkVersion): func() (interfaces.ReadOnlySignedBeaconBlock, error) {
return blocks.NewSignedBeaconBlock(
&ethpb.SignedBeaconBlockElectra{Block: &ethpb.BeaconBlockElectra{Body: &ethpb.BeaconBlockBodyElectra{ExecutionPayload: &enginev1.ExecutionPayloadDeneb{}}}},
&ethpb.SignedBeaconBlockElectra{Block: &ethpb.BeaconBlockElectra{Body: &ethpb.BeaconBlockBodyElectra{ExecutionPayload: &enginev1.ExecutionPayloadDeneb{}, ExecutionRequests: &enginev1.ExecutionRequests{}}}},
)
},
bytesutil.ToBytes4(params.BeaconConfig().FuluForkVersion): func() (interfaces.ReadOnlySignedBeaconBlock, error) {
return blocks.NewSignedBeaconBlock(
&ethpb.SignedBeaconBlockFulu{Block: &ethpb.BeaconBlockElectra{Body: &ethpb.BeaconBlockBodyElectra{ExecutionPayload: &enginev1.ExecutionPayloadDeneb{}}}},
&ethpb.SignedBeaconBlockFulu{Block: &ethpb.BeaconBlockElectra{Body: &ethpb.BeaconBlockBodyElectra{ExecutionPayload: &enginev1.ExecutionPayloadDeneb{}, ExecutionRequests: &enginev1.ExecutionRequests{}}}},
)
},
}

View File

@@ -73,7 +73,7 @@ func (e *endpoint) handlerWithMiddleware() http.HandlerFunc {
handler.ServeHTTP(rw, r)
if rw.statusCode >= 400 {
httpErrorCount.WithLabelValues(r.URL.Path, http.StatusText(rw.statusCode), r.Method).Inc()
httpErrorCount.WithLabelValues(e.name, http.StatusText(rw.statusCode), r.Method).Inc()
}
}
}

View File

@@ -993,10 +993,11 @@ func (s *Server) validateEquivocation(blk interfaces.ReadOnlyBeaconBlock) error
}
func (s *Server) validateBlobs(blk interfaces.SignedBeaconBlock, blobs [][]byte, proofs [][]byte) error {
const numberOfColumns = fieldparams.NumberOfColumns
if blk.Version() < version.Deneb {
return nil
}
numberOfColumns := params.BeaconConfig().NumberOfColumns
commitments, err := blk.Block().Body().BlobKzgCommitments()
if err != nil {
return errors.Wrap(err, "could not get blob kzg commitments")

View File

@@ -711,6 +711,7 @@ func (s *Server) SubmitAttesterSlashingsV2(w http.ResponseWriter, r *http.Reques
versionHeader := r.Header.Get(api.VersionHeader)
if versionHeader == "" {
httputil.HandleError(w, api.VersionHeader+" header is required", http.StatusBadRequest)
return
}
v, err := version.FromString(versionHeader)
if err != nil {

View File

@@ -2112,6 +2112,33 @@ func TestSubmitAttesterSlashingsV2(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, e.Code)
assert.StringContains(t, "Invalid attester slashing", e.Message)
})
t.Run("missing-version-header", func(t *testing.T) {
bs, err := util.NewBeaconStateElectra()
require.NoError(t, err)
broadcaster := &p2pMock.MockBroadcaster{}
s := &Server{
ChainInfoFetcher: &blockchainmock.ChainService{State: bs},
SlashingsPool: &slashingsmock.PoolMock{},
Broadcaster: broadcaster,
}
var body bytes.Buffer
_, err = body.WriteString(invalidAttesterSlashing)
require.NoError(t, err)
request := httptest.NewRequest(http.MethodPost, "http://example.com/beacon/pool/attester_slashings", &body)
// Intentionally do not set api.VersionHeader to verify missing header handling.
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.SubmitAttesterSlashingsV2(writer, request)
require.Equal(t, http.StatusBadRequest, writer.Code)
e := &httputil.DefaultJsonError{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e))
assert.Equal(t, http.StatusBadRequest, e.Code)
assert.StringContains(t, api.VersionHeader+" header is required", e.Message)
})
}
func TestSubmitProposerSlashing_InvalidSlashing(t *testing.T) {

View File

@@ -654,6 +654,10 @@ func (m *futureSyncMockFetcher) StateBySlot(context.Context, primitives.Slot) (s
return m.BeaconState, nil
}
func (m *futureSyncMockFetcher) StateByEpoch(context.Context, primitives.Epoch) (state.BeaconState, error) {
return m.BeaconState, nil
}
func TestGetSyncCommittees_Future(t *testing.T) {
st, _ := util.DeterministicGenesisStateAltair(t, params.BeaconConfig().SyncCommitteeSize)
syncCommittee := make([][]byte, params.BeaconConfig().SyncCommitteeSize)

View File

@@ -27,6 +27,7 @@ import (
"github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/testutil"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
mockSync "github.com/OffchainLabs/prysm/v7/beacon-chain/sync/initial-sync/testing"
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
@@ -3756,6 +3757,7 @@ func Test_validateBlobs(t *testing.T) {
})
t.Run("Fulu block with valid cell proofs", func(t *testing.T) {
const numberOfColumns = fieldparams.NumberOfColumns
blk := util.NewBeaconBlockFulu()
blk.Block.Slot = fs
@@ -3783,14 +3785,13 @@ func Test_validateBlobs(t *testing.T) {
require.NoError(t, err)
// Generate cell proofs for the blobs (flattened format like execution client)
numberOfColumns := params.BeaconConfig().NumberOfColumns
cellProofs := make([][]byte, uint64(blobCount)*numberOfColumns)
for blobIdx := range blobCount {
_, proofs, err := kzg.ComputeCellsAndKZGProofs(&kzgBlobs[blobIdx])
require.NoError(t, err)
for colIdx := range numberOfColumns {
cellProofIdx := uint64(blobIdx)*numberOfColumns + colIdx
cellProofIdx := blobIdx*numberOfColumns + colIdx
cellProofs[cellProofIdx] = proofs[colIdx][:]
}
}

View File

@@ -155,20 +155,19 @@ func TestGetSpec(t *testing.T) {
config.MaxAttesterSlashingsElectra = 88
config.MaxAttestationsElectra = 89
config.MaxWithdrawalRequestsPerPayload = 90
config.MaxCellsInExtendedMatrix = 91
config.UnsetDepositRequestsStartIndex = 92
config.MaxDepositRequestsPerPayload = 93
config.MaxPendingDepositsPerEpoch = 94
config.MaxBlobCommitmentsPerBlock = 95
config.MaxBytesPerTransaction = 96
config.MaxExtraDataBytes = 97
config.BytesPerLogsBloom = 98
config.MaxTransactionsPerPayload = 99
config.FieldElementsPerBlob = 100
config.KzgCommitmentInclusionProofDepth = 101
config.BlobsidecarSubnetCount = 102
config.BlobsidecarSubnetCountElectra = 103
config.SyncMessageDueBPS = 104
config.UnsetDepositRequestsStartIndex = 91
config.MaxDepositRequestsPerPayload = 92
config.MaxPendingDepositsPerEpoch = 93
config.MaxBlobCommitmentsPerBlock = 94
config.MaxBytesPerTransaction = 95
config.MaxExtraDataBytes = 96
config.BytesPerLogsBloom = 97
config.MaxTransactionsPerPayload = 98
config.FieldElementsPerBlob = 99
config.KzgCommitmentInclusionProofDepth = 100
config.BlobsidecarSubnetCount = 101
config.BlobsidecarSubnetCountElectra = 102
config.SyncMessageDueBPS = 103
var dbp [4]byte
copy(dbp[:], []byte{'0', '0', '0', '1'})
@@ -206,7 +205,7 @@ func TestGetSpec(t *testing.T) {
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), &resp))
data, ok := resp.Data.(map[string]any)
require.Equal(t, true, ok)
assert.Equal(t, 176, len(data))
assert.Equal(t, 175, len(data))
for k, v := range data {
t.Run(k, func(t *testing.T) {
switch k {
@@ -500,8 +499,6 @@ func TestGetSpec(t *testing.T) {
assert.Equal(t, "1024", v)
case "MAX_REQUEST_BLOCKS_DENEB":
assert.Equal(t, "128", v)
case "NUMBER_OF_COLUMNS":
assert.Equal(t, "128", v)
case "MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA":
assert.Equal(t, "128000000000", v)
case "MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT":
@@ -538,14 +535,12 @@ func TestGetSpec(t *testing.T) {
assert.Equal(t, "89", v)
case "MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD":
assert.Equal(t, "90", v)
case "MAX_CELLS_IN_EXTENDED_MATRIX":
assert.Equal(t, "91", v)
case "UNSET_DEPOSIT_REQUESTS_START_INDEX":
assert.Equal(t, "92", v)
assert.Equal(t, "91", v)
case "MAX_DEPOSIT_REQUESTS_PER_PAYLOAD":
assert.Equal(t, "93", v)
assert.Equal(t, "92", v)
case "MAX_PENDING_DEPOSITS_PER_EPOCH":
assert.Equal(t, "94", v)
assert.Equal(t, "93", v)
case "MAX_BLOBS_PER_BLOCK_ELECTRA":
assert.Equal(t, "9", v)
case "MAX_REQUEST_BLOB_SIDECARS_ELECTRA":
@@ -563,25 +558,25 @@ func TestGetSpec(t *testing.T) {
case "MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS":
assert.Equal(t, "4096", v)
case "MAX_BLOB_COMMITMENTS_PER_BLOCK":
assert.Equal(t, "95", v)
assert.Equal(t, "94", v)
case "MAX_BYTES_PER_TRANSACTION":
assert.Equal(t, "96", v)
assert.Equal(t, "95", v)
case "MAX_EXTRA_DATA_BYTES":
assert.Equal(t, "97", v)
assert.Equal(t, "96", v)
case "BYTES_PER_LOGS_BLOOM":
assert.Equal(t, "98", v)
assert.Equal(t, "97", v)
case "MAX_TRANSACTIONS_PER_PAYLOAD":
assert.Equal(t, "99", v)
assert.Equal(t, "98", v)
case "FIELD_ELEMENTS_PER_BLOB":
assert.Equal(t, "100", v)
assert.Equal(t, "99", v)
case "KZG_COMMITMENT_INCLUSION_PROOF_DEPTH":
assert.Equal(t, "101", v)
assert.Equal(t, "100", v)
case "BLOB_SIDECAR_SUBNET_COUNT":
assert.Equal(t, "102", v)
assert.Equal(t, "101", v)
case "BLOB_SIDECAR_SUBNET_COUNT_ELECTRA":
assert.Equal(t, "103", v)
assert.Equal(t, "102", v)
case "SYNC_MESSAGE_DUE_BPS":
assert.Equal(t, "104", v)
assert.Equal(t, "103", v)
case "BLOB_SCHEDULE":
blobSchedule, ok := v.([]any)
assert.Equal(t, true, ok)

View File

@@ -17,6 +17,7 @@ go_library(
"//beacon-chain/rpc/eth/helpers:go_default_library",
"//beacon-chain/rpc/eth/shared:go_default_library",
"//beacon-chain/rpc/lookup:go_default_library",
"//config/fieldparams:go_default_library",
"//config/params:go_default_library",
"//consensus-types/blocks:go_default_library",
"//consensus-types/primitives:go_default_library",

View File

@@ -15,6 +15,7 @@ import (
"github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/core"
"github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/eth/helpers"
"github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/eth/shared"
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
@@ -308,7 +309,7 @@ func (s *Server) DataColumnSidecars(w http.ResponseWriter, r *http.Request) {
// parseDataColumnIndices filters out invalid and duplicate data column indices
func parseDataColumnIndices(url *url.URL) ([]int, error) {
numberOfColumns := params.BeaconConfig().NumberOfColumns
const numberOfColumns = fieldparams.NumberOfColumns
rawIndices := url.Query()["indices"]
indices := make([]int, 0, numberOfColumns)
invalidIndices := make([]string, 0)

View File

@@ -709,15 +709,6 @@ func TestDataColumnSidecars(t *testing.T) {
}
func TestParseDataColumnIndices(t *testing.T) {
// Save the original config
originalConfig := params.BeaconConfig()
defer func() { params.OverrideBeaconConfig(originalConfig) }()
// Set NumberOfColumns to 128 for testing
config := params.BeaconConfig().Copy()
config.NumberOfColumns = 128
params.OverrideBeaconConfig(config)
tests := []struct {
name string
queryParams map[string][]string

View File

@@ -116,6 +116,7 @@ func (s *Server) GetLightClientUpdatesByRange(w http.ResponseWriter, req *http.R
for _, update := range updates {
if ctx.Err() != nil {
httputil.HandleError(w, "Context error: "+ctx.Err().Error(), http.StatusInternalServerError)
return
}
updateSlot := update.AttestedHeader().Beacon().Slot
@@ -131,12 +132,15 @@ func (s *Server) GetLightClientUpdatesByRange(w http.ResponseWriter, req *http.R
chunkLength = ssz.MarshalUint64(chunkLength, uint64(len(updateSSZ)+4))
if _, err := w.Write(chunkLength); err != nil {
httputil.HandleError(w, "Could not write chunk length: "+err.Error(), http.StatusInternalServerError)
return
}
if _, err := w.Write(updateEntry.ForkDigest[:]); err != nil {
httputil.HandleError(w, "Could not write fork digest: "+err.Error(), http.StatusInternalServerError)
return
}
if _, err := w.Write(updateSSZ); err != nil {
httputil.HandleError(w, "Could not write update SSZ: "+err.Error(), http.StatusInternalServerError)
return
}
}
} else {
@@ -145,6 +149,7 @@ func (s *Server) GetLightClientUpdatesByRange(w http.ResponseWriter, req *http.R
for _, update := range updates {
if ctx.Err() != nil {
httputil.HandleError(w, "Context error: "+ctx.Err().Error(), http.StatusInternalServerError)
return
}
updateJson, err := structs.LightClientUpdateFromConsensus(update)

View File

@@ -47,6 +47,10 @@ func TestLightClientHandler_GetLightClientBootstrap(t *testing.T) {
params.OverrideBeaconConfig(cfg)
for _, testVersion := range version.All()[1:] {
if testVersion == version.Gloas {
// TODO(16027): Unskip light client tests for Gloas
continue
}
t.Run(version.String(testVersion), func(t *testing.T) {
l := util.NewTestLightClient(t, testVersion)
@@ -178,6 +182,10 @@ func TestLightClientHandler_GetLightClientByRange(t *testing.T) {
t.Run("can save retrieve", func(t *testing.T) {
for _, testVersion := range version.All()[1:] {
if testVersion == version.Gloas {
// TODO(16027): Unskip light client tests for Gloas
continue
}
t.Run(version.String(testVersion), func(t *testing.T) {
slot := primitives.Slot(params.BeaconConfig().VersionToForkEpochMap()[testVersion] * primitives.Epoch(config.SlotsPerEpoch)).Add(1)
@@ -732,6 +740,10 @@ func TestLightClientHandler_GetLightClientFinalityUpdate(t *testing.T) {
})
for _, testVersion := range version.All()[1:] {
if testVersion == version.Gloas {
// TODO(16027): Unskip light client tests for Gloas
continue
}
t.Run(version.String(testVersion), func(t *testing.T) {
ctx := t.Context()
@@ -827,6 +839,10 @@ func TestLightClientHandler_GetLightClientOptimisticUpdate(t *testing.T) {
})
for _, testVersion := range version.All()[1:] {
if testVersion == version.Gloas {
// TODO(16027): Unskip light client tests for Gloas
continue
}
t.Run(version.String(testVersion), func(t *testing.T) {
ctx := t.Context()
l := util.NewTestLightClient(t, testVersion)

View File

@@ -132,6 +132,7 @@ func (s *Server) GetHealth(w http.ResponseWriter, r *http.Request) {
optimistic, err := s.OptimisticModeFetcher.IsOptimistic(ctx)
if err != nil {
httputil.HandleError(w, "Could not check optimistic status: "+err.Error(), http.StatusInternalServerError)
return
}
if s.SyncChecker.Synced() && !optimistic {
return

View File

@@ -228,7 +228,7 @@ func (s *Server) attRewardsState(w http.ResponseWriter, r *http.Request) (state.
}
st, err := s.Stater.StateBySlot(r.Context(), nextEpochEnd)
if err != nil {
httputil.HandleError(w, "Could not get state for epoch's starting slot: "+err.Error(), http.StatusInternalServerError)
shared.WriteStateFetchError(w, err)
return nil, false
}
return st, true

View File

@@ -19,7 +19,6 @@ go_library(
"//beacon-chain/cache:go_default_library",
"//beacon-chain/core/feed/operation:go_default_library",
"//beacon-chain/core/helpers:go_default_library",
"//beacon-chain/core/transition:go_default_library",
"//beacon-chain/db:go_default_library",
"//beacon-chain/operations/attestations:go_default_library",
"//beacon-chain/operations/synccommittee:go_default_library",
@@ -78,6 +77,7 @@ go_test(
"//beacon-chain/rpc/core:go_default_library",
"//beacon-chain/rpc/eth/rewards/testing:go_default_library",
"//beacon-chain/rpc/eth/shared/testing:go_default_library",
"//beacon-chain/rpc/lookup:go_default_library",
"//beacon-chain/rpc/testutil:go_default_library",
"//beacon-chain/state:go_default_library",
"//beacon-chain/state/stategen:go_default_library",
@@ -99,6 +99,7 @@ go_test(
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_prysmaticlabs_go_bitfield//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@com_github_sirupsen_logrus//hooks/test:go_default_library",
"@org_uber_go_mock//gomock:go_default_library",
],

View File

@@ -19,7 +19,6 @@ import (
"github.com/OffchainLabs/prysm/v7/beacon-chain/builder"
"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/transition"
"github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/core"
rpchelpers "github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/eth/helpers"
"github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/eth/shared"
@@ -835,12 +834,17 @@ func (s *Server) PrepareBeaconProposer(w http.ResponseWriter, r *http.Request) {
s.TrackedValidatorsCache.Set(val)
validatorIndices = append(validatorIndices, primitives.ValidatorIndex(validatorIndex))
}
if len(validatorIndices) == 0 {
return
}
log.WithFields(logrus.Fields{
"validatorIndices": validatorIndices,
}).Info("Updated fee recipient addresses")
log := log.WithField("validatorCount", len(validatorIndices))
if logrus.GetLevel() >= logrus.TraceLevel {
log = log.WithField("validatorIndices", validatorIndices)
}
log.Debug("Updated fee recipient addresses")
}
// GetAttesterDuties requests the beacon node to provide a set of attestation duties,
@@ -893,20 +897,15 @@ func (s *Server) GetAttesterDuties(w http.ResponseWriter, r *http.Request) {
return
}
var startSlot primitives.Slot
// For next epoch requests, we use the current epoch's state since committee
// assignments for next epoch can be computed from current epoch's state.
epochForState := requestedEpoch
if requestedEpoch == nextEpoch {
startSlot, err = slots.EpochStart(currentEpoch)
} else {
startSlot, err = slots.EpochStart(requestedEpoch)
epochForState = currentEpoch
}
st, err := s.Stater.StateByEpoch(ctx, epochForState)
if err != nil {
httputil.HandleError(w, fmt.Sprintf("Could not get start slot from epoch %d: %v", requestedEpoch, err), http.StatusInternalServerError)
return
}
st, err := s.Stater.StateBySlot(ctx, startSlot)
if err != nil {
httputil.HandleError(w, "Could not get state: "+err.Error(), http.StatusInternalServerError)
shared.WriteStateFetchError(w, err)
return
}
@@ -1015,39 +1014,11 @@ func (s *Server) GetProposerDuties(w http.ResponseWriter, r *http.Request) {
nextEpochLookahead = true
}
epochStartSlot, err := slots.EpochStart(requestedEpoch)
st, err := s.Stater.StateByEpoch(ctx, requestedEpoch)
if err != nil {
httputil.HandleError(w, fmt.Sprintf("Could not get start slot of epoch %d: %v", requestedEpoch, err), http.StatusInternalServerError)
shared.WriteStateFetchError(w, err)
return
}
var st state.BeaconState
// if the requested epoch is new, use the head state and the next slot cache
if requestedEpoch < currentEpoch {
st, err = s.Stater.StateBySlot(ctx, epochStartSlot)
if err != nil {
httputil.HandleError(w, fmt.Sprintf("Could not get state for slot %d: %v ", epochStartSlot, err), http.StatusInternalServerError)
return
}
} else {
st, err = s.HeadFetcher.HeadState(ctx)
if err != nil {
httputil.HandleError(w, fmt.Sprintf("Could not get head state: %v ", err), http.StatusInternalServerError)
return
}
// Notice that even for Fulu requests for the next epoch, we are only advancing the state to the start of the current epoch.
if st.Slot() < epochStartSlot {
headRoot, err := s.HeadFetcher.HeadRoot(ctx)
if err != nil {
httputil.HandleError(w, fmt.Sprintf("Could not get head root: %v ", err), http.StatusInternalServerError)
return
}
st, err = transition.ProcessSlotsUsingNextSlotCache(ctx, st, headRoot, epochStartSlot)
if err != nil {
httputil.HandleError(w, fmt.Sprintf("Could not process slots up to %d: %v ", epochStartSlot, err), http.StatusInternalServerError)
return
}
}
}
var assignments map[primitives.ValidatorIndex][]primitives.Slot
if nextEpochLookahead {
@@ -1098,7 +1069,8 @@ func (s *Server) GetProposerDuties(w http.ResponseWriter, r *http.Request) {
httputil.HandleError(w, "Could not check optimistic status: "+err.Error(), http.StatusInternalServerError)
return
}
if !sortProposerDuties(w, duties) {
if err = sortProposerDuties(duties); err != nil {
httputil.HandleError(w, "Could not sort proposer duties: "+err.Error(), http.StatusInternalServerError)
return
}
@@ -1169,14 +1141,10 @@ func (s *Server) GetSyncCommitteeDuties(w http.ResponseWriter, r *http.Request)
}
startingEpoch := min(requestedEpoch, currentEpoch)
slot, err := slots.EpochStart(startingEpoch)
st, err := s.Stater.StateByEpoch(ctx, startingEpoch)
if err != nil {
httputil.HandleError(w, "Could not get sync committee slot: "+err.Error(), http.StatusInternalServerError)
return
}
st, err := s.Stater.State(ctx, []byte(strconv.FormatUint(uint64(slot), 10)))
if err != nil {
httputil.HandleError(w, "Could not get sync committee state: "+err.Error(), http.StatusInternalServerError)
shared.WriteStateFetchError(w, err)
return
}
@@ -1322,7 +1290,7 @@ func (s *Server) GetLiveness(w http.ResponseWriter, r *http.Request) {
}
st, err = s.Stater.StateBySlot(ctx, epochEnd)
if err != nil {
httputil.HandleError(w, "Could not get slot for requested epoch: "+err.Error(), http.StatusInternalServerError)
shared.WriteStateFetchError(w, err)
return
}
participation, err = st.CurrentEpochParticipation()
@@ -1442,22 +1410,20 @@ func syncCommitteeDutiesAndVals(
return duties, vals, nil
}
func sortProposerDuties(w http.ResponseWriter, duties []*structs.ProposerDuty) bool {
ok := true
func sortProposerDuties(duties []*structs.ProposerDuty) error {
var err error
sort.Slice(duties, func(i, j int) bool {
si, err := strconv.ParseUint(duties[i].Slot, 10, 64)
if err != nil {
httputil.HandleError(w, "Could not parse slot: "+err.Error(), http.StatusInternalServerError)
ok = false
si, parseErr := strconv.ParseUint(duties[i].Slot, 10, 64)
if parseErr != nil {
err = errors.Wrap(parseErr, "could not parse slot")
return false
}
sj, err := strconv.ParseUint(duties[j].Slot, 10, 64)
if err != nil {
httputil.HandleError(w, "Could not parse slot: "+err.Error(), http.StatusInternalServerError)
ok = false
sj, parseErr := strconv.ParseUint(duties[j].Slot, 10, 64)
if parseErr != nil {
err = errors.Wrap(parseErr, "could not parse slot")
return false
}
return si < sj
})
return ok
return err
}

View File

@@ -25,6 +25,7 @@ import (
"github.com/OffchainLabs/prysm/v7/beacon-chain/operations/synccommittee"
p2pmock "github.com/OffchainLabs/prysm/v7/beacon-chain/p2p/testing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/core"
"github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/lookup"
"github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/testutil"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state/stategen"
@@ -44,6 +45,7 @@ import (
"github.com/OffchainLabs/prysm/v7/time/slots"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
logTest "github.com/sirupsen/logrus/hooks/test"
)
@@ -2005,6 +2007,7 @@ func TestGetAttesterDuties(t *testing.T) {
TimeFetcher: chain,
SyncChecker: &mockSync.Sync{IsSyncing: false},
OptimisticModeFetcher: chain,
HeadFetcher: chain,
BeaconDB: db,
}
@@ -2183,6 +2186,7 @@ func TestGetAttesterDuties(t *testing.T) {
Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{0: bs}},
TimeFetcher: chain,
OptimisticModeFetcher: chain,
HeadFetcher: chain,
SyncChecker: &mockSync.Sync{IsSyncing: false},
BeaconDB: db,
}
@@ -2223,6 +2227,62 @@ func TestGetAttesterDuties(t *testing.T) {
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e))
assert.Equal(t, http.StatusServiceUnavailable, e.Code)
})
t.Run("state not found returns 404", func(t *testing.T) {
chainSlot := primitives.Slot(0)
chain := &mockChain.ChainService{
State: bs, Root: genesisRoot[:], Slot: &chainSlot,
}
stateNotFoundErr := lookup.NewStateNotFoundError(8192, []byte("test"))
s := &Server{
Stater: &testutil.MockStater{CustomError: &stateNotFoundErr},
TimeFetcher: chain,
SyncChecker: &mockSync.Sync{IsSyncing: false},
OptimisticModeFetcher: chain,
HeadFetcher: chain,
}
var body bytes.Buffer
_, err = body.WriteString("[\"0\"]")
require.NoError(t, err)
request := httptest.NewRequest(http.MethodGet, "http://www.example.com/eth/v1/validator/duties/attester/{epoch}", &body)
request.SetPathValue("epoch", "0")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.GetAttesterDuties(writer, request)
assert.Equal(t, http.StatusNotFound, writer.Code)
e := &httputil.DefaultJsonError{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e))
assert.Equal(t, http.StatusNotFound, e.Code)
assert.StringContains(t, "State not found", e.Message)
})
t.Run("state fetch error returns 500", func(t *testing.T) {
chainSlot := primitives.Slot(0)
chain := &mockChain.ChainService{
State: bs, Root: genesisRoot[:], Slot: &chainSlot,
}
s := &Server{
Stater: &testutil.MockStater{CustomError: errors.New("internal error")},
TimeFetcher: chain,
SyncChecker: &mockSync.Sync{IsSyncing: false},
OptimisticModeFetcher: chain,
HeadFetcher: chain,
}
var body bytes.Buffer
_, err = body.WriteString("[\"0\"]")
require.NoError(t, err)
request := httptest.NewRequest(http.MethodGet, "http://www.example.com/eth/v1/validator/duties/attester/{epoch}", &body)
request.SetPathValue("epoch", "0")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.GetAttesterDuties(writer, request)
assert.Equal(t, http.StatusInternalServerError, writer.Code)
e := &httputil.DefaultJsonError{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e))
assert.Equal(t, http.StatusInternalServerError, e.Code)
})
}
func TestGetProposerDuties(t *testing.T) {
@@ -2426,6 +2486,60 @@ func TestGetProposerDuties(t *testing.T) {
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e))
assert.Equal(t, http.StatusServiceUnavailable, e.Code)
})
t.Run("state not found returns 404", func(t *testing.T) {
bs, err := transition.GenesisBeaconState(t.Context(), deposits, 0, eth1Data)
require.NoError(t, err)
chainSlot := primitives.Slot(0)
chain := &mockChain.ChainService{
State: bs, Root: genesisRoot[:], Slot: &chainSlot,
}
stateNotFoundErr := lookup.NewStateNotFoundError(8192, []byte("test"))
s := &Server{
Stater: &testutil.MockStater{CustomError: &stateNotFoundErr},
TimeFetcher: chain,
SyncChecker: &mockSync.Sync{IsSyncing: false},
OptimisticModeFetcher: chain,
HeadFetcher: chain,
}
request := httptest.NewRequest(http.MethodGet, "http://www.example.com/eth/v1/validator/duties/proposer/{epoch}", nil)
request.SetPathValue("epoch", "0")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.GetProposerDuties(writer, request)
assert.Equal(t, http.StatusNotFound, writer.Code)
e := &httputil.DefaultJsonError{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e))
assert.Equal(t, http.StatusNotFound, e.Code)
assert.StringContains(t, "State not found", e.Message)
})
t.Run("state fetch error returns 500", func(t *testing.T) {
bs, err := transition.GenesisBeaconState(t.Context(), deposits, 0, eth1Data)
require.NoError(t, err)
chainSlot := primitives.Slot(0)
chain := &mockChain.ChainService{
State: bs, Root: genesisRoot[:], Slot: &chainSlot,
}
s := &Server{
Stater: &testutil.MockStater{CustomError: errors.New("internal error")},
TimeFetcher: chain,
SyncChecker: &mockSync.Sync{IsSyncing: false},
OptimisticModeFetcher: chain,
HeadFetcher: chain,
}
request := httptest.NewRequest(http.MethodGet, "http://www.example.com/eth/v1/validator/duties/proposer/{epoch}", nil)
request.SetPathValue("epoch", "0")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.GetProposerDuties(writer, request)
assert.Equal(t, http.StatusInternalServerError, writer.Code)
e := &httputil.DefaultJsonError{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e))
assert.Equal(t, http.StatusInternalServerError, e.Code)
})
}
func TestGetSyncCommitteeDuties(t *testing.T) {
@@ -2456,7 +2570,7 @@ func TestGetSyncCommitteeDuties(t *testing.T) {
}
require.NoError(t, st.SetNextSyncCommittee(nextCommittee))
mockChainService := &mockChain.ChainService{Genesis: genesisTime}
mockChainService := &mockChain.ChainService{Genesis: genesisTime, State: st}
s := &Server{
Stater: &testutil.MockStater{BeaconState: st},
SyncChecker: &mockSync.Sync{IsSyncing: false},
@@ -2647,7 +2761,7 @@ func TestGetSyncCommitteeDuties(t *testing.T) {
return newSyncPeriodSt
}
}
mockChainService := &mockChain.ChainService{Genesis: genesisTime, Slot: &newSyncPeriodStartSlot}
mockChainService := &mockChain.ChainService{Genesis: genesisTime, Slot: &newSyncPeriodStartSlot, State: newSyncPeriodSt}
s := &Server{
Stater: &testutil.MockStater{BeaconState: stateFetchFn(newSyncPeriodStartSlot)},
SyncChecker: &mockSync.Sync{IsSyncing: false},
@@ -2728,8 +2842,7 @@ func TestGetSyncCommitteeDuties(t *testing.T) {
slot, err := slots.EpochStart(1)
require.NoError(t, err)
st2, err := util.NewBeaconStateBellatrix()
require.NoError(t, err)
st2 := st.Copy()
require.NoError(t, st2.SetSlot(slot))
mockChainService := &mockChain.ChainService{
@@ -2743,7 +2856,7 @@ func TestGetSyncCommitteeDuties(t *testing.T) {
State: st2,
}
s := &Server{
Stater: &testutil.MockStater{BeaconState: st},
Stater: &testutil.MockStater{BeaconState: st2},
SyncChecker: &mockSync.Sync{IsSyncing: false},
TimeFetcher: mockChainService,
HeadFetcher: mockChainService,
@@ -2788,6 +2901,62 @@ func TestGetSyncCommitteeDuties(t *testing.T) {
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e))
assert.Equal(t, http.StatusServiceUnavailable, e.Code)
})
t.Run("state not found returns 404", func(t *testing.T) {
slot := 2 * params.BeaconConfig().SlotsPerEpoch
chainService := &mockChain.ChainService{
Slot: &slot,
}
stateNotFoundErr := lookup.NewStateNotFoundError(8192, []byte("test"))
s := &Server{
Stater: &testutil.MockStater{CustomError: &stateNotFoundErr},
TimeFetcher: chainService,
SyncChecker: &mockSync.Sync{IsSyncing: false},
OptimisticModeFetcher: chainService,
HeadFetcher: chainService,
}
var body bytes.Buffer
_, err := body.WriteString("[\"1\"]")
require.NoError(t, err)
request := httptest.NewRequest(http.MethodGet, "http://www.example.com/eth/v1/validator/duties/sync/{epoch}", &body)
request.SetPathValue("epoch", "1")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.GetSyncCommitteeDuties(writer, request)
assert.Equal(t, http.StatusNotFound, writer.Code)
e := &httputil.DefaultJsonError{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e))
assert.Equal(t, http.StatusNotFound, e.Code)
assert.StringContains(t, "State not found", e.Message)
})
t.Run("state fetch error returns 500", func(t *testing.T) {
slot := 2 * params.BeaconConfig().SlotsPerEpoch
chainService := &mockChain.ChainService{
Slot: &slot,
}
s := &Server{
Stater: &testutil.MockStater{CustomError: errors.New("internal error")},
TimeFetcher: chainService,
SyncChecker: &mockSync.Sync{IsSyncing: false},
OptimisticModeFetcher: chainService,
HeadFetcher: chainService,
}
var body bytes.Buffer
_, err := body.WriteString("[\"1\"]")
require.NoError(t, err)
request := httptest.NewRequest(http.MethodGet, "http://www.example.com/eth/v1/validator/duties/sync/{epoch}", &body)
request.SetPathValue("epoch", "1")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.GetSyncCommitteeDuties(writer, request)
assert.Equal(t, http.StatusInternalServerError, writer.Code)
e := &httputil.DefaultJsonError{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e))
assert.Equal(t, http.StatusInternalServerError, e.Code)
})
}
func TestPrepareBeaconProposer(t *testing.T) {
@@ -2854,6 +3023,8 @@ func TestPrepareBeaconProposer(t *testing.T) {
func TestProposer_PrepareBeaconProposerOverlapping(t *testing.T) {
hook := logTest.NewGlobal()
logrus.SetLevel(logrus.DebugLevel)
db := dbutil.SetupDB(t)
// New validator

View File

@@ -11,6 +11,7 @@ go_library(
deps = [
"//beacon-chain/blockchain:go_default_library",
"//beacon-chain/core/peerdas:go_default_library",
"//beacon-chain/core/transition:go_default_library",
"//beacon-chain/db:go_default_library",
"//beacon-chain/db/filesystem:go_default_library",
"//beacon-chain/rpc/core:go_default_library",

View File

@@ -450,7 +450,7 @@ func (p *BeaconDbBlocker) blobsDataFromStoredDataColumns(root [fieldparams.RootL
if count < peerdas.MinimumColumnCountToReconstruct() {
// There is no way to reconstruct the data columns.
return nil, &core.RpcError{
Err: errors.Errorf("the node does not custody enough data columns to reconstruct blobs - please start the beacon node with the `--%s` flag to ensure this call to succeed, or retry later if it is already the case", flags.SubscribeAllDataSubnets.Name),
Err: errors.Errorf("the node does not custody enough data columns to reconstruct blobs - please start the beacon node with the `--%s` flag to ensure this call to succeed", flags.SemiSupernode.Name),
Reason: core.NotFound,
}
}
@@ -555,7 +555,7 @@ func (p *BeaconDbBlocker) blobSidecarsFromStoredDataColumns(block blocks.ROBlock
if count < peerdas.MinimumColumnCountToReconstruct() {
// There is no way to reconstruct the data columns.
return nil, &core.RpcError{
Err: errors.Errorf("the node does not custody enough data columns to reconstruct blobs - please start the beacon node with the `--%s` flag to ensure this call to succeed, or retry later if it is already the case", flags.SubscribeAllDataSubnets.Name),
Err: errors.Errorf("the node does not custody enough data columns to reconstruct blobs - please start the beacon node with the `--%s` flag to ensure this call to succeed, or retry later if it is already the case", flags.Supernode.Name),
Reason: core.NotFound,
}
}
@@ -628,6 +628,8 @@ func (p *BeaconDbBlocker) neededDataColumnSidecars(root [fieldparams.RootLength]
// - no block, 404
// - block exists, before Fulu fork, 400 (data columns are not supported before Fulu fork)
func (p *BeaconDbBlocker) DataColumns(ctx context.Context, id string, indices []int) ([]blocks.VerifiedRODataColumn, *core.RpcError) {
const numberOfColumns = fieldparams.NumberOfColumns
// Check for genesis block first (not supported for data columns)
if id == "genesis" {
return nil, &core.RpcError{Err: errors.New("data columns are not supported for Phase 0 fork"), Reason: core.BadRequest}
@@ -681,7 +683,6 @@ func (p *BeaconDbBlocker) DataColumns(ctx context.Context, id string, indices []
}
} else {
// Validate and convert indices
numberOfColumns := params.BeaconConfig().NumberOfColumns
for _, index := range indices {
if index < 0 || uint64(index) >= numberOfColumns {
return nil, &core.RpcError{

View File

@@ -8,6 +8,7 @@ import (
"strings"
"github.com/OffchainLabs/prysm/v7/beacon-chain/blockchain"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/transition"
"github.com/OffchainLabs/prysm/v7/beacon-chain/db"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state/stategen"
@@ -82,8 +83,8 @@ type StateRootNotFoundError struct {
}
// NewStateRootNotFoundError creates a new error instance.
func NewStateRootNotFoundError(stateRootsSize int) StateNotFoundError {
return StateNotFoundError{
func NewStateRootNotFoundError(stateRootsSize int) StateRootNotFoundError {
return StateRootNotFoundError{
message: fmt.Sprintf("state root not found in the last %d state roots", stateRootsSize),
}
}
@@ -98,6 +99,7 @@ type Stater interface {
State(ctx context.Context, id []byte) (state.BeaconState, error)
StateRoot(ctx context.Context, id []byte) ([]byte, error)
StateBySlot(ctx context.Context, slot primitives.Slot) (state.BeaconState, error)
StateByEpoch(ctx context.Context, epoch primitives.Epoch) (state.BeaconState, error)
}
// BeaconDbStater is an implementation of Stater. It retrieves states from the beacon chain database.
@@ -267,6 +269,46 @@ func (p *BeaconDbStater) StateBySlot(ctx context.Context, target primitives.Slot
return st, nil
}
// StateByEpoch returns the state for the start of the requested epoch.
// For current or next epoch, it uses the head state and next slot cache for efficiency.
// For past epochs, it replays blocks from the most recent canonical state.
func (p *BeaconDbStater) StateByEpoch(ctx context.Context, epoch primitives.Epoch) (state.BeaconState, error) {
ctx, span := trace.StartSpan(ctx, "statefetcher.StateByEpoch")
defer span.End()
targetSlot, err := slots.EpochStart(epoch)
if err != nil {
return nil, errors.Wrap(err, "could not get epoch start slot")
}
currentSlot := p.GenesisTimeFetcher.CurrentSlot()
currentEpoch := slots.ToEpoch(currentSlot)
// For past epochs, use the replay mechanism
if epoch < currentEpoch {
return p.StateBySlot(ctx, targetSlot)
}
// For current or next epoch, use head state + next slot cache (much faster)
headState, err := p.ChainInfoFetcher.HeadState(ctx)
if err != nil {
return nil, errors.Wrap(err, "could not get head state")
}
// If head state is already at or past the target slot, return it
if headState.Slot() >= targetSlot {
return headState, nil
}
// Process slots using the next slot cache
headRoot := p.ChainInfoFetcher.CachedHeadRoot()
st, err := transition.ProcessSlotsUsingNextSlotCache(ctx, headState, headRoot[:], targetSlot)
if err != nil {
return nil, errors.Wrapf(err, "could not process slots up to %d", targetSlot)
}
return st, nil
}
func (p *BeaconDbStater) headStateRoot(ctx context.Context) ([]byte, error) {
b, err := p.ChainInfoFetcher.HeadBlock(ctx)
if err != nil {

View File

@@ -444,3 +444,111 @@ func TestStateBySlot_AfterHeadSlot(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, primitives.Slot(101), st.Slot())
}
func TestStateByEpoch(t *testing.T) {
ctx := t.Context()
slotsPerEpoch := params.BeaconConfig().SlotsPerEpoch
t.Run("current epoch uses head state", func(t *testing.T) {
// Head is at slot 5 (epoch 0), requesting epoch 0
headSlot := primitives.Slot(5)
headSt, err := statenative.InitializeFromProtoPhase0(&ethpb.BeaconState{Slot: headSlot})
require.NoError(t, err)
currentSlot := headSlot
mock := &chainMock.ChainService{State: headSt, Slot: &currentSlot}
p := BeaconDbStater{ChainInfoFetcher: mock, GenesisTimeFetcher: mock}
st, err := p.StateByEpoch(ctx, 0)
require.NoError(t, err)
// Should return head state since it's already past epoch start
assert.Equal(t, headSlot, st.Slot())
})
t.Run("current epoch processes slots to epoch start", func(t *testing.T) {
// Head is at slot 5 (epoch 0), requesting epoch 1
// Current slot is 32 (epoch 1), so epoch 1 is current epoch
headSlot := primitives.Slot(5)
headSt, err := statenative.InitializeFromProtoPhase0(&ethpb.BeaconState{Slot: headSlot})
require.NoError(t, err)
currentSlot := slotsPerEpoch // slot 32, epoch 1
mock := &chainMock.ChainService{State: headSt, Slot: &currentSlot}
p := BeaconDbStater{ChainInfoFetcher: mock, GenesisTimeFetcher: mock}
// Note: This will fail since ProcessSlotsUsingNextSlotCache requires proper setup
// In real usage, the transition package handles this properly
_, err = p.StateByEpoch(ctx, 1)
// The error is expected since we don't have a fully initialized beacon state
// that can process slots (missing committees, etc.)
assert.NotNil(t, err)
})
t.Run("past epoch uses replay", func(t *testing.T) {
// Head is at epoch 2, requesting epoch 0 (past)
headSlot := slotsPerEpoch * 2 // slot 64, epoch 2
headSt, err := statenative.InitializeFromProtoPhase0(&ethpb.BeaconState{Slot: headSlot})
require.NoError(t, err)
pastEpochSt, err := statenative.InitializeFromProtoPhase0(&ethpb.BeaconState{Slot: 0})
require.NoError(t, err)
currentSlot := headSlot
mock := &chainMock.ChainService{State: headSt, Slot: &currentSlot}
mockReplayer := mockstategen.NewReplayerBuilder()
mockReplayer.SetMockStateForSlot(pastEpochSt, 0)
p := BeaconDbStater{ChainInfoFetcher: mock, GenesisTimeFetcher: mock, ReplayerBuilder: mockReplayer}
st, err := p.StateByEpoch(ctx, 0)
require.NoError(t, err)
assert.Equal(t, primitives.Slot(0), st.Slot())
})
t.Run("next epoch uses head state path", func(t *testing.T) {
// Head is at slot 30 (epoch 0), requesting epoch 1 (next)
// Current slot is 30 (epoch 0), so epoch 1 is next epoch
headSlot := primitives.Slot(30)
headSt, err := statenative.InitializeFromProtoPhase0(&ethpb.BeaconState{Slot: headSlot})
require.NoError(t, err)
currentSlot := headSlot
mock := &chainMock.ChainService{State: headSt, Slot: &currentSlot}
p := BeaconDbStater{ChainInfoFetcher: mock, GenesisTimeFetcher: mock}
// Note: This will fail since ProcessSlotsUsingNextSlotCache requires proper setup
_, err = p.StateByEpoch(ctx, 1)
// The error is expected since we don't have a fully initialized beacon state
assert.NotNil(t, err)
})
t.Run("head state already at target slot returns immediately", func(t *testing.T) {
// Head is at slot 32 (epoch 1 start), requesting epoch 1
headSlot := slotsPerEpoch // slot 32
headSt, err := statenative.InitializeFromProtoPhase0(&ethpb.BeaconState{Slot: headSlot})
require.NoError(t, err)
currentSlot := headSlot
mock := &chainMock.ChainService{State: headSt, Slot: &currentSlot}
p := BeaconDbStater{ChainInfoFetcher: mock, GenesisTimeFetcher: mock}
st, err := p.StateByEpoch(ctx, 1)
require.NoError(t, err)
assert.Equal(t, headSlot, st.Slot())
})
t.Run("head state past target slot returns head state", func(t *testing.T) {
// Head is at slot 40, requesting epoch 1 (starts at slot 32)
headSlot := primitives.Slot(40)
headSt, err := statenative.InitializeFromProtoPhase0(&ethpb.BeaconState{Slot: headSlot})
require.NoError(t, err)
currentSlot := headSlot
mock := &chainMock.ChainService{State: headSt, Slot: &currentSlot}
p := BeaconDbStater{ChainInfoFetcher: mock, GenesisTimeFetcher: mock}
st, err := p.StateByEpoch(ctx, 1)
require.NoError(t, err)
// Returns head state since it's already >= epoch start
assert.Equal(t, headSlot, st.Slot())
})
}

View File

@@ -547,11 +547,19 @@ func (vs *Server) PrepareBeaconProposer(
vs.TrackedValidatorsCache.Set(val)
validatorIndices = append(validatorIndices, r.ValidatorIndex)
}
if len(validatorIndices) != 0 {
log.WithFields(logrus.Fields{
"validatorCount": len(validatorIndices),
}).Debug("Updated fee recipient addresses for validator indices")
if len(validatorIndices) == 0 {
return &emptypb.Empty{}, nil
}
log := log.WithField("validatorCount", len(validatorIndices))
if logrus.GetLevel() >= logrus.TraceLevel {
log = log.WithField("validatorIndices", validatorIndices)
}
log.Debug("Updated fee recipient addresses")
return &emptypb.Empty{}, nil
}

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