Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,9 @@ impl App {
let coinbase = match tx_fees {
bitcoin::Amount::ZERO => Vec::new(),
_ => vec![types::Output {
address: self.wallet.get_new_address()?,
// A template is built on every poll and mostly thrown
// away, so it must not derive an address each time.
address: self.wallet.get_receive_address()?,
content: types::OutputContent::Value(tx_fees),
}],
};
Expand Down
6 changes: 6 additions & 0 deletions integration_tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use thunder_app_rpc_api::node::RpcClient as _;
use crate::{
block_template::block_template_trial,
ibd::ibd_trial,
receive_address::receive_address_trial,
setup::{Init, PostSetup},
unknown_withdrawal::unknown_withdrawal_trial,
util::BinPaths,
Expand Down Expand Up @@ -178,6 +179,11 @@ pub fn tests(
file_registry.clone(),
failure_collector.clone(),
),
receive_address_trial(
bin_paths.clone(),
file_registry.clone(),
failure_collector.clone(),
),
unknown_withdrawal_trial(bin_paths, file_registry, failure_collector),
]
}
1 change: 1 addition & 0 deletions integration_tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use tracing_subscriber::{filter as tracing_filter, layer::SubscriberExt};
mod block_template;
mod ibd;
mod integration_test;
mod receive_address;
mod setup;
mod unknown_withdrawal;
mod util;
Expand Down
148 changes: 148 additions & 0 deletions integration_tests/receive_address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
//! Test that the wallet reuses its receive address until it receives

use bip300301_enforcer_integration_tests::{
integration_test::{
activate_sidechain, deposit, fund_enforcer, propose_sidechain,
},
setup::{
Mode, Network, PostSetup as EnforcerPostSetup,
PreSetup as EnforcerPreSetup, SetupOpts as EnforcerSetupOpts,
Sidechain as _,
},
util::{
AbortOnDrop, AsyncTrial, BinPaths as EnforcerBinPaths,
TestFailureCollector, TestFileRegistry,
},
};
use bitcoin::Amount;
use futures::{
FutureExt as _, StreamExt as _, channel::mpsc, future::BoxFuture,
};
use thunder_app_rpc_api::wallet::RpcClient as _;
use tokio::time::sleep;
use tracing::Instrument as _;

use crate::{
setup::{Init, PostSetup},
util::BinPaths,
};

const DEPOSIT_AMOUNT: Amount = Amount::from_sat(21_000_000);
const DEPOSIT_FEE: Amount = Amount::from_sat(1_000_000);

/// Initial setup for the test
async fn setup(
enforcer_bin_paths: &EnforcerBinPaths,
res_tx: mpsc::UnboundedSender<anyhow::Result<()>>,
) -> anyhow::Result<EnforcerPostSetup> {
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::<PostSetup>(&mut enforcer_post_setup).await?;
let () = activate_sidechain::<PostSetup>(&mut enforcer_post_setup).await?;
let () = fund_enforcer::<PostSetup>(&mut enforcer_post_setup).await?;
Ok(enforcer_post_setup)
}

async fn receive_address_task(
bin_paths: BinPaths,
res_tx: mpsc::UnboundedSender<anyhow::Result<()>>,
) -> anyhow::Result<()> {
let mut enforcer_post_setup =
setup(&bin_paths.others, res_tx.clone()).await?;
let mut sidechain = PostSetup::setup(
Init {
thunder_app: bin_paths.thunder()?.clone(),
data_dir_suffix: None,
},
&enforcer_post_setup,
res_tx,
)
.await?;
tracing::info!("Setup thunder node successfully");

// Setup asks for one address and keeps it as the deposit address.
let deposit_address = sidechain.get_deposit_address().await?;
let before = sidechain.rpc_client.get_wallet_addresses().await?.len();

tracing::debug!("Checking that a template asks for no new address");
for _ in 0..10 {
let _template = sidechain.rpc_client.get_block_template().await?;
}
anyhow::ensure!(
sidechain.rpc_client.get_wallet_addresses().await?.len() == before
);

tracing::debug!("Checking that a fresh address is still fresh");
let fresh = sidechain.rpc_client.get_new_address().await?;
anyhow::ensure!(fresh.to_string() != deposit_address);
anyhow::ensure!(
sidechain.rpc_client.get_wallet_addresses().await?.len() == before + 1
);

tracing::debug!("Depositing, so the receive address receives");
let () = deposit(
&mut enforcer_post_setup,
&mut sidechain,
&deposit_address,
DEPOSIT_AMOUNT,
DEPOSIT_FEE,
)
.await?;
let after_deposit =
sidechain.rpc_client.get_wallet_addresses().await?.len();

tracing::debug!("Checking that a template still asks for no new address");
for _ in 0..10 {
let _template = sidechain.rpc_client.get_block_template().await?;
}
anyhow::ensure!(
sidechain.rpc_client.get_wallet_addresses().await?.len()
== after_deposit
);

drop(sidechain);
tracing::info!(
"Removing {}",
enforcer_post_setup.directories.base_dir.path().display()
);
drop(enforcer_post_setup.tasks);
// Wait for tasks to die
sleep(std::time::Duration::from_secs(1)).await;
enforcer_post_setup.directories.base_dir.cleanup()?;
Ok(())
}

async fn receive_address(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 = receive_address_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 receive_address_trial(
bin_paths: BinPaths,
file_registry: TestFileRegistry,
failure_collector: TestFailureCollector,
) -> AsyncTrial<BoxFuture<'static, anyhow::Result<()>>> {
AsyncTrial::new(
"receive_address",
receive_address(bin_paths).boxed(),
file_registry,
failure_collector,
)
}
83 changes: 83 additions & 0 deletions lib/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,8 @@ impl Wallet {
})
}

/// Derives an address the wallet never used. A change output takes one of
/// these, so two transactions never share a change address.
pub fn get_new_address(&self) -> Result<Address, Error> {
let mut txn = self.env.write_txn().map_err(EnvError::from)?;
let (last_index, _) = self
Expand All @@ -495,6 +497,43 @@ impl Wallet {
Ok(address)
}

/// The address to receive at. Derives a new one only once the current one
/// receives.
pub fn get_receive_address(&self) -> Result<Address, Error> {
{
let rotxn = self.env.read_txn().map_err(EnvError::from)?;
let last =
self.index_to_address.last(&rotxn).map_err(DbError::from)?;
if let Some((_, address)) = last
&& !self.address_received(&rotxn, &address)?
{
return Ok(address);
}
}
self.get_new_address()
}

/// True when any output the wallet holds or held pays this address.
fn address_received(
&self,
rotxn: &RoTxn,
address: &Address,
) -> Result<bool, Error> {
let mut utxos = self.utxos.iter(rotxn).map_err(DbError::from)?;
while let Some((_, output)) = utxos.next().map_err(DbError::from)? {
if output.address == *address {
return Ok(true);
}
}
let mut stxos = self.stxos.iter(rotxn).map_err(DbError::from)?;
while let Some((_, spent)) = stxos.next().map_err(DbError::from)? {
if spent.output.address == *address {
return Ok(true);
}
}
Ok(false)
}

/// Gets the latest generated address.
pub fn try_get_last_address(&self) -> Result<Option<Address>, Error> {
let txn = self.env.read_txn().map_err(EnvError::from)?;
Expand Down Expand Up @@ -582,6 +621,50 @@ impl Watchable<()> for Wallet {
mod tests {
use super::*;

#[test]
fn test_get_receive_address() -> anyhow::Result<()> {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_nanos();
let test_dir =
std::env::temp_dir().join(format!("thunder_test_receive_{nanos}"));
if test_dir.exists() {
let _unused = std::fs::remove_dir_all(&test_dir);
}

let wallet = Wallet::new(&test_dir)?;
wallet.set_seed(&[1u8; 64])?;

// An address that never received comes back every time.
let first = wallet.get_receive_address()?;
for _ in 0..10 {
assert_eq!(wallet.get_receive_address()?, first);
}
assert_eq!(wallet.get_addresses()?.len(), 1);

// A fresh address is still fresh, so a change output never reuses one.
let fresh = wallet.get_new_address()?;
assert_ne!(fresh, first);
assert_eq!(wallet.get_addresses()?.len(), 2);

// The receive address moves on once it receives.
let outpoint = OutPoint::Regular {
txid: [0; 32].into(),
vout: 0,
};
let output = Output {
address: wallet.get_receive_address()?,
content: OutputContent::Value(bitcoin::Amount::from_sat(1000)),
};
wallet.put_utxos(&HashMap::from([(outpoint, output)]))?;
let second = wallet.get_receive_address()?;
assert_ne!(second, first);
assert_eq!(wallet.get_receive_address()?, second);

let _unused = std::fs::remove_dir_all(&test_dir);
Ok(())
}

#[test]
fn test_get_or_generate_last_address() -> anyhow::Result<()> {
let nanos = std::time::SystemTime::now()
Expand Down
Loading