diff --git a/CHANGELOG.md b/CHANGELOG.md index e174e30ef05..b2cf0978023 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,7 @@ Description of the upcoming release here. #### Breaking - [#1536](https://github.com/FuelLabs/fuel-core/pull/1536): The change fixes the contracts tables to not touch SMT nodes of foreign contracts. Before, it was possible to invalidate the SMT from another contract. It is a breaking change and requires re-calculating the whole state from the beginning with new SMT roots. - +- [#1542](https://github.com/FuelLabs/fuel-core/pull/1542): Migrates information about peers to NodeInfo instead of ChainInfo. It also elides information about peers in the default node_info query. ## [Version 0.21.0] diff --git a/crates/client/assets/schema.sdl b/crates/client/assets/schema.sdl index b428252d41c..9f65647b0cd 100644 --- a/crates/client/assets/schema.sdl +++ b/crates/client/assets/schema.sdl @@ -94,7 +94,6 @@ type ChainInfo { name: String! latestBlock: Block! daHeight: U64! - peers: [PeerInfo!]! consensusParameters: ConsensusParameters! gasCosts: GasCosts! } @@ -613,6 +612,7 @@ type NodeInfo { maxTx: U64! maxDepth: U64! nodeVersion: String! + peers: [PeerInfo!]! } scalar Nonce diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 62fa43cc57b..61e1d79e6df 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -49,6 +49,7 @@ use fuel_core_types::{ BlockHeight, Nonce, }, + services::p2p::PeerInfo, }; #[cfg(feature = "subscriptions")] use futures::StreamExt; @@ -341,6 +342,13 @@ impl FuelClient { self.query(query).await.map(|r| r.node_info.into()) } + pub async fn connected_peers_info(&self) -> io::Result> { + let query = schema::node_info::QueryPeersInfo::build(()); + self.query(query) + .await + .map(|r| r.node_info.peers.into_iter().map(Into::into).collect()) + } + pub async fn chain_info(&self) -> io::Result { let query = schema::chain::ChainQuery::build(()); self.query(query).await.map(|r| r.chain.into()) diff --git a/crates/client/src/client/schema/chain.rs b/crates/client/src/client/schema/chain.rs index 12ca0b7415f..3c3bacf8c2f 100644 --- a/crates/client/src/client/schema/chain.rs +++ b/crates/client/src/client/schema/chain.rs @@ -322,22 +322,10 @@ pub struct ChainQuery { pub struct ChainInfo { pub da_height: U64, pub name: String, - pub peers: Vec, pub latest_block: Block, pub consensus_parameters: ConsensusParameters, } -#[derive(cynic::QueryFragment, Debug)] -#[cynic(schema_path = "./assets/schema.sdl", graphql_type = "PeerInfo")] -pub struct PeerInfo { - pub id: String, - pub addresses: Vec, - pub client_version: Option, - pub block_height: Option, - pub last_heartbeat_ms: U64, - pub app_score: f64, -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/client/src/client/schema/node_info.rs b/crates/client/src/client/schema/node_info.rs index b18744b4a91..a0433e8f85e 100644 --- a/crates/client/src/client/schema/node_info.rs +++ b/crates/client/src/client/schema/node_info.rs @@ -1,7 +1,19 @@ use crate::client::schema::{ schema, + U32, U64, }; +use fuel_core_types::services::p2p::{ + HeartbeatData, + PeerId, +}; +use std::{ + str::FromStr, + time::{ + Duration, + UNIX_EPOCH, + }, +}; #[derive(cynic::QueryFragment, Debug)] #[cynic(schema_path = "./assets/schema.sdl")] @@ -20,6 +32,49 @@ pub struct QueryNodeInfo { pub node_info: NodeInfo, } +// Use a separate GQL query for showing peer info, as the endpoint is bulky and may return an error +// if the `p2p` feature is disabled. + +#[derive(cynic::QueryFragment, Debug)] +#[cynic(schema_path = "./assets/schema.sdl", graphql_type = "NodeInfo")] +pub struct PeersInfo { + pub peers: Vec, +} + +#[derive(cynic::QueryFragment, Debug)] +#[cynic(schema_path = "./assets/schema.sdl", graphql_type = "Query")] +pub struct QueryPeersInfo { + pub node_info: PeersInfo, +} + +#[derive(cynic::QueryFragment, Debug)] +#[cynic(schema_path = "./assets/schema.sdl")] +pub struct PeerInfo { + pub id: String, + pub addresses: Vec, + pub client_version: Option, + pub block_height: Option, + pub last_heartbeat_ms: U64, + pub app_score: f64, +} + +impl From for fuel_core_types::services::p2p::PeerInfo { + fn from(info: PeerInfo) -> Self { + Self { + id: PeerId::from_str(info.id.as_str()).unwrap_or_default(), + peer_addresses: info.addresses.into_iter().collect(), + client_version: info.client_version, + heartbeat_data: HeartbeatData { + block_height: info.block_height.map(|h| h.0.into()), + last_heartbeat: UNIX_EPOCH + .checked_add(Duration::from_millis(info.last_heartbeat_ms.0)) + .unwrap_or(UNIX_EPOCH), + }, + app_score: info.app_score, + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -30,4 +85,11 @@ mod tests { let operation = QueryNodeInfo::build(()); insta::assert_snapshot!(operation.query) } + + #[test] + fn peers_info_query_gql_output() { + use cynic::QueryBuilder; + let operation = QueryPeersInfo::build(()); + insta::assert_snapshot!(operation.query) + } } diff --git a/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__chain__tests__chain_gql_query_output.snap b/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__chain__tests__chain_gql_query_output.snap index df06e076049..77256901502 100644 --- a/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__chain__tests__chain_gql_query_output.snap +++ b/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__chain__tests__chain_gql_query_output.snap @@ -6,14 +6,6 @@ query { chain { daHeight name - peers { - id - addresses - clientVersion - blockHeight - lastHeartbeatMs - appScore - } latestBlock { id header { diff --git a/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__node_info__tests__peers_info_query_gql_output.snap b/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__node_info__tests__peers_info_query_gql_output.snap new file mode 100644 index 00000000000..3322f6ca26b --- /dev/null +++ b/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__node_info__tests__peers_info_query_gql_output.snap @@ -0,0 +1,18 @@ +--- +source: crates/client/src/client/schema/node_info.rs +expression: operation.query +--- +query { + nodeInfo { + peers { + id + addresses + clientVersion + blockHeight + lastHeartbeatMs + appScore + } + } +} + + diff --git a/crates/client/src/client/types/chain_info.rs b/crates/client/src/client/types/chain_info.rs index 81ab508b795..2bc63625761 100644 --- a/crates/client/src/client/types/chain_info.rs +++ b/crates/client/src/client/types/chain_info.rs @@ -3,25 +3,13 @@ use crate::client::{ types::Block, }; use fuel_core_types::{ + self, fuel_tx::ConsensusParameters, - services::p2p::{ - HeartbeatData, - PeerId, - PeerInfo, - }, -}; -use std::{ - str::FromStr, - time::{ - Duration, - UNIX_EPOCH, - }, }; pub struct ChainInfo { pub da_height: u64, pub name: String, - pub peers: Vec, pub latest_block: Block, pub consensus_parameters: ConsensusParameters, } @@ -33,26 +21,8 @@ impl From for ChainInfo { Self { da_height: value.da_height.into(), name: value.name, - peers: value.peers.into_iter().map(|info| info.into()).collect(), latest_block: value.latest_block.into(), consensus_parameters: value.consensus_parameters.into(), } } } - -impl From for PeerInfo { - fn from(info: schema::chain::PeerInfo) -> Self { - Self { - id: PeerId::from_str(info.id.as_str()).unwrap_or_default(), - peer_addresses: info.addresses.into_iter().collect(), - client_version: info.client_version, - heartbeat_data: HeartbeatData { - block_height: info.block_height.map(|h| h.0.into()), - last_heartbeat: UNIX_EPOCH - .checked_add(Duration::from_millis(info.last_heartbeat_ms.0)) - .unwrap_or(UNIX_EPOCH), - }, - app_score: info.app_score, - } - } -} diff --git a/crates/fuel-core/src/schema/chain.rs b/crates/fuel-core/src/schema/chain.rs index 82198a62d92..e1df56c7eb2 100644 --- a/crates/fuel-core/src/schema/chain.rs +++ b/crates/fuel-core/src/schema/chain.rs @@ -23,7 +23,6 @@ use async_graphql::{ Union, }; use fuel_core_types::fuel_tx; -use std::time::UNIX_EPOCH; pub struct ChainInfo; pub struct ConsensusParameters(fuel_tx::ConsensusParameters); @@ -659,49 +658,6 @@ impl GasCosts { } } -struct PeerInfo(fuel_core_types::services::p2p::PeerInfo); - -#[Object] -impl PeerInfo { - /// The libp2p peer id - async fn id(&self) -> String { - self.0.id.to_string() - } - - /// The advertised multi-addrs that can be used to connect to this peer - async fn addresses(&self) -> Vec { - self.0.peer_addresses.iter().cloned().collect() - } - - /// The self-reported version of the client the peer is using - async fn client_version(&self) -> Option { - self.0.client_version.clone() - } - - /// The last reported height of the peer - async fn block_height(&self) -> Option { - self.0 - .heartbeat_data - .block_height - .map(|height| (*height).into()) - } - - /// The last heartbeat from this peer in unix epoch time ms - async fn last_heartbeat_ms(&self) -> U64 { - let time = self.0.heartbeat_data.last_heartbeat; - let time = time - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - U64(time.try_into().unwrap_or_default()) - } - - /// The internal fuel p2p reputation of this peer - async fn app_score(&self) -> f64 { - self.0.app_score - } -} - #[Object] impl LightOperation { async fn base(&self) -> U64 { @@ -748,23 +704,6 @@ impl ChainInfo { height.0.into() } - async fn peers(&self, _ctx: &Context<'_>) -> anyhow::Result> { - #[cfg(feature = "p2p")] - { - let p2p: &crate::fuel_core_graphql_api::service::P2pService = - _ctx.data_unchecked(); - let peer_info = p2p.all_peer_info().await?; - let peers = peer_info.into_iter().map(PeerInfo).collect(); - Ok(peers) - } - #[cfg(not(feature = "p2p"))] - { - Err(anyhow::anyhow!( - "Peering is disabled in this build, try using the `p2p` feature flag." - )) - } - } - async fn consensus_parameters( &self, ctx: &Context<'_>, diff --git a/crates/fuel-core/src/schema/node_info.rs b/crates/fuel-core/src/schema/node_info.rs index defb1f846fb..97ef85167c0 100644 --- a/crates/fuel-core/src/schema/node_info.rs +++ b/crates/fuel-core/src/schema/node_info.rs @@ -1,9 +1,13 @@ -use super::scalars::U64; +use super::scalars::{ + U32, + U64, +}; use crate::fuel_core_graphql_api::Config as GraphQLConfig; use async_graphql::{ Context, Object, }; +use std::time::UNIX_EPOCH; pub struct NodeInfo { utxo_validation: bool, @@ -39,6 +43,23 @@ impl NodeInfo { async fn node_version(&self) -> String { self.node_version.to_owned() } + + async fn peers(&self, _ctx: &Context<'_>) -> async_graphql::Result> { + #[cfg(feature = "p2p")] + { + let p2p: &crate::fuel_core_graphql_api::service::P2pService = + _ctx.data_unchecked(); + let peer_info = p2p.all_peer_info().await?; + let peers = peer_info.into_iter().map(PeerInfo).collect(); + Ok(peers) + } + #[cfg(not(feature = "p2p"))] + { + Err(async_graphql::Error::new( + "Peering is disabled in this build, try using the `p2p` feature flag.", + )) + } + } } #[derive(Default)] @@ -61,3 +82,46 @@ impl NodeQuery { }) } } + +struct PeerInfo(fuel_core_types::services::p2p::PeerInfo); + +#[Object] +impl PeerInfo { + /// The libp2p peer id + async fn id(&self) -> String { + self.0.id.to_string() + } + + /// The advertised multi-addrs that can be used to connect to this peer + async fn addresses(&self) -> Vec { + self.0.peer_addresses.iter().cloned().collect() + } + + /// The self-reported version of the client the peer is using + async fn client_version(&self) -> Option { + self.0.client_version.clone() + } + + /// The last reported height of the peer + async fn block_height(&self) -> Option { + self.0 + .heartbeat_data + .block_height + .map(|height| (*height).into()) + } + + /// The last heartbeat from this peer in unix epoch time ms + async fn last_heartbeat_ms(&self) -> U64 { + let time = self.0.heartbeat_data.last_heartbeat; + let time = time + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + U64(time.try_into().unwrap_or_default()) + } + + /// The internal fuel p2p reputation of this peer + async fn app_score(&self) -> f64 { + self.0.app_score + } +} diff --git a/tests/tests/chain.rs b/tests/tests/chain.rs index 7fa52eee66f..a068b27338e 100644 --- a/tests/tests/chain.rs +++ b/tests/tests/chain.rs @@ -24,80 +24,3 @@ async fn chain_info() { chain_info.consensus_parameters.gas_costs ); } - -#[cfg(feature = "p2p")] -#[tokio::test(flavor = "multi_thread")] -async fn test_peer_info() { - use fuel_core::p2p_test_helpers::{ - make_nodes, - BootstrapSetup, - Nodes, - ProducerSetup, - ValidatorSetup, - }; - use fuel_core_types::{ - fuel_tx::Input, - fuel_vm::SecretKey, - }; - use rand::{ - rngs::StdRng, - SeedableRng, - }; - use std::time::Duration; - - let mut rng = StdRng::seed_from_u64(line!() as u64); - - // Create a producer and a validator that share the same key pair. - let secret = SecretKey::random(&mut rng); - let pub_key = Input::owner(&secret.public_key()); - let Nodes { - mut producers, - mut validators, - bootstrap_nodes: _dont_drop, - } = make_nodes( - [Some(BootstrapSetup::new(pub_key))], - [Some( - ProducerSetup::new(secret).with_txs(1).with_name("Alice"), - )], - [Some(ValidatorSetup::new(pub_key).with_name("Bob"))], - None, - ) - .await; - - let producer = producers.pop().unwrap(); - let mut validator = validators.pop().unwrap(); - - // Insert the transactions into the tx pool and await them, - // to ensure we have a live p2p connection. - let expected = producer.insert_txs().await; - - // Wait up to 10 seconds for the validator to sync with the producer. - // This indicates we have a successful P2P connection. - validator.consistency_10s(&expected).await; - - let validator_peer_id = validator - .node - .shared - .config - .p2p - .unwrap() - .keypair - .public() - .to_peer_id(); - - // TODO: this needs to fetch peers from the GQL API, not the service directly. - // This is just a mock of what we should be able to do with GQL API. - let client = producer.node.bound_address; - let client = FuelClient::from(client); - let peers = client.chain_info().await.unwrap().peers; - assert_eq!(peers.len(), 2); - let info = peers - .iter() - .find(|info| info.id.to_string() == validator_peer_id.to_base58()) - .expect("Should be connected to validator"); - - let time_since_heartbeat = std::time::SystemTime::now() - .duration_since(info.heartbeat_data.last_heartbeat) - .unwrap(); - assert!(time_since_heartbeat < Duration::from_secs(10)); -} diff --git a/tests/tests/node_info.rs b/tests/tests/node_info.rs index d8deedcf9ef..5876be45ade 100644 --- a/tests/tests/node_info.rs +++ b/tests/tests/node_info.rs @@ -28,3 +28,80 @@ async fn node_info() { assert_eq!(max_depth, node_config.txpool.max_depth as u64); assert_eq!(max_tx, node_config.txpool.max_tx as u64); } + +#[cfg(feature = "p2p")] +#[tokio::test(flavor = "multi_thread")] +async fn test_peer_info() { + use fuel_core::p2p_test_helpers::{ + make_nodes, + BootstrapSetup, + Nodes, + ProducerSetup, + ValidatorSetup, + }; + use fuel_core_types::{ + fuel_tx::Input, + fuel_vm::SecretKey, + }; + use rand::{ + rngs::StdRng, + SeedableRng, + }; + use std::time::Duration; + + let mut rng = StdRng::seed_from_u64(line!() as u64); + + // Create a producer and a validator that share the same key pair. + let secret = SecretKey::random(&mut rng); + let pub_key = Input::owner(&secret.public_key()); + let Nodes { + mut producers, + mut validators, + bootstrap_nodes: _dont_drop, + } = make_nodes( + [Some(BootstrapSetup::new(pub_key))], + [Some( + ProducerSetup::new(secret).with_txs(1).with_name("Alice"), + )], + [Some(ValidatorSetup::new(pub_key).with_name("Bob"))], + None, + ) + .await; + + let producer = producers.pop().unwrap(); + let mut validator = validators.pop().unwrap(); + + // Insert the transactions into the tx pool and await them, + // to ensure we have a live p2p connection. + let expected = producer.insert_txs().await; + + // Wait up to 10 seconds for the validator to sync with the producer. + // This indicates we have a successful P2P connection. + validator.consistency_10s(&expected).await; + + let validator_peer_id = validator + .node + .shared + .config + .p2p + .unwrap() + .keypair + .public() + .to_peer_id(); + + // TODO: this needs to fetch peers from the GQL API, not the service directly. + // This is just a mock of what we should be able to do with GQL API. + let client = producer.node.bound_address; + let client = FuelClient::from(client); + let peers = client.connected_peers_info().await.unwrap(); + assert_eq!(peers.len(), 2); + let info = peers + .iter() + .find(|info| info.id.to_string() == validator_peer_id.to_base58()) + .expect("Should be connected to validator"); + + let time_since_heartbeat = std::time::SystemTime::now() + .duration_since(info.heartbeat_data.last_heartbeat) + .unwrap(); + assert!(time_since_heartbeat < Duration::from_secs(10)); +}