You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
420 lines
12 KiB
420 lines
12 KiB
package slash
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"math/big"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/rlp"
|
|
"github.com/harmony-one/harmony/block"
|
|
"github.com/harmony-one/harmony/consensus/votepower"
|
|
"github.com/harmony-one/harmony/core/state"
|
|
common2 "github.com/harmony-one/harmony/internal/common"
|
|
"github.com/harmony-one/harmony/internal/utils"
|
|
"github.com/harmony-one/harmony/numeric"
|
|
"github.com/harmony-one/harmony/shard"
|
|
staking "github.com/harmony-one/harmony/staking/types"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
const (
|
|
haveEnoughToPayOff = 1
|
|
paidOffExact = 0
|
|
debtCollectionsRepoUndelegations = -1
|
|
validatorsOwnDel = 0
|
|
)
|
|
|
|
// invariant assumes snapshot, current can be rlp.EncodeToBytes
|
|
func payDebt(
|
|
snapshot, current *staking.ValidatorWrapper,
|
|
slashDebt, payment *big.Int,
|
|
slashDiff *Application,
|
|
) error {
|
|
utils.Logger().Info().
|
|
RawJSON("snapshot", []byte(snapshot.String())).
|
|
RawJSON("current", []byte(current.String())).
|
|
Uint64("slash-debt", slashDebt.Uint64()).
|
|
Uint64("payment", payment.Uint64()).
|
|
RawJSON("slash-track", []byte(slashDiff.String())).
|
|
Msg("slash debt payment before application")
|
|
slashDiff.TotalSlashed.Add(slashDiff.TotalSlashed, payment)
|
|
slashDebt.Sub(slashDebt, payment)
|
|
if slashDebt.Cmp(common.Big0) == -1 {
|
|
x1, _ := rlp.EncodeToBytes(snapshot)
|
|
x2, _ := rlp.EncodeToBytes(current)
|
|
utils.Logger().Info().
|
|
Str("snapshot-rlp", hex.EncodeToString(x1)).
|
|
Str("current-rlp", hex.EncodeToString(x2)).
|
|
Msg("slashdebt balance cannot go below zero")
|
|
return errSlashDebtCannotBeNegative
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Moment ..
|
|
type Moment struct {
|
|
Epoch *big.Int `json:"epoch"`
|
|
Height *big.Int `json:"block-height"`
|
|
TimeUnixNano *big.Int `json:"time-unix-nano"`
|
|
ViewID uint64 `json:"view-id"`
|
|
ShardID uint32 `json:"shard-id"`
|
|
}
|
|
|
|
// Evidence ..
|
|
type Evidence struct {
|
|
Moment
|
|
ConflictingBallots
|
|
ProposalHeader *block.Header `json:"header"`
|
|
}
|
|
|
|
// ConflictingBallots ..
|
|
type ConflictingBallots struct {
|
|
AlreadyCastBallot votepower.Ballot `json:"already-cast-vote"`
|
|
DoubleSignedBallot votepower.Ballot `json:"double-signed-vote"`
|
|
}
|
|
|
|
// Record is an proof of a slashing made by a witness of a double-signing event
|
|
type Record struct {
|
|
// the reporter who will get rewarded
|
|
Evidence Evidence `json:"evidence"`
|
|
Reporter common.Address `json:"reporter"`
|
|
Offender common.Address `json:"offender"`
|
|
}
|
|
|
|
// Application ..
|
|
type Application struct {
|
|
TotalSlashed, TotalSnitchReward *big.Int
|
|
}
|
|
|
|
func (a *Application) String() string {
|
|
s, _ := json.Marshal(a)
|
|
return string(s)
|
|
}
|
|
|
|
// MarshalJSON ..
|
|
func (e Evidence) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(struct {
|
|
Moment
|
|
ConflictingBallots
|
|
ProposalHeader string `json:"header"`
|
|
}{e.Moment, e.ConflictingBallots, e.ProposalHeader.String()})
|
|
}
|
|
|
|
// Records ..
|
|
type Records []Record
|
|
|
|
func (r Records) String() string {
|
|
s, _ := json.Marshal(r)
|
|
return string(s)
|
|
}
|
|
|
|
var (
|
|
errBallotSignerKeysNotSame = errors.New("conflicting ballots must have same signer key")
|
|
errReporterAndOffenderSame = errors.New("reporter and offender cannot be same")
|
|
)
|
|
|
|
// SanityCheck fails if any of the slashes fail
|
|
func (r Records) SanityCheck() error {
|
|
for _, record := range r {
|
|
k1 := record.Evidence.AlreadyCastBallot.SignerPubKey
|
|
k2 := record.Evidence.DoubleSignedBallot.SignerPubKey
|
|
if k1 != k2 {
|
|
return errBallotSignerKeysNotSame
|
|
}
|
|
|
|
if record.Offender == record.Reporter {
|
|
return errReporterAndOffenderSame
|
|
}
|
|
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MarshalJSON ..
|
|
func (r Record) MarshalJSON() ([]byte, error) {
|
|
reporter, offender :=
|
|
common2.MustAddressToBech32(r.Reporter),
|
|
common2.MustAddressToBech32(r.Offender)
|
|
return json.Marshal(struct {
|
|
Evidence Evidence `json:"evidence"`
|
|
Beneficiary string `json:"beneficiary"`
|
|
AddressForBLSKey string `json:"offender"`
|
|
}{r.Evidence, reporter, offender})
|
|
}
|
|
|
|
func (e Evidence) String() string {
|
|
s, _ := json.Marshal(e)
|
|
return string(s)
|
|
}
|
|
|
|
func (r Record) String() string {
|
|
s, _ := json.Marshal(r)
|
|
return string(s)
|
|
}
|
|
|
|
// CommitteeReader ..
|
|
type CommitteeReader interface {
|
|
ReadShardState(epoch *big.Int) (*shard.State, error)
|
|
}
|
|
|
|
// Verify checks that the signature is valid
|
|
func Verify(chain CommitteeReader, candidate *Record) error {
|
|
first, second :=
|
|
candidate.Evidence.AlreadyCastBallot,
|
|
candidate.Evidence.DoubleSignedBallot
|
|
if shard.CompareBlsPublicKey(first.SignerPubKey, second.SignerPubKey) != 0 {
|
|
k1, k2 := first.SignerPubKey.Hex(), second.SignerPubKey.Hex()
|
|
return errors.Wrapf(
|
|
errBLSKeysNotEqual, "%s %s", k1, k2,
|
|
)
|
|
}
|
|
superCommittee, err := chain.ReadShardState(candidate.Evidence.Epoch)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
subCommittee := superCommittee.FindCommitteeByID(
|
|
candidate.Evidence.ShardID,
|
|
)
|
|
|
|
if subCommittee == nil {
|
|
return errors.Wrapf(
|
|
errShardIDNotKnown, "given shardID %d", candidate.Evidence.ShardID,
|
|
)
|
|
}
|
|
|
|
if _, err := subCommittee.AddressForBLSKey(second.SignerPubKey); err != nil {
|
|
return err
|
|
}
|
|
// TODO need to finish this implementation
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
errBLSKeysNotEqual = errors.New(
|
|
"bls keys in ballots accompanying slash evidence not equal ",
|
|
)
|
|
errSlashDebtCannotBeNegative = errors.New("slash debt cannot be negative")
|
|
errShardIDNotKnown = errors.New("nil subcommittee for shardID")
|
|
errValidatorNotFoundDuringSlash = errors.New("validator not found")
|
|
zero = numeric.ZeroDec()
|
|
oneDoubleSignerRate = numeric.MustNewDecFromStr("0.02")
|
|
)
|
|
|
|
// applySlashRate returns (amountPostSlash, amountOfReduction, amountOfReduction / 2)
|
|
func applySlashRate(amount *big.Int, rate numeric.Dec) *big.Int {
|
|
return numeric.NewDecFromBigInt(
|
|
amount,
|
|
).Mul(rate).TruncateInt()
|
|
}
|
|
|
|
func payDownAsMuchAsCan(
|
|
snapshot, current *staking.ValidatorWrapper,
|
|
slashDebt, nowAmt *big.Int,
|
|
slashDiff *Application,
|
|
) error {
|
|
if nowAmt.Cmp(common.Big0) == 1 && slashDebt.Cmp(common.Big0) == 1 {
|
|
// 0.50_amount > 0.06_debt => slash == 0.0, nowAmt == 0.44
|
|
if nowAmt.Cmp(slashDebt) >= 0 {
|
|
nowAmt.Sub(nowAmt, slashDebt)
|
|
if err := payDebt(
|
|
snapshot, current, slashDebt, slashDebt, slashDiff,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// 0.50_amount < 2.4_debt =>, slash == 1.9, nowAmt == 0.0
|
|
if err := payDebt(
|
|
snapshot, current, slashDebt, nowAmt, slashDiff,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
nowAmt.Sub(nowAmt, nowAmt)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func delegatorSlashApply(
|
|
snapshot, current *staking.ValidatorWrapper,
|
|
rate numeric.Dec,
|
|
state *state.DB,
|
|
reporter common.Address,
|
|
doubleSignEpoch *big.Int,
|
|
slashTrack *Application,
|
|
) error {
|
|
|
|
for _, delegationSnapshot := range snapshot.Delegations {
|
|
slashDebt := applySlashRate(delegationSnapshot.Amount, rate)
|
|
slashDiff := &Application{big.NewInt(0), big.NewInt(0)}
|
|
snapshotAddr := delegationSnapshot.DelegatorAddress
|
|
for _, delegationNow := range current.Delegations {
|
|
if nowAmt := delegationNow.Amount; delegationNow.DelegatorAddress == snapshotAddr {
|
|
utils.Logger().Info().
|
|
RawJSON("delegation-snapshot", []byte(delegationSnapshot.String())).
|
|
RawJSON("delegation-current", []byte(delegationNow.String())).
|
|
Uint64("initial-slash-debt", slashDebt.Uint64()).
|
|
Str("rate", rate.String()).
|
|
Msg("attempt to apply slashing based on snapshot amount to current state")
|
|
// Current delegation has some money and slashdebt is still not paid off
|
|
// so contribute as much as can with current delegation amount
|
|
if err := payDownAsMuchAsCan(
|
|
snapshot, current, slashDebt, nowAmt, slashDiff,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
// NOTE Assume did as much as could above, now check the undelegations
|
|
for _, undelegate := range delegationNow.Undelegations {
|
|
// the epoch matters, only those undelegation
|
|
// such that epoch>= doubleSignEpoch should be slashable
|
|
if undelegate.Epoch.Cmp(doubleSignEpoch) >= 0 {
|
|
if slashDebt.Cmp(common.Big0) <= 0 {
|
|
utils.Logger().Info().
|
|
RawJSON("delegation-snapshot", []byte(delegationSnapshot.String())).
|
|
RawJSON("delegation-current", []byte(delegationNow.String())).
|
|
Msg("paid off the slash debt")
|
|
break
|
|
}
|
|
nowAmt := undelegate.Amount
|
|
if err := payDownAsMuchAsCan(
|
|
snapshot, current, slashDebt, nowAmt, slashDiff,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
if nowAmt.Cmp(common.Big0) == 0 {
|
|
// TODO need to remove the undelegate
|
|
utils.Logger().Info().
|
|
RawJSON("delegation-snapshot", []byte(delegationSnapshot.String())).
|
|
RawJSON("delegation-current", []byte(delegationNow.String())).
|
|
Msg("delegation amount after paying slash debt is 0")
|
|
}
|
|
}
|
|
}
|
|
|
|
// if we still have a slashdebt
|
|
// even after taking away from delegation amount
|
|
// and even after taking away from undelegate,
|
|
// then we need to take from their pending rewards
|
|
if slashDebt.Cmp(common.Big0) == 1 {
|
|
nowAmt := delegationNow.Reward
|
|
utils.Logger().Info().
|
|
RawJSON("delegation-snapshot", []byte(delegationSnapshot.String())).
|
|
RawJSON("delegation-current", []byte(delegationNow.String())).
|
|
Uint64("slash-debt", slashDebt.Uint64()).
|
|
Uint64("now-amount-reward", nowAmt.Uint64()).
|
|
Msg("needed to dig into reward to pay off slash debt")
|
|
if err := payDownAsMuchAsCan(
|
|
snapshot, current, slashDebt, nowAmt, slashDiff,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// NOTE only need to pay snitch here,
|
|
// they only get half of what was actually dispersed
|
|
halfOfSlashDebt := new(big.Int).Div(slashDiff.TotalSlashed, common.Big2)
|
|
slashDiff.TotalSnitchReward.Add(slashDiff.TotalSnitchReward, halfOfSlashDebt)
|
|
utils.Logger().Info().
|
|
RawJSON("delegation-snapshot", []byte(delegationSnapshot.String())).
|
|
RawJSON("delegation-current", []byte(delegationNow.String())).
|
|
Uint64("reporter-reward", halfOfSlashDebt.Uint64()).
|
|
RawJSON("application", []byte(slashDiff.String())).
|
|
Msg("completed an application of slashing")
|
|
state.AddBalance(reporter, halfOfSlashDebt)
|
|
slashTrack.TotalSnitchReward.Add(
|
|
slashTrack.TotalSnitchReward, slashDiff.TotalSnitchReward,
|
|
)
|
|
slashTrack.TotalSlashed.Add(
|
|
slashTrack.TotalSlashed, slashDiff.TotalSlashed,
|
|
)
|
|
}
|
|
}
|
|
// after the loops, paid off as much as could
|
|
if slashDebt.Cmp(common.Big0) == -1 {
|
|
x1, _ := rlp.EncodeToBytes(snapshot)
|
|
x2, _ := rlp.EncodeToBytes(current)
|
|
utils.Logger().Error().Str("slash-rate", rate.String()).
|
|
Str("snapshot-rlp", hex.EncodeToString(x1)).
|
|
Str("current-rlp", hex.EncodeToString(x2)).
|
|
Msg("slash debt not paid off")
|
|
return errors.Wrapf(errSlashDebtCannotBeNegative, "amt %v", slashDebt)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TODO Need to keep a record in off-chain db of all the slashes?
|
|
|
|
// Apply ..
|
|
func Apply(
|
|
chain staking.ValidatorSnapshotReader, state *state.DB,
|
|
slashes Records, rate numeric.Dec,
|
|
) (*Application, error) {
|
|
slashDiff := &Application{big.NewInt(0), big.NewInt(0)}
|
|
for _, slash := range slashes {
|
|
// TODO Probably won't happen but we probably should
|
|
// be expilict about reading the right epoch validator snapshot,
|
|
// because it needs to be the epoch of which the double sign
|
|
// occurred
|
|
snapshot, err := chain.ReadValidatorSnapshot(
|
|
slash.Offender,
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, errors.Errorf(
|
|
"could not find validator %s",
|
|
common2.MustAddressToBech32(slash.Offender),
|
|
)
|
|
}
|
|
|
|
current, err := state.ValidatorWrapper(slash.Offender)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(
|
|
errValidatorNotFoundDuringSlash, " %s ", err.Error(),
|
|
)
|
|
}
|
|
// NOTE invariant: first delegation is the validators own
|
|
// stake, rest are external delegations.
|
|
// Bottom line: everyone will be slashed under the same rule.
|
|
if err := delegatorSlashApply(
|
|
snapshot, current, rate, state,
|
|
slash.Reporter, slash.Evidence.Epoch, slashDiff,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// finally, kick them off forever
|
|
current.Banned, current.Active = true, false
|
|
utils.Logger().Info().
|
|
RawJSON("delegation-current", []byte(current.String())).
|
|
RawJSON("slash", []byte(slash.String())).
|
|
Msg("about to update staking info for a validator after a slash")
|
|
|
|
if err := state.UpdateValidatorWrapper(
|
|
snapshot.Address, current,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return slashDiff, nil
|
|
}
|
|
|
|
// Rate is the slashing % rate
|
|
func Rate(doubleSignerCount, committeeSize int) numeric.Dec {
|
|
if doubleSignerCount == 0 || committeeSize == 0 {
|
|
return zero
|
|
}
|
|
switch doubleSignerCount {
|
|
case 1:
|
|
return oneDoubleSignerRate
|
|
default:
|
|
return numeric.NewDec(
|
|
int64(doubleSignerCount),
|
|
).Quo(numeric.NewDec(int64(committeeSize)))
|
|
}
|
|
}
|
|
|