Skip to content

Commit fdb75ad

Browse files
committed
[WIP] descriptors: add a method to get a PSBT's change outputs
1 parent c6b554d commit fdb75ad

File tree

1 file changed

+108
-0
lines changed

1 file changed

+108
-0
lines changed

src/descriptors/mod.rs

+108
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,14 @@ impl PartialEq<descriptor::Descriptor<descriptor::DescriptorPublicKey>> for Sing
116116
}
117117
}
118118

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+
119127
impl LianaDescriptor {
120128
pub fn new(spending_policy: LianaPolicy) -> LianaDescriptor {
121129
// Get the descriptor from the chosen spending policy.
@@ -299,6 +307,54 @@ impl LianaDescriptor {
299307
Ok(spend_info)
300308
}
301309

310+
/// TODO!
311+
pub fn change_indexes(
312+
&self,
313+
psbt: &Psbt,
314+
secp: &secp256k1::Secp256k1<impl secp256k1::Verification>,
315+
) -> Vec<ChangeOutput> {
316+
let mut indexes = Vec::new();
317+
318+
// We iterate through all the BIP32 derivations of each output, but note we only ever set
319+
// the BIP32 derivations for PSBT outputs which pay to ourselves.
320+
for (index, psbt_out) in psbt.outputs.iter().enumerate() {
321+
// We can only ever detect change on well-formed PSBTs. On such PSBTs, all keys in the
322+
// BIP32 derivations belong to us. And they all use the same last derivation index,
323+
// since that's where the wildcard is in the descriptor. So just pick the first one and
324+
// infer the derivation index to use to derive the spks below from it.
325+
let der_index = if let Some(i) = psbt_out
326+
.bip32_derivation
327+
.values()
328+
.next()
329+
.and_then(|(_, der_path)| der_path.into_iter().last())
330+
{
331+
i
332+
} else {
333+
continue;
334+
};
335+
336+
// If any of the change and deposit addresses at this derivation index match, count it
337+
// as a change output.
338+
if let Some(txo) = psbt.unsigned_tx.output.get(index) {
339+
let change_desc = self.change_desc.derive(*der_index, secp);
340+
if change_desc.script_pubkey() == txo.script_pubkey {
341+
indexes.push(ChangeOutput::ChangeAddress { index });
342+
continue;
343+
}
344+
let receive_desc = self.receive_desc.derive(*der_index, secp);
345+
if receive_desc.script_pubkey() == txo.script_pubkey {
346+
indexes.push(ChangeOutput::DepositAddress { index });
347+
}
348+
} else {
349+
log::error!(
350+
"Provided a PSBT with non-matching tx outputs count and PSBT outputs count."
351+
);
352+
}
353+
}
354+
355+
indexes
356+
}
357+
302358
/// Prune the BIP32 derivations in all the PSBT inputs for all the spending paths but the given
303359
/// one.
304360
pub fn prune_bip32_derivs(&self, mut psbt: Psbt, spending_path: &PathInfo) -> Psbt {
@@ -1279,5 +1335,57 @@ mod tests {
12791335
assert_eq!(psbt, pruned_psbt);
12801336
}
12811337

1338+
#[test]
1339+
fn change_detection() {
1340+
let secp = secp256k1::Secp256k1::verification_only();
1341+
1342+
// Reuse a desc from above desciptor_creation unit test and a psbt from unrelated above
1343+
// bip32_derivs_pruning unit test.
1344+
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();
1345+
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();
1346+
1347+
// The PSBT has unrelated outputs. Those aren't detected as change.
1348+
assert!(!psbt.outputs.is_empty());
1349+
assert_eq!(desc.change_indexes(&psbt, &secp).len(), 0);
1350+
1351+
// Add a change output, it's correctly detected as such.
1352+
let der_desc = desc.change_descriptor().derive(999.into(), &secp);
1353+
let txo = bitcoin::TxOut {
1354+
script_pubkey: der_desc.script_pubkey(),
1355+
value: bitcoin::Amount::MAX_MONEY,
1356+
};
1357+
let psbt_out = bitcoin::psbt::Output {
1358+
bip32_derivation: der_desc.bip32_derivations(),
1359+
..Default::default()
1360+
};
1361+
psbt.unsigned_tx.output.push(txo);
1362+
psbt.outputs.push(psbt_out);
1363+
let indexes = desc.change_indexes(&psbt, &secp);
1364+
assert_eq!(indexes.len(), 1);
1365+
assert!(matches!(
1366+
indexes[0],
1367+
ChangeOutput::ChangeAddress { index: 1 }
1368+
));
1369+
1370+
// Add another change output, but to a deposit address. Both change outputs are detected.
1371+
let der_desc = desc.receive_descriptor().derive(424242.into(), &secp);
1372+
let txo = bitcoin::TxOut {
1373+
script_pubkey: der_desc.script_pubkey(),
1374+
value: bitcoin::Amount::MAX_MONEY,
1375+
};
1376+
let psbt_out = bitcoin::psbt::Output {
1377+
bip32_derivation: der_desc.bip32_derivations(),
1378+
..Default::default()
1379+
};
1380+
psbt.unsigned_tx.output.push(txo);
1381+
psbt.outputs.push(psbt_out);
1382+
let indexes = desc.change_indexes(&psbt, &secp);
1383+
assert_eq!(indexes.len(), 2);
1384+
assert!(matches!(
1385+
indexes[1],
1386+
ChangeOutput::DepositAddress { index: 2 }
1387+
));
1388+
}
1389+
12821390
// TODO: test error conditions of deserialization.
12831391
}

0 commit comments

Comments
 (0)