1use {
4 crate::{
5 move_bindings::{
6 canonical_walrus_blob_id,
7 interface::graph::{InputPort, OutputPort},
8 protocol_limits::primitives::data::MAX_INLINE_DATA_BYTES,
9 sui_framework::vec_map::{Entry as VecMapEntry, VecMap},
10 },
11 types::{NexusData, NexusValue},
12 walrus::{StorageInfo, WalrusClient, WALRUS_MAX_EPOCHS},
13 },
14 futures_util::future::{join_all, try_join_all},
15 sha2::{Digest as _, Sha256},
16 thiserror::Error,
17};
18
19#[derive(Debug, Error, PartialEq, Eq)]
21#[error("Walrus content digest mismatch for blob ID '{blob_id}'")]
22pub struct WalrusContentDigestMismatch {
23 pub blob_id: String,
24}
25
26#[derive(Debug, Error)]
30pub enum WalrusFetchError<T> {
31 #[error("Walrus content unavailable: {source}")]
32 Unavailable {
33 #[source]
34 source: anyhow::Error,
35 },
36 #[error("{mismatch}")]
37 DigestMismatch {
38 resolved: T,
39 #[source]
40 mismatch: WalrusContentDigestMismatch,
41 },
42}
43
44impl<T> WalrusFetchError<T> {
45 fn into_strict_error(self) -> anyhow::Error {
46 match self {
47 Self::Unavailable { source } => source,
48 Self::DigestMismatch { mismatch, .. } => mismatch.into(),
49 }
50 }
51}
52
53#[derive(Clone, Debug, Default, PartialEq, Eq)]
54pub struct StorageConf {
55 pub walrus_publisher_url: Option<String>,
56 pub walrus_aggregator_url: Option<String>,
57 pub walrus_save_for_epochs: Option<u8>,
58}
59
60impl StorageConf {
61 pub fn validate_walrus_upload(&self) -> anyhow::Result<()> {
63 for (name, value) in [
64 ("publisher", self.walrus_publisher_url.as_deref()),
65 ("aggregator", self.walrus_aggregator_url.as_deref()),
66 ] {
67 let value = value
68 .ok_or_else(|| anyhow::anyhow!("Walrus {name} URL is not set in storage config"))?;
69 let url = reqwest::Url::parse(value)
70 .map_err(|error| anyhow::anyhow!("Walrus {name} URL is invalid: {error}"))?;
71 anyhow::ensure!(
72 matches!(url.scheme(), "http" | "https"),
73 "Walrus {name} URL must use http or https"
74 );
75 }
76
77 let store_for_epochs = self.walrus_save_for_epochs.ok_or_else(|| {
78 anyhow::anyhow!("Walrus save for epochs is not set in storage config")
79 })?;
80 anyhow::ensure!(
81 store_for_epochs <= WALRUS_MAX_EPOCHS,
82 "Walrus save for epochs exceeds maximum allowed ({WALRUS_MAX_EPOCHS})"
83 );
84 Ok(())
85 }
86}
87
88impl WalrusClient {
89 pub async fn upload_value(&self, bytes: Vec<u8>, epochs: u8) -> anyhow::Result<NexusValue> {
93 anyhow::ensure!(
94 bytes.len() <= crate::execution_limits::MAX_RESOLVED_DATA_BYTES,
95 "blob exceeds execution byte limit"
96 );
97 anyhow::ensure!(
98 (1..=WALRUS_MAX_EPOCHS).contains(&epochs),
99 "invalid storage duration"
100 );
101 let digest = Sha256::digest(&bytes).to_vec();
102 let response = self.upload_bytes(bytes.clone(), epochs, None).await?;
103 let blob_id = blob_id_from_storage_info(response)?;
104 let reference = NexusValue::walrus_data(blob_id.as_bytes(), digest)?;
105 let readback = self.read_file_bounded(&blob_id, bytes.len()).await?;
106 anyhow::ensure!(
107 readback == bytes,
108 "Walrus upload readback differs from the original bytes"
109 );
110 Ok(reference)
111 }
112}
113
114impl NexusData {
115 pub async fn fetch(self, conf: &StorageConf) -> anyhow::Result<NexusData> {
117 self.resolve(conf)
118 .await
119 .map_err(WalrusFetchError::into_strict_error)
120 }
121
122 pub async fn resolve(
124 self,
125 conf: &StorageConf,
126 ) -> Result<NexusData, WalrusFetchError<NexusData>> {
127 if !self.is_well_formed() {
128 return Err(WalrusFetchError::Unavailable {
129 source: anyhow::anyhow!("cannot resolve malformed NexusData"),
130 });
131 }
132 let has_walrus = self.has_walrus();
133 let client = if has_walrus {
134 Some(walrus_reader(conf).map_err(|source| WalrusFetchError::Unavailable { source })?)
135 } else {
136 None
137 };
138 let many = self.is_many();
139 let source_values = self
140 .into_values()
141 .map_err(|source| WalrusFetchError::Unavailable { source })?;
142 let mut resolved = source_values
143 .iter()
144 .map(|value| match value {
145 NexusValue::WalrusData { .. } => NexusValue::InlineData { bytes: Vec::new() },
146 value => value.clone(),
147 })
148 .collect::<Vec<_>>();
149 let mut mismatch = None;
150 for (index, value) in source_values.into_iter().enumerate() {
151 if let NexusValue::WalrusData {
152 blob_id,
153 content_digest,
154 } = value
155 {
156 let blob_id = blob_id_from_bytes(&blob_id)
157 .map_err(|source| WalrusFetchError::Unavailable { source })?;
158 let max_bytes = resolved_blob_byte_limit(&resolved, index, many)
159 .map_err(|source| WalrusFetchError::Unavailable { source })?;
160 let bytes = client
161 .as_ref()
162 .expect("Walrus client exists when a reference is present")
163 .read_file_bounded(&blob_id, max_bytes)
164 .await
165 .map_err(|source| WalrusFetchError::Unavailable {
166 source: source.into(),
167 })?;
168 let actual_digest: [u8; 32] = Sha256::digest(&bytes).into();
169 if actual_digest.as_slice() != content_digest && mismatch.is_none() {
170 mismatch = Some(WalrusContentDigestMismatch { blob_id });
171 }
172 resolved[index] = NexusValue::InlineData { bytes };
173 }
174 }
175 let resolved = NexusData::from_values(resolved, many)
176 .map_err(|source| WalrusFetchError::Unavailable { source })?;
177 match mismatch {
178 Some(mismatch) => Err(WalrusFetchError::DigestMismatch { resolved, mismatch }),
179 None => Ok(resolved),
180 }
181 }
182
183 pub async fn commit(self, conf: &StorageConf) -> anyhow::Result<Self> {
185 anyhow::ensure!(self.is_well_formed(), "cannot commit malformed NexusData");
186 if self.has_walrus() {
187 self.clone().fetch(conf).await?;
188 }
189 Ok(self)
190 }
191
192 pub async fn upload_data_fields(self, conf: &StorageConf) -> anyhow::Result<Self> {
194 anyhow::ensure!(self.is_well_formed(), "cannot upload malformed NexusData");
195 let (client, store_for_epochs) = walrus_writer(conf)?;
196 let many = self.is_many();
197 let mut uploaded = Vec::new();
198 for value in self.into_values()? {
199 match value {
200 NexusValue::InlineData { bytes } => {
201 uploaded.push(client.upload_value(bytes, store_for_epochs).await?);
202 }
203 NexusValue::WalrusData { .. } => {
204 anyhow::bail!("Data upload received an already remote value");
205 }
206 NexusValue::Object { .. } => {
207 anyhow::bail!("Data upload cannot store Object values in Walrus");
208 }
209 }
210 }
211 NexusData::from_values(uploaded, many)
212 }
213}
214
215impl VecMap<InputPort, NexusData> {
216 pub async fn commit_all(self, storage_conf: &StorageConf) -> anyhow::Result<Self> {
217 commit_entries(self.contents, storage_conf)
218 .await
219 .map(|contents| Self { contents })
220 }
221
222 pub async fn fetch_all(
223 self,
224 storage_conf: &StorageConf,
225 ) -> anyhow::Result<VecMap<InputPort, NexusData>> {
226 resolve_entries(self.contents, storage_conf)
227 .await
228 .map(|contents| VecMap { contents })
229 .map_err(WalrusFetchError::into_strict_error)
230 }
231
232 pub async fn resolve_all(
234 self,
235 storage_conf: &StorageConf,
236 ) -> Result<VecMap<InputPort, NexusData>, WalrusFetchError<VecMap<InputPort, NexusData>>> {
237 match resolve_entries(self.contents, storage_conf).await {
238 Ok(contents) => Ok(VecMap { contents }),
239 Err(WalrusFetchError::Unavailable { source }) => {
240 Err(WalrusFetchError::Unavailable { source })
241 }
242 Err(WalrusFetchError::DigestMismatch { resolved, mismatch }) => {
243 Err(WalrusFetchError::DigestMismatch {
244 resolved: VecMap { contents: resolved },
245 mismatch,
246 })
247 }
248 }
249 }
250}
251
252impl VecMap<OutputPort, NexusData> {
253 pub async fn commit_all(self, storage_conf: &StorageConf) -> anyhow::Result<Self> {
254 commit_entries(self.contents, storage_conf)
255 .await
256 .map(|contents| Self { contents })
257 }
258
259 pub async fn fetch_all(
260 self,
261 storage_conf: &StorageConf,
262 ) -> anyhow::Result<VecMap<OutputPort, NexusData>> {
263 resolve_entries(self.contents, storage_conf)
264 .await
265 .map(|contents| VecMap { contents })
266 .map_err(WalrusFetchError::into_strict_error)
267 }
268
269 pub async fn resolve_all(
271 self,
272 storage_conf: &StorageConf,
273 ) -> Result<VecMap<OutputPort, NexusData>, WalrusFetchError<VecMap<OutputPort, NexusData>>>
274 {
275 match resolve_entries(self.contents, storage_conf).await {
276 Ok(contents) => Ok(VecMap { contents }),
277 Err(WalrusFetchError::Unavailable { source }) => {
278 Err(WalrusFetchError::Unavailable { source })
279 }
280 Err(WalrusFetchError::DigestMismatch { resolved, mismatch }) => {
281 Err(WalrusFetchError::DigestMismatch {
282 resolved: VecMap { contents: resolved },
283 mismatch,
284 })
285 }
286 }
287 }
288}
289
290async fn commit_entries<P>(
291 contents: Vec<VecMapEntry<P, NexusData>>,
292 storage_conf: &StorageConf,
293) -> anyhow::Result<Vec<VecMapEntry<P, NexusData>>> {
294 validate_entries(&contents)?;
295 try_join_all(contents.into_iter().map(|entry| async move {
296 entry
297 .value
298 .commit(storage_conf)
299 .await
300 .map(|value| VecMapEntry {
301 key: entry.key,
302 value,
303 })
304 }))
305 .await
306}
307
308async fn resolve_entries<P>(
309 contents: Vec<VecMapEntry<P, NexusData>>,
310 storage_conf: &StorageConf,
311) -> Result<Vec<VecMapEntry<P, NexusData>>, WalrusFetchError<Vec<VecMapEntry<P, NexusData>>>> {
312 validate_entries(&contents).map_err(|source| WalrusFetchError::Unavailable { source })?;
313 let fetched = join_all(
314 contents
315 .into_iter()
316 .map(|entry| async move { (entry.key, entry.value.resolve(storage_conf).await) }),
317 )
318 .await;
319 let mut resolved = Vec::with_capacity(fetched.len());
320 let mut mismatch = None;
321 for (key, value) in fetched {
322 match value {
323 Ok(value) => resolved.push(VecMapEntry { key, value }),
324 Err(WalrusFetchError::Unavailable { source }) => {
325 return Err(WalrusFetchError::Unavailable { source });
326 }
327 Err(WalrusFetchError::DigestMismatch {
328 resolved: value,
329 mismatch: current,
330 }) => {
331 resolved.push(VecMapEntry { key, value });
332 mismatch.get_or_insert(current);
333 }
334 }
335 }
336 match mismatch {
337 Some(mismatch) => Err(WalrusFetchError::DigestMismatch { resolved, mismatch }),
338 None => Ok(resolved),
339 }
340}
341
342fn validate_entries<P>(contents: &[VecMapEntry<P, NexusData>]) -> anyhow::Result<()> {
343 anyhow::ensure!(
344 contents.iter().all(|entry| entry.value.is_well_formed()),
345 "cannot process malformed NexusData map",
346 );
347 Ok(())
348}
349
350fn resolved_blob_byte_limit(
351 resolved: &[NexusValue],
352 index: usize,
353 many: bool,
354) -> anyhow::Result<usize> {
355 anyhow::ensure!(
356 index < resolved.len(),
357 "Walrus value index is out of bounds"
358 );
359 let mut lower = 0;
360 let mut upper = MAX_INLINE_DATA_BYTES as usize;
361 while lower < upper {
362 let candidate = lower + (upper - lower).div_ceil(2);
363 let mut values = resolved.to_vec();
364 values[index] = NexusValue::InlineData {
365 bytes: vec![0; candidate],
366 };
367 if NexusData::from_values(values, many).is_ok() {
368 lower = candidate;
369 } else {
370 upper = candidate - 1;
371 }
372 }
373 Ok(lower)
374}
375
376fn walrus_reader(conf: &StorageConf) -> anyhow::Result<WalrusClient> {
377 let aggregator_url = conf
378 .walrus_aggregator_url
379 .as_ref()
380 .ok_or_else(|| anyhow::anyhow!("Walrus aggregator URL is not set in storage config"))?;
381 Ok(WalrusClient::builder()
382 .with_aggregator_url(aggregator_url)
383 .build())
384}
385
386fn walrus_writer(conf: &StorageConf) -> anyhow::Result<(WalrusClient, u8)> {
387 conf.validate_walrus_upload()?;
388 let publisher_url = conf
389 .walrus_publisher_url
390 .as_ref()
391 .ok_or_else(|| anyhow::anyhow!("Walrus publisher URL is not set in storage config"))?;
392 let aggregator_url = conf
393 .walrus_aggregator_url
394 .as_ref()
395 .ok_or_else(|| anyhow::anyhow!("Walrus aggregator URL is not set in storage config"))?;
396 let store_for_epochs = conf
397 .walrus_save_for_epochs
398 .ok_or_else(|| anyhow::anyhow!("Walrus save for epochs is not set in storage config"))?;
399 if store_for_epochs > WALRUS_MAX_EPOCHS {
400 anyhow::bail!("Walrus save for epochs exceeds maximum allowed ({WALRUS_MAX_EPOCHS})");
401 }
402 Ok((
403 WalrusClient::builder()
404 .with_publisher_url(publisher_url)
405 .with_aggregator_url(aggregator_url)
406 .build(),
407 store_for_epochs,
408 ))
409}
410
411fn blob_id_from_bytes(bytes: &[u8]) -> anyhow::Result<String> {
412 canonical_walrus_blob_id(bytes).map(str::to_owned)
413}
414
415fn blob_id_from_storage_info(info: StorageInfo) -> anyhow::Result<String> {
416 info.newly_created
417 .map(|created| created.blob_object.blob_id)
418 .or_else(|| info.already_certified.map(|certified| certified.blob_id))
419 .ok_or_else(|| anyhow::anyhow!("Failed to store data on Walrus: no committed blob info"))
420}
421
422#[cfg(test)]
423mod tests {
424 use {
425 super::*,
426 crate::walrus::{AlreadyCertified, BlobObject, BlobStorage, NewlyCreated, SuiEvent},
427 mockito::{Server, ServerGuard},
428 };
429
430 const BLOB_ID: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
431
432 async fn setup_mock_server_and_conf() -> anyhow::Result<(ServerGuard, StorageConf)> {
433 let server = Server::new_async().await;
434 let server_url = server.url();
435 Ok((
436 server,
437 StorageConf {
438 walrus_publisher_url: Some(server_url.clone()),
439 walrus_aggregator_url: Some(server_url),
440 walrus_save_for_epochs: Some(2),
441 },
442 ))
443 }
444
445 #[tokio::test]
446 async fn typed_inline_commit_and_fetch_are_noops() {
447 let storage_conf = StorageConf::default();
448 let data = NexusData::inline_data(b"payload").unwrap();
449 let fetched = data
450 .clone()
451 .commit(&storage_conf)
452 .await
453 .unwrap()
454 .fetch(&storage_conf)
455 .await
456 .unwrap();
457
458 assert!(matches!(
459 fetched.values().unwrap().as_slice(),
460 [NexusValue::InlineData { bytes }] if bytes == b"payload"
461 ));
462 }
463
464 #[tokio::test]
465 async fn empty_many_is_rejected_before_walrus_configuration() {
466 let value = NexusData::Many { values: Vec::new() };
467
468 assert!(value
469 .clone()
470 .upload_data_fields(&StorageConf::default())
471 .await
472 .unwrap_err()
473 .to_string()
474 .contains("cannot upload malformed NexusData"));
475 assert!(value
476 .clone()
477 .commit(&StorageConf::default())
478 .await
479 .unwrap_err()
480 .to_string()
481 .contains("cannot commit malformed NexusData"));
482 assert!(value
483 .fetch(&StorageConf::default())
484 .await
485 .unwrap_err()
486 .to_string()
487 .contains("cannot resolve malformed NexusData"));
488 }
489
490 #[tokio::test]
491 async fn malformed_map_entry_precedes_all_walrus_requests() {
492 let (mut server, storage_conf) = setup_mock_server_and_conf().await.unwrap();
493 let get = server
494 .mock("GET", format!("/v1/blobs/{BLOB_ID}").as_str())
495 .expect(0)
496 .create_async()
497 .await;
498 let valid = NexusData::walrus_data(BLOB_ID.as_bytes(), vec![0; 32]).unwrap();
499 let malformed = NexusData::Many { values: Vec::new() };
500 let map = || VecMap {
501 contents: vec![
502 VecMapEntry {
503 key: InputPort::new("valid"),
504 value: valid.clone(),
505 },
506 VecMapEntry {
507 key: InputPort::new("malformed"),
508 value: malformed.clone(),
509 },
510 ],
511 };
512
513 assert!(map()
514 .commit_all(&storage_conf)
515 .await
516 .unwrap_err()
517 .to_string()
518 .contains("cannot process malformed NexusData map"));
519 assert!(map()
520 .resolve_all(&storage_conf)
521 .await
522 .unwrap_err()
523 .to_string()
524 .contains("cannot process malformed NexusData map"));
525 get.assert_async().await;
526 }
527
528 #[tokio::test]
529 async fn typed_upload_reads_back_and_commits_digest() {
530 let (mut server, storage_conf) = setup_mock_server_and_conf().await.unwrap();
531 let response = StorageInfo {
532 newly_created: Some(NewlyCreated {
533 blob_object: BlobObject {
534 blob_id: BLOB_ID.to_string(),
535 id: "raw_object_id".to_string(),
536 storage: BlobStorage { end_epoch: 200 },
537 },
538 }),
539 already_certified: None,
540 };
541 let put = server
542 .mock("PUT", "/v1/blobs?epochs=2")
543 .with_status(200)
544 .with_header("content-type", "application/json")
545 .with_body(serde_json::to_string(&response).unwrap())
546 .create_async()
547 .await;
548 let get = server
549 .mock("GET", format!("/v1/blobs/{BLOB_ID}").as_str())
550 .with_status(200)
551 .with_body("payload")
552 .create_async()
553 .await;
554
555 let committed = NexusData::inline_data(b"payload")
556 .unwrap()
557 .upload_data_fields(&storage_conf)
558 .await
559 .unwrap();
560
561 assert!(matches!(
562 committed.values().unwrap().as_slice(),
563 [NexusValue::WalrusData { blob_id, .. }]
564 if blob_id == BLOB_ID.as_bytes()
565 ));
566 put.assert_async().await;
567 get.assert_async().await;
568 }
569
570 #[tokio::test]
571 async fn typed_fetch_rejects_digest_mismatch() {
572 let (mut server, storage_conf) = setup_mock_server_and_conf().await.unwrap();
573 let get = server
574 .mock("GET", format!("/v1/blobs/{BLOB_ID}").as_str())
575 .with_status(200)
576 .with_body("different")
577 .create_async()
578 .await;
579 let value = NexusData::walrus_data(BLOB_ID.as_bytes(), Sha256::digest(b"payload").to_vec())
580 .unwrap();
581
582 let error = value.fetch(&storage_conf).await.unwrap_err();
583
584 assert!(error
585 .downcast_ref::<WalrusContentDigestMismatch>()
586 .is_some());
587 get.assert_async().await;
588 }
589
590 #[tokio::test]
591 async fn typed_fetch_rejects_content_larger_than_inline_limit() {
592 let (mut server, storage_conf) = setup_mock_server_and_conf().await.unwrap();
593 let bytes = vec![b'x'; 61_441];
594 let get = server
595 .mock("GET", format!("/v1/blobs/{BLOB_ID}").as_str())
596 .with_status(200)
597 .with_body(bytes.clone())
598 .create_async()
599 .await;
600 let value =
601 NexusData::walrus_data(BLOB_ID.as_bytes(), Sha256::digest(&bytes).to_vec()).unwrap();
602
603 assert!(value.fetch(&storage_conf).await.is_err());
604 get.assert_async().await;
605 }
606
607 #[tokio::test]
608 async fn typed_resolve_preserves_fetched_bytes_for_digest_mismatch() {
609 let (mut server, storage_conf) = setup_mock_server_and_conf().await.unwrap();
610 let get = server
611 .mock("GET", format!("/v1/blobs/{BLOB_ID}").as_str())
612 .with_status(200)
613 .with_body("different")
614 .create_async()
615 .await;
616 let value = NexusData::walrus_data(BLOB_ID.as_bytes(), Sha256::digest(b"payload").to_vec())
617 .unwrap();
618
619 let WalrusFetchError::DigestMismatch { resolved, mismatch } =
620 value.resolve(&storage_conf).await.unwrap_err()
621 else {
622 panic!("successful fetch with unequal bytes must be an integrity mismatch");
623 };
624
625 assert_eq!(mismatch.blob_id, BLOB_ID);
626 assert!(matches!(
627 resolved.values().unwrap().as_slice(),
628 [NexusValue::InlineData { bytes }] if bytes == b"different"
629 ));
630 get.assert_async().await;
631 }
632
633 #[tokio::test]
634 async fn typed_resolve_classifies_unavailable_content_separately() {
635 let (mut server, storage_conf) = setup_mock_server_and_conf().await.unwrap();
636 let get = server
637 .mock("GET", format!("/v1/blobs/{BLOB_ID}").as_str())
638 .with_status(404)
639 .create_async()
640 .await;
641 let value = NexusData::walrus_data(BLOB_ID.as_bytes(), Sha256::digest(b"payload").to_vec())
642 .unwrap();
643
644 assert!(matches!(
645 value.resolve(&storage_conf).await.unwrap_err(),
646 WalrusFetchError::Unavailable { .. }
647 ));
648 get.assert_async().await;
649 }
650
651 #[tokio::test]
652 async fn typed_fetch_preserves_many_cardinality_and_order() {
653 let (mut server, storage_conf) = setup_mock_server_and_conf().await.unwrap();
654 let get = server
655 .mock("GET", format!("/v1/blobs/{BLOB_ID}").as_str())
656 .with_status(200)
657 .with_body("remote")
658 .create_async()
659 .await;
660 let value = NexusData::many(vec![
661 NexusValue::inline_data(b"inline").unwrap(),
662 NexusValue::walrus_data(BLOB_ID.as_bytes(), Sha256::digest(b"remote").to_vec())
663 .unwrap(),
664 ])
665 .unwrap();
666
667 let resolved = value.fetch(&storage_conf).await.unwrap();
668
669 assert!(matches!(
670 resolved,
671 NexusData::Many { values }
672 if matches!(
673 values.as_slice(),
674 [
675 NexusValue::InlineData { bytes: first },
676 NexusValue::InlineData { bytes: second },
677 ] if first == b"inline" && second == b"remote"
678 )
679 ));
680 get.assert_async().await;
681 }
682
683 #[test]
684 fn resolved_blob_limit_accounts_for_full_many_bcs_size() {
685 let resolved = vec![
686 NexusValue::InlineData {
687 bytes: vec![0; 61_440],
688 },
689 NexusValue::InlineData { bytes: Vec::new() },
690 ];
691
692 let limit = resolved_blob_byte_limit(&resolved, 1, true).unwrap();
693 let mut at_limit = resolved.clone();
694 at_limit[1] = NexusValue::InlineData {
695 bytes: vec![0; limit],
696 };
697 let mut over_limit = resolved;
698 over_limit[1] = NexusValue::InlineData {
699 bytes: vec![0; limit + 1],
700 };
701
702 assert!(limit < MAX_INLINE_DATA_BYTES as usize);
703 assert!(NexusData::from_values(at_limit, true).is_ok());
704 assert!(NexusData::from_values(over_limit, true).is_err());
705 }
706
707 #[test]
708 fn certified_response_returns_existing_blob_id() {
709 let info = StorageInfo {
710 newly_created: None,
711 already_certified: Some(AlreadyCertified {
712 blob_id: "existing".to_string(),
713 event: SuiEvent {
714 tx_digest: "0x1".to_string(),
715 },
716 end_epoch: 1,
717 }),
718 };
719
720 assert_eq!(blob_id_from_storage_info(info).unwrap(), "existing");
721 }
722}