Skip to content
Merged
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ configure_me_codegen = "0.4.0"
[dev-dependencies]
tempfile = "3"
corepc-node = { version = "0.12.0", default-features = false, features = ["30_2", "download"] }
silentpayments = { version = "0.5", features = ["sending", "encode"] }

[package.metadata.configure_me]
spec = "config_spec.toml"
Expand Down
167 changes: 167 additions & 0 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,18 @@ use std::path::{Path, PathBuf};
use std::process::{Child, Command, Output};
use std::time::{Duration, Instant};

use bitcoin::hashes::Hash;
use bitcoin::secp256k1::{Message, Secp256k1, SecretKey};
use bitcoin::sighash::{EcdsaSighashType, SighashCache};
use bitcoin::{
absolute::LockTime, key::TweakedPublicKey, transaction::Version, Address, Amount,
CompressedPublicKey, Network, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness,
};
use corepc_node::{Conf, Node, P2P};
use kernel_node::server_capnp::server;
use silentpayments::sending::generate_recipient_pubkeys;
use silentpayments::utils::sending::calculate_partial_secret;
use silentpayments::SilentPaymentAddress;
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};

const READY_TIMEOUT: Duration = Duration::from_secs(30);
Expand Down Expand Up @@ -131,6 +141,76 @@ impl TestNode {
}
}

pub fn import_keys(&self, scan_key_hex: &str, spend_pub_hex: &str) {
let out = self.cli(&["wallet", "import-keys", scan_key_hex, spend_pub_hex]);
assert!(
out.status.success(),
"import-keys failed: {}",
String::from_utf8_lossy(&out.stderr)
);
}

pub fn receive_address(&self) -> String {
let socket = self.socket_path();
self.rt
.block_on(tokio::task::LocalSet::new().run_until(async move {
let client = connect(&socket).await;
let wallet = client
.make_wallet_request()
.send()
.promise
.await
.unwrap()
.get()
.unwrap()
.get_wallet()
.unwrap();
let response = wallet.receive_request().send().promise.await.unwrap();
response
.get()
.unwrap()
.get_address()
.unwrap()
.to_string()
.unwrap()
}))
}

pub fn balance(&self) -> Amount {
let socket = self.socket_path();
self.rt
.block_on(tokio::task::LocalSet::new().run_until(async move {
let client = connect(&socket).await;
let wallet = client
.make_wallet_request()
.send()
.promise
.await
.unwrap()
.get()
.unwrap()
.get_wallet()
.unwrap();
let response = wallet.get_balance_request().send().promise.await.unwrap();
Amount::from_sat(response.get().unwrap().get_sats())
}))
}

pub fn wait_for_balance(&self, min: Amount, timeout: Duration) -> Amount {
let deadline = Instant::now() + timeout;
loop {
let balance = self.balance();
if balance >= min {
return balance;
}
assert!(
Instant::now() < deadline,
"balance did not reach {min} within {timeout:?} (at {balance})"
);
std::thread::sleep(TIP_POLL_INTERVAL);
}
}

pub fn stop(mut self) {
let out = self.cli(&["stop"]);
assert!(
Expand Down Expand Up @@ -170,3 +250,90 @@ impl Drop for TestNode {
let _ = self.process.wait();
}
}

// Core cannot send to a silent payment address, so build the BIP-352 payment
// here, broadcast it through Core, and mine it.
pub fn fund_silent_payment(core: &Node, sp_address: &str, amount: Amount) {
let secp = Secp256k1::new();

let sender_sk = SecretKey::from_slice(&[0x21; 32]).unwrap();
let sender_pk = CompressedPublicKey(sender_sk.public_key(&secp));
let sender_address = Address::p2wpkh(&sender_pk, Network::Regtest);

// 101 blocks so the first coinbase is mature.
core.client
.generate_to_address(101, &sender_address)
.unwrap();
let block1_hash: bitcoin::BlockHash = core.client.get_block_hash(1).unwrap().0.parse().unwrap();
let coinbase = core
.client
.get_block(block1_hash)
.unwrap()
.txdata
.into_iter()
.next()
.unwrap();
let prevout = OutPoint {
txid: coinbase.compute_txid(),
vout: 0,
};
let prev_txout = coinbase.output[0].clone();

let sp = SilentPaymentAddress::try_from(sp_address).unwrap();
let partial_secret = calculate_partial_secret(
&[(sender_sk, false)],
&[(prevout.txid.to_string(), prevout.vout)],
)
.unwrap();
let derived = generate_recipient_pubkeys(vec![sp], partial_secret).unwrap();
let output_key = derived
.get(&sp)
.and_then(|keys| keys.first())
.copied()
.unwrap();
let recipient_script =
ScriptBuf::new_p2tr_tweaked(TweakedPublicKey::dangerous_assume_tweaked(output_key));

let fee = Amount::from_sat(1_000);
let change = prev_txout.value - amount - fee;
let mut tx = Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: prevout,
script_sig: ScriptBuf::new(),
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
witness: Witness::new(),
}],
output: vec![
TxOut {
value: amount,
script_pubkey: recipient_script,
},
TxOut {
value: change,
script_pubkey: sender_address.script_pubkey(),
},
],
};

let sighash = SighashCache::new(&tx)
.p2wpkh_signature_hash(
0,
&prev_txout.script_pubkey,
prev_txout.value,
EcdsaSighashType::All,
)
.unwrap();
let signature = secp.sign_ecdsa(&Message::from_digest(sighash.to_byte_array()), &sender_sk);
let mut sig_bytes = signature.serialize_der().to_vec();
sig_bytes.push(EcdsaSighashType::All as u8);
let mut witness = Witness::new();
witness.push(sig_bytes);
witness.push(sender_pk.to_bytes());
tx.input[0].witness = witness;

core.client.send_raw_transaction(&tx).unwrap();
let miner = core.client.new_address().unwrap();
core.client.generate_to_address(1, &miner).unwrap();
}
46 changes: 46 additions & 0 deletions tests/wallet_receive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
mod common;

use std::time::Duration;

use bitcoin::hex::DisplayHex;
use bitcoin::secp256k1::{Secp256k1, SecretKey};
use bitcoin::Amount;
use common::{fund_silent_payment, start_bitcoind, TestNode};

const SYNC_TIMEOUT: Duration = Duration::from_secs(60);

#[test]
fn receives_a_silent_payment() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's a bad idea to add this as a unit test. This is by definition an integration test and should exist separately from unit tests.

let core = start_bitcoind();
let p2p = core.params.p2p_socket.unwrap();
println!("step 1/5: bitcoind started on regtest");

let node = TestNode::start_connected(p2p);
println!("step 2/5: node started and connected to Core");

let secp = Secp256k1::new();
let scan_key = SecretKey::from_slice(&[0x42; 32]).unwrap();
let spend_key = SecretKey::from_slice(&[0x43; 32]).unwrap();
let scan_hex = scan_key.secret_bytes().to_lower_hex_string();
let spend_pub_hex = spend_key
.x_only_public_key(&secp)
.0
.serialize()
.to_lower_hex_string();
node.import_keys(&scan_hex, &spend_pub_hex);
let sp_address = node.receive_address();
println!("step 3/5: imported scan keys, receive address {sp_address}");

let amount = Amount::from_sat(100_000_000);
fund_silent_payment(&core, &sp_address, amount);
println!(
"step 4/5: broadcast a {} sat silent payment to the node",
amount.to_sat()
);

let balance = node.wait_for_balance(amount, SYNC_TIMEOUT);
assert_eq!(balance, amount);
println!("step 5/5: node scanned the payment, balance = {balance}");

node.stop();
}
Loading