Browse Source

Handle fidelity bonds in client-server protocol

Parse incoming and announce outgoing fidelity bond messages

Fidelity bond proof messages will be checked and added to the internal
database just like offers. Such messages are not announced in public
but only directly to takers who ask for them, this is because the
signature proofs must commmit to the maker's and taker's IRC nicknames
in order to avoid replay attacks.
master
chris-belcher 6 years ago
parent
commit
6b6fc4ae5d
No known key found for this signature in database
GPG Key ID: EF734EA677F31129
  1. 8
      jmbase/jmbase/commands.py
  2. 14
      jmbase/test/test_commands.py
  3. 10
      jmclient/jmclient/client_protocol.py
  4. 136
      jmclient/jmclient/fidelity_bond.py
  5. 17
      jmclient/jmclient/wallet.py
  6. 4
      jmclient/test/test_client_protocol.py
  7. 1
      jmdaemon/jmdaemon/__init__.py
  8. 21
      jmdaemon/jmdaemon/daemon_protocol.py
  9. 12
      jmdaemon/jmdaemon/fidelity_bond_sanity_check.py
  10. 36
      jmdaemon/jmdaemon/message_channel.py
  11. 25
      jmdaemon/jmdaemon/orderbookwatch.py
  12. 2
      jmdaemon/jmdaemon/protocol.py
  13. 5
      jmdaemon/test/test_daemon_protocol.py
  14. 8
      jmdaemon/test/test_message_channel.py
  15. 235
      jmdaemon/test/test_orderbookwatch.py
  16. 2
      scripts/obwatch/ob-watcher.py

8
jmbase/jmbase/commands.py

@ -3,9 +3,10 @@ Commands defining client-server (daemon)
messaging protocol (*not* Joinmarket p2p protocol). messaging protocol (*not* Joinmarket p2p protocol).
Used for AMP asynchronous messages. 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 from .bigstring import BigUnicode
class DaemonNotReady(Exception): class DaemonNotReady(Exception):
pass pass
@ -45,7 +46,8 @@ class JMSetup(JMCommand):
role, passes initial offers for announcement (for TAKER, this data is "none") role, passes initial offers for announcement (for TAKER, this data is "none")
""" """
arguments = [(b'role', Unicode()), arguments = [(b'role', Unicode()),
(b'initdata', Unicode())] (b'offers', Unicode()),
(b'fidelity_bond', String())]
class JMMsgSignature(JMCommand): class JMMsgSignature(JMCommand):
"""A response to a request for a bitcoin signature """A response to a request for a bitcoin signature
@ -376,4 +378,4 @@ class BIP78InfoMsg(JMCommand):
from the daemon about current status at from the daemon about current status at
network level. network level.
""" """
arguments = [(b'infomsg', Unicode())] arguments = [(b'infomsg', Unicode())]

14
jmbase/test/test_commands.py

@ -63,8 +63,8 @@ class JMTestServerProtocol(JMBaseProtocol):
return {'accepted': True} return {'accepted': True}
@JMSetup.responder @JMSetup.responder
def on_JM_SETUP(self, role, initdata): def on_JM_SETUP(self, role, offers, use_fidelity_bond):
show_receipt("JMSETUP", role, initdata) show_receipt("JMSETUP", role, offers, use_fidelity_bond)
d = self.callRemote(JMSetupDone) d = self.callRemote(JMSetupDone)
self.defaultCallbacks(d) self.defaultCallbacks(d)
return {'accepted': True} return {'accepted': True}
@ -75,7 +75,8 @@ class JMTestServerProtocol(JMBaseProtocol):
#build a huge orderbook to test BigString Argument #build a huge orderbook to test BigString Argument
orderbook = ["aaaa" for _ in range(2**15)] orderbook = ["aaaa" for _ in range(2**15)]
d = self.callRemote(JMOffers, d = self.callRemote(JMOffers,
orderbook=json.dumps(orderbook)) orderbook=json.dumps(orderbook),
fidelitybonds="dummyfidelitybonds")
self.defaultCallbacks(d) self.defaultCallbacks(d)
return {'accepted': True} return {'accepted': True}
@ -156,7 +157,8 @@ class JMTestClientProtocol(JMBaseProtocol):
show_receipt("JMUP") show_receipt("JMUP")
d = self.callRemote(JMSetup, d = self.callRemote(JMSetup,
role="TAKER", role="TAKER",
initdata="none") offers="{}",
use_fidelity_bond=False)
self.defaultCallbacks(d) self.defaultCallbacks(d)
return {'accepted': True} return {'accepted': True}
@ -177,8 +179,8 @@ class JMTestClientProtocol(JMBaseProtocol):
return {'accepted': True} return {'accepted': True}
@JMOffers.responder @JMOffers.responder
def on_JM_OFFERS(self, orderbook): def on_JM_OFFERS(self, orderbook, fidelitybonds):
show_receipt("JMOFFERS", orderbook) show_receipt("JMOFFERS", orderbook, fidelitybonds)
d = self.callRemote(JMFill, d = self.callRemote(JMFill,
amount=100, amount=100,
commitment="dummycommitment", commitment="dummycommitment",

10
jmclient/jmclient/client_protocol.py

@ -392,9 +392,14 @@ class JMMakerClientProtocol(JMClientProtocol):
if not self.client.offerlist: if not self.client.offerlist:
return return
self.offers_ready_loop.stop() 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, d = self.callRemote(commands.JMSetup,
role="MAKER", role="MAKER",
initdata=json.dumps(self.client.offerlist)) offers=json.dumps(self.client.offerlist),
fidelity_bond=fidelity_bond_data)
self.defaultCallbacks(d) self.defaultCallbacks(d)
@commands.JMSetupDone.responder @commands.JMSetupDone.responder
@ -627,7 +632,8 @@ class JMTakerClientProtocol(JMClientProtocol):
def on_JM_UP(self): def on_JM_UP(self):
d = self.callRemote(commands.JMSetup, d = self.callRemote(commands.JMSetup,
role="TAKER", role="TAKER",
initdata="none") offers="{}",
fidelity_bond=b'')
self.defaultCallbacks(d) self.defaultCallbacks(d)
return {'accepted': True} return {'accepted': True}

136
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

17
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)) 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 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): class BIP49Wallet(BIP32PurposedWallet):
_PURPOSE = 2**31 + 49 _PURPOSE = 2**31 + 49
_ENGINE = ENGINES[TYPE_P2SH_P2WPKH] _ENGINE = ENGINES[TYPE_P2SH_P2WPKH]

4
jmclient/test/test_client_protocol.py

@ -184,8 +184,8 @@ class JMTestServerProtocol(JMBaseProtocol):
return {'accepted': True} return {'accepted': True}
@JMSetup.responder @JMSetup.responder
def on_JM_SETUP(self, role, initdata): def on_JM_SETUP(self, role, offers, fidelity_bond):
show_receipt("JMSETUP", role, initdata) show_receipt("JMSETUP", role, offers, fidelity_bond)
d = self.callRemote(JMSetupDone) d = self.callRemote(JMSetupDone)
self.defaultCallbacks(d) self.defaultCallbacks(d)
return {'accepted': True} return {'accepted': True}

1
jmdaemon/jmdaemon/__init__.py

@ -14,6 +14,7 @@ from .daemon_protocol import (JMDaemonServerProtocolFactory, JMDaemonServerProto
from .protocol import (COMMAND_PREFIX, ORDER_KEYS, NICK_HASH_LENGTH, from .protocol import (COMMAND_PREFIX, ORDER_KEYS, NICK_HASH_LENGTH,
NICK_MAX_ENCODED, JM_VERSION, JOINMARKET_NICK_HEADER) NICK_MAX_ENCODED, JM_VERSION, JOINMARKET_NICK_HEADER)
from .message_channel import MessageChannelCollection from .message_channel import MessageChannelCollection
# Set default logging handler to avoid "No handler found" warnings. # Set default logging handler to avoid "No handler found" warnings.
try: try:
from logging import NullHandler from logging import NullHandler

21
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, get_nontor_agent, BytesProducer, wrapped_urlparse,
bdict_sdict_convert, JMHTTPResource) bdict_sdict_convert, JMHTTPResource)
from jmbase.commands import * from jmbase.commands import *
from jmbitcoin.fidelity_bond import FidelityBond
from twisted.protocols import amp from twisted.protocols import amp
from twisted.internet import reactor, ssl, task from twisted.internet import reactor, ssl, task
from twisted.internet.protocol import ServerFactory from twisted.internet.protocol import ServerFactory
@ -474,6 +475,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
self.crypto_boxes = {} self.crypto_boxes = {}
self.sig_lock = threading.Lock() self.sig_lock = threading.Lock()
self.active_orders = {} self.active_orders = {}
self.fidelity_bond = None
def checkClientResponse(self, response): def checkClientResponse(self, response):
"""A generic check of client acceptance; any failure """A generic check of client acceptance; any failure
@ -551,7 +553,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
return {'accepted': True} return {'accepted': True}
@JMSetup.responder @JMSetup.responder
def on_JM_SETUP(self, role, initdata): def on_JM_SETUP(self, role, offers, fidelity_bond):
assert self.jm_state == 0 assert self.jm_state == 0
self.role = role self.role = role
self.crypto_boxes = {} self.crypto_boxes = {}
@ -565,8 +567,10 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
if self.role == "TAKER": if self.role == "TAKER":
self.mcc.pubmsg(COMMAND_PREFIX + "orderbook") self.mcc.pubmsg(COMMAND_PREFIX + "orderbook")
elif self.role == "MAKER": elif self.role == "MAKER":
self.offerlist = json.loads(initdata) self.offerlist = json.loads(offers)
self.mcc.announce_orders(self.offerlist) if fidelity_bond:
self.fidelity_bond = FidelityBond.deserialize(fidelity_bond)
self.mcc.announce_orders(self.offerlist, None, None, None)
self.jm_state = 1 self.jm_state = 1
return {'accepted': True} return {'accepted': True}
@ -711,7 +715,15 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
def on_orderbook_requested(self, nick, mc=None): def on_orderbook_requested(self, nick, mc=None):
"""Dealt with by daemon, assuming offerlist is up to date """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 @maker_only
def on_order_fill(self, nick, oid, amount, taker_pk, commit): def on_order_fill(self, nick, oid, amount, taker_pk, commit):
@ -1015,6 +1027,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
if self.mcc: if self.mcc:
self.mcc.shutdown() self.mcc.shutdown()
class JMDaemonServerProtocolFactory(ServerFactory): class JMDaemonServerProtocolFactory(ServerFactory):
protocol = JMDaemonServerProtocol protocol = JMDaemonServerProtocol

12
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

36
jmdaemon/jmdaemon/message_channel.py

@ -6,7 +6,8 @@ import threading
from twisted.internet import reactor from twisted.internet import reactor
from jmdaemon import encrypt_encode, decode_decrypt, COMMAND_PREFIX,\ from jmdaemon import encrypt_encode, decode_decrypt, COMMAND_PREFIX,\
NICK_HASH_LENGTH, NICK_MAX_ENCODED, plaintext_commands,\ 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 jmbase.support import get_log
from functools import wraps from functools import wraps
@ -284,7 +285,7 @@ class MessageChannelCollection(object):
"; cannot find on any message channel.") "; cannot find on any message channel.")
return 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 """Send orders defined in list orderlist either
to the shared public channel (pit), on all to the shared public channel (pit), on all
message channels, if nick=None, message channels, if nick=None,
@ -301,6 +302,7 @@ class MessageChannelCollection(object):
"Tried to announce orders on an unavailable message channel.") "Tried to announce orders on an unavailable message channel.")
return return
if nick is None: if nick is None:
assert fidelity_bond_proof_msg is None
for mc in self.available_channels(): for mc in self.available_channels():
mc.announce_orders(orderlines) mc.announce_orders(orderlines)
else: else:
@ -310,6 +312,9 @@ class MessageChannelCollection(object):
cmd = orderlist[0]['ordertype'] cmd = orderlist[0]['ordertype']
msg = ' '.join(orderlines[0].split(' ')[1:]) msg = ' '.join(orderlines[0].split(' ')[1:])
msg += ''.join(orderlines[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: if new_mc:
self.prepare_privmsg(nick, cmd, msg, mc=new_mc) self.prepare_privmsg(nick, cmd, msg, mc=new_mc)
else: else:
@ -563,7 +568,8 @@ class MessageChannelCollection(object):
# orderbook watcher commands # orderbook watcher commands
def register_orderbookwatch_callbacks(self, def register_orderbookwatch_callbacks(self,
on_order_seen=None, on_order_seen=None,
on_order_cancel=None): on_order_cancel=None,
on_fidelity_bond_seen=None):
"""Special cases: """Special cases:
on_order_seen: use it as a trigger for presence of nick. on_order_seen: use it as a trigger for presence of nick.
on_order_cancel: what happens if cancel/modify in one place 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 self.on_order_seen = on_order_seen
for mc in self.mchannels: for mc in self.mchannels:
mc.register_orderbookwatch_callbacks(self.on_order_seen_trigger, 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): def on_orderbook_requested_trigger(self, nick, mc):
"""Update nicks_seen state to reflect presence of """Update nicks_seen state to reflect presence of
@ -647,6 +653,7 @@ class MessageChannel(object):
# orderbook watch functions # orderbook watch functions
self.on_order_seen = None self.on_order_seen = None
self.on_order_cancel = None self.on_order_cancel = None
self.on_fidelity_bond_seen = None
# taker functions # taker functions
self.on_error = None self.on_error = None
self.on_pubkey = None self.on_pubkey = None
@ -730,9 +737,11 @@ class MessageChannel(object):
# orderbook watcher commands # orderbook watcher commands
def register_orderbookwatch_callbacks(self, def register_orderbookwatch_callbacks(self,
on_order_seen=None, 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_seen = on_order_seen
self.on_order_cancel = on_order_cancel self.on_order_cancel = on_order_cancel
self.on_fidelity_bond_seen = on_fidelity_bond_seen
# taker commands # taker commands
def register_taker_callbacks(self, def register_taker_callbacks(self,
@ -813,6 +822,21 @@ class MessageChannel(object):
return True return True
return False 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): def cancel_orders(self, oid_list):
clines = [COMMAND_PREFIX + 'cancel ' + str(oid) for oid in oid_list] clines = [COMMAND_PREFIX + 'cancel ' + str(oid) for oid in oid_list]
self.pubmsg(''.join(clines)) self.pubmsg(''.join(clines))
@ -952,6 +976,8 @@ class MessageChannel(object):
# orderbook watch commands # orderbook watch commands
if self.check_for_orders(nick, _chunks): if self.check_for_orders(nick, _chunks):
pass pass
elif self.check_for_fidelity_bond(nick, _chunks):
pass
# taker commands # taker commands
elif _chunks[0] == 'error': elif _chunks[0] == 'error':
error = " ".join(_chunks[1:]) error = " ".join(_chunks[1:])

25
jmdaemon/jmdaemon/orderbookwatch.py

@ -7,6 +7,7 @@ from decimal import InvalidOperation, Decimal
from numbers import Integral from numbers import Integral
from jmdaemon.protocol import JM_VERSION from jmdaemon.protocol import JM_VERSION
from jmdaemon import fidelity_bond_sanity_check
from jmbase.support import get_log, joinmarket_alert, DUST_THRESHOLD from jmbase.support import get_log, joinmarket_alert, DUST_THRESHOLD
log = get_log() log = get_log()
@ -21,13 +22,12 @@ def dict_factory(cursor, row):
class JMTakerError(Exception): class JMTakerError(Exception):
pass pass
class OrderbookWatch(object): class OrderbookWatch(object):
def set_msgchan(self, msgchan): def set_msgchan(self, msgchan):
self.msgchan = msgchan self.msgchan = msgchan
self.msgchan.register_orderbookwatch_callbacks(self.on_order_seen, 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.msgchan.register_channel_callbacks(
self.on_welcome, self.on_set_topic, None, self.on_disconnect, self.on_welcome, self.on_set_topic, None, self.on_disconnect,
self.on_nick_leave, None) self.on_nick_leave, None)
@ -41,6 +41,8 @@ class OrderbookWatch(object):
self.db.execute("CREATE TABLE orderbook(counterparty TEXT, " self.db.execute("CREATE TABLE orderbook(counterparty TEXT, "
"oid INTEGER, ordertype TEXT, minsize INTEGER, " "oid INTEGER, ordertype TEXT, minsize INTEGER, "
"maxsize INTEGER, txfee INTEGER, cjfee TEXT);") "maxsize INTEGER, txfee INTEGER, cjfee TEXT);")
self.db.execute("CREATE TABLE fidelitybonds(counterparty TEXT, "
"takernick TEXT, proof TEXT);");
finally: finally:
self.dblock.release() self.dblock.release()
@ -134,11 +136,29 @@ class OrderbookWatch(object):
finally: finally:
self.dblock.release() 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): def on_nick_leave(self, nick):
try: try:
self.dblock.acquire(True) self.dblock.acquire(True)
self.db.execute('DELETE FROM orderbook WHERE counterparty=?;', self.db.execute('DELETE FROM orderbook WHERE counterparty=?;',
(nick,)) (nick,))
self.db.execute('DELETE FROM fidelitybonds WHERE counterparty=?;',
(nick,))
finally: finally:
self.dblock.release() self.dblock.release()
@ -146,5 +166,6 @@ class OrderbookWatch(object):
try: try:
self.dblock.acquire(True) self.dblock.acquire(True)
self.db.execute('DELETE FROM orderbook;') self.db.execute('DELETE FROM orderbook;')
self.db.execute('DELETE FROM fidelitybonds;')
finally: finally:
self.dblock.release() self.dblock.release()

2
jmdaemon/jmdaemon/protocol.py

@ -20,6 +20,8 @@ offertypes = {"reloffer": [(int, "oid"), (int, "minsize"), (int, "maxsize"),
offername_list = list(offertypes.keys()) offername_list = list(offertypes.keys())
fidelity_bond_cmd_list = ["tbond"]
ORDER_KEYS = ['counterparty', 'oid', 'ordertype', 'minsize', 'maxsize', 'txfee', ORDER_KEYS = ['counterparty', 'oid', 'ordertype', 'minsize', 'maxsize', 'txfee',
'cjfee'] 'cjfee']

5
jmdaemon/test/test_daemon_protocol.py

@ -84,7 +84,8 @@ class JMTestClientProtocol(JMBaseProtocol):
show_receipt("JMUP") show_receipt("JMUP")
d = self.callRemote(JMSetup, d = self.callRemote(JMSetup,
role="TAKER", role="TAKER",
initdata="none") offers="{}",
fidelity_bond=b'')
self.defaultCallbacks(d) self.defaultCallbacks(d)
return {'accepted': True} return {'accepted': True}
@ -110,7 +111,7 @@ class JMTestClientProtocol(JMBaseProtocol):
self.defaultCallbacks(d) self.defaultCallbacks(d)
@JMOffers.responder @JMOffers.responder
def on_JM_OFFERS(self, orderbook): def on_JM_OFFERS(self, orderbook, fidelitybonds):
if end_early: if end_early:
return {'accepted': True} return {'accepted': True}
jlog.debug("JMOFFERS" + str(orderbook)) jlog.debug("JMOFFERS" + str(orderbook))

8
jmdaemon/test/test_message_channel.py

@ -194,13 +194,13 @@ def test_setup_mc():
del mcc.active_channels[cp1] del mcc.active_channels[cp1]
mcc.prepare_privmsg(cp1, "auth", "a b c") mcc.prepare_privmsg(cp1, "auth", "a b c")
#try announcing orders; first public #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 #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 #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 #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, #Next, set up 6 counterparties and fill their offers,
#send txs to them #send txs to them
cps = [make_valid_nick(i) for i in range(1, 7)] cps = [make_valid_nick(i) for i in range(1, 7)]

235
jmdaemon/test/test_orderbookwatch.py

@ -6,6 +6,9 @@ from jmdaemon.orderbookwatch import OrderbookWatch
from jmdaemon import IRCMessageChannel from jmdaemon import IRCMessageChannel
from jmclient import get_irc_mchannels, load_test_config from jmclient import get_irc_mchannels, load_test_config
from jmdaemon.protocol import JM_VERSION, ORDER_KEYS from jmdaemon.protocol import JM_VERSION, ORDER_KEYS
from jmbase.support import hextobin
from jmbitcoin.fidelity_bond import FidelityBondProof
class DummyDaemon(object): class DummyDaemon(object):
def request_signature_verify(self, a, b, c, d, e, def request_signature_verify(self, a, b, c, d, e,
f, g, h): 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

2
scripts/obwatch/ob-watcher.py

@ -128,7 +128,6 @@ def create_choose_units_form(selected_btc, selected_rel):
'<option selected="selected">' + selected_rel) '<option selected="selected">' + selected_rel)
return choose_units_form return choose_units_form
class OrderbookPageRequestHeader(http.server.SimpleHTTPRequestHandler): class OrderbookPageRequestHeader(http.server.SimpleHTTPRequestHandler):
def __init__(self, request, client_address, base_server): def __init__(self, request, client_address, base_server):
self.taker = base_server.taker self.taker = base_server.taker
@ -244,6 +243,7 @@ class OrderbookPageRequestHeader(http.server.SimpleHTTPRequestHandler):
return 0, result return 0, result
#print("len rows before filter: " + str(len(rows))) #print("len rows before filter: " + str(len(rows)))
rows = [o for o in rows if o["ordertype"] in filtered_offername_list] rows = [o for o in rows if o["ordertype"] in filtered_offername_list]
order_keys_display = (('ordertype', ordertype_display), order_keys_display = (('ordertype', ordertype_display),
('counterparty', do_nothing), ('oid', order_str), ('counterparty', do_nothing), ('oid', order_str),
('cjfee', cjfee_display), ('txfee', satoshi_to_unit), ('cjfee', cjfee_display), ('txfee', satoshi_to_unit),

Loading…
Cancel
Save