diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..0e1fba54 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +services: + - docker +language: rust +rust: + - stable +env: + - BITCOINVERSION=0.20.0 + +script: + - cargo build --verbose + - cargo test --verbose + # Integration test + - ./scripts/start_node.sh + # Bitcoind takes forever to be ready on travis + - sleep 5 + - (cd integration_test && cargo run) + - ./scripts/stop_node.sh + diff --git a/Cargo.toml b/Cargo.toml index 10eda5d1..96d6b68f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,4 +2,5 @@ members = [ "dlc", + "integration_test", ] diff --git a/integration_test/Cargo.toml b/integration_test/Cargo.toml new file mode 100644 index 00000000..c01b9096 --- /dev/null +++ b/integration_test/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "integration_test" +version = "0.0.1" +authors = ["Crypto Garage"] + +[dependencies] +bitcoin = {version="0.23.0-adaptor.0", git = "https://github.com/p2pderivatives/rust-bitcoin", branch = "ecdsa-adaptor", package="bitcoin", features = ["bitcoinconsensus"]} +secp256k1 = {version="0.19.0-adaptor.0", git = "https://github.com/p2pderivatives/rust-secp256k1", branch = "ecdsa-adaptor", package="secp256k1", features=["rand-std"]} +bitcoin_hashes = "0.7.6" +bitcoincore-rpc = {version="0.11.0-adaptor.0", git = "https://github.com/p2pderivatives/rust-bitcoincore-rpc", branch = "ecdsa-adaptor"} +bitcoincore-rpc-json = {version="0.11.0-adaptor.0", git = "https://github.com/p2pderivatives/rust-bitcoincore-rpc", branch = "ecdsa-adaptor"} +dlc = {path = "../dlc"} diff --git a/integration_test/src/main.rs b/integration_test/src/main.rs new file mode 100644 index 00000000..646ad7da --- /dev/null +++ b/integration_test/src/main.rs @@ -0,0 +1,310 @@ +extern crate bitcoin; +extern crate bitcoin_hashes; +extern crate bitcoincore_rpc; +extern crate bitcoincore_rpc_json; +extern crate dlc; +extern crate secp256k1; + +use bitcoincore_rpc::{Auth, Client, RpcApi}; +use bitcoincore_rpc_json::AddressType; + +use bitcoin::network::constants::Network; +use bitcoin::{Address, OutPoint, PrivateKey, SigHashType}; +use bitcoin_hashes::{sha256, Hash}; +use dlc::{PartyParams, Payout, SegwitInput, TxInputInfo}; +use secp256k1::{schnorrsig::SchnorrPublicKey, Message, PublicKey, SecretKey}; + +const LOCALPARTY: &str = "alice"; +const REMOTEPARTY: &str = "bob"; +const SINK: &str = "sink"; + +const RPCBASE: &str = "http://localhost:18443"; + +const BTC_TO_SAT: u64 = 100000000; +const PARTY_COLLATERAL: u64 = 1 * BTC_TO_SAT; +const WIN: &str = "WIN"; +const LOSE: &str = "LOSE"; + +const FUND_LOCK_TIME: u32 = 1000; +const CET_LOCK_TIME: u32 = FUND_LOCK_TIME + 1000; +const REFUND_LOCK_TIME: u32 = CET_LOCK_TIME + 1000; + +fn outcomes() -> Vec { + vec![ + Payout { + local: 2 * BTC_TO_SAT, + remote: 0, + }, + Payout { + local: 0, + remote: 2 * BTC_TO_SAT, + }, + ] +} + +fn hashed_msg() -> Vec { + [WIN, LOSE] + .iter() + .map(|m| { + Message::from_slice(sha256::Hash::hash(&m.as_bytes()).into_inner()[..].as_ref()) + .unwrap() + }) + .collect() +} + +fn get_new_wallet_rpc(default_rpc: &Client, wallet_name: &str, auth: Auth) -> Client { + default_rpc.create_wallet(wallet_name, Some(false)).unwrap(); + let rpc_url = format!("{}{}{}", RPCBASE, "/wallet/", wallet_name); + Client::new(rpc_url, auth).unwrap() +} + +fn init() -> (Client, Client, Box ()>) { + let auth = Auth::UserPass( + "testuser".to_string(), + "lq6zequb-gYTdF2_ZEUtr8ywTXzLYtknzWU4nV8uVoo=".to_string(), + ); + let rpc = Client::new(RPCBASE.to_string(), auth.clone()).unwrap(); + + let local_rpc = get_new_wallet_rpc(&rpc, LOCALPARTY, auth.clone()); + let remote_rpc = get_new_wallet_rpc(&rpc, REMOTEPARTY, auth.clone()); + let sink_rpc = get_new_wallet_rpc(&rpc, SINK, auth.clone()); + + let local_address = local_rpc + .get_new_address(None, Some(AddressType::Bech32)) + .unwrap(); + let remote_address = remote_rpc + .get_new_address(None, Some(AddressType::Bech32)) + .unwrap(); + let sink_address = sink_rpc + .get_new_address(None, Some(AddressType::Bech32)) + .unwrap(); + + sink_rpc.generate_to_address(1, &local_address).unwrap(); + sink_rpc.generate_to_address(1, &remote_address).unwrap(); + sink_rpc.generate_to_address(100, &sink_address).unwrap(); + + ( + local_rpc, + remote_rpc, + Box::new(move |nb_blocks| { + sink_rpc + .generate_to_address(nb_blocks, &sink_address) + .unwrap(); + }), + ) +} + +fn generate_dlc_parameters<'a, C: secp256k1::Signing>( + rpc: &Client, + secp: &secp256k1::Secp256k1, + collateral: u64, +) -> (PartyParams, SecretKey, SecretKey) { + let change_address = rpc + .get_new_address(None, Some(AddressType::Bech32)) + .unwrap(); + let final_address = rpc + .get_new_address(None, Some(AddressType::Bech32)) + .unwrap(); + let fund_address = rpc + .get_new_address(None, Some(AddressType::Bech32)) + .unwrap(); + let fund_priv_key = rpc.dump_private_key(&fund_address).unwrap().key; + let mut utxos = rpc + .list_unspent(None, None, None, Some(false), None) + .unwrap(); + let utxo = utxos.pop().unwrap(); + let input_sk = { + let address = utxo.address.clone().unwrap(); + let privkey = rpc.dump_private_key(&address).unwrap(); + privkey.key + }; + ( + PartyParams { + fund_pubkey: PublicKey::from_secret_key(secp, &fund_priv_key), + change_address: change_address, + final_address: final_address, + inputs: vec![TxInputInfo { + outpoint: OutPoint { + txid: utxo.txid, + vout: utxo.vout, + }, + input_type: SegwitInput::P2WPKH, + }], + input_amount: utxo.amount.as_sat(), + collateral, + }, + fund_priv_key, + input_sk, + ) +} + +fn main() { + let secp = secp256k1::Secp256k1::new(); + let oracle_sk = secp256k1::SecretKey::new(&mut secp256k1::rand::thread_rng()); + let oracle_pubkey = SchnorrPublicKey::from_secret_key(&secp, &oracle_sk); + let oracle_k_value = PrivateKey { + key: secp256k1::SecretKey::new(&mut secp256k1::rand::thread_rng()), + compressed: true, + network: Network::Regtest, + }; + let oracle_r_value = SchnorrPublicKey::from_secret_key(&secp, &oracle_k_value.key); + let (local_rpc, remote_rpc, generate_blocks) = init(); + + let (local_params, local_fund_sk, local_input_sk) = + generate_dlc_parameters(&local_rpc, &secp, PARTY_COLLATERAL); + let (remote_params, remote_fund_sk, remote_input_sk) = + generate_dlc_parameters(&remote_rpc, &secp, PARTY_COLLATERAL); + + let mut dlc_txs = dlc::create_dlc_transactions( + &local_params, + &remote_params, + &outcomes(), + REFUND_LOCK_TIME, + 2, + FUND_LOCK_TIME, + CET_LOCK_TIME, + ) + .expect("Error creating dlc transactions."); + + let funding_script_pubkey = + dlc::make_funding_redeemscript(&local_params.fund_pubkey, &remote_params.fund_pubkey); + let fund_output_value = dlc_txs.fund.output[0].value; + let msgs = hashed_msg(); + let remote_sig = { + let mut local_cets_sigs = dlc_txs.cets.iter().zip(&msgs).map(|z| { + ( + z, + dlc::create_cet_adaptor_sig( + &secp, + &z.0, + &oracle_pubkey, + &oracle_r_value, + &local_fund_sk, + &funding_script_pubkey, + fund_output_value, + z.1, + ) + .expect("Error creating adaptor sig"), + ) + }); + let mut remote_cets_sigs = dlc_txs.cets.iter().zip(&msgs).map(|z| { + ( + z, + dlc::create_cet_adaptor_sig( + &secp, + &z.0, + &oracle_pubkey, + &oracle_r_value, + &remote_fund_sk, + &funding_script_pubkey, + fund_output_value, + z.1, + ) + .expect("Error creating adaptor sig"), + ) + }); + + assert!(local_cets_sigs.all(|z| dlc::verify_cet_adaptor_sig( + &secp, + &(z.1).0, + &(z.1).1, + &(z.0).0, + &oracle_pubkey, + &oracle_r_value, + &local_params.fund_pubkey, + &funding_script_pubkey, + fund_output_value, + &(z.0).1 + ) + .is_ok())); + + assert!(remote_cets_sigs + .clone() + .all(|z| dlc::verify_cet_adaptor_sig( + &secp, + &(z.1).0, + &(z.1).1, + &(z.0).0, + &oracle_pubkey, + &oracle_r_value, + &remote_params.fund_pubkey, + &funding_script_pubkey, + fund_output_value, + &(z.0).1 + ) + .is_ok())); + remote_cets_sigs.nth(0).unwrap().1 + }; + + let oracle_sig = secp + .schnorr_sign_with_nonce(&msgs[0], &oracle_sk, &oracle_k_value.key) + .expect("Error creating oracle signature."); + + assert!(dlc::sign_cet( + &secp, + &mut dlc_txs.cets[0], + &remote_sig.0, + &oracle_sig, + &local_fund_sk, + &funding_script_pubkey, + fund_output_value, + true, + ) + .is_ok()); + + dlc::util::sign_p2wpkh_input( + &secp, + &local_input_sk, + &mut dlc_txs.fund, + 0, + &Address::p2pkh( + &::bitcoin::PublicKey { + key: PublicKey::from_secret_key(&secp, &local_input_sk), + compressed: true, + }, + Network::Regtest, + ) + .script_pubkey(), + SigHashType::All, + local_params.input_amount, + ); + + dlc::util::sign_p2wpkh_input( + &secp, + &remote_input_sk, + &mut dlc_txs.fund, + 1, + &Address::p2pkh( + &::bitcoin::PublicKey { + key: PublicKey::from_secret_key(&secp, &remote_input_sk), + compressed: true, + }, + Network::Regtest, + ) + .script_pubkey(), + SigHashType::All, + remote_params.input_amount, + ); + + let go_to_height = |height: u64| { + let block_count = local_rpc.get_block_count().unwrap(); + + generate_blocks(height - block_count); + }; + + // Should not be able to broadcast before fund lock time + assert!(local_rpc.send_raw_transaction(&dlc_txs.fund).is_err()); + + go_to_height(FUND_LOCK_TIME as u64); + + assert!(local_rpc.send_raw_transaction(&dlc_txs.fund).is_ok()); + + generate_blocks(1); + + // Should not be able to broadcast before cet lock time + assert!(local_rpc.send_raw_transaction(&dlc_txs.cets[0]).is_err()); + + go_to_height(CET_LOCK_TIME as u64); + + assert!(local_rpc.send_raw_transaction(&dlc_txs.cets[0]).is_ok()); +} diff --git a/scripts/start_node.sh b/scripts/start_node.sh new file mode 100755 index 00000000..951a145b --- /dev/null +++ b/scripts/start_node.sh @@ -0,0 +1,7 @@ +: "${BITCOINVERSION:=0.20.0}" +docker run --rm -d -p 18443:18443 --name bitcoin-node ruimarinho/bitcoin-core:$BITCOINVERSION \ + -regtest=1 \ + -rpcallowip=0.0.0/0 \ + -rpcbind=0.0.0.0 \ + -rpcauth='testuser:ea8070e0acccb49670309dd6c7812e16$2a3487173f9f6b603d43a70e6ccb0aa671a16dbee1cf86b098e77532d2515370' \ + -addresstype=bech32 diff --git a/scripts/stop_node.sh b/scripts/stop_node.sh new file mode 100755 index 00000000..6967210b --- /dev/null +++ b/scripts/stop_node.sh @@ -0,0 +1 @@ +docker stop bitcoin-node