Add bitcoin Block trait impl

This commit is contained in:
Luke Parker 2024-09-10 06:25:21 -04:00
parent 247cc8f0cc
commit 2d4b775b6e
5 changed files with 239 additions and 69 deletions

View file

@ -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 = <Secp256k1 as Ciphersuite>::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<Self::Output> {
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<Self::Eventuality>,
) -> HashMap<
<Self::Output as ReceivedOutput<Self::Key, Self::Address>>::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
}
}

View file

@ -6,8 +6,19 @@
static ALLOCATOR: zalloc::ZeroizingAlloc<std::alloc::System> = static ALLOCATOR: zalloc::ZeroizingAlloc<std::alloc::System> =
zalloc::ZeroizingAlloc(std::alloc::System); zalloc::ZeroizingAlloc(std::alloc::System);
mod scanner;
mod output; mod output;
mod transaction; 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}; 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<bool> {
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<u8> {
// 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)] #[cfg(test)]
pub fn sign_btc_input_for_p2pkh( pub fn sign_btc_input_for_p2pkh(
tx: &Transaction, tx: &Transaction,

View file

@ -8,6 +8,7 @@ use bitcoin_serai::{
key::{Parity, XOnlyPublicKey}, key::{Parity, XOnlyPublicKey},
consensus::Encodable, consensus::Encodable,
script::Instruction, script::Instruction,
transaction::Transaction,
}, },
wallet::ReceivedOutput as WalletOutput, wallet::ReceivedOutput as WalletOutput,
}; };
@ -22,6 +23,8 @@ use serai_client::{
use primitives::{OutputType, ReceivedOutput}; 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)] #[derive(Clone, PartialEq, Eq, Hash, Debug, Encode, Decode, BorshSerialize, BorshDeserialize)]
pub(crate) struct OutputId([u8; 36]); pub(crate) struct OutputId([u8; 36]);
impl Default for OutputId { impl Default for OutputId {
@ -48,6 +51,20 @@ pub(crate) struct Output {
data: Vec<u8>, data: Vec<u8>,
} }
impl Output {
pub fn new(key: <Secp256k1 as Ciphersuite>::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<<Secp256k1 as Ciphersuite>::G, Address> for Output { impl ReceivedOutput<<Secp256k1 as Ciphersuite>::G, Address> for Output {
type Id = OutputId; type Id = OutputId;
type TransactionId = [u8; 32]; type TransactionId = [u8; 32];
@ -63,7 +80,9 @@ impl ReceivedOutput<<Secp256k1 as Ciphersuite>::G, Address> for Output {
} }
fn transaction_id(&self) -> Self::TransactionId { 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) -> <Secp256k1 as Ciphersuite>::G { fn key(&self) -> <Secp256k1 as Ciphersuite>::G {

View file

@ -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<<Secp256k1 as Ciphersuite>::F> =
LazyLock::new(|| Secp256k1::hash_to_F(KEY_DST, b"branch"));
static CHANGE_BASE_OFFSET: LazyLock<<Secp256k1 as Ciphersuite>::F> =
LazyLock::new(|| Secp256k1::hash_to_F(KEY_DST, b"change"));
static FORWARD_BASE_OFFSET: LazyLock<<Secp256k1 as Ciphersuite>::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: <Secp256k1 as Ciphersuite>::G,
) -> HashMap<OutputType, <Secp256k1 as Ciphersuite>::F> {
let mut offsets = HashMap::from([(OutputType::External, <Secp256k1 as Ciphersuite>::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: <Secp256k1 as Ciphersuite>::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<Address> {
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<bool> {
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<u8> {
// 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![])
}

View file

@ -3,7 +3,7 @@ use std::collections::HashMap;
use group::{Group, GroupEncoding}; use group::{Group, GroupEncoding};
use crate::{Id, Address, ReceivedOutput, Eventuality, EventualityTracker}; use crate::{Address, ReceivedOutput, Eventuality, EventualityTracker};
/// A block header from an external network. /// A block header from an external network.
pub trait BlockHeader: Send + Sync + Sized + Clone + Debug { 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]; 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 from an external network.
/// ///
/// A block is defined as a consensus event associated with a set of transactions. It is not /// 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; type Key: Group + GroupEncoding;
/// The type used to represent addresses on this external network. /// The type used to represent addresses on this external network.
type Address: Address; 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. /// The type used to represent received outputs on this external network.
type Output: ReceivedOutput< type Output: ReceivedOutput<Self::Key, Self::Address>;
Self::Key,
Self::Address,
TransactionId = <Self::Transaction as Transaction>::Id,
>;
/// The type used to represent an Eventuality for a transaction on this external network. /// The type used to represent an Eventuality for a transaction on this external network.
type Eventuality: Eventuality< type Eventuality: Eventuality<
OutputId = <Self::Output as ReceivedOutput<Self::Key, Self::Address>>::Id, OutputId = <Self::Output as ReceivedOutput<Self::Key, Self::Address>>::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 /// Returns tbe resolved Eventualities, indexed by the ID of the transactions which resolved
/// them. /// them.
#[allow(clippy::type_complexity)]
fn check_for_eventuality_resolutions( fn check_for_eventuality_resolutions(
&self, &self,
eventualities: &mut EventualityTracker<Self::Eventuality>, eventualities: &mut EventualityTracker<Self::Eventuality>,
) -> HashMap<<Self::Transaction as Transaction>::Id, Self::Eventuality>; ) -> HashMap<
<Self::Output as ReceivedOutput<Self::Key, Self::Address>>::TransactionId,
Self::Eventuality,
>;
} }