Conflict Detection¶
Overview¶
Conflict detection is the entry point to the consensus system. A conflict (equivocation) occurs when an account publishes two or more blocks that share the same Previous hash — meaning they both claim to follow the same parent block. This is the block lattice equivalent of a double-spend.
The detection system identifies these forks, records them, stages the conflicting block, and triggers the voting process.
What Is an Equivocation?¶
In a well-behaved account chain, each block has a unique Previous hash pointing to the block before it:
An equivocation creates a fork:
Both Block 3a and Block 3b reference Block 2 as their Previous. Only one can be valid.
Open block conflicts
Two competing open blocks (first block on an account) both have Previous = "0". This is detected as a conflict the same way — two blocks with the same Previous hash for the same account.
Detection Process¶
checkAndRecordConflict()¶
The core detection function runs every time a new block is submitted to the ledger. It checks whether any existing block on the same account already uses the same Previous hash.
func checkAndRecordConflict(cs ConflictStore, chain *AccountChain, b *Block) (isConflict bool, isNew bool, err error)
| Return | Type | Meaning |
|---|---|---|
isConflict |
bool |
A conflict exists (including newly created ones) |
isNew |
bool |
This call caused the conflict set to reach size 2 for the first time |
err |
error |
Storage or lookup failure |
The function:
- Scans the account chain for any existing block with the same
Previoushash as the incoming block - If no match: returns
(false, false, nil)— no conflict - If match found, loads or creates the conflict record:
- New conflict: creates a
Conflictwith both block hashes, returns(true, true, nil) - Existing conflict: appends the new hash (if not already present), returns
(true, false, nil)
- New conflict: creates a
Caller must hold the account lock
checkAndRecordConflict() must be called with the per-account lock held to prevent race conditions between scanning the chain and creating the conflict record.
Conflict Cap¶
The number of block hashes in a single conflict is capped at 10. This prevents an attacker from generating unlimited equivocating blocks and consuming unbounded memory:
const maxConflictHashes = 10
if len(record.BlockHashes) >= maxConflictHashes {
return true, false, nil // conflict tracked, but reject further equivocations
}
After the cap is reached, additional equivocating blocks are acknowledged as conflicting but not added to the conflict record.
The Conflict Struct¶
type Conflict struct {
AccountAddress string `json:"account_address"`
PreviousHash string `json:"previous_hash"`
BlockHashes []string `json:"block_hashes"`
DetectedAt time.Time `json:"detected_at"`
WeightSnapshot map[string]uint64 `json:"weight_snapshot,omitempty"`
TotalWeight uint64 `json:"total_weight,omitempty"`
}
| Field | Type | Description |
|---|---|---|
AccountAddress |
string |
Hex-encoded public key of the equivocating account |
PreviousHash |
string |
The shared Previous hash (conflict point) |
BlockHashes |
[]string |
2-10 competing block hashes |
DetectedAt |
time.Time |
When the conflict was first detected |
WeightSnapshot |
map[string]uint64 |
Representative weights frozen at detection time |
TotalWeight |
uint64 |
Total delegated weight frozen at detection time |
Weight Snapshot¶
When the conflict callback fires, the ledger takes a point-in-time snapshot of all delegation weights. This snapshot is stored on the conflict and used for all subsequent vote weight lookups. Freezing weights prevents manipulation — an attacker cannot shift delegation between detection and resolution to influence the outcome.
Block Staging¶
When a conflicting block is detected, it is not added to the main chain. Instead, it is saved to a separate staging area via the ConflictStore:
The original block (first-seen) stays on the main chain. The second block goes to staging. If the voting process determines the staged block should win, the quorum resolution performs a swap — demoting the loser from the main chain to staging and promoting the winner.
Conflict Callback¶
When a conflict first reaches size 2 (the isNew flag), an asynchronous callback fires. This callback is how the VoteManager learns about new conflicts:
The callback fires exactly once per conflict. Subsequent equivocating blocks (adding hashes 3 through 10) do not re-trigger the callback.
Asynchronous callback
The conflict callback runs asynchronously to avoid blocking block processing. The callback receives the Conflict struct with the weight snapshot already populated.
ConflictStore Interface¶
The ConflictStore interface provides persistence for conflict records and staged blocks:
type ConflictStore interface {
// Conflict record CRUD
SaveConflict(c *Conflict) error
GetConflict(account, previousHash string) (*Conflict, error)
DeleteConflict(account, previousHash string) error
GetConflictsForAccount(account string) ([]*Conflict, error)
GetAllConflicts() ([]*Conflict, error)
// Staged block management
SaveStagedBlock(b *Block) error
GetStagedBlock(hash string) (*Block, error)
DeleteStagedBlock(hash string) error
}
Conflict Methods¶
| Method | Description |
|---|---|
SaveConflict |
Create or update a conflict record |
GetConflict |
Look up a conflict by account + previous hash |
DeleteConflict |
Remove a resolved conflict |
GetConflictsForAccount |
List all active conflicts for an account |
GetAllConflicts |
List all active conflicts (used by the stale conflict sweeper) |
Staged Block Methods¶
| Method | Description |
|---|---|
SaveStagedBlock |
Store a conflicting block in staging |
GetStagedBlock |
Retrieve a staged block by hash |
DeleteStagedBlock |
Remove a staged block after conflict resolution |
Helper Functions¶
GetConflictForBlock(cs, blockHash)¶
Scans all conflicts and returns the first one containing the given block hash. Uses a read lock for concurrent access. Returns (nil, false) if the block is not part of any known conflict.
RemoveConflict(cs, account, previous)¶
Deletes the conflict record for a given account and previous hash. Called by confirmConflict during cleanup.
NewConflict(account, previousHash, hashA, hashB)¶
Creates a new Conflict struct seeded with the two block hashes that caused the equivocation. Sets DetectedAt to the current time.
Concurrency¶
Conflict operations use a package-level sync.RWMutex (conflictRWMu):
- Read lock for
GetConflictForBlock()— allows concurrent lookups - Write lock for
RemoveConflict()— exclusive access during deletion
The per-account lock (held by the caller of checkAndRecordConflict()) prevents race conditions during detection. The conflictRWMu protects cross-account scans.
Related Pages¶
- Consensus Overview — how conflict detection fits into the consensus lifecycle
- Delegation — weight snapshots taken at detection time
- Voting — the callback that triggers vote casting
- Quorum — conflict resolution and block swap