Skip to content

CIOH labels multi-input from a single reused address as 'good', missing the address-reuse signal on the tx path #92

Description

@PeterXMR

Summary

A transaction with 2 or more non-coinbase inputs that all reference the same address (input-side address reuse) is currently classified by analyzeCioh as h3-single-input with severity: "good" and scoreImpact: 0. No other tx-level heuristic detects the reuse on the input side, so the user sees a green finding that affirms a pattern the project's own docs call "the single most damaging privacy practice in Bitcoin".

Root cause

The pipeline splits heuristics into two sets in heuristic-registry.ts:

  • TX_HEURISTICS runs when a txid is pasted
  • ADDRESS_HEURISTICS runs when an address is pasted

analyzeAddressReuse (which would catch this with the right severity) lives in ADDRESS_HEURISTICS only. On the txid path, the only heuristic that examines input-address identity is CIOH, and CIOH's early-return at line 41 collapses two semantically very different cases into one classification:

Inputs Unique input addresses Reality Tool says
1 1 Genuinely private (single-input tx) h3-single-input / good / impact 0
2+ from the same address 1 Catastrophic address reuse h3-single-input / good / impact 0

The existing test deduplicates same address across inputs explicitly asserts the second case returns h3-single-input with impact 0, so the bug is currently load-bearing on the suite.

Reproduction

End-to-end against the real analyzeTransaction pipeline (the full TX_HEURISTICS set), using fixtures from tx-factory.ts:

A) 2 inputs, same reused address, distinct parent txs

```
GRADE: C SCORE: 61/100
-6 [high] behavioral-fingerprint-rollup
-3 [medium] h5-zero-entropy-sweep
-3 [low] h11-wallet-fingerprint
+3 [good] h-coin-selection-bnb
0 [good] h3-single-input - Single input address <-- misleading
```

B) 3 inputs, same reused address, distinct parent txs

```
GRADE: C SCORE: 61/100
-6 [high] behavioral-fingerprint-rollup
-3 [medium] consolidation-fan-in <-- partial compensation via H-consolidation
-3 [low] h11-wallet-fingerprint
+3 [good] h-coin-selection-bnb
0 [good] h3-single-input <-- still misleading
```

C) 2 inputs, distinct addresses (baseline CIOH, for comparison)

```
GRADE: C SCORE: 55/100
-6 [high] behavioral-fingerprint-rollup
-6 [medium] h3-cioh - 2 input addresses clustered via CIOH
-3 [medium] h5-zero-entropy-sweep
-3 [low] h11-wallet-fingerprint
+3 [good] h-coin-selection-bnb
```

Impact

Numeric: the reused-address scenarios receive a ~6 point lower penalty than the equivalent distinct-address scenario, but both stay within the same letter-grade tier (C). This is not an A+ vs F flip - other heuristics (entropy, behavioral-fingerprint-rollup, consolidation when input count >= 3) absorb part of the signal.

User-facing: a finding marked severity: "good" with the text "Single input address ... No address clustering is possible from inputs alone" sits in the results list for a transaction that has just publicly linked multiple historical receives. The green signal on the exact pattern the README names catastrophic is the actual user-visible defect.

Why the bug type matters more than the score swing

False-positives on the positive side are the worst category for privacy / safety tools. A wrong red warning gets ignored; a wrong green check gets trusted. The score impact is modest, but the affirmation of a privacy-bad pattern as good is hard to defend even on the C-grade result page.

Severity

Tentatively classified as high rather than critical. It does not flip grade tiers, but it misleads users on the exact behavior the project's docs single out as the most damaging in Bitcoin.

Edge case the fix has to respect

Not every "all inputs share an address" tx is genuine reuse. Counter-example: an exchange withdraws to one of the user's addresses with two outputs in a single tx; the user later spends both. Inputs share an address, but all inputs trace to the same parent txid, and the address was funded only once - this is a batch receive, not reuse. The existing address-reuse heuristic already distinguishes these two cases at address-reuse.ts:85:

```ts
// Batch payment edge case: an exchange may send multiple outputs to the
// same address in a single transaction (funded_txo_count > 1 but tx_count <= 1).
// This is not true address reuse since only one transaction is involved.
```

A tx-side detector can mirror the test by counting distinct vin[].txid values: uniqueParentTxids.size > 1 is real reuse, === 1 is a batch receive.

Proposed fix sketch

In analyzeCioh, when uniqueInputAddresses.size === 1 && nonCoinbaseCount > 1:

```ts
const uniqueParentTxids = new Set(
tx.vin.filter((v) => !v.is_coinbase).map((v) => v.txid),
);

if (uniqueParentTxids.size > 1) {
// Real input-side address reuse
return { findings: [{
id: "h3-input-reuse",
severity: "critical" | "high" | "medium", // tiered by input count
scoreImpact: -X, // editorial call; suggest -25 to -45 to match scale with H8
...
}]};
}
// All inputs trace to one parent tx: legitimate batch receive
return { findings: [{
id: "h3-batch-receive-spend",
severity: "low",
scoreImpact: 0,
...
}]};
```

The existing h3-single-input finding stays as-is for the genuine 1-input case.

Plus:

  • i18n keys for the new finding IDs across en / es / de / fr / pt / pl
  • Update the existing test deduplicates same address across inputs so it now asserts the new behavior, and add a separate test for the genuine single-input case
  • Confirm interaction with the cross-heuristic CoinJoin suppression rules in cross-heuristic/ - a CoinJoin coordinator output should still suppress the new finding the same way it suppresses other CIOH variants

Why this is filed as an issue, not a PR

The fix involves editorial choices that belong to the maintainer:

  • The exact impact value (suggested -25 to -45 for real reuse, but the existing scoring model is yours)
  • User-facing finding text in six locales
  • Whether the dual case should be a new heuristic or a refactor of CIOH

Happy to follow up with a PR once the shape of the answer is settled. Reproduction is small enough that a maintainer can verify it independently in a few minutes.

Context for severity discussion

This issue surfaced during a manual code review pass following the recent batch of small fixes (#88, #89, #90, #91). Initial framing was more alarmist than the live numbers support - the report above reflects the corrected impact after running the actual pipeline.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions