Overview
The Fenines consensus layer implements a Hybrid Proof-of-Authority (PoA) mechanism named Fenine , which combines deterministic block production with smart contract-governed validator selection. This architecture decouples consensus validation from validator governance, enabling dynamic validator sets without protocol-level modifications.
Consensus Type : Proof-of-Authority (PoA)
Signature Algorithm : ECDSA (secp256k1)
Block Time : 3 seconds (T block = 3 s T_{\text{block}} = 3s T block = 3 s )
Epoch Length : 200 blocks (E period = 200 E_{\text{period}} = 200 E period = 200 )
Consensus Engine Architecture
Core Components
The Fenine consensus engine consists of four primary modules:
Mathematical Foundation
1. In-Turn Block Production
Validators produce blocks in a deterministic round-robin schedule defined by:
Signer ( B n ) = V [ n m o d ∣ V ∣ ] \text{Signer}(B_n) = \mathcal{V}\left[n \bmod |\mathcal{V}|\right] Signer ( B n ) = V [ n mod ∣ V ∣ ]
where:
B n B_n B n = Block at height n n n
V \mathcal{V} V = Ordered validator set from FenineSystem contract
∣ V ∣ |\mathcal{V}| ∣ V ∣ = Cardinality of active validators (1 ≤ ∣ V ∣ ≤ 101 1 \leq |\mathcal{V}| \leq 101 1 ≤ ∣ V ∣ ≤ 101 )
2. Difficulty Assignment
Block difficulty encodes the in-turn status:
Difficulty ( B n ) = { 2 if Signer ( B n ) = Expected ( B n ) 1 otherwise (out-of-turn) \text{Difficulty}(B_n) = \begin{cases}
2 & \text{if } \text{Signer}(B_n) = \text{Expected}(B_n) \\
1 & \text{otherwise (out-of-turn)}
\end{cases} Difficulty ( B n ) = { 2 1 if Signer ( B n ) = Expected ( B n ) otherwise (out-of-turn)
Difficulty serves as a tie-breaker during chain reorganizations, not a computational puzzle. Fenine uses signature verification, not hash-based mining.
3. Signature Verification
Each block header contains an ECDSA signature in the extraData field:
ExtraData = Vanity 32 ∥ Signature 65 \text{ExtraData} = \text{Vanity}_{32} \| \text{Signature}_{65} ExtraData = Vanity 32 ∥ Signature 65
Signature verification ensures:
Verify ( σ , H block , PubKey signer ) = { true if σ = Sign ( H block , PrivKey signer ) false otherwise \text{Verify}(\sigma, H_{\text{block}}, \text{PubKey}_{\text{signer}}) = \begin{cases}
\text{true} & \text{if } \sigma = \text{Sign}(H_{\text{block}}, \text{PrivKey}_{\text{signer}}) \\
\text{false} & \text{otherwise}
\end{cases} Verify ( σ , H block , PubKey signer ) = { true false if σ = Sign ( H block , PrivKey signer ) otherwise
where:
σ \sigma σ = 65-byte ECDSA signature (R, S, V)
H block H_{\text{block}} H block = Keccak256 hash of block header (excluding signature)
Block Production Protocol
Timing Model
Block production follows strict timing constraints to prevent network partitioning:
T seal = T parent + Delay ( position ) T_{\text{seal}} = T_{\text{parent}} + \text{Delay}(\text{position}) T seal = T parent + Delay ( position )
where:
Delay ( position ) = { T block if in-turn T block + T block 2 if out-of-turn \text{Delay}(\text{position}) = \begin{cases}
T_{\text{block}} & \text{if in-turn} \\
T_{\text{block}} + \frac{T_{\text{block}}}{2} & \text{if out-of-turn}
\end{cases} Delay ( position ) = { T block T block + 2 T block if in-turn if out-of-turn
Seal Algorithm
Header Preparation
header . Difficulty = DIFF_INTURN // or DIFF_NOTURN
header . Coinbase = validatorAddress
header . Extra = vanity [ 32 ] + placeholder [ 65 ]
Hash Computation
Compute block hash without signature: H unsigned = Keccak256 ( RLP ( header [ : 97 ] ) ) H_{\text{unsigned}} = \text{Keccak256}(\text{RLP}(\text{header}[:97])) H unsigned = Keccak256 ( RLP ( header [ : 97 ]))
Signature Generation
Sign the hash using validator’s private key: signature = Sign ( hash , privateKey )
// signature = [R(32) || S(32) || V(1)]
Signature Embedding
Replace placeholder with actual signature: header . Extra [ 32 : 97 ] = signature
Anti-Split-Brain Protection
To prevent simultaneous node startup from creating chain splits, Fenine implements a random delay:
Δ T startup ∼ U ( 0 , T block ) \Delta T_{\text{startup}} \sim \mathcal{U}(0, T_{\text{block}}) Δ T startup ∼ U ( 0 , T block )
This ensures statistical convergence to a single canonical chain within:
T convergence = O ( log n ) T_{\text{convergence}} = \mathcal{O}(\log n) T convergence = O ( log n )
blocks, where n = ∣ V ∣ n = |\mathcal{V}| n = ∣ V ∣ .
Snapshot System
Snapshot Data Structure
Snapshots cache validator sets to avoid repeated contract reads:
type Snapshot struct {
Number uint64 // Block height
Hash common . Hash // Block hash
Signers map [ Address ] struct {} // Authorized validators
Recents map [ uint64 ] Address // Recent signers (spam protection)
}
Snapshot Evolution
Snapshots evolve through the chain via the apply() function:
S n + 1 = apply ( S n , B n + 1 ) \mathcal{S}_{n+1} = \text{apply}(\mathcal{S}_n, B_{n+1}) S n + 1 = apply ( S n , B n + 1 )
where:
apply ( S , B ) = { loadFromContract ( B ) if B m o d E period = 0 S ∪ Signer ( B ) otherwise \text{apply}(\mathcal{S}, B) = \begin{cases}
\text{loadFromContract}(B) & \text{if } B \bmod E_{\text{period}} = 0 \\
\mathcal{S} \cup \\{\text{Signer}(B)\\} & \text{otherwise}
\end{cases} apply ( S , B ) = ⎩ ⎨ ⎧ loadFromContract ( B ) S ∪ Signer ( B ) if B mod E period = 0 otherwise
Snapshot Persistence
Snapshots are persisted to disk at checkpoint intervals to enable fast sync:
Checkpoint ( n ) ⟺ n m o d 1024 = 0 \text{Checkpoint}(n) \iff n \bmod 1024 = 0 Checkpoint ( n ) ⟺ n mod 1024 = 0
Database key schema:
Key: "fenine-" + BlockHash
Value: JSON(Snapshot)
LRU Caching Strategy
In-memory caching follows a dual-LRU architecture:
Cache Capacity Purpose recents128 snapshots Recent validator sets signatures4096 entries Block signer addresses
Cache hit rate analysis:
P hit = 1 − ( ∣ V ∣ C capacity ) k P_{\text{hit}} = 1 - \left(\frac{|\mathcal{V}|}{C_{\text{capacity}}}\right)^k P hit = 1 − ( C capacity ∣ V ∣ ) k
For ∣ V ∣ = 101 |\mathcal{V}| = 101 ∣ V ∣ = 101 and C = 128 C = 128 C = 128 , hit rate exceeds 99% for k > 2 k > 2 k > 2 lookbacks.
System Transaction Mechanism
Epoch Boundary Protocol
At every epoch block (n m o d 200 = 0 n \bmod 200 = 0 n mod 200 = 0 ), three system transactions execute atomically:
updateValidatorCandidates()
Purpose : Refresh active validator set from contract stateFunction Selector : 0x3c1cc290Effect : Synchronizes V consensus \mathcal{V}_{\text{consensus}} V consensus with V contract \mathcal{V}_{\text{contract}} V contract V n + 1 ← FenineSystem.activeValidatorSet \mathcal{V}_{n+1} \leftarrow \text{FenineSystem.activeValidatorSet} V n + 1 ← FenineSystem.activeValidatorSet
distributeBlockReward(uint256)
Purpose : Distribute epoch rewards to validatorsFunction Selector : 0x9a2e5597Reward Calculation :Ω epoch = R block × E period = 1 × 200 = 200 FEN \Omega_{\text{epoch}} = R_{\text{block}} \times E_{\text{period}} = 1 \times 200 = 200 \text{ FEN} Ω epoch = R block × E period = 1 × 200 = 200 FEN Distribution :distribute ( Ω ) = { splitAmongValidators ( Ω , V ) if ∣ V ∣ > 0 sendToTreasury ( Ω ) if ∣ V ∣ = 0 \text{distribute}(\Omega) = \begin{cases}
\text{splitAmongValidators}(\Omega, \mathcal{V}) & \text{if } |\mathcal{V}| > 0 \\
\text{sendToTreasury}(\Omega) & \text{if } |\mathcal{V}| = 0
\end{cases} distribute ( Ω ) = { splitAmongValidators ( Ω , V ) sendToTreasury ( Ω ) if ∣ V ∣ > 0 if ∣ V ∣ = 0
syncRewardState()
Purpose : Activate pending reward changesFunction Selector : 0x4d73a62aState Transition :R active ← R pending at epoch E activation R_{\text{active}} \leftarrow R_{\text{pending}} \text{ at epoch } E_{\text{activation}} R active ← R pending at epoch E activation
System Transaction Properties
System transactions have special characteristics:
Property Value Rationale Gas Fee Cap baseFeeMinimum EIP-1559 requirement Gas Tip Cap 0No priority fee Sender block.coinbaseValidator EOA Gas Check Exempted Balance may be zero Nonce Auto-increment Sequential ordering
Validator EOAs should maintain zero balance to prevent accidental user transactions. System TXs are exempt from balance checks.
Comprehensive Verification
The verifyHeader() function enforces 12 consensus rules :
if header . UncleHash != uncleHash {
return ErrInvalidUncleHash
}
H uncle = ? Keccak256 ( RLP ( [ ] ) ) H_{\text{uncle}} \stackrel{?}{=} \text{Keccak256}(\text{RLP}([])) H uncle = ? Keccak256 ( RLP ([ ]))
2. Coinbase (Signer Recovery)
signer := ecrecover ( header , signatures )
if signer != header . Coinbase {
return ErrInvalidCoinbase
}
Coinbase = ? EcRecover ( σ , H block ) \text{Coinbase} \stackrel{?}{=} \text{EcRecover}(\sigma, H_{\text{block}}) Coinbase = ? EcRecover ( σ , H block )
3. Timestamp Monotonicity
if header . Time <= parent . Time {
return ErrInvalidTimestamp
}
T n > T n − 1 T_n > T_{n-1} T n > T n − 1
if header . Time < parent . Time + period {
return ErrInvalidTimestamp
}
T n ≥ T n − 1 + T block T_n \geq T_{n-1} + T_{\text{block}} T n ≥ T n − 1 + T block
if header . Nonce != nonceAuthVote {
return ErrInvalidNonce
}
Nonce must be 0x0000000000000000 (no voting in Fenine).
if header . MixDigest != ( common . Hash {}) {
return ErrInvalidMixDigest
}
Must be zero (no PoW mining).
expected := CalcDifficulty ( chain , header . Time , parent )
if header . Difficulty . Cmp ( expected ) != 0 {
return ErrWrongDifficulty
}
D n = { 2 if in-turn 1 if out-of-turn D_n = \begin{cases}
2 & \text{if in-turn} \\
1 & \text{if out-of-turn}
\end{cases} D n = { 2 1 if in-turn if out-of-turn
if err := ecrecover ( header , signatures ); err != nil {
return ErrInvalidSignature
}
ECDSA signature must be valid.
snap := getSnapshot ( parent )
if _ , ok := snap . Signers [ signer ]; ! ok {
return ErrUnauthorizedSigner
}
Signer ( B n ) ∈ V n \text{Signer}(B_n) \in \mathcal{V}_n Signer ( B n ) ∈ V n
for seen , recent := range snap . Recents {
if recent == signer {
limit := ( len ( snap . Signers ) / 2 + 1 )
if number - seen < uint64 ( limit ) {
return ErrRecentlySigned
}
}
}
Prevents spam by enforcing minimum gap: B current − B last_signed ≥ ⌊ ∣ V ∣ 2 ⌋ + 1 B_{\text{current}} - B_{\text{last\_signed}} \geq \left\lfloor \frac{|\mathcal{V}|}{2} \right\rfloor + 1 B current − B last_signed ≥ ⌊ 2 ∣ V ∣ ⌋ + 1
diff := int64 ( parent . GasLimit ) - int64 ( header . GasLimit )
if diff < 0 {
diff *= - 1
}
if uint64 ( diff ) >= parent . GasLimit / 1024 {
return ErrInvalidGasLimit
}
Gas limit may only change by ±1/1024 per block: ∣ G n − G n − 1 ∣ < G n − 1 1024 \left| G_{n} - G_{n-1} \right| < \frac{G_{n-1}}{1024} ∣ G n − G n − 1 ∣ < 1024 G n − 1
Block Difficulty Calculation
CalcDifficulty Algorithm
func ( r * Fenine ) CalcDifficulty ( chain ChainHeaderReader ,
time uint64 ,
parent * types . Header ) * big . Int {
snap := r . snapshot ( chain , parent . Number . Uint64 (), parent . Hash (), nil )
return calcDifficulty ( snap , r . signer )
}
In-Turn Status Determination
InTurn ( n , s ) ⟺ V [ n m o d ∣ V ∣ ] = s \text{InTurn}(n, s) \iff \mathcal{V}\left[n \bmod |\mathcal{V}|\right] = s InTurn ( n , s ) ⟺ V [ n mod ∣ V ∣ ] = s
where:
n n n = Block number
s s s = Signer address
V \mathcal{V} V = Sorted validator set (lexicographic order)
Lexicographic Ordering
Validators are sorted by address to ensure determinism:
func ( s * Snapshot ) inturn ( number uint64 , signer common . Address ) bool {
signers := s . signers ()
offset := number % uint64 ( len ( signers ))
return signers [ offset ] == signer
}
func ( s * Snapshot ) signers () [] common . Address {
signers := make ([] common . Address , 0 , len ( s . Signers ))
for signer := range s . Signers {
signers = append ( signers , signer )
}
sort . Slice ( signers , func ( i , j int ) bool {
return bytes . Compare ( signers [ i ][:], signers [ j ][:]) < 0
})
return signers
}
Finality Semantics
Probabilistic Finality
Fenine provides probabilistic finality based on confirmation depth:
P finality ( k ) = 1 − ( 1 2 ) k P_{\text{finality}}(k) = 1 - \left(\frac{1}{2}\right)^k P finality ( k ) = 1 − ( 2 1 ) k
where k k k is the number of confirmations.
Confirmations Finality Probability Time (3s blocks) 1 50% 3s 2 75% 6s 3 87.5% 9s 6 98.4% 18s 12 99.98% 36s
Recommended Confirmations :
Small transactions: 3 blocks (9s)
Medium value: 6 blocks (18s)
High value: 12 blocks (36s)
Economic Finality
For transactions exceeding V tx V_{\text{tx}} V tx , required confirmations:
k safe = ⌈ log 2 ( V tx V stake ) ⌉ k_{\text{safe}} = \left\lceil \log_2\left(\frac{V_{\text{tx}}}{V_{\text{stake}}}\right) \right\rceil k safe = ⌈ log 2 ( V stake V tx ) ⌉
where V stake V_{\text{stake}} V stake is the minimum validator stake (10,000 FEN).
Chain Reorganization Handling
Fork Choice Rule
During competing chains, selection follows:
Chain canonical = arg max C l e f t ∑ B ∈ C D ( B ) , ∣ C ∣ r i g h t \text{Chain}_{\text{canonical}} = \arg\max_{\mathcal{C}} \\left\\{ \sum_{B \in \mathcal{C}} D(B), \; |\mathcal{C}| \\right\\} Chain canonical = arg C max l e f t B ∈ C ∑ D ( B ) , ∣ C ∣ r i g h t
where:
Prefer chain with higher cumulative difficulty
Tie-break by chain length
Tie-break by lexicographically smallest block hash
Reorg Protection
Maximum reorganization depth is bounded by:
Reorg max = min ( 128 , E period ) \text{Reorg}_{\text{max}} = \min(128, E_{\text{period}}) Reorg max = min ( 128 , E period )
Beyond 128 blocks, snapshots are checkpointed and considered immutable.
Block Production Latency
End-to-end block production time decomposition:
T total = T exec + T seal + T propagate T_{\text{total}} = T_{\text{exec}} + T_{\text{seal}} + T_{\text{propagate}} T total = T exec + T seal + T propagate
Typical values:
Component Time Percentage Transaction Execution 50-500ms 1.7-16.7% Block Sealing 1-5ms 0.03-0.17% Network Propagation 100-300ms 3.3-10% Consensus Overhead ~2-2.5s 66-83%
Throughput Characteristics
Theoretical limits:
TPS transfer = 30 , 000 , 000 21 , 000 × 3 ≈ 476 TPS TPS ERC20 = 30 , 000 , 000 65 , 000 × 3 ≈ 154 TPS TPS swap = 30 , 000 , 000 150 , 000 × 3 ≈ 67 TPS \begin{align*}
\text{TPS}_{\text{transfer}} &= \frac{30,000,000}{21,000 \times 3} \approx 476 \text{ TPS} \\
\text{TPS}_{\text{ERC20}} &= \frac{30,000,000}{65,000 \times 3} \approx 154 \text{ TPS} \\
\text{TPS}_{\text{swap}} &= \frac{30,000,000}{150,000 \times 3} \approx 67 \text{ TPS}
\end{align*} TPS transfer TPS ERC20 TPS swap = 21 , 000 × 3 30 , 000 , 000 ≈ 476 TPS = 65 , 000 × 3 30 , 000 , 000 ≈ 154 TPS = 150 , 000 × 3 30 , 000 , 000 ≈ 67 TPS
Consensus Safety Guarantees
Byzantine Fault Tolerance
Safety threshold:
f < n 2 f < \frac{n}{2} f < 2 n
where:
f f f = Number of Byzantine validators
n n n = Total active validators
Liveness Guarantee
Chain progresses if:
∣ V honest ∣ ≥ 1 |\mathcal{V}_{\text{honest}}| \geq 1 ∣ V honest ∣ ≥ 1
Even a single honest validator ensures liveness (though centralization risk).
Censorship Resistance
Transaction inclusion probability after k k k blocks:
P inclusion ( k ) = 1 − ( f n ) k P_{\text{inclusion}}(k) = 1 - \left(\frac{f}{n}\right)^k P inclusion ( k ) = 1 − ( n f ) k
For f / n = 0.3 f/n = 0.3 f / n = 0.3 (30% adversarial):
k = 1 k=1 k = 1 : 70% chance
k = 3 k=3 k = 3 : 97.3% chance
k = 5 k=5 k = 5 : 99.76% chance
System Contracts FenineSystem contract specification
FPoS Economics Validator selection and rewards
Security Model Attack vectors and mitigations
Network Parameters Configurable consensus constants