diff --git a/jmbase/jmbase/commands.py b/jmbase/jmbase/commands.py index b7138e3..5f1f2bb 100644 --- a/jmbase/jmbase/commands.py +++ b/jmbase/jmbase/commands.py @@ -3,9 +3,10 @@ Commands defining client-server (daemon) messaging protocol (*not* Joinmarket p2p protocol). Used for AMP asynchronous messages. """ -from twisted.protocols.amp import Boolean, Command, Integer, Unicode +from twisted.protocols.amp import Boolean, Command, Integer, Unicode, String from .bigstring import BigUnicode + class DaemonNotReady(Exception): pass @@ -45,7 +46,8 @@ class JMSetup(JMCommand): role, passes initial offers for announcement (for TAKER, this data is "none") """ arguments = [(b'role', Unicode()), - (b'initdata', Unicode())] + (b'offers', Unicode()), + (b'fidelity_bond', String())] class JMMsgSignature(JMCommand): """A response to a request for a bitcoin signature @@ -376,4 +378,4 @@ class BIP78InfoMsg(JMCommand): from the daemon about current status at network level. """ - arguments = [(b'infomsg', Unicode())] \ No newline at end of file + arguments = [(b'infomsg', Unicode())] diff --git a/jmbase/test/test_commands.py b/jmbase/test/test_commands.py index 12bceca..231ba19 100644 --- a/jmbase/test/test_commands.py +++ b/jmbase/test/test_commands.py @@ -63,8 +63,8 @@ class JMTestServerProtocol(JMBaseProtocol): return {'accepted': True} @JMSetup.responder - def on_JM_SETUP(self, role, initdata): - show_receipt("JMSETUP", role, initdata) + def on_JM_SETUP(self, role, offers, use_fidelity_bond): + show_receipt("JMSETUP", role, offers, use_fidelity_bond) d = self.callRemote(JMSetupDone) self.defaultCallbacks(d) return {'accepted': True} @@ -75,7 +75,8 @@ class JMTestServerProtocol(JMBaseProtocol): #build a huge orderbook to test BigString Argument orderbook = ["aaaa" for _ in range(2**15)] d = self.callRemote(JMOffers, - orderbook=json.dumps(orderbook)) + orderbook=json.dumps(orderbook), + fidelitybonds="dummyfidelitybonds") self.defaultCallbacks(d) return {'accepted': True} @@ -156,7 +157,8 @@ class JMTestClientProtocol(JMBaseProtocol): show_receipt("JMUP") d = self.callRemote(JMSetup, role="TAKER", - initdata="none") + offers="{}", + use_fidelity_bond=False) self.defaultCallbacks(d) return {'accepted': True} @@ -177,8 +179,8 @@ class JMTestClientProtocol(JMBaseProtocol): return {'accepted': True} @JMOffers.responder - def on_JM_OFFERS(self, orderbook): - show_receipt("JMOFFERS", orderbook) + def on_JM_OFFERS(self, orderbook, fidelitybonds): + show_receipt("JMOFFERS", orderbook, fidelitybonds) d = self.callRemote(JMFill, amount=100, commitment="dummycommitment", diff --git a/jmclient/jmclient/client_protocol.py b/jmclient/jmclient/client_protocol.py index 822345e..07bac06 100644 --- a/jmclient/jmclient/client_protocol.py +++ b/jmclient/jmclient/client_protocol.py @@ -392,9 +392,14 @@ class JMMakerClientProtocol(JMClientProtocol): if not self.client.offerlist: return self.offers_ready_loop.stop() + if self.client.fidelity_bond: + fidelity_bond_data = self.client.fidelity_bond.serialize() + else: + fidelity_bond_data = b'' d = self.callRemote(commands.JMSetup, role="MAKER", - initdata=json.dumps(self.client.offerlist)) + offers=json.dumps(self.client.offerlist), + fidelity_bond=fidelity_bond_data) self.defaultCallbacks(d) @commands.JMSetupDone.responder @@ -627,7 +632,8 @@ class JMTakerClientProtocol(JMClientProtocol): def on_JM_UP(self): d = self.callRemote(commands.JMSetup, role="TAKER", - initdata="none") + offers="{}", + fidelity_bond=b'') self.defaultCallbacks(d) return {'accepted': True} diff --git a/jmclient/jmclient/fidelity_bond.py b/jmclient/jmclient/fidelity_bond.py new file mode 100644 index 0000000..2340f55 --- /dev/null +++ b/jmclient/jmclient/fidelity_bond.py @@ -0,0 +1,136 @@ +import struct +import base64 +import json +from jmbitcoin import ecdsa_sign, ecdsa_verify +from jmdaemon import import fidelity_bond_sanity_check + + +def assert_is_utxo(utxo): + assert len(utxo) == 2 + assert isinstance(utxo[0], bytes) + assert len(utxo[0]) == 32 + assert isinstance(utxo[1], int) + assert utxo[1] >= 0 + + +def get_cert_msg(cert_pub, cert_expiry): + return b'fidelity-bond-cert|' + cert_pub + b'|' + str(cert_expiry).encode('ascii') + + +class FidelityBond: + def __init__(self, utxo, utxo_pubkey, locktime, cert_expiry, + cert_privkey, cert_pubkey, cert_signature): + assert_is_utxo(utxo) + assert isinstance(utxo_pubkey, bytes) + assert isinstance(locktime, int) + assert isinstance(cert_expiry, int) + assert isinstance(cert_privkey, bytes) + assert isinstance(cert_pubkey, bytes) + assert isinstance(cert_signature, bytes) + self.utxo = utxo + self.utxo_pubkey = utxo_pubkey + self.locktime = locktime + self.cert_expiry = cert_expiry + self.cert_privkey = cert_privkey + self.cert_pubkey = cert_pubkey + self.cert_signature = cert_signature + + def create_proof(self, maker_nick, taker_nick): + return FidelityBondProof( + maker_nick, taker_nick, self.cert_pubkey, self.cert_expiry, + self.cert_signature, self.utxo, self.utxo_pubkey, self.locktime) + + def serialize(self): + return json.dumps([ + self.utxo, + self.utxo_pubkey, + self.locktime, + self.cert_expiry, + self.cert_privkey, + self.cert_pubkey, + self.cert_signature, + ]) + + @classmethod + def deserialize(cls, data): + return cls(*json.loads(data)) + + +class FidelityBondProof: + # nick_sig + cert_sig + cert_pubkey + cert_expiry + utxo_pubkey + txid + vout + timelock + # 72 + 72 + 33 + 2 + 33 + 32 + 4 + 4 = 252 bytes + SER_STUCT_FMT = '<72s72s33sH33s32sII' + + def __init__(self, maker_nick, taker_nick, cert_pub, cert_expiry, + cert_sig, utxo, utxo_pub, locktime): + assert isinstance(maker_nick, str) + assert isinstance(taker_nick, str) + assert isinstance(cert_pub, bytes) + assert isinstance(cert_sig, bytes) + assert isinstance(utxo_pub, bytes) + assert isinstance(locktime, int) + assert_is_utxo(utxo) + self.maker_nick = maker_nick + self.taker_nick = taker_nick + self.cert_pub = cert_pub + self.cert_expiry = cert_expiry + self.cert_sig = cert_sig + self.utxo = utxo + self.utxo_pub = utxo_pub + self.locktime = locktime + + @property + def nick_msg(self): + return (self.taker_nick + '|' + self.maker_nick).encode('ascii') + + def create_proof_msg(self, cert_priv): + nick_sig = ecdsa_sign(self.nick_msg, cert_priv) + # FIXME: remove stupid base64 + nick_sig = base64.b64decode(nick_sig) + return self._serialize_proof_msg(nick_sig) + + def _serialize_proof_msg(self, msg_signature): + msg_signature = msg_signature.rjust(72, b'\xff') + cert_sig = self.cert_sig.rjust(72, b'\xff') + fidelity_bond_data = struct.pack( + self.SER_STUCT_FMT, + msg_signature, + cert_sig, + self.cert_pub, + self.cert_expiry, + self.utxo_pub, + self.utxo[0], + self.utxo[1], + self.locktime + ) + return base64.b64encode(fidelity_bond_data).decode('ascii') + + @staticmethod + def _verify_signature(message, signature, pubkey): + # FIXME: remove stupid base64 + return ecdsa_verify(message, base64.b64encode(signature), pubkey) + + @classmethod + def parse_and_verify_proof_msg(cls, maker_nick, taker_nick, data): + if not fidelity_bond_sanity_check.fidelity_bond_sanity_check(data): + raise ValueError("sanity check failed") + decoded_data = base64.b64decode(data) + + unpacked_data = struct.unpack(cls.SER_STUCT_FMT, decoded_data) + try: + signature = unpacked_data[0][unpacked_data[0].index(b'\x30'):] + cert_sig = unpacked_data[1][unpacked_data[1].index(b'\x30'):] + except ValueError: + #raised if index() doesnt find the position + raise ValueError("der signature header not found") + proof = cls(maker_nick, taker_nick, unpacked_data[2], unpacked_data[3], + cert_sig, (unpacked_data[5], unpacked_data[6]), + unpacked_data[4], unpacked_data[7]) + cert_msg = get_cert_msg(proof.cert_pub, proof.cert_expiry) + + if not cls._verify_signature(proof.nick_msg, signature, proof.cert_pub): + raise ValueError("nick sig does not verify") + if not cls._verify_signature(cert_msg, proof.cert_sig, proof.utxo_pub): + raise ValueError("cert sig does not verify") + + return proof diff --git a/jmclient/jmclient/wallet.py b/jmclient/jmclient/wallet.py index 1b2bec8..4312e00 100644 --- a/jmclient/jmclient/wallet.py +++ b/jmclient/jmclient/wallet.py @@ -2400,6 +2400,23 @@ class FidelityBondMixin(object): a = max(0, min(1, exp(r*T) - 1) - min(1, exp(r*max(0, t-L)) - 1)) return utxo_value*utxo_value*a*a + @classmethod + def get_validated_timelocked_fidelity_bond_utxo(cls, utxo, utxo_pubkey, locktime, + cert_expiry, current_block_height): + + utxo_data = jm_single().bc_interface.query_utxo_set(utxo, includeconf=True) + if utxo_data[0] == None: + return None + if utxo_data[0]["confirms"] <= 0: + return None + RETARGET_INTERVAL = 2016 + if current_block_height > cert_expiry*RETARGET_INTERVAL: + return None + implied_spk = btc.redeem_script_to_p2wsh_script(btc.mk_freeze_script(utxo_pubkey, locktime)) + if utxo_data[0]["script"] != implied_spk: + return None + return utxo_data[0] + class BIP49Wallet(BIP32PurposedWallet): _PURPOSE = 2**31 + 49 _ENGINE = ENGINES[TYPE_P2SH_P2WPKH] diff --git a/jmclient/test/test_client_protocol.py b/jmclient/test/test_client_protocol.py index d16f8df..b89c158 100644 --- a/jmclient/test/test_client_protocol.py +++ b/jmclient/test/test_client_protocol.py @@ -184,8 +184,8 @@ class JMTestServerProtocol(JMBaseProtocol): return {'accepted': True} @JMSetup.responder - def on_JM_SETUP(self, role, initdata): - show_receipt("JMSETUP", role, initdata) + def on_JM_SETUP(self, role, offers, fidelity_bond): + show_receipt("JMSETUP", role, offers, fidelity_bond) d = self.callRemote(JMSetupDone) self.defaultCallbacks(d) return {'accepted': True} diff --git a/jmdaemon/jmdaemon/__init__.py b/jmdaemon/jmdaemon/__init__.py index 0e854ba..384b5f7 100644 --- a/jmdaemon/jmdaemon/__init__.py +++ b/jmdaemon/jmdaemon/__init__.py @@ -14,6 +14,7 @@ from .daemon_protocol import (JMDaemonServerProtocolFactory, JMDaemonServerProto from .protocol import (COMMAND_PREFIX, ORDER_KEYS, NICK_HASH_LENGTH, NICK_MAX_ENCODED, JM_VERSION, JOINMARKET_NICK_HEADER) from .message_channel import MessageChannelCollection + # Set default logging handler to avoid "No handler found" warnings. try: from logging import NullHandler diff --git a/jmdaemon/jmdaemon/daemon_protocol.py b/jmdaemon/jmdaemon/daemon_protocol.py index 954293c..b3eadbb 100644 --- a/jmdaemon/jmdaemon/daemon_protocol.py +++ b/jmdaemon/jmdaemon/daemon_protocol.py @@ -13,6 +13,7 @@ from jmbase import (hextobin, is_hs_uri, get_tor_agent, JMHiddenService, get_nontor_agent, BytesProducer, wrapped_urlparse, bdict_sdict_convert, JMHTTPResource) from jmbase.commands import * +from jmbitcoin.fidelity_bond import FidelityBond from twisted.protocols import amp from twisted.internet import reactor, ssl, task from twisted.internet.protocol import ServerFactory @@ -474,6 +475,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch): self.crypto_boxes = {} self.sig_lock = threading.Lock() self.active_orders = {} + self.fidelity_bond = None def checkClientResponse(self, response): """A generic check of client acceptance; any failure @@ -551,7 +553,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch): return {'accepted': True} @JMSetup.responder - def on_JM_SETUP(self, role, initdata): + def on_JM_SETUP(self, role, offers, fidelity_bond): assert self.jm_state == 0 self.role = role self.crypto_boxes = {} @@ -565,8 +567,10 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch): if self.role == "TAKER": self.mcc.pubmsg(COMMAND_PREFIX + "orderbook") elif self.role == "MAKER": - self.offerlist = json.loads(initdata) - self.mcc.announce_orders(self.offerlist) + self.offerlist = json.loads(offers) + if fidelity_bond: + self.fidelity_bond = FidelityBond.deserialize(fidelity_bond) + self.mcc.announce_orders(self.offerlist, None, None, None) self.jm_state = 1 return {'accepted': True} @@ -711,7 +715,15 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch): def on_orderbook_requested(self, nick, mc=None): """Dealt with by daemon, assuming offerlist is up to date """ - self.mcc.announce_orders(self.offerlist, nick, mc) + if self.fidelity_bond: + taker_nick = nick + maker_nick = self.mcc.nick + proof = self.fidelity_bond.create_proof(maker_nick, taker_nick) + proof_msg = proof.create_proof_msg(self.fidelity_bond.cert_privkey) + else: + proof_msg = None + + self.mcc.announce_orders(self.offerlist, nick, proof_msg, mc) @maker_only def on_order_fill(self, nick, oid, amount, taker_pk, commit): @@ -1015,6 +1027,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch): if self.mcc: self.mcc.shutdown() + class JMDaemonServerProtocolFactory(ServerFactory): protocol = JMDaemonServerProtocol diff --git a/jmdaemon/jmdaemon/fidelity_bond_sanity_check.py b/jmdaemon/jmdaemon/fidelity_bond_sanity_check.py new file mode 100644 index 0000000..44251f3 --- /dev/null +++ b/jmdaemon/jmdaemon/fidelity_bond_sanity_check.py @@ -0,0 +1,12 @@ + +import base64 + +def fidelity_bond_sanity_check(proof): + try: + decoded_data = base64.b64decode(proof, validate=True) + if len(decoded_data) != 252: + return False + except Exception: + return False + return True + diff --git a/jmdaemon/jmdaemon/message_channel.py b/jmdaemon/jmdaemon/message_channel.py index a5cbe6c..1105e88 100644 --- a/jmdaemon/jmdaemon/message_channel.py +++ b/jmdaemon/jmdaemon/message_channel.py @@ -6,7 +6,8 @@ import threading from twisted.internet import reactor from jmdaemon import encrypt_encode, decode_decrypt, COMMAND_PREFIX,\ NICK_HASH_LENGTH, NICK_MAX_ENCODED, plaintext_commands,\ - encrypted_commands, commitment_broadcast_list, offername_list + encrypted_commands, commitment_broadcast_list, offername_list,\ + fidelity_bond_cmd_list from jmbase.support import get_log from functools import wraps @@ -284,7 +285,7 @@ class MessageChannelCollection(object): "; cannot find on any message channel.") return - def announce_orders(self, orderlist, nick=None, new_mc=None): + def announce_orders(self, orderlist, nick, fidelity_bond_proof_msg, new_mc): """Send orders defined in list orderlist either to the shared public channel (pit), on all message channels, if nick=None, @@ -301,6 +302,7 @@ class MessageChannelCollection(object): "Tried to announce orders on an unavailable message channel.") return if nick is None: + assert fidelity_bond_proof_msg is None for mc in self.available_channels(): mc.announce_orders(orderlines) else: @@ -310,6 +312,9 @@ class MessageChannelCollection(object): cmd = orderlist[0]['ordertype'] msg = ' '.join(orderlines[0].split(' ')[1:]) msg += ''.join(orderlines[1:]) + if fidelity_bond_proof_msg: + msg += (COMMAND_PREFIX + fidelity_bond_cmd_list[0] + " " + + fidelity_bond_proof_msg) if new_mc: self.prepare_privmsg(nick, cmd, msg, mc=new_mc) else: @@ -563,7 +568,8 @@ class MessageChannelCollection(object): # orderbook watcher commands def register_orderbookwatch_callbacks(self, on_order_seen=None, - on_order_cancel=None): + on_order_cancel=None, + on_fidelity_bond_seen=None): """Special cases: on_order_seen: use it as a trigger for presence of nick. on_order_cancel: what happens if cancel/modify in one place @@ -572,7 +578,7 @@ class MessageChannelCollection(object): self.on_order_seen = on_order_seen for mc in self.mchannels: mc.register_orderbookwatch_callbacks(self.on_order_seen_trigger, - on_order_cancel) + on_order_cancel, on_fidelity_bond_seen) def on_orderbook_requested_trigger(self, nick, mc): """Update nicks_seen state to reflect presence of @@ -647,6 +653,7 @@ class MessageChannel(object): # orderbook watch functions self.on_order_seen = None self.on_order_cancel = None + self.on_fidelity_bond_seen = None # taker functions self.on_error = None self.on_pubkey = None @@ -730,9 +737,11 @@ class MessageChannel(object): # orderbook watcher commands def register_orderbookwatch_callbacks(self, on_order_seen=None, - on_order_cancel=None): + on_order_cancel=None, + on_fidelity_bond_seen=None): self.on_order_seen = on_order_seen self.on_order_cancel = on_order_cancel + self.on_fidelity_bond_seen = on_fidelity_bond_seen # taker commands def register_taker_callbacks(self, @@ -813,6 +822,21 @@ class MessageChannel(object): return True return False + def check_for_fidelity_bond(self, nick, _chunks): + if _chunks[0] in fidelity_bond_cmd_list: + try: + fidelity_bond_proof_msg = _chunks[1] + if self.on_fidelity_bond_seen: + self.on_fidelity_bond_seen(nick, _chunks[0], fidelity_bond_proof_msg) + except IndexError as e: + log.debug(e) + log.debug('index error parsing chunks, possibly malformed ' + 'offer by other party. No user action required. ' + 'Triggered by: ' + str(nick)) + finally: + return True + return False + def cancel_orders(self, oid_list): clines = [COMMAND_PREFIX + 'cancel ' + str(oid) for oid in oid_list] self.pubmsg(''.join(clines)) @@ -952,6 +976,8 @@ class MessageChannel(object): # orderbook watch commands if self.check_for_orders(nick, _chunks): pass + elif self.check_for_fidelity_bond(nick, _chunks): + pass # taker commands elif _chunks[0] == 'error': error = " ".join(_chunks[1:]) diff --git a/jmdaemon/jmdaemon/orderbookwatch.py b/jmdaemon/jmdaemon/orderbookwatch.py index 62edcb7..abd8b57 100644 --- a/jmdaemon/jmdaemon/orderbookwatch.py +++ b/jmdaemon/jmdaemon/orderbookwatch.py @@ -7,6 +7,7 @@ from decimal import InvalidOperation, Decimal from numbers import Integral from jmdaemon.protocol import JM_VERSION +from jmdaemon import fidelity_bond_sanity_check from jmbase.support import get_log, joinmarket_alert, DUST_THRESHOLD log = get_log() @@ -21,13 +22,12 @@ def dict_factory(cursor, row): class JMTakerError(Exception): pass - class OrderbookWatch(object): def set_msgchan(self, msgchan): self.msgchan = msgchan self.msgchan.register_orderbookwatch_callbacks(self.on_order_seen, - self.on_order_cancel) + self.on_order_cancel, self.on_fidelity_bond_seen) self.msgchan.register_channel_callbacks( self.on_welcome, self.on_set_topic, None, self.on_disconnect, self.on_nick_leave, None) @@ -41,6 +41,8 @@ class OrderbookWatch(object): self.db.execute("CREATE TABLE orderbook(counterparty TEXT, " "oid INTEGER, ordertype TEXT, minsize INTEGER, " "maxsize INTEGER, txfee INTEGER, cjfee TEXT);") + self.db.execute("CREATE TABLE fidelitybonds(counterparty TEXT, " + "takernick TEXT, proof TEXT);"); finally: self.dblock.release() @@ -134,11 +136,29 @@ class OrderbookWatch(object): finally: self.dblock.release() + def on_fidelity_bond_seen(self, nick, bond_type, fidelity_bond_proof_msg): + taker_nick = self.msgchan.nick + maker_nick = nick + if not fidelity_bond_sanity_check.fidelity_bond_sanity_check(fidelity_bond_proof_msg): + log.debug("Failed to verify fidelity bond for {}, skipping." + .format(maker_nick)) + return + try: + self.dblock.acquire(True) + self.db.execute("DELETE FROM fidelitybonds WHERE counterparty=?;", + (nick, )) + self.db.execute("INSERT INTO fidelitybonds VALUES(?, ?, ?);", + (nick, taker_nick, fidelity_bond_proof_msg)) + finally: + self.dblock.release() + def on_nick_leave(self, nick): try: self.dblock.acquire(True) self.db.execute('DELETE FROM orderbook WHERE counterparty=?;', (nick,)) + self.db.execute('DELETE FROM fidelitybonds WHERE counterparty=?;', + (nick,)) finally: self.dblock.release() @@ -146,5 +166,6 @@ class OrderbookWatch(object): try: self.dblock.acquire(True) self.db.execute('DELETE FROM orderbook;') + self.db.execute('DELETE FROM fidelitybonds;') finally: self.dblock.release() diff --git a/jmdaemon/jmdaemon/protocol.py b/jmdaemon/jmdaemon/protocol.py index 7d531aa..eefe72d 100644 --- a/jmdaemon/jmdaemon/protocol.py +++ b/jmdaemon/jmdaemon/protocol.py @@ -20,6 +20,8 @@ offertypes = {"reloffer": [(int, "oid"), (int, "minsize"), (int, "maxsize"), offername_list = list(offertypes.keys()) +fidelity_bond_cmd_list = ["tbond"] + ORDER_KEYS = ['counterparty', 'oid', 'ordertype', 'minsize', 'maxsize', 'txfee', 'cjfee'] diff --git a/jmdaemon/test/test_daemon_protocol.py b/jmdaemon/test/test_daemon_protocol.py index 26083e8..40a2415 100644 --- a/jmdaemon/test/test_daemon_protocol.py +++ b/jmdaemon/test/test_daemon_protocol.py @@ -84,7 +84,8 @@ class JMTestClientProtocol(JMBaseProtocol): show_receipt("JMUP") d = self.callRemote(JMSetup, role="TAKER", - initdata="none") + offers="{}", + fidelity_bond=b'') self.defaultCallbacks(d) return {'accepted': True} @@ -110,7 +111,7 @@ class JMTestClientProtocol(JMBaseProtocol): self.defaultCallbacks(d) @JMOffers.responder - def on_JM_OFFERS(self, orderbook): + def on_JM_OFFERS(self, orderbook, fidelitybonds): if end_early: return {'accepted': True} jlog.debug("JMOFFERS" + str(orderbook)) diff --git a/jmdaemon/test/test_message_channel.py b/jmdaemon/test/test_message_channel.py index 9ca3891..684128e 100644 --- a/jmdaemon/test/test_message_channel.py +++ b/jmdaemon/test/test_message_channel.py @@ -194,13 +194,13 @@ def test_setup_mc(): del mcc.active_channels[cp1] mcc.prepare_privmsg(cp1, "auth", "a b c") #try announcing orders; first public - mcc.announce_orders(t_orderbook) + mcc.announce_orders(t_orderbook, nick=None, fidelity_bond_proof_msg=None, new_mc=None) #try on fake mc - mcc.announce_orders(t_orderbook, new_mc="fakemc") + mcc.announce_orders(t_orderbook, nick=None, fidelity_bond_proof_msg=None, new_mc="fakemc") #direct to one cp - mcc.announce_orders(t_orderbook, nick=cp1) + mcc.announce_orders(t_orderbook, nick=cp1, fidelity_bond_proof_msg=None, new_mc=None) #direct to one cp on one mc - mcc.announce_orders(t_orderbook, nick=cp1, new_mc=dmcs[0]) + mcc.announce_orders(t_orderbook, nick=cp1, fidelity_bond_proof_msg=None, new_mc=dmcs[0]) #Next, set up 6 counterparties and fill their offers, #send txs to them cps = [make_valid_nick(i) for i in range(1, 7)] diff --git a/jmdaemon/test/test_orderbookwatch.py b/jmdaemon/test/test_orderbookwatch.py index d02be08..e07ff23 100644 --- a/jmdaemon/test/test_orderbookwatch.py +++ b/jmdaemon/test/test_orderbookwatch.py @@ -6,6 +6,9 @@ from jmdaemon.orderbookwatch import OrderbookWatch from jmdaemon import IRCMessageChannel from jmclient import get_irc_mchannels, load_test_config from jmdaemon.protocol import JM_VERSION, ORDER_KEYS +from jmbase.support import hextobin +from jmbitcoin.fidelity_bond import FidelityBondProof + class DummyDaemon(object): def request_signature_verify(self, a, b, c, d, e, f, g, h): @@ -125,4 +128,234 @@ def test_disconnect_leave(): - +@pytest.mark.parametrize( + "valid, fidelity_bond_proof, maker_nick, taker_nick", + [ + ( + True, + { + #nicksig len = 71, certsig len = 71 + "nick-signature": (b'0E\x02!\x00\xdbb\x15\x96\xa0\x87\xb8\x1d\xe05\xddV\xa1\x1bn\x8f' + + b'q\x90&\x8cG@\x89"2\xb2\x81\x9b\xc00\xa5\xb6\x02 \x03\x14l\xd7BR\xba\x8c:\x88(' + + b'\x8e3l\xac\xf5`T\x87\xfa\xf5\xa9\x1f\x19\xc0\xb6\xe9\xbb\xdc\xc7y\x99'), + "certificate-signature": ("3045022100eb512af938113badb4d7b29e0c22061c51dadb113a9395e" + + "9ed81a46103391213022029170de414964f07228c4f0d404b1386272bae337f0133f1329d948a" + + "252fa2a0"), + "certificate-pubkey": "0258efb077960d6848f001904857f062fa453de26c1ad8736f55497254f56e8a74", + "certificate-expiry": 1, + "utxo-pubkey": "02f54f027377e84171296453828aa863c23fc4489453025f49bd3addfb3a359b3d", + "txid": "84c88fafe0bb75f507fe3bfb29a93d10b2e80c15a63b2943c1a5fecb5a55cba2", + "vout": 0, + "locktime": 1640995200 + }, + "J5A4k9ecQzRRDfBx", + "J55VZ6U6ZyFDNeuv" + ), + ( + True, + { + #nicksig len = 71, certsig len = 70 + "nick-signature": (b'0E\x02!\x00\x80\xc6$\x0c\xa1\x15YS\xacHB\xb33\xfa~\x9f\xb9`\xb3' + + b'\xfe\xed0\xadHq\xc1~\x03.B\xbb#\x02 #y~]\xd9\xbbX2\xc0\x1b\xe57\xf4\x0f\x1f' + + b'\xd6$\x01\xf9\x15Z\xc9X\xa5\x18\xbe\x83\x1a&4Y\xd4'), + "certificate-signature": ("304402205669ea394f7381e9abf0b3c013fac2b79d24c02feb86ff153" + + "cff83c658d7cf7402200b295ace655687f80738f3733c1dc5f1e2b8f351c017a05b8bd31983dd" + + "4d723f"), + "certificate-pubkey": "031d1c006a6310dbdf57341efc19c3a43c402379d7ccd2480416cadc7579f973f7", + "certificate-expiry": 1, + "utxo-pubkey": "02616c56412eb738a9eacfb0550b43a5a2e77e5d5205ea9e2ca8dfac34e50c9754", + "txid": "84c88fafe0bb75f507fe3bfb29a93d10b2e80c15a63b2943c1a5fecb5a55cba2", + "vout": 1, + "locktime": 1893456000 + }, + "J54LS6YyJPoseqFS", + "J55VZ6U6ZyFDNeuv" + ), + ( + True, + { #nicksig len = 70, certsig len = 71 + "nick-signature": (b'0D\x02 K)\xe9\x17d\x0b\xc0\x82(\xd1\xa2*l\xd8\x0eJ\xc7\x01NV\xbf' + + b'\xcb\x02O]\xc0\x11\x01\x01B"\xed\x02 ob\xa1\xf8>\x80U)\xc8\x96\x86\x1b \x0e' + + b'\x00.\xf8\x86}\xcd\xf8\x82T\xa2\xb5\x8a4\xdb4\xbe\xf3{'), + "certificate-signature": ("3045022100d3beb5660bef33d095f92a3023bbbab15ece48ab2f211fa" + + "935b62fe8b764c8c002204892deffb4c9aa0d734aa3f55cc8e2baae4a03fc5a9e571b4f671493" + + "f1254df9"), + "certificate-pubkey": "03a2d1d15290d6d21204d1153c062970b4ff757a675e47a451fd0ba5c084127807", + "certificate-expiry": 1, + "utxo-pubkey": "03b9c12c9c31286772349b986653d07232327b284bd0787ad5829a04ac68f59b89", + "txid": "70c2995b283db086813d97817264f10b8823b870298d30ab09cb43c6bf2670cf", + "vout": 0, + "locktime": 1735689600 + }, + "J59PRzM6ZsdA5uyJ", + "J55VZ6U6ZyFDNeuv" + ), + ( + False, + { #nick signature with no DER header + "nick-signature": (b'ZD\x02 K)\xe9\x17d\x0b\xc0\x82(\xd1\xa2*l\xd8\x0eJ\xc7\x01NV\xbf' + + b'\xcb\x02O]\xc0\x11\x01\x01B"\xed\x02 ob\xa1\xf8>\x80U)\xc8\x96\x86\x1b \x0e' + + b'\x00.\xf8\x86}\xcd\xf8\x82T\xa2\xb5\x8a4\xdb4\xbe\xf3{'), + "certificate-signature": ("3045022100d3beb5660bef33d095f92a3023bbbab15ece48ab2f211fa" + + "935b62fe8b764c8c002204892deffb4c9aa0d734aa3f55cc8e2baae4a03fc5a9e571b4f671493" + + "f1254df9"), + "certificate-pubkey": "03a2d1d15290d6d21204d1153c062970b4ff757a675e47a451fd0ba5c084127807", + "certificate-expiry": 1, + "utxo-pubkey": "03b9c12c9c31286772349b986653d07232327b284bd0787ad5829a04ac68f59b89", + "txid": "70c2995b283db086813d97817264f10b8823b870298d30ab09cb43c6bf2670cf", + "vout": 0, + "locktime": 1735689600 + }, + "J59PRzM6ZsdA5uyJ", + "J55VZ6U6ZyFDNeuv" + ), + ( + False, + { #nick signature which fails ecdsa_verify + "nick-signature": (b'0E\x02 K)\xe9\x17d\x0b\xc0\x82(\xd1\xa2*l\xd8\x0eJ\xc7\x01NV\xbf' + + b'\xcb\x02O]\xc0\x11\x01\x01B"\xed\x02 ob\xa1\xf8>\x80U)\xc8\x96\x86\x1b \x0e' + + b'\x00.\xf8\x86}\xcd\xf8\x82T\xa2\xb5\x8a4\xdb4\xbe\xf3{'), + "certificate-signature": ("3045022100d3beb5660bef33d095f92a3023bbbab15ece48ab2f211fa" + + "935b62fe8b764c8c002204892deffb4c9aa0d734aa3f55cc8e2baae4a03fc5a9e571b4f671493" + + "f1254df9"), + "certificate-pubkey": "03a2d1d15290d6d21204d1153c062970b4ff757a675e47a451fd0ba5c084127807", + "certificate-expiry": 1, + "utxo-pubkey": "03b9c12c9c31286772349b986653d07232327b284bd0787ad5829a04ac68f59b89", + "txid": "70c2995b283db086813d97817264f10b8823b870298d30ab09cb43c6bf2670cf", + "vout": 0, + "locktime": 1735689600 + }, + "J59PRzM6ZsdA5uyJ", + "J55VZ6U6ZyFDNeuv" + ), + ( + False, + { #cert signature which fails ecdsa_verify + "nick-signature": (b'0D\x02 K)\xe9\x17d\x0b\xc0\x82(\xd1\xa2*l\xd8\x0eJ\xc7\x01NV\xbf' + + b'\xcb\x02O]\xc0\x11\x01\x01B"\xed\x02 ob\xa1\xf8>\x80U)\xc8\x96\x86\x1b \x0e' + + b'\x00.\xf8\x86}\xcd\xf8\x82T\xa2\xb5\x8a4\xdb4\xbe\xf3{'), + "certificate-signature": ("3055022100d3beb5660bef33d095f92a3023bbbab15ece48ab2f211fa" + + "935b62fe8b764c8c002204892deffb4c9aa0d734aa3f55cc8e2baae4a03fc5a9e571b4f671493" + + "f1254df9"), + "certificate-pubkey": "03a2d1d15290d6d21204d1153c062970b4ff757a675e47a451fd0ba5c084127807", + "certificate-expiry": 1, + "utxo-pubkey": "03b9c12c9c31286772349b986653d07232327b284bd0787ad5829a04ac68f59b89", + "txid": "70c2995b283db086813d97817264f10b8823b870298d30ab09cb43c6bf2670cf", + "vout": 0, + "locktime": 1735689600 + }, + "J59PRzM6ZsdA5uyJ", + "J55VZ6U6ZyFDNeuv" + ) + ]) +def test_fidelity_bond_seen(valid, fidelity_bond_proof, maker_nick, taker_nick): + proof = FidelityBondProof( + maker_nick, taker_nick, hextobin(fidelity_bond_proof['certificate-pubkey']), + fidelity_bond_proof['certificate-expiry'], + hextobin(fidelity_bond_proof['certificate-signature']), + (hextobin(fidelity_bond_proof['txid']), fidelity_bond_proof['vout']), + hextobin(fidelity_bond_proof['utxo-pubkey']), fidelity_bond_proof['locktime'] + ) + serialized = proof._serialize_proof_msg(fidelity_bond_proof['nick-signature']) + + ob = get_ob() + ob.msgchan.nick = taker_nick + ob.on_fidelity_bond_seen(maker_nick, fidelity_bond_cmd_list[0], serialized) + rows = ob.db.execute("SELECT * FROM fidelitybonds;").fetchall() + assert len(rows) == 1 + assert rows[0]["counterparty"] == maker_nick + assert rows[0]["takernick"] == taker_nick + try: + parsed_proof = FidelityBondProof.parse_and_verify_proof_msg(rows[0]["counterparty"], + rows[0]["takernick"], rows[0]["proof"]) + except ValueError: + parsed_proof = None + if valid: + assert len(rows) == 1 + assert rows[0]["counterparty"] == maker_nick + assert rows[0]["vout"] == fidelity_bond_proof["vout"] + assert rows[0]["locktime"] == fidelity_bond_proof["locktime"] + assert rows[0]["certexpiry"] == fidelity_bond_proof["certificate-expiry"] + assert rows[0]["txid"] == hextobin(fidelity_bond_proof["txid"]) + assert rows[0]["utxopubkey"] == hextobin(fidelity_bond_proof["utxo-pubkey"]) + else: + assert len(rows) == 0 + +def test_duplicate_fidelity_bond_rejected(): + + fidelity_bond_info = ( + ( + { + "nick-signature": (b'0E\x02!\x00\xdbb\x15\x96\xa0\x87\xb8\x1d\xe05\xddV\xa1\x1bn\x8f' + + b'q\x90&\x8cG@\x89"2\xb2\x81\x9b\xc00\xa5\xb6\x02 \x03\x14l\xd7BR\xba\x8c:\x88(' + + b'\x8e3l\xac\xf5`T\x87\xfa\xf5\xa9\x1f\x19\xc0\xb6\xe9\xbb\xdc\xc7y\x99'), + "certificate-signature": ("3045022100eb512af938113badb4d7b29e0c22061c51dadb113a9395e" + + "9ed81a46103391213022029170de414964f07228c4f0d404b1386272bae337f0133f1329d948a" + + "252fa2a0"), + "certificate-pubkey": "0258efb077960d6848f001904857f062fa453de26c1ad8736f55497254f56e8a74", + "certificate-expiry": 1, + "utxo-pubkey": "02f54f027377e84171296453828aa863c23fc4489453025f49bd3addfb3a359b3d", + "txid": "84c88fafe0bb75f507fe3bfb29a93d10b2e80c15a63b2943c1a5fecb5a55cba2", + "vout": 0, + "locktime": 1640995200 + }, + "J5A4k9ecQzRRDfBx", + "J55VZ6U6ZyFDNeuv" + ), + ( + { + "nick-signature": (b'0E\x02!\x00\x80\xc6$\x0c\xa1\x15YS\xacHB\xb33\xfa~\x9f\xb9`\xb3' + + b'\xfe\xed0\xadHq\xc1~\x03.B\xbb#\x02 #y~]\xd9\xbbX2\xc0\x1b\xe57\xf4\x0f\x1f' + + b'\xd6$\x01\xf9\x15Z\xc9X\xa5\x18\xbe\x83\x1a&4Y\xd4'), + "certificate-signature": ("304402205669ea394f7381e9abf0b3c013fac2b79d24c02feb86ff153" + + "cff83c658d7cf7402200b295ace655687f80738f3733c1dc5f1e2b8f351c017a05b8bd31983dd" + + "4d723f"), + "certificate-pubkey": "031d1c006a6310dbdf57341efc19c3a43c402379d7ccd2480416cadc7579f973f7", + "certificate-expiry": 1, + "utxo-pubkey": "02616c56412eb738a9eacfb0550b43a5a2e77e5d5205ea9e2ca8dfac34e50c9754", + "txid": "84c88fafe0bb75f507fe3bfb29a93d10b2e80c15a63b2943c1a5fecb5a55cba2", + "vout": 1, + "locktime": 1893456000 + }, + "J54LS6YyJPoseqFS", + "J55VZ6U6ZyFDNeuv" + ) + ) + + ob = get_ob() + + fidelity_bond_proof1, maker_nick1, taker_nick1 = fidelity_bond_info[0] + proof = FidelityBondProof( + maker_nick1, taker_nick1, hextobin(fidelity_bond_proof1['certificate-pubkey']), + fidelity_bond_proof1['certificate-expiry'], + hextobin(fidelity_bond_proof1['certificate-signature']), + (hextobin(fidelity_bond_proof1['txid']), fidelity_bond_proof1['vout']), + hextobin(fidelity_bond_proof1['utxo-pubkey']), fidelity_bond_proof1['locktime'] + ) + serialized1 = proof._serialize_proof_msg(fidelity_bond_proof1['nick-signature']) + ob.msgchan.nick = taker_nick1 + + ob.on_fidelity_bond_seen(maker_nick1, fidelity_bond_cmd_list[0], serialized1) + rows = ob.db.execute("SELECT * FROM fidelitybonds;").fetchall() + assert len(rows) == 1 + + #show the same fidelity bond message again, check it gets rejected as duplicate + ob.on_fidelity_bond_seen(maker_nick1, fidelity_bond_cmd_list[0], serialized1) + rows = ob.db.execute("SELECT * FROM fidelitybonds;").fetchall() + assert len(rows) == 1 + + #show a different fidelity bond and check it does get accepted + fidelity_bond_proof2, maker_nick2, taker_nick2 = fidelity_bond_info[1] + proof2 = FidelityBondProof( + maker_nick1, taker_nick1, hextobin(fidelity_bond_proof2['certificate-pubkey']), + fidelity_bond_proof2['certificate-expiry'], + hextobin(fidelity_bond_proof2['certificate-signature']), + (hextobin(fidelity_bond_proof2['txid']), fidelity_bond_proof2['vout']), + hextobin(fidelity_bond_proof2['utxo-pubkey']), fidelity_bond_proof2['locktime'] + ) + serialized2 = proof2._serialize_proof_msg(fidelity_bond_proof2['nick-signature']) + ob.msgchan.nick = taker_nick2 + + ob.on_fidelity_bond_seen(maker_nick2, fidelity_bond_cmd_list[0], serialized2) + rows = ob.db.execute("SELECT * FROM fidelitybonds;").fetchall() + assert len(rows) == 2 diff --git a/scripts/obwatch/ob-watcher.py b/scripts/obwatch/ob-watcher.py index f5339ab..b0cdc84 100755 --- a/scripts/obwatch/ob-watcher.py +++ b/scripts/obwatch/ob-watcher.py @@ -128,7 +128,6 @@ def create_choose_units_form(selected_btc, selected_rel): '