hotmint_consensus/application.rs
1use ruc::*;
2
3use hotmint_types::Block;
4use hotmint_types::Height;
5use hotmint_types::block::BlockHash;
6use hotmint_types::context::{BlockContext, TxContext};
7use hotmint_types::evidence::EquivocationProof;
8use hotmint_types::validator::ValidatorId;
9use hotmint_types::validator_update::EndBlockResponse;
10
11/// Result of transaction validation, including priority and gas for mempool ordering.
12#[derive(Debug, Clone)]
13pub struct TxValidationResult {
14 /// Whether the transaction is valid.
15 pub valid: bool,
16 /// Priority for mempool ordering (higher = included first).
17 /// Applications typically derive this from gas price / fee.
18 pub priority: u64,
19 /// Gas units this transaction will consume. Used by `collect_payload`
20 /// to enforce `max_gas_per_block` limits. Default: 0 (no gas accounting).
21 pub gas_wanted: u64,
22}
23
24impl TxValidationResult {
25 pub fn accept(priority: u64) -> Self {
26 Self {
27 valid: true,
28 priority,
29 gas_wanted: 0,
30 }
31 }
32
33 pub fn accept_with_gas(priority: u64, gas_wanted: u64) -> Self {
34 Self {
35 valid: true,
36 priority,
37 gas_wanted,
38 }
39 }
40
41 pub fn reject() -> Self {
42 Self {
43 valid: false,
44 priority: 0,
45 gas_wanted: 0,
46 }
47 }
48}
49
50/// Application info returned by [`Application::info`].
51///
52/// Used on startup to reconcile the application's last committed state with
53/// the consensus engine's persisted state.
54#[derive(Debug, Clone, Default)]
55pub struct AppInfo {
56 /// The height of the last block the application has committed.
57 pub last_block_height: Height,
58 /// The app_hash after the last committed block.
59 pub last_block_app_hash: BlockHash,
60}
61
62/// Application interface for the consensus engine.
63///
64/// The lifecycle for each committed block:
65/// 1. `execute_block` — receives all decoded transactions at once; returns
66/// validator updates and events
67/// 2. `on_commit` — notification after the block is finalized
68///
69/// For block proposal:
70/// - `create_payload` — build the payload bytes for a new block
71///
72/// For validation (before voting):
73/// - `validate_block` — full block validation
74/// - `validate_tx` — individual transaction validation for mempool
75///
76/// For evidence:
77/// - `on_evidence` — called when equivocation is detected
78///
79/// All methods have default no-op implementations.
80pub trait Application: Send + Sync {
81 /// Return the application's last committed height and app_hash.
82 ///
83 /// Called on startup so the consensus engine can detect state divergence
84 /// between its persisted state and the application. Equivalent to
85 /// CometBFT's `Info` RPC.
86 fn info(&self) -> AppInfo {
87 AppInfo::default()
88 }
89
90 /// Initialize the application with genesis state.
91 ///
92 /// Called once before the first block when the chain starts from height 0.
93 /// The application should use this to set its initial state (e.g. genesis
94 /// accounts, initial parameters). Returns the initial app_hash.
95 ///
96 /// Equivalent to CometBFT's `InitChain`.
97 fn init_chain(&self, _app_state: &[u8]) -> Result<BlockHash> {
98 Ok(BlockHash::GENESIS)
99 }
100
101 /// Create a payload for a new block proposal.
102 /// Typically pulls transactions from the mempool.
103 ///
104 /// If your mempool is async, use `tokio::runtime::Handle::current().block_on(..)`
105 /// to bridge into this synchronous callback.
106 fn create_payload(&self, _ctx: &BlockContext) -> Vec<u8> {
107 vec![]
108 }
109
110 /// Validate a proposed block before voting.
111 fn validate_block(&self, _block: &Block, _ctx: &BlockContext) -> bool {
112 true
113 }
114
115 /// Validate a single transaction for mempool admission.
116 ///
117 /// Returns a [`TxValidationResult`] with `valid` and `priority`.
118 /// Priority determines ordering in the mempool (higher = included first).
119 ///
120 /// An optional [`TxContext`] provides the current chain height and epoch,
121 /// which can be useful for state-dependent validation (nonce checks, etc.).
122 fn validate_tx(&self, _tx: &[u8], _ctx: Option<&TxContext>) -> TxValidationResult {
123 TxValidationResult::accept(0)
124 }
125
126 /// Execute an entire block in one call.
127 ///
128 /// Receives all decoded transactions from the block payload at once,
129 /// allowing batch-optimised processing (bulk DB writes, parallel
130 /// signature verification, etc.).
131 ///
132 /// Return [`EndBlockResponse`] with `validator_updates` to schedule an
133 /// epoch transition, and/or `events` to emit application-defined events.
134 fn execute_block(&self, _txs: &[&[u8]], _ctx: &BlockContext) -> Result<EndBlockResponse> {
135 Ok(EndBlockResponse::default())
136 }
137
138 /// Called when a block is committed to the chain (notification).
139 fn on_commit(&self, _block: &Block, _ctx: &BlockContext) -> Result<()> {
140 Ok(())
141 }
142
143 /// Called when equivocation (double-voting) is detected.
144 /// The application can use this to implement slashing.
145 fn on_evidence(&self, _proof: &EquivocationProof) -> Result<()> {
146 Ok(())
147 }
148
149 /// Called at epoch boundaries with validators whose commit-QC sign rate
150 /// fell below the liveness threshold (>50% missed).
151 ///
152 /// The application can use this to apply downtime slashing.
153 /// Each entry contains `(validator_id, missed_commits, total_commits)`.
154 fn on_offline_validators(&self, _offline: &[crate::liveness::OfflineEvidence]) -> Result<()> {
155 Ok(())
156 }
157
158 /// Generate a vote extension for the given block (ABCI++ Vote Extensions).
159 /// Called before casting a Vote2 (second-phase vote).
160 /// Returns None to skip extension (default behavior).
161 fn extend_vote(&self, _block: &Block, _ctx: &BlockContext) -> Option<Vec<u8>> {
162 None
163 }
164
165 /// Verify a vote extension received from another validator.
166 /// Called when processing Vote2 messages that carry extensions.
167 /// Returns true if the extension is valid (default: accept all).
168 fn verify_vote_extension(
169 &self,
170 _extension: &[u8],
171 _block_hash: &BlockHash,
172 _validator: ValidatorId,
173 ) -> bool {
174 true
175 }
176
177 /// Query application state.
178 ///
179 /// Returns a [`hotmint_types::QueryResponse`] containing the result data and an optional
180 /// Merkle proof that allows light clients to verify the result against the
181 /// block's `app_hash` without trusting the full node.
182 fn query(&self, _path: &str, _data: &[u8]) -> Result<hotmint_types::QueryResponse> {
183 Ok(hotmint_types::QueryResponse::default())
184 }
185
186 /// List available state snapshots for state sync.
187 fn list_snapshots(&self) -> Vec<hotmint_types::sync::SnapshotInfo> {
188 vec![]
189 }
190
191 /// Load a chunk of a snapshot at the given height.
192 fn load_snapshot_chunk(&self, _height: hotmint_types::Height, _chunk_index: u32) -> Vec<u8> {
193 vec![]
194 }
195
196 /// Offer a snapshot to the application for state sync.
197 fn offer_snapshot(
198 &self,
199 _snapshot: &hotmint_types::sync::SnapshotInfo,
200 ) -> hotmint_types::sync::SnapshotOfferResult {
201 hotmint_types::sync::SnapshotOfferResult::Reject
202 }
203
204 /// Apply a snapshot chunk received during state sync.
205 fn apply_snapshot_chunk(
206 &self,
207 _chunk: Vec<u8>,
208 _chunk_index: u32,
209 ) -> hotmint_types::sync::ChunkApplyResult {
210 hotmint_types::sync::ChunkApplyResult::Abort
211 }
212
213 /// Whether this application produces and verifies `app_hash` state roots.
214 ///
215 /// Applications that do not maintain a deterministic state root (e.g. the
216 /// embedded [`NoopApplication`] used by fullnodes without an ABCI backend)
217 /// should return `false`. Sync will then bypass the app_hash equality
218 /// check and accept the chain's authoritative value, allowing the node to
219 /// follow a chain produced by peers running a real application.
220 fn tracks_app_hash(&self) -> bool {
221 true
222 }
223}
224
225/// No-op application stub for testing and fullnode-without-ABCI mode.
226pub struct NoopApplication;
227
228impl Application for NoopApplication {
229 /// NoopApplication does not maintain state, so app_hash tracking is skipped.
230 fn tracks_app_hash(&self) -> bool {
231 false
232 }
233}