pod_types/ledger/
log.rs

1use std::ops::Deref;
2
3pub use alloy_primitives::{Log, LogData};
4use alloy_rpc_types::Log as RPCLog;
5use alloy_sol_types::SolValue;
6use serde::{Deserialize, Serialize};
7
8use crate::{
9    Certificate, Committee, Signed, Timestamp, Transaction,
10    consensus::{attestation::TimestampedHeadlessAttestation, committee::CommitteeError},
11    cryptography::{
12        Hash, MerkleMultiProof, Merkleizable,
13        hash::Hashable,
14        merkle_tree::{MerkleBuilder, MerkleProof, StandardMerkleTree, index_prefix},
15    },
16    metadata::{MetadataWrappedItem, PodLogMetadata},
17};
18
19use super::Receipt;
20
21pub fn to_rpc_format(inner_log: Log, tx_hash: Hash) -> RPCLog {
22    RPCLog {
23        inner: inner_log,
24        block_hash: Some(Hash::default()),
25        block_number: Some(1),
26        block_timestamp: None,
27        transaction_hash: Some(tx_hash),
28        transaction_index: Some(0),
29        log_index: Some(0),
30        removed: false,
31    }
32}
33
34#[derive(Debug, Serialize, Deserialize, Clone)]
35#[serde(rename_all = "camelCase")]
36#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
37pub struct Event {
38    pub log: Log,
39    pub log_index: u64,
40    pub tx: Signed<Transaction>,
41    pub attestations: Vec<TimestampedHeadlessAttestation>,
42    pub receipt: Receipt,
43}
44
45// Implement Hashing for Ethereum LogData
46// Hash is collected from forming a Merkle tree by adding hashes of N topics as leafs, and appending the hash of the data at the end
47impl Merkleizable for LogData {
48    fn append_leaves(&self, builder: &mut MerkleBuilder) {
49        builder.add_slice("topics", self.topics());
50        builder.add_field("data", self.data.hash_custom());
51    }
52}
53
54impl Merkleizable for Log {
55    fn append_leaves(&self, builder: &mut MerkleBuilder) {
56        builder.add_field("address", self.address.hash_custom());
57        builder.add_merkleizable("data", &self.data);
58    }
59}
60
61impl Hashable for Log {
62    fn hash_custom(&self) -> Hash {
63        (self.address, self.data.topics(), &self.data.data)
64            .abi_encode()
65            .hash_custom()
66    }
67}
68
69pub type VerifiableLog = MetadataWrappedItem<RPCLog, PodLogMetadata>;
70
71impl VerifiableLog {
72    // returns none if RPC did not provide `log_index` or if the provided `log_index` does not correspond to any log on the receipt
73    // result can be proven with MerkleTree::verify_multi_proof(leaves, proof)
74    pub fn generate_multi_proof(&self) -> Option<(Vec<Hash>, MerkleMultiProof)> {
75        self.inner.log_index.and_then(|i| {
76            self.pod_metadata
77                .receipt
78                .generate_multi_proof_for_log(i.try_into().unwrap())
79        })
80    }
81    pub fn verify(&self, committee: &Committee) -> Result<(), CommitteeError> {
82        committee.verify_certificate(&Certificate {
83            signatures: self
84                .pod_metadata
85                .attestations
86                .iter()
87                .map(|att| att.signature)
88                .collect(),
89            certified: self.pod_metadata.receipt.clone(),
90        })
91    }
92    pub fn confirmation_time(&self) -> Timestamp {
93        let num_attestations = self.pod_metadata.attestations.len();
94        self.pod_metadata.attestations[num_attestations / 2].timestamp
95    }
96
97    pub fn generate_proof(&self) -> Option<MerkleProof> {
98        self.inner.log_index.and_then(|i| {
99            self.pod_metadata
100                .receipt
101                .generate_proof_for_log_hash(i.try_into().unwrap())
102        })
103    }
104
105    pub fn get_leaf(&self) -> Hash {
106        let log_index = self.inner.log_index.unwrap_or(0).try_into().unwrap();
107        StandardMerkleTree::hash_leaf(
108            index_prefix("log_hashes", log_index),
109            self.inner.inner.hash_custom(),
110        )
111    }
112
113    pub fn aggregate_signatures(&self) -> Vec<u8> {
114        self.pod_metadata
115            .attestations
116            .iter()
117            .map(|a| a.signature.as_bytes())
118            .fold(Vec::new(), |mut acc, sig| {
119                acc.extend_from_slice(&sig);
120                acc
121            })
122    }
123
124    pub fn verify_proof(&self, receipt_root: Hash, proof: MerkleProof) -> bool {
125        let leaf = self.get_leaf();
126        StandardMerkleTree::verify_proof(receipt_root, leaf, proof)
127    }
128}
129
130impl Deref for VerifiableLog {
131    type Target = alloy_rpc_types::Log;
132
133    fn deref(&self) -> &alloy_rpc_types::Log {
134        &self.inner
135    }
136}
137
138#[cfg(test)]
139mod test {
140    use super::*;
141    use alloy_primitives::{Address, Log, LogData, TxKind, U256};
142    use alloy_signer_local::PrivateKeySigner;
143
144    use crate::{Hashable, Merkleizable, Transaction};
145
146    #[tokio::test]
147    async fn test_verifiable_log_hash_proof() {
148        let log = Log {
149            address: "0x217f5658c6ecc27d439922263ad9bb8e992e0373"
150                .parse()
151                .unwrap(),
152            data: LogData::new_unchecked(
153                vec![
154                    "71a5674c44b823bc0df08201dfeb2e8bdf698cd684fd2bbaa79adcf2c99fc186"
155                        .parse()
156                        .unwrap(),
157                    "0000000000000000000000000000000000000000000000000000000067dc55a9"
158                        .parse()
159                        .unwrap(),
160                    "00000000000000000000000013791790bef192d14712d627f13a55c4abee52a4"
161                        .parse()
162                        .unwrap(),
163                    "00000000000000000000000000000000000000000000000000000000cfb8ab4d"
164                        .parse()
165                        .unwrap(),
166                ],
167                "0000000000000000000000000000000000000000000000000de0b6b3a7640000"
168                    .parse()
169                    .unwrap(),
170            ),
171        };
172
173        let to: Address = "0x217f5658c6ecc27d439922263ad9bb8e992e0373"
174            .parse()
175            .unwrap();
176        let transaction = Transaction {
177            chain_id: 0x50d,
178            to: TxKind::Call(to.clone()),
179            nonce: 0,
180            gas_limit: 22048,
181            max_fee_per_gas: 1000000000,
182            max_priority_fee_per_gas: 1000000000,
183            access_list: Default::default(),
184            value: U256::ZERO,
185            input: vec![
186                133, 44, 166, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
187                0, 0, 0, 0, 0, 0, 103, 220, 85, 169, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
188                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 207, 184, 171, 77, 0, 0, 0, 0, 0, 0, 0, 0,
189                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 224, 182, 179, 167, 100, 0, 0,
190                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
191                0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
192                0, 0, 0, 0, 0, 0, 0, 0, 2, 18, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
193                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
194            ]
195            .into(),
196        };
197        let signer = PrivateKeySigner::random();
198
199        let logs = vec![log.clone()];
200        let logs_tree = logs.to_merkle_tree();
201        let logs_root = logs_tree.root();
202
203        let rpc_log = RPCLog {
204            inner: log.clone(),
205            block_hash: Some(Hash::default()),
206            block_number: Some(0),
207            block_timestamp: Some(1742493092),
208            transaction_hash: Some(transaction.hash_custom()),
209            transaction_index: Some(0),
210            log_index: Some(0),
211            removed: false,
212        };
213
214        let verifiable_log = VerifiableLog {
215            inner: rpc_log,
216            pod_metadata: PodLogMetadata {
217                attestations: vec![],
218                receipt: Receipt {
219                    status: true,
220                    actual_gas_used: 21784,
221                    max_fee_per_gas: transaction.max_fee_per_gas,
222                    logs: logs.clone(),
223                    logs_root,
224                    tx_hash: transaction.hash_custom(),
225                    signer: signer.address(),
226                    to: Some(to),
227                    contract_address: None,
228                },
229            },
230        };
231
232        let proof = verifiable_log.generate_proof().unwrap();
233        let receipt_root = verifiable_log
234            .pod_metadata
235            .receipt
236            .to_merkle_tree()
237            .hash_custom();
238
239        assert!(verifiable_log.verify_proof(receipt_root, proof));
240        assert_eq!(verifiable_log.inner.log_index, Some(0));
241    }
242}