commit 99e2ff189e133a58ed70741aa85e145e8bfd8a5c
Author: chris-belcher <chris-belcher@users.noreply.github.com>
Date: Thu, 8 Feb 2018 16:45:35 +0000
first commit
Diffstat:
17 files changed, 3465 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,4 @@
+*.pyc
+*.swp
+config.cfg
+
diff --git a/LICENCE b/LICENCE
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
@@ -0,0 +1,88 @@
+# Electrum Personal Server
+
+Electrum Personal Server is an implementation of the Electrum server protocol
+which fulfills the specific need of using the Electrum UI with full node
+verification and privacy, but without the heavyweight server backend, for a
+single user. It allows the user to benefit from all of Bitcoin Core's
+resource-saving features like pruning, blocksonly and disabled txindex. All
+of Electrum's feature-richness like hardware wallet integration,
+multisignature wallets, offline signing, mnemonic recovery phrases and so on
+can still be used, but backed by the user's own full node.
+
+Using Electrum with Electrum Personal Server is probably the most
+resource-efficent way right now to use a hardware wallet connected to your
+own full node.
+
+For a longer explaination of this project and why it's important, see the
+[bitcointalk thread](https://bitcointalk.org/index.php?topic=2664747.msg27179198).
+
+See also the Electrum bitcoin wallet [website](https://electrum.org/).
+
+## How To Use
+
+This application requires python3 and a Bitcoin full node built with wallet
+capability.
+
+Download the latest release or clone the git repository. Enter the directory
+and rename the file `config.cfg_sample` to `config.cfg`, edit this file to
+configure your bitcoin node json-rpc authentication details. Next add your
+wallet addresses to the `[wallets]` section.
+
+Finally run `./server.py` on Linux or double-click `run-server.bat` on Windows.
+The first time the server is run it will import all configured addresses as
+watch-only into the Bitcoin node, and then exit giving you a chance to
+`-rescan` if your wallet contains historical transactions.
+
+Electrum wallet must be configured to connect to the server. SSL must be
+disabled which can be done either by `Tools` -> `Connection` -> Uncheck box
+`Use SSL`, or starting with the command line flag `--nossl`, depending on the
+version of Electrum. Tell Electrum to connect to the server in
+`Tools` -> `Server`, usually `localhost` if running on the same machine.
+
+Note that you can also try this with on [testnet bitcoin](https://en.bitcoin.it/wiki/Testnet).
+Electrum can be started in testnet mode with the command line flag `--testnet`.
+
+#### Exposure to the Internet
+
+You really don't want other people connecting to your server. They won't be
+able to synchronize their wallet, and they could potentially learn all your
+wallet addresses.
+
+By default the server will bind to and accept connections only from `localhost`
+so you should either run Electrum wallet from the same computer or use a SSH
+tunnel.
+
+## Project Readiness
+
+This project is in alpha stages as there are several essential missing
+features such as:
+
+* Merkle proofs are not handled, so every confirmed transaction is labelled
+ `Not Verified`.
+
+* The server does not support SSL so Electrum must be configured to disable it.
+
+* Deterministic wallets and master public keys are not supported. Addresses
+ must be imported individually.
+
+* Bech32 bitcoin addresses are not supported.
+
+* The Electrum server protocol has a caveat about multiple transactions included
+ in the same block. So there may be weird behaviour if that happens.
+
+* There's no way to turn off debug messages, so the console will be spammed by
+ them when used.
+
+When trying this, make sure you report any crashes, odd behaviour or times when
+Electrum disconnects (which indicates the server behaved unexpectedly).
+
+Someone should try running this on a Raspberry PI.
+
+## Contributing
+
+I welcome contributions. Please keep lines under 80 characters in length and
+ideally don't add any external dependencies to keep this as easy to install as
+possible.
+
+I can be contacted on freenode IRC on the `#bitcoin` and `#electrum` channels.
+
diff --git a/bitcoin/__init__.py b/bitcoin/__init__.py
@@ -0,0 +1,13 @@
+from bitcoin.py2specials import *
+from bitcoin.py3specials import *
+secp_present = False
+try:
+ import secp256k1
+ secp_present = True
+ from bitcoin.secp256k1_main import *
+ from bitcoin.secp256k1_transaction import *
+ from bitcoin.secp256k1_deterministic import *
+except ImportError as e:
+ from bitcoin.main import *
+ from bitcoin.deterministic import *
+ from bitcoin.transaction import *
diff --git a/bitcoin/deterministic.py b/bitcoin/deterministic.py
@@ -0,0 +1,128 @@
+from bitcoin.main import *
+import hmac
+import hashlib
+from binascii import hexlify
+
+# Below code ASSUMES binary inputs and compressed pubkeys
+MAINNET_PRIVATE = b'\x04\x88\xAD\xE4'
+MAINNET_PUBLIC = b'\x04\x88\xB2\x1E'
+TESTNET_PRIVATE = b'\x04\x35\x83\x94'
+TESTNET_PUBLIC = b'\x04\x35\x87\xCF'
+PRIVATE = [MAINNET_PRIVATE, TESTNET_PRIVATE]
+PUBLIC = [MAINNET_PUBLIC, TESTNET_PUBLIC]
+
+# BIP32 child key derivation
+
+def raw_bip32_ckd(rawtuple, i):
+ vbytes, depth, fingerprint, oldi, chaincode, key = rawtuple
+ i = int(i)
+
+ if vbytes in PRIVATE:
+ priv = key
+ pub = privtopub(key)
+ else:
+ pub = key
+
+ if i >= 2**31:
+ if vbytes in PUBLIC:
+ raise Exception("Can't do private derivation on public key!")
+ I = hmac.new(chaincode, b'\x00' + priv[:32] + encode(i, 256, 4),
+ hashlib.sha512).digest()
+ else:
+ I = hmac.new(chaincode, pub + encode(i, 256, 4),
+ hashlib.sha512).digest()
+
+ if vbytes in PRIVATE:
+ newkey = add_privkeys(I[:32] + B'\x01', priv)
+ fingerprint = bin_hash160(privtopub(key))[:4]
+ if vbytes in PUBLIC:
+ newkey = add_pubkeys(compress(privtopub(I[:32])), key)
+ fingerprint = bin_hash160(key)[:4]
+
+ return (vbytes, depth + 1, fingerprint, i, I[32:], newkey)
+
+
+def bip32_serialize(rawtuple):
+ vbytes, depth, fingerprint, i, chaincode, key = rawtuple
+ i = encode(i, 256, 4)
+ chaincode = encode(hash_to_int(chaincode), 256, 32)
+ keydata = b'\x00' + key[:-1] if vbytes in PRIVATE else key
+ bindata = vbytes + from_int_to_byte(
+ depth % 256) + fingerprint + i + chaincode + keydata
+ return changebase(bindata + bin_dbl_sha256(bindata)[:4], 256, 58)
+
+
+def bip32_deserialize(data):
+ dbin = changebase(data, 58, 256)
+ if bin_dbl_sha256(dbin[:-4])[:4] != dbin[-4:]:
+ raise Exception("Invalid checksum")
+ vbytes = dbin[0:4]
+ depth = from_byte_to_int(dbin[4])
+ fingerprint = dbin[5:9]
+ i = decode(dbin[9:13], 256)
+ chaincode = dbin[13:45]
+ key = dbin[46:78] + b'\x01' if vbytes in PRIVATE else dbin[45:78]
+ return (vbytes, depth, fingerprint, i, chaincode, key)
+
+
+def raw_bip32_privtopub(rawtuple):
+ vbytes, depth, fingerprint, i, chaincode, key = rawtuple
+ newvbytes = MAINNET_PUBLIC if vbytes == MAINNET_PRIVATE else TESTNET_PUBLIC
+ return (newvbytes, depth, fingerprint, i, chaincode, privtopub(key))
+
+
+def bip32_privtopub(data):
+ return bip32_serialize(raw_bip32_privtopub(bip32_deserialize(data)))
+
+
+def bip32_ckd(data, i):
+ return bip32_serialize(raw_bip32_ckd(bip32_deserialize(data), i))
+
+
+def bip32_master_key(seed, vbytes=MAINNET_PRIVATE):
+ I = hmac.new(
+ from_string_to_bytes("Bitcoin seed"), seed, hashlib.sha512).digest()
+ return bip32_serialize((vbytes, 0, b'\x00' * 4, 0, I[32:], I[:32] + b'\x01'
+ ))
+
+
+def bip32_bin_extract_key(data):
+ return bip32_deserialize(data)[-1]
+
+
+def bip32_extract_key(data):
+ return safe_hexlify(bip32_deserialize(data)[-1])
+
+# Exploits the same vulnerability as above in Electrum wallets
+# Takes a BIP32 pubkey and one of the child privkeys of its corresponding
+# privkey and returns the BIP32 privkey associated with that pubkey
+
+def raw_crack_bip32_privkey(parent_pub, priv):
+ vbytes, depth, fingerprint, i, chaincode, key = priv
+ pvbytes, pdepth, pfingerprint, pi, pchaincode, pkey = parent_pub
+ i = int(i)
+
+ if i >= 2**31:
+ raise Exception("Can't crack private derivation!")
+
+ I = hmac.new(pchaincode, pkey + encode(i, 256, 4), hashlib.sha512).digest()
+
+ pprivkey = subtract_privkeys(key, I[:32] + b'\x01')
+
+ newvbytes = MAINNET_PRIVATE if vbytes == MAINNET_PUBLIC else TESTNET_PRIVATE
+ return (newvbytes, pdepth, pfingerprint, pi, pchaincode, pprivkey)
+
+
+def crack_bip32_privkey(parent_pub, priv):
+ dsppub = bip32_deserialize(parent_pub)
+ dspriv = bip32_deserialize(priv)
+ return bip32_serialize(raw_crack_bip32_privkey(dsppub, dspriv))
+
+def bip32_descend(*args):
+ if len(args) == 2:
+ key, path = args
+ else:
+ key, path = args[0], map(int, args[1:])
+ for p in path:
+ key = bip32_ckd(key, p)
+ return bip32_extract_key(key)
diff --git a/bitcoin/main.py b/bitcoin/main.py
@@ -0,0 +1,504 @@
+#!/usr/bin/python
+from .py2specials import *
+from .py3specials import *
+import binascii
+import hashlib
+import re
+import sys
+import os
+import base64
+import time
+import random
+import hmac
+
+is_python2 = sys.version_info.major == 2
+
+# Elliptic curve parameters (secp256k1)
+
+P = 2**256 - 2**32 - 977
+N = 115792089237316195423570985008687907852837564279074904382605163141518161494337
+A = 0
+B = 7
+Gx = 55066263022277343669578718895168534326250603453777594175500187360389116729240
+Gy = 32670510020758816978083085130507043184471273380659243275938904335757337482424
+G = (Gx, Gy)
+
+# Extended Euclidean Algorithm
+def inv(a, n):
+ lm, hm = 1, 0
+ low, high = a % n, n
+ while low > 1:
+ r = high // low
+ nm, new = hm - lm * r, high - low * r
+ lm, low, hm, high = nm, new, lm, low
+ return lm % n
+
+# Elliptic curve Jordan form functions
+# P = (m, n, p, q) where m/n = x, p/q = y
+
+def isinf(p):
+ return p[0] == 0 and p[1] == 0
+
+
+def jordan_isinf(p):
+ return p[0][0] == 0 and p[1][0] == 0
+
+
+def mulcoords(c1, c2):
+ return (c1[0] * c2[0] % P, c1[1] * c2[1] % P)
+
+
+def mul_by_const(c, v):
+ return (c[0] * v % P, c[1])
+
+
+def addcoords(c1, c2):
+ return ((c1[0] * c2[1] + c2[0] * c1[1]) % P, c1[1] * c2[1] % P)
+
+
+def subcoords(c1, c2):
+ return ((c1[0] * c2[1] - c2[0] * c1[1]) % P, c1[1] * c2[1] % P)
+
+
+def invcoords(c):
+ return (c[1], c[0])
+
+
+def jordan_add(a, b):
+ if jordan_isinf(a):
+ return b
+ if jordan_isinf(b):
+ return a
+
+ if (a[0][0] * b[0][1] - b[0][0] * a[0][1]) % P == 0:
+ if (a[1][0] * b[1][1] - b[1][0] * a[1][1]) % P == 0:
+ return jordan_double(a)
+ else:
+ return ((0, 1), (0, 1))
+ xdiff = subcoords(b[0], a[0])
+ ydiff = subcoords(b[1], a[1])
+ m = mulcoords(ydiff, invcoords(xdiff))
+ x = subcoords(subcoords(mulcoords(m, m), a[0]), b[0])
+ y = subcoords(mulcoords(m, subcoords(a[0], x)), a[1])
+ return (x, y)
+
+
+def jordan_double(a):
+ if jordan_isinf(a):
+ return ((0, 1), (0, 1))
+ num = addcoords(mul_by_const(mulcoords(a[0], a[0]), 3), (A, 1))
+ den = mul_by_const(a[1], 2)
+ m = mulcoords(num, invcoords(den))
+ x = subcoords(mulcoords(m, m), mul_by_const(a[0], 2))
+ y = subcoords(mulcoords(m, subcoords(a[0], x)), a[1])
+ return (x, y)
+
+
+def jordan_multiply(a, n):
+ if jordan_isinf(a) or n == 0:
+ return ((0, 0), (0, 0))
+ if n == 1:
+ return a
+ if n < 0 or n >= N:
+ return jordan_multiply(a, n % N)
+ if (n % 2) == 0:
+ return jordan_double(jordan_multiply(a, n // 2))
+ if (n % 2) == 1:
+ return jordan_add(jordan_double(jordan_multiply(a, n // 2)), a)
+
+
+def to_jordan(p):
+ return ((p[0], 1), (p[1], 1))
+
+
+def from_jordan(p):
+ return (p[0][0] * inv(p[0][1], P) % P, p[1][0] * inv(p[1][1], P) % P)
+
+def fast_multiply(a, n):
+ return from_jordan(jordan_multiply(to_jordan(a), n))
+
+
+def fast_add(a, b):
+ return from_jordan(jordan_add(to_jordan(a), to_jordan(b)))
+
+# Functions for handling pubkey and privkey formats
+
+
+def get_pubkey_format(pub):
+ if is_python2:
+ two = '\x02'
+ three = '\x03'
+ four = '\x04'
+ else:
+ two = 2
+ three = 3
+ four = 4
+
+ if isinstance(pub, (tuple, list)): return 'decimal'
+ elif len(pub) == 65 and pub[0] == four: return 'bin'
+ elif len(pub) == 130 and pub[0:2] == '04': return 'hex'
+ elif len(pub) == 33 and pub[0] in [two, three]: return 'bin_compressed'
+ elif len(pub) == 66 and pub[0:2] in ['02', '03']: return 'hex_compressed'
+ elif len(pub) == 64: return 'bin_electrum'
+ elif len(pub) == 128: return 'hex_electrum'
+ else: raise Exception("Pubkey not in recognized format")
+
+
+def encode_pubkey(pub, formt):
+ if not isinstance(pub, (tuple, list)):
+ pub = decode_pubkey(pub)
+ if formt == 'decimal': return pub
+ elif formt == 'bin':
+ return b'\x04' + encode(pub[0], 256, 32) + encode(pub[1], 256, 32)
+ elif formt == 'bin_compressed':
+ return from_int_to_byte(2 + (pub[1] % 2)) + encode(pub[0], 256, 32)
+ elif formt == 'hex':
+ return '04' + encode(pub[0], 16, 64) + encode(pub[1], 16, 64)
+ elif formt == 'hex_compressed':
+ return '0' + str(2 + (pub[1] % 2)) + encode(pub[0], 16, 64)
+ elif formt == 'bin_electrum':
+ return encode(pub[0], 256, 32) + encode(pub[1], 256, 32)
+ elif formt == 'hex_electrum':
+ return encode(pub[0], 16, 64) + encode(pub[1], 16, 64)
+ else:
+ raise Exception("Invalid format!")
+
+
+def decode_pubkey(pub, formt=None):
+ if not formt: formt = get_pubkey_format(pub)
+ if formt == 'decimal': return pub
+ elif formt == 'bin':
+ return (decode(pub[1:33], 256), decode(pub[33:65], 256))
+ elif formt == 'bin_compressed':
+ x = decode(pub[1:33], 256)
+ beta = pow(int(x * x * x + A * x + B), int((P + 1) // 4), int(P))
+ y = (P - beta) if ((beta + from_byte_to_int(pub[0])) % 2) else beta
+ return (x, y)
+ elif formt == 'hex':
+ return (decode(pub[2:66], 16), decode(pub[66:130], 16))
+ elif formt == 'hex_compressed':
+ return decode_pubkey(safe_from_hex(pub), 'bin_compressed')
+ elif formt == 'bin_electrum':
+ return (decode(pub[:32], 256), decode(pub[32:64], 256))
+ elif formt == 'hex_electrum':
+ return (decode(pub[:64], 16), decode(pub[64:128], 16))
+ else:
+ raise Exception("Invalid format!")
+
+
+def get_privkey_format(priv):
+ if isinstance(priv, int_types): return 'decimal'
+ elif len(priv) == 32: return 'bin'
+ elif len(priv) == 33: return 'bin_compressed'
+ elif len(priv) == 64: return 'hex'
+ elif len(priv) == 66: return 'hex_compressed'
+ else:
+ bin_p = b58check_to_bin(priv)
+ if len(bin_p) == 32: return 'wif'
+ elif len(bin_p) == 33: return 'wif_compressed'
+ else: raise Exception("WIF does not represent privkey")
+
+
+def encode_privkey(priv, formt, vbyte=0):
+ if not isinstance(priv, int_types):
+ return encode_privkey(decode_privkey(priv), formt, vbyte)
+ if formt == 'decimal': return priv
+ elif formt == 'bin': return encode(priv, 256, 32)
+ elif formt == 'bin_compressed': return encode(priv, 256, 32) + b'\x01'
+ elif formt == 'hex': return encode(priv, 16, 64)
+ elif formt == 'hex_compressed': return encode(priv, 16, 64) + '01'
+ elif formt == 'wif':
+ return bin_to_b58check(encode(priv, 256, 32), 128 + int(vbyte))
+ elif formt == 'wif_compressed':
+ return bin_to_b58check(
+ encode(priv, 256, 32) + b'\x01', 128 + int(vbyte))
+ else:
+ raise Exception("Invalid format!")
+
+
+def decode_privkey(priv, formt=None):
+ if not formt: formt = get_privkey_format(priv)
+ if formt == 'decimal': return priv
+ elif formt == 'bin': return decode(priv, 256)
+ elif formt == 'bin_compressed': return decode(priv[:32], 256)
+ elif formt == 'hex': return decode(priv, 16)
+ elif formt == 'hex_compressed': return decode(priv[:64], 16)
+ elif formt == 'wif': return decode(b58check_to_bin(priv), 256)
+ elif formt == 'wif_compressed':
+ return decode(b58check_to_bin(priv)[:32], 256)
+ else:
+ raise Exception("WIF does not represent privkey")
+
+
+def add_pubkeys(p1, p2):
+ f1, f2 = get_pubkey_format(p1), get_pubkey_format(p2)
+ return encode_pubkey(
+ fast_add(
+ decode_pubkey(p1, f1), decode_pubkey(p2, f2)), f1)
+
+
+def add_privkeys(p1, p2):
+ f1, f2 = get_privkey_format(p1), get_privkey_format(p2)
+ return encode_privkey(
+ (decode_privkey(p1, f1) + decode_privkey(p2, f2)) % N, f1)
+
+
+def multiply(pubkey, privkey):
+ f1, f2 = get_pubkey_format(pubkey), get_privkey_format(privkey)
+ pubkey, privkey = decode_pubkey(pubkey, f1), decode_privkey(privkey, f2)
+ # http://safecurves.cr.yp.to/twist.html
+ if not isinf(pubkey) and (
+ pubkey[0]**3 + B - pubkey[1] * pubkey[1]) % P != 0:
+ raise Exception("Point not on curve")
+ return encode_pubkey(fast_multiply(pubkey, privkey), f1)
+
+
+def divide(pubkey, privkey):
+ factor = inv(decode_privkey(privkey), N)
+ return multiply(pubkey, factor)
+
+
+def compress(pubkey):
+ f = get_pubkey_format(pubkey)
+ if 'compressed' in f: return pubkey
+ elif f == 'bin':
+ return encode_pubkey(decode_pubkey(pubkey, f), 'bin_compressed')
+ elif f == 'hex' or f == 'decimal':
+ return encode_pubkey(decode_pubkey(pubkey, f), 'hex_compressed')
+
+
+def decompress(pubkey):
+ f = get_pubkey_format(pubkey)
+ if 'compressed' not in f: return pubkey
+ elif f == 'bin_compressed':
+ return encode_pubkey(decode_pubkey(pubkey, f), 'bin')
+ elif f == 'hex_compressed' or f == 'decimal':
+ return encode_pubkey(decode_pubkey(pubkey, f), 'hex')
+
+
+def privkey_to_pubkey(privkey):
+ f = get_privkey_format(privkey)
+ privkey = decode_privkey(privkey, f)
+ if privkey >= N:
+ raise Exception("Invalid privkey")
+ if f in ['bin', 'bin_compressed', 'hex', 'hex_compressed', 'decimal']:
+ return encode_pubkey(fast_multiply(G, privkey), f)
+ else:
+ return encode_pubkey(fast_multiply(G, privkey), f.replace('wif', 'hex'))
+
+
+privtopub = privkey_to_pubkey
+
+
+def privkey_to_address(priv, magicbyte=0):
+ return pubkey_to_address(privkey_to_pubkey(priv), magicbyte)
+
+
+privtoaddr = privkey_to_address
+
+
+def neg_pubkey(pubkey):
+ f = get_pubkey_format(pubkey)
+ pubkey = decode_pubkey(pubkey, f)
+ return encode_pubkey((pubkey[0], (P - pubkey[1]) % P), f)
+
+
+def neg_privkey(privkey):
+ f = get_privkey_format(privkey)
+ privkey = decode_privkey(privkey, f)
+ return encode_privkey((N - privkey) % N, f)
+
+
+def subtract_pubkeys(p1, p2):
+ f1, f2 = get_pubkey_format(p1), get_pubkey_format(p2)
+ k2 = decode_pubkey(p2, f2)
+ return encode_pubkey(
+ fast_add(
+ decode_pubkey(p1, f1), (k2[0], (P - k2[1]) % P)), f1)
+
+
+def subtract_privkeys(p1, p2):
+ f1, f2 = get_privkey_format(p1), get_privkey_format(p2)
+ k2 = decode_privkey(p2, f2)
+ return encode_privkey((decode_privkey(p1, f1) - k2) % N, f1)
+
+# Hashes
+
+
+def bin_hash160(string):
+ intermed = hashlib.sha256(string).digest()
+ digest = ''
+ digest = hashlib.new('ripemd160', intermed).digest()
+ return digest
+
+
+def hash160(string):
+ return safe_hexlify(bin_hash160(string))
+
+
+def bin_sha256(string):
+ binary_data = string if isinstance(string, bytes) else bytes(string,
+ 'utf-8')
+ return hashlib.sha256(binary_data).digest()
+
+
+def sha256(string):
+ return bytes_to_hex_string(bin_sha256(string))
+
+
+def bin_ripemd160(string):
+ digest = hashlib.new('ripemd160', string).digest()
+ return digest
+
+
+def ripemd160(string):
+ return safe_hexlify(bin_ripemd160(string))
+
+
+def bin_dbl_sha256(s):
+ bytes_to_hash = from_string_to_bytes(s)
+ return hashlib.sha256(hashlib.sha256(bytes_to_hash).digest()).digest()
+
+
+def dbl_sha256(string):
+ return safe_hexlify(bin_dbl_sha256(string))
+
+
+def bin_slowsha(string):
+ string = from_string_to_bytes(string)
+ orig_input = string
+ for i in range(100000):
+ string = hashlib.sha256(string + orig_input).digest()
+ return string
+
+
+def slowsha(string):
+ return safe_hexlify(bin_slowsha(string))
+
+
+def hash_to_int(x):
+ if len(x) in [40, 64]:
+ return decode(x, 16)
+ return decode(x, 256)
+
+
+def num_to_var_int(x):
+ x = int(x)
+ if x < 253: return from_int_to_byte(x)
+ elif x < 65536: return from_int_to_byte(253) + encode(x, 256, 2)[::-1]
+ elif x < 4294967296: return from_int_to_byte(254) + encode(x, 256, 4)[::-1]
+ else: return from_int_to_byte(255) + encode(x, 256, 8)[::-1]
+
+
+# WTF, Electrum?
+def electrum_sig_hash(message):
+ padded = b"\x18Bitcoin Signed Message:\n" + num_to_var_int(len(
+ message)) + from_string_to_bytes(message)
+ return bin_dbl_sha256(padded)
+
+# Encodings
+
+def b58check_to_bin(inp):
+ leadingzbytes = len(re.match('^1*', inp).group(0))
+ data = b'\x00' * leadingzbytes + changebase(inp, 58, 256)
+ assert bin_dbl_sha256(data[:-4])[:4] == data[-4:]
+ return data[1:-4]
+
+
+def get_version_byte(inp):
+ leadingzbytes = len(re.match('^1*', inp).group(0))
+ data = b'\x00' * leadingzbytes + changebase(inp, 58, 256)
+ assert bin_dbl_sha256(data[:-4])[:4] == data[-4:]
+ return ord(data[0])
+
+
+def hex_to_b58check(inp, magicbyte=0):
+ return bin_to_b58check(binascii.unhexlify(inp), magicbyte)
+
+
+def b58check_to_hex(inp):
+ return safe_hexlify(b58check_to_bin(inp))
+
+
+def pubkey_to_address(pubkey, magicbyte=0):
+ if isinstance(pubkey, (list, tuple)):
+ pubkey = encode_pubkey(pubkey, 'bin')
+ if len(pubkey) in [66, 130]:
+ return bin_to_b58check(
+ bin_hash160(binascii.unhexlify(pubkey)), magicbyte)
+ return bin_to_b58check(bin_hash160(pubkey), magicbyte)
+
+
+pubtoaddr = pubkey_to_address
+
+# EDCSA
+
+
+def encode_sig(v, r, s):
+ vb, rb, sb = from_int_to_byte(v), encode(r, 256), encode(s, 256)
+
+ result = base64.b64encode(vb + b'\x00' * (32 - len(rb)) + rb + b'\x00' * (
+ 32 - len(sb)) + sb)
+ return result if is_python2 else str(result, 'utf-8')
+
+
+def decode_sig(sig):
+ bytez = base64.b64decode(sig)
+ return from_byte_to_int(bytez[0]), decode(bytez[1:33], 256), decode(
+ bytez[33:], 256)
+
+# https://tools.ietf.org/html/rfc6979#section-3.2
+
+
+def deterministic_generate_k(msghash, priv):
+ v = b'\x01' * 32
+ k = b'\x00' * 32
+ priv = encode_privkey(priv, 'bin')
+ msghash = encode(hash_to_int(msghash), 256, 32)
+ k = hmac.new(k, v + b'\x00' + priv + msghash, hashlib.sha256).digest()
+ v = hmac.new(k, v, hashlib.sha256).digest()
+ k = hmac.new(k, v + b'\x01' + priv + msghash, hashlib.sha256).digest()
+ v = hmac.new(k, v, hashlib.sha256).digest()
+ return decode(hmac.new(k, v, hashlib.sha256).digest(), 256)
+
+
+def ecdsa_raw_sign(msghash, priv):
+
+ z = hash_to_int(msghash)
+ k = deterministic_generate_k(msghash, priv)
+
+ r, y = fast_multiply(G, k)
+ s = inv(k, N) * (z + r * decode_privkey(priv)) % N
+
+ return 27 + (y % 2), r, s
+
+
+def ecdsa_sign(msg, priv):
+ return encode_sig(*ecdsa_raw_sign(electrum_sig_hash(msg), priv))
+
+def ecdsa_raw_verify(msghash, vrs, pub):
+ v, r, s = vrs
+
+ w = inv(s, N)
+ z = hash_to_int(msghash)
+
+ u1, u2 = z * w % N, r * w % N
+ x, y = fast_add(fast_multiply(G, u1), fast_multiply(decode_pubkey(pub), u2))
+
+ return r == x
+
+def ecdsa_verify(msg, sig, pub):
+ return ecdsa_raw_verify(electrum_sig_hash(msg), decode_sig(sig), pub)
+
+def estimate_tx_size(ins, outs, txtype='p2pkh'):
+ '''Estimate transaction size.
+ Assuming p2pkh:
+ out: 8+1+3+2+20=34, in: 1+32+4+1+1+~73+1+1+33=147,
+ ver:4,seq:4, +2 (len in,out)
+ total ~= 34*len_out + 147*len_in + 10 (sig sizes vary slightly)
+ '''
+ if txtype=='p2pkh':
+ return 10 + ins*147 +34*outs
+ else:
+ raise NotImplementedError("Non p2pkh transaction size estimation not"+
+ "yet implemented")
diff --git a/bitcoin/py2specials.py b/bitcoin/py2specials.py
@@ -0,0 +1,85 @@
+import sys, re
+import binascii
+import os
+import hashlib
+
+if sys.version_info.major == 2:
+ string_types = (str, unicode)
+ string_or_bytes_types = string_types
+ int_types = (int, float, long)
+
+ # Base switching
+ code_strings = {
+ 2: '01',
+ 10: '0123456789',
+ 16: '0123456789abcdef',
+ 32: 'abcdefghijklmnopqrstuvwxyz234567',
+ 58: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz',
+ 256: ''.join([chr(x) for x in range(256)])
+ }
+
+ def bin_dbl_sha256(s):
+ bytes_to_hash = from_string_to_bytes(s)
+ return hashlib.sha256(hashlib.sha256(bytes_to_hash).digest()).digest()
+
+ def lpad(msg, symbol, length):
+ if len(msg) >= length:
+ return msg
+ return symbol * (length - len(msg)) + msg
+
+ def get_code_string(base):
+ if base in code_strings:
+ return code_strings[base]
+ else:
+ raise ValueError("Invalid base!")
+
+ def changebase(string, frm, to, minlen=0):
+ if frm == to:
+ return lpad(string, get_code_string(frm)[0], minlen)
+ return encode(decode(string, frm), to, minlen)
+
+ def bin_to_b58check(inp, magicbyte=0):
+ inp_fmtd = chr(int(magicbyte)) + inp
+ leadingzbytes = len(re.match('^\x00*', inp_fmtd).group(0))
+ checksum = bin_dbl_sha256(inp_fmtd)[:4]
+ return '1' * leadingzbytes + changebase(inp_fmtd + checksum, 256, 58)
+
+ def bytes_to_hex_string(b):
+ return b.encode('hex')
+
+ def safe_from_hex(s):
+ return s.decode('hex')
+
+ def from_int_to_byte(a):
+ return chr(a)
+
+ def from_byte_to_int(a):
+ return ord(a)
+
+ def from_string_to_bytes(a):
+ return a
+
+ def safe_hexlify(a):
+ return binascii.hexlify(a)
+
+ def encode(val, base, minlen=0):
+ base, minlen = int(base), int(minlen)
+ code_string = get_code_string(base)
+ result = ""
+ while val > 0:
+ result = code_string[val % base] + result
+ val //= base
+ return code_string[0] * max(minlen - len(result), 0) + result
+
+ def decode(string, base):
+ base = int(base)
+ code_string = get_code_string(base)
+ result = 0
+ if base == 16:
+ string = string.lower()
+ while len(string) > 0:
+ result *= base
+ result += code_string.find(string[0])
+ string = string[1:]
+ return result
+
diff --git a/bitcoin/py3specials.py b/bitcoin/py3specials.py
@@ -0,0 +1,115 @@
+import sys, os
+import binascii
+import hashlib
+
+if sys.version_info.major == 3:
+ string_types = (str)
+ string_or_bytes_types = (str, bytes)
+ int_types = (int, float)
+ # Base switching
+ code_strings = {
+ 2: '01',
+ 10: '0123456789',
+ 16: '0123456789abcdef',
+ 32: 'abcdefghijklmnopqrstuvwxyz234567',
+ 58: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz',
+ 256: ''.join([chr(x) for x in range(256)])
+ }
+
+ def bin_dbl_sha256(s):
+ bytes_to_hash = from_string_to_bytes(s)
+ return hashlib.sha256(hashlib.sha256(bytes_to_hash).digest()).digest()
+
+ def lpad(msg, symbol, length):
+ if len(msg) >= length:
+ return msg
+ return symbol * (length - len(msg)) + msg
+
+ def get_code_string(base):
+ if base in code_strings:
+ return code_strings[base]
+ else:
+ raise ValueError("Invalid base!")
+
+ def changebase(string, frm, to, minlen=0):
+ if frm == to:
+ return lpad(string, get_code_string(frm)[0], minlen)
+ return encode(decode(string, frm), to, minlen)
+
+ def bin_to_b58check(inp, magicbyte=0):
+ inp_fmtd = from_int_to_byte(int(magicbyte)) + inp
+
+ leadingzbytes = 0
+ for x in inp_fmtd:
+ if x != 0:
+ break
+ leadingzbytes += 1
+
+ checksum = bin_dbl_sha256(inp_fmtd)[:4]
+ return '1' * leadingzbytes + changebase(inp_fmtd + checksum, 256, 58)
+
+ def bytes_to_hex_string(b):
+ if isinstance(b, str):
+ return b
+
+ return ''.join('{:02x}'.format(y) for y in b)
+
+ def safe_from_hex(s):
+ return bytes.fromhex(s)
+
+ def from_int_to_byte(a):
+ return bytes([a])
+
+ def from_byte_to_int(a):
+ return a
+
+ def from_string_to_bytes(a):
+ return a if isinstance(a, bytes) else bytes(a, 'utf-8')
+
+ def safe_hexlify(a):
+ return str(binascii.hexlify(a), 'utf-8')
+
+ def encode(val, base, minlen=0):
+ base, minlen = int(base), int(minlen)
+ code_string = get_code_string(base)
+ result_bytes = bytes()
+ while val > 0:
+ curcode = code_string[val % base]
+ result_bytes = bytes([ord(curcode)]) + result_bytes
+ val //= base
+
+ pad_size = minlen - len(result_bytes)
+
+ padding_element = b'\x00' if base == 256 else b'1' \
+ if base == 58 else b'0'
+ if (pad_size > 0):
+ result_bytes = padding_element * pad_size + result_bytes
+
+ result_string = ''.join([chr(y) for y in result_bytes])
+ result = result_bytes if base == 256 else result_string
+
+ return result
+
+ def decode(string, base):
+ if base == 256 and isinstance(string, str):
+ string = bytes(bytearray.fromhex(string))
+ base = int(base)
+ code_string = get_code_string(base)
+ result = 0
+ if base == 256:
+
+ def extract(d, cs):
+ return d
+ else:
+
+ def extract(d, cs):
+ return cs.find(d if isinstance(d, str) else chr(d))
+
+ if base == 16:
+ string = string.lower()
+ while len(string) > 0:
+ result *= base
+ result += extract(string[0], code_string)
+ string = string[1:]
+ return result
+
diff --git a/bitcoin/secp256k1_deterministic.py b/bitcoin/secp256k1_deterministic.py
@@ -0,0 +1,92 @@
+from bitcoin.secp256k1_main import *
+import hmac
+import hashlib
+from binascii import hexlify
+
+# Below code ASSUMES binary inputs and compressed pubkeys
+MAINNET_PRIVATE = b'\x04\x88\xAD\xE4'
+MAINNET_PUBLIC = b'\x04\x88\xB2\x1E'
+TESTNET_PRIVATE = b'\x04\x35\x83\x94'
+TESTNET_PUBLIC = b'\x04\x35\x87\xCF'
+PRIVATE = [MAINNET_PRIVATE, TESTNET_PRIVATE]
+PUBLIC = [MAINNET_PUBLIC, TESTNET_PUBLIC]
+
+# BIP32 child key derivation
+
+def raw_bip32_ckd(rawtuple, i):
+ vbytes, depth, fingerprint, oldi, chaincode, key = rawtuple
+ i = int(i)
+
+ if vbytes in PRIVATE:
+ priv = key
+ pub = privtopub(key, False)
+ else:
+ pub = key
+
+ if i >= 2**31:
+ if vbytes in PUBLIC:
+ raise Exception("Can't do private derivation on public key!")
+ I = hmac.new(chaincode, b'\x00' + priv[:32] + encode(i, 256, 4),
+ hashlib.sha512).digest()
+ else:
+ I = hmac.new(chaincode, pub + encode(i, 256, 4),
+ hashlib.sha512).digest()
+
+ if vbytes in PRIVATE:
+ newkey = add_privkeys(I[:32] + B'\x01', priv, False)
+ fingerprint = bin_hash160(privtopub(key, False))[:4]
+ if vbytes in PUBLIC:
+ newkey = add_pubkeys([privtopub(I[:32] + '\x01', False), key], False)
+ fingerprint = bin_hash160(key)[:4]
+
+ return (vbytes, depth + 1, fingerprint, i, I[32:], newkey)
+
+def bip32_serialize(rawtuple):
+ vbytes, depth, fingerprint, i, chaincode, key = rawtuple
+ i = encode(i, 256, 4)
+ chaincode = encode(hash_to_int(chaincode), 256, 32)
+ keydata = b'\x00' + key[:-1] if vbytes in PRIVATE else key
+ bindata = vbytes + from_int_to_byte(
+ depth % 256) + fingerprint + i + chaincode + keydata
+ return changebase(bindata + bin_dbl_sha256(bindata)[:4], 256, 58)
+
+def bip32_deserialize(data):
+ dbin = changebase(data, 58, 256)
+ if bin_dbl_sha256(dbin[:-4])[:4] != dbin[-4:]:
+ raise Exception("Invalid checksum")
+ vbytes = dbin[0:4]
+ depth = from_byte_to_int(dbin[4])
+ fingerprint = dbin[5:9]
+ i = decode(dbin[9:13], 256)
+ chaincode = dbin[13:45]
+ key = dbin[46:78] + b'\x01' if vbytes in PRIVATE else dbin[45:78]
+ return (vbytes, depth, fingerprint, i, chaincode, key)
+
+def raw_bip32_privtopub(rawtuple):
+ vbytes, depth, fingerprint, i, chaincode, key = rawtuple
+ newvbytes = MAINNET_PUBLIC if vbytes == MAINNET_PRIVATE else TESTNET_PUBLIC
+ return (newvbytes, depth, fingerprint, i, chaincode, privtopub(key, False))
+
+def bip32_privtopub(data):
+ return bip32_serialize(raw_bip32_privtopub(bip32_deserialize(data)))
+
+def bip32_ckd(data, i):
+ return bip32_serialize(raw_bip32_ckd(bip32_deserialize(data), i))
+
+def bip32_master_key(seed, vbytes=MAINNET_PRIVATE):
+ I = hmac.new(
+ from_string_to_bytes("Bitcoin seed"), seed, hashlib.sha512).digest()
+ return bip32_serialize((vbytes, 0, b'\x00' * 4, 0, I[32:], I[:32] + b'\x01'
+ ))
+
+def bip32_extract_key(data):
+ return safe_hexlify(bip32_deserialize(data)[-1])
+
+def bip32_descend(*args):
+ if len(args) == 2:
+ key, path = args
+ else:
+ key, path = args[0], map(int, args[1:])
+ for p in path:
+ key = bip32_ckd(key, p)
+ return bip32_extract_key(key)
diff --git a/bitcoin/secp256k1_main.py b/bitcoin/secp256k1_main.py
@@ -0,0 +1,375 @@
+#!/usr/bin/python
+from .py2specials import *
+from .py3specials import *
+import binascii
+import hashlib
+import re
+import sys
+import os
+import base64
+import time
+import random
+import hmac
+import secp256k1
+
+ctx = secp256k1.lib.secp256k1_context_create(secp256k1.ALL_FLAGS)
+
+def privkey_to_address(priv, from_hex=True, magicbyte=0):
+ return pubkey_to_address(privkey_to_pubkey(priv, from_hex), magicbyte)
+
+privtoaddr = privkey_to_address
+
+# Hashes
+def bin_hash160(string):
+ intermed = hashlib.sha256(string).digest()
+ return hashlib.new('ripemd160', intermed).digest()
+
+def hash160(string):
+ return safe_hexlify(bin_hash160(string))
+
+def bin_sha256(string):
+ binary_data = string if isinstance(string, bytes) else bytes(string,
+ 'utf-8')
+ return hashlib.sha256(binary_data).digest()
+
+def sha256(string):
+ return bytes_to_hex_string(bin_sha256(string))
+
+def bin_dbl_sha256(s):
+ bytes_to_hash = from_string_to_bytes(s)
+ return hashlib.sha256(hashlib.sha256(bytes_to_hash).digest()).digest()
+
+def dbl_sha256(string):
+ return safe_hexlify(bin_dbl_sha256(string))
+
+def hash_to_int(x):
+ if len(x) in [40, 64]:
+ return decode(x, 16)
+ return decode(x, 256)
+
+def num_to_var_int(x):
+ x = int(x)
+ if x < 253: return from_int_to_byte(x)
+ elif x < 65536: return from_int_to_byte(253) + encode(x, 256, 2)[::-1]
+ elif x < 4294967296: return from_int_to_byte(254) + encode(x, 256, 4)[::-1]
+ else: return from_int_to_byte(255) + encode(x, 256, 8)[::-1]
+
+# WTF, Electrum?
+def electrum_sig_hash(message):
+ padded = b"\x18Bitcoin Signed Message:\n" + num_to_var_int(len(
+ message)) + from_string_to_bytes(message)
+ return bin_dbl_sha256(padded)
+
+# Encodings
+def b58check_to_bin(inp):
+ leadingzbytes = len(re.match('^1*', inp).group(0))
+ data = b'\x00' * leadingzbytes + changebase(inp, 58, 256)
+ assert bin_dbl_sha256(data[:-4])[:4] == data[-4:]
+ return data[1:-4]
+
+def get_version_byte(inp):
+ leadingzbytes = len(re.match('^1*', inp).group(0))
+ data = b'\x00' * leadingzbytes + changebase(inp, 58, 256)
+ assert bin_dbl_sha256(data[:-4])[:4] == data[-4:]
+ return ord(data[0])
+
+def hex_to_b58check(inp, magicbyte=0):
+ return bin_to_b58check(binascii.unhexlify(inp), magicbyte)
+
+def b58check_to_hex(inp):
+ return safe_hexlify(b58check_to_bin(inp))
+
+def pubkey_to_address(pubkey, magicbyte=0):
+ if len(pubkey) in [66, 130]:
+ return bin_to_b58check(
+ bin_hash160(binascii.unhexlify(pubkey)), magicbyte)
+ return bin_to_b58check(bin_hash160(pubkey), magicbyte)
+
+pubtoaddr = pubkey_to_address
+
+def wif_compressed_privkey(priv, vbyte=0):
+ """Convert privkey in hex compressed to WIF compressed
+ """
+ if len(priv) != 66:
+ raise Exception("Wrong length of compressed private key")
+ if priv[-2:] != '01':
+ raise Exception("Private key has wrong compression byte")
+ return bin_to_b58check(binascii.unhexlify(priv), 128 + int(vbyte))
+
+
+def from_wif_privkey(wif_priv, compressed=True, vbyte=0):
+ """Convert WIF compressed privkey to hex compressed.
+ Caller specifies the network version byte (0 for mainnet, 0x6f
+ for testnet) that the key should correspond to; if there is
+ a mismatch an error is thrown. WIF encoding uses 128+ this number.
+ """
+ bin_key = b58check_to_bin(wif_priv)
+ claimed_version_byte = get_version_byte(wif_priv)
+ if not 128+vbyte == claimed_version_byte:
+ raise Exception(
+ "WIF key version byte is wrong network (mainnet/testnet?)")
+ if compressed and not len(bin_key) == 33:
+ raise Exception("Compressed private key is not 33 bytes")
+ if compressed and not bin_key[-1] == '\x01':
+ raise Exception("Private key has incorrect compression byte")
+ return safe_hexlify(bin_key)
+
+def ecdsa_sign(msg, priv, usehex=True):
+ #Compatibility issue: old bots will be confused
+ #by different msg hashing algo; need to keep electrum_sig_hash, temporarily.
+ hashed_msg = electrum_sig_hash(msg)
+ if usehex:
+ #arguments to raw sign must be consistently hex or bin
+ hashed_msg = binascii.hexlify(hashed_msg)
+ dersig = ecdsa_raw_sign(hashed_msg, priv, usehex, rawmsg=True)
+ #see comments to legacy* functions
+ #also, note those functions only handles binary, not hex
+ if usehex:
+ dersig = binascii.unhexlify(dersig)
+ sig = legacy_ecdsa_sign_convert(dersig)
+ return base64.b64encode(sig)
+
+def ecdsa_verify(msg, sig, pub, usehex=True):
+ #See note to ecdsa_sign
+ hashed_msg = electrum_sig_hash(msg)
+ sig = base64.b64decode(sig)
+ #see comments to legacy* functions
+ sig = legacy_ecdsa_verify_convert(sig)
+ if usehex:
+ #arguments to raw_verify must be consistently hex or bin
+ hashed_msg = binascii.hexlify(hashed_msg)
+ sig = binascii.hexlify(sig)
+ return ecdsa_raw_verify(hashed_msg, pub, sig, usehex, rawmsg=True)
+
+#A sadly necessary hack until all joinmarket bots are running secp256k1 code.
+#pybitcointools *message* signatures (not transaction signatures) used an old signature
+#format, basically: [27+y%2] || 32 byte r || 32 byte s,
+#instead of DER. These two functions translate the new version into the old so that
+#counterparty bots can verify successfully.
+def legacy_ecdsa_sign_convert(dersig):
+ #note there is no sanity checking of DER format (e.g. leading length byte)
+ dersig = dersig[2:] #e.g. 3045
+ rlen = ord(dersig[1]) #ignore leading 02
+ #length of r and s: ALWAYS <=33, USUALLY >=32 but can be shorter
+ if rlen > 33:
+ raise Exception("Incorrectly formatted DER sig:" + binascii.hexlify(
+ dersig))
+ if dersig[2] == '\x00':
+ r = dersig[3:2 + rlen]
+ ssig = dersig[2 + rlen:]
+ else:
+ r = dersig[2:2 + rlen]
+ ssig = dersig[2 + rlen:]
+
+ slen = ord(ssig[1]) #ignore leading 02
+ if slen > 33:
+ raise Exception("Incorrectly formatted DER sig:" + binascii.hexlify(
+ dersig))
+ if len(ssig) != 2 + slen:
+ raise Exception("Incorrectly formatted DER sig:" + binascii.hexlify(
+ dersig))
+ if ssig[2] == '\x00':
+ s = ssig[3:2 + slen]
+ else:
+ s = ssig[2:2 + slen]
+
+ #the legacy version requires padding of r and s to 32 bytes with leading zeros
+ r = '\x00' * (32 - len(r)) + r
+ s = '\x00' * (32 - len(s)) + s
+
+ #note: in the original pybitcointools implementation,
+ #verification ignored the leading byte (it's only needed for pubkey recovery)
+ #so we just ignore parity here.
+ return chr(27) + r + s
+
+def legacy_ecdsa_verify_convert(sig):
+ sig = sig[1:] #ignore parity byte
+ r, s = sig[:32], sig[32:]
+ if not len(s) == 32:
+ #signature is invalid.
+ return False
+ #legacy code can produce high S. Need to reintroduce N ::cry::
+ N = 115792089237316195423570985008687907852837564279074904382605163141518161494337
+ s_int = decode(s, 256)
+ # note // is integer division operator in both 2.7 and 3
+ s_int = N - s_int if s_int > N // 2 else s_int #enforce low S.
+
+ #on re-encoding, don't use the minlen parameter, because
+ #DER does not used fixed (32 byte) length values, so we
+ #don't prepend zero bytes to shorter numbers.
+ s = encode(s_int, 256)
+
+ #as above, remove any front zero padding from r.
+ r = encode(decode(r, 256), 256)
+
+ #canonicalize r and s
+ r, s = ['\x00' + x if ord(x[0]) > 127 else x for x in [r, s]]
+ rlen = chr(len(r))
+ slen = chr(len(s))
+ total_len = 2 + len(r) + 2 + len(s)
+ return '\x30' + chr(total_len) + '\x02' + rlen + r + '\x02' + slen + s
+
+#Use secp256k1 to handle all EC and ECDSA operations.
+#Data types: only hex and binary.
+#Compressed and uncompressed private and public keys.
+def hexbin(func):
+ '''To enable each function to 'speak' either hex or binary,
+ requires that the decorated function's final positional argument
+ is a boolean flag, True for hex and False for binary.
+ '''
+
+ def func_wrapper(*args, **kwargs):
+ if args[-1]:
+ newargs = []
+ for arg in args[:-1]:
+ if isinstance(arg, (list, tuple)):
+ newargs += [[x.decode('hex') for x in arg]]
+ else:
+ newargs += [arg.decode('hex')]
+ newargs += [False]
+ returnval = func(*newargs, **kwargs)
+ if isinstance(returnval, bool):
+ return returnval
+ else:
+ return binascii.hexlify(returnval)
+ else:
+ return func(*args, **kwargs)
+
+ return func_wrapper
+
+def read_privkey(priv):
+ if len(priv) == 33:
+ if priv[-1] == '\x01':
+ compressed = True
+ else:
+ raise Exception("Invalid private key")
+ elif len(priv) == 32:
+ compressed = False
+ else:
+ raise Exception("Invalid private key")
+ return (compressed, priv[:32])
+
+@hexbin
+def privkey_to_pubkey_inner(priv, usehex):
+ '''Take 32/33 byte raw private key as input.
+ If 32 bytes, return compressed (33 byte) raw public key.
+ If 33 bytes, read the final byte as compression flag,
+ and return compressed/uncompressed public key as appropriate.'''
+ compressed, priv = read_privkey(priv)
+ #secp256k1 checks for validity of key value.
+ newpriv = secp256k1.PrivateKey(privkey=priv, ctx=ctx)
+ return newpriv.pubkey.serialize(compressed=compressed)
+
+def privkey_to_pubkey(priv, usehex=True):
+ '''To avoid changing the interface from the legacy system,
+ allow an *optional* hex argument here (called differently from
+ maker/taker code to how it's called in bip32 code), then
+ pass to the standard hexbin decorator under the hood.
+ '''
+ return privkey_to_pubkey_inner(priv, usehex)
+
+privtopub = privkey_to_pubkey
+
+@hexbin
+def multiply(s, pub, usehex, rawpub=True):
+ '''Input binary compressed pubkey P(33 bytes)
+ and scalar s(32 bytes), return s*P.
+ The return value is a binary compressed public key.
+ Note that the called function does the type checking
+ of the scalar s.
+ ('raw' options passed in)
+ '''
+ newpub = secp256k1.PublicKey(pub, raw=rawpub, ctx=ctx)
+ res = newpub.tweak_mul(s)
+ return res.serialize()
+
+@hexbin
+def add_pubkeys(pubkeys, usehex):
+ '''Input a list of binary compressed pubkeys
+ and return their sum as a binary compressed pubkey.'''
+ r = secp256k1.PublicKey(ctx=ctx) #dummy holding object
+ pubkey_list = [secp256k1.PublicKey(x,
+ raw=True,
+ ctx=ctx).public_key for x in pubkeys]
+ r.combine(pubkey_list)
+ return r.serialize()
+
+@hexbin
+def add_privkeys(priv1, priv2, usehex):
+ '''Add privkey 1 to privkey 2.
+ Input keys must be in binary either compressed or not.
+ Returned key will have the same compression state.
+ Error if compression state of both input keys is not the same.'''
+ y, z = [read_privkey(x) for x in [priv1, priv2]]
+ if y[0] != z[0]:
+ raise Exception("cannot add privkeys, mixed compression formats")
+ else:
+ compressed = y[0]
+ newpriv1, newpriv2 = (y[1], z[1])
+ p1 = secp256k1.PrivateKey(newpriv1, raw=True, ctx=ctx)
+ res = p1.tweak_add(newpriv2)
+ if compressed:
+ res += '\x01'
+ return res
+
+@hexbin
+def ecdsa_raw_sign(msg,
+ priv,
+ usehex,
+ rawpriv=True,
+ rawmsg=False,
+ usenonce=None):
+ '''Take the binary message msg and sign it with the private key
+ priv.
+ By default priv is just a 32 byte string, if rawpriv is false
+ it is assumed to be DER encoded.
+ If rawmsg is True, no sha256 hash is applied to msg before signing.
+ In this case, msg must be a precalculated hash (256 bit).
+ If rawmsg is False, the secp256k1 lib will hash the message as part
+ of the ECDSA-SHA256 signing algo.
+ If usenonce is not None, its value is passed to the secp256k1 library
+ sign() function as the ndata value, which is then used in conjunction
+ with a custom nonce generating function, such that the nonce used in the ECDSA
+ sign algorithm is exactly that value (ndata there, usenonce here). 32 bytes.
+ Return value: the calculated signature.'''
+ if rawmsg and len(msg) != 32:
+ raise Exception("Invalid hash input to ECDSA raw sign.")
+ if rawpriv:
+ compressed, p = read_privkey(priv)
+ newpriv = secp256k1.PrivateKey(p, raw=True, ctx=ctx)
+ else:
+ newpriv = secp256k1.PrivateKey(priv, raw=False, ctx=ctx)
+ if usenonce and len(usenonce) != 32:
+ raise ValueError("Invalid nonce passed to ecdsa_sign: " + str(usenonce))
+
+ sig = newpriv.ecdsa_sign(msg, raw=rawmsg)
+ return newpriv.ecdsa_serialize(sig)
+
+@hexbin
+def ecdsa_raw_verify(msg, pub, sig, usehex, rawmsg=False):
+ '''Take the binary message msg and binary signature sig,
+ and verify it against the pubkey pub.
+ If rawmsg is True, no sha256 hash is applied to msg before verifying.
+ In this case, msg must be a precalculated hash (256 bit).
+ If rawmsg is False, the secp256k1 lib will hash the message as part
+ of the ECDSA-SHA256 verification algo.
+ Return value: True if the signature is valid for this pubkey, False
+ otherwise. '''
+ if rawmsg and len(msg) != 32:
+ raise Exception("Invalid hash input to ECDSA raw sign.")
+ newpub = secp256k1.PublicKey(pubkey=pub, raw=True, ctx=ctx)
+ sigobj = newpub.ecdsa_deserialize(sig)
+ return newpub.ecdsa_verify(msg, sigobj, raw=rawmsg)
+
+def estimate_tx_size(ins, outs, txtype='p2pkh'):
+ '''Estimate transaction size.
+ Assuming p2pkh:
+ out: 8+1+3+2+20=34, in: 1+32+4+1+1+~73+1+1+33=147,
+ ver:4,seq:4, +2 (len in,out)
+ total ~= 34*len_out + 147*len_in + 10 (sig sizes vary slightly)
+ '''
+ if txtype == 'p2pkh':
+ return 10 + ins * 147 + 34 * outs
+ else:
+ raise NotImplementedError("Non p2pkh transaction size estimation not" +
+ "yet implemented")
diff --git a/bitcoin/secp256k1_transaction.py b/bitcoin/secp256k1_transaction.py
@@ -0,0 +1,452 @@
+#!/usr/bin/python
+import binascii, re, json, copy, sys
+from bitcoin.secp256k1_main import *
+from _functools import reduce
+import os
+
+is_python2 = sys.version_info.major == 2
+
+### Hex to bin converter and vice versa for objects
+def json_is_base(obj, base):
+ if not is_python2 and isinstance(obj, bytes):
+ return False
+
+ alpha = get_code_string(base)
+ if isinstance(obj, string_types):
+ for i in range(len(obj)):
+ if alpha.find(obj[i]) == -1:
+ return False
+ return True
+ elif isinstance(obj, int_types) or obj is None:
+ return True
+ elif isinstance(obj, list):
+ for i in range(len(obj)):
+ if not json_is_base(obj[i], base):
+ return False
+ return True
+ else:
+ for x in obj:
+ if not json_is_base(obj[x], base):
+ return False
+ return True
+
+
+def json_changebase(obj, changer):
+ if isinstance(obj, string_or_bytes_types):
+ return changer(obj)
+ elif isinstance(obj, int_types) or obj is None:
+ return obj
+ elif isinstance(obj, list):
+ return [json_changebase(x, changer) for x in obj]
+ return dict((x, json_changebase(obj[x], changer)) for x in obj)
+
+# Transaction serialization and deserialization
+
+
+def deserialize(tx):
+ if isinstance(tx, str) and re.match('^[0-9a-fA-F]*$', tx):
+ #tx = bytes(bytearray.fromhex(tx))
+ return json_changebase(
+ deserialize(binascii.unhexlify(tx)), lambda x: safe_hexlify(x))
+ # http://stackoverflow.com/questions/4851463/python-closure-write-to-variable-in-parent-scope
+ # Python's scoping rules are demented, requiring me to make pos an object
+ # so that it is call-by-reference
+ pos = [0]
+
+ def read_as_int(bytez):
+ pos[0] += bytez
+ return decode(tx[pos[0] - bytez:pos[0]][::-1], 256)
+
+ def read_var_int():
+ pos[0] += 1
+
+ val = from_byte_to_int(tx[pos[0] - 1])
+ if val < 253:
+ return val
+ return read_as_int(pow(2, val - 252))
+
+ def read_bytes(bytez):
+ pos[0] += bytez
+ return tx[pos[0] - bytez:pos[0]]
+
+ def read_var_string():
+ size = read_var_int()
+ return read_bytes(size)
+
+ obj = {"ins": [], "outs": []}
+ obj["version"] = read_as_int(4)
+ ins = read_var_int()
+ for i in range(ins):
+ obj["ins"].append({
+ "outpoint": {
+ "hash": read_bytes(32)[::-1],
+ "index": read_as_int(4)
+ },
+ "script": read_var_string(),
+ "sequence": read_as_int(4)
+ })
+ outs = read_var_int()
+ for i in range(outs):
+ obj["outs"].append({
+ "value": read_as_int(8),
+ "script": read_var_string()
+ })
+ obj["locktime"] = read_as_int(4)
+ return obj
+
+
+def serialize(txobj):
+ #if isinstance(txobj, bytes):
+ # txobj = bytes_to_hex_string(txobj)
+ o = []
+ if json_is_base(txobj, 16):
+ json_changedbase = json_changebase(txobj,
+ lambda x: binascii.unhexlify(x))
+ hexlified = safe_hexlify(serialize(json_changedbase))
+ return hexlified
+ o.append(encode(txobj["version"], 256, 4)[::-1])
+ o.append(num_to_var_int(len(txobj["ins"])))
+ for inp in txobj["ins"]:
+ o.append(inp["outpoint"]["hash"][::-1])
+ o.append(encode(inp["outpoint"]["index"], 256, 4)[::-1])
+ o.append(num_to_var_int(len(inp["script"])) + (inp["script"] if inp[
+ "script"] or is_python2 else bytes()))
+ o.append(encode(inp["sequence"], 256, 4)[::-1])
+ o.append(num_to_var_int(len(txobj["outs"])))
+ for out in txobj["outs"]:
+ o.append(encode(out["value"], 256, 8)[::-1])
+ o.append(num_to_var_int(len(out["script"])) + out["script"])
+ o.append(encode(txobj["locktime"], 256, 4)[::-1])
+
+ return ''.join(o) if is_python2 else reduce(lambda x, y: x + y, o, bytes())
+
+# Hashing transactions for signing
+
+SIGHASH_ALL = 1
+SIGHASH_NONE = 2
+SIGHASH_SINGLE = 3
+SIGHASH_ANYONECANPAY = 0x80
+
+def signature_form(tx, i, script, hashcode=SIGHASH_ALL):
+ i, hashcode = int(i), int(hashcode)
+ if isinstance(tx, string_or_bytes_types):
+ return serialize(signature_form(deserialize(tx), i, script, hashcode))
+ newtx = copy.deepcopy(tx)
+ for inp in newtx["ins"]:
+ inp["script"] = ""
+ newtx["ins"][i]["script"] = script
+ if hashcode & 0x1f == SIGHASH_NONE:
+ newtx["outs"] = []
+ for j, inp in enumerate(newtx["ins"]):
+ if j != i:
+ inp["sequence"] = 0
+ elif hashcode & 0x1f == SIGHASH_SINGLE:
+ if len(newtx["ins"]) > len(newtx["outs"]):
+ raise Exception(
+ "Transactions with sighash single should have len in <= len out")
+ newtx["outs"] = newtx["outs"][:i+1]
+ for out in newtx["outs"][:i]:
+ out['value'] = 2**64 - 1
+ out['script'] = ""
+ for j, inp in enumerate(newtx["ins"]):
+ if j != i:
+ inp["sequence"] = 0
+ if hashcode & SIGHASH_ANYONECANPAY:
+ newtx["ins"] = [newtx["ins"][i]]
+ else:
+ pass
+ return newtx
+
+def txhash(tx, hashcode=None):
+ if isinstance(tx, str) and re.match('^[0-9a-fA-F]*$', tx):
+ tx = changebase(tx, 16, 256)
+ if hashcode:
+ return dbl_sha256(from_string_to_bytes(tx) + encode(
+ int(hashcode), 256, 4)[::-1])
+ else:
+ return safe_hexlify(bin_dbl_sha256(tx)[::-1])
+
+
+def bin_txhash(tx, hashcode=None):
+ return binascii.unhexlify(txhash(tx, hashcode))
+
+
+def ecdsa_tx_sign(tx, priv, hashcode=SIGHASH_ALL, usenonce=None):
+ sig = ecdsa_raw_sign(
+ txhash(tx, hashcode),
+ priv,
+ True,
+ rawmsg=True,
+ usenonce=usenonce)
+ return sig + encode(hashcode, 16, 2)
+
+
+def ecdsa_tx_verify(tx, sig, pub, hashcode=SIGHASH_ALL):
+ return ecdsa_raw_verify(
+ txhash(tx, hashcode),
+ pub,
+ sig[:-2],
+ True,
+ rawmsg=True)
+
+# Scripts
+
+
+def mk_pubkey_script(addr):
+ # Keep the auxiliary functions around for altcoins' sake
+ return '76a914' + b58check_to_hex(addr) + '88ac'
+
+
+def mk_scripthash_script(addr):
+ return 'a914' + b58check_to_hex(addr) + '87'
+
+# Address representation to output script
+
+
+def address_to_script(addr):
+ if addr[0] == '3' or addr[0] == '2':
+ return mk_scripthash_script(addr)
+ else:
+ return mk_pubkey_script(addr)
+
+# Output script to address representation
+
+
+def script_to_address(script, vbyte=0):
+ if re.match('^[0-9a-fA-F]*$', script):
+ script = binascii.unhexlify(script)
+ if script[:3] == b'\x76\xa9\x14' and script[-2:] == b'\x88\xac' and len(
+ script) == 25:
+ return bin_to_b58check(script[3:-2], vbyte) # pubkey hash addresses
+ else:
+ if vbyte in [111, 196]:
+ # Testnet
+ scripthash_byte = 196
+ else:
+ scripthash_byte = 5
+ # BIP0016 scripthash addresses
+ return bin_to_b58check(script[2:-1], scripthash_byte)
+
+
+def p2sh_scriptaddr(script, magicbyte=5):
+ if re.match('^[0-9a-fA-F]*$', script):
+ script = binascii.unhexlify(script)
+ return hex_to_b58check(hash160(script), magicbyte)
+
+
+scriptaddr = p2sh_scriptaddr
+
+
+def deserialize_script(script):
+ if isinstance(script, str) and re.match('^[0-9a-fA-F]*$', script):
+ return json_changebase(
+ deserialize_script(binascii.unhexlify(script)),
+ lambda x: safe_hexlify(x))
+ out, pos = [], 0
+ while pos < len(script):
+ code = from_byte_to_int(script[pos])
+ if code == 0:
+ out.append(None)
+ pos += 1
+ elif code <= 75:
+ out.append(script[pos + 1:pos + 1 + code])
+ pos += 1 + code
+ elif code <= 78:
+ szsz = pow(2, code - 76)
+ sz = decode(script[pos + szsz:pos:-1], 256)
+ out.append(script[pos + 1 + szsz:pos + 1 + szsz + sz])
+ pos += 1 + szsz + sz
+ elif code <= 96:
+ out.append(code - 80)
+ pos += 1
+ else:
+ out.append(code)
+ pos += 1
+ return out
+
+
+def serialize_script_unit(unit):
+ if isinstance(unit, int):
+ if unit < 16:
+ return from_int_to_byte(unit + 80)
+ else:
+ return bytes([unit])
+ elif unit is None:
+ return b'\x00'
+ else:
+ if len(unit) <= 75:
+ return from_int_to_byte(len(unit)) + unit
+ elif len(unit) < 256:
+ return from_int_to_byte(76) + from_int_to_byte(len(unit)) + unit
+ elif len(unit) < 65536:
+ return from_int_to_byte(77) + encode(len(unit), 256, 2)[::-1] + unit
+ else:
+ return from_int_to_byte(78) + encode(len(unit), 256, 4)[::-1] + unit
+
+
+if is_python2:
+
+ def serialize_script(script):
+ if json_is_base(script, 16):
+ return binascii.hexlify(serialize_script(json_changebase(
+ script, lambda x: binascii.unhexlify(x))))
+ return ''.join(map(serialize_script_unit, script))
+else:
+
+ def serialize_script(script):
+ if json_is_base(script, 16):
+ return safe_hexlify(serialize_script(json_changebase(
+ script, lambda x: binascii.unhexlify(x))))
+
+ result = bytes()
+ for b in map(serialize_script_unit, script):
+ result += b if isinstance(b, bytes) else bytes(b, 'utf-8')
+ return result
+
+
+def mk_multisig_script(*args): # [pubs],k or pub1,pub2...pub[n],k
+ if isinstance(args[0], list):
+ pubs, k = args[0], int(args[1])
+ else:
+ pubs = list(filter(lambda x: len(str(x)) >= 32, args))
+ k = int(args[len(pubs)])
+ return serialize_script([k] + pubs + [len(pubs)]) + 'ae'
+
+# Signing and verifying
+
+
+def verify_tx_input(tx, i, script, sig, pub):
+ if re.match('^[0-9a-fA-F]*$', tx):
+ tx = binascii.unhexlify(tx)
+ if re.match('^[0-9a-fA-F]*$', script):
+ script = binascii.unhexlify(script)
+ if not re.match('^[0-9a-fA-F]*$', sig):
+ sig = safe_hexlify(sig)
+ if not re.match('^[0-9a-fA-F]*$', pub):
+ pub = safe_hexlify(pub)
+ hashcode = decode(sig[-2:], 16)
+ modtx = signature_form(tx, int(i), script, hashcode)
+ return ecdsa_tx_verify(modtx, sig, pub, hashcode)
+
+
+def sign(tx, i, priv, hashcode=SIGHASH_ALL, usenonce=None):
+ i = int(i)
+ if (not is_python2 and isinstance(re, bytes)) or not re.match(
+ '^[0-9a-fA-F]*$', tx):
+ return binascii.unhexlify(sign(safe_hexlify(tx), i, priv))
+ if len(priv) <= 33:
+ priv = safe_hexlify(priv)
+ pub = privkey_to_pubkey(priv, True)
+ address = pubkey_to_address(pub)
+ signing_tx = signature_form(tx, i, mk_pubkey_script(address), hashcode)
+ sig = ecdsa_tx_sign(signing_tx, priv, hashcode, usenonce=usenonce)
+ txobj = deserialize(tx)
+ txobj["ins"][i]["script"] = serialize_script([sig, pub])
+ return serialize(txobj)
+
+
+def signall(tx, priv):
+ # if priv is a dictionary, assume format is
+ # { 'txinhash:txinidx' : privkey }
+ if isinstance(priv, dict):
+ for e, i in enumerate(deserialize(tx)["ins"]):
+ k = priv["%s:%d" % (i["outpoint"]["hash"], i["outpoint"]["index"])]
+ tx = sign(tx, e, k)
+ else:
+ for i in range(len(deserialize(tx)["ins"])):
+ tx = sign(tx, i, priv)
+ return tx
+
+
+def multisign(tx, i, script, pk, hashcode=SIGHASH_ALL):
+ if re.match('^[0-9a-fA-F]*$', tx):
+ tx = binascii.unhexlify(tx)
+ if re.match('^[0-9a-fA-F]*$', script):
+ script = binascii.unhexlify(script)
+ modtx = signature_form(tx, i, script, hashcode)
+ return ecdsa_tx_sign(modtx, pk, hashcode)
+
+
+def apply_multisignatures(*args):
+ # tx,i,script,sigs OR tx,i,script,sig1,sig2...,sig[n]
+ tx, i, script = args[0], int(args[1]), args[2]
+ sigs = args[3] if isinstance(args[3], list) else list(args[3:])
+
+ if isinstance(script, str) and re.match('^[0-9a-fA-F]*$', script):
+ script = binascii.unhexlify(script)
+ sigs = [binascii.unhexlify(x) if x[:2] == '30' else x for x in sigs]
+ if isinstance(tx, str) and re.match('^[0-9a-fA-F]*$', tx):
+ return safe_hexlify(apply_multisignatures(
+ binascii.unhexlify(tx), i, script, sigs))
+
+ txobj = deserialize(tx)
+ txobj["ins"][i]["script"] = serialize_script([None] + sigs + [script])
+ return serialize(txobj)
+
+
+def is_inp(arg):
+ return len(arg) > 64 or "output" in arg or "outpoint" in arg
+
+
+def mktx(*args):
+ # [in0, in1...],[out0, out1...] or in0, in1 ... out0 out1 ...
+ ins, outs = [], []
+ for arg in args:
+ if isinstance(arg, list):
+ for a in arg:
+ (ins if is_inp(a) else outs).append(a)
+ else:
+ (ins if is_inp(arg) else outs).append(arg)
+
+ txobj = {"locktime": 0, "version": 1, "ins": [], "outs": []}
+ for i in ins:
+ if isinstance(i, dict) and "outpoint" in i:
+ txobj["ins"].append(i)
+ else:
+ if isinstance(i, dict) and "output" in i:
+ i = i["output"]
+ txobj["ins"].append({
+ "outpoint": {"hash": i[:64],
+ "index": int(i[65:])},
+ "script": "",
+ "sequence": 4294967295
+ })
+ for o in outs:
+ if isinstance(o, string_or_bytes_types):
+ addr = o[:o.find(':')]
+ val = int(o[o.find(':') + 1:])
+ o = {}
+ if re.match('^[0-9a-fA-F]*$', addr):
+ o["script"] = addr
+ else:
+ o["address"] = addr
+ o["value"] = val
+
+ outobj = {}
+ if "address" in o:
+ outobj["script"] = address_to_script(o["address"])
+ elif "script" in o:
+ outobj["script"] = o["script"]
+ else:
+ raise Exception("Could not find 'address' or 'script' in output.")
+ outobj["value"] = o["value"]
+ txobj["outs"].append(outobj)
+
+ return serialize(txobj)
+
+
+def select(unspent, value):
+ value = int(value)
+ high = [u for u in unspent if u["value"] >= value]
+ high.sort(key=lambda u: u["value"])
+ low = [u for u in unspent if u["value"] < value]
+ low.sort(key=lambda u: -u["value"])
+ if len(high):
+ return [high[0]]
+ i, tv = 0, 0
+ while tv < value and i < len(low):
+ tv += low[i]["value"]
+ i += 1
+ if tv < value:
+ raise Exception("Not enough funds")
+ return low[:i]
diff --git a/bitcoin/transaction.py b/bitcoin/transaction.py
@@ -0,0 +1,490 @@
+#!/usr/bin/python
+import binascii, re, json, copy, sys
+from bitcoin.main import *
+from _functools import reduce
+
+### Hex to bin converter and vice versa for objects
+
+
+def json_is_base(obj, base):
+ if not is_python2 and isinstance(obj, bytes):
+ return False
+
+ alpha = get_code_string(base)
+ if isinstance(obj, string_types):
+ for i in range(len(obj)):
+ if alpha.find(obj[i]) == -1:
+ return False
+ return True
+ elif isinstance(obj, int_types) or obj is None:
+ return True
+ elif isinstance(obj, list):
+ for i in range(len(obj)):
+ if not json_is_base(obj[i], base):
+ return False
+ return True
+ else:
+ for x in obj:
+ if not json_is_base(obj[x], base):
+ return False
+ return True
+
+
+def json_changebase(obj, changer):
+ if isinstance(obj, string_or_bytes_types):
+ return changer(obj)
+ elif isinstance(obj, int_types) or obj is None:
+ return obj
+ elif isinstance(obj, list):
+ return [json_changebase(x, changer) for x in obj]
+ return dict((x, json_changebase(obj[x], changer)) for x in obj)
+
+# Transaction serialization and deserialization
+
+
+def deserialize(tx):
+ if isinstance(tx, str) and re.match('^[0-9a-fA-F]*$', tx):
+ #tx = bytes(bytearray.fromhex(tx))
+ return json_changebase(
+ deserialize(binascii.unhexlify(tx)), lambda x: safe_hexlify(x))
+ # http://stackoverflow.com/questions/4851463/python-closure-write-to-variable-in-parent-scope
+ # Python's scoping rules are demented, requiring me to make pos an object
+ # so that it is call-by-reference
+ pos = [0]
+
+ def read_as_int(bytez):
+ pos[0] += bytez
+ return decode(tx[pos[0] - bytez:pos[0]][::-1], 256)
+
+ def read_var_int():
+ pos[0] += 1
+
+ val = from_byte_to_int(tx[pos[0] - 1])
+ if val < 253:
+ return val
+ return read_as_int(pow(2, val - 252))
+
+ def read_bytes(bytez):
+ pos[0] += bytez
+ return tx[pos[0] - bytez:pos[0]]
+
+ def read_var_string():
+ size = read_var_int()
+ return read_bytes(size)
+
+ obj = {"ins": [], "outs": []}
+ obj["version"] = read_as_int(4)
+ ins = read_var_int()
+ for i in range(ins):
+ obj["ins"].append({
+ "outpoint": {
+ "hash": read_bytes(32)[::-1],
+ "index": read_as_int(4)
+ },
+ "script": read_var_string(),
+ "sequence": read_as_int(4)
+ })
+ outs = read_var_int()
+ for i in range(outs):
+ obj["outs"].append({
+ "value": read_as_int(8),
+ "script": read_var_string()
+ })
+ obj["locktime"] = read_as_int(4)
+ return obj
+
+
+def serialize(txobj):
+ #if isinstance(txobj, bytes):
+ # txobj = bytes_to_hex_string(txobj)
+ o = []
+ if json_is_base(txobj, 16):
+ json_changedbase = json_changebase(txobj,
+ lambda x: binascii.unhexlify(x))
+ hexlified = safe_hexlify(serialize(json_changedbase))
+ return hexlified
+ o.append(encode(txobj["version"], 256, 4)[::-1])
+ o.append(num_to_var_int(len(txobj["ins"])))
+ for inp in txobj["ins"]:
+ o.append(inp["outpoint"]["hash"][::-1])
+ o.append(encode(inp["outpoint"]["index"], 256, 4)[::-1])
+ o.append(num_to_var_int(len(inp["script"])) + (inp["script"] if inp[
+ "script"] or is_python2 else bytes()))
+ o.append(encode(inp["sequence"], 256, 4)[::-1])
+ o.append(num_to_var_int(len(txobj["outs"])))
+ for out in txobj["outs"]:
+ o.append(encode(out["value"], 256, 8)[::-1])
+ o.append(num_to_var_int(len(out["script"])) + out["script"])
+ o.append(encode(txobj["locktime"], 256, 4)[::-1])
+
+ return ''.join(o) if is_python2 else reduce(lambda x, y: x + y, o, bytes())
+
+# Hashing transactions for signing
+
+SIGHASH_ALL = 1
+SIGHASH_NONE = 2
+SIGHASH_SINGLE = 3
+# this works like SIGHASH_ANYONECANPAY | SIGHASH_ALL, might as well make it explicit while
+# we fix the constant
+SIGHASH_ANYONECANPAY = 0x81
+
+
+def signature_form(tx, i, script, hashcode=SIGHASH_ALL):
+ i, hashcode = int(i), int(hashcode)
+ if isinstance(tx, string_or_bytes_types):
+ return serialize(signature_form(deserialize(tx), i, script, hashcode))
+ newtx = copy.deepcopy(tx)
+ for inp in newtx["ins"]:
+ inp["script"] = ""
+ newtx["ins"][i]["script"] = script
+ if hashcode == SIGHASH_NONE:
+ newtx["outs"] = []
+ elif hashcode == SIGHASH_SINGLE:
+ newtx["outs"] = newtx["outs"][:len(newtx["ins"])]
+ for out in range(len(newtx["ins"]) - 1):
+ out.value = 2**64 - 1
+ out.script = ""
+ elif hashcode == SIGHASH_ANYONECANPAY:
+ newtx["ins"] = [newtx["ins"][i]]
+ else:
+ pass
+ return newtx
+
+# Making the actual signatures
+
+
+def der_encode_sig(v, r, s):
+ """Takes (vbyte, r, s) as ints and returns hex der encode sig"""
+ #See https://github.com/vbuterin/pybitcointools/issues/89
+ #See https://github.com/simcity4242/pybitcointools/
+ s = N - s if s > N // 2 else s # BIP62 low s
+ b1, b2 = encode(r, 256), encode(s, 256)
+ if bytearray(b1)[
+ 0] & 0x80: # add null bytes if leading byte interpreted as negative
+ b1 = b'\x00' + b1
+ if bytearray(b2)[0] & 0x80:
+ b2 = b'\x00' + b2
+ left = b'\x02' + encode(len(b1), 256, 1) + b1
+ right = b'\x02' + encode(len(b2), 256, 1) + b2
+ return safe_hexlify(b'\x30' + encode(
+ len(left + right), 256, 1) + left + right)
+
+
+def der_decode_sig(sig):
+ leftlen = decode(sig[6:8], 16) * 2
+ left = sig[8:8 + leftlen]
+ rightlen = decode(sig[10 + leftlen:12 + leftlen], 16) * 2
+ right = sig[12 + leftlen:12 + leftlen + rightlen]
+ return (None, decode(left, 16), decode(right, 16))
+
+
+def txhash(tx, hashcode=None):
+ if isinstance(tx, str) and re.match('^[0-9a-fA-F]*$', tx):
+ tx = changebase(tx, 16, 256)
+ if hashcode:
+ return dbl_sha256(from_string_to_bytes(tx) + encode(
+ int(hashcode), 256, 4)[::-1])
+ else:
+ return safe_hexlify(bin_dbl_sha256(tx)[::-1])
+
+
+def bin_txhash(tx, hashcode=None):
+ return binascii.unhexlify(txhash(tx, hashcode))
+
+
+def ecdsa_tx_sign(tx, priv, hashcode=SIGHASH_ALL):
+ rawsig = ecdsa_raw_sign(bin_txhash(tx, hashcode), priv)
+ return der_encode_sig(*rawsig) + encode(hashcode, 16, 2)
+
+
+def ecdsa_tx_verify(tx, sig, pub, hashcode=SIGHASH_ALL):
+ return ecdsa_raw_verify(bin_txhash(tx, hashcode), der_decode_sig(sig), pub)
+
+# Scripts
+
+def mk_pubkey_script(addr):
+ # Keep the auxiliary functions around for altcoins' sake
+ return '76a914' + b58check_to_hex(addr) + '88ac'
+
+
+def mk_scripthash_script(addr):
+ return 'a914' + b58check_to_hex(addr) + '87'
+
+# Address representation to output script
+
+
+def address_to_script(addr):
+ if addr[0] == '3' or addr[0] == '2':
+ return mk_scripthash_script(addr)
+ else:
+ return mk_pubkey_script(addr)
+
+# Output script to address representation
+
+
+def script_to_address(script, vbyte=0):
+ if re.match('^[0-9a-fA-F]*$', script):
+ script = binascii.unhexlify(script)
+ if script[:3] == b'\x76\xa9\x14' and script[-2:] == b'\x88\xac' and len(
+ script) == 25:
+ return bin_to_b58check(script[3:-2], vbyte) # pubkey hash addresses
+ else:
+ if vbyte in [111, 196]:
+ # Testnet
+ scripthash_byte = 196
+ else:
+ scripthash_byte = 5
+ # BIP0016 scripthash addresses
+ return bin_to_b58check(script[2:-1], scripthash_byte)
+
+
+def p2sh_scriptaddr(script, magicbyte=5):
+ if re.match('^[0-9a-fA-F]*$', script):
+ script = binascii.unhexlify(script)
+ return hex_to_b58check(hash160(script), magicbyte)
+
+
+scriptaddr = p2sh_scriptaddr
+
+
+def deserialize_script(script):
+ if isinstance(script, str) and re.match('^[0-9a-fA-F]*$', script):
+ return json_changebase(
+ deserialize_script(binascii.unhexlify(script)),
+ lambda x: safe_hexlify(x))
+ out, pos = [], 0
+ while pos < len(script):
+ code = from_byte_to_int(script[pos])
+ if code == 0:
+ out.append(None)
+ pos += 1
+ elif code <= 75:
+ out.append(script[pos + 1:pos + 1 + code])
+ pos += 1 + code
+ elif code <= 78:
+ szsz = pow(2, code - 76)
+ sz = decode(script[pos + szsz:pos:-1], 256)
+ out.append(script[pos + 1 + szsz:pos + 1 + szsz + sz])
+ pos += 1 + szsz + sz
+ elif code <= 96:
+ out.append(code - 80)
+ pos += 1
+ else:
+ out.append(code)
+ pos += 1
+ return out
+
+
+def serialize_script_unit(unit):
+ if isinstance(unit, int):
+ if unit < 16:
+ return from_int_to_byte(unit + 80)
+ else:
+ return bytes([unit])
+ elif unit is None:
+ return b'\x00'
+ else:
+ if len(unit) <= 75:
+ return from_int_to_byte(len(unit)) + unit
+ elif len(unit) < 256:
+ return from_int_to_byte(76) + from_int_to_byte(len(unit)) + unit
+ elif len(unit) < 65536:
+ return from_int_to_byte(77) + encode(len(unit), 256, 2)[::-1] + unit
+ else:
+ return from_int_to_byte(78) + encode(len(unit), 256, 4)[::-1] + unit
+
+
+if is_python2:
+
+ def serialize_script(script):
+ if json_is_base(script, 16):
+ return binascii.hexlify(serialize_script(json_changebase(
+ script, lambda x: binascii.unhexlify(x))))
+ return ''.join(map(serialize_script_unit, script))
+else:
+
+ def serialize_script(script):
+ if json_is_base(script, 16):
+ return safe_hexlify(serialize_script(json_changebase(
+ script, lambda x: binascii.unhexlify(x))))
+
+ result = bytes()
+ for b in map(serialize_script_unit, script):
+ result += b if isinstance(b, bytes) else bytes(b, 'utf-8')
+ return result
+
+
+def mk_multisig_script(*args): # [pubs],k or pub1,pub2...pub[n],k
+ if isinstance(args[0], list):
+ pubs, k = args[0], int(args[1])
+ else:
+ pubs = list(filter(lambda x: len(str(x)) >= 32, args))
+ k = int(args[len(pubs)])
+ return serialize_script([k] + pubs + [len(pubs)]) + 'ae'
+
+# Signing and verifying
+
+
+def verify_tx_input(tx, i, script, sig, pub):
+ if re.match('^[0-9a-fA-F]*$', tx):
+ tx = binascii.unhexlify(tx)
+ if re.match('^[0-9a-fA-F]*$', script):
+ script = binascii.unhexlify(script)
+ if not re.match('^[0-9a-fA-F]*$', sig):
+ sig = safe_hexlify(sig)
+ hashcode = decode(sig[-2:], 16)
+ modtx = signature_form(tx, int(i), script, hashcode)
+ return ecdsa_tx_verify(modtx, sig, pub, hashcode)
+
+
+def sign(tx, i, priv, hashcode=SIGHASH_ALL):
+ i = int(i)
+ if (not is_python2 and isinstance(re, bytes)) or not re.match(
+ '^[0-9a-fA-F]*$', tx):
+ return binascii.unhexlify(sign(safe_hexlify(tx), i, priv))
+ if len(priv) <= 33:
+ priv = safe_hexlify(priv)
+ pub = privkey_to_pubkey(priv)
+ address = pubkey_to_address(pub)
+ signing_tx = signature_form(tx, i, mk_pubkey_script(address), hashcode)
+ sig = ecdsa_tx_sign(signing_tx, priv, hashcode)
+ txobj = deserialize(tx)
+ txobj["ins"][i]["script"] = serialize_script([sig, pub])
+ return serialize(txobj)
+
+
+def signall(tx, priv):
+ # if priv is a dictionary, assume format is
+ # { 'txinhash:txinidx' : privkey }
+ if isinstance(priv, dict):
+ for e, i in enumerate(deserialize(tx)["ins"]):
+ k = priv["%s:%d" % (i["outpoint"]["hash"], i["outpoint"]["index"])]
+ tx = sign(tx, e, k)
+ else:
+ for i in range(len(deserialize(tx)["ins"])):
+ tx = sign(tx, i, priv)
+ return tx
+
+
+def multisign(tx, i, script, pk, hashcode=SIGHASH_ALL):
+ if re.match('^[0-9a-fA-F]*$', tx):
+ tx = binascii.unhexlify(tx)
+ if re.match('^[0-9a-fA-F]*$', script):
+ script = binascii.unhexlify(script)
+ modtx = signature_form(tx, i, script, hashcode)
+ return ecdsa_tx_sign(modtx, pk, hashcode)
+
+
+def apply_multisignatures(*args):
+ # tx,i,script,sigs OR tx,i,script,sig1,sig2...,sig[n]
+ tx, i, script = args[0], int(args[1]), args[2]
+ sigs = args[3] if isinstance(args[3], list) else list(args[3:])
+
+ if isinstance(script, str) and re.match('^[0-9a-fA-F]*$', script):
+ script = binascii.unhexlify(script)
+ sigs = [binascii.unhexlify(x) if x[:2] == '30' else x for x in sigs]
+ if isinstance(tx, str) and re.match('^[0-9a-fA-F]*$', tx):
+ return safe_hexlify(apply_multisignatures(
+ binascii.unhexlify(tx), i, script, sigs))
+
+ txobj = deserialize(tx)
+ txobj["ins"][i]["script"] = serialize_script([None] + sigs + [script])
+ return serialize(txobj)
+
+
+def is_inp(arg):
+ return len(arg) > 64 or "output" in arg or "outpoint" in arg
+
+
+def mktx(*args):
+ # [in0, in1...],[out0, out1...] or in0, in1 ... out0 out1 ...
+ ins, outs = [], []
+ for arg in args:
+ if isinstance(arg, list):
+ for a in arg:
+ (ins if is_inp(a) else outs).append(a)
+ else:
+ (ins if is_inp(arg) else outs).append(arg)
+
+ txobj = {"locktime": 0, "version": 1, "ins": [], "outs": []}
+ for i in ins:
+ if isinstance(i, dict) and "outpoint" in i:
+ txobj["ins"].append(i)
+ else:
+ if isinstance(i, dict) and "output" in i:
+ i = i["output"]
+ txobj["ins"].append({
+ "outpoint": {"hash": i[:64],
+ "index": int(i[65:])},
+ "script": "",
+ "sequence": 4294967295
+ })
+ for o in outs:
+ if isinstance(o, string_or_bytes_types):
+ addr = o[:o.find(':')]
+ val = int(o[o.find(':') + 1:])
+ o = {}
+ if re.match('^[0-9a-fA-F]*$', addr):
+ o["script"] = addr
+ else:
+ o["address"] = addr
+ o["value"] = val
+
+ outobj = {}
+ if "address" in o:
+ outobj["script"] = address_to_script(o["address"])
+ elif "script" in o:
+ outobj["script"] = o["script"]
+ else:
+ raise Exception("Could not find 'address' or 'script' in output.")
+ outobj["value"] = o["value"]
+ txobj["outs"].append(outobj)
+
+ return serialize(txobj)
+
+
+def select(unspent, value):
+ value = int(value)
+ high = [u for u in unspent if u["value"] >= value]
+ high.sort(key=lambda u: u["value"])
+ low = [u for u in unspent if u["value"] < value]
+ low.sort(key=lambda u: -u["value"])
+ if len(high):
+ return [high[0]]
+ i, tv = 0, 0
+ while tv < value and i < len(low):
+ tv += low[i]["value"]
+ i += 1
+ if tv < value:
+ raise Exception("Not enough funds")
+ return low[:i]
+
+# Only takes inputs of the form { "output": blah, "value": foo }
+
+
+def mksend(*args):
+ argz, change, fee = args[:-2], args[-2], int(args[-1])
+ ins, outs = [], []
+ for arg in argz:
+ if isinstance(arg, list):
+ for a in arg:
+ (ins if is_inp(a) else outs).append(a)
+ else:
+ (ins if is_inp(arg) else outs).append(arg)
+
+ isum = sum([i["value"] for i in ins])
+ osum, outputs2 = 0, []
+ for o in outs:
+ if isinstance(o, string_types):
+ o2 = {"address": o[:o.find(':')], "value": int(o[o.find(':') + 1:])}
+ else:
+ o2 = o
+ outputs2.append(o2)
+ osum += o2["value"]
+
+ if isum < osum + fee:
+ raise Exception("Not enough money")
+ elif isum > osum + fee + 5430:
+ outputs2 += [{"address": change, "value": isum - osum - fee}]
+
+ return mktx(ins, outputs2)
diff --git a/config.cfg_sample b/config.cfg_sample
@@ -0,0 +1,37 @@
+
+## Electrum Personal Server configuration file
+## Comments start with #
+
+[wallets]
+## Add addresses to this section
+
+# These are just random addresses I found on a blockchain explorer
+
+# A key can be anything
+addr = 1DuqpoeTB9zLvVCXQG53VbMxvMkijk494n
+# A comma separated list is also accepted
+my_test_addresses = 1KKszdQEpXgSY4EvsnGcGEzbQFmdcFwuNS,1EuEVUtQQ8hmuMHNdMdjLwjpkm6Ef7RYVk
+# And space separated
+more_test_addresses = 3Hh7QujVLqz11tiQsnUE5CSL16WEHBmiyR 1Arzu6mWZuXGTF9yR2hZhMgBJgu1Xh2bNC 1PXRLo1FQoZyF1Jhnz4qbG5x8Bo3pFpybz
+
+[bitcoin-rpc]
+host = localhost
+port = 8332
+user = bitcoinrpc
+password = password
+
+#how often in seconds to poll for new transactions when electrum not connected
+poll_interval_listening = 30
+#how often in seconds to poll for new transactions when electrum is connected
+poll_interval_connected = 5
+
+[electrum-server]
+#0.0.0.0 to accept connections from any IP
+#127.0.0.1 to accept from only localhost
+#recommended you accept localhost only and connect with a ssh tunnel
+host = 127.0.0.1
+port = 50002
+
+[misc]
+#not implemented yet
+print_debug = false
diff --git a/jsonrpc.py b/jsonrpc.py
@@ -0,0 +1,59 @@
+#jsonrpc.py from https://github.com/JoinMarket-Org/joinmarket/blob/master/joinmarket/jsonrpc.py
+#copyright # Copyright (C) 2013,2015 by Daniel Kraft <d@domob.eu> and phelix / blockchained.com
+
+import base64
+import http.client
+import json
+
+class JsonRpcError(Exception):
+ def __init__(self, obj):
+ self.code = obj["code"]
+ self.message = obj["message"]
+
+class JsonRpcConnectionError(JsonRpcError): pass
+
+class JsonRpc(object):
+ def __init__(self, host, port, user, password):
+ self.host = host
+ self.port = port
+ self.authstr = "%s:%s" % (user, password)
+ self.queryId = 1
+
+ def queryHTTP(self, obj):
+ headers = {"User-Agent": "electrum-personal-server",
+ "Content-Type": "application/json",
+ "Accept": "application/json"}
+ headers["Authorization"] = "Basic %s" % base64.b64encode(
+ self.authstr.encode()).decode()
+ body = json.dumps(obj)
+ try:
+ conn = http.client.HTTPConnection(self.host, self.port)
+ conn.request("POST", "", body, headers)
+ response = conn.getresponse()
+ if response.status == 401:
+ conn.close()
+ raise JsonRpcConnectionError(
+ "authentication for JSON-RPC failed")
+ # All of the codes below are 'fine' from a JSON-RPC point of view.
+ if response.status not in [200, 404, 500]:
+ conn.close()
+ raise JsonRpcConnectionError("unknown error in JSON-RPC")
+ data = response.read()
+ conn.close()
+ return json.loads(data.decode())
+ except JsonRpcConnectionError as exc:
+ raise exc
+ except Exception as exc:
+ raise JsonRpcConnectionError("JSON-RPC connection failed. Err:" +
+ repr(exc))
+
+ def call(self, method, params):
+ currentId = self.queryId
+ self.queryId += 1
+ request = {"method": method, "params": params, "id": currentId}
+ response = self.queryHTTP(request)
+ if response["id"] != currentId:
+ raise JsonRpcConnectionError("invalid id returned by query")
+ if response["error"] is not None:
+ raise JsonRpcError(response["error"])
+ return response["result"]
diff --git a/run-server.bat b/run-server.bat
@@ -0,0 +1,3 @@
+@echo off
+python3 server.py
+pause+
\ No newline at end of file
diff --git a/server.py b/server.py
@@ -0,0 +1,634 @@
+#! /usr/bin/python3
+
+#add a feature where it prints the first 3 addresses from a deterministic
+# wallet, so you can check the addresses are correct before importing them
+# into the node
+
+#or deterministic wallets
+#should figure out what do regarding gap limits, when to import more addresses
+# and how many addresses to start with
+# maybe have a separate list of later addresses and if one of them get
+# requested then import more
+
+#TODO try to support ssl
+#doesnt support ssl yet you you must run ./electrum --nossl
+#https://github.com/spesmilo/electrum/commit/dc388d4c7c541fadb9869727e359edace4c9f6f0
+#maybe copy from electrumx
+#https://github.com/kyuupichan/electrumx/blob/35dd1f61996b02a84691ea71ff50f0900df969bc/server/peers.py#L476
+#https://github.com/kyuupichan/electrumx/blob/2d7403f2efed7e8f33c5cb93e2cd9144415cbb9f/server/controller.py#L259
+
+#merkle trees cant be used if bitcoin core has pruning enabled, this will
+# probably requires new code to be written for core
+#another possible use of merkleproofs in wallet.dat
+# https://github.com/JoinMarket-Org/joinmarket/issues/156#issuecomment-231059844
+
+#using core's multiple wallet feature might help, should read up on that
+
+#now that the rescanblockchain rpc call exists in 0.16 which allows specifying
+# a starting height, that will cut down the time to rescan as long as the user
+# has saved their wallet creation date
+
+#one day there could be a nice GUI which does everything, including converting
+# the wallet creation date to a block height and rescanning
+'''
+<belcher> now that 0.16 has this rpc called rescanblockchain which takes an optional start_height, i wonder what the most practical way of converting date to block height is
+<belcher> thinking about the situation where you have a mnemonic recovery phrase + the date you created it, and want to rescan
+<belcher> binary search the timestamps in the block headers i guess, then subtract two weeks just in case
+<wumpus> belcher: binary search in something that is not strictly increasing seems faulty
+<belcher> yes true, so maybe binary search to roughly get to the right block height then linear search +- a few blocks
+<wumpus> belcher: though my gut feeling is that subtracting the two weeks would fix it
+<belcher> when people write down the wallet creation date they probably wont be precise, you could get away with writing only the year and month i bet
+<wumpus> as the mismatch is at most 2 hours
+<Sentineo> wumpus: 2 hours for the clock scew allowed by peers? (when they throw away a block which is older than 2 hours from their actual time)?
+<wumpus> Sentineo: that's what I remember, I might be off though
+<Sentineo> I am not sure if it s 2 or 4 :D
+<Sentineo> lazyness :)
+<wumpus> in any case it is a bounded value, which means binary search might work within that precision, too lazy to look for proof though :)
+'''
+
+##### good things
+
+# well placed to take advantage of dandelion private tx broadcasting
+# and broadcasting through tor
+
+import socket, time, json, datetime, struct, binascii, math, pprint
+from configparser import ConfigParser, NoSectionError
+from decimal import Decimal
+
+from jsonrpc import JsonRpc, JsonRpcError
+import util
+import bitcoin as btc
+
+ADDRESSES_LABEL = "electrum-watchonly-addresses"
+
+VERSION_NUMBER = "0.1"
+
+BANNER = \
+"""Welcome to Electrum Personal Server
+gitub.com/whatever
+
+Monitoring {addr} addresses
+
+Connected bitcoin node: {useragent}
+Peers: {peers}
+Uptime: {uptime}
+Blocksonly: {blocksonly}
+Pruning: {pruning}
+"""
+
+##python has demented rules for variable scope, so these
+## global variables are actually mutable lists
+subscribed_to_headers = [False]
+bestblockhash = [None]
+last_known_recent_txid = [None]
+
+#log for checking up/seeing your wallet, debug for when something has gone wrong
+def debugorlog(line, ttype):
+ timestamp = datetime.datetime.now().strftime("%H:%M:%S,%f")
+ print(timestamp + " [" + ttype + "] " + line)
+
+def debug(line):
+ debugorlog(line, "DEBUG")
+
+def log(line):
+ debugorlog(line, " LOG")
+
+def send_response(sock, query, result):
+ query["result"] = result
+ query["jsonrpc"] = "2.0"
+ sock.sendall(json.dumps(query).encode('utf-8') + b'\n')
+ debug('<= ' + json.dumps(query))
+
+def send_update(sock, update):
+ update["jsonrpc"] = "2.0"
+ sock.sendall(json.dumps(update).encode('utf-8') + b'\n')
+ debug('<= ' + json.dumps(update))
+
+def on_heartbeat_listening(rpc, address_history, unconfirmed_txes):
+ debug("on heartbeat listening")
+ check_for_updated_txes(rpc, address_history, unconfirmed_txes)
+
+def on_heartbeat_connected(sock, rpc, address_history, unconfirmed_txes):
+ debug("on heartbeat connected")
+ is_tip_updated, header = check_for_new_blockchain_tip(rpc)
+ if is_tip_updated:
+ log("Blockchain tip updated")
+ if subscribed_to_headers[0]:
+ update = {"method": "blockchain.headers.subscribe",
+ "params": [header]}
+ send_update(sock, update)
+ updated_scripthashes = check_for_updated_txes(rpc, address_history,
+ unconfirmed_txes)
+ for scrhash in updated_scripthashes:
+ if not address_history[scrhash]["subscribed"]:
+ continue
+ history_hash = util.get_status_electrum( ((h["tx_hash"], h["height"])
+ for h in address_history[scrhash]["history"]) )
+ update = {"method": "blockchain.scripthash.subscribe", "params":
+ [scrhash, history_hash]}
+ send_update(sock, update)
+
+def on_disconnect(address_history):
+ subscribed_to_headers[0] = False
+ for srchash, his in address_history.items():
+ his["subscribed"] = False
+
+def handle_query(sock, line, rpc, address_history):
+ debug("=> " + line)
+ try:
+ query = json.loads(line)
+ except json.decoder.JSONDecodeError as e:
+ raise IOError(e)
+ method = query["method"]
+
+ #protocol documentation
+ #https://github.com/kyuupichan/electrumx/blob/master/docs/PROTOCOL.rst
+ if method == "blockchain.transaction.get":
+ try:
+ tx = rpc.call("gettransaction", [query["params"][0]])
+ send_response(sock, query, tx["hex"])
+ except JsonRpcError:
+ debug("Unable to get tx " + query["params"][0])
+ elif method == "blockchain.transaction.get_merkle":
+ #we dont support merkle proofs yet, but we must reply with
+ #something otherwise electrum will disconnect from us
+ #so reply with an invalid proof
+ #https://github.com/spesmilo/electrum/blob/c8e67e2bd07efe042703bc1368d499c5e555f854/lib/verifier.py#L74
+ txid = query["params"][0]
+ reply = {"block_height": 1, "pos": 0, "merkle": [txid]}
+ send_response(sock, query, reply)
+ elif method == "blockchain.scripthash.subscribe":
+ scrhash = query["params"][0]
+ if scrhash in address_history:
+ address_history[scrhash]["subscribed"] = True
+ history_hash = util.get_status_electrum((
+ (h["tx_hash"], h["height"])
+ for h in address_history[scrhash]["history"]))
+ else:
+ log("WARNING: address scripthash not known to us: " + scrhash)
+ history_hash = util.get_status_electrum([])
+ send_response(sock, query, history_hash)
+ elif method == "blockchain.scripthash.get_history":
+ scrhash = query["params"][0]
+ if scrhash in address_history:
+ history = address_history[scrhash]["history"]
+ else:
+ history = []
+ log("WARNING: address scripthash history not known to us: "
+ + scrhash)
+ send_response(sock, query, history)
+ elif method == "blockchain.headers.subscribe":
+ subscribed_to_headers[0] = True
+ new_bestblockhash, header = get_current_header(rpc)
+ send_response(sock, query, header)
+ elif method == "blockchain.block.get_header":
+ blockhash = rpc.call("getblockhash", [query["params"][0]])
+ header = get_block_header(rpc, blockhash)
+ send_response(sock, query, header)
+ elif method == "blockchain.block.get_chunk":
+ RETARGET_INTERVAL = 2016
+ index = query["params"][0]
+ tip_height = rpc.call("getblockchaininfo", [])["headers"]
+ #logic copied from kyuupichan's electrumx get_chunk() in controller.py
+ next_height = tip_height + 1
+ start_height = min(index*RETARGET_INTERVAL, next_height)
+ count = min(next_height - start_height, RETARGET_INTERVAL)
+ #read count number of headers starting from start_height
+ result = bytearray()
+ the_hash = rpc.call("getblockhash", [start_height])
+ for i in range(count):
+ header = rpc.call("getblockheader", [the_hash])
+ #add header hex to result
+ h1 = struct.pack("<i32s32sIII", header["version"],
+ binascii.unhexlify(header["previousblockhash"])[::-1],
+ binascii.unhexlify(header["merkleroot"])[::-1],
+ header["time"], int(header["bits"], 16), header["nonce"])
+ result.extend(h1)
+ if "nextblockhash" not in header:
+ break
+ the_hash = header["nextblockhash"]
+ send_response(sock, query, binascii.hexlify(result).decode("utf-8"))
+ elif method == "blockchain.transaction.broadcast":
+ try:
+ result = rpc.call("sendrawtransaction", [query["params"][0]])
+ except JsonRpcError as e:
+ result = e.message
+ debug("tx broadcast result = " + str(result))
+ send_response(sock, query, result)
+ elif method == "blockchain.estimatefee":
+ estimate = rpc.call("estimatesmartfee", [query["params"][0]])
+ feerate = 0.0001
+ if "feerate" in estimate:
+ feerate = estimate["feerate"]
+ send_response(sock, query, feerate)
+ elif method == "blockchain.relayfee":
+ networkinfo = rpc.call("getnetworkinfo", [])
+ send_response(sock, query, networkinfo["relayfee"])
+ elif method == "server.banner":
+ networkinfo = rpc.call("getnetworkinfo", [])
+ blockchaininfo = rpc.call("getblockchaininfo", [])
+ uptime = rpc.call("uptime", [])
+ send_response(sock, query, BANNER.format(
+ addr=len(address_history),
+ useragent=networkinfo["subversion"],
+ peers=networkinfo["connections"],
+ uptime=str(datetime.timedelta(seconds=uptime)),
+ blocksonly=not networkinfo["localrelay"],
+ pruning=blockchaininfo["pruned"]))
+ elif method == "server.donation_address":
+ send_response(sock, query, "bc1q5d8l0w33h65e2l5x7ty6wgnvkvlqcz0wfaslpz")
+ elif method == "server.version":
+ send_response(sock, query, ["ElectrumPersonalServer "
+ + VERSION_NUMBER, VERSION_NUMBER])
+ elif method == "server.peers.subscribe":
+ send_response(sock, query, []) #no peers to report
+ else:
+ log("*** BUG! Not handling method: " + method + " query=" + str(query))
+
+def get_block_header(rpc, blockhash):
+ rpc_head = rpc.call("getblockheader", [blockhash])
+ header = {"block_height": rpc_head["height"],
+ "prev_block_hash": rpc_head["previousblockhash"],
+ "timestamp": rpc_head["time"],
+ "merkle_root": rpc_head["merkleroot"],
+ "version": rpc_head["version"],
+ "nonce": rpc_head["nonce"],
+ "bits": int(rpc_head["bits"], 16)}
+ return header
+
+def get_current_header(rpc):
+ new_bestblockhash = rpc.call("getbestblockhash", [])
+ header = get_block_header(rpc, new_bestblockhash)
+ return new_bestblockhash, header
+
+def check_for_new_blockchain_tip(rpc):
+ #TODO might not handle more than one block appearing, might need to
+ # use a "last known block" similar to the transaction code
+ new_bestblockhash, header = get_current_header(rpc)
+ is_tip_new = bestblockhash[0] != new_bestblockhash
+ bestblockhash[0] = new_bestblockhash
+ return is_tip_new, header
+
+def create_server_socket(hostport):
+ server_sock = socket.socket()
+ server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ server_sock.bind(hostport)
+ log("Listening on " + str(hostport))
+ return server_sock
+
+def run_electrum_server(hostport, rpc, address_history, unconfirmed_txes,
+ poll_interval_listening, poll_interval_connected):
+ log("Starting electrum server")
+ while True:
+ try:
+ server_sock = create_server_socket(hostport)
+ server_sock.settimeout(poll_interval_listening)
+ while True:
+ try:
+ server_sock.listen(1)
+ sock, addr = server_sock.accept()
+ break
+ except socket.timeout:
+ on_heartbeat_listening(rpc, address_history,
+ unconfirmed_txes)
+ server_sock.close()
+ sock.settimeout(poll_interval_connected)
+ log('Electrum connected from ' + str(addr))
+ recv_buffer = bytearray()
+ while True:
+ try:
+ recv_data = sock.recv(4096)
+ if not recv_data or len(recv_data) == 0:
+ raise EOFError()
+ recv_buffer.extend(recv_data)
+ lb = recv_buffer.find(b'\n')
+ if lb == -1:
+ continue
+ while lb != -1:
+ line = recv_buffer[:lb].rstrip()
+ recv_buffer = recv_buffer[lb + 1:]
+ lb = recv_buffer.find(b'\n')
+ handle_query(sock, line.decode("utf-8"), rpc,
+ address_history)
+ except socket.timeout:
+ on_heartbeat_connected(sock, rpc, address_history,
+ unconfirmed_txes)
+ except (IOError, EOFError) as e:
+ if isinstance(e, EOFError):
+ log("Electrum wallet disconnected")
+ else:
+ log("IOError: " + repr(e))
+ on_disconnect(address_history)
+ time.sleep(0.2)
+ try:
+ server_sock.close()
+ except IOError:
+ pass
+
+def get_input_and_output_scriptpubkeys(rpc, txid):
+ gettx = rpc.call("gettransaction", [txid])
+ txd = btc.deserialize(gettx["hex"])
+ output_scriptpubkeys = [sc['script'] for sc in txd['outs']]
+ input_scriptpubkeys = []
+ for ins in txd["ins"]:
+ try:
+ wallet_tx = rpc.call("gettransaction", [ins["outpoint"][
+ "hash"]])
+ except JsonRpcError:
+ #wallet doesnt know about this tx, so the input isnt ours
+ continue
+ script = btc.deserialize(str(wallet_tx["hex"]))["outs"][ins[
+ "outpoint"]["index"]]["script"]
+ input_scriptpubkeys.append(script)
+ return output_scriptpubkeys, input_scriptpubkeys, txd
+
+def generate_new_history_element(rpc, tx, txd):
+ if tx["confirmations"] == 0:
+ unconfirmed_input = False
+ total_input_value = 0
+ for ins in txd["ins"]:
+ utxo = rpc.call("gettxout", [ins["outpoint"]["hash"],
+ ins["outpoint"]["index"], True])
+ if utxo is None:
+ utxo = rpc.call("gettxout", [ins["outpoint"]["hash"],
+ ins["outpoint"]["index"], False])
+ if utxo is None:
+ 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
+ debug("total_input_value = " + str(total_input_value))
+
+ fee = total_input_value - sum([sc["value"] for sc in txd["outs"]])
+ height = -1 if unconfirmed_input else 0
+ new_history_element = ({"tx_hash": tx["txid"], "height": height,
+ "fee": fee})
+ else:
+ blockheader = rpc.call("getblockheader", [tx['blockhash']])
+ new_history_element = ({"tx_hash": tx["txid"],
+ "height": blockheader["height"]})
+ return new_history_element
+
+def sort_address_history_list(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
+ #perhaps check if any heights are the same then get the pos only for those
+ #a 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(rpc, address_history, unconfirmed_txes):
+ updated_srchashes1 = check_for_unconfirmed_txes(rpc, address_history,
+ unconfirmed_txes)
+ updated_srchashes2 = check_for_confirmations(rpc, address_history,
+ unconfirmed_txes)
+ updated_srchashes = updated_srchashes1 | updated_srchashes2
+ for ush in updated_srchashes:
+ his = address_history[ush]
+ sort_address_history_list(his)
+ if len(updated_srchashes) > 0:
+ debug("new tx address_history =\n" + pprint.pformat(address_history))
+ debug("unconfirmed txes = " + pprint.pformat(unconfirmed_txes))
+ debug("updated_scripthashes = " + str(updated_srchashes))
+ else:
+ debug("no updated txes")
+ return updated_srchashes
+
+def check_for_confirmations(rpc, address_history, unconfirmed_txes):
+ confirmed_txes_srchashes = []
+ debug("check4con unconfirmed_txes = " + pprint.pformat(unconfirmed_txes))
+ for uc_txid, srchashes in unconfirmed_txes.items():
+ tx = rpc.call("gettransaction", [uc_txid])
+ debug("uc_txid=" + uc_txid + " => " + str(tx))
+ if tx["confirmations"] == 0:
+ continue #still unconfirmed
+ log("A transaction confirmed: " + uc_txid)
+ confirmed_txes_srchashes.append((uc_txid, srchashes))
+ block = rpc.call("getblockheader", [tx["blockhash"]])
+ for srchash in srchashes:
+ #delete the old unconfirmed entry in address_history
+ deleted_entries = [h for h in address_history[srchash][
+ "history"] if h["tx_hash"] == uc_txid]
+ for d_his in deleted_entries:
+ address_history[srchash]["history"].remove(d_his)
+ #create the new confirmed entry in address_history
+ address_history[srchash]["history"].append({"height":
+ block["height"], "tx_hash": uc_txid})
+ updated_srchashes = set()
+ for tx, srchashes in confirmed_txes_srchashes:
+ del unconfirmed_txes[tx]
+ updated_srchashes.update(set(srchashes))
+ return updated_srchashes
+
+def check_for_unconfirmed_txes(rpc, address_history, unconfirmed_txes):
+ 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):
+ debug("listtransactions tx_request_count=" + str(tx_request_count))
+ ret = rpc.call("listtransactions", ["*", tx_request_count, 0, True])
+ ret = ret[::-1]
+ if last_known_recent_txid[0] == 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 == last_known_recent_txid[0][0] and
+ addr == last_known_recent_txid[0][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
+ debug("recent tx index = " + str(recent_tx_index) + " ret = " + str(ret))
+ # str([(t["txid"], t["address"]) for t in ret]))
+ if len(ret) > 0:
+ last_known_recent_txid[0] = (ret[0]["txid"], ret[0]["address"])
+ debug("last_known_recent_txid = " + str(last_known_recent_txid[0]))
+ assert(recent_tx_index != -1)
+ if recent_tx_index == 0:
+ return set()
+ new_txes = ret[:recent_tx_index][::-1]
+ debug("new txes = " + str(new_txes))
+ #tests: finding one unconfirmed tx, finding one confirmed tx
+ #sending a tx that has nothing to do with our wallets
+ #getting a new tx on a completely empty wallet
+ #finding a confirmed and unconfirmed tx, in that order, then both confirm
+ #finding an unconfirmed and confirmed tx, in that order, then both confirm
+ #send a tx to an address which hasnt been used before
+ 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 = \
+ get_input_and_output_scriptpubkeys(rpc, tx["txid"])
+
+ matching_scripthashes = []
+ for spk in (output_scriptpubkeys + input_scriptpubkeys):
+ scripthash = util.script_to_scripthash(spk)
+ if scripthash in address_history:
+ matching_scripthashes.append(scripthash)
+ if len(matching_scripthashes) == 0:
+ continue
+ updated_scripthashes.extend(matching_scripthashes)
+ new_history_element = generate_new_history_element(rpc, tx, txd)
+ log("Found new unconfirmed tx: " + str(new_history_element))
+ for srchash in matching_scripthashes:
+ address_history[srchash]["history"].append(new_history_element)
+ if new_history_element["height"] == 0:
+ if tx["txid"] in unconfirmed_txes:
+ unconfirmed_txes[tx["txid"]].append(srchash)
+ else:
+ unconfirmed_txes[tx["txid"]] = [srchash]
+ return set(updated_scripthashes)
+
+def build_address_history_index(rpc, wallet_addresses):
+ log("Building history index with " + str(len(wallet_addresses)) +
+ " addresses")
+ st = time.time()
+ address_history = {}
+ for addr in wallet_addresses:
+ scripthash = util.address_to_scripthash(addr)
+ address_history[scripthash] = {'addr': addr, '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 = rpc.call("listtransactions", ["*", BATCH_SIZE, t, True])
+ 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
+ obtained_txids.add(tx["txid"])
+
+ #obtain all the addresses this transaction is involved with
+ output_scriptpubkeys, input_scriptpubkeys, txd = \
+ get_input_and_output_scriptpubkeys(rpc, tx["txid"])
+ output_scripthashes = [util.script_to_scripthash(sc)
+ for sc in output_scriptpubkeys]
+ sh_to_add = wallet_addr_scripthashes.intersection(set(
+ output_scripthashes))
+ input_scripthashes = [util.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
+
+ new_history_element = generate_new_history_element(rpc, tx, txd)
+ for scripthash in sh_to_add:
+ address_history[scripthash][
+ "history"].append(new_history_element)
+ count += 1
+
+ unconfirmed_txes = {}
+ for srchash, his in address_history.items():
+ uctx = sort_address_history_list(his)
+ for u in uctx:
+ if u["tx_hash"] in unconfirmed_txes:
+ unconfirmed_txes[u["tx_hash"]].append(srchash)
+ else:
+ unconfirmed_txes[u["tx_hash"]] = [srchash]
+ debug("unconfirmed_txes = " + str(unconfirmed_txes))
+ if len(ret) > 0:
+ #txid doesnt uniquely identify transactions from listtransactions
+ #but the tuple (txid, address) does
+ last_known_recent_txid[0] = (ret[-1]["txid"], ret[-1]["address"])
+ else:
+ last_known_recent_txid[0] = None
+ debug("last_known_recent_txid = " + str(last_known_recent_txid[0]))
+
+ et = time.time()
+ log("Found " + str(count) + " txes. Address history index built in "
+ + str(et - st) + "sec")
+ debug("address_history =\n" + pprint.pformat(address_history))
+
+ return address_history, unconfirmed_txes
+
+def import_watchonly_addresses(rpc, addrs):
+ log("Importing " + str(len(addrs)) + " watch-only addresses into the"
+ + " Bitcoin node after 5 seconds . . .")
+ debug("addrs = " + str(addrs))
+ time.sleep(5)
+ for a in addrs:
+ rpc.call("importaddress", [a, ADDRESSES_LABEL, False])
+ #TODO tell people about the `rescanblockchain` call which allows a range
+ log("Done.\nIf recovering a wallet which already has existing " +
+ "transactions, then\nrestart Bitcoin with -rescan. If your wallet " +
+ "is new and empty then just restart this script")
+
+def main():
+ try:
+ config = ConfigParser()
+ config.read(["config.cfg"])
+ config.options("wallets")
+ except NoSectionError:
+ log("Non-existant configuration file `config.cfg`")
+ return
+ rpc = JsonRpc(host = config.get("bitcoin-rpc", "host"),
+ port = int(config.get("bitcoin-rpc", "port")),
+ user = config.get("bitcoin-rpc", "user"),
+ password = config.get("bitcoin-rpc", "password"))
+ #TODO somewhere here loop until rpc works and fully sync'd, to allow
+ # people to run this script without waiting for their node to fully
+ # catch up sync'd when getblockchaininfo blocks == headers, or use
+ # verificationprogress
+ printed_error_msg = False
+ while bestblockhash[0] == None:
+ try:
+ bestblockhash[0] = rpc.call("getbestblockhash", [])
+ except TypeError:
+ if not printed_error_msg:
+ log("Error with bitcoin rpc, check host/port/username/password")
+ printed_error_msg = True
+ time.sleep(5)
+ wallet_addresses = []
+ for key in config.options("wallets"):
+ addrs = config.get("wallets", key).replace(' ', ',').split(',')
+ wallet_addresses.extend(addrs)
+ wallet_addresses = set(wallet_addresses)
+ imported_addresses = set(rpc.call("getaddressesbyaccount",
+ [ADDRESSES_LABEL]))
+ if not wallet_addresses.issubset(imported_addresses):
+ import_watchonly_addresses(rpc, wallet_addresses - imported_addresses)
+ else:
+ address_history, unconfirmed_txes = build_address_history_index(
+ rpc, wallet_addresses)
+ hostport = (config.get("electrum-server", "host"),
+ int(config.get("electrum-server", "port")))
+ run_electrum_server(hostport, rpc, address_history, unconfirmed_txes,
+ int(config.get("bitcoin-rpc", "poll_interval_listening")),
+ int(config.get("bitcoin-rpc", "poll_interval_connected")))
+
+main()
diff --git a/util.py b/util.py
@@ -0,0 +1,365 @@
+
+import bitcoin as btc
+import hashlib, binascii
+from math import ceil, log
+
+## stuff copied from electrum's source
+
+def to_bytes(something, encoding='utf8'):
+ """
+ cast string to bytes() like object, but for python2 support
+ it's bytearray copy
+ """
+ if isinstance(something, bytes):
+ return something
+ if isinstance(something, str):
+ return something.encode(encoding)
+ elif isinstance(something, bytearray):
+ return bytes(something)
+ else:
+ raise TypeError("Not a string or bytes like object")
+
+def sha256(x):
+ x = to_bytes(x, 'utf8')
+ return bytes(hashlib.sha256(x).digest())
+
+def bh2u(x):
+ return binascii.hexlify(x).decode('ascii')
+
+def script_to_scripthash(script):
+ """Electrum uses a format hash(scriptPubKey) as the index keys"""
+ h = sha256(bytes.fromhex(script))[0:32]
+ return bh2u(bytes(reversed(h)))
+
+def address_to_scripthash(addr):
+ script = btc.address_to_script(addr)
+ return script_to_scripthash(script)
+
+#the 'result' field in the blockchain.scripthash.subscribe method
+# reply uses this as a summary of the address
+def get_status_electrum(h):
+ if not h:
+ return None
+ status = ''
+ for tx_hash, height in h:
+ status += tx_hash + ':%d:' % height
+ return bh2u(hashlib.sha256(status.encode('ascii')).digest())
+
+bfh = bytes.fromhex
+hash_encode = lambda x: bh2u(x[::-1])
+hash_decode = lambda x: bfh(x)[::-1]
+
+def Hash(x):
+ x = to_bytes(x, 'utf8')
+ out = bytes(sha256(sha256(x)))
+ return out
+
+def hash_merkle_root(merkle_s, target_hash, pos):
+ h = hash_decode(target_hash)
+ for i in range(len(merkle_s)):
+ item = merkle_s[i]
+ h = Hash(hash_decode(item) + h) if ((pos >> i) & 1) else Hash(
+ h + hash_decode(item))
+ return hash_encode(h)
+
+## end of electrum copypaste
+
+
+def script_to_address(script):
+ #TODO bech32 addresses
+ #TODO testnet, although everything uses scripthash so the address vbyte doesnt matter
+ return btc.script_to_address(script, 0x00)
+
+def calc_tree_width(height, txcount):
+ return (txcount + (1 << height) - 1) >> height
+
+#follow the flags down into the tree, building up the datastructure
+def decend_merkle_tree(hashes, flags, height, txcount, pos):
+ flag = next(flags)
+ print("f=" + str(flag) + " height=" + str(height) + " txc=" +
+ str(txcount) + " pos=" + str(pos) + " width=" +
+ str(calc_tree_width(height, txcount)))
+ if height > 0:
+ #non-txid node
+ if flag:
+ left = decend_merkle_tree(hashes, flags, height-1, txcount, pos*2)
+ #bitcoin has a rule that if theres an odd number of nodes in
+ # the merkle tree, the last hash is duplicated
+ if pos*2+1 < calc_tree_width(height-1, txcount):
+ right = decend_merkle_tree(hashes, flags, height-1,
+ txcount, pos*2+1)
+ else:
+ right = left
+ #TODO decend down one branch and hash it up, place in right
+ return (left, right)
+ else:
+ hs = next(hashes)
+ hs = hs[:4] + '...' + hs[-4:]
+ print(hs)
+ return hs
+ else:
+ #txid node
+ hs = next(hashes)
+ hs = hs[:4] + '...' + hs[-4:]
+ print(hs)
+ if flag:
+ return "tx:" + str(pos) + ":" + hs
+ else:
+ return hs
+
+def deserialize_core_format_merkle_proof(hash_list, flag_value, txcount):
+ 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
+
+#recurse down into the tree, adding hashes to the result list in depth order
+def expand_tree_electrum_format(node, result):
+ left, right = node
+ if isinstance(left, tuple):
+ expand_tree_electrum_format(left, result)
+ if isinstance(right, tuple):
+ expand_tree_electrum_format(right, result)
+ if not isinstance(left, tuple):
+ result.append(left)
+ if not isinstance(right, tuple):
+ result.append(right)
+
+#https://github.com/bitcoin/bitcoin/blob/master/src/merkleblock.h
+#https://github.com/breadwallet/breadwallet-core/blob/master/BRMerkleBlock.c
+def convert_core_to_electrum_merkle_proof(proof):
+ 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]]
+
+ pos[0] = 80
+ txcount = read_as_int(4)
+ hash_count = read_var_int()
+ hashes = [binascii.hexlify(read_bytes(32)[::-1]).decode()
+ for i in range(hash_count)]
+ flags_count = read_var_int()
+ flags = read_bytes(flags_count)
+
+ print(hashes)
+ print([flags[i//8]&1 << i%8 != 0 for i in range(len(flags)*8)])
+ print(txcount)
+
+ root_node = deserialize_core_format_merkle_proof(hashes, flags, txcount)
+ print(root_node)
+ hashes_list = []
+ expand_tree_electrum_format(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]
+ pos, txid = tx.split(":")[1:3]
+ pos = int(pos)
+ blockhash = binascii.hexlify(btc.bin_dbl_sha256(proof[:80])[::-1])
+ result = {"pos": pos, "merkle": hashes_list, "txid": txid,
+ "blockhash": blockhash.decode()}
+ return result
+
+merkle_test_vectors = [
+ {'coreproof':
+ "0300000026e696fba00f0a43907239305eed9e55824e0e376636380f00000000000" +
+ "000004f8a2ce51d6c69988029837688cbfc2f580799fa1747456b9c80ab808c1431" +
+ "acd0b07f5543201618cadcfbf7330300000b0ff1e0050fed22ca360e0935e053b0f" +
+ "e098f6f9e090f5631013361620d964fe2fd88544ae10b40621e1cd24bb4306e3815" +
+ "dc237f77118a45d75ada9ee362314b70573732bce59615a3bcc1bbacd04b33b7819" +
+ "198212216b5d62d75be59221ada17ba4fb2476b689cccd3be54732fd5630832a94f" +
+ "11fa3f0dafd6f904d43219e0d7de110158446b5b598bd241f7b5df4da0ebc7d30e7" +
+ "748d487917b718df51c681174e6abab8042cc7c1c436221c098f06a56134f9247a8" +
+ "12126d675d69c82ba1c715cfc0cde462fd1fbe5dc87f6b8db2b9c060fcd59a20e7f" +
+ "e8e921c3676937a873ff88684f4be4d015f24f26af6d2cf78335e9218bcceba4507" +
+ "d0b4ba6cb933aa01ef77ae5eb411893ec0f74b69590fb0f5118ac937c02ccd47e9d" +
+ "90be78becd11ecf854d7d268eeb479b74d137278c0a5017d29e90cd5b35a4680201" +
+ "824fb0eb4f404e20dfeaec4d50549030b7e7e220b02eb2105f3d2e8bcc94d547214" +
+ "a9d03ff1600",
+ 'electrumproof':
+ {'pos': 5, 'merkle': [
+ '4b3162e39eda5ad7458a11777f23dc15386e30b44bd21c1e62400be14a5488fd',
+ 'e01932d404f9d6af0d3ffa114fa9320863d52f7354bed3cc9c686b47b24fba17',
+ 'e24f960d6261330131560f099e6f8f09feb053e035090e36ca22ed0f05e0f10f',
+ '681cf58d717b9187d448770ed3c7eba04ddfb5f741d28b595b6b44580111ded7',
+ 'a12bc8695d676d1212a847924f13566af098c02162431c7ccc4280ababe67411',
+ '7a9376361c928efee7209ad5fc60c0b9b28d6b7fc85dbe1ffd62e4cdc0cf15c7',
+ '33b96cbab4d00745bacebc18925e3378cfd2f66af2245f014dbef48486f83f87',
+ 'ec8be70bd9e947cd2cc037c98a11f5b00f59694bf7c03e8911b45eae77ef01aa',
+ 'b04f82010268a4355bcd909ed217500a8c2737d1749b47eb8e267d4d85cf1ed1',
+ '9d4a2147d594cc8b2e3d5f10b22eb020e2e7b7309054504deceadf204e404feb'],
+ 'txid':
+ 'da1a2259be752dd6b5162221989181b7334bd0acbbc1bca31596e5bc32375770',
+ 'blockhash':
+ "000000000000000014491e51be24278716c24d12ec0dbadf8c5f04f7f1846f5a"}
+ },
+ {"coreproof":
+ "0100000053696a625fbd16df418575bce0c4148886c422774fca5fcab8010000000" +
+ "000001532bfe4f9c4f56cd141028e5b59384c133740174b74b1982c7f01020b90ce" +
+ "05577c67508bdb051a7ec2ef942f000000076cde2eb7efa90b36d48aed612e559ff" +
+ "2ba638d8d400b14b0c58df00c6a6c33b65dc8fa02f4ca56e1f4dcf17186fa9bbd99" +
+ "0ce150b6e2dc9e9e56bb4f270fe56fde6bdd73a7a7e82767714862888e6b759568f" +
+ "b117674ad23050e2931197494d457efb72efdb9cb79cd4a435724908a0eb31ec7f7" +
+ "a67ee03837319e098b43edad3be9af75ae7b30db6f4f93ba0fdd941fdf70fe8cc38" +
+ "982e03bd292f5bd02f28137d343f908c7d6417379afe8349a257af3ca1f74f623be" +
+ "6a416fe1aa96a8f259983f2cf32121bce203955a378b3b44f132ea6ab94c7829a6c" +
+ "3b360c9f8da8e74027701",
+ "electrumproof":
+ {'pos': 9, 'merkle': [
+ '6fe50f274fbb569e9edce2b650e10c99bd9bfa8671f1dcf4e156caf402fac85d',
+ 'aded438b099e313738e07ea6f7c71eb30e8a902457434acd79cbb9fd2eb7ef57',
+ '81f202bdf592d23be08289c38cfe70df1f94dd0fba934f6fdb307bae75afe93b',
+ 'b6336c6a0cf08dc5b0140b408d8d63baf29f552e61ed8ad4360ba9efb72ede6c',
+ '59f2a896aae16f416abe23f6741fcaf37a259a34e8af797341d6c708f943d337',
+ '748edaf8c960b3c3a629784cb96aea32f1443b8b375a9503e2bc2121f32c3f98'],
+ 'txid':
+ 'd494741931290e0523ad747611fb6895756b8e886248716727e8a7a773dd6bde',
+ "blockhash":
+ "000000000000028113c80cc4be7058ab80a7767329d0253558d81d709f62ca40"}
+ },
+ {"coreproof":
+ "000000206365d5e1d8b7fdf0b846cfa902115c1b8ced9dd49cb1780000000000000" +
+ "000001032e829e1f9a5a09d0492f9cd3ec0762b7facea555989c3927d3d975fd407" +
+ "8c7718495a45960018edd3b9e0160a00000dfe856a7d5d77c23ebf85c68b5eb303d" +
+ "85e56491ed6d204372625d0b4383df5a44d6e46d2db09d936b9f5d0b53e0dbcb3ef" +
+ "b7773d457369c228fd1ce6e11645e366a58b3fc1e8a7c916710ce29a87265a6729a" +
+ "3b221b47ea9c8e6f48707b112b8d67e5cfb3db5f88b042dc49e4e5bc2e61c28e1e0" +
+ "fbcba4c741bb5c75cac58ca04161a7377d70f3fd19a3e248ed918c91709b49afd37" +
+ "60f89ed2fefbcc9c23447ccb40a2be7aba22b07189b0bf90c62db48a9fe37227e12" +
+ "c7af8c1d4c22f9f223530dacdd5f3ad850ad4badf16cc24049a65334f59bf28c15c" +
+ "ecda1a4cf3f2937bd70ee84f66569ce8ef951d50cca46d60337e6c697685b38ad21" +
+ "7967bbe6801d03c44fcb808cd035be31888380a2df1be14b6ff100de83cab0dce25" +
+ "0e2b40ca3b47e8309f848646bee63b6185c176d84f1546a482e7a65a87d1a2d0d5a" +
+ "2b683e2cae0520df1e3525a71d71e1f551abd7d238c3bcb4ecaeea7d5988745fa42" +
+ "1a8604a99857426957a2ccfa7cd8df145aa8293701989dd207505923fcb33984394" +
+ "4ce3d21dc259bcda9c251ed90d4e55af2cf5b15432050084f513ac74c0bdd4b6046" +
+ "fb70100",
+ "electrumproof":
+ {'pos': 330, 'merkle': [
+ '23f2f9224c1d8cafc7127e2237fea948db620cf90b9b18072ba2abe72b0ab4cc',
+ 'a08cc5ca755cbb41c7a4cbfbe0e1281ce6c25b4e9ec42d048bf8b53dfb5c7ed6',
+ '37293fcfa4a1cdce158cf29bf53453a64940c26cf1ad4bad50d83a5fddac0d53',
+ 'b812b10787f4e6c8a97eb421b2a329675a26879ae20c7116c9a7e8c13f8ba566',
+ '1d80e6bb677921ad385b6897c6e63703d646ca0cd551f98ece6965f684ee70bd',
+ 'a30cb4e250e2dcb0ca83de00f16f4be11bdfa280838831be35d08c80cb4fc403',
+ 'e34516e1e61cfd28c26973453d77b7efb3bc0d3eb5d0f5b936d909dbd2466e4d',
+ '3e682b5a0d2d1a7da8657a2e486a54f1846d175c18b663ee6b6448f809837eb4',
+ 'a4f53d38b4d025263704d2d61e49565ed803b35e8bc685bf3ec2775d7d6a85fe',
+ 'a821a45f7488597deaaeecb4bcc338d2d7ab51f5e1711da725351edf2005ae2c',
+ '94439833cb3f92057520dd8919709382aa45f18dcda7cf2c7a95267485994a60',
+ 'b6d4bdc074ac13f58400053254b1f52caf554e0dd91e259cdabc59c21dd2e34c'],
+ 'txid':
+ '4734c2c9bcef2fed890f76d3af499b70918c91ed48e2a319fdf3707d37a76141',
+ "blockhash":
+ "00000000000000000035c1e0b8f6c7886a5d41b685c4f0094a5b91759a5fe235"}
+ }
+]
+
+#response electrum3.hachre.de {'result': {'pos': 2860, 'block_height': 503961, 'merkle': ['590e0d07c8d33b0453748d1034d3fd4e779e1e78a2c8ef20c2e66830a6d4230d', '0f1d4d6aaa71beaf8d30b7df3cd776ece4bcd1169d1550e8abfb2b3053388ac8', 'cbd44606e7d8ca49ccaa409a51b60854d6e31534c5c6315a257ef571f1398db3', '7d4d426bb8a3b5146b0c35845b7e12dc7bcd7f44c570ff712632d0d86b695cbd', '20e5e6a7eb7cf42e4d3a9ac803f160973b10da3da74d68afb8bfef04d9a46d85', '9032b3b57d81862168733b5a6b6370eaeafb4aaaea5023bf4cf3a998f8ca67e2', 'a16ed5aa6bab2c9b64e91f033aa1fdffa44270f0907aeb8eedd31840514f8f26', 'a53a1448437ac49c9f560f3e5c4a769c6295df2a04242b713d1f0747d90a8fe4', '6922f4bd74e95ae28fcd71c35dfb95e4551876ba78cb828cbc863870b34add53', 'bf152261c5f22dc73cb2fe5ee85984f0c8d71ab8db28bd0e39931f43d6766f1e', '2cbe3c851f5a58e2a407bf38bb829fde76e4fd22005b5c3124d3eff4de55c3a5', '0b7ceffc6a25d3b3c0619fd2d254881e8987f9182c3fb12bf5db14311cd7208d']}, 'method': 'blockchain.transaction.get_merkle', 'id': 32, 'jsonrpc': '2.0', 'params': ['590e0d07c8d33b0453748d1034d3fd4e779e1e78a2c8ef20c2e66830a6d4230d', 503961]}
+#proof = "00000020c656c90b521a2bbca14174f2939b882a28d23d86144b0e000000000000000000cf5185a8e369c3de5f15e039e777760994fd66184b619d839dace3aec9953fd6d861595ac1910018ee097a972d0b0000078d20d71c3114dbf52bb13f2c18f987891e8854d2d29f61c0b3d3256afcef7c0b1e6f76d6431f93390ebd28dbb81ad7c8f08459e85efeb23cc72df2c5612215bf53dd4ab3703886bc8c82cb78ba761855e495fb5dc371cd8fe25ae974bdf42269e267caf898a9f34cbf2350eaaa4afbeaea70636b5a3b73682186817db5b33290bd5c696bd8d0322671ff70c5447fcd7bdc127e5b84350c6b14b5a3b86b424d7db38d39f171f57e255a31c6c53415e3d65408b6519a40aacc49cad8e70646d4cb0d23d4a63068e6c220efc8a2781e9e774efdd334108d7453043bd3c8070d0e5903ad5b07"
+
+#has 7 txes
+# 0000000000007d1bdd2cfd23ffb3c2bae3143772bd6577aecae9c6b29f88c2af
+#lasttx c40bbed8f34cb1c24660e2e0cb51e09a180f1ab97037265293fceab88247bccf
+#addr 15dcVuX7UT4fB74dikaAE4MXhCTkFZpV8F
+#response electrum3.hachre.de {'id': 33, 'params': ['c40bbed8f34cb1c24660e2e0cb51e09a180f1ab97037265293fceab88247bccf', 120013], 'result': {'block_height': 120013, 'pos': 6, 'merkle': ['c40bbed8f34cb1c24660e2e0cb51e09a180f1ab97037265293fceab88247bccf', 'ad69c91b8e9b7122dc2a2575cfa12a36de05595e0e8f59092d04b263b4c8f70f', '8ae24d1f1c3b0d65ec88f8c84cad7e02e98b26d7ad566bf3653158b72ebb3acd']}, 'jsonrpc': '2.0', 'method': 'blockchain.transaction.get_merkle'}
+#proof = "0100000056e02c6d3278c754e0699517834741f7c4ad3dcbfeb7803a3462000000000000af3bdd5dd465443fd003e9281455e60aae573dd4d46304d7ba17276ea33d506488cbb44dacb5001b9ebb193b0700000003cd3abb2eb7583165f36b56add7268be9027ead4cc8f888ec650d3b1c1f4de28a0ff7c8b463b2042d09598f0e5e5905de362aa1cf75252adc22719b8e1bc969adcfbc4782b8eafc9352263770b91a0f189ae051cbe0e26046c2b14cf3d8be0bc40135"
+
+#has 6 txes
+# 00000000000005163d8d16192985a3f2d0f6f44e668ad05b26f7edcd3385a37f
+# last tx eaefedc6dbb37223c771d8ccbbe4dac9e9d646ab90f17e387b63c866fad6e2c3
+# addr 1NwNmR7sd6NqxXBJMXrwt9yUms29pSDmm
+#response electrum3.hachre.de {'jsonrpc': '2.0', 'id': 33, 'method': 'blockchain.transaction.get_merkle', 'result': {'pos': 5, 'block_height': 150106, 'merkle': ['1f12a4c866548ab51766172f97a6741fbd62834ddfcadba249909ea8150eca88', 'f5a5aa78bd1f1ee5de900b7d1928864912425b67ece4a07e40af8eeb86f10d94', 'd52e599bc0ecc5e17bcb1e7539b61586c7457170923eab6d36243995ed452bf5']}, 'params': ['eaefedc6dbb37223c771d8ccbbe4dac9e9d646ab90f17e387b63c866fad6e2c3', 150106]}
+proof = "01000000299edfd28524eae4fb6012e4087afdb6e1b912db85e612374b03000000000000e16572394f8578a47bf36e15cd16faa5e3b9e18805cf4e271ae4ef95aa8cea7eb31fa14e4b6d0b1a42857d960600000003f52b45ed953924366dab3e92707145c78615b639751ecb7be1c5ecc09b592ed588ca0e15a89e9049a2dbcadf4d8362bd1f74a6972f176617b58a5466c8a4121fc3e2d6fa66c8637b387ef190ab46d6e9c9dae4bbccd871c72372b3dbc6edefea012d"
+
+'''
+ix = 1
+try:
+ #proof = merkle_test_vectors[ix]['coreproof']
+ merkleproof = convert_core_to_electrum_merkle_proof(proof)
+ print(merkleproof)
+except ValueError:
+ print("valueerror")
+'''
+
+'''
+h1 = b"1f12a4c866548ab51766172f97a6741fbd62834ddfcadba249909ea8150eca88"
+h2 = b"eaefedc6dbb37223c771d8ccbbe4dac9e9d646ab90f17e387b63c866fad6e2c3"
+h1 = binascii.unhexlify(h1)[::-1]
+h2 = binascii.unhexlify(h2)[::-1]
+print(btc.dbl_sha256(h2 + h1))
+
+merkle_s = {'pos': 5, 'block_height': 150106, 'merkle':
+['1f12a4c866548ab51766172f97a6741fbd62834ddfcadba249909ea8150eca88',
+'f5a5aa78bd1f1ee5de900b7d1928864912425b67ece4a07e40af8eeb86f10d94',
+'d52e599bc0ecc5e17bcb1e7539b61586c7457170923eab6d36243995ed452bf5']}
+'''
+
+
+'''
+ix = 1
+electrumproof = merkle_test_vectors[ix]['electrumproof']
+print("txid = " + electrumproof['txid'])
+print("mkpf = " + hash_merkle_root(electrumproof["merkle"],
+ electrumproof['txid'], electrumproof["pos"]))
+'''
+
+
+'''
+ merkle_test_vectors[ix]['coreproof'])
+assert(merkleproof['pos'] ==
+ merkle_test_vectors[ix]["electrumproof"]["pos"])
+assert(merkleproof['blockhash'] ==
+ merkle_test_vectors[ix]["electrumproof"]["blockhash"])
+assert(len(merkleproof["merkle"]) ==
+ len(merkle_test_vectors[ix]["electrumproof"]["merkle"]))
+for i in range(len(merkleproof["merkle"])):
+ assert(merkleproof["merkle"][i] ==
+ merkle_test_vectors[ix]["electrumproof"]["merkle"][i])
+'''
+
+'''
+def chunks(d, n):
+ return [d[x:x + n] for x in range(0, len(d), n)]
+
+print(merkleproof)
+print("\" + \n\"".join(chunks(proof, 67)))
+'''
+
+
+#has 15
+# 000000000000b2847f688808836c3905fab245cf8081befb11d1422ad59be780
+#should get a block with 13 txes
+#get a block with 1tx, only the coinbase
+
+'''
+print(address_to_scripthash(addr))
+print(spkhash + " should be")
+
+print(get_status([(txhash, txheight)]))
+print(history_hash + " should be")
+
+print(get_status([(r['tx_hash'], r['height']) for r in history['result']]))
+print(history_status + " should be")
+'''