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
22 changes: 21 additions & 1 deletion app/src/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use crate::store::{BlockByHeight, BlockRef};
use crate::{aura::Aura, block::SignedConsensusBlock, error::Error, store::Storage};
use async_trait::async_trait;
use bitcoin::{BlockHash, Transaction as BitcoinTransaction, Txid};
use bridge::Error as FederationError;
use bridge::SingleMemberTransactionSignatures;
use bridge::{BitcoinSignatureCollector, BitcoinSigner, Bridge, PegInInfo, Tree, UtxoManager};
use ethereum_types::{Address, H256, U64};
Expand Down Expand Up @@ -1137,11 +1138,22 @@ impl<DB: ItemStore<MainnetEthSpec>> Chain<DB> {
required_outputs.len()
);

self.bitcoin_wallet.read().await.check_payment_proposal(
let missing_utxos = self.bitcoin_wallet.read().await.check_payment_proposal(
required_outputs,
unverified_block.message.pegout_payment_proposal.as_ref(),
Some(&self.bridge),
)?;

// Register any missing UTXOs that were found on the Bitcoin network
if !missing_utxos.is_empty() {
let count = missing_utxos.len();
self.bitcoin_wallet
.write()
.await
.register_utxos(missing_utxos)?;
trace!("Registered {} missing UTXOs from Bitcoin network", count);
}

trace!("Pegout proposal is valid");
Ok(())
}
Expand Down Expand Up @@ -2284,6 +2296,14 @@ impl<DB: ItemStore<MainnetEthSpec>> Chain<DB> {
Error::CandidateCacheError => {
logging_closure(&mut blocks_failed)
}
Error::FederationError(FederationError::BitcoinBlockNotFound(block_hash)) => {
// Bitcoin block not found is a non-fatal error during sync
// This can happen when the Bitcoin node is not fully synced
warn!(
"Bitcoin block not found during sync at height {}: {}. Continuing sync...",
block_height, block_hash
);
}
_ => {
async {
logging_closure(&mut blocks_failed);
Expand Down
99 changes: 95 additions & 4 deletions crates/federation/src/bitcoin_signing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use bitcoin::sighash::{Prevouts, ScriptPath, SighashCache, TapSighashType};
use bitcoin::taproot::{LeafVersion, Signature as SchnorrSig, TaprootBuilder, TaprootSpendInfo};
use bitcoin::{Address, Network, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid, Witness};
use bitcoincore_rpc::bitcoin::hashes::Hash;
use bitcoincore_rpc::RpcApi;
use serde::{Deserialize, Serialize};
use std::collections::hash_map::Entry;
use std::collections::HashMap;
Expand Down Expand Up @@ -128,9 +129,10 @@ impl<T: Database> UtxoManager<T> {
&self,
required_outputs: Vec<TxOut>,
pegout_proposal: Option<&Transaction>,
) -> Result<(), Error> {
bridge: Option<&crate::Bridge>,
) -> Result<Vec<LocalUtxo>, Error> {
let tx = match pegout_proposal {
None if required_outputs.is_empty() => return Ok(()),
None if required_outputs.is_empty() => return Ok(vec![]),
None => return Err(Error::MissingPegoutProposal),
Some(ref proposal) => proposal,
};
Expand Down Expand Up @@ -161,10 +163,21 @@ impl<T: Database> UtxoManager<T> {
return Err(Error::InvalidPegoutOutput);
}

// check the inputs
let mut missing_utxos = Vec::new();

// check the inputs - attempt to fetch missing UTXOs from Bitcoin network
for input in tx.input.iter() {
if !self.has_spendable_utxo(input.previous_output)? {
return Err(Error::UnspendableInput);
// Try to fetch the missing UTXO from the Bitcoin network
if let Some(bridge) = bridge {
if let Ok(utxo) = self.try_fetch_utxo(input.previous_output, bridge) {
missing_utxos.push(utxo);
} else {
return Err(Error::UnspendableInput);
}
} else {
return Err(Error::UnspendableInput);
}
}
}

Expand All @@ -178,6 +191,84 @@ impl<T: Database> UtxoManager<T> {
actual_outputs.len()
);

Ok(missing_utxos)
}

/// Attempts to fetch a missing UTXO from the Bitcoin network
fn try_fetch_utxo(
&self,
outpoint: OutPoint,
bridge: &crate::Bridge,
) -> Result<LocalUtxo, Error> {
// Fetch the transaction from Bitcoin network
let tx = bridge
.bitcoin_core
.rpc
.get_raw_transaction(&outpoint.txid, None)
.map_err(|_| Error::BitcoinError)?;

// Check if the output exists and is unspent
if outpoint.vout as usize >= tx.output.len() {
return Err(Error::UnknownOrSpentInput);
}

let txout = &tx.output[outpoint.vout as usize];

// Check if this output belongs to the federation (matches our taproot address)
if !self
.federation
.taproot_address
.matches_script_pubkey(&txout.script_pubkey)
{
return Err(Error::UnknownOrSpentInput);
}

// Check if the output is already spent using Bitcoin Core's gettxout RPC method
// This method returns null if the output is spent or doesn't exist
match bridge
.bitcoin_core
.rpc
.get_tx_out(&outpoint.txid, outpoint.vout, None)
{
Ok(Some(_)) => {
// Output exists and is unspent
}
Ok(None) => {
// Output is spent or doesn't exist
return Err(Error::UnknownOrSpentInput);
}
Err(_) => {
// RPC call failed, fall back to the transaction-based check
// This is a simplified fallback - in a real implementation, you might want to
// check if this output appears as an input in any confirmed transaction
for input in &tx.input {
if input.previous_output == outpoint {
return Err(Error::UnknownOrSpentInput);
}
}
}
}

// Create the UTXO to be registered
let utxo = LocalUtxo {
txout: txout.clone(),
outpoint,
is_spent: false,
keychain: KeychainKind::External,
Copy link

Copilot AI Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The keychain type is hardcoded to External. Consider determining the correct keychain type based on the UTXO's characteristics or making this configurable.

Suggested change
keychain: KeychainKind::External,
keychain: if self
.federation
.taproot_address
.matches_script_pubkey(&txout.script_pubkey)
{
KeychainKind::External
} else {
KeychainKind::Internal
},

Copilot uses AI. Check for mistakes.
};

trace!("Found missing UTXO on Bitcoin network: {:?}", outpoint);

Ok(utxo)
}

/// Register multiple UTXOs in the wallet database
pub fn register_utxos(&mut self, utxos: Vec<LocalUtxo>) -> Result<(), Error> {
let count = utxos.len();
for utxo in utxos {
self.tree.set_utxo(&utxo).map_err(|_| Error::DbError)?;
Copy link

Copilot AI Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error mapping discards the original database error information. Consider preserving the original error details for better debugging and error handling.

Suggested change
self.tree.set_utxo(&utxo).map_err(|_| Error::DbError)?;
self.tree.set_utxo(&utxo).map_err(|e| Error::DbError(e.to_string()))?;

Copilot uses AI. Check for mistakes.
}
trace!("Registered {} UTXOs from Bitcoin network", count);
Ok(())
}

Expand Down
11 changes: 11 additions & 0 deletions crates/federation/src/bitcoin_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,17 @@ impl BitcoinCore {
tokio::time::sleep(RETRY_DURATION).await;
continue;
}
Err(BitcoinError::JsonRpc(JsonRpcError::Rpc(err)))
if BitcoinRpcError::from(err.clone())
== BitcoinRpcError::RpcInvalidAddressOrKey
&& err.message.contains("Block not found") =>
{
// Bitcoin Core sometimes returns RpcInvalidAddressOrKey with "Block not found"
// instead of RpcInvalidParameter for blocks that don't exist yet
warn!("block does not exist yet (RpcInvalidAddressOrKey), retrying...");
tokio::time::sleep(RETRY_DURATION).await;
continue;
}
Err(err) => {
return Err(err.into());
}
Expand Down
16 changes: 15 additions & 1 deletion crates/federation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use thiserror::Error;

use bitcoin::{Address as BitcoinAddress, BlockHash, Transaction, TxOut, Txid};
use bitcoin_stream::stream_blocks;
use bitcoincore_rpc::jsonrpc::Error as JsonRpcError;
use bitcoincore_rpc::Error as BitcoinError;
use bitcoincore_rpc::{Error as RpcError, RpcApi};
use ethers::prelude::*;
use futures::prelude::*;
Expand Down Expand Up @@ -64,6 +66,8 @@ pub enum Error {
InsufficientConfirmations(i32),
#[error("Transaction is not a valid peg-in transaction")]
NotAPegin,
#[error("Bitcoin block not found: {0}")]
BitcoinBlockNotFound(BlockHash),
#[error("Rpc error: {0}")]
RpcError(#[from] RpcError),
}
Expand Down Expand Up @@ -146,7 +150,17 @@ impl Bridge {
txid: &Txid,
block_hash: &BlockHash,
) -> Result<PegInInfo, Error> {
let block_info = self.bitcoin_core.rpc.get_block_header_info(block_hash)?;
let block_info = match self.bitcoin_core.rpc.get_block_header_info(block_hash) {
Ok(info) => info,
Err(BitcoinError::JsonRpc(JsonRpcError::Rpc(err)))
if err.code == -5 && err.message.contains("Block not found") =>
{
// Return a more specific error for missing blocks
return Err(Error::BitcoinBlockNotFound(*block_hash));
}
Err(e) => return Err(Error::RpcError(e)),
};

if block_info.confirmations < self.required_confirmations.into() {
return Err(Error::InsufficientConfirmations(block_info.confirmations));
}
Expand Down
Loading