From d84306f9a83a4a2358ef18a5033dfb0812170a87 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rafa=C5=82=20Chabowski?=
<88321181+rafal-ch@users.noreply.github.com>
Date: Thu, 2 Jan 2025 16:19:53 +0100
Subject: [PATCH] Integration test for balances and (non)retryable messages
(#2505)
Closes https://github.com/FuelLabs/fuel-core/issues/2474
## Description
This PR changes the following:
1. Adds new test in `tests/tests/relayer.rs` - to ensure that only
**non**-retryable messages contribute to the balance returned from
`balance()`, `balances()` and `coins_to_spend()`[^1] endpoints
2. Modifies the `balance()` test in `tests/tests/balances.rs` to also
include some retryable message which is expected **not** to contribute
to the total balance.
* for `balances()` and `coins_to_spend()` it was already the case
1st test checks the behavior of the actual blockchain
operations while the 2nd tests the proper initialization of
the balances index at genesis.
## Checklist
- [X] Breaking changes are clearly marked as such in the PR description
and changelog
- [X] New behavior is reflected in tests
### Before requesting review
- [X] I have reviewed the code myself
[^1]: currently, `coins_to_spend()` is not using indexation, this will
change when [this PR](https://github.com/FuelLabs/fuel-core/pull/2463)
is merged.
---------
Co-authored-by: Green Baneling
---
tests/tests/balances.rs | 36 +++---
tests/tests/relayer.rs | 237 ++++++++++++++++++++++++++++++++++++++++
2 files changed, 258 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![],
diff --git a/tests/tests/relayer.rs b/tests/tests/relayer.rs
index 9ea80b1e242..e92e335b3fc 100644
--- a/tests/tests/relayer.rs
+++ b/tests/tests/relayer.rs
@@ -22,6 +22,7 @@ use fuel_core_client::client::{
PaginationRequest,
},
types::{
+ CoinType,
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();
@@ -477,3 +484,233 @@ async fn handle(
Ok(Response::new(Body::from(r)))
}
+
+#[tokio::test(flavor = "multi_thread")]
+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());
+ 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_RESULTS: 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 RETRYABLE_AMOUNT: u64 = 99;
+ const RETRYABLE_NONCE: u64 = 0;
+ const NON_RETRYABLE_AMOUNT: u64 = 100;
+ 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.
+ 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: 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_RESULTS,
+ direction: PageDirection::Forward,
+ },
+ )
+ .await
+ .unwrap();
+
+ if !query.results.is_empty() {
+ break;
+ }
+ }
+ })
+ .await;
+ if let Err(_) = result {
+ panic!("Off-chain worker didn't process balances within timeout")
+ }
+ // Then
+
+ // Expect two messages to be available
+ let query = client
+ .messages(
+ None,
+ PaginationRequest {
+ cursor: None,
+ results: UNLIMITED_QUERY_RESULTS,
+ 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 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 balance to be returned via "balances"
+ let query = client
+ .balances(
+ &recipient,
+ PaginationRequest {
+ cursor: None,
+ results: UNLIMITED_QUERY_RESULTS,
+ direction: PageDirection::Forward,
+ },
+ )
+ .await
+ .unwrap();
+ assert_eq!(query.results.len(), 1);
+ 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);
+
+ // 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();
+}
+
+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()
+}