Documentation ¶
Overview ¶
nolint:dupword
Package slasher defines an optimized implementation of Ethereum proof-of-stake slashing detection, namely focused on catching "surround vote" slashable offenses as explained here: https://blog.ethereum.org/2020/01/13/validated-staking-on-eth2-1-incentives/.
Surround vote detection is a difficult problem if done naively, as slasher needs to keep track of every single attestation by every single validator in the network and be ready to efficiently detect whether incoming attestations are slashable with respect to older ones. To do this, the Sigma Prime team created an elaborate design document: https://hackmd.io/@sproul/min-max-slasher offering an optimal solution.
Attesting histories are kept for each validator in two separate arrays known as min and max spans, which are explained in our design document: https://hackmd.io/@prysmaticlabs/slasher.
A regular pair of min and max spans for a validator look as follows with length = H where H is the amount of epochs worth of history we want to persist for slashing detection.
validator_1_min_span = [2, 2, 2, ..., 2] validator_1_max_span = [0, 0, 0, ..., 0]
Instead of always dealing with length H arrays, which can be prohibitively expensive to handle in memory, we split these arrays into chunks of length C. For C = 3, for example, the 0th chunk of validator 1's min and max spans would look as follows:
validator_1_min_span_chunk_0 = [2, 2, 2] validator_1_max_span_chunk_0 = [2, 2, 2]
Next, on disk, we take chunks for K validators, and store them as flat slices. For example, if H = 3, C = 3, and K = 3, then we can store 3 validators' chunks as a flat slice as follows:
val0 val1 val2 | | | { } { } { } [2, 2, 2, 2, 2, 2, 2, 2, 2]
This is known as 2D chunking, pioneered by the Sigma Prime team here: https://hackmd.io/@sproul/min-max-slasher. The parameters H, C, and K will be used extensively throughout this package.
Package slasher implements slashing detection for eth2, able to catch slashable attestations and proposals that it receives via two event feeds, respectively. Any found slashings are then submitted to the beacon node's slashing operations pool. See the design document here https://hackmd.io/@prysmaticlabs/slasher.
Index ¶
- type Chunker
- type MaxSpanChunksSlice
- func (m *MaxSpanChunksSlice) CheckSlashable(ctx context.Context, slasherDB db.SlasherDatabase, ...) (*ethpb.AttesterSlashing, error)
- func (m *MaxSpanChunksSlice) Chunk() []uint16
- func (*MaxSpanChunksSlice) NeutralElement() uint16
- func (m *MaxSpanChunksSlice) NextChunkStartEpoch(startEpoch primitives.Epoch) primitives.Epoch
- func (*MaxSpanChunksSlice) StartEpoch(sourceEpoch, currentEpoch primitives.Epoch) (epoch primitives.Epoch, exists bool)
- func (m *MaxSpanChunksSlice) Update(chunkIndex uint64, currentEpoch primitives.Epoch, ...) (keepGoing bool, err error)
- type MinSpanChunksSlice
- func (m *MinSpanChunksSlice) CheckSlashable(ctx context.Context, slasherDB db.SlasherDatabase, ...) (*ethpb.AttesterSlashing, error)
- func (m *MinSpanChunksSlice) Chunk() []uint16
- func (*MinSpanChunksSlice) NeutralElement() uint16
- func (m *MinSpanChunksSlice) NextChunkStartEpoch(startEpoch primitives.Epoch) primitives.Epoch
- func (m *MinSpanChunksSlice) StartEpoch(sourceEpoch, currentEpoch primitives.Epoch) (epoch primitives.Epoch, exists bool)
- func (m *MinSpanChunksSlice) Update(chunkIndex uint64, currentEpoch primitives.Epoch, ...) (keepGoing bool, err error)
- type Parameters
- type Service
- type ServiceConfig
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Chunker ¶
type Chunker interface { NeutralElement() uint16 Chunk() []uint16 CheckSlashable( ctx context.Context, slasherDB db.SlasherDatabase, validatorIdx primitives.ValidatorIndex, attestation *slashertypes.IndexedAttestationWrapper, ) (*ethpb.AttesterSlashing, error) Update( chunkIndex uint64, currentEpoch primitives.Epoch, validatorIndex primitives.ValidatorIndex, startEpoch, newTargetEpoch primitives.Epoch, ) (keepGoing bool, err error) StartEpoch(sourceEpoch, currentEpoch primitives.Epoch) (epoch primitives.Epoch, exists bool) NextChunkStartEpoch(startEpoch primitives.Epoch) primitives.Epoch }
Chunker defines a struct which represents a slice containing a chunk for K different validator's min/max 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 MaxSpanChunksSlice ¶
type MaxSpanChunksSlice struct {
// contains filtered or unexported fields
}
MaxSpanChunksSlice represents the same data structure as MinSpanChunksSlice however keeps track of validator max spans for slashing detection instead.
func EmptyMaxSpanChunksSlice ¶
func EmptyMaxSpanChunksSlice(params *Parameters) *MaxSpanChunksSlice
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 MaxChunkSpansSliceFrom ¶
func MaxChunkSpansSliceFrom(params *Parameters, chunk []uint16) (*MaxSpanChunksSlice, error)
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 (*MaxSpanChunksSlice) CheckSlashable ¶
func (m *MaxSpanChunksSlice) CheckSlashable( ctx context.Context, slasherDB db.SlasherDatabase, validatorIdx primitives.ValidatorIndex, incomingAttWrapper *slashertypes.IndexedAttestationWrapper, ) (*ethpb.AttesterSlashing, error)
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 is surrounded by 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 (*MaxSpanChunksSlice) Chunk ¶
func (m *MaxSpanChunksSlice) Chunk() []uint16
Chunk returns the underlying slice of uint16's for the max chunks slice.
func (*MaxSpanChunksSlice) NeutralElement ¶
func (*MaxSpanChunksSlice) NeutralElement() uint16
NeutralElement for a max span chunks slice is 0.
func (*MaxSpanChunksSlice) NextChunkStartEpoch ¶
func (m *MaxSpanChunksSlice) NextChunkStartEpoch(startEpoch primitives.Epoch) primitives.Epoch
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 6. This is computed as:
first_epoch(chunkIndex(startEpoch)+1) first_epoch(chunkIndex(3)+1) first_epoch(1 + 1) first_epoch(2) 6
func (*MaxSpanChunksSlice) StartEpoch ¶
func (*MaxSpanChunksSlice) StartEpoch( sourceEpoch, currentEpoch primitives.Epoch, ) (epoch primitives.Epoch, exists bool)
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 (*MaxSpanChunksSlice) Update ¶
func (m *MaxSpanChunksSlice) Update( chunkIndex uint64, currentEpoch primitives.Epoch, validatorIndex primitives.ValidatorIndex, startEpoch, newTargetEpoch primitives.Epoch, ) (keepGoing bool, err error)
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.
type MinSpanChunksSlice ¶
type MinSpanChunksSlice struct {
// contains filtered or unexported fields
}
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.
nolint:dupword
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.
func EmptyMinSpanChunksSlice ¶
func EmptyMinSpanChunksSlice(params *Parameters) *MinSpanChunksSlice
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 MinChunkSpansSliceFrom ¶
func MinChunkSpansSliceFrom(params *Parameters, chunk []uint16) (*MinSpanChunksSlice, error)
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 (*MinSpanChunksSlice) CheckSlashable ¶
func (m *MinSpanChunksSlice) CheckSlashable( ctx context.Context, slasherDB db.SlasherDatabase, validatorIdx primitives.ValidatorIndex, incomingAttWrapper *slashertypes.IndexedAttestationWrapper, ) (*ethpb.AttesterSlashing, error)
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 (*MinSpanChunksSlice) Chunk ¶
func (m *MinSpanChunksSlice) Chunk() []uint16
Chunk returns the underlying slice of uint16's for the min chunks slice.
func (*MinSpanChunksSlice) NeutralElement ¶
func (*MinSpanChunksSlice) NeutralElement() uint16
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 (*MinSpanChunksSlice) NextChunkStartEpoch ¶
func (m *MinSpanChunksSlice) NextChunkStartEpoch(startEpoch primitives.Epoch) primitives.Epoch
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 (*MinSpanChunksSlice) StartEpoch ¶
func (m *MinSpanChunksSlice) StartEpoch( sourceEpoch, currentEpoch primitives.Epoch, ) (epoch primitives.Epoch, exists bool)
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 arguments are invalid for the chunk and the start epoch does not exist.
func (*MinSpanChunksSlice) Update ¶
func (m *MinSpanChunksSlice) Update( chunkIndex uint64, currentEpoch primitives.Epoch, validatorIndex primitives.ValidatorIndex, startEpoch, newTargetEpoch primitives.Epoch, ) (keepGoing bool, err error)
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.
type Parameters ¶
type Parameters struct {
// contains filtered or unexported fields
}
Parameters for slashing detection.
To properly access the element at epoch `e` for a validator index `i`, we leverage helper functions from these parameter values as nice abstractions. the following parameters are required for the helper functions defined in this file.
func DefaultParams ¶
func DefaultParams() *Parameters
DefaultParams defines default values for slasher's important parameters, defined based on optimization analysis for best and worst case scenarios for slasher's performance.
The default values for chunkSize and validatorChunkSize were decided after an optimization analysis performed by the Sigma Prime team. See: https://hackmd.io/@sproul/min-max-slasher#1D-vs-2D for more information. We decide to keep 4096 epochs worth of data in each validator's min max spans.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service defining a slasher implementation as part of the beacon node, able to detect eth2 slashable offenses.
func New ¶
func New(ctx context.Context, srvCfg *ServiceConfig) (*Service, error)
New instantiates a new slasher from configuration values.
type ServiceConfig ¶
type ServiceConfig struct { IndexedAttestationsFeed *event.Feed BeaconBlockHeadersFeed *event.Feed Database db.SlasherDatabase StateNotifier statefeed.Notifier AttestationStateFetcher blockchain.AttestationStateFetcher StateGen stategen.StateManager SlashingPoolInserter slashings.PoolInserter HeadStateFetcher blockchain.HeadFetcher SyncChecker beaconChainSync.Checker ClockWaiter startup.ClockWaiter }
ServiceConfig for the slasher service in the beacon node. This struct allows us to specify required dependencies and parameters for slasher to function as needed.