URI: 
       tfinished code for deterministic wallets, all electrum mpks work now - electrum-personal-server - Maximally lightweight electrum server for a single user
  HTML git clone https://git.parazyd.org/electrum-personal-server
   DIR Log
   DIR Files
   DIR Refs
   DIR README
       ---
   DIR commit 0d68b0827e7aac7c0202a1dc95d21a4353c6983e
   DIR parent 76386c21c7e1c6b766949144be00dd59036ba559
  HTML 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:
         M README.md                           |      21 ++++++++++++---------
         M bitcoin/deterministic.py            |      40 +++++++++++++++++++++++++++++++
         M bitcoin/py3specials.py              |       2 ++
         M config.cfg_sample                   |       9 +++------
         M deterministicwallet.py              |     141 ++++++++++++++++++++++++++++---
         M server.py                           |      12 ++++++++----
       
       6 files changed, 192 insertions(+), 33 deletions(-)
       ---
   DIR diff --git a/README.md b/README.md
       t@@ -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
       t@@ -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
       t@@ -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.
        
       t@@ -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
        
   DIR diff --git a/bitcoin/deterministic.py b/bitcoin/deterministic.py
       t@@ -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))
       +
   DIR diff --git a/bitcoin/py3specials.py b/bitcoin/py3specials.py
       t@@ -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')
   DIR diff --git a/config.cfg_sample b/config.cfg_sample
       t@@ -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
       t@@ -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
        
   DIR diff --git a/deterministicwallet.py b/deterministicwallet.py
       t@@ -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:
       t@@ -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 = {}
        
       t@@ -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)
       t@@ -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]
       t@@ -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
       t@@ -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 = [
   DIR diff --git a/server.py b/server.py
       t@@ -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:
       t@@ -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
       t@@ -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