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        // A-6: Enforce height monotonicity — reject replayed or older headers.
78        if header.height <= self.trusted_height {
79            return Err(eg!(
80                "header height {} <= trusted height {}",
81                header.height.as_u64(),
82                self.trusted_height.as_u64()
83            ));
84        }
85
86        // 1. Check QC's block_hash matches the header's hash
87        if qc.block_hash != header.hash {
88            return Err(eg!(
89                "QC block_hash mismatch: expected {}, got {}",
90                header.hash,
91                qc.block_hash
92            ));
93        }
94
95        // 2. Check quorum
96        if !has_quorum(&self.trusted_validator_set, &qc.aggregate_signature) {
97            return Err(eg!("QC does not have quorum"));
98        }
99
100        // 3. Verify aggregate signature
101        let signing_bytes = Vote::signing_bytes(
102            &self.chain_id_hash,
103            qc.epoch,
104            qc.view,
105            &qc.block_hash,
106            VoteType::Vote,
107        );
108        if !verifier.verify_aggregate(
109            &self.trusted_validator_set,
110            &signing_bytes,
111            &qc.aggregate_signature,
112        ) {
113            return Err(eg!("QC aggregate signature verification failed"));
114        }
115
116        Ok(())
117    }
118
119    /// Update the trusted validator set after an epoch transition.
120    pub fn update_validator_set(&mut self, new_vs: ValidatorSet, new_height: Height) {
121        self.trusted_validator_set = new_vs;
122        self.trusted_height = new_height;
123    }
124
125    /// Return the current trusted height.
126    pub fn trusted_height(&self) -> Height {
127        self.trusted_height
128    }
129
130    /// Return a reference to the current trusted validator set.
131    pub fn trusted_validator_set(&self) -> &ValidatorSet {
132        &self.trusted_validator_set
133    }
134
135    /// Verify an MPT state proof against a trusted app_hash.
136    ///
137    /// The `app_hash` should come from a verified block header (after
138    /// `verify_header` succeeds). The `proof_bytes` are the serialized
139    /// `MptProof` nodes (as returned by the `query` RPC `proof` field).
140    /// The `expected_key` is the raw key the caller expects the proof to cover.
141    ///
142    /// Returns `Ok(true)` if the proof is valid against the given root.
143    pub fn verify_state_proof(
144        app_hash: &[u8; 32],
145        expected_key: &[u8],
146        proof: &vsdb::MptProof,
147    ) -> ruc::Result<bool> {
148        vsdb::MptCalc::verify_proof(app_hash, expected_key, proof)
149            .map_err(|e| ruc::eg!(format!("MPT proof verification failed: {e}")))
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use hotmint_crypto::Ed25519Signer;
157    use hotmint_crypto::Ed25519Verifier;
158    use hotmint_crypto::aggregate::aggregate_votes;
159    use hotmint_types::crypto::Signer;
160    use hotmint_types::epoch::EpochNumber;
161    use hotmint_types::validator::ValidatorInfo;
162
163    const TEST_CHAIN: [u8; 32] = [0u8; 32];
164
165    fn make_env() -> (ValidatorSet, Vec<Ed25519Signer>) {
166        let signers: Vec<Ed25519Signer> = (0..4)
167            .map(|i| Ed25519Signer::generate(ValidatorId(i)))
168            .collect();
169        let infos: Vec<ValidatorInfo> = signers
170            .iter()
171            .map(|s| ValidatorInfo {
172                id: s.validator_id(),
173                public_key: s.public_key(),
174                power: 1,
175            })
176            .collect();
177        (ValidatorSet::new(infos), signers)
178    }
179
180    fn make_header(height: u64, hash: BlockHash) -> BlockHeader {
181        BlockHeader {
182            height: Height(height),
183            parent_hash: BlockHash::GENESIS,
184            view: ViewNumber(height),
185            proposer: ValidatorId(0),
186            timestamp: 0,
187            app_hash: BlockHash::GENESIS,
188            hash,
189        }
190    }
191
192    fn make_qc(
193        signers: &[Ed25519Signer],
194        vs: &ValidatorSet,
195        block_hash: BlockHash,
196        view: ViewNumber,
197        count: usize,
198    ) -> QuorumCertificate {
199        let epoch = EpochNumber(0);
200        let votes: Vec<hotmint_types::vote::Vote> = signers
201            .iter()
202            .take(count)
203            .map(|s| {
204                let bytes =
205                    Vote::signing_bytes(&TEST_CHAIN, epoch, view, &block_hash, VoteType::Vote);
206                hotmint_types::vote::Vote {
207                    block_hash,
208                    view,
209                    validator: s.validator_id(),
210                    signature: s.sign(&bytes),
211                    vote_type: VoteType::Vote,
212                    extension: None,
213                }
214            })
215            .collect();
216        let agg = aggregate_votes(vs, &votes).unwrap();
217        QuorumCertificate {
218            block_hash,
219            view,
220            aggregate_signature: agg,
221            epoch,
222        }
223    }
224
225    #[test]
226    fn test_valid_qc_passes_verification() {
227        let (vs, signers) = make_env();
228        let hash = BlockHash([1u8; 32]);
229        let header = make_header(1, hash);
230        let qc = make_qc(&signers, &vs, hash, ViewNumber(1), 3);
231        let verifier = Ed25519Verifier;
232        let client = LightClient::new(vs, Height(0), TEST_CHAIN);
233
234        assert!(client.verify_header(&header, &qc, &verifier).is_ok());
235    }
236
237    #[test]
238    fn test_wrong_block_hash_fails() {
239        let (vs, signers) = make_env();
240        let hash = BlockHash([1u8; 32]);
241        let wrong_hash = BlockHash([2u8; 32]);
242        let header = make_header(1, hash);
243        // QC signs wrong_hash, but header has hash
244        let qc = make_qc(&signers, &vs, wrong_hash, ViewNumber(1), 3);
245        let verifier = Ed25519Verifier;
246        let client = LightClient::new(vs, Height(0), TEST_CHAIN);
247
248        let err = client.verify_header(&header, &qc, &verifier);
249        assert!(err.is_err());
250        assert!(
251            err.unwrap_err().to_string().contains("block_hash mismatch"),
252            "expected block_hash mismatch error"
253        );
254    }
255
256    #[test]
257    fn test_no_quorum_fails() {
258        let (vs, signers) = make_env();
259        let hash = BlockHash([1u8; 32]);
260        let header = make_header(1, hash);
261        // Only 2 out of 4 validators sign — below quorum threshold of 3
262        let qc = make_qc(&signers, &vs, hash, ViewNumber(1), 2);
263        let verifier = Ed25519Verifier;
264        let client = LightClient::new(vs, Height(0), TEST_CHAIN);
265
266        let err = client.verify_header(&header, &qc, &verifier);
267        assert!(err.is_err());
268        assert!(
269            err.unwrap_err().to_string().contains("quorum"),
270            "expected quorum error"
271        );
272    }
273
274    #[test]
275    fn test_update_validator_set() {
276        let (vs, _signers) = make_env();
277        let mut client = LightClient::new(vs.clone(), Height(0), TEST_CHAIN);
278        assert_eq!(client.trusted_height(), Height(0));
279
280        let new_signers: Vec<Ed25519Signer> = (10..14)
281            .map(|i| Ed25519Signer::generate(ValidatorId(i)))
282            .collect();
283        let new_infos: Vec<ValidatorInfo> = new_signers
284            .iter()
285            .map(|s| ValidatorInfo {
286                id: s.validator_id(),
287                public_key: s.public_key(),
288                power: 1,
289            })
290            .collect();
291        let new_vs = ValidatorSet::new(new_infos);
292
293        client.update_validator_set(new_vs, Height(100));
294        assert_eq!(client.trusted_height(), Height(100));
295        assert_eq!(client.trusted_validator_set().validator_count(), 4);
296    }
297}