diff --git a/cl/aggregation/pool_impl.go b/cl/aggregation/pool_impl.go
index bcbc4e7353b..9f5a40ed1bb 100644
--- a/cl/aggregation/pool_impl.go
+++ b/cl/aggregation/pool_impl.go
@@ -84,7 +84,9 @@ func (p *aggregationPoolImpl) AddAttestation(inAtt *solid.Attestation) error {
defer p.aggregatesLock.Unlock()
att, ok := p.aggregates[hashRoot]
if !ok {
- p.aggregates[hashRoot] = inAtt.Copy()
+ storedAttestation := inAtt.Copy()
+ storedAttestation.SetVersion(clversion)
+ p.aggregates[hashRoot] = storedAttestation
return nil
}
@@ -111,19 +113,21 @@ func (p *aggregationPoolImpl) AddAttestation(inAtt *solid.Attestation) error {
return err
}
// update attestation
- p.aggregates[hashRoot] = &solid.Attestation{
+ mergedAttestation := &solid.Attestation{
AggregationBits: mergedBits,
Data: att.Data,
Signature: mergedSig,
}
+ mergedAttestation.SetVersion(clversion)
+ p.aggregates[hashRoot] = mergedAttestation
} else {
// Electra and after case, aggregate by committee
- p.aggregateByCommittee(inAtt)
+ return p.aggregateByCommittee(inAtt, clversion)
}
return nil
}
-func (p *aggregationPoolImpl) aggregateByCommittee(inAtt *solid.Attestation) error {
+func (p *aggregationPoolImpl) aggregateByCommittee(inAtt *solid.Attestation, version clparams.StateVersion) error {
indices := inAtt.CommitteeBits.GetOnIndices()
if len(indices) != 1 {
// it's composed of multiple committees, so ignore
@@ -140,7 +144,9 @@ func (p *aggregationPoolImpl) aggregateByCommittee(inAtt *solid.Attestation) err
}
att, exist := p.aggregatesInCommittee.Get(key)
if !exist {
- p.aggregatesInCommittee.Add(key, inAtt)
+ storedAttestation := inAtt.Copy()
+ storedAttestation.SetVersion(version)
+ p.aggregatesInCommittee.Add(key, storedAttestation)
return nil
}
@@ -164,12 +170,14 @@ func (p *aggregationPoolImpl) aggregateByCommittee(inAtt *solid.Attestation) err
}
var mergedSig [96]byte
copy(mergedSig[:], merged)
- p.aggregatesInCommittee.Add(key, &solid.Attestation{
+ mergedAttestation := &solid.Attestation{
AggregationBits: mergedAggrBits,
CommitteeBits: att.CommitteeBits,
Data: att.Data,
Signature: mergedSig,
- })
+ }
+ mergedAttestation.SetVersion(version)
+ p.aggregatesInCommittee.Add(key, mergedAttestation)
return nil
}
diff --git a/cl/aggregation/pool_test.go b/cl/aggregation/pool_test.go
index 11bb44bb612..951d0985373 100644
--- a/cl/aggregation/pool_test.go
+++ b/cl/aggregation/pool_test.go
@@ -113,6 +113,13 @@ func (t *PoolTestSuite) TestAddAttestationElectra() {
expectedCommitteeBits := solid.NewBitVector(64)
expectedCommitteeBits.SetBitAt(10, true)
expectedCommitteeBits.SetBitAt(10, true)
+ expected := &solid.Attestation{
+ AggregationBits: solid.BitlistFromBytes([]byte{0b00001101}, 2048*64),
+ Data: attData1,
+ Signature: mockAggrResult,
+ CommitteeBits: expectedCommitteeBits,
+ }
+ expected.SetVersion(clparams.ElectraVersion)
att1 := &solid.Attestation{
AggregationBits: solid.BitlistFromBytes([]byte{0b00001001}, 2048*64),
@@ -144,12 +151,7 @@ func (t *PoolTestSuite) TestAddAttestationElectra() {
t.mockEthClock.EXPECT().GetEpochAtSlot(gomock.Any()).Return(uint64(1)).Times(2)
t.mockEthClock.EXPECT().StateVersionByEpoch(gomock.Any()).Return(clparams.ElectraVersion).Times(2)
},
- expect: &solid.Attestation{
- AggregationBits: solid.BitlistFromBytes([]byte{0b00001101}, 2048*64),
- Data: attData1,
- Signature: mockAggrResult,
- CommitteeBits: expectedCommitteeBits,
- },
+ expect: expected,
},
}
@@ -169,6 +171,84 @@ func (t *PoolTestSuite) TestAddAttestationElectra() {
}
}
+func (t *PoolTestSuite) TestMergedGloasAttestationUsesProgressiveHash() {
+ committeeBits := solid.NewBitVector(64)
+ committeeBits.SetBitAt(10, true)
+ att1 := &solid.Attestation{
+ AggregationBits: solid.BitlistFromBytes([]byte{0b00001001}, 2048*64),
+ Data: attData1,
+ Signature: [96]byte{'a'},
+ CommitteeBits: committeeBits,
+ }
+ att2 := att1.Copy()
+ att2.AggregationBits = solid.BitlistFromBytes([]byte{0b00001100}, 2048*64)
+ att2.Signature = [96]byte{'b'}
+ t.mockEthClock.EXPECT().GetEpochAtSlot(gomock.Any()).Return(uint64(1)).Times(2)
+ t.mockEthClock.EXPECT().StateVersionByEpoch(gomock.Any()).Return(clparams.GloasVersion).Times(2)
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ pool := NewAggregationPool(ctx, t.mockBeaconConfig, nil, t.mockEthClock)
+
+ t.Require().NoError(pool.AddAttestation(att1))
+ t.Require().NoError(pool.AddAttestation(att2))
+ merged := pool.GetAggregatationByRootAndCommittee(attData1Root, 10)
+ got, err := merged.HashSSZ()
+ t.Require().NoError(err)
+ want, err := merged.HashSSZProgressive()
+ t.Require().NoError(err)
+ t.Equal(want, got)
+}
+
+func (t *PoolTestSuite) TestFirstGloasAttestationIsCopiedAndVersioned() {
+ committeeBits := solid.NewBitVector(64)
+ committeeBits.SetBitAt(10, true)
+ att := &solid.Attestation{
+ AggregationBits: solid.BitlistFromBytes([]byte{0b00001001}, 2048*64),
+ Data: attData1,
+ Signature: [96]byte{'a'},
+ CommitteeBits: committeeBits,
+ }
+ callerRoot, err := att.HashSSZ()
+ t.Require().NoError(err)
+ t.mockEthClock.EXPECT().GetEpochAtSlot(gomock.Any()).Return(uint64(1))
+ t.mockEthClock.EXPECT().StateVersionByEpoch(gomock.Any()).Return(clparams.GloasVersion)
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ pool := NewAggregationPool(ctx, t.mockBeaconConfig, nil, t.mockEthClock)
+
+ t.Require().NoError(pool.AddAttestation(att))
+ afterAddRoot, err := att.HashSSZ()
+ t.Require().NoError(err)
+ t.Equal(callerRoot, afterAddRoot)
+ cached := pool.GetAggregatationByRootAndCommittee(attData1Root, 10)
+ t.NotSame(att, cached)
+ att.SetVersion(clparams.ElectraVersion)
+ got, err := cached.HashSSZ()
+ t.Require().NoError(err)
+ want, err := cached.HashSSZProgressive()
+ t.Require().NoError(err)
+ t.Equal(want, got)
+}
+
+func (t *PoolTestSuite) TestElectraAggregationErrorIsReturned() {
+ committeeBits := solid.NewBitVector(64)
+ committeeBits.SetBitAt(10, true)
+ att := &solid.Attestation{
+ AggregationBits: solid.BitlistFromBytes([]byte{0b00001001}, 2048*64),
+ Data: attData1,
+ Signature: [96]byte{'a'},
+ CommitteeBits: committeeBits,
+ }
+ t.mockEthClock.EXPECT().GetEpochAtSlot(gomock.Any()).Return(uint64(1)).Times(2)
+ t.mockEthClock.EXPECT().StateVersionByEpoch(gomock.Any()).Return(clparams.ElectraVersion).Times(2)
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ pool := NewAggregationPool(ctx, t.mockBeaconConfig, nil, t.mockEthClock)
+
+ t.Require().NoError(pool.AddAttestation(att))
+ t.ErrorIs(pool.AddAttestation(att.Copy()), ErrIsSuperset)
+}
+
func (t *PoolTestSuite) TestAddAttestation() {
testcases := []struct {
name string
@@ -236,7 +316,9 @@ func (t *PoolTestSuite) TestAddAttestation() {
pool.AddAttestation(tc.atts[i])
}
att := pool.GetAggregatationByRoot(tc.hashRoot)
- t.Equal(tc.expect, att, tc.name)
+ expected := tc.expect.Copy()
+ expected.SetVersion(clparams.DenebVersion)
+ t.Equal(expected, att, tc.name)
}
}
diff --git a/cl/beacon/handler/block_production.go b/cl/beacon/handler/block_production.go
index 88e105e465f..21f88759ab7 100644
--- a/cl/beacon/handler/block_production.go
+++ b/cl/beacon/handler/block_production.go
@@ -1552,6 +1552,11 @@ func (a *ApiHandler) publishBlindedBlocks(w http.ResponseWriter, r *http.Request
if err := validateBlindedBlockRequest(signedBlindedBlock, version); err != nil {
return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, err)
}
+ if err := solid.RangeErr(signedBlindedBlock.Block.Body.Attestations, func(_ int, attestation *solid.Attestation, _ int) error {
+ return attestation.ValidateForConfig(a.beaconChainCfg, version)
+ }); err != nil {
+ return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, err)
+ }
if isJSON {
signedBlindedBlock.Block.SetVersion(version)
}
diff --git a/cl/beacon/handler/pool.go b/cl/beacon/handler/pool.go
index d9781105fe1..e40fd010ee8 100644
--- a/cl/beacon/handler/pool.go
+++ b/cl/beacon/handler/pool.go
@@ -346,7 +346,7 @@ func (a *ApiHandler) PostEthV1BeaconPoolBlsToExecutionChanges(w http.ResponseWri
return
}
failures := []poolingFailure{}
- for _, v := range req {
+ for idx, v := range req {
encodedSSZ, err := v.EncodeSSZ(nil)
if err != nil {
beaconhttp.NewEndpointError(http.StatusInternalServerError, err).WriteTo(w)
@@ -356,7 +356,7 @@ func (a *ApiHandler) PostEthV1BeaconPoolBlsToExecutionChanges(w http.ResponseWri
if err := a.blsToExecutionChangeService.ProcessMessage(r.Context(), nil, &services.SignedBLSToExecutionChangeForGossip{
SignedBLSToExecutionChange: v,
}); err != nil && !errors.Is(err, services.ErrIgnore) {
- failures = append(failures, poolingFailure{Index: len(failures), Message: err.Error()})
+ failures = append(failures, poolingFailure{Index: idx, Message: err.Error()})
continue
}
@@ -383,7 +383,22 @@ func (a *ApiHandler) PostEthV1ValidatorAggregatesAndProof(w http.ResponseWriter,
}
failures := []poolingFailure{}
- for _, v := range req {
+ for idx, v := range req {
+ if v == nil || v.Message == nil || v.Message.Aggregate == nil || v.Message.Aggregate.Data == nil || v.Message.Aggregate.AggregationBits == nil {
+ failures = append(failures, poolingFailure{Index: idx, Message: "invalid aggregate and proof"})
+ continue
+ }
+ epoch := v.Message.Aggregate.Data.Slot / a.beaconChainCfg.SlotsPerEpoch
+ version := a.beaconChainCfg.GetCurrentStateVersion(epoch)
+ if version >= clparams.ElectraVersion && v.Message.Aggregate.CommitteeBits == nil {
+ failures = append(failures, poolingFailure{Index: idx, Message: "invalid aggregate and proof: missing committee bits"})
+ continue
+ }
+ v.SetVersion(version)
+ if err := v.Message.Aggregate.ValidateForConfig(a.beaconChainCfg, version); err != nil {
+ failures = append(failures, poolingFailure{Index: idx, Message: err.Error()})
+ continue
+ }
encodedSSZ, err := v.EncodeSSZ(nil)
if err != nil {
beaconhttp.NewEndpointError(http.StatusInternalServerError, err).WriteTo(w)
@@ -399,7 +414,7 @@ func (a *ApiHandler) PostEthV1ValidatorAggregatesAndProof(w http.ResponseWriter,
log.Debug("[Beacon REST] aggregate ignored", "err", err, "slot", v.Message.Aggregate.Data.Slot)
} else if err != nil {
log.Warn("[Beacon REST] failed to process aggregate", "err", err)
- failures = append(failures, poolingFailure{Index: len(failures), Message: err.Error()})
+ failures = append(failures, poolingFailure{Index: idx, Message: err.Error()})
continue
}
if err := a.gossipManager.Publish(r.Context(), gossip.TopicNameBeaconAggregateAndProof, encodedSSZ); err != nil {
diff --git a/cl/beacon/handler/pool_test.go b/cl/beacon/handler/pool_test.go
index 4024808132e..c7ebec85306 100644
--- a/cl/beacon/handler/pool_test.go
+++ b/cl/beacon/handler/pool_test.go
@@ -37,16 +37,9 @@ import (
)
func TestPoolAttesterSlashings(t *testing.T) {
- attesterSlashing := &cltypes.AttesterSlashing{
- Attestation_1: &cltypes.IndexedAttestation{
- AttestingIndices: solid.NewRawUint64List(2048, []uint64{2, 3, 4, 5, 6}),
- Data: &solid.AttestationData{},
- },
- Attestation_2: &cltypes.IndexedAttestation{
- AttestingIndices: solid.NewRawUint64List(2048, []uint64{2, 3, 4, 1, 6}),
- Data: &solid.AttestationData{},
- },
- }
+ attesterSlashing := cltypes.NewAttesterSlashing(clparams.DenebVersion)
+ attesterSlashing.Attestation_1.AttestingIndices = solid.NewRawUint64List(2048, []uint64{2, 3, 4, 5, 6})
+ attesterSlashing.Attestation_2.AttestingIndices = solid.NewRawUint64List(2048, []uint64{2, 3, 4, 1, 6})
// find server
_, _, _, _, _, handler, _, syncedDataMgr, _, _ := setupTestingHandler(t, clparams.Phase0Version, log.Root(), false)
mockBeaconState := &state.CachingBeaconState{BeaconState: raw.New(&clparams.BeaconChainConfig{})}
@@ -305,6 +298,41 @@ func TestPoolAggregatesAndProofs(t *testing.T) {
require.Equal(t, msg[1].Message.Aggregate, out.Data[1])
}
+func TestPoolAggregatesAndProofsReportsRequestIndex(t *testing.T) {
+ msg := []*cltypes.SignedAggregateAndProof{
+ {
+ Message: &cltypes.AggregateAndProof{
+ Aggregate: &solid.Attestation{
+ AggregationBits: solid.BitlistFromBytes([]byte{1, 2}, 2048),
+ Data: &solid.AttestationData{},
+ Signature: common.Bytes96{3, 45, 6},
+ },
+ },
+ Signature: common.Bytes96{2},
+ },
+ nil,
+ }
+ _, _, _, _, _, handler, _, syncedDataMgr, _, _ := setupTestingHandler(t, clparams.Phase0Version, log.Root(), false)
+ mockBeaconState := &state.CachingBeaconState{BeaconState: raw.New(&clparams.BeaconChainConfig{})}
+ mockBeaconState.SetVersion(clparams.DenebVersion)
+ syncedDataMgr.(*sync_mock_services.MockSyncedData).EXPECT().ViewHeadState(gomock.Any()).DoAndReturn(func(vhsf synced_data.ViewHeadStateFn) error {
+ vhsf(mockBeaconState)
+ return nil
+ }).AnyTimes()
+ server := httptest.NewServer(handler.mux)
+ defer server.Close()
+ requestBody, err := json.Marshal(msg)
+ require.NoError(t, err)
+
+ resp, err := server.Client().Post(server.URL+"/eth/v1/validator/aggregate_and_proofs", "application/json", bytes.NewBuffer(requestBody))
+ require.NoError(t, err)
+ defer resp.Body.Close()
+ require.Equal(t, 400, resp.StatusCode)
+ var response poolingError
+ require.NoError(t, json.NewDecoder(resp.Body).Decode(&response))
+ require.Equal(t, []poolingFailure{{Index: 1, Message: "invalid aggregate and proof"}}, response.Failures)
+}
+
func TestPoolSyncCommittees(t *testing.T) {
msgs := []*cltypes.SyncCommitteeMessage{
{
diff --git a/cl/clparams/config.go b/cl/clparams/config.go
index c426207a4ad..6825a1d492c 100644
--- a/cl/clparams/config.go
+++ b/cl/clparams/config.go
@@ -1096,7 +1096,7 @@ var MainnetBeaconConfig BeaconChainConfig = BeaconChainConfig{
MaxDepositRequestsPerPayload: 8192,
MaxWithdrawalRequestsPerPayload: 16,
MaxConsolidationRequestsPerPayload: 2,
- MaxBuilderDepositRequestsPerPayload: 256,
+ MaxBuilderDepositRequestsPerPayload: 64,
MaxBuilderExitRequestsPerPayload: 16,
MinSlashingPenaltyQuotientElectra: 4096,
WhistleBlowerRewardQuotientElectra: 4096,
@@ -1130,14 +1130,14 @@ var MainnetBeaconConfig BeaconChainConfig = BeaconChainConfig{
ChurnLimitQuotientGloas: 1 << 15,
ConsolidationChurnLimitQuotient: 1 << 16,
MaxPerEpochActivationChurnLimitGloas: 256_000_000_000,
- BuilderWithdrawalPrefix: 0x03,
+ BuilderWithdrawalPrefix: 0xB0,
PayloadDueBps: 7500,
PtcSize: 512,
MaxPayloadAttestations: 4,
BuilderRegistryLimit: 1 << 40,
BuilderPendingWithdrawalsLimit: 1 << 20,
MaxBuildersPerWithdrawalsSweep: 1 << 14,
- MinBuilderWithdrawabilityDelay: 8192,
+ MinBuilderWithdrawabilityDelay: 64,
}
func mainnetConfig() BeaconChainConfig {
diff --git a/cl/clparams/devgenesis/devgenesis.go b/cl/clparams/devgenesis/devgenesis.go
index 34b0be33d8a..7cb06070fe6 100644
--- a/cl/clparams/devgenesis/devgenesis.go
+++ b/cl/clparams/devgenesis/devgenesis.go
@@ -191,10 +191,16 @@ func BuildGenesisState(
}
// Compute genesis validators root.
- validatorsRoot, err := beaconState.Validators().HashSSZ()
+ var validatorsRoot [32]byte
+ if version >= clparams.GloasVersion {
+ validatorsRoot, err = beaconState.Validators().HashSSZProgressive()
+ } else {
+ validatorsRoot, err = beaconState.Validators().HashSSZ()
+ }
if err != nil {
return nil, nil, fmt.Errorf("hash validators: %w", err)
}
+ beaconState.Validators().SetProgressiveHashing(version >= clparams.GloasVersion)
beaconState.SetGenesisValidatorsRoot(common.Hash(validatorsRoot))
// Initialize RANDAO mixes with the genesis validators root.
@@ -202,19 +208,38 @@ func BuildGenesisState(
beaconState.SetRandaoMixAt(int(i), common.Hash(validatorsRoot))
}
- // Set the latest execution payload header referencing the EL genesis block.
- execHeader := cltypes.NewEth1Header(version)
- execHeader.BlockHash = elGenesisHash
- beaconState.SetLatestExecutionPayloadHeader(execHeader)
+ var genesisBid *cltypes.ExecutionPayloadBid
+ if version >= clparams.GloasVersion {
+ emptyRequests := cltypes.NewExecutionRequestsWithVersion(cfg, version)
+ emptyRequestsRoot, err := emptyRequests.HashSSZ()
+ if err != nil {
+ return nil, nil, fmt.Errorf("hash empty execution requests: %w", err)
+ }
+ genesisBid = &cltypes.ExecutionPayloadBid{
+ ParentBlockHash: elGenesisHash,
+ BlobKzgCommitments: *solid.NewStaticProgressiveListSSZ[*cltypes.KZGCommitment](int(cfg.MaxBlobCommittmentsPerBlock), 48),
+ ExecutionRequestsRoot: emptyRequestsRoot,
+ }
+ beaconState.SetLatestExecutionPayloadBid(genesisBid)
+ beaconState.SetLatestBlockHash(elGenesisHash)
+ } else {
+ execHeader := cltypes.NewEth1Header(version)
+ execHeader.BlockHash = elGenesisHash
+ beaconState.SetLatestExecutionPayloadHeader(execHeader)
+ }
// Set latest block header. The body root for genesis is the hash of
// an empty BeaconBlockBody at the genesis version.
genesisBody := cltypes.NewBeaconBody(cfg, version)
- // Ensure the execution payload has all required sub-fields initialized.
- genesisBody.ExecutionPayload.Extra = solid.NewExtraData()
- genesisBody.ExecutionPayload.Transactions = &solid.TransactionsSSZ{}
- if version >= clparams.CapellaVersion {
- genesisBody.ExecutionPayload.Withdrawals = solid.NewStaticListSSZ[*cltypes.Withdrawal](int(cfg.MaxWithdrawalsPerPayload), 44)
+ if genesisBid != nil {
+ genesisBody.SignedExecutionPayloadBid.Message = genesisBid.Copy()
+ }
+ if genesisBody.ExecutionPayload != nil {
+ genesisBody.ExecutionPayload.Extra = solid.NewExtraData()
+ genesisBody.ExecutionPayload.Transactions = &solid.TransactionsSSZ{}
+ if version >= clparams.CapellaVersion {
+ genesisBody.ExecutionPayload.Withdrawals = solid.NewStaticListSSZ[*cltypes.Withdrawal](int(cfg.MaxWithdrawalsPerPayload), 44)
+ }
}
if version >= clparams.AltairVersion {
genesisBody.SyncAggregate = cltypes.NewSyncAggregateWithSize(int(cfg.SyncCommitteeSize) / 8)
diff --git a/cl/clparams/devgenesis/devgenesis_test.go b/cl/clparams/devgenesis/devgenesis_test.go
index e88fda7cb06..b6f37f30913 100644
--- a/cl/clparams/devgenesis/devgenesis_test.go
+++ b/cl/clparams/devgenesis/devgenesis_test.go
@@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/erigontech/erigon/cl/clparams"
+ "github.com/erigontech/erigon/cl/cltypes"
"github.com/erigontech/erigon/common"
)
@@ -67,6 +68,43 @@ func TestBuildGenesisState_Deterministic(t *testing.T) {
require.NotEqual(t, r1, r3, "different seed should produce different state root")
}
+func TestBuildGenesisState_GloasValidatorsRoot(t *testing.T) {
+ cfg := clparams.MainnetBeaconConfig
+ cfg.AltairForkEpoch = 0
+ cfg.BellatrixForkEpoch = 0
+ cfg.CapellaForkEpoch = 0
+ cfg.DenebForkEpoch = 0
+ cfg.ElectraForkEpoch = 0
+ cfg.FuluForkEpoch = 0
+ cfg.GloasForkEpoch = 0
+
+ elHash := common.HexToHash("0x1234")
+ genesisState, _, err := BuildGenesisState("gloas-seed", 16, &cfg, 1000, elHash)
+ require.NoError(t, err)
+
+ progressiveRoot, err := genesisState.Validators().HashSSZProgressive()
+ require.NoError(t, err)
+ legacyRoot, err := genesisState.Validators().HashSSZ()
+ require.NoError(t, err)
+ require.Equal(t, common.Hash(progressiveRoot), genesisState.GenesisValidatorsRoot())
+ require.NotEqual(t, legacyRoot, progressiveRoot)
+ require.Equal(t, elHash, genesisState.GetLatestBlockHash())
+ emptyRequests := cltypes.NewExecutionRequestsWithVersion(&cfg, clparams.GloasVersion)
+ emptyRequestsRoot, err := emptyRequests.HashSSZ()
+ require.NoError(t, err)
+ bid := genesisState.GetLatestExecutionPayloadBid()
+ require.NotNil(t, bid)
+ require.Equal(t, common.Hash{}, bid.BlockHash)
+ require.Equal(t, elHash, bid.ParentBlockHash)
+ require.Equal(t, common.Hash(emptyRequestsRoot), bid.ExecutionRequestsRoot)
+
+ body := cltypes.NewBeaconBody(&cfg, clparams.GloasVersion)
+ body.SignedExecutionPayloadBid.Message = bid.Copy()
+ bodyRoot, err := body.HashSSZ()
+ require.NoError(t, err)
+ require.Equal(t, common.Hash(bodyRoot), genesisState.LatestBlockHeader().BodyRoot)
+}
+
func TestDeriveSignerKey(t *testing.T) {
key1, addr1, err := DeriveSignerKey("test")
require.NoError(t, err)
diff --git a/cl/cltypes/aggregate.go b/cl/cltypes/aggregate.go
index 12dc04f0ad9..e938d0f5192 100644
--- a/cl/cltypes/aggregate.go
+++ b/cl/cltypes/aggregate.go
@@ -17,9 +17,11 @@
package cltypes
import (
+ "bytes"
"encoding/hex"
"encoding/json"
+ "github.com/erigontech/erigon/cl/clparams"
"github.com/erigontech/erigon/cl/cltypes/solid"
"github.com/erigontech/erigon/cl/merkle_tree"
ssz2 "github.com/erigontech/erigon/cl/ssz"
@@ -35,6 +37,47 @@ type AggregateAndProof struct {
AggregatorIndex uint64 `json:"aggregator_index,string"`
Aggregate *solid.Attestation `json:"aggregate"`
SelectionProof common.Bytes96 `json:"selection_proof"`
+ version clparams.StateVersion
+}
+
+func (a *AggregateAndProof) SetVersion(version clparams.StateVersion) {
+ a.version = version
+ if a.Aggregate != nil {
+ a.Aggregate.SetVersion(version)
+ }
+}
+
+func (a *AggregateAndProof) UnmarshalJSON(data []byte) error {
+ decoded := struct {
+ AggregatorIndex uint64 `json:"aggregator_index,string"`
+ SelectionProof common.Bytes96 `json:"selection_proof"`
+ }{AggregatorIndex: a.AggregatorIndex, SelectionProof: a.SelectionProof}
+ if err := json.Unmarshal(data, &decoded); err != nil {
+ return err
+ }
+ fields := make(map[string]json.RawMessage)
+ if err := json.Unmarshal(data, &fields); err != nil {
+ return err
+ }
+ aggregate := a.Aggregate
+ if raw, ok := fields["aggregate"]; ok {
+ if bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
+ aggregate = nil
+ } else {
+ aggregate = &solid.Attestation{}
+ aggregate.SetVersion(a.version)
+ if err := json.Unmarshal(raw, aggregate); err != nil {
+ return err
+ }
+ }
+ }
+ a.AggregatorIndex = decoded.AggregatorIndex
+ a.Aggregate = aggregate
+ a.SelectionProof = decoded.SelectionProof
+ if a.Aggregate != nil {
+ a.Aggregate.SetVersion(a.version)
+ }
+ return nil
}
func (a *AggregateAndProof) EncodeSSZ(dst []byte) ([]byte, error) {
@@ -46,6 +89,7 @@ func (a *AggregateAndProof) Static() bool {
}
func (a *AggregateAndProof) DecodeSSZ(buf []byte, version int) error {
+ a.version = clparams.StateVersion(version)
a.Aggregate = new(solid.Attestation)
return ssz2.UnmarshalSSZ(buf, version, &a.AggregatorIndex, a.Aggregate, a.SelectionProof[:])
}
@@ -61,6 +105,45 @@ func (a *AggregateAndProof) HashSSZ() ([32]byte, error) {
type SignedAggregateAndProof struct {
Message *AggregateAndProof `json:"message"`
Signature common.Bytes96 `json:"signature"`
+ version clparams.StateVersion
+}
+
+func (a *SignedAggregateAndProof) SetVersion(version clparams.StateVersion) {
+ a.version = version
+ if a.Message != nil {
+ a.Message.SetVersion(version)
+ }
+}
+
+func (a *SignedAggregateAndProof) UnmarshalJSON(data []byte) error {
+ decoded := struct {
+ Signature common.Bytes96 `json:"signature"`
+ }{Signature: a.Signature}
+ if err := json.Unmarshal(data, &decoded); err != nil {
+ return err
+ }
+ fields := make(map[string]json.RawMessage)
+ if err := json.Unmarshal(data, &fields); err != nil {
+ return err
+ }
+ message := a.Message
+ if raw, ok := fields["message"]; ok {
+ if bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
+ message = nil
+ } else {
+ message = &AggregateAndProof{}
+ message.SetVersion(a.version)
+ if err := json.Unmarshal(raw, message); err != nil {
+ return err
+ }
+ }
+ }
+ a.Message = message
+ a.Signature = decoded.Signature
+ if a.Message != nil {
+ a.Message.SetVersion(a.version)
+ }
+ return nil
}
func (a *SignedAggregateAndProof) EncodeSSZ(dst []byte) ([]byte, error) {
@@ -68,6 +151,7 @@ func (a *SignedAggregateAndProof) EncodeSSZ(dst []byte) ([]byte, error) {
}
func (a *SignedAggregateAndProof) DecodeSSZ(buf []byte, version int) error {
+ a.version = clparams.StateVersion(version)
a.Message = new(AggregateAndProof)
return ssz2.UnmarshalSSZ(buf, version, a.Message, a.Signature[:])
}
diff --git a/cl/cltypes/aggregate_gloas_test.go b/cl/cltypes/aggregate_gloas_test.go
new file mode 100644
index 00000000000..7b7acfd087d
--- /dev/null
+++ b/cl/cltypes/aggregate_gloas_test.go
@@ -0,0 +1,83 @@
+package cltypes
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/erigontech/erigon/cl/clparams"
+ "github.com/erigontech/erigon/cl/cltypes/solid"
+ "github.com/stretchr/testify/require"
+)
+
+func TestAggregateAndProofHashDoesNotChangeAggregateVersion(t *testing.T) {
+ aggregate := &solid.Attestation{
+ AggregationBits: solid.BitlistFromBytes([]byte{0x03}, 2048),
+ Data: &solid.AttestationData{},
+ CommitteeBits: solid.NewBitVector(64),
+ }
+ aggregate.SetVersion(clparams.GloasVersion)
+ want, err := aggregate.HashSSZ()
+ require.NoError(t, err)
+
+ _, err = (&AggregateAndProof{Aggregate: aggregate}).HashSSZ()
+ require.NoError(t, err)
+ got, err := aggregate.HashSSZ()
+ require.NoError(t, err)
+ require.Equal(t, want, got)
+}
+
+func TestAggregateAndProofJSONPreservesGloasVersion(t *testing.T) {
+ aggregate := &solid.Attestation{
+ AggregationBits: solid.BitlistFromBytes([]byte{0x03}, 2048),
+ Data: &solid.AttestationData{},
+ CommitteeBits: solid.NewBitVector(64),
+ }
+ message := &AggregateAndProof{Aggregate: aggregate}
+ message.SetVersion(clparams.GloasVersion)
+ signed := &SignedAggregateAndProof{Message: message}
+ signed.SetVersion(clparams.GloasVersion)
+
+ for name, original := range map[string]interface {
+ HashSSZ() ([32]byte, error)
+ }{"message": message, "signed": signed} {
+ t.Run(name, func(t *testing.T) {
+ want, err := original.HashSSZ()
+ require.NoError(t, err)
+ encoded, err := json.Marshal(original)
+ require.NoError(t, err)
+
+ var decoded interface {
+ HashSSZ() ([32]byte, error)
+ }
+ if name == "message" {
+ value := &AggregateAndProof{}
+ value.SetVersion(clparams.GloasVersion)
+ decoded = value
+ } else {
+ value := &SignedAggregateAndProof{}
+ value.SetVersion(clparams.GloasVersion)
+ decoded = value
+ }
+ require.NoError(t, json.Unmarshal(encoded, decoded))
+ got, err := decoded.HashSSZ()
+ require.NoError(t, err)
+ require.Equal(t, want, got)
+ })
+ }
+}
+
+func TestAggregateAndProofJSONDoesNotCreateMissingNestedObjects(t *testing.T) {
+ for _, input := range []string{`{}`, `{"aggregate":null}`} {
+ message := &AggregateAndProof{}
+ message.SetVersion(clparams.GloasVersion)
+ require.NoError(t, json.Unmarshal([]byte(input), message))
+ require.Nil(t, message.Aggregate)
+ }
+
+ for _, input := range []string{`{}`, `{"message":null}`} {
+ signed := &SignedAggregateAndProof{}
+ signed.SetVersion(clparams.GloasVersion)
+ require.NoError(t, json.Unmarshal([]byte(input), signed))
+ require.Nil(t, signed.Message)
+ }
+}
diff --git a/cl/cltypes/beacon_block.go b/cl/cltypes/beacon_block.go
index ee91c1fd3b7..04f190e18d8 100644
--- a/cl/cltypes/beacon_block.go
+++ b/cl/cltypes/beacon_block.go
@@ -264,22 +264,17 @@ type BeaconBody struct {
}
func NewBeaconBody(beaconCfg *clparams.BeaconChainConfig, version clparams.StateVersion) *BeaconBody {
- maxAttSlashing := MaxAttesterSlashings
- maxAttestation := MaxAttestations
- if version >= clparams.ElectraVersion {
- maxAttSlashing = int(beaconCfg.MaxAttesterSlashingsElectra)
- maxAttestation = int(beaconCfg.MaxAttestationsElectra)
- }
+ limits := beaconBodyLimitsForConfig(beaconCfg, version)
body := &BeaconBody{
beaconCfg: beaconCfg,
Eth1Data: &Eth1Data{},
- ProposerSlashings: solid.NewStaticListSSZ[*ProposerSlashing](MaxProposerSlashings, 416),
- AttesterSlashings: solid.NewDynamicListSSZ[*AttesterSlashing](maxAttSlashing),
- Attestations: solid.NewDynamicListSSZ[*solid.Attestation](maxAttestation),
- Deposits: solid.NewStaticListSSZ[*Deposit](MaxDeposits, 1240),
- VoluntaryExits: solid.NewStaticListSSZ[*SignedVoluntaryExit](MaxVoluntaryExits, 112),
- ExecutionChanges: solid.NewStaticListSSZ[*SignedBLSToExecutionChange](MaxExecutionChanges, 172),
+ ProposerSlashings: solid.NewStaticListSSZ[*ProposerSlashing](limits.proposerSlashings, 416),
+ AttesterSlashings: solid.NewDynamicListSSZ[*AttesterSlashing](limits.attesterSlashings),
+ Attestations: solid.NewDynamicListSSZ[*solid.Attestation](limits.attestations),
+ Deposits: solid.NewStaticListSSZ[*Deposit](limits.deposits, 1240),
+ VoluntaryExits: solid.NewStaticListSSZ[*SignedVoluntaryExit](limits.voluntaryExits, 112),
+ ExecutionChanges: solid.NewStaticListSSZ[*SignedBLSToExecutionChange](limits.executionChanges, 172),
Version: version,
}
@@ -287,43 +282,111 @@ func NewBeaconBody(beaconCfg *clparams.BeaconChainConfig, version clparams.State
if version < clparams.GloasVersion {
// Pre-GLOAS: ExecutionPayload, BlobKzgCommitments in BeaconBody
body.ExecutionPayload = NewEth1Block(version, beaconCfg)
- maxBlobCommitments := MaxBlobsCommittmentsPerBlock
- if beaconCfg != nil && beaconCfg.MaxBlobCommittmentsPerBlock > 0 {
- maxBlobCommitments = int(beaconCfg.MaxBlobCommittmentsPerBlock)
- }
- body.BlobKzgCommitments = solid.NewStaticListSSZ[*KZGCommitment](maxBlobCommitments, 48)
+ body.BlobKzgCommitments = solid.NewStaticListSSZ[*KZGCommitment](maxBlobCommitmentsForConfig(beaconCfg), 48)
if version >= clparams.ElectraVersion {
body.ExecutionRequests = NewExecutionRequestsWithVersion(beaconCfg, version)
}
} else {
+ body.resetGloasProgressiveLists()
// GLOAS: SignedExecutionPayloadBid and PayloadAttestations replace above
- maxBlobCommitmentsGloas := MaxBlobsCommittmentsPerBlock
- if beaconCfg != nil && beaconCfg.MaxBlobCommittmentsPerBlock > 0 {
- maxBlobCommitmentsGloas = int(beaconCfg.MaxBlobCommittmentsPerBlock)
- }
body.SignedExecutionPayloadBid = &SignedExecutionPayloadBid{
Message: &ExecutionPayloadBid{
- BlobKzgCommitments: *solid.NewStaticListSSZ[*KZGCommitment](maxBlobCommitmentsGloas, 48),
+ BlobKzgCommitments: *solid.NewStaticProgressiveListSSZ[*KZGCommitment](maxBlobCommitmentsForConfig(beaconCfg), 48),
},
}
- body.PayloadAttestations = solid.NewStaticListSSZ[*PayloadAttestation](int(beaconCfg.MaxPayloadAttestations), PayloadAttestationSSZSizeWithPtcSize(beaconCfg.PtcSize))
body.ParentExecutionRequests = NewExecutionRequestsWithVersion(beaconCfg, version)
}
return body
}
+func maxBlobCommitmentsForConfig(beaconCfg *clparams.BeaconChainConfig) int {
+ if beaconCfg != nil && beaconCfg.MaxBlobCommittmentsPerBlock > 0 {
+ return saturatingPositiveInt(beaconCfg.MaxBlobCommittmentsPerBlock, MaxBlobsCommittmentsPerBlock)
+ }
+ return MaxBlobsCommittmentsPerBlock
+}
+
+func maxPayloadAttestationsForConfig(beaconCfg *clparams.BeaconChainConfig) int {
+ fallback := int(clparams.MainnetBeaconConfig.MaxPayloadAttestations)
+ if globalCfg := clparams.GetBeaconConfig(); globalCfg != nil {
+ fallback = saturatingPositiveInt(globalCfg.MaxPayloadAttestations, fallback)
+ }
+ if beaconCfg != nil && beaconCfg.MaxPayloadAttestations > 0 {
+ return saturatingPositiveInt(beaconCfg.MaxPayloadAttestations, fallback)
+ }
+ return fallback
+}
+
+type beaconBodyListLimits struct {
+ proposerSlashings int
+ attesterSlashings int
+ attestations int
+ deposits int
+ voluntaryExits int
+ executionChanges int
+}
+
+func beaconBodyLimitsForConfig(beaconCfg *clparams.BeaconChainConfig, version clparams.StateVersion) beaconBodyListLimits {
+ limits := beaconBodyListLimits{
+ proposerSlashings: MaxProposerSlashings,
+ attesterSlashings: MaxAttesterSlashings,
+ attestations: MaxAttestations,
+ deposits: MaxDeposits,
+ voluntaryExits: MaxVoluntaryExits,
+ executionChanges: MaxExecutionChanges,
+ }
+ if version.AfterOrEqual(clparams.ElectraVersion) {
+ limits.attesterSlashings = MaxAttesterSlashingsElectra
+ limits.attestations = MaxAttestationsElectra
+ }
+ if beaconCfg == nil {
+ return limits
+ }
+ limits.proposerSlashings = saturatingPositiveInt(beaconCfg.MaxProposerSlashings, limits.proposerSlashings)
+ limits.deposits = saturatingPositiveInt(beaconCfg.MaxDeposits, limits.deposits)
+ limits.voluntaryExits = saturatingPositiveInt(beaconCfg.MaxVoluntaryExits, limits.voluntaryExits)
+ limits.executionChanges = saturatingPositiveInt(beaconCfg.MaxBlsToExecutionChanges, limits.executionChanges)
+ if version.AfterOrEqual(clparams.ElectraVersion) {
+ limits.attesterSlashings = saturatingPositiveInt(beaconCfg.MaxAttesterSlashingsElectra, limits.attesterSlashings)
+ limits.attestations = saturatingPositiveInt(beaconCfg.MaxAttestationsElectra, limits.attestations)
+ } else {
+ limits.attesterSlashings = saturatingPositiveInt(beaconCfg.MaxAttesterSlashings, limits.attesterSlashings)
+ limits.attestations = saturatingPositiveInt(beaconCfg.MaxAttestations, limits.attestations)
+ }
+ return limits
+}
+
+func saturatingPositiveInt(value uint64, fallback int) int {
+ if value == 0 {
+ return fallback
+ }
+ maxInt := int(^uint(0) >> 1)
+ if value > uint64(maxInt) {
+ return maxInt
+ }
+ return int(value)
+}
+
+func (b *BeaconBody) resetGloasProgressiveLists() {
+ limits := beaconBodyLimitsForConfig(b.beaconCfg, b.Version)
+ b.ProposerSlashings = solid.NewStaticProgressiveListSSZ[*ProposerSlashing](limits.proposerSlashings, 416)
+ b.AttesterSlashings = solid.NewDynamicProgressiveListSSZ[*AttesterSlashing](limits.attesterSlashings)
+ b.Attestations = solid.NewDynamicProgressiveListSSZ[*solid.Attestation](limits.attestations)
+ b.Deposits = solid.NewStaticProgressiveListSSZ[*Deposit](limits.deposits, 1240)
+ b.VoluntaryExits = solid.NewStaticProgressiveListSSZ[*SignedVoluntaryExit](limits.voluntaryExits, 112)
+ b.ExecutionChanges = solid.NewStaticProgressiveListSSZ[*SignedBLSToExecutionChange](limits.executionChanges, 172)
+ ptcSize := clparams.MaxPtcSize
+ if b.beaconCfg != nil && b.beaconCfg.PtcSize > 0 {
+ ptcSize = b.beaconCfg.PtcSize
+ }
+ b.PayloadAttestations = solid.NewStaticProgressiveListSSZ[*PayloadAttestation](maxPayloadAttestationsForConfig(b.beaconCfg), PayloadAttestationSSZSizeWithPtcSize(ptcSize))
+}
+
// ensureNilFields initializes any nil fields that must be present for SSZ encoding,
// hashing, and size computation. It is idempotent and safe to call multiple times.
func (b *BeaconBody) ensureNilFields() {
- var (
- maxAttSlashing = MaxAttesterSlashings
- maxAttestation = MaxAttestations
- )
- if b.Version.AfterOrEqual(clparams.ElectraVersion) {
- maxAttSlashing = MaxAttesterSlashingsElectra
- maxAttestation = MaxAttestationsElectra
- }
+ limits := beaconBodyLimitsForConfig(b.beaconCfg, b.Version)
if b.Eth1Data == nil {
b.Eth1Data = &Eth1Data{}
}
@@ -335,34 +398,54 @@ func (b *BeaconBody) ensureNilFields() {
b.SyncAggregate = NewSyncAggregateWithSize(bitsSize)
}
if b.ProposerSlashings == nil {
- b.ProposerSlashings = solid.NewStaticListSSZ[*ProposerSlashing](MaxProposerSlashings, 416)
+ if b.Version >= clparams.GloasVersion {
+ b.ProposerSlashings = solid.NewStaticProgressiveListSSZ[*ProposerSlashing](limits.proposerSlashings, 416)
+ } else {
+ b.ProposerSlashings = solid.NewStaticListSSZ[*ProposerSlashing](limits.proposerSlashings, 416)
+ }
}
if b.AttesterSlashings == nil {
- b.AttesterSlashings = solid.NewDynamicListSSZ[*AttesterSlashing](maxAttSlashing)
+ if b.Version >= clparams.GloasVersion {
+ b.AttesterSlashings = solid.NewDynamicProgressiveListSSZ[*AttesterSlashing](limits.attesterSlashings)
+ } else {
+ b.AttesterSlashings = solid.NewDynamicListSSZ[*AttesterSlashing](limits.attesterSlashings)
+ }
}
if b.Attestations == nil {
- b.Attestations = solid.NewDynamicListSSZ[*solid.Attestation](maxAttestation)
+ if b.Version >= clparams.GloasVersion {
+ b.Attestations = solid.NewDynamicProgressiveListSSZ[*solid.Attestation](limits.attestations)
+ } else {
+ b.Attestations = solid.NewDynamicListSSZ[*solid.Attestation](limits.attestations)
+ }
}
if b.Deposits == nil {
- b.Deposits = solid.NewStaticListSSZ[*Deposit](MaxDeposits, 1240)
+ if b.Version >= clparams.GloasVersion {
+ b.Deposits = solid.NewStaticProgressiveListSSZ[*Deposit](limits.deposits, 1240)
+ } else {
+ b.Deposits = solid.NewStaticListSSZ[*Deposit](limits.deposits, 1240)
+ }
}
if b.VoluntaryExits == nil {
- b.VoluntaryExits = solid.NewStaticListSSZ[*SignedVoluntaryExit](MaxVoluntaryExits, 112)
+ if b.Version >= clparams.GloasVersion {
+ b.VoluntaryExits = solid.NewStaticProgressiveListSSZ[*SignedVoluntaryExit](limits.voluntaryExits, 112)
+ } else {
+ b.VoluntaryExits = solid.NewStaticListSSZ[*SignedVoluntaryExit](limits.voluntaryExits, 112)
+ }
}
// [Modified in Gloas:EIP7732] ExecutionPayload removed in GLOAS
if b.ExecutionPayload == nil && b.Version < clparams.GloasVersion {
b.ExecutionPayload = NewEth1Block(b.Version, b.beaconCfg)
}
if b.ExecutionChanges == nil {
- b.ExecutionChanges = solid.NewStaticListSSZ[*SignedBLSToExecutionChange](MaxExecutionChanges, 172)
+ if b.Version >= clparams.GloasVersion {
+ b.ExecutionChanges = solid.NewStaticProgressiveListSSZ[*SignedBLSToExecutionChange](limits.executionChanges, 172)
+ } else {
+ b.ExecutionChanges = solid.NewStaticListSSZ[*SignedBLSToExecutionChange](limits.executionChanges, 172)
+ }
}
// [Modified in Gloas:EIP7732] BlobKzgCommitments removed in GLOAS
if b.BlobKzgCommitments == nil && b.Version < clparams.GloasVersion {
- maxBlobCommitments := MaxBlobsCommittmentsPerBlock
- if b.beaconCfg != nil && b.beaconCfg.MaxBlobCommittmentsPerBlock > 0 {
- maxBlobCommitments = int(b.beaconCfg.MaxBlobCommittmentsPerBlock)
- }
- b.BlobKzgCommitments = solid.NewStaticListSSZ[*KZGCommitment](maxBlobCommitments, 48)
+ b.BlobKzgCommitments = solid.NewStaticListSSZ[*KZGCommitment](maxBlobCommitmentsForConfig(b.beaconCfg), 48)
}
// [New in Electra] ExecutionRequests — removed in GLOAS
if b.ExecutionRequests == nil && b.Version.AfterOrEqual(clparams.ElectraVersion) && b.Version < clparams.GloasVersion {
@@ -373,18 +456,18 @@ func (b *BeaconBody) ensureNilFields() {
// [New in Gloas:EIP7732] Initialize GLOAS fields if nil
if b.Version >= clparams.GloasVersion {
if b.SignedExecutionPayloadBid == nil {
- maxBlobCommitmentsGloas := MaxBlobsCommittmentsPerBlock
- if b.beaconCfg != nil && b.beaconCfg.MaxBlobCommittmentsPerBlock > 0 {
- maxBlobCommitmentsGloas = int(b.beaconCfg.MaxBlobCommittmentsPerBlock)
- }
b.SignedExecutionPayloadBid = &SignedExecutionPayloadBid{
Message: &ExecutionPayloadBid{
- BlobKzgCommitments: *solid.NewStaticListSSZ[*KZGCommitment](maxBlobCommitmentsGloas, 48),
+ BlobKzgCommitments: *solid.NewStaticProgressiveListSSZ[*KZGCommitment](maxBlobCommitmentsForConfig(b.beaconCfg), 48),
},
}
}
if b.PayloadAttestations == nil {
- b.PayloadAttestations = solid.NewStaticListSSZ[*PayloadAttestation](int(b.beaconCfg.MaxPayloadAttestations), PayloadAttestationSSZSizeWithPtcSize(b.beaconCfg.PtcSize))
+ ptcSize := clparams.MaxPtcSize
+ if b.beaconCfg != nil && b.beaconCfg.PtcSize > 0 {
+ ptcSize = b.beaconCfg.PtcSize
+ }
+ b.PayloadAttestations = solid.NewStaticProgressiveListSSZ[*PayloadAttestation](maxPayloadAttestationsForConfig(b.beaconCfg), PayloadAttestationSSZSizeWithPtcSize(ptcSize))
}
if b.ParentExecutionRequests == nil {
b.ParentExecutionRequests = NewExecutionRequestsWithVersion(b.beaconCfg, b.Version)
@@ -434,6 +517,9 @@ func (b *BeaconBody) EncodingSizeSSZ() (size int) {
func (b *BeaconBody) DecodeSSZ(buf []byte, version int) error {
b.Version = clparams.StateVersion(version)
+ if b.Version >= clparams.GloasVersion {
+ b.resetGloasProgressiveLists()
+ }
if len(buf) < b.EncodingSizeSSZ() {
return fmt.Errorf("[BeaconBody] err: %s", ssz.ErrLowBufferSize)
@@ -445,16 +531,11 @@ func (b *BeaconBody) DecodeSSZ(buf []byte, version int) error {
}
// [New in Gloas:EIP7732] Initialize GLOAS fields for decoding
if b.Version >= clparams.GloasVersion {
- maxBlobCommitments := MaxBlobsCommittmentsPerBlock
- if b.beaconCfg != nil && b.beaconCfg.MaxBlobCommittmentsPerBlock > 0 {
- maxBlobCommitments = int(b.beaconCfg.MaxBlobCommittmentsPerBlock)
- }
b.SignedExecutionPayloadBid = &SignedExecutionPayloadBid{
Message: &ExecutionPayloadBid{
- BlobKzgCommitments: *solid.NewStaticListSSZ[*KZGCommitment](maxBlobCommitments, 48),
+ BlobKzgCommitments: *solid.NewStaticProgressiveListSSZ[*KZGCommitment](maxBlobCommitmentsForConfig(b.beaconCfg), 48),
},
}
- b.PayloadAttestations = solid.NewStaticListSSZ[*PayloadAttestation](int(b.beaconCfg.MaxPayloadAttestations), PayloadAttestationSSZSizeWithPtcSize(b.beaconCfg.PtcSize))
b.ParentExecutionRequests = NewExecutionRequestsWithVersion(b.beaconCfg, b.Version)
}
if err := ssz2.UnmarshalSSZ(buf, version, b.getSchema(false)...); err != nil {
@@ -465,10 +546,11 @@ func (b *BeaconBody) DecodeSSZ(buf []byte, version int) error {
// DecodeDynamicList calls DecodeSSZ on each element with hardcoded mainnet limits.
// We must override those limits for minimal preset compatibility.
if b.beaconCfg != nil && b.Version.AfterOrEqual(clparams.ElectraVersion) {
- b.Attestations.Range(func(_ int, att *solid.Attestation, _ int) bool {
- att.SetBeaconConfig(b.beaconCfg)
- return true
- })
+ if err := solid.RangeErr(b.Attestations, func(_ int, att *solid.Attestation, _ int) error {
+ return att.ValidateForConfig(b.beaconCfg, b.Version)
+ }); err != nil {
+ return err
+ }
b.AttesterSlashings.Range(func(_ int, as *AttesterSlashing, _ int) bool {
as.SetVersionWithConfig(b.Version, b.beaconCfg)
return true
@@ -509,9 +591,70 @@ func (b *BeaconBody) Blinded() (*BlindedBeaconBody, error) {
func (b *BeaconBody) HashSSZ() ([32]byte, error) {
b.ensureNilFields()
+ if b.Version >= clparams.GloasVersion {
+ return b.hashSSZGloas()
+ }
return merkle_tree.HashTreeRoot(b.getSchema(false)...)
}
+func (b *BeaconBody) hashSSZGloas() ([32]byte, error) {
+ schema, err := b.gloasHashSchema()
+ if err != nil {
+ return [32]byte{}, err
+ }
+ return merkle_tree.ProgressiveContainerRootAll(schema...)
+}
+
+func (b *BeaconBody) gloasHashSchema() ([]any, error) {
+ proposerSlashings, err := b.ProposerSlashings.HashSSZProgressive(nil)
+ if err != nil {
+ return nil, err
+ }
+ attesterSlashings, err := b.AttesterSlashings.HashSSZProgressive(func(slashing *AttesterSlashing) ([32]byte, error) {
+ return slashing.HashSSZProgressive()
+ })
+ if err != nil {
+ return nil, err
+ }
+ attestations, err := b.Attestations.HashSSZProgressive(func(att *solid.Attestation) ([32]byte, error) {
+ return att.HashSSZProgressive()
+ })
+ if err != nil {
+ return nil, err
+ }
+ deposits, err := b.Deposits.HashSSZProgressive(nil)
+ if err != nil {
+ return nil, err
+ }
+ voluntaryExits, err := b.VoluntaryExits.HashSSZProgressive(nil)
+ if err != nil {
+ return nil, err
+ }
+ executionChanges, err := b.ExecutionChanges.HashSSZProgressive(nil)
+ if err != nil {
+ return nil, err
+ }
+ payloadAttestations, err := b.PayloadAttestations.HashSSZProgressive(nil)
+ if err != nil {
+ return nil, err
+ }
+ return []any{
+ b.RandaoReveal[:],
+ b.Eth1Data,
+ b.Graffiti[:],
+ proposerSlashings[:],
+ attesterSlashings[:],
+ attestations[:],
+ deposits[:],
+ voluntaryExits[:],
+ b.SyncAggregate,
+ executionChanges[:],
+ b.SignedExecutionPayloadBid,
+ payloadAttestations[:],
+ b.ParentExecutionRequests,
+ }, nil
+}
+
func (b *BeaconBody) getSchema(storage bool) []any {
s := []any{b.RandaoReveal[:], b.Eth1Data, b.Graffiti[:], b.ProposerSlashings, b.AttesterSlashings, b.Attestations, b.Deposits, b.VoluntaryExits}
if b.Version >= clparams.AltairVersion {
@@ -551,6 +694,30 @@ func (b *BeaconBody) ExecutionPayloadMerkleProof() ([][32]byte, error) {
return merkle_tree.MerkleProof(4, 9, b.getSchema(false)...)
}
+func (b *BeaconBody) ExecutionBlockHashMerkleProof() ([][32]byte, error) {
+ if b.Version < clparams.GloasVersion || b.SignedExecutionPayloadBid == nil || b.SignedExecutionPayloadBid.Message == nil {
+ return nil, errors.New("execution block hash merkle proof requires a GLOAS execution payload bid")
+ }
+ // The Gloas execution proof targets ParentBlockHash through bid, signed-bid, and body layers.
+ bidProof, err := b.SignedExecutionPayloadBid.Message.ParentBlockHashMerkleProof()
+ if err != nil {
+ return nil, err
+ }
+ signedBidProof, err := merkle_tree.MerkleProof(1, 0, b.SignedExecutionPayloadBid.Message, b.SignedExecutionPayloadBid.Signature[:])
+ if err != nil {
+ return nil, err
+ }
+ schema, err := b.gloasHashSchema()
+ if err != nil {
+ return nil, err
+ }
+ bodyProof, err := merkle_tree.ProgressiveContainerProofAll(10, schema...)
+ if err != nil {
+ return nil, err
+ }
+ return append(append(bidProof, signedBidProof...), bodyProof...), nil
+}
+
func (b *BeaconBody) KzgCommitmentMerkleProof(index int) ([][32]byte, error) {
// [Modified in Gloas:EIP7732] BlobKzgCommitments not in BeaconBody for GLOAS
if b.Version >= clparams.GloasVersion {
@@ -576,14 +743,7 @@ func (b *BeaconBody) KzgCommitmentsInclusionProof() ([][32]byte, error) {
}
func (b *BeaconBody) UnmarshalJSON(buf []byte) error {
- var (
- maxAttSlashing = MaxAttesterSlashings
- maxAttestation = MaxAttestations
- )
- if b.Version.AfterOrEqual(clparams.ElectraVersion) {
- maxAttSlashing = MaxAttesterSlashingsElectra
- maxAttestation = MaxAttestationsElectra
- }
+ limits := beaconBodyLimitsForConfig(b.beaconCfg, b.Version)
var tmp struct {
RandaoReveal common.Bytes96 `json:"randao_reveal"`
@@ -604,50 +764,80 @@ func (b *BeaconBody) UnmarshalJSON(buf []byte) error {
PayloadAttestations *solid.ListSSZ[*PayloadAttestation] `json:"payload_attestations,omitempty"`
ParentExecutionRequests *ExecutionRequests `json:"parent_execution_requests,omitempty"`
}
- tmp.ProposerSlashings = solid.NewStaticListSSZ[*ProposerSlashing](MaxProposerSlashings, 416)
- tmp.AttesterSlashings = solid.NewDynamicListSSZ[*AttesterSlashing](maxAttSlashing)
- tmp.Attestations = solid.NewDynamicListSSZ[*solid.Attestation](maxAttestation)
- tmp.Deposits = solid.NewStaticListSSZ[*Deposit](MaxDeposits, 1240)
- tmp.VoluntaryExits = solid.NewStaticListSSZ[*SignedVoluntaryExit](MaxVoluntaryExits, 112)
- tmp.ExecutionChanges = solid.NewStaticListSSZ[*SignedBLSToExecutionChange](MaxExecutionChanges, 172)
+ tmp.ProposerSlashings = solid.NewStaticListSSZ[*ProposerSlashing](limits.proposerSlashings, 416)
+ tmp.AttesterSlashings = solid.NewDynamicListSSZ[*AttesterSlashing](limits.attesterSlashings)
+ tmp.Attestations = solid.NewDynamicListSSZ[*solid.Attestation](limits.attestations)
+ tmp.Deposits = solid.NewStaticListSSZ[*Deposit](limits.deposits, 1240)
+ tmp.VoluntaryExits = solid.NewStaticListSSZ[*SignedVoluntaryExit](limits.voluntaryExits, 112)
+ tmp.ExecutionChanges = solid.NewStaticListSSZ[*SignedBLSToExecutionChange](limits.executionChanges, 172)
// [Modified in Gloas:EIP7732] Only initialize pre-GLOAS fields when needed
if b.Version < clparams.GloasVersion {
- maxBlobCommitments := MaxBlobsCommittmentsPerBlock
- if b.beaconCfg != nil && b.beaconCfg.MaxBlobCommittmentsPerBlock > 0 {
- maxBlobCommitments = int(b.beaconCfg.MaxBlobCommittmentsPerBlock)
- }
- tmp.BlobKzgCommitments = solid.NewStaticListSSZ[*KZGCommitment](maxBlobCommitments, 48)
+ tmp.BlobKzgCommitments = solid.NewStaticListSSZ[*KZGCommitment](maxBlobCommitmentsForConfig(b.beaconCfg), 48)
tmp.ExecutionRequests = NewExecutionRequestsWithVersion(b.beaconCfg, b.Version)
tmp.ExecutionPayload = NewEth1Block(b.Version, b.beaconCfg)
}
// [New in Gloas:EIP7732] Initialize GLOAS fields
if b.Version >= clparams.GloasVersion {
- maxBlobCommitmentsGloas := MaxBlobsCommittmentsPerBlock
- if b.beaconCfg != nil && b.beaconCfg.MaxBlobCommittmentsPerBlock > 0 {
- maxBlobCommitmentsGloas = int(b.beaconCfg.MaxBlobCommittmentsPerBlock)
- }
+ tmp.ProposerSlashings = solid.NewStaticProgressiveListSSZ[*ProposerSlashing](limits.proposerSlashings, 416)
+ tmp.AttesterSlashings = solid.NewDynamicProgressiveListSSZ[*AttesterSlashing](limits.attesterSlashings)
+ tmp.Attestations = solid.NewDynamicProgressiveListSSZ[*solid.Attestation](limits.attestations)
+ tmp.Deposits = solid.NewStaticProgressiveListSSZ[*Deposit](limits.deposits, 1240)
+ tmp.VoluntaryExits = solid.NewStaticProgressiveListSSZ[*SignedVoluntaryExit](limits.voluntaryExits, 112)
+ tmp.ExecutionChanges = solid.NewStaticProgressiveListSSZ[*SignedBLSToExecutionChange](limits.executionChanges, 172)
tmp.SignedExecutionPayloadBid = &SignedExecutionPayloadBid{
Message: &ExecutionPayloadBid{
- BlobKzgCommitments: *solid.NewStaticListSSZ[*KZGCommitment](maxBlobCommitmentsGloas, 48),
+ BlobKzgCommitments: *solid.NewStaticProgressiveListSSZ[*KZGCommitment](maxBlobCommitmentsForConfig(b.beaconCfg), 48),
},
}
- tmp.PayloadAttestations = solid.NewStaticListSSZ[*PayloadAttestation](int(b.beaconCfg.MaxPayloadAttestations), PayloadAttestationSSZSizeWithPtcSize(b.beaconCfg.PtcSize))
+ ptcSize := clparams.MaxPtcSize
+ if b.beaconCfg != nil && b.beaconCfg.PtcSize > 0 {
+ ptcSize = b.beaconCfg.PtcSize
+ }
+ tmp.PayloadAttestations = solid.NewStaticProgressiveListSSZ[*PayloadAttestation](maxPayloadAttestationsForConfig(b.beaconCfg), PayloadAttestationSSZSizeWithPtcSize(ptcSize))
tmp.ParentExecutionRequests = NewExecutionRequestsWithVersion(b.beaconCfg, b.Version)
}
if err := json.Unmarshal(buf, &tmp); err != nil {
return err
}
+ if b.Version >= clparams.GloasVersion {
+ if tmp.SignedExecutionPayloadBid == nil || tmp.SignedExecutionPayloadBid.Message == nil {
+ return errors.New("gloas beacon body contains a null execution payload bid")
+ }
+ if err := solid.RangeErr(&tmp.SignedExecutionPayloadBid.Message.BlobKzgCommitments, func(i int, commitment *KZGCommitment, _ int) error {
+ if commitment == nil {
+ return fmt.Errorf("blob KZG commitment %d is null", i)
+ }
+ return nil
+ }); err != nil {
+ return err
+ }
+ if tmp.PayloadAttestations == nil {
+ return errors.New("gloas beacon body contains null payload attestations")
+ }
+ if err := solid.RangeErr(tmp.PayloadAttestations, func(i int, attestation *PayloadAttestation, _ int) error {
+ if attestation == nil || attestation.AggregationBits == nil || attestation.Data == nil {
+ return fmt.Errorf("payload attestation %d is incomplete", i)
+ }
+ return nil
+ }); err != nil {
+ return err
+ }
+ if tmp.ParentExecutionRequests == nil {
+ return errors.New("gloas beacon body contains null parent execution requests")
+ }
+ }
tmp.AttesterSlashings.Range(func(_ int, value *AttesterSlashing, _ int) bool {
// Set version with config for preset-aware limits
value.SetVersionWithConfig(b.Version, b.beaconCfg)
return true
})
if b.beaconCfg != nil && b.Version.AfterOrEqual(clparams.ElectraVersion) {
- tmp.Attestations.Range(func(_ int, att *solid.Attestation, _ int) bool {
- att.SetBeaconConfig(b.beaconCfg)
- return true
- })
+ if err := solid.RangeErr(tmp.Attestations, func(_ int, att *solid.Attestation, _ int) error {
+ return att.ValidateForConfig(b.beaconCfg, b.Version)
+ }); err != nil {
+ return err
+ }
}
b.RandaoReveal = tmp.RandaoReveal
diff --git a/cl/cltypes/beacon_block_blinded.go b/cl/cltypes/beacon_block_blinded.go
index cee80b51e38..7e343435444 100644
--- a/cl/cltypes/beacon_block_blinded.go
+++ b/cl/cltypes/beacon_block_blinded.go
@@ -386,10 +386,11 @@ func (b *BlindedBeaconBody) DecodeSSZ(buf []byte, version int) error {
// Post-decode fixup: propagate preset-aware limits to decoded attestations and slashings.
if b.beaconCfg != nil && b.Version.AfterOrEqual(clparams.ElectraVersion) {
- b.Attestations.Range(func(_ int, att *solid.Attestation, _ int) bool {
- att.SetBeaconConfig(b.beaconCfg)
- return true
- })
+ if err := solid.RangeErr(b.Attestations, func(_ int, att *solid.Attestation, _ int) error {
+ return att.ValidateForConfig(b.beaconCfg, b.Version)
+ }); err != nil {
+ return err
+ }
b.AttesterSlashings.Range(func(_ int, as *AttesterSlashing, _ int) bool {
as.SetVersionWithConfig(b.Version, b.beaconCfg)
return true
diff --git a/cl/cltypes/beacon_block_test.go b/cl/cltypes/beacon_block_test.go
index 07b6f458c71..895e270e8f6 100644
--- a/cl/cltypes/beacon_block_test.go
+++ b/cl/cltypes/beacon_block_test.go
@@ -19,6 +19,7 @@ package cltypes
import (
_ "embed"
"encoding/json"
+ "math"
"testing"
"github.com/holiman/uint256"
@@ -648,3 +649,32 @@ func TestBeaconBody_GetPayloadAttestations_VersionAware(t *testing.T) {
gloasBody := NewBeaconBody(bc, clparams.GloasVersion)
assert.NotNil(t, gloasBody.GetPayloadAttestations(), "GLOAS should have PayloadAttestations")
}
+
+func TestBeaconBodyGloasJSONRejectsNullRequiredFields(t *testing.T) {
+ for _, input := range []string{
+ `{"signed_execution_payload_bid":null}`,
+ `{"signed_execution_payload_bid":{"message":null}}`,
+ `{"signed_execution_payload_bid":{"message":{"blob_kzg_commitments":null}}}`,
+ `{"signed_execution_payload_bid":{"message":{"blob_kzg_commitments":[null]}}}`,
+ `{"payload_attestations":null}`,
+ `{"payload_attestations":[null]}`,
+ `{"payload_attestations":[{"aggregation_bits":null,"data":null}]}`,
+ `{"parent_execution_requests":null}`,
+ `{"parent_execution_requests":{"deposits":[null]}}`,
+ } {
+ t.Run(input, func(t *testing.T) {
+ body := NewBeaconBody(&clparams.MainnetBeaconConfig, clparams.GloasVersion)
+ require.Error(t, json.Unmarshal([]byte(input), body))
+ })
+ }
+}
+
+func TestBeaconBodyGloasProgressiveLimitsUseConfig(t *testing.T) {
+ for _, limit := range []uint64{32, math.MaxUint64} {
+ cfg := clparams.MainnetBeaconConfig
+ cfg.MaxProposerSlashings = limit
+ body := NewBeaconBody(&cfg, clparams.GloasVersion)
+
+ require.NoError(t, body.ProposerSlashings.DecodeSSZ(make([]byte, 33*416), int(clparams.GloasVersion)))
+ }
+}
diff --git a/cl/cltypes/column_sidecar.go b/cl/cltypes/column_sidecar.go
index 4020c09ab6c..b1a38dbf41f 100644
--- a/cl/cltypes/column_sidecar.go
+++ b/cl/cltypes/column_sidecar.go
@@ -82,10 +82,18 @@ func (d *DataColumnSidecar) tryInit() {
func (d *DataColumnSidecar) tryInitWithVersion(version clparams.StateVersion) {
cfg := clparams.GetBeaconConfig()
if d.Column == nil {
- d.Column = solid.NewStaticListSSZ[*Cell](int(cfg.MaxBlobCommittmentsPerBlock), BytesPerCell)
+ if version >= clparams.GloasVersion {
+ d.Column = solid.NewStaticProgressiveListSSZ[*Cell](int(cfg.MaxBlobCommittmentsPerBlock), BytesPerCell)
+ } else {
+ d.Column = solid.NewStaticListSSZ[*Cell](int(cfg.MaxBlobCommittmentsPerBlock), BytesPerCell)
+ }
}
if d.KzgProofs == nil {
- d.KzgProofs = solid.NewStaticListSSZ[*KZGProof](int(cfg.MaxBlobCommittmentsPerBlock), 48)
+ if version >= clparams.GloasVersion {
+ d.KzgProofs = solid.NewStaticProgressiveListSSZ[*KZGProof](int(cfg.MaxBlobCommittmentsPerBlock), 48)
+ } else {
+ d.KzgProofs = solid.NewStaticListSSZ[*KZGProof](int(cfg.MaxBlobCommittmentsPerBlock), 48)
+ }
}
// Pre-Gloas fields (Fulu and earlier)
if version < clparams.GloasVersion {
@@ -104,6 +112,8 @@ func (d *DataColumnSidecar) tryInitWithVersion(version clparams.StateVersion) {
func (d *DataColumnSidecar) DecodeSSZ(buf []byte, version int) error {
d.version = clparams.StateVersion(version)
+ d.Column = nil
+ d.KzgProofs = nil
d.tryInitWithVersion(d.version)
return ssz2.UnmarshalSSZ(buf, version, d.getSchemaForVersion(d.version)...)
}
diff --git a/cl/cltypes/epbs_payload.go b/cl/cltypes/epbs_payload.go
index 789bb2f1964..b11a3293b6a 100644
--- a/cl/cltypes/epbs_payload.go
+++ b/cl/cltypes/epbs_payload.go
@@ -17,6 +17,10 @@
package cltypes
import (
+ "bytes"
+ "encoding/json"
+ "errors"
+
"github.com/erigontech/erigon/cl/clparams"
"github.com/erigontech/erigon/cl/cltypes/solid"
"github.com/erigontech/erigon/cl/merkle_tree"
@@ -126,7 +130,7 @@ type PayloadAttestation struct {
}
func (p *PayloadAttestation) HashSSZ() ([32]byte, error) {
- return merkle_tree.HashTreeRoot(p.AggregationBits, p.Data, p.Signature[:])
+ return merkle_tree.ProgressiveContainerRootAll(p.AggregationBits, p.Data, p.Signature[:])
}
func (p *PayloadAttestation) EncodingSizeSSZ() int {
@@ -242,7 +246,7 @@ func (i *IndexedPayloadAttestation) EncodingSizeSSZ() int {
}
func (i *IndexedPayloadAttestation) HashSSZ() ([32]byte, error) {
- return merkle_tree.HashTreeRoot(i.AttestingIndices, i.Data, i.Signature[:])
+ return merkle_tree.ProgressiveContainerRootAll(i.AttestingIndices, i.Data, i.Signature[:])
}
func (i *IndexedPayloadAttestation) Clone() clonable.Clonable {
@@ -275,7 +279,27 @@ type ExecutionPayloadBid struct {
}
func (e *ExecutionPayloadBid) HashSSZ() ([32]byte, error) {
- return merkle_tree.HashTreeRoot(
+ schema, err := e.hashSchema()
+ if err != nil {
+ return [32]byte{}, err
+ }
+ return merkle_tree.ProgressiveContainerRootAll(schema...)
+}
+
+func (e *ExecutionPayloadBid) ParentBlockHashMerkleProof() ([][32]byte, error) {
+ schema, err := e.hashSchema()
+ if err != nil {
+ return nil, err
+ }
+ return merkle_tree.ProgressiveContainerProofAll(0, schema...)
+}
+
+func (e *ExecutionPayloadBid) hashSchema() ([]any, error) {
+ blobRoot, err := e.BlobKzgCommitments.HashSSZProgressive(nil)
+ if err != nil {
+ return nil, err
+ }
+ return []any{
e.ParentBlockHash[:],
e.ParentBlockRoot[:],
e.BlockHash[:],
@@ -286,9 +310,9 @@ func (e *ExecutionPayloadBid) HashSSZ() ([32]byte, error) {
e.Slot,
e.Value,
e.ExecutionPayment,
- &e.BlobKzgCommitments,
+ blobRoot[:],
e.ExecutionRequestsRoot[:],
- )
+ }, nil
}
func (e *ExecutionPayloadBid) EncodingSizeSSZ() int {
@@ -325,7 +349,7 @@ func (e *ExecutionPayloadBid) EncodeSSZ(buf []byte) ([]byte, error) {
}
func (e *ExecutionPayloadBid) DecodeSSZ(buf []byte, version int) error {
- e.BlobKzgCommitments = *solid.NewStaticListSSZ[*KZGCommitment](MaxBlobsCommittmentsPerBlock, 48)
+ e.BlobKzgCommitments.EnsureStaticProgressive(maxBlobCommitmentsForConfig(clparams.GetBeaconConfig()), 48)
return ssz2.UnmarshalSSZ(
buf, version,
e.ParentBlockHash[:],
@@ -344,11 +368,30 @@ func (e *ExecutionPayloadBid) DecodeSSZ(buf []byte, version int) error {
}
func (e *ExecutionPayloadBid) Clone() clonable.Clonable {
+ commitments := e.BlobKzgCommitments.Clone().(*solid.ListSSZ[*KZGCommitment])
+ commitments.EnsureStaticProgressive(maxBlobCommitmentsForConfig(clparams.GetBeaconConfig()), 48)
return &ExecutionPayloadBid{
- BlobKzgCommitments: *solid.NewStaticListSSZ[*KZGCommitment](MaxBlobsCommittmentsPerBlock, 48),
+ BlobKzgCommitments: *commitments,
}
}
+func (e *ExecutionPayloadBid) UnmarshalJSON(data []byte) error {
+ var fields map[string]json.RawMessage
+ if err := json.Unmarshal(data, &fields); err != nil {
+ return err
+ }
+ commitments, ok := fields["blob_kzg_commitments"]
+ if !ok || bytes.Equal(bytes.TrimSpace(commitments), []byte("null")) {
+ return errors.New("execution payload bid contains null blob KZG commitments")
+ }
+ type executionPayloadBid ExecutionPayloadBid
+ if err := json.Unmarshal(data, (*executionPayloadBid)(e)); err != nil {
+ return err
+ }
+ e.BlobKzgCommitments.EnsureStaticProgressive(maxBlobCommitmentsForConfig(clparams.GetBeaconConfig()), 48)
+ return nil
+}
+
func (e *ExecutionPayloadBid) Copy() *ExecutionPayloadBid {
return &ExecutionPayloadBid{
ParentBlockHash: e.ParentBlockHash,
@@ -389,7 +432,11 @@ func (s *SignedExecutionPayloadBid) EncodeSSZ(buf []byte) ([]byte, error) {
}
func (s *SignedExecutionPayloadBid) DecodeSSZ(buf []byte, version int) error {
- s.Message = new(ExecutionPayloadBid)
+ if s.Message == nil {
+ s.Message = &ExecutionPayloadBid{
+ BlobKzgCommitments: *solid.NewStaticProgressiveListSSZ[*KZGCommitment](maxBlobCommitmentsForConfig(clparams.GetBeaconConfig()), 48),
+ }
+ }
return ssz2.UnmarshalSSZ(buf, version, s.Message, s.Signature[:])
}
@@ -423,7 +470,7 @@ func NewExecutionPayloadEnvelope(cfg *clparams.BeaconChainConfig) *ExecutionPayl
}
func (e *ExecutionPayloadEnvelope) HashSSZ() ([32]byte, error) {
- return merkle_tree.HashTreeRoot(
+ return merkle_tree.ProgressiveContainerRootAll(
e.Payload,
e.ExecutionRequests,
e.BuilderIndex,
diff --git a/cl/cltypes/epbs_payload_test.go b/cl/cltypes/epbs_payload_test.go
index e3df7bd6988..0e7d5c54346 100644
--- a/cl/cltypes/epbs_payload_test.go
+++ b/cl/cltypes/epbs_payload_test.go
@@ -1,9 +1,13 @@
package cltypes
import (
+ "encoding/json"
+ "errors"
"testing"
+ "github.com/erigontech/erigon/cl/cltypes/solid"
"github.com/erigontech/erigon/common"
+ "github.com/erigontech/erigon/common/ssz"
"github.com/stretchr/testify/require"
)
@@ -55,3 +59,84 @@ func TestBuilderPendingPaymentCloneCopiesFields(t *testing.T) {
require.Equal(t, payment, cloned)
require.NotSame(t, payment.Withdrawal, cloned.Withdrawal)
}
+
+func TestExecutionPayloadBidDecodePreservesProgressiveLimit(t *testing.T) {
+ encoded := encodedExecutionPayloadBidWithCommitments(t, 17)
+ target := &ExecutionPayloadBid{
+ BlobKzgCommitments: *solid.NewStaticProgressiveListSSZ[*KZGCommitment](1, 48),
+ }
+
+ require.ErrorIs(t, target.DecodeSSZ(encoded, 0), ssz.ErrTooBigList)
+
+ encoded = encodedExecutionPayloadBidWithCommitments(t, 2)
+ require.NoError(t, target.DecodeSSZ(encoded, 0))
+}
+
+func TestSignedExecutionPayloadBidDecodePreservesMessageLimit(t *testing.T) {
+ message := &ExecutionPayloadBid{
+ BlobKzgCommitments: *solid.NewStaticProgressiveListSSZ[*KZGCommitment](1, 48),
+ }
+ target := &SignedExecutionPayloadBid{Message: message}
+ source := &SignedExecutionPayloadBid{Message: executionPayloadBidWithCommitments(17)}
+ encoded, err := source.EncodeSSZ(nil)
+ require.NoError(t, err)
+
+ err = target.DecodeSSZ(encoded, 0)
+ require.True(t, errors.Is(err, ssz.ErrTooBigList), err)
+ require.Same(t, message, target.Message)
+}
+
+func TestExecutionPayloadBidClonePreservesProgressiveLimit(t *testing.T) {
+ bid := &ExecutionPayloadBid{
+ BlobKzgCommitments: *solid.NewStaticProgressiveListSSZ[*KZGCommitment](1, 48),
+ }
+ cloned := bid.Clone().(*ExecutionPayloadBid)
+ encoded := make([]byte, 17*48)
+
+ require.ErrorIs(t, cloned.BlobKzgCommitments.DecodeSSZ(encoded, 0), ssz.ErrTooBigList)
+}
+
+func TestSignedExecutionPayloadBidJSONInitializesStaticProgressiveList(t *testing.T) {
+ source := &SignedExecutionPayloadBid{Message: executionPayloadBidWithCommitments(2)}
+ input, err := json.Marshal(source)
+ require.NoError(t, err)
+
+ var decoded SignedExecutionPayloadBid
+ require.NoError(t, json.Unmarshal(input, &decoded))
+ got, err := decoded.EncodeSSZ(nil)
+ require.NoError(t, err)
+ want, err := source.EncodeSSZ(nil)
+ require.NoError(t, err)
+ require.Equal(t, want, got)
+
+ var roundTrip SignedExecutionPayloadBid
+ require.NoError(t, roundTrip.DecodeSSZ(got, 0))
+ require.Equal(t, decoded.Message.BlobKzgCommitments.Len(), roundTrip.Message.BlobKzgCommitments.Len())
+}
+
+func TestExecutionPayloadBidJSONPreservesPreseededProgressiveLimit(t *testing.T) {
+ input, err := json.Marshal(executionPayloadBidWithCommitments(2))
+ require.NoError(t, err)
+ target := &ExecutionPayloadBid{
+ BlobKzgCommitments: *solid.NewStaticProgressiveListSSZ[*KZGCommitment](1, 48),
+ }
+ require.NoError(t, json.Unmarshal(input, target))
+
+ cloned := target.Clone().(*ExecutionPayloadBid)
+ require.ErrorIs(t, cloned.BlobKzgCommitments.DecodeSSZ(make([]byte, 17*48), 0), ssz.ErrTooBigList)
+}
+
+func encodedExecutionPayloadBidWithCommitments(t *testing.T, count int) []byte {
+ t.Helper()
+ encoded, err := executionPayloadBidWithCommitments(count).EncodeSSZ(nil)
+ require.NoError(t, err)
+ return encoded
+}
+
+func executionPayloadBidWithCommitments(count int) *ExecutionPayloadBid {
+ commitments := solid.NewStaticProgressiveListSSZ[*KZGCommitment](1, 48)
+ for range count {
+ commitments.Append(new(KZGCommitment))
+ }
+ return &ExecutionPayloadBid{BlobKzgCommitments: *commitments}
+}
diff --git a/cl/cltypes/eth1_block.go b/cl/cltypes/eth1_block.go
index 9df029de24a..ae137ddbf93 100644
--- a/cl/cltypes/eth1_block.go
+++ b/cl/cltypes/eth1_block.go
@@ -420,9 +420,48 @@ func (b *Eth1Block) EncodeSSZ(dst []byte) ([]byte, error) {
// HashSSZ calculates the SSZ hash of the Eth1Block's payload header.
func (b *Eth1Block) HashSSZ() ([32]byte, error) {
b.ensureSSZFields()
+ if b.version >= clparams.GloasVersion {
+ return b.hashSSZGloas()
+ }
return merkle_tree.HashTreeRoot(b.getSchema()...)
}
+func (b *Eth1Block) hashSSZGloas() ([32]byte, error) {
+ transactionsRoot, err := b.Transactions.HashSSZProgressive()
+ if err != nil {
+ return [32]byte{}, err
+ }
+ withdrawalsRoot, err := b.Withdrawals.HashSSZProgressive(nil)
+ if err != nil {
+ return [32]byte{}, err
+ }
+ blockAccessListRoot, err := b.BlockAccessList.HashSSZProgressive()
+ if err != nil {
+ return [32]byte{}, err
+ }
+ return merkle_tree.ProgressiveContainerRootAll(
+ b.ParentHash[:],
+ b.FeeRecipient[:],
+ b.StateRoot[:],
+ b.ReceiptsRoot[:],
+ b.LogsBloom[:],
+ b.PrevRandao[:],
+ b.BlockNumber,
+ b.GasLimit,
+ b.GasUsed,
+ b.Time,
+ b.Extra,
+ b.BaseFeePerGas[:],
+ b.BlockHash[:],
+ transactionsRoot[:],
+ withdrawalsRoot[:],
+ b.BlobGasUsed,
+ b.ExcessBlobGas,
+ blockAccessListRoot[:],
+ b.SlotNumber,
+ )
+}
+
// ensureSSZFields lazily initializes nil slice/list fields that getSchema()
// references, so that HashSSZ/EncodeSSZ never panic on a zero-value Eth1Block.
func (b *Eth1Block) ensureSSZFields() {
diff --git a/cl/cltypes/execution_requests.go b/cl/cltypes/execution_requests.go
index 0b8214e4e06..5ddfb690aad 100644
--- a/cl/cltypes/execution_requests.go
+++ b/cl/cltypes/execution_requests.go
@@ -57,19 +57,30 @@ func (e *ExecutionRequests) ensureLists() {
if e.cfg == nil {
panic("execution requests beacon config is nil")
}
- if e.Deposits == nil {
+ progressive := e.effectiveVersion() >= clparams.GloasVersion
+ if e.Deposits == nil && progressive {
+ e.Deposits = solid.NewStaticProgressiveListSSZ[*solid.DepositRequest](int(e.cfg.MaxDepositRequestsPerPayload), solid.SizeDepositRequest)
+ } else if e.Deposits == nil {
e.Deposits = solid.NewStaticListSSZ[*solid.DepositRequest](int(e.cfg.MaxDepositRequestsPerPayload), solid.SizeDepositRequest)
}
- if e.Withdrawals == nil {
+ if e.Withdrawals == nil && progressive {
+ e.Withdrawals = solid.NewStaticProgressiveListSSZ[*solid.WithdrawalRequest](int(e.cfg.MaxWithdrawalRequestsPerPayload), solid.SizeWithdrawalRequest)
+ } else if e.Withdrawals == nil {
e.Withdrawals = solid.NewStaticListSSZ[*solid.WithdrawalRequest](int(e.cfg.MaxWithdrawalRequestsPerPayload), solid.SizeWithdrawalRequest)
}
- if e.Consolidations == nil {
+ if e.Consolidations == nil && progressive {
+ e.Consolidations = solid.NewStaticProgressiveListSSZ[*solid.ConsolidationRequest](int(e.cfg.MaxConsolidationRequestsPerPayload), solid.SizeConsolidationRequest)
+ } else if e.Consolidations == nil {
e.Consolidations = solid.NewStaticListSSZ[*solid.ConsolidationRequest](int(e.cfg.MaxConsolidationRequestsPerPayload), solid.SizeConsolidationRequest)
}
- if e.BuilderDeposits == nil {
+ if e.BuilderDeposits == nil && progressive {
+ e.BuilderDeposits = solid.NewStaticProgressiveListSSZ[*solid.BuilderDepositRequest](int(e.cfg.MaxBuilderDepositRequestsPerPayload), solid.SizeBuilderDepositRequest)
+ } else if e.BuilderDeposits == nil {
e.BuilderDeposits = solid.NewStaticListSSZ[*solid.BuilderDepositRequest](int(e.cfg.MaxBuilderDepositRequestsPerPayload), solid.SizeBuilderDepositRequest)
}
- if e.BuilderExits == nil {
+ if e.BuilderExits == nil && progressive {
+ e.BuilderExits = solid.NewStaticProgressiveListSSZ[*solid.BuilderExitRequest](int(e.cfg.MaxBuilderExitRequestsPerPayload), solid.SizeBuilderExitRequest)
+ } else if e.BuilderExits == nil {
e.BuilderExits = solid.NewStaticListSSZ[*solid.BuilderExitRequest](int(e.cfg.MaxBuilderExitRequestsPerPayload), solid.SizeBuilderExitRequest)
}
}
@@ -100,7 +111,15 @@ func (e *ExecutionRequests) EncodeSSZ(buf []byte) ([]byte, error) {
}
func (e *ExecutionRequests) DecodeSSZ(buf []byte, version int) error {
- e.version = clparams.StateVersion(version)
+ decodedVersion := clparams.StateVersion(version)
+ if (e.effectiveVersion() >= clparams.GloasVersion) != (decodedVersion >= clparams.GloasVersion) {
+ e.Deposits = nil
+ e.Withdrawals = nil
+ e.Consolidations = nil
+ e.BuilderDeposits = nil
+ e.BuilderExits = nil
+ }
+ e.version = decodedVersion
e.ensureLists()
if e.effectiveVersion() < clparams.GloasVersion {
return ssz2.UnmarshalSSZ(buf, version, e.Deposits, e.Withdrawals, e.Consolidations)
@@ -159,7 +178,27 @@ func (e *ExecutionRequests) HashSSZ() ([32]byte, error) {
if e.effectiveVersion() < clparams.GloasVersion {
return merkle_tree.HashTreeRoot(e.Deposits, e.Withdrawals, e.Consolidations)
}
- return merkle_tree.HashTreeRoot(e.Deposits, e.Withdrawals, e.Consolidations, e.BuilderDeposits, e.BuilderExits)
+ deposits, err := e.Deposits.HashSSZProgressive(nil)
+ if err != nil {
+ return [32]byte{}, err
+ }
+ withdrawals, err := e.Withdrawals.HashSSZProgressive(nil)
+ if err != nil {
+ return [32]byte{}, err
+ }
+ consolidations, err := e.Consolidations.HashSSZProgressive(nil)
+ if err != nil {
+ return [32]byte{}, err
+ }
+ builderDeposits, err := e.BuilderDeposits.HashSSZProgressive(nil)
+ if err != nil {
+ return [32]byte{}, err
+ }
+ builderExits, err := e.BuilderExits.HashSSZProgressive(nil)
+ if err != nil {
+ return [32]byte{}, err
+ }
+ return merkle_tree.ProgressiveContainerRootAll(deposits[:], withdrawals[:], consolidations[:], builderDeposits[:], builderExits[:])
}
func (e *ExecutionRequests) Static() bool {
@@ -168,6 +207,18 @@ func (e *ExecutionRequests) Static() bool {
func (e *ExecutionRequests) UnmarshalJSON(b []byte) error {
e.ensureLists()
+ newDeposits := solid.NewStaticListSSZ[*solid.DepositRequest](int(e.cfg.MaxDepositRequestsPerPayload), solid.SizeDepositRequest)
+ newWithdrawals := solid.NewStaticListSSZ[*solid.WithdrawalRequest](int(e.cfg.MaxWithdrawalRequestsPerPayload), solid.SizeWithdrawalRequest)
+ newConsolidations := solid.NewStaticListSSZ[*solid.ConsolidationRequest](int(e.cfg.MaxConsolidationRequestsPerPayload), solid.SizeConsolidationRequest)
+ newBuilderDeposits := solid.NewStaticListSSZ[*solid.BuilderDepositRequest](int(e.cfg.MaxBuilderDepositRequestsPerPayload), solid.SizeBuilderDepositRequest)
+ newBuilderExits := solid.NewStaticListSSZ[*solid.BuilderExitRequest](int(e.cfg.MaxBuilderExitRequestsPerPayload), solid.SizeBuilderExitRequest)
+ if e.effectiveVersion() >= clparams.GloasVersion {
+ newDeposits = solid.NewStaticProgressiveListSSZ[*solid.DepositRequest](int(e.cfg.MaxDepositRequestsPerPayload), solid.SizeDepositRequest)
+ newWithdrawals = solid.NewStaticProgressiveListSSZ[*solid.WithdrawalRequest](int(e.cfg.MaxWithdrawalRequestsPerPayload), solid.SizeWithdrawalRequest)
+ newConsolidations = solid.NewStaticProgressiveListSSZ[*solid.ConsolidationRequest](int(e.cfg.MaxConsolidationRequestsPerPayload), solid.SizeConsolidationRequest)
+ newBuilderDeposits = solid.NewStaticProgressiveListSSZ[*solid.BuilderDepositRequest](int(e.cfg.MaxBuilderDepositRequestsPerPayload), solid.SizeBuilderDepositRequest)
+ newBuilderExits = solid.NewStaticProgressiveListSSZ[*solid.BuilderExitRequest](int(e.cfg.MaxBuilderExitRequestsPerPayload), solid.SizeBuilderExitRequest)
+ }
c := struct {
Deposits *solid.ListSSZ[*solid.DepositRequest] `json:"deposits"`
Withdrawals *solid.ListSSZ[*solid.WithdrawalRequest] `json:"withdrawals"`
@@ -175,15 +226,60 @@ func (e *ExecutionRequests) UnmarshalJSON(b []byte) error {
BuilderDeposits *solid.ListSSZ[*solid.BuilderDepositRequest] `json:"builder_deposits"`
BuilderExits *solid.ListSSZ[*solid.BuilderExitRequest] `json:"builder_exits"`
}{
- Deposits: solid.NewStaticListSSZ[*solid.DepositRequest](int(e.cfg.MaxDepositRequestsPerPayload), solid.SizeDepositRequest),
- Withdrawals: solid.NewStaticListSSZ[*solid.WithdrawalRequest](int(e.cfg.MaxWithdrawalRequestsPerPayload), solid.SizeWithdrawalRequest),
- Consolidations: solid.NewStaticListSSZ[*solid.ConsolidationRequest](int(e.cfg.MaxConsolidationRequestsPerPayload), solid.SizeConsolidationRequest),
- BuilderDeposits: solid.NewStaticListSSZ[*solid.BuilderDepositRequest](int(e.cfg.MaxBuilderDepositRequestsPerPayload), solid.SizeBuilderDepositRequest),
- BuilderExits: solid.NewStaticListSSZ[*solid.BuilderExitRequest](int(e.cfg.MaxBuilderExitRequestsPerPayload), solid.SizeBuilderExitRequest),
+ Deposits: newDeposits,
+ Withdrawals: newWithdrawals,
+ Consolidations: newConsolidations,
+ BuilderDeposits: newBuilderDeposits,
+ BuilderExits: newBuilderExits,
}
if err := json.Unmarshal(b, &c); err != nil {
return err
}
+ c.Deposits = coalesceExecutionRequestList(c.Deposits, newDeposits)
+ c.Withdrawals = coalesceExecutionRequestList(c.Withdrawals, newWithdrawals)
+ c.Consolidations = coalesceExecutionRequestList(c.Consolidations, newConsolidations)
+ c.BuilderDeposits = coalesceExecutionRequestList(c.BuilderDeposits, newBuilderDeposits)
+ c.BuilderExits = coalesceExecutionRequestList(c.BuilderExits, newBuilderExits)
+ if err := solid.RangeErr(c.Deposits, func(i int, request *solid.DepositRequest, _ int) error {
+ if request == nil {
+ return fmt.Errorf("deposit request %d is null", i)
+ }
+ return nil
+ }); err != nil {
+ return err
+ }
+ if err := solid.RangeErr(c.Withdrawals, func(i int, request *solid.WithdrawalRequest, _ int) error {
+ if request == nil {
+ return fmt.Errorf("withdrawal request %d is null", i)
+ }
+ return nil
+ }); err != nil {
+ return err
+ }
+ if err := solid.RangeErr(c.Consolidations, func(i int, request *solid.ConsolidationRequest, _ int) error {
+ if request == nil {
+ return fmt.Errorf("consolidation request %d is null", i)
+ }
+ return nil
+ }); err != nil {
+ return err
+ }
+ if err := solid.RangeErr(c.BuilderDeposits, func(i int, request *solid.BuilderDepositRequest, _ int) error {
+ if request == nil {
+ return fmt.Errorf("builder deposit request %d is null", i)
+ }
+ return nil
+ }); err != nil {
+ return err
+ }
+ if err := solid.RangeErr(c.BuilderExits, func(i int, request *solid.BuilderExitRequest, _ int) error {
+ if request == nil {
+ return fmt.Errorf("builder exit request %d is null", i)
+ }
+ return nil
+ }); err != nil {
+ return err
+ }
e.Deposits = c.Deposits
e.Withdrawals = c.Withdrawals
@@ -197,6 +293,13 @@ func (e *ExecutionRequests) UnmarshalJSON(b []byte) error {
return nil
}
+func coalesceExecutionRequestList[T solid.EncodableHashableSSZ](list, empty *solid.ListSSZ[T]) *solid.ListSSZ[T] {
+ if list == nil {
+ return empty
+ }
+ return list
+}
+
func (e *ExecutionRequests) MarshalJSON() ([]byte, error) {
e.ensureLists()
if e.effectiveVersion() < clparams.GloasVersion {
diff --git a/cl/cltypes/gloas_progressive_hash_test.go b/cl/cltypes/gloas_progressive_hash_test.go
new file mode 100644
index 00000000000..1417578e311
--- /dev/null
+++ b/cl/cltypes/gloas_progressive_hash_test.go
@@ -0,0 +1,45 @@
+// Copyright 2026 The Erigon Authors
+// This file is part of Erigon.
+//
+// Erigon is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Erigon is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with Erigon. If not, see .
+
+package cltypes
+
+import (
+ "encoding/base64"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/erigontech/erigon/cl/clparams"
+ "github.com/erigontech/erigon/common"
+)
+
+func TestGloasProgressiveBlockRoot(t *testing.T) {
+ encoded := `ZAAAAJYtQIWHk1ycTpGzhW0g2p7X6s5ks5Ubf1jIX2yArmqnYontvy0ASMpxZwFAkQlICQhpMhnL5ghg3ri/CLXKr0NuIKLrFab6aavneWflEwfCWqc0aYDEnRI/Jt2UeEkNp8EEAAAAAAAAzQEAAAAAAAB43v4sBrX296jsHdhWCjtKBCwHKiCaWsyPZXdANizHuwonuCaFkdSYYa3orUcF90o0g9s5Xfvb1nlznH5YfDc/VAAAAI8DTbX2llwS7QSbGqCGvpdH4ebYOXFjFz24HkSRWufwK6SwCKZnfkclBDHF8dLGPxZveC+9GyoS0b1TmeTAUryPbAE8LhQAptrTRot0fbsR97mPpYznLpxn70tgcoQGzNcKI0cxKFxoBMKk9WcR3bjILJl0DyB4VIkQKK804n5eAAAAAAAAAAA3/Ny/HDjdeJH+veQV6F3ZDQSTKrNXvCyRUZX4GW7fC2xvZGVzdGFyLWV0aHJleC0xIEVYODZkZExTYmZkZAAAjAEAAIwBAACMAQAAgAQAAIAEAACZg+iv/Rx9Yt+Xu/Hnf+/n/P1h3rvv4df27i1LzWnqvla+a48tvwr3/53/1Sz/3nC9fu+fbnH1dS1mv++fnzN/hunIdZDFkkCQL+jMLWyjT+DBuBix6PvJodVEJ9ZINklZOtI7TPZsoYd0jfWRUqm/C+gomaT6dFB8M/zeDF1X5KJaE0vZjYFawfgpr+8eCZiNOT+3qXCW9627GTZsrXHvgAQAAIAEAADECAAAxAgAAAwAAAAEAQAA/AEAAOwAAAC/BAAAAAAAAAAAAAAAAAAAeN7+LAa19veo7B3YVgo7SgQsByogmlrMj2V3QDYsx7skAAAAAAAAADd1mVev1X2E6rBufJgd+eNAgeYyIZDFXJzwE5PkkxJdJQAAAAAAAABb73WdxetG8KuQ8sCFdRkx1OUkxORvo2WdZP7z45cDhIa5MVf0VqdC8LeL1NZWUaPp5ncHqoduOnU2MGluhpBHXrsB//ILEZwGv8PlEiD5EAflPPvSCVtclBEPyNca9kswZZD9MlXL7oJj2BUFbLuu7Ps4zzotIoP6+mAVYcPWLAEAAAAAAAAA//////////////9/7AAAAMAEAAAAAAAAAQAAAAAAAAB43v4sBrX296jsHdhWCjtKBCwHKiCaWsyPZXdANizHuyUAAAAAAAAAW+91ncXrRvCrkPLAhXUZMdTlJMTkb6NlnWT+8+OXA4QmAAAAAAAAAHje/iwGtfb3qOwd2FYKO0oELAcqIJpazI9ld0A2LMe7gkcUbqEPulyg1S+j2258vUz8yDxuBUrSm3Uq5SKrKGvnJuJg8YMxz2hyPf2zMzDnFGsl0hXmBQEcROg/pENj0qUcn+U5qr5sRwTlbGM1OUKLChYerL2IVfZi0z8BQFBgAQAAAAAAAAA3GToEJDgHDJAZECDsAAAAwAQAAAAAAAAAAAAAAAAAAHje/iwGtfb3qOwd2FYKO0oELAcqIJpazI9ld0A2LMe7JQAAAAAAAABb73WdxetG8KuQ8sCFdRkx1OUkxORvo2WdZP7z45cDhCYAAAAAAAAAeN7+LAa19veo7B3YVgo7SgQsByogmlrMj2V3QDYsx7ug/Kb8F3PBzlJ57AnyYuJZnrPEGb8Q/XzKQkbFbOfteaYdZ9sq3I9FFsFM4gTDP18FNRBS6YD7RlZUsNtyTVm+AQLL9TLx2vcc4aXXEGp1EOtJJAriS/IF5wond/ATmbkBAAAAAAAAAAjihODQB9hwTUJsKmQAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB2/s8b9pHxWVGuVYoLRoGPht0jvfhULoAVglOu32rv1nje/iwGtfb3qOwd2FYKO0oELAcqIJpazI9ld0A2LMe7avGWvZgUy5vPpxR+yGqzSKaZWoWeh3of/AsxfAzFAl3aLCURsryYRyNg4hDBF87INvOejfSfw1PuZ9Vd80Pr+vl+GAwFDlqwciEa0sIT61ruTfE0vdT4BgAAAAD//////////8EEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAC4LuMEjC70JWA2RX604KDy903a+4HlmG6z8UXNCYuFRqlcqr0AnhibnyBeAyj/hHrYhuT45xm9chmHX7uWiPs/vncEux36fimTo96o0M92faQo94x9nN0FgXF91hYyqYe/sN67/RI3qYJaakxYVcxWbeehsX0r83IGIMY201sXoKICloI3VVti5q8g5x7rAaXoRTrluR0QZ2GvHv82Xv5IzhMKH70wFNEWiqGaUUpcd5PYiIRCptBcyOCnaQliSL8f9kghDxc32WCrHj85yedq92k6CkEFhob6ZR1kFQvKSJfZKSSBrAWhTuAjJ3oXudUXqZA79GR1+OOdHBfMu8p8cvGhvoeaKVaWgv1JgjNY65eH0megtNhCrOjzxKhVdhT9MWfkACkocxETzamDZz/7xkxqyQk6N+IXxPbg1JtWgKKufHFuc2VWuCIjGHGQqUgMMij67m/z4fwMqiD16GPUEzjg+jo7EfmWqSWj9LdcaauFix3w16X9oyOPlN65b/jahrX5YTEcEY3ZsMEEmafqkg1tLUWSoh3FapLBxvyKf6bUxx7oqgH+B3YgwRR0nYTArWEzsTtMKvJnazYI68jVP7WhLaoQ+ucuIDdqkAThrYodnsv9sKnO0aWLmfTScwU+Bb0J+6s0QuSyYftGcAWQpIKxf8iKEczxTUiSJz4l8ousc15cfz/EsbYuPhpCDerMZ272p5Ai4oRc5bZW/poFf2ZAfBhy5cJbaQ0Yzttu/Ku7FnNvg4C3C+GXlq7o5jxfrFQJonkknhClHvAGAxIVI5Ds7MGFn2mNF8Y3xEmBQ5A02NFoFEPAizQia13+LMfYWSuAaax9Fbw3M9987zJU+vyYeR9yd70E3NvfZPqY4qtYLpRQyH8LneyNRQ8FPKmJ+B+ppS167UYIotrNS1NQeYBxwDb9Wc3pXXC5YTq1pZkaZGJyxahQU1ke6jUXUR56rCUBuo3XEjsJyYhsICOvfmZd6OcfUCU066bSaZlvcJVZsaQOXwen5/iW3pnmASdyyL7dVOBtWfSZ6XHs3gLPEcH/Iq8HNbgmnNCq1syt+xQAAAAUAAAAFAAAABQAAAAUAAAA`
+ data, err := base64.StdEncoding.DecodeString(encoded)
+ require.NoError(t, err)
+
+ cfg := clparams.MainnetBeaconConfig
+ block := NewSignedBeaconBlock(&cfg, clparams.GloasVersion)
+ require.NoError(t, block.DecodeSSZ(data, int(clparams.GloasVersion)))
+
+ bodyRoot, err := block.Block.Body.HashSSZ()
+ require.NoError(t, err)
+ require.Equal(t, common.HexToHash("0x2a362b974b66fe01e8aa8d6b012340c5fb1a2faeec0aee8b010638b949999a8e"), common.Hash(bodyRoot))
+
+ blockRoot, err := block.Block.HashSSZ()
+ require.NoError(t, err)
+ require.Equal(t, common.HexToHash("0x44b8ee5de972ae05b8b103e18e06feb92a03cb817f5e05a5e66b6d282bf542cc"), common.Hash(blockRoot))
+}
diff --git a/cl/cltypes/indexed_attestation.go b/cl/cltypes/indexed_attestation.go
index dbd182bc522..a68f4f9f1df 100644
--- a/cl/cltypes/indexed_attestation.go
+++ b/cl/cltypes/indexed_attestation.go
@@ -39,6 +39,7 @@ type IndexedAttestation struct {
AttestingIndices *solid.RawUint64List `json:"attesting_indices"`
Data *solid.AttestationData `json:"data"`
Signature common.Bytes96 `json:"signature"`
+ version clparams.StateVersion
}
func NewIndexedAttestation(version clparams.StateVersion) *IndexedAttestation {
@@ -60,6 +61,7 @@ func NewIndexedAttestationWithConfig(version clparams.StateVersion, cfg *clparam
return &IndexedAttestation{
AttestingIndices: solid.NewRawUint64List(attLimit, []uint64{}),
Data: &solid.AttestationData{},
+ version: version,
}
}
@@ -70,6 +72,7 @@ func (i *IndexedAttestation) SetVersion(v clparams.StateVersion) {
// SetVersionWithConfig sets the version and adjusts the attesting indices limit based on config.
// If cfg is nil, mainnet defaults are used.
func (i *IndexedAttestation) SetVersionWithConfig(v clparams.StateVersion, cfg *clparams.BeaconChainConfig) {
+ i.version = v
if v >= clparams.ElectraVersion {
limit := attestingIndicesLimitElectra
if cfg != nil && cfg.MaxCommitteesPerSlot > 0 {
@@ -123,6 +126,7 @@ func (i *IndexedAttestation) DecodeSSZ(buf []byte, version int) error {
// DecodeSSZWithConfig ssz unmarshals the IndexedAttestation object with preset-aware limits.
// If cfg is nil, mainnet defaults are used.
func (i *IndexedAttestation) DecodeSSZWithConfig(buf []byte, version int, cfg *clparams.BeaconChainConfig) error {
+ i.version = clparams.StateVersion(version)
i.Data = &solid.AttestationData{}
if version >= int(clparams.ElectraVersion) {
limit := attestingIndicesLimitElectra
@@ -144,9 +148,20 @@ func (i *IndexedAttestation) EncodingSizeSSZ() int {
// HashSSZ ssz hashes the IndexedAttestation object
func (i *IndexedAttestation) HashSSZ() ([32]byte, error) {
+ if i.version >= clparams.GloasVersion {
+ return i.HashSSZProgressive()
+ }
return merkle_tree.HashTreeRoot(i.AttestingIndices, i.Data, i.Signature[:])
}
+func (i *IndexedAttestation) HashSSZProgressive() ([32]byte, error) {
+ indices, err := i.AttestingIndices.HashSSZProgressive()
+ if err != nil {
+ return [32]byte{}, err
+ }
+ return merkle_tree.ProgressiveContainerRootAll(indices[:], i.Data, i.Signature[:])
+}
+
func IsSlashableAttestationData(d1, d2 *solid.AttestationData) bool {
return (!d1.Equal(d2) && d1.Target.Epoch == d2.Target.Epoch) ||
(d1.Source.Epoch < d2.Source.Epoch && d2.Target.Epoch < d1.Target.Epoch)
diff --git a/cl/cltypes/light_client.go b/cl/cltypes/light_client.go
index 8ec3e4fbf47..3d70e9970e7 100644
--- a/cl/cltypes/light_client.go
+++ b/cl/cltypes/light_client.go
@@ -31,10 +31,8 @@ const (
// FINALIZED_ROOT_GINDEX get_generalized_index(altair.BeaconState, 'finalized_checkpoint', 'root') (= 105)
// CURRENT_SYNC_COMMITTEE_GINDEX get_generalized_index(altair.BeaconState, 'current_sync_committee') (= 54)
// NEXT_SYNC_COMMITTEE_GINDEX get_generalized_index(altair.BeaconState, 'next_sync_committee') (= 55)
- ExecutionBranchSize = 4
- // EXECUTION_BLOCK_HASH_GINDEX_GLOAS = get_generalized_index(BeaconBlockBody, 'signed_execution_payload_bid', 'message', 'parent_block_hash') (= 832)
- // floorlog2(832) = 9
- ExecutionBranchSizeGloas = 9
+ ExecutionBranchSize = 4
+ ExecutionBranchSizeGloas = 11
SyncCommitteeBranchSize = 5
CurrentSyncCommitteeBranchSize = 5
FinalizedBranchSize = 6
@@ -45,6 +43,8 @@ const (
SyncCommitteeBranchSizeElectra = 6
CurrentSyncCommitteeBranchSizeElectra = 6
FinalizedBranchSizeElectra = 7
+ CurrentSyncCommitteeBranchSizeGloas = 11
+ FinalizedBranchSizeGloas = 9
)
type LightClientHeader struct {
@@ -427,6 +427,9 @@ func (l *LightClientOptimisticUpdate) Clone() clonable.Clonable {
}
func getCurrentSyncCommitteeBranchSize(version clparams.StateVersion) int {
+ if version >= clparams.GloasVersion {
+ return CurrentSyncCommitteeBranchSizeGloas
+ }
if version >= clparams.ElectraVersion {
return CurrentSyncCommitteeBranchSizeElectra
}
@@ -434,6 +437,9 @@ func getCurrentSyncCommitteeBranchSize(version clparams.StateVersion) int {
}
func getFinalizedBranchSize(version clparams.StateVersion) int {
+ if version >= clparams.GloasVersion {
+ return FinalizedBranchSizeGloas
+ }
if version >= clparams.ElectraVersion {
return FinalizedBranchSizeElectra
}
diff --git a/cl/cltypes/partial_data_column.go b/cl/cltypes/partial_data_column.go
index 58f01fa1da6..ce1bc35133c 100644
--- a/cl/cltypes/partial_data_column.go
+++ b/cl/cltypes/partial_data_column.go
@@ -130,10 +130,18 @@ func (s *PartialDataColumnSidecar) init() {
s.CellsPresentBitmap = solid.NewBitList(0, int(cfg.MaxBlobCommittmentsPerBlock))
}
if s.PartialColumn == nil {
- s.PartialColumn = solid.NewStaticListSSZ[*Cell](int(cfg.MaxBlobCommittmentsPerBlock), BytesPerCell)
+ if s.version >= clparams.GloasVersion {
+ s.PartialColumn = solid.NewStaticProgressiveListSSZ[*Cell](int(cfg.MaxBlobCommittmentsPerBlock), BytesPerCell)
+ } else {
+ s.PartialColumn = solid.NewStaticListSSZ[*Cell](int(cfg.MaxBlobCommittmentsPerBlock), BytesPerCell)
+ }
}
if s.KzgProofs == nil {
- s.KzgProofs = solid.NewStaticListSSZ[*KZGProof](int(cfg.MaxBlobCommittmentsPerBlock), 48)
+ if s.version >= clparams.GloasVersion {
+ s.KzgProofs = solid.NewStaticProgressiveListSSZ[*KZGProof](int(cfg.MaxBlobCommittmentsPerBlock), 48)
+ } else {
+ s.KzgProofs = solid.NewStaticListSSZ[*KZGProof](int(cfg.MaxBlobCommittmentsPerBlock), 48)
+ }
}
if s.Header == nil {
s.Header = solid.NewDynamicListSSZ[*PartialDataColumnHeader](1)
@@ -144,6 +152,8 @@ func (s *PartialDataColumnSidecar) Version() clparams.StateVersion { return s.ve
func (s *PartialDataColumnSidecar) SetVersion(v clparams.StateVersion) {
s.version = v
+ s.PartialColumn = nil
+ s.KzgProofs = nil
s.init()
}
@@ -162,6 +172,8 @@ func (s *PartialDataColumnSidecar) EncodeSSZ(buf []byte) ([]byte, error) {
func (s *PartialDataColumnSidecar) DecodeSSZ(buf []byte, version int) error {
s.version = clparams.StateVersion(version)
+ s.PartialColumn = nil
+ s.KzgProofs = nil
s.init()
return ssz2.UnmarshalSSZ(buf, version, s.getSchema()...)
}
@@ -178,6 +190,21 @@ func (s *PartialDataColumnSidecar) EncodingSizeSSZ() int {
}
func (s *PartialDataColumnSidecar) HashSSZ() ([32]byte, error) {
+ if s.version >= clparams.GloasVersion {
+ bitmapRoot, err := s.CellsPresentBitmap.HashSSZProgressive()
+ if err != nil {
+ return [32]byte{}, err
+ }
+ partialColumnRoot, err := s.PartialColumn.HashSSZProgressive(nil)
+ if err != nil {
+ return [32]byte{}, err
+ }
+ proofsRoot, err := s.KzgProofs.HashSSZProgressive(nil)
+ if err != nil {
+ return [32]byte{}, err
+ }
+ return merkle_tree.HashTreeRoot(bitmapRoot[:], partialColumnRoot[:], proofsRoot[:])
+ }
return merkle_tree.HashTreeRoot(s.getSchema()...)
}
diff --git a/cl/cltypes/slashings.go b/cl/cltypes/slashings.go
index 542e0a338a7..ca5276b989d 100644
--- a/cl/cltypes/slashings.go
+++ b/cl/cltypes/slashings.go
@@ -90,3 +90,15 @@ func (a *AttesterSlashing) EncodingSizeSSZ() int {
func (a *AttesterSlashing) HashSSZ() ([32]byte, error) {
return merkle_tree.HashTreeRoot(a.Attestation_1, a.Attestation_2)
}
+
+func (a *AttesterSlashing) HashSSZProgressive() ([32]byte, error) {
+ first, err := a.Attestation_1.HashSSZProgressive()
+ if err != nil {
+ return [32]byte{}, err
+ }
+ second, err := a.Attestation_2.HashSSZProgressive()
+ if err != nil {
+ return [32]byte{}, err
+ }
+ return merkle_tree.HashTreeRoot(first[:], second[:])
+}
diff --git a/cl/cltypes/solid/attestation.go b/cl/cltypes/solid/attestation.go
index aee0086ca58..44a9d08d80e 100644
--- a/cl/cltypes/solid/attestation.go
+++ b/cl/cltypes/solid/attestation.go
@@ -21,6 +21,7 @@ import (
"encoding/binary"
"encoding/json"
"errors"
+ "fmt"
"github.com/erigontech/erigon/cl/clparams"
"github.com/erigontech/erigon/cl/merkle_tree"
@@ -43,6 +44,11 @@ type Attestation struct {
Data *AttestationData `json:"data"`
Signature common.Bytes96 `json:"signature"`
CommitteeBits *BitVector `json:"committee_bits,omitempty"` // Electra EIP-7549
+ version clparams.StateVersion
+}
+
+func (a *Attestation) SetVersion(version clparams.StateVersion) {
+ a.version = version
}
func (a *Attestation) GetCommitteeIndexFromBits() (uint64, error) {
@@ -63,6 +69,33 @@ func (a *Attestation) SetBeaconConfig(cfg *clparams.BeaconChainConfig) {
a.AggregationBits.SetLimit(int(cfg.MaxCommitteesPerSlot) * maxValidatorsPerCommittee)
}
+func (a *Attestation) ValidateForConfig(cfg *clparams.BeaconChainConfig, version clparams.StateVersion) error {
+ if a == nil || cfg == nil || a.AggregationBits == nil || a.Data == nil {
+ return errors.New("invalid attestation")
+ }
+ aggregationBitsLimit := maxValidatorsPerCommittee
+ if version >= clparams.ElectraVersion {
+ aggregationBitsLimit = int(cfg.MaxCommitteesPerSlot) * maxValidatorsPerCommittee
+ }
+ if a.AggregationBits.Bits() > aggregationBitsLimit {
+ return fmt.Errorf("aggregation bits length exceeds limit: %d > %d", a.AggregationBits.Bits(), aggregationBitsLimit)
+ }
+ a.AggregationBits.SetLimit(aggregationBitsLimit)
+ if version < clparams.ElectraVersion {
+ if a.CommitteeBits != nil {
+ return errors.New("committee bits before Electra")
+ }
+ return nil
+ }
+ if a.CommitteeBits == nil {
+ return errors.New("missing committee bits after Electra")
+ }
+ if err := a.CommitteeBits.ValidateSize(int(cfg.MaxCommitteesPerSlot)); err != nil {
+ return fmt.Errorf("invalid committee bits: %w", err)
+ }
+ return nil
+}
+
// Static returns whether the attestation is static or not. For Attestation, it's always false.
func (*Attestation) Static() bool {
return false
@@ -75,6 +108,7 @@ func (a *Attestation) Copy() *Attestation {
*new.Data = *a.Data
copy(new.Signature[:], a.Signature[:])
new.CommitteeBits = a.CommitteeBits.Copy()
+ new.version = a.version
return new
}
@@ -94,16 +128,15 @@ func (a *Attestation) EncodingSizeSSZ() (size int) {
return size + a.AggregationBits.EncodingSizeSSZ() + 4 // 4 bytes for the length of the size offset
}
-// DecodeSSZ decodes the provided buffer into the Attestation instance.
+// DecodeSSZ infers the committee vector width for preset-agnostic nested decoding; trust boundaries must call ValidateForConfig.
func (a *Attestation) DecodeSSZ(buf []byte, version int) error {
return a.DecodeSSZWithConfig(buf, version, nil)
}
-// DecodeSSZWithConfig decodes the provided buffer into the Attestation instance,
-// using the given beacon config to determine preset-aware limits (e.g. minimal vs mainnet).
-// If cfg is nil, mainnet defaults are used.
+// DecodeSSZWithConfig decodes the provided buffer using cfg when it is available.
func (a *Attestation) DecodeSSZWithConfig(buf []byte, version int, cfg *clparams.BeaconChainConfig) error {
clversion := clparams.StateVersion(version)
+ a.version = clversion
if clversion.AfterOrEqual(clparams.ElectraVersion) {
// The CommitteeBits size depends on MAX_COMMITTEES_PER_SLOT which differs between
// mainnet (64) and the minimal preset (4). Instead of hardcoding 64, infer the
@@ -118,13 +151,21 @@ func (a *Attestation) DecodeSSZWithConfig(buf []byte, version int, cfg *clparams
if committeeBitsBytes <= 0 {
return ssz.ErrLowBufferSize
}
+ committeeBitsLimit := committeeBitsBytes * 8
+ if cfg != nil && cfg.MaxCommitteesPerSlot > 0 {
+ committeeBitsLimit = int(cfg.MaxCommitteesPerSlot)
+ expectedBytes := (committeeBitsLimit + 7) / 8
+ if committeeBitsBytes != expectedBytes {
+ return fmt.Errorf("invalid committee bits byte length: %d != %d", committeeBitsBytes, expectedBytes)
+ }
+ }
aggrBitsLimit := aggregationBitsSizeElectra
if cfg != nil && cfg.MaxCommitteesPerSlot > 0 {
aggrBitsLimit = int(cfg.MaxCommitteesPerSlot) * maxValidatorsPerCommittee
}
a.AggregationBits = NewBitList(0, aggrBitsLimit)
a.Data = &AttestationData{}
- a.CommitteeBits = NewBitVector(committeeBitsBytes * 8)
+ a.CommitteeBits = NewBitVector(committeeBitsLimit)
return ssz2.UnmarshalSSZ(buf, version, a.AggregationBits, a.Data, a.Signature[:], a.CommitteeBits)
}
@@ -148,6 +189,9 @@ func (a *Attestation) EncodeSSZ(dst []byte) ([]byte, error) {
// HashSSZ hashes the Attestation instance using SSZ.
func (a *Attestation) HashSSZ() (o [32]byte, err error) {
+ if a.version >= clparams.GloasVersion {
+ return a.HashSSZProgressive()
+ }
if a.CommitteeBits != nil {
// Electra case
return merkle_tree.HashTreeRoot(a.AggregationBits, a.Data, a.Signature[:], a.CommitteeBits)
@@ -155,6 +199,17 @@ func (a *Attestation) HashSSZ() (o [32]byte, err error) {
return merkle_tree.HashTreeRoot(a.AggregationBits, a.Data, a.Signature[:])
}
+func (a *Attestation) HashSSZProgressive() ([32]byte, error) {
+ aggregationBitsRoot, err := a.AggregationBits.HashSSZProgressive()
+ if err != nil {
+ return [32]byte{}, err
+ }
+ if a.CommitteeBits != nil {
+ return merkle_tree.ProgressiveContainerRootAll(aggregationBitsRoot[:], a.Data, a.Signature[:], a.CommitteeBits)
+ }
+ return merkle_tree.ProgressiveContainerRootAll(aggregationBitsRoot[:], a.Data, a.Signature[:])
+}
+
// Clone creates a new clone of the Attestation instance.
func (a *Attestation) Clone() clonable.Clonable {
return &Attestation{}
@@ -255,12 +310,16 @@ func (s *SingleAttestation) ToAttestation(memberIndexInCommittee int, committeeL
aggrBitsLimit = int(cfg.MaxCommitteesPerSlot) * maxValidatorsPerCommittee
}
aggregationBits := BitlistFromBytes(bytes, aggrBitsLimit)
- return &Attestation{
+ attestation := &Attestation{
AggregationBits: aggregationBits,
Data: s.Data,
Signature: s.Signature,
CommitteeBits: committeeBits,
}
+ if cfg != nil && cfg.SlotsPerEpoch > 0 && s.Data != nil {
+ attestation.SetVersion(cfg.GetCurrentStateVersion(s.Data.Slot / cfg.SlotsPerEpoch))
+ }
+ return attestation
}
func (s *SingleAttestation) AttestationData() *AttestationData {
diff --git a/cl/cltypes/solid/attestation_config_test.go b/cl/cltypes/solid/attestation_config_test.go
new file mode 100644
index 00000000000..3ec8054fe31
--- /dev/null
+++ b/cl/cltypes/solid/attestation_config_test.go
@@ -0,0 +1,49 @@
+package solid
+
+import (
+ "encoding/binary"
+ "testing"
+
+ "github.com/erigontech/erigon/cl/clparams"
+ "github.com/stretchr/testify/require"
+)
+
+func TestAttestationDecodeSSZWithConfigRejectsWrongCommitteeBitsSize(t *testing.T) {
+ cfg := clparams.MainnetBeaconConfig
+ cfg.MaxCommitteesPerSlot = 4
+ attestation := &Attestation{
+ AggregationBits: BitlistFromBytes([]byte{1}, int(cfg.MaxCommitteesPerSlot)*maxValidatorsPerCommittee),
+ Data: &AttestationData{},
+ CommitteeBits: NewBitVector(int(cfg.MaxCommitteesPerSlot)),
+ }
+ encoded, err := attestation.EncodeSSZ(nil)
+ require.NoError(t, err)
+ offset := int(binary.LittleEndian.Uint32(encoded[:4]))
+ malformed := append([]byte(nil), encoded[:offset]...)
+ malformed = append(malformed, 0)
+ malformed = append(malformed, encoded[offset:]...)
+ binary.LittleEndian.PutUint32(malformed[:4], uint32(offset+1))
+
+ decoded := &Attestation{}
+ require.Error(t, decoded.DecodeSSZWithConfig(malformed, int(clparams.GloasVersion), &cfg))
+}
+
+func TestAttestationValidateForConfigNormalizesJSONCommitteeBits(t *testing.T) {
+ cfg := clparams.MainnetBeaconConfig
+ cfg.MaxCommitteesPerSlot = 4
+ committeeBits := &BitVector{}
+ require.NoError(t, committeeBits.UnmarshalJSON([]byte(`"0x01"`)))
+ attestation := &Attestation{
+ AggregationBits: BitlistFromBytes([]byte{1}, int(cfg.MaxCommitteesPerSlot)*maxValidatorsPerCommittee),
+ Data: &AttestationData{},
+ CommitteeBits: committeeBits,
+ }
+
+ require.NoError(t, attestation.ValidateForConfig(&cfg, clparams.GloasVersion))
+ require.Equal(t, 4, attestation.CommitteeBits.BitCap())
+
+ tooLong := &BitVector{}
+ require.NoError(t, tooLong.UnmarshalJSON([]byte(`"0x0100"`)))
+ attestation.CommitteeBits = tooLong
+ require.Error(t, attestation.ValidateForConfig(&cfg, clparams.GloasVersion))
+}
diff --git a/cl/cltypes/solid/bitlist.go b/cl/cltypes/solid/bitlist.go
index 88d7f71a87a..40fad079bac 100644
--- a/cl/cltypes/solid/bitlist.go
+++ b/cl/cltypes/solid/bitlist.go
@@ -19,6 +19,7 @@ package solid
import (
"encoding/json"
"errors"
+ "fmt"
"math/bits"
"slices"
@@ -188,6 +189,17 @@ func (u *BitList) HashSSZ() ([32]byte, error) {
return merkle_tree.BitlistRootWithLimit(u.u[:u.l], uint64(u.c))
}
+func (u *BitList) HashSSZProgressive() ([32]byte, error) {
+ bytes := u.Bytes()
+ bitLength := bitlistBits(bytes)
+ packed := append([]byte(nil), bytes...)
+ if bitLength < len(packed)*8 {
+ packed[bitLength/8] &^= 1 << uint(bitLength%8)
+ }
+ packed = packed[:(bitLength+7)/8]
+ return merkle_tree.ProgressiveBitlistRoot(packed, uint64(bitLength))
+}
+
// EncodeSSZ appends the underlying byte slice of the BitList to the destination byte slice.
// It returns the resulting byte slice.
func (u *BitList) EncodeSSZ(dst []byte) ([]byte, error) {
@@ -197,6 +209,13 @@ func (u *BitList) EncodeSSZ(dst []byte) ([]byte, error) {
// DecodeSSZ replaces the underlying byte slice of the BitList with a copy of the input byte slice.
// It then updates the length of the BitList to match the length of the new byte slice.
func (u *BitList) DecodeSSZ(dst []byte, _ int) error {
+ if len(dst) == 0 || dst[len(dst)-1] == 0 {
+ return errors.New("invalid bitlist: missing length bit")
+ }
+ bitLength := 8*(len(dst)-1) + bits.Len8(dst[len(dst)-1]) - 1
+ if bitLength > u.c {
+ return fmt.Errorf("invalid bitlist length: %d > %d", bitLength, u.c)
+ }
u.u = make([]byte, len(dst))
copy(u.u, dst)
u.l = len(dst)
@@ -217,13 +236,17 @@ func (u *BitList) Clone() clonable.Clonable {
// getBitlistLength return the amount of bits in given bitlist.
func (u *BitList) Bits() int {
- if len(u.u) == 0 {
+ return bitlistBits(u.Bytes())
+}
+
+func bitlistBits(data []byte) int {
+ if len(data) == 0 {
return 0
}
// The most significant bit is present in the last byte in the array.
var last byte
var byteLen int
- for i, b := range slices.Backward(u.u) {
+ for i, b := range slices.Backward(data) {
if b != 0 {
last = b
byteLen = i + 1
@@ -260,16 +283,18 @@ func (u *BitList) UnmarshalJSON(input []byte) error {
}
func (u *BitList) Merge(other *BitList) (*BitList, error) {
+ uBytes, otherBytes := u.Bytes(), other.Bytes()
+ if len(uBytes) != len(otherBytes) {
+ return nil, errors.New("bitlist union: different length")
+ }
if u.Bits() != other.Bits() {
log.Warn("bitlist union: different length", "u", u.Bits(), "other", other.Bits())
return nil, errors.New("bitlist union: different length")
}
- // copy by the longer one
- var ret, unionFrom *BitList
- ret = other.Copy()
- unionFrom = u
- for i := 0; i < len(unionFrom.u); i++ {
- ret.u[i] |= unionFrom.u[i]
+ ret := other.Copy()
+ retBytes := ret.Bytes()
+ for i := range uBytes {
+ retBytes[i] |= uBytes[i]
}
return ret, nil
}
diff --git a/cl/cltypes/solid/bitlist_test.go b/cl/cltypes/solid/bitlist_test.go
index 8b4d1b893ec..dfb0c3a1773 100644
--- a/cl/cltypes/solid/bitlist_test.go
+++ b/cl/cltypes/solid/bitlist_test.go
@@ -41,6 +41,50 @@ func TestBitListClear(t *testing.T) {
require.Zero(BitList.Length(), "BitList Clear did not reset the length to zero")
}
+func TestBitListProgressiveHashIgnoresBackingBytesPastLength(t *testing.T) {
+ bitlist := solid.NewBitList(1, 2048)
+ bitlist.Set(0, 0b00000010)
+ bitlist.Set(32, 1)
+ want, err := solid.BitlistFromBytes([]byte{0b00000010}, 2048).HashSSZProgressive()
+ require.NoError(t, err)
+
+ var got [32]byte
+ require.NotPanics(t, func() {
+ got, err = bitlist.HashSSZProgressive()
+ })
+ require.NoError(t, err)
+ require.Equal(t, want, got)
+}
+
+func TestBitListBitsIgnoresBackingBytesPastLength(t *testing.T) {
+ bitlist := solid.NewBitList(1, 2048)
+ bitlist.Set(0, 0b00000010)
+ bitlist.Set(32, 1)
+
+ require.Equal(t, 1, bitlist.Bits())
+}
+
+func TestBitListMergeIgnoresBackingBytesPastLength(t *testing.T) {
+ padded := solid.NewBitList(1, 2048)
+ padded.Set(0, 0b00000010)
+ padded.Set(32, 1)
+ compact := solid.BitlistFromBytes([]byte{0b00000010}, 2048)
+
+ for _, test := range []struct {
+ name string
+ left, right *solid.BitList
+ }{
+ {name: "padded into compact", left: padded, right: compact},
+ {name: "compact into padded", left: compact, right: padded},
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ merged, err := test.left.Merge(test.right)
+ require.NoError(t, err)
+ require.Equal(t, []byte{0b00000010}, merged.Bytes())
+ })
+ }
+}
+
func TestBitListCopyTo(t *testing.T) {
require := require.New(t)
@@ -123,6 +167,30 @@ func TestBitListCap(t *testing.T) {
require.Equal(10, capacity, "BitList Cap did not return the expected value")
}
+func TestBitListDecodeSSZRejectsNonCanonicalEncoding(t *testing.T) {
+ tests := []struct {
+ name string
+ data []byte
+ }{
+ {"empty", nil},
+ {"missing delimiter", []byte{0}},
+ {"trailing zero", []byte{1, 0}},
+ {"over limit", []byte{0, 2}},
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ bitlist := solid.NewBitList(0, 8)
+ require.Error(t, bitlist.DecodeSSZ(test.data, 0))
+ })
+ }
+}
+
+func TestBitListDecodeSSZAcceptsLimit(t *testing.T) {
+ bitlist := solid.NewBitList(0, 8)
+ require.NoError(t, bitlist.DecodeSSZ([]byte{0, 1}, 0))
+ require.Equal(t, 8, bitlist.Bits())
+}
+
// Add more tests as needed for other functions in the BitList struct.
func TestBitlistMerge(t *testing.T) {
diff --git a/cl/cltypes/solid/bitvector.go b/cl/cltypes/solid/bitvector.go
index ed9b7ff0b94..1f1f28f2c43 100644
--- a/cl/cltypes/solid/bitvector.go
+++ b/cl/cltypes/solid/bitvector.go
@@ -117,12 +117,40 @@ func (b *BitVector) EncodingSizeSSZ() int {
}
func (b *BitVector) DecodeSSZ(buf []byte, _ int) error {
+ if err := b.validateBytes(buf, b.bitCap); err != nil {
+ return err
+ }
b.bitLen = b.bitCap // bitCap must be set before decoding by NewBitVector
b.container = make([]byte, b.EncodingSizeSSZ())
copy(b.container, buf)
return nil
}
+func (b *BitVector) ValidateSize(bitCap int) error {
+ if b == nil {
+ return errors.New("nil bitvector")
+ }
+ if err := b.validateBytes(b.container, bitCap); err != nil {
+ return err
+ }
+ b.bitCap = bitCap
+ b.bitLen = bitCap
+ return nil
+}
+
+func (b *BitVector) validateBytes(buf []byte, bitCap int) error {
+ expectedBytes := (bitCap + 7) / 8
+ if len(buf) != expectedBytes {
+ return fmt.Errorf("invalid bitvector byte length: %d != %d", len(buf), expectedBytes)
+ }
+ if remainder := bitCap % 8; remainder != 0 && len(buf) > 0 {
+ if buf[len(buf)-1]&byte(0xff< 0 && l.bytesPerElement == bytesPerElement {
+ l.limit = progressiveDecodeLimit(l.limit)
+ } else {
+ l.limit = progressiveDecodeLimit(limit)
+ l.static = true
+ l.bytesPerElement = bytesPerElement
+ }
+ l.progressive = true
+}
+
+// Progressive lists are semantically unbounded, so decode limits are resource guards rather than protocol maxima.
+func progressiveDecodeLimit(semanticLimit int) int {
+ const minimum = 16
+ if semanticLimit <= minimum/2 {
+ return minimum
+ }
+ maxInt := int(^uint(0) >> 1)
+ if semanticLimit > maxInt/2 {
+ return maxInt
+ }
+ return semanticLimit * 2
+}
+
func (l ListSSZ[T]) MarshalJSON() ([]byte, error) {
return json.Marshal(l.list)
}
@@ -104,10 +140,11 @@ func (l *ListSSZ[T]) EncodeSSZ(buf []byte) (dst []byte, err error) {
}
func (l *ListSSZ[T]) DecodeSSZ(buf []byte, version int) (err error) {
+ limit := uint64(l.limit)
if l.static {
- l.list, err = ssz.DecodeStaticList[T](buf, 0, uint32(len(buf)), uint32(l.bytesPerElement), uint64(l.limit), version)
+ l.list, err = ssz.DecodeStaticList[T](buf, 0, uint32(len(buf)), uint32(l.bytesPerElement), limit, version)
} else {
- l.list, err = ssz.DecodeDynamicList[T](buf, 0, uint32(len(buf)), uint64(l.limit), version)
+ l.list, err = ssz.DecodeDynamicList[T](buf, 0, uint32(len(buf)), limit, version)
}
l.root = common.Hash{}
return
@@ -129,11 +166,40 @@ func (l *ListSSZ[T]) HashSSZ() ([32]byte, error) {
return l.root, nil
}
var err error
- l.root, err = merkle_tree.ListObjectSSZRoot(l.list, uint64(l.limit))
+ if l.progressive {
+ l.root, err = l.HashSSZProgressive(nil)
+ } else {
+ l.root, err = merkle_tree.ListObjectSSZRoot(l.list, uint64(l.limit))
+ }
return l.root, err
}
+func (l *ListSSZ[T]) HashSSZProgressive(hashElement func(T) ([32]byte, error)) ([32]byte, error) {
+ roots := make([][32]byte, len(l.list))
+ for i, element := range l.list {
+ var err error
+ if hashElement == nil {
+ roots[i], err = element.HashSSZ()
+ } else {
+ roots[i], err = hashElement(element)
+ }
+ if err != nil {
+ return [32]byte{}, err
+ }
+ }
+ return merkle_tree.ProgressiveListRoot(roots, uint64(len(l.list)))
+}
+
func (l *ListSSZ[T]) Clone() clonable.Clonable {
+ if l.progressive {
+ return &ListSSZ[T]{
+ list: make([]T, 0),
+ limit: l.limit,
+ static: l.static,
+ bytesPerElement: l.bytesPerElement,
+ progressive: true,
+ }
+ }
if l.static {
return NewStaticListSSZ[T](l.limit, l.bytesPerElement)
}
@@ -212,6 +278,7 @@ func (l *ListSSZ[T]) ShallowCopy() *ListSSZ[T] {
limit: l.limit,
static: l.static,
bytesPerElement: l.bytesPerElement,
+ progressive: l.progressive,
root: common.Hash(bytes.Clone(l.root[:])),
}
copy(cpy.list, l.list)
diff --git a/cl/cltypes/solid/list_ssz_test.go b/cl/cltypes/solid/list_ssz_test.go
index e09a7393d3e..8656b4e53a5 100644
--- a/cl/cltypes/solid/list_ssz_test.go
+++ b/cl/cltypes/solid/list_ssz_test.go
@@ -17,9 +17,11 @@
package solid
import (
+ "errors"
"testing"
"github.com/erigontech/erigon/common"
+ "github.com/erigontech/erigon/common/ssz"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -90,6 +92,44 @@ func TestListSSZEncodeDecodeSSZ(t *testing.T) {
assert.Equal(t, list.Len(), decodedList.Len())
}
+func TestProgressiveListSSZDecodeEnforcesLimit(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ list *ListSSZ[Validator]
+ }{
+ {
+ name: "static",
+ list: NewStaticProgressiveListSSZ[Validator](1, validatorSize),
+ },
+ {
+ name: "dynamic",
+ list: NewDynamicProgressiveListSSZ[Validator](1),
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ var source *ListSSZ[Validator]
+ if tc.list.static {
+ source = NewStaticListSSZ[Validator](tc.list.limit+1, validatorSize)
+ } else {
+ source = NewDynamicListSSZ[Validator](tc.list.limit + 1)
+ }
+ for range tc.list.limit {
+ source.Append(NewValidator())
+ }
+ encoded, err := source.EncodeSSZ(nil)
+ require.NoError(t, err)
+ require.NoError(t, tc.list.DecodeSSZ(encoded, 0))
+
+ source.Append(NewValidator())
+ encoded, err = source.EncodeSSZ(nil)
+ require.NoError(t, err)
+ err = tc.list.DecodeSSZ(encoded, 0)
+ require.Error(t, err)
+ assert.True(t, errors.Is(err, ssz.ErrTooBigList), "expected ErrTooBigList, got %v", err)
+ })
+ }
+}
+
func TestUint64VectorSSZ(t *testing.T) {
// Test NewUint64VectorSSZ
size := 5
diff --git a/cl/cltypes/solid/participation_bitlist.go b/cl/cltypes/solid/participation_bitlist.go
index 664271b9e2b..0f30a618b57 100644
--- a/cl/cltypes/solid/participation_bitlist.go
+++ b/cl/cltypes/solid/participation_bitlist.go
@@ -147,6 +147,10 @@ func (u *ParticipationBitList) HashSSZ() ([32]byte, error) {
return crypto.Sha256(baseRoot[:], lengthRoot[:]), nil
}
+func (u *ParticipationBitList) HashSSZProgressive() ([32]byte, error) {
+ return merkle_tree.ProgressiveBasicListRoot(u.Bytes(), uint64(u.l))
+}
+
func (arr *ParticipationBitList) getBaseHash(xs []byte, depth uint8) error {
elements := arr.u
offset := 32*(arr.l/32) + 32
diff --git a/cl/cltypes/solid/transactions.go b/cl/cltypes/solid/transactions.go
index 31239c7c7c1..5eaeeba8096 100644
--- a/cl/cltypes/solid/transactions.go
+++ b/cl/cltypes/solid/transactions.go
@@ -147,6 +147,18 @@ func (t *TransactionsSSZ) HashSSZ() ([32]byte, error) {
return t.root, err
}
+func (t *TransactionsSSZ) HashSSZProgressive() ([32]byte, error) {
+ roots := make([][32]byte, len(t.underlying))
+ for i, transaction := range t.underlying {
+ root, err := merkle_tree.ProgressiveBasicListRoot(transaction, uint64(len(transaction)))
+ if err != nil {
+ return [32]byte{}, err
+ }
+ roots[i] = root
+ }
+ return merkle_tree.ProgressiveListRoot(roots, uint64(len(roots)))
+}
+
func (t *TransactionsSSZ) EncodingSizeSSZ() (size int) {
if t == nil {
return 0
diff --git a/cl/cltypes/solid/uint64_raw_list.go b/cl/cltypes/solid/uint64_raw_list.go
index b051d3842be..c4358551b08 100644
--- a/cl/cltypes/solid/uint64_raw_list.go
+++ b/cl/cltypes/solid/uint64_raw_list.go
@@ -19,6 +19,7 @@ package solid
import (
"encoding/binary"
"encoding/json"
+ "fmt"
"strconv"
"github.com/erigontech/erigon/cl/merkle_tree"
@@ -102,6 +103,12 @@ func (arr *RawUint64List) EncodeSSZ(buf []byte) (dst []byte, err error) {
}
func (arr *RawUint64List) DecodeSSZ(buf []byte, _ int) error {
+ if len(buf)%8 != 0 {
+ return fmt.Errorf("invalid uint64 list byte length: %d", len(buf))
+ }
+ if len(buf)/8 > arr.c {
+ return fmt.Errorf("uint64 list length exceeds limit: %d > %d", len(buf)/8, arr.c)
+ }
arr.cachedHash = common.Hash{}
arr.u = make([]uint64, len(buf)/8)
for i := range arr.u {
@@ -171,6 +178,10 @@ func (arr *RawUint64List) HashSSZ() ([32]byte, error) {
return arr.cachedHash, nil
}
+func (arr *RawUint64List) HashSSZProgressive() ([32]byte, error) {
+ return merkle_tree.ProgressiveBasicListRoot(arr.Bytes(), uint64(len(arr.u)))
+}
+
func (arr *RawUint64List) Pop() uint64 {
panic("k")
}
diff --git a/cl/cltypes/solid/uint64_raw_list_test.go b/cl/cltypes/solid/uint64_raw_list_test.go
new file mode 100644
index 00000000000..b6ed8f86ba4
--- /dev/null
+++ b/cl/cltypes/solid/uint64_raw_list_test.go
@@ -0,0 +1,30 @@
+package solid_test
+
+import (
+ "testing"
+
+ "github.com/erigontech/erigon/cl/cltypes/solid"
+ "github.com/stretchr/testify/require"
+)
+
+func TestRawUint64ListDecodeSSZRejectsInvalidSize(t *testing.T) {
+ tests := []struct {
+ name string
+ data []byte
+ }{
+ {"partial element", make([]byte, 7)},
+ {"over limit", make([]byte, 16)},
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ list := solid.NewRawUint64List(1, nil)
+ require.Error(t, list.DecodeSSZ(test.data, 0))
+ })
+ }
+}
+
+func TestRawUint64ListDecodeSSZAcceptsLimit(t *testing.T) {
+ list := solid.NewRawUint64List(1, nil)
+ require.NoError(t, list.DecodeSSZ(make([]byte, 8), 0))
+ require.Equal(t, 1, list.Length())
+}
diff --git a/cl/cltypes/solid/validator_set.go b/cl/cltypes/solid/validator_set.go
index 5e27a28cd2d..ceb4a3683a0 100644
--- a/cl/cltypes/solid/validator_set.go
+++ b/cl/cltypes/solid/validator_set.go
@@ -18,6 +18,7 @@ package solid
import (
"encoding/json"
+ "slices"
"github.com/erigontech/erigon/cl/merkle_tree"
"github.com/erigontech/erigon/common"
@@ -46,7 +47,8 @@ type Phase0Data struct {
type ValidatorSet struct {
*merkle_tree.MerkleTree
- buffer []byte
+ progressiveTrees []*merkle_tree.MerkleTree
+ buffer []byte
l, c int
@@ -100,6 +102,7 @@ func (v *ValidatorSet) Append(val Validator) {
if v.MerkleTree != nil {
v.MerkleTree.AppendLeaf()
}
+ v.appendProgressiveLeaf(v.l)
v.zeroTreeHash(v.l)
if v.l >= len(v.phase0Data) {
@@ -134,6 +137,7 @@ func (v *ValidatorSet) Clear() {
v.l = 0
v.attesterBits = v.attesterBits[:0]
v.MerkleTree = nil
+ v.progressiveTrees = nil
}
func (v *ValidatorSet) Clone() clonable.Clonable {
@@ -167,6 +171,21 @@ func (v *ValidatorSet) CopyTo(t *ValidatorSet) {
} else {
t.MerkleTree = nil
}
+ if v.progressiveTrees != nil {
+ t.progressiveTrees = make([]*merkle_tree.MerkleTree, len(v.progressiveTrees))
+ start := 0
+ capacity := 1
+ for i, tree := range v.progressiveTrees {
+ t.progressiveTrees[i] = &merkle_tree.MerkleTree{}
+ tree.CopyInto(t.progressiveTrees[i])
+ segmentStart := start
+ t.progressiveTrees[i].SetComputeLeafFn(t.progressiveLeafFn(segmentStart))
+ start += capacity
+ capacity *= 4
+ }
+ } else {
+ t.progressiveTrees = nil
+ }
if cap(t.phase0Data) <= len(v.phase0Data) {
t.phase0Data = make([]Phase0Data, len(v.phase0Data), len(v.phase0Data)*2)
@@ -186,6 +205,7 @@ func (v *ValidatorSet) DecodeSSZ(buf []byte, _ int) error {
v.expandBuffer(len(buf) / validatorSize)
copy(v.buffer, buf)
v.MerkleTree = nil
+ v.progressiveTrees = nil
v.l = len(buf) / validatorSize
v.phase0Data = make([]Phase0Data, v.l)
v.attesterBits = make([]byte, v.l)
@@ -237,6 +257,30 @@ func (v *ValidatorSet) HashSSZ() ([32]byte, error) {
return crypto.Sha256(coreRoot[:], lengthRoot[:]), nil
}
+func (v *ValidatorSet) HashSSZProgressive() ([32]byte, error) {
+ if v.progressiveTrees == nil {
+ v.initializeProgressiveTrees()
+ }
+ var root common.Hash
+ for _, tree := range slices.Backward(v.progressiveTrees) {
+ left := tree.ComputeRoot()
+ root = crypto.Sha256(left[:], root[:])
+ }
+ lengthRoot := merkle_tree.Uint64Root(uint64(v.l))
+ return crypto.Sha256(root[:], lengthRoot[:]), nil
+}
+
+func (v *ValidatorSet) SetProgressiveHashing(enabled bool) {
+ if v == nil {
+ return
+ }
+ if enabled {
+ v.MerkleTree = nil
+ return
+ }
+ v.progressiveTrees = nil
+}
+
func (v *ValidatorSet) Set(idx int, val Validator) {
if idx >= v.l {
panic("ValidatorSet -- Set: out of bounds")
@@ -282,6 +326,63 @@ func (v *ValidatorSet) zeroTreeHash(idx int) {
if v.MerkleTree != nil {
v.MerkleTree.MarkLeafAsDirty(idx)
}
+ if v.progressiveTrees != nil {
+ segment, localIndex, _ := progressiveSegmentForIndex(idx)
+ v.progressiveTrees[segment].MarkLeafAsDirty(localIndex)
+ }
+}
+
+func (v *ValidatorSet) initializeProgressiveTrees() {
+ v.progressiveTrees = make([]*merkle_tree.MerkleTree, 0)
+ start := 0
+ capacity := 1
+ for start < v.l {
+ count := min(capacity, v.l-start)
+ tree := &merkle_tree.MerkleTree{}
+ limit := uint64(capacity)
+ tree.Initialize(count, merkle_tree.OptimalMaxTreeCacheDepth, v.progressiveLeafFn(start), &limit)
+ v.progressiveTrees = append(v.progressiveTrees, tree)
+ start += capacity
+ capacity *= 4
+ }
+}
+
+func (v *ValidatorSet) appendProgressiveLeaf(index int) {
+ if v.progressiveTrees == nil {
+ return
+ }
+ segment, localIndex, capacity := progressiveSegmentForIndex(index)
+ if segment < len(v.progressiveTrees) {
+ v.progressiveTrees[segment].AppendLeaf()
+ return
+ }
+ tree := &merkle_tree.MerkleTree{}
+ limit := uint64(capacity)
+ tree.Initialize(1, merkle_tree.OptimalMaxTreeCacheDepth, v.progressiveLeafFn(index-localIndex), &limit)
+ v.progressiveTrees = append(v.progressiveTrees, tree)
+}
+
+func (v *ValidatorSet) progressiveLeafFn(start int) func(int, []byte) {
+ var hashBuffer [8 * 32]byte
+ return func(idx int, out []byte) {
+ if err := v.Get(start + idx).CopyHashBufferTo(hashBuffer[:]); err != nil {
+ panic(err)
+ }
+ if err := merkle_tree.MerkleRootFromFlatLeaves(hashBuffer[:], out); err != nil {
+ panic(err)
+ }
+ }
+}
+
+func progressiveSegmentForIndex(index int) (segment, localIndex, capacity int) {
+ localIndex = index
+ capacity = 1
+ for localIndex >= capacity {
+ localIndex -= capacity
+ capacity *= 4
+ segment++
+ }
+ return segment, localIndex, capacity
}
func (v *ValidatorSet) IsCurrentMatchingSourceAttester(idx int) bool {
diff --git a/cl/cltypes/solid/validator_set_progressive_test.go b/cl/cltypes/solid/validator_set_progressive_test.go
new file mode 100644
index 00000000000..17463a7209f
--- /dev/null
+++ b/cl/cltypes/solid/validator_set_progressive_test.go
@@ -0,0 +1,83 @@
+package solid
+
+import (
+ "strconv"
+ "testing"
+
+ "github.com/erigontech/erigon/cl/merkle_tree"
+ "github.com/stretchr/testify/require"
+)
+
+func TestValidatorSetProgressiveRootMatchesReference(t *testing.T) {
+ for _, count := range []int{0, 1, 5, 21, 85} {
+ t.Run(strconv.Itoa(count), func(t *testing.T) {
+ validators := NewValidatorSet(1_000_000)
+ for i := range count {
+ validator := make(Validator, validatorSize)
+ validator[0] = byte(i + 1)
+ validators.Append(validator)
+ }
+
+ want := validatorSetProgressiveRootReference(t, validators)
+ got, err := validators.HashSSZProgressive()
+ require.NoError(t, err)
+ require.Equal(t, want, got)
+
+ if count > 0 {
+ validator := make(Validator, validatorSize)
+ validator[0] = 0xff
+ validators.Set(count/2, validator)
+ want = validatorSetProgressiveRootReference(t, validators)
+ got, err = validators.HashSSZProgressive()
+ require.NoError(t, err)
+ require.Equal(t, want, got)
+ }
+
+ appended := make(Validator, validatorSize)
+ appended[0] = 0xee
+ validators.Append(appended)
+ want = validatorSetProgressiveRootReference(t, validators)
+ got, err = validators.HashSSZProgressive()
+ require.NoError(t, err)
+ require.Equal(t, want, got)
+
+ copied := NewValidatorSet(validators.c)
+ validators.CopyTo(copied)
+ got, err = copied.HashSSZProgressive()
+ require.NoError(t, err)
+ require.Equal(t, want, got)
+ })
+ }
+}
+
+func TestValidatorSetSwitchesMerkleCacheMode(t *testing.T) {
+ validators := NewValidatorSet(16)
+ validators.Append(make(Validator, validatorSize))
+ _, err := validators.HashSSZ()
+ require.NoError(t, err)
+ require.NotNil(t, validators.MerkleTree)
+
+ validators.SetProgressiveHashing(true)
+ require.Nil(t, validators.MerkleTree)
+ _, err = validators.HashSSZProgressive()
+ require.NoError(t, err)
+ require.NotNil(t, validators.progressiveTrees)
+
+ validators.SetProgressiveHashing(false)
+ require.Nil(t, validators.progressiveTrees)
+ _, err = validators.HashSSZ()
+ require.NoError(t, err)
+ require.NotNil(t, validators.MerkleTree)
+}
+
+func validatorSetProgressiveRootReference(t *testing.T, validators *ValidatorSet) [32]byte {
+ roots := make([][32]byte, validators.l)
+ hashBuffer := make([]byte, 8*32)
+ for i := range roots {
+ require.NoError(t, validators.Get(i).CopyHashBufferTo(hashBuffer))
+ require.NoError(t, merkle_tree.MerkleRootFromFlatLeaves(hashBuffer, roots[i][:]))
+ }
+ root, err := merkle_tree.ProgressiveListRoot(roots, uint64(validators.l))
+ require.NoError(t, err)
+ return root
+}
diff --git a/cl/cltypes/solid/vector_test.go b/cl/cltypes/solid/vector_test.go
index 24d1573b512..01939294c64 100644
--- a/cl/cltypes/solid/vector_test.go
+++ b/cl/cltypes/solid/vector_test.go
@@ -41,6 +41,7 @@ func newTestAttestation(slot, committeeIndex uint64, numBytes int) *Attestation
for i := 0; i < numBytes && i < 10; i++ {
att.AggregationBits.Set(i, byte(i))
}
+ att.AggregationBits.Set(numBytes-1, 1)
return att
}
diff --git a/cl/merkle_tree/merkle_root.go b/cl/merkle_tree/merkle_root.go
index 5e0c32e84e2..1ac7dbe73cc 100644
--- a/cl/merkle_tree/merkle_root.go
+++ b/cl/merkle_tree/merkle_root.go
@@ -17,6 +17,7 @@
package merkle_tree
import (
+ "crypto/sha256"
"encoding/binary"
"errors"
"fmt"
@@ -101,6 +102,172 @@ func HashTreeRoot(schema ...any) ([32]byte, error) {
return common.BytesToHash(leaves[:length.Hash]), nil
}
+func ProgressiveContainerRoot(activeFields []bool, schema ...any) ([32]byte, error) {
+ if len(activeFields) == 0 || len(activeFields) > 256 || !activeFields[len(activeFields)-1] {
+ return [32]byte{}, errors.New("invalid progressive container active fields")
+ }
+ roots, err := progressiveSchemaRoots(schema)
+ if err != nil {
+ return [32]byte{}, err
+ }
+ chunks := make([][32]byte, len(activeFields))
+ rootIndex := 0
+ var activeRoot [32]byte
+ for i, active := range activeFields {
+ if !active {
+ continue
+ }
+ if rootIndex >= len(roots) {
+ return [32]byte{}, errors.New("progressive container has fewer fields than active bits")
+ }
+ chunks[i] = roots[rootIndex]
+ activeRoot[i/8] |= 1 << uint(i%8)
+ rootIndex++
+ }
+ if rootIndex != len(roots) {
+ return [32]byte{}, errors.New("progressive container has more fields than active bits")
+ }
+ root, err := MerkleizeProgressive(chunks)
+ if err != nil {
+ return [32]byte{}, err
+ }
+ return hashPair(root, activeRoot), nil
+}
+
+func ProgressiveContainerRootAll(schema ...any) ([32]byte, error) {
+ activeFields := make([]bool, len(schema))
+ for i := range activeFields {
+ activeFields[i] = true
+ }
+ return ProgressiveContainerRoot(activeFields, schema...)
+}
+
+func ProgressiveContainerProofAll(fieldIndex int, schema ...any) ([][32]byte, error) {
+ if len(schema) == 0 || len(schema) > 256 {
+ return nil, errors.New("invalid progressive container schema")
+ }
+ if fieldIndex < 0 || fieldIndex >= len(schema) {
+ return nil, errors.New("progressive container field index out of range")
+ }
+ roots, err := progressiveSchemaRoots(schema)
+ if err != nil {
+ return nil, err
+ }
+ proof, err := progressiveProof(roots, fieldIndex, 1)
+ if err != nil {
+ return nil, err
+ }
+ var activeRoot [32]byte
+ for i := range schema {
+ activeRoot[i/8] |= 1 << uint(i%8)
+ }
+ return append(proof, activeRoot), nil
+}
+
+func ProgressiveBitlistRoot(packed []byte, bitLength uint64) ([32]byte, error) {
+ chunks := make([][32]byte, (len(packed)+31)/32)
+ for i := range packed {
+ chunks[i/32][i%32] = packed[i]
+ }
+ return ProgressiveListRoot(chunks, bitLength)
+}
+
+func ProgressiveBasicListRoot(packed []byte, listLength uint64) ([32]byte, error) {
+ chunks := make([][32]byte, (len(packed)+31)/32)
+ for i := range packed {
+ chunks[i/32][i%32] = packed[i]
+ }
+ return ProgressiveListRoot(chunks, listLength)
+}
+
+func progressiveProof(chunks [][32]byte, target int, numLeaves uint64) ([][32]byte, error) {
+ if target < 0 || target >= len(chunks) {
+ return nil, errors.New("progressive proof target out of range")
+ }
+ count := min(len(chunks), int(numLeaves))
+ left := append([][32]byte(nil), chunks[:count]...)
+ leftRoot, err := MerkleizeVector(append([][32]byte(nil), left...), numLeaves)
+ if err != nil {
+ return nil, err
+ }
+ if numLeaves > ^uint64(0)/4 {
+ return nil, errors.New("progressive merkle tree is too large")
+ }
+ rightRoot, err := merkleizeProgressive(append([][32]byte(nil), chunks[count:]...), numLeaves*4)
+ if err != nil {
+ return nil, err
+ }
+ if target < count {
+ proof, err := vectorProof(left, target, int(numLeaves))
+ if err != nil {
+ return nil, err
+ }
+ return append(proof, rightRoot), nil
+ }
+ proof, err := progressiveProof(chunks[count:], target-count, numLeaves*4)
+ if err != nil {
+ return nil, err
+ }
+ return append(proof, leftRoot), nil
+}
+
+func vectorProof(chunks [][32]byte, target, capacity int) ([][32]byte, error) {
+ if capacity < 1 || target < 0 || target >= len(chunks) || len(chunks) > capacity {
+ return nil, errors.New("vector proof target out of range")
+ }
+ nodes := make([][32]byte, capacity)
+ copy(nodes, chunks)
+ proof := make([][32]byte, 0, GetDepth(uint64(capacity)))
+ for len(nodes) > 1 {
+ proof = append(proof, nodes[target^1])
+ next := make([][32]byte, len(nodes)/2)
+ for i := range next {
+ next[i] = hashPair(nodes[i*2], nodes[i*2+1])
+ }
+ nodes = next
+ target /= 2
+ }
+ return proof, nil
+}
+
+func progressiveSchemaRoots(schema []any) ([][32]byte, error) {
+ roots := make([][32]byte, len(schema))
+ for i, element := range schema {
+ switch obj := element.(type) {
+ case uint64:
+ binary.LittleEndian.PutUint64(roots[i][:], obj)
+ case *uint64:
+ binary.LittleEndian.PutUint64(roots[i][:], *obj)
+ case []byte:
+ if len(obj) < length.Hash {
+ copy(roots[i][:], obj)
+ continue
+ }
+ root, err := BytesRoot(obj)
+ if err != nil {
+ return nil, err
+ }
+ roots[i] = root
+ case ssz.HashableSSZ:
+ root, err := obj.HashSSZ()
+ if err != nil {
+ return nil, err
+ }
+ roots[i] = root
+ default:
+ panic(fmt.Sprintf("Can't create TreeRoot: unsupported type %T at index %d", obj, i))
+ }
+ }
+ return roots, nil
+}
+
+func hashPair(left, right [32]byte) [32]byte {
+ var pair [64]byte
+ copy(pair[:32], left[:])
+ copy(pair[32:], right[:])
+ return sha256.Sum256(pair[:])
+}
+
// HashByteSlice is gohashtree HashBytSlice but using our hopefully safer header conversion
func HashByteSlice(out, in []byte) error {
if len(in) == 0 {
diff --git a/cl/merkle_tree/merkle_root_test.go b/cl/merkle_tree/merkle_root_test.go
index 9a60e1b00c2..74542cf78e5 100644
--- a/cl/merkle_tree/merkle_root_test.go
+++ b/cl/merkle_tree/merkle_root_test.go
@@ -44,6 +44,20 @@ func TestHashTreeRootEmptySchema(t *testing.T) {
require.Error(t, err)
}
+func TestProgressiveContainerRootUnsupportedTypeMessage(t *testing.T) {
+ require.PanicsWithValue(t, "Can't create TreeRoot: unsupported type string at index 0", func() {
+ _, _ = merkle_tree.ProgressiveContainerRootAll("bad")
+ })
+}
+
+func TestProgressiveContainerRootInactiveFieldVector(t *testing.T) {
+ first := common.Hash{1}
+ third := common.Hash{2}
+ root, err := merkle_tree.ProgressiveContainerRoot([]bool{true, false, true}, first[:], third[:])
+ require.NoError(t, err)
+ require.Equal(t, common.HexToHash("0x3a6584864e28437da67deac288c46c9b60cee55880b19b12cfe68a7d1d5bc491"), common.Hash(root))
+}
+
func TestHashTreeRootTxs(t *testing.T) {
txs := [][]byte{
{1, 2, 3},
@@ -54,3 +68,13 @@ func TestHashTreeRootTxs(t *testing.T) {
require.NoError(t, err)
require.Equal(t, common.Hash(root), common.HexToHash("0x987269bc1075122edff32bfc38479757103cee5c1ed6e990de7ffee85b5dd18a"))
}
+
+func TestProgressiveContainerProofRejectsOversizedSchema(t *testing.T) {
+ schema := make([]any, 257)
+ for i := range schema {
+ schema[i] = uint64(i)
+ }
+
+ _, err := merkle_tree.ProgressiveContainerProofAll(0, schema...)
+ require.Error(t, err)
+}
diff --git a/cl/phase1/core/state/accessors.go b/cl/phase1/core/state/accessors.go
index 2632399cb1a..12000f4a125 100644
--- a/cl/phase1/core/state/accessors.go
+++ b/cl/phase1/core/state/accessors.go
@@ -167,8 +167,8 @@ func EligibleValidatorsIndicies(b abstract.BeaconState) (eligibleValidators []ui
func IsValidIndexedAttestation(b abstract.BeaconStateBasic, att *cltypes.IndexedAttestation) (bool, error) {
inds := att.AttestingIndices
- if inds.Length() == 0 || !solid.IsUint64SortedSet(inds) {
- return false, errors.New("isValidIndexedAttestation: attesting indices are not sorted or are null")
+ if err := ValidateIndexedAttestationIndices(b.BeaconConfig(), b.Version(), inds); err != nil {
+ return false, err
}
pks := make([][]byte, 0, inds.Length())
@@ -204,6 +204,30 @@ func IsValidIndexedAttestation(b abstract.BeaconStateBasic, att *cltypes.Indexed
return true, nil
}
+func ValidateIndexedAttestationIndices(cfg *clparams.BeaconChainConfig, version clparams.StateVersion, inds *solid.RawUint64List) error {
+ if inds == nil || inds.Length() == 0 {
+ return errors.New("isValidIndexedAttestation: attesting indices are not sorted or are null")
+ }
+ if cfg == nil {
+ return errors.New("isValidIndexedAttestation: beacon config is nil")
+ }
+ limit := cfg.MaxValidatorsPerCommittee
+ if version >= clparams.ElectraVersion {
+ if cfg.MaxCommitteesPerSlot != 0 && limit > ^uint64(0)/cfg.MaxCommitteesPerSlot {
+ limit = ^uint64(0)
+ } else {
+ limit *= cfg.MaxCommitteesPerSlot
+ }
+ }
+ if uint64(inds.Length()) > limit {
+ return fmt.Errorf("isValidIndexedAttestation: too many attesting indices: %d > %d", inds.Length(), limit)
+ }
+ if !solid.IsUint64SortedSet(inds) {
+ return errors.New("isValidIndexedAttestation: attesting indices are not sorted or are null")
+ }
+ return nil
+}
+
// IsValidatorEligibleForActivationQueue returns whether the validator is eligible to be placed into the activation queue.
// Implementation of is_eligible_for_activation_queue.
// Specs at: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#is_eligible_for_activation_queue
diff --git a/cl/phase1/core/state/accessors_gloas_test.go b/cl/phase1/core/state/accessors_gloas_test.go
new file mode 100644
index 00000000000..3fe33b91af7
--- /dev/null
+++ b/cl/phase1/core/state/accessors_gloas_test.go
@@ -0,0 +1,39 @@
+package state
+
+import (
+ "testing"
+
+ "github.com/erigontech/erigon/cl/clparams"
+ "github.com/erigontech/erigon/cl/cltypes"
+ "github.com/erigontech/erigon/cl/cltypes/solid"
+ "github.com/stretchr/testify/require"
+)
+
+func TestIsValidIndexedAttestationRejectsOversizedGloasIndicesBeforeLookup(t *testing.T) {
+ cfg := clparams.MainnetBeaconConfig
+ limit := int(cfg.MaxValidatorsPerCommittee * cfg.MaxCommitteesPerSlot)
+ indices := make([]uint64, limit+1)
+ for i := range indices {
+ indices[i] = uint64(i)
+ }
+ attestation := cltypes.NewIndexedAttestationWithConfig(clparams.GloasVersion, &cfg)
+ attestation.AttestingIndices = solid.NewRawUint64List(limit, indices)
+
+ valid, err := IsValidIndexedAttestation(New(&cfg), attestation)
+ require.False(t, valid)
+ require.ErrorContains(t, err, "too many attesting indices")
+}
+
+func TestValidateIndexedAttestationIndicesSaturatesConfigLimit(t *testing.T) {
+ cfg := clparams.MainnetBeaconConfig
+ cfg.MaxValidatorsPerCommittee = 1 << 63
+ cfg.MaxCommitteesPerSlot = 2
+ indices := solid.NewRawUint64List(1, []uint64{0})
+
+ require.NoError(t, ValidateIndexedAttestationIndices(&cfg, clparams.GloasVersion, indices))
+}
+
+func TestValidateIndexedAttestationIndicesRejectsNilConfig(t *testing.T) {
+ indices := solid.NewRawUint64List(1, []uint64{0})
+ require.Error(t, ValidateIndexedAttestationIndices(nil, clparams.GloasVersion, indices))
+}
diff --git a/cl/phase1/core/state/epbs.go b/cl/phase1/core/state/epbs.go
index 84e5882aed2..d4b9413d714 100644
--- a/cl/phase1/core/state/epbs.go
+++ b/cl/phase1/core/state/epbs.go
@@ -59,7 +59,7 @@ func IsActiveBuilder(state abstract.BeaconState, builderIndex uint64) bool {
}
// IsBuilderWithdrawalCredential checks if the withdrawal credentials belong to a builder.
-// Builder withdrawal credentials have the BUILDER_WITHDRAWAL_PREFIX (0x03) as the first byte.
+// Builder withdrawal credentials start with the configured builder prefix.
func IsBuilderWithdrawalCredential(withdrawalCredentials [32]byte, beaconConfig *clparams.BeaconChainConfig) bool {
return withdrawalCredentials[0] == byte(beaconConfig.BuilderWithdrawalPrefix)
}
@@ -377,6 +377,9 @@ func ApplyDepositForBuilder(s abstract.BeaconState, pubkey common.Bytes48, withd
}
func ApplyBuilderDepositRequest(s abstract.BeaconState, request *solid.BuilderDepositRequest) error {
+ if !IsBuilderWithdrawalCredential(request.WithdrawalCredentials, s.BeaconConfig()) {
+ return nil
+ }
builders := s.GetBuilders()
builderIndex := -1
if builders != nil {
@@ -397,7 +400,7 @@ func ApplyBuilderDepositRequest(s abstract.BeaconState, request *solid.BuilderDe
return AddBuilderToRegistry(
s,
request.PubKey,
- request.WithdrawalCredentials[0],
+ s.BeaconConfig().PayloadBuilderVersion,
common.BytesToAddress(request.WithdrawalCredentials[12:]),
request.Amount,
s.Slot(),
@@ -409,7 +412,7 @@ func ApplyBuilderDepositRequest(s abstract.BeaconState, request *solid.BuilderDe
return nil
}
newBuilder := *builder
- if newBuilder.WithdrawableEpoch != s.BeaconConfig().FarFutureEpoch {
+ if newBuilder.WithdrawableEpoch != s.BeaconConfig().FarFutureEpoch && newBuilder.Balance == 0 {
newBuilder.WithdrawableEpoch = GetEpochAtSlot(s.BeaconConfig(), s.Slot()) + s.BeaconConfig().MinBuilderWithdrawabilityDelay
}
if request.Amount > math.MaxUint64-newBuilder.Balance {
diff --git a/cl/phase1/core/state/epbs_test.go b/cl/phase1/core/state/epbs_test.go
index 199458ce2da..5d001f3083b 100644
--- a/cl/phase1/core/state/epbs_test.go
+++ b/cl/phase1/core/state/epbs_test.go
@@ -17,26 +17,21 @@ import (
"github.com/erigontech/erigon/common/crypto"
)
-// TestIsBuilderWithdrawalCredential_0x03 verifies that withdrawal credentials
-// with the 0x03 prefix are recognised as builder credentials.
-func TestIsBuilderWithdrawalCredential_0x03(t *testing.T) {
+func TestIsBuilderWithdrawalCredential(t *testing.T) {
cfg := clparams.MainnetBeaconConfig
var creds common.Hash
- creds[0] = 0x03
+ creds[0] = byte(cfg.BuilderWithdrawalPrefix)
addr := common.HexToAddress("0xdeadbeef")
copy(creds[12:], addr[:])
- require.True(t, state2.IsBuilderWithdrawalCredential(creds, &cfg),
- "0x03 prefix must be recognised as builder withdrawal credential")
+ require.True(t, state2.IsBuilderWithdrawalCredential(creds, &cfg))
}
-// TestIsBuilderWithdrawalCredential_NotBuilder tests that non-0x03 prefixes
-// are not classified as builder credentials.
func TestIsBuilderWithdrawalCredential_NotBuilder(t *testing.T) {
cfg := clparams.MainnetBeaconConfig
- for _, prefix := range []byte{0x00, 0x01, 0x02, 0x04, 0xFF} {
+ for _, prefix := range []byte{0x00, 0x01, 0x02, 0x03, 0xFF} {
var creds common.Hash
creds[0] = prefix
require.False(t, state2.IsBuilderWithdrawalCredential(creds, &cfg),
@@ -73,12 +68,6 @@ func TestGetProposerDependentRootRejectsUnderflow(t *testing.T) {
require.Error(t, err)
}
-// TestApplyDepositForBuilder_NewBuilder_WithValidSignature verifies that a
-// new builder deposit with 0x03 credentials and a valid signature creates
-// a builder entry in the state registry.
-//
-// This covers the routing path: deposit with 0x03 prefix → ApplyDepositForBuilder
-// → IsValidDepositSignature → AddBuilderToRegistry.
func TestApplyDepositForBuilder_NewBuilder_WithValidSignature(t *testing.T) {
cfg := clparams.MainnetBeaconConfig
@@ -89,7 +78,7 @@ func TestApplyDepositForBuilder_NewBuilder_WithValidSignature(t *testing.T) {
pubkey, creds, amount, sig := makeValidBuilderDeposit(t, &cfg)
// Pre-conditions.
- require.Equal(t, byte(0x03), creds[0])
+ require.Equal(t, byte(cfg.BuilderWithdrawalPrefix), creds[0])
require.True(t, state2.IsBuilderWithdrawalCredential(creds, &cfg))
slot := uint64(100)
@@ -264,8 +253,9 @@ func TestApplyBuilderDepositRequestTopUpSweptExitedBuilderResetsWithdrawableEpoc
s.SetBuilders(builders)
require.NoError(t, state2.ApplyBuilderDepositRequest(s, &solid.BuilderDepositRequest{
- PubKey: pubkey,
- Amount: 25,
+ PubKey: pubkey,
+ WithdrawalCredentials: common.Hash{byte(cfg.BuilderWithdrawalPrefix)},
+ Amount: 25,
}))
builder := s.GetBuilders().Get(0)
@@ -273,7 +263,7 @@ func TestApplyBuilderDepositRequestTopUpSweptExitedBuilderResetsWithdrawableEpoc
require.Equal(t, uint64(10)+cfg.MinBuilderWithdrawabilityDelay, builder.WithdrawableEpoch)
}
-func TestApplyBuilderDepositRequestTopUpUnsweptExitedBuilderResetsWithdrawableEpoch(t *testing.T) {
+func TestApplyBuilderDepositRequestTopUpUnsweptExitedBuilderKeepsWithdrawableEpoch(t *testing.T) {
cfg := clparams.MainnetBeaconConfig
s := state2.New(&cfg)
s.SetSlot(cfg.SlotsPerEpoch * 10)
@@ -288,13 +278,14 @@ func TestApplyBuilderDepositRequestTopUpUnsweptExitedBuilderResetsWithdrawableEp
s.SetBuilders(builders)
require.NoError(t, state2.ApplyBuilderDepositRequest(s, &solid.BuilderDepositRequest{
- PubKey: pubkey,
- Amount: 25,
+ PubKey: pubkey,
+ WithdrawalCredentials: common.Hash{byte(cfg.BuilderWithdrawalPrefix)},
+ Amount: 25,
}))
builder := s.GetBuilders().Get(0)
require.Equal(t, uint64(35), builder.Balance)
- require.Equal(t, uint64(10)+cfg.MinBuilderWithdrawabilityDelay, builder.WithdrawableEpoch)
+ require.Equal(t, uint64(1), builder.WithdrawableEpoch)
}
func TestBuilderHelpersRejectHugeIndex(t *testing.T) {
@@ -325,8 +316,9 @@ func TestApplyBuilderDepositRequestDoesNotOverflowBalance(t *testing.T) {
s.SetBuilders(builders)
err := state2.ApplyBuilderDepositRequest(s, &solid.BuilderDepositRequest{
- PubKey: pubkey,
- Amount: 1,
+ PubKey: pubkey,
+ WithdrawalCredentials: common.Hash{byte(cfg.BuilderWithdrawalPrefix)},
+ Amount: 1,
})
require.Error(t, err)
@@ -371,7 +363,6 @@ func makeValidBuilderDeposit(t *testing.T, cfg *clparams.BeaconChainConfig) (
feeRecipient := common.HexToAddress("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
- // Build withdrawal credentials: 0x03 + 11 zero bytes + 20-byte address.
withdrawalCredentials[0] = byte(cfg.BuilderWithdrawalPrefix)
copy(withdrawalCredentials[12:], feeRecipient[:])
diff --git a/cl/phase1/core/state/raw/hashing.go b/cl/phase1/core/state/raw/hashing.go
index d673118c763..394c991c16f 100644
--- a/cl/phase1/core/state/raw/hashing.go
+++ b/cl/phase1/core/state/raw/hashing.go
@@ -44,7 +44,11 @@ func (b *BeaconState) HashSSZ() (out [32]byte, err error) {
endIndex = StateLeafSizeFulu * 32
}
if b.Version() >= clparams.GloasVersion {
- endIndex = StateLeafSizeGloas * 32
+ schema := make([]any, StateLeafSizeGloas)
+ for i := range schema {
+ schema[i] = b.leaves[i*32 : (i+1)*32]
+ }
+ return merkle_tree.ProgressiveContainerRootAll(schema...)
}
err = merkle_tree.MerkleRootFromFlatLeaves(b.leaves[:endIndex], out[:])
return
@@ -72,7 +76,11 @@ func (b *BeaconState) CurrentSyncCommitteeBranch() ([][32]byte, error) {
leafSize = StateLeafSizeFulu
}
if b.Version() >= clparams.GloasVersion {
- leafSize = StateLeafSizeGloas
+ schema := make([]any, StateLeafSizeGloas)
+ for i := range schema {
+ schema[i] = b.leaves[i*32 : (i+1)*32]
+ }
+ return merkle_tree.ProgressiveContainerProofAll(22, schema...)
}
schema := []any{}
@@ -98,7 +106,11 @@ func (b *BeaconState) NextSyncCommitteeBranch() ([][32]byte, error) {
leafSize = StateLeafSizeFulu
}
if b.Version() >= clparams.GloasVersion {
- leafSize = StateLeafSizeGloas
+ schema := make([]any, StateLeafSizeGloas)
+ for i := range schema {
+ schema[i] = b.leaves[i*32 : (i+1)*32]
+ }
+ return merkle_tree.ProgressiveContainerProofAll(23, schema...)
}
schema := []any{}
@@ -123,7 +135,15 @@ func (b *BeaconState) FinalityRootBranch() ([][32]byte, error) {
leafSize = StateLeafSizeFulu
}
if b.Version() >= clparams.GloasVersion {
- leafSize = StateLeafSizeGloas
+ schema := make([]any, StateLeafSizeGloas)
+ for i := range schema {
+ schema[i] = b.leaves[i*32 : (i+1)*32]
+ }
+ proof, err := merkle_tree.ProgressiveContainerProofAll(20, schema...)
+ if err != nil {
+ return nil, err
+ }
+ return append([][32]byte{merkle_tree.Uint64Root(b.finalizedCheckpoint.Epoch)}, proof...), nil
}
schema := []any{}
@@ -144,19 +164,30 @@ type beaconStateHasher struct {
jobs map[StateLeafIndex]any
}
-func (p *beaconStateHasher) run() {
+type beaconStateHashJob func() ([32]byte, error)
+
+func (p *beaconStateHasher) run() error {
var wg sync.WaitGroup
if p.jobs == nil {
p.jobs = make(map[StateLeafIndex]any)
}
+ errs := make(chan error, len(p.jobs))
for idx, job := range p.jobs {
wg.Go(func() {
switch obj := job.(type) {
case ssz.HashableSSZ:
root, err := obj.HashSSZ()
if err != nil {
- panic(err)
+ errs <- err
+ return
+ }
+ p.b.updateLeaf(idx, root)
+ case beaconStateHashJob:
+ root, err := obj()
+ if err != nil {
+ errs <- err
+ return
}
p.b.updateLeaf(idx, root)
case uint64:
@@ -167,6 +198,11 @@ func (p *beaconStateHasher) run() {
})
}
wg.Wait()
+ close(errs)
+ for err := range errs {
+ return err
+ }
+ return nil
}
func (p *beaconStateHasher) add(idx StateLeafIndex, job any) {
@@ -180,6 +216,10 @@ func (p *beaconStateHasher) add(idx StateLeafIndex, job any) {
p.jobs[idx] = job
}
+func (p *beaconStateHasher) addHash(idx StateLeafIndex, job beaconStateHashJob) {
+ p.add(idx, job)
+}
+
func (b *BeaconState) computeDirtyLeaves() error {
beaconStateHasher := &beaconStateHasher{b: b}
// Update all dirty leafs.
@@ -194,8 +234,15 @@ func (b *BeaconState) computeDirtyLeaves() error {
beaconStateHasher.add(Eth1DataLeafIndex, b.eth1Data)
beaconStateHasher.add(Eth1DataVotesLeafIndex, b.eth1DataVotes)
beaconStateHasher.add(Eth1DepositIndexLeafIndex, b.eth1DepositIndex)
- beaconStateHasher.add(ValidatorsLeafIndex, b.validators)
- beaconStateHasher.add(BalancesLeafIndex, b.balances)
+ if b.version >= clparams.GloasVersion {
+ beaconStateHasher.addHash(ValidatorsLeafIndex, b.validators.HashSSZProgressive)
+ beaconStateHasher.addHash(BalancesLeafIndex, func() ([32]byte, error) {
+ return merkle_tree.ProgressiveBasicListRoot(b.balances.Bytes(), uint64(b.balances.Length()))
+ })
+ } else {
+ beaconStateHasher.add(ValidatorsLeafIndex, b.validators)
+ beaconStateHasher.add(BalancesLeafIndex, b.balances)
+ }
beaconStateHasher.add(RandaoMixesLeafIndex, b.randaoMixes)
beaconStateHasher.add(SlashingsLeafIndex, b.slashings)
// Special case for Participation, if phase0 use attestation format, otherwise use bitlist format.
@@ -203,8 +250,13 @@ func (b *BeaconState) computeDirtyLeaves() error {
beaconStateHasher.add(PreviousEpochParticipationLeafIndex, b.previousEpochAttestations)
beaconStateHasher.add(CurrentEpochParticipationLeafIndex, b.currentEpochAttestations)
} else {
- beaconStateHasher.add(PreviousEpochParticipationLeafIndex, b.previousEpochParticipation)
- beaconStateHasher.add(CurrentEpochParticipationLeafIndex, b.currentEpochParticipation)
+ if b.version >= clparams.GloasVersion {
+ beaconStateHasher.addHash(PreviousEpochParticipationLeafIndex, b.previousEpochParticipation.HashSSZProgressive)
+ beaconStateHasher.addHash(CurrentEpochParticipationLeafIndex, b.currentEpochParticipation.HashSSZProgressive)
+ } else {
+ beaconStateHasher.add(PreviousEpochParticipationLeafIndex, b.previousEpochParticipation)
+ beaconStateHasher.add(CurrentEpochParticipationLeafIndex, b.currentEpochParticipation)
+ }
}
// Field(17): JustificationBits
@@ -217,7 +269,13 @@ func (b *BeaconState) computeDirtyLeaves() error {
if b.version >= clparams.AltairVersion {
// Altair fields
- beaconStateHasher.add(InactivityScoresLeafIndex, b.inactivityScores)
+ if b.version >= clparams.GloasVersion {
+ beaconStateHasher.addHash(InactivityScoresLeafIndex, func() ([32]byte, error) {
+ return merkle_tree.ProgressiveBasicListRoot(b.inactivityScores.Bytes(), uint64(b.inactivityScores.Length()))
+ })
+ } else {
+ beaconStateHasher.add(InactivityScoresLeafIndex, b.inactivityScores)
+ }
beaconStateHasher.add(CurrentSyncCommitteeLeafIndex, b.currentSyncCommittee)
beaconStateHasher.add(NextSyncCommitteeLeafIndex, b.nextSyncCommittee)
}
@@ -246,9 +304,15 @@ func (b *BeaconState) computeDirtyLeaves() error {
beaconStateHasher.add(EarliestExitEpochLeafIndex, b.earliestExitEpoch)
beaconStateHasher.add(ConsolidationBalanceToConsumeLeafIndex, b.consolidationBalanceToConsume)
beaconStateHasher.add(EarliestConsolidationEpochLeafIndex, b.earliestConsolidationEpoch)
- beaconStateHasher.add(PendingDepositsLeafIndex, b.pendingDeposits)
- beaconStateHasher.add(PendingPartialWithdrawalsLeafIndex, b.pendingPartialWithdrawals)
- beaconStateHasher.add(PendingConsolidationsLeafIndex, b.pendingConsolidations)
+ if b.version >= clparams.GloasVersion {
+ beaconStateHasher.addHash(PendingDepositsLeafIndex, func() ([32]byte, error) { return b.pendingDeposits.HashSSZProgressive(nil) })
+ beaconStateHasher.addHash(PendingPartialWithdrawalsLeafIndex, func() ([32]byte, error) { return b.pendingPartialWithdrawals.HashSSZProgressive(nil) })
+ beaconStateHasher.addHash(PendingConsolidationsLeafIndex, func() ([32]byte, error) { return b.pendingConsolidations.HashSSZProgressive(nil) })
+ } else {
+ beaconStateHasher.add(PendingDepositsLeafIndex, b.pendingDeposits)
+ beaconStateHasher.add(PendingPartialWithdrawalsLeafIndex, b.pendingPartialWithdrawals)
+ beaconStateHasher.add(PendingConsolidationsLeafIndex, b.pendingConsolidations)
+ }
}
if b.version >= clparams.FuluVersion {
@@ -256,19 +320,17 @@ func (b *BeaconState) computeDirtyLeaves() error {
}
if b.version >= clparams.GloasVersion {
- beaconStateHasher.add(BuildersLeafIndex, b.builders)
+ beaconStateHasher.addHash(BuildersLeafIndex, func() ([32]byte, error) { return b.builders.HashSSZProgressive(nil) })
beaconStateHasher.add(NextWithdrawalBuilderIndexLeafIndex, b.nextWithdrawalBuilderIndex)
beaconStateHasher.add(ExecutionPayloadAvailabilityLeafIndex, b.executionPayloadAvailability)
beaconStateHasher.add(BuilderPendingPaymentsLeafIndex, b.builderPendingPayments)
- beaconStateHasher.add(BuilderPendingWithdrawalsLeafIndex, b.builderPendingWithdrawals)
+ beaconStateHasher.addHash(BuilderPendingWithdrawalsLeafIndex, func() ([32]byte, error) { return b.builderPendingWithdrawals.HashSSZProgressive(nil) })
beaconStateHasher.add(LatestExecutionPayloadBidLeafIndex, b.latestExecutionPayloadBid)
- beaconStateHasher.add(PayloadExpectedWithdrawalsLeafIndex, b.payloadExpectedWithdrawals)
+ beaconStateHasher.addHash(PayloadExpectedWithdrawalsLeafIndex, func() ([32]byte, error) { return b.payloadExpectedWithdrawals.HashSSZProgressive(nil) })
beaconStateHasher.add(PtcWindowLeafIndex, b.ptcWindow)
}
- beaconStateHasher.run()
-
- return nil
+ return beaconStateHasher.run()
}
// updateLeaf updates the leaf with the new value and marks it as clean. It's safe to call this function concurrently.
diff --git a/cl/phase1/core/state/raw/hashing_gloas_test.go b/cl/phase1/core/state/raw/hashing_gloas_test.go
new file mode 100644
index 00000000000..9712c5f05d1
--- /dev/null
+++ b/cl/phase1/core/state/raw/hashing_gloas_test.go
@@ -0,0 +1,86 @@
+package raw
+
+import (
+ "testing"
+
+ "github.com/erigontech/erigon/cl/clparams"
+ "github.com/erigontech/erigon/cl/merkle_tree"
+ "github.com/stretchr/testify/require"
+)
+
+func TestGloasBeaconStateUsesProgressiveHashing(t *testing.T) {
+ cfg := clparams.MainnetBeaconConfig
+ state := New(&cfg)
+ state.version = clparams.GloasVersion
+
+ require.NoError(t, state.computeDirtyLeaves())
+ emptyListRoot, err := merkle_tree.ProgressiveListRoot(nil, 0)
+ require.NoError(t, err)
+ for _, idx := range []StateLeafIndex{
+ ValidatorsLeafIndex,
+ BalancesLeafIndex,
+ PreviousEpochParticipationLeafIndex,
+ CurrentEpochParticipationLeafIndex,
+ InactivityScoresLeafIndex,
+ PendingDepositsLeafIndex,
+ PendingPartialWithdrawalsLeafIndex,
+ PendingConsolidationsLeafIndex,
+ BuildersLeafIndex,
+ BuilderPendingWithdrawalsLeafIndex,
+ PayloadExpectedWithdrawalsLeafIndex,
+ } {
+ copy(state.leaves[idx*32:], emptyListRoot[:])
+ }
+ schema := make([]any, StateLeafSizeGloas)
+ for i := range schema {
+ schema[i] = state.leaves[i*32 : (i+1)*32]
+ }
+ expected, err := merkle_tree.ProgressiveContainerRootAll(schema...)
+ require.NoError(t, err)
+
+ state.markLeaf(
+ ValidatorsLeafIndex,
+ BalancesLeafIndex,
+ PreviousEpochParticipationLeafIndex,
+ CurrentEpochParticipationLeafIndex,
+ InactivityScoresLeafIndex,
+ PendingDepositsLeafIndex,
+ PendingPartialWithdrawalsLeafIndex,
+ PendingConsolidationsLeafIndex,
+ BuildersLeafIndex,
+ BuilderPendingWithdrawalsLeafIndex,
+ PayloadExpectedWithdrawalsLeafIndex,
+ )
+ actual, err := state.HashSSZ()
+ require.NoError(t, err)
+ require.Equal(t, expected, actual)
+}
+
+func TestSetVersionAcrossGloasInvalidatesRoots(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ from clparams.StateVersion
+ to clparams.StateVersion
+ }{
+ {name: "upgrade", from: clparams.FuluVersion, to: clparams.GloasVersion},
+ {name: "downgrade", from: clparams.GloasVersion, to: clparams.FuluVersion},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ cfg := clparams.MainnetBeaconConfig
+ state := New(&cfg)
+ state.SetVersion(tc.from)
+ _, err := state.HashSSZ()
+ require.NoError(t, err)
+
+ state.SetVersion(tc.to)
+ actual, err := state.HashSSZ()
+ require.NoError(t, err)
+
+ fresh := New(&cfg)
+ fresh.SetVersion(tc.to)
+ expected, err := fresh.HashSSZ()
+ require.NoError(t, err)
+ require.Equal(t, expected, actual)
+ })
+ }
+}
diff --git a/cl/phase1/core/state/raw/setters.go b/cl/phase1/core/state/raw/setters.go
index 2890893a70c..3579b4ede3c 100644
--- a/cl/phase1/core/state/raw/setters.go
+++ b/cl/phase1/core/state/raw/setters.go
@@ -24,6 +24,20 @@ import (
)
func (b *BeaconState) SetVersion(version clparams.StateVersion) {
+ if (b.version < clparams.GloasVersion) != (version < clparams.GloasVersion) {
+ b.validators.SetProgressiveHashing(version >= clparams.GloasVersion)
+ b.markLeaf(
+ ValidatorsLeafIndex,
+ BalancesLeafIndex,
+ PreviousEpochParticipationLeafIndex,
+ CurrentEpochParticipationLeafIndex,
+ InactivityScoresLeafIndex,
+ LatestBlockHashLeafIndex,
+ PendingDepositsLeafIndex,
+ PendingPartialWithdrawalsLeafIndex,
+ PendingConsolidationsLeafIndex,
+ )
+ }
b.version = version
}
diff --git a/cl/phase1/forkchoice/checkpoint_state.go b/cl/phase1/forkchoice/checkpoint_state.go
index dc558dc1120..790fe2e081e 100644
--- a/cl/phase1/forkchoice/checkpoint_state.go
+++ b/cl/phase1/forkchoice/checkpoint_state.go
@@ -24,6 +24,7 @@ import (
"github.com/erigontech/erigon/cl/cltypes/solid"
"github.com/erigontech/erigon/cl/monitor"
"github.com/erigontech/erigon/cl/monitor/shuffling_metrics"
+ corestate "github.com/erigontech/erigon/cl/phase1/core/state"
"github.com/erigontech/erigon/cl/phase1/core/state/shuffling"
"github.com/erigontech/erigon/cl/phase1/forkchoice/public_keys_registry"
"github.com/erigontech/erigon/common"
@@ -180,8 +181,9 @@ func (c *checkpointState) getDomain(domainType [4]byte, epoch uint64) ([]byte, e
// isValidIndexedAttestation verifies indexed attestation
func (c *checkpointState) isValidIndexedAttestation(att *cltypes.IndexedAttestation) (bool, error) {
inds := att.AttestingIndices
- if inds.Length() == 0 || !solid.IsUint64SortedSet(inds) {
- return false, errors.New("isValidIndexedAttestation: attesting indices are not sorted or are null")
+ version := c.beaconConfig.GetCurrentStateVersion(att.Data.Target.Epoch)
+ if err := corestate.ValidateIndexedAttestationIndices(c.beaconConfig, version, inds); err != nil {
+ return false, err
}
domain, err := c.getDomain(c.beaconConfig.DomainBeaconAttester, att.Data.Target.Epoch)
diff --git a/cl/phase1/forkchoice/on_attester_slashing.go b/cl/phase1/forkchoice/on_attester_slashing.go
index 25df3f18c0a..6a9e08f7158 100644
--- a/cl/phase1/forkchoice/on_attester_slashing.go
+++ b/cl/phase1/forkchoice/on_attester_slashing.go
@@ -129,8 +129,8 @@ func (f *ForkChoiceStore) onProcessAttesterSlashing(attesterSlashing *cltypes.At
func getIndexedAttestationPublicKeys(b *state.CachingBeaconState, att *cltypes.IndexedAttestation) ([][]byte, error) {
inds := att.AttestingIndices
- if inds.Length() == 0 || !solid.IsUint64SortedSet(inds) {
- return nil, errors.New("isValidIndexedAttestation: attesting indices are not sorted or are null")
+ if err := state.ValidateIndexedAttestationIndices(b.BeaconConfig(), b.Version(), inds); err != nil {
+ return nil, err
}
pks := make([][]byte, 0, inds.Length())
if err := solid.RangeErr[uint64](inds, func(_ int, v uint64, _ int) error {
diff --git a/cl/phase1/network/services/aggregate_and_proof_service.go b/cl/phase1/network/services/aggregate_and_proof_service.go
index 99389655852..4431aebfbb8 100644
--- a/cl/phase1/network/services/aggregate_and_proof_service.go
+++ b/cl/phase1/network/services/aggregate_and_proof_service.go
@@ -179,6 +179,13 @@ func (a *aggregateAndProofServiceImpl) ProcessMessage(
subnet *uint64,
aggregateAndProof *SignedAggregateAndProofForGossip,
) error {
+ if aggregateAndProof == nil || aggregateAndProof.SignedAggregateAndProof == nil ||
+ aggregateAndProof.SignedAggregateAndProof.Message == nil ||
+ aggregateAndProof.SignedAggregateAndProof.Message.Aggregate == nil ||
+ aggregateAndProof.SignedAggregateAndProof.Message.Aggregate.Data == nil ||
+ aggregateAndProof.SignedAggregateAndProof.Message.Aggregate.AggregationBits == nil {
+ return errors.New("invalid aggregate and proof")
+ }
selectionProof := aggregateAndProof.SignedAggregateAndProof.Message.SelectionProof
aggregateData := aggregateAndProof.SignedAggregateAndProof.Message.Aggregate.Data
aggregate := aggregateAndProof.SignedAggregateAndProof.Message.Aggregate
@@ -192,7 +199,14 @@ func (a *aggregateAndProofServiceImpl) ProcessMessage(
epoch := slot / a.beaconCfg.SlotsPerEpoch
clversion := a.beaconCfg.GetCurrentStateVersion(epoch)
+ aggregateAndProof.SignedAggregateAndProof.SetVersion(clversion)
+ if err := aggregate.ValidateForConfig(a.beaconCfg, clversion); err != nil {
+ return err
+ }
if clversion.AfterOrEqual(clparams.ElectraVersion) {
+ if aggregate.CommitteeBits == nil {
+ return errors.New("invalid aggregate and proof: missing committee bits")
+ }
// [REJECT] len(committee_indices) == 1, where committee_indices = get_committee_indices(aggregate).
indices := aggregate.CommitteeBits.GetOnIndices()
if len(indices) != 1 {
diff --git a/cl/phase1/network/services/aggregate_and_proof_service_test.go b/cl/phase1/network/services/aggregate_and_proof_service_test.go
index f93a00c71e5..4a8a29c80c9 100644
--- a/cl/phase1/network/services/aggregate_and_proof_service_test.go
+++ b/cl/phase1/network/services/aggregate_and_proof_service_test.go
@@ -38,6 +38,39 @@ import (
"github.com/erigontech/erigon/common"
)
+func TestAggregateAndProofServiceRejectsMalformedNestedInput(t *testing.T) {
+ cfg := clparams.MainnetBeaconConfig
+ cfg.AltairForkEpoch = 0
+ cfg.BellatrixForkEpoch = 0
+ cfg.CapellaForkEpoch = 0
+ cfg.DenebForkEpoch = 0
+ cfg.ElectraForkEpoch = 0
+ cfg.FuluForkEpoch = 0
+ cfg.GloasForkEpoch = 0
+ service := &aggregateAndProofServiceImpl{beaconCfg: &cfg}
+
+ tests := []struct {
+ name string
+ input *SignedAggregateAndProofForGossip
+ }{
+ {"nil wrapper", nil},
+ {"nil signed message", &SignedAggregateAndProofForGossip{}},
+ {"nil message", &SignedAggregateAndProofForGossip{SignedAggregateAndProof: &cltypes.SignedAggregateAndProof{}}},
+ {"nil aggregate", &SignedAggregateAndProofForGossip{SignedAggregateAndProof: &cltypes.SignedAggregateAndProof{Message: &cltypes.AggregateAndProof{}}}},
+ {"nil data", &SignedAggregateAndProofForGossip{SignedAggregateAndProof: &cltypes.SignedAggregateAndProof{Message: &cltypes.AggregateAndProof{Aggregate: &solid.Attestation{}}}}},
+ {"nil aggregation bits", &SignedAggregateAndProofForGossip{SignedAggregateAndProof: &cltypes.SignedAggregateAndProof{Message: &cltypes.AggregateAndProof{Aggregate: &solid.Attestation{Data: &solid.AttestationData{}}}}}},
+ {"nil committee bits", &SignedAggregateAndProofForGossip{SignedAggregateAndProof: &cltypes.SignedAggregateAndProof{Message: &cltypes.AggregateAndProof{Aggregate: &solid.Attestation{Data: &solid.AttestationData{}, AggregationBits: solid.NewBitList(0, 1)}}}}},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ require.NotPanics(t, func() {
+ require.Error(t, service.ProcessMessage(context.Background(), nil, test.input))
+ })
+ })
+ }
+}
+
func getAggregateAndProofAndState(t *testing.T) (*SignedAggregateAndProofForGossip, *state.CachingBeaconState) {
_, _, s := tests.GetBellatrixRandom()
br, _ := s.BlockRoot()
diff --git a/cl/spectest/consensus_tests/appendix.go b/cl/spectest/consensus_tests/appendix.go
index b81d880d118..50de64e97fe 100644
--- a/cl/spectest/consensus_tests/appendix.go
+++ b/cl/spectest/consensus_tests/appendix.go
@@ -54,12 +54,17 @@ func init() {
With("proposer_lookahead", ProposerLookaheadTest).
With("historical_summaries_update", historicalSummariesUpdateTest).
With("builder_pending_payments", builderPendingPaymentsTest).
+ With("pending_deposits_churn", pendingDepositTest).
With("ptc_window", ptcWindowTest)
TestFormats.Add("finality").
With("finality", FinalityFinality)
TestFormats.Add("fork_choice").
With("get_head", &ForkChoice{}).
With("on_block", &ForkChoice{}).
+ With("on_attestation", &ForkChoice{}).
+ With("on_payload_attestation_message", &ForkChoice{}).
+ With("payload_data_availability", &ForkChoice{}).
+ With("payload_timeliness", &ForkChoice{}).
With("on_merge_block", &ForkChoice{}).
With("ex_ante", &ForkChoice{}).
With("on_execution_payload_envelope", &ForkChoice{}).
@@ -82,6 +87,7 @@ func init() {
WithFn("block_header", operationBlockHeaderHandler).
WithFn("deposit", operationDepositHandler).
WithFn("voluntary_exit", operationVoluntaryExitHandler).
+ WithFn("voluntary_exit_churn", operationVoluntaryExitHandler).
WithFn("sync_aggregate", operationSyncAggregateHandler).
WithFn("withdrawals", operationWithdrawalHandler).
WithFn("bls_to_execution_change", operationSignedBlsChangeHandler).
@@ -116,7 +122,10 @@ func init() {
WithFn("compute_columns_for_custody_group", TestComputeColumnsForCustodyGroup).
WithFn("get_custody_groups", TestGetCustodyGroups).
WithFn("gossip_attester_slashing", gossipAttesterSlashingHandler).
- WithFn("gossip_proposer_slashing", gossipProposerSlashingHandler)
+ WithFn("gossip_bls_to_execution_change", gossipBLSToExecutionChangeHandler).
+ WithFn("gossip_proposer_slashing", gossipProposerSlashingHandler).
+ WithFn("gossip_sync_committee_message", gossipSyncCommitteeMessageHandler).
+ WithFn("gossip_sync_committee_contribution_and_proof", gossipSyncContributionHandler)
addSszTests()
}
@@ -206,7 +215,9 @@ func addSszTests() {
)).
With("Attestation", sszStaticTestNewObjectByFunc(
func(v clparams.StateVersion) *solid.Attestation {
- return &solid.Attestation{}
+ attestation := &solid.Attestation{}
+ attestation.SetVersion(v)
+ return attestation
}, withTestJson(),
)).
With("SyncCommitteeMessage", sszStaticTestByEmptyObject(&cltypes.SyncCommitteeMessage{}, withTestJson())).
@@ -278,10 +289,11 @@ func addSszTests() {
With("SignedProposerPreferences", sszStaticTestByEmptyObject(&cltypes.SignedProposerPreferences{
Message: &cltypes.ProposerPreferences{},
}, runAfterVersion(clparams.GloasVersion))).
- // Types with fixtures but no Go SSZ implementation
- With("DepositMessage", spectest.UnimplementedHandler).
- With("ForkData", spectest.UnimplementedHandler).
+ With("DepositMessage", sszStaticTestByEmptyObject(&depositMessage{})).
+ With("Eth1Block", sszStaticTestByEmptyObject(&validatorEth1Block{})).
+ With("ForkData", sszStaticTestByEmptyObject(&forkData{})).
With("HistoricalBatch", spectest.UnimplementedHandler).
- With("PowBlock", spectest.UnimplementedHandler).
- With("SigningData", spectest.UnimplementedHandler)
+ With("PartialDataColumnGroupID", sszStaticTestByEmptyObject(&partialDataColumnGroupID{})).
+ With("PowBlock", sszStaticTestByEmptyObject(&powBlock{})).
+ With("SigningData", sszStaticTestByEmptyObject(&signingData{}))
}
diff --git a/cl/spectest/consensus_tests/gossip.go b/cl/spectest/consensus_tests/gossip.go
index 570837b5c33..f52e0e5775e 100644
--- a/cl/spectest/consensus_tests/gossip.go
+++ b/cl/spectest/consensus_tests/gossip.go
@@ -17,30 +17,422 @@
package consensus_tests
import (
+ "bytes"
+ "encoding/binary"
"fmt"
"io/fs"
+ "slices"
"testing"
"github.com/erigontech/erigon/cl/cltypes"
"github.com/erigontech/erigon/cl/cltypes/solid"
"github.com/erigontech/erigon/cl/fork"
"github.com/erigontech/erigon/cl/phase1/core/state"
+ "github.com/erigontech/erigon/cl/phase1/network/subnets"
"github.com/erigontech/erigon/cl/spectest/spectest"
+ "github.com/erigontech/erigon/cl/utils"
"github.com/erigontech/erigon/cl/utils/bls"
+ "github.com/erigontech/erigon/common"
+ "github.com/erigontech/erigon/common/crypto"
)
// gossipMeta represents the meta.yaml structure for gossip networking tests.
type gossipMeta struct {
- Topic string `yaml:"topic"`
- Messages []gossipMessage `yaml:"messages"`
+ Topic string `yaml:"topic"`
+ CurrentTimeMS uint64 `yaml:"current_time_ms"`
+ Messages []gossipMessage `yaml:"messages"`
}
type gossipMessage struct {
+ OffsetMS uint64 `yaml:"offset_ms"`
+ SubnetID uint64 `yaml:"subnet_id"`
Message string `yaml:"message"`
Expected string `yaml:"expected"`
Reason string `yaml:"reason"`
}
+type gossipConfig struct {
+ CapellaForkEpoch uint64 `yaml:"CAPELLA_FORK_EPOCH"`
+}
+
+func gossipBLSToExecutionChangeHandler(t *testing.T, root fs.FS, c spectest.TestCase) error {
+ beaconState, meta, err := readGossipStateAndMeta(root, c)
+ if err != nil {
+ return err
+ }
+ var config gossipConfig
+ if err := spectest.ReadYml(root, "config.yaml", &config); err != nil {
+ return err
+ }
+ seen := make(map[uint64]struct{})
+ for i, message := range meta.Messages {
+ messageTime, ok := gossipMessageTime(meta.CurrentTimeMS, message.OffsetMS)
+ if !ok {
+ return fmt.Errorf("message %d time overflows", i)
+ }
+ change := &cltypes.SignedBLSToExecutionChange{}
+ if err := spectest.ReadSszOld(root, change, c.Version(), message.Message+".ssz_snappy"); err != nil {
+ return err
+ }
+ result := validateGossipBLSToExecutionChange(beaconState, change, messageTime, config.CapellaForkEpoch, seen)
+ if result != message.Expected {
+ return gossipResultError(i, message, result)
+ }
+ if result == "valid" {
+ seen[change.Message.ValidatorIndex] = struct{}{}
+ }
+ }
+ return nil
+}
+
+func validateGossipBLSToExecutionChange(
+ beaconState *state.CachingBeaconState,
+ signedChange *cltypes.SignedBLSToExecutionChange,
+ currentTimeMS uint64,
+ capellaForkEpoch uint64,
+ seen map[uint64]struct{},
+) string {
+ if signedChange == nil || signedChange.Message == nil {
+ return "reject"
+ }
+ currentSlot, ok := gossipCurrentSlot(beaconState, currentTimeMS)
+ if !ok || currentSlot/beaconState.BeaconConfig().SlotsPerEpoch < capellaForkEpoch {
+ return "ignore"
+ }
+ change := signedChange.Message
+ if _, ok := seen[change.ValidatorIndex]; ok {
+ return "ignore"
+ }
+ validator, err := beaconState.ValidatorForValidatorIndex(int(change.ValidatorIndex))
+ if err != nil {
+ return "reject"
+ }
+ withdrawalCredentials := validator.WithdrawalCredentials()
+ if withdrawalCredentials[0] != byte(beaconState.BeaconConfig().BLSWithdrawalPrefixByte) {
+ return "reject"
+ }
+ hashedFrom := crypto.Sha256(change.From[:])
+ if !bytes.Equal(withdrawalCredentials[1:], hashedFrom[1:]) {
+ return "reject"
+ }
+ domain, err := fork.ComputeDomain(
+ beaconState.BeaconConfig().DomainBLSToExecutionChange[:],
+ utils.Uint32ToBytes4(uint32(beaconState.BeaconConfig().GenesisForkVersion)),
+ beaconState.GenesisValidatorsRoot(),
+ )
+ if err != nil {
+ return "reject"
+ }
+ signingRoot, err := fork.ComputeSigningRoot(change, domain)
+ if err != nil {
+ return "reject"
+ }
+ valid, err := bls.Verify(signedChange.Signature[:], signingRoot[:], change.From[:])
+ if err != nil || !valid {
+ return "reject"
+ }
+ return "valid"
+}
+
+type syncCommitteeMessageKey struct {
+ slot uint64
+ validatorIndex uint64
+ subnetID uint64
+}
+
+func gossipSyncCommitteeMessageHandler(t *testing.T, root fs.FS, c spectest.TestCase) error {
+ beaconState, meta, err := readGossipStateAndMeta(root, c)
+ if err != nil {
+ return err
+ }
+ seen := make(map[syncCommitteeMessageKey]struct{})
+ for i, message := range meta.Messages {
+ messageTime, ok := gossipMessageTime(meta.CurrentTimeMS, message.OffsetMS)
+ if !ok {
+ return fmt.Errorf("message %d time overflows", i)
+ }
+ syncMessage := &cltypes.SyncCommitteeMessage{}
+ if err := spectest.ReadSszOld(root, syncMessage, c.Version(), message.Message+".ssz_snappy"); err != nil {
+ return err
+ }
+ result := validateGossipSyncCommitteeMessage(beaconState, syncMessage, message.SubnetID, messageTime, seen)
+ if result != message.Expected {
+ return gossipResultError(i, message, result)
+ }
+ if result == "valid" {
+ seen[syncCommitteeMessageKey{syncMessage.Slot, syncMessage.ValidatorIndex, message.SubnetID}] = struct{}{}
+ }
+ }
+ return nil
+}
+
+func validateGossipSyncCommitteeMessage(
+ beaconState *state.CachingBeaconState,
+ message *cltypes.SyncCommitteeMessage,
+ subnetID uint64,
+ currentTimeMS uint64,
+ seen map[syncCommitteeMessageKey]struct{},
+) string {
+ if message == nil || !gossipSlotIsCurrent(beaconState, message.Slot, currentTimeMS) {
+ return "ignore"
+ }
+ if message.ValidatorIndex >= uint64(beaconState.ValidatorLength()) {
+ return "reject"
+ }
+ validSubnets, err := subnets.ComputeSubnetsForSyncCommittee(beaconState, message.ValidatorIndex)
+ if err != nil || !slices.Contains(validSubnets, subnetID) {
+ return "reject"
+ }
+ key := syncCommitteeMessageKey{message.Slot, message.ValidatorIndex, subnetID}
+ if _, ok := seen[key]; ok {
+ return "ignore"
+ }
+ publicKey, err := beaconState.ValidatorPublicKey(int(message.ValidatorIndex))
+ if err != nil {
+ return "reject"
+ }
+ domain, err := beaconState.GetDomain(beaconState.BeaconConfig().DomainSyncCommittee, message.Slot/beaconState.BeaconConfig().SlotsPerEpoch)
+ if err != nil {
+ return "reject"
+ }
+ signingRoot := crypto.Sha256(message.BeaconBlockRoot[:], domain)
+ valid, err := bls.Verify(message.Signature[:], signingRoot[:], publicKey[:])
+ if err != nil || !valid {
+ return "reject"
+ }
+ return "valid"
+}
+
+type syncContributionKey struct {
+ slot uint64
+ beaconBlockRoot common.Hash
+ subcommitteeIndex uint64
+}
+
+type syncContributionAggregatorKey struct {
+ aggregatorIndex uint64
+ slot uint64
+ subcommitteeIndex uint64
+}
+
+func gossipSyncContributionHandler(t *testing.T, root fs.FS, c spectest.TestCase) error {
+ beaconState, meta, err := readGossipStateAndMeta(root, c)
+ if err != nil {
+ return err
+ }
+ seenContributions := make(map[syncContributionKey][][]byte)
+ seenAggregators := make(map[syncContributionAggregatorKey]struct{})
+ for i, message := range meta.Messages {
+ messageTime, ok := gossipMessageTime(meta.CurrentTimeMS, message.OffsetMS)
+ if !ok {
+ return fmt.Errorf("message %d time overflows", i)
+ }
+ contribution := &cltypes.Contribution{}
+ contribution.SetAggregationBitsSize(int(beaconState.BeaconConfig().SyncCommitteeSize / beaconState.BeaconConfig().SyncCommitteeSubnetCount / 8))
+ signedContribution := &cltypes.SignedContributionAndProof{Message: &cltypes.ContributionAndProof{Contribution: contribution}}
+ if err := spectest.ReadSszOld(root, signedContribution, c.Version(), message.Message+".ssz_snappy"); err != nil {
+ return err
+ }
+ result := validateGossipSyncContribution(beaconState, signedContribution, messageTime, seenContributions, seenAggregators)
+ if result != message.Expected {
+ return gossipResultError(i, message, result)
+ }
+ if result == "valid" {
+ markGossipSyncContributionSeen(signedContribution.Message, seenContributions, seenAggregators)
+ }
+ }
+ return nil
+}
+
+func validateGossipSyncContribution(
+ beaconState *state.CachingBeaconState,
+ signedContribution *cltypes.SignedContributionAndProof,
+ currentTimeMS uint64,
+ seenContributions map[syncContributionKey][][]byte,
+ seenAggregators map[syncContributionAggregatorKey]struct{},
+) string {
+ if signedContribution == nil || signedContribution.Message == nil || signedContribution.Message.Contribution == nil {
+ return "reject"
+ }
+ message := signedContribution.Message
+ contribution := message.Contribution
+ if !gossipSlotIsCurrent(beaconState, contribution.Slot, currentTimeMS) {
+ return "ignore"
+ }
+ if contribution.SubcommitteeIndex >= beaconState.BeaconConfig().SyncCommitteeSubnetCount {
+ return "reject"
+ }
+ if !gossipBitsHaveParticipants(contribution.AggregationBits) {
+ return "reject"
+ }
+ config := beaconState.BeaconConfig()
+ if config.SyncCommitteeSubnetCount == 0 || config.TargetAggregatorsPerSyncSubcommittee == 0 {
+ return "reject"
+ }
+ modulo := max(uint64(1), config.SyncCommitteeSize/config.SyncCommitteeSubnetCount/config.TargetAggregatorsPerSyncSubcommittee)
+ selectionProofHash := crypto.Sha256(message.SelectionProof[:])
+ if binary.LittleEndian.Uint64(selectionProofHash[:8])%modulo != 0 {
+ return "reject"
+ }
+ if message.AggregatorIndex >= uint64(beaconState.ValidatorLength()) {
+ return "reject"
+ }
+ subcommitteePublicKeys, ok := gossipSyncSubcommitteePublicKeys(beaconState, contribution.SubcommitteeIndex)
+ if !ok || len(contribution.AggregationBits)*8 != len(subcommitteePublicKeys) {
+ return "reject"
+ }
+ aggregatorPublicKey, err := beaconState.ValidatorPublicKey(int(message.AggregatorIndex))
+ if err != nil || !slices.Contains(subcommitteePublicKeys, aggregatorPublicKey) {
+ return "reject"
+ }
+ contributionKey := syncContributionKey{contribution.Slot, contribution.BeaconBlockRoot, contribution.SubcommitteeIndex}
+ for _, seenBits := range seenContributions[contributionKey] {
+ if gossipBitsSuperset(seenBits, contribution.AggregationBits) {
+ return "ignore"
+ }
+ }
+ aggregatorKey := syncContributionAggregatorKey{message.AggregatorIndex, contribution.Slot, contribution.SubcommitteeIndex}
+ if _, ok := seenAggregators[aggregatorKey]; ok {
+ return "ignore"
+ }
+ selectionData := &cltypes.SyncAggregatorSelectionData{Slot: contribution.Slot, SubcommitteeIndex: contribution.SubcommitteeIndex}
+ domain, err := beaconState.GetDomain(beaconState.BeaconConfig().DomainSyncCommitteeSelectionProof, contribution.Slot/beaconState.BeaconConfig().SlotsPerEpoch)
+ if err != nil {
+ return "reject"
+ }
+ signingRoot, err := fork.ComputeSigningRoot(selectionData, domain)
+ if err != nil || !gossipSignatureValid(message.SelectionProof[:], signingRoot[:], aggregatorPublicKey[:]) {
+ return "reject"
+ }
+ domain, err = beaconState.GetDomain(beaconState.BeaconConfig().DomainContributionAndProof, contribution.Slot/beaconState.BeaconConfig().SlotsPerEpoch)
+ if err != nil {
+ return "reject"
+ }
+ signingRoot, err = fork.ComputeSigningRoot(message, domain)
+ if err != nil || !gossipSignatureValid(signedContribution.Signature[:], signingRoot[:], aggregatorPublicKey[:]) {
+ return "reject"
+ }
+ participantPublicKeys := make([][]byte, 0, len(subcommitteePublicKeys))
+ for index, publicKey := range subcommitteePublicKeys {
+ if utils.IsBitOn(contribution.AggregationBits, index) {
+ participantPublicKeys = append(participantPublicKeys, publicKey[:])
+ }
+ }
+ domain, err = beaconState.GetDomain(beaconState.BeaconConfig().DomainSyncCommittee, contribution.Slot/beaconState.BeaconConfig().SlotsPerEpoch)
+ if err != nil {
+ return "reject"
+ }
+ signingRoot = crypto.Sha256(contribution.BeaconBlockRoot[:], domain)
+ valid, err := bls.VerifyAggregate(contribution.Signature[:], signingRoot[:], participantPublicKeys)
+ if err != nil || !valid {
+ return "reject"
+ }
+ return "valid"
+}
+
+func readGossipStateAndMeta(root fs.FS, c spectest.TestCase) (*state.CachingBeaconState, gossipMeta, error) {
+ beaconState, err := spectest.ReadBeaconState(root, c.Version(), "state.ssz_snappy")
+ if err != nil {
+ return nil, gossipMeta{}, err
+ }
+ var meta gossipMeta
+ if err := spectest.ReadMeta(root, "meta.yaml", &meta); err != nil {
+ return nil, gossipMeta{}, err
+ }
+ return beaconState, meta, nil
+}
+
+func gossipSlotIsCurrent(beaconState *state.CachingBeaconState, slot uint64, currentTimeMS uint64) bool {
+ currentSlot, ok := gossipCurrentSlot(beaconState, currentTimeMS)
+ return ok && slot == currentSlot
+}
+
+func gossipCurrentSlot(beaconState *state.CachingBeaconState, currentTimeMS uint64) (uint64, bool) {
+ if beaconState.GenesisTime() > ^uint64(0)/1000 {
+ return 0, false
+ }
+ genesisTimeMS := beaconState.GenesisTime() * 1000
+ if currentTimeMS < genesisTimeMS || beaconState.BeaconConfig().SecondsPerSlot == 0 {
+ return 0, false
+ }
+ return (currentTimeMS - genesisTimeMS) / (beaconState.BeaconConfig().SecondsPerSlot * 1000), true
+}
+
+func gossipSyncSubcommitteePublicKeys(beaconState *state.CachingBeaconState, subcommitteeIndex uint64) ([]common.Bytes48, bool) {
+ config := beaconState.BeaconConfig()
+ if config.SyncCommitteeSubnetCount == 0 || config.SyncCommitteeSize%config.SyncCommitteeSubnetCount != 0 {
+ return nil, false
+ }
+ committee := beaconState.CurrentSyncCommittee()
+ if beaconState.Slot() == ^uint64(0) {
+ return nil, false
+ }
+ if config.SyncCommitteePeriod(beaconState.Slot()) != config.SyncCommitteePeriod(beaconState.Slot()+1) {
+ committee = beaconState.NextSyncCommittee()
+ }
+ if committee == nil {
+ return nil, false
+ }
+ subcommitteeSize := config.SyncCommitteeSize / config.SyncCommitteeSubnetCount
+ if subcommitteeSize == 0 || subcommitteeIndex > ^uint64(0)/subcommitteeSize {
+ return nil, false
+ }
+ start := subcommitteeIndex * subcommitteeSize
+ publicKeys := committee.GetCommittee()
+ if start > uint64(len(publicKeys)) || subcommitteeSize > uint64(len(publicKeys))-start {
+ return nil, false
+ }
+ return publicKeys[start : start+subcommitteeSize], true
+}
+
+func gossipMessageTime(currentTimeMS, offsetMS uint64) (uint64, bool) {
+ if offsetMS > ^uint64(0)-currentTimeMS {
+ return 0, false
+ }
+ return currentTimeMS + offsetMS, true
+}
+
+func markGossipSyncContributionSeen(
+ message *cltypes.ContributionAndProof,
+ seenContributions map[syncContributionKey][][]byte,
+ seenAggregators map[syncContributionAggregatorKey]struct{},
+) {
+ contribution := message.Contribution
+ contributionKey := syncContributionKey{contribution.Slot, contribution.BeaconBlockRoot, contribution.SubcommitteeIndex}
+ seenContributions[contributionKey] = append(seenContributions[contributionKey], bytes.Clone(contribution.AggregationBits))
+ seenAggregators[syncContributionAggregatorKey{message.AggregatorIndex, contribution.Slot, contribution.SubcommitteeIndex}] = struct{}{}
+}
+
+func gossipBitsHaveParticipants(bits []byte) bool {
+ for _, value := range bits {
+ if value != 0 {
+ return true
+ }
+ }
+ return false
+}
+
+func gossipBitsSuperset(superset, subset []byte) bool {
+ if len(superset) != len(subset) {
+ return false
+ }
+ for index := range subset {
+ if superset[index]&subset[index] != subset[index] {
+ return false
+ }
+ }
+ return true
+}
+
+func gossipSignatureValid(signature, signingRoot, publicKey []byte) bool {
+ valid, err := bls.Verify(signature, signingRoot, publicKey)
+ return err == nil && valid
+}
+
+func gossipResultError(index int, message gossipMessage, result string) error {
+ return fmt.Errorf("message %d (%s): expected %q but got %q", index, message.Message, message.Expected, result)
+}
+
func gossipAttesterSlashingHandler(t *testing.T, root fs.FS, c spectest.TestCase) error {
beaconState, err := spectest.ReadBeaconState(root, c.Version(), "state.ssz_snappy")
if err != nil {
diff --git a/cl/spectest/consensus_tests/gossip_bounds_test.go b/cl/spectest/consensus_tests/gossip_bounds_test.go
new file mode 100644
index 00000000000..2927bf03a2c
--- /dev/null
+++ b/cl/spectest/consensus_tests/gossip_bounds_test.go
@@ -0,0 +1,66 @@
+// Copyright 2026 The Erigon Authors
+// This file is part of Erigon.
+//
+// Erigon is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Erigon is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with Erigon. If not, see .
+
+package consensus_tests
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/erigontech/erigon/cl/clparams"
+ "github.com/erigontech/erigon/cl/phase1/core/state"
+)
+
+func TestGossipMessageTimeOverflow(t *testing.T) {
+ _, ok := gossipMessageTime(^uint64(0), 1)
+ require.False(t, ok)
+
+ messageTime, ok := gossipMessageTime(12_000, 500)
+ require.True(t, ok)
+ require.Equal(t, uint64(12_500), messageTime)
+}
+
+func TestGossipCurrentSlotRejectsInvalidTimeConfig(t *testing.T) {
+ config := clparams.MainnetBeaconConfig
+ beaconState := state.New(&config)
+ beaconState.SetGenesisTime(^uint64(0))
+ _, ok := gossipCurrentSlot(beaconState, 0)
+ require.False(t, ok)
+
+ config.SecondsPerSlot = 0
+ beaconState = state.New(&config)
+ _, ok = gossipCurrentSlot(beaconState, 0)
+ require.False(t, ok)
+}
+
+func TestGossipSyncSubcommitteePublicKeysRejectsInvalidBounds(t *testing.T) {
+ config := clparams.MainnetBeaconConfig
+ config.SyncCommitteeSubnetCount = 0
+ _, ok := gossipSyncSubcommitteePublicKeys(state.New(&config), 0)
+ require.False(t, ok)
+
+ config = clparams.MainnetBeaconConfig
+ _, ok = gossipSyncSubcommitteePublicKeys(state.New(&config), config.SyncCommitteeSubnetCount)
+ require.False(t, ok)
+}
+
+func TestGossipBitsSuperset(t *testing.T) {
+ require.True(t, gossipBitsSuperset([]byte{0b1110}, []byte{0b0110}))
+ require.True(t, gossipBitsSuperset([]byte{0b0110}, []byte{0b0110}))
+ require.False(t, gossipBitsSuperset([]byte{0b0010}, []byte{0b0110}))
+ require.False(t, gossipBitsSuperset([]byte{0b0110}, []byte{0b0110, 0}))
+}
diff --git a/cl/spectest/consensus_tests/light_client.go b/cl/spectest/consensus_tests/light_client.go
index 0fd377e995e..55c9d5aefa6 100644
--- a/cl/spectest/consensus_tests/light_client.go
+++ b/cl/spectest/consensus_tests/light_client.go
@@ -41,6 +41,11 @@ var LightClientBeaconBlockBodyExecutionMerkleProof = spectest.HandlerFunc(func(t
require.NoError(t, spectest.ReadSsz(root, c.Version(), spectest.ObjectSSZ, beaconBody))
proof, err = beaconBody.ExecutionPayloadMerkleProof()
require.NoError(t, err)
+ case "execution_block_hash_merkle_proof":
+ beaconBody := cltypes.NewBeaconBody(&clparams.MainnetBeaconConfig, c.Version())
+ require.NoError(t, spectest.ReadSsz(root, c.Version(), spectest.ObjectSSZ, beaconBody))
+ proof, err = beaconBody.ExecutionBlockHashMerkleProof()
+ require.NoError(t, err)
case "current_sync_committee_merkle_proof":
state := state.New(&clparams.MainnetBeaconConfig)
require.NoError(t, spectest.ReadSsz(root, c.Version(), spectest.ObjectSSZ, state))
diff --git a/cl/spectest/consensus_tests/rewards.go b/cl/spectest/consensus_tests/rewards.go
index 778525a5341..021345fb69b 100644
--- a/cl/spectest/consensus_tests/rewards.go
+++ b/cl/spectest/consensus_tests/rewards.go
@@ -17,29 +17,309 @@
package consensus_tests
import (
+ "encoding/binary"
+ "fmt"
"io/fs"
+ "slices"
"testing"
+ "github.com/erigontech/erigon/cl/clparams"
+ "github.com/erigontech/erigon/cl/phase1/core/state"
"github.com/erigontech/erigon/cl/spectest/spectest"
+ "github.com/erigontech/erigon/cl/transition/impl/eth2/statechange"
+ "github.com/erigontech/erigon/common/clonable"
)
type RewardsCore struct{}
-func (b *RewardsCore) Run(t *testing.T, root fs.FS, c spectest.TestCase) (err error) {
- t.Skipf("Skippinf attestation reward calculation tests for now")
- //preState, err := spectest.ReadBeaconState(root, c.Version(), spectest.PreSsz)
- //require.NoError(t, err)
-
- //source_deltas, err := readDelta(root, "source_deltas.ssz_snappy")
- //require.NoError(t, err)
- //target_deltas, err := readDelta(root, "target_deltas.ssz_snappy")
- //require.NoError(t, err)
- //head_deltas, err := readDelta(root, "head_deltas.ssz_snappy")
- //require.NoError(t, err)
- //inclusion_delay_deltas, err := readDelta(root, "inclusion_delay_deltas.ssz_snappy")
- //require.NoError(t, err)
- //inactivity_penalty_deltas, err := readDelta(root, "inactivity_penalty_deltas.ssz_snappy")
- //require.NoError(t, err)
+type rewardDeltas struct {
+ rewards []uint64
+ penalties []uint64
+}
+
+func (*rewardDeltas) Clone() clonable.Clonable { return &rewardDeltas{} }
+
+func (d *rewardDeltas) DecodeSSZ(buf []byte, _ int) error {
+ if len(buf) < 8 {
+ return fmt.Errorf("reward deltas are too short: %d", len(buf))
+ }
+ rewardsOffset := int(binary.LittleEndian.Uint32(buf))
+ penaltiesOffset := int(binary.LittleEndian.Uint32(buf[4:]))
+ if rewardsOffset != 8 || penaltiesOffset < rewardsOffset || penaltiesOffset > len(buf) {
+ return fmt.Errorf("invalid reward delta offsets: %d, %d", rewardsOffset, penaltiesOffset)
+ }
+ rewardsBytes, penaltiesBytes := buf[rewardsOffset:penaltiesOffset], buf[penaltiesOffset:]
+ if len(rewardsBytes)%8 != 0 || len(penaltiesBytes)%8 != 0 {
+ return fmt.Errorf("invalid reward delta lengths: %d, %d", len(rewardsBytes), len(penaltiesBytes))
+ }
+ d.rewards = decodeUint64s(rewardsBytes)
+ d.penalties = decodeUint64s(penaltiesBytes)
+ return nil
+}
+
+func decodeUint64s(buf []byte) []uint64 {
+ values := make([]uint64, len(buf)/8)
+ for i := range values {
+ values[i] = binary.LittleEndian.Uint64(buf[i*8:])
+ }
+ return values
+}
+
+func (b *RewardsCore) Run(t *testing.T, root fs.FS, c spectest.TestCase) error {
+ preState, err := spectest.ReadBeaconState(root, c.Version(), spectest.PreSsz)
+ if err != nil {
+ return err
+ }
+ if preState.Version() == clparams.Phase0Version {
+ return runPhase0Rewards(root, c, preState)
+ }
+ validatorCount := preState.ValidatorLength()
+ eligible := state.EligibleValidatorsIndicies(preState)
+ participation := statechange.GetUnslashedIndiciesSet(
+ preState.BeaconConfig(),
+ state.PreviousEpoch(preState),
+ preState.ValidatorSet(),
+ preState.PreviousEpochParticipation(),
+ )
+ weights := preState.BeaconConfig().ParticipationWeights()
+ activeIncrements := preState.GetTotalActiveBalance() / preState.BeaconConfig().EffectiveBalanceIncrement
+ if activeIncrements == 0 {
+ return fmt.Errorf("active balance has no effective balance increments")
+ }
+
+ participatingIncrements := make([]uint64, len(weights))
+ for flagIndex := range weights {
+ for validatorIndex := range validatorCount {
+ if !participation[flagIndex][validatorIndex] {
+ continue
+ }
+ effectiveBalance, err := preState.ValidatorEffectiveBalance(validatorIndex)
+ if err != nil {
+ return err
+ }
+ participatingIncrements[flagIndex] += effectiveBalance / preState.BeaconConfig().EffectiveBalanceIncrement
+ }
+ }
+
+ for flagIndex, name := range []string{"source_deltas.ssz_snappy", "target_deltas.ssz_snappy", "head_deltas.ssz_snappy"} {
+ have, err := flagRewardDeltas(preState, validatorCount, eligible, participation[flagIndex], weights[flagIndex], participatingIncrements[flagIndex], activeIncrements, flagIndex)
+ if err != nil {
+ return err
+ }
+ if err := compareRewardDeltas(root, c, name, have); err != nil {
+ return err
+ }
+ }
+
+ have, err := inactivityRewardDeltas(preState, validatorCount, eligible, participation[preState.BeaconConfig().TimelyTargetFlagIndex])
+ if err != nil {
+ return err
+ }
+ return compareRewardDeltas(root, c, "inactivity_penalty_deltas.ssz_snappy", have)
+}
+
+func runPhase0Rewards(root fs.FS, c spectest.TestCase, beaconState *state.CachingBeaconState) error {
+ validatorCount := beaconState.ValidatorLength()
+ eligible := state.EligibleValidatorsIndicies(beaconState)
+ matching := make([][]bool, 3)
+ for flagIndex := range matching {
+ matching[flagIndex] = make([]bool, validatorCount)
+ }
+ for validatorIndex := range validatorCount {
+ var values [3]bool
+ var err error
+ values[0], err = beaconState.ValidatorIsPreviousMatchingSourceAttester(validatorIndex)
+ if err != nil {
+ return err
+ }
+ values[1], err = beaconState.ValidatorIsPreviousMatchingTargetAttester(validatorIndex)
+ if err != nil {
+ return err
+ }
+ values[2], err = beaconState.ValidatorIsPreviousMatchingHeadAttester(validatorIndex)
+ if err != nil {
+ return err
+ }
+ validator, err := beaconState.ValidatorForValidatorIndex(validatorIndex)
+ if err != nil {
+ return err
+ }
+ if validator.Slashed() {
+ continue
+ }
+ for flagIndex := range values {
+ matching[flagIndex][validatorIndex] = values[flagIndex]
+ }
+ }
+
+ for flagIndex, name := range []string{"source_deltas.ssz_snappy", "target_deltas.ssz_snappy", "head_deltas.ssz_snappy"} {
+ have, err := phase0ComponentRewardDeltas(beaconState, eligible, matching[flagIndex])
+ if err != nil {
+ return err
+ }
+ if err := compareRewardDeltas(root, c, name, have); err != nil {
+ return err
+ }
+ }
+
+ have, err := phase0InclusionDelayRewardDeltas(beaconState, matching[0])
+ if err != nil {
+ return err
+ }
+ if err := compareRewardDeltas(root, c, "inclusion_delay_deltas.ssz_snappy", have); err != nil {
+ return err
+ }
+ have, err = phase0InactivityRewardDeltas(beaconState, eligible, matching[1])
+ if err != nil {
+ return err
+ }
+ return compareRewardDeltas(root, c, "inactivity_penalty_deltas.ssz_snappy", have)
+}
+
+func phase0ComponentRewardDeltas(beaconState *state.CachingBeaconState, eligible []uint64, matching []bool) (*rewardDeltas, error) {
+ validatorCount := beaconState.ValidatorLength()
+ deltas := &rewardDeltas{rewards: make([]uint64, validatorCount), penalties: make([]uint64, validatorCount)}
+ increment := beaconState.BeaconConfig().EffectiveBalanceIncrement
+ attestingBalance := increment
+ var sum uint64
+ for validatorIndex, isMatching := range matching {
+ if !isMatching {
+ continue
+ }
+ effectiveBalance, err := beaconState.ValidatorEffectiveBalance(validatorIndex)
+ if err != nil {
+ return nil, err
+ }
+ sum += effectiveBalance
+ }
+ attestingBalance = max(attestingBalance, sum)
+ activeIncrements := beaconState.GetTotalActiveBalance() / increment
+ if activeIncrements == 0 {
+ return nil, fmt.Errorf("active balance has no effective balance increments")
+ }
+ for _, validatorIndex := range eligible {
+ baseReward, err := beaconState.BaseReward(validatorIndex)
+ if err != nil {
+ return nil, err
+ }
+ if matching[validatorIndex] {
+ if state.InactivityLeaking(beaconState) {
+ deltas.rewards[validatorIndex] = baseReward
+ } else {
+ deltas.rewards[validatorIndex] = baseReward * (attestingBalance / increment) / activeIncrements
+ }
+ } else {
+ deltas.penalties[validatorIndex] = baseReward
+ }
+ }
+ return deltas, nil
+}
+
+func phase0InclusionDelayRewardDeltas(beaconState *state.CachingBeaconState, matchingSource []bool) (*rewardDeltas, error) {
+ deltas := &rewardDeltas{rewards: make([]uint64, beaconState.ValidatorLength()), penalties: make([]uint64, beaconState.ValidatorLength())}
+ for validatorIndex, isMatching := range matchingSource {
+ if !isMatching {
+ continue
+ }
+ attestation, err := beaconState.ValidatorMinPreviousInclusionDelayAttestation(validatorIndex)
+ if err != nil {
+ return nil, err
+ }
+ if attestation == nil || attestation.InclusionDelay == 0 || attestation.ProposerIndex >= uint64(beaconState.ValidatorLength()) {
+ return nil, fmt.Errorf("invalid inclusion delay attestation for validator %d", validatorIndex)
+ }
+ baseReward, err := beaconState.BaseReward(uint64(validatorIndex))
+ if err != nil {
+ return nil, err
+ }
+ proposerReward := baseReward / beaconState.BeaconConfig().ProposerRewardQuotient
+ deltas.rewards[attestation.ProposerIndex] += proposerReward
+ deltas.rewards[validatorIndex] += (baseReward - proposerReward) / attestation.InclusionDelay
+ }
+ return deltas, nil
+}
+
+func phase0InactivityRewardDeltas(beaconState *state.CachingBeaconState, eligible []uint64, matchingTarget []bool) (*rewardDeltas, error) {
+ deltas := &rewardDeltas{rewards: make([]uint64, beaconState.ValidatorLength()), penalties: make([]uint64, beaconState.ValidatorLength())}
+ if !state.InactivityLeaking(beaconState) {
+ return deltas, nil
+ }
+ for _, validatorIndex := range eligible {
+ baseReward, err := beaconState.BaseReward(validatorIndex)
+ if err != nil {
+ return nil, err
+ }
+ proposerReward := baseReward / beaconState.BeaconConfig().ProposerRewardQuotient
+ deltas.penalties[validatorIndex] = beaconState.BeaconConfig().BaseRewardsPerEpoch*baseReward - proposerReward
+ validator, err := beaconState.ValidatorForValidatorIndex(int(validatorIndex))
+ if err != nil {
+ return nil, err
+ }
+ if validator.Slashed() || !matchingTarget[validatorIndex] {
+ deltas.penalties[validatorIndex] += validator.EffectiveBalance() * state.FinalityDelay(beaconState) / beaconState.BeaconConfig().InactivityPenaltyQuotient
+ }
+ }
+ return deltas, nil
+}
+
+func flagRewardDeltas(
+ beaconState *state.CachingBeaconState,
+ validatorCount int,
+ eligible []uint64,
+ participating []bool,
+ weight uint64,
+ participatingIncrements uint64,
+ activeIncrements uint64,
+ flagIndex int,
+) (*rewardDeltas, error) {
+ deltas := &rewardDeltas{rewards: make([]uint64, validatorCount), penalties: make([]uint64, validatorCount)}
+ denominator := activeIncrements * beaconState.BeaconConfig().WeightDenominator
+ for _, validatorIndex := range eligible {
+ baseReward, err := beaconState.BaseReward(validatorIndex)
+ if err != nil {
+ return nil, err
+ }
+ if participating[validatorIndex] {
+ if !state.InactivityLeaking(beaconState) {
+ deltas.rewards[validatorIndex] = baseReward * weight * participatingIncrements / denominator
+ }
+ } else if flagIndex != int(beaconState.BeaconConfig().TimelyHeadFlagIndex) {
+ deltas.penalties[validatorIndex] = baseReward * weight / beaconState.BeaconConfig().WeightDenominator
+ }
+ }
+ return deltas, nil
+}
+
+func inactivityRewardDeltas(beaconState *state.CachingBeaconState, validatorCount int, eligible []uint64, timelyTarget []bool) (*rewardDeltas, error) {
+ deltas := &rewardDeltas{rewards: make([]uint64, validatorCount), penalties: make([]uint64, validatorCount)}
+ denominator := beaconState.BeaconConfig().InactivityScoreBias * beaconState.BeaconConfig().GetPenaltyQuotient(beaconState.Version())
+ if denominator == 0 {
+ return nil, fmt.Errorf("inactivity penalty denominator is zero")
+ }
+ for _, validatorIndex := range eligible {
+ if timelyTarget[validatorIndex] {
+ continue
+ }
+ effectiveBalance, err := beaconState.ValidatorEffectiveBalance(int(validatorIndex))
+ if err != nil {
+ return nil, err
+ }
+ inactivityScore, err := beaconState.ValidatorInactivityScore(int(validatorIndex))
+ if err != nil {
+ return nil, err
+ }
+ deltas.penalties[validatorIndex] = effectiveBalance * inactivityScore / denominator
+ }
+ return deltas, nil
+}
+func compareRewardDeltas(root fs.FS, c spectest.TestCase, name string, have *rewardDeltas) error {
+ want := &rewardDeltas{}
+ if err := spectest.ReadSsz(root, c.Version(), name, want); err != nil {
+ return err
+ }
+ if !slices.Equal(want.rewards, have.rewards) || !slices.Equal(want.penalties, have.penalties) {
+ return fmt.Errorf("%s mismatch", name)
+ }
return nil
}
diff --git a/cl/spectest/consensus_tests/rewards_test.go b/cl/spectest/consensus_tests/rewards_test.go
new file mode 100644
index 00000000000..41a997d7768
--- /dev/null
+++ b/cl/spectest/consensus_tests/rewards_test.go
@@ -0,0 +1,62 @@
+// Copyright 2026 The Erigon Authors
+// This file is part of Erigon.
+//
+// Erigon is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Erigon is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with Erigon. If not, see .
+
+package consensus_tests
+
+import (
+ "encoding/binary"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestRewardDeltasDecodeSSZRejectsInvalidBounds(t *testing.T) {
+ tests := []struct {
+ name string
+ buf []byte
+ }{
+ {name: "short", buf: make([]byte, 7)},
+ {name: "wrong first offset", buf: rewardDeltaOffsets(4, 8, 8)},
+ {name: "reversed offsets", buf: rewardDeltaOffsets(8, 7, 8)},
+ {name: "offset past end", buf: rewardDeltaOffsets(8, 16, 8)},
+ {name: "partial uint64", buf: rewardDeltaOffsets(8, 9, 9)},
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ require.Error(t, new(rewardDeltas).DecodeSSZ(test.buf, 0))
+ })
+ }
+}
+
+func TestRewardDeltasDecodeSSZ(t *testing.T) {
+ buf := rewardDeltaOffsets(8, 16, 24)
+ binary.LittleEndian.PutUint64(buf[8:], 11)
+ binary.LittleEndian.PutUint64(buf[16:], 22)
+
+ deltas := new(rewardDeltas)
+ require.NoError(t, deltas.DecodeSSZ(buf, 0))
+ require.Equal(t, []uint64{11}, deltas.rewards)
+ require.Equal(t, []uint64{22}, deltas.penalties)
+}
+
+func rewardDeltaOffsets(rewardsOffset, penaltiesOffset uint32, size int) []byte {
+ buf := make([]byte, size)
+ if size >= 8 {
+ binary.LittleEndian.PutUint32(buf, rewardsOffset)
+ binary.LittleEndian.PutUint32(buf[4:], penaltiesOffset)
+ }
+ return buf
+}
diff --git a/cl/spectest/consensus_tests/ssz_static_helpers.go b/cl/spectest/consensus_tests/ssz_static_helpers.go
new file mode 100644
index 00000000000..7044f4c23d1
--- /dev/null
+++ b/cl/spectest/consensus_tests/ssz_static_helpers.go
@@ -0,0 +1,138 @@
+package consensus_tests
+
+import (
+ "github.com/erigontech/erigon/cl/clparams"
+ "github.com/erigontech/erigon/cl/merkle_tree"
+ ssz2 "github.com/erigontech/erigon/cl/ssz"
+ "github.com/erigontech/erigon/common"
+ "github.com/erigontech/erigon/common/clonable"
+)
+
+type depositMessage struct {
+ Pubkey common.Bytes48
+ WithdrawalCredentials common.Hash
+ Amount uint64
+}
+
+func (*depositMessage) Clone() clonable.Clonable { return &depositMessage{} }
+func (*depositMessage) Static() bool { return true }
+func (*depositMessage) EncodingSizeSSZ() int { return 88 }
+func (d *depositMessage) EncodeSSZ(dst []byte) ([]byte, error) {
+ return ssz2.MarshalSSZ(dst, d.Pubkey[:], d.WithdrawalCredentials[:], d.Amount)
+}
+func (d *depositMessage) DecodeSSZ(buf []byte, version int) error {
+ return ssz2.UnmarshalSSZ(buf, version, d.Pubkey[:], d.WithdrawalCredentials[:], &d.Amount)
+}
+func (d *depositMessage) HashSSZ() ([32]byte, error) {
+ return merkle_tree.HashTreeRoot(d.Pubkey[:], d.WithdrawalCredentials[:], d.Amount)
+}
+
+type validatorEth1Block struct {
+ Timestamp uint64
+ DepositRoot common.Hash
+ DepositCount uint64
+}
+
+func (*validatorEth1Block) Clone() clonable.Clonable { return &validatorEth1Block{} }
+func (*validatorEth1Block) Static() bool { return true }
+func (*validatorEth1Block) EncodingSizeSSZ() int { return 48 }
+func (b *validatorEth1Block) EncodeSSZ(dst []byte) ([]byte, error) {
+ return ssz2.MarshalSSZ(dst, b.Timestamp, b.DepositRoot[:], b.DepositCount)
+}
+func (b *validatorEth1Block) DecodeSSZ(buf []byte, version int) error {
+ return ssz2.UnmarshalSSZ(buf, version, &b.Timestamp, b.DepositRoot[:], &b.DepositCount)
+}
+func (b *validatorEth1Block) HashSSZ() ([32]byte, error) {
+ return merkle_tree.HashTreeRoot(b.Timestamp, b.DepositRoot[:], b.DepositCount)
+}
+
+type forkData struct {
+ CurrentVersion [4]byte
+ GenesisValidatorsRoot common.Hash
+}
+
+func (*forkData) Clone() clonable.Clonable { return &forkData{} }
+func (*forkData) Static() bool { return true }
+func (*forkData) EncodingSizeSSZ() int { return 36 }
+func (d *forkData) EncodeSSZ(dst []byte) ([]byte, error) {
+ return ssz2.MarshalSSZ(dst, d.CurrentVersion[:], d.GenesisValidatorsRoot[:])
+}
+func (d *forkData) DecodeSSZ(buf []byte, version int) error {
+ return ssz2.UnmarshalSSZ(buf, version, d.CurrentVersion[:], d.GenesisValidatorsRoot[:])
+}
+func (d *forkData) HashSSZ() ([32]byte, error) {
+ return merkle_tree.HashTreeRoot(d.CurrentVersion[:], d.GenesisValidatorsRoot[:])
+}
+
+type powBlock struct {
+ BlockHash common.Hash
+ ParentHash common.Hash
+ TotalDifficulty [32]byte
+}
+
+func (*powBlock) Clone() clonable.Clonable { return &powBlock{} }
+func (*powBlock) Static() bool { return true }
+func (*powBlock) EncodingSizeSSZ() int { return 96 }
+func (b *powBlock) EncodeSSZ(dst []byte) ([]byte, error) {
+ return ssz2.MarshalSSZ(dst, b.BlockHash[:], b.ParentHash[:], b.TotalDifficulty[:])
+}
+func (b *powBlock) DecodeSSZ(buf []byte, version int) error {
+ return ssz2.UnmarshalSSZ(buf, version, b.BlockHash[:], b.ParentHash[:], b.TotalDifficulty[:])
+}
+func (b *powBlock) HashSSZ() ([32]byte, error) {
+ return merkle_tree.HashTreeRoot(b.BlockHash[:], b.ParentHash[:], b.TotalDifficulty[:])
+}
+
+type signingData struct {
+ ObjectRoot common.Hash
+ Domain common.Hash
+}
+
+func (*signingData) Clone() clonable.Clonable { return &signingData{} }
+func (*signingData) Static() bool { return true }
+func (*signingData) EncodingSizeSSZ() int { return 64 }
+func (d *signingData) EncodeSSZ(dst []byte) ([]byte, error) {
+ return ssz2.MarshalSSZ(dst, d.ObjectRoot[:], d.Domain[:])
+}
+func (d *signingData) DecodeSSZ(buf []byte, version int) error {
+ return ssz2.UnmarshalSSZ(buf, version, d.ObjectRoot[:], d.Domain[:])
+}
+func (d *signingData) HashSSZ() ([32]byte, error) {
+ return merkle_tree.HashTreeRoot(d.ObjectRoot[:], d.Domain[:])
+}
+
+type partialDataColumnGroupID struct {
+ BeaconBlockRoot common.Hash
+ Slot uint64
+ version clparams.StateVersion
+}
+
+func (g *partialDataColumnGroupID) Clone() clonable.Clonable {
+ return &partialDataColumnGroupID{version: g.version}
+}
+func (*partialDataColumnGroupID) Static() bool { return true }
+func (g *partialDataColumnGroupID) SetVersion(version clparams.StateVersion) {
+ g.version = version
+}
+func (g *partialDataColumnGroupID) schema() []any {
+ if g.version >= clparams.GloasVersion {
+ return []any{g.BeaconBlockRoot[:], &g.Slot}
+ }
+ return []any{g.BeaconBlockRoot[:]}
+}
+func (g *partialDataColumnGroupID) EncodingSizeSSZ() int {
+ if g.version >= clparams.GloasVersion {
+ return 40
+ }
+ return 32
+}
+func (g *partialDataColumnGroupID) EncodeSSZ(dst []byte) ([]byte, error) {
+ return ssz2.MarshalSSZ(dst, g.schema()...)
+}
+func (g *partialDataColumnGroupID) DecodeSSZ(buf []byte, version int) error {
+ g.version = clparams.StateVersion(version)
+ return ssz2.UnmarshalSSZ(buf, version, g.schema()...)
+}
+func (g *partialDataColumnGroupID) HashSSZ() ([32]byte, error) {
+ return merkle_tree.HashTreeRoot(g.schema()...)
+}
diff --git a/cl/ssz/decode.go b/cl/ssz/decode.go
index 9317b74c36f..18f0412e710 100644
--- a/cl/ssz/decode.go
+++ b/cl/ssz/decode.go
@@ -89,14 +89,15 @@ func UnmarshalSSZ(buf []byte, version int, schema ...any) (err error) {
case SizedObjectSSZ:
// If the element implements the SizedObjectSSZ interface
if obj.Static() {
- if len(buf) < position+obj.EncodingSizeSSZ() {
+ size := obj.EncodingSizeSSZ()
+ if len(buf) < position+size {
return ssz.ErrLowBufferSize
}
// If the object is static (fixed size), decode it from the buf and update the position
- if err = obj.DecodeSSZ(buf[position:], version); err != nil {
+ if err = obj.DecodeSSZ(buf[position:position+size], version); err != nil {
return fmt.Errorf("static element %d: %w", i, err)
}
- position += obj.EncodingSizeSSZ()
+ position += size
} else {
if len(buf) < position+4 {
return ssz.ErrLowBufferSize
diff --git a/cl/transition/impl/eth2/operations.go b/cl/transition/impl/eth2/operations.go
index 9b60f46f735..679005b04aa 100644
--- a/cl/transition/impl/eth2/operations.go
+++ b/cl/transition/impl/eth2/operations.go
@@ -623,6 +623,38 @@ func (imp *impl) ProcessExecutionPayloadBid(s abstract.BeaconState, block cltype
// payment, and updates latest_block_hash. This is the spec's apply_parent_execution_payload.
// [New in Gloas:EIP7732]
func (imp *impl) ApplyParentExecutionPayload(s abstract.BeaconState, requests *cltypes.ExecutionRequests) error {
+ if requests == nil {
+ return errors.New("ApplyParentExecutionPayload: nil execution requests")
+ }
+ cfg := s.BeaconConfig()
+ withdrawalCount, consolidationCount, builderDepositCount, builderExitCount := 0, 0, 0, 0
+ if requests.Withdrawals != nil {
+ withdrawalCount = requests.Withdrawals.Len()
+ }
+ if requests.Consolidations != nil {
+ consolidationCount = requests.Consolidations.Len()
+ }
+ if requests.BuilderDeposits != nil {
+ builderDepositCount = requests.BuilderDeposits.Len()
+ }
+ if requests.BuilderExits != nil {
+ builderExitCount = requests.BuilderExits.Len()
+ }
+ requestCounts := []struct {
+ name string
+ count int
+ limit uint64
+ }{
+ {"withdrawal", withdrawalCount, cfg.MaxWithdrawalRequestsPerPayload},
+ {"consolidation", consolidationCount, cfg.MaxConsolidationRequestsPerPayload},
+ {"builder deposit", builderDepositCount, cfg.MaxBuilderDepositRequestsPerPayload},
+ {"builder exit", builderExitCount, cfg.MaxBuilderExitRequestsPerPayload},
+ }
+ for _, requestCount := range requestCounts {
+ if uint64(requestCount.count) > requestCount.limit {
+ return fmt.Errorf("ApplyParentExecutionPayload: too many %s requests: %d > %d", requestCount.name, requestCount.count, requestCount.limit)
+ }
+ }
parentBid := s.GetLatestExecutionPayloadBid()
// Process execution requests (deposits, withdrawals, consolidations)
if requests.Deposits != nil {
diff --git a/cl/transition/impl/eth2/operations_gloas_test.go b/cl/transition/impl/eth2/operations_gloas_test.go
index 3c2b6aa096d..ab8da6f1ea7 100644
--- a/cl/transition/impl/eth2/operations_gloas_test.go
+++ b/cl/transition/impl/eth2/operations_gloas_test.go
@@ -88,8 +88,9 @@ func TestProcessBuilderDepositRequestTopsUpExistingBuilder(t *testing.T) {
machine := ð2.Impl{}
err := machine.ProcessBuilderDepositRequest(s, &solid.BuilderDepositRequest{
- PubKey: pubkey,
- Amount: 25,
+ PubKey: pubkey,
+ WithdrawalCredentials: common.Hash{byte(cfg.BuilderWithdrawalPrefix)},
+ Amount: 25,
})
require.NoError(t, err)
@@ -113,8 +114,9 @@ func TestProcessBuilderDepositRequestRejectsBalanceOverflow(t *testing.T) {
machine := ð2.Impl{}
err := machine.ProcessBuilderDepositRequest(s, &solid.BuilderDepositRequest{
- PubKey: pubkey,
- Amount: 1,
+ PubKey: pubkey,
+ WithdrawalCredentials: common.Hash{byte(cfg.BuilderWithdrawalPrefix)},
+ Amount: 1,
})
require.Error(t, err)
diff --git a/cl/transition/machine/block.go b/cl/transition/machine/block.go
index fa665b448a3..40381b1759a 100644
--- a/cl/transition/machine/block.go
+++ b/cl/transition/machine/block.go
@@ -160,6 +160,11 @@ func ProcessOperations(impl BlockOperationProcessor, s abstract.BeaconState, blo
case blockBody.GetDeposits().Len() != 0:
return nil, nil, nil, errors.New("old-style deposits are not allowed after Fulu")
}
+ if s.Version() >= clparams.GloasVersion {
+ if err := validateGloasOperationCounts(blockBody, s.BeaconConfig()); err != nil {
+ return nil, nil, nil, err
+ }
+ }
// Process each proposer slashing
sigs, msgs, pubKeys, err := processProposerSlashings(impl, s, blockBody)
@@ -225,6 +230,27 @@ func ProcessOperations(impl BlockOperationProcessor, s abstract.BeaconState, blo
return
}
+func validateGloasOperationCounts(blockBody cltypes.GenericBeaconBody, cfg *clparams.BeaconChainConfig) error {
+ operationCounts := []struct {
+ name string
+ count int
+ limit uint64
+ }{
+ {"proposer slashings", blockBody.GetProposerSlashings().Len(), cfg.MaxProposerSlashings},
+ {"attester slashings", blockBody.GetAttesterSlashings().Len(), cfg.MaxAttesterSlashingsElectra},
+ {"attestations", blockBody.GetAttestations().Len(), cfg.MaxAttestationsElectra},
+ {"voluntary exits", blockBody.GetVoluntaryExits().Len(), cfg.MaxVoluntaryExits},
+ {"BLS-to-execution changes", blockBody.GetExecutionChanges().Len(), cfg.MaxBlsToExecutionChanges},
+ {"payload attestations", blockBody.GetPayloadAttestations().Len(), cfg.MaxPayloadAttestations},
+ }
+ for _, operationCount := range operationCounts {
+ if uint64(operationCount.count) > operationCount.limit {
+ return fmt.Errorf("too many %s: %d > %d", operationCount.name, operationCount.count, operationCount.limit)
+ }
+ }
+ return nil
+}
+
func forEachProcess[T solid.EncodableHashableSSZ](
s abstract.BeaconState,
list *solid.ListSSZ[T],
diff --git a/cl/transition/machine/block_gloas_test.go b/cl/transition/machine/block_gloas_test.go
new file mode 100644
index 00000000000..fbb28f5a093
--- /dev/null
+++ b/cl/transition/machine/block_gloas_test.go
@@ -0,0 +1,96 @@
+package machine
+
+import (
+ "testing"
+
+ "github.com/erigontech/erigon/cl/abstract"
+ "github.com/erigontech/erigon/cl/clparams"
+ "github.com/erigontech/erigon/cl/cltypes"
+ "github.com/erigontech/erigon/cl/cltypes/solid"
+ corestate "github.com/erigontech/erigon/cl/phase1/core/state"
+ "github.com/stretchr/testify/require"
+)
+
+type noopBlockOperationProcessor struct{}
+
+func (noopBlockOperationProcessor) ProcessProposerSlashing(abstract.BeaconState, *cltypes.ProposerSlashing) error {
+ return nil
+}
+func (noopBlockOperationProcessor) ProcessAttesterSlashing(abstract.BeaconState, *cltypes.AttesterSlashing) error {
+ return nil
+}
+func (noopBlockOperationProcessor) ProcessAttestations(abstract.BeaconState, *solid.ListSSZ[*solid.Attestation]) error {
+ return nil
+}
+func (noopBlockOperationProcessor) ProcessDeposit(abstract.BeaconState, *cltypes.Deposit) error {
+ return nil
+}
+func (noopBlockOperationProcessor) ProcessVoluntaryExit(abstract.BeaconState, *cltypes.SignedVoluntaryExit) error {
+ return nil
+}
+func (noopBlockOperationProcessor) ProcessBlsToExecutionChange(abstract.BeaconState, *cltypes.SignedBLSToExecutionChange) error {
+ return nil
+}
+func (noopBlockOperationProcessor) ProcessDepositRequest(abstract.BeaconState, *solid.DepositRequest) error {
+ return nil
+}
+func (noopBlockOperationProcessor) ProcessWithdrawalRequest(abstract.BeaconState, *solid.WithdrawalRequest) error {
+ return nil
+}
+func (noopBlockOperationProcessor) ProcessConsolidationRequest(abstract.BeaconState, *solid.ConsolidationRequest) error {
+ return nil
+}
+func (noopBlockOperationProcessor) ProcessBuilderDepositRequest(abstract.BeaconState, *solid.BuilderDepositRequest) error {
+ return nil
+}
+func (noopBlockOperationProcessor) ProcessBuilderExitRequest(abstract.BeaconState, *solid.BuilderExitRequest) error {
+ return nil
+}
+func (noopBlockOperationProcessor) ProcessPayloadAttestation(abstract.BeaconState, *cltypes.PayloadAttestation) error {
+ return nil
+}
+func (noopBlockOperationProcessor) FullValidate() bool { return false }
+
+func TestProcessOperationsRejectsOversizedGloasLists(t *testing.T) {
+ cfg := clparams.MainnetBeaconConfig
+ tests := []struct {
+ name string
+ limit uint64
+ append func(*cltypes.BeaconBody)
+ }{
+ {"proposer slashings", cfg.MaxProposerSlashings, func(body *cltypes.BeaconBody) {
+ body.ProposerSlashings.Append(&cltypes.ProposerSlashing{})
+ }},
+ {"attester slashings", cfg.MaxAttesterSlashingsElectra, func(body *cltypes.BeaconBody) {
+ body.AttesterSlashings.Append(&cltypes.AttesterSlashing{})
+ }},
+ {"attestations", cfg.MaxAttestationsElectra, func(body *cltypes.BeaconBody) {
+ body.Attestations.Append(&solid.Attestation{})
+ }},
+ {"voluntary exits", cfg.MaxVoluntaryExits, func(body *cltypes.BeaconBody) {
+ body.VoluntaryExits.Append(&cltypes.SignedVoluntaryExit{})
+ }},
+ {"BLS-to-execution changes", cfg.MaxBlsToExecutionChanges, func(body *cltypes.BeaconBody) {
+ body.ExecutionChanges.Append(&cltypes.SignedBLSToExecutionChange{})
+ }},
+ {"payload attestations", cfg.MaxPayloadAttestations, func(body *cltypes.BeaconBody) {
+ body.PayloadAttestations.Append(&cltypes.PayloadAttestation{})
+ }},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ s := corestate.New(&cfg)
+ s.SetVersion(clparams.GloasVersion)
+ body := cltypes.NewBeaconBody(&cfg, clparams.GloasVersion)
+ for range test.limit {
+ test.append(body)
+ }
+ require.NoError(t, validateGloasOperationCounts(body, &cfg))
+ test.append(body)
+
+ _, _, _, err := ProcessOperations(noopBlockOperationProcessor{}, s, body)
+ require.ErrorContains(t, err, "too many "+test.name)
+ })
+ }
+}
diff --git a/cl/validator/devvalidator/aggregate.go b/cl/validator/devvalidator/aggregate.go
index 029561557c1..52a6eab100b 100644
--- a/cl/validator/devvalidator/aggregate.go
+++ b/cl/validator/devvalidator/aggregate.go
@@ -55,6 +55,8 @@ func (s *Service) submitAggregateAndProof(
Aggregate: aggregate,
SelectionProof: selectionProof,
}
+ version := s.cfg.GetCurrentStateVersion(slot / s.cfg.SlotsPerEpoch)
+ msg.SetVersion(version)
aggregatorSig, err := signAggregateAndProof(key, msg, slot, s.cfg, s.genesisValidatorsRoot)
if err != nil {
s.logger.Warn("[dev-validator] aggregate sign failed", "err", err)
@@ -64,6 +66,7 @@ func (s *Service) submitAggregateAndProof(
Message: msg,
Signature: aggregatorSig,
}
+ signed.SetVersion(version)
if err := s.client.post(ctx, "/eth/v1/validator/aggregate_and_proofs", []any{signed}); err != nil {
s.logger.Debug("[dev-validator] aggregate submit failed", "slot", slot, "err", err)
}
diff --git a/cl/validator/devvalidator/aggregate_test.go b/cl/validator/devvalidator/aggregate_test.go
index e3d7f87c082..b56151faf87 100644
--- a/cl/validator/devvalidator/aggregate_test.go
+++ b/cl/validator/devvalidator/aggregate_test.go
@@ -32,6 +32,28 @@ func TestBuildAggregateAttestation(t *testing.T) {
require.Equal(t, testAttData().Slot, agg.Data.Slot)
}
+func TestBuildAggregateAttestationUsesSlotForkVersion(t *testing.T) {
+ cfg := clparams.MainnetBeaconConfig
+ cfg.AltairForkEpoch = 0
+ cfg.BellatrixForkEpoch = 0
+ cfg.CapellaForkEpoch = 0
+ cfg.DenebForkEpoch = 0
+ cfg.ElectraForkEpoch = 0
+ cfg.FuluForkEpoch = 0
+ cfg.GloasForkEpoch = 0
+ single := &solid.SingleAttestation{
+ Data: testAttData(),
+ Signature: common.Bytes96{0x01},
+ }
+
+ aggregate := buildAggregateAttestation(single, 0, 4, &cfg)
+ got, err := aggregate.HashSSZ()
+ require.NoError(t, err)
+ want, err := aggregate.HashSSZProgressive()
+ require.NoError(t, err)
+ require.Equal(t, want, got)
+}
+
// TestSignedAggregateAndProof_RoundTrip verifies a SignedAggregateAndProof
// marshals to JSON that decodes back into the exact type the validator
// aggregate_and_proofs endpoint expects.
diff --git a/test-fixtures.json b/test-fixtures.json
index edb665914a1..5f2115fa1a5 100644
--- a/test-fixtures.json
+++ b/test-fixtures.json
@@ -21,9 +21,9 @@
"size": 497764775
},
"cl_mainnet": {
- "url": "https://github.com/ethereum/consensus-specs/releases/download/v1.7.0-alpha.11/mainnet.tar.gz",
- "sha256": "956cc05f9bb2e745ecd04b60fb2bb91679c80ede82e81b489b5d47a9d65eb66b",
- "size": 851476438
+ "url": "https://github.com/ethereum/consensus-specs/releases/download/v1.7.0-alpha.12/mainnet.tar.gz",
+ "sha256": "f0057d2acdea2730ec68cac2c796aad89174e10e9b38ac68ad7d8aa1c0cbd85c",
+ "size": 858531022
},
"legacy_cancun": {
"url": "https://github.com/ethereum/legacytests/archive/1f581b8ccdc4c63acf5f2c5c1b155c690c32a8eb.tar.gz",