commit 46ff7c47d51581b89baa84fc6e9caf06aff7d473
parent a891f09aedb6f219ed544184edcbf3e896e9e5dd
Author: triesap <tyson@radroots.org>
Date: Mon, 27 Jul 2026 07:26:32 +0000
transport: seal satisfaction policy state
- hide policy variants behind validated constructors and typed accessors
- make every quorum threshold fallible and target-count bounded
- preserve strict wire compatibility with bounded required-target decoding
- migrate runtime outbox and Nostr consumers to the sealed contract
Diffstat:
9 files changed, 392 insertions(+), 308 deletions(-)
diff --git a/crates/outbox/src/store.rs b/crates/outbox/src/store.rs
@@ -34,9 +34,10 @@ use radroots_event_store::{
RadrootsTransportObservationType,
};
use radroots_transport::{
- RADROOTS_RETICULUM_ENDPOINT_URI, RadrootsTransportError, RadrootsTransportKind,
- RadrootsTransportMeshScopeId, RadrootsTransportOutcomeKind, RadrootsTransportSatisfactionClass,
- RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget,
+ RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_TRANSPORT_TARGET_MAX_COUNT, RadrootsTransportError,
+ RadrootsTransportKind, RadrootsTransportMeshScopeId, RadrootsTransportOutcomeKind,
+ RadrootsTransportSatisfactionClass, RadrootsTransportSatisfactionPolicy,
+ RadrootsTransportSatisfactionPolicyKind, RadrootsTransportTarget,
RadrootsTransportTargetFingerprint, RadrootsTransportTargetLabel,
};
use serde::Serialize;
@@ -1886,22 +1887,15 @@ fn prepare_delivery_plan(
fn canonical_satisfaction_policy(
policy: &RadrootsTransportSatisfactionPolicy,
) -> Result<RadrootsTransportSatisfactionPolicy, RadrootsOutboxError> {
- match policy {
- RadrootsTransportSatisfactionPolicy::RequiredTargets { class, targets } => Ok(
- RadrootsTransportSatisfactionPolicy::required_targets(*class, targets.clone())?,
- ),
- RadrootsTransportSatisfactionPolicy::NoWait
- | RadrootsTransportSatisfactionPolicy::Any { .. }
- | RadrootsTransportSatisfactionPolicy::All { .. }
- | RadrootsTransportSatisfactionPolicy::Quorum { .. } => Ok(policy.clone()),
- }
+ policy.required_target_count(RADROOTS_TRANSPORT_TARGET_MAX_COUNT)?;
+ Ok(policy.clone())
}
fn validate_required_targets_belong_to_plan(
policy: &RadrootsTransportSatisfactionPolicy,
prepared_targets: &[PreparedDeliveryTarget],
) -> Result<(), RadrootsOutboxError> {
- let RadrootsTransportSatisfactionPolicy::RequiredTargets { targets, .. } = policy else {
+ let Some(targets) = policy.required_target_fingerprints() else {
return Ok(());
};
if targets.iter().all(|required| {
@@ -2570,18 +2564,12 @@ fn delivery_plan_status_targets<'a>(
policy: &RadrootsTransportSatisfactionPolicy,
targets: &'a [RadrootsOutboxDeliveryTargetRecord],
) -> Vec<&'a RadrootsOutboxDeliveryTargetRecord> {
- match policy {
- RadrootsTransportSatisfactionPolicy::RequiredTargets {
- targets: required_targets,
- ..
- } => targets
+ match policy.required_target_fingerprints() {
+ Some(required_targets) => targets
.iter()
.filter(|target| required_targets.contains(&target.endpoint_fingerprint))
.collect(),
- RadrootsTransportSatisfactionPolicy::NoWait
- | RadrootsTransportSatisfactionPolicy::Any { .. }
- | RadrootsTransportSatisfactionPolicy::All { .. }
- | RadrootsTransportSatisfactionPolicy::Quorum { .. } => targets.iter().collect(),
+ None => targets.iter().collect(),
}
}
@@ -2589,23 +2577,20 @@ fn outbox_satisfied_target_count(
policy: &RadrootsTransportSatisfactionPolicy,
targets: &[RadrootsOutboxDeliveryTargetRecord],
) -> i64 {
- match policy {
- RadrootsTransportSatisfactionPolicy::NoWait => 0,
- RadrootsTransportSatisfactionPolicy::Any { class }
- | RadrootsTransportSatisfactionPolicy::All { class }
- | RadrootsTransportSatisfactionPolicy::Quorum { class, .. } => targets
+ let Some(class) = policy.target_satisfaction_class() else {
+ return 0;
+ };
+ match policy.required_target_fingerprints() {
+ None => targets
.iter()
- .filter(|target| target.status.counts_as_transport_satisfaction(*class))
+ .filter(|target| target.status.counts_as_transport_satisfaction(class))
.count() as i64,
- RadrootsTransportSatisfactionPolicy::RequiredTargets {
- class,
- targets: required_targets,
- } => required_targets
+ Some(required_targets) => required_targets
.iter()
.filter(|required| {
targets.iter().any(|target| {
target.endpoint_fingerprint == **required
- && target.status.counts_as_transport_satisfaction(*class)
+ && target.status.counts_as_transport_satisfaction(class)
})
})
.count() as i64,
@@ -3176,22 +3161,32 @@ fn parse_optional_stored_mutation_id(
}
fn satisfaction_policy_storage_value(policy: &RadrootsTransportSatisfactionPolicy) -> String {
- match policy {
- RadrootsTransportSatisfactionPolicy::NoWait => "no_wait".to_owned(),
- RadrootsTransportSatisfactionPolicy::All { class } => {
- format!("all_{}", satisfaction_class_storage_value(*class))
+ let class = policy.target_satisfaction_class();
+ match policy.kind() {
+ RadrootsTransportSatisfactionPolicyKind::NoWait => "no_wait".to_owned(),
+ RadrootsTransportSatisfactionPolicyKind::All => {
+ format!(
+ "all_{}",
+ satisfaction_class_storage_value(class.expect("all policy class"))
+ )
}
- RadrootsTransportSatisfactionPolicy::Any { class } => {
- format!("any_{}", satisfaction_class_storage_value(*class))
+ RadrootsTransportSatisfactionPolicyKind::Any => {
+ format!(
+ "any_{}",
+ satisfaction_class_storage_value(class.expect("any policy class"))
+ )
}
- RadrootsTransportSatisfactionPolicy::Quorum { class, threshold } => {
+ RadrootsTransportSatisfactionPolicyKind::Quorum => {
format!(
- "quorum_{}:{threshold}",
- satisfaction_class_storage_value(*class)
+ "quorum_{}:{}",
+ satisfaction_class_storage_value(class.expect("quorum policy class")),
+ policy.quorum_threshold().expect("quorum threshold")
)
}
- RadrootsTransportSatisfactionPolicy::RequiredTargets { class, targets } => {
- let mut fingerprints = targets
+ RadrootsTransportSatisfactionPolicyKind::RequiredTargets => {
+ let mut fingerprints = policy
+ .required_target_fingerprints()
+ .expect("required-target policy fingerprints")
.iter()
.map(RadrootsTransportTargetFingerprint::as_str)
.collect::<Vec<_>>();
@@ -3199,7 +3194,7 @@ fn satisfaction_policy_storage_value(policy: &RadrootsTransportSatisfactionPolic
let fingerprints = fingerprints.join(",");
format!(
"required_{}:{fingerprints}",
- satisfaction_class_storage_value(*class)
+ satisfaction_class_storage_value(class.expect("required-target policy class"))
)
}
}
@@ -3241,13 +3236,13 @@ fn parse_satisfaction_policy(
.strip_prefix("all_")
.and_then(parse_satisfaction_class_storage_value)
{
- return Ok(RadrootsTransportSatisfactionPolicy::All { class });
+ return Ok(RadrootsTransportSatisfactionPolicy::all(class));
}
if let Some(class) = value
.strip_prefix("any_")
.and_then(parse_satisfaction_class_storage_value)
{
- return Ok(RadrootsTransportSatisfactionPolicy::Any { class });
+ return Ok(RadrootsTransportSatisfactionPolicy::any(class));
}
if let Some((class_label, threshold)) = value
.strip_prefix("quorum_")
@@ -3255,10 +3250,10 @@ fn parse_satisfaction_policy(
&& threshold == required_success_count.to_string()
&& let Some(class) = parse_satisfaction_class_storage_value(class_label)
{
- return Ok(RadrootsTransportSatisfactionPolicy::Quorum {
+ return Ok(RadrootsTransportSatisfactionPolicy::quorum(
class,
- threshold: required_count_u16(required_success_count)?,
- });
+ required_count_u16(required_success_count)?,
+ )?);
}
if let Some((class_label, fingerprints)) = value
.strip_prefix("required_")
@@ -3594,7 +3589,7 @@ mod tests {
2,
),
(
- RadrootsTransportSatisfactionPolicy::quorum_seen(2),
+ RadrootsTransportSatisfactionPolicy::quorum_seen(2).expect("valid quorum"),
"quorum_seen:2",
2,
),
@@ -3604,7 +3599,7 @@ mod tests {
1,
),
(
- RadrootsTransportSatisfactionPolicy::quorum_delivered(2),
+ RadrootsTransportSatisfactionPolicy::quorum_delivered(2).expect("valid quorum"),
"quorum_delivered:2",
2,
),
@@ -3621,14 +3616,16 @@ mod tests {
fn required_target_policy_idempotency_is_order_independent() {
let first = nostr_target("wss://required-one.example");
let second = nostr_target("wss://required-two.example");
- let first_policy = RadrootsTransportSatisfactionPolicy::RequiredTargets {
- class: RadrootsTransportSatisfactionClass::Accepted,
- targets: vec![second.fingerprint().clone(), first.fingerprint().clone()],
- };
- let second_policy = RadrootsTransportSatisfactionPolicy::RequiredTargets {
- class: RadrootsTransportSatisfactionClass::Accepted,
- targets: vec![first.fingerprint().clone(), second.fingerprint().clone()],
- };
+ let first_policy = RadrootsTransportSatisfactionPolicy::required_targets(
+ RadrootsTransportSatisfactionClass::Accepted,
+ vec![second.fingerprint().clone(), first.fingerprint().clone()],
+ )
+ .expect("first required-target policy");
+ let second_policy = RadrootsTransportSatisfactionPolicy::required_targets(
+ RadrootsTransportSatisfactionClass::Accepted,
+ vec![first.fingerprint().clone(), second.fingerprint().clone()],
+ )
+ .expect("second required-target policy");
let targets = vec![first, second];
let first_prepared = prepare_delivery_plan(
@@ -6805,7 +6802,7 @@ mod tests {
RadrootsOutboxDeliveryPlanInput::new(
"transport.nostr.local",
7,
- RadrootsTransportSatisfactionPolicy::quorum_accepted(1),
+ RadrootsTransportSatisfactionPolicy::quorum_accepted(1).expect("valid quorum"),
vec![
nostr_target(NOSTR_PRIMARY_WSS),
nostr_target(NOSTR_SECONDARY_WSS),
@@ -6825,7 +6822,7 @@ mod tests {
assert_eq!(plans.len(), 1);
assert_eq!(
plans[0].satisfaction_policy,
- RadrootsTransportSatisfactionPolicy::quorum_accepted(1)
+ RadrootsTransportSatisfactionPolicy::quorum_accepted(1).expect("valid quorum")
);
assert_eq!(plans[0].required_success_count, 1);
assert_eq!(plans[0].target_policy_version, 7);
@@ -7449,7 +7446,7 @@ mod tests {
RadrootsOutboxDeliveryPlanInput::new(
"transport.explicit.multi_target",
1,
- RadrootsTransportSatisfactionPolicy::quorum_accepted(2),
+ RadrootsTransportSatisfactionPolicy::quorum_accepted(2).expect("valid quorum"),
vec![
nostr_target(NOSTR_PRIMARY_WSS),
reticulum_target("reticulum:local"),
diff --git a/crates/runtime/src/transport.rs b/crates/runtime/src/transport.rs
@@ -382,20 +382,20 @@ fn satisfied_target_count_for_policy(
policy: &RadrootsTransportSatisfactionPolicy,
target_states: &[RadrootsRuntimeDeliveryTargetState],
) -> usize {
- match policy {
- RadrootsTransportSatisfactionPolicy::NoWait => 0,
- RadrootsTransportSatisfactionPolicy::Any { class }
- | RadrootsTransportSatisfactionPolicy::All { class }
- | RadrootsTransportSatisfactionPolicy::Quorum { class, .. } => target_states
+ let Some(class) = policy.target_satisfaction_class() else {
+ return 0;
+ };
+ match policy.required_target_fingerprints() {
+ None => target_states
.iter()
- .filter(|state| state.status.counts_as_satisfied(*class))
+ .filter(|state| state.status.counts_as_satisfied(class))
.count(),
- RadrootsTransportSatisfactionPolicy::RequiredTargets { class, targets } => targets
+ Some(targets) => targets
.iter()
.filter(|required| {
target_states.iter().any(|state| {
state.target.fingerprint() == *required
- && state.status.counts_as_satisfied(*class)
+ && state.status.counts_as_satisfied(class)
})
})
.count(),
@@ -407,15 +407,15 @@ fn target_states_satisfy_policy(
policy: &RadrootsTransportSatisfactionPolicy,
target_states: &[RadrootsRuntimeDeliveryTargetState],
) -> Result<bool, RadrootsRuntimeTransportError> {
- match policy {
- RadrootsTransportSatisfactionPolicy::NoWait => Ok(true),
- RadrootsTransportSatisfactionPolicy::Any { .. }
- | RadrootsTransportSatisfactionPolicy::All { .. }
- | RadrootsTransportSatisfactionPolicy::Quorum { .. } => Ok(policy.is_satisfied_by(
+ if policy.target_satisfaction_class().is_none() {
+ return Ok(true);
+ }
+ match policy.required_target_fingerprints() {
+ None => Ok(policy.is_satisfied_by(
target_states.len(),
satisfied_target_count_for_policy(policy, target_states),
)?),
- RadrootsTransportSatisfactionPolicy::RequiredTargets { targets, .. } => {
+ Some(targets) => {
policy.required_target_count(target_states.len())?;
Ok(satisfied_target_count_for_policy(policy, target_states) == targets.len())
}
@@ -427,8 +427,8 @@ fn dispatch_satisfaction_policy(
policy: &RadrootsTransportSatisfactionPolicy,
) -> RadrootsTransportSatisfactionPolicy {
match policy.target_satisfaction_class() {
- Some(class) => RadrootsTransportSatisfactionPolicy::All { class },
- None => RadrootsTransportSatisfactionPolicy::NoWait,
+ Some(class) => RadrootsTransportSatisfactionPolicy::all(class),
+ None => RadrootsTransportSatisfactionPolicy::no_wait(),
}
}
@@ -1403,7 +1403,8 @@ mod tests {
payload: opaque_payload(),
plans: vec![RadrootsRuntimeDeliveryPlan {
delivery_plan_id: 7,
- satisfaction_policy: RadrootsTransportSatisfactionPolicy::quorum_accepted(2),
+ satisfaction_policy: RadrootsTransportSatisfactionPolicy::quorum_accepted(2)
+ .expect("valid quorum"),
targets: vec![
RadrootsRuntimeDeliveryTarget::ready(
1,
@@ -1459,7 +1460,8 @@ mod tests {
payload: opaque_payload(),
plans: vec![RadrootsRuntimeDeliveryPlan {
delivery_plan_id: 7,
- satisfaction_policy: RadrootsTransportSatisfactionPolicy::quorum_accepted(2),
+ satisfaction_policy: RadrootsTransportSatisfactionPolicy::quorum_accepted(2)
+ .expect("valid quorum"),
targets: vec![
RadrootsRuntimeDeliveryTarget::ready(
1,
diff --git a/crates/transport/src/delivery.rs b/crates/transport/src/delivery.rs
@@ -22,9 +22,23 @@ pub enum RadrootsTransportSatisfactionClass {
DurableOrObserved,
}
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum RadrootsTransportSatisfactionPolicyKind {
+ NoWait,
+ Any,
+ All,
+ Quorum,
+ RequiredTargets,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsTransportSatisfactionPolicy {
+ body: RadrootsTransportSatisfactionPolicyBody,
+}
+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum RadrootsTransportSatisfactionPolicy {
+enum RadrootsTransportSatisfactionPolicyBody {
NoWait,
Any {
class: RadrootsTransportSatisfactionClass,
@@ -44,121 +58,108 @@ pub enum RadrootsTransportSatisfactionPolicy {
impl RadrootsTransportSatisfactionPolicy {
pub fn no_wait() -> Self {
- Self::NoWait
+ Self {
+ body: RadrootsTransportSatisfactionPolicyBody::NoWait,
+ }
}
- pub fn any_accepted() -> Self {
- Self::Any {
- class: RadrootsTransportSatisfactionClass::Accepted,
+ pub fn any(class: RadrootsTransportSatisfactionClass) -> Self {
+ Self {
+ body: RadrootsTransportSatisfactionPolicyBody::Any { class },
}
}
- pub fn all_accepted() -> Self {
- Self::All {
- class: RadrootsTransportSatisfactionClass::Accepted,
+ pub fn all(class: RadrootsTransportSatisfactionClass) -> Self {
+ Self {
+ body: RadrootsTransportSatisfactionPolicyBody::All { class },
}
}
- pub fn quorum_accepted(threshold: u16) -> Self {
- Self::Quorum {
- class: RadrootsTransportSatisfactionClass::Accepted,
- threshold,
+ pub fn quorum(
+ class: RadrootsTransportSatisfactionClass,
+ threshold: u16,
+ ) -> Result<Self, RadrootsTransportError> {
+ if threshold == 0 || usize::from(threshold) > RADROOTS_TRANSPORT_TARGET_MAX_COUNT {
+ return Err(RadrootsTransportError::InvalidSatisfactionPolicy);
}
+ Ok(Self {
+ body: RadrootsTransportSatisfactionPolicyBody::Quorum { class, threshold },
+ })
+ }
+
+ pub fn any_accepted() -> Self {
+ Self::any(RadrootsTransportSatisfactionClass::Accepted)
+ }
+
+ pub fn all_accepted() -> Self {
+ Self::all(RadrootsTransportSatisfactionClass::Accepted)
+ }
+
+ pub fn quorum_accepted(threshold: u16) -> Result<Self, RadrootsTransportError> {
+ Self::quorum(RadrootsTransportSatisfactionClass::Accepted, threshold)
}
pub fn any_forwarded() -> Self {
- Self::Any {
- class: RadrootsTransportSatisfactionClass::Forwarded,
- }
+ Self::any(RadrootsTransportSatisfactionClass::Forwarded)
}
pub fn all_forwarded() -> Self {
- Self::All {
- class: RadrootsTransportSatisfactionClass::Forwarded,
- }
+ Self::all(RadrootsTransportSatisfactionClass::Forwarded)
}
- pub fn quorum_forwarded(threshold: u16) -> Self {
- Self::Quorum {
- class: RadrootsTransportSatisfactionClass::Forwarded,
- threshold,
- }
+ pub fn quorum_forwarded(threshold: u16) -> Result<Self, RadrootsTransportError> {
+ Self::quorum(RadrootsTransportSatisfactionClass::Forwarded, threshold)
}
pub fn any_stored() -> Self {
- Self::Any {
- class: RadrootsTransportSatisfactionClass::Stored,
- }
+ Self::any(RadrootsTransportSatisfactionClass::Stored)
}
pub fn all_stored() -> Self {
- Self::All {
- class: RadrootsTransportSatisfactionClass::Stored,
- }
+ Self::all(RadrootsTransportSatisfactionClass::Stored)
}
- pub fn quorum_stored(threshold: u16) -> Self {
- Self::Quorum {
- class: RadrootsTransportSatisfactionClass::Stored,
- threshold,
- }
+ pub fn quorum_stored(threshold: u16) -> Result<Self, RadrootsTransportError> {
+ Self::quorum(RadrootsTransportSatisfactionClass::Stored, threshold)
}
pub fn any_seen() -> Self {
- Self::Any {
- class: RadrootsTransportSatisfactionClass::Seen,
- }
+ Self::any(RadrootsTransportSatisfactionClass::Seen)
}
pub fn all_seen() -> Self {
- Self::All {
- class: RadrootsTransportSatisfactionClass::Seen,
- }
+ Self::all(RadrootsTransportSatisfactionClass::Seen)
}
- pub fn quorum_seen(threshold: u16) -> Self {
- Self::Quorum {
- class: RadrootsTransportSatisfactionClass::Seen,
- threshold,
- }
+ pub fn quorum_seen(threshold: u16) -> Result<Self, RadrootsTransportError> {
+ Self::quorum(RadrootsTransportSatisfactionClass::Seen, threshold)
}
pub fn any_delivered() -> Self {
- Self::Any {
- class: RadrootsTransportSatisfactionClass::Delivered,
- }
+ Self::any(RadrootsTransportSatisfactionClass::Delivered)
}
pub fn all_delivered() -> Self {
- Self::All {
- class: RadrootsTransportSatisfactionClass::Delivered,
- }
+ Self::all(RadrootsTransportSatisfactionClass::Delivered)
}
- pub fn quorum_delivered(threshold: u16) -> Self {
- Self::Quorum {
- class: RadrootsTransportSatisfactionClass::Delivered,
- threshold,
- }
+ pub fn quorum_delivered(threshold: u16) -> Result<Self, RadrootsTransportError> {
+ Self::quorum(RadrootsTransportSatisfactionClass::Delivered, threshold)
}
pub fn any_durable_or_observed() -> Self {
- Self::Any {
- class: RadrootsTransportSatisfactionClass::DurableOrObserved,
- }
+ Self::any(RadrootsTransportSatisfactionClass::DurableOrObserved)
}
pub fn all_durable_or_observed() -> Self {
- Self::All {
- class: RadrootsTransportSatisfactionClass::DurableOrObserved,
- }
+ Self::all(RadrootsTransportSatisfactionClass::DurableOrObserved)
}
- pub fn quorum_durable_or_observed(threshold: u16) -> Self {
- Self::Quorum {
- class: RadrootsTransportSatisfactionClass::DurableOrObserved,
+ pub fn quorum_durable_or_observed(threshold: u16) -> Result<Self, RadrootsTransportError> {
+ Self::quorum(
+ RadrootsTransportSatisfactionClass::DurableOrObserved,
threshold,
- }
+ )
}
pub fn required_targets(
@@ -167,23 +168,62 @@ impl RadrootsTransportSatisfactionPolicy {
) -> Result<Self, RadrootsTransportError> {
validate_required_targets(&targets)?;
targets.sort();
- Ok(Self::RequiredTargets { class, targets })
+ Ok(Self {
+ body: RadrootsTransportSatisfactionPolicyBody::RequiredTargets { class, targets },
+ })
+ }
+
+ pub fn kind(&self) -> RadrootsTransportSatisfactionPolicyKind {
+ match &self.body {
+ RadrootsTransportSatisfactionPolicyBody::NoWait => {
+ RadrootsTransportSatisfactionPolicyKind::NoWait
+ }
+ RadrootsTransportSatisfactionPolicyBody::Any { .. } => {
+ RadrootsTransportSatisfactionPolicyKind::Any
+ }
+ RadrootsTransportSatisfactionPolicyBody::All { .. } => {
+ RadrootsTransportSatisfactionPolicyKind::All
+ }
+ RadrootsTransportSatisfactionPolicyBody::Quorum { .. } => {
+ RadrootsTransportSatisfactionPolicyKind::Quorum
+ }
+ RadrootsTransportSatisfactionPolicyBody::RequiredTargets { .. } => {
+ RadrootsTransportSatisfactionPolicyKind::RequiredTargets
+ }
+ }
+ }
+
+ pub fn quorum_threshold(&self) -> Option<u16> {
+ match &self.body {
+ RadrootsTransportSatisfactionPolicyBody::Quorum { threshold, .. } => Some(*threshold),
+ RadrootsTransportSatisfactionPolicyBody::NoWait
+ | RadrootsTransportSatisfactionPolicyBody::Any { .. }
+ | RadrootsTransportSatisfactionPolicyBody::All { .. }
+ | RadrootsTransportSatisfactionPolicyBody::RequiredTargets { .. } => None,
+ }
}
pub fn target_satisfaction_class(&self) -> Option<RadrootsTransportSatisfactionClass> {
- match self {
- Self::NoWait => None,
- Self::Any { class }
- | Self::All { class }
- | Self::Quorum { class, .. }
- | Self::RequiredTargets { class, .. } => Some(*class),
+ match &self.body {
+ RadrootsTransportSatisfactionPolicyBody::NoWait => None,
+ RadrootsTransportSatisfactionPolicyBody::Any { class }
+ | RadrootsTransportSatisfactionPolicyBody::All { class }
+ | RadrootsTransportSatisfactionPolicyBody::Quorum { class, .. }
+ | RadrootsTransportSatisfactionPolicyBody::RequiredTargets { class, .. } => {
+ Some(*class)
+ }
}
}
pub fn required_target_fingerprints(&self) -> Option<&[RadrootsTransportTargetFingerprint]> {
- match self {
- Self::RequiredTargets { targets, .. } => Some(targets),
- Self::NoWait | Self::Any { .. } | Self::All { .. } | Self::Quorum { .. } => None,
+ match &self.body {
+ RadrootsTransportSatisfactionPolicyBody::RequiredTargets { targets, .. } => {
+ Some(targets)
+ }
+ RadrootsTransportSatisfactionPolicyBody::NoWait
+ | RadrootsTransportSatisfactionPolicyBody::Any { .. }
+ | RadrootsTransportSatisfactionPolicyBody::All { .. }
+ | RadrootsTransportSatisfactionPolicyBody::Quorum { .. } => None,
}
}
@@ -191,20 +231,22 @@ impl RadrootsTransportSatisfactionPolicy {
&self,
total_targets: usize,
) -> Result<usize, RadrootsTransportError> {
- if total_targets == 0 && !matches!(self, Self::NoWait) {
+ if total_targets == 0 && self.kind() != RadrootsTransportSatisfactionPolicyKind::NoWait {
return Err(RadrootsTransportError::InvalidSatisfactionPolicy);
}
- match self {
- Self::NoWait => Ok(0),
- Self::All { .. } => Ok(total_targets),
- Self::Any { .. } => Ok(1),
- Self::Quorum { threshold, .. }
+ match &self.body {
+ RadrootsTransportSatisfactionPolicyBody::NoWait => Ok(0),
+ RadrootsTransportSatisfactionPolicyBody::All { .. } => Ok(total_targets),
+ RadrootsTransportSatisfactionPolicyBody::Any { .. } => Ok(1),
+ RadrootsTransportSatisfactionPolicyBody::Quorum { threshold, .. }
if *threshold > 0 && usize::from(*threshold) <= total_targets =>
{
Ok(usize::from(*threshold))
}
- Self::Quorum { .. } => Err(RadrootsTransportError::InvalidSatisfactionPolicy),
- Self::RequiredTargets { targets, .. } => {
+ RadrootsTransportSatisfactionPolicyBody::Quorum { .. } => {
+ Err(RadrootsTransportError::InvalidSatisfactionPolicy)
+ }
+ RadrootsTransportSatisfactionPolicyBody::RequiredTargets { targets, .. } => {
validate_required_targets(targets)?;
if targets.len() > total_targets {
return Err(RadrootsTransportError::InvalidSatisfactionPolicy);
@@ -219,7 +261,7 @@ impl RadrootsTransportSatisfactionPolicy {
total_targets: usize,
satisfied_targets: usize,
) -> Result<bool, RadrootsTransportError> {
- if matches!(self, Self::RequiredTargets { .. }) {
+ if self.kind() == RadrootsTransportSatisfactionPolicyKind::RequiredTargets {
return Err(RadrootsTransportError::InvalidSatisfactionPolicy);
}
let required = self.required_target_count(total_targets)?;
@@ -231,7 +273,8 @@ impl RadrootsTransportSatisfactionPolicy {
target_set: &RadrootsTransportTargetSet,
) -> Result<(), RadrootsTransportError> {
self.required_target_count(target_set.len())?;
- if let Self::RequiredTargets { targets, .. } = self {
+ if let RadrootsTransportSatisfactionPolicyBody::RequiredTargets { targets, .. } = &self.body
+ {
for required in targets {
if !target_set
.targets()
@@ -247,7 +290,18 @@ impl RadrootsTransportSatisfactionPolicy {
}
#[cfg(feature = "serde")]
+impl serde::Serialize for RadrootsTransportSatisfactionPolicy {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ self.body.serialize(serializer)
+ }
+}
+
+#[cfg(feature = "serde")]
#[derive(serde::Deserialize)]
+#[serde(deny_unknown_fields)]
enum RadrootsTransportSatisfactionPolicyWire {
NoWait,
Any {
@@ -262,6 +316,7 @@ enum RadrootsTransportSatisfactionPolicyWire {
},
RequiredTargets {
class: RadrootsTransportSatisfactionClass,
+ #[serde(deserialize_with = "deserialize_required_target_fingerprints")]
targets: Vec<RadrootsTransportTargetFingerprint>,
},
}
@@ -273,17 +328,12 @@ impl<'de> serde::Deserialize<'de> for RadrootsTransportSatisfactionPolicy {
D: serde::Deserializer<'de>,
{
match RadrootsTransportSatisfactionPolicyWire::deserialize(deserializer)? {
- RadrootsTransportSatisfactionPolicyWire::NoWait => Ok(Self::NoWait),
- RadrootsTransportSatisfactionPolicyWire::Any { class } => Ok(Self::Any { class }),
- RadrootsTransportSatisfactionPolicyWire::All { class } => Ok(Self::All { class }),
- RadrootsTransportSatisfactionPolicyWire::Quorum { class, threshold }
- if threshold > 0 =>
- {
- Ok(Self::Quorum { class, threshold })
+ RadrootsTransportSatisfactionPolicyWire::NoWait => Ok(Self::no_wait()),
+ RadrootsTransportSatisfactionPolicyWire::Any { class } => Ok(Self::any(class)),
+ RadrootsTransportSatisfactionPolicyWire::All { class } => Ok(Self::all(class)),
+ RadrootsTransportSatisfactionPolicyWire::Quorum { class, threshold } => {
+ Self::quorum(class, threshold).map_err(serde::de::Error::custom)
}
- RadrootsTransportSatisfactionPolicyWire::Quorum { .. } => Err(
- serde::de::Error::custom(RadrootsTransportError::InvalidSatisfactionPolicy),
- ),
RadrootsTransportSatisfactionPolicyWire::RequiredTargets { class, targets } => {
Self::required_targets(class, targets).map_err(serde::de::Error::custom)
}
@@ -291,6 +341,20 @@ impl<'de> serde::Deserialize<'de> for RadrootsTransportSatisfactionPolicy {
}
}
+#[cfg(feature = "serde")]
+fn deserialize_required_target_fingerprints<'de, D>(
+ deserializer: D,
+) -> Result<Vec<RadrootsTransportTargetFingerprint>, D::Error>
+where
+ D: serde::Deserializer<'de>,
+{
+ crate::serde_bounds::deserialize_vec(
+ deserializer,
+ "required_target_count",
+ RADROOTS_TRANSPORT_TARGET_MAX_COUNT,
+ )
+}
+
fn validate_required_targets(
targets: &[RadrootsTransportTargetFingerprint],
) -> Result<(), RadrootsTransportError> {
@@ -604,23 +668,23 @@ impl RadrootsTransportDeliveryReceipt {
policy: &RadrootsTransportSatisfactionPolicy,
) -> Result<bool, RadrootsTransportError> {
policy.validate_for_target_set(&self.target_set)?;
- match policy {
- RadrootsTransportSatisfactionPolicy::NoWait => Ok(true),
- RadrootsTransportSatisfactionPolicy::Any { class }
- | RadrootsTransportSatisfactionPolicy::All { class }
- | RadrootsTransportSatisfactionPolicy::Quorum { class, .. } => {
- policy.is_satisfied_by(self.target_set.len(), self.satisfied_target_count(*class))
- }
- RadrootsTransportSatisfactionPolicy::RequiredTargets { class, targets } => {
- validate_required_targets(targets)?;
- Ok(targets.iter().all(|required| {
- self.target_receipts.iter().any(|receipt| {
- receipt.target.fingerprint() == required
- && receipt.status.counts_as_satisfied(*class)
- })
- }))
- }
- }
+ if policy.kind() == RadrootsTransportSatisfactionPolicyKind::NoWait {
+ return Ok(true);
+ }
+ let class = policy
+ .target_satisfaction_class()
+ .ok_or(RadrootsTransportError::InvalidSatisfactionPolicy)?;
+ let Some(targets) = policy.required_target_fingerprints() else {
+ return policy
+ .is_satisfied_by(self.target_set.len(), self.satisfied_target_count(class));
+ };
+ validate_required_targets(targets)?;
+ Ok(targets.iter().all(|required| {
+ self.target_receipts.iter().any(|receipt| {
+ receipt.target.fingerprint() == required
+ && receipt.status.counts_as_satisfied(class)
+ })
+ }))
}
}
diff --git a/crates/transport/src/lib.rs b/crates/transport/src/lib.rs
@@ -20,7 +20,8 @@ mod transport;
pub use delivery::{
RADROOTS_TRANSPORT_DELIVERY_REQUEST_ID_MAX_BYTES, RadrootsTransportDeliveryReceipt,
RadrootsTransportDeliveryRequest, RadrootsTransportSatisfactionClass,
- RadrootsTransportSatisfactionPolicy, RadrootsTransportTargetReceipt,
+ RadrootsTransportSatisfactionPolicy, RadrootsTransportSatisfactionPolicyKind,
+ RadrootsTransportTargetReceipt,
};
pub use error::RadrootsTransportError;
pub use kind::{
diff --git a/crates/transport/tests/source_boundary.rs b/crates/transport/tests/source_boundary.rs
@@ -725,11 +725,11 @@ fn required_target_semantics_stay_fingerprint_exact() {
let nostr_publish_source =
read_source(crates_root.join("transport_nostr/src/publish.rs").as_path());
for required in [
- "RadrootsTransportSatisfactionPolicy::RequiredTargets { class, targets } =>",
+ "let Some(targets) = policy.required_target_fingerprints() else",
"let mut satisfied_required_targets = BTreeSet::new();",
"targets.contains(target.fingerprint())",
- "counts_as_satisfied(*class)",
- "targets\n .iter()\n .all(|target| satisfied_required_targets.contains(target))",
+ "counts_as_satisfied(class)",
+ "targets\n .iter()\n .all(|target| satisfied_required_targets.contains(target))",
] {
assert!(
nostr_publish_source.contains(required),
@@ -741,11 +741,11 @@ fn required_target_semantics_stay_fingerprint_exact() {
read_source(crates_root.join("transport_nostr/src/outbox.rs").as_path());
let publishable_relays_source = source_between(
nostr_outbox_source.as_str(),
- "let required_targets = match &plan.satisfaction_policy",
+ "let required_targets = plan",
"Ok(PublishableRelays {",
);
for required in [
- "RadrootsTransportSatisfactionPolicy::RequiredTargets { targets, .. } =>",
+ ".required_target_fingerprints()",
".is_none_or(|required| required.contains(&target.endpoint_fingerprint))",
".is_some_and(|required| required.contains(&target.endpoint_fingerprint))",
"required_targets.is_none() || required_for_satisfaction",
diff --git a/crates/transport/tests/transport.rs b/crates/transport/tests/transport.rs
@@ -11,8 +11,9 @@ use radroots_transport::{
RadrootsTransportFetchRequest, RadrootsTransportFuture, RadrootsTransportImplementationState,
RadrootsTransportKind, RadrootsTransportMeshScopeId, RadrootsTransportOutcome,
RadrootsTransportOutcomeKind, RadrootsTransportPayload, RadrootsTransportSatisfactionClass,
- RadrootsTransportSatisfactionPolicy, RadrootsTransportStatus, RadrootsTransportTarget,
- RadrootsTransportTargetFingerprint, RadrootsTransportTargetLabel,
+ RadrootsTransportSatisfactionPolicy, RadrootsTransportSatisfactionPolicyKind,
+ RadrootsTransportStatus, RadrootsTransportTarget, RadrootsTransportTargetFingerprint,
+ RadrootsTransportTargetLabel,
RadrootsTransportTargetReceipt, RadrootsTransportTargetSet, RadrootsTransportTargetUri,
ReticulumCapabilityReportV1, ReticulumDestinationV1, ReticulumDuplicateFragmentBehaviorV1,
ReticulumFragmentIntegrityV1, ReticulumFragmentationModeV1, ReticulumGatewaySemanticsV1,
@@ -314,14 +315,20 @@ fn satisfaction_policy_counts_target_statuses() {
let no_wait = RadrootsTransportSatisfactionPolicy::no_wait();
let all = RadrootsTransportSatisfactionPolicy::all_accepted();
let any = RadrootsTransportSatisfactionPolicy::any_accepted();
- let two = RadrootsTransportSatisfactionPolicy::quorum_accepted(2);
- let delivered = RadrootsTransportSatisfactionPolicy::quorum_delivered(2);
+ let two = RadrootsTransportSatisfactionPolicy::quorum_accepted(2).expect("valid quorum");
+ let delivered = RadrootsTransportSatisfactionPolicy::quorum_delivered(2).expect("valid quorum");
let forwarded = RadrootsTransportSatisfactionPolicy::any_forwarded();
let stored = RadrootsTransportSatisfactionPolicy::all_stored();
- let seen = RadrootsTransportSatisfactionPolicy::quorum_seen(2);
+ let seen = RadrootsTransportSatisfactionPolicy::quorum_seen(2).expect("valid quorum");
let durable_or_observed = RadrootsTransportSatisfactionPolicy::any_durable_or_observed();
assert_eq!(no_wait.required_target_count(0).expect("no wait"), 0);
+ assert_eq!(
+ no_wait.kind(),
+ RadrootsTransportSatisfactionPolicyKind::NoWait
+ );
+ assert_eq!(two.kind(), RadrootsTransportSatisfactionPolicyKind::Quorum);
+ assert_eq!(two.quorum_threshold(), Some(2));
assert_eq!(no_wait.required_target_count(3).expect("no wait"), 0);
assert!(no_wait.is_satisfied_by(0, 0).expect("no wait"));
assert_ne!(no_wait, all);
@@ -360,7 +367,7 @@ fn satisfaction_policy_counts_target_statuses() {
RadrootsTransportSatisfactionClass::Forwarded,
),
(
- RadrootsTransportSatisfactionPolicy::quorum_forwarded(2),
+ RadrootsTransportSatisfactionPolicy::quorum_forwarded(2).expect("valid quorum"),
RadrootsTransportSatisfactionClass::Forwarded,
),
(
@@ -368,7 +375,7 @@ fn satisfaction_policy_counts_target_statuses() {
RadrootsTransportSatisfactionClass::Stored,
),
(
- RadrootsTransportSatisfactionPolicy::quorum_stored(2),
+ RadrootsTransportSatisfactionPolicy::quorum_stored(2).expect("valid quorum"),
RadrootsTransportSatisfactionClass::Stored,
),
(
@@ -384,7 +391,8 @@ fn satisfaction_policy_counts_target_statuses() {
RadrootsTransportSatisfactionClass::DurableOrObserved,
),
(
- RadrootsTransportSatisfactionPolicy::quorum_durable_or_observed(2),
+ RadrootsTransportSatisfactionPolicy::quorum_durable_or_observed(2)
+ .expect("valid quorum"),
RadrootsTransportSatisfactionClass::DurableOrObserved,
),
] {
@@ -395,9 +403,7 @@ fn satisfaction_policy_counts_target_statuses() {
RadrootsTransportError::InvalidSatisfactionPolicy
);
assert_eq!(
- RadrootsTransportSatisfactionPolicy::quorum_accepted(0)
- .is_satisfied_by(3, 0)
- .expect_err("zero required targets"),
+ RadrootsTransportSatisfactionPolicy::quorum_accepted(0).expect_err("zero required targets"),
RadrootsTransportError::InvalidSatisfactionPolicy
);
}
@@ -890,6 +896,7 @@ fn satisfaction_and_target_status_cover_all_contract_states() {
);
assert_eq!(
RadrootsTransportSatisfactionPolicy::quorum_accepted(4)
+ .expect("bounded quorum")
.required_target_count(3)
.expect_err("at least too high"),
RadrootsTransportError::InvalidSatisfactionPolicy
@@ -1346,7 +1353,8 @@ fn delivery_contract_covers_every_policy_and_receipt_path() {
RadrootsTransportSatisfactionClass::Delivered,
),
(
- RadrootsTransportSatisfactionPolicy::quorum_durable_or_observed(2),
+ RadrootsTransportSatisfactionPolicy::quorum_durable_or_observed(2)
+ .expect("valid quorum"),
RadrootsTransportSatisfactionClass::DurableOrObserved,
),
] {
@@ -1356,7 +1364,7 @@ fn delivery_contract_covers_every_policy_and_receipt_path() {
RadrootsTransportSatisfactionPolicy::no_wait(),
RadrootsTransportSatisfactionPolicy::any_accepted(),
RadrootsTransportSatisfactionPolicy::all_accepted(),
- RadrootsTransportSatisfactionPolicy::quorum_accepted(1),
+ RadrootsTransportSatisfactionPolicy::quorum_accepted(1).expect("valid quorum"),
] {
assert!(policy.required_target_fingerprints().is_none());
}
@@ -1402,38 +1410,25 @@ fn delivery_contract_covers_every_policy_and_receipt_path() {
.is_satisfied_by(&RadrootsTransportSatisfactionPolicy::all_accepted())
.expect("all")
);
- assert!(
- receipt
- .is_satisfied_by(&RadrootsTransportSatisfactionPolicy::quorum_accepted(1))
- .expect("quorum")
- );
+ let quorum = RadrootsTransportSatisfactionPolicy::quorum_accepted(1).expect("valid quorum");
+ assert!(receipt.is_satisfied_by(&quorum).expect("quorum"));
assert!(!receipt.is_satisfied_by(&required).expect("required"));
- let invalid_empty = RadrootsTransportSatisfactionPolicy::RequiredTargets {
- class: RadrootsTransportSatisfactionClass::Accepted,
- targets: Vec::new(),
- };
assert_eq!(
- invalid_empty
- .required_target_count(2)
- .expect_err("empty required set"),
- RadrootsTransportError::EmptyRequiredTargetSet
- );
- assert_eq!(
- receipt
- .is_satisfied_by(&invalid_empty)
- .expect_err("empty required receipt policy"),
+ RadrootsTransportSatisfactionPolicy::required_targets(
+ RadrootsTransportSatisfactionClass::Accepted,
+ Vec::new(),
+ )
+ .expect_err("empty required set"),
RadrootsTransportError::EmptyRequiredTargetSet
);
- let duplicate = RadrootsTransportSatisfactionPolicy::RequiredTargets {
- class: RadrootsTransportSatisfactionClass::Accepted,
- targets: vec![one.fingerprint().clone(), one.fingerprint().clone()],
- };
assert_eq!(
- duplicate
- .required_target_count(2)
- .expect_err("duplicate required set"),
+ RadrootsTransportSatisfactionPolicy::required_targets(
+ RadrootsTransportSatisfactionClass::Accepted,
+ vec![one.fingerprint().clone(), one.fingerprint().clone()],
+ )
+ .expect_err("duplicate required set"),
RadrootsTransportError::DuplicateRequiredTargetFingerprint
);
}
@@ -1711,6 +1706,47 @@ fn delivery_request_and_receipt_deserialization_revalidates_invariants() {
}))
.is_err()
);
+ let exact_required = (0..RADROOTS_TRANSPORT_TARGET_MAX_COUNT)
+ .map(|index| format!("{index:064x}"))
+ .collect::<Vec<_>>();
+ let exact_required_wire = serde_json::json!({
+ "RequiredTargets": {
+ "class": "Accepted",
+ "targets": exact_required,
+ }
+ });
+ assert_eq!(
+ serde_json::from_value::<RadrootsTransportSatisfactionPolicy>(exact_required_wire)
+ .expect("decode exact required-target policy")
+ .required_target_fingerprints()
+ .expect("required targets")
+ .len(),
+ RADROOTS_TRANSPORT_TARGET_MAX_COUNT
+ );
+ let one_over_required = (0..=RADROOTS_TRANSPORT_TARGET_MAX_COUNT)
+ .map(|index| format!("{index:064x}"))
+ .collect::<Vec<_>>();
+ let one_over_required_wire = serde_json::json!({
+ "RequiredTargets": {
+ "class": "Accepted",
+ "targets": one_over_required,
+ }
+ });
+ assert!(
+ serde_json::from_value::<RadrootsTransportSatisfactionPolicy>(one_over_required_wire)
+ .expect_err("reject one-over required-target wire")
+ .to_string()
+ .contains("required_target_count")
+ );
+ assert!(
+ serde_json::from_value::<RadrootsTransportSatisfactionPolicy>(serde_json::json!({
+ "Any": {
+ "class": "Accepted",
+ "unknown": true,
+ }
+ }))
+ .is_err()
+ );
}
#[test]
diff --git a/crates/transport_nostr/src/outbox.rs b/crates/transport_nostr/src/outbox.rs
@@ -181,7 +181,7 @@ where
let target_strings = targets.relay_strings();
let active_delivery_plan_id = publishable.active_delivery_plan_id;
let request = RadrootsRelayPublishRequest::new(signed_event.clone(), targets, now_ms)?
- .with_satisfaction_policy(RadrootsTransportSatisfactionPolicy::NoWait)
+ .with_satisfaction_policy(RadrootsTransportSatisfactionPolicy::no_wait())
.try_with_idempotency_key(outbox_publish_idempotency_key(
claimed.outbox_event_id,
claimed.attempt_count,
@@ -885,15 +885,10 @@ async fn publishable_relays(
))
})?;
let satisfaction_required_count = plan.required_success_count as usize;
- let required_targets = match &plan.satisfaction_policy {
- RadrootsTransportSatisfactionPolicy::RequiredTargets { targets, .. } => {
- Some(targets.clone())
- }
- RadrootsTransportSatisfactionPolicy::NoWait
- | RadrootsTransportSatisfactionPolicy::Any { .. }
- | RadrootsTransportSatisfactionPolicy::All { .. }
- | RadrootsTransportSatisfactionPolicy::Quorum { .. } => None,
- };
+ let required_targets = plan
+ .satisfaction_policy
+ .required_target_fingerprints()
+ .map(<[_]>::to_vec);
let active_targets = targets
.iter()
.filter(|target| target.delivery_plan_id == active_delivery_plan_id)
@@ -1050,33 +1045,26 @@ fn satisfaction_policy_for_remaining_count(
exact_required_targets: Option<&[RadrootsTransportTargetFingerprint]>,
) -> RadrootsTransportSatisfactionPolicy {
if let Some(targets) = exact_required_targets {
- return RadrootsTransportSatisfactionPolicy::RequiredTargets {
- class: satisfaction_class,
- targets: targets.to_vec(),
- };
+ return RadrootsTransportSatisfactionPolicy::required_targets(
+ satisfaction_class,
+ targets.to_vec(),
+ )
+ .expect("remaining required targets retain a validated nonempty unique set");
}
if remaining_satisfaction_count >= target_count {
- return RadrootsTransportSatisfactionPolicy::All {
- class: satisfaction_class,
- };
+ return RadrootsTransportSatisfactionPolicy::all(satisfaction_class);
}
if remaining_satisfaction_count == 0 {
- return RadrootsTransportSatisfactionPolicy::NoWait;
+ return RadrootsTransportSatisfactionPolicy::no_wait();
}
if remaining_satisfaction_count == 1 {
- return RadrootsTransportSatisfactionPolicy::Any {
- class: satisfaction_class,
- };
+ return RadrootsTransportSatisfactionPolicy::any(satisfaction_class);
}
let Ok(count) = u16::try_from(remaining_satisfaction_count) else {
- return RadrootsTransportSatisfactionPolicy::All {
- class: satisfaction_class,
- };
+ return RadrootsTransportSatisfactionPolicy::all(satisfaction_class);
};
- RadrootsTransportSatisfactionPolicy::Quorum {
- class: satisfaction_class,
- threshold: count,
- }
+ RadrootsTransportSatisfactionPolicy::quorum(satisfaction_class, count)
+ .expect("remaining quorum is bounded by the validated target set")
}
async fn ingest_publish_observation(
@@ -1151,7 +1139,7 @@ mod tests {
3,
None
),
- RadrootsTransportSatisfactionPolicy::quorum_delivered(2)
+ RadrootsTransportSatisfactionPolicy::quorum_delivered(2).expect("valid quorum")
);
let required_target =
RadrootsTransportTarget::nostr_relay("wss://relay.example").expect("required target");
@@ -1175,9 +1163,7 @@ mod tests {
usize::from(u16::MAX) + 2,
None,
),
- RadrootsTransportSatisfactionPolicy::All {
- class: RadrootsTransportSatisfactionClass::Accepted,
- }
+ RadrootsTransportSatisfactionPolicy::all(RadrootsTransportSatisfactionClass::Accepted,)
);
assert_eq!(
satisfaction_policy_for_remaining_count(
@@ -1186,7 +1172,7 @@ mod tests {
3,
None,
),
- RadrootsTransportSatisfactionPolicy::NoWait
+ RadrootsTransportSatisfactionPolicy::no_wait()
);
assert!(counts_as_accepted_for_plan(
diff --git a/crates/transport_nostr/src/publish.rs b/crates/transport_nostr/src/publish.rs
@@ -266,7 +266,7 @@ where
let publish_request =
RadrootsRelayPublishRequest::new(signed_event, targets, request.now_ms())
.map_err(nostr_error_to_transport_error)?
- .with_satisfaction_policy(RadrootsTransportSatisfactionPolicy::NoWait)
+ .with_satisfaction_policy(RadrootsTransportSatisfactionPolicy::no_wait())
.try_with_idempotency_key(request.request_id())
.map_err(nostr_error_to_transport_error)?;
let relay_receipts = match publish_signed_event(&self.adapter, publish_request).await {
@@ -757,45 +757,41 @@ fn relay_publish_satisfies_policy(
target_count: usize,
relays: &[RadrootsRelayPublishRelayReceipt],
) -> Result<bool, RadrootsRelayTransportError> {
- match policy {
- RadrootsTransportSatisfactionPolicy::NoWait => Ok(true),
- RadrootsTransportSatisfactionPolicy::Any { class }
- | RadrootsTransportSatisfactionPolicy::All { class }
- | RadrootsTransportSatisfactionPolicy::Quorum { class, .. } => {
- let satisfied_count = relays
- .iter()
- .filter(|receipt| {
- relay_receipt_counts_toward_quorum(receipt)
- && receipt
- .outcome
- .to_transport_outcome()
- .status
- .counts_as_satisfied(*class)
- })
- .count();
- Ok(policy.is_satisfied_by(target_count, satisfied_count)?)
- }
- RadrootsTransportSatisfactionPolicy::RequiredTargets { class, targets } => {
- policy.required_target_count(target_count)?;
- let mut satisfied_required_targets = BTreeSet::new();
- for receipt in relays {
- let target = RadrootsTransportTarget::nostr_relay(&receipt.relay_url)?;
- if targets.contains(target.fingerprint())
- && relay_receipt_counts_toward_quorum(receipt)
+ let Some(class) = policy.target_satisfaction_class() else {
+ return Ok(true);
+ };
+ let Some(targets) = policy.required_target_fingerprints() else {
+ let satisfied_count = relays
+ .iter()
+ .filter(|receipt| {
+ relay_receipt_counts_toward_quorum(receipt)
&& receipt
.outcome
.to_transport_outcome()
.status
- .counts_as_satisfied(*class)
- {
- satisfied_required_targets.insert(target.fingerprint().clone());
- }
- }
- Ok(targets
- .iter()
- .all(|target| satisfied_required_targets.contains(target)))
+ .counts_as_satisfied(class)
+ })
+ .count();
+ return Ok(policy.is_satisfied_by(target_count, satisfied_count)?);
+ };
+ policy.required_target_count(target_count)?;
+ let mut satisfied_required_targets = BTreeSet::new();
+ for receipt in relays {
+ let target = RadrootsTransportTarget::nostr_relay(&receipt.relay_url)?;
+ if targets.contains(target.fingerprint())
+ && relay_receipt_counts_toward_quorum(receipt)
+ && receipt
+ .outcome
+ .to_transport_outcome()
+ .status
+ .counts_as_satisfied(class)
+ {
+ satisfied_required_targets.insert(target.fingerprint().clone());
}
}
+ Ok(targets
+ .iter()
+ .all(|target| satisfied_required_targets.contains(target)))
}
fn relay_receipt_matches_target(
diff --git a/crates/transport_nostr/tests/transport.rs b/crates/transport_nostr/tests/transport.rs
@@ -1124,7 +1124,9 @@ async fn mock_publish_preserves_exact_raw_json_and_counts_outcomes() {
1_000,
)
.expect("publish request")
- .with_satisfaction_policy(RadrootsTransportSatisfactionPolicy::quorum_accepted(2)),
+ .with_satisfaction_policy(
+ RadrootsTransportSatisfactionPolicy::quorum_accepted(2).expect("valid quorum"),
+ ),
)
.await
.expect("publish");
@@ -1662,7 +1664,7 @@ async fn publish_all_policy_uses_requested_target_count() {
&PartialPublishAdapter,
RadrootsRelayPublishRequest::new(verified_signed_event(signed), targets, 1_081)
.expect("publish request")
- .with_satisfaction_policy(RadrootsTransportSatisfactionPolicy::NoWait),
+ .with_satisfaction_policy(RadrootsTransportSatisfactionPolicy::no_wait()),
)
.await
.expect("no-wait publish");
@@ -4790,7 +4792,7 @@ async fn outbox_publish_marks_published_when_delivery_plan_satisfaction_is_met_w
RELAY_SECONDARY_WSS.to_owned(),
RELAY_TERTIARY_WSS.to_owned(),
],
- RadrootsTransportSatisfactionPolicy::quorum_accepted(2),
+ RadrootsTransportSatisfactionPolicy::quorum_accepted(2).expect("valid quorum"),
))
.await
.expect("enqueue");