1use ruc::*;
2
3use crate::application::Application;
4use crate::store::BlockStore;
5use hotmint_types::context::BlockContext;
6use hotmint_types::epoch::Epoch;
7use hotmint_types::{Block, BlockHash, DoubleCertificate, EndBlockResponse, Height, ViewNumber};
8use tracing::info;
9
10pub struct CommitResult {
12 pub committed_blocks: Vec<Block>,
13 pub commit_qc: hotmint_types::QuorumCertificate,
15 pub pending_epoch: Option<Epoch>,
17 pub last_app_hash: BlockHash,
19 pub block_responses: Vec<EndBlockResponse>,
21}
22
23pub fn decode_payload(payload: &[u8]) -> Vec<&[u8]> {
25 let mut txs = Vec::new();
26 let mut offset = 0;
27 while offset + 4 <= payload.len() {
28 let len = u32::from_le_bytes(payload[offset..offset + 4].try_into().unwrap()) as usize;
29 offset += 4;
30 if offset + len > payload.len() {
31 tracing::warn!(
32 offset,
33 claimed_len = len,
34 payload_len = payload.len(),
35 "decode_payload: length prefix exceeds remaining bytes, truncating"
36 );
37 break;
38 }
39 txs.push(&payload[offset..offset + len]);
40 offset += len;
41 }
42 txs
43}
44
45pub fn try_commit(
56 double_cert: &DoubleCertificate,
57 store: &dyn BlockStore,
58 app: &dyn Application,
59 last_committed_height: &mut Height,
60 current_epoch: &Epoch,
61) -> Result<CommitResult> {
62 let commit_hash = double_cert.inner_qc.block_hash;
63 let commit_block = store
64 .get_block(&commit_hash)
65 .c(d!("block to commit not found"))?;
66
67 if commit_block.height <= *last_committed_height {
68 return Ok(CommitResult {
69 committed_blocks: vec![],
70 commit_qc: double_cert.inner_qc.clone(),
71 pending_epoch: None,
72 last_app_hash: BlockHash::GENESIS,
73 block_responses: vec![],
74 });
75 }
76
77 let mut to_commit = Vec::new();
79 let mut current = commit_block;
80 loop {
81 if current.height <= *last_committed_height {
82 break;
83 }
84 let parent_hash = current.parent_hash;
85 let current_height = current.height;
86 to_commit.push(current);
87 if parent_hash == BlockHash::GENESIS {
88 break;
89 }
90 match store.get_block(&parent_hash) {
91 Some(parent) => current = parent,
92 None => {
93 if current_height > Height(last_committed_height.as_u64() + 1) {
96 return Err(eg!(
97 "missing ancestor block {} for height {} (last committed: {})",
98 parent_hash,
99 current_height,
100 last_committed_height
101 ));
102 }
103 break;
104 }
105 }
106 }
107
108 to_commit.reverse();
110
111 let mut pending_epoch: Option<Epoch> = None;
112 let mut last_app_hash = BlockHash::GENESIS;
113 let mut block_responses = Vec::with_capacity(to_commit.len());
114
115 for block in &to_commit {
116 let ctx = BlockContext {
117 height: block.height,
118 view: block.view,
119 proposer: block.proposer,
120 epoch: current_epoch.number,
121 epoch_start_view: current_epoch.start_view,
122 validator_set: ¤t_epoch.validator_set,
123 vote_extensions: vec![],
124 };
125
126 info!(height = block.height.as_u64(), hash = %block.hash, "committing block");
127
128 let txs = decode_payload(&block.payload);
129 let response = app.execute_block(&txs, &ctx).unwrap_or_else(|e| {
134 panic!(
135 "FATAL: execute_block failed for committed block height={} hash={}: {:?}. \
136 Node state is corrupt; restart from last committed height.",
137 block.height, block.hash, e
138 )
139 });
140
141 app.on_commit(block, &ctx).unwrap_or_else(|e| {
142 panic!(
143 "FATAL: on_commit failed for committed block height={} hash={}: {:?}. \
144 Node state is corrupt; restart from last committed height.",
145 block.height, block.hash, e
146 )
147 });
148
149 for proof in &block.evidence {
152 if let Err(e) = app.on_evidence(proof) {
153 tracing::warn!(
154 validator = %proof.validator,
155 error = %e,
156 "on_evidence failed for embedded proof"
157 );
158 }
159 }
160
161 last_app_hash = if app.tracks_app_hash() {
165 response.app_hash
166 } else {
167 block.app_hash
168 };
169
170 if !response.validator_updates.is_empty() {
171 let base_vs = if let Some(ref ep) = pending_epoch {
175 &ep.validator_set
176 } else {
177 ¤t_epoch.validator_set
178 };
179 let base_num = if let Some(ref ep) = pending_epoch {
180 ep.number
181 } else {
182 current_epoch.number
183 };
184 let new_vs = base_vs.apply_updates(&response.validator_updates);
185 let epoch_start = ViewNumber(block.view.as_u64() + 2);
186 pending_epoch = Some(Epoch::new(base_num.next(), epoch_start, new_vs));
187 }
188
189 block_responses.push(response);
190 *last_committed_height = block.height;
191 }
192
193 Ok(CommitResult {
194 committed_blocks: to_commit,
195 commit_qc: double_cert.inner_qc.clone(),
196 pending_epoch,
197 last_app_hash,
198 block_responses,
199 })
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use crate::application::NoopApplication;
206 use crate::store::MemoryBlockStore;
207 use hotmint_types::crypto::PublicKey;
208 use hotmint_types::epoch::EpochNumber;
209 use hotmint_types::validator::{ValidatorInfo, ValidatorSet};
210 use hotmint_types::{AggregateSignature, QuorumCertificate, ValidatorId, ViewNumber};
211
212 fn make_block(height: u64, parent: BlockHash) -> Block {
213 let hash = BlockHash([height as u8; 32]);
214 Block {
215 height: Height(height),
216 parent_hash: parent,
217 view: ViewNumber(height),
218 proposer: ValidatorId(0),
219 timestamp: 0,
220 payload: vec![],
221 app_hash: BlockHash::GENESIS,
222 evidence: Vec::new(),
223 hash,
224 }
225 }
226
227 fn make_qc(hash: BlockHash, view: u64) -> QuorumCertificate {
228 QuorumCertificate {
229 block_hash: hash,
230 view: ViewNumber(view),
231 aggregate_signature: AggregateSignature::new(4),
232 epoch: EpochNumber(0),
233 }
234 }
235
236 fn make_epoch() -> Epoch {
237 let vs = ValidatorSet::new(vec![ValidatorInfo {
238 id: ValidatorId(0),
239 public_key: PublicKey(vec![0]),
240 power: 1,
241 }]);
242 Epoch::genesis(vs)
243 }
244
245 #[test]
246 fn test_commit_single_block() {
247 let mut store = MemoryBlockStore::new();
248 let app = NoopApplication;
249 let epoch = make_epoch();
250 let b1 = make_block(1, BlockHash::GENESIS);
251 store.put_block(b1.clone());
252
253 let dc = DoubleCertificate {
254 vote_extensions: vec![],
255 inner_qc: make_qc(b1.hash, 1),
256 outer_qc: make_qc(b1.hash, 1),
257 };
258
259 let mut last = Height::GENESIS;
260 let result = try_commit(&dc, &store, &app, &mut last, &epoch).unwrap();
261 assert_eq!(result.committed_blocks.len(), 1);
262 assert_eq!(result.committed_blocks[0].height, Height(1));
263 assert_eq!(last, Height(1));
264 assert!(result.pending_epoch.is_none());
265 }
266
267 #[test]
268 fn test_commit_chain_of_blocks() {
269 let mut store = MemoryBlockStore::new();
270 let app = NoopApplication;
271 let epoch = make_epoch();
272 let b1 = make_block(1, BlockHash::GENESIS);
273 let b2 = make_block(2, b1.hash);
274 let b3 = make_block(3, b2.hash);
275 store.put_block(b1);
276 store.put_block(b2);
277 store.put_block(b3.clone());
278
279 let dc = DoubleCertificate {
280 vote_extensions: vec![],
281 inner_qc: make_qc(b3.hash, 3),
282 outer_qc: make_qc(b3.hash, 3),
283 };
284
285 let mut last = Height::GENESIS;
286 let result = try_commit(&dc, &store, &app, &mut last, &epoch).unwrap();
287 assert_eq!(result.committed_blocks.len(), 3);
288 assert_eq!(result.committed_blocks[0].height, Height(1));
289 assert_eq!(result.committed_blocks[1].height, Height(2));
290 assert_eq!(result.committed_blocks[2].height, Height(3));
291 assert_eq!(last, Height(3));
292 }
293
294 #[test]
295 fn test_commit_already_committed() {
296 let mut store = MemoryBlockStore::new();
297 let app = NoopApplication;
298 let epoch = make_epoch();
299 let b1 = make_block(1, BlockHash::GENESIS);
300 store.put_block(b1.clone());
301
302 let dc = DoubleCertificate {
303 vote_extensions: vec![],
304 inner_qc: make_qc(b1.hash, 1),
305 outer_qc: make_qc(b1.hash, 1),
306 };
307
308 let mut last = Height(1);
309 let result = try_commit(&dc, &store, &app, &mut last, &epoch).unwrap();
310 assert!(result.committed_blocks.is_empty());
311 }
312
313 #[test]
314 fn test_commit_partial_chain() {
315 let mut store = MemoryBlockStore::new();
316 let app = NoopApplication;
317 let epoch = make_epoch();
318 let b1 = make_block(1, BlockHash::GENESIS);
319 let b2 = make_block(2, b1.hash);
320 let b3 = make_block(3, b2.hash);
321 store.put_block(b1);
322 store.put_block(b2);
323 store.put_block(b3.clone());
324
325 let dc = DoubleCertificate {
326 vote_extensions: vec![],
327 inner_qc: make_qc(b3.hash, 3),
328 outer_qc: make_qc(b3.hash, 3),
329 };
330
331 let mut last = Height(1);
332 let result = try_commit(&dc, &store, &app, &mut last, &epoch).unwrap();
333 assert_eq!(result.committed_blocks.len(), 2);
334 assert_eq!(result.committed_blocks[0].height, Height(2));
335 assert_eq!(result.committed_blocks[1].height, Height(3));
336 }
337
338 #[test]
339 fn test_commit_missing_block() {
340 let store = MemoryBlockStore::new();
341 let app = NoopApplication;
342 let epoch = make_epoch();
343 let dc = DoubleCertificate {
344 vote_extensions: vec![],
345 inner_qc: make_qc(BlockHash([99u8; 32]), 1),
346 outer_qc: make_qc(BlockHash([99u8; 32]), 1),
347 };
348 let mut last = Height::GENESIS;
349 assert!(try_commit(&dc, &store, &app, &mut last, &epoch).is_err());
350 }
351
352 #[test]
353 fn test_decode_payload_empty() {
354 assert!(decode_payload(&[]).is_empty());
355 }
356
357 #[test]
358 fn test_decode_payload_roundtrip() {
359 let mut payload = Vec::new();
361 let tx1 = b"hello";
362 let tx2 = b"world";
363 payload.extend_from_slice(&(tx1.len() as u32).to_le_bytes());
364 payload.extend_from_slice(tx1);
365 payload.extend_from_slice(&(tx2.len() as u32).to_le_bytes());
366 payload.extend_from_slice(tx2);
367
368 let txs = decode_payload(&payload);
369 assert_eq!(txs.len(), 2);
370 assert_eq!(txs[0], b"hello");
371 assert_eq!(txs[1], b"world");
372 }
373}