From 7a50c768dab3b761ea09bc2093b4e1b00e440d44 Mon Sep 17 00:00:00 2001 From: chris-belcher Date: Wed, 27 May 2020 14:31:45 +0100 Subject: [PATCH] Announce yieldgenerator's fidelity bond If a yield generator is run with a fidelity bond wallet then the most-valuable bond will be found and announced. The announcement includes a proof of a UTXO and its locktime. Also a proof that the maker's IRC nickname controls the UTXO. There is also an intermediate signature called the certificate signature which can later be used when holding fidelity bond UTXOs in cold storage without the protocol needing to change. Right now this feature is unused so certificates are generated dynamically on each request. The certificates have an expiry block height, which is defined as the number of 2016-block retargeting periods since the genesis block, so to check if the expiry was passed the taker will check `current_block_height > cert_expiry*2016`. --- jmbase/jmbase/commands.py | 14 ++++++-- jmclient/jmclient/client_protocol.py | 17 +++++++--- jmclient/jmclient/fidelity_bond.py | 2 +- jmclient/jmclient/maker.py | 10 ++++++ jmclient/jmclient/yieldgenerator.py | 51 ++++++++++++++++++++++++++-- jmdaemon/jmdaemon/daemon_protocol.py | 29 +++++++++------- jmdaemon/jmdaemon/message_channel.py | 1 + 7 files changed, 102 insertions(+), 22 deletions(-) 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 = []