Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions docs/limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
- derivation path for each cosigner must be known and consistent with PSBT
- XFP values (fingerprints) MUST be unique for each of the co-signers
- multisig wallet `name` can only contain printable ASCII characters `range(32, 127)`
- for taproot multisig (musig) limitations check musig.md

### BIP-67

Expand Down
3 changes: 1 addition & 2 deletions docs/miniscript.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Miniscript

**COLDCARD<sup>&reg;</sup>** Mk4 experimental `EDGE` versions
support Miniscript and MiniTapscript.
**COLDCARD<sup>&reg;</sup>** `EDGE` versions support Miniscript and MiniTapscript.

## Import/Export

Expand Down
25 changes: 25 additions & 0 deletions docs/musig.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# MuSig2

**COLDCARD<sup>&reg;</sup>** `EDGE` versions support [MuSig2](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki) from version `6.4.2X` & `6.4.2QX`.

COLDCARD implements all following BIPs, further restricting their scope (read more in Limitations section):
* PSBT fields [BIP-373](https://github.com/bitcoin/bips/blob/master/bip-0373.mediawiki)
* `musig()` descriptor key expression [BIP-390](https://github.com/bitcoin/bips/blob/master/bip-0390.mediawiki)
* Derivation Scheme for MuSig2 Aggregate Keys [BIP-328](https://github.com/bitcoin/bips/blob/master/bip-0328.mediawiki)

### Limitations:
* COLDCARD must stay powered up between 1st and 2nd round as necessary musig session data are stored in volatile memory only
* `musig()` can only be used inside `tr()` expression as key expression
* cannot be nested within another `musig()` expression
* only one own key in `musig()` expression
* `musig(KEY, KEY, ..., KEY)/<NUM;NUM;...>/*`
* all `KEY`s MUST be unique - no repeated keys
* `KEY` expression MUST be extended key (not plain pubkey)
* `KEY` expression cannot contain child derivation, only `musig()` expression can contain derivation steps
* `KEY`s are sorted prior to aggregation
* hardened derivation not allowed for `musig()` expression
* derivation must end with `*` - only ranged `musig()` expression allowed, if `musig()` derivation is omitted, `/<0;1>/*` is implied
* PSBT must contain all the data required by BIP-373
* COLDCARD strictly differentiate between 1st & 2nd MuSig2 round. If COLDCARD provides nonce, it will not attempt to sign even if it could (a.k.a enough nonces from cosigners are available).
To provide both nonce(s) & signature(s) signing needs to be preformed twice.
* keys from WIF Store cannot be used for MuSig2 signing
2 changes: 1 addition & 1 deletion external/ckcc-protocol
12 changes: 5 additions & 7 deletions releases/EdgeChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,22 @@ This lists the changes in the most recent EDGE firmware, for each hardware platf

# Shared Improvements - Both Mk4 and Q

- New Feature: Support for v3 transactions
- New Feature: Send keystrokes with all derived BIP-85 secrets
- Enhancement: CCC allow to reset block height
- New Feature: Ability to sign MuSig2 UTXOs. Read more [here](docs/musig.md) TODO proper link
- Bugfix: PSBT global XPUBs validation when signing with specific wallet
- Bugfix: Do not allow sighash DEFAULT outside taproot context

# Mk4 Specific Changes

## 6.4.1X - 2026-xx-xx
## 6.5.0X - 2026-03-24

- todo
- synced with master up to `5.5.0`


# Q Specific Changes

## 6.4.0QX - 2026-xx-xx
## 6.5.0QX - 2026-03-24

- todo
- synced with master up to `1.4.0Q`


# Release History
Expand Down
26 changes: 19 additions & 7 deletions shared/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from uhashlib import sha256
from ustruct import pack
from public_constants import AFC_SCRIPT, AF_CLASSIC, AFC_BECH32, SUPPORTED_ADDR_FORMATS, AF_P2TR
from public_constants import STXN_FINALIZE, STXN_VISUALIZE, STXN_SIGNED, AF_P2SH, AF_P2WPKH_P2SH
from sffile import SFFile
Expand Down Expand Up @@ -275,8 +276,10 @@ async def try_push_tx(data, txid, txn_sha=None):

class ApproveTransaction(UserAuthorizedAction):
def __init__(self, psbt_len, flags=None, psbt_sha=None, input_method=None,
output_encoder=None, filename=None, miniscript_wallet=None):
output_encoder=None, filename=None, miniscript_wallet=None,
offset=TXN_INPUT_OFFSET):
super().__init__()
self.offset = offset
self.psbt_len = psbt_len

# do finalize is None if not USB, None = decide based on is_complete
Expand Down Expand Up @@ -405,7 +408,7 @@ async def interact(self):
# step 1: parse PSBT from PSRAM into in-memory objects.

try:
with SFFile(TXN_INPUT_OFFSET, length=self.psbt_len, message='Reading...') as fd:
with SFFile(self.offset, length=self.psbt_len, message='Reading...') as fd:
# NOTE: psbtObject captures the file descriptor and uses it later
self.psbt = psbtObject.read_psbt(fd)
except BaseException as exc:
Expand Down Expand Up @@ -433,6 +436,9 @@ async def interact(self):
# which outputs are change
self.psbt.consider_dangerous_sighash()

if self.psbt.session:
self.psbt.session.update(pack('<I', self.psbt.lock_time))

except FraudulentChangeOutput as exc:
# sys.print_exception(exc)
#print('FraudulentChangeOutput: ' + exc.args[0])
Expand Down Expand Up @@ -769,13 +775,14 @@ def output_summary_text(self, msg):
msg.write('%s %s\n\n' % self.chain.render_value(total_change - visible_change_sum))


def sign_transaction(psbt_len, flags=0x0, psbt_sha=None, miniscript_wallet=None):
def sign_transaction(psbt_len, flags=0x0, psbt_sha=None, miniscript_wallet=None,
offset=TXN_INPUT_OFFSET):
# transaction (binary) loaded into PSRAM already, checksum checked
# optional miniscript_wallet arg, choose particular enrolled wallet by name to sign
UserAuthorizedAction.check_busy(ApproveTransaction)
UserAuthorizedAction.active_request = ApproveTransaction(
psbt_len, flags, psbt_sha=psbt_sha, input_method="usb",
miniscript_wallet=miniscript_wallet,
miniscript_wallet=miniscript_wallet, offset=offset
)

# kill any menu stack, and put our thing at the top
Expand Down Expand Up @@ -816,6 +823,7 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
first_time = True
msg = None
title = None
base_title = "PSBT " + ("Signed" if psbt.sig_added else "Updated")

is_complete = psbt.is_complete()
if finalize is not None:
Expand Down Expand Up @@ -849,7 +857,7 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,

first_time = False
msg = noun + " shared via USB."
title = "PSBT Signed"
title = base_title

if txid and await try_push_tx(data_len, txid, data_sha2):
# go directly to reexport menu after pushTX
Expand Down Expand Up @@ -929,8 +937,9 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
elif (ch == 't') and not is_complete:
# they might want to teleport it, but only if we have PSBT
# there is no need to teleport PSBT if txn is already complete & ready to be broadcast
# updated PSBT is at TXN_OUTPUT_OFFSET (at TXN_INPUT_OFFSET is PSBT that is NOT updated)
from teleport import kt_send_psbt
ok = await kt_send_psbt(psbt, data_len)
ok = await kt_send_psbt(psbt, data_len, psbt_offset=TXN_OUTPUT_OFFSET)
if ok:
title = "Sent by Teleport"
else:
Expand All @@ -946,7 +955,7 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,

input_method = None
first_time = False
title = "PSBT Signed"
title = base_title

async def _save_to_disk(psbt, txid, save_options, is_complete, data_len, output_encoder, filename=None):
# Saving a PSBT from PSRAM to something disk-like.
Expand Down Expand Up @@ -1806,6 +1815,9 @@ def yield_item(self, offset, end, qr_items, change_idxs):
psbt_item += "Multisig: %dof%d\n\n" % (M, N)
except: pass

if inp.is_musig:
psbt_item += "MuSig2\n\n"

if inp.part_sigs or inp.taproot_script_sigs:
# do not show XFPs in case input is fully signed
# only part_sig should be available, as we haven't signed yet so added_sigs empty
Expand Down
4 changes: 2 additions & 2 deletions shared/bsms.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from files import CardSlot, CardMissingError, needs_microsd
from ux import ux_show_story, ux_enter_number, restore_menu, ux_input_text
from ux import the_ux, _import_prompt_builder, export_prompt_builder
from descriptor import Descriptor, Key, append_checksum
from descriptor import Descriptor, ExtendedKey, append_checksum
from miniscript import Sortedmulti, Number
from charcodes import KEY_NFC, KEY_QR

Expand Down Expand Up @@ -686,7 +686,7 @@ def get_token(index):
)
version, tok, key_exp, description, sig = data.strip().split("\n")
assert tok == token, "Token mismatch saved %s, received from signer %s" % (token, tok)
key = Key.from_string(key_exp)
key = ExtendedKey.from_string(key_exp)
dis.progress_bar_show(i_div_N / 4)
msg = signer_data_round1(token, key_exp, description)
digest = chain.hash_message(msg.encode())
Expand Down
Loading