diff --git a/jmbase/jmbase/commands.py b/jmbase/jmbase/commands.py index 5f1f2bb..21e1d6f 100644 --- a/jmbase/jmbase/commands.py +++ b/jmbase/jmbase/commands.py @@ -3,7 +3,7 @@ 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, String +from twisted.protocols.amp import Boolean, Command, Integer, Unicode from .bigstring import BigUnicode @@ -47,7 +47,7 @@ class JMSetup(JMCommand): """ arguments = [(b'role', Unicode()), (b'offers', Unicode()), - (b'fidelity_bond', String())] + (b'use_fidelity_bond', Boolean())] class JMMsgSignature(JMCommand): """A response to a request for a bitcoin signature @@ -109,6 +109,11 @@ class JMAnnounceOffers(JMCommand): (b'to_cancel', Unicode()), (b'offerlist', Unicode())] +class JMFidelityBondProof(JMCommand): + """Send requested fidelity bond proof message""" + arguments = [(b'nick', Unicode()), + (b'proof', Unicode())] + class JMIOAuth(JMCommand): """Send contents of !ioauth message after verifying Taker's auth message @@ -202,6 +207,11 @@ class JMSigReceived(JMCommand): """MAKER-specific commands """ +class JMFidelityBondProofRequest(JMCommand): + """MAKER wants to announce a fidelity bond proof message""" + arguments = [(b'takernick', Unicode()), + (b'makernick', Unicode())] + class JMAuthReceived(JMCommand): """Return the commitment and revelation provided in !fill, !auth by the TAKER, diff --git a/jmclient/jmclient/client_protocol.py b/jmclient/jmclient/client_protocol.py index 07bac06..97acb50 100644 --- a/jmclient/jmclient/client_protocol.py +++ b/jmclient/jmclient/client_protocol.py @@ -392,14 +392,10 @@ 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", offers=json.dumps(self.client.offerlist), - fidelity_bond=fidelity_bond_data) + use_fidelity_bond=(self.client.fidelity_bond != None)) self.defaultCallbacks(d) @commands.JMSetupDone.responder @@ -432,6 +428,17 @@ class JMMakerClientProtocol(JMClientProtocol): maker_timeout_sec=maker_timeout_sec) self.defaultCallbacks(d) + @commands.JMFidelityBondProofRequest.responder + def on_JM_FIDELITY_BOND_PROOF_REQUEST(self, takernick, makernick): + proof_msg = (self.client.fidelity_bond + .create_proof(makernick, takernick) + .create_proof_msg(self.client.fidelity_bond.cert_privkey)) + d = self.callRemote(commands.JMFidelityBondProof, + nick=takernick, + proof=proof_msg) + self.defaultCallbacks(d) + return {"accepted": True} + @commands.JMAuthReceived.responder def on_JM_AUTH_RECEIVED(self, nick, offer, commitment, revelation, amount, kphex): diff --git a/jmclient/jmclient/fidelity_bond.py b/jmclient/jmclient/fidelity_bond.py index 2340f55..e59a6bb 100644 --- a/jmclient/jmclient/fidelity_bond.py +++ b/jmclient/jmclient/fidelity_bond.py @@ -2,7 +2,7 @@ import struct import base64 import json from jmbitcoin import ecdsa_sign, ecdsa_verify -from jmdaemon import import fidelity_bond_sanity_check +from jmdaemon import fidelity_bond_sanity_check def assert_is_utxo(utxo): diff --git a/jmclient/jmclient/maker.py b/jmclient/jmclient/maker.py index 93d5c78..b535a86 100644 --- a/jmclient/jmclient/maker.py +++ b/jmclient/jmclient/maker.py @@ -21,6 +21,7 @@ class Maker(object): self.wallet_service = wallet_service self.nextoid = -1 self.offerlist = None + self.fidelity_bond = None self.sync_wait_loop = task.LoopingCall(self.try_to_create_my_orders) # don't fire on the first tick since reactor is still starting up # and may not shutdown appropriately if we immediately recognize @@ -38,6 +39,7 @@ class Maker(object): if not self.wallet_service.synced: return self.offerlist = self.create_my_orders() + self.fidelity_bond = self.get_fidelity_bond_template() self.sync_wait_loop.stop() if not self.offerlist: jlog.info("Failed to create offers, giving up.") @@ -277,3 +279,11 @@ class Maker(object): """Performs actions on receipt of 1st confirmation of a transaction into a block (e.g. announce orders) """ + + def get_fidelity_bond_template(self): + """ + Generates information about a fidelity bond which will be announced + By default returns no fidelity bond + Does not contain nick signature which has to be calculated individually + """ + return None diff --git a/jmclient/jmclient/yieldgenerator.py b/jmclient/jmclient/yieldgenerator.py index ba789fb..9f8c4bb 100644 --- a/jmclient/jmclient/yieldgenerator.py +++ b/jmclient/jmclient/yieldgenerator.py @@ -4,15 +4,19 @@ import datetime import os import time import abc +import base64 from twisted.python.log import startLogging from optparse import OptionParser from jmbase import get_log from jmclient import (Maker, jm_single, load_program_config, JMClientProtocolFactory, start_reactor, calc_cj_fee, WalletService, add_base_options, SNICKERReceiver, - SNICKERClientProtocolFactory) + SNICKERClientProtocolFactory, FidelityBondMixin, + get_interest_rate, fmt_utxo) from .wallet_utils import open_test_wallet_maybe, get_wallet_path from jmbase.support import EXIT_ARGERROR, EXIT_FAILURE, get_jm_version_str +import jmbitcoin as btc +from jmclient.fidelity_bond import FidelityBond jlog = get_log() @@ -66,7 +70,6 @@ class YieldGenerator(Maker): return to_cancel, to_announce - class YieldGeneratorBasic(YieldGenerator): """A simplest possible instantiation of a yieldgenerator. It will often (but not always) reannounce orders after transactions, @@ -112,6 +115,50 @@ class YieldGeneratorBasic(YieldGenerator): return [order] + def get_fidelity_bond_template(self): + if not isinstance(self.wallet_service.wallet, FidelityBondMixin): + jlog.info("Not a fidelity bond wallet, not announcing fidelity bond") + return None + blocks = jm_single().bc_interface.get_current_block_height() + mediantime = jm_single().bc_interface.get_best_block_median_time() + + BLOCK_COUNT_SAFETY = 2 #use this safety number to reduce chances of the proof expiring + #before the taker gets a chance to verify it + RETARGET_INTERVAL = 2016 + CERT_MAX_VALIDITY_TIME = 1 + cert_expiry = ((blocks + BLOCK_COUNT_SAFETY) // RETARGET_INTERVAL) + CERT_MAX_VALIDITY_TIME + + utxos = self.wallet_service.wallet.get_utxos_by_mixdepth(include_disabled=True, + includeheight=True)[FidelityBondMixin.FIDELITY_BOND_MIXDEPTH] + timelocked_utxos = [(outpoint, info) for outpoint, info in utxos.items() + if FidelityBondMixin.is_timelocked_path(info["path"])] + if len(timelocked_utxos) == 0: + jlog.info("No timelocked coins in wallet, not announcing fidelity bond") + return + timelocked_utxos_with_confirmation_time = [(outpoint, info, + jm_single().bc_interface.get_block_time( + jm_single().bc_interface.get_block_hash(info["height"]) + )) + for (outpoint, info) in timelocked_utxos] + + interest_rate = get_interest_rate() + max_valued_bond = max(timelocked_utxos_with_confirmation_time, key=lambda x: + FidelityBondMixin.calculate_timelocked_fidelity_bond_value(x[1]["value"], x[2], + x[1]["path"][-1], mediantime, interest_rate) + ) + (utxo_priv, locktime), engine = self.wallet_service.wallet._get_key_from_path( + max_valued_bond[1]["path"]) + utxo_pub = engine.privkey_to_pubkey(utxo_priv) + cert_priv = os.urandom(32) + b"\x01" + cert_pub = btc.privkey_to_pubkey(cert_priv) + cert_msg = b"fidelity-bond-cert|" + cert_pub + b"|" + str(cert_expiry).encode("ascii") + cert_sig = base64.b64decode(btc.ecdsa_sign(cert_msg, utxo_priv)) + utxo = (max_valued_bond[0][0], max_valued_bond[0][1]) + fidelity_bond = FidelityBond(utxo, utxo_pub, locktime, cert_expiry, + cert_priv, cert_pub, cert_sig) + jlog.info("Announcing fidelity bond coin {}".format(fmt_utxo(utxo))) + return fidelity_bond + def oid_to_order(self, offer, amount): total_amount = amount + offer["txfee"] real_cjfee = calc_cj_fee(offer["ordertype"], offer["cjfee"], amount) diff --git a/jmdaemon/jmdaemon/daemon_protocol.py b/jmdaemon/jmdaemon/daemon_protocol.py index b3eadbb..7714030 100644 --- a/jmdaemon/jmdaemon/daemon_protocol.py +++ b/jmdaemon/jmdaemon/daemon_protocol.py @@ -13,7 +13,6 @@ 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 @@ -475,7 +474,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch): self.crypto_boxes = {} self.sig_lock = threading.Lock() self.active_orders = {} - self.fidelity_bond = None + self.use_fidelity_bond = False def checkClientResponse(self, response): """A generic check of client acceptance; any failure @@ -553,7 +552,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch): return {'accepted': True} @JMSetup.responder - def on_JM_SETUP(self, role, offers, fidelity_bond): + def on_JM_SETUP(self, role, offers, use_fidelity_bond): assert self.jm_state == 0 self.role = role self.crypto_boxes = {} @@ -568,8 +567,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch): self.mcc.pubmsg(COMMAND_PREFIX + "orderbook") elif self.role == "MAKER": self.offerlist = json.loads(offers) - if fidelity_bond: - self.fidelity_bond = FidelityBond.deserialize(fidelity_bond) + self.use_fidelity_bond = use_fidelity_bond self.mcc.announce_orders(self.offerlist, None, None, None) self.jm_state = 1 return {'accepted': True} @@ -659,7 +657,13 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch): if len(to_cancel) > 0: self.mcc.cancel_orders(to_cancel) if len(to_announce) > 0: - self.mcc.announce_orders(to_announce, None, None) + self.mcc.announce_orders(to_announce, None, None, None) + return {"accepted": True} + + @JMFidelityBondProof.responder + def on_JM_FIDELITY_BOND_PROOF(self, nick, proof): + """Called by maker client as a reply to a request of a fidelity bond proof""" + self.mcc.announce_orders(self.offerlist, nick, proof, new_mc=None) return {"accepted": True} @JMIOAuth.responder @@ -715,15 +719,16 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch): def on_orderbook_requested(self, nick, mc=None): """Dealt with by daemon, assuming offerlist is up to date """ - if self.fidelity_bond: + if self.use_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) + d = self.callRemote(JMFidelityBondProofRequest, + takernick=taker_nick, + makernick=maker_nick) + self.defaultCallbacks(d) else: - proof_msg = None - - self.mcc.announce_orders(self.offerlist, nick, proof_msg, mc) + self.mcc.announce_orders(self.offerlist, nick, fidelity_bond_proof_msg=None, + new_mc=mc) @maker_only def on_order_fill(self, nick, oid, amount, taker_pk, commit): diff --git a/jmdaemon/jmdaemon/message_channel.py b/jmdaemon/jmdaemon/message_channel.py index 1105e88..1abdee4 100644 --- a/jmdaemon/jmdaemon/message_channel.py +++ b/jmdaemon/jmdaemon/message_channel.py @@ -291,6 +291,7 @@ class MessageChannelCollection(object): message channels, if nick=None, or to an individual counterparty nick, as privmsg, on a specific mc. + Fidelity bonds can only be announced over privmsg, nick must be nonNone """ order_keys = ['oid', 'minsize', 'maxsize', 'txfee', 'cjfee'] orderlines = []