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.
#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.Secondif 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.
#Block Status
Every block has a status that tracks its confirmation state:
type BlockStatus uint8const ( 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. |
#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 │└─────────────────────────────────────────────────┘#QuorumManager Construction
func NewQuorumManager( store QuorumStore, conflictStore ConflictStore, voteStore VoteStore, ledger *Ledger,) *QuorumManager| Parameter | 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