From d524bd7d07371880d6b03d940dc15f9fa8efa0fa Mon Sep 17 00:00:00 2001 From: akildemir Date: Mon, 11 Dec 2023 12:28:35 +0300 Subject: [PATCH 1/7] add input script check --- processor/src/networks/bitcoin.rs | 132 +++++++++++++++++++++--------- 1 file changed, 92 insertions(+), 40 deletions(-) diff --git a/processor/src/networks/bitcoin.rs b/processor/src/networks/bitcoin.rs index 65908aa38..c0c378c1b 100644 --- a/processor/src/networks/bitcoin.rs +++ b/processor/src/networks/bitcoin.rs @@ -21,7 +21,9 @@ use bitcoin_serai::{ consensus::{Encodable, Decodable}, script::Instruction, address::{NetworkChecked, Address as BAddress}, - Transaction, Block, Network as BNetwork, + Transaction, Block, Network as BNetwork, ScriptBuf, + opcodes::all::{OP_SHA256, OP_EQUAL}, + blockdata::transaction::{TxIn, TxOut}, }, wallet::{ tweak_keys, address_payload, ReceivedOutput, Scanner, TransactionError, @@ -37,7 +39,7 @@ use bitcoin_serai::bitcoin::{ sighash::{EcdsaSighashType, SighashCache}, script::{PushBytesBuf, Builder}, absolute::LockTime, - Amount as BAmount, Sequence, Script, Witness, OutPoint, TxOut, TxIn, + Amount as BAmount, Sequence, Script, Witness, OutPoint, transaction::Version, }; @@ -447,6 +449,79 @@ impl Bitcoin { } } } + + // expected script has to start with SHA256 PUSH MSG_HASH OP_EQ .. + fn expected_script_pattern(&self, script: &ScriptBuf) -> Option { + let mut ins = script.instructions(); + + // first item should be SHA256 code + if ins.next()?.ok()?.opcode()? != OP_SHA256 { + return Some(false); + } + + // next should be a data push + ins.next()?.ok()?.push_bytes()?; + + // next should be a equality check + if ins.next()?.ok()?.opcode()? != OP_EQUAL { + return Some(false); + } + + Some(true) + } + + async fn spent_output_of(&self, input: &TxIn) -> TxOut { + let mut spent_tx = input.previous_output.txid.as_raw_hash().to_byte_array(); + spent_tx.reverse(); + let mut tx; + while { + tx = self.get_transaction(&spent_tx).await; + tx.is_err() + } { + log::error!("couldn't get transaction from bitcoin node: {tx:?}"); + sleep(Duration::from_secs(5)).await; + } + tx.unwrap().output.swap_remove(usize::try_from(input.previous_output.vout).unwrap()) + } + + async fn extract_serai_data(&self, tx: &Transaction) -> Vec { + // check outputs + let mut data = (|| { + for output in &tx.output { + if output.script_pubkey.is_op_return() { + match output.script_pubkey.instructions_minimal().last() { + Some(Ok(Instruction::PushBytes(data))) => return data.as_bytes().to_vec(), + _ => continue, + } + } + } + vec![] + })(); + + // check inputs + if data.is_empty() { + for input in &tx.input { + if self.spent_output_of(input).await.script_pubkey.is_p2wsh() { + let witness = input.witness.to_vec(); + let redeem_script = ScriptBuf::from_bytes(witness[0].clone()); + if let Some(true) = self.expected_script_pattern(&redeem_script) { + data = witness[1].clone(); + break; + } + } + } + } + + data.truncate(MAX_DATA_LEN.try_into().unwrap()); + data + } + + async fn extract_origin(&self, tx: &Transaction) -> Option
{ + let spent_output = self.spent_output_of(&tx.input[0]).await; + BAddress::from_script(&spent_output.script_pubkey, BNetwork::Bitcoin) + .ok() + .and_then(Address::new) + } } #[async_trait] @@ -571,47 +646,24 @@ impl Network for Bitcoin { let offset_repr_ref: &[u8] = offset_repr.as_ref(); let kind = kinds[offset_repr_ref]; - let mut data = if kind == OutputType::External { - (|| { - for output in &tx.output { - if output.script_pubkey.is_op_return() { - match output.script_pubkey.instructions_minimal().last() { - Some(Ok(Instruction::PushBytes(data))) => return data.as_bytes().to_vec(), - _ => continue, - } - } - } - vec![] - })() - } else { - vec![] - }; - data.truncate(MAX_DATA_LEN.try_into().unwrap()); - - let presumed_origin = { - let spent_output = tx.input[0].previous_output; - let mut spent_tx = spent_output.txid.as_raw_hash().to_byte_array(); - spent_tx.reverse(); - let spent_output = { - let mut tx; - while { - tx = self.get_transaction(&spent_tx).await; - tx.is_err() - } { - log::error!("couldn't get transaction from bitcoin node: {tx:?}"); - sleep(Duration::from_secs(5)).await; - } - tx.unwrap().output.swap_remove(usize::try_from(spent_output.vout).unwrap()) - }; - BAddress::from_script(&spent_output.script_pubkey, BNetwork::Bitcoin) - .ok() - .and_then(Address::new) - }; - - let output = Output { kind, presumed_origin, output, data }; + let output = Output { kind, presumed_origin: None, output, data: vec![] }; assert_eq!(output.tx_id(), tx.id()); outputs.push(output); } + + if outputs.is_empty() { + continue; + } + + // populate the rest of the outputs + let presumed_origin = self.extract_origin(tx).await; + let data = self.extract_serai_data(tx).await; + for output in &mut outputs { + if output.kind == OutputType::External { + output.data = data.clone(); + } + output.presumed_origin = presumed_origin.clone(); + } } outputs From 706497eafee6031ee4aeb3743b0f159db8c373d4 Mon Sep 17 00:00:00 2001 From: akildemir Date: Tue, 12 Dec 2023 15:55:55 +0300 Subject: [PATCH 2/7] add test --- processor/src/networks/bitcoin.rs | 4 +- processor/src/tests/literal/mod.rs | 201 ++++++++++++++++++++++++++++- processor/src/tests/scanner.rs | 57 ++++---- 3 files changed, 236 insertions(+), 26 deletions(-) diff --git a/processor/src/networks/bitcoin.rs b/processor/src/networks/bitcoin.rs index c0c378c1b..7b37f44cc 100644 --- a/processor/src/networks/bitcoin.rs +++ b/processor/src/networks/bitcoin.rs @@ -503,9 +503,9 @@ impl Bitcoin { for input in &tx.input { if self.spent_output_of(input).await.script_pubkey.is_p2wsh() { let witness = input.witness.to_vec(); - let redeem_script = ScriptBuf::from_bytes(witness[0].clone()); + let redeem_script = ScriptBuf::from_bytes(witness[1].clone()); if let Some(true) = self.expected_script_pattern(&redeem_script) { - data = witness[1].clone(); + data = witness[0].clone(); break; } } diff --git a/processor/src/tests/literal/mod.rs b/processor/src/tests/literal/mod.rs index 2974d040f..5351b04c1 100644 --- a/processor/src/tests/literal/mod.rs +++ b/processor/src/tests/literal/mod.rs @@ -6,7 +6,31 @@ use dockertest::{ #[cfg(feature = "bitcoin")] mod bitcoin { use super::*; - use crate::networks::{Network, Bitcoin}; + use crate::{networks::{Network, Bitcoin, Output, OutputType, Block}, tests::scanner::new_scanner, multisigs::scanner::ScannerEvent}; + use sp_application_crypto::Pair; + + use bitcoin_serai::bitcoin::{ + secp256k1::{SECP256K1, SecretKey, Message}, + PrivateKey, PublicKey, + hashes::{HashEngine, Hash, sha256::Hash as Sha256}, + sighash::{EcdsaSighashType, SighashCache}, + script::{PushBytesBuf, Builder}, + absolute::LockTime, + Amount as BAmount, Sequence, Script, Witness, OutPoint, + address::Address as BAddress, + transaction::{Version, Transaction, TxIn, TxOut}, + Network as BNetwork, ScriptBuf, + opcodes::all::{OP_SHA256, OP_EQUAL}, + }; + + use frost::Participant; + use rand_core::OsRng; + use scale::{Encode, Decode}; + use serai_client::{ + in_instructions::primitives::{Shorthand, RefundableInInstruction, InInstruction}, primitives::insecure_pair_from_name, + }; + use serai_db::MemDb; + use tokio::time::{timeout, Duration}; #[test] fn test_dust_constant() { @@ -19,6 +43,181 @@ mod bitcoin { check::= bitcoin_serai::wallet::DUST }>>(); } + #[test] + fn test_receive_data_from_input() { + let docker = spawn_bitcoin(); + docker.run(|ops| async move { + let btc = bitcoin(&ops).await; + + // generate a musig address to receive the funds + let mut keys = + frost::tests::key_gen::<_, ::Curve>(&mut OsRng).remove(&Participant::new(1).unwrap()).unwrap(); + ::tweak_keys(&mut keys); + let group_key = keys.group_key(); + let address = ::external_address(group_key); + + // btc key pair to spend from. + let secret_key = SecretKey::new(&mut rand_core::OsRng); + let private_key = PrivateKey::new(secret_key, BNetwork::Regtest); + let public_key = PublicKey::from_private_key(SECP256K1, &private_key); + let main_addr = BAddress::p2pkh(&public_key, BNetwork::Regtest); + + // make some funds to spent + let new_block = btc.get_latest_block_number().await.unwrap() + 1; + btc + .rpc + .rpc_call::>("generatetoaddress", serde_json::json!([1, main_addr])) + .await + .unwrap(); + + for _ in 0 .. 100 { + btc.mine_block().await; + } + + // create a scanner + let db = MemDb::new(); + let mut scanner = new_scanner(&btc, &db, group_key).await; + + // make a transfer instruction & hash it. + let serai_address = insecure_pair_from_name("dadadadada").public(); + let message = + Shorthand::transfer(None, serai_address.into()).encode(); + let mut data = Sha256::engine(); + data.input(&message); + + // make the output script SHA256 PUSH MSG_HASH OP_EQ + let script = ScriptBuf::builder() + .push_opcode(OP_SHA256) + .push_slice(Sha256::from_engine(data).as_byte_array()) + .push_opcode(OP_EQUAL); + + let tx = btc.get_block(new_block).await.unwrap().txdata.swap_remove(0); + let mut tx = Transaction { + version: Version(2), + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint { txid: tx.txid(), vout: 0 }, + script_sig: Script::new().into(), + sequence: Sequence(u32::MAX), + witness: Witness::default(), + }], + output: vec![ + TxOut { + value: tx.output[0].value - + BAmount::from_sat(bitcoin_serai::wallet::DUST) - + BAmount::from_sat(10000), + script_pubkey: main_addr.script_pubkey(), + }, + TxOut { + value: BAmount::from_sat(bitcoin_serai::wallet::DUST), + script_pubkey: ScriptBuf::new_p2wsh(&script.as_script().wscript_hash()), + }, + ], + }; + // sign the input + let mut der = SECP256K1 + .sign_ecdsa_low_r( + &Message::from( + SighashCache::new(&tx) + .legacy_signature_hash(0, &main_addr.script_pubkey(), EcdsaSighashType::All.to_u32()) + .unwrap() + .to_raw_hash(), + ), + &private_key.inner, + ) + .serialize_der() + .to_vec(); + der.push(1); + tx.input[0].script_sig = Builder::new() + .push_slice(PushBytesBuf::try_from(der).unwrap()) + .push_key(&public_key) + .into_script(); + + // send it + btc.rpc.send_raw_transaction(&tx).await.unwrap(); + + // witness script + let mut witness = Witness::new(); + witness.push(message); + witness.push(script.as_script()); + + // make another tx that spends both outputs + let mut tx = Transaction { + version: Version(2), + lock_time: LockTime::ZERO, + input: vec![ + TxIn { + previous_output: OutPoint { txid: tx.txid(), vout: 0 }, + script_sig: Script::new().into(), + sequence: Sequence(u32::MAX), + witness: Witness::default(), + }, + TxIn { + previous_output: OutPoint { txid: tx.txid(), vout: 1 }, + script_sig: Script::new().into(), + sequence: Sequence(u32::MAX), + witness: witness, + }, + ], + output: vec![TxOut { + value: tx.output[0].value + tx.output[1].value - BAmount::from_sat(10000), + script_pubkey: address.0.script_pubkey(), + }], + }; + + // sign the first input + let mut der = SECP256K1 + .sign_ecdsa_low_r( + &Message::from( + SighashCache::new(&tx) + .legacy_signature_hash(0, &main_addr.script_pubkey(), EcdsaSighashType::All.to_u32()) + .unwrap() + .to_raw_hash(), + ), + &private_key.inner, + ) + .serialize_der() + .to_vec(); + der.push(1); + tx.input[0].script_sig = Builder::new() + .push_slice(PushBytesBuf::try_from(der).unwrap()) + .push_key(&public_key) + .into_script(); + + // send it + let block_number = btc.get_latest_block_number().await.unwrap() + 1; + btc.rpc.send_raw_transaction(&tx).await.unwrap(); + for _ in 0 .. ::CONFIRMATIONS { + btc.mine_block().await; + } + let tx_block = btc.get_block(block_number).await.unwrap(); + + // verify that scanner picked the output up with the right message + let outputs = match timeout(Duration::from_secs(30), scanner.events.recv()).await.unwrap().unwrap() { + ScannerEvent::Block { is_retirement_block, block, outputs } => { + scanner.multisig_completed.send(false).unwrap(); + assert!(!is_retirement_block); + assert_eq!(block, tx_block.id()); + assert_eq!(outputs.len(), 1); + assert_eq!(outputs[0].kind(), OutputType::External); + outputs + } + ScannerEvent::Completed(_, _, _, _) => { + panic!("unexpectedly got eventuality completion"); + } + }; + + let mut data = outputs[0].data(); + assert!(!data.is_empty()); + let Ok(shorthand) = Shorthand::decode(&mut data) else { panic!("can't decode data") }; + let Ok(instruction) = RefundableInInstruction::try_from(shorthand) else { panic!("can't decode ins") }; + match instruction.instruction { + InInstruction::Transfer(address) => assert_eq!(address, serai_address.into()), + _ => panic!("wrong ins") + } + }); + } + fn spawn_bitcoin() -> DockerTest { serai_docker_tests::build("bitcoin".to_string()); diff --git a/processor/src/tests/scanner.rs b/processor/src/tests/scanner.rs index ef5b572b2..bbb08e4a1 100644 --- a/processor/src/tests/scanner.rs +++ b/processor/src/tests/scanner.rs @@ -1,6 +1,7 @@ use core::time::Duration; use std::sync::Arc; +use ciphersuite::Ciphersuite; use rand_core::OsRng; use frost::{Participant, tests::key_gen}; @@ -14,6 +15,31 @@ use crate::{ multisigs::scanner::{ScannerEvent, Scanner, ScannerHandle}, }; +pub async fn new_scanner( + network: &N, + db: &D, + group_key: ::G, +) -> ScannerHandle { + let first = Arc::new(Mutex::new(true)); + let activation_number = network.get_latest_block_number().await.unwrap(); + let mut db = db.clone(); + let (mut scanner, current_keys) = Scanner::new(network.clone(), db.clone()); + let mut first = first.lock().await; + if *first { + assert!(current_keys.is_empty()); + let mut txn = db.txn(); + scanner.register_key(&mut txn, activation_number, group_key).await; + txn.commit(); + for _ in 0 .. N::CONFIRMATIONS { + network.mine_block().await; + } + *first = false; + } else { + assert_eq!(current_keys.len(), 1); + } + scanner +} + pub async fn test_scanner(network: N) { let mut keys = frost::tests::key_gen::<_, N::Curve>(&mut OsRng).remove(&Participant::new(1).unwrap()).unwrap(); @@ -25,28 +51,8 @@ pub async fn test_scanner(network: N) { network.mine_block().await; } - let first = Arc::new(Mutex::new(true)); - let activation_number = network.get_latest_block_number().await.unwrap(); let db = MemDb::new(); - let new_scanner = || async { - let mut db = db.clone(); - let (mut scanner, current_keys) = Scanner::new(network.clone(), db.clone()); - let mut first = first.lock().await; - if *first { - assert!(current_keys.is_empty()); - let mut txn = db.txn(); - scanner.register_key(&mut txn, activation_number, group_key).await; - txn.commit(); - for _ in 0 .. N::CONFIRMATIONS { - network.mine_block().await; - } - *first = false; - } else { - assert_eq!(current_keys.len(), 1); - } - scanner - }; - let scanner = new_scanner().await; + let scanner = new_scanner(&network, &db, group_key).await; // Receive funds let block = network.test_send(N::external_address(keys.group_key())).await; @@ -73,7 +79,7 @@ pub async fn test_scanner(network: N) { let (mut scanner, outputs) = verify_event(scanner).await; // Create a new scanner off the current DB and verify it re-emits the above events - verify_event(new_scanner().await).await; + verify_event(new_scanner(&network, &db, group_key).await).await; // Acknowledge the block let mut cloned_db = db.clone(); @@ -86,7 +92,12 @@ pub async fn test_scanner(network: N) { assert!(timeout(Duration::from_secs(30), scanner.events.recv()).await.is_err()); // Create a new scanner off the current DB and make sure it also does nothing - assert!(timeout(Duration::from_secs(30), new_scanner().await.events.recv()).await.is_err()); + assert!(timeout( + Duration::from_secs(30), + new_scanner(&network, &db, group_key).await.events.recv() + ) + .await + .is_err()); } pub async fn test_no_deadlock_in_multisig_completed(network: N) { From d93848c0bd1304dcbe158822da2819a82f41df45 Mon Sep 17 00:00:00 2001 From: akildemir Date: Tue, 12 Dec 2023 20:30:23 +0300 Subject: [PATCH 3/7] optimizations --- processor/src/networks/bitcoin.rs | 52 ++++++------ processor/src/tests/literal/mod.rs | 129 ++++++++++------------------- 2 files changed, 74 insertions(+), 107 deletions(-) diff --git a/processor/src/networks/bitcoin.rs b/processor/src/networks/bitcoin.rs index 7b37f44cc..daddaa028 100644 --- a/processor/src/networks/bitcoin.rs +++ b/processor/src/networks/bitcoin.rs @@ -37,7 +37,7 @@ use bitcoin_serai::bitcoin::{ secp256k1::{SECP256K1, SecretKey, Message}, PrivateKey, PublicKey, sighash::{EcdsaSighashType, SighashCache}, - script::{PushBytesBuf, Builder}, + script::PushBytesBuf, absolute::LockTime, Amount as BAmount, Sequence, Script, Witness, OutPoint, transaction::Version, @@ -396,6 +396,31 @@ impl Bitcoin { Ok(Fee(fee.max(1))) } + #[cfg(test)] + pub fn sign_btc_input_for_p2pkh(tx: &Transaction, private_key: &PrivateKey) -> ScriptBuf { + let public_key = PublicKey::from_private_key(SECP256K1, private_key); + let main_addr = BAddress::p2pkh(&public_key, BNetwork::Regtest); + + let mut der = SECP256K1 + .sign_ecdsa_low_r( + &Message::from( + SighashCache::new(tx) + .legacy_signature_hash(0, &main_addr.script_pubkey(), EcdsaSighashType::All.to_u32()) + .unwrap() + .to_raw_hash(), + ), + &private_key.inner, + ) + .serialize_der() + .to_vec(); + der.push(1); + + ScriptBuf::builder() + .push_slice(PushBytesBuf::try_from(der).unwrap()) + .push_key(&public_key) + .into_script() + } + async fn make_signable_transaction( &self, block_number: usize, @@ -826,14 +851,10 @@ impl Network for Bitcoin { let new_block = self.get_latest_block_number().await.unwrap() + 1; self .rpc - .rpc_call::>("generatetoaddress", serde_json::json!([1, main_addr])) + .rpc_call::>("generatetoaddress", serde_json::json!([100, main_addr])) .await .unwrap(); - for _ in 0 .. 100 { - self.mine_block().await; - } - let tx = self.get_block(new_block).await.unwrap().txdata.swap_remove(0); let mut tx = Transaction { version: Version(2), @@ -849,24 +870,7 @@ impl Network for Bitcoin { script_pubkey: address.as_ref().script_pubkey(), }], }; - - let mut der = SECP256K1 - .sign_ecdsa_low_r( - &Message::from( - SighashCache::new(&tx) - .legacy_signature_hash(0, &main_addr.script_pubkey(), EcdsaSighashType::All.to_u32()) - .unwrap() - .to_raw_hash(), - ), - &private_key.inner, - ) - .serialize_der() - .to_vec(); - der.push(1); - tx.input[0].script_sig = Builder::new() - .push_slice(PushBytesBuf::try_from(der).unwrap()) - .push_key(&public_key) - .into_script(); + tx.input[0].script_sig = Self::sign_btc_input_for_p2pkh(&tx, &private_key); let block = self.get_latest_block_number().await.unwrap() + 1; self.rpc.send_raw_transaction(&tx).await.unwrap(); diff --git a/processor/src/tests/literal/mod.rs b/processor/src/tests/literal/mod.rs index 5351b04c1..1707a73b2 100644 --- a/processor/src/tests/literal/mod.rs +++ b/processor/src/tests/literal/mod.rs @@ -6,15 +6,17 @@ use dockertest::{ #[cfg(feature = "bitcoin")] mod bitcoin { use super::*; - use crate::{networks::{Network, Bitcoin, Output, OutputType, Block}, tests::scanner::new_scanner, multisigs::scanner::ScannerEvent}; + use crate::{ + networks::{Network, Bitcoin, Output, OutputType, Block}, + tests::scanner::new_scanner, + multisigs::scanner::ScannerEvent, + }; use sp_application_crypto::Pair; use bitcoin_serai::bitcoin::{ - secp256k1::{SECP256K1, SecretKey, Message}, + secp256k1::{SECP256K1, SecretKey}, PrivateKey, PublicKey, hashes::{HashEngine, Hash, sha256::Hash as Sha256}, - sighash::{EcdsaSighashType, SighashCache}, - script::{PushBytesBuf, Builder}, absolute::LockTime, Amount as BAmount, Sequence, Script, Witness, OutPoint, address::Address as BAddress, @@ -27,7 +29,8 @@ mod bitcoin { use rand_core::OsRng; use scale::{Encode, Decode}; use serai_client::{ - in_instructions::primitives::{Shorthand, RefundableInInstruction, InInstruction}, primitives::insecure_pair_from_name, + in_instructions::primitives::{Shorthand, RefundableInInstruction, InInstruction}, + primitives::insecure_pair_from_name, }; use serai_db::MemDb; use tokio::time::{timeout, Duration}; @@ -50,15 +53,15 @@ mod bitcoin { let btc = bitcoin(&ops).await; // generate a musig address to receive the funds - let mut keys = - frost::tests::key_gen::<_, ::Curve>(&mut OsRng).remove(&Participant::new(1).unwrap()).unwrap(); + let mut keys = frost::tests::key_gen::<_, ::Curve>(&mut OsRng) + .remove(&Participant::new(1).unwrap()) + .unwrap(); ::tweak_keys(&mut keys); let group_key = keys.group_key(); let address = ::external_address(group_key); // btc key pair to spend from. - let secret_key = SecretKey::new(&mut rand_core::OsRng); - let private_key = PrivateKey::new(secret_key, BNetwork::Regtest); + let private_key = PrivateKey::new(SecretKey::new(&mut rand_core::OsRng), BNetwork::Regtest); let public_key = PublicKey::from_private_key(SECP256K1, &private_key); let main_addr = BAddress::p2pkh(&public_key, BNetwork::Regtest); @@ -66,32 +69,29 @@ mod bitcoin { let new_block = btc.get_latest_block_number().await.unwrap() + 1; btc .rpc - .rpc_call::>("generatetoaddress", serde_json::json!([1, main_addr])) + .rpc_call::>("generatetoaddress", serde_json::json!([100, main_addr])) .await .unwrap(); - for _ in 0 .. 100 { - btc.mine_block().await; - } - // create a scanner let db = MemDb::new(); let mut scanner = new_scanner(&btc, &db, group_key).await; - // make a transfer instruction & hash it. + // make a transfer instruction & hash it for script. let serai_address = insecure_pair_from_name("dadadadada").public(); - let message = - Shorthand::transfer(None, serai_address.into()).encode(); + let message = Shorthand::transfer(None, serai_address.into()).encode(); let mut data = Sha256::engine(); data.input(&message); - // make the output script SHA256 PUSH MSG_HASH OP_EQ + // make the output script => `OP_SHA256 PUSH MSG_HASH OP_EQUAL` let script = ScriptBuf::builder() .push_opcode(OP_SHA256) .push_slice(Sha256::from_engine(data).as_byte_array()) - .push_opcode(OP_EQUAL); + .push_opcode(OP_EQUAL) + .into_script(); let tx = btc.get_block(new_block).await.unwrap().txdata.swap_remove(0); + let dust = BAmount::from_sat(bitcoin_serai::wallet::DUST); let mut tx = Transaction { version: Version(2), lock_time: LockTime::ZERO, @@ -103,35 +103,13 @@ mod bitcoin { }], output: vec![ TxOut { - value: tx.output[0].value - - BAmount::from_sat(bitcoin_serai::wallet::DUST) - - BAmount::from_sat(10000), + value: tx.output[0].value - dust - BAmount::from_sat(10000), script_pubkey: main_addr.script_pubkey(), }, - TxOut { - value: BAmount::from_sat(bitcoin_serai::wallet::DUST), - script_pubkey: ScriptBuf::new_p2wsh(&script.as_script().wscript_hash()), - }, + TxOut { value: dust, script_pubkey: ScriptBuf::new_p2wsh(&script.wscript_hash()) }, ], }; - // sign the input - let mut der = SECP256K1 - .sign_ecdsa_low_r( - &Message::from( - SighashCache::new(&tx) - .legacy_signature_hash(0, &main_addr.script_pubkey(), EcdsaSighashType::All.to_u32()) - .unwrap() - .to_raw_hash(), - ), - &private_key.inner, - ) - .serialize_der() - .to_vec(); - der.push(1); - tx.input[0].script_sig = Builder::new() - .push_slice(PushBytesBuf::try_from(der).unwrap()) - .push_key(&public_key) - .into_script(); + tx.input[0].script_sig = Bitcoin::sign_btc_input_for_p2pkh(&tx, &private_key); // send it btc.rpc.send_raw_transaction(&tx).await.unwrap(); @@ -139,7 +117,7 @@ mod bitcoin { // witness script let mut witness = Witness::new(); witness.push(message); - witness.push(script.as_script()); + witness.push(script); // make another tx that spends both outputs let mut tx = Transaction { @@ -156,7 +134,7 @@ mod bitcoin { previous_output: OutPoint { txid: tx.txid(), vout: 1 }, script_sig: Script::new().into(), sequence: Sequence(u32::MAX), - witness: witness, + witness, }, ], output: vec![TxOut { @@ -164,25 +142,7 @@ mod bitcoin { script_pubkey: address.0.script_pubkey(), }], }; - - // sign the first input - let mut der = SECP256K1 - .sign_ecdsa_low_r( - &Message::from( - SighashCache::new(&tx) - .legacy_signature_hash(0, &main_addr.script_pubkey(), EcdsaSighashType::All.to_u32()) - .unwrap() - .to_raw_hash(), - ), - &private_key.inner, - ) - .serialize_der() - .to_vec(); - der.push(1); - tx.input[0].script_sig = Builder::new() - .push_slice(PushBytesBuf::try_from(der).unwrap()) - .push_key(&public_key) - .into_script(); + tx.input[0].script_sig = Bitcoin::sign_btc_input_for_p2pkh(&tx, &private_key); // send it let block_number = btc.get_latest_block_number().await.unwrap() + 1; @@ -192,28 +152,31 @@ mod bitcoin { } let tx_block = btc.get_block(block_number).await.unwrap(); - // verify that scanner picked the output up with the right message - let outputs = match timeout(Duration::from_secs(30), scanner.events.recv()).await.unwrap().unwrap() { - ScannerEvent::Block { is_retirement_block, block, outputs } => { - scanner.multisig_completed.send(false).unwrap(); - assert!(!is_retirement_block); - assert_eq!(block, tx_block.id()); - assert_eq!(outputs.len(), 1); - assert_eq!(outputs[0].kind(), OutputType::External); - outputs - } - ScannerEvent::Completed(_, _, _, _) => { - panic!("unexpectedly got eventuality completion"); - } - }; + // verify that scanner picked up the output + let outputs = + match timeout(Duration::from_secs(30), scanner.events.recv()).await.unwrap().unwrap() { + ScannerEvent::Block { is_retirement_block, block, outputs } => { + scanner.multisig_completed.send(false).unwrap(); + assert!(!is_retirement_block); + assert_eq!(block, tx_block.id()); + assert_eq!(outputs.len(), 1); + assert_eq!(outputs[0].kind(), OutputType::External); + outputs + } + _ => panic!("unexpectedly got eventuality completion"), + }; + // verify that message is correct let mut data = outputs[0].data(); - assert!(!data.is_empty()); - let Ok(shorthand) = Shorthand::decode(&mut data) else { panic!("can't decode data") }; - let Ok(instruction) = RefundableInInstruction::try_from(shorthand) else { panic!("can't decode ins") }; + let Ok(shorthand) = Shorthand::decode(&mut data) else { + panic!("can't decode msg data into Shorthand") + }; + let Ok(instruction) = RefundableInInstruction::try_from(shorthand) else { + panic!("can't convert Shorthand to instruction") + }; match instruction.instruction { InInstruction::Transfer(address) => assert_eq!(address, serai_address.into()), - _ => panic!("wrong ins") + _ => panic!("wrong instruction type"), } }); } From 97202a7b894c174fdee958d765b85354689ff0af Mon Sep 17 00:00:00 2001 From: akildemir Date: Wed, 13 Dec 2023 13:43:32 +0300 Subject: [PATCH 4/7] bug fix --- processor/src/tests/literal/mod.rs | 9 +++++++-- processor/src/tests/scanner.rs | 9 +++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/processor/src/tests/literal/mod.rs b/processor/src/tests/literal/mod.rs index 1707a73b2..0e147444c 100644 --- a/processor/src/tests/literal/mod.rs +++ b/processor/src/tests/literal/mod.rs @@ -5,6 +5,8 @@ use dockertest::{ #[cfg(feature = "bitcoin")] mod bitcoin { + use std::sync::Arc; + use super::*; use crate::{ networks::{Network, Bitcoin, Output, OutputType, Block}, @@ -33,7 +35,10 @@ mod bitcoin { primitives::insecure_pair_from_name, }; use serai_db::MemDb; - use tokio::time::{timeout, Duration}; + use tokio::{ + time::{timeout, Duration}, + sync::Mutex, + }; #[test] fn test_dust_constant() { @@ -75,7 +80,7 @@ mod bitcoin { // create a scanner let db = MemDb::new(); - let mut scanner = new_scanner(&btc, &db, group_key).await; + let mut scanner = new_scanner(&btc, &db, group_key, &Arc::new(Mutex::new(true))).await; // make a transfer instruction & hash it for script. let serai_address = insecure_pair_from_name("dadadadada").public(); diff --git a/processor/src/tests/scanner.rs b/processor/src/tests/scanner.rs index bbb08e4a1..5aad5bb51 100644 --- a/processor/src/tests/scanner.rs +++ b/processor/src/tests/scanner.rs @@ -19,8 +19,8 @@ pub async fn new_scanner( network: &N, db: &D, group_key: ::G, + first: &Arc>, ) -> ScannerHandle { - let first = Arc::new(Mutex::new(true)); let activation_number = network.get_latest_block_number().await.unwrap(); let mut db = db.clone(); let (mut scanner, current_keys) = Scanner::new(network.clone(), db.clone()); @@ -52,7 +52,8 @@ pub async fn test_scanner(network: N) { } let db = MemDb::new(); - let scanner = new_scanner(&network, &db, group_key).await; + let first = Arc::new(Mutex::new(true)); + let scanner = new_scanner(&network, &db, group_key, &first).await; // Receive funds let block = network.test_send(N::external_address(keys.group_key())).await; @@ -79,7 +80,7 @@ pub async fn test_scanner(network: N) { let (mut scanner, outputs) = verify_event(scanner).await; // Create a new scanner off the current DB and verify it re-emits the above events - verify_event(new_scanner(&network, &db, group_key).await).await; + verify_event(new_scanner(&network, &db, group_key, &first).await).await; // Acknowledge the block let mut cloned_db = db.clone(); @@ -94,7 +95,7 @@ pub async fn test_scanner(network: N) { // Create a new scanner off the current DB and make sure it also does nothing assert!(timeout( Duration::from_secs(30), - new_scanner(&network, &db, group_key).await.events.recv() + new_scanner(&network, &db, group_key, &first).await.events.recv() ) .await .is_err()); From a801b977d22cdf7170ec69786538d41566f4dc3c Mon Sep 17 00:00:00 2001 From: akildemir Date: Tue, 9 Jan 2024 17:24:01 +0300 Subject: [PATCH 5/7] fix pr comments --- processor/src/networks/bitcoin.rs | 80 ++++++++++++++++-------------- processor/src/tests/literal/mod.rs | 70 ++++++++++++++------------ 2 files changed, 82 insertions(+), 68 deletions(-) diff --git a/processor/src/networks/bitcoin.rs b/processor/src/networks/bitcoin.rs index daddaa028..73b47ca50 100644 --- a/processor/src/networks/bitcoin.rs +++ b/processor/src/networks/bitcoin.rs @@ -22,8 +22,7 @@ use bitcoin_serai::{ script::Instruction, address::{NetworkChecked, Address as BAddress}, Transaction, Block, Network as BNetwork, ScriptBuf, - opcodes::all::{OP_SHA256, OP_EQUAL}, - blockdata::transaction::{TxIn, TxOut}, + opcodes::all::{OP_SHA256, OP_EQUALVERIFY}, }, wallet::{ tweak_keys, address_payload, ReceivedOutput, Scanner, TransactionError, @@ -41,6 +40,7 @@ use bitcoin_serai::bitcoin::{ absolute::LockTime, Amount as BAmount, Sequence, Script, Witness, OutPoint, transaction::Version, + blockdata::transaction::{TxIn, TxOut}, }; use serai_client::{ @@ -397,7 +397,11 @@ impl Bitcoin { } #[cfg(test)] - pub fn sign_btc_input_for_p2pkh(tx: &Transaction, private_key: &PrivateKey) -> ScriptBuf { + pub fn sign_btc_input_for_p2pkh( + tx: &Transaction, + input_index: usize, + private_key: &PrivateKey, + ) -> ScriptBuf { let public_key = PublicKey::from_private_key(SECP256K1, private_key); let main_addr = BAddress::p2pkh(&public_key, BNetwork::Regtest); @@ -405,7 +409,11 @@ impl Bitcoin { .sign_ecdsa_low_r( &Message::from( SighashCache::new(tx) - .legacy_signature_hash(0, &main_addr.script_pubkey(), EcdsaSighashType::All.to_u32()) + .legacy_signature_hash( + input_index, + &main_addr.script_pubkey(), + EcdsaSighashType::All.to_u32(), + ) .unwrap() .to_raw_hash(), ), @@ -475,8 +483,8 @@ impl Bitcoin { } } - // expected script has to start with SHA256 PUSH MSG_HASH OP_EQ .. - fn expected_script_pattern(&self, script: &ScriptBuf) -> Option { + // Expected script has to start with SHA256 PUSH MSG_HASH OP_EQUALVERIFY .. + fn segwit_data_pattern(script: &ScriptBuf) -> Option { let mut ins = script.instructions(); // first item should be SHA256 code @@ -488,28 +496,14 @@ impl Bitcoin { ins.next()?.ok()?.push_bytes()?; // next should be a equality check - if ins.next()?.ok()?.opcode()? != OP_EQUAL { + if ins.next()?.ok()?.opcode()? != OP_EQUALVERIFY { return Some(false); } Some(true) } - async fn spent_output_of(&self, input: &TxIn) -> TxOut { - let mut spent_tx = input.previous_output.txid.as_raw_hash().to_byte_array(); - spent_tx.reverse(); - let mut tx; - while { - tx = self.get_transaction(&spent_tx).await; - tx.is_err() - } { - log::error!("couldn't get transaction from bitcoin node: {tx:?}"); - sleep(Duration::from_secs(5)).await; - } - tx.unwrap().output.swap_remove(usize::try_from(input.previous_output.vout).unwrap()) - } - - async fn extract_serai_data(&self, tx: &Transaction) -> Vec { + fn extract_serai_data(tx: &Transaction) -> Vec { // check outputs let mut data = (|| { for output in &tx.output { @@ -526,11 +520,12 @@ impl Bitcoin { // check inputs if data.is_empty() { for input in &tx.input { - if self.spent_output_of(input).await.script_pubkey.is_p2wsh() { - let witness = input.witness.to_vec(); - let redeem_script = ScriptBuf::from_bytes(witness[1].clone()); - if let Some(true) = self.expected_script_pattern(&redeem_script) { - data = witness[0].clone(); + let witness = input.witness.to_vec(); + // expected witness at least has to have 2 items, msg and the redeem script. + if witness.len() >= 2 { + let redeem_script = ScriptBuf::from_bytes(witness.last().unwrap().clone()); + if Self::segwit_data_pattern(&redeem_script) == Some(true) { + data = witness[witness.len() - 2].clone(); // len() - 1 is the redeem_script break; } } @@ -540,13 +535,6 @@ impl Bitcoin { data.truncate(MAX_DATA_LEN.try_into().unwrap()); data } - - async fn extract_origin(&self, tx: &Transaction) -> Option
{ - let spent_output = self.spent_output_of(&tx.input[0]).await; - BAddress::from_script(&spent_output.script_pubkey, BNetwork::Bitcoin) - .ok() - .and_then(Address::new) - } } #[async_trait] @@ -681,8 +669,26 @@ impl Network for Bitcoin { } // populate the rest of the outputs - let presumed_origin = self.extract_origin(tx).await; - let data = self.extract_serai_data(tx).await; + let presumed_origin = { + let spent_output = { + let input = &tx.input[0]; // TODO: why use 0? + let mut spent_tx = input.previous_output.txid.as_raw_hash().to_byte_array(); + spent_tx.reverse(); + let mut tx; + while { + tx = self.get_transaction(&spent_tx).await; + tx.is_err() + } { + log::error!("couldn't get transaction from bitcoin node: {tx:?}"); + sleep(Duration::from_secs(5)).await; + } + tx.unwrap().output.swap_remove(usize::try_from(input.previous_output.vout).unwrap()) + }; + BAddress::from_script(&spent_output.script_pubkey, BNetwork::Bitcoin) + .ok() + .and_then(Address::new) + }; + let data = Self::extract_serai_data(tx); for output in &mut outputs { if output.kind == OutputType::External { output.data = data.clone(); @@ -870,7 +876,7 @@ impl Network for Bitcoin { script_pubkey: address.as_ref().script_pubkey(), }], }; - tx.input[0].script_sig = Self::sign_btc_input_for_p2pkh(&tx, &private_key); + tx.input[0].script_sig = Self::sign_btc_input_for_p2pkh(&tx, 0, &private_key); let block = self.get_latest_block_number().await.unwrap() + 1; self.rpc.send_raw_transaction(&tx).await.unwrap(); diff --git a/processor/src/tests/literal/mod.rs b/processor/src/tests/literal/mod.rs index 0e147444c..38279f358 100644 --- a/processor/src/tests/literal/mod.rs +++ b/processor/src/tests/literal/mod.rs @@ -16,24 +16,22 @@ mod bitcoin { use sp_application_crypto::Pair; use bitcoin_serai::bitcoin::{ - secp256k1::{SECP256K1, SecretKey}, + secp256k1::{SECP256K1, SecretKey, Message}, PrivateKey, PublicKey, hashes::{HashEngine, Hash, sha256::Hash as Sha256}, + sighash::{SighashCache, EcdsaSighashType}, absolute::LockTime, Amount as BAmount, Sequence, Script, Witness, OutPoint, address::Address as BAddress, transaction::{Version, Transaction, TxIn, TxOut}, Network as BNetwork, ScriptBuf, - opcodes::all::{OP_SHA256, OP_EQUAL}, + opcodes::all::{OP_SHA256, OP_EQUALVERIFY}, }; use frost::Participant; use rand_core::OsRng; - use scale::{Encode, Decode}; - use serai_client::{ - in_instructions::primitives::{Shorthand, RefundableInInstruction, InInstruction}, - primitives::insecure_pair_from_name, - }; + use scale::Encode; + use serai_client::{in_instructions::primitives::Shorthand, primitives::insecure_pair_from_name}; use serai_db::MemDb; use tokio::{ time::{timeout, Duration}, @@ -88,12 +86,16 @@ mod bitcoin { let mut data = Sha256::engine(); data.input(&message); - // make the output script => `OP_SHA256 PUSH MSG_HASH OP_EQUAL` - let script = ScriptBuf::builder() + // make the output script => msg_script(OP_SHA256 PUSH MSG_HASH OP_EQUALVERIFY) + any_script + let mut script = ScriptBuf::builder() .push_opcode(OP_SHA256) .push_slice(Sha256::from_engine(data).as_byte_array()) - .push_opcode(OP_EQUAL) + .push_opcode(OP_EQUALVERIFY) .into_script(); + // append a regular spend script + for i in main_addr.script_pubkey().instructions() { + script.push_instruction(i.unwrap()); + } let tx = btc.get_block(new_block).await.unwrap().txdata.swap_remove(0); let dust = BAmount::from_sat(bitcoin_serai::wallet::DUST); @@ -114,16 +116,12 @@ mod bitcoin { TxOut { value: dust, script_pubkey: ScriptBuf::new_p2wsh(&script.wscript_hash()) }, ], }; - tx.input[0].script_sig = Bitcoin::sign_btc_input_for_p2pkh(&tx, &private_key); + tx.input[0].script_sig = Bitcoin::sign_btc_input_for_p2pkh(&tx, 0, &private_key); + let witness_value = tx.output[1].value; // send it btc.rpc.send_raw_transaction(&tx).await.unwrap(); - // witness script - let mut witness = Witness::new(); - witness.push(message); - witness.push(script); - // make another tx that spends both outputs let mut tx = Transaction { version: Version(2), @@ -139,15 +137,35 @@ mod bitcoin { previous_output: OutPoint { txid: tx.txid(), vout: 1 }, script_sig: Script::new().into(), sequence: Sequence(u32::MAX), - witness, + witness: Witness::new(), }, ], output: vec![TxOut { - value: tx.output[0].value + tx.output[1].value - BAmount::from_sat(10000), - script_pubkey: address.0.script_pubkey(), + value: tx.output[0].value + witness_value - BAmount::from_sat(10000), + script_pubkey: address.as_ref().script_pubkey(), }], }; - tx.input[0].script_sig = Bitcoin::sign_btc_input_for_p2pkh(&tx, &private_key); + // sign the first input + tx.input[0].script_sig = Bitcoin::sign_btc_input_for_p2pkh(&tx, 0, &private_key); + + // add the witness script + let mut sig = SECP256K1 + .sign_ecdsa_low_r( + &Message::from( + SighashCache::new(&tx) + .p2wsh_signature_hash(1, &script, witness_value, EcdsaSighashType::All) + .unwrap() + .to_raw_hash(), + ), + &private_key.inner, + ) + .serialize_der() + .to_vec(); + sig.push(1); + tx.input[1].witness.push(sig); + tx.input[1].witness.push(public_key.inner.serialize()); + tx.input[1].witness.push(message.clone()); + tx.input[1].witness.push(script); // send it let block_number = btc.get_latest_block_number().await.unwrap() + 1; @@ -172,17 +190,7 @@ mod bitcoin { }; // verify that message is correct - let mut data = outputs[0].data(); - let Ok(shorthand) = Shorthand::decode(&mut data) else { - panic!("can't decode msg data into Shorthand") - }; - let Ok(instruction) = RefundableInInstruction::try_from(shorthand) else { - panic!("can't convert Shorthand to instruction") - }; - match instruction.instruction { - InInstruction::Transfer(address) => assert_eq!(address, serai_address.into()), - _ => panic!("wrong instruction type"), - } + assert_eq!(outputs[0].data(), message); }); } From a201dccbbf2e2d60b3a305affde2d8b931bdfe95 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Sun, 18 Feb 2024 04:54:49 -0500 Subject: [PATCH 6/7] Test SegWit-encoded data using a single output (not two) --- processor/src/networks/bitcoin.rs | 66 ++++++++++---------- processor/src/tests/literal/mod.rs | 97 ++++++++++++++---------------- 2 files changed, 79 insertions(+), 84 deletions(-) diff --git a/processor/src/networks/bitcoin.rs b/processor/src/networks/bitcoin.rs index 73b47ca50..f2d42acac 100644 --- a/processor/src/networks/bitcoin.rs +++ b/processor/src/networks/bitcoin.rs @@ -396,39 +396,6 @@ impl Bitcoin { Ok(Fee(fee.max(1))) } - #[cfg(test)] - pub fn sign_btc_input_for_p2pkh( - tx: &Transaction, - input_index: usize, - private_key: &PrivateKey, - ) -> ScriptBuf { - let public_key = PublicKey::from_private_key(SECP256K1, private_key); - let main_addr = BAddress::p2pkh(&public_key, BNetwork::Regtest); - - let mut der = SECP256K1 - .sign_ecdsa_low_r( - &Message::from( - SighashCache::new(tx) - .legacy_signature_hash( - input_index, - &main_addr.script_pubkey(), - EcdsaSighashType::All.to_u32(), - ) - .unwrap() - .to_raw_hash(), - ), - &private_key.inner, - ) - .serialize_der() - .to_vec(); - der.push(1); - - ScriptBuf::builder() - .push_slice(PushBytesBuf::try_from(der).unwrap()) - .push_key(&public_key) - .into_script() - } - async fn make_signable_transaction( &self, block_number: usize, @@ -535,6 +502,39 @@ impl Bitcoin { data.truncate(MAX_DATA_LEN.try_into().unwrap()); data } + + #[cfg(test)] + pub fn sign_btc_input_for_p2pkh( + tx: &Transaction, + input_index: usize, + private_key: &PrivateKey, + ) -> ScriptBuf { + let public_key = PublicKey::from_private_key(SECP256K1, private_key); + let main_addr = BAddress::p2pkh(&public_key, BNetwork::Regtest); + + let mut der = SECP256K1 + .sign_ecdsa_low_r( + &Message::from( + SighashCache::new(tx) + .legacy_signature_hash( + input_index, + &main_addr.script_pubkey(), + EcdsaSighashType::All.to_u32(), + ) + .unwrap() + .to_raw_hash(), + ), + &private_key.inner, + ) + .serialize_der() + .to_vec(); + der.push(1); + + ScriptBuf::builder() + .push_slice(PushBytesBuf::try_from(der).unwrap()) + .push_key(&public_key) + .into_script() + } } #[async_trait] diff --git a/processor/src/tests/literal/mod.rs b/processor/src/tests/literal/mod.rs index 38279f358..192214eb1 100644 --- a/processor/src/tests/literal/mod.rs +++ b/processor/src/tests/literal/mod.rs @@ -7,13 +7,9 @@ use dockertest::{ mod bitcoin { use std::sync::Arc; - use super::*; - use crate::{ - networks::{Network, Bitcoin, Output, OutputType, Block}, - tests::scanner::new_scanner, - multisigs::scanner::ScannerEvent, - }; - use sp_application_crypto::Pair; + use rand_core::OsRng; + + use frost::Participant; use bitcoin_serai::bitcoin::{ secp256k1::{SECP256K1, SecretKey, Message}, @@ -28,16 +24,24 @@ mod bitcoin { opcodes::all::{OP_SHA256, OP_EQUALVERIFY}, }; - use frost::Participant; - use rand_core::OsRng; use scale::Encode; + use sp_application_crypto::Pair; use serai_client::{in_instructions::primitives::Shorthand, primitives::insecure_pair_from_name}; - use serai_db::MemDb; + use tokio::{ time::{timeout, Duration}, sync::Mutex, }; + use serai_db::MemDb; + + use super::*; + use crate::{ + networks::{Network, Bitcoin, Output, OutputType, Block}, + tests::scanner::new_scanner, + multisigs::scanner::ScannerEvent, + }; + #[test] fn test_dust_constant() { struct IsTrue; @@ -55,20 +59,20 @@ mod bitcoin { docker.run(|ops| async move { let btc = bitcoin(&ops).await; - // generate a musig address to receive the funds + // generate a multisig address to receive the coins let mut keys = frost::tests::key_gen::<_, ::Curve>(&mut OsRng) .remove(&Participant::new(1).unwrap()) .unwrap(); ::tweak_keys(&mut keys); let group_key = keys.group_key(); - let address = ::external_address(group_key); + let serai_btc_address = ::external_address(group_key); - // btc key pair to spend from. + // btc key pair to send from let private_key = PrivateKey::new(SecretKey::new(&mut rand_core::OsRng), BNetwork::Regtest); let public_key = PublicKey::from_private_key(SECP256K1, &private_key); let main_addr = BAddress::p2pkh(&public_key, BNetwork::Regtest); - // make some funds to spent + // get unlocked coins let new_block = btc.get_latest_block_number().await.unwrap() + 1; btc .rpc @@ -81,7 +85,7 @@ mod bitcoin { let mut scanner = new_scanner(&btc, &db, group_key, &Arc::new(Mutex::new(true))).await; // make a transfer instruction & hash it for script. - let serai_address = insecure_pair_from_name("dadadadada").public(); + let serai_address = insecure_pair_from_name("alice").public(); let message = Shorthand::transfer(None, serai_address.into()).encode(); let mut data = Sha256::engine(); data.input(&message); @@ -97,8 +101,8 @@ mod bitcoin { script.push_instruction(i.unwrap()); } + // Create the first transaction let tx = btc.get_block(new_block).await.unwrap().txdata.swap_remove(0); - let dust = BAmount::from_sat(bitcoin_serai::wallet::DUST); let mut tx = Transaction { version: Version(2), lock_time: LockTime::ZERO, @@ -108,52 +112,40 @@ mod bitcoin { sequence: Sequence(u32::MAX), witness: Witness::default(), }], - output: vec![ - TxOut { - value: tx.output[0].value - dust - BAmount::from_sat(10000), - script_pubkey: main_addr.script_pubkey(), - }, - TxOut { value: dust, script_pubkey: ScriptBuf::new_p2wsh(&script.wscript_hash()) }, - ], + output: vec![TxOut { + value: tx.output[0].value - BAmount::from_sat(10000), + script_pubkey: ScriptBuf::new_p2wsh(&script.wscript_hash()), + }], }; tx.input[0].script_sig = Bitcoin::sign_btc_input_for_p2pkh(&tx, 0, &private_key); - let witness_value = tx.output[1].value; + let initial_output_value = tx.output[0].value; // send it btc.rpc.send_raw_transaction(&tx).await.unwrap(); - // make another tx that spends both outputs + // Chain a transaction spending it with the InInstruction embedded in the input let mut tx = Transaction { version: Version(2), lock_time: LockTime::ZERO, - input: vec![ - TxIn { - previous_output: OutPoint { txid: tx.txid(), vout: 0 }, - script_sig: Script::new().into(), - sequence: Sequence(u32::MAX), - witness: Witness::default(), - }, - TxIn { - previous_output: OutPoint { txid: tx.txid(), vout: 1 }, - script_sig: Script::new().into(), - sequence: Sequence(u32::MAX), - witness: Witness::new(), - }, - ], + input: vec![TxIn { + previous_output: OutPoint { txid: tx.txid(), vout: 0 }, + script_sig: Script::new().into(), + sequence: Sequence(u32::MAX), + witness: Witness::new(), + }], output: vec![TxOut { - value: tx.output[0].value + witness_value - BAmount::from_sat(10000), - script_pubkey: address.as_ref().script_pubkey(), + value: tx.output[0].value - BAmount::from_sat(10000), + script_pubkey: serai_btc_address.as_ref().script_pubkey(), }], }; - // sign the first input - tx.input[0].script_sig = Bitcoin::sign_btc_input_for_p2pkh(&tx, 0, &private_key); // add the witness script + // This is the standard script with an extra argument of the InInstruction let mut sig = SECP256K1 .sign_ecdsa_low_r( &Message::from( SighashCache::new(&tx) - .p2wsh_signature_hash(1, &script, witness_value, EcdsaSighashType::All) + .p2wsh_signature_hash(0, &script, initial_output_value, EcdsaSighashType::All) .unwrap() .to_raw_hash(), ), @@ -162,14 +154,16 @@ mod bitcoin { .serialize_der() .to_vec(); sig.push(1); - tx.input[1].witness.push(sig); - tx.input[1].witness.push(public_key.inner.serialize()); - tx.input[1].witness.push(message.clone()); - tx.input[1].witness.push(script); + tx.input[0].witness.push(sig); + tx.input[0].witness.push(public_key.inner.serialize()); + tx.input[0].witness.push(message.clone()); + tx.input[0].witness.push(script); - // send it - let block_number = btc.get_latest_block_number().await.unwrap() + 1; + // Send it immediately, as Bitcoin allows mempool chaining btc.rpc.send_raw_transaction(&tx).await.unwrap(); + + // Mine enough confirmations + let block_number = btc.get_latest_block_number().await.unwrap() + 1; for _ in 0 .. ::CONFIRMATIONS { btc.mine_block().await; } @@ -189,7 +183,8 @@ mod bitcoin { _ => panic!("unexpectedly got eventuality completion"), }; - // verify that message is correct + // verify that the amount and message are correct + assert_eq!(outputs[0].balance().amount.0, tx.output[0].value.to_sat()); assert_eq!(outputs[0].data(), message); }); } From 18a95a55fc11ff83028e0d0e4fcae2792cb417eb Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Sun, 18 Feb 2024 04:58:47 -0500 Subject: [PATCH 7/7] Remove TODO used as a question, document origins when SegWit encoding --- processor/src/networks/bitcoin.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/processor/src/networks/bitcoin.rs b/processor/src/networks/bitcoin.rs index f2d42acac..606a3e123 100644 --- a/processor/src/networks/bitcoin.rs +++ b/processor/src/networks/bitcoin.rs @@ -668,10 +668,16 @@ impl Network for Bitcoin { continue; } - // populate the rest of the outputs + // populate the outputs with the origin and data let presumed_origin = { + // This may identify the P2WSH output *embedding the InInstruction* as the origin, which + // would be a bit trickier to spend that a traditional output... + // There's no risk of the InInstruction going missing as it'd already be on-chain though + // We *could* parse out the script *without the InInstruction prefix* and declare that the + // origin + // TODO let spent_output = { - let input = &tx.input[0]; // TODO: why use 0? + let input = &tx.input[0]; let mut spent_tx = input.previous_output.txid.as_raw_hash().to_byte_array(); spent_tx.reverse(); let mut tx;