Skip to content

Commit fe251c2

Browse files
Add regression tests for risk analysis
1 parent 0ebd636 commit fe251c2

3 files changed

Lines changed: 112 additions & 9 deletions

File tree

src/analyzer/psbt.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,17 +217,42 @@ fn psbt_complexity_score(psbt: &Psbt, signals: &ScriptSignals) -> u32 {
217217
#[cfg(test)]
218218
mod tests {
219219
use super::*;
220+
use bitcoin::{Amount, ScriptBuf, TxOut};
220221

221-
#[test]
222-
fn parses_minimal_psbt() {
222+
fn minimal_psbt() -> Psbt {
223223
let bytes = STANDARD
224224
.decode("cHNidP8BAHECAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wD/////AugDAAAAAAAAFgAUAAAAAAAAAAAAAAAAAAAAAAAAAABkAAAAAAAAABYAFBERERERERERERERERERERERERERAAAAAAAAAAA=")
225225
.unwrap();
226-
let psbt = Psbt::deserialize(&bytes).unwrap();
226+
227+
Psbt::deserialize(&bytes).unwrap()
228+
}
229+
230+
#[test]
231+
fn parses_minimal_psbt() {
232+
let psbt = minimal_psbt();
227233

228234
let report = analyze_psbt(&psbt);
229235

230236
assert_eq!(report.psbt.unwrap().input_count, 1);
231237
assert_eq!(report.risk, RiskLevel::Medium);
232238
}
239+
240+
#[test]
241+
fn warns_when_witness_utxo_implies_negative_fee() {
242+
let mut psbt = minimal_psbt();
243+
psbt.inputs[0].witness_utxo = Some(TxOut {
244+
value: Amount::from_sat(1_000),
245+
script_pubkey: ScriptBuf::new(),
246+
});
247+
248+
let report = analyze_psbt(&psbt);
249+
250+
assert_eq!(report.psbt.unwrap().estimated_fee_sats, Some(-100));
251+
assert_eq!(report.risk, RiskLevel::High);
252+
assert!(report.missing_data.is_empty());
253+
assert!(report
254+
.warnings
255+
.iter()
256+
.any(|warning| warning.code == "negative-fee"));
257+
}
233258
}

src/analyzer/tx.rs

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -230,16 +230,22 @@ fn complexity_score(tx: &Transaction, signals: &ScriptSignals) -> u32 {
230230
mod tests {
231231
use super::*;
232232

233+
const SAMPLE_TX_HEX: &str = "02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff02e80300000000000016001400000000000000000000000000000000000000006400000000000000160014111111111111111111111111111111111111111100000000";
234+
235+
fn sample_tx() -> Transaction {
236+
deserialize(&hex::decode(SAMPLE_TX_HEX).unwrap()).unwrap()
237+
}
238+
233239
#[test]
234240
fn estimates_fee_when_prevouts_are_present() {
235241
let input = TransactionInputFile {
236-
hex: "02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff02e80300000000000016001400000000000000000000000000000000000000006400000000000000160014111111111111111111111111111111111111111100000000".to_owned(),
242+
hex: SAMPLE_TX_HEX.to_owned(),
237243
prevouts: vec![PrevoutInput {
238244
value_sats: 2_000,
239245
script_pubkey: None,
240246
}],
241247
};
242-
let tx = deserialize::<Transaction>(&hex::decode(input.hex).unwrap()).unwrap();
248+
let tx = sample_tx();
243249

244250
let report = analyze_transaction(&tx, &input.prevouts, None);
245251

@@ -249,15 +255,54 @@ mod tests {
249255

250256
#[test]
251257
fn direct_hex_reports_fee_unavailable_without_prevouts() {
252-
let report = analyze_transaction_hex(
253-
"02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff02e80300000000000016001400000000000000000000000000000000000000006400000000000000160014111111111111111111111111111111111111111100000000",
254-
)
255-
.unwrap();
258+
let report = analyze_transaction_hex(SAMPLE_TX_HEX).unwrap();
256259

257260
let tx = report.transaction.unwrap();
258261
assert_eq!(tx.estimated_fee_sats, None);
259262
assert!(report
260263
.missing_data
261264
.contains(&"prevout values for every input".to_owned()));
262265
}
266+
267+
#[test]
268+
fn warns_when_prevouts_imply_negative_fee() {
269+
let tx = sample_tx();
270+
let prevouts = vec![PrevoutInput {
271+
value_sats: 1_000,
272+
script_pubkey: None,
273+
}];
274+
275+
let report = analyze_transaction(&tx, &prevouts, None);
276+
277+
assert_eq!(report.transaction.unwrap().estimated_fee_sats, Some(-100));
278+
assert_eq!(report.risk, RiskLevel::High);
279+
assert!(report
280+
.warnings
281+
.iter()
282+
.any(|warning| warning.code == "negative-fee"));
283+
}
284+
285+
#[test]
286+
fn detects_multisig_and_timelock_from_prevout_script_pubkeys() {
287+
let tx = sample_tx();
288+
let prevouts = vec![PrevoutInput {
289+
value_sats: 2_000,
290+
script_pubkey: Some("52aeb2".to_owned()),
291+
}];
292+
293+
let report = analyze_transaction(&tx, &prevouts, None);
294+
let analysis = report.transaction.unwrap();
295+
296+
assert!(analysis.signals.multisig);
297+
assert!(analysis.signals.relative_timelock);
298+
assert_eq!(report.risk, RiskLevel::Medium);
299+
assert!(report
300+
.warnings
301+
.iter()
302+
.any(|warning| warning.code == "multisig-signal"));
303+
assert!(report
304+
.warnings
305+
.iter()
306+
.any(|warning| warning.code == "timelock-signal"));
307+
}
263308
}

src/report/mod.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,36 @@ fn artifact_name(artifact_type: &ArtifactType) -> &'static str {
114114
ArtifactType::Script => "script",
115115
}
116116
}
117+
118+
#[cfg(test)]
119+
mod tests {
120+
use super::*;
121+
use crate::analyzer::analyze_script_input;
122+
123+
#[test]
124+
fn renders_script_report_as_stable_json_shape() {
125+
let report = analyze_script_input("OP_CHECKMULTISIG OP_CHECKSEQUENCEVERIFY").unwrap();
126+
127+
let rendered = render_report(&report, OutputFormat::Json).unwrap();
128+
let json: serde_json::Value = serde_json::from_str(&rendered).unwrap();
129+
130+
assert_eq!(json["artifact_type"], "script");
131+
assert_eq!(json["risk"], "medium");
132+
assert_eq!(json["script"]["signals"]["multisig"], true);
133+
assert_eq!(json["script"]["signals"]["relative_timelock"], true);
134+
}
135+
136+
#[test]
137+
fn renders_script_report_as_stable_markdown_sections() {
138+
let report = analyze_script_input("OP_CHECKMULTISIG OP_CHECKSEQUENCEVERIFY").unwrap();
139+
140+
let rendered = render_report(&report, OutputFormat::Markdown).unwrap();
141+
142+
assert!(rendered.starts_with("# BTC Risk Lab Report\n\n"));
143+
assert!(rendered.contains("- Artifact: `script`"));
144+
assert!(rendered.contains("- Risk: `medium`"));
145+
assert!(rendered.contains("## Warnings\n\n"));
146+
assert!(rendered.contains("## Script Detail\n\n"));
147+
assert!(rendered.contains("## Limitations\n\n"));
148+
}
149+
}

0 commit comments

Comments
 (0)