pod_types/consensus/
attestation.rs

1use alloy_primitives::{Address, Signature};
2use alloy_sol_types::{SolStruct, SolValue};
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    Merkleizable, Receipt, Signed, Timestamp,
7    cryptography::{Hash, hash::Hashable, merkle_tree::MerkleBuilder},
8};
9
10pub type ReceiptAttestation = Attestation<Receipt>;
11
12#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
13#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
14pub struct Indexed<T> {
15    #[serde(rename = "timestamp")]
16    pub index: Timestamp, // TODO: consider sequential numbers
17    pub value: T,
18}
19
20impl<T> Indexed<T> {
21    pub fn new(index: Timestamp, value: T) -> Self {
22        Indexed { index, value }
23    }
24}
25
26impl<T> From<Indexed<Attestation<T>>> for Indexed<HeadlessAttestation> {
27    fn from(value: Indexed<Attestation<T>>) -> Self {
28        Indexed {
29            index: value.index,
30            value: value.value.into(),
31        }
32    }
33}
34
35impl<T: Hashable> Hashable for Indexed<T> {
36    fn hash_custom(&self) -> Hash {
37        alloy_primitives::keccak256(
38            [
39                self.index.as_micros().abi_encode(),
40                self.value.hash_custom().to_vec(),
41            ]
42            .concat(),
43        )
44    }
45}
46
47// An Attestation<T> is T signed by a validator using ECDSA
48#[derive(Copy, Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
49pub struct Attestation<T> {
50    pub public_key: Address,
51    pub signature: Signature,
52    pub attested: T,
53}
54
55#[cfg(feature = "arbitrary")]
56impl<'a, T: arbitrary::Arbitrary<'a> + Hashable> arbitrary::Arbitrary<'a> for Attestation<T> {
57    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
58        use alloy_signer::SignerSync;
59        let signer = alloy_signer_local::PrivateKeySigner::random();
60        let attested = T::arbitrary(u)?;
61
62        Ok(Attestation {
63            public_key: signer.address(),
64            signature: signer.sign_hash_sync(&attested.hash_custom()).unwrap(),
65            attested,
66        })
67    }
68}
69
70impl<T: Hashable> Hashable for Attestation<T> {
71    fn hash_custom(&self) -> Hash {
72        let mut hasher = alloy_primitives::Keccak256::default();
73        hasher.update(self.public_key.as_slice());
74        hasher.update(self.signature.as_bytes());
75        hasher.update(self.attested.hash_custom().as_slice());
76        hasher.finalize()
77    }
78}
79
80impl<T> From<Signed<T>> for Attestation<T> {
81    fn from(signed: Signed<T>) -> Self {
82        Attestation {
83            public_key: signed.signer,
84            signature: signed.signature,
85            attested: signed.signed,
86        }
87    }
88}
89
90#[derive(Clone, Debug, Serialize, Deserialize)]
91#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
92pub struct TimestampedHeadlessAttestation {
93    pub timestamp: Timestamp,
94    pub public_key: Address,
95    pub signature: Signature,
96}
97
98#[derive(Clone, Debug, Serialize, Deserialize)]
99#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
100pub struct HeadlessAttestation {
101    pub public_key: Address,
102    pub signature: Signature,
103}
104
105impl<T> From<Attestation<T>> for HeadlessAttestation {
106    fn from(attestation: Attestation<T>) -> Self {
107        HeadlessAttestation {
108            public_key: attestation.public_key,
109            signature: attestation.signature,
110        }
111    }
112}
113
114impl From<Indexed<HeadlessAttestation>> for TimestampedHeadlessAttestation {
115    fn from(indexed: Indexed<HeadlessAttestation>) -> Self {
116        TimestampedHeadlessAttestation {
117            timestamp: indexed.index,
118            public_key: indexed.value.public_key,
119            signature: indexed.value.signature,
120        }
121    }
122}
123
124impl<T> From<Indexed<Attestation<T>>> for TimestampedHeadlessAttestation {
125    fn from(indexed: Indexed<Attestation<T>>) -> Self {
126        Self {
127            timestamp: indexed.index,
128            public_key: indexed.value.public_key,
129            signature: indexed.value.signature,
130        }
131    }
132}
133
134mod sol {
135    alloy_sol_types::sol! {
136        #[derive(Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
137        #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
138        struct AttestedTx {
139            bytes32 tx_hash;
140            bool success;
141            uint64 committee_epoch;
142        }
143    }
144}
145
146pub use sol::AttestedTx;
147
148impl AttestedTx {
149    pub fn success(tx_hash: Hash, committee_epoch: u64) -> Self {
150        Self {
151            tx_hash,
152            success: true,
153            committee_epoch,
154        }
155    }
156}
157
158impl Hashable for AttestedTx {
159    fn hash_custom(&self) -> Hash {
160        self.eip712_signing_hash(&alloy_sol_types::eip712_domain! {
161            name: "attest_tx",
162            version: "1",
163            chain_id: 0x50d,
164        })
165    }
166}
167
168impl Merkleizable for AttestedTx {
169    fn append_leaves(&self, builder: &mut MerkleBuilder) {
170        builder.add_field("tx_hash", self.tx_hash);
171        builder.add_field("success", self.success.abi_encode().hash_custom());
172        builder.add_field(
173            "committee_epoch",
174            self.committee_epoch.abi_encode().hash_custom(),
175        );
176    }
177}