Skip to main content

hyli_model/node/
mempool.rs

1use borsh::{BorshDeserialize, BorshSerialize};
2use hyli_net_traits::TcpMessageLabel;
3use serde::{Deserialize, Serialize};
4use sha3::{Digest, Sha3_256};
5use std::{fmt::Display, sync::RwLock};
6
7use crate::*;
8
9#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize, TcpMessageLabel)]
10pub enum MempoolStatusEvent {
11    WaitingDissemination {
12        parent_data_proposal_hash: DataProposalHash,
13        txs: Vec<Transaction>,
14    },
15    DataProposalCreated {
16        parent_data_proposal_hash: DataProposalHash,
17        data_proposal_hash: DataProposalHash,
18        txs_metadatas: Vec<TransactionMetadata>,
19    },
20}
21
22#[derive(Debug, Clone, Deserialize, Serialize)]
23pub enum MempoolBlockEvent {
24    BuiltSignedBlock(SignedBlock),
25    StartedBuildingBlocks(BlockHeight),
26}
27
28#[derive(
29    Debug,
30    Clone,
31    Serialize,
32    Deserialize,
33    BorshSerialize,
34    BorshDeserialize,
35    PartialEq,
36    Eq,
37    Hash,
38    Ord,
39    PartialOrd,
40)]
41#[cfg_attr(feature = "full", derive(utoipa::ToSchema))]
42pub enum DataProposalParent {
43    LaneRoot(LaneId),
44    DP(DataProposalHash),
45}
46
47impl DataProposalParent {
48    pub fn dp_hash(&self) -> Option<&DataProposalHash> {
49        match self {
50            DataProposalParent::DP(hash) => Some(hash),
51            DataProposalParent::LaneRoot(_) => None,
52        }
53    }
54
55    pub fn lane_id(&self) -> Option<&LaneId> {
56        match self {
57            DataProposalParent::LaneRoot(lane_id) => Some(lane_id),
58            DataProposalParent::DP(_) => None,
59        }
60    }
61
62    pub fn is_lane_root(&self) -> bool {
63        matches!(self, DataProposalParent::LaneRoot(_))
64    }
65
66    pub fn as_tx_parent_hash(&self) -> DataProposalHash {
67        match self {
68            DataProposalParent::LaneRoot(lane_id) => DataProposalHash(lane_id.to_bytes()),
69            DataProposalParent::DP(hash) => hash.clone(),
70        }
71    }
72}
73
74#[derive(Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, TcpMessageLabel)]
75#[readonly::make]
76pub struct DataProposal {
77    pub parent_data_proposal_hash: DataProposalParent,
78    pub txs: Vec<Transaction>,
79    /// Internal cache of the hash of the transaction
80    #[borsh(skip)]
81    hash_cache: RwLock<Option<DataProposalHash>>,
82}
83
84impl DataProposal {
85    pub fn new(parent_data_proposal_hash: DataProposalHash, txs: Vec<Transaction>) -> Self {
86        Self {
87            parent_data_proposal_hash: DataProposalParent::DP(parent_data_proposal_hash),
88            txs,
89            hash_cache: RwLock::new(None),
90        }
91    }
92
93    pub fn new_root(lane_id: LaneId, txs: Vec<Transaction>) -> Self {
94        Self {
95            parent_data_proposal_hash: DataProposalParent::LaneRoot(lane_id),
96            txs,
97            hash_cache: RwLock::new(None),
98        }
99    }
100
101    pub fn remove_proofs(&mut self) {
102        self.txs.iter_mut().for_each(|tx| {
103            match &mut tx.transaction_data {
104                TransactionData::VerifiedProof(proof_tx) => {
105                    proof_tx.proof = None;
106                }
107                TransactionData::Proof(_) => {
108                    // This can never happen.
109                    // A DataProposal that has been processed has turned all TransactionData::Proof into TransactionData::VerifiedProof
110                    unreachable!();
111                }
112                TransactionData::Blob(_) => {}
113            }
114        });
115    }
116
117    pub fn take_proofs(&mut self) -> Vec<(u64, ProofData)> {
118        self.txs
119            .iter_mut()
120            .enumerate()
121            .filter_map(|(tx_idx, tx)| {
122                match &mut tx.transaction_data {
123                    TransactionData::VerifiedProof(proof_tx) => {
124                        proof_tx.proof.take().map(|proof| (tx_idx as u64, proof))
125                    }
126                    TransactionData::Proof(_) => {
127                        // This can never happen.
128                        // A DataProposal that has been processed has turned all TransactionData::Proof into TransactionData::VerifiedProof
129                        unreachable!();
130                    }
131                    TransactionData::Blob(_) => None,
132                }
133            })
134            .collect()
135    }
136
137    /// Proofs are kept as a vector because DP transactions are fundamentally ordered.
138    pub fn hydrate_proofs(&mut self, proofs: Vec<(u64, ProofData)>) {
139        for (tx_idx, proof) in proofs {
140            let Ok(tx_idx) = usize::try_from(tx_idx) else {
141                continue;
142            };
143            let Some(tx) = self.txs.get_mut(tx_idx) else {
144                continue;
145            };
146            if let TransactionData::VerifiedProof(ref mut vpt) = tx.transaction_data {
147                if vpt.proof.is_none() {
148                    vpt.proof = Some(proof);
149                }
150            }
151        }
152    }
153
154    /// This is used to set the hash of the DataProposal when we can trust we know it
155    /// (specifically - deserializating from local storage)
156    /// # Safety
157    /// Marked unsafe so you think twice before using it, but this is safe rust-wise.
158    pub unsafe fn unsafe_set_hash(&mut self, hash: &DataProposalHash) {
159        self.hash_cache.write().unwrap().replace(hash.clone());
160    }
161}
162
163impl Clone for DataProposal {
164    fn clone(&self) -> Self {
165        DataProposal {
166            parent_data_proposal_hash: self.parent_data_proposal_hash.clone(),
167            txs: self.txs.clone(),
168            hash_cache: RwLock::new(self.hash_cache.read().unwrap().clone()),
169        }
170    }
171}
172
173impl PartialEq for DataProposal {
174    fn eq(&self, other: &Self) -> bool {
175        self.hashed() == other.hashed()
176    }
177}
178
179impl Eq for DataProposal {}
180
181impl DataSized for DataProposal {
182    fn estimate_size(&self) -> usize {
183        self.txs.iter().map(|tx| tx.estimate_size()).sum()
184    }
185}
186
187#[derive(
188    Default,
189    Serialize,
190    Deserialize,
191    Debug,
192    Clone,
193    PartialEq,
194    Eq,
195    Hash,
196    Ord,
197    PartialOrd,
198    BorshDeserialize,
199    BorshSerialize,
200)]
201#[cfg_attr(feature = "full", derive(utoipa::ToSchema))]
202pub struct TxId(pub DataProposalHash, pub TxHash);
203
204#[derive(
205    Clone,
206    Default,
207    Serialize,
208    Deserialize,
209    BorshSerialize,
210    BorshDeserialize,
211    PartialEq,
212    Eq,
213    Hash,
214    Ord,
215    PartialOrd,
216)]
217#[cfg_attr(feature = "full", derive(utoipa::ToSchema))]
218pub struct DataProposalHash(#[serde(with = "crate::utils::hex_bytes")] pub Vec<u8>);
219
220impl From<Vec<u8>> for DataProposalHash {
221    fn from(v: Vec<u8>) -> Self {
222        DataProposalHash(v)
223    }
224}
225impl From<&[u8]> for DataProposalHash {
226    fn from(v: &[u8]) -> Self {
227        DataProposalHash(v.to_vec())
228    }
229}
230impl<const N: usize> From<&[u8; N]> for DataProposalHash {
231    fn from(v: &[u8; N]) -> Self {
232        DataProposalHash(v.to_vec())
233    }
234}
235impl DataProposalHash {
236    pub fn from_hex(s: &str) -> Result<Self, hex::FromHexError> {
237        crate::utils::decode_hex_string_checked(s).map(DataProposalHash)
238    }
239}
240
241impl std::fmt::Debug for DataProposalHash {
242    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243        write!(f, "DataProposalHash({})", hex::encode(&self.0))
244    }
245}
246
247impl Hashed<DataProposalHash> for DataProposal {
248    fn hashed(&self) -> DataProposalHash {
249        if let Some(hash) = self.hash_cache.read().unwrap().as_ref() {
250            return hash.clone();
251        }
252        let mut hasher = Sha3_256::new();
253        match &self.parent_data_proposal_hash {
254            DataProposalParent::LaneRoot(lane_id) => {
255                hasher.update(lane_id.to_string().as_bytes());
256            }
257            DataProposalParent::DP(parent_data_proposal_hash) => {
258                hasher.update(&parent_data_proposal_hash.0);
259            }
260        }
261        for tx in self.txs.iter() {
262            hasher.update(&tx.hashed().0);
263        }
264        let hash = DataProposalHash(hasher.finalize().to_vec());
265        *self.hash_cache.write().unwrap() = Some(hash.clone());
266        hash
267    }
268}
269
270// Warning: hashing DPs can be slow, so use with care
271impl std::hash::Hash for DataProposal {
272    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
273        self.hashed().hash(state);
274    }
275}
276
277impl Display for DataProposalHash {
278    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279        write!(f, "{}", hex::encode(&self.0))
280    }
281}
282impl Display for DataProposal {
283    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284        write!(f, "{}", self.hashed())
285    }
286}
287
288pub type PoDA = AggregateSignature;
289pub type Cut = Vec<(LaneId, DataProposalHash, LaneBytesSize, PoDA)>;
290
291impl Display for TxId {
292    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293        write!(f, "{}/{}", self.0, self.1)
294    }
295}
296
297// Can't impl-display, but we can still make it a little nicer by default
298pub struct CutDisplay<'a>(pub &'a Cut);
299impl Display for CutDisplay<'_> {
300    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301        let mut cut_str = String::new();
302        for (lane_id, hash, size, _) in self.0.iter() {
303            cut_str.push_str(&format!("{lane_id}:{hash}({size}), "));
304        }
305        write!(f, "{}", cut_str.trim_end())
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    #[test]
314    fn data_proposal_hash_from_hex_str_roundtrip() {
315        let hex_str = "746573745f6470";
316        let hash = DataProposalHash::from_hex(hex_str).expect("data proposal hash hex");
317        assert_eq!(hash.0, b"test_dp".to_vec());
318        assert_eq!(format!("{hash}"), hex_str);
319        let json = serde_json::to_string(&hash).expect("serialize data proposal hash");
320        assert_eq!(json, "\"746573745f6470\"");
321        let decoded: DataProposalHash =
322            serde_json::from_str(&json).expect("deserialize data proposal hash");
323        assert_eq!(decoded, hash);
324    }
325}