@@ -19,7 +19,7 @@ use near_sdk::{
1919 test_utils:: VMContextBuilder , testing_env, AccountId , CurveType , NearToken , PublicKey ,
2020 VMContext ,
2121} ;
22- use std:: time:: Duration ;
22+ use std:: { collections :: HashSet , time:: Duration } ;
2323
2424const SECOND : Duration = Duration :: from_secs ( 1 ) ;
2525const NANOS_IN_SECOND : u64 = SECOND . as_nanos ( ) as u64 ;
@@ -61,12 +61,21 @@ impl TestSetup {
6161 }
6262
6363 fn submit_attestation_for_node ( & mut self , node_id : & NodeId , attestation : Attestation ) {
64+ self . try_submit_attestation_for_node ( node_id, attestation)
65+ . unwrap ( ) ;
66+ }
67+
68+ /// Try to submit attestation and return Result for testing failures
69+ fn try_submit_attestation_for_node (
70+ & mut self ,
71+ node_id : & NodeId ,
72+ attestation : Attestation ,
73+ ) -> Result < ( ) , mpc_contract:: errors:: Error > {
6474 let context = create_context_for_participant ( & node_id. account_id ) ;
6575 testing_env ! ( context) ;
6676 let tls_key_bytes: [ u8 ; 32 ] = node_id. tls_public_key . as_bytes ( ) [ 1 ..] . try_into ( ) . unwrap ( ) ;
6777 self . contract
6878 . submit_participant_info ( attestation, Ed25519PublicKey :: from ( tls_key_bytes) )
69- . unwrap ( ) ;
7079 }
7180
7281 /// Switches testing context to a given participant at a specific timestamp
@@ -84,11 +93,32 @@ impl TestSetup {
8493 self . contract . vote_code_hash ( hash. into ( ) ) . unwrap ( ) ;
8594 }
8695 }
96+
97+ /// Get NodeIds created from the existing participants
98+ fn get_participant_node_ids ( & self ) -> Vec < NodeId > {
99+ self . participants_list
100+ . iter ( )
101+ . map ( |( account_id, _, participant_info) | NodeId {
102+ account_id : account_id. clone ( ) ,
103+ tls_public_key : participant_info. sign_pk . clone ( ) ,
104+ } )
105+ . collect ( )
106+ }
107+
108+ /// Helper to create attestation with hash constraints
109+ fn create_hash_attestation ( hash : [ u8 ; 32 ] ) -> Attestation {
110+ Attestation :: Mock ( MockAttestation :: WithConstraints {
111+ mpc_docker_image_hash : Some ( hash) ,
112+ launcher_docker_compose_hash : None ,
113+ expiry_time_stamp_seconds : None ,
114+ } )
115+ }
87116}
88117
89118fn create_context_for_participant ( account_id : & AccountId ) -> VMContext {
90119 VMContextBuilder :: new ( )
91120 . signer_account_id ( account_id. clone ( ) )
121+ . block_timestamp ( near_sdk:: env:: block_timestamp ( ) )
92122 . build ( )
93123}
94124
@@ -437,3 +467,144 @@ fn latest_image_never_expires_if_its_not_superseded() {
437467 assert_eq ! ( allowed_image_hashes. len( ) , 1 ) ;
438468 assert_eq ! ( * allowed_image_hashes[ 0 ] , only_image_code_hash) ;
439469}
470+
471+ /// **Test for nodes starting with old but valid image hashes during grace period**
472+ ///
473+ /// This test simulates the scenario where new nodes join the network running
474+ /// older Docker image versions that are still within their grace period.
475+ /// It verifies that:
476+ /// 1. Multiple image versions can coexist during their grace periods
477+ /// 2. New nodes can successfully submit attestations with older but valid hashes
478+ /// 3. Nodes running older images remain valid until their specific grace period expires
479+ /// 4. The contract accepts attestations from nodes with any currently allowed hash
480+ ///
481+ /// This validates the scenario where nodes may start up with slightly
482+ /// older images after new ones have been voted in, as long as they're still
483+ /// within the tee_upgrade_deadline_duration.
484+ ///
485+ /// **Timeline Visualization (Grace Period = 15s):**
486+ /// ```
487+ /// Time: T=1s T=4s T=7s T=10s T=19s T=20s T=22s T=23s
488+ /// │ │ │ │ │ │ │ │
489+ /// v1 hash: ●─────────────────────────────────────────────────X (expires)
490+ /// v2 hash: ●────────────────────────────────────────────────────────────────X (expires)
491+ /// v3 hash: ●────────────────────────────────────────────────────────────→ (never expires)
492+ /// │ │ │ │ │ │ │ │
493+ /// Events: │ │ │ │ │ │ │ │
494+ /// v1 v2 v3 Test all v1 exp Check v1 v2 exp Check v2
495+ /// vote vote vote 3 versions @ T=19s expired @ T=22s expired
496+ /// still valid only v2,v3 only v3
497+ ///
498+ /// Grace Period Rules:
499+ /// - v1 expires at: T=4s + 15s + 1s = T=20s
500+ /// - v2 expires at: T=7s + 15s + 1s = T=23s
501+ /// - v3 never expires (no successor hash)
502+ ///
503+ /// Note: The +1s ensures we test *after* the grace period deadline has passed.
504+ /// Without it, the hash would still be valid exactly at the deadline timestamp.
505+ /// ```
506+ #[ test]
507+ fn nodes_can_start_with_old_valid_hashes_during_grace_period ( ) {
508+ const INITIAL_TIME_NANOS : u64 = NANOS_IN_SECOND ;
509+ const GRACE_PERIOD_SECONDS : u64 = 15 ;
510+ const GRACE_PERIOD_NANOS : u64 = GRACE_PERIOD_SECONDS * NANOS_IN_SECOND ;
511+ const HASH_DEPLOYMENT_INTERVAL_NANOS : u64 = 3 * NANOS_IN_SECOND ;
512+
513+ let init_config = InitConfig {
514+ tee_upgrade_deadline_duration_seconds : Some ( GRACE_PERIOD_SECONDS ) ,
515+ ..Default :: default ( )
516+ } ;
517+ let mut test_setup = TestSetup :: new ( 3 , 2 , Some ( init_config) ) ;
518+
519+ let hash_v1 = [ 1 ; 32 ] ; // Original version
520+ let hash_v2 = [ 2 ; 32 ] ; // Updated version
521+ let hash_v3 = [ 3 ; 32 ] ; // Latest version
522+
523+ // Deploy three hash versions at 3-second intervals (T=1s, T=4s, T=7s)
524+ let hashes = [ hash_v1, hash_v2, hash_v3] ;
525+ let mut deployment_times = Vec :: new ( ) ;
526+ let mut deployment_time = INITIAL_TIME_NANOS ;
527+
528+ for & hash in hashes. iter ( ) {
529+ test_setup. vote_with_all_participants ( hash, deployment_time) ;
530+ deployment_times. push ( deployment_time) ;
531+ deployment_time += HASH_DEPLOYMENT_INTERVAL_NANOS ;
532+ }
533+
534+ // At T=10s: All three versions should be allowed (within grace periods)
535+ let test_time_1 = deployment_times[ 2 ] + 3 * NANOS_IN_SECOND ;
536+ set_system_time ( test_time_1) ;
537+
538+ let allowed_set: HashSet < _ > = test_setup
539+ . contract
540+ . allowed_code_hashes ( )
541+ . iter ( )
542+ . map ( |h| * * h)
543+ . collect ( ) ;
544+ let expected_set: HashSet < _ > = hashes. into_iter ( ) . collect ( ) ;
545+
546+ assert_eq ! ( allowed_set, expected_set) ;
547+
548+ // Use existing participant nodes for testing different hash versions
549+ let node_ids = test_setup. get_participant_node_ids ( ) ;
550+
551+ // Test that nodes can submit attestations with all hash versions at T=10s
552+ // All attestations should succeed during grace period (current time: T=10s)
553+ for ( node, & hash) in node_ids. iter ( ) . zip ( hashes. iter ( ) ) {
554+ let attestation = TestSetup :: create_hash_attestation ( hash) ;
555+ test_setup. submit_attestation_for_node ( node, attestation) ;
556+ }
557+
558+ // Advance to T=19s: hash_v1 should expire (v2 deployed at T=4s + 15s grace = T=19s)
559+ // Note: v1 expires when its successor's (v2) grace period ends, not when v1's own grace period ends
560+ let v1_expiry_time = deployment_times[ 1 ] + GRACE_PERIOD_NANOS ;
561+
562+ // +1s ensures we're testing *after* expiration occurs - at T=19s the hash is still valid,
563+ // but at T=20s it has expired and should be filtered out by allowed_code_hashes()
564+ set_system_time ( v1_expiry_time + NANOS_IN_SECOND ) ; // T=20s
565+
566+ // Verify that only non-expired hashes remain valid at T=20s
567+ // hash_v1 should have already expired, so only hash_v2 and hash_v3 should be allowed
568+ let allowed_after_v1_expiry = test_setup. contract . allowed_code_hashes ( ) ;
569+ let allowed_set_after_v1_expiry: HashSet < _ > =
570+ allowed_after_v1_expiry. iter ( ) . map ( |h| * * h) . collect ( ) ;
571+ let expected_set_after_v1_expiry: HashSet < _ > = [ hash_v2, hash_v3] . into_iter ( ) . collect ( ) ;
572+
573+ assert_eq ! (
574+ allowed_set_after_v1_expiry, expected_set_after_v1_expiry,
575+ "Only hash_v2 and hash_v3 should remain valid at T=20s (hash_v1 expired)"
576+ ) ;
577+
578+ // Verify that submitting attestation with expired hash_v1 now fails
579+ let expired_attestation = TestSetup :: create_hash_attestation ( hash_v1) ;
580+ let result = test_setup. try_submit_attestation_for_node ( & node_ids[ 0 ] , expired_attestation) ;
581+ assert ! (
582+ result. is_err( ) ,
583+ "Attestation with expired hash_v1 should fail"
584+ ) ;
585+
586+ // Test late-joining nodes at current time T=20s (after hash_v1 expired)
587+ // Only hash_v2 and hash_v3 should be valid for new nodes
588+ // Reuse existing node_ids (nodes 2 and 3 since hash_v1 expired)
589+ for ( node, & hash) in node_ids[ 1 ..]
590+ . iter ( )
591+ . zip ( expected_set_after_v1_expiry. iter ( ) )
592+ {
593+ let late_attestation = TestSetup :: create_hash_attestation ( hash) ;
594+ test_setup. submit_attestation_for_node ( node, late_attestation) ;
595+ }
596+
597+ // Advance to T=22s: hash_v2 should expire (v3 deployed at T=7s + 15s grace = T=22s)
598+ let v2_expiry_time = deployment_times[ 2 ] + GRACE_PERIOD_NANOS ;
599+ set_system_time ( v2_expiry_time + NANOS_IN_SECOND ) ; // T=23s
600+
601+ let final_allowed_hashes = test_setup. contract . allowed_code_hashes ( ) ;
602+ assert_eq ! ( final_allowed_hashes. len( ) , 1 , "Only hash_v3 should remain" ) ;
603+ assert_eq ! ( * final_allowed_hashes[ 0 ] , hash_v3) ;
604+
605+ // Verify that only the latest hash is now accepted
606+ // Reuse the third node (index 2) for final validation
607+ let final_attestation = TestSetup :: create_hash_attestation ( hash_v3) ;
608+ // This should succeed since hash_v3 is the only remaining valid hash
609+ test_setup. submit_attestation_for_node ( & node_ids[ 2 ] , final_attestation) ;
610+ }
0 commit comments