Changeset View
Changeset View
Standalone View
Standalone View
test/functional/setup_scripts/chronik-client_plugins.py
| Show All 16 Lines | from test_framework.address import ( | ||||
| ADDRESS_ECREG_UNSPENDABLE, | ADDRESS_ECREG_UNSPENDABLE, | ||||
| P2SH_OP_TRUE, | P2SH_OP_TRUE, | ||||
| SCRIPTSIG_OP_TRUE, | SCRIPTSIG_OP_TRUE, | ||||
| ) | ) | ||||
| from test_framework.blocktools import COINBASE_MATURITY | from test_framework.blocktools import COINBASE_MATURITY | ||||
| from test_framework.messages import COutPoint, CTransaction, CTxIn, CTxOut | from test_framework.messages import COutPoint, CTransaction, CTxIn, CTxOut | ||||
| from test_framework.script import OP_RETURN, CScript | from test_framework.script import OP_RETURN, CScript | ||||
| from test_framework.txtools import pad_tx | from test_framework.txtools import pad_tx | ||||
| from test_framework.util import assert_equal | from test_framework.util import assert_equal, chronik_sub_plugin | ||||
| class ChronikClientPlugins(SetupFramework): | class ChronikClientPlugins(SetupFramework): | ||||
| def set_test_params(self): | def set_test_params(self): | ||||
| self.setup_clean_chain = True | self.setup_clean_chain = True | ||||
| self.num_nodes = 1 | self.num_nodes = 1 | ||||
| self.extra_args = [["-chronik"]] | self.extra_args = [["-chronik"]] | ||||
| def skip_test_if_missing_module(self): | def skip_test_if_missing_module(self): | ||||
| self.skip_if_no_chronik_plugins() | self.skip_if_no_chronik_plugins() | ||||
| def run_test(self): | def run_test(self): | ||||
| from test_framework.chronik.client import pb | from test_framework.chronik.client import pb | ||||
| node = self.nodes[0] | node = self.nodes[0] | ||||
| yield True | yield True | ||||
| chronik = node.get_chronik_client() | chronik = node.get_chronik_client() | ||||
| def ws_msg(txid: str, msg_type): | |||||
| return pb.WsMsg( | |||||
| tx=pb.MsgTx( | |||||
| msg_type=msg_type, | |||||
| txid=bytes.fromhex(txid)[::-1], | |||||
| ) | |||||
| ) | |||||
| def assert_start_raises(*args, **kwargs): | def assert_start_raises(*args, **kwargs): | ||||
| node.assert_start_raises_init_error(["-chronik"], *args, **kwargs) | node.assert_start_raises_init_error(["-chronik"], *args, **kwargs) | ||||
| # Without a plugins.toml, setting up a plugin context is skipped | # Without a plugins.toml, setting up a plugin context is skipped | ||||
| plugins_toml = os.path.join(node.datadir, "plugins.toml") | plugins_toml = os.path.join(node.datadir, "plugins.toml") | ||||
| plugins_dir = os.path.join(node.datadir, "plugins") | plugins_dir = os.path.join(node.datadir, "plugins") | ||||
| # Plugin that colors outputs with the corresponding PUSHDATA of the OP_RETURN, | # Plugin that colors outputs with the corresponding PUSHDATA of the OP_RETURN, | ||||
| ▲ Show 20 Lines • Show All 42 Lines • ▼ Show 20 Lines | """, | ||||
| with node.assert_debug_log( | with node.assert_debug_log( | ||||
| [ | [ | ||||
| "Plugin context initialized Python", | "Plugin context initialized Python", | ||||
| 'Loaded plugin my_plugin.MyPluginPlugin (version 0.1.0) with LOKAD IDs [b"TEST"]', | 'Loaded plugin my_plugin.MyPluginPlugin (version 0.1.0) with LOKAD IDs [b"TEST"]', | ||||
| ] | ] | ||||
| ): | ): | ||||
| self.restart_node(0, ["-chronik", "-chronikreindex"]) | self.restart_node(0, ["-chronik", "-chronikreindex"]) | ||||
| # Init and websockets here so we can confirm msgs are sent server-side | |||||
| ws1 = chronik.ws() | |||||
| ws2 = chronik.ws() | |||||
| coinblockhash = self.generatetoaddress(node, 1, ADDRESS_ECREG_P2SH_OP_TRUE)[0] | coinblockhash = self.generatetoaddress(node, 1, ADDRESS_ECREG_P2SH_OP_TRUE)[0] | ||||
| coinblock = node.getblock(coinblockhash) | coinblock = node.getblock(coinblockhash) | ||||
| cointx = coinblock["tx"][0] | cointx = coinblock["tx"][0] | ||||
| self.log.info("Step 1: Empty regtest chain") | self.log.info("Step 1: Empty regtest chain") | ||||
| yield True | yield True | ||||
| self.log.info("Step 2: Send a tx to create plugin utxos in group 'a'") | self.log.info("Step 2: Send a tx to create plugin utxos in group 'a'") | ||||
| self.generatetoaddress(node, COINBASE_MATURITY, ADDRESS_ECREG_UNSPENDABLE) | self.generatetoaddress(node, COINBASE_MATURITY, ADDRESS_ECREG_UNSPENDABLE) | ||||
| # Subscribe to websockets in test script to support timing match in chronik-client integration tests | |||||
| chronik_sub_plugin(ws1, node, "my_plugin", b"a") | |||||
| chronik_sub_plugin(ws2, node, "my_plugin", b"b") | |||||
| coinvalue = 5000000000 | coinvalue = 5000000000 | ||||
| tx1 = CTransaction() | tx1 = CTransaction() | ||||
| tx1.vin = [CTxIn(COutPoint(int(cointx, 16), 0), SCRIPTSIG_OP_TRUE)] | tx1.vin = [CTxIn(COutPoint(int(cointx, 16), 0), SCRIPTSIG_OP_TRUE)] | ||||
| tx1.vout = [ | tx1.vout = [ | ||||
| CTxOut(0, CScript([OP_RETURN, b"TEST", b"argo", b"alef", b"abc"])), | CTxOut(0, CScript([OP_RETURN, b"TEST", b"argo", b"alef", b"abc"])), | ||||
| CTxOut(1000, P2SH_OP_TRUE), | CTxOut(1000, P2SH_OP_TRUE), | ||||
| CTxOut(1000, P2SH_OP_TRUE), | CTxOut(1000, P2SH_OP_TRUE), | ||||
| CTxOut(coinvalue - 10000, P2SH_OP_TRUE), | CTxOut(coinvalue - 10000, P2SH_OP_TRUE), | ||||
| ] | ] | ||||
| pad_tx(tx1) | pad_tx(tx1) | ||||
| node.sendrawtransaction(tx1.serialize().hex()) | node.sendrawtransaction(tx1.serialize().hex()) | ||||
| assert_equal(ws1.recv(), ws_msg(tx1.hash, pb.TX_ADDED_TO_MEMPOOL)) | |||||
| # Plugin ran on the mempool tx | # Plugin ran on the mempool tx | ||||
| # Note: we must perform these assertions here before yield True | # Note: we must perform these assertions here before yield True | ||||
| # Ensures that plugins are properly indexed before we query for them | # Ensures that plugins are properly indexed before we query for them | ||||
| proto_tx1 = chronik.tx(tx1.hash).ok() | proto_tx1 = chronik.tx(tx1.hash).ok() | ||||
| tx1_plugin_outputs = [ | tx1_plugin_outputs = [ | ||||
| {}, | {}, | ||||
| {"my_plugin": pb.PluginEntry(data=[b"argo"], groups=[b"a"])}, | {"my_plugin": pb.PluginEntry(data=[b"argo"], groups=[b"a"])}, | ||||
| {"my_plugin": pb.PluginEntry(data=[b"alef"], groups=[b"a"])}, | {"my_plugin": pb.PluginEntry(data=[b"alef"], groups=[b"a"])}, | ||||
| ▲ Show 20 Lines • Show All 46 Lines • ▼ Show 20 Lines | """, | ||||
| [tx1_plugin_outputs[1], tx1_plugin_outputs[2]], # "abc" spent | [tx1_plugin_outputs[1], tx1_plugin_outputs[2]], # "abc" spent | ||||
| ) | ) | ||||
| proto_utxos2 = chronik.plugin("my_plugin").utxos(b"b").ok().utxos | proto_utxos2 = chronik.plugin("my_plugin").utxos(b"b").ok().utxos | ||||
| assert_equal( | assert_equal( | ||||
| [utxo.plugins for utxo in proto_utxos2], | [utxo.plugins for utxo in proto_utxos2], | ||||
| tx2_plugin_outputs[1:], | tx2_plugin_outputs[1:], | ||||
| ) | ) | ||||
| assert_equal(ws1.recv(), ws_msg(tx2.hash, pb.TX_ADDED_TO_MEMPOOL)) | |||||
| assert_equal(ws2.recv(), ws_msg(tx2.hash, pb.TX_ADDED_TO_MEMPOOL)) | |||||
| yield True | yield True | ||||
| self.log.info("Step 4: Mine these first two transactions") | self.log.info("Step 4: Mine these first two transactions") | ||||
| # Mine tx1 and tx2 | # Mine tx1 and tx2 | ||||
| block1 = self.generatetoaddress(node, 1, ADDRESS_ECREG_UNSPENDABLE)[-1] | block1 = self.generatetoaddress(node, 1, ADDRESS_ECREG_UNSPENDABLE)[-1] | ||||
| # Lexicographic order | |||||
| txids = sorted([tx1.hash, tx2.hash]) | |||||
| assert_equal(ws1.recv(), ws_msg(txids[0], pb.TX_CONFIRMED)) | |||||
| assert_equal(ws1.recv(), ws_msg(txids[1], pb.TX_CONFIRMED)) | |||||
| assert_equal(ws2.recv(), ws_msg(tx2.hash, pb.TX_CONFIRMED)) | |||||
| yield True | yield True | ||||
| self.log.info("Step 5: Send a third tx to create plugin utxos in group 'c'") | self.log.info("Step 5: Send a third tx to create plugin utxos in group 'c'") | ||||
| tx3 = CTransaction() | tx3 = CTransaction() | ||||
| tx3.vin = [ | tx3.vin = [ | ||||
| CTxIn(COutPoint(tx2.sha256, 1), SCRIPTSIG_OP_TRUE), | CTxIn(COutPoint(tx2.sha256, 1), SCRIPTSIG_OP_TRUE), | ||||
| CTxIn(COutPoint(tx2.sha256, 3), SCRIPTSIG_OP_TRUE), | CTxIn(COutPoint(tx2.sha256, 3), SCRIPTSIG_OP_TRUE), | ||||
| ] | ] | ||||
| tx3.vout = [ | tx3.vout = [ | ||||
| CTxOut(0, CScript([OP_RETURN, b"TEST", b"carp"])), | CTxOut(0, CScript([OP_RETURN, b"TEST", b"carp"])), | ||||
| CTxOut(coinvalue - 30000, P2SH_OP_TRUE), | CTxOut(coinvalue - 30000, P2SH_OP_TRUE), | ||||
| ] | ] | ||||
| pad_tx(tx3) | pad_tx(tx3) | ||||
| node.sendrawtransaction(tx3.serialize().hex()) | node.sendrawtransaction(tx3.serialize().hex()) | ||||
| assert_equal(ws2.recv(), ws_msg(tx3.hash, pb.TX_ADDED_TO_MEMPOOL)) | |||||
| proto_tx3 = chronik.tx(tx3.hash).ok() | proto_tx3 = chronik.tx(tx3.hash).ok() | ||||
| tx3_plugin_inputs = [tx2_plugin_outputs[1], tx2_plugin_outputs[3]] | tx3_plugin_inputs = [tx2_plugin_outputs[1], tx2_plugin_outputs[3]] | ||||
| tx3_plugin_outputs = [ | tx3_plugin_outputs = [ | ||||
| {}, | {}, | ||||
| { | { | ||||
| "my_plugin": pb.PluginEntry( | "my_plugin": pb.PluginEntry( | ||||
| data=[b"carp", b"blub", b"abc"], groups=[b"c"] | data=[b"carp", b"blub", b"abc"], groups=[b"c"] | ||||
| ), | ), | ||||
| Show All 19 Lines | """, | ||||
| ) | ) | ||||
| yield True | yield True | ||||
| self.log.info("Step 6: Mine this tx") | self.log.info("Step 6: Mine this tx") | ||||
| # Mine tx3 | # Mine tx3 | ||||
| block2 = self.generatetoaddress(node, 1, ADDRESS_ECREG_UNSPENDABLE)[-1] | block2 = self.generatetoaddress(node, 1, ADDRESS_ECREG_UNSPENDABLE)[-1] | ||||
| assert_equal(ws2.recv(), ws_msg(tx3.hash, pb.TX_CONFIRMED)) | |||||
| yield True | yield True | ||||
| self.log.info("Step 7: Invalidate the block with the third tx") | self.log.info("Step 7: Invalidate the block with the third tx") | ||||
| # Disconnect block2, inputs + outputs still work | # Disconnect block2, inputs + outputs still work | ||||
| node.invalidateblock(block2) | node.invalidateblock(block2) | ||||
| assert_equal(ws2.recv(), ws_msg(tx3.hash, pb.TX_ADDED_TO_MEMPOOL)) | |||||
| yield True | yield True | ||||
| self.log.info("Step 8: Invalidate the block with the first two txs") | self.log.info("Step 8: Invalidate the block with the first two txs") | ||||
| node.invalidateblock(block1) | node.invalidateblock(block1) | ||||
| # Topological order | |||||
| assert_equal(ws1.recv(), ws_msg(tx1.hash, pb.TX_ADDED_TO_MEMPOOL)) | |||||
| assert_equal(ws1.recv(), ws_msg(tx2.hash, pb.TX_ADDED_TO_MEMPOOL)) | |||||
| # Reorg first clears the mempool and then adds back in topological order | |||||
| assert_equal(ws2.recv(), ws_msg(tx3.hash, pb.TX_REMOVED_FROM_MEMPOOL)) | |||||
| assert_equal(ws2.recv(), ws_msg(tx2.hash, pb.TX_ADDED_TO_MEMPOOL)) | |||||
| assert_equal(ws2.recv(), ws_msg(tx3.hash, pb.TX_ADDED_TO_MEMPOOL)) | |||||
| yield True | yield True | ||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||
| ChronikClientPlugins().main() | ChronikClientPlugins().main() | ||||