|
5 | 5 | //! Provides deterministic verification of proof bundles against F3 certificates. |
6 | 6 | //! Used by validators during block attestation to verify parent finality proofs. |
7 | 7 |
|
8 | | -use crate::types::SerializableF3Certificate; |
9 | | -use anyhow::{Context, Result}; |
10 | | -use proofs::proofs::common::bundle::{ProofBlock, UnifiedProofBundle}; |
11 | | -use proofs::proofs::storage::{bundle::StorageProof, verifier::verify_storage_proof}; |
12 | | -use tracing::debug; |
13 | | - |
14 | | -/// Verify a unified proof bundle against a certificate |
15 | | -/// |
16 | | -/// This performs deterministic verification of: |
17 | | -/// - Storage proofs (contract state at parent height) |
18 | | -/// - Event proofs (emitted events at parent height) |
19 | | -/// |
20 | | -/// # Arguments |
21 | | -/// * `bundle` - The proof bundle to verify |
22 | | -/// * `certificate` - The certificate containing finalized epochs |
23 | | -/// |
24 | | -/// # Returns |
25 | | -/// `Ok(())` if all proofs are valid, `Err` otherwise |
26 | | -/// |
27 | | -/// # Usage in Block Attestation |
28 | | -/// |
29 | | -/// ```ignore |
30 | | -/// // When validator receives block with parent finality data |
31 | | -/// if cache.contains(cert.instance_id) { |
32 | | -/// // Already validated - just verify proofs |
33 | | -/// let cached = cache.get(cert.instance_id).unwrap(); |
34 | | -/// verify_proof_bundle(&cached.proof_bundle, &cached.certificate)?; |
35 | | -/// } else { |
36 | | -/// // Not cached - need full crypto validation first |
37 | | -/// let validated = f3_client.fetch_and_validate(cert.instance_id).await?; |
38 | | -/// let serializable_cert = SerializableF3Certificate::from(&certificate); |
39 | | -/// verify_proof_bundle(&proof_bundle, &serializable_cert)?; |
40 | | -/// } |
41 | | -/// ``` |
42 | | -pub fn verify_proof_bundle( |
43 | | - bundle: &UnifiedProofBundle, |
44 | | - certificate: &SerializableF3Certificate, |
45 | | -) -> Result<()> { |
46 | | - debug!( |
47 | | - gpbft_instance = certificate.gpbft_instance, |
48 | | - storage_proofs = bundle.storage_proofs.len(), |
49 | | - event_proofs = bundle.event_proofs.len(), |
50 | | - witness_blocks = bundle.blocks.len(), |
51 | | - "Verifying proof bundle" |
52 | | - ); |
53 | | - |
54 | | - // Verify all storage proofs |
55 | | - for (idx, storage_proof) in bundle.storage_proofs.iter().enumerate() { |
56 | | - verify_storage_proof_internal(storage_proof, &bundle.blocks, certificate) |
57 | | - .with_context(|| format!("Storage proof {} failed verification", idx))?; |
58 | | - } |
59 | | - |
60 | | - // Event proof verification uses a bundle-level API |
61 | | - // For now, we verify that the bundle structure is valid |
62 | | - // Full event proof verification will be added when the proofs library API is finalized |
63 | | - if !bundle.event_proofs.is_empty() { |
64 | | - debug!( |
65 | | - event_proofs = bundle.event_proofs.len(), |
66 | | - "Event proofs present (verification to be implemented with proofs library API)" |
67 | | - ); |
68 | | - } |
69 | | - |
70 | | - debug!( |
71 | | - gpbft_instance = certificate.gpbft_instance, |
72 | | - "Proof bundle verified successfully" |
73 | | - ); |
74 | | - |
75 | | - Ok(()) |
| 8 | +use crate::assembler::{NEW_POWER_CHANGE_REQUEST_SIGNATURE, NEW_TOPDOWN_MESSAGE_SIGNATURE}; |
| 9 | +use anyhow::Result; |
| 10 | +use cid::Cid; |
| 11 | +use filecoin_f3_certs::FinalityCertificate; |
| 12 | +use proofs::proofs::common::bundle::{UnifiedProofBundle, UnifiedVerificationResult}; |
| 13 | +use proofs::proofs::events::bundle::EventProofBundle; |
| 14 | +use proofs::proofs::events::verifier::verify_event_proof; |
| 15 | +use proofs::proofs::storage::verifier::verify_storage_proof; |
| 16 | + |
| 17 | +use proofs::proofs::common::evm::{ascii_to_bytes32, extract_evm_log, hash_event_signature}; |
| 18 | + |
| 19 | +pub struct ProofsVerifier { |
| 20 | + events: Vec<Vec<[u8; 32]>>, |
76 | 21 | } |
77 | 22 |
|
78 | | -/// Verify a single storage proof |
79 | | -/// |
80 | | -/// Uses the proofs library's verify_storage_proof function with the witness blocks. |
81 | | -fn verify_storage_proof_internal( |
82 | | - proof: &StorageProof, |
83 | | - blocks: &[ProofBlock], |
84 | | - certificate: &SerializableF3Certificate, |
85 | | -) -> Result<()> { |
86 | | - // Get finalized epochs from certificate |
87 | | - let finalized_epochs = certificate.finalized_epochs(); |
88 | | - |
89 | | - // Verify the proof's child epoch is in the certificate's finalized epochs |
90 | | - let child_epoch = proof.child_epoch; |
91 | | - if !finalized_epochs.contains(&child_epoch) { |
92 | | - anyhow::bail!( |
93 | | - "Storage proof child epoch {} not in certificate's finalized epochs", |
94 | | - child_epoch |
95 | | - ); |
96 | | - } |
97 | | - |
98 | | - // Use the proofs library to verify the storage proof |
99 | | - // The is_trusted_child_header function checks if the child epoch is finalized |
100 | | - let is_trusted = |epoch: i64, _cid: &cid::Cid| -> bool { finalized_epochs.contains(&epoch) }; |
101 | | - |
102 | | - let valid = verify_storage_proof(proof, blocks, &is_trusted) |
103 | | - .context("Storage proof verification failed")?; |
| 23 | +impl ProofsVerifier { |
| 24 | + pub fn new(subnet_id: String) -> Self { |
| 25 | + let events = vec![ |
| 26 | + vec![ |
| 27 | + hash_event_signature(NEW_TOPDOWN_MESSAGE_SIGNATURE), |
| 28 | + ascii_to_bytes32(&subnet_id), |
| 29 | + ], |
| 30 | + vec![hash_event_signature(NEW_POWER_CHANGE_REQUEST_SIGNATURE)], |
| 31 | + ]; |
104 | 32 |
|
105 | | - if !valid { |
106 | | - anyhow::bail!("Storage proof is invalid"); |
| 33 | + Self { events } |
107 | 34 | } |
108 | | - |
109 | | - Ok(()) |
110 | 35 | } |
111 | 36 |
|
112 | | -#[cfg(test)] |
113 | | -mod tests { |
114 | | - use super::*; |
115 | | - use proofs::proofs::common::bundle::UnifiedProofBundle; |
116 | | - |
117 | | - #[test] |
118 | | - fn test_verify_empty_bundle() { |
119 | | - let bundle = UnifiedProofBundle { |
120 | | - storage_proofs: vec![], |
121 | | - event_proofs: vec![], |
122 | | - blocks: vec![], |
| 37 | +impl ProofsVerifier { |
| 38 | + /// Verify a unified proof bundle against a certificate |
| 39 | + /// |
| 40 | + /// This performs deterministic verification of: |
| 41 | + /// - Storage proofs (contract state at parent height) |
| 42 | + /// - Event proofs (emitted events at parent height) |
| 43 | + /// |
| 44 | + /// # Arguments |
| 45 | + /// * `bundle` - The proof bundle to verify |
| 46 | + /// * `certificate` - The certificate containing finalized epochs |
| 47 | + pub fn verify_proof_bundle( |
| 48 | + &self, |
| 49 | + bundle: &UnifiedProofBundle, |
| 50 | + certificate: &FinalityCertificate, |
| 51 | + ) -> Result<UnifiedVerificationResult> { |
| 52 | + let tipset_verifier = |epoch: i64, cid: &Cid| -> bool { |
| 53 | + certificate |
| 54 | + .ec_chain |
| 55 | + .iter() |
| 56 | + .any(|ts| ts.epoch == epoch && ts.key == cid.to_bytes()) |
123 | 57 | }; |
124 | 58 |
|
125 | | - use crate::types::{SerializableECChainEntry, SerializableSupplementalData}; |
| 59 | + // Verify storage proofs |
| 60 | + let mut storage_results = Vec::new(); |
| 61 | + for proof in &bundle.storage_proofs { |
| 62 | + let result = verify_storage_proof(proof, &bundle.blocks, &tipset_verifier)?; |
| 63 | + storage_results.push(result); |
| 64 | + } |
| 65 | + |
| 66 | + // Verify event proofs - need to create an EventProofBundle for the verifier |
| 67 | + let event_bundle = EventProofBundle { |
| 68 | + proofs: bundle.event_proofs.clone(), |
| 69 | + blocks: bundle.blocks.clone(), |
| 70 | + }; |
126 | 71 |
|
127 | | - let cert = SerializableF3Certificate { |
128 | | - gpbft_instance: 1, |
129 | | - ec_chain: vec![ |
130 | | - SerializableECChainEntry { |
131 | | - epoch: 100, |
132 | | - key: vec![], |
133 | | - power_table: "test_cid".to_string(), |
134 | | - commitments: vec![0u8; 32], |
135 | | - }, |
136 | | - SerializableECChainEntry { |
137 | | - epoch: 101, |
138 | | - key: vec![], |
139 | | - power_table: "test_cid".to_string(), |
140 | | - commitments: vec![0u8; 32], |
141 | | - }, |
142 | | - ], |
143 | | - supplemental_data: SerializableSupplementalData { |
144 | | - power_table: "test_cid".to_string(), |
145 | | - commitments: vec![0u8; 32], |
146 | | - }, |
147 | | - signature: vec![], |
148 | | - signers: vec![], |
149 | | - power_table_delta: vec![], |
| 72 | + // TODO Karel - fix the library to take a single CID |
| 73 | + let parent_tipset_verifier = |epoch: i64, cids: &[Cid]| -> bool { |
| 74 | + certificate |
| 75 | + .ec_chain |
| 76 | + .iter() |
| 77 | + .any(|ts| ts.epoch == epoch && ts.key == cids.first().unwrap().to_bytes()) |
150 | 78 | }; |
151 | 79 |
|
152 | | - // Empty bundle should verify successfully |
153 | | - assert!(verify_proof_bundle(&bundle, &cert).is_ok()); |
| 80 | + let event_results = verify_event_proof( |
| 81 | + &event_bundle, |
| 82 | + &parent_tipset_verifier, |
| 83 | + &tipset_verifier, |
| 84 | + Some(&self.create_event_filter()), |
| 85 | + )?; |
| 86 | + |
| 87 | + Ok(UnifiedVerificationResult { |
| 88 | + storage_results, |
| 89 | + event_results, |
| 90 | + }) |
| 91 | + } |
| 92 | + |
| 93 | + fn create_event_filter(&self) -> impl Fn(&fvm_shared::event::ActorEvent) -> bool + '_ { |
| 94 | + |ev: &fvm_shared::event::ActorEvent| -> bool { |
| 95 | + if let Some(log) = extract_evm_log(ev) { |
| 96 | + self.events.iter().any(|expected_topics| { |
| 97 | + log.topics.len() >= expected_topics.len() |
| 98 | + && expected_topics |
| 99 | + .iter() |
| 100 | + .zip(log.topics.iter()) |
| 101 | + .all(|(expected, actual)| expected == actual) |
| 102 | + }) |
| 103 | + } else { |
| 104 | + false |
| 105 | + } |
| 106 | + } |
154 | 107 | } |
155 | 108 | } |
0 commit comments