diff --git a/docs/bitcoin-core-usage.md b/docs/bitcoin-core-usage.md index 364aa2217..6a0ad219d 100644 --- a/docs/bitcoin-core-usage.md +++ b/docs/bitcoin-core-usage.md @@ -1,37 +1,58 @@ # Using Coldcard with Bitcoin Core -## Background +As of Bitcoin Core v0.19.0+ the setup can be done fully airgapped, but spending +needs a USB connection and additional software such as [HWI](https://github.com/bitcoin-core/HWI). -Core has not always supported BIP32 hierarchical keys, and it does not presently -support BIP44 derivation. Instead it uses derivation like this: +## Setup Steps - m/0'/{change}'/{index}' +### Bitcoin Core v0.19.0+ -It will also, as of 0.16, do Segwit in P2SH by default. In time, `bech32` will -become the default address format. +For compatibility with other wallet software we use the BIP84 address derivation +(m/84'/0'/{account}'/{change}/{index}) and native SegWit (bech32) addresses. It's +recommended to set `addresstype=bech32` in [bitcoin.conf](https://github.com/bitcoin/bitcoin/blob/9546a785953b7f61a3a50e2175283cbf30bc2151/doc/bitcoin-conf.md). -## Setup Steps +First, generate a new seed phrase on the Coldcard. Then create a watch-only wallet +in Bitcoin Core: File -> Create Wallet. Give it a name, and ensure "Disable Private Keys" +is selected. -- generate a new seed phrase on the Coldcard -- export the xpub file from Coldcard (USB or MicroSD) -- import that xpub as a new wallet in core -- display balances +The public keys can exported via an SD card, or via USB. -## Day-to-day Operation +To export via SD card: -- generate unsigned transactions -- get that onto the Coldcard, and sign it there -- use core to broadcast the new txn for confirmation +- go to Advanced -> MicroSD card -> Bitcoin Core +- on your computer open public.txt, copy the `importmulti` command +- in Bitcoin Core, go to Windows -> Console +- select Coldcard in the wallet dropdown +- paste the `importmulti` command. It should respond with a success message + +To export via USB: + +- install HWI and follow the [instructions for Setup](https://github.com/bitcoin-core/HWI/blob/master/docs/bitcoin-core-usage.md#setup) +- during the `getkeypool` command, the use of `--wpkh` ensures compatibility with BIP84, +as long as you only use bech32 (native SegWit) addresses. -## Use of "dumpwallet" command +If you've used this wallet before, Bitcoin Core needs to rescan the blockchain to +show your balance and earlier transactions. Use the RPC command `rescanblockchain HEIGHT` +where `HEIGHT` is an old enough block (0 if you don't know). -- You can do a "dumpwallet" command and get the `xprv` associated with your -wallet. We can import that, and then you'd need to destroy the existing wallet -files, backups of those, and so on. +### Bitcoin Core v0.18.0 -- Our output file, called `public.txt`, can be compared to dumpwallet's output, but: - - you must find the section with appropriate derivation path for core - - core puts the addresses into a random order, not sequential like ours - - segwit, and p2sh segwit choice has to match +The same steps as Bitcoin Core v0.19.0, except that the wallet must be created +using the RPC (console window in the GUI): +``` +createwallet Coldcard true +``` + +## Day-to-day Operation + +### Bitcoin Core v0.18.0+ + +See HWI [instructions for usage](https://github.com/bitcoin-core/HWI/blob/master/docs/bitcoin-core-usage.md#usage). + +- generate unsigned transactions +- get that onto the Coldcard, and sign it there +- use core to broadcast the new txn for confirmation +When using the Bitcoin Core GUI (Graphical User Interface), avoid using P2SH wrapped receive +addresses, as this will cause incompatibility with other wallets. diff --git a/shared/actions.py b/shared/actions.py index 8d5c955b7..a010e9238 100644 --- a/shared/actions.py +++ b/shared/actions.py @@ -726,6 +726,24 @@ async def electrum_skeleton(*a): return MenuSystem(rv) +async def bitcoin_core_skeleton(*A): + # save output descriptors into a file + # - user has no choice, it's going to be bech32 with m/84'/{coin_type}'/0' path + import chains + + ch = chains.current_chain() + + if await ux_show_story('''\ +This saves a command onto the MicroSD card that includes the public keys.\ +You can then run that command in Bitcoin Core without ever connecting this Coldcard to a computer.\ +''' + SENSITIVE_NOT_SECRET) != 'y': + return + + # no choices to be made, just do it. + with imported('backups') as bk: + await bk.make_bitcoin_core_wallet() + + async def electrum_skeleton_step2(_1, _2, item): # pick a semi-random file name, render and save it. with imported('backups') as bk: diff --git a/shared/backups.py b/shared/backups.py index c0c8ad929..938ce9680 100644 --- a/shared/backups.py +++ b/shared/backups.py @@ -527,21 +527,13 @@ def generate_public_contents(): yield fp.getvalue() del fp -async def make_summary_file(fname_pattern='public.txt'): - # record **public** values and helpful data into a text file +# total_parts does need not be precise +async def write_text_file(fname_pattern, body, title, total_parts=72): from main import dis, pa, settings from files import CardSlot, CardMissingError from actions import needs_microsd - dis.fullscreen('Generating...') - - # generator function: - body = generate_public_contents() - - total_parts = 72 # need not be precise - # choose a filename - try: with CardSlot() as card: fname, nice = card.pick_filename(fname_pattern) @@ -559,9 +551,92 @@ async def make_summary_file(fname_pattern='public.txt'): await ux_show_story('Failed to write!\n\n\n'+str(e)) return - msg = '''Summary file written:\n\n%s''' % nice + msg = '''%s file written:\n\n%s''' % (title, nice) await ux_show_story(msg) +async def make_summary_file(fname_pattern='public.txt'): + from main import dis + + # record **public** values and helpful data into a text file + dis.fullscreen('Generating...') + + # generator function: + body = generate_public_contents() + + await write_text_file(fname_pattern, body, 'Summary') + +async def make_bitcoin_core_wallet(fname_pattern='bitcoin-core.txt'): + from main import dis, settings + import ustruct + xfp = b2a_hex(ustruct.pack(' Console in Bitcoin Core, +or using bitcoin-cli: + +importmulti '{payload}' + +'''.format(payload=payload, xfp=xfp, nb=chains.current_chain().name) + + await write_text_file(fname_pattern, body, 'Bitcoin Core') + +def generate_bitcoin_core_wallet(): + # Generate the data for an RPC command to import keys into Bitcoin Core + from descriptor import AddChecksum + from main import settings + import ustruct + + from public_constants import AF_P2WPKH + + chain = chains.current_chain() + assert chain.ctype in {'BTC', 'XTN'}, "Only Bitcoin supported" + + derive = "m/84'/{coin_type}'/{account}'".format(account=0, coin_type=chain.b44_cointype) + + with stash.SensitiveValues() as sv: + xpub = chain.serialize_public(sv.derive_path(derive)) + + xfp = settings.get('xfp') + txt_xfp = b2a_hex(ustruct.pack(' +# See SLIP 132 # for background on these version bytes. Not to be confused with SLIP-32 which involves Bech32. Slip132Version = namedtuple('Slip132Version', ('pub', 'priv', 'hint')) @@ -249,15 +249,12 @@ def current_chain(): # see bip49 for meaning of the meta vars CommonDerivations = [ # name, path.format(), addr format - ( '{core_name}', "m/{account}'/{change}'/{idx}'", AF_CLASSIC ), - ( '{core_name} (Segregated Witness, P2PKH)', - "m/{account}'/{change}'/{idx}'", AF_P2WPKH ), ( 'Electrum (not BIP44)', "m/{change}/{idx}", AF_CLASSIC ), ( 'BIP44 / Electrum', "m/44'/{coin_type}'/{account}'/{change}/{idx}", AF_CLASSIC ), ( 'BIP49 (P2WPKH-nested-in-P2SH)', "m/49'/{coin_type}'/{account}'/{change}/{idx}", AF_P2WPKH_P2SH ), # generates 3xxx/2xxx p2sh-looking addresses - ( 'BIP84 (Native Segwit P2PKH)', "m/84'/{coin_type}'/{account}'/{change}/{idx}", + ( 'BIP84 (Native Segwit P2WPKH)', "m/84'/{coin_type}'/{account}'/{change}/{idx}", AF_P2WPKH ), # generates bc1 bech32 addresses ] diff --git a/shared/descriptor.py b/shared/descriptor.py new file mode 100644 index 000000000..c059ed403 --- /dev/null +++ b/shared/descriptor.py @@ -0,0 +1,48 @@ +# From: https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp + +def PolyMod(c, val): + c0 = c >> 35 + c = ((c & 0x7ffffffff) << 5) ^ val + if (c0 & 1): + c ^= 0xf5dee51989 + if (c0 & 2): + c ^= 0xa9fdca3312 + if (c0 & 4): + c ^= 0x1bab10e32d + if (c0 & 8): + c ^= 0x3706b1677a + if (c0 & 16): + c ^= 0x644d626ffd + return c + +def DescriptorChecksum(desc): + INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ " + CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + + c = 1 + cls = 0 + clscount = 0 + for ch in desc: + pos = INPUT_CHARSET.find(ch) + if pos == -1: + return "" + c = PolyMod(c, pos & 31) + cls = cls * 3 + (pos >> 5) + clscount += 1 + if clscount == 3: + c = PolyMod(c, cls) + cls = 0 + clscount = 0 + if clscount > 0: + c = PolyMod(c, cls) + for j in range(0, 8): + c = PolyMod(c, 0) + c ^= 1 + + ret = [None] * 8 + for j in range(0, 8): + ret[j] = CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31] + return ''.join(ret) + +def AddChecksum(desc): + return desc + "#" + DescriptorChecksum(desc) diff --git a/shared/flow.py b/shared/flow.py index f7458ef60..f7c5b1f6b 100644 --- a/shared/flow.py +++ b/shared/flow.py @@ -59,6 +59,7 @@ async def which_pin_menu(_1,_2, item): MenuItem("Backup System", f=backup_everything), MenuItem("Dump Summary", f=dump_summary), MenuItem('Upgrade From SD', f=microsd_upgrade), + MenuItem("Bitcoin Core", f=bitcoin_core_skeleton), MenuItem("Electrum Wallet", f=electrum_skeleton), MenuItem("Wasabi Wallet", f=wasabi_skeleton), MenuItem('List Files', f=list_files),