Delegation
How representatives are delegated voting power
Overview
Delegation is the mechanism by which accounts assign their voting power to a representative. Representatives do not hold funds — they accumulate the XE balances of their delegators as voting weight, which they use to finalize blocks and resolve conflicts through the voting system.
[!WARNING] Only XE confers weight Only XE balances contribute to voting weight. XUSD balances are excluded entirely, because XUSD is mintable by an authorized minter (faucet, and later a bridge) and minting must never mint consensus weight. A representative's weight is the sum of the XE balances of all accounts that delegate to it — regardless of how much XUSD those accounts hold.
Setting a Representative
Every block includes an optional Representative field — a hex-encoded ed25519 public key (64 hex characters / 32 bytes). When an account publishes a block with this field set, it delegates its XE balance to that representative.
{
"type": "send",
"account": "a1b2c3...",
"previous": "d4e5f6...",
"balance": 50000,
"asset": "XE",
"representative": "f7e8d9...",
"destination": "...",
"amount": 10000,
"signature": "...",
"hash": "..."
}Empty Representative Field
An empty Representative field ("") means keep the current delegation. It does not clear the delegation. This allows accounts to publish blocks (sends, receives, etc.) without having to re-specify their representative on every block.
// Empty Representative means "keep current delegation". Only change
// delegation when the block explicitly sets a new representative.
effectiveRep := newRep
if effectiveRep == "" {
effectiveRep = oldRep
}[!NOTE] Changing vs. clearing delegation There is currently no mechanism to clear delegation once set. An empty
Representativefield preserves the existing delegation. To effectively remove voting weight from a representative, delegate to a different key.
Non-XE Blocks
Blocks on non-XE assets (e.g. XUSD) can set or change the Representative field. The delegation mapping updates, but the weight does not change because only XE balances confer weight. The representative field on an XUSD block affects who the account delegates to, not how much weight they contribute.
Weight Calculation
A representative's voting weight equals the sum of XE balances of all accounts currently delegating to it, in micro-XE.
Weight(R) = Σ XE_Balance(A) for all accounts A where Delegation(A) = RAtomic Updates
Weight is updated atomically on every block via updateDelegation(). The process:
- Read the account's current XE balance and current representative
- Subtract the previous XE balance from the old representative's weight
- Update the delegation mapping (if a new representative is specified)
- Add the new XE balance to the effective representative's weight
- Persist the delegation to the
DelegationStoreif the store supports it
| Step | Operation | Representative | Weight change |
|---|---|---|---|
| 1 | Old rep loses old balance | oldRep | -prevXEBal |
| 2 | Delegation map updated | — | — |
| 3 | New rep gains new balance | effectiveRep | +newXEBal |
For XE blocks, prevBalance is the XE balance before the block. For non-XE blocks the XE balance hasn't changed, so prevXEBal = newXEBal — the net weight change is zero (unless the representative changed). Blocks with an empty Asset (genesis and other legacy blocks) are treated as XE.
big.Int Arithmetic
All weight values are stored and computed using Go's math/big.Int to prevent overflow when summing many account balances. Weight underflow (which would indicate data corruption) is caught, counted, and logged, with the weight clamped to zero so quorum math stays well-defined.
n := l.delegationUnderflow.Add(1)
log.Printf("ERROR: delegation weight underflow for representative %s (balance subtracted: %d); clamped to 0 — DATA CORRUPTION, total underflows=%d",
shortAddr(oldRep), prevXEBal, n)
l.weights[oldRep].SetInt64(0)The running underflow count is surfaced on GET /node rather than only in logs, so the divergence is observable.
Key Functions
updateDelegation(account, prevBalance, newBlock)
Updates the in-memory delegation and weight maps after a block is processed. Called with the account lock held.
| Parameter | Type | Description |
|---|---|---|
account | string | The account that published the block |
prevBalance | uint64 | The account's XE balance before this block |
newBlock | *Block | The newly added block |
GetVoteWeight(representative) *big.Int
Returns a copy of the total vote weight delegated to a representative, in micro-XE. Returns zero if the representative has no delegators. Thread-safe (acquires read lock on delegation mutex).
GetRepresentative(account) string
Returns the current representative for an account, or "" if no delegation is set. Thread-safe.
GetTotalDelegatedWeight() *big.Int
Returns the sum of all representative vote weights across the entire network. Used as the denominator when checking quorum threshold. Thread-safe.
GetVoteWeights() map[string]uint64
Returns every representative's weight in micro-XE (a snapshotWeights() copy). This is what GET /delegation serves.
snapshotWeights() map[string]uint64
Returns a point-in-time copy of all representative weights as uint64 values, omitting representatives with zero weight. Used to freeze weights at conflict detection time. Values that exceed uint64 are clamped to math.MaxUint64.
Weight Snapshots
When a conflict is detected, the current delegation weights are snapshotted and stored on the Conflict struct:
type Conflict struct {
AccountAddress string
PreviousHash string
BlockHashes []string
DetectedAt time.Time
WeightSnapshot map[string]uint64 // rep → weight at detection time
TotalWeight uint64 // total delegated weight at detection time
}[!IMPORTANT] Immutable for the conflict Once snapshotted, the weights for a conflict are frozen. All vote weight lookups for that conflict use the snapshot, not the current live weights. This prevents an attacker from manipulating delegation between conflict detection and resolution — e.g., by rapidly shifting weight to a representative that voted for the attacker's preferred block.
The snapshot captures:
- Per-representative weight — used when storing individual votes
- Total delegated weight — used as the quorum denominator
Persistence
Delegation mappings are persisted via the DelegationStore interface:
type DelegationStore interface {
PutDelegation(account, representative string) error
DeleteDelegation(account string) error
}On startup, if the store implements DelegationIterator, the ledger rebuilds its in-memory delegation and weight maps by iterating all stored delegations:
type DelegationIterator interface {
IterateDelegations(fn func(account, representative string) error) error
}For each delegating account the rebuild walks its chain to recover the latest XE balance (the last block whose asset is XE, or empty) and adds it to the representative's weight. A store read error aborts the rebuild rather than silently dropping an account's weight — a partially-readable store must refuse to boot rather than diverge from a clean replay.
Genesis Bootstrap
Weight is derived from delegated XE, so a network whose genesis distributes XE without also delegating it starts with zero total weight — no block could ever reach quorum. The genesis block therefore carries an optional Representative field that seeds the treasury's delegation, so the network has non-zero vote weight from genesis onward.
[!IMPORTANT] Genesis change The genesis representative is part of the genesis block. Changing it means a new genesis, which only takes effect on a network that is wiped and re-bootstrapped.
Concurrency
The delegation system uses a sync.RWMutex (delegationMu) to protect the in-memory maps:
- Write lock held during
updateDelegation()— blocks are processed sequentially per account - Read lock held during
GetVoteWeight(),GetRepresentative(),GetTotalDelegatedWeight(), andsnapshotWeights()
This allows concurrent weight queries (e.g., during vote validation) without blocking block processing on other accounts.
Related Pages
- Conflict Detection — when and how weight snapshots are taken
- Voting — how representatives use their weight to vote
- Quorum — how weight determines conflict resolution
- Block Types — the
Representativefield on blocks - Get delegation weights — the
GET /delegationendpoint