Browse Source
masterc70b12fFor timelock addrs use new pubkey foreach locktime (chris-belcher)3dc8d86Fix importprivkey on fidelity bond wallets (chris-belcher)bbd3d1bPrint privacy warning when showing timelocked addr (chris-belcher)1ea62a7Fix bug with timelocked addrs in receive payjoin (chris-belcher)4868343Fix showutxos wallettool method for fidelity bonds (chris-belcher)81bade7Update ygrunner to use fidelity bonds (Adam Gibson)a3b775bIncrease default fee by 4x (chris-belcher)e970a01Create release notes section for fidelity bonds (chris-belcher)6cf4162Add fidelity bond protocol tests (chris-belcher)199b571Consider fidelity bonds when choosing makers (chris-belcher)97b8b3bShow fidelity bonds on orderbook watch html page (chris-belcher)7a50c76Announce yieldgenerator's fidelity bond (chris-belcher)6b6fc4aHandle fidelity bonds in client-server protocol (chris-belcher)662f097Write and update fidelity bond docs (chris-belcher)eb0a738Add interest rate option to config file (chris-belcher)d4b3f70Enable creation of fidelity bond wallets on cli (chris-belcher)b9eab6eIncrease max locktime of fidelity bond wallets (chris-belcher)a3b3cd4Make fidelity bond wallets be native segwit (chris-belcher)9a372fdAdd getblockhash RPC method (chris-belcher)e6c0847Add calculate fidelity bond value function + tests (chris-belcher) Tree-SHA512: d2ffde2b752f66c5fd0d3100670e69d0357fc27bd3c50f9438cef9b261a17b0554c14ee56567ca1980937f889ac8be75ecc96517b1a77c7370ed33f8b5650cc7
37 changed files with 1601 additions and 310 deletions
@ -0,0 +1,19 @@
|
||||
|
||||
Notable changes |
||||
=============== |
||||
|
||||
### Fidelity bond for improving sybil attack resistance |
||||
|
||||
From the very beginning of JoinMarket it was possible to attack the system by creating many many maker bots all controlled by the same person. If an unlucky taker came along and created a coinjoin only with those fake maker bots then their coinjoins could be easily unmixed. This is called a sybil attack and until now it was relatively cheap to do against JoinMarket. Some yield generators were already doing this by running multiple bots, because they could earn higher coinjoin fees from their multiple makers. |
||||
|
||||
Fidelity bonds are a new feature intended to make this sybil attack a lot more expensive. It works by allowing JoinMarket makers to lock up bitcoins into time locked addresses. Takers will still choose makers to coinjoin with randomly but they have a greater chance of choosing makers who have advertised more valuable fidelity bonds. Any sybil attacker then has to lock up many many bitcoins into time locked addresses. |
||||
|
||||
For full details of the scheme see: [Design for improving JoinMarket's resistance to sybil attacks using fidelity bonds](https://gist.github.com/chris-belcher/18ea0e6acdb885a2bfbdee43dcd6b5af/) |
||||
|
||||
This release implements all the features needed to add fidelity bonds to JoinMarket. Takers (via scripts such as `sendpayment.py` or `tumbler.py` or the Joinmarket-Qt app) will automatically give preference to makers who advertise fidelity bonds. Makers can optionally update their wallets to fidelity bond wallets. When a fidelity bond wallet is used with a yield generator script, it will automatically announce its fidelity bond publicly. Makers who don't create fidelity bonds by locking up bitcoins will still be chosen for coinjoins occasionally, but probably much less often than before. |
||||
|
||||
For full user documentation see the file `/docs/`fidelity-bonds.md` in the repository. |
||||
|
||||
With realistic assumptions we have calculated that an adversary would need to lock up around 50000 bitcoins for 6 months in order to sybil attack the JoinMarket system with 95% success rate. Now that fidelity bonds are being added to JoinMarket for real we can see how the system behaves in practice. |
||||
|
||||
Fidelity bond coins cannot be yet be held in cold storage, but this is easy to add later because the JoinMarket protocol is set up in a way that the change would be backward-compatible. |
||||
@ -0,0 +1,136 @@
|
||||
import struct |
||||
import base64 |
||||
import json |
||||
from jmbitcoin import ecdsa_sign, ecdsa_verify |
||||
from jmdaemon 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 |
||||
@ -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 |
||||
|
||||
@ -0,0 +1,81 @@
|
||||
|
||||
##this file calculates the success probability of a sybil attack on the |
||||
# orderbook with fidelity bonds used in joinmarket |
||||
# see https://gist.github.com/chris-belcher/87ebbcbb639686057a389acb9ab3e25b |
||||
|
||||
|
||||
#precomputed |
||||
#what sybil weight is required per-maker to sybil attack joinmarket with 95% success rate |
||||
#this is for when the honest weight (i.e. value of all fidelity bonds added up) equals 1 |
||||
#however it is linear, so to calculate for another honest_weight just multiply |
||||
#see |
||||
#https://gist.github.com/chris-belcher/87ebbcbb639686057a389acb9ab3e25b#appendix-1---fit-to-unit-honest-weight-sybil-attack |
||||
successful_attack_95pc_sybil_weight = { |
||||
1: 19.2125, |
||||
2: 28.829523311823312, |
||||
3: 35.37299702466422, |
||||
4: 40.27618399827166, |
||||
5: 44.19631358837695, |
||||
6: 47.46160578701477, |
||||
7: 50.25944623742167, |
||||
8: 52.706868994753286, |
||||
9: 54.881852860047836, |
||||
10: 56.8389576639515, |
||||
11: 58.61784778500215, |
||||
12: 60.248261563672784, |
||||
13: 61.75306801, |
||||
14: 62.97189476, |
||||
15: 64.28155594, |
||||
16: 65.21832112385313, |
||||
17: 66.29765063354174, |
||||
18: 67.315269563541, |
||||
19: 68.27785449480159, |
||||
20: 69.19105386203657, |
||||
21: 70.05968878944397, |
||||
22: 70.88790716279642, |
||||
23: 71.67930342495613, |
||||
24: 72.43701285697972, |
||||
25: 73.16378660022 |
||||
} |
||||
|
||||
def descend_probability_tree(weights, remaining_descents, branch_probability): |
||||
if remaining_descents == 0: |
||||
return branch_probability |
||||
else: |
||||
total_weight = sum(weights) |
||||
result = 0 |
||||
for i, w in enumerate(weights): |
||||
#honest makers are at index 0 |
||||
if i == 0: |
||||
#an honest maker being chosen means the sybil attack failed |
||||
#so this branch contributes zero to the attack success prob |
||||
continue |
||||
if w == 0: |
||||
continue |
||||
weight_cache = weights[i] |
||||
weights[i] = 0 |
||||
result += descend_probability_tree(weights, |
||||
remaining_descents-1, branch_probability*w/total_weight) |
||||
weights[i] = weight_cache |
||||
return result |
||||
|
||||
def calculate_top_makers_sybil_attack_success_probability(weights, taker_peer_count): |
||||
honest_weight = sum(weights[taker_peer_count:]) |
||||
weights = [honest_weight] + weights[:taker_peer_count] |
||||
return descend_probability_tree(weights, taker_peer_count, 1.0) |
||||
|
||||
|
||||
def weight_to_burned_coins(w): |
||||
#calculates how many coins need to be burned to produce a certain bond |
||||
return w**0.5 |
||||
|
||||
def weight_to_locked_coins(w, r, locktime_months): |
||||
#calculates how many coins need to be locked to produce a certain bond |
||||
return w**0.5 / r / locktime_months * 12 |
||||
|
||||
def coins_locked_to_weight(c, r, locktime_months): |
||||
return (c*r*locktime_months/12.0)**2 |
||||
|
||||
def coins_burned_to_weight(c): |
||||
return c*c |
||||
|
||||
Loading…
Reference in new issue