Skip to content

feat(contract): auto-expire unused launcher image hashes#3564

Open
barakeinav1 wants to merge 4 commits into
mainfrom
feat/auto-remove-launcher-hashes
Open

feat(contract): auto-expire unused launcher image hashes#3564
barakeinav1 wants to merge 4 commits into
mainfrom
feat/auto-remove-launcher-hashes

Conversation

@barakeinav1

@barakeinav1 barakeinav1 commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Implements the approved design (docs/design/auto-remove-launcher-hashes-design.md, PR #3488).

Usage-based expiry for allowed_launcher_image_hashes, replacing unanimous-vote-only removal for routine cleanup:

  • AllowedLauncherImage gains added / last_attested; expired when max(added, last_attested) + TTL < now
  • last_attested is refreshed on a successful attestation in submit_participant_info, but only for a current participant (enforced by requiring an AuthenticatedParticipantId) — a prospective/non-participant node cannot keep a launcher alive. No node-side changes.
  • All reads of the allowed launcher set filter expired entries; newest-entry fallback so the set never goes empty
  • verify_tee spawns a detached self-call to a new #[private] clean_expired_launcher_hashes (gas via clean_expired_launcher_hashes_tera_gas); never removes the last entry
  • Re-voting an existing hash refreshes its added (recovery for never-adopted hashes)
  • New config launcher_hash_unused_ttl_seconds (default 14d), validated >= DEFAULT_EXPIRATION_DURATION_SECONDS
  • State migration initializes timestamps for existing entries

Tests: refresh-on-use, expiry filtering, newest fallback, cleanup_expired, re-vote refresh, a migration round-trip test, plus updated existing launcher/tee tests.

Reviewer note — migration assumption

The migration shadows the 3.12.0 layout in v3_12_0_state.rs, adding OldTeeState / OldAllowedLauncherImage and filling the new timestamps with Timestamp::now() on upgrade. Only allowed_launcher_images changed borsh layout; all other TeeState fields reuse the real (byte-identical) types. Please confirm the live deployed version is 3.12.0 so this shadow path is the correct one. (migrating_launcher_images_preserves_hashes_and_stamps_timestamps covers the round-trip, including that timestamps are stamped to migration time rather than defaulting to epoch 0.)

Closes #3381

pub(crate) launcher_hash: LauncherImageHash,
pub(crate) compose_hashes: Vec<LauncherDockerComposeHash>,
pub(crate) added: Timestamp,
pub(crate) last_attested: Timestamp,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Two keep-alive signals, expiry is max(added, last_attested):

  • added — voted-in/re-vote time; gives a freshly voted hash a full TTL grace before any node runs it.
  • last_attested — last participant attestation; keeps an in-use hash alive.

Alive if either is recent. added is also read on its own to pick the newest entry for the all-expired fallback, so the pair is not collapsible to one field.

@barakeinav1 barakeinav1 force-pushed the feat/auto-remove-launcher-hashes branch from ba05472 to b2d72fe Compare July 2, 2026 10:31
Launcher image hashes accumulated forever; removal required a unanimous
vote. This adds usage-based expiry:

- AllowedLauncherImage gains added/last_attested timestamps; an entry is
  expired when max(added, last_attested) + TTL < now
- last_attested is refreshed on a successful attestation, but ONLY for a
  current participant (enforced by requiring an AuthenticatedParticipantId);
  a prospective/non-participant node cannot keep a stale launcher alive
- reads filter out expired entries, with a newest-entry fallback so the
  allowed set never goes empty
- verify_tee spawns a detached self-call to a new #[private]
  clean_expired_launcher_hashes that sweeps expired entries from storage
- re-voting an existing launcher hash refreshes its added timestamp
- new config launcher_hash_unused_ttl_seconds (default 14d), validated
  >= DEFAULT_EXPIRATION_DURATION_SECONDS
- state migration (v3_12_0_state) initializes timestamps for existing entries

Updates the design doc status to Implemented and regenerates the borsh-schema
and ABI snapshots for the new fields/method.

Closes #3381
@barakeinav1 barakeinav1 force-pushed the feat/auto-remove-launcher-hashes branch from b2d72fe to 99f6904 Compare July 2, 2026 11:32
@barakeinav1 barakeinav1 marked this pull request as ready for review July 2, 2026 11:46
Copilot AI review requested due to automatic review settings July 2, 2026 11:46

Copilot AI 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.

Pull request overview

Implements the approved “auto-expire unused launcher image hashes” design by adding usage-based expiry for allowed launcher images in the contract, including config/ABI updates, state migration for the new borsh layout, and test coverage for refresh/expiry/cleanup behavior.

Changes:

  • Add added / last_attested timestamps to launcher allowlist entries and filter expired entries at read/verify time (with newest-entry fallback).
  • Refresh last_attested on successful participant submissions and add a detached private cleanup self-call to physically evict expired entries.
  • Introduce new config knobs (launcher_hash_unused_ttl_seconds, clean_expired_launcher_hashes_tera_gas) with interface + DTO mapping + snapshots + migration/tests updated.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
docs/design/auto-remove-launcher-hashes-design.md Marks the design as implemented and updates wording/invariants/decisions to match the shipped behavior.
crates/test-utils/src/contract_types.rs Extends dummy config builder with the new TTL + cleanup gas fields.
crates/near-mpc-contract-interface/src/types/config.rs Adds the new config fields to InitConfig/Config and updates serialization tests.
crates/near-mpc-contract-interface/src/method_names.rs Adds the clean_expired_launcher_hashes method name constant.
crates/mpc-attestation/src/attestation.rs Exposes the launcher compose hash from a verified attestation to support refresh-on-use.
crates/contract/tests/snapshots/abi__abi_has_not_changed.snap Updates ABI snapshot for the new private cleanup method and config fields.
crates/contract/tests/sandbox/upgrade_from_current_contract.rs Updates sandbox upgrade test config to include the new TTL + cleanup gas fields.
crates/contract/tests/sandbox/contract_configuration.rs Updates sandbox init config test to include the new TTL + cleanup gas fields.
crates/contract/src/v3_12_0_state.rs Adds 3.12.0 shadow types for launcher allowlist migration and a migration round-trip test.
crates/contract/src/tee/tee_state.rs Threads launcher TTL through verification paths and adds refresh-on-use + cleanup hooks.
crates/contract/src/tee/proposal.rs Implements TTL filtering, fallback selection, refresh-on-use, re-vote refresh, and cleanup for launcher allowlist entries (with tests).
crates/contract/src/snapshots/mpc_contract__tests__mpc_contract_borsh_schema_has_not_changed.snap Updates borsh schema snapshot for new fields.
crates/contract/src/lib.rs Wires TTL into submit/verify/read paths, spawns detached cleanup self-call, adds private cleanup endpoint, and validates config updates.
crates/contract/src/dto_mapping.rs Maps new config fields between DTOs and contract config.
crates/contract/src/config.rs Adds new config fields, defaults, and a validation invariant tying TTL to attestation expiry window.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 2407 to +2412
pub fn update_config(&mut self, config: dtos::Config) {
self.config = config.into();
let new_config: Config = config.into();
if let Err(e) = new_config.validate() {
env::panic_str(e);
}
self.config = new_config;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — fixed in d7c0c65. init and init_running now build the config, call config.validate(), and propagate the error (both are #[handle_result]), so the contract can no longer be initialized with a TTL below the attestation validity window. Added init_rejects_launcher_ttl_below_attestation_validity to lock it in.

Comment thread crates/test-utils/src/contract_types.rs Outdated
@claude

claude Bot commented Jul 2, 2026

Copy link
Copy Markdown

Pull request overview

Implements usage-based expiry for allowed_launcher_image_hashes: each AllowedLauncherImage now carries added / last_attested timestamps, all reads filter by max(added, last_attested) + TTL >= now, submit_participant_info refreshes last_attested when the caller is authenticated as a current participant, and verify_tee spawns a detached self-call to a new #[private] clean_expired_launcher_hashes for physical eviction. Includes a 3.12.0 shadow borsh layout in v3_12_0_state.rs to migrate existing entries (stamping both timestamps to now).

Changes:

  • New AllowedLauncherImage.{added, last_attested} + is_expired / last_active helpers; AllowedLauncherImages::{live_indices, refresh_last_attested, cleanup_expired, from_entries} and add() semantics changed (re-adding an existing hash now refreshes added and returns true).
  • TeeState::refresh_launcher_usage gated by AuthenticatedParticipantId; add_participant, reverify_participants, reverify_and_cleanup_participants, clean_invalid_attestations, get_allowed_launcher_* all take a new launcher_unused_ttl: Duration.
  • New Config.launcher_hash_unused_ttl_seconds (default 14d) + clean_expired_launcher_hashes_tera_gas (default 5 Tgas); Config::validate() enforces TTL ≥ DEFAULT_EXPIRATION_DURATION_SECONDS, called from update_config.
  • New #[private] clean_expired_launcher_hashes; detached self-call from verify_tee.
  • VerifiedAttestation::launcher_compose_hash() accessor.
  • v3_12_0_state.rs gains OldTeeState / OldAllowedLauncherImage(s) shadow; migration stamps added = last_attested = now.
  • Snapshot updates (borsh schema, ABI) and design-doc status flip to Implemented; docstrings say "participant attestation".

Reviewed changes

Per-file summary
File Description
crates/contract/src/config.rs Adds two config fields (TTL, gas) with defaults and a Config::validate() invariant check
crates/contract/src/dto_mapping.rs Wires the two new config fields through init/get/update DTOs
crates/contract/src/lib.rs Threads launcher_unused_ttl into every TEE call site; refreshes on use gated by AuthenticatedParticipantId; detached sweep from verify_tee; new #[private] clean_expired_launcher_hashes; validation now called by update_config
crates/contract/src/tee/proposal.rs AllowedLauncherImage gains timestamps + is_expired; AllowedLauncherImages gains live_indices / refresh_last_attested / cleanup_expired / from_entries; add() now refreshes an existing entry and returns true
crates/contract/src/tee/tee_state.rs Adds refresh_launcher_usage, clean_expired_launcher_images; propagates TTL through all reads and reverifications; adds a refresh test
crates/contract/src/v3_12_0_state.rs Adds OldTeeState / OldAllowedLauncherImage(s) borsh shadow + From impl stamping timestamps to now; adds a round-trip migration test
crates/contract/src/snapshots/...borsh_schema.snap, crates/contract/tests/snapshots/abi.snap Regenerated snapshots for the schema/ABI additions
crates/contract/tests/sandbox/{contract_configuration,upgrade_from_current_contract}.rs Updates sandbox configs with the two new fields
crates/mpc-attestation/src/attestation.rs New VerifiedAttestation::launcher_compose_hash()
crates/near-mpc-contract-interface/src/method_names.rs Adds CLEAN_EXPIRED_LAUNCHER_HASHES
crates/near-mpc-contract-interface/src/types/config.rs Adds the two new config fields to Config/InitConfig DTOs
crates/test-utils/src/contract_types.rs Adds new fields to dummy_config
docs/design/auto-remove-launcher-hashes-design.md Status → Implemented; notes participant-gating; open questions → decisions

Findings

Blocking (must fix before merge):

  • crates/contract/src/lib.rs:2014,2081init and init_running build the config with init_config.map(Into::into).unwrap_or_default() but never call Config::validate(). Only update_config (line 2409) validates. An operator can therefore initialize the contract with launcher_hash_unused_ttl_seconds < DEFAULT_EXPIRATION_DURATION_SECONDS, breaking the very safety invariant the validator was added to enforce ("a hash backing a valid participant attestation is never expired"). Please call config.validate() on both init paths and propagate the error (both are already #[handle_result]).

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

  • crates/mpc-attestation/src/attestation.rs:28 vs crates/contract/src/config.rs:113, crates/test-utils/src/contract_types.rs:18, crates/contract/tests/sandbox/contract_configuration.rs:106, crates/contract/tests/sandbox/upgrade_from_current_contract.rs:122, docs/design/auto-remove-launcher-hashes-design.md:34,76 — the actual constant DEFAULT_EXPIRATION_DURATION_SECONDS = 60*60*24 is 1 day, but new comments and the design doc call it "7 days". The test values (14 days) still satisfy the real bound so nothing breaks, but the comments are misleading — either drop the parenthetical or match the current constant. The safety-invariant argument in the design doc also relies on "at most 7d old" for participant attestations, which needs a matching update if the 1-day constant is authoritative.

  • crates/contract/src/tee/proposal.rs:269-274is_expired returns false on checked_add overflow (deadline unrepresentable ⇒ "never expires"). Intentional and safe here, but worth a one-line WHY comment given the arithmetic policy in engineering-standards — otherwise a future reader may "fix" it to a panic.

  • crates/contract/src/tee/proposal.rs:302 — the log! on re-vote ("launcher hash already in allowed list, refreshing") does not mention that the caller's add now returns true for this path. vote_add_launcher_hash logs \"launcher hash add result: {}\" (lib.rs:1486) which will read true even though no new entry was created. Consider tightening the log message (or returning an enum) so operator logs distinguish "newly added" from "refreshed".

  • crates/contract/src/tee/proposal.rs:296-305 — refreshing an existing entry updates added but leaves compose_hashes untouched. This is consistent with add_mpc_image_compose_hashes being the sole path for adding new compose hashes, but a one-line comment on the refresh branch would help — otherwise it looks like a re-vote should also "refresh" the compose set against the current MPC image list.

  • crates/contract/src/tee/tee_state.rs:337-351refresh_launcher_usage takes _authenticated_participant: &AuthenticatedParticipantId purely as a type-level capability token. That's fine and matches other patterns in this codebase, but the current docstring says "Requires an [AuthenticatedParticipantId] so a non-participant submission cannot keep a launcher hash alive." — worth explicitly calling out that the argument is intentionally unused so a future reader doesn't "clean up" the parameter.

  • crates/contract/src/lib.rs:795-839 — the authenticated_participant is computed before add_participant runs but consumed after. add_participant doesn't touch the participant set so this is safe today, but a comment noting the intentional ordering (or hoisting the refresh to a helper) would make the invariant less fragile.

  • crates/contract/src/tee/proposal.rs:362-381cleanup_expired recomputes is_expired per entry twice on the fast path (once in any, once in retain). Micro; entries list is tiny. Leaving as-is is fine.

⚠️ Issues found

Address review feedback:
- init/init_running now validate the config (not just update_config), so the
  contract cannot be initialized with launcher_hash_unused_ttl_seconds below
  the attestation validity window; add a regression test.
- dummy_config: give clean_expired_launcher_hashes_tera_gas a unique offset
  (was duplicating remove_non_participant_tee_verifier_votes_tera_gas).
Address non-blocking review comments:
- is_expired: note why overflow returns not-expired (never panic on a bogus timestamp)
- add(): note re-vote refreshes only the clock; compose hashes managed separately
- refresh_launcher_usage: note the AuthenticatedParticipantId is a capability token
@barakeinav1

barakeinav1 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review. Responses inline:

init and init_running build the config with init_config.map(Into::into).unwrap_or_default() but never call Config::validate(). Only update_config validates.

Fixed in d7c0c65 — both init paths now call config.validate() and propagate the error (both are #[handle_result]), with a regression test init_rejects_launcher_ttl_below_attestation_validity.

is_expired returns false on checked_add overflow ... worth a one-line WHY comment

Done (a8694aa): added a comment explaining the deadline is unrepresentably far off and we must never panic on a bogus timestamp.

refreshing an existing entry updates added but leaves compose_hashes untouched ... a one-line comment ... would help

Done (a8694aa): the re-vote branch now notes compose hashes are maintained separately via add_mpc_image_compose_hashes.

refresh_launcher_usage takes _authenticated_participant: &AuthenticatedParticipantId purely as a type-level capability token ... worth explicitly calling out that the argument is intentionally unused

Done (a8694aa): docstring now states it is a capability token with the value intentionally unused.

the log! on re-vote ... does not mention that the callers add now returns true ... Consider tightening the log message (or returning an enum)

Left as-is: the proposal-level log! already distinguishes "refreshing" from a new add; an enum return felt like over-engineering for a log line.

authenticated_participant is computed before add_participant runs but consumed after ... a comment noting the intentional ordering

cleanup_expired recomputes is_expired per entry twice on the fast path ... Micro; entries list is tiny. Leaving as-is is fine.

Left as-is — micro / the list is tiny, as you noted.

…and design doc

DEFAULT_EXPIRATION_DURATION_SECONDS was changed 7d -> 1d in #3626. Update the
stale '7 days' wording; reference the constant instead of hardcoding a
day-count in test comments to avoid future drift. The MPC docker-image grace
period (DEFAULT_TEE_UPGRADE_DEADLINE_DURATION_SECONDS) remains 7 days.
@barakeinav1

barakeinav1 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

the actual constant DEFAULT_EXPIRATION_DURATION_SECONDS = 60*60*24 is 1 day, but new comments and the design doc call it "7 days" ... either drop the parenthetical or match the current constant. The safety-invariant argument in the design doc also relies on "at most 7d old" ... which needs a matching update

Good catch — fixed in cb955fb. It was deliberately changed 7d → 1d in #3626, so the wording was stale. Updated the comments/design doc to "1 day", and to avoid this drift recurring I dropped the hardcoded day-count in the test comments (they now just reference the constant). The design-doc safety argument is now phrased around the constant (TTL >= DEFAULT_EXPIRATION_DURATION_SECONDS holds regardless of its exact value). The unrelated DEFAULT_TEE_UPGRADE_DEADLINE_DURATION_SECONDS (MPC docker-image grace) stays 7 days, which is still correct.

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.

Auto-remove unused launcher image hashes from the contract

2 participants