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.

json
{  "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.

go
// 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.

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

  1. Read the account's current XUSD balance and current representative
  2. Subtract the previous XUSD balance from the old representative's weight
  3. Update the delegation mapping (if a new representative is specified)
  4. Add the new XUSD balance to the effective representative's weight
  5. Persist the delegation to the DelegationStore if the store supports it
StepOperationRepresentativeWeight Change
1Old rep loses old balanceoldRep-prevXUSDBal
2Delegation map updated——
3New rep gains new balanceeffectiveRep+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.

text
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.

ParameterTypeDescription
accountstringThe account that published the block
prevBalanceuint64The account's asset balance before this block
newBlock*BlockThe 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:

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

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

go
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(), and snapshotWeights()

This allows concurrent weight queries (e.g., during vote validation) without blocking block processing on other accounts.

  • 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 Representative field on blocks