use crate::{
checks::application::bitcoin::{
check_magic_number, check_minimum_length, validate_command_bytes, validate_total_length,
},
errors::application::bitcoin::BitcoinError,
};
#[cfg_attr(doc, aquamarine::aquamarine)]
#[derive(Debug)]
pub struct BitcoinPacket {
pub magic: u32,
pub command: String,
pub length: u32,
pub checksum: [u8; 4],
pub payload: Vec<u8>,
}
fn extract_command(payload: &[u8]) -> Result<String, BitcoinError> {
let bytes = &payload[4..16];
validate_command_bytes(bytes)?;
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
let command = std::str::from_utf8(&bytes[..end])
.map_err(|_| BitcoinError::InvalidCommandBytes)?
.to_string();
Ok(command)
}
fn extract_length(payload: &[u8]) -> u32 {
u32::from_le_bytes([payload[16], payload[17], payload[18], payload[19]])
}
fn extract_checksum(payload: &[u8]) -> [u8; 4] {
[payload[20], payload[21], payload[22], payload[23]]
}
fn extract_payload(payload: &[u8]) -> Vec<u8> {
payload[24..].to_vec()
}
impl TryFrom<&[u8]> for BitcoinPacket {
type Error = BitcoinError;
fn try_from(payload: &[u8]) -> Result<Self, Self::Error> {
check_minimum_length(payload)?;
let magic = check_magic_number(payload)?;
let command = extract_command(payload)?;
let checksum = extract_checksum(payload);
let length = extract_length(payload);
validate_total_length(payload, length)?;
let actual_payload = extract_payload(payload);
Ok(BitcoinPacket {
magic,
command,
length,
checksum,
payload: actual_payload,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_checksum() {
let payload = vec![
0xF9, 0xBE, 0xB4, 0xD9, 0x76, 0x65, 0x72, 0x61, 0x63, 0x6B, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x5D, 0xF6, 0xE0, 0xE2, ];
let expected_checksum = [0x5D, 0xF6, 0xE0, 0xE2];
let extracted_checksum = extract_checksum(&payload);
assert_eq!(extracted_checksum, expected_checksum);
}
#[test]
fn test_extract_checksum_incorrect_length() {
let payload = vec![0xF9, 0xBE, 0xB4]; let result = std::panic::catch_unwind(|| extract_checksum(&payload));
assert!(
result.is_err(),
"Expected panic due to short payload length"
);
}
#[test]
fn test_valid_bitcoin_packet() {
let bitcoin_payload = vec![
0xF9, 0xBE, 0xB4, 0xD9, 0x76, 0x65, 0x72, 0x61, 0x63, 0x6B, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x5D, 0xF6, 0xE0, 0xE2, ];
match BitcoinPacket::try_from(bitcoin_payload.as_slice()) {
Ok(packet) => {
assert_eq!(packet.magic, 3652501241);
assert_eq!(packet.command, "verack");
assert_eq!(packet.length, 0);
assert_eq!(packet.checksum, [0x5D, 0xF6, 0xE0, 0xE2]);
assert_eq!(packet.payload.len(), 0);
}
Err(_) => panic!("Expected Bitcoin packet"),
}
}
#[test]
fn test_invalid_magic_number() {
let invalid_magic_number = vec![
0x99, 0xBE, 0xB4, 0xD9, 0x76, 0x65, 0x72, 0x61, 0x63, 0x6B, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x5D, 0xF6, 0xE0, 0xE2, ];
let err = BitcoinPacket::try_from(invalid_magic_number.as_slice()).unwrap_err();
assert!(matches!(
err,
BitcoinError::InvalidMagic { magic: 0xD9B4BE99 }
));
}
#[test]
fn test_short_payload() {
let short_payload = vec![0xF9, 0xBE, 0xB4]; match BitcoinPacket::try_from(short_payload.as_slice()) {
Ok(_) => panic!("Expected non-Bitcoin packet due to short payload"),
Err(is_bitcoin) => assert!(is_bitcoin == BitcoinError::PacketTooShort { actual: 3 }),
}
}
#[test]
fn test_invalid_length() {
let invalid_length = vec![
0xF9, 0xBE, 0xB4, 0xD9, 0x76, 0x65, 0x72, 0x61, 0x63, 0x6B, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
let err = BitcoinPacket::try_from(invalid_length.as_slice()).unwrap_err();
assert!(matches!(
err,
BitcoinError::LengthMismatch { declared: 5, .. }
));
}
#[test]
fn test_check_minimum_length() {
assert!(check_minimum_length(&[0u8; 24]).is_ok());
assert!(check_minimum_length(&[0u8; 23]).is_err());
}
#[test]
fn test_check_magic_number() {
assert_eq!(
check_magic_number(&[0xF9, 0xBE, 0xB4, 0xD9]).unwrap(),
0xD9B4BEF9
);
assert!(check_magic_number(&[0x99, 0xBE, 0xB4, 0xD9]).is_err());
}
#[test]
fn test_extract_command() {
assert_eq!(
extract_command(&[
0xF9, 0xBE, 0xB4, 0xD9, 0x76, 0x65, 0x72, 0x61, 0x63, 0x6B, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00
])
.unwrap(),
"verack"
);
}
#[test]
fn test_extract_length() {
assert_eq!(
extract_length(&[
0xF9, 0xBE, 0xB4, 0xD9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x05, 0x00, 0x00, 0x00, ]),
5
);
}
#[test]
fn test_extract_payload() {
assert_eq!(
extract_payload(&[
0xF9, 0xBE, 0xB4, 0xD9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04,
0x05
]),
vec![0x01, 0x02, 0x03, 0x04, 0x05]
);
}
}