Skip to content

Conversation

@PastaPastaPasta
Copy link
Member

@PastaPastaPasta PastaPastaPasta commented Feb 2, 2026

this is a vesion of #368, with cleaned git history by codex. there should be no changes in the diff

  • Updated references from ManagedAccount to ManagedCoreAccount throughout the codebase to align with the new account structure.
  • Changed balance handling from WalletBalance to WalletCoreBalance to reflect the updated balance management.
  • Adjusted all related functions and tests to ensure compatibility with the new account and balance types.
  • Removed deprecated handling for Platform Payment accounts in transaction checks, as they operate separately from Core transactions.
  • Added a way to update platform account balances.

This update enhances the overall architecture by standardizing account management and balance tracking across the application.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for Platform Payment accounts (DIP-17) with dedicated balance management and address handling for platform-specific operations.
  • Refactoring

    • Reorganized account type handling to distinguish between core and platform-specific accounts.
    • Renamed transaction checking methods for clarity and consistency across the codebase.
  • Bug Fixes

    • Narrowed SegWit transaction handling to only apply to specific transaction types, improving compatibility.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 2, 2026

📝 Walkthrough

Walkthrough

This PR refactors the wallet account system by renaming ManagedAccount to ManagedCoreAccount, introduces ManagedPlatformAccount for DIP-17 platform payment addresses, renames WalletBalance to WalletCoreBalance, updates transaction checking to use CoreAccountTypeMatch, and simplifies SegWit/witness handling in blockdata. FFI APIs are updated comprehensively to reflect these changes.

Changes

Cohort / File(s) Summary
Core Account Type Renaming
key-wallet/src/managed_account/mod.rs, key-wallet/src/account/mod.rs, key-wallet/src/lib.rs, key-wallet-ffi/src/managed_account.rs, key-wallet-ffi/src/address_pool.rs, key-wallet-ffi/src/managed_account_collection.rs, key-wallet-ffi/src/managed_wallet.rs, key-wallet-ffi/tests/*
Renamed ManagedAccount to ManagedCoreAccount throughout, updated struct definitions, impl blocks, constructors, and all associated references and type hints.
Balance Type Renaming
key-wallet/src/wallet/balance.rs, key-wallet/src/wallet/mod.rs, key-wallet/src/wallet/managed_wallet_info/mod.rs, key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs, key-wallet/src/managed_account/managed_account_trait.rs, key-wallet-ffi/src/types.rs, key-wallet-manager/src/wallet_manager/mod.rs
Renamed WalletBalance to WalletCoreBalance across all imports, struct definitions, trait implementations, and method signatures.
Platform Payment Account Support
key-wallet/src/managed_account/managed_platform_account.rs, key-wallet/src/managed_account/platform_address.rs, key-wallet/src/transaction_checking/platform_checker.rs
Introduced new ManagedPlatformAccount struct with balance tracking, PlatformP2PKHAddress type for DIP-17 addresses, and WalletPlatformChecker trait for platform balance management.
Transaction Checking Refactor
key-wallet/src/transaction_checking/account_checker.rs, key-wallet/src/transaction_checking/wallet_checker.rs, key-wallet/src/transaction_checking/transaction_router/mod.rs, key-wallet/src/transaction_checking/transaction_router/tests/*, key-wallet-ffi/src/transaction_checking.rs
Renamed AccountTypeMatch to CoreAccountTypeMatch, replaced check_transaction with check_core_transaction, removed PlatformPayment variant handling from core checks, and added PlatformAccountConversionError for failed conversions.
Account Type Conversion Changes
key-wallet/src/account/account_type.rs, key-wallet/src/transaction_checking/transaction_router/mod.rs
Updated from From to TryFrom for AccountType/ManagedAccountType conversions to AccountTypeToCheck, with explicit error handling for PlatformPayment variants.
Managed Account Collection Updates
key-wallet/src/managed_account/managed_account_collection.rs, key-wallet-ffi/src/managed_account_collection.rs
Updated to use ManagedCoreAccount and new ManagedPlatformAccount, made insert return Result, added insert_platform_account, expanded platform payment account accessors.
FFI API Overhaul
key-wallet-ffi/include/key_wallet_ffi.h, key-wallet-ffi/FFI_API.md, key-wallet-ffi/cbindgen.toml
Renamed all FFI types from FFIManagedAccount to FFIManagedCoreAccount, added FFIManagedPlatformAccount and FFIPlatformPaymentAccountKey, updated 40+ function signatures, added platform account accessors and collection methods.
Wallet Helper Updates
key-wallet/src/wallet/helper.rs, key-wallet/src/wallet/managed_wallet_info/helpers.rs, key-wallet/src/wallet/managed_wallet_info/managed_accounts.rs
Updated return types to ManagedCoreAccount, removed PlatformPayment xpub handling, added new platform payment account helper methods for querying and mutation.
Transaction Handling Simplification
key-wallet-ffi/src/transaction.rs, dash/src/blockdata/transaction/mod.rs
Replaced check_transaction with check_core_transaction calls; narrowed SegWit/have_witness forcing to only AssetUnlock transactions, removing forced-false behavior for QuorumCommitment, MnhfSignal, and Coinbase.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Wallet as ManagedWalletInfo
    participant Collection as ManagedAccountCollection
    participant PlatAccount as ManagedPlatformAccount
    participant AddressPool as AddressPool

    Client->>Wallet: get_platform_payment_account(account_idx, key_class)
    Wallet->>Collection: get_platform_account(key)
    Collection->>PlatAccount: return reference
    PlatAccount->>AddressPool: maintain_gap_limit()
    AddressPool->>PlatAccount: next_unused_platform_address()
    PlatAccount->>PlatAccount: set_address_credit_balance()
    PlatAccount->>Client: return PlatformP2PKHAddress
Loading
sequenceDiagram
    participant Transaction as Transaction
    participant Wallet as ManagedWalletInfo
    participant Checker as WalletPlatformChecker
    participant PlatAccounts as Platform Accounts

    Transaction->>Wallet: check_core_transaction()
    Wallet->>Checker: set_platform_address_balance()
    Checker->>PlatAccounts: find_account_for_address()
    PlatAccounts->>PlatAccounts: add_address_credit_balance()
    PlatAccounts->>Checker: return updated balance
    Checker->>Wallet: balance updated
    Wallet->>Transaction: return TransactionCheckResult
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • ZocoLini

Poem

🐰 Core accounts hop with gleaming names so new,
Platform payments bloom in addresses of true,
Balance types renamed from old to bright,
Transaction checks redirected left and right,
Witness handling narrowed just right,
DIP-17 addresses shine with all their might! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly summarizes the primary change: introducing a new managed platform account system to track platform balances, which aligns with the extensive refactoring throughout the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/pr-368-clean

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
dash/src/blockdata/transaction/mod.rs (1)

597-606: ⚠️ Potential issue | 🟡 Minor

Comment is incomplete — decoding also disables SegWit for QuorumCommitment and MnhfSignal.

The comment on line 597 only mentions AssetUnlock, but lines 601-606 also force segwit = false for QuorumCommitment and MnhfSignal. This creates a discrepancy between the documentation and actual behavior.

Note: The asymmetry with the encoding path (which only handles AssetUnlock) may be intentional for backward compatibility when decoding legacy transactions, but the comment should reflect the complete behavior.

📝 Suggested comment update
-        // Forcing segwit to false for AssetUnlock, as currently Core doesn't support BIP141 SegWit.
+        // Forcing segwit to false for AssetUnlock, QuorumCommitment, and MnhfSignal,
+        // as currently Core doesn't support BIP141 SegWit for these special transaction types.
key-wallet-ffi/src/managed_wallet.rs (1)

975-985: ⚠️ Potential issue | 🟠 Major

Use address_array_free for external range cleanup.

The external range is allocated via Rust (Box::into_raw), so freeing it with libc::free is UB on non‑system allocators. Use the same address_array_free helper as the internal range test and avoid manually freeing individual CStrings.

🔧 Suggested fix
-        unsafe {
-            let addresses = std::slice::from_raw_parts(addresses_out, count_out);
-            for &addr_ptr in addresses {
-                let addr_str = CStr::from_ptr(addr_ptr).to_string_lossy();
-                assert!(!addr_str.is_empty());
-                println!("External address: {}", addr_str);
-                let _ = CString::from_raw(addr_ptr);
-            }
-            libc::free(addresses_out as *mut libc::c_void);
-        }
+        unsafe {
+            let addresses = std::slice::from_raw_parts(addresses_out, count_out);
+            for &addr_ptr in addresses {
+                let addr_str = CStr::from_ptr(addr_ptr).to_string_lossy();
+                assert!(!addr_str.is_empty());
+                println!("External address: {}", addr_str);
+            }
+            crate::address::address_array_free(addresses_out, count_out);
+        }

Based on learnings: Be careful with FFI memory management.

key-wallet-ffi/src/managed_account.rs (1)

205-215: ⚠️ Potential issue | 🟠 Major

Free FFIError messages before returning error results.

When wallet_manager_get_managed_wallet_info fails, error.message is allocated but never freed; the returned result contains a new CString, so the original message leaks. Please free the original FFIError message before returning. (Apply the same pattern in each early-return path.)

🔧 Suggested fix (example for `managed_wallet_get_account`)
-    if managed_wallet_ptr.is_null() {
-        return FFIManagedCoreAccountResult::error(
-            error.code,
-            if error.message.is_null() {
-                "Failed to get managed wallet info".to_string()
-            } else {
-                let c_str = std::ffi::CStr::from_ptr(error.message);
-                c_str.to_string_lossy().to_string()
-            },
-        );
-    }
+    if managed_wallet_ptr.is_null() {
+        let message = if error.message.is_null() {
+            "Failed to get managed wallet info".to_string()
+        } else {
+            let c_str = std::ffi::CStr::from_ptr(error.message);
+            c_str.to_string_lossy().to_string()
+        };
+        error.free_message();
+        return FFIManagedCoreAccountResult::error(error.code, message);
+    }

Based on learnings: Be careful with FFI memory management.

Also applies to: 317-327, 388-397, 450-457, 1103-1113

🤖 Fix all issues with AI agents
In `@key-wallet-ffi/FFI_API.md`:
- Around line 1276-1281: The generated Markdown uses inline `*` that
Markdownlint treats as emphasis in the "Arguments" section of the Platform
Payment account doc; update the generator to emit a proper bulleted list (one
`-` or `*` item per line) for the three arguments `wallet`, `account_index`, and
`key_class`, ensuring a blank line before the list so each becomes a standalone
list item rather than inline emphasis and thus fixes MD037.

In `@key-wallet-ffi/src/address_pool.rs`:
- Around line 498-508: Before calling to_account_type() on account_type_rust,
guard against variants that require extra params (PlatformPayment,
DashpayReceivingFunds, DashpayExternalAccount) so the conversion cannot panic
and unwind across the FFI; check account_type_rust first and if it matches any
of those forbidden variants call FFIError::set_error(error,
FFIErrorCode::InvalidInput, "Platform Payment accounts cannot be used for
address pool operations".to_string()) and return false, otherwise proceed to
call account_type_rust.try_into() (producing account_type_to_check) and continue
as before. Ensure you reference the existing symbols account_type_rust,
to_account_type(), account_type_to_check, FFIError::set_error,
FFIErrorCode::InvalidInput and error when implementing the guard.

In `@key-wallet-ffi/src/wallet.rs`:
- Around line 939-987: Validate the untrusted FFI inputs account_index and
key_class in wallet_add_platform_payment_account before creating
AccountType::PlatformPayment: explicitly check that both values are less than
2^31 (i.e. reject >= 2_147_483_648) and return
crate::types::FFIAccountResult::error with FFIErrorCode::InvalidInput and a
clear message if out of range; only after these guards succeed proceed to
construct AccountType::PlatformPayment and call w.add_account (so you never pass
invalid indices to derivation/ChildNumber::from_hardened_idx).

In `@key-wallet/src/managed_account/managed_platform_account.rs`:
- Around line 363-392: The Decode impl for ManagedPlatformAccount currently
trusts decoded network and credit_balance; update the bincode::Decode::decode
implementation to enforce invariants by (1) verifying that every key in
address_balances and addresses.network are consistent (or at minimum set network
= addresses.network) and (2) recomputing credit_balance as the sum of values in
address_balances and using that computed total instead of the decoded
credit_balance when constructing Self; make these adjustments inside the Decode
for ManagedPlatformAccount (the function that decodes account, key_class,
network, credit_balance, address_balances, addresses, metadata, is_watch_only)
so the returned instance always has network == addresses.network and
credit_balance == sum(address_balances).

In `@key-wallet/src/managed_account/platform_address.rs`:
- Around line 62-71: The to_address method uses
Hash160::from_slice(&self.0).expect(...) which calls expect in library code;
replace that with the infallible Hash160::from_byte_array(self.0) (or equivalent
method taking [u8;20]) to construct pubkey_hash without panicking, then build
Payload::PubkeyHash(pubkey_hash.into()) and return Address::new(network,
payload); update references to self.0 accordingly and remove the expect call.
🧹 Nitpick comments (1)
key-wallet/src/wallet/balance.rs (1)

55-58: Consider checked arithmetic for total() to avoid panic in library code.

The total() method can panic on overflow (as demonstrated in test_balance_overflow). While unlikely in practice, library code guidelines recommend avoiding panics. Consider returning Result<u64, _> or using saturating_add.

♻️ Optional: Use saturating arithmetic
     /// Get the total balance.
-    pub fn total(&self) -> u64 {
-        self.spendable + self.unconfirmed + self.immature + self.locked
+    pub fn total(&self) -> u64 {
+        self.spendable
+            .saturating_add(self.unconfirmed)
+            .saturating_add(self.immature)
+            .saturating_add(self.locked)
     }

As per coding guidelines: "Avoid unwrap() and expect() in library code; use explicit error types" - the same principle applies to operations that can panic.

Comment on lines +1276 to +1281
**Description:**
Add a Platform Payment account (DIP-17) to the wallet Platform Payment accounts use the derivation path: `m/9'/coin_type'/17'/account'/key_class'/index` # Arguments * `wallet` - Pointer to the wallet * `account_index` - The account index (hardened) in the derivation path * `key_class` - The key class (hardened) - typically 0' for main addresses # Safety This function dereferences a raw pointer to FFIWallet. The caller must ensure that: - The wallet pointer is either null or points to a valid FFIWallet - The FFIWallet remains valid for the duration of this call

**Safety:**
This function dereferences a raw pointer to FFIWallet. The caller must ensure that: - The wallet pointer is either null or points to a valid FFIWallet - The FFIWallet remains valid for the duration of this call

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix MD037 inline emphasis spacing by breaking the arguments list into bullets.
Markdownlint flags the inline * list as emphasis with spaces; please update the generator to emit a proper list.

📝 Proposed formatting fix (update generator)
-Add a Platform Payment account (DIP-17) to the wallet  Platform Payment accounts use the derivation path: `m/9'/coin_type'/17'/account'/key_class'/index`  # Arguments * `wallet` - Pointer to the wallet * `account_index` - The account index (hardened) in the derivation path * `key_class` - The key class (hardened) - typically 0' for main addresses  # Safety  This function dereferences a raw pointer to FFIWallet. The caller must ensure that: - The wallet pointer is either null or points to a valid FFIWallet - The FFIWallet remains valid for the duration of this call
+Add a Platform Payment account (DIP-17) to the wallet. Platform Payment accounts use the derivation path: `m/9'/coin_type'/17'/account'/key_class'/index`
+
+**Arguments:**
+- `wallet` - Pointer to the wallet
+- `account_index` - The account index (hardened) in the derivation path
+- `key_class` - The key class (hardened) - typically 0' for main addresses
+
+# Safety
+This function dereferences a raw pointer to FFIWallet. The caller must ensure that:
+- The wallet pointer is either null or points to a valid FFIWallet
+- The FFIWallet remains valid for the duration of this call
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

[warning] 1277-1277: Spaces inside emphasis markers

(MD037, no-space-in-emphasis)


[warning] 1277-1277: Spaces inside emphasis markers

(MD037, no-space-in-emphasis)

🤖 Prompt for AI Agents
In `@key-wallet-ffi/FFI_API.md` around lines 1276 - 1281, The generated Markdown
uses inline `*` that Markdownlint treats as emphasis in the "Arguments" section
of the Platform Payment account doc; update the generator to emit a proper
bulleted list (one `-` or `*` item per line) for the three arguments `wallet`,
`account_index`, and `key_class`, ensuring a blank line before the list so each
becomes a standalone list item rather than inline emphasis and thus fixes MD037.

Comment on lines +498 to +508
let account_type_to_check = match account_type_rust.try_into() {
Ok(check_type) => check_type,
Err(_) => {
FFIError::set_error(
error,
FFIErrorCode::InvalidInput,
"Platform Payment accounts cannot be used for address pool operations".to_string(),
);
return false;
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's find and examine the to_account_type implementation
echo "=== Searching for to_account_type implementation ==="
rg -n "fn to_account_type" key-wallet-ffi/src/

echo -e "\n=== Looking for account type conversions and panic patterns ==="
rg -n "PlatformPayment|DashpayReceivingFunds|DashpayExternalAccount|unreachable\!|panic\!" key-wallet-ffi/src/types.rs -A 2 -B 2

echo -e "\n=== Checking call sites of to_account_type in address_pool.rs ==="
rg -n "to_account_type" key-wallet-ffi/src/address_pool.rs -B 5 -A 10

Repository: dashpay/rust-dashcore

Length of output: 2406


🏁 Script executed:

#!/bin/bash
# Read the to_account_type implementation
echo "=== to_account_type implementation ==="
sed -n '190,220p' key-wallet-ffi/src/types.rs

echo -e "\n=== Look for unimplemented/panic for account types ==="
rg -n "unimplemented|panic" key-wallet-ffi/src/types.rs -B 2 -A 2

echo -e "\n=== Look for AccountType enum definition ==="
rg -n "enum FFIAccountType" key-wallet-ffi/src/types.rs -A 20

echo -e "\n=== Check try_into/Into implementations for AccountType ==="
rg -n "impl.*Into.*AccountType|impl.*TryInto" key-wallet-ffi/src/types.rs -A 30

Repository: dashpay/rust-dashcore

Length of output: 6472


🏁 Script executed:

#!/bin/bash
# Check context around the other two call sites
echo "=== Call site 1 (line 293) - full context ==="
sed -n '275,310p' key-wallet-ffi/src/address_pool.rs

echo -e "\n=== Call site 2 (line 399) - full context ==="
sed -n '385,420p' key-wallet-ffi/src/address_pool.rs

echo -e "\n=== Verify which account types are valid for address pools ==="
rg -n "get_managed_account_by_type|address pool" key-wallet-ffi/src/address_pool.rs -B 2 -A 2 | head -40

Repository: dashpay/rust-dashcore

Length of output: 4176


🏁 Script executed:

#!/bin/bash
# Search for all calls to to_account_type in the entire codebase
echo "=== All calls to to_account_type in key-wallet-ffi ==="
rg -n "to_account_type\(" key-wallet-ffi/src/ -B 3 | grep -E "\.rs:|to_account_type"

echo -e "\n=== Check if there's any pre-validation of account types in the codebase ==="
rg -n "matches.*FFIAccountType|if.*account_type.*==" key-wallet-ffi/src/ | head -20

Repository: dashpay/rust-dashcore

Length of output: 2306


🏁 Script executed:

#!/bin/bash
# Check wallet.rs call sites for error handling
echo "=== wallet.rs line 520 context ==="
sed -n '510,535p' key-wallet-ffi/src/wallet.rs

echo -e "\n=== wallet.rs line 749 context ==="
sed -n '740,765p' key-wallet-ffi/src/wallet.rs

echo -e "\n=== wallet.rs line 872 context ==="
sed -n '863,888p' key-wallet-ffi/src/wallet.rs

echo -e "\n=== managed_account.rs line 218 context ==="
sed -n '210,235p' key-wallet-ffi/src/managed_account.rs

Repository: dashpay/rust-dashcore

Length of output: 3724


🏁 Script executed:

#!/bin/bash
# Verify the exact code at lines 498-508 in address_pool.rs
echo "=== address_pool.rs lines 498-508 ==="
sed -n '498,508p' key-wallet-ffi/src/address_pool.rs

# Also check the function signature
echo -e "\n=== Function containing lines 498-508 ==="
sed -n '460,475p' key-wallet-ffi/src/address_pool.rs

Repository: dashpay/rust-dashcore

Length of output: 960


Add account type validation before to_account_type() to prevent FFI panics.

The to_account_type() call at line 496 panics for PlatformPayment, DashpayReceivingFunds, and DashpayExternalAccount without the required identity/index parameters. This panic can unwind across the FFI boundary. The try_into() error handler at line 498 is unreachable for these types.

Validate the account type before calling to_account_type():

Suggested guard
+    if matches!(
+        account_type,
+        FFIAccountType::PlatformPayment
+            | FFIAccountType::DashpayReceivingFunds
+            | FFIAccountType::DashpayExternalAccount
+    ) {
+        FFIError::set_error(
+            error,
+            FFIErrorCode::InvalidInput,
+            "Account type not supported for address pool operations".to_string(),
+        );
+        return false;
+    }
+
     let account_type_rust = account_type.to_account_type(account_index);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let account_type_to_check = match account_type_rust.try_into() {
Ok(check_type) => check_type,
Err(_) => {
FFIError::set_error(
error,
FFIErrorCode::InvalidInput,
"Platform Payment accounts cannot be used for address pool operations".to_string(),
);
return false;
}
};
if matches!(
account_type,
FFIAccountType::PlatformPayment
| FFIAccountType::DashpayReceivingFunds
| FFIAccountType::DashpayExternalAccount
) {
FFIError::set_error(
error,
FFIErrorCode::InvalidInput,
"Account type not supported for address pool operations".to_string(),
);
return false;
}
let account_type_rust = account_type.to_account_type(account_index);
let account_type_to_check = match account_type_rust.try_into() {
Ok(check_type) => check_type,
Err(_) => {
FFIError::set_error(
error,
FFIErrorCode::InvalidInput,
"Platform Payment accounts cannot be used for address pool operations".to_string(),
);
return false;
}
};
🤖 Prompt for AI Agents
In `@key-wallet-ffi/src/address_pool.rs` around lines 498 - 508, Before calling
to_account_type() on account_type_rust, guard against variants that require
extra params (PlatformPayment, DashpayReceivingFunds, DashpayExternalAccount) so
the conversion cannot panic and unwind across the FFI; check account_type_rust
first and if it matches any of those forbidden variants call
FFIError::set_error(error, FFIErrorCode::InvalidInput, "Platform Payment
accounts cannot be used for address pool operations".to_string()) and return
false, otherwise proceed to call account_type_rust.try_into() (producing
account_type_to_check) and continue as before. Ensure you reference the existing
symbols account_type_rust, to_account_type(), account_type_to_check,
FFIError::set_error, FFIErrorCode::InvalidInput and error when implementing the
guard.

Comment on lines +939 to +987
pub unsafe extern "C" fn wallet_add_platform_payment_account(
wallet: *mut FFIWallet,
account_index: c_uint,
key_class: c_uint,
) -> crate::types::FFIAccountResult {
use key_wallet::account::AccountType;

if wallet.is_null() {
return crate::types::FFIAccountResult::error(
FFIErrorCode::InvalidInput,
"Wallet is null".to_string(),
);
}

let wallet = &mut *wallet;

let account_type = AccountType::PlatformPayment {
account: account_index,
key_class,
};

match wallet.inner_mut() {
Some(w) => {
// Use the proper add_account method
match w.add_account(account_type, None) {
Ok(()) => {
// Get the account we just added
if let Some(account) = w.accounts.account_of_type(account_type) {
let ffi_account = crate::types::FFIAccount::new(account);
return crate::types::FFIAccountResult::success(Box::into_raw(Box::new(
ffi_account,
)));
}
crate::types::FFIAccountResult::error(
FFIErrorCode::WalletError,
"Failed to retrieve account after adding".to_string(),
)
}
Err(e) => crate::types::FFIAccountResult::error(
FFIErrorCode::WalletError,
format!("Failed to add platform payment account: {}", e),
),
}
}
None => crate::types::FFIAccountResult::error(
FFIErrorCode::InvalidState,
"Cannot modify wallet".to_string(),
),
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Search for PlatformPayment definition and usage
rg -n "PlatformPayment" -g '*.rs' --context=5

Repository: dashpay/rust-dashcore

Length of output: 50378


🏁 Script executed:

# Check AccountType definition and constraints
fd -e rs -path "*/account/*" | head -20

Repository: dashpay/rust-dashcore

Length of output: 233


🏁 Script executed:

# Look for validation of account_index and key_class parameters
rg -n "account_index|key_class" -g '*.rs' --context=3 | head -100

Repository: dashpay/rust-dashcore

Length of output: 8905


🏁 Script executed:

# Look for derivation_path implementation for PlatformPayment
sed -n '421,450p' key-wallet/src/account/account_type.rs

Repository: dashpay/rust-dashcore

Length of output: 1191


🏁 Script executed:

# Check if there's any validation during child key derivation
rg -A 10 "fn derive.*platform\|fn child_key" -i --context=3

Repository: dashpay/rust-dashcore

Length of output: 47


🏁 Script executed:

# Look for any bounds checking or validation error types
rg -n "invalid.*account\|invalid.*key_class\|bounds\|range" -i | grep -i "platform\|dip.17\|account.*key"

Repository: dashpay/rust-dashcore

Length of output: 47


🏁 Script executed:

# Find ChildNumber implementation to see hardened index bounds
rg -n "from_hardened_idx" --type rust -A 5

Repository: dashpay/rust-dashcore

Length of output: 38867


🏁 Script executed:

# Check the dashcore crate for ChildNumber constraints
fd -e rs | xargs rg -l "from_hardened_idx" | head -5

Repository: dashpay/rust-dashcore

Length of output: 264


🏁 Script executed:

# Look for any documentation or constants about valid index ranges
rg -n "0x[0-9a-f]+|2\^31|hardened.*bound|max.*index" --context=2 | head -50

Repository: dashpay/rust-dashcore

Length of output: 3351


🏁 Script executed:

# Check the add_account method to see if it validates these parameters
rg -A 15 "fn add_account" key-wallet/src/wallet/*.rs | head -80

Repository: dashpay/rust-dashcore

Length of output: 2494


🏁 Script executed:

# Look for any error handling or validation before account creation
rg -B 3 -A 10 "fn add_account.*account_type" key-wallet/src/wallet/mod.rs | head -60

Repository: dashpay/rust-dashcore

Length of output: 47


🏁 Script executed:

# Check the exact bounds validation in ChildNumber::from_hardened_idx
sed -n '622,630p' key-wallet/src/bip32.rs

Repository: dashpay/rust-dashcore

Length of output: 326


🏁 Script executed:

# Verify no other validation happens at the FFI boundary
sed -n '939,965p' key-wallet-ffi/src/wallet.rs

Repository: dashpay/rust-dashcore

Length of output: 846


Add input validation for platform account indices before account creation.

The FFI inputs account_index and key_class are untrusted and must be validated at the FFI boundary. ChildNumber::from_hardened_idx() rejects indices ≥ 2³¹, but validation occurs only during later derivation, allowing creation of invalid accounts. Add explicit range checks to reject values ≥ 2³¹ before calling add_account().

🤖 Prompt for AI Agents
In `@key-wallet-ffi/src/wallet.rs` around lines 939 - 987, Validate the untrusted
FFI inputs account_index and key_class in wallet_add_platform_payment_account
before creating AccountType::PlatformPayment: explicitly check that both values
are less than 2^31 (i.e. reject >= 2_147_483_648) and return
crate::types::FFIAccountResult::error with FFIErrorCode::InvalidInput and a
clear message if out of range; only after these guards succeed proceed to
construct AccountType::PlatformPayment and call w.add_account (so you never pass
invalid indices to derivation/ChildNumber::from_hardened_idx).

Comment on lines +363 to +392
#[cfg(feature = "bincode")]
impl<Context> bincode::Decode<Context> for ManagedPlatformAccount {
fn decode<D: bincode::de::Decoder<Context = Context>>(
decoder: &mut D,
) -> core::result::Result<Self, bincode::error::DecodeError> {
let account = bincode::Decode::decode(decoder)?;
let key_class = bincode::Decode::decode(decoder)?;
let network = bincode::Decode::decode(decoder)?;
let credit_balance = bincode::Decode::decode(decoder)?;

// Decode address_balances from vec of tuples
let address_balances_vec: Vec<(PlatformP2PKHAddress, u64)> =
bincode::Decode::decode(decoder)?;
let address_balances: BTreeMap<PlatformP2PKHAddress, u64> =
address_balances_vec.into_iter().collect();

let addresses = bincode::Decode::decode(decoder)?;
let metadata = bincode::Decode::decode(decoder)?;
let is_watch_only = bincode::Decode::decode(decoder)?;

Ok(Self {
account,
key_class,
network,
credit_balance,
address_balances,
addresses,
metadata,
is_watch_only,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's verify the file exists and examine the bincode Decode implementation
git ls-files key-wallet/src/managed_account/ | head -20

Repository: dashpay/rust-dashcore

Length of output: 527


🏁 Script executed:

# Check the specific file and lines mentioned
cd key-wallet && find . -name "managed_platform_account.rs" -type f

Repository: dashpay/rust-dashcore

Length of output: 114


🏁 Script executed:

# Let's also search for AddressPool definition to understand the structure
rg "struct AddressPool|enum AddressPool" key-wallet/src --type rust -A 5

Repository: dashpay/rust-dashcore

Length of output: 1587


🏁 Script executed:

# Search for the bincode Decode implementation for ManagedPlatformAccount
rg "impl.*bincode::Decode.*ManagedPlatformAccount" key-wallet/src --type rust -B 2 -A 35

Repository: dashpay/rust-dashcore

Length of output: 3715


🏁 Script executed:

# Get the ManagedPlatformAccount struct definition
rg "pub struct ManagedPlatformAccount" key-wallet/src --type rust -A 15

Repository: dashpay/rust-dashcore

Length of output: 1715


🏁 Script executed:

# Check what type the 'addresses' field is
rg "addresses:" key-wallet/src/managed_account/managed_platform_account.rs --type rust -B 2 -A 2

Repository: dashpay/rust-dashcore

Length of output: 524


🏁 Script executed:

# Look at the full struct to understand field types
head -150 key-wallet/src/managed_account/managed_platform_account.rs | tail -50

Repository: dashpay/rust-dashcore

Length of output: 2187


🏁 Script executed:

# Check the bincode Encode implementation to see how data is serialized
rg "impl.*bincode::Encode.*ManagedPlatformAccount" key-wallet/src --type rust -B 2 -A 35

Repository: dashpay/rust-dashcore

Length of output: 3929


🏁 Script executed:

# Check if there are any validation methods or invariants documented
rg "invariant|validate|check" key-wallet/src/managed_account/managed_platform_account.rs --type rust -B 2 -A 2

Repository: dashpay/rust-dashcore

Length of output: 47


🏁 Script executed:

# Look at how ManagedPlatformAccount is constructed and used elsewhere
rg "ManagedPlatformAccount::new" key-wallet/src --type rust -B 2 -A 5

Repository: dashpay/rust-dashcore

Length of output: 13478


🏁 Script executed:

# Check if there's any validation in the constructor
rg "pub fn new" key-wallet/src/managed_account/managed_platform_account.rs --type rust -A 20

Repository: dashpay/rust-dashcore

Length of output: 724


🏁 Script executed:

# Verify AddressPool contains network field and check its access
rg "pub network" key-wallet/src/managed_account/address_pool.rs --type rust -B 1 -A 1

Repository: dashpay/rust-dashcore

Length of output: 163


🏁 Script executed:

# Final check: Look for any tests covering bincode deserialization with mismatched data
rg "test.*bincode|bincode.*test" key-wallet/src/managed_account/managed_platform_account.rs --type rust -A 10

Repository: dashpay/rust-dashcore

Length of output: 47


🏁 Script executed:

# Check if there are any other deserialization validators or invariant checks
rg "validate|check_invariant" key-wallet/src/managed_account/managed_platform_account.rs --type rust -B 2 -A 5

Repository: dashpay/rust-dashcore

Length of output: 47


Validate network consistency and total balance on deserialization from untrusted bincode data.

ManagedPlatformAccount accepts deserialized network and credit_balance values without validation against the AddressPool and per-address balances. This violates the invariant established in the constructor (where network = addresses.network) and in normal balance operations (where credit_balance equals the sum of per-address balances). Untrusted input could cause mixed-network states or incorrect totals.

🔧 Suggested fix (normalize to consistent state)
-        let network = bincode::Decode::decode(decoder)?;
-        let credit_balance = bincode::Decode::decode(decoder)?;
+        let mut network = bincode::Decode::decode(decoder)?;
+        let mut credit_balance: u64 = bincode::Decode::decode(decoder)?;
@@
-        let addresses = bincode::Decode::decode(decoder)?;
+        let addresses: AddressPool = bincode::Decode::decode(decoder)?;
+        if addresses.network != network {
+            network = addresses.network;
+        }
@@
-        Ok(Self {
+        credit_balance = address_balances.values().sum();
+        Ok(Self {
             account,
             key_class,
             network,
🤖 Prompt for AI Agents
In `@key-wallet/src/managed_account/managed_platform_account.rs` around lines 363
- 392, The Decode impl for ManagedPlatformAccount currently trusts decoded
network and credit_balance; update the bincode::Decode::decode implementation to
enforce invariants by (1) verifying that every key in address_balances and
addresses.network are consistent (or at minimum set network = addresses.network)
and (2) recomputing credit_balance as the sum of values in address_balances and
using that computed total instead of the decoded credit_balance when
constructing Self; make these adjustments inside the Decode for
ManagedPlatformAccount (the function that decodes account, key_class, network,
credit_balance, address_balances, addresses, metadata, is_watch_only) so the
returned instance always has network == addresses.network and credit_balance ==
sum(address_balances).

Comment on lines +62 to +71
/// Convert to a dashcore::Address (P2PKH)
///
/// This creates a standard P2PKH address from the hash.
/// Note: This is for interoperability - Platform addresses typically
/// use bech32m encoding rather than base58 P2PKH format.
pub fn to_address(&self, network: Network) -> Address {
let pubkey_hash = Hash160::from_slice(&self.0).expect("20 bytes is valid for Hash160");
let payload = Payload::PubkeyHash(pubkey_hash.into());
Address::new(network, payload)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the file structure and understand the code
find . -name "platform_address.rs" -type f | head -5

Repository: dashpay/rust-dashcore

Length of output: 117


🏁 Script executed:

# Check if Hash160 is used elsewhere in the codebase
rg "Hash160" --type rs | head -20

Repository: dashpay/rust-dashcore

Length of output: 91


🏁 Script executed:

# Look for the PlatformP2PKHAddress definition to understand the structure
rg "struct PlatformP2PKHAddress\|type PlatformP2PKHAddress" --type rs -A 5

Repository: dashpay/rust-dashcore

Length of output: 91


🌐 Web query:

Hash160 constructor from_byte_array dashcore rust crate

💡 Result:

In dashcore, Hash160 is the HASH160 newtype dashcore::hashes::hash160::Hash, and the constructor you want is:

use dashcore::hashes::hash160;

let bytes: [u8; 20] = /* ... */;
let h160 = hash160::Hash::from_byte_array(bytes);

hash160::Hash also provides zero-copy conversions if you already have a fixed-size array reference:

use dashcore::hashes::hash160;

let bytes_ref: &[u8; 20] = /* ... */;
let h160_ref: &hash160::Hash = hash160::Hash::from_bytes_ref(bytes_ref);

[1] [2]


🏁 Script executed:

cat -n ./key-wallet/src/managed_account/platform_address.rs | head -75

Repository: dashpay/rust-dashcore

Length of output: 3110


🏁 Script executed:

# Get the struct definition
sed -n '1,100p' ./key-wallet/src/managed_account/platform_address.rs

Repository: dashpay/rust-dashcore

Length of output: 3428


Use Hash160::from_byte_array() to eliminate expect() in library code.

Hash160::from_slice(&self.0).expect(...) at line 68 violates the guideline to avoid expect() in library code. Since self.0 is already a fixed-size [u8; 20] array, use the infallible Hash160::from_byte_array() constructor instead.

Fix
-        let pubkey_hash = Hash160::from_slice(&self.0).expect("20 bytes is valid for Hash160");
+        let pubkey_hash = Hash160::from_byte_array(self.0);
🤖 Prompt for AI Agents
In `@key-wallet/src/managed_account/platform_address.rs` around lines 62 - 71, The
to_address method uses Hash160::from_slice(&self.0).expect(...) which calls
expect in library code; replace that with the infallible
Hash160::from_byte_array(self.0) (or equivalent method taking [u8;20]) to
construct pubkey_hash without panicking, then build
Payload::PubkeyHash(pubkey_hash.into()) and return Address::new(network,
payload); update references to self.0 accordingly and remove the expect call.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants