Browse Source

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`.
master
chris-belcher 6 years ago
parent
commit
7a50c768da
No known key found for this signature in database
GPG Key ID: EF734EA677F31129
  1. 14
      jmbase/jmbase/commands.py
  2. 17
      jmclient/jmclient/client_protocol.py
  3. 2
      jmclient/jmclient/fidelity_bond.py
  4. 10
      jmclient/jmclient/maker.py
  5. 51
      jmclient/jmclient/yieldgenerator.py
  6. 29
      jmdaemon/jmdaemon/daemon_protocol.py
  7. 1
      jmdaemon/jmdaemon/message_channel.py

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

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

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

10
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

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

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

1
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 = []

Loading…
Cancel
Save