Delegation¶
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.
Only XUSD confers weight
Only XUSD balances contribute to voting weight. XE balances are excluded entirely. A representative's weight is the sum of XUSD balances of all accounts that delegate to it — regardless of how much XE 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 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 := newRep
if effectiveRep == "" {
effectiveRep = oldRep
}
Changing vs. clearing delegation
There is currently no mechanism to clear delegation once set. An empty Representative field preserves the existing delegation. To effectively remove voting weight from a representative, delegate to a different key.
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.
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
}
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
}
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