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
65 changes: 50 additions & 15 deletions cmd/puppeth/wizard_genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,44 @@ func (w *wizard) loadGenesisInput() *GenesisInput {
}

// makeGenesis creates a new genesis struct based on some user input.
// masternodeRewards are the per-epoch reward figures derived from the staking
// threshold and the target yield, in whole coins.
type masternodeRewards struct {
TotalPerEpoch uint64 // rewards paid out to the whole masternode set each epoch
MasternodeReward float64 // reward of a single masternode (core validator)
ProtectorReward float64 // reward of a single protector, 80% of a masternode
ObserverReward float64 // reward of a single observer, 40% of a masternode
}

// calcMasternodeRewards derives the per-epoch rewards from the block period,
// the epoch length, the per-masternode staking threshold and the target reward
// yield in APY%. The per-node rewards are grossed up by the foundation cut, so
// that a masternode still earns the requested yield after the foundation has
// taken its percentage. It reports false when the period and epoch length do
// not add up to at least one epoch per year, in which case there is nothing to
// pay out and the caller should leave the reward config untouched.
func calcMasternodeRewards(period, epoch, threshold, yield uint64, masternodes int) (masternodeRewards, bool) {
if period == 0 || epoch == 0 {
return masternodeRewards{}, false
}
blocksPerYear := 31536000 / period
epochsPerYear := blocksPerYear / epoch
if epochsPerYear == 0 {
return masternodeRewards{}, false
}
rewardsPerYear := float64(threshold) * (float64(yield) / 100)
rewardPerEpochPerMNWithoutFoundation := rewardsPerYear / float64(epochsPerYear)
rewardPerEpochPerMN := rewardPerEpochPerMNWithoutFoundation * 100 / float64(100-common.RewardFoundationPercent)
totalRewardPerEpoch := rewardPerEpochPerMN * float64(masternodes)

return masternodeRewards{
TotalPerEpoch: uint64(math.Round(totalRewardPerEpoch)),
MasternodeReward: math.Round(rewardPerEpochPerMN*10000) / 10000,
ProtectorReward: math.Round(rewardPerEpochPerMN*0.8*10000) / 10000,
ObserverReward: math.Round(rewardPerEpochPerMN*0.4*10000) / 10000,
}, true
}

func (w *wizard) makeGenesis() {
genesis := &core.Genesis{
Timestamp: uint64(time.Now().Unix()),
Expand Down Expand Up @@ -267,27 +305,24 @@ func (w *wizard) makeGenesis() {
} else {
yield = uint64(w.readDefaultInt(10))
}
if genesis.Config.XDPoS.Period > 0 && genesis.Config.XDPoS.Epoch > 0 {
blocksPerYear := 31536000 / genesis.Config.XDPoS.Period
epochsPerYear := blocksPerYear / genesis.Config.XDPoS.Epoch
if epochsPerYear > 0 {
rewardsPerYear := float64(threshold) * (float64(yield) / float64(100))
rewardPerEpochPerMN := uint64(rewardsPerYear / float64(epochsPerYear))
totalRewardPerEpoch := rewardPerEpochPerMN * uint64(len(signers))
fmt.Println()
fmt.Println("Calculated Total Masternode rewards per epoch based on yield: ", totalRewardPerEpoch)
genesis.Config.XDPoS.Reward = totalRewardPerEpoch
genesis.Config.XDPoS.V2.CurrentConfig.MasternodeReward = math.Round(float64(rewardPerEpochPerMN)*1000) / 1000
genesis.Config.XDPoS.V2.CurrentConfig.ProtectorReward = math.Round(float64(rewardPerEpochPerMN)*0.8*1000) / 1000
genesis.Config.XDPoS.V2.CurrentConfig.ObserverReward = math.Round(float64(rewardPerEpochPerMN)*0.6*1000) / 1000

}
if rewards, ok := calcMasternodeRewards(genesis.Config.XDPoS.Period, genesis.Config.XDPoS.Epoch, threshold, yield, len(signers)); ok {
fmt.Println()
fmt.Println("Calculated Total Masternode rewards per epoch based on yield: ", rewards.TotalPerEpoch)
genesis.Config.XDPoS.Reward = rewards.TotalPerEpoch
genesis.Config.XDPoS.V2.CurrentConfig.MasternodeReward = rewards.MasternodeReward
genesis.Config.XDPoS.V2.CurrentConfig.ProtectorReward = rewards.ProtectorReward
genesis.Config.XDPoS.V2.CurrentConfig.ObserverReward = rewards.ObserverReward
}

fmt.Println()
fmt.Println("What is foundation wallet address (collect 10% of all rewards)? (default = xdc0000000000000000000000000000000000000068)")
if input == nil {
genesis.Config.XDPoS.FoundationWalletAddr = w.readDefaultAddress(common.FoundationAddrBinary)
} else {
if !common.IsHexAddress(input.FoundationWalletAddress) {
log.Crit("Invalid foundation wallet address", "address", input.FoundationWalletAddress)
}
genesis.Config.XDPoS.FoundationWalletAddr = common.HexToAddress(input.FoundationWalletAddress)
}
Comment thread
wanwiset25 marked this conversation as resolved.

// Validator Smart Contract Code
Expand Down
149 changes: 149 additions & 0 deletions cmd/puppeth/wizard_genesis_reward_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package main

import (
"flag"
"fmt"
"math"
"testing"

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

// Flags for TestCalcMasternodeRewardsManual, so the reward numbers for a
// candidate network can be eyeballed without editing the test. Note that
// -v is required: go test discards the output of a passing package without it.
//
// go test ./cmd/puppeth -run TestCalcMasternodeRewardsManual -v \
// -period=2 -epoch=900 -threshold=10_000_000 -yield=10 -signers=3
var (
flagPeriod = flag.Uint64("period", 2, "block period in seconds (manual reward calc)")
flagEpoch = flag.Uint64("epoch", 900, "blocks per epoch (manual reward calc)")
flagThreshold = flag.Uint64("threshold", 10_000_000, "per-masternode staking threshold in whole coins (manual reward calc)")
flagYield = flag.Uint64("yield", 10, "target masternode yield in APY% (manual reward calc)")
flagSigners = flag.Int("signers", 3, "number of initial masternodes (manual reward calc)")
)

// TestCalcMasternodeRewardsManual prints the reward figures puppeth would bake
// into the genesis config for the flag values above, as a manual check. It
// asserts nothing beyond the calculation being possible at all; the assertions
// live in TestCalcMasternodeRewards.
func TestCalcMasternodeRewardsManual(t *testing.T) {
rewards, ok := calcMasternodeRewards(*flagPeriod, *flagEpoch, *flagThreshold, *flagYield, *flagSigners)
if !ok {
t.Fatalf("period=%d epoch=%d yields less than one epoch per year, no rewards calculated", *flagPeriod, *flagEpoch)
}
epochsPerYear := (31536000 / *flagPeriod) / *flagEpoch
netOfFoundation := float64(100-common.RewardFoundationPercent) / 100

fmt.Printf("period=%ds epoch=%d blocks threshold=%d yield=%d%% masternodes=%d (%d epochs/year)",
*flagPeriod, *flagEpoch, *flagThreshold, *flagYield, *flagSigners, epochsPerYear)
fmt.Printf(" reward (total per epoch) = %d\n", rewards.TotalPerEpoch)
fmt.Printf(" masternodeReward = %v\n", rewards.MasternodeReward)
fmt.Printf(" protectorReward = %v\n", rewards.ProtectorReward)
fmt.Printf(" observerReward = %v\n", rewards.ObserverReward)
fmt.Printf(" effective yield per masternode = %.4f%% (net of the %d%% foundation cut)",
rewards.MasternodeReward*netOfFoundation*float64(epochsPerYear)/float64(*flagThreshold)*100, common.RewardFoundationPercent)
}

// TestCalcMasternodeRewards asserts the reward numbers for a set of fixed
// period/epoch/threshold/yield combinations. Run with -v to see the figures.
func TestCalcMasternodeRewards(t *testing.T) {
tests := []struct {
name string
period uint64
epoch uint64
threshold uint64
yield uint64
signers int
wantOK bool
wantTotal uint64
wantMN float64
wantProt float64
wantObs float64
}{
{
name: "mainnet-like defaults, 3 masternodes",
period: 2, epoch: 900, threshold: 10000000, yield: 10, signers: 3,
wantOK: true,
wantTotal: 190, wantMN: 63.4196, wantProt: 50.7357, wantObs: 25.3678,
},
{
name: "mainnet-like defaults, full 108 masternode set",
period: 2, epoch: 900, threshold: 10000000, yield: 10, signers: 108,
wantOK: true,
wantTotal: 6849, wantMN: 63.4196, wantProt: 50.7357, wantObs: 25.3678,
},
{
name: "double threshold at half yield pays the same per node",
period: 2, epoch: 900, threshold: 20000000, yield: 5, signers: 5,
wantOK: true,
wantTotal: 317, wantMN: 63.4196, wantProt: 50.7357, wantObs: 25.3678,
},
{
name: "15s period, fewer epochs per year, bigger per-epoch reward",
period: 15, epoch: 900, threshold: 10000000, yield: 10, signers: 3,
wantOK: true,
wantTotal: 1427, wantMN: 475.6469, wantProt: 380.5175, wantObs: 190.2588,
},
{
name: "zero yield pays nothing",
period: 2, epoch: 900, threshold: 10000000, yield: 0, signers: 3,
wantOK: true,
},
{
name: "zero period is rejected",
period: 0, epoch: 900, threshold: 10000000, yield: 10, signers: 3,
},
{
name: "zero epoch is rejected",
period: 2, epoch: 0, threshold: 10000000, yield: 10, signers: 3,
},
{
name: "epoch longer than a year is rejected",
period: 2, epoch: 20000000, threshold: 10000000, yield: 10, signers: 3,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rewards, ok := calcMasternodeRewards(tt.period, tt.epoch, tt.threshold, tt.yield, tt.signers)
if ok != tt.wantOK {
t.Fatalf("ok = %v, want %v", ok, tt.wantOK)
}
if !ok {
fmt.Printf("period=%ds epoch=%d -> rejected, reward config left untouched", tt.period, tt.epoch)
if rewards != (masternodeRewards{}) {
t.Fatalf("rewards = %+v, want zero value when not ok", rewards)
}
return
}
fmt.Printf("period=%ds epoch=%d threshold=%d yield=%d%% masternodes=%d -> total=%d mn=%v prot=%v obs=%v",
tt.period, tt.epoch, tt.threshold, tt.yield, tt.signers,
rewards.TotalPerEpoch, rewards.MasternodeReward, rewards.ProtectorReward, rewards.ObserverReward)

if rewards.TotalPerEpoch != tt.wantTotal {
t.Errorf("TotalPerEpoch = %d, want %d", rewards.TotalPerEpoch, tt.wantTotal)
}
if rewards.MasternodeReward != tt.wantMN {
t.Errorf("MasternodeReward = %v, want %v", rewards.MasternodeReward, tt.wantMN)
}
if rewards.ProtectorReward != tt.wantProt {
t.Errorf("ProtectorReward = %v, want %v", rewards.ProtectorReward, tt.wantProt)
}
if rewards.ObserverReward != tt.wantObs {
t.Errorf("ObserverReward = %v, want %v", rewards.ObserverReward, tt.wantObs)
}
if tt.yield == 0 {
return
}
// The requested yield must survive the foundation cut: a masternode
// earning MasternodeReward each epoch, minus the foundation share,
// should end the year on `yield`% of its stake.
epochsPerYear := float64((31536000 / tt.period) / tt.epoch)
netOfFoundation := float64(100-common.RewardFoundationPercent) / 100
gotYield := rewards.MasternodeReward * netOfFoundation * epochsPerYear / float64(tt.threshold) * 100
if math.Abs(gotYield-float64(tt.yield)) > 0.001 {
t.Errorf("effective yield = %.4f%%, want %d%%", gotYield, tt.yield)
}
})
}
}
14 changes: 10 additions & 4 deletions consensus/XDPoS/engines/engine_v2/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -697,11 +697,17 @@ func (x *XDPoS_v2) VerifySyncInfoMessage(chain consensus.ChainReader, syncInfo *
log.Warn("[VerifySyncInfoMessage] SyncInfo message verification failed due to QC", "blockNum", syncInfo.HighestQuorumCert.ProposedBlockInfo.Number, "round", syncInfo.HighestQuorumCert.ProposedBlockInfo.Round, "error", err)
return false, err
}
err = x.verifyTC(chain, syncInfo.HighestTimeoutCert)
if err != nil {
log.Warn("[VerifySyncInfoMessage] SyncInfo message verification failed due to TC", "gapNum", syncInfo.HighestTimeoutCert.GapNumber, "round", syncInfo.HighestTimeoutCert.Round, "error", err)
return false, err

if !isBlankTC(syncInfo.HighestTimeoutCert) {
err = x.verifyTC(chain, syncInfo.HighestTimeoutCert)
if err != nil {
log.Warn("[VerifySyncInfoMessage] SyncInfo message verification failed due to TC", "gapNum", syncInfo.HighestTimeoutCert.GapNumber, "round", syncInfo.HighestTimeoutCert.Round, "error", err)
return false, err
}
} else {
log.Debug("[VerifySyncInfoMessage] Detected blank TC, sender has no TC yet (fresh restart or new chain), skipping TC verification", "qcRound", syncInfo.HighestQuorumCert.ProposedBlockInfo.Round, "qcNumber", syncInfo.HighestQuorumCert.ProposedBlockInfo.Number, "qcHash", syncInfo.HighestQuorumCert.ProposedBlockInfo.Hash)
}
Comment thread
wanwiset25 marked this conversation as resolved.

return true, nil
}

Expand Down
9 changes: 9 additions & 0 deletions consensus/XDPoS/engines/engine_v2/timeout.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,15 @@ func (x *XDPoS_v2) getTCEpochInfo(chain consensus.ChainReader, timeoutRound type
}
return epochInfo, nil
}

// Round 0 with no signatures is the bootstrap TC installed by New(), not a real
// certificate: a TC only comes into existence once a round has timed out.
// Since TC lives in memory, when a node restarts, it's initialized as a blank.
// Rejecting the blank would discard the whole syncInfo which could contain useful QC.
func isBlankTC(timeoutCert *types.TimeoutCert) bool {
return timeoutCert != nil && timeoutCert.Round == types.Round(0) && len(timeoutCert.Signatures) == 0
}

func (x *XDPoS_v2) verifyTC(chain consensus.ChainReader, timeoutCert *types.TimeoutCert) error {
/*
1. Get epoch master node list by gapNumber
Expand Down
81 changes: 81 additions & 0 deletions consensus/tests/engine_v2_tests/sync_info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,87 @@ func TestSkipVerifySyncInfoIfBothQcTcNotQualified(t *testing.T) {
assert.Nil(t, err)
}

// A node that has never formed a TC holds the bootstrap TC (round 0, no signatures) and
// puts it into every syncInfo it sends. The QC in such a message is the only way for a
// node that missed a QC to catch up, so the placeholder TC must not invalidate it.
func TestVerifySyncInfoWithNewerQCAndBootstrapTC(t *testing.T) {
blockchain, _, currentBlock, _, _, _ := PrepareXDCTestBlockChainForV2Engine(t, 905, params.TestXDPoSMockChainConfig, nil)
engineV2 := blockchain.Engine().(*XDPoS.XDPoS).EngineV2

// The incoming syncInfo carries the newer QC, taken from the chain head.
var incoming types.ExtraFields_v2
if err := utils.DecodeBytesExtraFields(currentBlock.Extra(), &incoming); err != nil {
t.Fatal("Fail to decode extra data", err)
}

// Our node sits on an older QC and has never seen a TC, exactly like a node whose
// votes fell short of the threshold at network start.
var older types.ExtraFields_v2
if err := utils.DecodeBytesExtraFields(blockchain.GetBlockByNumber(903).Extra(), &older); err != nil {
t.Fatal("Fail to decode extra data", err)
}
bootstrapTC := &types.TimeoutCert{
Round: types.Round(0),
Signatures: []types.Signature{},
}
engineV2.SetPropertiesFaker(older.QuorumCert, bootstrapTC)

syncInfoMsg := &types.SyncInfo{
HighestQuorumCert: incoming.QuorumCert,
HighestTimeoutCert: bootstrapTC,
}

verified, err := engineV2.VerifySyncInfoMessage(blockchain, syncInfoMsg)
assert.Nil(t, err, "the bootstrap TC must not invalidate a syncInfo whose QC is newer and valid")
assert.True(t, verified)
}

// The exemption is deliberately narrow: it covers only the empty placeholder, and any TC
// that actually carries signatures is still put through verifyTC in full. This pins that
// boundary, so the exemption cannot be widened by sending a TC with a bogus signature set.
//
// The trade-off it also documents: a TC that would never be processed anyway - here one
// staler than the one we hold - still invalidates the whole message, discarding a QC we do
// need. Verifying only the certificate that is ahead of ours would avoid that, at the cost
// of a broader change to the syncInfo path.
func TestVerifySyncInfoStillVerifiesNonEmptyTC(t *testing.T) {
blockchain, _, currentBlock, _, _, _ := PrepareXDCTestBlockChainForV2Engine(t, 905, params.TestXDPoSMockChainConfig, nil)
engineV2 := blockchain.Engine().(*XDPoS.XDPoS).EngineV2

var incoming types.ExtraFields_v2
if err := utils.DecodeBytesExtraFields(currentBlock.Extra(), &incoming); err != nil {
t.Fatal("Fail to decode extra data", err)
}
var older types.ExtraFields_v2
if err := utils.DecodeBytesExtraFields(blockchain.GetBlockByNumber(903).Extra(), &older); err != nil {
t.Fatal("Fail to decode extra data", err)
}

// We already hold a newer TC than the one being sent to us.
ourTC := &types.TimeoutCert{
Round: types.Round(5),
Signatures: []types.Signature{},
}
engineV2.SetPropertiesFaker(older.QuorumCert, ourTC)

// Their TC carries a signature, so it is not the placeholder and gets verified: round 1
// with gap number 0, which has no snapshot here, so verification fails.
staleTC := &types.TimeoutCert{
Round: types.Round(1),
Signatures: []types.Signature{SignHashByPK(acc1Key, types.TimeoutSigHash(&types.TimeoutForSign{Round: types.Round(1), GapNumber: 0}).Bytes())},
GapNumber: 0,
}

syncInfoMsg := &types.SyncInfo{
HighestQuorumCert: incoming.QuorumCert,
HighestTimeoutCert: staleTC,
}

verified, err := engineV2.VerifySyncInfoMessage(blockchain, syncInfoMsg)
assert.NotNil(t, err, "a TC carrying signatures must still be verified, not exempted")
assert.False(t, verified)
}

func TestVerifySyncInfoIfTCRoundIsAtNextEpoch(t *testing.T) {
blockchain, _, _, _, _, _ := PrepareXDCTestBlockChainForV2Engine(t, 905, params.TestXDPoSMockChainConfig, nil)
engineV2 := blockchain.Engine().(*XDPoS.XDPoS).EngineV2
Expand Down
11 changes: 9 additions & 2 deletions core/genesis_setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ func TestSetupGenesisNormalizesLocalnetChainConfig(t *testing.T) {
TIPXDCXReceiverDisableBlock: big.NewInt(12),
EIP1559Block: big.NewInt(12),
CancunBlock: big.NewInt(34),
PragueBlock: big.NewInt(34),
DynamicGasLimitBlock: big.NewInt(34),
TIPUpgradeRewardBlock: big.NewInt(34),
TIPUpgradePenaltyBlock: big.NewInt(34),
TRC21IssuerSMC: params.LocalnetChainConfig.TRC21IssuerSMC,
XDCXListingSMC: params.LocalnetChainConfig.XDCXListingSMC,
RelayerRegistrationSMC: params.LocalnetChainConfig.RelayerRegistrationSMC,
Expand Down Expand Up @@ -73,8 +77,11 @@ func TestSetupGenesisNormalizesLocalnetChainConfig(t *testing.T) {
if cfg.Ethash == nil {
t.Fatal("expected non-whitelisted fields to be preserved")
}
if cfg.PragueBlock != nil || cfg.DynamicGasLimitBlock != nil || cfg.TIPUpgradeRewardBlock != nil {
t.Fatalf("unexpected localnet whitelist fields: Prague=%v DynamicGasLimit=%v TIPUpgradeReward=%v", cfg.PragueBlock, cfg.DynamicGasLimitBlock, cfg.TIPUpgradeRewardBlock)
if cfg.PragueBlock == nil || cfg.PragueBlock.Cmp(big.NewInt(34)) != 0 {
t.Fatalf("unexpected preserved Prague block: have %v want 34", cfg.PragueBlock)
}
if cfg.OsakaBlock != nil || cfg.TIPEpochHalvingBlock != nil {
t.Fatalf("unexpected localnet whitelist fields: Osaka=%v TIPEpochHalving=%v", cfg.OsakaBlock, cfg.TIPEpochHalvingBlock)
}
if cfg.LondonBlock == nil || cfg.LondonBlock.Cmp(big.NewInt(12)) != 0 {
t.Fatalf("unexpected localnet London block: have %v want 12", cfg.LondonBlock)
Expand Down
Loading