Skip to content

Commit 2b12cfb

Browse files
lklimekclaude
andcommitted
refactor(platform-wallet): share provider key account rebuild between storage and ffi
Provider key-material accounts (BLS ProviderOperatorKeys / EdDSA ProviderPlatformKeys) were rebuilt by two copies of the same logic: a pub(super) helper private to platform-wallet-storage's sqlite module and an inline copy in platform-wallet-ffi's build_wallet_start_state. The FFI crate does not depend on the storage crate, so the canonical helper now lives in platform-wallet (a dependency of both) as platform_wallet::changeset::provider_key_account::{ rebuild_provider_key_account, ProviderAccountRebuildError}, gated on the bls/eddsa features that make its variants exist. No behavior change: - storage keeps its Invalid -> AccountRecordInvalid and Rejected -> ProviderKeyAccountEntryMismatch mapping; - ffi keeps its bincode decode (and the unmaintained-bincode-decoder note), and maps helper errors to byte-identical Fatal PersistenceError::backend messages. Characterization tests pin both call sites before the move: build_wallet restoring/rejecting provider manifest entries (storage) and build_wallet_start_state restoring both provider accounts from bincode-encoded specs (ffi). The helper's own unit tests move with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 1363aa0 commit 2b12cfb

5 files changed

Lines changed: 349 additions & 147 deletions

File tree

‎packages/rs-platform-wallet-ffi/src/persistence.rs‎

Lines changed: 100 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
1212
use bincode::config;
1313
use key_wallet::account::account_collection::AccountCollection;
14-
use key_wallet::account::{Account, AccountType, BLSAccount, EdDSAAccount, StandardAccountType};
14+
use key_wallet::account::{Account, AccountType, StandardAccountType};
1515
use key_wallet::bip32::DerivationPath;
1616
use key_wallet::bip32::ExtendedPubKey;
1717
use key_wallet::derivation_bls_bip32::ExtendedBLSPubKey;
@@ -27,6 +27,9 @@ use parking_lot::Mutex;
2727
use std::str::FromStr;
2828

2929
use crate::types::{FFINetwork, Network};
30+
use platform_wallet::changeset::provider_key_account::{
31+
rebuild_provider_key_account, ProviderAccountRebuildError,
32+
};
3033
use platform_wallet::changeset::{
3134
AccountAddressPoolEntry, AccountRegistrationEntry, ClientStartState, ClientWalletStartState,
3235
ListedCoreTxid, PersistenceCapabilities, PersistenceError, PersistenceErrorKind,
@@ -4852,6 +4855,23 @@ impl Drop for LoadGuard {
48524855
}
48534856
}
48544857

4858+
/// Map a provider-account rebuild failure to a load error naming the
4859+
/// curve-specific constructor or `AccountCollection` insert that failed.
4860+
fn provider_rebuild_error(
4861+
constructor: &str,
4862+
insert: &str,
4863+
error: ProviderAccountRebuildError,
4864+
) -> PersistenceError {
4865+
match error {
4866+
ProviderAccountRebuildError::Invalid(e) => {
4867+
PersistenceError::backend(format!("{constructor} failed: {e:?}"))
4868+
}
4869+
ProviderAccountRebuildError::Rejected(e) => {
4870+
PersistenceError::backend(format!("AccountCollection::{insert} failed: {e}"))
4871+
}
4872+
}
4873+
}
4874+
48554875
/// Reconstruct an external-signable [`Wallet`] + matching start-state
48564876
/// bucket from a single `WalletRestoreEntryFFI`. The mnemonic / seed
48574877
/// stays in the host's keychain; signing requests route back through
@@ -4912,7 +4932,7 @@ fn build_wallet_start_state(
49124932
// platform node keys) live in dedicated `Option` fields on the
49134933
// collection and carry a non-secp256k1 extended public key in
49144934
// the same `account_xpub_bytes` slot. Rebuild them watch-only
4915-
// via the type-specific `new` + insert methods rather than the
4935+
// via the shared `rebuild_provider_key_account` rather than the
49164936
// ECDSA `Account::from_xpub` / `insert` path (which would fail
49174937
// to decode the bytes and reject the provider `AccountType`).
49184938
// Provider xpubs are stored raw (`bincode(xpub)`), exactly like the
@@ -4938,21 +4958,14 @@ fn build_wallet_start_state(
49384958
e
49394959
))
49404960
})?;
4941-
let bls_account = BLSAccount::new(
4942-
Some(entry.wallet_id.to_vec()),
4943-
account_type,
4944-
bls_pubkey,
4961+
rebuild_provider_key_account(
4962+
&mut accounts,
4963+
entry.wallet_id,
49454964
network,
4965+
account_type,
4966+
&ProviderKeyExtendedPubKey::Bls(bls_pubkey),
49464967
)
4947-
.map_err(|e| {
4948-
PersistenceError::backend(format!("BLSAccount::new failed: {:?}", e))
4949-
})?;
4950-
accounts.insert_bls_account(bls_account).map_err(|e| {
4951-
PersistenceError::backend(format!(
4952-
"AccountCollection::insert_bls_account failed: {}",
4953-
e
4954-
))
4955-
})?;
4968+
.map_err(|e| provider_rebuild_error("BLSAccount::new", "insert_bls_account", e))?;
49564969
continue;
49574970
}
49584971
AccountType::ProviderPlatformKeys => {
@@ -4963,20 +4976,15 @@ fn build_wallet_start_state(
49634976
e
49644977
))
49654978
})?;
4966-
let eddsa_account = EdDSAAccount::new(
4967-
Some(entry.wallet_id.to_vec()),
4968-
account_type,
4969-
ed_pubkey,
4979+
rebuild_provider_key_account(
4980+
&mut accounts,
4981+
entry.wallet_id,
49704982
network,
4983+
account_type,
4984+
&ProviderKeyExtendedPubKey::EdDSA(ed_pubkey),
49714985
)
49724986
.map_err(|e| {
4973-
PersistenceError::backend(format!("EdDSAAccount::new failed: {:?}", e))
4974-
})?;
4975-
accounts.insert_eddsa_account(eddsa_account).map_err(|e| {
4976-
PersistenceError::backend(format!(
4977-
"AccountCollection::insert_eddsa_account failed: {}",
4978-
e
4979-
))
4987+
provider_rebuild_error("EdDSAAccount::new", "insert_eddsa_account", e)
49804988
})?;
49814989
// The platform-node (Ed25519) pool is rehydrated from the
49824990
// persisted core-address rows like every other pool — see
@@ -8767,6 +8775,72 @@ mod tests {
87678775
);
87688776
}
87698777

8778+
/// `build_wallet_start_state` rebuilds the BLS operator-key and EdDSA
8779+
/// platform-node-key accounts watch-only from their bincode-encoded specs.
8780+
#[test]
8781+
fn provider_key_accounts_survive_restore_round_trip() {
8782+
let wallet = Wallet::from_seed_bytes(
8783+
[0x42; 64],
8784+
Network::Testnet,
8785+
key_wallet::wallet::initialization::WalletAccountCreationOptions::Default,
8786+
)
8787+
.expect("seeded wallet");
8788+
let bls = wallet
8789+
.accounts
8790+
.bls_account_of_type(AccountType::ProviderOperatorKeys)
8791+
.expect("a Default-created wallet has a BLS provider account")
8792+
.bls_public_key
8793+
.clone();
8794+
let eddsa = wallet
8795+
.accounts
8796+
.eddsa_account_of_type(AccountType::ProviderPlatformKeys)
8797+
.expect("a Default-created wallet has an EdDSA provider account")
8798+
.ed25519_public_key
8799+
.clone();
8800+
let bls_bytes = bincode::encode_to_vec(&bls, config::standard()).expect("encode BLS xpub");
8801+
let eddsa_bytes =
8802+
bincode::encode_to_vec(&eddsa, config::standard()).expect("encode EdDSA xpub");
8803+
let specs = [
8804+
build_account_spec_ffi(&AccountType::ProviderOperatorKeys, &bls_bytes),
8805+
build_account_spec_ffi(&AccountType::ProviderPlatformKeys, &eddsa_bytes),
8806+
];
8807+
let entry = WalletRestoreEntryFFI {
8808+
wallet_id: wallet.wallet_id,
8809+
accounts: specs.as_ptr(),
8810+
accounts_count: specs.len(),
8811+
..Default::default()
8812+
};
8813+
8814+
let (state, _) =
8815+
build_wallet_start_state(&entry).expect("provider key accounts must restore");
8816+
8817+
let restored_bls = state
8818+
.wallet
8819+
.accounts
8820+
.bls_account_of_type(AccountType::ProviderOperatorKeys)
8821+
.expect("BLS provider account must be rebuilt");
8822+
let restored_bls_bytes =
8823+
bincode::encode_to_vec(&restored_bls.bls_public_key, config::standard())
8824+
.expect("encode restored BLS xpub");
8825+
assert_eq!(restored_bls_bytes, bls_bytes);
8826+
assert_eq!(
8827+
restored_bls.parent_wallet_id.as_deref(),
8828+
Some(&wallet.wallet_id[..])
8829+
);
8830+
assert!(restored_bls.is_watch_only);
8831+
let restored_eddsa = state
8832+
.wallet
8833+
.accounts
8834+
.eddsa_account_of_type(AccountType::ProviderPlatformKeys)
8835+
.expect("EdDSA provider account must be rebuilt");
8836+
assert_eq!(restored_eddsa.ed25519_public_key, eddsa);
8837+
assert_eq!(
8838+
restored_eddsa.parent_wallet_id.as_deref(),
8839+
Some(&wallet.wallet_id[..])
8840+
);
8841+
assert!(restored_eddsa.is_watch_only);
8842+
}
8843+
87708844
/// Helper: a minimum valid consensus-encodable transaction —
87718845
/// version 1, one synthetic input, one zero-value output. The
87728846
/// restoration helper only cares that the bytes round-trip

‎packages/rs-platform-wallet-storage/src/sqlite/provider_accounts.rs‎

Lines changed: 1 addition & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,6 @@
1-
//! Provider account and public-key pool reconstruction for SQLite load.
1+
//! Platform-node public-key pool reconstruction for SQLite load.
22
33
use key_wallet::account::AccountType;
4-
use platform_wallet::changeset::ProviderKeyExtendedPubKey;
5-
6-
/// Why a provider key-material account could not be rebuilt into an
7-
/// [`AccountCollection`](key_wallet::account::account_collection::AccountCollection).
8-
#[derive(Debug, thiserror::Error)]
9-
pub(super) enum ProviderAccountRebuildError {
10-
/// The curve-specific account constructor rejected the key.
11-
#[error("provider key account is invalid")]
12-
Invalid(#[from] key_wallet::error::Error),
13-
/// The collection refused the account — its `account_type` does not match
14-
/// the curve (e.g. a BLS key offered as `ProviderPlatformKeys`).
15-
#[error("account collection rejected the provider key account: {0}")]
16-
Rejected(&'static str),
17-
}
18-
19-
/// Rebuild a watch-only provider account in its curve-specific collection slot.
20-
pub(super) fn rebuild_provider_key_account(
21-
accounts: &mut key_wallet::account::account_collection::AccountCollection,
22-
wallet_id: [u8; 32],
23-
network: key_wallet::Network,
24-
account_type: AccountType,
25-
extended_public_key: &ProviderKeyExtendedPubKey,
26-
) -> Result<(), ProviderAccountRebuildError> {
27-
match extended_public_key {
28-
ProviderKeyExtendedPubKey::Bls(key) => {
29-
let account = key_wallet::account::BLSAccount::new(
30-
Some(wallet_id.to_vec()),
31-
account_type,
32-
key.clone(),
33-
network,
34-
)?;
35-
accounts
36-
.insert_bls_account(account)
37-
.map_err(ProviderAccountRebuildError::Rejected)
38-
}
39-
ProviderKeyExtendedPubKey::EdDSA(key) => {
40-
let account = key_wallet::account::EdDSAAccount::new(
41-
Some(wallet_id.to_vec()),
42-
account_type,
43-
key.clone(),
44-
network,
45-
)?;
46-
accounts
47-
.insert_eddsa_account(account)
48-
.map_err(ProviderAccountRebuildError::Rejected)
49-
}
50-
}
51-
}
524

535
/// Errors while inserting a pre-derived platform-node key into its managed pool.
546
#[derive(Debug, thiserror::Error)]
@@ -160,74 +112,6 @@ mod tests {
160112
.expect("provider key test wallet")
161113
}
162114

163-
#[test]
164-
fn rebuild_provider_key_account_restores_bls_and_eddsa() {
165-
let wallet = provider_key_test_wallet();
166-
let bls_key = wallet
167-
.accounts
168-
.bls_account_of_type(AccountType::ProviderOperatorKeys)
169-
.expect("BLS provider account")
170-
.bls_public_key
171-
.clone();
172-
let eddsa_key = wallet
173-
.accounts
174-
.eddsa_account_of_type(AccountType::ProviderPlatformKeys)
175-
.expect("EdDSA provider account")
176-
.ed25519_public_key
177-
.clone();
178-
let mut accounts = key_wallet::account::account_collection::AccountCollection::new();
179-
let wallet_id = [0x24; 32];
180-
181-
rebuild_provider_key_account(
182-
&mut accounts,
183-
wallet_id,
184-
Network::Testnet,
185-
AccountType::ProviderOperatorKeys,
186-
&ProviderKeyExtendedPubKey::Bls(bls_key),
187-
)
188-
.expect("rebuild BLS provider account");
189-
rebuild_provider_key_account(
190-
&mut accounts,
191-
wallet_id,
192-
Network::Testnet,
193-
AccountType::ProviderPlatformKeys,
194-
&ProviderKeyExtendedPubKey::EdDSA(eddsa_key),
195-
)
196-
.expect("rebuild EdDSA provider account");
197-
198-
assert!(accounts
199-
.bls_account_of_type(AccountType::ProviderOperatorKeys)
200-
.is_some());
201-
assert!(accounts
202-
.eddsa_account_of_type(AccountType::ProviderPlatformKeys)
203-
.is_some());
204-
}
205-
206-
#[test]
207-
fn rebuild_provider_key_account_rejects_curve_account_type_mismatch() {
208-
let wallet = provider_key_test_wallet();
209-
let bls_key = wallet
210-
.accounts
211-
.bls_account_of_type(AccountType::ProviderOperatorKeys)
212-
.expect("BLS provider account")
213-
.bls_public_key
214-
.clone();
215-
let mut accounts = key_wallet::account::account_collection::AccountCollection::new();
216-
217-
let error = rebuild_provider_key_account(
218-
&mut accounts,
219-
[0x24; 32],
220-
Network::Testnet,
221-
AccountType::ProviderPlatformKeys,
222-
&ProviderKeyExtendedPubKey::Bls(bls_key),
223-
)
224-
.expect_err("BLS key must not rebuild as a platform-node account");
225-
226-
assert!(matches!(error, ProviderAccountRebuildError::Rejected(_)));
227-
assert!(accounts
228-
.eddsa_account_of_type(AccountType::ProviderPlatformKeys)
229-
.is_none());
230-
}
231115
#[test]
232116
fn insert_used_platform_node_pool_entry_restores_used_bookkeeping() {
233117
use dashcore::hashes::Hash;

0 commit comments

Comments
 (0)