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 XUSD balances of their delegators as voting weight, which they use to resolve conflicts through the voting system.
#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 XUSD 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 := newRepif effectiveRep == "" { effectiveRep = oldRep}#Non-XUSD Blocks
Blocks on non-XUSD assets (e.g., XE) can set or change the Representative field. The delegation mapping updates, but the weight does not change because only XUSD balances confer weight. The representative field on an XE block affects who the account delegates to, not how much weight they contribute.
#Weight Calculation
A representative's voting weight equals the sum of XUSD balances of all accounts currently delegating to it.
Weight(R) = Σ XUSD_Balance(A) for all accounts A where Delegation(A) = R#Atomic Updates
Weight is updated atomically on every block via updateDelegation(). The process:
- Read the account's current XUSD balance and current representative
- Subtract the previous XUSD balance from the old representative's weight
- Update the delegation mapping (if a new representative is specified)
- Add the new XUSD 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 | -prevXUSDBal |
| 2 | Delegation map updated | — | — |
| 3 | New rep gains new balance | effectiveRep | +newXUSDBal |
For XUSD blocks, prevBalance is the XUSD balance before the block. For non-XUSD blocks, the XUSD balance hasn't changed, so prevXUSDBal = newXUSDBal — the net weight change is zero (unless the representative changed).
#big.Int Arithmetic
All weight values are stored and computed using Go's math/big.Int to prevent overflow. The XUSD supply can be large, and summing many account balances could exceed uint64 range. Weight underflow (which would indicate data corruption) is caught and logged, with the weight clamped to zero.
if l.weights[oldRep].Sign() < 0 { log.Printf("ERROR: delegation weight underflow for representative %s", shortAddr(oldRep)) l.weights[oldRep].SetInt64(0)}#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 asset balance before this block |
newBlock | *Block | The newly confirmed block |
#GetVoteWeight(representative) *big.Int
Returns a copy of the total vote weight delegated to a representative. 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.
#snapshotWeights() map[string]uint64
Returns a point-in-time copy of all representative weights as uint64 values. 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}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}This ensures delegation state survives node restarts without requiring a full chain replay.
#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