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};
8use tracing::info;
9
10/// Result of a commit operation
11pub struct CommitResult {
12    pub committed_blocks: Vec<Block>,
13    /// If an epoch transition was triggered by end_block, the new epoch (start_view is placeholder)
14    pub pending_epoch: Option<Epoch>,
15}
16
17/// Decode length-prefixed transactions from a block payload.
18pub 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
33/// Execute the two-chain commit rule:
34/// When we get C_v(C_v(B_k)), commit the inner QC's block and all uncommitted ancestors.
35///
36/// For each committed block, runs the full application lifecycle:
37/// begin_block → deliver_tx (×N) → end_block → on_commit
38pub 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    // Collect all uncommitted ancestors (from highest to lowest)
58    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    // Commit from lowest height to highest
76    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: &current_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                // Placeholder — engine sets the real start_view in advance_view_to
109                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        // Encode: 4-byte LE length prefix + data
271        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}