Skip to content

Add support for custom sorting and deprecate BIP69 #556

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Removed default verification from `wallet::sync`. sync-time verification is added in `script_sync` and is activated by `verify` feature flag.
- `verify` flag removed from `TransactionDetails`.
- Deprecated `TxOrdering::Bip69Lexicographic` and added `TxOrdering::Custom`.

## [v0.16.1] - [v0.16.0]

Expand Down
1 change: 1 addition & 0 deletions src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2145,6 +2145,7 @@ pub(crate) mod test {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
#[allow(deprecated)]
builder
.add_recipient(addr.script_pubkey(), 30_000)
.add_recipient(addr.script_pubkey(), 10_000)
Expand Down
36 changes: 34 additions & 2 deletions src/wallet/tx_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,14 @@
//! # Ok::<(), bdk::Error>(())
//! ```

use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::collections::HashSet;
use std::default::Default;
use std::marker::PhantomData;
use std::sync::Arc;

use bitcoin::blockdata::transaction::{TxIn, TxOut};
use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt};
use bitcoin::{OutPoint, Script, SigHashType, Transaction};

Expand Down Expand Up @@ -646,14 +649,22 @@ impl<'a, B, D: BatchDatabase> TxBuilder<'a, B, D, DefaultCoinSelectionAlgorithm,
}

/// Ordering of the transaction's inputs and outputs
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
#[derive(Clone)]
pub enum TxOrdering {
/// Randomized (default)
Shuffle,
/// Unchanged
Untouched,
/// BIP69 / Lexicographic
#[deprecated = "BIP69 does not improve privacy as was the intention of the BIP"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if we can, but if possible I would add a link to the discussion here on github. Maybe you could make a multiline warning, first line stays the same, and in a new line something like "To know more read the discussion at: ...".

Similar to what the rust compiler does when you have an error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@afilini Can you give me an example in the rust lang? I can just look at their source code and implement the same way.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant something like:

warning: the feature `native_link_modifiers` is incomplete and may not be safe to use and/or cause compiler crashes
 --> library/unwind/src/lib.rs:8:12
  |
8 | #![feature(native_link_modifiers)]
  |            ^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(incomplete_features)]` on by default
  = note: see issue #81490 <https://github.com/rust-lang/rust/issues/81490> for more information

I like that "note: see issue ..." at the end

Bip69Lexicographic,
/// Provide custom comparison functions for sorting
Custom {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we may have to add some sort of "state" here, something that the two functions could look at to know what to do essentially. It doesn't have to be mutable, that would mess a lot of this up.

If we don't wanna add generic types we could maybe add a Box<dyn std::any::Any>, which you can then downcast to whatever you need in the functions. Obviously you would also have to add that as an extra argument to your functions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CC @LLFourn who proposed sorting with a shared secret. I think you would not be able to do that, unless you introduce this state.

I'm not sure what happens if you try borrowing from local variables in the two Fns, I'm guessing you are not allowed to do that because of lifetime issues. Maybe we should test this first, actually.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fadedcoder can you write a doc comment or a test which maybe demos how to use a secret to hash the input/output to produce the key?

Given the awkward Arc thing it might be better to have a convenience method like set_custom_ordering(i_f: impl Fn(&Txin...), o_f: impl Fn(&TxOut,..)) which would wrap the Arcs around them.

I think we may have to add some sort of "state" here, something that the two functions could look at to know what to do essentially. It doesn't have to be mutable, that would mess a lot of this up.

Just move in what you need?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just move in what you need?

For some reason I always instinctively think that move will only work with FnOnce because I feel like the value would be consumed at the first iteration.

So yeah, move probably works, but as you said it would be nice to have an example and more tests to verify this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah sure, I'll add some tests or examples soon!

/// Transaction inputs sort function
input_sort: Arc<dyn Fn(&TxIn, &TxIn) -> Ordering>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very minor nit, but this confused me for a second: I would just import std::cmp and then here use cmp::Ordering.

For a second I confused Ordering with TxOrdering and I couldn't figure out what was going on.

/// Transaction outputs sort function
output_sort: Arc<dyn Fn(&TxOut, &TxOut) -> Ordering>,
},
}

impl Default for TxOrdering {
Expand All @@ -662,6 +673,18 @@ impl Default for TxOrdering {
}
}

impl std::fmt::Debug for TxOrdering {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
TxOrdering::Shuffle => write!(f, "Randomized"),
TxOrdering::Untouched => write!(f, "Unchanged"),
#[allow(deprecated)]
TxOrdering::Bip69Lexicographic => write!(f, "BIP69 / Lexicographic"),
TxOrdering::Custom { .. } => write!(f, "Custom"),
}
}
}

impl TxOrdering {
/// Sort transaction inputs and outputs by [`TxOrdering`] variant
pub fn sort_tx(&self, tx: &mut Transaction) {
Expand All @@ -679,13 +702,21 @@ impl TxOrdering {

tx.output.shuffle(&mut rng);
}
#[allow(deprecated)]
TxOrdering::Bip69Lexicographic => {
tx.input.sort_unstable_by_key(|txin| {
(txin.previous_output.txid, txin.previous_output.vout)
});
tx.output
.sort_unstable_by_key(|txout| (txout.value, txout.script_pubkey.clone()));
}
TxOrdering::Custom {
input_sort,
output_sort,
} => {
tx.input.sort_unstable_by(|a, b| input_sort(a, b));
tx.output.sort_unstable_by(|a, b| output_sort(a, b));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 I think you want sort_by_cached_key here since you might do some expensive computation why computing the key e.g. hashing.

}
}
}
}
Expand Down Expand Up @@ -769,7 +800,7 @@ mod test {

#[test]
fn test_output_ordering_default_shuffle() {
assert_eq!(TxOrdering::default(), TxOrdering::Shuffle);
assert!(std::matches!(TxOrdering::default(), TxOrdering::Shuffle));
}

#[test]
Expand Down Expand Up @@ -800,6 +831,7 @@ mod test {
let original_tx = ordering_test_tx!();
let mut tx = original_tx;

#[allow(deprecated)]
TxOrdering::Bip69Lexicographic.sort_tx(&mut tx);

assert_eq!(
Expand Down