From 911d2fda38460d0f18c6ca4ddfed886255d95daa Mon Sep 17 00:00:00 2001 From: zebra-lucky Date: Wed, 30 Oct 2024 18:02:26 +0200 Subject: [PATCH] test_client_protocol.py: enhance tests coverage --- electrum/plugins/joinmarket/jm_qt.py | 2 +- electrum/plugins/joinmarket/jm_util.py | 1 - .../joinmarket/jmbitcoin/secp256k1_main.py | 2 +- .../joinmarket/jmclient/client_protocol.py | 2 +- .../tests/jmclient/test_client_protocol.py | 246 ++++++++++-------- 5 files changed, 143 insertions(+), 110 deletions(-) diff --git a/electrum/plugins/joinmarket/jm_qt.py b/electrum/plugins/joinmarket/jm_qt.py index 95d75dd47..a989b61ae 100644 --- a/electrum/plugins/joinmarket/jm_qt.py +++ b/electrum/plugins/joinmarket/jm_qt.py @@ -6,7 +6,7 @@ import asyncio from functools import partial from PyQt6.QtCore import Qt, QTimer, pyqtSignal -from PyQt6.QtGui import (QIcon, QTextCursor, QIntValidator, QFont, +from PyQt6.QtGui import (QTextCursor, QIntValidator, QFont, QKeySequence, QColor, QBrush, QAction) from PyQt6.QtWidgets import (QWidget, QGridLayout, QHBoxLayout, QLabel, QVBoxLayout, QDialog, QPushButton, QTabWidget, diff --git a/electrum/plugins/joinmarket/jm_util.py b/electrum/plugins/joinmarket/jm_util.py index 5e3115455..b87afc778 100644 --- a/electrum/plugins/joinmarket/jm_util.py +++ b/electrum/plugins/joinmarket/jm_util.py @@ -2,7 +2,6 @@ import attr import logging -import re from enum import IntEnum import electrum_ecc as ecc diff --git a/electrum/plugins/joinmarket/jmbitcoin/secp256k1_main.py b/electrum/plugins/joinmarket/jmbitcoin/secp256k1_main.py index ea6b4af74..392135a15 100644 --- a/electrum/plugins/joinmarket/jmbitcoin/secp256k1_main.py +++ b/electrum/plugins/joinmarket/jmbitcoin/secp256k1_main.py @@ -149,7 +149,7 @@ def multiply(s: bytes, pub: bytes, return_serialized: bool = True) -> bytes: return res.get_public_key_bytes() -def add_pubkeys(pubkeys: List[bytes]) -> ecc.ECPubkey: +def add_pubkeys(pubkeys: List[bytes]) -> bytes: '''Input a list of binary compressed pubkeys and return their sum as a binary compressed pubkey.''' pubkey_list = [ecc.ECPubkey(x) for x in pubkeys] diff --git a/electrum/plugins/joinmarket/jmclient/client_protocol.py b/electrum/plugins/joinmarket/jmclient/client_protocol.py index 5eedfb825..db03b896e 100644 --- a/electrum/plugins/joinmarket/jmclient/client_protocol.py +++ b/electrum/plugins/joinmarket/jmclient/client_protocol.py @@ -232,7 +232,7 @@ class JMTakerClientProtocol(JMClientProtocol): return {'accepted': True} @commands.JMSetupDone.responder - def on_JM_SETUP_DONE(self): + async def on_JM_SETUP_DONE(self): self.logger.info("JM daemon setup complete") # The daemon is ready and has requested the orderbook # from the pit; we can request the entire orderbook diff --git a/electrum/plugins/joinmarket/tests/jmclient/test_client_protocol.py b/electrum/plugins/joinmarket/tests/jmclient/test_client_protocol.py index 189d8bae7..f93381d9d 100644 --- a/electrum/plugins/joinmarket/tests/jmclient/test_client_protocol.py +++ b/electrum/plugins/joinmarket/tests/jmclient/test_client_protocol.py @@ -11,28 +11,26 @@ from electrum.plugins.joinmarket.jmbase import commands from electrum.plugins.joinmarket.jmbase import bintohex from electrum.plugins.joinmarket import jmbitcoin as bitcoin from electrum.plugins.joinmarket.jmclient import ( - Taker, JMClientProtocolFactory, JMTakerClientProtocol) + Taker, JMClientProtocolFactory, JMTakerClientProtocol, NO_ROUNDING, + get_max_cj_fee_values, fidelity_bond_weighted_order_choose) from electrum.plugins.joinmarket.tests import JMTestCase from .taker_test_data import t_raw_signed_tx -from .commontest import default_max_cj_fee -test_completed = False -clientfactory = None - LOGGING_SHORTCUT = 'J' jlog = get_logger(__name__) jlog.addFilter(ShortcutInjectingFilter(shortcut=LOGGING_SHORTCUT)) -def dummy_taker_finished(res, fromtx, waittime=0.0): - pass - - class DummyTaker(Taker): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.failutxos = 0 + self.failinit = 0 + def set_fail_init(self, val): self.failinit = val @@ -42,7 +40,7 @@ class DummyTaker(Taker): def default_taker_info_callback(self, infotype, msg): jlog.debug(infotype + ":" + msg) - def initialize(self, orderbook, fidelity_bonds_info): + async def initialize(self, orderbook, fidelity_bonds_info): """Once the daemon is active and has returned the current orderbook, select offers, re-initialize variables and prepare a commitment, then send it to the protocol to fill offers. @@ -56,7 +54,7 @@ class DummyTaker(Taker): return (True, 1000000, "aa"*32, {'dummy': 'revelation'}, orderbook[:2]) - def receive_utxos(self, ioauth_data): + async def receive_utxos(self, ioauth_data): """Triggered when the daemon returns utxo data from makers who responded; this is the completion of phase 1 of the protocol @@ -67,20 +65,14 @@ class DummyTaker(Taker): return (True, [x*64 + ":01" for x in ["a", "b", "c"]], base64.b16decode(t_raw_signed_tx, casefold=True)) - def on_sig(self, nick, sigb64): + async def on_sig(self, nick, sigb64): """For test, we exit 'early' on first message, since this marks the end of client-server communication with the daemon. """ jlog.debug("We got a sig: " + sigb64) - end_test() return None -class DummyWallet(object): - def get_wallet_id(self): - return 'aaaa' - - class JMBaseProtocol(commands.CallRemoteMock): def checkClientResponse(self, response): @@ -103,154 +95,196 @@ def show_receipt(name, *args): ",".join([str(x) for x in args])) -def end_client(client): - pass - - -def end_test(): - global test_completed - test_completed = True - client = clientfactory.getClient() - commands.callLater(1, end_client, client) - - class JMTestServerProtocol(JMBaseProtocol): @commands.JMInit.responder - def on_JM_INIT(self, bcsource, network, chan_configs, minmakers, - maker_timeout_sec, dust_threshold, blacklist_location): + async def on_JM_INIT(self, bcsource, network, chan_configs, + minmakers, maker_timeout_sec, + dust_threshold, blacklist_location): show_receipt("JMINIT", bcsource, network, chan_configs, minmakers, maker_timeout_sec, dust_threshold, blacklist_location) - d = self.callRemote(commands.JMInitProto, - nick_hash_length=1, - nick_max_encoded=2, - joinmarket_nick_header="J", - joinmarket_version=5) + d = await self.callRemote( + commands.JMInitProto, + self.factory.proto_client, + nick_hash_length=1, + nick_max_encoded=2, + joinmarket_nick_header="J", + joinmarket_version=5 + ) self.defaultCallbacks(d) return {'accepted': True} @commands.JMStartMC.responder - def on_JM_START_MC(self, nick): + async def on_JM_START_MC(self, nick): show_receipt("STARTMC", nick) d = self.callRemote(commands.JMUp) self.defaultCallbacks(d) return {'accepted': True} @commands.JMSetup.responder - def on_JM_SETUP(self, role, initdata, use_fidelity_bond): + async def on_JM_SETUP(self, role, initdata, use_fidelity_bond): show_receipt("JMSETUP", role, initdata, use_fidelity_bond) - d = self.callRemote(commands.JMSetupDone) + d = await self.callRemote(commands.JMSetupDone, + self.factory.proto_client) self.defaultCallbacks(d) return {'accepted': True} @commands.JMRequestOffers.responder - def on_JM_REQUEST_OFFERS(self): + async def on_JM_REQUEST_OFFERS(self): show_receipt("JMREQUESTOFFERS") # build a huge orderbook to test BigString Argument orderbook = ["aaaa" for _ in range(15)] fidelitybonds = ["bbbb" for _ in range(15)] - d = self.callRemote(commands.JMOffers, - orderbook=json.dumps(orderbook), - fidelitybonds=json.dumps(fidelitybonds)) + d = await self.callRemote(commands.JMOffers, + self.factory.proto_client, + orderbook=json.dumps(orderbook), + fidelitybonds=json.dumps(fidelitybonds)) self.defaultCallbacks(d) return {'accepted': True} @commands.JMFill.responder - def on_JM_FILL(self, amount, commitment, revelation, filled_offers): + async def on_JM_FILL(self, amount, commitment, revelation, filled_offers): success = False if amount == -1 else True show_receipt("JMFILL", amount, commitment, revelation, filled_offers) - d = self.callRemote(commands.JMFillResponse, - success=success, - ioauth_data=['dummy', 'list']) + d = await self.callRemote(commands.JMFillResponse, + self.factory.proto_client, success=success, + ioauth_data=['dummy', 'list']) self.defaultCallbacks(d) return {'accepted': True} @commands.JMMakeTx.responder - def on_JM_MAKE_TX(self, nick_list, tx): + async def on_JM_MAKE_TX(self, nick_list, tx): show_receipt("JMMAKETX", nick_list, tx) - d = self.callRemote(commands.JMSigReceived, - nick="dummynick", - sig="xxxsig") + d = await self.callRemote(commands.JMSigReceived, + self.factory.proto_client, + nick="dummynick", sig="xxxsig") self.defaultCallbacks(d) # add dummy calls to check message sign and message verify - d2 = self.callRemote(commands.JMRequestMsgSig, - nick="dummynickforsign", - cmd="command1", - msg="msgforsign", - msg_to_be_signed="fullmsgforsign", - hostid="hostid1") + d2 = await self.callRemote(commands.JMRequestMsgSig, + self.factory.proto_client, + nick="dummynickforsign", + cmd="command1", + msg="msgforsign", + msg_to_be_signed="fullmsgforsign", + hostid="hostid1") self.defaultCallbacks(d2) # To test, this must include a valid ecdsa sig fullmsg = "fullmsgforverify" priv = b"\xaa"*32 + b"\x01" - pub = bintohex(bitcoin.privkey_to_pubkey(priv)) + pub = bintohex(bitcoin.privkey_to_pubkey(priv).get_public_key_bytes()) sig = bitcoin.ecdsa_sign(fullmsg, priv) - d3 = self.callRemote(commands.JMRequestMsgSigVerify, - msg="msgforverify", - fullmsg=fullmsg, - sig=sig, - pubkey=pub, - nick="dummynickforverify", - hashlen=4, - max_encoded=5, - hostid="hostid2") + d3 = await self.callRemote(commands.JMRequestMsgSigVerify, + self.factory.proto_client, + msg="msgforverify", + fullmsg=fullmsg, + sig=sig, + pubkey=pub, + nick="dummynickforverify", + hashlen=4, + max_encoded=5, + hostid="hostid2") self.defaultCallbacks(d3) - d4 = self.callRemote(commands.JMSigReceived, - nick="dummynick", - sig="dummysig") + d4 = await self.callRemote(commands.JMSigReceived, + self.factory.proto_client, + nick="dummynick", sig="dummysig") self.defaultCallbacks(d4) return {'accepted': True} + @commands.JMPushTx.responder + async def on_JM_PushTx(self, nick, tx): + show_receipt("JMPUSHTX", nick, tx) + return {'accepted': True} + @commands.JMMsgSignature.responder - def on_JM_MSGSIGNATURE(self, nick, cmd, msg_to_return, hostid): + async def on_JM_MSGSIGNATURE(self, nick, cmd, msg_to_return, hostid): show_receipt("JMMSGSIGNATURE", nick, cmd, msg_to_return, hostid) return {'accepted': True} @commands.JMMsgSignatureVerify.responder - def on_JM_MSGSIGNATURE_VERIFY(self, verif_result, nick, fullmsg, hostid): + async def on_JM_MSGSIGNATURE_VERIFY(self, verif_result, nick, fullmsg, + hostid): show_receipt("JMMSGSIGVERIFY", verif_result, nick, fullmsg, hostid) return {'accepted': True} -class JMTestServerProtocolFactory: +class BaseClientProtocolTestCase(JMTestCase): - protocol = JMTestServerProtocol + def check_offers_callback(self, *args): + print('check_offers_callback', args) + def taker_info_callback(self, *args): + print('taker_info_callback', args) -class DummyClientProtocolFactory(JMClientProtocolFactory): + def taker_finished_callback(self, *args): + print('taker_finished_callback', args) - def buildProtocol(self): - return JMTakerClientProtocol(self, self.client, - nick_priv=b"\xaa"*32 + b"\x01") + async def asyncSetUp(self): + await super().asyncSetUp() + jmman = self.jmman + jmconf = self.jmconf + jmconf.maker_timeout_sec = 1 + jmconf.max_cj_fee_confirmed = True + self.schedule = [[0, 0, 2, 'INTERNAL', 0, NO_ROUNDING, 0]] + self.maxcjfee = get_max_cj_fee_values(jmman, None) + self.destaddrs = [] + self.taker = DummyTaker( + jmman, + self.schedule, + self.maxcjfee, + order_chooser=fidelity_bond_weighted_order_choose, + callbacks=[self.check_offers_callback, + self.taker_info_callback, + self.taker_finished_callback], + tdestaddrs=self.destaddrs, + custom_change_address=None, + ignored_makers=jmman.jmw.ignored_makers + ) + self.clientfactory = JMClientProtocolFactory(self.taker) + jmman.set_client_factory(self.clientfactory) + self.client_proto = self.clientfactory.getClient() + + +class ClientProtocolTestCase(BaseClientProtocolTestCase): + + async def test_on_JM_REQUEST_MSGSIG(self): + await self.client_proto.on_JM_REQUEST_MSGSIG( + nick="dummynickforsign", cmd="command1", msg="msgforsign", + msg_to_be_signed="fullmsgforsign", hostid="hostid1") + assert 0 + + +class TakerClientProtocolTestCase(BaseClientProtocolTestCase): + + async def test_clientStart(self): + await self.client_proto.clientStart() + + async def test_stallMonitor(self): + self.client_proto.stallMonitor(0) + + async def test_on_JM_UP(self): + await self.client_proto.on_JM_UP() + + async def test_on_JM_SETUP_DONE(self): + await self.client_proto.on_JM_SETUP_DONE() + + async def test_on_JM_FILL_RESPONSE(self): + await self.client_proto.on_JM_FILL_RESPONSE( + success=True, ioauth_data={'dummy': 'ioauth'}) + + async def test_on_JM_OFFERS(self): + orderbook = ["aaaa" for _ in range(15)] + fidelitybonds = ["bbbb" for _ in range(15)] + await self.client_proto.on_JM_OFFERS( + orderbook=json.dumps(orderbook), + fidelitybonds=json.dumps(fidelitybonds)) -class TrialTestJMClientProto(JMTestCase): + async def test_on_JM_SIG_RECEIVED(self): + await self.client_proto.on_JM_SIG_RECEIVED( + nick="dummynick", sig="xxxsig") - async def asyncSetUp(self): - await super().asyncSetUp() + async def test_get_offers(self): + await self.client_proto.get_offers() - global clientfactory - params = [[False, False], [True, False], [False, True], [-1, False]] - self.jmman.maker_timeout_sec = 1 - clientfactories = [] - takers = [ - DummyTaker( - self.jmman, - ["a", "b"], default_max_cj_fee, - callbacks=(None, None, dummy_taker_finished)) - for _ in range(len(params))] - for i, p in enumerate(params): - takers[i].set_fail_init(p[0]) - takers[i].set_fail_utxos(p[1]) - takers[i].testflag = True - if i != 0: - clientfactories.append(JMClientProtocolFactory(takers[i])) - else: - clientfactories.append(DummyClientProtocolFactory(takers[i])) - clientfactory = clientfactories[0] - - async def test_waiter(self): - return commands.deferLater(3, self._called_by_deffered) - - async def _called_by_deffered(self): - pass + async def test_push_tx(self): + await self.client_proto.push_tx("dummynick", b"deadbeef")