slasher

package
v5.0.1-rc.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 8, 2024 License: GPL-3.0 Imports: 30 Imported by: 0

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

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.

func (*Service) Start

func (s *Service) Start()

Start listening for received indexed attestations and blocks and perform slashing detection on them.

func (*Service) Status

func (*Service) Status() error

Status of the slasher service.

func (*Service) Stop

func (s *Service) Stop() error

Stop the slasher service.

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.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL