Skip to content

orchestrator: add IncrementalVerifier capability trait - #459

Open
chrysh wants to merge 1 commit into
OpenPRoT:mainfrom
9elements:async-verify
Open

chrysh wants to merge 1 commit into
OpenPRoT:mainfrom
9elements:async-verify

Conversation

@chrysh

@chrysh chrysh commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

TL;DR: Polled verification trait for the update pump, consuming-session
design so post-verdict poll is a compile error.

Part of 9elements#8.

Summary

Trait-only PR. Adds IncrementalVerifier and VerifySession to
orchestrator-capabilities: polled, chunked image verification so the
update pump never blocks on hashing megabytes of payload.

start() consumes the verifier into a VerifySession. Each poll()
consumes the session: Processing returns it inside the variant for the
next call, terminal outcomes return the verifier for reuse. A retained
verdict would be unsafe because poll() takes a payload reference per
call, so a swapped payload could get a stale result. The consuming design
makes that unrepresentable.

This diverges from Updatable (implicit-start, poll-on-self). Migrating
Updatable to the same session shape is a follow-up once its consumers
exist (the pump needs to land first so we can migrate callers).

Driver composition (the adapter that wraps a crypto backend behind this
trait, wired into the update pump) follows in a stacked PR.
Effect::AuthenticateUpdate remains stubbed in the SM until then.

Assisted-by: Claude

@chrysh
chrysh force-pushed the async-verify branch 4 times, most recently from fa9a4fe to a9a4e69 Compare September 8, 2026 18:28
@chrysh
chrysh marked this pull request as ready for review September 8, 2026 18:28
@chrysh
chrysh requested a review from leongross September 8, 2026 20:34

@leongross leongross left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note from a personal discussion with @chrysh: This only comes into effect if the orchestrator cannot block. Then, we need to incrementally verify one chunk at a time in the main loop.
We will further discuss this architectural decision and conclude whether we merge it as is.

Comment on lines +61 to +62
/// sleeps. An empty payload is a fault, never a vacuous
/// `Authenticated`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// sleeps. An empty payload is a fault, never a vacuous
/// `Authenticated`.
/// sleeps. An empty payload is a fault.

/// the verifier, ready for a new [`start`](IncrementalVerifier::start).
#[derive(Debug)]
pub enum PollOutcome<S: VerifySession> {
/// One chunk processed. `done` bytes so far out of `total`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a fan of done here; it implies duality, done or not done. I think we had this somewhere before, did we replace it with count?

Comment on lines +9 to +10
/// begin hashing a candidate image; the returned [`VerifySession`] does
/// the actual work one bounded step at a time.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am confused about the wording. Here you say we hash the images, and later you say we verify the signature? Should this component do both, or is there a mix-up?

Comment on lines +77 to +79
/// The complete image authenticated (signature and policy checks
/// passed).
Authenticated(S::Verifier),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the module description, you say, that this tracks the hashing, which makes sense with the gradual byte supply. How does this couple with signature verification?

Comment on lines +199 to +207
let payload = SlicePayload(&[0xAA; 10]);
let session = ChunkedVerifier.start();

// 4 bytes, 4 bytes, 2 bytes = 3 Processing steps, then verdict.
let PollOutcome::Processing {
session,
done: 4,
total: 10,
} = session.poll(&payload)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

potential future offset drift with hardcoded constant sizes

The update path hashes megabytes of payload; a synchronous verify would
stall the single-threaded runtime. IncrementalVerifier and VerifySession
split that work into bounded, non-blocking steps.

start() consumes the verifier into a VerifySession. Each poll() consumes
the session: Processing returns it inside the variant for the next call,
while Authenticated, Rejected, and Fault return the verifier for reuse.
Polling after a verdict is unrepresentable because the session is gone.
abandon() discards mid-session state and returns the verifier.

Processing { done, total } reports byte-level progress. Faults
(unreadable payload, crypto error) are the Fault variant, not a verdict:
Fault means the check did not run, Rejected means it ran and the image
failed.

Assisted-by: Claude
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.

2 participants