diff --git a/.github/workflows/check_lint_build.yaml b/.github/workflows/check_lint_build.yaml index e51e481..baddee0 100644 --- a/.github/workflows/check_lint_build.yaml +++ b/.github/workflows/check_lint_build.yaml @@ -59,6 +59,17 @@ jobs: chmod +x bitcoin-patched-bins/bitcoind chmod +x bitcoin-patched-bins/bitcoin-cli popd + + - name: Download bitcoin unpatched + run: | + pushd .. + wget https://bitcoincore.org/bin/bitcoin-core-30.2/bitcoin-30.2-x86_64-linux-gnu.tar.gz + tar -xf bitcoin-30.2-x86_64-linux-gnu.tar.gz + rm bitcoin-30.2-x86_64-linux-gnu.tar.gz + mv bitcoin-30.2/bin bitcoin-unpatched-bins + rm -r bitcoin-30.2 + popd + - name: Download latest bip300301_enforcer run: | pushd .. @@ -103,6 +114,7 @@ jobs: run: | export BIP300301_ENFORCER='../bip300301-enforcer' export BITCOIND='../bitcoin-patched-bins/bitcoind' + export BITCOIND_UNPATCHED='../bitcoin-unpatched-bins/bitcoind' export BITCOIN_CLI='../bitcoin-patched-bins/bitcoin-cli' export ELECTRS='../electrs/target/release/electrs' export BITNAMES_APP='target/debug/plain_bitnames_app' diff --git a/Cargo.lock b/Cargo.lock index d3dc192..06b0ebf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -763,7 +763,7 @@ dependencies = [ [[package]] name = "bip300301_enforcer_integration_tests" version = "0.3.4" -source = "git+https://github.com/LayerTwo-Labs/bip300301_enforcer?rev=a8d169ee34aeddf6d354aa52376d847831d7fbb3#a8d169ee34aeddf6d354aa52376d847831d7fbb3" +source = "git+https://github.com/LayerTwo-Labs/bip300301_enforcer?rev=9c3eb0bdcb5b9a458e6f92cafb8bdbf76a860f74#9c3eb0bdcb5b9a458e6f92cafb8bdbf76a860f74" dependencies = [ "anyhow", "bdk_wallet", @@ -796,7 +796,7 @@ dependencies = [ [[package]] name = "bip300301_enforcer_lib" version = "0.3.4" -source = "git+https://github.com/LayerTwo-Labs/bip300301_enforcer?rev=a8d169ee34aeddf6d354aa52376d847831d7fbb3#a8d169ee34aeddf6d354aa52376d847831d7fbb3" +source = "git+https://github.com/LayerTwo-Labs/bip300301_enforcer?rev=9c3eb0bdcb5b9a458e6f92cafb8bdbf76a860f74#9c3eb0bdcb5b9a458e6f92cafb8bdbf76a860f74" dependencies = [ "aes-gcm", "argon2", diff --git a/Cargo.toml b/Cargo.toml index 2d048c6..5c891a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,12 +48,12 @@ x25519-dalek = "2.0.0" [workspace.dependencies.bip300301_enforcer_lib] default-features = false git = "https://github.com/LayerTwo-Labs/bip300301_enforcer" -rev = "a8d169ee34aeddf6d354aa52376d847831d7fbb3" +rev = "9c3eb0bdcb5b9a458e6f92cafb8bdbf76a860f74" [workspace.dependencies.bip300301_enforcer_integration_tests] default-features = false git = "https://github.com/LayerTwo-Labs/bip300301_enforcer" -rev = "a8d169ee34aeddf6d354aa52376d847831d7fbb3" +rev = "9c3eb0bdcb5b9a458e6f92cafb8bdbf76a860f74" [workspace.dependencies.l2l-openapi] git = "https://github.com/Ash-L2L/l2l-openapi" diff --git a/app/rpc_server.rs b/app/rpc_server.rs index f769389..071dd38 100644 --- a/app/rpc_server.rs +++ b/app/rpc_server.rs @@ -353,7 +353,7 @@ impl RpcServer for RpcServerImpl { ) -> RpcResult> { self.app .node - .get_pending_withdrawal_bundle() + .try_get_pending_withdrawal_bundle() .map_err(custom_err) } diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index 7a56fb4..8260521 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -17,7 +17,7 @@ dotenvy = "0.15.7" futures = { workspace = true } jsonrpsee = { workspace = true } libtest-mimic = "0.8.1" -plain_bitnames = { path = "../lib" } +plain_bitnames = { path = "../lib", features = ["clap"] } plain_bitnames_app_rpc_api = { path = "../rpc-api" } reserve-port = "2.0.1" thiserror = { workspace = true } diff --git a/integration_tests/example.env b/integration_tests/example.env index b3523ea..5488b6f 100644 --- a/integration_tests/example.env +++ b/integration_tests/example.env @@ -1,5 +1,6 @@ BIP300301_ENFORCER='../bip300301_enforcer/target/debug/bip300301_enforcer' -BITCOIND='../bitcoin/build/src/bitcoind' +BITCOIND='../bitcoin-patched/build/src/bitcoind' +BITCOIND_UNPATCHED='../bitcoin/build/src/bitcoind' BITCOIN_CLI='../bitcoin/build/src/bitcoin-cli' ELECTRS='../electrs/target/release/electrs' BITNAMES_APP='target/debug/plain_bitnames_app' diff --git a/integration_tests/ibd.rs b/integration_tests/ibd.rs index 075ae5a..f511422 100644 --- a/integration_tests/ibd.rs +++ b/integration_tests/ibd.rs @@ -5,8 +5,9 @@ use std::net::SocketAddr; use bip300301_enforcer_integration_tests::{ integration_test::{activate_sidechain, fund_enforcer, propose_sidechain}, setup::{ - Mode, Network, PostSetup as EnforcerPostSetup, Sidechain as _, - setup as setup_enforcer, + Mode, Network, PostSetup as EnforcerPostSetup, + PreSetup as EnforcerPreSetup, SetupOpts as EnforcerSetupOpts, + Sidechain as _, }, util::{AbortOnDrop, AsyncTrial, TestFailureCollector, TestFileRegistry}, }; @@ -33,13 +34,14 @@ async fn setup( bin_paths: BinPaths, res_tx: mpsc::UnboundedSender>, ) -> anyhow::Result<(EnforcerPostSetup, BitNamesNodes)> { - let mut enforcer_post_setup = setup_enforcer( - &bin_paths.others, - Network::Regtest, - Mode::Mempool, - res_tx.clone(), - ) - .await?; + let enforcer_pre_setup = + EnforcerPreSetup::new(bin_paths.others, Network::Regtest)?; + let mut enforcer_post_setup = { + let setup_opts: EnforcerSetupOpts = Default::default(); + enforcer_pre_setup + .setup(Mode::Mempool, setup_opts, res_tx.clone()) + .await? + }; let sidechain_sender = PostSetup::setup( Init { bitnames_app: bin_paths.bitnames.clone(), diff --git a/integration_tests/integration_test.rs b/integration_tests/integration_test.rs index 8e0b49c..8dde7bd 100644 --- a/integration_tests/integration_test.rs +++ b/integration_tests/integration_test.rs @@ -1,8 +1,15 @@ use bip300301_enforcer_integration_tests::{ - setup::{Mode, Network}, + integration_test as bip300301_enforcer_integration_test, + setup::{ + Mode, Network, PostSetup as EnforcerPostSetup, + PreSetup as EnforcerPreSetup, SetupOpts as EnforcerSetupOpts, + Sidechain as _, + }, util::{AsyncTrial, TestFailureCollector, TestFileRegistry}, }; -use futures::{FutureExt, future::BoxFuture}; +use bip300301_enforcer_lib::bins::CommandExt; +use futures::{FutureExt, channel::mpsc::UnboundedSender, future::BoxFuture}; +use plain_bitnames_app_rpc_api::RpcClient as _; use crate::{ ibd::ibd_trial, @@ -12,7 +19,112 @@ use crate::{ util::BinPaths, }; -fn deposit_withdraw_roundtrip( +#[allow(clippy::significant_drop_tightening, reason = "false positive")] +pub async fn deposit_withdraw_roundtrip_task( + post_setup: &mut EnforcerPostSetup, + res_tx: UnboundedSender>, + init: Init, +) -> anyhow::Result { + use bip300301_enforcer_integration_test::{ + activate_sidechain, deposit, fund_enforcer, propose_sidechain, + wait_for_wallet_sync, withdraw_succeed, + }; + use bitcoin::Amount; + + const DEPOSIT_AMOUNT: Amount = Amount::from_sat(21_000_000); + const DEPOSIT_FEE: Amount = Amount::from_sat(1_000_000); + const WITHDRAW_AMOUNT: Amount = Amount::from_sat(18_000_000); + const WITHDRAW_FEE: Amount = Amount::from_sat(1_000_000); + + let mut sidechain = PostSetup::setup(init, post_setup, res_tx).await?; + tracing::info!("Setup successfully"); + let () = propose_sidechain::(post_setup).await?; + tracing::info!("Proposed sidechain successfully"); + let () = activate_sidechain::(post_setup).await?; + tracing::info!("Activated sidechain successfully"); + let () = fund_enforcer::(post_setup).await?; + tracing::info!("Funded enforcer successfully"); + let deposit_address = sidechain.get_deposit_address().await?; + let () = deposit( + post_setup, + &mut sidechain, + &deposit_address, + DEPOSIT_AMOUNT, + DEPOSIT_FEE, + ) + .await?; + tracing::info!("Deposited to sidechain successfully"); + // Wait for mempool to catch up before attempting second deposit + tracing::debug!("Waiting for wallet sync..."); + let () = wait_for_wallet_sync().await?; + tracing::info!("Attempting second deposit"); + let () = deposit( + post_setup, + &mut sidechain, + &deposit_address, + DEPOSIT_AMOUNT, + DEPOSIT_FEE, + ) + .await?; + tracing::info!("Deposited to sidechain successfully"); + let sidechain_block_count = sidechain.rpc_client.getblockcount().await?; + let target_sidechain_block_height = 5; + tracing::info!( + sidechain_block_count, + target_sidechain_block_height, + "BMMing sidechain blocks..." + ); + sidechain + .bmm( + post_setup, + target_sidechain_block_height - sidechain_block_count, + ) + .await?; + let () = withdraw_succeed( + post_setup, + &mut sidechain, + WITHDRAW_AMOUNT, + WITHDRAW_FEE, + Amount::ZERO, + ) + .await?; + tracing::info!("Withdrawal succeeded"); + let mainchain_block_count = post_setup + .bitcoin_cli + .command::([], "getblockcount", []) + .run_utf8() + .await?; + let sidechain_block_count = sidechain.rpc_client.getblockcount().await?; + tracing::info!(%mainchain_block_count, sidechain_block_count); + Ok(sidechain) +} + +async fn deposit_withdraw_roundtrip( + mut post_setup: EnforcerPostSetup, + init: Init, + res_tx: UnboundedSender>, +) -> anyhow::Result<()> { + let sidechain_post_setup = + deposit_withdraw_roundtrip_task(&mut post_setup, res_tx, init).await?; + // check that everything is ok after BMM'ing 3 blocks + let mut block_count_pre = + sidechain_post_setup.rpc_client.getblockcount().await?; + sidechain_post_setup.bmm_single(&mut post_setup).await?; + let mut block_count_post = + sidechain_post_setup.rpc_client.getblockcount().await?; + anyhow::ensure!(block_count_post == block_count_pre + 1); + block_count_pre = block_count_post; + sidechain_post_setup.bmm_single(&mut post_setup).await?; + block_count_post = sidechain_post_setup.rpc_client.getblockcount().await?; + anyhow::ensure!(block_count_post == block_count_pre + 1); + block_count_pre = block_count_post; + sidechain_post_setup.bmm_single(&mut post_setup).await?; + block_count_post = sidechain_post_setup.rpc_client.getblockcount().await?; + anyhow::ensure!(block_count_post == block_count_pre + 1); + Ok(()) +} + +fn deposit_withdraw_roundtrip_trial( bin_paths: BinPaths, file_registry: TestFileRegistry, failure_collector: TestFailureCollector, @@ -21,20 +133,25 @@ fn deposit_withdraw_roundtrip( "deposit_withdraw_roundtrip", async move { let (res_tx, _) = futures::channel::mpsc::unbounded(); - let post_setup = bip300301_enforcer_integration_tests::setup::setup( - &bin_paths.others, - Network::Regtest, - Mode::Mempool, - res_tx - ).await?; - bip300301_enforcer_integration_tests::integration_test::deposit_withdraw_roundtrip::( - post_setup, - Init { - bitnames_app: bin_paths.bitnames, - data_dir_suffix: None, - }, - ).await - }.boxed(), + let pre_setup = + EnforcerPreSetup::new(bin_paths.others, Network::Regtest)?; + let post_setup = { + let setup_opts: EnforcerSetupOpts = Default::default(); + pre_setup + .setup(Mode::Mempool, setup_opts, res_tx.clone()) + .await? + }; + deposit_withdraw_roundtrip( + post_setup, + Init { + bitnames_app: bin_paths.bitnames, + data_dir_suffix: None, + }, + res_tx, + ) + .await + } + .boxed(), file_registry, failure_collector, ) @@ -46,7 +163,7 @@ pub fn tests( failure_collector: TestFailureCollector, ) -> Vec>>> { vec![ - deposit_withdraw_roundtrip( + deposit_withdraw_roundtrip_trial( bin_paths.clone(), file_registry.clone(), failure_collector.clone(), diff --git a/integration_tests/main.rs b/integration_tests/main.rs index ca7f3e8..01cbafb 100644 --- a/integration_tests/main.rs +++ b/integration_tests/main.rs @@ -52,6 +52,7 @@ fn set_tracing_subscriber(log_level: tracing::Level) -> anyhow::Result<()> { let targets_filter = { let default_directives_str = targets_directive_str([ ("", saturating_pred_level(log_level)), + ("bip300301_enforcer_integration_tests", log_level), ("integration_tests", log_level), ]); let directives_str = diff --git a/integration_tests/register_bitname.rs b/integration_tests/register_bitname.rs index 5238efa..ab47b15 100644 --- a/integration_tests/register_bitname.rs +++ b/integration_tests/register_bitname.rs @@ -5,8 +5,9 @@ use bip300301_enforcer_integration_tests::{ activate_sidechain, deposit, fund_enforcer, propose_sidechain, }, setup::{ - Mode, Network, PostSetup as EnforcerPostSetup, Sidechain as _, - setup as setup_enforcer, + Mode, Network, PostSetup as EnforcerPostSetup, + PreSetup as EnforcerPreSetup, SetupOpts as EnforcerSetupOpts, + Sidechain as _, }, util::{AbortOnDrop, AsyncTrial, TestFailureCollector, TestFileRegistry}, }; @@ -28,16 +29,17 @@ const DEPOSIT_FEE: bitcoin::Amount = bitcoin::Amount::from_sat(1_000_000); /// Initial setup for the test async fn setup( - bin_paths: &BinPaths, + bin_paths: BinPaths, res_tx: mpsc::UnboundedSender>, ) -> anyhow::Result<(EnforcerPostSetup, PostSetup)> { - let mut enforcer_post_setup = setup_enforcer( - &bin_paths.others, - Network::Regtest, - Mode::Mempool, - res_tx.clone(), - ) - .await?; + let enforcer_pre_setup = + EnforcerPreSetup::new(bin_paths.others, Network::Regtest)?; + let mut enforcer_post_setup = { + let setup_opts: EnforcerSetupOpts = Default::default(); + enforcer_pre_setup + .setup(Mode::Mempool, setup_opts, 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?; @@ -45,7 +47,7 @@ async fn setup( let () = fund_enforcer::(&mut enforcer_post_setup).await?; let mut post_setup = PostSetup::setup( Init { - bitnames_app: bin_paths.bitnames.clone(), + bitnames_app: bin_paths.bitnames, data_dir_suffix: None, }, &enforcer_post_setup, @@ -72,7 +74,7 @@ async fn register_bitname_task( res_tx: mpsc::UnboundedSender>, ) -> anyhow::Result<()> { let (mut enforcer_post_setup, post_setup) = - setup(&bin_paths, res_tx.clone()).await?; + setup(bin_paths, res_tx.clone()).await?; tracing::info!("Reserving BitName"); let _: Txid = post_setup .rpc_client diff --git a/integration_tests/setup.rs b/integration_tests/setup.rs index 7ecd928..72bf656 100644 --- a/integration_tests/setup.rs +++ b/integration_tests/setup.rs @@ -10,7 +10,9 @@ use bip300301_enforcer_integration_tests::{ }; use bip300301_enforcer_lib::types::SidechainNumber; use futures::{TryFutureExt as _, channel::mpsc, future}; -use plain_bitnames::types::{FilledOutput, FilledOutputContent, PointedOutput}; +use plain_bitnames::types::{ + FilledOutput, FilledOutputContent, Network, PointedOutput, +}; use plain_bitnames_app_rpc_api::RpcClient as _; use reserve_port::ReservedPort; use thiserror::Error; @@ -169,6 +171,7 @@ impl Sidechain for PostSetup { .enforcer_serve_grpc .port(), net_port: reserved_ports.net.port(), + network: Network::Regtest, rpc_port: reserved_ports.rpc.port(), zmq_port: reserved_ports.zmq.port(), }; @@ -275,12 +278,10 @@ impl Sidechain for PostSetup { .latest_failed_withdrawal_bundle_height() .await? .unwrap_or(0); - match WITHDRAWAL_BUNDLE_FAILURE_GAP.saturating_sub( + let blocks_to_mine = WITHDRAWAL_BUNDLE_FAILURE_GAP.saturating_sub( block_height - latest_failed_withdrawal_bundle_height, - ) { - 0 => WITHDRAWAL_BUNDLE_FAILURE_GAP + 1, - blocks_to_mine => blocks_to_mine, - } + ); + std::cmp::max(1, blocks_to_mine) }; tracing::debug!( "Mining BitNames blocks until withdrawal bundle is broadcast" diff --git a/integration_tests/unknown_withdrawal.rs b/integration_tests/unknown_withdrawal.rs index c103069..ff700be 100644 --- a/integration_tests/unknown_withdrawal.rs +++ b/integration_tests/unknown_withdrawal.rs @@ -6,10 +6,14 @@ use bip300301_enforcer_integration_tests::{ withdraw_succeed, }, setup::{ - Mode, Network, PostSetup as EnforcerPostSetup, Sidechain as _, - setup as setup_enforcer, + Mode, Network, PostSetup as EnforcerPostSetup, + PreSetup as EnforcerPreSetup, SetupOpts as EnforcerSetupOpts, + Sidechain as _, + }, + util::{ + AbortOnDrop, AsyncTrial, BinPaths as EnforcerBinPaths, + TestFailureCollector, TestFileRegistry, }, - util::{AbortOnDrop, AsyncTrial, TestFailureCollector, TestFileRegistry}, }; use futures::{ FutureExt as _, StreamExt as _, channel::mpsc, future::BoxFuture, @@ -26,16 +30,17 @@ use crate::{ /// Initial setup for the test async fn setup( - bin_paths: &BinPaths, + enforcer_bin_paths: EnforcerBinPaths, 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 enforcer_pre_setup = + EnforcerPreSetup::new(enforcer_bin_paths, Network::Regtest)?; + let mut enforcer_post_setup = { + let setup_opts: EnforcerSetupOpts = Default::default(); + enforcer_pre_setup + .setup(Mode::Mempool, setup_opts, 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?; @@ -53,7 +58,8 @@ 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 enforcer_post_setup = + setup(bin_paths.others, res_tx.clone()).await?; let mut sidechain_withdrawer = PostSetup::setup( Init { bitnames_app: bin_paths.bitnames.clone(), diff --git a/integration_tests/util.rs b/integration_tests/util.rs index 2b6792a..02bf9ad 100644 --- a/integration_tests/util.rs +++ b/integration_tests/util.rs @@ -7,6 +7,7 @@ use bip300301_enforcer_integration_tests::util::{ AbortOnDrop, BinPaths as EnforcerBinPaths, VarError, get_env_var, spawn_command_with_args, }; +use plain_bitnames::types::Network; fn load_env_var_from_string(s: &str) -> Result<(), VarError> { dotenvy::from_read_override(s.as_bytes()) @@ -39,6 +40,7 @@ pub struct BitNamesApp { pub mainchain_grpc_port: u16, /// Port to use for P2P networking pub net_port: u16, + pub network: Network, /// Port to use for the RPC server pub rpc_port: u16, /// Port to use for ZMQ server @@ -67,6 +69,7 @@ impl BitNamesApp { self.mainchain_grpc_port.to_string(), "--net-addr".to_owned(), format!("127.0.0.1:{}", self.net_port), + format!("--network={}", self.network), "--rpc-addr".to_owned(), format!("127.0.0.1:{}", self.rpc_port), "--zmq-addr".to_owned(), diff --git a/lib/node/mod.rs b/lib/node/mod.rs index e1a3531..14a1c15 100644 --- a/lib/node/mod.rs +++ b/lib/node/mod.rs @@ -660,13 +660,13 @@ where } } - pub fn get_pending_withdrawal_bundle( + pub fn try_get_pending_withdrawal_bundle( &self, ) -> Result, Error> { let rotxn = self.env.read_txn()?; let bundle = self .state - .get_pending_withdrawal_bundle(&rotxn)? + .try_get_pending_withdrawal_bundle(&rotxn)? .map(|(bundle, _)| bundle); Ok(bundle) } @@ -808,7 +808,7 @@ where return Ok(false); }; let rotxn = self.env.read_txn()?; - let bundle = self.state.get_pending_withdrawal_bundle(&rotxn)?; + let bundle = self.state.try_get_pending_withdrawal_bundle(&rotxn)?; #[cfg(feature = "zmq")] { let height = self diff --git a/lib/state/block.rs b/lib/state/block.rs index 8a03f84..549f2bf 100644 --- a/lib/state/block.rs +++ b/lib/state/block.rs @@ -248,7 +248,7 @@ pub fn connect_prevalidated( let spent_output = state .utxos .try_get(rwtxn, &key)? - .ok_or(Error::NoUtxo { outpoint: *input })?; + .ok_or(error::NoUtxo { outpoint: *input })?; let spent_output = SpentOutput { output: spent_output, inpoint: InPoint::Regular { @@ -397,7 +397,7 @@ pub fn connect( let spent_output = state .utxos .try_get(rwtxn, &OutPointKey::from(input))? - .ok_or(Error::NoUtxo { outpoint: *input })?; + .ok_or(error::NoUtxo { outpoint: *input })?; let spent_output = SpentOutput { output: spent_output, inpoint: InPoint::Regular { @@ -543,9 +543,9 @@ pub fn disconnect_tip( vout: vout as u32, }; if state.utxos.delete(rwtxn, &OutPointKey::from(&outpoint))? { - Ok(()) + Ok::<_, Error>(()) } else { - Err(Error::NoUtxo { outpoint }) + Err(error::NoUtxo { outpoint }.into()) } }, )?; @@ -577,9 +577,9 @@ pub fn disconnect_tip( vout: vout as u32, }; if state.utxos.delete(rwtxn, &OutPointKey::from(&outpoint))? { - Ok(()) + Ok::<_, Error>(()) } else { - Err(Error::NoUtxo { outpoint }) + Err(error::NoUtxo { outpoint }.into()) } }, )?; diff --git a/lib/state/error.rs b/lib/state/error.rs index 51c2cfa..20fe090 100644 --- a/lib/state/error.rs +++ b/lib/state/error.rs @@ -45,7 +45,91 @@ impl From for BitName { } } -#[derive(Debug, thiserror::Error)] +#[derive(Debug, Error)] +#[error("utxo {outpoint} doesn't exist")] +pub struct NoUtxo { + pub outpoint: OutPoint, +} + +#[derive(Debug, Error)] +#[error("pending withdrawal bundle {0} unknown in withdrawal_bundles")] +#[repr(transparent)] +pub struct PendingWithdrawalBundleUnknown(pub M6id); + +#[allow(clippy::duplicated_attributes)] +#[derive(Debug, Error, Transitive)] +#[transitive( + from(db::Delete, db::Error), + from(db::Put, db::Error), + from(db::TryGet, db::Error) +)] +pub enum ConnectWithdrawalBundleSubmitted { + #[error( + "confirmed withdrawal bundle {} resubmitted in {}", + .m6id, + .event_block_hash, + )] + ConfirmedResubmitted { + event_block_hash: bitcoin::BlockHash, + m6id: M6id, + }, + #[error(transparent)] + Db(Box), + #[error( + "dropped withdrawal bundle {0} marked as pending in withdrawal_bundles" + )] + DroppedPending(M6id), + #[error(transparent)] + NoUtxo(#[from] NoUtxo), + #[error(transparent)] + PendingWithdrawalBundleUnknown(#[from] PendingWithdrawalBundleUnknown), + #[error( + "withdrawal bundle {} submitted in {} resubmitted in {}", + m6id, + submitted_block_height, + event_block_hash + )] + Resubmitted { + event_block_hash: bitcoin::BlockHash, + m6id: M6id, + submitted_block_height: u32, + }, + #[error( + "unknown confirmed withdrawal bundle {} marked as failed in {}", + .m6id, + .failed_block_height, + )] + UnknownConfirmedFailed { + m6id: M6id, + failed_block_height: u32, + }, + #[error( + "unknown withdrawal bundle {} marked as dropped in {}", + .m6id, + .dropped_block_height, + )] + UnknownDropped { + m6id: M6id, + dropped_block_height: u32, + }, + #[error( + "unknown withdrawal bundle {} marked as pending in {}", + .m6id, + .pending_block_height, + )] + UnknownPending { + m6id: M6id, + pending_block_height: u32, + }, +} + +impl From for ConnectWithdrawalBundleSubmitted { + fn from(err: db::Error) -> Self { + Self::Db(Box::new(err)) + } +} + +#[derive(Debug, Error)] pub enum InvalidHeader { #[error("expected block hash {expected}, but computed {computed}")] BlockHash { @@ -67,6 +151,7 @@ pub enum InvalidHeader { #[transitive(from(db::Clear, db::Error))] #[transitive(from(db::Delete, db::Error))] #[transitive(from(db::Error, sneed::Error))] +#[transitive(from(db::Get, db::Error))] #[transitive(from(db::IterInit, db::Error))] #[transitive(from(db::IterItem, db::Error))] #[transitive(from(db::Last, db::Error))] @@ -97,6 +182,8 @@ pub enum Error { #[error(transparent)] ComputeMerkleRoot(#[from] crate::types::ComputeMerkleRootError), #[error(transparent)] + ConnectWithdrawalBundleSubmitted(#[from] ConnectWithdrawalBundleSubmitted), + #[error(transparent)] Db(Box), #[error("failed to fill tx output contents: invalid transaction")] FillTxOutputContentsFailed, @@ -125,10 +212,12 @@ pub enum Error { NoStxo { outpoint: OutPoint }, #[error("no tip")] NoTip, - #[error("utxo {outpoint} doesn't exist")] - NoUtxo { outpoint: OutPoint }, + #[error(transparent)] + NoUtxo(#[from] NoUtxo), #[error("Withdrawal bundle event block doesn't exist")] NoWithdrawalBundleEventBlock, + #[error(transparent)] + PendingWithdrawalBundleUnknown(#[from] PendingWithdrawalBundleUnknown), #[error("Too few BitName outputs")] TooFewBitNameOutputs, #[error( diff --git a/lib/state/mod.rs b/lib/state/mod.rs index 5c4b32e..0b4b9bd 100644 --- a/lib/state/mod.rs +++ b/lib/state/mod.rs @@ -1,6 +1,6 @@ use std::collections::{BTreeMap, HashMap, HashSet}; -use fallible_iterator::FallibleIterator; +use fallible_iterator::FallibleIterator as _; use futures::Stream; use heed::types::SerdeBincode; use serde::{Deserialize, Serialize}; @@ -80,9 +80,8 @@ pub struct State { bitnames: bitnames::Dbs, pub utxos: DatabaseUnique>, pub stxos: DatabaseUnique>, - /// Pending withdrawal bundle and block height - pending_withdrawal_bundle: - DatabaseUnique>, + /// Pending withdrawal bundle. MUST exist in withdrawal_bundles + pending_withdrawal_bundle: DatabaseUnique>, /// Latest failed (known) withdrawal bundle latest_failed_withdrawal_bundle: DatabaseUnique>>>, @@ -249,10 +248,23 @@ impl State { }; let latest_failed_m6id = latest_failed_m6id.latest().value; let (_bundle, bundle_status) = self.withdrawal_bundles.try_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, latest_failed_m6id))) + .unwrap_or_else(|| + panic!("Inconsistent DBs: latest failed m6id {latest_failed_m6id} should exist in withdrawal_bundles") + ); + let failed_height = bundle_status + .iter() + .rev() + .find_map(|status| match status.value { + WithdrawalBundleStatus::Failed => Some(status.height), + WithdrawalBundleStatus::Confirmed + | WithdrawalBundleStatus::Dropped + | WithdrawalBundleStatus::Pending + | WithdrawalBundleStatus::Submitted => None, + }) + .unwrap_or_else(|| { + panic!("missing failure status for {latest_failed_m6id}") + }); + Ok(Some((failed_height, latest_failed_m6id))) } fn fill_transaction( @@ -265,7 +277,7 @@ impl State { let utxo = self .utxos .try_get(rotxn, &OutPointKey::from(input))? - .ok_or(Error::NoUtxo { outpoint: *input })?; + .ok_or(error::NoUtxo { outpoint: *input })?; spent_utxos.push(utxo); } Ok(FilledTransaction { @@ -318,11 +330,25 @@ impl State { } /// Get pending withdrawal bundle and block height - pub fn get_pending_withdrawal_bundle( + pub fn try_get_pending_withdrawal_bundle( &self, - txn: &RoTxn, + rotxn: &RoTxn, ) -> Result, Error> { - Ok(self.pending_withdrawal_bundle.try_get(txn, &())?) + let Some(m6id) = self.pending_withdrawal_bundle.try_get(rotxn, &())? + else { + return Ok(None); + }; + let (bundle_info, bundle_status) = + self.withdrawal_bundles.get(rotxn, &m6id)?; + let bundle = match bundle_info { + WithdrawalBundleInfo::Known(bundle) => bundle, + WithdrawalBundleInfo::Unknown + | WithdrawalBundleInfo::UnknownConfirmed { spend_utxos: _ } => { + return Err(error::PendingWithdrawalBundleUnknown(m6id).into()); + } + }; + let height = bundle_status.latest().height; + Ok(Some((bundle, height))) } /// Check that diff --git a/lib/state/rollback.rs b/lib/state/rollback.rs index fb14454..fe49f36 100644 --- a/lib/state/rollback.rs +++ b/lib/state/rollback.rs @@ -53,10 +53,16 @@ impl RollBack> { } /// Returns the earliest value + #[allow(dead_code)] pub fn earliest(&self) -> &HeightStamped { self.0.first() } + /// Iterate values, earliest to latest + pub fn iter(&self) -> impl DoubleEndedIterator> { + self.0.iter() + } + /// Returns the most recent value pub fn latest(&self) -> &HeightStamped { self.0.last() diff --git a/lib/state/two_way_peg_data.rs b/lib/state/two_way_peg_data.rs index c784171..3178295 100644 --- a/lib/state/two_way_peg_data.rs +++ b/lib/state/two_way_peg_data.rs @@ -8,13 +8,14 @@ use sneed::{RoTxn, RwTxn}; use crate::{ state::{ Error, State, WITHDRAWAL_BUNDLE_FAILURE_GAP, WithdrawalBundleInfo, + error, rollback::{HeightStamped, RollBack}, }, types::{ AggregatedWithdrawal, AmountOverflowError, FilledOutput, FilledOutputContent, InPoint, M6id, OutPoint, OutPointKey, SpentOutput, - WithdrawalBundle, WithdrawalBundleEvent, WithdrawalBundleStatus, - WithdrawalOutputContent, + WithdrawalBundle, WithdrawalBundleEvent, WithdrawalBundleEventStatus, + WithdrawalBundleStatus, WithdrawalOutputContent, proto::mainchain::{BlockEvent, TwoWayPegData}, }, }; @@ -114,49 +115,138 @@ fn connect_withdrawal_bundle_submitted( block_height: u32, event_block_hash: &bitcoin::BlockHash, m6id: M6id, -) -> Result<(), Error> { - if let Some((bundle, bundle_block_height)) = +) -> Result<(), error::ConnectWithdrawalBundleSubmitted> { + if let Some(bundle_m6id) = state.pending_withdrawal_bundle.try_get(rwtxn, &())? - && bundle.compute_m6id() == m6id + && bundle_m6id == m6id { - assert_eq!(bundle_block_height, block_height - 1); tracing::debug!( %block_height, %m6id, - "Withdrawal bundle successfully submitted" + "Pending withdrawal bundle submission confirmed" ); + let (bundle, mut bundle_status) = state + .withdrawal_bundles + .try_get(rwtxn, &m6id)? + .ok_or(error::PendingWithdrawalBundleUnknown(m6id))?; + let bundle = match bundle { + WithdrawalBundleInfo::Known(bundle) => bundle, + WithdrawalBundleInfo::Unknown + | WithdrawalBundleInfo::UnknownConfirmed { spend_utxos: _ } => { + let err = error::PendingWithdrawalBundleUnknown(m6id); + return Err(err.into()); + } + }; for (outpoint, spend_output) in bundle.spend_utxos() { - state.utxos.delete(rwtxn, &OutPointKey::from(outpoint))?; + let key = OutPointKey::from(outpoint); + if !state.utxos.delete(rwtxn, &key)? { + return Err(error::NoUtxo { + outpoint: *outpoint, + } + .into()); + }; let spent_output = SpentOutput { output: spend_output.clone(), inpoint: InPoint::Withdrawal { m6id }, }; - state.stxos.put( - rwtxn, - &OutPointKey::from(outpoint), - &spent_output, - )?; + state.stxos.put(rwtxn, &key, &spent_output)?; } + assert_eq!( + bundle_status.latest().value, + WithdrawalBundleStatus::Pending + ); + bundle_status + .push(WithdrawalBundleStatus::Submitted, block_height) + .expect("push submitted status should be valid"); state.withdrawal_bundles.put( rwtxn, &m6id, - &( - WithdrawalBundleInfo::Known(bundle), - RollBack::>::new( - WithdrawalBundleStatus::Submitted, - block_height, - ), - ), + &(WithdrawalBundleInfo::Known(bundle), bundle_status), )?; state.pending_withdrawal_bundle.delete(rwtxn, &())?; - } else if let Some((_bundle, bundle_status)) = + } else if let Some((bundle, mut bundle_status)) = state.withdrawal_bundles.try_get(rwtxn, &m6id)? { - // Already applied - assert_eq!( - bundle_status.earliest().value, - WithdrawalBundleStatus::Submitted - ); + match (&bundle, bundle_status.latest().value) { + (_, WithdrawalBundleStatus::Confirmed) => { + let err = error::ConnectWithdrawalBundleSubmitted::ConfirmedResubmitted { + event_block_hash: *event_block_hash, + m6id + }; + return Err(err); + } + (_, WithdrawalBundleStatus::Submitted) => { + let err = + error::ConnectWithdrawalBundleSubmitted::Resubmitted { + event_block_hash: *event_block_hash, + m6id, + submitted_block_height: bundle_status.latest().height, + }; + return Err(err); + } + ( + WithdrawalBundleInfo::Known(_), + WithdrawalBundleStatus::Dropped, + ) => { + tracing::warn!(%event_block_hash, %m6id, "dropped bundle submitted"); + } + ( + WithdrawalBundleInfo::Unknown + | WithdrawalBundleInfo::UnknownConfirmed { spend_utxos: _ }, + WithdrawalBundleStatus::Dropped, + ) => { + let err = + error::ConnectWithdrawalBundleSubmitted::UnknownDropped { + m6id, + dropped_block_height: bundle_status.latest().height, + }; + return Err(err); + } + ( + WithdrawalBundleInfo::Known(_), + WithdrawalBundleStatus::Pending, + ) => { + let err = + error::ConnectWithdrawalBundleSubmitted::DroppedPending( + m6id, + ); + return Err(err); + } + ( + WithdrawalBundleInfo::Unknown + | WithdrawalBundleInfo::UnknownConfirmed { spend_utxos: _ }, + WithdrawalBundleStatus::Pending, + ) => { + let err = + error::ConnectWithdrawalBundleSubmitted::UnknownPending { + m6id, + pending_block_height: bundle_status.latest().height, + }; + return Err(err); + } + ( + WithdrawalBundleInfo::Known(_) | WithdrawalBundleInfo::Unknown, + WithdrawalBundleStatus::Failed, + ) => { + tracing::warn!(%event_block_hash, %m6id, "failed bundle resubmitted"); + } + ( + WithdrawalBundleInfo::UnknownConfirmed { spend_utxos: _ }, + WithdrawalBundleStatus::Failed, + ) => { + let err = error::ConnectWithdrawalBundleSubmitted::UnknownConfirmedFailed { + m6id, + failed_block_height: bundle_status.latest().height, + }; + return Err(err); + } + } + bundle_status + .push(WithdrawalBundleStatus::Submitted, block_height) + .expect("push submitted status should be valid"); + state + .withdrawal_bundles + .put(rwtxn, &m6id, &(bundle, bundle_status))? } else { tracing::warn!( %event_block_hash, @@ -306,7 +396,7 @@ fn connect_withdrawal_bundle_event( event: &WithdrawalBundleEvent, ) -> Result<(), Error> { match event.status { - WithdrawalBundleStatus::Submitted => { + WithdrawalBundleEventStatus::Submitted => { connect_withdrawal_bundle_submitted( state, rwtxn, @@ -314,8 +404,9 @@ fn connect_withdrawal_bundle_event( event_block_hash, event.m6id, ) + .map_err(Error::ConnectWithdrawalBundleSubmitted) } - WithdrawalBundleStatus::Confirmed => { + WithdrawalBundleEventStatus::Confirmed => { connect_withdrawal_bundle_confirmed( state, rwtxn, @@ -324,12 +415,14 @@ fn connect_withdrawal_bundle_event( event.m6id, ) } - WithdrawalBundleStatus::Failed => connect_withdrawal_bundle_failed( - state, - rwtxn, - block_height, - event.m6id, - ), + WithdrawalBundleEventStatus::Failed => { + connect_withdrawal_bundle_failed( + state, + rwtxn, + block_height, + event.m6id, + ) + } } } @@ -428,10 +521,24 @@ pub fn connect( collect_withdrawal_bundle(state, rwtxn, block_height)? { let m6id = bundle.compute_m6id(); - state.pending_withdrawal_bundle.put( + state.pending_withdrawal_bundle.put(rwtxn, &(), &m6id)?; + let bundle_status = if let Some((_bundle, mut bundle_status)) = + state.withdrawal_bundles.try_get(rwtxn, &m6id)? + { + bundle_status + .push(WithdrawalBundleStatus::Pending, block_height) + .expect("push pending status should be valid"); + bundle_status + } else { + RollBack::>::new( + WithdrawalBundleStatus::Pending, + block_height, + ) + }; + state.withdrawal_bundles.put( rwtxn, - &(), - &(bundle, block_height), + &m6id, + &(WithdrawalBundleInfo::Known(bundle), bundle_status), )?; tracing::trace!( %block_height, @@ -451,9 +558,9 @@ fn disconnect_withdrawal_bundle_submitted( let Some((bundle, bundle_status)) = state.withdrawal_bundles.try_get(rwtxn, &m6id)? else { - if let Some((bundle, _)) = + if let Some(pending_bundle_m6id) = state.pending_withdrawal_bundle.try_get(rwtxn, &())? - && bundle.compute_m6id() == m6id + && pending_bundle_m6id == m6id { // Already applied return Ok(()); @@ -461,31 +568,46 @@ fn disconnect_withdrawal_bundle_submitted( 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 { + let (bundle_status, latest_bundle_status) = bundle_status.pop(); + assert_eq!( + latest_bundle_status.value, + WithdrawalBundleStatus::Submitted + ); + assert_eq!(latest_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, &OutPointKey::from(outpoint))? { - return Err(Error::NoStxo { - outpoint: *outpoint, - }); - }; - state - .utxos - .put(rwtxn, &OutPointKey::from(outpoint), output)?; + if let Some(bundle_status) = &bundle_status + && bundle_status.latest().value + == WithdrawalBundleStatus::Pending + { + for (outpoint, output) in bundle.spend_utxos().iter().rev() { + if !state + .stxos + .delete(rwtxn, &OutPointKey::from(outpoint))? + { + return Err(Error::NoStxo { + outpoint: *outpoint, + }); + }; + state.utxos.put( + rwtxn, + &OutPointKey::from(outpoint), + output, + )?; + } + state.pending_withdrawal_bundle.put(rwtxn, &(), &m6id)?; } - state.pending_withdrawal_bundle.put( - rwtxn, - &(), - &(bundle, bundle_status.height - 1), - )?; } } - state.withdrawal_bundles.delete(rwtxn, &m6id)?; + if let Some(bundle_status) = bundle_status { + state + .withdrawal_bundles + .put(rwtxn, &m6id, &(bundle, bundle_status))?; + } else { + state.withdrawal_bundles.delete(rwtxn, &m6id)?; + } Ok(()) } @@ -578,9 +700,10 @@ fn disconnect_withdrawal_bundle_failed( &spent_output, )?; if state.utxos.delete(rwtxn, &OutPointKey::from(outpoint))? { - return Err(Error::NoUtxo { + return Err(error::NoUtxo { outpoint: *outpoint, - }); + } + .into()); }; } let (prev_latest_failed_m6id, latest_failed_m6id) = state @@ -616,7 +739,7 @@ fn disconnect_withdrawal_bundle_event( event: &WithdrawalBundleEvent, ) -> Result<(), Error> { match event.status { - WithdrawalBundleStatus::Submitted => { + WithdrawalBundleEventStatus::Submitted => { disconnect_withdrawal_bundle_submitted( state, rwtxn, @@ -624,7 +747,7 @@ fn disconnect_withdrawal_bundle_event( event.m6id, ) } - WithdrawalBundleStatus::Confirmed => { + WithdrawalBundleEventStatus::Confirmed => { disconnect_withdrawal_bundle_confirmed( state, rwtxn, @@ -632,12 +755,14 @@ fn disconnect_withdrawal_bundle_event( event.m6id, ) } - WithdrawalBundleStatus::Failed => disconnect_withdrawal_bundle_failed( - state, - rwtxn, - block_height, - event.m6id, - ), + WithdrawalBundleEventStatus::Failed => { + disconnect_withdrawal_bundle_failed( + state, + rwtxn, + block_height, + event.m6id, + ) + } } } @@ -654,7 +779,7 @@ fn disconnect_event( BlockEvent::Deposit(deposit) => { let outpoint = OutPoint::Deposit(deposit.outpoint); if !state.utxos.delete(rwtxn, &OutPointKey::from(&outpoint))? { - return Err(Error::NoUtxo { outpoint }); + return Err(error::NoUtxo { outpoint }.into()); } *latest_deposit_block_hash = Some(event_block_hash); } @@ -729,11 +854,26 @@ 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_m6id) = state.pending_withdrawal_bundle.try_get(rwtxn, &())? - && bundle_height == block_height - 1 + && let (bundle, bundle_status) = state + .withdrawal_bundles + .try_get(rwtxn, &bundle_m6id)? + .ok_or(error::PendingWithdrawalBundleUnknown(bundle_m6id))? + && bundle_status.latest().height == block_height - 1 { state.pending_withdrawal_bundle.delete(rwtxn, &())?; + if let (Some(bundle_status), _latest_bundle_status) = + bundle_status.pop() + { + state.withdrawal_bundles.put( + rwtxn, + &bundle_m6id, + &(bundle, bundle_status), + )?; + } else { + state.withdrawal_bundles.delete(rwtxn, &bundle_m6id)?; + } } // Handle deposits if let Some(latest_deposit_block_hash) = latest_deposit_block_hash { diff --git a/lib/types/proto.rs b/lib/types/proto.rs index 6b7a1b7..0d783ef 100644 --- a/lib/types/proto.rs +++ b/lib/types/proto.rs @@ -533,7 +533,7 @@ pub mod mainchain { } impl From - for crate::types::WithdrawalBundleStatus + for crate::types::WithdrawalBundleEventStatus { fn from( event: generated::withdrawal_bundle_event::event::Event, @@ -553,7 +553,7 @@ pub mod mainchain { } impl TryFrom - for crate::types::WithdrawalBundleStatus + for crate::types::WithdrawalBundleEventStatus { type Error = super::Error; diff --git a/types/lib.rs b/types/lib.rs index be47235..135ae39 100644 --- a/types/lib.rs +++ b/types/lib.rs @@ -200,6 +200,16 @@ impl Header { #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] pub enum WithdrawalBundleStatus { + Confirmed, + /// Formerly pending bundle + Dropped, + Failed, + Pending, + Submitted, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum WithdrawalBundleEventStatus { Confirmed, Failed, Submitted, @@ -208,7 +218,7 @@ pub enum WithdrawalBundleStatus { #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct WithdrawalBundleEvent { pub m6id: M6id, - pub status: WithdrawalBundleStatus, + pub status: WithdrawalBundleEventStatus, } pub static OP_DRIVECHAIN_SCRIPT: LazyLock = @@ -714,7 +724,11 @@ pub struct Tip { } #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] -#[cfg_attr(feature = "clap", derive(clap::ValueEnum, strum::Display))] +#[cfg_attr( + feature = "clap", + derive(clap::ValueEnum, strum::Display), + strum(serialize_all = "lowercase") +)] pub enum Network { #[default] Signet,