Adjust Bitcoin processor layout

This commit is contained in:
Luke Parker
2024-09-11 03:09:44 -04:00
parent 776cbbb9a4
commit b61ba9d1bb
6 changed files with 13 additions and 119 deletions

View File

@@ -0,0 +1,77 @@
use core::fmt;
use std::collections::HashMap;
use ciphersuite::{Ciphersuite, Secp256k1};
use bitcoin_serai::bitcoin::block::{Header, Block as BBlock};
use serai_client::networks::bitcoin::Address;
use serai_db::Db;
use primitives::{ReceivedOutput, EventualityTracker};
use crate::{hash_bytes, scan::scanner, output::Output, transaction::Eventuality};
#[derive(Clone, Debug)]
pub(crate) struct BlockHeader(pub(crate) 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)]
pub(crate) struct Block<D: Db>(pub(crate) D, pub(crate) BBlock);
impl<D: Db> fmt::Debug for Block<D> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.debug_struct("Block").field("1", &self.1).finish_non_exhaustive()
}
}
#[async_trait::async_trait]
impl<D: Db> primitives::Block for Block<D> {
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.1.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.1.txdata[1 ..] {
for output in scanner.scan_transaction(tx) {
res.push(Output::new(&self.0, 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.1.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

@@ -0,0 +1,3 @@
pub(crate) mod output;
pub(crate) mod transaction;
pub(crate) mod block;

View File

@@ -0,0 +1,175 @@
use std::io;
use ciphersuite::{Ciphersuite, Secp256k1};
use bitcoin_serai::{
bitcoin::{
hashes::Hash as HashTrait,
key::{Parity, XOnlyPublicKey},
consensus::Encodable,
script::Instruction,
transaction::Transaction,
},
wallet::ReceivedOutput as WalletOutput,
};
use scale::{Encode, Decode, IoReader};
use borsh::{BorshSerialize, BorshDeserialize};
use serai_db::Get;
use serai_client::{
primitives::{Coin, Amount, Balance, ExternalAddress},
networks::bitcoin::Address,
};
use primitives::{OutputType, ReceivedOutput};
use crate::scan::{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 {
fn default() -> Self {
Self([0; 36])
}
}
impl AsRef<[u8]> for OutputId {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl AsMut<[u8]> for OutputId {
fn as_mut(&mut self) -> &mut [u8] {
self.0.as_mut()
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub(crate) struct Output {
kind: OutputType,
presumed_origin: Option<Address>,
pub(crate) output: WalletOutput,
data: Vec<u8>,
}
impl Output {
pub(crate) fn new(
getter: &impl Get,
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(getter, tx),
output,
data: extract_serai_data(tx),
}
}
pub(crate) fn new_with_presumed_origin(
key: <Secp256k1 as Ciphersuite>::G,
tx: &Transaction,
presumed_origin: Option<Address>,
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,
output,
data: extract_serai_data(tx),
}
}
}
impl ReceivedOutput<<Secp256k1 as Ciphersuite>::G, Address> for Output {
type Id = OutputId;
type TransactionId = [u8; 32];
fn kind(&self) -> OutputType {
self.kind
}
fn id(&self) -> Self::Id {
let mut id = OutputId::default();
self.output.outpoint().consensus_encode(&mut id.as_mut()).unwrap();
id
}
fn transaction_id(&self) -> Self::TransactionId {
let mut res = self.output.outpoint().txid.to_raw_hash().to_byte_array();
res.reverse();
res
}
fn key(&self) -> <Secp256k1 as Ciphersuite>::G {
// We read the key from the script pubkey so we don't have to independently store it
let script = &self.output.output().script_pubkey;
// These assumptions are safe since it's an output we successfully scanned
assert!(script.is_p2tr());
let Instruction::PushBytes(key) = script.instructions_minimal().last().unwrap().unwrap() else {
panic!("last item in v1 Taproot script wasn't bytes")
};
let key = XOnlyPublicKey::from_slice(key.as_ref())
.expect("last item in v1 Taproot script wasn't a valid x-only public key");
// Convert to a full key
let key = key.public_key(Parity::Even);
// Convert to a k256 key (from libsecp256k1)
let output_key = Secp256k1::read_G(&mut key.serialize().as_slice()).unwrap();
// The output's key minus the output's offset is the root key
output_key - (<Secp256k1 as Ciphersuite>::G::GENERATOR * self.output.offset())
}
fn presumed_origin(&self) -> Option<Address> {
self.presumed_origin.clone()
}
fn balance(&self) -> Balance {
Balance { coin: Coin::Bitcoin, amount: Amount(self.output.value()) }
}
fn data(&self) -> &[u8] {
&self.data
}
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
self.kind.write(writer)?;
let presumed_origin: Option<ExternalAddress> = self.presumed_origin.clone().map(Into::into);
writer.write_all(&presumed_origin.encode())?;
self.output.write(writer)?;
writer.write_all(&u16::try_from(self.data.len()).unwrap().to_le_bytes())?;
writer.write_all(&self.data)
}
fn read<R: io::Read>(mut reader: &mut R) -> io::Result<Self> {
Ok(Output {
kind: OutputType::read(reader)?,
presumed_origin: {
Option::<ExternalAddress>::decode(&mut IoReader(&mut reader))
.map_err(|e| io::Error::other(format!("couldn't decode ExternalAddress: {e:?}")))?
.map(|address| {
Address::try_from(address)
.map_err(|()| io::Error::other("couldn't decode Address from ExternalAddress"))
})
.transpose()?
},
output: WalletOutput::read(reader)?,
data: {
let mut data_len = [0; 2];
reader.read_exact(&mut data_len)?;
let mut data = vec![0; usize::from(u16::from_le_bytes(data_len))];
reader.read_exact(&mut data)?;
data
},
})
}
}

View File

@@ -0,0 +1,167 @@
use std::io;
use rand_core::{RngCore, CryptoRng};
use transcript::{Transcript, RecommendedTranscript};
use ciphersuite::Secp256k1;
use frost::{dkg::ThresholdKeys, sign::PreprocessMachine};
use bitcoin_serai::{
bitcoin::{
consensus::{Encodable, Decodable},
ScriptBuf, Transaction as BTransaction,
},
wallet::{
ReceivedOutput, TransactionError, SignableTransaction as BSignableTransaction,
TransactionMachine,
},
};
use borsh::{BorshSerialize, BorshDeserialize};
use serai_client::networks::bitcoin::Address;
use crate::output::OutputId;
#[derive(Clone, Debug)]
pub(crate) struct Transaction(pub(crate) BTransaction);
impl From<BTransaction> for Transaction {
fn from(tx: BTransaction) -> Self {
Self(tx)
}
}
impl scheduler::Transaction for Transaction {
fn read(reader: &mut impl io::Read) -> io::Result<Self> {
let tx =
BTransaction::consensus_decode(&mut io::BufReader::new(reader)).map_err(io::Error::other)?;
Ok(Self(tx))
}
fn write(&self, writer: &mut impl io::Write) -> io::Result<()> {
let mut writer = io::BufWriter::new(writer);
self.0.consensus_encode(&mut writer)?;
writer.into_inner()?;
Ok(())
}
}
#[derive(Clone, Debug)]
pub(crate) struct SignableTransaction {
pub(crate) inputs: Vec<ReceivedOutput>,
pub(crate) payments: Vec<(Address, u64)>,
pub(crate) change: Option<Address>,
pub(crate) fee_per_vbyte: u64,
}
impl SignableTransaction {
fn signable(self) -> Result<BSignableTransaction, TransactionError> {
BSignableTransaction::new(
self.inputs,
&self
.payments
.iter()
.cloned()
.map(|(address, amount)| (ScriptBuf::from(address), amount))
.collect::<Vec<_>>(),
self.change.map(ScriptBuf::from),
None,
self.fee_per_vbyte,
)
}
}
#[derive(Clone)]
pub(crate) struct ClonableTransctionMachine(SignableTransaction, ThresholdKeys<Secp256k1>);
impl PreprocessMachine for ClonableTransctionMachine {
type Preprocess = <TransactionMachine as PreprocessMachine>::Preprocess;
type Signature = <TransactionMachine as PreprocessMachine>::Signature;
type SignMachine = <TransactionMachine as PreprocessMachine>::SignMachine;
fn preprocess<R: RngCore + CryptoRng>(
self,
rng: &mut R,
) -> (Self::SignMachine, Self::Preprocess) {
self
.0
.signable()
.expect("signing an invalid SignableTransaction")
.multisig(&self.1, RecommendedTranscript::new(b"Serai Processor Bitcoin Transaction"))
.expect("incorrect keys used for SignableTransaction")
.preprocess(rng)
}
}
impl scheduler::SignableTransaction for SignableTransaction {
type Transaction = Transaction;
type Ciphersuite = Secp256k1;
type PreprocessMachine = ClonableTransctionMachine;
fn read(reader: &mut impl io::Read) -> io::Result<Self> {
let inputs = {
let mut input_len = [0; 4];
reader.read_exact(&mut input_len)?;
let mut inputs = vec![];
for _ in 0 .. u32::from_le_bytes(input_len) {
inputs.push(ReceivedOutput::read(reader)?);
}
inputs
};
let payments = <_>::deserialize_reader(reader)?;
let change = <_>::deserialize_reader(reader)?;
let fee_per_vbyte = <_>::deserialize_reader(reader)?;
Ok(Self { inputs, payments, change, fee_per_vbyte })
}
fn write(&self, writer: &mut impl io::Write) -> io::Result<()> {
writer.write_all(&u32::try_from(self.inputs.len()).unwrap().to_le_bytes())?;
for input in &self.inputs {
input.write(writer)?;
}
self.payments.serialize(writer)?;
self.change.serialize(writer)?;
self.fee_per_vbyte.serialize(writer)?;
Ok(())
}
fn id(&self) -> [u8; 32] {
self.clone().signable().unwrap().txid()
}
fn sign(self, keys: ThresholdKeys<Self::Ciphersuite>) -> Self::PreprocessMachine {
ClonableTransctionMachine(self, keys)
}
}
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
pub(crate) struct Eventuality {
pub(crate) txid: [u8; 32],
pub(crate) singular_spent_output: Option<OutputId>,
}
impl primitives::Eventuality for Eventuality {
type OutputId = OutputId;
fn id(&self) -> [u8; 32] {
self.txid
}
// We define the lookup as our ID since the resolving transaction only has a singular possible ID
fn lookup(&self) -> Vec<u8> {
self.txid.to_vec()
}
fn singular_spent_output(&self) -> Option<Self::OutputId> {
self.singular_spent_output.clone()
}
fn read(reader: &mut impl io::Read) -> io::Result<Self> {
Self::deserialize_reader(reader)
}
fn write(&self, writer: &mut impl io::Write) -> io::Result<()> {
self.serialize(writer)
}
}