diff --git a/Cargo.lock b/Cargo.lock index da97074..f8fc6b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,15 +113,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bitcoin-network" -version = "0.1.1" -source = "git+https://github.com/cyber-coop/bitcoin-network?branch=main#c9ce890b7fd9e9a9404b4f5d377451e0a8d770a5" -dependencies = [ - "bitcoin-varint", - "sha2", -] - [[package]] name = "bitcoin-varint" version = "0.1.0" @@ -573,15 +564,16 @@ dependencies = [ [[package]] name = "prototype" -version = "0.1.0" +version = "0.0.0" dependencies = [ - "bitcoin-network", + "bitcoin-varint", "env_logger", "hex", "log", "magic-bytes", "postgres", "serde", + "sha2", "toml", ] @@ -705,9 +697,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", diff --git a/Cargo.toml b/Cargo.toml index 2a71581..2c39788 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,5 @@ [package] name = "prototype" -version = "0.1.0" edition = "2021" authors = ["Lola Rigaut-Luczak "] @@ -8,10 +7,11 @@ authors = ["Lola Rigaut-Luczak "] [dependencies] hex = "0.4.3" -bitcoin-network = { git = "https://github.com/cyber-coop/bitcoin-network", branch = "main" } magic-bytes = "0.1.0" postgres = { version = "0.19" } log = "0.4" env_logger = "0.11" serde = { version = "1.0", features = ["derive"] } toml = "0.8" +sha2 = "0.10.9" +varint = {package = "bitcoin-varint", version = "0.1.0"} diff --git a/Makefile b/Makefile index 821c4ee..1e9c38c 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,5 @@ postgres: build-docker: docker build -t prototype . -run-docker: - docker run -it --rm -e NETWORK=dogecoin -e TESTNET=true --name prototype-dogecoin-testnet prototype - run: - RUST_LOG="prototype=trace" cargo r -- $(network) \ No newline at end of file + RUST_LOG="prototype=info" cargo r -- $(network) \ No newline at end of file diff --git a/TODO.md b/TODO.md index f8ce25e..dd28bf5 100644 --- a/TODO.md +++ b/TODO.md @@ -16,5 +16,5 @@ - [x] Resolve the mistery of the missing txid `8f311e28fb852de8456b1a55e68cf8e9be501fbfd8e386cc9c551b41f3e0809b` (probably wrong hash...) ... hash registered `\x83c4938e238bef8bf2b64333abf106226c68abd36a80f5b916cf1b6d8fe8d5bc` - [ ] Adding connection timeout - [ ] Need nodes fallback (or we can use dns look up to find nodes) -- [ ] Send an email once a long sql script is done (see `mail`) +- [ ] ~~Send an email once a long sql script is done (see `mail`)~~ - [ ] Create table on database at start \ No newline at end of file diff --git a/raw_50057.bin b/raw_50057.bin new file mode 100644 index 0000000..92a9f3e Binary files /dev/null and b/raw_50057.bin differ diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..5d56faf --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" diff --git a/src/database.rs b/src/database.rs index df63a2c..be5109d 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,4 +1,4 @@ -use bitcoin_network::block::Block; +use crate::p2p::block::Block; use postgres::Client; use std::io::prelude::*; use std::time::Instant; diff --git a/src/main.rs b/src/main.rs index 2502721..16fb41d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,19 @@ -use bitcoin_network::{block::Block, get_blocks::GetBlocks, get_data::GetData, message::Message}; -use std::env; -use std::sync::mpsc::sync_channel; -use std::thread; -use std::time::Instant; +#![feature(cursor_split)] pub mod configs; pub mod database; pub mod duplicate; pub mod networks; +pub mod p2p; pub mod peer; pub mod utils; +use p2p::{block::Block, get_blocks::GetBlocks, get_data::GetData, message::Message}; +use std::env; +use std::sync::mpsc::sync_channel; +use std::thread; +use std::time::Instant; + use crate::database::save_blocks; use crate::peer::Peer; diff --git a/src/p2p/address.rs b/src/p2p/address.rs new file mode 100644 index 0000000..2f5612a --- /dev/null +++ b/src/p2p/address.rs @@ -0,0 +1,38 @@ +use super::error::DeserializeError; +use std::io::{Cursor, Read}; + +#[derive(Debug, Clone, PartialEq)] +pub struct Address { + pub services: u64, + pub ip: [u8; 16], + pub port: u16, +} + +impl Address { + pub fn serialize(&self) -> Vec { + let mut result: Vec = vec![]; + result.extend_from_slice(&self.services.to_le_bytes()); + result.extend_from_slice(&self.ip); + result.extend_from_slice(&self.port.to_le_bytes()); + + result + } + + pub fn deserialize(bytes: &[u8]) -> Result { + let mut cur = Cursor::new(bytes); + + let mut buf = [0u8; 8]; + cur.read_exact(&mut buf)?; + let services = u64::from_le_bytes(buf); + + let mut buf = [0u8; 16]; + cur.read_exact(&mut buf)?; + let ip = buf; + + let mut buf = [0u8; 2]; + cur.read_exact(&mut buf)?; + let port = u16::from_le_bytes(buf); + + Ok(Self { services, ip, port }) + } +} diff --git a/src/p2p/block.rs b/src/p2p/block.rs new file mode 100644 index 0000000..9f7558a --- /dev/null +++ b/src/p2p/block.rs @@ -0,0 +1,208 @@ +use super::error::DeserializeError; +use super::tx::{Tx, TxIn, TxOut}; +use super::utils; +use std::io::{Cursor, Read}; +use varint::VarInt; + +const BLOCK_VERSION_AUXPOW_BIT: u32 = 0x100; + +#[derive(Debug, Clone, PartialEq)] +pub struct Block { + pub version: u32, + // auxpow header (to be compatible with Namecoin and Dogecoin) + pub auxpow_header: Option, + pub previous_hash: [u8; 32], + pub merkle_root: [u8; 32], + pub timestamp: u32, + pub bits: u32, + pub nonce: u32, + pub transactions: Vec, +} + +impl Block { + pub fn hash(&self) -> [u8; 32] { + let block_header = &self.serialize_header(); + utils::double_hash(block_header) + } + + pub fn serialize_header(&self) -> Vec { + let mut result: Vec = vec![]; + result.extend(self.version.to_le_bytes()); + result.extend(self.previous_hash); + result.extend(self.merkle_root); + result.extend(self.timestamp.to_le_bytes()); + result.extend(self.bits.to_le_bytes()); + result.extend(self.nonce.to_le_bytes()); + result + } + + pub fn serialize(&self) -> Vec { + let mut result: Vec = vec![]; + result.extend(self.version.to_le_bytes()); + result.extend(self.previous_hash); + result.extend(self.merkle_root); + result.extend(self.timestamp.to_le_bytes()); + result.extend(self.bits.to_le_bytes()); + result.extend(self.nonce.to_le_bytes()); + //TODO: serialize transactions + + /*self.transactions + .iter() + .for_each(|t| { + result.extend(t.serialize()); + });*/ + + result + } + + pub fn deserialize(bytes: &[u8], auxpow_activated: bool) -> Result { + let mut cur = Cursor::new(bytes); + + // Block headers + let mut buf = [0u8; 4]; + cur.read_exact(&mut buf)?; + let version = u32::from_le_bytes(buf); + + let mut buf = [0u8; 32]; + cur.read_exact(&mut buf)?; + let previous_hash = buf; + + let mut buf = [0u8; 32]; + cur.read_exact(&mut buf)?; + let merkle_root = buf; + + let mut buf = [0u8; 4]; + cur.read_exact(&mut buf)?; + let timestamp = u32::from_le_bytes(buf); + + let mut buf = [0u8; 4]; + cur.read_exact(&mut buf)?; + let bits = u32::from_le_bytes(buf); + + let mut buf = [0u8; 4]; + cur.read_exact(&mut buf)?; + let nonce = u32::from_le_bytes(buf); + + if auxpow_activated && (version & BLOCK_VERSION_AUXPOW_BIT) != 0 { + let (aux_power, size) = match AuxPoWHeader::deserialize_with_size(cur.split().1) { + Ok((aux_power, size)) => (aux_power, size), + Err(error) => { + return Err(error); + } + }; + cur.set_position(cur.position() + size); + } + + let count = VarInt::decode(cur.split().1)?; + let varint_size = VarInt::get_size(count)?; + cur.set_position(cur.position() + varint_size as u64); + + let mut transactions: Vec = vec![]; + for _ in 0..count { + let (tx, tx_size) = Tx::deserialize_with_size(cur.split().1)?; + cur.set_position(cur.position() + tx_size); + + transactions.push(tx); + } + + Ok(Self { + version, + auxpow_header: None, + previous_hash, + merkle_root, + timestamp, + bits, + nonce, + transactions, + }) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct AuxPoWHeader { + pub version: u32, +} + +impl AuxPoWHeader { + pub fn deserialize_with_size(bytes: &[u8]) -> Result<(Self, u64), DeserializeError> { + let mut cur = Cursor::new(bytes); + + let mut buf = [0u8; 4]; + cur.read_exact(&mut buf)?; + let version = u32::from_le_bytes(buf); + + let count = VarInt::decode(cur.split().1)?; + let varint_size = VarInt::get_size(count)? as u64; + cur.set_position(cur.position() + varint_size); + + let mut tx_ins: Vec = vec![]; + for _ in 0..count { + let (tx_in, size) = TxIn::deserialize_with_size(cur.split().1)?; + cur.set_position(cur.position() + size); + } + + let count = VarInt::decode(cur.split().1)?; + let varint_size = VarInt::get_size(count)? as u64; + cur.set_position(cur.position() + varint_size); + + let mut tx_outs: Vec = vec![]; + for _ in 0..count { + let (tx_out, size) = TxOut::deserialize_with_size(cur.split().1)?; + cur.set_position(cur.position() + size); + } + + let mut buf = [0u8; 4]; + cur.read_exact(&mut buf)?; + let lock_time = u32::from_le_bytes(buf); + + let mut buf = [0u8; 32]; + cur.read_exact(&mut buf)?; + let parent_hash = buf; + + let count = VarInt::decode(cur.split().1)?; + let varint_size = VarInt::get_size(count)? as u64; + cur.set_position(cur.position() + varint_size); + + for _ in 0..count { + let mut buf = [0u8; 32]; + cur.read_exact(&mut buf)?; + let merkle_hash = buf; + } + + let mut buf = [0u8; 4]; + cur.read_exact(&mut buf)?; + let bitmask = u32::from_le_bytes(buf); + + let count = VarInt::decode(cur.split().1)?; + let varint_size = VarInt::get_size(count)? as u64; + cur.set_position(cur.position() + varint_size); + + for _ in 0..count { + let mut buf = [0u8; 32]; + cur.read_exact(&mut buf)?; + let merkle_hash = buf; + } + + let mut buf = [0u8; 4]; + cur.read_exact(&mut buf)?; + let bitmask = u32::from_le_bytes(buf); + + let mut buf = [0u8; 80]; + cur.read_exact(&mut buf)?; + + Ok((Self { version }, cur.position())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn test_block_deserialize() { + let f = fs::read("./raw_50057.bin").unwrap(); + + let block = Block::deserialize(&f, false).expect("should deserialize raw block"); + } +} diff --git a/src/p2p/error.rs b/src/p2p/error.rs new file mode 100644 index 0000000..625185e --- /dev/null +++ b/src/p2p/error.rs @@ -0,0 +1,25 @@ +use std::error::Error; +use std::fmt::Display; + +#[derive(Debug)] +pub struct DeserializeError(pub String); + +impl Display for DeserializeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Error for DeserializeError {} + +impl From for DeserializeError { + fn from(_e: std::io::Error) -> Self { + DeserializeError("Failed to read varint".to_owned()) + } +} + +impl From for DeserializeError { + fn from(_e: std::string::FromUtf8Error) -> Self { + DeserializeError("Failed to convert from utf8".to_owned()) + } +} diff --git a/src/p2p/get_blocks.rs b/src/p2p/get_blocks.rs new file mode 100644 index 0000000..0be63f0 --- /dev/null +++ b/src/p2p/get_blocks.rs @@ -0,0 +1,63 @@ +use varint::VarInt; + +#[derive(Debug, Clone, PartialEq)] +pub struct GetBlocks { + pub version: u32, + pub hash_count: u64, + pub block_header_hashes: Vec<[u8; 32]>, + pub stop_hash: Option<[u8; 32]>, +} + +impl GetBlocks { + pub fn new( + version: u32, + block_header_hashes: Vec<[u8; 32]>, + stop_hash: Option<[u8; 32]>, + ) -> GetBlocks { + Self { + version, + hash_count: block_header_hashes.len() as u64, + block_header_hashes, + stop_hash, + } + } + + pub fn serialize(&self) -> Vec { + let mut result: Vec = Vec::new(); + result.extend(self.version.to_le_bytes()); + result.extend(VarInt::encode(self.hash_count).unwrap()); + for element in &self.block_header_hashes { + result.extend(element); + } + match self.stop_hash { + Some(x) => result.extend(x), + None => result.extend([0_u8; 32]), + } + result + } + + pub fn deserialize(bytes: &[u8]) -> GetBlocks { + let hash_count = VarInt::decode(&bytes[4..13]).unwrap(); + let varint_size = VarInt::get_size(hash_count).unwrap(); + let bytes_block_header_hashes = &bytes[(varint_size) as usize..]; + let mut block_header_hashes: Vec<[u8; 32]> = Vec::new(); + for i in 0..hash_count { + block_header_hashes.push( + bytes_block_header_hashes[(i * 32) as usize..((i + 1) * 32) as usize] + .try_into() + .unwrap(), + ); + } + let last_bytes: [u8; 32] = bytes[(bytes.len() - 32)..].try_into().unwrap(); + let stop_hash: Option<[u8; 32]> = match last_bytes { + x if x == [0_u8; 32] => None, + _ => Some(last_bytes), + }; + Self { + version: u32::from_le_bytes(bytes[0..4].try_into().unwrap()), + hash_count: hash_count, + block_header_hashes, + stop_hash, + } + } +} diff --git a/src/p2p/get_data.rs b/src/p2p/get_data.rs new file mode 100644 index 0000000..ac7175e --- /dev/null +++ b/src/p2p/get_data.rs @@ -0,0 +1,89 @@ +use super::error::DeserializeError; +use super::inventory::Inventory; +use std::io::{Cursor, Read}; +use varint::VarInt; + +#[derive(Debug, Clone, PartialEq)] +pub struct GetData { + pub count: u64, + pub inventory: Vec, +} + +impl GetData { + pub fn new(inventory: Vec) -> Self { + Self { + count: inventory.len() as u64, + inventory, + } + } + + pub fn serialize(&self) -> Vec { + let mut result: Vec = Vec::new(); + result.extend(VarInt::encode(self.count).unwrap()); + for element in &self.inventory { + result.extend(element.serialize()); + } + result + } + + pub fn deserialize(bytes: &[u8]) -> Result { + let mut cur = Cursor::new(bytes); + + let count = VarInt::decode(cur.split().1)?; + let varint_size = VarInt::get_size(count)? as u64; + cur.set_position(cur.position() + varint_size); + + let mut inventory: Vec = Vec::new(); + for _i in 0..count { + let mut buf = [0u8; 36]; + cur.read_exact(&mut buf)?; + let inv = Inventory::deserialize(&buf)?; + + inventory.push(inv); + } + Ok(Self::new(inventory)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_data_serialize() { + let mut hash = + hex::decode("5bf400bf44ac7a7cb0542ee7e3f9374f68be2dfdf0d64a654c2def6288b3936b") + .unwrap(); + hash.reverse(); + assert_eq!( + GetData::new(vec![Inventory { + identifier: 1, + hash: hash.try_into().unwrap(), + }]) + .serialize(), + [ + 1, 1, 0, 0, 0, 107, 147, 179, 136, 98, 239, 45, 76, 101, 74, 214, 240, 253, 45, + 190, 104, 79, 55, 249, 227, 231, 46, 84, 176, 124, 122, 172, 68, 191, 0, 244, 91, + ] + ) + } + + #[test] + fn test_get_data_deserialize() { + let mut hash = + hex::decode("5bf400bf44ac7a7cb0542ee7e3f9374f68be2dfdf0d64a654c2def6288b3936b") + .unwrap(); + hash.reverse(); + assert_eq!( + GetData::deserialize(&[ + 1, 1, 0, 0, 0, 107, 147, 179, 136, 98, 239, 45, 76, 101, 74, 214, 240, 253, 45, + 190, 104, 79, 55, 249, 227, 231, 46, 84, 176, 124, 122, 172, 68, 191, 0, 244, 91, + ]) + .unwrap(), + GetData::new(vec![Inventory { + identifier: 1, + hash: hash.try_into().unwrap(), + }]) + ) + } +} diff --git a/src/p2p/inventory.rs b/src/p2p/inventory.rs new file mode 100644 index 0000000..d9e1f91 --- /dev/null +++ b/src/p2p/inventory.rs @@ -0,0 +1,75 @@ +use super::error::DeserializeError; +use std::io::{Cursor, Read}; + +#[derive(Debug, Clone, PartialEq)] +pub struct Inventory { + pub identifier: u32, + pub hash: [u8; 32], +} +impl Inventory { + pub fn serialize(&self) -> Vec { + let mut result: Vec = vec![]; + result.extend_from_slice(&self.identifier.to_le_bytes()); + result.extend_from_slice(&self.hash); + result + } + + pub fn deserialize(bytes: &[u8]) -> Result { + let mut cur = Cursor::new(bytes); + + let mut buf = [0u8; 4]; + cur.read_exact(&mut buf)?; + let identifier = u32::from_le_bytes(buf); + + let mut buf = [0u8; 32]; + cur.read_exact(&mut buf)?; + let hash = buf; + + Ok(Self { identifier, hash }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_inventory_serialize() { + let mut hash = + hex::decode("5bf400bf44ac7a7cb0542ee7e3f9374f68be2dfdf0d64a654c2def6288b3936b") + .unwrap(); + hash.reverse(); + let bytes_reversed_hash = hash.try_into().unwrap(); + assert_eq!( + Inventory { + identifier: 1, + hash: bytes_reversed_hash, + } + .serialize(), + [ + 1, 0, 0, 0, 107, 147, 179, 136, 98, 239, 45, 76, 101, 74, 214, 240, 253, 45, 190, + 104, 79, 55, 249, 227, 231, 46, 84, 176, 124, 122, 172, 68, 191, 0, 244, 91, + ] + ); + } + + #[test] + fn test_inventory_deserialize() { + let mut hash = + hex::decode("5bf400bf44ac7a7cb0542ee7e3f9374f68be2dfdf0d64a654c2def6288b3936b") + .unwrap(); + hash.reverse(); + let bytes_reversed_hash = hash.try_into().unwrap(); + assert_eq!( + Inventory::deserialize(&[ + 1, 0, 0, 0, 107, 147, 179, 136, 98, 239, 45, 76, 101, 74, 214, 240, 253, 45, 190, + 104, 79, 55, 249, 227, 231, 46, 84, 176, 124, 122, 172, 68, 191, 0, 244, 91, + ]) + .unwrap(), + Inventory { + identifier: 1, + hash: bytes_reversed_hash, + } + ); + } +} diff --git a/src/p2p/message.rs b/src/p2p/message.rs new file mode 100644 index 0000000..e7ea9de --- /dev/null +++ b/src/p2p/message.rs @@ -0,0 +1,99 @@ +use super::error::DeserializeError; +use super::utils::checksum; +use std::io::{Cursor, Read}; + +#[derive(Debug, Clone, PartialEq)] +pub struct Message { + pub magic_bytes: [u8; 4], + pub command: String, + pub size: u32, + pub checksum: [u8; 4], + pub payload: Vec, +} + +impl Message { + pub fn new(magic_bytes: [u8; 4], command: String, payload: Vec) -> Self { + Self { + magic_bytes, + command, + size: payload.len() as u32, + checksum: checksum(&payload), + payload, + } + } + + pub fn serialize(&self) -> Vec { + let mut result: Vec = Vec::new(); + result.extend(self.magic_bytes); + let mut command = self.command.as_bytes().to_owned(); + command.resize(12, 0); + result.extend(command); + result.extend(self.size.to_le_bytes()); + result.extend(self.checksum); + result.extend(&self.payload); + result + } + + pub fn deserialize(bytes: &[u8]) -> Result { + let mut cur = Cursor::new(bytes); + + let mut buf = [0u8; 4]; + cur.read_exact(&mut buf)?; + let magic_bytes = buf; + + let mut buf = [0u8; 12]; + cur.read_exact(&mut buf)?; + let mut command = buf.to_vec(); + command.retain(|&x| x != 0); + let command = String::from_utf8(command)?; + + let mut buf = [0u8; 4]; + cur.read_exact(&mut buf)?; + let size = u32::from_le_bytes(buf); + + let mut buf = [0u8; 4]; + cur.read_exact(&mut buf)?; + let checksum = buf; + + let payload = cur.split().1.to_vec(); + + // TODO: verify if checksum equal checksum(payload) + + Ok(Self { + magic_bytes, + command, + size, + checksum, + payload, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_message_serialize() { + let verack = Message::new([0xFC, 0xC1, 0xB7, 0xDC], "verack".to_string(), vec![]); + assert_eq!( + verack.serialize(), + [ + 252, 193, 183, 220, 118, 101, 114, 97, 99, 107, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 93, + 246, 224, 226, + ] + ); + } + + #[test] + fn test_message_deserialize() { + let bytes: [u8; 24] = [ + 252, 193, 183, 220, 118, 101, 114, 97, 99, 107, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 93, 246, + 224, 226, + ]; + assert_eq!( + Message::deserialize(&bytes).unwrap(), + Message::new([0xFC, 0xC1, 0xB7, 0xDC], "verack".to_string(), vec![]) + ); + } +} diff --git a/src/p2p/mod.rs b/src/p2p/mod.rs new file mode 100644 index 0000000..06d374a --- /dev/null +++ b/src/p2p/mod.rs @@ -0,0 +1,11 @@ +pub mod address; +pub mod block; +pub mod error; +pub mod get_blocks; +pub mod get_data; +pub mod inventory; +pub mod message; +pub mod ping; +pub mod tx; +mod utils; +pub mod version; diff --git a/src/p2p/ping.rs b/src/p2p/ping.rs new file mode 100644 index 0000000..1f9df37 --- /dev/null +++ b/src/p2p/ping.rs @@ -0,0 +1,28 @@ +use super::error::DeserializeError; +use std::io::{Cursor, Read}; + +#[derive(Debug, Clone, PartialEq)] +pub struct Ping { + pub nonce: u64, +} + +impl Ping { + pub fn serialize(&self) -> Vec { + let mut result: Vec = vec![]; + result.extend_from_slice(&self.nonce.to_le_bytes()); + + result + } + + pub fn deserialize(bytes: &[u8]) -> Result { + let mut cur = Cursor::new(bytes); + + let mut buf = [0u8; 8]; + cur.read_exact(&mut buf)?; + let nonce = u64::from_le_bytes(buf); + + Ok(Self { nonce }) + } +} + +pub type Pong = Ping; diff --git a/src/p2p/tx.rs b/src/p2p/tx.rs new file mode 100644 index 0000000..a13117f --- /dev/null +++ b/src/p2p/tx.rs @@ -0,0 +1,232 @@ +use super::error::DeserializeError; +use super::utils; +use std::io::{Cursor, Read}; +use varint::VarInt; + +#[derive(Debug, Clone, PartialEq)] +pub struct Tx { + pub version: i32, + pub tx_ins: Vec, + pub tx_outs: Vec, + pub lock_time: u32, +} + +impl Tx { + pub fn hash(&self) -> [u8; 32] { + let tx = &self.serialize(); + utils::double_hash(tx) + } + + pub fn serialize(&self) -> Vec { + let mut result: Vec = vec![]; + + result.extend(self.version.to_le_bytes()); + result.extend(VarInt::encode(self.tx_ins.len() as u64).unwrap()); + self.tx_ins + .iter() + .for_each(|txin| result.extend(txin.serialize())); + result.extend(VarInt::encode(self.tx_outs.len() as u64).unwrap()); + self.tx_outs + .iter() + .for_each(|txout| result.extend(txout.serialize())); + result.extend(self.lock_time.to_le_bytes()); + + result + } + + // We only know the size of the tx after deserializing it. To know when the next tx start we have to return the value + pub fn deserialize_with_size(bytes: &[u8]) -> Result<(Tx, u64), DeserializeError> { + let mut cur = Cursor::new(bytes); + + let mut buf = [0u8; 4]; + cur.read_exact(&mut buf)?; + let version = i32::from_le_bytes(buf); + + // Deserialize tx inputs + let count = VarInt::decode(cur.split().1)?; + let varint_size = VarInt::get_size(count)? as u64; + cur.set_position(cur.position() + varint_size); + + let mut tx_ins: Vec = vec![]; + for _ in 0..count { + let (tx_in, size) = TxIn::deserialize_with_size(cur.split().1)?; + cur.set_position(cur.position() + size); + + tx_ins.push(tx_in); + } + + // Deserialize tx ouputs + let count = VarInt::decode(cur.split().1)?; + let varint_size = VarInt::get_size(count)? as u64; + cur.set_position(cur.position() + varint_size); + + let mut tx_outs: Vec = vec![]; + for _ in 0..count { + let (tx_out, size) = TxOut::deserialize_with_size(cur.split().1)?; + cur.set_position(cur.position() + size); + + tx_outs.push(tx_out); + } + + let mut buf = [0u8; 4]; + cur.read_exact(&mut buf)?; + let lock_time = u32::from_le_bytes(buf); + + Ok(( + Self { + version, + tx_ins, + tx_outs, + lock_time, + }, + cur.position(), + )) + } + + pub fn deserialize(bytes: &[u8]) -> Result { + Ok(Self::deserialize_with_size(bytes)?.0) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct TxIn { + pub previous_output: Outpoint, + pub signature_script: Vec, + pub sequence: u32, +} + +impl TxIn { + pub fn serialize(&self) -> Vec { + let mut result: Vec = vec![]; + + result.extend(self.previous_output.serialize()); + result.extend(VarInt::encode(self.signature_script.len() as u64).unwrap()); + result.extend(&self.signature_script); + result.extend(self.sequence.to_le_bytes()); + + result + } + + pub fn deserialize_with_size(bytes: &[u8]) -> Result<(TxIn, u64), DeserializeError> { + let mut cur = Cursor::new(bytes); + + let mut buf = [0u8; 36]; + cur.read_exact(&mut buf)?; + let previous_output = Outpoint::deserialize(&buf)?; + + let input_size = VarInt::decode(cur.split().1)?; + let varint_size = VarInt::get_size(input_size)? as u64; + cur.set_position(cur.position() + varint_size); + + let mut buf = vec![0; input_size as usize]; + cur.read_exact(&mut buf)?; + let signature_script = buf; + + let mut buf = [0u8; 4]; + cur.read_exact(&mut buf)?; + let sequence = u32::from_le_bytes(buf); + + Ok(( + Self { + previous_output, + signature_script, + sequence, + }, + cur.position(), + )) + } + + pub fn deserialize(bytes: &[u8]) -> Result { + Ok(Self::deserialize_with_size(bytes)?.0) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Outpoint { + pub previous_hash: [u8; 32], + pub index: u32, +} + +impl Outpoint { + pub fn serialize(&self) -> Vec { + let mut result: Vec = vec![]; + + result.extend(self.previous_hash.to_vec()); + result.extend(self.index.to_le_bytes()); + + result + } + + pub fn deserialize(bytes: &[u8]) -> Result { + let mut cur = Cursor::new(bytes); + + let mut buf = [0u8; 32]; + cur.read_exact(&mut buf)?; + let previous_hash = buf; + + let mut buf = [0u8; 4]; + cur.read_exact(&mut buf)?; + let index = u32::from_le_bytes(buf); + + Ok(Self { + previous_hash, + index, + }) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct TxOut { + pub value: i64, + pub pk_script: Vec, +} + +impl TxOut { + pub fn serialize(&self) -> Vec { + let mut result: Vec = vec![]; + + result.extend(self.value.to_le_bytes()); + result.extend(VarInt::encode(self.pk_script.len() as u64).unwrap()); + result.extend(&self.pk_script); + + result + } + + pub fn deserialize_with_size(bytes: &[u8]) -> Result<(TxOut, u64), DeserializeError> { + let mut cur = Cursor::new(bytes); + + let mut buf = [0u8; 8]; + cur.read_exact(&mut buf)?; + let value = i64::from_le_bytes(buf); + + let script_size = VarInt::decode(cur.split().1)?; + let varint_size = VarInt::get_size(script_size)? as u64; + cur.set_position(cur.position() + varint_size); + + let mut buf = vec![0; script_size as usize]; + cur.read_exact(&mut buf)?; + let pk_script = buf; + + Ok((Self { value, pk_script }, cur.position())) + } + + pub fn deserialize(bytes: &[u8]) -> Result { + Ok(Self::deserialize_with_size(bytes)?.0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_deserialize_tx() { + let raw_tx = hex::decode("01000000016277237f8fc506329d1f41c2e9a2bb23647f44460bec2a58a5e3f6f428bb15c2010000006b483045022100d7590246176a68adabb3de7c1a74058db0e39aba905bf7feaa4e8b6a2d5fe2bd0220082385abcfa0e94110445b4578f606eedd7daffd27f387bd98833ed867355d3601210245d41687cf6d72ac6c7e0e4e38043429724aed2fd3bb5a6c6b63f1dcab75f23d0000000002005a6202000000001976a914c664d0aa46ba90d12e79729a2da7e7adfbb6a87588acb81e490c000000001976a914bf2d46e52a44c123cff6ea866eb448249cad17c388ac00000000").unwrap(); + + let tx = Tx::deserialize(&raw_tx).unwrap(); + + let raw_tx_bis = tx.serialize(); + + assert_eq!(raw_tx_bis, raw_tx); + } +} diff --git a/src/p2p/utils.rs b/src/p2p/utils.rs new file mode 100644 index 0000000..7bb686a --- /dev/null +++ b/src/p2p/utils.rs @@ -0,0 +1,14 @@ +use sha2::{Digest, Sha256}; + +pub fn double_hash(message: &Vec) -> [u8; 32] { + let mut digest = Sha256::digest(message); + digest = Sha256::digest(digest); + + digest.try_into().unwrap() +} + +pub fn checksum(message: &Vec) -> [u8; 4] { + let hash = double_hash(message); + + hash[0..4].try_into().unwrap() +} diff --git a/src/p2p/version.rs b/src/p2p/version.rs new file mode 100644 index 0000000..d21cf2b --- /dev/null +++ b/src/p2p/version.rs @@ -0,0 +1,166 @@ +use super::address::Address; +use super::error::DeserializeError; +use std::io::{Cursor, Read}; +use varint::VarInt; + +#[derive(Debug, Clone, PartialEq)] +pub struct Version { + pub version: u32, + pub services: u64, + pub timestamp: u64, + pub addr_recv: Address, + pub addr_trans: Address, + pub nonce: u64, + pub user_agent: String, + pub start_height: u32, + pub relay: bool, +} + +impl Version { + pub fn serialize(&self) -> Vec { + let mut result: Vec = Vec::new(); + result.extend(self.version.to_le_bytes()); + result.extend(self.services.to_le_bytes()); + result.extend(self.timestamp.to_le_bytes()); + result.extend(self.addr_recv.serialize()); + result.extend(self.addr_trans.serialize()); + result.extend(self.nonce.to_le_bytes()); + result.extend(VarInt::encode(self.user_agent.len() as u64).unwrap()); + result.extend(self.user_agent.as_bytes()); + result.extend(self.start_height.to_le_bytes()); + result.push(self.relay as u8); + result + } + + pub fn deserialize(bytes: &[u8]) -> Result { + let mut cur = Cursor::new(bytes); + + let mut buf = [0u8; 4]; + cur.read_exact(&mut buf)?; + let version = u32::from_le_bytes(buf); + + let mut buf = [0u8; 8]; + cur.read_exact(&mut buf)?; + let services = u64::from_le_bytes(buf); + + let mut buf = [0u8; 8]; + cur.read_exact(&mut buf)?; + let timestamp = u64::from_le_bytes(buf); + + let mut buf = [0u8; 26]; + cur.read_exact(&mut buf)?; + let addr_recv = Address::deserialize(&buf)?; + + let mut buf = [0u8; 26]; + cur.read_exact(&mut buf)?; + let addr_trans = Address::deserialize(&buf)?; + + let mut buf = [0u8; 8]; + cur.read_exact(&mut buf)?; + let nonce = u64::from_le_bytes(buf); + + let varint = VarInt::decode(cur.split().1)?; + let varint_size = VarInt::get_size(varint)? as u64; + cur.set_position(cur.position() + varint_size); + + let mut buf = vec![0; varint as usize]; + cur.read_exact(&mut buf)?; + let user_agent = String::from_utf8(buf)?; + + let mut buf = [0u8; 4]; + cur.read_exact(&mut buf)?; + let start_height = u32::from_le_bytes(buf); + + // FIXME: Verify there is a a last byte + let relay = match cur.split().1[0] { + 0 => false, + 1 => true, + _ => { + return Err(DeserializeError( + "Failed to deserialize relay value".to_owned(), + )) + } + }; + + Ok(Self { + version, + services, + timestamp, + addr_recv, + addr_trans, + nonce, + user_agent, + start_height, + relay, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_version_serialize() { + let version = Version { + version: 70004, + services: 4, + timestamp: 1667596120, + addr_recv: Address { + services: 1, + ip: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 127, 0, 0, 1], + port: 0, + }, + addr_trans: Address { + services: 1, + ip: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 127, 0, 0, 1], + port: 0, + }, + nonce: 1, + user_agent: "ethicnology".to_owned(), + start_height: 0, + relay: false, + }; + assert_eq!( + version.serialize(), + [ + 116, 17, 1, 0, 4, 0, 0, 0, 0, 0, 0, 0, 88, 127, 101, 99, 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 127, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 127, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 11, 101, 116, 104, 105, 99, 110, 111, 108, 111, 103, 121, 0, 0, 0, 0, 0, + ] + ); + } + + #[test] + fn test_version_deserialize() { + assert_eq!( + Version::deserialize(&[ + 116, 17, 1, 0, 4, 0, 0, 0, 0, 0, 0, 0, 88, 127, 101, 99, 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 127, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 127, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 11, 101, 116, 104, 105, 99, 110, 111, 108, 111, 103, 121, 0, 0, 0, 0, 0, + ]) + .unwrap(), + Version { + version: 70004, + services: 4, + timestamp: 1667596120, + addr_recv: Address { + services: 1, + ip: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 127, 0, 0, 1], + port: 0, + }, + addr_trans: Address { + services: 1, + ip: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 127, 0, 0, 1], + port: 0, + }, + nonce: 1, + user_agent: "ethicnology".to_owned(), + start_height: 0, + relay: false, + } + ); + } +} diff --git a/src/peer.rs b/src/peer.rs index 21d2553..0e0e0b3 100644 --- a/src/peer.rs +++ b/src/peer.rs @@ -1,5 +1,5 @@ -use bitcoin_network::message::Message; -use bitcoin_network::version::Version; +use crate::p2p::message::Message; +use crate::p2p::version::Version; use std::error::Error; use std::io::prelude::*; use std::net::TcpStream; diff --git a/src/utils.rs b/src/utils.rs index d44429a..4ef53f6 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,5 @@ -use bitcoin_network::{address::Address, version::Version}; -use bitcoin_network::{block::Block, inventory::Inventory}; +use crate::p2p::{address::Address, version::Version}; +use crate::p2p::{block::Block, inventory::Inventory}; use std::error::Error; use std::time::{SystemTime, UNIX_EPOCH};