Quorum
Quorum thresholds and finalization
Overview
The QuorumManager determines when a conflict has been resolved. After each vote is stored, it retallies the conflict to check if any competing block has reached the required 67% of total delegated weight. When quorum is reached, the winning block is confirmed, losers are rejected, and all conflict state is cleaned up.
Quorum Threshold
A block reaches quorum when its accumulated vote weight meets or exceeds 67% of the total delegated XE weight:
blockWeight * 100 >= totalWeight * 67Both sides of the comparison use big.Int arithmetic to prevent overflow:
const (
quorumNumerator = 67
quorumDenominator = 100
)
bigThreshold := new(big.Int).Mul(bigTotal, big.NewInt(quorumNumerator))
bigBlock := new(big.Int).Mul(blockWeight, big.NewInt(quorumDenominator))
if bigBlock.Cmp(bigThreshold) >= 0 {
// quorum reached
}The totalWeight used as the denominator comes from the weight snapshot taken at conflict detection time. If no snapshot is available, the current total delegated weight is used as a fallback.
[!IMPORTANT] Snapshotted denominator Using the snapshotted total weight prevents an attacker from inflating the denominator after conflict detection (e.g., by creating new delegations) to make quorum harder to reach.
Tallying: OnVote()
OnVote is called by the VoteManager after each successfully stored vote. It triggers a retally of the referenced conflict.
Process
- Look up the conflict record from the
ConflictStore - Verify the voted block is part of this conflict's
BlockHashes - Acquire the quorum mutex (serializes tallying)
- Call
tallyConflict()to check for quorum - If quorum reached, call
confirmConflict()to finalize
tallyConflict()
Groups all votes for the conflict by BlockHash, summing weights with big.Int:
weightByBlock := make(map[string]*big.Int)
for _, v := range votes {
weightByBlock[v.BlockHash].Add(weightByBlock[v.BlockHash],
new(big.Int).SetUint64(v.Weight))
}Then checks each block against the 67% threshold. Returns a quorumResult if any block qualifies, or nil if quorum has not been reached.
Fallback Resolution
In some cases, the 67% threshold is unreachable because non-voting representatives inflate the total weight. For example, if 40% of weight belongs to offline or non-participating representatives, the remaining 60% can never reach 67%.
The fallback mechanism resolves this:
const fallbackDelay = 10 * time.Second[!INFO] Fallback conditions All three conditions must be met:
- The conflict is older than 10 seconds (measured from
DetectedAt)- All votes are on a single block (unanimous among actual voters)
- At least one vote exists
if len(weightByBlock) == 1 &&
!conflict.DetectedAt.IsZero() &&
Now().Sub(conflict.DetectedAt) >= fallbackDelay {
// resolve with unanimous agreement
}This ensures that conflicts are eventually resolved even when total participation is low, while still giving sufficient time for all active representatives to vote.
Conflict Confirmation
confirmConflict() finalizes a resolved conflict. This is a multi-step process that modifies the main chain.
Steps
Step
Action
Details
1
Guard: already confirmed?
If the winner already has StatusConfirmed, skip (idempotent)
2
Acquire account lock
Prevents AddBlock from processing children of the loser during the swap
3
Check staging
Is the winner in staging? (It was the second block seen)
4
Validate staged winner
Re-verify signature, PoW, and balance rules before promoting
5
Swap blocks
Demote loser from main chain to staging, promote winner to main chain
6
Confirm winner
Set winner status to StatusConfirmed
7
Update confirmation height
Record the height of the confirmed block
8
Reject losers
Set all loser blocks to StatusRejected
9
Clean up staged blocks
Delete all loser blocks from staging
10
Clean up votes
Delete all votes for this conflict
11
Remove conflict record
Delete the conflict from the ConflictStore
Block Swap
If the winning block is in staging (it was the second block seen, not the one on the main chain), a swap is required:
Before:
Main chain: ... ← Block 2 ← Block 3a (loser, on main)
Staging: Block 3b (winner, staged)
After:
Main chain: ... ← Block 2 ← Block 3b (winner, promoted)
Staging: Block 3a (loser, demoted)The swap is performed by swapBlockLocked() on the ledger, which replaces the block at the conflict position in the account chain. The demoted block is saved to staging before the swap to prevent data loss if a later step fails.
[!WARNING] Staged block validation Before promoting a staged block,
confirmConflictre-validates it: signature verification, PoW validation (if enabled), and semantic balance checks. A staged block that fails validation is refused promotion — the conflict remains unresolved rather than accepting an invalid block.
Block Status
Every block has a status that tracks its confirmation state:
type BlockStatus uint8
const (
StatusPending BlockStatus = 0 // default — not yet involved in a conflict
StatusConfirmed BlockStatus = 1 // won a conflict vote
StatusRejected BlockStatus = 2 // lost a conflict vote
)Status
Value
Meaning
StatusPending
0
Default state. Block has not been involved in a resolved conflict.
StatusConfirmed
1
Block won the conflict vote and is part of the canonical chain.
StatusRejected
2
Block lost the conflict vote and is invalid.
[!NOTE] Most blocks stay Pending The vast majority of blocks never participate in a conflict and remain
StatusPendingforever. This is fine —Pendingdoes not mean "unconfirmed" in the traditional blockchain sense. It means the block was never contested.
Stale Conflict Sweep
The StartStaleConflictSweep method launches a background goroutine that periodically retallies unresolved conflicts:
func (qm *QuorumManager) StartStaleConflictSweep(stop <-chan struct{})Behavior
- Runs every 15 seconds
- Iterates all conflicts via
GetAllConflicts() - Skips conflicts younger than
fallbackDelay(10 seconds) - Retallies each stale conflict via
tallyConflict() - If quorum or fallback conditions are met, calls
confirmConflict() - Stops when the
stopchannel is closed
┌─────────────────────────────────────────────────┐
│ Sweep Loop │
│ │
│ Every 15s: │
│ for each conflict: │
│ if age < 10s: skip │
│ tallyConflict() → result? │
│ yes → confirmConflict(result) │
│ no → continue │
└─────────────────────────────────────────────────┘[!INFO] Why sweep? The sweep handles the case where fallback conditions are met but no new votes arrive to trigger
OnVote(). Without the sweeper, a conflict with unanimous votes but below the 67% threshold would never resolve. The sweeper ensures the fallback path eventually fires.
QuorumManager Construction
func NewQuorumManager(
store QuorumStore,
conflictStore ConflictStore,
voteStore VoteStore,
ledger *Ledger,
) *QuorumManagerParameter
Description
store
QuorumStore for block status and confirmation heights
conflictStore
ConflictStore for conflict records and staged blocks
voteStore
VoteStore for retrieving votes during tallying
ledger
Ledger reference for weight lookups, block access, and chain operations
QuorumStore Interface
type QuorumStore interface {
SetBlockStatus(hash string, status BlockStatus) error
GetBlockStatus(hash string) (BlockStatus, error)
SetConfirmationHeight(account string, height uint64) error
GetConfirmationHeight(account string) (uint64, error)
DeleteVotesForConflict(account, previous string) error
}Method
Description
SetBlockStatus
Update a block's confirmation status
GetBlockStatus
Query a block's status (returns StatusPending if not found)
SetConfirmationHeight
Record the height of the last confirmed block for an account
GetConfirmationHeight
Query the confirmation height (returns 0 if not found)
DeleteVotesForConflict
Remove all vote records for a resolved conflict
Concurrency
The QuorumManager uses a single sync.Mutex to serialize all tallying and confirmation operations. This prevents race conditions when multiple votes for the same conflict arrive simultaneously, which could otherwise cause double-confirmation or inconsistent state.
During confirmConflict(), the per-account lock is also acquired to prevent AddBlock from processing new blocks on the affected account while the chain is being modified.
Related Pages
- Consensus Overview — the full consensus lifecycle
- Delegation — weight calculation and snapshots
- Conflict Detection — how conflicts are discovered and staged
- Voting — how votes are cast and validated
- Storage —
QuorumStoreandConflictStoreimplementations