AddressIndex improvements: LastUnused, FirstUnused, and get_batch_unused_addresses() - #546

Closed
nickfarrow wants to merge 5 commits into
bitcoindevkit:masterfrom
nickfarrow:first-unused
Closed

AddressIndex improvements: LastUnused, FirstUnused, and get_batch_unused_addresses()#546
nickfarrow wants to merge 5 commits into
bitcoindevkit:masterfrom
nickfarrow:first-unused

Conversation

@nickfarrow

@nickfarrownickfarrow commented Feb 16, 2022

Copy link
Copy Markdown
Contributor

Description

  • Change AddressIndex::LastUnused to look back further than current_index
  • Add AddressIndex::FirstUnused
  • Add get_batch_unused_addresses

Notes to the reviewers

Builds upon #522

Currently BDK supports address indexing via LastUnused, which will return the address with current_index if it is unused, otherwise it will return a New address.

With this current logic, if you get two new addresses A1 and A2 and use A2, then LastUnused will give you a New address rather than the unused A1.

In order to more consistently utilize unused addresses i've added a new function get_unused_key_indexes(keychain) which returns a vector of indexes for the unused addresses in that keychain. Making use of this function, LastUnused now returns the most recent address that has not yet been used, and New if all addresses have been used.

In some cases it may be desirable to utilize unused addresses that reside earlier in the keychain. i.e. AddressIndex::FirstUnused in this PR.

FirstUnused has the same caveat as LastUnused: that if the wallet has not yet detected an address has been used, it could return a used address.

Additionally a new public function get_batch_unused_addresses allows for retrieval of N unused addresses at once. Prioritizing unused addresses first, then populating the remaining with New addresses (like FirstUnused).

For example: if a wallet builds a transaction involving many New internal addresses but that transaction is never broadcast, then all of these addresses can now easily be used in a later transaction via get_batch_unused_addresses.

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • I've added tests for the new feature
  • I've added docs for the new feature
  • I've updated CHANGELOG.md

@LLFourn

Copy link
Copy Markdown
Collaborator

approach ACK. Needs rebase.

@nickfarrow
nickfarrowforce-pushed the first-unused branch 2 times, most recently from 1f4da05 to 8bf61deCompareMarch 17, 2022 07:33
@notmandatory
notmandatory self-requested a review April 4, 2022 05:38
@notmandatorynotmandatory added the new feature New feature or request label Apr 4, 2022
@notmandatory

Copy link
Copy Markdown
Member

Code changes look great but you'll need to do another rebase and can you add a signing key to Github and sign your commits when you do the rebase also?

@nickfarrow
nickfarrowforce-pushed the first-unused branch 4 times, most recently from f8875d8 to 05aeb23CompareApril 10, 2022 07:06
@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

force push updated CI

@rajarshimaitrarajarshimaitra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

tACK 05aeb23

Below are some suggested modification..

Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment on lines +3993 to +3956
// use the first address
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);

assert_eq!(
wallet.get_address(FirstUnused).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would like to see the test situation here where we extract multiple addresses, use some of them and get back a previous unused one when called again.. That would correctly test the intended behavior.. Right now its just testing the vanilla situation..

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hmm I actually don't know a better test to write than this one? With the batch unused you can write a more complicated test but with FristUnused there's not much you can do.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Something like derive a bunch of address.. Only use some of them selectively so the address gaps are simulated.. Then check if the first unused is returned correctly.. Am I missing some details why that can't work??

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It can work but I don't get why the gaps would effect the algorithm that finds the first unused. I mean I don't think that this will likely find a problem with the algorithm that this test wouldn't find.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Its not that the gaps would affect the algorithm, but to confirm that the behavior we are intending here is actually happening.. And this can be checked in single test for both first and last unused.. Once the behavior is pinned, we can decide later which one to use when or to keep both..

@LLFournLLFourn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @nickfarrow. Tests LGTM. See comments.

Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs
Comment on lines +3993 to +3956
// use the first address
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);

assert_eq!(
wallet.get_address(FirstUnused).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hmm I actually don't know a better test to write than this one? With the batch unused you can write a more complicated test but with FristUnused there's not much you can do.

@notmandatory

Copy link
Copy Markdown
Member

Hi, please rebase to pickup changes in #596. Thanks!

Signed-off-by: nickfarrow <nick@nickfarrow.com>
Signed-off-by: nickfarrow <nick@nickfarrow.com>
Signed-off-by: nickfarrow <nick@nickfarrow.com>
* get_batch_unused_addresses loops through address indexes `from_front = true` (for firstUnused) or `false` (for lastUnused).
* Relies on database having up to date script_pubkeys in such a manner
that script_pks.len() == self.fetch_index(keychain)
* 1 script pubkey per address index?
* Must work with current_address_index = 0
Signed-off-by: nickfarrow <nick@nickfarrow.com>
Comment threadsrc/wallet/mod.rs Outdated
.list_transactions(true)?
.iter()
// Return whether this address has been used in a transaction
fn has_address_been_used(&self, script_pk: &Script) -> bool {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

actually checking a Script..

Comment threadsrc/wallet/mod.rs Outdated
let check_indexes = if from_front {
(0..=current_address_index).collect::<Vec<_>>()
} else {
(0..=current_address_index).rev().collect::<Vec<_>>()

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

better way to do this?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Return a impl DoubleEndedIterator from the method instead.

@rajarshimaitrarajarshimaitra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am still not comfortable with the current state and approach of the PR. Some behaviour are not maintained, like get_batch(n, false, keychain) will give the list of addresses in reverse order. Not in ascending order of indexes.

Also I am thinking isn't it better to mark used addresses directly in the database? Knowing which one is used and not and saving the data seems to me more useful than figuring it out by transaction matching with the entire tx list, everytime we ask for an unused..

This will also simplify the LastUnused and FirstUnused fetching logic..

@LLFourn@afilini do you have any thought on this??

Comment threadsrc/wallet/mod.rs Outdated

/// Return vector of n unused addresses from the [`KeychainKind`].
/// If less than n unused addresses are returned, the rest will be populated by new addresses.
/// The unused addresses returned are in order of oldest in keychain first, with increasing index.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is not as per impl right now.. if from_front is set false, the addresses are returned in reverse order..

Comment threadsrc/wallet/mod.rs Outdated
.list_transactions(true)?
.iter()
// Return whether this address has been used in a transaction
fn has_address_been_used(&self, script_pk: &Script) -> bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This guy is better named as is_scriptpubkey_used..

Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +393 to +398
pub fn get_batch_unused_addresses(
&self,
n: usize,
from_front: bool,
keychain: KeychainKind,
) -> Result<Vec<AddressInfo>, Error> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am not feeling comfortable with the API. get_batched_unused should not be concerned with fornt or back. Thats an impl detail for LastUnused or FirstUnused. And should not be exposed in public API..

This is also breaking the doc above. The order is not maintained anymore..

Better to handle the handle the first or last logic in in their respective functions itself than to handle in the batch function which is more generic.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The correct thing is to return a impl DoubleEndedIterator over unused addresses I think.

Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +413 to +438
for i in check_indexes {
// if we have made a pubkey at this index, check whether the address has been used.
if i < script_pubkeys.len() {
let script_pk = &script_pubkeys[i];
if self.has_address_been_used(script_pk) {
continue;
}
}
if let Ok(unused_address) = self
.get_descriptor_for_keychain(keychain)
.as_derived(i as u32, &self.secp)
.address(self.network)
.map(|address| AddressInfo {
address,
index: i as u32,
keychain,
})
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
{
unused_addresses.push(unused_address);
}

if unused_addresses.len() >= n {
break;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Try using some rust list comprehensions with iters and maps. Much of this code can be simplified..

Comment threadsrc/wallet/mod.rs
Comment on lines +3983 to +4008
assert_eq!(
wallet
.get_batch_unused_addresses(3, true, KeychainKind::External)
.unwrap(),
vec![
AddressInfo {
index: 0,
address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 2,
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 3,
address: Address::from_str("tb1q32a23q6u3yy89l8svrt80a54h06qvn7gnuvsen")
.unwrap(),
keychain: KeychainKind::External,
}
]
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also need to assert that FirstUnused and LastUnused are working as intended..

  • Get 5 new addresses
  • Use only index 0 and 3
  • get_batch(3) should return index 0, 2, 4. current index should still be at 4.
  • get_first_unused() should return 0
  • get_last_unused() should return 4
  • get_batch(4) should return 0,2,4,5, and current index should be at 5.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Adding a check of the derivation index here would be good.

Comment threadsrc/wallet/mod.rs Outdated
// if we have made a pubkey at this index, check whether the address has been used.
if i < script_pubkeys.len() {
let script_pk = &script_pubkeys[i];
if self.has_address_been_used(script_pk) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Here for each spk we are iterating over the entire transaction list. For wallets with large transaction this will can cause massive overhead.

Instead a better way would be to handle Vec<Script> in the has_address_been_used function. Call list_transactions only once, filter out all the spks that haven't been used and return then as a Vec.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yep good idea, note this would then also check more addresses than necessary (not breaking early when finding n).
If this was stored in the database it could also break early if it's just a fast read

@rajarshimaitra

Copy link
Copy Markdown
Contributor

@nickfarrow also try to rebase on top of master instead of fetching and merging specific commit next time.. :)
That makes the commit history much cleaner and also applies your changes on top of current master..
just do git rebase master from the PR branch..

@notmandatory

Copy link
Copy Markdown
Member

I had to push this PR to the next release so the team can focus on #593, and after that one you'll probably need to rebase again. But then I promise we'll work on getting this one in. :-)

@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

Yep I think this one needs some bigger discussion first around whether it is worthwhile to mark used addresses directly in the database as @rajarshimaitra suggested.

If this were the case, this could be simplified to a function get_unused_addresses that (quickly) gets an iterator over unused addresses in the database up to the current addressindex. With FirstUnused and LastUnused addresses at either ends.

This PR's get_batch_unused_addresses (currently fetches n) would be superseded by get_unused_addresses where the user gets all the unused addresses and handles them how they desire.

Not sure what changed with old commits, possible I added signoff lines to the previous commits by mistake which may have updated them sorry. Will take care with next.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

Upon further thoughts on this I have this rough idea of how it can be done:

  • Add a new column in the script_pubkey table named used. Which will have binary value 0/1, defaulting to 0.
  • At each sync when we add utxos to the Utxo table, mark the pubkey as used too in the script_pubkey table.
  • Make a generic get_batch_unused() that will return a list of unused addresses, checking the used flag in the database.
  • Make FirstUnused as the front pop of the list, and LastUnused as the back pop of the list..

Pro:
Much more scalable than transaction list scanning for wallets with large list of transactions.

Cons:
This is going to change the DB structure and the BatchDatabase API.

I am willing to work on fleshing an impl out if this has Approach Acks..

@afilini

Copy link
Copy Markdown
Member

Keep in mind we also have the key/value db, we don't have tables and columns there. We can add a flag to mark a script as used (similarly to how I suggested adding a flag for scripts that we've already setup rather than relying on just the derivation index), but getting the list of unused addresses will still require scanning

@LLFournLLFourn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This PR is almost ready and provides something that is pretty useful. The main bits of work here are:

  1. To restore the previous (someone nonsensical) behaviour of LastUnused
  2. To add tests to check the wallets derivation index is correct after calling batch unused (can just try and get a new address after and check its index)

As @rajarshimaitra mentions the best way to implement is to index things properly which in bdk is currently done in the database backend. In bdk_core I've done indexing of unused addresses. Since I was the one who requested this feature and I'm focused on bdk_core we could simply close this PR and wait until it lands. Does anyone else want this feature presently? @nickfarrow what do you think?

Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +393 to +398
pub fn get_batch_unused_addresses(
&self,
n: usize,
from_front: bool,
keychain: KeychainKind,
) -> Result<Vec<AddressInfo>, Error> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The correct thing is to return a impl DoubleEndedIterator over unused addresses I think.

Comment threadsrc/wallet/mod.rs Outdated
let check_indexes = if from_front {
(0..=current_address_index).collect::<Vec<_>>()
} else {
(0..=current_address_index).rev().collect::<Vec<_>>()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Return a impl DoubleEndedIterator from the method instead.

Comment threadsrc/wallet/mod.rs
Comment on lines +3983 to +4008
assert_eq!(
wallet
.get_batch_unused_addresses(3, true, KeychainKind::External)
.unwrap(),
vec![
AddressInfo {
index: 0,
address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 2,
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 3,
address: Address::from_str("tb1q32a23q6u3yy89l8svrt80a54h06qvn7gnuvsen")
.unwrap(),
keychain: KeychainKind::External,
}
]
);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Adding a check of the derivation index here would be good.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

ACK on @LLFourn that this can go in as it is without much further changes.. I will check the behavior once again.. If this lands through bdk_core eventually then we might not wanna do the DataBase way for now, and just keep things simple..

* remove batch getting `n` unused addresses, just get them all (much
simpler)
* use next_back() and next() for last and first unused
* test more cases for get_unused_address_indexes
* inline functions and simplified next addr
* create HashSet of txn scripts before checking unused
* add firstunused testcase for repeated unused
@notmandatory

Copy link
Copy Markdown
Member

Is this one OK to add this to the 0.22.0 milestone? looks like it's about ready and I don't want it to be overlooked.

@LLFourn

Copy link
Copy Markdown
Collaborator

IMO this PR is suboptimal because of #701. I think it should be fixed first to make the code in this PR make sense. @nickfarrow?

@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

Yep agree it may as well wait for improvements to fetch_index, that function is relied upon a few times here

@danielabrozzoni

Copy link
Copy Markdown
Contributor

Hey, we are in the process of releasing BDK 1.0, which will under the hood work quite differently from the current BDK. For this reason, I'm closing all the PRs that don't really apply anymore. If you think this is a mistake, feel free to rebase on master and re-open!

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new featureNew feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@nickfarrow@LLFourn@notmandatory@rajarshimaitra@afilini@danielabrozzoni
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

AddressIndex improvements: LastUnused, FirstUnused, and get_batch_unused_addresses() - #546

Closed
nickfarrow wants to merge 5 commits into
bitcoindevkit:masterfrom
nickfarrow:first-unused
Closed

AddressIndex improvements: LastUnused, FirstUnused, and get_batch_unused_addresses()#546
nickfarrow wants to merge 5 commits into
bitcoindevkit:masterfrom
nickfarrow:first-unused

Conversation

@nickfarrow

@nickfarrownickfarrow commented Feb 16, 2022

Copy link
Copy Markdown
Contributor

Description

  • Change AddressIndex::LastUnused to look back further than current_index
  • Add AddressIndex::FirstUnused
  • Add get_batch_unused_addresses

Notes to the reviewers

Builds upon #522

Currently BDK supports address indexing via LastUnused, which will return the address with current_index if it is unused, otherwise it will return a New address.

With this current logic, if you get two new addresses A1 and A2 and use A2, then LastUnused will give you a New address rather than the unused A1.

In order to more consistently utilize unused addresses i've added a new function get_unused_key_indexes(keychain) which returns a vector of indexes for the unused addresses in that keychain. Making use of this function, LastUnused now returns the most recent address that has not yet been used, and New if all addresses have been used.

In some cases it may be desirable to utilize unused addresses that reside earlier in the keychain. i.e. AddressIndex::FirstUnused in this PR.

FirstUnused has the same caveat as LastUnused: that if the wallet has not yet detected an address has been used, it could return a used address.

Additionally a new public function get_batch_unused_addresses allows for retrieval of N unused addresses at once. Prioritizing unused addresses first, then populating the remaining with New addresses (like FirstUnused).

For example: if a wallet builds a transaction involving many New internal addresses but that transaction is never broadcast, then all of these addresses can now easily be used in a later transaction via get_batch_unused_addresses.

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • I've added tests for the new feature
  • I've added docs for the new feature
  • I've updated CHANGELOG.md

@LLFourn

Copy link
Copy Markdown
Collaborator

approach ACK. Needs rebase.

@nickfarrow
nickfarrowforce-pushed the first-unused branch 2 times, most recently from 1f4da05 to 8bf61deCompareMarch 17, 2022 07:33
@notmandatory
notmandatory self-requested a review April 4, 2022 05:38
@notmandatorynotmandatory added the new feature New feature or request label Apr 4, 2022
@notmandatory

Copy link
Copy Markdown
Member

Code changes look great but you'll need to do another rebase and can you add a signing key to Github and sign your commits when you do the rebase also?

@nickfarrow
nickfarrowforce-pushed the first-unused branch 4 times, most recently from f8875d8 to 05aeb23CompareApril 10, 2022 07:06
@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

force push updated CI

@rajarshimaitrarajarshimaitra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

tACK 05aeb23

Below are some suggested modification..

Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment on lines +3993 to +3956
// use the first address
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);

assert_eq!(
wallet.get_address(FirstUnused).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would like to see the test situation here where we extract multiple addresses, use some of them and get back a previous unused one when called again.. That would correctly test the intended behavior.. Right now its just testing the vanilla situation..

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hmm I actually don't know a better test to write than this one? With the batch unused you can write a more complicated test but with FristUnused there's not much you can do.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Something like derive a bunch of address.. Only use some of them selectively so the address gaps are simulated.. Then check if the first unused is returned correctly.. Am I missing some details why that can't work??

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It can work but I don't get why the gaps would effect the algorithm that finds the first unused. I mean I don't think that this will likely find a problem with the algorithm that this test wouldn't find.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Its not that the gaps would affect the algorithm, but to confirm that the behavior we are intending here is actually happening.. And this can be checked in single test for both first and last unused.. Once the behavior is pinned, we can decide later which one to use when or to keep both..

@LLFournLLFourn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @nickfarrow. Tests LGTM. See comments.

Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs
Comment on lines +3993 to +3956
// use the first address
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);

assert_eq!(
wallet.get_address(FirstUnused).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hmm I actually don't know a better test to write than this one? With the batch unused you can write a more complicated test but with FristUnused there's not much you can do.

@notmandatory

Copy link
Copy Markdown
Member

Hi, please rebase to pickup changes in #596. Thanks!

Signed-off-by: nickfarrow <nick@nickfarrow.com>
Signed-off-by: nickfarrow <nick@nickfarrow.com>
Signed-off-by: nickfarrow <nick@nickfarrow.com>
* get_batch_unused_addresses loops through address indexes `from_front = true` (for firstUnused) or `false` (for lastUnused).
* Relies on database having up to date script_pubkeys in such a manner
that script_pks.len() == self.fetch_index(keychain)
* 1 script pubkey per address index?
* Must work with current_address_index = 0
Signed-off-by: nickfarrow <nick@nickfarrow.com>
Comment threadsrc/wallet/mod.rs Outdated
.list_transactions(true)?
.iter()
// Return whether this address has been used in a transaction
fn has_address_been_used(&self, script_pk: &Script) -> bool {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

actually checking a Script..

Comment threadsrc/wallet/mod.rs Outdated
let check_indexes = if from_front {
(0..=current_address_index).collect::<Vec<_>>()
} else {
(0..=current_address_index).rev().collect::<Vec<_>>()

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

better way to do this?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Return a impl DoubleEndedIterator from the method instead.

@rajarshimaitrarajarshimaitra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am still not comfortable with the current state and approach of the PR. Some behaviour are not maintained, like get_batch(n, false, keychain) will give the list of addresses in reverse order. Not in ascending order of indexes.

Also I am thinking isn't it better to mark used addresses directly in the database? Knowing which one is used and not and saving the data seems to me more useful than figuring it out by transaction matching with the entire tx list, everytime we ask for an unused..

This will also simplify the LastUnused and FirstUnused fetching logic..

@LLFourn@afilini do you have any thought on this??

Comment threadsrc/wallet/mod.rs Outdated

/// Return vector of n unused addresses from the [`KeychainKind`].
/// If less than n unused addresses are returned, the rest will be populated by new addresses.
/// The unused addresses returned are in order of oldest in keychain first, with increasing index.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is not as per impl right now.. if from_front is set false, the addresses are returned in reverse order..

Comment threadsrc/wallet/mod.rs Outdated
.list_transactions(true)?
.iter()
// Return whether this address has been used in a transaction
fn has_address_been_used(&self, script_pk: &Script) -> bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This guy is better named as is_scriptpubkey_used..

Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +393 to +398
pub fn get_batch_unused_addresses(
&self,
n: usize,
from_front: bool,
keychain: KeychainKind,
) -> Result<Vec<AddressInfo>, Error> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am not feeling comfortable with the API. get_batched_unused should not be concerned with fornt or back. Thats an impl detail for LastUnused or FirstUnused. And should not be exposed in public API..

This is also breaking the doc above. The order is not maintained anymore..

Better to handle the handle the first or last logic in in their respective functions itself than to handle in the batch function which is more generic.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The correct thing is to return a impl DoubleEndedIterator over unused addresses I think.

Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +413 to +438
for i in check_indexes {
// if we have made a pubkey at this index, check whether the address has been used.
if i < script_pubkeys.len() {
let script_pk = &script_pubkeys[i];
if self.has_address_been_used(script_pk) {
continue;
}
}
if let Ok(unused_address) = self
.get_descriptor_for_keychain(keychain)
.as_derived(i as u32, &self.secp)
.address(self.network)
.map(|address| AddressInfo {
address,
index: i as u32,
keychain,
})
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
{
unused_addresses.push(unused_address);
}

if unused_addresses.len() >= n {
break;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Try using some rust list comprehensions with iters and maps. Much of this code can be simplified..

Comment threadsrc/wallet/mod.rs
Comment on lines +3983 to +4008
assert_eq!(
wallet
.get_batch_unused_addresses(3, true, KeychainKind::External)
.unwrap(),
vec![
AddressInfo {
index: 0,
address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 2,
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 3,
address: Address::from_str("tb1q32a23q6u3yy89l8svrt80a54h06qvn7gnuvsen")
.unwrap(),
keychain: KeychainKind::External,
}
]
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also need to assert that FirstUnused and LastUnused are working as intended..

  • Get 5 new addresses
  • Use only index 0 and 3
  • get_batch(3) should return index 0, 2, 4. current index should still be at 4.
  • get_first_unused() should return 0
  • get_last_unused() should return 4
  • get_batch(4) should return 0,2,4,5, and current index should be at 5.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Adding a check of the derivation index here would be good.

Comment threadsrc/wallet/mod.rs Outdated
// if we have made a pubkey at this index, check whether the address has been used.
if i < script_pubkeys.len() {
let script_pk = &script_pubkeys[i];
if self.has_address_been_used(script_pk) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Here for each spk we are iterating over the entire transaction list. For wallets with large transaction this will can cause massive overhead.

Instead a better way would be to handle Vec<Script> in the has_address_been_used function. Call list_transactions only once, filter out all the spks that haven't been used and return then as a Vec.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yep good idea, note this would then also check more addresses than necessary (not breaking early when finding n).
If this was stored in the database it could also break early if it's just a fast read

@rajarshimaitra

Copy link
Copy Markdown
Contributor

@nickfarrow also try to rebase on top of master instead of fetching and merging specific commit next time.. :)
That makes the commit history much cleaner and also applies your changes on top of current master..
just do git rebase master from the PR branch..

@notmandatory

Copy link
Copy Markdown
Member

I had to push this PR to the next release so the team can focus on #593, and after that one you'll probably need to rebase again. But then I promise we'll work on getting this one in. :-)

@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

Yep I think this one needs some bigger discussion first around whether it is worthwhile to mark used addresses directly in the database as @rajarshimaitra suggested.

If this were the case, this could be simplified to a function get_unused_addresses that (quickly) gets an iterator over unused addresses in the database up to the current addressindex. With FirstUnused and LastUnused addresses at either ends.

This PR's get_batch_unused_addresses (currently fetches n) would be superseded by get_unused_addresses where the user gets all the unused addresses and handles them how they desire.

Not sure what changed with old commits, possible I added signoff lines to the previous commits by mistake which may have updated them sorry. Will take care with next.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

Upon further thoughts on this I have this rough idea of how it can be done:

  • Add a new column in the script_pubkey table named used. Which will have binary value 0/1, defaulting to 0.
  • At each sync when we add utxos to the Utxo table, mark the pubkey as used too in the script_pubkey table.
  • Make a generic get_batch_unused() that will return a list of unused addresses, checking the used flag in the database.
  • Make FirstUnused as the front pop of the list, and LastUnused as the back pop of the list..

Pro:
Much more scalable than transaction list scanning for wallets with large list of transactions.

Cons:
This is going to change the DB structure and the BatchDatabase API.

I am willing to work on fleshing an impl out if this has Approach Acks..

@afilini

Copy link
Copy Markdown
Member

Keep in mind we also have the key/value db, we don't have tables and columns there. We can add a flag to mark a script as used (similarly to how I suggested adding a flag for scripts that we've already setup rather than relying on just the derivation index), but getting the list of unused addresses will still require scanning

@LLFournLLFourn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This PR is almost ready and provides something that is pretty useful. The main bits of work here are:

  1. To restore the previous (someone nonsensical) behaviour of LastUnused
  2. To add tests to check the wallets derivation index is correct after calling batch unused (can just try and get a new address after and check its index)

As @rajarshimaitra mentions the best way to implement is to index things properly which in bdk is currently done in the database backend. In bdk_core I've done indexing of unused addresses. Since I was the one who requested this feature and I'm focused on bdk_core we could simply close this PR and wait until it lands. Does anyone else want this feature presently? @nickfarrow what do you think?

Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +393 to +398
pub fn get_batch_unused_addresses(
&self,
n: usize,
from_front: bool,
keychain: KeychainKind,
) -> Result<Vec<AddressInfo>, Error> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The correct thing is to return a impl DoubleEndedIterator over unused addresses I think.

Comment threadsrc/wallet/mod.rs Outdated
let check_indexes = if from_front {
(0..=current_address_index).collect::<Vec<_>>()
} else {
(0..=current_address_index).rev().collect::<Vec<_>>()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Return a impl DoubleEndedIterator from the method instead.

Comment threadsrc/wallet/mod.rs
Comment on lines +3983 to +4008
assert_eq!(
wallet
.get_batch_unused_addresses(3, true, KeychainKind::External)
.unwrap(),
vec![
AddressInfo {
index: 0,
address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 2,
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 3,
address: Address::from_str("tb1q32a23q6u3yy89l8svrt80a54h06qvn7gnuvsen")
.unwrap(),
keychain: KeychainKind::External,
}
]
);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Adding a check of the derivation index here would be good.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

ACK on @LLFourn that this can go in as it is without much further changes.. I will check the behavior once again.. If this lands through bdk_core eventually then we might not wanna do the DataBase way for now, and just keep things simple..

* remove batch getting `n` unused addresses, just get them all (much
simpler)
* use next_back() and next() for last and first unused
* test more cases for get_unused_address_indexes
* inline functions and simplified next addr
* create HashSet of txn scripts before checking unused
* add firstunused testcase for repeated unused
@notmandatory

Copy link
Copy Markdown
Member

Is this one OK to add this to the 0.22.0 milestone? looks like it's about ready and I don't want it to be overlooked.

@LLFourn

Copy link
Copy Markdown
Collaborator

IMO this PR is suboptimal because of #701. I think it should be fixed first to make the code in this PR make sense. @nickfarrow?

@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

Yep agree it may as well wait for improvements to fetch_index, that function is relied upon a few times here

@danielabrozzoni

Copy link
Copy Markdown
Contributor

Hey, we are in the process of releasing BDK 1.0, which will under the hood work quite differently from the current BDK. For this reason, I'm closing all the PRs that don't really apply anymore. If you think this is a mistake, feel free to rebase on master and re-open!

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new featureNew feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@nickfarrow@LLFourn@notmandatory@rajarshimaitra@afilini@danielabrozzoni
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

AddressIndex improvements: LastUnused, FirstUnused, and get_batch_unused_addresses() - #546

Closed
nickfarrow wants to merge 5 commits into
bitcoindevkit:masterfrom
nickfarrow:first-unused
Closed

AddressIndex improvements: LastUnused, FirstUnused, and get_batch_unused_addresses()#546
nickfarrow wants to merge 5 commits into
bitcoindevkit:masterfrom
nickfarrow:first-unused

Conversation

@nickfarrow

@nickfarrownickfarrow commented Feb 16, 2022

Copy link
Copy Markdown
Contributor

Description

  • Change AddressIndex::LastUnused to look back further than current_index
  • Add AddressIndex::FirstUnused
  • Add get_batch_unused_addresses

Notes to the reviewers

Builds upon #522

Currently BDK supports address indexing via LastUnused, which will return the address with current_index if it is unused, otherwise it will return a New address.

With this current logic, if you get two new addresses A1 and A2 and use A2, then LastUnused will give you a New address rather than the unused A1.

In order to more consistently utilize unused addresses i've added a new function get_unused_key_indexes(keychain) which returns a vector of indexes for the unused addresses in that keychain. Making use of this function, LastUnused now returns the most recent address that has not yet been used, and New if all addresses have been used.

In some cases it may be desirable to utilize unused addresses that reside earlier in the keychain. i.e. AddressIndex::FirstUnused in this PR.

FirstUnused has the same caveat as LastUnused: that if the wallet has not yet detected an address has been used, it could return a used address.

Additionally a new public function get_batch_unused_addresses allows for retrieval of N unused addresses at once. Prioritizing unused addresses first, then populating the remaining with New addresses (like FirstUnused).

For example: if a wallet builds a transaction involving many New internal addresses but that transaction is never broadcast, then all of these addresses can now easily be used in a later transaction via get_batch_unused_addresses.

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • I've added tests for the new feature
  • I've added docs for the new feature
  • I've updated CHANGELOG.md

@LLFourn

Copy link
Copy Markdown
Collaborator

approach ACK. Needs rebase.

@nickfarrow
nickfarrowforce-pushed the first-unused branch 2 times, most recently from 1f4da05 to 8bf61deCompareMarch 17, 2022 07:33
@notmandatory
notmandatory self-requested a review April 4, 2022 05:38
@notmandatorynotmandatory added the new feature New feature or request label Apr 4, 2022
@notmandatory

Copy link
Copy Markdown
Member

Code changes look great but you'll need to do another rebase and can you add a signing key to Github and sign your commits when you do the rebase also?

@nickfarrow
nickfarrowforce-pushed the first-unused branch 4 times, most recently from f8875d8 to 05aeb23CompareApril 10, 2022 07:06
@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

force push updated CI

@rajarshimaitrarajarshimaitra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

tACK 05aeb23

Below are some suggested modification..

Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment on lines +3993 to +3956
// use the first address
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);

assert_eq!(
wallet.get_address(FirstUnused).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would like to see the test situation here where we extract multiple addresses, use some of them and get back a previous unused one when called again.. That would correctly test the intended behavior.. Right now its just testing the vanilla situation..

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hmm I actually don't know a better test to write than this one? With the batch unused you can write a more complicated test but with FristUnused there's not much you can do.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Something like derive a bunch of address.. Only use some of them selectively so the address gaps are simulated.. Then check if the first unused is returned correctly.. Am I missing some details why that can't work??

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It can work but I don't get why the gaps would effect the algorithm that finds the first unused. I mean I don't think that this will likely find a problem with the algorithm that this test wouldn't find.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Its not that the gaps would affect the algorithm, but to confirm that the behavior we are intending here is actually happening.. And this can be checked in single test for both first and last unused.. Once the behavior is pinned, we can decide later which one to use when or to keep both..

@LLFournLLFourn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @nickfarrow. Tests LGTM. See comments.

Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs
Comment on lines +3993 to +3956
// use the first address
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);

assert_eq!(
wallet.get_address(FirstUnused).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hmm I actually don't know a better test to write than this one? With the batch unused you can write a more complicated test but with FristUnused there's not much you can do.

@notmandatory

Copy link
Copy Markdown
Member

Hi, please rebase to pickup changes in #596. Thanks!

Signed-off-by: nickfarrow <nick@nickfarrow.com>
Signed-off-by: nickfarrow <nick@nickfarrow.com>
Signed-off-by: nickfarrow <nick@nickfarrow.com>
* get_batch_unused_addresses loops through address indexes `from_front = true` (for firstUnused) or `false` (for lastUnused).
* Relies on database having up to date script_pubkeys in such a manner
that script_pks.len() == self.fetch_index(keychain)
* 1 script pubkey per address index?
* Must work with current_address_index = 0
Signed-off-by: nickfarrow <nick@nickfarrow.com>
Comment threadsrc/wallet/mod.rs Outdated
.list_transactions(true)?
.iter()
// Return whether this address has been used in a transaction
fn has_address_been_used(&self, script_pk: &Script) -> bool {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

actually checking a Script..

Comment threadsrc/wallet/mod.rs Outdated
let check_indexes = if from_front {
(0..=current_address_index).collect::<Vec<_>>()
} else {
(0..=current_address_index).rev().collect::<Vec<_>>()

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

better way to do this?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Return a impl DoubleEndedIterator from the method instead.

@rajarshimaitrarajarshimaitra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am still not comfortable with the current state and approach of the PR. Some behaviour are not maintained, like get_batch(n, false, keychain) will give the list of addresses in reverse order. Not in ascending order of indexes.

Also I am thinking isn't it better to mark used addresses directly in the database? Knowing which one is used and not and saving the data seems to me more useful than figuring it out by transaction matching with the entire tx list, everytime we ask for an unused..

This will also simplify the LastUnused and FirstUnused fetching logic..

@LLFourn@afilini do you have any thought on this??

Comment threadsrc/wallet/mod.rs Outdated

/// Return vector of n unused addresses from the [`KeychainKind`].
/// If less than n unused addresses are returned, the rest will be populated by new addresses.
/// The unused addresses returned are in order of oldest in keychain first, with increasing index.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is not as per impl right now.. if from_front is set false, the addresses are returned in reverse order..

Comment threadsrc/wallet/mod.rs Outdated
.list_transactions(true)?
.iter()
// Return whether this address has been used in a transaction
fn has_address_been_used(&self, script_pk: &Script) -> bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This guy is better named as is_scriptpubkey_used..

Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +393 to +398
pub fn get_batch_unused_addresses(
&self,
n: usize,
from_front: bool,
keychain: KeychainKind,
) -> Result<Vec<AddressInfo>, Error> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am not feeling comfortable with the API. get_batched_unused should not be concerned with fornt or back. Thats an impl detail for LastUnused or FirstUnused. And should not be exposed in public API..

This is also breaking the doc above. The order is not maintained anymore..

Better to handle the handle the first or last logic in in their respective functions itself than to handle in the batch function which is more generic.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The correct thing is to return a impl DoubleEndedIterator over unused addresses I think.

Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +413 to +438
for i in check_indexes {
// if we have made a pubkey at this index, check whether the address has been used.
if i < script_pubkeys.len() {
let script_pk = &script_pubkeys[i];
if self.has_address_been_used(script_pk) {
continue;
}
}
if let Ok(unused_address) = self
.get_descriptor_for_keychain(keychain)
.as_derived(i as u32, &self.secp)
.address(self.network)
.map(|address| AddressInfo {
address,
index: i as u32,
keychain,
})
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
{
unused_addresses.push(unused_address);
}

if unused_addresses.len() >= n {
break;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Try using some rust list comprehensions with iters and maps. Much of this code can be simplified..

Comment threadsrc/wallet/mod.rs
Comment on lines +3983 to +4008
assert_eq!(
wallet
.get_batch_unused_addresses(3, true, KeychainKind::External)
.unwrap(),
vec![
AddressInfo {
index: 0,
address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 2,
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 3,
address: Address::from_str("tb1q32a23q6u3yy89l8svrt80a54h06qvn7gnuvsen")
.unwrap(),
keychain: KeychainKind::External,
}
]
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also need to assert that FirstUnused and LastUnused are working as intended..

  • Get 5 new addresses
  • Use only index 0 and 3
  • get_batch(3) should return index 0, 2, 4. current index should still be at 4.
  • get_first_unused() should return 0
  • get_last_unused() should return 4
  • get_batch(4) should return 0,2,4,5, and current index should be at 5.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Adding a check of the derivation index here would be good.

Comment threadsrc/wallet/mod.rs Outdated
// if we have made a pubkey at this index, check whether the address has been used.
if i < script_pubkeys.len() {
let script_pk = &script_pubkeys[i];
if self.has_address_been_used(script_pk) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Here for each spk we are iterating over the entire transaction list. For wallets with large transaction this will can cause massive overhead.

Instead a better way would be to handle Vec<Script> in the has_address_been_used function. Call list_transactions only once, filter out all the spks that haven't been used and return then as a Vec.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yep good idea, note this would then also check more addresses than necessary (not breaking early when finding n).
If this was stored in the database it could also break early if it's just a fast read

@rajarshimaitra

Copy link
Copy Markdown
Contributor

@nickfarrow also try to rebase on top of master instead of fetching and merging specific commit next time.. :)
That makes the commit history much cleaner and also applies your changes on top of current master..
just do git rebase master from the PR branch..

@notmandatory

Copy link
Copy Markdown
Member

I had to push this PR to the next release so the team can focus on #593, and after that one you'll probably need to rebase again. But then I promise we'll work on getting this one in. :-)

@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

Yep I think this one needs some bigger discussion first around whether it is worthwhile to mark used addresses directly in the database as @rajarshimaitra suggested.

If this were the case, this could be simplified to a function get_unused_addresses that (quickly) gets an iterator over unused addresses in the database up to the current addressindex. With FirstUnused and LastUnused addresses at either ends.

This PR's get_batch_unused_addresses (currently fetches n) would be superseded by get_unused_addresses where the user gets all the unused addresses and handles them how they desire.

Not sure what changed with old commits, possible I added signoff lines to the previous commits by mistake which may have updated them sorry. Will take care with next.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

Upon further thoughts on this I have this rough idea of how it can be done:

  • Add a new column in the script_pubkey table named used. Which will have binary value 0/1, defaulting to 0.
  • At each sync when we add utxos to the Utxo table, mark the pubkey as used too in the script_pubkey table.
  • Make a generic get_batch_unused() that will return a list of unused addresses, checking the used flag in the database.
  • Make FirstUnused as the front pop of the list, and LastUnused as the back pop of the list..

Pro:
Much more scalable than transaction list scanning for wallets with large list of transactions.

Cons:
This is going to change the DB structure and the BatchDatabase API.

I am willing to work on fleshing an impl out if this has Approach Acks..

@afilini

Copy link
Copy Markdown
Member

Keep in mind we also have the key/value db, we don't have tables and columns there. We can add a flag to mark a script as used (similarly to how I suggested adding a flag for scripts that we've already setup rather than relying on just the derivation index), but getting the list of unused addresses will still require scanning

@LLFournLLFourn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This PR is almost ready and provides something that is pretty useful. The main bits of work here are:

  1. To restore the previous (someone nonsensical) behaviour of LastUnused
  2. To add tests to check the wallets derivation index is correct after calling batch unused (can just try and get a new address after and check its index)

As @rajarshimaitra mentions the best way to implement is to index things properly which in bdk is currently done in the database backend. In bdk_core I've done indexing of unused addresses. Since I was the one who requested this feature and I'm focused on bdk_core we could simply close this PR and wait until it lands. Does anyone else want this feature presently? @nickfarrow what do you think?

Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +393 to +398
pub fn get_batch_unused_addresses(
&self,
n: usize,
from_front: bool,
keychain: KeychainKind,
) -> Result<Vec<AddressInfo>, Error> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The correct thing is to return a impl DoubleEndedIterator over unused addresses I think.

Comment threadsrc/wallet/mod.rs Outdated
let check_indexes = if from_front {
(0..=current_address_index).collect::<Vec<_>>()
} else {
(0..=current_address_index).rev().collect::<Vec<_>>()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Return a impl DoubleEndedIterator from the method instead.

Comment threadsrc/wallet/mod.rs
Comment on lines +3983 to +4008
assert_eq!(
wallet
.get_batch_unused_addresses(3, true, KeychainKind::External)
.unwrap(),
vec![
AddressInfo {
index: 0,
address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 2,
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 3,
address: Address::from_str("tb1q32a23q6u3yy89l8svrt80a54h06qvn7gnuvsen")
.unwrap(),
keychain: KeychainKind::External,
}
]
);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Adding a check of the derivation index here would be good.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

ACK on @LLFourn that this can go in as it is without much further changes.. I will check the behavior once again.. If this lands through bdk_core eventually then we might not wanna do the DataBase way for now, and just keep things simple..

* remove batch getting `n` unused addresses, just get them all (much
simpler)
* use next_back() and next() for last and first unused
* test more cases for get_unused_address_indexes
* inline functions and simplified next addr
* create HashSet of txn scripts before checking unused
* add firstunused testcase for repeated unused
@notmandatory

Copy link
Copy Markdown
Member

Is this one OK to add this to the 0.22.0 milestone? looks like it's about ready and I don't want it to be overlooked.

@LLFourn

Copy link
Copy Markdown
Collaborator

IMO this PR is suboptimal because of #701. I think it should be fixed first to make the code in this PR make sense. @nickfarrow?

@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

Yep agree it may as well wait for improvements to fetch_index, that function is relied upon a few times here

@danielabrozzoni

Copy link
Copy Markdown
Contributor

Hey, we are in the process of releasing BDK 1.0, which will under the hood work quite differently from the current BDK. For this reason, I'm closing all the PRs that don't really apply anymore. If you think this is a mistake, feel free to rebase on master and re-open!

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new featureNew feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@nickfarrow@LLFourn@notmandatory@rajarshimaitra@afilini@danielabrozzoni
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

AddressIndex improvements: LastUnused, FirstUnused, and get_batch_unused_addresses() - #546

Closed
nickfarrow wants to merge 5 commits into
bitcoindevkit:masterfrom
nickfarrow:first-unused
Closed

AddressIndex improvements: LastUnused, FirstUnused, and get_batch_unused_addresses()#546
nickfarrow wants to merge 5 commits into
bitcoindevkit:masterfrom
nickfarrow:first-unused

Conversation

@nickfarrow

@nickfarrownickfarrow commented Feb 16, 2022

Copy link
Copy Markdown
Contributor

Description

  • Change AddressIndex::LastUnused to look back further than current_index
  • Add AddressIndex::FirstUnused
  • Add get_batch_unused_addresses

Notes to the reviewers

Builds upon #522

Currently BDK supports address indexing via LastUnused, which will return the address with current_index if it is unused, otherwise it will return a New address.

With this current logic, if you get two new addresses A1 and A2 and use A2, then LastUnused will give you a New address rather than the unused A1.

In order to more consistently utilize unused addresses i've added a new function get_unused_key_indexes(keychain) which returns a vector of indexes for the unused addresses in that keychain. Making use of this function, LastUnused now returns the most recent address that has not yet been used, and New if all addresses have been used.

In some cases it may be desirable to utilize unused addresses that reside earlier in the keychain. i.e. AddressIndex::FirstUnused in this PR.

FirstUnused has the same caveat as LastUnused: that if the wallet has not yet detected an address has been used, it could return a used address.

Additionally a new public function get_batch_unused_addresses allows for retrieval of N unused addresses at once. Prioritizing unused addresses first, then populating the remaining with New addresses (like FirstUnused).

For example: if a wallet builds a transaction involving many New internal addresses but that transaction is never broadcast, then all of these addresses can now easily be used in a later transaction via get_batch_unused_addresses.

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • I've added tests for the new feature
  • I've added docs for the new feature
  • I've updated CHANGELOG.md

@LLFourn

Copy link
Copy Markdown
Collaborator

approach ACK. Needs rebase.

@nickfarrow
nickfarrowforce-pushed the first-unused branch 2 times, most recently from 1f4da05 to 8bf61deCompareMarch 17, 2022 07:33
@notmandatory
notmandatory self-requested a review April 4, 2022 05:38
@notmandatorynotmandatory added the new feature New feature or request label Apr 4, 2022
@notmandatory

Copy link
Copy Markdown
Member

Code changes look great but you'll need to do another rebase and can you add a signing key to Github and sign your commits when you do the rebase also?

@nickfarrow
nickfarrowforce-pushed the first-unused branch 4 times, most recently from f8875d8 to 05aeb23CompareApril 10, 2022 07:06
@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

force push updated CI

@rajarshimaitrarajarshimaitra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

tACK 05aeb23

Below are some suggested modification..

Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment on lines +3993 to +3956
// use the first address
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);

assert_eq!(
wallet.get_address(FirstUnused).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would like to see the test situation here where we extract multiple addresses, use some of them and get back a previous unused one when called again.. That would correctly test the intended behavior.. Right now its just testing the vanilla situation..

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hmm I actually don't know a better test to write than this one? With the batch unused you can write a more complicated test but with FristUnused there's not much you can do.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Something like derive a bunch of address.. Only use some of them selectively so the address gaps are simulated.. Then check if the first unused is returned correctly.. Am I missing some details why that can't work??

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It can work but I don't get why the gaps would effect the algorithm that finds the first unused. I mean I don't think that this will likely find a problem with the algorithm that this test wouldn't find.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Its not that the gaps would affect the algorithm, but to confirm that the behavior we are intending here is actually happening.. And this can be checked in single test for both first and last unused.. Once the behavior is pinned, we can decide later which one to use when or to keep both..

@LLFournLLFourn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @nickfarrow. Tests LGTM. See comments.

Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs
Comment on lines +3993 to +3956
// use the first address
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);

assert_eq!(
wallet.get_address(FirstUnused).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hmm I actually don't know a better test to write than this one? With the batch unused you can write a more complicated test but with FristUnused there's not much you can do.

@notmandatory

Copy link
Copy Markdown
Member

Hi, please rebase to pickup changes in #596. Thanks!

Signed-off-by: nickfarrow <nick@nickfarrow.com>
Signed-off-by: nickfarrow <nick@nickfarrow.com>
Signed-off-by: nickfarrow <nick@nickfarrow.com>
* get_batch_unused_addresses loops through address indexes `from_front = true` (for firstUnused) or `false` (for lastUnused).
* Relies on database having up to date script_pubkeys in such a manner
that script_pks.len() == self.fetch_index(keychain)
* 1 script pubkey per address index?
* Must work with current_address_index = 0
Signed-off-by: nickfarrow <nick@nickfarrow.com>
Comment threadsrc/wallet/mod.rs Outdated
.list_transactions(true)?
.iter()
// Return whether this address has been used in a transaction
fn has_address_been_used(&self, script_pk: &Script) -> bool {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

actually checking a Script..

Comment threadsrc/wallet/mod.rs Outdated
let check_indexes = if from_front {
(0..=current_address_index).collect::<Vec<_>>()
} else {
(0..=current_address_index).rev().collect::<Vec<_>>()

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

better way to do this?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Return a impl DoubleEndedIterator from the method instead.

@rajarshimaitrarajarshimaitra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am still not comfortable with the current state and approach of the PR. Some behaviour are not maintained, like get_batch(n, false, keychain) will give the list of addresses in reverse order. Not in ascending order of indexes.

Also I am thinking isn't it better to mark used addresses directly in the database? Knowing which one is used and not and saving the data seems to me more useful than figuring it out by transaction matching with the entire tx list, everytime we ask for an unused..

This will also simplify the LastUnused and FirstUnused fetching logic..

@LLFourn@afilini do you have any thought on this??

Comment threadsrc/wallet/mod.rs Outdated

/// Return vector of n unused addresses from the [`KeychainKind`].
/// If less than n unused addresses are returned, the rest will be populated by new addresses.
/// The unused addresses returned are in order of oldest in keychain first, with increasing index.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is not as per impl right now.. if from_front is set false, the addresses are returned in reverse order..

Comment threadsrc/wallet/mod.rs Outdated
.list_transactions(true)?
.iter()
// Return whether this address has been used in a transaction
fn has_address_been_used(&self, script_pk: &Script) -> bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This guy is better named as is_scriptpubkey_used..

Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +393 to +398
pub fn get_batch_unused_addresses(
&self,
n: usize,
from_front: bool,
keychain: KeychainKind,
) -> Result<Vec<AddressInfo>, Error> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am not feeling comfortable with the API. get_batched_unused should not be concerned with fornt or back. Thats an impl detail for LastUnused or FirstUnused. And should not be exposed in public API..

This is also breaking the doc above. The order is not maintained anymore..

Better to handle the handle the first or last logic in in their respective functions itself than to handle in the batch function which is more generic.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The correct thing is to return a impl DoubleEndedIterator over unused addresses I think.

Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +413 to +438
for i in check_indexes {
// if we have made a pubkey at this index, check whether the address has been used.
if i < script_pubkeys.len() {
let script_pk = &script_pubkeys[i];
if self.has_address_been_used(script_pk) {
continue;
}
}
if let Ok(unused_address) = self
.get_descriptor_for_keychain(keychain)
.as_derived(i as u32, &self.secp)
.address(self.network)
.map(|address| AddressInfo {
address,
index: i as u32,
keychain,
})
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
{
unused_addresses.push(unused_address);
}

if unused_addresses.len() >= n {
break;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Try using some rust list comprehensions with iters and maps. Much of this code can be simplified..

Comment threadsrc/wallet/mod.rs
Comment on lines +3983 to +4008
assert_eq!(
wallet
.get_batch_unused_addresses(3, true, KeychainKind::External)
.unwrap(),
vec![
AddressInfo {
index: 0,
address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 2,
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 3,
address: Address::from_str("tb1q32a23q6u3yy89l8svrt80a54h06qvn7gnuvsen")
.unwrap(),
keychain: KeychainKind::External,
}
]
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also need to assert that FirstUnused and LastUnused are working as intended..

  • Get 5 new addresses
  • Use only index 0 and 3
  • get_batch(3) should return index 0, 2, 4. current index should still be at 4.
  • get_first_unused() should return 0
  • get_last_unused() should return 4
  • get_batch(4) should return 0,2,4,5, and current index should be at 5.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Adding a check of the derivation index here would be good.

Comment threadsrc/wallet/mod.rs Outdated
// if we have made a pubkey at this index, check whether the address has been used.
if i < script_pubkeys.len() {
let script_pk = &script_pubkeys[i];
if self.has_address_been_used(script_pk) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Here for each spk we are iterating over the entire transaction list. For wallets with large transaction this will can cause massive overhead.

Instead a better way would be to handle Vec<Script> in the has_address_been_used function. Call list_transactions only once, filter out all the spks that haven't been used and return then as a Vec.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yep good idea, note this would then also check more addresses than necessary (not breaking early when finding n).
If this was stored in the database it could also break early if it's just a fast read

@rajarshimaitra

Copy link
Copy Markdown
Contributor

@nickfarrow also try to rebase on top of master instead of fetching and merging specific commit next time.. :)
That makes the commit history much cleaner and also applies your changes on top of current master..
just do git rebase master from the PR branch..

@notmandatory

Copy link
Copy Markdown
Member

I had to push this PR to the next release so the team can focus on #593, and after that one you'll probably need to rebase again. But then I promise we'll work on getting this one in. :-)

@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

Yep I think this one needs some bigger discussion first around whether it is worthwhile to mark used addresses directly in the database as @rajarshimaitra suggested.

If this were the case, this could be simplified to a function get_unused_addresses that (quickly) gets an iterator over unused addresses in the database up to the current addressindex. With FirstUnused and LastUnused addresses at either ends.

This PR's get_batch_unused_addresses (currently fetches n) would be superseded by get_unused_addresses where the user gets all the unused addresses and handles them how they desire.

Not sure what changed with old commits, possible I added signoff lines to the previous commits by mistake which may have updated them sorry. Will take care with next.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

Upon further thoughts on this I have this rough idea of how it can be done:

  • Add a new column in the script_pubkey table named used. Which will have binary value 0/1, defaulting to 0.
  • At each sync when we add utxos to the Utxo table, mark the pubkey as used too in the script_pubkey table.
  • Make a generic get_batch_unused() that will return a list of unused addresses, checking the used flag in the database.
  • Make FirstUnused as the front pop of the list, and LastUnused as the back pop of the list..

Pro:
Much more scalable than transaction list scanning for wallets with large list of transactions.

Cons:
This is going to change the DB structure and the BatchDatabase API.

I am willing to work on fleshing an impl out if this has Approach Acks..

@afilini

Copy link
Copy Markdown
Member

Keep in mind we also have the key/value db, we don't have tables and columns there. We can add a flag to mark a script as used (similarly to how I suggested adding a flag for scripts that we've already setup rather than relying on just the derivation index), but getting the list of unused addresses will still require scanning

@LLFournLLFourn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This PR is almost ready and provides something that is pretty useful. The main bits of work here are:

  1. To restore the previous (someone nonsensical) behaviour of LastUnused
  2. To add tests to check the wallets derivation index is correct after calling batch unused (can just try and get a new address after and check its index)

As @rajarshimaitra mentions the best way to implement is to index things properly which in bdk is currently done in the database backend. In bdk_core I've done indexing of unused addresses. Since I was the one who requested this feature and I'm focused on bdk_core we could simply close this PR and wait until it lands. Does anyone else want this feature presently? @nickfarrow what do you think?

Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +393 to +398
pub fn get_batch_unused_addresses(
&self,
n: usize,
from_front: bool,
keychain: KeychainKind,
) -> Result<Vec<AddressInfo>, Error> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The correct thing is to return a impl DoubleEndedIterator over unused addresses I think.

Comment threadsrc/wallet/mod.rs Outdated
let check_indexes = if from_front {
(0..=current_address_index).collect::<Vec<_>>()
} else {
(0..=current_address_index).rev().collect::<Vec<_>>()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Return a impl DoubleEndedIterator from the method instead.

Comment threadsrc/wallet/mod.rs
Comment on lines +3983 to +4008
assert_eq!(
wallet
.get_batch_unused_addresses(3, true, KeychainKind::External)
.unwrap(),
vec![
AddressInfo {
index: 0,
address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 2,
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 3,
address: Address::from_str("tb1q32a23q6u3yy89l8svrt80a54h06qvn7gnuvsen")
.unwrap(),
keychain: KeychainKind::External,
}
]
);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Adding a check of the derivation index here would be good.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

ACK on @LLFourn that this can go in as it is without much further changes.. I will check the behavior once again.. If this lands through bdk_core eventually then we might not wanna do the DataBase way for now, and just keep things simple..

* remove batch getting `n` unused addresses, just get them all (much
simpler)
* use next_back() and next() for last and first unused
* test more cases for get_unused_address_indexes
* inline functions and simplified next addr
* create HashSet of txn scripts before checking unused
* add firstunused testcase for repeated unused
@notmandatory

Copy link
Copy Markdown
Member

Is this one OK to add this to the 0.22.0 milestone? looks like it's about ready and I don't want it to be overlooked.

@LLFourn

Copy link
Copy Markdown
Collaborator

IMO this PR is suboptimal because of #701. I think it should be fixed first to make the code in this PR make sense. @nickfarrow?

@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

Yep agree it may as well wait for improvements to fetch_index, that function is relied upon a few times here

@danielabrozzoni

Copy link
Copy Markdown
Contributor

Hey, we are in the process of releasing BDK 1.0, which will under the hood work quite differently from the current BDK. For this reason, I'm closing all the PRs that don't really apply anymore. If you think this is a mistake, feel free to rebase on master and re-open!

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new featureNew feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@nickfarrow@LLFourn@notmandatory@rajarshimaitra@afilini@danielabrozzoni
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

AddressIndex improvements: LastUnused, FirstUnused, and get_batch_unused_addresses() - #546

Closed
nickfarrow wants to merge 5 commits into
bitcoindevkit:masterfrom
nickfarrow:first-unused
Closed

AddressIndex improvements: LastUnused, FirstUnused, and get_batch_unused_addresses()#546
nickfarrow wants to merge 5 commits into
bitcoindevkit:masterfrom
nickfarrow:first-unused

Conversation

@nickfarrow

@nickfarrownickfarrow commented Feb 16, 2022

Copy link
Copy Markdown
Contributor

Description

  • Change AddressIndex::LastUnused to look back further than current_index
  • Add AddressIndex::FirstUnused
  • Add get_batch_unused_addresses

Notes to the reviewers

Builds upon #522

Currently BDK supports address indexing via LastUnused, which will return the address with current_index if it is unused, otherwise it will return a New address.

With this current logic, if you get two new addresses A1 and A2 and use A2, then LastUnused will give you a New address rather than the unused A1.

In order to more consistently utilize unused addresses i've added a new function get_unused_key_indexes(keychain) which returns a vector of indexes for the unused addresses in that keychain. Making use of this function, LastUnused now returns the most recent address that has not yet been used, and New if all addresses have been used.

In some cases it may be desirable to utilize unused addresses that reside earlier in the keychain. i.e. AddressIndex::FirstUnused in this PR.

FirstUnused has the same caveat as LastUnused: that if the wallet has not yet detected an address has been used, it could return a used address.

Additionally a new public function get_batch_unused_addresses allows for retrieval of N unused addresses at once. Prioritizing unused addresses first, then populating the remaining with New addresses (like FirstUnused).

For example: if a wallet builds a transaction involving many New internal addresses but that transaction is never broadcast, then all of these addresses can now easily be used in a later transaction via get_batch_unused_addresses.

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • I've added tests for the new feature
  • I've added docs for the new feature
  • I've updated CHANGELOG.md

@LLFourn

Copy link
Copy Markdown
Collaborator

approach ACK. Needs rebase.

@nickfarrow
nickfarrowforce-pushed the first-unused branch 2 times, most recently from 1f4da05 to 8bf61deCompareMarch 17, 2022 07:33
@notmandatory
notmandatory self-requested a review April 4, 2022 05:38
@notmandatorynotmandatory added the new feature New feature or request label Apr 4, 2022
@notmandatory

Copy link
Copy Markdown
Member

Code changes look great but you'll need to do another rebase and can you add a signing key to Github and sign your commits when you do the rebase also?

@nickfarrow
nickfarrowforce-pushed the first-unused branch 4 times, most recently from f8875d8 to 05aeb23CompareApril 10, 2022 07:06
@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

force push updated CI

@rajarshimaitrarajarshimaitra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

tACK 05aeb23

Below are some suggested modification..

Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment on lines +3993 to +3956
// use the first address
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);

assert_eq!(
wallet.get_address(FirstUnused).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would like to see the test situation here where we extract multiple addresses, use some of them and get back a previous unused one when called again.. That would correctly test the intended behavior.. Right now its just testing the vanilla situation..

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hmm I actually don't know a better test to write than this one? With the batch unused you can write a more complicated test but with FristUnused there's not much you can do.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Something like derive a bunch of address.. Only use some of them selectively so the address gaps are simulated.. Then check if the first unused is returned correctly.. Am I missing some details why that can't work??

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It can work but I don't get why the gaps would effect the algorithm that finds the first unused. I mean I don't think that this will likely find a problem with the algorithm that this test wouldn't find.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Its not that the gaps would affect the algorithm, but to confirm that the behavior we are intending here is actually happening.. And this can be checked in single test for both first and last unused.. Once the behavior is pinned, we can decide later which one to use when or to keep both..

@LLFournLLFourn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @nickfarrow. Tests LGTM. See comments.

Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs
Comment on lines +3993 to +3956
// use the first address
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);

assert_eq!(
wallet.get_address(FirstUnused).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hmm I actually don't know a better test to write than this one? With the batch unused you can write a more complicated test but with FristUnused there's not much you can do.

@notmandatory

Copy link
Copy Markdown
Member

Hi, please rebase to pickup changes in #596. Thanks!

Signed-off-by: nickfarrow <nick@nickfarrow.com>
Signed-off-by: nickfarrow <nick@nickfarrow.com>
Signed-off-by: nickfarrow <nick@nickfarrow.com>
* get_batch_unused_addresses loops through address indexes `from_front = true` (for firstUnused) or `false` (for lastUnused).
* Relies on database having up to date script_pubkeys in such a manner
that script_pks.len() == self.fetch_index(keychain)
* 1 script pubkey per address index?
* Must work with current_address_index = 0
Signed-off-by: nickfarrow <nick@nickfarrow.com>
Comment threadsrc/wallet/mod.rs Outdated
.list_transactions(true)?
.iter()
// Return whether this address has been used in a transaction
fn has_address_been_used(&self, script_pk: &Script) -> bool {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

actually checking a Script..

Comment threadsrc/wallet/mod.rs Outdated
let check_indexes = if from_front {
(0..=current_address_index).collect::<Vec<_>>()
} else {
(0..=current_address_index).rev().collect::<Vec<_>>()

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

better way to do this?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Return a impl DoubleEndedIterator from the method instead.

@rajarshimaitrarajarshimaitra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am still not comfortable with the current state and approach of the PR. Some behaviour are not maintained, like get_batch(n, false, keychain) will give the list of addresses in reverse order. Not in ascending order of indexes.

Also I am thinking isn't it better to mark used addresses directly in the database? Knowing which one is used and not and saving the data seems to me more useful than figuring it out by transaction matching with the entire tx list, everytime we ask for an unused..

This will also simplify the LastUnused and FirstUnused fetching logic..

@LLFourn@afilini do you have any thought on this??

Comment threadsrc/wallet/mod.rs Outdated

/// Return vector of n unused addresses from the [`KeychainKind`].
/// If less than n unused addresses are returned, the rest will be populated by new addresses.
/// The unused addresses returned are in order of oldest in keychain first, with increasing index.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is not as per impl right now.. if from_front is set false, the addresses are returned in reverse order..

Comment threadsrc/wallet/mod.rs Outdated
.list_transactions(true)?
.iter()
// Return whether this address has been used in a transaction
fn has_address_been_used(&self, script_pk: &Script) -> bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This guy is better named as is_scriptpubkey_used..

Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +393 to +398
pub fn get_batch_unused_addresses(
&self,
n: usize,
from_front: bool,
keychain: KeychainKind,
) -> Result<Vec<AddressInfo>, Error> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am not feeling comfortable with the API. get_batched_unused should not be concerned with fornt or back. Thats an impl detail for LastUnused or FirstUnused. And should not be exposed in public API..

This is also breaking the doc above. The order is not maintained anymore..

Better to handle the handle the first or last logic in in their respective functions itself than to handle in the batch function which is more generic.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The correct thing is to return a impl DoubleEndedIterator over unused addresses I think.

Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +413 to +438
for i in check_indexes {
// if we have made a pubkey at this index, check whether the address has been used.
if i < script_pubkeys.len() {
let script_pk = &script_pubkeys[i];
if self.has_address_been_used(script_pk) {
continue;
}
}
if let Ok(unused_address) = self
.get_descriptor_for_keychain(keychain)
.as_derived(i as u32, &self.secp)
.address(self.network)
.map(|address| AddressInfo {
address,
index: i as u32,
keychain,
})
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
{
unused_addresses.push(unused_address);
}

if unused_addresses.len() >= n {
break;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Try using some rust list comprehensions with iters and maps. Much of this code can be simplified..

Comment threadsrc/wallet/mod.rs
Comment on lines +3983 to +4008
assert_eq!(
wallet
.get_batch_unused_addresses(3, true, KeychainKind::External)
.unwrap(),
vec![
AddressInfo {
index: 0,
address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 2,
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 3,
address: Address::from_str("tb1q32a23q6u3yy89l8svrt80a54h06qvn7gnuvsen")
.unwrap(),
keychain: KeychainKind::External,
}
]
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also need to assert that FirstUnused and LastUnused are working as intended..

  • Get 5 new addresses
  • Use only index 0 and 3
  • get_batch(3) should return index 0, 2, 4. current index should still be at 4.
  • get_first_unused() should return 0
  • get_last_unused() should return 4
  • get_batch(4) should return 0,2,4,5, and current index should be at 5.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Adding a check of the derivation index here would be good.

Comment threadsrc/wallet/mod.rs Outdated
// if we have made a pubkey at this index, check whether the address has been used.
if i < script_pubkeys.len() {
let script_pk = &script_pubkeys[i];
if self.has_address_been_used(script_pk) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Here for each spk we are iterating over the entire transaction list. For wallets with large transaction this will can cause massive overhead.

Instead a better way would be to handle Vec<Script> in the has_address_been_used function. Call list_transactions only once, filter out all the spks that haven't been used and return then as a Vec.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yep good idea, note this would then also check more addresses than necessary (not breaking early when finding n).
If this was stored in the database it could also break early if it's just a fast read

@rajarshimaitra

Copy link
Copy Markdown
Contributor

@nickfarrow also try to rebase on top of master instead of fetching and merging specific commit next time.. :)
That makes the commit history much cleaner and also applies your changes on top of current master..
just do git rebase master from the PR branch..

@notmandatory

Copy link
Copy Markdown
Member

I had to push this PR to the next release so the team can focus on #593, and after that one you'll probably need to rebase again. But then I promise we'll work on getting this one in. :-)

@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

Yep I think this one needs some bigger discussion first around whether it is worthwhile to mark used addresses directly in the database as @rajarshimaitra suggested.

If this were the case, this could be simplified to a function get_unused_addresses that (quickly) gets an iterator over unused addresses in the database up to the current addressindex. With FirstUnused and LastUnused addresses at either ends.

This PR's get_batch_unused_addresses (currently fetches n) would be superseded by get_unused_addresses where the user gets all the unused addresses and handles them how they desire.

Not sure what changed with old commits, possible I added signoff lines to the previous commits by mistake which may have updated them sorry. Will take care with next.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

Upon further thoughts on this I have this rough idea of how it can be done:

  • Add a new column in the script_pubkey table named used. Which will have binary value 0/1, defaulting to 0.
  • At each sync when we add utxos to the Utxo table, mark the pubkey as used too in the script_pubkey table.
  • Make a generic get_batch_unused() that will return a list of unused addresses, checking the used flag in the database.
  • Make FirstUnused as the front pop of the list, and LastUnused as the back pop of the list..

Pro:
Much more scalable than transaction list scanning for wallets with large list of transactions.

Cons:
This is going to change the DB structure and the BatchDatabase API.

I am willing to work on fleshing an impl out if this has Approach Acks..

@afilini

Copy link
Copy Markdown
Member

Keep in mind we also have the key/value db, we don't have tables and columns there. We can add a flag to mark a script as used (similarly to how I suggested adding a flag for scripts that we've already setup rather than relying on just the derivation index), but getting the list of unused addresses will still require scanning

@LLFournLLFourn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This PR is almost ready and provides something that is pretty useful. The main bits of work here are:

  1. To restore the previous (someone nonsensical) behaviour of LastUnused
  2. To add tests to check the wallets derivation index is correct after calling batch unused (can just try and get a new address after and check its index)

As @rajarshimaitra mentions the best way to implement is to index things properly which in bdk is currently done in the database backend. In bdk_core I've done indexing of unused addresses. Since I was the one who requested this feature and I'm focused on bdk_core we could simply close this PR and wait until it lands. Does anyone else want this feature presently? @nickfarrow what do you think?

Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +393 to +398
pub fn get_batch_unused_addresses(
&self,
n: usize,
from_front: bool,
keychain: KeychainKind,
) -> Result<Vec<AddressInfo>, Error> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The correct thing is to return a impl DoubleEndedIterator over unused addresses I think.

Comment threadsrc/wallet/mod.rs Outdated
let check_indexes = if from_front {
(0..=current_address_index).collect::<Vec<_>>()
} else {
(0..=current_address_index).rev().collect::<Vec<_>>()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Return a impl DoubleEndedIterator from the method instead.

Comment threadsrc/wallet/mod.rs
Comment on lines +3983 to +4008
assert_eq!(
wallet
.get_batch_unused_addresses(3, true, KeychainKind::External)
.unwrap(),
vec![
AddressInfo {
index: 0,
address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 2,
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 3,
address: Address::from_str("tb1q32a23q6u3yy89l8svrt80a54h06qvn7gnuvsen")
.unwrap(),
keychain: KeychainKind::External,
}
]
);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Adding a check of the derivation index here would be good.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

ACK on @LLFourn that this can go in as it is without much further changes.. I will check the behavior once again.. If this lands through bdk_core eventually then we might not wanna do the DataBase way for now, and just keep things simple..

* remove batch getting `n` unused addresses, just get them all (much
simpler)
* use next_back() and next() for last and first unused
* test more cases for get_unused_address_indexes
* inline functions and simplified next addr
* create HashSet of txn scripts before checking unused
* add firstunused testcase for repeated unused
@notmandatory

Copy link
Copy Markdown
Member

Is this one OK to add this to the 0.22.0 milestone? looks like it's about ready and I don't want it to be overlooked.

@LLFourn

Copy link
Copy Markdown
Collaborator

IMO this PR is suboptimal because of #701. I think it should be fixed first to make the code in this PR make sense. @nickfarrow?

@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

Yep agree it may as well wait for improvements to fetch_index, that function is relied upon a few times here

@danielabrozzoni

Copy link
Copy Markdown
Contributor

Hey, we are in the process of releasing BDK 1.0, which will under the hood work quite differently from the current BDK. For this reason, I'm closing all the PRs that don't really apply anymore. If you think this is a mistake, feel free to rebase on master and re-open!

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new featureNew feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@nickfarrow@LLFourn@notmandatory@rajarshimaitra@afilini@danielabrozzoni
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

AddressIndex improvements: LastUnused, FirstUnused, and get_batch_unused_addresses() - #546

Closed
nickfarrow wants to merge 5 commits into
bitcoindevkit:masterfrom
nickfarrow:first-unused
Closed

AddressIndex improvements: LastUnused, FirstUnused, and get_batch_unused_addresses()#546
nickfarrow wants to merge 5 commits into
bitcoindevkit:masterfrom
nickfarrow:first-unused

Conversation

@nickfarrow

@nickfarrownickfarrow commented Feb 16, 2022

Copy link
Copy Markdown
Contributor

Description

  • Change AddressIndex::LastUnused to look back further than current_index
  • Add AddressIndex::FirstUnused
  • Add get_batch_unused_addresses

Notes to the reviewers

Builds upon #522

Currently BDK supports address indexing via LastUnused, which will return the address with current_index if it is unused, otherwise it will return a New address.

With this current logic, if you get two new addresses A1 and A2 and use A2, then LastUnused will give you a New address rather than the unused A1.

In order to more consistently utilize unused addresses i've added a new function get_unused_key_indexes(keychain) which returns a vector of indexes for the unused addresses in that keychain. Making use of this function, LastUnused now returns the most recent address that has not yet been used, and New if all addresses have been used.

In some cases it may be desirable to utilize unused addresses that reside earlier in the keychain. i.e. AddressIndex::FirstUnused in this PR.

FirstUnused has the same caveat as LastUnused: that if the wallet has not yet detected an address has been used, it could return a used address.

Additionally a new public function get_batch_unused_addresses allows for retrieval of N unused addresses at once. Prioritizing unused addresses first, then populating the remaining with New addresses (like FirstUnused).

For example: if a wallet builds a transaction involving many New internal addresses but that transaction is never broadcast, then all of these addresses can now easily be used in a later transaction via get_batch_unused_addresses.

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • I've added tests for the new feature
  • I've added docs for the new feature
  • I've updated CHANGELOG.md

@LLFourn

Copy link
Copy Markdown
Collaborator

approach ACK. Needs rebase.

@nickfarrow
nickfarrowforce-pushed the first-unused branch 2 times, most recently from 1f4da05 to 8bf61deCompareMarch 17, 2022 07:33
@notmandatory
notmandatory self-requested a review April 4, 2022 05:38
@notmandatorynotmandatory added the new feature New feature or request label Apr 4, 2022
@notmandatory

Copy link
Copy Markdown
Member

Code changes look great but you'll need to do another rebase and can you add a signing key to Github and sign your commits when you do the rebase also?

@nickfarrow
nickfarrowforce-pushed the first-unused branch 4 times, most recently from f8875d8 to 05aeb23CompareApril 10, 2022 07:06
@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

force push updated CI

@rajarshimaitrarajarshimaitra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

tACK 05aeb23

Below are some suggested modification..

Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment on lines +3993 to +3956
// use the first address
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);

assert_eq!(
wallet.get_address(FirstUnused).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would like to see the test situation here where we extract multiple addresses, use some of them and get back a previous unused one when called again.. That would correctly test the intended behavior.. Right now its just testing the vanilla situation..

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hmm I actually don't know a better test to write than this one? With the batch unused you can write a more complicated test but with FristUnused there's not much you can do.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Something like derive a bunch of address.. Only use some of them selectively so the address gaps are simulated.. Then check if the first unused is returned correctly.. Am I missing some details why that can't work??

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It can work but I don't get why the gaps would effect the algorithm that finds the first unused. I mean I don't think that this will likely find a problem with the algorithm that this test wouldn't find.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Its not that the gaps would affect the algorithm, but to confirm that the behavior we are intending here is actually happening.. And this can be checked in single test for both first and last unused.. Once the behavior is pinned, we can decide later which one to use when or to keep both..

@LLFournLLFourn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @nickfarrow. Tests LGTM. See comments.

Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs
Comment on lines +3993 to +3956
// use the first address
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);

assert_eq!(
wallet.get_address(FirstUnused).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hmm I actually don't know a better test to write than this one? With the batch unused you can write a more complicated test but with FristUnused there's not much you can do.

@notmandatory

Copy link
Copy Markdown
Member

Hi, please rebase to pickup changes in #596. Thanks!

Signed-off-by: nickfarrow <nick@nickfarrow.com>
Signed-off-by: nickfarrow <nick@nickfarrow.com>
Signed-off-by: nickfarrow <nick@nickfarrow.com>
* get_batch_unused_addresses loops through address indexes `from_front = true` (for firstUnused) or `false` (for lastUnused).
* Relies on database having up to date script_pubkeys in such a manner
that script_pks.len() == self.fetch_index(keychain)
* 1 script pubkey per address index?
* Must work with current_address_index = 0
Signed-off-by: nickfarrow <nick@nickfarrow.com>
Comment threadsrc/wallet/mod.rs Outdated
.list_transactions(true)?
.iter()
// Return whether this address has been used in a transaction
fn has_address_been_used(&self, script_pk: &Script) -> bool {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

actually checking a Script..

Comment threadsrc/wallet/mod.rs Outdated
let check_indexes = if from_front {
(0..=current_address_index).collect::<Vec<_>>()
} else {
(0..=current_address_index).rev().collect::<Vec<_>>()

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

better way to do this?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Return a impl DoubleEndedIterator from the method instead.

@rajarshimaitrarajarshimaitra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am still not comfortable with the current state and approach of the PR. Some behaviour are not maintained, like get_batch(n, false, keychain) will give the list of addresses in reverse order. Not in ascending order of indexes.

Also I am thinking isn't it better to mark used addresses directly in the database? Knowing which one is used and not and saving the data seems to me more useful than figuring it out by transaction matching with the entire tx list, everytime we ask for an unused..

This will also simplify the LastUnused and FirstUnused fetching logic..

@LLFourn@afilini do you have any thought on this??

Comment threadsrc/wallet/mod.rs Outdated

/// Return vector of n unused addresses from the [`KeychainKind`].
/// If less than n unused addresses are returned, the rest will be populated by new addresses.
/// The unused addresses returned are in order of oldest in keychain first, with increasing index.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is not as per impl right now.. if from_front is set false, the addresses are returned in reverse order..

Comment threadsrc/wallet/mod.rs Outdated
.list_transactions(true)?
.iter()
// Return whether this address has been used in a transaction
fn has_address_been_used(&self, script_pk: &Script) -> bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This guy is better named as is_scriptpubkey_used..

Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +393 to +398
pub fn get_batch_unused_addresses(
&self,
n: usize,
from_front: bool,
keychain: KeychainKind,
) -> Result<Vec<AddressInfo>, Error> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am not feeling comfortable with the API. get_batched_unused should not be concerned with fornt or back. Thats an impl detail for LastUnused or FirstUnused. And should not be exposed in public API..

This is also breaking the doc above. The order is not maintained anymore..

Better to handle the handle the first or last logic in in their respective functions itself than to handle in the batch function which is more generic.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The correct thing is to return a impl DoubleEndedIterator over unused addresses I think.

Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +413 to +438
for i in check_indexes {
// if we have made a pubkey at this index, check whether the address has been used.
if i < script_pubkeys.len() {
let script_pk = &script_pubkeys[i];
if self.has_address_been_used(script_pk) {
continue;
}
}
if let Ok(unused_address) = self
.get_descriptor_for_keychain(keychain)
.as_derived(i as u32, &self.secp)
.address(self.network)
.map(|address| AddressInfo {
address,
index: i as u32,
keychain,
})
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
{
unused_addresses.push(unused_address);
}

if unused_addresses.len() >= n {
break;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Try using some rust list comprehensions with iters and maps. Much of this code can be simplified..

Comment threadsrc/wallet/mod.rs
Comment on lines +3983 to +4008
assert_eq!(
wallet
.get_batch_unused_addresses(3, true, KeychainKind::External)
.unwrap(),
vec![
AddressInfo {
index: 0,
address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 2,
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 3,
address: Address::from_str("tb1q32a23q6u3yy89l8svrt80a54h06qvn7gnuvsen")
.unwrap(),
keychain: KeychainKind::External,
}
]
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also need to assert that FirstUnused and LastUnused are working as intended..

  • Get 5 new addresses
  • Use only index 0 and 3
  • get_batch(3) should return index 0, 2, 4. current index should still be at 4.
  • get_first_unused() should return 0
  • get_last_unused() should return 4
  • get_batch(4) should return 0,2,4,5, and current index should be at 5.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Adding a check of the derivation index here would be good.

Comment threadsrc/wallet/mod.rs Outdated
// if we have made a pubkey at this index, check whether the address has been used.
if i < script_pubkeys.len() {
let script_pk = &script_pubkeys[i];
if self.has_address_been_used(script_pk) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Here for each spk we are iterating over the entire transaction list. For wallets with large transaction this will can cause massive overhead.

Instead a better way would be to handle Vec<Script> in the has_address_been_used function. Call list_transactions only once, filter out all the spks that haven't been used and return then as a Vec.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yep good idea, note this would then also check more addresses than necessary (not breaking early when finding n).
If this was stored in the database it could also break early if it's just a fast read

@rajarshimaitra

Copy link
Copy Markdown
Contributor

@nickfarrow also try to rebase on top of master instead of fetching and merging specific commit next time.. :)
That makes the commit history much cleaner and also applies your changes on top of current master..
just do git rebase master from the PR branch..

@notmandatory

Copy link
Copy Markdown
Member

I had to push this PR to the next release so the team can focus on #593, and after that one you'll probably need to rebase again. But then I promise we'll work on getting this one in. :-)

@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

Yep I think this one needs some bigger discussion first around whether it is worthwhile to mark used addresses directly in the database as @rajarshimaitra suggested.

If this were the case, this could be simplified to a function get_unused_addresses that (quickly) gets an iterator over unused addresses in the database up to the current addressindex. With FirstUnused and LastUnused addresses at either ends.

This PR's get_batch_unused_addresses (currently fetches n) would be superseded by get_unused_addresses where the user gets all the unused addresses and handles them how they desire.

Not sure what changed with old commits, possible I added signoff lines to the previous commits by mistake which may have updated them sorry. Will take care with next.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

Upon further thoughts on this I have this rough idea of how it can be done:

  • Add a new column in the script_pubkey table named used. Which will have binary value 0/1, defaulting to 0.
  • At each sync when we add utxos to the Utxo table, mark the pubkey as used too in the script_pubkey table.
  • Make a generic get_batch_unused() that will return a list of unused addresses, checking the used flag in the database.
  • Make FirstUnused as the front pop of the list, and LastUnused as the back pop of the list..

Pro:
Much more scalable than transaction list scanning for wallets with large list of transactions.

Cons:
This is going to change the DB structure and the BatchDatabase API.

I am willing to work on fleshing an impl out if this has Approach Acks..

@afilini

Copy link
Copy Markdown
Member

Keep in mind we also have the key/value db, we don't have tables and columns there. We can add a flag to mark a script as used (similarly to how I suggested adding a flag for scripts that we've already setup rather than relying on just the derivation index), but getting the list of unused addresses will still require scanning

@LLFournLLFourn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This PR is almost ready and provides something that is pretty useful. The main bits of work here are:

  1. To restore the previous (someone nonsensical) behaviour of LastUnused
  2. To add tests to check the wallets derivation index is correct after calling batch unused (can just try and get a new address after and check its index)

As @rajarshimaitra mentions the best way to implement is to index things properly which in bdk is currently done in the database backend. In bdk_core I've done indexing of unused addresses. Since I was the one who requested this feature and I'm focused on bdk_core we could simply close this PR and wait until it lands. Does anyone else want this feature presently? @nickfarrow what do you think?

Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +393 to +398
pub fn get_batch_unused_addresses(
&self,
n: usize,
from_front: bool,
keychain: KeychainKind,
) -> Result<Vec<AddressInfo>, Error> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The correct thing is to return a impl DoubleEndedIterator over unused addresses I think.

Comment threadsrc/wallet/mod.rs Outdated
let check_indexes = if from_front {
(0..=current_address_index).collect::<Vec<_>>()
} else {
(0..=current_address_index).rev().collect::<Vec<_>>()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Return a impl DoubleEndedIterator from the method instead.

Comment threadsrc/wallet/mod.rs
Comment on lines +3983 to +4008
assert_eq!(
wallet
.get_batch_unused_addresses(3, true, KeychainKind::External)
.unwrap(),
vec![
AddressInfo {
index: 0,
address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 2,
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 3,
address: Address::from_str("tb1q32a23q6u3yy89l8svrt80a54h06qvn7gnuvsen")
.unwrap(),
keychain: KeychainKind::External,
}
]
);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Adding a check of the derivation index here would be good.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

ACK on @LLFourn that this can go in as it is without much further changes.. I will check the behavior once again.. If this lands through bdk_core eventually then we might not wanna do the DataBase way for now, and just keep things simple..

* remove batch getting `n` unused addresses, just get them all (much
simpler)
* use next_back() and next() for last and first unused
* test more cases for get_unused_address_indexes
* inline functions and simplified next addr
* create HashSet of txn scripts before checking unused
* add firstunused testcase for repeated unused
@notmandatory

Copy link
Copy Markdown
Member

Is this one OK to add this to the 0.22.0 milestone? looks like it's about ready and I don't want it to be overlooked.

@LLFourn

Copy link
Copy Markdown
Collaborator

IMO this PR is suboptimal because of #701. I think it should be fixed first to make the code in this PR make sense. @nickfarrow?

@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

Yep agree it may as well wait for improvements to fetch_index, that function is relied upon a few times here

@danielabrozzoni

Copy link
Copy Markdown
Contributor

Hey, we are in the process of releasing BDK 1.0, which will under the hood work quite differently from the current BDK. For this reason, I'm closing all the PRs that don't really apply anymore. If you think this is a mistake, feel free to rebase on master and re-open!

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new featureNew feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@nickfarrow@LLFourn@notmandatory@rajarshimaitra@afilini@danielabrozzoni
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

AddressIndex improvements: LastUnused, FirstUnused, and get_batch_unused_addresses() - #546

Closed
nickfarrow wants to merge 5 commits into
bitcoindevkit:masterfrom
nickfarrow:first-unused
Closed

AddressIndex improvements: LastUnused, FirstUnused, and get_batch_unused_addresses()#546
nickfarrow wants to merge 5 commits into
bitcoindevkit:masterfrom
nickfarrow:first-unused

Conversation

@nickfarrow

@nickfarrownickfarrow commented Feb 16, 2022

Copy link
Copy Markdown
Contributor

Description

  • Change AddressIndex::LastUnused to look back further than current_index
  • Add AddressIndex::FirstUnused
  • Add get_batch_unused_addresses

Notes to the reviewers

Builds upon #522

Currently BDK supports address indexing via LastUnused, which will return the address with current_index if it is unused, otherwise it will return a New address.

With this current logic, if you get two new addresses A1 and A2 and use A2, then LastUnused will give you a New address rather than the unused A1.

In order to more consistently utilize unused addresses i've added a new function get_unused_key_indexes(keychain) which returns a vector of indexes for the unused addresses in that keychain. Making use of this function, LastUnused now returns the most recent address that has not yet been used, and New if all addresses have been used.

In some cases it may be desirable to utilize unused addresses that reside earlier in the keychain. i.e. AddressIndex::FirstUnused in this PR.

FirstUnused has the same caveat as LastUnused: that if the wallet has not yet detected an address has been used, it could return a used address.

Additionally a new public function get_batch_unused_addresses allows for retrieval of N unused addresses at once. Prioritizing unused addresses first, then populating the remaining with New addresses (like FirstUnused).

For example: if a wallet builds a transaction involving many New internal addresses but that transaction is never broadcast, then all of these addresses can now easily be used in a later transaction via get_batch_unused_addresses.

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • I've added tests for the new feature
  • I've added docs for the new feature
  • I've updated CHANGELOG.md

@LLFourn

Copy link
Copy Markdown
Collaborator

approach ACK. Needs rebase.

@nickfarrow
nickfarrowforce-pushed the first-unused branch 2 times, most recently from 1f4da05 to 8bf61deCompareMarch 17, 2022 07:33
@notmandatory
notmandatory self-requested a review April 4, 2022 05:38
@notmandatorynotmandatory added the new feature New feature or request label Apr 4, 2022
@notmandatory

Copy link
Copy Markdown
Member

Code changes look great but you'll need to do another rebase and can you add a signing key to Github and sign your commits when you do the rebase also?

@nickfarrow
nickfarrowforce-pushed the first-unused branch 4 times, most recently from f8875d8 to 05aeb23CompareApril 10, 2022 07:06
@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

force push updated CI

@rajarshimaitrarajarshimaitra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

tACK 05aeb23

Below are some suggested modification..

Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment on lines +3993 to +3956
// use the first address
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);

assert_eq!(
wallet.get_address(FirstUnused).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would like to see the test situation here where we extract multiple addresses, use some of them and get back a previous unused one when called again.. That would correctly test the intended behavior.. Right now its just testing the vanilla situation..

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hmm I actually don't know a better test to write than this one? With the batch unused you can write a more complicated test but with FristUnused there's not much you can do.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Something like derive a bunch of address.. Only use some of them selectively so the address gaps are simulated.. Then check if the first unused is returned correctly.. Am I missing some details why that can't work??

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It can work but I don't get why the gaps would effect the algorithm that finds the first unused. I mean I don't think that this will likely find a problem with the algorithm that this test wouldn't find.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Its not that the gaps would affect the algorithm, but to confirm that the behavior we are intending here is actually happening.. And this can be checked in single test for both first and last unused.. Once the behavior is pinned, we can decide later which one to use when or to keep both..

@LLFournLLFourn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @nickfarrow. Tests LGTM. See comments.

Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs
Comment on lines +3993 to +3956
// use the first address
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);

assert_eq!(
wallet.get_address(FirstUnused).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hmm I actually don't know a better test to write than this one? With the batch unused you can write a more complicated test but with FristUnused there's not much you can do.

@notmandatory

Copy link
Copy Markdown
Member

Hi, please rebase to pickup changes in #596. Thanks!

Signed-off-by: nickfarrow <nick@nickfarrow.com>
Signed-off-by: nickfarrow <nick@nickfarrow.com>
Signed-off-by: nickfarrow <nick@nickfarrow.com>
* get_batch_unused_addresses loops through address indexes `from_front = true` (for firstUnused) or `false` (for lastUnused).
* Relies on database having up to date script_pubkeys in such a manner
that script_pks.len() == self.fetch_index(keychain)
* 1 script pubkey per address index?
* Must work with current_address_index = 0
Signed-off-by: nickfarrow <nick@nickfarrow.com>
Comment threadsrc/wallet/mod.rs Outdated
.list_transactions(true)?
.iter()
// Return whether this address has been used in a transaction
fn has_address_been_used(&self, script_pk: &Script) -> bool {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

actually checking a Script..

Comment threadsrc/wallet/mod.rs Outdated
let check_indexes = if from_front {
(0..=current_address_index).collect::<Vec<_>>()
} else {
(0..=current_address_index).rev().collect::<Vec<_>>()

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

better way to do this?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Return a impl DoubleEndedIterator from the method instead.

@rajarshimaitrarajarshimaitra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am still not comfortable with the current state and approach of the PR. Some behaviour are not maintained, like get_batch(n, false, keychain) will give the list of addresses in reverse order. Not in ascending order of indexes.

Also I am thinking isn't it better to mark used addresses directly in the database? Knowing which one is used and not and saving the data seems to me more useful than figuring it out by transaction matching with the entire tx list, everytime we ask for an unused..

This will also simplify the LastUnused and FirstUnused fetching logic..

@LLFourn@afilini do you have any thought on this??

Comment threadsrc/wallet/mod.rs Outdated

/// Return vector of n unused addresses from the [`KeychainKind`].
/// If less than n unused addresses are returned, the rest will be populated by new addresses.
/// The unused addresses returned are in order of oldest in keychain first, with increasing index.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is not as per impl right now.. if from_front is set false, the addresses are returned in reverse order..

Comment threadsrc/wallet/mod.rs Outdated
.list_transactions(true)?
.iter()
// Return whether this address has been used in a transaction
fn has_address_been_used(&self, script_pk: &Script) -> bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This guy is better named as is_scriptpubkey_used..

Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +393 to +398
pub fn get_batch_unused_addresses(
&self,
n: usize,
from_front: bool,
keychain: KeychainKind,
) -> Result<Vec<AddressInfo>, Error> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am not feeling comfortable with the API. get_batched_unused should not be concerned with fornt or back. Thats an impl detail for LastUnused or FirstUnused. And should not be exposed in public API..

This is also breaking the doc above. The order is not maintained anymore..

Better to handle the handle the first or last logic in in their respective functions itself than to handle in the batch function which is more generic.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The correct thing is to return a impl DoubleEndedIterator over unused addresses I think.

Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +413 to +438
for i in check_indexes {
// if we have made a pubkey at this index, check whether the address has been used.
if i < script_pubkeys.len() {
let script_pk = &script_pubkeys[i];
if self.has_address_been_used(script_pk) {
continue;
}
}
if let Ok(unused_address) = self
.get_descriptor_for_keychain(keychain)
.as_derived(i as u32, &self.secp)
.address(self.network)
.map(|address| AddressInfo {
address,
index: i as u32,
keychain,
})
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
{
unused_addresses.push(unused_address);
}

if unused_addresses.len() >= n {
break;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Try using some rust list comprehensions with iters and maps. Much of this code can be simplified..

Comment threadsrc/wallet/mod.rs
Comment on lines +3983 to +4008
assert_eq!(
wallet
.get_batch_unused_addresses(3, true, KeychainKind::External)
.unwrap(),
vec![
AddressInfo {
index: 0,
address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 2,
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 3,
address: Address::from_str("tb1q32a23q6u3yy89l8svrt80a54h06qvn7gnuvsen")
.unwrap(),
keychain: KeychainKind::External,
}
]
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also need to assert that FirstUnused and LastUnused are working as intended..

  • Get 5 new addresses
  • Use only index 0 and 3
  • get_batch(3) should return index 0, 2, 4. current index should still be at 4.
  • get_first_unused() should return 0
  • get_last_unused() should return 4
  • get_batch(4) should return 0,2,4,5, and current index should be at 5.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Adding a check of the derivation index here would be good.

Comment threadsrc/wallet/mod.rs Outdated
// if we have made a pubkey at this index, check whether the address has been used.
if i < script_pubkeys.len() {
let script_pk = &script_pubkeys[i];
if self.has_address_been_used(script_pk) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Here for each spk we are iterating over the entire transaction list. For wallets with large transaction this will can cause massive overhead.

Instead a better way would be to handle Vec<Script> in the has_address_been_used function. Call list_transactions only once, filter out all the spks that haven't been used and return then as a Vec.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yep good idea, note this would then also check more addresses than necessary (not breaking early when finding n).
If this was stored in the database it could also break early if it's just a fast read

@rajarshimaitra

Copy link
Copy Markdown
Contributor

@nickfarrow also try to rebase on top of master instead of fetching and merging specific commit next time.. :)
That makes the commit history much cleaner and also applies your changes on top of current master..
just do git rebase master from the PR branch..

@notmandatory

Copy link
Copy Markdown
Member

I had to push this PR to the next release so the team can focus on #593, and after that one you'll probably need to rebase again. But then I promise we'll work on getting this one in. :-)

@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

Yep I think this one needs some bigger discussion first around whether it is worthwhile to mark used addresses directly in the database as @rajarshimaitra suggested.

If this were the case, this could be simplified to a function get_unused_addresses that (quickly) gets an iterator over unused addresses in the database up to the current addressindex. With FirstUnused and LastUnused addresses at either ends.

This PR's get_batch_unused_addresses (currently fetches n) would be superseded by get_unused_addresses where the user gets all the unused addresses and handles them how they desire.

Not sure what changed with old commits, possible I added signoff lines to the previous commits by mistake which may have updated them sorry. Will take care with next.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

Upon further thoughts on this I have this rough idea of how it can be done:

  • Add a new column in the script_pubkey table named used. Which will have binary value 0/1, defaulting to 0.
  • At each sync when we add utxos to the Utxo table, mark the pubkey as used too in the script_pubkey table.
  • Make a generic get_batch_unused() that will return a list of unused addresses, checking the used flag in the database.
  • Make FirstUnused as the front pop of the list, and LastUnused as the back pop of the list..

Pro:
Much more scalable than transaction list scanning for wallets with large list of transactions.

Cons:
This is going to change the DB structure and the BatchDatabase API.

I am willing to work on fleshing an impl out if this has Approach Acks..

@afilini

Copy link
Copy Markdown
Member

Keep in mind we also have the key/value db, we don't have tables and columns there. We can add a flag to mark a script as used (similarly to how I suggested adding a flag for scripts that we've already setup rather than relying on just the derivation index), but getting the list of unused addresses will still require scanning

@LLFournLLFourn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This PR is almost ready and provides something that is pretty useful. The main bits of work here are:

  1. To restore the previous (someone nonsensical) behaviour of LastUnused
  2. To add tests to check the wallets derivation index is correct after calling batch unused (can just try and get a new address after and check its index)

As @rajarshimaitra mentions the best way to implement is to index things properly which in bdk is currently done in the database backend. In bdk_core I've done indexing of unused addresses. Since I was the one who requested this feature and I'm focused on bdk_core we could simply close this PR and wait until it lands. Does anyone else want this feature presently? @nickfarrow what do you think?

Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +393 to +398
pub fn get_batch_unused_addresses(
&self,
n: usize,
from_front: bool,
keychain: KeychainKind,
) -> Result<Vec<AddressInfo>, Error> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The correct thing is to return a impl DoubleEndedIterator over unused addresses I think.

Comment threadsrc/wallet/mod.rs Outdated
let check_indexes = if from_front {
(0..=current_address_index).collect::<Vec<_>>()
} else {
(0..=current_address_index).rev().collect::<Vec<_>>()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Return a impl DoubleEndedIterator from the method instead.

Comment threadsrc/wallet/mod.rs
Comment on lines +3983 to +4008
assert_eq!(
wallet
.get_batch_unused_addresses(3, true, KeychainKind::External)
.unwrap(),
vec![
AddressInfo {
index: 0,
address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 2,
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 3,
address: Address::from_str("tb1q32a23q6u3yy89l8svrt80a54h06qvn7gnuvsen")
.unwrap(),
keychain: KeychainKind::External,
}
]
);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Adding a check of the derivation index here would be good.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

ACK on @LLFourn that this can go in as it is without much further changes.. I will check the behavior once again.. If this lands through bdk_core eventually then we might not wanna do the DataBase way for now, and just keep things simple..

* remove batch getting `n` unused addresses, just get them all (much
simpler)
* use next_back() and next() for last and first unused
* test more cases for get_unused_address_indexes
* inline functions and simplified next addr
* create HashSet of txn scripts before checking unused
* add firstunused testcase for repeated unused
@notmandatory

Copy link
Copy Markdown
Member

Is this one OK to add this to the 0.22.0 milestone? looks like it's about ready and I don't want it to be overlooked.

@LLFourn

Copy link
Copy Markdown
Collaborator

IMO this PR is suboptimal because of #701. I think it should be fixed first to make the code in this PR make sense. @nickfarrow?

@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

Yep agree it may as well wait for improvements to fetch_index, that function is relied upon a few times here

@danielabrozzoni

Copy link
Copy Markdown
Contributor

Hey, we are in the process of releasing BDK 1.0, which will under the hood work quite differently from the current BDK. For this reason, I'm closing all the PRs that don't really apply anymore. If you think this is a mistake, feel free to rebase on master and re-open!

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new featureNew feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@nickfarrow@LLFourn@notmandatory@rajarshimaitra@afilini@danielabrozzoni
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

AddressIndex improvements: LastUnused, FirstUnused, and get_batch_unused_addresses() - #546

Closed
nickfarrow wants to merge 5 commits into
bitcoindevkit:masterfrom
nickfarrow:first-unused
Closed

AddressIndex improvements: LastUnused, FirstUnused, and get_batch_unused_addresses()#546
nickfarrow wants to merge 5 commits into
bitcoindevkit:masterfrom
nickfarrow:first-unused

Conversation

@nickfarrow

@nickfarrownickfarrow commented Feb 16, 2022

Copy link
Copy Markdown
Contributor

Description

  • Change AddressIndex::LastUnused to look back further than current_index
  • Add AddressIndex::FirstUnused
  • Add get_batch_unused_addresses

Notes to the reviewers

Builds upon #522

Currently BDK supports address indexing via LastUnused, which will return the address with current_index if it is unused, otherwise it will return a New address.

With this current logic, if you get two new addresses A1 and A2 and use A2, then LastUnused will give you a New address rather than the unused A1.

In order to more consistently utilize unused addresses i've added a new function get_unused_key_indexes(keychain) which returns a vector of indexes for the unused addresses in that keychain. Making use of this function, LastUnused now returns the most recent address that has not yet been used, and New if all addresses have been used.

In some cases it may be desirable to utilize unused addresses that reside earlier in the keychain. i.e. AddressIndex::FirstUnused in this PR.

FirstUnused has the same caveat as LastUnused: that if the wallet has not yet detected an address has been used, it could return a used address.

Additionally a new public function get_batch_unused_addresses allows for retrieval of N unused addresses at once. Prioritizing unused addresses first, then populating the remaining with New addresses (like FirstUnused).

For example: if a wallet builds a transaction involving many New internal addresses but that transaction is never broadcast, then all of these addresses can now easily be used in a later transaction via get_batch_unused_addresses.

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • I've added tests for the new feature
  • I've added docs for the new feature
  • I've updated CHANGELOG.md

@LLFourn

Copy link
Copy Markdown
Collaborator

approach ACK. Needs rebase.

@nickfarrow
nickfarrowforce-pushed the first-unused branch 2 times, most recently from 1f4da05 to 8bf61deCompareMarch 17, 2022 07:33
@notmandatory
notmandatory self-requested a review April 4, 2022 05:38
@notmandatorynotmandatory added the new feature New feature or request label Apr 4, 2022
@notmandatory

Copy link
Copy Markdown
Member

Code changes look great but you'll need to do another rebase and can you add a signing key to Github and sign your commits when you do the rebase also?

@nickfarrow
nickfarrowforce-pushed the first-unused branch 4 times, most recently from f8875d8 to 05aeb23CompareApril 10, 2022 07:06
@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

force push updated CI

@rajarshimaitrarajarshimaitra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

tACK 05aeb23

Below are some suggested modification..

Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs
Comment on lines +3993 to +3956
// use the first address
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);

assert_eq!(
wallet.get_address(FirstUnused).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would like to see the test situation here where we extract multiple addresses, use some of them and get back a previous unused one when called again.. That would correctly test the intended behavior.. Right now its just testing the vanilla situation..

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hmm I actually don't know a better test to write than this one? With the batch unused you can write a more complicated test but with FristUnused there's not much you can do.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Something like derive a bunch of address.. Only use some of them selectively so the address gaps are simulated.. Then check if the first unused is returned correctly.. Am I missing some details why that can't work??

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It can work but I don't get why the gaps would effect the algorithm that finds the first unused. I mean I don't think that this will likely find a problem with the algorithm that this test wouldn't find.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Its not that the gaps would affect the algorithm, but to confirm that the behavior we are intending here is actually happening.. And this can be checked in single test for both first and last unused.. Once the behavior is pinned, we can decide later which one to use when or to keep both..

@LLFournLLFourn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @nickfarrow. Tests LGTM. See comments.

Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs Outdated
Comment threadsrc/wallet/mod.rs
Comment on lines +3993 to +3956
// use the first address
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);

assert_eq!(
wallet.get_address(FirstUnused).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hmm I actually don't know a better test to write than this one? With the batch unused you can write a more complicated test but with FristUnused there's not much you can do.

@notmandatory

Copy link
Copy Markdown
Member

Hi, please rebase to pickup changes in #596. Thanks!

Signed-off-by: nickfarrow <nick@nickfarrow.com>
Signed-off-by: nickfarrow <nick@nickfarrow.com>
Signed-off-by: nickfarrow <nick@nickfarrow.com>
* get_batch_unused_addresses loops through address indexes `from_front = true` (for firstUnused) or `false` (for lastUnused).
* Relies on database having up to date script_pubkeys in such a manner
that script_pks.len() == self.fetch_index(keychain)
* 1 script pubkey per address index?
* Must work with current_address_index = 0
Signed-off-by: nickfarrow <nick@nickfarrow.com>
Comment threadsrc/wallet/mod.rs Outdated
.list_transactions(true)?
.iter()
// Return whether this address has been used in a transaction
fn has_address_been_used(&self, script_pk: &Script) -> bool {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

actually checking a Script..

Comment threadsrc/wallet/mod.rs Outdated
let check_indexes = if from_front {
(0..=current_address_index).collect::<Vec<_>>()
} else {
(0..=current_address_index).rev().collect::<Vec<_>>()

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

better way to do this?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Return a impl DoubleEndedIterator from the method instead.

@rajarshimaitrarajarshimaitra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am still not comfortable with the current state and approach of the PR. Some behaviour are not maintained, like get_batch(n, false, keychain) will give the list of addresses in reverse order. Not in ascending order of indexes.

Also I am thinking isn't it better to mark used addresses directly in the database? Knowing which one is used and not and saving the data seems to me more useful than figuring it out by transaction matching with the entire tx list, everytime we ask for an unused..

This will also simplify the LastUnused and FirstUnused fetching logic..

@LLFourn@afilini do you have any thought on this??

Comment threadsrc/wallet/mod.rs Outdated

/// Return vector of n unused addresses from the [`KeychainKind`].
/// If less than n unused addresses are returned, the rest will be populated by new addresses.
/// The unused addresses returned are in order of oldest in keychain first, with increasing index.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is not as per impl right now.. if from_front is set false, the addresses are returned in reverse order..

Comment threadsrc/wallet/mod.rs Outdated
.list_transactions(true)?
.iter()
// Return whether this address has been used in a transaction
fn has_address_been_used(&self, script_pk: &Script) -> bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This guy is better named as is_scriptpubkey_used..

Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +393 to +398
pub fn get_batch_unused_addresses(
&self,
n: usize,
from_front: bool,
keychain: KeychainKind,
) -> Result<Vec<AddressInfo>, Error> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am not feeling comfortable with the API. get_batched_unused should not be concerned with fornt or back. Thats an impl detail for LastUnused or FirstUnused. And should not be exposed in public API..

This is also breaking the doc above. The order is not maintained anymore..

Better to handle the handle the first or last logic in in their respective functions itself than to handle in the batch function which is more generic.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The correct thing is to return a impl DoubleEndedIterator over unused addresses I think.

Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +413 to +438
for i in check_indexes {
// if we have made a pubkey at this index, check whether the address has been used.
if i < script_pubkeys.len() {
let script_pk = &script_pubkeys[i];
if self.has_address_been_used(script_pk) {
continue;
}
}
if let Ok(unused_address) = self
.get_descriptor_for_keychain(keychain)
.as_derived(i as u32, &self.secp)
.address(self.network)
.map(|address| AddressInfo {
address,
index: i as u32,
keychain,
})
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
{
unused_addresses.push(unused_address);
}

if unused_addresses.len() >= n {
break;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Try using some rust list comprehensions with iters and maps. Much of this code can be simplified..

Comment threadsrc/wallet/mod.rs
Comment on lines +3983 to +4008
assert_eq!(
wallet
.get_batch_unused_addresses(3, true, KeychainKind::External)
.unwrap(),
vec![
AddressInfo {
index: 0,
address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 2,
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 3,
address: Address::from_str("tb1q32a23q6u3yy89l8svrt80a54h06qvn7gnuvsen")
.unwrap(),
keychain: KeychainKind::External,
}
]
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also need to assert that FirstUnused and LastUnused are working as intended..

  • Get 5 new addresses
  • Use only index 0 and 3
  • get_batch(3) should return index 0, 2, 4. current index should still be at 4.
  • get_first_unused() should return 0
  • get_last_unused() should return 4
  • get_batch(4) should return 0,2,4,5, and current index should be at 5.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Adding a check of the derivation index here would be good.

Comment threadsrc/wallet/mod.rs Outdated
// if we have made a pubkey at this index, check whether the address has been used.
if i < script_pubkeys.len() {
let script_pk = &script_pubkeys[i];
if self.has_address_been_used(script_pk) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Here for each spk we are iterating over the entire transaction list. For wallets with large transaction this will can cause massive overhead.

Instead a better way would be to handle Vec<Script> in the has_address_been_used function. Call list_transactions only once, filter out all the spks that haven't been used and return then as a Vec.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yep good idea, note this would then also check more addresses than necessary (not breaking early when finding n).
If this was stored in the database it could also break early if it's just a fast read

@rajarshimaitra

Copy link
Copy Markdown
Contributor

@nickfarrow also try to rebase on top of master instead of fetching and merging specific commit next time.. :)
That makes the commit history much cleaner and also applies your changes on top of current master..
just do git rebase master from the PR branch..

@notmandatory

Copy link
Copy Markdown
Member

I had to push this PR to the next release so the team can focus on #593, and after that one you'll probably need to rebase again. But then I promise we'll work on getting this one in. :-)

@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

Yep I think this one needs some bigger discussion first around whether it is worthwhile to mark used addresses directly in the database as @rajarshimaitra suggested.

If this were the case, this could be simplified to a function get_unused_addresses that (quickly) gets an iterator over unused addresses in the database up to the current addressindex. With FirstUnused and LastUnused addresses at either ends.

This PR's get_batch_unused_addresses (currently fetches n) would be superseded by get_unused_addresses where the user gets all the unused addresses and handles them how they desire.

Not sure what changed with old commits, possible I added signoff lines to the previous commits by mistake which may have updated them sorry. Will take care with next.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

Upon further thoughts on this I have this rough idea of how it can be done:

  • Add a new column in the script_pubkey table named used. Which will have binary value 0/1, defaulting to 0.
  • At each sync when we add utxos to the Utxo table, mark the pubkey as used too in the script_pubkey table.
  • Make a generic get_batch_unused() that will return a list of unused addresses, checking the used flag in the database.
  • Make FirstUnused as the front pop of the list, and LastUnused as the back pop of the list..

Pro:
Much more scalable than transaction list scanning for wallets with large list of transactions.

Cons:
This is going to change the DB structure and the BatchDatabase API.

I am willing to work on fleshing an impl out if this has Approach Acks..

@afilini

Copy link
Copy Markdown
Member

Keep in mind we also have the key/value db, we don't have tables and columns there. We can add a flag to mark a script as used (similarly to how I suggested adding a flag for scripts that we've already setup rather than relying on just the derivation index), but getting the list of unused addresses will still require scanning

@LLFournLLFourn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This PR is almost ready and provides something that is pretty useful. The main bits of work here are:

  1. To restore the previous (someone nonsensical) behaviour of LastUnused
  2. To add tests to check the wallets derivation index is correct after calling batch unused (can just try and get a new address after and check its index)

As @rajarshimaitra mentions the best way to implement is to index things properly which in bdk is currently done in the database backend. In bdk_core I've done indexing of unused addresses. Since I was the one who requested this feature and I'm focused on bdk_core we could simply close this PR and wait until it lands. Does anyone else want this feature presently? @nickfarrow what do you think?

Comment threadsrc/wallet/mod.rs
Comment threadsrc/wallet/mod.rs Outdated
Comment on lines +393 to +398
pub fn get_batch_unused_addresses(
&self,
n: usize,
from_front: bool,
keychain: KeychainKind,
) -> Result<Vec<AddressInfo>, Error> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The correct thing is to return a impl DoubleEndedIterator over unused addresses I think.

Comment threadsrc/wallet/mod.rs Outdated
let check_indexes = if from_front {
(0..=current_address_index).collect::<Vec<_>>()
} else {
(0..=current_address_index).rev().collect::<Vec<_>>()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Return a impl DoubleEndedIterator from the method instead.

Comment threadsrc/wallet/mod.rs
Comment on lines +3983 to +4008
assert_eq!(
wallet
.get_batch_unused_addresses(3, true, KeychainKind::External)
.unwrap(),
vec![
AddressInfo {
index: 0,
address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 2,
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2")
.unwrap(),
keychain: KeychainKind::External,
},
AddressInfo {
index: 3,
address: Address::from_str("tb1q32a23q6u3yy89l8svrt80a54h06qvn7gnuvsen")
.unwrap(),
keychain: KeychainKind::External,
}
]
);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Adding a check of the derivation index here would be good.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

ACK on @LLFourn that this can go in as it is without much further changes.. I will check the behavior once again.. If this lands through bdk_core eventually then we might not wanna do the DataBase way for now, and just keep things simple..

* remove batch getting `n` unused addresses, just get them all (much
simpler)
* use next_back() and next() for last and first unused
* test more cases for get_unused_address_indexes
* inline functions and simplified next addr
* create HashSet of txn scripts before checking unused
* add firstunused testcase for repeated unused
@notmandatory

Copy link
Copy Markdown
Member

Is this one OK to add this to the 0.22.0 milestone? looks like it's about ready and I don't want it to be overlooked.

@LLFourn

Copy link
Copy Markdown
Collaborator

IMO this PR is suboptimal because of #701. I think it should be fixed first to make the code in this PR make sense. @nickfarrow?

@nickfarrow

Copy link
Copy Markdown
ContributorAuthor

Yep agree it may as well wait for improvements to fetch_index, that function is relied upon a few times here

@danielabrozzoni

Copy link
Copy Markdown
Contributor

Hey, we are in the process of releasing BDK 1.0, which will under the hood work quite differently from the current BDK. For this reason, I'm closing all the PRs that don't really apply anymore. If you think this is a mistake, feel free to rebase on master and re-open!

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new featureNew feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@nickfarrow@LLFourn@notmandatory@rajarshimaitra@afilini@danielabrozzoni