use std::process::Command;
use bsv_sdk::primitives::{sha256d, to_hex};
use bsv_sdk::transaction::{Beef, MerklePath, MerklePathLeaf, BEEF_V1};
use tempfile::TempDir;
fn wallet(dir: &TempDir) -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_bsv-wallet"));
cmd.current_dir(dir.path())
.env_remove("ROOT_KEY")
.env_remove("ARC_MODE")
.env_remove("ARCADE")
.env_remove("ARC_URL")
.env("RUST_LOG", "warn");
cmd
}
fn txid_of(raw: &[u8]) -> String {
let mut h = sha256d(raw).to_vec();
h.reverse();
to_hex(&h)
}
fn tx_spending(prev: &str, script_len: usize) -> Vec<u8> {
let mut raw = Vec::new();
raw.extend_from_slice(&1u32.to_le_bytes()); raw.push(1); let mut p = hex::decode(prev).unwrap();
p.reverse();
raw.extend_from_slice(&p);
raw.extend_from_slice(&0u32.to_le_bytes()); raw.push(0); raw.extend_from_slice(&0xffff_ffffu32.to_le_bytes()); raw.push(1); raw.extend_from_slice(&1000u64.to_le_bytes()); if script_len == 0 {
raw.push(0);
} else {
raw.push(0xfe);
raw.extend_from_slice(&(script_len as u32).to_le_bytes());
raw.push(0x6a); raw.extend(vec![0u8; script_len - 1]);
}
raw.extend_from_slice(&0u32.to_le_bytes()); raw
}
fn two_leaf_path(height: u32, a: (&str, bool), b: (&str, bool)) -> MerklePath {
let leaf = |offset, (hash, txid): (&str, bool)| MerklePathLeaf {
offset,
hash: Some(hash.to_string()),
txid,
duplicate: false,
};
MerklePath {
block_height: height,
path: vec![vec![leaf(0, a), leaf(1, b)]],
}
}
#[tokio::test]
async fn compact_with_chaintracks_off_writes_no_unchecked_proof_into_a_stored_beef() {
let dir = TempDir::new().expect("temp dir");
let init = wallet(&dir)
.args(["--db", "wallet.db", "init"])
.output()
.expect("run init");
assert!(
init.status.success(),
"init: {}",
String::from_utf8_lossy(&init.stderr)
);
let parent = tx_spending(&"11".repeat(32), 0);
let p = txid_of(&parent);
let s = "22".repeat(32);
let child = tx_spending(&p, 0);
let c = txid_of(&child);
let filler = tx_spending(&"33".repeat(32), 60_000);
let mut stored = Beef::with_version(BEEF_V1);
stored.merge_bump(two_leaf_path(500, (&p, false), (&s, true)));
stored.merge_raw_tx(parent.clone(), None);
stored.merge_raw_tx(filler, None);
stored.merge_raw_tx(child.clone(), None);
let stored_bytes = stored.to_binary_atomic(&c).expect("atomic BEEF");
assert!(
Beef::from_binary(&stored_bytes)
.unwrap()
.find_txid(&p)
.and_then(|t| t.bump_index())
.is_none(),
"precondition: P rides unproven"
);
let db = dir.path().join("wallet.db");
let pool = sqlx::SqlitePool::connect(&format!("sqlite:{}", db.display()))
.await
.expect("open wallet.db");
let proof = two_leaf_path(500, (&p, true), (&s, false));
let root = proof.compute_root(Some(&p)).unwrap();
let now = chrono::Utc::now();
sqlx::query(
"INSERT INTO proven_txs (txid, height, idx, block_hash, merkle_root, merkle_path, raw_tx, created_at, updated_at) VALUES (?, 500, 0, ?, ?, ?, ?, ?, ?)",
)
.bind(&p)
.bind("ab".repeat(32))
.bind(&root)
.bind(proof.to_binary())
.bind(&parent)
.bind(now)
.bind(now)
.execute(&pool)
.await
.expect("seed the unchecked proof");
sqlx::query(
"INSERT INTO proven_tx_reqs (txid, status, attempts, history, notified, notify, raw_tx, input_beef, created_at, updated_at) VALUES (?, 'completed', 0, '{}', 0, '{}', ?, ?, ?, ?)",
)
.bind(&c)
.bind(&child)
.bind(&stored_bytes)
.bind(now)
.bind(now)
.execute(&pool)
.await
.expect("seed the completed request");
pool.close().await;
let out = wallet(&dir)
.env("CHAINTRACKS_URL", "off")
.args(["--db", "wallet.db", "compact"])
.output()
.expect("run compact");
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(out.status.success(), "compact: {stdout}{stderr}");
let pool = sqlx::SqlitePool::connect(&format!("sqlite:{}", db.display()))
.await
.expect("reopen wallet.db");
let (after,): (Vec<u8>,) =
sqlx::query_as("SELECT input_beef FROM proven_tx_reqs WHERE txid = ?")
.bind(&c)
.fetch_one(&pool)
.await
.unwrap();
let after = Beef::from_binary(&after).expect("the stored BEEF parses");
assert!(
after.find_txid(&p).and_then(|t| t.bump_index()).is_none(),
"no unchecked proof is written into a stored BEEF with no tracker: {stdout}"
);
let unchanged: Option<(i64,)> =
sqlx::query_as("SELECT 1 FROM proven_tx_reqs WHERE txid = ? AND input_beef = ?")
.bind(&c)
.bind(&stored_bytes)
.fetch_optional(&pool)
.await
.unwrap();
assert!(unchanged.is_some(), "the stored BEEF is untouched");
pool.close().await;
}