xOne ports each penalty and reward rule from geth-XDC, and a spec page records the geth-XDC file and line for it. Golden vectors from geth-XDC data must match exactly, and eleven mutants must fail.

The rules are from the xOneRS pages under spec/consensus/. Line numbers are geth-XDC at the pin 6fc8e8c2 (conformance/PIN).

Who does xOne penalise, and when?

A masternode is penalised when it did too little in the epoch that just ended. The check runs at each V2 epoch switch and each V1 checkpoint.

V2. The code walks the parent chain from the parent of the switch block back to the previous switch. It counts the blocks each coinbase mined. A coinbase below minimum blocks is penalised, and so is a masternode of the epoch that mined none. minimum is 1 before TIPUpgradePenalty, then the config value minimumMinerBlockPerEpoch. At V2.SwitchBlock + 1 the list is empty. Source: engine_v2_hooks.go:318-565 (:346-366 the walk, :366-385 the minimum), engine.go:2054.

V1. The era picks the rule (v1-penalties.md):

  • BlockSigners (before TIPSigning): a candidate with no sign in the 0x89 state for the counted blocks is penalised (hooks.go:124-176).
  • Creators (from TIPSigning and TIPRandomize): a candidate that sealed none of blocks number - 1 down to number - (epoch - 1) is penalised (hooks.go:182-302).
  • ComebackOnly (from TIPSigning, below TIPRandomize): penalties_v1 returns nothing.

How long does a penalty last?

A penalty lasts until a fixed number of epochs pass. Then the comeback test applies.

  • V1: The Penalties field removes the address from the next four lists. A node penalised LimitPenaltyEpoch + 1 epochs ago faces the test. LimitPenaltyEpoch is 4, so the entry is at checkpoint number - 5 * epoch (hooks.go:199-203, constants.go:44).
  • V2 before TIPUpgradePenalty: LimitPenaltyEpochV2 is 0. The test applies when number > (LimitPenaltyEpochV2 + 1) * epoch + V2.SwitchBlock (engine_v2_hooks.go:44-50, :387-455).
  • V2 from TIPUpgradePenalty: limit is the config limitPenaltyEpoch, or 1 when that is 0. The test applies when number > limit * epoch + V2.SwitchBlock (:466-552).

How does a penalised node come back?

A node comes back through sign transactions (penalties-and-comeback.md). A comeback candidate was in an earlier Penalties list and is a candidate again.

  • Before the upgrade, and in V1: a candidate that sent a sign transaction for a block % 15 == 0 among the last 150 blocks (RangeReturnSigner) is released. The others stay on the list.
  • From the upgrade: a node listed in the newest header is released only if it was listed in all limit headers and has at least minimumSigningTx qualifying sign transactions in the epoch (:545). Otherwise it is penalised again, even if it is penalised already for this epoch. The list can then hold an address twice. With ParoleDedup, each (signer, signed hash) pair counts once (:500).

A sign transaction qualifies when it signs a block that is a multiple of 15 and that the scan has reached already. The scan runs in ascending block order. Run backwards, it finds fewer.

On Apothem the spec gives limitPenaltyEpoch 5, minimumMinerBlockPerEpoch 5 and minimumSigningTx 30 from round 27,360,000. The header check compares sets with duplicates (utils.go:59). A missing header or body is an error, never a pass.

How do rewards depend on signing?

The reward follows the count of sign transactions in the window.

Flat payout (rewards-v1.md). At checkpoint n with period r (900), the rewarded blocks are n - 2r + 1 ..= n - r. A sign transaction goes to 0x89, has exactly 68 bytes and selector e341eaa4 (reward.go:51). A masternode counts once for each rewarded block that is a multiple of 15. Each signer gets chainReward / totalSigner * sign, and the division comes first (reward.go:228).

Tiered payout (rewards-v2-tiered-proportional.md). Masternodes, protectors and observers are paid as three tiers. The fixed rule pays a signer the whole tier reward. The proportional rule pays chainReward * min(sign, signSlots) / signSlots, rounded down (reward.go:289). A node that signed 59 of 60 slots gets 59/60. The tier reward is floor(reward * 1e18) as a Go big.Float computes it, with 64 mantissa bits, so 63.42 pays 63420000000000001704 (engine_v2_hooks.go:133-138).

Which fork rows change these rules?

Six rows decide the rule. The fork table is generated from geth-XDC, never typed. Values are from forks.json. See also networks.

REFERENCE TABLEScroll to explore all columns →
Row Apothem Mainnet net2040
V2.SwitchBlock 56,828,700 80,370,000 10,800
TIPSigning (effective) 3,000,000 3,000,000 1,800
TIPRandomize (effective) 3,464,000 3,464,000 3,600
TIPUpgradePenalty 83,600,000 unscheduled 16,200
TIPFixedReward 83,600,000 unscheduled 16,200
TIPProportionalReward 84,733,000 unscheduled none

Mainnet V2 stays on the pre-upgrade penalty rule and the flat payout. Before block 84,733,000 on Apothem, a node that signed 59 of 60 slots got all of the tier reward. The spec records that Besu and Nethermind missed that fork and paid 23 of 73 owners the wrong amount.

How do the tests prove it is bit-exact?

Scripts write the vectors from geth-XDC data, never by hand. The engine must equal both the geth-XDC functions and the archive (gate overview).

  • reward_vectors.rs: apothem_reward_vectors, net2040_reward_vectors and synthetic_reward_vectors. The engine map must equal the oracle map and the archive balance change of every paid account. tier_boundary_83_600_000 and mutant_proportional_reward_at_84_733_000 test checkpoints on each side of the Apothem forks. tier_reward_floats_equal_the_oracle tests the float rule.
  • penalty_vectors.rs: apothem_penalties_equal_the_header_penalties_field (30 switches), net2040_penalties_equal_the_header_penalties_field (7), a_real_comeback_and_a_real_stay and apothem_v1_checkpoint_lists_equal_the_header_extra_data (10 V1 checkpoints).

The oracle is a Go program that calls the pinned geth-XDC reward functions (README). The vector READMEs (penalty, reward) state the gaps. Voters, protectors and observers come from synthetic cases only. The archive does not store the V1 penalty list, so V1 is checked through the list it shortens.

Which mutants must fail?

reward_mutants.rs flips one rule in the source, runs one named test (from reward_penalty.rs or reward_vectors.rs) and requires FAILED. It then restores the file. The test is the_tests_catch_every_mutant:

cargo test -p xone-consensus --test reward_mutants -- --ignored --nocapture
REFERENCE TABLEScroll to explore all columns →
Rule flipped Test that must fail
Rewards read the post-transaction state mutant_parent_state_root
Sign transactions counted out of order mutant_out_of_order_scan_loses_a_sign_in_the_flagging_block
Side blocks included mutant_side_block_is_not_counted
Penalty bounds fail open mutant_penalty_bounds_fail_open
Missing body returns zero mutant_missing_body_is_not_zero_signatures
Pre-upgrade limit after the upgrade mutant_limit_penalty_epoch_of_the_pre_upgrade_rule
V2.SwitchBlock + 1 not special mutant_switch_plus_one_special_case
Proportional reward off (payout) mutant_proportional_reward_at_84_733_000
Proportional reward off (rule) rule_follows_the_fork_rows
Multiply before divide flat_payout_divides_before_it_multiplies
V1 owner-is-foundation skip removed v1_owner_that_is_the_foundation_gets_no_owner_share_but_tiered_does

Frequently asked questions

Who is penalised in XDPoS V2?

A masternode that mined fewer than minimum blocks in the epoch, or none.

How does a penalised node get released?

Before TIPUpgradePenalty, it sends a sign transaction for a block that is a multiple of 15 in the last 150 blocks. From the upgrade, it must be listed in all limit headers and have at least minimumSigningTx qualifying sign transactions in the epoch.

Does mainnet use the proportional reward?

No. The mainnet rows TIPFixedReward and TIPProportionalReward are unscheduled. Apothem switches at 83,600,000 and 84,733,000.

Sources