@@ -116,6 +116,14 @@ impl PartialEq<descriptor::Descriptor<descriptor::DescriptorPublicKey>> for Sing
116
116
}
117
117
}
118
118
119
+ /// The index of a change output in a transaction's outputs list, differentiating between a change
120
+ /// output which uses a change address and one which uses a deposit address.
121
+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , Hash ) ]
122
+ pub enum ChangeOutput {
123
+ ChangeAddress { index : usize } ,
124
+ DepositAddress { index : usize } ,
125
+ }
126
+
119
127
impl LianaDescriptor {
120
128
pub fn new ( spending_policy : LianaPolicy ) -> LianaDescriptor {
121
129
// Get the descriptor from the chosen spending policy.
@@ -299,6 +307,57 @@ impl LianaDescriptor {
299
307
Ok ( spend_info)
300
308
}
301
309
310
+ /// List the indexes of the change outputs in this PSBT. It relies on the PSBT to be
311
+ /// well-formed: sane BIP32 derivations must be set for every change output, the inner
312
+ /// transaction must have the same number of outputs as the PSBT.
313
+ /// Will detect change outputs paying to either the change keychain or the deposit one.
314
+ pub fn change_indexes (
315
+ & self ,
316
+ psbt : & Psbt ,
317
+ secp : & secp256k1:: Secp256k1 < impl secp256k1:: Verification > ,
318
+ ) -> Vec < ChangeOutput > {
319
+ let mut indexes = Vec :: new ( ) ;
320
+
321
+ // We iterate through all the BIP32 derivations of each output, but note we only ever set
322
+ // the BIP32 derivations for PSBT outputs which pay to ourselves.
323
+ for ( index, psbt_out) in psbt. outputs . iter ( ) . enumerate ( ) {
324
+ // We can only ever detect change on well-formed PSBTs. On such PSBTs, all keys in the
325
+ // BIP32 derivations belong to us. And they all use the same last derivation index,
326
+ // since that's where the wildcard is in the descriptor. So just pick the first one and
327
+ // infer the derivation index to use to derive the spks below from it.
328
+ let der_index = if let Some ( i) = psbt_out
329
+ . bip32_derivation
330
+ . values ( )
331
+ . next ( )
332
+ . and_then ( |( _, der_path) | der_path. into_iter ( ) . last ( ) )
333
+ {
334
+ i
335
+ } else {
336
+ continue ;
337
+ } ;
338
+
339
+ // If any of the change and deposit addresses at this derivation index match, count it
340
+ // as a change output.
341
+ if let Some ( txo) = psbt. unsigned_tx . output . get ( index) {
342
+ let change_desc = self . change_desc . derive ( * der_index, secp) ;
343
+ if change_desc. script_pubkey ( ) == txo. script_pubkey {
344
+ indexes. push ( ChangeOutput :: ChangeAddress { index } ) ;
345
+ continue ;
346
+ }
347
+ let receive_desc = self . receive_desc . derive ( * der_index, secp) ;
348
+ if receive_desc. script_pubkey ( ) == txo. script_pubkey {
349
+ indexes. push ( ChangeOutput :: DepositAddress { index } ) ;
350
+ }
351
+ } else {
352
+ log:: error!(
353
+ "Provided a PSBT with non-matching tx outputs count and PSBT outputs count."
354
+ ) ;
355
+ }
356
+ }
357
+
358
+ indexes
359
+ }
360
+
302
361
/// Prune the BIP32 derivations in all the PSBT inputs for all the spending paths but the given
303
362
/// one.
304
363
pub fn prune_bip32_derivs ( & self , mut psbt : Psbt , spending_path : & PathInfo ) -> Psbt {
@@ -1279,5 +1338,61 @@ mod tests {
1279
1338
assert_eq ! ( psbt, pruned_psbt) ;
1280
1339
}
1281
1340
1341
+ #[ test]
1342
+ fn change_detection ( ) {
1343
+ let secp = secp256k1:: Secp256k1 :: verification_only ( ) ;
1344
+
1345
+ // Reuse a desc from above desciptor_creation unit test and a psbt from unrelated above
1346
+ // bip32_derivs_pruning unit test.
1347
+ let desc = LianaDescriptor :: from_str ( "wsh(or_d(multi(3,[aabb0011/48'/0'/0'/2']xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/0/<0;1>/*,[aabb0012/48'/0'/0'/2']xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/0/<0;1>/*,[aabb0013/48'/0'/0'/2']xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/0/<0;1>/*),and_v(v:thresh(2,pkh([aabb0011/48'/0'/0'/2']xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/1/<0;1>/*),a:pkh([aabb0012/48'/0'/0'/2']xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/1/<0;1>/*),a:pkh([aabb0013/48'/0'/0'/2']xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/1/<0;1>/*)),older(26352))))" ) . unwrap ( ) ;
1348
+ let mut psbt = Psbt :: from_str ( "cHNidP8BAFICAAAAAc+3IQFejOVro5Hlwy18au5Jr5mJX+tNMGk0ZE1hydIbAQAAAAD9////ARhzAQAAAAAAFgAUqJZUU7Fqu+bIvxjNw+TAtTwP9HQAAAAAAAEAzQIAAAAAAQEIoAeUdfZj04Ds8EspEK222TJdDNy1WZb/Mg1PJbQekwAAAAAA/f///wKQCQQAAAAAACJRIPJojBgnDc9oUS5lDNx/YJznYR2NPQue7h/d+o5Z+2FQoIYBAAAAAAAiACDZrCBvscZpg+S+IaoZBJjyKDdrNS3oXPaF17DNaB+4mAFAe9yuRS3Vn8A5NUglhwiX7vN0wpQ0Q43ClWtJRnC2HJ66h5HYJ/p8xHgHOhRDUWRzcXLLGl+brc5dW+k0OvIZEyuLAgABASughgEAAAAAACIAINmsIG+xxmmD5L4hqhkEmPIoN2s1Lehc9oXXsM1oH7iYAQX9GQFjdqkU2zK+b9oTL/KfnOSYtq3wmtf4qP6IrGt2qRTSNOD0U7fuHdAnKchIf8GmUO904YisbJNrdqkUE5TQk5mdyYtviaGAsIiOgc4y6wGIrGyTU4hWsmdTIQOirPI1KXBtP2Tg2FQxSo4BjFBTf+dCKtZwDQt056slgCEDDHE7Hpxq++JsjZdbfwsPiA6pmq0dV00tR3hc2sus8KkhA2nPUthIMe1SeFegiZEKZF69yJerP1RFVlyu66C5lOVVU65zZHapFEUmCTccyLJXczvUfPUOCXr7CN0uiKxrdqkUeJmVqUt1Q4aFREOUWKX9U/SuZZ2IrGyTa3apFBDmKn40ceTWVbwxRI21c2qji1tOiKxsk1KIU7JoaCIGAjCZLg7xtlG43xEvns0TRd5gHpPrZWzAaYjo3lheMw/hHJAxFe8wAACAAQAAgAAAAIACAACAAgAAAAgAAAAiBgI0Y2/HRNvXA3niUE3RvrzQcCDiJ4F6vVog0uIanRUWHhwXK6G8MAAAgAEAAIAAAACAAgAAgAIAAAAIAAAAIgYCQKZf/IBUWv4F4mGVTv5PlqCceXFtlhfOgW0kIAPI74scFyuhvDAAAIABAACAAAAAgAIAAIAEAAAACAAAACIGAkDfArY5kwHyHvKllcCMhQLErtDmT/A13vABH8PBQ6yIHGNq3z8wAACAAQAAgAAAAIACAACABAAAAAgAAAAiBgLp9dq4ku0u9UKpIRasIb5QEPgPkDcxdcSXYBfW7mUcqByQMRXvMAAAgAEAAIAAAACAAgAAgAQAAAAIAAAAIgYDDHE7Hpxq++JsjZdbfwsPiA6pmq0dV00tR3hc2sus8KkcFyuhvDAAAIABAACAAAAAgAIAAIAAAAAACAAAACIGA0SIq7IkQJYb7brFx54mPzwUl/DzCGja0pdwFFckfm6WHGNq3z8wAACAAQAAgAAAAIACAACAAgAAAAgAAAAiBgNpz1LYSDHtUnhXoImRCmRevciXqz9URVZcruuguZTlVRyQMRXvMAAAgAEAAIAAAACAAgAAgAAAAAAIAAAAIgYDoqzyNSlwbT9k4NhUMUqOAYxQU3/nQirWcA0LdOerJYAcY2rfPzAAAIABAACAAAAAgAIAAIAAAAAACAAAAAAA" ) . unwrap ( ) ;
1349
+
1350
+ // The PSBT has unrelated outputs. Those aren't detected as change.
1351
+ assert ! ( !psbt. outputs. is_empty( ) ) ;
1352
+ assert_eq ! ( desc. change_indexes( & psbt, & secp) . len( ) , 0 ) ;
1353
+
1354
+ // Add a change output, it's correctly detected as such.
1355
+ let der_desc = desc. change_descriptor ( ) . derive ( 999 . into ( ) , & secp) ;
1356
+ let txo = bitcoin:: TxOut {
1357
+ script_pubkey : der_desc. script_pubkey ( ) ,
1358
+ value : bitcoin:: Amount :: MAX_MONEY ,
1359
+ } ;
1360
+ let psbt_out = bitcoin:: psbt:: Output {
1361
+ bip32_derivation : der_desc. bip32_derivations ( ) ,
1362
+ ..Default :: default ( )
1363
+ } ;
1364
+ psbt. unsigned_tx . output . push ( txo) ;
1365
+ psbt. outputs . push ( psbt_out) ;
1366
+ let indexes = desc. change_indexes ( & psbt, & secp) ;
1367
+ assert_eq ! ( indexes. len( ) , 1 ) ;
1368
+ assert ! ( matches!(
1369
+ indexes[ 0 ] ,
1370
+ ChangeOutput :: ChangeAddress { index: 1 }
1371
+ ) ) ;
1372
+
1373
+ // Add another change output, but to a deposit address. Both change outputs are detected.
1374
+ let der_desc = desc. receive_descriptor ( ) . derive ( 424242 . into ( ) , & secp) ;
1375
+ let txo = bitcoin:: TxOut {
1376
+ script_pubkey : der_desc. script_pubkey ( ) ,
1377
+ value : bitcoin:: Amount :: MAX_MONEY ,
1378
+ } ;
1379
+ let psbt_out = bitcoin:: psbt:: Output {
1380
+ bip32_derivation : der_desc. bip32_derivations ( ) ,
1381
+ ..Default :: default ( )
1382
+ } ;
1383
+ psbt. unsigned_tx . output . push ( txo) ;
1384
+ psbt. outputs . push ( psbt_out) ;
1385
+ let indexes = desc. change_indexes ( & psbt, & secp) ;
1386
+ assert_eq ! ( indexes. len( ) , 2 ) ;
1387
+ assert ! ( matches!(
1388
+ indexes[ 0 ] ,
1389
+ ChangeOutput :: ChangeAddress { index: 1 }
1390
+ ) ) ;
1391
+ assert ! ( matches!(
1392
+ indexes[ 1 ] ,
1393
+ ChangeOutput :: DepositAddress { index: 2 }
1394
+ ) ) ;
1395
+ }
1396
+
1282
1397
// TODO: test error conditions of deserialization.
1283
1398
}
0 commit comments