Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions zakura/pir-transparent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## [Unreleased]

### Changed

- A publication ending below the wallet's target retains its public retry wait
even when private content, a work budget or a contradiction prevents the pass
from completing. An overload preserves its longer wait. Coverage and failed-entry
counts are unchanged, so the content-aware retry and hold policy still apply.

## [0.0.1-rc1] - 2026-10-14

### Added
Expand Down
8 changes: 8 additions & 0 deletions zakura/pir-transparent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,14 @@ retrieval reports complete, and any batch with failed entries as stalled,
whatever else stopped its pass. Read a fresh watch set when applying commits
grows an address window. `PassPlan::next` turns both into the next step.

For service scheduling, preserve `report.progress.retry()` when it is
`Retry::After`: publication lag and capacity refusals are public, independently
of private content failures. A publication ending below the target remains
`Behind` even when a private entry, contradiction or work budget prevents
completion; an overload keeps its longer wait. This does not grant coverage or
remove a failed-entry hold. The content-aware `report.retry()` still reports
`Stalled` for failed entries and must not erase that public service wait.

A private row whose content fails to decode or check out (a directory entry or
page fragment that is malformed, missing, held twice or out of place, or a run
link the shard cannot follow), or a record that contradicts the companion,
Expand Down
20 changes: 17 additions & 3 deletions zakura/pir-transparent/src/progress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,11 @@ impl Progress {
impl Progress {
/// Maps the reference client's report onto the adapter's narrower contract.
///
/// A `clamped` pass synced to the publication's end below the wallet's target,
/// so even its completion is [`Outcome::Behind`].
/// A `clamped` pass synced to a publication ending below the wallet's target.
/// Preserve that public wait even if private contents prevented completion;
/// failure counts remain separate. A capacity refusal keeps its longer wait.
pub(crate) fn from_sync_report(report: &SyncReport, clamped: bool) -> Self {
let outcome = match &report.completion {
Completion::Complete if clamped => Outcome::Behind,
Completion::Complete => Outcome::Complete,
Completion::Incomplete { reason, .. } => match reason {
IncompleteReason::QueryBudget
Expand All @@ -134,5 +134,19 @@ impl Progress {
covered_through: report.covered_through,
outcome,
}
.with_publication_wait(clamped)
}

/// A public publication boundary must not disappear because of private
/// content, a work budget, or a contradiction that left no sync report.
pub(crate) fn with_publication_wait(self, clamped: bool) -> Self {
if clamped && self.outcome != Outcome::Overloaded {
Self {
outcome: Outcome::Behind,
..self
}
} else {
self
}
}
}
3 changes: 2 additions & 1 deletion zakura/pir-transparent/src/transparent_history_recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,8 @@ impl TransparentHistoryRecovery {
Progress {
covered_through: 0,
outcome: Outcome::Stalled,
},
}
.with_publication_wait(sync_target.clamped),
FailedEntries::default(),
),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ fn report_outcomes_map_to_progress() {
outcome,
}
);
// A pass clamped to the publication's end is behind even when it
// completes; an incomplete one keeps its reason.
let clamped = if outcome == Outcome::Complete {
Outcome::Behind
// Private incompleteness cannot remove a public publication wait.
// A capacity refusal preserves its longer wait instead.
let clamped = if outcome == Outcome::Overloaded {
Outcome::Overloaded
} else {
outcome
Outcome::Behind
};
assert_eq!(
Progress::from_sync_report(&report, true),
Expand Down
31 changes: 31 additions & 0 deletions zakura/pir-transparent/tests/private_recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1693,6 +1693,37 @@ fn a_malformed_entry_fails_alone_and_its_third_failure_holds_the_account() {
f.assert_private(&[0]);
}

/// Publication lag is public even when a selected entry fails on content.
/// The actual source and adapter must retain the same service wait in both
/// cases; a coordinator must not infer it from synthetic fixture outcomes.
#[test]
fn a_malformed_entry_preserves_the_publication_wait() {
let _heavy = HEAVY.lock().unwrap_or_else(PoisonError::into_inner);
let published = H0 + 649;
for malformed in [false, true] {
let mut f = Fixture::new(H0 + 250, published + 1);
let (a0, a1) = (
f.derived(TransparentKeyScope::EXTERNAL, 0),
f.derived(TransparentKeyScope::EXTERNAL, 1),
);
f.publish(&failing_entry(a0, a1, published, malformed), SEAL);
let pass = f.drive();
assert_eq!(pass.report.failed_entries.entries, usize::from(malformed));
assert_eq!(
pass.report.progress.retry(),
Retry::After(Retry::BEHIND),
"a failed private entry must not remove the public publication wait"
);
// The private result still records the failure and cannot qualify
// coverage through the wallet's target.
assert!(pass.report.progress.covered_through < published + 1);
if malformed {
assert_eq!(pass.report.retry(), Retry::Stalled);
assert_eq!(pass.report.failed_entries.held, 0);
}
}
}

/// A service that publishes the wallet's directory entry malformed learns nothing
/// from the sync it answers when the honest entry is inline: the wallet sends the
/// same requests as it does to the honest service.
Expand Down
Loading