156 lines
5.9 KiB
Markdown
156 lines
5.9 KiB
Markdown
<!--
|
|
order: 6
|
|
-->
|
|
|
|
# BeginBlock
|
|
|
|
## Evidence Handling
|
|
|
|
Tendermint blocks can include
|
|
[Evidence](https://github.com/tendermint/tendermint/blob/master/docs/spec/blockchain/blockchain.md#evidence),
|
|
which indicates that a validator committed malicious behavior. The relevant information is
|
|
forwarded to the application as ABCI Evidence in `abci.RequestBeginBlock` so that
|
|
the validator an be accordingly punished.
|
|
|
|
### Equivocation
|
|
|
|
Currently, the SDK handles two types of evidence inside ABCI's `BeginBlock`:
|
|
|
|
- `DuplicateVoteEvidence`,
|
|
- `LightClientAttackEvidence`.
|
|
|
|
These two evidence types are handled the same way by the evidence module. First, the SDK converts the Tendermint concrete evidence type to a SDK `Evidence` interface using `Equivocation` as the concrete type.
|
|
|
|
```proto
|
|
// Equivocation implements the Evidence interface.
|
|
message Equivocation {
|
|
int64 height = 1;
|
|
google.protobuf.Timestamp time = 2;
|
|
int64 power = 3;
|
|
string consensus_address = 4;
|
|
}
|
|
```
|
|
|
|
For some `Equivocation` submitted in `block` to be valid, it must satisfy:
|
|
|
|
`Evidence.Timestamp >= block.Timestamp - MaxEvidenceAge`
|
|
|
|
Where `Evidence.Timestamp` is the timestamp in the block at height `Evidence.Height` and
|
|
`block.Timestamp` is the current block timestamp.
|
|
|
|
If valid `Equivocation` evidence is included in a block, the validator's stake is
|
|
reduced (slashed) by `SlashFractionDoubleSign`, which is defined by the `x/slashing` module,
|
|
of what their stake was when the infraction occurred (rather than when the evidence was discovered).
|
|
We want to "follow the stake", i.e. the stake which contributed to the infraction
|
|
should be slashed, even if it has since been redelegated or started unbonding.
|
|
|
|
In addition, the validator is permanently jailed and tombstoned making it impossible for that
|
|
validator to ever re-enter the validator set.
|
|
|
|
The `Equivocation` evidence is handled as follows:
|
|
|
|
```go
|
|
func (k Keeper) HandleEquivocationEvidence(ctx sdk.Context, evidence *types.Equivocation) {
|
|
logger := k.Logger(ctx)
|
|
consAddr := evidence.GetConsensusAddress()
|
|
|
|
if _, err := k.slashingKeeper.GetPubkey(ctx, consAddr.Bytes()); err != nil {
|
|
// Ignore evidence that cannot be handled.
|
|
//
|
|
// NOTE: We used to panic with:
|
|
// `panic(fmt.Sprintf("Validator consensus-address %v not found", consAddr))`,
|
|
// but this couples the expectations of the app to both Tendermint and
|
|
// the simulator. Both are expected to provide the full range of
|
|
// allowable but none of the disallowed evidence types. Instead of
|
|
// getting this coordination right, it is easier to relax the
|
|
// constraints and ignore evidence that cannot be handled.
|
|
return
|
|
}
|
|
|
|
// calculate the age of the evidence
|
|
infractionHeight := evidence.GetHeight()
|
|
infractionTime := evidence.GetTime()
|
|
ageDuration := ctx.BlockHeader().Time.Sub(infractionTime)
|
|
ageBlocks := ctx.BlockHeader().Height - infractionHeight
|
|
|
|
// Reject evidence if the double-sign is too old. Evidence is considered stale
|
|
// if the difference in time and number of blocks is greater than the allowed
|
|
// parameters defined.
|
|
cp := ctx.ConsensusParams()
|
|
if cp != nil && cp.Evidence != nil {
|
|
if ageDuration > cp.Evidence.MaxAgeDuration && ageBlocks > cp.Evidence.MaxAgeNumBlocks {
|
|
logger.Info(
|
|
"ignored equivocation; evidence too old",
|
|
"validator", consAddr,
|
|
"infraction_height", infractionHeight,
|
|
"max_age_num_blocks", cp.Evidence.MaxAgeNumBlocks,
|
|
"infraction_time", infractionTime,
|
|
"max_age_duration", cp.Evidence.MaxAgeDuration,
|
|
)
|
|
return
|
|
}
|
|
}
|
|
|
|
validator := k.stakingKeeper.ValidatorByConsAddr(ctx, consAddr)
|
|
if validator == nil || validator.IsUnbonded() {
|
|
// Defensive: Simulation doesn't take unbonding periods into account, and
|
|
// Tendermint might break this assumption at some point.
|
|
return
|
|
}
|
|
|
|
if ok := k.slashingKeeper.HasValidatorSigningInfo(ctx, consAddr); !ok {
|
|
panic(fmt.Sprintf("expected signing info for validator %s but not found", consAddr))
|
|
}
|
|
|
|
// ignore if the validator is already tombstoned
|
|
if k.slashingKeeper.IsTombstoned(ctx, consAddr) {
|
|
logger.Info(
|
|
"ignored equivocation; validator already tombstoned",
|
|
"validator", consAddr,
|
|
"infraction_height", infractionHeight,
|
|
"infraction_time", infractionTime,
|
|
)
|
|
return
|
|
}
|
|
|
|
logger.Info(
|
|
"confirmed equivocation",
|
|
"validator", consAddr,
|
|
"infraction_height", infractionHeight,
|
|
"infraction_time", infractionTime,
|
|
)
|
|
|
|
// We need to retrieve the stake distribution which signed the block, so we
|
|
// subtract ValidatorUpdateDelay from the evidence height.
|
|
// Note, that this *can* result in a negative "distributionHeight", up to
|
|
// -ValidatorUpdateDelay, i.e. at the end of the
|
|
// pre-genesis block (none) = at the beginning of the genesis block.
|
|
// That's fine since this is just used to filter unbonding delegations & redelegations.
|
|
distributionHeight := infractionHeight - sdk.ValidatorUpdateDelay
|
|
|
|
// Slash validator. The `power` is the int64 power of the validator as provided
|
|
// to/by Tendermint. This value is validator.Tokens as sent to Tendermint via
|
|
// ABCI, and now received as evidence. The fraction is passed in to separately
|
|
// to slash unbonding and rebonding delegations.
|
|
k.slashingKeeper.Slash(
|
|
ctx,
|
|
consAddr,
|
|
k.slashingKeeper.SlashFractionDoubleSign(ctx),
|
|
evidence.GetValidatorPower(), distributionHeight,
|
|
)
|
|
|
|
// Jail the validator if not already jailed. This will begin unbonding the
|
|
// validator if not already unbonding (tombstoned).
|
|
if !validator.IsJailed() {
|
|
k.slashingKeeper.Jail(ctx, consAddr)
|
|
}
|
|
|
|
k.slashingKeeper.JailUntil(ctx, consAddr, types.DoubleSignJailEndTime)
|
|
k.slashingKeeper.Tombstone(ctx, consAddr)
|
|
}
|
|
```
|
|
|
|
Note, the slashing, jailing, and tombstoning calls are delegated through the `x/slashing` module
|
|
which emit informative events and finally delegate calls to the `x/staking` module. Documentation
|
|
on slashing and jailing can be found in the [x/staking spec](/.././cosmos-sdk/x/staking/spec/02_state_transitions.md)
|