Skip to content

Conversation

@moisesPompilio
Copy link
Collaborator

Description and Notes

Adds comprehensive integration tests for wallet functionality and fixes several issues related to descriptor persistence and handling.

Key fixes:

  • Duplicate descriptor prevention: Added centralized validation to prevent the same descriptor from being stored multiple times in the database. The push_descriptor method now checks if a descriptor already exists before adding it.

  • Improved xpub parsing and descriptor generation:

    • Xpub parsing now rejects xprivs and multisig-related xpubs
    • Fixed descriptor generation to dynamically create wpkh, pkh, or sh-wpkh descriptors based on the provided xpub (previously only wpkh was generated)
    • Added network validation to ensure xpubs match the node's network

key improvements:

  • Centralized address derivation: Address derivation logic has been moved to floresta-watch-only. When a descriptor is added, addresses are automatically derived and cached, simplifying wallet initialization and management.

  • Consolidated descriptor parsing: Moved parse_descriptors, parse_xpubs, and slip132 logic to the floresta-watch-only module, removing scattered code from floresta-node and floresta-common. Removed the miniscript dependency from floresta-common.

  • Documentation clarity: Updated wallet_descriptor option documentation to accurately describe descriptor behavior (previously incorrectly referred to xpubs).

  • Typos configuration: Added _typos.toml to prevent the typos checker from analyzing strings between 32-150 characters (hashes, keys, addresses, descriptors).

New integration tests:

  • wallet_conf.py: Tests wallet loading via config.toml, verifying correct descriptor/xpub loading and rejection of xprivs and private key descriptors

  • wallet_flag.py: Tests wallet loading via command-line flags with the same validations

  • RPC method tests for loaddescriptor (including duplicate detection and private key rejection) and listdescriptors

  • Constants centralization: Created constants.py to centralize all constants used across integration tests, improving maintainability.

How to verify the changes you have done?

  • Run the integration tests using the standard command:
    ./tests/run.sh
  • Verify that all new wallet tests (wallet_conf.py, wallet_flag.py, loaddescriptor, listdescriptors) pass successfully.
  • Test that duplicate descriptors are properly rejected when using loaddescriptor RPC method.
  • Confirm that xprivs and descriptors with private keys are rejected during wallet initialization.
  • Verify that xpubs are correctly validated against the node's network and generate appropriate descriptor types (wpkh, pkh, sh-wpkh).

@moisesPompilio moisesPompilio self-assigned this Feb 2, 2026
@moisesPompilio moisesPompilio added bug Something isn't working chore Cleaning, refactoring, reducing complexity code quality Generally improves code readability and maintainability functional tests RPC Changes something with our JSON-RPC interface Wallet Changes related to the watch-only wallet Integration Issues related to our integration tests labels Feb 2, 2026
@jaoleal
Copy link
Collaborator

jaoleal commented Feb 2, 2026

The first commit seems unrelated

@moisesPompilio
Copy link
Collaborator Author

The first commit seems unrelated

The first commit introduces constants.py which centralizes the constants used in the code that are later used in the wallet tests, to store the xpubs and descriptors.

Copy link
Collaborator

@jaoleal jaoleal left a comment

Choose a reason for hiding this comment

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

Concept ACK.

Heres some initial review

self.run_node(self.florestad)

self.log("Checking initial descriptors...")
descriptors = self.florestad.rpc.list_descriptors()
Copy link
Collaborator

Choose a reason for hiding this comment

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

One thing to add, both listdescriptors.py and loaddescriptor.oy is fine but they are dependent on each other, if one fails they will both fail...

that will be a problem if breaking one raises a false alarm about the other.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In integration tests it's common to have some interdependencies, such as between wallet_conf and wallet_flag. They also depend on listdescriptors, because that's the single method we use to check the descriptors Floresta has stored.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, fine to depend on listdescriptors, but listdescriptors test dont need to depend on loaddescriptor you can instantiate and insert a descriptor from the Config.toml. It can help for this case.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

But in this case, if there is a problem with listdescriptors, the wallet_conf and wallet_flag tests will also fail, because they also depend on that RPC.

/// Parses each descriptor, validates it, and derives the specified number of addresses
/// starting from the given index.
pub fn derive_addresses_from_list_descriptors(
descriptors: &[String],
Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The descriptor is already a String, so using a &[&str] will make us waste resources converting it to &str. It's better to use it as a String directly; this method is used inside the wallet internals which already obtain the descriptor as a String.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Theres no cost casting to a &str. but i understand thats how our codebase is written right now, fine.

IIUC theres no cost at all to cast Strings to &strs, but theres some when casting &str to String, basically the same relation between [B] and Vec<B>... Being picky with this may lead us to less runtime allocs.

The ideal is to declare functions that only read and compare strings with a generic S where S: AsRef<str>

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The problem here is that we can have multiple database implementations; so if we pass only pointers (&) to objects retrieved from the database it will be wrong, because the objects live shorter than the pointers. It's better to pass the object in this case, for example we pass a Vec<String> for descriptors, because if we passed Vec<&str> the &str would need to live longer than the Vec<String> that only exists inside the descriptor-retrieval function.

That's why when we receive a Vec<String> and it needs to be used by another function, that function takes a &[String] to avoid allocating memory by creating a new array of strings.

@Davidson-Souza
Copy link
Member

Needs rebase

@moisesPompilio
Copy link
Collaborator Author

c2bff7d Performed a rebase and added some review requests.

@Davidson-Souza
Copy link
Member

In ae433c1 there's still a REGTEST_RPC_SERVER constant in rpc/{floresta, utreexod}.py. I think it should also be moved?

@moisesPompilio
Copy link
Collaborator Author

In ae433c1 there's still a REGTEST_RPC_SERVER constant in rpc/{floresta, utreexod}.py. I think it should also be moved?

I didn't add them because they will be removed in this PR #733 .

@moisesPompilio
Copy link
Collaborator Author

5ce80a0 I did the rebase, adjusted some parts of the code that were using String where they could use &str, and removed the descriptors-no-std feature from CI (introduced by PR #825).

@jaoleal jaoleal self-requested a review February 9, 2026 10:07
Created a `constants.py` file to centralize all constants used in the
test framework. This change improves maintainability by providing a single
location for managing constants and makes it easier to understand their
purpose and usage across the tests.
Updated the documentation for the `wallet_descriptor` option to explicitly
describe its behavior with descriptors. The previous text incorrectly referred
to xpubs, which could cause confusion.
Previously, it was possible to save the same descriptor multiple times,
leading to redundancy and potential inconsistencies. Adds a verification
step in the `push_descriptor` method to ensure that a descriptor is only
added if it does not already exist.
- Updated xpub parsing to reject xpriv and xpubs related to multisig, ensuring
only standard public keys are accepted for descriptor generation.
- Fixed an issue in descriptor generation by xpub where only `wpkh`
descriptors were being created. Descriptors are now generated dynamically
as `wpkh`, `pkh`, or `sh-wpkh`, depending on the xpub provided.
- Added a network validation step to ensure the xpub matches the network
the node is running on.
- Added \_typos\.toml configuration to prevent https://github.com/crate-ci/typos
from analyzing words with lengths between 32 and 150 characters, as these
correspond to hashes, public keys, private keys, XPUBs, descriptors, and
Bitcoin addresses.
- Added `wallet_conf.py` to test wallet loading via `config.toml`.
This test verifies:
  - Descriptors and XPUBs are correctly loaded from the configuration file.
  - XPRIVs and descriptors with private keys are rejected.

- Added `wallet_flag.py` to test wallet loading via command-line flags.
This test performs the same checks as `wallet_conf.py`, but uses flags to
pass wallet information during node initialization.
- Added tests to verify that the `loaddescriptor` RPC method can successfully
load a descriptor.
- Ensured that duplicate descriptors cannot be loaded.
- Verified that descriptors containing private keys are rejected.
…thod

- Added tests to verify that the `listdescriptors` RPC method correctly
displays the descriptors loaded in the node.
- Ensured that the method returns all loaded descriptors in the expected
format.
…-only` module

 Improved code organization by consolidating descriptor handling in a single module

- Moved `parse_descriptors`, `parse_xpubs`, and `slip132` to the
`floresta-watch-only` module.
- Centralized descriptor and XPUB parsing logic, previously scattered
across `floresta-node` and `floresta-common`.
- Removed descriptor-related features and the `miniscript` dependency
from `floresta-common`.
… and refactor wallet handling

- Centralized address derivation in `floresta-watch-only`:
  - Address derivation is now automatically performed when pushing a descriptor.
  - Added `push_xpub` method to convert `XPUBs` into `descriptors`, push them,
    and derive addresses for the wallet.

- Removed `walletInput` file from floresta-node:
  - This file was no longer necessary as its functionality (`XPUB`, `descriptor`,
    and address derivation) is now handled automatically by `floresta-watch-only`.
@moisesPompilio
Copy link
Collaborator Author

7f9b79b I did the rebase.

WatchOnlyError::DatabaseError(e) => {
write!(f, "Database error: {e:?}")
}
WatchOnlyError::DescriptorDuplicate => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

s/DescriptorDuplicate/DuplicateDescriptor

Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe it could receive a String to tell which descriptor was skipped, that way we can use just its to_string method, or inside a format! to have a well defined and standardized error message

Comment on lines 749 to 754
if let Err(e) = wallet.push_descriptor(&descriptor.to_string()) {
if let WatchOnlyError::DescriptorDuplicate = e {
warn!("Descriptor already exists in wallet, skipping: {descriptor}");
} else {
return Err(FlorestadError::from(e));
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

      match wallet.push_descriptor(&descriptor.to_string()) {
                Ok(()) => debug!("Descriptor added: {descriptor}"),
                Err(WatchOnlyError::DuplicateDescriptor) => {
                    warn!("Descriptor already exists in wallet, skipping: {descriptor}")
                }
                Err(e) => return Err(FlorestadError::CouldNotInitializeWallet(e)),
            }

let inner = self.inner.read().expect("poisoned lock");
let known_descs = inner.database.descs_get()?;
Ok(known_descs.contains(desc))
Ok(known_descs.contains(&String::from(desc)))
Copy link
Collaborator

Choose a reason for hiding this comment

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

i think you dont need to cast it to a String here

}

pub fn push_descriptor(&self, descriptor: &str) -> Result<(), WatchOnlyError<D::Error>> {
if self.is_cached(&String::from(descriptor))? {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Neither here.

fn parse_xpubs(xpubs: &[String]) -> Result<Vec<Descriptor<DescriptorPublicKey>>, FlorestadError> {
fn parse_xpubs(
xpubs: &[String],
network: Network,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you dont need to take ownership to read enums

if let FlorestadError::XpubNetworkMismatch(actual) = err {
assert_eq!(actual, xpub.to_string());
} else {
panic!("Expected XpubNetworkMismatch error");
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: you can skip this else by using an early continue


def load_descriptor(self, descriptor: str) -> dict:
"""
Load a descriptor into the node performing
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this a complete doc string ?

/// compute the average from it. This is not efficient, as it requires O(n) memory and O(n)
/// time to compute the average. Instead, we can use a fraction to compute the average in O(1)
/// time and O(1) memory, by keeping track of the sum of all elements and the number of elements.
pub struct FractionAvg {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this diff messed ?

Comment on lines 760 to 766
if let Err(e) = wallet.push_xpub(&xpub, self.config.network) {
if let WatchOnlyError::DescriptorDuplicate = e {
warn!("Descriptor for the provided XPUB already exists in the wallet. Skipping: {xpub}");
} else {
return Err(FlorestadError::from(e));
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Prefer a match expression just as i suggested earlier

Comment on lines +440 to +444
if let Err(DescriptorError::MiniscriptError(_)) = result {
// Expected error
} else {
panic!("Expected MiniscriptError");
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

 if Err(DescriptorError::MiniscriptError(_)) =! result {
                panic!("Expected MiniscriptError");
            }

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

Labels

bug Something isn't working chore Cleaning, refactoring, reducing complexity code quality Generally improves code readability and maintainability functional tests Integration Issues related to our integration tests RPC Changes something with our JSON-RPC interface Wallet Changes related to the watch-only wallet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants