88//! A better approach: only copy the structures that have changed and import the rest from the existing codebase.
99
1010use 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+ } ;
1215use near_sdk:: {
1316 AccountId , env,
1417 store:: { Lazy , LookupMap } ,
@@ -103,12 +106,59 @@ 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_capped_at`] 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+ // TODO(#3978): transitional one-time upgrade step — removed together with this
121+ // module when 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_capped_at ( expiry_timestamp_seconds) ;
145+ node_attestation. verified_attestation = VerifiedAttestation :: Mock ( stamped) ;
146+ }
147+ }
148+ }
149+
106150impl From < MpcContract > for crate :: MpcContract {
107151 fn from ( old : MpcContract ) -> Self {
108152 if !matches ! ( old. protocol_state, ProtocolContractState :: Running ( _) ) {
109153 env:: panic_str ( "Contract must be in running state when migrating." ) ;
110154 }
111155
156+ // Legacy `MockAttestation::Valid` entries never expire and can never be
157+ // cleaned up. Stamp an expiry on them so the standard cleanup flow can
158+ // evict stale mock entries after the upgrade.
159+ let mut tee_state = old. tee_state ;
160+ stamp_expiry_on_legacy_mocks ( & mut tee_state, TeeState :: current_time_seconds ( ) ) ;
161+
112162 crate :: MpcContract {
113163 protocol_state : old. protocol_state ,
114164 pending_signature_requests : old. pending_signature_requests ,
@@ -117,7 +167,7 @@ impl From<MpcContract> for crate::MpcContract {
117167 proposed_updates : old. proposed_updates ,
118168 node_foreign_chain_support : old. node_foreign_chain_support ,
119169 config : old. config . into ( ) ,
120- tee_state : old . tee_state ,
170+ tee_state,
121171 accept_requests : old. accept_requests ,
122172 node_migrations : old. node_migrations ,
123173 metrics : old. metrics ,
@@ -127,3 +177,59 @@ impl From<MpcContract> for crate::MpcContract {
127177 }
128178 }
129179}
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+ }
0 commit comments