Skip to main content

hotmint_consensus/
commit.rs

1use ruc::*;
2
3use crate::application::Application;
4use crate::store::BlockStore;
5use hotmint_types::{Block, BlockHash, DoubleCertificate, Height};
6use tracing::info;
7
8/// Execute the two-chain commit rule:
9/// When we get C_v(C_v(B_k)), commit the inner QC's block and all uncommitted ancestors
10pub fn try_commit(
11    double_cert: &DoubleCertificate,
12    store: &dyn BlockStore,
13    app: &dyn Application,
14    last_committed_height: &mut Height,
15) -> Result<Vec<Block>> {
16    let commit_hash = double_cert.inner_qc.block_hash;
17    let commit_block = store
18        .get_block(&commit_hash)
19        .c(d!("block to commit not found"))?;
20
21    if commit_block.height <= *last_committed_height {
22        return Ok(vec![]);
23    }
24
25    // Collect all uncommitted ancestors (from highest to lowest)
26    let mut to_commit = Vec::new();
27    let mut current = commit_block;
28    loop {
29        if current.height <= *last_committed_height {
30            break;
31        }
32        let parent_hash = current.parent_hash;
33        to_commit.push(current);
34        if parent_hash == BlockHash::GENESIS {
35            break;
36        }
37        match store.get_block(&parent_hash) {
38            Some(parent) => current = parent,
39            None => break,
40        }
41    }
42
43    // Commit from lowest height to highest
44    to_commit.reverse();
45
46    for block in &to_commit {
47        info!(height = block.height.as_u64(), hash = %block.hash, "committing block");
48        app.on_commit(block).c(d!("application commit failed"))?;
49        *last_committed_height = block.height;
50    }
51
52    Ok(to_commit)
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use crate::application::NoopApplication;
59    use crate::store::MemoryBlockStore;
60    use hotmint_types::{AggregateSignature, QuorumCertificate, ValidatorId, ViewNumber};
61
62    fn make_block(height: u64, parent: BlockHash) -> Block {
63        let hash = BlockHash([height as u8; 32]);
64        Block {
65            height: Height(height),
66            parent_hash: parent,
67            view: ViewNumber(height),
68            proposer: ValidatorId(0),
69            payload: vec![],
70            hash,
71        }
72    }
73
74    fn make_qc(hash: BlockHash, view: u64) -> QuorumCertificate {
75        QuorumCertificate {
76            block_hash: hash,
77            view: ViewNumber(view),
78            aggregate_signature: AggregateSignature::new(4),
79        }
80    }
81
82    #[test]
83    fn test_commit_single_block() {
84        let mut store = MemoryBlockStore::new();
85        let app = NoopApplication;
86        let b1 = make_block(1, BlockHash::GENESIS);
87        store.put_block(b1.clone());
88
89        let dc = DoubleCertificate {
90            inner_qc: make_qc(b1.hash, 1),
91            outer_qc: make_qc(b1.hash, 1),
92        };
93
94        let mut last = Height::GENESIS;
95        let committed = try_commit(&dc, &store, &app, &mut last).unwrap();
96        assert_eq!(committed.len(), 1);
97        assert_eq!(committed[0].height, Height(1));
98        assert_eq!(last, Height(1));
99    }
100
101    #[test]
102    fn test_commit_chain_of_blocks() {
103        let mut store = MemoryBlockStore::new();
104        let app = NoopApplication;
105        let b1 = make_block(1, BlockHash::GENESIS);
106        let b2 = make_block(2, b1.hash);
107        let b3 = make_block(3, b2.hash);
108        store.put_block(b1);
109        store.put_block(b2);
110        store.put_block(b3.clone());
111
112        let dc = DoubleCertificate {
113            inner_qc: make_qc(b3.hash, 3),
114            outer_qc: make_qc(b3.hash, 3),
115        };
116
117        let mut last = Height::GENESIS;
118        let committed = try_commit(&dc, &store, &app, &mut last).unwrap();
119        assert_eq!(committed.len(), 3);
120        assert_eq!(committed[0].height, Height(1));
121        assert_eq!(committed[1].height, Height(2));
122        assert_eq!(committed[2].height, Height(3));
123        assert_eq!(last, Height(3));
124    }
125
126    #[test]
127    fn test_commit_already_committed() {
128        let mut store = MemoryBlockStore::new();
129        let app = NoopApplication;
130        let b1 = make_block(1, BlockHash::GENESIS);
131        store.put_block(b1.clone());
132
133        let dc = DoubleCertificate {
134            inner_qc: make_qc(b1.hash, 1),
135            outer_qc: make_qc(b1.hash, 1),
136        };
137
138        let mut last = Height(1); // already committed
139        let committed = try_commit(&dc, &store, &app, &mut last).unwrap();
140        assert!(committed.is_empty());
141    }
142
143    #[test]
144    fn test_commit_partial_chain() {
145        let mut store = MemoryBlockStore::new();
146        let app = NoopApplication;
147        let b1 = make_block(1, BlockHash::GENESIS);
148        let b2 = make_block(2, b1.hash);
149        let b3 = make_block(3, b2.hash);
150        store.put_block(b1);
151        store.put_block(b2);
152        store.put_block(b3.clone());
153
154        let dc = DoubleCertificate {
155            inner_qc: make_qc(b3.hash, 3),
156            outer_qc: make_qc(b3.hash, 3),
157        };
158
159        let mut last = Height(1); // b1 already committed
160        let committed = try_commit(&dc, &store, &app, &mut last).unwrap();
161        assert_eq!(committed.len(), 2); // only b2 and b3
162        assert_eq!(committed[0].height, Height(2));
163        assert_eq!(committed[1].height, Height(3));
164    }
165
166    #[test]
167    fn test_commit_missing_block() {
168        let store = MemoryBlockStore::new();
169        let app = NoopApplication;
170        let dc = DoubleCertificate {
171            inner_qc: make_qc(BlockHash([99u8; 32]), 1),
172            outer_qc: make_qc(BlockHash([99u8; 32]), 1),
173        };
174        let mut last = Height::GENESIS;
175        assert!(try_commit(&dc, &store, &app, &mut last).is_err());
176    }
177}