Skip to content

Commit c869c0f

Browse files
Add property-based tests for RPC JSON responses
- Validate roundtrip serialization/deserialization for Txids, transactions, mempool entries, and fee responses. - Ensure Txid parsing/serialization matches Bitcoin Core RPC expectations.
1 parent 9b4c1b5 commit c869c0f

File tree

1 file changed

+161
-0
lines changed

1 file changed

+161
-0
lines changed

src/chain/bitcoind.rs

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1411,3 +1411,164 @@ impl std::fmt::Display for HttpError {
14111411
write!(f, "status_code: {}, contents: {}", self.status_code, contents)
14121412
}
14131413
}
1414+
1415+
#[cfg(test)]
1416+
mod tests {
1417+
use bitcoin::hashes::Hash;
1418+
use bitcoin::{FeeRate, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid, Witness};
1419+
use lightning_block_sync::http::JsonResponse;
1420+
use proptest::{arbitrary::any, collection::vec, prop_assert_eq, prop_compose, proptest};
1421+
use serde_json::json;
1422+
1423+
use crate::chain::bitcoind::{
1424+
FeeResponse, GetMempoolEntryResponse, GetRawMempoolResponse, GetRawTransactionResponse,
1425+
MempoolMinFeeResponse,
1426+
};
1427+
1428+
prop_compose! {
1429+
fn arbitrary_witness()(
1430+
witness_elements in vec(vec(any::<u8>(), 0..100), 0..20)
1431+
) -> Witness {
1432+
let mut witness = Witness::new();
1433+
for element in witness_elements {
1434+
witness.push(element);
1435+
}
1436+
witness
1437+
}
1438+
}
1439+
1440+
prop_compose! {
1441+
fn arbitrary_txin()(
1442+
outpoint_hash in any::<[u8; 32]>(),
1443+
outpoint_vout in any::<u32>(),
1444+
script_bytes in vec(any::<u8>(), 0..100),
1445+
witness in arbitrary_witness(),
1446+
sequence in any::<u32>()
1447+
) -> TxIn {
1448+
TxIn {
1449+
previous_output: OutPoint {
1450+
txid: Txid::from_byte_array(outpoint_hash),
1451+
vout: outpoint_vout,
1452+
},
1453+
script_sig: ScriptBuf::from_bytes(script_bytes),
1454+
sequence: bitcoin::Sequence::from_consensus(sequence),
1455+
witness,
1456+
}
1457+
}
1458+
}
1459+
1460+
prop_compose! {
1461+
fn arbitrary_txout()(
1462+
value in 0u64..21_000_000_00_000_000u64,
1463+
script_bytes in vec(any::<u8>(), 0..100)
1464+
) -> TxOut {
1465+
TxOut {
1466+
value: bitcoin::Amount::from_sat(value),
1467+
script_pubkey: ScriptBuf::from_bytes(script_bytes),
1468+
}
1469+
}
1470+
}
1471+
1472+
prop_compose! {
1473+
fn arbitrary_transaction()(
1474+
version in any::<i32>(),
1475+
inputs in vec(arbitrary_txin(), 1..20),
1476+
outputs in vec(arbitrary_txout(), 1..20),
1477+
lock_time in any::<u32>()
1478+
) -> Transaction {
1479+
Transaction {
1480+
version: bitcoin::transaction::Version(version),
1481+
input: inputs,
1482+
output: outputs,
1483+
lock_time: bitcoin::absolute::LockTime::from_consensus(lock_time),
1484+
}
1485+
}
1486+
}
1487+
1488+
proptest! {
1489+
#![proptest_config(proptest::test_runner::Config::with_cases(250))]
1490+
1491+
#[test]
1492+
fn prop_get_raw_mempool_response_roundtrip(txids in vec(any::<[u8;32]>(), 0..10)) {
1493+
let txid_vec: Vec<Txid> = txids.into_iter().map(Txid::from_byte_array).collect();
1494+
let original = GetRawMempoolResponse(txid_vec.clone());
1495+
1496+
let json_vec: Vec<String> = txid_vec.iter().map(|t| t.to_string()).collect();
1497+
let json_val = serde_json::Value::Array(json_vec.iter().map(|s| json!(s)).collect());
1498+
1499+
let resp = JsonResponse(json_val);
1500+
let decoded: GetRawMempoolResponse = resp.try_into().unwrap();
1501+
1502+
prop_assert_eq!(original.0.len(), decoded.0.len());
1503+
1504+
prop_assert_eq!(original.0, decoded.0);
1505+
}
1506+
1507+
#[test]
1508+
fn prop_get_mempool_entry_response_roundtrip(
1509+
time in any::<u64>(),
1510+
height in any::<u32>()
1511+
) {
1512+
let json_val = json!({
1513+
"time": time,
1514+
"height": height
1515+
});
1516+
1517+
let resp = JsonResponse(json_val);
1518+
let decoded: GetMempoolEntryResponse = resp.try_into().unwrap();
1519+
1520+
prop_assert_eq!(decoded.time, time);
1521+
prop_assert_eq!(decoded.height, height);
1522+
}
1523+
1524+
#[test]
1525+
fn prop_get_raw_transaction_response_roundtrip(tx in arbitrary_transaction()) {
1526+
let hex = bitcoin::consensus::encode::serialize_hex(&tx);
1527+
let json_val = serde_json::Value::String(hex.clone());
1528+
1529+
let resp = JsonResponse(json_val);
1530+
let decoded: GetRawTransactionResponse = resp.try_into().unwrap();
1531+
1532+
prop_assert_eq!(decoded.0.compute_txid(), tx.compute_txid());
1533+
prop_assert_eq!(decoded.0.compute_wtxid(), tx.compute_wtxid());
1534+
1535+
prop_assert_eq!(decoded.0, tx);
1536+
}
1537+
1538+
#[test]
1539+
fn prop_fee_response_roundtrip(fee_rate in any::<f64>()) {
1540+
let fee_rate = fee_rate.abs();
1541+
let json_val = json!({
1542+
"feerate": fee_rate,
1543+
"errors": serde_json::Value::Null
1544+
});
1545+
1546+
let resp = JsonResponse(json_val);
1547+
let decoded: FeeResponse = resp.try_into().unwrap();
1548+
1549+
let expected = {
1550+
let fee_rate_sat_per_kwu = (fee_rate * 25_000_000.0).round() as u64;
1551+
FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu)
1552+
};
1553+
prop_assert_eq!(decoded.0, expected);
1554+
}
1555+
1556+
#[test]
1557+
fn prop_mempool_min_fee_response_roundtrip(fee_rate in any::<f64>()) {
1558+
let fee_rate = fee_rate.abs();
1559+
let json_val = json!({
1560+
"mempoolminfee": fee_rate
1561+
});
1562+
1563+
let resp = JsonResponse(json_val);
1564+
let decoded: MempoolMinFeeResponse = resp.try_into().unwrap();
1565+
1566+
let expected = {
1567+
let fee_rate_sat_per_kwu = (fee_rate * 25_000_000.0).round() as u64;
1568+
FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu)
1569+
};
1570+
prop_assert_eq!(decoded.0, expected);
1571+
}
1572+
1573+
}
1574+
}

0 commit comments

Comments
 (0)