mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-09 15:37:56 -05:00
<!-- 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>
317 lines
9.1 KiB
Go
317 lines
9.1 KiB
Go
package kv
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/OffchainLabs/prysm/v7/config/params"
|
|
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
|
|
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
|
|
"github.com/OffchainLabs/prysm/v7/testing/require"
|
|
"github.com/OffchainLabs/prysm/v7/time/slots"
|
|
bolt "go.etcd.io/bbolt"
|
|
)
|
|
|
|
// getCustodyInfoFromDB reads the custody info directly from the database for testing purposes.
|
|
func getCustodyInfoFromDB(t *testing.T, db *Store) (primitives.Slot, uint64) {
|
|
t.Helper()
|
|
var earliestSlot primitives.Slot
|
|
var groupCount uint64
|
|
|
|
err := db.db.View(func(tx *bolt.Tx) error {
|
|
bucket := tx.Bucket(custodyBucket)
|
|
if bucket == nil {
|
|
return nil
|
|
}
|
|
|
|
// Read group count
|
|
groupCountBytes := bucket.Get(groupCountKey)
|
|
if len(groupCountBytes) != 0 {
|
|
groupCount = bytesutil.BytesToUint64BigEndian(groupCountBytes)
|
|
}
|
|
|
|
// Read earliest available slot
|
|
earliestSlotBytes := bucket.Get(earliestAvailableSlotKey)
|
|
if len(earliestSlotBytes) != 0 {
|
|
earliestSlot = primitives.Slot(bytesutil.BytesToUint64BigEndian(earliestSlotBytes))
|
|
}
|
|
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return earliestSlot, groupCount
|
|
}
|
|
|
|
// getSubscriptionStatusFromDB reads the subscription status directly from the database for testing purposes.
|
|
func getSubscriptionStatusFromDB(t *testing.T, db *Store) bool {
|
|
t.Helper()
|
|
var subscribed bool
|
|
|
|
err := db.db.View(func(tx *bolt.Tx) error {
|
|
bucket := tx.Bucket(custodyBucket)
|
|
if bucket == nil {
|
|
return nil
|
|
}
|
|
|
|
bytes := bucket.Get(subscribeAllDataSubnetsKey)
|
|
if len(bytes) != 0 && bytes[0] == 1 {
|
|
subscribed = true
|
|
}
|
|
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return subscribed
|
|
}
|
|
|
|
|
|
func TestUpdateCustodyInfo(t *testing.T) {
|
|
ctx := t.Context()
|
|
|
|
t.Run("initial update with empty database", func(t *testing.T) {
|
|
const (
|
|
earliestSlot = primitives.Slot(100)
|
|
groupCount = uint64(5)
|
|
)
|
|
|
|
db := setupDB(t)
|
|
|
|
slot, count, err := db.UpdateCustodyInfo(ctx, earliestSlot, groupCount)
|
|
require.NoError(t, err)
|
|
require.Equal(t, earliestSlot, slot)
|
|
require.Equal(t, groupCount, count)
|
|
|
|
storedSlot, storedCount := getCustodyInfoFromDB(t, db)
|
|
require.Equal(t, earliestSlot, storedSlot)
|
|
require.Equal(t, groupCount, storedCount)
|
|
})
|
|
|
|
t.Run("update with higher group count", func(t *testing.T) {
|
|
const (
|
|
initialSlot = primitives.Slot(100)
|
|
initialCount = uint64(5)
|
|
earliestSlot = primitives.Slot(200)
|
|
groupCount = uint64(10)
|
|
)
|
|
|
|
db := setupDB(t)
|
|
|
|
_, _, err := db.UpdateCustodyInfo(ctx, initialSlot, initialCount)
|
|
require.NoError(t, err)
|
|
|
|
slot, count, err := db.UpdateCustodyInfo(ctx, earliestSlot, groupCount)
|
|
require.NoError(t, err)
|
|
require.Equal(t, earliestSlot, slot)
|
|
require.Equal(t, groupCount, count)
|
|
|
|
storedSlot, storedCount := getCustodyInfoFromDB(t, db)
|
|
require.Equal(t, earliestSlot, storedSlot)
|
|
require.Equal(t, groupCount, storedCount)
|
|
})
|
|
|
|
t.Run("update with lower group count should not update", func(t *testing.T) {
|
|
const (
|
|
initialSlot = primitives.Slot(200)
|
|
initialCount = uint64(10)
|
|
earliestSlot = primitives.Slot(300)
|
|
groupCount = uint64(8)
|
|
)
|
|
|
|
db := setupDB(t)
|
|
|
|
_, _, err := db.UpdateCustodyInfo(ctx, initialSlot, initialCount)
|
|
require.NoError(t, err)
|
|
|
|
slot, count, err := db.UpdateCustodyInfo(ctx, earliestSlot, groupCount)
|
|
require.NoError(t, err)
|
|
require.Equal(t, initialSlot, slot)
|
|
require.Equal(t, initialCount, count)
|
|
|
|
storedSlot, storedCount := getCustodyInfoFromDB(t, db)
|
|
require.Equal(t, initialSlot, storedSlot)
|
|
require.Equal(t, initialCount, storedCount)
|
|
})
|
|
}
|
|
|
|
func TestUpdateEarliestAvailableSlot(t *testing.T) {
|
|
ctx := t.Context()
|
|
|
|
t.Run("allow decreasing earliest slot (backfill scenario)", func(t *testing.T) {
|
|
const (
|
|
initialSlot = primitives.Slot(300)
|
|
initialCount = uint64(10)
|
|
earliestSlot = primitives.Slot(200) // Lower than initial (backfill discovered earlier blocks)
|
|
)
|
|
|
|
db := setupDB(t)
|
|
|
|
// Initialize custody info
|
|
_, _, err := db.UpdateCustodyInfo(ctx, initialSlot, initialCount)
|
|
require.NoError(t, err)
|
|
|
|
// Update with a lower slot (should update for backfill)
|
|
err = db.UpdateEarliestAvailableSlot(ctx, earliestSlot)
|
|
require.NoError(t, err)
|
|
|
|
storedSlot, storedCount := getCustodyInfoFromDB(t, db)
|
|
require.Equal(t, earliestSlot, storedSlot)
|
|
require.Equal(t, initialCount, storedCount)
|
|
})
|
|
|
|
t.Run("allow increasing slot within MIN_EPOCHS_FOR_BLOCK_REQUESTS (pruning scenario)", func(t *testing.T) {
|
|
db := setupDB(t)
|
|
|
|
// Calculate the current slot and minimum required slot based on actual current time
|
|
genesisTime := time.Unix(int64(params.BeaconConfig().MinGenesisTime+params.BeaconConfig().GenesisDelay), 0)
|
|
currentSlot := slots.CurrentSlot(genesisTime)
|
|
currentEpoch := slots.ToEpoch(currentSlot)
|
|
minEpochsForBlocks := primitives.Epoch(params.BeaconConfig().MinEpochsForBlockRequests)
|
|
|
|
var minRequiredEpoch primitives.Epoch
|
|
if currentEpoch > minEpochsForBlocks {
|
|
minRequiredEpoch = currentEpoch - minEpochsForBlocks
|
|
} else {
|
|
minRequiredEpoch = 0
|
|
}
|
|
|
|
minRequiredSlot, err := slots.EpochStart(minRequiredEpoch)
|
|
require.NoError(t, err)
|
|
|
|
// Initial setup: set earliest slot well before minRequiredSlot
|
|
const groupCount = uint64(5)
|
|
initialSlot := primitives.Slot(1000)
|
|
|
|
_, _, err = db.UpdateCustodyInfo(ctx, initialSlot, groupCount)
|
|
require.NoError(t, err)
|
|
|
|
// Try to increase to a slot that's still BEFORE minRequiredSlot (should succeed)
|
|
validSlot := minRequiredSlot - 100
|
|
|
|
err = db.UpdateEarliestAvailableSlot(ctx, validSlot)
|
|
require.NoError(t, err)
|
|
|
|
// Verify the database was updated
|
|
storedSlot, storedCount := getCustodyInfoFromDB(t, db)
|
|
require.Equal(t, validSlot, storedSlot)
|
|
require.Equal(t, groupCount, storedCount)
|
|
})
|
|
|
|
t.Run("prevent increasing slot beyond MIN_EPOCHS_FOR_BLOCK_REQUESTS", func(t *testing.T) {
|
|
db := setupDB(t)
|
|
|
|
// Calculate the current slot and minimum required slot based on actual current time
|
|
genesisTime := time.Unix(int64(params.BeaconConfig().MinGenesisTime+params.BeaconConfig().GenesisDelay), 0)
|
|
currentSlot := slots.CurrentSlot(genesisTime)
|
|
currentEpoch := slots.ToEpoch(currentSlot)
|
|
minEpochsForBlocks := primitives.Epoch(params.BeaconConfig().MinEpochsForBlockRequests)
|
|
|
|
var minRequiredEpoch primitives.Epoch
|
|
if currentEpoch > minEpochsForBlocks {
|
|
minRequiredEpoch = currentEpoch - minEpochsForBlocks
|
|
} else {
|
|
minRequiredEpoch = 0
|
|
}
|
|
|
|
minRequiredSlot, err := slots.EpochStart(minRequiredEpoch)
|
|
require.NoError(t, err)
|
|
|
|
// Initial setup: set a valid earliest slot (well before minRequiredSlot)
|
|
const initialCount = uint64(5)
|
|
initialSlot := primitives.Slot(1000)
|
|
|
|
_, _, err = db.UpdateCustodyInfo(ctx, initialSlot, initialCount)
|
|
require.NoError(t, err)
|
|
|
|
// Try to set earliest slot beyond the minimum required slot
|
|
invalidSlot := minRequiredSlot + 100
|
|
|
|
// This should fail
|
|
err = db.UpdateEarliestAvailableSlot(ctx, invalidSlot)
|
|
require.ErrorContains(t, "cannot increase earliest available slot", err)
|
|
require.ErrorContains(t, "exceeds minimum required slot", err)
|
|
|
|
// Verify the database wasn't updated
|
|
storedSlot, storedCount := getCustodyInfoFromDB(t, db)
|
|
require.Equal(t, initialSlot, storedSlot)
|
|
require.Equal(t, initialCount, storedCount)
|
|
})
|
|
|
|
t.Run("no change when slot equals current slot", func(t *testing.T) {
|
|
const (
|
|
initialSlot = primitives.Slot(100)
|
|
initialCount = uint64(5)
|
|
)
|
|
|
|
db := setupDB(t)
|
|
|
|
// Initialize custody info
|
|
_, _, err := db.UpdateCustodyInfo(ctx, initialSlot, initialCount)
|
|
require.NoError(t, err)
|
|
|
|
// Update with the same slot
|
|
err = db.UpdateEarliestAvailableSlot(ctx, initialSlot)
|
|
require.NoError(t, err)
|
|
|
|
storedSlot, storedCount := getCustodyInfoFromDB(t, db)
|
|
require.Equal(t, initialSlot, storedSlot)
|
|
require.Equal(t, initialCount, storedCount)
|
|
})
|
|
}
|
|
|
|
func TestUpdateSubscribedToAllDataSubnets(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("initial update with empty database - set to false", func(t *testing.T) {
|
|
db := setupDB(t)
|
|
|
|
prev, err := db.UpdateSubscribedToAllDataSubnets(ctx, false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, false, prev)
|
|
|
|
stored := getSubscriptionStatusFromDB(t, db)
|
|
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)
|
|
|
|
_, err := db.UpdateSubscribedToAllDataSubnets(ctx, true)
|
|
require.NoError(t, err)
|
|
|
|
prev, err := db.UpdateSubscribedToAllDataSubnets(ctx, false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, true, prev)
|
|
|
|
stored := getSubscriptionStatusFromDB(t, db)
|
|
require.Equal(t, true, stored)
|
|
})
|
|
|
|
t.Run("update from true to true (no change)", func(t *testing.T) {
|
|
db := setupDB(t)
|
|
|
|
_, err := db.UpdateSubscribedToAllDataSubnets(ctx, true)
|
|
require.NoError(t, err)
|
|
|
|
prev, err := db.UpdateSubscribedToAllDataSubnets(ctx, true)
|
|
require.NoError(t, err)
|
|
require.Equal(t, true, prev)
|
|
|
|
stored := getSubscriptionStatusFromDB(t, db)
|
|
require.Equal(t, true, stored)
|
|
})
|
|
}
|