Consensus Mechanism
Mersennet uses Delegated Proof-of-Stake (DPoS) with round-robin proposer selection and priority-based weighting. This document provides a deep dive into how consensus works, from validator selection to block finalization and slashing.
Overviewβ
| Parameter | Value |
|---|---|
| Consensus | Delegated Proof-of-Stake (DPoS) |
| Block Time | ~1 second |
| Finality | BFT (Byzantine Fault Tolerant) |
| Implementation | Rust |
Validator Selectionβ
Validators are nodes that have staked PRIM tokens and registered in the validator set. Voting power is proportional to stake:
voting_power(validator) β staked_amount
Token holders can delegate their PRIM to validators, increasing that validator's voting power. The validator set is dynamicβnew validators can join by staking, and existing validators can leave by unbonding.
Proposer Rotationβ
Block production uses a round-robin algorithm with priority-based weighting:
- Each validator has a priority value that accumulates over time.
- The validator with the highest priority is selected as the proposer for the current block.
- After selection, the proposer's priority is reduced by the total weight of all validators.
- All validators' priorities are incremented by their normalized stake weight each round.
This ensures:
- Fair rotation β No single validator dominates block production
- Stake-weighted frequency β Validators with more stake are chosen more often
- Determinism β Given the same validator set and heights, proposer selection is reproducible
Priority Algorithm (Conceptual)β
priority[i] += normalized_weight[i] (each round)
priority[proposer] -= total_weight (after selection)
proposer = argmax(priority)
Weights are normalized from stake to prevent overflow with 18-decimal PRIM values.
Block Production Cycleβ
The block production cycle proceeds as follows:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BLOCK PRODUCTION CYCLE β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Height N
β
β 1. PROPOSER SELECTION
β ββ Select validator with highest priority
β
β 2. PROPOSAL PHASE
β ββ Proposer builds block (txs, orders, state)
β ββ Broadcasts block to all validators
β
β 3. PREVOTE PHASE
β ββ Validators receive block, validate
β ββ Broadcast prevote for block hash
β ββ Wait for 2/3+ prevotes
β
β 4. PRECOMMIT PHASE
β ββ Validators broadcast precommit
β ββ Wait for 2/3+ precommits
β
β 5. FINALIZATION
β ββ Block committed to chain
β ββ State root updated
β ββ Rewards distributed
β
βΌ
Height N+1
Diagram: Block Production Flowβ
Validator A Validator B Validator C Validator D
(Proposer) (Voter) (Voter) (Voter)
β β β β
β Create Block β β β
βββββββββββββββββββββββ€ β β
β β β β
β Broadcast Block β β β
βββββββββββββββββββββββΌβββββββββββββββββββββΌββββββββββββββββββββββ€
β β β β
β β Prevote β Prevote β Prevote
β ββββββββββββββββββββββΌββββββββββββββββββββββ€
β β β β
β β Precommit (2/3+) β Precommit β Precommit
β ββββββββββββββββββββββΌββββββββββββββββββββββ€
β β β β
β FINALIZE β β β
βββββββββββββββββββββββΌβββββββββββββββββββββΌββββββββββββββββββββββ€
β β β β
βΌ βΌ βΌ βΌ
Block Finalizationβ
A block is finalized when:
- Prevote threshold: More than 2/3 of total stake has broadcast a prevote for the block hash
- Precommit threshold: More than 2/3 of total stake has broadcast a precommit
T = 2/3 Γ total_stake + 1
finalized βΊ prevotes β₯ T AND precommits β₯ T
Finalized blocks are irreversibleβthere are no chain reorganizations. This provides fast, deterministic finality for applications.
Epoch Transitionsβ
Mersennet may use epochs for validator set updates (e.g. applying pending stake changes, unbonding completions). At epoch boundaries:
- Pending validator additions/removals are applied
- Unbonding queues are processed
- Validator set is updated for the next epoch
The exact epoch length is configurable. Validator set changes take effect at the start of the next epoch to ensure consensus continuity.
Slashing Mechanismβ
Slashing Typesβ
| Type | Trigger | Base Penalty | Consequence |
|---|---|---|---|
| Double-sign | Signing two different blocks at same height | 5% of stake | Tombstoned (permanent ban) |
| Timeout | Failing to precommit after prevoting | 1% of stake | Jailed (temporary exclusion) |
Escalationβ
Penalties escalate with repeated offenses:
actual_penalty = base_bps + (escalation_step Γ offense_count) + (escalation_step Γ rounds_missed)
actual_penalty = min(actual_penalty, max_escalation_bps)
With default parameters:
escalation_step= 25 bps (0.25%)max_escalation= 1000 bps (10%)
First timeout: 1%. Second: 1.25%. Third: 1.5%. And so on, up to 10%.
Slashing Source Priorityβ
When slashing is executed, tokens are taken from:
- Unbonding queue first β tokens being withdrawn
- Active stake second β if unbonding doesn't cover the penalty
- Remainder burned β if the validator doesn't have enough to cover
Tombstone vs. Jailβ
- Tombstoned: Permanently banned. Cannot rejoin.
- Jailed: Temporarily excluded. Can
unjail()after the jail period.
Block Structureβ
Every finalized block contains the following fields:
| Field | Type | Description |
|---|---|---|
number | u64 | Sequential block height starting from 0 (genesis) |
hash | B256 | Keccak-256 hash uniquely identifying this block |
chain_id | u64 | Network identifier (7919 for testnet) |
state_root | B256 | Merkle root of the post-execution state trie |
transactions | Vec<Transaction> | Ordered list of transactions included in the block |
receipts | Vec<Receipt> | Execution receipts corresponding 1:1 with transactions |
proposer | Address | Validator address that proposed this block |
coinbase | Address | Address receiving block rewards (same as proposer) |
gas_limit | u64 | Maximum gas allowed in this block (default 30,000,000) |
gas_used | u64 | Total gas consumed by all transactions |
base_fee | U256 | EIP-1559 base fee for this block (adjusts per block) |
finalized | bool | Whether 2/3+ stake committed to this block |
consensus | Finalization | BFT finalization data (prevotes, precommits, round info) |
rewards | Vec<Reward> | Per-validator block reward distributions |
total_reward | U256 | Sum of all rewards paid this block |
burned_reward | U256 | Portion of rewards burned (e.g. slashed stake) |
slashes | Vec<Slashing> | Slashing events applied in this block |
unbonded | Vec<Unbonding> | Completed unbonding operations |
domain_events | Vec<DomainEvent> | PrimeOrders and bridge events emitted during execution |
Receipt Formatβ
Each transaction produces a Receipt:
| Field | Type | Description |
|---|---|---|
success | bool | Whether the transaction executed without revert |
gas_used | u64 | Gas consumed by this transaction |
output | Bytes | Return data (ABI-encoded for contract calls) |
created_address | Option<Address> | Contract address if this was a deployment |
error | Option<String> | Human-readable error message on failure |
logs | Vec<LogEntry> | Emitted EVM logs (events) |
Transaction Lifecycleβ
A transaction moves through the following stages from submission to finalization:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TRANSACTION LIFECYCLE β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1. SUBMISSION
β Client sends signed transaction via JSON-RPC
β (eth_sendRawTransaction or prime_sendTransaction)
β
2. VALIDATION
β ββ Verify ECDSA signature (recover signer from r, s, v)
β ββ Check chain_id matches (7919 for testnet)
β ββ Verify nonce == account.nonce (no gaps, no replays)
β ββ Verify sender balance β₯ value + gas_limit Γ gas_price
β ββ Check gas_price β₯ base_fee (EIP-1559)
β
3. MEMPOOL
β ββ Insert into pending pool (max 10,000 total txs)
β ββ Per-sender limit: 1,000 pending txs
β ββ Replacement: new tx must bump gas_price by β₯10% (1000 bps)
β ββ Gossip transaction hash to connected peers
β
4. BLOCK INCLUSION
β ββ Proposer gathers txs from mempool ordered by gas_price
β ββ Txs included up to block gas_limit (30M gas)
β ββ Proposer builds candidate block
β
5. EVM EXECUTION
β ββ Execute each tx sequentially in revm
β ββ Apply state transitions (balance changes, storage writes)
β ββ Process PrimeOrders precompile calls (if any)
β ββ Generate receipt with logs, gas_used, status
β ββ Compute post-execution state_root
β
6. CONSENSUS & FINALIZATION
β ββ Proposer broadcasts block to validators
β ββ Validators verify block, broadcast prevote
β ββ On 2/3+ prevotes, broadcast precommit
β ββ On 2/3+ precommits, block is finalized (irreversible)
β
7. RECEIPT
ββ Client queries receipt via eth_getTransactionReceipt
Receipt includes: status, gas_used, logs, created_address
Nonce Managementβ
Nonces enforce strict transaction ordering per account:
- Each account has a monotonically increasing nonce starting at 0
- Transaction with nonce
Ncan only execute when the account's current nonce isN - If nonce
N+1arrives beforeN, it stays in the mempool queue untilNexecutes - Sending a new transaction with the same nonce replaces the pending one (if gas price is bumped by β₯10%)
State Managementβ
Mersennet maintains EVM-compatible world state using a Merkle trie structure.
State Trieβ
The world state is a mapping from addresses to account objects:
State Root (B256)
β
βββ Account 0x1234...
β βββ nonce: u64
β βββ balance: U256
β βββ code_hash: B256 (keccak256 of contract bytecode)
β βββ storage_root: B256 (root of account's storage trie)
β
βββ Account 0x5678...
β βββ ...
β
βββ ...
Each block produces a new state_root β the Merkle root computed over all account state after executing every transaction. This provides:
- Integrity verification β Any node can verify state correctness by recomputing the root
- Light client proofs β Merkle proofs can prove account balances without full state
- Determinism β Same transactions on same pre-state always produce the same state root
Storage Modelβ
Account storage follows the EVM model: each contract has a 256-bit key β 256-bit value mapping. Storage slots are accessed via SLOAD and SSTORE opcodes.
The current storage backend options are:
| Backend | Description | Use Case |
|---|---|---|
sled | Embedded key-value store (default) | Production nodes |
redb | Rust-native embedded database | Alternative production backend |
memory | In-memory (volatile) | Testing and development |
State Root Computationβ
After each block, the state root is computed using a sorted Merkle tree:
- Collect all
(address, account_data)pairs - Sort by address (deterministic ordering)
- Hash each pair:
keccak256(address || rlp(account)) - Build a binary Merkle tree from the leaf hashes
- The root hash becomes the block's
state_root
Network Protocolβ
Mersennet nodes communicate using a custom peer-to-peer protocol built on TCP and UDP.
Message Typesβ
Nodes exchange three categories of messages:
| Message | Transport | Purpose |
|---|---|---|
Tx | TCP + UDP gossip | Propagate new transactions to peers |
Block | TCP sync | Broadcast finalized blocks |
Vote | TCP + UDP gossip | Exchange prevote/precommit messages |
Peer Discoveryβ
Nodes discover peers through:
- Seed nodes β Configured in
p2p.peers(bootstrap addresses) - Peer exchange β Connected nodes share their known peer lists
- Persistent peer store β Known peers are saved to
peers.jsonfor reconnection on restart
Block Propagationβ
Proposer Node Validator Node A Validator Node B
β β β
β 1. Produce block β β
β 2. TCP broadcast ββββββββββββββ€ β
β βββββ TCP forward ββββββββββββββ€
β β β
β 3. Collect prevotes βββββββββββ€ β
β ββββββββββββΌβββββββββββββββββββββββββββββββ€
β β β
β 4. Collect precommits βββββββββ€ β
β βββββββββΌβββββββββββββββββββββββββββββββ€
β β β
β 5. Finalize & commit β Finalize & commit β Finalize & commit
βΌ βΌ βΌ
Transport Layersβ
| Layer | Protocol | Purpose |
|---|---|---|
| TCP Sync | TCP | Reliable block and state synchronization |
| UDP Gossip | UDP | Low-latency transaction and vote propagation |
| Noise Protocol | Optional | Encrypted P2P communication (enable with noise_enabled: true) |
Gossip Configurationβ
Transaction and vote gossip uses configurable parameters:
- Messages are forwarded to all connected peers
- Duplicate message detection prevents re-broadcasting
- Connection health is monitored with periodic pings
Configuration Referenceβ
Complete reference of all configuration parameters with their default values.
engine β Core Engine Settingsβ
| Parameter | Type | Default | Description |
|---|---|---|---|
chain_id | u64 | 7919 | Chain identifier (must match genesis) |
state_path | string | "state" | Directory for state storage |
gas_limit_per_block | u64 | 30000000 | Maximum gas per block (30M) |
fee_elasticity_multiplier | u64 | 2 | EIP-1559 elasticity multiplier |
fee_max_change_denominator | u64 | 8 | Max base fee change per block (12.5%) |
storage_backend | string | "sled" | Storage backend: "sled", "redb", or "memory" |
mempool β Transaction Poolβ
| Parameter | Type | Default | Description |
|---|---|---|---|
max_total | usize | 10000 | Maximum transactions in the mempool |
max_per_sender | usize | 1000 | Maximum pending txs per sender address |
bump_bps | u64 | 1000 | Minimum gas price bump for tx replacement (10%) |
p2p β Peer-to-Peer Networkingβ
| Parameter | Type | Default | Description |
|---|---|---|---|
node_key_path | string | "state/node_key.json" | Path to the node identity key |
peer_store_path | string | "state/peers.json" | Path to persistent peer list |
listen | string | "0.0.0.0:30303" | P2P listen address |
peers | string[] | [] | Seed peer addresses for bootstrap |
block_time_ms | u64 | 1000 | Target block production interval in ms |
noise_enabled | bool | false | Enable Noise protocol encryption |
rpc β JSON-RPC Serverβ
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable the HTTP JSON-RPC server |
addr | string | "127.0.0.1:8545" | RPC listen address and port |
ws β WebSocket Serverβ
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable the WebSocket server |
addr | string | "127.0.0.1:9945" | WebSocket listen address and port |
slashing β Slashing Parametersβ
| Parameter | Type | Default | Description |
|---|---|---|---|
double_sign_bps | u64 | 500 | Base penalty for double-signing (5%) |
timeout_bps | u64 | 100 | Base penalty for timeout (1%) |
escalation_step_bps | u64 | 25 | Penalty increase per repeated offense (0.25%) |
escalation_max_bps | u64 | 1000 | Maximum escalated penalty (10%) |
round_timeout_ms | u64 | 500 | Consensus round timeout in milliseconds |
unbonding_period | u64 | 2 | Epochs before unbonded stake is withdrawable |
token_economics β Reward & Supplyβ
| Parameter | Type | Default | Description |
|---|---|---|---|
max_supply | string | "1000000000000000000000000000" | Maximum PRIM supply (1B Γ 10ΒΉβΈ) |
initial_reward_per_block | string | "10000000000000000000" | Block reward (10 PRIM Γ 10ΒΉβΈ) |
halving_interval | u64 | 35000000 | Blocks between reward halvings |
prime_orders β PrimeOrders Precompileβ
| Parameter | Type | Default | Description |
|---|---|---|---|
initial_margin_bps | u64 | 0 | Initial margin requirement (basis points) |
maintenance_margin_bps | u64 | 0 | Maintenance margin requirement (basis points) |
bridge β Cross-Domain Bridgeβ
| Parameter | Type | Default | Description |
|---|---|---|---|
max_queue_len | usize | 10000 | Maximum pending bridge messages |
zk β Zero-Knowledge Proofsβ
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable ZK proof generation |
checkpoint_interval | u64 | 100 | Blocks between ZK checkpoints |
Example Full Configurationβ
{
"engine": {
"chain_id": 7919,
"state_path": "state",
"gas_limit_per_block": 30000000,
"fee_elasticity_multiplier": 2,
"fee_max_change_denominator": 8,
"storage_backend": "sled"
},
"mempool": {
"max_total": 10000,
"max_per_sender": 1000,
"bump_bps": 1000
},
"p2p": {
"node_key_path": "state/node_key.json",
"peer_store_path": "state/peers.json",
"listen": "0.0.0.0:30303",
"peers": [
"46.225.30.187:30303"
],
"block_time_ms": 1000,
"noise_enabled": false
},
"rpc": {
"enabled": true,
"addr": "0.0.0.0:8545"
},
"ws": {
"enabled": true,
"addr": "0.0.0.0:9945"
},
"slashing": {
"double_sign_bps": 500,
"timeout_bps": 100,
"escalation_step_bps": 25,
"escalation_max_bps": 1000,
"round_timeout_ms": 500,
"unbonding_period": 2
},
"token_economics": {
"max_supply": "1000000000000000000000000000",
"initial_reward_per_block": "10000000000000000000",
"halving_interval": 35000000
},
"prime_orders": {
"initial_margin_bps": 0,
"maintenance_margin_bps": 0
},
"bridge": {
"max_queue_len": 10000
},
"zk": {
"enabled": false,
"checkpoint_interval": 100
}
}
Summaryβ
Mersennet's DPoS consensus provides:
- Fast finality (~1s block time)
- Fair proposer rotation (stake-weighted round-robin)
- BFT security (2/3+ stake required)
- Economic security (slashing for misbehavior)
- No reversals (finalized blocks are final)