Skip to main content

hotmint_light/
lib.rs

1//! Light client verification for Hotmint BFT consensus.
2//!
3//! Verifies block headers using QC signatures without downloading full blocks.
4
5use ruc::*;
6
7use hotmint_crypto::has_quorum;
8use hotmint_types::block::{Block, BlockHash, Height};
9use hotmint_types::certificate::QuorumCertificate;
10use hotmint_types::crypto::Verifier;
11use hotmint_types::validator::{ValidatorId, ValidatorSet};
12use hotmint_types::view::ViewNumber;
13use hotmint_types::vote::{Vote, VoteType};
14
15/// Lightweight version of Block without the payload.
16#[derive(Debug, Clone)]
17pub struct BlockHeader {
18    pub height: Height,
19    pub parent_hash: BlockHash,
20    pub view: ViewNumber,
21    pub proposer: ValidatorId,
22    pub app_hash: BlockHash,
23    pub hash: BlockHash,
24}
25
26impl From<&Block> for BlockHeader {
27    fn from(block: &Block) -> Self {
28        Self {
29            height: block.height,
30            parent_hash: block.parent_hash,
31            view: block.view,
32            proposer: block.proposer,
33            app_hash: block.app_hash,
34            hash: block.hash,
35        }
36    }
37}
38
39/// Light client that verifies block headers against a trusted validator set.
40pub struct LightClient {
41    trusted_validator_set: ValidatorSet,
42    trusted_height: Height,
43    chain_id_hash: [u8; 32],
44}
45
46impl LightClient {
47    /// Create a new light client with a trusted validator set and height.
48    pub fn new(
49        trusted_validator_set: ValidatorSet,
50        trusted_height: Height,
51        chain_id_hash: [u8; 32],
52    ) -> Self {
53        Self {
54            trusted_validator_set,
55            trusted_height,
56            chain_id_hash,
57        }
58    }
59
60    /// Verify a block header against the given QC and the trusted validator set.
61    ///
62    /// Checks:
63    /// 1. QC's block_hash matches the header's hash
64    /// 2. The QC has quorum (>= 2f+1 voting power)
65    /// 3. The QC's aggregate signature is valid against the validator set
66    pub fn verify_header(
67        &self,
68        header: &BlockHeader,
69        qc: &QuorumCertificate,
70        verifier: &dyn Verifier,
71    ) -> Result<()> {
72        // 1. Check QC's block_hash matches the header's hash
73        if qc.block_hash != header.hash {
74            return Err(eg!(
75                "QC block_hash mismatch: expected {}, got {}",
76                header.hash,
77                qc.block_hash
78            ));
79        }
80
81        // 2. Check quorum
82        if !has_quorum(&self.trusted_validator_set, &qc.aggregate_signature) {
83            return Err(eg!("QC does not have quorum"));
84        }
85
86        // 3. Verify aggregate signature
87        let signing_bytes = Vote::signing_bytes(
88            &self.chain_id_hash,
89            qc.epoch,
90            qc.view,
91            &qc.block_hash,
92            VoteType::Vote,
93        );
94        if !verifier.verify_aggregate(
95            &self.trusted_validator_set,
96            &signing_bytes,
97            &qc.aggregate_signature,
98        ) {
99            return Err(eg!("QC aggregate signature verification failed"));
100        }
101
102        Ok(())
103    }
104
105    /// Update the trusted validator set after an epoch transition.
106    pub fn update_validator_set(&mut self, new_vs: ValidatorSet, new_height: Height) {
107        self.trusted_validator_set = new_vs;
108        self.trusted_height = new_height;
109    }
110
111    /// Return the current trusted height.
112    pub fn trusted_height(&self) -> Height {
113        self.trusted_height
114    }
115
116    /// Return a reference to the current trusted validator set.
117    pub fn trusted_validator_set(&self) -> &ValidatorSet {
118        &self.trusted_validator_set
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use hotmint_crypto::Ed25519Signer;
126    use hotmint_crypto::Ed25519Verifier;
127    use hotmint_crypto::aggregate::aggregate_votes;
128    use hotmint_types::crypto::Signer;
129    use hotmint_types::epoch::EpochNumber;
130    use hotmint_types::validator::ValidatorInfo;
131
132    const TEST_CHAIN: [u8; 32] = [0u8; 32];
133
134    fn make_env() -> (ValidatorSet, Vec<Ed25519Signer>) {
135        let signers: Vec<Ed25519Signer> = (0..4)
136            .map(|i| Ed25519Signer::generate(ValidatorId(i)))
137            .collect();
138        let infos: Vec<ValidatorInfo> = signers
139            .iter()
140            .map(|s| ValidatorInfo {
141                id: s.validator_id(),
142                public_key: s.public_key(),
143                power: 1,
144            })
145            .collect();
146        (ValidatorSet::new(infos), signers)
147    }
148
149    fn make_header(height: u64, hash: BlockHash) -> BlockHeader {
150        BlockHeader {
151            height: Height(height),
152            parent_hash: BlockHash::GENESIS,
153            view: ViewNumber(height),
154            proposer: ValidatorId(0),
155            app_hash: BlockHash::GENESIS,
156            hash,
157        }
158    }
159
160    fn make_qc(
161        signers: &[Ed25519Signer],
162        vs: &ValidatorSet,
163        block_hash: BlockHash,
164        view: ViewNumber,
165        count: usize,
166    ) -> QuorumCertificate {
167        let epoch = EpochNumber(0);
168        let votes: Vec<hotmint_types::vote::Vote> = signers
169            .iter()
170            .take(count)
171            .map(|s| {
172                let bytes =
173                    Vote::signing_bytes(&TEST_CHAIN, epoch, view, &block_hash, VoteType::Vote);
174                hotmint_types::vote::Vote {
175                    block_hash,
176                    view,
177                    validator: s.validator_id(),
178                    signature: s.sign(&bytes),
179                    vote_type: VoteType::Vote,
180                    extension: None,
181                }
182            })
183            .collect();
184        let agg = aggregate_votes(vs, &votes).unwrap();
185        QuorumCertificate {
186            block_hash,
187            view,
188            aggregate_signature: agg,
189            epoch,
190        }
191    }
192
193    #[test]
194    fn test_valid_qc_passes_verification() {
195        let (vs, signers) = make_env();
196        let hash = BlockHash([1u8; 32]);
197        let header = make_header(1, hash);
198        let qc = make_qc(&signers, &vs, hash, ViewNumber(1), 3);
199        let verifier = Ed25519Verifier;
200        let client = LightClient::new(vs, Height(0), TEST_CHAIN);
201
202        assert!(client.verify_header(&header, &qc, &verifier).is_ok());
203    }
204
205    #[test]
206    fn test_wrong_block_hash_fails() {
207        let (vs, signers) = make_env();
208        let hash = BlockHash([1u8; 32]);
209        let wrong_hash = BlockHash([2u8; 32]);
210        let header = make_header(1, hash);
211        // QC signs wrong_hash, but header has hash
212        let qc = make_qc(&signers, &vs, wrong_hash, ViewNumber(1), 3);
213        let verifier = Ed25519Verifier;
214        let client = LightClient::new(vs, Height(0), TEST_CHAIN);
215
216        let err = client.verify_header(&header, &qc, &verifier);
217        assert!(err.is_err());
218        assert!(
219            err.unwrap_err().to_string().contains("block_hash mismatch"),
220            "expected block_hash mismatch error"
221        );
222    }
223
224    #[test]
225    fn test_no_quorum_fails() {
226        let (vs, signers) = make_env();
227        let hash = BlockHash([1u8; 32]);
228        let header = make_header(1, hash);
229        // Only 2 out of 4 validators sign — below quorum threshold of 3
230        let qc = make_qc(&signers, &vs, hash, ViewNumber(1), 2);
231        let verifier = Ed25519Verifier;
232        let client = LightClient::new(vs, Height(0), TEST_CHAIN);
233
234        let err = client.verify_header(&header, &qc, &verifier);
235        assert!(err.is_err());
236        assert!(
237            err.unwrap_err().to_string().contains("quorum"),
238            "expected quorum error"
239        );
240    }
241
242    #[test]
243    fn test_update_validator_set() {
244        let (vs, _signers) = make_env();
245        let mut client = LightClient::new(vs.clone(), Height(0), TEST_CHAIN);
246        assert_eq!(client.trusted_height(), Height(0));
247
248        let new_signers: Vec<Ed25519Signer> = (10..14)
249            .map(|i| Ed25519Signer::generate(ValidatorId(i)))
250            .collect();
251        let new_infos: Vec<ValidatorInfo> = new_signers
252            .iter()
253            .map(|s| ValidatorInfo {
254                id: s.validator_id(),
255                public_key: s.public_key(),
256                power: 1,
257            })
258            .collect();
259        let new_vs = ValidatorSet::new(new_infos);
260
261        client.update_validator_set(new_vs, Height(100));
262        assert_eq!(client.trusted_height(), Height(100));
263        assert_eq!(client.trusted_validator_set().validator_count(), 4);
264    }
265}