Skip to content
Open
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
90 changes: 70 additions & 20 deletions consensus/XDPoS/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,28 @@ type SignerTypes struct {
MissingSigners []common.Address
}

// MasternodesStatus reports the node set at a block, split into masternodes,
// penalties and standby nodes. From the TIPUpgradeReward fork onwards the standby
// pool is further broken into the protector and observer reward tiers (signalled by
// TipUpgradeReward); candidates beyond the tier caps stay in Standbynodes, so
// Masternodes + Penalty + Protector + Observer + Standbynodes still reconciles to
// the full candidate set.
type MasternodesStatus struct {
Epoch uint64
Number uint64
Round types.Round
MasternodesLen int
Masternodes []common.Address
PenaltyLen int
Penalty []common.Address
StandbynodesLen int
Standbynodes []common.Address
Error error
Epoch uint64
Number uint64
Round types.Round
tipUpgradeReward bool // whether the protector/observer tiers are active at this block
MasternodesLen int
Comment thread
wanwiset25 marked this conversation as resolved.
Masternodes []common.Address
PenaltyLen int
Penalty []common.Address
ProtectorLen int `json:",omitempty"`
Protectornodes []common.Address `json:",omitempty"`
ObserverLen int `json:",omitempty"`
Observernodes []common.Address `json:",omitempty"`
StandbynodesLen int `json:",omitempty"`
Standbynodes []common.Address `json:",omitempty"`
Error error
}

type AccountEpochReward struct {
Expand Down Expand Up @@ -198,6 +209,13 @@ func (api *API) GetSignersAtHash(hash common.Hash) ([]common.Address, error) {
return api.XDPoS.GetAuthorisedSignersFromSnapshot(api.chain, header)
}

// GetMasternodesByNumber reports the node set at the given block: masternodes,
// penalties and standby nodes. From the TIPUpgradeReward fork onwards it also splits
// the standby pool into the protector and observer reward tiers (the standby list is
// already stake-descending, so the split is just a slice).
//
// The tiering is snapshot/consensus-consistent, not reward-payout-identical: it
// matches the epoch snapshot used here, whereas the reward hook reads live state.
func (api *API) GetMasternodesByNumber(number *rpc.BlockNumber) MasternodesStatus {
var header *types.Header
if number == nil || *number == rpc.LatestBlockNumber {
Expand Down Expand Up @@ -235,22 +253,54 @@ func (api *API) GetMasternodesByNumber(number *rpc.BlockNumber) MasternodesStatu
epochNum := api.XDPoS.config.V2.SwitchEpoch + uint64(round)/api.XDPoS.config.Epoch
masterNodes := api.XDPoS.EngineV2.GetMasternodes(api.chain, header)
penalties := api.XDPoS.EngineV2.GetPenalties(api.chain, header)
standbynodes := api.XDPoS.EngineV2.GetStandbynodes(api.chain, header)
standbyPool := api.XDPoS.EngineV2.GetStandbynodes(api.chain, header)

info := MasternodesStatus{
Epoch: epochNum,
Number: header.Number.Uint64(),
Round: round,
MasternodesLen: len(masterNodes),
Masternodes: masterNodes,
PenaltyLen: len(penalties),
Penalty: penalties,
StandbynodesLen: len(standbynodes),
Standbynodes: standbynodes,
Epoch: epochNum,
Number: header.Number.Uint64(),
Round: round,
tipUpgradeReward: api.chain.Config().IsTIPUpgradeReward(header.Number),
MasternodesLen: len(masterNodes),
Masternodes: masterNodes,
PenaltyLen: len(penalties),
Penalty: penalties,
}

// Before the reward upgrade there are no tiers; the whole standby pool stays
// standby (the caps are ignored in that case, so any value is fine here).
if !info.tipUpgradeReward {
info.splitStandbyPool(standbyPool, 0, 0)
return info
}

cfg := api.XDPoS.config.V2.Config(uint64(round))
info.splitStandbyPool(standbyPool, cfg.MaxProtectorNodes, cfg.MaxObverserNodes)
return info
}

// splitStandbyPool partitions the stake-descending standby pool into the reward
// tiers. Before the TIPUpgradeReward fork the whole pool stays standby; from the
// fork onwards the protector and observer tiers take the top maxProtector and
// maxObserver candidates respectively, and any remainder stays standby. The three
// tiers always concatenate back to the full pool, so the totals reconcile.
func (info *MasternodesStatus) splitStandbyPool(standbyPool []common.Address, maxProtector, maxObserver int) {
if !info.tipUpgradeReward {
info.Standbynodes = standbyPool
info.StandbynodesLen = len(standbyPool)
return
}

protectorEnd := min(maxProtector, len(standbyPool))
observerEnd := min(protectorEnd+maxObserver, len(standbyPool))

info.Protectornodes = standbyPool[:protectorEnd]
info.ProtectorLen = len(info.Protectornodes)
info.Observernodes = standbyPool[protectorEnd:observerEnd]
info.ObserverLen = len(info.Observernodes)
info.Standbynodes = standbyPool[observerEnd:]
info.StandbynodesLen = len(info.Standbynodes)
}

// Get current vote pool and timeout pool content and missing messages
func (api *API) GetLatestPoolStatus() MessageStatus {
header := api.chain.CurrentHeader()
Expand Down
77 changes: 77 additions & 0 deletions consensus/XDPoS/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,83 @@ func TestJsonNumberToBigInt(t *testing.T) {
}
}

// standbyAddrs returns n deterministic, distinct addresses standing in for a
// stake-descending standby pool.
func standbyAddrs(n int) []common.Address {
out := make([]common.Address, n)
for i := range out {
out[i][0] = byte(i + 1)
}
return out
}

// TestSplitStandbyPoolPreUpgrade asserts that before the TIPUpgradeReward fork the
// whole standby pool is returned unsplit and no protector/observer tier is set,
// regardless of the configured caps.
func TestSplitStandbyPoolPreUpgrade(t *testing.T) {
pool := standbyAddrs(5)

info := MasternodesStatus{tipUpgradeReward: false}
info.splitStandbyPool(pool, 3, 1) // caps must be ignored pre-upgrade

assert.Equal(t, pool, info.Standbynodes)
assert.Equal(t, 5, info.StandbynodesLen)
assert.Nil(t, info.Protectornodes)
assert.Nil(t, info.Observernodes)
assert.Zero(t, info.ProtectorLen)
assert.Zero(t, info.ObserverLen)
}

// TestSplitStandbyPoolPostUpgrade asserts that from the TIPUpgradeReward fork the
// standby pool is split deterministically according to the protector/observer caps,
// clamped to the pool size, and that the three tiers always reconcile back to the
// full pool.
func TestSplitStandbyPoolPostUpgrade(t *testing.T) {
pool := standbyAddrs(10)

tests := []struct {
name string
maxProtector int
maxObserver int
wantProtector int
wantObserver int
wantStandby int
}{
{"caps within pool", 3, 4, 3, 4, 3},
{"exact fit", 4, 6, 4, 6, 0},
{"only protector tier", 4, 0, 4, 0, 6},
{"zero caps keep all standby", 0, 0, 0, 0, 10},
{"protector cap exceeds pool", 20, 5, 10, 0, 0},
{"protector fills, observer overflows", 6, 20, 6, 4, 0},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
info := MasternodesStatus{tipUpgradeReward: true}
info.splitStandbyPool(pool, tt.maxProtector, tt.maxObserver)

assert.Equal(t, tt.wantProtector, info.ProtectorLen)
assert.Equal(t, tt.wantObserver, info.ObserverLen)
assert.Equal(t, tt.wantStandby, info.StandbynodesLen)
assert.Len(t, info.Protectornodes, tt.wantProtector)
assert.Len(t, info.Observernodes, tt.wantObserver)
assert.Len(t, info.Standbynodes, tt.wantStandby)

// Deterministic order: protectors are the top slice, observers the
// next, standbys the remainder.
assert.Equal(t, pool[:tt.wantProtector], info.Protectornodes)
assert.Equal(t, pool[tt.wantProtector:tt.wantProtector+tt.wantObserver], info.Observernodes)

// Reconciliation: the tiers concatenate back to the full pool.
got := make([]common.Address, 0, len(pool))
got = append(got, info.Protectornodes...)
got = append(got, info.Observernodes...)
got = append(got, info.Standbynodes...)
assert.Equal(t, pool, got)
})
}
}

func TestAPIGetConfig(t *testing.T) {
chain := newConfigChainMockWithCurrent(1500)
api := &API{chain: chain}
Expand Down
Loading