Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
74db499
fix(kotlin-sdk): unmanaged-identity local reads return absence, not N…
bfoss765 Jul 21, 2026
4ecbdcb
feat(kotlin-sdk): typed SigningKeyUnavailable for missing signing keys
bfoss765 Jul 21, 2026
f122d11
feat(kotlin-sdk): split identity-key Keystore aliases by security policy
bfoss765 Jul 21, 2026
0c6306c
feat(kotlin-sdk): degrade lockless AUTH_GATED writes to the device-bo…
bfoss765 Jul 21, 2026
3cbd428
feat(kotlin-sdk): layered identity-key retrieve ladder with cheap/pro…
bfoss765 Jul 21, 2026
8be0599
feat(kotlin-sdk): queryable pendingIdentityKeys state for watch-only …
bfoss765 Jul 21, 2026
eddb25c
feat(kotlin-sdk): make the pending-repair signal durable across resta…
bfoss765 Jul 21, 2026
9659f5f
feat(kotlin-sdk): force-replace and verify in identity-key repair
bfoss765 Jul 21, 2026
466e211
feat(kotlin-sdk)!: map platform-wallet code 98 to typed PlatformWalle…
bfoss765 Jul 21, 2026
22fd3e5
feat(sdk-ffi): structured SigningKeyUnavailable discriminator across …
bfoss765 Jul 21, 2026
1db39d1
test(kotlin-sdk): adapt instrumented Keystore tests to the policy-ali…
bfoss765 Jul 21, 2026
a7d564d
docs(sdk): record Keystore-rework parity divergences, code-31 capabil…
bfoss765 Jul 21, 2026
b503531
fix(kotlin-sdk): disprove replaced-alias ownership before counting a …
bfoss765 Jul 21, 2026
58fd885
fix(kotlin-sdk): surface KeyPermanentlyInvalidatedException as the ty…
bfoss765 Jul 21, 2026
8504f22
fix(kotlin-sdk): make a sign-time key invalidation durably repairable…
bfoss765 Jul 21, 2026
a8554a3
docs(kotlin-sdk): correct the marker-fallback rationale; note the run…
bfoss765 Jul 21, 2026
61dcfcd
fix(kotlin-sdk): rethrow KeyPermanentlyInvalidatedException from the …
bfoss765 Jul 21, 2026
2ddc838
fix(kotlin-sdk): derive identity-key repair from persisted breadcrumb…
bfoss765 Jul 22, 2026
2a0f54d
fix(kotlin-sdk): make the durable identity-key repair write fail closed
bfoss765 Jul 22, 2026
033df44
fix(kotlin-sdk): narrow the KeyMint lockless-keygen classifier to aut…
bfoss765 Jul 22, 2026
0621b90
fix(kotlin-sdk): rethrow CancellationException from the best-effort p…
bfoss765 Jul 22, 2026
48c4726
docs(kotlin-sdk): align DEVICE_BOUND KDoc with the software-keystore …
bfoss765 Jul 22, 2026
9bf31e7
fix(kotlin-sdk): make identity-key repair ownership check HASH160-awa…
bfoss765 Jul 23, 2026
60a84d3
fix(kotlin-sdk): stop treating Keystore error code 4 as no-lock-scree…
bfoss765 Jul 23, 2026
d3ba9b6
fix(kotlin-sdk): don't fail open on invalidation bookkeeping / load c…
bfoss765 Jul 23, 2026
e9b7681
fix(example): probe off-Main and label key-health by recoverability (…
bfoss765 Jul 23, 2026
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
20 changes: 20 additions & 0 deletions docs/dashpay/KOTLIN_MIGRATION_LEFTOVERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,26 @@ when the four stacked PRs collapse into one.
names its concrete remaining FFI, persistence, UI/catalog-adaptation, device,
or restart gate.

- **`SigningKeyUnavailable` MESSAGE_MARKER fallback removal.** The signer's
"missing key" failure now travels as a typed completion code (rs-sdk-ffi
`DashSDKSignerErrorCode::SigningKeyUnavailable` → platform-wallet code 31 →
`DashSdkError.PlatformWallet.SigningKeyUnavailable`). The message-marker
sniff on the catch-all codes is retained ONLY for the #4191 merge-order
transition and for conversion paths that lose the machine prefix — NOT for
mixed old-native/new-Kotlin builds, which the completion JNI arity change
(3→4 args) makes unsupported outright; delete it (and `MESSAGE_MARKER`'s
matcher role) in the next minor release. Accepted residual until rs-dpp grows a typed variant: the
Rust-internal segment rides the `signer_error:key_unavailable: ` prefix
through `ProtocolError::Generic` (typed at both ABI edges, one Rust-owned
constant bridging the string segment).

- **On-device `KeyPermanentlyInvalidatedException` coverage.** The
invalidation recovery (generation-checked alias deletion + re-derive via
forced repair) is pinned at the unit tier through the fake Keystore seam;
a REAL KPIE requires biometric re-enrollment mid-test, which CI's emulator
cannot do — same residual #4172 accepted. Exercise manually per the device
test plan when touching the invalidation path.

## Environment-bound (cannot be code-fixed here)

- **End-to-end send→accept→pay testnet UAT** — device/testnet-bound; not runnable in
Expand Down
41 changes: 39 additions & 2 deletions docs/sdk/KOTLIN_SWIFT_SHARED_PARITY_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -630,8 +630,10 @@ Required validation per slice:
## 12. Compatibility and rollout

- Database migrations are additive and serialized as v5 unsigned token storage, v6
FVK, v7 provider membership/position, and v8 invitations; do not
destructively rewrite address identity columns.
FVK, v7 provider membership/position, and v8 identity-key derivation
breadcrumbs (pending-repair durability, dashpay/platform#4060); the
invitations migration shifts to v9. Do not destructively rewrite address
identity columns.
- New Rust FFI functions are additive and new result PODs are opaque/versioned.
Existing released split-builder entry points are deprecated before removal;
existing struct layouts and JNI descriptors do not change silently. The
Expand All @@ -644,6 +646,41 @@ Required validation per slice:
- The parity manifest initially records known gaps. CI prevents regression but does
not require all gaps to close in the first slice.

### Keystore rework divergences and convergences (dashpay/platform#4060)

Recorded in `sdk-parity-manifest.json`; rationale here:

- **`KeySecurityPolicy` + Keystore alias split (Kotlin-only, deliberate).**
Android Keystore fixes authentication parameters at key generation, so the
AUTH_GATED/DEVICE_BOUND policies require distinct aliases; iOS Keychain has
no per-alias auth-parameter analog (item access control covers the same
ground), so the manifest marks Swift `not-applicable` — no Swift port is
planned. The lockless-device degradation (AUTH_GATED writes redirect to the
DEVICE_BOUND alias, surfaced via `effectiveKeySecurityPolicy`) is likewise
Android-specific: KeyMint rejects gated key generation without a secure
lock screen.
- **Platform-wallet code 98 is now CONVERGENT.** Kotlin previously collapsed
the blanket Option-miss code into the top-level `DashSdkError.NotFound`
while Swift kept it in the wallet family; Kotlin now maps 98 to
`DashSdkError.PlatformWallet.NotFound` (BREAKING for hosts that caught the
top-level type from platform-wallet operations).
- **Durable pending-repair surface (Kotlin-only, port candidate).**
`pendingIdentityKeys` + forced/verified `repairIdentityKey` + the Room v8
derivation breadcrumbs have no Swift counterpart; the manifest records the
gap as `unsupported` for Swift.
- **Structured `SigningKeyUnavailable` discriminator (both hosts).** The
signer completion carries a typed `error_code` (rs-sdk-ffi
`DashSDKSignerErrorCode`), restored as platform-wallet code 31 on both
hosts. The Rust-internal segment rides the machine prefix
`signer_error:key_unavailable: ` through `ProtocolError::Generic` (a typed
rs-dpp variant was rejected for serialization blast radius — accepted
residual). The Kotlin `MESSAGE_MARKER` text sniff survives ONLY as a
deprecated fallback for the #4191 merge-order transition (marker-based
classification predating the typed code) and for conversion paths that
lose the machine prefix; mixed old-native/new-Kotlin artifacts are
unsupported outright (the sign-completion JNI arity changed 3→4 args).
Remove it (and the marker's matcher role) in the next minor release.

## 13. Explicitly out of scope

- Redesigning DIP-13/DIP-15 invitation wire formats.
Expand Down
162 changes: 162 additions & 0 deletions docs/sdk/sdk-parity-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"core_wallet_broadcast_signed_transaction_v2": "packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs",
"core_wallet_get_balance": "packages/rs-platform-wallet-ffi/src/core_wallet/wallet.rs",
"core_wallet_tx_builder_finalize": "packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs",
"dash_sdk_sign_async_completion": "packages/rs-sdk-ffi/src/signer.rs",
"dpns_name_array_free": "packages/rs-platform-wallet-ffi/src/dpns.rs",
"managed_identity_get_contested_dpns_names": "packages/rs-platform-wallet-ffi/src/dpns.rs",
"on_load_shielded_viewing_keys_fn": "packages/rs-platform-wallet-ffi/src/persistence.rs",
Expand Down Expand Up @@ -1124,6 +1125,167 @@
"covers_restart": false
}
]
},
{
"id": "security.key_security_policy_alias_split",
"title": "Identity-key security policy selects a dedicated Keystore alias (AUTH_GATED default / DEVICE_BOUND opt-in), with honest lockless degradation",
"area": "shared_policy",
"shared_apis": [],
"required_persistence_capabilities": [],
"hosts": {
"swift": {
"sdk": "not-applicable",
"example_app": "not-applicable",
"restart": "not_applicable",
"reason": null
},
"kotlin": {
"sdk": "supported",
"example_app": "supported",
"restart": "tested",
"reason": null
}
},
"verification": [
{
"host": "kotlin",
"kind": "unit",
"file": "packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/security/KeySecurityPolicyTest.kt",
"id": "effectivePolicyDegradesToDeviceBoundOnALocklessDevice",
"command": "cd packages/kotlin-sdk && ./gradlew :sdk:testDebugUnitTest",
"covers_restart": false
},
{
"host": "kotlin",
"kind": "unit",
"file": "packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/security/WalletStorageUpgradeMatrixTest.kt",
"id": "aliasTagRoutesReadsAfterLocklessDegradation",
"command": "cd packages/kotlin-sdk && ./gradlew :sdk:testDebugUnitTest",
"covers_restart": true
}
]
},
{
"id": "errors.platform_wallet_not_found_typed",
"title": "Platform-wallet code 98 (Option miss) surfaces typed inside the wallet-error family on both hosts",
"area": "shared_policy",
"shared_apis": [],
"required_persistence_capabilities": [],
"hosts": {
"swift": {
"sdk": "supported",
"example_app": "not-applicable",
"restart": "not_applicable",
"reason": null
},
"kotlin": {
"sdk": "supported",
"example_app": "not-applicable",
"restart": "not_applicable",
"reason": null
}
},
"verification": [
{
"host": "swift",
"kind": "unit",
"file": "packages/swift-sdk/SwiftTests/SwiftDashSDKTests/ErrorHandlingTests.swift",
"id": "testPlatformWalletNotFoundFFIResultMapping",
"command": "swift test --package-path packages/swift-sdk",
"covers_restart": false
},
{
"host": "kotlin",
"kind": "unit",
"file": "packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/errors/DashSdkErrorTest.kt",
"id": "platformWalletNotFoundCodeMapsToTypedWalletNotFound",
"command": "cd packages/kotlin-sdk && ./gradlew :sdk:testDebugUnitTest",
"covers_restart": false
}
]
},
{
"id": "persistence.pending_identity_key_repair",
"title": "Watch-only identity-key failures surface as durable, restart-surviving pending-repair state",
"area": "persistence",
"shared_apis": [],
"required_persistence_capabilities": [
"atomic_changesets",
"wallet_restore"
],
"hosts": {
"swift": {
"sdk": "unsupported",
"example_app": "unsupported",
"restart": "not_applicable",
"reason": "Swift's persistence handler re-derives inline and has no queryable pending-keys surface or repair verification; the WalletKeyHealthSheet re-derive covers the user path but nothing is durable or typed. Port the pendingIdentityKeys/repairIdentityKey surface if iOS needs the same recoverability guarantees."
},
"kotlin": {
"sdk": "supported",
"example_app": "not-applicable",
"restart": "tested",
"reason": null
}
},
"verification": [
{
"host": "kotlin",
"kind": "unit",
"file": "packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/persistence/PlatformWalletPersistenceHandlerTest.kt",
"id": "reconstructionSeedsPendingFromBreadcrumbRowsWithNullIdentifier",
"command": "cd packages/kotlin-sdk && ./gradlew :sdk:testDebugUnitTest",
"covers_restart": true
},
{
"host": "kotlin",
"kind": "device",
"file": "packages/kotlin-sdk/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/persistence/DashDatabaseMigrationTest.kt",
"id": "migrate7To8AddsDerivationBreadcrumbColumns",
"command": "cd packages/kotlin-sdk && ./gradlew :sdk:connectedDebugAndroidTest",
"covers_restart": false
}
]
},
{
"id": "errors.signing_key_unavailable_discriminator",
"title": "Structured SigningKeyUnavailable signer discriminator (typed code 31, no message sniffing)",
"area": "shared_policy",
"shared_apis": [
"dash_sdk_sign_async_completion"
],
"required_persistence_capabilities": [],
"hosts": {
"swift": {
"sdk": "supported",
"example_app": "not-applicable",
"restart": "not_applicable",
"reason": null
},
"kotlin": {
"sdk": "supported",
"example_app": "not-applicable",
"restart": "not_applicable",
"reason": null
}
},
"verification": [
{
"host": "swift",
"kind": "unit",
"file": "packages/swift-sdk/SwiftTests/SwiftDashSDKTests/ErrorHandlingTests.swift",
"id": "testSigningKeyUnavailableFFIResultMapping",
"command": "swift test --package-path packages/swift-sdk",
"covers_restart": false
},
{
"host": "kotlin",
"kind": "unit",
"file": "packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/errors/DashSdkErrorTest.kt",
"id": "signingKeyUnavailableCode31MapsTyped",
"command": "cd packages/kotlin-sdk && ./gradlew :sdk:testDebugUnitTest",
"covers_restart": false
}
]
}
]
}
Loading
Loading