Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
c0303c0
fix: create identities with an authentication key at every security l…
lklimek Oct 28, 2026
0f16b5a
fix(platform-wallet): enforce the identity-create funding floor and t…
lklimek Oct 28, 2026
d21a122
fix: read identity funding minimums from Rust in the Swift and Kotlin…
lklimek Oct 28, 2026
c616cef
refactor(platform-wallet-ffi): move the host-facing key-layout APIs t…
lklimek Oct 28, 2026
7cde833
Merge branch 'v5.1-dev' into fix/platform-wallet-dip11-identity-key-l…
lklimek Oct 28, 2026
6fdb39b
fix(platform-wallet): create identities without an AUTHENTICATION/MED…
lklimek Oct 29, 2026
d08719f
Merge remote-tracking branch 'origin/v5.1-dev' into fix/platform-wall…
lklimek Oct 30, 2026
b1ce749
fix(platform-wallet): refuse identity keys Platform's structure valid…
lklimek Oct 30, 2026
96a84b0
Merge commit 'e269d6ff8eb38245846dbd88f7c4ad803fd4f457' into fix/plat…
lklimek Oct 30, 2026
d7c05a7
fix(platform-wallet): validate identity creation keys at the wallet's…
lklimek Oct 30, 2026
48f860f
docs(platform-wallet): say identity creation keys validate at the SDK…
lklimek Oct 30, 2026
5f17b3b
Merge remote-tracking branch 'origin/v5.1-dev' into fix/platform-wall…
lklimek Oct 30, 2026
2d17c6d
chore(platform-wallet): merge v5.1-dev into identity creation validation
lklimek Nov 3, 2026
8366cf7
fix(platform-wallet): revalidate creation keys at operation version
lklimek Nov 3, 2026
4ec64ef
Merge branch 'v5.1-dev' into fix/platform-wallet-dip11-identity-key-l…
lklimek Nov 3, 2026
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
6 changes: 6 additions & 0 deletions packages/rs-platform-wallet-ffi/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,12 @@ impl From<PlatformWalletError> for PlatformWalletFFIResult {
PlatformWalletError::InvalidParameter(..) => {
PlatformWalletFFIResultCode::ErrorInvalidParameter
}
// A key set the creation pre-flight refused (DIP-11 levels or
// structure): a caller-input error the host fixes by passing the
// Rust layout's keys. The typed Display names the missing level.
PlatformWalletError::IdentityCreationKeys(..) => {
PlatformWalletFFIResultCode::ErrorInvalidParameter
}
// A second producer of code 31 (the arm above is the first),
// reached without any marker inspection at this layer: message
// signing found no signable account for the address, or the signer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
//! [`rs_sdk_ffi::MnemonicResolverCoreSigner`].

use dashcore::hashes::Hash;
use dashcore::{OutPoint, Txid};
use dpp::identity::accessors::IdentityGettersV0;
use dpp::prelude::Identifier;
use platform_wallet::AssetLockFunding;
Expand All @@ -32,7 +33,7 @@ use rs_sdk_ffi::MnemonicResolverCoreSigner;
use rs_sdk_ffi::MnemonicResolverHandle;

fn existing_asset_lock_funding(
out_point: dashcore::OutPoint,
out_point: OutPoint,
consume_invitation_voucher: bool,
) -> AssetLockFunding {
AssetLockFunding::FromExistingAssetLock {
Expand Down Expand Up @@ -89,6 +90,10 @@ pub unsafe extern "C" fn platform_wallet_register_identity_with_funding_signer(
Ok(m) => m,
Err(e) => return e,
};
let funding = AssetLockFunding::FromWalletBalance {
amount_duffs,
account_index,
};

// Round-trip both handles through `usize` so the spawned future's
// capture is `Send + 'static` — same pattern used by the existing
Expand All @@ -105,6 +110,11 @@ pub unsafe extern "C" fn platform_wallet_register_identity_with_funding_signer(
// two disagreed.
let network = wallet.sdk().network;
block_on_worker(async move {
// Rust picks and runs the creation check before either signer
// is touched.
let keys = identity_wallet
.creation_keys_for_funding(keys_map, &funding)
.await?;
// SAFETY: see the fn-level safety doc — both handles are
// pinned alive for the duration of this FFI call.
let identity_signer: &VTableSigner = unsafe { &*(signer_addr as *const VTableSigner) };
Expand All @@ -117,12 +127,9 @@ pub unsafe extern "C" fn platform_wallet_register_identity_with_funding_signer(
};
identity_wallet
.register_identity_with_funding(
AssetLockFunding::FromWalletBalance {
amount_duffs,
account_index,
},
funding,
identity_index,
keys_map,
keys,
identity_signer,
&asset_lock_signer,
None,
Expand Down Expand Up @@ -210,15 +217,18 @@ pub unsafe extern "C" fn platform_wallet_resume_identity_with_existing_asset_loc
// convention already used across `rs-drive-abci` /
// `rs-platform-wallet-ffi/src/persistence.rs`.
let out_point_ffi = *out_point;
let resume_outpoint = dashcore::OutPoint {
txid: dashcore::Txid::from_byte_array(out_point_ffi.txid),
let resume_outpoint = OutPoint {
txid: Txid::from_byte_array(out_point_ffi.txid),
vout: out_point_ffi.vout,
};

let keys_map = match decode_identity_pubkeys(identity_pubkeys, identity_pubkeys_count) {
Ok(m) => m,
Err(e) => return e,
};
// Rust decides which creation check applies (DIP-11, or the pre-DIP-11
// check for a tracked lock that cannot fund the DIP-11 key set).
let funding = existing_asset_lock_funding(resume_outpoint, consume_invitation_voucher);

let signer_addr = signer_handle as usize;
let core_signer_addr = core_signer_handle as usize;
Expand All @@ -228,6 +238,11 @@ pub unsafe extern "C" fn platform_wallet_resume_identity_with_existing_asset_loc
let wallet_id = wallet.wallet_id();
let network = wallet.sdk().network;
block_on_worker(async move {
// Rust picks and runs the creation check before either signer
// is touched.
let keys = identity_wallet
.creation_keys_for_funding(keys_map, &funding)
.await?;
// SAFETY: see the fn-level safety doc — both handles are
// pinned alive for the duration of this FFI call.
let identity_signer: &VTableSigner = unsafe { &*(signer_addr as *const VTableSigner) };
Expand All @@ -240,9 +255,9 @@ pub unsafe extern "C" fn platform_wallet_resume_identity_with_existing_asset_loc
};
identity_wallet
.register_identity_with_funding(
existing_asset_lock_funding(resume_outpoint, consume_invitation_voucher),
funding,
identity_index,
keys_map,
keys,
identity_signer,
&asset_lock_signer,
None,
Expand Down Expand Up @@ -309,8 +324,8 @@ pub unsafe extern "C" fn platform_wallet_topup_identity_with_existing_asset_lock
*out_new_balance = 0;

let out_point_ffi = *out_point;
let reclaim_outpoint = dashcore::OutPoint {
txid: dashcore::Txid::from_byte_array(out_point_ffi.txid),
let reclaim_outpoint = OutPoint {
txid: Txid::from_byte_array(out_point_ffi.txid),
vout: out_point_ffi.vout,
};
let identity_id = Identifier::from(*identity_id);
Expand Down Expand Up @@ -436,7 +451,7 @@ mod topup_existing_lock_guard_tests {

#[test]
fn topup_reclaim_forwards_explicit_invitation_authority() {
let out_point = dashcore::OutPoint::null();
let out_point = OutPoint::null();
assert!(matches!(
existing_asset_lock_funding(out_point, true),
AssetLockFunding::FromExistingAssetLock {
Expand All @@ -446,3 +461,168 @@ mod topup_existing_lock_guard_tests {
));
}
}

#[cfg(test)]
mod creation_keys_wiring_tests {
use super::*;
use std::ffi::CStr;
use std::ptr::{self, NonNull};
use std::sync::Arc;

use platform_wallet::test_support::{
test_platform_wallet_manager, track_registration_asset_lock, NoopTestPersister,
};
use platform_wallet::{IdentityCreationKeyLayout, PlatformWalletManager};

use crate::error::{platform_wallet_ffi_result_free, PlatformWalletFFIResultCode};
use crate::runtime::runtime;

const TRACKED_TXID: [u8; 32] = [7; 32];
const PUBKEY: [u8; 33] = [0x02; 33];

/// A wallet handle over a real wallet, optionally tracking a
/// registration lock of `amount_duffs` at [`TRACKED_TXID`]:0. The
/// returned manager owns the wallet and must outlive the calls.
fn live_wallet(
tracked_amount_duffs: Option<u64>,
) -> (Arc<PlatformWalletManager<NoopTestPersister>>, Handle) {
runtime().block_on(async {
let (manager, wallet_id) = test_platform_wallet_manager().await;
let wallet = manager.get_wallet(&wallet_id).await.expect("test wallet");
if let Some(amount) = tracked_amount_duffs {
let out_point = OutPoint {
txid: Txid::from_byte_array(TRACKED_TXID),
vout: 0,
};
track_registration_asset_lock(&wallet, out_point, amount).await;
}
(manager, PLATFORM_WALLET_STORAGE.insert(wallet))
})
}

fn tracked_out_point() -> OutPointFFI {
OutPointFFI {
txid: TRACKED_TXID,
vout: 0,
}
}

/// One FFI row per slot of `layout` (no contract bounds: only the
/// non-DashPay layouts are used here).
fn rows(layout: IdentityCreationKeyLayout) -> Vec<IdentityPubkeyFFI> {
layout
.slots()
.iter()
.map(|slot| IdentityPubkeyFFI {
key_id: slot.key_id,
key_type: slot.key_type as u8,
purpose: slot.purpose as u8,
security_level: slot.security_level as u8,
pubkey_bytes: PUBKEY.as_ptr(),
pubkey_len: PUBKEY.len(),
read_only: false,
contract_bounds_kind: 0,
contract_bounds_id: ptr::null(),
contract_bounds_document_type: ptr::null(),
has_total_budget: false,
total_budget: 0,
has_expires_at: false,
expires_at: 0,
})
.collect()
}

/// Non-null signer handles that are never dereferenced: the creation
/// check fails before either signer is touched.
fn dangling_signers() -> (*mut SignerHandle, *mut MnemonicResolverHandle) {
(
NonNull::<SignerHandle>::dangling().as_ptr(),
NonNull::<MnemonicResolverHandle>::dangling().as_ptr(),
)
}

/// The result's code and message, freeing the result.
fn take(mut result: PlatformWalletFFIResult) -> (PlatformWalletFFIResultCode, String) {
let message = if result.message.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(result.message) }
.to_string_lossy()
.into_owned()
};
let code = result.code;
unsafe { platform_wallet_ffi_result_free(&mut result) };
(code, message)
}

fn resume(
handle: Handle,
layout: IdentityCreationKeyLayout,
) -> (PlatformWalletFFIResultCode, String) {
let rows = rows(layout);
let (signer, core_signer) = dangling_signers();
let mut out_id = [0u8; 32];
let mut out_handle: Handle = 0;
take(unsafe {
platform_wallet_resume_identity_with_existing_asset_lock_signer(
handle,
&tracked_out_point(),
0,
rows.as_ptr(),
rows.len(),
signer,
core_signer,
false,
&mut out_id,
&mut out_handle,
)
})
}

#[test]
fn should_refuse_a_wallet_funded_registration_without_a_medium_key_before_signing() {
let (_manager, handle) = live_wallet(None);
let rows = rows(IdentityCreationKeyLayout::AssetLockResume);
let (signer, core_signer) = dangling_signers();
let mut out_id = [0u8; 32];
let mut out_handle: Handle = 0;
let (code, message) = take(unsafe {
platform_wallet_register_identity_with_funding_signer(
handle,
10_000_000,
0,
0,
rows.as_ptr(),
rows.len(),
signer,
core_signer,
&mut out_id,
&mut out_handle,
)
});
assert_eq!(code, PlatformWalletFFIResultCode::ErrorInvalidParameter);
assert!(message.contains("MEDIUM"), "unexpected message: {message}");
PLATFORM_WALLET_STORAGE.remove(handle);
}

#[test]
fn should_resume_an_underfunded_lock_only_with_the_four_legacy_keys() {
let (_manager, handle) = live_wallet(Some(1));
let (code, message) = resume(handle, IdentityCreationKeyLayout::NewIdentity);
assert_eq!(code, PlatformWalletFFIResultCode::ErrorInvalidParameter);
assert!(
message.contains("exactly the four pre-DIP-11 keys"),
"unexpected message: {message}"
);
PLATFORM_WALLET_STORAGE.remove(handle);
}

#[test]
fn should_require_the_medium_key_to_resume_an_amply_funded_lock() {
let (_manager, handle) = live_wallet(Some(10_000_000));
let (code, message) = resume(handle, IdentityCreationKeyLayout::AssetLockResume);
assert_eq!(code, PlatformWalletFFIResultCode::ErrorInvalidParameter);
assert!(message.contains("MEDIUM"), "unexpected message: {message}");
PLATFORM_WALLET_STORAGE.remove(handle);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,10 @@ use dpp::fee::Credits;
use dpp::identity::accessors::IdentityGettersV0;
use dpp::identity::identity_public_key::contract_bounds::ContractBounds;
use dpp::identity::identity_public_key::v0::IdentityPublicKeyV0;
use dpp::identity::v0::IdentityV0;
use dpp::identity::{Identity, IdentityPublicKey, KeyType, Purpose, SecurityLevel};
use dpp::identity::{IdentityPublicKey, KeyType, Purpose, SecurityLevel};
use dpp::platform_value::BinaryData;
use dpp::prelude::Identifier;
use platform_wallet::derive_identity_auth_keypair;
use platform_wallet::{derive_identity_auth_keypair, IdentityCreationKeys, PlatformWalletError};
use rs_sdk_ffi::{SignerHandle, VTableSigner};
use std::collections::BTreeMap;
use std::convert::TryFrom;
Expand Down Expand Up @@ -518,17 +517,14 @@ pub unsafe extern "C" fn platform_wallet_register_identity_with_signer(
identity_pubkeys,
identity_pubkeys_count,
));
let keys = match IdentityCreationKeys::new_identity(keys_map) {
Ok(keys) => keys,
Err(e) => return PlatformWalletError::from(e).into(),
};

let option = PLATFORM_WALLET_STORAGE.with_item(wallet_handle, |wallet| {
let wallet = wallet.clone();

let placeholder = Identity::V0(IdentityV0 {
id: Identifier::default(),
public_keys: keys_map,
balance: 0,
revision: 0,
});

block_on_worker(async move {
let identity_signer: &VTableSigner =
unsafe { &*(signer_identity_addr as *const VTableSigner) };
Expand All @@ -540,7 +536,7 @@ pub unsafe extern "C" fn platform_wallet_register_identity_with_signer(
// the proof.
wallet
.register_from_addresses(
&placeholder,
keys,
input_map,
output_map,
identity_index,
Expand Down
7 changes: 5 additions & 2 deletions packages/rs-platform-wallet-ffi/src/invitation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use dpp::identity::accessors::IdentityGettersV0;
use platform_wallet::wallet::identity::crypto::{
parse_invitation_uri, InviterInfo, ParsedInvitation,
};
use platform_wallet::PlatformWalletError;
use platform_wallet::{IdentityCreationKeys, PlatformWalletError};
use rs_sdk_ffi::{MnemonicResolverCoreSigner, MnemonicResolverHandle, SignerHandle, VTableSigner};

use platform_wallet::wallet::identity::network::{
Expand Down Expand Up @@ -310,6 +310,9 @@ pub unsafe extern "C" fn platform_wallet_claim_invitation(
Ok(m) => m,
Err(e) => return e,
};
let keys = unwrap_result_or_return!(
IdentityCreationKeys::new_identity(keys_map).map_err(PlatformWalletError::from)
);

let signer_addr = signer_handle as usize;

Expand All @@ -320,7 +323,7 @@ pub unsafe extern "C" fn platform_wallet_claim_invitation(
// `signer_handle` for the duration of this call.
let identity_signer: &VTableSigner = unsafe { &*(signer_addr as *const VTableSigner) };
identity_wallet
.claim_invitation(invitation, identity_index, keys_map, identity_signer, None)
.claim_invitation(invitation, identity_index, keys, identity_signer, None)
.await
})
});
Expand Down
Loading
Loading