|
| 1 | +use core::panic; |
1 | 2 | use std::str::FromStr;
|
2 | 3 | use std::sync::Arc;
|
3 | 4 |
|
@@ -3026,3 +3027,205 @@ fn test_tx_ordering_untouched_preserves_insertion_ordering() {
|
3026 | 3027 | // Check vout is sorted by recipient insertion order
|
3027 | 3028 | assert!(txouts == vec![400, 300, 500]);
|
3028 | 3029 | }
|
| 3030 | + |
| 3031 | +#[test] |
| 3032 | +fn test_utxo_reservation_prevents_double_spend() { |
| 3033 | + let (mut wallet, _) = get_funded_wallet_wpkh(); |
| 3034 | + |
| 3035 | + receive_output_in_latest_block(&mut wallet, Amount::from_sat(30_000)); |
| 3036 | + receive_output_in_latest_block(&mut wallet, Amount::from_sat(30_000)); |
| 3037 | + |
| 3038 | + let addr = wallet.reveal_next_address(KeychainKind::External).address; |
| 3039 | + |
| 3040 | + let initial_utxos: Vec<_> = wallet.list_unspent().collect(); |
| 3041 | + assert!( |
| 3042 | + initial_utxos.len() >= 2, |
| 3043 | + "Need at least 2 UTXOs for this test" |
| 3044 | + ); |
| 3045 | + |
| 3046 | + let mut builder1 = wallet.build_tx(); |
| 3047 | + builder1.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); |
| 3048 | + let psbt1 = builder1.finish().expect("should create tx1"); |
| 3049 | + let tx1 = &psbt1.unsigned_tx; |
| 3050 | + |
| 3051 | + let tx1_inputs: Vec<OutPoint> = tx1 |
| 3052 | + .input |
| 3053 | + .iter() |
| 3054 | + .map(|input| input.previous_output) |
| 3055 | + .collect(); |
| 3056 | + |
| 3057 | + let mut builder2 = wallet.build_tx(); |
| 3058 | + builder2.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); |
| 3059 | + let psbt2 = builder2.finish().expect("should create tx2"); |
| 3060 | + let tx2 = &psbt2.unsigned_tx; |
| 3061 | + |
| 3062 | + let tx2_inputs: Vec<OutPoint> = tx2 |
| 3063 | + .input |
| 3064 | + .iter() |
| 3065 | + .map(|input| input.previous_output) |
| 3066 | + .collect(); |
| 3067 | + |
| 3068 | + for tx1_input in &tx1_inputs { |
| 3069 | + assert!( |
| 3070 | + !tx2_inputs.contains(tx1_input), |
| 3071 | + "tx2 should not reuse UTXO {:?} from tx1", |
| 3072 | + tx1_input |
| 3073 | + ); |
| 3074 | + } |
| 3075 | +} |
| 3076 | + |
| 3077 | +#[test] |
| 3078 | +fn test_cancel_tx_unreserves_utxos() { |
| 3079 | + let (mut wallet, _) = get_funded_wallet_wpkh(); |
| 3080 | + let addr = wallet.reveal_next_address(KeychainKind::External).address; |
| 3081 | + |
| 3082 | + let mut builder1 = wallet.build_tx(); |
| 3083 | + builder1.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); |
| 3084 | + let psbt1 = builder1.finish().expect("should create tx1"); |
| 3085 | + let tx1 = psbt1.unsigned_tx.clone(); |
| 3086 | + |
| 3087 | + let tx1_inputs: Vec<OutPoint> = tx1 |
| 3088 | + .input |
| 3089 | + .iter() |
| 3090 | + .map(|input| input.previous_output) |
| 3091 | + .collect(); |
| 3092 | + |
| 3093 | + wallet.cancel_tx(&tx1); |
| 3094 | + |
| 3095 | + let mut builder2 = wallet.build_tx(); |
| 3096 | + builder2.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); |
| 3097 | + let psbt2 = builder2.finish().expect("should create tx2"); |
| 3098 | + let tx2 = &psbt2.unsigned_tx; |
| 3099 | + |
| 3100 | + let tx2_inputs: Vec<OutPoint> = tx2 |
| 3101 | + .input |
| 3102 | + .iter() |
| 3103 | + .map(|input| input.previous_output) |
| 3104 | + .collect(); |
| 3105 | + |
| 3106 | + // After cancellation, tx2 should be able to use at least some UTXOs from tx1 |
| 3107 | + // (coin selection might choose different UTXOs, but they should be available) |
| 3108 | + let shared_inputs: Vec<&OutPoint> = tx1_inputs |
| 3109 | + .iter() |
| 3110 | + .filter(|input| tx2_inputs.contains(input)) |
| 3111 | + .collect(); |
| 3112 | + |
| 3113 | + // We expect tx2 to reuse at least one UTXO from tx1 since they have the same amount |
| 3114 | + assert!( |
| 3115 | + !shared_inputs.is_empty(), |
| 3116 | + "After cancel_tx, tx2 should be able to reuse UTXOs from tx1" |
| 3117 | + ); |
| 3118 | +} |
| 3119 | + |
| 3120 | +#[test] |
| 3121 | +fn test_unreserve_utxos_returns_correct_count() { |
| 3122 | + let (mut wallet, _) = get_funded_wallet_wpkh(); |
| 3123 | + let addr = wallet.reveal_next_address(KeychainKind::External).address; |
| 3124 | + |
| 3125 | + let mut builder = wallet.build_tx(); |
| 3126 | + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); |
| 3127 | + let psbt = builder.finish().expect("should create tx"); |
| 3128 | + let tx = psbt.unsigned_tx.clone(); |
| 3129 | + |
| 3130 | + let input_count = tx.input.len(); |
| 3131 | + |
| 3132 | + let unreserved_count = wallet.unreserve_utxos(&tx); |
| 3133 | + |
| 3134 | + // Should unreserve exactly as many UTXOs as there are inputs |
| 3135 | + assert_eq!( |
| 3136 | + unreserved_count, input_count, |
| 3137 | + "Should unreserve {} UTXOs", |
| 3138 | + input_count |
| 3139 | + ); |
| 3140 | + |
| 3141 | + // Unreserving again should return 0 (UTXOs already unreserved) |
| 3142 | + let second_unreserve_count = wallet.unreserve_utxos(&tx); |
| 3143 | + assert_eq!( |
| 3144 | + second_unreserve_count, 0, |
| 3145 | + "Second unreserve should return 0" |
| 3146 | + ); |
| 3147 | +} |
| 3148 | + |
| 3149 | +#[test] |
| 3150 | +fn test_multiple_unsigned_transactions() { |
| 3151 | + let (mut wallet, _) = get_funded_wallet_wpkh(); |
| 3152 | + |
| 3153 | + receive_output_in_latest_block(&mut wallet, Amount::from_sat(30_000)); |
| 3154 | + receive_output_in_latest_block(&mut wallet, Amount::from_sat(30_000)); |
| 3155 | + receive_output_in_latest_block(&mut wallet, Amount::from_sat(30_000)); |
| 3156 | + |
| 3157 | + let addr = wallet.reveal_next_address(KeychainKind::External).address; |
| 3158 | + |
| 3159 | + let initial_utxos: Vec<_> = wallet.list_unspent().collect(); |
| 3160 | + assert!( |
| 3161 | + initial_utxos.len() >= 3, |
| 3162 | + "Need at least 3 UTXOs for this test" |
| 3163 | + ); |
| 3164 | + |
| 3165 | + let mut transactions = Vec::new(); |
| 3166 | + for i in 0..3 { |
| 3167 | + let mut builder = wallet.build_tx(); |
| 3168 | + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(20_000 + i * 1_000)); |
| 3169 | + let psbt = builder |
| 3170 | + .finish() |
| 3171 | + .unwrap_or_else(|_| panic!("should create tx{}", i + 1)); |
| 3172 | + transactions.push(psbt.unsigned_tx.clone()); |
| 3173 | + } |
| 3174 | + |
| 3175 | + let mut all_inputs: Vec<OutPoint> = Vec::new(); |
| 3176 | + for tx in &transactions { |
| 3177 | + for input in &tx.input { |
| 3178 | + all_inputs.push(input.previous_output); |
| 3179 | + } |
| 3180 | + } |
| 3181 | + |
| 3182 | + // Verify no UTXO is used twice |
| 3183 | + let mut seen = std::collections::HashSet::new(); |
| 3184 | + for input in &all_inputs { |
| 3185 | + assert!( |
| 3186 | + seen.insert(input), |
| 3187 | + "UTXO {:?} was used in multiple transactions", |
| 3188 | + input |
| 3189 | + ); |
| 3190 | + } |
| 3191 | + |
| 3192 | + // Cancel all transactions and verify we can build new ones |
| 3193 | + for tx in &transactions { |
| 3194 | + wallet.cancel_tx(tx); |
| 3195 | + } |
| 3196 | + |
| 3197 | + // After cancelling all, we should be able to build a new transaction |
| 3198 | + let mut builder = wallet.build_tx(); |
| 3199 | + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); |
| 3200 | + let result = builder.finish(); |
| 3201 | + assert!( |
| 3202 | + result.is_ok(), |
| 3203 | + "Should be able to build tx after cancelling all previous ones" |
| 3204 | + ); |
| 3205 | +} |
| 3206 | + |
| 3207 | +#[test] |
| 3208 | +fn test_cancel_tx_unreserves_change_address() { |
| 3209 | + let (mut wallet, _) = get_funded_wallet_wpkh(); |
| 3210 | + let addr = wallet.reveal_next_address(KeychainKind::External).address; |
| 3211 | + |
| 3212 | + let mut builder = wallet.build_tx(); |
| 3213 | + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(10_000)); |
| 3214 | + let psbt = builder.finish().expect("should create tx"); |
| 3215 | + let tx = psbt.unsigned_tx.clone(); |
| 3216 | + |
| 3217 | + // Check if transaction has change output (more than 1 output) |
| 3218 | + let has_change = tx.output.len() > 1; |
| 3219 | + |
| 3220 | + if has_change { |
| 3221 | + let change_index_before = wallet.next_derivation_index(KeychainKind::Internal); |
| 3222 | + wallet.cancel_tx(&tx); |
| 3223 | + |
| 3224 | + let change_index_after = wallet.next_derivation_index(KeychainKind::Internal); |
| 3225 | + // After unreserving, we should be back to the same index |
| 3226 | + assert_eq!( |
| 3227 | + change_index_before, change_index_after, |
| 3228 | + "Change address should be unreserved" |
| 3229 | + ); |
| 3230 | + } |
| 3231 | +} |
0 commit comments