From 2f403897fca8d890183a16fae2699efeddf8378a Mon Sep 17 00:00:00 2001 From: Arsenii Lyashenko Date: Wed, 13 May 2026 19:38:53 +0300 Subject: [PATCH 1/8] Initial db-sync removal --- Cargo.lock | 137 ++- ethexe/network/Cargo.toml | 6 +- ethexe/network/src/bitswap.rs | 317 ++++++ ethexe/network/src/db_sync/mod.rs | 1237 --------------------- ethexe/network/src/db_sync/requests.rs | 1101 ------------------ ethexe/network/src/db_sync/responses.rs | 552 --------- ethexe/network/src/gossipsub.rs | 6 +- ethexe/network/src/injected.rs | 8 +- ethexe/network/src/lib.rs | 172 +-- ethexe/network/src/peer_score.rs | 28 +- ethexe/network/src/utils.rs | 38 +- ethexe/network/src/validator/discovery.rs | 3 +- ethexe/network/src/validator/topic.rs | 6 +- ethexe/service/src/fast_sync.rs | 2 +- ethexe/service/src/lib.rs | 60 +- 15 files changed, 497 insertions(+), 3176 deletions(-) create mode 100644 ethexe/network/src/bitswap.rs delete mode 100644 ethexe/network/src/db_sync/mod.rs delete mode 100644 ethexe/network/src/db_sync/requests.rs delete mode 100644 ethexe/network/src/db_sync/responses.rs diff --git a/Cargo.lock b/Cargo.lock index afd7b900b34..f3509512cee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1931,6 +1931,32 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "beetswap" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a920844a4696d53e72190a8348f0048a6799647b3448d38bef90b85e0310df0" +dependencies = [ + "asynchronous-codec 0.7.0", + "blockstore", + "bytes", + "cid 0.11.3", + "fnv", + "futures-core", + "futures-timer", + "futures-util", + "libp2p-core 0.43.2", + "libp2p-identity", + "libp2p-swarm 0.47.0", + "multihash-codetable", + "quick-protobuf", + "smallvec", + "thiserror 2.0.17", + "tracing", + "unsigned-varint 0.8.0", + "web-time", +] + [[package]] name = "bimap" version = "0.6.3" @@ -2174,6 +2200,18 @@ dependencies = [ "piper", ] +[[package]] +name = "blockstore" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509096e88e431095763b3f5ee1e1cdb09212e4d5b2eccc91ddea965deefedb7d" +dependencies = [ + "cid 0.11.3", + "dashmap 6.1.0", + "multihash 0.19.5", + "thiserror 2.0.17", +] + [[package]] name = "blst" version = "0.3.16" @@ -2718,6 +2756,17 @@ dependencies = [ "unsigned-varint 0.7.2", ] +[[package]] +name = "cid" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a304f95f84d169a6f31c4d0a30d784643aaa0bbc9c1e449a2c23e963ec4971" +dependencies = [ + "multibase", + "multihash 0.19.5", + "unsigned-varint 0.8.0", +] + [[package]] name = "cipher" version = "0.2.5" @@ -3688,7 +3737,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.114", ] [[package]] @@ -5337,6 +5386,9 @@ dependencies = [ "assert_matches", "async-trait", "auto_impl", + "beetswap", + "blockstore", + "cid 0.11.3", "derive_more 2.1.1", "ethexe-common", "ethexe-db", @@ -5355,6 +5407,7 @@ dependencies = [ "lru 0.16.3", "metrics", "metrics-derive", + "multihash 0.19.5", "nonempty 0.12.0", "parity-scale-codec", "prometheus-client 0.23.1", @@ -9865,7 +9918,7 @@ dependencies = [ "libp2p-identity", "log", "multiaddr 0.18.2", - "multihash 0.19.3", + "multihash 0.19.5", "multistream-select", "once_cell", "parking_lot 0.12.5", @@ -9891,7 +9944,7 @@ dependencies = [ "futures-timer", "libp2p-identity", "multiaddr 0.18.2", - "multihash 0.19.3", + "multihash 0.19.5", "multistream-select", "parking_lot 0.12.5", "pin-project", @@ -10022,7 +10075,7 @@ dependencies = [ "ed25519-dalek", "hkdf", "k256", - "multihash 0.19.3", + "multihash 0.19.5", "quick-protobuf", "rand 0.8.5", "serde", @@ -10177,7 +10230,7 @@ dependencies = [ "libp2p-identity", "log", "multiaddr 0.18.2", - "multihash 0.19.3", + "multihash 0.19.5", "once_cell", "quick-protobuf", "rand 0.8.5", @@ -11376,7 +11429,7 @@ dependencies = [ "data-encoding", "libp2p-identity", "multibase", - "multihash 0.19.3", + "multihash 0.19.5", "percent-encoding", "serde", "static_assertions", @@ -11407,7 +11460,7 @@ dependencies = [ "blake3", "core2", "digest 0.10.7", - "multihash-derive", + "multihash-derive 0.8.1", "sha2 0.10.9", "sha3", "unsigned-varint 0.7.2", @@ -11424,7 +11477,7 @@ dependencies = [ "blake3", "core2", "digest 0.10.7", - "multihash-derive", + "multihash-derive 0.8.1", "sha2 0.10.9", "sha3", "unsigned-varint 0.7.2", @@ -11432,14 +11485,32 @@ dependencies = [ [[package]] name = "multihash" -version = "0.19.3" +version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" +checksum = "577c63b00ad74d57e8c9aa870b5fccebf2fd64a308a5aee9f1bb88e4aea19447" dependencies = [ - "core2", "unsigned-varint 0.8.0", ] +[[package]] +name = "multihash-codetable" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67996849749d25f1da9f238e8ace2ece8f9d6bdf3f9750aaf2ae7de3a5cad8ea" +dependencies = [ + "blake2b_simd", + "blake2s_simd", + "blake3", + "core2", + "digest 0.10.7", + "multihash-derive 0.9.3", + "ripemd", + "sha1", + "sha2 0.10.9", + "sha3", + "strobe-rs", +] + [[package]] name = "multihash-derive" version = "0.8.1" @@ -11454,6 +11525,29 @@ dependencies = [ "synstructure 0.12.6", ] +[[package]] +name = "multihash-derive" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4723acdce756db211a53f01b3419dbdf63cb48cef5df86260f55309364735fbf" +dependencies = [ + "multihash 0.19.5", + "multihash-derive-impl", +] + +[[package]] +name = "multihash-derive-impl" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f932556f78452e5604cef711349d337ec081a9aa3c96e67b3127c8f5df05d550" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure 0.13.2", +] + [[package]] name = "multimap" version = "0.8.3" @@ -15360,7 +15454,7 @@ dependencies = [ "libp2p 0.52.4", "linked_hash_set", "log", - "multihash 0.19.3", + "multihash 0.19.5", "parity-scale-codec", "prost 0.12.6", "prost-build 0.12.6", @@ -16009,7 +16103,7 @@ dependencies = [ "litep2p", "log", "multiaddr 0.18.2", - "multihash 0.19.3", + "multihash 0.19.5", "rand 0.8.5", "thiserror 1.0.69", "zeroize", @@ -18263,6 +18357,19 @@ dependencies = [ "serde", ] +[[package]] +name = "strobe-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98fe17535ea31344936cc58d29fec9b500b0452ddc4cc24c429c8a921a0e84e5" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "keccak", + "subtle 2.6.1", + "zeroize", +] + [[package]] name = "strsim" version = "0.8.0" @@ -19623,9 +19730,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "ucd-trie" diff --git a/ethexe/network/Cargo.toml b/ethexe/network/Cargo.toml index 1e38e768d0f..f9e18effc68 100644 --- a/ethexe/network/Cargo.toml +++ b/ethexe/network/Cargo.toml @@ -21,9 +21,13 @@ ethexe-service-utils.workspace = true ethexe-common.workspace = true gprimitives = { workspace = true, features = ["std", "codec"] } -# libp2p +# libp2p & friends libp2p = { version = "0.56.0", features = ["mdns", "gossipsub", "kad", "identify", "ping", "secp256k1", "request-response", "quic", "tcp", "dns", "tls", "tokio", "macros", "plaintext", "yamux", "metrics"] } libp2p-gossipsub = { version = "0.49.4", features = ["metrics"] } +beetswap = "0.5.0" +blockstore = "0.8.0" +cid = "0.11.3" +multihash = "0.19.5" # other deps tokio = { workspace = true, features = ["macros", "sync"] } diff --git a/ethexe/network/src/bitswap.rs b/ethexe/network/src/bitswap.rs new file mode 100644 index 00000000000..f96302d3436 --- /dev/null +++ b/ethexe/network/src/bitswap.rs @@ -0,0 +1,317 @@ +// This file is part of Gear. +// +// Copyright (C) 2026 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use beetswap::multihasher::{Multihasher, MultihasherError}; +use blockstore::{block::CidError, cond_send::CondSend}; +use cid::{Cid, CidGeneric}; +use ethexe_common::db::HashStorageRO; +use futures::FutureExt; +use gprimitives::H256; +use libp2p::{ + Multiaddr, PeerId, + core::{Endpoint, transport::PortUse}, + swarm::{ + ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, THandler, THandlerInEvent, + THandlerOutEvent, ToSwarm, + }, +}; +use multihash::Multihash; +use std::{ + collections::HashMap, + mem, + sync::Arc, + task::{Context, Poll}, +}; +use tokio::{ + sync::{mpsc, oneshot}, + task, +}; + +#[derive(Clone)] +pub struct Handle(mpsc::UnboundedSender<(H256, oneshot::Sender>)>); + +impl Handle { + pub async fn request(&self, request: H256) -> Vec { + let (tx, rx) = oneshot::channel(); + + self.0 + .send((request, tx)) + .expect("channel should never be closed"); + + rx.await.expect("channel should never be closed") + } +} + +pub(crate) trait BlockstoreDatabase: Send + Sync + HashStorageRO { + fn clone_boxed(&self) -> Box; +} + +impl BlockstoreDatabase for ethexe_db::Database { + fn clone_boxed(&self) -> Box { + Box::new(self.clone()) + } +} + +pub struct Blockstore { + db: Box, +} + +impl Blockstore { + const MAX_BLOCK_SIZE: u64 = 1024 * 1024; // 1MB + const BLAKE2B_CODE: u64 = 0xb220; + const CID_CODEC: u64 = 0x55; +} + +impl blockstore::Blockstore for Blockstore { + fn get( + &self, + cid: &CidGeneric, + ) -> impl Future>>> + CondSend { + let hash = *cid.hash(); + let db = self.db.clone_boxed(); + task::spawn_blocking(move || { + let hash: Multihash<32> = + beetswap::utils::convert_multihash(&hash).ok_or(blockstore::Error::CidTooLarge)?; + if hash.code() != Self::BLAKE2B_CODE { + return Err(blockstore::Error::CidError(CidError::InvalidMultihashCode( + hash.code(), + Self::BLAKE2B_CODE, + ))); + } + if hash.size() as usize != mem::size_of::() { + return Err(blockstore::Error::CidError( + CidError::InvalidMultihashLength(hash.size() as usize), + )); + } + + let hash = H256::from_slice(hash.digest()); + let data = db.read_by_hash(hash); + + if let Some(data) = &data + && data.len() as u64 > Self::MAX_BLOCK_SIZE + { + log::warn!("{hash} is too large: {} bytes", data.len()); + return Err(blockstore::Error::ValueTooLarge); + } + + Ok(data) + }) + .map(|res| res.expect("database panicked")) + } + + async fn put_keyed( + &self, + _cid: &CidGeneric, + _data: &[u8], + ) -> blockstore::Result<()> { + Ok(()) + } + + async fn remove(&self, _cid: &CidGeneric) -> blockstore::Result<()> { + Ok(()) + } + + async fn close(self) -> blockstore::Result<()> { + Ok(()) + } +} + +struct Blake2b256Multihasher; + +impl Multihasher<32> for Blake2b256Multihasher { + async fn hash( + &self, + multihash_code: u64, + input: &[u8], + ) -> Result, MultihasherError> { + if multihash_code != Blockstore::BLAKE2B_CODE { + return Err(MultihasherError::UnknownMultihashCode); + } + + let hash = ethexe_db::hash(input); + let hash = Multihash::wrap(Blockstore::BLAKE2B_CODE, hash.as_bytes()) + .expect("size is always correct"); + Ok(hash) + } +} + +type InnerBehaviour = beetswap::Behaviour<32, Blockstore>; + +pub struct Behaviour { + inner: InnerBehaviour, + handle: Handle, + rx: mpsc::UnboundedReceiver<(H256, oneshot::Sender>)>, + requests: HashMap>>, +} + +impl Behaviour { + pub fn new(db: Box) -> Self { + let (handle, rx) = mpsc::unbounded_channel(); + let blockstore = Arc::new(Blockstore { db }); + + Self { + inner: InnerBehaviour::builder(blockstore) + .register_multihasher(Blake2b256Multihasher) + .protocol_prefix("/ethexe") + .expect("prefix is always correct") + .build(), + handle: Handle(handle), + rx, + requests: HashMap::new(), + } + } + + pub fn handle(&self) -> Handle { + self.handle.clone() + } + + fn cid(hash: H256) -> Cid { + Cid::new_v1( + Blockstore::CID_CODEC, + Multihash::wrap(Blockstore::BLAKE2B_CODE, hash.as_bytes()) + .expect("size is always correct"), + ) + } + + fn handle_inner_event(&mut self, event: beetswap::Event) { + match event { + beetswap::Event::GetQueryResponse { query_id, data } => { + if let Some(channel) = self.requests.remove(&query_id) { + let _ = channel.send(data); + } + } + beetswap::Event::GetQueryError { query_id, error } => { + // The wrapper builds CIDs itself, so invalid multihashes are impossible. + // Blockstore errors here mean local storage violated its read contract. + panic!("{query_id:?} query failed: {error}"); + } + } + } +} + +impl NetworkBehaviour for Behaviour { + type ConnectionHandler = THandler; + type ToSwarm = (); + + fn handle_pending_inbound_connection( + &mut self, + connection_id: ConnectionId, + local_addr: &Multiaddr, + remote_addr: &Multiaddr, + ) -> Result<(), ConnectionDenied> { + self.inner + .handle_pending_inbound_connection(connection_id, local_addr, remote_addr) + } + + fn handle_established_inbound_connection( + &mut self, + connection_id: ConnectionId, + peer: PeerId, + local_addr: &Multiaddr, + remote_addr: &Multiaddr, + ) -> Result, ConnectionDenied> { + self.inner.handle_established_inbound_connection( + connection_id, + peer, + local_addr, + remote_addr, + ) + } + + fn handle_pending_outbound_connection( + &mut self, + connection_id: ConnectionId, + maybe_peer: Option, + addresses: &[Multiaddr], + effective_role: Endpoint, + ) -> Result, ConnectionDenied> { + self.inner.handle_pending_outbound_connection( + connection_id, + maybe_peer, + addresses, + effective_role, + ) + } + + fn handle_established_outbound_connection( + &mut self, + connection_id: ConnectionId, + peer: PeerId, + addr: &Multiaddr, + role_override: Endpoint, + port_use: PortUse, + ) -> Result, ConnectionDenied> { + self.inner.handle_established_outbound_connection( + connection_id, + peer, + addr, + role_override, + port_use, + ) + } + + fn on_swarm_event(&mut self, event: FromSwarm) { + self.inner.on_swarm_event(event); + } + + fn on_connection_handler_event( + &mut self, + peer_id: PeerId, + connection_id: ConnectionId, + event: THandlerOutEvent, + ) { + self.inner + .on_connection_handler_event(peer_id, connection_id, event); + } + + fn poll( + &mut self, + cx: &mut Context<'_>, + ) -> Poll>> { + self.requests.retain(|&query_id, channel| { + if channel.is_closed() { + self.inner.cancel(query_id); + return false; + } + + true + }); + + while let Poll::Ready(Some((hash, channel))) = self.rx.poll_recv(cx) { + let cid = Self::cid(hash); + let query_id = self.inner.get(&cid); + self.requests.insert(query_id, channel); + } + + if let Poll::Ready(to_swarm) = self.inner.poll(cx) { + return match to_swarm { + ToSwarm::GenerateEvent(event) => { + self.handle_inner_event(event); + Poll::Pending + } + to_swarm => { + Poll::Ready(to_swarm.map_out(|_event| { + unreachable!("`ToSwarm::GenerateEvent` is handled above") + })) + } + }; + } + + Poll::Pending + } +} diff --git a/ethexe/network/src/db_sync/mod.rs b/ethexe/network/src/db_sync/mod.rs deleted file mode 100644 index a3771a4d82b..00000000000 --- a/ethexe/network/src/db_sync/mod.rs +++ /dev/null @@ -1,1237 +0,0 @@ -// This file is part of Gear. -// -// Copyright (C) 2024-2025 Gear Technologies Inc. -// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! Peer-to-peer database synchronization for `ethexe`. -//! -//! The protocol is built on libp2p request/response and is used to fetch data -//! that can be revalidated locally: raw CAS blobs, program-to-code mappings, -//! valid code sets, and announce chains. Requests are driven through -//! [`Handle`], while the behaviour internally retries across peers, enforces a -//! per-request timeout, and limits concurrent inbound responses. - -mod requests; -mod responses; - -pub(crate) use crate::{ - DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, - db_sync::{requests::RetriableRequest, responses::OngoingResponses}, - export::{Multiaddr, PeerId}, - utils::ParityScaleCodec, -}; -use crate::{db_sync::requests::OngoingRequests, peer_score, utils::AlternateCollectionFmt}; -use async_trait::async_trait; -use ethexe_common::{ - Announce, - db::{ - AnnounceStorageRO, BlockMetaStorageRO, CodesStorageRO, ConfigStorageRO, GlobalsStorageRO, - HashStorageRO, - }, - gear::CodeState, - network::{AnnouncesRequest, AnnouncesResponse}, -}; -use ethexe_db::Database; -use futures::FutureExt; -use gprimitives::{ActorId, CodeId, H256}; -use libp2p::{ - StreamProtocol, - core::{Endpoint, transport::PortUse}, - request_response, - request_response::{InboundFailure, Message, OutboundFailure, ProtocolSupport}, - swarm::{ - ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, THandler, THandlerInEvent, - THandlerOutEvent, ToSwarm, - }, -}; -use parity_scale_codec::{Decode, Encode}; -use std::{ - collections::{BTreeMap, BTreeSet}, - num::NonZeroU32, - pin::Pin, - sync::atomic::{AtomicU64, Ordering}, - task::{Context, Poll}, - time::Duration, -}; -use tokio::sync::{mpsc, oneshot}; - -const STREAM_PROTOCOL: StreamProtocol = StreamProtocol::new("/ethexe/db-sync/1.0.0"); - -#[derive(Clone, metrics_derive::Metrics)] -#[metrics(scope = "ethexe_network_db_sync")] -struct Metrics { - /// Number of either active or pending requests - ongoing_requests: metrics::Gauge, - /// Number of incoming dropped requests - incoming_dropped_requests: metrics::Counter, -} - -#[derive(Debug, Clone, Copy, Eq, PartialEq, derive_more::Display)] -pub enum RequestFailure { - /// Request had been processing for too long - #[display("Request had been processing for too long")] - Timeout, -} - -#[derive(Debug, Eq, PartialEq)] -pub enum Event { - /// Request is in a pending state because there are no peers - NoPeers { - /// The ID of request - request_id: RequestId, - }, - /// Request completion done - RequestSucceed { - /// The ID of request - request_id: RequestId, - }, - /// Request failed - RequestFailed { - /// The failed request - request_id: RequestId, - /// Reason of request failure - error: RequestFailure, - }, - /// Request canceled - /// - /// User dropped [`HandleFuture`]. - /// - /// NOTE: `Event` is not guaranteed in a multithreaded environment - RequestCancelled { - /// The canceled request - request_id: RequestId, - }, - /// Incoming request - IncomingRequest { - /// The ID of in-progress response - response_id: ResponseId, - /// Peer who requested - peer_id: PeerId, - }, - /// Request dropped because simultaneous limit exceeded - IncomingRequestDropped { - /// Peer who should have received the response - peer_id: PeerId, - }, - /// Response sent to incoming request - ResponseSent { - /// The ID of completed response - response_id: ResponseId, - /// Peer who should receive response - peer_id: PeerId, - }, -} - -#[derive(Debug, Clone)] -pub(crate) struct Config { - pub request_timeout: Duration, - pub max_simultaneous_responses: u32, - pub max_chain_len_for_announces_response: NonZeroU32, -} - -impl Default for Config { - fn default() -> Self { - Self { - request_timeout: Duration::from_secs(100), - max_simultaneous_responses: 10, - max_chain_len_for_announces_response: DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, - } - } -} - -#[cfg(test)] // used only in tests yet -impl Config { - pub(crate) fn with_request_timeout(mut self, request_timeout: Duration) -> Self { - self.request_timeout = request_timeout; - self - } - - pub(crate) fn with_max_simultaneous_responses( - mut self, - max_simultaneous_responses: u32, - ) -> Self { - self.max_simultaneous_responses = max_simultaneous_responses; - self - } -} - -/// An asynchronous provider of external blockchain data required for response validation. -#[async_trait] -pub trait ExternalDataProvider: Send + Sync { - /// Clone the provider as a trait object. - fn clone_boxed(&self) -> Box; - - /// Resolve program IDs to code IDs at the given block. - async fn programs_code_ids_at( - self: Box, - program_ids: BTreeSet, - block: H256, - ) -> anyhow::Result>; - - /// Resolve code IDs to code states at the given block. - async fn codes_states_at( - self: Box, - code_ids: BTreeSet, - block: H256, - ) -> anyhow::Result>; -} - -#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] -pub struct RequestId(u64); - -impl RequestId { - fn next() -> Self { - static COUNTER: AtomicU64 = AtomicU64::new(0); - RequestId(COUNTER.fetch_add(1, Ordering::Relaxed)) - } -} - -#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] -pub struct ResponseId(pub(crate) u64); - -#[derive(derive_more::Debug, Default, Clone, Eq, PartialEq, Encode, Decode, derive_more::From)] -pub struct HashesRequest( - #[debug("{:?}", AlternateCollectionFmt::set(_0, "hashes"))] pub BTreeSet, -); - -/// Request to fetch the program-to-code mapping visible at a specific block. -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct ProgramIdsRequest { - pub at: H256, - pub expected_count: u64, -} - -/// Request to fetch the current set of valid codes and verify the response -/// using [`ExternalDataProvider`] at a specific block. -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct ValidCodesRequest { - pub at: H256, - pub validated_count: u64, -} - -/// High-level db-sync request types supported by the network. -#[derive(Debug, Clone, Eq, PartialEq, derive_more::From)] -pub enum Request { - /// Fetch raw CAS blobs by hash. - Hashes(HashesRequest), - /// Fetch the program-to-code mapping for a block. - ProgramIds(ProgramIdsRequest), - /// Fetch the node's locally stored set of valid code IDs. - ValidCodes(ValidCodesRequest), - /// Fetch an announce chain segment. - Announces(AnnouncesRequest), -} - -impl Request { - /// Build a request for a set of CAS hashes. - pub fn hashes(request: impl Into>) -> Self { - Self::Hashes(HashesRequest(request.into())) - } - - /// Build a request for program-to-code mappings at `at`. - pub fn program_ids(at: H256, expected_count: u64) -> Self { - Self::ProgramIds(ProgramIdsRequest { at, expected_count }) - } - - /// Build a request for the valid code set, using `at` only for response - /// verification. - pub fn valid_codes(at: H256, validated_count: u64) -> Self { - Self::ValidCodes(ValidCodesRequest { - at, - validated_count, - }) - } -} - -/// Successful db-sync responses returned to callers. -#[derive(derive_more::Debug, Clone, Eq, PartialEq, derive_more::From, derive_more::Unwrap)] -pub enum Response { - /// Raw CAS blobs keyed by hash. - Hashes(#[debug("{:?}", AlternateCollectionFmt::map(_0, "entries"))] BTreeMap>), - /// Program-to-code mapping reconstructed for a block. - ProgramIds( - #[debug("{:?}", AlternateCollectionFmt::map(_0, "programs"))] BTreeMap, - ), - /// Set of valid code IDs known at a block. - ValidCodes(#[debug("{:?}", AlternateCollectionFmt::set(_0, "codes"))] BTreeSet), - /// Contiguous announce chain response. - Announces(AnnouncesResponse), -} - -/// Result delivered by [`HandleFuture`]. -pub type HandleResult = Result; - -enum HandleAction { - Request(RequestId, Request), - Retry(RetriableRequest), -} - -impl HandleAction { - fn request_id(&self) -> RequestId { - match self { - HandleAction::Request(request_id, _) => *request_id, - HandleAction::Retry(request) => request.id(), - } - } -} - -/// Future returned by [`Handle::request`] and [`Handle::retry`]. -pub struct HandleFuture { - request_id: RequestId, - rx: oneshot::Receiver, -} - -impl HandleFuture { - /// Returns the identifier assigned to this request. - pub fn request_id(&self) -> RequestId { - self.request_id - } -} - -impl Future for HandleFuture { - type Output = HandleResult; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - self.rx - .poll_unpin(cx) - .map(|res| res.expect("channel should never be closed")) - } -} - -#[derive(Clone)] -pub struct Handle(mpsc::UnboundedSender<(HandleAction, oneshot::Sender)>); - -impl Handle { - fn send(&self, action: HandleAction) -> HandleFuture { - let (tx, rx) = oneshot::channel(); - let request_id = action.request_id(); - - self.0 - .send((action, tx)) - .expect("channel should never be closed"); - - HandleFuture { request_id, rx } - } - - /// Enqueue a new request. - pub fn request(&self, request: Request) -> HandleFuture { - self.send(HandleAction::Request(RequestId::next(), request)) - } - - /// Re-enqueue a retriable request returned by a previous failure. - pub fn retry(&self, request: RetriableRequest) -> HandleFuture { - self.send(HandleAction::Retry(request)) - } -} - -#[derive(Debug, Clone, Eq, PartialEq, Encode, Decode)] -pub(crate) struct InnerProgramIdsRequest { - at: H256, -} - -/// Network-only type to be encoded-decoded and sent over the network -#[derive(Debug, Clone, Eq, PartialEq, Encode, Decode, derive_more::From)] -pub(crate) enum InnerRequest { - Hashes(HashesRequest), - ProgramIds(InnerProgramIdsRequest), - ValidCodes, - Announces(AnnouncesRequest), -} - -#[derive(Debug, Clone, Default, Eq, PartialEq, Encode, Decode)] -pub(crate) struct InnerHashesResponse(BTreeMap>); - -#[derive(Debug, Default, Eq, PartialEq, Encode, Decode)] -pub(crate) struct InnerProgramIdsResponse(BTreeSet); - -// TODO #4911: can be optimized - only not-base announces could be returned. -/// Response for announces request. -/// Must contain all announces for the requested range. -/// Must be sorted from predecessors to successors. -#[derive(Debug, Clone, Default, PartialEq, Eq, Encode, Decode)] -pub(crate) struct InnerAnnouncesResponse(Vec); - -/// Network-only type to be encoded-decoded and sent over the network -#[derive(Debug, Eq, PartialEq, derive_more::From, derive_more::Unwrap, Encode, Decode)] -pub(crate) enum InnerResponse { - Hashes(InnerHashesResponse), - ProgramIds(InnerProgramIdsResponse), - ValidCodes(BTreeSet), - Announces(InnerAnnouncesResponse), -} - -type InnerBehaviour = request_response::Behaviour>; - -#[auto_impl::auto_impl(&, Box)] -pub trait DbSyncDatabase: - Send - + HashStorageRO - + BlockMetaStorageRO - + AnnounceStorageRO - + CodesStorageRO - + ConfigStorageRO - + GlobalsStorageRO -{ - /// Clone the database as a trait object. - fn clone_boxed(&self) -> Box; -} - -impl DbSyncDatabase for Database { - fn clone_boxed(&self) -> Box { - Box::new(self.clone()) - } -} - -pub(crate) struct Behaviour { - inner: InnerBehaviour, - handle: Handle, - rx: mpsc::UnboundedReceiver<(HandleAction, oneshot::Sender)>, - ongoing_requests: OngoingRequests, - ongoing_responses: OngoingResponses, - metrics: Metrics, -} - -impl Behaviour { - pub(crate) fn new( - config: Config, - peer_score_handle: peer_score::Handle, - external_data_provider: Box, - db: Box, - ) -> Self { - let (handle, rx) = mpsc::unbounded_channel(); - let handle = Handle(handle); - - Self { - inner: InnerBehaviour::new( - [(STREAM_PROTOCOL, ProtocolSupport::Full)], - request_response::Config::default(), - ), - handle, - rx, - ongoing_requests: OngoingRequests::new( - &config, - peer_score_handle, - external_data_provider, - ), - ongoing_responses: OngoingResponses::new(db, &config), - metrics: Metrics::default(), - } - } - - pub fn handle(&self) -> Handle { - self.handle.clone() - } - - fn handle_inner_event( - &mut self, - event: request_response::Event, - ) -> Poll>> { - match event { - request_response::Event::Message { - peer, - connection_id: _, - message: - Message::Request { - request_id: _, - request, - channel, - }, - } => { - let response_id = self - .ongoing_responses - .handle_response(peer, channel, request); - - let event = if let Some(response_id) = response_id { - Event::IncomingRequest { - response_id, - peer_id: peer, - } - } else { - self.metrics.incoming_dropped_requests.increment(1); - Event::IncomingRequestDropped { peer_id: peer } - }; - - return Poll::Ready(ToSwarm::GenerateEvent(event)); - } - request_response::Event::Message { - peer: _, - connection_id: _, - message: - Message::Response { - request_id, - response, - }, - } => { - self.ongoing_requests.on_peer_response(request_id, response); - } - request_response::Event::OutboundFailure { - peer, - connection_id: _, - request_id, - error, - } => { - log::trace!("outbound failure for request {request_id} to {peer}: {error}"); - - if let OutboundFailure::UnsupportedProtocols = error { - log::debug!( - "request to {peer} failed because it doesn't support {STREAM_PROTOCOL} protocol" - ); - } - - self.ongoing_requests.on_peer_failure(request_id); - } - request_response::Event::InboundFailure { - peer, - connection_id: _, - request_id: _, - error: InboundFailure::UnsupportedProtocols, - } => { - log::debug!( - "request from {peer} failed because it doesn't support {STREAM_PROTOCOL} protocol" - ); - } - request_response::Event::InboundFailure { .. } => {} - request_response::Event::ResponseSent { .. } => {} - } - - Poll::Pending - } -} - -impl NetworkBehaviour for Behaviour { - type ConnectionHandler = THandler; - type ToSwarm = Event; - - fn handle_pending_inbound_connection( - &mut self, - connection_id: ConnectionId, - local_addr: &Multiaddr, - remote_addr: &Multiaddr, - ) -> Result<(), ConnectionDenied> { - self.inner - .handle_pending_inbound_connection(connection_id, local_addr, remote_addr) - } - - fn handle_established_inbound_connection( - &mut self, - connection_id: ConnectionId, - peer: PeerId, - local_addr: &Multiaddr, - remote_addr: &Multiaddr, - ) -> Result, ConnectionDenied> { - self.inner.handle_established_inbound_connection( - connection_id, - peer, - local_addr, - remote_addr, - ) - } - - fn handle_pending_outbound_connection( - &mut self, - connection_id: ConnectionId, - maybe_peer: Option, - addresses: &[Multiaddr], - effective_role: Endpoint, - ) -> Result, ConnectionDenied> { - self.inner.handle_pending_outbound_connection( - connection_id, - maybe_peer, - addresses, - effective_role, - ) - } - - fn handle_established_outbound_connection( - &mut self, - connection_id: ConnectionId, - peer: PeerId, - addr: &Multiaddr, - role_override: Endpoint, - port_use: PortUse, - ) -> Result, ConnectionDenied> { - self.inner.handle_established_outbound_connection( - connection_id, - peer, - addr, - role_override, - port_use, - ) - } - - fn on_swarm_event(&mut self, event: FromSwarm) { - self.inner.on_swarm_event(event); - self.ongoing_requests.on_swarm_event(event); - } - - fn on_connection_handler_event( - &mut self, - peer_id: PeerId, - connection_id: ConnectionId, - event: THandlerOutEvent, - ) { - self.inner - .on_connection_handler_event(peer_id, connection_id, event) - } - - fn poll( - &mut self, - cx: &mut Context<'_>, - ) -> Poll>> { - if let Poll::Ready(Some((action, channel))) = self.rx.poll_recv(cx) { - match action { - HandleAction::Request(request_id, request) => { - self.ongoing_requests.request(request_id, request, channel); - } - HandleAction::Retry(request) => { - self.ongoing_requests.retry(request, channel); - } - } - } - - if let Poll::Ready(request_event) = - self.ongoing_requests - .poll(cx, &mut self.inner, &self.metrics) - { - return Poll::Ready(ToSwarm::GenerateEvent(request_event)); - } - - if let Poll::Ready((peer_id, response_id)) = - self.ongoing_responses.poll(cx, &mut self.inner) - { - return Poll::Ready(ToSwarm::GenerateEvent(Event::ResponseSent { - response_id, - peer_id, - })); - } - - if let Poll::Ready(to_swarm) = self.inner.poll(cx) { - return match to_swarm { - ToSwarm::GenerateEvent(event) => self.handle_inner_event(event), - to_swarm => Poll::Ready(to_swarm.map_out::(|_event| { - unreachable!("`ToSwarm::GenerateEvent` is handled above") - })), - }; - } - - Poll::Pending - } -} - -#[cfg(test)] -pub(crate) mod tests { - use super::*; - use crate::{tests::DataProvider, utils::tests::init_logger}; - use assert_matches::assert_matches; - use ethexe_common::{Announce, HashOf, StateHashWithQueueSize, db::*}; - use ethexe_db::Database; - use libp2p::{ - Swarm, Transport, - core::{transport::MemoryTransport, upgrade::Version}, - futures::StreamExt, - identity::Keypair, - swarm, - swarm::SwarmEvent, - }; - use libp2p_swarm_test::SwarmExt; - use std::{iter, mem}; - use tokio::time; - - // exactly like `Swarm::new_ephemeral_tokio` but we can pass our own config - fn new_ephemeral_swarm( - config: swarm::Config, - behaviour: T, - ) -> Swarm { - let identity = Keypair::generate_ed25519(); - let peer_id = PeerId::from(identity.public()); - - let transport = MemoryTransport::default() - .or_transport(libp2p::tcp::tokio::Transport::default()) - .upgrade(Version::V1) - .authenticate(libp2p::plaintext::Config::new(&identity)) - .multiplex(libp2p::yamux::Config::default()) - .timeout(Duration::from_secs(20)) - .boxed(); - - Swarm::new(transport, behaviour, peer_id, config) - } - - async fn new_swarm_with_config(config: Config) -> (Swarm, Database, DataProvider) { - let data_provider = DataProvider::default(); - let db = Database::memory(); - let behaviour = Behaviour::new( - config, - peer_score::Handle::new_test(), - data_provider.clone_boxed(), - Box::new(db.clone()), - ); - let mut swarm = Swarm::new_ephemeral_tokio(move |_keypair| behaviour); - swarm.listen().with_memory_addr_external().await; - (swarm, db, data_provider) - } - - async fn new_swarm() -> (Swarm, Database, DataProvider) { - new_swarm_with_config(Config::default()).await - } - - #[tokio::test] - async fn smoke() { - init_logger(); - - let (mut alice, _alice_db, _data_provider) = new_swarm().await; - let alice_handle = alice.behaviour().handle(); - let (mut bob, bob_db, _data_provider) = new_swarm().await; - - let hello_hash = bob_db.cas().write(b"hello"); - let world_hash = bob_db.cas().write(b"world"); - - alice.connect(&mut bob).await; - tokio::spawn(async move { - let mut values = None; - - while let Some(event) = bob.next().await { - let Ok(event) = event.try_into_behaviour_event() else { - continue; - }; - - match event { - Event::IncomingRequest { - response_id, - peer_id, - } => { - values = Some((response_id, peer_id)); - } - Event::ResponseSent { - response_id, - peer_id, - } => { - let (initial_response_id, initial_peer_id) = - values.expect("IncomingRequest must be first"); - assert_eq!(initial_response_id, response_id); - assert_eq!(initial_peer_id, peer_id); - } - _ => {} - } - } - }); - - let request = alice_handle.request(Request::hashes([hello_hash, world_hash])); - let request_id = request.request_id(); - - let event = alice.next_behaviour_event().await; - assert_eq!(event, Event::RequestSucceed { request_id }); - - let response = request.await.unwrap(); - assert_eq!( - response, - Response::Hashes( - [ - (hello_hash, b"hello".to_vec()), - (world_hash, b"world".to_vec()) - ] - .into() - ) - ) - } - - #[tokio::test(start_paused = true)] - async fn timeout() { - init_logger(); - - let alice_config = Config::default().with_request_timeout(Duration::from_secs(3)); - let (mut alice, _alice_db, _data_provider) = new_swarm_with_config(alice_config).await; - let alice_handle = alice.behaviour().handle(); - - let mut bob = Swarm::new_ephemeral_tokio(|_keypair| { - InnerBehaviour::new( - [(STREAM_PROTOCOL, ProtocolSupport::Full)], - request_response::Config::default(), - ) - }); - bob.connect(&mut alice).await; - - let request = alice_handle.request(Request::hashes([])); - let request_id = request.request_id(); - - tokio::spawn(async move { - while let Some(event) = bob.next().await { - if let Ok(request_response::Event::Message { - message: - Message::Request { - channel, request, .. - }, - .. - }) = event.try_into_behaviour_event() - { - assert_eq!(request, InnerRequest::Hashes(HashesRequest::default())); - // just ignore request - mem::forget(channel); - } - } - }); - - time::advance(Config::default().request_timeout).await; - - let event = alice.next_behaviour_event().await; - assert_eq!( - event, - Event::RequestFailed { - request_id, - error: RequestFailure::Timeout, - } - ); - request.await.unwrap_err(); - } - - #[tokio::test] - async fn excessive_data_stripped() { - const DATA: [[u8; 1]; 3] = [*b"1", *b"2", *b"3"]; - - init_logger(); - - let (mut alice, _alice_db, _data_provider) = new_swarm().await; - let alice_handle = alice.behaviour().handle(); - - let mut bob = Swarm::new_ephemeral_tokio(move |_keypair| { - InnerBehaviour::new( - [(STREAM_PROTOCOL, ProtocolSupport::Full)], - request_response::Config::default(), - ) - }); - bob.connect(&mut alice).await; - - let data_0 = ethexe_db::hash(&DATA[0]); - let data_1 = ethexe_db::hash(&DATA[1]); - let data_2 = ethexe_db::hash(&DATA[2]); - - let request = alice_handle.request(Request::hashes([data_0, data_1])); - let request_id = request.request_id(); - - tokio::spawn(async move { - while let Some(event) = bob.next().await { - if let Ok(request_response::Event::Message { - message: - Message::Request { - channel, request, .. - }, - .. - }) = event.try_into_behaviour_event() - { - assert_eq!( - request, - InnerRequest::Hashes(HashesRequest([data_0, data_1].into())) - ); - bob.behaviour_mut() - .send_response( - channel, - InnerHashesResponse( - [ - (data_0, DATA[0].to_vec()), - (data_1, DATA[1].to_vec()), - (data_2, DATA[2].to_vec()), - ] - .into(), - ) - .into(), - ) - .unwrap(); - } - } - }); - - let event = alice.next_behaviour_event().await; - assert_eq!(event, Event::RequestSucceed { request_id }); - - let response = request.await.unwrap(); - assert_eq!( - response, - Response::Hashes([(data_0, DATA[0].to_vec()), (data_1, DATA[1].to_vec())].into()) - ); - } - - #[tokio::test] - async fn truncated_hashes_response_completed_from_same_peer() { - const DATA_LEN: usize = 6 * 1024 * 1024; - - init_logger(); - - let (mut alice, _alice_db, _data_provider) = new_swarm().await; - let alice_handle = alice.behaviour().handle(); - let (mut bob, bob_db, _data_provider) = new_swarm().await; - - let data_0 = vec![0; DATA_LEN]; - let data_1 = vec![1; DATA_LEN]; - let hash_0 = bob_db.cas().write(&data_0); - let hash_1 = bob_db.cas().write(&data_1); - - alice.connect(&mut bob).await; - tokio::spawn(bob.loop_on_next()); - - let request = alice_handle.request(Request::hashes([hash_0, hash_1])); - let request_id = request.request_id(); - - let event = alice.next_behaviour_event().await; - assert_eq!(event, Event::RequestSucceed { request_id }); - - let response = request.await.unwrap(); - assert_eq!( - response, - Response::Hashes([(hash_0, data_0), (hash_1, data_1)].into()) - ); - } - - #[tokio::test] - async fn request_response_type_mismatch() { - init_logger(); - - let alice_config = Config::default().with_request_timeout(Duration::ZERO); - let (mut alice, _alice_db, _data_provider) = new_swarm_with_config(alice_config).await; - let alice_handle = alice.behaviour().handle(); - - let mut bob = Swarm::new_ephemeral_tokio(move |_keypair| { - InnerBehaviour::new( - [(STREAM_PROTOCOL, ProtocolSupport::Full)], - request_response::Config::default(), - ) - }); - bob.connect(&mut alice).await; - - let request = alice_handle.request(Request::hashes([])); - let request_id = request.request_id(); - - tokio::spawn(async move { - while let Some(event) = bob.next().await { - if let Ok(request_response::Event::Message { - message: - Message::Request { - channel, request, .. - }, - .. - }) = event.try_into_behaviour_event() - { - assert_eq!(request, InnerRequest::Hashes(HashesRequest::default())); - bob.behaviour_mut() - .send_response(channel, InnerProgramIdsResponse::default().into()) - .unwrap(); - } - } - }); - - let event = alice.next_behaviour_event().await; - assert_eq!( - event, - Event::RequestFailed { - request_id, - error: RequestFailure::Timeout, - } - ); - - request.await.unwrap_err(); - } - - #[tokio::test] - async fn request_completed_with_3_peers() { - init_logger(); - - let (mut alice, _alice_db, _data_provider) = new_swarm().await; - let alice_handle = alice.behaviour().handle(); - let (mut bob, bob_db, _data_provider) = new_swarm().await; - let (mut charlie, charlie_db, _data_provider) = new_swarm().await; - let (mut dave, dave_db, _data_provider) = new_swarm().await; - - alice.connect(&mut bob).await; - alice.connect(&mut charlie).await; - alice.connect(&mut dave).await; - tokio::spawn(bob.loop_on_next()); - tokio::spawn(charlie.loop_on_next()); - tokio::spawn(dave.loop_on_next()); - - let hello_hash = bob_db.cas().write(b"hello"); - let world_hash = charlie_db.cas().write(b"world"); - let mark_hash = dave_db.cas().write(b"!"); - - let request = alice_handle.request(Request::hashes([hello_hash, world_hash, mark_hash])); - let request_id = request.request_id(); - - let event = alice.next_behaviour_event().await; - assert_eq!(event, Event::RequestSucceed { request_id }); - - let response = request.await.unwrap(); - assert_eq!( - response, - Response::Hashes( - [ - (hello_hash, b"hello".to_vec()), - (world_hash, b"world".to_vec()), - (mark_hash, b"!".to_vec()), - ] - .into() - ) - ); - } - - #[tokio::test] - async fn request_completed_after_new_peer() { - init_logger(); - - let (mut alice, _alice_db, _data_provider) = new_swarm().await; - let alice_handle = alice.behaviour().handle(); - let (mut bob, bob_db, _data_provider) = new_swarm().await; - let (charlie, charlie_db, _data_provider) = new_swarm().await; - let charlie_addr = charlie.external_addresses().next().cloned().unwrap(); - - alice.connect(&mut bob).await; - tokio::spawn(bob.loop_on_next()); - - let hello_hash = bob_db.cas().write(b"hello"); - let world_hash = charlie_db.cas().write(b"world"); - - let request = alice_handle.request(Request::hashes([hello_hash, world_hash])); - let request_id = request.request_id(); - - // first attempt - let event = alice.next_behaviour_event().now_or_never(); - assert_eq!(event, None); - - tokio::spawn(charlie.loop_on_next()); - alice.dial_and_wait(charlie_addr).await; - - // second attempt - let event = alice.next_behaviour_event().await; - assert_eq!(event, Event::RequestSucceed { request_id }); - - let response = request.await.unwrap(); - assert_eq!( - response, - Response::Hashes( - [ - (hello_hash, b"hello".to_vec()), - (world_hash, b"world".to_vec()) - ] - .into() - ) - ); - } - - #[tokio::test(start_paused = true)] - async fn unsupported_protocol_handled() { - const REQUEST_TIMEOUT: Duration = Duration::from_secs(2); - - init_logger(); - - let alice_config = Config::default().with_request_timeout(REQUEST_TIMEOUT); - let (mut alice, _alice_db, _data_provider) = new_swarm_with_config(alice_config).await; - let alice_handle = alice.behaviour().handle(); - - // idle connection timeout is lowered because `libp2p` uses `future_timer` inside, - // so we cannot advance time like in tokio - let mut bob = new_ephemeral_swarm( - swarm::Config::with_tokio_executor() - .with_idle_connection_timeout(Duration::from_secs(5)), - InnerBehaviour::new([], request_response::Config::default()), - ); - let bob_peer_id = *bob.local_peer_id(); - bob.connect(&mut alice).await; - tokio::spawn(bob.loop_on_next()); - - let request = alice_handle.request(Request::hashes([])); - let request_id = request.request_id(); - - // activate timer - let event = alice.next_behaviour_event().now_or_never(); - assert_eq!(event, None); - - time::advance(REQUEST_TIMEOUT).await; - - let event = alice.next_behaviour_event().await; - assert_eq!( - event, - Event::RequestFailed { - request_id, - error: RequestFailure::Timeout - } - ); - - let event = alice.next_swarm_event().await; - assert_matches!(event, SwarmEvent::ConnectionClosed { peer_id, .. } if peer_id == bob_peer_id); - } - - #[tokio::test] - async fn simultaneous_responses_limit() { - init_logger(); - - let alice_config = Config::default().with_max_simultaneous_responses(0); - let (mut alice, _alice_db, _data_provider) = new_swarm_with_config(alice_config).await; - - let (mut bob, _bob_db, _data_provider) = new_swarm().await; - let bob_handle = bob.behaviour().handle(); - let bob_peer_id = *bob.local_peer_id(); - - alice.connect(&mut bob).await; - tokio::spawn(bob.loop_on_next()); - - let fut = bob_handle.request(Request::hashes([])); - mem::forget(fut); - - let event = alice.next_behaviour_event().await; - assert_matches!(event, Event::IncomingRequestDropped { peer_id } if peer_id == bob_peer_id); - } - - #[tokio::test(start_paused = true)] - async fn retry() { - init_logger(); - - let (mut alice, _alice_db, _data_provider) = new_swarm().await; - let alice_handle = alice.behaviour().handle(); - let mut bob = Swarm::new_ephemeral_tokio(move |_keypair| { - InnerBehaviour::new( - [(STREAM_PROTOCOL, ProtocolSupport::Full)], - request_response::Config::default(), - ) - }); - bob.connect(&mut alice).await; - - let request_key = ethexe_db::hash(b"test"); - let request = alice_handle.request(Request::hashes([request_key])); - let request_id = request.request_id(); - - let bob_handle = tokio::spawn(async move { - while let Some(event) = bob.next().await { - if let Ok(request_response::Event::Message { - message: - Message::Request { - channel, request, .. - }, - .. - }) = event.try_into_behaviour_event() - { - assert_eq!( - request, - InnerRequest::Hashes(HashesRequest([request_key].into())) - ); - // just ignore request - mem::forget(channel); - } - } - }); - - time::advance(Config::default().request_timeout).await; - - // first attempt - let event = alice.next_behaviour_event().await; - assert_eq!( - event, - Event::RequestFailed { - request_id, - error: RequestFailure::Timeout, - } - ); - let (error, retriable_request) = request.await.unwrap_err(); - assert_eq!(error, RequestFailure::Timeout); - - time::resume(); - - bob_handle.abort(); - assert!(bob_handle.await.unwrap_err().is_cancelled()); - let (mut charlie, charlie_db, _data_provider) = new_swarm().await; - alice.connect(&mut charlie).await; - tokio::spawn(charlie.loop_on_next()); - - let key = charlie_db.cas().write(b"test"); - assert_eq!(request_key, key); - let request = alice_handle.retry(retriable_request); - let request_id = request.request_id(); - - // retry attempt - let event = alice.next_behaviour_event().await; - assert_eq!(event, Event::RequestSucceed { request_id }); - - let response = request.await.unwrap(); - assert_eq!( - response, - Response::Hashes([(request_key, b"test".to_vec())].into()) - ); - } - - #[tokio::test] - async fn external_data_provider() { - init_logger(); - - let (mut alice, _alice_db, alice_data_provider) = new_swarm().await; - let alice_handle = alice.behaviour().handle(); - let (mut bob, bob_db, _data_provider) = new_swarm().await; - - let expected_response = fill_data_provider(alice_data_provider, bob_db).await; - - alice.connect(&mut bob).await; - tokio::spawn(bob.loop_on_next()); - - let request = alice_handle.request(Request::program_ids(H256::zero(), 2)); - let request_id = request.request_id(); - - let event = alice.next_behaviour_event().await; - assert_eq!(event, Event::RequestSucceed { request_id }); - - let response = request.await.unwrap(); - assert_eq!(response, expected_response); - } - - #[tokio::test] - async fn request_cancelled() { - let (mut alice, _db, _data_provider) = new_swarm().await; - - let request = alice.behaviour().handle().request(Request::hashes([])); - let request_id = request.request_id(); - drop(request); - - let event = alice.next_behaviour_event().await; - assert_eq!(event, Event::RequestCancelled { request_id }); - } - - pub(crate) async fn fill_data_provider( - // data provider of the first peer - left_data_provider: DataProvider, - // database of the second peer - right_db: Database, - ) -> Response { - let program_ids: BTreeSet = [ActorId::new([1; 32]), ActorId::new([2; 32])].into(); - let code_ids = vec![CodeId::new([0xfe; 32]), CodeId::new([0xef; 32])]; - left_data_provider - .set_programs_code_ids_at(program_ids.clone(), H256::zero(), code_ids.clone()) - .await; - - let announce = Announce::base(H256::zero(), HashOf::zero()); - let announce_hash = announce.to_hash(); - right_db.mutate_block_announces(H256::zero(), |announces| { - announces.insert(announce_hash); - }); - - right_db.set_announce_program_states( - announce_hash, - iter::zip( - program_ids.clone(), - iter::repeat_with(H256::random).map(|hash| StateHashWithQueueSize { - hash, - canonical_queue_size: 0, - injected_queue_size: 0, - }), - ) - .collect(), - ); - - Response::ProgramIds(iter::zip(program_ids, code_ids).collect()) - } -} diff --git a/ethexe/network/src/db_sync/requests.rs b/ethexe/network/src/db_sync/requests.rs deleted file mode 100644 index f393569fc1d..00000000000 --- a/ethexe/network/src/db_sync/requests.rs +++ /dev/null @@ -1,1101 +0,0 @@ -// This file is part of Gear. -// -// Copyright (C) 2025 Gear Technologies Inc. -// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -use crate::{ - db_sync::{ - AnnouncesRequest, Config, Event, ExternalDataProvider, HandleResult, HashesRequest, - InnerAnnouncesResponse, InnerBehaviour, InnerHashesResponse, InnerProgramIdsRequest, - InnerProgramIdsResponse, InnerRequest, InnerResponse, Metrics, PeerId, ProgramIdsRequest, - Request, RequestFailure, RequestId, Response, ValidCodesRequest, - }, - peer_score::Handle, - utils::{ConnectionMap, NoLimits}, -}; -use anyhow::Context as _; -use ethexe_common::{ - Announce, HashOf, - gear::CodeState, - network::{AnnouncesRequestUntil, AnnouncesResponse}, -}; -use futures::{FutureExt, future::BoxFuture}; -use gprimitives::{ActorId, CodeId, H256}; -use itertools::EitherOrBoth; -use libp2p::{request_response::OutboundRequestId, swarm::FromSwarm}; -use rand::prelude::IteratorRandom; -use std::{ - cell::OnceCell, - collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}, - iter, - task::{Context, Poll, Waker}, - time::Duration, -}; -use tokio::{sync::oneshot, time}; - -ethexe_service_utils::task_local! { - static CONTEXT: OngoingRequestContext; -} - -type OngoingRequestFuture = BoxFuture<'static, Result>; - -pub(crate) struct OngoingRequests { - pending_events: VecDeque, - requests: HashMap>)>, - active_requests: HashMap, - responses: HashMap>, - connections: ConnectionMap, - waker: Option, - // used in requests themselves - peer_score_handle: Handle, - external_data_provider: Box, - // config - request_timeout: Duration, -} - -impl OngoingRequests { - pub(crate) fn new( - config: &Config, - peer_score_handle: Handle, - external_data_provider: Box, - ) -> Self { - Self { - pending_events: VecDeque::new(), - requests: Default::default(), - active_requests: Default::default(), - responses: Default::default(), - connections: ConnectionMap::without_limits(), - waker: None, - peer_score_handle, - external_data_provider, - request_timeout: config.request_timeout, - } - } - - fn wake(&mut self) { - if let Some(waker) = self.waker.take() { - waker.wake(); - } - } - - /// Tracks all active connections. - pub(crate) fn on_swarm_event(&mut self, event: FromSwarm) { - if self.connections.on_swarm_event(event) { - self.wake(); - } - } - - fn inner_request( - &mut self, - request_id: RequestId, - request: OngoingRequest, - channel: oneshot::Sender, - ) { - self.requests.insert( - request_id, - ( - request - .request( - self.peer_score_handle.clone(), - self.external_data_provider.clone_boxed(), - self.request_timeout, - ) - .boxed(), - Some(channel), - ), - ); - } - - pub(crate) fn request( - &mut self, - request_id: RequestId, - request: Request, - channel: oneshot::Sender, - ) { - self.inner_request(request_id, OngoingRequest::new(request), channel); - } - - pub(crate) fn retry( - &mut self, - request: RetriableRequest, - channel: oneshot::Sender, - ) { - let RetriableRequest { - request_id, - request, - } = request; - self.inner_request(request_id, request, channel); - } - - fn inner_on_peer( - &mut self, - outbound_request_id: OutboundRequestId, - res: Result, - ) { - let request_id = self - .active_requests - .remove(&outbound_request_id) - .expect("unknown outbound request id"); - let fut = self.requests.get_mut(&request_id); - - // request can be removed because of timeout, - // so we don't expect it's still inside `self.requests` - if fut.is_some() { - self.responses.insert(request_id, res); - self.wake(); - } else { - log::trace!("{outbound_request_id:?} has been skipped for {request_id:?}"); - } - } - - pub(crate) fn on_peer_response( - &mut self, - outbound_request_id: OutboundRequestId, - response: InnerResponse, - ) { - self.inner_on_peer(outbound_request_id, Ok(response)); - } - - pub(crate) fn on_peer_failure(&mut self, outbound_request_id: OutboundRequestId) { - self.inner_on_peer(outbound_request_id, Err(())); - } - - pub(crate) fn poll( - &mut self, - cx: &mut Context<'_>, - behaviour: &mut InnerBehaviour, - metrics: &Metrics, - ) -> Poll { - loop { - if let Some(event) = self.pending_events.pop_front() { - return Poll::Ready(event); - } - - let peers: HashSet = self.connections.peers().collect(); - - self.requests.retain(|&request_id, (fut, channel)| { - let response = self.responses.remove(&request_id); - - // it means `HandleFuture` is dropped, - // so we just remove the request and don't make any further work - if channel.as_ref().expect("always Some").is_closed() { - self.pending_events.push_back(Event::RequestCancelled { request_id }); - return false; - } - - let ctx = OngoingRequestContext { - state: OnceCell::new(), - peers: peers.clone(), - response, - }; - - let (ctx, poll) = CONTEXT.scope(ctx, || fut.poll_unpin(cx)); - let state = ctx.into_state(); - if state.is_some() && poll.is_ready() { - unreachable!( - "state machine invariant violated: unexpected ready poll with existing state" - ); - } - - if let Some(state) = state { - match state { - OngoingRequestState::NoPeers => { - - self.pending_events.push_back(Event::NoPeers { request_id }); - }, - OngoingRequestState::SendRequest(peer, request, ) => { - let outbound_request_id = behaviour.send_request(&peer, request); - self.active_requests.insert(outbound_request_id, request_id); - } - }; - } else if let Poll::Ready(res) = poll { - let (event, res) = match res { - Ok(response) => { - (Event::RequestSucceed { request_id }, Ok(response)) - } - Err((error, request)) => { - (Event::RequestFailed { request_id, error }, Err((error, RetriableRequest { - request_id, - request, - }, - ))) - } - }; - - self.pending_events.push_back(event); - - // channel can be dropped after `is_closed()` check during future polling - let res = channel.take().expect("always Some").send(res); - if res.is_err() { - self.pending_events.push_back(Event::RequestCancelled { request_id }); - } - - return false; - } - - true - }); - metrics.ongoing_requests.set(self.requests.len() as f64); - - // it means some futures are pending, so we definitely will wake the task - if !self.requests.is_empty() { - self.waker = Some(cx.waker().clone()); - } - - if !self.pending_events.is_empty() { - // immediately return event instead of task waking - continue; - } - - break Poll::Pending; - } - } -} - -#[derive(Debug)] -enum HashesResponseHandled { - Done { - response: BTreeMap>, - stripped: bool, - }, - IncompleteData { - acc: InnerHashesResponse, - new_request: HashesRequest, - stripped: bool, - }, - Err { - acc: InnerHashesResponse, - err: HashesResponseError, - stripped: bool, - }, -} - -impl HashesResponseHandled { - fn stripped(&self) -> bool { - match self { - Self::Done { stripped, .. } => *stripped, - Self::IncompleteData { stripped, .. } => *stripped, - Self::Err { stripped, .. } => *stripped, - } - } -} - -#[derive(Debug, derive_more::Unwrap)] -pub(crate) enum AnnouncesResponseHandled { - Done(AnnouncesResponse), - NewRound, - Err(AnnouncesResponseError), -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq, derive_more::Display)] -pub enum HashesResponseError { - #[display("hash mismatch from provided data")] - HashMismatch, -} - -#[derive(Debug, derive_more::Display)] -pub enum ProgramIdsResponseError { - #[display("not enough program-code ids expected {expected} but got {received}")] - NotEnoughIds { expected: usize, received: usize }, - #[display("router failed: {_0}")] - RouterQuery(anyhow::Error), -} - -#[derive(Debug, derive_more::Display)] -pub enum ValidCodesResponseError { - #[display("not enough validated codes")] - NotEnoughCodes, - #[display("{_0}")] - RouterQuery(anyhow::Error), -} - -#[derive(Debug, PartialEq, Eq, derive_more::Display)] -pub enum AnnouncesResponseError { - #[display("announces head mismatch, expected hash {expected}, received {received}")] - HeadMismatch { - expected: HashOf, - received: HashOf, - }, - #[display("announces tail mismatch, expected hash {expected}, received {received}")] - TailMismatch { - expected: HashOf, - received: HashOf, - }, - #[display("announces len expected {expected}, received {received}")] - LenMismatch { expected: usize, received: usize }, - #[display("announces chain is not linked")] - ChainIsNotLinked, -} - -#[derive(Debug, derive_more::Display, derive_more::From)] -pub(crate) enum ResponseError { - #[display("{_0}")] - Hashes(HashesResponseError), - #[display("{_0}")] - ProgramIds(ProgramIdsResponseError), - #[display("{_0}")] - ValidCodes(ValidCodesResponseError), - #[display("{_0}")] - Announces(AnnouncesResponseError), - #[display("request and response types mismatch")] - TypeMismatch, -} - -#[derive(Debug, derive_more::Unwrap)] -pub(crate) enum ResponseHandlerResult { - Ok(Response), - NewRound(ResponseHandler), - Err(ResponseHandler, ResponseError), -} - -impl From> for ResponseHandlerResult { - fn from(value: Result) -> Self { - match value { - Ok(response) => Self::Ok(response), - Err((handler, error)) => Self::Err(handler, error), - } - } -} - -#[derive(Debug, Eq, PartialEq)] -pub(crate) enum ResponseHandler { - Hashes { - acc: InnerHashesResponse, - request: HashesRequest, - }, - ProgramIds { - request: ProgramIdsRequest, - }, - ValidCodes { - request: ValidCodesRequest, - }, - Announces { - request: AnnouncesRequest, - }, -} - -impl ResponseHandler { - fn new(request: Request) -> Self { - match request { - Request::Hashes(request) => Self::Hashes { - acc: Default::default(), - request, - }, - Request::ProgramIds(request) => Self::ProgramIds { request }, - Request::ValidCodes(request) => Self::ValidCodes { request }, - Request::Announces(request) => Self::Announces { request }, - } - } - - fn inner_request(&self) -> InnerRequest { - match self { - ResponseHandler::Hashes { - request: reduced_request, - .. - } => InnerRequest::Hashes(reduced_request.clone()), - ResponseHandler::ProgramIds { - request: - ProgramIdsRequest { - at, - expected_count: _, - }, - } => InnerRequest::ProgramIds(InnerProgramIdsRequest { at: *at }), - ResponseHandler::ValidCodes { - request: - ValidCodesRequest { - at: _, - validated_count: _, - }, - } => InnerRequest::ValidCodes, - ResponseHandler::Announces { request } => InnerRequest::Announces(*request), - } - } - - fn handle_hashes( - mut acc: InnerHashesResponse, - reduced_request: &HashesRequest, - new_response: InnerHashesResponse, - ) -> HashesResponseHandled { - let mut new_request = BTreeSet::new(); - let mut stripped = false; - - let diff = itertools::merge_join_by( - reduced_request.0.iter().copied(), - new_response.0, - |req_key, (resp_key, _resp_val)| req_key.cmp(resp_key), - ); - - for either in diff { - match either { - EitherOrBoth::Both(req_key, (resp_key, resp_val)) => { - debug_assert_eq!(req_key, resp_key); - if req_key != ethexe_db::hash(&resp_val) { - return HashesResponseHandled::Err { - acc, - err: HashesResponseError::HashMismatch, - stripped, - }; - } - - acc.0.insert(resp_key, resp_val); - } - EitherOrBoth::Left(key) => { - // peer was unable to give this key - new_request.insert(key); - } - EitherOrBoth::Right(_key) => { - // peer sent more keys than we requested - stripped = true; - } - } - } - - if new_request.is_empty() { - HashesResponseHandled::Done { - response: acc.0, - stripped, - } - } else { - HashesResponseHandled::IncompleteData { - acc, - new_request: HashesRequest(new_request), - stripped, - } - } - } - - async fn handle_program_ids( - response: InnerProgramIdsResponse, - request: &ProgramIdsRequest, - external_data_provider: Box, - ) -> Result, ProgramIdsResponseError> { - let InnerProgramIdsResponse(response) = response; - - if response.len() as u64 != request.expected_count { - return Err(ProgramIdsResponseError::NotEnoughIds { - expected: request.expected_count as usize, - received: response.len(), - }); - } - - let code_ids = external_data_provider - .programs_code_ids_at(response.clone(), request.at) - .await - .context("failed to get code ids at block") - .map_err(ProgramIdsResponseError::RouterQuery)?; - - let program_code_ids = iter::zip(response, code_ids).collect(); - Ok(program_code_ids) - } - - async fn handle_valid_codes( - response: BTreeSet, - request: &ValidCodesRequest, - external_data_provider: Box, - ) -> Result, ValidCodesResponseError> { - // validated count at specified block can be less than - // the number of states at the latest block returned by peer - // but cannot be more - if (response.len() as u64) < request.validated_count { - return Err(ValidCodesResponseError::NotEnoughCodes); - } - - let states = external_data_provider - .codes_states_at(response.clone(), request.at) - .await - .context("failed to get code states at block") - .map_err(ValidCodesResponseError::RouterQuery)?; - - let code_ids: BTreeSet = iter::zip(response, states) - .flat_map(|(code_id, state)| { - if state == CodeState::Validated { - Some(code_id) - } else { - None - } - }) - .collect(); - if request.validated_count != code_ids.len() as u64 { - return Err(ValidCodesResponseError::NotEnoughCodes); - } - - Ok(code_ids) - } - - pub(crate) fn handle_announces( - response: InnerAnnouncesResponse, - request: AnnouncesRequest, - ) -> AnnouncesResponseHandled { - let InnerAnnouncesResponse(announces) = response; - - let Some((first, last)) = announces.first().zip(announces.last()) else { - return AnnouncesResponseHandled::NewRound; - }; - - if request.head != last.to_hash() { - return AnnouncesResponseHandled::Err(AnnouncesResponseError::HeadMismatch { - expected: request.head, - received: last.to_hash(), - }); - } - - match request.until { - AnnouncesRequestUntil::Tail(request_tail_hash) => { - if request_tail_hash != first.parent { - return AnnouncesResponseHandled::Err(AnnouncesResponseError::TailMismatch { - expected: request_tail_hash, - received: first.parent, - }); - } - } - AnnouncesRequestUntil::ChainLen(len) => { - if announces.len() != len.get() as usize { - return AnnouncesResponseHandled::Err(AnnouncesResponseError::LenMismatch { - expected: len.get() as usize, - received: announces.len(), - }); - } - } - } - - // Check chain linking - let mut expected_parent_hash = first.parent; - for announce in announces.iter() { - if announce.parent != expected_parent_hash { - return AnnouncesResponseHandled::Err(AnnouncesResponseError::ChainIsNotLinked); - } - expected_parent_hash = announce.to_hash(); - } - - unsafe { AnnouncesResponseHandled::Done(AnnouncesResponse::from_parts(request, announces)) } - } - - async fn handle( - self, - peer: PeerId, - response: InnerResponse, - peer_score_handle: &Handle, - external_data_provider: Box, - ) -> ResponseHandlerResult { - match (self, response) { - ( - Self::Hashes { - acc, - request: reduced_request, - }, - InnerResponse::Hashes(response), - ) => { - let handled = Self::handle_hashes(acc, &reduced_request, response); - - if handled.stripped() { - log::debug!("data stripped in response from {peer}"); - peer_score_handle.excessive_data(peer); - } - - match handled { - HashesResponseHandled::Done { - response, - stripped: _, - } => ResponseHandlerResult::Ok(Response::Hashes(response)), - HashesResponseHandled::IncompleteData { - acc, - new_request, - stripped: _, - } => ResponseHandlerResult::NewRound(Self::Hashes { - acc, - request: new_request, - }), - HashesResponseHandled::Err { - acc, - err, - stripped: _, - } => ResponseHandlerResult::Err( - Self::Hashes { - acc, - request: reduced_request, - }, - err.into(), - ), - } - } - (Self::ProgramIds { request }, InnerResponse::ProgramIds(response)) => { - Self::handle_program_ids(response, &request, external_data_provider) - .await - .map(Into::into) - .map_err(|err| (Self::ProgramIds { request }, err.into())) - .into() - } - (Self::ValidCodes { request }, InnerResponse::ValidCodes(response)) => { - Self::handle_valid_codes(response, &request, external_data_provider) - .await - .map(Into::into) - .map_err(|err| (Self::ValidCodes { request }, err.into())) - .into() - } - (Self::Announces { request }, InnerResponse::Announces(response)) => { - let handled = Self::handle_announces(response, request); - - match handled { - AnnouncesResponseHandled::Done(response) => { - ResponseHandlerResult::Ok(Response::Announces(response)) - } - AnnouncesResponseHandled::NewRound => { - ResponseHandlerResult::NewRound(Self::Announces { request }) - } - AnnouncesResponseHandled::Err(err) => { - ResponseHandlerResult::Err(Self::Announces { request }, err.into()) - } - } - } - (this, _) => ResponseHandlerResult::Err(this, ResponseError::TypeMismatch), - } - } -} - -#[derive(Debug)] -struct OngoingRequest { - response_handler: Option, - tried_peers: HashSet, -} - -impl OngoingRequest { - fn new(request: Request) -> Self { - Self { - response_handler: Some(ResponseHandler::new(request)), - tried_peers: Default::default(), - } - } - - async fn choose_next_peer(&mut self) -> PeerId { - let mut event_sent = None; - CONTEXT - .poll_fn(|_task_cx, ctx| { - if ctx.peers.is_empty() { - event_sent.get_or_insert_with(|| { - ctx.state - .set(OngoingRequestState::NoPeers) - .expect("set only once"); - }); - return Poll::Pending; - } - - loop { - let peer = ctx - .peers - .difference(&self.tried_peers) - .choose_stable(&mut rand::thread_rng()) - .copied(); - - if let Some(peer) = peer { - self.tried_peers.insert(peer); - break Poll::Ready(peer); - } else { - // just retry all peers again - self.tried_peers.clear(); - continue; - } - } - }) - .await - } - - async fn send_request(&mut self, peer: PeerId) -> Result { - CONTEXT.with_mut(|ctx| { - ctx.state - .set(OngoingRequestState::SendRequest( - peer, - self.response_handler - .as_ref() - .expect("always Some") - .inner_request(), - )) - .expect("set only once"); - }); - - CONTEXT - .poll_fn(|_task_cx, ctx| { - if let Some(res) = ctx.response.take() { - Poll::Ready(res) - } else { - Poll::Pending - } - }) - .await - } - - async fn next_round( - &mut self, - peer_score_handle: &Handle, - external_data_provider: Box, - ) -> Result { - let peer = self.choose_next_peer().await; - - let response = self.send_request(peer).await?; - - match self - .response_handler - .take() - .expect("always Some") - .handle(peer, response, peer_score_handle, external_data_provider) - .await - { - ResponseHandlerResult::Ok(response) => Ok(response), - ResponseHandlerResult::NewRound(handler) => { - log::trace!( - "response is incomplete from peer {peer}: we are going for a new round" - ); - self.response_handler = Some(handler); - Err(()) - } - ResponseHandlerResult::Err(handler, err) => { - log::warn!("response processing failed for request from {peer}: {err:?}"); - peer_score_handle.invalid_data(peer); - self.response_handler = Some(handler); - Err(()) - } - } - } - - async fn request( - mut self, - peer_score_handle: Handle, - external_data_provider: Box, - request_timeout: Duration, - ) -> Result { - let request_loop = async { - loop { - match self - .next_round(&peer_score_handle, external_data_provider.clone_boxed()) - .await - { - Ok(response) => break Ok(response), - Err(()) => continue, - }; - } - }; - - let res = time::timeout(request_timeout, request_loop) - .await - .map_err(|_elapsed| RequestFailure::Timeout) - .and_then(|res| res); - res.map_err(|failure| (failure, self)) - } -} - -#[derive(Debug)] -enum OngoingRequestState { - NoPeers, - SendRequest(PeerId, InnerRequest), -} - -struct OngoingRequestContext { - state: OnceCell, - peers: HashSet, - response: Option>, -} - -impl OngoingRequestContext { - fn into_state(self) -> Option { - let Self { - state, - peers: _, - response, - } = self; - let state = state.into_inner(); - debug_assert_eq!(response, None, "future must take provided response"); - state - } -} - -#[derive(Debug)] -pub struct RetriableRequest { - request_id: RequestId, - request: OngoingRequest, -} - -impl PartialEq for RetriableRequest { - fn eq(&self, other: &Self) -> bool { - self.request_id == other.request_id - } -} - -impl Eq for RetriableRequest {} - -impl RetriableRequest { - pub fn id(&self) -> RequestId { - self.request_id - } -} - -#[cfg(test)] -mod tests { - use super::*; - use async_trait::async_trait; - - struct UnreachableExternalDataProvider; - - #[async_trait] - impl ExternalDataProvider for UnreachableExternalDataProvider { - fn clone_boxed(&self) -> Box { - unreachable!() - } - - async fn programs_code_ids_at( - self: Box, - _program_ids: BTreeSet, - _block: H256, - ) -> anyhow::Result> { - unreachable!() - } - - async fn codes_states_at( - self: Box, - _code_ids: BTreeSet, - _block: H256, - ) -> anyhow::Result> { - unreachable!() - } - } - - fn make_chain(len: usize) -> Vec { - assert!(len > 0); - let mut chain = Vec::with_capacity(len); - let mut parent = HashOf::zero(); - - for idx in 0..len { - let announce = Announce::base(H256([idx as u8 + 1; 32]), parent); - parent = announce.to_hash(); - chain.push(announce); - } - - chain - } - - #[test] - fn validate_data_stripped() { - let hash1 = ethexe_db::hash(b"1"); - let hash2 = ethexe_db::hash(b"2"); - let hash3 = ethexe_db::hash(b"3"); - - let request = HashesRequest([hash1, hash2].into()); - let response = InnerHashesResponse( - [ - (hash1, b"1".to_vec()), - (hash2, b"2".to_vec()), - (hash3, b"3".to_vec()), - ] - .into(), - ); - let processed = ResponseHandler::handle_hashes(Default::default(), &request, response); - let HashesResponseHandled::Done { response, stripped } = processed else { - unreachable!("{processed:?}") - }; - assert_eq!( - response, - BTreeMap::from_iter([(hash1, b"1".to_vec()), (hash2, b"2".to_vec())]) - ); - assert!(stripped); - } - - #[test] - fn validate_data_hash_mismatch() { - let hash1 = ethexe_db::hash(b"1"); - - let request = HashesRequest([hash1].into()); - let response = InnerHashesResponse([(hash1, b"2".to_vec())].into()); - let processed = ResponseHandler::handle_hashes(Default::default(), &request, response); - let HashesResponseHandled::Err { acc, err, stripped } = processed else { - unreachable!("{processed:?}") - }; - assert_eq!(acc, Default::default()); - assert_eq!(err, HashesResponseError::HashMismatch); - assert!(!stripped); - } - - #[tokio::test] - async fn validate_data_hash_incomplete() { - let hash1 = ethexe_db::hash(b"1"); - let hash2 = ethexe_db::hash(b"2"); - - let request = HashesRequest([hash1, hash2].into()); - let response = InnerHashesResponse([(hash1, b"1".to_vec())].into()); - let processed = - ResponseHandler::handle_hashes(Default::default(), &request, response.clone()); - let HashesResponseHandled::IncompleteData { - acc, - new_request, - stripped, - } = processed - else { - unreachable!("{processed:?}") - }; - assert_eq!(acc, response); - assert_eq!(new_request, HashesRequest([hash2].into())); - assert!(!stripped); - - let handler = ResponseHandler::new(request.into()); - handler - .handle( - PeerId::random(), - response.into(), - &Handle::new_test(), - Box::new(UnreachableExternalDataProvider), - ) - .await - .unwrap_new_round(); - } - - #[test] - fn try_into_checked_accepts_valid_tail_range() { - let announces = make_chain(3); - let head_hash = announces.last().unwrap().to_hash(); - let tail_hash = announces.first().unwrap().parent; - - let request = AnnouncesRequest { - head: head_hash, - until: AnnouncesRequestUntil::Tail(tail_hash), - }; - let response = InnerAnnouncesResponse(announces.clone()); - - let response = ResponseHandler::handle_announces(response, request).unwrap_done(); - assert_eq!(response.request(), &request); - assert_eq!(response.announces(), announces.as_slice()); - } - - #[test] - fn try_into_checked_accepts_valid_chain_len() { - let announces = make_chain(4); - let head_hash = announces.last().unwrap().to_hash(); - - let request = AnnouncesRequest { - head: head_hash, - until: AnnouncesRequestUntil::ChainLen((announces.len() as u32).try_into().unwrap()), - }; - - let response = InnerAnnouncesResponse(announces.clone()); - - let response = ResponseHandler::handle_announces(response, request).unwrap_done(); - assert_eq!(response.request(), &request); - assert_eq!(response.announces(), announces.as_slice()); - } - - #[tokio::test] - async fn try_into_checked_rejects_empty_response() { - let request = AnnouncesRequest { - head: HashOf::zero(), - until: AnnouncesRequestUntil::ChainLen(1.try_into().unwrap()), - }; - - let response = InnerAnnouncesResponse(Vec::new()); - - ResponseHandler::handle_announces(response.clone(), request).unwrap_new_round(); - - let handler = ResponseHandler::new(request.into()); - handler - .handle( - PeerId::random(), - response.into(), - &Handle::new_test(), - Box::new(UnreachableExternalDataProvider), - ) - .await - .unwrap_new_round(); - } - - #[test] - fn try_into_checked_rejects_head_mismatch() { - let announces = make_chain(2); - let actual_head = announces.last().unwrap().to_hash(); - let wrong_head = HashOf::random(); - let tail_hash = announces.first().unwrap().parent; - - let request = AnnouncesRequest { - head: wrong_head, - until: AnnouncesRequestUntil::Tail(tail_hash), - }; - let response = InnerAnnouncesResponse(announces); - - let err = ResponseHandler::handle_announces(response, request).unwrap_err(); - assert_eq!( - err, - AnnouncesResponseError::HeadMismatch { - expected: wrong_head, - received: actual_head, - } - ); - } - - #[test] - fn try_into_checked_rejects_tail_mismatch() { - let announces = make_chain(3); - let actual_tail = announces.first().unwrap().parent; - let head_hash = announces.last().unwrap().to_hash(); - let wrong_tail = HashOf::random(); - - let request = AnnouncesRequest { - head: head_hash, - until: AnnouncesRequestUntil::Tail(wrong_tail), - }; - let response = InnerAnnouncesResponse(announces); - - let err = ResponseHandler::handle_announces(response, request).unwrap_err(); - assert_eq!( - err, - AnnouncesResponseError::TailMismatch { - expected: wrong_tail, - received: actual_tail, - } - ); - } - - #[test] - fn try_into_checked_rejects_len_mismatch() { - let announces = make_chain(2); - let head_hash = announces.last().unwrap().to_hash(); - - let request = AnnouncesRequest { - head: head_hash, - until: AnnouncesRequestUntil::ChainLen(3.try_into().unwrap()), - }; - let response = InnerAnnouncesResponse(announces); - - let err = ResponseHandler::handle_announces(response, request).unwrap_err(); - assert_eq!( - err, - AnnouncesResponseError::LenMismatch { - expected: 3, - received: 2, - } - ); - } - - #[test] - fn try_into_checked_rejects_non_linked_chain() { - let mut announces = make_chain(3); - announces[1].parent = HashOf::zero(); - let head_hash = announces.last().unwrap().to_hash(); - let tail_hash = announces.first().unwrap().parent; - - let request = AnnouncesRequest { - head: head_hash, - until: AnnouncesRequestUntil::Tail(tail_hash), - }; - let response = InnerAnnouncesResponse(announces); - - let err = ResponseHandler::handle_announces(response, request).unwrap_err(); - assert_eq!(err, AnnouncesResponseError::ChainIsNotLinked); - } -} diff --git a/ethexe/network/src/db_sync/responses.rs b/ethexe/network/src/db_sync/responses.rs deleted file mode 100644 index b79d44bbb61..00000000000 --- a/ethexe/network/src/db_sync/responses.rs +++ /dev/null @@ -1,552 +0,0 @@ -// This file is part of Gear. -// -// Copyright (C) 2024-2025 Gear Technologies Inc. -// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -use crate::{ - db_sync::{ - Config, DbSyncDatabase, InnerAnnouncesResponse, InnerBehaviour, InnerHashesResponse, - InnerProgramIdsResponse, InnerRequest, InnerResponse, ResponseId, - }, - export::PeerId, - utils::ParityScaleCodec, -}; -use ethexe_common::{ - Announce, HashOf, - db::{AnnounceStorageRO, ConfigStorageRO, GlobalsStorageRO}, - network::{AnnouncesRequest, AnnouncesRequestUntil}, -}; -use libp2p::request_response; -use parity_scale_codec::{Compact, Encode}; -use std::{ - collections::{BTreeMap, VecDeque}, - num::NonZeroU32, - task::{Context, Poll}, -}; -use thiserror::Error; -use tokio::task::JoinSet; - -struct OngoingResponse { - response_id: ResponseId, - peer_id: PeerId, - channel: request_response::ResponseChannel, - response: InnerResponse, -} - -pub(crate) struct OngoingResponses { - response_id_counter: u64, - db: Box, - db_readers: JoinSet, - max_simultaneous_responses: u32, - max_chain_len_for_announces_response: NonZeroU32, -} - -impl OngoingResponses { - pub(crate) fn new(db: Box, config: &Config) -> Self { - Self { - response_id_counter: 0, - db, - db_readers: JoinSet::new(), - max_simultaneous_responses: config.max_simultaneous_responses, - max_chain_len_for_announces_response: config.max_chain_len_for_announces_response, - } - } - - fn next_response_id(&mut self) -> ResponseId { - let id = self.response_id_counter; - self.response_id_counter += 1; - ResponseId(id) - } - - fn response_from_db( - request: InnerRequest, - db: Box, - max_chain_len_for_announces_response: NonZeroU32, - ) -> InnerResponse { - const MAX_RESPONSE_SIZE: u64 = ParityScaleCodec::<(), ()>::MAX_RESPONSE_SIZE; - - match request { - InnerRequest::Hashes(request) => { - let mut response = BTreeMap::new(); - let mut entries_size = 0; - - for hash in request.0 { - let Some(data) = db.read_by_hash(hash) else { - continue; - }; - - let entry_size = hash.encoded_size() + data.encoded_size(); - let next_response_size = 1 // InnerResponse discriminant size - + Compact((response.len() + 1) as u64).encoded_size() - + entries_size - + entry_size; - - if next_response_size > MAX_RESPONSE_SIZE as usize { - // don't try to put other hashes data to prevent abusive database reads - break; - } - - entries_size += entry_size; - response.insert(hash, data); - } - - InnerHashesResponse(response).into() - } - InnerRequest::ProgramIds(request) => InnerProgramIdsResponse( - db.block_announces(request.at) - .into_iter() - .flatten() - .find_map(|announce_hash| db.announce_program_states(announce_hash)) - .map(|states| states.into_keys().collect()) - .unwrap_or_else(|| { - log::warn!("no program states found for block {:?}", request.at); - Default::default() - }), // FIXME: Option might be more suitable - ) - .into(), - InnerRequest::ValidCodes => db.valid_codes().into(), - InnerRequest::Announces(request) => { - match Self::process_announce_request( - &db, - request, - max_chain_len_for_announces_response, - ) { - Ok(response) => response.into(), - Err(e) => { - log::trace!("cannot complete announces request {request:?}: {e}"); - InnerResponse::Announces(Default::default()) - } - } - } - } - } - - fn process_announce_request( - db: &DB, - request: AnnouncesRequest, - max_chain_len_for_announces_response: NonZeroU32, - ) -> Result { - let AnnouncesRequest { head, until } = request; - - // Check the requested chain length first to prevent abuse - if let AnnouncesRequestUntil::ChainLen(len) = until - && len > max_chain_len_for_announces_response - { - // TODO #4874: use peer score to punish the peer for such requests - return Err(ProcessAnnounceError::ChainLenExceedsMax { - requested: len, - max_allowed: max_chain_len_for_announces_response, - }); - } - - let genesis_announce_hash = db.config().genesis_announce_hash; - let start_announce_hash = db.globals().start_announce_hash; - - let mut announces = VecDeque::new(); - let mut announce_hash = head; - for _ in 0..max_chain_len_for_announces_response.get() { - match until { - AnnouncesRequestUntil::Tail(tail) if announce_hash == tail => { - return Ok(InnerAnnouncesResponse(announces.into())); - } - AnnouncesRequestUntil::ChainLen(len) if announces.len() == len.get() as usize => { - return Ok(InnerAnnouncesResponse(announces.into())); - } - _ => {} - } - - if announce_hash == start_announce_hash { - if start_announce_hash == genesis_announce_hash { - // Reaching genesis - request is invalid and should be punished. - // TODO #4874: use peer score to punish the peer for such requests - return Err(ProcessAnnounceError::ReachedGenesis { - genesis: genesis_announce_hash, - }); - } else { - // Reaching start announce - request can be valid, we just can't go further - return Err(ProcessAnnounceError::ReachedStart { - start: start_announce_hash, - }); - } - } - - let Some(announce) = db.announce(announce_hash) else { - return Err(ProcessAnnounceError::AnnounceMissing { - hash: announce_hash, - }); - }; - announce_hash = announce.parent; - announces.push_front(announce); - } - - // TODO #4874: use peer score to punish the peer for such requests - Err(ProcessAnnounceError::ReachedMaxChainLength { - max_allowed: max_chain_len_for_announces_response, - }) - } - - pub(crate) fn handle_response( - &mut self, - peer_id: PeerId, - channel: request_response::ResponseChannel, - request: InnerRequest, - ) -> Option { - if self.db_readers.len() >= self.max_simultaneous_responses as usize { - return None; - } - - let response_id = self.next_response_id(); - - let db = self.db.clone_boxed(); - let max_chain_len_for_announces_response = self.max_chain_len_for_announces_response; - self.db_readers.spawn_blocking(move || { - let response = - Self::response_from_db(request, db, max_chain_len_for_announces_response); - OngoingResponse { - response_id, - peer_id, - channel, - response, - } - }); - - Some(response_id) - } - - pub(crate) fn poll( - &mut self, - cx: &mut Context<'_>, - behaviour: &mut InnerBehaviour, - ) -> Poll<(PeerId, ResponseId)> { - if let Poll::Ready(Some(res)) = self.db_readers.poll_join_next(cx) { - let OngoingResponse { - response_id, - peer_id, - channel, - response, - } = res.expect("database panicked"); - let _res = behaviour.send_response(channel, response); - Poll::Ready((peer_id, response_id)) - } else { - Poll::Pending - } - } -} - -#[derive(Debug, Error, PartialEq, Eq)] -enum ProcessAnnounceError { - #[error("requested chain length {requested} exceeds maximum allowed {max_allowed}")] - ChainLenExceedsMax { - requested: NonZeroU32, - max_allowed: NonZeroU32, - }, - #[error("announce {hash} not found in database")] - AnnounceMissing { hash: HashOf }, - #[error("reached genesis announce {genesis}")] - ReachedGenesis { genesis: HashOf }, - #[error("reached start announce {start}")] - ReachedStart { start: HashOf }, - #[error("reached maximum chain length {max_allowed}")] - ReachedMaxChainLength { max_allowed: NonZeroU32 }, -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, - db_sync::{HashesRequest, requests::ResponseHandler}, - }; - use ethexe_common::{ - Announce, HashOf, ProtocolTimelines, - db::{AnnounceStorageRW, DBConfig, GlobalsStorageRW, SetConfig}, - }; - use ethexe_db::Database; - use gprimitives::H256; - use std::num::{NonZeroU32, NonZeroU64}; - - fn make_announce(block: u64, parent: HashOf) -> Announce { - Announce::base(H256::from_low_u64_be(block), parent) - } - - fn set_db_data(db: &Database, genesis: HashOf, start: HashOf) { - db.set_config(DBConfig { - version: 0, - chain_id: 0, - router_address: Default::default(), - timelines: ProtocolTimelines { - genesis_ts: 0, - era: NonZeroU64::new(1).unwrap(), - election: 0, - slot: NonZeroU64::new(1).unwrap(), - }, - genesis_block_hash: H256::zero(), - genesis_announce_hash: genesis, - max_validators: 0, - }); - - db.globals_mutate(|globals| globals.start_announce_hash = start); - } - - #[test] - fn response_from_db_truncates_hashes_response_at_encoded_limit() { - const ENTRIES_BEFORE_COMPACT_BOUNDARY: u64 = 0b0011_1111; - const MAX_RESPONSE_SIZE: usize = ParityScaleCodec::<(), ()>::MAX_RESPONSE_SIZE as usize; - - let db = Database::memory(); - - let entries = (0..ENTRIES_BEFORE_COMPACT_BOUNDARY as u8) - .map(|i| vec![i]) - .collect::>(); - let entries_size = entries - .iter() - .map(|data| H256::zero().encoded_size() + data.encoded_size()) - .sum::(); - for data in &entries { - db.cas().write(data); - } - - let last_entry_size = MAX_RESPONSE_SIZE - - 1 // `InnerResponse` discriminant - - Compact(ENTRIES_BEFORE_COMPACT_BOUNDARY + 1).encoded_size() - - entries_size - - H256::zero().encoded_size(); - let last_entry = vec![42; last_entry_size]; - let last_entry_hash = db.cas().write(&last_entry); - - let request = entries - .iter() - .map(|data| ethexe_db::hash(data)) - .chain(Some(last_entry_hash)) - .collect(); - let response = OngoingResponses::response_from_db( - HashesRequest(request).into(), - Box::new(db), - DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, - ); - - let response = response.unwrap_hashes(); - assert_eq!(response.0.len(), ENTRIES_BEFORE_COMPACT_BOUNDARY as usize); - assert!(InnerResponse::Hashes(response).encoded_size() <= MAX_RESPONSE_SIZE); - } - - #[test] - fn fails_chain_len_exceeding_max() { - let db = Database::memory(); - set_db_data(&db, HashOf::zero(), HashOf::zero()); - - let len = DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE - .checked_add(1) - .unwrap(); - let request = AnnouncesRequest { - head: HashOf::zero(), - until: AnnouncesRequestUntil::ChainLen(len), - }; - - let err = OngoingResponses::process_announce_request( - &db, - request, - DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, - ) - .unwrap_err(); - assert_eq!( - err, - ProcessAnnounceError::ChainLenExceedsMax { - requested: len, - max_allowed: DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, - } - ); - } - - #[test] - fn fails_announce_missing() { - let head = HashOf::random(); - let db = Database::memory(); - set_db_data(&db, HashOf::zero(), HashOf::zero()); - - let request = AnnouncesRequest { - head, - until: AnnouncesRequestUntil::Tail(HashOf::zero()), - }; - - let err = OngoingResponses::process_announce_request( - &db, - request, - DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, - ) - .unwrap_err(); - assert_eq!(err, ProcessAnnounceError::AnnounceMissing { hash: head }); - } - - #[test] - fn fails_when_reaching_genesis() { - let db = Database::memory(); - - let genesis_announce = make_announce(10, HashOf::random()); - let genesis = db.set_announce(genesis_announce); - let middle = make_announce(11, genesis); - let middle_hash = db.set_announce(middle.clone()); - let head = make_announce(12, middle_hash); - let head_hash = db.set_announce(head.clone()); - - set_db_data(&db, genesis, genesis); - - let request = AnnouncesRequest { - head: head_hash, - until: AnnouncesRequestUntil::Tail(HashOf::random()), - }; - - let err = OngoingResponses::process_announce_request( - &db, - request, - DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, - ) - .unwrap_err(); - assert_eq!(err, ProcessAnnounceError::ReachedGenesis { genesis }); - } - - #[test] - fn fails_reaching_start_non_genesis() { - let db = Database::memory(); - let start_announce = make_announce(10, HashOf::random()); - let start = db.set_announce(start_announce); - let genesis = HashOf::random(); - - set_db_data(&db, genesis, start); - - let head = make_announce(11, start); - let head_hash = db.set_announce(head); - - let request = AnnouncesRequest { - head: head_hash, - until: AnnouncesRequestUntil::Tail(HashOf::random()), - }; - - let err = OngoingResponses::process_announce_request( - &db, - request, - DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, - ) - .unwrap_err(); - assert_eq!(err, ProcessAnnounceError::ReachedStart { start }); - } - - #[test] - fn fails_reaching_max_chain_length() { - let db = Database::memory(); - - let mut parent = HashOf::random(); - let mut head_hash = parent; - let mut chain_hashes = Vec::new(); - - for i in 0..DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE.get() { - let announce = make_announce(10_000 + i as u64, parent); - let hash = db.set_announce(announce); - chain_hashes.push(hash); - parent = hash; - head_hash = hash; - } - - let start = HashOf::random(); - let genesis = HashOf::random(); - let tail = HashOf::random(); - - assert!(!chain_hashes.contains(&start)); - assert!(!chain_hashes.contains(&genesis)); - assert!(!chain_hashes.contains(&tail)); - - set_db_data(&db, genesis, start); - - let request = AnnouncesRequest { - head: head_hash, - until: AnnouncesRequestUntil::Tail(tail), - }; - - let err = OngoingResponses::process_announce_request( - &db, - request, - DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, - ) - .unwrap_err(); - assert_eq!( - err, - ProcessAnnounceError::ReachedMaxChainLength { - max_allowed: DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, - } - ); - } - - #[test] - fn returns_announces_until_tail() { - let db = Database::memory(); - - let tail = make_announce(10, HashOf::random()); - let tail_hash = db.set_announce(tail.clone()); - let middle = make_announce(11, tail_hash); - let middle_hash = db.set_announce(middle.clone()); - let head = make_announce(12, middle_hash); - let head_hash = db.set_announce(head.clone()); - - let genesis = HashOf::random(); - let start = HashOf::random(); - set_db_data(&db, genesis, start); - - let request = AnnouncesRequest { - head: head_hash, - until: AnnouncesRequestUntil::Tail(tail_hash), - }; - - let response = OngoingResponses::process_announce_request( - &db, - request, - DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, - ) - .unwrap(); - assert_eq!(response.0, vec![middle, head]); - ResponseHandler::handle_announces(response, request).unwrap_done(); - } - - #[test] - fn returns_announces_until_chain_len() { - let db = Database::memory(); - - let tail = make_announce(10, HashOf::random()); - let tail_hash = db.set_announce(tail.clone()); - let middle = make_announce(11, tail_hash); - let middle_hash = db.set_announce(middle.clone()); - let head = make_announce(12, middle_hash); - let head_hash = db.set_announce(head.clone()); - - let genesis = HashOf::random(); - let start = HashOf::random(); - set_db_data(&db, genesis, start); - - let length = NonZeroU32::new(2).unwrap(); - let request = AnnouncesRequest { - head: head_hash, - until: AnnouncesRequestUntil::ChainLen(length), - }; - - let response = OngoingResponses::process_announce_request( - &db, - request, - DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, - ) - .unwrap(); - assert_eq!(response.0, vec![middle, head]); - ResponseHandler::handle_announces(response, request).unwrap_done(); - } -} diff --git a/ethexe/network/src/gossipsub.rs b/ethexe/network/src/gossipsub.rs index cf1ee3e5bbc..7ab1e45209a 100644 --- a/ethexe/network/src/gossipsub.rs +++ b/ethexe/network/src/gossipsub.rs @@ -18,13 +18,11 @@ pub(crate) use libp2p::gossipsub::*; -use crate::{ - db_sync::{Multiaddr, PeerId}, - peer_score, -}; +use crate::peer_score; use anyhow::anyhow; use ethexe_common::{Address, injected::SignedCompactPromise, network::SignedValidatorMessage}; use libp2p::{ + Multiaddr, PeerId, core::{Endpoint, transport::PortUse}, gossipsub, identity::Keypair, diff --git a/ethexe/network/src/injected.rs b/ethexe/network/src/injected.rs index 0acd18c7c66..6f4777886fb 100644 --- a/ethexe/network/src/injected.rs +++ b/ethexe/network/src/injected.rs @@ -16,11 +16,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::{ - db_sync::{Multiaddr, PeerId}, - utils::ParityScaleCodec, - validator::discovery::ValidatorIdentities, -}; +use crate::{utils::ParityScaleCodec, validator::discovery::ValidatorIdentities}; use ethexe_common::{ Address, HashOf, injected::{ @@ -30,7 +26,7 @@ use ethexe_common::{ }; use futures::{FutureExt, StreamExt, future::BoxFuture, stream::FuturesUnordered}; use libp2p::{ - StreamProtocol, + Multiaddr, PeerId, StreamProtocol, core::{Endpoint, transport::PortUse}, request_response, request_response::{ diff --git a/ethexe/network/src/lib.rs b/ethexe/network/src/lib.rs index ecf41d69aa3..a39146f2c93 100644 --- a/ethexe/network/src/lib.rs +++ b/ethexe/network/src/lib.rs @@ -33,7 +33,7 @@ //! hands out protocol-specific handles such as [`db_sync::Handle`] for //! database synchronization and a peer-scoring handle for internal use. -pub mod db_sync; +mod bitswap; mod gossipsub; mod injected; mod kad; @@ -50,7 +50,6 @@ pub mod export { pub use injected::Event as NetworkInjectedEvent; use crate::{ - db_sync::DbSyncDatabase, utils::MultiaddrExt, validator::{ValidatorDatabase, list::ValidatorListSnapshot}, }; @@ -201,8 +200,6 @@ pub struct NetworkRuntimeConfig { pub general_signer: Signer, /// Signer used only to construct the libp2p networking keypair. pub network_signer: Signer, - /// External lookups needed by db-sync request validation. - pub external_data_provider: Box, /// Database backing validator discovery and db-sync responses. pub db: Database, } @@ -276,7 +273,6 @@ impl NetworkService { validator_key, general_signer, network_signer, - external_data_provider, db, } = runtime_config; @@ -299,8 +295,7 @@ impl NetworkService { let behaviour_config = BehaviourConfig { router_address, keypair: keypair.clone(), - external_data_provider, - db: DbSyncDatabase::clone_boxed(&db), + db: bitswap::BlockstoreDatabase::clone_boxed(&db), transport_type, validator_key, general_signer, @@ -451,7 +446,7 @@ impl NetworkService { } BehaviourEvent::Kad(event) => self.handle_kad_event(event), BehaviourEvent::Gossipsub(event) => return self.handle_gossipsub_event(event), - BehaviourEvent::DbSync(_event) => {} + BehaviourEvent::Bitswap(_event) => {} BehaviourEvent::Injected(event) => return self.handle_injected_event(event), BehaviourEvent::ValidatorDiscovery(event) => { return self.handle_validator_discovery_event(event); @@ -630,9 +625,9 @@ impl NetworkService { self.swarm.behaviour().peer_score.handle() } - /// Handle used by external services to start db-sync requests. - pub fn db_sync_handle(&self) -> db_sync::Handle { - self.swarm.behaviour().db_sync.handle() + /// Handle used by external services to start Bitswap requests. + pub fn bitswap_handle(&self) -> bitswap::Handle { + self.swarm.behaviour().bitswap.handle() } /// Refresh validator-era state after the chain head changes. @@ -702,8 +697,7 @@ impl NetworkService { struct BehaviourConfig<'a> { router_address: Address, keypair: identity::Keypair, - external_data_provider: Box, - db: Box, + db: Box, transport_type: TransportType, validator_key: Option, general_signer: Signer, @@ -734,8 +728,8 @@ pub(crate) struct Behaviour { pub kad: kad::Behaviour, // general communication pub gossipsub: gossipsub::Behaviour, - // database synchronization protocol - pub db_sync: db_sync::Behaviour, + // data request protocol + pub bitswap: bitswap::Behaviour, // injected transaction shenanigans pub injected: injected::Behaviour, // validator discovery @@ -747,7 +741,6 @@ impl Behaviour { let BehaviourConfig { router_address, keypair, - external_data_provider, db, transport_type, validator_key, @@ -805,15 +798,7 @@ impl Behaviour { ) .map_err(|e| anyhow!("`gossipsub::Behaviour` error: {e}"))?; - let db_sync = db_sync::Behaviour::new( - db_sync::Config { - max_chain_len_for_announces_response, - ..Default::default() - }, - peer_score_handle.clone(), - external_data_provider, - db, - ); + let bitswap = bitswap::Behaviour::new(db); let injected = injected::Behaviour::new(); @@ -836,7 +821,7 @@ impl Behaviour { mdns4, kad, gossipsub, - db_sync, + bitswap, injected, validator_discovery, }) @@ -846,95 +831,22 @@ impl Behaviour { #[cfg(test)] mod tests { use super::*; - use crate::{ - db_sync::{ExternalDataProvider, tests::fill_data_provider}, - utils::tests::{arb_value, init_logger}, - }; + use crate::utils::tests::{arb_value, init_logger}; use assert_matches::assert_matches; - use async_trait::async_trait; - use ethexe_common::{BlockHeader, ProtocolTimelines, db::*, gear::CodeState}; + use ethexe_common::{BlockHeader, ProtocolTimelines, db::*}; use ethexe_db::Database; - use gprimitives::{ActorId, CodeId, H256}; + use futures::future; + use gprimitives::H256; use gsigner::secp256k1::Signer; use nonempty::nonempty; - use std::{ - collections::{BTreeSet, HashMap}, - future, - num::NonZeroU64, - sync::Arc, - }; + use std::num::NonZeroU64; use tokio::{ - sync::RwLock, time, time::{Duration, timeout}, }; - #[derive(Default)] - struct DataProviderInner { - programs_code_ids_at: HashMap<(BTreeSet, H256), Vec>, - code_states_at: HashMap<(BTreeSet, H256), Vec>, - } - - #[derive(Default, Clone)] - pub struct DataProvider(Arc>); - - impl DataProvider { - pub async fn set_programs_code_ids_at( - &self, - program_ids: BTreeSet, - at: H256, - code_ids: Vec, - ) { - self.0 - .write() - .await - .programs_code_ids_at - .insert((program_ids, at), code_ids); - } - } - - #[async_trait] - impl ExternalDataProvider for DataProvider { - fn clone_boxed(&self) -> Box { - Box::new(self.clone()) - } - - async fn programs_code_ids_at( - self: Box, - program_ids: BTreeSet, - block: H256, - ) -> anyhow::Result> { - assert!(!program_ids.is_empty()); - Ok(self - .0 - .read() - .await - .programs_code_ids_at - .get(&(program_ids, block)) - .cloned() - .unwrap_or_default()) - } - - async fn codes_states_at( - self: Box, - code_ids: BTreeSet, - block: H256, - ) -> anyhow::Result> { - assert!(!code_ids.is_empty()); - Ok(self - .0 - .read() - .await - .code_states_at - .get(&(code_ids, block)) - .cloned() - .unwrap_or_default()) - } - } - struct NetworkServiceBuilder { db: Database, - data_provider: DataProvider, latest_validators: ValidatorsVec, signer: Signer, validator_key: Option, @@ -944,7 +856,6 @@ mod tests { fn new() -> Self { Self { db: Database::memory(), - data_provider: DataProvider::default(), latest_validators: nonempty![Address::default()].into(), signer: Signer::memory(), validator_key: None, @@ -966,7 +877,6 @@ mod tests { let Self { db, - data_provider, latest_validators, signer, validator_key, @@ -986,7 +896,6 @@ mod tests { validator_key, general_signer: signer.clone(), network_signer: signer, - external_data_provider: Box::new(data_provider), db, }; @@ -1013,7 +922,7 @@ mod tests { init_logger(); let mut service1 = new_service(); - let service1_handle = service1.db_sync_handle(); + let service1_handle = service1.bitswap_handle(); // second service let service2 = NetworkServiceBuilder::new(); @@ -1027,17 +936,15 @@ mod tests { tokio::spawn(service1.loop_on_next()); tokio::spawn(service2.loop_on_next()); - let request = service1_handle.request(db_sync::Request::hashes([hello, world])); - let response = timeout(Duration::from_secs(5), request) - .await - .expect("time has elapsed") - .unwrap(); - assert_eq!( - response, - db_sync::Response::Hashes( - [(hello, b"hello".to_vec()), (world, b"world".to_vec())].into() - ) - ); + let hello_request = service1_handle.request(hello); + let world_request = service1_handle.request(world); + let response = timeout( + Duration::from_secs(5), + future::join(hello_request, world_request), + ) + .await + .expect("time has elapsed"); + assert_eq!(response, (b"hello".to_vec(), b"world".to_vec())); } #[tokio::test] @@ -1065,33 +972,6 @@ mod tests { assert_matches!(event, NetworkEvent::PeerBlocked(peer_id) if peer_id == service2_peer_id); } - #[tokio::test] - async fn external_data_provider() { - init_logger(); - - let alice = NetworkServiceBuilder::new(); - let alice_data_provider = alice.data_provider.clone(); - let mut alice = alice.build(); - let alice_handle = alice.db_sync_handle(); - - let bob = NetworkServiceBuilder::new(); - let bob_db = bob.db.clone(); - let mut bob = bob.build(); - - alice.connect(&mut bob).await; - tokio::spawn(alice.loop_on_next()); - tokio::spawn(bob.loop_on_next()); - - let expected_response = fill_data_provider(alice_data_provider, bob_db).await; - - let request = alice_handle.request(db_sync::Request::program_ids(H256::zero(), 2)); - let response = timeout(Duration::from_secs(5), request) - .await - .expect("time has elapsed") - .unwrap(); - assert_eq!(response, expected_response); - } - #[tokio::test] async fn validator_discovery() { init_logger(); diff --git a/ethexe/network/src/peer_score.rs b/ethexe/network/src/peer_score.rs index 87d41c56c0d..835723c55f6 100644 --- a/ethexe/network/src/peer_score.rs +++ b/ethexe/network/src/peer_score.rs @@ -45,14 +45,12 @@ struct Metrics { #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub(crate) enum ScoreDecreaseReason { - ExcessiveData, InvalidData, } impl ScoreDecreaseReason { fn to_i8(self, config: &Config) -> i8 { match self { - ScoreDecreaseReason::ExcessiveData => config.excessive_data, ScoreDecreaseReason::InvalidData => config.invalid_data, } } @@ -70,10 +68,6 @@ impl Handle { Self(tx) } - pub fn excessive_data(&self, peer_id: PeerId) { - let _res = self.0.send((peer_id, ScoreDecreaseReason::ExcessiveData)); - } - pub fn invalid_data(&self, peer_id: PeerId) { let _res = self.0.send((peer_id, ScoreDecreaseReason::InvalidData)); } @@ -98,7 +92,6 @@ pub(crate) enum Event { /// Behaviour config. #[derive(Debug, Clone)] pub(crate) struct Config { - excessive_data: i8, invalid_data: i8, decay: i8, blocked_threshold: i8, @@ -109,7 +102,6 @@ pub(crate) struct Config { impl Config { const fn new() -> Self { Self { - excessive_data: i8::MIN / 5, invalid_data: i8::MIN / 3, decay: i8::MAX / 17, blocked_threshold: i8::MIN / 3, @@ -379,12 +371,12 @@ mod tests { #[tokio::test(start_paused = true)] async fn smoke() { - const EXCESSIVE_DATA: i8 = Config::new().blocked_threshold / 3 - 1; + const INVALID_DATA: i8 = Config::new().blocked_threshold / 3 - 1; init_logger(); let alice_config = Config { - excessive_data: EXCESSIVE_DATA, + invalid_data: INVALID_DATA, ..Default::default() }; let mut alice = new_swarm_with_config(alice_config.clone()).await; @@ -394,37 +386,37 @@ mod tests { tokio::spawn(chad.loop_on_next()); let handle = alice.behaviour_mut().handle(); - handle.excessive_data(chad_peer_id); + handle.invalid_data(chad_peer_id); let event = future::poll_immediate(alice.next_behaviour_event()).await; assert_eq!(event, None); assert_eq!( alice.behaviour().get_score(chad_peer_id), - Some(EXCESSIVE_DATA) + Some(INVALID_DATA) ); - handle.excessive_data(chad_peer_id); + handle.invalid_data(chad_peer_id); let event = future::poll_immediate(alice.next_behaviour_event()).await; assert_eq!(event, None); assert_eq!( alice.behaviour().get_score(chad_peer_id), - Some(2 * EXCESSIVE_DATA) + Some(2 * INVALID_DATA) ); - handle.excessive_data(chad_peer_id); + handle.invalid_data(chad_peer_id); let event = alice.next_behaviour_event().await; assert_eq!( event, Event::PeerBlocked { peer_id: chad_peer_id, - last_reason: ScoreDecreaseReason::ExcessiveData + last_reason: ScoreDecreaseReason::InvalidData } ); assert_eq!( alice.behaviour().get_score(chad_peer_id), - Some(3 * EXCESSIVE_DATA) + Some(3 * INVALID_DATA) ); let event = alice.next_swarm_event().await; @@ -448,7 +440,7 @@ mod tests { ); assert_eq!( alice.behaviour().get_score(chad_peer_id), - Some(EXCESSIVE_DATA * 3 + alice_config.decay) + Some(INVALID_DATA * 3 + alice_config.decay) ); } diff --git a/ethexe/network/src/utils.rs b/ethexe/network/src/utils.rs index 5a9c0b7c7dd..1e4e2a4a7b2 100644 --- a/ethexe/network/src/utils.rs +++ b/ethexe/network/src/utils.rs @@ -16,17 +16,13 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::db_sync::PeerId; use async_trait::async_trait; use ip_network::IpNetwork; use libp2p::{ - Multiaddr, StreamProtocol, + Multiaddr, PeerId, StreamProtocol, futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, multiaddr, request_response, - swarm::{ - ConnectionClosed, ConnectionId, DialError, DialFailure, FromSwarm, NewExternalAddrOfPeer, - behaviour::ConnectionEstablished, - }, + swarm::{ConnectionId, DialError, DialFailure, FromSwarm, NewExternalAddrOfPeer}, }; use lru::LruCache; use parity_scale_codec::{Decode, DecodeAll, Encode}; @@ -215,29 +211,6 @@ impl ConnectionMap { limit: NoLimits, } } - - /// Returns true if a new connection added - pub(crate) fn on_swarm_event(&mut self, event: FromSwarm) -> bool { - match event { - FromSwarm::ConnectionEstablished(ConnectionEstablished { - peer_id, - connection_id, - .. - }) => { - let Ok(new) = self.add_connection(peer_id, connection_id); - new - } - FromSwarm::ConnectionClosed(ConnectionClosed { - peer_id, - connection_id, - .. - }) => { - self.remove_connection(peer_id, connection_id); - false - } - _ => false, - } - } } /// A helper struct for formatting collections (BTreeSet, BTreeMap) with two display modes: @@ -404,11 +377,8 @@ impl Default for PeerAddresses { #[cfg(test)] pub(crate) mod tests { - use crate::{ - db_sync::PeerId, - utils::{ConnectionMap, ExponentialBackoffInterval}, - }; - use libp2p::swarm::ConnectionId; + use crate::utils::{ConnectionMap, ExponentialBackoffInterval}; + use libp2p::{PeerId, swarm::ConnectionId}; use proptest::{ arbitrary::Arbitrary, strategy::{Strategy, ValueTree}, diff --git a/ethexe/network/src/validator/discovery.rs b/ethexe/network/src/validator/discovery.rs index ac8e4143312..14671e793c8 100644 --- a/ethexe/network/src/validator/discovery.rs +++ b/ethexe/network/src/validator/discovery.rs @@ -21,7 +21,6 @@ //! Heavily based on Substrate authority discovery mechanism. use crate::{ - db_sync::PeerId, kad::{ self, GetRecordResult, PutRecordFuture, RecordKey, ValidatorIdentityKey, ValidatorIdentityRecord, @@ -42,7 +41,7 @@ use futures::{ use gsigner::secp256k1::{PrivateKey, Secp256k1SignerExt, Signer}; use indexmap::IndexSet; use libp2p::{ - Multiaddr, + Multiaddr, PeerId, core::{Endpoint, transport::PortUse}, identity::Keypair, multiaddr, diff --git a/ethexe/network/src/validator/topic.rs b/ethexe/network/src/validator/topic.rs index 52e3b8a302a..4dbfc465c0d 100644 --- a/ethexe/network/src/validator/topic.rs +++ b/ethexe/network/src/validator/topic.rs @@ -19,15 +19,13 @@ //! Validator-specific networking logic that verifies signed messages //! against on-chain state. -use crate::{ - db_sync::PeerId, gossipsub::MessageAcceptance, peer_score, - validator::list::ValidatorListSnapshot, -}; +use crate::{gossipsub::MessageAcceptance, peer_score, validator::list::ValidatorListSnapshot}; use ethexe_common::{ Address, HashOf, injected::{InjectedTransaction, SignedCompactPromise}, network::VerifiedValidatorMessage, }; +use libp2p::PeerId; use lru::LruCache; use std::{cmp::Ordering, collections::VecDeque, mem, num::NonZeroUsize, sync::Arc}; diff --git a/ethexe/service/src/fast_sync.rs b/ethexe/service/src/fast_sync.rs index 38d8b0409ff..2fc926e1844 100644 --- a/ethexe/service/src/fast_sync.rs +++ b/ethexe/service/src/fast_sync.rs @@ -44,7 +44,7 @@ use ethexe_db::{ visitor::DatabaseVisitor, }; use ethexe_ethereum::mirror::MirrorQuery; -use ethexe_network::{NetworkService, db_sync}; +use ethexe_network::NetworkService; use ethexe_observer::{ ObserverService, utils::{BlockId, BlockLoader}, diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 459809b9ba1..7042f388efc 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -53,10 +53,9 @@ use alloy::{ rpc::types::anvil::Metadata, }; use anyhow::{Context, Result, bail}; -use async_trait::async_trait; use ethexe_blob_loader::{BlobLoader, BlobLoaderEvent, BlobLoaderService, ConsensusLayerConfig}; use ethexe_common::{ - COMMITMENT_DELAY_LIMIT, CodeAndIdUnchecked, PromiseEmissionMode, gear::CodeState, + COMMITMENT_DELAY_LIMIT, CodeAndIdUnchecked, PromiseEmissionMode, network::VerifiedValidatorMessage, }; use ethexe_compute::{ComputeConfig, ComputeEvent, ComputeService}; @@ -67,10 +66,7 @@ use ethexe_db::{ Database, GenesisInitializer, InitConfig, RawDatabase, RocksDatabase, dump::StateDump, }; use ethexe_ethereum::{EthereumBuilder, deploy::EthereumDeployer, router::RouterQuery}; -use ethexe_network::{ - NetworkEvent, NetworkRuntimeConfig, NetworkService, - db_sync::{self, ExternalDataProvider}, -}; +use ethexe_network::{NetworkEvent, NetworkRuntimeConfig, NetworkService}; use ethexe_observer::{ ObserverConfig, ObserverEvent, ObserverService, utils::{BlockId, BlockLoader}, @@ -80,15 +76,9 @@ use ethexe_prometheus::{PrometheusEvent, PrometheusService}; use ethexe_rpc::{RpcEvent, RpcServer}; use ethexe_service_utils::{OptionFuture as _, OptionStreamNext as _}; use futures::{FutureExt, StreamExt, stream::FuturesUnordered}; -use gprimitives::{ActorId, CodeId, H256}; +use gprimitives::CodeId; use gsigner::secp256k1::{Address, PrivateKey, PublicKey, Signer}; -use std::{ - collections::{BTreeSet, HashMap}, - num::NonZero, - path::PathBuf, - pin::Pin, - time::Duration, -}; +use std::{collections::HashMap, num::NonZero, path::PathBuf, pin::Pin, time::Duration}; use tokio::sync::oneshot; pub mod config; @@ -109,32 +99,6 @@ pub enum Event { Fetching(db_sync::HandleResult), } -#[derive(Clone)] -struct RouterDataProvider(RouterQuery); - -#[async_trait] -impl ExternalDataProvider for RouterDataProvider { - fn clone_boxed(&self) -> Box { - Box::new(self.clone()) - } - - async fn programs_code_ids_at( - self: Box, - program_ids: BTreeSet, - block: H256, - ) -> Result> { - self.0.programs_code_ids_at(program_ids, block).await - } - - async fn codes_states_at( - self: Box, - code_ids: BTreeSet, - block: H256, - ) -> Result> { - self.0.codes_states_at(code_ids, block).await - } -} - /// ethexe service. pub struct Service { db: Database, @@ -450,7 +414,6 @@ impl Service { validator_key: validator_pub_key, general_signer: signer.clone(), network_signer, - external_data_provider: Box::new(RouterDataProvider(router_query)), db: db.clone(), }; @@ -803,20 +766,7 @@ impl Service { unreachable!("Fetching event is impossible without network service"); }; - match result { - Ok(db_sync::Response::Announces(response)) => { - consensus.receive_announces_response(response)?; - } - Ok(resp) => { - panic!("only announces are requested currently, but got: {resp:?}"); - } - Err((err, request)) => { - log::trace!( - "Retry fetching external data for request {request:?} due to error: {err:?}" - ); - network_fetcher.push(network.db_sync_handle().retry(request)); - } - } + consensus.receive_announces_response(result)?; } } } From 7b26975a32ef29a7fd8a53274cba6645050b815b Mon Sep 17 00:00:00 2001 From: Arsenii Lyashenko Date: Wed, 13 May 2026 19:58:35 +0300 Subject: [PATCH 2/8] Initial announces --- ethexe/common/src/network.rs | 131 +++++++++++++++++++++++++++++++- ethexe/network/src/lib.rs | 13 ++-- ethexe/service/src/fast_sync.rs | 93 ++++++++++------------- ethexe/service/src/lib.rs | 23 ++++-- 4 files changed, 193 insertions(+), 67 deletions(-) diff --git a/ethexe/common/src/network.rs b/ethexe/common/src/network.rs index de608fb5e32..9900a1f11fb 100644 --- a/ethexe/common/src/network.rs +++ b/ethexe/common/src/network.rs @@ -21,8 +21,9 @@ use crate::{ consensus::{BatchCommitmentValidationReply, BatchCommitmentValidationRequest}, ecdsa::{SignedData, VerifiedData}, }; -use alloc::vec::Vec; +use alloc::{collections::VecDeque, vec::Vec}; use core::{hash::Hash, num::NonZeroU32}; +use gprimitives::H256; use parity_scale_codec::{Decode, Encode}; use sha3::Keccak256; @@ -89,6 +90,11 @@ impl VerifiedValidatorMessage { } } +#[allow(async_fn_in_trait)] +pub trait BitswapHandle { + async fn request(&self, hash: H256) -> Vec; +} + /// Until condition for announces request (see [`AnnouncesRequest`]). #[derive(PartialEq, Eq, Hash, Debug, Clone, Copy, Encode, Decode, derive_more::From)] pub enum AnnouncesRequestUntil { @@ -113,15 +119,16 @@ pub struct AnnouncesRequest { #[derive(derive_more::Debug, Clone, Eq, PartialEq, derive_more::From)] pub struct AnnouncesResponse { /// Corresponding request for this response - request: AnnouncesRequest, + pub request: AnnouncesRequest, /// List of announces - announces: Vec, + pub announces: Vec, } impl AnnouncesResponse { /// # Safety /// - /// Response must be only created by network service + /// Response must be only created after checking that the announce chain + /// matches the corresponding request. pub unsafe fn from_parts(request: AnnouncesRequest, announces: Vec) -> Self { Self { request, announces } } @@ -138,3 +145,119 @@ impl AnnouncesResponse { (self.request, self.announces) } } + +#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display)] +pub enum AnnouncesRequestError { + #[display("requested chain length {_0} exceeds maximum allowed {_1}")] + ChainLenExceedsMax(NonZeroU32, NonZeroU32), + #[display("announce {_0} failed to decode")] + DecodeFailed(HashOf), + #[display("announces response is empty")] + EmptyResponse, + #[display("announces response head mismatch: expected {expected}, received {received}")] + HeadMismatch { + expected: HashOf, + received: HashOf, + }, + #[display("announces response tail mismatch: expected {expected}, received {received}")] + TailMismatch { + expected: HashOf, + received: HashOf, + }, + #[display("announces response length mismatch: expected {expected}, received {received}")] + LenMismatch { expected: usize, received: usize }, + #[display("announces response chain is not linked")] + ChainIsNotLinked, + #[display("reached maximum chain length {_0}")] + ReachedMaxChainLen(NonZeroU32), +} + +#[cfg(feature = "std")] +impl std::error::Error for AnnouncesRequestError {} + +pub async fn request_announces( + bitswap: &impl BitswapHandle, + request: AnnouncesRequest, + max_chain_len: NonZeroU32, +) -> Result { + if let AnnouncesRequestUntil::ChainLen(len) = request.until + && len > max_chain_len + { + return Err(AnnouncesRequestError::ChainLenExceedsMax( + len, + max_chain_len, + )); + } + + let mut announces = VecDeque::new(); + let mut announce_hash = request.head; + + loop { + match request.until { + AnnouncesRequestUntil::Tail(tail) if announce_hash == tail => { + return validate_announces(request, announces.into()); + } + AnnouncesRequestUntil::ChainLen(len) if announces.len() == len.get() as usize => { + return validate_announces(request, announces.into()); + } + _ => {} + } + + if announces.len() == max_chain_len.get() as usize { + return Err(AnnouncesRequestError::ReachedMaxChainLen(max_chain_len)); + } + + let data = bitswap.request(announce_hash.inner()).await; + let announce = Announce::decode(&mut data.as_slice()) + .map_err(|_| AnnouncesRequestError::DecodeFailed(announce_hash))?; + + announce_hash = announce.parent; + announces.push_front(announce); + } +} + +fn validate_announces( + request: AnnouncesRequest, + announces: Vec, +) -> Result { + let Some((first, last)) = announces.first().zip(announces.last()) else { + return Err(AnnouncesRequestError::EmptyResponse); + }; + + if request.head != last.to_hash() { + return Err(AnnouncesRequestError::HeadMismatch { + expected: request.head, + received: last.to_hash(), + }); + } + + match request.until { + AnnouncesRequestUntil::Tail(request_tail_hash) => { + if request_tail_hash != first.parent { + return Err(AnnouncesRequestError::TailMismatch { + expected: request_tail_hash, + received: first.parent, + }); + } + } + AnnouncesRequestUntil::ChainLen(len) => { + if announces.len() != len.get() as usize { + return Err(AnnouncesRequestError::LenMismatch { + expected: len.get() as usize, + received: announces.len(), + }); + } + } + } + + // Check chain linking + let mut expected_parent_hash = first.parent; + for announce in announces.iter() { + if announce.parent != expected_parent_hash { + return Err(AnnouncesRequestError::ChainIsNotLinked); + } + expected_parent_hash = announce.to_hash(); + } + + Ok(unsafe { AnnouncesResponse::from_parts(request, announces) }) +} diff --git a/ethexe/network/src/lib.rs b/ethexe/network/src/lib.rs index a39146f2c93..d0f804a1701 100644 --- a/ethexe/network/src/lib.rs +++ b/ethexe/network/src/lib.rs @@ -59,7 +59,7 @@ use ethexe_common::{ db::ConfigStorageRO, ecdsa::PublicKey, injected::{AddressedInjectedTransaction, SignedCompactPromise}, - network::{SignedValidatorMessage, VerifiedValidatorMessage}, + network::{BitswapHandle, SignedValidatorMessage, VerifiedValidatorMessage}, }; use ethexe_db::Database; use futures::{Stream, future::Either, ready, stream::FusedStream}; @@ -103,6 +103,12 @@ const MAX_PENDING_OUTGOING_CONNECTIONS: u32 = 10; /// response. pub const DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE: NonZeroU32 = NonZeroU32::new(1000).unwrap(); +impl BitswapHandle for bitswap::Handle { + async fn request(&self, hash: H256) -> Vec { + bitswap::Handle::request(self, hash).await + } +} + /// High-level events produced by [`NetworkService`]. #[derive(derive_more::Debug)] pub enum NetworkEvent { @@ -264,7 +270,7 @@ impl NetworkService { transport_type, router_address, allow_non_global_addresses, - max_chain_len_for_announces_response, + max_chain_len_for_announces_response: _, } = config; let NetworkRuntimeConfig { @@ -301,7 +307,6 @@ impl NetworkService { general_signer, validator_list_snapshot: validator_list_snapshot.clone(), allow_non_global_addresses, - max_chain_len_for_announces_response, metrics: (&mut registry, metrics.clone()), }; let behaviour = Behaviour::new(behaviour_config)?; @@ -703,7 +708,6 @@ struct BehaviourConfig<'a> { general_signer: Signer, validator_list_snapshot: Arc, allow_non_global_addresses: bool, - max_chain_len_for_announces_response: NonZeroU32, metrics: ( &'a mut libp2p::metrics::Registry, Arc, @@ -747,7 +751,6 @@ impl Behaviour { general_signer, validator_list_snapshot, allow_non_global_addresses, - max_chain_len_for_announces_response, metrics: (registry, metrics), } = config; diff --git a/ethexe/service/src/fast_sync.rs b/ethexe/service/src/fast_sync.rs index 2fc926e1844..ac1a18facf6 100644 --- a/ethexe/service/src/fast_sync.rs +++ b/ethexe/service/src/fast_sync.rs @@ -31,7 +31,7 @@ use ethexe_common::{ router::{AnnouncesCommittedEvent, BatchCommittedEvent}, }, injected, - network::{AnnouncesRequest, AnnouncesRequestUntil}, + network::{AnnouncesRequest, AnnouncesRequestUntil, request_announces}, }; use ethexe_compute::ComputeService; use ethexe_db::{ @@ -44,7 +44,7 @@ use ethexe_db::{ visitor::DatabaseVisitor, }; use ethexe_ethereum::mirror::MirrorQuery; -use ethexe_network::NetworkService; +use ethexe_network::{DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, NetworkService}; use ethexe_observer::{ ObserverService, utils::{BlockId, BlockLoader}, @@ -129,24 +129,34 @@ impl EventData { } } -async fn net_fetch( +async fn bitswap_fetch(network: &mut NetworkService, hash: H256) -> Vec { + let bitswap = network.bitswap_handle(); + let fut = bitswap.request(hash); + tokio::pin!(fut); + loop { + tokio::select! { + _ = network.select_next_some() => {}, + data = &mut fut => break data, + } + } +} + +async fn bitswap_fetch_announces( network: &mut NetworkService, - request: db_sync::Request, -) -> Result { - let mut fut = network.db_sync_handle().request(request); + request: AnnouncesRequest, +) -> Result { + let bitswap = network.bitswap_handle(); + let fut = request_announces( + &bitswap, + request, + DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, + ); + tokio::pin!(fut); + loop { tokio::select! { _ = network.select_next_some() => {}, - res = &mut fut => { - match res { - Ok(response) => break Ok(response), - Err((err, request)) => { - log::warn!("Request {:?} failed: {err}. Retrying...", request.id()); - fut = network.db_sync_handle().retry(request); - continue; - } - } - } + response = &mut fut => break response.map_err(Into::into), } } } @@ -154,22 +164,15 @@ async fn net_fetch( /// Collects program code IDs for the latest committed block. async fn collect_program_code_ids( observer: &mut ObserverService, - network: &mut NetworkService, + _network: &mut NetworkService, latest_committed_block: H256, ) -> Result> { let router_query = observer.router_query(); - let programs_count = router_query + let _programs_count = router_query .programs_count_at(latest_committed_block) .await?; - let response = net_fetch( - network, - db_sync::Request::program_ids(latest_committed_block, programs_count), - ) - .await?; - - let program_code_ids = response.unwrap_program_ids(); - Ok(program_code_ids) + anyhow::bail!("fast-sync program id enumeration via bitswap is not implemented yet") } async fn collect_announce( @@ -181,16 +184,14 @@ async fn collect_announce( return Ok(announce); } - let response = net_fetch( + let response = bitswap_fetch_announces( network, AnnouncesRequest { head: announce_hash, until: AnnouncesRequestUntil::ChainLen(NonZeroU32::MIN), - } - .into(), + }, ) - .await? - .unwrap_announces(); + .await?; // Response is checked so we can just take the first announce let (_, mut announces) = response.into_parts(); @@ -200,28 +201,17 @@ async fn collect_announce( /// Collects a set of valid code IDs that are not yet validated in the local database. async fn collect_code_ids( observer: &mut ObserverService, - network: &mut NetworkService, + _network: &mut NetworkService, db: &Database, latest_committed_block: H256, ) -> Result> { let router_query = observer.router_query(); - let codes_count = router_query + let _codes_count = router_query .validated_codes_count_at(latest_committed_block) .await?; - let response = net_fetch( - network, - db_sync::Request::valid_codes(latest_committed_block, codes_count), - ) - .await?; - - let code_ids = response - .unwrap_valid_codes() - .into_iter() - .filter(|&code_id| db.code_valid(code_id).is_none()) - .collect(); - - Ok(code_ids) + let _ = db; + anyhow::bail!("fast-sync valid code enumeration via bitswap is not implemented yet") } /// Collects the program states for a given set of program IDs at a specified block height. @@ -324,10 +314,10 @@ impl RequestManager { let pending_network_requests = self.handle_pending_requests(); if !pending_network_requests.is_empty() { - let request: BTreeSet = pending_network_requests.keys().copied().collect(); - let response = net_fetch(network, db_sync::Request::hashes(request)) - .await - .expect("no external validation required"); + let mut response = Vec::with_capacity(pending_network_requests.len()); + for &hash in pending_network_requests.keys() { + response.push((hash, bitswap_fetch(network, hash).await)); + } self.handle_response(pending_network_requests, response); } @@ -364,9 +354,8 @@ impl RequestManager { fn handle_response( &mut self, mut pending_network_requests: HashMap, - response: db_sync::Response, + data: Vec<(H256, Vec)>, ) { - let data = response.unwrap_hashes(); for (hash, data) in data { let metadata = pending_network_requests .remove(&hash) diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 7042f388efc..954a0e43b77 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -56,7 +56,7 @@ use anyhow::{Context, Result, bail}; use ethexe_blob_loader::{BlobLoader, BlobLoaderEvent, BlobLoaderService, ConsensusLayerConfig}; use ethexe_common::{ COMMITMENT_DELAY_LIMIT, CodeAndIdUnchecked, PromiseEmissionMode, - network::VerifiedValidatorMessage, + network::{AnnouncesRequestError, VerifiedValidatorMessage, request_announces}, }; use ethexe_compute::{ComputeConfig, ComputeEvent, ComputeService}; use ethexe_consensus::{ @@ -66,7 +66,10 @@ use ethexe_db::{ Database, GenesisInitializer, InitConfig, RawDatabase, RocksDatabase, dump::StateDump, }; use ethexe_ethereum::{EthereumBuilder, deploy::EthereumDeployer, router::RouterQuery}; -use ethexe_network::{NetworkEvent, NetworkRuntimeConfig, NetworkService}; +use ethexe_network::{ + DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, NetworkEvent, NetworkRuntimeConfig, + NetworkService, +}; use ethexe_observer::{ ObserverConfig, ObserverEvent, ObserverService, utils::{BlockId, BlockLoader}, @@ -96,7 +99,7 @@ pub enum Event { BlobLoader(BlobLoaderEvent), Rpc(RpcEvent), Prometheus(PrometheusEvent), - Fetching(db_sync::HandleResult), + Fetching(Result), } /// ethexe service. @@ -743,7 +746,15 @@ impl Service { panic!("Requesting announces is not allowed without network service"); }; - network_fetcher.push(network.db_sync_handle().request(request.into())); + let bitswap = network.bitswap_handle(); + network_fetcher.push(async move { + request_announces( + &bitswap, + request, + DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, + ) + .await + }); } ConsensusEvent::AnnounceAccepted(_) | ConsensusEvent::AnnounceRejected(_) => { // TODO #4940: consider to publish network message @@ -762,11 +773,11 @@ impl Service { } }, Event::Fetching(result) => { - let Some(network) = network.as_mut() else { + if network.is_none() { unreachable!("Fetching event is impossible without network service"); }; - consensus.receive_announces_response(result)?; + consensus.receive_announces_response(result?)?; } } } From cf5582d701bf0a38cb0e3fe5c2a69e3708f68ca9 Mon Sep 17 00:00:00 2001 From: Arsenii Lyashenko Date: Wed, 13 May 2026 20:58:50 +0300 Subject: [PATCH 3/8] Done announces --- ethexe/cli/src/params/network.rs | 5 +- ethexe/common/src/network.rs | 172 ++++------------------ ethexe/consensus/src/connect/mod.rs | 2 +- ethexe/consensus/src/validator/initial.rs | 69 ++++----- ethexe/network/src/bitswap.rs | 156 ++++++++++++++------ ethexe/network/src/lib.rs | 32 ++-- ethexe/service/src/fast_sync.rs | 19 +-- ethexe/service/src/lib.rs | 20 +-- ethexe/service/src/tests/utils/env.rs | 4 +- 9 files changed, 199 insertions(+), 280 deletions(-) diff --git a/ethexe/cli/src/params/network.rs b/ethexe/cli/src/params/network.rs index 294acaf24d1..20294244e06 100644 --- a/ethexe/cli/src/params/network.rs +++ b/ethexe/cli/src/params/network.rs @@ -23,7 +23,7 @@ use anyhow::{Context, Result}; use clap::Parser; use ethexe_common::Address; use ethexe_network::{ - DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, NetworkConfig, + NetworkConfig, export::{Multiaddr, Protocol}, }; use gsigner::secp256k1::Signer; @@ -146,9 +146,6 @@ impl NetworkParams { listen_addresses, transport_type: Default::default(), allow_non_global_addresses: is_dev, - max_chain_len_for_announces_response: self - .max_chain_len_for_announces_response - .unwrap_or(DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE), })) } } diff --git a/ethexe/common/src/network.rs b/ethexe/common/src/network.rs index 9900a1f11fb..c8c29046d6a 100644 --- a/ethexe/common/src/network.rs +++ b/ethexe/common/src/network.rs @@ -23,7 +23,6 @@ use crate::{ }; use alloc::{collections::VecDeque, vec::Vec}; use core::{hash::Hash, num::NonZeroU32}; -use gprimitives::H256; use parity_scale_codec::{Decode, Encode}; use sha3::Keccak256; @@ -91,8 +90,8 @@ impl VerifiedValidatorMessage { } #[allow(async_fn_in_trait)] -pub trait BitswapHandle { - async fn request(&self, hash: H256) -> Vec; +pub trait BitswapAnnouncesHandle { + async fn request(&self, hash: HashOf) -> Announce; } /// Until condition for announces request (see [`AnnouncesRequest`]). @@ -115,6 +114,35 @@ pub struct AnnouncesRequest { pub until: AnnouncesRequestUntil, } +impl AnnouncesRequest { + pub async fn request(self, bitswap: impl BitswapAnnouncesHandle) -> AnnouncesResponse { + let mut announces = VecDeque::new(); + let mut announce_hash = self.head; + + loop { + match self.until { + AnnouncesRequestUntil::Tail(tail) if announce_hash == tail => { + return AnnouncesResponse { + request: self, + announces: announces.into(), + }; + } + AnnouncesRequestUntil::ChainLen(len) if announces.len() == len.get() as usize => { + return AnnouncesResponse { + request: self, + announces: announces.into(), + }; + } + _ => {} + } + + let announce = bitswap.request(announce_hash).await; + announce_hash = announce.parent; + announces.push_front(announce); + } + } +} + /// Checked announces response ensuring that it matches the corresponding request. #[derive(derive_more::Debug, Clone, Eq, PartialEq, derive_more::From)] pub struct AnnouncesResponse { @@ -123,141 +151,3 @@ pub struct AnnouncesResponse { /// List of announces pub announces: Vec, } - -impl AnnouncesResponse { - /// # Safety - /// - /// Response must be only created after checking that the announce chain - /// matches the corresponding request. - pub unsafe fn from_parts(request: AnnouncesRequest, announces: Vec) -> Self { - Self { request, announces } - } - - pub fn request(&self) -> &AnnouncesRequest { - &self.request - } - - pub fn announces(&self) -> &[Announce] { - &self.announces - } - - pub fn into_parts(self) -> (AnnouncesRequest, Vec) { - (self.request, self.announces) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display)] -pub enum AnnouncesRequestError { - #[display("requested chain length {_0} exceeds maximum allowed {_1}")] - ChainLenExceedsMax(NonZeroU32, NonZeroU32), - #[display("announce {_0} failed to decode")] - DecodeFailed(HashOf), - #[display("announces response is empty")] - EmptyResponse, - #[display("announces response head mismatch: expected {expected}, received {received}")] - HeadMismatch { - expected: HashOf, - received: HashOf, - }, - #[display("announces response tail mismatch: expected {expected}, received {received}")] - TailMismatch { - expected: HashOf, - received: HashOf, - }, - #[display("announces response length mismatch: expected {expected}, received {received}")] - LenMismatch { expected: usize, received: usize }, - #[display("announces response chain is not linked")] - ChainIsNotLinked, - #[display("reached maximum chain length {_0}")] - ReachedMaxChainLen(NonZeroU32), -} - -#[cfg(feature = "std")] -impl std::error::Error for AnnouncesRequestError {} - -pub async fn request_announces( - bitswap: &impl BitswapHandle, - request: AnnouncesRequest, - max_chain_len: NonZeroU32, -) -> Result { - if let AnnouncesRequestUntil::ChainLen(len) = request.until - && len > max_chain_len - { - return Err(AnnouncesRequestError::ChainLenExceedsMax( - len, - max_chain_len, - )); - } - - let mut announces = VecDeque::new(); - let mut announce_hash = request.head; - - loop { - match request.until { - AnnouncesRequestUntil::Tail(tail) if announce_hash == tail => { - return validate_announces(request, announces.into()); - } - AnnouncesRequestUntil::ChainLen(len) if announces.len() == len.get() as usize => { - return validate_announces(request, announces.into()); - } - _ => {} - } - - if announces.len() == max_chain_len.get() as usize { - return Err(AnnouncesRequestError::ReachedMaxChainLen(max_chain_len)); - } - - let data = bitswap.request(announce_hash.inner()).await; - let announce = Announce::decode(&mut data.as_slice()) - .map_err(|_| AnnouncesRequestError::DecodeFailed(announce_hash))?; - - announce_hash = announce.parent; - announces.push_front(announce); - } -} - -fn validate_announces( - request: AnnouncesRequest, - announces: Vec, -) -> Result { - let Some((first, last)) = announces.first().zip(announces.last()) else { - return Err(AnnouncesRequestError::EmptyResponse); - }; - - if request.head != last.to_hash() { - return Err(AnnouncesRequestError::HeadMismatch { - expected: request.head, - received: last.to_hash(), - }); - } - - match request.until { - AnnouncesRequestUntil::Tail(request_tail_hash) => { - if request_tail_hash != first.parent { - return Err(AnnouncesRequestError::TailMismatch { - expected: request_tail_hash, - received: first.parent, - }); - } - } - AnnouncesRequestUntil::ChainLen(len) => { - if announces.len() != len.get() as usize { - return Err(AnnouncesRequestError::LenMismatch { - expected: len.get() as usize, - received: announces.len(), - }); - } - } - } - - // Check chain linking - let mut expected_parent_hash = first.parent; - for announce in announces.iter() { - if announce.parent != expected_parent_hash { - return Err(AnnouncesRequestError::ChainIsNotLinked); - } - expected_parent_hash = announce.to_hash(); - } - - Ok(unsafe { AnnouncesResponse::from_parts(request, announces) }) -} diff --git a/ethexe/consensus/src/connect/mod.rs b/ethexe/consensus/src/connect/mod.rs index 80b6d7aaf63..1e1a85460ae 100644 --- a/ethexe/consensus/src/connect/mod.rs +++ b/ethexe/consensus/src/connect/mod.rs @@ -328,7 +328,7 @@ impl ConsensusService for ConnectService { let block = *block; let producer = *producer; - let (request, announces) = response.into_parts(); + let AnnouncesResponse { request, announces } = response; if waiting_request != &request { return Ok(()); diff --git a/ethexe/consensus/src/validator/initial.rs b/ethexe/consensus/src/validator/initial.rs index 13d56f8b83c..548a464243a 100644 --- a/ethexe/consensus/src/validator/initial.rs +++ b/ethexe/consensus/src/validator/initial.rs @@ -174,12 +174,11 @@ impl StateHandler for Initial { block, chain, announces, - } if announces == *response.request() => { + } if announces == response.request => { tracing::debug!(block = %block.hash, "Received missing announces response"); let missing_announces = response - .into_parts() - .1 + .announces .into_iter() .map(|a| (a.to_hash(), a)) .collect(); @@ -382,20 +381,18 @@ mod tests { }; assert_eq!(state.context().output, vec![expected_request.into()]); - let response = unsafe { - AnnouncesResponse::from_parts( - expected_request, - vec![ - chain - .announces - .get(&chain.block_top_announce_hash(last - 3)) - .unwrap() - .announce - .clone(), - announce2.clone(), - announce1.clone(), - ], - ) + let response = AnnouncesResponse { + request: expected_request, + announces: vec![ + chain + .announces + .get(&chain.block_top_announce_hash(last - 3)) + .unwrap() + .announce + .clone(), + announce2.clone(), + announce1.clone(), + ], }; // In successful case no new events are produced @@ -566,14 +563,12 @@ mod tests { let invalid_announce = Announce::base(H256::random(), HashOf::random()); let invalid_announce_hash = invalid_announce.to_hash(); - let response = unsafe { - AnnouncesResponse::from_parts( - AnnouncesRequest { - head: invalid_announce_hash, - until: NonZeroU32::new(1).unwrap().into(), - }, - vec![invalid_announce], - ) + let response = AnnouncesResponse { + request: AnnouncesRequest { + head: invalid_announce_hash, + until: NonZeroU32::new(1).unwrap().into(), + }, + announces: vec![invalid_announce], }; let state = Initial::create_with_chain_head(ctx, block) @@ -642,19 +637,17 @@ mod tests { }; assert_eq!(state.context().output, vec![expected_request.into()]); - let response = unsafe { - AnnouncesResponse::from_parts( - expected_request, - vec![ - chain - .announces - .get(&chain.block_top_announce_hash(last - 7)) - .unwrap() - .announce - .clone(), - unknown_announce, - ], - ) + let response = AnnouncesResponse { + request: expected_request, + announces: vec![ + chain + .announces + .get(&chain.block_top_announce_hash(last - 7)) + .unwrap() + .announce + .clone(), + unknown_announce, + ], }; let state = state.process_announces_response(response).unwrap(); diff --git a/ethexe/network/src/bitswap.rs b/ethexe/network/src/bitswap.rs index f96302d3436..cb4057252f5 100644 --- a/ethexe/network/src/bitswap.rs +++ b/ethexe/network/src/bitswap.rs @@ -19,7 +19,10 @@ use beetswap::multihasher::{Multihasher, MultihasherError}; use blockstore::{block::CidError, cond_send::CondSend}; use cid::{Cid, CidGeneric}; -use ethexe_common::db::HashStorageRO; +use ethexe_common::{ + Announce, HashOf, + db::{AnnounceStorageRO, HashStorageRO}, +}; use futures::FutureExt; use gprimitives::H256; use libp2p::{ @@ -31,6 +34,7 @@ use libp2p::{ }, }; use multihash::Multihash; +use parity_scale_codec::{Decode, Encode}; use std::{ collections::HashMap, mem, @@ -42,22 +46,62 @@ use tokio::{ task, }; +#[derive(Debug, Copy, Clone, Eq, PartialEq, derive_more::From)] +pub enum Request { + Hash(H256), + Announce(HashOf), +} + +impl Request { + fn into_cid(self) -> Cid { + match self { + Request::Hash(hash) => Cid::new_v1( + Blockstore::RAW_CODEC, + Multihash::wrap(Blockstore::BLAKE2B_CODE, hash.as_bytes()) + .expect("size is always correct"), + ), + Request::Announce(hash) => Cid::new_v1( + Blockstore::ANNOUNCES_CODEC, + Multihash::wrap(Blockstore::BLAKE2B_CODE, hash.inner().as_bytes()) + .expect("size is always correct"), + ), + } + } + + fn into_response(self, data: Vec) -> Response { + match self { + Request::Hash(_) => Response::Hash(data), + Request::Announce(_) => { + Response::Announce(Announce::decode(&mut data.as_slice()).expect("valid announce")) + } + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq, derive_more::Unwrap)] +pub enum Response { + Hash(Vec), + Announce(Announce), +} + #[derive(Clone)] -pub struct Handle(mpsc::UnboundedSender<(H256, oneshot::Sender>)>); +pub struct Handle(mpsc::UnboundedSender<(Request, oneshot::Sender)>); impl Handle { - pub async fn request(&self, request: H256) -> Vec { + pub async fn request(&self, request: impl Into) -> Response { let (tx, rx) = oneshot::channel(); self.0 - .send((request, tx)) + .send((request.into(), tx)) .expect("channel should never be closed"); rx.await.expect("channel should never be closed") } } -pub(crate) trait BlockstoreDatabase: Send + Sync + HashStorageRO { +pub(crate) trait BlockstoreDatabase: + Send + Sync + HashStorageRO + AnnounceStorageRO +{ fn clone_boxed(&self) -> Box; } @@ -74,7 +118,25 @@ pub struct Blockstore { impl Blockstore { const MAX_BLOCK_SIZE: u64 = 1024 * 1024; // 1MB const BLAKE2B_CODE: u64 = 0xb220; - const CID_CODEC: u64 = 0x55; + const RAW_CODEC: u64 = 0x55; + const ANNOUNCES_CODEC: u64 = 0x300000; + + fn convert_multihash(multihash: &Multihash) -> blockstore::Result { + let hash: Multihash<32> = + beetswap::utils::convert_multihash(multihash).ok_or(blockstore::Error::CidTooLarge)?; + if hash.code() != Self::BLAKE2B_CODE { + return Err(blockstore::Error::CidError(CidError::InvalidMultihashCode( + hash.code(), + Self::BLAKE2B_CODE, + ))); + } + if hash.size() as usize != mem::size_of::() { + return Err(blockstore::Error::CidError( + CidError::InvalidMultihashLength(hash.size() as usize), + )); + } + Ok(H256::from_slice(hash.digest())) + } } impl blockstore::Blockstore for Blockstore { @@ -82,34 +144,41 @@ impl blockstore::Blockstore for Blockstore { &self, cid: &CidGeneric, ) -> impl Future>>> + CondSend { - let hash = *cid.hash(); let db = self.db.clone_boxed(); + let hash = *cid.hash(); + let codec = cid.codec(); task::spawn_blocking(move || { - let hash: Multihash<32> = - beetswap::utils::convert_multihash(&hash).ok_or(blockstore::Error::CidTooLarge)?; - if hash.code() != Self::BLAKE2B_CODE { - return Err(blockstore::Error::CidError(CidError::InvalidMultihashCode( - hash.code(), - Self::BLAKE2B_CODE, - ))); - } - if hash.size() as usize != mem::size_of::() { - return Err(blockstore::Error::CidError( - CidError::InvalidMultihashLength(hash.size() as usize), - )); - } - - let hash = H256::from_slice(hash.digest()); - let data = db.read_by_hash(hash); - - if let Some(data) = &data - && data.len() as u64 > Self::MAX_BLOCK_SIZE - { - log::warn!("{hash} is too large: {} bytes", data.len()); - return Err(blockstore::Error::ValueTooLarge); + let hash = Self::convert_multihash(&hash)?; + match codec { + Self::RAW_CODEC => { + let data = db.read_by_hash(hash); + + if let Some(data) = &data + && data.len() as u64 > Self::MAX_BLOCK_SIZE + { + log::warn!("{hash} is too large: {} bytes", data.len()); + return Err(blockstore::Error::ValueTooLarge); + } + + Ok(data) + } + Self::ANNOUNCES_CODEC => { + let hash = unsafe { HashOf::new(hash) }; + let announce = db.announce(hash); + + if let Some(announce) = &announce + && announce.encoded_size() as u64 > Self::MAX_BLOCK_SIZE + { + log::warn!("{hash} is too large: {} bytes", announce.encoded_size()); + return Err(blockstore::Error::ValueTooLarge); + } + + Ok(announce.map(|announce| announce.encode())) + } + codec => Err(blockstore::Error::CidError(CidError::InvalidCidCodec( + codec, + ))), } - - Ok(data) }) .map(|res| res.expect("database panicked")) } @@ -155,8 +224,8 @@ type InnerBehaviour = beetswap::Behaviour<32, Blockstore>; pub struct Behaviour { inner: InnerBehaviour, handle: Handle, - rx: mpsc::UnboundedReceiver<(H256, oneshot::Sender>)>, - requests: HashMap>>, + rx: mpsc::UnboundedReceiver<(Request, oneshot::Sender)>, + requests: HashMap)>, } impl Behaviour { @@ -180,19 +249,12 @@ impl Behaviour { self.handle.clone() } - fn cid(hash: H256) -> Cid { - Cid::new_v1( - Blockstore::CID_CODEC, - Multihash::wrap(Blockstore::BLAKE2B_CODE, hash.as_bytes()) - .expect("size is always correct"), - ) - } - fn handle_inner_event(&mut self, event: beetswap::Event) { match event { beetswap::Event::GetQueryResponse { query_id, data } => { - if let Some(channel) = self.requests.remove(&query_id) { - let _ = channel.send(data); + if let Some((request, channel)) = self.requests.remove(&query_id) { + let response = request.into_response(data); + let _ = channel.send(response); } } beetswap::Event::GetQueryError { query_id, error } => { @@ -283,7 +345,7 @@ impl NetworkBehaviour for Behaviour { &mut self, cx: &mut Context<'_>, ) -> Poll>> { - self.requests.retain(|&query_id, channel| { + self.requests.retain(|&query_id, (_, channel)| { if channel.is_closed() { self.inner.cancel(query_id); return false; @@ -292,10 +354,10 @@ impl NetworkBehaviour for Behaviour { true }); - while let Poll::Ready(Some((hash, channel))) = self.rx.poll_recv(cx) { - let cid = Self::cid(hash); + while let Poll::Ready(Some((request, channel))) = self.rx.poll_recv(cx) { + let cid = request.into_cid(); let query_id = self.inner.get(&cid); - self.requests.insert(query_id, channel); + self.requests.insert(query_id, (request, channel)); } if let Poll::Ready(to_swarm) = self.inner.poll(cx) { diff --git a/ethexe/network/src/lib.rs b/ethexe/network/src/lib.rs index d0f804a1701..72445f2495d 100644 --- a/ethexe/network/src/lib.rs +++ b/ethexe/network/src/lib.rs @@ -55,11 +55,11 @@ use crate::{ }; use anyhow::{Context, anyhow}; use ethexe_common::{ - Address, BlockHeader, ValidatorsVec, + Address, Announce, BlockHeader, HashOf, ValidatorsVec, db::ConfigStorageRO, ecdsa::PublicKey, injected::{AddressedInjectedTransaction, SignedCompactPromise}, - network::{BitswapHandle, SignedValidatorMessage, VerifiedValidatorMessage}, + network::{BitswapAnnouncesHandle, SignedValidatorMessage, VerifiedValidatorMessage}, }; use ethexe_db::Database; use futures::{Stream, future::Either, ready, stream::FusedStream}; @@ -78,10 +78,7 @@ use libp2p::{ }; #[cfg(test)] use libp2p_swarm_test::SwarmExt; -use std::{ - collections::HashSet, fmt::Write, num::NonZeroU32, pin::Pin, sync::Arc, task::Poll, - time::Duration, -}; +use std::{collections::HashSet, fmt::Write, pin::Pin, sync::Arc, task::Poll, time::Duration}; use validator::{list::ValidatorList, topic::ValidatorTopic}; /// Default listen port. @@ -99,13 +96,9 @@ const MAX_ESTABLISHED_OUTGOING_CONNECTIONS: u32 = 500; const MAX_PENDING_INCOMING_CONNECTIONS: u32 = 10; const MAX_PENDING_OUTGOING_CONNECTIONS: u32 = 10; -/// Hard cap for the amount of announces that can be returned in one db-sync -/// response. -pub const DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE: NonZeroU32 = NonZeroU32::new(1000).unwrap(); - -impl BitswapHandle for bitswap::Handle { - async fn request(&self, hash: H256) -> Vec { - bitswap::Handle::request(self, hash).await +impl BitswapAnnouncesHandle for bitswap::Handle { + async fn request(&self, hash: HashOf) -> Announce { + bitswap::Handle::request(self, hash).await.unwrap_announce() } } @@ -159,8 +152,6 @@ pub struct NetworkConfig { /// Whether private and local addresses are allowed in discovery and /// identify flows. pub allow_non_global_addresses: bool, - /// Upper bound for `Announces` db-sync responses served by this node. - pub max_chain_len_for_announces_response: NonZeroU32, } impl NetworkConfig { @@ -175,7 +166,6 @@ impl NetworkConfig { transport_type: TransportType::Default, router_address, allow_non_global_addresses: false, - max_chain_len_for_announces_response: DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, } } @@ -189,7 +179,6 @@ impl NetworkConfig { transport_type: TransportType::Test, router_address, allow_non_global_addresses: true, - max_chain_len_for_announces_response: DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, } } } @@ -270,7 +259,6 @@ impl NetworkService { transport_type, router_address, allow_non_global_addresses, - max_chain_len_for_announces_response: _, } = config; let NetworkRuntimeConfig { @@ -947,7 +935,13 @@ mod tests { ) .await .expect("time has elapsed"); - assert_eq!(response, (b"hello".to_vec(), b"world".to_vec())); + assert_eq!( + response, + ( + bitswap::Response::Hash(b"hello".to_vec()), + bitswap::Response::Hash(b"world".to_vec()) + ) + ); } #[tokio::test] diff --git a/ethexe/service/src/fast_sync.rs b/ethexe/service/src/fast_sync.rs index ac1a18facf6..069b1e66ad3 100644 --- a/ethexe/service/src/fast_sync.rs +++ b/ethexe/service/src/fast_sync.rs @@ -31,7 +31,7 @@ use ethexe_common::{ router::{AnnouncesCommittedEvent, BatchCommittedEvent}, }, injected, - network::{AnnouncesRequest, AnnouncesRequestUntil, request_announces}, + network::{AnnouncesRequest, AnnouncesRequestUntil}, }; use ethexe_compute::ComputeService; use ethexe_db::{ @@ -44,7 +44,7 @@ use ethexe_db::{ visitor::DatabaseVisitor, }; use ethexe_ethereum::mirror::MirrorQuery; -use ethexe_network::{DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, NetworkService}; +use ethexe_network::NetworkService; use ethexe_observer::{ ObserverService, utils::{BlockId, BlockLoader}, @@ -136,7 +136,7 @@ async fn bitswap_fetch(network: &mut NetworkService, hash: H256) -> Vec { loop { tokio::select! { _ = network.select_next_some() => {}, - data = &mut fut => break data, + data = &mut fut => break data.unwrap_hash(), } } } @@ -146,17 +146,13 @@ async fn bitswap_fetch_announces( request: AnnouncesRequest, ) -> Result { let bitswap = network.bitswap_handle(); - let fut = request_announces( - &bitswap, - request, - DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, - ); + let fut = request.request(bitswap); tokio::pin!(fut); loop { tokio::select! { _ = network.select_next_some() => {}, - response = &mut fut => break response.map_err(Into::into), + response = &mut fut => break Ok(response), } } } @@ -184,7 +180,7 @@ async fn collect_announce( return Ok(announce); } - let response = bitswap_fetch_announces( + let mut response = bitswap_fetch_announces( network, AnnouncesRequest { head: announce_hash, @@ -194,8 +190,7 @@ async fn collect_announce( .await?; // Response is checked so we can just take the first announce - let (_, mut announces) = response.into_parts(); - Ok(announces.remove(0)) + Ok(response.announces.remove(0)) } /// Collects a set of valid code IDs that are not yet validated in the local database. diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 954a0e43b77..7574836453e 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -56,7 +56,7 @@ use anyhow::{Context, Result, bail}; use ethexe_blob_loader::{BlobLoader, BlobLoaderEvent, BlobLoaderService, ConsensusLayerConfig}; use ethexe_common::{ COMMITMENT_DELAY_LIMIT, CodeAndIdUnchecked, PromiseEmissionMode, - network::{AnnouncesRequestError, VerifiedValidatorMessage, request_announces}, + network::{AnnouncesResponse, VerifiedValidatorMessage}, }; use ethexe_compute::{ComputeConfig, ComputeEvent, ComputeService}; use ethexe_consensus::{ @@ -66,10 +66,7 @@ use ethexe_db::{ Database, GenesisInitializer, InitConfig, RawDatabase, RocksDatabase, dump::StateDump, }; use ethexe_ethereum::{EthereumBuilder, deploy::EthereumDeployer, router::RouterQuery}; -use ethexe_network::{ - DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, NetworkEvent, NetworkRuntimeConfig, - NetworkService, -}; +use ethexe_network::{NetworkEvent, NetworkRuntimeConfig, NetworkService}; use ethexe_observer::{ ObserverConfig, ObserverEvent, ObserverService, utils::{BlockId, BlockLoader}, @@ -99,7 +96,7 @@ pub enum Event { BlobLoader(BlobLoaderEvent), Rpc(RpcEvent), Prometheus(PrometheusEvent), - Fetching(Result), + Fetching(AnnouncesResponse), } /// ethexe service. @@ -747,14 +744,7 @@ impl Service { }; let bitswap = network.bitswap_handle(); - network_fetcher.push(async move { - request_announces( - &bitswap, - request, - DEFAULT_MAX_CHAIN_LEN_FOR_ANNOUNCES_RESPONSE, - ) - .await - }); + network_fetcher.push(request.request(bitswap)); } ConsensusEvent::AnnounceAccepted(_) | ConsensusEvent::AnnounceRejected(_) => { // TODO #4940: consider to publish network message @@ -777,7 +767,7 @@ impl Service { unreachable!("Fetching event is impossible without network service"); }; - consensus.receive_announces_response(result?)?; + consensus.receive_announces_response(result)?; } } } diff --git a/ethexe/service/src/tests/utils/env.rs b/ethexe/service/src/tests/utils/env.rs index 80fc69ed17b..e4ab7cabd8a 100644 --- a/ethexe/service/src/tests/utils/env.rs +++ b/ethexe/service/src/tests/utils/env.rs @@ -17,7 +17,7 @@ // along with this program. If not, see . use crate::{ - RouterDataProvider, Service, + Service, config::EthereumConfig, tests::utils::{ InfiniteStreamExt, TestingEvent, TestingNetworkEvent, @@ -325,7 +325,6 @@ impl TestEnv { validator_key: None, general_signer: signer.clone(), network_signer: signer.clone(), - external_data_provider: Box::new(RouterDataProvider(router_query.clone())), db: db.clone(), }; @@ -1182,7 +1181,6 @@ impl Node { validator_key: self.validator_config.as_ref().map(|c| c.public_key), general_signer: self.signer.clone(), network_signer: self.signer.clone(), - external_data_provider: Box::new(RouterDataProvider(self.router_query.clone())), db: self.db.clone(), }; From 9313ad707ad630d771350cdd8ee836b13662fe42 Mon Sep 17 00:00:00 2001 From: Arsenii Lyashenko Date: Thu, 14 May 2026 02:54:20 +0300 Subject: [PATCH 4/8] Initial fast sync --- ethexe/ethereum/src/lib.rs | 25 +++++++++- ethexe/ethereum/src/router/events.rs | 68 +++++++++++++++++++------- ethexe/observer/src/utils.rs | 32 +++--------- ethexe/service/src/fast_sync.rs | 70 +++++++++++++++++---------- ethexe/service/src/lib.rs | 10 ++-- ethexe/service/src/tests/utils/env.rs | 11 +++-- 6 files changed, 135 insertions(+), 81 deletions(-) diff --git a/ethexe/ethereum/src/lib.rs b/ethexe/ethereum/src/lib.rs index 11f2a8b26d9..e49e63bf3d7 100644 --- a/ethexe/ethereum/src/lib.rs +++ b/ethexe/ethereum/src/lib.rs @@ -27,7 +27,7 @@ use crate::{ }; use alloy::{ consensus::SignableTransaction, - eips::BlockId, + eips::{BlockId, BlockNumberOrTag}, network::{self, Ethereum as AlloyEthereum, EthereumWallet, Network, TxSigner}, primitives::{Address as AlloyAddress, B256, ChainId, Signature, U256 as AlloyU256, address}, providers::{ @@ -664,6 +664,29 @@ pub(crate) use signatures_consts; use crate::wvara::WVara; +/// A helping trait for converting various types into `alloy::eips::BlockNumberOrTag`. +pub trait IntoBlockNumberOrTag { + fn into_block_number_or_tag(self) -> BlockNumberOrTag; +} + +impl IntoBlockNumberOrTag for u32 { + fn into_block_number_or_tag(self) -> BlockNumberOrTag { + BlockNumberOrTag::Number(self as u64) + } +} + +impl IntoBlockNumberOrTag for u64 { + fn into_block_number_or_tag(self) -> BlockNumberOrTag { + BlockNumberOrTag::Number(self) + } +} + +impl IntoBlockNumberOrTag for BlockNumberOrTag { + fn into_block_number_or_tag(self) -> BlockNumberOrTag { + self + } +} + /// A helping trait for converting various types into `alloy::eips::BlockId`. pub trait IntoBlockId { fn into_block_id(self) -> BlockId; diff --git a/ethexe/ethereum/src/router/events.rs b/ethexe/ethereum/src/router/events.rs index be163671a50..cb4e5fc82ec 100644 --- a/ethexe/ethereum/src/router/events.rs +++ b/ethexe/ethereum/src/router/events.rs @@ -17,7 +17,7 @@ // along with this program. If not, see . use crate::{ - IRouter, + IRouter, IntoBlockNumberOrTag, abi::utils::{bytes32_to_code_id, bytes32_to_h256}, decode_log, router::RouterQuery, @@ -210,31 +210,47 @@ impl<'a> AnnouncesCommittedEventBuilder<'a> { pub struct CodeGotValidatedEventBuilder<'a> { event: Event<&'a RootProvider, IRouter::CodeGotValidated>, - valid: Option, } impl<'a> CodeGotValidatedEventBuilder<'a> { pub(crate) fn new(query: &'a RouterQuery) -> Self { Self { event: query.instance.CodeGotValidated_filter(), - valid: None, } } pub fn valid(mut self, valid: bool) -> Self { - self.valid = Some(valid); + self.event = self.event.topic1(valid); self } + pub fn from_block(mut self, block: impl IntoBlockNumberOrTag) -> Self { + self.event = self.event.from_block(block.into_block_number_or_tag()); + self + } + + pub fn to_block(mut self, block: impl IntoBlockNumberOrTag) -> Self { + self.event = self.event.to_block(block.into_block_number_or_tag()); + self + } + + pub async fn query(self) -> Result> { + Ok(self + .event + .chunked() + .query() + .await? + .into_iter() + .map(|(event, log)| (event.into(), log)) + .collect()) + } + pub async fn subscribe( self, ) -> Result> + Unpin + use<>> { - let mut event = self.event; - if let Some(valid) = self.valid { - event = event.topic1(valid); - } - Ok(event + Ok(self + .event .subscribe() .await? .into_stream() @@ -329,31 +345,47 @@ impl<'a> ComputationSettingsChangedEventBuilder<'a> { pub struct ProgramCreatedEventBuilder<'a> { event: Event<&'a RootProvider, IRouter::ProgramCreated>, - code_id: Option, } impl<'a> ProgramCreatedEventBuilder<'a> { pub(crate) fn new(query: &'a RouterQuery) -> Self { Self { event: query.instance.ProgramCreated_filter(), - code_id: None, } } pub fn code_id(mut self, code_id: CodeId) -> Self { - self.code_id = Some(code_id); + let code_id: B256 = code_id.into_bytes().into(); + self.event = self.event.topic1(code_id); self } + pub fn from_block(mut self, block: impl IntoBlockNumberOrTag) -> Self { + self.event = self.event.from_block(block.into_block_number_or_tag()); + self + } + + pub fn to_block(mut self, block: impl IntoBlockNumberOrTag) -> Self { + self.event = self.event.to_block(block.into_block_number_or_tag()); + self + } + + pub async fn query(self) -> Result> { + Ok(self + .event + .chunked() + .query() + .await? + .into_iter() + .map(|(event, log)| (event.into(), log)) + .collect()) + } + pub async fn subscribe( self, ) -> Result> + Unpin + use<>> { - let mut event = self.event; - if let Some(code_id) = self.code_id { - let code_id: B256 = code_id.into_bytes().into(); - event = event.topic1(code_id); - } - Ok(event + Ok(self + .event .subscribe() .await? .into_stream() diff --git a/ethexe/observer/src/utils.rs b/ethexe/observer/src/utils.rs index f1a96ac997c..70e78f4b69e 100644 --- a/ethexe/observer/src/utils.rs +++ b/ethexe/observer/src/utils.rs @@ -32,7 +32,7 @@ use alloy::{ }; use anyhow::{Context, Result}; use ethexe_common::{Address, BlockData, BlockHeader, SimpleBlockData, events::BlockEvent}; -use ethexe_ethereum::{abi::IRouter, mirror, router}; +use ethexe_ethereum::{IntoBlockId, abi::IRouter, mirror, router}; use futures::{TryFutureExt, future}; use gprimitives::H256; use std::{collections::HashMap, future::IntoFuture, ops::RangeInclusive}; @@ -45,26 +45,9 @@ const LOGS_CHUNK_SIZE: u64 = 256; /// Maximum number of in-flight log chunk requests issued by [`alloy::contract::ChunkedEvent`]. const LOGS_MAX_CONCURRENCY: usize = 8; -#[derive(Debug, Copy, Clone, PartialEq, Eq, derive_more::From)] -pub enum BlockId { - Hash(H256), - Latest, - Finalized, -} - -impl BlockId { - fn as_alloy(self) -> alloy::eips::BlockId { - match self { - BlockId::Hash(hash) => alloy::eips::BlockId::hash(hash.0.into()), - BlockId::Latest => alloy::eips::BlockId::latest(), - BlockId::Finalized => alloy::eips::BlockId::finalized(), - } - } -} - #[allow(async_fn_in_trait)] pub trait BlockLoader { - async fn load_simple(&self, block: BlockId) -> Result; + async fn load_simple(&self, block: impl IntoBlockId) -> Result; async fn load(&self, block: H256, header: Option) -> Result; @@ -210,13 +193,10 @@ impl EthereumBlockLoader { } impl BlockLoader for EthereumBlockLoader { - async fn load_simple(&self, block: BlockId) -> Result { + async fn load_simple(&self, block: impl IntoBlockId) -> Result { + let block = block.into_block_id(); log::trace!("Querying simple data for one block {block:?}"); - let block = self - .provider - .get_block(block.as_alloy()) - .into_future() - .await?; + let block = self.provider.get_block(block).into_future().await?; let block = block.context("block not found")?; let (hash, header) = Self::block_response_to_data(block); Ok(SimpleBlockData { hash, header }) @@ -232,7 +212,7 @@ impl BlockLoader for EthereumBlockLoader { let (block_hash, header, logs) = if let Some(header) = header { (block, header, logs_request.await?) } else { - let data = self.load_simple(block.into()); + let data = self.load_simple(block); let (SimpleBlockData { hash, header }, logs) = future::try_join(data, logs_request).await?; (hash, header, logs) diff --git a/ethexe/service/src/fast_sync.rs b/ethexe/service/src/fast_sync.rs index 069b1e66ad3..1ee1cf08a02 100644 --- a/ethexe/service/src/fast_sync.rs +++ b/ethexe/service/src/fast_sync.rs @@ -17,6 +17,7 @@ // along with this program. If not, see . use crate::Service; +use alloy::eips::BlockId; use anyhow::{Context, Result}; use ethexe_common::{ Address, Announce, BlockData, CodeAndIdUnchecked, Digest, HashOf, ProgramStates, @@ -45,10 +46,7 @@ use ethexe_db::{ }; use ethexe_ethereum::mirror::MirrorQuery; use ethexe_network::NetworkService; -use ethexe_observer::{ - ObserverService, - utils::{BlockId, BlockLoader}, -}; +use ethexe_observer::{ObserverService, utils::BlockLoader}; use ethexe_runtime_common::{ ScheduleRestorer, state::{ @@ -160,15 +158,21 @@ async fn bitswap_fetch_announces( /// Collects program code IDs for the latest committed block. async fn collect_program_code_ids( observer: &mut ObserverService, - _network: &mut NetworkService, - latest_committed_block: H256, + genesis_block: u32, + latest_committed_block: u32, ) -> Result> { - let router_query = observer.router_query(); - let _programs_count = router_query - .programs_count_at(latest_committed_block) - .await?; - - anyhow::bail!("fast-sync program id enumeration via bitswap is not implemented yet") + Ok(observer + .router_query() + .events() + .program_created() + .from_block(genesis_block) + .to_block(latest_committed_block) + .query() + .await + .context("failed to ProgramCreated events")? + .into_iter() + .map(|(event, _log)| (event.actor_id, event.code_id)) + .collect()) } async fn collect_announce( @@ -196,17 +200,25 @@ async fn collect_announce( /// Collects a set of valid code IDs that are not yet validated in the local database. async fn collect_code_ids( observer: &mut ObserverService, - _network: &mut NetworkService, - db: &Database, - latest_committed_block: H256, + genesis_block: u32, + latest_committed_block: u32, ) -> Result> { - let router_query = observer.router_query(); - let _codes_count = router_query - .validated_codes_count_at(latest_committed_block) - .await?; - - let _ = db; - anyhow::bail!("fast-sync valid code enumeration via bitswap is not implemented yet") + Ok(observer + .router_query() + .events() + .code_got_validated() + .valid(true) + .from_block(genesis_block) + .to_block(latest_committed_block) + .query() + .await + .context("failed to query CodeGotValidated events")? + .into_iter() + .map(|(event, _log)| { + debug_assert!(event.valid); + event.code_id + }) + .collect()) } /// Collects the program states for a given set of program IDs at a specified block height. @@ -647,11 +659,19 @@ pub(crate) async fn sync(service: &mut Service) -> Result<()> { // we get finalized block to avoid block reorganization // because we restore the database only for the latest block of a chain, // and thus the reorganization can lead us to an empty block - .load_simple(BlockId::Finalized) + .load_simple(BlockId::finalized()) .await .context("failed to get latest block")? .hash; + let genesis_block = db.config().genesis_block_hash; + let genesis_block = observer + .block_loader() + .load_simple(genesis_block) + .await + .context("failed to get genesis block")?; + let genesis_block = genesis_block.header.height; + let block_loader = observer.block_loader(); let Some(EventData { @@ -676,8 +696,8 @@ pub(crate) async fn sync(service: &mut Service) -> Result<()> { events, } = block_loader.load(announce.block_hash, None).await?; - let code_ids = collect_code_ids(observer, network, db, announce.block_hash).await?; - let program_code_ids = collect_program_code_ids(observer, network, announce.block_hash).await?; + let code_ids = collect_code_ids(observer, genesis_block, header.height).await?; + let program_code_ids = collect_program_code_ids(observer, genesis_block, header.height).await?; // we fetch program states from the finalized block // because actual states are at the same block as we acquired the latest committed block let program_states = diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 7574836453e..7221e59eb8d 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -48,6 +48,7 @@ use crate::config::{Config, ConfigPublicKey}; use alloy::{ + eips::BlockId, node_bindings::{Anvil, AnvilInstance}, providers::{ProviderBuilder, RootProvider, ext::AnvilApi}, rpc::types::anvil::Metadata, @@ -67,10 +68,7 @@ use ethexe_db::{ }; use ethexe_ethereum::{EthereumBuilder, deploy::EthereumDeployer, router::RouterQuery}; use ethexe_network::{NetworkEvent, NetworkRuntimeConfig, NetworkService}; -use ethexe_observer::{ - ObserverConfig, ObserverEvent, ObserverService, - utils::{BlockId, BlockLoader}, -}; +use ethexe_observer::{ObserverConfig, ObserverEvent, ObserverService, utils::BlockLoader}; use ethexe_processor::{ProcessedCodeInfo, Processor, ProcessorConfig, ValidCodeInfo}; use ethexe_prometheus::{PrometheusEvent, PrometheusService}; use ethexe_rpc::{RpcEvent, RpcServer}; @@ -301,7 +299,7 @@ impl Service { let latest_block = observer .block_loader() - .load_simple(BlockId::Latest) + .load_simple(BlockId::latest()) .await .context("failed to get latest block")?; @@ -404,7 +402,7 @@ impl Service { let latest_block_data = observer .block_loader() - .load_simple(BlockId::Latest) + .load_simple(BlockId::latest()) .await .context("failed to get latest block")?; diff --git a/ethexe/service/src/tests/utils/env.rs b/ethexe/service/src/tests/utils/env.rs index e4ab7cabd8a..643bdbfff90 100644 --- a/ethexe/service/src/tests/utils/env.rs +++ b/ethexe/service/src/tests/utils/env.rs @@ -25,6 +25,7 @@ use crate::{ }, }; use alloy::{ + eips::BlockId, node_bindings::{Anvil, AnvilInstance}, providers::{ProviderBuilder, RootProvider, ext::AnvilApi}, rpc::types::anvil::{Metadata, MineOptions}, @@ -56,7 +57,7 @@ use ethexe_ethereum::{ use ethexe_network::{NetworkConfig, NetworkRuntimeConfig, NetworkService, export::Multiaddr}; use ethexe_observer::{ ObserverConfig, ObserverService, - utils::{BlockId, BlockLoader, EthereumBlockLoader}, + utils::{BlockLoader, EthereumBlockLoader}, }; use ethexe_processor::{DEFAULT_CHUNK_SIZE, Processor}; use ethexe_rpc::{DEFAULT_BLOCK_GAS_LIMIT_MULTIPLIER, RpcConfig, RpcServer}; @@ -268,7 +269,7 @@ impl TestEnv { .unwrap(); let latest_block = observer .block_loader() - .load_simple(BlockId::Latest) + .load_simple(BlockId::latest()) .await .context("failed to get latest block")?; let latest_validators = router_query @@ -677,7 +678,7 @@ impl TestEnv { pub async fn latest_block(&self) -> SimpleBlockData { EthereumBlockLoader::new(self.provider.clone(), self.eth_cfg.router_address) - .load_simple(BlockId::Latest) + .load_simple(BlockId::latest()) .await .unwrap() } @@ -975,7 +976,7 @@ impl Node { .unwrap(); let latest_block = observer .block_loader() - .load_simple(BlockId::Latest) + .load_simple(BlockId::latest()) .await .unwrap(); let latest_validators = observer @@ -1203,7 +1204,7 @@ impl Node { let provider = RootProvider::connect(&self.eth_cfg.rpc).await.unwrap(); let block_loader = EthereumBlockLoader::new(provider, self.eth_cfg.router_address); - let latest_block = block_loader.load_simple(BlockId::Latest).await.unwrap(); + let latest_block = block_loader.load_simple(BlockId::latest()).await.unwrap(); let latest_validators = self .router_query .validators_at(latest_block.hash) From 7abbff0d0d4fafcd981dff81311875da384c79ad Mon Sep 17 00:00:00 2001 From: Arsenii Lyashenko Date: Thu, 14 May 2026 03:13:19 +0300 Subject: [PATCH 5/8] Remove unused deps --- Cargo.lock | 4 ---- ethexe/network/Cargo.toml | 3 --- ethexe/observer/Cargo.toml | 1 - 3 files changed, 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f3509512cee..12dd43c7aaf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5392,14 +5392,12 @@ dependencies = [ "derive_more 2.1.1", "ethexe-common", "ethexe-db", - "ethexe-service-utils", "futures", "gear-workspace-hack", "gprimitives", "gsigner", "indexmap 2.13.0", "ip_network", - "itertools 0.13.0", "libp2p 0.56.0", "libp2p-gossipsub", "libp2p-swarm-test", @@ -5413,7 +5411,6 @@ dependencies = [ "prometheus-client 0.23.1", "proptest", "rand 0.8.5", - "thiserror 2.0.17", "tokio", "tracing-subscriber", ] @@ -5465,7 +5462,6 @@ version = "1.10.0" dependencies = [ "alloy", "anyhow", - "derive_more 2.1.1", "ethexe-common", "ethexe-db", "ethexe-ethereum", diff --git a/ethexe/network/Cargo.toml b/ethexe/network/Cargo.toml index f9e18effc68..d9d160e6280 100644 --- a/ethexe/network/Cargo.toml +++ b/ethexe/network/Cargo.toml @@ -17,7 +17,6 @@ ignored = ["libp2p-gossipsub"] # workspace members gsigner = { workspace = true, features = ["std", "secp256k1", "codec", "keyring", "serde"] } ethexe-db.workspace = true -ethexe-service-utils.workspace = true ethexe-common.workspace = true gprimitives = { workspace = true, features = ["std", "codec"] } @@ -38,11 +37,9 @@ async-trait.workspace = true rand = { workspace = true, features = ["std", "std_rng"] } futures.workspace = true derive_more.workspace = true -itertools = { workspace = true, features = ["use_std"] } nonempty.workspace = true auto_impl.workspace = true lru.workspace = true -thiserror.workspace = true indexmap.workspace = true ip_network.workspace = true prometheus-client = "0.23.1" # specific version that lip2p uses diff --git a/ethexe/observer/Cargo.toml b/ethexe/observer/Cargo.toml index 57865de136f..0c31b97d31b 100644 --- a/ethexe/observer/Cargo.toml +++ b/ethexe/observer/Cargo.toml @@ -33,7 +33,6 @@ tokio = { workspace = true, features = ["rt-multi-thread", "fs", "sync"] } futures.workspace = true log.workspace = true tracing.workspace = true -derive_more.workspace = true future-timing.workspace = true gear-workspace-hack.workspace = true From 1b22b0fbfaf789041c27bf87750c82a7dc4c8b05 Mon Sep 17 00:00:00 2001 From: Arsenii Lyashenko Date: Thu, 14 May 2026 14:26:58 +0300 Subject: [PATCH 6/8] Update docs --- ethexe/network/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ethexe/network/src/lib.rs b/ethexe/network/src/lib.rs index 72445f2495d..2d656b77e82 100644 --- a/ethexe/network/src/lib.rs +++ b/ethexe/network/src/lib.rs @@ -24,14 +24,14 @@ //! - peer management and connection caps; //! - Kademlia-backed validator discovery; //! - gossipsub topics for validator messages and public promises; -//! - request/response database synchronization; +//! - bitswap-backed data fetching; //! - private injected-transaction delivery to validators; //! - peer scoring and temporary peer blocking. //! //! [`NetworkService`] is the main integration point used by higher-level //! services. It owns the swarm, emits validated [`NetworkEvent`] items, and -//! hands out protocol-specific handles such as [`db_sync::Handle`] for -//! database synchronization and a peer-scoring handle for internal use. +//! hands out protocol-specific handles such as the bitswap handle for data +//! fetching and a peer-scoring handle for internal use. mod bitswap; mod gossipsub; @@ -195,7 +195,7 @@ pub struct NetworkRuntimeConfig { pub general_signer: Signer, /// Signer used only to construct the libp2p networking keypair. pub network_signer: Signer, - /// Database backing validator discovery and db-sync responses. + /// Database backing validator discovery and bitswap responses. pub db: Database, } From 5a8c5840ad846a58cff2495930f6b404a52e367d Mon Sep 17 00:00:00 2001 From: Arsenii Lyashenko Date: Thu, 14 May 2026 15:01:32 +0300 Subject: [PATCH 7/8] Add tests --- ethexe/network/src/bitswap.rs | 235 +++++++++++++++++++++++++++++++--- ethexe/network/src/lib.rs | 10 +- 2 files changed, 223 insertions(+), 22 deletions(-) diff --git a/ethexe/network/src/bitswap.rs b/ethexe/network/src/bitswap.rs index cb4057252f5..8c3fd44d689 100644 --- a/ethexe/network/src/bitswap.rs +++ b/ethexe/network/src/bitswap.rs @@ -40,12 +40,17 @@ use std::{ mem, sync::Arc, task::{Context, Poll}, + time::Duration, }; use tokio::{ sync::{mpsc, oneshot}, - task, + task, time, }; +const BLAKE2B_CODE: u64 = 0xb220; // standard BLAKE2b multihash code +const RAW_CODEC: u64 = 0x55; // standard CID raw codec +const ANNOUNCES_CODEC: u64 = 0x300000; // user-defined CID codec + #[derive(Debug, Copy, Clone, Eq, PartialEq, derive_more::From)] pub enum Request { Hash(H256), @@ -56,13 +61,12 @@ impl Request { fn into_cid(self) -> Cid { match self { Request::Hash(hash) => Cid::new_v1( - Blockstore::RAW_CODEC, - Multihash::wrap(Blockstore::BLAKE2B_CODE, hash.as_bytes()) - .expect("size is always correct"), + RAW_CODEC, + Multihash::wrap(BLAKE2B_CODE, hash.as_bytes()).expect("size is always correct"), ), Request::Announce(hash) => Cid::new_v1( - Blockstore::ANNOUNCES_CODEC, - Multihash::wrap(Blockstore::BLAKE2B_CODE, hash.inner().as_bytes()) + ANNOUNCES_CODEC, + Multihash::wrap(BLAKE2B_CODE, hash.inner().as_bytes()) .expect("size is always correct"), ), } @@ -84,15 +88,56 @@ pub enum Response { Announce(Announce), } +#[derive(Debug, Clone, Copy, Default)] +pub struct Config { + /// Restart stalled requests after some time. + /// + /// This is intended for test environment only, where a peer + /// can receive an announce request before it has caught up enough to serve + /// the corresponding data. Production environment is expected to have + /// enough peers to fulfill requests, so request scheduling is left to + /// Bitswap itself. + pub auto_retry: bool, +} + +impl Config { + pub fn with_auto_retry(mut self, auto_retry: bool) -> Self { + self.auto_retry = auto_retry; + self + } +} + #[derive(Clone)] -pub struct Handle(mpsc::UnboundedSender<(Request, oneshot::Sender)>); +pub struct Handle { + inner: mpsc::UnboundedSender<(Request, oneshot::Sender)>, + auto_retry: bool, +} impl Handle { + const RETRY_TIMEOUT: Duration = Duration::from_secs(5); + pub async fn request(&self, request: impl Into) -> Response { + let request = request.into(); + + if !self.auto_retry { + return self.inner_request(request).await; + } + + loop { + match time::timeout(Self::RETRY_TIMEOUT, self.inner_request(request)).await { + Ok(response) => return response, + Err(_) => { + log::warn!("Bitswap request {request:?} timed out, retrying"); + } + } + } + } + + async fn inner_request(&self, request: Request) -> Response { let (tx, rx) = oneshot::channel(); - self.0 - .send((request.into(), tx)) + self.inner + .send((request, tx)) .expect("channel should never be closed"); rx.await.expect("channel should never be closed") @@ -117,17 +162,14 @@ pub struct Blockstore { impl Blockstore { const MAX_BLOCK_SIZE: u64 = 1024 * 1024; // 1MB - const BLAKE2B_CODE: u64 = 0xb220; - const RAW_CODEC: u64 = 0x55; - const ANNOUNCES_CODEC: u64 = 0x300000; fn convert_multihash(multihash: &Multihash) -> blockstore::Result { let hash: Multihash<32> = beetswap::utils::convert_multihash(multihash).ok_or(blockstore::Error::CidTooLarge)?; - if hash.code() != Self::BLAKE2B_CODE { + if hash.code() != BLAKE2B_CODE { return Err(blockstore::Error::CidError(CidError::InvalidMultihashCode( hash.code(), - Self::BLAKE2B_CODE, + BLAKE2B_CODE, ))); } if hash.size() as usize != mem::size_of::() { @@ -150,7 +192,7 @@ impl blockstore::Blockstore for Blockstore { task::spawn_blocking(move || { let hash = Self::convert_multihash(&hash)?; match codec { - Self::RAW_CODEC => { + RAW_CODEC => { let data = db.read_by_hash(hash); if let Some(data) = &data @@ -162,7 +204,7 @@ impl blockstore::Blockstore for Blockstore { Ok(data) } - Self::ANNOUNCES_CODEC => { + ANNOUNCES_CODEC => { let hash = unsafe { HashOf::new(hash) }; let announce = db.announce(hash); @@ -208,13 +250,12 @@ impl Multihasher<32> for Blake2b256Multihasher { multihash_code: u64, input: &[u8], ) -> Result, MultihasherError> { - if multihash_code != Blockstore::BLAKE2B_CODE { + if multihash_code != BLAKE2B_CODE { return Err(MultihasherError::UnknownMultihashCode); } let hash = ethexe_db::hash(input); - let hash = Multihash::wrap(Blockstore::BLAKE2B_CODE, hash.as_bytes()) - .expect("size is always correct"); + let hash = Multihash::wrap(BLAKE2B_CODE, hash.as_bytes()).expect("size is always correct"); Ok(hash) } } @@ -229,7 +270,7 @@ pub struct Behaviour { } impl Behaviour { - pub fn new(db: Box) -> Self { + pub fn new(db: Box, config: Config) -> Self { let (handle, rx) = mpsc::unbounded_channel(); let blockstore = Arc::new(Blockstore { db }); @@ -239,7 +280,10 @@ impl Behaviour { .protocol_prefix("/ethexe") .expect("prefix is always correct") .build(), - handle: Handle(handle), + handle: Handle { + inner: handle, + auto_retry: config.auto_retry, + }, rx, requests: HashMap::new(), } @@ -377,3 +421,152 @@ impl NetworkBehaviour for Behaviour { Poll::Pending } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::utils::tests::arb_value; + use assert_matches::assert_matches; + use blockstore::Blockstore as _; + use ethexe_common::db::AnnounceStorageRW; + + #[test] + fn request_converts_to_expected_cid() { + let hash = H256::from([1; 32]); + let cid = Request::Hash(hash).into_cid(); + assert_eq!(cid.codec(), RAW_CODEC); + assert_eq!(cid.hash().code(), BLAKE2B_CODE); + assert_eq!(cid.hash().digest(), hash.as_bytes()); + + let announce_hash = unsafe { HashOf::new(H256::from([2; 32])) }; + let cid = Request::Announce(announce_hash).into_cid(); + assert_eq!(cid.codec(), ANNOUNCES_CODEC); + assert_eq!(cid.hash().code(), BLAKE2B_CODE); + assert_eq!(cid.hash().digest(), announce_hash.inner().as_bytes()); + } + + #[test] + fn announce_request_decodes_response() { + let announce = arb_value::(()); + let response = Request::Announce(announce.to_hash()).into_response(announce.encode()); + + assert_eq!(response, Response::Announce(announce)); + } + + #[tokio::test] + async fn blockstore_reads_raw_data() { + let db = ethexe_db::Database::memory(); + let hash = db.cas().write(b"hello"); + let blockstore = Blockstore { db: Box::new(db) }; + let cid = Request::Hash(hash).into_cid(); + + let data = blockstore.get(&cid).await.unwrap(); + + assert_eq!(data, Some(b"hello".to_vec())); + } + + #[tokio::test] + async fn blockstore_reads_announces() { + let db = ethexe_db::Database::memory(); + let announce = arb_value::(()); + let announce_hash = db.set_announce(announce.clone()); + let blockstore = Blockstore { db: Box::new(db) }; + let cid = Request::Announce(announce_hash).into_cid(); + + let data = blockstore.get(&cid).await.unwrap(); + + assert_eq!(data, Some(announce.encode())); + } + + #[tokio::test] + async fn blockstore_rejects_unknown_codec() { + let db = ethexe_db::Database::memory(); + let blockstore = Blockstore { db: Box::new(db) }; + let hash = H256::from([3; 32]); + let multihash = Multihash::wrap(BLAKE2B_CODE, hash.as_bytes()).unwrap(); + let cid = Cid::new_v1(0x99, multihash); + + let error = blockstore.get(&cid).await.unwrap_err(); + + assert_matches!( + error, + blockstore::Error::CidError(CidError::InvalidCidCodec(0x99)) + ); + } + + #[tokio::test] + async fn blockstore_rejects_unknown_multihash_code() { + let db = ethexe_db::Database::memory(); + let blockstore = Blockstore { db: Box::new(db) }; + let hash = H256::from([4; 32]); + let multihash = Multihash::wrap(0x12, hash.as_bytes()).unwrap(); + let cid = Cid::new_v1(RAW_CODEC, multihash); + + let error = blockstore.get(&cid).await.unwrap_err(); + + assert_matches!( + error, + blockstore::Error::CidError(CidError::InvalidMultihashCode(0x12, BLAKE2B_CODE)) + ); + } + + #[tokio::test] + async fn blockstore_rejects_oversized_raw_data() { + let db = ethexe_db::Database::memory(); + let hash = db.cas().write(&vec![0; MAX_BLOCK_SIZE as usize + 1]); + let blockstore = Blockstore { db: Box::new(db) }; + let cid = Request::Hash(hash).into_cid(); + + let error = blockstore.get(&cid).await.unwrap_err(); + + assert_matches!(error, blockstore::Error::ValueTooLarge); + } + + #[tokio::test] + async fn blake2b_multihasher_hashes_known_code() { + let multihash = Blake2b256Multihasher + .hash(BLAKE2B_CODE, b"hello") + .await + .unwrap(); + + assert_eq!(multihash.code(), BLAKE2B_CODE); + assert_eq!(multihash.digest(), ethexe_db::hash(b"hello").as_bytes()); + } + + #[tokio::test] + async fn blake2b_multihasher_rejects_unknown_code() { + let error = Blake2b256Multihasher + .hash(0x12, b"hello") + .await + .unwrap_err(); + + assert_matches!(error, MultihasherError::UnknownMultihashCode); + } + + #[tokio::test(start_paused = true)] + async fn handle_retries_timed_out_requests() { + let (inner, mut rx) = mpsc::unbounded_channel(); + let handle = Handle { + inner, + auto_retry: true, + }; + let hash = H256::from([5; 32]); + + let pending = tokio::spawn(async move { handle.request(hash).await }); + + let (request, first_response) = rx.recv().await.unwrap(); + assert_eq!(request, Request::Hash(hash)); + + time::advance(Handle::RETRY_TIMEOUT).await; + let (request, second_response) = rx.recv().await.unwrap(); + assert_eq!(request, Request::Hash(hash)); + assert!(first_response.is_closed()); + + second_response + .send(Response::Hash(b"hello".to_vec())) + .unwrap(); + let response = pending.await.unwrap(); + + assert_eq!(response, Response::Hash(b"hello".to_vec())); + } +} diff --git a/ethexe/network/src/lib.rs b/ethexe/network/src/lib.rs index 2d656b77e82..7c1e21fbf36 100644 --- a/ethexe/network/src/lib.rs +++ b/ethexe/network/src/lib.rs @@ -789,7 +789,15 @@ impl Behaviour { ) .map_err(|e| anyhow!("`gossipsub::Behaviour` error: {e}"))?; - let bitswap = bitswap::Behaviour::new(db); + // The test environment can start an announce request while the + // serving peer is still catching up and does not have the announce in + // its local DB yet. Retrying keeps those deterministic integration + // tests from waiting forever; the production environment is expected + // to have enough peers to fulfill requests, so scheduling is left to + // Bitswap itself. + let bitswap = bitswap::Config::default() + .with_auto_retry(matches!(transport_type, TransportType::Test)); + let bitswap = bitswap::Behaviour::new(db, bitswap); let injected = injected::Behaviour::new(); From 692b44714ee7e10a206c444cc4f1cd96bb16ee89 Mon Sep 17 00:00:00 2001 From: Arsenii Lyashenko Date: Thu, 14 May 2026 15:11:07 +0300 Subject: [PATCH 8/8] Map database panics to `FatalDatabaseError` and add corresponding test. --- ethexe/network/src/bitswap.rs | 79 +++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/ethexe/network/src/bitswap.rs b/ethexe/network/src/bitswap.rs index 8c3fd44d689..cfdafe38e3b 100644 --- a/ethexe/network/src/bitswap.rs +++ b/ethexe/network/src/bitswap.rs @@ -222,7 +222,10 @@ impl blockstore::Blockstore for Blockstore { ))), } }) - .map(|res| res.expect("database panicked")) + .map(|res| { + res.map_err(|err| blockstore::Error::FatalDatabaseError(err.to_string())) + .flatten() + }) } async fn put_keyed( @@ -428,7 +431,59 @@ mod tests { use crate::utils::tests::arb_value; use assert_matches::assert_matches; use blockstore::Blockstore as _; - use ethexe_common::db::AnnounceStorageRW; + use ethexe_common::{ + ProgramStates, Schedule, + db::{AnnounceMeta, AnnounceStorageRW}, + gear::StateTransition, + }; + use std::collections::BTreeSet; + + #[derive(Clone)] + struct PanickingDatabase; + + impl HashStorageRO for PanickingDatabase { + fn read_by_hash(&self, _hash: H256) -> Option> { + panic!("database read panic"); + } + } + + impl AnnounceStorageRO for PanickingDatabase { + fn announce(&self, _hash: HashOf) -> Option { + panic!("database announce panic"); + } + + fn announce_program_states( + &self, + _announce_hash: HashOf, + ) -> Option { + unreachable!("not used in this test") + } + + fn announce_outcome( + &self, + _announce_hash: HashOf, + ) -> Option> { + unreachable!("not used in this test") + } + + fn announce_schedule(&self, _announce_hash: HashOf) -> Option { + unreachable!("not used in this test") + } + + fn announce_meta(&self, _announce_hash: HashOf) -> AnnounceMeta { + unreachable!("not used in this test") + } + + fn block_announces(&self, _block_hash: H256) -> Option>> { + unreachable!("not used in this test") + } + } + + impl BlockstoreDatabase for PanickingDatabase { + fn clone_boxed(&self) -> Box { + Box::new(self.clone()) + } + } #[test] fn request_converts_to_expected_cid() { @@ -513,7 +568,9 @@ mod tests { #[tokio::test] async fn blockstore_rejects_oversized_raw_data() { let db = ethexe_db::Database::memory(); - let hash = db.cas().write(&vec![0; MAX_BLOCK_SIZE as usize + 1]); + let hash = db + .cas() + .write(&vec![0; Blockstore::MAX_BLOCK_SIZE as usize + 1]); let blockstore = Blockstore { db: Box::new(db) }; let cid = Request::Hash(hash).into_cid(); @@ -522,6 +579,22 @@ mod tests { assert_matches!(error, blockstore::Error::ValueTooLarge); } + #[tokio::test] + async fn blockstore_maps_database_panic_to_fatal_database_error() { + let blockstore = Blockstore { + db: Box::new(PanickingDatabase), + }; + let cid = Request::Hash(H256::from([6; 32])).into_cid(); + + let error = blockstore.get(&cid).await.unwrap_err(); + + assert_matches!( + error, + blockstore::Error::FatalDatabaseError(message) + if message.contains("database read panic") + ); + } + #[tokio::test] async fn blake2b_multihasher_hashes_known_code() { let multihash = Blake2b256Multihasher