taproot singlesig
This commit is contained in:
parent
a798e96de0
commit
d2920d1c60
@ -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
65
docs/taproot.md
Normal file
@ -0,0 +1,65 @@
|
||||
# Taproot
|
||||
|
||||
**COLDCARD<sup>®</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.
|
||||
2
external/ckcc-protocol
vendored
2
external/ckcc-protocol
vendored
@ -1 +1 @@
|
||||
Subproject commit a6d901f9fca50755835eca895586ca74d0ca81ed
|
||||
Subproject commit cad2722d1433dbb956e4457eac9ea01e1c77abbe
|
||||
2
external/libngu
vendored
2
external/libngu
vendored
@ -1 +1 @@
|
||||
Subproject commit 356b9137cf7ddf5de66ec4cdc0a4d757b2e42790
|
||||
Subproject commit 2537f1581d0bad2c16fa391bc6fade328450a217
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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...')
|
||||
|
||||
|
||||
@ -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
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -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"]:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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')
|
||||
|
||||
796
shared/psbt.py
796
shared/psbt.py
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
||||
@ -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('%')
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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']
|
||||
|
||||
BIN
testing/data/taproot/in_internal_key_len.psbt
Normal file
BIN
testing/data/taproot/in_internal_key_len.psbt
Normal file
Binary file not shown.
1
testing/data/taproot/in_key_pth_sig_len.psbt
Normal file
1
testing/data/taproot/in_key_pth_sig_len.psbt
Normal file
@ -0,0 +1 @@
|
||||
70736274ff010071020000000127744ababf3027fe0d6cf23a96eee2efb188ef52301954585883e69b6624b2420000000000ffffffff02787c01000000000016001483a7e34bd99ff03a4962ef8a1a101bb295461ece606b042a010000001600147ac369df1b20e033d6116623957b0ac49f3c52e8000000000001012b00f2052a010000002251205a2c2cf5b52cf31f83ad2e8da63ff03183ecd8f609c7510ae8a48e03910a075701133f173bb3d36c074afb716fec6307a069a2e450b995f3c82785945ab8df0e24260dcd703b0cbf34de399184a9481ac2b3586db6601f026a77f7e4938481bc3475000000
|
||||
1
testing/data/taproot/in_key_pth_sig_len1.psbt
Normal file
1
testing/data/taproot/in_key_pth_sig_len1.psbt
Normal file
@ -0,0 +1 @@
|
||||
70736274ff010071020000000127744ababf3027fe0d6cf23a96eee2efb188ef52301954585883e69b6624b2420000000000ffffffff02787c01000000000016001483a7e34bd99ff03a4962ef8a1a101bb295461ece606b042a010000001600147ac369df1b20e033d6116623957b0ac49f3c52e8000000000001012b00f2052a010000002251205a2c2cf5b52cf31f83ad2e8da63ff03183ecd8f609c7510ae8a48e03910a0757011342173bb3d36c074afb716fec6307a069a2e450b995f3c82785945ab8df0e24260dcd703b0cbf34de399184a9481ac2b3586db6601f026a77f7e4938481bc34751701aa000000
|
||||
1
testing/data/taproot/in_leaf_script_cb_len.psbt
Normal file
1
testing/data/taproot/in_leaf_script_cb_len.psbt
Normal file
@ -0,0 +1 @@
|
||||
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b6926315c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac06f7d62059e9497a1a4a267569d9876da60101aff38e3529b9b939ce7f91ae970115f2e490af7cc45c4f78511f36057ce5c5a5c56325a29fb44dfc203f356e1f80023202cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2acc00000
|
||||
1
testing/data/taproot/in_leaf_script_cb_len1.psbt
Normal file
1
testing/data/taproot/in_leaf_script_cb_len1.psbt
Normal file
@ -0,0 +1 @@
|
||||
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b6926115c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac06f7d62059e9497a1a4a267569d9876da60101aff38e3529b9b939ce7f91ae970115f2e490af7cc45c4f78511f36057ce5c5a5c56325a29fb44dfc203f356e123202cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2acc00000
|
||||
1
testing/data/taproot/in_script_sig_key_len.psbt
Normal file
1
testing/data/taproot/in_script_sig_key_len.psbt
Normal file
@ -0,0 +1 @@
|
||||
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b6924214022cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2cd970e15f53fc0c82f950fd560ffa919b76172be017368a89913af074f400b094089756aa3739ccc689ec0fcf3a360be32cc0b59b16e93a1e8bb4605726b2ca7a3ff706c4176649632b2cc68e1f912b8a578e3719ce7710885c7a966f49bcd43cb0000
|
||||
1
testing/data/taproot/in_script_sig_sig_len.psbt
Normal file
1
testing/data/taproot/in_script_sig_sig_len.psbt
Normal file
@ -0,0 +1 @@
|
||||
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b69241142cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2cd970e15f53fc0c82f950fd560ffa919b76172be017368a89913af074f400b094289756aa3739ccc689ec0fcf3a360be32cc0b59b16e93a1e8bb4605726b2ca7a3ff706c4176649632b2cc68e1f912b8a578e3719ce7710885c7a966f49bcd43cb01010000
|
||||
1
testing/data/taproot/in_script_sig_sig_len1.psbt
Normal file
1
testing/data/taproot/in_script_sig_sig_len1.psbt
Normal file
@ -0,0 +1 @@
|
||||
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b69241142cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2cd970e15f53fc0c82f950fd560ffa919b76172be017368a89913af074f400b093f89756aa3739ccc689ec0fcf3a360be32cc0b59b16e93a1e8bb4605726b2ca7a3ff706c4176649632b2cc68e1f912b8a578e3719ce7710885c7a966f49bcd430000
|
||||
1
testing/data/taproot/in_tr_deriv_key_len.psbt
Normal file
1
testing/data/taproot/in_tr_deriv_key_len.psbt
Normal file
@ -0,0 +1 @@
|
||||
70736274ff010071020000000127744ababf3027fe0d6cf23a96eee2efb188ef52301954585883e69b6624b2420000000000ffffffff02787c01000000000016001483a7e34bd99ff03a4962ef8a1a101bb295461ece606b042a010000001600147ac369df1b20e033d6116623957b0ac49f3c52e8000000000001012b00f2052a010000002251205a2c2cf5b52cf31f83ad2e8da63ff03183ecd8f609c7510ae8a48e03910a0757221602fe349064c98d6e2a853fa3c9b12bd8b304a19c195c60efa7ee2393046d3fa2321900772b2da75600008001000080000000800100000000000000000000
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:]
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user