mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-10 07:58:22 -05:00
Slasher Min/Max Chunk Logic (#9673)
* slasher chunks code * slasher chunks code * avoid using shared * testing helper * slasher gaz * radek comments * preston feedback
This commit is contained in:
@@ -11,6 +11,8 @@ go_library(
|
||||
],
|
||||
deps = [
|
||||
"//beacon-chain/db:go_default_library",
|
||||
"//beacon-chain/db/iface:go_default_library",
|
||||
"//beacon-chain/db/kv:go_default_library",
|
||||
"//beacon-chain/db/slasherkv:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/prysmaticlabs/prysm/beacon-chain/db"
|
||||
"github.com/prysmaticlabs/prysm/beacon-chain/db/iface"
|
||||
"github.com/prysmaticlabs/prysm/beacon-chain/db/kv"
|
||||
"github.com/prysmaticlabs/prysm/beacon-chain/db/slasherkv"
|
||||
)
|
||||
|
||||
// SetupDB instantiates and returns database backed by key value store.
|
||||
@@ -23,3 +25,17 @@ func SetupDB(t testing.TB) db.Database {
|
||||
})
|
||||
return s
|
||||
}
|
||||
|
||||
// SetupSlasherDB --
|
||||
func SetupSlasherDB(t testing.TB) iface.SlasherDatabase {
|
||||
s, err := slasherkv.NewKVStore(context.Background(), t.TempDir(), &slasherkv.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := s.Close(); err != nil {
|
||||
t.Fatalf("failed to close database: %v", err)
|
||||
}
|
||||
})
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -3,19 +3,25 @@ load("@prysm//tools/go:def.bzl", "go_library", "go_test")
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"chunks.go",
|
||||
"doc.go",
|
||||
"helpers.go",
|
||||
"metrics.go",
|
||||
"params.go",
|
||||
"service.go",
|
||||
],
|
||||
importpath = "github.com/prysmaticlabs/prysm/beacon-chain/slasher",
|
||||
visibility = ["//beacon-chain:__subpackages__"],
|
||||
deps = [
|
||||
"//beacon-chain/db:go_default_library",
|
||||
"//beacon-chain/slasher/types:go_default_library",
|
||||
"//config/params:go_default_library",
|
||||
"//container/slice:go_default_library",
|
||||
"//proto/prysm/v1alpha1:go_default_library",
|
||||
"@com_github_ferranbt_fastssz//:go_default_library",
|
||||
"@com_github_pkg_errors//:go_default_library",
|
||||
"@com_github_prometheus_client_golang//prometheus:go_default_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",
|
||||
],
|
||||
@@ -24,11 +30,13 @@ go_library(
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"chunks_test.go",
|
||||
"helpers_test.go",
|
||||
"params_test.go",
|
||||
],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//beacon-chain/db/testing:go_default_library",
|
||||
"//beacon-chain/slasher/types:go_default_library",
|
||||
"//config/params:go_default_library",
|
||||
"//encoding/bytesutil:go_default_library",
|
||||
|
||||
554
beacon-chain/slasher/chunks.go
Normal file
554
beacon-chain/slasher/chunks.go
Normal file
@@ -0,0 +1,554 @@
|
||||
package slasher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
types "github.com/prysmaticlabs/eth2-types"
|
||||
"github.com/prysmaticlabs/prysm/beacon-chain/db"
|
||||
slashertypes "github.com/prysmaticlabs/prysm/beacon-chain/slasher/types"
|
||||
ethpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1"
|
||||
)
|
||||
|
||||
// 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 types.Epoch
|
||||
}
|
||||
|
||||
// Chunker defines a struct which represents a slice containing a chunk for K different validator's
|
||||
// min spans used for surround vote detection in slasher. The interface defines methods used to check
|
||||
// if an attestation is slashable for a validator index based on the contents of
|
||||
// the chunk as well as the ability to update the data in the chunk with incoming information.
|
||||
type Chunker interface {
|
||||
NeutralElement() uint16
|
||||
Chunk() []uint16
|
||||
CheckSlashable(
|
||||
ctx context.Context,
|
||||
slasherDB db.SlasherDatabase,
|
||||
validatorIdx types.ValidatorIndex,
|
||||
attestation *slashertypes.IndexedAttestationWrapper,
|
||||
) (*ethpb.AttesterSlashing, error)
|
||||
Update(
|
||||
args *chunkUpdateArgs,
|
||||
validatorIndex types.ValidatorIndex,
|
||||
startEpoch,
|
||||
newTargetEpoch types.Epoch,
|
||||
) (keepGoing bool, err error)
|
||||
StartEpoch(sourceEpoch, currentEpoch types.Epoch) (epoch types.Epoch, exists bool)
|
||||
NextChunkStartEpoch(startEpoch types.Epoch) types.Epoch
|
||||
}
|
||||
|
||||
// MinSpanChunksSlice represents a slice containing a chunk for K different validator's min spans.
|
||||
//
|
||||
// For a given epoch, e, and attestations a validator index has produced, atts,
|
||||
// min_spans[e] is defined as min((att.target.epoch - e) for att in attestations)
|
||||
// where att.source.epoch > e. That is, it is the minimum distance between the
|
||||
// specified epoch and all attestation target epochs a validator has created
|
||||
// where att.source.epoch > e.
|
||||
//
|
||||
// Under ideal network conditions, where every target epoch immediately follows its source,
|
||||
// min spans for a validator will look as follows:
|
||||
//
|
||||
// min_spans = [2, 2, 2, ..., 2]
|
||||
//
|
||||
// Next, we can chunk this list of min spans into chunks of length C. For C = 2, for example:
|
||||
//
|
||||
// chunk0 chunk1 chunkN
|
||||
// { } { } { }
|
||||
// chunked_min_spans = [[2, 2], [2, 2], ..., [2, 2]]
|
||||
//
|
||||
// Finally, we can store each chunk index for K validators into a single flat slice. For K = 3:
|
||||
//
|
||||
// val0 val1 val2
|
||||
// { } { } { }
|
||||
// chunk_0_for_validators_0_to_2 = [[2, 2], [2, 2], [2, 2]]
|
||||
//
|
||||
// val0 val1 val2
|
||||
// { } { } { }
|
||||
// chunk_1_for_validators_0_to_2 = [[2, 2], [2, 2], [2, 2]]
|
||||
//
|
||||
// ...
|
||||
//
|
||||
// val0 val1 val2
|
||||
// { } { } { }
|
||||
// chunk_N_for_validators_0_to_2 = [[2, 2], [2, 2], [2, 2]]
|
||||
//
|
||||
// MinSpanChunksSlice represents the data structure above for a single chunk index.
|
||||
type MinSpanChunksSlice struct {
|
||||
params *Parameters
|
||||
data []uint16
|
||||
}
|
||||
|
||||
// MaxSpanChunksSlice represents the same data structure as MinSpanChunksSlice however
|
||||
// keeps track of validator max spans for slashing detection instead.
|
||||
type MaxSpanChunksSlice struct {
|
||||
params *Parameters
|
||||
data []uint16
|
||||
}
|
||||
|
||||
// EmptyMinSpanChunksSlice initializes a min span chunk of length C*K for
|
||||
// C = chunkSize and K = validatorChunkSize filled with neutral elements.
|
||||
// For min spans, the neutral element is `undefined`, represented by MaxUint16.
|
||||
func EmptyMinSpanChunksSlice(params *Parameters) *MinSpanChunksSlice {
|
||||
m := &MinSpanChunksSlice{
|
||||
params: params,
|
||||
}
|
||||
data := make([]uint16, params.chunkSize*params.validatorChunkSize)
|
||||
for i := 0; i < len(data); i++ {
|
||||
data[i] = m.NeutralElement()
|
||||
}
|
||||
m.data = data
|
||||
return m
|
||||
}
|
||||
|
||||
// EmptyMaxSpanChunksSlice initializes a max span chunk of length C*K for
|
||||
// C = chunkSize and K = validatorChunkSize filled with neutral elements.
|
||||
// For max spans, the neutral element is 0.
|
||||
func EmptyMaxSpanChunksSlice(params *Parameters) *MaxSpanChunksSlice {
|
||||
m := &MaxSpanChunksSlice{
|
||||
params: params,
|
||||
}
|
||||
data := make([]uint16, params.chunkSize*params.validatorChunkSize)
|
||||
for i := 0; i < len(data); i++ {
|
||||
data[i] = m.NeutralElement()
|
||||
}
|
||||
m.data = data
|
||||
return m
|
||||
}
|
||||
|
||||
// MinChunkSpansSliceFrom initializes a min span chunks slice from a slice of uint16 values.
|
||||
// Returns an error if the slice is not of length C*K for C = chunkSize and K = validatorChunkSize.
|
||||
func MinChunkSpansSliceFrom(params *Parameters, chunk []uint16) (*MinSpanChunksSlice, error) {
|
||||
requiredLen := params.chunkSize * params.validatorChunkSize
|
||||
if uint64(len(chunk)) != requiredLen {
|
||||
return nil, fmt.Errorf("chunk has wrong length, %d, expected %d", len(chunk), requiredLen)
|
||||
}
|
||||
return &MinSpanChunksSlice{
|
||||
params: params,
|
||||
data: chunk,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MaxChunkSpansSliceFrom initializes a max span chunks slice from a slice of uint16 values.
|
||||
// Returns an error if the slice is not of length C*K for C = chunkSize and K = validatorChunkSize.
|
||||
func MaxChunkSpansSliceFrom(params *Parameters, chunk []uint16) (*MaxSpanChunksSlice, error) {
|
||||
requiredLen := params.chunkSize * params.validatorChunkSize
|
||||
if uint64(len(chunk)) != requiredLen {
|
||||
return nil, fmt.Errorf("chunk has wrong length, %d, expected %d", len(chunk), requiredLen)
|
||||
}
|
||||
return &MaxSpanChunksSlice{
|
||||
params: params,
|
||||
data: chunk,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NeutralElement for a min span chunks slice is undefined, in this case
|
||||
// using MaxUint16 as a sane value given it is impossible we reach it.
|
||||
func (m *MinSpanChunksSlice) NeutralElement() uint16 {
|
||||
return math.MaxUint16
|
||||
}
|
||||
|
||||
// NeutralElement for a max span chunks slice is 0.
|
||||
func (m *MaxSpanChunksSlice) NeutralElement() uint16 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Chunk returns the underlying slice of uint16's for the min chunks slice.
|
||||
func (m *MinSpanChunksSlice) Chunk() []uint16 {
|
||||
return m.data
|
||||
}
|
||||
|
||||
// Chunk returns the underlying slice of uint16's for the max chunks slice.
|
||||
func (m *MaxSpanChunksSlice) Chunk() []uint16 {
|
||||
return m.data
|
||||
}
|
||||
|
||||
// CheckSlashable takes in a validator index and an incoming attestation
|
||||
// and checks if the validator is slashable depending on the data
|
||||
// within the min span chunks slice. Recall that for an incoming attestation, B, and an
|
||||
// existing attestation, A:
|
||||
//
|
||||
// B surrounds A if and only if B.target > min_spans[B.source]
|
||||
//
|
||||
// That is, this condition is sufficient to check if an incoming attestation
|
||||
// is surrounding a previous one. We also check if we indeed have an existing
|
||||
// attestation record in the database if the condition holds true in order
|
||||
// to be confident of a slashable offense.
|
||||
func (m *MinSpanChunksSlice) CheckSlashable(
|
||||
ctx context.Context,
|
||||
slasherDB db.SlasherDatabase,
|
||||
validatorIdx types.ValidatorIndex,
|
||||
attestation *slashertypes.IndexedAttestationWrapper,
|
||||
) (*ethpb.AttesterSlashing, error) {
|
||||
sourceEpoch := attestation.IndexedAttestation.Data.Source.Epoch
|
||||
targetEpoch := attestation.IndexedAttestation.Data.Target.Epoch
|
||||
minTarget, err := chunkDataAtEpoch(m.params, m.data, validatorIdx, sourceEpoch)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(
|
||||
err, "could not get min target for validator %d at epoch %d", validatorIdx, sourceEpoch,
|
||||
)
|
||||
}
|
||||
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 existingAttRecord != nil {
|
||||
if sourceEpoch < existingAttRecord.IndexedAttestation.Data.Source.Epoch {
|
||||
surroundingVotesTotal.Inc()
|
||||
return ðpb.AttesterSlashing{
|
||||
Attestation_1: attestation.IndexedAttestation,
|
||||
Attestation_2: existingAttRecord.IndexedAttestation,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// CheckSlashable takes in a validator index and an incoming attestation
|
||||
// and checks if the validator is slashable depending on the data
|
||||
// within the max span chunks slice. Recall that for an incoming attestation, B, and an
|
||||
// existing attestation, A:
|
||||
//
|
||||
// B surrounds A if and only if B.target < max_spans[B.source]
|
||||
//
|
||||
// That is, this condition is sufficient to check if an incoming attestation
|
||||
// is surrounded by a previous one. We also check if we indeed have an existing
|
||||
// attestation record in the database if the condition holds true in order
|
||||
// to be confident of a slashable offense.
|
||||
func (m *MaxSpanChunksSlice) CheckSlashable(
|
||||
ctx context.Context,
|
||||
slasherDB db.SlasherDatabase,
|
||||
validatorIdx types.ValidatorIndex,
|
||||
attestation *slashertypes.IndexedAttestationWrapper,
|
||||
) (*ethpb.AttesterSlashing, error) {
|
||||
sourceEpoch := attestation.IndexedAttestation.Data.Source.Epoch
|
||||
targetEpoch := attestation.IndexedAttestation.Data.Target.Epoch
|
||||
maxTarget, err := chunkDataAtEpoch(m.params, m.data, validatorIdx, sourceEpoch)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(
|
||||
err, "could not get max target for validator %d at epoch %d", validatorIdx, sourceEpoch,
|
||||
)
|
||||
}
|
||||
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 existingAttRecord != nil {
|
||||
if existingAttRecord.IndexedAttestation.Data.Source.Epoch < sourceEpoch {
|
||||
surroundedVotesTotal.Inc()
|
||||
return ðpb.AttesterSlashing{
|
||||
Attestation_1: existingAttRecord.IndexedAttestation,
|
||||
Attestation_2: attestation.IndexedAttestation,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Update a min span chunk for a validator index starting at the current epoch, e_c, then updating
|
||||
// down to e_c - H where H is the historyLength we keep for each span. This historyLength
|
||||
// corresponds to the weak subjectivity period of Ethereum consensus.
|
||||
// This means our updates are done in a sliding window manner. For example, if the current epoch
|
||||
// is 20 and the historyLength is 12, then we will update every value for the validator's min span
|
||||
// from epoch 20 down to epoch 9.
|
||||
//
|
||||
// Recall that for an epoch, e, min((att.target - e) for att in attestations where att.source > e)
|
||||
// That is, it is the minimum distance between the specified epoch and all attestation
|
||||
// target epochs a validator has created where att.source.epoch > e.
|
||||
//
|
||||
// Recall that a MinSpanChunksSlice struct represents a single slice for a chunk index
|
||||
// from the collection below:
|
||||
//
|
||||
// val0 val1 val2
|
||||
// { } { } { }
|
||||
// chunk_0_for_validators_0_to_2 = [[2, 2], [2, 2], [2, 2]]
|
||||
//
|
||||
// val0 val1 val2
|
||||
// { } { } { }
|
||||
// chunk_1_for_validators_0_to_2 = [[2, 2], [2, 2], [2, 2]]
|
||||
//
|
||||
// ...
|
||||
//
|
||||
// val0 val1 val2
|
||||
// { } { } { }
|
||||
// chunk_N_for_validators_0_to_2 = [[2, 2], [2, 2], [2, 2]]
|
||||
//
|
||||
// Let's take a look at how this update will look for a real set of min span chunk:
|
||||
// For the purposes of a simple example, let's set H = 2, meaning a min span
|
||||
// will hold 2 epochs worth of attesting history. Then we set C = 2 meaning we will
|
||||
// chunk the min span into arrays each of length 2.
|
||||
//
|
||||
// So assume we get an epoch 4 and validator 0, then, we need to update every epoch in the span from
|
||||
// 4 down to 3. First, we find out which chunk epoch 4 falls into, which is calculated as:
|
||||
// chunk_idx = (epoch % H) / C = (4 % 2) / 2 = 0
|
||||
//
|
||||
//
|
||||
// val0 val1 val2
|
||||
// { } { } { }
|
||||
// chunk_0_for_validators_0_to_3 = [[2, 2], [2, 2], [2, 2]]
|
||||
// |
|
||||
// |-> epoch 4 for validator 0
|
||||
//
|
||||
// Next up, we proceed with the update process for validator index 0, starting at epoch 4
|
||||
// all the way down to epoch 2. We will need to go down the array as far as we can get. If the
|
||||
// lowest epoch we need to update is < the lowest epoch of a chunk, we need to proceed to
|
||||
// a different chunk index.
|
||||
//
|
||||
// Once we finish updating a chunk, we need to move on to the next chunk. This function
|
||||
// returns a boolean named keepGoing which allows the caller to determine if we should
|
||||
// continue and update another chunk index. We stop whenever we reach the min epoch we need
|
||||
// to update. In our example, we stop at 2, which is still part of chunk 0, so no need
|
||||
// to jump to another min span chunks slice to perform updates.
|
||||
func (m *MinSpanChunksSlice) Update(
|
||||
args *chunkUpdateArgs,
|
||||
validatorIndex types.ValidatorIndex,
|
||||
startEpoch,
|
||||
newTargetEpoch types.Epoch,
|
||||
) (keepGoing bool, err error) {
|
||||
// The lowest epoch we need to update.
|
||||
minEpoch := types.Epoch(0)
|
||||
if args.currentEpoch > (m.params.historyLength - 1) {
|
||||
minEpoch = args.currentEpoch - (m.params.historyLength - 1)
|
||||
}
|
||||
epochInChunk := startEpoch
|
||||
// We go down the chunk for the validator, updating every value starting at start_epoch down to min_epoch.
|
||||
// As long as the epoch, e, in the same chunk index and e >= min_epoch, we proceed with
|
||||
// a for loop.
|
||||
for m.params.chunkIndex(epochInChunk) == args.chunkIndex && epochInChunk >= minEpoch {
|
||||
var chunkTarget types.Epoch
|
||||
chunkTarget, err = chunkDataAtEpoch(m.params, m.data, validatorIndex, epochInChunk)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "could not get chunk data at epoch %d", epochInChunk)
|
||||
return
|
||||
}
|
||||
// If the newly incoming value is < the existing value, we update
|
||||
// the data in the min span to meet with its definition.
|
||||
if newTargetEpoch < chunkTarget {
|
||||
if err = setChunkDataAtEpoch(m.params, m.data, validatorIndex, epochInChunk, newTargetEpoch); err != nil {
|
||||
err = errors.Wrapf(err, "could not set chunk data at epoch %d", epochInChunk)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// We can stop because spans are guaranteed to be minimums and
|
||||
// if we did not meet the minimum condition, there is nothing to update.
|
||||
return
|
||||
}
|
||||
if epochInChunk > 0 {
|
||||
epochInChunk -= 1
|
||||
}
|
||||
}
|
||||
// We should keep going and update the previous chunk if we are yet to reach
|
||||
// the minimum epoch required for the update procedure.
|
||||
keepGoing = epochInChunk >= minEpoch
|
||||
return
|
||||
}
|
||||
|
||||
// Update a max span chunk for a validator index starting at a given start epoch, e_c, then updating
|
||||
// up to the current epoch according to the definition of max spans. If we need to continue updating
|
||||
// a next chunk, this function returns a boolean letting the caller know it should keep going. To understand
|
||||
// more about how update exactly works, refer to the detailed documentation for the Update function for
|
||||
// MinSpanChunksSlice.
|
||||
func (m *MaxSpanChunksSlice) Update(
|
||||
args *chunkUpdateArgs,
|
||||
validatorIndex types.ValidatorIndex,
|
||||
startEpoch,
|
||||
newTargetEpoch types.Epoch,
|
||||
) (keepGoing bool, err error) {
|
||||
epochInChunk := startEpoch
|
||||
// We go down the chunk for the validator, updating every value starting at start_epoch up to
|
||||
// and including the current epoch. As long as the epoch, e, is in the same chunk index and e <= currentEpoch,
|
||||
// we proceed with a for loop.
|
||||
for m.params.chunkIndex(epochInChunk) == args.chunkIndex && epochInChunk <= args.currentEpoch {
|
||||
var chunkTarget types.Epoch
|
||||
chunkTarget, err = chunkDataAtEpoch(m.params, m.data, validatorIndex, epochInChunk)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "could not get chunk data at epoch %d", epochInChunk)
|
||||
return
|
||||
}
|
||||
// If the newly incoming value is > the existing value, we update
|
||||
// the data in the max span to meet with its definition.
|
||||
if newTargetEpoch > chunkTarget {
|
||||
if err = setChunkDataAtEpoch(m.params, m.data, validatorIndex, epochInChunk, newTargetEpoch); err != nil {
|
||||
err = errors.Wrapf(err, "could not set chunk data at epoch %d", epochInChunk)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// We can stop because spans are guaranteed to be maxima and
|
||||
// if we did not meet the condition, there is nothing to update.
|
||||
return
|
||||
}
|
||||
epochInChunk++
|
||||
}
|
||||
// If the epoch to update now lies beyond the current chunk, then
|
||||
// continue to the next chunk to update it.
|
||||
keepGoing = epochInChunk <= args.currentEpoch
|
||||
return
|
||||
}
|
||||
|
||||
// StartEpoch given a source epoch and current epoch, determines the start epoch of
|
||||
// a min span chunk for use in chunk updates. To compute this value, we look at the difference between
|
||||
// H = historyLength and the current epoch. Then, we check if the source epoch > difference. If so,
|
||||
// then the start epoch is source epoch - 1. Otherwise, we return to the caller a boolean signifying
|
||||
// the input argumets are invalid for the chunk and the start epoch does not exist.
|
||||
func (m *MinSpanChunksSlice) StartEpoch(
|
||||
sourceEpoch, currentEpoch types.Epoch,
|
||||
) (epoch types.Epoch, exists bool) {
|
||||
// Given min span chunks are used for detecting surrounding votes, we have no need
|
||||
// for a start epoch of the chunk if the source epoch is 0 in the input arguments.
|
||||
// To further clarify, min span chunks are updated in reverse order [a, b, c, d] where
|
||||
// if the start epoch is d, then we go down the chunk updating everything from d, c, b, to
|
||||
// a. If the source epoch is 0, this would correspond to a, which means there is nothing
|
||||
// more to update.
|
||||
if sourceEpoch == 0 {
|
||||
return
|
||||
}
|
||||
var difference types.Epoch
|
||||
if currentEpoch > m.params.historyLength {
|
||||
difference = currentEpoch - m.params.historyLength
|
||||
}
|
||||
if sourceEpoch <= difference {
|
||||
return
|
||||
}
|
||||
epoch = sourceEpoch.Sub(1)
|
||||
exists = true
|
||||
return
|
||||
}
|
||||
|
||||
// StartEpoch given a source epoch and current epoch, determines the start epoch of
|
||||
// a max span chunk for use in chunk updates. The source epoch cannot be >= the current epoch.
|
||||
func (m *MaxSpanChunksSlice) StartEpoch(
|
||||
sourceEpoch, currentEpoch types.Epoch,
|
||||
) (epoch types.Epoch, exists bool) {
|
||||
if sourceEpoch >= currentEpoch {
|
||||
return
|
||||
}
|
||||
// Given max spans is a list of max targets for source epochs, the precondition is that
|
||||
// every attestation's source epoch must be < than its target epoch. So the start epoch
|
||||
// for updates is given as source epoch + 1.
|
||||
epoch = sourceEpoch.Add(1)
|
||||
exists = true
|
||||
return
|
||||
}
|
||||
|
||||
// NextChunkStartEpoch given an epoch, determines the start epoch of the next chunk. For min
|
||||
// span chunks, this will be the last epoch of chunk index = (current chunk - 1). For example:
|
||||
//
|
||||
// chunk0 chunk1 chunk2
|
||||
// | | |
|
||||
// max_spans_val_i = [[-, -, -], [-, -, -], [-, -, -]]
|
||||
//
|
||||
// If C = chunkSize is 3 epochs per chunk, and we input start epoch of chunk 1 which is 3 then the next start
|
||||
// epoch is the last epoch of chunk 0, which is epoch 2. This is computed as:
|
||||
//
|
||||
// last_epoch(chunkIndex(startEpoch)-1)
|
||||
// last_epoch(chunkIndex(3) - 1)
|
||||
// last_epoch(1 - 1)
|
||||
// last_epoch(0)
|
||||
// 2
|
||||
func (m *MinSpanChunksSlice) NextChunkStartEpoch(startEpoch types.Epoch) types.Epoch {
|
||||
prevChunkIdx := m.params.chunkIndex(startEpoch)
|
||||
if prevChunkIdx > 0 {
|
||||
prevChunkIdx--
|
||||
}
|
||||
return m.params.lastEpoch(prevChunkIdx)
|
||||
}
|
||||
|
||||
// NextChunkStartEpoch given an epoch, determines the start epoch of the next chunk. For max
|
||||
// span chunks, this will be the start epoch of chunk index = (current chunk + 1). For example:
|
||||
//
|
||||
// chunk0 chunk1 chunk2
|
||||
// | | |
|
||||
// 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:
|
||||
//
|
||||
// first_epoch(chunkIndex(startEpoch)+1)
|
||||
// first_epoch(chunkIndex(3)+1)
|
||||
// first_epoch(1 + 1)
|
||||
// first_epoch(2)
|
||||
// 4
|
||||
func (m *MaxSpanChunksSlice) NextChunkStartEpoch(startEpoch types.Epoch) types.Epoch {
|
||||
return m.params.firstEpoch(m.params.chunkIndex(startEpoch) + 1)
|
||||
}
|
||||
|
||||
// Given a validator index and epoch, retrieves the target epoch at its specific
|
||||
// index for the validator index and epoch in a min/max span chunk.
|
||||
func chunkDataAtEpoch(
|
||||
params *Parameters, chunk []uint16, validatorIdx types.ValidatorIndex, epoch types.Epoch,
|
||||
) (types.Epoch, error) {
|
||||
requiredLen := params.chunkSize * params.validatorChunkSize
|
||||
if uint64(len(chunk)) != requiredLen {
|
||||
return 0, fmt.Errorf("chunk has wrong length, %d, expected %d", len(chunk), requiredLen)
|
||||
}
|
||||
cellIdx := params.cellIndex(validatorIdx, epoch)
|
||||
if cellIdx >= uint64(len(chunk)) {
|
||||
return 0, fmt.Errorf("cell index %d out of bounds (len(chunk) = %d)", cellIdx, len(chunk))
|
||||
}
|
||||
distance := chunk[cellIdx]
|
||||
return epoch.Add(uint64(distance)), nil
|
||||
}
|
||||
|
||||
// Updates the value at a specific index in a chunk for a validator index + epoch
|
||||
// pair given a target epoch. Recall that for min spans, each element in a chunk
|
||||
// is the minimum distance between the a given epoch, e, and all attestation target epochs
|
||||
// a validator has created where att.source.epoch > e.
|
||||
func setChunkDataAtEpoch(
|
||||
params *Parameters,
|
||||
chunk []uint16,
|
||||
validatorIdx types.ValidatorIndex,
|
||||
epochInChunk,
|
||||
targetEpoch types.Epoch,
|
||||
) error {
|
||||
distance, err := epochDistance(targetEpoch, epochInChunk)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return setChunkRawDistance(params, chunk, validatorIdx, epochInChunk, distance)
|
||||
}
|
||||
|
||||
// Updates the value at a specific index in a chunk for a validator index and epoch
|
||||
// to a specified, raw distance value.
|
||||
func setChunkRawDistance(
|
||||
params *Parameters,
|
||||
chunk []uint16,
|
||||
validatorIdx types.ValidatorIndex,
|
||||
epochInChunk types.Epoch,
|
||||
distance uint16,
|
||||
) error {
|
||||
cellIdx := params.cellIndex(validatorIdx, epochInChunk)
|
||||
if cellIdx >= uint64(len(chunk)) {
|
||||
return fmt.Errorf("cell index %d out of bounds (len(chunk) = %d)", cellIdx, len(chunk))
|
||||
}
|
||||
chunk[cellIdx] = distance
|
||||
return nil
|
||||
}
|
||||
|
||||
// Computes a distance between two epochs. Given the result stored in
|
||||
// min/max spans is at maximum WEAK_SUBJECTIVITY_PERIOD, we are guaranteed the
|
||||
// distance can be represented as a uint16 safely.
|
||||
func epochDistance(epoch, baseEpoch types.Epoch) (uint16, error) {
|
||||
if baseEpoch > epoch {
|
||||
return 0, fmt.Errorf("base epoch %d cannot be less than epoch %d", baseEpoch, epoch)
|
||||
}
|
||||
return uint16(epoch.Sub(uint64(baseEpoch))), nil
|
||||
}
|
||||
668
beacon-chain/slasher/chunks_test.go
Normal file
668
beacon-chain/slasher/chunks_test.go
Normal file
@@ -0,0 +1,668 @@
|
||||
package slasher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
types "github.com/prysmaticlabs/eth2-types"
|
||||
dbtest "github.com/prysmaticlabs/prysm/beacon-chain/db/testing"
|
||||
slashertypes "github.com/prysmaticlabs/prysm/beacon-chain/slasher/types"
|
||||
ethpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1"
|
||||
"github.com/prysmaticlabs/prysm/testing/assert"
|
||||
"github.com/prysmaticlabs/prysm/testing/require"
|
||||
)
|
||||
|
||||
var (
|
||||
_ = Chunker(&MinSpanChunksSlice{})
|
||||
_ = Chunker(&MaxSpanChunksSlice{})
|
||||
)
|
||||
|
||||
func TestMinSpanChunksSlice_Chunk(t *testing.T) {
|
||||
chunk := EmptyMinSpanChunksSlice(&Parameters{
|
||||
chunkSize: 2,
|
||||
validatorChunkSize: 2,
|
||||
})
|
||||
wanted := []uint16{math.MaxUint16, math.MaxUint16, math.MaxUint16, math.MaxUint16}
|
||||
require.DeepEqual(t, wanted, chunk.Chunk())
|
||||
}
|
||||
|
||||
func TestMaxSpanChunksSlice_Chunk(t *testing.T) {
|
||||
chunk := EmptyMaxSpanChunksSlice(&Parameters{
|
||||
chunkSize: 2,
|
||||
validatorChunkSize: 2,
|
||||
})
|
||||
wanted := []uint16{0, 0, 0, 0}
|
||||
require.DeepEqual(t, wanted, chunk.Chunk())
|
||||
}
|
||||
|
||||
func TestMinSpanChunksSlice_NeutralElement(t *testing.T) {
|
||||
chunk := EmptyMinSpanChunksSlice(&Parameters{})
|
||||
require.Equal(t, uint16(math.MaxUint16), chunk.NeutralElement())
|
||||
}
|
||||
|
||||
func TestMaxSpanChunksSlice_NeutralElement(t *testing.T) {
|
||||
chunk := EmptyMaxSpanChunksSlice(&Parameters{})
|
||||
require.Equal(t, uint16(0), chunk.NeutralElement())
|
||||
}
|
||||
|
||||
func TestMinSpanChunksSlice_MinChunkSpanFrom(t *testing.T) {
|
||||
params := &Parameters{
|
||||
chunkSize: 3,
|
||||
validatorChunkSize: 2,
|
||||
}
|
||||
_, err := MinChunkSpansSliceFrom(params, []uint16{})
|
||||
require.ErrorContains(t, "chunk has wrong length", err)
|
||||
|
||||
data := []uint16{2, 2, 2, 2, 2, 2}
|
||||
chunk, err := MinChunkSpansSliceFrom(&Parameters{
|
||||
chunkSize: 3,
|
||||
validatorChunkSize: 2,
|
||||
}, data)
|
||||
require.NoError(t, err)
|
||||
require.DeepEqual(t, data, chunk.Chunk())
|
||||
}
|
||||
|
||||
func TestMaxSpanChunksSlice_MaxChunkSpanFrom(t *testing.T) {
|
||||
params := &Parameters{
|
||||
chunkSize: 3,
|
||||
validatorChunkSize: 2,
|
||||
}
|
||||
_, err := MaxChunkSpansSliceFrom(params, []uint16{})
|
||||
require.ErrorContains(t, "chunk has wrong length", err)
|
||||
|
||||
data := []uint16{2, 2, 2, 2, 2, 2}
|
||||
chunk, err := MaxChunkSpansSliceFrom(&Parameters{
|
||||
chunkSize: 3,
|
||||
validatorChunkSize: 2,
|
||||
}, data)
|
||||
require.NoError(t, err)
|
||||
require.DeepEqual(t, data, chunk.Chunk())
|
||||
}
|
||||
|
||||
func TestMinSpanChunksSlice_CheckSlashable(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
slasherDB := dbtest.SetupSlasherDB(t)
|
||||
params := &Parameters{
|
||||
chunkSize: 3,
|
||||
validatorChunkSize: 2,
|
||||
historyLength: 3,
|
||||
}
|
||||
validatorIdx := types.ValidatorIndex(1)
|
||||
source := types.Epoch(1)
|
||||
target := types.Epoch(2)
|
||||
att := createAttestationWrapper(t, source, target, nil, nil)
|
||||
|
||||
// A faulty chunk should lead to error.
|
||||
chunk := &MinSpanChunksSlice{
|
||||
params: params,
|
||||
data: []uint16{},
|
||||
}
|
||||
_, err := chunk.CheckSlashable(ctx, nil, validatorIdx, att)
|
||||
require.ErrorContains(t, "could not get min target for validator", err)
|
||||
|
||||
// We initialize a proper slice with 2 chunks with chunk size 3, 2 validators, and
|
||||
// a history length of 3 representing a perfect attesting history.
|
||||
//
|
||||
// val0 val1
|
||||
// { } { }
|
||||
// [2, 2, 2, 2, 2, 2]
|
||||
data := []uint16{2, 2, 2, 2, 2, 2}
|
||||
chunk, err = MinChunkSpansSliceFrom(params, data)
|
||||
require.NoError(t, err)
|
||||
|
||||
// An attestation with source 1 and target 2 should not be slashable
|
||||
// based on our min chunk for either validator.
|
||||
slashing, err := chunk.CheckSlashable(ctx, slasherDB, validatorIdx, att)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, (*ethpb.AttesterSlashing)(nil), slashing)
|
||||
|
||||
slashing, err = chunk.CheckSlashable(ctx, slasherDB, validatorIdx.Sub(1), att)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, (*ethpb.AttesterSlashing)(nil), slashing)
|
||||
|
||||
// Next up we initialize an empty chunks slice and mark an attestation
|
||||
// with (source 1, target 2) as attested.
|
||||
chunk = EmptyMinSpanChunksSlice(params)
|
||||
source = types.Epoch(1)
|
||||
target = types.Epoch(2)
|
||||
att = createAttestationWrapper(t, source, target, nil, nil)
|
||||
chunkIdx := uint64(0)
|
||||
startEpoch := target
|
||||
currentEpoch := target
|
||||
args := &chunkUpdateArgs{
|
||||
chunkIndex: chunkIdx,
|
||||
currentEpoch: currentEpoch,
|
||||
}
|
||||
_, err = chunk.Update(args, validatorIdx, startEpoch, target)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Next up, we create a surrounding vote, but it should NOT be slashable
|
||||
// because we DO NOT have an existing attestation record in our database at the min target epoch.
|
||||
source = types.Epoch(0)
|
||||
target = types.Epoch(3)
|
||||
surroundingVote := createAttestationWrapper(t, source, target, nil, nil)
|
||||
|
||||
slashing, err = chunk.CheckSlashable(ctx, slasherDB, validatorIdx, surroundingVote)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, (*ethpb.AttesterSlashing)(nil), slashing)
|
||||
|
||||
// Next up, we save the old attestation record, then check if the
|
||||
// surrounding vote is indeed slashable.
|
||||
attData := att.IndexedAttestation.Data
|
||||
attRecord := createAttestationWrapper(t, attData.Source.Epoch, attData.Target.Epoch, []uint64{uint64(validatorIdx)}, []byte{1})
|
||||
err = slasherDB.SaveAttestationRecordsForValidators(
|
||||
ctx,
|
||||
[]*slashertypes.IndexedAttestationWrapper{attRecord},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
slashing, err = chunk.CheckSlashable(ctx, slasherDB, validatorIdx, surroundingVote)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, (*ethpb.AttesterSlashing)(nil), slashing)
|
||||
}
|
||||
|
||||
func TestMaxSpanChunksSlice_CheckSlashable(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
slasherDB := dbtest.SetupSlasherDB(t)
|
||||
params := &Parameters{
|
||||
chunkSize: 4,
|
||||
validatorChunkSize: 2,
|
||||
historyLength: 4,
|
||||
}
|
||||
validatorIdx := types.ValidatorIndex(1)
|
||||
source := types.Epoch(1)
|
||||
target := types.Epoch(2)
|
||||
att := createAttestationWrapper(t, source, target, nil, nil)
|
||||
|
||||
// A faulty chunk should lead to error.
|
||||
chunk := &MaxSpanChunksSlice{
|
||||
params: params,
|
||||
data: []uint16{},
|
||||
}
|
||||
_, err := chunk.CheckSlashable(ctx, nil, validatorIdx, att)
|
||||
require.ErrorContains(t, "could not get max target for validator", err)
|
||||
|
||||
// We initialize a proper slice with 2 chunks with chunk size 4, 2 validators, and
|
||||
// a history length of 4 representing a perfect attesting history.
|
||||
//
|
||||
// val0 val1
|
||||
// { } { }
|
||||
// [0, 0, 0, 0, 0, 0, 0, 0]
|
||||
data := []uint16{0, 0, 0, 0, 0, 0, 0, 0}
|
||||
chunk, err = MaxChunkSpansSliceFrom(params, data)
|
||||
require.NoError(t, err)
|
||||
|
||||
// An attestation with source 1 and target 2 should not be slashable
|
||||
// based on our max chunk for either validator.
|
||||
slashing, err := chunk.CheckSlashable(ctx, slasherDB, validatorIdx, att)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, (*ethpb.AttesterSlashing)(nil), slashing)
|
||||
|
||||
slashing, err = chunk.CheckSlashable(ctx, slasherDB, validatorIdx.Sub(1), att)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, (*ethpb.AttesterSlashing)(nil), slashing)
|
||||
|
||||
// Next up we initialize an empty chunks slice and mark an attestation
|
||||
// with (source 0, target 3) as attested.
|
||||
chunk = EmptyMaxSpanChunksSlice(params)
|
||||
source = types.Epoch(0)
|
||||
target = types.Epoch(3)
|
||||
att = createAttestationWrapper(t, source, target, nil, nil)
|
||||
chunkIdx := uint64(0)
|
||||
startEpoch := source
|
||||
currentEpoch := target
|
||||
args := &chunkUpdateArgs{
|
||||
chunkIndex: chunkIdx,
|
||||
currentEpoch: currentEpoch,
|
||||
}
|
||||
_, err = chunk.Update(args, validatorIdx, startEpoch, target)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Next up, we create a surrounded vote, but it should NOT be slashable
|
||||
// because we DO NOT have an existing attestation record in our database at the max target epoch.
|
||||
source = types.Epoch(1)
|
||||
target = types.Epoch(2)
|
||||
surroundedVote := createAttestationWrapper(t, source, target, nil, nil)
|
||||
|
||||
slashing, err = chunk.CheckSlashable(ctx, slasherDB, validatorIdx, surroundedVote)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, (*ethpb.AttesterSlashing)(nil), slashing)
|
||||
|
||||
// Next up, we save the old attestation record, then check if the
|
||||
// surroundedVote vote is indeed slashable.
|
||||
attData := att.IndexedAttestation.Data
|
||||
signingRoot := [32]byte{1}
|
||||
attRecord := createAttestationWrapper(
|
||||
t, attData.Source.Epoch, attData.Target.Epoch, []uint64{uint64(validatorIdx)}, signingRoot[:],
|
||||
)
|
||||
err = slasherDB.SaveAttestationRecordsForValidators(
|
||||
ctx,
|
||||
[]*slashertypes.IndexedAttestationWrapper{attRecord},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
slashing, err = chunk.CheckSlashable(ctx, slasherDB, validatorIdx, surroundedVote)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, (*ethpb.AttesterSlashing)(nil), slashing)
|
||||
}
|
||||
|
||||
func TestMinSpanChunksSlice_Update_MultipleChunks(t *testing.T) {
|
||||
// Let's set H = historyLength = 2, meaning a min span
|
||||
// will hold 2 epochs worth of attesting history. Then we set C = 2 meaning we will
|
||||
// chunk the min span into arrays each of length 2 and K = 3 meaning we store each chunk index
|
||||
// for 3 validators at a time.
|
||||
//
|
||||
// So assume we get a target 3 for source 0 and validator 0, then, we need to update every epoch in the span from
|
||||
// 3 to 0 inclusive. First, we find out which chunk epoch 3 falls into, which is calculated as:
|
||||
// chunk_idx = (epoch % H) / C = (3 % 4) / 2 = 1
|
||||
//
|
||||
// val0 val1 val2
|
||||
// { } { } { }
|
||||
// chunk_1_for_validators_0_to_3 = [[nil, nil], [nil, nil], [nil, nil]]
|
||||
// | |
|
||||
// | |-> epoch 3 for validator 0
|
||||
// |
|
||||
// |-> epoch 2 for validator 0
|
||||
//
|
||||
// val0 val1 val2
|
||||
// { } { } { }
|
||||
// chunk_0_for_validators_0_to_3 = [[nil, nil], [nil, nil], [nil, nil]]
|
||||
// | |
|
||||
// | |-> epoch 1 for validator 0
|
||||
// |
|
||||
// |-> epoch 0 for validator 0
|
||||
//
|
||||
// Next up, we proceed with the update process for validator index 0, starting epoch 3
|
||||
// updating every value along the way according to the update rules for min spans.
|
||||
//
|
||||
// Once we finish updating a chunk, we need to move on to the next chunk. This function
|
||||
// returns a boolean named keepGoing which allows the caller to determine if we should
|
||||
// continue and update another chunk index. We stop whenever we reach the min epoch we need
|
||||
// to update, in our example, we stop at 0, which is a part chunk 0, so we need to perform updates
|
||||
// across 2 different min span chunk slices as shown above.
|
||||
params := &Parameters{
|
||||
chunkSize: 2,
|
||||
validatorChunkSize: 3,
|
||||
historyLength: 4,
|
||||
}
|
||||
chunk := EmptyMinSpanChunksSlice(params)
|
||||
target := types.Epoch(3)
|
||||
chunkIdx := uint64(1)
|
||||
validatorIdx := types.ValidatorIndex(0)
|
||||
startEpoch := target
|
||||
currentEpoch := target
|
||||
args := &chunkUpdateArgs{
|
||||
chunkIndex: chunkIdx,
|
||||
currentEpoch: currentEpoch,
|
||||
}
|
||||
keepGoing, err := chunk.Update(args, validatorIdx, startEpoch, target)
|
||||
require.NoError(t, err)
|
||||
|
||||
// We should keep going! We still have to update the data for chunk index 0.
|
||||
require.Equal(t, true, keepGoing)
|
||||
want := []uint16{1, 0, math.MaxUint16, math.MaxUint16, math.MaxUint16, math.MaxUint16}
|
||||
require.DeepEqual(t, want, chunk.Chunk())
|
||||
|
||||
// Now we update for chunk index 0.
|
||||
chunk = EmptyMinSpanChunksSlice(params)
|
||||
chunkIdx = uint64(0)
|
||||
validatorIdx = types.ValidatorIndex(0)
|
||||
startEpoch = types.Epoch(1)
|
||||
currentEpoch = target
|
||||
args = &chunkUpdateArgs{
|
||||
chunkIndex: chunkIdx,
|
||||
currentEpoch: currentEpoch,
|
||||
}
|
||||
keepGoing, err = chunk.Update(args, validatorIdx, startEpoch, target)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, false, keepGoing)
|
||||
want = []uint16{3, 2, math.MaxUint16, math.MaxUint16, math.MaxUint16, math.MaxUint16}
|
||||
require.DeepEqual(t, want, chunk.Chunk())
|
||||
}
|
||||
|
||||
func TestMaxSpanChunksSlice_Update_MultipleChunks(t *testing.T) {
|
||||
params := &Parameters{
|
||||
chunkSize: 2,
|
||||
validatorChunkSize: 3,
|
||||
historyLength: 4,
|
||||
}
|
||||
chunk := EmptyMaxSpanChunksSlice(params)
|
||||
target := types.Epoch(3)
|
||||
chunkIdx := uint64(0)
|
||||
validatorIdx := types.ValidatorIndex(0)
|
||||
startEpoch := types.Epoch(0)
|
||||
currentEpoch := target
|
||||
args := &chunkUpdateArgs{
|
||||
chunkIndex: chunkIdx,
|
||||
currentEpoch: currentEpoch,
|
||||
}
|
||||
keepGoing, err := chunk.Update(args, validatorIdx, startEpoch, target)
|
||||
require.NoError(t, err)
|
||||
|
||||
// We should keep going! We still have to update the data for chunk index 1.
|
||||
require.Equal(t, true, keepGoing)
|
||||
want := []uint16{3, 2, 0, 0, 0, 0}
|
||||
require.DeepEqual(t, want, chunk.Chunk())
|
||||
|
||||
// Now we update for chunk index 1.
|
||||
chunk = EmptyMaxSpanChunksSlice(params)
|
||||
chunkIdx = uint64(1)
|
||||
validatorIdx = types.ValidatorIndex(0)
|
||||
startEpoch = types.Epoch(2)
|
||||
currentEpoch = target
|
||||
args = &chunkUpdateArgs{
|
||||
chunkIndex: chunkIdx,
|
||||
currentEpoch: currentEpoch,
|
||||
}
|
||||
keepGoing, err = chunk.Update(args, validatorIdx, startEpoch, target)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, false, keepGoing)
|
||||
want = []uint16{1, 0, 0, 0, 0, 0}
|
||||
require.DeepEqual(t, want, chunk.Chunk())
|
||||
}
|
||||
|
||||
func TestMinSpanChunksSlice_Update_SingleChunk(t *testing.T) {
|
||||
// Let's set H = historyLength = 2, meaning a min span
|
||||
// will hold 2 epochs worth of attesting history. Then we set C = 2 meaning we will
|
||||
// chunk the min span into arrays each of length 2 and K = 3 meaning we store each chunk index
|
||||
// for 3 validators at a time.
|
||||
//
|
||||
// So assume we get a target 1 for source 0 and validator 0, then, we need to update every epoch in the span from
|
||||
// 1 to 0 inclusive. First, we find out which chunk epoch 4 falls into, which is calculated as:
|
||||
// chunk_idx = (epoch % H) / C = (1 % 2) / 2 = 0
|
||||
//
|
||||
// val0 val1 val2
|
||||
// { } { } { }
|
||||
// chunk_0_for_validators_0_to_3 = [[nil, nil], [nil, nil], [nil, nil]]
|
||||
// |
|
||||
// |-> epoch 1 for validator 0
|
||||
//
|
||||
// Next up, we proceed with the update process for validator index 0, starting epoch 1
|
||||
// updating every value along the way according to the update rules for min spans.
|
||||
//
|
||||
// Once we finish updating a chunk, we need to move on to the next chunk. This function
|
||||
// returns a boolean named keepGoing which allows the caller to determine if we should
|
||||
// continue and update another chunk index. We stop whenever we reach the min epoch we need
|
||||
// to update, in our example, we stop at 0, which is still part of chunk 0, so there is no
|
||||
// need to keep going.
|
||||
params := &Parameters{
|
||||
chunkSize: 2,
|
||||
validatorChunkSize: 3,
|
||||
historyLength: 2,
|
||||
}
|
||||
chunk := EmptyMinSpanChunksSlice(params)
|
||||
target := types.Epoch(1)
|
||||
chunkIdx := uint64(0)
|
||||
validatorIdx := types.ValidatorIndex(0)
|
||||
startEpoch := target
|
||||
currentEpoch := target
|
||||
args := &chunkUpdateArgs{
|
||||
chunkIndex: chunkIdx,
|
||||
currentEpoch: currentEpoch,
|
||||
}
|
||||
keepGoing, err := chunk.Update(args, validatorIdx, startEpoch, target)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, false, keepGoing)
|
||||
want := []uint16{1, 0, math.MaxUint16, math.MaxUint16, math.MaxUint16, math.MaxUint16}
|
||||
require.DeepEqual(t, want, chunk.Chunk())
|
||||
}
|
||||
|
||||
func TestMaxSpanChunksSlice_Update_SingleChunk(t *testing.T) {
|
||||
params := &Parameters{
|
||||
chunkSize: 4,
|
||||
validatorChunkSize: 2,
|
||||
historyLength: 4,
|
||||
}
|
||||
chunk := EmptyMaxSpanChunksSlice(params)
|
||||
target := types.Epoch(3)
|
||||
chunkIdx := uint64(0)
|
||||
validatorIdx := types.ValidatorIndex(0)
|
||||
startEpoch := types.Epoch(0)
|
||||
currentEpoch := target
|
||||
args := &chunkUpdateArgs{
|
||||
chunkIndex: chunkIdx,
|
||||
currentEpoch: currentEpoch,
|
||||
}
|
||||
keepGoing, err := chunk.Update(args, validatorIdx, startEpoch, target)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, false, keepGoing)
|
||||
want := []uint16{3, 2, 1, 0, 0, 0, 0, 0}
|
||||
require.DeepEqual(t, want, chunk.Chunk())
|
||||
}
|
||||
|
||||
func TestMinSpanChunksSlice_StartEpoch(t *testing.T) {
|
||||
type args struct {
|
||||
sourceEpoch types.Epoch
|
||||
currentEpoch types.Epoch
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
params *Parameters
|
||||
args args
|
||||
wantEpoch types.Epoch
|
||||
shouldNotExist bool
|
||||
}{
|
||||
{
|
||||
name: "source_epoch == 0 returns false",
|
||||
params: DefaultParams(),
|
||||
args: args{
|
||||
sourceEpoch: 0,
|
||||
},
|
||||
shouldNotExist: true,
|
||||
},
|
||||
{
|
||||
name: "source_epoch == (current_epoch - HISTORY_LENGTH) returns false",
|
||||
params: &Parameters{
|
||||
historyLength: 3,
|
||||
},
|
||||
args: args{
|
||||
sourceEpoch: 1,
|
||||
currentEpoch: 4,
|
||||
},
|
||||
shouldNotExist: true,
|
||||
},
|
||||
{
|
||||
name: "source_epoch < (current_epoch - HISTORY_LENGTH) returns false",
|
||||
params: &Parameters{
|
||||
historyLength: 3,
|
||||
},
|
||||
args: args{
|
||||
sourceEpoch: 1,
|
||||
currentEpoch: 5,
|
||||
},
|
||||
shouldNotExist: true,
|
||||
},
|
||||
{
|
||||
name: "source_epoch > (current_epoch - HISTORY_LENGTH) returns true",
|
||||
params: &Parameters{
|
||||
historyLength: 3,
|
||||
},
|
||||
args: args{
|
||||
sourceEpoch: 1,
|
||||
currentEpoch: 3,
|
||||
},
|
||||
wantEpoch: 0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := &MinSpanChunksSlice{
|
||||
params: tt.params,
|
||||
}
|
||||
gotEpoch, gotExists := m.StartEpoch(tt.args.sourceEpoch, tt.args.currentEpoch)
|
||||
assert.Equal(t, false, tt.shouldNotExist && gotExists)
|
||||
assert.Equal(t, false, !tt.shouldNotExist && gotEpoch != tt.wantEpoch)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxSpanChunksSlice_StartEpoch(t *testing.T) {
|
||||
type args struct {
|
||||
sourceEpoch types.Epoch
|
||||
currentEpoch types.Epoch
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
params *Parameters
|
||||
args args
|
||||
wantEpoch types.Epoch
|
||||
shouldNotExist bool
|
||||
}{
|
||||
{
|
||||
name: "source_epoch == current_epoch returns false",
|
||||
params: DefaultParams(),
|
||||
args: args{
|
||||
sourceEpoch: 1,
|
||||
currentEpoch: 1,
|
||||
},
|
||||
shouldNotExist: true,
|
||||
},
|
||||
{
|
||||
name: "source_epoch > current_epoch returns false",
|
||||
params: DefaultParams(),
|
||||
args: args{
|
||||
sourceEpoch: 2,
|
||||
currentEpoch: 1,
|
||||
},
|
||||
shouldNotExist: true,
|
||||
},
|
||||
{
|
||||
name: "source_epoch < current_epoch returns true",
|
||||
params: DefaultParams(),
|
||||
args: args{
|
||||
sourceEpoch: 1,
|
||||
currentEpoch: 2,
|
||||
},
|
||||
wantEpoch: 2,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := &MaxSpanChunksSlice{
|
||||
params: tt.params,
|
||||
}
|
||||
gotEpoch, gotExists := m.StartEpoch(tt.args.sourceEpoch, tt.args.currentEpoch)
|
||||
assert.Equal(t, false, tt.shouldNotExist && gotExists)
|
||||
assert.Equal(t, false, !tt.shouldNotExist && gotEpoch != tt.wantEpoch)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinSpanChunksSlice_NextChunkStartEpoch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
params *Parameters
|
||||
startEpoch types.Epoch
|
||||
want types.Epoch
|
||||
}{
|
||||
{
|
||||
name: "Start epoch 0",
|
||||
params: &Parameters{
|
||||
chunkSize: 3,
|
||||
historyLength: 4096,
|
||||
},
|
||||
startEpoch: 0,
|
||||
want: 2,
|
||||
},
|
||||
{
|
||||
name: "Start epoch of chunk 1 returns last epoch of chunk 0",
|
||||
params: &Parameters{
|
||||
chunkSize: 3,
|
||||
historyLength: 4096,
|
||||
},
|
||||
startEpoch: 3,
|
||||
want: 2,
|
||||
},
|
||||
{
|
||||
name: "Start epoch inside of chunk 2 returns last epoch of chunk 1",
|
||||
params: &Parameters{
|
||||
chunkSize: 3,
|
||||
historyLength: 4096,
|
||||
},
|
||||
startEpoch: 8,
|
||||
want: 5,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := &MinSpanChunksSlice{
|
||||
params: tt.params,
|
||||
}
|
||||
got := m.NextChunkStartEpoch(tt.startEpoch)
|
||||
assert.Equal(t, true, got == tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxSpanChunksSlice_NextChunkStartEpoch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
params *Parameters
|
||||
startEpoch types.Epoch
|
||||
want types.Epoch
|
||||
}{
|
||||
{
|
||||
name: "Start epoch 0",
|
||||
params: &Parameters{
|
||||
chunkSize: 3,
|
||||
historyLength: 4,
|
||||
},
|
||||
startEpoch: 0,
|
||||
want: 3,
|
||||
},
|
||||
{
|
||||
name: "Start epoch of chunk 1 returns start epoch of chunk 2",
|
||||
params: &Parameters{
|
||||
chunkSize: 3,
|
||||
historyLength: 4,
|
||||
},
|
||||
startEpoch: 3,
|
||||
want: 6,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := &MaxSpanChunksSlice{
|
||||
params: tt.params,
|
||||
}
|
||||
got := m.NextChunkStartEpoch(tt.startEpoch)
|
||||
assert.Equal(t, true, got == tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_chunkDataAtEpoch_SetRetrieve(t *testing.T) {
|
||||
// We initialize a chunks slice for 2 validators and with chunk size 3,
|
||||
// which will look as follows:
|
||||
//
|
||||
// val0 val1
|
||||
// { } { }
|
||||
// [2, 2, 2, 2, 2, 2]
|
||||
//
|
||||
// To give an example, epoch 1 for validator 1 will be at the following position:
|
||||
//
|
||||
// [2, 2, 2, 2, 2, 2]
|
||||
// |-> epoch 1, validator 1.
|
||||
params := &Parameters{
|
||||
chunkSize: 3,
|
||||
validatorChunkSize: 2,
|
||||
}
|
||||
chunk := []uint16{2, 2, 2, 2, 2, 2}
|
||||
validatorIdx := types.ValidatorIndex(1)
|
||||
epochInChunk := types.Epoch(1)
|
||||
|
||||
// We expect a chunk with the wrong length to throw an error.
|
||||
_, err := chunkDataAtEpoch(params, []uint16{}, validatorIdx, epochInChunk)
|
||||
require.ErrorContains(t, "chunk has wrong length", err)
|
||||
|
||||
// We update the value for epoch 1 using target epoch 6.
|
||||
targetEpoch := types.Epoch(6)
|
||||
err = setChunkDataAtEpoch(params, chunk, validatorIdx, epochInChunk, targetEpoch)
|
||||
require.NoError(t, err)
|
||||
|
||||
// We expect the retrieved value at epoch 1 is the target epoch 6.
|
||||
received, err := chunkDataAtEpoch(params, chunk, validatorIdx, epochInChunk)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, targetEpoch, received)
|
||||
}
|
||||
56
beacon-chain/slasher/metrics.go
Normal file
56
beacon-chain/slasher/metrics.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package slasher
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
var (
|
||||
attestationDistance = promauto.NewHistogram(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "slasher_attestation_distance_epochs",
|
||||
Help: "The number of epochs between att target and source",
|
||||
Buckets: []float64{0, 1, 2, 3, 4, 5, 10, 20, 50, 100},
|
||||
},
|
||||
)
|
||||
chunksSavedTotal = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "slasher_chunks_saved_total",
|
||||
Help: "Total number of slasher chunks saved to disk",
|
||||
})
|
||||
deferredAttestationsTotal = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "slasher_attestations_deferred_total",
|
||||
Help: "Total number of attestations deferred by slasher for future processing",
|
||||
})
|
||||
droppedAttestationsTotal = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "slasher_attestations_dropped_total",
|
||||
Help: "Total number of attestations dropped by slasher due to invalidity",
|
||||
})
|
||||
processedAttestationsTotal = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "slasher_attestations_processed_total",
|
||||
Help: "Total number of attestations successfully processed by slasher",
|
||||
})
|
||||
receivedBlocksTotal = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "slasher_blocks_received_total",
|
||||
Help: "Total number of blocks received by slasher",
|
||||
})
|
||||
processedBlocksTotal = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "slasher_blocks_processed_total",
|
||||
Help: "Total number of blocks successfully processed by slasher",
|
||||
})
|
||||
doubleProposalsTotal = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "slasher_double_proposals_total",
|
||||
Help: "Total slashable proposals successfully detected by slasher",
|
||||
})
|
||||
doubleVotesTotal = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "slasher_double_votes_total",
|
||||
Help: "Total slashable double votes successfully detected by slasher",
|
||||
})
|
||||
surroundingVotesTotal = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "slasher_surrounding_votes_total",
|
||||
Help: "Total slashable surrounding votes successfully detected by slasher",
|
||||
})
|
||||
surroundedVotesTotal = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "slasher_surrounded_votes_total",
|
||||
Help: "Total slashable surrounded votes successfully detected by slasher",
|
||||
})
|
||||
)
|
||||
Reference in New Issue
Block a user