Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ esplora-client = { version = "0.12.0", default-features = false, features = [
"async-https",
"tokio",
] }
bincode = { version = "2.0.1", features = ["serde"] }
bincode = { version = "2.0.1", features = ["serde", "derive"] }
sha2 = "0.10.9"
rocksdb = "0.23.0"
async-trait = "0.1.88"
Expand Down
4 changes: 4 additions & 0 deletions crates/abci/src/tests/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ async fn test_execute_transaction_push_operations() {
value: vec![4, 5, 6],
},
],
None,
);

let result = executor
Expand Down Expand Up @@ -467,6 +468,7 @@ async fn test_execute_transaction_deposit_flow() {
},
Operation::OpIncrementBalance,
],
None,
);

let result = executor
Expand Down Expand Up @@ -503,6 +505,7 @@ async fn test_execute_transaction_withdrawal_flow() {
},
Operation::OpDecrementBalance,
],
None,
);

let result = executor
Expand Down Expand Up @@ -532,6 +535,7 @@ async fn test_execute_transaction_error_propagation() {
},
Operation::OpIncrementBalance,
],
None,
);

let result = executor
Expand Down
8 changes: 8 additions & 0 deletions crates/abci/src/tests/lib_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ async fn test_execute_deposit_transaction() {
},
Operation::OpIncrementBalance,
],
None,
);

// Execute transaction
Expand Down Expand Up @@ -252,6 +253,7 @@ async fn test_execute_withdrawal_transaction() {
},
Operation::OpIncrementBalance,
],
None,
);

chain_interface
Expand Down Expand Up @@ -280,6 +282,7 @@ async fn test_execute_withdrawal_transaction() {
},
Operation::OpDecrementBalance,
],
None,
);

// Execute withdrawal
Expand Down Expand Up @@ -321,6 +324,7 @@ async fn test_execute_transaction_insufficient_balance() {
},
Operation::OpDecrementBalance,
],
None,
);

// Add transaction to pending
Expand Down Expand Up @@ -373,6 +377,7 @@ async fn test_execute_transaction_state_persistence() {
},
Operation::OpIncrementBalance,
],
None,
);

chain_interface
Expand Down Expand Up @@ -427,6 +432,7 @@ async fn test_execute_multiple_transactions() {
},
Operation::OpIncrementBalance,
],
None,
);

chain_interface
Expand Down Expand Up @@ -465,6 +471,7 @@ async fn test_transaction_error_propagation() {
},
Operation::OpIncrementBalance, // This should fail due to no allowance
],
None,
);

let result = chain_interface
Expand Down Expand Up @@ -530,6 +537,7 @@ async fn test_concurrent_operations() {
},
Operation::OpIncrementBalance,
],
None,
);

chain_interface
Expand Down
1 change: 1 addition & 0 deletions crates/node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ num-traits.workspace = true
metrics.workspace = true
metrics-exporter-prometheus.workspace = true
actix-web.workspace = true
itertools = "0.14"

protocol = { path = "../protocol" }
types = { path = "../types" }
Expand Down
3 changes: 1 addition & 2 deletions crates/node/src/handlers/deposit/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,7 @@ impl<N: Network, W: Wallet> Handler<N, W> for DepositIntentState {
info!("✅ Added broadcasted transaction to pending pool");
}

let address = transaction.get_deposit_transaction_address()?;
let tx = node.oracle.get_transaction_by_address(&address).await?;
let tx = transaction.get_deposit_transaction_address()?;
node.wallet.ingest_external_tx(&tx)?;
}
Err(e) => {
Expand Down
1 change: 1 addition & 0 deletions crates/node/src/handlers/signing/create_signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ impl SigningState {
&tx,
pending.fee,
pending.user_pubkey,
pending.address_to,
)
.await?;
debug!("📤 Broadcasted transaction");
Expand Down
1 change: 1 addition & 0 deletions crates/node/src/handlers/signing/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ impl SigningState {
PendingSpend {
tx,
user_pubkey,
address_to: address.to_string(),
recipient_script,
fee: estimated_fee_sat,
},
Expand Down
9 changes: 8 additions & 1 deletion crates/node/src/handlers/withdrawl/create_withdrawl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,13 @@ impl SpendIntentState {
tx: &BitcoinTransaction,
fee: u64,
user_pubkey: String,
address_to: String,
) -> Result<(), NodeError> {
node.oracle.broadcast_transaction(tx).await?;

let transaction = Transaction::create_withdrawal_transaction(
&user_pubkey,
&address_to,
tx.output[0].value.to_sat() + fee,
)?;

Expand All @@ -155,6 +157,7 @@ impl SpendIntentState {
let spend_intent = PendingSpend {
tx: tx.clone(),
user_pubkey: user_pubkey.clone(),
address_to,
recipient_script,
fee,
};
Expand Down Expand Up @@ -184,7 +187,11 @@ impl SpendIntentState {

let debit = pay_out.value.to_sat() + pending.fee;

let transaction = Transaction::create_withdrawal_transaction(&pending.user_pubkey, debit)?;
let transaction = Transaction::create_withdrawal_transaction(
&pending.user_pubkey,
&pending.address_to,
debit,
)?;

let ChainResponse::AddTransactionToBlock { error: None } = node
.chain_interface_tx
Expand Down
11 changes: 6 additions & 5 deletions crates/node/src/start_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ pub async fn start_node(
.parse()
.unwrap();

let mut oracle: Box<dyn Oracle> = if use_mock_oracle.unwrap_or(false) {
let oracle: Box<dyn Oracle> = if use_mock_oracle.unwrap_or(false) {
Box::new(MockOracle::new(
swarm.network_events.clone(),
Some(deposit_intent_tx.clone()),
Expand Down Expand Up @@ -208,6 +208,11 @@ pub async fn start_node(
consensus_interface.start().await;
});

let mut oracle_clone = oracle.clone();
let deposit_monitor_handle = tokio::spawn(async move {
oracle_clone.poll_new_transactions(vec![]).await;
});

let mut node_state = NodeState::new_from_config(
&network_handle,
config,
Expand Down Expand Up @@ -261,10 +266,6 @@ pub async fn start_node(

let main_loop_handle = tokio::spawn(async move { node_state.start().await });

let deposit_monitor_handle = tokio::spawn(async move {
oracle.poll_new_transactions(vec![]).await;
});

// Create shutdown signal handler for Docker compatibility
let shutdown_signal = async {
#[cfg(unix)]
Expand Down
7 changes: 7 additions & 0 deletions crates/node/src/wallet/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// PendingSpend struct shared across node handlers
use bitcoin::{Address, PublicKey, Transaction, secp256k1::Scalar};
use protocol::block::Block;
use types::errors::NodeError;

pub mod taproot;
Expand All @@ -18,6 +19,12 @@ pub trait Wallet: Send + Sync {
dry_run: bool,
) -> Result<(Transaction, [u8; 32]), NodeError>;

fn get_transaction_for_block(
&self,
block: Block,
estimated_fee_sat: u64,
) -> Result<Transaction, NodeError>;

fn sign(
&mut self,
tx: &Transaction,
Expand Down
145 changes: 145 additions & 0 deletions crates/node/src/wallet/taproot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,22 @@ use bitcoin::{
Amount, Network, ScriptBuf, Sequence, Transaction, TxIn, TxOut, absolute::LockTime,
transaction::Version, witness::Witness,
};
use itertools::Itertools;
use oracle::oracle::Oracle;
use protocol::block::Block;
use protocol::transaction::TransactionType;
use std::str::FromStr;
use std::sync::Arc;
use types::errors::NodeError;
use types::utxo::Utxo;

use super::Wallet;

const IN_SZ_VBYTES: f64 = 68.0; // assume P2WPKH/P2TR key-spend
const OUT_SZ_VBYTES: f64 = 31.0; // P2WPKH/P2TR output
const TX_OVH_VBYTES: f64 = 10.5; // version + locktime + marker/flag
const DUST: u64 = 546;

#[derive(Debug, Clone)]
pub struct TrackedUtxo {
pub utxo: Utxo,
Expand Down Expand Up @@ -325,4 +334,140 @@ impl Wallet for TaprootWallet {
fn get_utxos(&self) -> Vec<TrackedUtxo> {
self.utxos.clone()
}

#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
fn get_transaction_for_block(
&self,
block: Block,
feerate_sat_per_vb: u64,
) -> Result<bitcoin::Transaction, NodeError> {
let mut payouts: Vec<(bitcoin::Address, u64)> = Vec::new();

for tx in &block.body.transactions {
if tx.r#type != TransactionType::Withdrawal {
continue;
}

let meta = tx
.metadata
.as_ref()
.ok_or_else(|| NodeError::Error("Withdrawal tx missing metadata".into()))?;

let addr_str = meta
.get("address_to")
.and_then(|v| v.as_str())
.ok_or_else(|| NodeError::Error("Withdrawal tx missing address_to".into()))?;

let amt_sat = meta
.get("amount_sat")
.and_then(serde_json::Value::as_u64)
.ok_or_else(|| NodeError::Error("Withdrawal tx missing amount_sat".into()))?;

let addr = bitcoin::Address::from_str(addr_str)
.map_err(|e| NodeError::Error(e.to_string()))
.map(bitcoin::Address::assume_checked)?;
payouts.push((addr, amt_sat));
}

if payouts.is_empty() {
return Err(NodeError::Error(
"Block contains no withdrawals to process".into(),
));
}

let fee_rate = feerate_sat_per_vb as f64;
let total_payout: u64 = payouts.iter().map(|(_, v)| *v).sum();
let utxos = &self.utxos; // shorthand

if utxos.is_empty() {
return Err(NodeError::Error("Wallet has no spendable UTXOs".into()));
}

let mut candidates = utxos.clone();
candidates.sort_by(|a, b| b.utxo.value.cmp(&a.utxo.value));

let mut chosen: Option<Vec<TrackedUtxo>> = None;
let mut chosen_change = 0_u64;

'outer: for k in 1..=candidates.len() {
for idx_set in (0..candidates.len()).combinations(k) {
let inputs: Vec<&TrackedUtxo> = idx_set.iter().map(|&i| &candidates[i]).collect();
let in_sum: u64 = inputs.iter().map(|t| t.utxo.value.to_sat()).sum();

let mut n_out = payouts.len() + 1;
let mut weight = (n_out as f64).mul_add(
OUT_SZ_VBYTES,
(k as f64).mul_add(IN_SZ_VBYTES, TX_OVH_VBYTES),
);
let mut fee = (weight * fee_rate).ceil() as u64;

if in_sum < total_payout + fee {
continue; // under-funded
}

let mut change = in_sum - total_payout - fee;
if change > 0 && change < DUST {
n_out -= 1;
weight = (n_out as f64).mul_add(
OUT_SZ_VBYTES,
(k as f64).mul_add(IN_SZ_VBYTES, TX_OVH_VBYTES),
);
fee = (weight * fee_rate).ceil() as u64;
change = in_sum - total_payout - fee;
}

if change == 0 || change >= DUST {
chosen = Some(inputs.into_iter().cloned().collect());
chosen_change = change;
break 'outer;
}
}
}

let selected = chosen.ok_or_else(|| {
NodeError::Error("Insufficient funds to satisfy withdrawals + fee".into())
})?;

let tx_inputs: Vec<TxIn> = selected
.iter()
.map(|t| TxIn {
previous_output: t.utxo.outpoint,
script_sig: ScriptBuf::new(),
sequence: Sequence::ZERO,
witness: Witness::new(),
})
.collect();

let mut tx_outputs: Vec<TxOut> = payouts
.iter()
.map(|(addr, amt)| TxOut {
value: Amount::from_sat(*amt),
script_pubkey: addr.script_pubkey(),
})
.collect();

if chosen_change > 0 {
let change_addr = self
.addresses
.first()
.ok_or_else(|| NodeError::Error("Wallet has no change address".into()))?;
tx_outputs.push(TxOut {
value: Amount::from_sat(chosen_change),
script_pubkey: change_addr.script_pubkey(),
});
}

let tx = bitcoin::Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input: tx_inputs,
output: tx_outputs,
};

Ok(tx)
}
}
2 changes: 1 addition & 1 deletion crates/oracle/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ impl Oracle for MockOracle {

async fn poll_new_transactions(&mut self, _addresses: Vec<Address>) {
info!("Polling new transactions");
let Some(dep_tx_sender) = self.deposit_intent_rx.take() else {
let Some(ref dep_tx_sender) = self.deposit_intent_rx else {
return;
};

Expand Down
Loading
Loading