From 488d20da77721635126fb571e28763f3bea86e0d Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Tue, 4 Feb 2025 01:21:29 +0800 Subject: [PATCH 01/17] miner: more BMM logs --- app/app.rs | 8 +++++--- lib/miner.rs | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/app.rs b/app/app.rs index 0528304..7e132f2 100644 --- a/app/app.rs +++ b/app/app.rs @@ -53,6 +53,7 @@ pub enum Error { } fn update_wallet(node: &Node, wallet: &Wallet) -> Result<(), Error> { + tracing::trace!("starting wallet update"); let addresses = wallet.get_addresses()?; let utxos = node.get_utxos_by_addresses(&addresses)?; let outpoints: Vec<_> = wallet.get_utxos()?.into_keys().collect(); @@ -63,6 +64,7 @@ fn update_wallet(node: &Node, wallet: &Wallet) -> Result<(), Error> { .collect(); wallet.put_utxos(&utxos)?; wallet.spend_utxos(&spent)?; + tracing::debug!("finished wallet update"); Ok(()) } @@ -485,14 +487,14 @@ impl App { miner_write .attempt_bmm(bribe.to_sat(), 0, header, body) .await?; - // miner_write.generate().await?; tracing::trace!("confirming bmm..."); if let Some((main_hash, header, body)) = miner_write.confirm_bmm().await? { tracing::trace!( - "confirmed bmm, submitting block {}", - header.hash() + %main_hash, + side_hash = %header.hash(), + "mine: confirmed BMM, submitting block", ); self.node.submit_block(main_hash, &header, &body).await?; } diff --git a/lib/miner.rs b/lib/miner.rs index 91e0bd9..483e3e9 100644 --- a/lib/miner.rs +++ b/lib/miner.rs @@ -57,7 +57,7 @@ where header.prev_main_hash, ) .await?; - tracing::info!("created BMM tx: {txid}"); + tracing::info!(%txid, "created BMM tx"); //assert_eq!(header.merkle_root, body.compute_merkle_root()); self.block = Some((header, body)); Ok(txid) @@ -81,7 +81,7 @@ where block_info, } => { if block_info.bmm_commitment == Some(block_hash) { - tracing::trace!(%block_hash, "verified bmm"); + tracing::debug!(%block_hash, "verified bmm"); self.block = None; return Ok(Some(( header_info.block_hash, @@ -90,10 +90,12 @@ where ))); } } + // BMM requests expire after one block, so if we we weren't able to + // get it in, the request failed. Event::DisconnectBlock { .. } => (), } }; - tracing::trace!(%block_hash, "bmm verification failed"); + tracing::debug!(%block_hash, "bmm verification failed"); self.block = None; Ok(None) } From e247d2de3fab5846606e50fb3dd91e2eb96bb135 Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Tue, 4 Feb 2025 02:00:28 +0800 Subject: [PATCH 02/17] app: more logs for mining, wallet updates, net --- app/cli.rs | 5 ++ app/gui/console_logs.rs | 6 +-- app/main.rs | 22 +++++--- app/rpc_server.rs | 114 ++++++++++++++-------------------------- cli/lib.rs | 28 ++++++++-- lib/miner.rs | 39 ++++++++++++-- lib/net/mod.rs | 59 +++++++++++++++++---- lib/net/peer.rs | 2 + lib/node/net_task.rs | 79 ++++++++++++++++++++-------- 9 files changed, 224 insertions(+), 130 deletions(-) diff --git a/app/cli.rs b/app/cli.rs index d06044d..4b84836 100644 --- a/app/cli.rs +++ b/app/cli.rs @@ -105,6 +105,9 @@ pub(super) struct Cli { /// Data directory for storing blockchain and wallet data #[command(flatten)] datadir: DatadirArg, + /// Log level for logs that get written to file + #[arg(default_value_t = tracing::Level::WARN, long)] + file_log_level: tracing::Level, /// If specified, the gui will not launch. #[arg(long)] headless: bool, @@ -142,6 +145,7 @@ pub(super) struct Cli { #[derive(Clone, Debug)] pub struct Config { pub datadir: PathBuf, + pub file_log_level: tracing::Level, pub headless: bool, /// If None, logging to file should be disabled. pub log_dir: Option, @@ -175,6 +179,7 @@ impl Cli { }; Ok(Config { datadir: self.datadir.0, + file_log_level: self.file_log_level, headless: self.headless, log_dir, log_level: self.log_level, diff --git a/app/gui/console_logs.rs b/app/gui/console_logs.rs index a3b1e7c..a1fc21f 100644 --- a/app/gui/console_logs.rs +++ b/app/gui/console_logs.rs @@ -70,10 +70,8 @@ impl ConsoleLogs { return; } }; - let cli = plain_bitnames_app_cli_lib::Cli { - rpc_addr: self.rpc_addr, - command, - }; + let cli = + plain_bitnames_app_cli_lib::Cli::new(command, self.rpc_addr, None); app.runtime.spawn({ let running_command = self.running_command.clone(); running_command.store(true, atomic::Ordering::SeqCst); diff --git a/app/main.rs b/app/main.rs index 9915975..135ba33 100644 --- a/app/main.rs +++ b/app/main.rs @@ -55,12 +55,12 @@ type RollingLoggerGuard = tracing_appender::non_blocking::WorkerGuard; /// to keep the file logger alive. fn rolling_logger( log_dir: &Path, + log_level: tracing::Level, ) -> anyhow::Result<(impl Layer, RollingLoggerGuard)> where S: tracing::Subscriber + for<'s> tracing_subscriber::registry::LookupSpan<'s>, { - const DEFAULT_LEVEL: tracing::Level = tracing::Level::WARN; const LOG_FILE_SUFFIX: &str = "log"; let rolling_log_appender = tracing_appender::rolling::Builder::new() .rotation(tracing_appender::rolling::Rotation::DAILY) @@ -68,8 +68,7 @@ where .build(log_dir)?; let (non_blocking_rolling_log_writer, rolling_log_guard) = tracing_appender::non_blocking(rolling_log_appender); - let level_filter = - tracing_filter::Targets::new().with_default(DEFAULT_LEVEL); + let level_filter = tracing_filter::Targets::new().with_default(log_level); let rolling_log_layer = tracing_subscriber::fmt::layer() .compact() .with_ansi(false) @@ -82,6 +81,7 @@ where // If the file logger is set, returns a guard that must be held for the // lifetime of the program in order to keep the file logger alive. fn set_tracing_subscriber( + file_log_level: tracing::Level, log_dir: Option<&Path>, log_level: tracing::Level, ) -> anyhow::Result<(LineBuffer, Option)> { @@ -113,7 +113,7 @@ fn set_tracing_subscriber( let (rolling_log_layer, rolling_log_guard) = match log_dir { None => (None, None), Some(log_dir) => { - let (layer, guard) = rolling_logger(log_dir)?; + let (layer, guard) = rolling_logger(log_dir, file_log_level)?; (Some(layer), Some(guard)) } }; @@ -135,10 +135,13 @@ fn set_tracing_subscriber( fn main() -> anyhow::Result<()> { let cli = cli::Cli::parse(); let config = cli.get_config()?; - let (line_buffer, _rolling_log_guard) = - set_tracing_subscriber(config.log_dir.as_deref(), config.log_level)?; - let app: Result = - app::App::new(&config).inspect(|app| { + let (line_buffer, _rolling_log_guard) = set_tracing_subscriber( + config.file_log_level, + config.log_dir.as_deref(), + config.log_level, + )?; + let app: Result = app::App::new(&config) + .inspect(|app| { // spawn rpc server app.runtime.spawn({ let app = app.clone(); @@ -146,6 +149,9 @@ fn main() -> anyhow::Result<()> { rpc_server::run_server(app, config.rpc_addr).await.unwrap() } }); + }) + .inspect_err(|err| { + tracing::error!("application error: {:?}", err); }); if config.headless { diff --git a/app/rpc_server.rs b/app/rpc_server.rs index 57d4201..57b2ee2 100644 --- a/app/rpc_server.rs +++ b/app/rpc_server.rs @@ -8,17 +8,16 @@ use jsonrpsee::{ }; use plain_bitnames::{ - node, types::{ hashes::BitName, Address, BitNameData, Block, BlockHash, EncryptionPubKey, FilledOutput, OutPoint, PointedOutput, Transaction, Txid, VerifyingKey, WithdrawalBundle, }, - wallet::{self, Balance}, + wallet::Balance, }; use plain_bitnames_app_rpc_api::{RpcServer, TxInfo}; -use crate::app::{self, App}; +use crate::app::App; pub struct RpcServerImpl { app: App, @@ -36,28 +35,10 @@ where custom_err_msg(format!("{error:#}")) } -fn convert_app_err(err: app::Error) -> ErrorObject<'static> { - let err = anyhow::anyhow!(err); - tracing::error!("{err:#}"); - custom_err(err) -} - -fn convert_node_err(err: node::Error) -> ErrorObject<'static> { - let err = anyhow::anyhow!(err); - tracing::error!("{err:#}"); - custom_err(err) -} - -fn convert_wallet_err(err: wallet::Error) -> ErrorObject<'static> { - let err = anyhow::anyhow!(err); - tracing::error!("{err:#}"); - custom_err(err) -} - #[async_trait] impl RpcServer for RpcServerImpl { async fn balance(&self) -> RpcResult { - self.app.wallet.get_balance().map_err(convert_wallet_err) + self.app.wallet.get_balance().map_err(custom_err) } async fn bitname_data( @@ -67,15 +48,15 @@ impl RpcServer for RpcServerImpl { self.app .node .get_current_bitname_data(&bitname_id) - .map_err(convert_node_err) + .map_err(custom_err) } async fn bitnames(&self) -> RpcResult> { - self.app.node.bitnames().map_err(convert_node_err) + self.app.node.bitnames().map_err(custom_err) } async fn connect_peer(&self, addr: SocketAddr) -> RpcResult<()> { - self.app.node.connect_peer(addr).map_err(convert_node_err) + self.app.node.connect_peer(addr).map_err(custom_err) } async fn create_deposit( @@ -91,7 +72,7 @@ impl RpcServer for RpcServerImpl { bitcoin::Amount::from_sat(value_sats), bitcoin::Amount::from_sat(fee_sats), ) - .map_err(convert_app_err) + .map_err(custom_err) }) .await .unwrap() @@ -133,38 +114,26 @@ impl RpcServer for RpcServerImpl { } async fn get_new_address(&self) -> RpcResult
{ - self.app - .wallet - .get_new_address() - .map_err(convert_wallet_err) + self.app.wallet.get_new_address().map_err(custom_err) } async fn get_new_encryption_key(&self) -> RpcResult { - self.app - .wallet - .get_new_encryption_key() - .map_err(convert_wallet_err) + self.app.wallet.get_new_encryption_key().map_err(custom_err) } async fn get_new_verifying_key(&self) -> RpcResult { - self.app - .wallet - .get_new_verifying_key() - .map_err(convert_wallet_err) + self.app.wallet.get_new_verifying_key().map_err(custom_err) } async fn get_paymail(&self) -> RpcResult> { - self.app.get_paymail(None).map_err(convert_app_err) + self.app.get_paymail(None).map_err(custom_err) } async fn get_transaction( &self, txid: Txid, ) -> RpcResult> { - self.app - .node - .try_get_transaction(txid) - .map_err(convert_node_err) + self.app.node.try_get_transaction(txid).map_err(custom_err) } async fn get_transaction_info( @@ -175,7 +144,7 @@ impl RpcServer for RpcServerImpl { .app .node .try_get_filled_transaction(txid) - .map_err(convert_node_err)? + .map_err(custom_err)? else { return Ok(None); }; @@ -185,13 +154,13 @@ impl RpcServer for RpcServerImpl { .app .node .try_get_tip_height() - .map_err(convert_node_err)? + .map_err(custom_err)? .expect("Height should exist for tip"); let height = self .app .node .get_height(txin.block_hash) - .map_err(convert_node_err)?; + .map_err(custom_err)?; Some(tip_height - height) } None => None, @@ -211,11 +180,7 @@ impl RpcServer for RpcServerImpl { } async fn get_wallet_addresses(&self) -> RpcResult> { - let addrs = self - .app - .wallet - .get_addresses() - .map_err(convert_wallet_err)?; + let addrs = self.app.wallet.get_addresses().map_err(custom_err)?; let mut res: Vec<_> = addrs.into_iter().collect(); res.sort_by_key(|addr| addr.as_base58()); Ok(res) @@ -224,7 +189,7 @@ impl RpcServer for RpcServerImpl { async fn get_wallet_utxos( &self, ) -> RpcResult>> { - let utxos = self.app.wallet.get_utxos().map_err(convert_wallet_err)?; + let utxos = self.app.wallet.get_utxos().map_err(custom_err)?; let utxos = utxos .into_iter() .map(|(outpoint, output)| PointedOutput { outpoint, output }) @@ -233,11 +198,7 @@ impl RpcServer for RpcServerImpl { } async fn getblockcount(&self) -> RpcResult { - let height = self - .app - .node - .try_get_tip_height() - .map_err(convert_node_err)?; + let height = self.app.node.try_get_tip_height().map_err(custom_err)?; let block_count = height.map_or(0, |height| height + 1); Ok(block_count) } @@ -249,7 +210,7 @@ impl RpcServer for RpcServerImpl { .app .node .get_latest_failed_withdrawal_bundle_height() - .map_err(convert_node_err)?; + .map_err(custom_err)?; Ok(height) } @@ -259,7 +220,7 @@ impl RpcServer for RpcServerImpl { } async fn list_utxos(&self) -> RpcResult>> { - let utxos = self.app.node.get_all_utxos().map_err(convert_node_err)?; + let utxos = self.app.node.get_all_utxos().map_err(custom_err)?; let res = utxos .into_iter() .map(|(outpoint, output)| PointedOutput { outpoint, output }) @@ -269,10 +230,14 @@ impl RpcServer for RpcServerImpl { async fn mine(&self, fee: Option) -> RpcResult<()> { let fee = fee.map(bitcoin::Amount::from_sat); - self.app.local_pool.spawn_pinned({ + self.app + .local_pool + .spawn_pinned({ let app = self.app.clone(); - move || async move { app.mine(fee).await.map_err(convert_app_err) } - }).await.unwrap() + move || async move { app.mine(fee).await.map_err(custom_err) } + }) + .await + .unwrap() } async fn my_utxos(&self) -> RpcResult>> { @@ -280,7 +245,7 @@ impl RpcServer for RpcServerImpl { .app .wallet .get_utxos() - .map_err(convert_wallet_err)? + .map_err(custom_err)? .into_iter() .map(|(outpoint, output)| PointedOutput { outpoint, output }) .collect(); @@ -299,17 +264,17 @@ impl RpcServer for RpcServerImpl { self.app .node .get_pending_withdrawal_bundle() - .map_err(convert_node_err) + .map_err(custom_err) } async fn reserve_bitname(&self, plain_name: String) -> RpcResult { let mut tx = Transaction::default(); let () = match self.app.wallet.reserve_bitname(&mut tx, &plain_name) { Ok(()) => (), - Err(err) => return Err(convert_wallet_err(err)), + Err(err) => return Err(custom_err(err)), }; let txid = tx.txid(); - self.app.sign_and_send(tx).map_err(convert_app_err)?; + self.app.sign_and_send(tx).map_err(custom_err)?; Ok(txid) } @@ -317,15 +282,12 @@ impl RpcServer for RpcServerImpl { self.app .wallet .set_seed_from_mnemonic(mnemonic.as_str()) - .map_err(convert_wallet_err) + .map_err(custom_err) } async fn sidechain_wealth_sats(&self) -> RpcResult { - let sidechain_wealth = self - .app - .node - .get_sidechain_wealth() - .map_err(convert_node_err)?; + let sidechain_wealth = + self.app.node.get_sidechain_wealth().map_err(custom_err)?; Ok(sidechain_wealth.to_sat()) } @@ -356,9 +318,9 @@ impl RpcServer for RpcServerImpl { Amount::from_sat(fee_sats), memo, ) - .map_err(convert_wallet_err)?; + .map_err(custom_err)?; let txid = tx.txid(); - self.app.sign_and_send(tx).map_err(convert_app_err)?; + self.app.sign_and_send(tx).map_err(custom_err)?; Ok(txid) } @@ -378,9 +340,9 @@ impl RpcServer for RpcServerImpl { Amount::from_sat(mainchain_fee_sats), Amount::from_sat(fee_sats), ) - .map_err(convert_wallet_err)?; + .map_err(custom_err)?; let txid = tx.txid(); - self.app.sign_and_send(tx).map_err(convert_app_err)?; + self.app.sign_and_send(tx).map_err(custom_err)?; Ok(txid) } } diff --git a/cli/lib.rs b/cli/lib.rs index 9b13016..7e3bd03 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -1,4 +1,7 @@ -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + time::Duration, +}; use clap::{Parser, Subcommand}; use jsonrpsee::http_client::{HttpClient, HttpClientBuilder}; @@ -135,20 +138,39 @@ async fn resolve_commit( Ok(bitname_commit.remove(&field_name)) } +const DEFAULT_TIMEOUT_SECS: u64 = 60; + #[derive(Clone, Debug, Parser)] #[command(author, version, about, long_about = None)] pub struct Cli { + #[command(subcommand)] + pub command: Command, /// address for use by the RPC server #[arg(default_value_t = DEFAULT_RPC_ADDR, long)] pub rpc_addr: SocketAddr, + /// Timeout for RPC requests in seconds. + #[arg(default_value_t = DEFAULT_TIMEOUT_SECS, long = "timeout")] + timeout_secs: u64, +} - #[command(subcommand)] - pub command: Command, +impl Cli { + pub fn new( + command: Command, + rpc_addr: SocketAddr, + timeout_secs: Option, + ) -> Self { + Self { + command, + rpc_addr, + timeout_secs: timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS), + } + } } impl Cli { pub async fn run(self) -> anyhow::Result { let rpc_client: HttpClient = HttpClientBuilder::default() + .request_timeout(Duration::from_secs(self.timeout_secs)) .build(format!("http://{}", self.rpc_addr))?; let res = match self.command { Command::Balance => { diff --git a/lib/miner.rs b/lib/miner.rs index 483e3e9..dd23572 100644 --- a/lib/miner.rs +++ b/lib/miner.rs @@ -63,6 +63,7 @@ where Ok(txid) } + // Wait for a block to be connected that contains our BMM request. pub async fn confirm_bmm( &mut self, ) -> Result, Error> { @@ -80,21 +81,49 @@ where header_info, block_info, } => { - if block_info.bmm_commitment == Some(block_hash) { - tracing::debug!(%block_hash, "verified bmm"); + if let Some(bmm_commitment) = block_info.bmm_commitment + && bmm_commitment == block_hash + { + tracing::debug!( + side_hash = %block_hash, + main_height = header_info.height, + main_hash = %header_info.block_hash, + bmm_commitment = %bmm_commitment, + "Verified BMM" + ); self.block = None; return Ok(Some(( header_info.block_hash, header, body, ))); + } else { + tracing::warn!( + side_hash = %block_hash, + main_height = header_info.height, + main_hash = %header_info.block_hash, + bmm_commitment = %block_info + .bmm_commitment + .map(|h| h.to_string()) + .unwrap_or("none".to_string()), + "Received new block without our BMM commitment" + ); } } - // BMM requests expire after one block, so if we we weren't able to - // get it in, the request failed. - Event::DisconnectBlock { .. } => (), + // This will actually never happen - there's currently no logic in + // the enforcer that ends up sending this message. This is left in + // for exhaustiveness purposes. + disconnect @ Event::DisconnectBlock { .. } => { + tracing::warn!( + %block_hash, + event = ?disconnect, + "received disconnect block event" + ); + } } }; + // BMM requests expire after one block, so if we we weren't able to + // get it in, the request failed. tracing::debug!(%block_hash, "bmm verification failed"); self.block = None; Ok(None) diff --git a/lib/net/mod.rs b/lib/net/mod.rs index 2779372..6489940 100644 --- a/lib/net/mod.rs +++ b/lib/net/mod.rs @@ -42,8 +42,12 @@ pub enum Error { Bincode(#[from] bincode::Error), #[error("connect error")] Connect(#[from] quinn::ConnectError), - #[error("connection error")] - Connection(#[from] quinn::ConnectionError), + #[error("connection error (remote address: {remote_address})")] + Connection { + #[source] + error: quinn::ConnectionError, + remote_address: SocketAddr, + }, #[error("heed error")] Heed(#[from] heed::Error), #[error("quinn error")] @@ -326,13 +330,38 @@ impl Net { }; let () = known_peers.into_iter().try_for_each(|(peer_addr, _)| { tracing::trace!(%peer_addr, "connecting to already known peer"); - net.connect_peer(env.clone(), peer_addr) + match net.connect_peer(env.clone(), peer_addr) { + Err(Error::Connect( + quinn::ConnectError::InvalidRemoteAddress(addr), + )) => { + tracing::warn!( + %addr, "new net: known peer with invalid remote address, removing" + ); + let mut tx = env.write_txn()?; + net.known_peers.delete(&mut tx, &peer_addr)?; + tx.commit()?; + tracing::info!( + %addr, + "new net: removed known peer with invalid remote address" + ); + Ok(()) + } + res => res, + } + }) + // TODO: would be better to indicate this in the return error? + .inspect_err(|err| { + tracing::error!("unable to connect to known peers during net construction: {err:#}"); })?; Ok((net, peer_info_rx)) } - /// Accept the next incoming connection - pub async fn accept_incoming(&self, env: heed::Env) -> Result<(), Error> { + /// Accept the next incoming connection. Returns Some(addr) if a connection was accepted + /// and a new peer was added. + pub async fn accept_incoming( + &self, + env: heed::Env, + ) -> Result, Error> { tracing::debug!( "listening for connections on `{}`", self.server @@ -342,8 +371,14 @@ impl Net { ); let connection = match self.server.accept().await { Some(conn) => { - tracing::trace!("accepted connection from {conn:?}"); - Connection(conn.await.map_err(Error::Connection)?) + let remote_address = conn.remote_address(); + tracing::trace!(%remote_address, "accepting connection"); + let raw_conn = + conn.await.map_err(|error| Error::Connection { + error, + remote_address, + })?; + Connection(raw_conn) } None => { tracing::debug!("server endpoint closed"); @@ -351,21 +386,23 @@ impl Net { } }; let addr = connection.addr(); + tracing::trace!(%addr, "accepted incoming connection"); if self.active_peers.read().contains_key(&addr) { tracing::info!( - "already connected to {addr}, refusing duplicate connection", + %addr, "already peered, refusing duplicate", ); connection .0 .close(quinn::VarInt::from_u32(1), b"already connected"); } if connection.0.close_reason().is_some() { - return Ok(()); + return Ok(None); } - tracing::info!("connected to peer at {addr}"); + tracing::info!(%addr, "connected to new peer"); let mut rwtxn = env.write_txn()?; self.known_peers.put(&mut rwtxn, &addr, &())?; rwtxn.commit()?; + tracing::trace!(%addr, "wrote peer to database"); let connection_ctxt = PeerConnectionCtxt { env, archive: self.archive.clone(), @@ -384,7 +421,7 @@ impl Net { } }); self.add_active_peer(addr, connection_handle)?; - Ok(()) + Ok(Some(addr)) } // Push an internal message to the specified peer diff --git a/lib/net/peer.rs b/lib/net/peer.rs index 9cb4c6f..3d643ac 100644 --- a/lib/net/peer.rs +++ b/lib/net/peer.rs @@ -1262,6 +1262,7 @@ pub fn handle( ctxt: ConnectionContext, connection: Connection, ) -> (ConnectionHandle, mpsc::UnboundedReceiver) { + let addr = connection.addr(); let (internal_message_tx, internal_message_rx) = mpsc::unbounded(); let (info_tx, info_rx) = mpsc::unbounded(); let connection_task = { @@ -1280,6 +1281,7 @@ pub fn handle( }; let task = spawn(async move { if let Err(err) = connection_task().await { + tracing::error!(%addr, "connection task error, sending on info_tx: {err:#}"); if let Err(send_error) = info_tx.unbounded_send(err.into()) && let Info::Error(err) = send_error.into_inner() { diff --git a/lib/node/net_task.rs b/lib/node/net_task.rs index d07d422..cd9928a 100644 --- a/lib/node/net_task.rs +++ b/lib/node/net_task.rs @@ -10,7 +10,7 @@ use std::{ use fallible_iterator::{FallibleIterator, IteratorExt}; use futures::{ channel::{ - mpsc::{self, UnboundedReceiver, UnboundedSender}, + mpsc::{self, TrySendError, UnboundedReceiver, UnboundedSender}, oneshot, }, stream, StreamExt, @@ -55,11 +55,11 @@ pub enum Error { #[error("Receive mainchain task response cancelled")] ReceiveMainchainTaskResponse, #[error("Receive reorg result cancelled (oneshot)")] - ReceiveReorgResultOneshot, + ReceiveReorgResultOneshot(#[source] oneshot::Canceled), #[error("Send mainchain task request failed")] SendMainchainTaskRequest, #[error("Send new tip ready failed")] - SendNewTipReady, + SendNewTipReady(#[source] TrySendError), #[error("Send reorg result error (oneshot)")] SendReorgResultOneshot, #[error("state error")] @@ -452,6 +452,7 @@ where resp: PeerResponse, req: PeerRequest, ) -> Result<(), Error> { + tracing::debug!(?req, ?resp, "starting response handler"); match (req, resp) { ( req @ PeerRequest::GetBlock { @@ -527,9 +528,14 @@ where main_block_hash, }; if header.prev_side_hash == tip_hash { + tracing::trace!( + ?block_tip, + origin = %addr, + "sending new tip ready" + ); let () = new_tip_ready_tx .unbounded_send((block_tip, Some(addr), None)) - .map_err(|_| Error::SendNewTipReady)?; + .map_err(Error::SendNewTipReady)?; } let Some(descendant_tips) = descendant_tips.remove(&block_tip) @@ -552,14 +558,23 @@ where common_ancestor, )?; if missing_bodies.is_empty() { + tracing::debug!( + ?descendant_tip, + "no missing bodies, submitting new tip ready to sources" + ); for addr in sources { + tracing::trace!( + %addr, + ?descendant_tip, + "sending new tip ready" + ); let () = new_tip_ready_tx .unbounded_send(( descendant_tip, Some(addr), None, )) - .map_err(|_| Error::SendNewTipReady)?; + .map_err(Error::SendNewTipReady)?; } } } @@ -698,9 +713,10 @@ where } async fn run(mut self) -> Result<(), Error> { + tracing::debug!("starting net task"); #[derive(Debug)] enum MailboxItem { - AcceptConnection(Result<(), Error>), + AcceptConnection(Result, Error>), // Forward a mainchain task request, along with the peer that // caused the request, and the peer state ID of the request ForwardMainchainTaskRequest( @@ -720,8 +736,15 @@ where let env = self.ctxt.env.clone(); let net = self.ctxt.net.clone(); let fut = async move { - let () = net.accept_incoming(env).await?; - Result::<_, Error>::Ok(Some(((), ()))) + let maybe_socket_addr = net.accept_incoming(env).await?; + // / Return: + // - The value to yield (maybe_socket_addr) + // - The state for the next iteration (()) + // Wrapped in Result and Option + Result::, ())>, Error>::Ok(Some(( + maybe_socket_addr, + (), + ))) }; Box::pin(fut) }) @@ -763,9 +786,21 @@ where HashSet<(SocketAddr, PeerStateId)>, >::new(); while let Some(mailbox_item) = mailbox_stream.next().await { - tracing::trace!("received mailbox item: {:#?}", mailbox_item); + tracing::trace!(?mailbox_item, "received new mailbox item"); match mailbox_item { - MailboxItem::AcceptConnection(res) => res?, + MailboxItem::AcceptConnection(res) => match res { + // We received a connection new incoming network connection, but no peer + // was added + Ok(None) => { + continue; + } + Ok(Some(addr)) => { + tracing::trace!(%addr, "accepted new incoming connection"); + } + Err(err) => { + tracing::error!(%err, "failed to accept connection"); + } + }, MailboxItem::ForwardMainchainTaskRequest( request, peer, @@ -863,6 +898,7 @@ where continue; } MailboxItem::PeerInfo(Some((addr, Some(peer_info)))) => { + tracing::trace!(%addr, ?peer_info, "mailbox item: received PeerInfo"); match peer_info { PeerConnectionInfo::Error(err) => { let err = anyhow::anyhow!(err); @@ -898,9 +934,14 @@ where })?; } PeerConnectionInfo::NewTipReady(new_tip) => { + tracing::debug!( + ?new_tip, + %addr, + "mailbox item: received NewTipReady from peer, sending on channel" + ); self.new_tip_ready_tx .unbounded_send((new_tip, Some(addr), None)) - .map_err(|_| Error::SendNewTipReady)?; + .map_err(Error::SendNewTipReady)?; } PeerConnectionInfo::NewTransaction(new_tx) => { let mut rwtxn = self.ctxt.env.write_txn()?; @@ -1006,14 +1047,6 @@ impl NetTaskHandle { } } - /// Push a tip that is ready to reorg to. - #[allow(dead_code)] - pub fn new_tip_ready(&self, new_tip: Tip) -> Result<(), Error> { - self.new_tip_ready_tx - .unbounded_send((new_tip, None, None)) - .map_err(|_| Error::SendNewTipReady) - } - /// Push a tip that is ready to reorg to, and await successful application. /// A result of Ok(true) indicates that the tip was applied and reorged /// to successfully. @@ -1022,14 +1055,13 @@ impl NetTaskHandle { &self, new_tip: Tip, ) -> Result { + tracing::debug!(?new_tip, "sending new tip ready confirm"); let (oneshot_tx, oneshot_rx) = oneshot::channel(); let () = self .new_tip_ready_tx .unbounded_send((new_tip, None, Some(oneshot_tx))) - .map_err(|_| Error::SendNewTipReady)?; - oneshot_rx - .await - .map_err(|_| Error::ReceiveReorgResultOneshot) + .map_err(Error::SendNewTipReady)?; + oneshot_rx.await.map_err(Error::ReceiveReorgResultOneshot) } } @@ -1039,6 +1071,7 @@ impl Drop for NetTaskHandle { // use `Arc::get_mut` since `Arc::into_inner` requires ownership of the // Arc, and cloning would increase the reference count if let Some(task) = Arc::get_mut(&mut self.task) { + tracing::debug!("dropping net task handle, aborting task"); task.abort() } } From 59a74394f2651388951a990e5f2730119d17a276 Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Thu, 6 Feb 2025 23:25:25 +0800 Subject: [PATCH 03/17] Do not return an error when setting seed to the same value --- lib/wallet.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/wallet.rs b/lib/wallet.rs index 5f16cc0..a843c0d 100644 --- a/lib/wallet.rs +++ b/lib/wallet.rs @@ -321,10 +321,19 @@ impl Wallet { /// Set the seed, if it does not already exist pub fn set_seed(&self, seed: &[u8; 64]) -> Result<(), Error> { - if self.has_seed()? { - Err(Error::SeedAlreadyExists) - } else { - self.overwrite_seed(seed) + let rotxn = self.env.read_txn()?; + match self.seed.try_get(&rotxn, &0)? { + Some(current_seed) => { + if current_seed == seed { + Ok(()) + } else { + Err(Error::SeedAlreadyExists) + } + } + None => { + drop(rotxn); + self.overwrite_seed(seed) + } } } From bf5fdbe09e09152eb28c7535a5cecf597ec32daa Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Thu, 6 Feb 2025 23:32:20 +0800 Subject: [PATCH 04/17] app: include mainchain URL in verify error message --- app/app.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/app.rs b/app/app.rs index 7e132f2..6e9d3b9 100644 --- a/app/app.rs +++ b/app/app.rs @@ -36,8 +36,6 @@ pub enum Error { AmountOverflow(#[from] AmountOverflowError), #[error("CUSF mainchain proto error")] CusfMainchain(#[from] plain_bitnames::types::proto::Error), - #[error("gRPC reflection client error")] - GrpcReflection(#[from] tonic::Status), #[error("io error")] Io(#[from] std::io::Error), #[error("miner error: {0}")] @@ -48,6 +46,13 @@ pub enum Error { NoCusfMainchainWalletClient, #[error(transparent)] Other(#[from] anyhow::Error), + #[error( + "Unable to verify existence of CUSF mainchain service(s) at {address}" + )] + VerifyMainchainServices { + address: std::net::SocketAddr, + source: tonic::Status, + }, #[error("wallet error")] Wallet(#[from] wallet::Error), } @@ -202,8 +207,11 @@ impl App { .concurrency_limit(256) .connect_lazy(); let (cusf_mainchain, cusf_mainchain_wallet) = if runtime - .block_on(Self::check_proto_support(transport.clone()))? - { + .block_on(Self::check_proto_support(transport.clone())) + .map_err(|err| Error::VerifyMainchainServices { + address: config.main_addr, + source: err, + })? { ( mainchain::ValidatorClient::new(transport.clone()), Some(mainchain::WalletClient::new(transport)), From 1db778951e896e47b71757f05b8e274a8767ea61 Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Thu, 6 Feb 2025 23:35:04 +0800 Subject: [PATCH 05/17] app: include caller locations in stdout logs --- app/main.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/main.rs b/app/main.rs index 135ba33..6c843a2 100644 --- a/app/main.rs +++ b/app/main.rs @@ -103,10 +103,14 @@ fn set_tracing_subscriber( }; tracing_filter::EnvFilter::builder().parse(directives_str)? }; - let line_buffer = LineBuffer::default(); + // Adding source location here means that the file name + line number + // is included, in such a way that it can be clicked on from within + // the IDE, and you're sent right to the specific line of code. Very handy! + let stdout_format = + tracing_subscriber::fmt::format().with_source_location(true); let mut stdout_layer = tracing_subscriber::fmt::layer() - .compact() - .with_line_number(true); + .with_target(true) + .event_format(stdout_format); let is_terminal = std::io::IsTerminal::is_terminal(&stdout_layer.writer()()); stdout_layer.set_ansi(is_terminal); @@ -117,6 +121,7 @@ fn set_tracing_subscriber( (Some(layer), Some(guard)) } }; + let line_buffer = LineBuffer::default(); let capture_layer = tracing_subscriber::fmt::layer() .compact() .with_line_number(true) From afe40dee955107bff702a8ce1dd09b41f39af0fc Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Thu, 6 Feb 2025 23:48:24 +0800 Subject: [PATCH 06/17] multi: refactor tokio runtime handling, listen for multiple signals --- Cargo.lock | 11 ----- app/Cargo.toml | 3 +- app/main.rs | 107 ++++++++++++++++++++++++++-------------------- app/rpc_server.rs | 1 - 4 files changed, 61 insertions(+), 61 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 79a221a..1d900f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1541,16 +1541,6 @@ dependencies = [ "cipher", ] -[[package]] -name = "ctrlc" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" -dependencies = [ - "nix", - "windows-sys 0.59.0", -] - [[package]] name = "cursor-icon" version = "1.1.0" @@ -4775,7 +4765,6 @@ dependencies = [ "blake3", "borsh", "clap", - "ctrlc", "dirs", "eframe", "either", diff --git a/app/Cargo.toml b/app/Cargo.toml index d0d3123..fddeecd 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -13,7 +13,6 @@ bitcoin = { workspace = true, features = ["serde"] } blake3 = { workspace = true } borsh = { workspace = true } clap = { workspace = true, features = ["derive"] } -ctrlc = "3.4.0" dirs = "6.0.0" eframe = "0.30.0" either = "1.13.0" @@ -33,7 +32,7 @@ shlex = "1.3.0" strum = { workspace = true, features = ["derive"] } thiserror = { workspace = true } tiny-bip39 = { workspace = true } -tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal"] } tokio-util = { workspace = true, features = ["rt"] } tonic = { workspace = true } tonic-health = "0.12.3" diff --git a/app/main.rs b/app/main.rs index 6c843a2..53282c7 100644 --- a/app/main.rs +++ b/app/main.rs @@ -1,9 +1,10 @@ #![feature(let_chains)] #![feature(try_find)] -use std::{path::Path, sync::mpsc}; +use std::path::Path; use clap::Parser as _; +use tokio::{signal::ctrl_c, sync::oneshot}; use tracing_subscriber::{ filter as tracing_filter, layer::SubscriberExt, Layer, }; @@ -137,6 +138,26 @@ fn set_tracing_subscriber( Ok((line_buffer, rolling_log_guard)) } +fn run_egui_app( + config: &crate::cli::Config, + line_buffer: LineBuffer, + app: Option, +) -> Result<(), eframe::Error> { + let native_options = eframe::NativeOptions::default(); + eframe::run_native( + "Plain Bitnames", + native_options, + Box::new(move |cc| { + Ok(Box::new(gui::EguiApp::new( + app, + cc, + line_buffer, + config.rpc_addr, + ))) + }), + ) +} + fn main() -> anyhow::Result<()> { let cli = cli::Cli::parse(); let config = cli.get_config()?; @@ -145,55 +166,47 @@ fn main() -> anyhow::Result<()> { config.log_dir.as_deref(), config.log_level, )?; - let app: Result = app::App::new(&config) - .inspect(|app| { - // spawn rpc server - app.runtime.spawn({ - let app = app.clone(); - async move { - rpc_server::run_server(app, config.rpc_addr).await.unwrap() + let (app_tx, app_rx) = oneshot::channel::(); + let app = app::App::new(&config).inspect(|app| { + // spawn rpc server + app.runtime.spawn({ + let app = app.clone(); + async move { + tracing::info!("starting RPC server at `{}`", config.rpc_addr); + if let Err(err) = + rpc_server::run_server(app, config.rpc_addr).await + { + app_tx.send(err).expect("failed to send error to app"); } - }); - }) - .inspect_err(|err| { - tracing::error!("application error: {:?}", err); + } }); - - if config.headless { - tracing::info!("Running in headless mode"); - drop(line_buffer); - let _app = app?; - // wait for ctrlc signal - let (tx, rx) = mpsc::channel(); - ctrlc::set_handler(move || { - tx.send(()).unwrap(); - }) - .expect("Error setting Ctrl-C handler"); - rx.recv().unwrap(); - tracing::info!("Received Ctrl-C signal, exiting..."); - } else { - let native_options = eframe::NativeOptions::default(); - let app: Option<_> = app.map_or_else( - |err| { + }); + if !config.headless { + let app = match app { + Ok(app) => Some(app), + Err(err) => { let err = anyhow::Error::from(err); tracing::error!("{err:#}"); None - }, - Some, - ); - eframe::run_native( - "Plain Bitnames", - native_options, - Box::new(move |cc| { - Ok(Box::new(gui::EguiApp::new( - app, - cc, - line_buffer, - config.rpc_addr, - ))) - }), - ) - .map_err(|err| anyhow::anyhow!("failed to launch egui app: {err}"))? - } - Ok(()) + } + }; + // For GUI mode we want the GUI to start, even if the app fails to start. + return run_egui_app(&config, line_buffer, app) + .map_err(|e| anyhow::anyhow!("failed to run egui app: {e:#}")); + }; + tracing::info!("Running in headless mode"); + drop(line_buffer); + // If we're headless, we want to exit hard if the app fails to start. + let app = app?; + app.runtime.block_on(async move { + tokio::select! { + Ok(_) = ctrl_c() => { + tracing::info!("Shutting down due to process interruption"); + Ok(()) + } + Ok(err) = app_rx => { + Err(anyhow::anyhow!("received error from RPC server: {err:#} ({err:?})")) + } + } + }) } diff --git a/app/rpc_server.rs b/app/rpc_server.rs index 57b2ee2..c8c2b22 100644 --- a/app/rpc_server.rs +++ b/app/rpc_server.rs @@ -354,7 +354,6 @@ pub async fn run_server( let server = Server::builder().build(rpc_addr).await?; let addr = server.local_addr()?; - tracing::info!("RPC server listening on {}", addr); let handle = server.start(RpcServerImpl { app }.into_rpc()); // In this example we don't care about doing shutdown so let's it run forever. From b23a44b1d44521efb6edf8851a4206106fbe7f69 Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Thu, 6 Feb 2025 23:56:56 +0800 Subject: [PATCH 07/17] Add integration test for unknown withdrawals --- Cargo.lock | 4 +- Cargo.toml | 4 +- integration_tests/ibd.rs | 22 +++- integration_tests/integration_test.rs | 18 ++- integration_tests/main.rs | 1 + integration_tests/setup.rs | 23 ++-- integration_tests/unknown_withdrawal.rs | 164 ++++++++++++++++++++++++ 7 files changed, 214 insertions(+), 22 deletions(-) create mode 100644 integration_tests/unknown_withdrawal.rs diff --git a/Cargo.lock b/Cargo.lock index 1d900f3..17f54df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -837,7 +837,7 @@ dependencies = [ [[package]] name = "bip300301_enforcer_integration_tests" version = "0.3.1" -source = "git+https://github.com/Ash-L2L/bip300301_enforcer?rev=0ec335eeaffef8ef693c86e7f0ff6515ebf4a124#0ec335eeaffef8ef693c86e7f0ff6515ebf4a124" +source = "git+https://github.com/Ash-L2L/bip300301_enforcer?rev=135d408213ee7309b59f49dfdc2608e6fa53db42#135d408213ee7309b59f49dfdc2608e6fa53db42" dependencies = [ "anyhow", "bdk_wallet", @@ -869,7 +869,7 @@ dependencies = [ [[package]] name = "bip300301_enforcer_lib" version = "0.3.1" -source = "git+https://github.com/Ash-L2L/bip300301_enforcer?rev=0ec335eeaffef8ef693c86e7f0ff6515ebf4a124#0ec335eeaffef8ef693c86e7f0ff6515ebf4a124" +source = "git+https://github.com/Ash-L2L/bip300301_enforcer?rev=135d408213ee7309b59f49dfdc2608e6fa53db42#135d408213ee7309b59f49dfdc2608e6fa53db42" dependencies = [ "aes-gcm", "argon2", diff --git a/Cargo.toml b/Cargo.toml index 8bc39e6..f388bb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,12 +36,12 @@ utoipa = { version = "5.2.0", default-features = false } [workspace.dependencies.bip300301_enforcer_lib] default-features = false git = "https://github.com/Ash-L2L/bip300301_enforcer" -rev = "0ec335eeaffef8ef693c86e7f0ff6515ebf4a124" +rev = "135d408213ee7309b59f49dfdc2608e6fa53db42" [workspace.dependencies.bip300301_enforcer_integration_tests] default-features = false git = "https://github.com/Ash-L2L/bip300301_enforcer" -rev = "0ec335eeaffef8ef693c86e7f0ff6515ebf4a124" +rev = "135d408213ee7309b59f49dfdc2608e6fa53db42" [workspace.dependencies.l2l-openapi] git = "https://github.com/Ash-L2L/l2l-openapi" diff --git a/integration_tests/ibd.rs b/integration_tests/ibd.rs index 27d9032..c65a72e 100644 --- a/integration_tests/ibd.rs +++ b/integration_tests/ibd.rs @@ -15,7 +15,10 @@ use plain_bitnames_app_rpc_api::RpcClient as _; use tokio::time::sleep; use tracing::Instrument as _; -use crate::{setup::PostSetup, util::BinPaths}; +use crate::{ + setup::{Init, PostSetup}, + util::BinPaths, +}; #[derive(Debug)] struct BitNamesNodes { @@ -38,15 +41,24 @@ async fn setup( ) .await?; let sidechain_sender = PostSetup::setup( - bin_paths.bitnames.clone(), + Init { + bitnames_app: bin_paths.bitnames.clone(), + data_dir_suffix: Some("sender".to_owned()), + }, &enforcer_post_setup, res_tx.clone(), ) .await?; tracing::info!("Setup BitNames send node successfully"); - let sidechain_syncer = - PostSetup::setup(bin_paths.bitnames, &enforcer_post_setup, res_tx) - .await?; + let sidechain_syncer = PostSetup::setup( + Init { + bitnames_app: bin_paths.bitnames.clone(), + data_dir_suffix: Some("syncer".to_owned()), + }, + &enforcer_post_setup, + res_tx, + ) + .await?; tracing::info!("Setup BitNames sync node successfully"); let bitnames_nodes = BitNamesNodes { sender: sidechain_sender, diff --git a/integration_tests/integration_test.rs b/integration_tests/integration_test.rs index f13fcf2..16dda88 100644 --- a/integration_tests/integration_test.rs +++ b/integration_tests/integration_test.rs @@ -4,15 +4,24 @@ use bip300301_enforcer_integration_tests::{ }; use futures::{future::BoxFuture, FutureExt}; -use crate::{ibd::ibd_trial, setup::PostSetup, util::BinPaths}; +use crate::{ + ibd::ibd_trial, + setup::{Init, PostSetup}, + unknown_withdrawal::unknown_withdrawal_trial, + util::BinPaths, +}; fn deposit_withdraw_roundtrip( bin_paths: BinPaths, ) -> AsyncTrial>> { AsyncTrial::new("deposit_withdraw_roundtrip", async move { bip300301_enforcer_integration_tests::integration_test::deposit_withdraw_roundtrip::( - bin_paths.others, Network::Regtest, Mode::Mempool, bin_paths.bitnames - ).await + bin_paths.others, Network::Regtest, Mode::Mempool, + Init { + bitnames_app: bin_paths.bitnames, + data_dir_suffix: None, + }, + ).await }.boxed()) } @@ -21,6 +30,7 @@ pub fn tests( ) -> Vec>>> { vec![ deposit_withdraw_roundtrip(bin_paths.clone()), - ibd_trial(bin_paths), + ibd_trial(bin_paths.clone()), + unknown_withdrawal_trial(bin_paths), ] } diff --git a/integration_tests/main.rs b/integration_tests/main.rs index 9049320..29737e7 100644 --- a/integration_tests/main.rs +++ b/integration_tests/main.rs @@ -4,6 +4,7 @@ use tracing_subscriber::{filter as tracing_filter, layer::SubscriberExt}; mod ibd; mod integration_test; mod setup; +mod unknown_withdrawal; mod util; #[derive(Parser)] diff --git a/integration_tests/setup.rs b/integration_tests/setup.rs index 68109e6..679b677 100644 --- a/integration_tests/setup.rs +++ b/integration_tests/setup.rs @@ -35,6 +35,12 @@ impl ReservedPorts { } } +#[derive(Debug)] +pub struct Init { + pub bitnames_app: PathBuf, + pub data_dir_suffix: Option, +} + #[derive(Debug, Error)] pub enum BmmError { #[error(transparent)] @@ -133,26 +139,25 @@ impl Sidechain for PostSetup { const SIDECHAIN_NUMBER: SidechainNumber = SidechainNumber(plain_bitnames::types::THIS_SIDECHAIN); - /// Path to bitnames_app - type Init = PathBuf; + type Init = Init; type SetupError = SetupError; async fn setup( - bitnames_app_path: Self::Init, + init: Self::Init, post_setup: &EnforcerPostSetup, res_tx: mpsc::UnboundedSender>, ) -> Result { let reserved_ports = ReservedPorts::new()?; - let bitnames_dir = post_setup.out_dir.path().join(format!( - "bitnames-{}-{}", - reserved_ports.net.port(), - reserved_ports.rpc.port() - )); + let bitnames_dir = if let Some(suffix) = init.data_dir_suffix { + post_setup.out_dir.path().join(format!("bitnames-{suffix}")) + } else { + post_setup.out_dir.path().join("bitnames") + }; std::fs::create_dir(&bitnames_dir) .map_err(Self::SetupError::CreateBitNamesDir)?; let bitnames_app = BitNamesApp { - path: bitnames_app_path, + path: init.bitnames_app, data_dir: bitnames_dir, log_level: Some(tracing::Level::TRACE), mainchain_grpc_port: post_setup diff --git a/integration_tests/unknown_withdrawal.rs b/integration_tests/unknown_withdrawal.rs new file mode 100644 index 0000000..290e029 --- /dev/null +++ b/integration_tests/unknown_withdrawal.rs @@ -0,0 +1,164 @@ +//! Test an unknown withdrawal event + +use bip300301_enforcer_integration_tests::{ + integration_test::{ + activate_sidechain, deposit, fund_enforcer, propose_sidechain, + withdraw_succeed, + }, + setup::{ + setup as setup_enforcer, Mode, Network, PostSetup as EnforcerPostSetup, + Sidechain as _, + }, + util::{AbortOnDrop, AsyncTrial}, +}; +use futures::{ + channel::mpsc, future::BoxFuture, FutureExt as _, StreamExt as _, +}; +use plain_bitnames_app_rpc_api::RpcClient as _; +use tokio::time::sleep; +use tracing::Instrument as _; + +use crate::{ + setup::{Init, PostSetup}, + util::BinPaths, +}; + +/// Initial setup for the test +async fn setup( + bin_paths: &BinPaths, + res_tx: mpsc::UnboundedSender>, +) -> anyhow::Result { + let mut enforcer_post_setup = setup_enforcer( + &bin_paths.others, + Network::Regtest, + Mode::Mempool, + res_tx.clone(), + ) + .await?; + let () = propose_sidechain::(&mut enforcer_post_setup).await?; + tracing::info!("Proposed sidechain successfully"); + let () = activate_sidechain::(&mut enforcer_post_setup).await?; + tracing::info!("Activated sidechain successfully"); + let () = fund_enforcer::(&mut enforcer_post_setup).await?; + Ok(enforcer_post_setup) +} + +const DEPOSIT_AMOUNT: bitcoin::Amount = bitcoin::Amount::from_sat(21_000_000); +const DEPOSIT_FEE: bitcoin::Amount = bitcoin::Amount::from_sat(1_000_000); +const WITHDRAW_AMOUNT: bitcoin::Amount = bitcoin::Amount::from_sat(18_000_000); +const WITHDRAW_FEE: bitcoin::Amount = bitcoin::Amount::from_sat(1_000_000); + +async fn unknown_withdrawal_task( + bin_paths: BinPaths, + res_tx: mpsc::UnboundedSender>, +) -> anyhow::Result<()> { + let mut enforcer_post_setup = setup(&bin_paths, res_tx.clone()).await?; + let mut sidechain_withdrawer = PostSetup::setup( + Init { + bitnames_app: bin_paths.bitnames.clone(), + data_dir_suffix: Some("withdrawer".to_owned()), + }, + &enforcer_post_setup, + res_tx.clone(), + ) + .await?; + tracing::info!("Setup BitNames withdrawer node successfully"); + let withdrawer_deposit_address = + sidechain_withdrawer.get_deposit_address().await?; + let () = deposit( + &mut enforcer_post_setup, + &mut sidechain_withdrawer, + &withdrawer_deposit_address, + DEPOSIT_AMOUNT, + DEPOSIT_FEE, + ) + .await?; + tracing::info!("Deposited to sidechain successfully"); + let () = withdraw_succeed( + &mut enforcer_post_setup, + &mut sidechain_withdrawer, + WITHDRAW_AMOUNT, + WITHDRAW_FEE, + bitcoin::Amount::ZERO, + ) + .await?; + tracing::info!("Withdrawal succeeded"); + // New sidechain node, starting from scratch + let mut sidechain_successor = PostSetup::setup( + Init { + bitnames_app: bin_paths.bitnames, + data_dir_suffix: Some("successor".to_owned()), + }, + &enforcer_post_setup, + res_tx, + ) + .await?; + tracing::info!("Setup BitNames successor node successfully"); + tracing::debug!("BMM 1 block"); + sidechain_successor + .bmm_single(&mut enforcer_post_setup) + .await?; + tracing::debug!("Checking that successor sidechain has exactly 1 block"); + let successor_block_count = + sidechain_successor.rpc_client.getblockcount().await?; + anyhow::ensure!(successor_block_count == 1); + tracing::debug!("Checking that successor sidechain has no deposit UTXOs"); + let successor_utxos = sidechain_successor.rpc_client.list_utxos().await?; + anyhow::ensure!(successor_utxos.is_empty()); + let successor_deposit_address = + sidechain_successor.get_deposit_address().await?; + let () = deposit( + &mut enforcer_post_setup, + &mut sidechain_successor, + &successor_deposit_address, + DEPOSIT_AMOUNT, + DEPOSIT_FEE, + ) + .await?; + tracing::info!("Deposited to sidechain successfully"); + tracing::debug!("Checking that withdrawer sidechain recognizes deposit"); + { + let withdrawer_utxos_count = + sidechain_withdrawer.rpc_client.list_utxos().await?.len(); + sidechain_withdrawer + .bmm_single(&mut enforcer_post_setup) + .await?; + let utxos_count_delta = sidechain_withdrawer + .rpc_client + .list_utxos() + .await? + .len() + .checked_sub(withdrawer_utxos_count); + anyhow::ensure!(utxos_count_delta == Some(1)); + } + drop(sidechain_successor); + drop(sidechain_withdrawer); + tracing::info!("Removing {}", enforcer_post_setup.out_dir.path().display()); + drop(enforcer_post_setup.tasks); + // Wait for tasks to die + sleep(std::time::Duration::from_secs(1)).await; + enforcer_post_setup.out_dir.cleanup()?; + Ok(()) +} + +async fn unknown_withdrawal(bin_paths: BinPaths) -> anyhow::Result<()> { + let (res_tx, mut res_rx) = mpsc::unbounded(); + let _test_task: AbortOnDrop<()> = tokio::task::spawn({ + let res_tx = res_tx.clone(); + async move { + let res = unknown_withdrawal_task(bin_paths, res_tx.clone()).await; + let _send_err: Result<(), _> = res_tx.unbounded_send(res); + } + .in_current_span() + }) + .into(); + res_rx.next().await.ok_or_else(|| { + anyhow::anyhow!("Unexpected end of test task result stream") + })? +} + +pub fn unknown_withdrawal_trial( + bin_paths: BinPaths, +) -> AsyncTrial>> { + AsyncTrial::new("unknown_withdrawal", unknown_withdrawal(bin_paths).boxed()) +} From 13461b46d604485b521b4362e44ccbc288cb7c8a Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Fri, 7 Feb 2025 00:21:05 +0800 Subject: [PATCH 08/17] state: handle unknown withdrawals better --- lib/state/error.rs | 7 + lib/state/mod.rs | 424 ++++++++++++++++++++++++++++----------------- 2 files changed, 272 insertions(+), 159 deletions(-) diff --git a/lib/state/error.rs b/lib/state/error.rs index fea4ec9..728f026 100644 --- a/lib/state/error.rs +++ b/lib/state/error.rs @@ -95,6 +95,13 @@ pub enum Error { }, #[error("Unknown withdrawal bundle: {m6id}")] UnknownWithdrawalBundle { m6id: M6id }, + #[error( + "Unknown withdrawal bundle confirmed in {event_block_hash}: {m6id}" + )] + UnknownWithdrawalBundleConfirmed { + event_block_hash: bitcoin::BlockHash, + m6id: M6id, + }, #[error("utxo double spent")] UtxoDoubleSpent, #[error(transparent)] diff --git a/lib/state/mod.rs b/lib/state/mod.rs index ccbfe05..143ec3f 100644 --- a/lib/state/mod.rs +++ b/lib/state/mod.rs @@ -1,5 +1,6 @@ use std::collections::{BTreeMap, HashMap, HashSet}; +use fallible_iterator::FallibleIterator as _; use futures::Stream; use heed::{types::SerdeBincode, Database, RoTxn, RwTxn}; @@ -8,14 +9,14 @@ use crate::{ types::{ self, constants, hashes::{self, BitName}, - proto::mainchain::TwoWayPegData, + proto::mainchain::{BlockEvent, TwoWayPegData}, Address, AggregatedWithdrawal, AmountOverflowError, Authorized, AuthorizedTransaction, BatchIcannRegistrationData, BitNameDataUpdates, BitNameSeqId, BlockHash, Body, FilledOutput, FilledOutputContent, FilledTransaction, GetAddress as _, GetValue as _, Hash, Header, InPoint, M6id, MerkleRoot, OutPoint, OutputContent, SpentOutput, Transaction, TxData, Txid, Verify as _, WithdrawalBundle, - WithdrawalBundleStatus, + WithdrawalBundleEvent, WithdrawalBundleStatus, }, util::{EnvExt, UnitKey, Watchable, WatchableDb}, }; @@ -33,7 +34,7 @@ pub const WITHDRAWAL_BUNDLE_FAILURE_GAP: u32 = 4; type WithdrawalBundlesDb = Database< SerdeBincode, SerdeBincode<( - WithdrawalBundle, + Option, RollBack>, )>, >; @@ -55,10 +56,14 @@ pub struct State { /// Pending withdrawal bundle and block height pub pending_withdrawal_bundle: Database, SerdeBincode<(WithdrawalBundle, u32)>>, + /// Latest failed (known) withdrawal bundle latest_failed_withdrawal_bundle: Database< SerdeBincode, SerdeBincode>>, >, + /// Withdrawal bundles and their status. + /// Some withdrawal bundles may be unknown. + /// in which case they are `None`. withdrawal_bundles: WithdrawalBundlesDb, /// deposit blocks and the height at which they were applied, keyed sequentially pub deposit_blocks: @@ -226,18 +231,18 @@ impl State { pub fn get_latest_failed_withdrawal_bundle( &self, rotxn: &RoTxn, - ) -> Result, Error> { + ) -> Result, Error> { let Some(latest_failed_m6id) = self.latest_failed_withdrawal_bundle.get(rotxn, &UnitKey)? else { return Ok(None); }; let latest_failed_m6id = latest_failed_m6id.latest().value; - let (bundle, bundle_status) = self.withdrawal_bundles.get(rotxn, &latest_failed_m6id)? + let (_bundle, bundle_status) = self.withdrawal_bundles.get(rotxn, &latest_failed_m6id)? .expect("Inconsistent DBs: latest failed m6id should exist in withdrawal_bundles"); let bundle_status = bundle_status.latest(); assert_eq!(bundle_status.value, WithdrawalBundleStatus::Failed); - Ok(Some((bundle_status.height, bundle))) + Ok(Some((bundle_status.height, latest_failed_m6id))) } fn fill_transaction( @@ -646,6 +651,242 @@ impl State { Ok(block_hash) } + fn connect_2wpd_withdrawal_bundle_submitted( + &self, + rwtxn: &mut RwTxn, + block_height: u32, + event_block_hash: &bitcoin::BlockHash, + m6id: M6id, + ) -> Result<(), Error> { + if let Some((bundle, bundle_block_height)) = + self.pending_withdrawal_bundle.get(rwtxn, &UnitKey)? + && bundle.compute_m6id() == m6id + { + assert_eq!(bundle_block_height, block_height - 1); + tracing::debug!( + %block_height, + %m6id, + "Withdrawal bundle successfully submitted" + ); + for (outpoint, spend_output) in bundle.spend_utxos() { + self.utxos.delete(rwtxn, outpoint)?; + let spent_output = SpentOutput { + output: spend_output.clone(), + inpoint: InPoint::Withdrawal { m6id }, + }; + self.stxos.put(rwtxn, outpoint, &spent_output)?; + } + self.withdrawal_bundles.put( + rwtxn, + &m6id, + &( + Some(bundle), + RollBack::>::new( + WithdrawalBundleStatus::Submitted, + block_height, + ), + ), + )?; + self.pending_withdrawal_bundle.delete(rwtxn, &UnitKey)?; + } else if let Some((_bundle, bundle_status)) = + self.withdrawal_bundles.get(rwtxn, &m6id)? + { + // Already applied + assert_eq!( + bundle_status.earliest().value, + WithdrawalBundleStatus::Submitted + ); + } else { + tracing::warn!( + %event_block_hash, + %m6id, + "Unknown withdrawal bundle submitted" + ); + self.withdrawal_bundles.put( + rwtxn, + &m6id, + &( + None, + RollBack::>::new( + WithdrawalBundleStatus::Submitted, + block_height, + ), + ), + )?; + }; + Ok(()) + } + + fn connect_2wpd_withdrawal_bundle_confirmed( + &self, + rwtxn: &mut RwTxn, + block_height: u32, + event_block_hash: &bitcoin::BlockHash, + m6id: M6id, + ) -> Result<(), Error> { + let (bundle, mut bundle_status) = self + .withdrawal_bundles + .get(rwtxn, &m6id)? + .ok_or(Error::UnknownWithdrawalBundle { m6id })?; + if bundle_status.latest().value == WithdrawalBundleStatus::Confirmed { + // Already applied + return Ok(()); + } + assert_eq!( + bundle_status.latest().value, + WithdrawalBundleStatus::Submitted + ); + // If an unknown bundle is confirmed, all UTXOs older than the + // bundle submission are potentially spent. + // This is only accepted in the case that block height is 0, + // and so no UTXOs could possibly have been double-spent yet. + // In this case, ALL UTXOs are considered spent. + if bundle.is_none() { + if block_height == 0 { + tracing::warn!( + %event_block_hash, + %m6id, + "Unknown withdrawal bundle confirmed, marking all UTXOs as spent" + ); + let utxos: Vec<_> = + self.utxos.iter(rwtxn)?.collect::>()?; + for (outpoint, output) in utxos { + let spent_output = SpentOutput { + output, + inpoint: InPoint::Withdrawal { m6id }, + }; + self.stxos.put(rwtxn, &outpoint, &spent_output)?; + } + self.utxos.clear(rwtxn)?; + } else { + return Err(Error::UnknownWithdrawalBundleConfirmed { + event_block_hash: *event_block_hash, + m6id, + }); + } + } + bundle_status + .push(WithdrawalBundleStatus::Confirmed, block_height) + .expect("Push confirmed status should be valid"); + self.withdrawal_bundles + .put(rwtxn, &m6id, &(bundle, bundle_status))?; + Ok(()) + } + + fn connect_2wpd_withdrawal_bundle_failed( + &self, + rwtxn: &mut RwTxn, + block_height: u32, + m6id: M6id, + ) -> Result<(), Error> { + tracing::debug!( + %block_height, + %m6id, + "Handling failed withdrawal bundle"); + let (bundle, mut bundle_status) = self + .withdrawal_bundles + .get(rwtxn, &m6id)? + .ok_or_else(|| Error::UnknownWithdrawalBundle { m6id })?; + if bundle_status.latest().value == WithdrawalBundleStatus::Failed { + // Already applied + return Ok(()); + } + assert_eq!( + bundle_status.latest().value, + WithdrawalBundleStatus::Submitted + ); + bundle_status + .push(WithdrawalBundleStatus::Failed, block_height) + .expect("Push failed status should be valid"); + if let Some(bundle) = &bundle { + for (outpoint, output) in bundle.spend_utxos() { + self.stxos.delete(rwtxn, outpoint)?; + self.utxos.put(rwtxn, outpoint, output)?; + } + let latest_failed_m6id = if let Some(mut latest_failed_m6id) = + self.latest_failed_withdrawal_bundle.get(rwtxn, &UnitKey)? + { + latest_failed_m6id + .push(m6id, block_height) + .expect("Push latest failed m6id should be valid"); + latest_failed_m6id + } else { + RollBack::>::new(m6id, block_height) + }; + self.latest_failed_withdrawal_bundle.put( + rwtxn, + &UnitKey, + &latest_failed_m6id, + )?; + } + self.withdrawal_bundles + .put(rwtxn, &m6id, &(bundle, bundle_status))?; + Ok(()) + } + + fn connect_2wpd_withdrawal_bundle_event( + &self, + rwtxn: &mut RwTxn, + block_height: u32, + event_block_hash: &bitcoin::BlockHash, + event: &WithdrawalBundleEvent, + ) -> Result<(), Error> { + match event.status { + WithdrawalBundleStatus::Submitted => self + .connect_2wpd_withdrawal_bundle_submitted( + rwtxn, + block_height, + event_block_hash, + event.m6id, + ), + WithdrawalBundleStatus::Confirmed => self + .connect_2wpd_withdrawal_bundle_confirmed( + rwtxn, + block_height, + event_block_hash, + event.m6id, + ), + WithdrawalBundleStatus::Failed => self + .connect_2wpd_withdrawal_bundle_failed( + rwtxn, + block_height, + event.m6id, + ), + } + } + + fn connect_2wpd_event( + &self, + rwtxn: &mut RwTxn, + block_height: u32, + latest_deposit_block_hash: &mut Option, + latest_withdrawal_bundle_event_block_hash: &mut Option< + bitcoin::BlockHash, + >, + event_block_hash: bitcoin::BlockHash, + event: &BlockEvent, + ) -> Result<(), Error> { + match event { + BlockEvent::Deposit(deposit) => { + let outpoint = OutPoint::Deposit(deposit.outpoint); + let output = deposit.output.clone(); + self.utxos.put(rwtxn, &outpoint, &output)?; + *latest_deposit_block_hash = Some(event_block_hash); + } + BlockEvent::WithdrawalBundle(withdrawal_bundle_event) => { + let () = self.connect_2wpd_withdrawal_bundle_event( + rwtxn, + block_height, + &event_block_hash, + withdrawal_bundle_event, + )?; + *latest_withdrawal_bundle_event_block_hash = + Some(event_block_hash); + } + } + Ok(()) + } + pub fn connect_two_way_peg_data( &self, rwtxn: &mut RwTxn, @@ -654,9 +895,23 @@ impl State { let block_height = self.try_get_height(rwtxn)?.ok_or(Error::NoTip)?; tracing::trace!(%block_height, "Connecting 2WPD..."); // Handle deposits. - if let Some(latest_deposit_block_hash) = - two_way_peg_data.latest_deposit_block_hash() + let mut latest_deposit_block_hash = None; + let mut latest_withdrawal_bundle_event_block_hash = None; + for (event_block_hash, event_block_info) in &two_way_peg_data.block_info { + for event in &event_block_info.events { + let () = self.connect_2wpd_event( + rwtxn, + block_height, + &mut latest_deposit_block_hash, + &mut latest_withdrawal_bundle_event_block_hash, + *event_block_hash, + event, + )?; + } + } + // Handle deposits. + if let Some(latest_deposit_block_hash) = latest_deposit_block_hash { let deposit_block_seq_idx = self .deposit_blocks .last(rwtxn)? @@ -667,18 +922,9 @@ impl State { &(latest_deposit_block_hash, block_height), )?; } - for deposit in two_way_peg_data - .deposits() - .flat_map(|(_, deposits)| deposits) - { - let outpoint = OutPoint::Deposit(deposit.outpoint); - let output = deposit.output.clone(); - self.utxos.put(rwtxn, &outpoint, &output)?; - } - // Handle withdrawals if let Some(latest_withdrawal_bundle_event_block_hash) = - two_way_peg_data.latest_withdrawal_bundle_event_block_hash() + latest_withdrawal_bundle_event_block_hash { let withdrawal_bundle_event_block_seq_idx = self .withdrawal_bundle_event_blocks @@ -687,7 +933,7 @@ impl State { self.withdrawal_bundle_event_blocks.put( rwtxn, &withdrawal_bundle_event_block_seq_idx, - &(*latest_withdrawal_bundle_event_block_hash, block_height), + &(latest_withdrawal_bundle_event_block_hash, block_height), )?; } let last_withdrawal_bundle_failure_height = self @@ -717,146 +963,6 @@ impl State { ); } } - for (_, event) in two_way_peg_data.withdrawal_bundle_events() { - match event.status { - WithdrawalBundleStatus::Submitted => { - let Some((bundle, bundle_block_height)) = - self.pending_withdrawal_bundle.get(rwtxn, &UnitKey)? - else { - if let Some((_bundle, bundle_status)) = - self.withdrawal_bundles.get(rwtxn, &event.m6id)? - { - // Already applied - assert_eq!( - bundle_status.earliest().value, - WithdrawalBundleStatus::Submitted - ); - continue; - } - return Err(Error::UnknownWithdrawalBundle { - m6id: event.m6id, - }); - }; - assert_eq!(bundle_block_height, block_height - 1); - if bundle.compute_m6id() != event.m6id { - return Err(Error::UnknownWithdrawalBundle { - m6id: event.m6id, - }); - } - tracing::debug!( - %block_height, - m6id = %event.m6id, - "Withdrawal bundle successfully submitted" - ); - for (outpoint, spend_output) in bundle.spend_utxos() { - self.utxos.delete(rwtxn, outpoint)?; - let spent_output = SpentOutput { - output: spend_output.clone(), - inpoint: InPoint::Withdrawal { m6id: event.m6id }, - }; - self.stxos.put(rwtxn, outpoint, &spent_output)?; - } - self.withdrawal_bundles.put( - rwtxn, - &event.m6id, - &( - bundle, - RollBack::>::new( - WithdrawalBundleStatus::Submitted, - block_height, - ), - ), - )?; - self.pending_withdrawal_bundle.delete(rwtxn, &UnitKey)?; - } - WithdrawalBundleStatus::Confirmed => { - let Some((bundle, mut bundle_status)) = - self.withdrawal_bundles.get(rwtxn, &event.m6id)? - else { - return Err(Error::UnknownWithdrawalBundle { - m6id: event.m6id, - }); - }; - if bundle_status.latest().value - == WithdrawalBundleStatus::Confirmed - { - // Already applied - continue; - } else { - assert_eq!( - bundle_status.latest().value, - WithdrawalBundleStatus::Submitted - ); - } - bundle_status - .push(WithdrawalBundleStatus::Confirmed, block_height) - .expect("Push confirmed status should be valid"); - self.withdrawal_bundles.put( - rwtxn, - &event.m6id, - &(bundle, bundle_status), - )?; - } - WithdrawalBundleStatus::Failed => { - tracing::debug!( - %block_height, - m6id = %event.m6id, - "Handling failed withdrawal bundle"); - let Some((bundle, mut bundle_status)) = - self.withdrawal_bundles.get(rwtxn, &event.m6id)? - else { - return Err(Error::UnknownWithdrawalBundle { - m6id: event.m6id, - }); - }; - if bundle_status.latest().value - == WithdrawalBundleStatus::Failed - { - // Already applied - continue; - } else { - assert_eq!( - bundle_status.latest().value, - WithdrawalBundleStatus::Submitted - ); - } - bundle_status - .push(WithdrawalBundleStatus::Failed, block_height) - .expect("Push failed status should be valid"); - for (outpoint, output) in bundle.spend_utxos() { - self.stxos.delete(rwtxn, outpoint)?; - self.utxos.put(rwtxn, outpoint, output)?; - } - let latest_failed_m6id = - if let Some(mut latest_failed_m6id) = self - .latest_failed_withdrawal_bundle - .get(rwtxn, &UnitKey)? - { - latest_failed_m6id - .push(event.m6id, block_height) - .expect( - "Push latest failed m6id should be valid", - ); - latest_failed_m6id - } else { - RollBack::>::new( - event.m6id, - block_height, - ) - }; - self.latest_failed_withdrawal_bundle.put( - rwtxn, - &UnitKey, - &latest_failed_m6id, - )?; - self.withdrawal_bundles.put( - rwtxn, - &event.m6id, - &(bundle, bundle_status), - )?; - } - } - } Ok(()) } From 01dafdebb6b42ec3f254e8aa3def0247c10bdf58 Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Fri, 7 Feb 2025 00:44:06 +0800 Subject: [PATCH 09/17] state: refactor --- lib/state/block.rs | 337 +++++++++++ lib/state/mod.rs | 1025 ++------------------------------- lib/state/two_way_peg_data.rs | 649 +++++++++++++++++++++ 3 files changed, 1039 insertions(+), 972 deletions(-) create mode 100644 lib/state/block.rs create mode 100644 lib/state/two_way_peg_data.rs diff --git a/lib/state/block.rs b/lib/state/block.rs new file mode 100644 index 0000000..c863607 --- /dev/null +++ b/lib/state/block.rs @@ -0,0 +1,337 @@ +//! Connect and disconnect blocks + +use std::collections::HashSet; + +use heed::{RoTxn, RwTxn}; + +use crate::{ + state::{error, Error, State}, + types::{ + AmountOverflowError, Authorization, Body, FilledOutput, + FilledOutputContent, FilledTransaction, GetAddress as _, GetValue as _, + Header, InPoint, MerkleRoot, OutPoint, OutputContent, SpentOutput, + TxData, Verify as _, + }, + util::UnitKey, +}; + +/// Validate a block, returning the merkle root and fees +pub fn validate( + state: &State, + rotxn: &RoTxn, + header: &Header, + body: &Body, +) -> Result<(bitcoin::Amount, MerkleRoot), Error> { + let tip_hash = state.try_get_tip(rotxn)?; + if header.prev_side_hash != tip_hash { + let err = error::InvalidHeader::PrevSideHash { + expected: tip_hash, + received: header.prev_side_hash, + }; + return Err(Error::InvalidHeader(err)); + }; + let mut coinbase_value = bitcoin::Amount::ZERO; + for output in &body.coinbase { + coinbase_value = coinbase_value + .checked_add(output.get_value()) + .ok_or(AmountOverflowError)?; + } + let mut total_fees = bitcoin::Amount::ZERO; + let mut spent_utxos = HashSet::new(); + let filled_txs: Vec<_> = body + .transactions + .iter() + .map(|t| state.fill_transaction(rotxn, t)) + .collect::>()?; + for filled_tx in &filled_txs { + for input in &filled_tx.transaction.inputs { + if spent_utxos.contains(input) { + return Err(Error::UtxoDoubleSpent); + } + spent_utxos.insert(*input); + } + total_fees = total_fees + .checked_add(state.validate_filled_transaction(rotxn, filled_tx)?) + .ok_or(AmountOverflowError)?; + } + if coinbase_value > total_fees { + return Err(Error::NotEnoughFees); + } + let merkle_root = Body::compute_merkle_root( + body.coinbase.as_slice(), + filled_txs.as_slice(), + )? + .ok_or(Error::MerkleRoot)?; + if merkle_root != header.merkle_root { + let err = Error::InvalidBody { + expected: header.merkle_root, + computed: merkle_root, + }; + return Err(err); + } + let spent_utxos = filled_txs + .iter() + .flat_map(|t| t.spent_utxos_requiring_auth().into_iter()); + for (authorization, spent_utxo) in + body.authorizations.iter().zip(spent_utxos) + { + if authorization.get_address() != spent_utxo.address { + return Err(Error::WrongPubKeyForAddress); + } + } + if Authorization::verify_body(body).is_err() { + return Err(Error::AuthorizationError); + } + Ok((total_fees, merkle_root)) +} + +pub fn connect( + state: &State, + rwtxn: &mut RwTxn, + header: &Header, + body: &Body, +) -> Result { + let height = state.try_get_height(rwtxn)?.map_or(0, |height| height + 1); + let tip_hash = state.try_get_tip(rwtxn)?; + if tip_hash != header.prev_side_hash { + let err = error::InvalidHeader::PrevSideHash { + expected: tip_hash, + received: header.prev_side_hash, + }; + return Err(Error::InvalidHeader(err)); + } + for (vout, output) in body.coinbase.iter().enumerate() { + let outpoint = OutPoint::Coinbase { + merkle_root: header.merkle_root, + vout: vout as u32, + }; + let filled_content = match output.content.clone() { + OutputContent::Bitcoin(value) => { + FilledOutputContent::Bitcoin(value) + } + OutputContent::Withdrawal { + value, + main_fee, + main_address, + } => FilledOutputContent::BitcoinWithdrawal { + value, + main_fee, + main_address, + }, + OutputContent::BitName | OutputContent::BitNameReservation => { + return Err(Error::BadCoinbaseOutputContent); + } + }; + let filled_output = FilledOutput { + address: output.address, + content: filled_content, + memo: output.memo.clone(), + }; + state.utxos.put(rwtxn, &outpoint, &filled_output)?; + } + let mut filled_txs: Vec = Vec::new(); + for transaction in &body.transactions { + let filled_tx = state.fill_transaction(rwtxn, transaction)?; + let txid = filled_tx.txid(); + for (vin, input) in filled_tx.inputs().iter().enumerate() { + let spent_output = state + .utxos + .get(rwtxn, input)? + .ok_or(Error::NoUtxo { outpoint: *input })?; + let spent_output = SpentOutput { + output: spent_output, + inpoint: InPoint::Regular { + txid, + vin: vin as u32, + }, + }; + state.utxos.delete(rwtxn, input)?; + state.stxos.put(rwtxn, input, &spent_output)?; + } + let filled_outputs = filled_tx + .filled_outputs() + .ok_or(Error::FillTxOutputContentsFailed)?; + for (vout, filled_output) in filled_outputs.iter().enumerate() { + let outpoint = OutPoint::Regular { + txid, + vout: vout as u32, + }; + state.utxos.put(rwtxn, &outpoint, filled_output)?; + } + match &transaction.data { + None => (), + Some(TxData::BitNameReservation { commitment }) => { + state.bitname_reservations.put(rwtxn, &txid, commitment)?; + } + Some(TxData::BitNameRegistration { + name_hash, + revealed_nonce: _, + bitname_data, + }) => { + let () = state.apply_bitname_registration( + rwtxn, + &filled_tx, + *name_hash, + bitname_data, + height, + )?; + } + Some(TxData::BitNameUpdate(bitname_updates)) => { + let () = state.apply_bitname_updates( + rwtxn, + &filled_tx, + (**bitname_updates).clone(), + height, + )?; + } + Some(TxData::BatchIcann(batch_icann_data)) => { + let () = state.apply_batch_icann( + rwtxn, + &filled_tx, + batch_icann_data, + )?; + } + } + filled_txs.push(filled_tx); + } + let merkle_root = Body::compute_merkle_root( + body.coinbase.as_slice(), + filled_txs.as_slice(), + )? + .ok_or(Error::MerkleRoot)?; + if merkle_root != header.merkle_root { + let err = Error::InvalidBody { + expected: header.merkle_root, + computed: merkle_root, + }; + return Err(err); + } + let block_hash = header.hash(); + state.tip.put(rwtxn, &UnitKey, &block_hash)?; + state.height.put(rwtxn, &UnitKey, &height)?; + Ok(merkle_root) +} + +pub fn disconnect_tip( + state: &State, + rwtxn: &mut RwTxn, + header: &Header, + body: &Body, +) -> Result<(), Error> { + let tip_hash = state.tip.try_get(rwtxn, &UnitKey)?.ok_or(Error::NoTip)?; + if tip_hash != header.hash() { + let err = error::InvalidHeader::BlockHash { + expected: tip_hash, + computed: header.hash(), + }; + return Err(Error::InvalidHeader(err)); + } + let height = state + .try_get_height(rwtxn)? + .expect("Height should not be None"); + // revert txs, last-to-first + let mut filled_txs: Vec = Vec::new(); + body.transactions.iter().rev().try_for_each(|tx| { + let txid = tx.txid(); + let filled_tx = state.fill_transaction_from_stxos(rwtxn, tx.clone())?; + // revert transaction effects + match &tx.data { + None => (), + Some(TxData::BitNameReservation { .. }) => { + if !state.bitname_reservations.delete(rwtxn, &txid)? { + return Err(Error::MissingReservation { txid }); + } + } + Some(TxData::BitNameRegistration { + name_hash, + revealed_nonce: _, + bitname_data: _, + }) => { + let () = state.revert_bitname_registration( + rwtxn, &filled_tx, *name_hash, + )?; + } + Some(TxData::BitNameUpdate(bitname_updates)) => { + let () = state.revert_bitname_updates( + rwtxn, + &filled_tx, + (**bitname_updates).clone(), + height - 1, + )?; + } + Some(TxData::BatchIcann(batch_icann_data)) => { + let () = state.revert_batch_icann( + rwtxn, + &filled_tx, + batch_icann_data, + )?; + } + } + filled_txs.push(filled_tx); + // delete UTXOs, last-to-first + tx.outputs.iter().enumerate().rev().try_for_each( + |(vout, _output)| { + let outpoint = OutPoint::Regular { + txid, + vout: vout as u32, + }; + if state.utxos.delete(rwtxn, &outpoint)? { + Ok(()) + } else { + Err(Error::NoUtxo { outpoint }) + } + }, + )?; + // unspend STXOs, last-to-first + tx.inputs.iter().rev().try_for_each(|outpoint| { + if let Some(spent_output) = state.stxos.get(rwtxn, outpoint)? { + state.stxos.delete(rwtxn, outpoint)?; + state.utxos.put(rwtxn, outpoint, &spent_output.output)?; + Ok(()) + } else { + Err(Error::NoStxo { + outpoint: *outpoint, + }) + } + }) + })?; + filled_txs.reverse(); + // delete coinbase UTXOs, last-to-first + body.coinbase.iter().enumerate().rev().try_for_each( + |(vout, _output)| { + let outpoint = OutPoint::Coinbase { + merkle_root: header.merkle_root, + vout: vout as u32, + }; + if state.utxos.delete(rwtxn, &outpoint)? { + Ok(()) + } else { + Err(Error::NoUtxo { outpoint }) + } + }, + )?; + let merkle_root = Body::compute_merkle_root( + body.coinbase.as_slice(), + filled_txs.as_slice(), + )? + .ok_or(Error::MerkleRoot)?; + if merkle_root != header.merkle_root { + let err = Error::InvalidBody { + expected: header.merkle_root, + computed: merkle_root, + }; + return Err(err); + } + match (header.prev_side_hash, height) { + (None, 0) => { + state.tip.delete(rwtxn, &UnitKey)?; + state.height.delete(rwtxn, &UnitKey)?; + } + (None, _) | (_, 0) => return Err(Error::NoTip), + (Some(prev_side_hash), height) => { + state.tip.put(rwtxn, &UnitKey, &prev_side_hash)?; + state.height.put(rwtxn, &UnitKey, &(height - 1))?; + } + } + Ok(()) +} diff --git a/lib/state/mod.rs b/lib/state/mod.rs index 143ec3f..baf5727 100644 --- a/lib/state/mod.rs +++ b/lib/state/mod.rs @@ -1,6 +1,5 @@ -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{HashMap, HashSet}; -use fallible_iterator::FallibleIterator as _; use futures::Stream; use heed::{types::SerdeBincode, Database, RoTxn, RwTxn}; @@ -9,21 +8,22 @@ use crate::{ types::{ self, constants, hashes::{self, BitName}, - proto::mainchain::{BlockEvent, TwoWayPegData}, - Address, AggregatedWithdrawal, AmountOverflowError, Authorized, - AuthorizedTransaction, BatchIcannRegistrationData, BitNameDataUpdates, - BitNameSeqId, BlockHash, Body, FilledOutput, FilledOutputContent, - FilledTransaction, GetAddress as _, GetValue as _, Hash, Header, - InPoint, M6id, MerkleRoot, OutPoint, OutputContent, SpentOutput, - Transaction, TxData, Txid, Verify as _, WithdrawalBundle, - WithdrawalBundleEvent, WithdrawalBundleStatus, + proto::mainchain::TwoWayPegData, + Address, AmountOverflowError, Authorized, AuthorizedTransaction, + BatchIcannRegistrationData, BitNameDataUpdates, BitNameSeqId, + BlockHash, Body, FilledOutput, FilledTransaction, GetAddress as _, + GetValue as _, Hash, Header, InPoint, M6id, MerkleRoot, OutPoint, + SpentOutput, Transaction, Txid, Verify as _, WithdrawalBundle, + WithdrawalBundleStatus, }, util::{EnvExt, UnitKey, Watchable, WatchableDb}, }; mod bitname_data; +mod block; pub mod error; mod rollback; +mod two_way_peg_data; use bitname_data::BitNameData; pub use error::Error; @@ -307,95 +307,6 @@ impl State { }) } - fn collect_withdrawal_bundle( - &self, - txn: &RoTxn, - block_height: u32, - ) -> Result, Error> { - // Weight of a bundle with 0 outputs. - const BUNDLE_0_WEIGHT: u64 = 504; - // Weight of a single output. - const OUTPUT_WEIGHT: u64 = 128; - // Turns out to be 3121. - const MAX_BUNDLE_OUTPUTS: usize = - ((bitcoin::policy::MAX_STANDARD_TX_WEIGHT as u64 - BUNDLE_0_WEIGHT) - / OUTPUT_WEIGHT) as usize; - - // Aggregate all outputs by destination. - // destination -> (value, mainchain fee, spent_utxos) - let mut address_to_aggregated_withdrawal = HashMap::< - bitcoin::Address, - AggregatedWithdrawal, - >::new(); - for item in self.utxos.iter(txn)? { - let (outpoint, output) = item?; - if let FilledOutputContent::BitcoinWithdrawal { - value, - ref main_address, - main_fee, - } = output.content - { - let aggregated = address_to_aggregated_withdrawal - .entry(main_address.clone()) - .or_insert(AggregatedWithdrawal { - spend_utxos: HashMap::new(), - main_address: main_address.clone(), - value: bitcoin::Amount::ZERO, - main_fee: bitcoin::Amount::ZERO, - }); - // Add up all values. - aggregated.value = aggregated - .value - .checked_add(value) - .ok_or(AmountOverflowError)?; - aggregated.main_fee = aggregated - .main_fee - .checked_add(main_fee) - .ok_or(AmountOverflowError)?; - aggregated.spend_utxos.insert(outpoint, output); - } - } - if address_to_aggregated_withdrawal.is_empty() { - return Ok(None); - } - let mut aggregated_withdrawals: Vec<_> = - address_to_aggregated_withdrawal.into_values().collect(); - aggregated_withdrawals.sort_by_key(|a| std::cmp::Reverse(a.clone())); - let mut fee = bitcoin::Amount::ZERO; - let mut spend_utxos = BTreeMap::::new(); - let mut bundle_outputs = vec![]; - for aggregated in &aggregated_withdrawals { - if bundle_outputs.len() > MAX_BUNDLE_OUTPUTS { - break; - } - let bundle_output = bitcoin::TxOut { - value: aggregated.value, - script_pubkey: aggregated - .main_address - .assume_checked_ref() - .script_pubkey(), - }; - spend_utxos.extend(aggregated.spend_utxos.clone()); - bundle_outputs.push(bundle_output); - fee += aggregated.main_fee; - } - let bundle = WithdrawalBundle::new( - block_height, - fee, - spend_utxos, - bundle_outputs, - )?; - if bundle.tx().weight().to_wu() - > bitcoin::policy::MAX_STANDARD_TX_WEIGHT as u64 - { - Err(Error::BundleTooHeavy { - weight: bundle.tx().weight().to_wu(), - max_weight: bitcoin::policy::MAX_STANDARD_TX_WEIGHT as u64, - })?; - } - Ok(Some(bundle)) - } - /// Get pending withdrawal bundle and block height pub fn get_pending_withdrawal_bundle( &self, @@ -557,78 +468,6 @@ impl State { Ok(fee) } - /// Validate a block, returning the merkle root and fees - pub fn validate_block( - &self, - rotxn: &RoTxn, - header: &Header, - body: &Body, - ) -> Result<(bitcoin::Amount, MerkleRoot), Error> { - let tip_hash = self.try_get_tip(rotxn)?; - if header.prev_side_hash != tip_hash { - let err = error::InvalidHeader::PrevSideHash { - expected: tip_hash, - received: header.prev_side_hash, - }; - return Err(Error::InvalidHeader(err)); - }; - let mut coinbase_value = bitcoin::Amount::ZERO; - for output in &body.coinbase { - coinbase_value = coinbase_value - .checked_add(output.get_value()) - .ok_or(AmountOverflowError)?; - } - let mut total_fees = bitcoin::Amount::ZERO; - let mut spent_utxos = HashSet::new(); - let filled_txs: Vec<_> = body - .transactions - .iter() - .map(|t| self.fill_transaction(rotxn, t)) - .collect::>()?; - for filled_tx in &filled_txs { - for input in &filled_tx.transaction.inputs { - if spent_utxos.contains(input) { - return Err(Error::UtxoDoubleSpent); - } - spent_utxos.insert(*input); - } - total_fees = total_fees - .checked_add( - self.validate_filled_transaction(rotxn, filled_tx)?, - ) - .ok_or(AmountOverflowError)?; - } - if coinbase_value > total_fees { - return Err(Error::NotEnoughFees); - } - let merkle_root = Body::compute_merkle_root( - body.coinbase.as_slice(), - filled_txs.as_slice(), - )? - .ok_or(Error::MerkleRoot)?; - if merkle_root != header.merkle_root { - let err = Error::InvalidBody { - expected: header.merkle_root, - computed: merkle_root, - }; - return Err(err); - } - let spent_utxos = filled_txs - .iter() - .flat_map(|t| t.spent_utxos_requiring_auth().into_iter()); - for (authorization, spent_utxo) in - body.authorizations.iter().zip(spent_utxos) - { - if authorization.get_address() != spent_utxo.address { - return Err(Error::WrongPubKeyForAddress); - } - } - if Authorization::verify_body(body).is_err() { - return Err(Error::AuthorizationError); - } - Ok((total_fees, merkle_root)) - } - pub fn get_last_deposit_block_hash( &self, rotxn: &RoTxn, @@ -651,554 +490,6 @@ impl State { Ok(block_hash) } - fn connect_2wpd_withdrawal_bundle_submitted( - &self, - rwtxn: &mut RwTxn, - block_height: u32, - event_block_hash: &bitcoin::BlockHash, - m6id: M6id, - ) -> Result<(), Error> { - if let Some((bundle, bundle_block_height)) = - self.pending_withdrawal_bundle.get(rwtxn, &UnitKey)? - && bundle.compute_m6id() == m6id - { - assert_eq!(bundle_block_height, block_height - 1); - tracing::debug!( - %block_height, - %m6id, - "Withdrawal bundle successfully submitted" - ); - for (outpoint, spend_output) in bundle.spend_utxos() { - self.utxos.delete(rwtxn, outpoint)?; - let spent_output = SpentOutput { - output: spend_output.clone(), - inpoint: InPoint::Withdrawal { m6id }, - }; - self.stxos.put(rwtxn, outpoint, &spent_output)?; - } - self.withdrawal_bundles.put( - rwtxn, - &m6id, - &( - Some(bundle), - RollBack::>::new( - WithdrawalBundleStatus::Submitted, - block_height, - ), - ), - )?; - self.pending_withdrawal_bundle.delete(rwtxn, &UnitKey)?; - } else if let Some((_bundle, bundle_status)) = - self.withdrawal_bundles.get(rwtxn, &m6id)? - { - // Already applied - assert_eq!( - bundle_status.earliest().value, - WithdrawalBundleStatus::Submitted - ); - } else { - tracing::warn!( - %event_block_hash, - %m6id, - "Unknown withdrawal bundle submitted" - ); - self.withdrawal_bundles.put( - rwtxn, - &m6id, - &( - None, - RollBack::>::new( - WithdrawalBundleStatus::Submitted, - block_height, - ), - ), - )?; - }; - Ok(()) - } - - fn connect_2wpd_withdrawal_bundle_confirmed( - &self, - rwtxn: &mut RwTxn, - block_height: u32, - event_block_hash: &bitcoin::BlockHash, - m6id: M6id, - ) -> Result<(), Error> { - let (bundle, mut bundle_status) = self - .withdrawal_bundles - .get(rwtxn, &m6id)? - .ok_or(Error::UnknownWithdrawalBundle { m6id })?; - if bundle_status.latest().value == WithdrawalBundleStatus::Confirmed { - // Already applied - return Ok(()); - } - assert_eq!( - bundle_status.latest().value, - WithdrawalBundleStatus::Submitted - ); - // If an unknown bundle is confirmed, all UTXOs older than the - // bundle submission are potentially spent. - // This is only accepted in the case that block height is 0, - // and so no UTXOs could possibly have been double-spent yet. - // In this case, ALL UTXOs are considered spent. - if bundle.is_none() { - if block_height == 0 { - tracing::warn!( - %event_block_hash, - %m6id, - "Unknown withdrawal bundle confirmed, marking all UTXOs as spent" - ); - let utxos: Vec<_> = - self.utxos.iter(rwtxn)?.collect::>()?; - for (outpoint, output) in utxos { - let spent_output = SpentOutput { - output, - inpoint: InPoint::Withdrawal { m6id }, - }; - self.stxos.put(rwtxn, &outpoint, &spent_output)?; - } - self.utxos.clear(rwtxn)?; - } else { - return Err(Error::UnknownWithdrawalBundleConfirmed { - event_block_hash: *event_block_hash, - m6id, - }); - } - } - bundle_status - .push(WithdrawalBundleStatus::Confirmed, block_height) - .expect("Push confirmed status should be valid"); - self.withdrawal_bundles - .put(rwtxn, &m6id, &(bundle, bundle_status))?; - Ok(()) - } - - fn connect_2wpd_withdrawal_bundle_failed( - &self, - rwtxn: &mut RwTxn, - block_height: u32, - m6id: M6id, - ) -> Result<(), Error> { - tracing::debug!( - %block_height, - %m6id, - "Handling failed withdrawal bundle"); - let (bundle, mut bundle_status) = self - .withdrawal_bundles - .get(rwtxn, &m6id)? - .ok_or_else(|| Error::UnknownWithdrawalBundle { m6id })?; - if bundle_status.latest().value == WithdrawalBundleStatus::Failed { - // Already applied - return Ok(()); - } - assert_eq!( - bundle_status.latest().value, - WithdrawalBundleStatus::Submitted - ); - bundle_status - .push(WithdrawalBundleStatus::Failed, block_height) - .expect("Push failed status should be valid"); - if let Some(bundle) = &bundle { - for (outpoint, output) in bundle.spend_utxos() { - self.stxos.delete(rwtxn, outpoint)?; - self.utxos.put(rwtxn, outpoint, output)?; - } - let latest_failed_m6id = if let Some(mut latest_failed_m6id) = - self.latest_failed_withdrawal_bundle.get(rwtxn, &UnitKey)? - { - latest_failed_m6id - .push(m6id, block_height) - .expect("Push latest failed m6id should be valid"); - latest_failed_m6id - } else { - RollBack::>::new(m6id, block_height) - }; - self.latest_failed_withdrawal_bundle.put( - rwtxn, - &UnitKey, - &latest_failed_m6id, - )?; - } - self.withdrawal_bundles - .put(rwtxn, &m6id, &(bundle, bundle_status))?; - Ok(()) - } - - fn connect_2wpd_withdrawal_bundle_event( - &self, - rwtxn: &mut RwTxn, - block_height: u32, - event_block_hash: &bitcoin::BlockHash, - event: &WithdrawalBundleEvent, - ) -> Result<(), Error> { - match event.status { - WithdrawalBundleStatus::Submitted => self - .connect_2wpd_withdrawal_bundle_submitted( - rwtxn, - block_height, - event_block_hash, - event.m6id, - ), - WithdrawalBundleStatus::Confirmed => self - .connect_2wpd_withdrawal_bundle_confirmed( - rwtxn, - block_height, - event_block_hash, - event.m6id, - ), - WithdrawalBundleStatus::Failed => self - .connect_2wpd_withdrawal_bundle_failed( - rwtxn, - block_height, - event.m6id, - ), - } - } - - fn connect_2wpd_event( - &self, - rwtxn: &mut RwTxn, - block_height: u32, - latest_deposit_block_hash: &mut Option, - latest_withdrawal_bundle_event_block_hash: &mut Option< - bitcoin::BlockHash, - >, - event_block_hash: bitcoin::BlockHash, - event: &BlockEvent, - ) -> Result<(), Error> { - match event { - BlockEvent::Deposit(deposit) => { - let outpoint = OutPoint::Deposit(deposit.outpoint); - let output = deposit.output.clone(); - self.utxos.put(rwtxn, &outpoint, &output)?; - *latest_deposit_block_hash = Some(event_block_hash); - } - BlockEvent::WithdrawalBundle(withdrawal_bundle_event) => { - let () = self.connect_2wpd_withdrawal_bundle_event( - rwtxn, - block_height, - &event_block_hash, - withdrawal_bundle_event, - )?; - *latest_withdrawal_bundle_event_block_hash = - Some(event_block_hash); - } - } - Ok(()) - } - - pub fn connect_two_way_peg_data( - &self, - rwtxn: &mut RwTxn, - two_way_peg_data: &TwoWayPegData, - ) -> Result<(), Error> { - let block_height = self.try_get_height(rwtxn)?.ok_or(Error::NoTip)?; - tracing::trace!(%block_height, "Connecting 2WPD..."); - // Handle deposits. - let mut latest_deposit_block_hash = None; - let mut latest_withdrawal_bundle_event_block_hash = None; - for (event_block_hash, event_block_info) in &two_way_peg_data.block_info - { - for event in &event_block_info.events { - let () = self.connect_2wpd_event( - rwtxn, - block_height, - &mut latest_deposit_block_hash, - &mut latest_withdrawal_bundle_event_block_hash, - *event_block_hash, - event, - )?; - } - } - // Handle deposits. - if let Some(latest_deposit_block_hash) = latest_deposit_block_hash { - let deposit_block_seq_idx = self - .deposit_blocks - .last(rwtxn)? - .map_or(0, |(seq_idx, _)| seq_idx + 1); - self.deposit_blocks.put( - rwtxn, - &deposit_block_seq_idx, - &(latest_deposit_block_hash, block_height), - )?; - } - // Handle withdrawals - if let Some(latest_withdrawal_bundle_event_block_hash) = - latest_withdrawal_bundle_event_block_hash - { - let withdrawal_bundle_event_block_seq_idx = self - .withdrawal_bundle_event_blocks - .last(rwtxn)? - .map_or(0, |(seq_idx, _)| seq_idx + 1); - self.withdrawal_bundle_event_blocks.put( - rwtxn, - &withdrawal_bundle_event_block_seq_idx, - &(latest_withdrawal_bundle_event_block_hash, block_height), - )?; - } - let last_withdrawal_bundle_failure_height = self - .get_latest_failed_withdrawal_bundle(rwtxn)? - .map(|(height, _bundle)| height) - .unwrap_or_default(); - if block_height - last_withdrawal_bundle_failure_height - >= WITHDRAWAL_BUNDLE_FAILURE_GAP - && self - .pending_withdrawal_bundle - .get(rwtxn, &UnitKey)? - .is_none() - { - if let Some(bundle) = - self.collect_withdrawal_bundle(rwtxn, block_height)? - { - let m6id = bundle.compute_m6id(); - self.pending_withdrawal_bundle.put( - rwtxn, - &UnitKey, - &(bundle, block_height), - )?; - tracing::trace!( - %block_height, - %m6id, - "Stored pending withdrawal bundle" - ); - } - } - Ok(()) - } - - pub fn disconnect_two_way_peg_data( - &self, - rwtxn: &mut RwTxn, - two_way_peg_data: &TwoWayPegData, - ) -> Result<(), Error> { - let block_height = self - .try_get_height(rwtxn)? - .expect("Height should not be None"); - // Restore pending withdrawal bundle - for (_, event) in two_way_peg_data.withdrawal_bundle_events().rev() { - match event.status { - WithdrawalBundleStatus::Submitted => { - let Some((bundle, bundle_status)) = - self.withdrawal_bundles.get(rwtxn, &event.m6id)? - else { - if let Some((bundle, _)) = self - .pending_withdrawal_bundle - .get(rwtxn, &UnitKey)? - && bundle.compute_m6id() == event.m6id - { - // Already applied - continue; - } - return Err(Error::UnknownWithdrawalBundle { - m6id: event.m6id, - }); - }; - let bundle_status = bundle_status.latest(); - assert_eq!( - bundle_status.value, - WithdrawalBundleStatus::Submitted - ); - assert_eq!(bundle_status.height, block_height); - for (outpoint, output) in bundle.spend_utxos().iter().rev() - { - if !self.stxos.delete(rwtxn, outpoint)? { - return Err(Error::NoStxo { - outpoint: *outpoint, - }); - }; - self.utxos.put(rwtxn, outpoint, output)?; - } - self.pending_withdrawal_bundle.put( - rwtxn, - &UnitKey, - &(bundle, bundle_status.height - 1), - )?; - self.withdrawal_bundles.delete(rwtxn, &event.m6id)?; - } - WithdrawalBundleStatus::Confirmed => { - let Some((bundle, bundle_status)) = - self.withdrawal_bundles.get(rwtxn, &event.m6id)? - else { - return Err(Error::UnknownWithdrawalBundle { - m6id: event.m6id, - }); - }; - let (prev_bundle_status, latest_bundle_status) = - bundle_status.pop(); - if latest_bundle_status.value - == WithdrawalBundleStatus::Submitted - { - // Already applied - continue; - } else { - assert_eq!( - latest_bundle_status.value, - WithdrawalBundleStatus::Confirmed - ); - } - assert_eq!(latest_bundle_status.height, block_height); - let prev_bundle_status = prev_bundle_status - .expect("Pop confirmed bundle status should be valid"); - assert_eq!( - prev_bundle_status.latest().value, - WithdrawalBundleStatus::Submitted - ); - self.withdrawal_bundles.put( - rwtxn, - &event.m6id, - &(bundle, prev_bundle_status), - )?; - } - WithdrawalBundleStatus::Failed => { - let Some((bundle, bundle_status)) = - self.withdrawal_bundles.get(rwtxn, &event.m6id)? - else { - return Err(Error::UnknownWithdrawalBundle { - m6id: event.m6id, - }); - }; - let (prev_bundle_status, latest_bundle_status) = - bundle_status.pop(); - if latest_bundle_status.value - == WithdrawalBundleStatus::Submitted - { - // Already applied - continue; - } else { - assert_eq!( - latest_bundle_status.value, - WithdrawalBundleStatus::Failed - ); - } - assert_eq!(latest_bundle_status.height, block_height); - let prev_bundle_status = prev_bundle_status - .expect("Pop failed bundle status should be valid"); - assert_eq!( - prev_bundle_status.latest().value, - WithdrawalBundleStatus::Submitted - ); - for (outpoint, output) in bundle.spend_utxos().iter().rev() - { - let spent_output = SpentOutput { - output: output.clone(), - inpoint: InPoint::Withdrawal { m6id: event.m6id }, - }; - self.stxos.put(rwtxn, outpoint, &spent_output)?; - if self.utxos.delete(rwtxn, outpoint)? { - return Err(Error::NoUtxo { - outpoint: *outpoint, - }); - }; - } - self.withdrawal_bundles.put( - rwtxn, - &event.m6id, - &(bundle, prev_bundle_status), - )?; - let (prev_latest_failed_m6id, latest_failed_m6id) = self - .latest_failed_withdrawal_bundle - .get(rwtxn, &UnitKey)? - .expect("latest failed withdrawal bundle should exist") - .pop(); - assert_eq!(latest_failed_m6id.value, event.m6id); - assert_eq!(latest_failed_m6id.height, block_height); - if let Some(prev_latest_failed_m6id) = - prev_latest_failed_m6id - { - self.latest_failed_withdrawal_bundle.put( - rwtxn, - &UnitKey, - &prev_latest_failed_m6id, - )?; - } else { - self.latest_failed_withdrawal_bundle - .delete(rwtxn, &UnitKey)?; - } - } - } - } - // Handle withdrawals - if let Some(latest_withdrawal_bundle_event_block_hash) = - two_way_peg_data.latest_withdrawal_bundle_event_block_hash() - { - let ( - last_withdrawal_bundle_event_block_seq_idx, - ( - last_withdrawal_bundle_event_block_hash, - last_withdrawal_bundle_event_block_height, - ), - ) = self - .withdrawal_bundle_event_blocks - .last(rwtxn)? - .ok_or(Error::NoWithdrawalBundleEventBlock)?; - assert_eq!( - *latest_withdrawal_bundle_event_block_hash, - last_withdrawal_bundle_event_block_hash - ); - assert_eq!( - block_height - 1, - last_withdrawal_bundle_event_block_height - ); - if !self - .deposit_blocks - .delete(rwtxn, &last_withdrawal_bundle_event_block_seq_idx)? - { - return Err(Error::NoWithdrawalBundleEventBlock); - }; - } - let last_withdrawal_bundle_failure_height = self - .get_latest_failed_withdrawal_bundle(rwtxn)? - .map(|(height, _bundle)| height) - .unwrap_or_default(); - if block_height - last_withdrawal_bundle_failure_height - > WITHDRAWAL_BUNDLE_FAILURE_GAP - && let Some((bundle, bundle_height)) = - self.pending_withdrawal_bundle.get(rwtxn, &UnitKey)? - && bundle_height == block_height - 1 - { - self.pending_withdrawal_bundle.delete(rwtxn, &UnitKey)?; - for (outpoint, output) in bundle.spend_utxos().iter().rev() { - if !self.stxos.delete(rwtxn, outpoint)? { - return Err(Error::NoStxo { - outpoint: *outpoint, - }); - }; - self.utxos.put(rwtxn, outpoint, output)?; - } - } - // Handle deposits - if let Some(latest_deposit_block_hash) = - two_way_peg_data.latest_deposit_block_hash() - { - let ( - last_deposit_block_seq_idx, - (last_deposit_block_hash, last_deposit_block_height), - ) = self - .deposit_blocks - .last(rwtxn)? - .ok_or(Error::NoDepositBlock)?; - assert_eq!(latest_deposit_block_hash, last_deposit_block_hash); - assert_eq!(block_height - 1, last_deposit_block_height); - if !self - .deposit_blocks - .delete(rwtxn, &last_deposit_block_seq_idx)? - { - return Err(Error::NoDepositBlock); - }; - } - for deposit in two_way_peg_data - .deposits() - .flat_map(|(_, deposits)| deposits) - .rev() - { - let outpoint = OutPoint::Deposit(deposit.outpoint); - if !self.utxos.delete(rwtxn, &outpoint)? { - return Err(Error::NoUtxo { outpoint }); - } - } - Ok(()) - } - // apply bitname registration fn apply_bitname_registration( &self, @@ -1416,259 +707,6 @@ impl State { Ok(()) } - pub fn connect_block( - &self, - rwtxn: &mut RwTxn, - header: &Header, - body: &Body, - ) -> Result { - let height = self.try_get_height(rwtxn)?.map_or(0, |height| height + 1); - let tip_hash = self.try_get_tip(rwtxn)?; - if tip_hash != header.prev_side_hash { - let err = error::InvalidHeader::PrevSideHash { - expected: tip_hash, - received: header.prev_side_hash, - }; - return Err(Error::InvalidHeader(err)); - } - for (vout, output) in body.coinbase.iter().enumerate() { - let outpoint = OutPoint::Coinbase { - merkle_root: header.merkle_root, - vout: vout as u32, - }; - let filled_content = match output.content.clone() { - OutputContent::Bitcoin(value) => { - FilledOutputContent::Bitcoin(value) - } - OutputContent::Withdrawal { - value, - main_fee, - main_address, - } => FilledOutputContent::BitcoinWithdrawal { - value, - main_fee, - main_address, - }, - OutputContent::BitName | OutputContent::BitNameReservation => { - return Err(Error::BadCoinbaseOutputContent); - } - }; - let filled_output = FilledOutput { - address: output.address, - content: filled_content, - memo: output.memo.clone(), - }; - self.utxos.put(rwtxn, &outpoint, &filled_output)?; - } - let mut filled_txs: Vec = Vec::new(); - for transaction in &body.transactions { - let filled_tx = self.fill_transaction(rwtxn, transaction)?; - let txid = filled_tx.txid(); - for (vin, input) in filled_tx.inputs().iter().enumerate() { - let spent_output = self - .utxos - .get(rwtxn, input)? - .ok_or(Error::NoUtxo { outpoint: *input })?; - let spent_output = SpentOutput { - output: spent_output, - inpoint: InPoint::Regular { - txid, - vin: vin as u32, - }, - }; - self.utxos.delete(rwtxn, input)?; - self.stxos.put(rwtxn, input, &spent_output)?; - } - let filled_outputs = filled_tx - .filled_outputs() - .ok_or(Error::FillTxOutputContentsFailed)?; - for (vout, filled_output) in filled_outputs.iter().enumerate() { - let outpoint = OutPoint::Regular { - txid, - vout: vout as u32, - }; - self.utxos.put(rwtxn, &outpoint, filled_output)?; - } - match &transaction.data { - None => (), - Some(TxData::BitNameReservation { commitment }) => { - self.bitname_reservations.put(rwtxn, &txid, commitment)?; - } - Some(TxData::BitNameRegistration { - name_hash, - revealed_nonce: _, - bitname_data, - }) => { - let () = self.apply_bitname_registration( - rwtxn, - &filled_tx, - *name_hash, - bitname_data, - height, - )?; - } - Some(TxData::BitNameUpdate(bitname_updates)) => { - let () = self.apply_bitname_updates( - rwtxn, - &filled_tx, - (**bitname_updates).clone(), - height, - )?; - } - Some(TxData::BatchIcann(batch_icann_data)) => { - let () = self.apply_batch_icann( - rwtxn, - &filled_tx, - batch_icann_data, - )?; - } - } - filled_txs.push(filled_tx); - } - let merkle_root = Body::compute_merkle_root( - body.coinbase.as_slice(), - filled_txs.as_slice(), - )? - .ok_or(Error::MerkleRoot)?; - if merkle_root != header.merkle_root { - let err = Error::InvalidBody { - expected: header.merkle_root, - computed: merkle_root, - }; - return Err(err); - } - let block_hash = header.hash(); - self.tip.put(rwtxn, &UnitKey, &block_hash)?; - self.height.put(rwtxn, &UnitKey, &height)?; - Ok(merkle_root) - } - - pub fn disconnect_tip( - &self, - rwtxn: &mut RwTxn, - header: &Header, - body: &Body, - ) -> Result<(), Error> { - let tip_hash = - self.tip.try_get(rwtxn, &UnitKey)?.ok_or(Error::NoTip)?; - if tip_hash != header.hash() { - let err = error::InvalidHeader::BlockHash { - expected: tip_hash, - computed: header.hash(), - }; - return Err(Error::InvalidHeader(err)); - } - let height = self - .try_get_height(rwtxn)? - .expect("Height should not be None"); - // revert txs, last-to-first - let mut filled_txs: Vec = Vec::new(); - body.transactions.iter().rev().try_for_each(|tx| { - let txid = tx.txid(); - let filled_tx = - self.fill_transaction_from_stxos(rwtxn, tx.clone())?; - // revert transaction effects - match &tx.data { - None => (), - Some(TxData::BitNameReservation { .. }) => { - if !self.bitname_reservations.delete(rwtxn, &txid)? { - return Err(Error::MissingReservation { txid }); - } - } - Some(TxData::BitNameRegistration { - name_hash, - revealed_nonce: _, - bitname_data: _, - }) => { - let () = self.revert_bitname_registration( - rwtxn, &filled_tx, *name_hash, - )?; - } - Some(TxData::BitNameUpdate(bitname_updates)) => { - let () = self.revert_bitname_updates( - rwtxn, - &filled_tx, - (**bitname_updates).clone(), - height - 1, - )?; - } - Some(TxData::BatchIcann(batch_icann_data)) => { - let () = self.revert_batch_icann( - rwtxn, - &filled_tx, - batch_icann_data, - )?; - } - } - filled_txs.push(filled_tx); - // delete UTXOs, last-to-first - tx.outputs.iter().enumerate().rev().try_for_each( - |(vout, _output)| { - let outpoint = OutPoint::Regular { - txid, - vout: vout as u32, - }; - if self.utxos.delete(rwtxn, &outpoint)? { - Ok(()) - } else { - Err(Error::NoUtxo { outpoint }) - } - }, - )?; - // unspend STXOs, last-to-first - tx.inputs.iter().rev().try_for_each(|outpoint| { - if let Some(spent_output) = self.stxos.get(rwtxn, outpoint)? { - self.stxos.delete(rwtxn, outpoint)?; - self.utxos.put(rwtxn, outpoint, &spent_output.output)?; - Ok(()) - } else { - Err(Error::NoStxo { - outpoint: *outpoint, - }) - } - }) - })?; - filled_txs.reverse(); - // delete coinbase UTXOs, last-to-first - body.coinbase.iter().enumerate().rev().try_for_each( - |(vout, _output)| { - let outpoint = OutPoint::Coinbase { - merkle_root: header.merkle_root, - vout: vout as u32, - }; - if self.utxos.delete(rwtxn, &outpoint)? { - Ok(()) - } else { - Err(Error::NoUtxo { outpoint }) - } - }, - )?; - let merkle_root = Body::compute_merkle_root( - body.coinbase.as_slice(), - filled_txs.as_slice(), - )? - .ok_or(Error::MerkleRoot)?; - if merkle_root != header.merkle_root { - let err = Error::InvalidBody { - expected: header.merkle_root, - computed: merkle_root, - }; - return Err(err); - } - match (header.prev_side_hash, height) { - (None, 0) => { - self.tip.delete(rwtxn, &UnitKey)?; - self.height.delete(rwtxn, &UnitKey)?; - } - (None, _) | (_, 0) => return Err(Error::NoTip), - (Some(prev_side_hash), height) => { - self.tip.put(rwtxn, &UnitKey, &prev_side_hash)?; - self.height.put(rwtxn, &UnitKey, &(height - 1))?; - } - } - Ok(()) - } - /// Get total sidechain wealth in Bitcoin pub fn sidechain_wealth( &self, @@ -1708,6 +746,49 @@ impl State { .ok_or(AmountOverflowError)?; Ok(total_wealth) } + + pub fn validate_block( + &self, + rotxn: &RoTxn, + header: &Header, + body: &Body, + ) -> Result<(bitcoin::Amount, MerkleRoot), Error> { + block::validate(self, rotxn, header, body) + } + + pub fn connect_block( + &self, + rwtxn: &mut RwTxn, + header: &Header, + body: &Body, + ) -> Result { + block::connect(self, rwtxn, header, body) + } + + pub fn disconnect_tip( + &self, + rwtxn: &mut RwTxn, + header: &Header, + body: &Body, + ) -> Result<(), Error> { + block::disconnect_tip(self, rwtxn, header, body) + } + + pub fn connect_two_way_peg_data( + &self, + rwtxn: &mut RwTxn, + two_way_peg_data: &TwoWayPegData, + ) -> Result<(), Error> { + two_way_peg_data::connect(self, rwtxn, two_way_peg_data) + } + + pub fn disconnect_two_way_peg_data( + &self, + rwtxn: &mut RwTxn, + two_way_peg_data: &TwoWayPegData, + ) -> Result<(), Error> { + two_way_peg_data::disconnect(self, rwtxn, two_way_peg_data) + } } impl Watchable<()> for State { diff --git a/lib/state/two_way_peg_data.rs b/lib/state/two_way_peg_data.rs new file mode 100644 index 0000000..1624886 --- /dev/null +++ b/lib/state/two_way_peg_data.rs @@ -0,0 +1,649 @@ +//! Connect and disconnect two-way peg data + +use std::collections::{BTreeMap, HashMap}; + +use heed::{RoTxn, RwTxn}; + +use crate::{ + state::{ + rollback::{HeightStamped, RollBack}, + Error, State, WITHDRAWAL_BUNDLE_FAILURE_GAP, + }, + types::{ + proto::mainchain::{BlockEvent, TwoWayPegData}, + AggregatedWithdrawal, AmountOverflowError, FilledOutput, + FilledOutputContent, InPoint, M6id, OutPoint, SpentOutput, + WithdrawalBundle, WithdrawalBundleEvent, WithdrawalBundleStatus, + }, + util::UnitKey, +}; + +fn collect_withdrawal_bundle( + state: &State, + txn: &RoTxn, + block_height: u32, +) -> Result, Error> { + // Weight of a bundle with 0 outputs. + const BUNDLE_0_WEIGHT: u64 = 504; + // Weight of a single output. + const OUTPUT_WEIGHT: u64 = 128; + // Turns out to be 3121. + const MAX_BUNDLE_OUTPUTS: usize = + ((bitcoin::policy::MAX_STANDARD_TX_WEIGHT as u64 - BUNDLE_0_WEIGHT) + / OUTPUT_WEIGHT) as usize; + + // Aggregate all outputs by destination. + // destination -> (value, mainchain fee, spent_utxos) + let mut address_to_aggregated_withdrawal = HashMap::< + bitcoin::Address, + AggregatedWithdrawal, + >::new(); + for item in state.utxos.iter(txn)? { + let (outpoint, output) = item?; + if let FilledOutputContent::BitcoinWithdrawal { + value, + ref main_address, + main_fee, + } = output.content + { + let aggregated = address_to_aggregated_withdrawal + .entry(main_address.clone()) + .or_insert(AggregatedWithdrawal { + spend_utxos: HashMap::new(), + main_address: main_address.clone(), + value: bitcoin::Amount::ZERO, + main_fee: bitcoin::Amount::ZERO, + }); + // Add up all values. + aggregated.value = aggregated + .value + .checked_add(value) + .ok_or(AmountOverflowError)?; + aggregated.main_fee = aggregated + .main_fee + .checked_add(main_fee) + .ok_or(AmountOverflowError)?; + aggregated.spend_utxos.insert(outpoint, output); + } + } + if address_to_aggregated_withdrawal.is_empty() { + return Ok(None); + } + let mut aggregated_withdrawals: Vec<_> = + address_to_aggregated_withdrawal.into_values().collect(); + aggregated_withdrawals.sort_by_key(|a| std::cmp::Reverse(a.clone())); + let mut fee = bitcoin::Amount::ZERO; + let mut spend_utxos = BTreeMap::::new(); + let mut bundle_outputs = vec![]; + for aggregated in &aggregated_withdrawals { + if bundle_outputs.len() > MAX_BUNDLE_OUTPUTS { + break; + } + let bundle_output = bitcoin::TxOut { + value: aggregated.value, + script_pubkey: aggregated + .main_address + .assume_checked_ref() + .script_pubkey(), + }; + spend_utxos.extend(aggregated.spend_utxos.clone()); + bundle_outputs.push(bundle_output); + fee += aggregated.main_fee; + } + let bundle = + WithdrawalBundle::new(block_height, fee, spend_utxos, bundle_outputs)?; + if bundle.tx().weight().to_wu() + > bitcoin::policy::MAX_STANDARD_TX_WEIGHT as u64 + { + Err(Error::BundleTooHeavy { + weight: bundle.tx().weight().to_wu(), + max_weight: bitcoin::policy::MAX_STANDARD_TX_WEIGHT as u64, + })?; + } + Ok(Some(bundle)) +} + +fn connect_withdrawal_bundle_submitted( + state: &State, + rwtxn: &mut RwTxn, + block_height: u32, + event_block_hash: &bitcoin::BlockHash, + m6id: M6id, +) -> Result<(), Error> { + if let Some((bundle, bundle_block_height)) = + state.pending_withdrawal_bundle.get(rwtxn, &UnitKey)? + && bundle.compute_m6id() == m6id + { + assert_eq!(bundle_block_height, block_height - 1); + tracing::debug!( + %block_height, + %m6id, + "Withdrawal bundle successfully submitted" + ); + for (outpoint, spend_output) in bundle.spend_utxos() { + state.utxos.delete(rwtxn, outpoint)?; + let spent_output = SpentOutput { + output: spend_output.clone(), + inpoint: InPoint::Withdrawal { m6id }, + }; + state.stxos.put(rwtxn, outpoint, &spent_output)?; + } + state.withdrawal_bundles.put( + rwtxn, + &m6id, + &( + Some(bundle), + RollBack::>::new( + WithdrawalBundleStatus::Submitted, + block_height, + ), + ), + )?; + state.pending_withdrawal_bundle.delete(rwtxn, &UnitKey)?; + } else if let Some((_bundle, bundle_status)) = + state.withdrawal_bundles.get(rwtxn, &m6id)? + { + // Already applied + assert_eq!( + bundle_status.earliest().value, + WithdrawalBundleStatus::Submitted + ); + } else { + tracing::warn!( + %event_block_hash, + %m6id, + "Unknown withdrawal bundle submitted" + ); + self.withdrawal_bundles.put( + rwtxn, + &m6id, + &( + None, + RollBack::>::new( + WithdrawalBundleStatus::Submitted, + block_height, + ), + ), + )?; + }; + Ok(()) +} + +fn connect_withdrawal_bundle_confirmed( + state: &State, + rwtxn: &mut RwTxn, + block_height: u32, + event_block_hash: &bitcoin::BlockHash, + m6id: M6id, +) -> Result<(), Error> { + let (bundle, mut bundle_status) = state + .withdrawal_bundles + .get(rwtxn, &m6id)? + .ok_or(Error::UnknownWithdrawalBundle { m6id })?; + if bundle_status.latest().value == WithdrawalBundleStatus::Confirmed { + // Already applied + return Ok(()); + } + assert_eq!( + bundle_status.latest().value, + WithdrawalBundleStatus::Submitted + ); + // If an unknown bundle is confirmed, all UTXOs older than the + // bundle submission are potentially spent. + // This is only accepted in the case that block height is 0, + // and so no UTXOs could possibly have been double-spent yet. + // In this case, ALL UTXOs are considered spent. + if bundle.is_none() { + if block_height == 0 { + tracing::warn!( + %event_block_hash, + %m6id, + "Unknown withdrawal bundle confirmed, marking all UTXOs as spent" + ); + let utxos: Vec<_> = + state.utxos.iter(rwtxn)?.collect::>()?; + for (outpoint, output) in utxos { + let spent_output = SpentOutput { + output, + inpoint: InPoint::Withdrawal { m6id }, + }; + state.stxos.put(rwtxn, &outpoint, &spent_output)?; + } + state.utxos.clear(rwtxn)?; + } else { + return Err(Error::UnknownWithdrawalBundleConfirmed { + event_block_hash: *event_block_hash, + m6id, + }); + } + } + bundle_status + .push(WithdrawalBundleStatus::Confirmed, block_height) + .expect("Push confirmed status should be valid"); + state + .withdrawal_bundles + .put(rwtxn, &m6id, &(bundle, bundle_status))?; + Ok(()) +} + +fn connect_withdrawal_bundle_failed( + state: &State, + rwtxn: &mut RwTxn, + block_height: u32, + m6id: M6id, +) -> Result<(), Error> { + tracing::debug!( + %block_height, + %m6id, + "Handling failed withdrawal bundle"); + let (bundle, mut bundle_status) = state + .withdrawal_bundles + .get(rwtxn, &m6id)? + .ok_or_else(|| Error::UnknownWithdrawalBundle { m6id })?; + if bundle_status.latest().value == WithdrawalBundleStatus::Failed { + // Already applied + return Ok(()); + } + assert_eq!( + bundle_status.latest().value, + WithdrawalBundleStatus::Submitted + ); + bundle_status + .push(WithdrawalBundleStatus::Failed, block_height) + .expect("Push failed status should be valid"); + if let Some(bundle) = &bundle { + for (outpoint, output) in bundle.spend_utxos() { + state.stxos.delete(rwtxn, outpoint)?; + state.utxos.put(rwtxn, outpoint, output)?; + } + let latest_failed_m6id = if let Some(mut latest_failed_m6id) = + state.latest_failed_withdrawal_bundle.get(rwtxn, &UnitKey)? + { + latest_failed_m6id + .push(m6id, block_height) + .expect("Push latest failed m6id should be valid"); + latest_failed_m6id + } else { + RollBack::>::new(m6id, block_height) + }; + state.latest_failed_withdrawal_bundle.put( + rwtxn, + &UnitKey, + &latest_failed_m6id, + )?; + } + state + .withdrawal_bundles + .put(rwtxn, &m6id, &(bundle, bundle_status))?; + Ok(()) +} + +fn connect_withdrawal_bundle_event( + state: &State, + rwtxn: &mut RwTxn, + block_height: u32, + event_block_hash: &bitcoin::BlockHash, + event: &WithdrawalBundleEvent, +) -> Result<(), Error> { + match event.status { + WithdrawalBundleStatus::Submitted => { + connect_withdrawal_bundle_submitted( + state, + rwtxn, + block_height, + event_block_hash, + event.m6id, + ) + } + WithdrawalBundleStatus::Confirmed => { + connect_withdrawal_bundle_confirmed( + state, + rwtxn, + block_height, + event_block_hash, + event.m6id, + ) + } + WithdrawalBundleStatus::Failed => connect_withdrawal_bundle_failed( + state, + rwtxn, + block_height, + event.m6id, + ), + } +} + +fn connect_2wpd_event( + state: &State, + rwtxn: &mut RwTxn, + block_height: u32, + latest_deposit_block_hash: &mut Option, + latest_withdrawal_bundle_event_block_hash: &mut Option, + event_block_hash: bitcoin::BlockHash, + event: &BlockEvent, +) -> Result<(), Error> { + match event { + BlockEvent::Deposit(deposit) => { + let outpoint = OutPoint::Deposit(deposit.outpoint); + let output = deposit.output.clone(); + state.utxos.put(rwtxn, &outpoint, &output)?; + *latest_deposit_block_hash = Some(event_block_hash); + } + BlockEvent::WithdrawalBundle(withdrawal_bundle_event) => { + let () = connect_withdrawal_bundle_event( + state, + rwtxn, + block_height, + &event_block_hash, + withdrawal_bundle_event, + )?; + *latest_withdrawal_bundle_event_block_hash = Some(event_block_hash); + } + } + Ok(()) +} + +pub fn connect( + state: &State, + rwtxn: &mut RwTxn, + two_way_peg_data: &TwoWayPegData, +) -> Result<(), Error> { + let block_height = state.try_get_height(rwtxn)?.ok_or(Error::NoTip)?; + tracing::trace!(%block_height, "Connecting 2WPD..."); + // Handle deposits. + let mut latest_deposit_block_hash = None; + let mut latest_withdrawal_bundle_event_block_hash = None; + for (event_block_hash, event_block_info) in &two_way_peg_data.block_info { + for event in &event_block_info.events { + let () = connect_2wpd_event( + state, + rwtxn, + block_height, + &mut latest_deposit_block_hash, + &mut latest_withdrawal_bundle_event_block_hash, + *event_block_hash, + event, + )?; + } + } + // Handle deposits. + if let Some(latest_deposit_block_hash) = latest_deposit_block_hash { + let deposit_block_seq_idx = state + .deposit_blocks + .last(rwtxn)? + .map_or(0, |(seq_idx, _)| seq_idx + 1); + state.deposit_blocks.put( + rwtxn, + &deposit_block_seq_idx, + &(latest_deposit_block_hash, block_height), + )?; + } + // Handle withdrawals + if let Some(latest_withdrawal_bundle_event_block_hash) = + latest_withdrawal_bundle_event_block_hash + { + let withdrawal_bundle_event_block_seq_idx = state + .withdrawal_bundle_event_blocks + .last(rwtxn)? + .map_or(0, |(seq_idx, _)| seq_idx + 1); + state.withdrawal_bundle_event_blocks.put( + rwtxn, + &withdrawal_bundle_event_block_seq_idx, + &(latest_withdrawal_bundle_event_block_hash, block_height), + )?; + } + let last_withdrawal_bundle_failure_height = state + .get_latest_failed_withdrawal_bundle(rwtxn)? + .map(|(height, _bundle)| height) + .unwrap_or_default(); + if block_height - last_withdrawal_bundle_failure_height + >= WITHDRAWAL_BUNDLE_FAILURE_GAP + && state + .pending_withdrawal_bundle + .get(rwtxn, &UnitKey)? + .is_none() + { + if let Some(bundle) = + collect_withdrawal_bundle(state, rwtxn, block_height)? + { + let m6id = bundle.compute_m6id(); + state.pending_withdrawal_bundle.put( + rwtxn, + &UnitKey, + &(bundle, block_height), + )?; + tracing::trace!( + %block_height, + %m6id, + "Stored pending withdrawal bundle" + ); + } + } + Ok(()) +} + +pub fn disconnect( + state: &State, + rwtxn: &mut RwTxn, + two_way_peg_data: &TwoWayPegData, +) -> Result<(), Error> { + let block_height = state + .try_get_height(rwtxn)? + .expect("Height should not be None"); + // Restore pending withdrawal bundle + for (_, event) in two_way_peg_data.withdrawal_bundle_events().rev() { + match event.status { + WithdrawalBundleStatus::Submitted => { + let Some((bundle, bundle_status)) = + state.withdrawal_bundles.get(rwtxn, &event.m6id)? + else { + if let Some((bundle, _)) = + state.pending_withdrawal_bundle.get(rwtxn, &UnitKey)? + && bundle.compute_m6id() == event.m6id + { + // Already applied + continue; + } + return Err(Error::UnknownWithdrawalBundle { + m6id: event.m6id, + }); + }; + let bundle_status = bundle_status.latest(); + assert_eq!( + bundle_status.value, + WithdrawalBundleStatus::Submitted + ); + assert_eq!(bundle_status.height, block_height); + for (outpoint, output) in bundle.spend_utxos().iter().rev() { + if !state.stxos.delete(rwtxn, outpoint)? { + return Err(Error::NoStxo { + outpoint: *outpoint, + }); + }; + state.utxos.put(rwtxn, outpoint, output)?; + } + state.pending_withdrawal_bundle.put( + rwtxn, + &UnitKey, + &(bundle, bundle_status.height - 1), + )?; + state.withdrawal_bundles.delete(rwtxn, &event.m6id)?; + } + WithdrawalBundleStatus::Confirmed => { + let Some((bundle, bundle_status)) = + state.withdrawal_bundles.get(rwtxn, &event.m6id)? + else { + return Err(Error::UnknownWithdrawalBundle { + m6id: event.m6id, + }); + }; + let (prev_bundle_status, latest_bundle_status) = + bundle_status.pop(); + if latest_bundle_status.value + == WithdrawalBundleStatus::Submitted + { + // Already applied + continue; + } else { + assert_eq!( + latest_bundle_status.value, + WithdrawalBundleStatus::Confirmed + ); + } + assert_eq!(latest_bundle_status.height, block_height); + let prev_bundle_status = prev_bundle_status + .expect("Pop confirmed bundle status should be valid"); + assert_eq!( + prev_bundle_status.latest().value, + WithdrawalBundleStatus::Submitted + ); + state.withdrawal_bundles.put( + rwtxn, + &event.m6id, + &(bundle, prev_bundle_status), + )?; + } + WithdrawalBundleStatus::Failed => { + let Some((bundle, bundle_status)) = + state.withdrawal_bundles.get(rwtxn, &event.m6id)? + else { + return Err(Error::UnknownWithdrawalBundle { + m6id: event.m6id, + }); + }; + let (prev_bundle_status, latest_bundle_status) = + bundle_status.pop(); + if latest_bundle_status.value + == WithdrawalBundleStatus::Submitted + { + // Already applied + continue; + } else { + assert_eq!( + latest_bundle_status.value, + WithdrawalBundleStatus::Failed + ); + } + assert_eq!(latest_bundle_status.height, block_height); + let prev_bundle_status = prev_bundle_status + .expect("Pop failed bundle status should be valid"); + assert_eq!( + prev_bundle_status.latest().value, + WithdrawalBundleStatus::Submitted + ); + for (outpoint, output) in bundle.spend_utxos().iter().rev() { + let spent_output = SpentOutput { + output: output.clone(), + inpoint: InPoint::Withdrawal { m6id: event.m6id }, + }; + state.stxos.put(rwtxn, outpoint, &spent_output)?; + if state.utxos.delete(rwtxn, outpoint)? { + return Err(Error::NoUtxo { + outpoint: *outpoint, + }); + }; + } + state.withdrawal_bundles.put( + rwtxn, + &event.m6id, + &(bundle, prev_bundle_status), + )?; + let (prev_latest_failed_m6id, latest_failed_m6id) = state + .latest_failed_withdrawal_bundle + .get(rwtxn, &UnitKey)? + .expect("latest failed withdrawal bundle should exist") + .pop(); + assert_eq!(latest_failed_m6id.value, event.m6id); + assert_eq!(latest_failed_m6id.height, block_height); + if let Some(prev_latest_failed_m6id) = prev_latest_failed_m6id { + state.latest_failed_withdrawal_bundle.put( + rwtxn, + &UnitKey, + &prev_latest_failed_m6id, + )?; + } else { + state + .latest_failed_withdrawal_bundle + .delete(rwtxn, &UnitKey)?; + } + } + } + } + // Handle withdrawals + if let Some(latest_withdrawal_bundle_event_block_hash) = + two_way_peg_data.latest_withdrawal_bundle_event_block_hash() + { + let ( + last_withdrawal_bundle_event_block_seq_idx, + ( + last_withdrawal_bundle_event_block_hash, + last_withdrawal_bundle_event_block_height, + ), + ) = state + .withdrawal_bundle_event_blocks + .last(rwtxn)? + .ok_or(Error::NoWithdrawalBundleEventBlock)?; + assert_eq!( + *latest_withdrawal_bundle_event_block_hash, + last_withdrawal_bundle_event_block_hash + ); + assert_eq!(block_height - 1, last_withdrawal_bundle_event_block_height); + if !state + .deposit_blocks + .delete(rwtxn, &last_withdrawal_bundle_event_block_seq_idx)? + { + return Err(Error::NoWithdrawalBundleEventBlock); + }; + } + let last_withdrawal_bundle_failure_height = state + .get_latest_failed_withdrawal_bundle(rwtxn)? + .map(|(height, _bundle)| height) + .unwrap_or_default(); + if block_height - last_withdrawal_bundle_failure_height + > WITHDRAWAL_BUNDLE_FAILURE_GAP + && let Some((bundle, bundle_height)) = + state.pending_withdrawal_bundle.get(rwtxn, &UnitKey)? + && bundle_height == block_height - 1 + { + state.pending_withdrawal_bundle.delete(rwtxn, &UnitKey)?; + for (outpoint, output) in bundle.spend_utxos().iter().rev() { + if !state.stxos.delete(rwtxn, outpoint)? { + return Err(Error::NoStxo { + outpoint: *outpoint, + }); + }; + state.utxos.put(rwtxn, outpoint, output)?; + } + } + // Handle deposits + if let Some(latest_deposit_block_hash) = + two_way_peg_data.latest_deposit_block_hash() + { + let ( + last_deposit_block_seq_idx, + (last_deposit_block_hash, last_deposit_block_height), + ) = state + .deposit_blocks + .last(rwtxn)? + .ok_or(Error::NoDepositBlock)?; + assert_eq!(latest_deposit_block_hash, last_deposit_block_hash); + assert_eq!(block_height - 1, last_deposit_block_height); + if !state + .deposit_blocks + .delete(rwtxn, &last_deposit_block_seq_idx)? + { + return Err(Error::NoDepositBlock); + }; + } + for deposit in two_way_peg_data + .deposits() + .flat_map(|(_, deposits)| deposits) + .rev() + { + let outpoint = OutPoint::Deposit(deposit.outpoint); + if !state.utxos.delete(rwtxn, &outpoint)? { + return Err(Error::NoUtxo { outpoint }); + } + } + Ok(()) +} From 62924ac322eb77cf5be753c4dc0d1d6ff4ad096f Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Fri, 7 Feb 2025 00:57:40 +0800 Subject: [PATCH 10/17] state: handle unknown withdrawals during block disconnects --- lib/state/mod.rs | 28 ++- lib/state/two_way_peg_data.rs | 455 ++++++++++++++++++++-------------- 2 files changed, 297 insertions(+), 186 deletions(-) diff --git a/lib/state/mod.rs b/lib/state/mod.rs index baf5727..ed7daa7 100644 --- a/lib/state/mod.rs +++ b/lib/state/mod.rs @@ -1,7 +1,8 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use futures::Stream; use heed::{types::SerdeBincode, Database, RoTxn, RwTxn}; +use serde::{Deserialize, Serialize}; use crate::{ authorization::Authorization, @@ -31,10 +32,33 @@ use rollback::{HeightStamped, RollBack}; pub const WITHDRAWAL_BUNDLE_FAILURE_GAP: u32 = 4; +/// Information we have regarding a withdrawal bundle +#[derive(Debug, Deserialize, Serialize)] +enum WithdrawalBundleInfo { + /// Withdrawal bundle is known + Known(WithdrawalBundle), + /// Withdrawal bundle is unknown but unconfirmed / failed + Unknown, + /// If an unknown withdrawal bundle is confirmed, ALL UTXOs are + /// considered spent. + UnknownConfirmed { + spend_utxos: BTreeMap, + }, +} + +impl WithdrawalBundleInfo { + fn is_known(&self) -> bool { + match self { + Self::Known(_) => true, + Self::Unknown | Self::UnknownConfirmed { .. } => false, + } + } +} + type WithdrawalBundlesDb = Database< SerdeBincode, SerdeBincode<( - Option, + WithdrawalBundleInfo, RollBack>, )>, >; diff --git a/lib/state/two_way_peg_data.rs b/lib/state/two_way_peg_data.rs index 1624886..41bf4c5 100644 --- a/lib/state/two_way_peg_data.rs +++ b/lib/state/two_way_peg_data.rs @@ -7,7 +7,7 @@ use heed::{RoTxn, RwTxn}; use crate::{ state::{ rollback::{HeightStamped, RollBack}, - Error, State, WITHDRAWAL_BUNDLE_FAILURE_GAP, + Error, State, WithdrawalBundleInfo, WITHDRAWAL_BUNDLE_FAILURE_GAP, }, types::{ proto::mainchain::{BlockEvent, TwoWayPegData}, @@ -132,7 +132,7 @@ fn connect_withdrawal_bundle_submitted( rwtxn, &m6id, &( - Some(bundle), + WithdrawalBundleInfo::Known(bundle), RollBack::>::new( WithdrawalBundleStatus::Submitted, block_height, @@ -154,11 +154,11 @@ fn connect_withdrawal_bundle_submitted( %m6id, "Unknown withdrawal bundle submitted" ); - self.withdrawal_bundles.put( + state.withdrawal_bundles.put( rwtxn, &m6id, &( - None, + WithdrawalBundleInfo::Unknown, RollBack::>::new( WithdrawalBundleStatus::Submitted, block_height, @@ -176,7 +176,7 @@ fn connect_withdrawal_bundle_confirmed( event_block_hash: &bitcoin::BlockHash, m6id: M6id, ) -> Result<(), Error> { - let (bundle, mut bundle_status) = state + let (mut bundle, mut bundle_status) = state .withdrawal_bundles .get(rwtxn, &m6id)? .ok_or(Error::UnknownWithdrawalBundle { m6id })?; @@ -193,23 +193,25 @@ fn connect_withdrawal_bundle_confirmed( // This is only accepted in the case that block height is 0, // and so no UTXOs could possibly have been double-spent yet. // In this case, ALL UTXOs are considered spent. - if bundle.is_none() { + if !bundle.is_known() { if block_height == 0 { tracing::warn!( %event_block_hash, %m6id, "Unknown withdrawal bundle confirmed, marking all UTXOs as spent" ); - let utxos: Vec<_> = + let utxos: BTreeMap<_, _> = state.utxos.iter(rwtxn)?.collect::>()?; - for (outpoint, output) in utxos { + for (outpoint, output) in &utxos { let spent_output = SpentOutput { - output, + output: output.clone(), inpoint: InPoint::Withdrawal { m6id }, }; - state.stxos.put(rwtxn, &outpoint, &spent_output)?; + state.stxos.put(rwtxn, outpoint, &spent_output)?; } state.utxos.clear(rwtxn)?; + bundle = + WithdrawalBundleInfo::UnknownConfirmed { spend_utxos: utxos }; } else { return Err(Error::UnknownWithdrawalBundleConfirmed { event_block_hash: *event_block_hash, @@ -251,26 +253,30 @@ fn connect_withdrawal_bundle_failed( bundle_status .push(WithdrawalBundleStatus::Failed, block_height) .expect("Push failed status should be valid"); - if let Some(bundle) = &bundle { - for (outpoint, output) in bundle.spend_utxos() { - state.stxos.delete(rwtxn, outpoint)?; - state.utxos.put(rwtxn, outpoint, output)?; + match &bundle { + WithdrawalBundleInfo::Unknown + | WithdrawalBundleInfo::UnknownConfirmed { .. } => (), + WithdrawalBundleInfo::Known(bundle) => { + for (outpoint, output) in bundle.spend_utxos() { + state.stxos.delete(rwtxn, outpoint)?; + state.utxos.put(rwtxn, outpoint, output)?; + } + let latest_failed_m6id = if let Some(mut latest_failed_m6id) = + state.latest_failed_withdrawal_bundle.get(rwtxn, &UnitKey)? + { + latest_failed_m6id + .push(m6id, block_height) + .expect("Push latest failed m6id should be valid"); + latest_failed_m6id + } else { + RollBack::>::new(m6id, block_height) + }; + state.latest_failed_withdrawal_bundle.put( + rwtxn, + &UnitKey, + &latest_failed_m6id, + )?; } - let latest_failed_m6id = if let Some(mut latest_failed_m6id) = - state.latest_failed_withdrawal_bundle.get(rwtxn, &UnitKey)? - { - latest_failed_m6id - .push(m6id, block_height) - .expect("Push latest failed m6id should be valid"); - latest_failed_m6id - } else { - RollBack::>::new(m6id, block_height) - }; - state.latest_failed_withdrawal_bundle.put( - rwtxn, - &UnitKey, - &latest_failed_m6id, - )?; } state .withdrawal_bundles @@ -422,156 +428,257 @@ pub fn connect( Ok(()) } -pub fn disconnect( +fn disconnect_withdrawal_bundle_submitted( state: &State, rwtxn: &mut RwTxn, - two_way_peg_data: &TwoWayPegData, + block_height: u32, + m6id: M6id, ) -> Result<(), Error> { - let block_height = state - .try_get_height(rwtxn)? - .expect("Height should not be None"); - // Restore pending withdrawal bundle - for (_, event) in two_way_peg_data.withdrawal_bundle_events().rev() { - match event.status { - WithdrawalBundleStatus::Submitted => { - let Some((bundle, bundle_status)) = - state.withdrawal_bundles.get(rwtxn, &event.m6id)? - else { - if let Some((bundle, _)) = - state.pending_withdrawal_bundle.get(rwtxn, &UnitKey)? - && bundle.compute_m6id() == event.m6id - { - // Already applied - continue; - } - return Err(Error::UnknownWithdrawalBundle { - m6id: event.m6id, + let Some((bundle, bundle_status)) = + state.withdrawal_bundles.get(rwtxn, &m6id)? + else { + if let Some((bundle, _)) = + state.pending_withdrawal_bundle.get(rwtxn, &UnitKey)? + && bundle.compute_m6id() == m6id + { + // Already applied + return Ok(()); + } else { + return Err(Error::UnknownWithdrawalBundle { m6id }); + } + }; + let bundle_status = bundle_status.latest(); + assert_eq!(bundle_status.value, WithdrawalBundleStatus::Submitted); + assert_eq!(bundle_status.height, block_height); + match bundle { + WithdrawalBundleInfo::Unknown + | WithdrawalBundleInfo::UnknownConfirmed { .. } => (), + WithdrawalBundleInfo::Known(bundle) => { + for (outpoint, output) in bundle.spend_utxos().iter().rev() { + if !state.stxos.delete(rwtxn, outpoint)? { + return Err(Error::NoStxo { + outpoint: *outpoint, }); }; - let bundle_status = bundle_status.latest(); - assert_eq!( - bundle_status.value, - WithdrawalBundleStatus::Submitted - ); - assert_eq!(bundle_status.height, block_height); - for (outpoint, output) in bundle.spend_utxos().iter().rev() { - if !state.stxos.delete(rwtxn, outpoint)? { - return Err(Error::NoStxo { - outpoint: *outpoint, - }); - }; - state.utxos.put(rwtxn, outpoint, output)?; - } - state.pending_withdrawal_bundle.put( - rwtxn, - &UnitKey, - &(bundle, bundle_status.height - 1), - )?; - state.withdrawal_bundles.delete(rwtxn, &event.m6id)?; + state.utxos.put(rwtxn, outpoint, output)?; } - WithdrawalBundleStatus::Confirmed => { - let Some((bundle, bundle_status)) = - state.withdrawal_bundles.get(rwtxn, &event.m6id)? - else { - return Err(Error::UnknownWithdrawalBundle { - m6id: event.m6id, - }); + state.pending_withdrawal_bundle.put( + rwtxn, + &UnitKey, + &(bundle, bundle_status.height - 1), + )?; + } + } + state.withdrawal_bundles.delete(rwtxn, &m6id)?; + Ok(()) +} + +fn disconnect_withdrawal_bundle_confirmed( + state: &State, + rwtxn: &mut RwTxn, + block_height: u32, + m6id: M6id, +) -> Result<(), Error> { + let (mut bundle, bundle_status) = state + .withdrawal_bundles + .get(rwtxn, &m6id)? + .ok_or_else(|| Error::UnknownWithdrawalBundle { m6id })?; + let (prev_bundle_status, latest_bundle_status) = bundle_status.pop(); + if latest_bundle_status.value == WithdrawalBundleStatus::Submitted { + // Already applied + return Ok(()); + } + assert_eq!( + latest_bundle_status.value, + WithdrawalBundleStatus::Confirmed + ); + assert_eq!(latest_bundle_status.height, block_height); + let prev_bundle_status = prev_bundle_status + .expect("Pop confirmed bundle status should be valid"); + assert_eq!( + prev_bundle_status.latest().value, + WithdrawalBundleStatus::Submitted + ); + match bundle { + WithdrawalBundleInfo::Known(_) | WithdrawalBundleInfo::Unknown => (), + WithdrawalBundleInfo::UnknownConfirmed { spend_utxos } => { + for (outpoint, output) in spend_utxos { + state.utxos.put(rwtxn, &outpoint, &output)?; + if !state.stxos.delete(rwtxn, &outpoint)? { + return Err(Error::NoStxo { outpoint }); }; - let (prev_bundle_status, latest_bundle_status) = - bundle_status.pop(); - if latest_bundle_status.value - == WithdrawalBundleStatus::Submitted - { - // Already applied - continue; - } else { - assert_eq!( - latest_bundle_status.value, - WithdrawalBundleStatus::Confirmed - ); - } - assert_eq!(latest_bundle_status.height, block_height); - let prev_bundle_status = prev_bundle_status - .expect("Pop confirmed bundle status should be valid"); - assert_eq!( - prev_bundle_status.latest().value, - WithdrawalBundleStatus::Submitted - ); - state.withdrawal_bundles.put( - rwtxn, - &event.m6id, - &(bundle, prev_bundle_status), - )?; } - WithdrawalBundleStatus::Failed => { - let Some((bundle, bundle_status)) = - state.withdrawal_bundles.get(rwtxn, &event.m6id)? - else { - return Err(Error::UnknownWithdrawalBundle { - m6id: event.m6id, + bundle = WithdrawalBundleInfo::Unknown; + } + } + state.withdrawal_bundles.put( + rwtxn, + &m6id, + &(bundle, prev_bundle_status), + )?; + Ok(()) +} + +fn disconnect_withdrawal_bundle_failed( + state: &State, + rwtxn: &mut RwTxn, + block_height: u32, + m6id: M6id, +) -> Result<(), Error> { + let (bundle, bundle_status) = + state + .withdrawal_bundles + .get(rwtxn, &m6id)? + .ok_or_else(|| Error::UnknownWithdrawalBundle { m6id })?; + let (prev_bundle_status, latest_bundle_status) = bundle_status.pop(); + if latest_bundle_status.value == WithdrawalBundleStatus::Submitted { + // Already applied + return Ok(()); + } else { + assert_eq!(latest_bundle_status.value, WithdrawalBundleStatus::Failed); + } + assert_eq!(latest_bundle_status.height, block_height); + let prev_bundle_status = + prev_bundle_status.expect("Pop failed bundle status should be valid"); + assert_eq!( + prev_bundle_status.latest().value, + WithdrawalBundleStatus::Submitted + ); + match &bundle { + WithdrawalBundleInfo::Unknown + | WithdrawalBundleInfo::UnknownConfirmed { .. } => (), + WithdrawalBundleInfo::Known(bundle) => { + for (outpoint, output) in bundle.spend_utxos().iter().rev() { + let spent_output = SpentOutput { + output: output.clone(), + inpoint: InPoint::Withdrawal { m6id }, + }; + state.stxos.put(rwtxn, outpoint, &spent_output)?; + if state.utxos.delete(rwtxn, outpoint)? { + return Err(Error::NoUtxo { + outpoint: *outpoint, }); }; - let (prev_bundle_status, latest_bundle_status) = - bundle_status.pop(); - if latest_bundle_status.value - == WithdrawalBundleStatus::Submitted - { - // Already applied - continue; - } else { - assert_eq!( - latest_bundle_status.value, - WithdrawalBundleStatus::Failed - ); - } - assert_eq!(latest_bundle_status.height, block_height); - let prev_bundle_status = prev_bundle_status - .expect("Pop failed bundle status should be valid"); - assert_eq!( - prev_bundle_status.latest().value, - WithdrawalBundleStatus::Submitted - ); - for (outpoint, output) in bundle.spend_utxos().iter().rev() { - let spent_output = SpentOutput { - output: output.clone(), - inpoint: InPoint::Withdrawal { m6id: event.m6id }, - }; - state.stxos.put(rwtxn, outpoint, &spent_output)?; - if state.utxos.delete(rwtxn, outpoint)? { - return Err(Error::NoUtxo { - outpoint: *outpoint, - }); - }; - } - state.withdrawal_bundles.put( + } + let (prev_latest_failed_m6id, latest_failed_m6id) = state + .latest_failed_withdrawal_bundle + .get(rwtxn, &UnitKey)? + .expect("latest failed withdrawal bundle should exist") + .pop(); + assert_eq!(latest_failed_m6id.value, m6id); + assert_eq!(latest_failed_m6id.height, block_height); + if let Some(prev_latest_failed_m6id) = prev_latest_failed_m6id { + state.latest_failed_withdrawal_bundle.put( rwtxn, - &event.m6id, - &(bundle, prev_bundle_status), + &UnitKey, + &prev_latest_failed_m6id, )?; - let (prev_latest_failed_m6id, latest_failed_m6id) = state + } else { + state .latest_failed_withdrawal_bundle - .get(rwtxn, &UnitKey)? - .expect("latest failed withdrawal bundle should exist") - .pop(); - assert_eq!(latest_failed_m6id.value, event.m6id); - assert_eq!(latest_failed_m6id.height, block_height); - if let Some(prev_latest_failed_m6id) = prev_latest_failed_m6id { - state.latest_failed_withdrawal_bundle.put( - rwtxn, - &UnitKey, - &prev_latest_failed_m6id, - )?; - } else { - state - .latest_failed_withdrawal_bundle - .delete(rwtxn, &UnitKey)?; - } + .delete(rwtxn, &UnitKey)?; } } } + state.withdrawal_bundles.put( + rwtxn, + &m6id, + &(bundle, prev_bundle_status), + )?; + Ok(()) +} + +fn disconnect_withdrawal_bundle_event( + state: &State, + rwtxn: &mut RwTxn, + block_height: u32, + event: &WithdrawalBundleEvent, +) -> Result<(), Error> { + match event.status { + WithdrawalBundleStatus::Submitted => { + disconnect_withdrawal_bundle_submitted( + state, + rwtxn, + block_height, + event.m6id, + ) + } + WithdrawalBundleStatus::Confirmed => { + disconnect_withdrawal_bundle_confirmed( + state, + rwtxn, + block_height, + event.m6id, + ) + } + WithdrawalBundleStatus::Failed => disconnect_withdrawal_bundle_failed( + state, + rwtxn, + block_height, + event.m6id, + ), + } +} + +fn disconnect_event( + state: &State, + rwtxn: &mut RwTxn, + block_height: u32, + latest_deposit_block_hash: &mut Option, + latest_withdrawal_bundle_event_block_hash: &mut Option, + event_block_hash: bitcoin::BlockHash, + event: &BlockEvent, +) -> Result<(), Error> { + match event { + BlockEvent::Deposit(deposit) => { + let outpoint = OutPoint::Deposit(deposit.outpoint); + if !state.utxos.delete(rwtxn, &outpoint)? { + return Err(Error::NoUtxo { outpoint }); + } + *latest_deposit_block_hash = Some(event_block_hash); + } + BlockEvent::WithdrawalBundle(withdrawal_bundle_event) => { + let () = disconnect_withdrawal_bundle_event( + state, + rwtxn, + block_height, + withdrawal_bundle_event, + )?; + *latest_withdrawal_bundle_event_block_hash = Some(event_block_hash); + } + } + Ok(()) +} + +pub fn disconnect( + state: &State, + rwtxn: &mut RwTxn, + two_way_peg_data: &TwoWayPegData, +) -> Result<(), Error> { + let block_height = state + .try_get_height(rwtxn)? + .expect("Height should not be None"); + let mut latest_deposit_block_hash = None; + let mut latest_withdrawal_bundle_event_block_hash = None; + // Restore pending withdrawal bundle + for (event_block_hash, event_block_info) in + two_way_peg_data.block_info.iter().rev() + { + for event in event_block_info.events.iter().rev() { + let () = disconnect_event( + state, + rwtxn, + block_height, + &mut latest_deposit_block_hash, + &mut latest_withdrawal_bundle_event_block_hash, + *event_block_hash, + event, + )?; + } + } // Handle withdrawals if let Some(latest_withdrawal_bundle_event_block_hash) = - two_way_peg_data.latest_withdrawal_bundle_event_block_hash() + latest_withdrawal_bundle_event_block_hash { let ( last_withdrawal_bundle_event_block_seq_idx, @@ -584,7 +691,7 @@ pub fn disconnect( .last(rwtxn)? .ok_or(Error::NoWithdrawalBundleEventBlock)?; assert_eq!( - *latest_withdrawal_bundle_event_block_hash, + latest_withdrawal_bundle_event_block_hash, last_withdrawal_bundle_event_block_hash ); assert_eq!(block_height - 1, last_withdrawal_bundle_event_block_height); @@ -601,24 +708,14 @@ pub fn disconnect( .unwrap_or_default(); if block_height - last_withdrawal_bundle_failure_height > WITHDRAWAL_BUNDLE_FAILURE_GAP - && let Some((bundle, bundle_height)) = + && let Some((_bundle, bundle_height)) = state.pending_withdrawal_bundle.get(rwtxn, &UnitKey)? && bundle_height == block_height - 1 { state.pending_withdrawal_bundle.delete(rwtxn, &UnitKey)?; - for (outpoint, output) in bundle.spend_utxos().iter().rev() { - if !state.stxos.delete(rwtxn, outpoint)? { - return Err(Error::NoStxo { - outpoint: *outpoint, - }); - }; - state.utxos.put(rwtxn, outpoint, output)?; - } } // Handle deposits - if let Some(latest_deposit_block_hash) = - two_way_peg_data.latest_deposit_block_hash() - { + if let Some(latest_deposit_block_hash) = latest_deposit_block_hash { let ( last_deposit_block_seq_idx, (last_deposit_block_hash, last_deposit_block_height), @@ -635,15 +732,5 @@ pub fn disconnect( return Err(Error::NoDepositBlock); }; } - for deposit in two_way_peg_data - .deposits() - .flat_map(|(_, deposits)| deposits) - .rev() - { - let outpoint = OutPoint::Deposit(deposit.outpoint); - if !state.utxos.delete(rwtxn, &outpoint)? { - return Err(Error::NoUtxo { outpoint }); - } - } Ok(()) } From e62144e7d7be24ec37fb08d0df8417d808e0f5ee Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Fri, 7 Feb 2025 01:54:42 +0800 Subject: [PATCH 11/17] integration test fixups --- integration_tests/unknown_withdrawal.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/integration_tests/unknown_withdrawal.rs b/integration_tests/unknown_withdrawal.rs index 290e029..d872ae5 100644 --- a/integration_tests/unknown_withdrawal.rs +++ b/integration_tests/unknown_withdrawal.rs @@ -14,6 +14,7 @@ use bip300301_enforcer_integration_tests::{ use futures::{ channel::mpsc, future::BoxFuture, FutureExt as _, StreamExt as _, }; +use plain_bitnames::types::OutPoint; use plain_bitnames_app_rpc_api::RpcClient as _; use tokio::time::sleep; use tracing::Instrument as _; @@ -118,18 +119,25 @@ async fn unknown_withdrawal_task( tracing::info!("Deposited to sidechain successfully"); tracing::debug!("Checking that withdrawer sidechain recognizes deposit"); { - let withdrawer_utxos_count = - sidechain_withdrawer.rpc_client.list_utxos().await?.len(); + let withdrawer_deposit_utxos_count = sidechain_withdrawer + .rpc_client + .list_utxos() + .await? + .into_iter() + .filter(|utxo| matches!(utxo.outpoint, OutPoint::Deposit(_))) + .count(); sidechain_withdrawer .bmm_single(&mut enforcer_post_setup) .await?; - let utxos_count_delta = sidechain_withdrawer + let deposit_utxos_count_delta = sidechain_withdrawer .rpc_client .list_utxos() .await? - .len() - .checked_sub(withdrawer_utxos_count); - anyhow::ensure!(utxos_count_delta == Some(1)); + .into_iter() + .filter(|utxo| matches!(utxo.outpoint, OutPoint::Deposit(_))) + .count() + .checked_sub(withdrawer_deposit_utxos_count); + anyhow::ensure!(deposit_utxos_count_delta == Some(1)); } drop(sidechain_successor); drop(sidechain_withdrawer); From 069760428b574123c201376a3f3016885a32f46a Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Fri, 7 Feb 2025 02:11:11 +0800 Subject: [PATCH 12/17] update deps --- Cargo.lock | 567 ++++++++++++++++++++----------------- app/Cargo.toml | 2 +- app/gui/encrypt_message.rs | 5 +- deny.toml | 2 +- 4 files changed, 321 insertions(+), 255 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17f54df..75f7daf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -177,7 +177,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", "zerocopy", @@ -306,11 +306,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4" dependencies = [ "clipboard-win", + "core-graphics", + "image", "log", "objc2", "objc2-app-kit", "objc2-foundation", "parking_lot", + "windows-sys 0.48.0", "x11rb", ] @@ -458,7 +461,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -498,7 +501,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -509,13 +512,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.85" +version = "0.1.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" +checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -607,9 +610,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.25.0" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71b2ddd3ada61a305e1d8bb6c005d1eaa7d14d903681edfc400406d523a9b491" +checksum = "54ac4f13dad353b209b34cbec082338202cbc01c8f00336b55c750c13ac91f8f" dependencies = [ "bindgen", "cc", @@ -755,9 +758,9 @@ dependencies = [ [[package]] name = "bdk_wallet" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6362d350e13a94f68a102dafadbcc1500705016e4c2971b4c4e6f46054cb18" +checksum = "6a13c947be940d32a91b876fc5223a6d839a40bc219496c5c78af74714b1b3f7" dependencies = [ "bdk_chain", "bdk_file_store", @@ -809,7 +812,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.96", + "syn 2.0.98", "which", ] @@ -1112,32 +1115,32 @@ dependencies = [ [[package]] name = "borsh" -version = "1.5.4" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb65153674e51d3a42c8f27b05b9508cea85edfaade8aa46bc8fc18cecdfef3" +checksum = "5430e3be710b68d984d1391c854eb431a9d548640711faa54eecb1df93db91cc" dependencies = [ "borsh-derive", - "cfg_aliases 0.2.1", + "cfg_aliases", ] [[package]] name = "borsh-derive" -version = "1.5.4" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a396e17ad94059c650db3d253bb6e25927f1eb462eede7e7a153bb6e75dce0a7" +checksum = "f8b668d39970baad5356d7c83a86fee3a539e6f93bf6764c97368243e17a0487" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "bytemuck" @@ -1156,7 +1159,7 @@ checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1173,9 +1176,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" [[package]] name = "calloop" @@ -1205,9 +1208,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.9" +version = "1.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b" +checksum = "755717a7de9ec452bf7f3f1a3099085deabd7f2962b861dae91ecd7a365903d2" dependencies = [ "jobserver", "libc", @@ -1235,12 +1238,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "cfg_aliases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" - [[package]] name = "cfg_aliases" version = "0.2.1" @@ -1294,9 +1291,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.26" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783" +checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" dependencies = [ "clap_builder", "clap_derive", @@ -1304,9 +1301,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.26" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" dependencies = [ "anstream", "anstyle", @@ -1316,14 +1313,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.24" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1462,9 +1459,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -1572,7 +1569,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1627,7 +1624,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1638,7 +1635,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1696,7 +1693,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "unicode-xid", ] @@ -1746,7 +1743,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1802,9 +1799,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "ecolor" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d72e9c39f6e11a2e922d04a34ec5e7ef522ea3f5a1acfca7a19d16ad5fe50f5" +checksum = "878e9005799dd739e5d5d89ff7480491c12d0af571d44399bcaefa1ee172dd76" dependencies = [ "bytemuck", "emath", @@ -1858,14 +1855,14 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] name = "eframe" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2f2d9e7ea2d11ec9e98a8683b6eb99f9d7d0448394ef6e0d6d91bd4eb817220" +checksum = "eba4c50d905804fe9ec4e159fde06b9d38f9440228617ab64a03d7a2091ece63" dependencies = [ "ahash", "bytemuck", @@ -1874,7 +1871,7 @@ dependencies = [ "egui-wgpu", "egui-winit", "egui_glow", - "glow 0.16.0", + "glow", "glutin", "glutin-winit", "image", @@ -1899,12 +1896,13 @@ dependencies = [ [[package]] name = "egui" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "252d52224d35be1535d7fd1d6139ce071fb42c9097773e79f7665604f5596b5e" +checksum = "7d2768eaa6d5c80a6e2a008da1f0e062dff3c83eb2b28605ea2d0732d46e74d6" dependencies = [ "accesskit", "ahash", + "bitflags 2.8.0", "emath", "epaint", "log", @@ -1914,9 +1912,9 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c1e821d2d8921ef6ce98b258c7e24d9d6aab2ca1f9cdf374eca997e7f67f59" +checksum = "6d8151704bcef6271bec1806c51544d70e79ef20e8616e5eac01facfd9c8c54a" dependencies = [ "ahash", "bytemuck", @@ -1934,13 +1932,14 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e84c2919cd9f3a38a91e8f84ac6a245c19251fd95226ed9fae61d5ea564fce3" +checksum = "ace791b367c1f63e6044aef2f3834904509d1d1a6912fd23ebf3f6a9af92cd84" dependencies = [ "accesskit_winit", "ahash", "arboard", + "bytemuck", "egui", "log", "profiling", @@ -1953,14 +1952,14 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf6264cc7608e3e69a7d57a6175f438275f1b3889c1a551b418277721c95e6" +checksum = "9a53e2374a964c3c793cb0b8ead81bca631f24974bc0b747d1a5622f4e39fdd0" dependencies = [ "ahash", "bytemuck", "egui", - "glow 0.16.0", + "glow", "log", "memoffset", "profiling", @@ -1995,9 +1994,9 @@ dependencies = [ [[package]] name = "emath" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4fe73c1207b864ee40aa0b0c038d6092af1030744678c60188a05c28553515d" +checksum = "55b7b6be5ad1d247f11738b0e4699d9c20005ed366f2c29f5ec1f8e1de180bc2" dependencies = [ "bytemuck", ] @@ -2031,14 +2030,14 @@ checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] name = "enumflags2" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" +checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147" dependencies = [ "enumflags2_derive", "serde", @@ -2046,20 +2045,20 @@ dependencies = [ [[package]] name = "enumflags2_derive" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" +checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] name = "epaint" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5666f8d25236293c966fbb3635eac18b04ad1914e3bab55bc7d44b9980cafcac" +checksum = "275b665a7b9611d8317485187e5458750850f9e64604d3c58434bb3fc1d22915" dependencies = [ "ab_glyph", "ahash", @@ -2075,9 +2074,9 @@ dependencies = [ [[package]] name = "epaint_default_fonts" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66f6ddac3e6ac6fd4c3d48bb8b1943472f8da0f43a4303bcd8a18aa594401c80" +checksum = "9343d356d7cac894dacafc161b4654e0881301097bdf32a122ed503d97cb94b6" [[package]] name = "equivalent" @@ -2140,7 +2139,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2211,7 +2210,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2294,7 +2293,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2402,7 +2401,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2474,10 +2473,22 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", +] + [[package]] name = "ghash" version = "0.5.1" @@ -2557,18 +2568,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "glow" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51fa363f025f5c111e03f13eda21162faeacb6911fe8caa0c0349f9cf0c4483" -dependencies = [ - "js-sys", - "slotmap", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "glow" version = "0.16.0" @@ -2588,7 +2587,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03642b8b0cce622392deb0ee3e88511f75df2daac806102597905c3ea1974848" dependencies = [ "bitflags 2.8.0", - "cfg_aliases 0.2.1", + "cfg_aliases", "cgl", "core-foundation 0.9.4", "dispatch", @@ -2612,7 +2611,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" dependencies = [ - "cfg_aliases 0.2.1", + "cfg_aliases", "glutin", "raw-window-handle", "winit", @@ -2910,9 +2909,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.5" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" [[package]] name = "httpdate" @@ -2928,9 +2927,9 @@ checksum = "9994b79e8c1a39b3166c63ae7823bb2b00831e2a96a31399c50fe69df408eaeb" [[package]] name = "hyper" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", @@ -3135,7 +3134,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -3175,6 +3174,7 @@ dependencies = [ "byteorder-lite", "num-traits", "png", + "tiff", ] [[package]] @@ -3355,6 +3355,12 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + [[package]] name = "js-sys" version = "0.3.77" @@ -3367,9 +3373,9 @@ dependencies = [ [[package]] name = "jsonrpsee" -version = "0.24.7" +version = "0.24.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5c71d8c1a731cc4227c2f698d377e7848ca12c8a48866fc5e6951c43a4db843" +checksum = "834af00800e962dee8f7bfc0f60601de215e73e78e5497d733a2919da837d3c8" dependencies = [ "jsonrpsee-client-transport", "jsonrpsee-core", @@ -3385,9 +3391,9 @@ dependencies = [ [[package]] name = "jsonrpsee-client-transport" -version = "0.24.7" +version = "0.24.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "548125b159ba1314104f5bb5f38519e03a41862786aa3925cf349aae9cdd546e" +checksum = "def0fd41e2f53118bd1620478d12305b2c75feef57ea1f93ef70568c98081b7e" dependencies = [ "base64 0.22.1", "futures-channel", @@ -3410,9 +3416,9 @@ dependencies = [ [[package]] name = "jsonrpsee-core" -version = "0.24.7" +version = "0.24.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2882f6f8acb9fdaec7cefc4fd607119a9bd709831df7d7672a1d3b644628280" +checksum = "76637f6294b04e747d68e69336ef839a3493ca62b35bf488ead525f7da75c5bb" dependencies = [ "async-trait", "bytes", @@ -3425,7 +3431,7 @@ dependencies = [ "parking_lot", "pin-project", "rand", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "serde", "serde_json", "thiserror 1.0.69", @@ -3437,9 +3443,9 @@ dependencies = [ [[package]] name = "jsonrpsee-http-client" -version = "0.24.7" +version = "0.24.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3638bc4617f96675973253b3a45006933bde93c2fd8a6170b33c777cc389e5b" +checksum = "87c24e981ad17798bbca852b0738bfb7b94816ed687bd0d5da60bfa35fa0fdc3" dependencies = [ "async-trait", "base64 0.22.1", @@ -3462,22 +3468,22 @@ dependencies = [ [[package]] name = "jsonrpsee-proc-macros" -version = "0.24.7" +version = "0.24.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06c01ae0007548e73412c08e2285ffe5d723195bf268bce67b1b77c3bb2a14d" +checksum = "6fcae0c6c159e11541080f1f829873d8f374f81eda0abc67695a13fc8dc1a580" dependencies = [ "heck 0.5.0", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] name = "jsonrpsee-server" -version = "0.24.7" +version = "0.24.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82ad8ddc14be1d4290cd68046e7d1d37acd408efed6d3ca08aefcc3ad6da069c" +checksum = "66b7a3df90a1a60c3ed68e7ca63916b53e9afa928e33531e87f61a9c8e9ae87b" dependencies = [ "futures-util", "http", @@ -3502,9 +3508,9 @@ dependencies = [ [[package]] name = "jsonrpsee-types" -version = "0.24.7" +version = "0.24.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a178c60086f24cc35bb82f57c651d0d25d99c4742b4d335de04e97fa1f08a8a1" +checksum = "ddb81adb1a5ae9182df379e374a79e24e992334e7346af4d065ae5b2acb8d4c6" dependencies = [ "http", "serde", @@ -3514,9 +3520,9 @@ dependencies = [ [[package]] name = "jsonrpsee-wasm-client" -version = "0.24.7" +version = "0.24.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a01cd500915d24ab28ca17527e23901ef1be6d659a2322451e1045532516c25" +checksum = "42e41af42ca39657313748174d02766e5287d3a57356f16756dbd8065b933977" dependencies = [ "jsonrpsee-client-transport", "jsonrpsee-core", @@ -3525,9 +3531,9 @@ dependencies = [ [[package]] name = "jsonrpsee-ws-client" -version = "0.24.7" +version = "0.24.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe322e0896d0955a3ebdd5bf813571c53fea29edd713bc315b76620b327e86d" +checksum = "6f4f3642a292f5b76d8a16af5c88c16a0860f2ccc778104e5c848b28183d9538" dependencies = [ "http", "jsonrpsee-client-transport", @@ -3580,7 +3586,7 @@ dependencies = [ "proc-macro2", "proc_macro_roids", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -3759,7 +3765,7 @@ dependencies = [ "proc-macro2", "quote", "regex-syntax 0.8.5", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -3858,9 +3864,9 @@ dependencies = [ [[package]] name = "metal" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" dependencies = [ "bitflags 2.8.0", "block", @@ -3873,9 +3879,9 @@ dependencies = [ [[package]] name = "miette" -version = "7.4.0" +version = "7.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317f146e2eb7021892722af37cf1b971f0a70c8406f487e24952667616192c64" +checksum = "1a955165f87b37fd1862df2a59547ac542c77ef6d17c666f619d1ad22dd89484" dependencies = [ "cfg-if", "miette-derive", @@ -3885,13 +3891,13 @@ dependencies = [ [[package]] name = "miette-derive" -version = "7.4.0" +version = "7.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23c9b935fbe1d6cbd1dac857b54a688145e2d93f48db36010514d0f612d0ad67" +checksum = "bf45bf44ab49be92fd1227a3be6fc6f617f1a337c06af54981048574d8783147" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -3934,7 +3940,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -3956,7 +3962,7 @@ checksum = "a7ce64b975ed4f123575d11afd9491f2e37bbd5813fbfbc0f09ae1fbddea74e0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -3967,22 +3973,23 @@ checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" [[package]] name = "naga" -version = "23.1.0" +version = "24.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "364f94bc34f61332abebe8cad6f6cd82a5b65cff22c828d05d0968911462ca4f" +checksum = "e380993072e52eef724eddfcde0ed013b0c023c3f0417336ed041aa9f076994e" dependencies = [ "arrayvec", "bit-set", "bitflags 2.8.0", - "cfg_aliases 0.1.1", + "cfg_aliases", "codespan-reporting", "hexf-parse", "indexmap 2.7.1", "log", "rustc-hash 1.1.0", "spirv", + "strum", "termcolor", - "thiserror 1.0.69", + "thiserror 2.0.11", "unicode-xid", ] @@ -4039,7 +4046,7 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.8.0", "cfg-if", - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", "memoffset", ] @@ -4152,7 +4159,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -4264,9 +4271,9 @@ dependencies = [ [[package]] name = "objc2-encode" -version = "4.0.3" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" @@ -4417,14 +4424,14 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" @@ -4453,6 +4460,15 @@ dependencies = [ "libredox", ] +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -4493,7 +4509,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -4633,7 +4649,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -4647,22 +4663,22 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" +checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" +checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -4935,7 +4951,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" dependencies = [ "proc-macro2", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -4988,7 +5004,7 @@ checksum = "d0c2a098cd8aaa29f66da27a684ad19f4b7bc886f576abf12f7df4a7391071c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -5023,7 +5039,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.96", + "syn 2.0.98", "tempfile", ] @@ -5037,14 +5053,14 @@ dependencies = [ "itertools 0.13.0", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] name = "prost-reflect" -version = "0.14.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9647f03b808b79abca8408b1609be9887ba90453c940d00332a60eeb6f5748" +checksum = "e92b959d24e05a3e2da1d0beb55b48bc8a97059b8336ea617780bd6addbbfb5a" dependencies = [ "logos", "miette", @@ -5091,9 +5107,9 @@ dependencies = [ [[package]] name = "psl" -version = "2.1.78" +version = "2.1.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "923004c561dc4ed49d1e19cbf2af39780666b32d99941dafe8561f311daffd3d" +checksum = "2d460333b9d744945e0b9abd5469056aa925d526d48a458d6afc06eea9cbe206" dependencies = [ "psl-types", ] @@ -5116,9 +5132,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.36.2" +version = "0.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" dependencies = [ "memchr", ] @@ -5133,7 +5149,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "rustls", "socket2", "thiserror 2.0.11", @@ -5148,10 +5164,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" dependencies = [ "bytes", - "getrandom", + "getrandom 0.2.15", "rand", "ring", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "rustls", "rustls-pki-types", "rustls-platform-verifier 0.4.0", @@ -5168,7 +5184,7 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904" dependencies = [ - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", "once_cell", "socket2", @@ -5212,7 +5228,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", ] [[package]] @@ -5287,7 +5303,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ - "getrandom", + "getrandom 0.2.15", "libredox", "thiserror 2.0.11", ] @@ -5360,7 +5376,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.15", "libc", "spin", "untrusted", @@ -5411,9 +5427,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustc_version" @@ -5426,9 +5442,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.43" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags 2.8.0", "errno", @@ -5439,9 +5455,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.21" +version = "0.23.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" +checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7" dependencies = [ "aws-lc-rs", "log", @@ -5477,9 +5493,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" dependencies = [ "web-time", ] @@ -5501,7 +5517,7 @@ dependencies = [ "rustls-webpki", "security-framework", "security-framework-sys", - "webpki-roots 0.26.7", + "webpki-roots 0.26.8", "winapi", ] @@ -5552,9 +5568,9 @@ checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" [[package]] name = "ryu-js" @@ -5682,14 +5698,14 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] name = "serde_json" -version = "1.0.135" +version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" dependencies = [ "itoa", "memchr", @@ -5726,7 +5742,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -5754,7 +5770,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -5987,7 +6003,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -6009,9 +6025,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.96" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", @@ -6041,7 +6057,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -6051,13 +6067,13 @@ source = "git+https://gitlab.com/A-Manning/leonhard-llc-ops.git?branch=temp-dir- [[package]] name = "tempfile" -version = "3.15.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" dependencies = [ "cfg-if", "fastrand", - "getrandom", + "getrandom 0.3.1", "once_cell", "rustix", "windows-sys 0.59.0", @@ -6098,7 +6114,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -6109,7 +6125,7 @@ checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -6122,6 +6138,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.3.37" @@ -6246,7 +6273,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -6295,9 +6322,9 @@ checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" dependencies = [ "indexmap 2.7.1", "toml_datetime", @@ -6345,7 +6372,7 @@ dependencies = [ "prost-build", "prost-types", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -6439,7 +6466,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -6561,9 +6588,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] name = "unicode-normalization" @@ -6663,23 +6690,23 @@ checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] name = "uuid" -version = "1.12.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4" +checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" dependencies = [ - "getrandom", + "getrandom 0.3.1", ] [[package]] name = "valuable" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vcpkg" @@ -6751,6 +6778,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -6773,7 +6809,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "wasm-bindgen-shared", ] @@ -6808,7 +6844,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6824,9 +6860,9 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6" +checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" dependencies = [ "cc", "downcast-rs", @@ -6838,9 +6874,9 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280" +checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" dependencies = [ "bitflags 2.8.0", "rustix", @@ -6861,9 +6897,9 @@ dependencies = [ [[package]] name = "wayland-cursor" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b08bc3aafdb0035e7fe0fdf17ba0c09c268732707dca4ae098f60cb28c9e4c" +checksum = "a93029cbb6650748881a00e4922b076092a6a08c11e7fbdb923f064b23968c5d" dependencies = [ "rustix", "wayland-client", @@ -6872,9 +6908,9 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.32.5" +version = "0.32.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd0ade57c4e6e9a8952741325c30bf82f4246885dca8bf561898b86d0c1f58e" +checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc" dependencies = [ "bitflags 2.8.0", "wayland-backend", @@ -6884,9 +6920,9 @@ dependencies = [ [[package]] name = "wayland-protocols-plasma" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b31cab548ee68c7eb155517f2212049dc151f7cd7910c2b66abfd31c3ee12bd" +checksum = "7ccaacc76703fefd6763022ac565b590fcade92202492381c95b2edfdf7d46b3" dependencies = [ "bitflags 2.8.0", "wayland-backend", @@ -6897,9 +6933,9 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "782e12f6cd923c3c316130d56205ebab53f55d6666b7faddfad36cecaeeb4022" +checksum = "248a02e6f595aad796561fa82d25601bd2c8c3b145b1c7453fc8f94c1a58f8b2" dependencies = [ "bitflags 2.8.0", "wayland-backend", @@ -6910,20 +6946,20 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.5" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" +checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" dependencies = [ "proc-macro2", - "quick-xml 0.36.2", + "quick-xml 0.37.2", "quote", ] [[package]] name = "wayland-sys" -version = "0.31.5" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09" +checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" dependencies = [ "dlib", "log", @@ -6971,9 +7007,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "0.26.7" +version = "0.26.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cd5da49bdf1f30054cfe0b8ce2958b8fbeb67c4d82c8967a598af481bef255c" +checksum = "09aed61f5e8d2c18344b3faa33a4c837855fe56642757754775548fee21386c4" dependencies = [ "rustls-pki-types", ] @@ -6986,21 +7022,28 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "0.26.7" +version = "0.26.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" +checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "wgpu" -version = "23.0.1" +version = "24.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80f70000db37c469ea9d67defdc13024ddf9a5f1b89cb2941b812ad7cde1735a" +checksum = "47f55718f85c2fa756edffa0e7f0e0a60aba463d1362b57e23123c58f035e4b6" dependencies = [ "arrayvec", - "cfg_aliases 0.1.1", + "bitflags 2.8.0", + "cfg_aliases", "document-features", "js-sys", "log", @@ -7019,14 +7062,14 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "23.0.1" +version = "24.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d63c3c478de8e7e01786479919c8769f62a22eec16788d8c2ac77ce2c132778a" +checksum = "82a39b8842dc9ffcbe34346e3ab6d496b32a47f6497e119d762c97fcaae3cb37" dependencies = [ "arrayvec", "bit-vec", "bitflags 2.8.0", - "cfg_aliases 0.1.1", + "cfg_aliases", "document-features", "indexmap 2.7.1", "log", @@ -7037,25 +7080,25 @@ dependencies = [ "raw-window-handle", "rustc-hash 1.1.0", "smallvec", - "thiserror 1.0.69", + "thiserror 2.0.11", "wgpu-hal", "wgpu-types", ] [[package]] name = "wgpu-hal" -version = "23.0.1" +version = "24.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89364b8a0b211adc7b16aeaf1bd5ad4a919c1154b44c9ce27838213ba05fd821" +checksum = "5a782e5056b060b0b4010881d1decddd059e44f2ecd01e2db2971b48ad3627e5" dependencies = [ "android_system_properties", "arrayvec", "ash", "bitflags 2.8.0", "bytemuck", - "cfg_aliases 0.1.1", + "cfg_aliases", "core-graphics-types", - "glow 0.14.2", + "glow", "glutin_wgl_sys", "gpu-alloc", "gpu-descriptor", @@ -7069,13 +7112,14 @@ dependencies = [ "ndk-sys 0.5.0+25.2.9519653", "objc", "once_cell", + "ordered-float", "parking_lot", "profiling", "raw-window-handle", "renderdoc-sys", "rustc-hash 1.1.0", "smallvec", - "thiserror 1.0.69", + "thiserror 2.0.11", "wasm-bindgen", "web-sys", "wgpu-types", @@ -7084,12 +7128,13 @@ dependencies = [ [[package]] name = "wgpu-types" -version = "23.0.0" +version = "24.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "610f6ff27778148c31093f3b03abc4840f9636d58d597ca2f5977433acfe0068" +checksum = "50ac044c0e76c03a0378e7786ac505d010a873665e2d51383dcff8dd227dc69c" dependencies = [ "bitflags 2.8.0", "js-sys", + "log", "web-sys", ] @@ -7176,7 +7221,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -7187,7 +7232,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -7218,6 +7263,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -7416,9 +7470,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winit" -version = "0.30.8" +version = "0.30.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d74280aabb958072864bff6cfbcf9025cf8bfacdde5e32b5e12920ef703b0f" +checksum = "a809eacf18c8eca8b6635091543f02a5a06ddf3dad846398795460e6e0ae3cc0" dependencies = [ "ahash", "android-activity", @@ -7427,7 +7481,7 @@ dependencies = [ "block2", "bytemuck", "calloop", - "cfg_aliases 0.2.1", + "cfg_aliases", "concurrent-queue", "core-foundation 0.9.4", "core-graphics", @@ -7468,13 +7522,22 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.24" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" +checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f" dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.8.0", +] + [[package]] name = "write16" version = "1.0.0" @@ -7601,7 +7664,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "synstructure", ] @@ -7661,7 +7724,7 @@ checksum = "709ab20fc57cb22af85be7b360239563209258430bccf38d8b979c5a2ae3ecce" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "zbus-lockstep", "zbus_xml", "zvariant", @@ -7676,7 +7739,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "zvariant_utils", ] @@ -7722,7 +7785,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -7742,7 +7805,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "synstructure", ] @@ -7763,7 +7826,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -7812,7 +7875,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -7837,7 +7900,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "zvariant_utils", ] @@ -7849,5 +7912,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] diff --git a/app/Cargo.toml b/app/Cargo.toml index fddeecd..203e49b 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -14,7 +14,7 @@ blake3 = { workspace = true } borsh = { workspace = true } clap = { workspace = true, features = ["derive"] } dirs = "6.0.0" -eframe = "0.30.0" +eframe = "0.31.0" either = "1.13.0" futures = { workspace = true } hex = { workspace = true } diff --git a/app/gui/encrypt_message.rs b/app/gui/encrypt_message.rs index 1d74cb3..bdfba65 100644 --- a/app/gui/encrypt_message.rs +++ b/app/gui/encrypt_message.rs @@ -118,7 +118,10 @@ impl EncryptMessage { "Encrypted message: \n{ciphertext}" )); if ui.button("📋").on_hover_text("Click to copy").clicked() { - ui.output_mut(|po| po.copied_text.clone_from(ciphertext)); + ui.output_mut(|po| { + po.commands + .push(egui::OutputCommand::CopyText(ciphertext.clone())) + }); }; }); } diff --git a/deny.toml b/deny.toml index b414b4d..bf1a27d 100644 --- a/deny.toml +++ b/deny.toml @@ -113,7 +113,7 @@ confidence-threshold = 0.8 exceptions = [ # Each entry is the crate and version constraint, and its specific allow # list - { allow = ["LicenseRef-UFL-1.0", "OFL-1.1"], name = "epaint_default_fonts", version = "0.30.0" }, + { allow = ["LicenseRef-UFL-1.0", "OFL-1.1", "Ubuntu-font-1.0"], name = "epaint_default_fonts", version = "0.31.0" }, { allow = ["MPL-2.0"], name = "bitmaps", version = "3.2.1" }, { allow = ["MPL-2.0"], name = "imbl", version = "3.0.0" }, { allow = ["MPL-2.0"], name = "imbl-sized-chunks", version = "0.1.3" }, From 9bfdb56f2417f60589baaf550a2d28f46fd80808 Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Fri, 7 Feb 2025 02:12:20 +0800 Subject: [PATCH 13/17] ci: tune bip300301_enforcer download path --- .github/workflows/check_lint_build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check_lint_build.yaml b/.github/workflows/check_lint_build.yaml index e3191ee..8ce56b4 100644 --- a/.github/workflows/check_lint_build.yaml +++ b/.github/workflows/check_lint_build.yaml @@ -58,7 +58,7 @@ jobs: wget https://releases.drivechain.info/bip300301-enforcer-latest-x86_64-unknown-linux-gnu.zip unzip bip300301-enforcer-latest-x86_64-unknown-linux-gnu.zip rm bip300301-enforcer-latest-x86_64-unknown-linux-gnu.zip - mv bip300301-enforcer-latest-x86_64-unknown-linux-gnu/bip300301_enforcer-*-x86_64-unknown-linux-gnu bip300301-enforcer + mv bip300301-enforcer-latest-x86_64-unknown-linux-gnu/bip300301-enforcer-*-x86_64-unknown-linux-gnu bip300301-enforcer rm -r bip300301-enforcer-latest-x86_64-unknown-linux-gnu chmod +x bip300301-enforcer popd From 65540e0da7d68c955493c7173a31fdd173c93837 Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Fri, 7 Feb 2025 02:13:20 +0800 Subject: [PATCH 14/17] ci: avoid running checks for both PRs and PR branch pushes --- .github/workflows/check_lint_build.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check_lint_build.yaml b/.github/workflows/check_lint_build.yaml index 8ce56b4..f36492f 100644 --- a/.github/workflows/check_lint_build.yaml +++ b/.github/workflows/check_lint_build.yaml @@ -1,4 +1,8 @@ -on: [push] +on: + pull_request: + push: + branches: + - master name: Check, Lint, Build From a657cb9b9a8a66d9a26962c944189eb5f5f69bfe Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Fri, 7 Feb 2025 02:32:34 +0800 Subject: [PATCH 15/17] peer handling: friendlier error message on invalid address, more RPC output, introduce peer state concept --- app/rpc_server.rs | 3 +- cli/Cargo.toml | 2 +- integration_tests/ibd.rs | 8 +++- lib/Cargo.toml | 6 +-- lib/net/mod.rs | 83 +++++++++++++++++++++------------------- lib/net/peer.rs | 69 +++++++++++++++++++++++++++++++++ lib/node/mod.rs | 4 +- lib/types/schema.rs | 14 +++++++ rpc-api/Cargo.toml | 2 +- rpc-api/lib.rs | 14 ++++--- rpc-api/schema.rs | 9 ----- 11 files changed, 150 insertions(+), 64 deletions(-) diff --git a/app/rpc_server.rs b/app/rpc_server.rs index c8c2b22..4713ffd 100644 --- a/app/rpc_server.rs +++ b/app/rpc_server.rs @@ -8,6 +8,7 @@ use jsonrpsee::{ }; use plain_bitnames::{ + net::Peer, types::{ hashes::BitName, Address, BitNameData, Block, BlockHash, EncryptionPubKey, FilledOutput, OutPoint, PointedOutput, Transaction, @@ -214,7 +215,7 @@ impl RpcServer for RpcServerImpl { Ok(height) } - async fn list_peers(&self) -> RpcResult> { + async fn list_peers(&self) -> RpcResult> { let peers = self.app.node.get_active_peers(); Ok(peers) } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index d90ad88..6d65103 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -12,7 +12,7 @@ bitcoin = { workspace = true, features = ["serde"] } blake3 = { workspace = true } clap = { workspace = true, features = ["derive"] } jsonrpsee = { workspace = true, features = ["http-client"] } -plain_bitnames = { path = "../lib" } +plain_bitnames = { path = "../lib", features = ["clap"] } plain_bitnames_app_rpc_api = { path = "../rpc-api" } serde_json = { workspace = true } serde_json_canonicalizer = "0.3.0" diff --git a/integration_tests/ibd.rs b/integration_tests/ibd.rs index c65a72e..06372a1 100644 --- a/integration_tests/ibd.rs +++ b/integration_tests/ibd.rs @@ -78,7 +78,13 @@ async fn check_peer_connection( bitnames_setup: &PostSetup, expected_peer: SocketAddr, ) -> anyhow::Result<()> { - let peers = bitnames_setup.rpc_client.list_peers().await?; + let peers = bitnames_setup + .rpc_client + .list_peers() + .await? + .iter() + .map(|p| p.address) + .collect::>(); if peers.contains(&expected_peer) { Ok(()) } else { diff --git a/lib/Cargo.toml b/lib/Cargo.toml index e6646d0..8f151d1 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -44,7 +44,7 @@ semver = { version = "1.0.25", features = ["serde"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } serde_with = { workspace = true } -strum = { workspace = true, features = ["derive"], optional = true} +strum = { workspace = true, features = ["derive"] } thiserror = { workspace = true } tiny-bip39 = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "sync"] } @@ -57,7 +57,7 @@ x25519-dalek = { version = "2.0.0", features = ["serde"] } zeromq = { version = "0.4.1", optional = true } [features] -clap = ["dep:clap", "dep:strum"] +clap = ["dep:clap"] zmq = ["dep:zeromq"] [lints] @@ -65,4 +65,4 @@ workspace = true [lib] name = "plain_bitnames" -path = "lib.rs" \ No newline at end of file +path = "lib.rs" diff --git a/lib/net/mod.rs b/lib/net/mod.rs index 6489940..abd91a3 100644 --- a/lib/net/mod.rs +++ b/lib/net/mod.rs @@ -1,6 +1,6 @@ use std::{ collections::{hash_map, HashMap, HashSet}, - net::SocketAddr, + net::{IpAddr, SocketAddr}, sync::Arc, }; @@ -13,6 +13,7 @@ use heed::{ use parking_lot::RwLock; use quinn::{ClientConfig, Endpoint, ServerConfig}; use tokio_stream::StreamNotifyClose; +use tracing::instrument; use crate::{ archive::Archive, @@ -28,8 +29,8 @@ use peer::{ }; pub use peer::{ ConnectionError as PeerConnectionError, Info as PeerConnectionInfo, - InternalMessage as PeerConnectionMessage, PeerStateId, - Request as PeerRequest, Response as PeerResponse, + InternalMessage as PeerConnectionMessage, Peer, PeerConnectionStatus, + PeerStateId, Request as PeerRequest, Response as PeerResponse, }; #[derive(Debug, thiserror::Error)] @@ -68,6 +69,10 @@ pub enum Error { SendDatagram(#[from] quinn::SendDatagramError), #[error("server endpoint closed")] ServerEndpointClosed, + /// Unspecified peer IP addresses cannot be connected to. + /// `0.0.0.0` is one example of an "unspecified" IP. + #[error("unspecified peer ip address (cannot connect to '{0}')")] + UnspecfiedPeerIP(IpAddr), #[error("write error")] Write(#[from] quinn::WriteError), } @@ -159,11 +164,11 @@ fn configure_server() -> Result<(ServerConfig, Vec), Error> { /// /// - a stream of incoming QUIC connections /// - server certificate serialized into DER format -#[allow(unused)] pub fn make_server_endpoint( bind_addr: SocketAddr, ) -> Result<(Endpoint, Vec), Error> { let (server_config, server_cert) = configure_server()?; + tracing::info!(%bind_addr, "creating server endpoint"); let mut endpoint = Endpoint::server(server_config, bind_addr)?; let client_cfg = configure_client()?; endpoint.set_default_client_config(client_cfg); @@ -189,9 +194,6 @@ const fn seed_node_addrs(network: Network) -> &'static [SocketAddr] { } } -// State. -// Archive. - // Keep track of peer state // Exchange metadata // Bulk download @@ -245,20 +247,36 @@ impl Net { } } - // TODO: This should have more context. Last received message, connection state, etc. - pub fn get_active_peers(&self) -> Vec { - self.active_peers.read().keys().copied().collect() + // TODO: This should have more context. + // Last received message, connection state, etc. + pub fn get_active_peers(&self) -> Vec { + self.active_peers + .read() + .iter() + .map(|(addr, conn_handle)| Peer { + address: *addr, + status: conn_handle.connection_status(), + }) + .collect() } + #[instrument(skip_all, fields(addr), err(Debug))] pub fn connect_peer( &self, env: heed::Env, addr: SocketAddr, ) -> Result<(), Error> { if self.active_peers.read().contains_key(&addr) { - tracing::error!(%addr, "already connected"); + tracing::error!("already connected"); return Err(Error::AlreadyConnected(addr)); } + // This check happens within Quinn with a + // generic "invalid remote address". We run the + // same check, and provide a friendlier error + // message. + if addr.ip().is_unspecified() { + return Err(Error::UnspecfiedPeerIP(addr.ip())); + } let connecting = self.server.connect(addr, "localhost")?; let mut rwtxn = env.write_txn()?; self.known_peers.put(&mut rwtxn, &addr, &())?; @@ -270,18 +288,18 @@ impl Net { }; let (connection_handle, info_rx) = peer::connect(connecting, connection_ctxt); - tracing::trace!(%addr, "spawning info rx"); + tracing::trace!("spawning info rx"); tokio::spawn({ let info_rx = StreamNotifyClose::new(info_rx) .map(move |info| Ok((addr, info))); let peer_info_tx = self.peer_info_tx.clone(); async move { if let Err(_send_err) = info_rx.forward(peer_info_tx).await { - tracing::error!(%addr, "Failed to send peer connection info"); + tracing::error!("Failed to send peer connection info"); } } }); - tracing::trace!(%addr, "adding to active peers"); + tracing::trace!("adding to active peers"); self.add_active_peer(addr, connection_handle)?; Ok(()) } @@ -420,6 +438,7 @@ impl Net { } } }); + // TODO: is this the right state? self.add_active_peer(addr, connection_handle)?; Ok(Some(addr)) } @@ -431,9 +450,9 @@ impl Net { addr: SocketAddr, ) -> Result<(), Error> { let active_peers_read = self.active_peers.read(); - let Some(peer_connection_handle) = active_peers_read.get(&addr) else { - return Err(Error::MissingPeerConnection(addr)); - }; + let peer_connection_handle = active_peers_read + .get(&addr) + .ok_or_else(|| Error::MissingPeerConnection(addr))?; if let Err(send_err) = peer_connection_handle .internal_message_tx .unbounded_send(message) @@ -444,29 +463,6 @@ impl Net { Ok(()) } - // Push a request to the specified peers - pub fn push_request( - &self, - request: PeerRequest, - peers: &HashSet, - ) { - let active_peers_read = self.active_peers.read(); - for addr in peers { - let Some(peer_connection_handle) = active_peers_read.get(addr) - else { - continue; - }; - if let Err(_send_err) = peer_connection_handle - .internal_message_tx - .unbounded_send(request.clone().into()) - { - tracing::warn!( - "Failed to push request to peer at {addr}: {request:?}" - ) - } - } - } - /// Push a tx to all active peers, except those in the provided set pub fn push_tx( &self, @@ -478,6 +474,13 @@ impl Net { .iter() .filter(|(addr, _)| !exclude.contains(addr)) .for_each(|(addr, peer_connection_handle)| { + match peer_connection_handle.connection_status() { + PeerConnectionStatus::Connecting => { + tracing::trace!(%addr, "skipping peer at {addr} because it is not fully connected"); + return; + } + PeerConnectionStatus::Connected => {} + } let request = PeerRequest::PushTransaction { transaction: tx.clone(), }; diff --git a/lib/net/peer.rs b/lib/net/peer.rs index 3d643ac..9037dfb 100644 --- a/lib/net/peer.rs +++ b/lib/net/peer.rs @@ -2,6 +2,10 @@ use std::{ cmp::Ordering, collections::{HashMap, HashSet}, net::SocketAddr, + sync::{ + atomic::{self, AtomicBool}, + Arc, + }, }; use bitcoin::Work; @@ -1244,13 +1248,60 @@ impl ConnectionTask { } } +#[derive( + Clone, + Copy, + Eq, + PartialEq, + serde::Serialize, + serde::Deserialize, + strum::Display, + utoipa::ToSchema, +)] +pub enum PeerConnectionStatus { + /// We're still in the process of initializing the peer connection + Connecting, + /// The connection is successfully established + Connected, +} + +impl PeerConnectionStatus { + /// Convert from boolean representation + // Should remain private to this module + fn from_repr(repr: bool) -> Self { + match repr { + false => Self::Connecting, + true => Self::Connected, + } + } + + /// Convert to boolean representation + // Should remain private to this module + fn as_repr(self) -> bool { + match self { + Self::Connecting => false, + Self::Connected => true, + } + } +} + /// Connection killed on drop pub struct ConnectionHandle { task: JoinHandle<()>, + /// Representation of [`PeerConnectionStatus`] + pub(in crate::net) status_repr: Arc, /// Push messages from connection task / net task / node pub internal_message_tx: mpsc::UnboundedSender, } +impl ConnectionHandle { + pub fn connection_status(&self) -> PeerConnectionStatus { + PeerConnectionStatus::from_repr( + self.status_repr.load(atomic::Ordering::SeqCst), + ) + } +} + impl Drop for ConnectionHandle { fn drop(&mut self) { self.task.abort() @@ -1289,8 +1340,10 @@ pub fn handle( } } }); + let status = PeerConnectionStatus::Connected; let connection_handle = ConnectionHandle { task, + status_repr: Arc::new(AtomicBool::new(status.as_repr())), internal_message_tx, }; (connection_handle, info_rx) @@ -1300,13 +1353,20 @@ pub fn connect( connecting: quinn::Connecting, ctxt: ConnectionContext, ) -> (ConnectionHandle, mpsc::UnboundedReceiver) { + let connection_status = PeerConnectionStatus::Connecting; + let status_repr = Arc::new(AtomicBool::new(connection_status.as_repr())); let (internal_message_tx, internal_message_rx) = mpsc::unbounded(); let (info_tx, info_rx) = mpsc::unbounded(); let connection_task = { + let status_repr = status_repr.clone(); let info_tx = info_tx.clone(); let internal_message_tx = internal_message_tx.clone(); move || async move { let connection = Connection::new(connecting).await?; + status_repr.store( + PeerConnectionStatus::Connected.as_repr(), + atomic::Ordering::SeqCst, + ); let connection_task = ConnectionTask { connection, ctxt, @@ -1328,7 +1388,16 @@ pub fn connect( }); let connection_handle = ConnectionHandle { task, + status_repr, internal_message_tx, }; (connection_handle, info_rx) } + +// RPC output representation for peer + state +#[derive(Clone, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] +pub struct Peer { + #[schema(value_type = crate::types::schema::SocketAddr)] + pub address: SocketAddr, + pub status: PeerConnectionStatus, +} diff --git a/lib/node/mod.rs b/lib/node/mod.rs index e8f267b..d9ee2b9 100644 --- a/lib/node/mod.rs +++ b/lib/node/mod.rs @@ -17,7 +17,7 @@ use tonic::transport::Channel; use crate::{ archive::{self, Archive}, mempool::{self, MemPool}, - net::{self, Net}, + net::{self, Net, Peer}, state::{self, State}, types::{ proto::{self, mainchain}, @@ -642,7 +642,7 @@ where .map_err(Error::from) } - pub fn get_active_peers(&self) -> Vec { + pub fn get_active_peers(&self) -> Vec { self.net.get_active_peers() } diff --git a/lib/types/schema.rs b/lib/types/schema.rs index 91bda74..647dd88 100644 --- a/lib/types/schema.rs +++ b/lib/types/schema.rs @@ -63,3 +63,17 @@ impl ToSchema for BitcoinTransaction { std::borrow::Cow::Borrowed("bitcoin.Transaction") } } + +pub struct SocketAddr; + +impl PartialSchema for SocketAddr { + fn schema() -> RefOr { + let obj = utoipa::openapi::Object::with_type(openapi::Type::String); + RefOr::T(Schema::Object(obj)) + } +} +impl ToSchema for SocketAddr { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("net.SocketAddr") + } +} diff --git a/rpc-api/Cargo.toml b/rpc-api/Cargo.toml index 213c00c..3c774d2 100644 --- a/rpc-api/Cargo.toml +++ b/rpc-api/Cargo.toml @@ -24,4 +24,4 @@ workspace = true [lib] name = "plain_bitnames_app_rpc_api" -path = "lib.rs" \ No newline at end of file +path = "lib.rs" diff --git a/rpc-api/lib.rs b/rpc-api/lib.rs index 4a81619..0277a9e 100644 --- a/rpc-api/lib.rs +++ b/rpc-api/lib.rs @@ -5,6 +5,7 @@ use std::{collections::HashMap, net::SocketAddr}; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; use l2l_openapi::open_api; use plain_bitnames::{ + net::{Peer, PeerConnectionStatus}, types::{ hashes::BitName, schema as bitnames_schema, Address, Authorization, BatchIcannRegistrationData, BitNameData, BitNameDataUpdates, @@ -33,11 +34,12 @@ pub struct TxInfo { #[open_api(ref_schemas[ bitnames_schema::BitcoinAddr, bitnames_schema::BitcoinBlockHash, bitnames_schema::BitcoinOutPoint, bitnames_schema::BitcoinTransaction, + bitnames_schema::SocketAddr, Address, Authorization, BatchIcannRegistrationData, BitName, BitNameDataUpdates, BitNameSeqId, BlockHash, Body, EncryptionPubKey, FilledOutput, FilledOutputContent, Header, MerkleRoot, MutableBitNameData, - OutPoint, Output, OutputContent, Transaction, TransactionData, Txid, TxIn, - VerifyingKey, + OutPoint, Output, OutputContent, PeerConnectionStatus, Transaction, + TransactionData, Txid, TxIn, VerifyingKey, ])] #[rpc(client, server)] pub trait Rpc { @@ -73,7 +75,9 @@ pub trait Rpc { #[method(name = "connect_peer")] async fn connect_peer( &self, - #[open_api_method_arg(schema(ToSchema = "schema::SocketAddr"))] + #[open_api_method_arg(schema( + ToSchema = "bitnames_schema::SocketAddr" + ))] addr: SocketAddr, ) -> RpcResult<()>; @@ -154,10 +158,8 @@ pub trait Rpc { ) -> RpcResult>; /// List peers - /// TODO: Use schema::SocketAddr. Cannot get it to work. Also, add more info about peers - #[open_api_method(output_schema(PartialSchema = "schema::SocketAddr"))] #[method(name = "list_peers")] - async fn list_peers(&self) -> RpcResult>; + async fn list_peers(&self) -> RpcResult>; /// List all UTXOs #[open_api_method(output_schema( diff --git a/rpc-api/schema.rs b/rpc-api/schema.rs index 8c5b7f7..d8fd55a 100644 --- a/rpc-api/schema.rs +++ b/rpc-api/schema.rs @@ -41,12 +41,3 @@ impl PartialSchema for OpenApi { RefOr::T(Schema::Object(obj)) } } - -pub struct SocketAddr; - -impl PartialSchema for SocketAddr { - fn schema() -> RefOr { - let obj = utoipa::openapi::Object::with_type(openapi::Type::String); - RefOr::T(Schema::Object(obj)) - } -} From f12650999735a0e440aa450cd5afea441ba157a8 Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Fri, 7 Feb 2025 02:33:40 +0800 Subject: [PATCH 16/17] Run CI on pushed tags and workflow dispatch --- .github/workflows/check_lint_build.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check_lint_build.yaml b/.github/workflows/check_lint_build.yaml index f36492f..6686da8 100644 --- a/.github/workflows/check_lint_build.yaml +++ b/.github/workflows/check_lint_build.yaml @@ -1,8 +1,11 @@ on: pull_request: push: - branches: + branches: - master + tags: + - '*' + workflow_dispatch: name: Check, Lint, Build From 06b1c8af500db836f53ccdf9f172a3f2789384cb Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Fri, 7 Feb 2025 02:34:18 +0800 Subject: [PATCH 17/17] bump version for release --- Cargo.lock | 10 +++++----- Cargo.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 75f7daf..d2c8132 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4722,7 +4722,7 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "plain_bitnames" -version = "0.10.0" +version = "0.10.1" dependencies = [ "addr", "anyhow", @@ -4773,7 +4773,7 @@ dependencies = [ [[package]] name = "plain_bitnames_app" -version = "0.10.0" +version = "0.10.1" dependencies = [ "anyhow", "bincode", @@ -4813,7 +4813,7 @@ dependencies = [ [[package]] name = "plain_bitnames_app_cli" -version = "0.10.0" +version = "0.10.1" dependencies = [ "anyhow", "bitcoin", @@ -4830,7 +4830,7 @@ dependencies = [ [[package]] name = "plain_bitnames_app_rpc_api" -version = "0.10.0" +version = "0.10.1" dependencies = [ "anyhow", "bitcoin", @@ -4845,7 +4845,7 @@ dependencies = [ [[package]] name = "plain_bitnames_integration_tests" -version = "0.10.0" +version = "0.10.1" dependencies = [ "anyhow", "bip300301_enforcer_integration_tests", diff --git a/Cargo.toml b/Cargo.toml index f388bb3..ad01d76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,7 +64,7 @@ authors = [ "Ash Manning " ] edition = "2021" license-file = "LICENSE.txt" publish = false -version = "0.10.0" +version = "0.10.1" # temp-dir does not leak correctly after clone # https://gitlab.com/leonhard-llc/ops/-/issues/17