From 682677c2e12e07b9cff636fa5813a890e2aaa00d Mon Sep 17 00:00:00 2001 From: shyam-patel-kira Date: Fri, 6 Dec 2024 18:13:20 +0530 Subject: [PATCH 01/14] Add equivocation detection logic; broadcast slashing immediately on equivocation --- beacon-chain/blockchain/process_block.go | 28 +++++++++++++ beacon-chain/blockchain/service.go | 50 ++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/beacon-chain/blockchain/process_block.go b/beacon-chain/blockchain/process_block.go index c816c2388157..1a9ab7b9bfca 100644 --- a/beacon-chain/blockchain/process_block.go +++ b/beacon-chain/blockchain/process_block.go @@ -72,6 +72,26 @@ func (s *Service) postBlockProcess(cfg *postBlockProcessConfig) error { defer reportProcessingTime(startTime) defer reportAttestationInclusion(cfg.roblock.Block()) + // Check for equivocation before inserting into fork choice + slashing, slashingErr := s.detectEquivocatingBlock(cfg.ctx, cfg.roblock) + if slashingErr != nil { + return errors.Wrap(slashingErr, "could not detect equivocating block") + } + + if slashing != nil { + // Immediately broadcast the slashing + if err := s.broadcastProposerSlashing(cfg.ctx, slashing); err != nil { + log.WithError(err).Error("Could not broadcast proposer slashing") + // Don't return error here, we still want to continue processing + } + + // Also insert into slashing pool + if err := s.cfg.SlashingPool.InsertProposerSlashing(cfg.ctx, cfg.postState, slashing); err != nil { + log.WithError(err).Error("Could not insert proposer slashing into pool") + // Don't return error here, we still want to continue processing + } + } + err := s.cfg.ForkChoiceStore.InsertNode(ctx, cfg.postState, cfg.roblock) if err != nil { // Do not use parent context in the event it deadlined @@ -704,3 +724,11 @@ func (s *Service) rollbackBlock(ctx context.Context, blockRoot [32]byte) { log.WithError(err).Errorf("Could not delete block with block root %#x", blockRoot) } } + +func (s *Service) broadcastProposerSlashing(ctx context.Context, slashing *ethpb.ProposerSlashing) error { + if features.Get().DisableBroadcastSlashings { + return nil + } + + return s.cfg.P2p.Broadcast(ctx, slashing) +} diff --git a/beacon-chain/blockchain/service.go b/beacon-chain/blockchain/service.go index c984a2f79750..f832b0d86b62 100644 --- a/beacon-chain/blockchain/service.go +++ b/beacon-chain/blockchain/service.go @@ -583,3 +583,53 @@ func spawnCountdownIfPreGenesis(ctx context.Context, genesisTime time.Time, db d } go slots.CountdownToGenesis(ctx, genesisTime, uint64(gState.NumValidators()), gRoot) } + +func (s *Service) detectEquivocatingBlock(ctx context.Context, block interfaces.ReadOnlySignedBeaconBlock) (*ethpb.ProposerSlashing, error) { + // Get the incoming block's header + header1, err := block.Header() + if err != nil { + return nil, errors.Wrap(err, "could not get header from incoming block") + } + + s.headLock.RLock() + headBlock, err := s.headBlock() + if err != nil { + s.headLock.RUnlock() + return nil, errors.Wrap(err, "could not get head block") + } + s.headLock.RUnlock() + + // Get the head block's header + header2, err := headBlock.Header() + if err != nil { + return nil, errors.Wrap(err, "could not get header from head block") + } + + // Check for equivocation: + // 1. Same slot + // 2. Same proposer index + // 3. Different signing roots + if header1.Header.Slot == header2.Header.Slot && + header1.Header.ProposerIndex == header2.Header.ProposerIndex { + + header1Root, err := header1.Header.HashTreeRoot() + if err != nil { + return nil, errors.Wrap(err, "could not hash header 1") + } + + header2Root, err := header2.Header.HashTreeRoot() + if err != nil { + return nil, errors.Wrap(err, "could not hash header 2") + } + + // Different header roots means equivocation + if header1Root != header2Root { + return ðpb.ProposerSlashing{ + Header_1: header1, + Header_2: header2, + }, nil + } + } + + return nil, nil +} From 4861bede2df9e60cd47d1612a91fb0492d1e9ea8 Mon Sep 17 00:00:00 2001 From: shyam-patel-kira Date: Fri, 6 Dec 2024 23:07:17 +0530 Subject: [PATCH 02/14] nit: comments --- beacon-chain/blockchain/process_block.go | 3 --- beacon-chain/blockchain/service.go | 3 --- 2 files changed, 6 deletions(-) diff --git a/beacon-chain/blockchain/process_block.go b/beacon-chain/blockchain/process_block.go index 1a9ab7b9bfca..8d4525ba79fe 100644 --- a/beacon-chain/blockchain/process_block.go +++ b/beacon-chain/blockchain/process_block.go @@ -79,16 +79,13 @@ func (s *Service) postBlockProcess(cfg *postBlockProcessConfig) error { } if slashing != nil { - // Immediately broadcast the slashing if err := s.broadcastProposerSlashing(cfg.ctx, slashing); err != nil { log.WithError(err).Error("Could not broadcast proposer slashing") - // Don't return error here, we still want to continue processing } // Also insert into slashing pool if err := s.cfg.SlashingPool.InsertProposerSlashing(cfg.ctx, cfg.postState, slashing); err != nil { log.WithError(err).Error("Could not insert proposer slashing into pool") - // Don't return error here, we still want to continue processing } } diff --git a/beacon-chain/blockchain/service.go b/beacon-chain/blockchain/service.go index f832b0d86b62..567b364e8182 100644 --- a/beacon-chain/blockchain/service.go +++ b/beacon-chain/blockchain/service.go @@ -606,9 +606,6 @@ func (s *Service) detectEquivocatingBlock(ctx context.Context, block interfaces. } // Check for equivocation: - // 1. Same slot - // 2. Same proposer index - // 3. Different signing roots if header1.Header.Slot == header2.Header.Slot && header1.Header.ProposerIndex == header2.Header.ProposerIndex { From 8d7270e8635e8bb23787dc1c91623aa79c9567eb Mon Sep 17 00:00:00 2001 From: Shyam Patel Date: Sat, 25 Jan 2025 13:07:59 -0800 Subject: [PATCH 03/14] move equivocation detection to validateBeaconBlockPubSub --- beacon-chain/blockchain/process_block.go | 17 --------- beacon-chain/blockchain/service.go | 47 ------------------------ 2 files changed, 64 deletions(-) diff --git a/beacon-chain/blockchain/process_block.go b/beacon-chain/blockchain/process_block.go index 4b345ae67fb8..a937075f6060 100644 --- a/beacon-chain/blockchain/process_block.go +++ b/beacon-chain/blockchain/process_block.go @@ -75,23 +75,6 @@ func (s *Service) postBlockProcess(cfg *postBlockProcessConfig) error { defer reportProcessingTime(startTime) defer reportAttestationInclusion(cfg.roblock.Block()) - // Check for equivocation before inserting into fork choice - slashing, slashingErr := s.detectEquivocatingBlock(cfg.ctx, cfg.roblock) - if slashingErr != nil { - return errors.Wrap(slashingErr, "could not detect equivocating block") - } - - if slashing != nil { - if err := s.broadcastProposerSlashing(cfg.ctx, slashing); err != nil { - log.WithError(err).Error("Could not broadcast proposer slashing") - } - - // Also insert into slashing pool - if err := s.cfg.SlashingPool.InsertProposerSlashing(cfg.ctx, cfg.postState, slashing); err != nil { - log.WithError(err).Error("Could not insert proposer slashing into pool") - } - } - err := s.cfg.ForkChoiceStore.InsertNode(ctx, cfg.postState, cfg.roblock) if err != nil { // Do not use parent context in the event it deadlined diff --git a/beacon-chain/blockchain/service.go b/beacon-chain/blockchain/service.go index a3b63fbcb611..dbd7685714bd 100644 --- a/beacon-chain/blockchain/service.go +++ b/beacon-chain/blockchain/service.go @@ -586,50 +586,3 @@ func spawnCountdownIfPreGenesis(ctx context.Context, genesisTime time.Time, db d } go slots.CountdownToGenesis(ctx, genesisTime, uint64(gState.NumValidators()), gRoot) } - -func (s *Service) detectEquivocatingBlock(ctx context.Context, block interfaces.ReadOnlySignedBeaconBlock) (*ethpb.ProposerSlashing, error) { - // Get the incoming block's header - header1, err := block.Header() - if err != nil { - return nil, errors.Wrap(err, "could not get header from incoming block") - } - - s.headLock.RLock() - headBlock, err := s.headBlock() - if err != nil { - s.headLock.RUnlock() - return nil, errors.Wrap(err, "could not get head block") - } - s.headLock.RUnlock() - - // Get the head block's header - header2, err := headBlock.Header() - if err != nil { - return nil, errors.Wrap(err, "could not get header from head block") - } - - // Check for equivocation: - if header1.Header.Slot == header2.Header.Slot && - header1.Header.ProposerIndex == header2.Header.ProposerIndex { - - header1Root, err := header1.Header.HashTreeRoot() - if err != nil { - return nil, errors.Wrap(err, "could not hash header 1") - } - - header2Root, err := header2.Header.HashTreeRoot() - if err != nil { - return nil, errors.Wrap(err, "could not hash header 2") - } - - // Different header roots means equivocation - if header1Root != header2Root { - return ðpb.ProposerSlashing{ - Header_1: header1, - Header_2: header2, - }, nil - } - } - - return nil, nil -} From f14cacb54aa372c59e3ef100656d15bde3448fdd Mon Sep 17 00:00:00 2001 From: Shyam Patel Date: Sat, 25 Jan 2025 13:08:34 -0800 Subject: [PATCH 04/14] include broadcasting logic within the helper function --- beacon-chain/sync/validate_beacon_blocks.go | 75 +++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/beacon-chain/sync/validate_beacon_blocks.go b/beacon-chain/sync/validate_beacon_blocks.go index 0e8085be9391..4f454eba8113 100644 --- a/beacon-chain/sync/validate_beacon_blocks.go +++ b/beacon-chain/sync/validate_beacon_blocks.go @@ -23,6 +23,7 @@ import ( "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" "github.com/prysmaticlabs/prysm/v5/monitoring/tracing" "github.com/prysmaticlabs/prysm/v5/monitoring/tracing/trace" + ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v5/runtime/version" prysmTime "github.com/prysmaticlabs/prysm/v5/time" "github.com/prysmaticlabs/prysm/v5/time/slots" @@ -99,6 +100,10 @@ func (s *Service) validateBeaconBlockPubSub(ctx context.Context, pid peer.ID, ms // Verify the block is the first block received for the proposer for the slot. if s.hasSeenBlockIndexSlot(blk.Block().Slot(), blk.Block().ProposerIndex()) { + // Attempt to detect and broadcast equivocation before ignoring + if err := s.detectAndBroadcastEquivocation(ctx, blk); err != nil { + log.WithError(err).Debug("Could not detect/broadcast equivocation") + } return pubsub.ValidationIgnore, nil } @@ -456,3 +461,73 @@ func getBlockFields(b interfaces.ReadOnlySignedBeaconBlock) logrus.Fields { "version": b.Block().Version(), } } + +func (s *Service) detectAndBroadcastEquivocation(ctx context.Context, blk interfaces.ReadOnlySignedBeaconBlock) error { + // If we've seen this proposer/slot combo before, it means this is an equivocating block + slot := blk.Block().Slot() + proposerIndex := blk.Block().ProposerIndex() + + // Get the head state for block verification + headState, err := s.cfg.chain.HeadStateReadOnly(ctx) + if err != nil { + return err + } + + // Compare signatures since we haven't computed roots yet + sig1 := blk.Signature() + + s.seenBlockLock.RLock() + // Create a unique cache key by combining slot and proposer index + // Convert slot and proposer index to 32 bytes and combine them into a single byte slice + b := append(bytesutil.Bytes32(uint64(slot)), bytesutil.Bytes32(uint64(proposerIndex))...) + existingBlock, seen := s.seenBlockCache.Get(string(b)) + s.seenBlockLock.RUnlock() + + if !seen { + return nil + } + + // Type assert to get the existing block + existingSignedBlock, ok := existingBlock.(interfaces.ReadOnlySignedBeaconBlock) + if !ok { + return errors.New("invalid type assertion for existing block") + } + + sig2 := existingSignedBlock.Signature() + + // Different signatures indicate equivocation + if sig1 != sig2 { + header1, err := blk.Header() + if err != nil { + return err + } + header2, err := existingSignedBlock.Header() + if err != nil { + return err + } + + slashing := ðpb.ProposerSlashing{ + Header_1: header1, + Header_2: header2, + } + + // Verify the proposer slashing is valid + if err := blocks.VerifyProposerSlashing(headState, slashing); err != nil { + return err + } + + // Broadcast if verification passes + if !features.Get().DisableBroadcastSlashings { + if err := s.cfg.p2p.Broadcast(ctx, slashing); err != nil { + return errors.Wrap(err, "could not broadcast slashing object") + } + } + + // Also insert into slashing pool + if err := s.cfg.slashingPool.InsertProposerSlashing(ctx, headState, slashing); err != nil { + return errors.Wrap(err, "could not insert proposer slashing into pool") + } + } + + return nil +} From a3797e6e268f883442f6177143aa6d554cd3523e Mon Sep 17 00:00:00 2001 From: Shyam Patel Date: Sat, 25 Jan 2025 16:53:55 -0800 Subject: [PATCH 05/14] fix lint --- beacon-chain/blockchain/process_block.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/beacon-chain/blockchain/process_block.go b/beacon-chain/blockchain/process_block.go index a937075f6060..646323c0c51a 100644 --- a/beacon-chain/blockchain/process_block.go +++ b/beacon-chain/blockchain/process_block.go @@ -715,11 +715,3 @@ func (s *Service) rollbackBlock(ctx context.Context, blockRoot [32]byte) { log.WithError(err).Errorf("Could not delete block with block root %#x", blockRoot) } } - -func (s *Service) broadcastProposerSlashing(ctx context.Context, slashing *ethpb.ProposerSlashing) error { - if features.Get().DisableBroadcastSlashings { - return nil - } - - return s.cfg.P2p.Broadcast(ctx, slashing) -} From 1bfc38e37616f7eda8e5fbaa46a3a8d6961266ee Mon Sep 17 00:00:00 2001 From: Shyam Patel Date: Sat, 25 Jan 2025 17:15:59 -0800 Subject: [PATCH 06/14] Add unit tests for equivocation detection --- .../sync/validate_beacon_blocks_test.go | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/beacon-chain/sync/validate_beacon_blocks_test.go b/beacon-chain/sync/validate_beacon_blocks_test.go index 353cf817b8d2..4a775bba257d 100644 --- a/beacon-chain/sync/validate_beacon_blocks_test.go +++ b/beacon-chain/sync/validate_beacon_blocks_test.go @@ -21,6 +21,7 @@ import ( dbtest "github.com/prysmaticlabs/prysm/v5/beacon-chain/db/testing" doublylinkedtree "github.com/prysmaticlabs/prysm/v5/beacon-chain/forkchoice/doubly-linked-tree" "github.com/prysmaticlabs/prysm/v5/beacon-chain/operations/attestations" + slashingsmock "github.com/prysmaticlabs/prysm/v5/beacon-chain/operations/slashings/mock" "github.com/prysmaticlabs/prysm/v5/beacon-chain/p2p" p2ptest "github.com/prysmaticlabs/prysm/v5/beacon-chain/p2p/testing" "github.com/prysmaticlabs/prysm/v5/beacon-chain/startup" @@ -1495,3 +1496,136 @@ func Test_validateDenebBeaconBlock(t *testing.T) { require.NoError(t, err) require.ErrorIs(t, validateDenebBeaconBlock(bdb.Block()), errRejectCommitmentLen) } + +func TestDetectAndBroadcastEquivocation_NoEquivocation(t *testing.T) { + p := p2ptest.NewTestP2P(t) + ctx := context.Background() + beaconState, privKeys := util.DeterministicGenesisState(t, 100) + + block := util.NewBeaconBlock() + block.Block.Slot = 1 + block.Block.ProposerIndex = 0 + + sig, err := signing.ComputeDomainAndSign(beaconState, 0, block.Block, params.BeaconConfig().DomainBeaconProposer, privKeys[0]) + require.NoError(t, err) + block.Signature = sig + + chainService := &mock.ChainService{ + State: beaconState, + Genesis: time.Now(), + } + + slashingPool := &slashingsmock.PoolMock{} + r := &Service{ + cfg: &config{ + p2p: p, + chain: chainService, + slashingPool: slashingPool, + }, + seenBlockCache: lruwrpr.New(10), + } + + b, err := blocks.NewSignedBeaconBlock(block) + require.NoError(t, err) + + err = r.detectAndBroadcastEquivocation(ctx, b) + assert.NoError(t, err) + assert.Equal(t, 0, len(slashingPool.PendingPropSlashings), "Expected no slashings to be inserted") +} + +func TestDetectAndBroadcastEquivocation_EquivocationDetected(t *testing.T) { + p := p2ptest.NewTestP2P(t) + ctx := context.Background() + beaconState, privKeys := util.DeterministicGenesisState(t, 100) + + // Create first block + block1 := util.NewBeaconBlock() + block1.Block.Slot = 1 + block1.Block.ProposerIndex = 0 + block1.Block.ParentRoot = bytesutil.PadTo([]byte("parent1"), 32) + sig1, err := signing.ComputeDomainAndSign(beaconState, 0, block1.Block, params.BeaconConfig().DomainBeaconProposer, privKeys[0]) + require.NoError(t, err) + block1.Signature = sig1 + + // Create second block with same slot/proposer but different contents + block2 := util.NewBeaconBlock() + block2.Block.Slot = 1 + block2.Block.ProposerIndex = 0 + block2.Block.ParentRoot = bytesutil.PadTo([]byte("parent2"), 32) + sig2, err := signing.ComputeDomainAndSign(beaconState, 0, block2.Block, params.BeaconConfig().DomainBeaconProposer, privKeys[0]) + require.NoError(t, err) + block2.Signature = sig2 + + slashingPool := &slashingsmock.PoolMock{} + chainService := &mock.ChainService{ + State: beaconState, + Genesis: time.Now(), + } + r := &Service{ + cfg: &config{ + p2p: p, + chain: chainService, + slashingPool: slashingPool, + }, + seenBlockCache: lruwrpr.New(10), + } + + // Cache first block + r.setSeenBlockIndexSlot(block1.Block.Slot, block1.Block.ProposerIndex) + b := append(bytesutil.Bytes32(uint64(block1.Block.Slot)), bytesutil.Bytes32(uint64(block1.Block.ProposerIndex))...) + signedBlock1, err := blocks.NewSignedBeaconBlock(block1) + require.NoError(t, err) + r.seenBlockCache.Add(string(b), signedBlock1) + + // Process second block which should trigger equivocation + signedBlock2, err := blocks.NewSignedBeaconBlock(block2) + require.NoError(t, err) + err = r.detectAndBroadcastEquivocation(ctx, signedBlock2) + require.NoError(t, err) + + // Verify slashing was inserted + require.Equal(t, 1, len(slashingPool.PendingPropSlashings), "Expected a slashing to be inserted") + slashing := slashingPool.PendingPropSlashings[0] + assert.Equal(t, primitives.ValidatorIndex(0), slashing.Header_1.Header.ProposerIndex, "Wrong proposer index") + assert.Equal(t, primitives.Slot(1), slashing.Header_1.Header.Slot, "Wrong slot") +} + +func TestDetectAndBroadcastEquivocation_SameSignature(t *testing.T) { + p := p2ptest.NewTestP2P(t) + ctx := context.Background() + beaconState, privKeys := util.DeterministicGenesisState(t, 100) + + // Create a block + block := util.NewBeaconBlock() + block.Block.Slot = 1 + block.Block.ProposerIndex = 0 + sig, err := signing.ComputeDomainAndSign(beaconState, 0, block.Block, params.BeaconConfig().DomainBeaconProposer, privKeys[0]) + require.NoError(t, err) + block.Signature = sig + + slashingPool := &slashingsmock.PoolMock{} + chainService := &mock.ChainService{ + State: beaconState, + Genesis: time.Now(), + } + r := &Service{ + cfg: &config{ + p2p: p, + chain: chainService, + slashingPool: slashingPool, + }, + seenBlockCache: lruwrpr.New(10), + } + + // Cache the block + r.setSeenBlockIndexSlot(block.Block.Slot, block.Block.ProposerIndex) + b := append(bytesutil.Bytes32(uint64(block.Block.Slot)), bytesutil.Bytes32(uint64(block.Block.ProposerIndex))...) + signedBlock, err := blocks.NewSignedBeaconBlock(block) + require.NoError(t, err) + r.seenBlockCache.Add(string(b), signedBlock) + + // Try to process the same block again + err = r.detectAndBroadcastEquivocation(ctx, signedBlock) + require.NoError(t, err) + assert.Equal(t, 0, len(slashingPool.PendingPropSlashings), "Expected no slashings for same signature") +} From e915e428918237b51c5c350a569b8f2690f1f341 Mon Sep 17 00:00:00 2001 From: Shyam Patel Date: Sat, 25 Jan 2025 18:20:18 -0800 Subject: [PATCH 07/14] remove comment that are not required --- beacon-chain/sync/BUILD.bazel | 1 + beacon-chain/sync/validate_beacon_blocks.go | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/beacon-chain/sync/BUILD.bazel b/beacon-chain/sync/BUILD.bazel index 85a0317be3eb..77f746f9a814 100644 --- a/beacon-chain/sync/BUILD.bazel +++ b/beacon-chain/sync/BUILD.bazel @@ -211,6 +211,7 @@ go_test( "//beacon-chain/operations/attestations:go_default_library", "//beacon-chain/operations/blstoexec:go_default_library", "//beacon-chain/operations/slashings:go_default_library", + "//beacon-chain/operations/slashings/mock:go_default_library", "//beacon-chain/p2p:go_default_library", "//beacon-chain/p2p/encoder:go_default_library", "//beacon-chain/p2p/peers:go_default_library", diff --git a/beacon-chain/sync/validate_beacon_blocks.go b/beacon-chain/sync/validate_beacon_blocks.go index 4f454eba8113..90dfa8bfb679 100644 --- a/beacon-chain/sync/validate_beacon_blocks.go +++ b/beacon-chain/sync/validate_beacon_blocks.go @@ -473,17 +473,15 @@ func (s *Service) detectAndBroadcastEquivocation(ctx context.Context, blk interf return err } - // Compare signatures since we haven't computed roots yet sig1 := blk.Signature() - s.seenBlockLock.RLock() // Create a unique cache key by combining slot and proposer index // Convert slot and proposer index to 32 bytes and combine them into a single byte slice b := append(bytesutil.Bytes32(uint64(slot)), bytesutil.Bytes32(uint64(proposerIndex))...) - existingBlock, seen := s.seenBlockCache.Get(string(b)) + existingBlock, isSeen := s.seenBlockCache.Get(string(b)) s.seenBlockLock.RUnlock() - if !seen { + if !isSeen { return nil } From b8989d3c2e0de03aa1e7fc4bd819a61797fcc0ed Mon Sep 17 00:00:00 2001 From: Shyam Patel Date: Sun, 26 Jan 2025 01:47:04 -0800 Subject: [PATCH 08/14] Add changelog file --- changelog/kira_broadcast_slashings.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelog/kira_broadcast_slashings.md diff --git a/changelog/kira_broadcast_slashings.md b/changelog/kira_broadcast_slashings.md new file mode 100644 index 000000000000..bb529477652a --- /dev/null +++ b/changelog/kira_broadcast_slashings.md @@ -0,0 +1,5 @@ +### Added +- Added broadcast of slashing events (proposer and attester slashings) upon detection of equivocation. + +### Updated +- Updated attestation pool logic to prioritize slashings over regular attestations during block production. From b9b9cde92d577bea1e441296092db1ac204ce8a2 Mon Sep 17 00:00:00 2001 From: Shyam Patel Date: Wed, 29 Jan 2025 17:00:47 -0800 Subject: [PATCH 09/14] Add descriptive comment for detectAndBroadcastEquivocation --- beacon-chain/sync/validate_beacon_blocks.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/beacon-chain/sync/validate_beacon_blocks.go b/beacon-chain/sync/validate_beacon_blocks.go index 90dfa8bfb679..3dce7a99a88f 100644 --- a/beacon-chain/sync/validate_beacon_blocks.go +++ b/beacon-chain/sync/validate_beacon_blocks.go @@ -462,6 +462,10 @@ func getBlockFields(b interfaces.ReadOnlySignedBeaconBlock) logrus.Fields { } } +// detectAndBroadcastEquivocation checks if the given block is an equivocating block (i.e. a different block +// with the same slot and proposer index but different signature than one we've already seen). If an equivocation +// is detected, it creates a proposer slashing object and broadcasts it to the network before adding it to the +// slashing pool. func (s *Service) detectAndBroadcastEquivocation(ctx context.Context, blk interfaces.ReadOnlySignedBeaconBlock) error { // If we've seen this proposer/slot combo before, it means this is an equivocating block slot := blk.Block().Slot() From 462f569d1ee15ccf918175e4eefdba6f535de664 Mon Sep 17 00:00:00 2001 From: Shyam Patel Date: Mon, 3 Feb 2025 15:38:11 -0800 Subject: [PATCH 10/14] use head block instead of block cache for equivocation detection --- beacon-chain/sync/validate_beacon_blocks.go | 88 +++++----- .../sync/validate_beacon_blocks_test.go | 153 ++++++++++++++---- 2 files changed, 159 insertions(+), 82 deletions(-) diff --git a/beacon-chain/sync/validate_beacon_blocks.go b/beacon-chain/sync/validate_beacon_blocks.go index 3dce7a99a88f..75606b8c694b 100644 --- a/beacon-chain/sync/validate_beacon_blocks.go +++ b/beacon-chain/sync/validate_beacon_blocks.go @@ -462,73 +462,67 @@ func getBlockFields(b interfaces.ReadOnlySignedBeaconBlock) logrus.Fields { } } -// detectAndBroadcastEquivocation checks if the given block is an equivocating block (i.e. a different block -// with the same slot and proposer index but different signature than one we've already seen). If an equivocation -// is detected, it creates a proposer slashing object and broadcasts it to the network before adding it to the -// slashing pool. +// detectAndBroadcastEquivocation checks if the given block is an equivocating block by comparing it with +// the head block when a duplicate slot/proposer index is detected. If an equivocation is found, it creates +// and broadcasts a proposer slashing object and adds it to the slashing pool. func (s *Service) detectAndBroadcastEquivocation(ctx context.Context, blk interfaces.ReadOnlySignedBeaconBlock) error { - // If we've seen this proposer/slot combo before, it means this is an equivocating block + // If this proposer/slot combo is seen before, it means this is a potential equivocating block slot := blk.Block().Slot() proposerIndex := blk.Block().ProposerIndex() + if !s.hasSeenBlockIndexSlot(slot, proposerIndex) { + return nil + } + // Get the head state for block verification headState, err := s.cfg.chain.HeadStateReadOnly(ctx) if err != nil { - return err + return errors.Wrap(err, "could not get head state") } + // Compare signatures since they must be different for equivocation sig1 := blk.Signature() - s.seenBlockLock.RLock() - // Create a unique cache key by combining slot and proposer index - // Convert slot and proposer index to 32 bytes and combine them into a single byte slice - b := append(bytesutil.Bytes32(uint64(slot)), bytesutil.Bytes32(uint64(proposerIndex))...) - existingBlock, isSeen := s.seenBlockCache.Get(string(b)) - s.seenBlockLock.RUnlock() + headBlock, err := s.cfg.chain.HeadBlock(ctx) + if err != nil { + return errors.Wrap(err, "could not get head block") + } - if !isSeen { + sig2 := headBlock.Signature() + + // Different signatures indicate equivocation + if sig1 == sig2 { return nil } - // Type assert to get the existing block - existingSignedBlock, ok := existingBlock.(interfaces.ReadOnlySignedBeaconBlock) - if !ok { - return errors.New("invalid type assertion for existing block") + header1, err := blk.Header() + if err != nil { + return errors.Wrap(err, "could not get header from new block") + } + header2, err := headBlock.Header() + if err != nil { + return errors.Wrap(err, "could not get header from head block") } - sig2 := existingSignedBlock.Signature() - - // Different signatures indicate equivocation - if sig1 != sig2 { - header1, err := blk.Header() - if err != nil { - return err - } - header2, err := existingSignedBlock.Header() - if err != nil { - return err - } - - slashing := ðpb.ProposerSlashing{ - Header_1: header1, - Header_2: header2, - } + slashing := ðpb.ProposerSlashing{ + Header_1: header1, + Header_2: header2, + } - // Verify the proposer slashing is valid - if err := blocks.VerifyProposerSlashing(headState, slashing); err != nil { - return err - } + // Verify the proposer slashing is valid + if err := blocks.VerifyProposerSlashing(headState, slashing); err != nil { + return errors.Wrap(err, "could not verify proposer slashing") + } - // Broadcast if verification passes - if !features.Get().DisableBroadcastSlashings { - if err := s.cfg.p2p.Broadcast(ctx, slashing); err != nil { - return errors.Wrap(err, "could not broadcast slashing object") - } + // Broadcast if verification passes + if !features.Get().DisableBroadcastSlashings { + if err := s.cfg.p2p.Broadcast(ctx, slashing); err != nil { + return errors.Wrap(err, "could not broadcast slashing object") } + } - // Also insert into slashing pool - if err := s.cfg.slashingPool.InsertProposerSlashing(ctx, headState, slashing); err != nil { - return errors.Wrap(err, "could not insert proposer slashing into pool") - } + // Also insert into slashing pool + if err := s.cfg.slashingPool.InsertProposerSlashing(ctx, headState, slashing); err != nil { + return errors.Wrap(err, "could not insert proposer slashing into pool") } return nil diff --git a/beacon-chain/sync/validate_beacon_blocks_test.go b/beacon-chain/sync/validate_beacon_blocks_test.go index 4a775bba257d..78a4dafbb013 100644 --- a/beacon-chain/sync/validate_beacon_blocks_test.go +++ b/beacon-chain/sync/validate_beacon_blocks_test.go @@ -1498,6 +1498,7 @@ func Test_validateDenebBeaconBlock(t *testing.T) { } func TestDetectAndBroadcastEquivocation_NoEquivocation(t *testing.T) { + // db := dbtest.SetupDB(t) p := p2ptest.NewTestP2P(t) ctx := context.Background() beaconState, privKeys := util.DeterministicGenesisState(t, 100) @@ -1514,23 +1515,20 @@ func TestDetectAndBroadcastEquivocation_NoEquivocation(t *testing.T) { State: beaconState, Genesis: time.Now(), } - - slashingPool := &slashingsmock.PoolMock{} r := &Service{ cfg: &config{ p2p: p, chain: chainService, - slashingPool: slashingPool, + slashingPool: &slashingsmock.PoolMock{}, }, seenBlockCache: lruwrpr.New(10), } - b, err := blocks.NewSignedBeaconBlock(block) + signedBlock, err := blocks.NewSignedBeaconBlock(block) require.NoError(t, err) - err = r.detectAndBroadcastEquivocation(ctx, b) - assert.NoError(t, err) - assert.Equal(t, 0, len(slashingPool.PendingPropSlashings), "Expected no slashings to be inserted") + err = r.detectAndBroadcastEquivocation(ctx, signedBlock) + require.NoError(t, err) } func TestDetectAndBroadcastEquivocation_EquivocationDetected(t *testing.T) { @@ -1538,28 +1536,32 @@ func TestDetectAndBroadcastEquivocation_EquivocationDetected(t *testing.T) { ctx := context.Background() beaconState, privKeys := util.DeterministicGenesisState(t, 100) - // Create first block - block1 := util.NewBeaconBlock() - block1.Block.Slot = 1 - block1.Block.ProposerIndex = 0 - block1.Block.ParentRoot = bytesutil.PadTo([]byte("parent1"), 32) - sig1, err := signing.ComputeDomainAndSign(beaconState, 0, block1.Block, params.BeaconConfig().DomainBeaconProposer, privKeys[0]) + // Create head block + headBlock := util.NewBeaconBlock() + headBlock.Block.Slot = 1 + headBlock.Block.ProposerIndex = 0 + headBlock.Block.ParentRoot = bytesutil.PadTo([]byte("parent1"), 32) + sig1, err := signing.ComputeDomainAndSign(beaconState, 0, headBlock.Block, params.BeaconConfig().DomainBeaconProposer, privKeys[0]) require.NoError(t, err) - block1.Signature = sig1 + headBlock.Signature = sig1 // Create second block with same slot/proposer but different contents - block2 := util.NewBeaconBlock() - block2.Block.Slot = 1 - block2.Block.ProposerIndex = 0 - block2.Block.ParentRoot = bytesutil.PadTo([]byte("parent2"), 32) - sig2, err := signing.ComputeDomainAndSign(beaconState, 0, block2.Block, params.BeaconConfig().DomainBeaconProposer, privKeys[0]) + newBlock := util.NewBeaconBlock() + newBlock.Block.Slot = 1 + newBlock.Block.ProposerIndex = 0 + newBlock.Block.ParentRoot = bytesutil.PadTo([]byte("parent2"), 32) + sig2, err := signing.ComputeDomainAndSign(beaconState, 0, newBlock.Block, params.BeaconConfig().DomainBeaconProposer, privKeys[0]) + require.NoError(t, err) + newBlock.Signature = sig2 + + signedHeadBlock, err := blocks.NewSignedBeaconBlock(headBlock) require.NoError(t, err) - block2.Signature = sig2 slashingPool := &slashingsmock.PoolMock{} chainService := &mock.ChainService{ State: beaconState, Genesis: time.Now(), + Block: signedHeadBlock, } r := &Service{ cfg: &config{ @@ -1570,17 +1572,13 @@ func TestDetectAndBroadcastEquivocation_EquivocationDetected(t *testing.T) { seenBlockCache: lruwrpr.New(10), } - // Cache first block - r.setSeenBlockIndexSlot(block1.Block.Slot, block1.Block.ProposerIndex) - b := append(bytesutil.Bytes32(uint64(block1.Block.Slot)), bytesutil.Bytes32(uint64(block1.Block.ProposerIndex))...) - signedBlock1, err := blocks.NewSignedBeaconBlock(block1) - require.NoError(t, err) - r.seenBlockCache.Add(string(b), signedBlock1) + // Mark block as seen + r.setSeenBlockIndexSlot(newBlock.Block.Slot, newBlock.Block.ProposerIndex) - // Process second block which should trigger equivocation - signedBlock2, err := blocks.NewSignedBeaconBlock(block2) + signedNewBlock, err := blocks.NewSignedBeaconBlock(newBlock) require.NoError(t, err) - err = r.detectAndBroadcastEquivocation(ctx, signedBlock2) + + err = r.detectAndBroadcastEquivocation(ctx, signedNewBlock) require.NoError(t, err) // Verify slashing was inserted @@ -1595,7 +1593,7 @@ func TestDetectAndBroadcastEquivocation_SameSignature(t *testing.T) { ctx := context.Background() beaconState, privKeys := util.DeterministicGenesisState(t, 100) - // Create a block + // Create block block := util.NewBeaconBlock() block.Block.Slot = 1 block.Block.ProposerIndex = 0 @@ -1603,10 +1601,14 @@ func TestDetectAndBroadcastEquivocation_SameSignature(t *testing.T) { require.NoError(t, err) block.Signature = sig + signedBlock, err := blocks.NewSignedBeaconBlock(block) + require.NoError(t, err) + slashingPool := &slashingsmock.PoolMock{} chainService := &mock.ChainService{ State: beaconState, Genesis: time.Now(), + Block: signedBlock, } r := &Service{ cfg: &config{ @@ -1617,15 +1619,96 @@ func TestDetectAndBroadcastEquivocation_SameSignature(t *testing.T) { seenBlockCache: lruwrpr.New(10), } - // Cache the block + // Mark block as seen r.setSeenBlockIndexSlot(block.Block.Slot, block.Block.ProposerIndex) - b := append(bytesutil.Bytes32(uint64(block.Block.Slot)), bytesutil.Bytes32(uint64(block.Block.ProposerIndex))...) - signedBlock, err := blocks.NewSignedBeaconBlock(block) - require.NoError(t, err) - r.seenBlockCache.Add(string(b), signedBlock) // Try to process the same block again err = r.detectAndBroadcastEquivocation(ctx, signedBlock) require.NoError(t, err) assert.Equal(t, 0, len(slashingPool.PendingPropSlashings), "Expected no slashings for same signature") } + +func TestDetectAndBroadcastEquivocation_DifferentSignatures(t *testing.T) { + // db := dbtest.SetupDB(t) + p := p2ptest.NewTestP2P(t) + ctx := context.Background() + beaconState, privKeys := util.DeterministicGenesisState(t, 100) + + // Create head block + headBlock := util.NewBeaconBlock() + headBlock.Block.Slot = 1 + headBlock.Block.ProposerIndex = 0 + sig1, err := signing.ComputeDomainAndSign(beaconState, 0, headBlock.Block, params.BeaconConfig().DomainBeaconProposer, privKeys[0]) + require.NoError(t, err) + headBlock.Signature = sig1 + + // Create new block with same slot/proposer but different content + newBlock := util.NewBeaconBlock() + newBlock.Block.Slot = 1 + newBlock.Block.ProposerIndex = 0 + newBlock.Block.ParentRoot = bytesutil.PadTo([]byte("different"), 32) + sig2, err := signing.ComputeDomainAndSign(beaconState, 0, newBlock.Block, params.BeaconConfig().DomainBeaconProposer, privKeys[0]) + require.NoError(t, err) + newBlock.Signature = sig2 + + signedHeadBlock, err := blocks.NewSignedBeaconBlock(headBlock) + require.NoError(t, err) + + slashingPool := &slashingsmock.PoolMock{} + chainService := &mock.ChainService{ + State: beaconState, + Genesis: time.Now(), + Block: signedHeadBlock, + } + r := &Service{ + cfg: &config{ + p2p: p, + chain: chainService, + slashingPool: slashingPool, + }, + seenBlockCache: lruwrpr.New(10), + } + + // Mark block as seen + r.setSeenBlockIndexSlot(newBlock.Block.Slot, newBlock.Block.ProposerIndex) + + signedNewBlock, err := blocks.NewSignedBeaconBlock(newBlock) + require.NoError(t, err) + + err = r.detectAndBroadcastEquivocation(ctx, signedNewBlock) + require.NoError(t, err) + + // Verify slashing was inserted + require.Equal(t, 1, len(slashingPool.PendingPropSlashings)) +} + +func TestDetectAndBroadcastEquivocation_HeadStateError(t *testing.T) { + ctx := context.Background() + p := p2ptest.NewTestP2P(t) + + block := util.NewBeaconBlock() + block.Block.Slot = 1 + block.Block.ProposerIndex = 0 + + signedBlock, err := blocks.NewSignedBeaconBlock(block) + require.NoError(t, err) + + chainService := &mock.ChainService{ + State: nil, // This will cause HeadStateReadOnly to return nil state + Block: signedBlock, + } + r := &Service{ + cfg: &config{ + p2p: p, + chain: chainService, + slashingPool: &slashingsmock.PoolMock{}, + }, + seenBlockCache: lruwrpr.New(10), + } + + // Mark block as seen to trigger equivocation check + r.setSeenBlockIndexSlot(block.Block.Slot, block.Block.ProposerIndex) + + err = r.detectAndBroadcastEquivocation(ctx, signedBlock) + require.ErrorContains(t, "could not get head state", err) +} From 1709546ca4275e6d0f08fa197068cdd5b83ab3b8 Mon Sep 17 00:00:00 2001 From: Shyam Patel Date: Mon, 3 Feb 2025 15:38:59 -0800 Subject: [PATCH 11/14] add more equivocation unit tests; update a mock to include HeadState error --- beacon-chain/blockchain/testing/mock.go | 4 ++++ beacon-chain/sync/validate_beacon_blocks_test.go | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/beacon-chain/blockchain/testing/mock.go b/beacon-chain/blockchain/testing/mock.go index 92edfa23a9a7..60f7dd8e4452 100644 --- a/beacon-chain/blockchain/testing/mock.go +++ b/beacon-chain/blockchain/testing/mock.go @@ -53,6 +53,7 @@ type ChainService struct { InitSyncBlockRoots map[[32]byte]bool DB db.Database State state.BeaconState + HeadStateErr error Block interfaces.ReadOnlySignedBeaconBlock VerifyBlkDescendantErr error stateNotifier statefeed.Notifier @@ -360,6 +361,9 @@ func (s *ChainService) HeadState(context.Context) (state.BeaconState, error) { // HeadStateReadOnly mocks HeadStateReadOnly method in chain service. func (s *ChainService) HeadStateReadOnly(context.Context) (state.ReadOnlyBeaconState, error) { + if s.HeadStateErr != nil { + return nil, s.HeadStateErr + } return s.State, nil } diff --git a/beacon-chain/sync/validate_beacon_blocks_test.go b/beacon-chain/sync/validate_beacon_blocks_test.go index 78a4dafbb013..e596ce280451 100644 --- a/beacon-chain/sync/validate_beacon_blocks_test.go +++ b/beacon-chain/sync/validate_beacon_blocks_test.go @@ -1694,8 +1694,9 @@ func TestDetectAndBroadcastEquivocation_HeadStateError(t *testing.T) { require.NoError(t, err) chainService := &mock.ChainService{ - State: nil, // This will cause HeadStateReadOnly to return nil state - Block: signedBlock, + State: nil, + Block: signedBlock, + HeadStateErr: errors.New("could not get head state"), } r := &Service{ cfg: &config{ From e6ce574d64cdf3669e2558af65aa5688102508d6 Mon Sep 17 00:00:00 2001 From: Shyam Patel Date: Sun, 9 Feb 2025 19:16:53 -0800 Subject: [PATCH 12/14] update the order of the checks --- beacon-chain/sync/validate_beacon_blocks.go | 73 ++++++++++++++++----- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/beacon-chain/sync/validate_beacon_blocks.go b/beacon-chain/sync/validate_beacon_blocks.go index 75606b8c694b..bcc9e759a26a 100644 --- a/beacon-chain/sync/validate_beacon_blocks.go +++ b/beacon-chain/sync/validate_beacon_blocks.go @@ -463,33 +463,29 @@ func getBlockFields(b interfaces.ReadOnlySignedBeaconBlock) logrus.Fields { } // detectAndBroadcastEquivocation checks if the given block is an equivocating block by comparing it with -// the head block when a duplicate slot/proposer index is detected. If an equivocation is found, it creates -// and broadcasts a proposer slashing object and adds it to the slashing pool. +// the head block. If the blocks are from the same slot and proposer but have different signatures, +// it verifies the difference constitutes a slashable offense before creating and broadcasting a proposer +// slashing object. func (s *Service) detectAndBroadcastEquivocation(ctx context.Context, blk interfaces.ReadOnlySignedBeaconBlock) error { - // If this proposer/slot combo is seen before, it means this is a potential equivocating block slot := blk.Block().Slot() proposerIndex := blk.Block().ProposerIndex() - if !s.hasSeenBlockIndexSlot(slot, proposerIndex) { - return nil - } - - // Get the head state for block verification - headState, err := s.cfg.chain.HeadStateReadOnly(ctx) - if err != nil { - return errors.Wrap(err, "could not get head state") - } - - // Compare signatures since they must be different for equivocation - sig1 := blk.Signature() + // Get head block for comparison headBlock, err := s.cfg.chain.HeadBlock(ctx) if err != nil { return errors.Wrap(err, "could not get head block") } + // Only proceed if this block is from same slot and proposer as head + if headBlock.Block().Slot() != slot || headBlock.Block().ProposerIndex() != proposerIndex { + return nil + } + + // Compare signatures + sig1 := blk.Signature() sig2 := headBlock.Signature() - // Different signatures indicate equivocation + // If signatures match, these are the same block if sig1 == sig2 { return nil } @@ -508,7 +504,18 @@ func (s *Service) detectAndBroadcastEquivocation(ctx context.Context, blk interf Header_2: header2, } - // Verify the proposer slashing is valid + // Verify basic slashing conditions before state check + if err := verifySlashableBlock(slashing); err != nil { + return errors.Wrap(err, "block headers not slashable") + } + + // Get state for final verification + headState, err := s.cfg.chain.HeadStateReadOnly(ctx) + if err != nil { + return errors.Wrap(err, "could not get head state") + } + + // Verify the slashing against current state if err := blocks.VerifyProposerSlashing(headState, slashing); err != nil { return errors.Wrap(err, "could not verify proposer slashing") } @@ -520,10 +527,40 @@ func (s *Service) detectAndBroadcastEquivocation(ctx context.Context, blk interf } } - // Also insert into slashing pool + // Insert into slashing pool if err := s.cfg.slashingPool.InsertProposerSlashing(ctx, headState, slashing); err != nil { return errors.Wrap(err, "could not insert proposer slashing into pool") } return nil } + +func verifySlashableBlock(slashing *ethpb.ProposerSlashing) error { + header1 := slashing.Header_1.Header + header2 := slashing.Header_2.Header + + // Headers should be from same proposer + if header1.ProposerIndex != header2.ProposerIndex { + return errors.New("headers are not from same proposer") + } + + // Headers should be for same slot + if header1.Slot != header2.Slot { + return errors.New("headers are not from same slot") + } + + // Headers should be different + root1, err := header1.HashTreeRoot() + if err != nil { + return errors.Wrap(err, "could not hash header 1") + } + root2, err := header2.HashTreeRoot() + if err != nil { + return errors.Wrap(err, "could not hash header 2") + } + if root1 == root2 { + return errors.New("headers are identical") + } + + return nil +} From 5b71bba24ff3b453d5ec4b676107b9c6f3bc9413 Mon Sep 17 00:00:00 2001 From: Shyam Patel Date: Wed, 12 Feb 2025 00:03:10 -0800 Subject: [PATCH 13/14] move slashing before state fetch; update Tests --- beacon-chain/sync/validate_beacon_blocks.go | 4 + .../sync/validate_beacon_blocks_test.go | 154 ++++++++++++++++-- 2 files changed, 142 insertions(+), 16 deletions(-) diff --git a/beacon-chain/sync/validate_beacon_blocks.go b/beacon-chain/sync/validate_beacon_blocks.go index bcc9e759a26a..f5c5f3e2fff4 100644 --- a/beacon-chain/sync/validate_beacon_blocks.go +++ b/beacon-chain/sync/validate_beacon_blocks.go @@ -535,6 +535,10 @@ func (s *Service) detectAndBroadcastEquivocation(ctx context.Context, blk interf return nil } +// verifySlashableBlock performs basic verification of a proposer slashing object to ensure the headers represent +// a valid slashing condition. It checks that: both headers are from the same proposer, +// both headers are for the same slot, headers have different roots +// This verification is done before the state-dependent checks in blocks.VerifyProposerSlashing func verifySlashableBlock(slashing *ethpb.ProposerSlashing) error { header1 := slashing.Header_1.Header header2 := slashing.Header_2.Header diff --git a/beacon-chain/sync/validate_beacon_blocks_test.go b/beacon-chain/sync/validate_beacon_blocks_test.go index e596ce280451..b725ab654492 100644 --- a/beacon-chain/sync/validate_beacon_blocks_test.go +++ b/beacon-chain/sync/validate_beacon_blocks_test.go @@ -1498,7 +1498,6 @@ func Test_validateDenebBeaconBlock(t *testing.T) { } func TestDetectAndBroadcastEquivocation_NoEquivocation(t *testing.T) { - // db := dbtest.SetupDB(t) p := p2ptest.NewTestP2P(t) ctx := context.Background() beaconState, privKeys := util.DeterministicGenesisState(t, 100) @@ -1511,9 +1510,17 @@ func TestDetectAndBroadcastEquivocation_NoEquivocation(t *testing.T) { require.NoError(t, err) block.Signature = sig + // Create head block with different slot/proposer + headBlock := util.NewBeaconBlock() + headBlock.Block.Slot = 2 // Different slot + headBlock.Block.ProposerIndex = 1 // Different proposer + signedHeadBlock, err := blocks.NewSignedBeaconBlock(headBlock) + require.NoError(t, err) + chainService := &mock.ChainService{ State: beaconState, Genesis: time.Now(), + Block: signedHeadBlock, } r := &Service{ cfg: &config{ @@ -1521,7 +1528,6 @@ func TestDetectAndBroadcastEquivocation_NoEquivocation(t *testing.T) { chain: chainService, slashingPool: &slashingsmock.PoolMock{}, }, - seenBlockCache: lruwrpr.New(10), } signedBlock, err := blocks.NewSignedBeaconBlock(block) @@ -1569,12 +1575,8 @@ func TestDetectAndBroadcastEquivocation_EquivocationDetected(t *testing.T) { chain: chainService, slashingPool: slashingPool, }, - seenBlockCache: lruwrpr.New(10), } - // Mark block as seen - r.setSeenBlockIndexSlot(newBlock.Block.Slot, newBlock.Block.ProposerIndex) - signedNewBlock, err := blocks.NewSignedBeaconBlock(newBlock) require.NoError(t, err) @@ -1616,13 +1618,8 @@ func TestDetectAndBroadcastEquivocation_SameSignature(t *testing.T) { chain: chainService, slashingPool: slashingPool, }, - seenBlockCache: lruwrpr.New(10), } - // Mark block as seen - r.setSeenBlockIndexSlot(block.Block.Slot, block.Block.ProposerIndex) - - // Try to process the same block again err = r.detectAndBroadcastEquivocation(ctx, signedBlock) require.NoError(t, err) assert.Equal(t, 0, len(slashingPool.PendingPropSlashings), "Expected no slashings for same signature") @@ -1685,17 +1682,33 @@ func TestDetectAndBroadcastEquivocation_DifferentSignatures(t *testing.T) { func TestDetectAndBroadcastEquivocation_HeadStateError(t *testing.T) { ctx := context.Background() p := p2ptest.NewTestP2P(t) + beaconState, privKeys := util.DeterministicGenesisState(t, 100) block := util.NewBeaconBlock() block.Block.Slot = 1 block.Block.ProposerIndex = 0 + block.Block.ParentRoot = bytesutil.PadTo([]byte("parent1"), 32) + sig1, err := signing.ComputeDomainAndSign(beaconState, 0, block.Block, params.BeaconConfig().DomainBeaconProposer, privKeys[0]) + require.NoError(t, err) + block.Signature = sig1 + + headBlock := util.NewBeaconBlock() + headBlock.Block.Slot = 1 // Same slot + headBlock.Block.ProposerIndex = 0 // Same proposer + headBlock.Block.ParentRoot = bytesutil.PadTo([]byte("parent2"), 32) // Different parent root + sig2, err := signing.ComputeDomainAndSign(beaconState, 0, headBlock.Block, params.BeaconConfig().DomainBeaconProposer, privKeys[0]) + require.NoError(t, err) + headBlock.Signature = sig2 signedBlock, err := blocks.NewSignedBeaconBlock(block) require.NoError(t, err) + signedHeadBlock, err := blocks.NewSignedBeaconBlock(headBlock) + require.NoError(t, err) + chainService := &mock.ChainService{ State: nil, - Block: signedBlock, + Block: signedHeadBlock, HeadStateErr: errors.New("could not get head state"), } r := &Service{ @@ -1704,12 +1717,121 @@ func TestDetectAndBroadcastEquivocation_HeadStateError(t *testing.T) { chain: chainService, slashingPool: &slashingsmock.PoolMock{}, }, - seenBlockCache: lruwrpr.New(10), } - // Mark block as seen to trigger equivocation check - r.setSeenBlockIndexSlot(block.Block.Slot, block.Block.ProposerIndex) - err = r.detectAndBroadcastEquivocation(ctx, signedBlock) require.ErrorContains(t, "could not get head state", err) } + +func TestVerifySlashableBlock(t *testing.T) { + tests := []struct { + name string + header1 *ethpb.SignedBeaconBlockHeader + header2 *ethpb.SignedBeaconBlockHeader + expectedError string + }{ + { + name: "Valid slashable block", + header1: ðpb.SignedBeaconBlockHeader{ + Header: ðpb.BeaconBlockHeader{ + Slot: 1, + ProposerIndex: 0, + ParentRoot: bytesutil.PadTo([]byte("parent1"), 32), + StateRoot: bytesutil.PadTo([]byte("state1"), 32), + BodyRoot: bytesutil.PadTo([]byte("body1"), 32), + }, + }, + header2: ðpb.SignedBeaconBlockHeader{ + Header: ðpb.BeaconBlockHeader{ + Slot: 1, + ProposerIndex: 0, + ParentRoot: bytesutil.PadTo([]byte("parent2"), 32), + StateRoot: bytesutil.PadTo([]byte("state2"), 32), + BodyRoot: bytesutil.PadTo([]byte("body2"), 32), + }, + }, + expectedError: "", + }, + { + name: "Different proposers", + header1: ðpb.SignedBeaconBlockHeader{ + Header: ðpb.BeaconBlockHeader{ + Slot: 1, + ProposerIndex: 0, + ParentRoot: bytesutil.PadTo([]byte("parent"), 32), + StateRoot: bytesutil.PadTo([]byte("state"), 32), + BodyRoot: bytesutil.PadTo([]byte("body"), 32), + }, + }, + header2: ðpb.SignedBeaconBlockHeader{ + Header: ðpb.BeaconBlockHeader{ + Slot: 1, + ProposerIndex: 1, + ParentRoot: bytesutil.PadTo([]byte("parent"), 32), + StateRoot: bytesutil.PadTo([]byte("state"), 32), + BodyRoot: bytesutil.PadTo([]byte("body"), 32), + }, + }, + expectedError: "headers are not from same proposer", + }, + { + name: "Different slots", + header1: ðpb.SignedBeaconBlockHeader{ + Header: ðpb.BeaconBlockHeader{ + Slot: 1, + ProposerIndex: 0, + ParentRoot: bytesutil.PadTo([]byte("parent"), 32), + StateRoot: bytesutil.PadTo([]byte("state"), 32), + BodyRoot: bytesutil.PadTo([]byte("body"), 32), + }, + }, + header2: ðpb.SignedBeaconBlockHeader{ + Header: ðpb.BeaconBlockHeader{ + Slot: 2, + ProposerIndex: 0, + ParentRoot: bytesutil.PadTo([]byte("parent"), 32), + StateRoot: bytesutil.PadTo([]byte("state"), 32), + BodyRoot: bytesutil.PadTo([]byte("body"), 32), + }, + }, + expectedError: "headers are not from same slot", + }, + { + name: "Identical headers", + header1: ðpb.SignedBeaconBlockHeader{ + Header: ðpb.BeaconBlockHeader{ + Slot: 1, + ProposerIndex: 0, + ParentRoot: bytesutil.PadTo([]byte("parent"), 32), + StateRoot: bytesutil.PadTo([]byte("state"), 32), + BodyRoot: bytesutil.PadTo([]byte("body"), 32), + }, + }, + header2: ðpb.SignedBeaconBlockHeader{ + Header: ðpb.BeaconBlockHeader{ + Slot: 1, + ProposerIndex: 0, + ParentRoot: bytesutil.PadTo([]byte("parent"), 32), + StateRoot: bytesutil.PadTo([]byte("state"), 32), + BodyRoot: bytesutil.PadTo([]byte("body"), 32), + }, + }, + expectedError: "headers are identical", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + slashing := ðpb.ProposerSlashing{ + Header_1: tt.header1, + Header_2: tt.header2, + } + err := verifySlashableBlock(slashing) + if tt.expectedError == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, tt.expectedError, err) + } + }) + } +} From 8da810f4639eb57198541b1591e16f17f57a3370 Mon Sep 17 00:00:00 2001 From: Shyam Patel Date: Wed, 12 Feb 2025 13:57:14 -0800 Subject: [PATCH 14/14] update changelog --- changelog/kira_broadcast_slashings.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/changelog/kira_broadcast_slashings.md b/changelog/kira_broadcast_slashings.md index bb529477652a..db05b7a48cf2 100644 --- a/changelog/kira_broadcast_slashings.md +++ b/changelog/kira_broadcast_slashings.md @@ -1,5 +1,10 @@ -### Added -- Added broadcast of slashing events (proposer and attester slashings) upon detection of equivocation. +### Added +- Added immediate broadcasting of proposer slashings when equivocating blocks are detected during block processing. +- Added dedicated block header verification for proposer slashing detection. -### Updated -- Updated attestation pool logic to prioritize slashings over regular attestations during block production. +### Changed +- Improved equivocation detection by validating blocks against head block instead of cache. +- Removed reliance on seen block cache for slashing detection. + +### Fixed +- Fixed potential false positives in equivocation detection by adding header validation. \ No newline at end of file