Bitcoin Output/Transaction definitions

This commit is contained in:
Luke Parker
2024-09-10 03:48:06 -04:00
parent 0ccf71df1e
commit 247cc8f0cc
17 changed files with 504 additions and 299 deletions

View File

@@ -18,14 +18,15 @@ workspace = true
[dependencies]
async-trait = { version = "0.1", default-features = false }
rand_core = { version = "0.6", default-features = false }
const-hex = { version = "1", default-features = false }
hex = { version = "0.4", default-features = false, features = ["std"] }
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["std"] }
borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] }
serde_json = { version = "1", default-features = false, features = ["std"] }
k256 = { version = "^0.13.1", default-features = false, features = ["std"] }
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", default-features = false, features = ["std", "recommended"] }
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std", "secp256k1"] }
frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false }
secp256k1 = { version = "0.29", default-features = false, features = ["std", "global-context", "rand-std"] }
bitcoin-serai = { path = "../../networks/bitcoin", default-features = false, features = ["std"] }
@@ -37,8 +38,13 @@ zalloc = { path = "../../common/zalloc" }
serai-db = { path = "../../common/db" }
serai-env = { path = "../../common/env" }
serai-client = { path = "../../substrate/client", default-features = false, features = ["bitcoin"] }
messages = { package = "serai-processor-messages", path = "../messages" }
primitives = { package = "serai-processor-primitives", path = "../primitives" }
scheduler = { package = "serai-processor-scheduler-primitives", path = "../scheduler/primitives" }
message-queue = { package = "serai-message-queue", path = "../../message-queue" }
[features]

View File

View File

@@ -2,7 +2,15 @@
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
use std::{sync::OnceLock, time::Duration, io, collections::HashMap};
#[global_allocator]
static ALLOCATOR: zalloc::ZeroizingAlloc<std::alloc::System> =
zalloc::ZeroizingAlloc(std::alloc::System);
mod output;
mod transaction;
/*
use std::{sync::LazyLock, time::Duration, io, collections::HashMap};
use async_trait::async_trait;
@@ -49,127 +57,9 @@ use serai_client::{
primitives::{MAX_DATA_LEN, Coin, NetworkId, Amount, Balance},
networks::bitcoin::Address,
};
*/
use crate::{
networks::{
NetworkError, Block as BlockTrait, OutputType, Output as OutputTrait,
Transaction as TransactionTrait, SignableTransaction as SignableTransactionTrait,
Eventuality as EventualityTrait, EventualitiesTracker, Network, UtxoNetwork,
},
Payment,
multisigs::scheduler::utxo::Scheduler,
};
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct OutputId(pub [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 struct Output {
kind: OutputType,
presumed_origin: Option<Address>,
output: ReceivedOutput,
data: Vec<u8>,
}
impl OutputTrait<Bitcoin> for Output {
type Id = OutputId;
fn kind(&self) -> OutputType {
self.kind
}
fn id(&self) -> Self::Id {
let mut res = OutputId::default();
self.output.outpoint().consensus_encode(&mut res.as_mut()).unwrap();
debug_assert_eq!(
{
let mut outpoint = vec![];
self.output.outpoint().consensus_encode(&mut outpoint).unwrap();
outpoint
},
res.as_ref().to_vec()
);
res
}
fn tx_id(&self) -> [u8; 32] {
let mut hash = *self.output.outpoint().txid.as_raw_hash().as_byte_array();
hash.reverse();
hash
}
fn key(&self) -> ProjectivePoint {
let script = &self.output.output().script_pubkey;
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 x-only public key");
Secp256k1::read_G(&mut key.public_key(Parity::Even).serialize().as_slice()).unwrap() -
(ProjectivePoint::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<Vec<u8>> = 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: {
let mut io_reader = scale::IoReader(reader);
let res = Option::<Vec<u8>>::decode(&mut io_reader)
.unwrap()
.map(|address| Address::try_from(address).unwrap());
reader = io_reader.0;
res
},
output: ReceivedOutput::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
},
})
}
}
/*
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Fee(u64);
@@ -201,71 +91,6 @@ impl TransactionTrait<Bitcoin> for Transaction {
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Eventuality([u8; 32]);
#[derive(Clone, PartialEq, Eq, Default, Debug)]
pub struct EmptyClaim;
impl AsRef<[u8]> for EmptyClaim {
fn as_ref(&self) -> &[u8] {
&[]
}
}
impl AsMut<[u8]> for EmptyClaim {
fn as_mut(&mut self) -> &mut [u8] {
&mut []
}
}
impl EventualityTrait for Eventuality {
type Claim = EmptyClaim;
type Completion = Transaction;
fn lookup(&self) -> Vec<u8> {
self.0.to_vec()
}
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
let mut id = [0; 32];
reader
.read_exact(&mut id)
.map_err(|_| io::Error::other("couldn't decode ID in eventuality"))?;
Ok(Eventuality(id))
}
fn serialize(&self) -> Vec<u8> {
self.0.to_vec()
}
fn claim(_: &Transaction) -> EmptyClaim {
EmptyClaim
}
fn serialize_completion(completion: &Transaction) -> Vec<u8> {
let mut buf = vec![];
completion.consensus_encode(&mut buf).unwrap();
buf
}
fn read_completion<R: io::Read>(reader: &mut R) -> io::Result<Transaction> {
Transaction::consensus_decode(&mut io::BufReader::with_capacity(0, reader))
.map_err(|e| io::Error::other(format!("{e}")))
}
}
#[derive(Clone, Debug)]
pub struct SignableTransaction {
actual: BSignableTransaction,
}
impl PartialEq for SignableTransaction {
fn eq(&self, other: &SignableTransaction) -> bool {
self.actual == other.actual
}
}
impl Eq for SignableTransaction {}
impl SignableTransactionTrait for SignableTransaction {
fn fee(&self) -> u64 {
self.actual.fee()
}
}
#[async_trait]
impl BlockTrait<Bitcoin> for Block {
type Id = [u8; 32];
@@ -944,3 +769,4 @@ impl Network for Bitcoin {
impl UtxoNetwork for Bitcoin {
const MAX_INPUTS: usize = MAX_INPUTS;
}
*/

View File

@@ -0,0 +1,133 @@
use std::io;
use ciphersuite::{Ciphersuite, Secp256k1};
use bitcoin_serai::{
bitcoin::{
hashes::Hash as HashTrait,
key::{Parity, XOnlyPublicKey},
consensus::Encodable,
script::Instruction,
},
wallet::ReceivedOutput as WalletOutput,
};
use scale::{Encode, Decode, IoReader};
use borsh::{BorshSerialize, BorshDeserialize};
use serai_client::{
primitives::{Coin, Amount, Balance, ExternalAddress},
networks::bitcoin::Address,
};
use primitives::{OutputType, ReceivedOutput};
#[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>,
output: WalletOutput,
data: Vec<u8>,
}
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 {
self.output.outpoint().txid.to_raw_hash().to_byte_array()
}
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,170 @@
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(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 {
inputs: Vec<ReceivedOutput>,
payments: Vec<(Address, u64)>,
change: Option<Address>,
data: Option<Vec<u8>>,
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),
self.data,
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 data = <_>::deserialize_reader(reader)?;
let fee_per_vbyte = <_>::deserialize_reader(reader)?;
Ok(Self { inputs, payments, change, data, 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.data.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 {
txid: [u8; 32],
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)
}
}