Skip to content

Commit

Permalink
tests: Add whitelist plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
edouardparis committed Jul 7, 2022
1 parent 1375fc5 commit e819759
Show file tree
Hide file tree
Showing 6 changed files with 337 additions and 9 deletions.
137 changes: 137 additions & 0 deletions tests/plugins/whitelist/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions tests/plugins/whitelist/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "whitelist"
version = "0.1.0"
edition = "2021"

[dependencies]
bitcoin = "0.28.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
72 changes: 72 additions & 0 deletions tests/plugins/whitelist/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use std::error::Error;
use std::fs::File;
use std::io::{self, BufRead, Write};
use std::str::FromStr;

use bitcoin::{consensus::encode, hashes::hex::FromHex, Address, Script, Transaction};
use serde::{Deserialize, Deserializer};
use serde_json::json;

/// A plugin which returns any attempt with a spend transaction sending funds to unknown addresses
fn main() -> Result<(), Box<dyn Error>> {
let mut buffer = String::new();
let stdin = io::stdin();
stdin.read_line(&mut buffer)?;
let req: Request = serde_json::from_str(&buffer)?;

let whitelist_file = File::open(req.config.whitelist_file_path)?;
let mut whitelist: Vec<Script> = Vec::new();
for line in io::BufReader::new(whitelist_file).lines() {
if let Ok(value) = line {
let address = Address::from_str(&value)?;
whitelist.push(address.payload.script_pubkey());
}
}

let mut vaults_to_revault: Vec<String> = Vec::new();
for attempt in req.block_info.new_attempts {
for output in attempt.candidate_tx.output {
if !whitelist.contains(&output.script_pubkey) {
vaults_to_revault.push(attempt.deposit_outpoint);
break;
}
}
}

let resp = json!({ "revault": vaults_to_revault });
let bytes = serde_json::to_vec(&resp)?;
io::stdout().write(&bytes)?;
Ok(())
}

#[derive(Deserialize)]
struct Request {
block_info: BlockInfo,
config: Config,
}

#[derive(Deserialize)]
struct Config {
whitelist_file_path: String,
}

#[derive(Deserialize)]
struct BlockInfo {
new_attempts: Vec<Attempt>,
}

#[derive(Deserialize)]
struct Attempt {
deposit_outpoint: String,
#[serde(deserialize_with = "deserialize_tx")]
candidate_tx: Transaction,
}

pub fn deserialize_tx<'de, D>(deserializer: D) -> Result<Transaction, D::Error>
where
D: Deserializer<'de>,
{
let hex = String::deserialize(deserializer)?;
let bytes = Vec::from_hex(&hex).map_err(serde::de::Error::custom)?;
encode::deserialize::<Transaction>(&bytes).map_err(serde::de::Error::custom)
}
1 change: 1 addition & 0 deletions tests/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 12 additions & 8 deletions tests/test_framework/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,17 @@ def get_descriptors(stks_xpubs, cosigs_keys, mans_xpubs, mans_thresh, cpfp_xpubs
)


def compile_rust_binary(directory_path):
cargo_toml = os.path.join(directory_path, "Cargo.toml")
try:
subprocess.check_call(
["cargo", "build", "--manifest-path", cargo_toml]
)
except subprocess.CalledProcessError as e:
logging.error(f"Error compiling {str(directory_path)}: {str(e)}")
raise e


# FIXME: have a python-revault-tx lib to avoid this hack..
def get_signed_txs(
stks_xprivs,
Expand All @@ -166,14 +177,7 @@ def get_signed_txs(
"txbuilder",
)
)
txbuilder_cargo_toml = os.path.join(txbuilder_dir, "Cargo.toml")
try:
subprocess.check_call(
["cargo", "build", "--manifest-path", txbuilder_cargo_toml]
)
except subprocess.CalledProcessError as e:
logging.error(f"Error compiling txbuilder: {str(e)}")
raise e
compile_rust_binary(txbuilder_dir)

txbuilder_bin = os.path.join(txbuilder_dir, "target", "debug", "txbuilder")
cmd = [
Expand Down
107 changes: 106 additions & 1 deletion tests/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from base64 import b64encode

from fixtures import *
from test_framework.utils import COIN, DEPOSIT_ADDRESS, DERIV_INDEX, CSV
from test_framework.utils import COIN, DEPOSIT_ADDRESS, DERIV_INDEX, CSV, compile_rust_binary


def test_max_value_in_flight(miradord, bitcoind):
Expand Down Expand Up @@ -195,6 +195,111 @@ 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"
)
whitelist_file = open(whitelist_file_path, "w")
whitelist_file.close()

whitelist_directory = os.path.join(os.path.dirname(__file__), "plugins", "whitelist")
compile_rust_binary(whitelist_directory)

plugin_path = os.path.join(whitelist_directory, "target", "debug", "whitelist")
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
)
vaults_outpoints.append(deposit_outpoint)
vaults_txs.append(txs)

# 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 second vault
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]}'"
)

# 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()

# Unvault the first vault
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",
]
)

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

0 comments on commit e819759

Please sign in to comment.