From 55bd88813090e88924ba17568f274f27d63503f8 Mon Sep 17 00:00:00 2001 From: edouard Date: Thu, 2 Jun 2022 17:53:28 +0200 Subject: [PATCH 1/7] poller: fetch spend tx from coordinator for each unvaulted vault --- doc/PLUGIN.md | 11 ++++--- src/config.rs | 3 +- src/coordinator.rs | 81 ++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 4 ++- src/plugins.rs | 14 ++++++-- src/poller.rs | 33 ++++++++++++++++++- 6 files changed, 136 insertions(+), 10 deletions(-) create mode 100644 src/coordinator.rs diff --git a/doc/PLUGIN.md b/doc/PLUGIN.md index c270053..775a33c 100644 --- a/doc/PLUGIN.md +++ b/doc/PLUGIN.md @@ -32,11 +32,12 @@ An overview of the spending attempts at the current block. ##### Vault resource -| Field | Type | Description | -| ------------------ | ------- | -------------------------------------------- | -| `value` | integer | Value of the vault in satoshis | -| `deposit_outpoint` | string | Deposit outpoint of the vault | -| `unvault_tx` | string | Psbt of the unvault transaction of the vault | +| Field | Type | Description | +| ------------------ | -------------- | -------------------------------------------- | +| `value` | integer | Value of the vault in satoshis | +| `deposit_outpoint` | string | Deposit outpoint of the vault | +| `unvault_tx` | string | Psbt of the unvault transaction of the vault | +| `candidate_tx` | string or null | Hex encoded transaction spending the vault, null if the watchtower did not retrieve it from coordinator | #### Plugin Response diff --git a/src/config.rs b/src/config.rs index 6b4c597..91f4fc8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -150,7 +150,8 @@ pub struct Config { #[serde(deserialize_with = "deserialize_noisepubkey")] pub stakeholder_noise_key: NoisePubkey, /// The host of the sync server (may be an IP or a hidden service) - pub coordinator_host: String, + /// TODO: change for enum handling multiple choice. + pub coordinator_host: SocketAddr, /// The Noise static public key of the sync server #[serde(deserialize_with = "deserialize_noisepubkey")] pub coordinator_noise_key: NoisePubkey, diff --git a/src/coordinator.rs b/src/coordinator.rs new file mode 100644 index 0000000..8d5cb55 --- /dev/null +++ b/src/coordinator.rs @@ -0,0 +1,81 @@ +use std::net::SocketAddr; + +use revault_net::{ + message::coordinator::{GetSpendTx, SpendTx}, + noise::{PublicKey, SecretKey}, + transport::KKTransport, +}; + +use revault_tx::bitcoin::{OutPoint, Transaction}; + +const COORDINATOR_CLIENT_RETRIES: usize = 3; + +pub struct CoordinatorClient { + host: SocketAddr, + our_noise_secret_key: SecretKey, + pub_key: PublicKey, + /// How many times the client will try again + /// to send a request to coordinator upon failure + retries: usize, +} + +impl CoordinatorClient { + pub fn new(our_noise_secret_key: SecretKey, host: SocketAddr, pub_key: PublicKey) -> Self { + Self { + host, + our_noise_secret_key, + pub_key, + retries: COORDINATOR_CLIENT_RETRIES, + } + } + + /// Wrapper to retry a request sent to coordinator upon IO failure + /// according to the configured number of retries. + fn retry Result>( + &self, + request: R, + ) -> Result { + let mut error: Option = None; + for _ in 0..self.retries { + match request() { + Ok(res) => return Ok(res), + Err(e) => error = Some(e), + } + log::debug!( + "Error while communicating with coordinator: {}, retrying", + error.as_ref().expect("An error must have happened"), + ); + std::thread::sleep(std::time::Duration::from_secs(3)); + } + Err(error.expect("An error must have happened")) + } + + fn send_req(&self, req: &revault_net::message::Request) -> Result + where + T: serde::de::DeserializeOwned, + { + log::debug!( + "Sending request to Coordinator: '{}'", + serde_json::to_string(req).unwrap(), + ); + let mut transport = + KKTransport::connect(self.host, &self.our_noise_secret_key, &self.pub_key)?; + transport.send_req(&req) + } + + // Get Spend transaction spending the vault with the given deposit outpoint. + pub fn get_spend_transaction( + &self, + deposit_outpoint: OutPoint, + ) -> Result, revault_net::Error> { + let resp: SpendTx = self.retry(|| { + let msg = GetSpendTx { deposit_outpoint }; + self.send_req(&msg.into()) + })?; + log::debug!( + "Got from Coordinator: '{}'", + serde_json::to_string(&resp).unwrap() + ); + Ok(resp.spend_tx) + } +} diff --git a/src/main.rs b/src/main.rs index 14be63e..2f74c9d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod bitcoind; mod config; +mod coordinator; mod daemonize; mod database; mod keys; @@ -235,6 +236,7 @@ fn main() { let _db_path = db_path.clone(); let _config = config.clone(); let _bitcoind = bitcoind.clone(); + let noise_secret = noise_secret.clone(); move || { listener_main(&_db_path, &_config, _bitcoind, &noise_secret).unwrap_or_else(|e| { log::error!("Error in listener loop: '{}'", e); @@ -246,7 +248,7 @@ fn main() { log::info!("Started miradord.",); let secp_ctx = secp256k1::Secp256k1::verification_only(); - poller::main_loop(&db_path, &secp_ctx, &config, &bitcoind).unwrap_or_else(|e| { + poller::main_loop(&db_path, &secp_ctx, &config, &bitcoind, noise_secret).unwrap_or_else(|e| { log::error!("Error in main loop: '{}'", e); process::exit(1); }); diff --git a/src/plugins.rs b/src/plugins.rs index 98a70f2..a879df1 100644 --- a/src/plugins.rs +++ b/src/plugins.rs @@ -1,5 +1,5 @@ use revault_tx::{ - bitcoin::{Amount, OutPoint}, + bitcoin::{consensus::encode, Amount, OutPoint, Transaction}, transactions::UnvaultTransaction, }; @@ -63,6 +63,12 @@ fn serialize_amount(amount: &Amount, serializer: S) -> Result(tx: &Option, serializer: S) -> Result { + tx.as_ref() + .map(|tx| encode::serialize_hex(tx)) + .serialize(serializer) +} + #[derive(Debug, Clone, Eq, PartialEq)] pub struct Plugin { path: path::PathBuf, @@ -76,7 +82,8 @@ pub struct VaultInfo { pub value: Amount, pub deposit_outpoint: OutPoint, pub unvault_tx: UnvaultTransaction, - // TODO: Spend tx + #[serde(serialize_with = "serialize_tx")] + pub candidate_tx: Option, } /// Information we are passing to a plugin after a new block if there was any update. @@ -204,6 +211,7 @@ mod tests { value: Amount::from_sat(567890), deposit_outpoint, unvault_tx: unvault_tx.clone(), + candidate_tx: None, }) .collect(); let many_outpoints: Vec = (0..10000).map(|_| deposit_outpoint).collect(); @@ -239,6 +247,7 @@ mod tests { ) .unwrap(), unvault_tx: unvault_tx.clone(), + candidate_tx: None, }; let new_block = NewBlockInfo { new_attempts: vec![vault_info], @@ -263,6 +272,7 @@ mod tests { value: Amount::from_sat(567890), deposit_outpoint, unvault_tx: unvault_tx.clone(), + candidate_tx: None, }; let new_block = NewBlockInfo { new_attempts: vec![vault_info], diff --git a/src/poller.rs b/src/poller.rs index 6db4d78..d30c2cb 100644 --- a/src/poller.rs +++ b/src/poller.rs @@ -21,8 +21,12 @@ use revault_tx::{ txouts::DepositTxOut, }; +use revault_net::noise::SecretKey as NoisePrivkey; + use std::{collections::HashMap, convert::TryInto, path, thread}; +use crate::coordinator; + /// How many blocks are we waiting to consider a consumed vault irreversably spent const REORG_WATCH_LIMIT: i32 = 288; @@ -357,6 +361,7 @@ fn check_for_unvault( bitcoind: &BitcoinD, current_tip: &ChainTip, db_updates: &mut DbUpdates, + coordinator_client: &coordinator::CoordinatorClient, ) -> Result, PollerError> { let deleg_vaults = db_blank_vaults(db_path)?; let mut new_attempts = vec![]; @@ -392,10 +397,21 @@ fn check_for_unvault( db_vault.unvault_height = Some(unvault_height); // If needed to be canceled it will be marked as such when plugins tell us so. db_updates.new_unvaulted.insert(db_vault.id, db_vault); + + let candidate_tx = + match coordinator_client.get_spend_transaction(db_vault.deposit_outpoint.clone()) { + Ok(res) => res, + Err(_e) => { + // Because we do not trust the coordinator, we consider it refuses to deliver the + // spend tx if a communication error happened. + None + } + }; let vault_info = VaultInfo { value: db_vault.amount, deposit_outpoint: db_vault.deposit_outpoint, unvault_tx, + candidate_tx, }; new_attempts.push(vault_info); } @@ -483,6 +499,7 @@ fn new_block( config: &Config, bitcoind: &BitcoinD, current_tip: &ChainTip, + coordinator_client: &coordinator::CoordinatorClient, ) -> Result<(), PollerError> { // We want to update our state for a given height, therefore we need to stop the updating // process if we notice that the chain moved forward under us (or we could end up assuming @@ -507,6 +524,7 @@ fn new_block( bitcoind, current_tip, &mut db_updates, + coordinator_client, )?; // Any Cancel tx still unconfirmed? Any vault to forget about? @@ -564,7 +582,13 @@ pub fn main_loop( secp: &secp256k1::Secp256k1, config: &Config, bitcoind: &BitcoinD, + noise_privkey: NoisePrivkey, ) -> Result<(), PollerError> { + let coordinator_client = coordinator::CoordinatorClient::new( + noise_privkey, + config.coordinator_host, + config.coordinator_noise_key, + ); loop { let db_instance = db_instance(db_path)?; let bitcoind_tip = bitcoind.chain_tip(); @@ -575,7 +599,14 @@ pub fn main_loop( panic!("No reorg handling yet"); } - match new_block(db_path, secp, config, bitcoind, &bitcoind_tip) { + match new_block( + db_path, + secp, + config, + bitcoind, + &bitcoind_tip, + &coordinator_client, + ) { Ok(()) => {} // Retry immediately if the tip changed while we were updating ourselves Err(PollerError::TipChanged) => continue, From f985b2e2530b25cadb2d803e1d6550469625eda5 Mon Sep 17 00:00:00 2001 From: edouard Date: Thu, 9 Jun 2022 18:02:59 +0200 Subject: [PATCH 2/7] Make coordinator config optional --- src/config.rs | 34 ++++++++++++++++++++++------------ src/poller.rs | 26 +++++++++++++++----------- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/config.rs b/src/config.rs index 91f4fc8..2f50418 100644 --- a/src/config.rs +++ b/src/config.rs @@ -127,6 +127,17 @@ pub struct BitcoindConfig { pub poll_interval_secs: Duration, } +/// Everything we need to know for talking to coordinator +#[derive(Debug, Clone, Deserialize)] +pub struct CoordinatorConfig { + /// The Noise static public key of the sync server + #[serde(deserialize_with = "deserialize_noisepubkey")] + pub noise_key: NoisePubkey, + /// The host of the sync server (may be an IP or a hidden service) + /// TODO: change for enum handling multiple choice. + pub host: SocketAddr, +} + #[derive(Debug, Clone, Deserialize)] pub struct ScriptsConfig { #[serde(deserialize_with = "deserialize_fromstr")] @@ -149,12 +160,8 @@ pub struct Config { /// The Noise static public keys of "our" stakeholder #[serde(deserialize_with = "deserialize_noisepubkey")] pub stakeholder_noise_key: NoisePubkey, - /// The host of the sync server (may be an IP or a hidden service) - /// TODO: change for enum handling multiple choice. - pub coordinator_host: SocketAddr, - /// The Noise static public key of the sync server - #[serde(deserialize_with = "deserialize_noisepubkey")] - pub coordinator_noise_key: NoisePubkey, + /// Everything we need to know to talk to coordinator + pub coordinator_config: Option, /// An optional custom data directory // TODO: have a default implem as in cosignerd pub data_dir: Option, @@ -353,8 +360,9 @@ mod tests { stakeholder_noise_key = "3de4539519b6baca35ad14cd5bac9a4e0875a851632112405bb0547e6fcf16f6" - coordinator_host = "127.0.0.1:1" - coordinator_noise_key = "d91563973102454a7830137e92d0548bc83b4ea2799f1df04622ca1307381402" + [coordinator_config] + host = "127.0.0.1:1" + noise_key = "d91563973102454a7830137e92d0548bc83b4ea2799f1df04622ca1307381402" [scripts_config] cpfp_descriptor = "wsh(thresh(1,pk(xpub6BaZSKgpaVvibu2k78QsqeDWXp92xLHZxiu1WoqLB9hKhsBf3miBUDX7PJLgSPvkj66ThVHTqdnbXpeu8crXFmDUd4HeM4s4miQS2xsv3Qb/*)))#cwycq5xu" @@ -385,8 +393,9 @@ mod tests { stakeholder_noise_key = "3de4539519b6baca35ad14cd5bac9a4e0875a851632112405bb0547e6fcf16f6" - coordinator_host = "127.0.0.1:1" - coordinator_noise_key = "d91563973102454a7830137e92d0548bc83b4ea2799f1df04622ca1307381402" + [coordinator_config] + host = "127.0.0.1:1" + noise_key = "d91563973102454a7830137e92d0548bc83b4ea2799f1df04622ca1307381402" [[plugins]] path = "src/config.rs" @@ -417,8 +426,9 @@ mod tests { stakeholder_noise_key = "3de4539519b6baca35ad14cd5bac9a4e0875a851632112405bb0547e6fcf16f6" - coordinator_host = "127.0.0.1:1" - coordinator_noise_key = "d91563973102454a7830137e92d0548bc83b4ea2799f1df04622ca1307381402" + [coordinator_config] + host = "127.0.0.1:1" + noise_key = "d91563973102454a7830137e92d0548bc83b4ea2799f1df04622ca1307381402" [scripts_config] cpfp_descriptor = "wsh(thresh(1,pk(xpub6BaZSKgpaVvibu2k78QsqeDWXp92xLHZxiu1WoqLB9hKhsBf3miBUDX7PJLgSPvkj66ThVHTqdnbXpeu8crXFmDUd4HeM4s4miQS2xsv3Qb/*)))#cwycq5xu" diff --git a/src/poller.rs b/src/poller.rs index d30c2cb..488060e 100644 --- a/src/poller.rs +++ b/src/poller.rs @@ -361,7 +361,7 @@ fn check_for_unvault( bitcoind: &BitcoinD, current_tip: &ChainTip, db_updates: &mut DbUpdates, - coordinator_client: &coordinator::CoordinatorClient, + coordinator_client: Option<&coordinator::CoordinatorClient>, ) -> Result, PollerError> { let deleg_vaults = db_blank_vaults(db_path)?; let mut new_attempts = vec![]; @@ -398,15 +398,21 @@ fn check_for_unvault( // If needed to be canceled it will be marked as such when plugins tell us so. db_updates.new_unvaulted.insert(db_vault.id, db_vault); - let candidate_tx = - match coordinator_client.get_spend_transaction(db_vault.deposit_outpoint.clone()) { + let candidate_tx = if let Some(client) = coordinator_client { + match client.get_spend_transaction(db_vault.deposit_outpoint.clone()) { Ok(res) => res, Err(_e) => { // Because we do not trust the coordinator, we consider it refuses to deliver the // spend tx if a communication error happened. None } - }; + } + } else { + // No coordinator configuration was found in the config + // therefore no spend transaction can be shared to plugins + None + }; + let vault_info = VaultInfo { value: db_vault.amount, deposit_outpoint: db_vault.deposit_outpoint, @@ -499,7 +505,7 @@ fn new_block( config: &Config, bitcoind: &BitcoinD, current_tip: &ChainTip, - coordinator_client: &coordinator::CoordinatorClient, + coordinator_client: Option<&coordinator::CoordinatorClient>, ) -> Result<(), PollerError> { // We want to update our state for a given height, therefore we need to stop the updating // process if we notice that the chain moved forward under us (or we could end up assuming @@ -584,11 +590,9 @@ pub fn main_loop( bitcoind: &BitcoinD, noise_privkey: NoisePrivkey, ) -> Result<(), PollerError> { - let coordinator_client = coordinator::CoordinatorClient::new( - noise_privkey, - config.coordinator_host, - config.coordinator_noise_key, - ); + let coordinator_client = config.coordinator_config.as_ref().map(|config| { + coordinator::CoordinatorClient::new(noise_privkey, config.host, config.noise_key) + }); loop { let db_instance = db_instance(db_path)?; let bitcoind_tip = bitcoind.chain_tip(); @@ -605,7 +609,7 @@ pub fn main_loop( config, bitcoind, &bitcoind_tip, - &coordinator_client, + coordinator_client.as_ref(), ) { Ok(()) => {} // Retry immediately if the tip changed while we were updating ourselves From 269d299b7093cef7dfff9fafafd1a15a9c85b24e Mon Sep 17 00:00:00 2001 From: edouard Date: Mon, 13 Jun 2022 12:21:46 +0200 Subject: [PATCH 3/7] Add dummy coordinator to tests --- tests/fixtures.py | 49 +++++-- tests/test_framework/coordinator.py | 192 ++++++++++++++++++++++++++++ tests/test_framework/miradord.py | 9 +- tests/test_framework/utils.py | 14 ++ 4 files changed, 250 insertions(+), 14 deletions(-) create mode 100644 tests/test_framework/coordinator.py diff --git a/tests/fixtures.py b/tests/fixtures.py index 9f6a805..2a854cc 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,6 +1,7 @@ from concurrent import futures from ephemeral_port_reserve import reserve from test_framework.bitcoind import Bitcoind, BitcoindRpcProxy +from test_framework.coordinator import DummyCoordinator from test_framework.miradord import Miradord from test_framework.utils import ( get_descriptors, @@ -9,6 +10,7 @@ MANS_XPUBS, COSIG_PUBKEYS, CSV, + NoiseKeypair, ) import os @@ -114,7 +116,40 @@ def bitcoind(directory): @pytest.fixture -def miradord(request, bitcoind, directory): +def noise_keys(): + # The Noise keys are interdependant, so generate everything in advance + # to avoid roundtrips + coordinator_keys = NoiseKeypair(os.urandom(32)) + manager_keys = NoiseKeypair(os.urandom(32)) + stakeholder_keys = NoiseKeypair(os.urandom(32)) + watchtower_keys = NoiseKeypair(os.urandom(32)) + noise_keys = { + "coordinator": coordinator_keys, + "manager": manager_keys, + "stakeholder": stakeholder_keys, + "watchtower": watchtower_keys, + } + yield noise_keys + + +@pytest.fixture +def coordinator(noise_keys): + coordinator_port = reserve() + coordinator = DummyCoordinator( + coordinator_port, + noise_keys["coordinator"].privkey, + [ + noise_keys["manager"].pubkey, + noise_keys["stakeholder"].pubkey, + noise_keys["watchtower"].pubkey + ], + ) + coordinator.start() + yield coordinator + + +@pytest.fixture +def miradord(request, bitcoind, coordinator, noise_keys, directory): """If a 'mock_bitcoind' pytest marker is set, it will create a proxy for the communication from the miradord process to the bitcoind process. An optional 'mocks' parameter can be set for this marker in order to specify some pre-registered mock of RPC commands. @@ -130,10 +165,6 @@ def miradord(request, bitcoind, directory): ) emer_addr = "bcrt1qewc2348370pgw8kjz8gy09z8xyh0d9fxde6nzamd3txc9gkmjqmq8m4cdq" - coordinator_noise_key = ( - "d91563973102454a7830137e92d0548bc83b4ea2799f1df04622ca1307381402" - ) - bitcoind_cookie = os.path.join(bitcoind.bitcoin_dir, "regtest", ".cookie") bitcoind_rpcport = bitcoind.rpcport @@ -151,10 +182,10 @@ def miradord(request, bitcoind, directory): cpfp_desc, emer_addr, reserve(), - os.urandom(32), - os.urandom(32), - coordinator_noise_key, # Unused yet - reserve(), # Unused yet + noise_keys["watchtower"].privkey, + noise_keys["stakeholder"].privkey, + coordinator.coordinator_pubkey, + coordinator.port, bitcoind_rpcport, bitcoind_cookie, ) diff --git a/tests/test_framework/coordinator.py b/tests/test_framework/coordinator.py new file mode 100644 index 0000000..54fc1a7 --- /dev/null +++ b/tests/test_framework/coordinator.py @@ -0,0 +1,192 @@ +import cryptography +import json +import os +import select +import socket +import threading + +from nacl.public import PrivateKey as Curve25519Private +from noise.connection import NoiseConnection, Keypair +from test_framework.utils import ( + TIMEOUT, +) + +HANDSHAKE_MSG = b"practical_revault_0" + + +class DummyCoordinator: + """A simple in-RAM synchronization server.""" + + def __init__( + self, + port, + coordinator_privkey, + client_pubkeys, + ): + self.port = port + self.coordinator_privkey = coordinator_privkey + self.coordinator_pubkey = bytes( + Curve25519Private(coordinator_privkey).public_key + ) + self.client_pubkeys = client_pubkeys + + # Spin up the coordinator proxy + self.s = socket.socket() + self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.s.bind(("localhost", self.port)) + self.s.listen(1_000) + # Use a pipe to communicate to threads to stop + self.r_close_chann, self.w_close_chann = os.pipe() + + # A mapping from txid to pubkey to signature + self.sigs = {} + # A mapping from deposit_outpoint to base64 tx + self.spend_txs = {} + + def __del__(self): + self.cleanup() + + def start(self): + self.server_thread = threading.Thread(target=self.run) + self.server_thread.start() + + def cleanup(self): + # Write to the pipe to notify the thread it needs to stop + os.write(self.w_close_chann, b".") + self.server_thread.join() + + def run(self): + """Accept new connections until we are told to stop.""" + while True: + r_fds, _, _ = select.select([self.r_close_chann, self.s.fileno()], [], []) + + # First check if we've been told to stop, then spawn a new thread per connection + if self.r_close_chann in r_fds: + break + if self.s.fileno() in r_fds: + t = threading.Thread(target=self.connection_handle, daemon=True) + t.start() + + def connection_handle(self): + """Read and treat requests from this client. Blocking.""" + client_fd, _ = self.s.accept() + client_fd.settimeout(TIMEOUT // 2) + client_noise = self.server_noise_conn(client_fd) + + while True: + # Manually do the select to check if we've been told to stop + r_fds, _, _ = select.select([self.r_close_chann, client_fd], [], []) + if self.r_close_chann in r_fds: + break + req = self.read_msg(client_fd, client_noise) + if req is None: + break + request = json.loads(req) + method, params = request["method"], request["params"] + + if method == "sig": + # TODO: mutex + if params["txid"] not in self.sigs: + self.sigs[params["txid"]] = {} + self.sigs[params["txid"]][params["pubkey"]] = params["signature"] + # TODO: remove this useless response from the protocol + resp = {"result": {"ack": True}, "id": request["id"]} + self.send_msg(client_fd, client_noise, json.dumps(resp)) + + elif method == "get_sigs": + txid = params["txid"] + sigs = self.sigs.get(txid, {}) + resp = {"result": {"signatures": sigs}, "id": request["id"]} + self.send_msg(client_fd, client_noise, json.dumps(resp)) + + elif method == "set_spend_tx": + for outpoint in params["deposit_outpoints"]: + self.spend_txs[outpoint] = params["spend_tx"] + # TODO: remove this useless response from the protocol + resp = {"result": {"ack": True}, "id": request["id"]} + self.send_msg(client_fd, client_noise, json.dumps(resp)) + + elif method == "get_spend_tx": + spend_tx = self.spend_txs.get(params["deposit_outpoint"]) + resp = {"result": {"spend_tx": spend_tx}, "id": request["id"]} + self.send_msg(client_fd, client_noise, json.dumps(resp)) + + else: + assert False, "Invalid request '{}'".format(method) + + def server_noise_conn(self, fd): + """Do practical-revault's Noise handshake with a given client connection.""" + # Read the first message of the handshake only once + data = self.read_data(fd, 32 + len(HANDSHAKE_MSG) + 16) + + # We brute force all pubkeys. FIXME! + for pubkey in self.client_pubkeys: + # Set the local and remote static keys + conn = NoiseConnection.from_name(b"Noise_KK_25519_ChaChaPoly_SHA256") + conn.set_as_responder() + conn.set_keypair_from_private_bytes( + Keypair.STATIC, self.coordinator_privkey + ) + conn.set_keypair_from_public_bytes(Keypair.REMOTE_STATIC, pubkey) + + # Now, get the first message of the handshake + conn.start_handshake() + try: + plaintext = conn.read_message(data) + except cryptography.exceptions.InvalidTag: + continue + else: + assert plaintext[: len(HANDSHAKE_MSG)] == HANDSHAKE_MSG + + # If it didn't fail it was the right key! Finalize the handshake. + resp = conn.write_message() + fd.sendall(resp) + assert conn.handshake_finished + + return conn + + raise Exception( + f"Unknown client key. Keys: {','.join(k.hex() for k in self.client_pubkeys)}" + ) + + def read_msg(self, fd, noise_conn): + """read a noise-encrypted message from this stream. + + Returns None if the socket closed. + """ + # Read first the length prefix + cypher_header = self.read_data(fd, 2 + 16) + if cypher_header == b"": + return None + msg_header = noise_conn.decrypt(cypher_header) + msg_len = int.from_bytes(msg_header, "big") + + # And then the message + cypher_msg = self.read_data(fd, msg_len) + assert len(cypher_msg) == msg_len + msg = noise_conn.decrypt(cypher_msg).decode("utf-8") + return msg + + def send_msg(self, fd, noise_conn, msg): + """Write a noise-encrypted message from this stream.""" + assert isinstance(msg, str) + + # Compute the message header + msg_raw = msg.encode("utf-8") + length_prefix = (len(msg_raw) + 16).to_bytes(2, "big") + encrypted_header = noise_conn.encrypt(length_prefix) + encrypted_body = noise_conn.encrypt(msg_raw) + + # Then send both the header and the message concatenated + fd.sendall(encrypted_header + encrypted_body) + + def read_data(self, fd, max_len): + """Read data from the given fd until there is nothing to read.""" + data = b"" + while True: + d = fd.recv(max_len) + if len(d) == max_len: + return d + if d == b"": + return data + data += d diff --git a/tests/test_framework/miradord.py b/tests/test_framework/miradord.py index 65f4154..ee9b753 100644 --- a/tests/test_framework/miradord.py +++ b/tests/test_framework/miradord.py @@ -76,13 +76,12 @@ def __init__( f.write("daemon = false\n") f.write(f"log_level = '{LOG_LEVEL}'\n") + f.write(f'listen = "127.0.0.1:{listen_port}"\n') f.write(f'stakeholder_noise_key = "{stk_noise_key.hex()}"\n') - f.write(f'coordinator_host = "127.0.0.1:{coordinator_port}"\n') - f.write(f'coordinator_noise_key = "{coordinator_noise_key}"\n') - f.write("coordinator_poll_seconds = 5\n") - - f.write(f'listen = "127.0.0.1:{listen_port}"\n') + f.write("[coordinator_config]\n") + f.write(f'host = "127.0.0.1:{coordinator_port}"\n') + f.write(f'noise_key = "{coordinator_noise_key.hex()}"\n') f.write("[scripts_config]\n") f.write(f'deposit_descriptor = "{deposit_desc}"\n') diff --git a/tests/test_framework/utils.py b/tests/test_framework/utils.py index e970374..bb97512 100644 --- a/tests/test_framework/utils.py +++ b/tests/test_framework/utils.py @@ -12,6 +12,7 @@ import threading import time +from nacl.public import PrivateKey as Curve25519Private TIMEOUT = int(os.getenv("TIMEOUT", 60)) EXECUTOR_WORKERS = int(os.getenv("EXECUTOR_WORKERS", 20)) @@ -63,6 +64,19 @@ DEPOSIT_ADDRESS = "bcrt1qgprmrfkz5mucga0ec046v0sf8yg2y4za99c0h26ew5ycfx64sgdsl0u2j3" +class NoiseKeypair: + """A pair of Curve25519 keys""" + + def __init__( + self, + privkey, + ): + self.privkey = privkey + self.pubkey = bytes( + Curve25519Private(privkey).public_key + ) + + def wait_for(success, timeout=TIMEOUT, debug_fn=None): """ Run success() either until it returns True, or until the timeout is reached. From 69710c61e0ff6c315588531ef9549cc4f9d9d3a5 Mon Sep 17 00:00:00 2001 From: edouard Date: Wed, 15 Jun 2022 18:12:17 +0200 Subject: [PATCH 4/7] Check spend tx against libbitcoinconsensus --- Cargo.lock | 1 + Cargo.toml | 1 + src/coordinator.rs | 4 +++- src/poller.rs | 35 +++++++++++++++++++++++++++++++++-- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1b36bb4..7c2aa38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -307,6 +307,7 @@ name = "miradord" version = "0.0.2" dependencies = [ "backtrace", + "bitcoinconsensus", "dirs", "fastrand", "fern", diff --git a/Cargo.toml b/Cargo.toml index a8e6cd3..b3495cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ authors = ["Antoine Poinsot "] edition = "2018" [dependencies] +bitcoinconsensus = "0.19.0-2" revault_tx = { version = "0.5", features = ["use-serde"] } revault_net = "0.3" diff --git a/src/coordinator.rs b/src/coordinator.rs index 8d5cb55..fe8d46e 100644 --- a/src/coordinator.rs +++ b/src/coordinator.rs @@ -63,7 +63,9 @@ impl CoordinatorClient { transport.send_req(&req) } - // Get Spend transaction spending the vault with the given deposit outpoint. + /// Get Spend transaction spending the vault with the given deposit outpoint. + /// Beware that the spend transaction may be invalid and needs to be verified against + /// libbitcoinconsensus. pub fn get_spend_transaction( &self, deposit_outpoint: OutPoint, diff --git a/src/poller.rs b/src/poller.rs index 488060e..eacbd55 100644 --- a/src/poller.rs +++ b/src/poller.rs @@ -18,7 +18,7 @@ use revault_tx::{ CancelTransaction, RevaultPresignedTransaction, RevaultTransaction, UnvaultTransaction, }, txins::{DepositTxIn, RevaultTxIn, UnvaultTxIn}, - txouts::DepositTxOut, + txouts::{DepositTxOut, RevaultTxOut}, }; use revault_net::noise::SecretKey as NoisePrivkey; @@ -400,7 +400,38 @@ fn check_for_unvault( let candidate_tx = if let Some(client) = coordinator_client { match client.get_spend_transaction(db_vault.deposit_outpoint.clone()) { - Ok(res) => res, + Ok(Some(tx)) => { + let spent_unvault_outpoint = unvault_txin.outpoint(); + if let Some(i) = tx + .input + .iter() + .position(|input| input.previous_output == spent_unvault_outpoint) + { + let txout = unvault_txin.txout().txout(); + if let Err(e) = bitcoinconsensus::verify( + &txout.script_pubkey.as_bytes(), + txout.value, + &encode::serialize(&tx), + i, + ) { + log::error!( + "Coordinator sent a suspicious tx {}, libbitcoinconsensus error: {:?}", + tx.txid(), + e + ); + None + } else { + Some(tx) + } + } else { + log::error!( + "Coordinator sent a suspicious tx {}, the transaction does not spend the vault", + tx.txid(), + ); + None + } + } + Ok(None) => None, Err(_e) => { // Because we do not trust the coordinator, we consider it refuses to deliver the // spend tx if a communication error happened. From 81fc75dd14b7f79348a446475477ce3c5c90ed2c Mon Sep 17 00:00:00 2001 From: edouard Date: Thu, 16 Jun 2022 16:52:56 +0200 Subject: [PATCH 5/7] Add plugin revault_no_spend.py --- tests/plugins/revault_no_spend.py | 27 ++++++++++ tests/test_framework/coordinator.py | 57 +++++++++++++++++++++ tests/test_plugins.py | 78 ++++++++++++++++++++++++++++- 3 files changed, 161 insertions(+), 1 deletion(-) create mode 100755 tests/plugins/revault_no_spend.py diff --git a/tests/plugins/revault_no_spend.py b/tests/plugins/revault_no_spend.py new file mode 100755 index 0000000..66a988a --- /dev/null +++ b/tests/plugins/revault_no_spend.py @@ -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() diff --git a/tests/test_framework/coordinator.py b/tests/test_framework/coordinator.py index 54fc1a7..5f92e00 100644 --- a/tests/test_framework/coordinator.py +++ b/tests/test_framework/coordinator.py @@ -1,6 +1,7 @@ import cryptography import json import os +import random import select import socket import threading @@ -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. @@ -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"] diff --git a/tests/test_plugins.py b/tests/test_plugins.py index f243a7b..7109d76 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -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 @@ -119,6 +120,81 @@ 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}]) + + 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.""" From 1375fc5e94c805c8471fd10e6fcda915f815c3af Mon Sep 17 00:00:00 2001 From: edouard Date: Wed, 29 Jun 2022 17:42:58 +0200 Subject: [PATCH 6/7] Add spend transaction caching in poller --- src/poller.rs | 73 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/src/poller.rs b/src/poller.rs index eacbd55..b0b6b76 100644 --- a/src/poller.rs +++ b/src/poller.rs @@ -12,13 +12,13 @@ use crate::{ plugins::{NewBlockInfo, VaultInfo}, }; use revault_tx::{ - bitcoin::{consensus::encode, secp256k1, Amount, OutPoint}, + bitcoin::{consensus::encode, secp256k1, Amount, OutPoint, Transaction}, scripts::{DerivedCpfpDescriptor, DerivedDepositDescriptor, DerivedUnvaultDescriptor}, transactions::{ CancelTransaction, RevaultPresignedTransaction, RevaultTransaction, UnvaultTransaction, }, txins::{DepositTxIn, RevaultTxIn, UnvaultTxIn}, - txouts::{DepositTxOut, RevaultTxOut}, + txouts::{DepositTxOut, RevaultTxOut, UnvaultTxOut}, }; use revault_net::noise::SecretKey as NoisePrivkey; @@ -365,6 +365,8 @@ fn check_for_unvault( ) -> Result, PollerError> { let deleg_vaults = db_blank_vaults(db_path)?; let mut new_attempts = vec![]; + // Map of the spend transactions with their input previous_output outpoints as keys. + let mut spend_cache = HashMap::::new(); for mut db_vault in deleg_vaults { let (deposit_desc, unvault_desc, cpfp_desc) = descriptors(secp, config, &db_vault); @@ -398,45 +400,49 @@ fn check_for_unvault( // If needed to be canceled it will be marked as such when plugins tell us so. db_updates.new_unvaulted.insert(db_vault.id, db_vault); + let unvault_txin_outpoint = unvault_txin.outpoint(); let candidate_tx = if let Some(client) = coordinator_client { - match client.get_spend_transaction(db_vault.deposit_outpoint.clone()) { - Ok(Some(tx)) => { - let spent_unvault_outpoint = unvault_txin.outpoint(); - if let Some(i) = tx - .input - .iter() - .position(|input| input.previous_output == spent_unvault_outpoint) - { - let txout = unvault_txin.txout().txout(); - if let Err(e) = bitcoinconsensus::verify( - &txout.script_pubkey.as_bytes(), - txout.value, - &encode::serialize(&tx), - i, - ) { - log::error!( + if let Some(tx) = spend_cache.get(&unvault_txin_outpoint) { + Some(tx.clone()) + } else { + match client.get_spend_transaction(db_vault.deposit_outpoint) { + Ok(Some(tx)) => { + if let Some(i) = tx + .input + .iter() + .position(|input| input.previous_output == unvault_txin_outpoint) + { + let txout = unvault_txin.txout().txout(); + if let Err(e) = bitcoinconsensus::verify( + &txout.script_pubkey.as_bytes(), + txout.value, + &encode::serialize(&tx), + i, + ) { + log::error!( "Coordinator sent a suspicious tx {}, libbitcoinconsensus error: {:?}", tx.txid(), e ); - None + None + } else { + Some(tx) + } } else { - Some(tx) - } - } else { - log::error!( + log::error!( "Coordinator sent a suspicious tx {}, the transaction does not spend the vault", tx.txid(), ); + None + } + } + Ok(None) => None, + Err(_e) => { + // Because we do not trust the coordinator, we consider it refuses to deliver the + // spend tx if a communication error happened. None } } - Ok(None) => None, - Err(_e) => { - // Because we do not trust the coordinator, we consider it refuses to deliver the - // spend tx if a communication error happened. - None - } } } else { // No coordinator configuration was found in the config @@ -444,6 +450,15 @@ fn check_for_unvault( None }; + // Populate the spend cache + if spend_cache.get(&unvault_txin_outpoint).is_none() { + if let Some(tx) = candidate_tx.as_ref() { + for input in &tx.input { + spend_cache.insert(input.previous_output, tx.clone()); + } + } + } + let vault_info = VaultInfo { value: db_vault.amount, deposit_outpoint: db_vault.deposit_outpoint, From e819759516b8e2596602ef194f3c2d156facf3c1 Mon Sep 17 00:00:00 2001 From: edouard Date: Tue, 5 Jul 2022 18:46:21 +0200 Subject: [PATCH 7/7] tests: Add whitelist plugin --- tests/plugins/whitelist/Cargo.lock | 137 ++++++++++++++++++++++++++++ tests/plugins/whitelist/Cargo.toml | 9 ++ tests/plugins/whitelist/src/main.rs | 72 +++++++++++++++ tests/requirements.txt | 1 + tests/test_framework/utils.py | 20 ++-- tests/test_plugins.py | 107 +++++++++++++++++++++- 6 files changed, 337 insertions(+), 9 deletions(-) create mode 100644 tests/plugins/whitelist/Cargo.lock create mode 100644 tests/plugins/whitelist/Cargo.toml create mode 100644 tests/plugins/whitelist/src/main.rs diff --git a/tests/plugins/whitelist/Cargo.lock b/tests/plugins/whitelist/Cargo.lock new file mode 100644 index 0000000..2e2d15b --- /dev/null +++ b/tests/plugins/whitelist/Cargo.lock @@ -0,0 +1,137 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bech32" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" + +[[package]] +name = "bitcoin" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05bba324e6baf655b882df672453dbbc527bc938cadd27750ae510aaccc3a66a" +dependencies = [ + "bech32", + "bitcoin_hashes", + "secp256k1", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006cc91e1a1d99819bc5b8214be3555c1f0611b169f527a1fdc54ed1f2b745b0" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "itoa" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" + +[[package]] +name = "proc-macro2" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" + +[[package]] +name = "secp256k1" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26947345339603ae8395f68e2f3d85a6b0a8ddfe6315818e80b8504415099db0" +dependencies = [ + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152e20a0fd0519390fc43ab404663af8a0b794273d2a91d60ad4a39f13ffe110" +dependencies = [ + "cc", +] + +[[package]] +name = "serde" +version = "1.0.138" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1578c6245786b9d168c5447eeacfb96856573ca56c9d68fdcf394be134882a47" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.138" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023e9b1467aef8a10fb88f25611870ada9800ef7e22afce356bb0d2387b6f27c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" + +[[package]] +name = "whitelist" +version = "0.1.0" +dependencies = [ + "bitcoin", + "serde", + "serde_json", +] diff --git a/tests/plugins/whitelist/Cargo.toml b/tests/plugins/whitelist/Cargo.toml new file mode 100644 index 0000000..478bbca --- /dev/null +++ b/tests/plugins/whitelist/Cargo.toml @@ -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" diff --git a/tests/plugins/whitelist/src/main.rs b/tests/plugins/whitelist/src/main.rs new file mode 100644 index 0000000..fc3922c --- /dev/null +++ b/tests/plugins/whitelist/src/main.rs @@ -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> { + 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