From 31c691d1dbd5b226b64252931d0f91cdfac74bf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Chabowski?= Date: Wed, 11 Dec 2024 12:55:10 +0100 Subject: [PATCH 1/6] Add retryable message to `balance` test --- tests/tests/balances.rs | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/tests/tests/balances.rs b/tests/tests/balances.rs index b2804141295..a658e36e9f2 100644 --- a/tests/tests/balances.rs +++ b/tests/tests/balances.rs @@ -33,6 +33,9 @@ use fuel_core_types::{ }, }; +const RETRYABLE: &[u8] = &[1]; +const NON_RETRYABLE: &[u8] = &[]; + #[tokio::test] async fn balance() { let owner = Address::default(); @@ -55,18 +58,22 @@ async fn balance() { ..coin_generator.generate() }) .collect(), - messages: vec![(owner, 60), (owner, 90)] - .into_iter() - .enumerate() - .map(|(nonce, (owner, amount))| MessageConfig { - sender: owner, - recipient: owner, - nonce: (nonce as u64).into(), - amount, - data: vec![], - da_height: DaBlockHeight::from(0usize), - }) - .collect(), + messages: vec![ + (owner, 60, NON_RETRYABLE), + (owner, 90, NON_RETRYABLE), + (owner, 200000, RETRYABLE), + ] + .into_iter() + .enumerate() + .map(|(nonce, (owner, amount, data))| MessageConfig { + sender: owner, + recipient: owner, + nonce: (nonce as u64).into(), + amount, + data: data.to_vec(), + da_height: DaBlockHeight::from(0usize), + }) + .collect(), ..Default::default() }; let config = Config::local_node_with_state_config(state_config); @@ -129,6 +136,8 @@ async fn balance() { client.submit_and_await_commit(&tx).await.unwrap(); let balance = client.balance(&owner, Some(&asset_id)).await.unwrap(); + + // Note that the big (200000) message, which is RETRYABLE is not included in the balance assert_eq!(balance, 449); } @@ -137,9 +146,6 @@ async fn balance_messages_only() { let owner = Address::default(); let asset_id = AssetId::BASE; - const RETRYABLE: &[u8] = &[1]; - const NON_RETRYABLE: &[u8] = &[]; - // setup config let state_config = StateConfig { contracts: vec![], From 777c9778b16722b0f5936f004a4c0481ebb5f95e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Chabowski?= Date: Tue, 17 Dec 2024 11:14:44 +0100 Subject: [PATCH 2/6] Add tests for balances not returning retryable messages --- tests/tests/relayer.rs | 139 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/tests/tests/relayer.rs b/tests/tests/relayer.rs index 9ea80b1e242..84a4b5b32bb 100644 --- a/tests/tests/relayer.rs +++ b/tests/tests/relayer.rs @@ -477,3 +477,142 @@ async fn handle( Ok(Response::new(Body::from(r))) } + +#[tokio::test(flavor = "multi_thread")] +async fn balances_do_not_return_retryable_messages() { + let mut rng = StdRng::seed_from_u64(1234); + let mut config = Config::local_node(); + config.relayer = Some(relayer::Config::default()); + let relayer_config = config.relayer.as_mut().expect("Expected relayer config"); + let eth_node = MockMiddleware::default(); + let contract_address = relayer_config.eth_v2_listening_contracts[0]; + + // Large enough to get all messages, but not to trigger the "query is too complex" error. + const UNLIMITED_QUERY: i32 = 100; + + // Given + + // setup a retryable and non-retryable message + let secret_key: SecretKey = SecretKey::random(&mut rng); + let public_key = secret_key.public_key(); + let recipient = Input::owner(&public_key); + const SENDER: Address = Address::zeroed(); + + let retryable_data = Some(vec![1]); + const RETRYABLE_AMOUNT: u64 = 99; + + const NON_RETRYABLE_DATA: Option> = None; + const NON_RETRYABLE_AMOUNT: u64 = 100; + + let logs = vec![ + make_message_event( + Nonce::from(1u64), + 5, + contract_address, + Some(SENDER.into()), + Some(recipient.into()), + Some(NON_RETRYABLE_AMOUNT), + NON_RETRYABLE_DATA, + 0, + ), + make_message_event( + Nonce::from(2u64), + 5, + contract_address, + Some(SENDER.into()), + Some(recipient.into()), + Some(RETRYABLE_AMOUNT), + retryable_data, + 0, + ), + ]; + eth_node.update_data(|data| data.logs_batch = vec![logs.clone()]); + // Setup the eth node with a block high enough that there + // will be some finalized blocks. + eth_node.update_data(|data| data.best_block.number = Some(200.into())); + let eth_node = Arc::new(eth_node); + let eth_node_handle = spawn_eth_node(eth_node).await; + + relayer_config.relayer = Some(vec![format!("http://{}", eth_node_handle.address) + .as_str() + .try_into() + .unwrap()]); + + config.utxo_validation = true; + + // setup fuel node with mocked eth url + let db = Database::in_memory(); + + let srv = FuelService::from_database(db.clone(), config) + .await + .unwrap(); + + let client = FuelClient::from(srv.bound_address); + let base_asset_id = client + .consensus_parameters(0) + .await + .unwrap() + .unwrap() + .base_asset_id() + .clone(); + + // When + + // wait for relayer to catch up to eth node + srv.await_relayer_synced().await.unwrap(); + // Wait for the block producer to create a block that targets the latest da height. + srv.shared + .poa_adapter + .manually_produce_blocks( + None, + Mode::Blocks { + number_of_blocks: 100, + }, + ) + .await + .unwrap(); + + // Then + + // Expect two messages to be available + let query = client + .messages( + None, + PaginationRequest { + cursor: None, + results: UNLIMITED_QUERY, + direction: PageDirection::Forward, + }, + ) + .await + .unwrap(); + assert_eq!(query.results.len(), 2); + let total_amount = query.results.iter().map(|m| m.amount).sum::(); + assert_eq!(total_amount, NON_RETRYABLE_AMOUNT + RETRYABLE_AMOUNT); + + // Expect only the non-retryable message to be returned via "balance" + let query = client + .balance(&recipient, Some(&base_asset_id)) + .await + .unwrap(); + assert_eq!(query, NON_RETRYABLE_AMOUNT); + + // Expect only the non-retryable message to be returned via "balances" + let query = client + .balances( + &recipient, + PaginationRequest { + cursor: None, + results: UNLIMITED_QUERY, + direction: PageDirection::Forward, + }, + ) + .await + .unwrap(); + assert_eq!(query.results.len(), 1); + let total_amount = query.results.iter().map(|m| m.amount).sum::(); + assert_eq!(total_amount, NON_RETRYABLE_AMOUNT as u128); + + srv.send_stop_signal_and_await_shutdown().await.unwrap(); + eth_node_handle.shutdown.send(()).unwrap(); +} From 557b209b59f5627cafc3a4a06cdb2c599207e090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Chabowski?= Date: Tue, 17 Dec 2024 12:04:56 +0100 Subject: [PATCH 3/6] Improve test to wait for the off-chain worker --- tests/tests/relayer.rs | 121 +++++++++++++++++++++++++++++++---------- 1 file changed, 91 insertions(+), 30 deletions(-) diff --git a/tests/tests/relayer.rs b/tests/tests/relayer.rs index 84a4b5b32bb..8bfc2d3cabc 100644 --- a/tests/tests/relayer.rs +++ b/tests/tests/relayer.rs @@ -22,6 +22,7 @@ use fuel_core_client::client::{ PaginationRequest, }, types::{ + message::MessageStatus, RelayedTransactionStatus as ClientRelayedTransactionStatus, TransactionStatus, }, @@ -72,9 +73,15 @@ use std::{ SocketAddr, }, sync::Arc, + time::Duration, }; use tokio::sync::oneshot::Sender; +enum MessageKind { + Retryable { nonce: u64, amount: u64 }, + NonRetryable { nonce: u64, amount: u64 }, +} + #[tokio::test(flavor = "multi_thread")] async fn relayer_can_download_logs() { let mut config = Config::local_node(); @@ -486,6 +493,7 @@ async fn balances_do_not_return_retryable_messages() { let relayer_config = config.relayer.as_mut().expect("Expected relayer config"); let eth_node = MockMiddleware::default(); let contract_address = relayer_config.eth_v2_listening_contracts[0]; + const TIMEOUT: Duration = Duration::from_secs(1); // Large enough to get all messages, but not to trigger the "query is too complex" error. const UNLIMITED_QUERY: i32 = 100; @@ -496,36 +504,23 @@ async fn balances_do_not_return_retryable_messages() { let secret_key: SecretKey = SecretKey::random(&mut rng); let public_key = secret_key.public_key(); let recipient = Input::owner(&public_key); - const SENDER: Address = Address::zeroed(); - let retryable_data = Some(vec![1]); const RETRYABLE_AMOUNT: u64 = 99; - - const NON_RETRYABLE_DATA: Option> = None; + const RETRYABLE_NONCE: u64 = 0; const NON_RETRYABLE_AMOUNT: u64 = 100; - - let logs = vec![ - make_message_event( - Nonce::from(1u64), - 5, - contract_address, - Some(SENDER.into()), - Some(recipient.into()), - Some(NON_RETRYABLE_AMOUNT), - NON_RETRYABLE_DATA, - 0, - ), - make_message_event( - Nonce::from(2u64), - 5, - contract_address, - Some(SENDER.into()), - Some(recipient.into()), - Some(RETRYABLE_AMOUNT), - retryable_data, - 0, - ), + const NON_RETRYABLE_NONCE: u64 = 1; + let messages = vec![ + MessageKind::Retryable { + nonce: RETRYABLE_NONCE, + amount: RETRYABLE_AMOUNT, + }, + MessageKind::NonRetryable { + nonce: NON_RETRYABLE_NONCE, + amount: NON_RETRYABLE_AMOUNT, + }, ]; + let logs: Vec<_> = setup_messages(&messages, &recipient, &contract_address); + eth_node.update_data(|data| data.logs_batch = vec![logs.clone()]); // Setup the eth node with a block high enough that there // will be some finalized blocks. @@ -566,12 +561,37 @@ async fn balances_do_not_return_retryable_messages() { .manually_produce_blocks( None, Mode::Blocks { - number_of_blocks: 100, + number_of_blocks: 1, }, ) .await .unwrap(); + // Balances are processed in the off-chain worker, so we need to wait for it + // to process the messages before we can assert the balances. + let result = tokio::time::timeout(TIMEOUT, async { + loop { + let query = client + .balances( + &recipient, + PaginationRequest { + cursor: None, + results: UNLIMITED_QUERY, + direction: PageDirection::Forward, + }, + ) + .await + .unwrap(); + + if !query.results.is_empty() { + break; + } + } + }) + .await; + if let Err(_) = result { + panic!("Off-chain worker didn't process balances withing timeout") + } // Then // Expect two messages to be available @@ -590,14 +610,14 @@ async fn balances_do_not_return_retryable_messages() { let total_amount = query.results.iter().map(|m| m.amount).sum::(); assert_eq!(total_amount, NON_RETRYABLE_AMOUNT + RETRYABLE_AMOUNT); - // Expect only the non-retryable message to be returned via "balance" + // Expect only the non-retryable message balance to be returned via "balance" let query = client .balance(&recipient, Some(&base_asset_id)) .await .unwrap(); assert_eq!(query, NON_RETRYABLE_AMOUNT); - // Expect only the non-retryable message to be returned via "balances" + // Expect only the non-retryable message balance to be returned via "balances" let query = client .balances( &recipient, @@ -610,9 +630,50 @@ async fn balances_do_not_return_retryable_messages() { .await .unwrap(); assert_eq!(query.results.len(), 1); - let total_amount = query.results.iter().map(|m| m.amount).sum::(); + let total_amount = query + .results + .iter() + .map(|m| { + assert_eq!(m.asset_id, base_asset_id); + m.amount + }) + .sum::(); assert_eq!(total_amount, NON_RETRYABLE_AMOUNT as u128); srv.send_stop_signal_and_await_shutdown().await.unwrap(); eth_node_handle.shutdown.send(()).unwrap(); } + +fn setup_messages( + messages: &[MessageKind], + recipient: &Address, + contract_address: &Bytes20, +) -> Vec { + const SENDER: Address = Address::zeroed(); + + messages + .iter() + .map(|m| match m { + MessageKind::Retryable { nonce, amount } => make_message_event( + Nonce::from(*nonce), + 5, + *contract_address, + Some(SENDER.into()), + Some((*recipient).into()), + Some(*amount), + Some(vec![1]), + 0, + ), + MessageKind::NonRetryable { nonce, amount } => make_message_event( + Nonce::from(*nonce), + 5, + *contract_address, + Some(SENDER.into()), + Some((*recipient).into()), + Some(*amount), + None, + 0, + ), + }) + .collect() +} From e2f3d7b8a0c19d9887e204c12745dc7817c659bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Chabowski?= Date: Tue, 17 Dec 2024 12:08:37 +0100 Subject: [PATCH 4/6] Fix typo --- tests/tests/relayer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests/relayer.rs b/tests/tests/relayer.rs index 8bfc2d3cabc..f5a2ce64ab3 100644 --- a/tests/tests/relayer.rs +++ b/tests/tests/relayer.rs @@ -590,7 +590,7 @@ async fn balances_do_not_return_retryable_messages() { }) .await; if let Err(_) = result { - panic!("Off-chain worker didn't process balances withing timeout") + panic!("Off-chain worker didn't process balances within timeout") } // Then From 90ead90ed4d368d87e311286846d8cbb9133eb35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Chabowski?= Date: Tue, 17 Dec 2024 13:37:45 +0100 Subject: [PATCH 5/6] Extend the test to also cover coins to spend --- tests/tests/relayer.rs | 47 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/tests/tests/relayer.rs b/tests/tests/relayer.rs index f5a2ce64ab3..a5988b2fd88 100644 --- a/tests/tests/relayer.rs +++ b/tests/tests/relayer.rs @@ -486,7 +486,7 @@ async fn handle( } #[tokio::test(flavor = "multi_thread")] -async fn balances_do_not_return_retryable_messages() { +async fn balances_and_coins_to_spend_never_return_retryable_messages() { let mut rng = StdRng::seed_from_u64(1234); let mut config = Config::local_node(); config.relayer = Some(relayer::Config::default()); @@ -496,7 +496,7 @@ async fn balances_do_not_return_retryable_messages() { const TIMEOUT: Duration = Duration::from_secs(1); // Large enough to get all messages, but not to trigger the "query is too complex" error. - const UNLIMITED_QUERY: i32 = 100; + const UNLIMITED_QUERY_RESULTS: i32 = 100; // Given @@ -576,7 +576,7 @@ async fn balances_do_not_return_retryable_messages() { &recipient, PaginationRequest { cursor: None, - results: UNLIMITED_QUERY, + results: UNLIMITED_QUERY_RESULTS, direction: PageDirection::Forward, }, ) @@ -600,7 +600,7 @@ async fn balances_do_not_return_retryable_messages() { None, PaginationRequest { cursor: None, - results: UNLIMITED_QUERY, + results: UNLIMITED_QUERY_RESULTS, direction: PageDirection::Forward, }, ) @@ -623,7 +623,7 @@ async fn balances_do_not_return_retryable_messages() { &recipient, PaginationRequest { cursor: None, - results: UNLIMITED_QUERY, + results: UNLIMITED_QUERY_RESULTS, direction: PageDirection::Forward, }, ) @@ -640,6 +640,43 @@ async fn balances_do_not_return_retryable_messages() { .sum::(); assert_eq!(total_amount, NON_RETRYABLE_AMOUNT as u128); + // Expect only the non-retryable message balance to be returned via "coins to spend" + let query = client + .coins_to_spend( + &recipient, + vec![(base_asset_id, NON_RETRYABLE_AMOUNT, None)], + None, + ) + .await + .unwrap(); + let message_coins: Vec<_> = query + .iter() + .flatten() + .map(|m| { + let CoinType::MessageCoin(m) = m else { + panic!("should have message coin") + }; + m + }) + .collect(); + assert_eq!(message_coins.len(), 1); + assert_eq!(message_coins[0].amount, NON_RETRYABLE_AMOUNT); + assert_eq!(message_coins[0].nonce, NON_RETRYABLE_NONCE.into()); + + // Expect no messages when querying more than the available non-retryable amount + let query = client + .coins_to_spend( + &recipient, + vec![(base_asset_id, NON_RETRYABLE_AMOUNT + 1, None)], + None, + ) + .await + .unwrap_err(); + assert_eq!( + query.to_string(), + "Response errors; not enough coins to fit the target" + ); + srv.send_stop_signal_and_await_shutdown().await.unwrap(); eth_node_handle.shutdown.send(()).unwrap(); } From 0f582aae842979c5ce4ff156a865cb21500f2662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Chabowski?= Date: Tue, 17 Dec 2024 13:41:06 +0100 Subject: [PATCH 6/6] Satisfy clippy --- tests/tests/relayer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests/relayer.rs b/tests/tests/relayer.rs index a5988b2fd88..e92e335b3fc 100644 --- a/tests/tests/relayer.rs +++ b/tests/tests/relayer.rs @@ -22,7 +22,7 @@ use fuel_core_client::client::{ PaginationRequest, }, types::{ - message::MessageStatus, + CoinType, RelayedTransactionStatus as ClientRelayedTransactionStatus, TransactionStatus, },