1use serde::{Deserialize, Serialize};
8use serde_big_array::BigArray;
9
10use crate::{Address, Hash};
11
12pub const CHUNK_SIZE: u64 = 1_048_576;
16
17pub const CHALLENGE_TTL_BLOCKS: u64 = 50;
19
20pub const CHALLENGE_INTERVAL_BLOCKS: u64 = 100;
22
23pub const CHALLENGE_REWARD: u64 = 10_000_000_000;
25
26pub const SLASH_PERCENTAGE: u64 = 5;
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub struct StorageMetadata {
34 pub merkle_root: Hash,
36 pub owner: Address,
38 pub total_size_bytes: u64,
40 pub access_list: Vec<Address>,
42 pub fee_pool: u64,
44 pub created_at: u64,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54pub struct StorageChallenge {
55 pub challenge_id: Hash,
57 pub merkle_root: Hash,
59 pub chunk_index: u32,
61 pub target_node: Address,
63 pub created_at_height: u64,
65 pub expires_at_height: u64,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub enum StorageMetadataOperation {
74 RegisterFile {
76 merkle_root: Hash,
77 total_size_bytes: u64,
78 access_list: Vec<Address>,
79 fee_deposit: u64,
80 },
81 UpdateAccessList {
83 merkle_root: Hash,
84 new_access_list: Vec<Address>,
85 },
86 AddAccess {
88 merkle_root: Hash,
89 address: Address,
90 },
91 RemoveAccess {
93 merkle_root: Hash,
94 address: Address,
95 },
96 TopUpFeePool {
98 merkle_root: Hash,
99 amount: u64,
100 },
101 SubmitStorageProof {
103 challenge_id: Hash,
105 merkle_root: Hash,
107 chunk_index: u32,
109 chunk_hash: Hash,
111 merkle_path: Vec<Hash>,
113 },
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118pub struct StorageMetadataTxData {
119 pub operation: StorageMetadataOperation,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
137pub struct EncryptedKeyBundleV2(#[serde(with = "BigArray")] pub [u8; 80]);
138
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
142pub struct AccessEntryV2 {
143 pub address: Address,
144 pub encrypted_key_bundle: Option<EncryptedKeyBundleV2>,
147 pub expires_at: Option<u64>,
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
154#[repr(u8)]
155pub enum FileLifecycleV2 {
156 Pending = 0,
157 Active = 1,
158 Abandoned = 2,
159}
160
161impl FileLifecycleV2 {
162 pub fn from_byte(b: u8) -> Option<Self> {
163 match b {
164 0 => Some(Self::Pending),
165 1 => Some(Self::Active),
166 2 => Some(Self::Abandoned),
167 _ => None,
168 }
169 }
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
175#[repr(u8)]
176pub enum FileVisibilityV2 {
177 Public = 0,
178 Private = 1,
179}
180
181impl FileVisibilityV2 {
182 pub fn from_byte(b: u8) -> Option<Self> {
183 match b {
184 0 => Some(Self::Public),
185 1 => Some(Self::Private),
186 _ => None,
187 }
188 }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
194pub enum StorageMetadataOperationV2 {
195 RegisterFilePendingV2 {
200 merkle_root: Hash,
201 plaintext_size_bytes: u64,
202 stored_size_bytes: u64,
203 chunk_count: u32,
204 fee_deposit: u64,
205 visibility: u8,
208 initial_access: Vec<AccessEntryV2>,
209 },
210 ActivateFileV2 {
213 merkle_root: Hash,
214 },
215 AbandonFileV2 {
219 merkle_root: Hash,
220 },
221 AcceptAssignmentV2 {
224 merkle_root: Hash,
225 chunk_indices: Vec<u32>,
226 },
227 AddAccessV2 {
229 merkle_root: Hash,
230 entry: AccessEntryV2,
231 },
232 RemoveAccessV2 {
234 merkle_root: Hash,
235 address: Address,
236 },
237 UpdateAccessV2 {
240 merkle_root: Hash,
241 address: Address,
242 new_entry: AccessEntryV2,
243 },
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
248pub struct StorageMetadataV2TxData {
249 pub operation: StorageMetadataOperationV2,
250}
251
252pub const SNIP_V2_ASSIGNMENT_CONTEXT: &str = "sumchain SNIP-V2 chunk-assignment v1";
258
259pub fn assigned_archives(
274 merkle_root: &Hash,
275 snapshot_addresses: &[Address],
276 chunk_index: u32,
277 replication_factor: u32,
278) -> Vec<Address> {
279 let mut addrs: Vec<Address> = snapshot_addresses.to_vec();
283 addrs.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
284 addrs.dedup_by(|a, b| a.as_bytes() == b.as_bytes());
285
286 let r_eff = (replication_factor as usize).min(addrs.len());
287 if r_eff == 0 {
288 return Vec::new();
289 }
290
291 let mut scored: Vec<(u64, Address)> = Vec::with_capacity(addrs.len());
294 let chunk_be = chunk_index.to_be_bytes();
295 for a in &addrs {
296 let mut input = [0u8; 56];
297 input[..32].copy_from_slice(merkle_root.as_bytes());
298 input[32..36].copy_from_slice(&chunk_be);
299 input[36..56].copy_from_slice(a.as_bytes());
300 let derived = blake3::derive_key(SNIP_V2_ASSIGNMENT_CONTEXT, &input);
301 let score = u64::from_be_bytes(derived[..8].try_into().expect("8-byte slice"));
302 scored.push((score, *a));
303 }
304
305 scored.sort_by(|x, y| {
308 x.0.cmp(&y.0)
309 .then_with(|| x.1.as_bytes().cmp(y.1.as_bytes()))
310 });
311
312 scored.into_iter().take(r_eff).map(|(_, a)| a).collect()
313}
314
315pub fn assigned_archives_presorted(
327 merkle_root: &Hash,
328 sorted_addresses: &[Address],
329 chunk_index: u32,
330 replication_factor: u32,
331) -> Vec<Address> {
332 let r_eff = (replication_factor as usize).min(sorted_addresses.len());
333 if r_eff == 0 {
334 return Vec::new();
335 }
336 let mut scored: Vec<(u64, Address)> = Vec::with_capacity(sorted_addresses.len());
337 let chunk_be = chunk_index.to_be_bytes();
338 for a in sorted_addresses {
339 let mut input = [0u8; 56];
340 input[..32].copy_from_slice(merkle_root.as_bytes());
341 input[32..36].copy_from_slice(&chunk_be);
342 input[36..56].copy_from_slice(a.as_bytes());
343 let derived = blake3::derive_key(SNIP_V2_ASSIGNMENT_CONTEXT, &input);
344 let score = u64::from_be_bytes(derived[..8].try_into().expect("8-byte slice"));
345 scored.push((score, *a));
346 }
347 scored.sort_by(|x, y| {
348 x.0.cmp(&y.0)
349 .then_with(|| x.1.as_bytes().cmp(y.1.as_bytes()))
350 });
351 scored.into_iter().take(r_eff).map(|(_, a)| a).collect()
352}
353
354pub fn is_archive_assigned_to_chunk(
357 merkle_root: &Hash,
358 snapshot_addresses: &[Address],
359 chunk_index: u32,
360 replication_factor: u32,
361 archive: &Address,
362) -> bool {
363 assigned_archives(merkle_root, snapshot_addresses, chunk_index, replication_factor)
364 .iter()
365 .any(|a| a.as_bytes() == archive.as_bytes())
366}
367
368#[cfg(test)]
370mod assignment_tests {
371 use super::*;
372
373 fn fixture_archives() -> [Address; 5] {
376 let mut out = [Address::new([0u8; 20]); 5];
377 for j in 0..5 {
378 let label = format!("snip-v2-archive-{}", j + 1);
379 let h = blake3::hash(label.as_bytes());
380 out[j] = Address::from_slice(&h.as_bytes()[..20]).expect("20 bytes");
381 }
382 out
383 }
384
385 fn fixture_root(i: usize) -> Hash {
388 let label = format!("snip-v2-test-file-{}", i + 1);
389 let h = blake3::hash(label.as_bytes());
390 Hash::from_slice(h.as_bytes()).expect("32 bytes")
391 }
392
393 #[test]
397 fn appendix_c_fixture_construction_matches() {
398 let roots = [fixture_root(0), fixture_root(1), fixture_root(2)];
399 let archives = fixture_archives();
400
401 let expected_roots = [
402 "a5e2668f5022b62b5e4a1342aa0cfbfcbde2af2e3626b2fd57d6cf44e8f615a4",
403 "eed453d08260268bbd3675997f407174d901d842711f3addb6a2e05f776bccce",
404 "81137f39ea2a36bae5333d021052c44c0fc4763769c9988241e6669af16dfa74",
405 ];
406 for (i, h) in expected_roots.iter().enumerate() {
407 assert_eq!(hex::encode(roots[i].as_bytes()), *h, "merkle_root[{}]", i);
408 }
409 let expected_archives = [
410 "37c4401960bd5a26d8ed7b676b1ef47c78fac5bb",
411 "f1a469857483cc381865df996b2cccd254878a16",
412 "8c6a62e786d02ae255a6f481580b95fe05bafffc",
413 "f8967230e6a6d6b5b4ce6816d43f406f24d3cdad",
414 "7e65c99f5b3994f2014187f24ee9230a027526bd",
415 ];
416 for (j, h) in expected_archives.iter().enumerate() {
417 assert_eq!(hex::encode(archives[j].as_bytes()), *h, "archive[{}]", j);
418 }
419 }
420
421 #[test]
425 fn appendix_c_scores_for_root0_chunk0() {
426 let root = fixture_root(0);
427 let archives = fixture_archives();
428 let chunk_be = 0u32.to_be_bytes();
429
430 let cases: [(Address, u64); 5] = [
432 (archives[4], 0x4cd8130d5f5c7f55),
433 (archives[2], 0x73e9ad5ef9a6ba04),
434 (archives[1], 0xc8859dade38f7649),
435 (archives[3], 0xd2823bf6a2d883bb),
436 (archives[0], 0xf3c350979cb3f293),
437 ];
438
439 for (archive, expected_score) in cases.iter() {
440 let mut input = [0u8; 56];
441 input[..32].copy_from_slice(root.as_bytes());
442 input[32..36].copy_from_slice(&chunk_be);
443 input[36..56].copy_from_slice(archive.as_bytes());
444 let derived = blake3::derive_key(SNIP_V2_ASSIGNMENT_CONTEXT, &input);
445 let score = u64::from_be_bytes(derived[..8].try_into().unwrap());
446 assert_eq!(
447 score,
448 *expected_score,
449 "score mismatch for archive {} — most likely cause: wrong context string \
450 (\"{}\" expected) or keyed_hash-vs-derive_key drift",
451 hex::encode(archive.as_bytes()),
452 SNIP_V2_ASSIGNMENT_CONTEXT,
453 );
454 }
455 }
456
457 #[test]
460 fn appendix_c_assignment_outputs() {
461 let snapshot = fixture_archives().to_vec();
462 let r0 = fixture_root(0);
463 let r1 = fixture_root(1);
464 let r2 = fixture_root(2);
465
466 struct Case<'a> {
468 root: &'a Hash,
469 chunk_index: u32,
470 r: u32,
471 expected_hex: &'a [&'a str],
472 }
473 let cases = [
474 Case { root: &r0, chunk_index: 0, r: 1, expected_hex: &[
475 "7e65c99f5b3994f2014187f24ee9230a027526bd",
476 ]},
477 Case { root: &r0, chunk_index: 0, r: 3, expected_hex: &[
478 "7e65c99f5b3994f2014187f24ee9230a027526bd",
479 "8c6a62e786d02ae255a6f481580b95fe05bafffc",
480 "f1a469857483cc381865df996b2cccd254878a16",
481 ]},
482 Case { root: &r0, chunk_index: 7, r: 3, expected_hex: &[
483 "f8967230e6a6d6b5b4ce6816d43f406f24d3cdad",
484 "37c4401960bd5a26d8ed7b676b1ef47c78fac5bb",
485 "7e65c99f5b3994f2014187f24ee9230a027526bd",
486 ]},
487 Case { root: &r1, chunk_index: 0, r: 3, expected_hex: &[
488 "f1a469857483cc381865df996b2cccd254878a16",
489 "8c6a62e786d02ae255a6f481580b95fe05bafffc",
490 "37c4401960bd5a26d8ed7b676b1ef47c78fac5bb",
491 ]},
492 Case { root: &r1, chunk_index: 1, r: 3, expected_hex: &[
493 "7e65c99f5b3994f2014187f24ee9230a027526bd",
494 "8c6a62e786d02ae255a6f481580b95fe05bafffc",
495 "f8967230e6a6d6b5b4ce6816d43f406f24d3cdad",
496 ]},
497 Case { root: &r2, chunk_index: 42, r: 3, expected_hex: &[
498 "f1a469857483cc381865df996b2cccd254878a16",
499 "8c6a62e786d02ae255a6f481580b95fe05bafffc",
500 "f8967230e6a6d6b5b4ce6816d43f406f24d3cdad",
501 ]},
502 Case { root: &r2, chunk_index: 42, r: 5, expected_hex: &[
504 "f1a469857483cc381865df996b2cccd254878a16",
505 "8c6a62e786d02ae255a6f481580b95fe05bafffc",
506 "f8967230e6a6d6b5b4ce6816d43f406f24d3cdad",
507 "37c4401960bd5a26d8ed7b676b1ef47c78fac5bb",
508 "7e65c99f5b3994f2014187f24ee9230a027526bd",
509 ]},
510 Case { root: &r2, chunk_index: 42, r: 7, expected_hex: &[
512 "f1a469857483cc381865df996b2cccd254878a16",
513 "8c6a62e786d02ae255a6f481580b95fe05bafffc",
514 "f8967230e6a6d6b5b4ce6816d43f406f24d3cdad",
515 "37c4401960bd5a26d8ed7b676b1ef47c78fac5bb",
516 "7e65c99f5b3994f2014187f24ee9230a027526bd",
517 ]},
518 ];
519
520 for c in &cases {
521 let got = assigned_archives(c.root, &snapshot, c.chunk_index, c.r);
522 let got_hex: Vec<String> =
523 got.iter().map(|a| hex::encode(a.as_bytes())).collect();
524 let want_hex: Vec<String> = c.expected_hex.iter().map(|s| s.to_string()).collect();
525 assert_eq!(
526 got_hex, want_hex,
527 "case (root={}, chunk_index={}, R={}) — assignment drift",
528 hex::encode(c.root.as_bytes()),
529 c.chunk_index,
530 c.r,
531 );
532 }
533 }
534
535 #[test]
539 fn assigned_archives_is_snapshot_order_independent() {
540 let mut snap_a = fixture_archives().to_vec();
541 let mut snap_b = snap_a.clone();
542 snap_b.reverse();
543 let snap_c = vec![snap_a[2], snap_a[0], snap_a[4], snap_a[1], snap_a[3]];
544
545 let root = fixture_root(2);
546 let a = assigned_archives(&root, &snap_a, 42, 3);
547 let b = assigned_archives(&root, &snap_b, 42, 3);
548 let c = assigned_archives(&root, &snap_c, 42, 3);
549 assert_eq!(a, b);
550 assert_eq!(a, c);
551
552 snap_a.push(snap_a[0]);
554 snap_a.push(snap_a[2]);
555 let d = assigned_archives(&root, &snap_a, 42, 3);
556 assert_eq!(a, d);
557 }
558
559 #[test]
561 fn is_archive_assigned_matches_assigned_archives() {
562 let snap = fixture_archives().to_vec();
563 let root = fixture_root(0);
564 let assigned = assigned_archives(&root, &snap, 0, 3);
565 for a in &snap {
566 let in_set = assigned.iter().any(|x| x.as_bytes() == a.as_bytes());
567 assert_eq!(
568 is_archive_assigned_to_chunk(&root, &snap, 0, 3, a),
569 in_set,
570 "is_archive_assigned_to_chunk disagrees with assigned_archives for {}",
571 hex::encode(a.as_bytes()),
572 );
573 }
574 }
575
576 #[test]
578 fn empty_snapshot_returns_empty() {
579 let root = fixture_root(0);
580 assert!(assigned_archives(&root, &[], 0, 3).is_empty());
581 }
582}
583
584#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
587pub struct StorageMetadataV2 {
588 pub merkle_root: Hash,
589 pub owner: Address,
590 pub plaintext_size_bytes: u64,
591 pub stored_size_bytes: u64,
592 pub chunk_count: u32,
593 pub fee_pool: u64,
596 pub created_at: u64,
598 pub activated_at_height: Option<u64>,
600 pub abandoned_at_height: Option<u64>,
604 pub assignment_height: u64,
607 pub visibility: FileVisibilityV2,
608 pub lifecycle: FileLifecycleV2,
609 pub access_list: Vec<AccessEntryV2>,
610 pub predecessor_root: Option<Hash>,
612}