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
119 .execute_block(&txs, &ctx)
120 .c(d!("execute_block failed"))?;
121
122 app.on_commit(block, &ctx)
123 .c(d!("application commit failed"))?;
124
125 last_app_hash = if app.tracks_app_hash() {
129 response.app_hash
130 } else {
131 block.app_hash
132 };
133
134 if !response.validator_updates.is_empty() {
135 let new_vs = current_epoch
136 .validator_set
137 .apply_updates(&response.validator_updates);
138 let epoch_start = ViewNumber(block.view.as_u64() + 2);
139 pending_epoch = Some(Epoch::new(current_epoch.number.next(), epoch_start, new_vs));
140 }
141
142 *last_committed_height = block.height;
143 }
144
145 Ok(CommitResult {
146 committed_blocks: to_commit,
147 commit_qc: double_cert.inner_qc.clone(),
148 pending_epoch,
149 last_app_hash,
150 })
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use crate::application::NoopApplication;
157 use crate::store::MemoryBlockStore;
158 use hotmint_types::crypto::PublicKey;
159 use hotmint_types::validator::{ValidatorInfo, ValidatorSet};
160 use hotmint_types::{AggregateSignature, QuorumCertificate, ValidatorId, ViewNumber};
161
162 fn make_block(height: u64, parent: BlockHash) -> Block {
163 let hash = BlockHash([height as u8; 32]);
164 Block {
165 height: Height(height),
166 parent_hash: parent,
167 view: ViewNumber(height),
168 proposer: ValidatorId(0),
169 payload: vec![],
170 app_hash: BlockHash::GENESIS,
171 hash,
172 }
173 }
174
175 fn make_qc(hash: BlockHash, view: u64) -> QuorumCertificate {
176 QuorumCertificate {
177 block_hash: hash,
178 view: ViewNumber(view),
179 aggregate_signature: AggregateSignature::new(4),
180 }
181 }
182
183 fn make_epoch() -> Epoch {
184 let vs = ValidatorSet::new(vec![ValidatorInfo {
185 id: ValidatorId(0),
186 public_key: PublicKey(vec![0]),
187 power: 1,
188 }]);
189 Epoch::genesis(vs)
190 }
191
192 #[test]
193 fn test_commit_single_block() {
194 let mut store = MemoryBlockStore::new();
195 let app = NoopApplication;
196 let epoch = make_epoch();
197 let b1 = make_block(1, BlockHash::GENESIS);
198 store.put_block(b1.clone());
199
200 let dc = DoubleCertificate {
201 inner_qc: make_qc(b1.hash, 1),
202 outer_qc: make_qc(b1.hash, 1),
203 };
204
205 let mut last = Height::GENESIS;
206 let result = try_commit(&dc, &store, &app, &mut last, &epoch).unwrap();
207 assert_eq!(result.committed_blocks.len(), 1);
208 assert_eq!(result.committed_blocks[0].height, Height(1));
209 assert_eq!(last, Height(1));
210 assert!(result.pending_epoch.is_none());
211 }
212
213 #[test]
214 fn test_commit_chain_of_blocks() {
215 let mut store = MemoryBlockStore::new();
216 let app = NoopApplication;
217 let epoch = make_epoch();
218 let b1 = make_block(1, BlockHash::GENESIS);
219 let b2 = make_block(2, b1.hash);
220 let b3 = make_block(3, b2.hash);
221 store.put_block(b1);
222 store.put_block(b2);
223 store.put_block(b3.clone());
224
225 let dc = DoubleCertificate {
226 inner_qc: make_qc(b3.hash, 3),
227 outer_qc: make_qc(b3.hash, 3),
228 };
229
230 let mut last = Height::GENESIS;
231 let result = try_commit(&dc, &store, &app, &mut last, &epoch).unwrap();
232 assert_eq!(result.committed_blocks.len(), 3);
233 assert_eq!(result.committed_blocks[0].height, Height(1));
234 assert_eq!(result.committed_blocks[1].height, Height(2));
235 assert_eq!(result.committed_blocks[2].height, Height(3));
236 assert_eq!(last, Height(3));
237 }
238
239 #[test]
240 fn test_commit_already_committed() {
241 let mut store = MemoryBlockStore::new();
242 let app = NoopApplication;
243 let epoch = make_epoch();
244 let b1 = make_block(1, BlockHash::GENESIS);
245 store.put_block(b1.clone());
246
247 let dc = DoubleCertificate {
248 inner_qc: make_qc(b1.hash, 1),
249 outer_qc: make_qc(b1.hash, 1),
250 };
251
252 let mut last = Height(1);
253 let result = try_commit(&dc, &store, &app, &mut last, &epoch).unwrap();
254 assert!(result.committed_blocks.is_empty());
255 }
256
257 #[test]
258 fn test_commit_partial_chain() {
259 let mut store = MemoryBlockStore::new();
260 let app = NoopApplication;
261 let epoch = make_epoch();
262 let b1 = make_block(1, BlockHash::GENESIS);
263 let b2 = make_block(2, b1.hash);
264 let b3 = make_block(3, b2.hash);
265 store.put_block(b1);
266 store.put_block(b2);
267 store.put_block(b3.clone());
268
269 let dc = DoubleCertificate {
270 inner_qc: make_qc(b3.hash, 3),
271 outer_qc: make_qc(b3.hash, 3),
272 };
273
274 let mut last = Height(1);
275 let result = try_commit(&dc, &store, &app, &mut last, &epoch).unwrap();
276 assert_eq!(result.committed_blocks.len(), 2);
277 assert_eq!(result.committed_blocks[0].height, Height(2));
278 assert_eq!(result.committed_blocks[1].height, Height(3));
279 }
280
281 #[test]
282 fn test_commit_missing_block() {
283 let store = MemoryBlockStore::new();
284 let app = NoopApplication;
285 let epoch = make_epoch();
286 let dc = DoubleCertificate {
287 inner_qc: make_qc(BlockHash([99u8; 32]), 1),
288 outer_qc: make_qc(BlockHash([99u8; 32]), 1),
289 };
290 let mut last = Height::GENESIS;
291 assert!(try_commit(&dc, &store, &app, &mut last, &epoch).is_err());
292 }
293
294 #[test]
295 fn test_decode_payload_empty() {
296 assert!(decode_payload(&[]).is_empty());
297 }
298
299 #[test]
300 fn test_decode_payload_roundtrip() {
301 let mut payload = Vec::new();
303 let tx1 = b"hello";
304 let tx2 = b"world";
305 payload.extend_from_slice(&(tx1.len() as u32).to_le_bytes());
306 payload.extend_from_slice(tx1);
307 payload.extend_from_slice(&(tx2.len() as u32).to_le_bytes());
308 payload.extend_from_slice(tx2);
309
310 let txs = decode_payload(&payload);
311 assert_eq!(txs.len(), 2);
312 assert_eq!(txs[0], b"hello");
313 assert_eq!(txs[1], b"world");
314 }
315}