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