From 4fae5a26a74494adeb87d09ca071f40a0874dc0a Mon Sep 17 00:00:00 2001 From: Luc Date: Fri, 8 Sep 2023 19:54:29 +0000 Subject: [PATCH] Partial bitcoin parsing (#19) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Antonio F. Š --- .gitignore | 1 + Cargo.lock | 58 ++++++++++++----------- Cargo.toml | 2 + src/main.rs | 1 + src/models/lookup/multicoin.rs | 46 +++++++----------- src/models/multicoin/decoding/bitcoin.rs | 60 ++++++++++++++++++++++++ src/models/multicoin/decoding/mod.rs | 3 ++ src/models/multicoin/decoding/p2pkh.rs | 47 +++++++++++++++++++ src/models/multicoin/decoding/p2sh.rs | 47 +++++++++++++++++++ src/models/multicoin/mod.rs | 1 + src/models/profile/mod.rs | 3 +- src/utils/mod.rs | 1 + src/utils/sha256.rs | 8 ++++ 13 files changed, 219 insertions(+), 59 deletions(-) create mode 100644 src/models/multicoin/decoding/bitcoin.rs create mode 100644 src/models/multicoin/decoding/mod.rs create mode 100644 src/models/multicoin/decoding/p2pkh.rs create mode 100644 src/models/multicoin/decoding/p2sh.rs create mode 100644 src/utils/mod.rs create mode 100644 src/utils/sha256.rs diff --git a/.gitignore b/.gitignore index fedaa2b..5c6f94c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target .env +.idea/ diff --git a/Cargo.lock b/Cargo.lock index df26e6e..fb26d7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -472,13 +472,13 @@ dependencies = [ "bincode", "bs58 0.4.0", "coins-core", - "digest 0.10.6", + "digest 0.10.7", "getrandom", "hmac", "k256", "lazy_static", "serde", - "sha2 0.10.6", + "sha2 0.10.7", "thiserror", ] @@ -495,7 +495,7 @@ dependencies = [ "once_cell", "pbkdf2 0.12.1", "rand", - "sha2 0.10.6", + "sha2 0.10.7", "thiserror", ] @@ -508,13 +508,13 @@ dependencies = [ "base64 0.21.0", "bech32", "bs58 0.4.0", - "digest 0.10.6", + "digest 0.10.7", "generic-array", "hex", "ripemd", "serde", "serde_derive", - "sha2 0.10.6", + "sha2 0.10.7", "sha3", "thiserror", ] @@ -715,9 +715,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "const-oid", @@ -806,7 +806,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0997c976637b606099b9985693efa3581e84e41f5c11ba5255f88711058ad428" dependencies = [ "der", - "digest 0.10.6", + "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", @@ -827,7 +827,7 @@ checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b" dependencies = [ "base16ct", "crypto-bigint", - "digest 0.10.6", + "digest 0.10.7", "ff", "generic-array", "group", @@ -884,6 +884,7 @@ dependencies = [ "axum-macros", "bs58 0.5.0", "chrono", + "digest 0.10.7", "dotenvy", "ethers", "ethers-ccip-read", @@ -898,6 +899,7 @@ dependencies = [ "rustls", "serde", "serde_json", + "sha2 0.10.7", "thiserror", "tokio", "tower-http", @@ -936,7 +938,7 @@ checksum = "1fda3bf123be441da5260717e0661c25a2fd9cb2b2c1d20bf2e05580047158ab" dependencies = [ "aes", "ctr", - "digest 0.10.6", + "digest 0.10.7", "hex", "hmac", "pbkdf2 0.11.0", @@ -944,7 +946,7 @@ dependencies = [ "scrypt", "serde", "serde_json", - "sha2 0.10.6", + "sha2 0.10.7", "sha3", "thiserror", "uuid", @@ -1225,7 +1227,7 @@ dependencies = [ "ethers-core", "hex", "rand", - "sha2 0.10.6", + "sha2 0.10.7", "thiserror", "tracing", ] @@ -1603,7 +1605,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -1897,7 +1899,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "once_cell", - "sha2 0.10.6", + "sha2 0.10.7", "signature", ] @@ -1999,7 +2001,7 @@ version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -2329,10 +2331,10 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "hmac", "password-hash", - "sha2 0.10.6", + "sha2 0.10.7", ] [[package]] @@ -2341,7 +2343,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0ca0b5a68607598bf3bad68f32227a8164f6254833f84eafaac409cd6746c31" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "hmac", ] @@ -2784,7 +2786,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -2840,7 +2842,7 @@ version = "7.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512b0ab6853f7e14e3c8754acb43d6f748bb9ced66aa5915a6553ac8213f7731" dependencies = [ - "sha2 0.10.6", + "sha2 0.10.7", "walkdir", ] @@ -3011,7 +3013,7 @@ dependencies = [ "hmac", "pbkdf2 0.11.0", "salsa20", - "sha2 0.10.6", + "sha2 0.10.7", ] [[package]] @@ -3162,7 +3164,7 @@ checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -3186,13 +3188,13 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -3201,7 +3203,7 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "keccak", ] @@ -3238,7 +3240,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "rand_core", ] @@ -3395,7 +3397,7 @@ dependencies = [ "semver", "serde", "serde_json", - "sha2 0.10.6", + "sha2 0.10.7", "thiserror", "url", "zip", diff --git a/Cargo.toml b/Cargo.toml index fa2adb7..c11eb61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,8 @@ thiserror = "1.0.48" regex = "1.9.5" rustls = "0.21.7" bs58 = "0.5.0" +sha2 = "0.10.7" +digest = "0.10.7" hex-literal = "0.4.1" axum-macros = "0.3.8" lazy_static = "1.4.0" diff --git a/src/main.rs b/src/main.rs index e5b9a8d..a2bfa48 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ mod models; mod routes; mod state; mod provider; +mod utils; use dotenvy::dotenv; use state::AppState; diff --git a/src/models/lookup/multicoin.rs b/src/models/lookup/multicoin.rs index 5db7197..f1dafee 100644 --- a/src/models/lookup/multicoin.rs +++ b/src/models/lookup/multicoin.rs @@ -7,7 +7,10 @@ use ethers_core::{ use hex_literal::hex; use tracing::info; -use crate::models::multicoin::cointype::{coins::CoinType, evm::ChainId, slip44::SLIP44}; +use crate::models::multicoin::{ + cointype::{coins::CoinType, evm::ChainId, slip44::SLIP44}, + decoding::bitcoin::decode_btc, +}; use super::{ENSLookup, ENSLookupError}; @@ -28,8 +31,6 @@ impl ENSLookup for Multicoin { } fn decode(&self, data: &[u8]) -> Result { - info!("Decoding: {:?}", data); - let decoded_abi = ethers_core::abi::decode(&[ParamType::Bytes], data) .map_err(|_| ENSLookupError::AbiError)?; let value = decoded_abi @@ -46,13 +47,12 @@ impl ENSLookup for Multicoin { // SLIP-044 Chain Address Decoding (see ensip-9) CoinType::Slip44(slip44) => match slip44 { // Bitcoin Decoding - SLIP44::Bitcoin => Ok(format!("btc:{}", bs58::encode(value).into_string())), + SLIP44::Bitcoin => Ok(decode_btc(value.as_slice()).unwrap()), // Lightcoin Decoding SLIP44::Litecoin => { Err(ENSLookupError::Unknown(anyhow!( "Litecoin Decoding Not Implemented" ))) - // Ok(format!("ltc:{}", bs58::encode(value).into_string())) } // Unsupported SLIP44 Chain @@ -61,36 +61,22 @@ impl ENSLookup for Multicoin { // Ok(format!("SLIP-{:?}", value)) // Unsupported - Err(ENSLookupError::Unsupported("Chain Not Supported".to_string())) + Err(ENSLookupError::Unsupported( + "Chain Not Supported".to_string(), + )) } }, // Implement EVM Chain Address Decoding (mostly ChecksummedHex, sometimes ChecksummedHex(chainId)) (see ensip-11) - CoinType::Evm(evm) => match evm { - // TODO: EVM Exceptions go here - // ChainId::Ethereum => { - // // Verify length is 20 bytes - // if value.len() != 20 { - // // TODO: throw invalid length - // return Ok("Invalid Length".to_string()); - // } - - // let address = hex::encode(value); - - // Ok(format!("0x{address}")) - // }, - - // Every EVM Chain - _ => { - // Verify length is 20 bytes - if value.len() != 20 { - // TODO: throw invalid length - return Ok("Invalid Length".to_string()); - } + CoinType::Evm(_evm) => { + // Verify length is 20 bytes + if value.len() != 20 { + // TODO: throw invalid length + return Ok("Invalid Length".to_string()); + } - let address = hex::encode(value); + let address = hex::encode(value); - Ok(format!("0x{address}")) - } + Ok(format!("0x{address}")) }, } } diff --git a/src/models/multicoin/decoding/bitcoin.rs b/src/models/multicoin/decoding/bitcoin.rs new file mode 100644 index 0000000..4d4cfa0 --- /dev/null +++ b/src/models/multicoin/decoding/bitcoin.rs @@ -0,0 +1,60 @@ +use thiserror::Error; + +use super::{p2pkh, p2sh}; + +#[derive(Error, Debug)] +pub enum BtcDecodeError { + #[error("Invalid address")] + InvalidAddress, + + #[error("Address type not supported")] + NotSupported +} + +pub fn decode_btc(bytes: &[u8]) -> Result { + if bytes.len() == 25 { + return p2pkh::decode(bytes, 0x00).map_err(|_| BtcDecodeError::InvalidAddress) + } + + if bytes.len() == 23 { + return p2sh::decode(bytes, 0x05).map_err(|_| BtcDecodeError::InvalidAddress) + } + + if bytes.starts_with(&[0x98, 0x99]) { + return Err(BtcDecodeError::NotSupported) + } + + Err(BtcDecodeError::NotSupported) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_btc_p2pkh() { + let decoded = decode_btc( + &hex::decode("76a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac").unwrap() + ).unwrap(); + + assert_eq!(decoded, "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa".to_string()); + } + + #[tokio::test] + async fn test_btc_p2sh() { + let decoded = decode_btc( + &hex::decode("a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1887").unwrap() + ).unwrap(); + + assert_eq!(decoded, "3Ai1JZ8pdJb2ksieUV8FsxSNVJCpoPi8W6".to_string()); + } + + #[tokio::test] + async fn test_btc_segwit() { + let decoded = decode_btc( + &hex::decode("0014751e76e8199196d454941c45d1b3a323f1433bd6").unwrap() + ).unwrap(); + + assert_eq!(decoded, "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4".to_string()); + } +} diff --git a/src/models/multicoin/decoding/mod.rs b/src/models/multicoin/decoding/mod.rs new file mode 100644 index 0000000..e462406 --- /dev/null +++ b/src/models/multicoin/decoding/mod.rs @@ -0,0 +1,3 @@ +pub mod p2pkh; +pub mod p2sh; +pub mod bitcoin; diff --git a/src/models/multicoin/decoding/p2pkh.rs b/src/models/multicoin/decoding/p2pkh.rs new file mode 100644 index 0000000..1cdde6a --- /dev/null +++ b/src/models/multicoin/decoding/p2pkh.rs @@ -0,0 +1,47 @@ +use bs58::Alphabet; +use thiserror::Error; +use crate::utils; + + +#[derive(Error, Debug)] +pub enum P2PKHError { + #[error("Invalid P2PKH structure")] + InvalidStructure(String) +} + +pub fn decode(bytes: &[u8], version: u8) -> Result { + let bytes_len = bytes.len(); + if bytes_len < 3 { + return Err(P2PKHError::InvalidStructure("len < 3".to_string())); + } + + if bytes[..2] != [0x76, 0xa9] { + return Err(P2PKHError::InvalidStructure("invalid header".to_string())); + } + + let len = bytes[2] as usize; + let expected_len = 3 + len + 2; + + if bytes_len != expected_len { + return Err(P2PKHError::InvalidStructure(format!("invalid length ({bytes_len:?} != {expected_len:?})"))); + } + + if bytes[bytes_len - 2..bytes_len] != [0x88, 0xac] { + return Err(P2PKHError::InvalidStructure("invalid end".to_string())); + } + + let pub_key_hash = &bytes[3..3 + len]; + + let mut full = pub_key_hash.to_vec(); + full.insert(0, version); + + let full_checksum = utils::sha256::hash(utils::sha256::hash(full.clone())); + + full.extend_from_slice(&full_checksum[..4]); + + let value = bs58::encode(full) + .with_alphabet(Alphabet::BITCOIN) + .into_string(); + + Ok(value) +} diff --git a/src/models/multicoin/decoding/p2sh.rs b/src/models/multicoin/decoding/p2sh.rs new file mode 100644 index 0000000..28f2b58 --- /dev/null +++ b/src/models/multicoin/decoding/p2sh.rs @@ -0,0 +1,47 @@ +use bs58::Alphabet; +use thiserror::Error; +use crate::utils; + + +#[derive(Error, Debug)] +pub enum P2SHError { + #[error("Invalid P2SH structure")] + InvalidStructure(String) +} + +pub fn decode(bytes: &[u8], version: u8) -> Result { + let bytes_len = bytes.len(); + if bytes_len < 2 { + return Err(P2SHError::InvalidStructure("len < 2".to_string())); + } + + if bytes[0] != 0xa9 { + return Err(P2SHError::InvalidStructure("invalid header".to_string())); + } + + let len = bytes[1] as usize; + let expected_len = 2 + len + 1; + + if bytes_len != expected_len { + return Err(P2SHError::InvalidStructure(format!("invalid length ({bytes_len:?} != {expected_len:?})"))); + } + + if bytes[bytes_len - 1] != 0x87 { + return Err(P2SHError::InvalidStructure("invalid end".to_string())); + } + + let script_hash = &bytes[2..2 + len]; + + let mut full = script_hash.to_vec(); + full.insert(0, version); + + let full_checksum = utils::sha256::hash(utils::sha256::hash(full.clone())); + + full.extend_from_slice(&full_checksum[..4]); + + let value = bs58::encode(full) + .with_alphabet(Alphabet::BITCOIN) + .into_string(); + + Ok(value) +} diff --git a/src/models/multicoin/mod.rs b/src/models/multicoin/mod.rs index 8e8238a..250b4f6 100644 --- a/src/models/multicoin/mod.rs +++ b/src/models/multicoin/mod.rs @@ -1 +1,2 @@ pub mod cointype; +pub mod decoding; diff --git a/src/models/profile/mod.rs b/src/models/profile/mod.rs index e2b72ef..b8498d5 100644 --- a/src/models/profile/mod.rs +++ b/src/models/profile/mod.rs @@ -1,5 +1,6 @@ -use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; use utoipa::ToSchema; pub mod error; diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..d2c2c65 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod sha256; \ No newline at end of file diff --git a/src/utils/sha256.rs b/src/utils/sha256.rs new file mode 100644 index 0000000..58b5228 --- /dev/null +++ b/src/utils/sha256.rs @@ -0,0 +1,8 @@ +use sha2::{Digest, Sha256}; + +pub fn hash>(data: T) -> Vec { + let mut hasher = Sha256::new(); + hasher.update(data); + + hasher.finalize().as_slice().into() +} \ No newline at end of file