diff --git a/CHANGELOG.md b/CHANGELOG.md index ace1d37f6cf..451e72038bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed +- [2473](https://github.com/FuelLabs/fuel-core/pull/2473): Graphql requests and responses make use of a new `extensions` object to specify request/response metadata. A request `extensions` object can contain an integer-valued `required_fuel_block_height` field. When specified, the request will return an error unless the node's current fuel block height is at least the value specified in the `required_fuel_block_height` field. All graphql responses now contain an integer-valued `current_fuel_block_height` field in the `extensions` object, which contains the block height of the last block processed by the node. - [2653](https://github.com/FuelLabs/fuel-core/pull/2653): Added cleaner error for wasm-executor upon failed deserialization. - [2705](https://github.com/FuelLabs/fuel-core/pull/2705): Update the default value for `--max-block-size` and `--max-transmit-size` to 50 MB diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index bdbcc054b49..000f95abe71 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1,33 +1,40 @@ #[cfg(feature = "subscriptions")] use crate::client::types::StatusWithTransaction; -use crate::client::{ - schema::{ - block::BlockByHeightArgs, - coins::{ - ExcludeInput, - SpendQueryElementInput, +use crate::{ + client::{ + schema::{ + block::BlockByHeightArgs, + coins::{ + ExcludeInput, + SpendQueryElementInput, + }, + contract::ContractBalanceQueryArgs, + gas_price::EstimateGasPrice, + message::MessageStatusArgs, + relayed_tx::RelayedTransactionStatusArgs, + tx::DryRunArg, + Tai64Timestamp, + TransactionId, }, - contract::ContractBalanceQueryArgs, - gas_price::EstimateGasPrice, - message::MessageStatusArgs, - relayed_tx::RelayedTransactionStatusArgs, - tx::DryRunArg, - Tai64Timestamp, - TransactionId, - }, - types::{ - asset::AssetDetail, - gas_price::LatestGasPrice, - message::MessageStatus, - primitives::{ - Address, - AssetId, - BlockId, - ContractId, - UtxoId, + types::{ + asset::AssetDetail, + gas_price::LatestGasPrice, + message::MessageStatus, + primitives::{ + Address, + AssetId, + BlockId, + ContractId, + UtxoId, + }, + upgrades::StateTransitionBytecode, + RelayedTransactionStatus, }, - upgrades::StateTransitionBytecode, - RelayedTransactionStatus, + }, + reqwest_ext::{ + FuelGraphQlResponse, + FuelOperation, + ReqwestExt, }, }; use anyhow::Context; @@ -39,8 +46,6 @@ use base64::prelude::{ #[cfg(feature = "subscriptions")] use cynic::StreamingOperation; use cynic::{ - http::ReqwestExt, - GraphQlResponse, Id, MutationBuilder, Operation, @@ -129,6 +134,10 @@ use std::{ self, FromStr, }, + sync::{ + Arc, + Mutex, + }, }; use tai64::Tai64; use tracing as _; @@ -151,12 +160,56 @@ pub mod types; type RegisterId = u32; +#[derive(Debug, derive_more::Display, derive_more::From)] +#[non_exhaustive] +/// Error occurring during interaction with the FuelClient +// anyhow::Error is wrapped inside a custom Error type, +// so that we can specific error variants in the future. +pub enum Error { + /// Unknown or not expected(by architecture) error. + #[from] + Other(anyhow::Error), +} + +/// Consistency policy for the [`FuelClient`] to define the strategy +/// for the required height feature. +#[derive(Debug)] +pub enum ConsistencyPolicy { + /// Automatically fetch the next block height from the response and + /// use it as an input to the next query to guarantee consistency + /// of the results for the queries. + Auto { + /// The required block height for the queries. + height: Arc>>, + }, + /// Use manually sets the block height for all queries + /// via the [`FuelClient::with_required_fuel_block_height`]. + Manual { + /// The required block height for the queries. + height: Option, + }, +} + +impl Clone for ConsistencyPolicy { + fn clone(&self) -> Self { + match self { + Self::Auto { height } => Self::Auto { + // We don't want to share the same mutex between the different + // instances of the `FuelClient`. + height: Arc::new(Mutex::new(height.lock().ok().and_then(|h| *h))), + }, + Self::Manual { height } => Self::Manual { height: *height }, + } + } +} + #[derive(Debug, Clone)] pub struct FuelClient { client: reqwest::Client, #[cfg(feature = "subscriptions")] cookie: std::sync::Arc, url: reqwest::Url, + require_height: ConsistencyPolicy, } impl FromStr for FuelClient { @@ -184,13 +237,22 @@ impl FromStr for FuelClient { client, cookie, url, + require_height: ConsistencyPolicy::Auto { + height: Arc::new(Mutex::new(None)), + }, }) } #[cfg(not(feature = "subscriptions"))] { let client = reqwest::Client::new(); - Ok(Self { client, url }) + Ok(Self { + client, + url, + require_height: ConsistencyPolicy::Auto { + height: Arc::new(Mutex::new(None)), + }, + }) } } } @@ -223,6 +285,36 @@ impl FuelClient { Self::from_str(url.as_ref()) } + pub fn with_required_fuel_block_height( + &mut self, + new_height: Option, + ) -> &mut Self { + match &mut self.require_height { + ConsistencyPolicy::Auto { height } => { + *height.lock().expect("Mutex poisoned") = new_height; + } + ConsistencyPolicy::Manual { height } => { + *height = new_height; + } + } + self + } + + pub fn use_manual_consistency_policy( + &mut self, + height: Option, + ) -> &mut Self { + self.require_height = ConsistencyPolicy::Manual { height }; + self + } + + pub fn required_block_height(&self) -> Option { + match &self.require_height { + ConsistencyPolicy::Auto { height } => height.lock().ok().and_then(|h| *h), + ConsistencyPolicy::Manual { height } => *height, + } + } + /// Send the GraphQL query to the client. pub async fn query( &self, @@ -232,20 +324,59 @@ impl FuelClient { Vars: serde::Serialize, ResponseData: serde::de::DeserializeOwned + 'static, { + let required_fuel_block_height = self.required_block_height(); + let fuel_operation = FuelOperation::new(q, required_fuel_block_height); let response = self .client .post(self.url.clone()) - .run_graphql(q) + .run_fuel_graphql(fuel_operation) .await .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; - Self::decode_response(response) + let inner_required_height = match &self.require_height { + ConsistencyPolicy::Auto { height } => Some(height.clone()), + _ => None, + }; + + Self::decode_response(response, inner_required_height) } - fn decode_response(response: GraphQlResponse) -> io::Result + fn decode_response( + response: FuelGraphQlResponse, + inner_required_height: Option>>>, + ) -> io::Result where R: serde::de::DeserializeOwned + 'static, { + if let Some(inner_required_height) = inner_required_height { + if let Some(current_fuel_block_height) = response + .extensions + .as_ref() + .and_then(|e| e.current_fuel_block_height) + { + let mut lock = inner_required_height.lock().expect("Mutex poisoned"); + + if current_fuel_block_height >= lock.unwrap_or_default() { + *lock = Some(current_fuel_block_height); + } + } + } + + if let Some(failed) = response + .extensions + .as_ref() + .and_then(|e| e.fuel_block_height_precondition_failed) + { + if failed { + return Err(io::Error::new( + io::ErrorKind::Other, + "The required block height was not met", + )); + } + } + + let response = response.response; + match (response.data, response.errors) { (Some(d), _) => Ok(d), (_, Some(e)) => Err(from_strings_errors_to_std_error( @@ -271,7 +402,11 @@ impl FuelClient { use reqwest::cookie::CookieStore; let mut url = self.url.clone(); url.set_path("/v1/graphql-sub"); - let json_query = serde_json::to_string(&q)?; + + let required_fuel_block_height = self.required_block_height(); + let fuel_operation = FuelOperation::new(q, required_fuel_block_height); + + let json_query = serde_json::to_string(&fuel_operation)?; let mut client_builder = es::ClientBuilder::for_url(url.as_str()) .map_err(|e| { io::Error::new( @@ -329,18 +464,25 @@ impl FuelClient { let mut last = None; + let inner_required_height = match &self.require_height { + ConsistencyPolicy::Auto { height } => Some(height.clone()), + _ => None, + }; + let stream = es::Client::stream(&client) - .take_while(|result| { + .zip(futures::stream::repeat(inner_required_height)) + .take_while(|(result, _)| { futures::future::ready(!matches!(result, Err(es::Error::Eof))) }) - .filter_map(move |result| { + .filter_map(move |(result, inner_required_height)| { tracing::debug!("Got result: {result:?}"); let r = match result { Ok(es::SSE::Event(es::Event { data, .. })) => { - match serde_json::from_str::>(&data) - { + match serde_json::from_str::>( + &data, + ) { Ok(resp) => { - match Self::decode_response(resp) { + match Self::decode_response(resp, inner_required_height) { Ok(resp) => { match last.replace(data) { // Remove duplicates diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 728cd0be4dd..d415e09c906 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -3,6 +3,7 @@ #![deny(unused_crate_dependencies)] #![deny(warnings)] pub mod client; +pub mod reqwest_ext; pub mod schema; /// The GraphQL schema used by the library. diff --git a/crates/client/src/reqwest_ext.rs b/crates/client/src/reqwest_ext.rs new file mode 100644 index 00000000000..c0ca68111bf --- /dev/null +++ b/crates/client/src/reqwest_ext.rs @@ -0,0 +1,214 @@ +use cynic::{ + http::CynicReqwestError, + GraphQlResponse, + Operation, +}; +use fuel_core_types::fuel_types::BlockHeight; +use std::{ + future::Future, + marker::PhantomData, + pin::Pin, +}; + +#[derive(Debug, Clone, serde::Serialize)] +pub struct ExtensionsRequest { + pub required_fuel_block_height: Option, +} + +#[derive(Debug, Clone, serde::Deserialize)] +pub struct ExtensionsResponse { + pub required_fuel_block_height: Option, + pub current_fuel_block_height: Option, + pub fuel_block_height_precondition_failed: Option, +} + +#[derive(Debug, serde::Serialize)] +pub struct FuelOperation { + #[serde(flatten)] + pub operation: Operation, + pub extensions: ExtensionsRequest, +} + +#[derive(Debug, serde::Deserialize)] +pub struct FuelGraphQlResponse { + #[serde(flatten)] + pub response: GraphQlResponse, + pub extensions: Option, +} + +impl FuelOperation { + pub fn new( + operation: Operation, + required_fuel_block_height: Option, + ) -> Self { + Self { + operation, + extensions: ExtensionsRequest { + required_fuel_block_height, + }, + } + } +} + +#[cfg(not(target_arch = "wasm32"))] +type BoxFuture<'a, T> = Pin + Send + 'a>>; + +#[cfg(target_arch = "wasm32")] +type BoxFuture<'a, T> = Pin + 'a>>; + +/// An extension trait for reqwest::RequestBuilder. +/// +/// ```rust,no_run +/// # mod schema { +/// # cynic::use_schema!("../schemas/starwars.schema.graphql"); +/// # } +/// # +/// # #[derive(cynic::QueryFragment)] +/// # #[cynic( +/// # schema_path = "../schemas/starwars.schema.graphql", +/// # schema_module = "schema", +/// # )] +/// # struct Film { +/// # title: Option, +/// # director: Option +/// # } +/// # +/// # #[derive(cynic::QueryFragment)] +/// # #[cynic( +/// # schema_path = "../schemas/starwars.schema.graphql", +/// # schema_module = "schema", +/// # graphql_type = "Root" +/// # )] +/// # struct FilmDirectorQuery { +/// # #[arguments(id = cynic::Id::new("ZmlsbXM6MQ=="))] +/// # film: Option, +/// # } +/// use cynic::{http::ReqwestExt, QueryBuilder}; +/// +/// # async move { +/// let operation = FilmDirectorQuery::build(()); +/// +/// let client = reqwest::Client::new(); +/// let response = client.post("https://swapi-graphql.netlify.app/.netlify/functions/index") +/// .run_graphql(operation) +/// .await +/// .unwrap(); +/// +/// println!( +/// "The director is {}", +/// response.data +/// .and_then(|d| d.film) +/// .and_then(|f| f.director) +/// .unwrap() +/// ); +/// # }; +/// ``` +pub trait ReqwestExt { + /// Runs a GraphQL query with the parameters in RequestBuilder, deserializes + /// the and returns the result. + fn run_fuel_graphql( + self, + operation: FuelOperation>, + ) -> CynicReqwestBuilder + where + Vars: serde::Serialize, + ResponseData: serde::de::DeserializeOwned + 'static; +} + +/// A builder for cynics reqwest integration +/// +/// Implements `IntoFuture`, users should `.await` the builder or call +/// `into_future` directly when they're ready to send the request. +pub struct CynicReqwestBuilder { + builder: reqwest::RequestBuilder, + _marker: std::marker::PhantomData (ResponseData, ErrorExtensions)>, +} + +impl CynicReqwestBuilder { + pub fn new(builder: reqwest::RequestBuilder) -> Self { + Self { + builder, + _marker: std::marker::PhantomData, + } + } +} + +impl + std::future::IntoFuture for CynicReqwestBuilder +{ + type Output = Result, CynicReqwestError>; + + type IntoFuture = BoxFuture< + 'static, + Result, CynicReqwestError>, + >; + + fn into_future(self) -> Self::IntoFuture { + Box::pin(async move { + let http_result = self.builder.send().await; + deser_gql(http_result).await + }) + } +} + +impl CynicReqwestBuilder { + /// Sets the type that will be deserialized for the extensions fields of any errors in the response + pub fn retain_extensions( + self, + ) -> CynicReqwestBuilder + where + ErrorExtensions: serde::de::DeserializeOwned, + { + let CynicReqwestBuilder { builder, _marker } = self; + + CynicReqwestBuilder { + builder, + _marker: PhantomData, + } + } +} + +async fn deser_gql( + response: Result, +) -> Result, CynicReqwestError> +where + ResponseData: serde::de::DeserializeOwned, + ErrorExtensions: serde::de::DeserializeOwned, +{ + let response = match response { + Ok(response) => response, + Err(e) => return Err(CynicReqwestError::ReqwestError(e)), + }; + + let status = response.status(); + if !status.is_success() { + let text = response.text().await; + let text = match text { + Ok(text) => text, + Err(e) => return Err(CynicReqwestError::ReqwestError(e)), + }; + + let Ok(deserred) = serde_json::from_str(&text) else { + let response = CynicReqwestError::ErrorResponse(status, text); + return Err(response); + }; + + Ok(deserred) + } else { + let json = response.json().await; + json.map_err(CynicReqwestError::ReqwestError) + } +} + +impl ReqwestExt for reqwest::RequestBuilder { + fn run_fuel_graphql( + self, + operation: FuelOperation>, + ) -> CynicReqwestBuilder + where + Vars: serde::Serialize, + ResponseData: serde::de::DeserializeOwned + 'static, + { + CynicReqwestBuilder::new(self.json(&operation)) + } +} diff --git a/crates/fuel-core/src/graphql_api.rs b/crates/fuel-core/src/graphql_api.rs index e9d069bff9a..aa4a419c53b 100644 --- a/crates/fuel-core/src/graphql_api.rs +++ b/crates/fuel-core/src/graphql_api.rs @@ -14,6 +14,7 @@ pub mod database; pub(crate) mod indexation; pub(crate) mod metrics_extension; pub mod ports; +pub(crate) mod required_fuel_block_height_extension; pub mod storage; pub(crate) mod validation_extension; pub(crate) mod view_extension; diff --git a/crates/fuel-core/src/graphql_api/api_service.rs b/crates/fuel-core/src/graphql_api/api_service.rs index f8baa21e58e..9552dd78af1 100644 --- a/crates/fuel-core/src/graphql_api/api_service.rs +++ b/crates/fuel-core/src/graphql_api/api_service.rs @@ -15,7 +15,10 @@ use crate::{ view_extension::ViewExtension, Config, }, - graphql_api, + graphql_api::{ + self, + required_fuel_block_height_extension::RequiredFuelBlockHeightExtension, + }, schema::{ CoreSchema, CoreSchemaBuilder, @@ -281,6 +284,9 @@ where )) .extension(async_graphql::extensions::Tracing) .extension(ViewExtension::new()) + // `RequiredFuelBlockHeightExtension` uses the view set by the ViewExtension. + // Do not reorder this line before adding the `ViewExtension`. + .extension(RequiredFuelBlockHeightExtension::new()) .finish(); let graphql_endpoint = "/v1/graphql"; diff --git a/crates/fuel-core/src/graphql_api/required_fuel_block_height_extension.rs b/crates/fuel-core/src/graphql_api/required_fuel_block_height_extension.rs new file mode 100644 index 00000000000..441c4838b6c --- /dev/null +++ b/crates/fuel-core/src/graphql_api/required_fuel_block_height_extension.rs @@ -0,0 +1,167 @@ +use super::database::ReadView; + +use crate::fuel_core_graphql_api::database::ReadDatabase; +use async_graphql::{ + extensions::{ + Extension, + ExtensionContext, + ExtensionFactory, + NextExecute, + NextPrepareRequest, + }, + Pos, + Request, + Response, + ServerError, + ServerResult, + Value, +}; +use async_graphql_value::ConstValue; +use fuel_core_types::fuel_types::BlockHeight; +use std::sync::{ + Arc, + OnceLock, +}; + +const REQUIRED_FUEL_BLOCK_HEIGHT: &str = "required_fuel_block_height"; +const CURRENT_FUEL_BLOCK_HEIGHT: &str = "current_fuel_block_height"; +const FUEL_BLOCK_HEIGHT_PRECONDITION_FAILED: &str = + "fuel_block_height_precondition_failed"; + +/// The extension that implements the logic for checking whether +/// the precondition that REQUIRED_FUEL_BLOCK_HEADER must +/// be higher than the current block height is met. +/// The value of the REQUIRED_FUEL_BLOCK_HEADER is set in +/// the request data by the graphql handler as a value of type +/// `RequiredHeight`. +#[derive(Debug, derive_more::Display, derive_more::From)] +pub(crate) struct RequiredFuelBlockHeightExtension; + +impl RequiredFuelBlockHeightExtension { + pub fn new() -> Self { + Self + } +} + +pub(crate) struct RequiredFuelBlockHeightInner { + required_height: OnceLock, +} + +impl RequiredFuelBlockHeightInner { + pub fn new() -> Self { + Self { + required_height: OnceLock::new(), + } + } +} + +impl ExtensionFactory for RequiredFuelBlockHeightExtension { + fn create(&self) -> Arc { + Arc::new(RequiredFuelBlockHeightInner::new()) + } +} + +#[async_trait::async_trait] +impl Extension for RequiredFuelBlockHeightInner { + async fn prepare_request( + &self, + ctx: &ExtensionContext<'_>, + request: Request, + next: NextPrepareRequest<'_>, + ) -> ServerResult { + let required_fuel_block_height = + request.extensions.get(REQUIRED_FUEL_BLOCK_HEIGHT); + + if let Some(ConstValue::Number(required_fuel_block_height)) = + required_fuel_block_height + { + if let Some(required_fuel_block_height) = required_fuel_block_height.as_u64() + { + let required_fuel_block_height: u32 = + required_fuel_block_height.try_into().unwrap_or(u32::MAX); + let required_block_height: BlockHeight = + required_fuel_block_height.into(); + self.required_height + .set(required_block_height) + .expect("`prepare_request` called only once; qed"); + } + } + + next.run(ctx, request).await + } + + async fn execute( + &self, + ctx: &ExtensionContext<'_>, + operation_name: Option<&str>, + next: NextExecute<'_>, + ) -> Response { + let view: &ReadView = ctx.data_unchecked(); + + let current_block_height = view.latest_block_height(); + + if let Some(required_block_height) = self.required_height.get() { + if let Ok(current_block_height) = current_block_height { + if *required_block_height > current_block_height { + let (line, column) = (line!(), column!()); + let mut response = Response::from_errors(vec![ServerError::new( + format!( + "The required fuel block height is higher than the current block height. \ + Required: {}, Current: {}", + // required_block_height: &BlockHeight, dereference twice to get the + // corresponding value as u32. This is necessary because the Display + // implementation for BlockHeight displays values in hexadecimal format. + **required_block_height, + // current_fuel_block_height: BlockHeight, dereference once to get the + // corresponding value as u32. + *current_block_height + ), + Some(Pos { + line: line as usize, + column: column as usize, + }), + )]); + + response.extensions.insert( + CURRENT_FUEL_BLOCK_HEIGHT.to_string(), + Value::Number((*current_block_height).into()), + ); + response.extensions.insert( + FUEL_BLOCK_HEIGHT_PRECONDITION_FAILED.to_string(), + Value::Boolean(true), + ); + + return response + } + } + } + + let mut response = next.run(ctx, operation_name).await; + + // TODO: After https://github.com/FuelLabs/fuel-core/pull/2682 + // request the latest block height from the `ReadDatabase` directly. + let database: &ReadDatabase = ctx.data_unchecked(); + let view = database.view(); + let current_block_height = view.and_then(|view| view.latest_block_height()); + + if let Ok(current_block_height) = current_block_height { + let current_block_height: u32 = *current_block_height; + response.extensions.insert( + CURRENT_FUEL_BLOCK_HEIGHT.to_string(), + Value::Number(current_block_height.into()), + ); + // If the request contained a required fuel block height, add a field signalling that + // the precondition was met. + if self.required_height.get().is_some() { + response.extensions.insert( + FUEL_BLOCK_HEIGHT_PRECONDITION_FAILED.to_string(), + Value::Boolean(false), + ); + } + } else { + tracing::error!("Failed to get the current block height"); + } + + response + } +} diff --git a/tests/tests/blob.rs b/tests/tests/blob.rs index 74a1a7d13aa..d158eccaf0b 100644 --- a/tests/tests/blob.rs +++ b/tests/tests/blob.rs @@ -179,7 +179,11 @@ async fn blob__cannot_post_already_existing_blob() { // Then let err = result.expect_err("Should fail because of the same blob id"); - assert!(err.to_string().contains("BlobId is already taken")); + assert!( + err.to_string().contains("BlobId is already taken"), + "{}", + err + ); } #[tokio::test] diff --git a/tests/tests/lib.rs b/tests/tests/lib.rs index afedaac7f37..1d12415e8be 100644 --- a/tests/tests/lib.rs +++ b/tests/tests/lib.rs @@ -50,6 +50,8 @@ mod regenesis; #[cfg(not(feature = "only-p2p"))] mod relayer; #[cfg(not(feature = "only-p2p"))] +mod required_fuel_block_height_extension; +#[cfg(not(feature = "only-p2p"))] mod snapshot; #[cfg(not(feature = "only-p2p"))] mod state_rewind; diff --git a/tests/tests/required_fuel_block_height_extension.rs b/tests/tests/required_fuel_block_height_extension.rs new file mode 100644 index 00000000000..fe950a38d65 --- /dev/null +++ b/tests/tests/required_fuel_block_height_extension.rs @@ -0,0 +1,138 @@ +use fuel_core::{ + chain_config::StateConfig, + service::{ + Config, + FuelService, + }, +}; +use fuel_core_client::client::{ + types::primitives::{ + Address, + AssetId, + }, + FuelClient, +}; +use fuel_core_types::fuel_tx; + +#[tokio::test] +async fn request_with_required_block_height_extension_field_works() { + let owner = Address::default(); + let asset_id = AssetId::BASE; + + // setup config + let state_config = StateConfig::default(); + let config = Config::local_node_with_state_config(state_config); + + // setup server & client + let srv = FuelService::new_node(config).await.unwrap(); + let mut client: FuelClient = FuelClient::from(srv.bound_address); + + client.with_required_fuel_block_height(Some(100u32.into())); + // Issue a request with wrong precondition + let error = client.balance(&owner, Some(&asset_id)).await.unwrap_err(); + + assert!( + error + .to_string() + .contains("The required block height was not met"), + "Error: {}", + error + ); + + // Disable extension metadata, otherwise the request fails + client.with_required_fuel_block_height(None); + + // Meet precondition on server side + client.produce_blocks(100, None).await.unwrap(); + + // Set the header and issue request again + client.with_required_fuel_block_height(Some(100u32.into())); + let result = client.balance(&owner, Some(&asset_id)).await; + + assert!(result.is_ok()); +} + +#[tokio::test] +async fn current_fuel_block_height_extension_fields_are_present_on_failed_request() { + // setup config + let state_config = StateConfig::default(); + let mut config = Config::local_node_with_state_config(state_config); + // It will cause request to fail. + config.debug = false; + + // setup server & client + let srv = FuelService::new_node(config).await.unwrap(); + let mut client: FuelClient = FuelClient::from(srv.bound_address); + + // When + client.with_required_fuel_block_height(None); + let result = client.produce_blocks(50, None).await; + + // Then + assert!(result.is_err()); + assert_eq!(client.required_block_height(), Some(0u32.into())); +} + +#[tokio::test] +async fn current_fuel_block_height_header_is_present_on_successful_request() { + // setup config + let state_config = StateConfig::default(); + let config = Config::local_node_with_state_config(state_config); + + // setup server & client + let srv = FuelService::new_node(config).await.unwrap(); + let mut client: FuelClient = FuelClient::from(srv.bound_address); + + // When + client.with_required_fuel_block_height(None); + client.produce_blocks(50, None).await.unwrap(); + + // Then + assert_eq!(client.required_block_height(), Some(50u32.into())); +} + +#[tokio::test] +async fn current_fuel_block_height_header_is_present_on_successful_committed_transaction() +{ + // setup config + let state_config = StateConfig::default(); + let config = Config::local_node_with_state_config(state_config); + + // setup server & client + let srv = FuelService::new_node(config).await.unwrap(); + let mut client: FuelClient = FuelClient::from(srv.bound_address); + + // When + client.with_required_fuel_block_height(None); + let tx = fuel_tx::Transaction::default_test_tx(); + client.submit_and_await_commit(&tx).await.unwrap(); + + // Then + assert_eq!(client.required_block_height(), Some(1u32.into())); +} + +#[tokio::test] +async fn submitting_transaction_with_future_required_height_return_error() { + // setup config + let state_config = StateConfig::default(); + let config = Config::local_node_with_state_config(state_config); + + // setup server & client + let srv = FuelService::new_node(config).await.unwrap(); + let mut client: FuelClient = FuelClient::from(srv.bound_address); + + // When + client.with_required_fuel_block_height(Some(100u32.into())); + let tx = fuel_tx::Transaction::default_test_tx(); + let result = client.submit_and_await_commit(&tx).await; + + // Then + let error = result.unwrap_err(); + assert!( + error + .to_string() + .contains("The required block height was not met"), + "Error: {}", + error + ); +} diff --git a/version-compatibility/forkless-upgrade/src/backward_compatibility.rs b/version-compatibility/forkless-upgrade/src/backward_compatibility.rs index cbec490a41f..1dab88af3cf 100644 --- a/version-compatibility/forkless-upgrade/src/backward_compatibility.rs +++ b/version-compatibility/forkless-upgrade/src/backward_compatibility.rs @@ -1,11 +1,14 @@ -use crate::tests_helper::{ - default_multiaddr, - GenesisFuelCoreDriver, - LatestFuelCoreDriver, - Version36FuelCoreDriver, - IGNITION_TESTNET_SNAPSHOT, - POA_SECRET_KEY, - V36_TESTNET_SNAPSHOT, +use crate::{ + select_port, + tests_helper::{ + default_multiaddr, + GenesisFuelCoreDriver, + LatestFuelCoreDriver, + Version36FuelCoreDriver, + IGNITION_TESTNET_SNAPSHOT, + POA_SECRET_KEY, + V36_TESTNET_SNAPSHOT, + }, }; use latest_fuel_core_type::{ fuel_tx::Transaction, @@ -56,7 +59,7 @@ async fn latest_binary_is_backward_compatible_and_follows_blocks_created_by_gene // Given let genesis_keypair = SecpKeypair::generate(); let hexed_secret = hex::encode(genesis_keypair.secret().to_bytes()); - let genesis_port = "30333"; + let genesis_port = select_port(format!("{}:{}.{}", file!(), line!(), column!())); let genesis_node = GenesisFuelCoreDriver::spawn(&[ "--service-name", "GenesisProducer", @@ -125,7 +128,7 @@ async fn latest_binary_is_backward_compatible_and_follows_blocks_created_by_v36_ // Given let v36_keypair = SecpKeypair::generate(); let hexed_secret = hex::encode(v36_keypair.secret().to_bytes()); - let v36_port = "30334"; + let v36_port = select_port(format!("{}:{}.{}", file!(), line!(), column!())); let v36_node = Version36FuelCoreDriver::spawn(&[ "--service-name", "V36Producer", @@ -195,7 +198,7 @@ async fn latest_binary_is_backward_compatible_and_can_deserialize_errors_from_ge // Given let genesis_keypair = SecpKeypair::generate(); let hexed_secret = hex::encode(genesis_keypair.secret().to_bytes()); - let genesis_port = "30335"; + let genesis_port = select_port(format!("{}:{}.{}", file!(), line!(), column!())); let node_with_genesis_transition = LatestFuelCoreDriver::spawn(&[ "--service-name", "GenesisProducer", diff --git a/version-compatibility/forkless-upgrade/src/forward_compatibility.rs b/version-compatibility/forkless-upgrade/src/forward_compatibility.rs index 3523468f430..3dcd8420ecb 100644 --- a/version-compatibility/forkless-upgrade/src/forward_compatibility.rs +++ b/version-compatibility/forkless-upgrade/src/forward_compatibility.rs @@ -2,14 +2,17 @@ //! we need to remove old tests(usually, we need to create a new test per each release) //! and write a new test(only one) to track new forward compatibility. -use crate::tests_helper::{ - default_multiaddr, - transactions_from_subsections, - upgrade_transaction, - Version36FuelCoreDriver, - IGNITION_TESTNET_SNAPSHOT, - POA_SECRET_KEY, - SUBSECTION_SIZE, +use crate::{ + select_port, + tests_helper::{ + default_multiaddr, + transactions_from_subsections, + upgrade_transaction, + Version36FuelCoreDriver, + IGNITION_TESTNET_SNAPSHOT, + POA_SECRET_KEY, + SUBSECTION_SIZE, + }, }; use fuel_tx::{ field::ChargeableBody, @@ -45,7 +48,7 @@ async fn latest_state_transition_function_is_forward_compatible_with_v36_binary( let v36_keypair = SecpKeypair::generate(); let hexed_secret = hex::encode(v36_keypair.secret().to_bytes()); - let v36_port = "40333"; + let v36_port = select_port(format!("{}:{}.{}", file!(), line!(), column!())); let v36_node = Version36FuelCoreDriver::spawn(&[ "--service-name", "V36Producer", diff --git a/version-compatibility/forkless-upgrade/src/gas_price_algo_compatibility.rs b/version-compatibility/forkless-upgrade/src/gas_price_algo_compatibility.rs index 6f35cdc78ca..d1445c73c00 100644 --- a/version-compatibility/forkless-upgrade/src/gas_price_algo_compatibility.rs +++ b/version-compatibility/forkless-upgrade/src/gas_price_algo_compatibility.rs @@ -1,11 +1,14 @@ #![allow(unused_imports)] -use crate::tests_helper::{ - default_multiaddr, - LatestFuelCoreDriver, - Version36FuelCoreDriver, - IGNITION_TESTNET_SNAPSHOT, - POA_SECRET_KEY, +use crate::{ + select_port, + tests_helper::{ + default_multiaddr, + LatestFuelCoreDriver, + Version36FuelCoreDriver, + IGNITION_TESTNET_SNAPSHOT, + POA_SECRET_KEY, + }, }; use latest_fuel_core_gas_price_service::{ common::{ @@ -48,7 +51,7 @@ async fn v1_gas_price_metadata_updates_successfully_from_v0() { // Given let genesis_keypair = SecpKeypair::generate(); let hexed_secret = hex::encode(genesis_keypair.secret().to_bytes()); - let genesis_port = "30333"; + let genesis_port = select_port(format!("{}:{}.{}", file!(), line!(), column!())); let starting_gas_price = 987; let old_driver = Version36FuelCoreDriver::spawn(&[ "--service-name", diff --git a/version-compatibility/forkless-upgrade/src/lib.rs b/version-compatibility/forkless-upgrade/src/lib.rs index f2170f0043b..65e3ffaf1e9 100644 --- a/version-compatibility/forkless-upgrade/src/lib.rs +++ b/version-compatibility/forkless-upgrade/src/lib.rs @@ -16,3 +16,33 @@ pub(crate) mod tests_helper; #[cfg(test)] fuel_core_trace::enable_tracing!(); + +#[cfg(test)] +/// Old versions of the `fuel-core` doesn't expose the port used by the P2P. +/// So, if we use `--peering-port 0`, we don't know what port is assign to which node. +/// Some tests require to know the port to use the node as a bootstrap(or reserved node). +/// This function generates a port based on the seed, where seed based on the file, line, +/// and column. While it is possible that we will generate the same port for several tests, +/// it is done at least in a deterministic way, so we should be able to reproduce +/// the error locally. +fn select_port(seed: String) -> &'static str { + use rand::{ + rngs::StdRng, + Rng, + SeedableRng, + }; + use std::hash::{ + DefaultHasher, + Hash, + Hasher, + }; + + let mut hasher = DefaultHasher::new(); + seed.hash(&mut hasher); + let hash = hasher.finish(); + + let mut rng = StdRng::seed_from_u64(hash); + let port: u16 = rng.gen_range(500..65000); + + port.to_string().leak() +}