Skip to content

Commit 9292d90

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 10cc283 commit 9292d90

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

0 commit comments

Comments
 (0)