Skip to content

Commit 75d7793

Browse files
committed
tests: Add whitelist plugin
1 parent 130963a commit 75d7793

File tree

6 files changed

+337
-9
lines changed

6 files changed

+337
-9
lines changed

tests/plugins/whitelist/Cargo.lock

Lines changed: 137 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/plugins/whitelist/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[package]
2+
name = "whitelist"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
bitcoin = "0.28.1"
8+
serde = { version = "1.0", features = ["derive"] }
9+
serde_json = "1.0"
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
use std::error::Error;
2+
use std::fs::File;
3+
use std::io::{self, BufRead, Write};
4+
use std::str::FromStr;
5+
6+
use bitcoin::{consensus::encode, hashes::hex::FromHex, Address, Script, Transaction};
7+
use serde::{Deserialize, Deserializer};
8+
use serde_json::json;
9+
10+
/// A plugin which returns any attempt with a spend transaction sending funds to unknown addresses
11+
fn main() -> Result<(), Box<dyn Error>> {
12+
let mut buffer = String::new();
13+
let stdin = io::stdin();
14+
stdin.read_line(&mut buffer)?;
15+
let req: Request = serde_json::from_str(&buffer)?;
16+
17+
let whitelist_file = File::open(req.config.whitelist_file_path)?;
18+
let mut whitelist: Vec<Script> = Vec::new();
19+
for line in io::BufReader::new(whitelist_file).lines() {
20+
if let Ok(value) = line {
21+
let address = Address::from_str(&value)?;
22+
whitelist.push(address.payload.script_pubkey());
23+
}
24+
}
25+
26+
let mut vaults_to_revault: Vec<String> = Vec::new();
27+
for attempt in req.block_info.new_attempts {
28+
for output in attempt.candidate_tx.output {
29+
if !whitelist.contains(&output.script_pubkey) {
30+
vaults_to_revault.push(attempt.deposit_outpoint);
31+
break;
32+
}
33+
}
34+
}
35+
36+
let resp = json!({ "revault": vaults_to_revault });
37+
let bytes = serde_json::to_vec(&resp)?;
38+
io::stdout().write(&bytes)?;
39+
Ok(())
40+
}
41+
42+
#[derive(Deserialize)]
43+
struct Request {
44+
block_info: BlockInfo,
45+
config: Config,
46+
}
47+
48+
#[derive(Deserialize)]
49+
struct Config {
50+
whitelist_file_path: String,
51+
}
52+
53+
#[derive(Deserialize)]
54+
struct BlockInfo {
55+
new_attempts: Vec<Attempt>,
56+
}
57+
58+
#[derive(Deserialize)]
59+
struct Attempt {
60+
deposit_outpoint: String,
61+
#[serde(deserialize_with = "deserialize_tx")]
62+
candidate_tx: Transaction,
63+
}
64+
65+
pub fn deserialize_tx<'de, D>(deserializer: D) -> Result<Transaction, D::Error>
66+
where
67+
D: Deserializer<'de>,
68+
{
69+
let hex = String::deserialize(deserializer)?;
70+
let bytes = Vec::from_hex(&hex).map_err(serde::de::Error::custom)?;
71+
encode::deserialize::<Transaction>(&bytes).map_err(serde::de::Error::custom)
72+
}

tests/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ bip32~=2.0
66
pynacl==1.4
77
noiseprotocol==0.3.1
88
toml==0.10.2
9+
bitcoin==1.1.42
910

1011
# For the bitcoind proxy
1112
Flask==2.0.3

tests/test_framework/utils.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,17 @@ def get_descriptors(stks_xpubs, cosigs_keys, mans_xpubs, mans_thresh, cpfp_xpubs
140140
)
141141

142142

143+
def compile_rust_binary(directory_path):
144+
cargo_toml = os.path.join(directory_path, "Cargo.toml")
145+
try:
146+
subprocess.check_call(
147+
["cargo", "build", "--manifest-path", cargo_toml]
148+
)
149+
except subprocess.CalledProcessError as e:
150+
logging.error(f"Error compiling {str(directory_path)}: {str(e)}")
151+
raise e
152+
153+
143154
# FIXME: have a python-revault-tx lib to avoid this hack..
144155
def get_signed_txs(
145156
stks_xprivs,
@@ -166,14 +177,7 @@ def get_signed_txs(
166177
"txbuilder",
167178
)
168179
)
169-
txbuilder_cargo_toml = os.path.join(txbuilder_dir, "Cargo.toml")
170-
try:
171-
subprocess.check_call(
172-
["cargo", "build", "--manifest-path", txbuilder_cargo_toml]
173-
)
174-
except subprocess.CalledProcessError as e:
175-
logging.error(f"Error compiling txbuilder: {str(e)}")
176-
raise e
180+
compile_rust_binary(txbuilder_dir)
177181

178182
txbuilder_bin = os.path.join(txbuilder_dir, "target", "debug", "txbuilder")
179183
cmd = [

tests/test_plugins.py

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from base64 import b64encode
55

66
from fixtures import *
7-
from test_framework.utils import COIN, DEPOSIT_ADDRESS, DERIV_INDEX, CSV
7+
from test_framework.utils import COIN, DEPOSIT_ADDRESS, DERIV_INDEX, CSV, compile_rust_binary
88

99

1010
def test_max_value_in_flight(miradord, bitcoind):
@@ -195,6 +195,111 @@ def test_revault_attempts_without_spend_tx(miradord, bitcoind, coordinator, nois
195195
miradord.wait_for_log(f"Forgetting about consumed vault at '{deposit_outpoint}'")
196196

197197

198+
def test_whitelist(miradord, bitcoind, coordinator, noise_keys):
199+
"""
200+
Sanity check that we are only going to revault attempts that have no candidate
201+
spend transaction.
202+
"""
203+
204+
whitelist_file_path = os.path.join(
205+
os.path.dirname(__file__), "plugins", "whitelist.txt"
206+
)
207+
whitelist_file = open(whitelist_file_path, "w")
208+
whitelist_file.close()
209+
210+
whitelist_directory = os.path.join(os.path.dirname(__file__), "plugins", "whitelist")
211+
compile_rust_binary(whitelist_directory)
212+
213+
plugin_path = os.path.join(whitelist_directory, "target", "debug", "whitelist")
214+
miradord.add_plugins(
215+
[{"path": plugin_path, "config": {"whitelist_file_path": whitelist_file_path}}]
216+
)
217+
218+
vaults_txs = []
219+
vaults_outpoints = []
220+
deposit_value = 4
221+
for i in range(2):
222+
deposit_txid, deposit_outpoint = bitcoind.create_utxo(
223+
DEPOSIT_ADDRESS,
224+
deposit_value,
225+
)
226+
bitcoind.generate_block(1, deposit_txid)
227+
txs = miradord.watch_vault(
228+
deposit_outpoint, deposit_value * COIN, DERIV_INDEX
229+
)
230+
vaults_outpoints.append(deposit_outpoint)
231+
vaults_txs.append(txs)
232+
233+
# We share the spend txs to the coordinators
234+
spend_tx = b64encode(bytes.fromhex(vaults_txs[0]["spend"]["tx"])).decode()
235+
coordinator.set_spend_tx(
236+
noise_keys["manager"].privkey, [vaults_outpoints[0]], spend_tx
237+
)
238+
spend_tx = b64encode(bytes.fromhex(vaults_txs[1]["spend"]["tx"])).decode()
239+
coordinator.set_spend_tx(
240+
noise_keys["manager"].privkey, [vaults_outpoints[1]], spend_tx
241+
)
242+
243+
# Unvault the second vault
244+
bitcoind.rpc.sendrawtransaction(vaults_txs[1]["unvault"]["tx"])
245+
unvault_txid = bitcoind.rpc.decoderawtransaction(vaults_txs[1]["unvault"]["tx"])[
246+
"txid"
247+
]
248+
bitcoind.generate_block(1, unvault_txid)
249+
miradord.wait_for_logs(
250+
[
251+
f"Got a confirmed Unvault UTXO for vault at '{vaults_outpoints[1]}'",
252+
f"Broadcasted Cancel transaction '{vaults_txs[1]['cancel']['tx']['20']}'",
253+
]
254+
)
255+
256+
# The Cancel transactions has been broadcast for vault #1 because the spend
257+
# was sending funds to an address not present in the whitelist file.
258+
cancel_txid = bitcoind.rpc.decoderawtransaction(
259+
vaults_txs[1]["cancel"]["tx"]["20"]
260+
)["txid"]
261+
bitcoind.generate_block(1, wait_for_mempool=cancel_txid)
262+
miradord.wait_for_log(
263+
f"Cancel transaction was confirmed for vault at '{vaults_outpoints[1]}'"
264+
)
265+
266+
# We append the address of the first spend tx to the whitelist
267+
whitelist_file = open(whitelist_file_path, "w")
268+
spend_tx = bitcoind.rpc.decoderawtransaction(vaults_txs[0]["spend"]["tx"])
269+
for output in spend_tx["vout"]:
270+
whitelist_file.write(output["scriptPubKey"]["address"])
271+
whitelist_file.write("\n")
272+
whitelist_file.close()
273+
274+
# Unvault the first vault
275+
bitcoind.rpc.sendrawtransaction(vaults_txs[0]["unvault"]["tx"])
276+
unvault_txid = bitcoind.rpc.decoderawtransaction(vaults_txs[0]["unvault"]["tx"])[
277+
"txid"
278+
]
279+
bitcoind.generate_block(1, unvault_txid)
280+
miradord.wait_for_logs(
281+
[
282+
f"Got a confirmed Unvault UTXO for vault at '{vaults_outpoints[0]}'",
283+
"Done processing block",
284+
]
285+
)
286+
287+
# Now mine the spend tx for vault #0
288+
bitcoind.generate_block(CSV)
289+
bitcoind.rpc.sendrawtransaction(vaults_txs[0]["spend"]["tx"])
290+
spend_txid = bitcoind.rpc.decoderawtransaction(vaults_txs[0]["spend"]["tx"])["txid"]
291+
bitcoind.generate_block(1, wait_for_mempool=spend_txid)
292+
miradord.wait_for_log(
293+
f"Noticed .* that Spend transaction was confirmed for vault at '{vaults_outpoints[0]}'"
294+
)
295+
# Generate two days worth of blocks, the WT should forget about this vault
296+
bitcoind.generate_block(288)
297+
miradord.wait_for_log(f"Forgetting about consumed vault at '{deposit_outpoint}'")
298+
299+
# clean the whitelist file
300+
os.remove(whitelist_file_path)
301+
302+
198303
def test_multiple_plugins(miradord, bitcoind):
199304
"""Test we use the union of all plugins output to revault. That is, the stricter one
200305
will always rule."""

0 commit comments

Comments
 (0)