diff --git a/hmy/staking.go b/hmy/staking.go index 83e800544c..de5a7b98a9 100644 --- a/hmy/staking.go +++ b/hmy/staking.go @@ -353,7 +353,7 @@ func (hmy *Harmony) GetValidatorInformation( } computed := availability.ComputeCurrentSigning( - snapshot.Validator, wrapper, + snapshot.Validator, wrapper, bc.Config().IsHIP32(now), ) lastBlockOfEpoch := shard.Schedule.EpochLastBlock(hmy.BeaconChain.CurrentBlock().Header().Epoch().Uint64()) @@ -479,7 +479,7 @@ func (hmy *Harmony) GetMedianRawStakeSnapshot() ( // Compute for next epoch epoch := big.NewInt(0).Add(hmy.CurrentBlock().Epoch(), big.NewInt(1)) instance := shard.Schedule.InstanceForEpoch(epoch) - return committee.NewEPoSRound(epoch, hmy.BlockChain, hmy.BlockChain.Config().IsEPoSBound35(epoch), instance.SlotsLimit(), int(instance.NumShards())) + return committee.NewEPoSRound(epoch, hmy.BlockChain, instance.SlotsLimit(), int(instance.NumShards())) }, ) if err != nil { @@ -634,7 +634,7 @@ func (hmy *Harmony) GetTotalStakingSnapshot() *big.Int { snapshot, _ := hmy.BlockChain.ReadValidatorSnapshot(candidates[i]) validator, _ := hmy.BlockChain.ReadValidatorInformation(candidates[i]) if !committee.IsEligibleForEPoSAuction( - snapshot, validator, + snapshot, validator, hmy.BlockChain.Config(), ) { continue } diff --git a/internal/params/config.go b/internal/params/config.go index 421ae3223c..91e121c5f0 100644 --- a/internal/params/config.go +++ b/internal/params/config.go @@ -358,6 +358,7 @@ var ( big.NewInt(0), // MaxRateEpoch big.NewInt(0), // MaxRateEpoch big.NewInt(0), + big.NewInt(0), } // TestChainConfig ... @@ -406,6 +407,7 @@ var ( big.NewInt(0), // MaxRateEpoch big.NewInt(0), // MaxRateEpoch big.NewInt(0), + big.NewInt(0), } // TestRules ... @@ -578,6 +580,10 @@ type ChainConfig struct { // MaxRateEpoch will make sure the validator max-rate is at least equal to the minRate + the validator max-rate-increase MaxRateEpoch *big.Int `json:"max-rate-epoch,omitempty"` + + // vote power feature https://github.com/harmony-one/harmony/pull/4683 + // if crosslink are not sent for an entire epoch signed and toSign will be 0 and 0. when that happen, next epoch there will no shard 1 validator elected in the committee. + HIP32Epoch *big.Int `json:"hip32-epoch,omitempty"` } // String implements the fmt.Stringer interface. @@ -833,6 +839,10 @@ func (c *ChainConfig) IsValidatorCodeFix(epoch *big.Int) bool { return isForked(c.ValidatorCodeFixEpoch, epoch) } +func (c *ChainConfig) IsHIP32(epoch *big.Int) bool { + return isForked(c.HIP32Epoch, epoch) +} + func (c *ChainConfig) IsHIP30(epoch *big.Int) bool { return isForked(c.HIP30Epoch, epoch) } diff --git a/node/node_handler.go b/node/node_handler.go index b745ca7136..3479cd3afd 100644 --- a/node/node_handler.go +++ b/node/node_handler.go @@ -328,6 +328,7 @@ func getCrosslinkHeadersForShards(shardChain core.BlockChain, curBlock *types.Bl // PostConsensusProcessing is called by consensus participants, after consensus is done, to: // 1. [leader] send new block to the client // 2. [leader] send cross shard tx receipts to destination shard +// newBlock is already inserted. func (node *Node) PostConsensusProcessing(newBlock *types.Block) error { if node.Consensus.IsLeader() { if node.IsRunningBeaconChain() { @@ -386,7 +387,7 @@ func (node *Node) PostConsensusProcessing(newBlock *types.Block) error { return nil } computed := availability.ComputeCurrentSigning( - snapshot.Validator, wrapper, + snapshot.Validator, wrapper, node.Beaconchain().Config().IsHIP32(newBlock.Epoch()), ) lastBlockOfEpoch := shard.Schedule.EpochLastBlock(node.Beaconchain().CurrentBlock().Header().Epoch().Uint64()) diff --git a/shard/committee/assignment.go b/shard/committee/assignment.go index 4978b61559..5fba3e6200 100644 --- a/shard/committee/assignment.go +++ b/shard/committee/assignment.go @@ -35,6 +35,7 @@ type StakingCandidatesReader interface { ) (*staking.ValidatorWrapper, error) ReadValidatorSnapshot(addr common.Address) (*staking.ValidatorSnapshot, error) ValidatorCandidates() []common.Address + Config() *params.ChainConfig } // CandidatesForEPoS .. @@ -73,7 +74,7 @@ func (p CandidateOrder) MarshalJSON() ([]byte, error) { // NewEPoSRound runs a fresh computation of EPoS using // latest data always -func NewEPoSRound(epoch *big.Int, stakedReader StakingCandidatesReader, isExtendedBound bool, slotsLimit, shardCount int) ( +func NewEPoSRound(epoch *big.Int, stakedReader StakingCandidatesReader, slotsLimit, shardCount int) ( *CompletedEPoSRound, error, ) { eligibleCandidate, err := prepareOrders(stakedReader, slotsLimit, shardCount) @@ -84,7 +85,7 @@ func NewEPoSRound(epoch *big.Int, stakedReader StakingCandidatesReader, isExtend epoch, ) median, winners := effective.Apply( - eligibleCandidate, maxExternalSlots, isExtendedBound, + eligibleCandidate, maxExternalSlots, stakedReader.Config().IsEPoSBound35(epoch), ) auctionCandidates := make([]*CandidateOrder, len(eligibleCandidate)) @@ -159,7 +160,7 @@ func prepareOrders( if err != nil { return nil, err } - if !IsEligibleForEPoSAuction(snapshot, validator) { + if !IsEligibleForEPoSAuction(snapshot, validator, stakedReader.Config()) { continue } @@ -208,7 +209,7 @@ func prepareOrders( } // IsEligibleForEPoSAuction .. -func IsEligibleForEPoSAuction(snapshot *staking.ValidatorSnapshot, validator *staking.ValidatorWrapper) bool { +func IsEligibleForEPoSAuction(snapshot *staking.ValidatorSnapshot, validator *staking.ValidatorWrapper, config *params.ChainConfig) bool { // This original condition to check whether a validator is in last committee is not stable // because cross-links may arrive after the epoch ends and it still got counted into the // NumBlocksToSign, making this condition to be true when the validator is actually not in committee @@ -219,7 +220,7 @@ func IsEligibleForEPoSAuction(snapshot *staking.ValidatorSnapshot, validator *st // validator was in last epoch's committee // validator with below-threshold signing activity won't be considered for next epoch // and their status will be turned to inactive in FinalizeNewBlock - computed := availability.ComputeCurrentSigning(snapshot.Validator, validator) + computed := availability.ComputeCurrentSigning(snapshot.Validator, validator, config.IsHIP32(snapshot.Epoch)) if computed.IsBelowThreshold { return false } @@ -349,7 +350,7 @@ func eposStakedCommittee( } // TODO(audit): make sure external validator BLS key are also not duplicate to Harmony's keys - completedEPoSRound, err := NewEPoSRound(epoch, stakerReader, stakerReader.Config().IsEPoSBound35(epoch), s.SlotsLimit(), shardCount) + completedEPoSRound, err := NewEPoSRound(epoch, stakerReader, s.SlotsLimit(), shardCount) if err != nil { return nil, err diff --git a/staking/availability/interface.go b/staking/availability/interface.go index 5b5c781943..f7df8482ef 100644 --- a/staking/availability/interface.go +++ b/staking/availability/interface.go @@ -4,6 +4,7 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" + "github.com/harmony-one/harmony/internal/params" staking "github.com/harmony-one/harmony/staking/types" ) @@ -12,6 +13,7 @@ type Reader interface { ReadValidatorSnapshot( addr common.Address, ) (*staking.ValidatorSnapshot, error) + Config() *params.ChainConfig } // RoundHeader is the interface of block.Header for calculating the BallotResult. diff --git a/staking/availability/measure.go b/staking/availability/measure.go index 3c7dcb8eb9..34f573df56 100644 --- a/staking/availability/measure.go +++ b/staking/availability/measure.go @@ -3,9 +3,8 @@ package availability import ( "math/big" - "github.com/harmony-one/harmony/core/state" - "github.com/ethereum/go-ethereum/common" + "github.com/harmony-one/harmony/core/state" "github.com/harmony-one/harmony/crypto/bls" "github.com/harmony-one/harmony/internal/utils" "github.com/harmony-one/harmony/numeric" @@ -141,9 +140,7 @@ func IncrementValidatorSigningCounts( } // ComputeCurrentSigning returns (signed, toSign, quotient, error) -func ComputeCurrentSigning( - snapshot, wrapper *staking.ValidatorWrapper, -) *staking.Computed { +func ComputeCurrentSigning(snapshot, wrapper *staking.ValidatorWrapper, isHip32 bool) *staking.Computed { statsNow, snapSigned, snapToSign := wrapper.Counters, snapshot.Counters.NumBlocksSigned, @@ -158,7 +155,9 @@ func ComputeCurrentSigning( ) if toSign.Cmp(common.Big0) == 0 { - computed.IsBelowThreshold = false + if isHip32 { + computed.IsBelowThreshold = false + } return computed } @@ -209,7 +208,7 @@ func ComputeAndMutateEPOSStatus( return err } - computed := ComputeCurrentSigning(snapshot.Validator, wrapper) + computed := ComputeCurrentSigning(snapshot.Validator, wrapper, bc.Config().IsHIP32(snapshot.Epoch)) utils.Logger(). Info().Msg("check if signing percent is meeting required threshold") diff --git a/staking/availability/measure_test.go b/staking/availability/measure_test.go index 9a35ad80eb..668b482326 100644 --- a/staking/availability/measure_test.go +++ b/staking/availability/measure_test.go @@ -7,10 +7,10 @@ import ( "reflect" "testing" - "github.com/harmony-one/harmony/crypto/bls" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/rlp" + "github.com/harmony-one/harmony/crypto/bls" + "github.com/harmony-one/harmony/internal/params" "github.com/harmony-one/harmony/numeric" "github.com/harmony-one/harmony/shard" "github.com/harmony-one/harmony/staking/effective" @@ -188,7 +188,48 @@ func TestComputeCurrentSigning(t *testing.T) { snapWrapper := makeTestWrapper(common.Address{}, test.snapSigned, test.snapToSign) curWrapper := makeTestWrapper(common.Address{}, test.curSigned, test.curToSign) - computed := ComputeCurrentSigning(&snapWrapper, &curWrapper) + computed := ComputeCurrentSigning(&snapWrapper, &curWrapper, false) + + if computed.Signed.Cmp(new(big.Int).SetInt64(test.diffSigned)) != 0 { + t.Errorf("test %v: computed signed not expected: %v / %v", + i, computed.Signed, test.diffSigned) + } + if computed.ToSign.Cmp(new(big.Int).SetInt64(test.diffToSign)) != 0 { + t.Errorf("test %v: computed to sign not expected: %v / %v", + i, computed.ToSign, test.diffToSign) + } + expPct := numeric.NewDec(test.pctNum).Quo(numeric.NewDec(test.pctDiv)) + if !computed.Percentage.Equal(expPct) { + t.Errorf("test %v: computed percentage not expected: %v / %v", + i, computed.Percentage, expPct) + } + if computed.IsBelowThreshold != test.isBelowThreshold { + t.Errorf("test %v: computed is below threshold `%v` expected: `%v`", + i, computed.IsBelowThreshold, test.isBelowThreshold) + } + } +} + +func TestComputeCurrentSigningHIP32(t *testing.T) { + tests := []struct { + snapSigned, curSigned, diffSigned int64 + snapToSign, curToSign, diffToSign int64 + pctNum, pctDiv int64 + isBelowThreshold bool + }{ + {0, 0, 0, 0, 0, 0, 0, 1, false}, + {0, 1, 1, 0, 1, 1, 1, 1, false}, + {0, 2, 2, 0, 3, 3, 2, 3, true}, + {0, 1, 1, 0, 3, 3, 1, 3, true}, + {100, 225, 125, 200, 350, 150, 5, 6, false}, + {100, 200, 100, 200, 350, 150, 2, 3, true}, + {100, 200, 100, 200, 400, 200, 1, 2, true}, + } + for i, test := range tests { + snapWrapper := makeTestWrapper(common.Address{}, test.snapSigned, test.snapToSign) + curWrapper := makeTestWrapper(common.Address{}, test.curSigned, test.curToSign) + + computed := ComputeCurrentSigning(&snapWrapper, &curWrapper, true) if computed.Signed.Cmp(new(big.Int).SetInt64(test.diffSigned)) != 0 { t.Errorf("test %v: computed signed not expected: %v / %v", @@ -204,7 +245,7 @@ func TestComputeCurrentSigning(t *testing.T) { i, computed.Percentage, expPct) } if computed.IsBelowThreshold != test.isBelowThreshold { - t.Errorf("test %v: computed is below threshold not expected: %v / %v", + t.Errorf("test %v: computed is below threshold `%v`, expected: `%v`", i, computed.IsBelowThreshold, test.isBelowThreshold) } } @@ -628,16 +669,22 @@ func (state testStateDB) GetCode(addr common.Address, isValidatorCode bool) []by } // testReader is the fake Reader for testing -type testReader map[common.Address]staking.ValidatorWrapper +type testReader struct { + m map[common.Address]staking.ValidatorWrapper + config *params.ChainConfig +} // newTestReader creates an empty test reader func newTestReader() testReader { - reader := make(testReader) - return reader + m := make(map[common.Address]staking.ValidatorWrapper) + return testReader{ + m: m, + config: ¶ms.ChainConfig{}, + } } func (reader testReader) ReadValidatorSnapshot(addr common.Address) (*staking.ValidatorSnapshot, error) { - wrapper, ok := reader[addr] + wrapper, ok := reader.m[addr] if !ok { return nil, errors.New("not a valid validator address") } @@ -646,8 +693,12 @@ func (reader testReader) ReadValidatorSnapshot(addr common.Address) (*staking.Va }, nil } +func (reader testReader) Config() *params.ChainConfig { + return reader.config +} + func (reader testReader) updateValidatorWrapper(addr common.Address, val *staking.ValidatorWrapper) { - reader[addr] = *val + reader.m[addr] = *val } func makeTestShardState(numShards, numSlots int) *shard.State {