Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions key-wallet-ffi/FFI_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -852,7 +852,7 @@ Get the parent wallet ID of a managed account Note: ManagedAccount doesn't stor
#### `managed_wallet_check_transaction`

```c
managed_wallet_check_transaction(managed_wallet: *mut FFIManagedWalletInfo, wallet: *mut FFIWallet, tx_bytes: *const u8, tx_len: usize, context_type: FFITransactionContext, block_height: c_uint, block_hash: *const u8, // 32 bytes if not null timestamp: u64, update_state: bool, result_out: *mut FFITransactionCheckResult, error: *mut FFIError,) -> bool
managed_wallet_check_transaction(managed_wallet: *mut FFIManagedWalletInfo, wallet: *mut FFIWallet, tx_bytes: *const u8, tx_len: usize, context_type: FFITransactionContext, block_info: FFIBlockInfo, update_state: bool, result_out: *mut FFITransactionCheckResult, error: *mut FFIError,) -> bool
```

**Description:**
Expand Down Expand Up @@ -1300,7 +1300,7 @@ Build and sign a transaction using the wallet's managed info This is the recomm
#### `wallet_check_transaction`

```c
wallet_check_transaction(wallet: *mut FFIWallet, tx_bytes: *const u8, tx_len: usize, context_type: FFITransactionContext, block_height: u32, block_hash: *const u8, // 32 bytes if not null timestamp: u64, update_state: bool, result_out: *mut FFITransactionCheckResult, error: *mut FFIError,) -> bool
wallet_check_transaction(wallet: *mut FFIWallet, tx_bytes: *const u8, tx_len: usize, context_type: FFITransactionContext, block_info: FFIBlockInfo, update_state: bool, result_out: *mut FFITransactionCheckResult, error: *mut FFIError,) -> bool
```

**Description:**
Expand Down
24 changes: 5 additions & 19 deletions key-wallet-ffi/src/managed_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use dashcore::hashes::Hash;

use crate::address_pool::{FFIAddressPool, FFIAddressPoolType};
use crate::error::{FFIError, FFIErrorCode};
use crate::types::FFIAccountType;
use crate::types::{FFIAccountType, FFITransactionContextDetails};
use crate::wallet_manager::FFIWalletManager;
use crate::FFINetwork;
use key_wallet::account::account_collection::{DashpayAccountKey, PlatformPaymentAccountKey};
Expand Down Expand Up @@ -666,12 +666,8 @@ pub struct FFITransactionRecord {
pub txid: [u8; 32],
/// Net amount for this account (positive = received, negative = sent)
pub net_amount: i64,
/// Block height if confirmed, 0 if unconfirmed
pub height: u32,
/// Block hash if confirmed (32 bytes), all zeros if unconfirmed
pub block_hash: [u8; 32],
/// Unix timestamp
pub timestamp: u64,
/// Transaction context (mempool, instant-send, in-block, chain-locked + block info)
pub context: FFITransactionContextDetails,
/// Fee if known, 0 if unknown
pub fee: u64,
/// Whether this is our transaction
Expand Down Expand Up @@ -729,18 +725,8 @@ pub unsafe extern "C" fn managed_core_account_get_transactions(
// Copy net amount
ffi_record.net_amount = record.net_amount;

// Copy height (0 if unconfirmed)
ffi_record.height = record.height.unwrap_or(0);

// Copy block hash (zeros if unconfirmed)
if let Some(block_hash) = record.block_hash {
ffi_record.block_hash = block_hash.to_byte_array();
} else {
ffi_record.block_hash = [0u8; 32];
}

// Copy timestamp
ffi_record.timestamp = record.timestamp;
// Copy transaction context
ffi_record.context = FFITransactionContextDetails::from(record.context);

// Copy fee (0 if unknown)
ffi_record.fee = record.fee.unwrap_or(0);
Expand Down
28 changes: 13 additions & 15 deletions key-wallet-ffi/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoIn
use secp256k1::{Message, Secp256k1, SecretKey};

use crate::error::{FFIError, FFIErrorCode};
use crate::types::{block_info_from_ffi, FFINetwork, FFITransactionContext, FFIWallet};
use crate::types::{
transaction_context_from_ffi, FFIBlockInfo, FFINetwork, FFITransactionContext, FFIWallet,
};
use crate::FFIWalletManager;

// MARK: - Transaction Types
Expand Down Expand Up @@ -388,9 +390,7 @@ pub unsafe extern "C" fn wallet_check_transaction(
tx_bytes: *const u8,
tx_len: usize,
context_type: FFITransactionContext,
block_height: u32,
block_hash: *const u8, // 32 bytes if not null
timestamp: u64,
block_info: FFIBlockInfo,
update_state: bool,
result_out: *mut FFITransactionCheckResult,
error: *mut FFIError,
Expand Down Expand Up @@ -419,18 +419,16 @@ pub unsafe extern "C" fn wallet_check_transaction(
};

// Build the transaction context
use key_wallet::transaction_checking::TransactionContext;
let context = match context_type {
FFITransactionContext::Mempool => TransactionContext::Mempool,
FFITransactionContext::InBlock => {
let info = block_info_from_ffi(block_height, block_hash, timestamp);
TransactionContext::InBlock(info)
}
FFITransactionContext::InChainLockedBlock => {
let info = block_info_from_ffi(block_height, block_hash, timestamp);
TransactionContext::InChainLockedBlock(info)
let context = match transaction_context_from_ffi(context_type, &block_info) {
Some(ctx) => ctx,
None => {
FFIError::set_error(
error,
FFIErrorCode::InvalidInput,
"Block info must not be zeroed for confirmed contexts".to_string(),
);
return false;
}
FFITransactionContext::InstantSend => TransactionContext::InstantSend,
};

// Create a ManagedWalletInfo from the wallet
Expand Down
27 changes: 12 additions & 15 deletions key-wallet-ffi/src/transaction_checking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ use std::slice;

use crate::error::{FFIError, FFIErrorCode};
use crate::managed_wallet::{managed_wallet_info_free, FFIManagedWalletInfo};
use crate::types::{block_info_from_ffi, FFITransactionContext, FFIWallet};
use crate::types::{transaction_context_from_ffi, FFIBlockInfo, FFITransactionContext, FFIWallet};
use dashcore::consensus::Decodable;
use dashcore::Transaction;
use key_wallet::transaction_checking::{
account_checker::CoreAccountTypeMatch, TransactionContext, WalletTransactionChecker,
account_checker::CoreAccountTypeMatch, WalletTransactionChecker,
};
use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo;

Expand Down Expand Up @@ -112,9 +112,7 @@ pub unsafe extern "C" fn managed_wallet_check_transaction(
tx_bytes: *const u8,
tx_len: usize,
context_type: FFITransactionContext,
block_height: c_uint,
block_hash: *const u8, // 32 bytes if not null
timestamp: u64,
block_info: FFIBlockInfo,
update_state: bool,
result_out: *mut FFITransactionCheckResult,
error: *mut FFIError,
Expand All @@ -141,17 +139,16 @@ pub unsafe extern "C" fn managed_wallet_check_transaction(
};

// Build the transaction context
let context = match context_type {
FFITransactionContext::Mempool => TransactionContext::Mempool,
FFITransactionContext::InBlock => {
let info = block_info_from_ffi(block_height, block_hash, timestamp);
TransactionContext::InBlock(info)
}
FFITransactionContext::InChainLockedBlock => {
let info = block_info_from_ffi(block_height, block_hash, timestamp);
TransactionContext::InChainLockedBlock(info)
let context = match transaction_context_from_ffi(context_type, &block_info) {
Some(ctx) => ctx,
None => {
FFIError::set_error(
error,
FFIErrorCode::InvalidInput,
"Block info must not be zeroed for confirmed contexts".to_string(),
);
return false;
}
FFITransactionContext::InstantSend => TransactionContext::InstantSend,
};

// Check the transaction - wallet is now required
Expand Down
147 changes: 91 additions & 56 deletions key-wallet-ffi/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,72 @@
use dashcore::hashes::Hash;
use key_wallet::transaction_checking::{BlockInfo, TransactionContext};
use key_wallet::{Network, Wallet};
use std::os::raw::{c_char, c_uint};
use std::os::raw::c_char;
use std::sync::Arc;

/// Convert FFI block parameters to a `BlockInfo`.
///
/// # Safety
/// FFI-compatible block metadata (height, hash, timestamp).
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct FFIBlockInfo {
/// Block height
pub height: u32,
/// Block hash (32 bytes)
pub block_hash: [u8; 32],
/// Unix timestamp
pub timestamp: u32,
}

impl FFIBlockInfo {
/// All-zeros placeholder used for unconfirmed contexts.
pub fn empty() -> Self {
Self {
height: 0,
block_hash: [0u8; 32],
timestamp: 0,
}
}

/// Convert to native `BlockInfo`.
pub fn to_block_info(&self) -> BlockInfo {
let block_hash = dashcore::BlockHash::from_byte_array(self.block_hash);
BlockInfo::new(self.height, block_hash, self.timestamp)
}
Comment on lines +31 to +35
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

Make confirmed FFIBlockInfo conversion validate the whole invariant.

This only rejects the exact empty sentinel. A caller that leaves just one field zeroed still gets a confirmed TransactionContext, and FFIBlockInfo::to_block_info() remains an infallible escape hatch around the guard. Please make the confirmed conversion itself fallible and require both a non-zero hash and a non-zero timestamp before constructing block-based contexts.

🔧 Suggested direction
 impl FFIBlockInfo {
-    /// Convert to native `BlockInfo`.
-    pub fn to_block_info(&self) -> BlockInfo {
-        let block_hash = dashcore::BlockHash::from_byte_array(self.block_hash);
-        BlockInfo::new(self.height, block_hash, self.timestamp)
+    fn to_confirmed_block_info(&self) -> Option<BlockInfo> {
+        if self.block_hash == [0u8; 32] || self.timestamp == 0 {
+            return None;
+        }
+
+        let block_hash = dashcore::BlockHash::from_byte_array(self.block_hash);
+        Some(BlockInfo::new(self.height, block_hash, self.timestamp))
     }
 }
@@
         FFITransactionContext::Mempool => Some(TransactionContext::Mempool),
         FFITransactionContext::InstantSend => Some(TransactionContext::InstantSend),
-        FFITransactionContext::InBlock => {
-            if block_info.block_hash == [0u8; 32] && block_info.timestamp == 0 {
-                return None;
-            }
-            Some(TransactionContext::InBlock(block_info.to_block_info()))
-        }
-        FFITransactionContext::InChainLockedBlock => {
-            if block_info.block_hash == [0u8; 32] && block_info.timestamp == 0 {
-                return None;
-            }
-            Some(TransactionContext::InChainLockedBlock(block_info.to_block_info()))
-        }
+        FFITransactionContext::InBlock => block_info
+            .to_confirmed_block_info()
+            .map(TransactionContext::InBlock),
+        FFITransactionContext::InChainLockedBlock => block_info
+            .to_confirmed_block_info()
+            .map(TransactionContext::InChainLockedBlock),
     }
 }

As per coding guidelines, **/*-ffi/**/*.rs: Handle memory safety carefully at FFI boundaries.

Also applies to: 52-71

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@key-wallet-ffi/src/types.rs` around lines 31 - 35,
FFIBlockInfo::to_block_info is currently infallible and only rejects the exact
zero-hash sentinel; change it to be fallible (return Result<BlockInfo, Error> or
Option<BlockInfo>) and validate the full invariant: require the block_hash not
equal to the all-zero byte array and require timestamp != 0 before calling
BlockInfo::new. Update all call sites (where FFIBlockInfo is used to build
confirmed TransactionContext or any "block-based" context) to handle the failure
path and avoid constructing confirmed contexts when either hash or timestamp are
zero; apply the same change for the related conversions in the 52-71 region.

}

impl From<BlockInfo> for FFIBlockInfo {
fn from(info: BlockInfo) -> Self {
Self {
height: info.height(),
block_hash: info.block_hash().to_byte_array(),
timestamp: info.timestamp(),
}
}
}

/// Convert an `FFIBlockInfo` and context type to a native `TransactionContext`.
///
/// If `block_hash` is non-null it must point to 32 readable bytes.
pub(crate) unsafe fn block_info_from_ffi(
height: u32,
block_hash: *const u8,
timestamp: u64,
) -> BlockInfo {
let block_hash = if !block_hash.is_null() {
let hash_bytes = std::slice::from_raw_parts(block_hash, 32);
let mut arr = [0u8; 32];
arr.copy_from_slice(hash_bytes);
dashcore::BlockHash::from_byte_array(arr)
} else {
dashcore::BlockHash::all_zeros()
};
BlockInfo::new(height, block_hash, timestamp as u32)
/// Returns `None` when block info is all-zeros for confirmed contexts (`InBlock`,
/// `InChainLockedBlock`), indicating invalid input from the FFI caller.
pub(crate) fn transaction_context_from_ffi(
context_type: FFITransactionContext,
block_info: &FFIBlockInfo,
) -> Option<TransactionContext> {
match context_type {
FFITransactionContext::Mempool => Some(TransactionContext::Mempool),
FFITransactionContext::InstantSend => Some(TransactionContext::InstantSend),
FFITransactionContext::InBlock => {
if block_info.block_hash == [0u8; 32] && block_info.timestamp == 0 {
return None;
}
Some(TransactionContext::InBlock(block_info.to_block_info()))
}
FFITransactionContext::InChainLockedBlock => {
if block_info.block_hash == [0u8; 32] && block_info.timestamp == 0 {
return None;
}
Some(TransactionContext::InChainLockedBlock(block_info.to_block_info()))
}
}
Comment on lines +48 to +71
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

Add regression tests for the new confirmed-context rejection.

This fix now guards an FFI bug, but the local test module still doesn’t cover FFIBlockInfo::empty() for InBlock / InChainLockedBlock, or the accepted paths for Mempool / InstantSend. A small #[cfg(test)] table test here would make this much harder to regress.

As per coding guidelines, Write unit tests for new functionality in Rust code and Place unit tests alongside code with #[cfg(test)] attribute.

Also applies to: 839-841

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@key-wallet-ffi/src/types.rs` around lines 48 - 71, Add a #[cfg(test)]
unit-test module next to transaction_context_from_ffi that exercises all
FFITransactionContext variants using both a FFIBlockInfo::empty() and a
non-empty FFIBlockInfo: assert that
transaction_context_from_ffi(FFITransactionContext::InBlock,
&FFIBlockInfo::empty()) and the InChainLockedBlock variant return None, and
assert that Mempool and InstantSend return
Some(TransactionContext::Mempool/InstantSend) even when given
FFIBlockInfo::empty(), plus validate that InBlock/InChainLockedBlock return
Some(...) for a non-empty block_info; structure as a small table-driven test to
cover accepted and rejected paths.

}

/// FFI Network type (single network)
Expand Down Expand Up @@ -760,62 +804,53 @@ impl From<TransactionContext> for FFITransactionContext {
pub struct FFITransactionContextDetails {
/// The context type
pub context_type: FFITransactionContext,
/// Block height (0 for mempool)
pub height: c_uint,
/// Block hash (32 bytes, null for mempool or if unknown)
pub block_hash: *const u8,
/// Timestamp (0 if unknown)
pub timestamp: c_uint,
/// Block info (zeroed for mempool/instant-send contexts)
pub block_info: FFIBlockInfo,
}

impl FFITransactionContextDetails {
/// Create a mempool context
pub fn mempool() -> Self {
FFITransactionContextDetails {
Self {
context_type: FFITransactionContext::Mempool,
height: 0,
block_hash: std::ptr::null(),
timestamp: 0,
block_info: FFIBlockInfo::empty(),
}
}

/// Create an in-block context
pub fn in_block(height: c_uint, block_hash: *const u8, timestamp: c_uint) -> Self {
FFITransactionContextDetails {
pub fn in_block(block_info: FFIBlockInfo) -> Self {
Self {
context_type: FFITransactionContext::InBlock,
height,
block_hash,
timestamp,
block_info,
}
}

/// Create a chain-locked block context
pub fn in_chain_locked_block(height: c_uint, block_hash: *const u8, timestamp: c_uint) -> Self {
FFITransactionContextDetails {
pub fn in_chain_locked_block(block_info: FFIBlockInfo) -> Self {
Self {
context_type: FFITransactionContext::InChainLockedBlock,
height,
block_hash,
timestamp,
block_info,
}
}

/// Convert to the native TransactionContext
pub fn to_transaction_context(&self) -> TransactionContext {
match self.context_type {
FFITransactionContext::Mempool => TransactionContext::Mempool,
FFITransactionContext::InBlock => {
let info = unsafe {
block_info_from_ffi(self.height, self.block_hash, self.timestamp as u64)
};
TransactionContext::InBlock(info)
}
FFITransactionContext::InChainLockedBlock => {
let info = unsafe {
block_info_from_ffi(self.height, self.block_hash, self.timestamp as u64)
};
TransactionContext::InChainLockedBlock(info)
}
FFITransactionContext::InstantSend => TransactionContext::InstantSend,
/// Convert to the native `TransactionContext`.
///
/// Returns `None` when block info is all-zeros for confirmed contexts.
pub fn to_transaction_context(&self) -> Option<TransactionContext> {
transaction_context_from_ffi(self.context_type, &self.block_info)
}
}

impl From<TransactionContext> for FFITransactionContextDetails {
fn from(ctx: TransactionContext) -> Self {
let context_type = FFITransactionContext::from(ctx);
let block_info = ctx
.block_info()
.map(|info| FFIBlockInfo::from(*info))
.unwrap_or_else(FFIBlockInfo::empty);
Self {
context_type,
block_info,
}
}
}
12 changes: 11 additions & 1 deletion key-wallet-ffi/src/wallet_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -772,7 +772,17 @@ pub unsafe extern "C" fn wallet_manager_process_transaction(
};

// Convert FFI context to native TransactionContext
let context = unsafe { (*context).to_transaction_context() };
let context = match unsafe { (*context).to_transaction_context() } {
Some(ctx) => ctx,
None => {
FFIError::set_error(
error,
FFIErrorCode::InvalidInput,
"Block info must not be zeroed for confirmed contexts".to_string(),
);
return false;
}
};

// Get the manager
let manager_ref = unsafe { &mut *manager };
Expand Down
Loading
Loading