use std::{
net::{IpAddr, Ipv4Addr, SocketAddrV4},
path::PathBuf,
time::Duration,
};
use bitcoin::{
absolute,
address::NetworkChecked,
key::{
rand::{rngs::StdRng, SeedableRng},
Keypair, Secp256k1, TapTweak,
},
secp256k1::SecretKey,
sighash::{Prevouts, SighashCache},
Amount, KnownHrp, OutPoint, ScriptBuf, Sequence, TapSighashType, TxIn, TxOut, Witness,
};
use corepc_node::serde_json;
use corepc_node::{anyhow, exe_path};
use kyoto::{
chain::checkpoints::HeaderCheckpoint, client::Client, lookup_host, node::Node, Address,
BlockHash, Event, Info, ServiceFlags, SqliteHeaderDb, SqlitePeerDb, Transaction, TrustedPeer,
Warning,
};
use tokio::sync::mpsc::Receiver;
use tokio::sync::mpsc::UnboundedReceiver;
fn start_bitcoind(with_v2_transport: bool) -> anyhow::Result<(corepc_node::Node, SocketAddrV4)> {
let path = exe_path()?;
let mut conf = corepc_node::Conf::default();
conf.p2p = corepc_node::P2P::Yes;
conf.args.push("--txindex");
conf.args.push("--blockfilterindex");
conf.args.push("--peerblockfilters");
conf.args.push("--rest=1");
conf.args.push("--server=1");
conf.args.push("--listen=1");
let tempdir = tempfile::TempDir::new()?;
conf.tmpdir = Some(tempdir.path().to_owned());
if with_v2_transport {
conf.args.push("--v2transport=1")
} else {
conf.args.push("--v2transport=0");
}
let bitcoind = corepc_node::Node::with_conf(path, &conf)?;
let socket_addr = bitcoind.params.p2p_socket.unwrap();
Ok((bitcoind, socket_addr))
}
fn new_node(
socket_addr: SocketAddrV4,
tempdir_path: PathBuf,
checkpoint: Option<HeaderCheckpoint>,
) -> (Node<SqliteHeaderDb, SqlitePeerDb>, Client) {
let host = (IpAddr::V4(*socket_addr.ip()), Some(socket_addr.port()));
let mut trusted: TrustedPeer = host.into();
trusted.set_services(ServiceFlags::P2P_V2);
let mut builder = kyoto::builder::Builder::new(bitcoin::Network::Regtest);
if let Some(checkpoint) = checkpoint {
builder = builder.after_checkpoint(checkpoint);
}
let (node, client) = builder
.add_peer(host)
.data_dir(tempdir_path)
.build()
.unwrap();
(node, client)
}
fn num_blocks(rpc: &corepc_node::Client) -> i64 {
rpc.get_blockchain_info().unwrap().blocks
}
fn best_hash(rpc: &corepc_node::Client) -> BlockHash {
rpc.get_best_block_hash().unwrap().block_hash().unwrap()
}
async fn mine_blocks(
rpc: &corepc_node::Client,
miner: &bitcoin::Address<NetworkChecked>,
num_blocks: usize,
time: u64,
) {
rpc.generate_to_address(num_blocks, miner).unwrap();
tokio::time::sleep(Duration::from_secs(time)).await;
}
async fn invalidate_block(rpc: &corepc_node::Client, hash: &bitcoin::BlockHash) {
let value = serde_json::to_value(hash).unwrap();
rpc.call::<()>("invalidateblock", &[value]).unwrap();
tokio::time::sleep(Duration::from_secs(2)).await;
}
async fn sync_assert(best: &bitcoin::BlockHash, channel: &mut UnboundedReceiver<Event>) {
loop {
tokio::select! {
event = channel.recv() => {
if let Some(Event::FiltersSynced(update)) = event {
assert_eq!(update.tip().hash, *best);
println!("Correct sync");
break;
};
}
}
}
}
async fn print_logs(mut info_rx: Receiver<Info>, mut warn_rx: UnboundedReceiver<Warning>) {
loop {
tokio::select! {
log = info_rx.recv() => {
if let Some(log) = log {
println!("{log}")
}
}
warn = warn_rx.recv() => {
if let Some(warn) = warn {
println!("{warn}")
}
}
}
}
}
#[tokio::test]
async fn live_reorg() {
let (bitcoind, socket_addr) = start_bitcoind(true).unwrap();
let rpc = &bitcoind.client;
let tempdir = tempfile::TempDir::new().unwrap().path().to_owned();
let miner = rpc.new_address().unwrap();
mine_blocks(rpc, &miner, 10, 2).await;
let best = best_hash(rpc);
let (node, client) = new_node(socket_addr, tempdir, None);
tokio::task::spawn(async move { node.run().await });
let Client {
requester,
info_rx,
warn_rx,
event_rx: mut channel,
} = client;
tokio::task::spawn(async move { print_logs(info_rx, warn_rx).await });
sync_assert(&best, &mut channel).await;
let old_best = best;
let old_height = num_blocks(rpc);
invalidate_block(rpc, &best).await;
mine_blocks(rpc, &miner, 2, 1).await;
let best = best_hash(rpc);
while let Some(message) = channel.recv().await {
match message {
kyoto::messages::Event::BlocksDisconnected {
accepted: _,
disconnected: blocks,
} => {
assert_eq!(blocks.len(), 1);
assert_eq!(blocks.first().unwrap().header.block_hash(), old_best);
assert_eq!(old_height as u32, blocks.first().unwrap().height);
}
kyoto::messages::Event::FiltersSynced(update) => {
assert_eq!(update.tip().hash, best);
requester.shutdown().unwrap();
break;
}
_ => {}
}
}
requester.shutdown().unwrap();
rpc.stop().unwrap();
}
#[tokio::test]
async fn live_reorg_additional_sync() {
let (bitcoind, socket_addr) = start_bitcoind(true).unwrap();
let rpc = &bitcoind.client;
let tempdir = tempfile::TempDir::new().unwrap().path().to_owned();
let miner = rpc.new_address().unwrap();
mine_blocks(rpc, &miner, 10, 2).await;
let best = best_hash(rpc);
let (node, client) = new_node(socket_addr, tempdir, None);
tokio::task::spawn(async move { node.run().await });
let Client {
requester,
info_rx,
warn_rx,
event_rx: mut channel,
} = client;
tokio::task::spawn(async move { print_logs(info_rx, warn_rx).await });
sync_assert(&best, &mut channel).await;
let old_best = best;
let old_height = num_blocks(rpc);
let fetched_header = requester.get_header(10).await.unwrap();
assert_eq!(old_best, fetched_header.block_hash());
invalidate_block(rpc, &best).await;
mine_blocks(rpc, &miner, 2, 1).await;
let best = best_hash(rpc);
while let Some(message) = channel.recv().await {
match message {
kyoto::messages::Event::BlocksDisconnected {
accepted: _,
disconnected: blocks,
} => {
assert_eq!(blocks.len(), 1);
assert_eq!(blocks.first().unwrap().header.block_hash(), old_best);
assert_eq!(old_height as u32, blocks.first().unwrap().height);
}
kyoto::messages::Event::FiltersSynced(update) => {
assert_eq!(update.tip().hash, best);
break;
}
_ => {}
}
}
mine_blocks(rpc, &miner, 2, 1).await;
let best = best_hash(rpc);
sync_assert(&best, &mut channel).await;
requester.shutdown().unwrap();
rpc.stop().unwrap();
}
#[tokio::test]
async fn various_client_methods() {
let (bitcoind, socket_addr) = start_bitcoind(true).unwrap();
let rpc = &bitcoind.client;
let tempdir = tempfile::TempDir::new().unwrap().path().to_owned();
let miner = rpc.new_address().unwrap();
mine_blocks(rpc, &miner, 500, 15).await;
let best = best_hash(rpc);
let (node, client) = new_node(socket_addr, tempdir, None);
tokio::task::spawn(async move { node.run().await });
let Client {
requester,
info_rx,
warn_rx,
event_rx: mut channel,
} = client;
tokio::task::spawn(async move { print_logs(info_rx, warn_rx).await });
sync_assert(&best, &mut channel).await;
let batch = requester.get_header_range(10_000..10_002).await.unwrap();
assert!(batch.is_empty());
let _ = requester.broadcast_min_feerate().await.unwrap();
let _ = requester.get_header(3).await.unwrap();
assert!(requester.is_running());
requester.shutdown().unwrap();
rpc.stop().unwrap();
}
#[tokio::test]
async fn stop_reorg_resync() {
let (bitcoind, socket_addr) = start_bitcoind(true).unwrap();
let rpc = &bitcoind.client;
let tempdir: PathBuf = tempfile::TempDir::new().unwrap().path().to_owned();
let miner = rpc.new_address().unwrap();
mine_blocks(rpc, &miner, 10, 2).await;
let best = best_hash(rpc);
let (node, client) = new_node(socket_addr, tempdir.clone(), None);
tokio::task::spawn(async move { node.run().await });
let Client {
requester,
info_rx,
warn_rx,
event_rx: mut channel,
} = client;
tokio::task::spawn(async move { print_logs(info_rx, warn_rx).await });
sync_assert(&best, &mut channel).await;
let batch = requester.get_header_range(0..10).await.unwrap();
assert!(!batch.is_empty());
requester.shutdown().unwrap();
let old_best = best;
let old_height = num_blocks(rpc);
invalidate_block(rpc, &best).await;
mine_blocks(rpc, &miner, 2, 1).await;
let best = best_hash(rpc);
let (node, client) = new_node(socket_addr, tempdir.clone(), None);
tokio::task::spawn(async move { node.run().await });
let Client {
requester,
info_rx,
warn_rx,
event_rx: mut channel,
} = client;
let handle = tokio::task::spawn(async move { print_logs(info_rx, warn_rx).await });
while let Some(message) = channel.recv().await {
match message {
kyoto::messages::Event::BlocksDisconnected {
accepted: _,
disconnected: blocks,
} => {
assert_eq!(blocks.len(), 1);
assert_eq!(blocks.first().unwrap().header.block_hash(), old_best);
assert_eq!(old_height as u32, blocks.first().unwrap().height);
}
kyoto::messages::Event::FiltersSynced(update) => {
println!("Done");
assert_eq!(update.tip().hash, best);
break;
}
_ => {}
}
}
requester.shutdown().unwrap();
drop(handle);
mine_blocks(rpc, &miner, 2, 1).await;
let best = best_hash(rpc);
let (node, client) = new_node(socket_addr, tempdir, None);
tokio::task::spawn(async move { node.run().await });
let Client {
requester,
info_rx,
warn_rx,
event_rx: mut channel,
} = client;
tokio::task::spawn(async move { print_logs(info_rx, warn_rx).await });
sync_assert(&best, &mut channel).await;
requester.shutdown().unwrap();
rpc.stop().unwrap();
}
#[tokio::test]
async fn stop_reorg_two_resync() {
let (bitcoind, socket_addr) = start_bitcoind(true).unwrap();
let rpc = &bitcoind.client;
let tempdir: PathBuf = tempfile::TempDir::new().unwrap().path().to_owned();
let miner = rpc.new_address().unwrap();
mine_blocks(rpc, &miner, 10, 2).await;
let best = best_hash(rpc);
let (node, client) = new_node(socket_addr, tempdir.clone(), None);
tokio::task::spawn(async move { node.run().await });
let Client {
requester,
info_rx,
warn_rx,
event_rx: mut channel,
} = client;
let handle = tokio::task::spawn(async move { print_logs(info_rx, warn_rx).await });
sync_assert(&best, &mut channel).await;
requester.shutdown().unwrap();
let old_height = num_blocks(rpc);
let old_best = best;
invalidate_block(rpc, &best).await;
let best = best_hash(rpc);
invalidate_block(rpc, &best).await;
mine_blocks(rpc, &miner, 3, 1).await;
let best = best_hash(rpc);
drop(handle);
let (node, client) = new_node(socket_addr, tempdir.clone(), None);
tokio::task::spawn(async move { node.run().await });
let Client {
requester,
info_rx,
warn_rx,
event_rx: mut channel,
} = client;
let handle = tokio::task::spawn(async move { print_logs(info_rx, warn_rx).await });
while let Some(message) = channel.recv().await {
match message {
kyoto::messages::Event::BlocksDisconnected {
accepted: _,
disconnected: blocks,
} => {
assert_eq!(blocks.len(), 2);
assert_eq!(blocks.last().unwrap().header.block_hash(), old_best);
assert_eq!(old_height as u32, blocks.last().unwrap().height);
}
kyoto::messages::Event::FiltersSynced(update) => {
println!("Done");
assert_eq!(update.tip().hash, best);
break;
}
_ => {}
}
}
drop(handle);
requester.shutdown().unwrap();
mine_blocks(rpc, &miner, 2, 1).await;
let best = best_hash(rpc);
let (node, client) = new_node(socket_addr, tempdir, None);
tokio::task::spawn(async move { node.run().await });
let Client {
requester,
info_rx,
warn_rx,
event_rx: mut channel,
} = client;
tokio::task::spawn(async move { print_logs(info_rx, warn_rx).await });
sync_assert(&best, &mut channel).await;
requester.shutdown().unwrap();
rpc.stop().unwrap();
}
#[tokio::test]
async fn stop_reorg_start_on_orphan() {
let (bitcoind, socket_addr) = start_bitcoind(true).unwrap();
let rpc = &bitcoind.client;
let tempdir: PathBuf = tempfile::TempDir::new().unwrap().path().to_owned();
let miner = rpc.new_address().unwrap();
mine_blocks(rpc, &miner, 17, 3).await;
let best = best_hash(rpc);
let (node, client) = new_node(socket_addr, tempdir.clone(), None);
tokio::task::spawn(async move { node.run().await });
let Client {
requester,
info_rx,
warn_rx,
event_rx: mut channel,
} = client;
let handle = tokio::task::spawn(async move { print_logs(info_rx, warn_rx).await });
sync_assert(&best, &mut channel).await;
drop(handle);
requester.shutdown().unwrap();
let old_best = best;
let old_height = num_blocks(rpc);
invalidate_block(rpc, &best).await;
mine_blocks(rpc, &miner, 2, 1).await;
let best = best_hash(rpc);
let (node, client) = new_node(
socket_addr,
tempdir.clone(),
Some(HeaderCheckpoint::new(old_height as u32, old_best)),
);
tokio::task::spawn(async move { node.run().await });
let Client {
requester,
info_rx,
warn_rx,
event_rx: mut channel,
} = client;
let handle = tokio::task::spawn(async move { print_logs(info_rx, warn_rx).await });
while let Some(message) = channel.recv().await {
match message {
kyoto::messages::Event::BlocksDisconnected {
accepted: _,
disconnected: blocks,
} => {
assert_eq!(blocks.len(), 1);
assert_eq!(blocks.first().unwrap().header.block_hash(), old_best);
assert_eq!(old_height as u32, blocks.first().unwrap().height);
}
kyoto::messages::Event::FiltersSynced(update) => {
println!("Done");
assert_eq!(update.tip().hash, best);
break;
}
_ => {}
}
}
drop(handle);
requester.shutdown().unwrap();
let cp = best_hash(rpc);
let old_height = num_blocks(rpc);
let best = best_hash(rpc);
let (node, client) = new_node(
socket_addr,
tempdir.clone(),
Some(HeaderCheckpoint::new(old_height as u32, cp)),
);
tokio::task::spawn(async move { node.run().await });
let Client {
requester,
info_rx,
warn_rx,
event_rx: mut channel,
} = client;
let handle = tokio::task::spawn(async move { print_logs(info_rx, warn_rx).await });
sync_assert(&best, &mut channel).await;
drop(handle);
requester.shutdown().unwrap();
let cp = best_hash(rpc);
let old_height = num_blocks(rpc);
mine_blocks(rpc, &miner, 2, 1).await;
let best = best_hash(rpc);
let (node, client) = new_node(
socket_addr,
tempdir,
Some(HeaderCheckpoint::new(old_height as u32, cp)),
);
tokio::task::spawn(async move { node.run().await });
let Client {
requester,
info_rx,
warn_rx,
event_rx: mut channel,
} = client;
tokio::task::spawn(async move { print_logs(info_rx, warn_rx).await });
sync_assert(&best, &mut channel).await;
requester.shutdown().unwrap();
rpc.stop().unwrap();
}
#[tokio::test]
async fn tx_can_broadcast() {
let amount_to_us = Amount::from_sat(100_000);
let amount_to_op_return = Amount::from_sat(50_000);
let (bitcoind, socket_addr) = start_bitcoind(true).unwrap();
let rpc = &bitcoind.client;
let tempdir = tempfile::TempDir::new().unwrap().path().to_owned();
let mut rng = StdRng::seed_from_u64(10001);
let secret = SecretKey::new(&mut rng);
let secp = Secp256k1::new();
let keypair = Keypair::from_secret_key(&secp, &secret);
let (internal_key, _) = keypair.x_only_public_key();
let send_to_this_address = Address::p2tr(&secp, internal_key, None, KnownHrp::Regtest);
println!("Mining blocks to an internal address...");
let miner = rpc.new_address().unwrap();
mine_blocks(rpc, &miner, 110, 10).await;
let tx_info = rpc
.send_to_address(&send_to_this_address, amount_to_us)
.unwrap();
let txid = tx_info.txid().unwrap();
println!("Sent to {send_to_this_address}");
let tx_details = rpc.get_transaction(txid).unwrap().details;
let (vout, amt) = tx_details
.iter()
.find(|detail| detail.address.eq(&send_to_this_address.to_string()))
.map(|detail| (detail.vout, detail.amount))
.unwrap();
println!("{amt} Bitcoin locked at {vout} vout in {txid}");
let bytes = b"Am I spam?";
let op_return = ScriptBuf::new_op_return(bytes);
let txout = TxOut {
script_pubkey: op_return.clone(),
value: amount_to_op_return,
};
let outpoint = OutPoint { txid, vout };
let txin = TxIn {
previous_output: outpoint,
script_sig: ScriptBuf::default(),
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
witness: Witness::default(),
};
let mut unsigned_tx = Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: absolute::LockTime::ZERO,
input: vec![txin],
output: vec![txout],
};
let input_index = 0;
let sighash_type = TapSighashType::Default;
let prevout = TxOut {
script_pubkey: send_to_this_address.script_pubkey(),
value: Amount::from_btc(amt.abs()).unwrap(),
};
let prevouts = vec![prevout];
let prevouts = Prevouts::All(&prevouts);
let mut sighasher = SighashCache::new(&mut unsigned_tx);
let sighash = sighasher
.taproot_key_spend_signature_hash(input_index, &prevouts, sighash_type)
.unwrap();
let tweaked: bitcoin::key::TweakedKeypair = keypair.tap_tweak(&secp, None);
let msg = bitcoin::secp256k1::Message::from(sighash);
let signature = secp.sign_schnorr(&msg, &tweaked.to_keypair());
let signature = bitcoin::taproot::Signature {
signature,
sighash_type,
};
*sighasher.witness_mut(input_index).unwrap() = Witness::p2tr_key_spend(&signature);
let tx = sighasher.into_transaction().to_owned();
println!("Signed transaction");
let (node, client) = new_node(socket_addr, tempdir.clone(), None);
tokio::task::spawn(async move { node.run().await });
let Client {
requester,
mut info_rx,
mut warn_rx,
mut event_rx,
} = client;
requester.broadcast_random(tx).unwrap();
tokio::time::timeout(tokio::time::Duration::from_secs(60), async move {
loop {
tokio::select! {
info = info_rx.recv() => {
if let Some(info) = info {
match info {
Info::TxGossiped(_) => { break; },
_ => println!("{info}"),
}
}
}
event = event_rx.recv() => { drop(event); }
warn = warn_rx.recv() => {
if let Some(warn) = warn {
match warn {
Warning::TransactionRejected { payload: _ } => {
panic!("Transaction should be valid");
}
_ => println!("{warn}")
}
}
}
}
}
})
.await
.unwrap();
}
#[tokio::test]
async fn dns_fn_works() {
let cloudflare = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1));
let addrs = lookup_host("seed.bitcoin.sipa.be", cloudflare).await;
assert!(!addrs.is_empty());
}