1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
//! Consensus configuration
use serde::{Deserialize, Serialize};
use std::time::Duration;
/// Configuration for the consensus engine
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsensusConfig {
/// Target block time in milliseconds (default: 400ms)
pub block_time_ms: u64,
/// Maximum block size in bytes (default: 2MB)
pub max_block_size: usize,
/// Maximum transactions per block (default: 10000)
pub max_transactions_per_block: usize,
/// Maximum gas per block (default: 30M)
pub max_gas_per_block: u64,
/// View timeout in milliseconds — **bootstrap seed only**.
///
/// This value seeds `ViewChangeTimer::base_timeout` at engine
/// construction. After the first successful view, the adaptive
/// tuner (`ViewChangeTimer::record_observed_view_latency`)
/// retunes the base timeout to track the cluster's actually-
/// observed quorum-formation latency.
///
/// **Why this is a seed, not a default**: the validator set is
/// open. A validator on residential WiFi joining a cluster of
/// datacenter peers cannot share a static default with a
/// single-region testnet. The adaptive algorithm tracks an EWMA
/// of observed view-to-QC latency and sets `base_timeout =
/// safety_multiplier × ewma`, clamped to `[base_floor,
/// base_ceiling]`. The seed only matters until the cluster has
/// produced a few successful views — after that, this value is
/// overwritten.
///
/// **Seed choice**: 1000ms is a reasonable midpoint. Lower seeds
/// (e.g. 200ms) cause noisy view-change storms during bootstrap
/// when no peer has spoken yet; higher seeds (e.g. 5000ms) waste
/// wall-clock on the first few views before adaptation kicks in.
/// Operators rarely need to tune this.
pub view_timeout_ms: u64,
/// Minimum validator count (default: 4)
pub min_validators: usize,
/// Byzantine fault tolerance threshold (default: 2f+1 where f = (n-1)/3)
pub bft_threshold: BftThreshold,
/// Epoch duration in blocks (default: 10000)
pub epoch_duration: u64,
/// Mempool size limit in bytes (default: 100MB)
pub mempool_size_limit: usize,
/// Maximum number of transactions in mempool (default: 10000)
pub mempool_max_transactions: usize,
/// Minimum gas price (in wei) accepted by the mempool, before lane
/// fee-floor multipliers are applied. Set to 0 to disable the static
/// floor entirely (mainnet should drive this off live EIP-1559 base fee).
/// Default: 1 Gwei (1e9 wei). Spec 2 lane fee-floor multipliers
/// (`fee_floor_mult(lane)`) scale this value at admission time so the
/// effective floor for a Verified-lane controller is `1.0 × mempool_min_gas_price`,
/// Delegated `1.5×`, and Open `4.0×` (per `AdmissionConfig::Default`).
pub mempool_min_gas_price: u64,
/// Transaction TTL in seconds (default: 600)
pub transaction_ttl_seconds: u64,
/// Enable optimistic responsiveness (default: true)
pub optimistic_responsiveness: bool,
/// Proposer election strategy.
///
/// Default is [`ProposerElectionKind::Reputation`] (Aptos LeaderReputation),
/// which prevents the chain from stalling when a single validator becomes
/// unresponsive. [`ProposerElectionKind::RoundRobin`] is retained for
/// tests and replay benchmarks.
pub proposer_election: ProposerElectionKind,
}
impl Default for ConsensusConfig {
fn default() -> Self {
Self {
block_time_ms: 400,
max_block_size: 2 * 1024 * 1024, // 2MB
max_transactions_per_block: 10_000,
max_gas_per_block: 30_000_000,
view_timeout_ms: 1000,
min_validators: 4,
bft_threshold: BftThreshold::TwoThirdsPlusOne,
epoch_duration: 10_000,
mempool_size_limit: 100 * 1024 * 1024, // 100MB
mempool_max_transactions: 10_000,
mempool_min_gas_price: 1_000_000_000, // 1 Gwei
transaction_ttl_seconds: 600,
optimistic_responsiveness: true,
proposer_election: ProposerElectionKind::Reputation,
}
}
}
impl ConsensusConfig {
/// Creates a new consensus configuration with default values
pub fn new() -> Self {
Self::default()
}
/// Sets the block time
pub fn with_block_time(mut self, block_time_ms: u64) -> Self {
self.block_time_ms = block_time_ms;
self
}
/// Sets the max block size
pub fn with_max_block_size(mut self, max_block_size: usize) -> Self {
self.max_block_size = max_block_size;
self
}
/// Sets the view timeout
pub fn with_view_timeout(mut self, view_timeout_ms: u64) -> Self {
self.view_timeout_ms = view_timeout_ms;
self
}
/// Sets the proposer election strategy.
pub fn with_proposer_election(mut self, kind: ProposerElectionKind) -> Self {
self.proposer_election = kind;
self
}
/// Returns the block time as a Duration
pub fn block_time(&self) -> Duration {
Duration::from_millis(self.block_time_ms)
}
/// Returns the view timeout as a Duration
pub fn view_timeout(&self) -> Duration {
Duration::from_millis(self.view_timeout_ms)
}
/// Returns the transaction TTL as a Duration
pub fn transaction_ttl(&self) -> Duration {
Duration::from_secs(self.transaction_ttl_seconds)
}
/// Calculates the quorum threshold for the given validator count
pub fn quorum_threshold(&self, validator_count: usize) -> usize {
self.bft_threshold.calculate(validator_count)
}
}
/// BFT threshold calculation strategy
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BftThreshold {
/// 2f+1 where f = (n-1)/3 (classic BFT)
TwoThirdsPlusOne,
/// Simple majority (n/2 + 1)
SimpleMajority,
}
impl BftThreshold {
/// Calculate the threshold for the given validator count
pub fn calculate(&self, validator_count: usize) -> usize {
match self {
BftThreshold::TwoThirdsPlusOne => {
// 2f+1 where f = (n-1)/3
// This tolerates f Byzantine faults
let f = (validator_count.saturating_sub(1)) / 3;
2 * f + 1
}
BftThreshold::SimpleMajority => validator_count / 2 + 1,
}
}
}
/// Proposer election strategy.
///
/// Renamed from `LeaderRotation` to avoid colliding with the
/// `ProposerElection` *trait* in `validator.rs`. The "Kind" suffix marks
/// this as the configuration discriminator: the engine resolves it to a
/// concrete `Box<dyn ProposerElection>` at startup.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProposerElectionKind {
/// Naïve `view % N` round-robin. Tests and very small validator sets only.
RoundRobin,
/// Aptos LeaderReputation: stake-weighted seeded draw with observed-
/// behaviour multipliers. Default and recommended for production.
Reputation,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = ConsensusConfig::default();
assert_eq!(config.block_time_ms, 400);
assert_eq!(config.max_block_size, 2 * 1024 * 1024);
assert_eq!(config.view_timeout_ms, 1000);
}
#[test]
fn test_bft_threshold() {
let threshold = BftThreshold::TwoThirdsPlusOne;
// With 4 validators: f=1, need 3 votes
assert_eq!(threshold.calculate(4), 3);
// With 7 validators: f=2, need 5 votes
assert_eq!(threshold.calculate(7), 5);
// With 10 validators: f=3, need 7 votes
assert_eq!(threshold.calculate(10), 7);
}
#[test]
fn test_config_builder() {
let config = ConsensusConfig::new()
.with_block_time(500)
.with_max_block_size(3 * 1024 * 1024)
.with_view_timeout(3000);
assert_eq!(config.block_time_ms, 500);
assert_eq!(config.max_block_size, 3 * 1024 * 1024);
assert_eq!(config.view_timeout_ms, 3000);
}
}