Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## v0.4.0 - 2026-06-19

### Added

- Added `review-pack --input DIR --format json|markdown [--output FILE]` for consolidated local review of descriptor, PSBT, transaction, script, policy, and notes artifacts.
- Added schema version `0.4` `ReviewPackReport` output with detected artifacts, per-artifact summaries, consolidated risk, warnings, missing data, cross-artifact findings, review questions, and limitations.
- Added cross-artifact checks for descriptor/PSBT multisig and timelock signals, descriptor threshold limitations, and PSBT/transaction input-output counts.
- Added review-pack fixtures and CLI regression tests for JSON stdout and Markdown file output.

### Security

- `review-pack` performs local file analysis only. It does not sign, create wallets, handle keys, broadcast transactions, or make network calls.

## v0.3.0 - 2026-06-18

### Added
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "btc-risk-lab"
version = "0.3.0"
version = "0.4.0"
edition = "2021"
authors = ["Jose Robles"]
description = "A Rust CLI for explainable Bitcoin transaction, PSBT, and script risk reports."
Expand Down
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Bitcoin and Web3 systems often fail at the edges: incomplete transaction context
- explicit missing-data dependencies
- human-readable warning explanations
- JSON output suitable for downstream automation
- consolidated review packs for descriptor, PSBT, transaction, script, policy, and notes directories
- optional AI summary layer that never replaces the technical report
- clear security boundaries around private keys and funds

Expand All @@ -34,6 +35,7 @@ This MVP is intentionally small, but it is structured like a real due diligence
- script inspection heuristics
- structured reporting with `serde`
- Markdown and JSON output
- review-pack reports with cross-artifact checks
- CI with `fmt`, `clippy`, and tests
- a security posture that avoids custody, signing, seed phrases, and private key handling

Expand Down Expand Up @@ -107,6 +109,29 @@ Analyze an output descriptor:
btc-risk-lab analyze-descriptor --descriptor "wsh(sortedmulti(2,02...,03...,04...))" --format markdown
```

Analyze a local review pack directory:

```bash
btc-risk-lab review-pack --input tests/fixtures/review-packs/complete --format markdown
```

Write a review pack report to a file:

```bash
btc-risk-lab review-pack --input ./review-pack --format json --output review-pack-report.json
```

`review-pack` looks for these optional files:

- `descriptor.txt`
- `psbt.base64`
- `tx.json`
- `script.txt`
- `policy.json`
- `notes.md`

It reuses the existing descriptor, PSBT, transaction, and script analyzers, then emits a schema `0.4` `ReviewPackReport` with detected artifacts, per-artifact summaries, consolidated risk, warnings, missing data, cross-artifact findings, review questions, and limitations.

Generate an optional executive summary from an existing JSON report:

```bash
Expand Down Expand Up @@ -172,6 +197,7 @@ Current analysis includes:
- descriptor sanity check through `miniscript`
- descriptor max satisfaction weight where available
- threshold and multisig policy hints
- review-pack cross-artifact checks for descriptor/PSBT policy signals and PSBT/transaction input-output counts
- script complexity score
- report schema versioning
- missing-data dependencies
Expand All @@ -188,6 +214,7 @@ Current analysis includes:
- handle private keys
- request seed phrases
- broadcast transactions
- make network calls from `review-pack`
- promise consensus-level validation
- send secrets to an LLM

Expand All @@ -203,7 +230,9 @@ AI support is optional and isolated behind the `ai` feature flag. The intended p

This MVP uses heuristics. It does not perform full Bitcoin Core policy validation, mempool acceptance simulation, chain lookup, script execution, wallet state analysis, or consensus-level validation.

Risk classifications are only as complete as the artifact data provided. Missing UTXO data, omitted redeem scripts, absent witness scripts, incomplete PSBT maps, and descriptors without operational wallet context can all reduce confidence.
Risk classifications are only as complete as the artifact data provided. Missing UTXO data, omitted redeem scripts, absent witness scripts, incomplete PSBT maps, descriptors without operational wallet context, and review packs without matching descriptor/PSBT/transaction artifacts can all reduce confidence.

Review-pack cross-artifact checks are intentionally limited. The tool compares available policy signals and input/output counts, but it does not prove descriptor-to-PSBT equivalence, transaction extraction from PSBT, key origin correctness, signer-set ownership, or wallet state.

## Technical Due Diligence Connection

Expand Down
4 changes: 3 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- [x] PSBT analysis from base64 input
- [x] script analysis from hex or a small ASM subset
- [x] descriptor parsing and policy hints with `miniscript`
- [x] review-pack reports for descriptor, PSBT, transaction, script, policy, and notes directories
- [x] report schema versioning
- [x] JSON and Markdown reports
- [x] risk warnings with human explanations
Expand All @@ -30,7 +31,7 @@

## Due Diligence Use Cases

- batch analysis for transaction review packs
- richer batch analysis for transaction review packs
- policy review for multisig and timelock setups
- PSBT readiness checklist
- executive PDF or Markdown due diligence reports
Expand All @@ -44,4 +45,5 @@
- seed phrase handling
- custody
- broadcasting
- review-pack network calls
- consensus-level validation claims
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod analyzer;
pub mod report;
pub mod review_pack;

#[cfg(feature = "ai")]
pub mod ai;
Expand Down
34 changes: 32 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use std::path::PathBuf;
use std::{fs, path::PathBuf};

use anyhow::{bail, Result};
use clap::{Parser, Subcommand, ValueEnum};

use btc_risk_lab::analyzer;
use btc_risk_lab::report::{render_report, OutputFormat};
use btc_risk_lab::report::{render_report, render_review_pack_report, OutputFormat};

#[derive(Debug, Parser)]
#[command(author, version, about)]
Expand Down Expand Up @@ -77,6 +77,18 @@ enum Commands {
format: CliFormat,
},

/// Analyze a local directory containing descriptor, PSBT, transaction, script, policy, and notes artifacts.
ReviewPack {
#[arg(long, value_name = "DIR")]
input: PathBuf,

#[arg(long, value_enum, default_value_t = CliFormat::Markdown)]
format: CliFormat,

#[arg(long, value_name = "FILE")]
output: Option<PathBuf>,
},

/// Generate an optional executive summary from an existing technical JSON report.
Summarize {
#[arg(long)]
Expand Down Expand Up @@ -136,12 +148,30 @@ fn main() -> Result<()> {
let report = analyzer::analyze_descriptor_input(&descriptor)?;
println!("{}", render_report(&report, format.into())?);
}
Commands::ReviewPack {
input,
format,
output,
} => {
let report = btc_risk_lab::review_pack::analyze_review_pack(&input)?;
write_or_print(render_review_pack_report(&report, format.into())?, output)?;
}
Commands::Summarize { input, provider } => summarize(input, provider)?,
}

Ok(())
}

fn write_or_print(rendered: String, output: Option<PathBuf>) -> Result<()> {
if let Some(output) = output {
fs::write(&output, rendered)?;
} else {
println!("{rendered}");
}

Ok(())
}

#[cfg(feature = "fetch")]
fn fetch_tx(txid: &str) -> Result<btc_risk_lab::analyzer::RiskReport> {
let runtime = tokio::runtime::Builder::new_multi_thread()
Expand Down
112 changes: 112 additions & 0 deletions src/report/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use anyhow::Result;

use crate::analyzer::{ArtifactType, RiskReport};
use crate::review_pack::ReviewPackReport;

#[derive(Clone, Copy, Debug)]
pub enum OutputFormat {
Expand All @@ -15,6 +16,16 @@ pub fn render_report(report: &RiskReport, format: OutputFormat) -> Result<String
}
}

pub fn render_review_pack_report(
report: &ReviewPackReport,
format: OutputFormat,
) -> Result<String> {
match format {
OutputFormat::Json => Ok(serde_json::to_string_pretty(report)?),
OutputFormat::Markdown => Ok(render_review_pack_markdown(report)),
}
}

fn render_markdown(report: &RiskReport) -> String {
let mut markdown = String::new();
markdown.push_str("# BTC Risk Lab Report\n\n");
Expand Down Expand Up @@ -130,6 +141,107 @@ fn push_outputs_table(markdown: &mut String, outputs: &[crate::analyzer::OutputA
}
}

fn render_review_pack_markdown(report: &ReviewPackReport) -> String {
let mut markdown = String::new();
markdown.push_str("# BTC Risk Lab Review Pack\n\n");
markdown.push_str(&format!("- Schema: `{}`\n", report.schema_version));
markdown.push_str(&format!(
"- Consolidated risk: `{:?}`\n\n",
report.consolidated_risk
));
Comment thread
josediegorobles marked this conversation as resolved.

markdown.push_str("## Artifacts Detected\n\n");
if report.artifacts_detected.is_empty() {
markdown.push_str("- No known review pack artifacts were detected.\n\n");
} else {
markdown.push_str("| Artifact | File |\n|---|---|\n");
for artifact in &report.artifacts_detected {
markdown.push_str(&format!(
"| `{}` | `{}` |\n",
artifact.artifact, artifact.file
));
}
markdown.push('\n');
}

markdown.push_str("## Per-Artifact Summary\n\n");
if report.per_artifact_summary.is_empty() {
markdown.push_str("- No artifact summaries available.\n\n");
} else {
for artifact in &report.per_artifact_summary {
markdown.push_str(&format!(
"### `{}` ({:?})\n\n",
artifact.artifact, artifact.status
));
if let Some(risk) = &artifact.risk {
markdown.push_str(&format!("- Risk: `{:?}`\n", risk));
}
Comment thread
josediegorobles marked this conversation as resolved.
for item in &artifact.summary {
markdown.push_str(&format!("- {}: `{}`\n", item.label, item.value));
}
if !artifact.missing_data.is_empty() {
markdown.push_str("- Missing data:\n");
for item in &artifact.missing_data {
markdown.push_str(&format!(" - {}\n", item));
}
}
if !artifact.warnings.is_empty() {
markdown.push_str("- Warnings:\n");
for warning in &artifact.warnings {
markdown.push_str(&format!(
" - **{}** (`{:?}`, `{}`): {}\n",
warning.title, warning.severity, warning.code, warning.explanation
));
}
}
markdown.push('\n');
}
}

if !report.warnings.is_empty() {
markdown.push_str("## Warnings\n\n");
for warning in &report.warnings {
markdown.push_str(&format!(
"- **{}** (`{:?}`, `{}`): {}\n",
warning.title, warning.severity, warning.code, warning.explanation
));
}
markdown.push('\n');
}

if !report.missing_data.is_empty() {
markdown.push_str("## Missing Data\n\n");
for item in &report.missing_data {
markdown.push_str(&format!("- {}\n", item));
}
markdown.push('\n');
}

if !report.cross_artifact_findings.is_empty() {
markdown.push_str("## Cross-Artifact Findings\n\n");
for finding in &report.cross_artifact_findings {
markdown.push_str(&format!(
"- **{}** (`{:?}`, `{}`): {}\n",
finding.title, finding.severity, finding.code, finding.explanation
));
}
markdown.push('\n');
}

markdown.push_str("## Review Questions\n\n");
for question in &report.review_questions {
markdown.push_str(&format!("- {}\n", question));
}
markdown.push('\n');

markdown.push_str("## Limitations\n\n");
for limitation in &report.limitations {
markdown.push_str(&format!("- {}\n", limitation));
}

markdown
}

fn artifact_name(artifact_type: &ArtifactType) -> &'static str {
match artifact_type {
ArtifactType::Transaction => "transaction",
Expand Down
Loading
Loading