You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
425 lines
18 KiB
425 lines
18 KiB
"""Module to support bitcoin operations using a |
|
different codebase than joinmarket's own. |
|
""" |
|
#Protocol constants |
|
BTC_P2PK_VBYTE = {"mainnet": 0x00, "testnet": 0x6f} |
|
BTC_P2SH_VBYTE = {"mainnet": 0x05, "testnet": 0xc4} |
|
PODLE_COMMIT_FILE = None |
|
|
|
from base.support import get_log |
|
import binascii, sys, re, hashlib, base64 |
|
from pprint import pformat |
|
log = get_log() |
|
|
|
#Required only for PoDLE calculation: |
|
N = 115792089237316195423570985008687907852837564279074904382605163141518161494337 |
|
|
|
interface = "joinmarket-joinmarket" |
|
|
|
try: |
|
from bitcoin import * |
|
bjm = True |
|
except ImportError: |
|
#TODO figure out the right flexibility structure |
|
|
|
interface = "joinmarket-electrum" |
|
|
|
if interface != "joinmarket-electrum": |
|
raise NotImplementedError |
|
|
|
not_supported_string = "not supported by: " + interface |
|
|
|
#Electrum specific code starts here |
|
import electrum.bitcoin as ebt |
|
import electrum.transaction as etr |
|
from ecdsa.ecdsa import curve_secp256k1, generator_secp256k1, point_is_valid |
|
from ecdsa.util import string_to_number, sigdecode_der, sigencode_der |
|
from ecdsa import VerifyingKey, BadSignatureError, BadDigestError |
|
from ecdsa.curves import SECP256k1 |
|
from ecdsa.numbertheory import square_root_mod_prime |
|
from ecdsa.ellipticcurve import Point |
|
|
|
class PPubKey(object): |
|
def __init__(self, serP): |
|
self._point = ebt.ser_to_point(serP) |
|
def serialize(self): |
|
return ebt.point_to_ser(self._point) |
|
|
|
class PPrivKey(object): |
|
def __init__(self, scalar): |
|
self._privkey = ebt.EC_KEY(scalar) |
|
self.private_key = scalar |
|
self.pubkey = PPubKey(binascii.unhexlify( |
|
self._privkey.get_public_key())) |
|
|
|
podle_PublicKey_class = PPubKey |
|
podle_PrivateKey_class = PPrivKey |
|
|
|
def podle_PublicKey(P): |
|
return PPubKey(P) |
|
|
|
def podle_PrivateKey(priv): |
|
return PPrivKey(priv) |
|
|
|
def multiply(s, pub, usehex, rawpub=True, return_serialized=True): |
|
"""s should be 32 byte scalar, pub should be of type |
|
podle_PublicKey_class |
|
""" |
|
if usehex: |
|
s = binascii.unhexlify(s) |
|
pub = binascii.unhexlify(pub) |
|
ppub = PPubKey(pub) |
|
p = ppub._point |
|
s_int = decode(s, 256) |
|
m = p * s_int |
|
r = PPubKey(ebt.point_to_ser(m)) |
|
if return_serialized: |
|
return r.serialize() |
|
return r |
|
|
|
def add_pubkeys(pubkeys, usehex): |
|
"""Pubkeys should be a list (for compatibility). |
|
""" |
|
#Not supporting more than 2 items for now, not needed. |
|
assert len(pubkeys) == 2 |
|
if usehex: |
|
pubkeys = [binascii.unhexlify(x) for x in pubkeys] |
|
p1pt, p2pt = [ebt.ser_to_point(x) for x in pubkeys] |
|
sumpt = p1pt + p2pt |
|
return ebt.point_to_ser(sumpt) |
|
|
|
def getG(compressed=True): |
|
scalar = "\x00"*31 + "\x01" |
|
return binascii.unhexlify( |
|
ebt.EC_KEY(scalar).get_public_key(compressed=compressed)) |
|
|
|
def sign(tx): |
|
#transaction signing is handled by the wallet for Electrum |
|
raise NotImplementedError("sign " + not_supported_string) |
|
|
|
def get_version_byte(inp): |
|
leadingzbytes = len(re.match('^1*', inp).group(0)) |
|
data = b'\x00' * leadingzbytes + b58check_to_bin(inp) |
|
return ord(data[0]) |
|
|
|
def b58check_to_bin(addr): |
|
return ebt.DecodeBase58Check(addr)[1:] |
|
|
|
def changebase(inp, frm=256, to=58): |
|
"""Implementation of base58 (*not* b58check) conversion |
|
only. Used in message channel verifiable nick construction. |
|
Explicitly disabling any other conversion for now. |
|
""" |
|
if not (frm==256 and to==58): |
|
raise NotImplementedError |
|
return ebt.base_encode(inp, 58) |
|
|
|
def address_to_script(addr): |
|
return etr.Transaction.pay_script(ebt.TYPE_ADDRESS, addr) |
|
|
|
def script_to_address(script): |
|
bin_script = binascii.unhexlify(script) |
|
res = etr.get_address_from_output_script(bin_script) |
|
if not res[0] == ebt.TYPE_ADDRESS: |
|
raise ValueError("Invalid script for bitcoin address") |
|
return res[1] |
|
|
|
def bin_dbl_sha256(x): |
|
return ebt.sha256(ebt.sha256(x)) |
|
|
|
def dbl_sha256(x): |
|
return binascii.hexlify(bin_dbl_sha256(x)) |
|
|
|
def verify_tx_input(tx, i, script, sig, pub): |
|
pub, sig, script = (binascii.unhexlify(x) for x in [pub, sig, script]) |
|
t = etr.Transaction(tx) |
|
t.deserialize() |
|
#to prepare for verification (to do the txhash for modtx) |
|
#we need to have the "address" field set in the input. |
|
typ, addr = etr.get_address_from_output_script(script) |
|
if not typ == ebt.TYPE_ADDRESS: |
|
#Don't support non-p2sh, non-p2pkh for now |
|
log.debug("Invalid script") |
|
return False |
|
t.inputs()[i]["address"] = addr |
|
txforsig = etr.Hash(t.tx_for_sig(i).decode('hex')) |
|
ecdsa_pub = get_ecdsa_verifying_key(pub) |
|
if not ecdsa_pub: |
|
return False |
|
try: |
|
verified = ecdsa_pub.verify_digest(sig, txforsig, |
|
sigdecode = sigdecode_der) |
|
except BadSignatureError, BadDigestError: |
|
return False |
|
return True |
|
|
|
def get_ecdsa_verifying_key(pub): |
|
#some shenanigans required to validate a transaction sig; see |
|
#python.ecdsa PR #54. This will be a lot simpler when that's merged. |
|
#https://github.com/warner/python-ecdsa/pull/54/files |
|
if not pub[0] in ["\x02", "\x03"]: |
|
log.debug("Invalid pubkey") |
|
return None |
|
is_even = pub.startswith('\x02') |
|
x = string_to_number(pub[1:]) |
|
order = SECP256k1.order |
|
p = SECP256k1.curve.p() |
|
alpha = (pow(x, 3, p) + (SECP256k1.curve.a() * x) + SECP256k1.curve.b()) % p |
|
beta = square_root_mod_prime(alpha, p) |
|
if is_even == bool(beta & 1): |
|
y = p - beta |
|
else: |
|
y = beta |
|
if not point_is_valid(SECP256k1.generator, x, y): |
|
return None |
|
|
|
point = Point(SECP256k1.curve, x, y, order) |
|
return VerifyingKey.from_public_point(point, SECP256k1, |
|
hashfunc=hashlib.sha256) |
|
|
|
def ecdsa_verify(msg, sig, pub, usehex=True): |
|
sig = base64.b64decode(sig) |
|
if usehex: |
|
pub = binascii.unhexlify(pub) |
|
verif_key = get_ecdsa_verifying_key(pub) |
|
return verif_key.verify_digest(sig, |
|
ebt.Hash(ebt.msg_magic(msg)), |
|
sigdecode = sigdecode_der) |
|
|
|
def ecdsa_sign(msg, priv, usehex=True): |
|
if usehex: |
|
priv = binascii.unhexlify(priv) |
|
compressed = False |
|
if len(priv) == 33 and priv[-1]=="\x01": |
|
compressed = True |
|
signkey = ebt.EC_KEY(priv[:32]) |
|
private_key = ebt.MySigningKey.from_secret_exponent(signkey.secret, |
|
curve=SECP256k1) |
|
sig = private_key.sign_digest_deterministic(ebt.Hash(ebt.msg_magic(msg)), |
|
hashfunc=hashlib.sha256, |
|
sigencode = sigencode_der) |
|
return base64.b64encode(sig) |
|
|
|
def serialize(txobj): |
|
#It is a rather chunky matter to re-use electrum.transaction code |
|
#to do serialization, it has a very different approach. Hence some |
|
#code duplication here with bitcoin-joinmarket. However we use the |
|
#number encoding functions from Electrum. Also, this is always in hex. |
|
o = [] |
|
o.append(ebt.int_to_hex(txobj["version"], 4)) |
|
o.append(ebt.var_int(len(txobj["ins"]))) |
|
for inp in txobj["ins"]: |
|
binhash = binascii.unhexlify(inp["outpoint"]["hash"]) |
|
binhash = binhash[::-1] |
|
o.append(binascii.hexlify(binhash)) |
|
o.append(ebt.int_to_hex(inp["outpoint"]["index"], 4)) |
|
o.append(ebt.var_int(len(inp["script"])/2) + inp["script"]) |
|
o.append(ebt.int_to_hex(inp["sequence"], 4)) |
|
o.append(ebt.var_int(len(txobj["outs"]))) |
|
for out in txobj["outs"]: |
|
o.append(ebt.int_to_hex(out["value"], 8)) |
|
o.append(ebt.var_int(len(out["script"])/2) + out["script"]) |
|
o.append(ebt.int_to_hex(txobj["locktime"], 4)) |
|
return ''.join(o) |
|
|
|
def deserialize_script(scriptSig): |
|
#Assumes P2PKH scriptSig |
|
d = {} |
|
etr.parse_scriptSig(d, binascii.unhexlify(scriptSig)) |
|
return (d["signatures"][0], d["pubkeys"][0]) |
|
|
|
def deserialize(txhex): |
|
t = etr.deserialize(txhex) |
|
#translation from Electrum deserialization |
|
#to pybitcointools form as used in joinmarket |
|
#pybitcointools structure: |
|
#obj = {"ins": [..], "outs": [..], "locktime": int} |
|
#where ins elements are: |
|
#{"outpoint": {"hash": bigendian32,"index": int}, |
|
#"script": hex,"sequence": int} |
|
#and outs elements are: |
|
#{"script": hex, "value": int} |
|
# |
|
#while electrum.transaction.deserialize returns object |
|
#like: |
|
#{"version": int, "inputs": [..], "outputs": [..], "lockTime": int} |
|
obj = {} |
|
obj["version"] = t["version"] |
|
obj["locktime"] = t["lockTime"] |
|
obj["ins"] = [] |
|
obj["outs"] = [] |
|
for i in t["inputs"]: |
|
outpoint = {"hash": i["prevout_hash"], "index": i["prevout_n"]} |
|
scr = i["scriptSig"] |
|
sequence = i["sequence"] |
|
obj["ins"].append({"outpoint": outpoint, "script": scr, "sequence": sequence}) |
|
for i in t["outputs"]: |
|
obj["outs"].append({"script": i["scriptPubKey"], "value": i["value"]}) |
|
return obj |
|
|
|
def privkey_to_pubkey(privkey, usehex=True): |
|
if usehex: |
|
privkey = binascii.unhexlify(privkey) |
|
if len(privkey)==33 and privkey[-1] == "\x01": |
|
compressed = True |
|
privkey = privkey[:32] |
|
elif len(privkey)==32: |
|
compressed=False |
|
else: |
|
raise ValueError("Invalid private key") |
|
sec = ebt.SecretToASecret(privkey, compressed=compressed, |
|
addrtype=BTC_P2PK_VBYTE["mainnet"]) |
|
|
|
retval = ebt.public_key_from_private_key(sec) |
|
if usehex: |
|
return retval |
|
return binascii.unhexlify(retval) |
|
|
|
privtopub = privkey_to_pubkey |
|
|
|
def privkey_to_address(privkey, magicbyte=0): |
|
pubkey = privkey_to_pubkey(privkey) |
|
return pubkey_to_address(pubkey, magicbyte) |
|
|
|
privtoaddr = privkey_to_address |
|
|
|
def pubkey_to_address(pub, magicbyte=0): |
|
h160 = ebt.hash_160(pub.decode('hex')) |
|
return ebt.hash_160_to_bc_address(h160, addrtype=magicbyte) |
|
|
|
pubtoaddr = pubkey_to_address |
|
|
|
def from_wif_privkey(privkey, vbyte=0): |
|
#converts a WIF compressed privkey to a hex private key |
|
return binascii.hexlify(ebt.ASecretToSecret(privkey, addrtype=vbyte)) |
|
|
|
def txhash(txhex): |
|
t = etr.Transaction(txhex) |
|
return t.hash() |
|
|
|
#A simple copy-paste for now; move into support.py perhaps? TODO |
|
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") |
|
|
|
def mktx(ins, outs): |
|
#Copy-paste from bitcoin-joinmarket |
|
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 not isinstance(o, dict): |
|
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 set_commitment_file(file_location): |
|
global PODLE_COMMIT_FILE |
|
PODLE_COMMIT_FILE = file_location |
|
|
|
def test_btc(): |
|
#Sign and verify test (for message signing in joinmarket handshake) |
|
print("Using interface " + interface) |
|
priv = dbl_sha256("hello") + "01" |
|
x = ecdsa_sign("helloxxx", priv) |
|
log.debug("Got: " + x) |
|
y = ecdsa_verify("helloxxx", x, privkey_to_pubkey(priv)) |
|
log.debug("Sig ver: " + str(y)) |
|
assert y |
|
|
|
#address/script conversion test |
|
test_addr = "1LT6rwv26bV7mgvRosoSCyGM7ttVRsYidP" |
|
#Electrum has no support for testnet! |
|
#test_test_addr = "mgvipZr8kX7fZFQU7QsKTCJT9QCfaiswV7" |
|
assert script_to_address(address_to_script(test_addr))==test_addr |
|
assert get_version_byte(test_addr)==0 |
|
|
|
#Transaction creation test. |
|
raw_valid_tx = "01000000064cdfe43ad43b187b738644363144784a09bf6d408012409cf9934591109a789b060000006b483045022100d4309edbb8253e62fb59462f2ff5c3445923e0299bf1a15ac5f7db3da5752bee022066f3f219de7e6ee56c3d600da757ec1051cbd11b42969b8935ae35642b6a2e84012102e94b49525342110266a1dc7651221507318c4cb914ede004b3098650e9b951b6ffffffffc2a9b3e8285c2e7aaee2ea50f792172c920c43a675fa8e8d70976727c8752adf030000006a47304402202763d8ad9e41c99c5af587c69d267493773dc9567519a64db8b707af5daf07f0022011729c6d241ad5abe48687d084644bd442b5f9038db04fb28da674126183aca5012102d2cbeb9386fd201bc6eecf27b2858f7bc27462cd9b43ae464e9ef3281f97a3e0ffffffffa787e89792a93111ff08f5a083234c7c2410bd69b6eef42be0fc5f026a3a1cf0030000006b483045022100c3b86d7acadf1be3d8ea6706daedb842b09732621e830440481370d423703741022009fd0f90a07babd481f1011ec883b2aa248c6a4a433599c5b203c6b93fc03b67012103f9a47d3958281b6749921fdf6d9edde0176342c00ced7caacab9ab3a64795086ffffffff23fb90cebcb1784a7a4a0a35489356ba64cf95c0afdc5a0f0184dc22668ff41f050000006b483045022100ea698e5952e23ffdf6d58bdc73e91c555867e3ad99ac9b583f492882395ace9a0220705abe597972d45923fe0515695dd7b99dcfa50e69d49c03a8126180fd263bc70121036532aa886851548a5b62bff29b4c36bfdc33e68c7dbee8efb4b440e50c5ebc6effffffffd401de8afd8fd323ab6abd9db1d261ac69e7c1d2be7f1a40004e7659b7d6cd9b030000006b483045022100b09c4e7f227f2f86d1965edbc4c92b9058243300f3bc62a3169591aacb60ca4d0220390d0d7ae2ee7dab200e166337c65d4a62b576dc4fa138ce40efd240c57346fc0121034cd59665d736d927d9613c7624f8d616d483b06ab8993446f6119f18e22731feffffffff38b8b3ae5fe9ef09c9f1583c9d6cc128bbd2639d49aca97b7686a74ba91bb32a040000006a4730440220105d93aba953edf008cc5b16ac81c10d97db6e59a3e13062ceef7cc1fbffd2ad022027b14b4162d70c4448bec7cb086b4e52880b51b282de98019ec3038153e25ed0012102cdbfb52b3e164203845f72391a3a58205834a3ad473a9d9878488dc1594aa0d4ffffffff087edb0000000000001976a914a1e5f40c6171e91183533f16bbda35e45182bcfa88ac80d97800000000001976a91482985ea6f877d70692072af967af305005fc86fd88ac80d97800000000001976a914a698b206b9f654974afd2056c85c52f88e4c2b2488ac9970af05000000001976a914b05dbb0ede1191e2871209affd8a5922e0a3275288ac80d97800000000001976a914619b3b22b7b66220d22907b8600724aecc49f03488acabc80000000000001976a914911c8c57eb12aa2c1cdce92f82c7e0405a2f3c6988ac80d97800000000001976a91464cd0ed04862f2b7101e9394285d2b3066e5e4dc88ac13b14100000000001976a9143f81fa4fd890845882fbb5226539d9643c99f0f488ac00000000" |
|
rvtxid = "4489a8cc933cb4e94915ead5b57b4aa707212c1f7b317187b500491e068c7887" |
|
if interface == "joinmarket-electrum": |
|
t = etr.Transaction(raw_valid_tx) |
|
assert rvtxid == t.hash() |
|
|
|
#Transaction deserialization/serialization test |
|
#Electrum requires this call to fill out Transactionfields |
|
t.deserialize() |
|
#log.debug("Got inputs: " + str(t.inputs)) |
|
ourdeser = deserialize(t.raw) |
|
ourraw = serialize(ourdeser) |
|
#log.debug("Recreated: \n" + ourraw) |
|
assert ourraw == raw_valid_tx |
|
#double check round trip too |
|
assert deserialize(ourraw) == ourdeser |
|
txinslist = t.inputs() |
|
elif interface == "joinmarket-joinmarket": |
|
assert serialize(deserialize(raw_valid_tx)) == raw_valid_tx |
|
t = deserialize(raw_valid_tx) |
|
txinslist = t["ins"] |
|
else: |
|
raise NotImplementedError("No such interface?") |
|
|
|
#Transaction signature verification tests. |
|
#All currently assuming 100% p2pkh. |
|
for i, tin in enumerate(txinslist): |
|
if interface == "joinmarket-electrum": |
|
script = address_to_script(tin["address"]) |
|
sig = tin["signatures"][0] |
|
pub = tin["pubkeys"][0] |
|
elif interface == "joinmarket-joinmarket": |
|
log.debug("Joinmarket working with this script: " + tin["script"]) |
|
scriptSig = tin["script"] |
|
#We need to parse out the pubkey, convert to address, then convert |
|
#to a pubkeyscript; this assumes p2pkh. Note that this is handled |
|
#internally by the joinmarket blockchain/maker/taker code, so only |
|
#for tests. |
|
pub = scriptSig[-66:] |
|
script = address_to_script(pubkey_to_address(pub)) |
|
log.debug("Converted to this addr script: " + script) |
|
#drop the length bytes from the start of sig and pub |
|
sig = scriptSig[2:-68] |
|
else: |
|
raise NotImplementedError("No such interface?") |
|
log.debug("Got sig, script, pub: " + " ".join([sig, script, pub])) |
|
assert verify_tx_input(raw_valid_tx, i, script, sig, pub) |
|
log.debug("Sig at: " + str(i) + " OK.") |
|
|
|
#Note there are no transaction signing tests, as |
|
#this is done by the wallet in this interface. |
|
log.debug("All tests passed.") |