taproot singlesig

This commit is contained in:
scgbckbone 2024-06-07 17:50:15 +02:00 committed by doc-hex
parent a798e96de0
commit d2920d1c60
39 changed files with 1756 additions and 471 deletions

View File

@ -122,7 +122,8 @@ We will summarize transaction outputs as "change" back into same wallet, however
- `p2wsh-p2sh`: _redeemScript_ (which is: `0x00 + 0x20 + sha256(witnessScript)`), and
_witnessScript_ (which contains the multisig script)
- `p2wsh`: only _witnessScript_ (which contains the actual multisig script)
- `p2tr`(keypath singlesig): no _redeemScript_, no _witnessScript_ and output key MUST commit to an unspendable script path as follows `Q = P + int(hashTapTweak(bytes(P)))G`
- `p2tr`(scriptpath multisig): _taproot_merkle_root_ and _leaf_script_ more info in docs/taproot.md
# Derivation Paths

65
docs/taproot.md Normal file
View File

@ -0,0 +1,65 @@
# Taproot
**COLDCARD<sup>&reg;</sup>** Mk4 experimental `EDGE` versions
support Schnorr signatures ([BIP-0340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki)),
Taproot ([BIP-0341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki))
and Tapscript ([BIP-0342](https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki)) support.
## Output script (a.k.a address) generation
If the spending conditions do not require a script path, the output key MUST commit to an unspendable script path.
`Q = P + int(hashTapTweak(bytes(P)))G` a.k.a internal key MUST be tweaked by `TapTweak` tagged hash of itself. If
the spending conditions require script path, internal key MUST be tweaked by `TapTweak` tagged hash of tree merkle root.
Addresses in `Address Explorer` for `p2tr` are generated with above-mentioned methods. Outputs `scriptPubkeys` in PSBT
MUST be generated with above-mentoned methods to be considered change.
## Allowed descriptors
1. Single signature wallet without script path: `tr(key)`
2. Tapscript multisig with internal key and up to 8 leaf scripts:
* `tr(internal_key, sortedmulti_a(2,@0,@1))`
* `tr(internal_key, pk(@0))`
* `tr(internal_key, {sortedmulti_a(2,@0,@1),pk(@2)})`
* `tr(internal_key, {or_d(pk(@0),and_v(v:pkh(@1),older(1000))),pk(@2)})`
## Provably unspendable internal key
There are few methods to provide/generate provably unspendable internal key, if users wish to only use script path
for multisig.
1. use provably unspendable internal key H from [BIP-0341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs). This way is leaking the information that key path spending is not possible and therefore not recommended privacy-wise.
`tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0, sortedmulti_a(2,@0,@1))`
2. use COLDCARD specific placeholder `@` to let HWW pick a fresh integer r in the range 0...n-1 uniformly at random and use `H + rG` as internal key. COLDCARD will not store r and therefore user is not able to prove to other party how the key was generated and whether it is actually unspendable.
`tr(r=@, sortedmulti_a(MofN))`
3. pick a fresh integer r in the range 0...n-1 uniformly at random yourself and provide that in the descriptor. COLDCARD generates internal key with `H + rG`. It is possible to prove to other party that this internal key does not have a known discrete logarithm with respect to G by revealing r to a verifier who can then reconstruct how the internal key was created.
`tr(r=77ec0c0fdb9733e6a3c753b1374c4a465cba80dff52fc196972640a26dd08b76, sortedmulti_a(2,@0,@1))`
## Limitations
### Tapscript Limitations
In current version only `TREE` of max depth 4 is allowed (max 8 leaf script allowed).
Taproot single leaf multisig has artificial limit of max 32 signers (M=N=32).
Number of keys in taptree is limited to 32.
If Coldcard can sign by both key path and script path - key path has precedence.
### PSBT Requirements
PSBT provider MUST provide following Taproot specific input fields in PSBT:
1. `PSBT_IN_TAP_BIP32_DERIVATION` with all the necessary keys with their leaf hashes and derivation (including XFP). Internal key has to be specified here with empty leaf hashes.
2. `PSBT_IN_TAP_INTERNAL_KEY` MUST match internal key provided in `PSBT_IN_TAP_BIP32_DERIVATION`
3. `PSBT_IN_TAP_MERKLE_ROOT` MUST be empty if there is no script path. Otherwise it MUST match what Coldcard can calculate from registered descriptor.
4. `PSBT_IN_TAP_LEAF_SCRIPT` MUST be specified if there is a script path. Currently MUST be of length 1 (only one script allowed)
PSBT provider MUST provide following Taproot specific output fields in PSBT:
1. `PSBT_OUT_TAP_BIP32_DERIVATION` with all the necessary keys with their leaf hashes and derivation (including XFP). Internal key has to be specified here with empty leaf hashes.
2. `PSBT_OUT_TAP_INTERNAL_KEY` must match internal key provided in `PSBT_OUT_TAP_BIP32_DERIVATION`
3. `PSBT_OUT_TAP_TREE` with depth, leaf version and script defined. Currently only one script is allowed.

@ -1 +1 @@
Subproject commit a6d901f9fca50755835eca895586ca74d0ca81ed
Subproject commit cad2722d1433dbb956e4457eac9ea01e1c77abbe

2
external/libngu vendored

@ -1 +1 @@
Subproject commit 356b9137cf7ddf5de66ec4cdc0a4d757b2e42790
Subproject commit 2537f1581d0bad2c16fa391bc6fade328450a217

View File

@ -16,7 +16,7 @@ from export import make_json_wallet, make_summary_file, make_descriptor_wallet_e
from export import make_bitcoin_core_wallet, generate_wasabi_wallet, generate_generic_export
from export import generate_unchained_export, generate_electrum_wallet
from files import CardSlot, CardMissingError, needs_microsd
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR
from glob import settings
from pincodes import pa
from menu import start_chooser, MenuSystem, MenuItem
@ -1014,7 +1014,7 @@ async def export_xpub(label, _2, item):
path = "m"
addr_fmt = AF_CLASSIC
else:
remap = {44:0, 49:1, 84:2}[mode]
remap = {44:0, 49:1, 84:2,86:3}[mode]
_, path, addr_fmt = chains.CommonDerivations[remap]
path = path.format(account='{acct}', coin_type=chain.b44_cointype, change=0, idx=0)[:-4]
@ -1095,7 +1095,7 @@ def ss_descriptor_export_story(addition="", background="", acct=True):
async def ss_descriptor_skeleton(_0, _1, item):
# Export of descriptor data (wallet)
int_ext, addition, f_pattern = None, "", "descriptor.txt"
allowed_af = [AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH]
allowed_af = [AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH, AF_P2TR]
if item.arg:
int_ext, allowed_af, ll, f_pattern = item.arg
addition = " for " + ll
@ -1572,9 +1572,18 @@ async def file_picker(suffix=None, min_size=1, max_size=1000000, taster=None,
# ignore subdirs
continue
if suffix and not fn.lower().endswith(suffix):
# wrong suffix
continue
if suffix:
if isinstance(suffix, list):
for sfx in suffix:
if fn.lower().endswith(sfx):
break
else:
# wrong suffix
continue
else:
if not fn.lower().endswith(suffix):
# wrong suffix
continue
if fn[0] == '.': continue

View File

@ -8,7 +8,7 @@ import chains, stash, version
from ux import ux_show_story, the_ux, ux_enter_bip32_index
from ux import export_prompt_builder, import_export_prompt_decode
from menu import MenuSystem, MenuItem
from public_constants import AFC_BECH32, AFC_BECH32M, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
from public_constants import AFC_BECH32, AFC_BECH32M, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR
from multisig import MultisigWallet
from uasyncio import sleep_ms
from uhashlib import sha256
@ -41,6 +41,7 @@ class KeypathMenu(MenuSystem):
MenuItem("m/44h/⋯", f=self.deeper),
MenuItem("m/49h/⋯", f=self.deeper),
MenuItem("m/84h/⋯", f=self.deeper),
MenuItem("m/86h/⋯", f=self.deeper),
MenuItem("m/0/{idx}", menu=self.done),
MenuItem("m/{idx}", menu=self.done),
MenuItem("m", f=self.done),
@ -67,7 +68,7 @@ class KeypathMenu(MenuSystem):
pl = p[0:p.rfind('/')].rfind('/')
else:
self.prefix = p # displayed on mk4 only
pl = len(p)-2
pl = len(p)-2
for mi in items:
mi.arg = mi.label
mi.label = ''+mi.label[pl:]
@ -112,9 +113,8 @@ class PickAddrFmtMenu(MenuSystem):
def __init__(self, path, parent):
self.parent = parent
items = [
MenuItem(addr_fmt_label(AF_CLASSIC), f=self.done, arg=(path, AF_CLASSIC)),
MenuItem(addr_fmt_label(AF_P2WPKH), f=self.done, arg=(path, AF_P2WPKH)),
MenuItem(addr_fmt_label(AF_P2WPKH_P2SH), f=self.done, arg=(path, AF_P2WPKH_P2SH)),
MenuItem(addr_fmt_label(af), f=self.done, arg=(path, af))
for af in [AF_CLASSIC, AF_P2WPKH, AF_P2TR, AF_P2WPKH_P2SH]
]
super().__init__(items)
if path.startswith("m/84h"):
@ -494,7 +494,7 @@ async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num,
dis.progress_sofar(idx, count or 1)
sig_nice = None
if not ms_wallet:
if not ms_wallet and addr_fmt != AF_P2TR:
derive = path.format(account=account_num, change=change, idx=start) # first addr
sig_nice = write_sig_file([(h.digest(), fname)], derive, addr_fmt)

View File

@ -8,7 +8,7 @@ from ubinascii import b2a_base64, a2b_base64
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from uhashlib import sha256
from public_constants import MSG_SIGNING_MAX_LENGTH, SUPPORTED_ADDR_FORMATS
from public_constants import MSG_SIGNING_MAX_LENGTH, SUPPORTED_ADDR_FORMATS, AF_P2TR
from public_constants import AFC_SCRIPT, AF_CLASSIC, AFC_BECH32, AF_P2WPKH, AF_P2WPKH_P2SH
from public_constants import STXN_FLAGS_MASK, STXN_FINALIZE, STXN_VISUALIZE, STXN_SIGNED
from sffile import SFFile
@ -310,6 +310,10 @@ class ApproveMessageSign(UserAuthorizedAction):
self.addr_fmt = parse_addr_fmt_str(addr_fmt)
self.approved_cb = approved_cb
# temporary - no p2tr support
if self.addr_fmt == AF_P2TR:
raise ValueError("Unsupported address format: 'p2tr'")
from glob import dis
dis.fullscreen('Wait...')

View File

@ -8,6 +8,8 @@ from ubinascii import hexlify as b2a_hex
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2TR
from public_constants import AF_P2SH, AF_P2WSH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH
from public_constants import AFC_PUBKEY, AFC_SEGWIT, AFC_BECH32, AFC_SCRIPT
from public_constants import TAPROOT_LEAF_TAPSCRIPT, TAPROOT_LEAF_MASK
from serializations import hash160, ser_compact_size, disassemble, ser_string
from serializations import hash160, ser_compact_size, disassemble
from ucollections import namedtuple
from opcodes import OP_RETURN, OP_1, OP_16
@ -26,6 +28,27 @@ Slip132Version = namedtuple('Slip132Version', ('pub', 'priv', 'hint'))
# - from <https://github.com/Bit-Wasp/bitcoin-php/issues/576>
# - also electrum source: electrum/lib/constants.py
def taptweak(internal_key, tweak=None):
# BIP 341 states: "If the spending conditions do not require a script path,
# the output key should commit to an unspendable script path instead of having no script path.
# This can be achieved by computing the output key point as:
# Q = P + int(hashTapTweak(bytes(P)))G."
actual_tweak = internal_key if tweak is None else internal_key + tweak
tweak = ngu.secp256k1.tagged_sha256(b"TapTweak", actual_tweak)
xo_pubkey = ngu.secp256k1.xonly_pubkey(internal_key)
xo_pubkey_tweaked = xo_pubkey.tweak_add(tweak)
return xo_pubkey_tweaked.to_bytes()
def tapscript_serialize(script, leaf_version=TAPROOT_LEAF_TAPSCRIPT):
# leaf version is only 7 msb
lv = leaf_version % TAPROOT_LEAF_MASK
return bytes([lv]) + ser_string(script)
def tapleaf_hash(script, leaf_version=TAPROOT_LEAF_TAPSCRIPT):
return ngu.secp256k1.tagged_sha256(b"TapLeaf",
tapscript_serialize(script, leaf_version))
class ChainsBase:
curve = 'secp256k1'
@ -110,23 +133,30 @@ class ChainsBase:
# - works only with single-key addresses
assert not addr_fmt & AFC_SCRIPT
keyhash = ngu.hash.hash160(pubkey)
if addr_fmt == AF_CLASSIC:
script = b'\x76\xA9\x14' + keyhash + b'\x88\xAC'
elif addr_fmt == AF_P2WPKH_P2SH:
redeem_script = b'\x00\x14' + keyhash
scripthash = ngu.hash.hash160(redeem_script)
script = b'\xA9\x14' + scripthash + b'\x87'
elif addr_fmt == AF_P2WPKH:
script = b'\x00\x14' + keyhash
if addr_fmt == AF_P2TR:
assert len(pubkey) == 32 # internal
script = b'\x51\x20' + taptweak(pubkey)
else:
raise ValueError('bad address template: %s' % addr_fmt)
keyhash = ngu.hash.hash160(pubkey)
if addr_fmt == AF_CLASSIC:
script = b'\x76\xA9\x14' + keyhash + b'\x88\xAC'
elif addr_fmt == AF_P2WPKH_P2SH:
redeem_script = b'\x00\x14' + keyhash
scripthash = ngu.hash.hash160(redeem_script)
script = b'\xA9\x14' + scripthash + b'\x87'
elif addr_fmt == AF_P2WPKH:
script = b'\x00\x14' + keyhash
else:
raise ValueError('bad address template: %s' % addr_fmt)
return cls.render_address(script)
@classmethod
def address(cls, node, addr_fmt):
# return a human-readable, properly formatted address
if addr_fmt == AF_P2TR:
xo_pk = node.pubkey()[1:]
return ngu.codecs.segwit_encode(cls.bech32_hrp, 1, taptweak(xo_pk))
if addr_fmt == AF_CLASSIC:
# olde fashioned P2PKH
@ -295,6 +325,7 @@ class BitcoinMain(ChainsBase):
AF_P2WPKH: Slip132Version(0x04b24746, 0x04b2430c, 'z'),
AF_P2WSH_P2SH: Slip132Version(0x0295b43f, 0x0295b005, 'Y'),
AF_P2WSH: Slip132Version(0x02aa7ed3, 0x02aa7a99, 'Z'),
AF_P2TR: Slip132Version(0x0488B21E, 0x0488ADE4, 'x'),
}
bech32_hrp = 'bc'
@ -316,6 +347,7 @@ class BitcoinTestnet(BitcoinMain):
AF_P2WPKH: Slip132Version(0x045f1cf6, 0x045f18bc, 'v'),
AF_P2WSH_P2SH: Slip132Version(0x024289ef, 0x024285b5, 'U'),
AF_P2WSH: Slip132Version(0x02575483, 0x02575048, 'V'),
AF_P2TR: Slip132Version(0x043587cf, 0x04358394, 't'),
}
bech32_hrp = 'tb'
@ -338,6 +370,7 @@ class BitcoinRegtest(BitcoinMain):
AF_P2WPKH: Slip132Version(0x045f1cf6, 0x045f18bc, 'v'),
AF_P2WSH_P2SH: Slip132Version(0x024289ef, 0x024285b5, 'U'),
AF_P2WSH: Slip132Version(0x02575483, 0x02575048, 'V'),
AF_P2TR: Slip132Version(0x043587cf, 0x04358394, 't'),
}
bech32_hrp = 'bcrt'
@ -399,6 +432,8 @@ CommonDerivations = [
AF_P2WPKH_P2SH ), # generates 3xxx/2xxx p2sh-looking addresses
( 'BIP-84 (Native Segwit P2WPKH)', "m/84h/{coin_type}h/{account}h/{change}/{idx}",
AF_P2WPKH ), # generates bc1 bech32 addresses
('BIP-86 (Taproot Segwit P2TR)', "m/86h/{coin_type}h/{account}h/{change}/{idx}",
AF_P2TR), # generates bc1p bech32m addresses
]

View File

@ -4,7 +4,7 @@
#
# Based on: https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp
#
from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH
from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH, AF_P2TR
MULTI_FMT_TO_SCRIPT = {
AF_P2SH: "sh(%s)",
@ -19,6 +19,7 @@ MULTI_FMT_TO_SCRIPT = {
}
SINGLE_FMT_TO_SCRIPT = {
AF_P2TR: "tr(%s)",
AF_P2WPKH: "wpkh(%s)",
AF_CLASSIC: "pkh(%s)",
AF_P2WPKH_P2SH: "sh(wpkh(%s))",
@ -227,6 +228,11 @@ class Descriptor:
tmp_desc = desc.replace("wpkh(", "")
tmp_desc = tmp_desc.rstrip(")")
elif desc.startswith("tr("):
addr_fmt = AF_P2TR
tmp_desc = desc.replace("tr(", "")
tmp_desc = tmp_desc.rstrip(")")
# wrapped segwit
elif desc.startswith("sh(wpkh("):
addr_fmt = AF_P2WPKH_P2SH
@ -234,7 +240,7 @@ class Descriptor:
tmp_desc = tmp_desc.rstrip("))")
else:
raise ValueError("Unsupported descriptor. Supported: pkh(, wpkh(, sh(wpkh(.")
raise ValueError("Unsupported descriptor. Supported: pkh(, wpkh(, sh(wpkh( and tr(.")
koi, key = cls.parse_key_orig_info(tmp_desc)
if key[0:4] not in ["tpub", "xpub"]:

View File

@ -9,7 +9,7 @@ from utils import xfp2str, swab32, chunk_writer
from ux import ux_show_story
from glob import settings
from auth import write_sig_file
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH, AF_P2TR
from charcodes import KEY_NFC, KEY_CANCEL, KEY_QR
from ownership import OWNERSHIP
@ -103,7 +103,7 @@ be needed for different systems.
node = sv.derive_path(hard_sub, register=False)
yield ("%s => %s\n" % (hard_sub, chain.serialize_public(node)))
if show_slip132 and addr_fmt != AF_CLASSIC and (addr_fmt in chain.slip132):
if show_slip132 and addr_fmt not in (AF_CLASSIC, AF_P2TR) and (addr_fmt in chain.slip132):
yield ("%s => %s ##SLIP-132##\n" % (
hard_sub, chain.serialize_public(node, addr_fmt)))
@ -160,8 +160,10 @@ async def write_text_file(fname_pattern, body, title, derive, addr_fmt):
with open(fname, 'wb') as fd:
chunk_writer(fd, body)
h = ngu.hash.sha256s(body.encode())
sig_nice = write_sig_file([(h, fname)], derive, addr_fmt)
sig_nice = None
if addr_fmt != AF_P2TR:
h = ngu.hash.sha256s(body.encode())
sig_nice = write_sig_file([(h, fname)], derive, addr_fmt)
except CardMissingError:
await needs_microsd()
@ -170,8 +172,9 @@ async def write_text_file(fname_pattern, body, title, derive, addr_fmt):
await ux_show_story('Failed to write!\n\n\n'+str(e))
return
msg = '%s file written:\n\n%s\n\n%s signature file written:\n\n%s' % (title, nice, title,
sig_nice)
msg = '%s file written:\n\n%s' % (title, nice)
if sig_nice:
msg += '\n\n%s signature file written:\n\n%s' % (title, sig_nice)
await ux_show_story(msg)
async def make_summary_file(fname_pattern='public.txt'):
@ -364,8 +367,10 @@ def generate_generic_export(account_num=0):
( 'bip44', "m/44h/{ct}h/{acc}h", AF_CLASSIC, 'p2pkh', False ),
( 'bip49', "m/49h/{ct}h/{acc}h", AF_P2WPKH_P2SH, 'p2sh-p2wpkh', False ), # was "p2wpkh-p2sh"
( 'bip84', "m/84h/{ct}h/{acc}h", AF_P2WPKH, 'p2wpkh', False ),
('bip86', "m/86h/{ct}h/{acc}h", AF_P2TR, 'p2tr', False),
( 'bip48_1', "m/48h/{ct}h/{acc}h/1h", AF_P2WSH_P2SH, 'p2sh-p2wsh', True ),
( 'bip48_2', "m/48h/{ct}h/{acc}h/2h", AF_P2WSH, 'p2wsh', True ),
('bip48_3', "m/48h/{ct}h/{acc}h/3h", AF_P2TR, 'p2tr', True ),
( 'bip45', "m/45h", AF_P2SH, 'p2sh', True ),
]:
if fmt == AF_P2SH and account_num:
@ -375,7 +380,7 @@ def generate_generic_export(account_num=0):
node = sv.derive_path(dd)
xfp = xfp2str(swab32(node.my_fp()))
xp = chain.serialize_public(node, AF_CLASSIC)
zp = chain.serialize_public(node, fmt) if fmt != AF_CLASSIC else None
zp = chain.serialize_public(node, fmt) if fmt not in (AF_CLASSIC, AF_P2TR) else None
if is_ms:
desc = multisig_descriptor_template(xp, dd, master_xfp_str, fmt)
else:
@ -520,13 +525,16 @@ async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int
mode = 84
elif addr_type == AF_P2WPKH_P2SH:
mode = 49
elif addr_type == AF_P2TR:
mode = 86
else:
raise ValueError(addr_type)
OWNERSHIP.note_wallet_used(addr_type, account_num)
derive = "m/{mode}h/{coin_type}h/{account}h".format(mode=mode,
account=account_num, coin_type=chain.b44_cointype)
derive = "m/{mode}h/{coin_type}h/{account}h".format(
mode=mode, account=account_num, coin_type=chain.b44_cointype
)
dis.progress_bar_show(0.2)
with stash.SensitiveValues() as sv:
dis.progress_bar_show(0.3)

View File

@ -3,14 +3,15 @@
#
# paper.py - generate paper wallets, based on random values (not linked to wallet)
#
import ujson
import ujson, ngu, chains
from ubinascii import hexlify as b2a_hex
from utils import imported
from public_constants import AF_CLASSIC, AF_P2WPKH
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2TR
from ux import ux_show_story, ux_dramatic_pause
from files import CardSlot, CardMissingError, needs_microsd
from actions import file_picker
from menu import MenuSystem, MenuItem
from stash import blank_object
background_msg = '''\
Coldcard will pick a random private key (which has no relation to your seed words), \
@ -29,10 +30,6 @@ can still be made. Visit the Coldcard website to get some interesting templates.
SECP256K1_ORDER = b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xba\xae\xdc\xe6\xaf\x48\xa0\x3b\xbf\xd2\x5e\x8c\xd0\x36\x41\x41"
# Aprox. time of this feature release (Nov 20/2019) so no need to scan
# blockchain earlier than this during "importmulti"
FEATURE_RELEASE_TIME = const(1574277000)
# These very-specific text values are matched on the Coldcard; cannot be changed.
class placeholders:
addr = b'ADDRESS_XXXXXXXXXXXXXXXXXXXXXXXXXXXXX' # 37 long
@ -51,6 +48,12 @@ class PaperWalletMaker:
self.my_menu = my_menu
self.template_fn = None
self.is_segwit = False
self.is_taproot = False
def atype(self):
if self.is_taproot: return 2, 'Taproot P2TR'
if self.is_segwit: return 1, 'Segwit P2WPKH'
return 0, 'Classic P2PKH'
async def pick_template(self, *a):
fn = await file_picker(suffix='.pdf', min_size=20000, taster=template_taster,
@ -62,17 +65,17 @@ class PaperWalletMaker:
def addr_format_chooser(self, *a):
# simple bool choice
def set(idx, text):
self.is_segwit = bool(idx)
self.is_segwit = idx == 1
self.is_taproot = idx == 2
self.update_menu()
return int(self.is_segwit), ['Classic P2PKH', 'Segwit P2WPKH'], set
return self.atype()[0], ['Classic P2PKH', 'Segwit P2WPKH', 'Taproot P2TR'], set
def update_menu(self):
# Reconstruct the menu contents based on our state.
self.my_menu.replace_items([
MenuItem("Don't make PDF" if not self.template_fn else 'Making PDF',
f=self.pick_template),
MenuItem('Classic P2PKH' if not self.is_segwit else 'Segwit P2WPKH',
chooser=self.addr_format_chooser),
MenuItem(self.atype()[1], chooser=self.addr_format_chooser),
MenuItem('Use Dice', f=self.use_dice),
MenuItem('GENERATE WALLET', f=self.doit),
], keep_position=True)
@ -82,12 +85,6 @@ class PaperWalletMaker:
from glob import dis, VD
try:
import ngu
from auth import write_sig_file
from chains import current_chain
from serializations import hash160
from stash import blank_object
if not have_key:
# get some random bytes
await ux_dramatic_pause("Picking key...", 2)
@ -104,12 +101,16 @@ class PaperWalletMaker:
dis.fullscreen("Rendering...")
# make payment address
digest = hash160(pubkey)
ch = current_chain()
ch = chains.current_chain()
if self.is_segwit:
addr = ngu.codecs.segwit_encode(ch.bech32_hrp, 0, digest)
af = AF_P2WPKH
elif self.is_taproot:
af = AF_P2TR
pubkey = pubkey[1:]
else:
addr = ngu.codecs.b58_encode(ch.b58_addr + digest)
af = AF_CLASSIC
addr = ch.pubkey_to_address(pubkey, af)
wif = ngu.codecs.b58_encode(ch.b58_privkey + privkey + b'\x01')
@ -164,8 +165,11 @@ class PaperWalletMaker:
else:
nice_pdf = ''
nice_sig = write_sig_file(sig_cont, pk=privkey, sig_name=basename,
addr_fmt=AF_P2WPKH if self.is_segwit else AF_CLASSIC)
nice_sig = None
if af != AF_P2TR:
from auth import write_sig_file
nice_sig = write_sig_file(sig_cont, pk=privkey, sig_name=basename,
addr_fmt=AF_P2WPKH if self.is_segwit else AF_CLASSIC)
# Half-hearted attempt to cleanup secrets-contaminated memory
# - better would be force user to reboot
@ -185,7 +189,8 @@ class PaperWalletMaker:
story = "Done! Created file(s):\n\n%s" % nice_txt
if nice_pdf:
story += "\n\n%s" % nice_pdf
story += "\n\n%s" % nice_sig
if nice_sig:
story += "\n\n%s" % nice_sig
await ux_show_story(story)
async def use_dice(self, *a):
@ -214,10 +219,17 @@ class PaperWalletMaker:
fp.write('Bitcoin Core command:\n\n')
# new hotness: output descriptors
desc = ('wpkh(%s)' if self.is_segwit else 'pkh(%s)') % wif
multi = ujson.dumps(dict(timestamp=FEATURE_RELEASE_TIME, desc=append_checksum(desc)))
fp.write(" bitcoin-cli importmulti '[%s]'\n\n" % multi)
fp.write('# OR (more compatible, but slower)\n\n bitcoin-cli importprivkey "%s"\n\n' % wif)
if self.is_taproot:
desc = 'tr(%s)'
elif self.is_segwit:
desc = 'wpkh(%s)'
else:
desc = 'pkh(%s)'
desc = desc % wif
descriptor = ujson.dumps(dict(timestamp="now", desc=append_checksum(desc)))
fp.write(" bitcoin-cli importdescriptors '[%s]'\n\n" % descriptor)
if not self.is_taproot:
fp.write('# OR (only supported with legacy wallets)\n\n bitcoin-cli importprivkey "%s"\n\n' % wif)
if qr_addr and qr_wif:
fp.write('\n\n--- QR Codes --- (requires UTF-8, unicode, white background)\n\n\n\n')

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,6 @@ ser_*, deser_*: functions that handle serialization/deserialization
"""
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
import ustruct as struct
import ngu
from opcodes import *
@ -30,6 +29,7 @@ hash160 = ngu.hash.hash160
def bytes_to_hex_str(s):
return str(b2a_hex(s), 'ascii')
SIGHASH_DEFAULT = const(0) # in taproot meaning same as SIGHASH_ALL (over whole TX)
SIGHASH_ALL = const(1)
SIGHASH_NONE = const(2)
SIGHASH_SINGLE = const(3)
@ -37,6 +37,7 @@ SIGHASH_ANYONECANPAY = const(0x80)
# list containing all flags that we support signing for
ALL_SIGHASH_FLAGS = [
SIGHASH_DEFAULT,
SIGHASH_ALL,
SIGHASH_NONE,
SIGHASH_SINGLE,
@ -367,6 +368,11 @@ class CTxOut(object):
# aka. P2WPKH
return 'p2pkh', self.scriptPubKey[2:2+20], True
if len(self.scriptPubKey) == 34 and \
self.scriptPubKey[0] == 81 and self.scriptPubKey[1] == 32:
# aka. P2TR
return 'p2tr', self.scriptPubKey[2:2+32], True
if len(self.scriptPubKey) == 34 and \
self.scriptPubKey[0] == 0 and self.scriptPubKey[1] == 32:
# aka. P2WSH

View File

@ -2,12 +2,14 @@
#
# utils.py - Misc utils. My favourite kind of source file.
#
import gc, sys, ustruct, ngu, chains, ure, time
import gc, sys, ustruct, ngu, chains, ure, time, version, uos, uio
from ubinascii import unhexlify as a2b_hex
from ubinascii import hexlify as b2a_hex
from ubinascii import a2b_base64, b2a_base64
from uhashlib import sha256
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR, MAX_PATH_DEPTH
from public_constants import AF_P2WSH, AF_P2WSH_P2SH
B2A = lambda x: str(b2a_hex(x), 'ascii')
@ -91,7 +93,6 @@ def pop_count(i):
def get_filesize(fn):
# like os.path.getsize()
import uos
try:
return uos.stat(fn)[6]
except OSError:
@ -220,8 +221,6 @@ def to_ascii_printable(s, strip=False):
def problem_file_line(exc):
# return a string of just the filename.py and line number where
# an exception occured. Best used on AssertionError.
import uio, sys, ure
tmp = uio.StringIO()
sys.print_exception(exc, tmp)
lines = tmp.getvalue().split('\n')[-3:]
@ -251,7 +250,6 @@ def cleanup_deriv_path(bin_path, allow_star=False):
# - assume 'm' prefix, so '34' becomes 'm/34', etc
# - do not assume /// is m/0/0/0
# - if allow_star, then final position can be * or *h (wildcard)
import ure
from public_constants import MAX_PATH_DEPTH
s = to_ascii_printable(bin_path, strip=True).lower()
@ -345,6 +343,13 @@ def match_deriv_path(patterns, path):
return False
def validate_derivation_path_length(length, allow_master=False):
# force them to use a derived key, never the master
if not allow_master:
assert length >= 4, 'too short key path'
assert (length % 4) == 0, 'corrupt key path'
assert (length // 4) <= MAX_PATH_DEPTH, 'too deep'
class DecodeStreamer:
def __init__(self):
self.runt = bytearray()
@ -431,7 +436,7 @@ def clean_shutdown(style=0):
# wipe SPI flash and shutdown (wiping main memory)
# - mk4: SPI flash not used, but NFC may hold data (PSRAM cleared by bootrom)
# - bootrom wipes every byte of SRAM, so no need to repeat here
import callgate, version, uasyncio
import callgate, uasyncio
# save if anything pending
from glob import settings
@ -507,9 +512,7 @@ def word_wrap(ln, w):
def parse_addr_fmt_str(addr_fmt):
# accepts strings and also integers if already parsed
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
if addr_fmt in [AF_P2WPKH_P2SH, AF_P2WPKH, AF_CLASSIC]:
if addr_fmt in [AF_P2WPKH_P2SH, AF_P2WPKH, AF_CLASSIC, AF_P2TR]:
return addr_fmt
addr_fmt = addr_fmt.lower()
@ -519,9 +522,10 @@ def parse_addr_fmt_str(addr_fmt):
return AF_CLASSIC
elif addr_fmt == "p2wpkh":
return AF_P2WPKH
elif addr_fmt == "p2tr":
return AF_P2TR
else:
raise ValueError("Invalid address format: '%s'\n\n"
"Choose from p2pkh, p2wpkh, p2sh-p2wpkh." % addr_fmt)
raise ValueError("Unsupported address format: '%s'" % addr_fmt)
def parse_extended_key(ln, private=False):
# read an xpub/ypub/etc and return BIP-32 node and what chain it's on.
@ -563,7 +567,10 @@ def addr_fmt_label(addr_fmt):
return {
AF_CLASSIC: "Classic P2PKH",
AF_P2WPKH_P2SH: "P2SH-Segwit",
AF_P2WPKH: "Segwit P2WPKH"
AF_P2WPKH: "Segwit P2WPKH",
AF_P2TR: "Taproot P2TR",
AF_P2WSH: "Segwit P2WSH",
AF_P2WSH_P2SH: "P2SH-P2WSH"
}[addr_fmt]
@ -620,8 +627,6 @@ def url_decode(u):
# - equiv to urllib.parse.unquote_plus
# - ure.sub is missing, so not being clever here.
# - give up on syntax errors, and return unchanged
import ure
u = u.replace('+', ' ')
while 1:
pos = u.find('%')

View File

@ -4,7 +4,7 @@
#
import chains
from descriptor import Descriptor
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR
from stash import SensitiveValues
MAX_BIP32_IDX = (2 ** 31) - 1
@ -40,8 +40,10 @@ class MasterSingleSigWallet(WalletABC):
# Construct a wallet based on current master secret, and chain.
# - path is optional, and then we use standard path for addr_fmt
# - path can be overriden when we come here via address explorer
if addr_fmt == AF_P2WPKH:
if addr_fmt == AF_P2TR:
n = 'Taproot P2TR'
prefix = path or 'm/86h/{coin_type}h/{account}h'
elif addr_fmt == AF_P2WPKH:
n = 'Segwit P2WPKH'
prefix = path or 'm/84h/{coin_type}h/{account}h'
elif addr_fmt == AF_CLASSIC:
@ -66,7 +68,6 @@ class MasterSingleSigWallet(WalletABC):
if self.chain.ctype == 'XRT':
n += ' (Regtest)'
self.name = n
self.addr_fmt = addr_fmt
@ -82,7 +83,6 @@ class MasterSingleSigWallet(WalletABC):
self._path = p
def yield_addresses(self, start_idx, count, change_idx=None):
# Render a range of addresses. Slow to start, since accesses SE in general
# - if count==1, don't derive any subkey, just do path.

View File

@ -6,8 +6,9 @@ from io import BytesIO
try:
from pysecp256k1 import (
ec_seckey_verify, ec_pubkey_create, ec_pubkey_serialize, ec_pubkey_parse,
ec_seckey_tweak_add, ec_pubkey_tweak_add,
ec_seckey_tweak_add, ec_pubkey_tweak_add, tagged_sha256
)
from pysecp256k1.extrakeys import xonly_pubkey_from_pubkey, xonly_pubkey_serialize, xonly_pubkey_tweak_add
except ImportError:
import ecdsa
SECP256k1 = ecdsa.curves.SECP256k1
@ -119,6 +120,10 @@ class PrivateKey(object):
tweaked = ec_seckey_tweak_add(self.k, tweak32)
return PrivateKey(sec_exp=tweaked)
def address(self, compressed: bool = True, chain: str = "BTC",
addr_fmt: str = "p2wpkh") -> str:
return self.K.address(compressed, chain, addr_fmt)
@classmethod
def from_wif(cls, wif_str: str) -> "PrivateKey":
"""
@ -193,8 +198,17 @@ class PublicKey(object):
return self.K.to_string(encoding="compressed" if compressed else "uncompressed")
def tweak_add(self, tweak32: bytes) -> "PublicKey":
assert len(tweak32) == 32
return PublicKey(pub_key=ec_pubkey_tweak_add(self.K, tweak32))
def taptweak(self, tweak32: bytes = None) -> "bytes":
xonly_key, _ = xonly_pubkey_from_pubkey(self.K)
tweak = tweak32 or xonly_pubkey_serialize(xonly_key)
tweak = tagged_sha256(b"TapTweak", tweak)
tweaked_pubkey = xonly_pubkey_tweak_add(xonly_key, tweak)
tweaked_xonly_pubkey, parity = xonly_pubkey_from_pubkey(tweaked_pubkey)
return xonly_pubkey_serialize(tweaked_xonly_pubkey)
@classmethod
def parse(cls, key_bytes: bytes) -> "PublicKey":
"""
@ -227,7 +241,7 @@ class PublicKey(object):
"""
return hash160(self.sec(compressed=compressed))
def address(self, compressed: bool = True, testnet: bool = False,
def address(self, compressed: bool = True, chain: str = "BTC",
addr_fmt: str = "p2wpkh") -> str:
"""
Generates bitcoin address from public key.
@ -240,18 +254,33 @@ class PublicKey(object):
3. p2wpkh (default)
:return: bitcoin address
"""
if chain == "BTC":
hrp = "bc"
pkh_prefix = b"\x00"
sh_prefix = b"\x05"
else:
pkh_prefix = b"\x6f"
sh_prefix = b"\xc4"
if chain == "XRT":
hrp = "bcrt"
elif chain == "XTN":
hrp = "tb"
else:
assert False
if addr_fmt == "p2tr":
tweaked_xonly = self.taptweak()
return bech32.encode(hrp=hrp, witver=1, witprog=tweaked_xonly)
h160 = self.h160(compressed=compressed)
if addr_fmt == "p2pkh":
prefix = b"\x6f" if testnet else b"\x00"
return encode_base58_checksum(prefix + h160)
return encode_base58_checksum(pkh_prefix + h160)
elif addr_fmt == "p2wpkh":
hrp = "tb" if testnet else "bc"
return bech32.encode(hrp=hrp, witver=0, witprog=h160)
elif addr_fmt == "p2sh-p2wpkh":
scr = b"\x00\x14" + h160 # witversion 0 + pubkey hash
h160 = hash160(scr)
prefix = b"\xc4" if testnet else b"\x05"
return encode_base58_checksum(prefix + h160)
return encode_base58_checksum(sh_prefix + h160)
raise ValueError("Unsupported address type.")
@ -730,9 +759,9 @@ class BIP32Node:
def hash160(self, compressed=True):
return self.node.public_key.h160(compressed)
def address(self, compressed=True, netcode="XTN", addr_fmt="p2pkh"):
def address(self, compressed=True, chain="XTN", addr_fmt="p2pkh"):
return self.node.public_key.address(compressed, addr_fmt=addr_fmt,
testnet=False if netcode == "BTC" else True)
chain=chain)
def sec(self, compressed=True):
return self.node.public_key.sec(compressed)

View File

@ -1,9 +1,9 @@
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
import pytest, time, sys, random, re, ndef, os, glob, hashlib, json, functools, io, math, pdb
import pytest, time, sys, random, re, ndef, os, glob, hashlib, json, functools, io, math, bech32, pdb
from subprocess import check_output
from ckcc.protocol import CCProtocolPacker
from helpers import B2A, U2SAT, hash160
from helpers import B2A, U2SAT, hash160, taptweak
from base58 import decode_base58_checksum
from bip32 import BIP32Node
from msg import verify_message
@ -285,26 +285,30 @@ def addr_vs_path(master_xpub):
from bip32 import BIP32Node
from ckcc_protocol.constants import AF_CLASSIC, AFC_PUBKEY, AF_P2WPKH, AFC_SCRIPT
from ckcc_protocol.constants import AF_P2WPKH_P2SH, AF_P2SH, AF_P2WSH, AF_P2WSH_P2SH
from bech32 import bech32_decode, convertbits, Encoding
from bech32 import bech32_decode, convertbits, decode, Encoding
from hashlib import sha256
def doit(given_addr, path=None, addr_fmt=None, script=None, testnet=True):
def doit(given_addr, path=None, addr_fmt=None, script=None, chain="XTN"):
if not script:
try:
# prefer using xpub if we can
mk = BIP32Node.from_wallet_key(master_xpub)
if not testnet:
mk._netcode = "BTC"
sk = mk.subkey_for_path(path[2:])
mk._netcode = chain
sk = mk.subkey_for_path(path)
except:
mk = BIP32Node.from_wallet_key(simulator_fixed_tprv)
if not testnet:
mk._netcode = "BTC"
sk = mk.subkey_for_path(path[2:])
mk._netcode = chain
sk = mk.subkey_for_path(path)
if addr_fmt in {None, AF_CLASSIC}:
if addr_fmt == AF_P2TR:
tweaked_xonly = taptweak(sk.sec()[1:])
decoded = decode(given_addr[:2], given_addr)
assert not given_addr.startswith("bcrt") # regtest
assert tweaked_xonly == bytes(decoded[1])
elif addr_fmt in {None, AF_CLASSIC}:
# easy
assert sk.address(netcode="XTN" if testnet else "BTC") == given_addr
assert sk.address(chain=chain) == given_addr
elif addr_fmt & AFC_PUBKEY:
@ -352,7 +356,6 @@ def addr_vs_path(master_xpub):
return doit
@pytest.fixture(scope='module')
def capture_enabled(sim_eval):
# need to have sim_display imported early, see unix/frozen-modules/ckcc
@ -2173,6 +2176,30 @@ def txout_explorer(cap_story, press_cancel, need_keypress, is_q1):
return doit
@pytest.fixture
def validate_address():
# Check whether an address is covered by the given subkey
def doit(addr, sk):
if addr[0] in '1mn':
chain = "XTN" if addr[0] != "1" else "BTC"
assert addr == sk.address(addr_fmt="p2pkh", chain=chain)
elif addr[0:4] in {'bc1q', 'tb1q'}:
chain = "XTN" if addr[0:4] != 'bc1q' else "BTC"
assert addr == sk.address(addr_fmt="p2wpkh", chain=chain)
elif addr[0:6] == "bcrt1q":
assert addr == sk.address(addr_fmt="p2wpkh", chain="XRT")
elif addr[0:4] in {'bc1p', 'tb1p'}:
chain = "XTN" if addr[0:4] != 'bc1p' else "BTC"
assert addr == sk.address(addr_fmt="p2tr", chain=chain)
elif addr[0:6] == "bcrt1p":
assert addr == sk.address(addr_fmt="p2tr", chain="XRT")
elif addr[0] in '23':
chain = "XTN" if addr[0] != '3' else "BTC"
assert addr == sk.address(addr_fmt="p2sh-p2wpkh", chain=chain)
else:
raise ValueError(addr)
return doit
@pytest.fixture
def skip_if_useless_way(is_q1, nfc_disabled):

View File

@ -25,9 +25,11 @@ unmap_addr_fmt = {
'p2wsh': AF_P2WSH,
'p2wsh-p2sh': AF_P2WSH_P2SH,
'p2sh-p2wsh': AF_P2WSH_P2SH,
"p2tr": AF_P2TR,
}
msg_sign_unmap_addr_fmt = {
'p2tr': AF_P2TR, # not supported for msg signign tho
'p2pkh': AF_CLASSIC,
'p2wpkh': AF_P2WPKH,
'p2sh-p2wpkh': AF_P2WPKH_P2SH,
@ -35,6 +37,7 @@ msg_sign_unmap_addr_fmt = {
}
addr_fmt_names = {
AF_P2TR: 'p2tr',
AF_CLASSIC: 'p2pkh',
AF_P2SH: 'p2sh',
AF_P2WPKH: 'p2wpkh',
@ -45,10 +48,10 @@ addr_fmt_names = {
# all possible addr types, including multisig/scripts
ADDR_STYLES = ['p2wpkh', 'p2wsh', 'p2sh', 'p2pkh', 'p2wsh-p2sh', 'p2wpkh-p2sh']
ADDR_STYLES = ['p2wpkh', 'p2wsh', 'p2sh', 'p2pkh', 'p2wsh-p2sh', 'p2wpkh-p2sh', 'p2tr']
# single-signer
ADDR_STYLES_SINGLE = ['p2wpkh', 'p2pkh', 'p2wpkh-p2sh']
ADDR_STYLES_SINGLE = ['p2wpkh', 'p2pkh', 'p2wpkh-p2sh', 'p2tr']
# multi signer
ADDR_STYLES_MS = ['p2sh', 'p2wsh', 'p2wsh-p2sh']

Binary file not shown.

View File

@ -0,0 +1 @@
70736274ff010071020000000127744ababf3027fe0d6cf23a96eee2efb188ef52301954585883e69b6624b2420000000000ffffffff02787c01000000000016001483a7e34bd99ff03a4962ef8a1a101bb295461ece606b042a010000001600147ac369df1b20e033d6116623957b0ac49f3c52e8000000000001012b00f2052a010000002251205a2c2cf5b52cf31f83ad2e8da63ff03183ecd8f609c7510ae8a48e03910a075701133f173bb3d36c074afb716fec6307a069a2e450b995f3c82785945ab8df0e24260dcd703b0cbf34de399184a9481ac2b3586db6601f026a77f7e4938481bc3475000000

View File

@ -0,0 +1 @@
70736274ff010071020000000127744ababf3027fe0d6cf23a96eee2efb188ef52301954585883e69b6624b2420000000000ffffffff02787c01000000000016001483a7e34bd99ff03a4962ef8a1a101bb295461ece606b042a010000001600147ac369df1b20e033d6116623957b0ac49f3c52e8000000000001012b00f2052a010000002251205a2c2cf5b52cf31f83ad2e8da63ff03183ecd8f609c7510ae8a48e03910a0757011342173bb3d36c074afb716fec6307a069a2e450b995f3c82785945ab8df0e24260dcd703b0cbf34de399184a9481ac2b3586db6601f026a77f7e4938481bc34751701aa000000

View File

@ -0,0 +1 @@
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b6926315c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac06f7d62059e9497a1a4a267569d9876da60101aff38e3529b9b939ce7f91ae970115f2e490af7cc45c4f78511f36057ce5c5a5c56325a29fb44dfc203f356e1f80023202cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2acc00000

View File

@ -0,0 +1 @@
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b6926115c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac06f7d62059e9497a1a4a267569d9876da60101aff38e3529b9b939ce7f91ae970115f2e490af7cc45c4f78511f36057ce5c5a5c56325a29fb44dfc203f356e123202cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2acc00000

View File

@ -0,0 +1 @@
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b6924214022cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2cd970e15f53fc0c82f950fd560ffa919b76172be017368a89913af074f400b094089756aa3739ccc689ec0fcf3a360be32cc0b59b16e93a1e8bb4605726b2ca7a3ff706c4176649632b2cc68e1f912b8a578e3719ce7710885c7a966f49bcd43cb0000

View File

@ -0,0 +1 @@
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b69241142cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2cd970e15f53fc0c82f950fd560ffa919b76172be017368a89913af074f400b094289756aa3739ccc689ec0fcf3a360be32cc0b59b16e93a1e8bb4605726b2ca7a3ff706c4176649632b2cc68e1f912b8a578e3719ce7710885c7a966f49bcd43cb01010000

View File

@ -0,0 +1 @@
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b69241142cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2cd970e15f53fc0c82f950fd560ffa919b76172be017368a89913af074f400b093f89756aa3739ccc689ec0fcf3a360be32cc0b59b16e93a1e8bb4605726b2ca7a3ff706c4176649632b2cc68e1f912b8a578e3719ce7710885c7a966f49bcd430000

View File

@ -0,0 +1 @@
70736274ff010071020000000127744ababf3027fe0d6cf23a96eee2efb188ef52301954585883e69b6624b2420000000000ffffffff02787c01000000000016001483a7e34bd99ff03a4962ef8a1a101bb295461ece606b042a010000001600147ac369df1b20e033d6116623957b0ac49f3c52e8000000000001012b00f2052a010000002251205a2c2cf5b52cf31f83ad2e8da63ff03183ecd8f609c7510ae8a48e03910a0757221602fe349064c98d6e2a853fa3c9b12bd8b304a19c195c60efa7ee2393046d3fa2321900772b2da75600008001000080000000800100000000000000000000

View File

@ -24,13 +24,15 @@ def prandom(count):
return bytes(random.randint(0, 255) for i in range(count))
def taptweak(internal_key, tweak=None):
tweak = internal_key if tweak is None else internal_key + tweak
assert len(internal_key) == 32, "not xonly-pubkey (len!=32)"
if tweak is not None:
assert len(tweak) == 32, "tweak (len!=32)"
tweak = internal_key if tweak is None else internal_key + tweak
xonly_pubkey = xonly_pubkey_parse(internal_key)
tweak = tagged_sha256(b"TapTweak", tweak)
tweaked_pubkey = xonly_pubkey_tweak_add(xonly_pubkey, tweak)
tweaked_xonnly_pubkey, parity = xonly_pubkey_from_pubkey(tweaked_pubkey)
return xonly_pubkey_serialize(tweaked_xonnly_pubkey)
tweaked_xonly_pubkey, parity = xonly_pubkey_from_pubkey(tweaked_pubkey)
return xonly_pubkey_serialize(tweaked_xonly_pubkey)
def fake_dest_addr(style='p2pkh'):
# Make a plausible output address, but it's random garbage. Cant use for change outs

View File

@ -123,6 +123,9 @@ class BasicPSBTInput(PSBTSection):
self.taproot_bip32_paths = {}
self.taproot_internal_key = None
self.taproot_key_sig = None
self.taproot_merkle_root = None
self.taproot_scripts = {}
self.taproot_script_sigs = {}
self.redeem_script = None
self.witness_script = None
self.previous_txid = None # v2
@ -147,6 +150,9 @@ class BasicPSBTInput(PSBTSection):
a.taproot_key_sig == b.taproot_key_sig and \
a.taproot_bip32_paths == b.taproot_bip32_paths and \
a.taproot_internal_key == b.taproot_internal_key and \
a.taproot_merkle_root == b.taproot_merkle_root and \
a.taproot_scripts == b.taproot_scripts and \
a.taproot_script_sigs == b.taproot_script_sigs and \
sorted(a.part_sigs.keys()) == sorted(b.part_sigs.keys()) and \
a.previous_txid == b.previous_txid and \
a.prevout_idx == b.prevout_idx and \
@ -189,7 +195,7 @@ class BasicPSBTInput(PSBTSection):
self.others[kt] = val
elif kt == PSBT_IN_TAP_BIP32_DERIVATION:
self.taproot_bip32_paths[key] = val
elif kt == PSBT_OUT_TAP_INTERNAL_KEY:
elif kt == PSBT_IN_TAP_INTERNAL_KEY:
self.taproot_internal_key = val
elif kt == PSBT_IN_TAP_KEY_SIG:
self.taproot_key_sig = val
@ -203,6 +209,21 @@ class BasicPSBTInput(PSBTSection):
self.req_time_locktime = struct.unpack("<I", val)[0]
elif kt == PSBT_IN_REQUIRED_HEIGHT_LOCKTIME:
self.req_height_locktime = struct.unpack("<I", val)[0]
elif kt == PSBT_IN_TAP_SCRIPT_SIG:
assert len(key) == 64, "PSBT_IN_TAP_SCRIPT_SIG key length != 64"
assert len(val) in (64, 65), "PSBT_IN_TAP_SCRIPT_SIG signature length != 64 or 65"
xonly_pubkey, script_hash = key[:32], key[32:]
self.taproot_script_sigs[(xonly_pubkey, script_hash)] = val
elif kt == PSBT_IN_TAP_LEAF_SCRIPT:
assert len(key) > 32, "PSBT_IN_TAP_LEAF_SCRIPT control block is too short"
assert (len(key) - 1) % 32 == 0, "PSBT_IN_TAP_LEAF_SCRIPT control block is not valid"
assert len(val) != 0, "PSBT_IN_TAP_LEAF_SCRIPT cannot be empty"
leaf_script = (val[:-1], int(val[-1]))
if leaf_script not in self.taproot_scripts:
self.taproot_scripts[leaf_script] = set()
self.taproot_scripts[leaf_script].add(key)
elif kt == PSBT_IN_TAP_MERKLE_ROOT:
self.taproot_merkle_root = val
else:
self.unknown[bytes([kt]) + key] = val
@ -236,6 +257,16 @@ class BasicPSBTInput(PSBTSection):
if self.taproot_key_sig:
wr(PSBT_IN_TAP_KEY_SIG, self.taproot_key_sig)
if self.taproot_merkle_root:
wr(PSBT_IN_TAP_MERKLE_ROOT, self.taproot_merkle_root)
if self.taproot_scripts:
for (script, leaf_ver), control_blocks in self.taproot_scripts.items():
for control_block in control_blocks:
wr(PSBT_IN_TAP_LEAF_SCRIPT, script + struct.pack("B", leaf_ver), control_block)
if self.taproot_script_sigs:
for (xonly, leaf_hash), sig in self.taproot_script_sigs.items():
wr(PSBT_IN_TAP_SCRIPT_SIG, sig, xonly + leaf_hash)
if v2:
if self.previous_txid is not None:
wr(PSBT_IN_PREVIOUS_TXID, self.previous_txid)
@ -267,6 +298,7 @@ class BasicPSBTOutput(PSBTSection):
self.bip32_paths = {}
self.taproot_bip32_paths = {}
self.taproot_internal_key = None
self.taproot_tree = None
self.script = None # v2
self.amount = None # v2
self.proprietary = {}
@ -282,6 +314,7 @@ class BasicPSBTOutput(PSBTSection):
a.taproot_bip32_paths == b.taproot_bip32_paths and \
a.taproot_internal_key == b.taproot_internal_key and \
a.proprietary == b.proprietary and \
a.taproot_tree == b.taproot_tree and \
a.unknown == b.unknown
def parse_kv(self, kt, key, val):
@ -297,6 +330,18 @@ class BasicPSBTOutput(PSBTSection):
self.taproot_bip32_paths[key] = val
elif kt == PSBT_OUT_TAP_INTERNAL_KEY:
self.taproot_internal_key = val
elif kt == PSBT_OUT_TAP_TREE:
res = []
reader = io.BytesIO(val)
while True:
depth = reader.read(1)
if not depth:
break
leaf_version = reader.read(1)[0]
script_len = deser_compact_size(reader)
script = reader.read(script_len)
res.append((depth[0], leaf_version, script))
self.taproot_tree = res
elif kt == PSBT_OUT_SCRIPT:
self.script = val
elif kt == PSBT_OUT_AMOUNT:
@ -319,6 +364,11 @@ class BasicPSBTOutput(PSBTSection):
wr(PSBT_OUT_TAP_BIP32_DERIVATION, self.taproot_bip32_paths[k], k)
if self.taproot_internal_key:
wr(PSBT_OUT_TAP_INTERNAL_KEY, self.taproot_internal_key)
if self.taproot_tree:
res = b''
for depth, leaf_version, script in self.taproot_tree:
res += bytes([depth, leaf_version]) + ser_compact_size(len(script)) + script
wr(PSBT_OUT_TAP_TREE, res)
if v2 and self.script is not None:
wr(PSBT_OUT_SCRIPT, self.script)
if v2 and self.amount is not None:

View File

@ -12,7 +12,7 @@ from charcodes import KEY_QR
from constants import msg_sign_unmap_addr_fmt
@pytest.mark.parametrize('path', [ 'm', "m/1/2", "m/1'/100'"])
@pytest.mark.parametrize('addr_fmt', [ AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH ])
@pytest.mark.parametrize('addr_fmt', [ AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR ])
def test_show_addr_usb(dev, press_select, addr_vs_path, path, addr_fmt, is_simulator):
addr = dev.send_recv(CCProtocolPacker.show_address(path, addr_fmt), timeout=None)
@ -27,7 +27,7 @@ def test_show_addr_usb(dev, press_select, addr_vs_path, path, addr_fmt, is_simul
@pytest.mark.qrcode
@pytest.mark.parametrize('path', [ 'm', "m/1/2", "m/1'/100'", "m/0h/500h"])
@pytest.mark.parametrize('addr_fmt', [ AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH ])
@pytest.mark.parametrize('addr_fmt', [ AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR ])
def test_show_addr_displayed(dev, need_keypress, addr_vs_path, path, addr_fmt,
cap_story, cap_screen_qr, qr_quality_check,
press_cancel, is_q1):
@ -60,25 +60,40 @@ def test_show_addr_displayed(dev, need_keypress, addr_vs_path, path, addr_fmt,
assert qr == addr or qr == addr.upper()
@pytest.mark.bitcoind
def test_addr_vs_bitcoind(use_regtest, press_select, dev, bitcoind_d_sim_sign):
@pytest.mark.parametrize("addr_fmt", [
(AF_CLASSIC, "legacy"),
(AF_P2WPKH_P2SH, "p2sh-segwit"),
(AF_P2WPKH, "bech32"),
(AF_P2TR, "bech32m")
])
def test_addr_vs_bitcoind(addr_fmt, use_regtest, press_select, dev, bitcoind_d_sim_sign):
# check our p2wpkh wrapped in p2sh is right
use_regtest()
addr_fmt, addr_fmt_bitcoind = addr_fmt
for i in range(5):
core_addr = bitcoind_d_sim_sign.getnewaddress(f"{i}-addr", "p2sh-segwit")
assert core_addr[0] == '2'
core_addr = bitcoind_d_sim_sign.getnewaddress(f"{i}-addr", addr_fmt_bitcoind)
resp = bitcoind_d_sim_sign.getaddressinfo(core_addr)
assert resp['embedded']['iswitness'] == True
assert resp['isscript'] == True
assert resp["ismine"] is True
if addr_fmt in (AF_P2TR, AF_P2WPKH):
wit_ver = resp["witness_version"]
if addr_fmt == AF_P2TR:
assert wit_ver == 1
else:
assert wit_ver == 0
assert resp["iswitness"] is True
if addr_fmt == AF_P2WPKH_P2SH:
assert resp['embedded']['iswitness'] is True
assert resp['isscript'] is True
assert resp['embedded']['witness_version'] == 0
path = resp['hdkeypath']
addr = dev.send_recv(CCProtocolPacker.show_address(path, AF_P2WPKH_P2SH), timeout=None)
addr = dev.send_recv(CCProtocolPacker.show_address(path, addr_fmt), timeout=None)
press_select()
assert addr == core_addr
@pytest.mark.parametrize("body_err", [
("m\np2wsh", "Invalid address format: 'p2wsh'"),
("m\np2sh-p2wsh", "Invalid address format: 'p2sh-p2wsh'"),
("m\np2tr", "Invalid address format: 'p2tr'"),
("m\np2wsh", "Unsupported address format: 'p2wsh'"),
("m\np2sh-p2wsh", "Unsupported address format: 'p2sh-p2wsh'"),
("m/0/0/0/0/0/0/0/0/0/0/0/0/0\np2pkh", "too deep"),
("m/0/0/0/0/0/q/0/0/0\np2pkh", "invalid characters"),
])
@ -94,7 +109,7 @@ def test_show_addr_nfc_invalid(body_err, goto_home, pick_menu_item, nfc_write_te
assert err in story
@pytest.mark.parametrize("path", ["m/84'/0'/0'/300/0", "m/800h/0h", "m/0/0/0/0/1/1/1"])
@pytest.mark.parametrize("str_addr_fmt", ["p2pkh", "", "p2wpkh", "p2wpkh-p2sh", "p2sh-p2wpkh"])
@pytest.mark.parametrize("str_addr_fmt", ["p2pkh", "", "p2wpkh", "p2wpkh-p2sh", "p2sh-p2wpkh", "p2tr"])
def test_show_addr_nfc(path, str_addr_fmt, nfc_write_text, nfc_read_text, pick_menu_item,
goto_home, cap_story, press_nfc, addr_vs_path, press_select, is_q1,
cap_screen):
@ -142,4 +157,59 @@ def test_show_addr_nfc(path, str_addr_fmt, nfc_write_text, nfc_read_text, pick_m
assert story_addr == addr
addr_vs_path(addr, path, addr_fmt)
# EOF
def test_bip86(dev, set_seed_words, use_mainnet, need_keypress):
# https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
set_seed_words(mnemonic)
use_mainnet()
path = "m/86'/0'/0'"
xp = dev.send_recv(CCProtocolPacker.get_xpub(path), timeout=None)
# xprv = "xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk"
xpub = "xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ"
assert xp == xpub
# Account 0, first receiving
path = "m/86'/0'/0'/0/0"
addr = dev.send_recv(CCProtocolPacker.show_address(path, AF_P2TR), timeout=None)
need_keypress('y')
xp = dev.send_recv(CCProtocolPacker.get_xpub(path), timeout=None)
# xprv = "xprvA449goEeU9okwCzzZaxiy475EQGQzBkc65su82nXEvcwzfSskb2hAt2WymrjyRL6kpbVTGL3cKtp9herYXSjjQ1j4stsXXiRF7kXkCacK3T"
xpub = "xpub6H3W6JmYJXN49h5TfcVjLC3onS6uPeUTTJoVvRC8oG9vsTn2J8LwigLzq5tHbrwAzH9DGo6ThGUdWsqce8dGfwHVBxSbixjDADGGdzF7t2B"
# internal_key = "cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115"
# output_key = "a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c"
# scriptPubKey = "5120a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c"
address = "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr"
assert xp == xpub
assert addr == address
# Account 0, second receiving
path = "m/86'/0'/0'/0/1"
addr = dev.send_recv(CCProtocolPacker.show_address(path, AF_P2TR), timeout=None)
need_keypress('y')
xp = dev.send_recv(CCProtocolPacker.get_xpub(path), timeout=None)
# xprv = "xxprvA449goEeU9okyiF1LmKiDaTgeXvmh87DVyRd35VPbsSop8n8uALpbtrUhUXByPFKK7C2yuqrB1FrhiDkEMC4RGmA5KTwsE1aB5jRu9zHsuQ"
xpub = "xpub6H3W6JmYJXN4CCKUSnriaiQRCZmG6aq4sCMDqTu1ACyngw7HShf59hAxYjXgKDuuHThVEUzdHrc3aXCr9kfvQvZPit5dnD3K9xVRBzjK3rX"
# internal_key = "83dfe85a3151d2517290da461fe2815591ef69f2b18a2ce63f01697a8b313145"
# output_key = "a82f29944d65b86ae6b5e5cc75e294ead6c59391a1edc5e016e3498c67fc7bbb"
# scriptPubKey = "5120a82f29944d65b86ae6b5e5cc75e294ead6c59391a1edc5e016e3498c67fc7bbb"
address = "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh"
assert xp == xpub
assert addr == address
# Account 0, first change
path = "m/86'/0'/0'/1/0"
addr = dev.send_recv(CCProtocolPacker.show_address(path, AF_P2TR), timeout=None)
need_keypress('y')
xp = dev.send_recv(CCProtocolPacker.get_xpub(path), timeout=None)
# xprv = "xprvA3Ln3Gt3aphvUgzgEDT8vE2cYqb4PjFfpmbiFKphxLg1FjXQpkAk5M1ZKDY15bmCAHA35jTiawbFuwGtbDZogKF1WfjwxML4gK7WfYW5JRP"
xpub = "xpub6GL8SnQwRCGDhB59LEz9HMyM6sRYoByXBzXK3iEKWgCz8XrZNHUzd9L3AUBELW5NzA7dEFvMas1F84TuPH3xqdUA5tumaGWFgihJzWytXe3"
# internal_key = "399f1b2f4393f29a18c937859c5dd8a77350103157eb880f02e8c08214277cef"
# output_key = "882d74e5d0572d5a816cef0041a96b6c1de832f6f9676d9605c44d5e9a97d3dc"
# scriptPubKey = "5120882d74e5d0572d5a816cef0041a96b6c1de832f6f9676d9605c44d5e9a97d3dc"
address = "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7"
assert xp == xpub
assert addr == address
# EOF

View File

@ -24,9 +24,10 @@ def mk_common_derivations():
# Removed in v4.1.3: ( "m/{change}/{idx}", AF_CLASSIC ),
#( "m/{account}'/{change}'/{idx}'", AF_CLASSIC ),
#( "m/{account}'/{change}'/{idx}'", AF_P2WPKH ),
( "m/44h/{coin_type}h/{account}h/{change}/{idx}".replace('{coin_type}', coin_type), AF_CLASSIC ),
( "m/49h/{coin_type}h/{account}h/{change}/{idx}".replace('{coin_type}', coin_type), AF_P2WPKH_P2SH ),
( "m/84h/{coin_type}h/{account}h/{change}/{idx}".replace('{coin_type}', coin_type), AF_P2WPKH )
("m/44h/{coin_type}h/{account}h/{change}/{idx}".replace('{coin_type}', coin_type), AF_CLASSIC),
("m/49h/{coin_type}h/{account}h/{change}/{idx}".replace('{coin_type}', coin_type), AF_P2WPKH_P2SH),
("m/84h/{coin_type}h/{account}h/{change}/{idx}".replace('{coin_type}', coin_type), AF_P2WPKH),
("m/86h/{coin_type}h/{account}h/{change}/{idx}".replace('{coin_type}', coin_type), AF_P2TR),
]
return doit
@ -57,32 +58,14 @@ def parse_display_screen(cap_story, is_mark3):
return d
return doit
@pytest.fixture
def validate_address():
# Check whether an address is covered by the given subkey
def doit(addr, sk):
if addr[0] in '1mn':
assert addr == sk.address()
elif addr[0:3] in { 'bc1', 'tb1' }:
h20 = sk.hash160()
assert addr == bech32.encode(addr[0:2], 0, h20)
elif addr[0:5] == "bcrt1":
h20 = sk.hash160()
assert addr == bech32.encode(addr[0:4], 0, h20)
elif addr[0] in '23':
h20 = hash160(b'\x00\x14' + sk.hash160())
assert h20 == decode_base58_checksum(addr)[1:]
else:
raise ValueError(addr)
return doit
@pytest.fixture
def generate_addresses_file(goto_address_explorer, need_keypress, cap_story, microsd_path,
virtdisk_path, nfc_read_text, load_export_and_verify_signature,
press_select, press_nfc):
press_select, press_nfc, load_export):
# Generates the address file through the simulator, reads the file and
# returns a list of tuples of the form (subpath, address)
def doit(start_idx=0, way="sd", change=False, is_custom_single=False):
def doit(start_idx=0, way="sd", change=False, is_custom_single=False, is_p2tr=False):
expected_qty = 250 if way != "nfc" else 10
if (start_idx + expected_qty) > MAX_BIP32_IDX:
expected_qty = (MAX_BIP32_IDX - start_idx) + 1
@ -92,7 +75,8 @@ def generate_addresses_file(goto_address_explorer, need_keypress, cap_story, mic
if change and not is_custom_single:
need_keypress("0")
if way == "sd":
need_keypress('1')
if "Press (1)" in story:
need_keypress('1')
elif way == "vdisk":
if "save to Virtual Disk" not in story:
raise pytest.skip("Vdisk disabled")
@ -112,7 +96,12 @@ def generate_addresses_file(goto_address_explorer, need_keypress, cap_story, mic
time.sleep(.5) # always long enough to write the file?
title, body = cap_story()
contents, sig_addr = load_export_and_verify_signature(body, way, label="Address summary")
if is_p2tr:
# p2tr - no signature file
contents = load_export(way, label="Address summary", is_json=False, sig_check=False)
sig_addr = None
else:
contents, sig_addr = load_export_and_verify_signature(body, way, label="Address summary")
addr_dump = io.StringIO(contents)
cc = csv.reader(addr_dump)
hdr = next(cc)
@ -120,7 +109,8 @@ def generate_addresses_file(goto_address_explorer, need_keypress, cap_story, mic
for n, (idx, addr, deriv) in enumerate(cc, start=start_idx):
assert int(idx) == n
if n == start_idx:
assert sig_addr == addr
if sig_addr:
assert sig_addr == addr
if not is_custom_single:
assert ('/%s' % idx) in deriv
@ -272,7 +262,7 @@ def test_address_display(goto_address_explorer, parse_display_screen, mk_common_
press_cancel() # back
@pytest.mark.parametrize('click_idx', ["Classic P2PKH", "P2SH-Segwit", "Segwit P2WPKH"])
@pytest.mark.parametrize('click_idx', ["Classic P2PKH", "P2SH-Segwit", "Segwit P2WPKH", 'Taproot P2TR'])
@pytest.mark.parametrize("change", [True, False])
@pytest.mark.parametrize("start_idx", [MAX_BIP32_IDX, 80965, 0])
@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc"])
@ -289,7 +279,8 @@ def test_dump_addresses(way, change, generate_addresses_file, mk_common_derivati
set_addr_exp_start_idx(start_idx)
pick_menu_item(click_idx)
# Generate the addresses file and get each line in a list
for subpath, addr in generate_addresses_file(way=way, start_idx=start_idx, change=change):
is_p2tr = click_idx == 'Taproot P2TR'
for subpath, addr in generate_addresses_file(way=way, start_idx=start_idx, change=change, is_p2tr=is_p2tr):
# derive the subkey and validate the corresponding address
assert subpath.split("/")[-2] == "1" if change else "0"
sk = node_prv.subkey_for_path(subpath)
@ -336,7 +327,7 @@ def test_account_menu(way, account_num, sim_execfile, pick_menu_item,
# derive index=0 address
assert '{account}' in path
subpath = path.format(account=account_num, change=0, idx=start_idx) # e.g. "m/44'/1'/X'/0/0"
subpath = path.format(account=account_num, change=0, idx=start_idx, is_p2tr=addr_format==AF_P2TR) # e.g. "m/44'/1'/X'/0/0"
sk = node_prv.subkey_for_path(subpath)
# capture full index=0 address from display screen & validate it
@ -357,7 +348,7 @@ def test_account_menu(way, account_num, sim_execfile, pick_menu_item,
assert expected_addr.startswith(start)
assert expected_addr.endswith(end)
for subpath, addr in generate_addresses_file(way=way, start_idx=start_idx):
for subpath, addr in generate_addresses_file(way=way, start_idx=start_idx,is_p2tr=addr_format==AF_P2TR):
assert subpath.split('/')[-3] == str(account_num)+"h"
sk = node_prv.subkey_for_path(subpath)
validate_address(addr, sk)
@ -378,7 +369,7 @@ def test_account_menu(way, account_num, sim_execfile, pick_menu_item,
("m/1/2/3/4/5", MAX_BIP32_IDX),
("m/1h/2h/3h/4h/5h", 0),
])
@pytest.mark.parametrize('which_fmt', [ AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH ])
@pytest.mark.parametrize('which_fmt', [ AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR])
def test_custom_path(path_sidx, which_fmt, addr_vs_path, pick_menu_item, goto_address_explorer,
need_keypress, cap_menu, parse_display_screen, validate_address,
cap_screen_qr, qr_quality_check, nfc_read_text, get_setting,
@ -445,12 +436,14 @@ def test_custom_path(path_sidx, which_fmt, addr_vs_path, pick_menu_item, goto_ad
m = cap_menu()
assert m[0] == 'Classic P2PKH'
assert m[1] == 'Segwit P2WPKH'
assert m[2] == 'P2SH-Segwit'
assert m[2] == 'Taproot P2TR'
assert m[3] == 'P2SH-Segwit'
fmts = {
AF_CLASSIC: 'Classic P2PKH',
AF_P2WPKH: 'Segwit P2WPKH',
AF_P2WPKH_P2SH: 'P2SH-Segwit',
AF_P2TR: 'Taproot P2TR',
}
pick_menu_item(fmts[which_fmt])
@ -475,7 +468,7 @@ def test_custom_path(path_sidx, which_fmt, addr_vs_path, pick_menu_item, goto_ad
need_keypress(KEY_QR if is_q1 else '4')
qr = cap_screen_qr().decode('ascii')
if which_fmt == AF_P2WPKH:
if which_fmt in (AF_P2WPKH, AF_P2TR):
assert qr == addr.upper()
else:
assert qr == addr
@ -497,7 +490,7 @@ def test_custom_path(path_sidx, which_fmt, addr_vs_path, pick_menu_item, goto_ad
# remove QR from screen
press_cancel()
addr_gen = generate_addresses_file(change=False, is_custom_single=True)
addr_gen = generate_addresses_file(change=False, is_custom_single=True, is_p2tr=which_fmt == AF_P2TR)
f_path, f_addr = next(addr_gen)
assert f_path == path
assert f_addr == addr
@ -526,7 +519,7 @@ def test_custom_path(path_sidx, which_fmt, addr_vs_path, pick_menu_item, goto_ad
need_keypress(KEY_QR if is_q1 else '4')
for i in range(n):
qr = cap_screen_qr().decode('ascii')
if which_fmt == AF_P2WPKH:
if which_fmt in (AF_P2WPKH, AF_P2TR):
qr = qr.lower()
qr_addr_list.append(qr)
need_keypress(KEY_RIGHT if is_q1 else "9")
@ -539,11 +532,92 @@ def test_custom_path(path_sidx, which_fmt, addr_vs_path, pick_menu_item, goto_ad
assert sorted(qr_addr_list) == sorted(addr_dict.values())
addr_gen = generate_addresses_file(start_idx=start_idx, change=False)
addr_gen = generate_addresses_file(start_idx=start_idx, change=False, is_p2tr=which_fmt==AF_P2TR)
assert addr_dict == {p: a for i,(p, a) in enumerate(addr_gen) if i < n}
# check the rest of file export
for p, a in addr_gen:
addr_vs_path(a, p, addr_fmt=which_fmt)
@pytest.mark.bitcoind
@pytest.mark.parametrize("addr_fmt", [AF_P2WPKH, AF_P2WPKH_P2SH, AF_CLASSIC, AF_P2TR])
@pytest.mark.parametrize("acct_num", [None, "999"])
def test_bitcoind_descriptor_address(addr_fmt, acct_num, bitcoind, goto_home, pick_menu_item, cap_story,
use_regtest, need_keypress, microsd_path, generate_addresses_file,
bitcoind_d_wallet_w_sk, load_export, settings_set, cap_menu,
goto_address_explorer, press_cancel, press_select, enter_number):
# export single sig descriptors (external, internal)
# export addressses from address explorer
# derive addresses from descriptor with bitcoind
# compare bitcoind derived addressses with those exported from address explorer
bitcoind = bitcoind_d_wallet_w_sk
use_regtest()
goto_home()
pick_menu_item("Advanced/Tools")
pick_menu_item("Export Wallet")
pick_menu_item("Descriptor")
time.sleep(.1)
_, story = cap_story()
assert "This saves a ranged xpub descriptor" in story
assert "Press (1) to enter a non-zero account number" in story
assert "sensitive--in terms of privacy" in story
assert "not compromise your funds directly" in story
if isinstance(acct_num, str):
need_keypress("1") # chosse account number
for ch in acct_num:
need_keypress(ch) # input num
press_select() # confirm selection
else:
press_select() # confirm story
time.sleep(.1)
_, story = cap_story()
assert "press (1) to export receiving and change descriptors separately" in story
need_keypress("1")
sig_check = True
if addr_fmt == AF_P2WPKH:
menu_item = "Segwit P2WPKH"
desc_prefix = "wpkh("
elif addr_fmt == AF_P2WPKH_P2SH:
menu_item = "P2SH-Segwit"
desc_prefix = "sh(wpkh("
elif addr_fmt == AF_P2TR:
menu_item = "Taproot P2TR"
desc_prefix = "tr("
sig_check = False
else:
# addr_fmt == AF_CLASSIC:
menu_item = "Classic P2PKH"
desc_prefix = "pkh("
pick_menu_item(menu_item)
contents = load_export("sd", label="Descriptor", is_json=False, addr_fmt=addr_fmt,
sig_check=sig_check)
descriptors = contents.strip()
ext_desc, int_desc = descriptors.split("\n")
assert ext_desc.startswith(desc_prefix)
assert int_desc.startswith(desc_prefix)
# check both external and internal
for chng in [False, True]:
goto_address_explorer()
if acct_num:
menu = cap_menu()
# can be "Account number" or "Account: N"
mi = [m for m in menu if "Account" in m]
assert len(mi) == 1
pick_menu_item(mi[0])
enter_number(acct_num)
desc = int_desc if chng else ext_desc
settings_set("axi", 0)
pick_menu_item(menu_item)
cc_addrs_gen = generate_addresses_file(change=chng, is_p2tr=addr_fmt == AF_P2TR)
cc_addrs = [addr for deriv, addr in cc_addrs_gen]
bitcoind_addrs = bitcoind.deriveaddresses(desc, [0, 249])
assert cc_addrs == bitcoind_addrs
# EOF

View File

@ -305,7 +305,7 @@ def test_export_electrum(way, dev, mode, acct_num, pick_menu_item, goto_home, ca
@pytest.mark.parametrize('acct_num', [ None, '99', '1236'])
@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc", "qr"])
@pytest.mark.parametrize('testnet', [True, False])
@pytest.mark.parametrize('chain', ["BTC", "XTN"])
@pytest.mark.parametrize('app', [
# no need to run them all - just name check differs
("Generic JSON", "Generic Export"),
@ -317,12 +317,12 @@ def test_export_electrum(way, dev, mode, acct_num, pick_menu_item, goto_home, ca
])
def test_export_coldcard(way, dev, acct_num, app, pick_menu_item, goto_home, cap_story, need_keypress,
microsd_path, nfc_read_json, virtdisk_path, addr_vs_path, enter_number,
load_export, testnet, use_mainnet, press_select,
load_export, chain, use_mainnet, press_select,
skip_if_useless_way, expect_acctnum_captured):
skip_if_useless_way(way)
if not testnet:
if chain == "BTC":
use_mainnet()
export_mi, app_f_name = app
@ -377,8 +377,8 @@ def test_export_coldcard(way, dev, acct_num, app, pick_menu_item, goto_home, cap
addr = v.get('first', None)
if fn == 'bip44':
assert first.address(netcode="XTN" if testnet else "BTC") == v['first']
addr_vs_path(addr, v['deriv'] + '/0/0', AF_CLASSIC, testnet=testnet)
assert first.address(chain=chain) == v['first']
addr_vs_path(addr, v['deriv'] + '/0/0', AF_CLASSIC, chain=chain)
elif ('bip48_' in fn) or (fn == 'bip45'):
# multisig: cant do addrs
assert addr == None
@ -389,11 +389,11 @@ def test_export_coldcard(way, dev, acct_num, app, pick_menu_item, goto_home, cap
h20 = first.hash160()
if fn == 'bip84':
assert addr == bech32.encode(addr[0:2], 0, h20)
addr_vs_path(addr, v['deriv'] + '/0/0', AF_P2WPKH, testnet=testnet)
addr_vs_path(addr, v['deriv'] + '/0/0', AF_P2WPKH, chain=chain)
elif fn == 'bip49':
# don't have test logic for verifying these addrs
# - need to make script, and bleh
assert first.address(addr_fmt="p2sh-p2wpkh", netcode="XTN" if testnet else "BTC") == v['first']
assert first.address(addr_fmt="p2sh-p2wpkh", chain=chain) == v['first']
else:
assert False
@ -455,15 +455,14 @@ def test_export_unchained(way, dev, pick_menu_item, goto_home, cap_story, need_k
@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc", "qr"])
@pytest.mark.parametrize('testnet', [True, False])
@pytest.mark.parametrize('chain', ["BTC", "XTN"])
def test_export_public_txt(way, dev, pick_menu_item, goto_home, press_select, microsd_path,
addr_vs_path, virtdisk_path, nfc_read_text, cap_story, use_mainnet,
load_export, testnet, skip_if_useless_way):
addr_vs_path, virtdisk_path, nfc_read_text, cap_story, use_testnet,
load_export, chain, skip_if_useless_way):
# test UX and values produced.
skip_if_useless_way(way)
if not testnet:
use_mainnet()
use_testnet(chain == "XTN")
goto_home()
pick_menu_item('Advanced/Tools')
pick_menu_item('File Management')
@ -481,7 +480,7 @@ def test_export_public_txt(way, dev, pick_menu_item, goto_home, press_select, mi
xfp = xfp2str(simulator_fixed_xfp).upper()
ek = simulator_fixed_tprv if testnet else simulator_fixed_xprv
ek = simulator_fixed_tprv if chain == "XTN" else simulator_fixed_xprv
root = BIP32Node.from_wallet_key(ek)
for ln in fp:
@ -508,14 +507,16 @@ def test_export_public_txt(way, dev, pick_menu_item, goto_home, press_select, mi
if not f:
if rhs[0] in '1mn':
f = AF_CLASSIC
elif rhs[0:3] in ['tb1', "bc1"]:
elif rhs[0:4] in ['tb1q', "bc1q"]:
f = AF_P2WPKH
elif rhs[0:4] in ['tb1p', "bc1p"]:
f = AF_P2TR
elif rhs[0] in '23':
f = AF_P2WPKH_P2SH
else:
raise ValueError(rhs)
addr_vs_path(rhs, path=lhs, addr_fmt=f, testnet=testnet)
addr_vs_path(rhs, path=lhs, addr_fmt=f, chain=chain)
@pytest.mark.qrcode
@ -538,6 +539,8 @@ def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home
is_xfp = False
if '-84' in m:
expect = "m/84h/0h/{acct}h"
elif '86' in m and 'P2TR' in m:
expect = "m/86h/0h/{acct}h"
elif '-44' in m:
expect = "m/44h/0h/{acct}h"
elif '49' in m:
@ -603,7 +606,7 @@ def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home
@pytest.mark.parametrize("chain", ["BTC", "XTN", "XRT"])
@pytest.mark.parametrize("way", ["sd", "vdisk", "nfc", "qr"])
@pytest.mark.parametrize("addr_fmt", [AF_P2WPKH, AF_P2WPKH_P2SH, AF_CLASSIC])
@pytest.mark.parametrize("addr_fmt", [AF_P2WPKH, AF_P2WPKH_P2SH, AF_CLASSIC, AF_P2TR])
@pytest.mark.parametrize("acct_num", [None, 0, 1, (2 ** 31) - 1])
@pytest.mark.parametrize("int_ext", [True, False])
def test_generic_descriptor_export(chain, addr_fmt, acct_num, goto_home,
@ -651,6 +654,10 @@ def test_generic_descriptor_export(chain, addr_fmt, acct_num, goto_home,
menu_item = "P2SH-Segwit"
desc_prefix = "sh(wpkh("
bip44_purpose = 49
elif addr_fmt == AF_P2TR:
menu_item = "Taproot P2TR"
desc_prefix = "tr("
bip44_purpose = 86
else:
# addr_fmt == AF_CLASSIC:
menu_item = "Classic P2PKH"
@ -662,7 +669,11 @@ def test_generic_descriptor_export(chain, addr_fmt, acct_num, goto_home,
expect_acctnum_captured(acct_num)
contents = load_export(way, label="Descriptor", is_json=False, addr_fmt=addr_fmt)
sig_check = True
if addr_fmt == AF_P2TR:
sig_check = False
contents = load_export(way, label="Descriptor", is_json=False, addr_fmt=addr_fmt,
sig_check=sig_check)
descriptor = contents.strip()
if int_ext is False:

View File

@ -594,7 +594,7 @@ def test_whitelist_single(dev, start_hsm, tweak_rule, attempt_psbt, fake_txn, wi
start_hsm(policy)
# try all addr types
for style in ['p2wpkh', 'p2wsh', 'p2sh', 'p2pkh', 'p2wsh-p2sh', 'p2wpkh-p2sh']:
for style in ['p2wpkh', 'p2wsh', 'p2sh', 'p2pkh', 'p2wsh-p2sh', 'p2wpkh-p2sh', 'p2tr']:
dests = []
psbt = fake_txn(1, 2, dev.master_xpub,
outstyles=[style, 'p2wpkh'],
@ -1537,7 +1537,8 @@ def test_op_return_output_local(op_return_data, start_hsm, attempt_psbt, fake_tx
def test_op_return_output_bitcoind(op_return_data, start_hsm, attempt_psbt, bitcoind_d_sim_watch, bitcoind, hsm_reset):
cc = bitcoind_d_sim_watch
dest_address = cc.getnewaddress()
bitcoind.supply_wallet.generatetoaddress(101, dest_address)
bitcoind.supply_wallet.sendtoaddress(dest_address, 49)
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
psbt = cc.walletcreatefundedpsbt([], [{dest_address: 1.0}, {"data": op_return_data.hex()}], 0, {"fee_rate": 20})["psbt"]
policy = DICT(rules=[dict(max_amount=10)])
start_hsm(policy)

View File

@ -358,6 +358,7 @@ def test_ms_import_variations(N, make_multisig, offer_ms_import, press_cancel, i
# the different addr formats
for af in unmap_addr_fmt.keys():
if af == "p2tr": continue
config = f'format: {af}\n'
config += '\n'.join(sk.hwif(as_private=False) for xfp,m,sk in keys)
title, story = offer_ms_import(config)
@ -1385,7 +1386,7 @@ def test_ms_sign_myself(M, use_regtest, make_myself_wallet, segwit, num_ins, dev
# IMPORTANT: wont work if you start simulator with --ms flag. Use no args
all_out_styles = list(unmap_addr_fmt.keys())
all_out_styles = [af for af in unmap_addr_fmt.keys() if af != "p2tr"]
num_outs = len(all_out_styles)
clear_ms()

View File

@ -5,9 +5,9 @@
import pytest, time, io, csv
from txn import fake_address
from base58 import encode_base58_checksum
from helpers import hash160
from helpers import hash160, taptweak
from bip32 import BIP32Node
from constants import AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
from constants import AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR
from constants import simulator_fixed_xprv, simulator_fixed_tprv, addr_fmt_names
@pytest.fixture
@ -23,7 +23,7 @@ def wipe_cache(sim_exec):
[14, 8, 26, 1, 7, 19]
'''
@pytest.mark.parametrize('addr_fmt', [
AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR
])
@pytest.mark.parametrize('testnet', [ False, True] )
def test_negative(addr_fmt, testnet, sim_exec):
@ -36,24 +36,26 @@ def test_negative(addr_fmt, testnet, sim_exec):
assert 'Explained' in lst
@pytest.mark.parametrize('addr_fmt, testnet', [
(AF_CLASSIC, True),
(AF_CLASSIC, False),
(AF_P2WPKH, True),
(AF_P2WPKH, False),
(AF_P2WPKH_P2SH, True),
(AF_P2WPKH_P2SH, False),
@pytest.mark.parametrize('addr_fmt, chain', [
(AF_CLASSIC, "XTN"),
(AF_CLASSIC, "BTC"),
(AF_P2WPKH, "XTN"),
(AF_P2WPKH, "BTC"),
(AF_P2WPKH_P2SH, "XTN"),
(AF_P2WPKH_P2SH, "BTC"),
(AF_P2TR, "XTN"),
(AF_P2TR, "BTC"),
# multisig - testnet only
(AF_P2WSH, True),
(AF_P2SH, True),
(AF_P2WSH_P2SH,True),
(AF_P2WSH, "XTN"),
(AF_P2SH, "XTN"),
(AF_P2WSH_P2SH, "XTN"),
])
@pytest.mark.parametrize('offset', [ 3, 760] )
@pytest.mark.parametrize('subaccount', [ 0, 34] )
@pytest.mark.parametrize('change_idx', [ 0, 1] )
@pytest.mark.parametrize('from_empty', [ True, False] )
def test_positive(addr_fmt, offset, subaccount, testnet, from_empty, change_idx,
def test_positive(addr_fmt, offset, subaccount, chain, from_empty, change_idx,
sim_exec, wipe_cache, make_myself_wallet, use_testnet, goto_home, pick_menu_item,
enter_number, press_cancel, settings_set, import_ms_wallet, clear_ms
):
@ -61,17 +63,23 @@ def test_positive(addr_fmt, offset, subaccount, testnet, from_empty, change_idx,
# API/Unit test, limited UX
if not testnet and addr_fmt in { AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH }:
# multisig jigs assume testnet
raise pytest.skip('testnet only')
if chain == "BTC":
use_testnet(False)
testnet = False
if addr_fmt in { AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH }:
# multisig jigs assume testnet
raise pytest.skip('testnet only')
coin_type = 0
if chain == "XTN":
use_testnet(True)
coin_type = 1
testnet = True
use_testnet(testnet)
if from_empty:
wipe_cache() # very different codepaths
settings_set('accts', [])
coin_type = 1 if testnet else 0
if addr_fmt in { AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH }:
from test_multisig import make_ms_address, HARD
M, N = 1, 3
@ -99,6 +107,9 @@ def test_positive(addr_fmt, offset, subaccount, testnet, from_empty, change_idx,
elif addr_fmt == AF_P2WPKH:
menu_item = expect_name = 'Segwit P2WPKH'
path = "m/84h/{ct}h/{acc}h"
elif addr_fmt == AF_P2TR:
menu_item = expect_name = 'Taproot P2TR'
path = "m/86h/{ct}h/{acc}h"
else:
raise ValueError(addr_fmt)
@ -108,14 +119,18 @@ def test_positive(addr_fmt, offset, subaccount, testnet, from_empty, change_idx,
# see addr_vs_path
mk = BIP32Node.from_wallet_key(simulator_fixed_tprv if testnet else simulator_fixed_xprv)
sk = mk.subkey_for_path(path[2:].replace('h', "'"))
sk = mk.subkey_for_path(path)
if addr_fmt == AF_CLASSIC:
addr = sk.address(netcode="XTN" if testnet else "BTC")
addr = sk.address(chain=chain)
elif addr_fmt == AF_P2WPKH_P2SH:
pkh = sk.hash160()
digest = hash160(b'\x00\x14' + pkh)
addr = encode_base58_checksum(bytes([196 if testnet else 5]) + digest)
elif addr_fmt == AF_P2TR:
from bech32 import encode
tweked_xonly = taptweak(sk.sec()[1:])
addr = encode("tb" if testnet else "bc", 1, tweked_xonly)
else:
pkh = sk.hash160()
addr = bech32_encode('tb' if testnet else 'bc', 0, pkh)
@ -166,7 +181,7 @@ def test_ux(valid, testnet, method,
mk = BIP32Node.from_wallet_key(simulator_fixed_tprv if testnet else simulator_fixed_xprv)
path = "m/44h/{ct}h/{acc}h/0/3".format(acc=0, ct=(1 if testnet else 0))
sk = mk.subkey_for_path(path)
addr = sk.address(netcode="XTN" if testnet else "BTC")
addr = sk.address(chain="XTN" if testnet else "BTC")
else:
addr = fake_address(addr_fmt, testnet)
@ -220,19 +235,19 @@ def test_ux(valid, testnet, method,
assert 'Searched ' in story
assert 'candidates without finding a match' in story
@pytest.mark.parametrize("af", ["P2SH-Segwit", "Segwit P2WPKH", "Classic P2PKH", "ms0"])
@pytest.mark.parametrize("af", ["P2SH-Segwit", "Segwit P2WPKH", "Classic P2PKH", "Taproot P2TR", "ms0"])
def test_address_explorer_saver(af, wipe_cache, settings_set, goto_address_explorer,
pick_menu_item, need_keypress, sim_exec, clear_ms,
import_ms_wallet, press_select, goto_home, nfc_write,
load_shared_mod, load_export_and_verify_signature,
cap_story):
cap_story, load_export):
goto_home()
wipe_cache()
settings_set('accts', [])
if af == "ms0":
clear_ms()
import_ms_wallet(2,3, name=af)
import_ms_wallet(2, 3, name=af)
press_select() # accept ms import
goto_address_explorer()
@ -249,7 +264,13 @@ def test_address_explorer_saver(af, wipe_cache, settings_set, goto_address_explo
return # multisig addresses are blanked
title, body = cap_story()
contents, sig_addr = load_export_and_verify_signature(body, "sd", label="Address summary")
if af == "Taproot P2TR":
# p2tr - no signature file
contents = load_export("sd", label="Address summary", is_json=False, sig_check=False)
sig_addr = None
else:
contents, sig_addr = load_export_and_verify_signature(body, "sd", label="Address summary")
addr_dump = io.StringIO(contents)
cc = csv.reader(addr_dump)
hdr = next(cc)

View File

@ -6,19 +6,19 @@
# This module can and should be run with `-l` and without it.
#
import pytest, time, os, shutil, re, random
import pytest, time, os, shutil, re, random, json
from binascii import a2b_hex
from hashlib import sha256
from bip32 import PrivateKey
from ckcc_protocol.constants import *
@pytest.mark.parametrize('mode', ["classic", 'segwit'])
@pytest.mark.parametrize('mode', ["classic", 'segwit', 'taproot'])
@pytest.mark.parametrize('pdf', [False, True])
@pytest.mark.parametrize('netcode', ["XTN", "BTC"])
@pytest.mark.parametrize('netcode', ["XRT", "BTC", "XTN"])
def test_generate(mode, pdf, netcode, dev, cap_menu, pick_menu_item, goto_home, cap_story,
need_keypress, microsd_path, verify_detached_signature_file, settings_set,
press_select):
press_select, validate_address, bitcoind):
# test UX and operation of the 'bitcoin core' wallet export
mx = "Don't make PDF"
@ -26,10 +26,7 @@ def test_generate(mode, pdf, netcode, dev, cap_menu, pick_menu_item, goto_home,
goto_home()
pick_menu_item('Advanced/Tools')
try:
pick_menu_item('Paper Wallets')
except:
raise pytest.skip('Feature absent')
pick_menu_item('Paper Wallets')
time.sleep(0.1)
title, story = cap_story()
@ -45,6 +42,11 @@ def test_generate(mode, pdf, netcode, dev, cap_menu, pick_menu_item, goto_home,
pick_menu_item('Segwit P2WPKH')
time.sleep(0.5)
if mode == 'taproot':
pick_menu_item('Classic P2PKH')
pick_menu_item('Taproot P2TR')
time.sleep(0.5)
if pdf:
assert mx in cap_menu()
shutil.copy('../docs/paperwallet.pdf', microsd_path('paperwallet.pdf'))
@ -58,7 +60,7 @@ def test_generate(mode, pdf, netcode, dev, cap_menu, pick_menu_item, goto_home,
time.sleep(0.1)
title, story = cap_story()
if "Press (1) to save paper wallet file to SD Card" in story:
if "Press (1)" in story:
need_keypress("1")
time.sleep(0.2)
title, story = cap_story()
@ -68,20 +70,32 @@ def test_generate(mode, pdf, netcode, dev, cap_menu, pick_menu_item, goto_home,
story = [i for i in story.split('\n') if i]
sig_file = story[-1]
if not pdf:
fname = story[-2]
fnames = [fname]
if mode == "taproot":
fname = story[-1]
else:
fname = story[-2]
fnames = [fname]
else:
fname = story[-3]
pdf_name = story[-2]
fnames = [fname, pdf_name]
if mode == "taproot":
fname = story[-2]
pdf_name = story[-1]
else:
fname = story[-3]
pdf_name = story[-2]
fnames = [fname, pdf_name]
assert pdf_name.endswith('.pdf')
assert fname.endswith('.txt')
assert sig_file.endswith(".sig")
verify_detached_signature_file(fnames, sig_file, "sd",
addr_fmt=AF_CLASSIC if mode == "classic" else AF_P2WPKH)
if mode != 'taproot':
assert sig_file.endswith(".sig")
verify_detached_signature_file(fnames, sig_file, "sd",
addr_fmt=AF_CLASSIC if mode == "classic" else AF_P2WPKH)
path = microsd_path(fname)
_wif = None
_sk = None
_addr = None
_idesc = None
with open(path, 'rt') as fp:
hdr = None
for ln in fp:
@ -98,27 +112,46 @@ def test_generate(mode, pdf, netcode, dev, cap_menu, pick_menu_item, goto_home,
val = ln.strip()
if 'Deposit address' in hdr:
assert val == fname.split('.', 1)[0].split('-', 1)[0]
txt_addr = val
addr = val
_addr = val
elif hdr == 'Private key:': # for QR case
assert val == wif
assert val == _wif
elif 'Private key' in hdr and 'WIF=Wallet' in hdr:
wif = val
k1 = PrivateKey.from_wif(val)
_wif = val
elif 'Private key' in hdr and 'Hex, 32 bytes' in hdr:
k2 = PrivateKey(sec_exp=a2b_hex(val))
_sk = val
elif 'Bitcoin Core command':
assert wif in val
assert 'importmulti' in val or 'importprivkey' in val
assert _wif in val
if 'importdescriptors' in val:
_idesc = val
assert 'importprivkey' in val or 'importdescriptors' in val
else:
print(f'{hdr} => {val}')
raise ValueError(hdr)
assert k1.K.sec() == k2.K.sec()
assert addr == k1.K.address(addr_fmt="p2wpkh" if mode == "segwit" else "p2pkh",
testnet=True if netcode == "XTN" else False)
if netcode != "XRT":
from bip32 import PrivateKey
k1 = PrivateKey.from_wif(_wif)
k2 = PrivateKey.parse(a2b_hex(_sk))
assert k1 == k2
validate_address(_addr, k1)
else:
if mode == "segwit":
assert _addr.startswith("bcrt1q")
elif mode == "taproot":
assert _addr.startswith("bcrt1p")
else:
assert _addr[0] in "mn"
os.unlink(path)
# bitcoind on regtest
conn = bitcoind.create_wallet(wallet_name="paper", disable_private_keys=False, blank=True,
passphrase=None, avoid_reuse=False, descriptors=True)
desc_obj_s, desc_obj_e = _idesc.find("["), _idesc.find("]") + 1
desc_obj = json.loads(_idesc[desc_obj_s:desc_obj_e])
desc = desc_obj[0]["desc"]
res = conn.importdescriptors(desc_obj)
assert res[0]["success"]
assert _addr == conn.deriveaddresses(desc)[0]
bitcoind.delete_wallet_files()
if not pdf: return
@ -126,8 +159,8 @@ def test_generate(mode, pdf, netcode, dev, cap_menu, pick_menu_item, goto_home,
with open(path, 'rb') as fp:
d = fp.read()
assert wif.encode('ascii') in d
assert txt_addr.encode('ascii') in d
assert _wif.encode('ascii') in d
assert _addr.encode('ascii') in d
os.unlink(path)
@ -276,7 +309,7 @@ def test_dice_generate(rolls, testnet, dev, cap_menu, pick_menu_item, goto_home,
val, = hx
k2 = PrivateKey(sec_exp=a2b_hex(val))
assert addr == k2.K.address(testnet=testnet, addr_fmt="p2pkh")
assert addr == k2.K.address(chain="XTN" if testnet else "BTC", addr_fmt="p2pkh")
assert val == sha256(rolls.encode('ascii')).hexdigest()

View File

@ -12,7 +12,7 @@ from pprint import pprint, pformat
from decimal import Decimal
from base64 import b64encode, b64decode
from base58 import encode_base58_checksum
from helpers import B2A, U2SAT, prandom, fake_dest_addr, make_change_addr, parse_change_back
from helpers import B2A, fake_dest_addr, parse_change_back
from helpers import xfp2str, seconds2human_readable, hash160
from msg import verify_message
from bip32 import BIP32Node
@ -133,8 +133,8 @@ def test_psbt_proxy_parsing(fn, sim_execfile, sim_exec):
assert oo == rb
@pytest.mark.unfinalized
def test_speed_test(dev, fake_txn, is_mark3, is_mark4, start_sign, end_sign,
press_select):
@pytest.mark.parametrize("taproot", [True, False])
def test_speed_test(dev, taproot, fake_txn, is_mark3, is_mark4, start_sign, end_sign, press_select):
# measure time to sign a larger txn
if is_mark4:
# Mk4: expect
@ -149,7 +149,10 @@ def test_speed_test(dev, fake_txn, is_mark3, is_mark4, start_sign, end_sign,
num_in = 9
num_out = 100
psbt = fake_txn(num_in, num_out, dev.master_xpub, segwit_in=True)
if taproot:
psbt = fake_txn(num_in, num_out, dev.master_xpub, taproot_in=True)
else:
psbt = fake_txn(num_in, num_out, dev.master_xpub, segwit_in=True)
open('debug/speed.psbt', 'wb').write(psbt)
dt = time.time()
@ -191,8 +194,9 @@ if 0:
@pytest.mark.bitcoind
@pytest.mark.veryslow
@pytest.mark.parametrize('segwit', [True, False])
def test_io_size(request, use_regtest, decode_with_bitcoind, fake_txn,
start_sign, end_sign, dev, segwit, accept = True):
@pytest.mark.parametrize('taproot', [True, False])
def test_io_size(request, use_regtest, decode_with_bitcoind, fake_txn, is_mark3, is_mark4,
start_sign, end_sign, dev, segwit, taproot, accept=True):
# try a bunch of different bigger sized txns
# - important to test on real device, due to it's limited memory
@ -209,7 +213,8 @@ def test_io_size(request, use_regtest, decode_with_bitcoind, fake_txn,
num_in = 250
num_out = 2000
psbt = fake_txn(num_in, num_out, dev.master_xpub, segwit_in=segwit, outstyles=ADDR_STYLES)
psbt = fake_txn(num_in, num_out, dev.master_xpub, segwit_in=segwit,
taproot_in=taproot, outstyles=ADDR_STYLES)
open('debug/last.psbt', 'wb').write(psbt)
@ -262,11 +267,13 @@ def test_io_size(request, use_regtest, decode_with_bitcoind, fake_txn,
@pytest.mark.bitcoind
@pytest.mark.parametrize('num_ins', [ 2, 7, 15 ])
@pytest.mark.parametrize('segwit', [True, False])
def test_real_signing(fake_txn, use_regtest, try_sign, dev, num_ins, segwit, decode_with_bitcoind):
@pytest.mark.parametrize('taproot', [True, False])
def test_real_signing(fake_txn, use_regtest, try_sign, dev, num_ins, segwit,taproot,
decode_with_bitcoind):
# create a TXN using actual addresses that are correct for DUT
xp = dev.master_xpub
psbt = fake_txn(num_ins, 1, xp, segwit_in=segwit)
psbt = fake_txn(num_ins, 1, xp, segwit_in=segwit, taproot_in=taproot)
open('debug/real-%d.psbt' % num_ins, 'wb').write(psbt)
_, txn = try_sign(psbt, accept=True, finalize=True)
@ -908,16 +915,17 @@ def test_network_fee_unlimited(fake_txn, start_sign, end_sign, dev, settings_set
@pytest.mark.parametrize('num_outs', [ 2, 7, 15 ])
@pytest.mark.parametrize('act_outs', [ 2, 1, -1])
@pytest.mark.parametrize('segwit', [True, False])
@pytest.mark.parametrize('taproot', [True, False])
@pytest.mark.parametrize('add_xpub', [True, False])
@pytest.mark.parametrize('out_style', ADDR_STYLES_SINGLE)
@pytest.mark.parametrize('visualized', [0, STXN_VISUALIZE, STXN_VISUALIZE|STXN_SIGNED])
def test_change_outs(fake_txn, start_sign, end_sign, cap_story, dev, num_outs, master_xpub,
act_outs, segwit, out_style, visualized, add_xpub, num_ins=3):
act_outs, segwit, taproot, out_style, visualized, add_xpub, num_ins=3):
# create a TXN which has change outputs, which shouldn't be shown to user, and also not fail.
xp = dev.master_xpub
couts = num_outs if act_outs == -1 else num_ins-act_outs
psbt = fake_txn(num_ins, num_outs, xp, segwit_in=segwit,
psbt = fake_txn(num_ins, num_outs, xp, segwit_in=segwit, taproot_in=taproot,
outstyles=[out_style], change_outputs=range(couts), add_xpub=add_xpub)
open('debug/change.psbt', 'wb').write(psbt)
@ -1209,8 +1217,10 @@ def hist_count(sim_exec):
@pytest.mark.parametrize('num_utxo', [9, 100])
@pytest.mark.parametrize('segwit_in', [False, True])
def test_bip143_attack_data_capture(num_utxo, segwit_in, try_sign, fake_txn, settings_set,
settings_get, cap_story, sim_exec, hist_count):
@pytest.mark.parametrize('taproot_in', [False, True])
def test_bip143_attack_data_capture(num_utxo, segwit_in, taproot_in, try_sign, fake_txn,
settings_set, settings_get, cap_story, sim_exec,
hist_count):
# cleanup prev runs, if very first time thru
sim_exec('import history; history.OutptValueCache.clear()')
@ -1219,12 +1229,13 @@ def test_bip143_attack_data_capture(num_utxo, segwit_in, try_sign, fake_txn, set
# make a txn, capture the outputs of that as inputs for another txn
psbt = fake_txn(1, num_utxo+3, segwit_in=segwit_in, change_outputs=range(num_utxo+2),
outstyles=(['p2wpkh']*num_utxo) + ['p2wpkh-p2sh', 'p2pkh'])
taproot_in=taproot_in,
outstyles=(['p2wpkh']*num_utxo) + ['p2wpkh-p2sh', 'p2pkh'])
_, txn = try_sign(psbt, accept=True, finalize=True)
open('debug/funding.psbt', 'wb').write(psbt)
num_inp_utxo = (1 if segwit_in else 0)
num_inp_utxo = (1 if (segwit_in or taproot_in) else 0)
time.sleep(.1)
title, story = cap_story()
@ -1268,12 +1279,15 @@ def test_bip143_attack_data_capture(num_utxo, segwit_in, try_sign, fake_txn, set
@pytest.mark.parametrize('segwit', [False, True])
@pytest.mark.parametrize('taproot', [False, True])
@pytest.mark.parametrize('num_ins', [1, 17])
def test_txid_calc(num_ins, fake_txn, try_sign, dev, segwit, decode_with_bitcoind, cap_story):
@pytest.mark.parametrize('num_outs', [1, 17])
def test_txid_calc(num_ins, fake_txn, try_sign, dev, segwit, decode_with_bitcoind,
cap_story, taproot, num_outs):
# verify correct txid for transactions is being calculated
xp = dev.master_xpub
psbt = fake_txn(num_ins, 1, xp, segwit_in=segwit)
psbt = fake_txn(num_ins, num_outs, xp, segwit_in=segwit, taproot_in=taproot)
_, txn = try_sign(psbt, accept=True, finalize=True)
@ -1320,7 +1334,7 @@ def test_sdcard_signing(encoding, num_outs, del_after, partial, try_sign_microsd
pp = psbt.inputs[0].bip32_paths[pk]
psbt.inputs[0].bip32_paths[pk] = b'what' + pp[4:]
psbt = fake_txn(2, num_outs, xp, segwit_in=True, psbt_hacker=hack)
psbt = fake_txn(3, num_outs, xp, segwit_in=True, taproot_in=True, psbt_hacker=hack)
_, txn, txid = try_sign_microsd(psbt, finalize=not partial,
encoding=encoding, del_after=del_after)
@ -1352,7 +1366,8 @@ def test_payjoin_signing(num_ins, num_outs, fake_txn, try_sign, start_sign, end_
txn = end_sign(True, finalize=False)
@pytest.mark.parametrize('segwit', [False, True])
def test_fully_unsigned(fake_txn, try_sign, segwit):
@pytest.mark.parametrize('taproot', [False, True])
def test_fully_unsigned(fake_txn, try_sign, segwit, taproot):
# A PSBT which is unsigned but all inputs lack keypaths
@ -1360,8 +1375,9 @@ def test_fully_unsigned(fake_txn, try_sign, segwit):
# change all inputs to be "not ours" ... but with utxo details
for i in psbt.inputs:
i.bip32_paths.clear()
i.taproot_bip32_paths.clear()
psbt = fake_txn(7, 2, segwit_in=segwit, psbt_hacker=hack)
psbt = fake_txn(7, 2, segwit_in=segwit, taproot_in=taproot, psbt_hacker=hack)
with pytest.raises(CCProtoError) as ee:
orig, result = try_sign(psbt, accept=True)
@ -1369,7 +1385,8 @@ def test_fully_unsigned(fake_txn, try_sign, segwit):
assert 'does not contain any key path information' in str(ee)
@pytest.mark.parametrize('segwit', [False, True])
def test_wrong_xfp(fake_txn, try_sign, segwit):
@pytest.mark.parametrize('taproot', [False, True])
def test_wrong_xfp(fake_txn, try_sign, segwit, taproot):
# A PSBT which is unsigned and doesn't involve our XFP value
@ -1380,8 +1397,10 @@ def test_wrong_xfp(fake_txn, try_sign, segwit):
for i in psbt.inputs:
for pubkey in i.bip32_paths:
i.bip32_paths[pubkey] = wrong_xfp + i.bip32_paths[pubkey][4:]
for xonly_pubkey in i.taproot_bip32_paths:
i.taproot_bip32_paths[xonly_pubkey] = b"\x00" + wrong_xfp + i.taproot_bip32_paths[xonly_pubkey][5:]
psbt = fake_txn(7, 2, segwit_in=segwit, psbt_hacker=hack)
psbt = fake_txn(7, 2, segwit_in=segwit, taproot_in=taproot, psbt_hacker=hack)
with pytest.raises(CCProtoError) as ee:
orig, result = try_sign(psbt, accept=True)
@ -1390,7 +1409,8 @@ def test_wrong_xfp(fake_txn, try_sign, segwit):
assert 'found 12345678' in str(ee)
@pytest.mark.parametrize('segwit', [False, True])
def test_wrong_xfp_multi(fake_txn, try_sign, segwit):
@pytest.mark.parametrize('taproot', [False, True])
def test_wrong_xfp_multi(fake_txn, try_sign, segwit, taproot):
# A PSBT which is unsigned and doesn't involve our XFP value
# - but multiple wrong XFP values
@ -1404,8 +1424,12 @@ def test_wrong_xfp_multi(fake_txn, try_sign, segwit):
here = struct.pack('<I', idx+73)
i.bip32_paths[pubkey] = here + i.bip32_paths[pubkey][4:]
wrongs.add(xfp2str(idx+73))
for xonly_pubkey in i.taproot_bip32_paths:
here = struct.pack('<I', idx + 73)
i.taproot_bip32_paths[xonly_pubkey] = b"\x00" + here + i.taproot_bip32_paths[xonly_pubkey][5:]
wrongs.add(xfp2str(idx + 73))
psbt = fake_txn(7, 2, segwit_in=segwit, psbt_hacker=hack)
psbt = fake_txn(7, 2, segwit_in=segwit, taproot_in=taproot, psbt_hacker=hack)
open('debug/wrong-xfp.psbt', 'wb').write(psbt)
with pytest.raises(CCProtoError) as ee:
@ -1420,15 +1444,16 @@ def test_wrong_xfp_multi(fake_txn, try_sign, segwit):
@pytest.mark.parametrize('out_style', ADDR_STYLES_SINGLE)
@pytest.mark.parametrize('segwit', [False, True])
@pytest.mark.parametrize('taproot', [False, True])
@pytest.mark.parametrize('outval', ['.5', '.788888', '0.92640866'])
def test_render_outs(out_style, segwit, outval, fake_txn, start_sign, end_sign, dev):
def test_render_outs(out_style, segwit, outval, fake_txn, start_sign, end_sign, dev, taproot):
# check how we render the value of outputs
# - works on simulator and connected USB real-device
xp = dev.master_xpub
oi = int(Decimal(outval) * int(1E8))
psbt = fake_txn(1, 2, dev.master_xpub, segwit_in=segwit, outvals=[oi, int(1E8-oi)],
outstyles=[out_style], change_outputs=[1])
taproot_in=taproot, outstyles=[out_style], change_outputs=[1])
open('debug/render.psbt', 'wb').write(psbt)
@ -1461,6 +1486,8 @@ def test_render_outs(out_style, segwit, outval, fake_txn, start_sign, end_sign,
elif out_style == 'p2wpkh-p2sh':
assert len(set(i[0] for i in addrs)) == 1
assert addrs[0][0] in {'2', '3'}
elif out_style == 'p2tr':
assert all(i.startswith(("bc1p", "tb1p", "bcrt1p")) for i in addrs)
def test_negative_fee(dev, fake_txn, try_sign):
@ -1643,7 +1670,8 @@ def test_zero_xfp(dev, start_sign, end_sign, fake_txn, cap_story):
@pytest.mark.parametrize("segwit_in", [True, False])
@pytest.mark.parametrize('num_not_ours', [1, 3, 4])
def test_foreign_utxo_missing(segwit_in, num_not_ours, dev, fake_txn, start_sign, cap_story, end_sign):
def test_foreign_utxo_missing(segwit_in, num_not_ours, dev, fake_txn, start_sign,
cap_story, end_sign):
def hack(psbt):
# change first input to not be ours
for i in range(num_not_ours):
@ -1666,16 +1694,17 @@ def test_foreign_utxo_missing(segwit_in, num_not_ours, dev, fake_txn, start_sign
assert signed != psbt
@pytest.mark.parametrize("segwit_in", [True, False])
@pytest.mark.parametrize("taproot_in", [True, False])
@pytest.mark.parametrize("num_missing", [1, 3, 4])
def test_own_utxo_missing(segwit_in, num_missing, dev, fake_txn, start_sign, cap_story, end_sign,
press_cancel):
press_cancel, taproot_in):
def hack(psbt):
for i in range(num_missing):
# no utxo provided for our input
psbt.inputs[i].utxo = None
psbt.inputs[i].witness_utxo = None
psbt = fake_txn(5, 2, dev.master_xpub, segwit_in=segwit_in, psbt_hacker=hack)
psbt = fake_txn(5, 2, dev.master_xpub, segwit_in=segwit_in, taproot_in=taproot_in, psbt_hacker=hack)
start_sign(psbt)
time.sleep(.1)
title, story = cap_story()
@ -1693,17 +1722,22 @@ def test_bitcoind_missing_foreign_utxo(bitcoind, bitcoind_d_sim_watch, microsd_p
alice = bitcoind.create_wallet(wallet_name="alice")
bob = bitcoind.create_wallet(wallet_name="bob")
cc = bitcoind_d_sim_watch
tap_dave = bitcoind.create_wallet(wallet_name="tap_dave")
alice_addr = alice.getnewaddress()
alice_pubkey = alice.getaddressinfo(alice_addr)["pubkey"]
bob_addr = bob.getnewaddress()
bob_pubkey = bob.getaddressinfo(bob_addr)["pubkey"]
cc_addr = cc.getnewaddress()
cc_pubkey = cc.getaddressinfo(cc_addr)["pubkey"]
tap_dave_addr = tap_dave.getnewaddress("", "bech32m")
# fund all addresses
for addr in (alice_addr, bob_addr, cc_addr):
bitcoind.supply_wallet.generatetoaddress(101, addr)
for addr in (alice_addr, bob_addr, cc_addr, tap_dave_addr):
bitcoind.supply_wallet.sendtoaddress(addr, 2)
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
psbt_list = []
for w in (alice, bob, cc):
for w in (alice, bob, cc, tap_dave):
assert w.listunspent()
psbt = w.walletcreatefundedpsbt([], [{dest_address: 1.0}], 0, {"fee_rate": 20})["psbt"]
psbt_list.append(psbt)
@ -1718,6 +1752,9 @@ def test_bitcoind_missing_foreign_utxo(bitcoind, bitcoind_d_sim_watch, microsd_p
assert pk.hex() in (alice_pubkey, bob_pubkey)
inp.utxo = None
inp.witness_utxo = None
for xo_pk, _ in inp.taproot_bip32_paths.items():
inp.utxo = None
inp.witness_utxo = None
psbt0 = the_psbt_obj.as_bytes()
orig, res = try_sign(psbt0, accept=True)
@ -1726,10 +1763,12 @@ def test_bitcoind_missing_foreign_utxo(bitcoind, bitcoind_d_sim_watch, microsd_p
# lets sign with bob first - bobs wallet will ignore missing alice UTXO but will supply his UTXO
psbt1 = bob.walletprocesspsbt(base64.b64encode(res).decode(), True, "ALL")["psbt"]
# finally sign with alice
res = alice.walletprocesspsbt(psbt1, True, "ALL")
res = alice.walletprocesspsbt(psbt1, True)
psbt2 = res["psbt"]
res = tap_dave.walletprocesspsbt(psbt2, True)
psbt3 = res["psbt"]
assert res["complete"] is True
tx = alice.finalizepsbt(psbt2)["hex"]
tx = alice.finalizepsbt(psbt3)["hex"]
assert alice.testmempoolaccept([tx])[0]["allowed"] is True
tx_id = alice.sendrawtransaction(tx)
assert isinstance(tx_id, str) and len(tx_id) == 64
@ -1747,7 +1786,8 @@ def test_bitcoind_missing_foreign_utxo(bitcoind, bitcoind_d_sim_watch, microsd_p
def test_op_return_signing(op_return_data, dev, fake_txn, bitcoind_d_sim_watch, bitcoind, start_sign, end_sign, cap_story):
cc = bitcoind_d_sim_watch
dest_address = cc.getnewaddress()
bitcoind.supply_wallet.generatetoaddress(101, dest_address)
bitcoind.supply_wallet.sendtoaddress(dest_address, 49)
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
psbt = cc.walletcreatefundedpsbt([], [{dest_address: 1.0}, {"data": op_return_data.hex()}], 0, {"fee_rate": 20})["psbt"]
start_sign(base64.b64decode(psbt))
time.sleep(.1)
@ -1853,11 +1893,17 @@ def test_duplicate_unknow_values_in_psbt(dev, start_sign, end_sign, fake_txn):
@pytest.fixture
def _test_single_sig_sighash(microsd_wipe, microsd_path, goto_home, cap_story, press_select,
bitcoind, bitcoind_d_sim_watch, settings_set, finalize_v2_v0_convert):
bitcoind, bitcoind_d_sim_watch, settings_set, finalize_v2_v0_convert,
bitcoind_d_wallet_w_sk):
def doit(addr_fmt, sighash, num_inputs=2, num_outputs=2, consolidation=False, sh_checks=False,
psbt_v2=False):
from decimal import Decimal, ROUND_DOWN
# supply wallet is legacy wallet (does not support taproot)
aa = bitcoind_d_wallet_w_sk.getnewaddress()
bitcoind.supply_wallet.sendtoaddress(address=aa, amount=49)
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress()) # mine
settings_set("sighshchk", int(not sh_checks))
microsd_wipe()
time.sleep(0.1)
@ -1866,24 +1912,24 @@ def _test_single_sig_sighash(microsd_wipe, microsd_path, goto_home, cap_story, p
not_all_ALL = any(sh != "ALL" for sh in sighash)
bitcoind_d_sim_watch.keypoolrefill(num_inputs + num_outputs)
input_val = bitcoind.supply_wallet.getbalance() / num_inputs
input_val = bitcoind_d_wallet_w_sk.getbalance() / num_inputs
cc_dest = [
{bitcoind_d_sim_watch.getnewaddress("", addr_fmt): Decimal(input_val).quantize(Decimal('.0000001'), rounding=ROUND_DOWN)}
for _ in range(num_inputs)
]
psbt = bitcoind.supply_wallet.walletcreatefundedpsbt(
psbt = bitcoind_d_wallet_w_sk.walletcreatefundedpsbt(
[], cc_dest, 0, {"fee_rate": 20, "subtractFeeFromOutputs": [0]}
)["psbt"]
psbt = bitcoind.supply_wallet.walletprocesspsbt(psbt, True, "ALL")["psbt"]
resp = bitcoind.supply_wallet.finalizepsbt(psbt)
psbt = bitcoind_d_wallet_w_sk.walletprocesspsbt(psbt, True, "ALL")["psbt"]
resp = bitcoind_d_wallet_w_sk.finalizepsbt(psbt)
assert resp["complete"] is True
assert len(bitcoind.supply_wallet.sendrawtransaction(resp["hex"])) == 64
assert len(bitcoind_d_wallet_w_sk.sendrawtransaction(resp["hex"])) == 64
# mine above txs
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
bitcoind_d_wallet_w_sk.generatetoaddress(1, bitcoind_d_wallet_w_sk.getnewaddress())
unspent = bitcoind_d_sim_watch.listunspent()
output_val = bitcoind_d_sim_watch.getbalance() / num_outputs
# consolidation or not?
dest_wal = bitcoind_d_sim_watch if consolidation else bitcoind.supply_wallet
dest_wal = bitcoind_d_sim_watch if consolidation else bitcoind_d_wallet_w_sk
destinations = [
{dest_wal.getnewaddress("", addr_fmt): Decimal(output_val).quantize(Decimal('.0000001'), rounding=ROUND_DOWN)}
for _ in range(num_outputs)
@ -1914,6 +1960,7 @@ def _test_single_sig_sighash(microsd_wipe, microsd_path, goto_home, cap_story, p
with open(microsd_path("sighash.psbt"), "w") as f:
f.write(psbt_sh)
press_select()
time.sleep(0.2)
title, story = cap_story()
@ -2014,7 +2061,7 @@ def _test_single_sig_sighash(microsd_wipe, microsd_path, goto_home, cap_story, p
@pytest.mark.bitcoind
@pytest.mark.parametrize("addr_fmt", ["legacy", "p2sh-segwit", "bech32"])
@pytest.mark.parametrize("addr_fmt", ["legacy", "p2sh-segwit", "bech32", "bech32m"])
@pytest.mark.parametrize("sighash", [sh for sh in SIGHASH_MAP if sh != 'ALL'])
@pytest.mark.parametrize("num_outs", [1, 3, 5])
@pytest.mark.parametrize("num_ins", [2, 5])
@ -2026,7 +2073,7 @@ def test_sighash_same(addr_fmt, sighash, num_ins, num_outs, psbt_v2, _test_singl
@pytest.mark.bitcoind
@pytest.mark.parametrize("addr_fmt", ["legacy", "p2sh-segwit", "bech32"])
@pytest.mark.parametrize("addr_fmt", ["legacy", "p2sh-segwit", "bech32", "bech32m"])
@pytest.mark.parametrize("sighash", list(itertools.combinations(SIGHASH_MAP.keys(), 2)))
@pytest.mark.parametrize("num_outs", [2, 3, 5])
@pytest.mark.parametrize("psbt_v2", [True, False])
@ -2037,7 +2084,7 @@ def test_sighash_different(addr_fmt, sighash, num_outs, psbt_v2, _test_single_si
@pytest.mark.bitcoind
@pytest.mark.parametrize("addr_fmt", ["legacy", "p2sh-segwit", "bech32"])
@pytest.mark.parametrize("addr_fmt", ["legacy", "p2sh-segwit", "bech32", "bech32m"])
@pytest.mark.parametrize("num_outs", [5, 8])
@pytest.mark.parametrize("psbt_v2", [True, False])
def test_sighash_fullmix(addr_fmt, num_outs, psbt_v2, _test_single_sig_sighash):
@ -2103,7 +2150,7 @@ def test_send2taproot_addresss(fake_txn , start_sign, end_sign, cap_story):
assert title == "OK TO SEND?"
# we do not understand change in taproot (taproot not supported)
assert "Consolidating" not in story
assert "Change back" not in story
assert "Change back" in story
# but we should show address
assert "to script" not in story
assert "tb1p" in story
@ -2984,4 +3031,269 @@ def test_txout_explorer_op_return(fake_txn, start_sign, cap_story, is_q1,
press_cancel()
press_cancel()
# EOF
@pytest.mark.bitcoind
def test_taproot_keyspend(use_regtest, bitcoind_d_sim_watch, start_sign, end_sign, microsd_path, cap_story, goto_home,
press_select, pick_menu_item, bitcoind):
use_regtest()
sim = bitcoind_d_sim_watch
sim.keypoolrefill(10)
addr = sim.getnewaddress("", "bech32m")
bitcoind.supply_wallet.sendtoaddress(addr, 49)
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
dest_addr = sim.getnewaddress("", "bech32m") # self-spend
psbt_resp = sim.walletcreatefundedpsbt([], [{dest_addr: 1.0}], 0, {"fee_rate": 20})
psbt = psbt_resp.get("psbt")
psbt_fname = "tr.psbt"
open('debug/last.psbt', 'w').write(psbt)
with open(microsd_path(psbt_fname), "w") as f:
f.write(psbt)
goto_home()
pick_menu_item('Ready To Sign')
time.sleep(.1)
title, story = cap_story()
if 'OK TO SEND?' not in title:
pick_menu_item(psbt_fname)
time.sleep(0.1)
title, story = cap_story()
assert title == 'OK TO SEND?'
assert "Consolidating" in story # self-spend
assert "1 ins - fee" in story # one input
assert "2 outs" in story # two outputs
addrs = story.split("\n\n")[3].split("\n")[-2:]
assert len(addrs) == 2
for addr in addrs:
assert addr.startswith("bcrt1p")
press_select() # confirm signing
time.sleep(0.1)
title, story = cap_story()
assert title == 'PSBT Signed'
assert "Updated PSBT is:" in story
assert "Finalized transaction (ready for broadcast)" in story
assert "TXID" in story
split_story = story.split("\n\n")
story_txid = split_story[-1].split("\n")[-1]
signed_psbt_fname = split_story[1]
with open(microsd_path(signed_psbt_fname), "r") as f:
signed_psbt = f.read().strip()
open('debug/last.psbt', 'w').write(psbt)
signed_txn_fname = split_story[3]
with open(microsd_path(signed_txn_fname), "r") as f:
signed_txn = f.read().strip()
assert signed_psbt != psbt
finalize_res = sim.finalizepsbt(signed_psbt)
bitcoind_signed_txn = finalize_res["hex"]
assert finalize_res["complete"] is True
accept_res = sim.testmempoolaccept([bitcoind_signed_txn])[0]
assert accept_res["allowed"] is True
assert signed_txn == bitcoind_signed_txn
txid = sim.sendrawtransaction(signed_txn)
assert len(txid) == 64
assert txid == story_txid
addr_segwit = sim.getnewaddress("", "bech32")
sim.generatetoaddress(1, addr_segwit) # mine transaction sent and also new coins to p2wpkh
addr_nested_segwit = sim.getnewaddress("", "p2sh-segwit")
sim.generatetoaddress(1, addr_nested_segwit)
addr_legacy = sim.getnewaddress("", "legacy")
sim.generatetoaddress(1, addr_legacy)
# try to sign tx with all input types (legacy, nested segwit, native segwit, taproot)
all_of_it = sim.getbalance()
dest_addr0 = sim.getnewaddress("", "bech32m") # self-spend
dest_addr1 = sim.getnewaddress("", "bech32m") # self-spend
dest_addr2 = sim.getnewaddress("", "bech32m") # self-spend
chunk = round(all_of_it / 3, 6)
psbt_resp = sim.walletcreatefundedpsbt([], [{dest_addr0: chunk}, {dest_addr1: chunk}, {dest_addr2: chunk}],
0, {'subtractFeeFromOutputs': [0], "fee_rate": 20})
psbt = psbt_resp.get("psbt")
psbt_fname = "tr-all.psbt"
open('debug/last.psbt', 'w').write(psbt)
with open(microsd_path(psbt_fname), "w") as f:
f.write(psbt)
goto_home()
pick_menu_item('Ready To Sign')
time.sleep(.1)
title, story = cap_story()
if 'OK TO SEND?' not in title:
pick_menu_item(psbt_fname)
time.sleep(0.1)
title, story = cap_story()
assert title == 'OK TO SEND?'
assert "Consolidating" in story # self-spend
assert "2 ins - fee" in story # five inputs
assert "3 outs" in story # one output
press_select() # confirm signing
time.sleep(0.1)
title, story = cap_story()
assert title == 'PSBT Signed'
assert "Updated PSBT is:" in story
assert "Finalized transaction (ready for broadcast)" in story
assert "TXID" in story
split_story = story.split("\n\n")
story_txid = split_story[-1].split("\n")[-1]
signed_psbt_fname = split_story[1]
with open(microsd_path(signed_psbt_fname), "r") as f:
signed_psbt = f.read().strip()
open('debug/last.psbt', 'w').write(psbt)
signed_txn_fname = split_story[3]
with open(microsd_path(signed_txn_fname), "r") as f:
signed_txn = f.read().strip()
assert signed_psbt != psbt
finalize_res = sim.finalizepsbt(signed_psbt)
bitcoind_signed_txn = finalize_res["hex"]
assert finalize_res["complete"] is True
accept_res = sim.testmempoolaccept([bitcoind_signed_txn])[0]
assert accept_res["allowed"] is True
assert signed_txn == bitcoind_signed_txn
txid = sim.sendrawtransaction(signed_txn)
assert len(txid) == 64
assert txid == story_txid
# multi p2tr output consolidation
addr_segwit = sim.getnewaddress("", "bech32")
sim.generatetoaddress(1, addr_segwit) # mine transaction sent and also new coins to p2wpkh
all_of_it = sim.getbalance()
dest_addr = sim.getnewaddress("", "bech32m")
psbt_resp = sim.walletcreatefundedpsbt([], [{dest_addr: all_of_it}],
0, {'subtractFeeFromOutputs': [0], "fee_rate": 20})
psbt = psbt_resp.get("psbt")
psbt_fname = "tr-multi-out-consolidation.psbt"
with open(microsd_path(psbt_fname), "w") as f:
f.write(psbt)
goto_home()
pick_menu_item('Ready To Sign')
time.sleep(.1)
title, story = cap_story()
if 'OK TO SEND?' not in title:
pick_menu_item(psbt_fname)
time.sleep(0.1)
title, story = cap_story()
assert title == 'OK TO SEND?'
assert "Consolidating" in story # self-spend
assert "3 ins - fee" in story # five inputs
assert "1 outs" in story # one output
press_select() # confirm signing
time.sleep(0.1)
title, story = cap_story()
assert title == 'PSBT Signed'
assert "Updated PSBT is:" in story
assert "Finalized transaction (ready for broadcast)" in story
assert "TXID" in story
split_story = story.split("\n\n")
story_txid = split_story[-1].split("\n")[-1]
signed_psbt_fname = split_story[1]
with open(microsd_path(signed_psbt_fname), "r") as f:
signed_psbt = f.read().strip()
open('debug/last.psbt', 'w').write(psbt)
signed_txn_fname = split_story[3]
with open(microsd_path(signed_txn_fname), "r") as f:
signed_txn = f.read().strip()
assert signed_psbt != psbt
finalize_res = sim.finalizepsbt(signed_psbt)
bitcoind_signed_txn = finalize_res["hex"]
assert finalize_res["complete"] is True
accept_res = sim.testmempoolaccept([bitcoind_signed_txn])[0]
assert accept_res["allowed"] is True
assert signed_txn == bitcoind_signed_txn
txid = sim.sendrawtransaction(signed_txn)
assert len(txid) == 64
assert txid == story_txid
# send it all to bob, he's a good guy
bob_w = bitcoind.create_wallet("bob")
dst = bob_w.getnewaddress("", "bech32m")
all_of_it = sim.getbalance()
psbt_resp = sim.walletcreatefundedpsbt([], [{dst: all_of_it}],
0, {'subtractFeeFromOutputs': [0], "fee_rate": 20})
psbt = psbt_resp.get("psbt")
psbt_fname = "tr2bob.psbt"
with open(microsd_path(psbt_fname), "w") as f:
f.write(psbt)
goto_home()
pick_menu_item('Ready To Sign')
time.sleep(.1)
title, story = cap_story()
if 'OK TO SEND?' not in title:
pick_menu_item(psbt_fname)
time.sleep(0.1)
title, story = cap_story()
assert title == 'OK TO SEND?'
assert "Consolidating" not in story # NOT a self-spend
assert "to address" in story
assert dst in story
press_select() # confirm signing
time.sleep(0.1)
title, story = cap_story()
assert title == 'PSBT Signed'
assert "Updated PSBT is:" in story
assert "Finalized transaction (ready for broadcast)" in story
assert "TXID" in story
split_story = story.split("\n\n")
story_txid = split_story[-1].split("\n")[-1]
signed_psbt_fname = split_story[1]
with open(microsd_path(signed_psbt_fname), "r") as f:
signed_psbt = f.read().strip()
signed_txn_fname = split_story[3]
with open(microsd_path(signed_txn_fname), "r") as f:
signed_txn = f.read().strip()
assert signed_psbt != psbt
finalize_res = sim.finalizepsbt(signed_psbt)
bitcoind_signed_txn = finalize_res["hex"]
assert finalize_res["complete"] is True
accept_res = sim.testmempoolaccept([bitcoind_signed_txn])[0]
assert accept_res["allowed"] is True
assert signed_txn == bitcoind_signed_txn
txid = sim.sendrawtransaction(signed_txn)
assert len(txid) == 64
assert txid == story_txid
@pytest.mark.parametrize('fn_err_msg', [
('data/taproot/in_internal_key_len.psbt', 'PSBT_IN_TAP_INTERNAL_KEY length != 32'),
('data/taproot/in_key_pth_sig_len.psbt', 'PSBT_IN_TAP_KEY_SIG length != 64 or 65'),
('data/taproot/in_key_pth_sig_len1.psbt', 'PSBT_IN_TAP_KEY_SIG length != 64 or 65'),
('data/taproot/in_tr_deriv_key_len.psbt', 'PSBT_IN_TAP_BIP32_DERIVATION xonly-pubkey length != 32'),
('data/taproot/in_script_sig_key_len.psbt', 'PSBT_IN_TAP_SCRIPT_SIG key length != 64'),
('data/taproot/in_script_sig_sig_len.psbt', 'PSBT_IN_TAP_SCRIPT_SIG signature length != 64 or 65'),
('data/taproot/in_script_sig_sig_len1.psbt', 'PSBT_IN_TAP_SCRIPT_SIG signature length != 64 or 65'),
('data/taproot/in_leaf_script_cb_len.psbt', 'PSBT_IN_TAP_LEAF_SCRIPT control block is not valid'),
('data/taproot/in_leaf_script_cb_len1.psbt', 'PSBT_IN_TAP_LEAF_SCRIPT control block is not valid'),
])
def test_invalid_input_taproot_psbt(start_sign, fn_err_msg, cap_story):
fn, err_msg = fn_err_msg
start_sign(fn)
title, story = cap_story()
assert title == "Failure"
assert 'Invalid PSBT' in story
# error messages are disabled to save some space - problem file line is still included
# assert err_msg in story
def test_invalid_output_tapproot_psbt(fake_txn, start_sign, cap_story, dev):
psbt = fake_txn(3, 2, master_xpub=dev.master_xpub, taproot_in=True, outstyles=["p2tr"], change_outputs=[1])
# invalid internal key length
psbt_obj = BasicPSBT().parse(psbt)
for o in psbt_obj.outputs:
o.taproot_internal_key = b"\x03" + b"a" * 32
psbt0 = BytesIO()
psbt_obj.serialize(psbt0)
start_sign(psbt0.getvalue())
title, story = cap_story()
assert title == "Failure"
assert 'Invalid PSBT' in story
# error messages are disabled to save some space - problem file line is still included
# assert "PSBT_OUT_TAP_INTERNAL_KEY length != 32" in story
# invalid internal key length in bip32 taproot paths
psbt_obj = BasicPSBT().parse(psbt)
for o in psbt_obj.outputs:
o.taproot_bip32_paths = {b"\x03" + b"a" * 32: 12 * b"1"}
psbt0 = BytesIO()
psbt_obj.serialize(psbt0)
start_sign(psbt0.getvalue())
title, story = cap_story()
assert title == "Failure"
assert 'Invalid PSBT' in story
# error messages are disabled to save some space - problem file line is still included
# assert "PSBT_IN_TAP_BIP32_DERIVATION xonly-pubkey length != 32" in story

View File

@ -4,7 +4,7 @@
#
import pytest, os, shutil
from helpers import B2A
from helpers import B2A, taptweak
def test_remote_exec(sim_exec):
@ -71,9 +71,13 @@ def test_public(sim_execfile):
assert sk.hwif() == result
elif result[0] in '1mn':
assert result == sk.address()
elif result[0:3] in { 'bc1', 'tb1' }:
elif result[0:4] in {'bc1q', 'tb1q'}:
h20 = sk.hash160()
assert result == bech32.encode(result[0:2], 0, h20)
elif result[0:4] in {'bc1p', 'tb1p'}:
from bech32 import encode
tweked_xonly = taptweak(sk.sec()[1:])
assert result == encode(result[:2], 1, tweked_xonly)
elif result[0] in '23':
h20 = hash160(b'\x00\x14' + sk.hash160())
assert h20 == decode_base58_checksum(result)[1:]

View File

@ -6,7 +6,7 @@ import pytest, struct
from ckcc_protocol.protocol import MAX_TXN_LEN
from psbt import BasicPSBT, BasicPSBTInput, BasicPSBTOutput
from io import BytesIO
from helpers import fake_dest_addr, make_change_addr, hash160
from helpers import fake_dest_addr, make_change_addr, hash160, taptweak
from base58 import decode_base58
from bip32 import BIP32Node
from constants import ADDR_STYLES, simulator_fixed_tprv
@ -24,7 +24,7 @@ def fake_txn(dev, pytestconfig):
invals=None, outvals=None, segwit_in=False, wrapped=False,
outstyles=['p2pkh'], psbt_hacker=None, change_outputs=[],
capture_scripts=None, add_xpub=None, op_return=None,
psbt_v2=None, input_amount=1E8):
psbt_v2=None, input_amount=1E8, taproot_in=False):
psbt = BasicPSBT()
@ -61,7 +61,40 @@ def fake_txn(dev, pytestconfig):
assert len(sec) == 33, "expect compressed"
assert subpath[0:2] == '0/'
psbt.inputs[i].bip32_paths[sec] = xfp + struct.pack('<II', 0, i)
if taproot_in:
tweaked_xonly = taptweak(sec[1:])
if segwit_in and taproot_in:
# if both specified:
# even is segwit v0
# odd is segvit v1 (taproot)
if i % 2 == 0:
psbt.inputs[i].bip32_paths[sec] = xfp + struct.pack('<II', 0, i)
scr = bytes([0x00, 0x14]) + subkey.hash160()
if wrapped:
# p2sh-p2wpkh
psbt.inputs[i].redeem_script = scr
scr = bytes([0xa9, 0x14]) + hash160(scr) + bytes([0x87])
else:
psbt.inputs[i].taproot_bip32_paths[sec[1:]] = b"\x00" + xfp + struct.pack('<II', 0, i)
scr = bytes([81, 32]) + tweaked_xonly
# UTXO that provides the funding for to-be-signed txn
elif taproot_in:
psbt.inputs[i].taproot_bip32_paths[sec[1:]] = b"\x00" + xfp + struct.pack('<II', 0, i)
scr = bytes([81, 32]) + tweaked_xonly
else:
psbt.inputs[i].bip32_paths[sec] = xfp + struct.pack('<II', 0, i)
if segwit_in:
# p2wpkh
scr = bytes([0x00, 0x14]) + subkey.hash160()
if wrapped:
# p2sh-p2wpkh
psbt.inputs[i].redeem_script = scr
scr = bytes([0xa9, 0x14]) + hash160(scr) + bytes([0x87])
else:
# p2pkh
scr = bytes([0x76, 0xa9, 0x14]) + subkey.hash160() + bytes([0x88, 0xac])
# UTXO that provides the funding for to-be-signed txn
supply = CTransaction()
@ -72,17 +105,6 @@ def fake_txn(dev, pytestconfig):
)
supply.vin = [CTxIn(out_point, nSequence=0xffffffff)]
if segwit_in:
# p2wpkh
scr = bytes([0x00, 0x14]) + subkey.hash160()
if wrapped:
# p2sh-p2wpkh
psbt.inputs[i].redeem_script = scr
scr = bytes([0xa9, 0x14]) + hash160(scr) + bytes([0x87])
else:
# p2pkh
scr = bytes([0x76, 0xa9, 0x14]) + subkey.hash160() + bytes([0x88, 0xac])
supply.vout.append(CTxOut(int(input_amount if not invals else invals[i]), scr))
if segwit_in: