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}