From e497886ff3c3b01fde4b83fa53d3e2f1866a673f Mon Sep 17 00:00:00 2001 From: zebra-lucky Date: Sat, 2 Nov 2024 22:35:23 +0200 Subject: [PATCH] test_onionmc.py: add OnionPeerTestCase --- .../plugins/joinmarket/jmdaemon/onionmc.py | 10 ++ .../joinmarket/tests/jmdaemon/test_onionmc.py | 150 +++++++++++++++++- 2 files changed, 156 insertions(+), 4 deletions(-) diff --git a/electrum/plugins/joinmarket/jmdaemon/onionmc.py b/electrum/plugins/joinmarket/jmdaemon/onionmc.py index c26aee55e..f65fc2bd9 100644 --- a/electrum/plugins/joinmarket/jmdaemon/onionmc.py +++ b/electrum/plugins/joinmarket/jmdaemon/onionmc.py @@ -269,6 +269,13 @@ class OnionPeer: # directories and onion-serving peers, sending # messages backwards on a connection created towards them). self.alternate_location = "" + if self.hostname != NOT_SERVING_ONION_HOSTNAME: + # There is no harm in always setting it by default; + # it only gets used if we don't have an outbound. + self.set_alternate_location(location_tuple_to_str( + location_tuple)) + if directory and not self.hostname: + raise OnionPeerDirectoryWithoutHostError() self.directory = directory self._status = PEER_STATUS_UNCONNECTED # A function to be called to initiate a handshake; @@ -286,6 +293,9 @@ class OnionPeer: # TODO: prefer state machine update self.connecting = False + def set_alternate_location(self, location_string: str) -> None: + self.alternate_location = location_string + def update_status(self, destn_status: int) -> None: """ Wrapping state updates to enforce: (a) that the handshake is triggered by connection diff --git a/electrum/plugins/joinmarket/tests/jmdaemon/test_onionmc.py b/electrum/plugins/joinmarket/tests/jmdaemon/test_onionmc.py index 19ebff28e..70ace4839 100644 --- a/electrum/plugins/joinmarket/tests/jmdaemon/test_onionmc.py +++ b/electrum/plugins/joinmarket/tests/jmdaemon/test_onionmc.py @@ -3,9 +3,13 @@ import asyncio import io +from electrum.plugins.joinmarket.jmbase import commands from electrum.plugins.joinmarket.jmdaemon import ( OnionMessageChannel, MessageChannelCollection) -from electrum.plugins.joinmarket.jmdaemon.onionmc import OnionPeer +from electrum.plugins.joinmarket.jmdaemon.onionmc import ( + PEER_STATUS_UNCONNECTED, PEER_STATUS_CONNECTED, PEER_STATUS_HANDSHAKED, + PEER_STATUS_DISCONNECTED, NOT_SERVING_ONION_HOSTNAME, OnionPeerError, + OnionCustomMessage, JM_MESSAGE_TYPES) from electrum.plugins.joinmarket.jmdaemon.onionmc_support import ( TorClientService) @@ -47,6 +51,78 @@ class DummyMC(OnionMessageChannel): class OnionBaseTestCase(JMTestCase): + async def on_connect(self, x): + print('simulated on-connect', x) + + async def on_welcome(self, mc): + print('simulated on-welcome', mc) + if mc.nick == "irc_publisher": + d = commands.deferLater(3.0, self.junk_pubmsgs, mc) + d.addCallback(self.junk_longmsgs) + d.addCallback(self.junk_announce) + d.addCallback(self.junk_fill) + + async def on_disconnect(self, x): + print('simulated on-disconnect', x) + + def on_order_seen(self, dummy, counterparty, oid, ordertype, minsize, + maxsize, txfee, cjfee): + self.yg_name = counterparty + + async def on_pubkey(self, pubkey): + print("received pubkey: " + pubkey) + + async def junk_pubmsgs(self, mc): + # start a raw IRCMessageChannel instance in a thread; + # then call send_* on it with various errant messages + await asyncio.sleep(1) + await mc.request_orderbook() + await asyncio.sleep(1) + # now try directly + await mc.pubmsg("!orderbook") + await asyncio.sleep(1) + # should be ignored; can we check? + await mc.pubmsg("!orderbook!orderbook") + return mc + + async def junk_longmsgs(self, mc): + # assuming MAX_PRIVMSG_LEN is not something crazy + # big like 550, this should fail + # with pytest.raises(AssertionError) as e_info: + await mc.pubmsg("junk and crap"*40) + await asyncio.sleep(1) + # assuming MAX_PRIVMSG_LEN is not something crazy + # small like 180, this should succeed + await mc.pubmsg("junk and crap"*15) + await asyncio.sleep(1) + return mc + + async def junk_announce(self, mc): + # try a long order announcement in public + # because we don't want to build a real orderbook, + # call the underlying IRC announce function. + # TODO: how to test that the sent format was correct? + print('got here') + await mc._announce_orders(["!abc def gh 0001"]*30) + await asyncio.sleep(1) + return mc + + async def junk_fill(self, mc): + cpname = "irc_receiver" + # send a fill with an invalid pubkey to the existing yg; + # this should trigger a NaclError but should NOT kill it. + await mc._privmsg(cpname, "fill", "0 10000000 abcdef") + # Try with ob flag + await mc._pubmsg("!reloffer stuff") + await asyncio.sleep(1) + # Trigger throttling with large messages + await mc._privmsg(cpname, "tx", "aa"*5000) + await asyncio.sleep(1) + # with pytest.raises(CJPeerError) as e_info: + await mc.send_error(cpname, "fly you fools!") + await asyncio.sleep(1) + return mc + async def asyncSetUp(self): await super().asyncSetUp() jmman = self.jmman @@ -55,7 +131,16 @@ class OnionBaseTestCase(JMTestCase): configdata = list(jmconf.get_msg_channels().values())[2] directory_node = configdata['directory_nodes'].split(',')[0] configdata['directory_nodes'] = directory_node - self.mc = DummyMC(jmman, configdata, 'onionnick') + self.mc = mc = DummyMC(jmman, configdata, 'onionnick') + mc.register_orderbookwatch_callbacks(on_order_seen=self.on_order_seen) + mc.register_taker_callbacks(on_pubkey=self.on_pubkey) + mc.on_connect = self.on_connect + mc.on_disconnect = self.on_disconnect + mc.on_welcome = self.on_welcome + for p in self.mc.peers: + p.connect() + p._status = PEER_STATUS_HANDSHAKED + self.peer = list(self.mc.peers)[0] self.mcc = MessageChannelCollection([self.mc], jmman) await self.mcc.run() @@ -68,5 +153,62 @@ class OnionBaseTestCase(JMTestCase): await cb(self.mc) - async def test_setup(self): - assert 0 + +class OnionPeerTestCase(OnionBaseTestCase): + + async def test_update_status(self): + peer = self.peer + assert peer.status() == PEER_STATUS_HANDSHAKED + peer._status = PEER_STATUS_UNCONNECTED + assert peer.status() == PEER_STATUS_UNCONNECTED + peer.update_status(PEER_STATUS_CONNECTED) + assert peer.status() == PEER_STATUS_CONNECTED + peer.update_status(PEER_STATUS_HANDSHAKED) + assert peer.status() == PEER_STATUS_HANDSHAKED + peer.update_status(PEER_STATUS_DISCONNECTED) + assert peer.status() == PEER_STATUS_DISCONNECTED + + async def test_set_nick(self): + peer = self.peer + assert peer.nick == '' + peer.set_nick('dummynick') + assert peer.nick == 'dummynick' + + async def test_get_nick_peerlocation_ser(self): + peer = self.peer + with self.assertRaises(OnionPeerError): + peer.get_nick_peerlocation_ser() + peer.set_nick('dummynick') + assert peer.get_nick_peerlocation_ser() == ( + 'dummynick;' + 'rr6f6qtleiiwic45bby4zwmiwjrj3jsbmcvutwpqxjziaydjydkk5iad' + '.onion:5222') + + async def test_set_location(self): + peer = self.peer + assert peer.set_location(NOT_SERVING_ONION_HOSTNAME) + assert not peer.set_location('host:port') + assert not peer.set_location('host:-5') + assert peer.set_location('host:5222') + + async def test_peer_location(self): + peer = self.peer + assert peer.peer_location() == ( + 'rr6f6qtleiiwic45bby4zwmiwjrj3jsbmcvutwpqxjziaydjydkk5iad' + '.onion:5222') + assert peer.set_location('host:5222') + assert peer.peer_location() == 'host:5222' + + async def test_send(self): + peer = self.peer + msg = OnionCustomMessage('dummymsg', JM_MESSAGE_TYPES["pubmsg"]) + assert peer.send(msg) + + async def test_receive_message(self): + msg = OnionCustomMessage('!dummymsg test', JM_MESSAGE_TYPES["pubmsg"]) + peer = self.peer + await peer.receive_message(msg) + + async def test_notify_message_unsendable(self): + peer = self.peer + peer.notify_message_unsendable()