The core protocol of WoopChain
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.
 
 
 
woop/staking/slash/double-sign.go

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)))
}
}