Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
138 changes: 129 additions & 9 deletions packages/rs-platform-wallet-ffi/src/core_wallet_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -916,7 +916,14 @@ fn transaction_type_to_u8(
/// [`MasternodeRecord`], built by [`masternode_entry_ffi`] and
/// returned by `platform_wallet_manager_list_masternodes`. Inline
/// fixed-size hashes with `has_*` gates (mirroring `TransactionRecordFFI`)
/// keep heap ownership to the three C strings.
/// keep heap ownership to the C strings.
///
/// # ABI stability
///
/// This is the original, frozen layout returned by the unversioned
/// `platform_wallet_manager_list_masternodes` entry point. Do not add, remove,
/// or reorder fields. New projections belong in a versioned wrapper such as
/// [`MasternodeEntryV2FFI`].
#[repr(C)]
pub struct MasternodeEntryFFI {
/// proTxHash (32 wire bytes) — group key; also the registration txid.
Expand Down Expand Up @@ -994,12 +1001,6 @@ pub struct MasternodeEntryFFI {
pub platform_in_wallet: bool,
pub platform_account_type: u8,
pub platform_key_index: u32,
/// Where this record came from: 0 = one of the wallet's own masternodes
/// (aggregated from its provider transactions), 1 = tracked by the user
/// independently of every wallet.
pub source: u8,
/// User label of a tracked masternode, or null.
pub label: *mut c_char,
/// Whether the platform-node ownership check was actually *possible* for
/// this query: `true` when the wallet's derived platform-node index had
/// entries to compare against, `false` when it was empty/unavailable (no
Expand All @@ -1011,6 +1012,20 @@ pub struct MasternodeEntryFFI {
pub platform_ownership_checked: bool,
}

/// Version 2 masternode projection. The frozen V1 entry remains the first
/// field, preserving one canonical definition for all established fields;
/// V2 adds record provenance and the optional tracked-node label.
#[repr(C)]
pub struct MasternodeEntryV2FFI {
pub v1: MasternodeEntryFFI,
/// Where this record came from: 0 = one of the wallet's own masternodes
/// (aggregated from its provider transactions), 1 = tracked by the user
/// independently of every wallet.
pub source: u8,
/// User label of a tracked masternode, or null.
pub label: *mut c_char,
}

/// Encode a hash160 as a network-specific base58 P2PKH address string
/// (heap C string), or null on the (impossible-for-a-valid-hash) CString
/// interior-nul error.
Expand Down Expand Up @@ -1138,14 +1153,26 @@ pub(crate) fn masternode_entry_ffi(
platform_in_wallet,
platform_account_type,
platform_key_index,
platform_ownership_checked: mn.platform_ownership_checked,
}
}

/// Flatten one record into the additive V2 C-ABI entry.
pub(crate) fn masternode_entry_v2_ffi(
mn: &MasternodeRecord,
network: dashcore::Network,
) -> MasternodeEntryV2FFI {
use std::ffi::CString;

MasternodeEntryV2FFI {
v1: masternode_entry_ffi(mn, network),
source: mn.source.as_u8(),
label: mn
.label
.clone()
.and_then(|l| CString::new(l).ok())
.and_then(|label| CString::new(label).ok())
.map(CString::into_raw)
.unwrap_or(std::ptr::null_mut()),
platform_ownership_checked: mn.platform_ownership_checked,
}
}

Expand Down Expand Up @@ -1491,4 +1518,97 @@ mod tests {
let entries = Box::into_raw(vec![entry].into_boxed_slice()) as *mut MasternodeEntryFFI;
unsafe { crate::wallet::platform_wallet_manager_free_masternodes(entries, 1) };
}

/// Pin the original array element layout used by already-built C/Swift
/// consumers. A field addition or reorder here is an ABI break even when
/// all Rust callers are recompiled together.
#[test]
#[cfg(target_pointer_width = "64")]
fn masternode_entry_v1_layout_is_frozen() {
assert_eq!(std::mem::size_of::<MasternodeEntryFFI>(), 296);
assert_eq!(std::mem::align_of::<MasternodeEntryFFI>(), 8);
assert_eq!(
std::mem::offset_of!(MasternodeEntryFFI, service_address),
144
);
assert_eq!(std::mem::offset_of!(MasternodeEntryFFI, owner_address), 160);
assert_eq!(
std::mem::offset_of!(MasternodeEntryFFI, operator_public_key),
176
);
assert_eq!(
std::mem::offset_of!(MasternodeEntryFFI, payout_address),
248
);
assert_eq!(
std::mem::offset_of!(MasternodeEntryFFI, platform_key_index),
284
);
assert_eq!(
std::mem::offset_of!(MasternodeEntryFFI, platform_ownership_checked),
288
);
assert_eq!(std::mem::offset_of!(MasternodeEntryV2FFI, v1), 0);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Suggestion: The new V2 C ABI layout is not pinned

The layout regression test freezes V1 but only verifies that V2 begins with its nested V1 member. The stride test constructs and reads the array through the same current Rust type, so it remains green if source or label is reordered, padding changes, or another field is inserted. Because MasternodeEntryV2FFI is now a public versioned C array element, pin its 64-bit size, alignment, and additive-field offsets so incompatible changes require a V3 API.

Suggested change
assert_eq!(std::mem::offset_of!(MasternodeEntryV2FFI, v1), 0);
assert_eq!(std::mem::size_of::<MasternodeEntryV2FFI>(), 312);
assert_eq!(std::mem::align_of::<MasternodeEntryV2FFI>(), 8);
assert_eq!(std::mem::offset_of!(MasternodeEntryV2FFI, v1), 0);
assert_eq!(std::mem::offset_of!(MasternodeEntryV2FFI, source), 296);
assert_eq!(std::mem::offset_of!(MasternodeEntryV2FFI, label), 304);

source: ['codex']

}

#[test]
fn masternode_entry_v2_carries_additive_fields_and_frees_them() {
let mut mn = MasternodeRecord::default();
mn.source = platform_wallet::masternode::MasternodeSource::Tracked;
mn.label = Some("tracked label".to_string());
let entry = masternode_entry_v2_ffi(&mn, dashcore::Network::Testnet);
assert_eq!(entry.source, 1);
assert_eq!(
unsafe { std::ffi::CStr::from_ptr(entry.label) }
.to_str()
.unwrap(),
"tracked label"
);
let entries = Box::into_raw(vec![entry].into_boxed_slice()) as *mut MasternodeEntryV2FFI;
unsafe { crate::wallet::platform_wallet_manager_free_masternodes_v2(entries, 1) };
}

#[test]
fn masternode_v1_and_v2_arrays_preserve_second_element_stride() {
let mut first = MasternodeRecord::default();
first.pro_tx_hash = [1; 32];
first.service_address = Some("1.1.1.1:9999".to_string());
first.source = platform_wallet::masternode::MasternodeSource::Tracked;
first.label = Some("first".to_string());
let mut second = MasternodeRecord::default();
second.pro_tx_hash = [2; 32];
second.service_address = Some("2.2.2.2:9999".to_string());
second.source = platform_wallet::masternode::MasternodeSource::Tracked;
second.label = Some("second".to_string());

let v1 = vec![
masternode_entry_ffi(&first, dashcore::Network::Testnet),
masternode_entry_ffi(&second, dashcore::Network::Testnet),
];
let v1 = Box::into_raw(v1.into_boxed_slice()) as *mut MasternodeEntryFFI;
let v1_slice = unsafe { std::slice::from_raw_parts(v1, 2) };
assert_eq!(v1_slice[1].pro_tx_hash, [2; 32]);
assert_eq!(
unsafe { std::ffi::CStr::from_ptr(v1_slice[1].service_address) }
.to_str()
.unwrap(),
"2.2.2.2:9999"
);
unsafe { crate::wallet::platform_wallet_manager_free_masternodes(v1, 2) };

let v2 = vec![
masternode_entry_v2_ffi(&first, dashcore::Network::Testnet),
masternode_entry_v2_ffi(&second, dashcore::Network::Testnet),
];
let v2 = Box::into_raw(v2.into_boxed_slice()) as *mut MasternodeEntryV2FFI;
let v2_slice = unsafe { std::slice::from_raw_parts(v2, 2) };
assert_eq!(v2_slice[1].v1.pro_tx_hash, [2; 32]);
assert_eq!(
unsafe { std::ffi::CStr::from_ptr(v2_slice[1].label) }
.to_str()
.unwrap(),
"second"
);
unsafe { crate::wallet::platform_wallet_manager_free_masternodes_v2(v2, 2) };
}
}
58 changes: 42 additions & 16 deletions packages/rs-platform-wallet-ffi/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ use crate::event_handler::{
};
use crate::handle::*;
use crate::persistence::{
FFIPersister, PersistenceCallbacks, PersistenceCallbacksExtension, PersistenceCapabilitiesFFI,
PersistenceExtensionCallbacks, PLATFORM_WALLET_PERSISTENCE_CALLBACKS_EXTENSION_VERSION,
FFIPersister, FreeTrackedMasternodesFn, LoadTrackedMasternodesFn, PersistDpnsNameStatesFn,
PersistTrackedMasternodesFn, PersistenceCallbacks, PersistenceCallbacksExtension,
PersistenceCapabilitiesFFI, PersistenceExtensionCallbacks,
PLATFORM_WALLET_PERSISTENCE_CALLBACKS_EXTENSION_VERSION,
};
use crate::runtime::runtime;
use crate::types::{FFINetwork, Network};
Expand Down Expand Up @@ -187,9 +189,9 @@ unsafe fn persistence_extension_callbacks(
/// Read one size-gated `Option<fn>` field: present only when the
/// caller's `struct_size` proves the complete field exists.
macro_rules! gated {
($field:ident) => {{
($field:ident, $callback:ty) => {{
let end = std::mem::offset_of!(PersistenceCallbacksExtension, $field)
+ std::mem::size_of_val(&(*extension).$field);
+ std::mem::size_of::<Option<$callback>>();
if supplied_size < end {
None
} else {
Expand All @@ -199,10 +201,16 @@ unsafe fn persistence_extension_callbacks(
}

PersistenceExtensionCallbacks {
dpns_name_states: gated!(on_persist_dpns_name_states_fn),
persist_tracked_masternodes: gated!(on_persist_tracked_masternodes_fn),
load_tracked_masternodes: gated!(on_load_tracked_masternodes_fn),
load_tracked_masternodes_free: gated!(on_load_tracked_masternodes_free_fn),
dpns_name_states: gated!(on_persist_dpns_name_states_fn, PersistDpnsNameStatesFn),
persist_tracked_masternodes: gated!(
on_persist_tracked_masternodes_fn,
PersistTrackedMasternodesFn
),
load_tracked_masternodes: gated!(on_load_tracked_masternodes_fn, LoadTrackedMasternodesFn),
load_tracked_masternodes_free: gated!(
on_load_tracked_masternodes_free_fn,
FreeTrackedMasternodesFn
),
}
}

Expand Down Expand Up @@ -788,6 +796,16 @@ mod tests {
0
}

/// Exact allocation shape used by a host compiled before the tracked
/// callback trio was appended to `PersistenceCallbacksExtension`.
#[repr(C)]
struct DpnsOnlyPersistenceCallbacksExtension {
struct_size: usize,
version: u32,
reserved: u32,
on_persist_dpns_name_states_fn: Option<PersistDpnsNameStatesFn>,
}

fn persistence_callbacks() -> PersistenceCallbacks {
PersistenceCallbacks {
on_changeset_begin_fn: Some(begin_changeset),
Expand Down Expand Up @@ -1120,16 +1138,24 @@ mod tests {
/// yields the dpns callback and nothing else — additive size gating.
#[test]
fn dpns_only_sized_extension_reads_only_the_dpns_field() {
let dpns_only_size = std::mem::offset_of!(
PersistenceCallbacksExtension,
on_persist_tracked_masternodes_fn
);
let ext = PersistenceCallbacksExtension {
struct_size: dpns_only_size,
let ext = DpnsOnlyPersistenceCallbacksExtension {
struct_size: std::mem::size_of::<DpnsOnlyPersistenceCallbacksExtension>(),
version: PLATFORM_WALLET_PERSISTENCE_CALLBACKS_EXTENSION_VERSION,
reserved: 0,
on_persist_dpns_name_states_fn: Some(persist_dpns_name_states),
..Default::default()
};
let read = unsafe { persistence_extension_callbacks(&ext) };
assert_eq!(
ext.struct_size,
std::mem::offset_of!(
PersistenceCallbacksExtension,
on_persist_tracked_masternodes_fn
)
);
let read = unsafe {
persistence_extension_callbacks(
(&ext as *const DpnsOnlyPersistenceCallbacksExtension).cast(),
)
};
assert!(read.dpns_name_states.is_some());
assert!(read.persist_tracked_masternodes.is_none());
assert!(read.load_tracked_masternodes.is_none());
Expand Down
26 changes: 13 additions & 13 deletions packages/rs-platform-wallet-ffi/src/tracked_masternode.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! FFI for tracked (wallet-independent) masternodes: track / untrack /
//! rename, list, refresh, capabilities, and withdraw with a host-supplied
//! key. Thin marshalling over `platform_wallet::masternode::tracked`; the
//! records reuse [`MasternodeEntryFFI`] (`source == 1`) so hosts render
//! records reuse [`MasternodeEntryV2FFI`] (`source == 1`) so hosts render
//! wallet and tracked masternodes with the same code.

use std::ffi::{c_char, CStr};
Expand All @@ -11,7 +11,7 @@ use platform_wallet::masternode::{
capabilities_for_roles, LocatorSecret, MasternodeKeyRole, MasternodeRecord,
};

use crate::core_wallet_types::{masternode_entry_ffi, MasternodeEntryFFI};
use crate::core_wallet_types::{masternode_entry_v2_ffi, MasternodeEntryV2FFI};
use crate::error::*;
use crate::handle::*;
use crate::runtime::block_on_worker;
Expand All @@ -37,12 +37,12 @@ unsafe fn optional_string(ptr: *const c_char) -> Result<Option<String>, Platform
unsafe fn write_records(
records: Vec<MasternodeRecord>,
network: dashcore::Network,
out_entries: *mut *const MasternodeEntryFFI,
out_entries: *mut *const MasternodeEntryV2FFI,
out_count: *mut usize,
) {
let entries: Vec<MasternodeEntryFFI> = records
let entries: Vec<MasternodeEntryV2FFI> = records
.iter()
.map(|record| masternode_entry_ffi(record, network))
.map(|record| masternode_entry_v2_ffi(record, network))
.collect();
let count = entries.len();
if count == 0 {
Expand All @@ -59,7 +59,7 @@ unsafe fn write_records(
/// the current masternode list when available — local, no network; call
/// [`platform_wallet_manager_refresh_tracked_masternode`] afterwards for the
/// Platform / registration details. Returns the new record as a one-entry
/// array (free with `platform_wallet_manager_free_masternodes`).
/// array (free with `platform_wallet_manager_free_masternodes_v2`).
///
/// Whether the row survives a restart depends on the configured persister —
/// see `PLATFORM_WALLET_PERSISTENCE_CAPABILITY_TRACKED_MASTERNODES`.
Expand All @@ -74,7 +74,7 @@ pub unsafe extern "C" fn platform_wallet_manager_track_masternode(
manager_handle: Handle,
pro_tx_hash: *const u8,
label: *const c_char,
out_entry: *mut *const MasternodeEntryFFI,
out_entry: *mut *const MasternodeEntryV2FFI,
out_count: *mut usize,
) -> PlatformWalletFFIResult {
check_ptr!(pro_tx_hash);
Expand Down Expand Up @@ -153,18 +153,18 @@ pub unsafe extern "C" fn platform_wallet_manager_set_tracked_masternode_label(
PlatformWalletFFIResult::ok()
}

/// Every tracked masternode as a [`MasternodeEntryFFI`] (`source == 1`,
/// Every tracked masternode as a [`MasternodeEntryV2FFI`] (`source == 1`,
/// `label` set when named), with its status resolved against the CURRENT
/// masternode list (Active / Inactive / Retired, `Unknown` while the list
/// is unavailable). Sorted by when they were tracked. Free with
/// [`crate::wallet::platform_wallet_manager_free_masternodes`].
/// [`crate::wallet::platform_wallet_manager_free_masternodes_v2`].
///
/// # Safety
/// `out_entries` / `out_count` must be writable.
#[no_mangle]
pub unsafe extern "C" fn platform_wallet_manager_list_tracked_masternodes(
manager_handle: Handle,
out_entries: *mut *const MasternodeEntryFFI,
out_entries: *mut *const MasternodeEntryV2FFI,
out_count: *mut usize,
) -> PlatformWalletFFIResult {
check_ptr!(out_entries);
Expand All @@ -188,7 +188,7 @@ pub unsafe extern "C" fn platform_wallet_manager_list_tracked_masternodes(
/// keys). Blocks on the network round-trips. Partial results are kept and
/// persisted even when a step fails (the error is still returned). On
/// success returns the refreshed record as a one-entry array (free with
/// `platform_wallet_manager_free_masternodes`).
/// `platform_wallet_manager_free_masternodes_v2`).
///
/// # Safety
/// `pro_tx_hash` must point at 32 readable bytes; `out_entry` / `out_count`
Expand All @@ -197,7 +197,7 @@ pub unsafe extern "C" fn platform_wallet_manager_list_tracked_masternodes(
pub unsafe extern "C" fn platform_wallet_manager_refresh_tracked_masternode(
manager_handle: Handle,
pro_tx_hash: *const u8,
out_entry: *mut *const MasternodeEntryFFI,
out_entry: *mut *const MasternodeEntryV2FFI,
out_count: *mut usize,
) -> PlatformWalletFFIResult {
check_ptr!(pro_tx_hash);
Expand Down Expand Up @@ -342,7 +342,7 @@ mod tests {
#[test]
fn unknown_handles_are_invalid_handles() {
let hash = [0u8; 32];
let mut entries: *const MasternodeEntryFFI = std::ptr::null();
let mut entries: *const MasternodeEntryV2FFI = std::ptr::null();
let mut count = 5usize;
let mut r = unsafe {
platform_wallet_manager_track_masternode(
Expand Down
Loading
Loading