Bitcoin processor (#232)

* serai Dockerfile & Makefile fixed

* added new bitcoin mod & bitcoinhram

* couple changes

* added odd&even check for bitcoin signing

* sign message updated

* print_keys commented out

* fixed signing process

* Added new bitcoin library & added most of bitcoin processor logic

* added new crate and refactored the bitcoin coin library

* added signing test function

* moved signature.rs

* publish set to false

* tests moved back to the root

* added new functions to rpc

* added utxo test

* added new rpc methods and refactored bitcoin processor

* added spendable output & fixed errors & added new logic for sighash & opened port 18443 for bitcoin docker

* changed tweak keys

* added tweak_keys & publish transaction and refactored bitcoin processor

* added new structs and fixed problems for testing purposes

* reverted dockerfile back its original

* reverted block generation of bitcoin to 5 seconds

* deleted unnecessary test function

* added new sighash & added new dbg messages & fixed couple errors

* fixed couple issue & removed unused functions

* fix for signing process

* crypto file for bitcoin refactored

* disabled test_send & removed some of the debug logs

* signing implemented & transaction weight calculation added & change address logic added

* refactored tweak_keys

* refactored mine_block & fixed change_address logic

* implemented new traits to bitcoin processor& refactored bitcoin processor

* added new line to tests file

* added new line to bitcoin's wallet.rs

* deleted Cargo.toml from coins folder

* edited bitcoin's Cargo.toml and added LICENSE

* added new line to bitcoin's Cargo.toml

* added spaces

* added spaces

* deleted unnecessary object

* added spaces

* deleted patch numbers

* updated sha256 parameter for message

* updated tag as const

* deleted unnecessary brackets and imports

* updated rpc.rs to 2 space indent

* deleted unnecessary brackers

* deleted unnecessary brackets

* changed it to explicit

* updated to explicit

* deleted unnecessary parsing

* added ? for easy return

* updated imports

* updated height to number

* deleted unnecessary brackets

* updated clsag to sig & to_vec to as_ref

* updated _sig to schnorr_signature

* deleted unnecessary variable

* updated Cargo.toml of processor and bitcoin

* updated imports of bitcoin processor

* updated MBlock to BBlock

* updated MSignable to BSignable

* updated imports

* deleted mask from Fee

* updated get_block function return

* updated comparison logic for scripts

* updated assert to debug_assert

* updated height to number

* updated txid logic

* updated tweak_keys definition

* updated imports

* deleted new line

* delete HashMap from monero

* deleted old test code parts

* updated test amount to a round number

* changed the test code part back to its original

* updated imports of rpc.rs

* deleted unnecessary return assignments

* deleted get_fee_per_byte

* deleted create_raw_transaction

* deleted fund_raw_transaction

* deleted sign transaction rpc

* delete verify_message rpc

* deleted get_balance

* deleted decode_raw_transaction rpc

* deleted list_transactions rpc

* changed test_send to p2wpkh

* updated imports of test_send

* fixed imports of test_send

* updated bitcoin's mine_block function

* updated bitcoin's test_send

* updated bitcoin's hram and test_signing

* deleted 2 rpc function (is_confirmed & get_transaction_block_number)

* deleted get_raw_transaction_hex

* deleted get_raw_transaction_info

* deleted new_address

* deleted test_mempool_accept

* updated remove(0) to remove(index)

* deleted ger_raw_transaction

* deleted RawTx trait and converted type to Transaction

* reverted raw_hex feature back

* added NotEnoughFunds to CoinError

* changed Sighash to all

* removed lifetime of RpcParams

* changed pub to pub(crate) & changed sig_hash line

* changed taproot_key_spend_signature_hash to internal

* added Clone to RpcError & deleted get_utxo_for

* changed to_hex to as_bytes for weight calculation

* updated SpendableOutput

* deleted unnecessary parentheses

* updated serialize of Output s id field

* deleted unused crate & added lazy_static

* updated RPC init function

* added lazy_static for TAG_HASH & updated imported crates

* changed get_block_index to get_block_number

* deleted get_block_info

* updated get_height to get_latest_block_number

* removed GetBlockWithDetailResult and get_block_with_transactions

* deleted unnecessary imports from rpc_helper

* removed lock and unlock_unspent

* deleted get_transactions and get_transaction and renamed get_raw_transaction to get_transaction

* updated opt_into_json

* changed payment_address and amount to output_script and amount for transcript

* refactored error logic for rpc & deleted anyhow crate

* added a dedicated file for json helper functions

* refactored imports and deleted unused code

* added clippy::non_snake_case

* removed unused Error items

* added new line to Cargo

* rekmoved Block and used bitcoin::Block direcetly

* removed added println and futures.len check

* removed HashMap from coin mod.rs

* updated Testnet to Regtest

* removed unnecessary variable

* updated as_str to &

* removed RawTx trait

* added newline

* changed test transaction to p2pkh

* updated test_send

* updated test_send

* updated test_send

* reformatted bitcoin processor

* moved sighash logic into signmachine

* removed generate_to_address

* added test_address function to bitcoin processor

* updated RpcResponse to enum and added Clone trait

* removed old RpcResponse

* updated shared_key to internal_key

* updated fee part

* updated test_send block logic

* added a test function for getting spendables

* updated tweaking keys logic

* updated calculate_weight logic

* added todo for BitcoinSchnorr Algorithm

* updated calculate_weight

* updated calculate_weight

* updated calculate_weight

* added a TODO for bitcoin's signing process

* removed unused code

* Finish merging develop

* cargo fmt

* cargo machete

* Handle most clippy lints on bitcoin

Doesn't handle the unused transcript due to pending cryptographic considerations.

* Rearrange imports and clippy tests

* Misc processor lint

* Update deny.toml

* Remove unnecessary RPC code

* updated test_send

* added bitcoin ci & updated test-dependencies yml

* fixed bitcoin ci

* updated bitcoin ci yml

* Remove mining from the bitcoin/monero docker files

The tests should control block production in order to test various
circumstances. The automatic mining disrupts assumptions made in testing. Since
we're now using the Bitcoin docker container for testing...

* Multiple fixes to the Bitcoin processor

Doesn't unwrap on RPC errors. Returns the expected connection error.

Fee calculation has a random - 1. This has been removed.

Supports the change address being an Option, as it is. This should not have
been blindly unwrapped.

* Remove unnecessary RPC code

* Further RPC simplifications

* Simplify Bitcoin action

It should not be mining.

* cargo fmt

* Finish RPC simplifications

* Run bitcoind as a daemon

* Remove the requirement on txindex

Saves tens of GB.

Also has attempt_send no longer return a list of outputs. That's incompatible
with this and only relevant to old scheduling designs.

* Remove number from Bitcoin SignableTransaction

Monero requires the current block number for decoy selection. Bitcoin doesn't
have a use.

* Ban coinbase transactions

These are burdened by maturity, so it's critically flawed to support them.

This causes the test_send function to fail as its working was premised on
a coinbase output. While it does make an actual output, it had insufficient
funds for the test's expectations due to regtest halving every 150 blocks.

In order to workaround this, the test will invalidate any existing chain,
offering a fresh start.

Also removes test_get_spendables and simplifies test_send.

* Various simplifications

Modifies SpendableOutput further to not require RPC calls at time of sign.

Removes the need to have get_transaction in the RPC.

* Clean prepare_send

* Update the Bitcoin TransactionMachine to output a Transaction

* Bitcoin TransactionMachine simplifications

* Update XOnly key handling

* Use a single sighash cache

* Move tweak_keys

* Remove unnecessary PSBT sets

* Restore removed newlines

* Other newlines

* Replace calculate_weight's custom math with a dummy TX serialize

* Move BTC TX construction code from processor to bitcoin

* Rename transactions.rs to wallet.rs

* Remove unused crate

* Note TODO

* Clean bitcoin signature test

* Make unit test out of BTC FROST signing test

* Final lint

* Remove usage of PartiallySignedTransaction

---------

Co-authored-by: Luke Parker <lukeparker5132@gmail.com>
This commit is contained in:
VRx 2023-01-31 13:48:14 +01:00 committed by GitHub
parent fba5b7fed4
commit c6bd00e778
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 970 additions and 59 deletions

41
.github/actions/bitcoin/action.yml vendored Normal file
View file

@ -0,0 +1,41 @@
name: bitcoin-regtest
description: Spawns a regtest Bitcoin daemon
inputs:
version:
description: "Version to download and run"
required: false
default: 24.0.1
runs:
using: "composite"
steps:
- name: Bitcoin Daemon Cache
id: cache-bitcoind
uses: actions/cache@v3
with:
path: bitcoind
key: bitcoind-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}
- name: Download the Bitcoin Daemon
if: steps.cache-bitcoind.outputs.cache-hit != 'true'
shell: bash
run: |
RUNNER_OS=linux
RUNNER_ARCH=x86_64
BASE=bitcoin-${{ inputs.version }}
FILE=$BASE-$RUNNER_ARCH-$RUNNER_OS-gnu.tar.gz
wget https://bitcoincore.org/bin/bitcoin-core-${{ inputs.version }}/$FILE
tar xzvf $FILE
cd bitcoin-${{ inputs.version }}
sudo mv bin/* /bin && sudo mv lib/* /lib
- name: Bitcoin Regtest Daemon
shell: bash
run: |
RPC_USER=serai
RPC_PASS=seraidex
bitcoind -regtest -rpcuser=$RPC_USER -rpcpassword=$RPC_PASS -daemon

View file

@ -12,6 +12,11 @@ inputs:
required: false
default: v0.18.0.0
bitcoin-version:
description: "Bitcoin version to download and run as a regtest node"
required: false
default: 24.0.1
serai:
description: "Run a Serai development node in the background"
required: false
@ -35,6 +40,11 @@ runs:
with:
version: ${{ inputs.monero-version }}
- name: Run a Bitcoin Regtest Node
uses: ./.github/actions/bitcoin
with:
version: ${{ inputs.bitcoin-version }}
- name: Run a Monero Wallet-RPC
uses: ./.github/actions/monero-wallet-rpc

55
Cargo.lock generated
View file

@ -503,6 +503,12 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dabbe35f96fb9507f7330793dc490461b2962659ac5d427181e451a623751d1"
[[package]]
name = "bech32"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445"
[[package]]
name = "beef"
version = "0.5.2"
@ -555,6 +561,46 @@ version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bitcoin"
version = "0.29.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0694ea59225b0c5f3cb405ff3f670e4828358ed26aec49dc352f730f0cb1a8a3"
dependencies = [
"bech32 0.9.1",
"bitcoin_hashes",
"secp256k1",
"serde",
]
[[package]]
name = "bitcoin-serai"
version = "0.1.0"
dependencies = [
"bitcoin",
"flexible-transcript",
"hex",
"k256",
"lazy_static",
"modular-frost",
"rand_core 0.6.4",
"reqwest",
"secp256k1",
"serde",
"serde_json",
"sha2 0.10.6",
"thiserror",
]
[[package]]
name = "bitcoin_hashes"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4"
dependencies = [
"serde",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@ -1112,7 +1158,7 @@ checksum = "c94090a6663f224feae66ab01e41a2555a8296ee07b5f20dab8888bdefc9f617"
dependencies = [
"base58check",
"base64 0.12.3",
"bech32",
"bech32 0.7.3",
"blake2",
"digest 0.10.6",
"generic-array 0.14.6",
@ -7882,7 +7928,10 @@ version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b1629c9c557ef9b293568b338dddfc8208c98a18c59d722a9d53f859d9c9b62"
dependencies = [
"bitcoin_hashes",
"rand 0.8.5",
"secp256k1-sys",
"serde",
]
[[package]]
@ -8027,15 +8076,19 @@ name = "serai-processor"
version = "0.1.0"
dependencies = [
"async-trait",
"bitcoin",
"bitcoin-serai",
"curve25519-dalek 3.2.0",
"dalek-ff-group",
"flexible-transcript",
"futures",
"group",
"hex",
"k256",
"modular-frost",
"monero-serai",
"rand_core 0.6.4",
"secp256k1",
"serde",
"serde_json",
"thiserror",

29
coins/bitcoin/Cargo.toml Normal file
View file

@ -0,0 +1,29 @@
[package]
name = "bitcoin-serai"
version = "0.1.0"
description = "A Bitcoin library for FROST-signing transactions"
license = "AGPL-3.0-only"
repository = "https://github.com/serai-dex/serai/tree/develop/coins/bitcoin"
authors = ["Luke Parker <lukeparker5132@gmail.com>", "Vrx <vrx00@proton.me>"]
edition = "2021"
publish = false
[dependencies]
lazy_static = "1"
thiserror = "1"
rand_core = "0.6"
sha2 = "0.10"
secp256k1 = { version = "0.24", features = ["global-context"] }
bitcoin = { version = "0.29", features = ["serde"] }
k256 = { version = "0.11", features = ["arithmetic"] }
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", version = "0.2", features = ["recommended"] }
frost = { version = "0.5", package = "modular-frost", path = "../../crypto/frost", features = ["secp256k1", "tests"] }
hex = "0.4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.11", features = ["json"] }

15
coins/bitcoin/LICENSE Normal file
View file

@ -0,0 +1,15 @@
AGPL-3.0-only license
Copyright (c) 2022-2023 Luke Parker
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License Version 3 as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

View file

@ -0,0 +1,58 @@
use lazy_static::lazy_static;
use sha2::{Digest, Sha256};
use k256::{
elliptic_curve::{
ops::Reduce,
sec1::{Tag, ToEncodedPoint},
},
U256, Scalar, ProjectivePoint,
};
use bitcoin::XOnlyPublicKey;
use frost::{algorithm::Hram, curve::Secp256k1};
/// Get the x coordinate of a non-infinity, even point.
pub fn x(key: &ProjectivePoint) -> [u8; 32] {
let encoded = key.to_encoded_point(true);
assert_eq!(encoded.tag(), Tag::CompressedEvenY);
(*encoded.x().expect("point at infinity")).into()
}
pub fn x_only(key: &ProjectivePoint) -> XOnlyPublicKey {
XOnlyPublicKey::from_slice(&x(key)).unwrap()
}
pub fn make_even(mut key: ProjectivePoint) -> (ProjectivePoint, u64) {
let mut c = 0;
while key.to_encoded_point(true).tag() == Tag::CompressedOddY {
key += ProjectivePoint::GENERATOR;
c += 1;
}
(key, c)
}
#[derive(Clone)]
pub struct BitcoinHram {}
lazy_static! {
static ref TAG_HASH: [u8; 32] = Sha256::digest(b"BIP0340/challenge").into();
}
#[allow(non_snake_case)]
impl Hram<Secp256k1> for BitcoinHram {
fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar {
let (R, _) = make_even(*R);
let mut data = Sha256::new();
data.update(*TAG_HASH);
data.update(*TAG_HASH);
data.update(x(&R));
data.update(x(A));
data.update(m);
Scalar::from_uint_reduced(U256::from_be_slice(&data.finalize()))
}
}

6
coins/bitcoin/src/lib.rs Normal file
View file

@ -0,0 +1,6 @@
pub mod crypto;
pub mod wallet;
pub mod rpc;
#[cfg(test)]
mod tests;

80
coins/bitcoin/src/rpc.rs Normal file
View file

@ -0,0 +1,80 @@
use core::fmt::Debug;
use thiserror::Error;
use serde::{Deserialize, de::DeserializeOwned};
use serde_json::json;
use bitcoin::{
hashes::hex::{FromHex, ToHex},
consensus::encode,
Txid, Transaction, BlockHash, Block,
};
#[derive(Clone, Debug, Deserialize)]
#[serde(untagged)]
pub(crate) enum RpcResponse<T> {
Ok { result: T },
Err { error: String },
}
#[derive(Clone, Debug)]
pub struct Rpc(String);
#[derive(Clone, PartialEq, Eq, Debug, Error)]
pub enum RpcError {
#[error("couldn't connect to node")]
ConnectionError,
#[error("request had an error: {0}")]
RequestError(String),
#[error("node sent an invalid response")]
InvalidResponse,
}
impl Rpc {
pub fn new(url: String) -> Rpc {
Rpc(url)
}
pub async fn rpc_call<Response: DeserializeOwned + Debug>(
&self,
method: &str,
params: serde_json::Value,
) -> Result<Response, RpcError> {
let client = reqwest::Client::new();
let res = client
.post(&self.0)
.json(&json!({ "jsonrpc": "2.0", "method": method, "params": params }))
.send()
.await
.map_err(|_| RpcError::ConnectionError)?
.text()
.await
.map_err(|_| RpcError::ConnectionError)?;
let res: RpcResponse<Response> =
serde_json::from_str(&res).map_err(|_| RpcError::InvalidResponse)?;
match res {
RpcResponse::Ok { result } => Ok(result),
RpcResponse::Err { error } => Err(RpcError::RequestError(error)),
}
}
pub async fn get_latest_block_number(&self) -> Result<usize, RpcError> {
self.rpc_call("getblockcount", json!([])).await
}
pub async fn get_block_hash(&self, number: usize) -> Result<BlockHash, RpcError> {
self.rpc_call("getblockhash", json!([number])).await
}
pub async fn get_block(&self, block_hash: &BlockHash) -> Result<Block, RpcError> {
let hex = self.rpc_call::<String>("getblock", json!([block_hash.to_hex(), 0])).await?;
let bytes: Vec<u8> = FromHex::from_hex(&hex).map_err(|_| RpcError::InvalidResponse)?;
encode::deserialize(&bytes).map_err(|_| RpcError::InvalidResponse)
}
pub async fn send_raw_transaction(&self, tx: &Transaction) -> Result<Txid, RpcError> {
self.rpc_call("sendrawtransaction", json!([encode::serialize_hex(tx)])).await
}
}

View file

@ -0,0 +1,47 @@
use rand_core::OsRng;
use sha2::{Digest, Sha256};
use secp256k1::{SECP256K1, Message, schnorr::Signature};
use bitcoin::hashes::{Hash as HashTrait, sha256::Hash};
use k256::Scalar;
use frost::{
curve::Secp256k1,
algorithm::Schnorr,
tests::{algorithm_machines, key_gen, sign},
};
use crate::crypto::{BitcoinHram, x_only, make_even};
#[test]
fn test_signing() {
let mut keys = key_gen::<_, Secp256k1>(&mut OsRng);
const MESSAGE: &[u8] = b"Hello, World!";
for (_, keys) in keys.iter_mut() {
let (_, offset) = make_even(keys.group_key());
*keys = keys.offset(Scalar::from(offset));
}
let algo = Schnorr::<Secp256k1, BitcoinHram>::new();
let mut sig = sign(
&mut OsRng,
algo,
keys.clone(),
algorithm_machines(&mut OsRng, Schnorr::<Secp256k1, BitcoinHram>::new(), &keys),
&Sha256::digest(MESSAGE),
);
let offset;
(sig.R, offset) = make_even(sig.R);
sig.s += Scalar::from(offset);
SECP256K1
.verify_schnorr(
&Signature::from_slice(&sig.serialize()[1 .. 65]).unwrap(),
&Message::from(Hash::hash(MESSAGE)),
&x_only(&keys[&1].group_key()),
)
.unwrap()
}

295
coins/bitcoin/src/wallet.rs Normal file
View file

@ -0,0 +1,295 @@
use std::{
io::{self, Read},
collections::HashMap,
};
use rand_core::RngCore;
use transcript::{Transcript, RecommendedTranscript};
use k256::{elliptic_curve::sec1::ToEncodedPoint, Scalar};
use frost::{curve::Secp256k1, ThresholdKeys, FrostError, algorithm::Schnorr, sign::*};
use bitcoin::{
hashes::Hash,
consensus::encode::{Encodable, Decodable, serialize},
util::sighash::{SchnorrSighashType, SighashCache, Prevouts},
OutPoint, Script, Sequence, Witness, TxIn, TxOut, PackedLockTime, Transaction, Address,
};
use crate::crypto::{BitcoinHram, make_even};
#[derive(Clone, Debug)]
pub struct SpendableOutput {
pub output: TxOut,
pub outpoint: OutPoint,
}
impl SpendableOutput {
pub fn id(&self) -> [u8; 36] {
serialize(&self.outpoint).try_into().unwrap()
}
pub fn read<R: Read>(r: &mut R) -> io::Result<SpendableOutput> {
Ok(SpendableOutput {
output: TxOut::consensus_decode(r)
.map_err(|_| io::Error::new(io::ErrorKind::Other, "invalid TxOut"))?,
outpoint: OutPoint::consensus_decode(r)
.map_err(|_| io::Error::new(io::ErrorKind::Other, "invalid OutPoint"))?,
})
}
pub fn serialize(&self) -> Vec<u8> {
let mut res = serialize(&self.output);
self.outpoint.consensus_encode(&mut res).unwrap();
res
}
}
#[derive(Clone, Debug)]
pub struct SignableTransaction(Transaction, Vec<TxOut>);
impl SignableTransaction {
fn calculate_weight(inputs: usize, payments: &[(Address, u64)], change: Option<&Address>) -> u64 {
let mut tx = Transaction {
version: 2,
lock_time: PackedLockTime::ZERO,
input: vec![
TxIn {
previous_output: OutPoint::default(),
script_sig: Script::new(),
sequence: Sequence::MAX,
witness: Witness::from_vec(vec![vec![0; 64]])
};
inputs
],
output: payments
.iter()
.map(|payment| TxOut { value: payment.1, script_pubkey: payment.0.script_pubkey() })
.collect(),
};
if let Some(change) = change {
tx.output.push(TxOut { value: 0, script_pubkey: change.script_pubkey() });
}
u64::try_from(tx.weight()).unwrap()
}
pub fn new(
mut inputs: Vec<SpendableOutput>,
payments: &[(Address, u64)],
change: Option<Address>,
fee: u64,
) -> Option<SignableTransaction> {
let input_sat = inputs.iter().map(|input| input.output.value).sum::<u64>();
let tx_ins = inputs
.iter()
.map(|input| TxIn {
previous_output: input.outpoint,
script_sig: Script::new(),
sequence: Sequence::MAX,
witness: Witness::new(),
})
.collect::<Vec<_>>();
let payment_sat = payments.iter().map(|payment| payment.1).sum::<u64>();
let mut tx_outs = payments
.iter()
.map(|payment| TxOut { value: payment.1, script_pubkey: payment.0.script_pubkey() })
.collect::<Vec<_>>();
let actual_fee = fee * Self::calculate_weight(tx_ins.len(), payments, None);
if payment_sat > (input_sat - actual_fee) {
return None;
}
// If there's a change address, check if there's a meaningful change
if let Some(change) = change.as_ref() {
let fee_with_change = fee * Self::calculate_weight(tx_ins.len(), payments, Some(change));
// If there's a non-zero change, add it
if let Some(value) = input_sat.checked_sub(payment_sat + fee_with_change) {
tx_outs.push(TxOut { value, script_pubkey: change.script_pubkey() });
}
}
// TODO: Drop outputs which BTC will consider spam (outputs worth less than the cost to spend
// them)
Some(SignableTransaction(
Transaction { version: 2, lock_time: PackedLockTime::ZERO, input: tx_ins, output: tx_outs },
inputs.drain(..).map(|input| input.output).collect(),
))
}
pub async fn multisig(
self,
keys: ThresholdKeys<Secp256k1>,
mut transcript: RecommendedTranscript,
) -> Result<TransactionMachine, FrostError> {
transcript.domain_separate(b"bitcoin_transaction");
transcript.append_message(b"root_key", keys.group_key().to_encoded_point(true).as_bytes());
// Transcript the inputs and outputs
let tx = &self.0;
for input in &tx.input {
transcript.append_message(b"input_hash", input.previous_output.txid.as_hash().into_inner());
transcript.append_message(b"input_output_index", input.previous_output.vout.to_le_bytes());
}
for payment in &tx.output {
transcript.append_message(b"output_script", payment.script_pubkey.as_bytes());
transcript.append_message(b"output_amount", payment.value.to_le_bytes());
}
let mut sigs = vec![];
for _ in 0 .. tx.input.len() {
// TODO: Use the above transcript here
sigs.push(
AlgorithmMachine::new(Schnorr::<Secp256k1, BitcoinHram>::new(), keys.clone()).unwrap(),
);
}
Ok(TransactionMachine { tx: self, transcript, sigs })
}
}
pub struct TransactionMachine {
tx: SignableTransaction,
transcript: RecommendedTranscript,
sigs: Vec<AlgorithmMachine<Secp256k1, Schnorr<Secp256k1, BitcoinHram>>>,
}
impl PreprocessMachine for TransactionMachine {
type Preprocess = Vec<Preprocess<Secp256k1, ()>>;
type Signature = Transaction;
type SignMachine = TransactionSignMachine;
fn preprocess<R: RngCore + rand_core::CryptoRng>(
mut self,
rng: &mut R,
) -> (Self::SignMachine, Self::Preprocess) {
let mut preprocesses = Vec::with_capacity(self.sigs.len());
let sigs = self
.sigs
.drain(..)
.map(|sig| {
let (sig, preprocess) = sig.preprocess(rng);
preprocesses.push(preprocess);
sig
})
.collect();
(TransactionSignMachine { tx: self.tx, transcript: self.transcript, sigs }, preprocesses)
}
}
pub struct TransactionSignMachine {
tx: SignableTransaction,
transcript: RecommendedTranscript,
sigs: Vec<AlgorithmSignMachine<Secp256k1, Schnorr<Secp256k1, BitcoinHram>>>,
}
impl SignMachine<Transaction> for TransactionSignMachine {
type Params = ();
type Keys = ThresholdKeys<Secp256k1>;
type Preprocess = Vec<Preprocess<Secp256k1, ()>>;
type SignatureShare = Vec<SignatureShare<Secp256k1>>;
type SignatureMachine = TransactionSignatureMachine;
fn cache(self) -> CachedPreprocess {
unimplemented!(
"Bitcoin transactions don't support caching their preprocesses due to {}",
"being already bound to a specific transaction"
);
}
fn from_cache(
_: (),
_: ThresholdKeys<Secp256k1>,
_: CachedPreprocess,
) -> Result<Self, FrostError> {
unimplemented!(
"Bitcoin transactions don't support caching their preprocesses due to {}",
"being already bound to a specific transaction"
);
}
fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess> {
self.sigs.iter().map(|sig| sig.read_preprocess(reader)).collect()
}
fn sign(
mut self,
commitments: HashMap<u16, Self::Preprocess>,
msg: &[u8],
) -> Result<(TransactionSignatureMachine, Self::SignatureShare), FrostError> {
if !msg.is_empty() {
Err(FrostError::InternalError(
"message was passed to the TransactionMachine when it generates its own",
))?;
}
let commitments = (0 .. self.sigs.len())
.map(|c| {
commitments
.iter()
.map(|(l, commitments)| (*l, commitments[c].clone()))
.collect::<HashMap<_, _>>()
})
.collect::<Vec<_>>();
let mut cache = SighashCache::new(&self.tx.0);
let prevouts = Prevouts::All(&self.tx.1);
let mut shares = Vec::with_capacity(self.sigs.len());
let sigs = self
.sigs
.drain(..)
.enumerate()
.map(|(i, sig)| {
let tx_sighash = cache
.taproot_key_spend_signature_hash(i, &prevouts, SchnorrSighashType::Default)
.unwrap();
let (sig, share) = sig.sign(commitments[i].clone(), &tx_sighash)?;
shares.push(share);
Ok(sig)
})
.collect::<Result<_, _>>()?;
Ok((TransactionSignatureMachine { tx: self.tx.0, sigs }, shares))
}
}
pub struct TransactionSignatureMachine {
tx: Transaction,
sigs: Vec<AlgorithmSignatureMachine<Secp256k1, Schnorr<Secp256k1, BitcoinHram>>>,
}
impl SignatureMachine<Transaction> for TransactionSignatureMachine {
type SignatureShare = Vec<SignatureShare<Secp256k1>>;
fn read_share<R: Read>(&self, reader: &mut R) -> io::Result<Self::SignatureShare> {
self.sigs.iter().map(|sig| sig.read_share(reader)).collect()
}
fn complete(
mut self,
mut shares: HashMap<u16, Self::SignatureShare>,
) -> Result<Transaction, FrostError> {
for (input, schnorr) in self.tx.input.iter_mut().zip(self.sigs.drain(..)) {
let mut sig = schnorr.complete(
shares.iter_mut().map(|(l, shares)| (*l, shares.remove(0))).collect::<HashMap<_, _>>(),
)?;
// TODO: Implement BitcoinSchnorr Algorithm to handle this
let offset;
(sig.R, offset) = make_even(sig.R);
sig.s += Scalar::from(offset);
let mut witness: Witness = Witness::new();
witness.push(&sig.serialize()[1 .. 65]);
input.witness = witness;
}
Ok(self.tx)
}
}

View file

@ -44,6 +44,7 @@ allow-osi-fsf-free = "neither"
default = "deny"
exceptions = [
{ allow = ["AGPL-3.0"], name = "bitcoin-serai" },
{ allow = ["AGPL-3.0"], name = "ethereum-serai" },
{ allow = ["AGPL-3.0"], name = "serai-processor" },

View file

@ -8,7 +8,7 @@ ENV BITCOIN_DATA=/home/bitcoin/.bitcoin
WORKDIR /home/bitcoin
RUN apk update \
&& apk --no-cache add ca-certificates gnupg bash su-exec
&& apk --no-cache add ca-certificates gnupg bash su-exec
# Get Binary
# TODO: When bitcoin.org publishes 23.0, retrieve checksums from there.
@ -49,6 +49,3 @@ COPY ./scripts /scripts
EXPOSE 8332 8333 18332 18333 18443 18444
VOLUME ["/home/bitcoin/.bitcoin"]
# Run
CMD ["bitcoind"]

View file

@ -1,29 +1,6 @@
#!/bin/sh
RPC_USER="${RPC_USER:=serai}"
RPC_PASS="${RPC_PASS:=seraidex}"
# address: bcrt1q7kc7tm3a4qljpw4gg5w73cgya6g9nfydtessgs
# private key: cV9X6E3J9jq7R1XR8uPED2JqFxqcd6KrC8XWPy1GchZj7MA7G9Wx
MINER="${MINER:=bcrt1q7kc7tm3a4qljpw4gg5w73cgya6g9nfydtessgs}"
PRIV_KEY="${PRIV_KEY:=cV9X6E3J9jq7R1XR8uPED2JqFxqcd6KrC8XWPy1GchZj7MA7G9Wx}"
BLOCK_TIME=${BLOCK_TIME:=5}
bitcoind -regtest -txindex -fallbackfee=0.000001 -rpcuser=$RPC_USER -rpcpassword=$RPC_PASS -rpcallowip=0.0.0.0/0 -rpcbind=127.0.0.1 -rpcbind=$(hostname) &
# give time to bitcoind to start
while true
do
bitcoin-cli -regtest -rpcuser=$RPC_USER -rpcpassword=$RPC_PASS generatetoaddress 100 $MINER && break
sleep 5
done
bitcoin-cli -regtest -rpcuser=$RPC_USER -rpcpassword=$RPC_PASS createwallet "miner" false false $RPC_PASS false false true &&
bitcoin-cli -regtest -rpcuser=$RPC_USER -rpcpassword=$RPC_PASS walletpassphrase $RPC_PASS 60 &&
bitcoin-cli -regtest -rpcuser=$RPC_USER -rpcpassword=$RPC_PASS importprivkey $PRIV_KEY
# mine a new block every BLOCK_TIME
while true
do
bitcoin-cli -regtest -rpcuser=$RPC_USER -rpcpassword=$RPC_PASS generatetoaddress 1 $MINER
sleep $BLOCK_TIME
done
bitcoind -regtest -rpcuser=$RPC_USER -rpcpassword=$RPC_PASS -rpcallowip=0.0.0.0/0 -rpcbind=127.0.0.1 -rpcbind=$(hostname)

View file

@ -1,5 +1,5 @@
# Prepare Environment
FROM alpine:latest as builder
# https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.1.0.tar.bz2
# Verification will fail if MONERO_VERSION doesn't match the latest
# due to the way monero publishes releases. They overwrite a single hashes.txt file
@ -38,5 +38,3 @@ COPY ./scripts /scripts
EXPOSE 18080 18081
VOLUME /home/monero/.bitmonero
CMD ["monerod"]

View file

@ -9,10 +9,3 @@ BLOCK_TIME=${BLOCK_TIME:=5}
monerod --regtest --rpc-access-control-origins * --confirm-external-bind \
--rpc-bind-ip=0.0.0.0 --offline --fixed-difficulty=1 \
--non-interactive --mining-threads 1 --detach
# give time to monerod to start
while true; do
sleep 5
done
# Create wallet from PRIV_KEY in monero wallet

View file

@ -152,6 +152,8 @@ services:
volumes:
- "./coins/bitcoin/scripts:/scripts"
entrypoint: /scripts/entry-dev.sh
ports:
- "18443:18443"
ethereum:
profiles:

View file

@ -26,10 +26,18 @@ curve25519-dalek = { version = "3", features = ["std"] }
dalek-ff-group = { path = "../crypto/dalek-ff-group" }
transcript = { package = "flexible-transcript", path = "../crypto/transcript" }
frost = { package = "modular-frost", path = "../crypto/frost", features = ["ed25519"] }
frost = { package = "modular-frost", path = "../crypto/frost", features = ["secp256k1", "ed25519"] }
# Monero
monero-serai = { path = "../coins/monero", features = ["multisig"] }
bitcoin-serai = { path = "../coins/bitcoin" }
k256 = { version = "0.11", features = ["arithmetic"] }
bitcoin = "0.29"
hex = "0.4"
secp256k1 = { version = "0.24", features = ["global-context", "rand-std"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[dev-dependencies]
rand_core = "0.6"
@ -41,4 +49,4 @@ serde_json = "1.0"
futures = "0.3"
tokio = { version = "1", features = ["full"] }
frost = { package = "modular-frost", path = "../crypto/frost", features = ["ed25519", "tests"] }
frost = { package = "modular-frost", path = "../crypto/frost", features = ["tests"] }

View file

@ -0,0 +1,283 @@
use std::io;
use async_trait::async_trait;
#[rustfmt::skip]
use bitcoin::{
hashes::Hash, schnorr::TweakedPublicKey, OutPoint, Transaction, Block, Network, Address
};
#[cfg(test)]
use bitcoin::{
secp256k1::{SECP256K1, SecretKey, Message},
PrivateKey, PublicKey, EcdsaSighashType,
blockdata::script::Builder,
PackedLockTime, Sequence, Script, Witness, TxIn, TxOut,
};
use transcript::RecommendedTranscript;
use k256::{
ProjectivePoint, Scalar,
elliptic_curve::sec1::{ToEncodedPoint, Tag},
};
use frost::{curve::Secp256k1, ThresholdKeys};
use bitcoin_serai::{
crypto::{x_only, make_even},
wallet::{SpendableOutput, TransactionMachine, SignableTransaction as BSignableTransaction},
rpc::Rpc,
};
use crate::coin::{CoinError, Block as BlockTrait, OutputType, Output as OutputTrait, Coin};
impl BlockTrait for Block {
type Id = [u8; 32];
fn id(&self) -> Self::Id {
self.block_hash().as_hash().into_inner()
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Fee(u64);
#[derive(Clone, Debug)]
pub struct Output(SpendableOutput);
impl OutputTrait for Output {
type Id = [u8; 36];
// TODO: Implement later
fn kind(&self) -> OutputType {
OutputType::External
}
fn id(&self) -> Self::Id {
self.0.id()
}
fn amount(&self) -> u64 {
self.0.output.value
}
fn serialize(&self) -> Vec<u8> {
self.0.serialize()
}
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
SpendableOutput::read(reader).map(Output)
}
}
#[derive(Debug)]
pub struct SignableTransaction {
keys: ThresholdKeys<Secp256k1>,
transcript: RecommendedTranscript,
actual: BSignableTransaction,
}
#[derive(Clone, Debug)]
pub struct Bitcoin {
pub(crate) rpc: Rpc,
}
impl Bitcoin {
pub async fn new(url: String) -> Bitcoin {
Bitcoin { rpc: Rpc::new(url) }
}
#[cfg(test)]
pub async fn fresh_chain(&self) {
if self.rpc.get_latest_block_number().await.unwrap() > 0 {
self
.rpc
.rpc_call("invalidateblock", serde_json::json!([self.rpc.get_block_hash(1).await.unwrap()]))
.await
.unwrap()
}
}
}
#[async_trait]
impl Coin for Bitcoin {
type Curve = Secp256k1;
type Fee = Fee;
type Transaction = Transaction;
type Block = Block;
type Output = Output;
type SignableTransaction = SignableTransaction;
type TransactionMachine = TransactionMachine;
type Address = Address;
const ID: &'static [u8] = b"Bitcoin";
const CONFIRMATIONS: usize = 3;
// TODO: Get hard numbers and tune
const MAX_INPUTS: usize = 128;
const MAX_OUTPUTS: usize = 16;
fn tweak_keys(&self, key: &mut ThresholdKeys<Self::Curve>) {
let (_, offset) = make_even(key.group_key());
*key = key.offset(Scalar::from(offset));
}
fn address(&self, key: ProjectivePoint) -> Self::Address {
debug_assert!(key.to_encoded_point(true).tag() == Tag::CompressedEvenY, "YKey is odd");
Address::p2tr_tweaked(
TweakedPublicKey::dangerous_assume_tweaked(x_only(&key)),
Network::Regtest,
)
}
// TODO: Implement later
fn branch_address(&self, key: ProjectivePoint) -> Self::Address {
self.address(key)
}
async fn get_latest_block_number(&self) -> Result<usize, CoinError> {
Ok(self.rpc.get_latest_block_number().await.map_err(|_| CoinError::ConnectionError)?)
}
async fn get_block(&self, number: usize) -> Result<Self::Block, CoinError> {
let block_hash =
self.rpc.get_block_hash(number).await.map_err(|_| CoinError::ConnectionError)?;
self.rpc.get_block(&block_hash).await.map_err(|_| CoinError::ConnectionError)
}
async fn get_outputs(
&self,
block: &Self::Block,
key: ProjectivePoint,
) -> Result<Vec<Self::Output>, CoinError> {
let main_addr = self.address(key);
let mut outputs = Vec::new();
// Skip the coinbase transaction which is burdened by maturity
for tx in &block.txdata[1 ..] {
for (vout, output) in tx.output.iter().enumerate() {
if output.script_pubkey == main_addr.script_pubkey() {
outputs.push(Output(SpendableOutput {
output: output.clone(),
outpoint: OutPoint { txid: tx.txid(), vout: u32::try_from(vout).unwrap() },
}));
}
}
}
Ok(outputs)
}
async fn prepare_send(
&self,
keys: ThresholdKeys<Secp256k1>,
transcript: RecommendedTranscript,
_: usize,
mut inputs: Vec<Output>,
payments: &[(Address, u64)],
change: Option<ProjectivePoint>,
fee: Fee,
) -> Result<Self::SignableTransaction, CoinError> {
Ok(SignableTransaction {
keys,
transcript,
actual: BSignableTransaction::new(
inputs.drain(..).map(|input| input.0).collect(),
payments,
// TODO: Diversify to a proper change address
change.map(|change| self.address(change)),
fee.0,
)
.ok_or(CoinError::NotEnoughFunds)?,
})
}
async fn attempt_send(
&self,
transaction: Self::SignableTransaction,
) -> Result<Self::TransactionMachine, CoinError> {
transaction
.actual
.clone()
.multisig(transaction.keys.clone(), transaction.transcript.clone())
.await
.map_err(|_| CoinError::ConnectionError)
}
async fn publish_transaction(&self, tx: &Self::Transaction) -> Result<Vec<u8>, CoinError> {
Ok(self.rpc.send_raw_transaction(tx).await.unwrap().to_vec())
}
#[cfg(test)]
async fn get_fee(&self) -> Self::Fee {
Fee(1)
}
#[cfg(test)]
async fn mine_block(&self) {
self
.rpc
.rpc_call::<Vec<String>>(
"generatetoaddress",
serde_json::json!([
1,
Address::p2sh(&Script::new(), Network::Regtest).unwrap().to_string()
]),
)
.await
.unwrap();
}
#[cfg(test)]
async fn test_send(&self, address: Self::Address) {
let secret_key = SecretKey::new(&mut rand_core::OsRng);
let private_key = PrivateKey::new(secret_key, Network::Regtest);
let public_key = PublicKey::from_private_key(SECP256K1, &private_key);
let main_addr = Address::p2pkh(&public_key, Network::Regtest);
let new_block = self.get_latest_block_number().await.unwrap() + 1;
self
.rpc
.rpc_call::<Vec<String>>("generatetoaddress", serde_json::json!([1, main_addr]))
.await
.unwrap();
for _ in 0 .. 100 {
self.mine_block().await;
}
// TODO: Consider grabbing bdk as a dev dependency
let tx = self.get_block(new_block).await.unwrap().txdata.swap_remove(0);
let mut tx = Transaction {
version: 2,
lock_time: PackedLockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint { txid: tx.txid(), vout: 0 },
script_sig: Script::default(),
sequence: Sequence(u32::MAX),
witness: Witness::default(),
}],
output: vec![TxOut {
value: tx.output[0].value - 10000,
script_pubkey: address.script_pubkey(),
}],
};
let mut der = SECP256K1
.sign_ecdsa_low_r(
&Message::from(
tx.signature_hash(0, &main_addr.script_pubkey(), EcdsaSighashType::All.to_u32())
.as_hash(),
),
&private_key.inner,
)
.serialize_der()
.to_vec();
der.push(1);
tx.input[0].script_sig = Builder::new().push_slice(&der).push_key(&public_key).into_script();
self.rpc.send_raw_transaction(&tx).await.unwrap();
for _ in 0 .. Self::CONFIRMATIONS {
self.mine_block().await;
}
}
}

View file

@ -10,6 +10,9 @@ use frost::{
sign::PreprocessMachine,
};
pub mod bitcoin;
pub use self::bitcoin::Bitcoin;
pub mod monero;
pub use self::monero::Monero;
@ -17,6 +20,8 @@ pub use self::monero::Monero;
pub enum CoinError {
#[error("failed to connect to coin daemon")]
ConnectionError,
#[error("not enough funds")]
NotEnoughFunds,
}
pub trait Block: Sized + Clone {
@ -62,6 +67,8 @@ pub trait Coin {
const MAX_INPUTS: usize;
const MAX_OUTPUTS: usize; // TODO: Decide if this includes change or not
fn tweak_keys(&self, key: &mut ThresholdKeys<Self::Curve>);
/// Address for the given group key to receive external coins to.
// Doesn't have to take self, enables some level of caching which is pleasant
fn address(&self, key: <Self::Curve as Ciphersuite>::G) -> Self::Address;
@ -93,10 +100,7 @@ pub trait Coin {
transaction: Self::SignableTransaction,
) -> Result<Self::TransactionMachine, CoinError>;
async fn publish_transaction(
&self,
tx: &Self::Transaction,
) -> Result<(Vec<u8>, Vec<<Self::Output as Output>::Id>), CoinError>;
async fn publish_transaction(&self, tx: &Self::Transaction) -> Result<Vec<u8>, CoinError>;
#[cfg(test)]
async fn get_fee(&self) -> Self::Fee;

View file

@ -162,6 +162,9 @@ impl Coin for Monero {
const MAX_INPUTS: usize = 128;
const MAX_OUTPUTS: usize = 16;
// Monero doesn't require/benefit from tweaking
fn tweak_keys(&self, _: &mut ThresholdKeys<Self::Curve>) {}
fn address(&self, key: dfg::EdwardsPoint) -> Self::Address {
self.address_internal(key, EXTERNAL_SUBADDRESS)
}
@ -258,12 +261,9 @@ impl Coin for Monero {
.map_err(|_| CoinError::ConnectionError)
}
async fn publish_transaction(
&self,
tx: &Self::Transaction,
) -> Result<(Vec<u8>, Vec<<Self::Output as OutputTrait>::Id>), CoinError> {
async fn publish_transaction(&self, tx: &Self::Transaction) -> Result<Vec<u8>, CoinError> {
self.rpc.publish_transaction(tx).await.map_err(|_| CoinError::ConnectionError)?;
Ok((tx.hash().to_vec(), tx.prefix.outputs.iter().map(|output| output.key.to_bytes()).collect()))
Ok(tx.hash().to_vec())
}
#[cfg(test)]

View file

@ -0,0 +1,12 @@
use crate::{
coin::{Coin, Bitcoin},
tests::test_send,
};
#[tokio::test]
async fn bitcoin() {
let bitcoin = Bitcoin::new("http://serai:seraidex@127.0.0.1:18443".to_string()).await;
bitcoin.fresh_chain().await;
let fee = bitcoin.get_fee().await;
test_send(bitcoin, fee).await;
}

View file

@ -1,4 +1,5 @@
mod send;
pub(crate) use send::test_send;
mod bitcoin;
mod monero;

View file

@ -94,7 +94,7 @@ pub async fn test_send<C: Coin + Clone>(coin: C, fee: C::Fee) {
let latest = coin.get_latest_block_number().await.unwrap();
wallet.acknowledge_block(1, latest - (C::CONFIRMATIONS - 1));
let signable = wallet
.prepare_sends(1, vec![(wallet.address(), 10000000000)], fee)
.prepare_sends(1, vec![(wallet.address(), 100000000)], fee)
.await
.unwrap()
.1
@ -102,5 +102,5 @@ pub async fn test_send<C: Coin + Clone>(coin: C, fee: C::Fee) {
futures.push(wallet.attempt_send(network, signable));
}
println!("{:?}", hex::encode(futures::future::join_all(futures).await.swap_remove(0).unwrap().0));
println!("{:?}", hex::encode(futures::future::join_all(futures).await.swap_remove(0).unwrap()));
}

View file

@ -225,7 +225,10 @@ impl<D: CoinDb, C: Coin> Wallet<D, C> {
}
pub fn add_keys(&mut self, keys: &WalletKeys<C::Curve>) {
self.pending.push((self.acknowledged_block(keys.creation_block), keys.bind(C::ID)));
let creation_block = keys.creation_block;
let mut keys = keys.bind(C::ID);
self.coin.tweak_keys(&mut keys);
self.pending.push((self.acknowledged_block(creation_block), keys));
}
pub fn address(&self) -> C::Address {
@ -262,8 +265,7 @@ impl<D: CoinDb, C: Coin> Wallet<D, C> {
.coin
.get_outputs(&block, keys.group_key())
.await?
.iter()
.cloned()
.drain(..)
.filter(|output| self.db.add_output(output)),
);
}
@ -282,7 +284,7 @@ impl<D: CoinDb, C: Coin> Wallet<D, C> {
pub async fn prepare_sends(
&mut self,
canonical: usize,
payments: Vec<(C::Address, u64)>,
mut payments: Vec<(C::Address, u64)>,
fee: C::Fee,
) -> Result<(Vec<(C::Address, u64)>, Vec<C::SignableTransaction>), CoinError> {
if payments.is_empty() {
@ -296,7 +298,6 @@ impl<D: CoinDb, C: Coin> Wallet<D, C> {
// As each payment re-appears, let mut payments = schedule[payment] where the only input is
// the source payment
// let (mut payments, schedule) = schedule(payments);
let mut payments = payments;
let mut txs = vec![];
for (keys, outputs) in self.keys.iter_mut() {
@ -342,7 +343,7 @@ impl<D: CoinDb, C: Coin> Wallet<D, C> {
&mut self,
network: &mut N,
prepared: C::SignableTransaction,
) -> Result<(Vec<u8>, Vec<<C::Output as Output>::Id>), SignError> {
) -> Result<Vec<u8>, SignError> {
let attempt = self.coin.attempt_send(prepared).await.map_err(SignError::CoinError)?;
let (attempt, commitments) = attempt.preprocess(&mut OsRng);