Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 11 additions & 58 deletions consensus/ibft/effective_power.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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++ {
Expand Down
52 changes: 4 additions & 48 deletions consensus/ibft/effective_power_snapshot_height_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
83 changes: 0 additions & 83 deletions consensus/ibft/effective_power_stake_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion e2e/pos_stakingv2_delegation_lifecycle_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading