Skip to content

feat: external signable wallets#3639

Open
ZocoLini wants to merge 1 commit into
v3.1-devfrom
feat/external-signable-wallets
Open

feat: external signable wallets#3639
ZocoLini wants to merge 1 commit into
v3.1-devfrom
feat/external-signable-wallets

Conversation

@ZocoLini
Copy link
Copy Markdown
Collaborator

@ZocoLini ZocoLini commented May 13, 2026

After Wallet::from_mnemonic and Wallet::from_seed I do a wallet downgrade into ExternableSignable, forcing every transaction to be signed externally

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 13, 2026

Warning

Rate limit exceeded

@ZocoLini has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 55 minutes and 49 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 055bc94d-247f-4b23-8d8c-5bbb349b0fb7

📥 Commits

Reviewing files that changed from the base of the PR and between 6a3b904 and e3473a5.

📒 Files selected for processing (1)
  • packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/external-signable-wallets

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added this to the v3.1.0 milestone May 13, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 13, 2026

📖 Book Preview built successfully.

Download the preview from the workflow artifacts.
To view locally: download the artifact, unzip, and open index.html.

Updated at 2026-05-13T17:57:17.078Z

@ZocoLini ZocoLini force-pushed the feat/external-signable-wallets branch from b9ad462 to dc7dafb Compare May 13, 2026 17:56
After the colleague's MnemonicResolverCoreSigner landed every signing
path is signer-driven, so freshly-derived wallets no longer need to
keep the root xpriv in-process. `create_wallet_from_mnemonic` and
`create_wallet_from_seed_bytes` now derive accounts via
`Wallet::from_mnemonic`/`from_seed_bytes` and immediately downgrade
to an ExternalSignable wallet with the same wallet_id + account xpubs.
The seed stays in the host's secure storage and the resolver vtable
remains the only way to sign.
@ZocoLini ZocoLini force-pushed the feat/external-signable-wallets branch from dc7dafb to e3473a5 Compare May 19, 2026 17:30
@ZocoLini ZocoLini changed the title feat: external signable wallets and tx building with signer feat: external signable wallets May 19, 2026
@ZocoLini ZocoLini marked this pull request as ready for review May 19, 2026 19:24
@ZocoLini ZocoLini requested a review from QuantumExplorer as a code owner May 19, 2026 19:24
@codecov
Copy link
Copy Markdown

codecov Bot commented May 19, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.84%. Comparing base (6a3b904) to head (e3473a5).

Additional details and impacted files
@@             Coverage Diff              @@
##           v3.1-dev    #3639      +/-   ##
============================================
- Coverage     88.01%   87.84%   -0.18%     
============================================
  Files          2521     2537      +16     
  Lines        309007   311315    +2308     
============================================
+ Hits         271977   273471    +1494     
- Misses        37030    37844     +814     
Components Coverage Δ
dpp 87.83% <ø> (ø)
drive 87.03% <ø> (ø)
drive-abci 90.05% <ø> (ø)
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.17% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 53.13% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Copy Markdown
Contributor

✅ DashSDKFFI.xcframework built for this PR.

SwiftPM (host the zip at a stable URL, then use):

.binaryTarget(
  name: "DashSDKFFI",
  url: "https://your.cdn.example/DashSDKFFI.xcframework.zip",
  checksum: "e5bb55681f2182394317fd310371cc905ceb33ed9756aa2edf42cebaabf40b28"
)

Xcode manual integration:

  • Download 'DashSDKFFI.xcframework' artifact from the run link above.
  • Drag it into your app target (Frameworks, Libraries & Embedded Content) and set Embed & Sign.
  • If using the Swift wrapper package, point its binaryTarget to the xcframework location or add the package and place the xcframework at the expected path.

Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

One-file change that ensures create_wallet_from_mnemonic / create_wallet_from_seed_bytes always yield an ExternalSignable wallet. The type-level invariant is correctly enforced, but the helper's documented security guarantee ("strip the in-process xpriv") is not actually delivered: drop(temp) runs no zeroization because key_wallet::Wallet has only a manual Zeroize impl, no Drop/ZeroizeOnDrop. The exact same downgrade path upstream in key-wallet-manager calls wallet.zeroize() before drop. Two secondary issues: the cloned per-account is_watch_only flag diverges from how the restore path reconstructs the same wallet, and the new invariant has no test coverage.

🟡 3 suggestion(s)

Comment on lines +387 to +392
fn downgrade_to_external_signable(temp: Wallet) -> Wallet {
let wallet_id = temp.wallet_id;
let network = temp.network;
let accounts = temp.accounts.clone();
drop(temp);
Wallet::new_external_signable(network, wallet_id, accounts)
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.

🟡 Suggestion: drop(temp) does not zeroize the xpriv the doc-comment promises to strip

The doc-comment states this helper exists to "strip the in-process xpriv from a freshly-derived wallet," but drop(temp) runs only the default Drop. key_wallet::Wallet defines a manual impl Zeroize (key-wallet/src/wallet/mod.rs:153) and has no Drop or ZeroizeOnDrop impl. As a result, the Mnemonic chain-code material, BIP-39 Seed, and RootExtendedPrivKey bytes are simply deallocated — they linger in freed heap until reused. The PR's stated motivation ("force every transaction to be signed externally", seed kept only in host secure storage) is satisfied only at the type level; the in-process secret residue is identical to before. The upstream key-wallet-manager performs this same downgrade and explicitly calls wallet.zeroize(); drop(wallet); (key-wallet-manager/src/lib.rs:237-238) — mirror that here. secp256k1::SecretKey still can't be wiped from Rust, but the mnemonic and chain-code material are scrubbed by Wallet::zeroize, which is strictly better than the current no-op.

Suggested change
fn downgrade_to_external_signable(temp: Wallet) -> Wallet {
let wallet_id = temp.wallet_id;
let network = temp.network;
let accounts = temp.accounts.clone();
drop(temp);
Wallet::new_external_signable(network, wallet_id, accounts)
fn downgrade_to_external_signable(mut temp: Wallet) -> Wallet {
use zeroize::Zeroize;
let wallet_id = temp.wallet_id;
let network = temp.network;
let accounts = temp.accounts.clone();
// `Wallet` implements `Zeroize` but not `ZeroizeOnDrop`, so `drop`
// alone leaves the mnemonic/seed/chain-code material in freed heap.
// Mirror the upstream `key-wallet-manager` downgrade path.
temp.zeroize();
drop(temp);
Wallet::new_external_signable(network, wallet_id, accounts)
}

source: ['claude', 'codex']

Comment on lines +387 to +392
fn downgrade_to_external_signable(temp: Wallet) -> Wallet {
let wallet_id = temp.wallet_id;
let network = temp.network;
let accounts = temp.accounts.clone();
drop(temp);
Wallet::new_external_signable(network, wallet_id, accounts)
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.

🟡 Suggestion: Cloned accounts keep is_watch_only=false while the wallet becomes ExternalSignable

Wallet::from_mnemonic / from_seed_bytes populate accounts via Account::from_xpriv, which sets is_watch_only: false (key-wallet/src/account/mod.rs:97). This helper clones that AccountCollection verbatim and rebuilds the wallet as WalletType::ExternalSignable, so the result has wallet_type == ExternalSignable but every Account inside still reports is_watch_only == false. The mirror code path on restart — rs-platform-wallet-ffi/src/persistence.rs:1775,1788 — reconstructs the logically-equivalent wallet via Account::from_xpub + new_external_signable, which yields is_watch_only == true. A wallet created on first launch and the same wallet restored on the next launch therefore have different per-account flags. key_wallet branches on account.is_watch_only in places (e.g. Account::derive_xpriv_from_master_xpriv returns Error::WatchOnly when set), so the create vs restore divergence is fragile. Map the cloned accounts through Account::to_watch_only() (key-wallet/src/account/mod.rs:133) before passing them to new_external_signable so the two paths converge.

source: ['claude']

Comment on lines 82 to 94
seed_bytes: [u8; 64],
accounts: WalletAccountCreationOptions,
) -> Result<Arc<PlatformWallet>, PlatformWalletError> {
let wallet = Wallet::from_seed_bytes(seed_bytes, network, accounts).map_err(|e| {
let temp = Wallet::from_seed_bytes(seed_bytes, network, accounts).map_err(|e| {
PlatformWalletError::WalletCreation(format!(
"Failed to create wallet from seed bytes: {}",
"Failed to derive accounts from seed bytes: {}",
e
))
})?;
let wallet = downgrade_to_external_signable(temp);
self.register_wallet(wallet).await
}

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.

🟡 Suggestion: New ExternalSignable invariant has no test

The PR's behavioral guarantee — that create_wallet_from_mnemonic and create_wallet_from_seed_bytes always return an ExternalSignable wallet — is not asserted by any test in the crate. The only existing call site (tests/spv_sync.rs:185) exercises create_wallet_from_seed_bytes but never inspects wallet_type. Without a regression test, a future edit to the helper or to register_wallet can silently restore in-process signing capability. A small unit test asserting matches!(wallet.wallet_type, WalletType::ExternalSignable) on the wallet returned from each manager constructor would lock the invariant in cheaply.

source: ['claude']

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants