diff --git a/key-wallet/src/tests/special_transaction_matching_tests.rs b/key-wallet/src/tests/special_transaction_matching_tests.rs index 5f62ebf38..bd2d2876e 100644 --- a/key-wallet/src/tests/special_transaction_matching_tests.rs +++ b/key-wallet/src/tests/special_transaction_matching_tests.rs @@ -432,6 +432,97 @@ async fn provider_registration_with_operator_public_key_matches_provider_operato assert_matched_account_type(&result, AccountTypeToCheck::ProviderOperatorKeys); } +/// After a ProRegTx uses an operator key, gap-limit maintenance must extend +/// the operator pool with freshly derived BLS keys — exactly like the ECDSA +/// owner/voting pools. Regression test for the operator pool being skipped +/// because the wallet exposes no ECDSA xpub for BLS accounts (the checker +/// bailed before `maintain_gap_limit`). +#[cfg(feature = "bls")] +#[tokio::test] +async fn provider_registration_extends_operator_key_gap_limit() { + use crate::managed_account::address_pool::PublicKeyType; + use crate::managed_account::managed_account_type::ManagedAccountType; + use crate::AccountType; + + let (mut wallet, mut info) = make_wallet(); + let (_other_wallet, mut other_info) = make_wallet(); + + let owner_addr = other_info + .provider_owner_keys_managed_account_mut() + .expect("other owner") + .next_address(None, true) + .expect("derive owner"); + let voting_addr = other_info + .provider_voting_keys_managed_account_mut() + .expect("other voting") + .next_address(None, true) + .expect("derive voting"); + // Takes operator key 0 and marks it used, so the pool's unused window + // shrinks below the gap limit. + let operator_pk = info + .provider_operator_keys_managed_account_mut() + .expect("provider_operator_keys managed") + .next_bls_operator_key(None, true) + .expect("derive operator"); + + let operator_pool_state = |info: &ManagedWalletInfo| { + let account = + info.provider_operator_keys_managed_account().expect("operator managed account"); + match account.managed_account_type() { + ManagedAccountType::ProviderOperatorKeys { + addresses, + } => (addresses.highest_generated, addresses.highest_used, addresses.gap_limit), + _ => unreachable!("operator account holds an operator pool"), + } + }; + + let (highest_generated_before, highest_used_before, gap_limit) = operator_pool_state(&info); + assert_eq!(highest_used_before, Some(0)); + assert!( + highest_generated_before < Some(gap_limit), + "setup: the pre-generated window must not already satisfy the gap limit" + ); + + let tx = prov_reg_tx( + ProviderMasternodeType::Regular, + derive_pubkey_hash(&owner_addr), + derive_pubkey_hash(&voting_addr), + operator_pk.0.to_compressed().into(), + ScriptBuf::new(), + None, + ); + let result = + info.check_core_transaction(&tx, test_block_context(), &mut wallet, true, true).await; + + assert_matched_account_type(&result, AccountTypeToCheck::ProviderOperatorKeys); + + // The pool must now hold `gap_limit` unused keys past the used index 0. + let (highest_generated_after, highest_used_after, _) = operator_pool_state(&info); + assert_eq!(highest_used_after, Some(0)); + assert_eq!(highest_generated_after, Some(gap_limit)); + + // The freshly derived keys are surfaced to callers and carry BLS public + // keys matching the audited gate-free derivation entry point. + let bls_account = wallet + .accounts + .bls_account_of_type(AccountType::ProviderOperatorKeys) + .expect("operator account exists"); + let new_operator_keys: Vec<_> = result + .new_addresses + .iter() + .filter(|derived| derived.account_type == AccountType::ProviderOperatorKeys) + .collect(); + assert!(!new_operator_keys.is_empty(), "gap maintenance must derive new operator keys"); + for derived in new_operator_keys { + let expected = bls_account + .operator_public_key_at(derived.info.index) + .expect("gate-free public derivation") + .to_bytes() + .to_vec(); + assert_eq!(derived.info.public_key, Some(PublicKeyType::BLS(expected))); + } +} + #[cfg(feature = "eddsa")] #[tokio::test] async fn provider_registration_with_platform_node_id_matches_provider_platform_keys() { diff --git a/key-wallet/src/transaction_checking/wallet_checker.rs b/key-wallet/src/transaction_checking/wallet_checker.rs index 356ce45da..7ea653924 100644 --- a/key-wallet/src/transaction_checking/wallet_checker.rs +++ b/key-wallet/src/transaction_checking/wallet_checker.rs @@ -178,14 +178,13 @@ impl WalletTransactionChecker for ManagedWalletInfo { account.mark_address_used(&address_info.address); } - let Some(xpub) = wallet.extended_public_key_for_account_type( + let key_source = wallet.key_source_for_account_type( &account_match.account_type_match.to_account_type_to_check(), account_match.account_type_match.account_index(), - ) else { + ); + if matches!(key_source, KeySource::NoKeySource) { continue; - }; - - let key_source = KeySource::Public(xpub); + } let rev_before = result.new_addresses.len(); let owning_account_type = account.managed_account_type().to_account_type(); for pool in account.managed_account_type_mut().address_pools_mut() { diff --git a/key-wallet/src/wallet/helper.rs b/key-wallet/src/wallet/helper.rs index a282804e8..a7cfc209c 100644 --- a/key-wallet/src/wallet/helper.rs +++ b/key-wallet/src/wallet/helper.rs @@ -616,4 +616,38 @@ impl Wallet { } } } + + /// Get a [`crate::KeySource`] capable of public (watch-side) derivation + /// for a specific account type. + /// + /// This is the key-source counterpart of + /// [`Self::extended_public_key_for_account_type`]: ECDSA accounts yield + /// [`crate::KeySource::Public`], while the BLS provider operator account + /// yields [`crate::KeySource::BLSPublic`] (legacy-scheme non-hardened + /// derivation) so gap-limit maintenance can extend the operator key pool + /// just like the owner/voting pools. + /// + /// Returns [`crate::KeySource::NoKeySource`] for account types that + /// cannot derive publicly: the Ed25519 platform node account (SLIP-0010 + /// supports hardened derivation only) and Dashpay accounts (not + /// retrieved via this helper). + pub fn key_source_for_account_type( + &self, + account_type: &crate::transaction_checking::transaction_router::AccountTypeToCheck, + account_index: Option, + ) -> crate::KeySource { + match account_type { + #[cfg(feature = "bls")] + crate::transaction_checking::transaction_router::AccountTypeToCheck::ProviderOperatorKeys => self + .accounts + .provider_operator_keys + .as_ref() + .map(|a| crate::KeySource::BLSPublic(a.bls_public_key.clone())) + .unwrap_or(crate::KeySource::NoKeySource), + _ => self + .extended_public_key_for_account_type(account_type, account_index) + .map(crate::KeySource::Public) + .unwrap_or(crate::KeySource::NoKeySource), + } + } }