Skip to content

feat(platform-wallet): classic Dash message signing (signMessage) over FFI, JNI, Kotlin, and Swift - #4319

Merged
QuantumExplorer merged 3 commits into
v4.2-devfrom
feat/kotlin-sdk-sign-message-v2
Aug 6, 2026
Merged

feat(platform-wallet): classic Dash message signing (signMessage) over FFI, JNI, Kotlin, and Swift#4319
QuantumExplorer merged 3 commits into
v4.2-devfrom
feat/kotlin-sdk-sign-message-v2

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Aug 6, 2026

Copy link
Copy Markdown
Member

Supersedes #4259, which lives on an external fork and had merge conflicts with
v4.2-dev. This branch remakes it in-repo as two commits:

  1. Squash of feat(platform-wallet): classic Dash message signing (signMessage) over FFI, JNI, Kotlin, and Swift #4259 (authored by @HashEngineering, head 09a27ac60d),
    rebased onto v4.2-dev.
  2. Post-review fixups on top (details below).

All review findings on #4259 were addressed there before its final approval —
the digest-capability guard, FFI lock lifetime, null-pointer empty-message ABI
test, UTF-8 length-prefix docs, unpaired-surrogate rejection, the
MessageSigningMessageInvalidErrorInvalidParameter mapping, and the
MessageSigningFailed doc correction. thepastaclaw approved at 09a27ac60d.

What this adds

CoreWallet::sign_message — classic Dash signed messages ("proof you own this
address"), exposed through the full binding chain. Given a P2PKH address the
wallet holds keys for and an arbitrary UTF-8 message, it returns the 65-byte
recoverable signature, base64-encoded — the same wire format as dashj's
ECKey.signMessage and Dash Core's signmessage RPC, so existing verifiers
(verifymessage, dashj signedMessageToKey) accept it as-is.

Primary consumer: the Android/iOS wallets' CrowdNode integrations, where API
withdrawal and email registration are signed-message proofs of address
ownership (currently done in dashj on Android; this makes the kotlin-sdk /
swift-sdk route possible).

API surface

Layer Signature
Rust CoreWallet::sign_message(&self, address, message, signer) -> Result<String>
C FFI core_wallet_sign_message(handle, address_ptr/len, message_ptr/len, core_signer_handle, out_signature)
Kotlin ManagedPlatformWallet.signMessage(address: String, message: String, coreSignerHandle: Long): String (suspend)
Swift ManagedCoreWallet.signMessage(address: String, message: String) throws -> String

(Swift takes no signer parameter deliberately — the swift-sdk convention is a
per-call internal MnemonicResolver, as at every other seed-backed call site.)

Design decisions

  • Recovery id by trial, not recoverable signing. The digest and
    serialization come from dashcore::sign_message; the recovery id is found
    by trying the four candidates against the signer's public key — the same
    approach dashj itself uses (ECKey.findRecoveryId). Every Signer backend
    gets signed-message support without needing a recoverable-signing method.
  • Digest capability honored. The signer must advertise
    SignerMethod::Digest before sign_ecdsa is called; a refusal maps to the
    typed MessageSigningFailed.
  • Key material never crosses the FFI. core_wallet_sign_message takes the
    caller's existing MnemonicResolverHandle, exactly like the send paths.
  • Signable accounts only. Watch-only DashPay external accounts are refused
    with a typed error; BIP44/BIP32/CoinJoin and DashPay receiving accounts sign.
  • Typed errors. Unknown/watch-only address → FFI code 31
    (ErrorSigningKeyUnavailable); malformed address or non-UTF-8 message bytes
    ErrorInvalidParameter; mirrored in Kotlin
    (DashSdkError.PlatformWallet.SigningKeyUnavailable) and Swift
    (signingKeyUnavailable).
  • No FFI lock held across the host callback. The entry point clones the
    Arc-backed CoreWallet out of HandleStorage and runs block_on outside
    the guard, matching the broadcast paths.

Deltas vs #4259 (the fixup commit + rebase resolution)

Verification

  • Byte-for-byte dashj parity goldens (RFC6979 golden + dashj's own
    signed-message test vector), unchanged across the port.
  • cargo test -p platform-wallet -p platform-wallet-ffi green on the merged
    tree.
  • Kotlin :sdk:compileDebugKotlin clean.
  • cargo fmt --all produced no diff.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added classic Dash message signing for wallet-owned P2PKH addresses in the Kotlin and Swift SDKs.
    • Signatures are returned in base64 format, including support for empty messages and embedded content.
    • Added validation for invalid, foreign, watch-only, and wrong-network addresses.
  • Bug Fixes

    • Improved error reporting for invalid messages, unavailable signing keys, and signing failures.
    • Clarified that missing keys should be repaired or the address corrected rather than retried.

HashEngineering and others added 2 commits August 6, 2026 12:24
…r FFI, JNI, Kotlin, and Swift

Squash of PR #4259 (feat/kotlin-sdk-sign-message, head 09a27ac),
rebased onto v4.2-dev.

CoreWallet::sign_message — classic Dash signed messages, exposed through
the full binding chain (Rust -> C FFI -> Kotlin/JNI and Swift). Given a
P2PKH address the wallet holds keys for and an arbitrary UTF-8 message,
returns the 65-byte recoverable signature base64-encoded — the same wire
format as dashj's ECKey.signMessage and Dash Core's signmessage RPC.
Includes digest-capability guard, typed errors (FFI code 31
ErrorSigningKeyUnavailable), null-pointer empty-message ABI contract,
unpaired-surrogate rejection in Kotlin, and dashj byte-for-byte parity
goldens.

Rebase resolution: the TxMetadataPayloadTooLarge FFI mapping arm was
dropped — the variant it matched was removed from v4.2-dev by the
encrypted-txMetadata revert (#4279), so the arm no longer compiles and
the error cannot be produced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Kotlin-sdk porting guideline: when porting behavior from iOS, cite the
Swift source file in the KDoc, as the neighbouring methods do
(sendToAddresses -> SendViewModel.swift, tokens -> TokenActions.swift).
Names ManagedCoreWallet.signMessage(address:message:) and explains the
one signature divergence (Swift builds a per-call MnemonicResolver, so
it takes no signer parameter). Review follow-up from the original PR
(#4259).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added this to the v4.2.0 milestone Aug 6, 2026
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@QuantumExplorer, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 30 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bc46e61e-b42d-45c8-ad56-358cc0d7dcad

📥 Commits

Reviewing files that changed from the base of the PR and between 171c665 and 2eb6dd2.

📒 Files selected for processing (2)
  • packages/rs-platform-wallet-ffi/src/core_wallet/sign_message.rs
  • packages/rs-unified-sdk-jni/src/wallet_manager.rs
📝 Walkthrough

Walkthrough

The PR adds classic Dash message signing to CoreWallet and exposes it through Rust FFI, JNI, Kotlin, and Swift APIs. It validates addresses and message encoding, returns base64 recoverable signatures, maps signing errors, and adds compatibility and regression tests.

Changes

Classic Dash message signing

Layer / File(s) Summary
Wallet signing implementation
packages/rs-platform-wallet/Cargo.toml, packages/rs-platform-wallet/src/error.rs, packages/rs-platform-wallet/src/test_support.rs, packages/rs-platform-wallet/src/wallet/core/*
CoreWallet.sign_message validates network, P2PKH format, ownership, signer capability, public-key recovery, and signature encoding. Typed errors, deterministic fixtures, compatibility tests, and regression tests cover the signing paths.
FFI signing and error bridge
packages/rs-platform-wallet-ffi/src/core_wallet/*, packages/rs-platform-wallet-ffi/src/error.rs
The FFI binding accepts UTF-8 address and message buffers, supports empty messages, invokes resolver-backed signing, returns an owned base64 string, and maps message-signing failures to SDK error codes.
JNI, Kotlin, and Swift SDK APIs
packages/rs-unified-sdk-jni/src/wallet_manager.rs, packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/{ffi,wallet,errors}/*, packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/*
JNI, Kotlin, and Swift expose message signing. The bindings validate inputs, preserve message bytes, release native memory, map errors, and document signing-key-unavailable cases.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SDK
  participant JNI
  participant FFI
  participant CoreWallet
  participant Signer
  SDK->>JNI: request message signature
  JNI->>FFI: pass address, message bytes, and signer handle
  FFI->>CoreWallet: invoke sign_message
  CoreWallet->>Signer: sign Dash message digest
  Signer-->>CoreWallet: return signature data
  CoreWallet-->>FFI: return base64 recoverable signature
  FFI-->>JNI: return owned native string
  JNI-->>SDK: return signature or mapped error
Loading

Possibly related PRs

  • dashpay/platform#4191: Defines the SigningKeyUnavailable marker and error mapping used by this message-signing flow.
  • dashpay/platform#4204: Also updates SigningKeyUnavailable documentation and mapping.
  • dashpay/platform#4259: Covers the same classic Dash message-signing implementation across the Rust and SDK layers.

Suggested reviewers: lklimek, llbartekll, zocolini

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: classic Dash message signing across the platform-wallet FFI, JNI, Kotlin, and Swift APIs.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/kotlin-sdk-sign-message-v2

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

@thepastaclaw

thepastaclaw commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

🔍 Review in progress — actively reviewing now (commit 2eb6dd2)
Stage: Codex precheck starting
ETA: complete ~06:44 UTC (median 19m across 30 recent reviews)
Running 4m · Last checked: 2026-08-06 06:30 UTC

@codecov

codecov Bot commented Aug 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.61%. Comparing base (13c3a23) to head (2eb6dd2).

Additional details and impacted files
@@            Coverage Diff            @@
##           v4.2-dev    #4319   +/-   ##
=========================================
  Coverage     87.61%   87.61%           
=========================================
  Files          2704     2704           
  Lines        345206   345206           
=========================================
  Hits         302446   302446           
  Misses        42760    42760           
Components Coverage Δ
dpp 88.83% <ø> (ø)
drive 86.25% <ø> (ø)
drive-abci 89.66% <ø> (ø)
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.88% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 48.02% <ø> (ø)
🚀 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/rs-platform-wallet-ffi/src/core_wallet/sign_message.rs (1)

164-166: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Run message signing through block_on_worker. Construct MnemonicResolverCoreSigner inside its async move closure so the signer remains alive for the synchronous call and the future satisfies Send + 'static. This uses the configured worker stack. block_on_worker still uses runtime().block_on internally, so it does not make calls from an existing Tokio runtime safe.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rs-platform-wallet-ffi/src/core_wallet/sign_message.rs` around lines
164 - 166, Update the message-signing flow around wallet.sign_message to call it
through block_on_worker instead of runtime().block_on. Construct
MnemonicResolverCoreSigner inside the async move closure, keeping the signer
alive for the synchronous call and ensuring the future is Send + 'static while
using the configured worker stack.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/rs-unified-sdk-jni/src/wallet_manager.rs`:
- Around line 1093-1095: Update the `env.new_string(signature)` handling in the
signing function to throw a `DashSdkException` through the JNI environment when
string creation fails before returning the null pointer. Preserve the existing
successful `into_raw()` path and ensure the failure path matches the other
exception-first returns and the documented null-after-throw contract.

---

Nitpick comments:
In `@packages/rs-platform-wallet-ffi/src/core_wallet/sign_message.rs`:
- Around line 164-166: Update the message-signing flow around
wallet.sign_message to call it through block_on_worker instead of
runtime().block_on. Construct MnemonicResolverCoreSigner inside the async move
closure, keeping the signer alive for the synchronous call and ensuring the
future is Send + 'static while using the configured worker stack.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6f8c54e0-e7ce-4ff5-82e3-0af557d79db8

📥 Commits

Reviewing files that changed from the base of the PR and between 13c3a23 and 171c665.

📒 Files selected for processing (15)
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/errors/DashSdkError.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/WalletManagerNative.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedCoreWallet.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedPlatformWallet.kt
  • packages/rs-platform-wallet-ffi/src/core_wallet/mod.rs
  • packages/rs-platform-wallet-ffi/src/core_wallet/sign_message.rs
  • packages/rs-platform-wallet-ffi/src/error.rs
  • packages/rs-platform-wallet/Cargo.toml
  • packages/rs-platform-wallet/src/error.rs
  • packages/rs-platform-wallet/src/test_support.rs
  • packages/rs-platform-wallet/src/wallet/core/mod.rs
  • packages/rs-platform-wallet/src/wallet/core/sign_message.rs
  • packages/rs-unified-sdk-jni/src/wallet_manager.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/ManagedCoreWallet.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletResult.swift

Comment thread packages/rs-unified-sdk-jni/src/wallet_manager.rs Outdated
…JNI string-allocation failure

Two review follow-ups from the first round on this PR:

- core_wallet_sign_message now polls through block_on_worker instead of
  runtime().block_on, so the signing round-trip (which includes proof-free
  but recursion-happy key derivation in the resolver path) runs on a
  runtime worker with the 8 MB stack rather than the caller's thread —
  iOS dispatch threads default to ~512 KB. The
  MnemonicResolverCoreSigner is constructed inside the async move block,
  so the future is Send + 'static without borrowing the caller's frame;
  the calling thread still blocks until completion, so the resolver
  handle stays pinned for the signer's whole lifetime.

- The JNI coreWalletSignMessage now throws DashSdkException when
  env.new_string fails instead of silently returning null. Kotlin
  declares a non-null return, so the bare null surfaced as an
  unexplained NullPointerException at the platform-type boundary,
  breaking the KDoc's "null only after throwing" contract every other
  failure path in the function honors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@QuantumExplorer
QuantumExplorer merged commit 4f293c4 into v4.2-dev Aug 6, 2026
18 checks passed
@QuantumExplorer
QuantumExplorer deleted the feat/kotlin-sdk-sign-message-v2 branch August 6, 2026 06:31
QuantumExplorer added a commit that referenced this pull request Aug 6, 2026
Union-resolves module/test-helper both-sides-adds with #4319
(signMessage); adapts its merged-in test fixtures to this branch's
PlatformWalletInfo.generation (Arc<WalletGeneration> replaced the bare
balance Arc as the per-generation identity).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

3 participants