diff --git a/tests/plugins/whitelist.py b/tests/plugins/whitelist.py new file mode 100755 index 0000000..4ccd6e6 --- /dev/null +++ b/tests/plugins/whitelist.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""A plugin which returns any attempt with a spend transaction sending funds to unknown addresses""" + +import json +import sys +import bitcoin + + +def read_request(): + """Read a JSON request from stdin up to the '\n' delimiter.""" + buf = "" + while len(buf) == 0 or buf[-1] != "\n": + buf += sys.stdin.read() + return json.loads(buf) + + +if __name__ == "__main__": + req = read_request() + block_info = req["block_info"] + whitelist_file = open(req["config"]["whitelist_file_path"], 'r') + whitelist = map(lambda address: address.strip(), whitelist_file.readlines()) + + vaults_without_spend_outpoints = [] + for vault in block_info["new_attempts"]: + if vault["candidate_tx"] is None: + vaults_without_spend_outpoints.append(vault["deposit_outpoint"]) + else: + candidate_tx = bitcoin.deserialize(vault["candidate_tx"]) + vaults_without_spend_outpoints.append(vault["deposit_outpoint"]) + + resp = {"revault": vaults_without_spend_outpoints} + sys.stdout.write(json.dumps(resp)) + sys.stdout.flush() diff --git a/tests/requirements.txt b/tests/requirements.txt index 7013952..ea6b8ed 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -6,6 +6,7 @@ bip32~=2.0 pynacl==1.4 noiseprotocol==0.3.1 toml==0.10.2 +bitcoin==1.1.42 # For the bitcoind proxy Flask==2.0.3 diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 7109d76..81991a1 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -195,6 +195,103 @@ def test_revault_attempts_without_spend_tx(miradord, bitcoind, coordinator, nois miradord.wait_for_log(f"Forgetting about consumed vault at '{deposit_outpoint}'") +def test_whitelist(miradord, bitcoind, coordinator, noise_keys): + """ + Sanity check that we are only going to revault attempts that have no candidate + spend transaction. + """ + + whitelist_file_path = os.path.join( + os.path.dirname(__file__), "plugins", "whitelist.txt" + ) + plugin_path = os.path.join(os.path.dirname(__file__), "plugins", "whitelist.py") + miradord.add_plugins( + [{"path": plugin_path, "config": {"whitelist_file_path": whitelist_file_path}}] + ) + + vaults_txs = [] + vaults_outpoints = [] + deposit_value = 4 + for i in range(2): + deposit_txid, deposit_outpoint = bitcoind.create_utxo( + DEPOSIT_ADDRESS, + deposit_value, + ) + bitcoind.generate_block(1, deposit_txid) + txs = miradord.watch_vault( + deposit_outpoint, deposit_value * COIN, DERIV_INDEX + i + ) + vaults_outpoints.append(deposit_outpoint) + vaults_txs.append(txs) + + # We append the address of the first spend tx to the whitelist + whitelist_file = open(whitelist_file_path, "w") + spend_tx = bitcoind.rpc.decoderawtransaction(vaults_txs[0]["spend"]["tx"]) + for output in spend_tx["vout"]: + whitelist_file.write(output["scriptPubKey"]["address"]) + whitelist_file.write("\n") + whitelist_file.close() + + # We share the spend txs to the coordinators + spend_tx = b64encode(bytes.fromhex(vaults_txs[0]["spend"]["tx"])).decode() + coordinator.set_spend_tx( + noise_keys["manager"].privkey, [vaults_outpoints[0]], spend_tx + ) + spend_tx = b64encode(bytes.fromhex(vaults_txs[1]["spend"]["tx"])).decode() + coordinator.set_spend_tx( + noise_keys["manager"].privkey, [vaults_outpoints[1]], spend_tx + ) + + # Unvault the two vaults + bitcoind.rpc.sendrawtransaction(vaults_txs[0]["unvault"]["tx"]) + unvault_txid = bitcoind.rpc.decoderawtransaction(vaults_txs[0]["unvault"]["tx"])[ + "txid" + ] + bitcoind.generate_block(1, unvault_txid) + miradord.wait_for_logs( + [ + f"Got a confirmed Unvault UTXO for vault at '{vaults_outpoints[0]}'", + "Done processing block", + ] + ) + bitcoind.rpc.sendrawtransaction(vaults_txs[1]["unvault"]["tx"]) + unvault_txid = bitcoind.rpc.decoderawtransaction(vaults_txs[1]["unvault"]["tx"])[ + "txid" + ] + bitcoind.generate_block(1, unvault_txid) + miradord.wait_for_logs( + [ + f"Got a confirmed Unvault UTXO for vault at '{vaults_outpoints[1]}'", + f"Broadcasted Cancel transaction '{vaults_txs[1]['cancel']['tx']['20']}'", + ] + ) + + # The Cancel transactions has been broadcast for vault #1 because the spend + # was sending funds to an address not present in the whitelist file. + cancel_txid = bitcoind.rpc.decoderawtransaction( + vaults_txs[1]["cancel"]["tx"]["20"] + )["txid"] + bitcoind.generate_block(1, wait_for_mempool=cancel_txid) + miradord.wait_for_log( + f"Cancel transaction was confirmed for vault at '{vaults_outpoints[1]}'" + ) + + # Now mine the spend tx for vault #0 + bitcoind.generate_block(CSV) + bitcoind.rpc.sendrawtransaction(vaults_txs[0]["spend"]["tx"]) + spend_txid = bitcoind.rpc.decoderawtransaction(vaults_txs[0]["spend"]["tx"])["txid"] + bitcoind.generate_block(1, wait_for_mempool=spend_txid) + miradord.wait_for_log( + f"Noticed .* that Spend transaction was confirmed for vault at '{vaults_outpoints[0]}'" + ) + # Generate two days worth of blocks, the WT should forget about this vault + bitcoind.generate_block(288) + miradord.wait_for_log(f"Forgetting about consumed vault at '{deposit_outpoint}'") + + # clean the whitelist file + os.remove(whitelist_file_path) + + def test_multiple_plugins(miradord, bitcoind): """Test we use the union of all plugins output to revault. That is, the stricter one will always rule."""