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 chacha20poly1305::{
9    aead::{Aead, AeadCore, KeyInit},
10    ChaCha20Poly1305, Key, Nonce,
11};
12use getrandom::getrandom;
13use secp256k1::{
14    ellswift::{ElligatorSwift, ElligatorSwiftSharedSecret},
15    PublicKey, Scalar, Secp256k1, SecretKey, XOnlyPublicKey,
16};
17use sha2::{Digest, Sha256};
18use std::borrow::Cow;
19
20/// BIP324 v2 transport encryption state
21pub struct V2Transport {
22    /// Send key material (updated on rekey per BIP324 FSChaCha20Poly1305)
23    send_key: [u8; 32],
24    /// Receive key material (updated on rekey)
25    recv_key: [u8; 32],
26    /// Send cipher (encrypt outgoing messages)
27    send_cipher: ChaCha20Poly1305,
28    /// Receive cipher (decrypt incoming messages)
29    recv_cipher: ChaCha20Poly1305,
30    /// Send nonce counter
31    send_nonce: u64,
32    /// Receive nonce counter
33    recv_nonce: u64,
34}
35
36/// BIP324 handshake state
37pub enum V2Handshake {
38    /// Initiator handshake (client connecting)
39    Initiator {
40        private_key: SecretKey,
41        ellswift: ElligatorSwift, // Store our ElligatorSwift encoding
42    },
43    /// Responder handshake (server accepting)
44    Responder {
45        private_key: SecretKey,
46        initiator_ellswift: Option<ElligatorSwift>, // Store peer's ElligatorSwift
47    },
48}
49
50/// BIP324: rekey after this many AEAD operations per direction (`REKEY_INTERVAL`).
51const REKEY_INTERVAL: u64 = 224;
52
53fn rekey_derive_next_key(key: &[u8; 32], rekey_epoch: u64) -> Result<[u8; 32]> {
54    let mut rekey_nonce = [0u8; 12];
55    rekey_nonce[0..4].copy_from_slice(&[0xff, 0xff, 0xff, 0xff]);
56    rekey_nonce[4..12].copy_from_slice(&rekey_epoch.to_le_bytes());
57    let cipher = ChaCha20Poly1305::new(Key::from_slice(key));
58    let ct = cipher
59        .encrypt(Nonce::from_slice(&rekey_nonce), [0u8; 32].as_slice())
60        .map_err(|e| {
61            ProtocolError::Consensus(blvm_consensus::error::ConsensusError::Serialization(
62                Cow::Owned(format!("BIP324 rekey encrypt failed: {e}")),
63            ))
64        })?;
65    let mut new_key = [0u8; 32];
66    new_key.copy_from_slice(&ct[..32]);
67    Ok(new_key)
68}
69
70impl V2Transport {
71    /// Create a new v2 transport with established keys
72    pub fn new(send_key: [u8; 32], recv_key: [u8; 32]) -> Self {
73        let send_cipher = ChaCha20Poly1305::new(&Key::from_slice(&send_key));
74        let recv_cipher = ChaCha20Poly1305::new(&Key::from_slice(&recv_key));
75
76        Self {
77            send_key,
78            recv_key,
79            send_cipher,
80            recv_cipher,
81            send_nonce: 0,
82            recv_nonce: 0,
83        }
84    }
85
86    /// Encrypt a message for sending
87    pub fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>> {
88        // BIP324 packet format: [16-byte poly1305 tag][3-byte length][1-byte ignored][encrypted payload]
89        // Nonce format: 12 bytes (8-byte counter + 4-byte zero padding)
90        let mut nonce_bytes = [0u8; 12];
91        nonce_bytes[..8].copy_from_slice(&self.send_nonce.to_le_bytes());
92        let nonce = Nonce::from_slice(&nonce_bytes);
93
94        // Encrypt plaintext
95        let ciphertext = self.send_cipher.encrypt(nonce, plaintext).map_err(|e| {
96            ProtocolError::Consensus(blvm_consensus::error::ConsensusError::Serialization(
97                Cow::Owned(format!("Encryption failed: {}", e)),
98            ))
99        })?;
100
101        // Increment nonce counter
102        self.send_nonce += 1;
103
104        // BIP324 FSChaCha20Poly1305: rekey every REKEY_INTERVAL packets
105        if self.send_nonce.is_multiple_of(REKEY_INTERVAL) {
106            let rekey_epoch = (self.send_nonce / REKEY_INTERVAL) - 1;
107            self.send_key = rekey_derive_next_key(&self.send_key, rekey_epoch)?;
108            self.send_cipher = ChaCha20Poly1305::new(Key::from_slice(&self.send_key));
109        }
110
111        // Build packet: [tag(16)][length(3)][ignored(1)][payload(var)]
112        let mut packet = Vec::with_capacity(20 + ciphertext.len());
113
114        // Extract tag (last 16 bytes of ciphertext are the tag)
115        if ciphertext.len() < 16 {
116            return Err(ProtocolError::Consensus(
117                blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
118                    "Ciphertext too short".to_string(),
119                )),
120            ));
121        }
122        let tag_start = ciphertext.len() - 16;
123        packet.extend_from_slice(&ciphertext[tag_start..]);
124
125        // Length (3 bytes, little-endian, max 2^24-1)
126        let payload_len = ciphertext.len() - 16; // Exclude tag
127        if payload_len > 0xFFFFFF {
128            return Err(ProtocolError::Consensus(
129                blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
130                    "Payload too large".to_string(),
131                )),
132            ));
133        }
134        let len_bytes = (payload_len as u32).to_le_bytes();
135        packet.extend_from_slice(&len_bytes[..3]);
136
137        // Ignored byte (set to 0)
138        packet.push(0);
139
140        // Payload (ciphertext without tag)
141        packet.extend_from_slice(&ciphertext[..tag_start]);
142
143        Ok(packet)
144    }
145
146    /// Decrypt a received message
147    pub fn decrypt(&mut self, packet: &[u8]) -> Result<Vec<u8>> {
148        // BIP324 packet format: [16-byte poly1305 tag][3-byte length][1-byte ignored][encrypted payload]
149        if packet.len() < 20 {
150            return Err(ProtocolError::Consensus(
151                blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
152                    "Packet too short".to_string(),
153                )),
154            ));
155        }
156
157        // Extract components
158        let tag = &packet[0..16];
159        let length_bytes = [packet[16], packet[17], packet[18], 0];
160        let payload_len = u32::from_le_bytes(length_bytes) as usize;
161        // Ignore byte at packet[19]
162        let payload_start = 20;
163        let payload_end = payload_start + payload_len;
164
165        if packet.len() < payload_end {
166            return Err(ProtocolError::Consensus(
167                blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
168                    "Packet incomplete".to_string(),
169                )),
170            ));
171        }
172
173        // Reconstruct ciphertext: payload + tag
174        let mut ciphertext = Vec::with_capacity(payload_len + 16);
175        ciphertext.extend_from_slice(&packet[payload_start..payload_end]);
176        ciphertext.extend_from_slice(tag);
177
178        // Nonce format: 12 bytes (8-byte counter + 4-byte zero padding)
179        let mut nonce_bytes = [0u8; 12];
180        nonce_bytes[..8].copy_from_slice(&self.recv_nonce.to_le_bytes());
181        let nonce = Nonce::from_slice(&nonce_bytes);
182
183        // Decrypt
184        let plaintext = self
185            .recv_cipher
186            .decrypt(nonce, ciphertext.as_slice())
187            .map_err(|e| {
188                ProtocolError::Consensus(blvm_consensus::error::ConsensusError::Serialization(
189                    Cow::Owned(format!("Decryption failed: {}", e)),
190                ))
191            })?;
192
193        // Increment nonce counter
194        self.recv_nonce += 1;
195
196        if self.recv_nonce.is_multiple_of(REKEY_INTERVAL) {
197            let rekey_epoch = (self.recv_nonce / REKEY_INTERVAL) - 1;
198            self.recv_key = rekey_derive_next_key(&self.recv_key, rekey_epoch)?;
199            self.recv_cipher = ChaCha20Poly1305::new(Key::from_slice(&self.recv_key));
200        }
201
202        Ok(plaintext)
203    }
204}
205
206impl V2Handshake {
207    /// Create a new initiator handshake
208    pub fn new_initiator() -> (Vec<u8>, Self) {
209        let secp = Secp256k1::new();
210        // Generate random 32 bytes for secret key
211        let mut key_bytes = [0u8; 32];
212        getrandom(&mut key_bytes).expect("Failed to generate random bytes");
213        let private_key = SecretKey::from_slice(&key_bytes).expect("Failed to generate secret key");
214
215        // Generate random aux_rand for ElligatorSwift encoding
216        let mut aux_rand = [0u8; 32];
217        getrandom(&mut aux_rand).expect("Failed to generate aux_rand");
218
219        // Create ElligatorSwift encoding from secret key (BIP324-compatible)
220        let ellswift = ElligatorSwift::from_seckey(&secp, private_key, Some(aux_rand));
221        let encoded = ellswift.to_array();
222
223        (
224            encoded.to_vec(),
225            Self::Initiator {
226                private_key,
227                ellswift,
228            },
229        )
230    }
231
232    /// Create a new responder handshake
233    pub fn new_responder() -> Self {
234        // Generate random 32 bytes for secret key
235        let mut key_bytes = [0u8; 32];
236        getrandom(&mut key_bytes).expect("Failed to generate random bytes");
237        let private_key = SecretKey::from_slice(&key_bytes).expect("Failed to generate secret key");
238
239        Self::Responder {
240            private_key,
241            initiator_ellswift: None,
242        }
243    }
244
245    /// Process initiator message (responder side)
246    pub fn process_initiator_message(
247        &mut self,
248        initiator_msg: &[u8],
249    ) -> Result<(Vec<u8>, V2Transport)> {
250        if initiator_msg.len() != 64 {
251            return Err(ProtocolError::Consensus(
252                blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
253                    "Invalid initiator message length".to_string(),
254                )),
255            ));
256        }
257
258        // Decode initiator's ElligatorSwift encoding
259        let mut initiator_msg_array = [0u8; 64];
260        initiator_msg_array.copy_from_slice(initiator_msg);
261        let initiator_ellswift = elligator_swift_decode(&initiator_msg_array);
262
263        // Generate responder's ElligatorSwift encoding
264        let secp = Secp256k1::new();
265        let responder_private = match self {
266            Self::Responder { private_key, .. } => *private_key,
267            _ => {
268                return Err(ProtocolError::Consensus(
269                    blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
270                        "Not a responder handshake".to_string(),
271                    )),
272                ));
273            }
274        };
275
276        // Generate random aux_rand for ElligatorSwift encoding
277        let mut aux_rand = [0u8; 32];
278        getrandom(&mut aux_rand).expect("Failed to generate aux_rand");
279
280        // Create ElligatorSwift encoding from secret key (BIP324-compatible)
281        let responder_ellswift =
282            ElligatorSwift::from_seckey(&secp, responder_private, Some(aux_rand));
283        let responder_ellswift_bytes = responder_ellswift.to_array();
284
285        // Perform X-only ECDH using ElligatorSwift shared secret (BIP324-compatible)
286        // Use secp256k1's built-in shared_secret computation with ElligatorSwift objects
287        use secp256k1::ellswift::ElligatorSwiftParty;
288
289        // Compute shared secret using ElligatorSwift::shared_secret (BIP324-compatible)
290        let shared_secret = ElligatorSwift::shared_secret(
291            initiator_ellswift,
292            responder_ellswift,
293            responder_private,
294            ElligatorSwiftParty::B, // Responder is party B
295            None,                   // No additional data for BIP324
296        );
297
298        let shared_x = shared_secret.to_secret_bytes();
299
300        // Derive keys using HKDF
301        let send_key = hkdf_sha256(&shared_x, b"bitcoin_v2_shared_secret_send");
302        let recv_key = hkdf_sha256(&shared_x, b"bitcoin_v2_shared_secret_recv");
303
304        // Create transport
305        let transport = V2Transport::new(send_key, recv_key);
306
307        // Update handshake state
308        if let Self::Responder {
309            initiator_ellswift: ref mut iell,
310            ..
311        } = self
312        {
313            *iell = Some(initiator_ellswift);
314        }
315
316        Ok((responder_ellswift_bytes.to_vec(), transport))
317    }
318
319    /// Complete handshake (initiator side)
320    pub fn complete_handshake(self, responder_msg: &[u8]) -> Result<V2Transport> {
321        if responder_msg.len() != 64 {
322            return Err(ProtocolError::Consensus(
323                blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
324                    "Invalid responder message length".to_string(),
325                )),
326            ));
327        }
328
329        // Decode responder's ElligatorSwift encoding
330        let mut responder_msg_array = [0u8; 64];
331        responder_msg_array.copy_from_slice(responder_msg);
332        let responder_ellswift = elligator_swift_decode(&responder_msg_array);
333
334        // Perform X-only ECDH using ElligatorSwift shared secret (BIP324-compatible)
335        // Use secp256k1's built-in shared_secret computation with ElligatorSwift objects
336        use secp256k1::ellswift::ElligatorSwiftParty;
337        let (private_key, initiator_ellswift) = match self {
338            Self::Initiator {
339                private_key,
340                ellswift,
341                ..
342            } => {
343                (private_key, ellswift) // ellswift is Copy, private_key is not
344            }
345            _ => {
346                return Err(ProtocolError::Consensus(
347                    blvm_consensus::error::ConsensusError::Serialization(Cow::Owned(
348                        "Not an initiator handshake".to_string(),
349                    )),
350                ));
351            }
352        };
353
354        // Compute shared secret using ElligatorSwift::shared_secret (BIP324-compatible)
355        let shared_secret = ElligatorSwift::shared_secret(
356            initiator_ellswift,
357            responder_ellswift,
358            private_key,
359            ElligatorSwiftParty::A, // Initiator is party A
360            None,                   // No additional data for BIP324
361        );
362
363        let shared_x = shared_secret.to_secret_bytes();
364
365        // Derive keys using HKDF
366        let send_key = hkdf_sha256(&shared_x, b"bitcoin_v2_shared_secret_send");
367        let recv_key = hkdf_sha256(&shared_x, b"bitcoin_v2_shared_secret_recv");
368
369        // Create transport
370        Ok(V2Transport::new(send_key, recv_key))
371    }
372}
373
374/// ElligatorSwift encoding (BIP324)
375/// Encodes a secp256k1 public key to 64 bytes using secp256k1's ElligatorSwift implementation
376fn elligator_swift_encode(pubkey: &PublicKey) -> [u8; 64] {
377    // Use secp256k1's built-in ElligatorSwift encoding
378    let ellswift = ElligatorSwift::from_pubkey(*pubkey);
379    ellswift.to_array()
380}
381
382/// ElligatorSwift decoding (BIP324)
383/// Decodes 64 bytes to an ElligatorSwift object (not directly to PublicKey)
384///
385/// Note: For BIP324, we work with ElligatorSwift objects directly in the handshake.
386/// The shared secret computation uses ElligatorSwift objects, not raw public keys.
387fn elligator_swift_decode(encoded: &[u8; 64]) -> ElligatorSwift {
388    ElligatorSwift::from_array(*encoded)
389}
390
391// Note: xonly_ecdh function removed - we now use ElligatorSwiftSharedSecret directly
392// in the handshake functions, which is the proper BIP324 approach using secp256k1's library.
393
394/// HKDF-SHA256 key derivation (BIP324)
395/// Uses the hkdf library for proper HMAC-SHA256-based key derivation
396fn hkdf_sha256(ikm: &[u8], info: &[u8]) -> [u8; 32] {
397    use hkdf::Hkdf;
398
399    // BIP324 uses HKDF with empty salt and info parameter
400    let hk = Hkdf::<sha2::Sha256>::new(None, ikm);
401    let mut okm = [0u8; 32];
402    hk.expand(info, &mut okm)
403        .expect("HKDF expansion failed (should never happen for 32-byte output)");
404    okm
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410
411    #[test]
412    fn test_v2_transport_encrypt_decrypt() {
413        // Use the same key for both send and recv for testing
414        // In a real BIP324 connection, send_key_A == recv_key_B (derived from shared secret)
415        let key = [0x42; 32];
416        let mut transport = V2Transport::new(key, key);
417
418        let plaintext = b"Hello, Bitcoin!";
419        let encrypted = transport.encrypt(plaintext).unwrap();
420        let decrypted = transport.decrypt(&encrypted).unwrap();
421
422        assert_eq!(plaintext, decrypted.as_slice());
423    }
424
425    #[test]
426    fn test_v2_transport_rekey_round_trip() {
427        let key = [0x11; 32];
428        let mut enc = V2Transport::new(key, key);
429        let mut dec = V2Transport::new(key, key);
430        for i in 0..300 {
431            let pt = format!("msg{i}").into_bytes();
432            let pkt = enc.encrypt(&pt).unwrap();
433            let out = dec.decrypt(&pkt).unwrap();
434            assert_eq!(pt, out, "round-trip failed at i={i}");
435        }
436    }
437
438    #[test]
439    fn test_elligator_swift_encode_decode() {
440        let secp = Secp256k1::new();
441        let private_key = SecretKey::from_slice(&[0x01; 32]).unwrap();
442        let public_key = PublicKey::from_secret_key(&secp, &private_key);
443
444        let encoded = elligator_swift_encode(&public_key);
445        let decoded_ellswift = elligator_swift_decode(&encoded);
446
447        // Verify encoding/decoding works (ElligatorSwift objects should match)
448        let re_encoded = decoded_ellswift.to_array();
449        assert_eq!(encoded, re_encoded);
450    }
451}