eq_sdk/
types.rs

1use std::{error::Error, fmt::Display, str::FromStr};
2
3use base64::Engine;
4use celestia_types::{blob::Commitment, block::Height as BlockHeight, nmt::Namespace};
5use serde::{Deserialize, Serialize};
6
7#[derive(Serialize, Deserialize, Clone, PartialEq)]
8pub struct BlobId {
9    pub height: BlockHeight,
10    pub namespace: Namespace,
11    pub commitment: Commitment,
12}
13
14#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
15pub struct JobId {
16    pub blob_id: BlobId,
17    // Used to prevent replay of proofs for the same blob
18    pub l2_chain_id: u64,
19    pub batch_number: u32,
20}
21
22impl JobId {
23    pub fn new(blob_id: BlobId, l2_chain_id: u64, batch_number: u32) -> Self {
24        Self {
25            blob_id,
26            l2_chain_id,
27            batch_number,
28        }
29    }
30}
31
32/// Format = "height:namespace:commitment:l2_chain_id:batch_number" using integers for height, l2_chain_id, and batch; base64 encoding for namespace and commitment
33impl FromStr for JobId {
34    type Err = Box<dyn Error>;
35
36    fn from_str(s: &str) -> Result<Self, Self::Err> {
37        // Split from the right so the left remainder is the whole BlobId string
38        let mut parts = s.rsplitn(3, ':');
39
40        let batch_number: u32 = parts.next().ok_or("Batch number missing (u32)")?.parse()?;
41
42        let l2_chain_id: u64 = parts.next().ok_or("L2 chain ID missing (u64)")?.parse()?;
43
44        let blob_str = parts.next().ok_or("BlobId missing")?;
45        let blob_id = BlobId::from_str(blob_str)?;
46
47        Ok(Self {
48            blob_id,
49            l2_chain_id,
50            batch_number,
51        })
52    }
53}
54
55/// Format = "height:namespace:commitment:l2_chain_id:batch_number" using integers for height, l2_chain_id, and batch; base64 encoding for namespace and commitment
56impl Display for JobId {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        write!(
59            f,
60            "{}:{}:{}",
61            self.blob_id, self.l2_chain_id, self.batch_number
62        )
63    }
64}
65
66impl BlobId {
67    pub fn new(height: BlockHeight, namespace: Namespace, commitment: Commitment) -> Self {
68        Self {
69            height,
70            namespace,
71            commitment,
72        }
73    }
74}
75
76impl std::fmt::Debug for BlobId {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        let namespace_string = if let Some(namespace) = &self.namespace.id_v0() {
79            base64::engine::general_purpose::STANDARD.encode(namespace)
80        } else {
81            "Invalid v0 ID".to_string()
82        };
83        let commitment_string =
84            base64::engine::general_purpose::STANDARD.encode(&self.commitment.hash());
85        f.debug_struct("BlobId")
86            .field("height", &self.height.value())
87            .field("namespace", &namespace_string)
88            .field("commitment", &commitment_string)
89            .finish()
90    }
91}
92
93/// Format = "height:namespace:commitment:l2_chain_id:batch_number" using integers for height, l2_chain_id, and batch; base64 encoding for namespace and commitment
94impl Display for BlobId {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        let namespace_string = if let Some(namespace) = &self.namespace.id_v0() {
97            base64::engine::general_purpose::STANDARD.encode(namespace)
98        } else {
99            "Invalid v0 ID".to_string()
100        };
101        let commitment_string =
102            base64::engine::general_purpose::STANDARD.encode(&self.commitment.hash());
103        write!(
104            f,
105            "{}:{}:{}",
106            self.height.value(),
107            namespace_string,
108            commitment_string
109        )
110    }
111}
112
113/// Format = "height:namespace:commitment:l2_chain_id:batch_number" using integers for height, l2_chain_id, and batch; base64 encoding for namespace and commitment
114impl FromStr for BlobId {
115    type Err = Box<dyn Error>;
116
117    fn from_str(s: &str) -> Result<Self, Self::Err> {
118        let mut parts = s.splitn(5, ":");
119
120        let height = BlockHeight::from_str(parts.next().ok_or("Height missing (u64)")?)?;
121
122        let n_base64 = parts
123            .next()
124            .ok_or("Namespace missing (base64)")?
125            .to_string();
126        println!("{}", &n_base64);
127        let n_bytes = base64::engine::general_purpose::STANDARD.decode(n_base64)?;
128        let namespace = Namespace::new_v0(&n_bytes)?;
129
130        let c_base64 = parts
131            .next()
132            .ok_or("Commitment missing (base64)")?
133            .to_string();
134        let c_bytes = base64::engine::general_purpose::STANDARD.decode(c_base64)?;
135        let c_hash: [u8; 32] = c_bytes
136            .try_into()
137            .map_err(|_| "Commitment must be 32 bytes!")?;
138        let commitment = Commitment::new(c_hash.into());
139
140        Ok(Self {
141            height,
142            namespace,
143            commitment,
144        })
145    }
146}
147
148#[cfg(test)]
149mod test {
150    use base64::engine::general_purpose::STANDARD;
151
152    use super::*;
153    use bincode;
154
155    #[test]
156    fn test_job_id_from_str() {
157        let height: u32 = 6952283;
158        // namespace in hex = 0x000000000000000000000000000000000000736f762d6d696e692d61
159        let namespace = "c292LW1pbmktYQ==";
160        let commitment = "JkVWHw0eLp6eeCEG28rLwF1xwUWGDI3+DbEyNNKq9fE=";
161        let l2_chain_id = 0u64;
162        let batch_number = 0u32;
163
164        let blob_id = BlobId::new(
165            BlockHeight::from(height),
166            Namespace::new_v0(STANDARD.decode(namespace).unwrap().as_slice()).unwrap(),
167            Commitment::new(STANDARD.decode(commitment).unwrap().try_into().unwrap()),
168        );
169
170        let job_id = JobId {
171            blob_id,
172            l2_chain_id,
173            batch_number,
174        };
175
176        let job_id_to_str = job_id.to_string();
177        let job_id_from_str = JobId::from_str(
178            "6952283:c292LW1pbmktYQ==:JkVWHw0eLp6eeCEG28rLwF1xwUWGDI3+DbEyNNKq9fE=:0:0",
179        )
180        .unwrap();
181
182        assert_eq!(job_id_from_str, job_id);
183        assert_eq!(
184            job_id_to_str,
185            "6952283:c292LW1pbmktYQ==:JkVWHw0eLp6eeCEG28rLwF1xwUWGDI3+DbEyNNKq9fE=:0:0"
186        );
187    }
188
189    #[test]
190    fn test_job_id_bincode() {
191        let height: u32 = 7640999;
192        // namespace in hex = 0x000000000000000000000000000000000000736f762d6d696e692d61
193        let namespace = "J3fU2WHHWlJt2A==";
194        let commitment = "SLzsmvT0rHZtgxS2yHHB7Hr7N6FkPi/UtUOHW0mtIqQ=";
195        let l2_chain_id = 0u64;
196        let batch_number = 0u32;
197
198        let blob_id = BlobId::new(
199            BlockHeight::from(height),
200            Namespace::new_v0(STANDARD.decode(namespace).unwrap().as_slice()).unwrap(),
201            Commitment::new(STANDARD.decode(commitment).unwrap().try_into().unwrap()),
202        );
203        let job_id = JobId::new(blob_id, l2_chain_id, batch_number);
204
205        println!("job_id: {:?}", job_id);
206        let job_id_bincode = bincode::serialize(&job_id).unwrap();
207        let job_id_from_bincode: JobId = bincode::deserialize(&job_id_bincode).unwrap();
208
209        assert_eq!(job_id_from_bincode, job_id);
210    }
211}