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. 6
      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. 233
      jmdaemon/test/test_orderbookwatch.py
  16. 2
      scripts/obwatch/ob-watcher.py

6
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

14
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",

10
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}

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))
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]

4
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}

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,
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

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,
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

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 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:])

25
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()

2
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']

5
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))

8
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)]

233
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

2
scripts/obwatch/ob-watcher.py

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

Loading…
Cancel
Save