# Initial Crypto Audit Findings

Date: 2026-06-30

Status: preliminary findings from the first proof-oriented audit batch. These are based on code inspection plus newly added tests, including a curated subset of official BIP-352 vectors. Some originally observed Silent Payment compatibility mismatches were traced to address/parity handling bugs and are now fixed in the current working tree.

Related tests:

- [tests/test_signer.py](/Users/juthicamallela/bitcoinarmory/tests/test_signer.py:1)
- [tests/test_descriptor_and_psbt_metadata.py](/Users/juthicamallela/bitcoinarmory/tests/test_descriptor_and_psbt_metadata.py:1)
- [tests/test_silent_payment_official_vectors.py](/Users/juthicamallela/bitcoinarmory/tests/test_silent_payment_official_vectors.py:1)

## 1. Standard BIP86 signing now has direct coverage

Result:

1. a new end-to-end BIP86 signing test passes
2. descriptor-origin snapshots for BIP84 and BIP86 pass
3. PSBT change-output derivation metadata tests pass

Interpretation:

This does not prove the entire Taproot surface is correct, but it materially reduces uncertainty around the standard BIP86 receive and sign path.

## 2. Silent Payment compatibility has improved, but coverage is still partial

Result:

1. the curated official BIP-352 subset now passes in the current working tree
2. the checked-in audit fixture now covers all 28 official-vector cases, including input ordering, same-transaction inputs, byte-lexicographic outpoint selection, repeated inputs from the same pubkey, Taproot-only and mixed Taproot/non-Taproot inputs, repeated outputs, multiple recipients, labeled address recovery, unrelated-output ignoring, change-label recovery, malformed legacy input extraction, invalid-input skipping, intermediate key-sum edge behavior, and `K_max` enforcement
3. some sender-side vectors are still validated through higher-level eligibility/extraction checks rather than a single end-to-end sender helper

Interpretation:

This materially increases confidence in the Silent Payment implementation, but does not complete the audit. Silent Payments should still be treated as under active audit until backward-compatibility and sender-side policy boundaries are reviewed end-to-end.

## 3. Root cause found and fixed: BIP-352 address codec dropped pubkey parity

Evidence:

- official-vector mismatches originally appeared in cases involving odd-parity scan or spend pubkeys
- current code now passes the curated official subset after fixing the codec and parity handling

Observed behavior:

The original implementation encoded Silent Payment addresses as 64 bytes of x-only scan/spend keys. BIP-352 actually uses 66 bytes of compressed SEC1 pubkeys. Dropping the prefix bit lost key parity, which changed sender output derivation and receiver scanning in cases where parity mattered.

Likely impact:

1. before the fix, sender and receiver could disagree with BIP-352 for some addresses
2. outputs to recipients with odd-parity scan or spend pubkeys could be derived incorrectly
3. this was a real fund-loss risk for Silent Payments

Fixed areas:

- [core/silent_payment.py](/Users/juthicamallela/bitcoinarmory/core/silent_payment.py:84)
- [core/silent_payment.py](/Users/juthicamallela/bitcoinarmory/core/silent_payment.py:223)
- [core/silent_payment.py](/Users/juthicamallela/bitcoinarmory/core/silent_payment.py:264)
- [offline/signer.py](/Users/juthicamallela/bitcoinarmory/offline/signer.py:160)

## 4. Root cause found and fixed: receive-side parity handling still assumed x-only scan/spend keys

Evidence:

- internal SP round-trip tests failed immediately after the address-codec fix until receiver scanning logic was updated
- current code now passes those tests after parity handling was corrected

Observed behavior:

The receiver-side scan logic still negated `b_scan` and lifted spend keys as even-Y points as if the address only carried x-only keys. Once the sender began using full compressed keys, that behavior produced mismatched shared-secret serialization.

Likely impact:

1. sender output resolution and receiver scanning would diverge for some valid BIP-352 addresses
2. funds could be missed during scan or derived to the wrong destination on send

Fixed area:

- [core/silent_payment.py](/Users/juthicamallela/bitcoinarmory/core/silent_payment.py:264)

## 5. Multiple-output and labeled-output sender semantics are now aligned with the official subset

Evidence:

- repeated-output, labeled-output, and multiple-recipient cases in the curated subset now pass against the official vectors

Observed behavior:

For transactions with multiple outputs sharing the same scan key, the protocol outcome depends on consuming a single `k` sequence per scan key, including across labeled and unlabeled addresses. The official vectors allow multiple valid sender output sets because output ordering is not normative. The current implementation now matches that behavior.

Likely impact:

1. no remaining blocking mismatch is currently shown in the curated subset
2. sender-side test expectations now reflect the official vector semantics instead of forcing one arbitrary ordering
3. output-order semantics should still be checked when the full official vector set is integrated

Remaining work:

- keep the official-vector coverage aligned with the exact boundary where sender policy is enforced
- review whether sender-side input-eligibility checks should be represented by a dedicated helper instead of being distributed across callers and tests

## 6. Receiver-side input extraction and skip rules were materially incomplete and are now hardened

Evidence:

- the scanner now passes official-vector cases covering:
  taproot NUMS-point skipping,
  malleated P2PKH scriptSig extraction,
  uncompressed key skipping,
  valid nested P2SH-P2WPKH acceptance,
  invalid P2SH skipping,
  and point-at-infinity receiver skip behavior

Observed behavior:

The original scanner logic only handled a narrow set of witness shapes and aborted the whole transaction when it encountered unsupported inputs. It also did not parse serialized witness stacks from the official vectors, did not extract compressed pubkeys from legacy P2PKH `scriptSig`, and did not distinguish valid nested P2SH-P2WPKH from invalid P2SH cases.

Likely impact:

1. the wallet could miss valid Silent Payment outputs in third-party transactions containing a mix of supported and unsupported inputs
2. malformed or unsupported inputs could cause whole-transaction false negatives instead of being skipped per the BIP intent
3. the receiver path could raise on point-at-infinity input sums instead of safely skipping the transaction

Fixed areas:

- [online/scanner.py](/Users/juthicamallela/bitcoinarmory/online/scanner.py:1)
- [core/silent_payment.py](/Users/juthicamallela/bitcoinarmory/core/silent_payment.py:320)
- [tests/test_scanner.py](/Users/juthicamallela/bitcoinarmory/tests/test_scanner.py:1)

## 7. Legacy draft Silent Payment address strings appear migration-safe, but should remain compatibility-only

Evidence:

1. wallet persistence stores `sp_scan_pubkey`, `sp_spend_pubkey`, and related SP key material, not a rendered SP address string
2. watch-only export/import also preserves SP key material directly rather than persisting a previously rendered SP address
3. direct regression tests now cover:
   - decoding a legacy 64-byte draft SP address and re-encoding it canonically
   - creating a PSBT with a legacy SP destination string
   - offline signer resolution of a legacy SP destination string into the real Taproot output

Observed behavior:

The current decoder accepts the legacy 64-byte x-only draft address format and lifts both keys to compressed even-Y pubkeys for compatibility. Any wallet loaded from persisted key material now re-emits the canonical 66-byte BIP-352 address format from those keys.

Likely impact:

1. existing wallet files do not appear to carry a direct coin-loss migration risk from the address-format fix, because they store SP keys rather than the old rendered address string
2. previously shared or printed legacy SP address strings should still be treated as compatibility inputs only, not as canonical output
3. user-visible migration guidance may still be warranted if old SP addresses were ever displayed or exported outside wallet files

Covered areas:

- [core/wallet.py](/Users/juthicamallela/bitcoinarmory/core/wallet.py:155)
- [core/silent_payment.py](/Users/juthicamallela/bitcoinarmory/core/silent_payment.py:102)
- [offline/signer.py](/Users/juthicamallela/bitcoinarmory/offline/signer.py:89)
- [online/setup.py](/Users/juthicamallela/bitcoinarmory/online/setup.py:2137)
- [tests/test_silent_payment.py](/Users/juthicamallela/bitcoinarmory/tests/test_silent_payment.py:1)
- [tests/test_signer.py](/Users/juthicamallela/bitcoinarmory/tests/test_signer.py:1)
- [tests/test_descriptor_and_psbt_metadata.py](/Users/juthicamallela/bitcoinarmory/tests/test_descriptor_and_psbt_metadata.py:1)

## 8. Remaining gaps after this pass

1. the low-level sender primitive `create_sp_output_pubkeys()` still assumes it is given already-filtered valid input keys; it is not itself a full implementation of the sender-side input-eligibility rules from the official vectors
2. the broader transaction-building path should still be reviewed for invariants beyond UTXO ownership and PSBT metadata stamping, such as future changes to coin selection or imported external inputs
3. if historical releases exposed legacy SP address strings to users, operator-facing migration guidance should still be written explicitly even though the current code paths are compatibility-safe

## 9. Online/offline sender boundary now has direct proof coverage

Evidence:

- Electrs-derived UTXOs are now re-filtered against wallet-owned addresses before PSBT coin selection
- checked-in tests cover:
  rejection of foreign UTXOs even if they appear in Electrs results,
  and mixed eligible input sets (`bip84`, `bip86`, detected SP UTXO) for Silent Payment destinations

Observed behavior:

The online builder stamps each eligible input with the metadata the offline signer needs to resolve or sign it:

1. `bip84` inputs carry standard BIP32 derivation
2. `bip86` inputs carry taproot derivation plus internal key
3. detected SP inputs carry the proprietary SP tweak

Likely impact:

1. reduces the chance that a polluted Electrs cache or buggy response could inject a foreign input into SP output derivation
2. gives the offline signer a deterministic resolution path for every input type currently admitted by the sender

Fixed / covered areas:

- [online/setup.py](/Users/juthicamallela/bitcoinarmory/online/setup.py:2023)
- [online/setup.py](/Users/juthicamallela/bitcoinarmory/online/setup.py:2101)
- [tests/test_descriptor_and_psbt_metadata.py](/Users/juthicamallela/bitcoinarmory/tests/test_descriptor_and_psbt_metadata.py:1)

## 10. Ownership and SP input-policy checks are now stricter

Evidence:

1. wallet UTXO filtering now requires both:
   - the advertised address to belong to the wallet
   - the advertised `script_pubkey` to exactly match the wallet's derived script for that address
2. Silent Payment sends now explicitly reject selected inputs whose script types are not `p2wpkh` or `p2tr`
3. selected inputs carrying `sp_tweak` metadata are now required to be Taproot outputs

Observed behavior:

The earlier sender path trusted the address field more than the script and relied on caller discipline for SP input eligibility. That was defensible for the current wallet model, but it was weaker than the audit target because a poisoned indexer response or future imported-input feature could blur the boundary between "wallet-owned" and "safe to use in SP derivation."

Likely impact:

1. reduces the risk that a mismatched address/script pair could be selected and signed
2. narrows the SP sender contract so unsupported input types fail early on the online side instead of relying on downstream assumptions
3. improves the "never derive from inputs we do not fully understand" invariant for SP sends

Covered areas:

- [online/setup.py](/Users/juthicamallela/bitcoinarmory/online/setup.py:2101)
- [online/setup.py](/Users/juthicamallela/bitcoinarmory/online/setup.py:4713)
- [tests/test_descriptor_and_psbt_metadata.py](/Users/juthicamallela/bitcoinarmory/tests/test_descriptor_and_psbt_metadata.py:1)

## 11. Fee-bump path now enforces the same spendability boundary

Evidence:

1. `create_rbf_psbt()` now rejects inputs unless they are either:
   - exact-script matches for wallet-owned addresses, or
   - known SP UTXOs with Taproot scripts
2. change detection in the RBF flow now uses wallet script ownership rather than trusting decoded address strings alone
3. direct regression tests cover:
   - rejecting a wallet-owned address paired with the wrong input script in the RBF path
   - rejecting a fake SP-tweak RBF input whose prevout is not Taproot

Observed behavior:

Before this pass, the fee-bump flow reconstructed spendability largely from decoded addresses and previous transaction data. That was weaker than the base send path because it did not apply the newer exact-script ownership rule or the SP-specific Taproot constraint.

Likely impact:

1. reduces the chance that RBF rebuilds a PSBT around an input the wallet should not sign
2. keeps the fee-bump path aligned with the primary send-path ownership and SP-input invariants

Covered areas:

- [online/setup.py](/Users/juthicamallela/bitcoinarmory/online/setup.py:2914)
- [tests/test_descriptor_and_psbt_metadata.py](/Users/juthicamallela/bitcoinarmory/tests/test_descriptor_and_psbt_metadata.py:1)

## 12. Spend-construction entry points appear fully enumerated in the current app

Evidence:

1. codebase sweep did not identify additional transaction-construction entry points beyond:
   - `create_psbt()`
   - `create_rbf_psbt()`
2. the remaining SP-related helper `recover_buggy_sp_output()` reconstructs historical buggy output ownership but does not build or sign a new spend transaction
3. broader online-app regression suites were updated to model the stricter address-plus-script ownership invariant and now pass

Observed behavior:

The current application appears to centralize spend construction in two online entry points. That is a good security property because the audited ownership and SP-input policy checks are not fragmented across multiple hidden flows.

Likely impact:

1. reduces concern that a current in-repo sweep/edit/send path silently bypasses the new ownership checks
2. narrows remaining audit work to future or externalized spend sources rather than undocumented current ones

Covered areas:

- [online/setup.py](/Users/juthicamallela/bitcoinarmory/online/setup.py:2073)
- [online/setup.py](/Users/juthicamallela/bitcoinarmory/online/setup.py:2914)
- [online/setup.py](/Users/juthicamallela/bitcoinarmory/online/setup.py:4940)
- [tests/test_online_app.py](/Users/juthicamallela/bitcoinarmory/tests/test_online_app.py:1)
- [tests/test_utxo_filter.py](/Users/juthicamallela/bitcoinarmory/tests/test_utxo_filter.py:1)

## 13. Immediate recommendation

Before Silent Payments are trusted with real funds, the next audit pass should focus on:

1. reviewing any non-wallet or future imported-input flows against the new exact-script ownership invariant
2. preserving these same ownership and SP-input policy checks if future sweep, transaction-edit, or imported-input features are added
3. writing explicit migration guidance if any released builds ever surfaced legacy SP address strings to operators or end users
