diff --git a/lib/bitcoin/__init__.py b/lib/bitcoin/__init__.py index 67bc1852..a4a06d81 100644 --- a/lib/bitcoin/__init__.py +++ b/lib/bitcoin/__init__.py @@ -4,4 +4,5 @@ from bitcoin.transaction import * from bitcoin.deterministic import * from bitcoin.bci import * +from bitcoin.podle import * diff --git a/lib/bitcoin/podle.py b/lib/bitcoin/podle.py new file mode 100644 index 00000000..c5a328ba --- /dev/null +++ b/lib/bitcoin/podle.py @@ -0,0 +1,100 @@ +#Proof Of Discrete Logarithm Equivalence +#For algorithm steps, see https://gist.github.com/AdamISZ/9cbba5e9408d23813ca8 +import secp256k1 +import os +from py2specials import * +from py3specials import * + +N = 115792089237316195423570985008687907852837564279074904382605163141518161494337 +dummy_pub = secp256k1.PublicKey() + +'''NUMS - an alternate basepoint on the secp256k1 curve +For background (taken from https://github.com/AdamISZ/ConfidentialTransactionsDoc/blob/master/essayonCT.pdf) +>>> import bitcoin as btc +>>> import os +>>>H_x = int(sha256(btc.encode_pubkey(btc.G,'hex').decode('hex')).hexdigest(),16) +>>> H_x + 36444060476547731421425013472121489344383018981262552973668657287772036414144L +>>> H_y = pow(int(H_x*H_x*H_x + 7), int((btc.P+1)//4), int(btc.P)) +>>> H_y + 93254584761608041185240733468443117438813272608612929589951789286136240436011L +>>> H = (H_x, H_y) +''' +J_raw = '0350929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0' +J = secp256k1.PublicKey(safe_from_hex(J_raw), raw=True) + +def getP2(priv): + priv_raw = priv.private_key + return J.tweak_mul(priv_raw) + +def generate_podle(priv): + '''Given a raw private key, in hex format, + construct a commitment sha256(P2), which is + the hash of the value x*J, where x is the private + key as a raw scalar, and J is a NUMS alternative + basepoint on the Elliptic Curve. Also construct + a signature (s,e) of Schnorr type, which will serve + as a zero knowledge proof that the private key of P2 + is the same as the private key of P (=x*G). + Signature is constructed as: + s = k + x*e + where k is a standard 32 byte nonce and: + e = sha256(k*G || k*J || P || P2) + ''' + if len(priv)==66 and priv[-2:]=='01': + priv = priv[:-2] + priv = secp256k1.PrivateKey(safe_from_hex(priv)) + P = priv.pubkey + k = os.urandom(32) + KG = secp256k1.PrivateKey(k).pubkey + KJ = J.tweak_mul(k) + P2 = getP2(priv) + commitment = hashlib.sha256(P2.serialize()).digest() + e = hashlib.sha256(''.join([x.serialize() for x in [KG, KJ, P, P2]])).digest() + k_int = decode(k, 256) + priv_int = decode(priv.private_key, 256) + e_int = decode(e, 256) + sig_int = (k_int + priv_int*e_int) % N + sig = encode(sig_int, 256, minlen=32) + P2hex, chex, shex, ehex = [safe_hexlify(x) for x in [P2.serialize(),commitment, sig, e]] + return {'P2':P2hex, 'commit': chex, 'sig': shex, 'e':ehex} + +def verify_podle(Pser, P2ser, sig, e, commitment): + Pser, P2ser, sig, e, commitment = [safe_from_hex(x) for x in [Pser, P2ser, sig, e, commitment]] + #check 1: Hash(P2ser) =?= commitment + if not hashlib.sha256(P2ser).digest() == commitment: + return False + sig_priv = secp256k1.PrivateKey(sig,raw=True) + sG = sig_priv.pubkey + sJ = J.tweak_mul(sig) + P = secp256k1.PublicKey(Pser, raw=True) + P2 = secp256k1.PublicKey(P2ser, raw=True) + e_int = decode(e, 256) + minus_e = encode(-e_int % N, 256, minlen=32) + minus_e_P = P.tweak_mul(minus_e) + minus_e_P2 = P2.tweak_mul(minus_e) + KG = dummy_pub.combine([sG.public_key, minus_e_P.public_key]) + KJ = dummy_pub.combine([sJ.public_key, minus_e_P2.public_key]) + KGser = secp256k1.PublicKey(KG).serialize() + KJser = secp256k1.PublicKey(KJ).serialize() + #check 2: e =?= H(K_G || K_J || P || P2) + e_check = hashlib.sha256(KGser + KJser + Pser + P2ser).digest() + if not e_check == e: + return False + return True + +if __name__ == '__main__': + + for i in range(10000): + priv = os.urandom(32) + Priv = secp256k1.PrivateKey(priv) + Pser = safe_hexlify(Priv.pubkey.serialize()) + podle_sig = generate_podle(safe_hexlify(priv)) + P2ser, s, e, commitment = (podle_sig['P2'], podle_sig['sig'], + podle_sig['e'], podle_sig['commit']) + if not verify_podle(Pser, P2ser, s, e, commitment): + print 'failed to verify' + + + + \ No newline at end of file diff --git a/lib/bitcoin/secp256k1.py b/lib/bitcoin/secp256k1.py index f871a4c6..9d017b9a 100644 --- a/lib/bitcoin/secp256k1.py +++ b/lib/bitcoin/secp256k1.py @@ -1,6 +1,6 @@ import os import hashlib - +import binascii from _libsecp256k1 import ffi, lib import _noncefunc diff --git a/lib/common.py b/lib/common.py index 1a02904d..b04e12d6 100644 --- a/lib/common.py +++ b/lib/common.py @@ -25,7 +25,8 @@ config_location = 'joinmarket.cfg' # FIXME: Add rpc_* options here in the future! required_options = {'BLOCKCHAIN':['blockchain_source', 'network'], - 'MESSAGING':['host','channel','port']} + 'MESSAGING':['host','channel','port'], + 'LIMITS':['taker_utxo_retries']} defaultconfig =\ """ @@ -188,6 +189,33 @@ def debug_dump_object(obj, skip_fields=[]): else: debug(str(v)) +def check_utxo_blacklist(utxo): + #write/read the file here; could be inferior + #performance-wise; should cache. TODO + #there also needs to be formatting error checking here, + #but not a high priority TODO + blacklist = {} + if os.path.isfile("logs/blacklist"): + with open("logs/blacklist","rb") as f: + blacklist_lines = f.readlines() + else: + blacklist_lines = [] + for bl in blacklist_lines: + ut, ct = bl.split(',') + ut = ut.strip() + ct = int(ct.strip()) + blacklist[ut]=ct + if utxo in blacklist.keys(): + blacklist[utxo] += 1 + if blacklist[utxo] >= config.getint("LIMITS","taker_utxo_retries"): + return False + else: + blacklist[utxo]=1 + with open("logs/blacklist","wb") as f: + for k,v in blacklist.iteritems(): + f.write(k+' , '+str(v)+'\n') + return True + def select_gradual(unspent, value): ''' UTXO selection algorithm for gradual dust reduction diff --git a/lib/irc.py b/lib/irc.py index 6ff81682..ab16b10a 100644 --- a/lib/irc.py +++ b/lib/irc.py @@ -95,13 +95,18 @@ def request_orderbook(self): self.__pubmsg(COMMAND_PREFIX + 'orderbook') #Taker callbacks - def fill_orders(self, nickoid_dict, cj_amount, taker_pubkey): + def fill_orders(self, nickoid_dict, cj_amount, taker_pubkey, commitment=None): for c, oid in nickoid_dict.iteritems(): msg = str(oid) + ' ' + str(cj_amount) + ' ' + taker_pubkey + if commitment: + msg += ' '+commitment self.__privmsg(c, 'fill', msg) - def send_auth(self, nick, pubkey, sig): - message = pubkey + ' ' + sig + def send_auth(self, nick, pubkey, sig, utxo=None, P2=None, s=None, e=None): + fields = [pubkey, sig] + if all([utxo, P2, s, e]): + fields += [utxo, P2, s, e] + message = ' '.join(fields) self.__privmsg(nick, 'auth', message) def send_tx(self, nick_list, txhex): @@ -240,18 +245,35 @@ def __on_privmsg(self, nick, message): oid = int(chunks[1]) amount = int(chunks[2]) taker_pk = chunks[3] + if len(chunks)>4: + commit = chunks[4] + else: + commit = None except (ValueError, IndexError) as e: self.send_error(nick, str(e)) if self.on_order_fill: - self.on_order_fill(nick, oid, amount, taker_pk) + self.on_order_fill(nick, oid, amount, + taker_pk, commit) elif chunks[0] == 'auth': try: i_utxo_pubkey = chunks[1] btc_sig = chunks[2] + if len(chunks)>3: + i_utxo = chunks[3] + p2 = chunks[4] + sig = chunks[5] + e_val = chunks[6] + else: + i_utxo = None + p2 = None + sig = None + e_val = None except (ValueError, IndexError) as e: self.send_error(nick, str(e)) if self.on_seen_auth: - self.on_seen_auth(nick, i_utxo_pubkey, btc_sig) + self.on_seen_auth(nick, i_utxo_pubkey, + btc_sig, i_utxo, + p2, sig, e_val) elif chunks[0] == 'tx': b64tx = chunks[1] try: diff --git a/lib/maker.py b/lib/maker.py index 1512eca9..e70163b5 100644 --- a/lib/maker.py +++ b/lib/maker.py @@ -57,16 +57,32 @@ def __init__(self, maker, nick, oid, amount, taker_pk): # orders to find out which addresses you use self.maker.msgchan.send_pubkey(nick, self.kp.hex_pk()) - def auth_counterparty(self, nick, i_utxo_pubkey, btc_sig): + def auth_counterparty(self, nick, i_utxo_pubkey, btc_sig, i_utxo, p2, s, e): self.i_utxo_pubkey = i_utxo_pubkey - + #check the btc signature of the encryption pubkey if not btc.ecdsa_verify(self.taker_pk, btc_sig, binascii.unhexlify(self.i_utxo_pubkey)): - print 'signature didnt match pubkey and message' + common.debug("signature from nick: " + nick + "didnt match pubkey and message") return False + + if all([i_utxo, p2, s, e]): + #check the validity of the proof of discrete log equivalence + if not btc.verify_podle(self.i_utxo_pubkey, p2, s, e, self.maker.commit): + common.debug("PODLE verification failed; counterparty utxo is not verified.") + return False + #finally, check that the proffered utxo is real, + #and corresponds to the pubkey + res = common.bc_interface.query_utxo_set([i_utxo]) + if len(res) != 1: + common.debug("Input utxo: "+str(i_utxo)+" from nick: "+nick+" is not valid.") + return False + real_utxo = res[0] + if real_utxo['address'] != btc.pubkey_to_address(i_utxo_pubkey, common.get_p2pk_vbyte()): + return False + #TODO: could add check for coin age and value here + #(need to edit query_utxo_set if we want coin age) + #authorisation of taker passed - #(but input utxo pubkey is checked in verify_unsigned_tx). #Send auth request to taker - #TODO the next 2 lines are a little inefficient. btc_key = self.maker.wallet.get_key_from_addr(self.cj_addr) btc_pub = btc.privtopub(btc_key, True) btc_sig = btc.ecdsa_sign(self.kp.hex_pk(), binascii.unhexlify(btc_key)) @@ -189,22 +205,30 @@ def get_crypto_box_from_nick(self, nick): def on_orderbook_requested(self, nick): self.msgchan.announce_orders(self.orderlist, nick) - def on_order_fill(self, nick, oid, amount, taker_pubkey): + def on_order_fill(self, nick, oid, amount, taker_pubkey, commit = None): if nick in self.active_orders and self.active_orders[nick] != None: self.active_orders[nick] = None debug('had a partially filled order but starting over now') + if commit: + self.commit = commit + if not common.check_utxo_blacklist(self.commit): + common.debug("Taker utxo commitment is in blacklist, having been used "\ + + common.config.get("LIMITS","taker_utxo_retries")+ " times, rejecting.") + return + self.wallet_unspent_lock.acquire() try: self.active_orders[nick] = CoinJoinOrder(self, nick, oid, amount, taker_pubkey) finally: self.wallet_unspent_lock.release() - def on_seen_auth(self, nick, pubkey, sig): + def on_seen_auth(self, nick, pubkey, sig, i_utxo, p2, s, e_val): if nick not in self.active_orders or self.active_orders[nick] == None: self.msgchan.send_error(nick, 'No open order from this nick') - self.active_orders[nick].auth_counterparty(nick, pubkey, sig) - #TODO if auth_counterparty returns false, remove this order from active_orders - # and send an error + if not self.active_orders[nick].auth_counterparty(nick, pubkey, + sig, i_utxo, p2, s, e_val): + self.active_orders[nick]=None + self.msgchan.send_error(nick, "Authorisation failed.") def on_seen_tx(self, nick, txhex): if nick not in self.active_orders or self.active_orders[nick] == None: diff --git a/lib/message_channel.py b/lib/message_channel.py index a4c53006..67512b35 100644 --- a/lib/message_channel.py +++ b/lib/message_channel.py @@ -59,8 +59,8 @@ def register_taker_callbacks(self, on_error=None, on_pubkey=None, on_ioauth=None self.on_pubkey = on_pubkey self.on_ioauth = on_ioauth self.on_sig = on_sig - def fill_orders(self, nickoid_dict, cj_amount, taker_pubkey): pass - def send_auth(self, nick, pubkey, sig): pass + def fill_orders(self, nickoid_dict, cj_amount, taker_pubkey, commitment=None): pass + def send_auth(self, nick, pubkey, sig, utxo=None, P2=None, s=None, e=None): pass def send_tx(self, nick_list, txhex): pass def push_tx(self, nick, txhex): pass diff --git a/lib/taker.py b/lib/taker.py index 68cb48e0..6b7f9b7a 100644 --- a/lib/taker.py +++ b/lib/taker.py @@ -47,7 +47,15 @@ def __init__(self, msgchan, wallet, db, cj_amount, orders, input_utxos, my_cj_ad #create DH keypair on the fly for this Tx object self.kp = enc_wrapper.init_keypair() self.crypto_boxes = {} - self.msgchan.fill_orders(self.active_orders, self.cj_amount, self.kp.hex_pk()) + if not self.auth_addr: + self.auth_addr = self.input_utxos.itervalues().next()['address'] + self.auth_utxo = self.input_utxos.iterkeys().next() + self.auth_priv = self.wallet.get_key_from_addr(self.auth_addr) + self.podle = btc.generate_podle(self.auth_priv) + debug("Generated PoDLE: "+pprint.pformat(self.podle)) + + self.msgchan.fill_orders(self.active_orders, self.cj_amount, + self.kp.hex_pk(), self.podle['commit']) def start_encryption(self, nick, maker_pk): if nick not in self.active_orders.keys(): @@ -55,15 +63,11 @@ def start_encryption(self, nick, maker_pk): return self.crypto_boxes[nick] = [maker_pk, enc_wrapper.as_init_encryption(\ self.kp, enc_wrapper.init_pubkey(maker_pk))] - #send authorisation request - if self.auth_addr: - my_btc_addr = self.auth_addr - else: - my_btc_addr = self.input_utxos.itervalues().next()['address'] - my_btc_priv = self.wallet.get_key_from_addr(my_btc_addr) - my_btc_pub = btc.privtopub(my_btc_priv, True) - my_btc_sig = btc.ecdsa_sign(self.kp.hex_pk(), binascii.unhexlify(my_btc_priv)) - self.msgchan.send_auth(nick, my_btc_pub, my_btc_sig) + my_btc_pub = btc.privtopub(self.auth_priv, True) + my_btc_sig = btc.ecdsa_sign(self.kp.hex_pk(), binascii.unhexlify(self.auth_priv)) + self.msgchan.send_auth(nick, my_btc_pub, my_btc_sig, + str(self.auth_utxo), str(self.podle['P2']), + str(self.podle['sig']), str(self.podle['e'])) def auth_counterparty(self, nick, btc_sig, cj_pub): '''Validate the counterpartys claim to own the btc diff --git a/test/blacklist-test.py b/test/blacklist-test.py new file mode 100644 index 00000000..be321b42 --- /dev/null +++ b/test/blacklist-test.py @@ -0,0 +1,105 @@ +import sys +import os, time +data_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) +sys.path.insert(0, os.path.join(data_dir, 'lib')) +import subprocess +import unittest +import common +import commontest +from blockchaininterface import * +import bitcoin as btc +import binascii + +class BlackListPassTests(unittest.TestCase): + '''This test case intends to simulate + a single join with a single counterparty. In that sense, + it's not realistic, because nobody (should) do joins with only 1 maker, + but this test has the virtue of being the simplest possible thing + that JoinMarket can do. ''' + def setUp(self): + #create 2 new random wallets. + #put 10 coins into the first receive address + #to allow that bot to start. + self.wallets = commontest.make_wallets(2, + wallet_structures=[[1,0,0,0,0],[1,0,0,0,0]], mean_amt=10) + + + def blacklist_run(self, n, m, fake): + if os.path.isfile('logs/blacklist'): + os.remove('logs/blacklist') + #start yield generator with wallet1 + yigen_proc = commontest.local_command(['python','yield-generator.py', + str(self.wallets[0]['seed'])],bg=True) + + #A significant delay is needed to wait for the yield generator to sync its wallet + time.sleep(10) + + #run a single sendpayment call with wallet2 + amt = n*100000000 #in satoshis + dest_address = btc.privkey_to_address(os.urandom(32), from_hex=False, magicbyte=common.get_p2pk_vbyte()) + try: + for i in range(m): + sp_proc = commontest.local_command(['python','sendpayment.py','--yes','-N','1', self.wallets[1]['seed'],\ + str(amt), dest_address]) + except subprocess.CalledProcessError, e: + if yigen_proc: + yigen_proc.terminate() + print e.returncode + print e.message + raise + + if yigen_proc: + yigen_proc.terminate() + if not fake: + received = common.bc_interface.get_received_by_addr([dest_address], None)['data'][0]['balance'] + if received != amt*m: + common.debug('received was: '+str(received)+ ' but amount was: '+str(amt)) + return False + #check sanity in blacklist + with open('logs/blacklist','rb') as f: + blacklist_lines = f.readlines() + if not fake: + required_bl_lines = m + bl_count = 1 + else: + required_bl_lines = 1 + bl_count = m + if len(blacklist_lines) != required_bl_lines: + common.debug('wrong number of blacklist lines: '+str(len(blacklist_lines))) + return False + + for bl in blacklist_lines: + if len(bl.split(',')[0].strip()) != 64: + common.debug('malformed utxo: '+str(len(bl.split(',')[0].strip()))) + return False + if int(bl.split(',')[1]) != bl_count: + common.debug('wrong blacklist count:'+str(bl.split(',')[1])) + return False + return True + + def test_blacklist(self): + self.failUnless(self.blacklist_run(2, 2, False)) + + + + +def main(): + os.chdir(data_dir) + common.load_program_config() + unittest.main() + +if __name__ == '__main__': + #Big kludge, but there is currently no way to inject this code: + print """this test is to be run in two modes, first + with no changes, then second adding a 'return' in + taker.CoinJoinTX.push() return (so it does nothing), + and further changing the third parameter to blacklist_run to 'True' + and the second parameter to '3' from '2' + In both cases the test should pass for success. + Also, WARNING! This test will delete your blacklist, better + not run it in a "real" repo or back it up. + """ + raw_input("OK?") + main() + + diff --git a/test/regtest.py b/test/regtest.py index 3070247f..962700a9 100644 --- a/test/regtest.py +++ b/test/regtest.py @@ -72,7 +72,7 @@ def run_simple_send(self, n, m): str(self.wallets[0]['seed'])],bg=True) #A significant delay is needed to wait for the yield generator to sync its wallet - time.sleep(30) + time.sleep(20) #run a single sendpayment call with wallet2 amt = n*100000000 #in satoshis @@ -126,7 +126,7 @@ def run_3party_join(self): yigen_procs.append(ygp) #A significant delay is needed to wait for the yield generators to sync - time.sleep(60) + time.sleep(20) #run a single sendpayment call amt = 100000000 #in satoshis diff --git a/test/wallet-test.py b/test/wallet-test.py index 41bad568..e4482947 100644 --- a/test/wallet-test.py +++ b/test/wallet-test.py @@ -18,6 +18,8 @@ def test_generate(self): #testing a variety of passwords self.failUnless(self.run_generate('abc123')) self.failUnless(self.run_generate('dddddddddddddddddddddddddddddddddddddddddddd')) + #silly length + self.failUnless(self.run_generate('abc8'*1000)) #null password is accepted self.failUnless(self.run_generate('')) #binary password is accepted; good luck with that! @@ -31,7 +33,7 @@ def run_generate(self, pwd): expected = ['Enter wallet encryption passphrase:', 'Reenter wallet encryption passphrase:', 'Input wallet file name'] - testlog = open('test/testlog-'+pwd, 'wb') + testlog = open('test/testlog-'+pwd[:25], 'wb') p = pexpect.spawn('python wallet-tool.py generate', logfile=testlog) commontest.interact(p, test_in, expected) p.expect('saved to') @@ -39,7 +41,7 @@ def run_generate(self, pwd): p.close() testlog.close() #anything to check in the log? - with open(os.path.join('test','testlog-'+pwd)) as f: + with open(os.path.join('test','testlog-'+pwd[:25])) as f: print f.read() if p.exitstatus != 0: print 'failed due to exit status: '+str(p.exitstatus) @@ -50,7 +52,8 @@ def run_generate(self, pwd): print 'failed due to wallet missing' return False os.remove('wallets/testwallet.json') - except: + except Exception as e: + print 'try except failed with error: '+repr(e) return False return True @@ -99,4 +102,4 @@ def run_recover(self, seed): if __name__ == '__main__': os.chdir(data_dir) common.load_program_config() - unittest.main() \ No newline at end of file + unittest.main() diff --git a/yield-generator.py b/yield-generator.py index f57f71e2..caf65843 100644 --- a/yield-generator.py +++ b/yield-generator.py @@ -113,10 +113,7 @@ def on_tx_unconfirmed(self, cjorder, txid, removed_utxos): return ([], [neworders[0]]) def on_tx_confirmed(self, cjorder, confirmations, txid): - if cjorder.cj_addr in self.tx_unconfirm_timestamp: - confirm_time = int(time.time()) - self.tx_unconfirm_timestamp[cjorder.cj_addr] - else: - confirm_time = 0 + confirm_time = int(time.time()) - self.tx_unconfirm_timestamp[cjorder.cj_addr] timestamp = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S") self.log_statement([timestamp, cjorder.cj_amount, len(cjorder.utxos), sum([av['value'] for av in cjorder.utxos.values()]), cjorder.real_cjfee,