Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 44 additions & 23 deletions docs/bitcoin-core-usage.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 18 additions & 0 deletions shared/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
97 changes: 86 additions & 11 deletions shared/backups.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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('<I', settings.get('xfp'))).decode().lower()

dis.fullscreen('Generating...')

# generator function:
payload = ujson.dumps(generate_bitcoin_core_wallet())

body = '''\
# Bitcoin Core Wallet Import File

https://github.com/Coldcard/firmware/blob/master/docs/bitcoin-core-usage.md

## For wallet with master key fingerprint: {xfp}

Wallet operates on blockchain: {nb}

## IMPORTANT WARNING

Do **not** deposit to any address in this file unless you have a working
wallet system that is ready to handle the funds at that address!

## Bitcoin Core RPC

The following command can be entered after opening Window -> 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('<I', xfp)).decode().lower()

chain = chains.current_chain()

_,vers,_ = version.get_mpy_version()

return list(map(lambda internal: {
'desc': AddChecksum("wpkh([{fingerprint}/84h/{coin_type}h/{account}h]{xpub}/{change}/*)".format(
fingerprint=txt_xfp,
coin_type=chain.b44_cointype,
account=0,
xpub=xpub,
change=1 if internal else 0
)),
'range': [0, 1000],
'timestamp': 'now',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does Colcard have a clock and know when the seed was generated? If so, that could be added here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sadly Coldcard has no idea of time nor block height.

'internal': internal,
'keypool': True,
'watchonly': True
}, [False, True]))

def generate_wasabi_wallet():
# Generate the data for a JSON file which Wasabi can open directly as a new wallet.
Expand Down
7 changes: 2 additions & 5 deletions shared/chains.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from ucollections import namedtuple
from opcodes import OP_CHECKMULTISIG

# See SLIP 132 <https://github.com/satoshilabs/slips/blob/master/slip-0132.md>
# See SLIP 132 <https://github.com/satoshilabs/slips/blob/master/slip-0132.md>
# for background on these version bytes. Not to be confused with SLIP-32 which involves Bech32.
Slip132Version = namedtuple('Slip132Version', ('pub', 'priv', 'hint'))

Expand Down Expand Up @@ -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
]

Expand Down
48 changes: 48 additions & 0 deletions shared/descriptor.py
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions shared/flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down