serai/processor/bitcoin/src/primitives/output.rs
Luke Parker 59fa49f750 Continue filling out main loop
Adds generics to the db_channel macro, fixes the bug where it needed at least
one key.
2024-09-19 23:36:32 -07:00

170 lines
4.7 KiB
Rust

use std::io;
use ciphersuite::{Ciphersuite, Secp256k1};
use bitcoin_serai::{
bitcoin::{
hashes::Hash as HashTrait, 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::{
primitives::x_coord_to_even_point,
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 = x_coord_to_even_point(key.as_ref())
.expect("last item in scanned v1 Taproot script wasn't a valid x-only public key");
// The output's key minus the output's offset is the root key
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
},
})
}
}