Skip to content

Commit a63883b

Browse files
committed
Fix envelopes-by-range to only serve canonical payloads via backward walk
1 parent 1362654 commit a63883b

15 files changed

Lines changed: 510 additions & 165 deletions

beacon-chain/blockchain/gloas_test.go

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -782,11 +782,17 @@ func TestSaveHead_GloasForkBoundary_PreforkBidForcesEmptyHead(t *testing.T) {
782782
LatestBlockHeader: &ethpb.BeaconBlockHeader{ParentRoot: make([]byte, 32), StateRoot: make([]byte, 32), BodyRoot: make([]byte, 32)},
783783
Eth1Data: &ethpb.Eth1Data{DepositRoot: make([]byte, 32), BlockHash: make([]byte, 32)},
784784
LatestExecutionPayloadBid: &ethpb.ExecutionPayloadBid{BlockHash: make([]byte, 32), ParentBlockHash: make([]byte, 32), ParentBlockRoot: make([]byte, 32), PrevRandao: make([]byte, 32), FeeRecipient: make([]byte, 20), BlobKzgCommitments: [][]byte{make([]byte, 48)}},
785-
BuilderPendingPayments: func() []*ethpb.BuilderPendingPayment { pp := make([]*ethpb.BuilderPendingPayment, 64); for i := range pp { pp[i] = &ethpb.BuilderPendingPayment{Withdrawal: &ethpb.BuilderPendingWithdrawal{FeeRecipient: make([]byte, 20)}} }; return pp }(),
785+
BuilderPendingPayments: func() []*ethpb.BuilderPendingPayment {
786+
pp := make([]*ethpb.BuilderPendingPayment, 64)
787+
for i := range pp {
788+
pp[i] = &ethpb.BuilderPendingPayment{Withdrawal: &ethpb.BuilderPendingWithdrawal{FeeRecipient: make([]byte, 20)}}
789+
}
790+
return pp
791+
}(),
786792
ExecutionPayloadAvailability: make([]byte, 1024),
787-
LatestBlockHash: make([]byte, 32),
788-
PayloadExpectedWithdrawals: make([]*enginev1.Withdrawal, 0),
789-
ProposerLookahead: make([]uint64, 64),
793+
LatestBlockHash: make([]byte, 32),
794+
PayloadExpectedWithdrawals: make([]*enginev1.Withdrawal, 0),
795+
ProposerLookahead: make([]uint64, 64),
790796
})
791797
require.NoError(t, err2)
792798
oldRoot := bytesutil.ToBytes32([]byte("oldroot1"))
@@ -853,11 +859,17 @@ func TestSaveHead_GloasForkBoundary_PostforkBidSetsFullHead(t *testing.T) {
853859
LatestBlockHeader: &ethpb.BeaconBlockHeader{ParentRoot: make([]byte, 32), StateRoot: make([]byte, 32), BodyRoot: make([]byte, 32)},
854860
Eth1Data: &ethpb.Eth1Data{DepositRoot: make([]byte, 32), BlockHash: make([]byte, 32)},
855861
LatestExecutionPayloadBid: &ethpb.ExecutionPayloadBid{BlockHash: make([]byte, 32), ParentBlockHash: make([]byte, 32), ParentBlockRoot: make([]byte, 32), PrevRandao: make([]byte, 32), FeeRecipient: make([]byte, 20), BlobKzgCommitments: [][]byte{make([]byte, 48)}},
856-
BuilderPendingPayments: func() []*ethpb.BuilderPendingPayment { pp := make([]*ethpb.BuilderPendingPayment, 64); for i := range pp { pp[i] = &ethpb.BuilderPendingPayment{Withdrawal: &ethpb.BuilderPendingWithdrawal{FeeRecipient: make([]byte, 20)}} }; return pp }(),
862+
BuilderPendingPayments: func() []*ethpb.BuilderPendingPayment {
863+
pp := make([]*ethpb.BuilderPendingPayment, 64)
864+
for i := range pp {
865+
pp[i] = &ethpb.BuilderPendingPayment{Withdrawal: &ethpb.BuilderPendingWithdrawal{FeeRecipient: make([]byte, 20)}}
866+
}
867+
return pp
868+
}(),
857869
ExecutionPayloadAvailability: make([]byte, 1024),
858-
LatestBlockHash: make([]byte, 32),
859-
PayloadExpectedWithdrawals: make([]*enginev1.Withdrawal, 0),
860-
ProposerLookahead: make([]uint64, 64),
870+
LatestBlockHash: make([]byte, 32),
871+
PayloadExpectedWithdrawals: make([]*enginev1.Withdrawal, 0),
872+
ProposerLookahead: make([]uint64, 64),
861873
})
862874
require.NoError(t, err2)
863875
oldRoot2 := bytesutil.ToBytes32([]byte("oldroot2"))

beacon-chain/db/iface/interface.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type ReadOnlyDatabase interface {
3434
IsFinalizedBlock(ctx context.Context, blockRoot [32]byte) bool
3535
FinalizedChildBlock(ctx context.Context, blockRoot [32]byte) (interfaces.ReadOnlySignedBeaconBlock, error)
3636
HighestRootsBelowSlot(ctx context.Context, slot primitives.Slot) (primitives.Slot, [][32]byte, error)
37+
LowestRootsAboveSlot(ctx context.Context, slot primitives.Slot) (primitives.Slot, [][32]byte, error)
3738
EarliestSlot(ctx context.Context) (primitives.Slot, error)
3839
// State related methods.
3940
State(ctx context.Context, blockRoot [32]byte) (state.BeaconState, error)
@@ -68,6 +69,7 @@ type ReadOnlyDatabase interface {
6869

6970
// Execution payload envelope operations (Gloas+).
7071
ExecutionPayloadEnvelope(ctx context.Context, blockRoot [32]byte) (*ethpb.SignedBlindedExecutionPayloadEnvelope, error)
72+
ExecutionPayloadEnvelopeByBlockHash(ctx context.Context, blockHash [32]byte) (*ethpb.SignedBlindedExecutionPayloadEnvelope, error)
7173
HasExecutionPayloadEnvelope(ctx context.Context, blockRoot [32]byte) bool
7274

7375
// P2P Metadata operations.

beacon-chain/db/kv/blocks.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"fmt"
7+
"math"
78
"slices"
89

910
"github.com/OffchainLabs/prysm/v7/beacon-chain/db/filters"
@@ -808,6 +809,43 @@ func (s *Store) HighestRootsBelowSlot(ctx context.Context, slot primitives.Slot)
808809
return fs, roots, nil
809810
}
810811

812+
// LowestRootsAboveSlot returns roots from the database slot index from the lowest slot above the input slot.
813+
// The returned slot is the slot where the roots were found. This is the mirror of HighestRootsBelowSlot.
814+
// If no block exists above the given slot, an empty root slice is returned.
815+
func (s *Store) LowestRootsAboveSlot(ctx context.Context, slot primitives.Slot) (fs primitives.Slot, roots [][32]byte, err error) {
816+
ctx, span := trace.StartSpan(ctx, "BeaconDB.LowestRootsAboveSlot")
817+
defer span.End()
818+
819+
// Guard against overflow: if slot is already max, nothing can be above it.
820+
if slot == math.MaxUint64 {
821+
return 0, nil, nil
822+
}
823+
// Seek to slot+1 so we find the first key strictly above slot.
824+
sk := bytesutil.Uint64ToBytesBigEndian(uint64(slot + 1))
825+
err = s.db.View(func(tx *bolt.Tx) error {
826+
bkt := tx.Bucket(blockSlotIndicesBucket)
827+
c := bkt.Cursor()
828+
// Seek positions the cursor at the smallest key >= sk.
829+
// If no key >= sk exists, sl is nil and we return empty.
830+
for sl, r := c.Seek(sk); sl != nil; sl, r = c.Next() {
831+
if ctx.Err() != nil {
832+
return ctx.Err()
833+
}
834+
if r == nil {
835+
continue
836+
}
837+
fs = bytesutil.BytesToSlotBigEndian(sl)
838+
roots, err = splitRoots(r)
839+
if err != nil {
840+
return errors.Wrapf(err, "error parsing packed roots %#x", r)
841+
}
842+
return nil
843+
}
844+
return nil
845+
})
846+
return fs, roots, err
847+
}
848+
811849
// FeeRecipientByValidatorID returns the fee recipient for a validator id.
812850
// `ErrNotFoundFeeRecipient` is returned if the validator id is not found.
813851
func (s *Store) FeeRecipientByValidatorID(ctx context.Context, id primitives.ValidatorIndex) (common.Address, error) {

beacon-chain/db/kv/blocks_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package kv
33
import (
44
"context"
55
"fmt"
6+
"math"
67
"testing"
78
"time"
89

@@ -1460,3 +1461,51 @@ func TestStore_EarliestSlot(t *testing.T) {
14601461
assert.Equal(t, nextEpochSlot, slot)
14611462
})
14621463
}
1464+
1465+
func TestStore_LowestRootsAboveSlot(t *testing.T) {
1466+
for _, tt := range blockTests {
1467+
t.Run(tt.name, func(t *testing.T) {
1468+
db := setupDB(t)
1469+
ctx := t.Context()
1470+
1471+
block1, err := tt.newBlock(primitives.Slot(10), nil)
1472+
require.NoError(t, err)
1473+
block2, err := tt.newBlock(primitives.Slot(50), nil)
1474+
require.NoError(t, err)
1475+
block3, err := tt.newBlock(primitives.Slot(100), nil)
1476+
require.NoError(t, err)
1477+
1478+
require.NoError(t, db.SaveBlock(ctx, block1))
1479+
require.NoError(t, db.SaveBlock(ctx, block2))
1480+
require.NoError(t, db.SaveBlock(ctx, block3))
1481+
1482+
// Exact next-slot hit: slot 9 → block at slot 10.
1483+
foundSlot, roots, err := db.LowestRootsAboveSlot(ctx, 9)
1484+
require.NoError(t, err)
1485+
require.Equal(t, 1, len(roots))
1486+
assert.Equal(t, primitives.Slot(10), foundSlot)
1487+
1488+
// Gap across missed slots: slot 10 → block at slot 50 (slots 11-49 missing).
1489+
foundSlot, roots, err = db.LowestRootsAboveSlot(ctx, 10)
1490+
require.NoError(t, err)
1491+
require.Equal(t, 1, len(roots))
1492+
assert.Equal(t, primitives.Slot(50), foundSlot)
1493+
1494+
// Another gap: slot 50 → block at slot 100.
1495+
foundSlot, roots, err = db.LowestRootsAboveSlot(ctx, 50)
1496+
require.NoError(t, err)
1497+
require.Equal(t, 1, len(roots))
1498+
assert.Equal(t, primitives.Slot(100), foundSlot)
1499+
1500+
// No slot above: slot 100 is the last block.
1501+
_, roots, err = db.LowestRootsAboveSlot(ctx, 100)
1502+
require.NoError(t, err)
1503+
assert.Equal(t, 0, len(roots))
1504+
1505+
// Max-slot overflow: should return empty, not wrap to 0.
1506+
_, roots, err = db.LowestRootsAboveSlot(ctx, math.MaxUint64)
1507+
require.NoError(t, err)
1508+
assert.Equal(t, 0, len(roots))
1509+
})
1510+
}
1511+
}

beacon-chain/db/kv/execution_payload_envelope.go

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import (
1515
// beacon block root. The envelope is stored in blinded form: the full execution payload is replaced
1616
// with its block hash. The full payload can later be retrieved from the EL via
1717
// engine_getPayloadBodiesByHash.
18+
// A secondary index from BlockHash → BeaconBlockRoot is maintained so that
19+
// envelopes can be looked up by execution block hash.
1820
func (s *Store) SaveExecutionPayloadEnvelope(ctx context.Context, env *ethpb.SignedExecutionPayloadEnvelope) error {
1921
_, span := trace.StartSpan(ctx, "BeaconDB.SaveExecutionPayloadEnvelope")
2022
defer span.End()
@@ -24,6 +26,7 @@ func (s *Store) SaveExecutionPayloadEnvelope(ctx context.Context, env *ethpb.Sig
2426
}
2527

2628
blockRoot := bytesutil.ToBytes32(env.Message.BeaconBlockRoot)
29+
blockHash := bytesutil.ToBytes32(env.Message.Payload.BlockHash)
2730
blinded := blindEnvelope(env)
2831

2932
enc, err := encodeBlindedEnvelope(blinded)
@@ -32,8 +35,10 @@ func (s *Store) SaveExecutionPayloadEnvelope(ctx context.Context, env *ethpb.Sig
3235
}
3336

3437
return s.db.Update(func(tx *bolt.Tx) error {
35-
bkt := tx.Bucket(executionPayloadEnvelopesBucket)
36-
return bkt.Put(blockRoot[:], enc)
38+
if err := tx.Bucket(executionPayloadEnvelopesBucket).Put(blockRoot[:], enc); err != nil {
39+
return err
40+
}
41+
return tx.Bucket(executionPayloadEnvelopeBlockHashBucket).Put(blockHash[:], blockRoot[:])
3742
})
3843
}
3944

@@ -56,6 +61,30 @@ func (s *Store) ExecutionPayloadEnvelope(ctx context.Context, blockRoot [32]byte
5661
return decodeBlindedEnvelope(enc)
5762
}
5863

64+
// ExecutionPayloadEnvelopeByBlockHash retrieves the blinded signed execution payload envelope
65+
// by execution block hash. It uses the secondary BlockHash → BeaconBlockRoot index and then
66+
// fetches the envelope from the primary bucket.
67+
func (s *Store) ExecutionPayloadEnvelopeByBlockHash(ctx context.Context, blockHash [32]byte) (*ethpb.SignedBlindedExecutionPayloadEnvelope, error) {
68+
_, span := trace.StartSpan(ctx, "BeaconDB.ExecutionPayloadEnvelopeByBlockHash")
69+
defer span.End()
70+
71+
var enc []byte
72+
if err := s.db.View(func(tx *bolt.Tx) error {
73+
blockRoot := tx.Bucket(executionPayloadEnvelopeBlockHashBucket).Get(blockHash[:])
74+
if blockRoot == nil {
75+
return nil
76+
}
77+
enc = tx.Bucket(executionPayloadEnvelopesBucket).Get(blockRoot)
78+
return nil
79+
}); err != nil {
80+
return nil, err
81+
}
82+
if enc == nil {
83+
return nil, errors.Wrap(ErrNotFound, "execution payload envelope not found for block hash")
84+
}
85+
return decodeBlindedEnvelope(enc)
86+
}
87+
5988
// HasExecutionPayloadEnvelope checks whether an execution payload envelope exists for the given beacon block root.
6089
func (s *Store) HasExecutionPayloadEnvelope(ctx context.Context, blockRoot [32]byte) bool {
6190
_, span := trace.StartSpan(ctx, "BeaconDB.HasExecutionPayloadEnvelope")
@@ -72,13 +101,25 @@ func (s *Store) HasExecutionPayloadEnvelope(ctx context.Context, blockRoot [32]b
72101
return exists
73102
}
74103

75-
// DeleteExecutionPayloadEnvelope removes a signed execution payload envelope by beacon block root.
104+
// DeleteExecutionPayloadEnvelope removes a signed execution payload envelope by beacon block root
105+
// and cleans up the BlockHash index entry.
76106
func (s *Store) DeleteExecutionPayloadEnvelope(ctx context.Context, blockRoot [32]byte) error {
77107
_, span := trace.StartSpan(ctx, "BeaconDB.DeleteExecutionPayloadEnvelope")
78108
defer span.End()
79109

80110
return s.db.Update(func(tx *bolt.Tx) error {
81111
bkt := tx.Bucket(executionPayloadEnvelopesBucket)
112+
// Read the existing entry to find the BlockHash for index cleanup.
113+
enc := bkt.Get(blockRoot[:])
114+
if enc != nil {
115+
blinded, err := decodeBlindedEnvelope(enc)
116+
if err == nil && blinded.Message != nil {
117+
blockHash := bytesutil.ToBytes32(blinded.Message.BlockHash)
118+
if err := tx.Bucket(executionPayloadEnvelopeBlockHashBucket).Delete(blockHash[:]); err != nil {
119+
return err
120+
}
121+
}
122+
}
82123
return bkt.Delete(blockRoot[:])
83124
})
84125
}
@@ -95,6 +136,7 @@ func blindEnvelope(env *ethpb.SignedExecutionPayloadEnvelope) *ethpb.SignedBlind
95136
BeaconBlockRoot: env.Message.BeaconBlockRoot,
96137
Slot: env.Message.Slot,
97138
StateRoot: env.Message.StateRoot,
139+
ParentBlockHash: env.Message.Payload.ParentHash,
98140
},
99141
Signature: env.Signature,
100142
}

beacon-chain/db/kv/execution_payload_envelope_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package kv
22

33
import (
4+
"bytes"
45
"context"
56
"testing"
67

@@ -75,6 +76,7 @@ func TestStore_SaveAndRetrieveExecutionPayloadEnvelope(t *testing.T) {
7576

7677
// BlockHash should be the payload's block hash (not a hash tree root).
7778
assert.DeepEqual(t, env.Message.Payload.BlockHash, loaded.Message.BlockHash)
79+
assert.Equal(t, true, bytes.Equal(env.Message.Payload.ParentHash, loaded.Message.ParentBlockHash))
7880
}
7981

8082
func TestStore_DeleteExecutionPayloadEnvelope(t *testing.T) {
@@ -107,13 +109,60 @@ func TestStore_SaveExecutionPayloadEnvelope_NilRejected(t *testing.T) {
107109
require.ErrorContains(t, "nil", err)
108110
}
109111

112+
func TestStore_ExecutionPayloadEnvelopeByBlockHash(t *testing.T) {
113+
db := setupDB(t)
114+
ctx := context.Background()
115+
env := testEnvelope(t)
116+
blockHash := bytesutil.ToBytes32(env.Message.Payload.BlockHash)
117+
118+
// Save envelope — should populate both primary and BlockHash index.
119+
require.NoError(t, db.SaveExecutionPayloadEnvelope(ctx, env))
120+
121+
// Look up by block hash.
122+
loaded, err := db.ExecutionPayloadEnvelopeByBlockHash(ctx, blockHash)
123+
require.NoError(t, err)
124+
assert.Equal(t, env.Message.Slot, loaded.Message.Slot)
125+
assert.DeepEqual(t, env.Message.Payload.BlockHash, loaded.Message.BlockHash)
126+
assert.Equal(t, true, bytes.Equal(env.Message.Payload.ParentHash, loaded.Message.ParentBlockHash))
127+
}
128+
129+
func TestStore_ExecutionPayloadEnvelopeByBlockHash_NotFound(t *testing.T) {
130+
db := setupDB(t)
131+
ctx := context.Background()
132+
nonExistent := bytesutil.ToBytes32([]byte("nonexistent"))
133+
134+
_, err := db.ExecutionPayloadEnvelopeByBlockHash(ctx, nonExistent)
135+
require.ErrorContains(t, "not found", err)
136+
}
137+
138+
func TestStore_DeleteExecutionPayloadEnvelope_CleansBlockHashIndex(t *testing.T) {
139+
db := setupDB(t)
140+
ctx := context.Background()
141+
env := testEnvelope(t)
142+
blockRoot := bytesutil.ToBytes32(env.Message.BeaconBlockRoot)
143+
blockHash := bytesutil.ToBytes32(env.Message.Payload.BlockHash)
144+
145+
require.NoError(t, db.SaveExecutionPayloadEnvelope(ctx, env))
146+
147+
// Verify BlockHash lookup works before delete.
148+
_, err := db.ExecutionPayloadEnvelopeByBlockHash(ctx, blockHash)
149+
require.NoError(t, err)
150+
151+
// Delete should clean up both buckets.
152+
require.NoError(t, db.DeleteExecutionPayloadEnvelope(ctx, blockRoot))
153+
154+
_, err = db.ExecutionPayloadEnvelopeByBlockHash(ctx, blockHash)
155+
require.ErrorContains(t, "not found", err)
156+
}
157+
110158
func TestBlindEnvelope_PreservesBlockHash(t *testing.T) {
111159
env := testEnvelope(t)
112160

113161
blinded := blindEnvelope(env)
114162

115163
// Should contain the block hash from the payload, not a hash tree root.
116164
assert.DeepEqual(t, env.Message.Payload.BlockHash, blinded.Message.BlockHash)
165+
assert.Equal(t, true, bytes.Equal(env.Message.Payload.ParentHash, blinded.Message.ParentBlockHash))
117166

118167
// Metadata should be preserved.
119168
assert.Equal(t, env.Message.BuilderIndex, blinded.Message.BuilderIndex)

beacon-chain/db/kv/kv.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ var Buckets = [][]byte{
127127
registrationBucket,
128128
custodyBucket,
129129
executionPayloadEnvelopesBucket,
130+
executionPayloadEnvelopeBlockHashBucket,
130131
}
131132

132133
// KVStoreOption is a functional option that modifies a kv.Store.

beacon-chain/db/kv/schema.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,18 @@ package kv
77
// it easy to scan for keys that have a certain shard number as a prefix and return those
88
// corresponding attestations.
99
var (
10-
blocksBucket = []byte("blocks")
11-
stateBucket = []byte("state")
12-
stateSummaryBucket = []byte("state-summary")
13-
chainMetadataBucket = []byte("chain-metadata")
14-
checkpointBucket = []byte("check-point")
15-
powchainBucket = []byte("powchain")
16-
stateValidatorsBucket = []byte("state-validators")
17-
feeRecipientBucket = []byte("fee-recipient")
18-
registrationBucket = []byte("registration")
19-
stateDiffBucket = []byte("state-diff")
20-
executionPayloadEnvelopesBucket = []byte("execution-payload-envelopes")
10+
blocksBucket = []byte("blocks")
11+
stateBucket = []byte("state")
12+
stateSummaryBucket = []byte("state-summary")
13+
chainMetadataBucket = []byte("chain-metadata")
14+
checkpointBucket = []byte("check-point")
15+
powchainBucket = []byte("powchain")
16+
stateValidatorsBucket = []byte("state-validators")
17+
feeRecipientBucket = []byte("fee-recipient")
18+
registrationBucket = []byte("registration")
19+
stateDiffBucket = []byte("state-diff")
20+
executionPayloadEnvelopesBucket = []byte("execution-payload-envelopes")
21+
executionPayloadEnvelopeBlockHashBucket = []byte("execution-payload-envelope-block-hash-index")
2122

2223
// Light Client Updates Bucket
2324
lightClientUpdatesBucket = []byte("light-client-updates")

0 commit comments

Comments
 (0)