release: 7.14.3 — bitcoin-only variant with taproot - #461
Closed
BitHighlander wants to merge 8 commits into
Closed
Conversation
Everything 7.14.3 sits on top of: the 7.14.2 disclosure and EVM work, its release documentation, and the accompanying unit tests. Separated from the 7.14.3 commits below so the new work can be read on its own.
Schnorr signing, the taproot sighash and witness serialisation, and BIP-86 path validation in coins.c, with 18 BIP-340/341 vectors. Proven on hardware: a P2TR spend has been executed on a real device on MAINNET, so the sighash, signature and witness serialisation were all accepted by the network.
KK_BITCOIN_ONLY gates the non-Bitcoin coin families out of the image, and
release.yml builds `full` and `bitcoin-only` from one matrix.
Three collisions had to be fixed for two variants to coexist, each of which
would have shipped wrong artifacts from a GREEN job: ${SUFFIX} was referenced
but never assigned, so both variants wrote the same filename; both legs
uploaded the same artifact name, a hard failure on upload-artifact@v4+; and
both wrote HASHES.txt, so one manifest silently overwrote the other under
merge-multiple.
STORAGE_VERSION_LAST_SHIPPED records the highest version actually shipped, with a static assert so lowering STORAGE_VERSION below it fails the build instead of silently wiping every device that upgrades. A seed CREATED under bitcoin-only firmware is stamped 10000 + STORAGE_VERSION so multi-chain firmware refuses it -- without this a bitcoin-only seed was indistinguishable from a multi-chain one. Only creation paths stamp, so a pre-existing multi-chain wallet migrated under bitcoin-only firmware keeps its portable version. Multi-chain firmware REFUSES rather than wipes; bitcoin-only firmware recovers the underlying version and loads through the normal migration chain. The lock is enforced at CHECK_NOT_INITIALIZED, not only at the write. A locked device looks uninitialized (the RAM shadow was reset), so without this guard it would accept a full ResetDevice ceremony, write nothing, and report Success -- leaving a funded seed in RAM only.
BNB, CMT, GTO, IOST, MCO, ODEM and QTUM each migrated off Ethereum or died. The vetted source now gives three competing addresses for six of them and none at all for ODEM, so the generator refuses them -- it takes a symbol only when the source gives exactly one address, so a scam token cannot inherit a real one's label. The device was advertising seven tokens it cannot identify. Coins resolve by name, never by table index, and no index is persisted, so removing entries cannot shift the meaning of stored data.
device-protocol is pinned to the MINIMAL taproot change (Features.supports_taproot), not the full 7.15 proto branch: that one additionally carries structured EIP-712 and Solana LUT fields with no .options entries in this tree, so nanopb emits pb_callback_t and both the ARM and emulator builds fail. No source file here references any of them. Both pins are heads of open PRs into master and become plain master pins when those merge.
BitHighlander
force-pushed
the
release/7.14.3-bitcoin-only
branch
from
August 22, 2026 23:57
3c5f3ed to
66f78ec
Compare
Merged
BitHighlander
commented
Aug 23, 2026
BitHighlander
left a comment
Collaborator
Author
There was a problem hiding this comment.
Release-path review for the 7.14.3 head.
Backported from release/7.15, where the collection UI and the physical
short-press/long-hold classification are hardware-verified. Eight files
arrive byte-identical to that tree (dice_input.{c,h}, rng_health.{c,h},
rng.{c,h}, u2f.c, keepkey_flash.c) along with both unit-test suites; the
reset.c / fsm_msg_common.h / fsm_msg_debug.h integration is ported
surgically and diffs clean against 7.15 except for the one deliberate
divergence below.
ResetDevice.dice_entropy collects 50/75/99 rolls on the device (short press
advances 1-6/undo, 800 ms hold commits) and folds
SHA256(int_entropy || rolls) into the internal entropy BEFORE
EntropyRequest, so the host contribution arrives strictly after the device
has committed to its own. The completion screen shows the first 8 bytes of
SHA256(rolls) so the user can recompute it from their written rolls; the
same digest is served over DebugLink for test parity. docs/DiceEntropy.md
covers what the digest proves and what it deliberately does not.
Divergence from 7.15, on purpose: display_random is RETAINED, because
already-shipped 7.14-line hosts request the Internal Entropy screen and the
pinned python suite asserts it below 7.15.0. The two are mutually exclusive
by refusal -- ResetDevice with both flags fails with a SyntaxError, since
the screen shows the POST-mix value, which is the seed pre-image once
ext_entropy is known.
The seed-time RNG gate covers every call site on rng_health.h's documented
list, so the header's COMPLETE LIST claim is true in this tree: reset_init
(fail with FirmwareError), the storage encryption and wipe-code keys and
the V1-upgrade PIN-KDF salt (storage_drawKeyMaterial, halt), U2F key-handle
derivation (refuse registration), and the one-shot OTP randomness block
(skip the write, leave the block claimable by a healthy boot).
Pins: deps/device-protocol advances to 8545cd5b -- the exact pin
release/7.15 builds against; every bytes/string field new to the compiled
protos verified to carry nanopb sizing (the options files here were already
pre-staged for it, plus three Solana LUT lines). deps/python-keepkey
advances to 9aaaa84, which moves the dice end-to-end test, the ceremony
re-entry regression, and report Section K from a 7.15.0 gate to 7.14.3 --
exact for the fleet, since no firmware between 7.14.3 and 7.15.0 exists
without dice. Those tests previously SKIPPED against this branch and CI
went green with zero device-level dice coverage.
ci: build-arm-firmware compiles both release variants on every PR, so a
change that only breaks the bitcoin-only image can no longer go green.
Verified in CI-parity docker (same base image digest): bitcoin-only ARM
links at .text 372,928 and full at 523,600; firmware-unit 103/103 including
Dice.* (5) and RngHealth.* (15); the full python-integration compose suite
passes 449/449 selected with test_reset_device_dice and
test_reset_reentry_disarms_entropy_ack RUNNING and green; clang-format-20
clean on every touched file.
Collaborator
Author
|
Parking this work in the fork for later integration rather than merging it upstream now. Preserved as draft BitHighlander#604 at the same head SHA ( |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cuts 7.14.3: a bitcoin-only firmware variant, with taproot, built from the
same tree as the full image.
developis 7.14.1; this is 121 commits / +6,721 −893.What this delivers
A second build variant.
release.ymlnow buildsfullandbitcoin-onlyfrom one matrix. Porting that surfaced three collisions that would each have
shipped wrong artifacts from a green job:
${SUFFIX}was referenced but never assigned, so both variants wrote thesame filename and one clobbered the other
upload-artifact@v4+HASHES.txt, so one manifest silently overwrote the otherunder
merge-multiple, leaving a manifest describing one variant sittingbeside two binaries
Verified by enumerating what each leg produces: 6 distinct names, no collisions.
Taproot. BIP-340/341, 18/18 unit tests, and BIP-86 path validation in
coins.c. A P2TR spend has been executed on a real device, on mainnet — thestrongest evidence available, and stronger than screenshots: the network
accepted the sighash, signature and witness serialisation.
A storage band for bitcoin-only seeds. A seed CREATED under bitcoin-only
firmware is stamped
10000 + STORAGE_VERSION, so multi-chain firmware refusesit. Without this, a bitcoin-only seed was indistinguishable from a multi-chain
one and any multi-chain build would load it — the variant's whole point is that
a seed born under it is never exposed to multi-chain code.
Only creation paths stamp, so a pre-existing multi-chain wallet migrated under
bitcoin-only firmware keeps its portable version. Multi-chain firmware
refuses rather than wipes (flash untouched; reflashing bitcoin-only
recovers it), bitcoin-only firmware recovers the underlying version and loads
through the normal migration chain,
storage_commit()is gated on the lock,storage_wipe()is the only way out, and a static assert keepsSTORAGE_VERSIONbelow the band forever.A storage floor.
STORAGE_VERSION_LAST_SHIPPED(17) with a companionstatic assert, so lowering
STORAGE_VERSIONbelow a shipped version now failsthe build instead of silently wiping every device that upgrades.
Seven dead ERC-20s removed — BNB, CMT, GTO, IOST, MCO, ODEM, QTUM. Each
migrated off Ethereum or died; the vetted source now gives three competing
addresses for six and none for ODEM, so the token generator refuses them and
the device was advertising tokens it cannot identify. (Also filed standalone as
#459 against
master, since it stands on its own.)Submodule pins
Both are heads of open PRs into master, and become plain master pins when
those merge:
deps/device-protocol1958cddaFeatures.supports_taproot(3 lines)deps/python-keepkeyd58dc63fdevice-protocol is pinned to the minimal taproot PR, not the full 7.15
proto PR (#112). #112 additionally carries structured EIP-712 and Solana LUT
fields that have no
.optionsentries in this tree, so nanopb emitspb_callback_tand both the ARM and emulator builds fail. No source filehere references any of them.
Review notes, including what is NOT proven
The storage band is now verified — on hardware and on the emulator.
On a real K1-14AM running this exact bitcoin-only build
(
sha256 e9cfc7cc46a628ccc5fbac65e7b52a2d35ea8f793e7053e2887aec6d6e226c21):10017→ replug → still initialized, all three addresses identicalbc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcrfirmware_variant = KeepKeyBTC,supports_taproot = TrueThe power-cycle result is the one that mattered: it exercises the
#if BITCOIN_ONLYarm recoveringunderlying = 10017 - 10000 = 17, thearithmetic adapted by hand from alpha's V20 ladder. It had no coverage, and
every bitcoin-only user hits it on every boot.
On the emulator, both transition directions were verified against one
persistent flash image:
Use Wipe first.", and the flash image is byte-identical afterwards
(
109e2cdc…before and after). It refuses without wiping. This is exactlythe path whose absence produced the
Success("Device reset")defect.ResetDeviceisthe ordinary "Device is initialized already" rather than the band lock —
which is what proves the classification is right. A pre-existing multi-chain
wallet is never stamped, so it stays portable.
Still NOT proven: that storage survives a real firmware swap on hardware.
That is the bootloader's preserve path, not the firmware's, and it cannot be
shown with unsigned images — the bootloader wipes on any unsigned transition by
design (
docs/StorageVersionGate.md). It needs a signed build on a productiondevice.
The band deliberately wipes on band-unaware firmware. Already-shipped
7.14.1/7.14.2 classify an in-band version as unknown →
SUS_Invalid→storage_reset(); storage_commit(). That is alpha's documented design ("oldermulti-chain firmware treats it as an unknown version and resets"), carried here
faithfully — but it is a data-loss path on downgrade and deserves an explicit
decision rather than inheritance. The alternative (a flags bit older firmware
ignores) trades the wipe for weaker isolation.
An adversarial review of the band found a critical defect, now fixed
(
dfd24432b). The lock had no caller: the accessor sat in a private headerfsm.ccannot include, so a locked device looked uninitialized, accepted a fullResetDeviceceremony, wrote nothing, and reportedSuccess("Device reset")—with the seed in RAM only and, under
no_backup, never displayed. It nowrefuses at
CHECK_NOT_INITIALIZED, before the user does any work. The samereview found locked devices reporting an empty
device_id; also fixed.This PR will be updated in place as the pins and remaining items land.
Dice-roll entropy (added in this PR)
On-device dice entropy for seed creation, backported from the 7.15 line, where
its collection UI and press classification have been verified on hardware.
ResetDevice.dice_entropycollects 50/75/99 physical rolls on the device(short press advances 1–6/undo, 800 ms hold commits) and folds them into the
internal entropy before
EntropyRequest, so the host contribution arrivesstrictly after the device has committed to its own. The seed recipe
SHA256(internal ‖ external)is unchanged; a completion screen showsSHA256(rolls)[0..8]so the user can recompute it from their own writtenrolls.
docs/DiceEntropy.mddocuments what the digest proves and, moreimportantly, what it deliberately does not.
How it was brought over. Eight files are byte-identical to the
hardware-verified 7.15 versions (
dice_input.{c,h},rng_health.{c,h},rng.{c,h},u2f.c,keepkey_flash.c, plus both unit-test files); thereset.c/fsm_msg_common.h/fsm_msg_debug.hintegration hunks wereported surgically and diff clean against 7.15 except for the one deliberate
divergence below. The device-protocol pin advances to
8545cd5b(the samepin release/7.15 builds against); every new bytes/string field on the compiled
protos was verified to carry nanopb sizing, and the pre-staged options files in
this tree already covered all but three Solana LUT lines.
One deliberate divergence from 7.15:
display_randomis RETAINED here,not removed. Already-shipped 7.14-line hosts legitimately request the
Internal Entropy screen, and the pinned python suite asserts it below 7.15.0.
The two features are mutually exclusive by refusal:
ResetDevicewith bothdisplay_randomanddice_entropyfails with a SyntaxError, because thescreen shows the POST-mix value — honoring both would hand a host that reads
it the seed pre-image and make the dice fold-in worthless.
The seed-time RNG gate comes with it.
reset_init()now draws throughrng_health_check()+random_buffer_checked()(SP 800-90B RCT/APT,fail-closed, boot-latched), and the same gate covers every other call site on
rng_health.h's documented list: the storage encryption key and wipe-code key(
storage_drawKeyMaterial()), the V1-upgrade PIN-KDF salt, U2F key-handlederivation, and the one-shot OTP randomness block. The list in the header is
the complete list in this tree, verified by grep — an earlier draft of this
backport gated only
reset_init()and shipped a header that overclaimed;an adversarial audit caught it and the remaining sites were ported.
Test coverage runs, rather than skipping. The python-keepkey pin advances
one commit: the dice end-to-end test, the ceremony re-entry regression
(host-chosen-seed guard), and report Section K were gated
>= 7.15.0andsilently skipped against this branch — CI went green with zero device-level
dice coverage. The gates now read
>= 7.14.3, which is exact for the wholefleet: no firmware between 7.14.3 and 7.15.0 exists without dice. CI on this
PR therefore exercises the full choreography over DebugLink (99 injected
rolls, digest parity, seed parity against a host-side recomputation) plus
Dice.*andRngHealth.*gtests (20), on every push.ci.ymladditionallygains a bitcoin-only ARM compile leg so the variant this release exists for
is built on every PR, not first at release time.
Verified locally before pushing (CI-parity docker, same base image
digest): both ARM variants link (bitcoin-only .text 372.5 KB, full 523.5 KB);
firmware-unit 103/103; the full python-integration compose suite passes with
zero failures; clang-format-20 clean.
Known limits, stated plainly: the physical short-press/long-hold
classification cannot be exercised on the emulator (the button ISR body is
compiled out) — it is proven on hardware on the 7.15 tree, whose
dice_input.cis byte-identical, but not yet on a device flashed with thisexact build. 99 rolls carry 255.91 bits against the 256-bit label
(d6 = 2.585 bits/roll, Coldcard convention); the RNG mix makes the seed at
least as strong as either source alone. A cancelled ceremony leaves the
derived mnemonic in bip39's static buffer until the next derivation — a
pre-existing property shared byte-for-byte with the baseline and 7.15, noted
here rather than fixed to keep this PR's diff reviewable.