Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
db3234a
fix(bitcoin-only): enforce Vault API boundary
BitHighlander Aug 23, 2026
2f00224
test(bitcoin-only): make boundary checks future-safe
BitHighlander Aug 23, 2026
e391f8b
fix(bitcoin-only): enforce host boundary end to end
BitHighlander Aug 23, 2026
7efe519
fix(bitcoin-only): fence stale host activity
BitHighlander Aug 23, 2026
dbf5ee2
fix(bitcoin-only): fail closed across host backends
BitHighlander Aug 24, 2026
eae9cff
fix(bitcoin-only): fence stale host accounting state
BitHighlander Aug 24, 2026
496cb7b
docs(alpha): refresh bitcoin-only test target
BitHighlander Aug 24, 2026
f407a3e
test(alpha): cover legacy bitcoin-only signing
BitHighlander Aug 24, 2026
126d5df
fix(bitcoin-only): close stale host boundary gaps
BitHighlander Aug 24, 2026
ddf09c1
fix(bitcoin-only): close direct data API bypasses
BitHighlander Aug 24, 2026
577a123
fix(bitcoin-only): close multichain agent bridge
BitHighlander Aug 24, 2026
4a8c931
test(alpha): exercise direct bitcoin-only host fences
BitHighlander Aug 24, 2026
bcf83e5
fix(bitcoin-only): fence binance route family
BitHighlander Aug 24, 2026
03a999c
fix(bitcoin-only): validate generic xpub requests
BitHighlander Aug 24, 2026
a6b5ae9
fix(bitcoin-only): revalidate queued approvals
BitHighlander Aug 24, 2026
179db09
fix(bitcoin-only): validate batch xpub networks
BitHighlander Aug 24, 2026
09ad1cd
docs(alpha): finalize bitcoin-only test handoff
BitHighlander Aug 24, 2026
dd50d7f
ci(bitcoin-only): execute host policy gate
BitHighlander Aug 24, 2026
41c0702
docs(alpha): clean bitcoin-only handoff format
BitHighlander Aug 24, 2026
f3f1846
fix(alpha): harden Pioneer bitcoin boundary
BitHighlander Aug 24, 2026
788589d
fix(bitcoin-only): use bitcoinOnlyActivityList for getRecentActivity …
BitHighlander Aug 26, 2026
69b19f0
Merge develop (certified EVM/Solana ClearSign) into fix/alpha-bitcoin…
BitHighlander Aug 26, 2026
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
19 changes: 19 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,25 @@ jobs:
run: cd projects/keepkey-vault && bun install --frozen-lockfile
shell: bash

- name: Test Bitcoin-only host boundary (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
cd projects/keepkey-vault
bun test \
src/bun/bitcoin-only-boundary.test.ts \
src/bun/perf-telemetry-offline.test.ts \
src/bun/txbuilder/utxo-selfhost-policy.test.ts \
src/bun/txbuilder/utxo-taproot.test.ts \
__tests__/taproot-host.test.ts
bun src/bun/btc-backend/address-discovery.test.ts
bun src/bun/btc-backend/core.test.ts
bun src/bun/btc-backend/normalize.test.ts
bun src/bun/btc-backend/device-only.test.ts
bun src/bun/offline-policy.test.ts
bun src/bun/pioneer-guard.test.ts
node --check ../keepkey-sdk/tests/alpha/bitcoin-only-hardware.js

- name: Install protoc (macOS)
if: runner.os == 'macOS'
run: brew install protobuf
Expand Down
141 changes: 141 additions & 0 deletions docs/handoff-pioneer-alpha-bitcoin-only.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Handoff: Pioneer requirements for alpha Bitcoin-only testing

Date: 2026-08-23

From: Vault BTC-only audit

To: Pioneer server/client owner

Vault PR: #425 (`fix/alpha-bitcoin-only-api-boundary`)

Pioneer source inspected: `release/v1.3.155` at `2b76a1ac6`

Do not add a `bitcoinOnly` mode to Pioneer. Firmware identity and feature
restriction belong to Vault. In Pioneer mode, the device makes ordinary Bitcoin
mainnet or testnet requests. In self-hosted mode, Vault bypasses Pioneer for
mainnet and fails closed on testnet; offline mode blocks every network. Its
runtime guard rejects accidental BTC mainnet and testnet calls.

Pioneer changes are still required before the whole advertised Bitcoin-only
surface can be called ready.

## Live contract evidence

These unauthenticated probes were run against the deployed service on
2026-08-23. They contain no wallet data:

| Probe | Production result | Why it matters |
|---|---|---|
| mainnet `fee-rate` | HTTP 200, numeric `fastest/fast/average`, no unit | mainnet works, but the unit is implicit |
| testnet `fee-rate` | HTTP 400 `Unsupported UTXO networkId` | advertised Bitcoin testnet is not implemented |
| malformed xpub `ListUnspent` | HTTP 200 `[]` | invalid input is indistinguishable from an empty wallet |
| malformed xpub `GetPubkeyInfo` | HTTP 200 `{success:false}` | application failure is hidden behind HTTP success |
| malformed txid `LookupUtxoTx` | HTTP 200 `{success:false}` | not-found/error transport semantics are ambiguous |

Mainnet and testnet fee probes produced the same results on
`api-blue.keepkey.info`. Preserve equivalent probes as deployment smoke tests.

## P0: support Bitcoin testnet consistently

Vault and Bitcoin-only firmware both expose Bitcoin testnet:

- network: `bip122:000000000933ea01ad0ee984209779ba`
- asset: `bip122:000000000933ea01ad0ee984209779ba/slip44:1`
- extended keys: `tpub` (and script-specific testnet forms where supported)

Pioneer's `UTXO_NETWORKS` and broadcast network map currently contain Bitcoin
mainnet but not testnet. Add one canonical testnet mapping and use it for:

- `ListUnspent`
- `GetPubkeyInfo`
- `GetFeeRate` / `GetFeeRateByNetwork`
- `LookupUtxoTx`
- `Broadcast`
- portfolio balances and transaction history for the testnet asset CAIP

If there is no production testnet indexer, return an explicit unsupported or
service-unavailable error. Do not return an empty wallet.

## P0: failures must not masquerade as valid empty data

The UTXO, fee, pubkey-info, lookup, and broadcast controllers commonly catch an
upstream exception and return HTTP 200 with `{success:false,error}`. Cold history
can also end its 30-second wait with `success:true`, `transactions:[]`, and
`loading:true`.

Use transport status and a stable error body:

- 400 for malformed or unsupported network/xpub/txid/hex;
- 404 only for a transaction that is genuinely absent;
- 502/503 for indexer, node, queue, or Redis failure;
- 202 with `loading:true` is acceptable for asynchronous cold history, but 200
with an empty transaction array must mean a completed, authoritative empty
result.

This distinction is safety-relevant: an empty UTXO set hides spendable funds;
missing address tokens can make a host reuse index 0; synthetic fallback fees
can materially alter a transaction.

Vault PR #425 now also fails closed on Pioneer `{success:false}`, malformed
UTXO responses, missing fee rates, and missing address-token data. That protects
the current client but does not replace a correct server contract.

## P0: freeze the BTC money-path response contract

Return these fields consistently through the generated Pioneer client:

```text
ListUnspent -> [{ txid, vout, value, path, address?, hex? }]
GetPubkeyInfo -> { tokens: [{ path, transfers, name? }], ... }
GetFeeRate -> { slow, average, fast|fastest, unit: "sat/vB" }
LookupUtxoTx -> { success: true, data: { txid, hex, ... } }
Broadcast -> { success: true, txid }
```

`value` must be an integer number of satoshis. `path` must be the full BIP32
path belonging to the UTXO. Legacy P2PKH signing requires the complete raw
previous transaction hex, not just its txid or selected output.

The generated operation is `LookupUtxoTx`; do not rename it silently. Vault
PR #425 accepts both that name and the older `UtxoLookup` alias during migration
and now unwraps the server's nested lookup response.

Fee units must be explicit. Vault still recognizes the legacy sat/kB shape by
magnitude for compatibility, but a heuristic is not an acceptable permanent
money-path contract.

## P1: history and discovery completion semantics

For mainnet and testnet xpub/zpub/ypub/tpub account queries:

- a cold cache must enqueue and await the indexer or return explicit `loading`;
- `forceRefresh=true` must not silently degrade to a completed empty history;
- pagination must preserve `caip`, `pubkey`, `page`, and completion state;
- `GetPubkeyInfo.tokens` must include every used receive/change address with
its exact path and positive transfer count;
- an upstream response with transactions but no token/path detail is an error,
not change index 0.

## Acceptance gate

Run against the exact alpha Pioneer candidate, not only mocks:

1. Mainnet and testnet CAIP forms reach the intended node/indexer for every
listed operation; unknown `bip122:*` returns non-2xx.
2. A funded BIP44, BIP49, BIP84, and BIP86 account returns exact integer-satoshi
UTXOs and full derivation paths.
3. A legacy UTXO returns complete raw previous-transaction hex and the txid of
that hex matches the requested txid.
4. Fee response declares `sat/vB`; `slow <= average <= fast/fastest`; no default
success response is emitted when every estimator is unavailable.
5. Cold and forced history for a known-used key return transactions or explicit
in-progress/error state, never authoritative empty success.
6. Missing Blockbook token detail and simulated Redis/indexer/node outages are
visible failures.
7. Broadcast a disposable testnet transaction, assert the returned txid, then
repeat it and define the idempotent/already-known behavior.
8. Capture status code and JSON for every case as the handback evidence.

No Pioneer deployment is required for the first alpha physical pass if testing
only Bitcoin mainnet on a healthy Pioneer backend. Testnet and degraded-backend
claims remain blocked until this gate passes.
199 changes: 199 additions & 0 deletions docs/handoff-testing/09-alpha-bitcoin-only.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# Alpha Bitcoin-only acceptance

Owner: BTC-only test lead
Target: `alpha`, after firmware PR #534 (including merged #535 and its follow-up fixes) is included

Safety: the automated runner never wipes, resets, recovers, loads, changes settings, or broadcasts

## Release blocker found before device testing

The physical Bitcoin-only artifact at firmware head `b67e53547` did not contain
`KeepKeyBTC`. Physical firmware returned the ordinary `KeepKey` variant, while
Vault's Bitcoin-only boundary deliberately recognizes only `KeepKeyBTC` and
`EmulatorBTC`.

Consequences on physical hardware:

- Vault would render the multi-chain UI.
- Add Chain, ShapeShift, and WalletConnect would remain reachable.
- Vault's non-Bitcoin REST fence would not run, allowing background altcoin
probes to reach a firmware that cannot service them.
- Flash-time inspection could not distinguish the Bitcoin-only image.

Firmware PR #534 now carries #535's physical-identity fix and adds a CI
assertion over the actual flashable ARM artifact. Do not spend physical-device
time on an artifact that predates #534's final green head, has a red Bitcoin-only
ARM job, or does not
embed `KeepKeyBTC\0`.

## Candidate preparation

Download the exact `bitcoin-only` artifact from the CI run for the candidate
commit. Record its SHA-256; do not use a filename or branch name as identity.
Start the alpha Vault build and connect an initialized, test-safe device flashed
with that artifact.

Run from `projects/keepkey-sdk`:

```sh
export BTC_ALPHA_HARDWARE_TEST=1
export BTC_ALPHA_ARTIFACT=/absolute/path/to/firmware.keepkey.bin
export BTC_ALPHA_EXPECT_FIRMWARE_HASH=<64-char-sha256>
export BTC_ALPHA_EXPECT_VERSION=7.16.0
export BTC_ALPHA_EVIDENCE_FILE=/absolute/path/to/evidence/bitcoin-only-all.json
export KEEPKEY_URL=http://localhost:1646
node tests/alpha/bitcoin-only-hardware.js all
```

Set `KEEPKEY_API_KEY` when reusing an existing pairing. Otherwise the SDK may
start a pairing approval.

The runner fails before wallet testing unless all three identities agree:

1. the supplied file hashes to the expected candidate hash;
2. the file embeds `KeepKeyBTC\0`;
3. the connected device reports the same hash and `firmware_variant` exactly
`KeepKeyBTC`.

## What the runner proves

- Every non-Bitcoin address and signing endpoint returns HTTP 501 at Vault's
REST boundary, including altcoin names passed through generic UTXO/xpub
routes; the advertised coin list contains only Bitcoin networks.
- BIP44/P2PKH, BIP49/P2SH-P2WPKH, BIP84/P2WPKH, and BIP86/P2TR derive with the
correct mainnet encoding.
- Each of those four addresses is shown on the physical OLED, compared
character-for-character and by QR, and matches the non-display derivation.
- Legacy, nested SegWit, native SegWit, and Taproot sign offline synthetic
transactions. The legacy case supplies a complete, structurally valid
nonexistent previous transaction whose output belongs to the derived BIP44
address.
- Vault and the device independently display the expected destination, amount,
and fee.
- Device rejection propagates as a rejected SDK promise.
- The app presents the Bitcoin-only splash/navigation/portfolio/settings and
stays restricted across disconnect/reconnect.

The Vault candidate also has executable host-boundary coverage beyond REST:

- every privileged renderer RPC is wrapped before dispatch; chain-specific
prefixes, non-Bitcoin `chainId` values, generic UTXO coin names, xpub batches,
ClearSign, swaps, and WalletConnect pairing fail before handler side effects;
- portfolio, history, report, mobile-pairing, audit, watch-only, cached-balance,
and address-book reads are Bitcoin-scoped at their source;
- dynamic market/token requests and stale full-firmware ledger/report records
are fenced while the Bitcoin-only device is connected;
- a full-firmware → Bitcoin-only transition resets in-memory account managers
and removes persisted non-Bitcoin balances and xpubs;
- existing WalletConnect sessions are destroyed, queued deep links discarded,
and each device-signing callback independently re-checks the live firmware
variant while teardown is in progress;
- Zcash/Hive capability startup is disabled for Bitcoin-only firmware even when
its semantic version would otherwise enable those services.
- authenticated direct REST reads cannot recover stale altcoin activity by ID,
swap history/discovery, token visibility, or raw debug portfolio state;
- the MCP/BEX agent bridge is closed because its extension-owned tool catalog
and historical multichain results cannot be sanitized by Vault;
- generic Pioneer v2 routes accept only Bitcoin mainnet/testnet CAIPs and
network IDs, while multichain catalogs, account-state queries, and staking
are rejected before network access.
- a signing request queued under full firmware is revalidated against the live
device at approval time, so swapping to Bitcoin-only firmware cannot carry an
already-open Litecoin or altcoin approval across the transition;
- batch xpub requests validate both the coin and every declared network, so a
Bitcoin xpub cannot be returned under an Ethereum or other altcoin label.

The synthetic prevouts do not exist, so signed transactions cannot be
broadcast. Evidence is written with mode `0600`, including the exact artifact,
firmware identity, addresses, serialized-transaction hashes, and operator
attestations. It contains no seed, private key, PIN, or passphrase.

## Manual in-app matrix still required

The runner exercises the production SDK/REST/hdwallet/device path, but it does
not prove the complete graphical send flow or backends. Use a funded disposable
test wallet and make an explicit spending decision before these cases:

| Mode | Receive/discovery | Build and review | Broadcast | Required observation |
|---|---|---|---|---|
| Pioneer | BIP44/49/84/86 accounts | Small P2WPKH and P2TR sends | One minimal-value transaction | Correct UTXO selection, change path, amount, fee, txid, and post-confirm balance |
| Self-hosted Blockbook | Same four accounts; next-unused indexes from Blockbook | Small P2WPKH and P2TR sends | One minimal-value transaction | No Pioneer chain-data fallback; Blockbook history and post-confirm balance agree |
| Self-hosted Bitcoin Core | Same four accounts; manually select/verify receive index | No-change/send-max build only | One minimal-value no-change transaction, if deliberately funded for it | No Pioneer chain-data fallback; Core UTXOs, fees, txid, and post-confirm balance agree; history and automatic change discovery are explicitly unavailable |
| Offline | Cached account plus manually selected/verified receive index | Device address derivation and raw/synthetic signing only | Build, history, sweep, swap, report, and broadcast must reject | No outbound sockets after airplane mode is enabled; cached balances remain readable and device address/signing still work |

Pioneer server/client follow-up is tracked in
[`../handoff-pioneer-alpha-bitcoin-only.md`](../handoff-pioneer-alpha-bitcoin-only.md).
Mainnet Pioneer testing may proceed, but do not claim Pioneer-backed testnet or
degraded-backend correctness until that handoff's acceptance gate passes.

Bitcoin Core's `scantxoutset` sees only the current UTXO set. It cannot prove
that an empty address was previously spent from, so it cannot safely choose the
next unused receive index or reconstruct transaction history. Vault must show
that limitation and must not consult Pioneer behind the node. Full self-hosted
history/index discovery requires Blockbook (or future descriptor-wallet import
and rescan support); a silent index-0 default is a test failure. A normal Core
send that would create change must fail before device signing rather than reuse
a guessed change address.

Also test RBF on a disposable transaction, high-fee rejection/warning, dust
handling, insufficient funds, cancellation at the Vault gate, cancellation on
the device, PIN-locked reconnect, passphrase account separation, and unplugging
during address display and signing.

The legacy P2PKH case must keep supplying the complete previous transaction.
Reducing it to only a txid/amount would bypass the legacy streaming path and is
not acceptable evidence, even if a future host shim made that request appear to
pass.

### Hot-transition regression (required once)

This catches state that a cold-start test cannot see:

1. On the same disposable device and seed, boot full firmware, load the
portfolio, save one non-Bitcoin address-book row, and establish a disposable
WalletConnect session.
2. Flash the exact Bitcoin-only candidate without restarting Vault.
3. Confirm Vault returns to the Bitcoin tab, closes WalletConnect, and the dApp
session is disconnected rather than merely hidden.
4. Confirm no non-Bitcoin balance, xpub, activity, address-book row, swap,
ClearSign, Zcash, Hive, Add Chain, or WalletConnect control remains visible.
5. Leave Vault idle through two portfolio refresh intervals and confirm the
device receives no altcoin `Unknown message` probes.
6. Disconnect/reconnect and repeat the visibility and idle checks.
7. Return to full firmware and confirm full-firmware features return normally;
this proves the restriction follows device identity rather than permanently
corrupting the app's global preferences.

Record screenshots of the full-firmware before state and Bitcoin-only after
state, plus the dApp-side WalletConnect disconnect. Never use a production seed.

## Host verification already completed

- Bitcoin-only policy: 26 tests / 197 assertions, including
activity/ledger/report filtering,
watch-only snapshot authority, address-book history filtering, and the
renderer/REST/Pioneer dispatch boundaries, queued-approval revalidation, and
batch-xpub network validation.
- Focused host matrix: 33 tests / 213 assertions.
- The Linux packaging workflow now runs the broader executable gate: 41 Bun
tests / 232 expectations plus 87 standalone backend/policy assertions and a
syntax check of the physical runner. Packaging green is no longer the only
automated Vault signal.
- Bitcoin backend: Core 35 assertions, normalization 16, device-only 5.
- Taproot builder: P2TR input/change and BIP86 account-path selection, 3 cases.
- hdwallet Taproot: 8 tests, 28 assertions covering protocol enums, BIP86
display/xpub requests, capability gating, and Schnorr input requirements.
- production Bun backend bundle and Vite renderer build both complete.

## Merge gate

Bitcoin-only alpha is not cleared until:

- the identity-fix CI matrix is green for its exact head;
- the Vault REST-boundary fix and its complete signing-route matrix test are
present;
- the downloaded ARM artifact passes its CI identity assertion;
- `all` completes on physical hardware with preserved evidence;
- the in-app Pioneer, self-hosted, and offline matrix is recorded;
- a full firmware artifact on the same source head proves `KeepKeyBTC\0` is
absent, preventing accidental Bitcoin-only branding of the normal build.
Loading
Loading