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