Skip to content

fix(sofi): the producer and the verifier read prior attempts the same way - #952

Merged
cryptskii merged 1 commit into
mainfrom
fix/sofi-prior-attempt-acquisition
Sep 21, 2026
Merged

cryptskii merged 1 commit into
mainfrom
fix/sofi-prior-attempt-acquisition

Conversation

@cryptskii

Copy link
Copy Markdown
Collaborator

The producer and the verifier read prior attempts the same way

No retry in the system could install. Conformance item 5 requires the key an attempt skips past to have a permanent storage resolution. acquire_conformance_evidence handed conformance an empty prior_attempts map unconditionally, so every attempt above zero answered Unavailable(PriorAttempt) and install_fulfillment refused.

This was proved by execution against the merged tree before it was fixed, with a throwaway probe that was deleted afterwards:

PROBE prior_attempts acquired = {}
PROBE conformance at attempt 1 = Unavailable(PriorAttempt { vault_id: 85 42 c4 96 …, attempt: 0 })
PROBE install at attempt 1     = Err(Unavailable(PriorAttempt { vault_id: 85 42 c4 96 …, attempt: 0 }))

The verifier's resolution did read those cells — in its own inline loop, and only the key immediately before. Two paths, two behaviours, one question.

What changes

Per the ruling in §44.4, R9 and R12 now share one acquisition. acquire_prior_attempts(set, P, F, budget) reads, for every leg the fulfillment names at attempt a, the resolution of K^(0) … K^(a-1) of that leg's vault at its parent root. The install calls it; the resolver calls it and its inline loop is deleted.

  • A key that could not be read is absent from the map, and conformance answers Unavailable for it. Nothing is assumed skipped.
  • An empty map never means there were no prior attempts. It means nothing was read. An attempt of zero has no earlier key and contributes no entry, which is the one case where empty is the truth, and the tests pin that case separately.
  • The per-leg bound is the walk's own (sofi_resolve::WALK_BUDGET), for the same reason: past it the earlier keys are unread, so they are Unavailable and the position waits.

Controls

Executed at attempt 1 and attempt 2, as the ruling asks. The second is what proves the acquisition reads the whole run rather than just the key before.

Mutation Test that goes red
The map is handed over empty (the state before this change) both of the two below
Only the key immediately before is read an_attempt_above_zero_installs_once_the_keys_it_skips_are_final
A cell read as open is recorded Final an_attempt_whose_earlier_key_is_open_waits_and_installs_nothing

The last two were first run together, and reddened both tests — which proves nothing about which mutation the tests are sensitive to. They were re-run singly, and the table records that result. Each restored byte-identical afterwards.

Gates

  • SDK (release, --features test-utils): sdk::sofi_* 48/0, two tests new. Core (release): sofi:: 181/0.
  • make lint exit 0; ci/production_safety_checks.sh passed; ci/sofi_reachability.py unchanged by this PR.

Spec

§20.2 records the shared acquisition and what an empty map does and does not mean.

… way

No retry in the system could install. Conformance item 5 requires the key
an attempt skips past to have a permanent storage resolution, and
acquire_conformance_evidence handed conformance an empty prior_attempts
map unconditionally, so every attempt above zero answered
Unavailable(PriorAttempt). Proved by execution before it was fixed:

  prior_attempts acquired = {}
  conformance at attempt 1 = Unavailable(PriorAttempt { attempt: 0 })
  install at attempt 1     = Err(Unavailable(PriorAttempt { attempt: 0 }))

The verifier's resolution already read those cells, in its own inline
loop, and only the key immediately before. Owner ruling §44.4: R9 and
R12 share ONE acquisition path that reads the required cells 0..q-1 from
the committed set; an unread or unresolved prior attempt is Unavailable;
an empty map never means there were no prior attempts.

acquire_prior_attempts is that path. The install calls it, the resolver
calls it and its inline loop is deleted. The per-leg bound is the walk's
own, for the same reason: past it the earlier keys are unread, never
assumed skipped. An attempt of zero has no earlier key and contributes
no entry, which is the one case where the map is legitimately empty.

Controls, each restored byte-identical:
  M1 the map handed over empty (the state before this change)
     -> an_attempt_above_zero_installs_once_the_keys_it_skips_are_final
        and an_attempt_whose_earlier_key_is_open_waits_and_installs_nothing
  M2 only the key immediately before is read
     -> an_attempt_above_zero_installs_once_the_keys_it_skips_are_final
  M3 a cell read as open is recorded Final
     -> an_attempt_whose_earlier_key_is_open_waits_and_installs_nothing

M2 and M3 were first run together and reddened both tests; re-run singly
for attribution, which is what the table above records.
@cryptskii
cryptskii merged commit 14cd61d into main Sep 21, 2026
23 checks passed
@cryptskii
cryptskii deleted the fix/sofi-prior-attempt-acquisition branch September 21, 2026 12:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant