mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-09 21:38:05 -05:00
Detect Slashable Blocks in Optimized Slasher (#9693)
* pass * Update beacon-chain/slasher/detect_blocks.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> * Update beacon-chain/slasher/detect_blocks.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> Co-authored-by: prylabs-bulldozer[bot] <58059840+prylabs-bulldozer[bot]@users.noreply.github.com>
This commit is contained in:
@@ -4,6 +4,7 @@ go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"chunks.go",
|
||||
"detect_blocks.go",
|
||||
"doc.go",
|
||||
"helpers.go",
|
||||
"metrics.go",
|
||||
@@ -36,6 +37,7 @@ go_library(
|
||||
"@com_github_prometheus_client_golang//prometheus/promauto:go_default_library",
|
||||
"@com_github_prysmaticlabs_eth2_types//:go_default_library",
|
||||
"@com_github_sirupsen_logrus//:go_default_library",
|
||||
"@io_opencensus_go//trace:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -43,6 +45,7 @@ go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"chunks_test.go",
|
||||
"detect_blocks_test.go",
|
||||
"helpers_test.go",
|
||||
"params_test.go",
|
||||
"process_slashings_test.go",
|
||||
|
||||
92
beacon-chain/slasher/detect_blocks.go
Normal file
92
beacon-chain/slasher/detect_blocks.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package slasher
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
types "github.com/prysmaticlabs/eth2-types"
|
||||
slashertypes "github.com/prysmaticlabs/prysm/beacon-chain/slasher/types"
|
||||
ethpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1"
|
||||
"go.opencensus.io/trace"
|
||||
)
|
||||
|
||||
// detectProposerSlashings takes in signed block header wrappers and returns a list of proposer slashings detected.
|
||||
func (s *Service) detectProposerSlashings(
|
||||
ctx context.Context,
|
||||
proposedBlocks []*slashertypes.SignedBlockHeaderWrapper,
|
||||
) ([]*ethpb.ProposerSlashing, error) {
|
||||
ctx, span := trace.StartSpan(ctx, "slasher.detectProposerSlashings")
|
||||
defer span.End()
|
||||
// We check if there are any slashable double proposals in the input list
|
||||
// of proposals with respect to each other.
|
||||
slashings := make([]*ethpb.ProposerSlashing, 0)
|
||||
existingProposals := make(map[string]*slashertypes.SignedBlockHeaderWrapper)
|
||||
for i, proposal := range proposedBlocks {
|
||||
key := proposalKey(proposal)
|
||||
existingProposal, ok := existingProposals[key]
|
||||
if !ok {
|
||||
existingProposals[key] = proposal
|
||||
continue
|
||||
}
|
||||
if isDoubleProposal(proposedBlocks[i].SigningRoot, existingProposal.SigningRoot) {
|
||||
doubleProposalsTotal.Inc()
|
||||
slashing := ðpb.ProposerSlashing{
|
||||
Header_1: existingProposal.SignedBeaconBlockHeader,
|
||||
Header_2: proposedBlocks[i].SignedBeaconBlockHeader,
|
||||
}
|
||||
slashings = append(slashings, slashing)
|
||||
}
|
||||
}
|
||||
|
||||
proposerSlashings, err := s.serviceCfg.Database.CheckDoubleBlockProposals(ctx, proposedBlocks)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not check for double proposals on disk")
|
||||
}
|
||||
if err := s.saveSafeProposals(ctx, proposedBlocks, proposerSlashings); err != nil {
|
||||
return nil, errors.Wrap(err, "could not save safe proposals")
|
||||
}
|
||||
slashings = append(slashings, proposerSlashings...)
|
||||
return slashings, nil
|
||||
}
|
||||
|
||||
// Check for double proposals in our database given a list of incoming block proposals.
|
||||
// For the proposals that were not slashable, we save them to the database.
|
||||
func (s *Service) saveSafeProposals(
|
||||
ctx context.Context,
|
||||
proposedBlocks []*slashertypes.SignedBlockHeaderWrapper,
|
||||
proposerSlashings []*ethpb.ProposerSlashing,
|
||||
) error {
|
||||
ctx, span := trace.StartSpan(ctx, "slasher.saveSafeProposals")
|
||||
defer span.End()
|
||||
return s.serviceCfg.Database.SaveBlockProposals(
|
||||
ctx,
|
||||
filterSafeProposals(proposedBlocks, proposerSlashings),
|
||||
)
|
||||
}
|
||||
|
||||
func filterSafeProposals(
|
||||
proposedBlocks []*slashertypes.SignedBlockHeaderWrapper,
|
||||
proposerSlashings []*ethpb.ProposerSlashing,
|
||||
) []*slashertypes.SignedBlockHeaderWrapper {
|
||||
// We initialize a map of proposers that are safe from slashing.
|
||||
safeProposers := make(map[types.ValidatorIndex]*slashertypes.SignedBlockHeaderWrapper, len(proposedBlocks))
|
||||
for _, proposal := range proposedBlocks {
|
||||
safeProposers[proposal.SignedBeaconBlockHeader.Header.ProposerIndex] = proposal
|
||||
}
|
||||
for _, doubleProposal := range proposerSlashings {
|
||||
// If a proposer is found to have committed a slashable offense, we delete
|
||||
// them from the safe proposers map.
|
||||
delete(safeProposers, doubleProposal.Header_1.Header.ProposerIndex)
|
||||
}
|
||||
// We save all the proposals that are determined "safe" and not-slashable to our database.
|
||||
safeProposals := make([]*slashertypes.SignedBlockHeaderWrapper, 0, len(safeProposers))
|
||||
for _, proposal := range safeProposers {
|
||||
safeProposals = append(safeProposals, proposal)
|
||||
}
|
||||
return safeProposals
|
||||
}
|
||||
|
||||
func proposalKey(proposal *slashertypes.SignedBlockHeaderWrapper) string {
|
||||
header := proposal.SignedBeaconBlockHeader.Header
|
||||
return uintToString(uint64(header.Slot)) + ":" + uintToString(uint64(header.ProposerIndex))
|
||||
}
|
||||
149
beacon-chain/slasher/detect_blocks_test.go
Normal file
149
beacon-chain/slasher/detect_blocks_test.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package slasher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
types "github.com/prysmaticlabs/eth2-types"
|
||||
mock "github.com/prysmaticlabs/prysm/beacon-chain/blockchain/testing"
|
||||
"github.com/prysmaticlabs/prysm/beacon-chain/core/signing"
|
||||
dbtest "github.com/prysmaticlabs/prysm/beacon-chain/db/testing"
|
||||
"github.com/prysmaticlabs/prysm/beacon-chain/operations/slashings"
|
||||
slashertypes "github.com/prysmaticlabs/prysm/beacon-chain/slasher/types"
|
||||
"github.com/prysmaticlabs/prysm/beacon-chain/state/stategen"
|
||||
"github.com/prysmaticlabs/prysm/config/params"
|
||||
"github.com/prysmaticlabs/prysm/crypto/bls"
|
||||
"github.com/prysmaticlabs/prysm/encoding/bytesutil"
|
||||
ethpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1"
|
||||
"github.com/prysmaticlabs/prysm/testing/require"
|
||||
"github.com/prysmaticlabs/prysm/testing/util"
|
||||
logTest "github.com/sirupsen/logrus/hooks/test"
|
||||
)
|
||||
|
||||
func Test_processQueuedBlocks_DetectsDoubleProposals(t *testing.T) {
|
||||
hook := logTest.NewGlobal()
|
||||
slasherDB := dbtest.SetupSlasherDB(t)
|
||||
beaconDB := dbtest.SetupDB(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
beaconState, err := util.NewBeaconState()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Initialize validators in the state.
|
||||
numVals := params.BeaconConfig().MinGenesisActiveValidatorCount
|
||||
validators := make([]*ethpb.Validator, numVals)
|
||||
privKeys := make([]bls.SecretKey, numVals)
|
||||
for i := range validators {
|
||||
privKey, err := bls.RandKey()
|
||||
require.NoError(t, err)
|
||||
privKeys[i] = privKey
|
||||
validators[i] = ðpb.Validator{
|
||||
PublicKey: privKey.PublicKey().Marshal(),
|
||||
WithdrawalCredentials: make([]byte, 32),
|
||||
}
|
||||
}
|
||||
err = beaconState.SetValidators(validators)
|
||||
require.NoError(t, err)
|
||||
domain, err := signing.Domain(
|
||||
beaconState.Fork(),
|
||||
0,
|
||||
params.BeaconConfig().DomainBeaconProposer,
|
||||
beaconState.GenesisValidatorRoot(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
mockChain := &mock.ChainService{
|
||||
State: beaconState,
|
||||
}
|
||||
s := &Service{
|
||||
serviceCfg: &ServiceConfig{
|
||||
Database: slasherDB,
|
||||
StateNotifier: &mock.MockStateNotifier{},
|
||||
HeadStateFetcher: mockChain,
|
||||
StateGen: stategen.New(beaconDB),
|
||||
SlashingPoolInserter: &slashings.PoolMock{},
|
||||
},
|
||||
params: DefaultParams(),
|
||||
blksQueue: newBlocksQueue(),
|
||||
}
|
||||
|
||||
parentRoot := bytesutil.ToBytes32([]byte("parent"))
|
||||
err = s.serviceCfg.StateGen.SaveState(ctx, parentRoot, beaconState)
|
||||
require.NoError(t, err)
|
||||
|
||||
currentSlotChan := make(chan types.Slot)
|
||||
exitChan := make(chan struct{})
|
||||
go func() {
|
||||
s.processQueuedBlocks(ctx, currentSlotChan)
|
||||
exitChan <- struct{}{}
|
||||
}()
|
||||
|
||||
signedBlkHeaders := []*slashertypes.SignedBlockHeaderWrapper{
|
||||
createProposalWrapper(t, 4, 1, []byte{1}),
|
||||
createProposalWrapper(t, 4, 1, []byte{1}),
|
||||
createProposalWrapper(t, 4, 1, []byte{1}),
|
||||
createProposalWrapper(t, 4, 1, []byte{2}),
|
||||
}
|
||||
|
||||
// Add valid signatures to the block headers we are testing.
|
||||
for _, proposalWrapper := range signedBlkHeaders {
|
||||
proposalWrapper.SignedBeaconBlockHeader.Header.ParentRoot = parentRoot[:]
|
||||
headerHtr, err := proposalWrapper.SignedBeaconBlockHeader.Header.HashTreeRoot()
|
||||
require.NoError(t, err)
|
||||
container := ðpb.SigningData{
|
||||
ObjectRoot: headerHtr[:],
|
||||
Domain: domain,
|
||||
}
|
||||
signingRoot, err := container.HashTreeRoot()
|
||||
require.NoError(t, err)
|
||||
privKey := privKeys[proposalWrapper.SignedBeaconBlockHeader.Header.ProposerIndex]
|
||||
proposalWrapper.SignedBeaconBlockHeader.Signature = privKey.Sign(signingRoot[:]).Marshal()
|
||||
}
|
||||
|
||||
s.blksQueue.extend(signedBlkHeaders)
|
||||
|
||||
currentSlot := types.Slot(4)
|
||||
currentSlotChan <- currentSlot
|
||||
cancel()
|
||||
<-exitChan
|
||||
require.LogsContain(t, hook, "Proposer slashing detected")
|
||||
}
|
||||
|
||||
func Test_processQueuedBlocks_NotSlashable(t *testing.T) {
|
||||
hook := logTest.NewGlobal()
|
||||
slasherDB := dbtest.SetupSlasherDB(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
beaconState, err := util.NewBeaconState()
|
||||
require.NoError(t, err)
|
||||
currentSlot := types.Slot(4)
|
||||
require.NoError(t, beaconState.SetSlot(currentSlot))
|
||||
mockChain := &mock.ChainService{
|
||||
State: beaconState,
|
||||
Slot: ¤tSlot,
|
||||
}
|
||||
|
||||
s := &Service{
|
||||
serviceCfg: &ServiceConfig{
|
||||
Database: slasherDB,
|
||||
StateNotifier: &mock.MockStateNotifier{},
|
||||
HeadStateFetcher: mockChain,
|
||||
},
|
||||
params: DefaultParams(),
|
||||
blksQueue: newBlocksQueue(),
|
||||
}
|
||||
currentSlotChan := make(chan types.Slot)
|
||||
exitChan := make(chan struct{})
|
||||
go func() {
|
||||
s.processQueuedBlocks(ctx, currentSlotChan)
|
||||
exitChan <- struct{}{}
|
||||
}()
|
||||
s.blksQueue.extend([]*slashertypes.SignedBlockHeaderWrapper{
|
||||
createProposalWrapper(t, 4, 1, []byte{1}),
|
||||
createProposalWrapper(t, 4, 1, []byte{1}),
|
||||
})
|
||||
currentSlotChan <- currentSlot
|
||||
cancel()
|
||||
<-exitChan
|
||||
require.LogsDoNotContain(t, hook, "Proposer slashing detected")
|
||||
}
|
||||
@@ -148,8 +148,11 @@ func (s *Service) processQueuedBlocks(ctx context.Context, slotTicker <-chan typ
|
||||
|
||||
start := time.Now()
|
||||
// Check for slashings.
|
||||
// TODO(#8331): Detect slashings.
|
||||
slashings := make([]*ethpb.ProposerSlashing, 0)
|
||||
slashings, err := s.detectProposerSlashings(ctx, blocks)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Could not detect proposer slashings")
|
||||
continue
|
||||
}
|
||||
|
||||
// Process proposer slashings by verifying their signatures, submitting
|
||||
// to the beacon node's operations pool, and logging them.
|
||||
|
||||
Reference in New Issue
Block a user