Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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: 5 additions & 1 deletion packages/rs-platform-wallet-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ description = "C FFI bindings for platform-wallet"
crate-type = ["staticlib", "cdylib", "rlib"]

[dependencies]
platform-wallet = { path = "../rs-platform-wallet" }
# `bls`/`eddsa` are required explicitly, not just inherited from
# `platform-wallet`'s default features: `persistence.rs` calls
# `rebuild_provider_key_account` and matches on `ProviderKeyExtendedPubKey`,
# both gated behind those features in `platform-wallet`.
platform-wallet = { path = "../rs-platform-wallet", features = ["bls", "eddsa"] }
dpp = { path = "../rs-dpp" }
dash-sdk = { path = "../rs-sdk", features = ["wallet"] }
# Needed for `SignerHandle` + `VTableSigner` so the `*_with_signer`
Expand Down
126 changes: 100 additions & 26 deletions packages/rs-platform-wallet-ffi/src/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

use bincode::config;
use key_wallet::account::account_collection::AccountCollection;
use key_wallet::account::{Account, AccountType, BLSAccount, EdDSAAccount, StandardAccountType};
use key_wallet::account::{Account, AccountType, StandardAccountType};
use key_wallet::bip32::DerivationPath;
use key_wallet::bip32::ExtendedPubKey;
use key_wallet::derivation_bls_bip32::ExtendedBLSPubKey;
Expand All @@ -27,6 +27,9 @@ use parking_lot::Mutex;
use std::str::FromStr;

use crate::types::{FFINetwork, Network};
use platform_wallet::changeset::provider_key_account::{
rebuild_provider_key_account, ProviderAccountRebuildError,
};
use platform_wallet::changeset::{
AccountAddressPoolEntry, AccountRegistrationEntry, ClientStartState, ClientWalletStartState,
ListedCoreTxid, PersistenceCapabilities, PersistenceError, PersistenceErrorKind,
Expand Down Expand Up @@ -4969,6 +4972,23 @@ impl Drop for LoadGuard {
}
}

/// Map a provider-account rebuild failure to a load error naming the
/// curve-specific constructor or `AccountCollection` insert that failed.
fn provider_rebuild_error(
constructor: &str,
insert: &str,
error: ProviderAccountRebuildError,
) -> PersistenceError {
match error {
ProviderAccountRebuildError::Invalid(e) => {
PersistenceError::backend(format!("{constructor} failed: {e:?}"))
}
ProviderAccountRebuildError::Rejected(e) => {
PersistenceError::backend(format!("AccountCollection::{insert} failed: {e}"))
}
}
}

/// Reconstruct an external-signable [`Wallet`] + matching start-state
/// bucket from a single `WalletRestoreEntryFFI`. The mnemonic / seed
/// stays in the host's keychain; signing requests route back through
Expand Down Expand Up @@ -5029,7 +5049,7 @@ fn build_wallet_start_state(
// platform node keys) live in dedicated `Option` fields on the
// collection and carry a non-secp256k1 extended public key in
// the same `account_xpub_bytes` slot. Rebuild them watch-only
// via the type-specific `new` + insert methods rather than the
// via the shared `rebuild_provider_key_account` rather than the
// ECDSA `Account::from_xpub` / `insert` path (which would fail
// to decode the bytes and reject the provider `AccountType`).
// Provider xpubs are stored raw (`bincode(xpub)`), exactly like the
Expand All @@ -5056,21 +5076,14 @@ fn build_wallet_start_state(
.map_err(|e| {
PersistenceError::backend(format!("failed to decode provider BLS xpub: {}", e))
})?;
let bls_account = BLSAccount::new(
Some(entry.wallet_id.to_vec()),
account_type,
bls_pubkey,
rebuild_provider_key_account(
&mut accounts,
entry.wallet_id,
network,
account_type,
&ProviderKeyExtendedPubKey::Bls(bls_pubkey),
)
.map_err(|e| {
PersistenceError::backend(format!("BLSAccount::new failed: {:?}", e))
})?;
accounts.insert_bls_account(bls_account).map_err(|e| {
PersistenceError::backend(format!(
"AccountCollection::insert_bls_account failed: {}",
e
))
})?;
.map_err(|e| provider_rebuild_error("BLSAccount::new", "insert_bls_account", e))?;
continue;
}
AccountType::ProviderPlatformKeys => {
Expand All @@ -5084,20 +5097,15 @@ fn build_wallet_start_state(
e
))
})?;
let eddsa_account = EdDSAAccount::new(
Some(entry.wallet_id.to_vec()),
account_type,
ed_pubkey,
rebuild_provider_key_account(
&mut accounts,
entry.wallet_id,
network,
account_type,
&ProviderKeyExtendedPubKey::EdDSA(ed_pubkey),
)
.map_err(|e| {
PersistenceError::backend(format!("EdDSAAccount::new failed: {:?}", e))
})?;
accounts.insert_eddsa_account(eddsa_account).map_err(|e| {
PersistenceError::backend(format!(
"AccountCollection::insert_eddsa_account failed: {}",
e
))
provider_rebuild_error("EdDSAAccount::new", "insert_eddsa_account", e)
})?;
// The platform-node (Ed25519) pool is rehydrated from the
// persisted core-address rows like every other pool — see
Expand Down Expand Up @@ -9107,6 +9115,72 @@ mod tests {
);
}

/// `build_wallet_start_state` rebuilds the BLS operator-key and EdDSA
/// platform-node-key accounts watch-only from their bincode-encoded specs.
#[test]
fn provider_key_accounts_survive_restore_round_trip() {
let wallet = Wallet::from_seed_bytes(
[0x42; 64],
Network::Testnet,
key_wallet::wallet::initialization::WalletAccountCreationOptions::Default,
)
Comment on lines +9122 to +9126

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: Use the shared provider-key wallet fixture in the new FFI restore tests

The new tests inline the same Wallet::from_seed_bytes(..., WalletAccountCreationOptions::Default) construction that the shared provider_key_test_wallet() fixture was introduced to centralize. The fixture pins the options that guarantee both provider accounts exist; using it here and in the corresponding SQLite tests prevents these assumptions from drifting independently when upstream wallet defaults change.

Suggested change
let wallet = Wallet::from_seed_bytes(
[0x42; 64],
Network::Testnet,
key_wallet::wallet::initialization::WalletAccountCreationOptions::Default,
)
let wallet =
platform_wallet::changeset::provider_key_account::provider_key_test_wallet();

source: glm-5.3-flash (phase1-reviewer: general, ffi-engineer, rust-quality)

.expect("seeded wallet");
let bls = wallet
.accounts
.bls_account_of_type(AccountType::ProviderOperatorKeys)
.expect("a Default-created wallet has a BLS provider account")
.bls_public_key
.clone();
let eddsa = wallet
.accounts
.eddsa_account_of_type(AccountType::ProviderPlatformKeys)
.expect("a Default-created wallet has an EdDSA provider account")
.ed25519_public_key
.clone();
let bls_bytes = bincode::encode_to_vec(&bls, config::standard()).expect("encode BLS xpub");
let eddsa_bytes =
bincode::encode_to_vec(&eddsa, config::standard()).expect("encode EdDSA xpub");
let specs = [
build_account_spec_ffi(&AccountType::ProviderOperatorKeys, &bls_bytes),
build_account_spec_ffi(&AccountType::ProviderPlatformKeys, &eddsa_bytes),
];
let entry = WalletRestoreEntryFFI {
wallet_id: wallet.wallet_id,
accounts: specs.as_ptr(),
accounts_count: specs.len(),
..Default::default()
};

let (state, _) =
build_wallet_start_state(&entry).expect("provider key accounts must restore");

let restored_bls = state
.wallet
.accounts
.bls_account_of_type(AccountType::ProviderOperatorKeys)
.expect("BLS provider account must be rebuilt");
let restored_bls_bytes =
bincode::encode_to_vec(&restored_bls.bls_public_key, config::standard())
.expect("encode restored BLS xpub");
assert_eq!(restored_bls_bytes, bls_bytes);
assert_eq!(
restored_bls.parent_wallet_id.as_deref(),
Some(&wallet.wallet_id[..])
);
assert!(restored_bls.is_watch_only);
let restored_eddsa = state
.wallet
.accounts
.eddsa_account_of_type(AccountType::ProviderPlatformKeys)
.expect("EdDSA provider account must be rebuilt");
assert_eq!(restored_eddsa.ed25519_public_key, eddsa);
assert_eq!(
restored_eddsa.parent_wallet_id.as_deref(),
Some(&wallet.wallet_id[..])
);
assert!(restored_eddsa.is_watch_only);
}

/// The BLS xpub decoder is the only account-xpub graph with a
/// length-prefixed field (the public key bytes). A host-supplied blob
/// whose prefix claims a gigabyte must fail on the decode budget, before
Expand Down
11 changes: 11 additions & 0 deletions packages/rs-platform-wallet-storage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,15 @@ hex = "0.4"
# (dashpay writer). `dash-sdk` is here for the `AddressFunds` re-export
# in `schema/platform_addrs.rs`. Storage declares only the features it uses
# directly; `platform-wallet` adds `dash-sdk/wallet` to Cargo's unified set.
# `bls`/`eddsa` are required explicitly (not just inherited from
# `platform-wallet`'s default features) because `rebuild_provider_key_account`
# and `ProviderKeyExtendedPubKey`'s variants live behind those gates — a
# `platform-wallet` built with `default-features = false` must not silently
# break this crate's compile.
platform-wallet = { path = "../rs-platform-wallet", features = [
"serde",
"bls",
"eddsa",
], optional = true }
serde = { version = "1", features = ["derive"], optional = true }
key-wallet = { workspace = true, optional = true }
Expand Down Expand Up @@ -154,6 +161,10 @@ apple-native-keyring-store = { version = "=1.0.0", features = ["keychain"], opti
windows-native-keyring-store = { version = "=1.0.0", optional = true }

[dev-dependencies]
# `test-utils` reaches `provider_key_test_wallet`, shared with
# `platform-wallet`'s own `rebuild_provider_key_account` tests — see its use
# in `sqlite/provider_accounts.rs`'s test module.
platform-wallet = { path = "../rs-platform-wallet", features = ["test-utils"] }
proptest = "1"
assert_cmd = "2"
static_assertions = "1"
Expand Down
131 changes: 5 additions & 126 deletions packages/rs-platform-wallet-storage/src/sqlite/provider_accounts.rs
Original file line number Diff line number Diff line change
@@ -1,54 +1,6 @@
//! Provider account and public-key pool reconstruction for SQLite load.
//! Platform-node public-key pool reconstruction for SQLite load.

use key_wallet::account::AccountType;
use platform_wallet::changeset::ProviderKeyExtendedPubKey;

/// Why a provider key-material account could not be rebuilt into an
/// [`AccountCollection`](key_wallet::account::account_collection::AccountCollection).
#[derive(Debug, thiserror::Error)]
pub(super) enum ProviderAccountRebuildError {
/// The curve-specific account constructor rejected the key.
#[error("provider key account is invalid")]
Invalid(#[from] key_wallet::error::Error),
/// The collection refused the account — its `account_type` does not match
/// the curve (e.g. a BLS key offered as `ProviderPlatformKeys`).
#[error("account collection rejected the provider key account: {0}")]
Rejected(&'static str),
}

/// Rebuild a watch-only provider account in its curve-specific collection slot.
pub(super) fn rebuild_provider_key_account(
accounts: &mut key_wallet::account::account_collection::AccountCollection,
wallet_id: [u8; 32],
network: key_wallet::Network,
account_type: AccountType,
extended_public_key: &ProviderKeyExtendedPubKey,
) -> Result<(), ProviderAccountRebuildError> {
match extended_public_key {
ProviderKeyExtendedPubKey::Bls(key) => {
let account = key_wallet::account::BLSAccount::new(
Some(wallet_id.to_vec()),
account_type,
key.clone(),
network,
)?;
accounts
.insert_bls_account(account)
.map_err(ProviderAccountRebuildError::Rejected)
}
ProviderKeyExtendedPubKey::EdDSA(key) => {
let account = key_wallet::account::EdDSAAccount::new(
Some(wallet_id.to_vec()),
account_type,
key.clone(),
network,
)?;
accounts
.insert_eddsa_account(account)
.map_err(ProviderAccountRebuildError::Rejected)
}
}
}

/// Errors while inserting a pre-derived platform-node key into its managed pool.
#[derive(Debug, thiserror::Error)]
Expand Down Expand Up @@ -149,85 +101,12 @@ pub(super) fn insert_platform_node_pool_entry(
mod tests {
use super::*;
use key_wallet::Network;
// Shared with `platform-wallet`'s own `rebuild_provider_key_account` tests
// via its `test-utils` feature (see this crate's `[dev-dependencies]`) —
// one fixture instead of two drifting copies.
use platform_wallet::changeset::provider_key_account::provider_key_test_wallet;
use platform_wallet::wallet::provider_key_at_index::derive_platform_node_public_keys;

fn provider_key_test_wallet() -> key_wallet::wallet::Wallet {
key_wallet::wallet::Wallet::from_seed_bytes(
[0x42; 64],
Network::Testnet,
key_wallet::wallet::initialization::WalletAccountCreationOptions::Default,
)
.expect("provider key test wallet")
}

#[test]
fn rebuild_provider_key_account_restores_bls_and_eddsa() {
let wallet = provider_key_test_wallet();
let bls_key = wallet
.accounts
.bls_account_of_type(AccountType::ProviderOperatorKeys)
.expect("BLS provider account")
.bls_public_key
.clone();
let eddsa_key = wallet
.accounts
.eddsa_account_of_type(AccountType::ProviderPlatformKeys)
.expect("EdDSA provider account")
.ed25519_public_key
.clone();
let mut accounts = key_wallet::account::account_collection::AccountCollection::new();
let wallet_id = [0x24; 32];

rebuild_provider_key_account(
&mut accounts,
wallet_id,
Network::Testnet,
AccountType::ProviderOperatorKeys,
&ProviderKeyExtendedPubKey::Bls(bls_key),
)
.expect("rebuild BLS provider account");
rebuild_provider_key_account(
&mut accounts,
wallet_id,
Network::Testnet,
AccountType::ProviderPlatformKeys,
&ProviderKeyExtendedPubKey::EdDSA(eddsa_key),
)
.expect("rebuild EdDSA provider account");

assert!(accounts
.bls_account_of_type(AccountType::ProviderOperatorKeys)
.is_some());
assert!(accounts
.eddsa_account_of_type(AccountType::ProviderPlatformKeys)
.is_some());
}

#[test]
fn rebuild_provider_key_account_rejects_curve_account_type_mismatch() {
let wallet = provider_key_test_wallet();
let bls_key = wallet
.accounts
.bls_account_of_type(AccountType::ProviderOperatorKeys)
.expect("BLS provider account")
.bls_public_key
.clone();
let mut accounts = key_wallet::account::account_collection::AccountCollection::new();

let error = rebuild_provider_key_account(
&mut accounts,
[0x24; 32],
Network::Testnet,
AccountType::ProviderPlatformKeys,
&ProviderKeyExtendedPubKey::Bls(bls_key),
)
.expect_err("BLS key must not rebuild as a platform-node account");

assert!(matches!(error, ProviderAccountRebuildError::Rejected(_)));
assert!(accounts
.eddsa_account_of_type(AccountType::ProviderPlatformKeys)
.is_none());
}
#[test]
fn insert_used_platform_node_pool_entry_restores_used_bookkeeping() {
use dashcore::hashes::Hash;
Expand Down
Loading
Loading