Skip to content

Commit

Permalink
Add fundrawtransaction and signrawtransactionwithwallet (#51)
Browse files Browse the repository at this point in the history
* transactions: Move input/output amount checkher to their own function.

* rpc_api: Add fund_raw_transaction.

* utils: Move hex serializers to utils from rpc_adapter.

* rpc_api: Add initial sign_raw_transaction_with_wallet.

* rpc_api: Add new test for sign_raw_transaction_with_wallet.

* rpc_api: Return a fixed address.

* rpc: Add unimplemented new RPC functions for the future.

* rpc_api: Use the new get_constant_credential_from_witness.

* rpc_api: Fix sign_raw_transaction_with_wallet test.

* rpc_api: Assign script_sig to new input in fund_raw_transaction.

* rpc_api: Compare output script pubkey instead of input in sign_raw_transaction_with_wallet.

* tests: Add fund_sign_raw_transaction_with_wallet.
  • Loading branch information
ceyhunsen authored Aug 23, 2024
1 parent 52ecf72 commit f0334d2
Show file tree
Hide file tree
Showing 13 changed files with 426 additions and 76 deletions.
2 changes: 1 addition & 1 deletion src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ impl RpcApiWrapper for bitcoincore_rpc::Client {
}

/// Mock Bitcoin RPC client.
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct Client {
/// Bitcoin ledger.
ledger: Ledger,
Expand Down
229 changes: 223 additions & 6 deletions src/client/rpc_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,23 @@
//! `Client`.
use super::Client;
use crate::ledger::Ledger;
use crate::{
ledger::{self, errors::LedgerError},
utils::encode_to_hex,
};
use bitcoin::{
address::NetworkChecked,
consensus::{encode, Encodable},
hashes::Hash,
params::Params,
Address, Amount, BlockHash, SignedAmount, Transaction, Txid,
Address, Amount, BlockHash, OutPoint, SignedAmount, Transaction, TxIn, TxOut, Txid,
};
use bitcoincore_rpc::{
json::{
self, GetRawTransactionResult, GetRawTransactionResultVin,
GetRawTransactionResultVinScriptSig, GetRawTransactionResultVout,
GetRawTransactionResultVoutScriptPubKey, GetTransactionResult, GetTransactionResultDetail,
GetTransactionResultDetailCategory, GetTxOutResult, WalletTxInfo,
GetTransactionResultDetailCategory, GetTxOutResult, SignRawTransactionResult, WalletTxInfo,
},
Error, RpcApi,
};
Expand Down Expand Up @@ -276,7 +279,7 @@ impl RpcApi for Client {
_label: Option<&str>,
_address_type: Option<json::AddressType>,
) -> bitcoincore_rpc::Result<Address<bitcoin::address::NetworkUnchecked>> {
let address = Ledger::generate_address_from_witness();
let address = ledger::Ledger::get_constant_credential_from_witness().address;

Ok(address.as_unchecked().to_owned())
}
Expand Down Expand Up @@ -353,12 +356,144 @@ impl RpcApi for Client {
fn get_block_count(&self) -> bitcoincore_rpc::Result<u64> {
Ok(self.ledger.get_block_height()?.into())
}

fn fund_raw_transaction<R: bitcoincore_rpc::RawTx>(
&self,
tx: R,
_options: Option<&json::FundRawTransactionOptions>,
_is_witness: Option<bool>,
) -> bitcoincore_rpc::Result<json::FundRawTransactionResult> {
let mut transaction: Transaction = encode::deserialize_hex(&tx.raw_hex())?;
tracing::debug!("Decoded input transaction: {transaction:?}");

let mut hex: Vec<u8> = Vec::new();
let tx = encode_to_hex(&transaction);
tx.consensus_encode(&mut hex).unwrap();

let diff = match self.ledger.check_transaction_funds(&transaction) {
// If input amount is sufficient, no need to modify anything.
Ok(()) => {
return Ok(json::FundRawTransactionResult {
hex,
fee: Amount::from_sat(0),
change_position: -1,
})
}
// Input funds are lower than the output funds, use the difference.
Err(LedgerError::InputFundsNotEnough(diff)) => diff,
// Other ledger errors.
Err(e) => return Err(e.into()),
};

tracing::debug!(
"Input funds are {diff} sats lower than the output sats, adding new input."
);

// Generate a new txout.
let address = self.get_new_address(None, None)?.assume_checked();
let txid = self.send_to_address(
&address,
Amount::from_sat(diff * diff),
None,
None,
None,
None,
None,
None,
)?;

let txin = TxIn {
previous_output: OutPoint { txid, vout: 0 },
..Default::default()
};

transaction.input.insert(0, txin);
tracing::debug!("New transaction: {transaction:?}");

let tx = encode_to_hex(&transaction);
let mut hex: Vec<u8> = Vec::new();
tx.consensus_encode(&mut hex).unwrap();

Ok(json::FundRawTransactionResult {
hex,
fee: Amount::from_sat(0),
change_position: 0,
})
}

fn sign_raw_transaction_with_wallet<R: bitcoincore_rpc::RawTx>(
&self,
tx: R,
_utxos: Option<&[json::SignRawTransactionInput]>,
_sighash_type: Option<json::SigHashType>,
) -> bitcoincore_rpc::Result<json::SignRawTransactionResult> {
let mut transaction: Transaction = encode::deserialize_hex(&tx.raw_hex())?;
tracing::debug!("Decoded input transaction: {transaction:?}");

let credentials = ledger::Ledger::get_constant_credential_from_witness();

let mut txouts: Vec<TxOut> = Vec::new();
for input in transaction.input.clone() {
let tx = match self.get_raw_transaction(&input.previous_output.txid, None) {
Ok(tx) => tx,
Err(e) => return Err(e),
};

let txout = match tx.output.get(input.previous_output.vout as usize) {
Some(txout) => txout,
None => {
return Err(LedgerError::Transaction(format!(
"No txout for {:?}",
input.previous_output
))
.into())
}
};

txouts.push(txout.clone());
}

let inputs: Vec<TxIn> = transaction
.input
.iter()
.enumerate()
.map(|(idx, input)| {
let mut input = input.to_owned();
tracing::trace!("Examining input {input:?}");

if input.witness.is_empty()
&& txouts[idx].script_pubkey == credentials.address.script_pubkey()
{
tracing::debug!(
"Signing input {input:?} with witness {:?}",
credentials.witness.clone().unwrap()
);
input.witness = credentials.witness.clone().unwrap();
}

input
})
.collect();

transaction.input = inputs;
tracing::trace!("Final inputs {:?}", transaction.input);

let mut hex: Vec<u8> = Vec::new();
let tx = encode_to_hex(&transaction);
tx.consensus_encode(&mut hex).unwrap();

Ok(SignRawTransactionResult {
hex,
complete: true,
errors: None,
})
}
}

#[cfg(test)]
mod tests {
use crate::{ledger::Ledger, Client, RpcApiWrapper};
use bitcoin::{Amount, Network, OutPoint, TxIn};
use crate::{ledger::Ledger, utils::decode_from_hex, Client, RpcApiWrapper};
use bitcoin::{consensus::Decodable, Amount, Network, OutPoint, Transaction, TxIn};
use bitcoincore_rpc::RpcApi;

#[test]
Expand Down Expand Up @@ -645,4 +780,86 @@ mod tests {

assert_eq!(rpc.get_block_count().unwrap(), 1);
}

#[test]
fn fund_raw_transaction() {
let rpc = Client::new("fund_raw_transaction", bitcoincore_rpc::Auth::None).unwrap();

let address = Ledger::generate_credential_from_witness().address;
let txid = rpc
.send_to_address(
&address,
Amount::from_sat(0x1F),
None,
None,
None,
None,
None,
None,
)
.unwrap();
let txin = rpc.ledger.create_txin(txid, 0);
let txout = rpc
.ledger
.create_txout(Amount::from_sat(0x45), address.script_pubkey());
let og_tx = rpc.ledger.create_transaction(vec![txin], vec![txout]);

let res = rpc.fund_raw_transaction(&og_tx, None, None).unwrap();
let tx = String::consensus_decode(&mut res.hex.as_slice()).unwrap();
let tx = decode_from_hex::<Transaction>(tx).unwrap();

assert_ne!(og_tx, tx);
assert_eq!(res.change_position, 0);

let res = rpc.fund_raw_transaction(&tx, None, None).unwrap();
let new_tx = String::consensus_decode(&mut res.hex.as_slice()).unwrap();
let new_tx = decode_from_hex::<Transaction>(new_tx).unwrap();

assert_eq!(tx, new_tx);
assert_eq!(res.change_position, -1);
}

#[test]
fn sign_raw_transaction_with_wallet() {
let rpc = Client::new(
"sign_raw_transaction_with_wallet",
bitcoincore_rpc::Auth::None,
)
.unwrap();

let address = Ledger::get_constant_credential_from_witness().address;
let txid = rpc
.send_to_address(
&address,
Amount::from_sat(0x1F),
None,
None,
None,
None,
None,
None,
)
.unwrap();
let txin = TxIn {
previous_output: OutPoint { txid, vout: 0 },
script_sig: address.script_pubkey(),
..Default::default()
};
let txout = rpc
.ledger
.create_txout(Amount::from_sat(0x45), address.script_pubkey());
let tx = rpc
.ledger
.create_transaction(vec![txin.clone()], vec![txout]);

assert!(txin.witness.is_empty());

let res = rpc
.sign_raw_transaction_with_wallet(&tx, None, None)
.unwrap();
let new_tx = String::consensus_decode(&mut res.hex.as_slice()).unwrap();
let new_tx = decode_from_hex::<Transaction>(new_tx).unwrap();

assert!(!new_tx.input.first().unwrap().witness.is_empty());
}
}
31 changes: 31 additions & 0 deletions src/ledger/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,36 @@ impl Ledger {

credential
}
/// Generates the constant Bitcoin credentials from a witness program.
#[tracing::instrument]
pub fn get_constant_credential_from_witness() -> UserCredential {
let secp = Secp256k1::new();
let secret_key = SecretKey::from_slice(&[0x45; 32]).unwrap();
let public_key = PublicKey::from_secret_key(&secp, &secret_key);
let x_only_public_key =
XOnlyPublicKey::from_keypair(&Keypair::from_secret_key(&secp, &secret_key)).0;
let address = Address::p2tr(&secp, x_only_public_key, None, Network::Regtest);

let mut credential = UserCredential {
secp,
secret_key,
public_key,
x_only_public_key,
address,
witness: None,
witness_program: None,
};
tracing::trace!("Constant credentials: {credential:?}");

Ledger::create_witness(&mut credential);

credential.address = Address::from_witness_program(
credential.witness_program.unwrap(),
bitcoin::Network::Regtest,
);

credential
}

/// Generates a random Bicoin address.
pub fn _generate_address() -> Address {
Expand Down Expand Up @@ -137,6 +167,7 @@ mod tests {
#[test]
fn generate_credentials() {
let credential = Ledger::generate_credential();
println!("{:?}", credential.secret_key);

assert_eq!(
credential.address.address_type().unwrap(),
Expand Down
2 changes: 2 additions & 0 deletions src/ledger/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use thiserror::Error;
pub enum LedgerError {
#[error("Transaction error: {0}")]
Transaction(String),
#[error("Transaction's input funds are {0} sats lower than the output funds")]
InputFundsNotEnough(u64),
#[error("UTXO error: {0}")]
Utxo(String),
#[error("SpendingRequirements error: {0}")]
Expand Down
2 changes: 1 addition & 1 deletion src/ledger/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use std::{
sync::{Arc, Mutex},
};

mod address;
pub mod address;
mod block;
pub(crate) mod errors;
mod script;
Expand Down
25 changes: 16 additions & 9 deletions src/ledger/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,15 +162,7 @@ impl Ledger {
/// No checks for if that UTXO is spendable or not.
#[tracing::instrument]
pub fn check_transaction(&self, transaction: &Transaction) -> Result<(), LedgerError> {
let input_value = self.calculate_transaction_input_value(transaction)?;
let output_value = self.calculate_transaction_output_value(transaction);

if input_value < output_value {
return Err(LedgerError::Transaction(format!(
"Input amount is smaller than output amount: {} < {}",
input_value, output_value
)));
}
self.check_transaction_funds(transaction)?;

let mut txouts = vec![];
for input in transaction.input.iter() {
Expand Down Expand Up @@ -226,6 +218,21 @@ impl Ledger {
Ok(())
}

/// Checks if transactions input amount is equal or bigger than the output
/// amount.
pub fn check_transaction_funds(&self, transaction: &Transaction) -> Result<(), LedgerError> {
let input_value = self.calculate_transaction_input_value(transaction)?;
let output_value = self.calculate_transaction_output_value(transaction);

if input_value < output_value {
Err(LedgerError::InputFundsNotEnough(
output_value.to_sat() - input_value.to_sat(),
))
} else {
Ok(())
}
}

/// Calculates a transaction's total output value.
///
/// # Panics
Expand Down
Loading

0 comments on commit f0334d2

Please sign in to comment.