Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 2 additions & 0 deletions packages/rs-dpp/src/shielded/builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ mod unshield;

pub use self::shield::build_shield_transition;
pub use shield_from_asset_lock::build_shield_from_asset_lock_transition;
#[cfg(feature = "core_key_wallet")]
pub use shield_from_asset_lock::build_shield_from_asset_lock_transition_with_signer;
pub use shielded_transfer::build_shielded_transfer_transition;
pub use shielded_withdrawal::build_shielded_withdrawal_transition;
pub use unshield::build_unshield_transition;
Expand Down
60 changes: 60 additions & 0 deletions packages/rs-dpp/src/shielded/builder/shield_from_asset_lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,66 @@ pub fn build_shield_from_asset_lock_transition<P: OrchardProver>(
)
}

/// Builds a ShieldFromAssetLock state transition where the
/// asset-lock-proof signature is produced by an external
/// [`key_wallet::signer::Signer`] (Swift / hardware-wallet / HSM
/// flow). The raw private key never crosses the FFI boundary;
/// derive + sign + zeroise happen inside the signer.
///
/// # Parameters
/// - `recipient` - Orchard address to receive the shielded note
/// - `shield_amount` - Amount of credits to shield (from the asset lock)
/// - `asset_lock_proof` - Proof that funds are locked on core chain
/// - `asset_lock_proof_path` - BIP32 path to the asset-lock key inside `asset_lock_signer`
/// - `asset_lock_signer` - External signer that produces the outer ECDSA signature
/// - `prover` - Orchard prover (holds the Halo 2 proving key)
/// - `memo` - 36-byte structured memo for the recipient (4-byte type tag + 32-byte payload)
/// - `platform_version` - Protocol version
#[cfg(feature = "core_key_wallet")]
#[allow(clippy::too_many_arguments)]
pub async fn build_shield_from_asset_lock_transition_with_signer<P, AS>(
recipient: &OrchardAddress,
shield_amount: u64,
asset_lock_proof: AssetLockProof,
asset_lock_proof_path: &::key_wallet::bip32::DerivationPath,
asset_lock_signer: &AS,
prover: &P,
memo: [u8; 36],
platform_version: &PlatformVersion,
) -> Result<StateTransition, ProtocolError>
where
P: OrchardProver,
AS: ::key_wallet::signer::Signer,
{
let bundle = build_output_only_bundle(recipient, shield_amount, memo, prover)?;
let sb = serialize_authorized_bundle(&bundle);

// For output-only bundles, Orchard value_balance is negative (value flowing in).
// Convert to u64 (absolute amount entering the pool).
let value_balance = sb
.value_balance
.checked_neg()
.and_then(|v| u64::try_from(v).ok())
.ok_or_else(|| {
ProtocolError::ShieldedBuildError(
"shield_from_asset_lock: bundle value_balance is not negative".to_string(),
)
})?;

ShieldFromAssetLockTransition::try_from_asset_lock_with_bundle_and_signer(
asset_lock_proof,
asset_lock_proof_path,
asset_lock_signer,
sb.actions,
value_balance,
sb.anchor,
sb.proof,
sb.binding_signature,
platform_version,
)
.await
}

#[cfg(test)]
mod tests {
use super::super::{build_output_only_bundle, serialize_authorized_bundle};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,48 @@ impl ShieldFromAssetLockTransitionMethodsV0 for ShieldFromAssetLockTransition {
}),
}
}

#[cfg(all(feature = "state-transition-signing", feature = "core_key_wallet"))]
async fn try_from_asset_lock_with_bundle_and_signer<AS>(
asset_lock_proof: AssetLockProof,
asset_lock_proof_path: &::key_wallet::bip32::DerivationPath,
asset_lock_signer: &AS,
actions: Vec<SerializedAction>,
value_balance: u64,
anchor: [u8; 32],
proof: Vec<u8>,
binding_signature: [u8; 64],
platform_version: &PlatformVersion,
) -> Result<StateTransition, ProtocolError>
where
AS: ::key_wallet::signer::Signer,
{
match platform_version
.dpp
.state_transition_serialization_versions
.shield_from_asset_lock_state_transition
.default_current_version
{
0 => {
ShieldFromAssetLockTransitionV0::try_from_asset_lock_with_bundle_and_signer(
asset_lock_proof,
asset_lock_proof_path,
asset_lock_signer,
actions,
value_balance,
anchor,
proof,
binding_signature,
platform_version,
)
.await
}
version => Err(ProtocolError::UnknownVersionMismatch {
method: "ShieldFromAssetLockTransition::try_from_asset_lock_with_bundle_and_signer"
.to_string(),
known_versions: vec![0],
received: version,
}),
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,33 @@ pub trait ShieldFromAssetLockTransitionMethodsV0 {
platform_version: &PlatformVersion,
) -> Result<StateTransition, ProtocolError>;

/// Build a `ShieldFromAssetLock` state transition where the
/// asset-lock-proof signature is produced by an external
/// [`key_wallet::signer::Signer`].
///
/// `asset_lock_signer` produces the outer state-transition ECDSA
/// signature for the key at `asset_lock_proof_path` — atomically
/// deriving, signing, and zeroising inside the signer's trust
/// boundary. This is the signing path used by hosts that hold
/// their private keys outside Rust (the iOS Swift SDK, hardware
/// wallets, remote signers); the raw key never crosses the FFI
/// boundary.
#[cfg(all(feature = "state-transition-signing", feature = "core_key_wallet"))]
#[allow(clippy::too_many_arguments)]
async fn try_from_asset_lock_with_bundle_and_signer<AS>(
asset_lock_proof: AssetLockProof,
asset_lock_proof_path: &::key_wallet::bip32::DerivationPath,
asset_lock_signer: &AS,
actions: Vec<SerializedAction>,
value_balance: u64,
anchor: [u8; 32],
proof: Vec<u8>,
binding_signature: [u8; 64],
platform_version: &PlatformVersion,
) -> Result<StateTransition, ProtocolError>
where
AS: ::key_wallet::signer::Signer;

/// Get State Transition Type
fn get_type() -> StateTransitionType {
StateTransitionType::ShieldFromAssetLock
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,46 @@ impl ShieldFromAssetLockTransitionMethodsV0 for ShieldFromAssetLockTransitionV0

Ok(transition.into())
}

#[cfg(all(feature = "state-transition-signing", feature = "core_key_wallet"))]
async fn try_from_asset_lock_with_bundle_and_signer<AS>(
asset_lock_proof: AssetLockProof,
asset_lock_proof_path: &::key_wallet::bip32::DerivationPath,
asset_lock_signer: &AS,
actions: Vec<SerializedAction>,
value_balance: u64,
anchor: [u8; 32],
proof: Vec<u8>,
binding_signature: [u8; 64],
_platform_version: &PlatformVersion,
) -> Result<StateTransition, ProtocolError>
where
AS: ::key_wallet::signer::Signer,
{
// Build the unsigned inner transition. The `signature` field
// is `#[platform_signable(exclude_from_sig_hash)]`, so the
// signable bytes are stable across the empty-then-signed
// transition.
let transition = ShieldFromAssetLockTransitionV0 {
asset_lock_proof,
actions,
value_balance,
anchor,
proof,
binding_signature,
signature: Default::default(),
};

// Hand the outer ST to `sign_with_core_signer`, which routes
// the asset-lock-proof signature through the external Signer.
// The derive + sign + zeroise sequence happens inside the
// signer — the host never sees a raw private key; only a
// 32-byte digest goes in and a serialised signature comes out.
let mut state_transition: StateTransition = transition.into();
state_transition
.sign_with_core_signer(asset_lock_proof_path, asset_lock_signer)
.await?;

Ok(state_transition)
}
}
Loading
Loading