You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

246 lines
9.9 KiB

#! /usr/bin/env python
'''Creates wallets and yield generators in regtest.
Provides seed for joinmarket-qt test.
This should be run via pytest, even though
it's NOT part of the test-suite, because that
makes it much easier to handle start up and
shut down of the environment.
Run it like:
PYTHONPATH=.:$PYTHONPATH pytest \
--btcroot=/path/to/bitcoin/bin/ \
--btcpwd=123456abcdef --btcconf=/blah/bitcoin.conf \
--nirc=2 -s test/ygrunner.py
'''
from twisted.internet import task
from common import make_wallets
import pytest
import random
from datetime import datetime
from jmbase import jmprint
from jmclient import YieldGeneratorBasic, load_test_config, jm_single,\
JMClientProtocolFactory, start_reactor, SegwitWallet,\
SegwitLegacyWallet, cryptoengine, SNICKERClientProtocolFactory, SNICKERReceiver
from jmclient.wallet_utils import wallet_gettimelockaddress
# For quicker testing, restrict the range of timelock
# addresses to avoid slow load of multiple bots.
# Note: no need to revert this change as ygrunner runs
# in isolation.
from jmclient import FidelityBondMixin
FidelityBondMixin.TIMELOCK_ERA_YEARS = 2
FidelityBondMixin.TIMELOCK_EPOCH_YEAR = datetime.now().year
FidelityBondMixin.TIMENUMBERS_PER_PUBKEY = 12
class MaliciousYieldGenerator(YieldGeneratorBasic):
"""Overrides, randomly, some maker functions
to prevent taker continuing successfully (unless
they can complete-with-subset).
"""
def set_maliciousness(self, frac, mtype=None):
self.authmal = False
self.txmal = False
if mtype == "tx":
self.txmal = True
elif mtype == "auth":
self.authmal = True
else:
self.txmal = True
self.authmal = True
self.mfrac = frac
def on_auth_received(self, nick, offer, commitment, cr, amount, kphex):
if self.authmal:
if random.randint(1, 100) < self.mfrac:
jmprint("Counterparty commitment rejected maliciously", "debug")
return (False,)
return super().on_auth_received(nick, offer, commitment, cr, amount, kphex)
def on_tx_received(self, nick, tx, offerinfo):
if self.txmal:
if random.randint(1, 100) < self.mfrac:
jmprint("Counterparty tx rejected maliciously", "debug")
return (False, "malicious tx rejection")
return super().on_tx_received(nick, tx, offerinfo)
class DeterministicMaliciousYieldGenerator(YieldGeneratorBasic):
"""Overrides, randomly chosen persistently, some maker functions
to prevent taker continuing successfully (unless
they can complete-with-subset).
"""
def set_maliciousness(self, frac, mtype=None):
self.authmal = False
self.txmal = False
if mtype == "tx":
if random.randint(1, 100) < frac:
self.txmal = True
elif mtype == "auth":
if random.randint(1, 100) < frac:
self.authmal = True
else:
if random.randint(1, 100) < frac:
self.txmal = True
self.authmal = True
def on_auth_received(self, nick, offer, commitment, cr, amount, kphex):
if self.authmal:
jmprint("Counterparty commitment rejected maliciously", "debug")
return (False,)
return super().on_auth_received(nick, offer, commitment, cr, amount, kphex)
def on_tx_received(self, nick, tx, offerinfo):
if self.txmal:
jmprint("Counterparty tx rejected maliciously", "debug")
return (False, "malicious tx rejection")
return super().on_tx_received(nick, tx, offerinfo)
@pytest.mark.parametrize(
"num_ygs, wallet_structures, fb_indices, mean_amt, malicious, deterministic",
[
# 1sp 3yg, honest makers, one maker has FB:
(3, [[1, 3, 0, 0, 0]] * 4, [], 2, 0, False),
# 1sp 3yg, malicious makers reject on auth and on tx 30% of time
#(3, [[1, 3, 0, 0, 0]] * 4, 2, 30, False),
# 1 sp 9 ygs, deterministically malicious 50% of time
#(9, [[1, 3, 0, 0, 0]] * 10, 2, 50, True),
])
def test_start_ygs(setup_ygrunner, num_ygs, wallet_structures, fb_indices,
mean_amt, malicious, deterministic):
"""Set up some wallets, for the ygs and 1 sp.
Then start the ygs in background and publish
the seed of the sp wallet for easy import into -qt
"""
if jm_single().config.get("POLICY", "native") == "true":
walletclass = SegwitWallet
else:
# TODO add Legacy
walletclass = SegwitLegacyWallet
wallet_services = make_wallets(num_ygs + 1,
wallet_structures=wallet_structures,
mean_amt=mean_amt,
walletclass=walletclass,
fb_indices=fb_indices)
#the sendpayment bot uses the last wallet in the list
wallet_service = wallet_services[num_ygs]['wallet']
jmprint("\n\nTaker wallet seed : " + wallet_services[num_ygs]['seed'])
# for manual audit if necessary, show the maker's wallet seeds
# also (note this audit should be automated in future)
jmprint("\n\nMaker wallet seeds: ")
for i in range(num_ygs):
jmprint("Maker seed: " + wallet_services[i]['seed'])
jmprint("\n")
wallet_service.sync_wallet(fast=True)
ygclass = YieldGeneratorBasic
# As per previous note, override non-default command line settings:
options = {}
for x in ["ordertype", "txfee_contribution", "txfee_contribution_factor",
"cjfee_a", "cjfee_r", "cjfee_factor", "minsize", "size_factor"]:
options[x] = jm_single().config.get("YIELDGENERATOR", x)
ordertype = options["ordertype"]
txfee_contribution = int(options["txfee_contribution"])
txfee_contribution_factor = float(options["txfee_contribution_factor"])
cjfee_factor = float(options["cjfee_factor"])
size_factor = float(options["size_factor"])
if ordertype == 'reloffer':
cjfee_r = options["cjfee_r"]
# minimum size is such that you always net profit at least 20%
#of the miner fee
minsize = max(int(1.2 * txfee_contribution / float(cjfee_r)),
int(options["minsize"]))
cjfee_a = None
elif ordertype == 'absoffer':
cjfee_a = int(options["cjfee_a"])
minsize = int(options["minsize"])
cjfee_r = None
else:
assert False, "incorrect offertype config for yieldgenerator."
txtype = wallet_service.get_txtype()
if txtype == "p2wpkh":
prefix = "sw0"
elif txtype == "p2sh-p2wpkh":
prefix = "sw"
elif txtype == "p2pkh":
prefix = ""
else:
assert False, "Unsupported wallet type for yieldgenerator: " + txtype
ordertype = prefix + ordertype
if malicious:
if deterministic:
ygclass = DeterministicMaliciousYieldGenerator
else:
ygclass = MaliciousYieldGenerator
for i in range(num_ygs):
cfg = [txfee_contribution, cjfee_a, cjfee_r, ordertype, minsize,
txfee_contribution_factor, cjfee_factor, size_factor]
wallet_service_yg = wallet_services[i]["wallet"]
wallet_service_yg.startService()
yg = ygclass(wallet_service_yg, cfg)
if i in fb_indices:
# create a timelocked address and fund it;
# must be done after sync, so deferred:
wallet_service_yg.timelock_funded = False
sync_wait_loop = task.LoopingCall(get_addr_and_fund, yg)
sync_wait_loop.start(1.0, now=False)
if malicious:
yg.set_maliciousness(malicious, mtype="tx")
clientfactory = JMClientProtocolFactory(yg, proto_type="MAKER")
if jm_single().config.get("SNICKER", "enabled") == "true":
snicker_r = SNICKERReceiver(wallet_service_yg)
servers = jm_single().config.get("SNICKER", "servers").split(",")
snicker_factory = SNICKERClientProtocolFactory(snicker_r, servers)
else:
snicker_factory = None
nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
daemon = True if nodaemon == 1 else False
rs = True if i == num_ygs - 1 else False
start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
jm_single().config.getint("DAEMON", "daemon_port"),
clientfactory, snickerfactory=snicker_factory,
daemon=daemon, rs=rs)
def get_addr_and_fund(yg):
""" This function allows us to create
and publish a fidelity bond for a particular
yield generator object after the wallet has reached
a synced state and is therefore ready to serve up
timelock addresses. We create the TL address, fund it,
refresh the wallet and then republish our offers, which
will also publish the new FB.
"""
if not yg.wallet_service.synced:
return
if yg.wallet_service.timelock_funded:
return
addr = wallet_gettimelockaddress(yg.wallet_service.wallet, "2023-11")
print("Got timelockaddress: {}".format(addr))
# pay into it; amount is randomized for now.
# Note that grab_coins already mines 1 block.
fb_amt = random.randint(1, 5)
jm_single().bc_interface.grab_coins(addr, fb_amt)
# we no longer have to run this loop (TODO kill with nonlocal)
yg.wallet_service.timelock_funded = True
# force wallet to check for the new coins so the new
# yg offers will include them:
yg.wallet_service.transaction_monitor()
# publish a new offer:
yg.offerlist = yg.create_my_orders()
yg.fidelity_bond = yg.get_fidelity_bond_template()
jmprint('updated offerlist={}'.format(yg.offerlist))
@pytest.fixture(scope="module")
def setup_ygrunner(setup_regtest_bitcoind, setup_miniircd):
load_test_config()
jm_single().bc_interface.tick_forward_chain_interval = 10
jm_single().bc_interface.simulate_blocks()
# handles the custom regtest hrp for bech32
cryptoengine.BTC_P2WPKH.VBYTE = 100