Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Slasher: Refactor and add tests #13589

Merged
merged 38 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
7dfbb3c
`helpers.go`: Improve naming consistency.
nalepae Feb 2, 2024
c0f524c
`detect_attestations.go`: Improve readability.
nalepae Feb 2, 2024
cc4f2ec
`receive.go`: Add `attsQueueSize` in log message.
nalepae Feb 2, 2024
1d88c76
`checkSlashableAttestations`: Improve logging.
nalepae Feb 2, 2024
c87bb06
`loadChunks`: Use explicit `chunkKind` and `chunkIndices`.
nalepae Feb 2, 2024
aa2600b
`getChunk`: Use specific `chunkIndex` and `chunkKind`.
nalepae Feb 2, 2024
a446708
`validatorIndicesInChunk` -> `validatorIndexesInChunk`.
nalepae Feb 2, 2024
3c31ad7
`epochUpdateForValidator`: Use explicit arguments.
nalepae Feb 2, 2024
5e0b851
`getChunk`: Change order of arguments.
nalepae Feb 2, 2024
5d46108
`latestEpochWrittenForValidator`: Use `ok` parameter.
nalepae Feb 2, 2024
24e1be7
`applyAttestationForValidator`: Use explicit arguments.
nalepae Feb 2, 2024
06853c4
`updateSpans`: Use explicit arguments.
nalepae Feb 2, 2024
fedb1ef
`saveUpdatedChunks`: Use explicit arguments.
nalepae Feb 2, 2024
75d36f1
`checkSurrounds`: Use explicit arguments.
nalepae Feb 2, 2024
3377420
`epochUpdateForValidator`: Set modified by the function argument first.
nalepae Feb 2, 2024
bbbe4da
`applyAttestationForValidator`: Set mutated argument `chunksByChunkId…
nalepae Feb 2, 2024
0646d11
`applyAttestationForValidator`: Rename variables.
nalepae Feb 2, 2024
c7e6aa9
`Test_processQueuedAttestations`: Fix test.
nalepae Feb 2, 2024
fdcfe27
`updateSpans`: Keep happy path in the outer scope.
nalepae Feb 2, 2024
41af050
`checkSurrounds`: Rename variable.
nalepae Feb 2, 2024
36adda9
`getChunk`: Avoid side effects.
nalepae Feb 2, 2024
d091142
`CheckSlashable`: Flatten.
nalepae Feb 3, 2024
e28858e
`detect_attestations_test.go`: Simplify.
nalepae Feb 3, 2024
7b136b9
`CheckSlashable`: Add error log in case of missing attestation.
nalepae Feb 5, 2024
caeb947
`processQueuedAttestations`: Extract a sub function.
nalepae Feb 5, 2024
f7d4725
`processAttesterSlashings` and `processProposerSlashings`: Improve.
nalepae Feb 5, 2024
0aeb405
`processAttesterSlashings`: Return processed slashings.
nalepae Feb 5, 2024
54c13c5
`createAttestationWrapper`: Rename variables.
nalepae Feb 6, 2024
fd3c5ac
`signingRoot` ==> `headerRoot` or `dataRoot`.
nalepae Feb 6, 2024
af8a6c6
`createAttestationWrapper` => `createAttestationWrapperEmptySig`.
nalepae Feb 6, 2024
4561cd5
Implement `createAttestationWrapper`.
nalepae Feb 6, 2024
9bee736
`processAttestations`: Return processed attester slashings.
nalepae Feb 6, 2024
cb51e74
Test `processAttestations` instead of `processQueuedAttestations`.
nalepae Feb 6, 2024
a945004
`Test_processAttestations`: Allow multiple steps.
nalepae Feb 6, 2024
6ea4e41
`Test_processAttestations`: Add double steps tests.
nalepae Feb 7, 2024
d1665e8
`NextChunkStartEpoch`: Fix function comment.
nalepae Feb 8, 2024
e41c281
`chunks.go`: Avoid templating log messages.
nalepae Feb 8, 2024
2ffff9c
`checkSlashableAttestations`: Simplify duration computation.
nalepae Feb 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 23 additions & 24 deletions beacon-chain/db/slasherkv/slasher.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ import (
)

const (
// Signing root (32 bytes)
attestationRecordKeySize = 32 // Bytes.
signingRootSize = 32 // Bytes.
rootSize = 32 // Bytes.
)

// LastEpochWrittenForValidators given a list of validator indices returns the latest
Expand Down Expand Up @@ -170,15 +169,15 @@ func (s *Store) CheckAttesterDoubleVotes(
continue
}

// Retrieve the attestation record corresponding to the signing root
// Retrieve the attestation record corresponding to the data root
// from the database.
encExistingAttRecord := attRecordsBkt.Get(attRecordsKey)
if encExistingAttRecord == nil {
continue
}

existingSigningRoot := bytesutil.ToBytes32(attRecordsKey[:signingRootSize])
if existingSigningRoot == attToProcess.SigningRoot {
existingDataRoot := bytesutil.ToBytes32(attRecordsKey[:rootSize])
if existingDataRoot == attToProcess.DataRoot {
continue
}

Expand Down Expand Up @@ -281,20 +280,20 @@ func (s *Store) SaveAttestationRecordsForValidators(

return s.db.Update(func(tx *bolt.Tx) error {
attRecordsBkt := tx.Bucket(attestationRecordsBucket)
signingRootsBkt := tx.Bucket(attestationDataRootsBucket)
dataRootsBkt := tx.Bucket(attestationDataRootsBucket)

for i := len(attestations) - 1; i >= 0; i-- {
attestation := attestations[i]

if err := attRecordsBkt.Put(attestation.SigningRoot[:], encodedRecords[i]); err != nil {
if err := attRecordsBkt.Put(attestation.DataRoot[:], encodedRecords[i]); err != nil {
return err
}

for _, valIdx := range attestation.IndexedAttestation.AttestingIndices {
encIdx := encodeValidatorIndex(primitives.ValidatorIndex(valIdx))

key := append(encodedTargetEpoch[i], encIdx...)
if err := signingRootsBkt.Put(key, attestation.SigningRoot[:]); err != nil {
if err := dataRootsBkt.Put(key, attestation.DataRoot[:]); err != nil {
return err
}
}
Expand Down Expand Up @@ -396,14 +395,14 @@ func (s *Store) CheckDoubleBlockProposals(

// If there is no existing proposal record (empty result), then there is no double proposal.
// We can continue to the next proposal.
if len(encExistingProposalWrapper) < signingRootSize {
if len(encExistingProposalWrapper) < rootSize {
continue
}

// Compare the proposal signing root in the DB with the incoming proposal signing root.
// If they differ, we have a double proposal.
existingSigningRoot := bytesutil.ToBytes32(encExistingProposalWrapper[:signingRootSize])
if existingSigningRoot != incomingProposal.SigningRoot {
existingRoot := bytesutil.ToBytes32(encExistingProposalWrapper[:rootSize])
if existingRoot != incomingProposal.HeaderRoot {
existingProposalWrapper, err := decodeProposalRecord(encExistingProposalWrapper)
if err != nil {
return err
Expand Down Expand Up @@ -610,18 +609,18 @@ func encodeAttestationRecord(att *slashertypes.IndexedAttestationWrapper) ([]byt
// Compress attestation.
compressedAtt := snappy.Encode(nil, encodedAtt)

return append(att.SigningRoot[:], compressedAtt...), nil
return append(att.DataRoot[:], compressedAtt...), nil
}

// Decode attestation record from bytes.
// The input encoded attestation record consists in the signing root concatened with the compressed attestation record.
func decodeAttestationRecord(encoded []byte) (*slashertypes.IndexedAttestationWrapper, error) {
if len(encoded) < signingRootSize {
return nil, fmt.Errorf("wrong length for encoded attestation record, want minimum %d, got %d", signingRootSize, len(encoded))
if len(encoded) < rootSize {
return nil, fmt.Errorf("wrong length for encoded attestation record, want minimum %d, got %d", rootSize, len(encoded))
}

// Decompress attestation.
decodedAttBytes, err := snappy.Decode(nil, encoded[signingRootSize:])
decodedAttBytes, err := snappy.Decode(nil, encoded[rootSize:])
if err != nil {
return nil, err
}
Expand All @@ -633,13 +632,13 @@ func decodeAttestationRecord(encoded []byte) (*slashertypes.IndexedAttestationWr
}

// Decode signing root.
signingRootBytes := encoded[:signingRootSize]
signingRoot := bytesutil.ToBytes32(signingRootBytes)
dataRootBytes := encoded[:rootSize]
dataRoot := bytesutil.ToBytes32(dataRootBytes)

// Return decoded attestation.
attestation := &slashertypes.IndexedAttestationWrapper{
IndexedAttestation: decodedAtt,
SigningRoot: signingRoot,
DataRoot: dataRoot,
}

return attestation, nil
Expand All @@ -654,18 +653,18 @@ func encodeProposalRecord(blkHdr *slashertypes.SignedBlockHeaderWrapper) ([]byte
return nil, err
}
compressedHdr := snappy.Encode(nil, encodedHdr)
return append(blkHdr.SigningRoot[:], compressedHdr...), nil
return append(blkHdr.HeaderRoot[:], compressedHdr...), nil
}

func decodeProposalRecord(encoded []byte) (*slashertypes.SignedBlockHeaderWrapper, error) {
if len(encoded) < signingRootSize {
if len(encoded) < rootSize {
return nil, fmt.Errorf(
"wrong length for encoded proposal record, want %d, got %d", signingRootSize, len(encoded),
"wrong length for encoded proposal record, want %d, got %d", rootSize, len(encoded),
)
}
signingRoot := encoded[:signingRootSize]
dataRoot := encoded[:rootSize]
decodedBlkHdr := &ethpb.SignedBeaconBlockHeader{}
decodedHdrBytes, err := snappy.Decode(nil, encoded[signingRootSize:])
decodedHdrBytes, err := snappy.Decode(nil, encoded[rootSize:])
if err != nil {
return nil, err
}
Expand All @@ -674,7 +673,7 @@ func decodeProposalRecord(encoded []byte) (*slashertypes.SignedBlockHeaderWrappe
}
return &slashertypes.SignedBlockHeaderWrapper{
SignedBeaconBlockHeader: decodedBlkHdr,
SigningRoot: bytesutil.ToBytes32(signingRoot),
HeaderRoot: bytesutil.ToBytes32(dataRoot),
}, nil
}

Expand Down
24 changes: 13 additions & 11 deletions beacon-chain/db/slasherkv/slasher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func TestStore_AttestationRecordForValidator_SaveRetrieve(t *testing.T) {
require.NoError(t, err)
assert.DeepEqual(t, target, attRecord.IndexedAttestation.Data.Target.Epoch)
assert.DeepEqual(t, source, attRecord.IndexedAttestation.Data.Source.Epoch)
assert.DeepEqual(t, sr, attRecord.SigningRoot)
assert.DeepEqual(t, sr, attRecord.DataRoot)
}

func TestStore_LastEpochWrittenForValidators(t *testing.T) {
Expand Down Expand Up @@ -224,9 +224,9 @@ func TestStore_ExistingBlockProposals(t *testing.T) {

// Second time checking same proposals but all with different signing root should
// return all double proposals.
proposals[0].SigningRoot = bytesutil.ToBytes32([]byte{2})
proposals[1].SigningRoot = bytesutil.ToBytes32([]byte{2})
proposals[2].SigningRoot = bytesutil.ToBytes32([]byte{2})
proposals[0].HeaderRoot = bytesutil.ToBytes32([]byte{2})
proposals[1].HeaderRoot = bytesutil.ToBytes32([]byte{2})
proposals[2].HeaderRoot = bytesutil.ToBytes32([]byte{2})

doubleProposals, err = beaconDB.CheckDoubleBlockProposals(ctx, proposals)
require.NoError(t, err)
Expand Down Expand Up @@ -515,7 +515,7 @@ func createProposalWrapper(t *testing.T, slot primitives.Slot, proposerIndex pri
StateRoot: bytesutil.PadTo(signingRoot, 32),
BodyRoot: params.BeaconConfig().ZeroHash[:],
}
signRoot, err := header.HashTreeRoot()
headerRoot, err := header.HashTreeRoot()
if err != nil {
t.Fatal(err)
}
Expand All @@ -524,15 +524,16 @@ func createProposalWrapper(t *testing.T, slot primitives.Slot, proposerIndex pri
Header: header,
Signature: params.BeaconConfig().EmptySignature[:],
},
SigningRoot: signRoot,
HeaderRoot: headerRoot,
}
}

func createAttestationWrapper(source, target primitives.Epoch, indices []uint64, signingRoot []byte) *slashertypes.IndexedAttestationWrapper {
signRoot := bytesutil.ToBytes32(signingRoot)
if signingRoot == nil {
signRoot = params.BeaconConfig().ZeroHash
func createAttestationWrapper(source, target primitives.Epoch, indices []uint64, dataRootBytes []byte) *slashertypes.IndexedAttestationWrapper {
dataRoot := bytesutil.ToBytes32(dataRootBytes)
if dataRootBytes == nil {
dataRoot = params.BeaconConfig().ZeroHash
}

data := &ethpb.AttestationData{
BeaconBlockRoot: params.BeaconConfig().ZeroHash[:],
Source: &ethpb.Checkpoint{
Expand All @@ -544,13 +545,14 @@ func createAttestationWrapper(source, target primitives.Epoch, indices []uint64,
Root: params.BeaconConfig().ZeroHash[:],
},
}

return &slashertypes.IndexedAttestationWrapper{
IndexedAttestation: &ethpb.IndexedAttestation{
AttestingIndices: indices,
Data: data,
Signature: params.BeaconConfig().EmptySignature[:],
},
SigningRoot: signRoot,
DataRoot: dataRoot,
}
}

Expand Down
2 changes: 1 addition & 1 deletion beacon-chain/slasher/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ go_library(
"//beacon-chain/operations/slashings:go_default_library",
"//beacon-chain/slasher/types:go_default_library",
"//beacon-chain/startup:go_default_library",
"//beacon-chain/state:go_default_library",
"//beacon-chain/state/stategen:go_default_library",
"//beacon-chain/sync:go_default_library",
"//config/fieldparams:go_default_library",
Expand Down Expand Up @@ -78,6 +77,7 @@ go_test(
"//config/params:go_default_library",
"//consensus-types/primitives:go_default_library",
"//crypto/bls:go_default_library",
"//crypto/bls/common:go_default_library",
"//encoding/bytesutil:go_default_library",
"//proto/prysm/v1alpha1:go_default_library",
"//testing/assert:go_default_library",
Expand Down
118 changes: 74 additions & 44 deletions beacon-chain/slasher/chunks.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,15 @@ import (
slashertypes "github.com/prysmaticlabs/prysm/v4/beacon-chain/slasher/types"
"github.com/prysmaticlabs/prysm/v4/consensus-types/primitives"
ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
"github.com/sirupsen/logrus"
)

// A struct encapsulating input arguments to
// functions used for attester slashing detection and
// loading, saving, and updating min/max span chunks.
type chunkUpdateArgs struct {
kind slashertypes.ChunkKind
chunkIndex uint64
validatorChunkIndex uint64
currentEpoch primitives.Epoch
chunkIndex uint64
currentEpoch primitives.Epoch
}

// Chunker defines a struct which represents a slice containing a chunk for K different validator's
Expand Down Expand Up @@ -199,30 +198,45 @@ func (m *MinSpanChunksSlice) CheckSlashable(
)
}

if targetEpoch > minTarget {
existingAttRecord, err := slasherDB.AttestationRecordForValidator(
ctx, validatorIdx, minTarget,
)
if err != nil {
return nil, errors.Wrapf(
err, "could not get existing attestation record at target %d", minTarget,
)
}
if targetEpoch <= minTarget {
// The incoming attestation does not surround any existing ones.
return nil, nil
}

if existingAttRecord == nil {
return nil, nil
}
// The incoming attestation surrounds an existing one.
existingAttRecord, err := slasherDB.AttestationRecordForValidator(ctx, validatorIdx, minTarget)
if err != nil {
return nil, errors.Wrapf(err, "could not get existing attestation record at target %d", minTarget)
}

if sourceEpoch < existingAttRecord.IndexedAttestation.Data.Source.Epoch {
surroundingVotesTotal.Inc()
return &ethpb.AttesterSlashing{
Attestation_1: attestation.IndexedAttestation,
Attestation_2: existingAttRecord.IndexedAttestation,
}, nil
if existingAttRecord == nil {
// This case should normally not happen. If this happen, it means we previously
// recorded in our min/max DB an distance corresponding to an attestaiton, but WITHOUT
// recording the attestation itself. As a consequence, we say there is no surrounding vote,
// but we log an error.
fields := logrus.Fields{
"validatorIndex": validatorIdx,
"targetEpoch": minTarget,
}

log.WithFields(fields).Error("No existing attestation record found while a surrounding vote was detected.")
return nil, nil
}

if existingAttRecord.IndexedAttestation.Data.Source.Epoch <= sourceEpoch {
// This case should normally not happen, since if we have targetEpoch > minTarget,
// then there is at least one attestation we surround.
// However, it can happens if we have multiple attestation with the same target
// but with a different source. In this case, we have both a double vote AND a surround vote.
// The validator will be slashed for the double vote, and the surround vote will be ignored.
return nil, nil
}

return nil, nil
surroundingVotesTotal.Inc()
return &ethpb.AttesterSlashing{
Attestation_1: attestation.IndexedAttestation,
Attestation_2: existingAttRecord.IndexedAttestation,
}, nil
}

// CheckSlashable takes in a validator index and an incoming attestation
Expand Down Expand Up @@ -252,29 +266,45 @@ func (m *MaxSpanChunksSlice) CheckSlashable(
)
}

if targetEpoch < maxTarget {
existingAttRecord, err := slasherDB.AttestationRecordForValidator(
ctx, validatorIdx, maxTarget,
)
if err != nil {
return nil, errors.Wrapf(
err, "could not get existing attestation record at target %d", maxTarget,
)
}
if targetEpoch >= maxTarget {
// The incoming attestation is not surrounded by any existing ones.
return nil, nil
}

if existingAttRecord == nil {
return nil, nil
}
// The incoming attestation is surrounded by an existing one.
existingAttRecord, err := slasherDB.AttestationRecordForValidator(ctx, validatorIdx, maxTarget)
if err != nil {
return nil, errors.Wrapf(err, "could not get existing attestation record at target %d", maxTarget)
}

if existingAttRecord.IndexedAttestation.Data.Source.Epoch < sourceEpoch {
surroundedVotesTotal.Inc()
return &ethpb.AttesterSlashing{
Attestation_1: existingAttRecord.IndexedAttestation,
Attestation_2: attestation.IndexedAttestation,
}, nil
if existingAttRecord == nil {
// This case should normally not happen. If this happen, it means we previously
// recorded in our min/max DB an distance corresponding to an attestaiton, but WITHOUT
// recording the attestation itself. As a consequence, we say there is no surrounded vote,
// but we log an error.
fields := logrus.Fields{
"validatorIndex": validatorIdx,
"targetEpoch": maxTarget,
}

log.WithFields(fields).Error("No existing attestation record found while a surrounded vote was detected.")
return nil, nil
}

if existingAttRecord.IndexedAttestation.Data.Source.Epoch >= sourceEpoch {
// This case should normally not happen, since if we have targetEpoch < maxTarget,
// then there is at least one attestation that surrounds us.
// However, it can happens if we have multiple attestation with the same target
// but with a different source. In this case, we have both a double vote AND a surround vote.
// The validator will be slashed for the double vote, and the surround vote will be ignored.
return nil, nil
}
return nil, nil

surroundedVotesTotal.Inc()
return &ethpb.AttesterSlashing{
Attestation_1: existingAttRecord.IndexedAttestation,
Attestation_2: attestation.IndexedAttestation,
}, nil
}

// Update a min span chunk for a validator index starting at the current epoch, e_c, then updating
Expand Down Expand Up @@ -492,13 +522,13 @@ func (m *MinSpanChunksSlice) NextChunkStartEpoch(startEpoch primitives.Epoch) pr
// max_spans_val_i = [[-, -, -], [-, -, -], [-, -, -]]
//
// If C = chunkSize is 3 epochs per chunk, and we input start epoch of chunk 1 which is 3. The next start
// epoch is the start epoch of chunk 2, which is epoch 4. This is computed as:
// epoch is the start epoch of chunk 2, which is epoch 6. This is computed as:
//
// first_epoch(chunkIndex(startEpoch)+1)
// first_epoch(chunkIndex(3)+1)
// first_epoch(1 + 1)
// first_epoch(2)
// 4
// 6
func (m *MaxSpanChunksSlice) NextChunkStartEpoch(startEpoch primitives.Epoch) primitives.Epoch {
return m.params.firstEpoch(m.params.chunkIndex(startEpoch) + 1)
}
Expand Down
Loading
Loading