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
15 changes: 14 additions & 1 deletion key-wallet-ffi/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1146,8 +1146,21 @@ pub unsafe extern "C" fn wallet_build_and_sign_asset_lock_transaction(
// Write outputs
*fee_out = result.fee;

// `build_asset_lock` always returns private keys; the signer-variant
// path uses a different FFI entry point.
let private_keys = match &result.keys {
key_wallet::wallet::managed_wallet_info::asset_lock_builder::AssetLockCreditKeys::Private(k) => k,
key_wallet::wallet::managed_wallet_info::asset_lock_builder::AssetLockCreditKeys::Public(_) => {
FFIError::set_error(
error,
FFIErrorCode::WalletError,
"Unexpected public-key result from build_asset_lock".to_string(),
);
return false;
}
};
let keys_out = slice::from_raw_parts_mut(private_keys_out, credit_outputs_count);
for (i, key) in result.keys.iter().enumerate() {
for (i, key) in private_keys.iter().enumerate() {
if i < keys_out.len() {
keys_out[i] = *key;
}
Expand Down
2 changes: 2 additions & 0 deletions key-wallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub mod managed_account;
pub mod mnemonic;
pub mod psbt;
pub mod seed;
pub mod signer;
pub mod transaction_checking;
pub(crate) mod utils;
pub mod utxo;
Expand All @@ -61,6 +62,7 @@ pub use managed_account::managed_platform_account::ManagedPlatformAccount;
pub use managed_account::platform_address::PlatformP2PKHAddress;
pub use mnemonic::Mnemonic;
pub use seed::Seed;
pub use signer::{Signer, SignerMethod, TransactionCategory};
pub use utxo::Utxo;
pub use wallet::{balance::WalletCoreBalance, Wallet};

Expand Down
25 changes: 25 additions & 0 deletions key-wallet/src/managed_account/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1079,6 +1079,31 @@ impl ManagedCoreAccount {
Ok(private_key)
}

/// Consume the next unused address and return only its derivation path.
///
/// Analogous to [`Self::next_private_key`] but does not require any
/// root extended private key: used when signing is delegated to an
/// external [`Signer`](crate::signer::Signer), which holds the keys
/// and only needs the path to produce signatures or public keys.
///
/// Only works for single-pool account types (not Standard accounts).
pub fn next_path(&mut self) -> Result<crate::DerivationPath, &'static str> {
if matches!(self.account_type, ManagedAccountType::Standard { .. }) {
return Err("Standard accounts must use next_receive_address or next_change_address");
}

let mut pools = self.account_type.address_pools_mut();
let pool = pools.first_mut().ok_or("Account has no address pool")?;

let info = pool
.next_unused_with_info(&address_pool::KeySource::NoKeySource, false)
.map_err(|_| "No unused address available")?;

pool.mark_index_used(info.index);

Ok(info.path)
}

/// Get the derivation path for an address if it belongs to this account
pub fn address_derivation_path(&self, address: &Address) -> Option<crate::DerivationPath> {
self.account_type.get_address_derivation_path(address)
Expand Down
113 changes: 113 additions & 0 deletions key-wallet/src/signer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//! External signer abstraction.
//!
//! A [`Signer`] answers signing requests for private keys the host does not
//! hold. It is the integration point for hardware wallets and remote signers
//! used with [`WalletType::ExternalSignable`](crate::wallet::WalletType::ExternalSignable):
//! the device owns every private key, and the host only sends derivation paths
//! plus either pre-computed sighashes or full transactions — depending on what
//! the device supports (see [`SignerMethod`]).
//!
//! The trait is async because hardware-wallet round-trips are inherently
//! asynchronous (USB, BLE, network). Soft-wallet implementations can wrap a
//! sync derive-and-sign in `async {}` without meaningful overhead.

use async_trait::async_trait;
use secp256k1::{ecdsa, PublicKey};

use crate::bip32::DerivationPath;

/// A signing method a [`Signer`] can perform.
///
/// Callers check which methods a signer supports via
/// [`Signer::supported_methods`] and dispatch accordingly. A remote cloud
/// signer or soft wallet typically supports [`SignerMethod::Digest`] (blind
/// sighash signing). A hardware wallet protecting the user from a
/// compromised host cannot safely sign blind digests — it needs the full
/// transaction to re-hash and display — so it advertises
/// [`SignerMethod::Transaction`] instead.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SignerMethod {
/// Sign a host-computed 32-byte digest. The device trusts that the
/// digest matches the intended transaction: fast, but offers no
/// on-device review of what's actually being signed. Suitable for
/// trusted remote signers and HSMs; **not** suitable for hardware
/// wallets that defend against a compromised host.
Digest,

/// Sign a full Dash transaction of a given [`TransactionCategory`].
/// The signer receives the unsigned transaction plus per-input
/// metadata, re-hashes it internally, and (for hardware wallets) may
/// present transaction details to the user for approval.
///
/// A signer advertises one variant per category it can parse and
/// render — hardware-wallet firmware typically ships support for
/// categories rather than individual transaction types.
Transaction(TransactionCategory),
}

/// Category of Dash transaction, grouped by on-chain purpose.
///
/// Categories correspond to the transaction shapes a signer has to
/// understand in order to safely display and sign them. Grouping by
/// category (rather than by the raw DIP-2 type byte) matches how
/// hardware-wallet firmware tends to gate feature support: a firmware
/// release either understands "masternode lifecycle transactions" or it
/// doesn't — not one specific sub-type.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TransactionCategory {
/// Classical transaction — P2PKH / P2SH value transfer, no special
/// payload. DIP-2 type 0.
Classical,

/// Platform credit flow. Locks Dash on L1 to credit Platform, or
/// unlocks credits back to L1. DIP-2 types 8 (AssetLock) and 9
/// (AssetUnlock).
PlatformCredits,

/// DIP-3 masternode lifecycle. Register, update, or revoke a
/// masternode. DIP-2 types 3 (ProRegTx), 4 (ProUpServTx), 5
/// (ProUpRegTx), 6 (ProUpRevTx).
MasternodeLifecycle,
}

/// Sign on behalf of keys the host does not possess.
#[async_trait]
pub trait Signer {
/// Error produced by the underlying signing device or service.
type Error: std::fmt::Display + Send + Sync + 'static;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// Signing methods this signer can perform. A caller that needs a
/// method the signer doesn't advertise should fail fast rather than
/// invoke a trait method it knows will be rejected.
///
/// Returned as a borrowed slice so signers can back this with a
/// `&'static` constant when capabilities are fixed, or with a field
/// when they're resolved at runtime (e.g. after a firmware-version
/// handshake).
fn supported_methods(&self) -> &[SignerMethod];

/// Convenience: whether `method` appears in [`Self::supported_methods`].
fn supports(&self, method: SignerMethod) -> bool {
self.supported_methods().contains(&method)
}

/// Produce an ECDSA signature over `sighash` for the key at `path`,
/// along with the compressed public key needed to assemble the scriptSig.
///
/// `sighash` is the pre-computed 32-byte message digest (e.g. a legacy
/// P2PKH sighash). The signer must not re-derive or alter it.
///
/// Only valid when the signer supports [`SignerMethod::Digest`].
async fn sign_ecdsa(
&self,
path: &DerivationPath,
sighash: [u8; 32],
) -> Result<(ecdsa::Signature, PublicKey), Self::Error>;

/// Return the compressed public key at `path` without signing.
///
/// Used to capture per-output public keys (e.g. asset-lock credit-output
/// keys) that the caller later references when signing Platform state
/// transitions.
async fn public_key(&self, path: &DerivationPath) -> Result<PublicKey, Self::Error>;
}
Loading
Loading