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:

text
blockWeight * 100 >= totalWeight * 67

Both sides of the comparison use big.Int arithmetic to prevent overflow:

go
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

  1. Look up the conflict record from the ConflictStore
  2. Verify the voted block is part of this conflict's BlockHashes
  3. Acquire the quorum mutex (serializes tallying)
  4. Call tallyConflict() to check for quorum
  5. If quorum reached, call confirmConflict() to finalize

#tallyConflict()

Groups all votes for the conflict by BlockHash, summing weights with big.Int:

go
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:

go
const fallbackDelay = 10 * time.Second
text
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

StepActionDetails
1Guard: already confirmed?If the winner already has StatusConfirmed, skip (idempotent)
2Acquire account lockPrevents AddBlock from processing children of the loser during the swap
3Check stagingIs the winner in staging? (It was the second block seen)
4Validate staged winnerRe-verify signature, PoW, and balance rules before promoting
5Swap blocksDemote loser from main chain to staging, promote winner to main chain
6Confirm winnerSet winner status to StatusConfirmed
7Update confirmation heightRecord the height of the confirmed block
8Reject losersSet all loser blocks to StatusRejected
9Clean up staged blocksDelete all loser blocks from staging
10Clean up votesDelete all votes for this conflict
11Remove conflict recordDelete 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:

diagram
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:

text
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)
StatusValueMeaning
StatusPending0Default state. Block has not been involved in a resolved conflict.
StatusConfirmed1Block won the conflict vote and is part of the canonical chain.
StatusRejected2Block lost the conflict vote and is invalid.

#Stale Conflict Sweep

The StartStaleConflictSweep method launches a background goroutine that periodically retallies unresolved conflicts:

go
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 stop channel is closed
diagram
┌─────────────────────────────────────────────────┐│                 Sweep Loop                       ││                                                  ││  Every 15s:                                      ││    for each conflict:                            ││      if age < 10s: skip                          ││      tallyConflict() → result?                   ││        yes → confirmConflict(result)             ││        no  → continue                            │└─────────────────────────────────────────────────┘

#QuorumManager Construction

go
func NewQuorumManager(    store QuorumStore,    conflictStore ConflictStore,    voteStore VoteStore,    ledger *Ledger,) *QuorumManager
ParameterDescription
storeQuorumStore for block status and confirmation heights
conflictStoreConflictStore for conflict records and staged blocks
voteStoreVoteStore for retrieving votes during tallying
ledgerLedger reference for weight lookups, block access, and chain operations

#QuorumStore Interface

go
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}
MethodDescription
SetBlockStatusUpdate a block's confirmation status
GetBlockStatusQuery a block's status (returns StatusPending if not found)
SetConfirmationHeightRecord the height of the last confirmed block for an account
GetConfirmationHeightQuery the confirmation height (returns 0 if not found)
DeleteVotesForConflictRemove 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.