1use 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
16pub struct V2Transport {
18 send_key: [u8; 32],
20 recv_key: [u8; 32],
22 send_cipher: ChaCha20Poly1305,
24 recv_cipher: ChaCha20Poly1305,
26 send_nonce: u64,
28 recv_nonce: u64,
30}
31
32pub enum V2Handshake {
34 Initiator {
36 private_key: [u8; 32],
37 ellswift: [u8; 64],
38 },
39 Responder {
41 private_key: [u8; 32],
42 initiator_ellswift: Option<[u8; 64]>,
43 },
44}
45
46const 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 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 pub fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>> {
84 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 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 self.send_nonce += 1;
99
100 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 let mut packet = Vec::with_capacity(20 + ciphertext.len());
109
110 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 let payload_len = ciphertext.len() - 16; 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 packet.push(0);
135
136 packet.extend_from_slice(&ciphertext[..tag_start]);
138
139 Ok(packet)
140 }
141
142 pub fn decrypt(&mut self, packet: &[u8]) -> Result<Vec<u8>> {
144 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 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 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 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 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 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 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 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 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 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 let shared_x = ellswift_xdh(
265 &initiator_ell64,
266 &responder_ell64,
267 &responder_private,
268 true, )
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 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 let shared_x = ellswift_xdh(
315 &initiator_ell64,
316 &responder_ell64,
317 &private_key,
318 false, )
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
328fn 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 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 let seckey = [0x01u8; 32];
374 let ell = ellswift_create(&seckey, None).expect("valid key");
375 assert_eq!(ell.len(), 64);
376 let shared = ellswift_xdh(&ell, &ell, &seckey, false);
378 assert!(shared.is_some(), "XDH should not fail on valid inputs");
379 }
380}