Skip to content

wallet: immediate lookahead table expansion + set_subaddresss_lookahead RPC endpoint #9953

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

nahuhh
Copy link
Contributor

@nahuhh nahuhh commented Jun 12, 2025

replaces #8981

no changes
changes from original:

  • added header to fix test failure #include <boost/optional/optional_io.hpp>
  • updates & fixes by @j-berman

resolves #8980
resolves #8954
resolves #7364

Basically the current behavior is that if you make a wallet with the default settings in the cli and then do set subaddress-lookahead 50:300, then the subaddress pubkey table hasnt actually expanded and you aren't looking any further when you scan. Instead, the table will get expanded to this look ahead only after either a new enote is found with the old lookahead or if the user requests a new subaddress with address new.

This patch makes sure that the wallet adjusts the pubkey table immediately after sending the set command if an increase in the lookahead is requested.

This patch also adds a new endpoint to the wallet-rpc program, /set_subaddress_lookahead which allows the user to expand the subaddress lookahead table via remote command. As mentioned in the original ticket, the change does not persist to disk when the wallet closes and changing the wallet-rpc program to save those cached values looks like kinda a pain but I hope that the requestor finds that something is better than nothing for now.

@jeffro256
Copy link
Contributor

Why was the original PR closed instead of continuing there ?

@nahuhh
Copy link
Contributor Author

nahuhh commented Jun 16, 2025

merge conflicts, op unavailable (is what i thought. idc where it continues. Was requested that someone continue it)

@plowsof
Copy link
Contributor

plowsof commented Jun 19, 2025

a review on if the logic is in the correct place / manner and is not duplicating some other function would be useful. e.g. we have a should_expand function: (and expand_subaddresses)
https://github.com/monero-project/monero/pull/8513/files#r952031123

so if someone who knows what they're looking at can give feedback on if this needs, or would be better as a should_expand then call to expand_subaddresses that would be much appreciated D:

Copy link
Collaborator

@j-berman j-berman left a comment

Choose a reason for hiding this comment

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

or would be better as a should_expand then call to expand_subaddresses

@plowsof I thought this might be the case too, but actually that would create labels for all subaddresses in the lookahead range, which would clutter UI's / isn't necessary. @benevanoff's decision not to do that looks correct to me. My suggestion is minor (but worth doing imo), the PR looks good as is and this was tricky to get right. Thank you @benevanoff for this code. And thank you @nahuhh for following up.

Copy link
Collaborator

@j-berman j-berman left a comment

Choose a reason for hiding this comment

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

Sorry, @plowsof's comment here made me realize a bug in my code (and a separate bug in the original code as well). These changes include a fix and test cases that capture the bugs.

The fix matches the logic of set_address_lookahead to expand_subaddresses. So repeating @plowsof 's test:

starting with a 1:10 lookahead. sending to above expanded , then decreasing idx until i find the max idx

With this fix, the table will look like this:

lookahead max idx
1:10 9
  18
  27
1:20 46
1:1000 1045

It's a little counter-intuitive that the lookahead only reaches the max idx + lookahead - 1 in each step.

I think that may make sense to change in both expand_subaddresses and set_subaddress_lookahead in a separate PR. But for now, this matches the logic of set_subaddress_lookahead to expand_subaddresses.

Copy link
Collaborator

@j-berman j-berman left a comment

Choose a reason for hiding this comment

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

This matches set_subaddress_lookahead to expand_subaddresses for major indexes as well (the prior only matched behavior for minor subaddrs). Sorry for the spam.

@plowsof
Copy link
Contributor

plowsof commented Jun 27, 2025

confirming the above behaviour. thanks for going through my ramblings and debugging further @j-berman . for those testing: do note that an output within the expanded area will be detected in the pool but it will not further expand until at least 1 block confirmation.

closes #7364 can be added to this one

@nahuhh
Copy link
Contributor Author

nahuhh commented Jun 27, 2025

before anyone asks for squash, i'm waiting for 9971 merge (ci fix)

@plowsof
Copy link
Contributor

plowsof commented Jun 27, 2025

sanity checking @nahuhh comment here about wallet-rpc and persistance #9954 (comment) (i thought this was a simple case of not calling store)

curl -X POST http://127.0.0.1:18082/json_rpc -d '{"jsonrpc":"2.0","id":"0","method":"set_subaddress_lookahead","params":{"major_idx":"1","minor_idx":"500"}}' -H 'Content-Type: application/json'
<receive tx at max idx 499>
curl -X POST http://127.0.0.1:18082/json_rpc -d '{"jsonrpc":"2.0","id":"0","method":"store"}' -H 'Content-Type: application/json'
curl -X POST http://127.0.0.1:18082/json_rpc -d '{"jsonrpc":"2.0","id":"0","method":"stop_wallet"}' -H 'Content-Type: application/json'

open with cli set will show a subaddress lookahead of 50:200 - so store does not make this persistent.

  • send to max possible idx of 999 - nothing the max idx would be 998 ****
  • send to 700 - but it detected it, huh while set shows 50:200 and last received tx at idx 499

@j-berman
Copy link
Collaborator

RPC persistence should probably be in this PR imo. Try this patch:

diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp
index 1ed8eb9fc..4f349e4bd 100644
--- a/src/wallet/wallet_rpc_server.cpp
+++ b/src/wallet/wallet_rpc_server.cpp
@@ -707,11 +707,24 @@ namespace tools
   bool wallet_rpc_server::on_set_subaddr_lookahead(const wallet_rpc::COMMAND_RPC_SET_SUBADDR_LOOKAHEAD::request& req, wallet_rpc::COMMAND_RPC_SET_SUBADDR_LOOKAHEAD::response& res, epee::json_rpc::error& er, const connection_context *ctx)
   {
     if (!m_wallet) return not_open(er);
-    try {
-      m_wallet->set_subaddress_lookahead(req.major_idx, req.minor_idx);
+    CHECK_IF_BACKGROUND_SYNCING();
+    const std::string wallet_file = m_wallet->get_wallet_file();
+    if (wallet_file == "" || m_wallet->verify_password(req.password))
+    {
+      try
+      {
+        m_wallet->set_subaddress_lookahead(req.major_idx, req.minor_idx);
+        m_wallet->rewrite(wallet_file, req.password);
+      }
+      catch (const std::exception& e) {
+        handle_rpc_exception(std::current_exception(), er, WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR);
+        return false;
+      }
     }
-    catch (const std::exception& e) {
-      handle_rpc_exception(std::current_exception(), er, WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR);
+    else
+    {
+      er.code = WALLET_RPC_ERROR_CODE_INVALID_PASSWORD;
+      er.message = "Invalid password.";
       return false;
     }
     return true;
diff --git a/src/wallet/wallet_rpc_server_commands_defs.h b/src/wallet/wallet_rpc_server_commands_defs.h
index 0871d6369..d0273d162 100644
--- a/src/wallet/wallet_rpc_server_commands_defs.h
+++ b/src/wallet/wallet_rpc_server_commands_defs.h
@@ -186,9 +186,11 @@ namespace wallet_rpc
   {
     struct request_t
     {
+      std::string password;
       uint64_t major_idx;
       uint64_t minor_idx;
       BEGIN_KV_SERIALIZE_MAP()
+        KV_SERIALIZE(password)
         KV_SERIALIZE(major_idx)
         KV_SERIALIZE(minor_idx)
       END_KV_SERIALIZE_MAP()
diff --git a/utils/python-rpc/framework/wallet.py b/utils/python-rpc/framework/wallet.py
index 88cece2bc..d0a516c1f 100644
--- a/utils/python-rpc/framework/wallet.py
+++ b/utils/python-rpc/framework/wallet.py
@@ -334,11 +334,12 @@ class Wallet(object):
         }
         return self.rpc.send_json_rpc_request(generate_from_keys)
 
-    def set_subaddress_lookahead(self, major_idx: int, minor_idx: int):
+    def set_subaddress_lookahead(self, major_idx: int, minor_idx: int, password = ""):
         lookahead = {
             'method': 'set_subaddress_lookahead',
             'jsonrpc': '2.0',
             'params' : {
+                'password': password,
                 'major_idx': major_idx,
                 'minor_idx': minor_idx
             },

@plowsof
Copy link
Contributor

plowsof commented Jun 27, 2025

patch works , thank you!

curl -X POST http://127.0.0.1:18082/json_rpc -d '{"jsonrpc":"2.0","id":"0","method":"set_subaddress_lookahead","params":{"major_idx":"1","minor_idx":"600","password":""}}' -H 'Content-Type: application/json'
[..store..]
[..stop_wallet..]

wallet cli shows the new lookahead value 👍

Comment on lines +1960 to +1985
// Expand the subaddresses map so that outputs received to the higher lookaheads will be identified in the scan loop
hw::device &hwdev = m_account.get_device();
cryptonote::subaddress_index index2;
const uint32_t max_major_idx = this->get_num_subaddress_accounts() > 0 ? (this->get_num_subaddress_accounts() - 1) : 0;
const uint32_t major_end = get_subaddress_clamped_sum(max_major_idx, major);
for (index2.major = 0; index2.major < major_end; ++index2.major)
{
// The existing minor addresses already set for this account
const uint32_t n_minor_subaddrs = this->get_num_subaddresses(index2.major);

// The subaddress lookahead is expected to expand from the max index in expand_subaddresses
const uint32_t max_minor_idx = n_minor_subaddrs > 0 ? (n_minor_subaddrs - 1) : 0;
const uint32_t begin = (n_minor_subaddrs || index2.major < old_major_lookahead) ? get_subaddress_clamped_sum(max_minor_idx, old_minor_lookahead) : 0;
// The expected new n minor subaddresses allocated for this account
const uint32_t end = get_subaddress_clamped_sum(max_minor_idx, minor);

if (begin >= end)
continue;

const std::vector<crypto::public_key> pkeys = hwdev.get_subaddress_spend_public_keys(m_account.get_keys(), index2.major, begin, end);
for (index2.minor = begin; index2.minor < end; ++index2.minor)
{
const crypto::public_key &D = pkeys.at(index2.minor - begin);
m_subaddresses[D] = index2;
}
}
Copy link
Contributor

@jeffro256 jeffro256 Jun 30, 2025

Choose a reason for hiding this comment

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

Why not use this->expand_subaddresses()? (Before/conditional on setting m_subaddress_lookahead_major and m_subaddress_lookahead_minor above)

Copy link
Collaborator

Choose a reason for hiding this comment

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

expand_subaddresses expects a user to have received to a specific subaddr index, and then expands the subaddress map (and adds subaddress labels) from there. UI's populate subaddresses based on the labels that are set. So we don't want to add any new labels from setting this lookahead. We could have expand_subaddresses reuse this logic from here, but I think this approach is reasonable.

Copy link
Contributor

@jeffro256 jeffro256 Jun 30, 2025

Choose a reason for hiding this comment

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

I will need to double-check, but I think a lot of the wallet2 balance calls will break if the subaddress map is populated, but not the subaddress labels. See: #9934 (comment). A lot of higher-level calling code iterates through the subaddresses wallet2 provides using the get_num_subaddresses() and get_num_subaddress_accounts() calls, which query m_subaddress_labels, not m_subaddresses.

Copy link
Collaborator

Choose a reason for hiding this comment

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

expand_subaddresses already does expand the subaddresses map past the subaddress labels the same way that this new logic does (see the new unit tests), so the wallet2 balance calls would be currently broken if true

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay good point. On first receive during refresh, should_expand will return true for that receive address index if the subaddress lookahead was already set previously, in which case the subaddress labels will be filled up to that index. So basically the account/subaddress counts will be "wrong" until the first receive, but those would have 0 balance in them anyways. You also can't lookup the subaddress by index until it's first receive, which is funky, but that's already the case for lookahead addresses. I still don't like the code duplication though...

@nahuhh
Copy link
Contributor Author

nahuhh commented Jun 30, 2025

only changes in the rebase are to apply this patch from @j-berman
https://github.com/monero-project/monero/compare/b02f62b58eb15ff8aadad715e28ae080007e1726..6b441a1782d9ecc2248145fa87efe3e099a6db43

@@ -182,6 +182,29 @@ namespace wallet_rpc
typedef epee::misc_utils::struct_init<response_t> response;
};

struct COMMAND_RPC_SET_SUBADDR_LOOKAHEAD
Copy link
Contributor

Choose a reason for hiding this comment

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

The RPC minor version should be bumped.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done, thanks

@jeffro256
Copy link
Contributor

Here's an alternative approach (apply diff to current HEAD):

diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp
index 23a0880e6..ca8343c51 100644
--- a/src/wallet/wallet2.cpp
+++ b/src/wallet/wallet2.cpp
@@ -1597,39 +1597,62 @@ bool wallet2::should_expand(const cryptonote::subaddress_index &index) const
 //----------------------------------------------------------------------------------------------------
 void wallet2::expand_subaddresses(const cryptonote::subaddress_index& index)
 {
-  hw::device &hwdev = m_account.get_device();
+  // check if index will overflow container (usually only applicable on 32-bit systems)
+  if constexpr (sizeof(std::size_t) <= sizeof(std::uint32_t))
+  {
+    static constexpr std::uint32_t max_idx = static_cast<std::uint32_t>(std::numeric_limits<std::size_t>::max());
+    const bool cannot_label_index = index.major == max_idx || index.minor == max_idx;
+    THROW_WALLET_EXCEPTION_IF(cannot_label_index, error::wallet_internal_error, "subaddress index out of range");
+  }
+
+  // resize subaddress labels just big enough that `m_subaddress_labels[index.major][index.minor]` is present
   if (m_subaddress_labels.size() <= index.major)
+    m_subaddress_labels.resize(index.major + 1, {"Untitled account"});
+  auto &subaddr_labels_in_account = m_subaddress_labels[index.major];
+  if (subaddr_labels_in_account.size() <= index.minor)
+    subaddr_labels_in_account.resize(index.minor + 1);
+  get_account_tags(); //trigger m_account_tags integrity checks
+
+  // compile all indices present in subaddress scanning map, as well as find highest major index
+  std::unordered_set<cryptonote::subaddress_index> all_indices;
+  std::uint32_t max_major_index = 0;
+  for (const auto &p : m_subaddresses)
   {
-    // add new accounts
-    cryptonote::subaddress_index index2;
-    const uint32_t major_end = get_subaddress_clamped_sum(index.major, m_subaddress_lookahead_major);
-    for (index2.major = m_subaddress_labels.size(); index2.major < major_end; ++index2.major)
+    all_indices.insert(p.second);
+    max_major_index = std::max(max_major_index, p.second.major);
+  }
+
+  // find lowest "missing" minor index in map, for all major indices
+  // this is an optimization which allows us to skip re-generating pubkeys that we already have
+  std::vector<std::uint32_t> missing_minor_index(max_major_index + 1);
+  for (std::uint32_t major = 0; major < missing_minor_index.size(); ++major)
+  {
+    std::uint32_t &minor = missing_minor_index[major];
+    while (minor < std::numeric_limits<std::uint32_t>::max())
     {
-      const uint32_t end = get_subaddress_clamped_sum((index2.major == index.major ? index.minor : 0), m_subaddress_lookahead_minor);
-      const std::vector<crypto::public_key> pkeys = hwdev.get_subaddress_spend_public_keys(m_account.get_keys(), index2.major, 0, end);
-      for (index2.minor = 0; index2.minor < end; ++index2.minor)
-      {
-         const crypto::public_key &D = pkeys[index2.minor];
-         m_subaddresses[D] = index2;
-      }
+      if (!all_indices.count({major, minor}))
+        break;
+      ++minor;
     }
-    m_subaddress_labels.resize(index.major + 1, {"Untitled account"});
-    m_subaddress_labels[index.major].resize(index.minor + 1);
-    get_account_tags();
   }
-  else if (m_subaddress_labels[index.major].size() <= index.minor)
+
+  // resize subaddress scanning map to highest historical received subaddress index plus lookahead
+  hw::device &hwdev = m_account.get_device();
+  const std::uint32_t major_base = std::max<std::uint32_t>(m_subaddress_labels.size(), 1) - 1;
+  const std::uint32_t major_end = get_subaddress_clamped_sum(major_base, m_subaddress_lookahead_major);
+  for (std::uint32_t major = 0; major < major_end; ++major)
   {
-    // add new subaddresses
-    const uint32_t end = get_subaddress_clamped_sum(index.minor, m_subaddress_lookahead_minor);
-    const uint32_t begin = m_subaddress_labels[index.major].size();
-    cryptonote::subaddress_index index2 = {index.major, begin};
-    const std::vector<crypto::public_key> pkeys = hwdev.get_subaddress_spend_public_keys(m_account.get_keys(), index2.major, index2.minor, end);
-    for (; index2.minor < end; ++index2.minor)
+    const std::size_t n_minor_labels = (major < m_subaddress_labels.size()) ? m_subaddress_labels.at(major).size() : 0;
+    const std::uint32_t minor_base = std::max<std::uint32_t>(n_minor_labels, 1) - 1;
+    const std::uint32_t minor_end = get_subaddress_clamped_sum(minor_base, m_subaddress_lookahead_minor);
+    const std::uint32_t minor_begin = (major < missing_minor_index.size()) ? missing_minor_index.at(major) : 0;
+    const std::vector<crypto::public_key> pkeys
+      = hwdev.get_subaddress_spend_public_keys(m_account.get_keys(), major, minor_begin, minor_end);
+    for (std::uint32_t minor = minor_begin; minor < minor_end; ++minor)
     {
-       const crypto::public_key &D = pkeys[index2.minor - begin];
-       m_subaddresses[D] = index2;
+      const crypto::public_key &D = pkeys.at(minor - minor_begin);
+      m_subaddresses[D] = {major, minor};
     }
-    m_subaddress_labels[index.major].resize(index.minor + 1);
   }
 }
 //----------------------------------------------------------------------------------------------------
@@ -1957,32 +1980,7 @@ void wallet2::set_subaddress_lookahead(size_t major, size_t minor)
   if (old_major_lookahead >= major && old_minor_lookahead >= minor)
     return;
 
-  // Expand the subaddresses map so that outputs received to the higher lookaheads will be identified in the scan loop
-  hw::device &hwdev = m_account.get_device();
-  cryptonote::subaddress_index index2;
-  const uint32_t max_major_idx = this->get_num_subaddress_accounts() > 0 ? (this->get_num_subaddress_accounts() - 1) : 0;
-  const uint32_t major_end = get_subaddress_clamped_sum(max_major_idx, major);
-  for (index2.major = 0; index2.major < major_end; ++index2.major)
-  {
-    // The existing minor addresses already set for this account
-    const uint32_t n_minor_subaddrs = this->get_num_subaddresses(index2.major);
-
-    // The subaddress lookahead is expected to expand from the max index in expand_subaddresses
-    const uint32_t max_minor_idx = n_minor_subaddrs > 0 ? (n_minor_subaddrs - 1) : 0;
-    const uint32_t begin = (n_minor_subaddrs || index2.major < old_major_lookahead) ? get_subaddress_clamped_sum(max_minor_idx, old_minor_lookahead) : 0;
-    // The expected new n minor subaddresses allocated for this account
-    const uint32_t end = get_subaddress_clamped_sum(max_minor_idx, minor);
-
-    if (begin >= end)
-      continue;
-
-    const std::vector<crypto::public_key> pkeys = hwdev.get_subaddress_spend_public_keys(m_account.get_keys(), index2.major, begin, end);
-    for (index2.minor = begin; index2.minor < end; ++index2.minor)
-    {
-      const crypto::public_key &D = pkeys.at(index2.minor - begin);
-      m_subaddresses[D] = index2;
-    }
-  }
+  expand_subaddresses({0, 0});
 }
 //----------------------------------------------------------------------------------------------------
 /*!
diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h
index 47b1753d7..41f1dffaa 100644
--- a/src/wallet/wallet2.h
+++ b/src/wallet/wallet2.h
@@ -1124,6 +1124,13 @@ private:
     size_t get_num_subaddress_accounts() const { return m_subaddress_labels.size(); }
     size_t get_num_subaddresses(uint32_t index_major) const { return index_major < m_subaddress_labels.size() ? m_subaddress_labels[index_major].size() : 0; }
     void add_subaddress(uint32_t index_major, const std::string& label); // throws when index is out of bound
+    /**
+     * brief: expand subaddress labels up to `index`, and scanning map to highest labeled index plus current lookahead
+     * param: index -
+     *
+     * All calls to `expand_subaddresses()` will *always* expand the subaddress map if the lookahead
+     * values have been increased since the last call.
+     */
     void expand_subaddresses(const cryptonote::subaddress_index& index);
     void create_one_off_subaddress(const cryptonote::subaddress_index& index);
     std::string get_subaddress_label(const cryptonote::subaddress_index& index) const;

As compared to current master, this mainly touches just wallet2::expand_subaddresses(), and calls wallet2::expand_subaddresses({0, 0}) in wallet2::set_subaddress_lookahead(). This keeps all the expansion logic in one place, as compared to the current iteration of this PR. It doesn't expand labels unnecessarily like my previous suggestion, and it also includes an optimization which speeds up expansion when the map is mostly present (which is useful for people which large lookaheads). I can remove the optimization to simplify this approach if desired.

@jeffro256
Copy link
Contributor

Here's a functional test that tests live transfers to subaddresses at high lookaheads, IDK, might be worth adding?:

diff --git a/tests/functional_tests/transfer.py b/tests/functional_tests/transfer.py
index 0df870e9c..f1fb6e51d 100755
--- a/tests/functional_tests/transfer.py
+++ b/tests/functional_tests/transfer.py
@@ -86,6 +86,7 @@ class TransferTest():
         self.check_subtract_fee_from_outputs()
         self.check_background_sync()
         self.check_background_sync_reorg_recovery()
+        self.check_subaddress_lookahead()
 
     def reset(self):
         print('Resetting blockchain')
@@ -1531,5 +1532,47 @@ class TransferTest():
         self.wallet[0].close_wallet()
         self.wallet[0].restore_deterministic_wallet(seed = seeds[0])
 
+    def check_subaddress_lookahead(self):
+        daemon = Daemon()
+
+        print('Testing transfers to subaddresses with large lookahead')
+
+        # From wallet 1 to wallet 0 at subaddress (0, 999)
+
+        address_0_999 = '8BQKgTSSqJjP14AKnZUBwnXWj46MuNmLvHfPTpmry52DbfNjjHVvHUk4mczU8nj8yZ57zBhksTJ8kM5xKeJXw55kCMVqyG7' # this is the address for address 999 of the main account in the test wallet
+        try:  # assert address_1_999 is not in the current pubkey table
+            self.wallet[0].get_address_index(address_0_999)
+            assert False # address should not already be loaded
+        except Exception as e:
+            assert str(e) ==  "{'error': {'code': -2, 'message': \"Address doesn't belong to the wallet\"}, 'id': '0', 'jsonrpc': '2.0'}"
+        # update the lookahead and assert the high index address is now in the table
+        self.wallet[0].set_subaddress_lookahead(50, 1000)
+        res = self.wallet[0].get_address_index(address_0_999)
+        assert res['index']['major'] == 0
+        assert res['index']['minor'] == 999
+
+        dst = {'address': address_0_999, 'amount': 454545454545}
+
+        self.wallet[1].refresh()
+        assert self.wallet[1].get_balance().balance > dst['amount']
+        self.wallet[1].transfer([dst])
+        daemon.generateblocks('46r4nYSevkfBUMhuykdK3gQ98XDqDTYW1hNLaXNvjpsJaSbNtdXh1sKMsdVgqkaihChAzEy29zEDPMR3NHQvGoZCLGwTerK', 1)
+        self.wallet[0].refresh()
+
+        res = self.wallet[0].get_balance()
+        balance_info_0_999 = None
+        for balance_info in res['per_subaddress']:
+            if balance_info['account_index'] == 0 and balance_info['address_index'] == 999:
+                balance_info_0_999 = balance_info
+                break
+        assert balance_info_0_999 is not None, "balance info for address (0, 999) not found"
+        assert balance_info_0_999['address'] == address_0_999
+        assert balance_info_0_999['balance'] == dst['amount']
+        assert balance_info_0_999['unlocked_balance'] == 0
+        assert balance_info_0_999['label'] == ''
+        assert balance_info_0_999['num_unspent_outputs'] == 1
+        assert balance_info_0_999['blocks_to_unlock'] == 9
+        assert balance_info_0_999['time_to_unlock'] == 0
+
 if __name__ == '__main__':
     TransferTest().run_test()

@j-berman
Copy link
Collaborator

j-berman commented Jul 1, 2025

I'm fine with de-duplicating logic, calling expand_subaddresses with {0,0}, and modifying expand_subaddresses to have correct behavior for both uses. Imo it's riskier because expand_subaddresses is currently used in a wide number of places (whereas setting the subaddress lookahead is rare), and so we need to make sure not to mess up expand_subaddresses here too, but it does yield easier-to-maintain code long-term, so good with me.

Looks like there are a couple problems with the updated expand_subaddresses in the patch:

  • It looks possible for minor_begin to be >= minor_end (e.g. after reducing the lookahead and then receiving, or even just receiving normally.. I think tests should catch this), which would cause get_subaddress_spend_public_keys to throw. Easily fixed by continuing if minor_begin >= minor_end (EDIT: get_subaddress_spend_public_keys would only throw if minor_begin > minor_end, and would do an unnecessary point de-compression if minor_begin == minor_end).
  • If someone has created a very high one-off subaddress, it will be identified as the max major, and that missing_minor_index vector will be unnecessarily huge.

@nahuhh
Copy link
Contributor Author

nahuhh commented Jul 3, 2025

Force pushed to remove extra line on EOF and to rerun tests (passing now)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
6 participants