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, 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}
20
21pub fn decode_payload(payload: &[u8]) -> Vec<&[u8]> {
23 let mut txs = Vec::new();
24 let mut offset = 0;
25 while offset + 4 <= payload.len() {
26 let len = u32::from_le_bytes(payload[offset..offset + 4].try_into().unwrap()) as usize;
27 offset += 4;
28 if offset + len > payload.len() {
29 break;
30 }
31 txs.push(&payload[offset..offset + len]);
32 offset += len;
33 }
34 txs
35}
36
37pub fn try_commit(
48 double_cert: &DoubleCertificate,
49 store: &dyn BlockStore,
50 app: &dyn Application,
51 last_committed_height: &mut Height,
52 current_epoch: &Epoch,
53) -> Result<CommitResult> {
54 let commit_hash = double_cert.inner_qc.block_hash;
55 let commit_block = store
56 .get_block(&commit_hash)
57 .c(d!("block to commit not found"))?;
58
59 if commit_block.height <= *last_committed_height {
60 return Ok(CommitResult {
61 committed_blocks: vec![],
62 commit_qc: double_cert.inner_qc.clone(),
63 pending_epoch: None,
64 last_app_hash: BlockHash::GENESIS,
65 });
66 }
67
68 let mut to_commit = Vec::new();
70 let mut current = commit_block;
71 loop {
72 if current.height <= *last_committed_height {
73 break;
74 }
75 let parent_hash = current.parent_hash;
76 let current_height = current.height;
77 to_commit.push(current);
78 if parent_hash == BlockHash::GENESIS {
79 break;
80 }
81 match store.get_block(&parent_hash) {
82 Some(parent) => current = parent,
83 None => {
84 if current_height > Height(last_committed_height.as_u64() + 1) {
87 return Err(eg!(
88 "missing ancestor block {} for height {} (last committed: {})",
89 parent_hash,
90 current_height,
91 last_committed_height
92 ));
93 }
94 break;
95 }
96 }
97 }
98
99 to_commit.reverse();
101
102 let mut pending_epoch = None;
103 let mut last_app_hash = BlockHash::GENESIS;
104
105 for block in &to_commit {
106 let ctx = BlockContext {
107 height: block.height,
108 view: block.view,
109 proposer: block.proposer,
110 epoch: current_epoch.number,
111 epoch_start_view: current_epoch.start_view,
112 validator_set: ¤t_epoch.validator_set,
113 };
114
115 info!(height = block.height.as_u64(), hash = %block.hash, "committing block");
116
117 let txs = decode_payload(&block.payload);
118 let response = app.execute_block(&txs, &ctx).unwrap_or_else(|e| {
123 panic!(
124 "FATAL: execute_block failed for committed block height={} hash={}: {:?}. \
125 Node state is corrupt; restart from last committed height.",
126 block.height, block.hash, e
127 )
128 });
129
130 app.on_commit(block, &ctx).unwrap_or_else(|e| {
131 panic!(
132 "FATAL: on_commit failed for committed block height={} hash={}: {:?}. \
133 Node state is corrupt; restart from last committed height.",
134 block.height, block.hash, e
135 )
136 });
137
138 last_app_hash = if app.tracks_app_hash() {
142 response.app_hash
143 } else {
144 block.app_hash
145 };
146
147 if !response.validator_updates.is_empty() {
148 let new_vs = current_epoch
149 .validator_set
150 .apply_updates(&response.validator_updates);
151 let epoch_start = ViewNumber(block.view.as_u64() + 2);
152 pending_epoch = Some(Epoch::new(current_epoch.number.next(), epoch_start, new_vs));
153 }
154
155 *last_committed_height = block.height;
156 }
157
158 Ok(CommitResult {
159 committed_blocks: to_commit,
160 commit_qc: double_cert.inner_qc.clone(),
161 pending_epoch,
162 last_app_hash,
163 })
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use crate::application::NoopApplication;
170 use crate::store::MemoryBlockStore;
171 use hotmint_types::crypto::PublicKey;
172 use hotmint_types::epoch::EpochNumber;
173 use hotmint_types::validator::{ValidatorInfo, ValidatorSet};
174 use hotmint_types::{AggregateSignature, QuorumCertificate, ValidatorId, ViewNumber};
175
176 fn make_block(height: u64, parent: BlockHash) -> Block {
177 let hash = BlockHash([height as u8; 32]);
178 Block {
179 height: Height(height),
180 parent_hash: parent,
181 view: ViewNumber(height),
182 proposer: ValidatorId(0),
183 payload: vec![],
184 app_hash: BlockHash::GENESIS,
185 hash,
186 }
187 }
188
189 fn make_qc(hash: BlockHash, view: u64) -> QuorumCertificate {
190 QuorumCertificate {
191 block_hash: hash,
192 view: ViewNumber(view),
193 aggregate_signature: AggregateSignature::new(4),
194 epoch: EpochNumber(0),
195 }
196 }
197
198 fn make_epoch() -> Epoch {
199 let vs = ValidatorSet::new(vec![ValidatorInfo {
200 id: ValidatorId(0),
201 public_key: PublicKey(vec![0]),
202 power: 1,
203 }]);
204 Epoch::genesis(vs)
205 }
206
207 #[test]
208 fn test_commit_single_block() {
209 let mut store = MemoryBlockStore::new();
210 let app = NoopApplication;
211 let epoch = make_epoch();
212 let b1 = make_block(1, BlockHash::GENESIS);
213 store.put_block(b1.clone());
214
215 let dc = DoubleCertificate {
216 inner_qc: make_qc(b1.hash, 1),
217 outer_qc: make_qc(b1.hash, 1),
218 };
219
220 let mut last = Height::GENESIS;
221 let result = try_commit(&dc, &store, &app, &mut last, &epoch).unwrap();
222 assert_eq!(result.committed_blocks.len(), 1);
223 assert_eq!(result.committed_blocks[0].height, Height(1));
224 assert_eq!(last, Height(1));
225 assert!(result.pending_epoch.is_none());
226 }
227
228 #[test]
229 fn test_commit_chain_of_blocks() {
230 let mut store = MemoryBlockStore::new();
231 let app = NoopApplication;
232 let epoch = make_epoch();
233 let b1 = make_block(1, BlockHash::GENESIS);
234 let b2 = make_block(2, b1.hash);
235 let b3 = make_block(3, b2.hash);
236 store.put_block(b1);
237 store.put_block(b2);
238 store.put_block(b3.clone());
239
240 let dc = DoubleCertificate {
241 inner_qc: make_qc(b3.hash, 3),
242 outer_qc: make_qc(b3.hash, 3),
243 };
244
245 let mut last = Height::GENESIS;
246 let result = try_commit(&dc, &store, &app, &mut last, &epoch).unwrap();
247 assert_eq!(result.committed_blocks.len(), 3);
248 assert_eq!(result.committed_blocks[0].height, Height(1));
249 assert_eq!(result.committed_blocks[1].height, Height(2));
250 assert_eq!(result.committed_blocks[2].height, Height(3));
251 assert_eq!(last, Height(3));
252 }
253
254 #[test]
255 fn test_commit_already_committed() {
256 let mut store = MemoryBlockStore::new();
257 let app = NoopApplication;
258 let epoch = make_epoch();
259 let b1 = make_block(1, BlockHash::GENESIS);
260 store.put_block(b1.clone());
261
262 let dc = DoubleCertificate {
263 inner_qc: make_qc(b1.hash, 1),
264 outer_qc: make_qc(b1.hash, 1),
265 };
266
267 let mut last = Height(1);
268 let result = try_commit(&dc, &store, &app, &mut last, &epoch).unwrap();
269 assert!(result.committed_blocks.is_empty());
270 }
271
272 #[test]
273 fn test_commit_partial_chain() {
274 let mut store = MemoryBlockStore::new();
275 let app = NoopApplication;
276 let epoch = make_epoch();
277 let b1 = make_block(1, BlockHash::GENESIS);
278 let b2 = make_block(2, b1.hash);
279 let b3 = make_block(3, b2.hash);
280 store.put_block(b1);
281 store.put_block(b2);
282 store.put_block(b3.clone());
283
284 let dc = DoubleCertificate {
285 inner_qc: make_qc(b3.hash, 3),
286 outer_qc: make_qc(b3.hash, 3),
287 };
288
289 let mut last = Height(1);
290 let result = try_commit(&dc, &store, &app, &mut last, &epoch).unwrap();
291 assert_eq!(result.committed_blocks.len(), 2);
292 assert_eq!(result.committed_blocks[0].height, Height(2));
293 assert_eq!(result.committed_blocks[1].height, Height(3));
294 }
295
296 #[test]
297 fn test_commit_missing_block() {
298 let store = MemoryBlockStore::new();
299 let app = NoopApplication;
300 let epoch = make_epoch();
301 let dc = DoubleCertificate {
302 inner_qc: make_qc(BlockHash([99u8; 32]), 1),
303 outer_qc: make_qc(BlockHash([99u8; 32]), 1),
304 };
305 let mut last = Height::GENESIS;
306 assert!(try_commit(&dc, &store, &app, &mut last, &epoch).is_err());
307 }
308
309 #[test]
310 fn test_decode_payload_empty() {
311 assert!(decode_payload(&[]).is_empty());
312 }
313
314 #[test]
315 fn test_decode_payload_roundtrip() {
316 let mut payload = Vec::new();
318 let tx1 = b"hello";
319 let tx2 = b"world";
320 payload.extend_from_slice(&(tx1.len() as u32).to_le_bytes());
321 payload.extend_from_slice(tx1);
322 payload.extend_from_slice(&(tx2.len() as u32).to_le_bytes());
323 payload.extend_from_slice(tx2);
324
325 let txs = decode_payload(&payload);
326 assert_eq!(txs.len(), 2);
327 assert_eq!(txs[0], b"hello");
328 assert_eq!(txs[1], b"world");
329 }
330}