80 KiB
Jericho Consensus
Overview
Jericho is a Byzantine fault-tolerant consensus mechanism combining CPU-equivalent proof of work with ephemeral bonding and democratic validation. It aims to eliminate monetary privilege (Cantillon effect) while maintaining security through distributed vigilance rather than capital hostage-taking.
Core Components
Block Production
Miners compete using a proof-of-work hash function based on large integer long division—an operation that cannot be meaningfully accelerated by specialized hardware. This ensures fair token issuance where acquisition cost is equivalent regardless of participant scale.
Proof of Work Function
The proof of work uses iterative squaring with long division, expanding the working number until it exceeds 128 MB. This creates memory-hard, CPU-bound computation that resists ASIC acceleration.
Algorithm
function proof_of_work(block_header, nonce) -> hash:
// Initialize from block header
seed = SHA3-256(block_header || nonce)
N = big_int_from_bytes(seed) // 256-bit initial value
D = extract_divisor(seed) // 128-bit divisor from least significant bits
// Expansion loop: square and divide until size exceeds 128 MB
while byte_length(N) < 134,217,728: // 2^27 bytes = 128 MB
N = N * N // Square (doubles bit length)
D = extract_divisor(N) // Update divisor from LSB 128 bits
(Q, R) = divmod(N, D) // Long division
N = Q ^ R // XOR quotient and remainder
// Final hash of expanded state
return SHA3-256(N.to_bytes())
Divisor Extraction
The divisor is derived from the least significant 128 bits of the current number, with safeguards against trivial division:
function extract_divisor(N) -> D:
raw = N & ((1 << 128) - 1) // Extract LSB 128 bits
D = raw | (1 << 127) // Set high bit (ensures D >= 2^127)
D = D | 1 // Set low bit (ensures D is odd)
return D
Setting the high bit guarantees the divisor is always at least 2^127, preventing degenerate cases where division barely reduces the number. Setting the low bit ensures the divisor is odd, maximizing the information mixed by the remainder.
Expansion Dynamics
Each iteration approximately doubles the bit length through squaring, then the XOR of quotient and remainder preserves most of the entropy:
Iteration | Approximate Size | Operations
----------|------------------|---------------------------
0 | 256 bits | Initial seed
1 | 512 bits | Square → divide → XOR
2 | 1 KB | Square → divide → XOR
3 | 2 KB | Square → divide → XOR
... | ... | ...
17 | 32 MB | Square → divide → XOR
18 | 64 MB | Square → divide → XOR
19 | 128 MB | Square → divide → XOR (terminal)
The algorithm completes in approximately 19-20 iterations, each requiring:
- One large integer multiplication (squaring)
- One large integer division
- One XOR operation
Memory Hardness
The working number N must be held entirely in memory during computation:
- Cannot be streamed or computed in chunks (squaring requires full number)
- Division requires random access across the full dividend
- Peak memory: ~128 MB per proof attempt
This memory requirement makes the algorithm hostile to:
- ASICs: Would need 128 MB on-chip memory per parallel unit
- GPUs: Limited by memory bandwidth for large integer operations
- FPGAs: Same memory constraints as ASICs
Standard CPUs with large L3 caches and system RAM handle this workload efficiently, equalizing hardware advantage.
CPU Equivalence
Long division of large integers is inherently sequential:
- Each digit of the quotient depends on the running remainder
- Cannot be parallelized beyond word-level operations
- Modern CPUs are highly optimized for this pattern (DIV instructions, branch prediction)
Custom hardware gains minimal advantage because:
- Memory bandwidth dominates (128 MB working set)
- Division is already near-optimal on CPUs
- Squaring uses standard multiplication circuits
Difficulty Adjustment
The proof is valid if the final hash meets the difficulty target:
valid = (SHA3-256(N.to_bytes()) < difficulty_target)
Difficulty target encoding:
The difficulty target is encoded as a variable-length integer using a floating-point-like representation that provides uniform relative precision across the entire difficulty range:
encoded_difficulty = (exponent << mantissa_bits) | mantissa
where:
mantissa_bits = 24
exponent: 8 bits (0-255)
mantissa: 24 bits (0 to 2^24 - 1)
decoded_target = (2^24 + mantissa) × 2^(exponent - 24 - 24)
= (2^24 + mantissa) × 2^(exponent - 48)
Encoded size: 32 bits (4 bytes) fixed
This encoding provides:
- Uniform relative error: Each mantissa increment changes the target by ~0.000006% (1/2^24)
- Full range coverage: From 2^-48 to 2^231 (exponent 0-255)
- No wasted precision: Unlike fixed-point, precision scales with magnitude
Encoding/decoding functions:
function encode_target(target) -> uint32:
if target == 0:
return 0
// Find exponent: position of highest bit
exp = floor(log2(target))
// Extract 24-bit mantissa (excluding implicit leading 1)
if exp >= 24:
mantissa = (target >> (exp - 24)) & 0xFFFFFF
else:
mantissa = (target << (24 - exp)) & 0xFFFFFF
// Adjust exponent for encoding offset
encoded_exp = exp + 48 - 24
// Clamp to valid range
if encoded_exp < 0:
return 0
if encoded_exp > 255:
return 0xFFFFFFFF
return (encoded_exp << 24) | (mantissa & 0xFFFFFF)
function decode_target(encoded) -> uint256:
if encoded == 0:
return 0
exp = encoded >> 24
mantissa = encoded & 0xFFFFFF
// Reconstruct: (2^24 + mantissa) × 2^(exp - 48)
significand = (1 << 24) | mantissa
shift = exp - 48
if shift >= 0:
return significand << shift
else:
return significand >> (-shift)
Adjustment with asymptotic damping:
The proportional difficulty adjustment uses asymptotic damping rather than hard clamping. Large deviations produce diminishing adjustments, preventing both liveness failures and mercenary mining exploits:
function adjust_difficulty(current_encoded, actual_time, expected_time) -> uint32:
// Compute raw ratio
raw_ratio = expected_time / actual_time
// Apply asymptotic damping: large ratios are compressed
// Uses tanh-like function that approaches ±1 asymptotically
damped_ratio = dampen(raw_ratio)
// Decode current target
current_target = decode_target(current_encoded)
// Apply adjustment
new_target = current_target × damped_ratio
// Re-encode
return encode_target(new_target)
function dampen(ratio) -> float:
// Convert ratio to log scale for symmetric handling
// ratio > 1 means blocks too fast (decrease target)
// ratio < 1 means blocks too slow (increase target)
log_ratio = ln(ratio)
// Asymptotic compression using scaled tanh
// k controls the "knee" where damping begins
// max_log controls the asymptotic limit
k = 0.5 // damping begins around 1.6× or 0.6×
max_log = 1.5 // asymptotic limit ~4.5× or ~0.22×
damped_log = max_log × tanh(log_ratio / k)
return e^damped_log
Damping curve properties:
Raw Ratio | Log Ratio | Damped Log | Damped Ratio | Effective Adjustment
----------|-----------|------------|--------------|---------------------
0.10 | -2.30 | -1.46 | 0.23 | +77% easier (was +900%)
0.25 | -1.39 | -1.28 | 0.28 | +72% easier (was +300%)
0.50 | -0.69 | -0.87 | 0.42 | +58% easier (was +100%)
0.75 | -0.29 | -0.47 | 0.63 | +37% easier (was +33%)
1.00 | 0.00 | 0.00 | 1.00 | No change
1.33 | 0.29 | 0.47 | 1.60 | -38% harder (was -25%)
2.00 | 0.69 | 0.87 | 2.39 | -58% harder (was -50%)
4.00 | 1.39 | 1.28 | 3.60 | -72% harder (was -75%)
10.00 | 2.30 | 1.46 | 4.31 | -77% harder (was -90%)
Asymmetric protection:
The damping function is symmetric in log-space, providing balanced protection against both failure modes:
| Threat | Without Damping | With Damping |
|---|---|---|
| Mercenary spike (10× hashrate for one period) | Difficulty increases 10×, then 90% of honest miners can't find blocks | Difficulty increases ~4.3×, honest miners remain viable |
| Hashrate collapse (90% miners leave) | Difficulty stays high, blocks take ~100 minutes | Difficulty decreases ~4.3×, blocks recover to ~23 minutes |
| Sustained 2× hashrate | Full 2× adjustment per period | ~2.4× adjustment, slightly faster convergence |
| Sustained 0.5× hashrate | Full 0.5× adjustment per period | ~0.42× adjustment, slightly faster recovery |
Liveness guarantee:
The asymptotic limit ensures that even with complete hashrate collapse, difficulty eventually decreases enough for any remaining miners to produce blocks:
Worst case: 99.9% hashrate disappears instantly
Without damping (4× clamp):
Period 1: 4× easier, blocks still 250× too slow
Period 2: 4× easier, blocks still 62× too slow
Period 3: 4× easier, blocks still 15× too slow
Period 4: 4× easier, blocks still 4× too slow
Period 5: 4× easier, blocks approach normal
Recovery: 5 periods × 2016 blocks × (avg ~25 min) = ~175 days
With asymptotic damping:
Period 1: 4.3× easier, blocks ~230× too slow
Period 2: 4.3× easier, blocks ~53× too slow
Period 3: 4.3× easier, blocks ~12× too slow
Period 4: 4.3× easier, blocks ~3× too slow
Period 5: ~3× easier, blocks approach normal
Recovery: Similar duration but smoother, no discontinuities
Crucially: damping applies per-period, so recovery accelerates
as the ratio shrinks back toward 1.0
Anti-mercenary properties:
Mercenary miners who briefly contribute massive hashrate to manipulate difficulty are countered:
Attack: Mercenary adds 9× network hashrate for one period
Without damping:
Period N: Blocks 10× faster, difficulty increases 10×
Period N+1: Mercenary leaves, honest miners face 10× difficulty
Period N+2: Blocks 10× slower, difficulty decreases (clamped to 4×)
Period N+3: Still 2.5× too hard
Period N+4: Still too hard...
Result: Extended period of slow blocks, chain vulnerable
With asymptotic damping:
Period N: Blocks 10× faster, difficulty increases ~4.3×
Period N+1: Mercenary leaves, honest miners face 4.3× difficulty
Period N+2: Blocks 4.3× slower, difficulty decreases ~3.6×
Period N+3: Blocks ~1.2× slower, difficulty decreases ~1.15×
Period N+4: Near equilibrium
Result: Faster recovery, reduced attack profitability
Fixed sampling window:
The adjustment uses a fixed 2016-block sampling window (not dynamic):
adjustment_trigger = (block_height % 2016 == 0)
actual_time = timestamp[block_height] - timestamp[block_height - 2016]
expected_time = 2016 × 600 seconds
Control properties:
- Fixed window: predictable, not gameable by timestamp manipulation
- No integral term: prevents windup from sustained deviations
- No derivative term: avoids oscillation from noise
- Asymptotic damping: prevents both liveness failure and mercenary exploitation
Adjustment error analysis:
| Adjustment Size | Encoding Error | Damping Distortion | Combined Error |
|---|---|---|---|
| 1% change | ±0.000006% | < 0.1% | < 0.1% |
| 10% change | ±0.000006% | < 1% | < 1% |
| 50% change | ±0.000006% | ~8% | ~8% |
| 100% change | ±0.000006% | ~20% | ~20% |
| 400% change | ±0.000006% | ~40% | ~40% |
The damping distortion is intentional—it prevents extreme adjustments while still responding proportionally to moderate deviations. The 24-bit mantissa ensures encoding error remains negligible at all scales.
Comparison with Bitcoin's encoding:
| Property | Bitcoin (nBits) | Jericho |
|---|---|---|
| Mantissa bits | 23 (with sign) | 24 |
| Exponent bits | 8 | 8 |
| Relative precision | ~0.00001% | ~0.000006% |
| Negative values | Possible (invalid) | Not representable |
| Zero handling | Special case | Clean encoding |
| Byte order | Little-endian quirks | Big-endian, clean |
The Jericho encoding eliminates Bitcoin's historical quirks while providing slightly better precision.
Verification
Verifying a proof requires re-executing the expansion loop—there is no shortcut. However, verification is deterministic and completes in the same time as mining one attempt. Since valid blocks are rare (one per 10 minutes network-wide), verification cost is negligible compared to mining cost.
function verify_proof(block_header, nonce, claimed_hash) -> bool:
computed_hash = proof_of_work(block_header, nonce)
return computed_hash == claimed_hash AND computed_hash < difficulty_target
Security Properties
| Property | Guarantee |
|---|---|
| Progress-free | Each nonce attempt is independent |
| Memory-hard | 128 MB working set required |
| CPU-equivalent | No significant ASIC advantage |
| Deterministic | Same inputs always produce same output |
| Non-reversible | Cannot work backwards from hash to nonce |
Token Denomination
Tokens are represented as 128-bit unsigned fixed-point integers:
Token = uint128
Layout:
[ 32 bits whole | 96 bits fractional ]
whole part: 2^32 maximum whole tokens (4,294,967,296)
fractional part: 2^96 subdivisions per token (~28.9 decimal digits precision)
Terminal supply: 2^32 whole tokens (4,294,967,296 tokens)
This provides:
- Exact binary arithmetic (no floating-point rounding)
- Extreme precision for micro-transactions (1/2^96 ≈ 1.26 × 10^-29 tokens)
- Clean power-of-two total supply equals maximum representable whole value
- Simple overflow check: any value ≥ 2^128 is invalid
Conversion:
raw_value = whole_tokens × 2^96 + fractional_part
whole_tokens = raw_value >> 96
fractional = raw_value & ((1 << 96) - 1)
Maximum raw value: 2^128 - 1 (represents 2^32 - 2^-96 tokens, just under total supply)
Token Emission
Block rewards follow a smooth exponential decay equivalent to Bitcoin's halving schedule. Rather than discrete halvings every 210,000 blocks, the reward decreases continuously:
block_reward = base_reward × e^(-λ × block_height)
where:
base_reward = 2^25 tokens = 33,554,432 tokens
λ = ln(2) / 210,000 ≈ 0.0000033 (decay constant)
terminal_supply = base_reward / λ = 2^32 tokens
This produces halving-equivalent checkpoints:
At block 210,000: reward = 2^24 tokens (16,777,216)
At block 420,000: reward = 2^23 tokens (8,388,608)
At block 630,000: reward = 2^22 tokens (4,194,304)
| Block Height | Reward (tokens) | Reward (2^n) | Cumulative Supply |
|---|---|---|---|
| 0 | 33,554,432 | 2^25 | 0 |
| 105,000 | 23,726,566 | ~2^24.5 | ~2^31 |
| 210,000 | 16,777,216 | 2^24 | ~2^31.5 |
| 420,000 | 8,388,608 | 2^23 | ~2^31.8 |
| 630,000 | 4,194,304 | 2^22 | ~2^31.9 |
| ... | ... | ... | ... |
| ∞ | → 0 | → 0 | 2^32 |
Cumulative supply formula:
total_supply(h) = terminal_supply × (1 - e^(-λ × h))
= 2^32 × (1 - e^(-0.0000033 × h))
In fixed-point representation:
raw_supply(h) = 2^128 × (1 - e^(-λ × h))
Implementation note: The exponential can be computed via fixed-point arithmetic using Taylor series or lookup tables with interpolation to avoid floating-point operations in consensus-critical code.
Target block time: 10 minutes, adjusted via difficulty retargeting every 2016 blocks.
Block Size
Block size follows an inverse exponential growth pattern—the mirror image of reward decay. As block rewards decrease, block capacity increases, reaching a terminal maximum that can ossify without constraining state channel operations.
block_size_limit(h) = terminal_size × (1 - e^(-λ × h))
where:
terminal_size = 2^23 bytes (8 MB)
λ = ln(2) / 210,000 (same decay constant as emission)
h = block height
| Block Height | Size Limit | Equivalent Era |
|---|---|---|
| 0 | 0 bytes | Genesis |
| 1,000 | ~26 KB | Bootstrap |
| 10,000 | ~260 KB | Early |
| 105,000 | ~2.4 MB | Mid-cycle 1 |
| 210,000 | ~4 MB | Halving 1 |
| 420,000 | ~6 MB | Halving 2 |
| 630,000 | ~7 MB | Halving 3 |
| ∞ | 8 MB | Terminal |
Genesis bootstrap: At block 0 the formula yields 0 bytes, so a minimum floor applies:
effective_limit(h) = max(block_size_limit(h), 2^16 bytes)
minimum_floor = 65,536 bytes (64 KB)
Rationale for terminal size:
The 8 MB terminal limit is designed to support permanent ossification while preserving state channel capacity:
-
Channel operations are compact:
- Channel open: ~500 bytes (funding tx + announcement)
- Channel close: ~300 bytes (settlement tx)
- On-chain HTLC resolution: ~200 bytes per HTLC
-
Terminal throughput calculation:
At 8 MB blocks, 10-minute intervals: ~16,000 channel opens per block ~96,000 channel opens per hour ~2.3M channel opens per day With triangular channels (3 parties each): ~6.9M new channel participants per day capacity -
State channel efficiency:
- Once opened, channels transact off-chain indefinitely
- On-chain footprint only at open/close/dispute
- 8 MB supports network of billions of channels with normal churn
Growth-to-ossification path:
| Phase | Blocks | Size Growth | Network State |
|---|---|---|---|
| Bootstrap | 0-10K | 64KB → 260KB | Initial validator set, few channels |
| Growth | 10K-210K | 260KB → 4MB | Channel network forming |
| Maturity | 210K-630K | 4MB → 7MB | Dense channel graph |
| Terminal | 630K+ | 7MB → 8MB | Ossification-ready |
After ~12 years (630,000 blocks), block size reaches 87.5% of terminal. The protocol can ossify at any point after sufficient channel density, with 8 MB providing permanent headroom for:
- Channel churn (open/close cycles)
- Dispute resolution spikes
- New user onboarding via channel creation
Interaction with emission:
Block size and emission are inverse curves crossing at the halving points:
emission(h) + normalized_size(h) ≈ constant
where:
emission(h) = e^(-λh) // decaying
normalized_size(h) = 1 - e^(-λh) // growing
At h = 210,000 (first halving):
emission = 0.5 (50% of initial reward)
size = 0.5 (50% of terminal size = 4 MB)
This creates natural economic balance: early blocks have high rewards but low capacity; mature blocks have low rewards but high capacity. Validators are compensated primarily through fees as block space becomes the scarce resource.
Block rewards are distributed:
- 70% to the block producer (miner)
- 20% split among endorsing validators (proportional to trust weight)
- 10% to the challenge reserve pool (funds successful fraud proof rewards)
Ephemeral Bonding
To propose or endorse a block, validators must post a temporary bond locked for a fixed challenge period. Bonds release automatically if unchallenged, creating rolling capital commitment without permanent lock-up. Validators who stop bonding automatically lose consensus influence—no explicit exit required.
Bond Parameters:
minimum_proposer_bond = 10 tokens
minimum_endorser_bond = 1 token
challenge_period = 100 blocks (~16.7 hours)
challenger_bond = 0.1 × proposer_bond
Democratic Endorsement
Blocks require endorsement from validators whose trust weight exceeds a threshold. Trust flows through a web of attestations between participants. Attestation weight decays with bonding inactivity, causing the trust graph to self-prune inactive or abandoned nodes.
Endorsement threshold: A block is considered endorsed when validators representing ≥67% of active trust weight sign the block within the endorsement window (10 blocks after proposal).
Adversarial Challenge Layer
During the challenge period, any participant may submit a fraud proof disputing block validity. Challengers post a smaller bond; the dispute resolves on-chain. Losers forfeit their bond to winners. This creates 1-of-N security: a single honest challenger defeats fraud.
Fraudulent Block Definition
A block is fraudulent if it violates any of the following validity rules:
Transaction Validity Fraud
| Fraud Type | Description | Proof Required |
|---|---|---|
| Double spend | Transaction spends already-spent output | Both conflicting transactions with merkle proofs |
| Invalid signature | Transaction signature fails verification | The invalid transaction + public key |
| Insufficient balance | Spend exceeds available balance | Account state proof showing balance < spend |
| Invalid script | Script execution fails | Transaction + execution trace |
| Replay attack | Transaction replayed from different context | Original transaction + replay context |
Block Structure Fraud
| Fraud Type | Description | Proof Required |
|---|---|---|
| Invalid PoW | Block hash does not meet difficulty target | Block header |
| Invalid merkle root | Declared root mismatches transaction set | Transaction list + computed root |
| Invalid parent | Parent hash references non-existent/invalid block | Block header + chain state |
| Timestamp violation | Timestamp outside acceptable window | Block header + median time past |
| Oversized block | Block exceeds maximum size limit | Block header + size proof |
Consensus Rule Fraud
| Fraud Type | Description | Proof Required |
|---|---|---|
| Invalid coinbase | Block reward exceeds allowed emission | Coinbase transaction + block height |
| Missing endorsements | Insufficient trust weight endorsed | Block header + endorsement set |
| Bond violation | Proposer bond insufficient or expired | Proposer identity + bond state |
| Vesting violation | Tokens spent before vesting release | Transaction + vesting schedule proof |
| Invalid state root | Post-execution state root incorrect | Pre-state + transactions + computed post-state |
Fraud Proof Structure
fraud_proof = {
block_hash: hash of disputed block
fraud_type: enumerated fraud category
evidence: type-specific proof data
witness: merkle proofs for state access
challenger: challenger public key
bond_tx: challenger bond transaction
signature: proof of challenger key ownership
}
Fraud Resolution
- Submission: Challenger submits fraud proof with bond (0.1 × proposer bond)
- Verification window: 288 blocks (~48 hours) for on-chain verification
- Adjudication: Deterministic verification of proof validity
- Settlement:
- If fraud proven: proposer loses bond, challenger receives proposer bond + own bond return + challenge reserve bonus
- If fraud not proven: challenger loses bond to proposer
fraud_payout = {
to_challenger: proposer_bond + challenger_bond + reserve_bonus
reserve_bonus: min(proposer_bond × 0.5, challenge_reserve_balance × 0.01)
}
Trust Attestation System
Trust attestations form the foundation of validator influence in the consensus. Unlike stake-weighted systems, trust is earned through explicit peer endorsement and sustained by ongoing participation.
Attestation Structure
attestation = {
attester: attester public key
attestee: attestee public key
weight: integer weight (1-100)
expiry: block height when attestation expires (0 = no expiry)
conditions: optional behavioral conditions
signature: attester signature over attestation
}
Trust Weight Calculation
Each validator's effective trust weight is computed as:
trust_weight(V) = Σ(attestation_weight(A, V) × attester_effectiveness(A))
for all attesters A who attested to V
attestation_weight(A, V) = base_weight(A, V) × time_decay(A, V) × activity_factor(A)
where:
base_weight(A, V) = weight declared in attestation (1-100)
time_decay(A, V) = 0.99^(blocks_since_attestation / 1000)
activity_factor(A) = min(1, bonding_blocks_last_10000 / 5000)
Attester Effectiveness
An attester's own effectiveness determines how much weight their attestations carry:
attester_effectiveness(A) = trust_weight(A) × activity_factor(A) × reputation_factor(A)
reputation_factor(A) = 1 - (slashing_events(A) × 0.1)
clamped to [0, 1]
This creates recursive trust: validators who are themselves trusted carry more attestation weight.
Trust Graph Properties
Transitivity limits: Trust does not flow transitively beyond direct attestations. If A trusts B and B trusts C, A does not automatically trust C. This prevents trust inflation.
Decay mechanics: Attestations decay over time unless refreshed. An attestation loses ~1% of its weight every 1000 blocks (~1 week). Attesters must periodically re-sign to maintain trust relationships.
Activity requirement: Attesters who stop bonding see their attestation effectiveness drop. After 10,000 blocks (~10 weeks) of inactivity, attestations from that identity carry zero weight.
Attestation Operations
| Operation | Bond Required | Effect |
|---|---|---|
| Create attestation | 0.1 tokens | New trust edge in graph |
| Update weight | 0.05 tokens | Modify existing attestation weight |
| Revoke attestation | 0 tokens | Remove trust edge immediately |
| Refresh attestation | 0.01 tokens | Reset decay timer |
Trust Threshold for Consensus
endorsement_threshold = 0.67 × total_active_trust_weight
total_active_trust_weight = Σ(trust_weight(V) × activity_factor(V))
for all validators V with activity_factor > 0.1
A block achieves consensus when:
Σ(trust_weight(E)) ≥ endorsement_threshold
for all endorsers E who signed the block
Sybil Resistance in Trust
The trust system resists Sybil attacks through:
- Identity cost: Creating validators costs ~12 weeks CPU work
- Probation period: New identities have 10% attestation effectiveness for 1000 blocks
- No self-attestation: Validators cannot attest to themselves
- Attestation limits: Each validator can create max 100 active attestations
- Collusion detection: Highly interconnected clusters with no external attestations receive reduced weight
cluster_penalty(C) = 1 - (internal_edges(C) / (internal_edges(C) + external_edges(C)))^2
where cluster C is detected via graph community analysis
Trust Bootstrap at Genesis
At network genesis, a predefined set of bootstrap attestations creates initial trust:
genesis_attestations = [
{attester: genesis_key_1, attestee: genesis_key_2, weight: 50},
{attester: genesis_key_2, attestee: genesis_key_1, weight: 50},
...
]
These bootstrap identities must have completed the work-bond registration. After 10,000 blocks, genesis attestations decay like any other, and the network trust graph becomes purely organic.
Identity Bootstrap and Sybil Resistance
New validator identities must satisfy a work-bond requirement before participating in consensus. This mechanism serves dual purpose: bootstrapping the network without privileged token distribution, and creating ongoing Sybil resistance through identity cost.
Work-Bond Mechanism
Every new public key entering the validator set must submit a proof-of-work demonstrating sustained computational commitment:
identity_proof = {
pubkey: new validator public key
work: PoW solution (long division hash)
timestamp: block height at submission
signature: proof of key ownership
}
The work requirement targets approximately twelve weeks of continuous single-thread CPU computation at genesis, establishing genuine cost for identity creation.
Adaptive Identity Difficulty
As network token supply matures, identity cost transitions from pure computation to capital:
identity_cost = max(work_requirement, token_bond_equivalent)
where:
work_requirement = base_difficulty × decay_factor(supply)
token_bond_equivalent = minimum_bond × time_weight_factor
decay_factor(supply) = 2^(-supply / halving_constant)
| Network Maturity | Work Requirement | Token Alternative |
|---|---|---|
| Genesis | ~12 weeks CPU | Not available |
| Early (low supply) | ~6 weeks CPU | Expensive (scarce tokens) |
| Growth | ~2 weeks CPU | Moderate token bond |
| Mature | ~1 week CPU | Standard token bond preferred |
This creates equivalence: early participants pay in computation what later participants pay in capital. Neither enjoys privilege over the other.
Sybil Economics
Creating validator identities always costs real resources:
sybil_attack_cost = N × identity_cost
where N = number of fake identities needed
At any network stage, identity cost equals approximately 12 weeks CPU work OR equivalent token bond.
To achieve 33% Byzantine threshold with 100 honest validators:
- Need ~50 Sybil identities
- Cost: 600 weeks CPU time OR 50 × token_bond
Identity cost scales with ambition. Casual participation is accessible; capture attempts are expensive.
Identity Lifecycle
┌─────────────────────────────────────────────────────────┐
│ 1. REGISTRATION │
│ Submit identity_proof with PoW or token bond │
│ Identity enters probationary state │
└────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 2. PROBATION (1000 blocks) │
│ Can bond and validate │
│ Attestation weight: 10% of normal │
│ Cannot attest to other new identities │
└────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 3. ACTIVE │
│ Full participation rights │
│ Attestation weight grows with bonding activity │
│ Can attest to new identities │
└────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 4. INACTIVE (no bonding for N blocks) │
│ Attestation weight decays │
│ Eventually: identity suspended │
│ Reactivation: reduced work-bond (not full) │
└─────────────────────────────────────────────────────────┘
Reward Vesting Schedule
Block rewards vest over one year following a halving pattern with progressively doubling periods. Each period releases double the previous amount over double the time, maintaining constant release rate while extending commitment duration.
Schedule Structure
| Period | Duration | Cumulative Time | Release | Cumulative Release |
|---|---|---|---|---|
| 1 | 5.7 days | 5.7 days | 1/128 (0.78%) | 0.78% |
| 2 | 11.4 days | 17.1 days | 1/64 (1.56%) | 2.34% |
| 3 | 22.8 days | 39.9 days | 1/32 (3.13%) | 5.47% |
| 4 | 45.6 days | 85.5 days | 1/16 (6.25%) | 11.72% |
| 5 | 91.3 days | 176.8 days | 1/8 (12.5%) | 24.22% |
| 6 | 182.5 days | 359.3 days | 1/4 (25%) | 49.22% |
| 7 | 5.7 days | 365 days | ~1/2 (50.78%) | 100% |
Game-Theoretic Effect
| Exit Timing | Reward Forfeited |
|---|---|
| Early exit (1 month) | ~95% |
| Early exit (3 months) | ~88% |
| Early exit (6 months) | ~75% |
| Full year commitment | 0% |
This creates:
- Continuous incentive: Every additional day of honest participation unlocks more value
- Compounding commitment: Later periods contain majority of reward, binding validators to long-term network health
- Attack deterrent: Misbehavior forfeits substantial unvested rewards beyond immediate bond loss
- Anti-mercenary pressure: Quick profit-taking captures minimal value; patience rewarded exponentially
Forfeiture Mechanics
Unvested rewards from slashed or departed validators return to the block reward pool, benefiting remaining honest participants. This creates collective incentive to identify and challenge bad actors early.
Countermeasures
Random Audits (Anti-Apathy)
Each block, a deterministic random selection obliges specific validators to demonstrate liveness and state awareness within a response window. Failure results in attestation weight reduction and partial bond slashing. Consecutive failures trigger temporary exclusion. This unpredictable obligation prevents passive participation.
Time-Weighted Bonding (Anti-Concentration)
Newly bonded capital enters at reduced effectiveness (10%), scaling to full weight over thousands of blocks. This prevents flash concentration attacks and rewards patient commitment over mercenary capital deployment. Combined with sublinear influence curves, small long-term participants can match large short-term holders.
Security Model
| Threat | Countermeasure |
|---|---|
| Hardware privilege | CPU-equivalent PoW (long division) |
| Validation fraud | Economic bonds + adversarial challenges |
| Voter apathy | Random audits with slashing |
| Capital concentration | Time-weighted bonding + sublinear returns |
| Mercenary behavior | Progressive vesting over one year |
| Zombie validators | Automatic decay via bonding inactivity |
| Sybil attacks | Work-bond identity cost (~12 weeks CPU per identity) |
| Bootstrap privilege | Work-to-capital transition preserves cost equivalence |
Properties
| Aspect | Mechanism |
|---|---|
| Sybil resistance | Work-bond identity registration (~12 weeks CPU) |
| Zombie elimination | Automatic via bonding inactivity decay |
| Attack cost | Bond loss + challenge certainty + vested reward forfeiture |
| Entry barrier | Moderate work OR equivalent capital (never free) |
| Governance | Democratic endorsement with activity-weighted voting |
| Commitment horizon | One year through vesting schedule |
| Bootstrap fairness | Work-capital equivalence across network maturity |
Trade-offs
Jericho sacrifices the passive security of capital hostage-taking (Bitcoin's ASIC lock-in) for active security through distributed vigilance. It requires ongoing participation but offers fair issuance, accessible entry, and resistance to monetary privilege accumulation. The vesting schedule creates soft lock-in through future rewards rather than stranded hardware, aligning incentives without creating permanent barriers to exit.
The work-bond identity mechanism ensures that validator creation always costs real resources—computation early, capital later—eliminating both Sybil attacks and privileged genesis distribution. Early and late participants face equivalent barriers in different currencies.
Cryptographic Primitives and Address Model
Signature Scheme
The protocol uses secp256k1 Schnorr signatures for all authentication:
Key generation:
private_key = random 256-bit integer mod n
public_key = private_key × G (33 bytes compressed, or 32 bytes x-only)
Signature:
sig = schnorr_sign(private_key, message)
sig_size = 64 bytes (r: 32 bytes, s: 32 bytes)
Verification:
valid = schnorr_verify(public_key, message, sig)
Schnorr advantages over ECDSA:
- Provable security under standard assumptions
- Linear signature aggregation (n signatures → 1 signature for n-of-n multisig)
- Batch verification (~2× faster for multiple signatures)
- Simpler, non-malleable signatures
Note on quantum resistance: secp256k1 Schnorr signatures are not quantum-resistant. Shor's algorithm can solve the discrete logarithm problem, enabling key recovery from exposed public keys. The single-use address model (below) limits exposure, but does not provide full quantum security. Post-quantum migration may be considered before protocol ossification.
Address Model
Addresses are SHA-256 hashes of public keys, not raw public keys:
address = SHA256(public_key)
address_size = 32 bytes
Public key hiding: The public key is not revealed until spending. This provides:
- Partial quantum protection (public key unknown until spend)
- Smaller on-chain footprint for unspent outputs
- Address commitments possible before key generation
Single-Use Address Enforcement
The protocol rejects any transaction that spends to an address that has previously appeared as an output.
consensus_rule:
for each output in transaction.outputs:
if output.address ∈ historical_output_addresses:
REJECT transaction as invalid
Rationale:
| Problem | Single-Use Solution |
|---|---|
| Public key exposure | Key revealed only once, at spend time |
| Address clustering | No repeated addresses to link |
| Quantum harvest attacks | Exposed keys cannot receive future funds |
| Change address correlation | Forced fresh addresses break heuristics |
Implementation:
Nodes maintain a bloom filter or set of all historical output addresses:
historical_addresses: Set<Address>
on_new_block(block):
for tx in block.transactions:
for output in tx.outputs:
if output.address in historical_addresses:
reject_block("address reuse detected")
historical_addresses.add(output.address)
The storage overhead is ~32 bytes per historical output, but can be compressed using probabilistic structures with negligible false positive rate.
Recommended UTXO Management
Wallets should implement automated UTXO management to minimize address linkage while maintaining efficient coin selection. The recommended strategy uses interleaved change addresses that distribute change outputs across the wallet's value range.
UTXO Set Structure
Wallets maintain UTXOs sorted by value:
wallet_utxos = [
{address: A1, value: 0.5},
{address: A2, value: 2.3},
{address: A3, value: 5.0},
{address: A4, value: 12.7},
{address: A5, value: 50.0},
]
Interleaved Change Strategy
When spending requires change, generate 2-3 change outputs that interleave with existing UTXOs by value:
function create_change_outputs(change_amount, wallet_utxos) -> [output]:
// Determine number of change outputs (2-3)
num_changes = 2 + (random() % 2)
// Find value gaps in existing UTXO set
gaps = find_value_gaps(wallet_utxos)
// Distribute change across gaps
change_outputs = []
remaining = change_amount
for i in 0..(num_changes - 1):
// Target a value that falls within a gap
target_value = select_gap_value(gaps[i % len(gaps)])
// Allocate portion of change (with randomization)
if i == num_changes - 1:
portion = remaining
else:
portion = remaining × (0.3 + random() × 0.4) // 30-70% of remaining
change_outputs.append({
address: generate_fresh_address(),
value: portion
})
remaining -= portion
return change_outputs
function find_value_gaps(utxos) -> [gap]:
gaps = []
sorted_utxos = sort_by_value(utxos)
for i in 0..(len(sorted_utxos) - 1):
gap = {
lower: sorted_utxos[i].value,
upper: sorted_utxos[i + 1].value,
midpoint: (sorted_utxos[i].value + sorted_utxos[i + 1].value) / 2
}
gaps.append(gap)
// Add gaps at extremes
if len(sorted_utxos) > 0:
gaps.append({lower: 0, upper: sorted_utxos[0].value})
gaps.append({lower: sorted_utxos[-1].value, upper: ∞})
return gaps
Example: Interleaved Change
Before transaction:
UTXOs: [0.5, 2.3, 5.0, 12.7, 50.0]
Gaps: [0-0.5, 0.5-2.3, 2.3-5.0, 5.0-12.7, 12.7-50.0, 50.0+]
Payment: 8.0 tokens from the 12.7 UTXO
Change: 4.7 tokens to distribute
Interleaved change outputs (example):
Change 1: 1.8 tokens (fills gap 0.5-2.3)
Change 2: 2.9 tokens (fills gap 2.3-5.0)
After transaction:
UTXOs: [0.5, 1.8, 2.3, 2.9, 5.0, 50.0]
Result: Change is distributed, not concentrated in one traceable output
Privacy Properties
| Heuristic Attack | Mitigation |
|---|---|
| Largest output is payment | Multiple change outputs obscure which is payment |
| Change returns to same value range | Interleaving distributes across ranges |
| Round number detection | Randomized splits avoid round numbers |
| UTXO count fingerprinting | Gradual UTXO count changes, not dramatic |
| Value clustering | Values spread across wallet's range |
Coin Selection Algorithm
When selecting inputs for a payment:
function select_inputs(payment_amount, wallet_utxos) -> [utxo]:
// Prefer single UTXO if possible (minimizes linkage)
for utxo in wallet_utxos:
if utxo.value >= payment_amount AND utxo.value < payment_amount × 1.5:
return [utxo]
// Otherwise, select minimal set with preference for:
// 1. UTXOs that will produce change fitting existing gaps
// 2. Older UTXOs (reduce UTXO set age variance)
// 3. UTXOs from different historical transactions (if metadata available)
selected = []
total = 0
candidates = sort_by_selection_score(wallet_utxos, payment_amount)
for utxo in candidates:
selected.append(utxo)
total += utxo.value
if total >= payment_amount + min_change:
break
return selected
function selection_score(utxo, payment_amount, wallet_utxos) -> float:
// Score based on how well resulting change fits gaps
potential_change = utxo.value - payment_amount
gap_fit = score_gap_fit(potential_change, wallet_utxos)
// Prefer older UTXOs
age_score = utxo.confirmation_height / current_height
// Combine scores
return gap_fit × 0.6 + age_score × 0.4
UTXO Consolidation
Periodically consolidate UTXOs during low-fee periods:
function consolidation_candidates(wallet_utxos) -> [utxo]:
// Identify dust or near-dust UTXOs
dust_threshold = median(wallet_utxos.values) × 0.01
candidates = [u for u in wallet_utxos if u.value < dust_threshold]
// Only consolidate if it improves UTXO distribution
if len(candidates) >= 3:
return candidates
else:
return []
Consolidation creates a single new UTXO at a value that fills
the largest gap in the remaining UTXO distribution.
Wallet Implementation Requirements
Compliant wallets MUST:
- Generate fresh addresses for all outputs (enforced by consensus)
- Implement interleaved change (RECOMMENDED for privacy)
- Never expose public keys until spending
- Maintain UTXO metadata for intelligent selection
Compliant wallets SHOULD:
- Use 2-3 change outputs for transactions with significant change
- Randomize change value distribution (not exact splits)
- Consolidate dust during low-fee periods
- Avoid creating outputs smaller than median fee × 100
Collusion Resistance and Protocol Ossification
The protocol is designed to resist capture by cartels seeking to modify consensus rules, inject arbitrary data, or expand scripting capabilities. These defenses promote eventual ossification—a stable end-state where the protocol becomes immutable.
No On-Chain Governance
Protocol changes cannot be enacted through on-chain voting or stake-weighted governance:
- No upgrade mechanism: The protocol has no built-in facility for rule changes
- No signaling bits: Block headers contain no version bits or feature flags
- No admin keys: No privileged keys can modify consensus parameters
Any protocol change requires a hard fork—a new software release that honest nodes must independently choose to adopt. Cartels cannot use captured trust weight or bonded capital to amend rules; they can only propose forks that the network may reject.
Minimal Transaction Format
Transactions follow a fixed schema with no extension points:
transaction = {
version: 1 (fixed, no upgrade path)
inputs: [input, ...]
outputs: [output, ...]
lock_time: absolute block height or timestamp
witnesses: [witness, ...]
}
input = {
prev_tx: transaction hash
prev_index: output index
sequence: relative timelock (if enabled)
}
output = {
amount: token amount (128-bit fixed-point)
script: locking script (restricted grammar)
}
witness = {
signatures: [signature, ...]
preimages: [hash preimage, ...] (for HTLCs)
}
No arbitrary data fields: There is no OP_RETURN equivalent, no memo field, no metadata extension. Every byte must conform to the transaction schema.
Restricted Script Grammar
Locking scripts use a minimal, non-Turing-complete grammar supporting only payment operations:
script := pubkey_lock
| multisig_lock
| timelock_wrapper(script)
| hashlock_wrapper(script)
pubkey_lock := CHECK_SIG(pubkey)
multisig_lock := CHECK_MULTISIG(threshold, [pubkey, ...])
timelock_wrapper := IF block_height >= N THEN script
hashlock_wrapper := IF SHA256(witness) == hash THEN script
Supported operations (exhaustive list):
| Operation | Function |
|---|---|
| CHECK_SIG | Verify single signature against pubkey |
| CHECK_MULTISIG | Verify n-of-m signatures |
| CHECK_TIMELOCK_ABS | Require minimum block height |
| CHECK_TIMELOCK_REL | Require minimum blocks since input confirmed |
| CHECK_HASHLOCK | Require preimage of specified hash |
Explicitly excluded:
- Loops or recursion
- Arithmetic beyond comparison
- String manipulation
- External data references (oracles)
- Dynamic script generation
- Stack manipulation beyond witness consumption
This grammar supports all necessary payment patterns:
- Simple transfers (single signature)
- Joint custody (multisig)
- Vesting/escrow (timelocks)
- Atomic swaps and HTLCs (hashlocks)
- State channel operations (multisig + timelocks + hashlocks)
Complex smart contracts are not possible and cannot be added without a hard fork that redefines the script grammar.
Content-Addressed Block Structure
Every byte in a block is accounted for:
block = {
header: block_header
transactions: [transaction, ...]
}
block_header = {
version: 1 (fixed)
prev_block: hash of previous block header
merkle_root: merkle root of transaction hashes
timestamp: unix timestamp
difficulty: current difficulty target
nonce: proof of work nonce
}
Validity requires:
merkle_root == compute_merkle_root(transactions)
No additional space exists outside header + merkle-committed transactions
Miners cannot include arbitrary data without either:
- Creating invalid transactions (rejected by all nodes)
- Wasting transaction outputs (economically costly, limited by restricted script)
Strict Standardness Rules
Nodes enforce standardness at the mempool level, not just block validity:
standardness_rules = {
// Transaction structure
max_transaction_size: 100 KB
max_inputs: 1000
max_outputs: 1000
// Script restrictions
max_script_size: 520 bytes
max_multisig_keys: 20
max_timelock_height: 500,000,000 (far future)
// Witness restrictions
max_signature_size: 73 bytes (DER + sighash)
max_preimage_size: 32 bytes
// Economic minimums
min_output_amount: 1 token (prevents dust)
min_fee_rate: 1 token per KB
}
Non-standard transactions:
- Are not relayed between nodes
- Are not included in mempools
- May be valid if mined, but miners have no way to receive them
Even a cartel-controlled miner cannot populate blocks with non-standard transactions because they cannot propagate through the network.
Economic Deterrents
Block space is scarce and fee-driven:
data_storage_cost = size × fee_rate × (1 / utility)
For a 1 KB arbitrary data payload at mature network:
- Competes with ~3 actual payment transactions
- Pays same fee as those transactions
- Provides no utility (cannot be spent or referenced)
- Must be wrapped in valid transaction structure (overhead)
The restricted script grammar means "data storage" outputs are unspendable, permanently consuming UTXO set space. Rational miners prefer fee-paying transactions that will eventually be spent.
Trust Attestation Decay
Cartel formation requires accumulating trust weight, but attestation effectiveness decays:
Cartel accumulation timeline:
Week 0: Cartel begins acquiring identities
Week 12: First identity completes work-bond
Week 12+: Cartel members must continuously bond to maintain weight
If cartel stops bonding to coordinate attack:
activity_factor drops
attestation_weight of cartel members decays
honest validators' relative weight increases
Cartels cannot accumulate dormant power. Trust weight requires ongoing capital commitment, creating continuous cost for maintaining attack capability.
Identity Cost Barrier
Capturing 33% of network trust requires:
At N honest validators with average mass M:
cartel_identities_needed = N × 0.5 (to reach 33% of total)
cartel_cost = cartel_identities_needed × identity_cost
identity_cost = ~12 weeks CPU OR equivalent token bond
Example with 1000 honest validators:
Need ~500 cartel identities
Cost: 6000 weeks CPU time (~115 years single-threaded)
OR 500 × token_bond
The identity cost creates a slow, expensive barrier. Cartels cannot rapidly acquire influence; they must either:
- Spend years computing identity proofs
- Acquire massive token holdings (visible, market-moving)
Both approaches are detectable long before reaching capture threshold.
Ossification Path
The protocol is designed to reach a stable end-state:
| Component | Ossification Trigger | Final State |
|---|---|---|
| Block size | Approaches 8 MB terminal | Fixed at 2^23 bytes |
| Emission | Approaches zero | No new issuance |
| Script grammar | Already minimal | No expansion possible |
| Transaction format | Already fixed | No versioning |
| Difficulty adjustment | Continues indefinitely | Self-regulating |
Post-ossification operation:
Once the network reaches sufficient channel density and emission becomes negligible:
- Base layer: Processes only channel opens, closes, and disputes
- Fee market: Validators compensated entirely by transaction fees
- State channels: Handle all regular payment volume off-chain
- Protocol changes: Effectively impossible (no governance, no upgrade path)
The protocol becomes a minimal, stable settlement layer—resistant to feature creep, regulatory capture, and cartel manipulation.
Soft Fork Resistance
Even "soft" forks (tightening rules) face resistance:
soft_fork_attempt:
1. Cartel tightens rules in their blocks
2. Honest nodes accept these blocks (valid under old rules)
3. But honest miners produce blocks cartel rejects
4. Network splits along cartel/honest boundary
5. Cartel must maintain >50% hash power indefinitely
6. Any lapse allows honest chain to overtake
Without a coordination mechanism (upgrade signaling, governance), even soft forks require permanent majority control—economically unsustainable.
Collusion Detection
The trust attestation system includes collusion detection:
cluster_penalty(C) = 1 - (internal_edges(C) / (internal_edges(C) + external_edges(C)))^2
Highly interconnected groups with few external attestations receive reduced weight. This makes cartel formation visible and penalized:
- Cartels naturally attest heavily to each other (coordination requirement)
- External attestations require trust from honest validators (who won't attest to suspicious clusters)
- Cluster penalty reduces cartel's effective trust weight
Cartels face a dilemma: tight coordination (detectable, penalized) or loose coordination (ineffective).
Triangular State Channels
Jericho implements off-chain payment channels using hash time-locked contracts (HTLCs) with a three-party triangular geometry that improves routing efficiency over traditional two-party channels.
Channel Structure
A triangular channel connects three parties (A, B, C) with a single on-chain funding transaction:
funding_tx = {
inputs: [A_contribution, B_contribution, C_contribution]
output: 3-of-3 multisig(A, B, C)
capacity: sum of contributions
channel_id: hash(funding_tx)
}
State representation:
channel_state = {
channel_id: funding transaction hash
sequence: monotonically increasing state number
balances: {A: amount, B: amount, C: amount}
htlcs: [pending HTLC commitments]
signatures: {A: sig, B: sig, C: sig}
}
All three parties must sign each state update. Any party can broadcast the latest mutually-signed state to close the channel.
Triangular Geometry Advantages
Traditional two-party channels form a graph where routing requires finding paths through sequential hops. Triangular channels create a hypergraph where each channel connects three nodes:
Two-party topology: Triangular topology:
A───B───C───D A───B
│ │ │ │ ╲ │ ╱
E───F───G───H ╲│╱
C
╱│╲
╱ │ ╲
D───E
Pathfinding benefits:
| Metric | Two-party | Triangular |
|---|---|---|
| Average hops for N nodes | O(√N) | O(∛N) |
| Routing table size | O(N²) | O(N^1.5) |
| Failure recovery options | 1 alternate per hop | 2 alternates per hop |
| Liquidity utilization | Sequential | Parallel rebalancing |
Within a triangle, any party can route to either other party in one hop. Payments between triangles require fewer intermediate hops because each triangle provides two potential next-hops rather than one.
HTLC Protocol
Hash time-locked contracts enable atomic multi-hop payments:
htlc = {
payment_hash: H(preimage)
amount: payment amount
sender: originating party within channel
receiver: destination party within channel
expiry: block height deadline
direction: which balance decrements/increments on resolution
}
HTLC lifecycle:
- Offer: Sender proposes HTLC, all three parties sign new state with HTLC pending
- Forward: Receiver creates corresponding HTLC in adjacent channel (with shorter expiry)
- Fulfill: Final recipient reveals preimage, HTLCs resolve backwards
- Timeout: If expiry passes without preimage, HTLC cancels and funds return to sender
Triangular HTLC routing:
Payment A→D through triangle (A,B,C) and triangle (C,D,E):
A offers HTLC to C (via triangle ABC)
C offers HTLC to D (via triangle CDE)
D reveals preimage to C
C reveals preimage to A
Total hops: 2 (vs 3+ in two-party topology)
State Update Protocol
Channel state advances through signed commitment transactions:
commitment = {
channel_id: channel identifier
sequence: new state number (must exceed previous)
new_balances: {A: amount, B: amount, C: amount}
htlc_updates: [add/remove HTLC operations]
fee_allocation: how on-chain fees are split if broadcast
}
update_message = {
commitment: proposed new state
signature: proposer's signature
revocation: revocation key for proposer's previous state
}
Three-phase update:
- Propose: One party broadcasts proposed state with their signature
- Countersign: Other two parties each respond with their signature + revocation
- Finalize: All parties now hold fully-signed new state
Any state update requires 3 messages minimum (propose + 2 countersigns). Revocation keys ensure that broadcasting an old state results in complete fund loss to the other parties.
Revocation Mechanism
To prevent broadcast of superseded states, each party provides revocation secrets:
revocation_key(party, sequence) = hash(party_secret || sequence)
penalty_clause = {
condition: "if state sequence < current AND revocation_key revealed"
action: "all channel funds go to non-broadcasting parties"
}
When signing state N+1, each party reveals their revocation key for state N. This makes broadcasting state N provably punishable—the other parties can claim all funds using the revealed revocation key.
Triangular penalty distribution:
If party A broadcasts revoked state, parties B and C split A's balance:
penalty_distribution = {
B: A_balance × (B_balance / (B_balance + C_balance))
C: A_balance × (C_balance / (B_balance + C_balance))
}
Channel Closure
Cooperative close (all parties online and agreeing):
closing_tx = {
input: funding_tx output
outputs: [A_final_balance, B_final_balance, C_final_balance]
signatures: [A_sig, B_sig, C_sig]
}
Immediate finality, minimal fees.
Unilateral close (one party broadcasts latest state):
force_close_tx = {
input: funding_tx output
outputs: [
A_balance (timelocked: 288 blocks if A broadcasted, else immediate),
B_balance (timelocked: 288 blocks if B broadcasted, else immediate),
C_balance (timelocked: 288 blocks if C broadcasted, else immediate),
htlc_outputs...
]
signatures: [A_sig, B_sig, C_sig] (from last valid state)
}
The broadcasting party's funds are timelocked to allow fraud proof submission if the state was revoked.
HTLC resolution on-chain:
Pending HTLCs become separate outputs that resolve via:
- Preimage reveal (receiver claims)
- Timeout expiry (sender reclaims)
Channel Capacity Gossip Protocol
Efficient routing requires near-real-time knowledge of channel capacities across the network. The gossip protocol propagates capacity updates with minimal latency.
Gossip Message Types
channel_announcement = {
channel_id: funding tx hash
participants: [pubkey_A, pubkey_B, pubkey_C]
capacity: total channel capacity
funding_proof: merkle proof of funding tx in block
signatures: [A_sig, B_sig, C_sig]
}
capacity_update = {
channel_id: channel identifier
timestamp: unix timestamp (monotonic per channel)
available: {
A_to_B: amount, A_to_C: amount,
B_to_A: amount, B_to_C: amount,
C_to_A: amount, C_to_B: amount
}
fee_rates: {A: rate, B: rate, C: rate}
signature: any one participant's signature
}
capacity_digest = {
channels: [channel_id, ...]
checksums: [hash(latest_update), ...]
timestamp: digest generation time
}
Propagation Strategy
Immediate propagation for significant changes:
significant_change = (
|new_capacity - old_capacity| > threshold OR
direction_became_unavailable OR
direction_became_available
)
threshold = max(0.1 × channel_capacity, 1000 tokens)
Batched propagation for minor fluctuations:
- Accumulate minor updates for 10 seconds
- Broadcast single aggregate update
- Reduces message volume by ~90% during high activity
Protocol flow:
1. State change occurs in channel ABC
2. Any participant (say A) generates capacity_update
3. A sends to all connected peers
4. Peers validate signature and timestamp monotonicity
5. Peers update local routing table
6. Peers forward to their peers (with deduplication)
Anti-Spam Measures
rate_limits = {
updates_per_channel_per_hour: 60
updates_per_node_per_minute: 100
minimum_update_interval: 1 second
}
spam_penalty = {
condition: exceeding rate limits
action: temporary ignore (exponential backoff)
}
Nodes track update frequency per channel and per source node. Excessive updates trigger temporary filtering.
Routing Table Structure
Each node maintains:
routing_table = {
channels: {
channel_id: {
participants: [A, B, C]
capacity: total
available: directional capacities
fee_rates: per-participant fees
last_update: timestamp
reliability: success rate estimate
}
},
node_index: {
pubkey: [channel_ids...] // channels involving this node
},
distance_cache: {
(source, dest): {
paths: [precomputed paths]
expires: cache expiry time
}
}
}
Synchronization
New nodes or nodes recovering from disconnection:
sync_request = {
known_channels: [channel_id, ...]
known_checksums: [hash(latest_update), ...]
}
sync_response = {
missing_announcements: [channel_announcement, ...]
stale_updates: [capacity_update, ...]
}
Nodes exchange capacity_digest messages periodically to detect desynchronization. Missing or stale entries trigger targeted sync requests.
Latency Targets
| Metric | Target |
|---|---|
| Update propagation (95th percentile) | < 500ms |
| Full network sync (1000 channels) | < 30 seconds |
| Routing table memory (10,000 channels) | < 50 MB |
| Path computation (10 hops max) | < 10ms |
Payment Failure Prevention
The gossip protocol's low latency enables proactive failure prevention:
Pre-flight capacity check:
Before initiating payment:
1. Query local routing table for path capacities
2. Add safety margin (10%) for in-flight updates
3. If insufficient capacity, wait for fresh gossip or try alternate path
Speculative path reservation:
For large payments:
1. Send capacity_probe along intended path
2. Each hop tentatively reserves capacity (60 second hold)
3. If all hops confirm, proceed with HTLC
4. If any hop fails, release reservations and reroute
Failure feedback:
payment_failure = {
failed_channel: channel_id where failure occurred
failure_reason: insufficient_capacity | node_offline | htlc_timeout
suggested_wait: seconds until retry recommended
}
Failed payments immediately update local routing table and optionally broadcast capacity corrections if the failure reveals stale gossip data.
DAG-Structured Onion Routing
Payments use onion-encrypted routing with a directed acyclic graph (DAG) structure rather than a single linear path. The onion format supports:
- Multiple parallel paths for redundancy (first-wins settlement)
- Multiple destinations for split payments (multi-party settlement)
- Embedded branch and join instructions within uniform-size onion packets
Uniform Onion Packet Size
All onion packets use a fixed 8 KB (8192 bytes) size regardless of path complexity:
ONION_PACKET_SIZE = 8192 bytes
onion_packet = {
version: 1 byte
ephemeral_pubkey: 33 bytes
encrypted_payload: 8126 bytes
hmac: 32 bytes
}
The large uniform size accommodates:
- Up to 32 hops in a linear path
- Embedded branch instructions (split to multiple next-hops)
- Embedded join instructions (reconvergence points)
- Multiple terminal destinations with separate amounts
- Random padding indistinguishable from real routing data
Size rationale:
| Routing Complexity | Approximate Payload Required |
|---|---|
| Simple 10-hop linear | ~2 KB |
| 3-path DAG, 10 hops each | ~4 KB |
| Multi-destination (5 recipients) | ~3 KB |
| Complex DAG with 3 destinations | ~6 KB |
| Maximum (32 hops, 8 destinations, full DAG) | ~8 KB |
The 8 KB size provides headroom for complex payment structures while remaining efficient for simple payments (excess is random padding).
Onion Layer Structure
Each layer contains routing instructions that may specify linear forwarding, branching, or termination:
onion_layer = {
layer_type: enum { FORWARD, BRANCH, JOIN, TERMINAL }
payload: type-specific data
inner_onion: encrypted remainder (variable size within packet)
padding: random bytes to maintain fixed packet size
}
// FORWARD: Simple single next-hop
forward_payload = {
next_hop: pubkey of next node
forward_amount: amount to pass
expiry: block height deadline
path_id: DAG path identifier
}
// BRANCH: Split to multiple next-hops (DAG divergence)
branch_payload = {
branches: [
{
next_hop: pubkey of branch destination
branch_amount: amount for this branch
expiry: block height deadline
branch_onion: encrypted onion for this branch (embedded)
branch_id: unique identifier for this branch
},
...
]
branch_mode: enum { ALL_REQUIRED, FIRST_WINS, THRESHOLD(n) }
}
// JOIN: Reconvergence point expecting multiple incoming branches
join_payload = {
expected_branches: [branch_id, ...]
forward_amount: total to forward after join
next_hop: pubkey of next node after join
timeout: blocks to wait for all branches
}
// TERMINAL: Final destination
terminal_payload = {
destination: pubkey of recipient
amount: payment amount for this destination
payment_hash: H(preimage) for this destination's HTLC
payment_data: encrypted memo/invoice data (optional)
}
Multi-Destination Payments
A single payment can terminate at multiple destinations on different DAG branches:
Multi-destination payment structure:
Sender A pays:
- 50 tokens to Destination G
- 30 tokens to Destination H
- 20 tokens to Destination I
┌───B───────────→ G (50 tokens)
│
A ──── BRANCH ──┼───C───D───────→ H (30 tokens)
│
└───E───F───────→ I (20 tokens)
Multi-destination onion construction:
multi_dest_onion = {
payment_id: unique payment identifier
total_amount: sum of all destination amounts
destinations: [
{dest: G, amount: 50, payment_hash: H(preimage_G)},
{dest: H, amount: 30, payment_hash: H(preimage_H)},
{dest: I, amount: 20, payment_hash: H(preimage_I)},
]
settlement_mode: "all_required" | "threshold(n)" | "any"
}
Each destination receives a separate HTLC with its own payment_hash. Settlement modes:
| Mode | Behavior |
|---|---|
| all_required | All destinations must receive and settle; any failure cancels all |
| threshold(n) | At least n destinations must settle; others may fail |
| any | Each destination settles independently |
Branch Embedding
Branch instructions are embedded within the onion layer, with each branch containing its own encrypted sub-onion:
function construct_branch_layer(branches, shared_secret):
branch_payloads = []
for branch in branches:
// Construct sub-onion for this branch
sub_onion = construct_onion(branch.path, branch.ephemeral)
// Embed sub-onion in branch payload
branch_payloads.append({
next_hop: branch.first_hop,
branch_amount: branch.amount,
expiry: branch.expiry,
branch_onion: sub_onion, // Full 8KB embedded
branch_id: branch.id
})
// Encrypt branch layer
layer = {
layer_type: BRANCH,
payload: {
branches: branch_payloads,
branch_mode: payment.branch_mode
}
}
return encrypt(shared_secret, layer)
Branch processing by intermediate node:
function process_branch_layer(encrypted_layer, node_private_key):
// Decrypt layer
shared_secret = ECDH(node_private_key, ephemeral_pubkey)
layer = decrypt(shared_secret, encrypted_layer)
if layer.layer_type != BRANCH:
return process_other_layer_type(layer)
// Process each branch
for branch in layer.payload.branches:
// Extract embedded sub-onion
sub_onion = branch.branch_onion
// Create HTLC for this branch
create_htlc(
next_hop: branch.next_hop,
amount: branch.branch_amount,
expiry: branch.expiry,
onion: sub_onion
)
// Track branches for potential join
register_pending_branches(layer.payload.branches)
Join Points (Reconvergence)
Join points aggregate multiple incoming branches before forwarding:
Join point processing:
B ──┐
│
A ── BRANCH ────┼──→ F (JOIN) ──→ G (destination)
│
D ──┘
Node F receives:
- HTLC from B with branch_id=0, amount=60
- HTLC from D with branch_id=1, amount=40
F's join layer specifies:
expected_branches: [0, 1]
forward_amount: 100
next_hop: G
Join processing:
function process_join(join_layer, incoming_htlcs):
expected = set(join_layer.expected_branches)
received = set(htlc.branch_id for htlc in incoming_htlcs)
if received != expected:
if timeout_reached:
// Cancel all received HTLCs
for htlc in incoming_htlcs:
cancel_htlc(htlc, reason="incomplete_join")
return FAIL
// Wait for remaining branches
return PENDING
// All branches received, forward aggregated payment
total = sum(htlc.amount for htlc in incoming_htlcs)
if total < join_layer.forward_amount:
// Insufficient funds across branches
cancel_all(incoming_htlcs, reason="insufficient_join_amount")
return FAIL
// Create forwarding HTLC
create_htlc(
next_hop: join_layer.next_hop,
amount: join_layer.forward_amount,
expiry: min(htlc.expiry for htlc in incoming_htlcs) - safety_margin,
onion: join_layer.inner_onion
)
return SUCCESS
Multi-Path Redundancy with Multi-Destination
Combining redundant paths with multiple destinations:
Payment: A pays 100 to G and 50 to H
Path redundancy for G:
┌───B───┐
│ │
A───┼───C───┼───F───→ G (100 tokens, first-wins)
│ │
│ JOIN
│
A───┴───D───E───────→ H (50 tokens)
Construction:
- Paths to G (via B→F and C→F) use same payment_hash, first-wins
- Path to H uses different payment_hash
- All embedded in single 8KB onion from A
Onion Padding Strategy
Random padding is cryptographically indistinguishable from encrypted routing data:
function pad_onion(payload, target_size):
current_size = len(payload)
padding_needed = target_size - current_size
// Generate deterministic-looking random padding
// (seeded from shared secret so it appears encrypted)
padding = generate_pseudo_random_bytes(
seed: hash(shared_secret || "padding"),
length: padding_needed
)
return payload || padding
function generate_pseudo_random_bytes(seed, length):
// Use ChaCha20 in counter mode with the seed
output = []
for i in 0..(length / 64):
block = ChaCha20(key=seed[0:32], nonce=i, input=zeros(64))
output.append(block)
return output[0:length]
Indistinguishability properties:
| Property | Guarantee |
|---|---|
| Padding vs payload | Encrypted padding indistinguishable from encrypted routing |
| Simple vs complex | 10-hop linear looks identical to 32-hop DAG |
| Single vs multi-dest | Cannot determine number of destinations |
| Branch points | Cannot identify which nodes perform branching |
Path-Specific Ephemeral Keys
Each branch in the DAG uses independent ephemeral key derivation:
// Root ephemeral for payment
root_ephemeral = random_scalar()
// Derive branch-specific ephemeral keys
branch_ephemeral[branch_id] = root_ephemeral × hash(
"branch" || payment_id || branch_id || root_ephemeral × G
)
// Each branch's path uses its ephemeral independently
for hop in branch.path:
ephemeral_pubkey[hop] = derive_hop_ephemeral(branch_ephemeral[branch_id], hop)
This ensures:
- Branches cannot be correlated by ephemeral key analysis
- Compromise of one branch reveals nothing about others
- Join points cannot link incoming branches except by branch_id
Settlement Modes
First-wins (redundant paths to single destination):
first_wins_settlement = {
paths: [path_0, path_1, path_2]
destination: G
amount: 100
payment_hash: H(preimage)
Settlement:
- First path to deliver HTLC wins
- Destination reveals preimage to winning path only
- Other paths timeout and cancel
}
All-required (multi-destination atomic):
all_required_settlement = {
destinations: [
{dest: G, amount: 50, hash: H(preimage_G)},
{dest: H, amount: 30, hash: H(preimage_H)},
]
Settlement:
- All destinations must receive their HTLCs
- Preimages revealed only if all destinations ready
- Any failure cancels entire payment
- Uses hash chain: preimage_G = hash(preimage_H || secret)
}
Threshold (partial success acceptable):
threshold_settlement = {
destinations: [G, H, I, J]
threshold: 3
amounts: [25, 25, 25, 25]
Settlement:
- At least 3 of 4 destinations must succeed
- Successful destinations settle independently
- Failed destinations timeout
- Sender accepts partial delivery
}
Cancellation Propagation
cancel_signal = {
payment_id: payment being cancelled
branch_id: specific branch to cancel (or ALL)
reason: enum { PATH_SETTLED, TIMEOUT, INSUFFICIENT_CAPACITY, JOIN_FAILED }
signature: proof of authority to cancel
}
Propagation:
1. Cancel signal sent backwards along branch
2. Each hop releases locked capacity
3. At branch points, cancel propagates to all sub-branches
4. At join points, cancel waits for all incoming branches
Cancellation is an optimization—HTLCs will timeout regardless, but prompt cancellation improves capital efficiency.
Path Selection Algorithm
Sender constructs DAG paths to maximize success probability:
select_dag_paths(sender, destination, amount, max_paths=3):
candidates = []
# Find diverse paths using modified Dijkstra
for i in 0..max_paths:
path = shortest_path(
sender, destination,
edge_weight = fee + (1/reliability) + (1/capacity),
excluded_edges = edges_in(candidates) # force diversity
)
if path exists:
candidates.append(path)
# Score paths by independence
dag = optimize_dag(candidates):
- Merge common suffixes (reconvergence points)
- Remove redundant intermediate hops
- Balance: more paths = higher success, but more locked liquidity
return dag
Independence scoring:
independence(path_a, path_b) = 1 - (shared_hops / total_hops)
dag_score = Σ(independence(path_i, path_j)) for all pairs
+ path_count × success_bonus
- path_count × liquidity_penalty
Locked Liquidity Management
Multi-path payments temporarily lock more liquidity than single-path:
total_locked = Σ(amount × path_i.hop_count) for all paths
vs single path:
single_locked = amount × best_path.hop_count
Mitigation strategies:
- Short timeouts on redundant paths: Non-primary paths use 50% shorter HTLC expiry
- Fast cancellation: Cancel signals release capacity immediately upon settlement
- Adaptive path count: More paths for larger payments, fewer for small
recommended_paths(amount) = {
amount < 1 token: 1 path (single)
amount < 100 tokens: 2 paths
amount < 10000 tokens: 3 paths
amount >= 10000 tokens: 3 paths + proportional split
}
Proportional Split Mode
For very large payments, instead of first-wins settlement, the payment splits across paths:
split_payment = {
payment_hash: H(preimage)
total_amount: 10000 tokens
paths: [
{path_id: 0, amount: 4000, encrypted_route: onion_0},
{path_id: 1, amount: 3500, encrypted_route: onion_1},
{path_id: 2, amount: 2500, encrypted_route: onion_2},
]
settlement_mode: "proportional"
preimage_shares: [share_0, share_1, share_2] // Shamir secret sharing
}
Atomic multi-path settlement:
Using verifiable secret sharing, the preimage is split such that the destination can only reconstruct it after receiving all path amounts:
preimage = reconstruct(share_0, share_1, share_2)
Each path's HTLC uses:
payment_hash = H(preimage)
partial_preimage = share_i
Settlement requires destination to:
1. Receive all shares via successful path HTLCs
2. Reconstruct full preimage
3. Reveal full preimage to settle all paths atomically
If any path fails, the destination cannot reconstruct the preimage, and all paths timeout—ensuring atomicity.
Privacy Properties
| Property | Guarantee |
|---|---|
| Sender anonymity | Intermediate hops cannot identify sender |
| Receiver anonymity | Intermediate hops cannot identify receiver (even with multi-dest) |
| Path length | Hidden by 8 KB uniform onion size |
| Path complexity | Simple linear indistinguishable from complex DAG |
| Multi-path correlation | Branches use independent ephemeral keys |
| Multi-destination hiding | Cannot determine number of destinations |
| Branch point hiding | Cannot identify which nodes perform branching |
| Amount per destination | Only final destination sees its amount |
| Payment linkage | Different payment_hash per attempt prevents linking retries |