lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 70abf6eb6bcbf07f4074b6fba237add4681c00eb
parent 91006304a47ff335c6f36fbbcfcb68ecfc76f6c7
Author: triesap <tyson@radroots.org>
Date:   Sun, 20 Sep 2026 15:47:58 +0000

sdk: expose frozen upload retry policy

- Query bounded retry delays from immutable upload transactions
- Share existing exponential delay calculation with the transport
- Preserve unsigned and cancelled operation refusal in wrapper tests
- Verify SDK coverage and the complete producer graph

Diffstat:
Mcontracts/api_baselines/radroots_sdk.txt | 1+
Mcrates/sdk/src/adapters/blossom.rs | 7+------
Mcrates/sdk/src/sync.rs | 14+++++++++++---
Mcrates/sdk/src/transport.rs | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 83 insertions(+), 9 deletions(-)

diff --git a/contracts/api_baselines/radroots_sdk.txt b/contracts/api_baselines/radroots_sdk.txt @@ -660,6 +660,7 @@ impl radroots_sdk::transport::BlossomUploadTransaction pub const fn radroots_sdk::transport::BlossomUploadTransaction::config_fingerprint(&self) -> radroots_sdk::transport::BlossomConfigFingerprint pub const fn radroots_sdk::transport::BlossomUploadTransaction::expected_url(&self) -> &radroots_blossom::url::BlobUrl pub const fn radroots_sdk::transport::BlossomUploadTransaction::request(&self) -> &radroots_sdk::transport::BlossomUploadRequest +pub fn radroots_sdk::transport::BlossomUploadTransaction::retry_delay_after(&self, u8) -> core::option::Option<core::time::Duration> impl core::fmt::Debug for radroots_sdk::transport::BlossomUploadTransaction pub fn radroots_sdk::transport::BlossomUploadTransaction::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct radroots_sdk::transport::DaemonConfig diff --git a/crates/sdk/src/adapters/blossom.rs b/crates/sdk/src/adapters/blossom.rs @@ -1112,12 +1112,7 @@ async fn retry_delay( phase: BlossomPhase, possible_orphan: bool, ) -> Result<(), BlossomError> { - let exponent = u32::from(attempt.saturating_sub(1)).min(16); - let factor = 1_u32 << exponent; - let delay = config - .initial_retry_delay() - .saturating_mul(factor) - .min(Duration::from_secs(30)); + let delay = config.retry_delay(attempt); tokio::select! { biased; _ = cancellation.cancelled() => Err(failure( diff --git a/crates/sdk/src/sync.rs b/crates/sdk/src/sync.rs @@ -493,6 +493,10 @@ mod tests { } fn push_request() -> PushRequest { + push_request_for_id(8) + } + + fn push_request_for_id(id: u8) -> PushRequest { let actor = Actor::new( PublicKey::from_hex(PUBLIC_KEY).expect("public key"), ActorSource::ExplicitPublicKey, @@ -512,8 +516,8 @@ mod tests { ) .expect("authored plan"); PushRequest::new( - SyncId::new([8; 16]).expect("operation id"), - IdempotencyKey::parse("sdk-sync-wrapper").expect("idempotency key"), + SyncId::new([id; 16]).expect("operation id"), + IdempotencyKey::parse(format!("sdk-sync-wrapper-{id}")).expect("idempotency key"), actor, plan, TargetSet::new(vec![target()]).expect("targets"), @@ -600,7 +604,7 @@ mod tests { ); assert_eq!( operations.deliver_push(operation_id).await, - Err(Error::MissingSink) + Err(Error::InvalidSignerOutput) ); let status = operations @@ -677,6 +681,10 @@ mod tests { let submitted = push_request(); assert_eq!( operations.submit_push(submitted).await, + Err(Error::SigningCancelled) + ); + assert_eq!( + operations.submit_push(push_request_for_id(9)).await, Err(Error::MissingSigner) ); } diff --git a/crates/sdk/src/transport.rs b/crates/sdk/src/transport.rs @@ -420,6 +420,13 @@ impl BlossomConfig { pub(crate) const fn initial_retry_delay(&self) -> Duration { self.initial_retry_delay } + + pub(crate) fn retry_delay(&self, attempt: u8) -> Duration { + let exponent = u32::from(attempt.saturating_sub(1)).min(16); + self.initial_retry_delay() + .saturating_mul(1_u32 << exponent) + .min(MAX_BLOSSOM_RETRY_DELAY) + } } /// Stable, non-secret identity of a completely validated Blossom configuration. @@ -820,6 +827,18 @@ impl BlossomEndpointEvidence { #[cfg(feature = "blossom")] impl BlossomUploadTransaction { + /// Returns the frozen policy's minimum delay before another attempt. + /// Zero or exhausted completed-attempt counts admit no retry. This is a + /// pure policy query, not evidence of native inactivity, renewed signing + /// access, or absence of remote effects. The caller owns those prerequisites + /// and the durable attempt count; current slot configuration cannot change + /// this transaction's budget. + #[must_use] + pub fn retry_delay_after(&self, completed_attempts: u8) -> Option<Duration> { + (completed_attempts > 0 && completed_attempts < self.config.max_attempts()) + .then(|| self.config.retry_delay(completed_attempts)) + } + #[must_use] pub const fn config_fingerprint(&self) -> BlossomConfigFingerprint { self.config_fingerprint @@ -2531,6 +2550,57 @@ mod tests { } #[cfg(feature = "blossom")] + #[test] + fn upload_retry_policy_is_bounded_and_frozen_in_the_transaction() { + let profile = public_blossom_profile("https://media.example").unwrap(); + for (maximum, initial_ms) in [(1, 250), (3, 250), (5, 20_000)] { + let config = BlossomConfig::from_profile(profile.clone()) + .with_network_policy( + Duration::from_secs(10), + Duration::from_secs(60), + maximum, + Duration::from_millis(initial_ms), + ) + .unwrap(); + let slot = BlossomSlot::new(); + slot.configure(config.clone()).unwrap(); + let transaction = slot.prepare_upload(blossom_request("ignored")).unwrap(); + for completed in 0..=u8::MAX { + let expected = if completed > 0 && completed < maximum { + Some(Duration::from_millis( + (initial_ms * (1 << (completed - 1))).min(30_000), + )) + } else { + None + }; + assert_eq!(transaction.retry_delay_after(completed), expected); + } + slot.configure( + BlossomConfig::from_profile(profile.clone()) + .with_network_policy( + Duration::from_secs(10), + Duration::from_secs(60), + 2, + Duration::from_millis(7), + ) + .unwrap(), + ) + .unwrap(); + let later = slot.prepare_upload(blossom_request("ignored")).unwrap(); + assert_ne!(transaction.config_fingerprint(), later.config_fingerprint()); + assert_eq!(later.retry_delay_after(1), Some(Duration::from_millis(7))); + assert_eq!(later.retry_delay_after(2), None); + assert_eq!(transaction.config_fingerprint(), config.fingerprint()); + assert_eq!( + transaction.retry_delay_after(1), + (maximum > 1).then_some(Duration::from_millis(initial_ms)) + ); + assert_eq!(config.retry_delay(0), Duration::from_millis(initial_ms)); + assert_eq!(config.retry_delay(u8::MAX), Duration::from_secs(30)); + } + } + + #[cfg(feature = "blossom")] fn simulator_blossom_profile(origin: &str) -> Result<BlossomProfile, BlossomError> { BlossomProfile::new( BlossomHostKind::Simulator,