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