Skip to content

Commit

Permalink
[Bitcoin/Rust] Calculate transaction fees (#3317)
Browse files Browse the repository at this point in the history
  • Loading branch information
lamafab committed Jul 28, 2023
1 parent 67b1c29 commit 45c0ff5
Show file tree
Hide file tree
Showing 17 changed files with 386 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.trustwallet.core.app.blockchains.bitcoin

import com.trustwallet.core.app.utils.Numeric
import com.trustwallet.core.app.utils.toHexBytes
import org.junit.Assert.assertEquals
import org.junit.Test
import wallet.core.jni.BitcoinFee

class TestBitcoinFee {

init {
System.loadLibrary("TrustWalletCore")
}

@Test
fun P2pkhCalculateFee() {
val satVb: Long = 10
val tx = Numeric.hexStringToByteArray("02000000017be4e642bb278018ab12277de9427773ad1c5f5b1d164a157e0d99aa48dc1c1e000000006a473044022078eda020d4b86fcb3af78ef919912e6d79b81164dbbb0b0b96da6ac58a2de4b102201a5fd8d48734d5a02371c4b5ee551a69dca3842edbf577d863cf8ae9fdbbd4590121036666dd712e05a487916384bfcd5973eb53e8038eccbbf97f7eed775b87389536ffffffff01c0aff629010000001976a9145eaaa4f458f9158f86afcba08dd7448d27045e3d88ac00000000")
var fee = BitcoinFee.calculateFee(tx, satVb)

assertEquals(fee, 191 * satVb)
}

@Test
fun P2wpkhCalculateFee() {
val satVb: Long = 12
val tx = Numeric.hexStringToByteArray("020000000111b9f62923af73e297abb69f749e7a1aa2735fbdfd32ac5f6aa89e5c96841c18000000006b483045022100df9ed0b662b759e68b89a42e7144cddf787782a7129d4df05642dd825930e6e6022051a08f577f11cc7390684bbad2951a6374072253ffcf2468d14035ed0d8cd6490121028d7dce6d72fb8f7af9566616c6436349c67ad379f2404dd66fe7085fe0fba28fffffffff01c0aff629010000001600140d0e1cec6c2babe8badde5e9b3dea667da90036d00000000")
var fee = BitcoinFee.calculateFee(tx, satVb)

assertEquals(fee, 189 * satVb)
}

@Test
// Metadata can be observed live on:
// https://www.blockchain.com/explorer/transactions/btc/797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1
//
// Fee/VB 19.608 sat/vByte
// Size 235 Bytes
// Weight 610
fun Brc20TransferCommitCalculateFee() {
val satVb: Long = 19
val tx = Numeric.hexStringToByteArray("02000000000101089098890d2653567b9e8df2d1fbe5c3c8bf1910ca7184e301db0ad3b495c88e0100000000ffffffff02581b000000000000225120e8b706a97732e705e22ae7710703e7f589ed13c636324461afa443016134cc051040000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d02483045022100a44aa28446a9a886b378a4a65e32ad9a3108870bd725dc6105160bed4f317097022069e9de36422e4ce2e42b39884aa5f626f8f94194d1013007d5a1ea9220a06dce0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000")
var fee = BitcoinFee.calculateFee(tx, satVb)

assertEquals(fee, 153 * satVb) // 153 = ceil(610/4)
}
}
19 changes: 19 additions & 0 deletions include/TrustWalletCore/TWBitcoinFee.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright © 2017-2023 Trust Wallet.
//
// This file is part of Trust. The full Trust copyright notice, including
// terms governing use, modification, and redistribution, is contained in the
// file LICENSE at the root of the source code distribution tree.

#pragma once

#include "TWData.h"

TW_EXTERN_C_BEGIN

TW_EXPORT_CLASS
struct TWBitcoinFee;

TW_EXPORT_STATIC_METHOD
uint64_t TWBitcoinFeeCalculateFee(TWData* _Nonnull data, uint64_t satVb);

TW_EXTERN_C_END
38 changes: 30 additions & 8 deletions rust/tw_bitcoin/src/ffi/mod.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
#![allow(clippy::missing_safety_doc)]

use crate::{Error, Result, TXOutputP2TRScriptPath, TaprootScript, TxInputP2TRScriptPath};
use crate::{
calculate_fee, Error, Result, TXOutputP2TRScriptPath, TaprootScript, TxInputP2TRScriptPath,
};
use bitcoin::{
consensus::Decodable,
taproot::{NodeInfo, TapNodeHash, TaprootSpendInfo},
PublicKey, ScriptBuf, Txid,
PublicKey, ScriptBuf, Transaction, Txid,
};
use secp256k1::hashes::Hash;
use secp256k1::KeyPair;
use std::borrow::Cow;
use tw_memory::ffi::c_byte_array::CByteArray;
use tw_memory::ffi::c_byte_array_ref::CByteArrayRef;
use tw_memory::ffi::c_result::CUInt64Result;
use tw_memory::ffi::c_result::ErrorCode;
use tw_misc::try_or_else;
use tw_proto::Bitcoin::Proto::{
OutPoint, SigningInput, SigningOutput, Transaction, TransactionInput, TransactionOutput,
TransactionVariant as TrVariant,
OutPoint, SigningInput, SigningOutput, Transaction as ProtoTransaction, TransactionInput,
TransactionOutput, TransactionVariant as TrVariant,
};

pub mod address;
Expand All @@ -29,6 +33,27 @@ use crate::{
TxOutput, TxOutputP2PKH, TxOutputP2TRKeyPath, TxOutputP2WPKH,
};

#[no_mangle]
pub unsafe extern "C" fn tw_bitcoin_calculate_transaction_fee(
input: *const u8,
input_len: usize,
sat_vb: u64,
) -> CUInt64Result {
let Some(mut encoded) = CByteArrayRef::new(input, input_len).as_slice() else {
return CUInt64Result::error(1);
};

// Decode transaction.
let Ok(tx) = Transaction::consensus_decode(&mut encoded) else {
return CUInt64Result::error(1);
};

// Calculate fee.
let (_weight, fee) = calculate_fee(&tx, sat_vb);

CUInt64Result::ok(fee)
}

#[no_mangle]
pub unsafe extern "C" fn tw_taproot_build_and_sign_transaction(
input: *const u8,
Expand Down Expand Up @@ -199,9 +224,6 @@ pub(crate) fn taproot_build_and_sign_transaction(proto: SigningInput) -> Result<
script: {
// If `scriptSig` is empty, then the Witness is being used.
if input.script_sig.is_empty() {
// TODO: `to_vec` returns a `Vec<Vec<u8>>` representing
// individual items. Is it appropriate to simply merge
// everything here?
let witness: Vec<u8> = input.witness.to_vec().into_iter().flatten().collect();
Cow::from(witness)
} else {
Expand All @@ -223,7 +245,7 @@ pub(crate) fn taproot_build_and_sign_transaction(proto: SigningInput) -> Result<

// Create Protobuf structure of the full transaction.
let mut signing = SigningOutput {
transaction: Some(Transaction {
transaction: Some(ProtoTransaction {
version,
lockTime: lock_time,
inputs: proto_inputs,
Expand Down
8 changes: 4 additions & 4 deletions rust/tw_bitcoin/src/tests/brc20_transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pub const COMMIT_TX_RAW: &str = "02000000000101089098890d2653567b9e8df2d1fbe5c3c
// Used for revealing the Inscription.
// https://www.blockchain.com/explorer/transactions/btc/7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca
pub const REVEAL_TXID: &str = "797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1";
pub const REVEAL_RAW: &str = "02000000000101b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790000000000ffffffff012202000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d0340de6fd13e43700f59876d305e5a4a5c41ad7ada10bc5a4e4bdd779eb0060c0a78ebae9c33daf77bb3725172edb5bd12e26f00c08f9263e480d53b93818138ad0b5b0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800377b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f616466222c22616d74223a223230227d6821c00f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000";
pub const REVEAL_TX_RAW: &str = "02000000000101b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790000000000ffffffff012202000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d0340de6fd13e43700f59876d305e5a4a5c41ad7ada10bc5a4e4bdd779eb0060c0a78ebae9c33daf77bb3725172edb5bd12e26f00c08f9263e480d53b93818138ad0b5b0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800377b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f616466222c22616d74223a223230227d6821c00f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000";

// Used for transfering the Inscription ("BRC20 transfer").
// https://www.blockchain.com/explorer/transactions/btc/3e3576eb02667fac284a5ecfcb25768969680cc4c597784602d0a33ba7c654b7
Expand Down Expand Up @@ -126,11 +126,11 @@ fn brc20_transfer() {
// Encode the signed transaction.
let hex = hex::encode(&transaction, false);

assert_eq!(hex[..164], REVEAL_RAW[..164]);
assert_eq!(hex[..164], REVEAL_TX_RAW[..164]);
// We ignore the 64-byte Schnorr signature, since it uses random data for
// signing on each construction and is therefore not reproducible.
assert_ne!(hex[164..292], REVEAL_RAW[164..292]);
assert_eq!(hex[292..], REVEAL_RAW[292..]);
assert_ne!(hex[164..292], REVEAL_TX_RAW[164..292]);
assert_eq!(hex[292..], REVEAL_TX_RAW[292..]);

// # Actually transfer the "transferable" tokens.
// Based on Bitcoin transaction:
Expand Down
104 changes: 104 additions & 0 deletions rust/tw_bitcoin/src/tests/fee.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use crate::calculate_fee;
use bitcoin::{consensus::Decodable, Transaction};

// 10 satoshis per virtual byte.
const SAT_VB: u64 = 12;

fn decode_tx(raw: &str) -> Transaction {
let hex = tw_encoding::hex::decode(raw).unwrap();
Transaction::consensus_decode(&mut hex.as_slice()).unwrap()
}

#[test]
fn p2pkh_fee() {
let tx = decode_tx(super::p2pkh::TX_RAW);

let (weight, fee) = calculate_fee(&tx, SAT_VB);
assert_eq!(weight.to_vbytes_ceil(), 191);
assert_eq!(fee, 191 * SAT_VB);
}

#[test]
fn p2wpkh_fee() {
let tx = decode_tx(super::p2wpkh::TX_RAW);

let (weight, fee) = calculate_fee(&tx, SAT_VB);
assert_eq!(weight.to_vbytes_ceil(), 189);
assert_eq!(fee, 189 * SAT_VB);
}

#[test]
fn brc20_commit_fee() {
// Metadata can be observed live on:
// https://www.blockchain.com/explorer/transactions/btc/797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1
//
// Fee/VB 19.608 sat/vByte
// Size 235 Bytes
// Weight 610

// 19 satoshis per vbyte.
const SAT_19_VB: u64 = 19;

let tx = decode_tx(super::brc20_transfer::COMMIT_TX_RAW);

let (weight, fee) = calculate_fee(&tx, SAT_19_VB);
assert_eq!(weight.to_vbytes_ceil(), 153); // 153 = ceil(610/4)
assert_eq!(fee, 153 * SAT_19_VB);
}

#[test]
fn brc20_reveal_fee() {
// Metadata can be observed live on:
// https://www.blockchain.com/explorer/transactions/btc/7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca
//
// Fee/VB 49.267 sat/vByte
// Size 276 Bytes
// Weight 522

// 49 satoshis per vbyte (slightly overpaid here...)
const SAT_49_VB: u64 = 49;

let tx = decode_tx(super::brc20_transfer::REVEAL_TX_RAW);

let (weight, fee) = calculate_fee(&tx, SAT_49_VB);
assert_eq!(weight.to_vbytes_ceil(), 131); // 131 = ceil(522/4)
assert_eq!(fee, 131 * SAT_49_VB);
}

#[test]
fn ordinal_nft_commit_fee() {
// Metadata can be observed live on:
// https://www.blockchain.com/explorer/transactions/btc/f1e708e5c5847339e16accf8716c14b33717c14d6fe68f9db36627cecbde7117
//
// Fee/VB 10.656 sat/vByte
// Size 203 Bytes
// Weight 485

// 19 satoshis per vbyte.
const SAT_10_VB: u64 = 10;

let tx = decode_tx(super::nft::COMMIT_RAW_TX);

let (weight, fee) = calculate_fee(&tx, SAT_10_VB);
assert_eq!(weight.to_vbytes_ceil(), 122); // 122 = ceil(485/4)
assert_eq!(fee, 122 * SAT_10_VB);
}

#[test]
fn ordinal_nft_reveal_fee() {
// Metadata can be observed live on:
// https://www.blockchain.com/explorer/transactions/btc/173f8350b722243d44cc8db5584de76b432eb6d0888d9e66e662db51584f44ac
//
// Fee/VB 15.133 sat/vByte
// Size 7'829 Bytes
// Weight 8'075

// 19 satoshis per vbyte.
const SAT_15_VB: u64 = 15;

let tx = decode_tx(super::nft::REVEAL_RAW_TX);

let (weight, fee) = calculate_fee(&tx, SAT_15_VB);
assert_eq!(weight.to_vbytes_ceil(), 2019); // 2019 = ceil(8_075/4)
assert_eq!(fee, 2019 * SAT_15_VB);
}
6 changes: 3 additions & 3 deletions rust/tw_bitcoin/src/tests/ffi/brc20_transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,11 @@ fn proto_sign_brc20_transfer_inscription_reveal() {
let signed = taproot_build_and_sign_transaction(signing).unwrap();
let hex = hex::encode(&signed.encoded, false);

assert_eq!(hex[..164], REVEAL_RAW[..164]);
assert_eq!(hex[..164], REVEAL_TX_RAW[..164]);
// We ignore the 64-byte Schnorr signature, since it uses random data for
// signing on each construction and is therefore not reproducible.
assert_ne!(hex[164..292], REVEAL_RAW[164..292]);
assert_eq!(hex[292..], REVEAL_RAW[292..]);
assert_ne!(hex[164..292], REVEAL_TX_RAW[164..292]);
assert_eq!(hex[292..], REVEAL_TX_RAW[292..]);
}

/// Transfer the Inscription with P2WPKH.
Expand Down
48 changes: 48 additions & 0 deletions rust/tw_bitcoin/src/tests/ffi/fees.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use crate::ffi::tw_bitcoin_calculate_transaction_fee;
use tw_memory::ffi::c_result::CUInt64Result;

/// Convenience wrapper.
fn call_ffi_calculate_fee(hex: &str, sat_vb: u64) -> u64 {
let hex = tw_encoding::hex::decode(hex).unwrap();

let res: CUInt64Result =
unsafe { tw_bitcoin_calculate_transaction_fee(hex.as_ptr(), hex.len(), sat_vb) };

res.unwrap()
}

#[test]
fn ffi_calculate_p2pkh_fee() {
let fee = call_ffi_calculate_fee(crate::tests::p2pkh::TX_RAW, 10);
assert_eq!(fee, 191 * 10);
}

#[test]
fn ffi_calculate_p2wpkh_fee() {
let fee = call_ffi_calculate_fee(crate::tests::p2wpkh::TX_RAW, 10);
assert_eq!(fee, 189 * 10);
}

#[test]
fn ffi_calculate_brc20_commit_fee() {
let fee = call_ffi_calculate_fee(crate::tests::brc20_transfer::COMMIT_TX_RAW, 19);
assert_eq!(fee, 153 * 19);
}

#[test]
fn ffi_calculate_brc20_reveal_fee() {
let fee = call_ffi_calculate_fee(crate::tests::brc20_transfer::REVEAL_TX_RAW, 49);
assert_eq!(fee, 131 * 49);
}

#[test]
fn ffi_calculate_ordinal_nft_commit_fee() {
let fee = call_ffi_calculate_fee(crate::tests::nft::COMMIT_RAW_TX, 10);
assert_eq!(fee, 122 * 10);
}

#[test]
fn ffi_calculate_ordinal_nft_reveal_fee() {
let fee = call_ffi_calculate_fee(crate::tests::nft::REVEAL_RAW_TX, 15);
assert_eq!(fee, 2019 * 15);
}
1 change: 1 addition & 0 deletions rust/tw_bitcoin/src/tests/ffi/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod brc20_transfer;
mod fees;
mod nft;
mod scripts;
mod transaction;
Expand Down
1 change: 1 addition & 0 deletions rust/tw_bitcoin/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod address;
mod brc20_transfer;
mod data;
mod fee;
mod ffi;
mod nft;
mod p2pkh;
Expand Down
9 changes: 8 additions & 1 deletion rust/tw_bitcoin/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@ use bitcoin::blockdata::locktime::absolute::{Height, LockTime};
use bitcoin::consensus::Encodable;
use bitcoin::sighash::{EcdsaSighashType, SighashCache, TapSighashType};
use bitcoin::taproot::{LeafVersion, TapLeafHash};
use bitcoin::transaction::Transaction;
use bitcoin::{secp256k1, Address, TxIn, TxOut};
use bitcoin::{Transaction, Weight};

/// Determines the weight of the transaction and calculates the fee with the
/// given satoshis per vbyte.
pub fn calculate_fee(tx: &Transaction, sat_vb: u64) -> (Weight, u64) {
let weight = tx.weight();
(weight, weight.to_vbytes_ceil() * sat_vb)
}

#[derive(Debug, Clone)]
pub struct TransactionBuilder {
Expand Down
7 changes: 7 additions & 0 deletions rust/tw_memory/src/ffi/c_result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ pub struct CBoolResult {
pub result: bool,
}

#[repr(C)]
pub struct CUInt64Result {
pub code: i32,
pub result: u64,
}

impl_c_result!(CStrResult, *const c_char, core::ptr::null());
impl_c_result!(CStrMutResult, *mut c_char, core::ptr::null_mut());
impl_c_result!(CBoolResult, bool, false);
impl_c_result!(CUInt64Result, u64, 0);
Loading

0 comments on commit 45c0ff5

Please sign in to comment.