use crate::imp::core::Bytes32;
use alloc::vec;
use alloc::vec::Vec;
use chacha20::cipher::{KeyIvInit, StreamCipher};
use chacha20::ChaCha20;
use sha2::{Digest, Sha256};
const MIN_SIZE: usize = 1024;
const MAX_SIZE: usize = 256 * 1024;
const OCTAVES: u64 = 8;
fn seed(retrieval_key: &Bytes32, tag: &[u8]) -> [u8; 32] {
let mut h = Sha256::new();
h.update(retrieval_key.0);
h.update(tag);
let out = h.finalize();
let mut s = [0u8; 32];
s.copy_from_slice(&out);
s
}
fn stream(seed: [u8; 32], len: usize) -> Vec<u8> {
let mut buf = vec![0u8; len];
let nonce = [0u8; 12]; let mut c = ChaCha20::new(&seed.into(), &nonce.into());
c.apply_keystream(&mut buf);
buf
}
fn size_of(v: u64) -> usize {
let octave = (v >> 61) % OCTAVES; let frac = v & 0xFFFF_FFFF;
let band_lo = (MIN_SIZE as u64) << octave; let band_hi = (band_lo * 2).min(MAX_SIZE as u64);
let span = band_hi - band_lo;
let offset = if span == 0 { 0 } else { (frac * span) >> 32 };
let size = (band_lo + offset) as usize;
size.clamp(MIN_SIZE, MAX_SIZE)
}
pub fn decoy_size(retrieval_key: &Bytes32) -> usize {
let s = seed(retrieval_key, b"digstore-decoy-size-v1");
let mut raw = [0u8; 8];
raw.copy_from_slice(&s[0..8]);
let v = u64::from_be_bytes(raw);
size_of(v)
}
pub fn decoy_bytes(retrieval_key: &Bytes32) -> Vec<u8> {
let n = decoy_size(retrieval_key);
stream(seed(retrieval_key, b"digstore-decoy-bytes-v1"), n)
}
pub fn decoy_proof_blob(retrieval_key: &Bytes32) -> Vec<u8> {
stream(seed(retrieval_key, b"digstore-decoy-proof-v1"), 256)
}
use crate::imp::core::{ContentResponse, MerkleProof, ProofStep};
const CHUNK_TARGET: usize = 64 * 1024;
fn decoy_chunk_lens(retrieval_key: &Bytes32, total: usize) -> Vec<u32> {
if total == 0 {
return Vec::new();
}
if total <= CHUNK_TARGET {
return vec![total as u32];
}
let bytes = stream(
seed(retrieval_key, b"digstore-decoy-chunklens-v1"),
4 * (total / CHUNK_TARGET + 2),
);
let mut lens: Vec<u32> = Vec::new();
let mut rem = total;
let mut i = 0usize;
while rem > CHUNK_TARGET {
let r = u32::from_be_bytes([bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]]) as usize;
i += 4;
let mut size = CHUNK_TARGET / 2 + (r % CHUNK_TARGET);
if rem.saturating_sub(size) < CHUNK_TARGET / 2 {
size = rem;
}
lens.push(size as u32);
rem -= size;
}
if rem > 0 {
lens.push(rem as u32);
}
lens
}
pub fn decoy_content_response(retrieval_key: &Bytes32, root: &Bytes32) -> ContentResponse {
let ciphertext = decoy_bytes(retrieval_key);
let chunk_lens = decoy_chunk_lens(retrieval_key, ciphertext.len());
let leaf_seed = seed(retrieval_key, b"digstore-decoy-leaf-v1");
let step_seed = seed(retrieval_key, b"digstore-decoy-step-v1");
let path = alloc::vec![ProofStep {
hash: Bytes32(step_seed),
is_left: (step_seed[0] & 1) == 1,
}];
let merkle_proof = MerkleProof {
leaf: Bytes32(leaf_seed),
path,
root: *root,
};
ContentResponse {
ciphertext,
merkle_proof,
roothash: *root,
chunk_lens,
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
#[test]
fn octave_uses_top_three_bits() {
for octave in 0..OCTAVES {
let v = octave << 61;
let expected_floor = (MIN_SIZE as u64) << octave;
assert_eq!(
size_of(v) as u64,
expected_floor,
"top 3 bits = {octave} must select band floor {expected_floor}"
);
}
}
#[test]
fn middle_bits_are_dead() {
let base = 0u64; let size_base = size_of(base);
for bit in 32..=60u32 {
let v = base | (1u64 << bit);
assert_eq!(
size_of(v),
size_base,
"flipping bit {bit} must not change the size (no middle-bit signal)"
);
}
}
}