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