From 939ca24e4c5c4b52cf4c1a8d0b9bc8726133c3ef Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 14:24:20 +0700 Subject: [PATCH 01/42] cl/ssz: support progressive Gloas block hashing --- cl/cltypes/beacon_block.go | 53 +++++++++ cl/cltypes/epbs_payload.go | 10 +- cl/cltypes/execution_requests.go | 22 +++- cl/cltypes/gloas_progressive_hash_test.go | 45 ++++++++ cl/cltypes/indexed_attestation.go | 8 ++ cl/cltypes/slashings.go | 12 +++ cl/cltypes/solid/attestation.go | 11 ++ cl/cltypes/solid/bitlist.go | 10 ++ cl/cltypes/solid/list_ssz.go | 16 +++ cl/cltypes/solid/uint64_raw_list.go | 4 + cl/merkle_tree/merkle_root.go | 126 ++++++++++++++++++++++ 11 files changed, 313 insertions(+), 4 deletions(-) create mode 100644 cl/cltypes/gloas_progressive_hash_test.go diff --git a/cl/cltypes/beacon_block.go b/cl/cltypes/beacon_block.go index ee91c1fd3b7..d47f9141728 100644 --- a/cl/cltypes/beacon_block.go +++ b/cl/cltypes/beacon_block.go @@ -509,9 +509,62 @@ 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) { + proposerSlashings, err := b.ProposerSlashings.HashSSZProgressive(nil) + if err != nil { + return [32]byte{}, err + } + attesterSlashings, err := b.AttesterSlashings.HashSSZProgressive(func(slashing *AttesterSlashing) ([32]byte, error) { + return slashing.HashSSZProgressive() + }) + if err != nil { + return [32]byte{}, err + } + attestations, err := b.Attestations.HashSSZProgressive(func(att *solid.Attestation) ([32]byte, error) { + return att.HashSSZProgressive() + }) + if err != nil { + return [32]byte{}, err + } + deposits, err := b.Deposits.HashSSZProgressive(nil) + if err != nil { + return [32]byte{}, err + } + voluntaryExits, err := b.VoluntaryExits.HashSSZProgressive(nil) + if err != nil { + return [32]byte{}, err + } + executionChanges, err := b.ExecutionChanges.HashSSZProgressive(nil) + if err != nil { + return [32]byte{}, err + } + payloadAttestations, err := b.PayloadAttestations.HashSSZProgressive(nil) + if err != nil { + return [32]byte{}, err + } + return merkle_tree.ProgressiveContainerRootAll( + b.RandaoReveal[:], + b.Eth1Data, + b.Graffiti[:], + proposerSlashings[:], + attesterSlashings[:], + attestations[:], + deposits[:], + voluntaryExits[:], + b.SyncAggregate, + executionChanges[:], + b.SignedExecutionPayloadBid, + payloadAttestations[:], + b.ParentExecutionRequests, + ) +} + 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 { diff --git a/cl/cltypes/epbs_payload.go b/cl/cltypes/epbs_payload.go index 789bb2f1964..d0f917f6dde 100644 --- a/cl/cltypes/epbs_payload.go +++ b/cl/cltypes/epbs_payload.go @@ -126,7 +126,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 { @@ -275,7 +275,11 @@ type ExecutionPayloadBid struct { } func (e *ExecutionPayloadBid) HashSSZ() ([32]byte, error) { - return merkle_tree.HashTreeRoot( + blobRoot, err := e.BlobKzgCommitments.HashSSZProgressive(nil) + if err != nil { + return [32]byte{}, err + } + return merkle_tree.ProgressiveContainerRootAll( e.ParentBlockHash[:], e.ParentBlockRoot[:], e.BlockHash[:], @@ -286,7 +290,7 @@ func (e *ExecutionPayloadBid) HashSSZ() ([32]byte, error) { e.Slot, e.Value, e.ExecutionPayment, - &e.BlobKzgCommitments, + blobRoot[:], e.ExecutionRequestsRoot[:], ) } diff --git a/cl/cltypes/execution_requests.go b/cl/cltypes/execution_requests.go index 0b8214e4e06..5536a18f40e 100644 --- a/cl/cltypes/execution_requests.go +++ b/cl/cltypes/execution_requests.go @@ -159,7 +159,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 { 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..b53581ecec0 100644 --- a/cl/cltypes/indexed_attestation.go +++ b/cl/cltypes/indexed_attestation.go @@ -147,6 +147,14 @@ func (i *IndexedAttestation) HashSSZ() ([32]byte, error) { 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/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..74804a18455 100644 --- a/cl/cltypes/solid/attestation.go +++ b/cl/cltypes/solid/attestation.go @@ -155,6 +155,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{} diff --git a/cl/cltypes/solid/bitlist.go b/cl/cltypes/solid/bitlist.go index 88d7f71a87a..b00b474005c 100644 --- a/cl/cltypes/solid/bitlist.go +++ b/cl/cltypes/solid/bitlist.go @@ -188,6 +188,16 @@ 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) { + bitLength := u.Bits() + packed := append([]byte(nil), u.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) { diff --git a/cl/cltypes/solid/list_ssz.go b/cl/cltypes/solid/list_ssz.go index 60b712105de..8b94aebc59a 100644 --- a/cl/cltypes/solid/list_ssz.go +++ b/cl/cltypes/solid/list_ssz.go @@ -133,6 +133,22 @@ func (l *ListSSZ[T]) HashSSZ() ([32]byte, error) { 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.static { return NewStaticListSSZ[T](l.limit, l.bytesPerElement) diff --git a/cl/cltypes/solid/uint64_raw_list.go b/cl/cltypes/solid/uint64_raw_list.go index b051d3842be..924639a95af 100644 --- a/cl/cltypes/solid/uint64_raw_list.go +++ b/cl/cltypes/solid/uint64_raw_list.go @@ -171,6 +171,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/merkle_tree/merkle_root.go b/cl/merkle_tree/merkle_root.go index 5e0c32e84e2..646caf6903e 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,131 @@ 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 ProgressiveListRoot(roots [][32]byte, listLength uint64) ([32]byte, error) { + root, err := MerkleizeProgressive(roots) + if err != nil { + return [32]byte{}, err + } + return hashPair(root, Uint64Root(listLength)), 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 MerkleizeProgressive(chunks [][32]byte) ([32]byte, error) { + return merkleizeProgressive(chunks, 1) +} + +func merkleizeProgressive(chunks [][32]byte, numLeaves uint64) ([32]byte, error) { + if len(chunks) == 0 { + return [32]byte{}, nil + } + count := min(uint64(len(chunks)), numLeaves) + left, err := MerkleizeVector(chunks[:count], numLeaves) + if err != nil { + return [32]byte{}, err + } + if numLeaves > ^uint64(0)/4 { + return [32]byte{}, errors.New("progressive merkle tree is too large") + } + right, err := merkleizeProgressive(chunks[count:], numLeaves*4) + if err != nil { + return [32]byte{}, err + } + return hashPair(left, right), 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: unsported type %T at index %d", i, obj)) + } + } + 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 { From 774faad58cbdda5733b6e2e8bc576e571617c81f Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 14:37:44 +0700 Subject: [PATCH 02/42] cl/state: support progressive Gloas hashing --- cl/cltypes/solid/participation_bitlist.go | 4 + cl/cltypes/solid/validator_set.go | 14 ++++ cl/phase1/core/state/raw/hashing.go | 80 +++++++++++++++---- .../core/state/raw/hashing_gloas_test.go | 57 +++++++++++++ 4 files changed, 138 insertions(+), 17 deletions(-) create mode 100644 cl/phase1/core/state/raw/hashing_gloas_test.go 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/validator_set.go b/cl/cltypes/solid/validator_set.go index 5e27a28cd2d..2ff450142f7 100644 --- a/cl/cltypes/solid/validator_set.go +++ b/cl/cltypes/solid/validator_set.go @@ -237,6 +237,20 @@ func (v *ValidatorSet) HashSSZ() ([32]byte, error) { return crypto.Sha256(coreRoot[:], lengthRoot[:]), nil } +func (v *ValidatorSet) HashSSZProgressive() ([32]byte, error) { + roots := make([][32]byte, v.l) + hashBuffer := make([]byte, 8*32) + for i := range roots { + if err := v.Get(i).CopyHashBufferTo(hashBuffer); err != nil { + return [32]byte{}, err + } + if err := merkle_tree.MerkleRootFromFlatLeaves(hashBuffer, roots[i][:]); err != nil { + return [32]byte{}, err + } + } + return merkle_tree.ProgressiveListRoot(roots, uint64(v.l)) +} + func (v *ValidatorSet) Set(idx int, val Validator) { if idx >= v.l { panic("ValidatorSet -- Set: out of bounds") diff --git a/cl/phase1/core/state/raw/hashing.go b/cl/phase1/core/state/raw/hashing.go index d673118c763..04f146d2d86 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 @@ -144,19 +148,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 +182,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 +200,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 +218,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 +234,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 +253,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 +288,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 +304,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..7e59041ccf6 --- /dev/null +++ b/cl/phase1/core/state/raw/hashing_gloas_test.go @@ -0,0 +1,57 @@ +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) +} From 9b67a4c33b4b6ff75178074fd6ef53a0862a0e38 Mon Sep 17 00:00:00 2001 From: kewei Date: Mon, 27 Jul 2026 15:17:22 +0900 Subject: [PATCH 03/42] cl/merkle_tree: use canonical progressive list helper --- cl/merkle_tree/merkle_root.go | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/cl/merkle_tree/merkle_root.go b/cl/merkle_tree/merkle_root.go index 646caf6903e..d2adf97fd4b 100644 --- a/cl/merkle_tree/merkle_root.go +++ b/cl/merkle_tree/merkle_root.go @@ -142,14 +142,6 @@ func ProgressiveContainerRootAll(schema ...any) ([32]byte, error) { return ProgressiveContainerRoot(activeFields, schema...) } -func ProgressiveListRoot(roots [][32]byte, listLength uint64) ([32]byte, error) { - root, err := MerkleizeProgressive(roots) - if err != nil { - return [32]byte{}, err - } - return hashPair(root, Uint64Root(listLength)), nil -} - func ProgressiveBitlistRoot(packed []byte, bitLength uint64) ([32]byte, error) { chunks := make([][32]byte, (len(packed)+31)/32) for i := range packed { @@ -166,29 +158,6 @@ func ProgressiveBasicListRoot(packed []byte, listLength uint64) ([32]byte, error return ProgressiveListRoot(chunks, listLength) } -func MerkleizeProgressive(chunks [][32]byte) ([32]byte, error) { - return merkleizeProgressive(chunks, 1) -} - -func merkleizeProgressive(chunks [][32]byte, numLeaves uint64) ([32]byte, error) { - if len(chunks) == 0 { - return [32]byte{}, nil - } - count := min(uint64(len(chunks)), numLeaves) - left, err := MerkleizeVector(chunks[:count], numLeaves) - if err != nil { - return [32]byte{}, err - } - if numLeaves > ^uint64(0)/4 { - return [32]byte{}, errors.New("progressive merkle tree is too large") - } - right, err := merkleizeProgressive(chunks[count:], numLeaves*4) - if err != nil { - return [32]byte{}, err - } - return hashPair(left, right), nil -} - func progressiveSchemaRoots(schema []any) ([][32]byte, error) { roots := make([][32]byte, len(schema)) for i, element := range schema { From bb24ad9ab3d5653c0016b9c3923535413e97687e Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 15:05:15 +0700 Subject: [PATCH 04/42] cl/state: invalidate roots at Gloas fork --- .../core/state/raw/hashing_gloas_test.go | 29 +++++++++++++++++++ cl/phase1/core/state/raw/setters.go | 13 +++++++++ 2 files changed, 42 insertions(+) diff --git a/cl/phase1/core/state/raw/hashing_gloas_test.go b/cl/phase1/core/state/raw/hashing_gloas_test.go index 7e59041ccf6..9712c5f05d1 100644 --- a/cl/phase1/core/state/raw/hashing_gloas_test.go +++ b/cl/phase1/core/state/raw/hashing_gloas_test.go @@ -55,3 +55,32 @@ func TestGloasBeaconStateUsesProgressiveHashing(t *testing.T) { 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..5f9033e4bea 100644 --- a/cl/phase1/core/state/raw/setters.go +++ b/cl/phase1/core/state/raw/setters.go @@ -24,6 +24,19 @@ import ( ) func (b *BeaconState) SetVersion(version clparams.StateVersion) { + if (b.version < clparams.GloasVersion) != (version < clparams.GloasVersion) { + b.markLeaf( + ValidatorsLeafIndex, + BalancesLeafIndex, + PreviousEpochParticipationLeafIndex, + CurrentEpochParticipationLeafIndex, + InactivityScoresLeafIndex, + LatestBlockHashLeafIndex, + PendingDepositsLeafIndex, + PendingPartialWithdrawalsLeafIndex, + PendingConsolidationsLeafIndex, + ) + } b.version = version } From a65109e8867e42b1db2d63e683f7b88a479bf641 Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:02:20 +0700 Subject: [PATCH 05/42] cl/ssz: retain attestation fork version for hashing --- cl/cltypes/solid/attestation.go | 10 ++++++++++ cl/spectest/consensus_tests/appendix.go | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cl/cltypes/solid/attestation.go b/cl/cltypes/solid/attestation.go index 74804a18455..4bbc431f336 100644 --- a/cl/cltypes/solid/attestation.go +++ b/cl/cltypes/solid/attestation.go @@ -43,6 +43,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) { @@ -75,6 +80,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 } @@ -104,6 +110,7 @@ func (a *Attestation) DecodeSSZ(buf []byte, version int) error { // If cfg is nil, mainnet defaults are used. 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 @@ -148,6 +155,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) diff --git a/cl/spectest/consensus_tests/appendix.go b/cl/spectest/consensus_tests/appendix.go index b81d880d118..202a0167784 100644 --- a/cl/spectest/consensus_tests/appendix.go +++ b/cl/spectest/consensus_tests/appendix.go @@ -206,7 +206,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())). From 4f536b2006f3902158921cb1af791673985b1b49 Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:03:37 +0700 Subject: [PATCH 06/42] cl/ssz: retain indexed attestation fork version --- cl/cltypes/indexed_attestation.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cl/cltypes/indexed_attestation.go b/cl/cltypes/indexed_attestation.go index b53581ecec0..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,6 +148,9 @@ 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[:]) } From 70f1cb0a83560b924f989194c845e004a6e556ba Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:05:46 +0700 Subject: [PATCH 07/42] cl/ssz: propagate aggregate fork version --- cl/cltypes/aggregate.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/cl/cltypes/aggregate.go b/cl/cltypes/aggregate.go index 12dc04f0ad9..b815d0d9ee7 100644 --- a/cl/cltypes/aggregate.go +++ b/cl/cltypes/aggregate.go @@ -20,6 +20,7 @@ import ( "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 +36,14 @@ 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) EncodeSSZ(dst []byte) ([]byte, error) { @@ -46,6 +55,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[:]) } @@ -55,12 +65,23 @@ func (a *AggregateAndProof) EncodingSizeSSZ() int { } func (a *AggregateAndProof) HashSSZ() ([32]byte, error) { + if a.Aggregate != nil { + a.Aggregate.SetVersion(a.version) + } return merkle_tree.HashTreeRoot(a.AggregatorIndex, a.Aggregate, a.SelectionProof[:]) } 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) EncodeSSZ(dst []byte) ([]byte, error) { @@ -68,6 +89,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[:]) } @@ -77,6 +99,9 @@ func (a *SignedAggregateAndProof) EncodingSizeSSZ() int { } func (a *SignedAggregateAndProof) HashSSZ() ([32]byte, error) { + if a.Message != nil { + a.Message.SetVersion(a.version) + } return merkle_tree.HashTreeRoot(a.Message, a.Signature[:]) } From 2a251c1254c0298ce481018cccbaa1fc541b8165 Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:07:57 +0700 Subject: [PATCH 08/42] cl/ssz: hash Gloas execution payload progressively --- cl/cltypes/eth1_block.go | 39 ++++++++++++++++++++++++++++++++ cl/cltypes/solid/byte_list.go | 4 ++++ cl/cltypes/solid/transactions.go | 12 ++++++++++ 3 files changed, 55 insertions(+) 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/solid/byte_list.go b/cl/cltypes/solid/byte_list.go index 329cb42e3a6..1e9356471d1 100644 --- a/cl/cltypes/solid/byte_list.go +++ b/cl/cltypes/solid/byte_list.go @@ -126,6 +126,10 @@ func (b *ByteListSSZ) HashSSZ() ([32]byte, error) { return result, nil } +func (b *ByteListSSZ) HashSSZProgressive() ([32]byte, error) { + return merkle_tree.ProgressiveBasicListRoot(b.data, uint64(len(b.data))) +} + // Bytes returns a copy of the underlying data. func (b *ByteListSSZ) Bytes() []byte { return bytes.Clone(b.data) 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 From 6d94508df3290ac3aaf519c33cc56a4e258d97c2 Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:09:54 +0700 Subject: [PATCH 09/42] cl/ssz: hash Gloas payload containers progressively --- cl/cltypes/epbs_payload.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cl/cltypes/epbs_payload.go b/cl/cltypes/epbs_payload.go index d0f917f6dde..113bbfb4233 100644 --- a/cl/cltypes/epbs_payload.go +++ b/cl/cltypes/epbs_payload.go @@ -242,7 +242,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 { @@ -427,7 +427,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, From 6872ca4723fcecabe375b558c9dbee4a2fc3a229 Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:13:19 +0700 Subject: [PATCH 10/42] cl/ssz: decode Gloas execution request progressive lists --- cl/cltypes/execution_requests.go | 53 +++++++++++++++++++++++++------- cl/cltypes/solid/list_ssz.go | 34 ++++++++++++++++++-- 2 files changed, 73 insertions(+), 14 deletions(-) diff --git a/cl/cltypes/execution_requests.go b/cl/cltypes/execution_requests.go index 5536a18f40e..fef4d98cf8e 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](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](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](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](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](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) @@ -188,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](solid.SizeDepositRequest) + newWithdrawals = solid.NewStaticProgressiveListSSZ[*solid.WithdrawalRequest](solid.SizeWithdrawalRequest) + newConsolidations = solid.NewStaticProgressiveListSSZ[*solid.ConsolidationRequest](solid.SizeConsolidationRequest) + newBuilderDeposits = solid.NewStaticProgressiveListSSZ[*solid.BuilderDepositRequest](solid.SizeBuilderDepositRequest) + newBuilderExits = solid.NewStaticProgressiveListSSZ[*solid.BuilderExitRequest](solid.SizeBuilderExitRequest) + } c := struct { Deposits *solid.ListSSZ[*solid.DepositRequest] `json:"deposits"` Withdrawals *solid.ListSSZ[*solid.WithdrawalRequest] `json:"withdrawals"` @@ -195,11 +226,11 @@ 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 diff --git a/cl/cltypes/solid/list_ssz.go b/cl/cltypes/solid/list_ssz.go index 8b94aebc59a..62ef1c0832f 100644 --- a/cl/cltypes/solid/list_ssz.go +++ b/cl/cltypes/solid/list_ssz.go @@ -42,6 +42,7 @@ type ListSSZ[T EncodableHashableSSZ] struct { // then we can cache its size instead of calling EncodeSizeSSZ on // an always newly created object bytesPerElement int + progressive bool // We can keep hash_tree_root result cached root common.Hash } @@ -62,6 +63,14 @@ func NewStaticListSSZ[T EncodableHashableSSZ](limit int, bytesPerElement int) *L } } +func NewDynamicProgressiveListSSZ[T EncodableHashableSSZ]() *ListSSZ[T] { + return &ListSSZ[T]{list: make([]T, 0), progressive: true} +} + +func NewStaticProgressiveListSSZ[T EncodableHashableSSZ](bytesPerElement int) *ListSSZ[T] { + return &ListSSZ[T]{list: make([]T, 0), static: true, bytesPerElement: bytesPerElement, progressive: true} +} + func (l ListSSZ[T]) MarshalJSON() ([]byte, error) { return json.Marshal(l.list) } @@ -104,10 +113,18 @@ 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.progressive { + if l.static && l.bytesPerElement > 0 { + limit = uint64(len(buf) / l.bytesPerElement) + } else { + limit = uint64(len(buf) / 4) + } + } 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,7 +146,11 @@ 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 } @@ -150,6 +171,12 @@ func (l *ListSSZ[T]) HashSSZProgressive(hashElement func(T) ([32]byte, error)) ( } func (l *ListSSZ[T]) Clone() clonable.Clonable { + if l.progressive { + if l.static { + return NewStaticProgressiveListSSZ[T](l.bytesPerElement) + } + return NewDynamicProgressiveListSSZ[T]() + } if l.static { return NewStaticListSSZ[T](l.limit, l.bytesPerElement) } @@ -228,6 +255,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) From b04b997610999f9e17a1784334dd1bb12e577500 Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:16:52 +0700 Subject: [PATCH 11/42] cl/ssz: decode Gloas beacon body progressive lists --- cl/cltypes/beacon_block.go | 98 ++++++++++++++++++++++++++------------ 1 file changed, 68 insertions(+), 30 deletions(-) diff --git a/cl/cltypes/beacon_block.go b/cl/cltypes/beacon_block.go index d47f9141728..f26682b474b 100644 --- a/cl/cltypes/beacon_block.go +++ b/cl/cltypes/beacon_block.go @@ -296,23 +296,33 @@ func NewBeaconBody(beaconCfg *clparams.BeaconChainConfig, version clparams.State 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](48), }, } - body.PayloadAttestations = solid.NewStaticListSSZ[*PayloadAttestation](int(beaconCfg.MaxPayloadAttestations), PayloadAttestationSSZSizeWithPtcSize(beaconCfg.PtcSize)) body.ParentExecutionRequests = NewExecutionRequestsWithVersion(beaconCfg, version) } return body } +func (b *BeaconBody) resetGloasProgressiveLists() { + b.ProposerSlashings = solid.NewStaticProgressiveListSSZ[*ProposerSlashing](416) + b.AttesterSlashings = solid.NewDynamicProgressiveListSSZ[*AttesterSlashing]() + b.Attestations = solid.NewDynamicProgressiveListSSZ[*solid.Attestation]() + b.Deposits = solid.NewStaticProgressiveListSSZ[*Deposit](1240) + b.VoluntaryExits = solid.NewStaticProgressiveListSSZ[*SignedVoluntaryExit](112) + b.ExecutionChanges = solid.NewStaticProgressiveListSSZ[*SignedBLSToExecutionChange](172) + ptcSize := clparams.MaxPtcSize + if b.beaconCfg != nil && b.beaconCfg.PtcSize > 0 { + ptcSize = b.beaconCfg.PtcSize + } + b.PayloadAttestations = solid.NewStaticProgressiveListSSZ[*PayloadAttestation](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() { @@ -335,26 +345,50 @@ 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](416) + } else { + b.ProposerSlashings = solid.NewStaticListSSZ[*ProposerSlashing](MaxProposerSlashings, 416) + } } if b.AttesterSlashings == nil { - b.AttesterSlashings = solid.NewDynamicListSSZ[*AttesterSlashing](maxAttSlashing) + if b.Version >= clparams.GloasVersion { + b.AttesterSlashings = solid.NewDynamicProgressiveListSSZ[*AttesterSlashing]() + } else { + b.AttesterSlashings = solid.NewDynamicListSSZ[*AttesterSlashing](maxAttSlashing) + } } if b.Attestations == nil { - b.Attestations = solid.NewDynamicListSSZ[*solid.Attestation](maxAttestation) + if b.Version >= clparams.GloasVersion { + b.Attestations = solid.NewDynamicProgressiveListSSZ[*solid.Attestation]() + } else { + b.Attestations = solid.NewDynamicListSSZ[*solid.Attestation](maxAttestation) + } } if b.Deposits == nil { - b.Deposits = solid.NewStaticListSSZ[*Deposit](MaxDeposits, 1240) + if b.Version >= clparams.GloasVersion { + b.Deposits = solid.NewStaticProgressiveListSSZ[*Deposit](1240) + } else { + b.Deposits = solid.NewStaticListSSZ[*Deposit](MaxDeposits, 1240) + } } if b.VoluntaryExits == nil { - b.VoluntaryExits = solid.NewStaticListSSZ[*SignedVoluntaryExit](MaxVoluntaryExits, 112) + if b.Version >= clparams.GloasVersion { + b.VoluntaryExits = solid.NewStaticProgressiveListSSZ[*SignedVoluntaryExit](112) + } else { + b.VoluntaryExits = solid.NewStaticListSSZ[*SignedVoluntaryExit](MaxVoluntaryExits, 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](172) + } else { + b.ExecutionChanges = solid.NewStaticListSSZ[*SignedBLSToExecutionChange](MaxExecutionChanges, 172) + } } // [Modified in Gloas:EIP7732] BlobKzgCommitments removed in GLOAS if b.BlobKzgCommitments == nil && b.Version < clparams.GloasVersion { @@ -373,18 +407,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](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](PayloadAttestationSSZSizeWithPtcSize(ptcSize)) } if b.ParentExecutionRequests == nil { b.ParentExecutionRequests = NewExecutionRequestsWithVersion(b.beaconCfg, b.Version) @@ -434,6 +468,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 +482,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](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 { @@ -675,16 +707,22 @@ func (b *BeaconBody) UnmarshalJSON(buf []byte) error { } // [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](416) + tmp.AttesterSlashings = solid.NewDynamicProgressiveListSSZ[*AttesterSlashing]() + tmp.Attestations = solid.NewDynamicProgressiveListSSZ[*solid.Attestation]() + tmp.Deposits = solid.NewStaticProgressiveListSSZ[*Deposit](1240) + tmp.VoluntaryExits = solid.NewStaticProgressiveListSSZ[*SignedVoluntaryExit](112) + tmp.ExecutionChanges = solid.NewStaticProgressiveListSSZ[*SignedBLSToExecutionChange](172) tmp.SignedExecutionPayloadBid = &SignedExecutionPayloadBid{ Message: &ExecutionPayloadBid{ - BlobKzgCommitments: *solid.NewStaticListSSZ[*KZGCommitment](maxBlobCommitmentsGloas, 48), + BlobKzgCommitments: *solid.NewStaticProgressiveListSSZ[*KZGCommitment](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](PayloadAttestationSSZSizeWithPtcSize(ptcSize)) tmp.ParentExecutionRequests = NewExecutionRequestsWithVersion(b.beaconCfg, b.Version) } From f26c262b9dedf338a26b8631959487af0372dc5a Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:20:14 +0700 Subject: [PATCH 12/42] cl/ssz: hash Gloas data columns progressively --- cl/cltypes/column_sidecar.go | 14 ++++++++++++-- cl/cltypes/partial_data_column.go | 31 +++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/cl/cltypes/column_sidecar.go b/cl/cltypes/column_sidecar.go index 4020c09ab6c..e0e0269b475 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](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](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/partial_data_column.go b/cl/cltypes/partial_data_column.go index 58f01fa1da6..8f5ba4aceec 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](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](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()...) } From 60e1a2aadc55b1f626ce3b687d723e1d6f72f138 Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:21:11 +0700 Subject: [PATCH 13/42] cl/ssz: merkleize partial column roots --- cl/cltypes/partial_data_column.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cl/cltypes/partial_data_column.go b/cl/cltypes/partial_data_column.go index 8f5ba4aceec..9be11441bae 100644 --- a/cl/cltypes/partial_data_column.go +++ b/cl/cltypes/partial_data_column.go @@ -203,7 +203,7 @@ func (s *PartialDataColumnSidecar) HashSSZ() ([32]byte, error) { if err != nil { return [32]byte{}, err } - return merkle_tree.HashTreeRoot(bitmapRoot, partialColumnRoot, proofsRoot) + return merkle_tree.HashTreeRoot(bitmapRoot[:], partialColumnRoot[:], proofsRoot[:]) } return merkle_tree.HashTreeRoot(s.getSchema()...) } From 1cd69b1918e596e220ac06a39551804e8a31e520 Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:23:00 +0700 Subject: [PATCH 14/42] cl/lightclient: update Gloas proof depths --- cl/cltypes/light_client.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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 } From 532df0229d8ac8e4190bf4356f49f43a9bfd363c Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:27:39 +0700 Subject: [PATCH 15/42] cl/merkle: build progressive container proofs --- cl/merkle_tree/merkle_root.go | 69 +++++++++++++++++++++++++++++ cl/phase1/core/state/raw/hashing.go | 22 +++++++-- 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/cl/merkle_tree/merkle_root.go b/cl/merkle_tree/merkle_root.go index d2adf97fd4b..6b0e60919a0 100644 --- a/cl/merkle_tree/merkle_root.go +++ b/cl/merkle_tree/merkle_root.go @@ -142,6 +142,25 @@ func ProgressiveContainerRootAll(schema ...any) ([32]byte, error) { return ProgressiveContainerRoot(activeFields, schema...) } +func ProgressiveContainerProofAll(fieldIndex int, schema ...any) ([][32]byte, error) { + 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 { @@ -158,6 +177,56 @@ func ProgressiveBasicListRoot(packed []byte, listLength uint64) ([32]byte, error 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 { diff --git a/cl/phase1/core/state/raw/hashing.go b/cl/phase1/core/state/raw/hashing.go index 04f146d2d86..394c991c16f 100644 --- a/cl/phase1/core/state/raw/hashing.go +++ b/cl/phase1/core/state/raw/hashing.go @@ -76,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{} @@ -102,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{} @@ -127,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{} From 6415c9c7d0133b987201678aa323ace02267a4a7 Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:30:03 +0700 Subject: [PATCH 16/42] cl/state: follow alpha.12 builder deposits --- cl/clparams/config.go | 2 +- cl/phase1/core/state/epbs.go | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/cl/clparams/config.go b/cl/clparams/config.go index c426207a4ad..677df0f1cd2 100644 --- a/cl/clparams/config.go +++ b/cl/clparams/config.go @@ -1130,7 +1130,7 @@ 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, 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 { From c9652dd19628d0969106c9fe3c40e4450a34329f Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:32:39 +0700 Subject: [PATCH 17/42] cl/config: update builder withdrawal delay --- cl/clparams/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cl/clparams/config.go b/cl/clparams/config.go index 677df0f1cd2..f849265060f 100644 --- a/cl/clparams/config.go +++ b/cl/clparams/config.go @@ -1137,7 +1137,7 @@ var MainnetBeaconConfig BeaconChainConfig = BeaconChainConfig{ BuilderRegistryLimit: 1 << 40, BuilderPendingWithdrawalsLimit: 1 << 20, MaxBuildersPerWithdrawalsSweep: 1 << 14, - MinBuilderWithdrawabilityDelay: 8192, + MinBuilderWithdrawabilityDelay: 64, } func mainnetConfig() BeaconChainConfig { From c77083c1ec6ab9c629260e43e159ad49ca6a2132 Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:34:01 +0700 Subject: [PATCH 18/42] cl/state: test alpha.12 builder credentials --- cl/phase1/core/state/epbs_test.go | 41 ++++++++----------- .../impl/eth2/operations_gloas_test.go | 10 +++-- 2 files changed, 22 insertions(+), 29 deletions(-) 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/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) From f985e7b80ae7f3c501a6b832b88cc5ddedd6ae4f Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:36:01 +0700 Subject: [PATCH 19/42] cl/transition: bound Gloas execution requests --- cl/transition/impl/eth2/operations.go | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) 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 { From 117c1ec4b96b85fedbba064e6bf03236970430b5 Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:36:58 +0700 Subject: [PATCH 20/42] cl/config: update builder deposit request limit --- cl/clparams/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cl/clparams/config.go b/cl/clparams/config.go index f849265060f..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, From ab6ac3b3cbc11cb7c0fee4999ff912316c4ddb64 Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:40:17 +0700 Subject: [PATCH 21/42] cl/spectest: cover static helper containers --- cl/spectest/consensus_tests/appendix.go | 11 +- .../consensus_tests/ssz_static_helpers.go | 138 ++++++++++++++++++ 2 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 cl/spectest/consensus_tests/ssz_static_helpers.go diff --git a/cl/spectest/consensus_tests/appendix.go b/cl/spectest/consensus_tests/appendix.go index 202a0167784..eb829deb510 100644 --- a/cl/spectest/consensus_tests/appendix.go +++ b/cl/spectest/consensus_tests/appendix.go @@ -280,10 +280,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/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()...) +} From 06df6f38f8962199151ad6f617a3e7ca68547051 Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:41:56 +0700 Subject: [PATCH 22/42] cl/lightclient: build Gloas execution hash proof --- cl/cltypes/beacon_block.go | 49 +++++++++++++++++---- cl/cltypes/epbs_payload.go | 22 +++++++-- cl/spectest/consensus_tests/light_client.go | 5 +++ 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/cl/cltypes/beacon_block.go b/cl/cltypes/beacon_block.go index f26682b474b..ac092193f09 100644 --- a/cl/cltypes/beacon_block.go +++ b/cl/cltypes/beacon_block.go @@ -548,39 +548,47 @@ func (b *BeaconBody) HashSSZ() ([32]byte, error) { } func (b *BeaconBody) hashSSZGloas() ([32]byte, error) { - proposerSlashings, err := b.ProposerSlashings.HashSSZProgressive(nil) + 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 [32]byte{}, err + return nil, err } attestations, err := b.Attestations.HashSSZProgressive(func(att *solid.Attestation) ([32]byte, error) { return att.HashSSZProgressive() }) if err != nil { - return [32]byte{}, err + return nil, err } deposits, err := b.Deposits.HashSSZProgressive(nil) if err != nil { - return [32]byte{}, err + return nil, err } voluntaryExits, err := b.VoluntaryExits.HashSSZProgressive(nil) if err != nil { - return [32]byte{}, err + return nil, err } executionChanges, err := b.ExecutionChanges.HashSSZProgressive(nil) if err != nil { - return [32]byte{}, err + return nil, err } payloadAttestations, err := b.PayloadAttestations.HashSSZProgressive(nil) if err != nil { - return [32]byte{}, err + return nil, err } - return merkle_tree.ProgressiveContainerRootAll( + return []any{ b.RandaoReveal[:], b.Eth1Data, b.Graffiti[:], @@ -594,7 +602,7 @@ func (b *BeaconBody) hashSSZGloas() ([32]byte, error) { b.SignedExecutionPayloadBid, payloadAttestations[:], b.ParentExecutionRequests, - ) + }, nil } func (b *BeaconBody) getSchema(storage bool) []any { @@ -636,6 +644,29 @@ 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") + } + 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 { diff --git a/cl/cltypes/epbs_payload.go b/cl/cltypes/epbs_payload.go index 113bbfb4233..d6e9fb2f310 100644 --- a/cl/cltypes/epbs_payload.go +++ b/cl/cltypes/epbs_payload.go @@ -275,11 +275,27 @@ type ExecutionPayloadBid struct { } func (e *ExecutionPayloadBid) HashSSZ() ([32]byte, error) { - blobRoot, err := e.BlobKzgCommitments.HashSSZProgressive(nil) + schema, err := e.hashSchema() if err != nil { return [32]byte{}, err } - return merkle_tree.ProgressiveContainerRootAll( + 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[:], @@ -292,7 +308,7 @@ func (e *ExecutionPayloadBid) HashSSZ() ([32]byte, error) { e.ExecutionPayment, blobRoot[:], e.ExecutionRequestsRoot[:], - ) + }, nil } func (e *ExecutionPayloadBid) EncodingSizeSSZ() int { 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)) From 6f0f870754ae38e68ddc7f06bb775cfca9607686 Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:46:58 +0700 Subject: [PATCH 23/42] cl/spectest: run Gloas fork choice cases --- cl/spectest/consensus_tests/appendix.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cl/spectest/consensus_tests/appendix.go b/cl/spectest/consensus_tests/appendix.go index eb829deb510..c3eecfc305b 100644 --- a/cl/spectest/consensus_tests/appendix.go +++ b/cl/spectest/consensus_tests/appendix.go @@ -60,6 +60,10 @@ func init() { 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{}). From 9e0650d0378bcd1bf28a8cdfe7b5f46d3962ebf5 Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:48:21 +0700 Subject: [PATCH 24/42] cl/spectest: run Gloas churn cases --- cl/spectest/consensus_tests/appendix.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cl/spectest/consensus_tests/appendix.go b/cl/spectest/consensus_tests/appendix.go index c3eecfc305b..eb459ab25d3 100644 --- a/cl/spectest/consensus_tests/appendix.go +++ b/cl/spectest/consensus_tests/appendix.go @@ -54,6 +54,7 @@ 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) @@ -86,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). From 4407c467a9a5096461aad7a4c236bb5f9d0e7428 Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:50:58 +0700 Subject: [PATCH 25/42] cl/spectest: run attestation reward cases --- cl/spectest/consensus_tests/rewards.go | 159 ++++++++++++++++++++++--- 1 file changed, 144 insertions(+), 15 deletions(-) diff --git a/cl/spectest/consensus_tests/rewards.go b/cl/spectest/consensus_tests/rewards.go index 778525a5341..17bb3b8259d 100644 --- a/cl/spectest/consensus_tests/rewards.go +++ b/cl/spectest/consensus_tests/rewards.go @@ -17,29 +17,158 @@ package consensus_tests import ( + "encoding/binary" + "fmt" "io/fs" + "slices" "testing" + "github.com/erigontech/erigon/cl/phase1/core/state" "github.com/erigontech/erigon/cl/spectest/spectest" + "github.com/erigontech/erigon/cl/transition/impl/eth2/statechange" ) 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 (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 + } + 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 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 } From 0215ee9146e89e0eea2f6e6c4106cf843cbb75d8 Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:52:35 +0700 Subject: [PATCH 26/42] cl/spectest: make reward deltas decodable --- cl/spectest/consensus_tests/rewards.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cl/spectest/consensus_tests/rewards.go b/cl/spectest/consensus_tests/rewards.go index 17bb3b8259d..ce3d8a8edd2 100644 --- a/cl/spectest/consensus_tests/rewards.go +++ b/cl/spectest/consensus_tests/rewards.go @@ -26,6 +26,7 @@ import ( "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{} @@ -35,6 +36,8 @@ type rewardDeltas struct { 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)) From fdb227b189db9a0cfeb707e88a083dbf3355c2f0 Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:56:20 +0700 Subject: [PATCH 27/42] cl/spectest: run sync committee gossip cases --- cl/spectest/consensus_tests/appendix.go | 5 +- cl/spectest/consensus_tests/gossip.go | 326 +++++++++++++++++++++++- 2 files changed, 328 insertions(+), 3 deletions(-) diff --git a/cl/spectest/consensus_tests/appendix.go b/cl/spectest/consensus_tests/appendix.go index eb459ab25d3..50de64e97fe 100644 --- a/cl/spectest/consensus_tests/appendix.go +++ b/cl/spectest/consensus_tests/appendix.go @@ -122,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() } diff --git a/cl/spectest/consensus_tests/gossip.go b/cl/spectest/consensus_tests/gossip.go index 570837b5c33..6cf29cecdb2 100644 --- a/cl/spectest/consensus_tests/gossip.go +++ b/cl/spectest/consensus_tests/gossip.go @@ -17,30 +17,352 @@ 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" ) // 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"` } +func gossipBLSToExecutionChangeHandler(t *testing.T, root fs.FS, c spectest.TestCase) error { + beaconState, meta, err := readGossipStateAndMeta(root, c) + if err != nil { + return err + } + seen := make(map[uint64]struct{}) + for i, message := range meta.Messages { + change := &cltypes.SignedBLSToExecutionChange{} + if err := spectest.ReadSszOld(root, change, c.Version(), message.Message+".ssz_snappy"); err != nil { + return err + } + result := validateGossipBLSToExecutionChange(beaconState, change, 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, seen map[uint64]struct{}) string { + if signedChange == nil || signedChange.Message == nil { + return "reject" + } + 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 := utils.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 { + 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, meta.CurrentTimeMS+message.OffsetMS, 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 := utils.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 { + 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, meta.CurrentTimeMS+message.OffsetMS, 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" + } + modulo := max(uint64(1), beaconState.BeaconConfig().SyncCommitteeSize/beaconState.BeaconConfig().SyncCommitteeSubnetCount/beaconState.BeaconConfig().TargetAggregatorsPerSyncSubcommittee) + selectionProofHash := utils.Sha256(message.SelectionProof[:]) + if binary.LittleEndian.Uint64(selectionProofHash[:8])%modulo != 0 { + return "reject" + } + if message.AggregatorIndex >= uint64(beaconState.ValidatorLength()) { + return "reject" + } + subcommitteePublicKeys := gossipSyncSubcommitteePublicKeys(beaconState, contribution.SubcommitteeIndex) + 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 = utils.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 { + genesisTimeMS := beaconState.GenesisTime() * 1000 + if currentTimeMS < genesisTimeMS { + return false + } + return slot == (currentTimeMS-genesisTimeMS)/(beaconState.BeaconConfig().SecondsPerSlot*1000) +} + +func gossipSyncSubcommitteePublicKeys(beaconState *state.CachingBeaconState, subcommitteeIndex uint64) []common.Bytes48 { + committee := beaconState.CurrentSyncCommittee() + if beaconState.BeaconConfig().SyncCommitteePeriod(beaconState.Slot()) != beaconState.BeaconConfig().SyncCommitteePeriod(beaconState.Slot()+1) { + committee = beaconState.NextSyncCommittee() + } + subcommitteeSize := beaconState.BeaconConfig().SyncCommitteeSize / beaconState.BeaconConfig().SyncCommitteeSubnetCount + start := subcommitteeIndex * subcommitteeSize + return committee.GetCommittee()[start : start+subcommitteeSize] +} + +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 { From 5c46a5b192cf7e412db4d117ac891ff8a9888e4a Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 16:58:40 +0700 Subject: [PATCH 28/42] cl/spectest: cover phase0 reward deltas --- cl/spectest/consensus_tests/rewards.go | 148 +++++++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/cl/spectest/consensus_tests/rewards.go b/cl/spectest/consensus_tests/rewards.go index ce3d8a8edd2..021345fb69b 100644 --- a/cl/spectest/consensus_tests/rewards.go +++ b/cl/spectest/consensus_tests/rewards.go @@ -23,6 +23,7 @@ import ( "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" @@ -69,6 +70,9 @@ func (b *RewardsCore) Run(t *testing.T, root fs.FS, c spectest.TestCase) error { 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( @@ -114,6 +118,150 @@ func (b *RewardsCore) Run(t *testing.T, root fs.FS, c spectest.TestCase) error { 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, From 3a1c19117622971d33ea6fff00318f655a3bd99f Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 17:00:36 +0700 Subject: [PATCH 29/42] cl/spectest: honor gossip fork epochs --- cl/spectest/consensus_tests/gossip.go | 31 +++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/cl/spectest/consensus_tests/gossip.go b/cl/spectest/consensus_tests/gossip.go index 6cf29cecdb2..23b65f2ec0a 100644 --- a/cl/spectest/consensus_tests/gossip.go +++ b/cl/spectest/consensus_tests/gossip.go @@ -50,18 +50,26 @@ type gossipMessage struct { 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 { change := &cltypes.SignedBLSToExecutionChange{} if err := spectest.ReadSszOld(root, change, c.Version(), message.Message+".ssz_snappy"); err != nil { return err } - result := validateGossipBLSToExecutionChange(beaconState, change, seen) + result := validateGossipBLSToExecutionChange(beaconState, change, meta.CurrentTimeMS+message.OffsetMS, config.CapellaForkEpoch, seen) if result != message.Expected { return gossipResultError(i, message, result) } @@ -72,10 +80,20 @@ func gossipBLSToExecutionChangeHandler(t *testing.T, root fs.FS, c spectest.Test return nil } -func validateGossipBLSToExecutionChange(beaconState *state.CachingBeaconState, signedChange *cltypes.SignedBLSToExecutionChange, seen map[uint64]struct{}) string { +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" @@ -305,11 +323,16 @@ func readGossipStateAndMeta(root fs.FS, c spectest.TestCase) (*state.CachingBeac } 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) { genesisTimeMS := beaconState.GenesisTime() * 1000 if currentTimeMS < genesisTimeMS { - return false + return 0, false } - return slot == (currentTimeMS-genesisTimeMS)/(beaconState.BeaconConfig().SecondsPerSlot*1000) + return (currentTimeMS - genesisTimeMS) / (beaconState.BeaconConfig().SecondsPerSlot * 1000), true } func gossipSyncSubcommitteePublicKeys(beaconState *state.CachingBeaconState, subcommitteeIndex uint64) []common.Bytes48 { From c3f473ffee68011cff66ac2d375bffae8a13a036 Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 17:01:21 +0700 Subject: [PATCH 30/42] cl/spectest: update consensus fixtures to alpha.12 --- test-fixtures.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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", From 19584c7170910efaf1172666b9d8838028e419f6 Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 17:04:22 +0700 Subject: [PATCH 31/42] cl/spectest: reject malformed gossip bounds --- cl/spectest/consensus_tests/gossip.go | 66 +++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/cl/spectest/consensus_tests/gossip.go b/cl/spectest/consensus_tests/gossip.go index 23b65f2ec0a..e97ef48efb5 100644 --- a/cl/spectest/consensus_tests/gossip.go +++ b/cl/spectest/consensus_tests/gossip.go @@ -65,11 +65,15 @@ func gossipBLSToExecutionChangeHandler(t *testing.T, root fs.FS, c spectest.Test } 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, meta.CurrentTimeMS+message.OffsetMS, config.CapellaForkEpoch, seen) + result := validateGossipBLSToExecutionChange(beaconState, change, messageTime, config.CapellaForkEpoch, seen) if result != message.Expected { return gossipResultError(i, message, result) } @@ -142,11 +146,15 @@ func gossipSyncCommitteeMessageHandler(t *testing.T, root fs.FS, c spectest.Test } 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, meta.CurrentTimeMS+message.OffsetMS, seen) + result := validateGossipSyncCommitteeMessage(beaconState, syncMessage, message.SubnetID, messageTime, seen) if result != message.Expected { return gossipResultError(i, message, result) } @@ -214,13 +222,17 @@ func gossipSyncContributionHandler(t *testing.T, root fs.FS, c spectest.TestCase 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, meta.CurrentTimeMS+message.OffsetMS, seenContributions, seenAggregators) + result := validateGossipSyncContribution(beaconState, signedContribution, messageTime, seenContributions, seenAggregators) if result != message.Expected { return gossipResultError(i, message, result) } @@ -252,7 +264,11 @@ func validateGossipSyncContribution( if !gossipBitsHaveParticipants(contribution.AggregationBits) { return "reject" } - modulo := max(uint64(1), beaconState.BeaconConfig().SyncCommitteeSize/beaconState.BeaconConfig().SyncCommitteeSubnetCount/beaconState.BeaconConfig().TargetAggregatorsPerSyncSubcommittee) + config := beaconState.BeaconConfig() + if config.SyncCommitteeSubnetCount == 0 || config.TargetAggregatorsPerSyncSubcommittee == 0 { + return "reject" + } + modulo := max(uint64(1), config.SyncCommitteeSize/config.SyncCommitteeSubnetCount/config.TargetAggregatorsPerSyncSubcommittee) selectionProofHash := utils.Sha256(message.SelectionProof[:]) if binary.LittleEndian.Uint64(selectionProofHash[:8])%modulo != 0 { return "reject" @@ -260,7 +276,10 @@ func validateGossipSyncContribution( if message.AggregatorIndex >= uint64(beaconState.ValidatorLength()) { return "reject" } - subcommitteePublicKeys := gossipSyncSubcommitteePublicKeys(beaconState, contribution.SubcommitteeIndex) + 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" @@ -328,21 +347,48 @@ func gossipSlotIsCurrent(beaconState *state.CachingBeaconState, slot uint64, cur } 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 { + 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 { +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.BeaconConfig().SyncCommitteePeriod(beaconState.Slot()) != beaconState.BeaconConfig().SyncCommitteePeriod(beaconState.Slot()+1) { + if beaconState.Slot() == ^uint64(0) { + return nil, false + } + if config.SyncCommitteePeriod(beaconState.Slot()) != config.SyncCommitteePeriod(beaconState.Slot()+1) { committee = beaconState.NextSyncCommittee() } - subcommitteeSize := beaconState.BeaconConfig().SyncCommitteeSize / beaconState.BeaconConfig().SyncCommitteeSubnetCount + if committee == nil { + return nil, false + } + subcommitteeSize := config.SyncCommitteeSize / config.SyncCommitteeSubnetCount + if subcommitteeSize == 0 || subcommitteeIndex > ^uint64(0)/subcommitteeSize { + return nil, false + } start := subcommitteeIndex * subcommitteeSize - return committee.GetCommittee()[start : start+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( From c30d9d3073549688973f83e21f0ee0300b1f7995 Mon Sep 17 00:00:00 2001 From: kewei Date: Wed, 15 Jul 2026 17:05:40 +0700 Subject: [PATCH 32/42] cl/spectest: test malformed fixture bounds --- .../consensus_tests/gossip_bounds_test.go | 66 +++++++++++++++++++ cl/spectest/consensus_tests/rewards_test.go | 62 +++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 cl/spectest/consensus_tests/gossip_bounds_test.go create mode 100644 cl/spectest/consensus_tests/rewards_test.go 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/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 +} From 31bc7f88a3c8906b062d38572d43ad6ce889cac7 Mon Sep 17 00:00:00 2001 From: kewei Date: Mon, 27 Jul 2026 15:22:32 +0900 Subject: [PATCH 33/42] cl/spectest: use common sha256 helper --- cl/spectest/consensus_tests/gossip.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cl/spectest/consensus_tests/gossip.go b/cl/spectest/consensus_tests/gossip.go index e97ef48efb5..f52e0e5775e 100644 --- a/cl/spectest/consensus_tests/gossip.go +++ b/cl/spectest/consensus_tests/gossip.go @@ -33,6 +33,7 @@ import ( "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. @@ -110,7 +111,7 @@ func validateGossipBLSToExecutionChange( if withdrawalCredentials[0] != byte(beaconState.BeaconConfig().BLSWithdrawalPrefixByte) { return "reject" } - hashedFrom := utils.Sha256(change.From[:]) + hashedFrom := crypto.Sha256(change.From[:]) if !bytes.Equal(withdrawalCredentials[1:], hashedFrom[1:]) { return "reject" } @@ -194,7 +195,7 @@ func validateGossipSyncCommitteeMessage( if err != nil { return "reject" } - signingRoot := utils.Sha256(message.BeaconBlockRoot[:], domain) + signingRoot := crypto.Sha256(message.BeaconBlockRoot[:], domain) valid, err := bls.Verify(message.Signature[:], signingRoot[:], publicKey[:]) if err != nil || !valid { return "reject" @@ -269,7 +270,7 @@ func validateGossipSyncContribution( return "reject" } modulo := max(uint64(1), config.SyncCommitteeSize/config.SyncCommitteeSubnetCount/config.TargetAggregatorsPerSyncSubcommittee) - selectionProofHash := utils.Sha256(message.SelectionProof[:]) + selectionProofHash := crypto.Sha256(message.SelectionProof[:]) if binary.LittleEndian.Uint64(selectionProofHash[:8])%modulo != 0 { return "reject" } @@ -321,7 +322,7 @@ func validateGossipSyncContribution( if err != nil { return "reject" } - signingRoot = utils.Sha256(contribution.BeaconBlockRoot[:], domain) + signingRoot = crypto.Sha256(contribution.BeaconBlockRoot[:], domain) valid, err := bls.VerifyAggregate(contribution.Signature[:], signingRoot[:], participantPublicKeys) if err != nil || !valid { return "reject" From 77e0d9f763815b5af8f27764062868fa0624023d Mon Sep 17 00:00:00 2001 From: kewei Date: Fri, 31 Jul 2026 18:32:52 +0700 Subject: [PATCH 34/42] cl: harden Gloas progressive boundaries --- cl/beacon/handler/block_production.go | 5 + cl/beacon/handler/pool.go | 15 +++ cl/clparams/devgenesis/devgenesis.go | 45 ++++++-- cl/clparams/devgenesis/devgenesis_test.go | 38 ++++++ cl/cltypes/aggregate.go | 6 - cl/cltypes/aggregate_gloas_test.go | 26 +++++ cl/cltypes/beacon_block.go | 18 +-- cl/cltypes/beacon_block_blinded.go | 9 +- cl/cltypes/solid/attestation.go | 50 +++++++- cl/cltypes/solid/attestation_config_test.go | 49 ++++++++ cl/cltypes/solid/bitlist.go | 8 ++ cl/cltypes/solid/bitlist_test.go | 24 ++++ cl/cltypes/solid/bitvector.go | 28 +++++ cl/cltypes/solid/bitvector_test.go | 7 ++ cl/cltypes/solid/uint64_raw_list.go | 7 ++ cl/cltypes/solid/uint64_raw_list_test.go | 30 +++++ cl/cltypes/solid/validator_set.go | 109 ++++++++++++++++-- .../solid/validator_set_progressive_test.go | 83 +++++++++++++ cl/cltypes/solid/vector_test.go | 1 + cl/phase1/core/state/accessors.go | 21 +++- cl/phase1/core/state/accessors_gloas_test.go | 25 ++++ cl/phase1/core/state/raw/setters.go | 1 + cl/phase1/forkchoice/checkpoint_state.go | 6 +- cl/phase1/forkchoice/on_attester_slashing.go | 4 +- .../services/aggregate_and_proof_service.go | 14 +++ .../aggregate_and_proof_service_test.go | 33 ++++++ cl/ssz/decode.go | 7 +- cl/transition/machine/block.go | 26 +++++ cl/transition/machine/block_gloas_test.go | 96 +++++++++++++++ cl/validator/devvalidator/aggregate.go | 3 + cl/validator/devvalidator/aggregate_test.go | 22 ++++ 31 files changed, 762 insertions(+), 54 deletions(-) create mode 100644 cl/cltypes/aggregate_gloas_test.go create mode 100644 cl/cltypes/solid/attestation_config_test.go create mode 100644 cl/cltypes/solid/uint64_raw_list_test.go create mode 100644 cl/cltypes/solid/validator_set_progressive_test.go create mode 100644 cl/phase1/core/state/accessors_gloas_test.go create mode 100644 cl/transition/machine/block_gloas_test.go 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..72ecc3470f1 100644 --- a/cl/beacon/handler/pool.go +++ b/cl/beacon/handler/pool.go @@ -384,6 +384,21 @@ func (a *ApiHandler) PostEthV1ValidatorAggregatesAndProof(w http.ResponseWriter, failures := []poolingFailure{} for _, 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: len(failures), 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: len(failures), 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: len(failures), Message: err.Error()}) + continue + } encodedSSZ, err := v.EncodeSSZ(nil) if err != nil { beaconhttp.NewEndpointError(http.StatusInternalServerError, err).WriteTo(w) diff --git a/cl/clparams/devgenesis/devgenesis.go b/cl/clparams/devgenesis/devgenesis.go index 34b0be33d8a..f35fb79070a 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](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 b815d0d9ee7..add159db4c3 100644 --- a/cl/cltypes/aggregate.go +++ b/cl/cltypes/aggregate.go @@ -65,9 +65,6 @@ func (a *AggregateAndProof) EncodingSizeSSZ() int { } func (a *AggregateAndProof) HashSSZ() ([32]byte, error) { - if a.Aggregate != nil { - a.Aggregate.SetVersion(a.version) - } return merkle_tree.HashTreeRoot(a.AggregatorIndex, a.Aggregate, a.SelectionProof[:]) } @@ -99,9 +96,6 @@ func (a *SignedAggregateAndProof) EncodingSizeSSZ() int { } func (a *SignedAggregateAndProof) HashSSZ() ([32]byte, error) { - if a.Message != nil { - a.Message.SetVersion(a.version) - } return merkle_tree.HashTreeRoot(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..3cde3971f0b --- /dev/null +++ b/cl/cltypes/aggregate_gloas_test.go @@ -0,0 +1,26 @@ +package cltypes + +import ( + "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) +} diff --git a/cl/cltypes/beacon_block.go b/cl/cltypes/beacon_block.go index ac092193f09..40054c692ff 100644 --- a/cl/cltypes/beacon_block.go +++ b/cl/cltypes/beacon_block.go @@ -497,10 +497,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 @@ -766,10 +767,11 @@ func (b *BeaconBody) UnmarshalJSON(buf []byte) error { 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/solid/attestation.go b/cl/cltypes/solid/attestation.go index 4bbc431f336..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" @@ -68,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 @@ -100,14 +128,12 @@ 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 @@ -125,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) } @@ -276,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 b00b474005c..ae3601482fd 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" @@ -207,6 +208,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) diff --git a/cl/cltypes/solid/bitlist_test.go b/cl/cltypes/solid/bitlist_test.go index 8b4d1b893ec..0eda57a1e49 100644 --- a/cl/cltypes/solid/bitlist_test.go +++ b/cl/cltypes/solid/bitlist_test.go @@ -123,6 +123,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< 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 { 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 2ff450142f7..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) @@ -238,17 +258,27 @@ func (v *ValidatorSet) HashSSZ() ([32]byte, error) { } func (v *ValidatorSet) HashSSZProgressive() ([32]byte, error) { - roots := make([][32]byte, v.l) - hashBuffer := make([]byte, 8*32) - for i := range roots { - if err := v.Get(i).CopyHashBufferTo(hashBuffer); err != nil { - return [32]byte{}, err - } - if err := merkle_tree.MerkleRootFromFlatLeaves(hashBuffer, roots[i][:]); err != nil { - return [32]byte{}, err - } + 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 } - return merkle_tree.ProgressiveListRoot(roots, uint64(v.l)) + if enabled { + v.MerkleTree = nil + return + } + v.progressiveTrees = nil } func (v *ValidatorSet) Set(idx int, val Validator) { @@ -296,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/phase1/core/state/accessors.go b/cl/phase1/core/state/accessors.go index 2632399cb1a..9b8f7e22f6a 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,23 @@ 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") + } + limit := cfg.MaxValidatorsPerCommittee + if version >= clparams.ElectraVersion { + 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..65fc4b7eef5 --- /dev/null +++ b/cl/phase1/core/state/accessors_gloas_test.go @@ -0,0 +1,25 @@ +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") +} diff --git a/cl/phase1/core/state/raw/setters.go b/cl/phase1/core/state/raw/setters.go index 5f9033e4bea..3579b4ede3c 100644 --- a/cl/phase1/core/state/raw/setters.go +++ b/cl/phase1/core/state/raw/setters.go @@ -25,6 +25,7 @@ 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, 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/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/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. From 893a3d8af37490dd2c4c2cfa40a081d3fce9d0d0 Mon Sep 17 00:00:00 2001 From: kewei Date: Sat, 1 Aug 2026 05:21:46 +0700 Subject: [PATCH 35/42] cl: preserve aggregate fork version across JSON --- cl/beacon/handler/pool_test.go | 13 ++---- cl/cltypes/aggregate.go | 65 ++++++++++++++++++++++++++++++ cl/cltypes/aggregate_gloas_test.go | 57 ++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 10 deletions(-) diff --git a/cl/beacon/handler/pool_test.go b/cl/beacon/handler/pool_test.go index 4024808132e..b332acf3d92 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{})} diff --git a/cl/cltypes/aggregate.go b/cl/cltypes/aggregate.go index add159db4c3..e938d0f5192 100644 --- a/cl/cltypes/aggregate.go +++ b/cl/cltypes/aggregate.go @@ -17,6 +17,7 @@ package cltypes import ( + "bytes" "encoding/hex" "encoding/json" @@ -46,6 +47,39 @@ func (a *AggregateAndProof) SetVersion(version clparams.StateVersion) { } } +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) { return ssz2.MarshalSSZ(dst, a.AggregatorIndex, a.Aggregate, a.SelectionProof[:]) } @@ -81,6 +115,37 @@ func (a *SignedAggregateAndProof) SetVersion(version clparams.StateVersion) { } } +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) { return ssz2.MarshalSSZ(dst, a.Message, a.Signature[:]) } diff --git a/cl/cltypes/aggregate_gloas_test.go b/cl/cltypes/aggregate_gloas_test.go index 3cde3971f0b..7b7acfd087d 100644 --- a/cl/cltypes/aggregate_gloas_test.go +++ b/cl/cltypes/aggregate_gloas_test.go @@ -1,6 +1,7 @@ package cltypes import ( + "encoding/json" "testing" "github.com/erigontech/erigon/cl/clparams" @@ -24,3 +25,59 @@ func TestAggregateAndProofHashDoesNotChangeAggregateVersion(t *testing.T) { 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) + } +} From 77d6098f4710bea67c19f7918a80795d02327b3e Mon Sep 17 00:00:00 2001 From: kewei Date: Mon, 3 Aug 2026 16:36:33 +0700 Subject: [PATCH 36/42] cl: harden progressive decoding boundaries --- cl/clparams/devgenesis/devgenesis.go | 2 +- cl/cltypes/beacon_block.go | 97 +++++++++++++++++++++------- cl/cltypes/beacon_block_test.go | 18 ++++++ cl/cltypes/column_sidecar.go | 4 +- cl/cltypes/execution_requests.go | 72 ++++++++++++++++++--- cl/cltypes/partial_data_column.go | 4 +- cl/cltypes/solid/list_ssz.go | 37 +++++++---- cl/cltypes/solid/list_ssz_test.go | 40 ++++++++++++ cl/merkle_tree/merkle_root.go | 3 + cl/merkle_tree/merkle_root_test.go | 10 +++ 10 files changed, 233 insertions(+), 54 deletions(-) diff --git a/cl/clparams/devgenesis/devgenesis.go b/cl/clparams/devgenesis/devgenesis.go index f35fb79070a..7cb06070fe6 100644 --- a/cl/clparams/devgenesis/devgenesis.go +++ b/cl/clparams/devgenesis/devgenesis.go @@ -217,7 +217,7 @@ func BuildGenesisState( } genesisBid = &cltypes.ExecutionPayloadBid{ ParentBlockHash: elGenesisHash, - BlobKzgCommitments: *solid.NewStaticProgressiveListSSZ[*cltypes.KZGCommitment](48), + BlobKzgCommitments: *solid.NewStaticProgressiveListSSZ[*cltypes.KZGCommitment](int(cfg.MaxBlobCommittmentsPerBlock), 48), ExecutionRequestsRoot: emptyRequestsRoot, } beaconState.SetLatestExecutionPayloadBid(genesisBid) diff --git a/cl/cltypes/beacon_block.go b/cl/cltypes/beacon_block.go index 40054c692ff..1f3e39aa716 100644 --- a/cl/cltypes/beacon_block.go +++ b/cl/cltypes/beacon_block.go @@ -300,7 +300,7 @@ func NewBeaconBody(beaconCfg *clparams.BeaconChainConfig, version clparams.State // GLOAS: SignedExecutionPayloadBid and PayloadAttestations replace above body.SignedExecutionPayloadBid = &SignedExecutionPayloadBid{ Message: &ExecutionPayloadBid{ - BlobKzgCommitments: *solid.NewStaticProgressiveListSSZ[*KZGCommitment](48), + BlobKzgCommitments: *solid.NewStaticProgressiveListSSZ[*KZGCommitment](maxBlobCommitmentsForConfig(beaconCfg), 48), }, } body.ParentExecutionRequests = NewExecutionRequestsWithVersion(beaconCfg, version) @@ -309,18 +309,38 @@ func NewBeaconBody(beaconCfg *clparams.BeaconChainConfig, version clparams.State return body } +func maxBlobCommitmentsForConfig(beaconCfg *clparams.BeaconChainConfig) int { + if beaconCfg != nil && beaconCfg.MaxBlobCommittmentsPerBlock > 0 { + return int(beaconCfg.MaxBlobCommittmentsPerBlock) + } + return MaxBlobsCommittmentsPerBlock +} + +func maxPayloadAttestationsForConfig(beaconCfg *clparams.BeaconChainConfig) int { + if beaconCfg != nil && beaconCfg.MaxPayloadAttestations > 0 { + return int(beaconCfg.MaxPayloadAttestations) + } + return int(clparams.GetBeaconConfig().MaxPayloadAttestations) +} + func (b *BeaconBody) resetGloasProgressiveLists() { - b.ProposerSlashings = solid.NewStaticProgressiveListSSZ[*ProposerSlashing](416) - b.AttesterSlashings = solid.NewDynamicProgressiveListSSZ[*AttesterSlashing]() - b.Attestations = solid.NewDynamicProgressiveListSSZ[*solid.Attestation]() - b.Deposits = solid.NewStaticProgressiveListSSZ[*Deposit](1240) - b.VoluntaryExits = solid.NewStaticProgressiveListSSZ[*SignedVoluntaryExit](112) - b.ExecutionChanges = solid.NewStaticProgressiveListSSZ[*SignedBLSToExecutionChange](172) + maxAttesterSlashings := MaxAttesterSlashings + maxAttestations := MaxAttestations + if b.Version.AfterOrEqual(clparams.ElectraVersion) { + maxAttesterSlashings = MaxAttesterSlashingsElectra + maxAttestations = MaxAttestationsElectra + } + b.ProposerSlashings = solid.NewStaticProgressiveListSSZ[*ProposerSlashing](MaxProposerSlashings, 416) + b.AttesterSlashings = solid.NewDynamicProgressiveListSSZ[*AttesterSlashing](maxAttesterSlashings) + b.Attestations = solid.NewDynamicProgressiveListSSZ[*solid.Attestation](maxAttestations) + b.Deposits = solid.NewStaticProgressiveListSSZ[*Deposit](MaxDeposits, 1240) + b.VoluntaryExits = solid.NewStaticProgressiveListSSZ[*SignedVoluntaryExit](MaxVoluntaryExits, 112) + b.ExecutionChanges = solid.NewStaticProgressiveListSSZ[*SignedBLSToExecutionChange](MaxExecutionChanges, 172) ptcSize := clparams.MaxPtcSize if b.beaconCfg != nil && b.beaconCfg.PtcSize > 0 { ptcSize = b.beaconCfg.PtcSize } - b.PayloadAttestations = solid.NewStaticProgressiveListSSZ[*PayloadAttestation](PayloadAttestationSSZSizeWithPtcSize(ptcSize)) + b.PayloadAttestations = solid.NewStaticProgressiveListSSZ[*PayloadAttestation](maxPayloadAttestationsForConfig(b.beaconCfg), PayloadAttestationSSZSizeWithPtcSize(ptcSize)) } // ensureNilFields initializes any nil fields that must be present for SSZ encoding, @@ -346,35 +366,35 @@ func (b *BeaconBody) ensureNilFields() { } if b.ProposerSlashings == nil { if b.Version >= clparams.GloasVersion { - b.ProposerSlashings = solid.NewStaticProgressiveListSSZ[*ProposerSlashing](416) + b.ProposerSlashings = solid.NewStaticProgressiveListSSZ[*ProposerSlashing](MaxProposerSlashings, 416) } else { b.ProposerSlashings = solid.NewStaticListSSZ[*ProposerSlashing](MaxProposerSlashings, 416) } } if b.AttesterSlashings == nil { if b.Version >= clparams.GloasVersion { - b.AttesterSlashings = solid.NewDynamicProgressiveListSSZ[*AttesterSlashing]() + b.AttesterSlashings = solid.NewDynamicProgressiveListSSZ[*AttesterSlashing](maxAttSlashing) } else { b.AttesterSlashings = solid.NewDynamicListSSZ[*AttesterSlashing](maxAttSlashing) } } if b.Attestations == nil { if b.Version >= clparams.GloasVersion { - b.Attestations = solid.NewDynamicProgressiveListSSZ[*solid.Attestation]() + b.Attestations = solid.NewDynamicProgressiveListSSZ[*solid.Attestation](maxAttestation) } else { b.Attestations = solid.NewDynamicListSSZ[*solid.Attestation](maxAttestation) } } if b.Deposits == nil { if b.Version >= clparams.GloasVersion { - b.Deposits = solid.NewStaticProgressiveListSSZ[*Deposit](1240) + b.Deposits = solid.NewStaticProgressiveListSSZ[*Deposit](MaxDeposits, 1240) } else { b.Deposits = solid.NewStaticListSSZ[*Deposit](MaxDeposits, 1240) } } if b.VoluntaryExits == nil { if b.Version >= clparams.GloasVersion { - b.VoluntaryExits = solid.NewStaticProgressiveListSSZ[*SignedVoluntaryExit](112) + b.VoluntaryExits = solid.NewStaticProgressiveListSSZ[*SignedVoluntaryExit](MaxVoluntaryExits, 112) } else { b.VoluntaryExits = solid.NewStaticListSSZ[*SignedVoluntaryExit](MaxVoluntaryExits, 112) } @@ -385,7 +405,7 @@ func (b *BeaconBody) ensureNilFields() { } if b.ExecutionChanges == nil { if b.Version >= clparams.GloasVersion { - b.ExecutionChanges = solid.NewStaticProgressiveListSSZ[*SignedBLSToExecutionChange](172) + b.ExecutionChanges = solid.NewStaticProgressiveListSSZ[*SignedBLSToExecutionChange](MaxExecutionChanges, 172) } else { b.ExecutionChanges = solid.NewStaticListSSZ[*SignedBLSToExecutionChange](MaxExecutionChanges, 172) } @@ -409,7 +429,7 @@ func (b *BeaconBody) ensureNilFields() { if b.SignedExecutionPayloadBid == nil { b.SignedExecutionPayloadBid = &SignedExecutionPayloadBid{ Message: &ExecutionPayloadBid{ - BlobKzgCommitments: *solid.NewStaticProgressiveListSSZ[*KZGCommitment](48), + BlobKzgCommitments: *solid.NewStaticProgressiveListSSZ[*KZGCommitment](maxBlobCommitmentsForConfig(b.beaconCfg), 48), }, } } @@ -418,7 +438,7 @@ func (b *BeaconBody) ensureNilFields() { if b.beaconCfg != nil && b.beaconCfg.PtcSize > 0 { ptcSize = b.beaconCfg.PtcSize } - b.PayloadAttestations = solid.NewStaticProgressiveListSSZ[*PayloadAttestation](PayloadAttestationSSZSizeWithPtcSize(ptcSize)) + b.PayloadAttestations = solid.NewStaticProgressiveListSSZ[*PayloadAttestation](maxPayloadAttestationsForConfig(b.beaconCfg), PayloadAttestationSSZSizeWithPtcSize(ptcSize)) } if b.ParentExecutionRequests == nil { b.ParentExecutionRequests = NewExecutionRequestsWithVersion(b.beaconCfg, b.Version) @@ -484,7 +504,7 @@ func (b *BeaconBody) DecodeSSZ(buf []byte, version int) error { if b.Version >= clparams.GloasVersion { b.SignedExecutionPayloadBid = &SignedExecutionPayloadBid{ Message: &ExecutionPayloadBid{ - BlobKzgCommitments: *solid.NewStaticProgressiveListSSZ[*KZGCommitment](48), + BlobKzgCommitments: *solid.NewStaticProgressiveListSSZ[*KZGCommitment](maxBlobCommitmentsForConfig(b.beaconCfg), 48), }, } b.ParentExecutionRequests = NewExecutionRequestsWithVersion(b.beaconCfg, b.Version) @@ -739,28 +759,55 @@ func (b *BeaconBody) UnmarshalJSON(buf []byte) error { } // [New in Gloas:EIP7732] Initialize GLOAS fields if b.Version >= clparams.GloasVersion { - tmp.ProposerSlashings = solid.NewStaticProgressiveListSSZ[*ProposerSlashing](416) - tmp.AttesterSlashings = solid.NewDynamicProgressiveListSSZ[*AttesterSlashing]() - tmp.Attestations = solid.NewDynamicProgressiveListSSZ[*solid.Attestation]() - tmp.Deposits = solid.NewStaticProgressiveListSSZ[*Deposit](1240) - tmp.VoluntaryExits = solid.NewStaticProgressiveListSSZ[*SignedVoluntaryExit](112) - tmp.ExecutionChanges = solid.NewStaticProgressiveListSSZ[*SignedBLSToExecutionChange](172) + tmp.ProposerSlashings = solid.NewStaticProgressiveListSSZ[*ProposerSlashing](MaxProposerSlashings, 416) + tmp.AttesterSlashings = solid.NewDynamicProgressiveListSSZ[*AttesterSlashing](maxAttSlashing) + tmp.Attestations = solid.NewDynamicProgressiveListSSZ[*solid.Attestation](maxAttestation) + tmp.Deposits = solid.NewStaticProgressiveListSSZ[*Deposit](MaxDeposits, 1240) + tmp.VoluntaryExits = solid.NewStaticProgressiveListSSZ[*SignedVoluntaryExit](MaxVoluntaryExits, 112) + tmp.ExecutionChanges = solid.NewStaticProgressiveListSSZ[*SignedBLSToExecutionChange](MaxExecutionChanges, 172) tmp.SignedExecutionPayloadBid = &SignedExecutionPayloadBid{ Message: &ExecutionPayloadBid{ - BlobKzgCommitments: *solid.NewStaticProgressiveListSSZ[*KZGCommitment](48), + BlobKzgCommitments: *solid.NewStaticProgressiveListSSZ[*KZGCommitment](maxBlobCommitmentsForConfig(b.beaconCfg), 48), }, } ptcSize := clparams.MaxPtcSize if b.beaconCfg != nil && b.beaconCfg.PtcSize > 0 { ptcSize = b.beaconCfg.PtcSize } - tmp.PayloadAttestations = solid.NewStaticProgressiveListSSZ[*PayloadAttestation](PayloadAttestationSSZSizeWithPtcSize(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) diff --git a/cl/cltypes/beacon_block_test.go b/cl/cltypes/beacon_block_test.go index 07b6f458c71..cd1b187f149 100644 --- a/cl/cltypes/beacon_block_test.go +++ b/cl/cltypes/beacon_block_test.go @@ -648,3 +648,21 @@ 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]}}}`, + `{"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)) + }) + } +} diff --git a/cl/cltypes/column_sidecar.go b/cl/cltypes/column_sidecar.go index e0e0269b475..b1a38dbf41f 100644 --- a/cl/cltypes/column_sidecar.go +++ b/cl/cltypes/column_sidecar.go @@ -83,14 +83,14 @@ func (d *DataColumnSidecar) tryInitWithVersion(version clparams.StateVersion) { cfg := clparams.GetBeaconConfig() if d.Column == nil { if version >= clparams.GloasVersion { - d.Column = solid.NewStaticProgressiveListSSZ[*Cell](BytesPerCell) + d.Column = solid.NewStaticProgressiveListSSZ[*Cell](int(cfg.MaxBlobCommittmentsPerBlock), BytesPerCell) } else { d.Column = solid.NewStaticListSSZ[*Cell](int(cfg.MaxBlobCommittmentsPerBlock), BytesPerCell) } } if d.KzgProofs == nil { if version >= clparams.GloasVersion { - d.KzgProofs = solid.NewStaticProgressiveListSSZ[*KZGProof](48) + d.KzgProofs = solid.NewStaticProgressiveListSSZ[*KZGProof](int(cfg.MaxBlobCommittmentsPerBlock), 48) } else { d.KzgProofs = solid.NewStaticListSSZ[*KZGProof](int(cfg.MaxBlobCommittmentsPerBlock), 48) } diff --git a/cl/cltypes/execution_requests.go b/cl/cltypes/execution_requests.go index fef4d98cf8e..5ddfb690aad 100644 --- a/cl/cltypes/execution_requests.go +++ b/cl/cltypes/execution_requests.go @@ -59,27 +59,27 @@ func (e *ExecutionRequests) ensureLists() { } progressive := e.effectiveVersion() >= clparams.GloasVersion if e.Deposits == nil && progressive { - e.Deposits = solid.NewStaticProgressiveListSSZ[*solid.DepositRequest](solid.SizeDepositRequest) + 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 && progressive { - e.Withdrawals = solid.NewStaticProgressiveListSSZ[*solid.WithdrawalRequest](solid.SizeWithdrawalRequest) + 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 && progressive { - e.Consolidations = solid.NewStaticProgressiveListSSZ[*solid.ConsolidationRequest](solid.SizeConsolidationRequest) + 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 && progressive { - e.BuilderDeposits = solid.NewStaticProgressiveListSSZ[*solid.BuilderDepositRequest](solid.SizeBuilderDepositRequest) + 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 && progressive { - e.BuilderExits = solid.NewStaticProgressiveListSSZ[*solid.BuilderExitRequest](solid.SizeBuilderExitRequest) + 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) } @@ -213,11 +213,11 @@ func (e *ExecutionRequests) UnmarshalJSON(b []byte) error { 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](solid.SizeDepositRequest) - newWithdrawals = solid.NewStaticProgressiveListSSZ[*solid.WithdrawalRequest](solid.SizeWithdrawalRequest) - newConsolidations = solid.NewStaticProgressiveListSSZ[*solid.ConsolidationRequest](solid.SizeConsolidationRequest) - newBuilderDeposits = solid.NewStaticProgressiveListSSZ[*solid.BuilderDepositRequest](solid.SizeBuilderDepositRequest) - newBuilderExits = solid.NewStaticProgressiveListSSZ[*solid.BuilderExitRequest](solid.SizeBuilderExitRequest) + 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"` @@ -235,6 +235,51 @@ func (e *ExecutionRequests) UnmarshalJSON(b []byte) error { 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 @@ -248,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/partial_data_column.go b/cl/cltypes/partial_data_column.go index 9be11441bae..ce1bc35133c 100644 --- a/cl/cltypes/partial_data_column.go +++ b/cl/cltypes/partial_data_column.go @@ -131,14 +131,14 @@ func (s *PartialDataColumnSidecar) init() { } if s.PartialColumn == nil { if s.version >= clparams.GloasVersion { - s.PartialColumn = solid.NewStaticProgressiveListSSZ[*Cell](BytesPerCell) + s.PartialColumn = solid.NewStaticProgressiveListSSZ[*Cell](int(cfg.MaxBlobCommittmentsPerBlock), BytesPerCell) } else { s.PartialColumn = solid.NewStaticListSSZ[*Cell](int(cfg.MaxBlobCommittmentsPerBlock), BytesPerCell) } } if s.KzgProofs == nil { if s.version >= clparams.GloasVersion { - s.KzgProofs = solid.NewStaticProgressiveListSSZ[*KZGProof](48) + s.KzgProofs = solid.NewStaticProgressiveListSSZ[*KZGProof](int(cfg.MaxBlobCommittmentsPerBlock), 48) } else { s.KzgProofs = solid.NewStaticListSSZ[*KZGProof](int(cfg.MaxBlobCommittmentsPerBlock), 48) } diff --git a/cl/cltypes/solid/list_ssz.go b/cl/cltypes/solid/list_ssz.go index 62ef1c0832f..d7678ce70da 100644 --- a/cl/cltypes/solid/list_ssz.go +++ b/cl/cltypes/solid/list_ssz.go @@ -63,12 +63,25 @@ func NewStaticListSSZ[T EncodableHashableSSZ](limit int, bytesPerElement int) *L } } -func NewDynamicProgressiveListSSZ[T EncodableHashableSSZ]() *ListSSZ[T] { - return &ListSSZ[T]{list: make([]T, 0), progressive: true} +func NewDynamicProgressiveListSSZ[T EncodableHashableSSZ](limit int) *ListSSZ[T] { + return &ListSSZ[T]{list: make([]T, 0), limit: progressiveDecodeLimit(limit), progressive: true} } -func NewStaticProgressiveListSSZ[T EncodableHashableSSZ](bytesPerElement int) *ListSSZ[T] { - return &ListSSZ[T]{list: make([]T, 0), static: true, bytesPerElement: bytesPerElement, progressive: true} +func NewStaticProgressiveListSSZ[T EncodableHashableSSZ](limit int, bytesPerElement int) *ListSSZ[T] { + return &ListSSZ[T]{list: make([]T, 0), limit: progressiveDecodeLimit(limit), static: true, bytesPerElement: bytesPerElement, 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) { @@ -114,13 +127,6 @@ 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.progressive { - if l.static && l.bytesPerElement > 0 { - limit = uint64(len(buf) / l.bytesPerElement) - } else { - limit = uint64(len(buf) / 4) - } - } if l.static { l.list, err = ssz.DecodeStaticList[T](buf, 0, uint32(len(buf)), uint32(l.bytesPerElement), limit, version) } else { @@ -172,10 +178,13 @@ func (l *ListSSZ[T]) HashSSZProgressive(hashElement func(T) ([32]byte, error)) ( func (l *ListSSZ[T]) Clone() clonable.Clonable { if l.progressive { - if l.static { - return NewStaticProgressiveListSSZ[T](l.bytesPerElement) + return &ListSSZ[T]{ + list: make([]T, 0), + limit: l.limit, + static: l.static, + bytesPerElement: l.bytesPerElement, + progressive: true, } - return NewDynamicProgressiveListSSZ[T]() } if l.static { return NewStaticListSSZ[T](l.limit, l.bytesPerElement) 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/merkle_tree/merkle_root.go b/cl/merkle_tree/merkle_root.go index 6b0e60919a0..b978b8d5228 100644 --- a/cl/merkle_tree/merkle_root.go +++ b/cl/merkle_tree/merkle_root.go @@ -143,6 +143,9 @@ func ProgressiveContainerRootAll(schema ...any) ([32]byte, error) { } 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") } diff --git a/cl/merkle_tree/merkle_root_test.go b/cl/merkle_tree/merkle_root_test.go index 9a60e1b00c2..0344e8a9555 100644 --- a/cl/merkle_tree/merkle_root_test.go +++ b/cl/merkle_tree/merkle_root_test.go @@ -54,3 +54,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) +} From 42ec832d785d7527a66d79317c98d77e19fec897 Mon Sep 17 00:00:00 2001 From: kewei Date: Mon, 3 Aug 2026 16:51:41 +0700 Subject: [PATCH 37/42] cl: preserve progressive bid decode limits --- cl/cltypes/beacon_block.go | 179 +++++++++++++++++--------------- cl/cltypes/beacon_block_test.go | 12 +++ cl/cltypes/epbs_payload.go | 29 +++++- cl/cltypes/epbs_payload_test.go | 54 ++++++++++ cl/cltypes/solid/list_ssz.go | 15 +++ 5 files changed, 204 insertions(+), 85 deletions(-) diff --git a/cl/cltypes/beacon_block.go b/cl/cltypes/beacon_block.go index 1f3e39aa716..7d5e5557dc9 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,11 +282,7 @@ 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) } @@ -311,31 +302,77 @@ func NewBeaconBody(beaconCfg *clparams.BeaconChainConfig, version clparams.State func maxBlobCommitmentsForConfig(beaconCfg *clparams.BeaconChainConfig) int { if beaconCfg != nil && beaconCfg.MaxBlobCommittmentsPerBlock > 0 { - return int(beaconCfg.MaxBlobCommittmentsPerBlock) + return saturatingPositiveInt(beaconCfg.MaxBlobCommittmentsPerBlock, MaxBlobsCommittmentsPerBlock) } return MaxBlobsCommittmentsPerBlock } func maxPayloadAttestationsForConfig(beaconCfg *clparams.BeaconChainConfig) int { + fallback := saturatingPositiveInt(clparams.GetBeaconConfig().MaxPayloadAttestations, int(clparams.MainnetBeaconConfig.MaxPayloadAttestations)) if beaconCfg != nil && beaconCfg.MaxPayloadAttestations > 0 { - return int(beaconCfg.MaxPayloadAttestations) + 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 int(clparams.GetBeaconConfig().MaxPayloadAttestations) + 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() { - maxAttesterSlashings := MaxAttesterSlashings - maxAttestations := MaxAttestations - if b.Version.AfterOrEqual(clparams.ElectraVersion) { - maxAttesterSlashings = MaxAttesterSlashingsElectra - maxAttestations = MaxAttestationsElectra - } - b.ProposerSlashings = solid.NewStaticProgressiveListSSZ[*ProposerSlashing](MaxProposerSlashings, 416) - b.AttesterSlashings = solid.NewDynamicProgressiveListSSZ[*AttesterSlashing](maxAttesterSlashings) - b.Attestations = solid.NewDynamicProgressiveListSSZ[*solid.Attestation](maxAttestations) - b.Deposits = solid.NewStaticProgressiveListSSZ[*Deposit](MaxDeposits, 1240) - b.VoluntaryExits = solid.NewStaticProgressiveListSSZ[*SignedVoluntaryExit](MaxVoluntaryExits, 112) - b.ExecutionChanges = solid.NewStaticProgressiveListSSZ[*SignedBLSToExecutionChange](MaxExecutionChanges, 172) + 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 @@ -346,14 +383,7 @@ func (b *BeaconBody) resetGloasProgressiveLists() { // 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{} } @@ -366,37 +396,37 @@ func (b *BeaconBody) ensureNilFields() { } if b.ProposerSlashings == nil { if b.Version >= clparams.GloasVersion { - b.ProposerSlashings = solid.NewStaticProgressiveListSSZ[*ProposerSlashing](MaxProposerSlashings, 416) + b.ProposerSlashings = solid.NewStaticProgressiveListSSZ[*ProposerSlashing](limits.proposerSlashings, 416) } else { - b.ProposerSlashings = solid.NewStaticListSSZ[*ProposerSlashing](MaxProposerSlashings, 416) + b.ProposerSlashings = solid.NewStaticListSSZ[*ProposerSlashing](limits.proposerSlashings, 416) } } if b.AttesterSlashings == nil { if b.Version >= clparams.GloasVersion { - b.AttesterSlashings = solid.NewDynamicProgressiveListSSZ[*AttesterSlashing](maxAttSlashing) + b.AttesterSlashings = solid.NewDynamicProgressiveListSSZ[*AttesterSlashing](limits.attesterSlashings) } else { - b.AttesterSlashings = solid.NewDynamicListSSZ[*AttesterSlashing](maxAttSlashing) + b.AttesterSlashings = solid.NewDynamicListSSZ[*AttesterSlashing](limits.attesterSlashings) } } if b.Attestations == nil { if b.Version >= clparams.GloasVersion { - b.Attestations = solid.NewDynamicProgressiveListSSZ[*solid.Attestation](maxAttestation) + b.Attestations = solid.NewDynamicProgressiveListSSZ[*solid.Attestation](limits.attestations) } else { - b.Attestations = solid.NewDynamicListSSZ[*solid.Attestation](maxAttestation) + b.Attestations = solid.NewDynamicListSSZ[*solid.Attestation](limits.attestations) } } if b.Deposits == nil { if b.Version >= clparams.GloasVersion { - b.Deposits = solid.NewStaticProgressiveListSSZ[*Deposit](MaxDeposits, 1240) + b.Deposits = solid.NewStaticProgressiveListSSZ[*Deposit](limits.deposits, 1240) } else { - b.Deposits = solid.NewStaticListSSZ[*Deposit](MaxDeposits, 1240) + b.Deposits = solid.NewStaticListSSZ[*Deposit](limits.deposits, 1240) } } if b.VoluntaryExits == nil { if b.Version >= clparams.GloasVersion { - b.VoluntaryExits = solid.NewStaticProgressiveListSSZ[*SignedVoluntaryExit](MaxVoluntaryExits, 112) + b.VoluntaryExits = solid.NewStaticProgressiveListSSZ[*SignedVoluntaryExit](limits.voluntaryExits, 112) } else { - b.VoluntaryExits = solid.NewStaticListSSZ[*SignedVoluntaryExit](MaxVoluntaryExits, 112) + b.VoluntaryExits = solid.NewStaticListSSZ[*SignedVoluntaryExit](limits.voluntaryExits, 112) } } // [Modified in Gloas:EIP7732] ExecutionPayload removed in GLOAS @@ -405,18 +435,14 @@ func (b *BeaconBody) ensureNilFields() { } if b.ExecutionChanges == nil { if b.Version >= clparams.GloasVersion { - b.ExecutionChanges = solid.NewStaticProgressiveListSSZ[*SignedBLSToExecutionChange](MaxExecutionChanges, 172) + b.ExecutionChanges = solid.NewStaticProgressiveListSSZ[*SignedBLSToExecutionChange](limits.executionChanges, 172) } else { - b.ExecutionChanges = solid.NewStaticListSSZ[*SignedBLSToExecutionChange](MaxExecutionChanges, 172) + 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 { @@ -713,14 +739,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"` @@ -741,30 +760,26 @@ 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 { - tmp.ProposerSlashings = solid.NewStaticProgressiveListSSZ[*ProposerSlashing](MaxProposerSlashings, 416) - tmp.AttesterSlashings = solid.NewDynamicProgressiveListSSZ[*AttesterSlashing](maxAttSlashing) - tmp.Attestations = solid.NewDynamicProgressiveListSSZ[*solid.Attestation](maxAttestation) - tmp.Deposits = solid.NewStaticProgressiveListSSZ[*Deposit](MaxDeposits, 1240) - tmp.VoluntaryExits = solid.NewStaticProgressiveListSSZ[*SignedVoluntaryExit](MaxVoluntaryExits, 112) - tmp.ExecutionChanges = solid.NewStaticProgressiveListSSZ[*SignedBLSToExecutionChange](MaxExecutionChanges, 172) + 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.NewStaticProgressiveListSSZ[*KZGCommitment](maxBlobCommitmentsForConfig(b.beaconCfg), 48), diff --git a/cl/cltypes/beacon_block_test.go b/cl/cltypes/beacon_block_test.go index cd1b187f149..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" @@ -653,6 +654,7 @@ 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]}`, @@ -666,3 +668,13 @@ func TestBeaconBodyGloasJSONRejectsNullRequiredFields(t *testing.T) { }) } } + +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/epbs_payload.go b/cl/cltypes/epbs_payload.go index d6e9fb2f310..20e3291503e 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" @@ -345,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[:], @@ -364,11 +368,26 @@ 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 + return json.Unmarshal(data, (*executionPayloadBid)(e)) +} + func (e *ExecutionPayloadBid) Copy() *ExecutionPayloadBid { return &ExecutionPayloadBid{ ParentBlockHash: e.ParentBlockHash, @@ -409,7 +428,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[:]) } diff --git a/cl/cltypes/epbs_payload_test.go b/cl/cltypes/epbs_payload_test.go index e3df7bd6988..397467e7d01 100644 --- a/cl/cltypes/epbs_payload_test.go +++ b/cl/cltypes/epbs_payload_test.go @@ -1,9 +1,12 @@ package cltypes import ( + "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 +58,54 @@ 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 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/solid/list_ssz.go b/cl/cltypes/solid/list_ssz.go index d7678ce70da..69c98776bad 100644 --- a/cl/cltypes/solid/list_ssz.go +++ b/cl/cltypes/solid/list_ssz.go @@ -71,6 +71,21 @@ func NewStaticProgressiveListSSZ[T EncodableHashableSSZ](limit int, bytesPerElem return &ListSSZ[T]{list: make([]T, 0), limit: progressiveDecodeLimit(limit), static: true, bytesPerElement: bytesPerElement, progressive: true} } +func (l *ListSSZ[T]) EnsureStaticProgressive(limit int, bytesPerElement int) { + if l.progressive && l.static && l.bytesPerElement == bytesPerElement { + return + } + if l.static && l.limit > 0 && l.bytesPerElement == bytesPerElement { + l.limit = progressiveDecodeLimit(l.limit) + } else { + l.list = make([]T, 0) + 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 From c397add07c88d233ef9e39caae5adf19afd87328 Mon Sep 17 00:00:00 2001 From: kewei Date: Mon, 3 Aug 2026 16:57:59 +0700 Subject: [PATCH 38/42] cl: initialize progressive bids from JSON --- cl/cltypes/epbs_payload.go | 6 +++++- cl/cltypes/epbs_payload_test.go | 31 +++++++++++++++++++++++++++++++ cl/cltypes/solid/list_ssz.go | 1 - 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/cl/cltypes/epbs_payload.go b/cl/cltypes/epbs_payload.go index 20e3291503e..b11a3293b6a 100644 --- a/cl/cltypes/epbs_payload.go +++ b/cl/cltypes/epbs_payload.go @@ -385,7 +385,11 @@ func (e *ExecutionPayloadBid) UnmarshalJSON(data []byte) error { return errors.New("execution payload bid contains null blob KZG commitments") } type executionPayloadBid ExecutionPayloadBid - return json.Unmarshal(data, (*executionPayloadBid)(e)) + 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 { diff --git a/cl/cltypes/epbs_payload_test.go b/cl/cltypes/epbs_payload_test.go index 397467e7d01..0e7d5c54346 100644 --- a/cl/cltypes/epbs_payload_test.go +++ b/cl/cltypes/epbs_payload_test.go @@ -1,6 +1,7 @@ package cltypes import ( + "encoding/json" "errors" "testing" @@ -95,6 +96,36 @@ func TestExecutionPayloadBidClonePreservesProgressiveLimit(t *testing.T) { 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) diff --git a/cl/cltypes/solid/list_ssz.go b/cl/cltypes/solid/list_ssz.go index 69c98776bad..ac334b7ad7b 100644 --- a/cl/cltypes/solid/list_ssz.go +++ b/cl/cltypes/solid/list_ssz.go @@ -78,7 +78,6 @@ func (l *ListSSZ[T]) EnsureStaticProgressive(limit int, bytesPerElement int) { if l.static && l.limit > 0 && l.bytesPerElement == bytesPerElement { l.limit = progressiveDecodeLimit(l.limit) } else { - l.list = make([]T, 0) l.limit = progressiveDecodeLimit(limit) l.static = true l.bytesPerElement = bytesPerElement From 6cb2eab005d88781aa13d56efc4c50ee25d11fdb Mon Sep 17 00:00:00 2001 From: kewei Date: Mon, 3 Aug 2026 17:33:15 +0700 Subject: [PATCH 39/42] cl: address Copilot review findings --- cl/beacon/handler/pool.go | 14 +++++++------- cl/beacon/handler/pool_test.go | 35 ++++++++++++++++++++++++++++++++++ cl/cltypes/beacon_block.go | 5 ++++- cl/merkle_tree/merkle_root.go | 2 +- 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/cl/beacon/handler/pool.go b/cl/beacon/handler/pool.go index 72ecc3470f1..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,20 +383,20 @@ 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: len(failures), Message: "invalid aggregate and proof"}) + 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: len(failures), Message: "invalid aggregate and proof: missing committee bits"}) + 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: len(failures), Message: err.Error()}) + failures = append(failures, poolingFailure{Index: idx, Message: err.Error()}) continue } encodedSSZ, err := v.EncodeSSZ(nil) @@ -414,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 b332acf3d92..c7ebec85306 100644 --- a/cl/beacon/handler/pool_test.go +++ b/cl/beacon/handler/pool_test.go @@ -298,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/cltypes/beacon_block.go b/cl/cltypes/beacon_block.go index 7d5e5557dc9..77d1bf73f66 100644 --- a/cl/cltypes/beacon_block.go +++ b/cl/cltypes/beacon_block.go @@ -308,7 +308,10 @@ func maxBlobCommitmentsForConfig(beaconCfg *clparams.BeaconChainConfig) int { } func maxPayloadAttestationsForConfig(beaconCfg *clparams.BeaconChainConfig) int { - fallback := saturatingPositiveInt(clparams.GetBeaconConfig().MaxPayloadAttestations, int(clparams.MainnetBeaconConfig.MaxPayloadAttestations)) + 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) } diff --git a/cl/merkle_tree/merkle_root.go b/cl/merkle_tree/merkle_root.go index b978b8d5228..8fce1b19a76 100644 --- a/cl/merkle_tree/merkle_root.go +++ b/cl/merkle_tree/merkle_root.go @@ -255,7 +255,7 @@ func progressiveSchemaRoots(schema []any) ([][32]byte, error) { } roots[i] = root default: - panic(fmt.Sprintf("Can't create TreeRoot: unsported type %T at index %d", i, obj)) + panic(fmt.Sprintf("Can't create TreeRoot: unsupported type %T at index %d", i, obj)) } } return roots, nil From 281153a9a6a2d3dd80daf90f5d1dbb746fb8e4ef Mon Sep 17 00:00:00 2001 From: kewei Date: Mon, 3 Aug 2026 18:07:15 +0700 Subject: [PATCH 40/42] cl: address Gloas review findings --- cl/aggregation/pool_impl.go | 11 +++-- cl/aggregation/pool_test.go | 42 +++++++++++++++++--- cl/cltypes/beacon_block.go | 1 + cl/cltypes/solid/bitlist.go | 13 ++++-- cl/cltypes/solid/bitlist_test.go | 15 +++++++ cl/merkle_tree/merkle_root.go | 2 +- cl/merkle_tree/merkle_root_test.go | 14 +++++++ cl/phase1/core/state/accessors.go | 9 ++++- cl/phase1/core/state/accessors_gloas_test.go | 14 +++++++ 9 files changed, 105 insertions(+), 16 deletions(-) diff --git a/cl/aggregation/pool_impl.go b/cl/aggregation/pool_impl.go index bcbc4e7353b..f6ad7bf430f 100644 --- a/cl/aggregation/pool_impl.go +++ b/cl/aggregation/pool_impl.go @@ -79,6 +79,7 @@ func (p *aggregationPoolImpl) AddAttestation(inAtt *solid.Attestation) error { epoch := p.ethClock.GetEpochAtSlot(inAtt.Data.Slot) clversion := p.ethClock.StateVersionByEpoch(epoch) + inAtt.SetVersion(clversion) if clversion.BeforeOrEqual(clparams.DenebVersion) { p.aggregatesLock.Lock() defer p.aggregatesLock.Unlock() @@ -118,12 +119,12 @@ func (p *aggregationPoolImpl) AddAttestation(inAtt *solid.Attestation) error { } } else { // Electra and after case, aggregate by committee - p.aggregateByCommittee(inAtt) + 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 @@ -164,12 +165,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..78800e2b117 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,34 @@ 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) TestAddAttestation() { testcases := []struct { name string diff --git a/cl/cltypes/beacon_block.go b/cl/cltypes/beacon_block.go index 77d1bf73f66..04f190e18d8 100644 --- a/cl/cltypes/beacon_block.go +++ b/cl/cltypes/beacon_block.go @@ -698,6 +698,7 @@ 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 diff --git a/cl/cltypes/solid/bitlist.go b/cl/cltypes/solid/bitlist.go index ae3601482fd..484adde259c 100644 --- a/cl/cltypes/solid/bitlist.go +++ b/cl/cltypes/solid/bitlist.go @@ -190,8 +190,9 @@ func (u *BitList) HashSSZ() ([32]byte, error) { } func (u *BitList) HashSSZProgressive() ([32]byte, error) { - bitLength := u.Bits() - packed := append([]byte(nil), u.Bytes()...) + bytes := u.Bytes() + bitLength := bitlistBits(bytes) + packed := append([]byte(nil), bytes...) if bitLength < len(packed)*8 { packed[bitLength/8] &^= 1 << uint(bitLength%8) } @@ -235,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.u) +} + +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 diff --git a/cl/cltypes/solid/bitlist_test.go b/cl/cltypes/solid/bitlist_test.go index 0eda57a1e49..7ceec2d9403 100644 --- a/cl/cltypes/solid/bitlist_test.go +++ b/cl/cltypes/solid/bitlist_test.go @@ -41,6 +41,21 @@ 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 TestBitListCopyTo(t *testing.T) { require := require.New(t) diff --git a/cl/merkle_tree/merkle_root.go b/cl/merkle_tree/merkle_root.go index 8fce1b19a76..1ac7dbe73cc 100644 --- a/cl/merkle_tree/merkle_root.go +++ b/cl/merkle_tree/merkle_root.go @@ -255,7 +255,7 @@ func progressiveSchemaRoots(schema []any) ([][32]byte, error) { } roots[i] = root default: - panic(fmt.Sprintf("Can't create TreeRoot: unsupported type %T at index %d", i, obj)) + panic(fmt.Sprintf("Can't create TreeRoot: unsupported type %T at index %d", obj, i)) } } return roots, nil diff --git a/cl/merkle_tree/merkle_root_test.go b/cl/merkle_tree/merkle_root_test.go index 0344e8a9555..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}, diff --git a/cl/phase1/core/state/accessors.go b/cl/phase1/core/state/accessors.go index 9b8f7e22f6a..12000f4a125 100644 --- a/cl/phase1/core/state/accessors.go +++ b/cl/phase1/core/state/accessors.go @@ -208,9 +208,16 @@ func ValidateIndexedAttestationIndices(cfg *clparams.BeaconChainConfig, version 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 { - limit *= cfg.MaxCommitteesPerSlot + 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) diff --git a/cl/phase1/core/state/accessors_gloas_test.go b/cl/phase1/core/state/accessors_gloas_test.go index 65fc4b7eef5..3fe33b91af7 100644 --- a/cl/phase1/core/state/accessors_gloas_test.go +++ b/cl/phase1/core/state/accessors_gloas_test.go @@ -23,3 +23,17 @@ func TestIsValidIndexedAttestationRejectsOversizedGloasIndicesBeforeLookup(t *te 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)) +} From 3cc88f219c03d74489e309ec780a80eacc8b567c Mon Sep 17 00:00:00 2001 From: kewei Date: Mon, 3 Aug 2026 18:17:04 +0700 Subject: [PATCH 41/42] cl: harden attestation aggregation state --- cl/aggregation/pool_impl.go | 15 ++++++--- cl/aggregation/pool_test.go | 54 +++++++++++++++++++++++++++++++- cl/cltypes/solid/bitlist.go | 2 +- cl/cltypes/solid/bitlist_test.go | 20 ++++++++++++ 4 files changed, 84 insertions(+), 7 deletions(-) diff --git a/cl/aggregation/pool_impl.go b/cl/aggregation/pool_impl.go index f6ad7bf430f..9f5a40ed1bb 100644 --- a/cl/aggregation/pool_impl.go +++ b/cl/aggregation/pool_impl.go @@ -79,13 +79,14 @@ func (p *aggregationPoolImpl) AddAttestation(inAtt *solid.Attestation) error { epoch := p.ethClock.GetEpochAtSlot(inAtt.Data.Slot) clversion := p.ethClock.StateVersionByEpoch(epoch) - inAtt.SetVersion(clversion) if clversion.BeforeOrEqual(clparams.DenebVersion) { p.aggregatesLock.Lock() 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 } @@ -112,14 +113,16 @@ 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, clversion) + return p.aggregateByCommittee(inAtt, clversion) } return nil } @@ -141,7 +144,9 @@ func (p *aggregationPoolImpl) aggregateByCommittee(inAtt *solid.Attestation, ver } 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 } diff --git a/cl/aggregation/pool_test.go b/cl/aggregation/pool_test.go index 78800e2b117..951d0985373 100644 --- a/cl/aggregation/pool_test.go +++ b/cl/aggregation/pool_test.go @@ -199,6 +199,56 @@ func (t *PoolTestSuite) TestMergedGloasAttestationUsesProgressiveHash() { 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 @@ -266,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/cltypes/solid/bitlist.go b/cl/cltypes/solid/bitlist.go index 484adde259c..4c93064af36 100644 --- a/cl/cltypes/solid/bitlist.go +++ b/cl/cltypes/solid/bitlist.go @@ -236,7 +236,7 @@ func (u *BitList) Clone() clonable.Clonable { // getBitlistLength return the amount of bits in given bitlist. func (u *BitList) Bits() int { - return bitlistBits(u.u) + return bitlistBits(u.Bytes()) } func bitlistBits(data []byte) int { diff --git a/cl/cltypes/solid/bitlist_test.go b/cl/cltypes/solid/bitlist_test.go index 7ceec2d9403..996a27ed04d 100644 --- a/cl/cltypes/solid/bitlist_test.go +++ b/cl/cltypes/solid/bitlist_test.go @@ -56,6 +56,26 @@ func TestBitListProgressiveHashIgnoresBackingBytesPastLength(t *testing.T) { 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) { + bitlist := solid.NewBitList(1, 2048) + bitlist.Set(0, 0b00000010) + bitlist.Set(32, 1) + other := solid.NewBitList(1, 2048) + other.Set(0, 0b00000010) + + merged, err := bitlist.Merge(other) + require.NoError(t, err) + require.Equal(t, []byte{0b00000010}, merged.Bytes()) +} + func TestBitListCopyTo(t *testing.T) { require := require.New(t) From 5b4517179717fa9910c3db414496768ce1fe247d Mon Sep 17 00:00:00 2001 From: kewei Date: Mon, 3 Aug 2026 18:21:23 +0700 Subject: [PATCH 42/42] cltypes: merge bitlists by logical length --- cl/cltypes/solid/bitlist.go | 14 ++++++++------ cl/cltypes/solid/bitlist_test.go | 27 ++++++++++++++++++--------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/cl/cltypes/solid/bitlist.go b/cl/cltypes/solid/bitlist.go index 4c93064af36..40fad079bac 100644 --- a/cl/cltypes/solid/bitlist.go +++ b/cl/cltypes/solid/bitlist.go @@ -283,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 996a27ed04d..dfb0c3a1773 100644 --- a/cl/cltypes/solid/bitlist_test.go +++ b/cl/cltypes/solid/bitlist_test.go @@ -65,15 +65,24 @@ func TestBitListBitsIgnoresBackingBytesPastLength(t *testing.T) { } func TestBitListMergeIgnoresBackingBytesPastLength(t *testing.T) { - bitlist := solid.NewBitList(1, 2048) - bitlist.Set(0, 0b00000010) - bitlist.Set(32, 1) - other := solid.NewBitList(1, 2048) - other.Set(0, 0b00000010) - - merged, err := bitlist.Merge(other) - require.NoError(t, err) - require.Equal(t, []byte{0b00000010}, merged.Bytes()) + 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) {