Skip to content

refactor: implement vulnerability detection in Rust crate#16

Merged
LORDBABUINO merged 11 commits intostealth-bitcoin:mainfrom
satsfy:add-rust-detector
Apr 4, 2026
Merged

refactor: implement vulnerability detection in Rust crate#16
LORDBABUINO merged 11 commits intostealth-bitcoin:mainfrom
satsfy:add-rust-detector

Conversation

@satsfy
Copy link
Copy Markdown
Collaborator

@satsfy satsfy commented Mar 26, 2026

Partially solves #10

This PR is a combination of rust refactors from @brenorb in #15 and me.

New crates:

  • engine -> wallet analysis engine in a dedicated Rust crate.
  • model -> types and interfaces to be shared around multiple crates.
  • bitcoincore -> implementing adapter for Bitcoin Core.

This PR:

  • Converts the repo into a Rust workspace with separate crates.
  • Introduces a canonical AnalysisEngine + BlockchainGateway architecture so API, CLI, and library usage all go through one main scan path.
  • Introduces TxGraph into a prebuilt graph from WalletHistory, with precomputed caches and a spending index for deterministic analysis.
  • Rewrites a large part of the docs/README/testing story: documents the workspace crates, project structure, detector taxonomy, and updates integration tests to use the new gateway/engine path instead of direct client wiring.

Reviewer Notes

In future PRs, a bitcoin core adapter, cli and api will be added. The only review possible at this point is running tests with cargo test.

@satsfy satsfy marked this pull request as ready for review March 26, 2026 15:00
@satsfy satsfy force-pushed the add-rust-detector branch 6 times, most recently from 9409a2b to b2da99c Compare March 26, 2026 15:50
@satsfy
Copy link
Copy Markdown
Collaborator Author

satsfy commented Mar 26, 2026

Previous force pushes were adapting the PR to reviewer requests. There will not be further changed unless a reviewer asks. @LORDBABUINO.

@satsfy satsfy force-pushed the add-rust-detector branch 3 times, most recently from e8c1e2a to 3311ec5 Compare March 31, 2026 01:21
satsfy added 5 commits March 30, 2026 23:11
Removed from crates/stealth-bitcoincore and moved to bitcoincore as a
standalone package. This change is part of the refactor to create
separate packages for each component of the stealth project, allowing
for better modularity and separation of concerns.
@satsfy satsfy force-pushed the add-rust-detector branch from 3311ec5 to 9eb6b8b Compare March 31, 2026 02:15
@satsfy
Copy link
Copy Markdown
Collaborator Author

satsfy commented Mar 31, 2026

@LORDBABUINO Thanks for the great review!

All but one requests implemented.

@satsfy satsfy requested a review from LORDBABUINO March 31, 2026 02:16
@LORDBABUINO
Copy link
Copy Markdown
Collaborator

LORDBABUINO commented Mar 31, 2026

Thanks for addressing all the review comments — the code is looking much better. The adoption of bitcoin::Txid, bitcoin::Address, and bitcoin::Amount goes beyond what I asked and is a welcome improvement.

However, I need to flag again: please don't force push during review. We've talked about this before. I need to see what changed per comment without re-reviewing the entire PR. Use fixup commits during review.

Now, I apologize for not raising this sooner — I should have run this comparison before the first round of review, which would have saved you from fixing code that may need further changes. After testing both branches against the same regtest environment, your code detects 7 fewer findings than main:

Detector main PR #16 Diff
CHANGE_DETECTION 7 2 -5
CIOH 11 10 -1
CLUSTER_MERGE 11 10 -1
Total 39 32 -7

The biggest gap is CHANGE_DETECTION. Please investigate and fix these regressions.

How to reproduce

  1. Start regtest and create vulnerability scenarios:
cd backend/script
./setup.sh --fresh
python3 reproduce.py
  1. Run on main branch:
git checkout main
mkdir -p crates/stealth-app/examples

Create crates/stealth-app/examples/scan_wallet.rs:

use std::env;
use stealth_bitcoincore::{BitcoinCoreConfig, BitcoinCoreRpc};
use stealth_core::engine::{AnalysisEngine, EngineSettings, ScanTarget};

fn main() {
    let wallet = env::args().nth(1).unwrap_or_else(|| "alice".into());
    let cookie = std::fs::read_to_string("backend/script/bitcoin-data/regtest/.cookie")
        .expect("cannot read cookie file — is regtest running?");
    let mut parts = cookie.trim().splitn(2, ':');
    let user = parts.next().unwrap().to_string();
    let pass = parts.next().unwrap().to_string();

    let config = BitcoinCoreConfig {
        network: "regtest".into(),
        datadir: None,
        rpchost: "127.0.0.1".into(),
        rpcport: 18443,
        rpcuser: Some(user),
        rpcpassword: Some(pass),
    };

    let gateway = BitcoinCoreRpc::new(config).expect("failed to connect");
    let engine = AnalysisEngine::new(&gateway, EngineSettings::default());
    let report = engine.analyze(ScanTarget::WalletName(wallet)).expect("analysis failed");
    println!("{}", serde_json::to_string_pretty(&report).unwrap());
}
cargo run -p stealth-app --example scan_wallet 2>/dev/null | python3 -c "
import json, sys; r = json.load(sys.stdin)
from collections import Counter
types = Counter(f['type'] for f in r['findings'])
for t, c in sorted(types.items()): print(f'  {t}: {c}')
print(f'  TOTAL: {sum(types.values())}')
"
  1. Run on your branch:
git checkout add-rust-detector
mkdir -p engine/examples

Create engine/examples/scan_wallet.rs:

use std::env;
use stealth_bitcoincore::BitcoinCoreRpc;
use stealth_engine::gateway::BlockchainGateway;
use stealth_engine::{EngineSettings, TxGraph};

fn main() {
    let wallet = env::args().nth(1).unwrap_or_else(|| "alice".into());
    let cookie = std::fs::read_to_string("backend/script/bitcoin-data/regtest/.cookie")
        .expect("cannot read cookie file — is regtest running?");
    let mut parts = cookie.trim().splitn(2, ':');
    let user = parts.next().unwrap().to_string();
    let pass = parts.next().unwrap().to_string();

    let gateway = BitcoinCoreRpc::from_url("http://127.0.0.1:18443", Some(user), Some(pass))
        .expect("failed to connect");
    let history = gateway.scan_wallet(&wallet).expect("scan_wallet failed");
    let graph = TxGraph::from_wallet_history(history);
    let settings = EngineSettings::default();
    let report = graph.detect_all(
        &settings.config.thresholds,
        settings.known_risky_txids.as_ref(),
        settings.known_exchange_txids.as_ref(),
    );
    println!("{}", serde_json::to_string_pretty(&report).unwrap());
}
cargo run --example scan_wallet 2>/dev/null | python3 -c "
import json, sys; r = json.load(sys.stdin)
from collections import Counter
types = Counter(f['type'] for f in r['findings'])
for t, c in sorted(types.items()): print(f'  {t}: {c}')
print(f'  TOTAL: {sum(types.values())}')
"

Important: both runs must use the same regtest chain — don't run setup.sh --fresh between them.

Copy link
Copy Markdown
Collaborator

@LORDBABUINO LORDBABUINO left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes — see my comment above for regression details and reproduction steps.

satsfy added 2 commits March 31, 2026 19:31
…nterfaces

Track all addresses derived from wallet descriptors in WalletHistory, not only internal/change addresses. This gives TxGraph a complete address set to compare ownership against later.
Derive addresses from all wallet descriptors, external and internal, and persist both internal_addresses and derived_addresses on WalletHistory. This aligns the Rust path with the Python reference behavior.
…hestration

Seed TxGraph ownership state from all derived addresses, not just addresses discovered from transaction outputs. This makes is_ours() recognize every descriptor-derived address and preserves internal/change tagging separately.
@satsfy
Copy link
Copy Markdown
Collaborator Author

satsfy commented Mar 31, 2026

Nice catch @LORDBABUINO! My bad for not validating this properly

The Rust TxGraph only added addresses to our_addrs from non-"send" listtransactions entries and current UTXOs. The Python reference uses set(addr_map.keys()), getting every address derived from the wallet's descriptors. Change outputs that had already been spent (only appearing as "send" inputs) were not recognized as "ours", so CHANGE_DETECTION silently skipped those transactions. So I added a derived_addresses field to WalletHistory, populated by deriving all addresses (external + internal chains) in both scan_wallet and scan_descriptors. from_wallet_history then seeds our_addrs from that full set. Also update scan_wallet.rs to call known_wallet_txids for the risky and exchange wallets, mirroring the Python implementation.

However, reproducing in my machine I was able to get only 38 detections from reproduction in main branch versus your mentioned 39, suggesting we may be dealing with a different regtest state. I generated mine directly from given command.

Command used in main and in my branch (using the exact same regtest):

renato@renato:~/Desktop/bitcoin/stealth/backend/script$ cargo run -p stealth-app --example scan_wallet 2>/dev/null | python3 -c "
import json, sys; r = json.load(sys.stdin)
from collections import Counter
types = Counter(f['type'] for f in r['findings'])
for t, c in sorted(types.items()): print(f'  {t}: {c}')
print(f'  TOTAL: {sum(types.values())}')
"
  ADDRESS_REUSE: 1
  BEHAVIORAL_FINGERPRINT: 1
  CHANGE_DETECTION: 7
  CIOH: 11
  CLUSTER_MERGE: 11
  CONSOLIDATION: 1
  DUST: 2
  DUST_SPENDING: 2
  EXCHANGE_ORIGIN: 1
  SCRIPT_TYPE_MIXING: 1
  TOTAL: 38

@satsfy satsfy requested a review from LORDBABUINO March 31, 2026 22:34
satsfy added 3 commits March 31, 2026 19:40
…nterfaces

Tighten the derived_addresses doc comment to describe the field's purpose without referencing the Python implementation details.
Trim the scan_wallet comment so it only states the Rust-side behavior: derive every descriptor address, without the extra cross-reference to the Python path.
…hestration

Shorten the TxGraph comment to describe seeding ownership from derived addresses without the removed Python-reference explanation.
@satsfy
Copy link
Copy Markdown
Collaborator Author

satsfy commented Mar 31, 2026

These last 3 commits just remove some irrelevant comments accidentally left.

@LORDBABUINO LORDBABUINO merged commit cf07b94 into stealth-bitcoin:main Apr 4, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants