From 9c382585b0fc371c34eef1dbed2e87dc1c0ddf5d Mon Sep 17 00:00:00 2001 From: Toufic Date: Mon, 16 Feb 2026 10:40:44 +0400 Subject: [PATCH 01/10] update ipfs link --- README.md | 12 ++++++------ src/ctrng/ipfs.rs | 2 +- tests/ipfs_live.rs | 2 +- tests/mixed_tests.rs | 2 +- tests/testu01_tests.rs | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index dc9549d..73a826b 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,8 @@ crypto-ctrng = { git = "https://github.com/spacecomputer-io/crypto-ctrng.git", t ```rust use crypto_ctrng::{IpfsCtrng, RandomBlockSource}; -let gateway = "https://ipfs.io"; -let beacon_key = "k2k4r8pigrw8i34z63om8f015tt5igdq0c46xupq8spp1bogt35k5vhe"; +let gateway = "https://ipfs.filebase.io/ipns/"; +let beacon_key = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn09f"; let mut ctrng = IpfsCtrng::new(gateway, beacon_key); let block = ctrng.next_block().expect("failed to fetch IPFS block"); @@ -58,8 +58,8 @@ let block = ctrng.next_block().expect("failed to fetch IPFS block"); use crypto_ctrng::{IpfsCtrng, MixedCtrng, ReseedingRng}; use rand_core::RngCore; -let gateway = "https://ipfs.io"; -let beacon_key = "k2k4r8pigrw8i34z63om8f015tt5igdq0c46xupq8spp1bogt35k5vhe"; +let gateway = "https://ipfs.filebase.io/ipns/"; +let beacon_key = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn09f"; let ipfs = IpfsCtrng::new(gateway, beacon_key); let mixed = MixedCtrng::new(ipfs).unwrap(); @@ -75,8 +75,8 @@ rng.fill_bytes(&mut key); use std::time::Duration; use crypto_ctrng::{IpfsCtrng, MixedCtrng, ReseedConfig, ReseedingRng}; -let gateway = "https://ipfs.io"; -let beacon_key = "k2k4r8pigrw8i34z63om8f015tt5igdq0c46xupq8spp1bogt35k5vhe"; +let gateway = "https://ipfs.filebase.io/ipns/"; +let beacon_key = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn09f"; let ipfs = IpfsCtrng::new(gateway, beacon_key); let mixed = MixedCtrng::new(ipfs).unwrap(); diff --git a/src/ctrng/ipfs.rs b/src/ctrng/ipfs.rs index b2e381d..14d9cb9 100644 --- a/src/ctrng/ipfs.rs +++ b/src/ctrng/ipfs.rs @@ -30,7 +30,7 @@ impl IpfsCtrng { fn refill_cache(&mut self) -> Result<(), SourceError> { let url = format!( - "{}/ipns/{}", + "{}/{}", self.gateway_url.trim_end_matches('/'), self.beacon_key ); diff --git a/tests/ipfs_live.rs b/tests/ipfs_live.rs index 2554693..6c2e861 100644 --- a/tests/ipfs_live.rs +++ b/tests/ipfs_live.rs @@ -1,7 +1,7 @@ use crypto_ctrng::RandomBlockSource; const BEACON_KEY: &str = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn09f"; -const GATEWAY: &str = "https://ipfs.io"; +const GATEWAY: &str = "https://ipfs.filebase.io/ipns/"; #[test] #[ignore] diff --git a/tests/mixed_tests.rs b/tests/mixed_tests.rs index 37def64..c3ef66a 100644 --- a/tests/mixed_tests.rs +++ b/tests/mixed_tests.rs @@ -1,7 +1,7 @@ use crypto_ctrng::RandomBlockSource; const BEACON_KEY: &str = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn09f"; -const GATEWAY: &str = "https://ipfs.io"; +const GATEWAY: &str = "https://ipfs.filebase.io/ipns/"; #[test] fn local_is_32_bytes() { diff --git a/tests/testu01_tests.rs b/tests/testu01_tests.rs index c0fd4a6..76f5aba 100644 --- a/tests/testu01_tests.rs +++ b/tests/testu01_tests.rs @@ -11,7 +11,7 @@ use testu01_runner::{delete_unif01_gen, make_unif01_gen, register_rng}; #[cfg(feature = "testu01")] fn build_ipfs_seeded_rng() -> ChaCha20Rng { - let gateway = "https://ipfs.io"; + let gateway = "https://ipfs.filebase.io/ipns/"; let beacon_key = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn09f"; let mut ctrng = IpfsCtrng::new(gateway, beacon_key); let seed = ctrng.next_block().expect("failed to fetch IPFS block"); From db3e213353f5347cc0e6d594b372b89ee227dd8a Mon Sep 17 00:00:00 2001 From: Toufic Date: Mon, 16 Feb 2026 10:53:18 +0400 Subject: [PATCH 02/10] consistency --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 73a826b..2f8b4e3 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,8 @@ This crate's responsibility is to **verify** that the cTRNG backend provides uni | Mode | Code | DRBG | Reseed | |------|------|------|--------| -| Raw entropy | `BlockRng::new(source)` | ❌ | N/A | -| Seeded DRBG (testing) | `ChaCha20Rng::from_seed(seed)` | ChaCha20 | ❌ | +| Raw entropy | `BlockRng::new(source)` | N/A | N/A | +| Seeded DRBG (testing) | `ChaCha20Rng::from_seed(seed)` | ChaCha20 | N/A | | DRBG + auto reseed | `ReseedingRng::new(source)` | ChaCha20 | 3200 bytes / 1 week | | DRBG + prediction resistance | `ReseedingRng::with_prediction_resistance(source)` | ChaCha20 | every call | From fac38b92729e1c17259005a0c91a745f5fa6fa47 Mon Sep 17 00:00:00 2001 From: Toufic Date: Mon, 16 Feb 2026 14:41:24 +0400 Subject: [PATCH 03/10] fallback --- README.md | 34 ++++++++++++++++++- src/ctrng/ipfs.rs | 85 +++++++++++++++++++++++++++++++++++++++------- src/lib.rs | 2 +- tests/ipfs_live.rs | 52 ++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 2f8b4e3..fc47ca4 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ To use this crate, add it to your `Cargo.toml`: crypto-ctrng = { git = "https://github.com/spacecomputer-io/crypto-ctrng.git", tag = "v0.1.0" } ``` -### Fetch raw entropy +### Fetch raw entropy (single gateway) ```rust use crypto_ctrng::{IpfsCtrng, RandomBlockSource}; @@ -52,6 +52,38 @@ let mut ctrng = IpfsCtrng::new(gateway, beacon_key); let block = ctrng.next_block().expect("failed to fetch IPFS block"); ``` +### Multi-gateway with automatic fallback + +If a gateway is unresponsive or down, the next one in the list is tried automatically. +The default timeout is 10 seconds per gateway; pass a custom `Duration` to override. + +```rust +use std::time::Duration; +use crypto_ctrng::{IpfsCtrng, RandomBlockSource, DEFAULT_GATEWAYS}; + +let beacon_key = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn09f"; + +// Use the built-in default gateways (ipfs.io, ipfs.filebase.io, dweb.link) +let mut ctrng = IpfsCtrng::new_with_gateways( + DEFAULT_GATEWAYS.iter().copied(), + beacon_key, + None, // default 10s timeout +); +let block = ctrng.next_block().expect("at least one gateway should respond"); + +// Or provide your own list with a custom timeout +let gateways = vec![ + "https://ipfs.filebase.io/ipns/", + "https://ipfs.io/ipns/", + "https://dweb.link/ipns/", +]; +let mut ctrng = IpfsCtrng::new_with_gateways( + gateways, + beacon_key, + Some(Duration::from_secs(5)), +); +``` + ### DRBG with automatic reseeding (recommended) ```rust diff --git a/src/ctrng/ipfs.rs b/src/ctrng/ipfs.rs index 14d9cb9..eebe0c5 100644 --- a/src/ctrng/ipfs.rs +++ b/src/ctrng/ipfs.rs @@ -1,13 +1,24 @@ +use std::time::Duration; + use crate::error::SourceError; use crate::traits::RandomBlockSource; use super::types::{BeaconResponse, CtrngBlock}; +pub const DEFAULT_GATEWAYS: &[&str] = &[ + "https://ipfs.io/ipns/", + "https://ipfs.filebase.io/ipns/", + "https://dweb.link/ipns/", +]; + +pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); + /// cTRNG backed by an IPFS beacon. #[derive(Debug)] pub struct IpfsCtrng { - gateway_url: String, + gateways: Vec, beacon_key: String, + timeout: Duration, cache: Vec, cursor: usize, last_served: Option, @@ -15,9 +26,27 @@ pub struct IpfsCtrng { impl IpfsCtrng { pub fn new(gateway_url: impl Into, beacon_key: impl Into) -> Self { + let mut gateways = vec![gateway_url.into()]; + gateways.extend(DEFAULT_GATEWAYS.iter().map(|s| s.to_string())); + Self { + gateways, + beacon_key: beacon_key.into(), + timeout: DEFAULT_TIMEOUT, + cache: Vec::new(), + cursor: 0, + last_served: None, + } + } + + pub fn new_with_gateways( + gateways: impl IntoIterator>, + beacon_key: impl Into, + timeout: Option, + ) -> Self { Self { - gateway_url: gateway_url.into(), + gateways: gateways.into_iter().map(Into::into).collect(), beacon_key: beacon_key.into(), + timeout: timeout.unwrap_or(DEFAULT_TIMEOUT), cache: Vec::new(), cursor: 0, last_served: None, @@ -28,24 +57,56 @@ impl IpfsCtrng { self.last_served } - fn refill_cache(&mut self) -> Result<(), SourceError> { + fn try_gateway(&self, gateway: &str) -> Result, String> { let url = format!( "{}/{}", - self.gateway_url.trim_end_matches('/'), + gateway.trim_end_matches('/'), self.beacon_key ); - let resp = reqwest::blocking::get(&url) - .map_err(|e| SourceError::ctrng(format!("ipfs fetch failed: {e}")))?; + + let client = reqwest::blocking::Client::builder() + .timeout(self.timeout) + .build() + .map_err(|e| format!("{gateway}: client build failed: {e}"))?; + + let resp = client + .get(&url) + .send() + .map_err(|e| format!("{gateway}: fetch failed: {e}"))?; + if !resp.status().is_success() { - return Err(SourceError::ctrng(format!( - "ipfs returned HTTP {}", - resp.status() - ))); + return Err(format!("{gateway}: HTTP {}", resp.status())); } + let bytes = resp .bytes() - .map_err(|e| SourceError::ctrng(format!("ipfs body read failed: {e}")))?; - let parsed: BeaconResponse = serde_json::from_slice(&bytes) + .map_err(|e| format!("{gateway}: body read failed: {e}"))?; + + Ok(bytes.to_vec()) + } + + fn refill_cache(&mut self) -> Result<(), SourceError> { + let mut errors = Vec::new(); + + for gateway in &self.gateways { + match self.try_gateway(gateway) { + Ok(bytes) => { + return self.parse_and_store(&bytes); + } + Err(e) => { + errors.push(e); + } + } + } + + Err(SourceError::ctrng(format!( + "all gateways failed: [{}]", + errors.join(", ") + ))) + } + + fn parse_and_store(&mut self, bytes: &[u8]) -> Result<(), SourceError> { + let parsed: BeaconResponse = serde_json::from_slice(bytes) .map_err(|e| SourceError::ctrng(format!("ipfs json parse failed: {e}")))?; let mut new_cache = Vec::with_capacity(parsed.data.ctrng.len()); diff --git a/src/lib.rs b/src/lib.rs index d3c4a43..98f5734 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ pub mod reseed; pub mod rng; pub mod traits; -pub use ctrng::ipfs::IpfsCtrng; +pub use ctrng::ipfs::{DEFAULT_GATEWAYS, DEFAULT_TIMEOUT, IpfsCtrng}; pub use derive::derive_seed; pub use error::SourceError; pub use local::LocalRng; diff --git a/tests/ipfs_live.rs b/tests/ipfs_live.rs index 6c2e861..7851262 100644 --- a/tests/ipfs_live.rs +++ b/tests/ipfs_live.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use crypto_ctrng::RandomBlockSource; const BEACON_KEY: &str = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn09f"; @@ -35,3 +37,53 @@ fn ipfs_unique_blocks() { } } } + +#[test] +#[ignore] +fn ipfs_multi_gateway_fallback() { + // First gateway is invalid, second is valid — should fall back successfully + let gateways = vec![ + "https://invalid-gateway.example.com/ipns/", + GATEWAY, + ]; + let mut client = crypto_ctrng::IpfsCtrng::new_with_gateways( + gateways, + BEACON_KEY, + Some(Duration::from_secs(5)), + ); + let block = client.next_block().expect("fallback should succeed"); + assert_eq!(block.len(), 32); +} + +#[test] +#[ignore] +fn ipfs_all_gateways_fail() { + let gateways = vec![ + "https://invalid-gateway1.example.com/ipns/", + "https://invalid-gateway2.example.com/ipns/", + ]; + let mut client = crypto_ctrng::IpfsCtrng::new_with_gateways( + gateways, + BEACON_KEY, + Some(Duration::from_secs(3)), + ); + let result = client.next_block(); + assert!(result.is_err(), "all gateways should fail"); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("all gateways failed"), + "error should mention all gateways failed: {err}" + ); +} + +#[test] +#[ignore] +fn ipfs_default_gateways_list() { + let mut client = crypto_ctrng::IpfsCtrng::new_with_gateways( + crypto_ctrng::DEFAULT_GATEWAYS.iter().copied(), + BEACON_KEY, + None, + ); + let block = client.next_block().expect("default gateways should work"); + assert_eq!(block.len(), 32); +} From 087279356594e870ae9eb36f1c6371e238a8793d Mon Sep 17 00:00:00 2001 From: Toufic Date: Thu, 5 Mar 2026 13:16:07 +0100 Subject: [PATCH 04/10] update gateway --- README.md | 2 +- src/ctrng/ipfs.rs | 45 ++++++++++++++++++++++++++++++++------------- src/lib.rs | 2 +- tests/ipfs_live.rs | 8 ++++---- 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index fc47ca4..03f3eb3 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ This crate's responsibility is to **verify** that the cTRNG backend provides uni - `MixedCtrng` combines entropy sources (XOR of IPFS beacon + local OS randomness) for enhanced security, but still relies on the gateway for uniqueness guarantees. **Uniqueness guarantees:** -- This crate ensures that each `next_block()` call returns a block with a timestamp strictly greater than the previous one, preventing global block reuse (I thought so, but according to today's bug, we can have same ctrng for two different timestamp, so we need to find another way). +- This crate ensures that each `next_block()` call returns a block with a timestamp strictly greater than the previous one, preventing global block reuse. - The calling library must use `derive_seed()` to personalize blocks per user/execution to prevent collisions between different users reading the same raw block. ## Usage modes diff --git a/src/ctrng/ipfs.rs b/src/ctrng/ipfs.rs index eebe0c5..140372e 100644 --- a/src/ctrng/ipfs.rs +++ b/src/ctrng/ipfs.rs @@ -6,17 +6,40 @@ use crate::traits::RandomBlockSource; use super::types::{BeaconResponse, CtrngBlock}; pub const DEFAULT_GATEWAYS: &[&str] = &[ - "https://ipfs.io/ipns/", - "https://ipfs.filebase.io/ipns/", - "https://dweb.link/ipns/", + "https://ipfs.io", + "https://ipfs.filebase.io", + "https://dweb.link", ]; pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); +#[derive(Debug, Clone)] +pub struct IpfsGateway(String); + +impl IpfsGateway { + pub fn new(url: impl Into) -> Self { + Self(url.into().trim_end_matches('/').to_string()) + } + + pub fn ipfs(&self, key: &str) -> String { + format!("{}/ipfs/{}", self.0, key) + } + + pub fn ipns(&self, key: &str) -> String { + format!("{}/ipns/{}", self.0, key) + } +} + +impl std::fmt::Display for IpfsGateway { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + /// cTRNG backed by an IPFS beacon. #[derive(Debug)] pub struct IpfsCtrng { - gateways: Vec, + gateways: Vec, beacon_key: String, timeout: Duration, cache: Vec, @@ -26,8 +49,8 @@ pub struct IpfsCtrng { impl IpfsCtrng { pub fn new(gateway_url: impl Into, beacon_key: impl Into) -> Self { - let mut gateways = vec![gateway_url.into()]; - gateways.extend(DEFAULT_GATEWAYS.iter().map(|s| s.to_string())); + let mut gateways = vec![IpfsGateway::new(gateway_url)]; + gateways.extend(DEFAULT_GATEWAYS.iter().map(|s| IpfsGateway::new(*s))); Self { gateways, beacon_key: beacon_key.into(), @@ -44,7 +67,7 @@ impl IpfsCtrng { timeout: Option, ) -> Self { Self { - gateways: gateways.into_iter().map(Into::into).collect(), + gateways: gateways.into_iter().map(IpfsGateway::new).collect(), beacon_key: beacon_key.into(), timeout: timeout.unwrap_or(DEFAULT_TIMEOUT), cache: Vec::new(), @@ -57,12 +80,8 @@ impl IpfsCtrng { self.last_served } - fn try_gateway(&self, gateway: &str) -> Result, String> { - let url = format!( - "{}/{}", - gateway.trim_end_matches('/'), - self.beacon_key - ); + fn try_gateway(&self, gateway: &IpfsGateway) -> Result, String> { + let url = gateway.ipns(&self.beacon_key); let client = reqwest::blocking::Client::builder() .timeout(self.timeout) diff --git a/src/lib.rs b/src/lib.rs index 98f5734..6511d57 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ pub mod reseed; pub mod rng; pub mod traits; -pub use ctrng::ipfs::{DEFAULT_GATEWAYS, DEFAULT_TIMEOUT, IpfsCtrng}; +pub use ctrng::ipfs::{DEFAULT_GATEWAYS, DEFAULT_TIMEOUT, IpfsCtrng, IpfsGateway}; pub use derive::derive_seed; pub use error::SourceError; pub use local::LocalRng; diff --git a/tests/ipfs_live.rs b/tests/ipfs_live.rs index 7851262..90c48c1 100644 --- a/tests/ipfs_live.rs +++ b/tests/ipfs_live.rs @@ -3,7 +3,7 @@ use std::time::Duration; use crypto_ctrng::RandomBlockSource; const BEACON_KEY: &str = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn09f"; -const GATEWAY: &str = "https://ipfs.filebase.io/ipns/"; +const GATEWAY: &str = "https://ipfs.filebase.io"; #[test] #[ignore] @@ -43,7 +43,7 @@ fn ipfs_unique_blocks() { fn ipfs_multi_gateway_fallback() { // First gateway is invalid, second is valid — should fall back successfully let gateways = vec![ - "https://invalid-gateway.example.com/ipns/", + "https://invalid-gateway.example.com", GATEWAY, ]; let mut client = crypto_ctrng::IpfsCtrng::new_with_gateways( @@ -59,8 +59,8 @@ fn ipfs_multi_gateway_fallback() { #[ignore] fn ipfs_all_gateways_fail() { let gateways = vec![ - "https://invalid-gateway1.example.com/ipns/", - "https://invalid-gateway2.example.com/ipns/", + "https://invalid-gateway1.example.com", + "https://invalid-gateway2.example.com", ]; let mut client = crypto_ctrng::IpfsCtrng::new_with_gateways( gateways, From 29ceea1c4b5d11a7ce5ec0882b94dcb774870319 Mon Sep 17 00:00:00 2001 From: Toufic Date: Thu, 5 Mar 2026 17:13:47 +0100 Subject: [PATCH 05/10] ctrng abstraction + gateway unification --- README.md | 48 +++++++++++++++++-------------------- src/ctrng/ipfs.rs | 54 +++++++++++++++++++++++++++++------------- src/ctrng/mod.rs | 42 +++++++++++++++++++++++++++++++- src/lib.rs | 3 ++- tests/ipfs_live.rs | 38 +++++++++++++++-------------- tests/mixed_tests.rs | 10 ++++---- tests/testu01_tests.rs | 6 ++--- 7 files changed, 130 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 03f3eb3..1c045d0 100644 --- a/README.md +++ b/README.md @@ -40,60 +40,57 @@ To use this crate, add it to your `Cargo.toml`: crypto-ctrng = { git = "https://github.com/spacecomputer-io/crypto-ctrng.git", tag = "v0.1.0" } ``` -### Fetch raw entropy (single gateway) +### Fetch raw entropy (default gateways) ```rust -use crypto_ctrng::{IpfsCtrng, RandomBlockSource}; +use crypto_ctrng::{Ctrng, RandomBlockSource}; -let gateway = "https://ipfs.filebase.io/ipns/"; let beacon_key = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn09f"; -let mut ctrng = IpfsCtrng::new(gateway, beacon_key); +let mut ctrng = Ctrng::ipfs([], beacon_key, None); let block = ctrng.next_block().expect("failed to fetch IPFS block"); ``` -### Multi-gateway with automatic fallback +### Custom gateways with automatic fallback If a gateway is unresponsive or down, the next one in the list is tried automatically. -The default timeout is 10 seconds per gateway; pass a custom `Duration` to override. +The default timeout is 10 seconds per gateway. ```rust +use crypto_ctrng::{Ctrng, IpfsConfig, RandomBlockSource}; use std::time::Duration; -use crypto_ctrng::{IpfsCtrng, RandomBlockSource, DEFAULT_GATEWAYS}; let beacon_key = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn09f"; -// Use the built-in default gateways (ipfs.io, ipfs.filebase.io, dweb.link) -let mut ctrng = IpfsCtrng::new_with_gateways( - DEFAULT_GATEWAYS.iter().copied(), +// Custom gateways + default gateways appended automatically +let mut ctrng = Ctrng::ipfs( + ["https://my-gateway.example.com"], beacon_key, - None, // default 10s timeout + None, ); let block = ctrng.next_block().expect("at least one gateway should respond"); -// Or provide your own list with a custom timeout -let gateways = vec![ - "https://ipfs.filebase.io/ipns/", - "https://ipfs.io/ipns/", - "https://dweb.link/ipns/", -]; -let mut ctrng = IpfsCtrng::new_with_gateways( - gateways, +// Custom gateways only (no defaults), with a custom timeout +let config = IpfsConfig { + use_defaults: false, + timeout: Duration::from_secs(5), +}; +let mut ctrng = Ctrng::ipfs( + ["https://ipfs.filebase.io", "https://ipfs.io"], beacon_key, - Some(Duration::from_secs(5)), + Some(config), ); ``` ### DRBG with automatic reseeding (recommended) ```rust -use crypto_ctrng::{IpfsCtrng, MixedCtrng, ReseedingRng}; +use crypto_ctrng::{Ctrng, MixedCtrng, ReseedingRng}; use rand_core::RngCore; -let gateway = "https://ipfs.filebase.io/ipns/"; let beacon_key = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn09f"; -let ipfs = IpfsCtrng::new(gateway, beacon_key); +let ipfs = Ctrng::ipfs([], beacon_key, None); let mixed = MixedCtrng::new(ipfs).unwrap(); let mut rng = ReseedingRng::new(mixed).unwrap(); @@ -105,12 +102,11 @@ rng.fill_bytes(&mut key); ```rust use std::time::Duration; -use crypto_ctrng::{IpfsCtrng, MixedCtrng, ReseedConfig, ReseedingRng}; +use crypto_ctrng::{Ctrng, MixedCtrng, ReseedConfig, ReseedingRng}; -let gateway = "https://ipfs.filebase.io/ipns/"; let beacon_key = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn09f"; -let ipfs = IpfsCtrng::new(gateway, beacon_key); +let ipfs = Ctrng::ipfs([], beacon_key, None); let mixed = MixedCtrng::new(ipfs).unwrap(); let config = ReseedConfig::new(1600, Duration::from_secs(3600)); // 50 blocks, 1 hour diff --git a/src/ctrng/ipfs.rs b/src/ctrng/ipfs.rs index 140372e..34955b1 100644 --- a/src/ctrng/ipfs.rs +++ b/src/ctrng/ipfs.rs @@ -13,6 +13,22 @@ pub const DEFAULT_GATEWAYS: &[&str] = &[ pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); +/// Configuration for IPFS-backed cTRNG. +#[derive(Debug, Clone)] +pub struct IpfsConfig { + pub use_defaults: bool, + pub timeout: Duration, +} + +impl Default for IpfsConfig { + fn default() -> Self { + Self { + use_defaults: true, + timeout: DEFAULT_TIMEOUT, + } + } +} + #[derive(Debug, Clone)] pub struct IpfsGateway(String); @@ -36,6 +52,12 @@ impl std::fmt::Display for IpfsGateway { } } +impl PartialEq for IpfsGateway { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + /// cTRNG backed by an IPFS beacon. #[derive(Debug)] pub struct IpfsCtrng { @@ -48,28 +70,26 @@ pub struct IpfsCtrng { } impl IpfsCtrng { - pub fn new(gateway_url: impl Into, beacon_key: impl Into) -> Self { - let mut gateways = vec![IpfsGateway::new(gateway_url)]; - gateways.extend(DEFAULT_GATEWAYS.iter().map(|s| IpfsGateway::new(*s))); - Self { - gateways, - beacon_key: beacon_key.into(), - timeout: DEFAULT_TIMEOUT, - cache: Vec::new(), - cursor: 0, - last_served: None, - } - } - - pub fn new_with_gateways( + pub fn new( gateways: impl IntoIterator>, beacon_key: impl Into, - timeout: Option, + config: Option, ) -> Self { + let config = config.unwrap_or_default(); + let mut gw_list: Vec = + gateways.into_iter().map(IpfsGateway::new).collect(); + if config.use_defaults { + for default in DEFAULT_GATEWAYS { + let gw = IpfsGateway::new(*default); + if !gw_list.contains(&gw) { + gw_list.push(gw); + } + } + } Self { - gateways: gateways.into_iter().map(IpfsGateway::new).collect(), + gateways: gw_list, beacon_key: beacon_key.into(), - timeout: timeout.unwrap_or(DEFAULT_TIMEOUT), + timeout: config.timeout, cache: Vec::new(), cursor: 0, last_served: None, diff --git a/src/ctrng/mod.rs b/src/ctrng/mod.rs index 2196ed7..3b84ad7 100644 --- a/src/ctrng/mod.rs +++ b/src/ctrng/mod.rs @@ -1,2 +1,42 @@ -pub mod ipfs; +pub(crate) mod ipfs; pub mod types; + +use ipfs::{IpfsConfig, IpfsCtrng}; +use types::CtrngBlock; +use crate::error::SourceError; +use crate::traits::RandomBlockSource; + +/// Unified cTRNG abstraction supporting multiple backends. +#[derive(Debug)] +pub enum Ctrng { + Ipfs(IpfsCtrng), + // Future: DataHeaven(DataHeavenCtrng), +} + +impl Ctrng { + /// Create an IPFS cTRNG source. + /// + /// Pass `None` for default config (default gateways appended, 10s timeout). + pub fn ipfs( + gateways: impl IntoIterator>, + beacon_key: impl Into, + config: Option, + ) -> Self { + Self::Ipfs(IpfsCtrng::new(gateways, beacon_key, config)) + } + + /// Returns the last block served, if any. + pub fn last_served_block(&self) -> Option { + match self { + Self::Ipfs(ipfs) => ipfs.last_served_block(), + } + } +} + +impl RandomBlockSource for Ctrng { + fn next_block(&mut self) -> Result<[u8; 32], SourceError> { + match self { + Self::Ipfs(ipfs) => ipfs.next_block(), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 6511d57..9dae946 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,8 @@ pub mod reseed; pub mod rng; pub mod traits; -pub use ctrng::ipfs::{DEFAULT_GATEWAYS, DEFAULT_TIMEOUT, IpfsCtrng, IpfsGateway}; +pub use ctrng::ipfs::{DEFAULT_GATEWAYS, DEFAULT_TIMEOUT, IpfsConfig, IpfsGateway}; +pub use ctrng::Ctrng; pub use derive::derive_seed; pub use error::SourceError; pub use local::LocalRng; diff --git a/tests/ipfs_live.rs b/tests/ipfs_live.rs index 90c48c1..d43c2cd 100644 --- a/tests/ipfs_live.rs +++ b/tests/ipfs_live.rs @@ -8,7 +8,7 @@ const GATEWAY: &str = "https://ipfs.filebase.io"; #[test] #[ignore] fn ipfs_client_can_fetch_live_block() { - let mut client = crypto_ctrng::IpfsCtrng::new(GATEWAY, BEACON_KEY); + let mut client = crypto_ctrng::Ctrng::ipfs([GATEWAY], BEACON_KEY, None); let block = client.next_block().expect("fetch 32-byte block"); assert_eq!(block.len(), 32); } @@ -16,7 +16,7 @@ fn ipfs_client_can_fetch_live_block() { #[test] #[ignore] fn ipfs_unique_blocks() { - let mut client = crypto_ctrng::IpfsCtrng::new(GATEWAY, BEACON_KEY); + let mut client = crypto_ctrng::Ctrng::ipfs([GATEWAY], BEACON_KEY, None); let a = client.next_block().expect("first block"); let b = client.next_block().expect("second block"); @@ -41,15 +41,14 @@ fn ipfs_unique_blocks() { #[test] #[ignore] fn ipfs_multi_gateway_fallback() { - // First gateway is invalid, second is valid — should fall back successfully - let gateways = vec![ - "https://invalid-gateway.example.com", - GATEWAY, - ]; - let mut client = crypto_ctrng::IpfsCtrng::new_with_gateways( - gateways, + let config = crypto_ctrng::IpfsConfig { + use_defaults: false, + timeout: Duration::from_secs(5), + }; + let mut client = crypto_ctrng::Ctrng::ipfs( + ["https://invalid-gateway.example.com", GATEWAY], BEACON_KEY, - Some(Duration::from_secs(5)), + Some(config), ); let block = client.next_block().expect("fallback should succeed"); assert_eq!(block.len(), 32); @@ -58,14 +57,17 @@ fn ipfs_multi_gateway_fallback() { #[test] #[ignore] fn ipfs_all_gateways_fail() { - let gateways = vec![ - "https://invalid-gateway1.example.com", - "https://invalid-gateway2.example.com", - ]; - let mut client = crypto_ctrng::IpfsCtrng::new_with_gateways( - gateways, + let config = crypto_ctrng::IpfsConfig { + use_defaults: false, + timeout: Duration::from_secs(3), + }; + let mut client = crypto_ctrng::Ctrng::ipfs( + [ + "https://invalid-gateway1.example.com", + "https://invalid-gateway2.example.com", + ], BEACON_KEY, - Some(Duration::from_secs(3)), + Some(config), ); let result = client.next_block(); assert!(result.is_err(), "all gateways should fail"); @@ -79,7 +81,7 @@ fn ipfs_all_gateways_fail() { #[test] #[ignore] fn ipfs_default_gateways_list() { - let mut client = crypto_ctrng::IpfsCtrng::new_with_gateways( + let mut client = crypto_ctrng::Ctrng::ipfs( crypto_ctrng::DEFAULT_GATEWAYS.iter().copied(), BEACON_KEY, None, diff --git a/tests/mixed_tests.rs b/tests/mixed_tests.rs index c3ef66a..da76f45 100644 --- a/tests/mixed_tests.rs +++ b/tests/mixed_tests.rs @@ -1,7 +1,7 @@ use crypto_ctrng::RandomBlockSource; const BEACON_KEY: &str = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn09f"; -const GATEWAY: &str = "https://ipfs.filebase.io/ipns/"; +const GATEWAY: &str = "https://ipfs.filebase.io"; #[test] fn local_is_32_bytes() { @@ -21,7 +21,7 @@ fn local_unique_blocks() { #[test] #[ignore] fn mixed_is_32_bytes() { - let ipfs = crypto_ctrng::IpfsCtrng::new(GATEWAY, BEACON_KEY); + let ipfs = crypto_ctrng::Ctrng::ipfs([GATEWAY], BEACON_KEY, None); let mut client = crypto_ctrng::MixedCtrng::new(ipfs).unwrap(); let block = client.next_block().unwrap(); assert_eq!(block.len(), 32); @@ -30,8 +30,8 @@ fn mixed_is_32_bytes() { #[test] #[ignore] fn each_source_is_different() { - let mut ipfs = crypto_ctrng::IpfsCtrng::new(GATEWAY, BEACON_KEY); - let ipfs2 = crypto_ctrng::IpfsCtrng::new(GATEWAY, BEACON_KEY); + let mut ipfs = crypto_ctrng::Ctrng::ipfs([GATEWAY], BEACON_KEY, None); + let ipfs2 = crypto_ctrng::Ctrng::ipfs([GATEWAY], BEACON_KEY, None); let mut mixed = crypto_ctrng::MixedCtrng::new(ipfs2).unwrap(); let ipfs_block = ipfs.next_block().unwrap(); @@ -43,7 +43,7 @@ fn each_source_is_different() { #[test] #[ignore] fn mixed_unique_blocks() { - let ipfs = crypto_ctrng::IpfsCtrng::new(GATEWAY, BEACON_KEY); + let ipfs = crypto_ctrng::Ctrng::ipfs([GATEWAY], BEACON_KEY, None); let mut client = crypto_ctrng::MixedCtrng::new(ipfs).unwrap(); let a = client.next_block().unwrap(); let b = client.next_block().unwrap(); diff --git a/tests/testu01_tests.rs b/tests/testu01_tests.rs index 76f5aba..a7a7eeb 100644 --- a/tests/testu01_tests.rs +++ b/tests/testu01_tests.rs @@ -1,5 +1,5 @@ #[cfg(feature = "testu01")] -use crypto_ctrng::{IpfsCtrng, RandomBlockSource}; +use crypto_ctrng::{Ctrng, RandomBlockSource}; #[cfg(feature = "testu01")] use rand_chacha::ChaCha20Rng; #[cfg(feature = "testu01")] @@ -11,9 +11,9 @@ use testu01_runner::{delete_unif01_gen, make_unif01_gen, register_rng}; #[cfg(feature = "testu01")] fn build_ipfs_seeded_rng() -> ChaCha20Rng { - let gateway = "https://ipfs.filebase.io/ipns/"; + let gateway = "https://ipfs.filebase.io"; let beacon_key = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn09f"; - let mut ctrng = IpfsCtrng::new(gateway, beacon_key); + let mut ctrng = Ctrng::ipfs([gateway], beacon_key, None); let seed = ctrng.next_block().expect("failed to fetch IPFS block"); ChaCha20Rng::from_seed(seed) } From e7add55b6534c6d6cced253d26d7d74af4926fff Mon Sep 17 00:00:00 2001 From: Toufic Date: Thu, 5 Mar 2026 17:48:00 +0100 Subject: [PATCH 06/10] default gateway --- README.md | 26 +++++++++++++------------- src/ctrng/ipfs.rs | 10 ++++------ src/ctrng/mod.rs | 10 +++------- tests/ipfs_live.rs | 34 +++++++++++++--------------------- tests/mixed_tests.rs | 9 ++++----- tests/testu01_tests.rs | 2 +- 6 files changed, 38 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 1c045d0..235cd8b 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ use crypto_ctrng::{Ctrng, RandomBlockSource}; let beacon_key = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn09f"; -let mut ctrng = Ctrng::ipfs([], beacon_key, None); +let mut ctrng = Ctrng::ipfs(beacon_key, None); let block = ctrng.next_block().expect("failed to fetch IPFS block"); ``` @@ -63,23 +63,23 @@ use std::time::Duration; let beacon_key = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn09f"; // Custom gateways + default gateways appended automatically -let mut ctrng = Ctrng::ipfs( - ["https://my-gateway.example.com"], - beacon_key, - None, -); +let config = IpfsConfig { + gateways: vec!["https://my-gateway.example.com".into()], + ..Default::default() +}; +let mut ctrng = Ctrng::ipfs(beacon_key, Some(config)); let block = ctrng.next_block().expect("at least one gateway should respond"); // Custom gateways only (no defaults), with a custom timeout let config = IpfsConfig { + gateways: vec![ + "https://ipfs.filebase.io".into(), + "https://ipfs.io".into(), + ], use_defaults: false, timeout: Duration::from_secs(5), }; -let mut ctrng = Ctrng::ipfs( - ["https://ipfs.filebase.io", "https://ipfs.io"], - beacon_key, - Some(config), -); +let mut ctrng = Ctrng::ipfs(beacon_key, Some(config)); ``` ### DRBG with automatic reseeding (recommended) @@ -90,7 +90,7 @@ use rand_core::RngCore; let beacon_key = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn09f"; -let ipfs = Ctrng::ipfs([], beacon_key, None); +let ipfs = Ctrng::ipfs(beacon_key, None); let mixed = MixedCtrng::new(ipfs).unwrap(); let mut rng = ReseedingRng::new(mixed).unwrap(); @@ -106,7 +106,7 @@ use crypto_ctrng::{Ctrng, MixedCtrng, ReseedConfig, ReseedingRng}; let beacon_key = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn09f"; -let ipfs = Ctrng::ipfs([], beacon_key, None); +let ipfs = Ctrng::ipfs(beacon_key, None); let mixed = MixedCtrng::new(ipfs).unwrap(); let config = ReseedConfig::new(1600, Duration::from_secs(3600)); // 50 blocks, 1 hour diff --git a/src/ctrng/ipfs.rs b/src/ctrng/ipfs.rs index 34955b1..bb53805 100644 --- a/src/ctrng/ipfs.rs +++ b/src/ctrng/ipfs.rs @@ -16,6 +16,7 @@ pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); /// Configuration for IPFS-backed cTRNG. #[derive(Debug, Clone)] pub struct IpfsConfig { + pub gateways: Vec, pub use_defaults: bool, pub timeout: Duration, } @@ -23,6 +24,7 @@ pub struct IpfsConfig { impl Default for IpfsConfig { fn default() -> Self { Self { + gateways: Vec::new(), use_defaults: true, timeout: DEFAULT_TIMEOUT, } @@ -70,14 +72,10 @@ pub struct IpfsCtrng { } impl IpfsCtrng { - pub fn new( - gateways: impl IntoIterator>, - beacon_key: impl Into, - config: Option, - ) -> Self { + pub fn new(beacon_key: impl Into, config: Option) -> Self { let config = config.unwrap_or_default(); let mut gw_list: Vec = - gateways.into_iter().map(IpfsGateway::new).collect(); + config.gateways.into_iter().map(IpfsGateway::new).collect(); if config.use_defaults { for default in DEFAULT_GATEWAYS { let gw = IpfsGateway::new(*default); diff --git a/src/ctrng/mod.rs b/src/ctrng/mod.rs index 3b84ad7..59cf094 100644 --- a/src/ctrng/mod.rs +++ b/src/ctrng/mod.rs @@ -16,13 +16,9 @@ pub enum Ctrng { impl Ctrng { /// Create an IPFS cTRNG source. /// - /// Pass `None` for default config (default gateways appended, 10s timeout). - pub fn ipfs( - gateways: impl IntoIterator>, - beacon_key: impl Into, - config: Option, - ) -> Self { - Self::Ipfs(IpfsCtrng::new(gateways, beacon_key, config)) + /// Pass `None` for default config (default gateways, 10s timeout). + pub fn ipfs(beacon_key: impl Into, config: Option) -> Self { + Self::Ipfs(IpfsCtrng::new(beacon_key, config)) } /// Returns the last block served, if any. diff --git a/tests/ipfs_live.rs b/tests/ipfs_live.rs index d43c2cd..cad2fa0 100644 --- a/tests/ipfs_live.rs +++ b/tests/ipfs_live.rs @@ -8,7 +8,7 @@ const GATEWAY: &str = "https://ipfs.filebase.io"; #[test] #[ignore] fn ipfs_client_can_fetch_live_block() { - let mut client = crypto_ctrng::Ctrng::ipfs([GATEWAY], BEACON_KEY, None); + let mut client = crypto_ctrng::Ctrng::ipfs(BEACON_KEY, None); let block = client.next_block().expect("fetch 32-byte block"); assert_eq!(block.len(), 32); } @@ -16,13 +16,12 @@ fn ipfs_client_can_fetch_live_block() { #[test] #[ignore] fn ipfs_unique_blocks() { - let mut client = crypto_ctrng::Ctrng::ipfs([GATEWAY], BEACON_KEY, None); + let mut client = crypto_ctrng::Ctrng::ipfs(BEACON_KEY, None); let a = client.next_block().expect("first block"); let b = client.next_block().expect("second block"); let c = client.next_block().expect("third block"); - // 4th call triggers refill_cache() - may fail if new batch has timestamp <= last served let result = client.next_block(); assert_ne!(a, b, "first and second blocks should be different"); @@ -42,14 +41,14 @@ fn ipfs_unique_blocks() { #[ignore] fn ipfs_multi_gateway_fallback() { let config = crypto_ctrng::IpfsConfig { + gateways: vec![ + "https://invalid-gateway.example.com".into(), + GATEWAY.into(), + ], use_defaults: false, timeout: Duration::from_secs(5), }; - let mut client = crypto_ctrng::Ctrng::ipfs( - ["https://invalid-gateway.example.com", GATEWAY], - BEACON_KEY, - Some(config), - ); + let mut client = crypto_ctrng::Ctrng::ipfs(BEACON_KEY, Some(config)); let block = client.next_block().expect("fallback should succeed"); assert_eq!(block.len(), 32); } @@ -58,17 +57,14 @@ fn ipfs_multi_gateway_fallback() { #[ignore] fn ipfs_all_gateways_fail() { let config = crypto_ctrng::IpfsConfig { + gateways: vec![ + "https://invalid-gateway1.example.com".into(), + "https://invalid-gateway2.example.com".into(), + ], use_defaults: false, timeout: Duration::from_secs(3), }; - let mut client = crypto_ctrng::Ctrng::ipfs( - [ - "https://invalid-gateway1.example.com", - "https://invalid-gateway2.example.com", - ], - BEACON_KEY, - Some(config), - ); + let mut client = crypto_ctrng::Ctrng::ipfs(BEACON_KEY, Some(config)); let result = client.next_block(); assert!(result.is_err(), "all gateways should fail"); let err = result.unwrap_err().to_string(); @@ -81,11 +77,7 @@ fn ipfs_all_gateways_fail() { #[test] #[ignore] fn ipfs_default_gateways_list() { - let mut client = crypto_ctrng::Ctrng::ipfs( - crypto_ctrng::DEFAULT_GATEWAYS.iter().copied(), - BEACON_KEY, - None, - ); + let mut client = crypto_ctrng::Ctrng::ipfs(BEACON_KEY, None); let block = client.next_block().expect("default gateways should work"); assert_eq!(block.len(), 32); } diff --git a/tests/mixed_tests.rs b/tests/mixed_tests.rs index da76f45..7184593 100644 --- a/tests/mixed_tests.rs +++ b/tests/mixed_tests.rs @@ -1,7 +1,6 @@ use crypto_ctrng::RandomBlockSource; const BEACON_KEY: &str = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn09f"; -const GATEWAY: &str = "https://ipfs.filebase.io"; #[test] fn local_is_32_bytes() { @@ -21,7 +20,7 @@ fn local_unique_blocks() { #[test] #[ignore] fn mixed_is_32_bytes() { - let ipfs = crypto_ctrng::Ctrng::ipfs([GATEWAY], BEACON_KEY, None); + let ipfs = crypto_ctrng::Ctrng::ipfs(BEACON_KEY, None); let mut client = crypto_ctrng::MixedCtrng::new(ipfs).unwrap(); let block = client.next_block().unwrap(); assert_eq!(block.len(), 32); @@ -30,8 +29,8 @@ fn mixed_is_32_bytes() { #[test] #[ignore] fn each_source_is_different() { - let mut ipfs = crypto_ctrng::Ctrng::ipfs([GATEWAY], BEACON_KEY, None); - let ipfs2 = crypto_ctrng::Ctrng::ipfs([GATEWAY], BEACON_KEY, None); + let mut ipfs = crypto_ctrng::Ctrng::ipfs(BEACON_KEY, None); + let ipfs2 = crypto_ctrng::Ctrng::ipfs(BEACON_KEY, None); let mut mixed = crypto_ctrng::MixedCtrng::new(ipfs2).unwrap(); let ipfs_block = ipfs.next_block().unwrap(); @@ -43,7 +42,7 @@ fn each_source_is_different() { #[test] #[ignore] fn mixed_unique_blocks() { - let ipfs = crypto_ctrng::Ctrng::ipfs([GATEWAY], BEACON_KEY, None); + let ipfs = crypto_ctrng::Ctrng::ipfs(BEACON_KEY, None); let mut client = crypto_ctrng::MixedCtrng::new(ipfs).unwrap(); let a = client.next_block().unwrap(); let b = client.next_block().unwrap(); diff --git a/tests/testu01_tests.rs b/tests/testu01_tests.rs index a7a7eeb..62f900d 100644 --- a/tests/testu01_tests.rs +++ b/tests/testu01_tests.rs @@ -13,7 +13,7 @@ use testu01_runner::{delete_unif01_gen, make_unif01_gen, register_rng}; fn build_ipfs_seeded_rng() -> ChaCha20Rng { let gateway = "https://ipfs.filebase.io"; let beacon_key = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn09f"; - let mut ctrng = Ctrng::ipfs([gateway], beacon_key, None); + let mut ctrng = Ctrng::ipfs(beacon_key, None); let seed = ctrng.next_block().expect("failed to fetch IPFS block"); ChaCha20Rng::from_seed(seed) } From 01ee292d4bbe3994fff6cdebf8aee501a52b1d85 Mon Sep 17 00:00:00 2001 From: Toufic Date: Thu, 5 Mar 2026 17:48:20 +0100 Subject: [PATCH 07/10] fmt --- src/ctrng/mod.rs | 4 ++-- src/lib.rs | 2 +- tests/ipfs_live.rs | 5 +---- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/ctrng/mod.rs b/src/ctrng/mod.rs index 59cf094..3ce34bb 100644 --- a/src/ctrng/mod.rs +++ b/src/ctrng/mod.rs @@ -1,10 +1,10 @@ pub(crate) mod ipfs; pub mod types; -use ipfs::{IpfsConfig, IpfsCtrng}; -use types::CtrngBlock; use crate::error::SourceError; use crate::traits::RandomBlockSource; +use ipfs::{IpfsConfig, IpfsCtrng}; +use types::CtrngBlock; /// Unified cTRNG abstraction supporting multiple backends. #[derive(Debug)] diff --git a/src/lib.rs b/src/lib.rs index 9dae946..ff16063 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,8 +7,8 @@ pub mod reseed; pub mod rng; pub mod traits; -pub use ctrng::ipfs::{DEFAULT_GATEWAYS, DEFAULT_TIMEOUT, IpfsConfig, IpfsGateway}; pub use ctrng::Ctrng; +pub use ctrng::ipfs::{DEFAULT_GATEWAYS, DEFAULT_TIMEOUT, IpfsConfig, IpfsGateway}; pub use derive::derive_seed; pub use error::SourceError; pub use local::LocalRng; diff --git a/tests/ipfs_live.rs b/tests/ipfs_live.rs index cad2fa0..54c4caa 100644 --- a/tests/ipfs_live.rs +++ b/tests/ipfs_live.rs @@ -41,10 +41,7 @@ fn ipfs_unique_blocks() { #[ignore] fn ipfs_multi_gateway_fallback() { let config = crypto_ctrng::IpfsConfig { - gateways: vec![ - "https://invalid-gateway.example.com".into(), - GATEWAY.into(), - ], + gateways: vec!["https://invalid-gateway.example.com".into(), GATEWAY.into()], use_defaults: false, timeout: Duration::from_secs(5), }; From 9566746c412086dd345eea50a8ff657f6e4ca876 Mon Sep 17 00:00:00 2001 From: Toufic Date: Thu, 5 Mar 2026 17:50:41 +0100 Subject: [PATCH 08/10] remove derive_seed --- README.md | 4 ++-- src/derive.rs | 16 ---------------- src/lib.rs | 4 ++-- tests/rng_tests.rs | 14 +------------- 4 files changed, 5 insertions(+), 33 deletions(-) delete mode 100644 src/derive.rs diff --git a/README.md b/README.md index 235cd8b..9c9f950 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,14 @@ This crate's responsibility is to **verify** that the cTRNG backend provides uni - **Gateway/back-end** (e.g. IPFS beacon, hardware TRNG) is responsible for generating unique 32-byte blocks with monotonic timestamps. This responsibility sits outside of this crate. - **This crate** (`crypto-ctrng`) verifies uniqueness by enforcing timestamp monotonicity and rejecting duplicate blocks. -- **Application/client code** (e.g. TECDSA library) is responsible for personalizing the provided blocks using `derive_seed(execution_id, party_id, counter, block)` to domain-separate pulls so a single user never receives the same derived value twice, even if they consume the same cTRNG block as other users (but this is something that we are looking to prevent in the future). +- **Application/client code** (e.g. TECDSA library) is responsible for personalizing the provided blocks to domain-separate pulls so a single user never receives the same derived value twice, even if they consume the same cTRNG block as other users (but this is something that we are looking to prevent in the future). **Note on entropy mixing:** - `MixedCtrng` combines entropy sources (XOR of IPFS beacon + local OS randomness) for enhanced security, but still relies on the gateway for uniqueness guarantees. **Uniqueness guarantees:** - This crate ensures that each `next_block()` call returns a block with a timestamp strictly greater than the previous one, preventing global block reuse. -- The calling library must use `derive_seed()` to personalize blocks per user/execution to prevent collisions between different users reading the same raw block. +- The calling library must personalize blocks per user/execution to prevent collisions between different users reading the same raw block. ## Usage modes diff --git a/src/derive.rs b/src/derive.rs deleted file mode 100644 index a8ce15f..0000000 --- a/src/derive.rs +++ /dev/null @@ -1,16 +0,0 @@ -use sha2::{Digest, Sha256}; - -/// Derives a personalized seed using SHA-256 domain separation. -pub fn derive_seed( - execution_id: &[u8], - party_id: u16, - counter: u64, - ctrng_block: &[u8; 32], -) -> [u8; 32] { - let mut hasher = Sha256::new(); - hasher.update(execution_id); - hasher.update(party_id.to_be_bytes()); - hasher.update(counter.to_be_bytes()); - hasher.update(ctrng_block); - hasher.finalize().into() -} diff --git a/src/lib.rs b/src/lib.rs index ff16063..ee19ae0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ pub mod ctrng; -pub mod derive; + pub mod error; pub mod local; pub mod mixed; @@ -9,7 +9,7 @@ pub mod traits; pub use ctrng::Ctrng; pub use ctrng::ipfs::{DEFAULT_GATEWAYS, DEFAULT_TIMEOUT, IpfsConfig, IpfsGateway}; -pub use derive::derive_seed; + pub use error::SourceError; pub use local::LocalRng; pub use mixed::MixedCtrng; diff --git a/tests/rng_tests.rs b/tests/rng_tests.rs index 03d77fe..a4aa41b 100644 --- a/tests/rng_tests.rs +++ b/tests/rng_tests.rs @@ -1,6 +1,5 @@ -use crypto_ctrng::{BlockRng, RandomBlockSource, SourceError, derive_seed}; +use crypto_ctrng::{BlockRng, RandomBlockSource, SourceError}; use rand_core::RngCore; -use std::collections::HashSet; #[test] fn flaky_source_propagates_error() { @@ -53,14 +52,3 @@ fn flaky_source_propagates_error() { let mut buf3 = [0u8; 64]; assert!(rng_trait.try_fill_bytes(&mut buf3).is_err()); } - -#[test] -fn derive_seed_yields_unique_material() { - let exec = b"derive-seed-exec-id"; - let ctrng = [0xAA; 32]; - let mut seen = HashSet::new(); - for ctr in 0..256u64 { - let seed = derive_seed(exec, 1, ctr, &ctrng); - assert!(seen.insert(seed), "duplicate seed for counter {ctr}"); - } -} From 276253c7fb3a71398fcfb5746169588a2c6b3b67 Mon Sep 17 00:00:00 2001 From: Toufic Date: Tue, 10 Mar 2026 14:46:20 +0100 Subject: [PATCH 09/10] rename + e2e --- .github/workflows/rust.yml | 2 +- README.md | 17 +++++++++++--- src/ctrng/{ipfs.rs => ipfs_beacon.rs} | 6 ++--- src/ctrng/mod.rs | 8 +++---- src/lib.rs | 2 +- tests/{ipfs_live.rs => e2e_ipfs.rs} | 17 +++++--------- tests/e2e_mixed.rs | 32 +++++++++++++++++++++++++ tests/mixed_tests.rs | 34 --------------------------- 8 files changed, 61 insertions(+), 57 deletions(-) rename src/ctrng/{ipfs.rs => ipfs_beacon.rs} (98%) rename tests/{ipfs_live.rs => e2e_ipfs.rs} (91%) create mode 100644 tests/e2e_mixed.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9fd45e0..b9dd740 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -19,4 +19,4 @@ jobs: - name: Build run: cargo build --verbose - name: Run tests - run: cargo test --verbose + run: cargo test --verbose -- --skip test_e2e_ diff --git a/README.md b/README.md index 9c9f950..ab2c8bd 100644 --- a/README.md +++ b/README.md @@ -117,11 +117,22 @@ let mut rng = ReseedingRng::with_config(mixed, config).unwrap(); To run offline tests : -```cargo test``` +```bash +cargo test -- --skip test_e2e_ +``` + +To run the IPFS-backed end-to-end tests : + +```bash +cargo test test_e2e_ +``` -To run offline and online tests : +You can also run the dedicated e2e integration targets individually: -```cargo test -- --include-ignored``` +```bash +cargo test --test e2e_ipfs +cargo test --test e2e_mixed +``` ### TestU01 statistical runner diff --git a/src/ctrng/ipfs.rs b/src/ctrng/ipfs_beacon.rs similarity index 98% rename from src/ctrng/ipfs.rs rename to src/ctrng/ipfs_beacon.rs index bb53805..ebb34b9 100644 --- a/src/ctrng/ipfs.rs +++ b/src/ctrng/ipfs_beacon.rs @@ -62,7 +62,7 @@ impl PartialEq for IpfsGateway { /// cTRNG backed by an IPFS beacon. #[derive(Debug)] -pub struct IpfsCtrng { +pub struct IpfsBeacon { gateways: Vec, beacon_key: String, timeout: Duration, @@ -71,7 +71,7 @@ pub struct IpfsCtrng { last_served: Option, } -impl IpfsCtrng { +impl IpfsBeacon { pub fn new(beacon_key: impl Into, config: Option) -> Self { let config = config.unwrap_or_default(); let mut gw_list: Vec = @@ -183,7 +183,7 @@ impl IpfsCtrng { } } -impl RandomBlockSource for IpfsCtrng { +impl RandomBlockSource for IpfsBeacon { fn next_block(&mut self) -> Result<[u8; 32], SourceError> { if self.cursor < self.cache.len() { let block = self.cache[self.cursor]; diff --git a/src/ctrng/mod.rs b/src/ctrng/mod.rs index 3ce34bb..1d18b6f 100644 --- a/src/ctrng/mod.rs +++ b/src/ctrng/mod.rs @@ -1,15 +1,15 @@ -pub(crate) mod ipfs; +pub(crate) mod ipfs_beacon; pub mod types; use crate::error::SourceError; use crate::traits::RandomBlockSource; -use ipfs::{IpfsConfig, IpfsCtrng}; +use ipfs_beacon::{IpfsConfig, IpfsBeacon}; use types::CtrngBlock; /// Unified cTRNG abstraction supporting multiple backends. #[derive(Debug)] pub enum Ctrng { - Ipfs(IpfsCtrng), + Ipfs(IpfsBeacon), // Future: DataHeaven(DataHeavenCtrng), } @@ -18,7 +18,7 @@ impl Ctrng { /// /// Pass `None` for default config (default gateways, 10s timeout). pub fn ipfs(beacon_key: impl Into, config: Option) -> Self { - Self::Ipfs(IpfsCtrng::new(beacon_key, config)) + Self::Ipfs(IpfsBeacon::new(beacon_key, config)) } /// Returns the last block served, if any. diff --git a/src/lib.rs b/src/lib.rs index ee19ae0..8b88bf3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ pub mod rng; pub mod traits; pub use ctrng::Ctrng; -pub use ctrng::ipfs::{DEFAULT_GATEWAYS, DEFAULT_TIMEOUT, IpfsConfig, IpfsGateway}; +pub use ctrng::ipfs_beacon::{DEFAULT_GATEWAYS, DEFAULT_TIMEOUT, IpfsConfig, IpfsGateway}; pub use error::SourceError; pub use local::LocalRng; diff --git a/tests/ipfs_live.rs b/tests/e2e_ipfs.rs similarity index 91% rename from tests/ipfs_live.rs rename to tests/e2e_ipfs.rs index 54c4caa..d288697 100644 --- a/tests/ipfs_live.rs +++ b/tests/e2e_ipfs.rs @@ -6,16 +6,14 @@ const BEACON_KEY: &str = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn0 const GATEWAY: &str = "https://ipfs.filebase.io"; #[test] -#[ignore] -fn ipfs_client_can_fetch_live_block() { +fn test_e2e_ipfs_client_can_fetch_live_block() { let mut client = crypto_ctrng::Ctrng::ipfs(BEACON_KEY, None); let block = client.next_block().expect("fetch 32-byte block"); assert_eq!(block.len(), 32); } #[test] -#[ignore] -fn ipfs_unique_blocks() { +fn test_e2e_ipfs_unique_blocks() { let mut client = crypto_ctrng::Ctrng::ipfs(BEACON_KEY, None); let a = client.next_block().expect("first block"); @@ -32,14 +30,13 @@ fn ipfs_unique_blocks() { assert_ne!(c, d, "third and fourth blocks should be different"); } Err(_) => { - // Expected failure confirms timestamp validation works + // Expected failure confirms timestamp validation works. } } } #[test] -#[ignore] -fn ipfs_multi_gateway_fallback() { +fn test_e2e_ipfs_multi_gateway_fallback() { let config = crypto_ctrng::IpfsConfig { gateways: vec!["https://invalid-gateway.example.com".into(), GATEWAY.into()], use_defaults: false, @@ -51,8 +48,7 @@ fn ipfs_multi_gateway_fallback() { } #[test] -#[ignore] -fn ipfs_all_gateways_fail() { +fn test_e2e_ipfs_all_gateways_fail() { let config = crypto_ctrng::IpfsConfig { gateways: vec![ "https://invalid-gateway1.example.com".into(), @@ -72,8 +68,7 @@ fn ipfs_all_gateways_fail() { } #[test] -#[ignore] -fn ipfs_default_gateways_list() { +fn test_e2e_ipfs_default_gateways_list() { let mut client = crypto_ctrng::Ctrng::ipfs(BEACON_KEY, None); let block = client.next_block().expect("default gateways should work"); assert_eq!(block.len(), 32); diff --git a/tests/e2e_mixed.rs b/tests/e2e_mixed.rs new file mode 100644 index 0000000..afd9449 --- /dev/null +++ b/tests/e2e_mixed.rs @@ -0,0 +1,32 @@ +use crypto_ctrng::RandomBlockSource; + +const BEACON_KEY: &str = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn09f"; + +#[test] +fn test_e2e_mixed_is_32_bytes() { + let ipfs = crypto_ctrng::Ctrng::ipfs(BEACON_KEY, None); + let mut client = crypto_ctrng::MixedCtrng::new(ipfs).unwrap(); + let block = client.next_block().unwrap(); + assert_eq!(block.len(), 32); +} + +#[test] +fn test_e2e_each_source_is_different() { + let mut ipfs = crypto_ctrng::Ctrng::ipfs(BEACON_KEY, None); + let ipfs2 = crypto_ctrng::Ctrng::ipfs(BEACON_KEY, None); + let mut mixed = crypto_ctrng::MixedCtrng::new(ipfs2).unwrap(); + + let ipfs_block = ipfs.next_block().unwrap(); + let mixed_block = mixed.next_block().unwrap(); + + assert_ne!(ipfs_block, mixed_block); +} + +#[test] +fn test_e2e_mixed_unique_blocks() { + let ipfs = crypto_ctrng::Ctrng::ipfs(BEACON_KEY, None); + let mut client = crypto_ctrng::MixedCtrng::new(ipfs).unwrap(); + let a = client.next_block().unwrap(); + let b = client.next_block().unwrap(); + assert_ne!(a, b); +} diff --git a/tests/mixed_tests.rs b/tests/mixed_tests.rs index 7184593..cd3dad9 100644 --- a/tests/mixed_tests.rs +++ b/tests/mixed_tests.rs @@ -1,7 +1,5 @@ use crypto_ctrng::RandomBlockSource; -const BEACON_KEY: &str = "k2k4r8lvomw737sajfnpav0dpeernugnryng50uheyk1k39lursmn09f"; - #[test] fn local_is_32_bytes() { let mut client = crypto_ctrng::LocalRng::new(); @@ -16,35 +14,3 @@ fn local_unique_blocks() { let b = client.next_block().unwrap(); assert_ne!(a, b); } - -#[test] -#[ignore] -fn mixed_is_32_bytes() { - let ipfs = crypto_ctrng::Ctrng::ipfs(BEACON_KEY, None); - let mut client = crypto_ctrng::MixedCtrng::new(ipfs).unwrap(); - let block = client.next_block().unwrap(); - assert_eq!(block.len(), 32); -} - -#[test] -#[ignore] -fn each_source_is_different() { - let mut ipfs = crypto_ctrng::Ctrng::ipfs(BEACON_KEY, None); - let ipfs2 = crypto_ctrng::Ctrng::ipfs(BEACON_KEY, None); - let mut mixed = crypto_ctrng::MixedCtrng::new(ipfs2).unwrap(); - - let ipfs_block = ipfs.next_block().unwrap(); - let mixed_block = mixed.next_block().unwrap(); - - assert_ne!(ipfs_block, mixed_block); -} - -#[test] -#[ignore] -fn mixed_unique_blocks() { - let ipfs = crypto_ctrng::Ctrng::ipfs(BEACON_KEY, None); - let mut client = crypto_ctrng::MixedCtrng::new(ipfs).unwrap(); - let a = client.next_block().unwrap(); - let b = client.next_block().unwrap(); - assert_ne!(a, b); -} From 69e04e186673ff98d1e5add5b5cbacfab167e7bb Mon Sep 17 00:00:00 2001 From: Toufic Date: Wed, 11 Mar 2026 15:19:47 +0100 Subject: [PATCH 10/10] fix e2e_ --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ab2c8bd..7807308 100644 --- a/README.md +++ b/README.md @@ -118,13 +118,13 @@ let mut rng = ReseedingRng::with_config(mixed, config).unwrap(); To run offline tests : ```bash -cargo test -- --skip test_e2e_ +cargo test -- --skip test_e2e ``` To run the IPFS-backed end-to-end tests : ```bash -cargo test test_e2e_ +cargo test test_e2e ``` You can also run the dedicated e2e integration targets individually: