Skip to content

Commit

Permalink
Add plugin revault_no_spend.py
Browse files Browse the repository at this point in the history
  • Loading branch information
edouardparis committed Jun 17, 2022
1 parent f1a5109 commit 253b3e8
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 1 deletion.
27 changes: 27 additions & 0 deletions tests/plugins/revault_no_spend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
"""A plugin which returns any attempt without candidate spend transaction as needing to be revaulted"""

import json
import sys


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

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"])

resp = {"revault": vaults_without_spend_outpoints}
sys.stdout.write(json.dumps(resp))
sys.stdout.flush()
57 changes: 57 additions & 0 deletions tests/test_framework/coordinator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import cryptography
import json
import os
import random
import select
import socket
import threading
Expand Down Expand Up @@ -149,6 +150,26 @@ def server_noise_conn(self, fd):
f"Unknown client key. Keys: {','.join(k.hex() for k in self.client_pubkeys)}"
)

def client_noise_conn(self, client_noisepriv):
"""Create a new connection to the coordinator, performing the Noise handshake."""
conn = NoiseConnection.from_name(b"Noise_KK_25519_ChaChaPoly_SHA256")

conn.set_as_initiator()
conn.set_keypair_from_private_bytes(Keypair.STATIC, client_noisepriv)
conn.set_keypair_from_private_bytes(Keypair.REMOTE_STATIC, self.coordinator_privkey)
conn.start_handshake()

sock = socket.socket()
sock.settimeout(TIMEOUT // 10)
sock.connect(("localhost", self.port))
msg = conn.write_message(b"practical_revault_0")
sock.sendall(msg)
resp = sock.recv(32 + 16) # Key size + Mac size
assert len(resp) > 0
conn.read_message(resp)

return sock, conn

def read_msg(self, fd, noise_conn):
"""read a noise-encrypted message from this stream.
Expand Down Expand Up @@ -190,3 +211,39 @@ def read_data(self, fd, max_len):
if d == b"":
return data
data += d

def set_spend_tx(
self,
manager_privkey,
deposit_outpoints,
spend_tx,
):
"""
Send a `set_spend_tx` message to the coordinator
"""
(sock, conn) = self.client_noise_conn(manager_privkey)
msg_id = random.randint(0, 2 ** 32)
msg = {
"id": msg_id,
"method": "set_spend_tx",
"params": {
"deposit_outpoints": deposit_outpoints,
"spend_tx": spend_tx,
}
}

msg_serialized = json.dumps(msg)
self.send_msg(sock, conn, msg_serialized)

# Same for decryption, careful to read length first and then the body
resp_header = sock.recv(2 + 16)
assert len(resp_header) > 0
resp_header = conn.decrypt(resp_header)
resp_len = int.from_bytes(resp_header, "big")
resp = sock.recv(resp_len)
assert len(resp) == resp_len
resp = conn.decrypt(resp)

resp = json.loads(resp)
assert resp["id"] == msg_id, "Reusing the same Noise connection across threads?"
assert resp["result"]["ack"]
70 changes: 69 additions & 1 deletion tests/test_plugins.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import tempfile
import time

from base64 import b64encode

from fixtures import *
from test_framework.utils import COIN, DEPOSIT_ADDRESS, DERIV_INDEX, CSV
Expand Down Expand Up @@ -119,6 +120,73 @@ def test_max_value_in_flight(miradord, bitcoind):
miradord.wait_for_log(f"Forgetting about consumed vault at '{deposit_outpoint}'")


def test_revault_attempts_without_spend_tx(miradord, bitcoind, coordinator, noise_keys):
"""
Sanity check that we are only going to revault attempts that have no candidate
spend transaction.
"""
plugin_path = os.path.join(
os.path.dirname(__file__), "plugins", "revault_no_spend.py"
)
miradord.add_plugins([{"path": plugin_path}])

# Should get us exactly to the max value
vaults_txs = []
vaults_outpoints = []
deposit_value = 4
for _ 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)
vaults_outpoints.append(deposit_outpoint)
vaults_txs.append(txs)

# We share the spend to the coordinator only for vault #0
spend_tx = b64encode(bytes.fromhex(vaults_txs[0]["spend"]["tx"])).decode()
coordinator.set_spend_tx(noise_keys["manager"].privkey, [vaults_outpoints[0]], spend_tx)

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 because the spend was not
# shared to coordinator.
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}'")


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."""
Expand Down

0 comments on commit 253b3e8

Please sign in to comment.