@@ -2101,6 +2101,111 @@ where
21012101 Ok ( ( ) )
21022102}
21032103
2104+ #[ test_case( MemoryStorageBuilder :: default ( ) ; "memory" ) ]
2105+ #[ cfg_attr( feature = "storage-service" , test_case( ServiceStorageBuilder :: new( ) ; "storage_service" ) ) ]
2106+ #[ cfg_attr( feature = "rocksdb" , test_case( RocksDbStorageBuilder :: new( ) . await ; "rocks_db" ) ) ]
2107+ #[ cfg_attr( feature = "dynamodb" , test_case( DynamoDbStorageBuilder :: default ( ) ; "dynamo_db" ) ) ]
2108+ #[ cfg_attr( feature = "scylladb" , test_case( ScyllaDbStorageBuilder :: default ( ) ; "scylla_db" ) ) ]
2109+ #[ test_log:: test( tokio:: test) ]
2110+ async fn test_request_leader_timeout_client_behind_validators < B > (
2111+ storage_builder : B ,
2112+ ) -> anyhow:: Result < ( ) >
2113+ where
2114+ B : StorageBuilder ,
2115+ {
2116+ let signer = InMemorySigner :: new ( None ) ;
2117+ let clock = storage_builder. clock ( ) . clone ( ) ;
2118+ let mut builder = TestBuilder :: new ( storage_builder, 4 , 1 , signer) . await ?;
2119+ let client = builder. add_root_chain ( 1 , Amount :: from_tokens ( 3 ) ) . await ?;
2120+ let observer = builder. add_root_chain ( 2 , Amount :: ZERO ) . await ?;
2121+ let chain_id = client. chain_id ( ) ;
2122+ let observer_id = observer. chain_id ( ) ;
2123+ let owner0 = client. identity ( ) . await . unwrap ( ) ;
2124+ let owner1 = AccountSecretKey :: generate ( ) . public ( ) . into ( ) ;
2125+
2126+ // Set up multi-owner chain.
2127+ let owners = [ ( owner0, 100 ) , ( owner1, 100 ) ] ;
2128+ let ownership = ChainOwnership :: multiple ( owners, 0 , TimeoutConfig :: default ( ) ) ;
2129+ client. change_ownership ( ownership. clone ( ) ) . await . unwrap ( ) ;
2130+
2131+ // Advance to a round where owner1 is the leader (so owner0 is not).
2132+ let round_where_owner0_not_leader = loop {
2133+ let manager = client. chain_info ( ) . await . unwrap ( ) . manager ;
2134+ if manager. leader == Some ( owner1) {
2135+ break manager. current_round ;
2136+ }
2137+ clock. set ( manager. round_timeout . unwrap ( ) ) ;
2138+ client. request_leader_timeout ( ) . await . unwrap ( ) ;
2139+ } ;
2140+ let round_number = match round_where_owner0_not_leader {
2141+ Round :: SingleLeader ( n) => n,
2142+ round => panic ! ( "Unexpected round {round:?}" ) ,
2143+ } ;
2144+
2145+ // Now create a second client on the same chain to advance validators ahead
2146+ // to the next round, where owner0 will be the leader.
2147+ let client2 = builder
2148+ . make_client ( chain_id, None , BlockHeight :: ZERO )
2149+ . await ?;
2150+
2151+ // Sync client2 to get the current state.
2152+ client2. synchronize_from_validators ( ) . await ?;
2153+
2154+ // Advance validators one more round using client2.
2155+ let timeout = client2
2156+ . chain_info ( )
2157+ . await
2158+ . unwrap ( )
2159+ . manager
2160+ . round_timeout
2161+ . expect ( "round_timeout should be set after sync" ) ;
2162+ clock. set ( timeout) ;
2163+ client2. request_leader_timeout ( ) . await . unwrap ( ) ;
2164+
2165+ // Validators are now in round_number + 1.
2166+ let validator_round = Round :: SingleLeader ( round_number + 1 ) ;
2167+ builder
2168+ . check_that_validators_are_in_round ( chain_id, BlockHeight :: from ( 1 ) , validator_round, 3 )
2169+ . await ;
2170+
2171+ // At this point:
2172+ // - The client's local state shows it's in round_number (where owner1 is the leader).
2173+ // - The validators are in round_number + 1 (where owner0 is the leader).
2174+ // When the client tries to transfer, it will see it's not the leader in its current round
2175+ // and will try to request a timeout. That timeout request for round_number will be rejected
2176+ // by validators (who are in round_number + 1). The client should handle this error,
2177+ // sync to the actual round, and complete the transfer.
2178+
2179+ let result = client
2180+ . transfer (
2181+ AccountOwner :: CHAIN ,
2182+ Amount :: ONE ,
2183+ Account :: chain ( observer_id) ,
2184+ )
2185+ . await ;
2186+
2187+ // The transfer should succeed after the client discovers it's actually the leader in the validator's current round.
2188+ match result {
2189+ Ok ( ClientOutcome :: Committed ( _) ) => {
2190+ // Success! The client handled the round mismatch and completed the transfer.
2191+ }
2192+ Ok ( ClientOutcome :: WaitForTimeout ( _) ) => {
2193+ panic ! (
2194+ "Transfer returned WaitForTimeout, but the client should have discovered \
2195+ it's the leader in the validator's current round and completed the transfer."
2196+ ) ;
2197+ }
2198+ Err ( e) => {
2199+ panic ! (
2200+ "Transfer failed with error: {e:?}. The client should have handled the \
2201+ round mismatch automatically by syncing with validators."
2202+ ) ;
2203+ }
2204+ }
2205+
2206+ Ok ( ( ) )
2207+ }
2208+
21042209#[ test_case( MemoryStorageBuilder :: default ( ) ; "memory" ) ]
21052210#[ cfg_attr( feature = "storage-service" , test_case( ServiceStorageBuilder :: new( ) ; "storage_service" ) ) ]
21062211#[ cfg_attr( feature = "rocksdb" , test_case( RocksDbStorageBuilder :: new( ) . await ; "rocks_db" ) ) ]
0 commit comments