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
47 changes: 41 additions & 6 deletions params/config_compat.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"strings"

"github.com/XinFinOrg/XDPoSChain/common"
"github.com/XinFinOrg/XDPoSChain/log"
)

// CheckCompatible checks whether scheduled fork transitions have been imported
Expand Down Expand Up @@ -190,18 +191,52 @@ func checkXDCSystemContractCompatible(stored, newcfg *ChainConfig, head *big.Int

// checkV2ScheduleCompatible compares the historically active V2 round configs
// between stored and new chain config.
func checkV2ScheduleCompatible(stored, newcfg *V2, xdposRound *uint64) *ConfigCompatError {
activeRound := v2ActiveCompatRound(stored, newcfg, xdposRound)
for _, round := range v2HistoricalRounds(stored, newcfg, activeRound) {
func checkV2ScheduleCompatible(stored, candidate *V2, xdposRound *uint64) *ConfigCompatError {
activeRound := v2ActiveCompatRound(stored, candidate, xdposRound)
for _, round := range v2HistoricalRounds(stored, candidate, activeRound) {
storedCfg, storedOK := stored.AllConfigs[round]
newCfg, newOK := newcfg.AllConfigs[round]
if !storedOK || !newOK || !v2ConsensusConfigEqual(storedCfg, newCfg) {
return newCompatError(fmt.Sprintf("XDPoS.V2 config at round %d", round), xdposV2CompatBlock(stored), xdposV2CompatBlock(newcfg))
candidateCfg, newOK := candidate.AllConfigs[round]
if !storedOK || !newOK || !v2ConsensusConfigEqual(storedCfg, candidateCfg) {
log.Warn(strings.Repeat("-", 153))
log.Warn("V2 config mismatch", "stored", stored.StableLogValue())
log.Warn(strings.Repeat("-", 153))
log.Warn("V2 config mismatch", "candidate", candidate.StableLogValue())
log.Warn(strings.Repeat("-", 153))
log.Warn("V2 config mismatch",
"round", round,
"activeRound", activeRound,
"storedOK", storedOK,
"newOK", newOK,
"storedCfg", v2ConsensusConfigSnapshot(storedCfg),
"candidateCfg", v2ConsensusConfigSnapshot(candidateCfg),
)
log.Warn(strings.Repeat("-", 153))
return newCompatError(fmt.Sprintf("XDPoS.V2 config at round %d", round), xdposV2CompatBlock(stored), xdposV2CompatBlock(candidate))
}
}
return nil
}

func v2ConsensusConfigSnapshot(cfg *V2Config) map[string]any {
if cfg == nil {
return nil
}
return map[string]any{
"maxMasternodes": cfg.MaxMasternodes,
"maxProtectorNodes": cfg.MaxProtectorNodes,
"maxObverserNodes": cfg.MaxObverserNodes,
"switchRound": cfg.SwitchRound,
"minePeriod": cfg.MinePeriod,
"certThreshold": cfg.CertThreshold,
"masternodeReward": cfg.MasternodeReward,
"protectorReward": cfg.ProtectorReward,
"observerReward": cfg.ObserverReward,
"minimumMinerBlockPerEpoch": cfg.MinimumMinerBlockPerEpoch,
"limitPenaltyEpoch": cfg.LimitPenaltyEpoch,
"minimumSigningTx": cfg.MinimumSigningTx,
}
}

// v2ActiveCompatRound returns the latest round whose config must remain stable
// across restarts.
func v2ActiveCompatRound(stored, newcfg *V2, xdposRound *uint64) uint64 {
Expand Down
48 changes: 48 additions & 0 deletions params/config_xdpos.go
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,54 @@ func (v2 *V2) String() string {
return fmt.Sprintf("V2{SwitchEpoch: %v, SwitchBlock: %v, %s}", snapshot.switchEpoch, snapshot.switchBlock, snapshot.currentConfig.String())
}

type stableLogConfigEntry struct {
Round uint64 `json:"round"`
Config *V2Config `json:"config"`
}

type stableLogView struct {
SwitchEpoch uint64 `json:"switchEpoch"`
SwitchBlock string `json:"switchBlock"`
CurrentConfig *V2Config `json:"currentConfig"`
AllConfigs []stableLogConfigEntry `json:"allConfigs"`
ConfigIndex []uint64 `json:"configIndex"`
}

// StableLogValue returns a full, deterministic structured value for startup
// logs. Callers can pass the returned object directly as a field value to
// avoid JSON string escaping in terminal log output.
func (v2 *V2) StableLogValue() stableLogView {
if v2 == nil {
return stableLogView{}
}

snapshot := snapshotV2ReadOnly(v2)

keys := slices.Collect(maps.Keys(snapshot.allConfigs))
slices.Sort(keys)

allConfigs := make([]stableLogConfigEntry, 0, len(keys))
for _, round := range keys {
allConfigs = append(allConfigs, stableLogConfigEntry{
Round: round,
Config: snapshot.allConfigs[round].Clone(),
})
}

var switchBlock string
if snapshot.switchBlock != nil {
switchBlock = snapshot.switchBlock.String()
}

return stableLogView{
SwitchEpoch: snapshot.switchEpoch,
SwitchBlock: switchBlock,
CurrentConfig: snapshot.currentConfig.Clone(),
AllConfigs: allConfigs,
ConfigIndex: append([]uint64(nil), snapshot.configIndex...),
}
}

// Description returns a human-readable description of V2
// NOTE: don't append "\n" to end
func (v2 *V2) Description(indent int) string {
Expand Down
39 changes: 39 additions & 0 deletions params/config_xdpos_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package params
import (
"encoding/json"
"math/big"
"strings"
"sync"
"testing"

Expand Down Expand Up @@ -192,6 +193,44 @@ func TestV2StringConcurrentWithUpdateConfig(t *testing.T) {
wg.Wait()
}

func TestV2StableLogValueDeterministicAndComplete(t *testing.T) {
v2 := &V2{
SwitchEpoch: 63143,
SwitchBlock: big.NewInt(56828700),
CurrentConfig: &V2Config{
MaxMasternodes: 15,
},
AllConfigs: map[uint64]*V2Config{
10: {SwitchRound: 10, MaxMasternodes: 16},
0: {SwitchRound: 0, MaxMasternodes: 15},
},
configIndex: []uint64{10, 0},
}

firstBytes, err := json.Marshal(v2.StableLogValue())
assert.NoError(t, err)
secondBytes, err := json.Marshal(v2.StableLogValue())
assert.NoError(t, err)

first := string(firstBytes)
second := string(secondBytes)
assert.Equal(t, first, second)

assert.Contains(t, first, `"switchEpoch":63143`)
assert.Contains(t, first, `"switchBlock":"56828700"`)
assert.Contains(t, first, `"configIndex":[10,0]`)
assert.Contains(t, first, `"currentConfig"`)

idxRound0 := strings.Index(first, `"round":0`)
idxRound10 := strings.Index(first, `"round":10`)
if idxRound0 == -1 || idxRound10 == -1 {
t.Fatalf("expected allConfigs rounds in stable log output, got %q", first)
}
if idxRound0 > idxRound10 {
t.Fatalf("expected allConfigs rounds sorted ascending, got %q", first)
}
}

func TestBuildConfigIndex(t *testing.T) {
TestXDPoSMockChainConfig.XDPoS.V2.BuildConfigIndex()
index := TestXDPoSMockChainConfig.XDPoS.V2.ConfigIndex()
Expand Down
Loading