use std::collections::HashMap; use rand_core::{RngCore, OsRng}; use k256::{ elliptic_curve::{ group::{ff::Field, Group}, sec1::{Tag, ToEncodedPoint}, }, Scalar, ProjectivePoint, }; use frost::{ curve::Secp256k1, Participant, ThresholdKeys, tests::{THRESHOLD, key_gen, sign_without_caching}, }; use bitcoin_serai::{ bitcoin::{ hashes::Hash as HashTrait, blockdata::opcodes::all::OP_RETURN, script::{PushBytesBuf, Instruction, Instructions, Script}, OutPoint, Amount, TxOut, Transaction, Network, Address, }, wallet::{ tweak_keys, p2tr_script_buf, ReceivedOutput, Scanner, TransactionError, SignableTransaction, }, rpc::Rpc, }; mod runner; use runner::rpc; const FEE: u64 = 20; fn is_even(key: ProjectivePoint) -> bool { key.to_encoded_point(true).tag() == Tag::CompressedEvenY } async fn send_and_get_output(rpc: &Rpc, scanner: &Scanner, key: ProjectivePoint) -> ReceivedOutput { let block_number = rpc.get_latest_block_number().await.unwrap() + 1; rpc .rpc_call::>( "generatetoaddress", serde_json::json!([ 1, Address::from_script(&p2tr_script_buf(key).unwrap(), Network::Regtest).unwrap() ]), ) .await .unwrap(); // Mine until maturity rpc .rpc_call::>( "generatetoaddress", serde_json::json!([100, Address::p2sh(Script::new(), Network::Regtest).unwrap()]), ) .await .unwrap(); let block = rpc.get_block(&rpc.get_block_hash(block_number).await.unwrap()).await.unwrap(); let mut outputs = scanner.scan_block(&block); assert_eq!(outputs, scanner.scan_transaction(&block.txdata[0])); assert_eq!(outputs.len(), 1); assert_eq!(outputs[0].outpoint(), &OutPoint::new(block.txdata[0].compute_txid(), 0)); assert_eq!(outputs[0].value(), block.txdata[0].output[0].value.to_sat()); assert_eq!( ReceivedOutput::read::<&[u8]>(&mut outputs[0].serialize().as_ref()).unwrap(), outputs[0] ); outputs.swap_remove(0) } fn keys() -> (HashMap>, ProjectivePoint) { let mut keys = key_gen(&mut OsRng); for keys in keys.values_mut() { *keys = tweak_keys(keys); } let key = keys.values().next().unwrap().group_key(); (keys, key) } fn sign( keys: &HashMap>, tx: &SignableTransaction, ) -> Transaction { let mut machines = HashMap::new(); for i in (1 ..= THRESHOLD).map(|i| Participant::new(i).unwrap()) { machines.insert(i, tx.clone().multisig(&keys[&i].clone()).unwrap()); } sign_without_caching(&mut OsRng, machines, &[]) } async_sequential! { async fn test_scanner() { // Test Scanners are creatable for even keys. for _ in 0 .. 128 { let key = ProjectivePoint::random(&mut OsRng); assert_eq!(Scanner::new(key).is_some(), is_even(key)); } let mut key = ProjectivePoint::random(&mut OsRng); while !is_even(key) { key += ProjectivePoint::GENERATOR; } { let mut scanner = Scanner::new(key).unwrap(); for _ in 0 .. 128 { let mut offset = Scalar::random(&mut OsRng); let registered = scanner.register_offset(offset).unwrap(); // Registering this again should return None assert!(scanner.register_offset(offset).is_none()); // We can only register offsets resulting in even keys // Make this even while !is_even(key + (ProjectivePoint::GENERATOR * offset)) { offset += Scalar::ONE; } // Ensure it matches the registered offset assert_eq!(registered, offset); // Assert registering this again fails assert!(scanner.register_offset(offset).is_none()); } } let rpc = rpc().await; let mut scanner = Scanner::new(key).unwrap(); assert_eq!(send_and_get_output(&rpc, &scanner, key).await.offset(), Scalar::ZERO); // Register an offset and test receiving to it let offset = scanner.register_offset(Scalar::random(&mut OsRng)).unwrap(); assert_eq!( send_and_get_output(&rpc, &scanner, key + (ProjectivePoint::GENERATOR * offset)) .await .offset(), offset ); } async fn test_transaction_errors() { let (_, key) = keys(); let rpc = rpc().await; let scanner = Scanner::new(key).unwrap(); let output = send_and_get_output(&rpc, &scanner, key).await; assert_eq!(output.offset(), Scalar::ZERO); let inputs = vec![output]; let addr = || p2tr_script_buf(key).unwrap(); let payments = vec![(addr(), 1000)]; assert!(SignableTransaction::new(inputs.clone(), &payments, None, None, FEE).is_ok()); assert_eq!( SignableTransaction::new(vec![], &payments, None, None, FEE), Err(TransactionError::NoInputs) ); // No change assert!(SignableTransaction::new(inputs.clone(), &[(addr(), 1000)], None, None, FEE).is_ok()); // Consolidation TX assert!(SignableTransaction::new(inputs.clone(), &[], Some(addr()), None, FEE).is_ok()); // Data assert!(SignableTransaction::new(inputs.clone(), &[], None, Some(vec![]), FEE).is_ok()); // No outputs assert_eq!( SignableTransaction::new(inputs.clone(), &[], None, None, FEE), Err(TransactionError::NoOutputs), ); assert_eq!( SignableTransaction::new(inputs.clone(), &[(addr(), 1)], None, None, FEE), Err(TransactionError::DustPayment), ); assert!( SignableTransaction::new(inputs.clone(), &payments, None, Some(vec![0; 80]), FEE).is_ok() ); assert_eq!( SignableTransaction::new(inputs.clone(), &payments, None, Some(vec![0; 81]), FEE), Err(TransactionError::TooMuchData), ); assert_eq!( SignableTransaction::new(inputs.clone(), &[], Some(addr()), None, 0), Err(TransactionError::TooLowFee), ); assert!(matches!( SignableTransaction::new(inputs.clone(), &[(addr(), inputs[0].value() * 2)], None, None, FEE), Err(TransactionError::NotEnoughFunds { .. }), )); assert_eq!( SignableTransaction::new(inputs, &vec![(addr(), 1000); 10000], None, None, FEE), Err(TransactionError::TooLargeTransaction), ); } async fn test_send() { let (keys, key) = keys(); let rpc = rpc().await; let mut scanner = Scanner::new(key).unwrap(); // Get inputs, one not offset and one offset let output = send_and_get_output(&rpc, &scanner, key).await; assert_eq!(output.offset(), Scalar::ZERO); let offset = scanner.register_offset(Scalar::random(&mut OsRng)).unwrap(); let offset_key = key + (ProjectivePoint::GENERATOR * offset); let offset_output = send_and_get_output(&rpc, &scanner, offset_key).await; assert_eq!(offset_output.offset(), offset); // Declare payments, change, fee let payments = [ (p2tr_script_buf(key).unwrap(), 1005), (p2tr_script_buf(offset_key).unwrap(), 1007) ]; let change_offset = scanner.register_offset(Scalar::random(&mut OsRng)).unwrap(); let change_key = key + (ProjectivePoint::GENERATOR * change_offset); let change_addr = p2tr_script_buf(change_key).unwrap(); // Create and sign the TX let tx = SignableTransaction::new( vec![output.clone(), offset_output.clone()], &payments, Some(change_addr.clone()), None, FEE ).unwrap(); let needed_fee = tx.needed_fee(); let expected_id = tx.txid(); let tx = sign(&keys, &tx); assert_eq!(tx.output.len(), 3); // Ensure we can scan it let outputs = scanner.scan_transaction(&tx); for (o, output) in outputs.iter().enumerate() { assert_eq!(output.outpoint(), &OutPoint::new(tx.compute_txid(), u32::try_from(o).unwrap())); assert_eq!(&ReceivedOutput::read::<&[u8]>(&mut output.serialize().as_ref()).unwrap(), output); } assert_eq!(outputs[0].offset(), Scalar::ZERO); assert_eq!(outputs[1].offset(), offset); assert_eq!(outputs[2].offset(), change_offset); // Make sure the payments were properly created for ((output, scanned), payment) in tx.output.iter().zip(outputs.iter()).zip(payments.iter()) { assert_eq!( output, &TxOut { script_pubkey: payment.0.clone(), value: Amount::from_sat(payment.1) }, ); assert_eq!(scanned.value(), payment.1 ); } // Make sure the change is correct assert_eq!(needed_fee, u64::try_from(tx.vsize()).unwrap() * FEE); let input_value = output.value() + offset_output.value(); let output_value = tx.output.iter().map(|output| output.value.to_sat()).sum::(); assert_eq!(input_value - output_value, needed_fee); let change_amount = input_value - payments.iter().map(|payment| payment.1).sum::() - needed_fee; assert_eq!( tx.output[2], TxOut { script_pubkey: change_addr, value: Amount::from_sat(change_amount) }, ); // This also tests send_raw_transaction and get_transaction, which the RPC test can't // effectively test rpc.send_raw_transaction(&tx).await.unwrap(); let mut hash = *tx.compute_txid().as_raw_hash().as_byte_array(); hash.reverse(); assert_eq!(tx, rpc.get_transaction(&hash).await.unwrap()); assert_eq!(expected_id, hash); } async fn test_data() { let (keys, key) = keys(); let rpc = rpc().await; let scanner = Scanner::new(key).unwrap(); let output = send_and_get_output(&rpc, &scanner, key).await; assert_eq!(output.offset(), Scalar::ZERO); let data_len = 60 + usize::try_from(OsRng.next_u64() % 21).unwrap(); let mut data = vec![0; data_len]; OsRng.fill_bytes(&mut data); let tx = sign( &keys, &SignableTransaction::new( vec![output], &[], Some(p2tr_script_buf(key).unwrap()), Some(data.clone()), FEE ).unwrap() ); assert!(tx.output[0].script_pubkey.is_op_return()); let check = |mut instructions: Instructions| { assert_eq!(instructions.next().unwrap().unwrap(), Instruction::Op(OP_RETURN)); assert_eq!( instructions.next().unwrap().unwrap(), Instruction::PushBytes(&PushBytesBuf::try_from(data.clone()).unwrap()), ); assert!(instructions.next().is_none()); }; check(tx.output[0].script_pubkey.instructions()); check(tx.output[0].script_pubkey.instructions_minimal()); } }