Skip to content

fix(node): retry attestation submission only until the next resubmission tick - #4281

Merged
pbeza merged 13 commits into
mainfrom
4280-simplify-attestation-submission-retry
Sep 2, 2026
Merged

fix(node): retry attestation submission only until the next resubmission tick#4281
pbeza merged 13 commits into
mainfrom
4280-simplify-attestation-submission-retry

Conversation

@pbeza

@pbeza pbeza commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Closes #4280
Closes #3747

@pbeza

pbeza commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

On hold until #3746 is addressed, as requested in #4280 (comment). Removing monitor_attestation_removal there first should make this change simpler; I'll rebase once it lands.

@netrome

netrome commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

#3746 is merged, so this should be unblocked. There are conflicts to resolve though

pbeza added 6 commits August 31, 2026 15:41
…tion-submission-retry

Resolve the conflict in the attestation submission module in favour of main:
#4282 removed monitor_attestation_removal and moved the tick abstraction into
its own module, superseding this branch's changes there.
…ion tick

A submission could be retried for up to 12 hours, so a late success stored a
quote that old even though the periodic task had generated fresher ones in the
meantime. Cap the retry window at the resubmission interval instead: once it
elapses, the next tick supersedes the attempt with a freshly generated
attestation.

generate_and_submit no longer reports whether it reached the contract; its only
consumer was monitor_attestation_removal, removed in #4282.

Closes #4280
Closes #3747
…l after it starts

The retry window was one interval measured from the moment the submission
started, so attestation generation time was added on top of it. A slow PCCS
collateral fetch pushed the retry past the tick it belonged to, and a late
success stored a correspondingly older quote.

The ticker now reports when the next round falls due, derived from the
interval's own period, and that instant bounds the submission directly.
Generation stays outside it, so a generation failure still waits for the next
tick.
The acceptance criterion "a generation failure just waits for the next hourly
tick" was unverified: attestation generation could not be faulted in a test,
because TeeAuthority::Local always succeeds and only the Dstack variant does
fallible I/O.

Inject generation through a GenerateAttestation trait, mirroring the
ReadAttestationExpiry seam next to it but generic rather than boxed, so a test
double can fail every attempt and count them. The new test drives the loop with
a single scheduled round and shows it runs exactly one, submits nothing, and
starts the next only once the ticker yields another.
@pbeza
pbeza marked this pull request as ready for review August 31, 2026 16:57
@claude

claude Bot commented Aug 31, 2026

Copy link
Copy Markdown

Pull request overview

Replaces the fixed 12-hour attestation-submission retry window with a deadline derived from the resubmission cadence, so a failing round stops retrying exactly when the next round falls due instead of overrunning eleven ticks. Tick::tick now returns the next round's due instant, which periodic_attestation_submission threads down to submit_remote_attestation as a timeout_at deadline. A new GenerateAttestation trait decouples the submitter from TeeAuthority so generation failures can be unit-tested without a real authority.

Changes:

  • MAX_RETRY_DURATION (12 h) removed; submit_remote_attestation takes an explicit deadline: Instant and uses timeout_at instead of timeout.
  • Tick::tick returns Instant (the next round's due time); MockTicker gains with_period.
  • New pub(crate) trait GenerateAttestation, implemented for TeeAuthority; AttestationSubmitter becomes generic over it.
  • generate_and_submit drops its unused bool return.
  • submit_remote_attestation demoted from pub to private (no external callers).
  • Two new unit tests plus a Tick-for-Interval test; design doc updated for the removed monitor_attestation_removal and the new window.

Reviewed changes

Per-file summary
File Description
crates/node/src/tee/remote_attestation.rs Deadline plumbing, GenerateAttestation abstraction, generic AttestationSubmitter, new tests
crates/node/src/tick.rs Tick::tick returns the next-round deadline; MockTicker::with_period; test for the Interval impl
docs/design/attestation-verifier-contract.md Drops the removed monitor_attestation_removal references; restates the retry cap as the resubmission interval

Findings

Blocking (must fix before merge):

  • crates/node/src/tick.rs:11-19 — the doc comment asserts an invariant the implementation does not hold. tokio::time::Interval::tick() resolves to the instant the tick was scheduled for, not Instant::now(), so scheduled + period is the next tick only when no tick was missed. run_periodic_attestation_submission sets MissedTickBehavior::Skip (remote_attestation.rs:244), under which a round that overruns by more than one full period gets a deadline that is already in the past: timeout_at at remote_attestation.rs:126 then expires on the first poll, the round gets zero retries, and the log still reads "failed to submit attestation after multiple retry attempts".

    This is reachable, not theoretical: nothing bounds the generation half of the round. generate_dstack_attestation calls get_with_backoff(..., None) twice (crates/tee-authority/src/tee_authority.rs:424 and :433) — unlimited retries, 60 s max delay, no overall timeout — so a wedged dstack socket can push generation past two periods while the deadline keeps counting from the tick.

    Two ways out, either is fine:

    1. Bound the whole round instead of just the submission, which also removes the deadline plumbing through two layers:
      let deadline = interval_ticker.tick().await;
      let _ = submitter.generate_and_submit().timeout_at(deadline).await;
      (generate_and_submit and submit_remote_attestation go back to taking no deadline.)
    2. Keep the current shape but qualify the comment — say the returned instant is the next round's due time assuming the round does not overrun the period, and note the Skip interaction.

    Worth a test either way: the new tick__should_report_the_next_round_as_the_deadline only exercises the non-missed path, and MockTicker cannot reproduce a missed tick at all, so the one interaction the PR actually changes in production is uncovered.

Non-blocking (nits, follow-ups, suggestions):

  • crates/node/src/tick.rs:36MockTicker::new defaults period to Duration::ZERO, i.e. a deadline equal to "now". Any future test that reaches submit_remote_attestation through a default-constructed MockTicker silently gets a zero-length retry window and will look like "retries are broken". periodic_attestation_submission__should_wait_for_the_next_tick_when_generation_fails (remote_attestation.rs:471) only escapes this because generation fails first. Making the period a required argument of new — or defaulting it to something plainly non-expiring — removes the footgun.
  • crates/node/src/foreign_chain_probe.rs:33 — the only other Tick caller drops the new return value. Not a bug, but the trait now carries a scheduling concern one of its two callers has no use for; a separate next_deadline() (or leaving tick() unit-returning and exposing the period) would keep the capability single-purpose per the trait guidance in docs/engineering-standards.md.
  • crates/node/src/tee/remote_attestation.rs:70 — the closing line of the doc comment still says "within the retry window" while the summary above it was updated to "the given deadline"; worth aligning the two.
  • docs/design/attestation-verifier-contract.md:106 — "exponential backoff (100 ms → 60 s, capped at that same interval)" mixes the per-attempt delay range with the total retry window in one parenthetical. Suggest splitting: "…exponential backoff (per-attempt delay 100 ms → 60 s), retrying only until the next hourly tick."

Nothing to flag on secrets, panics in production paths, or the GenerateAttestation abstraction — the pub(crate) trait behind a pub signature is fine here because mod tee is private, and the async fn-implements--> impl Future + Send pattern is correct. The removed monitor_attestation_removal references check out: no code or doc references remain repo-wide.

⚠️ Issues found

@haiyuechen-nearone haiyuechen-nearone left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@pbeza please take a look at the blocking finding from Claude. I will be happy to approve if we added a hard stop at the outer layer to capture the whole process.

I suggest that we add a timeout option for the dstack rpc call as well, but that is not blocking as it is adjacent to the issue fixed.

Comment thread crates/node/src/tee/remote_attestation.rs Outdated
Comment thread crates/node/src/tee/remote_attestation.rs
Comment thread crates/node/src/tee/remote_attestation.rs Outdated

@gilcu3 gilcu3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Added some comments, and requested a change in line with @haiyuechen-nearone's review, as apparently the generation path can take forever, which would affect what this PR is trying to achieve.

Comment thread docs/design/attestation-verifier-contract.md
Comment thread crates/node/src/tick.rs Outdated
Comment thread crates/node/src/tee/remote_attestation.rs Outdated
Comment thread crates/node/src/tick.rs Outdated
Design docs record what was decided at the time and are not kept in sync with
the code once the design has shipped, so this branch's edits to the retry
window and the attestation-removal references are reverted.
@claude

claude Bot commented Sep 1, 2026

Copy link
Copy Markdown

PR title type suggestion: The phrase "only until the next resubmission tick" suggests a behavioral change to retry logic rather than a pure restructuring. Consider using fix: (if this addresses a bug) or perf: (if this optimizes retry behavior) instead of refactor:.

Only the submission was bounded, so a round could run past its deadline in
generation and, because tokio reports the instant a tick was scheduled for
rather than the current time, a round overrunning by more than one period was
handed a deadline already in the past. The submission then gave up without a
single retry while still logging that it had exhausted its attempts.

Derive the deadline from the tick's own start instead, and apply it to
generation and to the pre-submit expiry read as well, so every await in a round
shares one absolute deadline. Generation timeouts get their own metric label,
since they need a different response than a generation error.

The submission keeps bounding itself rather than being wrapped from outside: a
timeout there would drop the future, stranding a signed transaction that the
sender has already handed to a detached task, and skipping the counter that
records the failure.
@pbeza pbeza changed the title refactor(node): retry attestation submission only until the next resubmission tick fix(node): retry attestation submission only until the next resubmission tick Sep 1, 2026
gilcu3
gilcu3 previously approved these changes Sep 1, 2026

@gilcu3 gilcu3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you! LGTM, but I am not sure about the new tick pattern. Left a question for @netrome in a comment

Comment thread crates/node/src/tee/remote_attestation.rs Outdated
Comment thread crates/node/src/tee/remote_attestation.rs Outdated
Comment thread crates/node/src/tick.rs
…threading a deadline

The loop now cancels a round with one timeout instead of passing a
deadline into generate_and_submit, and the Tick trait returns to being
a pure tick. The round publishes the stage it is in through a watch
channel, so a timed-out round is logged as a warning and counted in
mpc_tee_attestation_round_timeouts_total under the stage that was
running when the round was cut off.
Comment thread crates/node/src/tee/remote_attestation.rs Outdated
Comment thread crates/node/src/tee/remote_attestation.rs Outdated
Comment thread crates/node/src/tee/remote_attestation.rs Outdated
…in the loop

generate_and_submit is split into the three stage methods and the loop
drives them directly, each bounded by timeout_at against one loop-local
deadline. The stage enum, the watch channel, and the round timeout
parameter are gone; each timeout site names its own stage label.
@pbeza
pbeza requested a review from gilcu3 September 2, 2026 08:29
@pbeza

pbeza commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

@gilcu3 I followed your suggestion (thx for it!). It feels more idiomatic now. Let me know if this is how you pictured it, and feel free to re-review when you get a chance.

Also, @haiyuechen-nearone PTAL when you get a chance. Thanks, both!

@haiyuechen-nearone haiyuechen-nearone left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚢

@gilcu3 gilcu3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice!

@netrome netrome left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nice, looks good!

@pbeza
pbeza added this pull request to the merge queue Sep 2, 2026
Merged via the queue into main with commit 1d39f5f Sep 2, 2026
15 checks passed
@pbeza
pbeza deleted the 4280-simplify-attestation-submission-retry branch September 2, 2026 11:46
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.

Simplify attestation submission retry logic into a single hourly loop Reduce attestation submission timeout

4 participants