pod_types/ledger/
log.rs

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