Skip to content

Commit d994389

Browse files
committed
refactor(mock-attestation): address review
- Move stamp_expiry_on_legacy_mocks (+ its test) into the v3_13_0_state migration module, since it is migration-only (netrome). - Simplify submitted_attestation_landed docstring: a changed stored expiry is enough to conclude the submit landed; drop the confusing 'identity' wording (the legacy equality fallback is covered by TODO(#3786)).
1 parent 1330b2f commit d994389

3 files changed

Lines changed: 106 additions & 91 deletions

File tree

crates/contract/src/tee/tee_state.rs

Lines changed: 0 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -473,46 +473,6 @@ impl TeeState {
473473
removed
474474
}
475475

476-
/// One-time migration helper: stamps an expiry on every stored mock attestation
477-
/// that lacks or exceeds one — both user-submitted mocks and the genesis
478-
/// sentinels written by [`TeeState::with_mocked_participant_attestations`].
479-
/// Legacy [`MockAttestation::Valid`] entries pass re-verification forever and
480-
/// can therefore never be evicted by [`TeeState::clean_invalid_attestations`]. [`MockAttestation::with_expiry`]
481-
/// rewrites them as expiring [`MockAttestation::WithConstraints`] mocks so the
482-
/// normal cleanup flow can remove stale entries once the window elapses. An
483-
/// entry whose expiry is longer than (or missing) the default window is capped
484-
/// at it; a shorter existing expiry is left as-is.
485-
///
486-
// TODO(#3978): transitional — remove this and its migration call site once the
487-
// pre-expiry state migration is retired.
488-
pub(crate) fn stamp_expiry_on_legacy_mocks(&mut self, current_timestamp_seconds: u64) {
489-
let expiry_timestamp_seconds =
490-
current_timestamp_seconds + attestation::DEFAULT_EXPIRATION_DURATION_SECONDS;
491-
492-
// Collect keys before mutating to avoid iterator invalidation.
493-
let mock_tls_keys: Vec<Ed25519PublicKey> = self
494-
.stored_attestations
495-
.iter()
496-
.filter(|(_, node_attestation)| {
497-
matches!(
498-
node_attestation.verified_attestation,
499-
VerifiedAttestation::Mock(_)
500-
)
501-
})
502-
.map(|(tls_pk, _)| tls_pk.clone())
503-
.collect();
504-
505-
for tls_pk in mock_tls_keys {
506-
let Some(node_attestation) = self.stored_attestations.get_mut(&tls_pk) else {
507-
continue;
508-
};
509-
if let VerifiedAttestation::Mock(mock) = &node_attestation.verified_attestation {
510-
let stamped = mock.clone().with_expiry(expiry_timestamp_seconds);
511-
node_attestation.verified_attestation = VerifiedAttestation::Mock(stamped);
512-
}
513-
}
514-
}
515-
516476
/// Returns the list of accounts that currently have TEE attestations stored.
517477
/// Note: This may include accounts that are no longer active protocol participants.
518478
pub fn get_tee_accounts(&self) -> Vec<NodeId> {
@@ -832,49 +792,6 @@ mod tests {
832792
);
833793
}
834794

835-
#[test]
836-
fn stamp_expiry_on_legacy_mocks__should_make_valid_mock_cleanable() {
837-
// Given: a legacy `MockAttestation::Valid` entry stored with no expiry, as
838-
// written by older contract versions. Such entries pass re-verification
839-
// forever and cannot be cleaned up.
840-
testing_env!(VMContextBuilder::new().block_timestamp(0).build());
841-
842-
let mut tee_state = TeeState::default();
843-
let node_id = NodeId {
844-
account_id: "legacy.near".parse().unwrap(),
845-
tls_public_key: bogus_ed25519_public_key(),
846-
account_public_key: bogus_ed25519_public_key(),
847-
};
848-
tee_state.stored_attestations.insert(
849-
node_id.tls_public_key.clone(),
850-
NodeAttestation {
851-
node_id: node_id.clone(),
852-
verified_attestation: VerifiedAttestation::Mock(MockAttestation::Valid),
853-
},
854-
);
855-
856-
// Sanity: past the default window but without migration, the un-stamped
857-
// entry survives cleanup indefinitely.
858-
set_block_timestamp((attestation::DEFAULT_EXPIRATION_DURATION_SECONDS + 1) * 1_000_000_000);
859-
assert_eq!(
860-
tee_state.clean_invalid_attestations(Duration::from_secs(0), 100),
861-
0
862-
);
863-
864-
// When: the migration stamps an expiry as of block time 0 (window ends at
865-
// DEFAULT), which the clock (already at DEFAULT + 1) is past.
866-
tee_state.stamp_expiry_on_legacy_mocks(0);
867-
let removed = tee_state.clean_invalid_attestations(Duration::from_secs(0), 100);
868-
869-
// Then: the stale legacy mock entry is removed.
870-
assert_eq!(removed, 1);
871-
assert!(
872-
!tee_state
873-
.stored_attestations
874-
.contains_key(&node_id.tls_public_key)
875-
);
876-
}
877-
878795
#[test]
879796
fn clean_invalid_attestations__should_honor_max_scan() {
880797
// Given: ten expired attestations stored.

crates/contract/src/v3_13_0_state.rs

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
//! A better approach: only copy the structures that have changed and import the rest from the existing codebase.
99
1010
use borsh::{BorshDeserialize, BorshSerialize};
11-
use near_mpc_contract_interface::types::{Metrics, VerifyForeignTransactionRequest};
11+
use mpc_attestation::attestation::{self, VerifiedAttestation};
12+
use near_mpc_contract_interface::types::{
13+
Ed25519PublicKey, Metrics, VerifyForeignTransactionRequest,
14+
};
1215
use near_sdk::{
1316
AccountId, env,
1417
store::{Lazy, LookupMap},
@@ -103,6 +106,47 @@ pub struct MpcContract {
103106
tee_verifier_votes: TeeVerifierVotes,
104107
}
105108

109+
/// Stamps an expiry on every stored mock attestation that lacks or exceeds one —
110+
/// both user-submitted mocks and the genesis sentinels written by
111+
/// [`TeeState::with_mocked_participant_attestations`]. Legacy
112+
/// [`mpc_attestation::attestation::MockAttestation::Valid`] entries pass
113+
/// re-verification forever and can therefore never be evicted by
114+
/// [`TeeState::clean_invalid_attestations`];
115+
/// [`mpc_attestation::attestation::MockAttestation::with_expiry`] rewrites them as
116+
/// expiring mocks so the normal cleanup flow can remove stale entries once the
117+
/// window elapses. An entry whose expiry is longer than (or missing) the default
118+
/// window is capped at it; a shorter existing expiry is left as-is.
119+
///
120+
/// This is a one-time upgrade step; it is removed together with this module when
121+
/// the pre-expiry migration is retired.
122+
fn stamp_expiry_on_legacy_mocks(tee_state: &mut TeeState, current_timestamp_seconds: u64) {
123+
let expiry_timestamp_seconds =
124+
current_timestamp_seconds + attestation::DEFAULT_EXPIRATION_DURATION_SECONDS;
125+
126+
// Collect keys before mutating to avoid iterator invalidation.
127+
let mock_tls_keys: Vec<Ed25519PublicKey> = tee_state
128+
.stored_attestations
129+
.iter()
130+
.filter(|(_, node_attestation)| {
131+
matches!(
132+
node_attestation.verified_attestation,
133+
VerifiedAttestation::Mock(_)
134+
)
135+
})
136+
.map(|(tls_pk, _)| tls_pk.clone())
137+
.collect();
138+
139+
for tls_pk in mock_tls_keys {
140+
let Some(node_attestation) = tee_state.stored_attestations.get_mut(&tls_pk) else {
141+
continue;
142+
};
143+
if let VerifiedAttestation::Mock(mock) = &node_attestation.verified_attestation {
144+
let stamped = mock.clone().with_expiry(expiry_timestamp_seconds);
145+
node_attestation.verified_attestation = VerifiedAttestation::Mock(stamped);
146+
}
147+
}
148+
}
149+
106150
impl From<MpcContract> for crate::MpcContract {
107151
fn from(old: MpcContract) -> Self {
108152
if !matches!(old.protocol_state, ProtocolContractState::Running(_)) {
@@ -113,7 +157,7 @@ impl From<MpcContract> for crate::MpcContract {
113157
// cleaned up. Stamp an expiry on them so the standard cleanup flow can
114158
// evict stale mock entries after the upgrade.
115159
let mut tee_state = old.tee_state;
116-
tee_state.stamp_expiry_on_legacy_mocks(TeeState::current_time_seconds());
160+
stamp_expiry_on_legacy_mocks(&mut tee_state, TeeState::current_time_seconds());
117161

118162
crate::MpcContract {
119163
protocol_state: old.protocol_state,
@@ -133,3 +177,59 @@ impl From<MpcContract> for crate::MpcContract {
133177
}
134178
}
135179
}
180+
181+
#[cfg(test)]
182+
#[expect(non_snake_case)]
183+
mod tests {
184+
use super::{TeeState, VerifiedAttestation, attestation, stamp_expiry_on_legacy_mocks};
185+
use crate::primitives::test_utils::bogus_ed25519_public_key;
186+
use crate::tee::tee_state::{NodeAttestation, NodeId};
187+
use crate::tee::test_utils::set_block_timestamp;
188+
use mpc_attestation::attestation::MockAttestation;
189+
use near_sdk::test_utils::VMContextBuilder;
190+
use near_sdk::testing_env;
191+
use std::time::Duration;
192+
193+
#[test]
194+
fn stamp_expiry_on_legacy_mocks__should_make_valid_mock_cleanable() {
195+
// Given: a legacy `MockAttestation::Valid` entry stored with no expiry, as
196+
// written by older contract versions. Such entries pass re-verification
197+
// forever and cannot be cleaned up.
198+
testing_env!(VMContextBuilder::new().block_timestamp(0).build());
199+
200+
let mut tee_state = TeeState::default();
201+
let node_id = NodeId {
202+
account_id: "legacy.near".parse().unwrap(),
203+
tls_public_key: bogus_ed25519_public_key(),
204+
account_public_key: bogus_ed25519_public_key(),
205+
};
206+
tee_state.stored_attestations.insert(
207+
node_id.tls_public_key.clone(),
208+
NodeAttestation {
209+
node_id: node_id.clone(),
210+
verified_attestation: VerifiedAttestation::Mock(MockAttestation::Valid),
211+
},
212+
);
213+
214+
// Sanity: past the default window but without migration, the un-stamped
215+
// entry survives cleanup indefinitely.
216+
set_block_timestamp((attestation::DEFAULT_EXPIRATION_DURATION_SECONDS + 1) * 1_000_000_000);
217+
assert_eq!(
218+
tee_state.clean_invalid_attestations(Duration::from_secs(0), 100),
219+
0
220+
);
221+
222+
// When: the migration stamps an expiry as of block time 0 (window ends at
223+
// DEFAULT), which the clock (already at DEFAULT + 1) is past.
224+
stamp_expiry_on_legacy_mocks(&mut tee_state, 0);
225+
let removed = tee_state.clean_invalid_attestations(Duration::from_secs(0), 100);
226+
227+
// Then: the stale legacy mock entry is removed.
228+
assert_eq!(removed, 1);
229+
assert!(
230+
!tee_state
231+
.stored_attestations
232+
.contains_key(&node_id.tls_public_key)
233+
);
234+
}
235+
}

crates/node/src/indexer/tx_sender.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -192,13 +192,11 @@ fn attestation_expiry_changed(pre_submit_expiry: Option<u64>, stored_expiry: u64
192192

193193
/// Whether the attestation we submitted is now the one stored on chain.
194194
///
195-
/// An entry that carries an expiry keeps no stored per-submission identity, so it is confirmed via
196-
/// [`attestation_expiry_changed`]: an accepted submit re-stamps the entry's expiry (to the submit
197-
/// block time plus
195+
/// An accepted submit re-stamps the entry's expiry (to the submit block time plus
198196
/// [`DEFAULT_EXPIRATION_DURATION_SECONDS`](mpc_attestation::attestation::DEFAULT_EXPIRATION_DURATION_SECONDS)),
199-
/// and only the owning account may rewrite it, so a changed expiry means our submit landed. This
200-
/// covers every Dstack entry, and mock entries stored by contracts that stamp expiries.
201-
/// A mock stored by an older contract carries no expiry and is matched by identity instead.
197+
/// and only the owning account may rewrite it, so observing a **changed stored expiry** is enough
198+
/// to conclude our submit landed — for every Dstack entry and for mocks stored with an expiry. A
199+
/// legacy mock with no stored expiry falls back to an equality check (see the `TODO(#3786)` below).
202200
// TODO(#1639): match a certificate-derived identity instead of this expiry heuristic.
203201
fn submitted_attestation_landed(
204202
pre_submit_expiry: Option<u64>,

0 commit comments

Comments
 (0)