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.
Summary
A transaction with 2 or more non-coinbase inputs that all reference the same address (input-side address reuse) is currently classified by
analyzeCiohash3-single-inputwithseverity: "good"andscoreImpact: 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_HEURISTICSruns when a txid is pastedADDRESS_HEURISTICSruns when an address is pastedanalyzeAddressReuse(which would catch this with the right severity) lives inADDRESS_HEURISTICSonly. 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:h3-single-input/ good / impact 0h3-single-input/ good / impact 0The existing test
deduplicates same address across inputsexplicitly asserts the second case returnsh3-single-inputwith impact 0, so the bug is currently load-bearing on the suite.Reproduction
End-to-end against the real
analyzeTransactionpipeline (the full TX_HEURISTICS set), using fixtures fromtx-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
highrather thancritical. 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[].txidvalues:uniqueParentTxids.size > 1is real reuse,=== 1is a batch receive.Proposed fix sketch
In
analyzeCioh, whenuniqueInputAddresses.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-inputfinding stays as-is for the genuine 1-input case.Plus:
deduplicates same address across inputsso it now asserts the new behavior, and add a separate test for the genuine single-input casecross-heuristic/- a CoinJoin coordinator output should still suppress the new finding the same way it suppresses other CIOH variantsWhy this is filed as an issue, not a PR
The fix involves editorial choices that belong to the maintainer:
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.