A follower of XDPoS v2 imports blocks and does not sign. For each header it checks the quorum certificate (QC) against the masternodes of the parent's epoch. It counts distinct signers and compares the count with a threshold. Votes, timeouts, timeout certificates (TC) and SyncInfo belong to the vote path.
This post follows the specification pages of xOneRS. Each rule has the geth-XDC file:line that the page gives, at the pin 6fc8e8c2. In the fork, engine_v2/ is consensus/XDPoS/engines/engine_v2/. The fork is private. The replay gate explains the pin.
What do a QC and a TC contain?
A QC holds block info (hash, round, number), signatures and a gap number. A TC holds a round, one signature per signer and a gap number. Each is one RLP value. xOneRS refuses trailing bytes and non-canonical integers.
- QC digest:
keccak(rlp([[hash, round, number], gapNumber])). - TC digest:
keccak256(rlp([round, gap]))(core/types/consensus_v2.go:227-247). - The gap number is
epochSwitch - epochSwitch % epoch - gap, or 0 if that is not abovegap.epochSwitchis the epoch-switch block of the parent's epoch.
How does xOneRS count signers and apply the quorum threshold?
It counts distinct recovered addresses. A certificate passes when float64(count) >= float64(len(masternodes)) * certificateThreshold. There is no rounding step.
With a threshold of 0.667, 3 masternodes need 3 signers and 108 need 73. The threshold is the V2 config at the round of the certificate: the certified block's round for a QC, the TC's own round for a TC.
The first bad signature refuses the whole certificate:
- A signature that does not recover a key is refused (
SignatureUnrecoverable). - A signature with
sabove half the curve order is refused (SignatureHighS). Its twin(r, N - s)recovers the same address. - A signer outside the masternode list of the epoch refuses the certificate (
SignerNotMasternode). It is not skipped. - A byte-identical signature is skipped. Two signatures that recover the same address count once.
Rule 4 exists because one key can make many valid signatures with random nonces (Hacken F-2026-19525).
geth-XDC: signer recovery engine_v2/utils.go:131-250 (high-S :86, member test :210, duplicate skip :221); verifyQC utils.go:315 with its threshold gate at :363-373; verifyTC timeout.go:373 with its gate at :429-436.
A QC of round 0 (the certified block is the V2 switch block) skips the threshold but not the other checks. A TC has no such exemption.
What does a follower check on each header?
A follower runs 14 header checks in order and stops at the first failure. Step 8 requires round > qc.proposedBlockInfo.round (verifyHeader.go:153). Step 9 is the QC.
The QC is checked against the epoch of the parent, not of the block. At an epoch switch the new list signs nothing yet. The checks are:
- the parent's masternode list is not empty, and the QC's block hash is the parent's hash (
QcEpochInfo); - each distinct signature recovers, has low
sand belongs to a masternode; - the signer count meets the threshold, if the QC's round is above 0 (
QcBelowThreshold); - the gap number (
QcGapNumber), the block number (QcBlockNumber) and the round (QcRound, orQcSwitchRound).
Source: verifyHeader.go:170, engine_v2/utils.go:315-472, :619-668. On the gap number, xOneRS is stricter than geth-XDC. geth accepts a mismatch when the number names a snapshot with the same signer set (utils.go:440-466). xOneRS rejects it.
What does a voter do that a header check does not?
A voter signs with a masternode key. The header page says the vote, timeout, TC and SyncInfo rules are not header rules. The vote path has these parts:
- Proposal. The block's QC goes through
processQC. The key must be a masternode of the block's epoch. Then the voting rule applies (utils.go:758-790). - Voting rule. Vote only if
currentRound > highestVotedRound, the block's round equals the current round, and the lock QC allows it (vote.go:404-446). - Safety record. The node writes the intent before it signs. The record also counts timeouts, which is stricter than geth-XDC (
vote.go:406). - Timeout. When the round timer ends, a masternode signs a timeout for the round. The pool keeps one timeout per signer and round. When the signers reach the threshold, the node forms a TC. It enters round + 1 if the TC's round is at least the current round (
timeout.go:93-146).
How does SyncInfo handling protect a follower?
A SyncInfo carries the highest QC and the highest TC. It reaches the node as bare RLP with code 0xe2. Gossip drops duplicates by hash before the driver sees it (bft-gossip). The node applies nothing until every check has passed (utils.go:672-733):
- A SyncInfo not newer than the local state is dropped.
- More than 150 signatures in the QC, or in a non-blank TC, is refused before any signature work (
maxPeerSigCap,utils.go:23-28). - The QC is verified in full.
- The TC is verified. One exception exists: the blank bootstrap TC, round 0 without signatures, from a sender that has seen no timeout (
isBlankTC,:739). - Only then does the node apply the QC and the TC. A good QC with a failed TC changes nothing.
What is the 3-chain commit rule?
A QC for block B commits B's grandparent G when B, its parent and G sit in three consecutive rounds. A timeout in between breaks the chain.
The checks are, in order (utils.go:530-598):
- Nothing commits if
number(B) - 2 <= V2.SwitchBlock. - The parent's round must be
round(B) - 1, and G's roundround(B) - 2. - Nothing commits if a committed block already has round
>= round(G)or number>= number(G).
After a commit, forensics compares the QCs with those of the previous commit. Two conflicting QCs make one event for each id (forensics.go:50-53).
Which negative vectors guard these rules?
Each starts from a real block, applies one recipe and expects an exact typed error, written next to the recipe. The vector README keeps up to 6 Apothem and 4 net2040 vectors for each rule.
File in negative/ |
Recipe | Expected error |
|---|---|---|
v2_qc_below_threshold |
keep only the first QC signature | QcBelowThreshold |
v2_qc_duplicate_signer |
copy the first signature over all | QcBelowThreshold |
v2_qc_signature_high_s |
replace the first with (r, N - s, v ^ 1) |
QcSignatureHighS |
v2_qc_signature_unrecoverable |
set recovery id of the first to 4 | QcSignatureUnrecoverable |
v2_qc_block_hash |
flip a bit of the QC block hash | QcEpochInfo |
v2_qc_gap_number_epoch_switch |
change the parent's epoch-switch context | QcGapNumber |
v2_round_not_above_qc |
round equals the QC's round | RoundInvalid |
Two limits apply. A signature covers its field, so a changed QC round or number fails at the signature first. QcBlockNumber, QcRound and QcSwitchRound have no negative vector. TC, SyncInfo and commit rules use other vectors: 220 certificate cases (v2/cert.json) and 1,600 commit cases, 268 of which commit.
Frequently asked questions
What is the quorum threshold for a QC?
The count of distinct recovered signers must be at least len(masternodes) * certificateThreshold, compared as float64.
Does a duplicate signature count twice?
No. Byte-identical signatures are skipped, and two signatures that recover one address count once. A high-S twin is refused.
Does a follower verify TCs?
Header verification checks the QC in each header. TC and SyncInfo rules belong to the vote path. A TC without signatures fails the threshold.
Sources
This article describes the source and evidence at its review date. Check the linked repository for current implementation and qualification status.
Explore the engineering evidenceEdit this page on GitHub opens the Markdown source (content/blog/qc-tc-timeouts-xdpos-v2/index.md). GitHub may ask you to sign in. Report a correction opens a prefilled GitHub issue for this page.