diff --git a/Cargo.lock b/Cargo.lock index 81e3d1de..ee8c8a99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8127,18 +8127,21 @@ dependencies = [ "async-trait", "bitcoin-serai", "borsh", - "const-hex", + "ciphersuite", "env_logger", - "hex", - "k256", + "flexible-transcript", "log", + "modular-frost", "parity-scale-codec", + "rand_core", "secp256k1", + "serai-client", "serai-db", "serai-env", "serai-message-queue", "serai-processor-messages", - "serde_json", + "serai-processor-primitives", + "serai-processor-scheduler-primitives", "tokio", "zalloc", ] @@ -8151,6 +8154,7 @@ dependencies = [ "bitcoin", "bitvec", "blake2", + "borsh", "ciphersuite", "dockertest", "frame-system", diff --git a/processor/bitcoin/Cargo.toml b/processor/bitcoin/Cargo.toml index a5749542..656c7c40 100644 --- a/processor/bitcoin/Cargo.toml +++ b/processor/bitcoin/Cargo.toml @@ -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] diff --git a/processor/bitcoin/src/block.rs b/processor/bitcoin/src/block.rs new file mode 100644 index 00000000..e69de29b diff --git a/processor/bitcoin/src/lib.rs b/processor/bitcoin/src/lib.rs index bccdc286..112d8fd3 100644 --- a/processor/bitcoin/src/lib.rs +++ b/processor/bitcoin/src/lib.rs @@ -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 = + 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
, - output: ReceivedOutput, - data: Vec, -} - -impl OutputTrait 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
{ - 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(&self, writer: &mut W) -> io::Result<()> { - self.kind.write(writer)?; - let presumed_origin: Option> = 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(mut reader: &mut R) -> io::Result { - Ok(Output { - kind: OutputType::read(reader)?, - presumed_origin: { - let mut io_reader = scale::IoReader(reader); - let res = Option::>::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 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 { - self.0.to_vec() - } - - fn read(reader: &mut R) -> io::Result { - 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 { - self.0.to_vec() - } - - fn claim(_: &Transaction) -> EmptyClaim { - EmptyClaim - } - fn serialize_completion(completion: &Transaction) -> Vec { - let mut buf = vec![]; - completion.consensus_encode(&mut buf).unwrap(); - buf - } - fn read_completion(reader: &mut R) -> io::Result { - 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 for Block { type Id = [u8; 32]; @@ -944,3 +769,4 @@ impl Network for Bitcoin { impl UtxoNetwork for Bitcoin { const MAX_INPUTS: usize = MAX_INPUTS; } +*/ diff --git a/processor/bitcoin/src/output.rs b/processor/bitcoin/src/output.rs new file mode 100644 index 00000000..cc624319 --- /dev/null +++ b/processor/bitcoin/src/output.rs @@ -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
, + output: WalletOutput, + data: Vec, +} + +impl ReceivedOutput<::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) -> ::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 - (::G::GENERATOR * self.output.offset()) + } + + fn presumed_origin(&self) -> Option
{ + 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(&self, writer: &mut W) -> io::Result<()> { + self.kind.write(writer)?; + let presumed_origin: Option = 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(mut reader: &mut R) -> io::Result { + Ok(Output { + kind: OutputType::read(reader)?, + presumed_origin: { + Option::::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 + }, + }) + } +} diff --git a/processor/bitcoin/src/transaction.rs b/processor/bitcoin/src/transaction.rs new file mode 100644 index 00000000..ef48d3f0 --- /dev/null +++ b/processor/bitcoin/src/transaction.rs @@ -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 for Transaction { + fn from(tx: BTransaction) -> Self { + Self(tx) + } +} + +impl scheduler::Transaction for Transaction { + fn read(reader: &mut impl io::Read) -> io::Result { + 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, + payments: Vec<(Address, u64)>, + change: Option
, + data: Option>, + fee_per_vbyte: u64, +} + +impl SignableTransaction { + fn signable(self) -> Result { + BSignableTransaction::new( + self.inputs, + &self + .payments + .iter() + .cloned() + .map(|(address, amount)| (ScriptBuf::from(address), amount)) + .collect::>(), + self.change.map(ScriptBuf::from), + self.data, + self.fee_per_vbyte, + ) + } +} + +#[derive(Clone)] +pub(crate) struct ClonableTransctionMachine(SignableTransaction, ThresholdKeys); +impl PreprocessMachine for ClonableTransctionMachine { + type Preprocess = ::Preprocess; + type Signature = ::Signature; + type SignMachine = ::SignMachine; + + fn preprocess( + 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 { + 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::PreprocessMachine { + ClonableTransctionMachine(self, keys) + } +} + +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] +pub(crate) struct Eventuality { + txid: [u8; 32], + singular_spent_output: Option, +} + +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 { + self.txid.to_vec() + } + + fn singular_spent_output(&self) -> Option { + self.singular_spent_output.clone() + } + + fn read(reader: &mut impl io::Read) -> io::Result { + Self::deserialize_reader(reader) + } + fn write(&self, writer: &mut impl io::Write) -> io::Result<()> { + self.serialize(writer) + } +} diff --git a/processor/primitives/src/lib.rs b/processor/primitives/src/lib.rs index 4e45fa8f..cc915ca2 100644 --- a/processor/primitives/src/lib.rs +++ b/processor/primitives/src/lib.rs @@ -46,7 +46,24 @@ pub trait Id: + BorshDeserialize { } -impl Id for [u8; N] where [u8; N]: Default {} +impl< + I: Send + + Sync + + Clone + + Default + + PartialEq + + Eq + + Hash + + AsRef<[u8]> + + AsMut<[u8]> + + Debug + + Encode + + Decode + + BorshSerialize + + BorshDeserialize, + > Id for I +{ +} /// A wrapper for a group element which implements the scale/borsh traits. #[derive(Clone, Copy, PartialEq, Eq, Debug)] diff --git a/processor/primitives/src/output.rs b/processor/primitives/src/output.rs index cbfe59f3..76acde60 100644 --- a/processor/primitives/src/output.rs +++ b/processor/primitives/src/output.rs @@ -19,10 +19,19 @@ pub trait Address: + BorshSerialize + BorshDeserialize { - /// Write this address. - fn write(&self, writer: &mut impl io::Write) -> io::Result<()>; - /// Read an address. - fn read(reader: &mut impl io::Read) -> io::Result; +} +// This casts a wide net, yet it only implements `Address` for things `Into` so +// it should only implement this for addresses +impl< + A: Send + + Sync + + Clone + + Into + + TryFrom + + BorshSerialize + + BorshDeserialize, + > Address for A +{ } /// The type of the output. diff --git a/processor/primitives/src/payment.rs b/processor/primitives/src/payment.rs index 67a5bbad..4c1e04f4 100644 --- a/processor/primitives/src/payment.rs +++ b/processor/primitives/src/payment.rs @@ -48,7 +48,7 @@ impl Payment { /// Read a Payment. pub fn read(reader: &mut impl io::Read) -> io::Result { - let address = A::read(reader)?; + let address = A::deserialize_reader(reader)?; let reader = &mut IoReader(reader); let balance = Balance::decode(reader).map_err(io::Error::other)?; let data = Option::>::decode(reader).map_err(io::Error::other)?; @@ -56,7 +56,7 @@ impl Payment { } /// Write the Payment. pub fn write(&self, writer: &mut impl io::Write) -> io::Result<()> { - self.address.write(writer).unwrap(); + self.address.serialize(writer)?; self.balance.encode_to(writer); self.data.encode_to(writer); Ok(()) diff --git a/processor/scanner/src/db.rs b/processor/scanner/src/db.rs index 52a36419..ef37ef38 100644 --- a/processor/scanner/src/db.rs +++ b/processor/scanner/src/db.rs @@ -10,7 +10,7 @@ use serai_db::{Get, DbTxn, create_db, db_channel}; use serai_in_instructions_primitives::{InInstructionWithBalance, Batch}; use serai_coins_primitives::OutInstructionWithBalance; -use primitives::{EncodableG, Address, ReceivedOutput}; +use primitives::{EncodableG, ReceivedOutput}; use crate::{ lifetime::{LifetimeStage, Lifetime}, @@ -49,7 +49,7 @@ impl OutputWithInInstruction { let mut opt = [0xff]; reader.read_exact(&mut opt)?; assert!((opt[0] == 0) || (opt[0] == 1)); - (opt[0] == 1).then(|| AddressFor::::read(reader)).transpose()? + (opt[0] == 1).then(|| AddressFor::::deserialize_reader(reader)).transpose()? }; let in_instruction = InInstructionWithBalance::decode(&mut IoReader(reader)).map_err(io::Error::other)?; @@ -59,7 +59,7 @@ impl OutputWithInInstruction { self.output.write(writer)?; if let Some(return_address) = &self.return_address { writer.write_all(&[1])?; - return_address.write(writer)?; + return_address.serialize(writer)?; } else { writer.write_all(&[0])?; } @@ -278,7 +278,7 @@ impl ScannerGlobalDb { buf.read_exact(&mut opt).unwrap(); assert!((opt[0] == 0) || (opt[0] == 1)); - let address = (opt[0] == 1).then(|| AddressFor::::read(&mut buf).unwrap()); + let address = (opt[0] == 1).then(|| AddressFor::::deserialize_reader(&mut buf).unwrap()); Some((address, InInstructionWithBalance::decode(&mut IoReader(buf)).unwrap())) } } @@ -338,7 +338,7 @@ impl ScanToEventualityDb { let mut buf = vec![]; if let Some(address) = &forward.return_address { buf.write_all(&[1]).unwrap(); - address.write(&mut buf).unwrap(); + address.serialize(&mut buf).unwrap(); } else { buf.write_all(&[0]).unwrap(); } @@ -435,7 +435,8 @@ impl Returnable { reader.read_exact(&mut opt).unwrap(); assert!((opt[0] == 0) || (opt[0] == 1)); - let return_address = (opt[0] == 1).then(|| AddressFor::::read(reader)).transpose()?; + let return_address = + (opt[0] == 1).then(|| AddressFor::::deserialize_reader(reader)).transpose()?; let in_instruction = InInstructionWithBalance::decode(&mut IoReader(reader)).map_err(io::Error::other)?; @@ -444,7 +445,7 @@ impl Returnable { fn write(&self, writer: &mut impl io::Write) -> io::Result<()> { if let Some(return_address) = &self.return_address { writer.write_all(&[1])?; - return_address.write(writer)?; + return_address.serialize(writer)?; } else { writer.write_all(&[0])?; } diff --git a/processor/scanner/src/lib.rs b/processor/scanner/src/lib.rs index 5919ff7e..9831d41a 100644 --- a/processor/scanner/src/lib.rs +++ b/processor/scanner/src/lib.rs @@ -7,6 +7,7 @@ use std::{io, collections::HashMap}; use group::GroupEncoding; +use borsh::{BorshSerialize, BorshDeserialize}; use serai_db::{Get, DbTxn, Db}; use serai_primitives::{NetworkId, Coin, Amount}; @@ -179,12 +180,12 @@ pub struct Return { impl Return { pub(crate) fn write(&self, writer: &mut impl io::Write) -> io::Result<()> { - self.address.write(writer)?; + self.address.serialize(writer)?; self.output.write(writer) } pub(crate) fn read(reader: &mut impl io::Read) -> io::Result { - let address = AddressFor::::read(reader)?; + let address = AddressFor::::deserialize_reader(reader)?; let output = OutputFor::::read(reader)?; Ok(Return { address, output }) } diff --git a/processor/scanner/src/report/db.rs b/processor/scanner/src/report/db.rs index 05239779..10a3f6bb 100644 --- a/processor/scanner/src/report/db.rs +++ b/processor/scanner/src/report/db.rs @@ -4,12 +4,11 @@ use std::io::{Read, Write}; use group::GroupEncoding; use scale::{Encode, Decode, IoReader}; +use borsh::{BorshSerialize, BorshDeserialize}; use serai_db::{Get, DbTxn, create_db}; use serai_primitives::Balance; -use primitives::Address; - use crate::{ScannerFeed, KeyFor, AddressFor}; create_db!( @@ -92,7 +91,7 @@ impl ReportDb { for return_information in return_information { if let Some(ReturnInformation { address, balance }) = return_information { buf.write_all(&[1]).unwrap(); - address.write(&mut buf).unwrap(); + address.serialize(&mut buf).unwrap(); balance.encode_to(&mut buf); } else { buf.write_all(&[0]).unwrap(); @@ -115,7 +114,7 @@ impl ReportDb { assert!((opt[0] == 0) || (opt[0] == 1)); res.push((opt[0] == 1).then(|| { - let address = AddressFor::::read(&mut buf).unwrap(); + let address = AddressFor::::deserialize_reader(&mut buf).unwrap(); let balance = Balance::decode(&mut IoReader(&mut buf)).unwrap(); ReturnInformation { address, balance } })); diff --git a/processor/scheduler/primitives/src/lib.rs b/processor/scheduler/primitives/src/lib.rs index f146027d..3c214d15 100644 --- a/processor/scheduler/primitives/src/lib.rs +++ b/processor/scheduler/primitives/src/lib.rs @@ -11,7 +11,7 @@ use frost::{dkg::ThresholdKeys, sign::PreprocessMachine}; use serai_db::DbTxn; /// A transaction. -pub trait Transaction: Sized { +pub trait Transaction: Sized + Send { /// Read a `Transaction`. fn read(reader: &mut impl io::Read) -> io::Result; /// Write a `Transaction`. @@ -20,10 +20,12 @@ pub trait Transaction: Sized { /// A signable transaction. pub trait SignableTransaction: 'static + Sized + Send + Sync + Clone { + /// The underlying transaction type. + type Transaction: Transaction; /// The ciphersuite used to sign this transaction. type Ciphersuite: Ciphersuite; /// The preprocess machine for the signing protocol for this transaction. - type PreprocessMachine: Clone + PreprocessMachine; + type PreprocessMachine: Clone + PreprocessMachine>; /// Read a `SignableTransaction`. fn read(reader: &mut impl io::Read) -> io::Result; @@ -42,8 +44,7 @@ pub trait SignableTransaction: 'static + Sized + Send + Sync + Clone { } /// The transaction type for a SignableTransaction. -pub type TransactionFor = - <::PreprocessMachine as PreprocessMachine>::Signature; +pub type TransactionFor = ::Transaction; mod db { use serai_db::{Get, DbTxn, create_db, db_channel}; diff --git a/processor/signers/src/transaction/mod.rs b/processor/signers/src/transaction/mod.rs index 9311eb32..b9b62e75 100644 --- a/processor/signers/src/transaction/mod.rs +++ b/processor/signers/src/transaction/mod.rs @@ -185,6 +185,8 @@ impl> } } Response::Signature { id, signature: signed_tx } => { + let signed_tx: TransactionFor = signed_tx.into(); + // Save this transaction to the database { let mut buf = Vec::with_capacity(256); diff --git a/substrate/client/Cargo.toml b/substrate/client/Cargo.toml index e653c9af..5cba05f0 100644 --- a/substrate/client/Cargo.toml +++ b/substrate/client/Cargo.toml @@ -24,6 +24,7 @@ bitvec = { version = "1", default-features = false, features = ["alloc", "serde" hex = "0.4" scale = { package = "parity-scale-codec", version = "3" } +borsh = { version = "1" } serde = { version = "1", features = ["derive"], optional = true } serde_json = { version = "1", optional = true } diff --git a/substrate/client/src/networks/bitcoin.rs b/substrate/client/src/networks/bitcoin.rs index 502bfb44..28f66053 100644 --- a/substrate/client/src/networks/bitcoin.rs +++ b/substrate/client/src/networks/bitcoin.rs @@ -1,6 +1,7 @@ use core::{str::FromStr, fmt}; use scale::{Encode, Decode}; +use borsh::{BorshSerialize, BorshDeserialize}; use bitcoin::{ hashes::{Hash as HashTrait, hash160::Hash}, @@ -10,47 +11,10 @@ use bitcoin::{ address::{AddressType, NetworkChecked, Address as BAddress}, }; -#[derive(Clone, Eq, Debug)] -pub struct Address(ScriptBuf); +use crate::primitives::ExternalAddress; -impl PartialEq for Address { - fn eq(&self, other: &Self) -> bool { - // Since Serai defines the Bitcoin-address specification as a variant of the script alone, - // define equivalency as the script alone - self.0 == other.0 - } -} - -impl From
for ScriptBuf { - fn from(addr: Address) -> ScriptBuf { - addr.0 - } -} - -impl FromStr for Address { - type Err = (); - fn from_str(str: &str) -> Result { - Address::new( - BAddress::from_str(str) - .map_err(|_| ())? - .require_network(Network::Bitcoin) - .map_err(|_| ())? - .script_pubkey(), - ) - .ok_or(()) - } -} - -impl fmt::Display for Address { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - BAddress::::from_script(&self.0, Network::Bitcoin) - .map_err(|_| fmt::Error)? - .fmt(f) - } -} - -// SCALE-encoded variant of Monero addresses. -#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)] +// SCALE-encodable representation of Bitcoin addresses, used internally. +#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, BorshSerialize, BorshDeserialize)] enum EncodedAddress { P2PKH([u8; 20]), P2SH([u8; 20]), @@ -59,34 +23,13 @@ enum EncodedAddress { P2TR([u8; 32]), } -impl TryFrom> for Address { +impl TryFrom<&ScriptBuf> for EncodedAddress { type Error = (); - fn try_from(data: Vec) -> Result { - Ok(Address(match EncodedAddress::decode(&mut data.as_ref()).map_err(|_| ())? { - EncodedAddress::P2PKH(hash) => { - ScriptBuf::new_p2pkh(&PubkeyHash::from_raw_hash(Hash::from_byte_array(hash))) - } - EncodedAddress::P2SH(hash) => { - ScriptBuf::new_p2sh(&ScriptHash::from_raw_hash(Hash::from_byte_array(hash))) - } - EncodedAddress::P2WPKH(hash) => { - ScriptBuf::new_witness_program(&WitnessProgram::new(WitnessVersion::V0, &hash).unwrap()) - } - EncodedAddress::P2WSH(hash) => { - ScriptBuf::new_witness_program(&WitnessProgram::new(WitnessVersion::V0, &hash).unwrap()) - } - EncodedAddress::P2TR(key) => { - ScriptBuf::new_witness_program(&WitnessProgram::new(WitnessVersion::V1, &key).unwrap()) - } - })) - } -} - -fn try_to_vec(addr: &Address) -> Result, ()> { - let parsed_addr = - BAddress::::from_script(&addr.0, Network::Bitcoin).map_err(|_| ())?; - Ok( - (match parsed_addr.address_type() { + fn try_from(script_buf: &ScriptBuf) -> Result { + // This uses mainnet as our encodings don't specify a network. + let parsed_addr = + BAddress::::from_script(script_buf, Network::Bitcoin).map_err(|_| ())?; + Ok(match parsed_addr.address_type() { Some(AddressType::P2pkh) => { EncodedAddress::P2PKH(*parsed_addr.pubkey_hash().unwrap().as_raw_hash().as_byte_array()) } @@ -110,23 +53,119 @@ fn try_to_vec(addr: &Address) -> Result, ()> { } _ => Err(())?, }) - .encode(), - ) + } } -impl From
for Vec { - fn from(addr: Address) -> Vec { +impl From for ScriptBuf { + fn from(encoded: EncodedAddress) -> Self { + match encoded { + EncodedAddress::P2PKH(hash) => { + ScriptBuf::new_p2pkh(&PubkeyHash::from_raw_hash(Hash::from_byte_array(hash))) + } + EncodedAddress::P2SH(hash) => { + ScriptBuf::new_p2sh(&ScriptHash::from_raw_hash(Hash::from_byte_array(hash))) + } + EncodedAddress::P2WPKH(hash) => { + ScriptBuf::new_witness_program(&WitnessProgram::new(WitnessVersion::V0, &hash).unwrap()) + } + EncodedAddress::P2WSH(hash) => { + ScriptBuf::new_witness_program(&WitnessProgram::new(WitnessVersion::V0, &hash).unwrap()) + } + EncodedAddress::P2TR(key) => { + ScriptBuf::new_witness_program(&WitnessProgram::new(WitnessVersion::V1, &key).unwrap()) + } + } + } +} + +/// A Bitcoin address usable with Serai. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Address(ScriptBuf); + +// Support consuming into the underlying ScriptBuf. +impl From
for ScriptBuf { + fn from(addr: Address) -> ScriptBuf { + addr.0 + } +} + +impl From<&Address> for BAddress { + fn from(addr: &Address) -> BAddress { + // This fails if the script doesn't have an address representation, yet all our representable + // addresses' scripts do + BAddress::::from_script(&addr.0, Network::Bitcoin).unwrap() + } +} + +// Support converting a string into an address. +impl FromStr for Address { + type Err = (); + fn from_str(str: &str) -> Result { + Address::new( + BAddress::from_str(str) + .map_err(|_| ())? + .require_network(Network::Bitcoin) + .map_err(|_| ())? + .script_pubkey(), + ) + .ok_or(()) + } +} + +// Support converting an address into a string. +impl fmt::Display for Address { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + BAddress::from(self).fmt(f) + } +} + +impl TryFrom for Address { + type Error = (); + fn try_from(data: ExternalAddress) -> Result { + // Decode as an EncodedAddress, then map to a ScriptBuf + let mut data = data.as_ref(); + let encoded = EncodedAddress::decode(&mut data).map_err(|_| ())?; + if !data.is_empty() { + Err(())? + } + Ok(Address(ScriptBuf::from(encoded))) + } +} + +impl From
for EncodedAddress { + fn from(addr: Address) -> EncodedAddress { // Safe since only encodable addresses can be created - try_to_vec(&addr).unwrap() + EncodedAddress::try_from(&addr.0).unwrap() + } +} + +impl From
for ExternalAddress { + fn from(addr: Address) -> ExternalAddress { + // Safe since all variants are fixed-length and fit into MAX_ADDRESS_LEN + ExternalAddress::new(EncodedAddress::from(addr).encode()).unwrap() + } +} + +impl BorshSerialize for Address { + fn serialize(&self, writer: &mut W) -> borsh::io::Result<()> { + EncodedAddress::from(self.clone()).serialize(writer) + } +} + +impl BorshDeserialize for Address { + fn deserialize_reader(reader: &mut R) -> borsh::io::Result { + Ok(Self(ScriptBuf::from(EncodedAddress::deserialize_reader(reader)?))) } } impl Address { - pub fn new(address: ScriptBuf) -> Option { - let res = Self(address); - if try_to_vec(&res).is_ok() { - return Some(res); + /// Create a new Address from a ScriptBuf. + pub fn new(script_buf: ScriptBuf) -> Option { + // If we can represent this Script, it's an acceptable address + if EncodedAddress::try_from(&script_buf).is_ok() { + return Some(Self(script_buf)); } + // Else, it isn't acceptable None } } diff --git a/substrate/primitives/src/lib.rs b/substrate/primitives/src/lib.rs index d2c52219..2cf37e00 100644 --- a/substrate/primitives/src/lib.rs +++ b/substrate/primitives/src/lib.rs @@ -62,7 +62,7 @@ pub fn borsh_deserialize_bounded_vec &[u8] { - self.0.as_ref() - } - #[cfg(feature = "std")] pub fn consume(self) -> Vec { self.0.into_inner()