Quorum¶
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:
Both 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.
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:
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.
Staged block validation
Before promoting a staged block, confirmConflict re-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. |
Most blocks stay Pending
The vast majority of blocks never participate in a conflict and remain StatusPending forever. This is fine — Pending does 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:
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 │
└─────────────────────────────────────────────────┘
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,
) *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