Skip to content
Merged
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
49 changes: 44 additions & 5 deletions packages/rs-platform-wallet-ffi/src/core_address_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,56 @@ pub enum AddressPoolTypeTagFFI {
AbsentHardened = 3,
}

/// Curve discriminant for [`CoreAddressEntryFFI::key_type_tag`],
/// selecting which `key_wallet::managed_account::address_pool::PublicKeyType`
/// variant [`CoreAddressEntryFFI::public_key`] holds. Meaningful only
/// when `public_key_len > 0`. Kept stable across releases — it lands in
/// SwiftData rows next to the key bytes so a BLS operator or Ed25519
/// platform-node key survives the persist/restore round-trip that the
/// ECDSA-only 33-byte slot used to drop.
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyTypeTagFFI {
/// 33-byte compressed secp256k1 public key.
ECDSA = 0,
/// 48-byte BLS masternode-operator public key.
BLS = 1,
/// 32-byte Ed25519 platform-node public key.
EdDSA = 2,
}

impl KeyTypeTagFFI {
/// Validate a foreign byte into a `KeyTypeTagFFI` before use — reading
/// an out-of-range value directly into the `repr(u8)` field would be UB.
pub fn try_from_u8(b: u8) -> Option<Self> {
Some(match b {
0 => Self::ECDSA,
1 => Self::BLS,
2 => Self::EdDSA,
_ => return None,
})
}
}

/// A single on-chain address entry.
///
/// `*const c_char` strings are Rust-owned and valid only for the
/// duration of the callback; Swift must copy them before returning.
#[repr(C)]
pub struct CoreAddressEntryFFI {
/// 33-byte compressed secp256k1 public key. Valid iff
/// `has_public_key == true`; zero-filled otherwise. Watch-only
/// reconstructed accounts currently always carry the pubkey.
pub public_key: [u8; 33],
pub has_public_key: bool,
/// Typed public key, left-aligned in a fixed 48-byte slot sized for
/// the widest supported key (BLS-48). Exactly `public_key_len`
/// leading bytes are meaningful; the remainder is zero-filled. The
/// whole slot is zero when `public_key_len == 0` (no key).
pub public_key: [u8; 48],
/// Count of valid leading bytes in `public_key`: `0` = no key,
/// `33` = ECDSA, `48` = BLS, `32` = EdDSA. The `(public_key_len,
/// key_type_tag)` pair must agree with those widths; a mismatch is
/// treated as "no key" on decode.
pub public_key_len: u8,
/// [`KeyTypeTagFFI`] raw value. Meaningful only when
/// `public_key_len > 0`; ignored (and conventionally `0`) otherwise.
pub key_type_tag: u8,
/// `AddressPoolTypeTagFFI` raw value.
pub pool_type_tag: u8,
/// Derivation index within this pool.
Expand Down
467 changes: 214 additions & 253 deletions packages/rs-platform-wallet-ffi/src/persistence.rs

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions packages/rs-platform-wallet-ffi/src/provider_key_at_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,35 @@ pub unsafe extern "C" fn platform_wallet_provider_key_at_index_free(
out.private_key_hex = std::ptr::null_mut();
}
}

/// Compute the 20-byte Tenderdash platform node id
/// (`SHA256(ed25519 pubkey)[..20]`, rust-dashcore #884) for a raw 32-byte
/// Ed25519 public key. Pure helper — no wallet handle, no key material
/// beyond the public key.
///
/// Wraps `dashcore::PlatformNodeId::from_ed25519_public_key` so the host
/// can render the node id of a persisted platform-node public key (which
/// carries only the pubkey) without re-implementing the SHA-256 digest.
///
/// Returns `true` on success, having written 20 bytes into
/// `out_node_id_20`; `false` (and no write) when `pubkey_ptr` is null,
/// `pubkey_len != 32`, or `out_node_id_20` is null.
///
/// # Safety
/// `pubkey_ptr` must point to `pubkey_len` readable bytes; `out_node_id_20`,
/// when non-null, must point to at least 20 writable bytes.
#[no_mangle]
pub unsafe extern "C" fn platform_wallet_platform_node_id_from_ed25519_pubkey(
pubkey_ptr: *const u8,
pubkey_len: usize,
out_node_id_20: *mut u8,
) -> bool {
if pubkey_ptr.is_null() || out_node_id_20.is_null() || pubkey_len != 32 {
return false;
}
let mut pk32 = [0u8; 32];
pk32.copy_from_slice(unsafe { std::slice::from_raw_parts(pubkey_ptr, 32) });
let node_id = dashcore::PlatformNodeId::from_ed25519_public_key(&pk32).to_byte_array();
unsafe { std::ptr::copy_nonoverlapping(node_id.as_ptr(), out_node_id_20, 20) };
true
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,22 +67,13 @@ unsafe impl Sync for AccountAddressPoolFFI {}
// Expected layout on 64-bit targets (all fields in declaration
// order under `#[repr(C)]`):
//
// 0..=111 account AccountSpecFFI (108 bytes
// + 4 bytes inline pad to align
// the trailing 8-byte pointers
// — includes the appended
// derived-platform-node-keys
// ptr/len pair; see the layout
// note on AccountSpecFFI)
// 0..=95 account AccountSpecFFI (96 bytes)
// ...
//
// The exact internal padding inside `AccountSpecFFI` is fixed by the
// upstream layout guard in `wallet_restore_types`; we only pin the
// outer struct size here. On 64-bit targets the trailing pool fields
// add `1 + 7 (pad) + 8 (ptr) + 8 (len) = 24` bytes after a 112-byte
// account (96 + the derived-platform-node-keys ptr/len pair), for a
// total of 136.
// We only pin the outer struct size here. On 64-bit targets the
// trailing pool fields add `1 + 7 (pad) + 8 (ptr) + 8 (len) = 24`
// bytes after the 96-byte account spec, for a total of 120.
//
// Recompute via `std::mem::size_of` if the spec layout changes.
const _: [u8; 136] = [0u8; std::mem::size_of::<AccountAddressPoolFFI>()];
const _: [u8; 120] = [0u8; std::mem::size_of::<AccountAddressPoolFFI>()];
const _: [u8; 8] = [0u8; std::mem::align_of::<AccountAddressPoolFFI>()];
39 changes: 0 additions & 39 deletions packages/rs-platform-wallet-ffi/src/wallet_restore_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,30 +117,6 @@ impl StandardAccountTypeTagFFI {
}
}

/// One pre-derived platform-node (Ed25519) key carried on
/// [`AccountSpecFFI::derived_platform_node_keys`] for the
/// `ProviderPlatformKeys` account (`type_tag == 11`).
///
/// Ed25519/SLIP-10 is hardened-only, so the wallet can never extend
/// its platform-node pool without the seed — the batch is pre-derived
/// at registration (while the seed is in hand) and surfaced here so
/// the host can persist + display it with no keychain prompt. Plain
/// POD (no pointers): the Tenderdash node id (SHA256[..20], #884) is precomputed on the Rust
/// side so the host needs no RIPEMD-160 of its own. The private scalar
/// is never carried — a per-index reveal still routes through
/// `platform_wallet_provider_key_at_index` with the resolver.
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct ProviderPlatformNodeKeyFFI {
/// Hardened key index within the platform-node pool (`#0..`).
pub index: u32,
/// Raw 32-byte Ed25519 public key at this index.
pub public_key: [u8; 32],
/// 20-byte platform node id — `SHA256(ed25519 pubkey)[..20]` (#884)
/// (the ProRegTx `platform_node_id`).
pub node_id: [u8; 20],
}

/// Flat account spec carried in `WalletRestoreEntryFFI.accounts`.
///
/// Field relevance per `type_tag`:
Expand Down Expand Up @@ -188,21 +164,6 @@ pub struct AccountSpecFFI {
/// callback duration only; Swift owns the allocation.
pub account_xpub_bytes: *const u8,
pub account_xpub_bytes_len: usize,
/// Pre-derived platform-node (Ed25519) public keys — only populated
/// on the **write** callback for the `ProviderPlatformKeys` account
/// (`type_tag == 11`); `null` / `0` for every other account type.
///
/// On the write callback (`on_persist_account_registrations_fn`)
/// this is Rust-owned and valid for the callback window only — the
/// host copies the rows into its account row so the Node Keys
/// screen can list them from persistence without re-deriving. On
/// the **load** callback the host leaves this `null` / `0`: the
/// Rust load path does not consume it (it is display data the host
/// is the sole source of truth for), and the persisted account row
/// is never rewritten after registration, so the batch survives the
/// SwiftData → restore → re-persist cycle untouched.
pub derived_platform_node_keys: *const ProviderPlatformNodeKeyFFI,
pub derived_platform_node_keys_count: usize,
}

/// Per-identity public-key row carried on
Expand Down
14 changes: 5 additions & 9 deletions packages/rs-platform-wallet/src/changeset/changeset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,11 @@ pub enum ProviderKeyExtendedPubKey {
/// list these keys later from an external-signable / watch-only
/// wallet without re-prompting for the mnemonic. Only the public parts
/// are carried — the private scalar stays resolver-gated per index.
///
/// Produced by [`derive_platform_node_public_keys`](crate::wallet::provider_key_at_index::derive_platform_node_public_keys)
/// and fed straight into the managed platform-node pool at registration
/// via [`populate_platform_node_pool`](crate::wallet::provider_key_at_index::populate_platform_node_pool),
/// from which the keys persist as ordinary typed core-address rows.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ProviderPlatformNodePubKey {
Expand Down Expand Up @@ -1133,15 +1138,6 @@ pub struct ProviderKeyAccountEntry {
pub account_type: AccountType,
/// The account's extended public key.
pub extended_public_key: ProviderKeyExtendedPubKey,
/// Pre-derived platform-node (Ed25519) public keys, captured at
/// registration while the seed was in hand. Only populated for the
/// `ProviderPlatformKeys` (EdDSA) entry — always empty for the BLS
/// operator entry, whose pool the wallet can re-derive on demand
/// from the account xpub (non-hardened `ckd_pub`, no seed). The FFI
/// layer surfaces these to the host as a flat display array so the
/// Node Keys screen can list them from persistence with no keychain
/// prompt. See [`ProviderPlatformNodePubKey`].
pub derived_platform_node_keys: Vec<ProviderPlatformNodePubKey>,
}

/// Address-pool snapshot for one `(account_type, pool_type)` pair.
Expand Down
30 changes: 23 additions & 7 deletions packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,9 @@ impl<P: PlatformWalletPersistence + 'static> PlatformWalletManager<P> {
}
};

let wallet_info = ManagedWalletInfo::from_wallet(&wallet, birth_height);
// `mut` so the platform-node (Ed25519) pool can be populated in
// place below, BEFORE the address-pool snapshot is taken.
let mut wallet_info = ManagedWalletInfo::from_wallet(&wallet, birth_height);

let balance = Arc::new(WalletBalance::new());

Expand Down Expand Up @@ -213,10 +215,6 @@ impl<P: PlatformWalletPersistence + 'static> PlatformWalletManager<P> {
provider_key_account_registrations.push(ProviderKeyAccountEntry {
account_type: key_wallet::account::AccountType::ProviderOperatorKeys,
extended_public_key: ProviderKeyExtendedPubKey::Bls(bls.bls_public_key.clone()),
// The BLS operator pool extends on demand from the
// account xpub (non-hardened `ckd_pub`, no seed), so it
// needs no pre-derived batch.
derived_platform_node_keys: Vec::new(),
});
}
#[cfg(feature = "eddsa")]
Expand All @@ -228,7 +226,10 @@ impl<P: PlatformWalletPersistence + 'static> PlatformWalletManager<P> {
// the wallet is still seed-bearing (`downgrade_to_external_signable`
// hasn't run yet). Ed25519/SLIP-10 is hardened-only, so this
// pool can never be extended later from the watch-only restore —
// capturing the public parts now lets the Node Keys screen list
// populating the managed pool now lets those keys ride the
// normal typed-address persistence pipeline (persisted as
// `PublicKeyType::EdDSA` core-address rows, rehydrated on load
// by `restore_core_address_pools`) so the Node Keys screen lists
// them from persistence with no keychain prompt. A derivation
// failure here is non-fatal: fall back to an empty batch (the UI
// then uses its resolver-based "Load Keys" path) rather than
Expand All @@ -247,12 +248,27 @@ impl<P: PlatformWalletPersistence + 'static> PlatformWalletManager<P> {
);
Vec::new()
});
// Populate the managed platform-node pool in place, BEFORE the
// address-pool snapshot below reads `all_managed_accounts()`, so
// the EdDSA keys are captured as typed core-address rows. A
// population failure is non-fatal for the same reason the
// derivation failure above is.
if let Err(e) = crate::wallet::provider_key_at_index::populate_platform_node_pool(
&mut wallet_info,
&derived_platform_node_keys,
wallet.network,
) {
tracing::warn!(
error = %e,
"failed to populate the managed platform-node pool at registration; \
the Node Keys screen will fall back to the resolver path"
);
}
provider_key_account_registrations.push(ProviderKeyAccountEntry {
account_type: key_wallet::account::AccountType::ProviderPlatformKeys,
extended_public_key: ProviderKeyExtendedPubKey::EdDSA(
eddsa.ed25519_public_key.clone(),
),
derived_platform_node_keys,
});
}
// Snapshot core (BIP44/CoinJoin/identity/provider/DashPay)
Expand Down
Loading
Loading