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
7 changes: 7 additions & 0 deletions packages/rs-platform-wallet-ffi/src/shielded_send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,11 @@ pub unsafe extern "C" fn platform_wallet_manager_shielded_withdraw(
/// denomination + full key set) + a per-key proof-of-possession produced via
/// `signer_identity_handle`. There is NO platform identity signature.
///
/// `identity_index` is the DIP-9 identity-registration slot the new identity occupies. On a
/// successful broadcast the wallet registers the proof-verified identity at this slot in its local
/// `IdentityManager` (mirroring address-funded registration), which drives the host persister's
/// identity-row emit. It carries no decision here — it is marshalled straight through to the wallet.
///
/// On success the 32-byte new identity id (`double_sha256(sorted nullifiers)`) is written to
/// `out_identity_id`. The id is deterministic in the spent notes, so the host can also predict it
/// independently if needed.
Expand Down Expand Up @@ -367,6 +372,7 @@ pub unsafe extern "C" fn platform_wallet_manager_shielded_identity_create_from_p
handle: Handle,
wallet_id_bytes: *const u8,
account: u32,
identity_index: u32,
identity_pubkeys: *const IdentityPubkeyFFI,
identity_pubkeys_count: usize,
denomination: u64,
Expand Down Expand Up @@ -444,6 +450,7 @@ pub unsafe extern "C" fn platform_wallet_manager_shielded_identity_create_from_p
.shielded_identity_create_from_pool(
&coordinator,
account,
identity_index,
public_keys,
denomination,
send_to_address_on_creation_failure,
Expand Down
97 changes: 73 additions & 24 deletions packages/rs-platform-wallet/src/wallet/platform_wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -711,13 +711,21 @@ impl PlatformWallet {
/// `public_keys` is the new identity's key set (each entry pairs the `IdentityPublicKey` with
/// its `IdentityPublicKeyInCreation` form); `identity_signer` produces each key's
/// proof-of-possession signature. The Orchard spend authority comes from the wallet's own
/// `OrchardKeySet` (the ASK never crosses to the coordinator). Returns the new identity's id.
/// `OrchardKeySet` (the ASK never crosses to the coordinator).
///
/// `identity_index` is the DIP-9 identity-registration slot the new identity occupies in the
/// local `IdentityManager`; on a successful broadcast the proof-verified identity is registered
/// there (mirroring `register_from_addresses`) so the host persister emits the
/// `IdentityChangeSet` / `IdentityKeysChangeSet` that creates the app's identity row. A failed
/// registration after a successful broadcast is logged and swallowed — the identity already
/// exists on chain, so the next sync heals the local row. Returns the new identity's id.
#[cfg(feature = "shielded")]
#[allow(clippy::too_many_arguments)]
pub async fn shielded_identity_create_from_pool<P, IS>(
&self,
coordinator: &Arc<crate::wallet::shielded::NetworkShieldedCoordinator>,
account: u32,
identity_index: u32,
public_keys: Vec<(
dpp::identity::IdentityPublicKey,
dpp::state_transition::public_key_in_creation::IdentityPublicKeyInCreation,
Expand All @@ -731,29 +739,70 @@ impl PlatformWallet {
P: dpp::shielded::builder::OrchardProver,
IS: dpp::identity::signer::Signer<dpp::identity::IdentityPublicKey> + Send + Sync,
{
let guard = self.shielded_keys.read().await;
let keys = guard
.as_ref()
.ok_or(PlatformWalletError::ShieldedNotBound)?;
let keyset = keys.get(&account).ok_or_else(|| {
PlatformWalletError::ShieldedKeyDerivation(format!(
"shielded account {account} not bound"
))
})?;
super::shielded::operations::identity_create_from_shielded_pool(
&self.sdk,
coordinator.store(),
Some(&self.persister),
self.wallet_id,
keyset,
account,
public_keys,
denomination,
send_to_address_on_creation_failure,
identity_signer,
&prover,
)
.await
let (identity_id, identity) = {
// Scope the read guard so it's released before we take the wallet-manager write lock
// below — the keyset is only needed for the spend, not for the registration step.
let guard = self.shielded_keys.read().await;
let keys = guard
.as_ref()
.ok_or(PlatformWalletError::ShieldedNotBound)?;
let keyset = keys.get(&account).ok_or_else(|| {
PlatformWalletError::ShieldedKeyDerivation(format!(
"shielded account {account} not bound"
))
})?;
super::shielded::operations::identity_create_from_shielded_pool(
&self.sdk,
coordinator.store(),
Some(&self.persister),
self.wallet_id,
keyset,
account,
public_keys,
denomination,
send_to_address_on_creation_failure,
identity_signer,
&prover,
)
.await?
};

// Register the proof-verified identity in the local manager at its HD slot, exactly like
// `register_from_addresses`' Step 3 — this drives the host persister's
// `IdentityChangeSet` / `IdentityKeysChangeSet` emit so the app's identity row is created.
// The broadcast already succeeded; a registration failure here (e.g. the slot is already
// occupied locally) is logged and swallowed rather than surfaced as an error, since the
// identity exists on chain and the next sync heals the local view.
{
let mut wm = self.wallet_manager.write().await;
match wm.get_wallet_info_mut(&self.wallet_id) {
Some(info) => {
if let Err(e) = info.identity_manager.add_identity(
identity,
identity_index,
self.wallet_id,
&self.persister,
) {
tracing::warn!(
identity_index,
error = %e,
"IdentityCreateFromShieldedPool broadcast succeeded but registering the \
identity in the local manager failed; the on-chain identity exists and \
the next sync will heal the local row"
);
}
}
None => {
tracing::warn!(
identity_index,
"IdentityCreateFromShieldedPool broadcast succeeded but the wallet info was \
not found in the manager; skipping local registration (heals on next sync)"
);
}
}
}

Ok(identity_id)
}

/// Shield credits from a Platform Payment account into the
Expand Down
95 changes: 79 additions & 16 deletions packages/rs-platform-wallet/src/wallet/shielded/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ use dpp::address_funds::{
AddressFundsFeeStrategy, AddressFundsFeeStrategyStep, OrchardAddress, PlatformAddress,
};
use dpp::fee::Credits;
use dpp::identity::accessors::{IdentityGettersV0, IdentitySettersV0};
use dpp::identity::core_script::CoreScript;
use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0;
use dpp::identity::signer::Signer;
use dpp::identity::IdentityPublicKey;
use dpp::identity::{Identity, IdentityPublicKey};
use dpp::prelude::Identifier;
use dpp::shielded::builder::{
build_identity_create_from_shielded_pool_transition, build_shield_transition,
Expand Down Expand Up @@ -668,8 +670,10 @@ pub async fn withdraw<S: ShieldedStore, P: OrchardProver>(
/// per-action spend-auth signatures + binding signature (which commits the derived id + denomination
/// + full key set) + the per-key PoP — there is NO platform identity signature.
///
/// Returns the new identity's id (`double_sha256(sorted nullifiers)`), derived deterministically
/// from the spent notes' nullifiers.
/// Returns the new identity's id (`double_sha256(sorted nullifiers)`, derived deterministically
/// from the spent notes' nullifiers) together with the proof-verified [`Identity`] returned by the
/// SDK broadcast. The caller registers that `Identity` in its local `IdentityManager` so the host
/// persister emits the row, mirroring the address-funded registration path.
#[allow(clippy::too_many_arguments)]
pub async fn identity_create_from_shielded_pool<S, P, IS>(
sdk: &Arc<dash_sdk::Sdk>,
Expand All @@ -683,7 +687,7 @@ pub async fn identity_create_from_shielded_pool<S, P, IS>(
send_to_address_on_creation_failure: PlatformAddress,
identity_signer: &IS,
prover: &P,
) -> Result<Identifier, PlatformWalletError>
) -> Result<(Identifier, Identity), PlatformWalletError>
where
S: ShieldedStore,
P: OrchardProver,
Expand Down Expand Up @@ -715,6 +719,16 @@ where
"IdentityCreateFromShieldedPool"
);

// Snapshot the submitted `IdentityPublicKey` halves keyed by their `KeyID` BEFORE the build
// consumes `public_keys`. This is the canonical record of the key set the transition commits to
// (the binding signature covers it), so it's the defensive fallback if the proof-verified
// identity comes back with an empty `public_keys()` map — same pattern register_from_addresses
// uses for its address-funded `put_*` stub.
let submitted_public_keys: BTreeMap<u32, IdentityPublicKey> = public_keys
.iter()
.map(|(key, _)| (key.id(), key.clone()))
.collect();

// From here on every error path must release the reservation taken above.
let result = async {
let (spends, anchor) = extract_spends_and_anchor(store, &selected_notes).await?;
Expand All @@ -741,22 +755,71 @@ where
trace!("IdentityCreateFromShieldedPool: built, broadcasting via SDK helper...");
// Broadcast through the SDK helper, which re-assembles the transition from the PoP-signed
// keys + bundle params (preserving the per-key signatures) and waits for proven execution.
sdk.identity_create_from_shielded_pool(
build.public_keys,
denomination,
send_to_address_on_creation_failure,
build.bundle,
None,
)
.await
.map_err(|e| PlatformWalletError::ShieldedBroadcastFailed(e.to_string()))?;
// It returns a `VerifiedIdentityWithShieldedNullifiers` proof result carrying the
// proof-verified `Identity` (and the consumed nullifiers).
let proof_result = sdk
.identity_create_from_shielded_pool(
build.public_keys,
denomination,
send_to_address_on_creation_failure,
build.bundle,
None,
)
.await
.map_err(|e| PlatformWalletError::ShieldedBroadcastFailed(e.to_string()))?;

// Pull the verified `Identity` out of the proof result. The expected variant is
// `VerifiedIdentityWithShieldedNullifiers`; if drive-abci ever returns a different one the
// broadcast still SUCCEEDED, so we don't turn it into an error — we synthesize the identity
// from the derived id + submitted keys (the binding signature committed both) and warn, so
// the local row is still created.
let identity = match proof_result {
StateTransitionProofResult::VerifiedIdentityWithShieldedNullifiers(
mut identity,
_nullifiers,
) => {
// The proof-verified id is authoritative: it's recomputed from the proven nullifier
// set, while `identity_id` was derived pre-broadcast. They should match (the derived
// id is committed in the sighash), but trust the verified one.
if identity.id() != identity_id {
warn!(
derived_id = %identity_id,
verified_id = %identity.id(),
"IdentityCreateFromShieldedPool: derived id differs from proof-verified id; \
using the proof-verified id"
);
}
// Defensive: a proof result can hand back an identity whose `public_keys` map is
// empty. Fill it from the submitted set so downstream auth-key checks see the keys
// immediately without waiting for the next identity-fetch round (the transition
// committed exactly these keys, so id reproducibility is preserved).
if identity.public_keys().is_empty() {
identity.set_public_keys(submitted_public_keys);
}
identity
}
other => {
warn!(
derived_id = %identity_id,
result = %other,
"IdentityCreateFromShieldedPool: unexpected proof-result variant; synthesizing \
the identity from the derived id + submitted keys so the local row still lands"
);
Identity::new_with_id_and_keys(
identity_id,
submitted_public_keys,
sdk.version(),
)
.map_err(|e| PlatformWalletError::ShieldedBuildError(e.to_string()))?
}
};

Ok::<Identifier, PlatformWalletError>(identity_id)
Ok::<(Identifier, Identity), PlatformWalletError>((identity.id(), identity))
}
.await;

match result {
Ok(identity_id) => {
Ok((identity_id, identity)) => {
// Best-effort post-broadcast bookkeeping (see `unshield`): mark the spent notes so the
// local balance reflects the exit immediately; any drift heals on the next nullifier
// sync. The on-chain nullifier set — not this local mark — is the authoritative
Expand All @@ -776,7 +839,7 @@ where
identity_id = %identity_id,
"IdentityCreateFromShieldedPool broadcast succeeded"
);
Ok(identity_id)
Ok((identity_id, identity))
}
Err(e) => {
cancel_pending(store, id, &selected_notes).await;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,13 @@ extension PlatformWalletManager {
/// the bound wallet's own key. Returns the 32-byte new identity id
/// (`double_sha256(sorted nullifiers)`).
///
/// `identityIndex` is the DIP-9 identity-registration slot the new
/// identity occupies. On a successful broadcast the Rust wallet
/// registers the proof-verified identity at this slot in its local
/// `IdentityManager` (mirroring address-funded registration), which
/// drives the persister callbacks that create the app's identity
/// row. This wrapper only marshals it across the FFI.
///
/// `sendToAddressOnCreationFailure` is the REQUIRED fallback
/// platform address as raw `PlatformAddress` storage bytes (21
/// bytes: 1-byte variant tag + 20-byte hash, the encoding
Expand All @@ -639,6 +646,7 @@ extension PlatformWalletManager {
public func shieldedIdentityCreateFromPool(
walletId: Data,
account: UInt32 = 0,
identityIndex: UInt32,
identityPubkeys: [ManagedPlatformWallet.IdentityPubkey],
denomination: UInt64,
sendToAddressOnCreationFailure: Data,
Expand Down Expand Up @@ -717,6 +725,7 @@ extension PlatformWalletManager {
handle,
widPtr,
account,
identityIndex,
ffiRowsPtr,
UInt(ffiRowsCount),
denomination,
Expand Down
Loading
Loading