Skip to main content

blvm_protocol/
v2_transport.rs

1//! BIP324: Version 2 P2P Encrypted Transport Protocol
2//!
3//! This module implements the encrypted transport protocol for Bitcoin P2P connections.
4//! It provides ElligatorSwift encoding, X-only ECDH key exchange, and ChaCha20Poly1305 encryption.
5
6use crate::error::ProtocolError;
7use crate::Result;
8use blvm_secp256k1::ellswift::{ellswift_create, ellswift_xdh};
9use chacha20poly1305::{
10    aead::{Aead, AeadCore, KeyInit},
11    ChaCha20Poly1305, Key, Nonce,
12};
13use getrandom::getrandom;
14use sha2::{Digest, Sha256};
15use std::borrow::Cow;
16
17/// BIP324 v2 transport encryption state
18pub struct V2Transport {
19    /// Send key material (updated on rekey per BIP324 FSChaCha20Poly1305)
20    send_key: [u8; 32],
21    /// Receive key material (updated on rekey)
22    recv_key: [u8; 32],
23    /// Send cipher (encrypt outgoing messages)
24    send_cipher: ChaCha20Poly1305,
25    /// Receive cipher (decrypt incoming messages)
26    recv_cipher: ChaCha20Poly1305,
27    /// Send nonce counter
28    send_nonce: u64,
29    /// Receive nonce counter
30    recv_nonce: u64,
31}
32
33/// BIP324 handshake state
34pub enum V2Handshake {
35    /// Initiator handshake (client connecting)
36    Initiator {
37        private_key: [u8; 32],
38        ellswift: [u8; 64],
39    },
40    /// Responder handshake (server accepting)
41    Responder {
42        private_key: [u8; 32],
43        initiator_ellswift: Option<[u8; 64]>,
44    },
45}
46
47/// BIP324: rekey after this many AEAD operations per direction (`REKEY_INTERVAL`).
48const REKEY_INTERVAL: u64 = 224;
49
50fn rekey_derive_next_key(key: &[u8; 32], rekey_epoch: u64) -> Result<[u8; 32]> {
51    let mut rekey_nonce = [0u8; 12];
52    rekey_nonce[0..4].copy_from_slice(&[0xff, 0xff, 0xff, 0xff]);
53    rekey_nonce[4..12].copy_from_slice(&rekey_epoch.to_le_bytes());
54    let cipher = ChaCha20Poly1305::new(Key::from_slice(key));
55    let ct = cipher
56        .encrypt(Nonce::from_slice(&rekey_nonce), [0u8; 32].as_slice())
57        .map_err(|e| {
58            ProtocolError::Consensus(blvm_consensus::error::ConsensusError::Serialization(
59                Cow::Owned(format!("BIP324 rekey encrypt failed: {e}")),
60            ))
61        })?;
62    let mut new_key = [0u8; 32];
63    new_key.copy_from_slice(&ct[..32]);
64    Ok(new_key)
65}
66
67impl V2Transport {
68    /// Create a new v2 transport with established keys
69    pub fn new(send_key: [u8; 32], recv_key: [u8; 32]) -> Self {
70        let send_cipher = ChaCha20Poly1305::new(&Key::from_slice(&send_key));
71        let recv_cipher = ChaCha20Poly1305::new(&Key::from_slice(&recv_key));
72
73        Self {
74            send_key,
75            recv_key,
76            send_cipher,
77            recv_cipher,
78            send_nonce: 0,
79            recv_nonce: 0,
80        }
81    }
82
83    /// Encrypt a message for sending
84    pub fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>> {
85        // BIP324 packet format: [16-byte poly1305 tag][3-byte length][1-byte ignored][encrypted payload]
86        // Nonce format: 12 bytes (8-byte counter + 4-byte zero padding)
87        let mut nonce_bytes = [0u8; 12];
88        nonce_bytes[..8].copy_from_slice(&self.send_nonce.to_le_bytes());
89        let nonce = Nonce::from_slice(&nonce_bytes);
90
91        // Encrypt plaintext
92        let ciphertext = self.send_cipher.encrypt(nonce, plaintext).map_err(|e| {
93            ProtocolError::Consensus(blvm_consensus::error::ConsensusError::Serialization(
94                Cow::Owned(format!("Encryption failed: {}", e)),
95            ))
96        })?;
97
98        // Increment nonce counter
99        self.send_nonce += 1;
100
101        // BIP324 FSChaCha20Poly1305: rekey every REKEY_INTERVAL packets
102        if self.send_nonce.is_multiple_of(REKEY_INTERVAL) {
103            let rekey_epoch = (self.send_nonce / REKEY_INTERVAL) - 1;
104            self.send_key = rekey_derive_next_key(&self.send_key, rekey_epoch)?;
105            self.send_cipher = ChaCha20Poly1305::new(Key::from_slice(&self.send_key));
106        }
107
108        // Build packet: [tag(16)][length(3)][ignored(1)][payload(var)]
109        let mut packet = Vec::with_capacity(20 + ciphertext.len());
110
111        // Extract tag (last 16 bytes of ciphertext are the tag)
112        if ciphertext.len() < 16 {
113            return Err(ProtocolError::Consensus(
114                blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
115                    "Ciphertext too short".to_string(),
116                )),
117            ));
118        }
119        let tag_start = ciphertext.len() - 16;
120        packet.extend_from_slice(&ciphertext[tag_start..]);
121
122        // Length (3 bytes, little-endian, max 2^24-1)
123        let payload_len = ciphertext.len() - 16; // Exclude tag
124        if payload_len > 0xFFFFFF {
125            return Err(ProtocolError::Consensus(
126                blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
127                    "Payload too large".to_string(),
128                )),
129            ));
130        }
131        let len_bytes = (payload_len as u32).to_le_bytes();
132        packet.extend_from_slice(&len_bytes[..3]);
133
134        // Ignored byte (set to 0)
135        packet.push(0);
136
137        // Payload (ciphertext without tag)
138        packet.extend_from_slice(&ciphertext[..tag_start]);
139
140        Ok(packet)
141    }
142
143    /// Decrypt a received message
144    pub fn decrypt(&mut self, packet: &[u8]) -> Result<Vec<u8>> {
145        // BIP324 packet format: [16-byte poly1305 tag][3-byte length][1-byte ignored][encrypted payload]
146        if packet.len() < 20 {
147            return Err(ProtocolError::Consensus(
148                blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
149                    "Packet too short".to_string(),
150                )),
151            ));
152        }
153
154        // Extract components
155        let tag = &packet[0..16];
156        let length_bytes = [packet[16], packet[17], packet[18], 0];
157        let payload_len = u32::from_le_bytes(length_bytes) as usize;
158        // Ignore byte at packet[19]
159        let payload_start = 20;
160        let payload_end = payload_start + payload_len;
161
162        if packet.len() < payload_end {
163            return Err(ProtocolError::Consensus(
164                blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
165                    "Packet incomplete".to_string(),
166                )),
167            ));
168        }
169
170        // Reconstruct ciphertext: payload + tag
171        let mut ciphertext = Vec::with_capacity(payload_len + 16);
172        ciphertext.extend_from_slice(&packet[payload_start..payload_end]);
173        ciphertext.extend_from_slice(tag);
174
175        // Nonce format: 12 bytes (8-byte counter + 4-byte zero padding)
176        let mut nonce_bytes = [0u8; 12];
177        nonce_bytes[..8].copy_from_slice(&self.recv_nonce.to_le_bytes());
178        let nonce = Nonce::from_slice(&nonce_bytes);
179
180        // Decrypt
181        let plaintext = self
182            .recv_cipher
183            .decrypt(nonce, ciphertext.as_slice())
184            .map_err(|e| {
185                ProtocolError::Consensus(blvm_consensus::error::ConsensusError::Serialization(
186                    Cow::Owned(format!("Decryption failed: {}", e)),
187                ))
188            })?;
189
190        // Increment nonce counter
191        self.recv_nonce += 1;
192
193        if self.recv_nonce.is_multiple_of(REKEY_INTERVAL) {
194            let rekey_epoch = (self.recv_nonce / REKEY_INTERVAL) - 1;
195            self.recv_key = rekey_derive_next_key(&self.recv_key, rekey_epoch)?;
196            self.recv_cipher = ChaCha20Poly1305::new(Key::from_slice(&self.recv_key));
197        }
198
199        Ok(plaintext)
200    }
201}
202
203impl V2Handshake {
204    /// Create a new initiator handshake
205    pub fn new_initiator() -> (Vec<u8>, Self) {
206        let mut key_bytes = [0u8; 32];
207        getrandom(&mut key_bytes).expect("Failed to generate random bytes");
208        let mut aux_rand = [0u8; 32];
209        getrandom(&mut aux_rand).expect("Failed to generate aux_rand");
210        let ellswift = ellswift_create(&key_bytes, Some(&aux_rand))
211            .expect("Failed to create ElligatorSwift encoding");
212        (
213            ellswift.to_vec(),
214            Self::Initiator {
215                private_key: key_bytes,
216                ellswift,
217            },
218        )
219    }
220
221    /// Create a new responder handshake
222    pub fn new_responder() -> Self {
223        let mut key_bytes = [0u8; 32];
224        getrandom(&mut key_bytes).expect("Failed to generate random bytes");
225        Self::Responder {
226            private_key: key_bytes,
227            initiator_ellswift: None,
228        }
229    }
230
231    /// Process initiator message (responder side)
232    pub fn process_initiator_message(
233        &mut self,
234        initiator_msg: &[u8],
235    ) -> Result<(Vec<u8>, V2Transport)> {
236        if initiator_msg.len() != 64 {
237            return Err(ProtocolError::Consensus(
238                blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
239                    "Invalid initiator message length".to_string(),
240                )),
241            ));
242        }
243
244        let mut initiator_ell64 = [0u8; 64];
245        initiator_ell64.copy_from_slice(initiator_msg);
246
247        let responder_private = match self {
248            Self::Responder { private_key, .. } => *private_key,
249            _ => {
250                return Err(ProtocolError::Consensus(
251                    blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
252                        "Not a responder handshake".to_string(),
253                    )),
254                ));
255            }
256        };
257
258        let mut aux_rand = [0u8; 32];
259        getrandom(&mut aux_rand).expect("Failed to generate aux_rand");
260
261        let responder_ell64 = ellswift_create(&responder_private, Some(&aux_rand))
262            .expect("Failed to create ElligatorSwift encoding");
263
264        // X-only ECDH: responder is party B (party = true).
265        let shared_x = ellswift_xdh(
266            &initiator_ell64,
267            &responder_ell64,
268            &responder_private,
269            true, // B = responder
270        )
271        .expect("ElligatorSwift ECDH failed");
272
273        let send_key = hkdf_sha256(&shared_x, b"bitcoin_v2_shared_secret_send");
274        let recv_key = hkdf_sha256(&shared_x, b"bitcoin_v2_shared_secret_recv");
275        let transport = V2Transport::new(send_key, recv_key);
276
277        if let Self::Responder {
278            initiator_ellswift: ref mut iell,
279            ..
280        } = self
281        {
282            *iell = Some(initiator_ell64);
283        }
284
285        Ok((responder_ell64.to_vec(), transport))
286    }
287
288    /// Complete handshake (initiator side)
289    pub fn complete_handshake(self, responder_msg: &[u8]) -> Result<V2Transport> {
290        if responder_msg.len() != 64 {
291            return Err(ProtocolError::Consensus(
292                blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
293                    "Invalid responder message length".to_string(),
294                )),
295            ));
296        }
297
298        let mut responder_ell64 = [0u8; 64];
299        responder_ell64.copy_from_slice(responder_msg);
300
301        let (private_key, initiator_ell64) = match self {
302            Self::Initiator {
303                private_key,
304                ellswift,
305            } => (private_key, ellswift),
306            _ => {
307                return Err(ProtocolError::Consensus(
308                    blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
309                        "Not an initiator handshake".to_string(),
310                    )),
311                ));
312            }
313        };
314
315        // X-only ECDH: initiator is party A (party = false).
316        let shared_x = ellswift_xdh(
317            &initiator_ell64,
318            &responder_ell64,
319            &private_key,
320            false, // A = initiator
321        )
322        .expect("ElligatorSwift ECDH failed");
323
324        let send_key = hkdf_sha256(&shared_x, b"bitcoin_v2_shared_secret_send");
325        let recv_key = hkdf_sha256(&shared_x, b"bitcoin_v2_shared_secret_recv");
326        Ok(V2Transport::new(send_key, recv_key))
327    }
328}
329
330/// HKDF-SHA256 key derivation (BIP324).
331fn hkdf_sha256(ikm: &[u8], info: &[u8]) -> [u8; 32] {
332    use hkdf::Hkdf;
333    let hk = Hkdf::<sha2::Sha256>::new(None, ikm);
334    let mut okm = [0u8; 32];
335    hk.expand(info, &mut okm)
336        .expect("HKDF expansion failed (should never happen for 32-byte output)");
337    okm
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    #[test]
345    fn test_v2_transport_encrypt_decrypt() {
346        // Use the same key for both send and recv for testing
347        // In a real BIP324 connection, send_key_A == recv_key_B (derived from shared secret)
348        let key = [0x42; 32];
349        let mut transport = V2Transport::new(key, key);
350
351        let plaintext = b"Hello, Bitcoin!";
352        let encrypted = transport.encrypt(plaintext).unwrap();
353        let decrypted = transport.decrypt(&encrypted).unwrap();
354
355        assert_eq!(plaintext, decrypted.as_slice());
356    }
357
358    #[test]
359    fn test_v2_transport_rekey_round_trip() {
360        let key = [0x11; 32];
361        let mut enc = V2Transport::new(key, key);
362        let mut dec = V2Transport::new(key, key);
363        for i in 0..300 {
364            let pt = format!("msg{i}").into_bytes();
365            let pkt = enc.encrypt(&pt).unwrap();
366            let out = dec.decrypt(&pkt).unwrap();
367            assert_eq!(pt, out, "round-trip failed at i={i}");
368        }
369    }
370
371    #[test]
372    fn test_elligator_swift_encode_decode() {
373        use blvm_secp256k1::ellswift::{ellswift_create, ellswift_xdh};
374        // Create from a known secret key (no randomness for determinism).
375        let seckey = [0x01u8; 32];
376        let ell = ellswift_create(&seckey, None).expect("valid key");
377        assert_eq!(ell.len(), 64);
378        // Verify XDH with itself is consistent (reflexive sanity check).
379        let shared = ellswift_xdh(&ell, &ell, &seckey, false);
380        assert!(shared.is_some(), "XDH should not fail on valid inputs");
381    }
382}