From 2d4b775b6e89674ca5ce59789dfc96d6ae2ba625 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Tue, 10 Sep 2024 06:25:21 -0400 Subject: [PATCH] Add bitcoin Block trait impl --- processor/bitcoin/src/block.rs | 70 ++++++++++++++++ processor/bitcoin/src/lib.rs | 64 +++------------ processor/bitcoin/src/output.rs | 21 ++++- processor/bitcoin/src/scanner.rs | 131 ++++++++++++++++++++++++++++++ processor/primitives/src/block.rs | 22 ++--- 5 files changed, 239 insertions(+), 69 deletions(-) create mode 100644 processor/bitcoin/src/scanner.rs diff --git a/processor/bitcoin/src/block.rs b/processor/bitcoin/src/block.rs index e69de29b..304f19e3 100644 --- a/processor/bitcoin/src/block.rs +++ b/processor/bitcoin/src/block.rs @@ -0,0 +1,70 @@ +use std::collections::HashMap; + +use ciphersuite::{Ciphersuite, Secp256k1}; + +use bitcoin_serai::bitcoin::block::{Header, Block as BBlock}; + +use serai_client::networks::bitcoin::Address; + +use primitives::{ReceivedOutput, EventualityTracker}; + +use crate::{hash_bytes, scanner::scanner, output::Output, transaction::Eventuality}; + +#[derive(Clone, Debug)] +pub(crate) struct BlockHeader(Header); +impl primitives::BlockHeader for BlockHeader { + fn id(&self) -> [u8; 32] { + hash_bytes(self.0.block_hash().to_raw_hash()) + } + fn parent(&self) -> [u8; 32] { + hash_bytes(self.0.prev_blockhash.to_raw_hash()) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct Block(BBlock); + +#[async_trait::async_trait] +impl primitives::Block for Block { + type Header = BlockHeader; + + type Key = ::G; + type Address = Address; + type Output = Output; + type Eventuality = Eventuality; + + fn id(&self) -> [u8; 32] { + primitives::BlockHeader::id(&BlockHeader(self.0.header)) + } + + fn scan_for_outputs_unordered(&self, key: Self::Key) -> Vec { + let scanner = scanner(key); + + let mut res = vec![]; + // We skip the coinbase transaction as its burdened by maturity + for tx in &self.0.txdata[1 ..] { + for output in scanner.scan_transaction(tx) { + res.push(Output::new(key, tx, output)); + } + } + res + } + + #[allow(clippy::type_complexity)] + fn check_for_eventuality_resolutions( + &self, + eventualities: &mut EventualityTracker, + ) -> HashMap< + >::TransactionId, + Self::Eventuality, + > { + let mut res = HashMap::new(); + for tx in &self.0.txdata[1 ..] { + let id = hash_bytes(tx.compute_txid().to_raw_hash()); + if let Some(eventuality) = eventualities.active_eventualities.remove(id.as_slice()) { + res.insert(id, eventuality); + } + } + res + } +} diff --git a/processor/bitcoin/src/lib.rs b/processor/bitcoin/src/lib.rs index 112d8fd3..03c9e903 100644 --- a/processor/bitcoin/src/lib.rs +++ b/processor/bitcoin/src/lib.rs @@ -6,8 +6,19 @@ static ALLOCATOR: zalloc::ZeroizingAlloc = zalloc::ZeroizingAlloc(std::alloc::System); +mod scanner; + mod output; mod transaction; +mod block; + +pub(crate) fn hash_bytes(hash: bitcoin_serai::bitcoin::hashes::sha256d::Hash) -> [u8; 32] { + use bitcoin_serai::bitcoin::hashes::Hash; + + let mut res = hash.to_byte_array(); + res.reverse(); + res +} /* use std::{sync::LazyLock, time::Duration, io, collections::HashMap}; @@ -299,59 +310,6 @@ impl Bitcoin { } } - // Expected script has to start with SHA256 PUSH MSG_HASH OP_EQUALVERIFY .. - fn segwit_data_pattern(script: &ScriptBuf) -> Option { - let mut ins = script.instructions(); - - // first item should be SHA256 code - if ins.next()?.ok()?.opcode()? != OP_SHA256 { - return Some(false); - } - - // next should be a data push - ins.next()?.ok()?.push_bytes()?; - - // next should be a equality check - if ins.next()?.ok()?.opcode()? != OP_EQUALVERIFY { - return Some(false); - } - - Some(true) - } - - fn extract_serai_data(tx: &Transaction) -> Vec { - // check outputs - let mut data = (|| { - for output in &tx.output { - if output.script_pubkey.is_op_return() { - match output.script_pubkey.instructions_minimal().last() { - Some(Ok(Instruction::PushBytes(data))) => return data.as_bytes().to_vec(), - _ => continue, - } - } - } - vec![] - })(); - - // check inputs - if data.is_empty() { - for input in &tx.input { - let witness = input.witness.to_vec(); - // expected witness at least has to have 2 items, msg and the redeem script. - if witness.len() >= 2 { - let redeem_script = ScriptBuf::from_bytes(witness.last().unwrap().clone()); - if Self::segwit_data_pattern(&redeem_script) == Some(true) { - data.clone_from(&witness[witness.len() - 2]); // len() - 1 is the redeem_script - break; - } - } - } - } - - data.truncate(MAX_DATA_LEN.try_into().unwrap()); - data - } - #[cfg(test)] pub fn sign_btc_input_for_p2pkh( tx: &Transaction, diff --git a/processor/bitcoin/src/output.rs b/processor/bitcoin/src/output.rs index cc624319..c7ed060f 100644 --- a/processor/bitcoin/src/output.rs +++ b/processor/bitcoin/src/output.rs @@ -8,6 +8,7 @@ use bitcoin_serai::{ key::{Parity, XOnlyPublicKey}, consensus::Encodable, script::Instruction, + transaction::Transaction, }, wallet::ReceivedOutput as WalletOutput, }; @@ -22,6 +23,8 @@ use serai_client::{ use primitives::{OutputType, ReceivedOutput}; +use crate::scanner::{offsets_for_key, presumed_origin, extract_serai_data}; + #[derive(Clone, PartialEq, Eq, Hash, Debug, Encode, Decode, BorshSerialize, BorshDeserialize)] pub(crate) struct OutputId([u8; 36]); impl Default for OutputId { @@ -48,6 +51,20 @@ pub(crate) struct Output { data: Vec, } +impl Output { + pub fn new(key: ::G, tx: &Transaction, output: WalletOutput) -> Self { + Self { + kind: offsets_for_key(key) + .into_iter() + .find_map(|(kind, offset)| (offset == output.offset()).then_some(kind)) + .expect("scanned output for unknown offset"), + presumed_origin: presumed_origin(tx), + output, + data: extract_serai_data(tx), + } + } +} + impl ReceivedOutput<::G, Address> for Output { type Id = OutputId; type TransactionId = [u8; 32]; @@ -63,7 +80,9 @@ impl ReceivedOutput<::G, Address> for Output { } fn transaction_id(&self) -> Self::TransactionId { - self.output.outpoint().txid.to_raw_hash().to_byte_array() + let mut res = self.output.outpoint().txid.to_raw_hash().to_byte_array(); + res.reverse(); + res } fn key(&self) -> ::G { diff --git a/processor/bitcoin/src/scanner.rs b/processor/bitcoin/src/scanner.rs new file mode 100644 index 00000000..43518b57 --- /dev/null +++ b/processor/bitcoin/src/scanner.rs @@ -0,0 +1,131 @@ +use std::{sync::LazyLock, collections::HashMap}; + +use ciphersuite::{Ciphersuite, Secp256k1}; + +use bitcoin_serai::{ + bitcoin::{ + blockdata::opcodes, + script::{Instruction, ScriptBuf}, + Transaction, + }, + wallet::Scanner, +}; + +use serai_client::networks::bitcoin::Address; + +use primitives::OutputType; + +const KEY_DST: &[u8] = b"Serai Bitcoin Processor Key Offset"; +static BRANCH_BASE_OFFSET: LazyLock<::F> = + LazyLock::new(|| Secp256k1::hash_to_F(KEY_DST, b"branch")); +static CHANGE_BASE_OFFSET: LazyLock<::F> = + LazyLock::new(|| Secp256k1::hash_to_F(KEY_DST, b"change")); +static FORWARD_BASE_OFFSET: LazyLock<::F> = + LazyLock::new(|| Secp256k1::hash_to_F(KEY_DST, b"forward")); + +// Unfortunately, we have per-key offsets as it's the root key plus the base offset may not be +// even. While we could tweak the key until all derivations are even, that'd require significantly +// more tweaking. This algorithmic complexity is preferred. +pub(crate) fn offsets_for_key( + key: ::G, +) -> HashMap::F> { + let mut offsets = HashMap::from([(OutputType::External, ::F::ZERO)]); + + // We create an actual Bitcoin scanner as upon adding an offset, it yields the tweaked offset + // actually used + let mut scanner = Scanner::new(key).unwrap(); + let mut register = |kind, offset| { + let tweaked_offset = scanner.register_offset(offset).expect("offset collision"); + offsets.insert(kind, tweaked_offset); + }; + + register(OutputType::Branch, *BRANCH_BASE_OFFSET); + register(OutputType::Change, *CHANGE_BASE_OFFSET); + register(OutputType::Forwarded, *FORWARD_BASE_OFFSET); + + offsets +} + +pub(crate) fn scanner(key: ::G) -> Scanner { + let mut scanner = Scanner::new(key).unwrap(); + for (_, offset) in offsets_for_key(key) { + let tweaked_offset = scanner.register_offset(offset).unwrap(); + assert_eq!(tweaked_offset, offset); + } + scanner +} + +pub(crate) fn presumed_origin(tx: &Transaction) -> Option
{ + todo!("TODO") + + /* + let spent_output = { + let input = &tx.input[0]; + let mut spent_tx = input.previous_output.txid.as_raw_hash().to_byte_array(); + spent_tx.reverse(); + let mut tx; + while { + tx = self.rpc.get_transaction(&spent_tx).await; + tx.is_err() + } { + log::error!("couldn't get transaction from bitcoin node: {tx:?}"); + sleep(Duration::from_secs(5)).await; + } + tx.unwrap().output.swap_remove(usize::try_from(input.previous_output.vout).unwrap()) + }; + Address::new(spent_output.script_pubkey) + */ +} + +// Checks if this script matches SHA256 PUSH MSG_HASH OP_EQUALVERIFY .. +fn matches_segwit_data(script: &ScriptBuf) -> Option { + let mut ins = script.instructions(); + + // first item should be SHA256 code + if ins.next()?.ok()?.opcode()? != opcodes::all::OP_SHA256 { + return Some(false); + } + + // next should be a data push + ins.next()?.ok()?.push_bytes()?; + + // next should be a equality check + if ins.next()?.ok()?.opcode()? != opcodes::all::OP_EQUALVERIFY { + return Some(false); + } + + Some(true) +} + +// Extract the data for Serai from a transaction +pub(crate) fn extract_serai_data(tx: &Transaction) -> Vec { + // Check for an OP_RETURN output + let mut data = (|| { + for output in &tx.output { + if output.script_pubkey.is_op_return() { + match output.script_pubkey.instructions_minimal().last() { + Some(Ok(Instruction::PushBytes(data))) => return Some(data.as_bytes().to_vec()), + _ => continue, + } + } + } + None + })(); + + // Check the inputs + if data.is_none() { + for input in &tx.input { + let witness = input.witness.to_vec(); + // The witness has to have at least 2 items, msg and the redeem script + if witness.len() >= 2 { + let redeem_script = ScriptBuf::from_bytes(witness.last().unwrap().clone()); + if matches_segwit_data(&redeem_script) == Some(true) { + data = Some(witness[witness.len() - 2].clone()); // len() - 1 is the redeem_script + break; + } + } + } + } + + data.unwrap_or(vec![]) +} diff --git a/processor/primitives/src/block.rs b/processor/primitives/src/block.rs index 89dff54f..4f721d02 100644 --- a/processor/primitives/src/block.rs +++ b/processor/primitives/src/block.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use group::{Group, GroupEncoding}; -use crate::{Id, Address, ReceivedOutput, Eventuality, EventualityTracker}; +use crate::{Address, ReceivedOutput, Eventuality, EventualityTracker}; /// A block header from an external network. pub trait BlockHeader: Send + Sync + Sized + Clone + Debug { @@ -16,12 +16,6 @@ pub trait BlockHeader: Send + Sync + Sized + Clone + Debug { fn parent(&self) -> [u8; 32]; } -/// A transaction from an external network. -pub trait Transaction: Send + Sync + Sized { - /// The type used to identify transactions on this external network. - type Id: Id; -} - /// A block from an external network. /// /// A block is defined as a consensus event associated with a set of transactions. It is not @@ -37,14 +31,8 @@ pub trait Block: Send + Sync + Sized + Clone + Debug { type Key: Group + GroupEncoding; /// The type used to represent addresses on this external network. type Address: Address; - /// The type used to represent transactions on this external network. - type Transaction: Transaction; /// The type used to represent received outputs on this external network. - type Output: ReceivedOutput< - Self::Key, - Self::Address, - TransactionId = ::Id, - >; + type Output: ReceivedOutput; /// The type used to represent an Eventuality for a transaction on this external network. type Eventuality: Eventuality< OutputId = >::Id, @@ -64,8 +52,12 @@ pub trait Block: Send + Sync + Sized + Clone + Debug { /// /// Returns tbe resolved Eventualities, indexed by the ID of the transactions which resolved /// them. + #[allow(clippy::type_complexity)] fn check_for_eventuality_resolutions( &self, eventualities: &mut EventualityTracker, - ) -> HashMap<::Id, Self::Eventuality>; + ) -> HashMap< + >::TransactionId, + Self::Eventuality, + >; }