1use celestia_types::{
2 consts::appconsts::{
3 CONTINUATION_SPARSE_SHARE_CONTENT_SIZE, FIRST_SPARSE_SHARE_CONTENT_SIZE, NAMESPACE_SIZE,
4 SEQUENCE_LEN_BYTES, SHARE_INFO_BYTES, SHARE_SIZE, SIGNER_SIZE,
5 },
6 ShareProof,
7};
8use serde::{Deserialize, Serialize};
9use sha3::{Digest, Keccak256};
10
11#[cfg(feature = "host")]
12mod error;
13#[cfg(feature = "host")]
14pub use error::{ErrorLabels, InclusionServiceError};
15
16#[cfg(feature = "grpc")]
17pub mod eqs {
19 include!("generated/eqs.rs");
20}
21
22#[derive(Serialize, Deserialize, Clone, Debug)]
28pub struct ZKStackEqProofInput {
29 pub share_proof: ShareProof,
30 pub share_version: bool,
31 pub tail_padding: usize,
32 pub data_availability_root: [u8; 32],
33 pub batch_number: u32,
34 pub chain_id: u64,
35}
36
37pub struct ZKStackEqProofOutput {
38 pub keccak_hash: [u8; 32],
39 pub data_availability_root: [u8; 32],
40 pub batch_number: u32,
41 pub chain_id: u64,
42}
43
44impl ZKStackEqProofOutput {
45 pub fn to_vec(&self) -> Vec<u8> {
47 let mut encoded = Vec::new();
48 encoded.extend_from_slice(&self.keccak_hash);
49 encoded.extend_from_slice(&self.data_availability_root);
50 encoded.extend_from_slice(&self.batch_number.to_le_bytes());
51 encoded.extend_from_slice(&self.chain_id.to_le_bytes());
52 encoded
53 }
54
55 #[cfg(feature = "host")]
56 pub fn from_bytes(data: &[u8]) -> Result<Self, InclusionServiceError> {
57 if data.len() != 76 {
58 return Err(InclusionServiceError::OutputDeserializationError);
59 }
60 let decoded = ZKStackEqProofOutput {
61 keccak_hash: data[0..32]
62 .try_into()
63 .map_err(|_| InclusionServiceError::OutputDeserializationError)?,
64 data_availability_root: data[32..64]
65 .try_into()
66 .map_err(|_| InclusionServiceError::OutputDeserializationError)?,
67 batch_number: u32::from_le_bytes(
68 data[64..68]
69 .try_into()
70 .map_err(|_| InclusionServiceError::OutputDeserializationError)?,
71 ),
72 chain_id: u64::from_le_bytes(
73 data[68..76]
74 .try_into()
75 .map_err(|_| InclusionServiceError::OutputDeserializationError)?,
76 ),
77 };
78 Ok(decoded)
79 }
80}
81
82pub fn compute_share_raw_data_keccak(
100 raw_shares: &[[u8; SHARE_SIZE]],
101 share_version: bool,
102 tail_padding: usize,
103) -> [u8; 32] {
104 let mut hasher = Keccak256::new();
105 let n = raw_shares.len();
106 let off_first = first_data_offset(share_version);
107
108 if n == 1 {
109 let take = FIRST_SPARSE_SHARE_CONTENT_SIZE - tail_padding;
111 let s0 = raw_shares[0].as_ref();
112 hasher.update(&s0[off_first..off_first + take]);
113 return hasher.finalize().into();
114 }
115
116 {
119 let s0 = raw_shares[0].as_ref();
120 let end0 = off_first + FIRST_SPARSE_SHARE_CONTENT_SIZE;
121 hasher.update(&s0[off_first..end0]);
122 }
123
124 let off_cont = NAMESPACE_SIZE + SHARE_INFO_BYTES;
125 let last_take = CONTINUATION_SPARSE_SHARE_CONTENT_SIZE - tail_padding;
126 let last_full = tail_padding == 0;
127
128 let full_end = if last_full { n } else { n - 1 };
130 for i in 1..full_end {
131 let si = raw_shares[i].as_ref();
132 let endi = off_cont + CONTINUATION_SPARSE_SHARE_CONTENT_SIZE;
133 hasher.update(&si[off_cont..endi]);
134 }
135
136 if !last_full {
138 let slast = raw_shares[n - 1].as_ref();
139 hasher.update(&slast[off_cont..off_cont + last_take]);
140 }
141
142 hasher.finalize().into()
143}
144
145#[inline(always)]
146fn first_data_offset(version: bool) -> usize {
147 let base = NAMESPACE_SIZE + SHARE_INFO_BYTES + SEQUENCE_LEN_BYTES;
148 if version {
149 base + SIGNER_SIZE
150 } else {
151 base
152 }
153}
154
155#[inline(always)]
161pub fn tail_padding_for_len(total_len: usize) -> usize {
162 if total_len <= FIRST_SPARSE_SHARE_CONTENT_SIZE {
163 FIRST_SPARSE_SHARE_CONTENT_SIZE - total_len
165 } else {
166 let rem = total_len - FIRST_SPARSE_SHARE_CONTENT_SIZE;
167 let tail_bytes = rem % CONTINUATION_SPARSE_SHARE_CONTENT_SIZE;
168 if tail_bytes == 0 {
169 0 } else {
171 CONTINUATION_SPARSE_SHARE_CONTENT_SIZE - tail_bytes
172 }
173 }
174}
175
176#[cfg(feature = "host")]
180#[inline(always)]
181pub fn exact_u8_to_bool(val: u8) -> bool {
182 match val {
183 0 => false,
184 1 => true,
185 _ => panic!("u8->bool not 0 or 1: {}", val),
186 }
187}
188
189#[cfg(all(test, feature = "host"))]
190mod test {
191 use super::*; use celestia_types::{nmt::Namespace, AppVersion, Blob};
193 use rand::{rngs::StdRng, Rng, SeedableRng};
194 use sha3::{Digest, Keccak256};
195
196 #[test]
197 fn test_serialization() {
198 let output = ZKStackEqProofOutput {
199 keccak_hash: [0; 32],
200 data_availability_root: [0; 32],
201 batch_number: 0u32,
202 chain_id: 0u64,
203 };
204 let encoded = output.to_vec();
205 let decoded = ZKStackEqProofOutput::from_bytes(&encoded).unwrap();
206 assert_eq!(output.keccak_hash, decoded.keccak_hash);
207 assert_eq!(
208 output.data_availability_root,
209 decoded.data_availability_root
210 );
211 }
212
213 #[test]
214 fn keccak_of_data_matches_keccak_from_blob_shares_randomized() {
215 let mut rng = StdRng::seed_from_u64(0xCE1E5);
217
218 for _ in 0..5 {
220 let len = rng.gen_range(100usize..=1_000_000usize);
221 let mut data = vec![0u8; len];
222 rng.fill(&mut data[..]);
223
224 let ns = Namespace::new_v0(&[1, 2, 3, 4, 5]).expect("invalid namespace");
225
226 let blob = Blob::new(ns, data.clone(), None, AppVersion::latest())
227 .expect("blob construction failed");
228 let share_version = exact_u8_to_bool(blob.share_version);
229
230 let shares: Vec<[u8; SHARE_SIZE]> = blob
231 .to_shares()
232 .expect("invalid blob->shares")
233 .iter()
234 .map(|s| s.data().to_owned())
235 .collect();
236
237 let first = shares.first().expect("no shares emitted");
239 let info = first[NAMESPACE_SIZE]; let derived_version_byte = info >> 1; let derived_version_bool = exact_u8_to_bool(derived_version_byte);
242 assert_eq!(
243 derived_version_bool, share_version,
244 "share version mismatch"
245 );
246
247 let expected = {
249 let mut h = Keccak256::new();
250 h.update(&data);
251 <[u8; 32]>::from(h.finalize())
252 };
253
254 let tail_padding = tail_padding_for_len(data.len());
255
256 let got = compute_share_raw_data_keccak(&shares, derived_version_bool, tail_padding);
258
259 assert_eq!(
260 got, expected,
261 "keccak(blob shares) != keccak(raw data) for len={len}, version={share_version}"
262 );
263 }
264 }
265}