diff --git a/consensus/ibft/effective_power.go b/consensus/ibft/effective_power.go index cf53dc2..958a4e9 100644 --- a/consensus/ibft/effective_power.go +++ b/consensus/ibft/effective_power.go @@ -25,36 +25,18 @@ func (i *backendIBFT) effectiveVotingPowerSnapshot( if parentHeader == nil { return nil, effectivePowerSnapshot{}, fmt.Errorf("parent header required for stake-weighted snapshot at height %d", height) } + stateTx, err = i.executor.BeginTxn(parentHeader.StateRoot, parentHeader, types.ZeroAddress) + if err != nil { + return nil, effectivePowerSnapshot{}, err + } + // Block 1 in direct-PoS chains has parent=genesis(0). Genesis bootstrap stakes // are marked joinedAtBlock=1, so snapshot(0) is intentionally not mature yet. // Keep block 1 in unit voting mode and enable stake-weighted mode from block 2. parentPoSActive := parentHeader != nil && parentHeader.Number > 0 && i.forkManager.IsPosActive(parentHeader.Number) heightPoSActive := i.forkManager.IsPosActive(height) stakeWeightedActive := parentPoSActive - snapshotHeight, err := i.stakeSnapshotHeight(height, parentHeader, stakeWeightedActive) - if err != nil { - return nil, effectivePowerSnapshot{}, err - } - - snapshotHeader := parentHeader - if stakeWeightedActive && snapshotHeight != parentHeader.Number { - if i.blockchain == nil { - return nil, effectivePowerSnapshot{}, fmt.Errorf( - "stake-weighted voting power snapshot requires blockchain header lookup: height=%d snapshotHeight=%d parent=%d", - height, - snapshotHeight, - parentHeader.Number, - ) - } - - var ok bool - snapshotHeader, ok = i.blockchain.GetHeaderByNumber(snapshotHeight) - if !ok { - return nil, effectivePowerSnapshot{}, fmt.Errorf("stake-weighted voting power snapshot header not found at height %d", snapshotHeight) - } - } - - stateTx, err = i.executor.BeginTxn(snapshotHeader.StateRoot, snapshotHeader, types.ZeroAddress) + snapshotHeight, err := stakeSnapshotHeight(height, parentHeader, stakeWeightedActive) if err != nil { return nil, effectivePowerSnapshot{}, err } @@ -158,51 +140,22 @@ func (i *backendIBFT) snapshotVotingPowers( }, nil } -func (i *backendIBFT) stakeSnapshotHeight(height uint64, parentHeader *types.Header, stakeWeightedActive bool) (uint64, error) { +func stakeSnapshotHeight(height uint64, parentHeader *types.Header, stakeWeightedActive bool) (uint64, error) { if !stakeWeightedActive { // Unit voting mode ignores stake snapshots entirely. return 0, nil } - if parentHeader == nil { - if height == 0 { - return 0, fmt.Errorf("stake-weighted voting power snapshot requires parent state (height=0)") - } - - return height - 1, nil - } - - if i == nil || i.epochSize < 2 { - return parentHeader.Number, nil - } - - var snapshotHeight uint64 - if height%uint64(i.epochSize) == 0 { - if height == 0 { - return 0, fmt.Errorf("stake-weighted voting power snapshot requires parent state (height=0)") - } - snapshotHeight = height - 1 - } else { - snapshotHeight = (height / uint64(i.epochSize)) * uint64(i.epochSize) - } - - if snapshotHeight > parentHeader.Number { + if parentHeader != nil { return parentHeader.Number, nil } - if i.forkManager != nil && !i.forkManager.IsPosActive(snapshotHeight) { - for h := snapshotHeight + 1; h <= parentHeader.Number; h++ { - if i.forkManager.IsPosActive(h) { - return h, nil - } - } - - return parentHeader.Number, nil + if height == 0 { + return 0, fmt.Errorf("stake-weighted voting power snapshot requires parent state (height=0)") } - return snapshotHeight, nil + return height - 1, nil } - func hashEffectiveWeights(weights map[string]*big.Int, set validators.Validators, total, quorum *big.Int) types.Hash { hasher := make([]byte, 0, set.Len()*64+64) for idx := 0; idx < set.Len(); idx++ { diff --git a/consensus/ibft/effective_power_snapshot_height_test.go b/consensus/ibft/effective_power_snapshot_height_test.go index a17baef..28d04d8 100644 --- a/consensus/ibft/effective_power_snapshot_height_test.go +++ b/consensus/ibft/effective_power_snapshot_height_test.go @@ -5,72 +5,28 @@ import ( "github.com/stretchr/testify/require" "github.com/xgr-network/xgr-node/types" - "github.com/xgr-network/xgr-node/validators" ) func TestStakeSnapshotHeight_UnitModeReturnsZero(t *testing.T) { t.Parallel() - h, err := (&backendIBFT{}).stakeSnapshotHeight(12, nil, false) + h, err := stakeSnapshotHeight(12, nil, false) require.NoError(t, err) require.Equal(t, uint64(0), h) } -func TestStakeSnapshotHeight_PosWithoutEpochUsesParentHeaderNumber(t *testing.T) { +func TestStakeSnapshotHeight_PosUsesParentHeaderNumber(t *testing.T) { t.Parallel() - h, err := (&backendIBFT{}).stakeSnapshotHeight(100, &types.Header{Number: 77}, true) + h, err := stakeSnapshotHeight(100, &types.Header{Number: 77}, true) require.NoError(t, err) require.Equal(t, uint64(77), h) } -func TestStakeSnapshotHeight_PosUsesEpochBoundarySnapshot(t *testing.T) { - t.Parallel() - - backend := &backendIBFT{epochSize: 10} - - h, err := backend.stakeSnapshotHeight(32, &types.Header{Number: 31}, true) - require.NoError(t, err) - require.Equal(t, uint64(30), h) - - h, err = backend.stakeSnapshotHeight(40, &types.Header{Number: 39}, true) - require.NoError(t, err) - require.Equal(t, uint64(39), h) -} - -func TestStakeSnapshotHeight_PosClampsToFirstActiveHeight(t *testing.T) { - t.Parallel() - - backend := &backendIBFT{ - epochSize: 20, - forkManager: &votingPowerForkManager{active: func(h uint64) bool { return h >= 15 }}, - } - - h, err := backend.stakeSnapshotHeight(17, &types.Header{Number: 16}, true) - require.NoError(t, err) - require.Equal(t, uint64(15), h) -} - -func TestEffectiveVotingPowerSnapshot_RequiresBlockchainLookupForHistoricalStakeSnapshot(t *testing.T) { - t.Parallel() - - backend := &backendIBFT{ - epochSize: 10, - forkManager: &votingPowerForkManager{active: func(uint64) bool { return true }}, - } - valSet := validators.NewECDSAValidatorSet( - validators.NewECDSAValidator(types.StringToAddress("0x1000000000000000000000000000000000000001")), - ) - - _, _, err := backend.effectiveVotingPowerSnapshot(32, valSet, &types.Header{Number: 31}) - require.Error(t, err) - require.Contains(t, err.Error(), "requires blockchain header lookup") -} - func TestStakeSnapshotHeight_PosHeightZeroWithoutParentFails(t *testing.T) { t.Parallel() - _, err := (&backendIBFT{}).stakeSnapshotHeight(0, nil, true) + _, err := stakeSnapshotHeight(0, nil, true) require.Error(t, err) require.Contains(t, err.Error(), "requires parent state") } diff --git a/consensus/ibft/effective_power_stake_test.go b/consensus/ibft/effective_power_stake_test.go index d3dc9db..7b62445 100644 --- a/consensus/ibft/effective_power_stake_test.go +++ b/consensus/ibft/effective_power_stake_test.go @@ -9,7 +9,6 @@ import ( "github.com/hashicorp/go-hclog" "github.com/stretchr/testify/require" - "github.com/xgr-network/xgr-node/blockchain" "github.com/xgr-network/xgr-node/chain" "github.com/xgr-network/xgr-node/consensus/ibft/fork" "github.com/xgr-network/xgr-node/consensus/ibft/pos" @@ -349,88 +348,6 @@ func TestSnapshotVotingPowers_MidEpochStakeChangesDoNotAffectCurrentPower(t *tes assertVotingPower(nextParent, 21, 1_500, 500, 2_000) } -func TestEffectiveVotingPowerSnapshot_ValidatorFullUnstakeUsesEpochBoundaryStake(t *testing.T) { - const epochSize uint64 = 10 - - pool := newTesterAccountPool(t) - pool.add("A", "B", "C", "D") - valSet := pool.ValidatorSet() - validatorsByName := map[string]types.Address{ - "A": pool.get("A").Address(), - "B": pool.get("B").Address(), - "C": pool.get("C").Address(), - "D": pool.get("D").Address(), - } - - st := itrie.NewState(itrie.NewMemoryStorage()) - ex := state.NewExecutor(&chain.Params{Forks: chain.AllForksEnabled, BurnContract: map[uint64]types.Address{0: types.ZeroAddress}}, st, hclog.NewNullLogger()) - genesisRoot, err := ex.WriteGenesis(nil, types.Hash{}) - require.NoError(t, err) - ex.GetHash = func(*types.Header) state.GetHashByNumber { return func(uint64) types.Hash { return genesisRoot } } - - boundaryTx, err := ex.BeginTxn(genesisRoot, &types.Header{Number: 30}, types.ZeroAddress) - require.NoError(t, err) - contractState, err := stakingHelper.PredeployStakingSC(valSet, stakingHelper.PredeployParams{MinValidatorCount: 0, MaxValidatorCount: uint64(valSet.Len()), EpochSize: epochSize}) - require.NoError(t, err) - require.NoError(t, boundaryTx.SetAccountDirectly(staking.AddrStakingContract, contractState)) - stakes := map[string]uint64{"A": 10_000, "B": 20_000, "C": 30_000, "D": 40_000} - for name, stake := range stakes { - addr := validatorsByName[name] - setValidatorSelfStake(t, boundaryTx, addr, stake) - setNominalAndEffectiveWeight(boundaryTx, addr, 10_000, 10_000) - } - _, boundaryRoot, err := boundaryTx.Commit() - require.NoError(t, err) - - exited := validatorsByName["D"] - exitTx, err := ex.BeginTxn(boundaryRoot, &types.Header{Number: 31}, types.ZeroAddress) - require.NoError(t, err) - setValidatorSelfStakeRaw(t, exitTx, exited, 0, 0, types.ZeroAddress) - setNominalAndEffectiveWeight(exitTx, exited, 10_000, 10_000) - _, exitRoot, err := exitTx.Commit() - require.NoError(t, err) - - headers := make([]*types.Header, 32) - for n := uint64(0); n <= 31; n++ { - h := &types.Header{Number: n, Difficulty: 1, StateRoot: genesisRoot} - if n > 0 { - h.ParentHash = headers[n-1].Hash - } - switch n { - case 30: - h.StateRoot = boundaryRoot - case 31: - h.StateRoot = exitRoot - } - h.ComputeHash() - headers[n] = h - } - - backend := &backendIBFT{ - blockchain: blockchain.NewTestBlockchain(t, headers), - executor: ex, - epochSize: epochSize, - uptimeCfg: pos.UptimeConfig{MicroEpochSize: 2, MicroEpochNominalWeight: 10_000}, - forkManager: &votingPowerForkManager{active: func(uint64) bool { return true }}, - } - - powers, snap, err := backend.effectiveVotingPowerSnapshot(32, valSet, headers[31]) - require.NoError(t, err) - require.Equal(t, "100000", snap.totalVotingPower) - require.Equal(t, uint64(40_000), powers[types.AddressToString(exited)].Uint64(), "exited validator must retain last positive boundary stake while still in the epoch validator set") - require.NotEqual(t, uint64(1), powers[types.AddressToString(exited)].Uint64(), "stake-weighted path must not fall back to classic unit voting power") - for name, stake := range stakes { - addr := validatorsByName[name] - require.Equal(t, stake, powers[types.AddressToString(addr)].Uint64(), "validator %s must use stake-weighted boundary power", name) - } - - parentTx, err := ex.BeginTxn(headers[31].StateRoot, headers[31], types.ZeroAddress) - require.NoError(t, err) - _, _, err = backend.snapshotVotingPowers(32, 31, valSet, parentTx, true) - require.Error(t, err) - require.Contains(t, err.Error(), "missing effective stake", "parent state after full unstake intentionally lacks the exited validator stake") -} - func TestPoS_Slashing_ReducesValidatorSelfStakeAndVotingPowerWithoutDelegationDrift(t *testing.T) { validatorA := types.StringToAddress("0x1000000000000000000000000000000000000101") validatorB := types.StringToAddress("0x1000000000000000000000000000000000000102") diff --git a/e2e/pos_stakingv2_delegation_lifecycle_e2e_test.go b/e2e/pos_stakingv2_delegation_lifecycle_e2e_test.go index 25dbb56..0be2b3f 100644 --- a/e2e/pos_stakingv2_delegation_lifecycle_e2e_test.go +++ b/e2e/pos_stakingv2_delegation_lifecycle_e2e_test.go @@ -1542,7 +1542,7 @@ func queryStakerInfo(srv *framework.TestServer, from, staker types.Address) (*st return queryStakerInfoAt(srv, from, staker, ethgo.Latest) } -func queryStakerInfoAt(srv *framework.TestServer, from, staker types.Address, block ethgo.BlockNumber) (*stakingStakerInfo, error) { +func queryStakerInfoAt(srv *framework.TestServer, from, staker types.Address, block ethgo.BlockNumberOrHash) (*stakingStakerInfo, error) { m := abis.StakingABI.Methods["stakerInfo"] if m == nil { return nil, errors.New("staking ABI missing stakerInfo")