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