mirror of
https://github.com/serai-dex/serai.git
synced 2025-01-18 16:55:16 +00:00
Merge branch 'bitcoin-audit' into develop
This commit is contained in:
commit
d2a0ff13f2
6 changed files with 185 additions and 52 deletions
|
@ -32,13 +32,14 @@ pub fn x(key: &ProjectivePoint) -> [u8; 32] {
|
||||||
(*encoded.x().expect("point at infinity")).into()
|
(*encoded.x().expect("point at infinity")).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a non-infinite even point to a XOnlyPublicKey. Panics on invalid input.
|
/// Convert a non-infinity even point to a XOnlyPublicKey. Panics on invalid input.
|
||||||
pub fn x_only(key: &ProjectivePoint) -> XOnlyPublicKey {
|
pub fn x_only(key: &ProjectivePoint) -> XOnlyPublicKey {
|
||||||
XOnlyPublicKey::from_slice(&x(key)).unwrap()
|
XOnlyPublicKey::from_slice(&x(key)).expect("x_only was passed a point which was infinity or odd")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Make a point even by adding the generator until it is even. Returns the even point and the
|
/// Make a point even by adding the generator until it is even.
|
||||||
/// amount of additions required.
|
///
|
||||||
|
/// Returns the even point and the amount of additions required.
|
||||||
pub fn make_even(mut key: ProjectivePoint) -> (ProjectivePoint, u64) {
|
pub fn make_even(mut key: ProjectivePoint) -> (ProjectivePoint, u64) {
|
||||||
let mut c = 0;
|
let mut c = 0;
|
||||||
while key.to_encoded_point(true).tag() == Tag::CompressedOddY {
|
while key.to_encoded_point(true).tag() == Tag::CompressedOddY {
|
||||||
|
@ -51,8 +52,10 @@ pub fn make_even(mut key: ProjectivePoint) -> (ProjectivePoint, u64) {
|
||||||
/// A BIP-340 compatible HRAm for use with the modular-frost Schnorr Algorithm.
|
/// A BIP-340 compatible HRAm for use with the modular-frost Schnorr Algorithm.
|
||||||
///
|
///
|
||||||
/// If passed an odd nonce, it will have the generator added until it is even.
|
/// If passed an odd nonce, it will have the generator added until it is even.
|
||||||
|
///
|
||||||
|
/// If the key is odd, this will panic.
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct Hram {}
|
pub struct Hram;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref TAG_HASH: [u8; 32] = Sha256::digest(b"BIP0340/challenge").into();
|
static ref TAG_HASH: [u8; 32] = Sha256::digest(b"BIP0340/challenge").into();
|
||||||
|
@ -145,7 +148,8 @@ impl<T: Sync + Clone + Debug + Transcript> Algorithm<Secp256k1> for Schnorr<T> {
|
||||||
// s = r + cx. Since we added to the r, add to s
|
// s = r + cx. Since we added to the r, add to s
|
||||||
sig.s += Scalar::from(offset);
|
sig.s += Scalar::from(offset);
|
||||||
// Convert to a secp256k1 signature
|
// Convert to a secp256k1 signature
|
||||||
Signature::from_slice(&sig.serialize()[1 ..]).unwrap()
|
Signature::from_slice(&sig.serialize()[1 ..])
|
||||||
|
.expect("couldn't convert SchnorrSignature to Signature")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
use core::fmt::Debug;
|
use core::fmt::Debug;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use serde::{Deserialize, de::DeserializeOwned};
|
use serde::{Deserialize, de::DeserializeOwned};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
use reqwest::Client;
|
||||||
|
|
||||||
use bitcoin::{
|
use bitcoin::{
|
||||||
hashes::{Hash, hex::FromHex},
|
hashes::{Hash, hex::FromHex},
|
||||||
consensus::encode,
|
consensus::encode,
|
||||||
|
@ -26,7 +29,10 @@ enum RpcResponse<T> {
|
||||||
|
|
||||||
/// A minimal asynchronous Bitcoin RPC client.
|
/// A minimal asynchronous Bitcoin RPC client.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Rpc(String);
|
pub struct Rpc {
|
||||||
|
client: Client,
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Error)]
|
#[derive(Clone, PartialEq, Eq, Debug, Error)]
|
||||||
pub enum RpcError {
|
pub enum RpcError {
|
||||||
|
@ -34,15 +40,64 @@ pub enum RpcError {
|
||||||
ConnectionError,
|
ConnectionError,
|
||||||
#[error("request had an error: {0:?}")]
|
#[error("request had an error: {0:?}")]
|
||||||
RequestError(Error),
|
RequestError(Error),
|
||||||
#[error("node sent an invalid response")]
|
#[error("node replied with invalid JSON")]
|
||||||
InvalidResponse,
|
InvalidJson(serde_json::error::Category),
|
||||||
|
#[error("node sent an invalid response ({0})")]
|
||||||
|
InvalidResponse(&'static str),
|
||||||
|
#[error("node was missing expected methods")]
|
||||||
|
MissingMethods(HashSet<&'static str>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Rpc {
|
impl Rpc {
|
||||||
|
/// Create a new connection to a Bitcoin RPC.
|
||||||
|
///
|
||||||
|
/// An RPC call is performed to ensure the node is reachable (and that an invalid URL wasn't
|
||||||
|
/// provided).
|
||||||
|
///
|
||||||
|
/// Additionally, a set of expected methods is checked to be offered by the Bitcoin RPC. If these
|
||||||
|
/// methods aren't provided, an error with the missing methods is returned. This ensures all RPC
|
||||||
|
/// routes explicitly provided by this library are at least possible.
|
||||||
|
///
|
||||||
|
/// Each individual RPC route may still fail at time-of-call, regardless of the arguments
|
||||||
|
/// provided to this library, if the RPC has an incompatible argument layout. That is not checked
|
||||||
|
/// at time of RPC creation.
|
||||||
pub async fn new(url: String) -> Result<Rpc, RpcError> {
|
pub async fn new(url: String) -> Result<Rpc, RpcError> {
|
||||||
let rpc = Rpc(url);
|
let rpc = Rpc { client: Client::new(), url };
|
||||||
|
|
||||||
// Make an RPC request to verify the node is reachable and sane
|
// Make an RPC request to verify the node is reachable and sane
|
||||||
rpc.get_latest_block_number().await?;
|
let res: String = rpc.rpc_call("help", json!([])).await?;
|
||||||
|
|
||||||
|
// Verify all methods we expect are present
|
||||||
|
// If we had a more expanded RPC, due to differences in RPC versions, it wouldn't make sense to
|
||||||
|
// error if all methods weren't present
|
||||||
|
// We only provide a very minimal set of methods which have been largely consistent, hence why
|
||||||
|
// this is sane
|
||||||
|
let mut expected_methods = HashSet::from([
|
||||||
|
"help",
|
||||||
|
"getblockcount",
|
||||||
|
"getblockhash",
|
||||||
|
"getblockheader",
|
||||||
|
"getblock",
|
||||||
|
"sendrawtransaction",
|
||||||
|
"getrawtransaction",
|
||||||
|
]);
|
||||||
|
for line in res.split('\n') {
|
||||||
|
// This doesn't check if the arguments are as expected
|
||||||
|
// This is due to Bitcoin supporting a large amount of optional arguments, which
|
||||||
|
// occassionally change, with their own mechanism of text documentation, making matching off
|
||||||
|
// it a quite involved task
|
||||||
|
// Instead, once we've confirmed the methods are present, we assume our arguments are aligned
|
||||||
|
// Else we'll error at time of call
|
||||||
|
if expected_methods.remove(line.split(' ').next().unwrap_or("")) &&
|
||||||
|
expected_methods.is_empty()
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !expected_methods.is_empty() {
|
||||||
|
Err(RpcError::MissingMethods(expected_methods))?;
|
||||||
|
};
|
||||||
|
|
||||||
Ok(rpc)
|
Ok(rpc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,9 +107,9 @@ impl Rpc {
|
||||||
method: &str,
|
method: &str,
|
||||||
params: serde_json::Value,
|
params: serde_json::Value,
|
||||||
) -> Result<Response, RpcError> {
|
) -> Result<Response, RpcError> {
|
||||||
let client = reqwest::Client::new();
|
let res = self
|
||||||
let res = client
|
.client
|
||||||
.post(&self.0)
|
.post(&self.url)
|
||||||
.json(&json!({ "jsonrpc": "2.0", "method": method, "params": params }))
|
.json(&json!({ "jsonrpc": "2.0", "method": method, "params": params }))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
|
@ -64,7 +119,7 @@ impl Rpc {
|
||||||
.map_err(|_| RpcError::ConnectionError)?;
|
.map_err(|_| RpcError::ConnectionError)?;
|
||||||
|
|
||||||
let res: RpcResponse<Response> =
|
let res: RpcResponse<Response> =
|
||||||
serde_json::from_str(&res).map_err(|_| RpcError::InvalidResponse)?;
|
serde_json::from_str(&res).map_err(|e| RpcError::InvalidJson(e.classify()))?;
|
||||||
match res {
|
match res {
|
||||||
RpcResponse::Ok { result } => Ok(result),
|
RpcResponse::Ok { result } => Ok(result),
|
||||||
RpcResponse::Err { error } => Err(RpcError::RequestError(error)),
|
RpcResponse::Err { error } => Err(RpcError::RequestError(error)),
|
||||||
|
@ -107,13 +162,15 @@ impl Rpc {
|
||||||
/// Get a block by its hash.
|
/// Get a block by its hash.
|
||||||
pub async fn get_block(&self, hash: &[u8; 32]) -> Result<Block, RpcError> {
|
pub async fn get_block(&self, hash: &[u8; 32]) -> Result<Block, RpcError> {
|
||||||
let hex = self.rpc_call::<String>("getblock", json!([hex::encode(hash), 0])).await?;
|
let hex = self.rpc_call::<String>("getblock", json!([hex::encode(hash), 0])).await?;
|
||||||
let bytes: Vec<u8> = FromHex::from_hex(&hex).map_err(|_| RpcError::InvalidResponse)?;
|
let bytes: Vec<u8> = FromHex::from_hex(&hex)
|
||||||
let block: Block = encode::deserialize(&bytes).map_err(|_| RpcError::InvalidResponse)?;
|
.map_err(|_| RpcError::InvalidResponse("node didn't use hex to encode the block"))?;
|
||||||
|
let block: Block = encode::deserialize(&bytes)
|
||||||
|
.map_err(|_| RpcError::InvalidResponse("node sent an improperly serialized block"))?;
|
||||||
|
|
||||||
let mut block_hash = *block.block_hash().as_raw_hash().as_byte_array();
|
let mut block_hash = *block.block_hash().as_raw_hash().as_byte_array();
|
||||||
block_hash.reverse();
|
block_hash.reverse();
|
||||||
if hash != &block_hash {
|
if hash != &block_hash {
|
||||||
Err(RpcError::InvalidResponse)?;
|
Err(RpcError::InvalidResponse("node replied with a different block"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(block)
|
Ok(block)
|
||||||
|
@ -123,7 +180,7 @@ impl Rpc {
|
||||||
pub async fn send_raw_transaction(&self, tx: &Transaction) -> Result<Txid, RpcError> {
|
pub async fn send_raw_transaction(&self, tx: &Transaction) -> Result<Txid, RpcError> {
|
||||||
let txid = self.rpc_call("sendrawtransaction", json!([encode::serialize_hex(tx)])).await?;
|
let txid = self.rpc_call("sendrawtransaction", json!([encode::serialize_hex(tx)])).await?;
|
||||||
if txid != tx.txid() {
|
if txid != tx.txid() {
|
||||||
Err(RpcError::InvalidResponse)?;
|
Err(RpcError::InvalidResponse("returned TX ID inequals calculated TX ID"))?;
|
||||||
}
|
}
|
||||||
Ok(txid)
|
Ok(txid)
|
||||||
}
|
}
|
||||||
|
@ -131,13 +188,15 @@ impl Rpc {
|
||||||
/// Get a transaction by its hash.
|
/// Get a transaction by its hash.
|
||||||
pub async fn get_transaction(&self, hash: &[u8; 32]) -> Result<Transaction, RpcError> {
|
pub async fn get_transaction(&self, hash: &[u8; 32]) -> Result<Transaction, RpcError> {
|
||||||
let hex = self.rpc_call::<String>("getrawtransaction", json!([hex::encode(hash)])).await?;
|
let hex = self.rpc_call::<String>("getrawtransaction", json!([hex::encode(hash)])).await?;
|
||||||
let bytes: Vec<u8> = FromHex::from_hex(&hex).map_err(|_| RpcError::InvalidResponse)?;
|
let bytes: Vec<u8> = FromHex::from_hex(&hex)
|
||||||
let tx: Transaction = encode::deserialize(&bytes).map_err(|_| RpcError::InvalidResponse)?;
|
.map_err(|_| RpcError::InvalidResponse("node didn't use hex to encode the transaction"))?;
|
||||||
|
let tx: Transaction = encode::deserialize(&bytes)
|
||||||
|
.map_err(|_| RpcError::InvalidResponse("node sent an improperly serialized transaction"))?;
|
||||||
|
|
||||||
let mut tx_hash = *tx.txid().as_raw_hash().as_byte_array();
|
let mut tx_hash = *tx.txid().as_raw_hash().as_byte_array();
|
||||||
tx_hash.reverse();
|
tx_hash.reverse();
|
||||||
if hash != &tx_hash {
|
if hash != &tx_hash {
|
||||||
Err(RpcError::InvalidResponse)?;
|
Err(RpcError::InvalidResponse("node replied with a different transaction"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(tx)
|
Ok(tx)
|
||||||
|
|
|
@ -15,7 +15,8 @@ use frost::{
|
||||||
use bitcoin::{
|
use bitcoin::{
|
||||||
consensus::encode::{Decodable, serialize},
|
consensus::encode::{Decodable, serialize},
|
||||||
key::TweakedPublicKey,
|
key::TweakedPublicKey,
|
||||||
OutPoint, ScriptBuf, TxOut, Transaction, Block, Network, Address,
|
address::Payload,
|
||||||
|
OutPoint, ScriptBuf, TxOut, Transaction, Block,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::crypto::{x_only, make_even};
|
use crate::crypto::{x_only, make_even};
|
||||||
|
@ -24,18 +25,23 @@ mod send;
|
||||||
pub use send::*;
|
pub use send::*;
|
||||||
|
|
||||||
/// Tweak keys to ensure they're usable with Bitcoin.
|
/// Tweak keys to ensure they're usable with Bitcoin.
|
||||||
|
///
|
||||||
|
/// Taproot keys, which these keys are used as, must be even. This offsets the keys until they're
|
||||||
|
/// even.
|
||||||
pub fn tweak_keys(keys: &ThresholdKeys<Secp256k1>) -> ThresholdKeys<Secp256k1> {
|
pub fn tweak_keys(keys: &ThresholdKeys<Secp256k1>) -> ThresholdKeys<Secp256k1> {
|
||||||
let (_, offset) = make_even(keys.group_key());
|
let (_, offset) = make_even(keys.group_key());
|
||||||
keys.offset(Scalar::from(offset))
|
keys.offset(Scalar::from(offset))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the Taproot address for a public key.
|
/// Return the Taproot address payload for a public key.
|
||||||
pub fn address(network: Network, key: ProjectivePoint) -> Option<Address> {
|
///
|
||||||
|
/// If the key is odd, this will return None.
|
||||||
|
pub fn address_payload(key: ProjectivePoint) -> Option<Payload> {
|
||||||
if key.to_encoded_point(true).tag() != Tag::CompressedEvenY {
|
if key.to_encoded_point(true).tag() != Tag::CompressedEvenY {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(Address::p2tr_tweaked(TweakedPublicKey::dangerous_assume_tweaked(x_only(&key)), network))
|
Some(Payload::p2tr_tweaked(TweakedPublicKey::dangerous_assume_tweaked(x_only(&key))))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A spendable output.
|
/// A spendable output.
|
||||||
|
@ -104,8 +110,7 @@ impl Scanner {
|
||||||
/// Returns None if this key can't be scanned for.
|
/// Returns None if this key can't be scanned for.
|
||||||
pub fn new(key: ProjectivePoint) -> Option<Scanner> {
|
pub fn new(key: ProjectivePoint) -> Option<Scanner> {
|
||||||
let mut scripts = HashMap::new();
|
let mut scripts = HashMap::new();
|
||||||
// Uses Network::Bitcoin since network is irrelevant here
|
scripts.insert(address_payload(key)?.script_pubkey(), Scalar::ZERO);
|
||||||
scripts.insert(address(Network::Bitcoin, key)?.script_pubkey(), Scalar::ZERO);
|
|
||||||
Some(Scanner { key, scripts })
|
Some(Scanner { key, scripts })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,9 +119,15 @@ impl Scanner {
|
||||||
/// Due to Bitcoin's requirement that points are even, not every offset may be used.
|
/// Due to Bitcoin's requirement that points are even, not every offset may be used.
|
||||||
/// If an offset isn't usable, it will be incremented until it is. If this offset is already
|
/// If an offset isn't usable, it will be incremented until it is. If this offset is already
|
||||||
/// present, None is returned. Else, Some(offset) will be, with the used offset.
|
/// present, None is returned. Else, Some(offset) will be, with the used offset.
|
||||||
|
///
|
||||||
|
/// This means offsets are surjective, not bijective, and the order offsets are registered in
|
||||||
|
/// may determine the validity of future offsets.
|
||||||
pub fn register_offset(&mut self, mut offset: Scalar) -> Option<Scalar> {
|
pub fn register_offset(&mut self, mut offset: Scalar) -> Option<Scalar> {
|
||||||
|
// This loop will terminate as soon as an even point is found, with any point having a ~50%
|
||||||
|
// chance of being even
|
||||||
|
// That means this should terminate within a very small amount of iterations
|
||||||
loop {
|
loop {
|
||||||
match address(Network::Bitcoin, self.key + (ProjectivePoint::GENERATOR * offset)) {
|
match address_payload(self.key + (ProjectivePoint::GENERATOR * offset)) {
|
||||||
Some(address) => {
|
Some(address) => {
|
||||||
let script = address.script_pubkey();
|
let script = address.script_pubkey();
|
||||||
if self.scripts.contains_key(&script) {
|
if self.scripts.contains_key(&script) {
|
||||||
|
@ -134,11 +145,16 @@ impl Scanner {
|
||||||
pub fn scan_transaction(&self, tx: &Transaction) -> Vec<ReceivedOutput> {
|
pub fn scan_transaction(&self, tx: &Transaction) -> Vec<ReceivedOutput> {
|
||||||
let mut res = vec![];
|
let mut res = vec![];
|
||||||
for (vout, output) in tx.output.iter().enumerate() {
|
for (vout, output) in tx.output.iter().enumerate() {
|
||||||
|
// If the vout index exceeds 2**32, stop scanning outputs
|
||||||
|
let Ok(vout) = u32::try_from(vout) else {
|
||||||
|
break
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(offset) = self.scripts.get(&output.script_pubkey) {
|
if let Some(offset) = self.scripts.get(&output.script_pubkey) {
|
||||||
res.push(ReceivedOutput {
|
res.push(ReceivedOutput {
|
||||||
offset: *offset,
|
offset: *offset,
|
||||||
output: output.clone(),
|
output: output.clone(),
|
||||||
outpoint: OutPoint::new(tx.txid(), u32::try_from(vout).unwrap()),
|
outpoint: OutPoint::new(tx.txid(), vout),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,12 +16,12 @@ use bitcoin::{
|
||||||
sighash::{TapSighashType, SighashCache, Prevouts},
|
sighash::{TapSighashType, SighashCache, Prevouts},
|
||||||
absolute::LockTime,
|
absolute::LockTime,
|
||||||
script::{PushBytesBuf, ScriptBuf},
|
script::{PushBytesBuf, ScriptBuf},
|
||||||
OutPoint, Sequence, Witness, TxIn, TxOut, Transaction, Network, Address,
|
OutPoint, Sequence, Witness, TxIn, TxOut, Transaction, Address,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
crypto::Schnorr,
|
crypto::Schnorr,
|
||||||
wallet::{address, ReceivedOutput},
|
wallet::{ReceivedOutput, address_payload},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
|
@ -29,8 +29,22 @@ use crate::{
|
||||||
const MAX_STANDARD_TX_WEIGHT: u64 = 400_000;
|
const MAX_STANDARD_TX_WEIGHT: u64 = 400_000;
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
//https://github.com/bitcoin/bitcoin/blob/a245429d680eb95cf4c0c78e58e63e3f0f5d979a/src/test/transaction_tests.cpp#L815-L816
|
// https://github.com/bitcoin/bitcoin/blob/306ccd4927a2efe325c8d84be1bdb79edeb29b04/src/policy/policy.cpp#L26-L63
|
||||||
const DUST: u64 = 674;
|
// As the above notes, a lower amount may not be considered dust if contained in a SegWit output
|
||||||
|
// This doesn't bother with delineation due to how marginal these values are, and because it isn't
|
||||||
|
// worth the complexity to implement differentation
|
||||||
|
const DUST: u64 = 546;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
// The constant is from:
|
||||||
|
// https://github.com/bitcoin/bitcoin/blob/306ccd4927a2efe325c8d84be1bdb79edeb29b04/src/policy/policy.h#L56-L57
|
||||||
|
// It's used here:
|
||||||
|
// https://github.com/bitcoin/bitcoin/blob/296735f7638749906243c9e203df7bd024493806/src/net_processing.cpp#L5386-L5390
|
||||||
|
// Peers won't relay TXs below the filter's fee rate, yet they calculate the fee not against weight yet vsize
|
||||||
|
// https://github.com/bitcoin/bitcoin/blob/296735f7638749906243c9e203df7bd024493806/src/net_processing.cpp#L5721-L5732
|
||||||
|
// And then the fee itself is fee per thousand units, not fee per unit
|
||||||
|
// https://github.com/bitcoin/bitcoin/blob/306ccd4927a2efe325c8d84be1bdb79edeb29b04/src/policy/feerate.cpp#L23-L37
|
||||||
|
const MIN_FEE_PER_KILO_VSIZE: u64 = 1000;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Error)]
|
#[derive(Clone, PartialEq, Eq, Debug, Error)]
|
||||||
pub enum TransactionError {
|
pub enum TransactionError {
|
||||||
|
@ -42,6 +56,8 @@ pub enum TransactionError {
|
||||||
DustPayment,
|
DustPayment,
|
||||||
#[error("too much data was specified")]
|
#[error("too much data was specified")]
|
||||||
TooMuchData,
|
TooMuchData,
|
||||||
|
#[error("fee was too low to pass the default minimum fee rate")]
|
||||||
|
TooLowFee,
|
||||||
#[error("not enough funds for these payments")]
|
#[error("not enough funds for these payments")]
|
||||||
NotEnoughFunds,
|
NotEnoughFunds,
|
||||||
#[error("transaction was too large")]
|
#[error("transaction was too large")]
|
||||||
|
@ -163,6 +179,26 @@ impl SignableTransaction {
|
||||||
|
|
||||||
let mut weight = Self::calculate_weight(tx_ins.len(), payments, None);
|
let mut weight = Self::calculate_weight(tx_ins.len(), payments, None);
|
||||||
let mut needed_fee = fee_per_weight * weight;
|
let mut needed_fee = fee_per_weight * weight;
|
||||||
|
|
||||||
|
// "Virtual transaction size" is weight ceildiv 4 per
|
||||||
|
// https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki
|
||||||
|
|
||||||
|
// https://github.com/bitcoin/bitcoin/blob/306ccd4927a2efe325c8d84be1bdb79edeb29b04/
|
||||||
|
// src/policy/policy.cpp#L295-L298
|
||||||
|
// implements this as expected
|
||||||
|
|
||||||
|
// Technically, it takes whatever's greater, the weight or the amount of signature operatons
|
||||||
|
// multiplied by DEFAULT_BYTES_PER_SIGOP (20)
|
||||||
|
// We only use 1 signature per input, and our inputs have a weight exceeding 20
|
||||||
|
// Accordingly, our inputs' weight will always be greater than the cost of the signature ops
|
||||||
|
let vsize = (weight + 3) / 4;
|
||||||
|
// Technically, if there isn't change, this TX may still pay enough of a fee to pass the
|
||||||
|
// minimum fee. Such edge cases aren't worth programming when they go against intent, as the
|
||||||
|
// specified fee rate is too low to be valid
|
||||||
|
if needed_fee < ((MIN_FEE_PER_KILO_VSIZE * vsize) / 1000) {
|
||||||
|
Err(TransactionError::TooLowFee)?;
|
||||||
|
}
|
||||||
|
|
||||||
if input_sat < (payment_sat + needed_fee) {
|
if input_sat < (payment_sat + needed_fee) {
|
||||||
Err(TransactionError::NotEnoughFunds)?;
|
Err(TransactionError::NotEnoughFunds)?;
|
||||||
}
|
}
|
||||||
|
@ -221,12 +257,12 @@ impl SignableTransaction {
|
||||||
let mut sigs = vec![];
|
let mut sigs = vec![];
|
||||||
for i in 0 .. tx.input.len() {
|
for i in 0 .. tx.input.len() {
|
||||||
let mut transcript = transcript.clone();
|
let mut transcript = transcript.clone();
|
||||||
|
// This unwrap is safe since any transaction with this many inputs violates the maximum
|
||||||
|
// size allowed under standards, which this lib will error on creation of
|
||||||
transcript.append_message(b"signing_input", u32::try_from(i).unwrap().to_le_bytes());
|
transcript.append_message(b"signing_input", u32::try_from(i).unwrap().to_le_bytes());
|
||||||
|
|
||||||
let offset = keys.clone().offset(self.offsets[i]);
|
let offset = keys.clone().offset(self.offsets[i]);
|
||||||
if address(Network::Bitcoin, offset.group_key())?.script_pubkey() !=
|
if address_payload(offset.group_key())?.script_pubkey() != self.prevouts[i].script_pubkey {
|
||||||
self.prevouts[i].script_pubkey
|
|
||||||
{
|
|
||||||
None?;
|
None?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,7 +279,7 @@ impl SignableTransaction {
|
||||||
/// A FROST signing machine to produce a Bitcoin transaction.
|
/// A FROST signing machine to produce a Bitcoin transaction.
|
||||||
///
|
///
|
||||||
/// This does not support caching its preprocess. When sign is called, the message must be empty.
|
/// This does not support caching its preprocess. When sign is called, the message must be empty.
|
||||||
/// This will panic if it isn't.
|
/// This will panic if either `cache` is called or the message isn't empty.
|
||||||
pub struct TransactionMachine {
|
pub struct TransactionMachine {
|
||||||
tx: SignableTransaction,
|
tx: SignableTransaction,
|
||||||
sigs: Vec<AlgorithmMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
|
sigs: Vec<AlgorithmMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
|
||||||
|
@ -339,7 +375,9 @@ impl SignMachine<Transaction> for TransactionSignMachine {
|
||||||
commitments[i].clone(),
|
commitments[i].clone(),
|
||||||
cache
|
cache
|
||||||
.taproot_key_spend_signature_hash(i, &prevouts, TapSighashType::Default)
|
.taproot_key_spend_signature_hash(i, &prevouts, TapSighashType::Default)
|
||||||
.unwrap()
|
// This should never happen since the inputs align with the TX the cache was
|
||||||
|
// constructed with, and because i is always < prevouts.len()
|
||||||
|
.expect("taproot_key_spend_signature_hash failed to return a hash")
|
||||||
.as_ref(),
|
.as_ref(),
|
||||||
)?;
|
)?;
|
||||||
shares.push(share);
|
shares.push(share);
|
||||||
|
|
|
@ -22,9 +22,12 @@ use bitcoin_serai::{
|
||||||
hashes::Hash as HashTrait,
|
hashes::Hash as HashTrait,
|
||||||
blockdata::opcodes::all::OP_RETURN,
|
blockdata::opcodes::all::OP_RETURN,
|
||||||
script::{PushBytesBuf, Instruction, Instructions, Script},
|
script::{PushBytesBuf, Instruction, Instructions, Script},
|
||||||
|
address::NetworkChecked,
|
||||||
OutPoint, TxOut, Transaction, Network, Address,
|
OutPoint, TxOut, Transaction, Network, Address,
|
||||||
},
|
},
|
||||||
wallet::{tweak_keys, address, ReceivedOutput, Scanner, TransactionError, SignableTransaction},
|
wallet::{
|
||||||
|
tweak_keys, address_payload, ReceivedOutput, Scanner, TransactionError, SignableTransaction,
|
||||||
|
},
|
||||||
rpc::Rpc,
|
rpc::Rpc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -43,7 +46,10 @@ async fn send_and_get_output(rpc: &Rpc, scanner: &Scanner, key: ProjectivePoint)
|
||||||
rpc
|
rpc
|
||||||
.rpc_call::<Vec<String>>(
|
.rpc_call::<Vec<String>>(
|
||||||
"generatetoaddress",
|
"generatetoaddress",
|
||||||
serde_json::json!([1, address(Network::Regtest, key).unwrap()]),
|
serde_json::json!([
|
||||||
|
1,
|
||||||
|
Address::<NetworkChecked>::new(Network::Regtest, address_payload(key).unwrap())
|
||||||
|
]),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -187,7 +193,7 @@ async_sequential! {
|
||||||
assert_eq!(output.offset(), Scalar::ZERO);
|
assert_eq!(output.offset(), Scalar::ZERO);
|
||||||
|
|
||||||
let inputs = vec![output];
|
let inputs = vec![output];
|
||||||
let addr = || address(Network::Regtest, key).unwrap();
|
let addr = || Address::<NetworkChecked>::new(Network::Regtest, address_payload(key).unwrap());
|
||||||
let payments = vec![(addr(), 1000)];
|
let payments = vec![(addr(), 1000)];
|
||||||
|
|
||||||
assert!(SignableTransaction::new(inputs.clone(), &payments, None, None, FEE).is_ok());
|
assert!(SignableTransaction::new(inputs.clone(), &payments, None, None, FEE).is_ok());
|
||||||
|
@ -222,13 +228,18 @@ async_sequential! {
|
||||||
Err(TransactionError::TooMuchData),
|
Err(TransactionError::TooMuchData),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
SignableTransaction::new(inputs.clone(), &[], Some(addr()), None, 0),
|
||||||
|
Err(TransactionError::TooLowFee),
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
SignableTransaction::new(inputs.clone(), &[(addr(), inputs[0].value() * 2)], None, None, FEE),
|
SignableTransaction::new(inputs.clone(), &[(addr(), inputs[0].value() * 2)], None, None, FEE),
|
||||||
Err(TransactionError::NotEnoughFunds),
|
Err(TransactionError::NotEnoughFunds),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
SignableTransaction::new(inputs, &vec![(addr(), 1000); 10000], None, None, 0),
|
SignableTransaction::new(inputs, &vec![(addr(), 1000); 10000], None, None, FEE),
|
||||||
Err(TransactionError::TooLargeTransaction),
|
Err(TransactionError::TooLargeTransaction),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -250,13 +261,14 @@ async_sequential! {
|
||||||
|
|
||||||
// Declare payments, change, fee
|
// Declare payments, change, fee
|
||||||
let payments = [
|
let payments = [
|
||||||
(address(Network::Regtest, key).unwrap(), 1005),
|
(Address::<NetworkChecked>::new(Network::Regtest, address_payload(key).unwrap()), 1005),
|
||||||
(address(Network::Regtest, offset_key).unwrap(), 1007)
|
(Address::<NetworkChecked>::new(Network::Regtest, address_payload(offset_key).unwrap()), 1007)
|
||||||
];
|
];
|
||||||
|
|
||||||
let change_offset = scanner.register_offset(Scalar::random(&mut OsRng)).unwrap();
|
let change_offset = scanner.register_offset(Scalar::random(&mut OsRng)).unwrap();
|
||||||
let change_key = key + (ProjectivePoint::GENERATOR * change_offset);
|
let change_key = key + (ProjectivePoint::GENERATOR * change_offset);
|
||||||
let change_addr = address(Network::Regtest, change_key).unwrap();
|
let change_addr =
|
||||||
|
Address::<NetworkChecked>::new(Network::Regtest, address_payload(change_key).unwrap());
|
||||||
|
|
||||||
// Create and sign the TX
|
// Create and sign the TX
|
||||||
let tx = SignableTransaction::new(
|
let tx = SignableTransaction::new(
|
||||||
|
@ -327,7 +339,7 @@ async_sequential! {
|
||||||
SignableTransaction::new(
|
SignableTransaction::new(
|
||||||
vec![output],
|
vec![output],
|
||||||
&[],
|
&[],
|
||||||
address(Network::Regtest, key),
|
Some(Address::<NetworkChecked>::new(Network::Regtest, address_payload(key).unwrap())),
|
||||||
Some(data.clone()),
|
Some(data.clone()),
|
||||||
FEE
|
FEE
|
||||||
).unwrap()
|
).unwrap()
|
||||||
|
|
|
@ -17,10 +17,11 @@ use bitcoin_serai::{
|
||||||
hashes::Hash as HashTrait,
|
hashes::Hash as HashTrait,
|
||||||
consensus::{Encodable, Decodable},
|
consensus::{Encodable, Decodable},
|
||||||
script::Instruction,
|
script::Instruction,
|
||||||
|
address::{NetworkChecked, Address as BAddress},
|
||||||
OutPoint, Transaction, Block, Network as BitcoinNetwork,
|
OutPoint, Transaction, Block, Network as BitcoinNetwork,
|
||||||
},
|
},
|
||||||
wallet::{
|
wallet::{
|
||||||
tweak_keys, address, ReceivedOutput, Scanner, TransactionError,
|
tweak_keys, address_payload, ReceivedOutput, Scanner, TransactionError,
|
||||||
SignableTransaction as BSignableTransaction, TransactionMachine,
|
SignableTransaction as BSignableTransaction, TransactionMachine,
|
||||||
},
|
},
|
||||||
rpc::{RpcError, Rpc},
|
rpc::{RpcError, Rpc},
|
||||||
|
@ -33,7 +34,7 @@ use bitcoin_serai::bitcoin::{
|
||||||
sighash::{EcdsaSighashType, SighashCache},
|
sighash::{EcdsaSighashType, SighashCache},
|
||||||
script::{PushBytesBuf, Builder},
|
script::{PushBytesBuf, Builder},
|
||||||
absolute::LockTime,
|
absolute::LockTime,
|
||||||
Sequence, Script, Witness, TxIn, TxOut, Address as BAddress,
|
Sequence, Script, Witness, TxIn, TxOut,
|
||||||
};
|
};
|
||||||
|
|
||||||
use serai_client::{
|
use serai_client::{
|
||||||
|
@ -326,7 +327,7 @@ impl Network for Bitcoin {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn address(key: ProjectivePoint) -> Address {
|
fn address(key: ProjectivePoint) -> Address {
|
||||||
Address(address(BitcoinNetwork::Bitcoin, key).unwrap())
|
Address(BAddress::<NetworkChecked>::new(BitcoinNetwork::Bitcoin, address_payload(key).unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn branch_address(key: ProjectivePoint) -> Self::Address {
|
fn branch_address(key: ProjectivePoint) -> Self::Address {
|
||||||
|
@ -482,7 +483,12 @@ impl Network for Bitcoin {
|
||||||
}
|
}
|
||||||
// No outputs left and the change isn't worth enough
|
// No outputs left and the change isn't worth enough
|
||||||
Err(TransactionError::NoOutputs) => None,
|
Err(TransactionError::NoOutputs) => None,
|
||||||
|
// amortize_fee removes payments which fall below the dust threshold
|
||||||
|
Err(TransactionError::DustPayment) => panic!("dust payment despite removing dust"),
|
||||||
Err(TransactionError::TooMuchData) => panic!("too much data despite not specifying data"),
|
Err(TransactionError::TooMuchData) => panic!("too much data despite not specifying data"),
|
||||||
|
Err(TransactionError::TooLowFee) => {
|
||||||
|
panic!("created a transaction whose fee is below the minimum")
|
||||||
|
}
|
||||||
Err(TransactionError::NotEnoughFunds) => {
|
Err(TransactionError::NotEnoughFunds) => {
|
||||||
if tx_fee.is_none() {
|
if tx_fee.is_none() {
|
||||||
// Mot even enough funds to pay the fee
|
// Mot even enough funds to pay the fee
|
||||||
|
@ -491,8 +497,6 @@ impl Network for Bitcoin {
|
||||||
panic!("not enough funds for bitcoin TX despite amortizing the fee")
|
panic!("not enough funds for bitcoin TX despite amortizing the fee")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// amortize_fee removes payments which fall below the dust threshold
|
|
||||||
Err(TransactionError::DustPayment) => panic!("dust payment despite removing dust"),
|
|
||||||
Err(TransactionError::TooLargeTransaction) => {
|
Err(TransactionError::TooLargeTransaction) => {
|
||||||
panic!("created a too large transaction despite limiting inputs/outputs")
|
panic!("created a too large transaction despite limiting inputs/outputs")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue