From 84541ed406879a5154e1d7fbdde8971d415dce2e Mon Sep 17 00:00:00 2001 From: Matteo Almanza Date: Wed, 22 Jan 2025 14:50:55 +0100 Subject: [PATCH] feat: use conservative default sBTC limits (#1254) * feat: use conservative default sBTC limits * chore: restore trait import * chore: use max money as minimum * chore: remove default in favor of explicit methods --- signer/src/bitcoin/utxo.rs | 34 +- signer/src/context/signer_state.rs | 32 +- signer/src/testing/block_observer.rs | 2 +- .../tests/integration/bitcoin_validation.rs | 9 +- signer/tests/integration/block_observer.rs | 13 +- signer/tests/integration/contracts.rs | 1 - signer/tests/integration/rbf.rs | 2 +- signer/tests/integration/setup.rs | 4 +- .../integration/transaction_coordinator.rs | 429 +++++++++++++++++- .../tests/integration/transaction_signer.rs | 2 + signer/tests/integration/utxo_construction.rs | 4 +- 11 files changed, 494 insertions(+), 38 deletions(-) diff --git a/signer/src/bitcoin/utxo.rs b/signer/src/bitcoin/utxo.rs index 29f610f20..d50d299e0 100644 --- a/signer/src/bitcoin/utxo.rs +++ b/signer/src/bitcoin/utxo.rs @@ -1672,7 +1672,7 @@ mod tests { }, num_signers: 10, accept_threshold: 2, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: DEFAULT_MAX_DEPOSITS_PER_BITCOIN_TX, }; let keypair = Keypair::new_global(&mut OsRng); @@ -1786,7 +1786,7 @@ mod tests { }, num_signers: 10, accept_threshold: 0, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: DEFAULT_MAX_DEPOSITS_PER_BITCOIN_TX, }; @@ -1876,7 +1876,7 @@ mod tests { }, num_signers: 10, accept_threshold: 0, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: DEFAULT_MAX_DEPOSITS_PER_BITCOIN_TX, }; @@ -1985,7 +1985,7 @@ mod tests { }, num_signers: 10, accept_threshold: 0, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: DEFAULT_MAX_DEPOSITS_PER_BITCOIN_TX, }; @@ -2071,7 +2071,7 @@ mod tests { }, num_signers: 10, accept_threshold: 0, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: DEFAULT_MAX_DEPOSITS_PER_BITCOIN_TX, }; @@ -2116,7 +2116,7 @@ mod tests { }, num_signers: 10, accept_threshold: 0, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: DEFAULT_MAX_DEPOSITS_PER_BITCOIN_TX, }; @@ -2167,7 +2167,7 @@ mod tests { }, num_signers: 10, accept_threshold: 0, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: DEFAULT_MAX_DEPOSITS_PER_BITCOIN_TX, }; @@ -2215,7 +2215,7 @@ mod tests { }, num_signers: 10, accept_threshold: 8, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: DEFAULT_MAX_DEPOSITS_PER_BITCOIN_TX, }; @@ -2275,7 +2275,7 @@ mod tests { }, num_signers: 10, accept_threshold: 8, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: DEFAULT_MAX_DEPOSITS_PER_BITCOIN_TX, }; @@ -2375,7 +2375,7 @@ mod tests { }, num_signers: 10, accept_threshold: 8, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: DEFAULT_MAX_DEPOSITS_PER_BITCOIN_TX, }; @@ -2441,7 +2441,7 @@ mod tests { }, num_signers: 10, accept_threshold: 8, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: DEFAULT_MAX_DEPOSITS_PER_BITCOIN_TX, }; @@ -2521,7 +2521,7 @@ mod tests { }, num_signers: 10, accept_threshold: 8, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: DEFAULT_MAX_DEPOSITS_PER_BITCOIN_TX, }; let mut transactions = requests.construct_transactions().unwrap(); @@ -2558,7 +2558,7 @@ mod tests { }, num_signers: 10, accept_threshold: 0, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: DEFAULT_MAX_DEPOSITS_PER_BITCOIN_TX, }; @@ -2616,7 +2616,7 @@ mod tests { }, num_signers: 10, accept_threshold: 8, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: DEFAULT_MAX_DEPOSITS_PER_BITCOIN_TX, }; @@ -2919,7 +2919,7 @@ mod tests { }, num_signers: 11, accept_threshold: 6, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: DEFAULT_MAX_DEPOSITS_PER_BITCOIN_TX, }; @@ -2966,7 +2966,7 @@ mod tests { }, accept_threshold: 127, num_signers: 128, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: DEFAULT_MAX_DEPOSITS_PER_BITCOIN_TX, }; @@ -3022,7 +3022,7 @@ mod tests { }, accept_threshold: 10, num_signers: 14, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: DEFAULT_MAX_DEPOSITS_PER_BITCOIN_TX, }; diff --git a/signer/src/context/signer_state.rs b/signer/src/context/signer_state.rs index bc5284b18..66dabb8cb 100644 --- a/signer/src/context/signer_state.rs +++ b/signer/src/context/signer_state.rs @@ -14,7 +14,7 @@ use crate::keys::PublicKey; /// A struct for holding internal signer state. This struct is served by /// the [`SignerContext`] and can be used to cache global state instead of /// fetching it via I/O for frequently accessed information. -#[derive(Debug, Default)] +#[derive(Debug)] pub struct SignerState { current_signer_set: SignerSet, current_limits: RwLock, @@ -77,8 +77,20 @@ impl SignerState { } } +impl Default for SignerState { + fn default() -> Self { + Self { + current_signer_set: Default::default(), + current_limits: RwLock::new(SbtcLimits::zero()), + sbtc_contracts_deployed: Default::default(), + sbtc_bitcoin_start_height: Default::default(), + is_sbtc_bitcoin_start_height_set: Default::default(), + } + } +} + /// Represents the current sBTC limits. -#[derive(Debug, Clone, Default, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub struct SbtcLimits { /// Represents the total cap for all pegged-in BTC/sBTC. total_cap: Option, @@ -120,6 +132,22 @@ impl SbtcLimits { } } + /// Create a new `SbtcLimits` object without any limits + pub fn unlimited() -> Self { + Self::new(None, None, None, None, None) + } + + /// Create a new `SbtcLimits` object with limits set to zero (fully constraining) + pub fn zero() -> Self { + Self::new( + Some(Amount::ZERO), + Some(Amount::MAX_MONEY), + Some(Amount::ZERO), + Some(Amount::ZERO), + Some(Amount::ZERO), + ) + } + /// Get the total cap for all pegged-in BTC/sBTC. pub fn total_cap(&self) -> Amount { self.total_cap.unwrap_or(Amount::MAX_MONEY) diff --git a/signer/src/testing/block_observer.rs b/signer/src/testing/block_observer.rs index fda3dfe8c..7f0619ab8 100644 --- a/signer/src/testing/block_observer.rs +++ b/signer/src/testing/block_observer.rs @@ -514,7 +514,7 @@ impl EmilyInteract for TestHarness { } async fn get_limits(&self) -> Result { - Ok(SbtcLimits::default()) + Ok(SbtcLimits::unlimited()) } } diff --git a/signer/tests/integration/bitcoin_validation.rs b/signer/tests/integration/bitcoin_validation.rs index 092d0657c..cffa0df7a 100644 --- a/signer/tests/integration/bitcoin_validation.rs +++ b/signer/tests/integration/bitcoin_validation.rs @@ -100,6 +100,7 @@ async fn one_tx_per_request_set() { .with_mocked_stacks_client() .with_mocked_emily_client() .build(); + ctx.state().update_current_limits(SbtcLimits::unlimited()); let signers = TestSignerSet::new(&mut rng); let amounts = [DepositAmounts { @@ -195,6 +196,7 @@ async fn one_invalid_deposit_invalidates_tx() { .with_mocked_stacks_client() .with_mocked_emily_client() .build(); + ctx.state().update_current_limits(SbtcLimits::unlimited()); let signers = TestSignerSet::new(&mut rng); let amounts = [ @@ -382,6 +384,7 @@ async fn cannot_sign_deposit_is_ok() { .with_mocked_stacks_client() .with_mocked_emily_client() .build(); + ctx.state().update_current_limits(SbtcLimits::unlimited()); let amounts = [ DepositAmounts { @@ -515,7 +518,7 @@ async fn cannot_sign_deposit_is_ok() { signer_state: signer_btc_state(&ctx, &request, &btc_ctx).await, accept_threshold: 2, num_signers: 3, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: ctx.config().signer.max_deposits_per_bitcoin_tx.get(), }; let txs = sbtc_requests.construct_transactions().unwrap(); @@ -545,6 +548,7 @@ async fn sighashes_match_from_sbtc_requests_object() { .with_mocked_stacks_client() .with_mocked_emily_client() .build(); + ctx.state().update_current_limits(SbtcLimits::unlimited()); let signers = TestSignerSet::new(&mut rng); let amounts = [ @@ -647,7 +651,7 @@ async fn sighashes_match_from_sbtc_requests_object() { signer_state: signer_btc_state(&ctx, &request, &btc_ctx).await, accept_threshold: 2, num_signers: 3, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: ctx.config().signer.max_deposits_per_bitcoin_tx.get(), }; let txs = sbtc_requests.construct_transactions().unwrap(); @@ -677,6 +681,7 @@ async fn outcome_is_independent_of_input_order() { .with_mocked_stacks_client() .with_mocked_emily_client() .build(); + ctx.state().update_current_limits(SbtcLimits::unlimited()); let signers = TestSignerSet::new(&mut rng); let amounts = [ diff --git a/signer/tests/integration/block_observer.rs b/signer/tests/integration/block_observer.rs index bad684a67..1155c7c57 100644 --- a/signer/tests/integration/block_observer.rs +++ b/signer/tests/integration/block_observer.rs @@ -28,7 +28,6 @@ use signer::context::SbtcLimits; use signer::emily_client::EmilyClient; use signer::error::Error; use signer::keys::SignerScriptPubKey as _; -use signer::logging::setup_logging; use signer::stacks::api::TenureBlocks; use signer::storage::model; use signer::storage::model::BitcoinBlockHash; @@ -83,6 +82,7 @@ async fn load_latest_deposit_requests_persists_requests_from_past(blocks_ago: u6 .with_mocked_emily_client() .with_mocked_stacks_client() .build(); + ctx.state().update_current_limits(SbtcLimits::unlimited()); // We're going to create two confirmed deposits. This also generates // sweep transactions, but this information is not in our database, so @@ -104,7 +104,7 @@ async fn load_latest_deposit_requests_persists_requests_from_past(blocks_ago: u6 client .expect_get_limits() .times(1..) - .returning(|| Box::pin(async { Ok(SbtcLimits::default()) })); + .returning(|| Box::pin(async { Ok(SbtcLimits::unlimited()) })); }) .await; @@ -246,8 +246,6 @@ async fn load_latest_deposit_requests_persists_requests_from_past(blocks_ago: u6 #[ignore = "This is an integration test that requires devenv running"] #[tokio::test] async fn link_blocks() { - setup_logging("info", true); - let db = testing::storage::new_test_database().await; let stacks_client = StacksClient::new(Url::parse("http://localhost:20443").unwrap()).unwrap(); @@ -379,7 +377,6 @@ async fn fetch_input(db: &PgStore, output_type: TxPrevoutType) -> Vec async fn block_observer_stores_donation_and_sbtc_utxos() { let mut rng = rand::rngs::StdRng::seed_from_u64(51); let (rpc, faucet) = regtest::initialize_blockchain(); - // signer::logging::setup_logging("info,signer=debug", false); // We need to populate our databases, so let's fetch the data. let emily_client = @@ -578,7 +575,7 @@ async fn block_observer_stores_donation_and_sbtc_utxos() { }, accept_threshold: 4, num_signers: 7, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: ctx.config().signer.max_deposits_per_bitcoin_tx.get(), }; @@ -659,9 +656,9 @@ async fn block_observer_stores_donation_and_sbtc_utxos() { } #[cfg_attr(not(feature = "integration-tests"), ignore)] -#[test_case::test_case(false, SbtcLimits::default(); "no contracts, default limits")] +#[test_case::test_case(false, SbtcLimits::unlimited(); "no contracts, default limits")] #[test_case::test_case(false, SbtcLimits::new(Some(bitcoin::Amount::from_sat(1_000)), None, None, None, None); "no contracts, total cap limit")] -#[test_case::test_case(true, SbtcLimits::default(); "deployed contracts, default limits")] +#[test_case::test_case(true, SbtcLimits::unlimited(); "deployed contracts, default limits")] #[test_case::test_case(true, SbtcLimits::new(Some(bitcoin::Amount::from_sat(1_000)), None, None, None, None); "deployed contracts, total cap limit")] #[tokio::test] async fn block_observer_handles_update_limits(deployed: bool, sbtc_limits: SbtcLimits) { diff --git a/signer/tests/integration/contracts.rs b/signer/tests/integration/contracts.rs index dc2b8e5bc..c784cb9b5 100644 --- a/signer/tests/integration/contracts.rs +++ b/signer/tests/integration/contracts.rs @@ -220,7 +220,6 @@ async fn complete_deposit_wrapper_tx_accepted(contract: Contr #[ignore = "This is an integration test that requires a stacks-node to work"] #[tokio::test] async fn estimate_tx_fees() { - signer::logging::setup_logging("info", false); let client = stacks_client(); let payload = SmartContract::SbtcRegistry; diff --git a/signer/tests/integration/rbf.rs b/signer/tests/integration/rbf.rs index bd6d58040..edccb1afa 100644 --- a/signer/tests/integration/rbf.rs +++ b/signer/tests/integration/rbf.rs @@ -230,7 +230,7 @@ pub fn transaction_with_rbf( }, accept_threshold: failure_threshold, num_signers: 2 * failure_threshold, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: DEFAULT_MAX_DEPOSITS_PER_BITCOIN_TX, }; diff --git a/signer/tests/integration/setup.rs b/signer/tests/integration/setup.rs index 875079161..b9fec89f6 100644 --- a/signer/tests/integration/setup.rs +++ b/signer/tests/integration/setup.rs @@ -155,7 +155,7 @@ impl TestSweepSetup { }, accept_threshold: 4, num_signers: 7, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: DEFAULT_MAX_DEPOSITS_PER_BITCOIN_TX, }; @@ -766,7 +766,7 @@ impl TestSweepSetup2 { }, accept_threshold: 4, num_signers: 7, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: DEFAULT_MAX_DEPOSITS_PER_BITCOIN_TX, }; diff --git a/signer/tests/integration/transaction_coordinator.rs b/signer/tests/integration/transaction_coordinator.rs index b59e1296b..bb255be6a 100644 --- a/signer/tests/integration/transaction_coordinator.rs +++ b/signer/tests/integration/transaction_coordinator.rs @@ -2,6 +2,7 @@ use std::collections::BTreeSet; use std::num::NonZeroU32; use std::num::NonZeroU64; use std::num::NonZeroUsize; +use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicU8; use std::sync::atomic::Ordering; use std::sync::Arc; @@ -22,6 +23,8 @@ use blockstack_lib::chainstate::stacks::TransactionPayload; use blockstack_lib::net::api::getcontractsrc::ContractSrcResponse; use blockstack_lib::net::api::getpoxinfo::RPCPoxInfoData; use blockstack_lib::net::api::getsortition::SortitionInfo; +use clarity::types::chainstate::StacksAddress; +use clarity::vm::types::PrincipalData; use clarity::vm::types::SequenceData; use clarity::vm::Value as ClarityValue; use emily_client::apis::deposit_api; @@ -45,6 +48,7 @@ use signer::bitcoin::utxo::Fees; use signer::bitcoin::BitcoinInteract as _; use signer::context::RequestDeciderEvent; +use signer::context::SbtcLimits; use signer::context::TxCoordinatorEvent; use signer::keys::PrivateKey; use signer::network::in_memory2::SignerNetwork; @@ -57,8 +61,11 @@ use signer::stacks::contracts::SmartContract; use signer::storage::model::BitcoinBlockHash; use signer::storage::model::BitcoinTx; use signer::storage::postgres::PgStore; +use signer::testing::stacks::DUMMY_SORTITION_INFO; use signer::testing::stacks::DUMMY_TENURE_INFO; use signer::testing::transaction_coordinator::select_coordinator; +use signer::testing::wsts::SignerInfo; +use signer::transaction_coordinator::given_key_is_coordinator; use stacks_common::types::chainstate::BurnchainHeaderHash; use stacks_common::types::chainstate::ConsensusHash; use stacks_common::types::chainstate::SortitionId; @@ -102,7 +109,10 @@ use tokio::sync::broadcast::Sender; use crate::complete_deposit::make_complete_deposit; use crate::setup::backfill_bitcoin_blocks; +use crate::setup::TestSignerSet; use crate::setup::TestSweepSetup; +use crate::setup::TestSweepSetup2; +use crate::utxo_construction::generate_withdrawal; use crate::utxo_construction::make_deposit_request; use crate::zmq::BITCOIN_CORE_ZMQ_ENDPOINT; @@ -616,7 +626,6 @@ async fn deploy_smart_contracts_coordinator( ) where F: FnOnce(u64, Sender) -> Box, { - signer::logging::setup_logging("info", false); let db = testing::storage::new_test_database().await; let mut rng = rand::rngs::StdRng::seed_from_u64(51); @@ -1823,7 +1832,6 @@ async fn sign_bitcoin_transaction() { async fn sign_bitcoin_transaction_multiple_locking_keys() { let (_, signer_key_pairs): (_, [Keypair; 3]) = testing::wallet::regtest_bootstrap_wallet(); let (rpc, faucet) = regtest::initialize_blockchain(); - signer::logging::setup_logging("info,signer=debug", false); // We need to populate our databases, so let's fetch the data. let emily_client = @@ -3075,3 +3083,420 @@ async fn test_get_btc_state_with_available_sweep_transactions_and_rbf() { testing::storage::drop_db(db).await; } + +fn create_signer_set(signers: &[Keypair], threshold: u32) -> (SignerSet, InMemoryNetwork) { + let network = network::InMemoryNetwork::new(); + + let signer_public_keys: BTreeSet<_> = signers.iter().map(|kp| kp.public_key().into()).collect(); + let signer_info: Vec<_> = signers + .iter() + .map(|kp| SignerInfo { + signer_private_key: kp.secret_key().into(), + signer_public_keys: signer_public_keys.clone(), + }) + .collect(); + ( + SignerSet::new(&signer_info, threshold, || network.connect()), + network, + ) +} + +fn create_test_setup( + dkg_shares: &EncryptedDkgShares, + signatures_required: u16, + faucet: ®test::Faucet, + rpc: &bitcoincore_rpc::Client, + bitcoin_client: &BitcoinCoreClient, +) -> TestSweepSetup2 { + let depositor = Recipient::new(AddressType::P2tr); + faucet.send_to(50_000_000, &depositor.address); + faucet.generate_blocks(1); + + let signer_address = Address::from_script( + &dkg_shares.script_pubkey, + bitcoin::Network::Regtest.params(), + ) + .unwrap(); + let donation = faucet.send_to(100_000, &signer_address); + faucet.generate_blocks(1); + + let utxo = depositor.get_utxos(rpc, None).pop().unwrap(); + let (deposit_tx, deposit_request, deposit_info) = make_deposit_request( + &depositor, + 5_000_000, + utxo, + 100_000, + dkg_shares.aggregate_key.x_only_public_key().0, + ); + rpc.send_raw_transaction(&deposit_tx).unwrap(); + + let deposit_block_hash = faucet.generate_blocks(1).pop().unwrap(); + let tx_info = bitcoin_client + .get_tx_info(&deposit_tx.compute_txid(), &deposit_block_hash) + .unwrap() + .unwrap(); + let test_signers = TestSignerSet { + keys: dkg_shares.signer_set_public_keys.clone(), + // We don't use `signer` + signer: Recipient::new(AddressType::P2tr), + }; + TestSweepSetup2 { + deposit_block_hash, + deposits: vec![(deposit_info, deposit_request, tx_info)], + sweep_tx_info: None, + donation, + signers: test_signers, + withdrawal_request: generate_withdrawal().0, + withdrawal_sender: PrincipalData::from(StacksAddress::burn_address(false)), + signatures_required, + } +} + +/// Test that we use conservative initial limits so that we don't process +/// requests until we can fetch limits from Emily. +/// +/// Since the block observer doesn't signal the request decider (and tx coordinator) +/// if it fails to get the limits, the scenario required to check this issue is: +/// - The signers fetch a deposit from Emily, and it is validated and inserted +/// into the db +/// - Emily goes offline for some of the signers (at least one can still reach it) +/// - Those same signers are restarted, thus reloading the default sBTC limits +/// - The signer that didn't restart act as coordinator (since it can reach Emily +/// the block observer does signal the tx coordinator); as coordinator, it tries +/// to sweep the deposit above. +/// - The other signers cannot act as coordinator, but they can respond as tx +/// signers, and they do so using the default limits. +/// If the default limits are permissive (ie, default all `None`), they will +/// happily mint anything. If the limits are conservative, they will refuse to +/// mint (eg, `would exceed sBTC supply cap`). +#[cfg_attr(not(feature = "integration-tests"), ignore)] +#[tokio::test] +async fn test_conservative_initial_sbtc_limits() { + let (rpc, faucet) = regtest::initialize_blockchain(); + let mut rng = rand::rngs::StdRng::seed_from_u64(56); + + let (_, signer_key_pairs): (_, [Keypair; 3]) = testing::wallet::regtest_bootstrap_wallet(); + let signatures_required: u16 = 2; + + let network = WanNetwork::default(); + + let chain_tip_info = rpc.get_chain_tips().unwrap().pop().unwrap(); + + // ========================================================================= + // Create a database, an associated context, and a Keypair for each of the + // signers in the signing set. + // ------------------------------------------------------------------------- + // - We load the database with a bitcoin blocks going back to some + // genesis block. + // ========================================================================= + let mut signers = Vec::new(); + for kp in signer_key_pairs.iter() { + let db = testing::storage::new_test_database().await; + backfill_bitcoin_blocks(&db, rpc, &chain_tip_info.hash).await; + + // Ensure a stacks tip exists before DKG + let mut stacks_block: model::StacksBlock = Faker.fake_with_rng(&mut rng); + stacks_block.bitcoin_anchor = chain_tip_info.hash.into(); + db.write_stacks_block(&stacks_block).await.unwrap(); + + let ctx = TestContext::builder() + .with_storage(db.clone()) + .with_first_bitcoin_core_client() + .with_mocked_stacks_client() + .with_mocked_emily_client() + .build(); + + let network = network.connect(&ctx); + + signers.push((ctx, db, kp, network)); + } + + // ========================================================================= + // Compute DKG and store it into db + // ========================================================================= + let mut signer_set = create_signer_set(&signer_key_pairs, signatures_required as u32).0; + let dkg_txid = testing::dummy::txid(&fake::Faker, &mut rng); + + let (aggregate_key, encrypted_shares) = signer_set + .run_dkg(chain_tip_info.hash.into(), dkg_txid, &mut rng) + .await; + + for ((_, db, _, _), dkg_shares) in signers.iter_mut().zip(&encrypted_shares) { + signer_set + .write_as_rotate_keys_tx(db, &chain_tip_info.hash.into(), dkg_shares, &mut rng) + .await; + + db.write_encrypted_dkg_shares(&dkg_shares) + .await + .expect("failed to write encrypted shares"); + } + + // ========================================================================= + // Setup the emily client mocks. + // ========================================================================= + let enable_emily_limits = Arc::new(AtomicBool::new(false)); + for (i, (ctx, _, _, _)) in signers.iter_mut().enumerate() { + ctx.with_emily_client(|client| { + // We already stored the deposit, we don't need it from Emily + client + .expect_get_deposits() + .returning(|| Box::pin(std::future::ready(Ok(vec![])))); + + // We don't care about this + client.expect_accept_deposits().returning(|_, _| { + Box::pin(std::future::ready(Err(Error::InvalidStacksResponse( + "dummy", + )))) + }); + + let enable_emily_limits = enable_emily_limits.clone(); + let i = i; + client.expect_get_limits().times(1..).returning(move || { + // Since we don't signal the coordinator if we fail to fetch the limits + // we need the coordinator to be able to fetch them. + // But we want the other signers to fail fetching limits. + let limits = if i == 0 || enable_emily_limits.load(Ordering::SeqCst) { + Ok(SbtcLimits::unlimited()) + } else { + // Just a random error, we don't care about it + Err(Error::InvalidStacksResponse("dummy")) + }; + Box::pin(std::future::ready(limits)) + }); + }) + .await; + } + + // ========================================================================= + // Setup the stacks client mocks. + // ------------------------------------------------------------------------- + // - Set up the mocks to that the block observer fetches at least one + // Stacks block. This is necessary because we need the stacks chain + // tip in the transaction coordinator. + // ========================================================================= + for (ctx, _, _, _) in signers.iter_mut() { + ctx.with_stacks_client(|client| { + client + .expect_get_tenure_info() + .returning(move || Box::pin(std::future::ready(Ok(DUMMY_TENURE_INFO.clone())))); + + client.expect_get_block().returning(|_| { + let response = Ok(NakamotoBlock { + header: NakamotoBlockHeader::empty(), + txs: vec![], + }); + Box::pin(std::future::ready(response)) + }); + + let chain_tip = model::BitcoinBlockHash::from(chain_tip_info.hash); + client.expect_get_tenure().returning(move |_| { + let mut tenure = TenureBlocks::nearly_empty().unwrap(); + tenure.anchor_block_hash = chain_tip; + Box::pin(std::future::ready(Ok(tenure))) + }); + + client.expect_get_pox_info().returning(|| { + let response = serde_json::from_str::(GET_POX_INFO_JSON) + .map_err(Error::JsonSerialize); + Box::pin(std::future::ready(response)) + }); + + client + .expect_estimate_fees() + .returning(|_, _, _| Box::pin(std::future::ready(Ok(25)))); + + // The coordinator will try to further process the deposit to submit + // the stacks tx, but we are not interested (for the current test iteration). + client.expect_get_account().returning(|_| { + let response = Ok(AccountInfo { + balance: 0, + locked: 0, + unlock_height: 0, + // this is the only part used to create the stacks transaction. + nonce: 12, + }); + Box::pin(std::future::ready(response)) + }); + + client + .expect_get_sortition_info() + .returning(move |_| Box::pin(std::future::ready(Ok(DUMMY_SORTITION_INFO)))); + + // The coordinator broadcasts a rotate keys transaction if it + // is not up-to-date with their view of the current aggregate + // key. The response of None means that the stacks node does + // not have a record of a rotate keys contract call being + // executed, so the coordinator will construct and broadcast + // one. + client + .expect_get_current_signers_aggregate_key() + .returning(move |_| Box::pin(std::future::ready(Ok(Some(aggregate_key))))); + + // The coordinator will get the total supply of sBTC to + // determine the amount of mintable sBTC. + client + .expect_get_sbtc_total_supply() + .returning(move |_| Box::pin(async move { Ok(Amount::ZERO) })); + }) + .await; + } + + // ========================================================================= + // Setup a deposit + // ------------------------------------------------------------------------- + // - Write the deposit (and anything required for it to be swept) + // ========================================================================= + let dkg_shares = encrypted_shares.first().cloned().unwrap(); + let bitcoin_client = signers[0].0.clone().bitcoin_client; + let setup = create_test_setup( + &dkg_shares, + signatures_required, + faucet, + rpc, + &bitcoin_client, + ); + for (_, db, _, _) in signers.iter_mut() { + backfill_bitcoin_blocks(&db, rpc, &setup.deposit_block_hash).await; + setup.store_stacks_genesis_block(&db).await; + setup.store_donation(&db).await; + setup.store_deposit_txs(&db).await; + setup.store_deposit_request(&db).await; + setup.store_deposit_decisions(&db).await; + } + + // ========================================================================= + // Start the TxCoordinatorEventLoop, TxSignerEventLoop and BlockObserver + // processes for each signer. + // ------------------------------------------------------------------------- + // - We only proceed with the test after all processes have started, and + // we use a counter to notify us when that happens. + // ========================================================================= + let start_count = Arc::new(AtomicU8::new(0)); + + for (ctx, _, kp, network) in signers.iter() { + ctx.state().set_sbtc_contracts_deployed(); + let ev = TxCoordinatorEventLoop { + network: network.spawn(), + context: ctx.clone(), + context_window: 10000, + private_key: kp.secret_key().into(), + signing_round_max_duration: Duration::from_secs(2), + bitcoin_presign_request_max_duration: Duration::from_secs(2), + threshold: signatures_required, + dkg_max_duration: Duration::from_secs(10), + is_epoch3: true, + }; + let counter = start_count.clone(); + tokio::spawn(async move { + counter.fetch_add(1, Ordering::Relaxed); + ev.run().await + }); + + let ev = TxSignerEventLoop { + network: network.spawn(), + threshold: signatures_required as u32, + context: ctx.clone(), + context_window: 10000, + wsts_state_machines: LruCache::new(NonZeroUsize::new(100).unwrap()), + signer_private_key: kp.secret_key().into(), + rng: rand::rngs::OsRng, + dkg_begin_pause: None, + }; + let counter = start_count.clone(); + tokio::spawn(async move { + counter.fetch_add(1, Ordering::Relaxed); + ev.run().await + }); + + let ev = RequestDeciderEventLoop { + network: network.spawn(), + context: ctx.clone(), + context_window: 10000, + blocklist_checker: Some(()), + signer_private_key: kp.secret_key().into(), + }; + let counter = start_count.clone(); + tokio::spawn(async move { + counter.fetch_add(1, Ordering::Relaxed); + ev.run().await + }); + + let zmq_stream = + BitcoinCoreMessageStream::new_from_endpoint(BITCOIN_CORE_ZMQ_ENDPOINT, &["hashblock"]) + .await + .unwrap(); + let (sender, receiver) = tokio::sync::mpsc::channel(100); + + tokio::spawn(async move { + let mut stream = zmq_stream.to_block_hash_stream(); + while let Some(block) = stream.next().await { + sender.send(block).await.unwrap(); + } + }); + + let block_observer = BlockObserver { + context: ctx.clone(), + bitcoin_blocks: ReceiverStream::new(receiver), + }; + let counter = start_count.clone(); + tokio::spawn(async move { + counter.fetch_add(1, Ordering::Relaxed); + block_observer.run().await + }); + } + + while start_count.load(Ordering::SeqCst) < 12 { + tokio::time::sleep(Duration::from_millis(10)).await; + } + + // ========================================================================= + // Wait for the first signer to be the coordinator + // ------------------------------------------------------------------------- + // - Two of three signers will not be able to coordinate, because of failing + // in getting deposits (so no signal is sent to request decider and tx + // coordinator) + // ========================================================================= + let signers_key = setup.signers.signer_keys().iter().cloned().collect(); + loop { + let chain_tip: BitcoinBlockHash = faucet.generate_blocks(1).pop().unwrap().into(); + + if given_key_is_coordinator(signers[0].2.public_key().into(), &chain_tip, &signers_key) { + break; + } + } + // Giving enough time to process the transaction + tokio::time::sleep(Duration::from_secs(3)).await; + + // ========================================================================= + // Check we did NOT process the deposit + // ========================================================================= + let (ctx, _, _, _) = signers.first().unwrap(); + let txids = ctx.bitcoin_client.inner_client().get_raw_mempool().unwrap(); + + assert!(txids.is_empty()); + + // ========================================================================= + // Re-enable limits fetching + // ========================================================================= + enable_emily_limits.store(true, Ordering::SeqCst); + + faucet.generate_blocks(1); + tokio::time::sleep(Duration::from_secs(3)).await; + // ========================================================================= + // Check we did process the deposit now + // ========================================================================= + let (ctx, _, _, _) = signers.first().unwrap(); + let txids = ctx.bitcoin_client.inner_client().get_raw_mempool().unwrap(); + + assert_eq!(txids.len(), 1); + let tx_info = bitcoin_client.get_tx(&txids[0]).unwrap().unwrap(); + + assert_eq!( + tx_info.tx.input[1].previous_output, + setup.deposit_outpoints()[0] + ); + + for (_, db, _, _) in signers { + testing::storage::drop_db(db).await; + } +} diff --git a/signer/tests/integration/transaction_signer.rs b/signer/tests/integration/transaction_signer.rs index 12ffb406e..f975cc7d6 100644 --- a/signer/tests/integration/transaction_signer.rs +++ b/signer/tests/integration/transaction_signer.rs @@ -13,6 +13,7 @@ use signer::bitcoin::utxo::Requests; use signer::bitcoin::utxo::UnsignedTransaction; use signer::bitcoin::validation::TxRequestIds; use signer::context::Context; +use signer::context::SbtcLimits; use signer::error::Error; use signer::keys::PrivateKey; use signer::keys::PublicKey; @@ -236,6 +237,7 @@ pub async fn assert_should_be_able_to_handle_sbtc_requests() { .with_mocked_emily_client() .with_mocked_stacks_client() .build(); + ctx.state().update_current_limits(SbtcLimits::unlimited()); let (rpc, faucet) = sbtc::testing::regtest::initialize_blockchain(); diff --git a/signer/tests/integration/utxo_construction.rs b/signer/tests/integration/utxo_construction.rs index c7bd8737b..b197432de 100644 --- a/signer/tests/integration/utxo_construction.rs +++ b/signer/tests/integration/utxo_construction.rs @@ -225,7 +225,7 @@ fn deposits_add_to_controlled_amounts() { }, accept_threshold: 4, num_signers: 7, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: DEFAULT_MAX_DEPOSITS_PER_BITCOIN_TX, }; @@ -291,7 +291,7 @@ fn withdrawals_reduce_to_signers_amounts() { }, accept_threshold: 4, num_signers: 7, - sbtc_limits: SbtcLimits::default(), + sbtc_limits: SbtcLimits::unlimited(), max_deposits_per_bitcoin_tx: DEFAULT_MAX_DEPOSITS_PER_BITCOIN_TX, };