Skip to content

Commit cc23359

Browse files
committed
Add plugin revault_no_spend.py
1 parent b01f8c5 commit cc23359

File tree

3 files changed

+153
-1
lines changed

3 files changed

+153
-1
lines changed

tests/plugins/revault_no_spend.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env python3
2+
"""A plugin which returns any attempt without candidate spend transaction as needing to be revaulted"""
3+
4+
import json
5+
import sys
6+
7+
8+
def read_request():
9+
"""Read a JSON request from stdin up to the '\n' delimiter."""
10+
buf = ""
11+
while len(buf) == 0 or buf[-1] != "\n":
12+
buf += sys.stdin.read()
13+
return json.loads(buf)
14+
15+
16+
if __name__ == "__main__":
17+
req = read_request()
18+
block_info = req["block_info"]
19+
20+
vaults_without_spend_outpoints = []
21+
for vault in block_info["new_attempts"]:
22+
if vault["candidate_tx"] is None:
23+
vaults_without_spend_outpoints.append(vault["deposit_outpoint"])
24+
25+
resp = {"revault": vaults_without_spend_outpoints}
26+
sys.stdout.write(json.dumps(resp))
27+
sys.stdout.flush()

tests/test_framework/coordinator.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import cryptography
22
import json
33
import os
4+
import random
45
import select
56
import socket
67
import threading
@@ -149,6 +150,26 @@ def server_noise_conn(self, fd):
149150
f"Unknown client key. Keys: {','.join(k.hex() for k in self.client_pubkeys)}"
150151
)
151152

153+
def client_noise_conn(self, client_noisepriv):
154+
"""Create a new connection to the coordinator, performing the Noise handshake."""
155+
conn = NoiseConnection.from_name(b"Noise_KK_25519_ChaChaPoly_SHA256")
156+
157+
conn.set_as_initiator()
158+
conn.set_keypair_from_private_bytes(Keypair.STATIC, client_noisepriv)
159+
conn.set_keypair_from_private_bytes(Keypair.REMOTE_STATIC, self.coordinator_privkey)
160+
conn.start_handshake()
161+
162+
sock = socket.socket()
163+
sock.settimeout(TIMEOUT // 10)
164+
sock.connect(("localhost", self.port))
165+
msg = conn.write_message(b"practical_revault_0")
166+
sock.sendall(msg)
167+
resp = sock.recv(32 + 16) # Key size + Mac size
168+
assert len(resp) > 0
169+
conn.read_message(resp)
170+
171+
return sock, conn
172+
152173
def read_msg(self, fd, noise_conn):
153174
"""read a noise-encrypted message from this stream.
154175
@@ -190,3 +211,39 @@ def read_data(self, fd, max_len):
190211
if d == b"":
191212
return data
192213
data += d
214+
215+
def set_spend_tx(
216+
self,
217+
manager_privkey,
218+
deposit_outpoints,
219+
spend_tx,
220+
):
221+
"""
222+
Send a `set_spend_tx` message to the coordinator
223+
"""
224+
(sock, conn) = self.client_noise_conn(manager_privkey)
225+
msg_id = random.randint(0, 2 ** 32)
226+
msg = {
227+
"id": msg_id,
228+
"method": "set_spend_tx",
229+
"params": {
230+
"deposit_outpoints": deposit_outpoints,
231+
"spend_tx": spend_tx,
232+
}
233+
}
234+
235+
msg_serialized = json.dumps(msg)
236+
self.send_msg(sock, conn, msg_serialized)
237+
238+
# Same for decryption, careful to read length first and then the body
239+
resp_header = sock.recv(2 + 16)
240+
assert len(resp_header) > 0
241+
resp_header = conn.decrypt(resp_header)
242+
resp_len = int.from_bytes(resp_header, "big")
243+
resp = sock.recv(resp_len)
244+
assert len(resp) == resp_len
245+
resp = conn.decrypt(resp)
246+
247+
resp = json.loads(resp)
248+
assert resp["id"] == msg_id, "Reusing the same Noise connection across threads?"
249+
assert resp["result"]["ack"]

tests/test_plugins.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
import tempfile
3-
import time
3+
4+
from base64 import b64encode
45

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

121122

123+
def test_revault_attempts_without_spend_tx(miradord, bitcoind, coordinator, noise_keys):
124+
"""
125+
Sanity check that we are only going to revault attempts that have no candidate
126+
spend transaction.
127+
"""
128+
plugin_path = os.path.join(
129+
os.path.dirname(__file__), "plugins", "revault_no_spend.py"
130+
)
131+
miradord.add_plugins([{"path": plugin_path}])
132+
133+
# Should get us exactly to the max value
134+
vaults_txs = []
135+
vaults_outpoints = []
136+
deposit_value = 4
137+
for _ in range(2):
138+
deposit_txid, deposit_outpoint = bitcoind.create_utxo(
139+
DEPOSIT_ADDRESS, deposit_value,
140+
)
141+
bitcoind.generate_block(1, deposit_txid)
142+
txs = miradord.watch_vault(deposit_outpoint, deposit_value * COIN, DERIV_INDEX)
143+
vaults_outpoints.append(deposit_outpoint)
144+
vaults_txs.append(txs)
145+
146+
# We share the spend to the coordinator only for vault #0
147+
spend_tx = b64encode(bytes.fromhex(vaults_txs[0]["spend"]["tx"])).decode()
148+
coordinator.set_spend_tx(noise_keys["manager"].privkey, [vaults_outpoints[0]], spend_tx)
149+
150+
bitcoind.rpc.sendrawtransaction(vaults_txs[0]["unvault"]["tx"])
151+
unvault_txid = bitcoind.rpc.decoderawtransaction(vaults_txs[0]["unvault"]["tx"])["txid"]
152+
bitcoind.generate_block(1, unvault_txid)
153+
miradord.wait_for_logs(
154+
[
155+
f"Got a confirmed Unvault UTXO for vault at '{vaults_outpoints[0]}'",
156+
"Done processing block",
157+
]
158+
)
159+
bitcoind.rpc.sendrawtransaction(vaults_txs[1]["unvault"]["tx"])
160+
unvault_txid = bitcoind.rpc.decoderawtransaction(vaults_txs[1]["unvault"]["tx"])["txid"]
161+
bitcoind.generate_block(1, unvault_txid)
162+
miradord.wait_for_logs(
163+
[
164+
f"Got a confirmed Unvault UTXO for vault at '{vaults_outpoints[1]}'",
165+
f"Broadcasted Cancel transaction '{vaults_txs[1]['cancel']['tx']['20']}'",
166+
]
167+
)
168+
169+
# The Cancel transactions has been broadcast because the spend was not
170+
# shared to coordinator.
171+
cancel_txid = bitcoind.rpc.decoderawtransaction(vaults_txs[1]["cancel"]["tx"]["20"])["txid"]
172+
bitcoind.generate_block(1, wait_for_mempool=cancel_txid)
173+
miradord.wait_for_log(
174+
f"Cancel transaction was confirmed for vault at '{vaults_outpoints[1]}'"
175+
)
176+
177+
# Now mine the spend tx for vault #0
178+
bitcoind.generate_block(CSV)
179+
bitcoind.rpc.sendrawtransaction(vaults_txs[0]["spend"]["tx"])
180+
spend_txid = bitcoind.rpc.decoderawtransaction(vaults_txs[0]["spend"]["tx"])["txid"]
181+
bitcoind.generate_block(1, wait_for_mempool=spend_txid)
182+
miradord.wait_for_log(
183+
f"Noticed .* that Spend transaction was confirmed for vault at '{vaults_outpoints[0]}'"
184+
)
185+
# Generate two days worth of blocks, the WT should forget about this vault
186+
bitcoind.generate_block(288)
187+
miradord.wait_for_log(f"Forgetting about consumed vault at '{deposit_outpoint}'")
188+
189+
122190
def test_multiple_plugins(miradord, bitcoind):
123191
"""Test we use the union of all plugins output to revault. That is, the stricter one
124192
will always rule."""

0 commit comments

Comments
 (0)