mirror of
https://github.com/serai-dex/serai.git
synced 2024-12-22 19:49:22 +00:00
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:
parent
fba5b7fed4
commit
c6bd00e778
24 changed files with 970 additions and 59 deletions
41
.github/actions/bitcoin/action.yml
vendored
Normal file
41
.github/actions/bitcoin/action.yml
vendored
Normal 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
|
10
.github/actions/test-dependencies/action.yml
vendored
10
.github/actions/test-dependencies/action.yml
vendored
|
@ -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
55
Cargo.lock
generated
|
@ -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
29
coins/bitcoin/Cargo.toml
Normal 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
15
coins/bitcoin/LICENSE
Normal 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/>.
|
58
coins/bitcoin/src/crypto.rs
Normal file
58
coins/bitcoin/src/crypto.rs
Normal 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
6
coins/bitcoin/src/lib.rs
Normal 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
80
coins/bitcoin/src/rpc.rs
Normal 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
|
||||
}
|
||||
}
|
47
coins/bitcoin/src/tests/mod.rs
Normal file
47
coins/bitcoin/src/tests/mod.rs
Normal 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
295
coins/bitcoin/src/wallet.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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" },
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -152,6 +152,8 @@ services:
|
|||
volumes:
|
||||
- "./coins/bitcoin/scripts:/scripts"
|
||||
entrypoint: /scripts/entry-dev.sh
|
||||
ports:
|
||||
- "18443:18443"
|
||||
|
||||
ethereum:
|
||||
profiles:
|
||||
|
|
|
@ -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"] }
|
||||
|
|
283
processor/src/coin/bitcoin.rs
Normal file
283
processor/src/coin/bitcoin.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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)]
|
||||
|
|
12
processor/src/tests/bitcoin.rs
Normal file
12
processor/src/tests/bitcoin.rs
Normal 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;
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
mod send;
|
||||
pub(crate) use send::test_send;
|
||||
|
||||
mod bitcoin;
|
||||
mod monero;
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue