Skip to content

Commit b0b1c91

Browse files
committed
Add tx sender module
1 parent db889f1 commit b0b1c91

File tree

6 files changed

+324
-4
lines changed

6 files changed

+324
-4
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ header-chain = { git = "https://github.com/chainwayxyz/risc0-to-bitvm2", rev = "
4949
bitvm = { git = "https://github.com/BitVM/BitVM", rev = "89558b3a50e7d08a5eb42aa7de266e7912df3f98" }
5050

5151
[patch.crates-io]
52-
bitcoincore-rpc = { version = "0.18.0", git = "https://github.com/chainwayxyz/rust-bitcoincore-rpc.git", rev = "ca3cfa2" }
52+
bitcoincore-rpc = { version = "0.18.0", git = "https://github.com/chainwayxyz/rust-bitcoincore-rpc.git", rev = "54f2392" }
5353
secp256k1 = { git = "https://github.com/jlest01/rust-secp256k1", rev = "1cc7410df436b73d06db3c8ff7cbb29a78916b06" }
5454

5555

core/src/config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ impl Default for BridgeConfig {
219219
user_takes_after: 200,
220220

221221
network: Network::Regtest,
222-
bitcoin_rpc_url: "http://127.0.0.1:18443".to_string(),
222+
bitcoin_rpc_url: "http://127.0.0.1:18443/wallet/admin".to_string(),
223223
bitcoin_rpc_user: "admin".to_string(),
224224
bitcoin_rpc_password: "admin".to_string(),
225225

core/src/errors.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,12 @@ pub enum BridgeError {
250250

251251
#[error("Not enough operators")]
252252
NotEnoughOperators,
253+
254+
#[error("Bitcoin RPC signing error: {0:?}")]
255+
BitcoinRPCSigningError(Vec<String>),
256+
257+
#[error("Fee estimation error: {0:?}")]
258+
FeeEstimationError(Vec<String>),
253259
}
254260

255261
impl From<BridgeError> for ErrorObject<'static> {

core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub mod servers;
2525
pub mod utils;
2626
pub mod verifier;
2727
pub mod watchtower;
28+
pub mod tx_sender;
2829

2930
#[cfg(test)]
3031
mod test_utils;

core/src/tx_sender.rs

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
use std::collections::HashMap;
2+
3+
use bitcoin::{hashes::Hash, Address, Amount, OutPoint, Transaction, TxOut, Txid};
4+
use bitcoincore_rpc::{
5+
json::{EstimateMode, FundRawTransactionOptions},
6+
RpcApi,
7+
};
8+
9+
use crate::{
10+
actor::Actor,
11+
builder::{
12+
self,
13+
transaction::{
14+
input::SpendableTxIn, output::UnspentTxOut, TxHandlerBuilder, DEFAULT_SEQUENCE,
15+
},
16+
},
17+
database::Database,
18+
errors::BridgeError,
19+
extended_rpc::ExtendedRpc,
20+
};
21+
22+
/// Operator needs to bump fees of txs:
23+
/// These txs should be sent fast:
24+
/// Kickoff Tx (this depends on the number of kickoff connectors per sequential collateral tx.)
25+
/// Start Happy Reimburse Tx
26+
/// Assert Txs
27+
/// Disprove Timeout
28+
/// Reimburse Tx
29+
/// Happy Reimburse Tx
30+
///
31+
/// Operator also can send these txs with RBF:
32+
/// Payout Tx
33+
/// Operator Challenge ACK Tx
34+
///
35+
struct TxSender {
36+
pub(crate) signer: Actor,
37+
pub(crate) rpc: ExtendedRpc,
38+
pub(crate) db: Database,
39+
pub(crate) network: bitcoin::Network,
40+
}
41+
42+
impl TxSender {
43+
pub fn new(signer: Actor, rpc: ExtendedRpc, db: Database, network: bitcoin::Network) -> Self {
44+
Self {
45+
signer,
46+
rpc,
47+
db,
48+
network,
49+
}
50+
}
51+
52+
pub async fn get_fee_rate(&self) -> Result<Amount, BridgeError> {
53+
let fee_rate = self
54+
.rpc
55+
.client
56+
.estimate_smart_fee(1, Some(EstimateMode::Conservative))
57+
.await;
58+
59+
if fee_rate.is_err() {
60+
return Ok(Amount::from_sat(1));
61+
}
62+
63+
let fee_rate = fee_rate?;
64+
if fee_rate.errors.is_some() {
65+
tracing::error!("Fee estimation errors: {:?}", fee_rate.errors);
66+
return Ok(Amount::from_sat(1));
67+
// Err(BridgeError::FeeEstimationError(
68+
// fee_rate
69+
// .errors
70+
// .expect("Fee estimation errors should be present"),
71+
// ))
72+
} else {
73+
Ok(fee_rate
74+
.fee_rate
75+
.expect("Fee rate should be present when no errors"))
76+
}
77+
}
78+
79+
/// We want to allocate more than the required amount to be able to bump fees.
80+
pub fn calculate_required_amount_for_fee_payer(
81+
&self,
82+
bumped_tx_size: u64,
83+
fee_rate: Amount,
84+
) -> Result<Amount, BridgeError> {
85+
let required_amount = fee_rate * 3 * bumped_tx_size;
86+
Ok(required_amount)
87+
}
88+
89+
/// Uses trick in https://bitcoin.stackexchange.com/a/106204
90+
async fn custom_send_to_address(
91+
&self,
92+
address: &Address,
93+
amount_sats: Amount,
94+
) -> Result<OutPoint, BridgeError> {
95+
let mut outputs = HashMap::new();
96+
outputs.insert(address.to_string(), amount_sats);
97+
98+
let raw_tx = self
99+
.rpc
100+
.client
101+
.create_raw_transaction(&[], &outputs, None, None)
102+
.await?;
103+
104+
let fee_rate = self.get_fee_rate().await?;
105+
106+
let options = FundRawTransactionOptions {
107+
change_position: Some(1),
108+
lock_unspents: Some(true),
109+
fee_rate: Some(fee_rate),
110+
replaceable: Some(true),
111+
..Default::default()
112+
};
113+
114+
let funded_tx = self
115+
.rpc
116+
.client
117+
.fund_raw_transaction(&raw_tx, Some(&options), Some(true))
118+
.await?;
119+
120+
// Sign the funded tx
121+
let signed_tx = self
122+
.rpc
123+
.client
124+
.sign_raw_transaction_with_wallet(funded_tx.hex.as_ref() as &[u8], None, None)
125+
.await?;
126+
127+
if signed_tx.complete {
128+
let txid = self
129+
.rpc
130+
.client
131+
.send_raw_transaction(signed_tx.hex.as_ref() as &[u8])
132+
.await?;
133+
134+
Ok(OutPoint { txid, vout: 0 })
135+
} else {
136+
Err(BridgeError::BitcoinRPCSigningError(
137+
signed_tx
138+
.errors
139+
.expect("Signing errors should be present when incomplete")
140+
.iter()
141+
.map(|e| e.error.clone())
142+
.collect(),
143+
))
144+
}
145+
}
146+
147+
pub async fn create_fee_payer_tx(
148+
&self,
149+
bumped_txid: Txid,
150+
bumped_tx_size: u64,
151+
) -> Result<OutPoint, BridgeError> {
152+
let fee_rate = self.get_fee_rate().await?;
153+
tracing::info!("Fee rate: {}", fee_rate);
154+
let required_amount =
155+
self.calculate_required_amount_for_fee_payer(bumped_tx_size, fee_rate)?;
156+
157+
tracing::info!("Required amount: {}", required_amount);
158+
159+
let outpoint = self
160+
.custom_send_to_address(&self.signer.address, required_amount)
161+
.await?;
162+
163+
// save the db
164+
// self.db.save_fee_payer_tx(bumped_txid, outpoint.txid, outpoint.vout, self.signer.address.script_pubkey(), required_amount)?;
165+
166+
Ok(outpoint)
167+
}
168+
169+
/// Creates a child tx that spends the p2a anchor using the fee payer tx.
170+
/// It assumes the parent tx pays 0 fees.
171+
/// It also assumes the fee payer tx is signable by the self.signer.
172+
fn create_child_tx(
173+
&self,
174+
p2a_anchor: OutPoint,
175+
fee_payer_outpoint: OutPoint,
176+
fee_payer_amount: Amount,
177+
parent_tx_size: Amount,
178+
fee_rate: Amount,
179+
) -> Result<Transaction, BridgeError> {
180+
let (address, spend_info) = builder::address::create_taproot_address(
181+
&[],
182+
Some(self.signer.xonly_public_key),
183+
self.network,
184+
);
185+
186+
let child_tx_size = Amount::from_sat(300); // TODO: Fix this.
187+
let required_fee = fee_rate * (child_tx_size + parent_tx_size).to_sat();
188+
189+
let mut builder = TxHandlerBuilder::new()
190+
.add_input(
191+
SpendableTxIn::new_partial(p2a_anchor, builder::transaction::anchor_output()),
192+
DEFAULT_SEQUENCE,
193+
)
194+
.add_input(
195+
SpendableTxIn::new(
196+
fee_payer_outpoint,
197+
TxOut {
198+
value: fee_payer_amount,
199+
script_pubkey: address.script_pubkey(),
200+
},
201+
vec![],
202+
Some(spend_info),
203+
),
204+
DEFAULT_SEQUENCE,
205+
)
206+
.add_output(UnspentTxOut::from_partial(TxOut {
207+
value: fee_payer_amount - required_fee,
208+
script_pubkey: address.script_pubkey(), // TODO: This should be the wallet address, not the signer address
209+
}))
210+
.finalize();
211+
212+
let sighash = builder.calculate_pubkey_spend_sighash(1, None)?;
213+
let signature = self.signer.sign_with_tweak(sighash, None)?;
214+
builder.set_p2tr_key_spend_witness(
215+
&bitcoin::taproot::Signature {
216+
signature,
217+
sighash_type: bitcoin::TapSighashType::All,
218+
},
219+
1,
220+
)?;
221+
let child_tx = builder.get_cached_tx().clone();
222+
let child_tx_size = child_tx.weight().to_wu();
223+
tracing::info!("Child tx size: {}", child_tx_size);
224+
Ok(builder.get_cached_tx().clone())
225+
}
226+
227+
/// This will just persist the raw tx to the db
228+
pub async fn send_tx_with_cpfp(&self, tx: Transaction) -> Result<(), BridgeError> {
229+
let bumped_txid = tx.compute_txid();
230+
// let (
231+
// fee_payer_txid,
232+
// fee_payer_vout,
233+
// fee_payer_scriptpubkey,
234+
// fee_payer_amount,
235+
// is_confirmed,
236+
// ) = self
237+
// .db
238+
// .get_fee_payer_tx(bumped_txid, self.signer.address.script_pubkey())?;
239+
240+
// Now create the raw tx.
241+
242+
// let txid = self.rpc.client.submit_package(tx).await?;
243+
Ok(())
244+
}
245+
}
246+
247+
#[cfg(test)]
248+
mod tests {
249+
250+
// Imports required for create_test_config_with_thread_name macro.
251+
use crate::config::BridgeConfig;
252+
use crate::utils::initialize_logger;
253+
use crate::{create_test_config_with_thread_name, database::Database, initialize_database};
254+
use std::env;
255+
use std::thread;
256+
257+
use bitcoin::secp256k1::SecretKey;
258+
use secp256k1::rand;
259+
260+
use super::*;
261+
262+
async fn create_test_tx_sender() -> (TxSender, ExtendedRpc, Database) {
263+
let sk = SecretKey::new(&mut rand::thread_rng());
264+
let network = bitcoin::Network::Regtest;
265+
let actor = Actor::new(sk, None, network);
266+
267+
let config = create_test_config_with_thread_name!(None);
268+
let rpc = ExtendedRpc::connect(
269+
config.bitcoin_rpc_url.clone(),
270+
config.bitcoin_rpc_user.clone(),
271+
config.bitcoin_rpc_password.clone(),
272+
)
273+
.await
274+
.unwrap();
275+
276+
let db = Database::new(&config).await.unwrap();
277+
278+
let tx_sender = TxSender::new(actor, rpc.clone(), db.clone(), network);
279+
280+
(tx_sender, rpc, db)
281+
}
282+
283+
#[tokio::test]
284+
async fn test_create_fee_payer_tx() {
285+
let (tx_sender, rpc, _db) = create_test_tx_sender().await;
286+
287+
let mut outputs = HashMap::new();
288+
outputs.insert(
289+
"bcrt1qytffvlfa4a9jhffs9pddcr2w42940h6tc6fm9l".to_string(),
290+
Amount::from_sat(100000000),
291+
);
292+
293+
let raw_tx = rpc
294+
.client
295+
.create_raw_transaction(&vec![], &outputs, None, None)
296+
.await.unwrap();
297+
298+
tracing::info!("Raw tx: {:?}", raw_tx);
299+
300+
let outpoint = tx_sender
301+
.create_fee_payer_tx(Txid::all_zeros(), 300000)
302+
.await
303+
.unwrap();
304+
305+
let tx = rpc
306+
.client
307+
.get_raw_transaction(&outpoint.txid, None)
308+
.await
309+
.unwrap();
310+
311+
println!("tx: {:#?}", tx);
312+
}
313+
}

0 commit comments

Comments
 (0)