Skip to main content

hotmint_consensus/
commit.rs

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
10/// Result of a commit operation
11pub struct CommitResult {
12    pub committed_blocks: Vec<Block>,
13    /// The QC that certified the committed block (for sync protocol).
14    pub commit_qc: hotmint_types::QuorumCertificate,
15    /// If an epoch transition was triggered by end_block, the new epoch (start_view is placeholder)
16    pub pending_epoch: Option<Epoch>,
17}
18
19/// Decode length-prefixed transactions from a block payload.
20pub 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
35/// Execute the two-chain commit rule:
36/// When we get C_v(C_v(B_k)), commit the inner QC's block and all uncommitted ancestors.
37///
38/// For each committed block, runs the full application lifecycle:
39/// begin_block → deliver_tx (×N) → end_block → on_commit
40///
41/// # Safety
42/// Caller MUST verify both inner_qc and outer_qc aggregate signatures
43/// and quorum counts before calling this function. This function trusts
44/// the DoubleCertificate completely and performs no cryptographic checks.
45pub 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    // Collect all uncommitted ancestors (from highest to lowest)
66    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    // Commit from lowest height to highest
84    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: &current_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            // The new epoch starts 2 views after the committing block's view.
113            // This deterministic gap gives lagging nodes time to catch up and
114            // ensures all honest nodes apply the epoch at the same view.
115            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        // Encode: 4-byte LE length prefix + data
277        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}