Skip to content

Commit 7c3e456

Browse files
authored
Fix proposer to use advanced state for sync committee position calculation (#15905)
* Sync committee use correct state to calculate position * Unit test
1 parent 96429c5 commit 7c3e456

File tree

4 files changed

+109
-15
lines changed

4 files changed

+109
-15
lines changed

beacon-chain/rpc/prysm/v1alpha1/validator/proposer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ func (vs *Server) BuildBlockParallel(ctx context.Context, sBlk interfaces.Signed
229229
sBlk.SetVoluntaryExits(vs.getExits(head, sBlk.Block().Slot()))
230230

231231
// Set sync aggregate. New in Altair.
232-
vs.setSyncAggregate(ctx, sBlk)
232+
vs.setSyncAggregate(ctx, sBlk, head)
233233

234234
// Set bls to execution change. New in Capella.
235235
vs.setBlsToExecData(sBlk, head)

beacon-chain/rpc/prysm/v1alpha1/validator/proposer_altair.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66

77
"github.com/OffchainLabs/prysm/v6/beacon-chain/core/helpers"
8+
"github.com/OffchainLabs/prysm/v6/beacon-chain/state"
89
"github.com/OffchainLabs/prysm/v6/config/params"
910
"github.com/OffchainLabs/prysm/v6/consensus-types/interfaces"
1011
"github.com/OffchainLabs/prysm/v6/consensus-types/primitives"
@@ -20,12 +21,12 @@ import (
2021
"github.com/prysmaticlabs/go-bitfield"
2122
)
2223

23-
func (vs *Server) setSyncAggregate(ctx context.Context, blk interfaces.SignedBeaconBlock) {
24+
func (vs *Server) setSyncAggregate(ctx context.Context, blk interfaces.SignedBeaconBlock, headState state.BeaconState) {
2425
if blk.Version() < version.Altair {
2526
return
2627
}
2728

28-
syncAggregate, err := vs.getSyncAggregate(ctx, slots.PrevSlot(blk.Block().Slot()), blk.Block().ParentRoot())
29+
syncAggregate, err := vs.getSyncAggregate(ctx, slots.PrevSlot(blk.Block().Slot()), blk.Block().ParentRoot(), headState)
2930
if err != nil {
3031
log.WithError(err).Error("Could not get sync aggregate")
3132
emptySig := [96]byte{0xC0}
@@ -47,7 +48,7 @@ func (vs *Server) setSyncAggregate(ctx context.Context, blk interfaces.SignedBea
4748

4849
// getSyncAggregate retrieves the sync contributions from the pool to construct the sync aggregate object.
4950
// The contributions are filtered based on matching of the input root and slot then profitability.
50-
func (vs *Server) getSyncAggregate(ctx context.Context, slot primitives.Slot, root [32]byte) (*ethpb.SyncAggregate, error) {
51+
func (vs *Server) getSyncAggregate(ctx context.Context, slot primitives.Slot, root [32]byte, headState state.BeaconState) (*ethpb.SyncAggregate, error) {
5152
_, span := trace.StartSpan(ctx, "ProposerServer.getSyncAggregate")
5253
defer span.End()
5354

@@ -62,7 +63,7 @@ func (vs *Server) getSyncAggregate(ctx context.Context, slot primitives.Slot, ro
6263
// Contributions have to match the input root
6364
proposerContributions := proposerSyncContributions(poolContributions).filterByBlockRoot(root)
6465

65-
aggregatedContributions, err := vs.aggregatedSyncCommitteeMessages(ctx, slot, root, poolContributions)
66+
aggregatedContributions, err := vs.aggregatedSyncCommitteeMessages(ctx, slot, root, poolContributions, headState)
6667
if err != nil {
6768
return nil, errors.Wrap(err, "could not get aggregated sync committee messages")
6869
}
@@ -123,6 +124,7 @@ func (vs *Server) aggregatedSyncCommitteeMessages(
123124
slot primitives.Slot,
124125
root [32]byte,
125126
poolContributions []*ethpb.SyncCommitteeContribution,
127+
st state.BeaconState,
126128
) ([]*ethpb.SyncCommitteeContribution, error) {
127129
subcommitteeCount := params.BeaconConfig().SyncCommitteeSubnetCount
128130
subcommitteeSize := params.BeaconConfig().SyncCommitteeSize / subcommitteeCount
@@ -146,10 +148,7 @@ func (vs *Server) aggregatedSyncCommitteeMessages(
146148
messageSigs = append(messageSigs, msg.Signature)
147149
}
148150
}
149-
st, err := vs.HeadFetcher.HeadState(ctx)
150-
if err != nil {
151-
return nil, errors.Wrap(err, "could not get head state")
152-
}
151+
153152
positions, err := helpers.CurrentPeriodPositions(st, messageIndices)
154153
if err != nil {
155154
return nil, errors.Wrap(err, "could not get sync committee positions")

beacon-chain/rpc/prysm/v1alpha1/validator/proposer_altair_test.go

Lines changed: 98 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
mockSync "github.com/OffchainLabs/prysm/v6/beacon-chain/sync/initial-sync/testing"
1010
"github.com/OffchainLabs/prysm/v6/config/params"
1111
"github.com/OffchainLabs/prysm/v6/consensus-types/blocks"
12+
"github.com/OffchainLabs/prysm/v6/consensus-types/primitives"
1213
"github.com/OffchainLabs/prysm/v6/crypto/bls"
1314
"github.com/OffchainLabs/prysm/v6/encoding/bytesutil"
1415
ethpb "github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1"
@@ -51,15 +52,15 @@ func TestProposer_GetSyncAggregate_OK(t *testing.T) {
5152
require.NoError(t, proposerServer.SyncCommitteePool.SaveSyncCommitteeContribution(cont))
5253
}
5354

54-
aggregate, err := proposerServer.getSyncAggregate(t.Context(), 1, bytesutil.ToBytes32(conts[0].BlockRoot))
55+
aggregate, err := proposerServer.getSyncAggregate(t.Context(), 1, bytesutil.ToBytes32(conts[0].BlockRoot), st)
5556
require.NoError(t, err)
5657
require.DeepEqual(t, bitfield.Bitvector32{0xf, 0xf, 0xf, 0xf}, aggregate.SyncCommitteeBits)
5758

58-
aggregate, err = proposerServer.getSyncAggregate(t.Context(), 2, bytesutil.ToBytes32(conts[0].BlockRoot))
59+
aggregate, err = proposerServer.getSyncAggregate(t.Context(), 2, bytesutil.ToBytes32(conts[0].BlockRoot), st)
5960
require.NoError(t, err)
6061
require.DeepEqual(t, bitfield.Bitvector32{0xaa, 0xaa, 0xaa, 0xaa}, aggregate.SyncCommitteeBits)
6162

62-
aggregate, err = proposerServer.getSyncAggregate(t.Context(), 3, bytesutil.ToBytes32(conts[0].BlockRoot))
63+
aggregate, err = proposerServer.getSyncAggregate(t.Context(), 3, bytesutil.ToBytes32(conts[0].BlockRoot), st)
6364
require.NoError(t, err)
6465
require.DeepEqual(t, bitfield.NewBitvector32(), aggregate.SyncCommitteeBits)
6566
}
@@ -68,7 +69,7 @@ func TestServer_SetSyncAggregate_EmptyCase(t *testing.T) {
6869
b, err := blocks.NewSignedBeaconBlock(util.NewBeaconBlockAltair())
6970
require.NoError(t, err)
7071
s := &Server{} // Sever is not initialized with sync committee pool.
71-
s.setSyncAggregate(t.Context(), b)
72+
s.setSyncAggregate(t.Context(), b, nil)
7273
agg, err := b.Block().Body().SyncAggregate()
7374
require.NoError(t, err)
7475

@@ -138,7 +139,7 @@ func TestProposer_GetSyncAggregate_IncludesSyncCommitteeMessages(t *testing.T) {
138139
}
139140

140141
// The final sync aggregates must have indexes [0,1,2,3] set for both subcommittees
141-
sa, err := proposerServer.getSyncAggregate(t.Context(), 1, r)
142+
sa, err := proposerServer.getSyncAggregate(t.Context(), 1, r, st)
142143
require.NoError(t, err)
143144
assert.Equal(t, true, sa.SyncCommitteeBits.BitAt(0))
144145
assert.Equal(t, true, sa.SyncCommitteeBits.BitAt(1))
@@ -194,8 +195,99 @@ func Test_aggregatedSyncCommitteeMessages_NoIntersectionWithPoolContributions(t
194195
BlockRoot: r[:],
195196
}
196197

197-
aggregated, err := proposerServer.aggregatedSyncCommitteeMessages(t.Context(), 1, r, []*ethpb.SyncCommitteeContribution{cont})
198+
aggregated, err := proposerServer.aggregatedSyncCommitteeMessages(t.Context(), 1, r, []*ethpb.SyncCommitteeContribution{cont}, st)
198199
require.NoError(t, err)
199200
require.Equal(t, 1, len(aggregated))
200201
assert.Equal(t, false, aggregated[0].AggregationBits.BitAt(3))
201202
}
203+
204+
func TestGetSyncAggregate_CorrectStateAtSyncCommitteePeriodBoundary(t *testing.T) {
205+
helpers.ClearCache()
206+
syncPeriodBoundaryEpoch := primitives.Epoch(274176) // Real epoch from the bug report
207+
slotsPerEpoch := params.BeaconConfig().SlotsPerEpoch
208+
209+
preEpochState, keys := util.DeterministicGenesisStateAltair(t, 100)
210+
require.NoError(t, preEpochState.SetSlot(primitives.Slot(syncPeriodBoundaryEpoch)*slotsPerEpoch-1)) // Last slot of previous epoch
211+
212+
postEpochState := preEpochState.Copy()
213+
require.NoError(t, postEpochState.SetSlot(primitives.Slot(syncPeriodBoundaryEpoch)*slotsPerEpoch+2)) // After 2 missed slots
214+
215+
oldCommittee := &ethpb.SyncCommittee{
216+
Pubkeys: make([][]byte, params.BeaconConfig().SyncCommitteeSize),
217+
}
218+
newCommittee := &ethpb.SyncCommittee{
219+
Pubkeys: make([][]byte, params.BeaconConfig().SyncCommitteeSize),
220+
}
221+
222+
for i := 0; i < int(params.BeaconConfig().SyncCommitteeSize); i++ {
223+
if i < len(keys) {
224+
oldCommittee.Pubkeys[i] = keys[i%len(keys)].PublicKey().Marshal()
225+
// Use different keys for new committee to simulate rotation
226+
newCommittee.Pubkeys[i] = keys[(i+10)%len(keys)].PublicKey().Marshal()
227+
}
228+
}
229+
230+
require.NoError(t, preEpochState.SetCurrentSyncCommittee(oldCommittee))
231+
require.NoError(t, postEpochState.SetCurrentSyncCommittee(newCommittee))
232+
233+
mockChainService := &chainmock.ChainService{
234+
State: postEpochState,
235+
}
236+
237+
proposerServer := &Server{
238+
HeadFetcher: mockChainService,
239+
SyncChecker: &mockSync.Sync{IsSyncing: false},
240+
SyncCommitteePool: synccommittee.NewStore(),
241+
}
242+
243+
slot := primitives.Slot(syncPeriodBoundaryEpoch)*slotsPerEpoch + 1 // First slot of new epoch
244+
blockRoot := [32]byte{0x01, 0x02, 0x03}
245+
246+
msg1 := &ethpb.SyncCommitteeMessage{
247+
Slot: slot,
248+
BlockRoot: blockRoot[:],
249+
ValidatorIndex: 0, // This validator is in position 0 of OLD committee
250+
Signature: bls.NewAggregateSignature().Marshal(),
251+
}
252+
msg2 := &ethpb.SyncCommitteeMessage{
253+
Slot: slot,
254+
BlockRoot: blockRoot[:],
255+
ValidatorIndex: 1, // This validator is in position 1 of OLD committee
256+
Signature: bls.NewAggregateSignature().Marshal(),
257+
}
258+
259+
require.NoError(t, proposerServer.SyncCommitteePool.SaveSyncCommitteeMessage(msg1))
260+
require.NoError(t, proposerServer.SyncCommitteePool.SaveSyncCommitteeMessage(msg2))
261+
262+
aggregateWrongState, err := proposerServer.getSyncAggregate(t.Context(), slot, blockRoot, postEpochState)
263+
require.NoError(t, err)
264+
265+
aggregateCorrectState, err := proposerServer.getSyncAggregate(t.Context(), slot, blockRoot, preEpochState)
266+
require.NoError(t, err)
267+
268+
wrongStateBits := bitfield.Bitlist(aggregateWrongState.SyncCommitteeBits)
269+
correctStateBits := bitfield.Bitlist(aggregateCorrectState.SyncCommitteeBits)
270+
271+
wrongStateHasValidators := false
272+
correctStateHasValidators := false
273+
274+
for i := 0; i < len(wrongStateBits); i++ {
275+
if wrongStateBits[i] != 0 {
276+
wrongStateHasValidators = true
277+
break
278+
}
279+
}
280+
281+
for i := 0; i < len(correctStateBits); i++ {
282+
if correctStateBits[i] != 0 {
283+
correctStateHasValidators = true
284+
break
285+
}
286+
}
287+
288+
assert.Equal(t, true, correctStateHasValidators, "Correct state should include validators that sent messages")
289+
assert.Equal(t, false, wrongStateHasValidators, "Wrong state should not find validators in incorrect sync committee")
290+
291+
t.Logf("Wrong state aggregate bits: %x (has validators: %v)", wrongStateBits, wrongStateHasValidators)
292+
t.Logf("Correct state aggregate bits: %x (has validators: %v)", correctStateBits, correctStateHasValidators)
293+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Fixed
2+
3+
- Sync committee uses correct state to calculate position

0 commit comments

Comments
 (0)