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