diff --git a/app/src/chain.rs b/app/src/chain.rs index b08b888f..1e21d2d9 100644 --- a/app/src/chain.rs +++ b/app/src/chain.rs @@ -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}; @@ -1137,11 +1138,22 @@ impl> Chain { 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(()) } @@ -2284,6 +2296,14 @@ impl> Chain { 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); diff --git a/crates/federation/src/bitcoin_signing.rs b/crates/federation/src/bitcoin_signing.rs index a36b2202..5d13ba6b 100644 --- a/crates/federation/src/bitcoin_signing.rs +++ b/crates/federation/src/bitcoin_signing.rs @@ -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; @@ -128,9 +129,10 @@ impl UtxoManager { &self, required_outputs: Vec, pegout_proposal: Option<&Transaction>, - ) -> Result<(), Error> { + bridge: Option<&crate::Bridge>, + ) -> Result, 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, }; @@ -161,10 +163,21 @@ impl UtxoManager { 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); + } } } @@ -178,6 +191,84 @@ impl UtxoManager { 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 { + // 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, + }; + + 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) -> Result<(), Error> { + let count = utxos.len(); + for utxo in utxos { + self.tree.set_utxo(&utxo).map_err(|_| Error::DbError)?; + } + trace!("Registered {} UTXOs from Bitcoin network", count); Ok(()) } diff --git a/crates/federation/src/bitcoin_stream.rs b/crates/federation/src/bitcoin_stream.rs index 8572c484..971572ee 100644 --- a/crates/federation/src/bitcoin_stream.rs +++ b/crates/federation/src/bitcoin_stream.rs @@ -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()); } diff --git a/crates/federation/src/lib.rs b/crates/federation/src/lib.rs index 525280ac..fa6d5666 100644 --- a/crates/federation/src/lib.rs +++ b/crates/federation/src/lib.rs @@ -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::*; @@ -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), } @@ -146,7 +150,17 @@ impl Bridge { txid: &Txid, block_hash: &BlockHash, ) -> Result { - 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)); }