commit 0d68b0827e7aac7c0202a1dc95d21a4353c6983e
parent 76386c21c7e1c6b766949144be00dd59036ba559
Author: chris-belcher <chris-belcher@users.noreply.github.com>
Date: Tue, 20 Mar 2018 02:11:10 +0000
finished code for deterministic wallets, all electrum mpks work now
Diffstat:
6 files changed, 192 insertions(+), 33 deletions(-)
diff --git a/README.md b/README.md
@@ -33,7 +33,8 @@ of any binaries before running them, or compile from source.
Download the latest release or clone the git repository. Enter the directory
and rename the file `config.cfg_sample` to `config.cfg`, edit this file to
configure your bitcoin node json-rpc authentication details. Next add your
-wallet addresses to the `[wallets]` section.
+wallet master public keys or watch-only addresses to the `[master-public-keys]`
+and `[watch-only-addresses]` sections.
Finally run `./server.py` on Linux or double-click `run-server.bat` on Windows.
The first time the server is run it will import all configured addresses as
@@ -41,9 +42,10 @@ watch-only into the Bitcoin node, and then exit giving you a chance to
rescan if your wallet contains historical transactions.
Tell Electrum to connect to the server in `Tools` -> `Server`, usually
-`localhost` if running on the same machine.
+`localhost` if running on the same machine. Make sure the port number matches
+whats written in `config.cfg`.
-Note that you can also try this with on [testnet bitcoin](https://en.bitcoin.it/wiki/Testnet).
+You can also try this with on [testnet bitcoin](https://en.bitcoin.it/wiki/Testnet).
Electrum can be started in testnet mode with the command line flag `--testnet`.
#### Exposure to the Internet
@@ -61,17 +63,15 @@ tunnel.
This project is in alpha stages as there are several essential missing
features such as:
-* Deterministic wallets and master public keys are not supported. Addresses
- must be imported individually.
-
* The Electrum server protocol has a caveat about multiple transactions included
in the same block. So there may be weird behaviour if that happens.
* There's no way to turn off debug messages, so the console will be spammed by
them when used.
-When trying this, make sure you report any crashes, odd behaviour or times when
-Electrum disconnects (which indicates the server behaved unexpectedly).
+When trying this, make sure you report any crashes, odd behaviour, transactions
+appearing as `Not Verified` or times when Electrum disconnects (which
+indicates the server behaved unexpectedly).
Someone should try running this on a Raspberry PI.
@@ -81,7 +81,10 @@ I welcome contributions. Please keep lines under 80 characters in length and
ideally don't add any external dependencies to keep this as easy to install as
possible.
-I can be contacted on freenode IRC on the `#bitcoin` and `#electrum` channels.
+I can be contacted on freenode IRC on the `#bitcoin` and `#electrum` channels,
+or by email.
+
+My PGP key fingerprint is: `0A8B 038F 5E10 CC27 89BF CFFF EF73 4EA6 77F3 1129`.
## Media Coverage
diff --git a/bitcoin/deterministic.py b/bitcoin/deterministic.py
@@ -141,3 +141,43 @@ def bip32_descend(*args):
for p in path:
key = bip32_ckd(key, p)
return bip32_extract_key(key)
+
+# electrum
+def electrum_stretch(seed):
+ return slowsha(seed)
+
+# Accepts seed or stretched seed, returns master public key
+
+def electrum_mpk(seed):
+ if len(seed) == 32:
+ seed = electrum_stretch(seed)
+ return privkey_to_pubkey(seed)[2:]
+
+# Accepts (seed or stretched seed), index and secondary index
+# (conventionally 0 for ordinary addresses, 1 for change) , returns privkey
+
+
+def electrum_privkey(seed, n, for_change=0):
+ if len(seed) == 32:
+ seed = electrum_stretch(seed)
+ mpk = electrum_mpk(seed)
+ offset = dbl_sha256(from_int_representation_to_bytes(n)+b':'+
+ from_int_representation_to_bytes(for_change)+b':'+
+ binascii.unhexlify(mpk))
+ return add_privkeys(seed, offset)
+
+# Accepts (seed or stretched seed or master pubkey), index and secondary index
+# (conventionally 0 for ordinary addresses, 1 for change) , returns pubkey
+
+def electrum_pubkey(masterkey, n, for_change=0):
+ if len(masterkey) == 32:
+ mpk = electrum_mpk(electrum_stretch(masterkey))
+ elif len(masterkey) == 64:
+ mpk = electrum_mpk(masterkey)
+ else:
+ mpk = masterkey
+ bin_mpk = encode_pubkey(mpk, 'bin_electrum')
+ offset = bin_dbl_sha256(from_int_representation_to_bytes(n)+b':'+
+ from_int_representation_to_bytes(for_change)+b':'+bin_mpk)
+ return add_pubkeys('04'+mpk, privtopub(offset))
+
diff --git a/bitcoin/py3specials.py b/bitcoin/py3specials.py
@@ -113,3 +113,5 @@ if sys.version_info.major == 3:
string = string[1:]
return result
+ def from_int_representation_to_bytes(a):
+ return bytes(str(a), 'utf-8')
diff --git a/config.cfg_sample b/config.cfg_sample
@@ -2,18 +2,15 @@
## Electrum Personal Server configuration file
## Comments start with #
-[electrum-master-public-keys]
+[master-public-keys]
## Add electrum master public keys to this section
+## Create a wallet in electrum then go Wallet -> Information
#any_key_works = xpub661MyMwAqRbcFseXCwRdRVkhVuzEiskg4QUp5XpUdNf2uGXvQmnD4zcofZ1MN6Fo8PjqQ5cemJQ39f7RTwDVVputHMFjPUn8VRp2pJQMgEF
# Multisig wallets use format `required-signatures [list of master pub keys]`
#multisig_wallet = 2 xpub661MyMwAqRbcFseXCwRdRVkhVuzEiskg4QUp5XpUdNf2uGXvQmnD4zcofZ1MN6Fo8PjqQ5cemJQ39f7RTwDVVputHMFjPUn8VRp2pJQMgEF xpub661MyMwAqRbcFseXCwRdRVkhVuzEiskg4QUp5XpUdNf2uGXvQmnD4zcofZ1MN6Fo8PjqQ5cemJQ39f7RTwDVVputHMFjPUn8VRp2pJQMgEF
-[bip39-master-public-keys]
-## Add master public keys in the bip39 standard to this section
-## Most hardware wallet keys go here
-
[watch-only-addresses]
## Add addresses to this section
@@ -35,7 +32,7 @@ poll_interval_connected = 5
# Parameters for dealing with deterministic wallets
# how many addresses to import first time, should be big because if you import too little you may have to rescan again
-initial_import_count = 200
+initial_import_count = 1000
# number of unused addresses kept at the head of the wallet
gap_limit = 25
diff --git a/deterministicwallet.py b/deterministicwallet.py
@@ -2,16 +2,40 @@
import bitcoin as btc
import util
+# the class hierarchy for deterministic wallets in this file:
+# subclasses are written towards the right
+# each class knows how to create the scriptPubKeys of that wallet
+#
+# |-- SingleSigOldMnemonicWallet
+# |-- SingleSigP2PKHWallet
+# |-- SingleSigP2WPKHWallet
+# SingleSigWallet --|
+# / |-- SingleSigP2WPKH_P2SHWallet
+# DeterministicWallet
+# \ |-- MultisigP2SHWallet
+# MultisigWallet --|
+# |-- MultisigP2WSHWallet
+# |-- MultisigP2WSH_P2SHWallet
+
#the wallet types are here
#https://github.com/spesmilo/electrum/blob/3.0.6/RELEASE-NOTES
#and
#https://github.com/spesmilo/electrum-docs/blob/master/xpub_version_bytes.rst
+def is_string_parsable_as_hex_int(s):
+ try:
+ int(s, 16)
+ return True
+ except:
+ return False
+
def parse_electrum_master_public_key(keydata, gaplimit):
if keydata[:4] in ("xpub", "tpub"):
- return SingleSigP2PKHWallet(keydata, gaplimit)
+ wallet = SingleSigP2PKHWallet(keydata)
elif keydata[:4] in ("zpub", "vpub"):
- return SingleSigP2WPKHWallet(keydata, gaplimit)
+ wallet = SingleSigP2WPKHWallet(keydata)
+ elif keydata[:4] in ("ypub", "upub"):
+ wallet = SingleSigP2WPKH_P2SHWallet(keydata)
elif keydata.find(" ") != -1: #multiple keys = multisig
chunks = keydata.split(" ")
try:
@@ -23,15 +47,21 @@ def parse_electrum_master_public_key(keydata, gaplimit):
if not all([pubkeys[0][:4] == pub[:4] for pub in pubkeys[1:]]):
raise ValueError("inconsistent bip32 pubkey types")
if pubkeys[0][:4] in ("xpub", "tpub"):
- return MultisigP2SHWallet(m, pubkeys, gaplimit)
- if pubkeys[0][:4] in("Zpub", "Vpub"):
- return MultisigP2WSHWallet(m, pubkeys, gaplimit)
+ wallet = MultisigP2SHWallet(m, pubkeys)
+ elif pubkeys[0][:4] in ("Zpub", "Vpub"):
+ wallet = MultisigP2WSHWallet(m, pubkeys)
+ elif pubkeys[0][:4] in ("Ypub", "Upub"):
+ wallet = MultisigP2WSH_P2SHWallet(m, pubkeys)
+ elif is_string_parsable_as_hex_int(keydata) and len(keydata) == 128:
+ wallet = SingleSigOldMnemonicWallet(keydata)
else:
raise ValueError("Unrecognized electrum mpk format: " + keydata[:4])
+ wallet.gaplimit = gaplimit
+ return wallet
class DeterministicWallet(object):
- def __init__(self, gaplimit):
- self.gaplimit = gaplimit
+ def __init__(self):
+ self.gaplimit = 0
self.next_index = [0, 0]
self.scriptpubkey_index = {}
@@ -73,19 +103,22 @@ class DeterministicWallet(object):
self.next_index[change] -= 1
class SingleSigWallet(DeterministicWallet):
- def __init__(self, mpk, gaplimit):
- super(SingleSigWallet, self).__init__(gaplimit)
+ def __init__(self, mpk):
+ super(SingleSigWallet, self).__init__()
self.branches = (btc.bip32_ckd(mpk, 0), btc.bip32_ckd(mpk, 1))
#m/change/i
def pubkey_to_scriptpubkey(self, pubkey):
raise RuntimeError()
+ def get_pubkey(self, change, index):
+ return btc.bip32_extract_key(btc.bip32_ckd(self.branches[change],
+ index))
+
def get_scriptpubkeys(self, change, from_index, count):
result = []
for index in range(from_index, from_index + count):
- pubkey = btc.bip32_extract_key(btc.bip32_ckd(self.branches[change],
- index))
+ pubkey = self.get_pubkey(change, index)
scriptpubkey = self.pubkey_to_scriptpubkey(pubkey)
self.scriptpubkey_index[scriptpubkey] = (change, index)
result.append(scriptpubkey)
@@ -105,9 +138,30 @@ class SingleSigP2WPKHWallet(SingleSigWallet):
#witness version is always 0, length is always 0x14
return "0014" + pkh
+class SingleSigP2WPKH_P2SHWallet(SingleSigWallet):
+ def pubkey_to_scriptpubkey(self, pubkey):
+ #witness-version length pubkeyhash
+ #witness version is always 0, length is always 0x14
+ redeem_script = '0014' + util.bh2u(util.hash_160(util.bfh(pubkey)))
+ sh = util.bh2u(util.hash_160(util.bfh(redeem_script)))
+ return "a914" + sh + "87"
+
+class SingleSigOldMnemonicWallet(SingleSigWallet):
+ def __init__(self, mpk):
+ super(SingleSigWallet, self).__init__()
+ self.mpk = mpk
+
+ def get_pubkey(self, change, index):
+ return btc.electrum_pubkey(self.mpk, index, change)
+
+ def pubkey_to_scriptpubkey(self, pubkey):
+ pkh = util.bh2u(util.hash_160(util.bfh(pubkey)))
+ #op_dup op_hash_160 length hash160 op_equalverify op_checksig
+ return "76a914" + pkh + "88ac"
+
class MultisigWallet(DeterministicWallet):
- def __init__(self, m, mpk_list, gaplimit):
- super(MultisigWallet, self).__init__(gaplimit)
+ def __init__(self, m, mpk_list):
+ super(MultisigWallet, self).__init__()
self.m = m
self.pubkey_branches = [(btc.bip32_ckd(mpk, 0), btc.bip32_ckd(mpk, 1))
for mpk in mpk_list]
@@ -138,15 +192,28 @@ class MultisigWallet(DeterministicWallet):
class MultisigP2SHWallet(MultisigWallet):
def redeem_script_to_scriptpubkey(self, redeem_script):
sh = util.bh2u(util.hash_160(util.bfh(redeem_script)))
- return "a914" + sh + "87"
#op_hash160 length hash160 op_equal
+ return "a914" + sh + "87"
class MultisigP2WSHWallet(MultisigWallet):
def redeem_script_to_scriptpubkey(self, redeem_script):
sh = util.bh2u(util.sha256(util.bfh(redeem_script)))
+ #witness-version length sha256
+ #witness version is always 0, length is always 0x20
return "0020" + sh
+
+class MultisigP2WSH_P2SHWallet(MultisigWallet):
+ def redeem_script_to_scriptpubkey(self, redeem_script):
#witness-version length sha256
#witness version is always 0, length is always 0x20
+ nested_redeemScript = "0020" + util.bh2u(util.sha256(
+ util.bfh(redeem_script)))
+ sh = util.bh2u(util.hash_160(util.bfh(nested_redeemScript)))
+ #op_hash160 length hash160 op_equal
+ return "a914" + sh + "87"
+
+# electrum has its own tests here
+#https://github.com/spesmilo/electrum/blob/03b40a3c0a7dd84e76bc0d0ea2ad390dafc92250/lib/tests/test_wallet_vertical.py
electrum_keydata_test_vectors = [
#p2pkh wallet
@@ -267,6 +334,52 @@ electrum_keydata_test_vectors = [
'00207a3e478266e5fe49fe22e3d8f04d3adda3b6a0835806a0db1f77b84d0ba7f79c',
'002059e66462023ecd54e20d4dce286795e7d5823af511989736edc0c7a844e249f5',
'0020bd8077906dd367d6d107d960397e46db2daba5793249f1f032d8d7e12e6f193c'])
+ , #p2wpkh-p2sh
+ ("upub5E4QEumGPNTmSKD95TrYX2xqLwwvBULbRzzHkrpW9WKKCB1y9DEfPXDnUyQjLjmVs" +
+ "7gSd7k5vRb1FoSb6BjyiWNg4arkJLaqk1jULzbwA5q",
+ ["a914ae8f84a06668742f713d0743c1f54d248040e63387", #recv
+ "a914c2e9bdcc48596b8cce418042ade72198fddf3cd987",
+ "a914a44b6ad63ccef0ae1741eaccee99bf2fa83f842987",
+ "a9148cf1c891d96a0be07893d0bddcf00ed5dad2c46e87",
+ "a91414d677b32f2409f4dfb3073d382c302bcd6ed33587",
+ "a9141b284bee7198d5134512f37ef60e4048864b4bd687"],
+ ["a914a5aacff65860440893107b01912dc8f60cadab2b87", #change
+ "a914dcd74ebc8bfc5cf0535717a3e833592d54b3c48687",
+ "a91446793cae4c2b8149ade61c1627b96b90599bc08787",
+ "a91439f3776831f321125bdb5099fbbd654923f8316c87"])
+ , #p2wpkh-p2sh
+ ("ypub6XrRLtXNB7NQo3vDaMNnffXVJe1WVaebXcb4ncpTHHADLuFYmf2CcPn96YzUbMt8s" +
+ "HSMmtr1mCcMgCBLqNdY2hrXXcdiLxCdD9e2dChBLun",
+ ["a91429c2ad045bbb162ef3c2d9cacb9812bec463061787", #recv
+ "a91433ec6bb67b113978d9cfd307a97fd15bc0a5a62087",
+ "a91450523020275ccbf4e916a0d8523ae42391ad988a87",
+ "a91438c2e5e76a874d86cfc914fe9fc1868b6afb5c5487"],
+ ["a91475f608698bb735120a17699fee854bce9a8dc8d387",
+ "a91477e69344ef53587051c85a06a52a646457b44e6c87",
+ "a914607c98ea34fbdffe39fee161ae2ffd5517bf1a5587"])
+ , #old mnemonic mpk
+ ("e9d4b7866dd1e91c862aebf62a49548c7dbf7bcc6e4b7b8c9da820c7737968df9c09d" +
+ "5a3e271dc814a29981f81b3faaf2737b551ef5dcc6189cf0f8252c442b3",
+ ["76a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac", #recv
+ "76a914c30f2af6a79296b6531bf34dba14c8419be8fb7d88ac",
+ "76a9145eb4eeaefcf9a709f8671444933243fbd05366a388ac",
+ "76a914f96669095e6df76cfdf5c7e49a1909f002e123d088ac"],
+ ["76a914ca14915184a2662b5d1505ce7142c8ca066c70e288ac", #change
+ "76a9148942ac692ace81019176c4fb0ac408b18b49237f88ac",
+ "76a914e1232622a96a04f5e5a24ca0792bb9c28b089d6e88ac"])
+ , #p2wsh-p2sh 2of2 multisig
+ ("2 Ypub6hWbqA2p47QgsLt5J4nxrR3ngu8xsPGb7PdV8CDh48KyNngNqPKSqertAqYhQ4u" +
+ "mELu1UsZUCYfj9XPA6AdSMZWDZQobwF7EJ8uNrECaZg1 Ypub6iNDhL4WWq5kFZcdFqHHw" +
+ "X4YTH4rYGp8xbndpRrY7WNZFFRfogSrL7wRTajmVHgR46AT1cqUG1mrcRd7h1WXwBsgX2Q" +
+ "vT3zFbBCDiSDLkau",
+ ["a91428060ade179c792fac07fc8817fd150ce7cdd3f987", #recv
+ "a9145ba5ed441b9f3e22f71193d4043b645183e6aeee87",
+ "a91484cc1f317b7d5afff115916f1e27319919601d0187",
+ "a9144001695a154cac4d118af889d3fdcaf929af315787",
+ "a914897888f3152a27cbd7611faf6aa01085931e542a87"],
+ ["a91454dbb52de65795d144f3c4faeba0e37d9765c85687", #change
+ "a914f725cbd61c67f34ed40355f243b5bb0650ce61c587",
+ "a9143672bcd3d02d3ea7c3205ddbc825028a0d2a781987"])
]
electrum_bad_keydata_test_vectors = [
diff --git a/server.py b/server.py
@@ -444,6 +444,7 @@ def check_for_new_txes(rpc, address_history, unconfirmed_txes,
#finding a confirmed and unconfirmed tx, in that order, then both confirm
#finding an unconfirmed and confirmed tx, in that order, then both confirm
#send a tx to an address which hasnt been used before
+ #import two addresses, transaction from one to the other, unc then confirm
obtained_txids = set()
updated_scripthashes = []
for tx in new_txes:
@@ -581,12 +582,11 @@ def get_scriptpubkeys_to_monitor(rpc, config):
[ADDRESSES_LABEL]))
deterministic_wallets = []
- for key in config.options("electrum-master-public-keys"):
+ for key in config.options("master-public-keys"):
wal = deterministicwallet.parse_electrum_master_public_key(
- config.get("electrum-master-public-keys", key),
+ config.get("master-public-keys", key),
int(config.get("bitcoin-rpc", "gap_limit")))
deterministic_wallets.append(wal)
- #add bip39 wallets here
#check whether these deterministic wallets have already been imported
import_needed = False
@@ -649,14 +649,18 @@ def get_scriptpubkeys_to_monitor(rpc, config):
def import_addresses(rpc, addrs):
debug("importing addrs = " + str(addrs))
+ log("Importing " + str(len(addrs)) + " addresses in total")
+ st = time.time()
for a in addrs:
rpc.call("importaddress", [a, ADDRESSES_LABEL, False])
+ et = time.time()
+ debug("imported addresses in " + str(et - st) + " sec")
def main():
try:
config = ConfigParser()
config.read(["config.cfg"])
- config.options("electrum-master-public-keys")
+ config.options("master-public-keys")
except NoSectionError:
log("Non-existant configuration file `config.cfg`")
return