commit c18d6c9b136a10dc94869e744bb2544bdce5a72e
parent 2c8cd9aa12ae7d5344e9ecf2c518a642f7a0da6d
Author: chris-belcher <chris-belcher@users.noreply.github.com>
Date: Tue, 27 Mar 2018 20:23:17 +0100
moved python modules and ssl certs to other directories to make root less cluttered
Diffstat:
12 files changed, 1432 insertions(+), 1428 deletions(-)
diff --git a/cert.crt b/certs/cert.crt
diff --git a/cert.key b/certs/cert.key
diff --git a/deterministicwallet.py b/deterministicwallet.py
@@ -1,429 +0,0 @@
-
-import bitcoin as btc
-from hashes import bh2u, hash_160, bfh, sha256
-
-# 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"):
- wallet = SingleSigP2PKHWallet(keydata)
- elif keydata[:4] in ("zpub", "vpub"):
- wallet = SingleSigP2WPKHWallet(keydata)
- elif keydata[:4] in ("ypub", "upub"):
- wallet = SingleSigP2WPKH_P2SHWallet(keydata)
- elif keydata.find(" ") != -1: #multiple keys = multisig
- chunks = keydata.split(" ")
- try:
- m = int(chunks[0])
- except ValueError:
- raise ValueError("Unable to parse m in multisig key data: "
- + chunks[0])
- pubkeys = chunks[1:]
- 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"):
- 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):
- self.gaplimit = 0
- self.next_index = [0, 0]
- self.scriptpubkey_index = {}
-
- def get_new_scriptpubkeys(self, change, count):
- """Returns newly-generated addresses from this deterministic wallet"""
- return self.get_scriptpubkeys(change, self.next_index[change],
- count)
-
- def get_scriptpubkeys(self, change, from_index, count):
- """Returns addresses from this deterministic wallet"""
- pass
-
- #called in check_for_new_txes() when a new tx of ours arrives
- #to see if we need to import more addresses
- def have_scriptpubkeys_overrun_gaplimit(self, scriptpubkeys):
- """Return None if they havent, or how many addresses to
- import if they have"""
- result = {}
- for spk in scriptpubkeys:
- if spk not in self.scriptpubkey_index:
- continue
- change, index = self.scriptpubkey_index[spk]
- distance_from_next = self.next_index[change] - index
- if distance_from_next > self.gaplimit:
- continue
- #need to import more
- if change in result:
- result[change] = max(result[change], self.gaplimit
- - distance_from_next + 1)
- else:
- result[change] = self.gaplimit - distance_from_next + 1
- if len(result) > 0:
- return result
- else:
- return None
-
- def rewind_one(self, change):
- """Go back one pubkey in a branch"""
- self.next_index[change] -= 1
-
-class SingleSigWallet(DeterministicWallet):
- 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 = self.get_pubkey(change, index)
- scriptpubkey = self.pubkey_to_scriptpubkey(pubkey)
- self.scriptpubkey_index[scriptpubkey] = (change, index)
- result.append(scriptpubkey)
- self.next_index[change] = max(self.next_index[change], from_index+count)
- return result
-
-class SingleSigP2PKHWallet(SingleSigWallet):
- def pubkey_to_scriptpubkey(self, pubkey):
- pkh = bh2u(hash_160(bfh(pubkey)))
- #op_dup op_hash_160 length hash160 op_equalverify op_checksig
- return "76a914" + pkh + "88ac"
-
-class SingleSigP2WPKHWallet(SingleSigWallet):
- def pubkey_to_scriptpubkey(self, pubkey):
- pkh = bh2u(hash_160(bfh(pubkey)))
- #witness-version length hash160
- #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' + bh2u(hash_160(bfh(pubkey)))
- sh = bh2u(hash_160(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 = bh2u(hash_160(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):
- 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]
- #derivation path for pubkeys is m/change/index
-
- def redeem_script_to_scriptpubkey(self, redeem_script):
- raise RuntimeError()
-
- def get_scriptpubkeys(self, change, from_index, count):
- result = []
- for index in range(from_index, from_index + count):
- pubkeys = [btc.bip32_extract_key(btc.bip32_ckd(branch[change],
- index)) for branch in self.pubkey_branches]
- pubkeys = sorted(pubkeys)
- redeemScript = ""
- redeemScript += "%x"%(0x50 + self.m) #op_m
- for p in pubkeys:
- redeemScript += "21" #length
- redeemScript += p
- redeemScript += "%x"%(0x50 + len(pubkeys)) #op_n
- redeemScript += "ae" # op_checkmultisig
- scriptpubkey = self.redeem_script_to_scriptpubkey(redeemScript)
- self.scriptpubkey_index[scriptpubkey] = (change, index)
- result.append(scriptpubkey)
- self.next_index[change] = max(self.next_index[change], from_index+count)
- return result
-
-class MultisigP2SHWallet(MultisigWallet):
- def redeem_script_to_scriptpubkey(self, redeem_script):
- sh = bh2u(hash_160(bfh(redeem_script)))
- #op_hash160 length hash160 op_equal
- return "a914" + sh + "87"
-
-class MultisigP2WSHWallet(MultisigWallet):
- def redeem_script_to_scriptpubkey(self, redeem_script):
- sh = bh2u(sha256(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" + bh2u(sha256(bfh(redeem_script)))
- sh = bh2u(hash_160(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
- ("xpub661MyMwAqRbcGVQTLtBFzc3ENvyZHoUEhWRdGwoqLZaf5wXP9VcDY2VJV7usvsFLZz" +
- "2RUTVhCVXYXc3S8zpLyAFbDFcfrpUiwLoE9VWH2yz", #pubkey
- ["76a914b1847c763c9a9b12631ab42335751c1bf843880c88ac" #recv scriptpubkeys
- ,"76a914d8b6b932e892fad5132ea888111adac2171c5af588ac"
- ,"76a914e44b19ef74814f977ae4e2823dd0a0b33480472a88ac"],
- ["76a914d2c2905ca383a5b8f94818cb7903498061a6286688ac" #change scriptpubkeys
- ,"76a914e7b4ddb7cede132e84ba807defc092cf52e005b888ac"
- ,"76a91433bdb046a1d373728d7844df89aa24f788443a4588ac"])
- , #p2wpkh wallet
- ("zpub6mr7wBKy3oJn89TCiXUAPBWpTTTx58BgEjPLzDNf5kMThvd6xchrobPTsJ5mP" +
- "w3NJ7zRhckN8cv4FhQBfwurZzNE5uTW5C5PYqNTkRAnTkP", #pubkey
- ['00142b82c61a7a48b7b10801f0eb247af46821bd33f5' #recv scriptpubkeys
- ,'0014073dc6bcbb18d6468c5996bdeba926f6805b74b1'
- ,'001400fa0b5cb21e8d442a7bd61af3d558a62be0c9aa'],
- ['00144f4a0655a4b586be1e08d97a2f55125120b84c69' #change scriptpubkeys
- ,'0014ef7967a7a56c23bbc9f317e612c93a5e23d25ffe'
- ,'0014ad768a11730bf54d10c72184d53239de0f310bc9'])
- ,#p2sh 2of2 multisig wallet
- ("2 tpubD6NzVbkrYhZ4YVMVzC7wZeRfz3bhqcHvV8M3UiULCfzFtLtp5nwvi6LnBQegrkx" +
- "YGPkSzXUEvcPEHcKdda8W1YShVBkhFBGkLxjSQ1Nx3cJ tpubD6NzVbkrYhZ4WjgNYq2nF" +
- "TbiSLW2SZAzs4g5JHLqwQ3AmR3tCWpqsZJJEoZuP5HAEBNxgYQhtWMezszoaeTCg6FWGQB" +
- "T74sszGaxaf64o5s", #m=2, 2 pubkeys, n=len(pubkeys)
- ['a914fe30a46a4e1b41f9bb758448fd84ee4628c103e187' #recv
- ,'a914dad5dd605871560ae5d219cd6275e6ad19bc6b9987'
- ,'a914471e158e2db190acdd8c76ed6d2ade102fe1e8ac87'
- ,'a914013449715a32f21d1a8a2b95a01b40eb41ada16f87'
- ,'a914ae3dd25567fb7c2f87be41220dd14025ca68b0e087'
- ,'a91462b90344947b610c4eadb7dd460fee3f32fefe7687'
- ,'a914d4388c7d5771ebf26b6e650c42e60e4cf7d4c5a187'
- ,'a914e4f0832e56591d01b71c72b9a3777dc8f9d9a92e87'
- ,'a914a5d5accd96d27403c7663b92fdb57299d7a871eb87'
- ,'a914f8f2c6ef2d80f972e4d8b418a15337a3c38af37f87'
- ,'a914a2bd2f67fac7c24e609b574ccc8cfaa2f90ebf8c87'
- ,'a914a56298a7decde1d18306f55d9305577c3fce690187'
- ,'a91430f2f83238ac29125a539055fa59efc86a73a23987'
- ,'a914263b4585d0735c5065987922af359d5eabeb880d87'
- ,'a91455d9d47113fb8b37705bdf6d4107d438afd63e4687'
- ,'a914970d754163b8957b73f4e8baaf23dea5f6e3db2287'
- ,'a914facbc921203a9ffd751cc246a884918beaac21b687'
- ,'a914fc7556833eca1e0f84c6d7acb875e645f7ed4e9687'
- ,'a914bbfe6a032d633f113b5d605e3a97cc08a47cc87d87'
- ,'a91403d733c4ca337b5fa1de95970ba6f898a9d36c4887'
- ,'a9148af27dc7c950e17c11e164065e672cd60ae3d48d87'
- ,'a914c026aa45377f2a4a62136bac1d3350c318fee5c587'
- ,'a9146337f59e3ea55e73725c9f2fc52a5ca5d68c361687'],
- ['a914aeaebf9d567ab8a6813e89668e16f40bf419408e87' #change
- ,'a914f2a6264dd3975297fa2a5a8e17321299a44f76d987'
- ,'a9142067a6c47958090a645137cc0898c0c7bbc69b5387'
- ,'a914210840f77ea5b7eb11cb55e5d719a93b7746fb9387'
- ,'a914163db6b8ca00362be63a26502c5f7bf64787506b87'
- ,'a91479b2c527594059c056e5367965ae92bbcf63512187'])
- ,#p2sh 2of3 multisig wallet
- ("2 tpubD6NzVbkrYhZ4WwaMJ3od4hANxdMVpb63Du3ERq1xjtowxVJEcTbGH2rFd9TFXxw" +
- "KJRKDn9vQjDPxFeaku6BHW6wHn2KPF1ijS4LwgwQFJ3B tpubD6NzVbkrYhZ4Wjv4ZRPD6" +
- "MNdiLmfvXztbKuuatkqHjukU3S6GXhmKnbAF5eU9bR2Nryiq8v67emUUSM1VUrAx5wcZ19" +
- "AsaGg3ZLmjbbwLXr tpubD6NzVbkrYhZ4Xxa2fEp7YsbnFnwuQNaogijbiX42Deqd4NiAD" +
- "tqNU6AXCU2d2kPFWBpAGG7K3HAKYwUfZBPgTLkfQp2dDg9SLVnkgYPgEXN",
- ['a914167c95beb25b984ace517d4346e6cdbf1381793687', #recv addrs
- 'a914378bbda1ba7a713de18c3ba3c366f42212bfb45087',
- 'a9142a5c9881c70906180f37dd02d8c830e9b6328d4a87',
- 'a914ffe0832375b72ee5307bfa502896ba28cc470ee987',
- 'a9147607d40e039fbea57d9c04e48b198c9fcf3356c187',
- 'a9148d9582ad4cf0581c6e0697e4cba6a12e66ca1a0087',
- 'a914d153a743b315ba19690823119019e16e3762104d87',
- 'a914b4accc89e48610043e70371153fd8cb5a3eef34287',
- 'a91406febca615e3631253fd75a1d819436e1d046e0487',
- 'a914b863cbb888c6b28291cb87a2390539e28be37a9587',
- 'a914ec39094e393184d2c352a29b9d7a3caddaccb6cf87',
- 'a914da4faa4babbdf611caf511d287133f06c1c3244a87',
- 'a9146e64561d0c5e2e9159ecff65db02e04b3277402487',
- 'a914377d66386972492192ae827fb2208596af0941d187',
- 'a914448d364ff2374449e57df13db33a40f5b099997c87',
- 'a914f24b875d2cb99e0b138ab0e6dd65027932b3c6e787',
- 'a914aa4bcee53406b1ef6c83852e3844e38a3a9d9f3087',
- 'a9145e5ec40fdab54be0d6e21107bc38c39df97e37fc87',
- 'a9141de4d402c82f4e9b0e6b792b331232a5405ebd3f87',
- 'a9148873ee280e51f9c64d257dd6dedc8712fd652cc687'],
- ['a9142cc87d7562a85029a57cc37026e12dab72223db287', #change
- 'a91499f4aee0b274f0b3ab48549a2c58cd667a62c0cb87',
- 'a91497a89cd5ada3a766a1275f8151e9256fcf537f6c87',
- 'a9147ffc9f3a3b60635ea1783243274f4d07ab617cb487',
- 'a9143423113ab913d86fd47e55488a0c559e18b457b987',
- 'a914a28a3773a37c52ff6fd7dff497d0eaf80a46febb87'])
- , #p2wsh 1of2 multisig wallet
- ("1 Vpub5fAqpSRkLmvXwqbuR61MaKMSwj5z5xUBwanaz3qnJ5MgaBDpFSLUvKTiNK9zHp" +
- "dvrg2LHHXkKxSXBHNWNpZz9b1VqADjmcCs3arSoxN3F3r Vpub5fvEo4MUpbVs9sZqr45" +
- "zmRVEsTcQ49MA9m3MLht3XzdZvS9eMXLLu1H6TL1j2SMnykHqXNzG5ycMyQmFDvEE5B32" +
- "sP8TmRe6wW8HjBgMssh",
- #recv scriptpubkeys
- ['002031fbaa839e96fc1abaf3453b9f770e0ccfe2d8e3e990bb381fdcb7db4722986a',
- '0020820ae739b36f4feb1c299ced201db383bbcf1634e0071e489b385f43c2323761',
- '0020eff05f4d14aa1968a7142b1009aa57a6208fb01b212f8b8f7df63645d26a1292',
- '002049c6e17979dca380ffb66295d27f609bea2879d4f0b590c96c70ff12260a8721',
- '002002bf2430fc7ebc6fb27da1cb80e52702edcc62a29f65c997e5c924dcd98411bd',
- '0020c7a58dcf9633453ba12860b57c14af67d87d022be5c52bf6be7a6abdc295c6e0',
- '0020136696059a5e932c72f4f0a05fa7f52faf9b54f1b7694e15acce710e6cc9e89d',
- '0020c372e880227f35c2ee35d0724bf05cea95e74dcb3e6aa67ff15f561a29c0645d',
- '002095c705590e2b84996fa44bff64179b26669e53bbd58d76bb6bbb5c5498a981ce',
- '00207217754dae083c3c365c7e1ce3ad889ca2bd88e4f809cec66b9987adc390aa26',
- '0020bee30906450e099357cc96a1f472c1ef70089cd4a0cba96749adfe1c9a2f9e87',
- '0020b1838b3d5a386ad6c90eeae9a27a9b812e32ce06376f261dea89e405bc8209d9',
- '0020231a3d05886efff601f0702d4c8450dfcce8d6a4bd90f17f7ff76f5c25c632de',
- '002071220f3941b5f65aca90e464db4291cd5ea63f37fa858fd5b66d5019f0dbab0f',
- '0020fc3c7db9f0e773f9f9c725d4286ddcc88db9575c45b2441d458018150eb4ef10',
- '00209f037bfc98dee2fc0d3cca54df09b2d20e92a0133fa381a4dd74c49e4d0a89f5',
- '0020c9060d0554ba2ca92048e1772e806d796ba41f10bf6aee2653a9eba96b05c944',
- '0020a7cb1dd2730dba564f414ed8d9312370ff89c34df1441b83125cb4d97a96005a',
- '00209fddc9b4e070b887dec034ed74f15f62d075a3ac8cf6eb95a88c635e0207534c',
- '0020c48f9c50958ab8e386a8bd3888076f31d12e5cf011ff46cc83c6fadfe6d47d20',
- '0020a659f4621dca404571917e73dedb26b6d7c49a07dacbf15890760ac0583d3267'],
- #change scriptpubkeys
- ['002030213b5d3b6988b86aa13a9eaca08e718d51f32dc130c70981abb0102173c791',
- '002027bd198f9783a58e9bc4d3fdbd1c75cc74154905cce1d23c7bd3e051695418fe',
- '0020c1fd2cdebf120d3b1dc990dfdaca62382ff9525beeb6a79a908ddecb40e2162c',
- '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 = [
- "zpub661MyMwAqRbcGVQTLtBFzc3ENvyZHoUEhWRdGwoqLZaf5wXP9VcDY2VJV7usvsFLZz" +
- "2RUTVhCVXYXc3S8zpLyAFbDFcfrpUiwLoE9VWH2yz", #bad checksum
- "a tpubD6NzVbkrYhZ4YVMVzC7wZeRfz3bhqcHvV8M3UiULCfzFtLtp5nwvi6LnBQegrkx" +
- "YGPkSzXUEvcPEHcKdda8W1YShVBkhFBGkLxjSQ1Nx3cJ tpubD6NzVbkrYhZ4WjgNYq2nF" +
- "TbiSLW2SZAzs4g5JHLqwQ3AmR3tCWpqsZJJEoZuP5HAEBNxgYQhtWMezszoaeTCg6FWGQB" +
- "T74sszGaxaf64o5s", #unparsable m number
- "2 tpubD6NzVbkrYhZ4YVMVzC7wZeRfz3bhqcHvV8M3UiULCfzFtLtp5nwvi6LnBQegrkx" +
- "YGPkSzXUEvcPEHcKdda8W1YShVBkhFBGkLxjSQ1Nx3cJ Vpub5fAqpSRkLmvXwqbuR61M" +
- "aKMSwj5z5xUBwanaz3qnJ5MgaBDpFSLUvKTiNK9zHpdvrg2LHHXkKxSXBHNWNpZz9b1Vq" +
- "ADjmcCs3arSoxN3F3r" #inconsistent magic
-]
-
-def test():
- for keydata, recv_spks, change_spks in electrum_keydata_test_vectors:
- initial_count = 15
- gaplimit = 5
- wal = parse_electrum_master_public_key(keydata, gaplimit)
- spks = wal.get_scriptpubkeys(0, 0, initial_count)
- #for test, generate 15, check that the last 5 lead to gap limit overrun
- for i in range(initial_count - gaplimit):
- ret = wal.have_scriptpubkeys_overrun_gaplimit([spks[i]])
- assert ret == None
- for i in range(gaplimit):
- index = i + initial_count - gaplimit
- ret = wal.have_scriptpubkeys_overrun_gaplimit([spks[index]])
- assert ret != None and ret[0] == i+1
- last_index_add = 3
- last_index = initial_count - gaplimit + last_index_add
- ret = wal.have_scriptpubkeys_overrun_gaplimit(spks[2:last_index])
- assert ret[0] == last_index_add
- assert wal.get_scriptpubkeys(0, 0, len(recv_spks)) == recv_spks
- assert wal.get_scriptpubkeys(1, 0, len(change_spks)) == change_spks
- for keydata in electrum_bad_keydata_test_vectors:
- try:
- parse_electrum_master_public_key(keydata, 5)
- raised_error = False
- except (ValueError, Exception):
- raised_error = True
- assert raised_error
- print("All tests passed successfully")
-
-if __name__ == "__main__":
- test()
- pass
-
diff --git a/electrumpersonalserver/deterministicwallet.py b/electrumpersonalserver/deterministicwallet.py
@@ -0,0 +1,429 @@
+
+import bitcoin as btc
+from electrumpersonalserver.hashes import bh2u, hash_160, bfh, sha256
+
+# 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"):
+ wallet = SingleSigP2PKHWallet(keydata)
+ elif keydata[:4] in ("zpub", "vpub"):
+ wallet = SingleSigP2WPKHWallet(keydata)
+ elif keydata[:4] in ("ypub", "upub"):
+ wallet = SingleSigP2WPKH_P2SHWallet(keydata)
+ elif keydata.find(" ") != -1: #multiple keys = multisig
+ chunks = keydata.split(" ")
+ try:
+ m = int(chunks[0])
+ except ValueError:
+ raise ValueError("Unable to parse m in multisig key data: "
+ + chunks[0])
+ pubkeys = chunks[1:]
+ 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"):
+ 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):
+ self.gaplimit = 0
+ self.next_index = [0, 0]
+ self.scriptpubkey_index = {}
+
+ def get_new_scriptpubkeys(self, change, count):
+ """Returns newly-generated addresses from this deterministic wallet"""
+ return self.get_scriptpubkeys(change, self.next_index[change],
+ count)
+
+ def get_scriptpubkeys(self, change, from_index, count):
+ """Returns addresses from this deterministic wallet"""
+ pass
+
+ #called in check_for_new_txes() when a new tx of ours arrives
+ #to see if we need to import more addresses
+ def have_scriptpubkeys_overrun_gaplimit(self, scriptpubkeys):
+ """Return None if they havent, or how many addresses to
+ import if they have"""
+ result = {}
+ for spk in scriptpubkeys:
+ if spk not in self.scriptpubkey_index:
+ continue
+ change, index = self.scriptpubkey_index[spk]
+ distance_from_next = self.next_index[change] - index
+ if distance_from_next > self.gaplimit:
+ continue
+ #need to import more
+ if change in result:
+ result[change] = max(result[change], self.gaplimit
+ - distance_from_next + 1)
+ else:
+ result[change] = self.gaplimit - distance_from_next + 1
+ if len(result) > 0:
+ return result
+ else:
+ return None
+
+ def rewind_one(self, change):
+ """Go back one pubkey in a branch"""
+ self.next_index[change] -= 1
+
+class SingleSigWallet(DeterministicWallet):
+ 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 = self.get_pubkey(change, index)
+ scriptpubkey = self.pubkey_to_scriptpubkey(pubkey)
+ self.scriptpubkey_index[scriptpubkey] = (change, index)
+ result.append(scriptpubkey)
+ self.next_index[change] = max(self.next_index[change], from_index+count)
+ return result
+
+class SingleSigP2PKHWallet(SingleSigWallet):
+ def pubkey_to_scriptpubkey(self, pubkey):
+ pkh = bh2u(hash_160(bfh(pubkey)))
+ #op_dup op_hash_160 length hash160 op_equalverify op_checksig
+ return "76a914" + pkh + "88ac"
+
+class SingleSigP2WPKHWallet(SingleSigWallet):
+ def pubkey_to_scriptpubkey(self, pubkey):
+ pkh = bh2u(hash_160(bfh(pubkey)))
+ #witness-version length hash160
+ #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' + bh2u(hash_160(bfh(pubkey)))
+ sh = bh2u(hash_160(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 = bh2u(hash_160(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):
+ 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]
+ #derivation path for pubkeys is m/change/index
+
+ def redeem_script_to_scriptpubkey(self, redeem_script):
+ raise RuntimeError()
+
+ def get_scriptpubkeys(self, change, from_index, count):
+ result = []
+ for index in range(from_index, from_index + count):
+ pubkeys = [btc.bip32_extract_key(btc.bip32_ckd(branch[change],
+ index)) for branch in self.pubkey_branches]
+ pubkeys = sorted(pubkeys)
+ redeemScript = ""
+ redeemScript += "%x"%(0x50 + self.m) #op_m
+ for p in pubkeys:
+ redeemScript += "21" #length
+ redeemScript += p
+ redeemScript += "%x"%(0x50 + len(pubkeys)) #op_n
+ redeemScript += "ae" # op_checkmultisig
+ scriptpubkey = self.redeem_script_to_scriptpubkey(redeemScript)
+ self.scriptpubkey_index[scriptpubkey] = (change, index)
+ result.append(scriptpubkey)
+ self.next_index[change] = max(self.next_index[change], from_index+count)
+ return result
+
+class MultisigP2SHWallet(MultisigWallet):
+ def redeem_script_to_scriptpubkey(self, redeem_script):
+ sh = bh2u(hash_160(bfh(redeem_script)))
+ #op_hash160 length hash160 op_equal
+ return "a914" + sh + "87"
+
+class MultisigP2WSHWallet(MultisigWallet):
+ def redeem_script_to_scriptpubkey(self, redeem_script):
+ sh = bh2u(sha256(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" + bh2u(sha256(bfh(redeem_script)))
+ sh = bh2u(hash_160(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
+ ("xpub661MyMwAqRbcGVQTLtBFzc3ENvyZHoUEhWRdGwoqLZaf5wXP9VcDY2VJV7usvsFLZz" +
+ "2RUTVhCVXYXc3S8zpLyAFbDFcfrpUiwLoE9VWH2yz", #pubkey
+ ["76a914b1847c763c9a9b12631ab42335751c1bf843880c88ac" #recv scriptpubkeys
+ ,"76a914d8b6b932e892fad5132ea888111adac2171c5af588ac"
+ ,"76a914e44b19ef74814f977ae4e2823dd0a0b33480472a88ac"],
+ ["76a914d2c2905ca383a5b8f94818cb7903498061a6286688ac" #change scriptpubkeys
+ ,"76a914e7b4ddb7cede132e84ba807defc092cf52e005b888ac"
+ ,"76a91433bdb046a1d373728d7844df89aa24f788443a4588ac"])
+ , #p2wpkh wallet
+ ("zpub6mr7wBKy3oJn89TCiXUAPBWpTTTx58BgEjPLzDNf5kMThvd6xchrobPTsJ5mP" +
+ "w3NJ7zRhckN8cv4FhQBfwurZzNE5uTW5C5PYqNTkRAnTkP", #pubkey
+ ['00142b82c61a7a48b7b10801f0eb247af46821bd33f5' #recv scriptpubkeys
+ ,'0014073dc6bcbb18d6468c5996bdeba926f6805b74b1'
+ ,'001400fa0b5cb21e8d442a7bd61af3d558a62be0c9aa'],
+ ['00144f4a0655a4b586be1e08d97a2f55125120b84c69' #change scriptpubkeys
+ ,'0014ef7967a7a56c23bbc9f317e612c93a5e23d25ffe'
+ ,'0014ad768a11730bf54d10c72184d53239de0f310bc9'])
+ ,#p2sh 2of2 multisig wallet
+ ("2 tpubD6NzVbkrYhZ4YVMVzC7wZeRfz3bhqcHvV8M3UiULCfzFtLtp5nwvi6LnBQegrkx" +
+ "YGPkSzXUEvcPEHcKdda8W1YShVBkhFBGkLxjSQ1Nx3cJ tpubD6NzVbkrYhZ4WjgNYq2nF" +
+ "TbiSLW2SZAzs4g5JHLqwQ3AmR3tCWpqsZJJEoZuP5HAEBNxgYQhtWMezszoaeTCg6FWGQB" +
+ "T74sszGaxaf64o5s", #m=2, 2 pubkeys, n=len(pubkeys)
+ ['a914fe30a46a4e1b41f9bb758448fd84ee4628c103e187' #recv
+ ,'a914dad5dd605871560ae5d219cd6275e6ad19bc6b9987'
+ ,'a914471e158e2db190acdd8c76ed6d2ade102fe1e8ac87'
+ ,'a914013449715a32f21d1a8a2b95a01b40eb41ada16f87'
+ ,'a914ae3dd25567fb7c2f87be41220dd14025ca68b0e087'
+ ,'a91462b90344947b610c4eadb7dd460fee3f32fefe7687'
+ ,'a914d4388c7d5771ebf26b6e650c42e60e4cf7d4c5a187'
+ ,'a914e4f0832e56591d01b71c72b9a3777dc8f9d9a92e87'
+ ,'a914a5d5accd96d27403c7663b92fdb57299d7a871eb87'
+ ,'a914f8f2c6ef2d80f972e4d8b418a15337a3c38af37f87'
+ ,'a914a2bd2f67fac7c24e609b574ccc8cfaa2f90ebf8c87'
+ ,'a914a56298a7decde1d18306f55d9305577c3fce690187'
+ ,'a91430f2f83238ac29125a539055fa59efc86a73a23987'
+ ,'a914263b4585d0735c5065987922af359d5eabeb880d87'
+ ,'a91455d9d47113fb8b37705bdf6d4107d438afd63e4687'
+ ,'a914970d754163b8957b73f4e8baaf23dea5f6e3db2287'
+ ,'a914facbc921203a9ffd751cc246a884918beaac21b687'
+ ,'a914fc7556833eca1e0f84c6d7acb875e645f7ed4e9687'
+ ,'a914bbfe6a032d633f113b5d605e3a97cc08a47cc87d87'
+ ,'a91403d733c4ca337b5fa1de95970ba6f898a9d36c4887'
+ ,'a9148af27dc7c950e17c11e164065e672cd60ae3d48d87'
+ ,'a914c026aa45377f2a4a62136bac1d3350c318fee5c587'
+ ,'a9146337f59e3ea55e73725c9f2fc52a5ca5d68c361687'],
+ ['a914aeaebf9d567ab8a6813e89668e16f40bf419408e87' #change
+ ,'a914f2a6264dd3975297fa2a5a8e17321299a44f76d987'
+ ,'a9142067a6c47958090a645137cc0898c0c7bbc69b5387'
+ ,'a914210840f77ea5b7eb11cb55e5d719a93b7746fb9387'
+ ,'a914163db6b8ca00362be63a26502c5f7bf64787506b87'
+ ,'a91479b2c527594059c056e5367965ae92bbcf63512187'])
+ ,#p2sh 2of3 multisig wallet
+ ("2 tpubD6NzVbkrYhZ4WwaMJ3od4hANxdMVpb63Du3ERq1xjtowxVJEcTbGH2rFd9TFXxw" +
+ "KJRKDn9vQjDPxFeaku6BHW6wHn2KPF1ijS4LwgwQFJ3B tpubD6NzVbkrYhZ4Wjv4ZRPD6" +
+ "MNdiLmfvXztbKuuatkqHjukU3S6GXhmKnbAF5eU9bR2Nryiq8v67emUUSM1VUrAx5wcZ19" +
+ "AsaGg3ZLmjbbwLXr tpubD6NzVbkrYhZ4Xxa2fEp7YsbnFnwuQNaogijbiX42Deqd4NiAD" +
+ "tqNU6AXCU2d2kPFWBpAGG7K3HAKYwUfZBPgTLkfQp2dDg9SLVnkgYPgEXN",
+ ['a914167c95beb25b984ace517d4346e6cdbf1381793687', #recv addrs
+ 'a914378bbda1ba7a713de18c3ba3c366f42212bfb45087',
+ 'a9142a5c9881c70906180f37dd02d8c830e9b6328d4a87',
+ 'a914ffe0832375b72ee5307bfa502896ba28cc470ee987',
+ 'a9147607d40e039fbea57d9c04e48b198c9fcf3356c187',
+ 'a9148d9582ad4cf0581c6e0697e4cba6a12e66ca1a0087',
+ 'a914d153a743b315ba19690823119019e16e3762104d87',
+ 'a914b4accc89e48610043e70371153fd8cb5a3eef34287',
+ 'a91406febca615e3631253fd75a1d819436e1d046e0487',
+ 'a914b863cbb888c6b28291cb87a2390539e28be37a9587',
+ 'a914ec39094e393184d2c352a29b9d7a3caddaccb6cf87',
+ 'a914da4faa4babbdf611caf511d287133f06c1c3244a87',
+ 'a9146e64561d0c5e2e9159ecff65db02e04b3277402487',
+ 'a914377d66386972492192ae827fb2208596af0941d187',
+ 'a914448d364ff2374449e57df13db33a40f5b099997c87',
+ 'a914f24b875d2cb99e0b138ab0e6dd65027932b3c6e787',
+ 'a914aa4bcee53406b1ef6c83852e3844e38a3a9d9f3087',
+ 'a9145e5ec40fdab54be0d6e21107bc38c39df97e37fc87',
+ 'a9141de4d402c82f4e9b0e6b792b331232a5405ebd3f87',
+ 'a9148873ee280e51f9c64d257dd6dedc8712fd652cc687'],
+ ['a9142cc87d7562a85029a57cc37026e12dab72223db287', #change
+ 'a91499f4aee0b274f0b3ab48549a2c58cd667a62c0cb87',
+ 'a91497a89cd5ada3a766a1275f8151e9256fcf537f6c87',
+ 'a9147ffc9f3a3b60635ea1783243274f4d07ab617cb487',
+ 'a9143423113ab913d86fd47e55488a0c559e18b457b987',
+ 'a914a28a3773a37c52ff6fd7dff497d0eaf80a46febb87'])
+ , #p2wsh 1of2 multisig wallet
+ ("1 Vpub5fAqpSRkLmvXwqbuR61MaKMSwj5z5xUBwanaz3qnJ5MgaBDpFSLUvKTiNK9zHp" +
+ "dvrg2LHHXkKxSXBHNWNpZz9b1VqADjmcCs3arSoxN3F3r Vpub5fvEo4MUpbVs9sZqr45" +
+ "zmRVEsTcQ49MA9m3MLht3XzdZvS9eMXLLu1H6TL1j2SMnykHqXNzG5ycMyQmFDvEE5B32" +
+ "sP8TmRe6wW8HjBgMssh",
+ #recv scriptpubkeys
+ ['002031fbaa839e96fc1abaf3453b9f770e0ccfe2d8e3e990bb381fdcb7db4722986a',
+ '0020820ae739b36f4feb1c299ced201db383bbcf1634e0071e489b385f43c2323761',
+ '0020eff05f4d14aa1968a7142b1009aa57a6208fb01b212f8b8f7df63645d26a1292',
+ '002049c6e17979dca380ffb66295d27f609bea2879d4f0b590c96c70ff12260a8721',
+ '002002bf2430fc7ebc6fb27da1cb80e52702edcc62a29f65c997e5c924dcd98411bd',
+ '0020c7a58dcf9633453ba12860b57c14af67d87d022be5c52bf6be7a6abdc295c6e0',
+ '0020136696059a5e932c72f4f0a05fa7f52faf9b54f1b7694e15acce710e6cc9e89d',
+ '0020c372e880227f35c2ee35d0724bf05cea95e74dcb3e6aa67ff15f561a29c0645d',
+ '002095c705590e2b84996fa44bff64179b26669e53bbd58d76bb6bbb5c5498a981ce',
+ '00207217754dae083c3c365c7e1ce3ad889ca2bd88e4f809cec66b9987adc390aa26',
+ '0020bee30906450e099357cc96a1f472c1ef70089cd4a0cba96749adfe1c9a2f9e87',
+ '0020b1838b3d5a386ad6c90eeae9a27a9b812e32ce06376f261dea89e405bc8209d9',
+ '0020231a3d05886efff601f0702d4c8450dfcce8d6a4bd90f17f7ff76f5c25c632de',
+ '002071220f3941b5f65aca90e464db4291cd5ea63f37fa858fd5b66d5019f0dbab0f',
+ '0020fc3c7db9f0e773f9f9c725d4286ddcc88db9575c45b2441d458018150eb4ef10',
+ '00209f037bfc98dee2fc0d3cca54df09b2d20e92a0133fa381a4dd74c49e4d0a89f5',
+ '0020c9060d0554ba2ca92048e1772e806d796ba41f10bf6aee2653a9eba96b05c944',
+ '0020a7cb1dd2730dba564f414ed8d9312370ff89c34df1441b83125cb4d97a96005a',
+ '00209fddc9b4e070b887dec034ed74f15f62d075a3ac8cf6eb95a88c635e0207534c',
+ '0020c48f9c50958ab8e386a8bd3888076f31d12e5cf011ff46cc83c6fadfe6d47d20',
+ '0020a659f4621dca404571917e73dedb26b6d7c49a07dacbf15890760ac0583d3267'],
+ #change scriptpubkeys
+ ['002030213b5d3b6988b86aa13a9eaca08e718d51f32dc130c70981abb0102173c791',
+ '002027bd198f9783a58e9bc4d3fdbd1c75cc74154905cce1d23c7bd3e051695418fe',
+ '0020c1fd2cdebf120d3b1dc990dfdaca62382ff9525beeb6a79a908ddecb40e2162c',
+ '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 = [
+ "zpub661MyMwAqRbcGVQTLtBFzc3ENvyZHoUEhWRdGwoqLZaf5wXP9VcDY2VJV7usvsFLZz" +
+ "2RUTVhCVXYXc3S8zpLyAFbDFcfrpUiwLoE9VWH2yz", #bad checksum
+ "a tpubD6NzVbkrYhZ4YVMVzC7wZeRfz3bhqcHvV8M3UiULCfzFtLtp5nwvi6LnBQegrkx" +
+ "YGPkSzXUEvcPEHcKdda8W1YShVBkhFBGkLxjSQ1Nx3cJ tpubD6NzVbkrYhZ4WjgNYq2nF" +
+ "TbiSLW2SZAzs4g5JHLqwQ3AmR3tCWpqsZJJEoZuP5HAEBNxgYQhtWMezszoaeTCg6FWGQB" +
+ "T74sszGaxaf64o5s", #unparsable m number
+ "2 tpubD6NzVbkrYhZ4YVMVzC7wZeRfz3bhqcHvV8M3UiULCfzFtLtp5nwvi6LnBQegrkx" +
+ "YGPkSzXUEvcPEHcKdda8W1YShVBkhFBGkLxjSQ1Nx3cJ Vpub5fAqpSRkLmvXwqbuR61M" +
+ "aKMSwj5z5xUBwanaz3qnJ5MgaBDpFSLUvKTiNK9zHpdvrg2LHHXkKxSXBHNWNpZz9b1Vq" +
+ "ADjmcCs3arSoxN3F3r" #inconsistent magic
+]
+
+def test():
+ for keydata, recv_spks, change_spks in electrum_keydata_test_vectors:
+ initial_count = 15
+ gaplimit = 5
+ wal = parse_electrum_master_public_key(keydata, gaplimit)
+ spks = wal.get_scriptpubkeys(0, 0, initial_count)
+ #for test, generate 15, check that the last 5 lead to gap limit overrun
+ for i in range(initial_count - gaplimit):
+ ret = wal.have_scriptpubkeys_overrun_gaplimit([spks[i]])
+ assert ret == None
+ for i in range(gaplimit):
+ index = i + initial_count - gaplimit
+ ret = wal.have_scriptpubkeys_overrun_gaplimit([spks[index]])
+ assert ret != None and ret[0] == i+1
+ last_index_add = 3
+ last_index = initial_count - gaplimit + last_index_add
+ ret = wal.have_scriptpubkeys_overrun_gaplimit(spks[2:last_index])
+ assert ret[0] == last_index_add
+ assert wal.get_scriptpubkeys(0, 0, len(recv_spks)) == recv_spks
+ assert wal.get_scriptpubkeys(1, 0, len(change_spks)) == change_spks
+ for keydata in electrum_bad_keydata_test_vectors:
+ try:
+ parse_electrum_master_public_key(keydata, 5)
+ raised_error = False
+ except (ValueError, Exception):
+ raised_error = True
+ assert raised_error
+ print("All tests passed successfully")
+
+if __name__ == "__main__":
+ test()
+ pass
+
diff --git a/hashes.py b/electrumpersonalserver/hashes.py
diff --git a/jsonrpc.py b/electrumpersonalserver/jsonrpc.py
diff --git a/electrumpersonalserver/merkleproof.py b/electrumpersonalserver/merkleproof.py
@@ -0,0 +1,271 @@
+
+import bitcoin as btc
+import binascii
+from math import ceil, log
+
+from electrumpersonalserver.hashes import hash_encode, hash_decode
+from electrumpersonalserver.hashes import Hash, hash_merkle_root
+
+#lots of ideas and code taken from bitcoin core and breadwallet
+#https://github.com/bitcoin/bitcoin/blob/master/src/merkleblock.h
+#https://github.com/breadwallet/breadwallet-core/blob/master/BRMerkleBlock.c
+
+def calc_tree_width(height, txcount):
+ """Efficently calculates the number of nodes at given merkle tree height"""
+ return (txcount + (1 << height) - 1) >> height
+
+def decend_merkle_tree(hashes, flags, height, txcount, pos):
+ """Function recursively follows the flags bitstring down into the
+ tree, building up a tree in memory"""
+ flag = next(flags)
+ if height > 0:
+ #non-txid node
+ if flag:
+ left = decend_merkle_tree(hashes, flags, height-1, txcount, pos*2)
+ #bitcoin's merkle tree format has a rule that if theres an
+ # odd number of nodes in then the tree, the last hash is duplicated
+ #in the electrum format we must hash together the duplicate
+ # tree branch
+ if pos*2+1 < calc_tree_width(height-1, txcount):
+ right = decend_merkle_tree(hashes, flags, height-1,
+ txcount, pos*2+1)
+ else:
+ if isinstance(left, tuple):
+ right = expand_tree_hashing(left)
+ else:
+ right = left
+ return (left, right)
+ else:
+ hs = next(hashes)
+ return hs
+ else:
+ #txid node
+ hs = next(hashes)
+ if flag:
+ #for the actual transaction, also store its position with a flag
+ return "tx:" + str(pos) + ":" + hs
+ else:
+ return hs
+
+def deserialize_core_format_merkle_proof(hash_list, flag_value, txcount):
+ """Converts core's format for a merkle proof into a tree in memory"""
+ tree_depth = int(ceil(log(txcount, 2)))
+ hashes = iter(hash_list)
+ #one-liner which converts the flags value to a list of True/False bits
+ flags = (flag_value[i//8]&1 << i%8 != 0 for i in range(len(flag_value)*8))
+ try:
+ root_node = decend_merkle_tree(hashes, flags, tree_depth, txcount, 0)
+ return root_node
+ except StopIteration:
+ raise ValueError
+
+def expand_tree_electrum_format_merkle_proof(node, result):
+ """Recurse down into the tree, adding hashes to the result list
+ in depth order"""
+ left, right = node
+ if isinstance(left, tuple):
+ expand_tree_electrum_format_merkle_proof(left, result)
+ if isinstance(right, tuple):
+ expand_tree_electrum_format_merkle_proof(right, result)
+ if not isinstance(left, tuple):
+ result.append(left)
+ if not isinstance(right, tuple):
+ result.append(right)
+
+def get_node_hash(node):
+ if node.startswith("tx"):
+ return node.split(":")[2]
+ else:
+ return node
+
+def expand_tree_hashing(node):
+ """Recurse down into the tree, hashing everything and
+ returning root hash"""
+ left, right = node
+ if isinstance(left, tuple):
+ hash_left = expand_tree_hashing(left)
+ else:
+ hash_left = get_node_hash(left)
+ if isinstance(right, tuple):
+ hash_right = expand_tree_hashing(right)
+ else:
+ hash_right = get_node_hash(right)
+ return hash_encode(Hash(hash_decode(hash_left) + hash_decode(hash_right)))
+
+def convert_core_to_electrum_merkle_proof(proof):
+ """Bitcoin Core and Electrum use different formats for merkle
+ proof, this function converts from Core's format to Electrum's format"""
+ proof = binascii.unhexlify(proof)
+ pos = [0]
+ def read_as_int(bytez):
+ pos[0] += bytez
+ return btc.decode(proof[pos[0] - bytez:pos[0]][::-1], 256)
+ def read_var_int():
+ pos[0] += 1
+ val = btc.from_byte_to_int(proof[pos[0] - 1])
+ if val < 253:
+ return val
+ return read_as_int(pow(2, val - 252))
+ def read_bytes(bytez):
+ pos[0] += bytez
+ return proof[pos[0] - bytez:pos[0]]
+
+ merkle_root = proof[36:36+32]
+ pos[0] = 80
+ txcount = read_as_int(4)
+ hash_count = read_var_int()
+ hashes = [hash_encode(read_bytes(32)) for i in range(hash_count)]
+ flags_count = read_var_int()
+ flags = read_bytes(flags_count)
+
+ root_node = deserialize_core_format_merkle_proof(hashes, flags, txcount)
+ #check special case of a tree of zero height, block with only coinbase tx
+ if not isinstance(root_node, tuple):
+ root_node = root_node[5:] #remove the "tx:0:"
+ result = {"pos": 0, "merkle": [], "txid": root_node,
+ "merkleroot": hash_encode(merkle_root)}
+ return result
+
+ hashes_list = []
+ expand_tree_electrum_format_merkle_proof(root_node, hashes_list)
+ #remove the first or second element which is the txhash
+ tx = hashes_list[0]
+ if hashes_list[1].startswith("tx"):
+ tx = hashes_list[1]
+ assert(tx.startswith("tx"))
+ hashes_list.remove(tx)
+ #if the txhash was duplicated, that _is_ included in electrum's format
+ if hashes_list[0].startswith("tx"):
+ hashes_list[0] = tx.split(":")[2]
+ tx_pos, txid = tx.split(":")[1:3]
+ tx_pos = int(tx_pos)
+ result = {"pos": tx_pos, "merkle": hashes_list, "txid": txid,
+ "merkleroot": hash_encode(merkle_root)}
+ return result
+
+merkle_proof_test_vectors = [
+ #txcount 819, pos 5
+ "0300000026e696fba00f0a43907239305eed9e55824e0e376636380f000000000000000" +
+ "04f8a2ce51d6c69988029837688cbfc2f580799fa1747456b9c80ab808c1431acd0b07f" +
+ "5543201618cadcfbf7330300000b0ff1e0050fed22ca360e0935e053b0fe098f6f9e090" +
+ "f5631013361620d964fe2fd88544ae10b40621e1cd24bb4306e3815dc237f77118a45d7" +
+ "5ada9ee362314b70573732bce59615a3bcc1bbacd04b33b7819198212216b5d62d75be5" +
+ "9221ada17ba4fb2476b689cccd3be54732fd5630832a94f11fa3f0dafd6f904d43219e0" +
+ "d7de110158446b5b598bd241f7b5df4da0ebc7d30e7748d487917b718df51c681174e6a" +
+ "bab8042cc7c1c436221c098f06a56134f9247a812126d675d69c82ba1c715cfc0cde462" +
+ "fd1fbe5dc87f6b8db2b9c060fcd59a20e7fe8e921c3676937a873ff88684f4be4d015f2" +
+ "4f26af6d2cf78335e9218bcceba4507d0b4ba6cb933aa01ef77ae5eb411893ec0f74b69" +
+ "590fb0f5118ac937c02ccd47e9d90be78becd11ecf854d7d268eeb479b74d137278c0a5" +
+ "017d29e90cd5b35a4680201824fb0eb4f404e20dfeaec4d50549030b7e7e220b02eb210" +
+ "5f3d2e8bcc94d547214a9d03ff1600",
+ #txcount 47, pos 9
+ "0100000053696a625fbd16df418575bce0c4148886c422774fca5fcab80100000000000" +
+ "01532bfe4f9c4f56cd141028e5b59384c133740174b74b1982c7f01020b90ce05577c67" +
+ "508bdb051a7ec2ef942f000000076cde2eb7efa90b36d48aed612e559ff2ba638d8d400" +
+ "b14b0c58df00c6a6c33b65dc8fa02f4ca56e1f4dcf17186fa9bbd990ce150b6e2dc9e9e" +
+ "56bb4f270fe56fde6bdd73a7a7e82767714862888e6b759568fb117674ad23050e29311" +
+ "97494d457efb72efdb9cb79cd4a435724908a0eb31ec7f7a67ee03837319e098b43edad" +
+ "3be9af75ae7b30db6f4f93ba0fdd941fdf70fe8cc38982e03bd292f5bd02f28137d343f" +
+ "908c7d6417379afe8349a257af3ca1f74f623be6a416fe1aa96a8f259983f2cf32121bc" +
+ "e203955a378b3b44f132ea6ab94c7829a6c3b360c9f8da8e74027701",
+ #txcount 2582, pos 330
+ "000000206365d5e1d8b7fdf0b846cfa902115c1b8ced9dd49cb17800000000000000000" +
+ "01032e829e1f9a5a09d0492f9cd3ec0762b7facea555989c3927d3d975fd4078c771849" +
+ "5a45960018edd3b9e0160a00000dfe856a7d5d77c23ebf85c68b5eb303d85e56491ed6d" +
+ "204372625d0b4383df5a44d6e46d2db09d936b9f5d0b53e0dbcb3efb7773d457369c228" +
+ "fd1ce6e11645e366a58b3fc1e8a7c916710ce29a87265a6729a3b221b47ea9c8e6f4870" +
+ "7b112b8d67e5cfb3db5f88b042dc49e4e5bc2e61c28e1e0fbcba4c741bb5c75cac58ca0" +
+ "4161a7377d70f3fd19a3e248ed918c91709b49afd3760f89ed2fefbcc9c23447ccb40a2" +
+ "be7aba22b07189b0bf90c62db48a9fe37227e12c7af8c1d4c22f9f223530dacdd5f3ad8" +
+ "50ad4badf16cc24049a65334f59bf28c15cecda1a4cf3f2937bd70ee84f66569ce8ef95" +
+ "1d50cca46d60337e6c697685b38ad217967bbe6801d03c44fcb808cd035be31888380a2" +
+ "df1be14b6ff100de83cab0dce250e2b40ca3b47e8309f848646bee63b6185c176d84f15" +
+ "46a482e7a65a87d1a2d0d5a2b683e2cae0520df1e3525a71d71e1f551abd7d238c3bcb4" +
+ "ecaeea7d5988745fa421a8604a99857426957a2ccfa7cd8df145aa8293701989dd20750" +
+ "5923fcb339843944ce3d21dc259bcda9c251ed90d4e55af2cf5b15432050084f513ac74" +
+ "c0bdd4b6046fb70100",
+ #txcount 2861, pos 2860, last tx with many duplicated nodes down the tree
+ "00000020c656c90b521a2bbca14174f2939b882a28d23d86144b0e00000000000000000" +
+ "0cf5185a8e369c3de5f15e039e777760994fd66184b619d839dace3aec9953fd6d86159" +
+ "5ac1910018ee097a972d0b0000078d20d71c3114dbf52bb13f2c18f987891e8854d2d29" +
+ "f61c0b3d3256afcef7c0b1e6f76d6431f93390ebd28dbb81ad7c8f08459e85efeb23cc7" +
+ "2df2c5612215bf53dd4ab3703886bc8c82cb78ba761855e495fb5dc371cd8fe25ae974b" +
+ "df42269e267caf898a9f34cbf2350eaaa4afbeaea70636b5a3b73682186817db5b33290" +
+ "bd5c696bd8d0322671ff70c5447fcd7bdc127e5b84350c6b14b5a3b86b424d7db38d39f" +
+ "171f57e255a31c6c53415e3d65408b6519a40aacc49cad8e70646d4cb0d23d4a63068e6" +
+ "c220efc8a2781e9e774efdd334108d7453043bd3c8070d0e5903ad5b07",
+ #txcount 7, pos 6, duplicated entry in the last depth, at tx level
+ "0100000056e02c6d3278c754e0699517834741f7c4ad3dcbfeb7803a346200000000000" +
+ "0af3bdd5dd465443fd003e9281455e60aae573dd4d46304d7ba17276ea33d506488cbb4" +
+ "4dacb5001b9ebb193b0700000003cd3abb2eb7583165f36b56add7268be9027ead4cc8f" +
+ "888ec650d3b1c1f4de28a0ff7c8b463b2042d09598f0e5e5905de362aa1cf75252adc22" +
+ "719b8e1bc969adcfbc4782b8eafc9352263770b91a0f189ae051cbe0e26046c2b14cf3d" +
+ "8be0bc40135",
+ #txcount 6, pos 5, duplicated entry in the last-but-one depth
+ "01000000299edfd28524eae4fb6012e4087afdb6e1b912db85e612374b0300000000000" +
+ "0e16572394f8578a47bf36e15cd16faa5e3b9e18805cf4e271ae4ef95aa8cea7eb31fa1" +
+ "4e4b6d0b1a42857d960600000003f52b45ed953924366dab3e92707145c78615b639751" +
+ "ecb7be1c5ecc09b592ed588ca0e15a89e9049a2dbcadf4d8362bd1f74a6972f176617b5" +
+ "8a5466c8a4121fc3e2d6fa66c8637b387ef190ab46d6e9c9dae4bbccd871c72372b3dbc" +
+ "6edefea012d",
+ #txcount 5, pos 4, duplicated on the last and second last depth
+ "010000004d891de57908d89e9e5585648978d7650adc67e856c2d8c18c1800000000000" +
+ "04746fd317bffecd4ffb320239caa06685bafe0c1b5463b24d636e45788796657843d1b" +
+ "4d4c86041be68355c40500000002d8e26c89c46477f2407d866d2badbd98e43e732a670" +
+ "e96001faf1744b27e5fdd018733d72e31a2d6a0d94f2a3b35fcc66fb110c40c5bbff82b" +
+ "f87606553d541d011d",
+ #txcount 2739, pos 0, coinbase tx
+ "000000209f283da030c6e6d0ff5087a87c430d140ed6b4564fa34d00000000000000000" +
+ "0ec1513723e3652c6b8e777c41eb267ad8dd2025e85228840f5cfca7ffe1fb331afff8a" +
+ "5af8e961175e0f7691b30a00000df403e21a4751fbd52457f535378ac2dcf111199e9ea" +
+ "6f78f6c2663cb99b58203438d8f3b26f7f2804668c1df7d394a4726363d4873b2d85b71" +
+ "2e44cf4f5e4f33f22a8f3a1672846bd7c4570c668e6ee12befda23bfa3d0fcd30b1b079" +
+ "19b01c40b1e31b6d34fcdbb99539d46eb97a3ae15386f1ab0f28ecacadd9fc3fa4ce49a" +
+ "1a1839d815229f54036c8a3035d91e80e8dc127b62032b4e652550b4fc0aee0f6e85a14" +
+ "307d85ed9dde62acff9a0f7e3b52370a10d6c83ec13a0b4a8fafe87af368a167d7e9b63" +
+ "3b84b6ea65f1ce5e8ccc1840be0a4dab0099e25afccc7f2fdbda54cd65ecbac8d9a550c" +
+ "108b4e18d3af59129d373fde4c80848858fd6f7fc1e27387a38833473ca8a47729fa6e1" +
+ "cc14b584c14dad768108ff18cc6acdc9c31d32dc71c3c80856664a3fff870fe419a59aa" +
+ "9033356590475d36086f0b3c0ece34c0f3756675c610fb980ff3363af6f9c0918a7c677" +
+ "23371849de9c1026515c2900a80b3aee4f2625c8f48cd5eb967560ee8ebe58a8d41c331" +
+ "f6d5199795735d4f0494bdf592d166fa291062733619f0f133605087365639de2d9d5d6" +
+ "921f4b4204ff1f0000",
+ #txcount 1, pos 0, coinbase tx in an empty block, tree with height 1
+ "010000000508085c47cc849eb80ea905cc7800a3be674ffc57263cf210c59d8d0000000" +
+ "0112ba175a1e04b14ba9e7ea5f76ab640affeef5ec98173ac9799a852fa39add320cd66" +
+ "49ffff001d1e2de5650100000001112ba175a1e04b14ba9e7ea5f76ab640affeef5ec98" +
+ "173ac9799a852fa39add30101",
+ #txcount 2, pos 1, tree with height 2
+ "010000004e24a2880cd72d9bde7502087bd3756819794dc7548f68dd68dc30010000000" +
+ "02793fce9cdf91b4f84760571bf6009d5f0ffaddbfdc9234ef58a036096092117b10f4b" +
+ "4cfd68011c903e350b0200000002ee50562fc6f995eff2df61be0d5f943bac941149aa2" +
+ "1aacb32adc130c0f17d6a2077a642b1eabbc5120e31566a11e2689aa4d39b01cce9a190" +
+ "2360baa5e4328e0105"
+]
+
+def test():
+ for proof in merkle_proof_test_vectors:
+ try:
+ electrum_proof = convert_core_to_electrum_merkle_proof(proof)
+ #print(electrum_proof)
+ implied_merkle_root = hash_merkle_root(
+ electrum_proof["merkle"], electrum_proof["txid"],
+ electrum_proof["pos"])
+ assert implied_merkle_root == electrum_proof["merkleroot"]
+ except ValueError:
+ import traceback
+ traceback.print_exc()
+ assert 0
+ print("All tests passed")
+
+'''
+proof = ""
+def chunks(d, n):
+ return [d[x:x + n] for x in range(0, len(d), n)]
+#print(proof)
+print("\" + \n\"".join(chunks(proof, 71)))
+'''
+
+if __name__ == "__main__":
+ test()
+
diff --git a/electrumpersonalserver/transactionmonitor.py b/electrumpersonalserver/transactionmonitor.py
@@ -0,0 +1,723 @@
+
+import time, pprint, math, sys
+from decimal import Decimal
+
+from electrumpersonalserver.jsonrpc import JsonRpcError
+import electrumpersonalserver.hashes as hashes
+
+#internally this code uses scriptPubKeys, it only converts to bitcoin addresses
+# when importing to bitcoind or checking whether enough addresses have been
+# imported
+#the electrum protocol uses sha256(scriptpubkey) as a key for lookups
+# this code calls them scripthashes
+
+#code will generate the first address from each deterministic wallet
+# and check whether they have been imported into the bitcoin node
+# if no then initial_import_count addresses will be imported, then exit
+# if yes then initial_import_count addresses will be generated and extra
+# addresses will be generated one-by-one, each time checking whether they have
+# been imported into the bitcoin node
+# when an address has been reached that has not been imported, that means
+# we've reached the end, then rewind the deterministic wallet index by one
+
+#when a transaction happens paying to an address from a deterministic wallet
+# lookup the position of that address, if its less than gap_limit then
+# import more addresses
+
+ADDRESSES_LABEL = "electrum-watchonly-addresses"
+
+def import_addresses(rpc, addrs, debug, log):
+ debug("importing addrs = " + str(addrs))
+ log("Importing " + str(len(addrs)) + " addresses in total")
+ addr_i = iter(addrs)
+ notifications = 10
+ for i in range(notifications):
+ pc = int(100.0 * i / notifications)
+ sys.stdout.write("[" + str(pc) + "%]... ")
+ sys.stdout.flush()
+ for j in range(int(len(addrs) / notifications)):
+ rpc.call("importaddress", [next(addr_i), ADDRESSES_LABEL, False])
+ for a in addr_i: #import the reminder of addresses
+ rpc.call("importaddress", [a, ADDRESSES_LABEL, False])
+ print("[100%]")
+ log("Importing done")
+
+class TransactionMonitor(object):
+ """
+ Class which monitors the bitcoind wallet for new transactions
+ and builds a history datastructure for sending to electrum
+ """
+ def __init__(self, rpc, deterministic_wallets, debug, log):
+ self.rpc = rpc
+ self.deterministic_wallets = deterministic_wallets
+ self.debug = debug
+ self.log = log
+ self.last_known_wallet_txid = None
+ self.address_history = None
+ self.unconfirmed_txes = None
+
+ def get_electrum_history_hash(self, scrhash):
+ return hashes.get_status_electrum( ((h["tx_hash"], h["height"])
+ for h in self.address_history[scrhash]["history"]) )
+
+ def get_electrum_history(self, scrhash):
+ if scrhash in self.address_history:
+ return self.address_history[scrhash]["history"]
+ else:
+ return None
+
+ def subscribe_address(self, scrhash):
+ if scrhash in self.address_history:
+ self.address_history[scrhash]["subscribed"] = True
+ return True
+ else:
+ return False
+
+ def unsubscribe_all_addresses(self):
+ for scrhash, his in self.address_history.items():
+ his["subscribed"] = False
+
+ def build_address_history(self, monitored_scriptpubkeys):
+ self.log("Building history with " + str(len(monitored_scriptpubkeys)) +
+ " addresses")
+ st = time.time()
+ address_history = {}
+ for spk in monitored_scriptpubkeys:
+ address_history[hashes.script_to_scripthash(spk)] = {'history': [],
+ 'subscribed': False}
+ wallet_addr_scripthashes = set(address_history.keys())
+ #populate history
+ #which is a blockheight-ordered list of ("txhash", height)
+ #unconfirmed transactions go at the end as ("txhash", 0, fee)
+ # 0=unconfirmed -1=unconfirmed with unconfirmed parents
+
+ BATCH_SIZE = 1000
+ ret = list(range(BATCH_SIZE))
+ t = 0
+ count = 0
+ obtained_txids = set()
+ while len(ret) == BATCH_SIZE:
+ ret = self.rpc.call("listtransactions", ["*", BATCH_SIZE, t, True])
+ self.debug("listtransactions skip=" + str(t) + " len(ret)="
+ + str(len(ret)))
+ t += len(ret)
+ for tx in ret:
+ if "txid" not in tx or "category" not in tx:
+ continue
+ if tx["category"] not in ("receive", "send"):
+ continue
+ if tx["txid"] in obtained_txids:
+ continue
+ self.debug("adding obtained tx=" + str(tx["txid"]))
+ obtained_txids.add(tx["txid"])
+
+ #obtain all the addresses this transaction is involved with
+ output_scriptpubkeys, input_scriptpubkeys, txd = \
+ self.get_input_and_output_scriptpubkeys(tx["txid"])
+ output_scripthashes = [hashes.script_to_scripthash(sc)
+ for sc in output_scriptpubkeys]
+ sh_to_add = wallet_addr_scripthashes.intersection(set(
+ output_scripthashes))
+ input_scripthashes = [hashes.script_to_scripthash(sc)
+ for sc in input_scriptpubkeys]
+ sh_to_add |= wallet_addr_scripthashes.intersection(set(
+ input_scripthashes))
+ if len(sh_to_add) == 0:
+ continue
+
+ for wal in self.deterministic_wallets:
+ overrun_depths = wal.have_scriptpubkeys_overrun_gaplimit(
+ output_scriptpubkeys)
+ if overrun_depths != None:
+ self.log("ERROR: Not enough addresses imported.")
+ self.log("Delete wallet.dat and increase the value " +
+ "of `initial_import_count` in the file " +
+ "`config.cfg` then reimport and rescan")
+ #TODO make it so users dont have to delete wallet.dat
+ # check whether all initial_import_count addresses are
+ # imported rather than just the first one
+ return False
+ new_history_element = self.generate_new_history_element(tx, txd)
+ for scripthash in sh_to_add:
+ address_history[scripthash][
+ "history"].append(new_history_element)
+ count += 1
+
+ unconfirmed_txes = {}
+ for scrhash, his in address_history.items():
+ uctx = self.sort_address_history_list(his)
+ for u in uctx:
+ if u["tx_hash"] in unconfirmed_txes:
+ unconfirmed_txes[u["tx_hash"]].append(scrhash)
+ else:
+ unconfirmed_txes[u["tx_hash"]] = [scrhash]
+ self.debug("unconfirmed_txes = " + str(unconfirmed_txes))
+ if len(ret) > 0:
+ #txid doesnt uniquely identify transactions from listtransactions
+ #but the tuple (txid, address) does
+ self.last_known_wallet_txid = (ret[-1]["txid"], ret[-1]["address"])
+ else:
+ self.last_known_wallet_txid = None
+ self.debug("last_known_wallet_txid = " + str(
+ self.last_known_wallet_txid))
+
+ et = time.time()
+ self.debug("address_history =\n" + pprint.pformat(address_history))
+ self.log("Found " + str(count) + " txes. History built in " +
+ str(et - st) + "sec")
+ self.address_history = address_history
+ self.unconfirmed_txes = unconfirmed_txes
+ return True
+
+ def get_input_and_output_scriptpubkeys(self, txid):
+ gettx = self.rpc.call("gettransaction", [txid])
+ txd = self.rpc.call("decoderawtransaction", [gettx["hex"]])
+ output_scriptpubkeys = [out["scriptPubKey"]["hex"]
+ for out in txd["vout"]]
+ input_scriptpubkeys = []
+ for inn in txd["vin"]:
+ try:
+ wallet_tx = self.rpc.call("gettransaction", [inn["txid"]])
+ except JsonRpcError:
+ #wallet doesnt know about this tx, so the input isnt ours
+ continue
+ input_decoded = self.rpc.call("decoderawtransaction", [wallet_tx[
+ "hex"]])
+ script = input_decoded["vout"][inn["vout"]]["scriptPubKey"]["hex"]
+ input_scriptpubkeys.append(script)
+ return output_scriptpubkeys, input_scriptpubkeys, txd
+
+ def generate_new_history_element(self, tx, txd):
+ if tx["confirmations"] == 0:
+ unconfirmed_input = False
+ total_input_value = 0
+ for inn in txd["vin"]:
+ utxo = self.rpc.call("gettxout", [inn["txid"], inn["vout"],
+ True])
+ if utxo is None:
+ utxo = self.rpc.call("gettxout", [inn["txid"], inn["vout"],
+ False])
+ if utxo is None:
+ self.debug("utxo not found(!)")
+ #TODO detect this and figure out how to tell
+ # electrum that we dont know the fee
+ total_input_value += int(Decimal(utxo["value"]) * Decimal(1e8))
+ unconfirmed_input = (unconfirmed_input or
+ utxo["confirmations"] == 0)
+ self.debug("total_input_value = " + str(total_input_value))
+
+ fee = total_input_value - sum([int(Decimal(out["value"])
+ * Decimal(1e8)) for out in txd["vout"]])
+ height = -1 if unconfirmed_input else 0
+ new_history_element = ({"tx_hash": tx["txid"], "height": height,
+ "fee": fee})
+ else:
+ blockheader = self.rpc.call("getblockheader", [tx['blockhash']])
+ new_history_element = ({"tx_hash": tx["txid"],
+ "height": blockheader["height"]})
+ return new_history_element
+
+ def sort_address_history_list(self, his):
+ unconfirm_txes = list(filter(lambda h:h["height"] == 0, his["history"]))
+ confirm_txes = filter(lambda h:h["height"] != 0, his["history"])
+ #TODO txes must be "in blockchain order"
+ # the order they appear in the block
+ # it might be "blockindex" in listtransactions and gettransaction
+ #so must sort with key height+':'+blockindex
+ #maybe check if any heights are the same then get the pos only for those
+ #better way to do this is to have a separate dict that isnt in history
+ # which maps txid => blockindex
+ # and then sort by key height+":"+idx[txid]
+ his["history"] = sorted(confirm_txes, key=lambda h:h["height"])
+ his["history"].extend(unconfirm_txes)
+ return unconfirm_txes
+
+ def check_for_updated_txes(self):
+ updated_scrhashes1 = self.check_for_new_txes()
+ updated_scrhashes2 = self.check_for_confirmations()
+ updated_scrhashes = updated_scrhashes1 | updated_scrhashes2
+ for ush in updated_scrhashes:
+ his = self.address_history[ush]
+ self.sort_address_history_list(his)
+ if len(updated_scrhashes) > 0:
+ self.debug("new tx address_history =\n"
+ + pprint.pformat(self.address_history))
+ self.debug("unconfirmed txes = " +
+ pprint.pformat(self.unconfirmed_txes))
+ self.debug("updated_scripthashes = " + str(updated_scrhashes))
+ updated_scrhashes = filter(lambda sh:self.address_history[sh][
+ "subscribed"], updated_scrhashes)
+ return updated_scrhashes
+
+ def check_for_confirmations(self):
+ confirmed_txes_scrhashes = []
+ self.debug("check4con unconfirmed_txes = "
+ + pprint.pformat(self.unconfirmed_txes))
+ for uc_txid, scrhashes in self.unconfirmed_txes.items():
+ tx = self.rpc.call("gettransaction", [uc_txid])
+ self.debug("uc_txid=" + uc_txid + " => " + str(tx))
+ if tx["confirmations"] == 0:
+ continue #still unconfirmed
+ self.log("A transaction confirmed: " + uc_txid)
+ confirmed_txes_scrhashes.append((uc_txid, scrhashes))
+ block = self.rpc.call("getblockheader", [tx["blockhash"]])
+ for scrhash in scrhashes:
+ #delete the old unconfirmed entry in address_history
+ deleted_entries = [h for h in self.address_history[scrhash][
+ "history"] if h["tx_hash"] == uc_txid]
+ for d_his in deleted_entries:
+ self.address_history[scrhash]["history"].remove(d_his)
+ #create the new confirmed entry in address_history
+ self.address_history[scrhash]["history"].append({"height":
+ block["height"], "tx_hash": uc_txid})
+ updated_scrhashes = set()
+ for tx, scrhashes in confirmed_txes_scrhashes:
+ del self.unconfirmed_txes[tx]
+ updated_scrhashes.update(set(scrhashes))
+ return updated_scrhashes
+
+ def check_for_new_txes(self):
+ MAX_TX_REQUEST_COUNT = 256
+ tx_request_count = 2
+ max_attempts = int(math.log(MAX_TX_REQUEST_COUNT, 2))
+ for i in range(max_attempts):
+ self.debug("listtransactions tx_request_count="
+ + str(tx_request_count))
+ ret = self.rpc.call("listtransactions", ["*", tx_request_count, 0,
+ True])
+ ret = ret[::-1]
+ if self.last_known_wallet_txid == None:
+ recent_tx_index = len(ret) #=0 means no new txes
+ break
+ else:
+ txid_list = [(tx["txid"], tx["address"]) for tx in ret]
+ recent_tx_index = next((i for i, (txid, addr)
+ in enumerate(txid_list) if
+ txid == self.last_known_wallet_txid[0] and
+ addr == self.last_known_wallet_txid[1]), -1)
+ if recent_tx_index != -1:
+ break
+ tx_request_count *= 2
+
+ #TODO low priority: handle a user getting more than 255 new
+ # transactions in 15 seconds
+ self.debug("recent tx index = " + str(recent_tx_index) + " ret = " +
+ str([(t["txid"], t["address"]) for t in ret]))
+ if len(ret) > 0:
+ self.last_known_wallet_txid = (ret[0]["txid"], ret[0]["address"])
+ self.debug("last_known_wallet_txid = " + str(
+ self.last_known_wallet_txid))
+ assert(recent_tx_index != -1)
+ if recent_tx_index == 0:
+ return set()
+ new_txes = ret[:recent_tx_index][::-1]
+ self.debug("new txes = " + str(new_txes))
+ obtained_txids = set()
+ updated_scripthashes = []
+ for tx in new_txes:
+ if "txid" not in tx or "category" not in tx:
+ continue
+ if tx["category"] not in ("receive", "send"):
+ continue
+ if tx["txid"] in obtained_txids:
+ continue
+ obtained_txids.add(tx["txid"])
+ output_scriptpubkeys, input_scriptpubkeys, txd = \
+ self.get_input_and_output_scriptpubkeys(tx["txid"])
+ matching_scripthashes = []
+ for spk in (output_scriptpubkeys + input_scriptpubkeys):
+ scripthash = hashes.script_to_scripthash(spk)
+ if scripthash in self.address_history:
+ matching_scripthashes.append(scripthash)
+ if len(matching_scripthashes) == 0:
+ continue
+
+ for wal in self.deterministic_wallets:
+ overrun_depths = wal.have_scriptpubkeys_overrun_gaplimit(
+ output_scriptpubkeys)
+ if overrun_depths != None:
+ for change, import_count in overrun_depths.items():
+ spks = wal.get_new_scriptpubkeys(change, import_count)
+ for spk in spks:
+ self.address_history[hashes.script_to_scripthash(
+ spk)] = {'history': [], 'subscribed': False}
+ new_addrs = [hashes.script_to_address(s, self.rpc)
+ for s in spks]
+ self.debug("importing " + str(len(spks)) +
+ " into change=" + str(change))
+ import_addresses(self.rpc, new_addrs, self.debug,
+ self.log)
+
+ updated_scripthashes.extend(matching_scripthashes)
+ new_history_element = self.generate_new_history_element(tx, txd)
+ self.log("Found new tx: " + str(new_history_element))
+ for scrhash in matching_scripthashes:
+ self.address_history[scrhash]["history"].append(
+ new_history_element)
+ if new_history_element["height"] == 0:
+ if tx["txid"] in self.unconfirmed_txes:
+ self.unconfirmed_txes[tx["txid"]].append(scrhash)
+ else:
+ self.unconfirmed_txes[tx["txid"]] = [scrhash]
+ #check whether gap limits have been overrun and import more addrs
+ return set(updated_scripthashes)
+
+
+## start tests here
+
+class TestJsonRpc(object):
+ def __init__(self, txlist, utxoset, block_heights):
+ self.txlist = txlist
+ self.utxoset = utxoset
+ self.block_heights = block_heights
+ self.imported_addresses = []
+
+ def call(self, method, params):
+ if method == "listtransactions":
+ count = int(params[1])
+ skip = int(params[2])
+ return self.txlist[skip:skip + count]
+ elif method == "gettransaction":
+ for t in self.txlist:
+ if t["txid"] == params[0]:
+ return t
+ raise JsonRpcError({"code": None, "message": None})
+ elif method == "decoderawtransaction":
+ for t in self.txlist:
+ if t["hex"] == params[0]:
+ return t
+ assert 0
+ elif method == "gettxout":
+ for u in self.utxoset:
+ if u["txid"] == params[0] and u["vout"] == params[1]:
+ return u
+ assert 0
+ elif method == "getblockheader":
+ if params[0] not in self.block_heights:
+ assert 0
+ return {"height": self.block_heights[params[0]]}
+ elif method == "decodescript":
+ return {"addresses": [test_spk_to_address(params[0])]}
+ elif method == "importaddress":
+ self.imported_addresses.append(params[0])
+ else:
+ raise ValueError("unknown method in test jsonrpc")
+
+ def add_transaction(self, tx):
+ self.txlist.append(tx)
+
+ def get_imported_addresses(self):
+ return self.imported_addresses
+
+from electrumpersonalserver.deterministicwallet import DeterministicWallet
+
+class TestDeterministicWallet(DeterministicWallet):
+ """Empty deterministic wallets"""
+ def __init__(self):
+ pass
+
+ def have_scriptpubkeys_overrun_gaplimit(self, scriptpubkeys):
+ return None #not overrun
+
+ def get_new_scriptpubkeys(self, change, count):
+ pass
+
+def test_spk_to_address(spk):
+ return spk + "-address"
+
+def assert_address_history_tx(address_history, spk, height, txid, subscribed):
+ history_element = address_history[hashes.script_to_scripthash(spk)]
+ assert history_element["history"][0]["height"] == height
+ assert history_element["history"][0]["tx_hash"] == txid
+ #fee always zero, its easier to test because otherwise you have
+ # to use Decimal to stop float weirdness
+ if height == 0:
+ assert history_element["history"][0]["fee"] == 0
+ assert history_element["subscribed"] == subscribed
+
+def test():
+ debugf = lambda x: x
+ logf = lambda x: x
+ #empty deterministic wallets
+ deterministic_wallets = [TestDeterministicWallet()]
+ test_spk1 = "deadbeefdeadbeefdeadbeefdeadbeef"
+ test_containing_block1 = "blockhash-placeholder1"
+ test_paying_in_tx1 = {
+ "txid": "placeholder-test-txid1",
+ "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}],
+ "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk1}}],
+ "address": test_spk_to_address(test_spk1),
+ "category": "receive",
+ "confirmations": 1,
+ "blockhash": test_containing_block1,
+ "hex": "placeholder-test-txhex1"
+ }
+ test_spk2 = "deadbeefdeadbeefdeadbeef"
+ test_containing_block2 = "blockhash-placeholder2"
+ test_paying_in_tx2 = {
+ "txid": "placeholder-test-txid2",
+ "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}],
+ "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk2}}],
+ "address": test_spk_to_address(test_spk2),
+ "category": "receive",
+ "confirmations": 1,
+ "blockhash": test_containing_block2,
+ "hex": "placeholder-test-txhex2"
+ }
+
+ ###single confirmed tx in wallet belonging to us, address history built
+ rpc = TestJsonRpc([test_paying_in_tx1], [],
+ {test_containing_block1: 420000})
+ txmonitor1 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf)
+ assert txmonitor1.build_address_history([test_spk1])
+ assert len(txmonitor1.address_history) == 1
+ assert_address_history_tx(txmonitor1.address_history, spk=test_spk1,
+ height=420000, txid=test_paying_in_tx1["txid"], subscribed=False)
+
+ ###two confirmed txes in wallet belonging to us, addr history built
+ rpc = TestJsonRpc([test_paying_in_tx1, test_paying_in_tx2], [],
+ {test_containing_block1: 1, test_containing_block2: 2})
+ deterministic_wallets = [TestDeterministicWallet()]
+ txmonitor2 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf)
+ assert txmonitor2.build_address_history([test_spk1, test_spk2])
+ assert len(txmonitor2.address_history) == 2
+ assert_address_history_tx(txmonitor2.address_history, spk=test_spk1,
+ height=1, txid=test_paying_in_tx1["txid"], subscribed=False)
+ assert_address_history_tx(txmonitor2.address_history, spk=test_spk2,
+ height=2, txid=test_paying_in_tx2["txid"], subscribed=False)
+
+ ###one unconfirmed tx in wallet belonging to us, with confirmed inputs,
+ ### addr history built, then tx confirms, not subscribed to address
+ test_spk3 = "deadbeefdeadbeef"
+ test_containing_block3 = "blockhash-placeholder3"
+ input_utxo3 = {"txid": "placeholder-unknown-input-txid", "vout": 0,
+ "value": 1, "confirmations": 1}
+ test_paying_in_tx3 = {
+ "txid": "placeholder-test-txid3",
+ "vin": [input_utxo3],
+ "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk3}}],
+ "address": test_spk_to_address(test_spk3),
+ "category": "receive",
+ "confirmations": 0,
+ "blockhash": test_containing_block3,
+ "hex": "placeholder-test-txhex3"
+ }
+ rpc = TestJsonRpc([test_paying_in_tx3], [input_utxo3],
+ {test_containing_block3: 10})
+ txmonitor3 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf)
+ assert txmonitor3.build_address_history([test_spk3])
+ assert len(txmonitor3.address_history) == 1
+ assert_address_history_tx(txmonitor3.address_history, spk=test_spk3,
+ height=0, txid=test_paying_in_tx3["txid"], subscribed=False)
+ assert len(list(txmonitor3.check_for_updated_txes())) == 0
+ test_paying_in_tx3["confirmations"] = 1 #tx confirms
+ #not subscribed so still only returns an empty list
+ assert len(list(txmonitor3.check_for_updated_txes())) == 0
+ assert_address_history_tx(txmonitor3.address_history, spk=test_spk3,
+ height=10, txid=test_paying_in_tx3["txid"], subscribed=False)
+
+ ###build empty address history, subscribe one address
+ ### an unconfirmed tx appears, then confirms
+ test_spk4 = "deadbeefdeadbeefaa"
+ test_containing_block4 = "blockhash-placeholder4"
+ input_utxo4 = {"txid": "placeholder-unknown-input-txid", "vout": 0,
+ "value": 1, "confirmations": 1}
+ test_paying_in_tx4 = {
+ "txid": "placeholder-test-txid4",
+ "vin": [input_utxo4],
+ "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk4}}],
+ "address": test_spk_to_address(test_spk4),
+ "category": "receive",
+ "confirmations": 0,
+ "blockhash": test_containing_block4,
+ "hex": "placeholder-test-txhex4"
+ }
+ rpc = TestJsonRpc([], [input_utxo4], {test_containing_block4: 10})
+ txmonitor4 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf)
+ assert txmonitor4.build_address_history([test_spk4])
+ assert len(txmonitor4.address_history) == 1
+ sh4 = hashes.script_to_scripthash(test_spk4)
+ assert len(txmonitor4.get_electrum_history(sh4)) == 0
+ txmonitor4.subscribe_address(sh4)
+ # unconfirm transaction appears
+ assert len(list(txmonitor4.check_for_updated_txes())) == 0
+ rpc.add_transaction(test_paying_in_tx4)
+ assert len(list(txmonitor4.check_for_updated_txes())) == 1
+ assert_address_history_tx(txmonitor4.address_history, spk=test_spk4,
+ height=0, txid=test_paying_in_tx4["txid"], subscribed=True)
+ # transaction confirms
+ test_paying_in_tx4["confirmations"] = 1
+ assert len(list(txmonitor4.check_for_updated_txes())) == 1
+ assert_address_history_tx(txmonitor4.address_history, spk=test_spk4,
+ height=10, txid=test_paying_in_tx4["txid"], subscribed=True)
+
+ ###transaction that has nothing to do with our wallet
+ test_spk5 = "deadbeefdeadbeefbb"
+ test_containing_block5 = "blockhash-placeholder5"
+ test_paying_in_tx5 = {
+ "txid": "placeholder-test-txid5",
+ "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}],
+ "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk5}}],
+ "address": test_spk_to_address(test_spk5),
+ "category": "receive",
+ "confirmations": 0,
+ "blockhash": test_containing_block5,
+ "hex": "placeholder-test-txhex5"
+ }
+ test_spk5_1 = "deadbeefdeadbeefcc"
+ rpc = TestJsonRpc([test_paying_in_tx5], [], {test_containing_block4: 10})
+ txmonitor5 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf)
+ assert txmonitor5.build_address_history([test_spk5_1])
+ assert len(txmonitor5.address_history) == 1
+ assert len(txmonitor5.get_electrum_history(hashes.script_to_scripthash(
+ test_spk5_1))) == 0
+
+ ###transaction which arrives to an address which already has a tx on it
+ test_spk6 = "deadbeefdeadbeefdd"
+ test_containing_block6 = "blockhash-placeholder6"
+ test_paying_in_tx6 = {
+ "txid": "placeholder-test-txid6",
+ "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}],
+ "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk6}}],
+ "address": test_spk_to_address(test_spk6),
+ "category": "receive",
+ "confirmations": 1,
+ "blockhash": test_containing_block6,
+ "hex": "placeholder-test-txhex6"
+ }
+ test_paying_in_tx6_1 = {
+ "txid": "placeholder-test-txid6_1",
+ "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}],
+ "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk6}}],
+ "address": test_spk_to_address(test_spk6),
+ "category": "receive",
+ "confirmations": 1,
+ "blockhash": test_containing_block6,
+ "hex": "placeholder-test-txhex6"
+ }
+ rpc = TestJsonRpc([test_paying_in_tx6], [], {test_containing_block6: 10})
+ txmonitor6 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf)
+ assert txmonitor6.build_address_history([test_spk6])
+ sh = hashes.script_to_scripthash(test_spk6)
+ assert len(txmonitor6.get_electrum_history(sh)) == 1
+ rpc.add_transaction(test_paying_in_tx6_1)
+ assert len(txmonitor6.get_electrum_history(sh)) == 1
+ txmonitor6.check_for_updated_txes()
+ assert len(txmonitor6.get_electrum_history(sh)) == 2
+
+ ###transaction spending FROM one of our addresses
+ test_spk7 = "deadbeefdeadbeefee"
+ test_input_containing_block7 = "blockhash-input-placeholder7"
+ test_input_tx7 = {
+ "txid": "placeholder-input-test-txid7",
+ "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}],
+ "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk7}}],
+ "address": test_spk_to_address(test_spk7),
+ "category": "send",
+ "confirmations": 2,
+ "blockhash": test_input_containing_block7,
+ "hex": "placeholder-input-test-txhex7"
+ }
+ test_containing_block7 = "blockhash-placeholder7"
+ test_paying_from_tx7 = {
+ "txid": "placeholder-test-txid7",
+ "vin": [{"txid": test_input_tx7["txid"], "vout": 0}],
+ "vout": [{"value": 1, "scriptPubKey": {"hex": "deadbeef"}}],
+ "address": test_spk_to_address(test_spk7),
+ "category": "receive",
+ "confirmations": 1,
+ "blockhash": test_containing_block7,
+ "hex": "placeholder-test-txhex7"
+ }
+ rpc = TestJsonRpc([test_input_tx7, test_paying_from_tx7], [],
+ {test_containing_block7: 9, test_input_containing_block7: 8})
+ txmonitor7 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf)
+ assert txmonitor7.build_address_history([test_spk7])
+ sh = hashes.script_to_scripthash(test_spk7)
+ assert len(txmonitor7.get_electrum_history(sh)) == 2
+
+ ###transaction from one address to the other, both addresses in wallet
+ test_spk8 = "deadbeefdeadbeefee"
+ test_spk8_1 = "deadbeefdeadbeefff"
+ test_input_containing_block8 = "blockhash-input-placeholder8"
+ test_input_tx8 = {
+ "txid": "placeholder-input-test-txid8",
+ "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}],
+ "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk8}}],
+ "address": test_spk_to_address(test_spk8),
+ "category": "send",
+ "confirmations": 2,
+ "blockhash": test_input_containing_block8,
+ "hex": "placeholder-input-test-txhex8"
+ }
+ test_containing_block8 = "blockhash-placeholder8"
+ test_paying_from_tx8 = {
+ "txid": "placeholder-test-txid8",
+ "vin": [{"txid": test_input_tx8["txid"], "vout": 0}],
+ "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk8_1}}],
+ "address": test_spk_to_address(test_spk8),
+ "category": "receive",
+ "confirmations": 1,
+ "blockhash": test_containing_block8,
+ "hex": "placeholder-test-txhex8"
+ }
+ rpc = TestJsonRpc([test_input_tx8, test_paying_from_tx8], [],
+ {test_containing_block8: 9, test_input_containing_block8: 8})
+ txmonitor8 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf)
+ assert txmonitor8.build_address_history([test_spk8, test_spk8_1])
+ assert len(txmonitor8.get_electrum_history(hashes.script_to_scripthash(
+ test_spk8))) == 2
+ assert len(txmonitor8.get_electrum_history(hashes.script_to_scripthash(
+ test_spk8_1))) == 1
+
+ ###overrun gap limit so import address is needed
+ test_spk9 = "deadbeefdeadbeef00"
+ test_containing_block9 = "blockhash-placeholder9"
+ test_paying_in_tx9 = {
+ "txid": "placeholder-test-txid9",
+ "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}],
+ "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk9}}],
+ "address": test_spk_to_address(test_spk9),
+ "category": "receive",
+ "confirmations": 1,
+ "blockhash": test_containing_block9,
+ "hex": "placeholder-test-txhex9"
+ }
+ test_spk9_imported = "deadbeefdeadbeef11"
+ class TestImportDeterministicWallet(DeterministicWallet):
+ def __init__(self):
+ pass
+
+ def have_scriptpubkeys_overrun_gaplimit(self, scriptpubkeys):
+ return {0: 1} #overrun by one
+
+ def get_new_scriptpubkeys(self, change, count):
+ return [test_spk9_imported]
+
+ rpc = TestJsonRpc([], [], {test_containing_block9: 10})
+ txmonitor9 = TransactionMonitor(rpc, [TestImportDeterministicWallet()],
+ debugf, logf)
+ assert txmonitor9.build_address_history([test_spk9])
+ assert len(txmonitor9.address_history) == 1
+ assert len(list(txmonitor9.check_for_updated_txes())) == 0
+ assert len(txmonitor9.get_electrum_history(hashes.script_to_scripthash(
+ test_spk9))) == 0
+ rpc.add_transaction(test_paying_in_tx9)
+ assert len(list(txmonitor9.check_for_updated_txes())) == 0
+ assert len(txmonitor9.get_electrum_history(hashes.script_to_scripthash(
+ test_spk9))) == 1
+ assert len(txmonitor9.get_electrum_history(hashes.script_to_scripthash(
+ test_spk9_imported))) == 0
+ assert len(rpc.get_imported_addresses()) == 1
+ assert rpc.get_imported_addresses()[0] == test_spk_to_address(
+ test_spk9_imported)
+
+ #other possible stuff to test:
+ #finding confirmed and unconfirmed tx, in that order, then both confirm
+ #finding unconfirmed and confirmed tx, in that order, then both confirm
+
+ print("\nAll tests passed")
+
+if __name__ == "__main__":
+ test()
+
diff --git a/merkleproof.py b/merkleproof.py
@@ -1,270 +0,0 @@
-
-import bitcoin as btc
-import binascii
-from math import ceil, log
-
-from hashes import hash_encode, hash_decode, Hash, hash_merkle_root
-
-#lots of ideas and code taken from bitcoin core and breadwallet
-#https://github.com/bitcoin/bitcoin/blob/master/src/merkleblock.h
-#https://github.com/breadwallet/breadwallet-core/blob/master/BRMerkleBlock.c
-
-def calc_tree_width(height, txcount):
- """Efficently calculates the number of nodes at given merkle tree height"""
- return (txcount + (1 << height) - 1) >> height
-
-def decend_merkle_tree(hashes, flags, height, txcount, pos):
- """Function recursively follows the flags bitstring down into the
- tree, building up a tree in memory"""
- flag = next(flags)
- if height > 0:
- #non-txid node
- if flag:
- left = decend_merkle_tree(hashes, flags, height-1, txcount, pos*2)
- #bitcoin's merkle tree format has a rule that if theres an
- # odd number of nodes in then the tree, the last hash is duplicated
- #in the electrum format we must hash together the duplicate
- # tree branch
- if pos*2+1 < calc_tree_width(height-1, txcount):
- right = decend_merkle_tree(hashes, flags, height-1,
- txcount, pos*2+1)
- else:
- if isinstance(left, tuple):
- right = expand_tree_hashing(left)
- else:
- right = left
- return (left, right)
- else:
- hs = next(hashes)
- return hs
- else:
- #txid node
- hs = next(hashes)
- if flag:
- #for the actual transaction, also store its position with a flag
- return "tx:" + str(pos) + ":" + hs
- else:
- return hs
-
-def deserialize_core_format_merkle_proof(hash_list, flag_value, txcount):
- """Converts core's format for a merkle proof into a tree in memory"""
- tree_depth = int(ceil(log(txcount, 2)))
- hashes = iter(hash_list)
- #one-liner which converts the flags value to a list of True/False bits
- flags = (flag_value[i//8]&1 << i%8 != 0 for i in range(len(flag_value)*8))
- try:
- root_node = decend_merkle_tree(hashes, flags, tree_depth, txcount, 0)
- return root_node
- except StopIteration:
- raise ValueError
-
-def expand_tree_electrum_format_merkle_proof(node, result):
- """Recurse down into the tree, adding hashes to the result list
- in depth order"""
- left, right = node
- if isinstance(left, tuple):
- expand_tree_electrum_format_merkle_proof(left, result)
- if isinstance(right, tuple):
- expand_tree_electrum_format_merkle_proof(right, result)
- if not isinstance(left, tuple):
- result.append(left)
- if not isinstance(right, tuple):
- result.append(right)
-
-def get_node_hash(node):
- if node.startswith("tx"):
- return node.split(":")[2]
- else:
- return node
-
-def expand_tree_hashing(node):
- """Recurse down into the tree, hashing everything and
- returning root hash"""
- left, right = node
- if isinstance(left, tuple):
- hash_left = expand_tree_hashing(left)
- else:
- hash_left = get_node_hash(left)
- if isinstance(right, tuple):
- hash_right = expand_tree_hashing(right)
- else:
- hash_right = get_node_hash(right)
- return hash_encode(Hash(hash_decode(hash_left) + hash_decode(hash_right)))
-
-def convert_core_to_electrum_merkle_proof(proof):
- """Bitcoin Core and Electrum use different formats for merkle
- proof, this function converts from Core's format to Electrum's format"""
- proof = binascii.unhexlify(proof)
- pos = [0]
- def read_as_int(bytez):
- pos[0] += bytez
- return btc.decode(proof[pos[0] - bytez:pos[0]][::-1], 256)
- def read_var_int():
- pos[0] += 1
- val = btc.from_byte_to_int(proof[pos[0] - 1])
- if val < 253:
- return val
- return read_as_int(pow(2, val - 252))
- def read_bytes(bytez):
- pos[0] += bytez
- return proof[pos[0] - bytez:pos[0]]
-
- merkle_root = proof[36:36+32]
- pos[0] = 80
- txcount = read_as_int(4)
- hash_count = read_var_int()
- hashes = [hash_encode(read_bytes(32)) for i in range(hash_count)]
- flags_count = read_var_int()
- flags = read_bytes(flags_count)
-
- root_node = deserialize_core_format_merkle_proof(hashes, flags, txcount)
- #check special case of a tree of zero height, block with only coinbase tx
- if not isinstance(root_node, tuple):
- root_node = root_node[5:] #remove the "tx:0:"
- result = {"pos": 0, "merkle": [], "txid": root_node,
- "merkleroot": hash_encode(merkle_root)}
- return result
-
- hashes_list = []
- expand_tree_electrum_format_merkle_proof(root_node, hashes_list)
- #remove the first or second element which is the txhash
- tx = hashes_list[0]
- if hashes_list[1].startswith("tx"):
- tx = hashes_list[1]
- assert(tx.startswith("tx"))
- hashes_list.remove(tx)
- #if the txhash was duplicated, that _is_ included in electrum's format
- if hashes_list[0].startswith("tx"):
- hashes_list[0] = tx.split(":")[2]
- tx_pos, txid = tx.split(":")[1:3]
- tx_pos = int(tx_pos)
- result = {"pos": tx_pos, "merkle": hashes_list, "txid": txid,
- "merkleroot": hash_encode(merkle_root)}
- return result
-
-merkle_proof_test_vectors = [
- #txcount 819, pos 5
- "0300000026e696fba00f0a43907239305eed9e55824e0e376636380f000000000000000" +
- "04f8a2ce51d6c69988029837688cbfc2f580799fa1747456b9c80ab808c1431acd0b07f" +
- "5543201618cadcfbf7330300000b0ff1e0050fed22ca360e0935e053b0fe098f6f9e090" +
- "f5631013361620d964fe2fd88544ae10b40621e1cd24bb4306e3815dc237f77118a45d7" +
- "5ada9ee362314b70573732bce59615a3bcc1bbacd04b33b7819198212216b5d62d75be5" +
- "9221ada17ba4fb2476b689cccd3be54732fd5630832a94f11fa3f0dafd6f904d43219e0" +
- "d7de110158446b5b598bd241f7b5df4da0ebc7d30e7748d487917b718df51c681174e6a" +
- "bab8042cc7c1c436221c098f06a56134f9247a812126d675d69c82ba1c715cfc0cde462" +
- "fd1fbe5dc87f6b8db2b9c060fcd59a20e7fe8e921c3676937a873ff88684f4be4d015f2" +
- "4f26af6d2cf78335e9218bcceba4507d0b4ba6cb933aa01ef77ae5eb411893ec0f74b69" +
- "590fb0f5118ac937c02ccd47e9d90be78becd11ecf854d7d268eeb479b74d137278c0a5" +
- "017d29e90cd5b35a4680201824fb0eb4f404e20dfeaec4d50549030b7e7e220b02eb210" +
- "5f3d2e8bcc94d547214a9d03ff1600",
- #txcount 47, pos 9
- "0100000053696a625fbd16df418575bce0c4148886c422774fca5fcab80100000000000" +
- "01532bfe4f9c4f56cd141028e5b59384c133740174b74b1982c7f01020b90ce05577c67" +
- "508bdb051a7ec2ef942f000000076cde2eb7efa90b36d48aed612e559ff2ba638d8d400" +
- "b14b0c58df00c6a6c33b65dc8fa02f4ca56e1f4dcf17186fa9bbd990ce150b6e2dc9e9e" +
- "56bb4f270fe56fde6bdd73a7a7e82767714862888e6b759568fb117674ad23050e29311" +
- "97494d457efb72efdb9cb79cd4a435724908a0eb31ec7f7a67ee03837319e098b43edad" +
- "3be9af75ae7b30db6f4f93ba0fdd941fdf70fe8cc38982e03bd292f5bd02f28137d343f" +
- "908c7d6417379afe8349a257af3ca1f74f623be6a416fe1aa96a8f259983f2cf32121bc" +
- "e203955a378b3b44f132ea6ab94c7829a6c3b360c9f8da8e74027701",
- #txcount 2582, pos 330
- "000000206365d5e1d8b7fdf0b846cfa902115c1b8ced9dd49cb17800000000000000000" +
- "01032e829e1f9a5a09d0492f9cd3ec0762b7facea555989c3927d3d975fd4078c771849" +
- "5a45960018edd3b9e0160a00000dfe856a7d5d77c23ebf85c68b5eb303d85e56491ed6d" +
- "204372625d0b4383df5a44d6e46d2db09d936b9f5d0b53e0dbcb3efb7773d457369c228" +
- "fd1ce6e11645e366a58b3fc1e8a7c916710ce29a87265a6729a3b221b47ea9c8e6f4870" +
- "7b112b8d67e5cfb3db5f88b042dc49e4e5bc2e61c28e1e0fbcba4c741bb5c75cac58ca0" +
- "4161a7377d70f3fd19a3e248ed918c91709b49afd3760f89ed2fefbcc9c23447ccb40a2" +
- "be7aba22b07189b0bf90c62db48a9fe37227e12c7af8c1d4c22f9f223530dacdd5f3ad8" +
- "50ad4badf16cc24049a65334f59bf28c15cecda1a4cf3f2937bd70ee84f66569ce8ef95" +
- "1d50cca46d60337e6c697685b38ad217967bbe6801d03c44fcb808cd035be31888380a2" +
- "df1be14b6ff100de83cab0dce250e2b40ca3b47e8309f848646bee63b6185c176d84f15" +
- "46a482e7a65a87d1a2d0d5a2b683e2cae0520df1e3525a71d71e1f551abd7d238c3bcb4" +
- "ecaeea7d5988745fa421a8604a99857426957a2ccfa7cd8df145aa8293701989dd20750" +
- "5923fcb339843944ce3d21dc259bcda9c251ed90d4e55af2cf5b15432050084f513ac74" +
- "c0bdd4b6046fb70100",
- #txcount 2861, pos 2860, last tx with many duplicated nodes down the tree
- "00000020c656c90b521a2bbca14174f2939b882a28d23d86144b0e00000000000000000" +
- "0cf5185a8e369c3de5f15e039e777760994fd66184b619d839dace3aec9953fd6d86159" +
- "5ac1910018ee097a972d0b0000078d20d71c3114dbf52bb13f2c18f987891e8854d2d29" +
- "f61c0b3d3256afcef7c0b1e6f76d6431f93390ebd28dbb81ad7c8f08459e85efeb23cc7" +
- "2df2c5612215bf53dd4ab3703886bc8c82cb78ba761855e495fb5dc371cd8fe25ae974b" +
- "df42269e267caf898a9f34cbf2350eaaa4afbeaea70636b5a3b73682186817db5b33290" +
- "bd5c696bd8d0322671ff70c5447fcd7bdc127e5b84350c6b14b5a3b86b424d7db38d39f" +
- "171f57e255a31c6c53415e3d65408b6519a40aacc49cad8e70646d4cb0d23d4a63068e6" +
- "c220efc8a2781e9e774efdd334108d7453043bd3c8070d0e5903ad5b07",
- #txcount 7, pos 6, duplicated entry in the last depth, at tx level
- "0100000056e02c6d3278c754e0699517834741f7c4ad3dcbfeb7803a346200000000000" +
- "0af3bdd5dd465443fd003e9281455e60aae573dd4d46304d7ba17276ea33d506488cbb4" +
- "4dacb5001b9ebb193b0700000003cd3abb2eb7583165f36b56add7268be9027ead4cc8f" +
- "888ec650d3b1c1f4de28a0ff7c8b463b2042d09598f0e5e5905de362aa1cf75252adc22" +
- "719b8e1bc969adcfbc4782b8eafc9352263770b91a0f189ae051cbe0e26046c2b14cf3d" +
- "8be0bc40135",
- #txcount 6, pos 5, duplicated entry in the last-but-one depth
- "01000000299edfd28524eae4fb6012e4087afdb6e1b912db85e612374b0300000000000" +
- "0e16572394f8578a47bf36e15cd16faa5e3b9e18805cf4e271ae4ef95aa8cea7eb31fa1" +
- "4e4b6d0b1a42857d960600000003f52b45ed953924366dab3e92707145c78615b639751" +
- "ecb7be1c5ecc09b592ed588ca0e15a89e9049a2dbcadf4d8362bd1f74a6972f176617b5" +
- "8a5466c8a4121fc3e2d6fa66c8637b387ef190ab46d6e9c9dae4bbccd871c72372b3dbc" +
- "6edefea012d",
- #txcount 5, pos 4, duplicated on the last and second last depth
- "010000004d891de57908d89e9e5585648978d7650adc67e856c2d8c18c1800000000000" +
- "04746fd317bffecd4ffb320239caa06685bafe0c1b5463b24d636e45788796657843d1b" +
- "4d4c86041be68355c40500000002d8e26c89c46477f2407d866d2badbd98e43e732a670" +
- "e96001faf1744b27e5fdd018733d72e31a2d6a0d94f2a3b35fcc66fb110c40c5bbff82b" +
- "f87606553d541d011d",
- #txcount 2739, pos 0, coinbase tx
- "000000209f283da030c6e6d0ff5087a87c430d140ed6b4564fa34d00000000000000000" +
- "0ec1513723e3652c6b8e777c41eb267ad8dd2025e85228840f5cfca7ffe1fb331afff8a" +
- "5af8e961175e0f7691b30a00000df403e21a4751fbd52457f535378ac2dcf111199e9ea" +
- "6f78f6c2663cb99b58203438d8f3b26f7f2804668c1df7d394a4726363d4873b2d85b71" +
- "2e44cf4f5e4f33f22a8f3a1672846bd7c4570c668e6ee12befda23bfa3d0fcd30b1b079" +
- "19b01c40b1e31b6d34fcdbb99539d46eb97a3ae15386f1ab0f28ecacadd9fc3fa4ce49a" +
- "1a1839d815229f54036c8a3035d91e80e8dc127b62032b4e652550b4fc0aee0f6e85a14" +
- "307d85ed9dde62acff9a0f7e3b52370a10d6c83ec13a0b4a8fafe87af368a167d7e9b63" +
- "3b84b6ea65f1ce5e8ccc1840be0a4dab0099e25afccc7f2fdbda54cd65ecbac8d9a550c" +
- "108b4e18d3af59129d373fde4c80848858fd6f7fc1e27387a38833473ca8a47729fa6e1" +
- "cc14b584c14dad768108ff18cc6acdc9c31d32dc71c3c80856664a3fff870fe419a59aa" +
- "9033356590475d36086f0b3c0ece34c0f3756675c610fb980ff3363af6f9c0918a7c677" +
- "23371849de9c1026515c2900a80b3aee4f2625c8f48cd5eb967560ee8ebe58a8d41c331" +
- "f6d5199795735d4f0494bdf592d166fa291062733619f0f133605087365639de2d9d5d6" +
- "921f4b4204ff1f0000",
- #txcount 1, pos 0, coinbase tx in an empty block, tree with height 1
- "010000000508085c47cc849eb80ea905cc7800a3be674ffc57263cf210c59d8d0000000" +
- "0112ba175a1e04b14ba9e7ea5f76ab640affeef5ec98173ac9799a852fa39add320cd66" +
- "49ffff001d1e2de5650100000001112ba175a1e04b14ba9e7ea5f76ab640affeef5ec98" +
- "173ac9799a852fa39add30101",
- #txcount 2, pos 1, tree with height 2
- "010000004e24a2880cd72d9bde7502087bd3756819794dc7548f68dd68dc30010000000" +
- "02793fce9cdf91b4f84760571bf6009d5f0ffaddbfdc9234ef58a036096092117b10f4b" +
- "4cfd68011c903e350b0200000002ee50562fc6f995eff2df61be0d5f943bac941149aa2" +
- "1aacb32adc130c0f17d6a2077a642b1eabbc5120e31566a11e2689aa4d39b01cce9a190" +
- "2360baa5e4328e0105"
-]
-
-def test():
- for proof in merkle_proof_test_vectors:
- try:
- electrum_proof = convert_core_to_electrum_merkle_proof(proof)
- #print(electrum_proof)
- implied_merkle_root = hash_merkle_root(
- electrum_proof["merkle"], electrum_proof["txid"],
- electrum_proof["pos"])
- assert implied_merkle_root == electrum_proof["merkleroot"]
- except ValueError:
- import traceback
- traceback.print_exc()
- assert 0
- print("All tests passed")
-
-'''
-proof = ""
-def chunks(d, n):
- return [d[x:x + n] for x in range(0, len(d), n)]
-#print(proof)
-print("\" + \n\"".join(chunks(proof, 71)))
-'''
-
-if __name__ == "__main__":
- test()
-
diff --git a/rescan-script.py b/rescan-script.py
@@ -1,7 +1,7 @@
#! /usr/bin/python3
from configparser import ConfigParser, NoSectionError
-from jsonrpc import JsonRpc, JsonRpcError
+from electrumpersonalserver.jsonrpc import JsonRpc, JsonRpcError
from datetime import datetime
import server
diff --git a/server.py b/server.py
@@ -1,13 +1,13 @@
#! /usr/bin/python3
import socket, time, json, datetime, struct, binascii, ssl, os.path, platform
-import sys
from configparser import ConfigParser, NoSectionError
-from jsonrpc import JsonRpc, JsonRpcError
-import hashes, merkleproof, deterministicwallet, transactionmonitor
-
-ADDRESSES_LABEL = "electrum-watchonly-addresses"
+from electrumpersonalserver.jsonrpc import JsonRpc, JsonRpcError
+import electrumpersonalserver.hashes as hashes
+import electrumpersonalserver.merkleproof as merkleproof
+import electrumpersonalserver.deterministicwallet as deterministicwallet
+import electrumpersonalserver.transactionmonitor as transactionmonitor
VERSION_NUMBER = "0.1"
@@ -304,7 +304,7 @@ def run_electrum_server(hostport, rpc, txmonitor, poll_interval_listening,
def get_scriptpubkeys_to_monitor(rpc, config):
imported_addresses = set(rpc.call("getaddressesbyaccount",
- [ADDRESSES_LABEL]))
+ [transactionmonitor.ADDRESSES_LABEL]))
deterministic_wallets = []
for key in config.options("master-public-keys"):
@@ -373,22 +373,6 @@ def get_scriptpubkeys_to_monitor(rpc, config):
for addr in watch_only_addresses])
return False, spks_to_monitor, deterministic_wallets
-def import_addresses(rpc, addrs):
- debug("importing addrs = " + str(addrs))
- log("Importing " + str(len(addrs)) + " addresses in total")
- addr_i = iter(addrs)
- notifications = 10
- for i in range(notifications):
- pc = int(100.0 * i / notifications)
- sys.stdout.write("[" + str(pc) + "%]... ")
- sys.stdout.flush()
- for j in range(int(len(addrs) / notifications)):
- rpc.call("importaddress", [next(addr_i), ADDRESSES_LABEL, False])
- for a in addr_i: #import the reminder of addresses
- rpc.call("importaddress", [a, ADDRESSES_LABEL, False])
- print("[100%]")
- log("Importing done")
-
def obtain_rpc_username_password(datadir):
if len(datadir.strip()) == 0:
debug("no datadir configuration, checking in default location")
@@ -446,7 +430,8 @@ def main():
import_needed, relevant_spks_addrs, deterministic_wallets = \
get_scriptpubkeys_to_monitor(rpc, config)
if import_needed:
- import_addresses(rpc, relevant_spks_addrs)
+ transactionmonitor.import_addresses(rpc, relevant_spks_addrs, debug,
+ log)
log("Done.\nIf recovering a wallet which already has existing " +
"transactions, then\nrun the rescan script. If you're confident " +
"that the wallets are new\nand empty then there's no need to " +
diff --git a/transactionmonitor.py b/transactionmonitor.py
@@ -1,705 +0,0 @@
-
-import time, pprint, math
-from decimal import Decimal
-
-from jsonrpc import JsonRpcError
-import server as s
-import hashes
-
-#internally this code uses scriptPubKeys, it only converts to bitcoin addresses
-# when importing to bitcoind or checking whether enough addresses have been
-# imported
-#the electrum protocol uses sha256(scriptpubkey) as a key for lookups
-# this code calls them scripthashes
-
-#code will generate the first address from each deterministic wallet
-# and check whether they have been imported into the bitcoin node
-# if no then initial_import_count addresses will be imported, then exit
-# if yes then initial_import_count addresses will be generated and extra
-# addresses will be generated one-by-one, each time checking whether they have
-# been imported into the bitcoin node
-# when an address has been reached that has not been imported, that means
-# we've reached the end, then rewind the deterministic wallet index by one
-
-#when a transaction happens paying to an address from a deterministic wallet
-# lookup the position of that address, if its less than gap_limit then
-# import more addresses
-
-class TransactionMonitor(object):
- """
- Class which monitors the bitcoind wallet for new transactions
- and builds a history datastructure for sending to electrum
- """
- def __init__(self, rpc, deterministic_wallets, debug, log):
- self.rpc = rpc
- self.deterministic_wallets = deterministic_wallets
- self.debug = debug
- self.log = log
- self.last_known_wallet_txid = None
- self.address_history = None
- self.unconfirmed_txes = None
-
- def get_electrum_history_hash(self, scrhash):
- return hashes.get_status_electrum( ((h["tx_hash"], h["height"])
- for h in self.address_history[scrhash]["history"]) )
-
- def get_electrum_history(self, scrhash):
- if scrhash in self.address_history:
- return self.address_history[scrhash]["history"]
- else:
- return None
-
- def subscribe_address(self, scrhash):
- if scrhash in self.address_history:
- self.address_history[scrhash]["subscribed"] = True
- return True
- else:
- return False
-
- def unsubscribe_all_addresses(self):
- for scrhash, his in self.address_history.items():
- his["subscribed"] = False
-
- def build_address_history(self, monitored_scriptpubkeys):
- self.log("Building history with " + str(len(monitored_scriptpubkeys)) +
- " addresses")
- st = time.time()
- address_history = {}
- for spk in monitored_scriptpubkeys:
- address_history[hashes.script_to_scripthash(spk)] = {'history': [],
- 'subscribed': False}
- wallet_addr_scripthashes = set(address_history.keys())
- #populate history
- #which is a blockheight-ordered list of ("txhash", height)
- #unconfirmed transactions go at the end as ("txhash", 0, fee)
- # 0=unconfirmed -1=unconfirmed with unconfirmed parents
-
- BATCH_SIZE = 1000
- ret = list(range(BATCH_SIZE))
- t = 0
- count = 0
- obtained_txids = set()
- while len(ret) == BATCH_SIZE:
- ret = self.rpc.call("listtransactions", ["*", BATCH_SIZE, t, True])
- self.debug("listtransactions skip=" + str(t) + " len(ret)="
- + str(len(ret)))
- t += len(ret)
- for tx in ret:
- if "txid" not in tx or "category" not in tx:
- continue
- if tx["category"] not in ("receive", "send"):
- continue
- if tx["txid"] in obtained_txids:
- continue
- self.debug("adding obtained tx=" + str(tx["txid"]))
- obtained_txids.add(tx["txid"])
-
- #obtain all the addresses this transaction is involved with
- output_scriptpubkeys, input_scriptpubkeys, txd = \
- self.get_input_and_output_scriptpubkeys(tx["txid"])
- output_scripthashes = [hashes.script_to_scripthash(sc)
- for sc in output_scriptpubkeys]
- sh_to_add = wallet_addr_scripthashes.intersection(set(
- output_scripthashes))
- input_scripthashes = [hashes.script_to_scripthash(sc)
- for sc in input_scriptpubkeys]
- sh_to_add |= wallet_addr_scripthashes.intersection(set(
- input_scripthashes))
- if len(sh_to_add) == 0:
- continue
-
- for wal in self.deterministic_wallets:
- overrun_depths = wal.have_scriptpubkeys_overrun_gaplimit(
- output_scriptpubkeys)
- if overrun_depths != None:
- self.log("ERROR: Not enough addresses imported.")
- self.log("Delete wallet.dat and increase the value " +
- "of `initial_import_count` in the file " +
- "`config.cfg` then reimport and rescan")
- #TODO make it so users dont have to delete wallet.dat
- # check whether all initial_import_count addresses are
- # imported rather than just the first one
- return False
- new_history_element = self.generate_new_history_element(tx, txd)
- for scripthash in sh_to_add:
- address_history[scripthash][
- "history"].append(new_history_element)
- count += 1
-
- unconfirmed_txes = {}
- for scrhash, his in address_history.items():
- uctx = self.sort_address_history_list(his)
- for u in uctx:
- if u["tx_hash"] in unconfirmed_txes:
- unconfirmed_txes[u["tx_hash"]].append(scrhash)
- else:
- unconfirmed_txes[u["tx_hash"]] = [scrhash]
- self.debug("unconfirmed_txes = " + str(unconfirmed_txes))
- if len(ret) > 0:
- #txid doesnt uniquely identify transactions from listtransactions
- #but the tuple (txid, address) does
- self.last_known_wallet_txid = (ret[-1]["txid"], ret[-1]["address"])
- else:
- self.last_known_wallet_txid = None
- self.debug("last_known_wallet_txid = " + str(
- self.last_known_wallet_txid))
-
- et = time.time()
- self.debug("address_history =\n" + pprint.pformat(address_history))
- self.log("Found " + str(count) + " txes. History built in " +
- str(et - st) + "sec")
- self.address_history = address_history
- self.unconfirmed_txes = unconfirmed_txes
- return True
-
- def get_input_and_output_scriptpubkeys(self, txid):
- gettx = self.rpc.call("gettransaction", [txid])
- txd = self.rpc.call("decoderawtransaction", [gettx["hex"]])
- output_scriptpubkeys = [out["scriptPubKey"]["hex"]
- for out in txd["vout"]]
- input_scriptpubkeys = []
- for inn in txd["vin"]:
- try:
- wallet_tx = self.rpc.call("gettransaction", [inn["txid"]])
- except JsonRpcError:
- #wallet doesnt know about this tx, so the input isnt ours
- continue
- input_decoded = self.rpc.call("decoderawtransaction", [wallet_tx[
- "hex"]])
- script = input_decoded["vout"][inn["vout"]]["scriptPubKey"]["hex"]
- input_scriptpubkeys.append(script)
- return output_scriptpubkeys, input_scriptpubkeys, txd
-
- def generate_new_history_element(self, tx, txd):
- if tx["confirmations"] == 0:
- unconfirmed_input = False
- total_input_value = 0
- for inn in txd["vin"]:
- utxo = self.rpc.call("gettxout", [inn["txid"], inn["vout"],
- True])
- if utxo is None:
- utxo = self.rpc.call("gettxout", [inn["txid"], inn["vout"],
- False])
- if utxo is None:
- self.debug("utxo not found(!)")
- #TODO detect this and figure out how to tell
- # electrum that we dont know the fee
- total_input_value += int(Decimal(utxo["value"]) * Decimal(1e8))
- unconfirmed_input = (unconfirmed_input or
- utxo["confirmations"] == 0)
- self.debug("total_input_value = " + str(total_input_value))
-
- fee = total_input_value - sum([int(Decimal(out["value"])
- * Decimal(1e8)) for out in txd["vout"]])
- height = -1 if unconfirmed_input else 0
- new_history_element = ({"tx_hash": tx["txid"], "height": height,
- "fee": fee})
- else:
- blockheader = self.rpc.call("getblockheader", [tx['blockhash']])
- new_history_element = ({"tx_hash": tx["txid"],
- "height": blockheader["height"]})
- return new_history_element
-
- def sort_address_history_list(self, his):
- unconfirm_txes = list(filter(lambda h:h["height"] == 0, his["history"]))
- confirm_txes = filter(lambda h:h["height"] != 0, his["history"])
- #TODO txes must be "in blockchain order"
- # the order they appear in the block
- # it might be "blockindex" in listtransactions and gettransaction
- #so must sort with key height+':'+blockindex
- #maybe check if any heights are the same then get the pos only for those
- #better way to do this is to have a separate dict that isnt in history
- # which maps txid => blockindex
- # and then sort by key height+":"+idx[txid]
- his["history"] = sorted(confirm_txes, key=lambda h:h["height"])
- his["history"].extend(unconfirm_txes)
- return unconfirm_txes
-
- def check_for_updated_txes(self):
- updated_scrhashes1 = self.check_for_new_txes()
- updated_scrhashes2 = self.check_for_confirmations()
- updated_scrhashes = updated_scrhashes1 | updated_scrhashes2
- for ush in updated_scrhashes:
- his = self.address_history[ush]
- self.sort_address_history_list(his)
- if len(updated_scrhashes) > 0:
- self.debug("new tx address_history =\n"
- + pprint.pformat(self.address_history))
- self.debug("unconfirmed txes = " +
- pprint.pformat(self.unconfirmed_txes))
- self.debug("updated_scripthashes = " + str(updated_scrhashes))
- updated_scrhashes = filter(lambda sh:self.address_history[sh][
- "subscribed"], updated_scrhashes)
- return updated_scrhashes
-
- def check_for_confirmations(self):
- confirmed_txes_scrhashes = []
- self.debug("check4con unconfirmed_txes = "
- + pprint.pformat(self.unconfirmed_txes))
- for uc_txid, scrhashes in self.unconfirmed_txes.items():
- tx = self.rpc.call("gettransaction", [uc_txid])
- self.debug("uc_txid=" + uc_txid + " => " + str(tx))
- if tx["confirmations"] == 0:
- continue #still unconfirmed
- self.log("A transaction confirmed: " + uc_txid)
- confirmed_txes_scrhashes.append((uc_txid, scrhashes))
- block = self.rpc.call("getblockheader", [tx["blockhash"]])
- for scrhash in scrhashes:
- #delete the old unconfirmed entry in address_history
- deleted_entries = [h for h in self.address_history[scrhash][
- "history"] if h["tx_hash"] == uc_txid]
- for d_his in deleted_entries:
- self.address_history[scrhash]["history"].remove(d_his)
- #create the new confirmed entry in address_history
- self.address_history[scrhash]["history"].append({"height":
- block["height"], "tx_hash": uc_txid})
- updated_scrhashes = set()
- for tx, scrhashes in confirmed_txes_scrhashes:
- del self.unconfirmed_txes[tx]
- updated_scrhashes.update(set(scrhashes))
- return updated_scrhashes
-
- def check_for_new_txes(self):
- MAX_TX_REQUEST_COUNT = 256
- tx_request_count = 2
- max_attempts = int(math.log(MAX_TX_REQUEST_COUNT, 2))
- for i in range(max_attempts):
- self.debug("listtransactions tx_request_count="
- + str(tx_request_count))
- ret = self.rpc.call("listtransactions", ["*", tx_request_count, 0,
- True])
- ret = ret[::-1]
- if self.last_known_wallet_txid == None:
- recent_tx_index = len(ret) #=0 means no new txes
- break
- else:
- txid_list = [(tx["txid"], tx["address"]) for tx in ret]
- recent_tx_index = next((i for i, (txid, addr)
- in enumerate(txid_list) if
- txid == self.last_known_wallet_txid[0] and
- addr == self.last_known_wallet_txid[1]), -1)
- if recent_tx_index != -1:
- break
- tx_request_count *= 2
-
- #TODO low priority: handle a user getting more than 255 new
- # transactions in 15 seconds
- self.debug("recent tx index = " + str(recent_tx_index) + " ret = " +
- str([(t["txid"], t["address"]) for t in ret]))
- if len(ret) > 0:
- self.last_known_wallet_txid = (ret[0]["txid"], ret[0]["address"])
- self.debug("last_known_wallet_txid = " + str(
- self.last_known_wallet_txid))
- assert(recent_tx_index != -1)
- if recent_tx_index == 0:
- return set()
- new_txes = ret[:recent_tx_index][::-1]
- self.debug("new txes = " + str(new_txes))
- obtained_txids = set()
- updated_scripthashes = []
- for tx in new_txes:
- if "txid" not in tx or "category" not in tx:
- continue
- if tx["category"] not in ("receive", "send"):
- continue
- if tx["txid"] in obtained_txids:
- continue
- obtained_txids.add(tx["txid"])
- output_scriptpubkeys, input_scriptpubkeys, txd = \
- self.get_input_and_output_scriptpubkeys(tx["txid"])
- matching_scripthashes = []
- for spk in (output_scriptpubkeys + input_scriptpubkeys):
- scripthash = hashes.script_to_scripthash(spk)
- if scripthash in self.address_history:
- matching_scripthashes.append(scripthash)
- if len(matching_scripthashes) == 0:
- continue
-
- for wal in self.deterministic_wallets:
- overrun_depths = wal.have_scriptpubkeys_overrun_gaplimit(
- output_scriptpubkeys)
- if overrun_depths != None:
- for change, import_count in overrun_depths.items():
- spks = wal.get_new_scriptpubkeys(change, import_count)
- for spk in spks:
- self.address_history[hashes.script_to_scripthash(
- spk)] = {'history': [], 'subscribed': False}
- new_addrs = [hashes.script_to_address(s, self.rpc)
- for s in spks]
- self.debug("importing " + str(len(spks)) +
- " into change=" + str(change))
- s.import_addresses(self.rpc, new_addrs)
-
- updated_scripthashes.extend(matching_scripthashes)
- new_history_element = self.generate_new_history_element(tx, txd)
- self.log("Found new tx: " + str(new_history_element))
- for scrhash in matching_scripthashes:
- self.address_history[scrhash]["history"].append(
- new_history_element)
- if new_history_element["height"] == 0:
- if tx["txid"] in self.unconfirmed_txes:
- self.unconfirmed_txes[tx["txid"]].append(scrhash)
- else:
- self.unconfirmed_txes[tx["txid"]] = [scrhash]
- #check whether gap limits have been overrun and import more addrs
- return set(updated_scripthashes)
-
-
-## start tests here
-
-class TestJsonRpc(object):
- def __init__(self, txlist, utxoset, block_heights):
- self.txlist = txlist
- self.utxoset = utxoset
- self.block_heights = block_heights
- self.imported_addresses = []
-
- def call(self, method, params):
- if method == "listtransactions":
- count = int(params[1])
- skip = int(params[2])
- return self.txlist[skip:skip + count]
- elif method == "gettransaction":
- for t in self.txlist:
- if t["txid"] == params[0]:
- return t
- raise JsonRpcError({"code": None, "message": None})
- elif method == "decoderawtransaction":
- for t in self.txlist:
- if t["hex"] == params[0]:
- return t
- assert 0
- elif method == "gettxout":
- for u in self.utxoset:
- if u["txid"] == params[0] and u["vout"] == params[1]:
- return u
- assert 0
- elif method == "getblockheader":
- if params[0] not in self.block_heights:
- assert 0
- return {"height": self.block_heights[params[0]]}
- elif method == "decodescript":
- return {"addresses": [test_spk_to_address(params[0])]}
- elif method == "importaddress":
- self.imported_addresses.append(params[0])
- else:
- raise ValueError("unknown method in test jsonrpc")
-
- def add_transaction(self, tx):
- self.txlist.append(tx)
-
- def get_imported_addresses(self):
- return self.imported_addresses
-
-from deterministicwallet import DeterministicWallet
-
-class TestDeterministicWallet(DeterministicWallet):
- """Empty deterministic wallets"""
- def __init__(self):
- pass
-
- def have_scriptpubkeys_overrun_gaplimit(self, scriptpubkeys):
- return None #not overrun
-
- def get_new_scriptpubkeys(self, change, count):
- pass
-
-def test_spk_to_address(spk):
- return spk + "-address"
-
-def assert_address_history_tx(address_history, spk, height, txid, subscribed):
- history_element = address_history[hashes.script_to_scripthash(spk)]
- assert history_element["history"][0]["height"] == height
- assert history_element["history"][0]["tx_hash"] == txid
- #fee always zero, its easier to test because otherwise you have
- # to use Decimal to stop float weirdness
- if height == 0:
- assert history_element["history"][0]["fee"] == 0
- assert history_element["subscribed"] == subscribed
-
-def test():
- debugf = lambda x: x
- logf = lambda x: x
- #empty deterministic wallets
- deterministic_wallets = [TestDeterministicWallet()]
- test_spk1 = "deadbeefdeadbeefdeadbeefdeadbeef"
- test_containing_block1 = "blockhash-placeholder1"
- test_paying_in_tx1 = {
- "txid": "placeholder-test-txid1",
- "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}],
- "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk1}}],
- "address": test_spk_to_address(test_spk1),
- "category": "receive",
- "confirmations": 1,
- "blockhash": test_containing_block1,
- "hex": "placeholder-test-txhex1"
- }
- test_spk2 = "deadbeefdeadbeefdeadbeef"
- test_containing_block2 = "blockhash-placeholder2"
- test_paying_in_tx2 = {
- "txid": "placeholder-test-txid2",
- "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}],
- "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk2}}],
- "address": test_spk_to_address(test_spk2),
- "category": "receive",
- "confirmations": 1,
- "blockhash": test_containing_block2,
- "hex": "placeholder-test-txhex2"
- }
-
- ###single confirmed tx in wallet belonging to us, address history built
- rpc = TestJsonRpc([test_paying_in_tx1], [],
- {test_containing_block1: 420000})
- txmonitor1 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf)
- assert txmonitor1.build_address_history([test_spk1])
- assert len(txmonitor1.address_history) == 1
- assert_address_history_tx(txmonitor1.address_history, spk=test_spk1,
- height=420000, txid=test_paying_in_tx1["txid"], subscribed=False)
-
- ###two confirmed txes in wallet belonging to us, addr history built
- rpc = TestJsonRpc([test_paying_in_tx1, test_paying_in_tx2], [],
- {test_containing_block1: 1, test_containing_block2: 2})
- deterministic_wallets = [TestDeterministicWallet()]
- txmonitor2 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf)
- assert txmonitor2.build_address_history([test_spk1, test_spk2])
- assert len(txmonitor2.address_history) == 2
- assert_address_history_tx(txmonitor2.address_history, spk=test_spk1,
- height=1, txid=test_paying_in_tx1["txid"], subscribed=False)
- assert_address_history_tx(txmonitor2.address_history, spk=test_spk2,
- height=2, txid=test_paying_in_tx2["txid"], subscribed=False)
-
- ###one unconfirmed tx in wallet belonging to us, with confirmed inputs,
- ### addr history built, then tx confirms, not subscribed to address
- test_spk3 = "deadbeefdeadbeef"
- test_containing_block3 = "blockhash-placeholder3"
- input_utxo3 = {"txid": "placeholder-unknown-input-txid", "vout": 0,
- "value": 1, "confirmations": 1}
- test_paying_in_tx3 = {
- "txid": "placeholder-test-txid3",
- "vin": [input_utxo3],
- "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk3}}],
- "address": test_spk_to_address(test_spk3),
- "category": "receive",
- "confirmations": 0,
- "blockhash": test_containing_block3,
- "hex": "placeholder-test-txhex3"
- }
- rpc = TestJsonRpc([test_paying_in_tx3], [input_utxo3],
- {test_containing_block3: 10})
- txmonitor3 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf)
- assert txmonitor3.build_address_history([test_spk3])
- assert len(txmonitor3.address_history) == 1
- assert_address_history_tx(txmonitor3.address_history, spk=test_spk3,
- height=0, txid=test_paying_in_tx3["txid"], subscribed=False)
- assert len(list(txmonitor3.check_for_updated_txes())) == 0
- test_paying_in_tx3["confirmations"] = 1 #tx confirms
- #not subscribed so still only returns an empty list
- assert len(list(txmonitor3.check_for_updated_txes())) == 0
- assert_address_history_tx(txmonitor3.address_history, spk=test_spk3,
- height=10, txid=test_paying_in_tx3["txid"], subscribed=False)
-
- ###build empty address history, subscribe one address
- ### an unconfirmed tx appears, then confirms
- test_spk4 = "deadbeefdeadbeefaa"
- test_containing_block4 = "blockhash-placeholder4"
- input_utxo4 = {"txid": "placeholder-unknown-input-txid", "vout": 0,
- "value": 1, "confirmations": 1}
- test_paying_in_tx4 = {
- "txid": "placeholder-test-txid4",
- "vin": [input_utxo4],
- "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk4}}],
- "address": test_spk_to_address(test_spk4),
- "category": "receive",
- "confirmations": 0,
- "blockhash": test_containing_block4,
- "hex": "placeholder-test-txhex4"
- }
- rpc = TestJsonRpc([], [input_utxo4], {test_containing_block4: 10})
- txmonitor4 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf)
- assert txmonitor4.build_address_history([test_spk4])
- assert len(txmonitor4.address_history) == 1
- sh4 = hashes.script_to_scripthash(test_spk4)
- assert len(txmonitor4.get_electrum_history(sh4)) == 0
- txmonitor4.subscribe_address(sh4)
- # unconfirm transaction appears
- assert len(list(txmonitor4.check_for_updated_txes())) == 0
- rpc.add_transaction(test_paying_in_tx4)
- assert len(list(txmonitor4.check_for_updated_txes())) == 1
- assert_address_history_tx(txmonitor4.address_history, spk=test_spk4,
- height=0, txid=test_paying_in_tx4["txid"], subscribed=True)
- # transaction confirms
- test_paying_in_tx4["confirmations"] = 1
- assert len(list(txmonitor4.check_for_updated_txes())) == 1
- assert_address_history_tx(txmonitor4.address_history, spk=test_spk4,
- height=10, txid=test_paying_in_tx4["txid"], subscribed=True)
-
- ###transaction that has nothing to do with our wallet
- test_spk5 = "deadbeefdeadbeefbb"
- test_containing_block5 = "blockhash-placeholder5"
- test_paying_in_tx5 = {
- "txid": "placeholder-test-txid5",
- "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}],
- "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk5}}],
- "address": test_spk_to_address(test_spk5),
- "category": "receive",
- "confirmations": 0,
- "blockhash": test_containing_block5,
- "hex": "placeholder-test-txhex5"
- }
- test_spk5_1 = "deadbeefdeadbeefcc"
- rpc = TestJsonRpc([test_paying_in_tx5], [], {test_containing_block4: 10})
- txmonitor5 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf)
- assert txmonitor5.build_address_history([test_spk5_1])
- assert len(txmonitor5.address_history) == 1
- assert len(txmonitor5.get_electrum_history(hashes.script_to_scripthash(
- test_spk5_1))) == 0
-
- ###transaction which arrives to an address which already has a tx on it
- test_spk6 = "deadbeefdeadbeefdd"
- test_containing_block6 = "blockhash-placeholder6"
- test_paying_in_tx6 = {
- "txid": "placeholder-test-txid6",
- "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}],
- "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk6}}],
- "address": test_spk_to_address(test_spk6),
- "category": "receive",
- "confirmations": 1,
- "blockhash": test_containing_block6,
- "hex": "placeholder-test-txhex6"
- }
- test_paying_in_tx6_1 = {
- "txid": "placeholder-test-txid6_1",
- "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}],
- "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk6}}],
- "address": test_spk_to_address(test_spk6),
- "category": "receive",
- "confirmations": 1,
- "blockhash": test_containing_block6,
- "hex": "placeholder-test-txhex6"
- }
- rpc = TestJsonRpc([test_paying_in_tx6], [], {test_containing_block6: 10})
- txmonitor6 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf)
- assert txmonitor6.build_address_history([test_spk6])
- sh = hashes.script_to_scripthash(test_spk6)
- assert len(txmonitor6.get_electrum_history(sh)) == 1
- rpc.add_transaction(test_paying_in_tx6_1)
- assert len(txmonitor6.get_electrum_history(sh)) == 1
- txmonitor6.check_for_updated_txes()
- assert len(txmonitor6.get_electrum_history(sh)) == 2
-
- ###transaction spending FROM one of our addresses
- test_spk7 = "deadbeefdeadbeefee"
- test_input_containing_block7 = "blockhash-input-placeholder7"
- test_input_tx7 = {
- "txid": "placeholder-input-test-txid7",
- "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}],
- "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk7}}],
- "address": test_spk_to_address(test_spk7),
- "category": "send",
- "confirmations": 2,
- "blockhash": test_input_containing_block7,
- "hex": "placeholder-input-test-txhex7"
- }
- test_containing_block7 = "blockhash-placeholder7"
- test_paying_from_tx7 = {
- "txid": "placeholder-test-txid7",
- "vin": [{"txid": test_input_tx7["txid"], "vout": 0}],
- "vout": [{"value": 1, "scriptPubKey": {"hex": "deadbeef"}}],
- "address": test_spk_to_address(test_spk7),
- "category": "receive",
- "confirmations": 1,
- "blockhash": test_containing_block7,
- "hex": "placeholder-test-txhex7"
- }
- rpc = TestJsonRpc([test_input_tx7, test_paying_from_tx7], [],
- {test_containing_block7: 9, test_input_containing_block7: 8})
- txmonitor7 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf)
- assert txmonitor7.build_address_history([test_spk7])
- sh = hashes.script_to_scripthash(test_spk7)
- assert len(txmonitor7.get_electrum_history(sh)) == 2
-
- ###transaction from one address to the other, both addresses in wallet
- test_spk8 = "deadbeefdeadbeefee"
- test_spk8_1 = "deadbeefdeadbeefff"
- test_input_containing_block8 = "blockhash-input-placeholder8"
- test_input_tx8 = {
- "txid": "placeholder-input-test-txid8",
- "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}],
- "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk8}}],
- "address": test_spk_to_address(test_spk8),
- "category": "send",
- "confirmations": 2,
- "blockhash": test_input_containing_block8,
- "hex": "placeholder-input-test-txhex8"
- }
- test_containing_block8 = "blockhash-placeholder8"
- test_paying_from_tx8 = {
- "txid": "placeholder-test-txid8",
- "vin": [{"txid": test_input_tx8["txid"], "vout": 0}],
- "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk8_1}}],
- "address": test_spk_to_address(test_spk8),
- "category": "receive",
- "confirmations": 1,
- "blockhash": test_containing_block8,
- "hex": "placeholder-test-txhex8"
- }
- rpc = TestJsonRpc([test_input_tx8, test_paying_from_tx8], [],
- {test_containing_block8: 9, test_input_containing_block8: 8})
- txmonitor8 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf)
- assert txmonitor8.build_address_history([test_spk8, test_spk8_1])
- assert len(txmonitor8.get_electrum_history(hashes.script_to_scripthash(
- test_spk8))) == 2
- assert len(txmonitor8.get_electrum_history(hashes.script_to_scripthash(
- test_spk8_1))) == 1
-
- ###overrun gap limit so import address is needed
- test_spk9 = "deadbeefdeadbeef00"
- test_containing_block9 = "blockhash-placeholder9"
- test_paying_in_tx9 = {
- "txid": "placeholder-test-txid9",
- "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}],
- "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk9}}],
- "address": test_spk_to_address(test_spk9),
- "category": "receive",
- "confirmations": 1,
- "blockhash": test_containing_block9,
- "hex": "placeholder-test-txhex9"
- }
- test_spk9_imported = "deadbeefdeadbeef11"
- class TestImportDeterministicWallet(DeterministicWallet):
- def __init__(self):
- pass
-
- def have_scriptpubkeys_overrun_gaplimit(self, scriptpubkeys):
- return {0: 1} #overrun by one
-
- def get_new_scriptpubkeys(self, change, count):
- return [test_spk9_imported]
-
- rpc = TestJsonRpc([], [], {test_containing_block9: 10})
- txmonitor9 = TransactionMonitor(rpc, [TestImportDeterministicWallet()],
- debugf, logf)
- assert txmonitor9.build_address_history([test_spk9])
- assert len(txmonitor9.address_history) == 1
- assert len(list(txmonitor9.check_for_updated_txes())) == 0
- assert len(txmonitor9.get_electrum_history(hashes.script_to_scripthash(
- test_spk9))) == 0
- rpc.add_transaction(test_paying_in_tx9)
- assert len(list(txmonitor9.check_for_updated_txes())) == 0
- assert len(txmonitor9.get_electrum_history(hashes.script_to_scripthash(
- test_spk9))) == 1
- assert len(txmonitor9.get_electrum_history(hashes.script_to_scripthash(
- test_spk9_imported))) == 0
- assert len(rpc.get_imported_addresses()) == 1
- assert rpc.get_imported_addresses()[0] == test_spk_to_address(
- test_spk9_imported)
-
- #other possible stuff to test:
- #finding confirmed and unconfirmed tx, in that order, then both confirm
- #finding unconfirmed and confirmed tx, in that order, then both confirm
-
- print("\nAll tests passed")
-
-if __name__ == "__main__":
- test()
-