bp_core/network/
behaviour.rs

1//! Combined libp2p `NetworkBehaviour` for BillPouch.
2//!
3//! Stack:
4//!   • Gossipsub        — flooding broadcast for NodeInfo announcements
5//!   • Kademlia         — DHT for peer discovery and content addressing
6//!   • Identify         — exchange protocol version and listen addresses
7//!   • mDNS             — local-network peer discovery (zero-config)
8//!   • RequestResponse  — direct fragment fetch/push between Pouches
9//!   • AutoNAT          — detect public reachability from the internet
10//!   • RelayClient      — dial peers through a relay when behind NAT
11
12use libp2p::{
13    autonat, gossipsub, identify, identity::Keypair, kad, mdns, relay, request_response,
14    swarm::NetworkBehaviour, PeerId, StreamProtocol,
15};
16use serde::{Deserialize, Serialize};
17use std::time::Duration;
18
19// ── Fragment exchange protocol ────────────────────────────────────────────────
20
21/// Request sent between Pouch nodes for fragment operations.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub enum FragmentRequest {
24    /// Ask a remote Pouch for a specific fragment by id.
25    Fetch {
26        chunk_id: String,
27        fragment_id: String,
28    },
29    /// Ask a remote Pouch for ALL fragments it holds for a chunk.
30    FetchChunkFragments { chunk_id: String },
31    /// Push a fragment to a remote Pouch for storage.
32    Store {
33        chunk_id: String,
34        fragment_id: String,
35        data: Vec<u8>,
36    },
37    /// Liveness ping — expects a `Pong` with the same nonce.
38    Ping { nonce: u64 },
39    /// Proof-of-Storage challenge — expects a BLAKE3 proof.
40    ///
41    /// The responder must load the fragment identified by `(chunk_id,
42    /// fragment_id)`, compute `BLAKE3(raw_data || nonce.to_le_bytes())`,
43    /// and return it in a [`FragmentResponse::ProofOfStorageOk`].
44    ProofOfStorage {
45        chunk_id: String,
46        fragment_id: String,
47        nonce: u64,
48    },
49}
50
51/// Response to a fragment request.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub enum FragmentResponse {
54    /// Single fragment found (response to Fetch).
55    Found { data: Vec<u8> },
56    /// Multiple fragments found (response to FetchChunkFragments).
57    FoundMany { fragments: Vec<(String, Vec<u8>)> },
58    /// Fragment/chunk not found on this peer.
59    NotFound,
60    /// Fragment stored successfully (response to Store).
61    Stored,
62    /// Store failed (response to Store).
63    StoreFailed { reason: String },
64    /// Pong response to a Ping — echoes the nonce.
65    Pong { nonce: u64 },
66    /// Proof-of-Storage response: `BLAKE3(fragment_data || nonce.to_le_bytes())`.
67    ///
68    /// Returned by a Pouch that successfully loaded the challenged fragment.
69    ProofOfStorageOk { proof: [u8; 32] },
70}
71
72// ── Combined behaviour ────────────────────────────────────────────────────────
73
74/// Single combined behaviour injected into the libp2p Swarm.
75///
76/// All seven sub-behaviours are polled by the swarm in a single `select!`-style
77/// event loop inside [`run_network_loop`].
78///
79/// [`run_network_loop`]: crate::network::run_network_loop
80#[derive(NetworkBehaviour)]
81pub struct BillPouchBehaviour {
82    /// Flooding pub/sub: used to broadcast [`NodeInfo`] announcements.
83    ///
84    /// [`NodeInfo`]: crate::network::state::NodeInfo
85    pub gossipsub: gossipsub::Behaviour,
86    /// Kademlia DHT: distributed peer discovery and content addressing.
87    pub kad: kad::Behaviour<kad::store::MemoryStore>,
88    /// Identify: exchange protocol version string and listen addresses with peers.
89    pub identify: identify::Behaviour,
90    /// mDNS: zero-configuration local-network peer discovery via multicast DNS.
91    pub mdns: mdns::tokio::Behaviour,
92    /// Direct fragment fetch/push between Pouch nodes.
93    pub fragment_exchange: request_response::cbor::Behaviour<FragmentRequest, FragmentResponse>,
94    /// AutoNAT: probes remote peers to detect whether this node is publicly reachable.
95    ///
96    /// Reports [`autonat::NatStatus`] changes so the daemon can decide whether to
97    /// activate relay-assisted connectivity.
98    pub autonat: autonat::Behaviour,
99    /// Relay client: allows this node to route connections through a relay peer
100    /// when a direct connection is not possible (e.g. behind symmetric NAT).
101    pub relay: relay::client::Behaviour,
102}
103
104impl BillPouchBehaviour {
105    /// Build the combined behaviour from a keypair and a relay client handle.
106    ///
107    /// The `relay_client` is obtained from the `SwarmBuilder::with_relay_client()`
108    /// step and must be passed in rather than constructed here.
109    ///
110    /// Configures:
111    /// - Gossipsub with strict message signing and a 10-second heartbeat.
112    /// - Kademlia with an in-memory record store.
113    /// - Identify with the `/billpouch/id/1.0.0` protocol string.
114    /// - mDNS with default settings.
115    /// - RequestResponse with the `/billpouch/fragment/1.1.0` protocol.
116    /// - AutoNAT with default probe config.
117    /// - Relay client (handle passed by the SwarmBuilder).
118    ///
119    /// # Errors
120    /// Returns an error if gossipsub config validation fails or mDNS cannot
121    /// bind its multicast socket.
122    pub fn new(keypair: &Keypair, relay_client: relay::client::Behaviour) -> anyhow::Result<Self> {
123        let peer_id = PeerId::from_public_key(&keypair.public());
124
125        // ── Gossipsub ─────────────────────────────────────────────────────────
126        let gossipsub_config = gossipsub::ConfigBuilder::default()
127            .heartbeat_interval(Duration::from_secs(10))
128            .validation_mode(gossipsub::ValidationMode::Strict)
129            .history_gossip(5)
130            .build()
131            .map_err(|e| anyhow::anyhow!("Gossipsub config error: {}", e))?;
132
133        let gossipsub = gossipsub::Behaviour::new(
134            gossipsub::MessageAuthenticity::Signed(keypair.clone()),
135            gossipsub_config,
136        )
137        .map_err(|e| anyhow::anyhow!("Gossipsub init error: {}", e))?;
138
139        // ── Kademlia ──────────────────────────────────────────────────────────
140        let store = kad::store::MemoryStore::new(peer_id);
141        let kad = kad::Behaviour::new(peer_id, store);
142
143        // ── Identify ──────────────────────────────────────────────────────────
144        let identify = identify::Behaviour::new(identify::Config::new(
145            "/billpouch/id/1.0.0".into(),
146            keypair.public(),
147        ));
148
149        // ── mDNS ──────────────────────────────────────────────────────────────
150        let mdns = mdns::tokio::Behaviour::new(mdns::Config::default(), peer_id)?;
151
152        // ── Fragment exchange ──────────────────────────────────────────────────
153        let fragment_exchange = request_response::cbor::Behaviour::new(
154            [(
155                StreamProtocol::new("/billpouch/fragment/1.1.0"),
156                request_response::ProtocolSupport::Full,
157            )],
158            request_response::Config::default(),
159        );
160
161        // ── AutoNAT ────────────────────────────────────────────────────────────
162        let autonat = autonat::Behaviour::new(peer_id, autonat::Config::default());
163
164        Ok(Self {
165            gossipsub,
166            kad,
167            identify,
168            mdns,
169            fragment_exchange,
170            autonat,
171            relay: relay_client,
172        })
173    }
174}