Skip to content
Open
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ee6546a
feat(kotlin-sdk): wire-compatible encrypted txMetadata document creat…
bfoss765 Jul 10, 2026
2571bbb
test(platform-wallet): pin txMetadata wire-compat to a dashj-generate…
bfoss765 Jul 11, 2026
dfc3c23
test(platform-wallet): pin encrypted-txMetadata FETCH to the real tes…
bfoss765 Jul 11, 2026
edaa67f
debug(platform-wallet): warn-level stage breadcrumbs on the encrypted…
bfoss765 Jul 11, 2026
bcafc60
fix(platform-wallet): dual-emit encrypted-document breadcrumbs throug…
bfoss765 Jul 11, 2026
8b4290b
fix(platform-wallet): derive txMetadata keys through the mnemonic res…
bfoss765 Jul 11, 2026
ebfc9c4
fix(platform-wallet): nonzero-identity wire vector; keep master xprv …
bfoss765 Jul 11, 2026
988368a
perf(platform-wallet): share the fetched DataContract Arc instead of …
bfoss765 Jul 12, 2026
87b6f37
fix(kotlin-sdk): reject txMetadata version bytes the legacy stack can…
bfoss765 Jul 12, 2026
7f8ac4d
fix(platform-wallet): enforce txMetadata wire version (0|1) in Rust c…
bfoss765 Jul 12, 2026
8a74912
docs(txmetadata): scope legacy wire-compat to identity_index=0; relab…
bfoss765 Jul 12, 2026
5e5220d
test(txmetadata): check in a real-DerivationPathFactory provenance ve…
bfoss765 Jul 12, 2026
194ec86
fix(kotlin-sdk): gate encrypted-document create/fetch through the Tea…
bfoss765 Jul 16, 2026
d456822
fix(platform-wallet): pre-check txMetadata payload size before deriva…
bfoss765 Jul 21, 2026
0c5527a
fix(platform-wallet-ffi): zeroize + drop txMetadata plaintext before …
bfoss765 Jul 21, 2026
c8063e4
docs(txmetadata): mark the fetch regression test as a manual/testnet-…
bfoss765 Jul 21, 2026
9af3ece
chore(txmetadata): strip internal tracker tokens from comments
bfoss765 Jul 21, 2026
5accc5d
test(platform-wallet): anchor ENCRYPTED_METADATA_FIELD_MAX to the con…
bfoss765 Jul 21, 2026
64bec68
test(txmetadata): pin an independent legacy-INSTALL wire-compat vector
bfoss765 Jul 21, 2026
983154d
fix(kotlin-sdk): zeroize + early-drop the JNI txMetadata plaintext copy
bfoss765 Jul 22, 2026
d0431bd
feat(platform-wallet): allocate txMetadata encryptionKeyIndex in Rust…
bfoss765 Jul 21, 2026
db7dc38
docs(keyindex): note allocator serialization and failure-gap trade-offs
bfoss765 Jul 21, 2026
4f2eb06
fix(platform-wallet): validate txMetadata payload size before allocat…
bfoss765 Jul 22, 2026
9efc0b7
feat(swift-sdk): Swift wrappers for encrypted-txMetadata FFI exports
bfoss765 Jul 21, 2026
b56899e
Merge remote-tracking branch 'origin/v4.1-dev' into fix/txmetadata-de…
shumkov Jul 23, 2026
7616365
fix(sdk): zeroize txMetadata decrypt buffers
shumkov Jul 28, 2026
e39703d
Merge remote-tracking branch 'origin/v4.1-dev' into fix/txmetadata-de…
shumkov Jul 28, 2026
5d99b88
docs(sdk): fix plaintext lifetime wording
shumkov Jul 28, 2026
9870f4f
style(platform-wallet): apply current rustfmt output
shumkov Jul 28, 2026
4868eca
docs(sdk): remove internal plaintext lifetime spec
shumkov Jul 30, 2026
202093a
merge: v4.2-dev into txMetadata integration
shumkov Aug 2, 2026
188549c
fix(sdk): complete encrypted txMetadata parity
shumkov Aug 2, 2026
f845f9d
fix(kotlin-sdk): type wallet invalid-parameter errors
shumkov Aug 2, 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
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package org.dashfoundation.dashsdk.documents

import org.dashfoundation.dashsdk.wallet.op

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.dashfoundation.dashsdk.errors.mapNativeErrors
import org.dashfoundation.dashsdk.ffi.TransactionsNative

Expand Down Expand Up @@ -251,4 +248,164 @@ class DocumentTransactions internal constructor(
)
}
}

/**
* Create + broadcast an ENCRYPTED wallet-contract document (the wire-
* compatible `txMetadata` shape) on [contractId]'s [documentType], owned by
* [ownerId] — signed via [signerHandle]. Implements the create half of the
* legacy `BlockchainIdentity.publishTxMetaData` retirement: the SDK derives
* the identity encryption key, seals [payload] into the legacy
* `version ‖ IV ‖ AES-256-CBC` blob, and writes
* `{keyIndex, encryptionKeyIndex, encryptedMetadata}`.
*
* Batching stays app-side: the caller serializes its items into [payload]
* (a protobuf `TxMetadataBatch`). The identity encryption key id (the
* `keyIndex` field) is chosen SDK-side to match the legacy stack, so the key
* never crosses the FFI boundary.
*
* ### `encryptionKeyIndex` allocation
* Leave [encryptionKeyIndex] `null` (the default) to let the SDK allocate
* the per-document index in Rust from authoritative Platform state — the
* host-thin path. Rust counts the identity's existing documents of this
* contract and document type on Platform and uses `1 + count`, the same
* series the legacy stack produced, serialized in process so concurrent
* creates never pick the same index. The index is best-effort unique PER
* DEVICE; a cross-device duplicate is not data loss, because each document
* stores its own index and the reader derives that document's key from it,
* so both decrypt independently. It follows that the index is an
* encryption-key selector, NOT a document sequence number: do not order,
* count, address, or gap-check documents by it.
*
* Passing an explicit non-negative [encryptionKeyIndex] is retained ONLY for
* migration / tests and is discouraged: a caller-supplied counter can
* collide across concurrent callers and devices, and choosing the index is
* the SDK's job.
*
* ### What is not scrubbed
* The SDK zeroizes the native copies it makes of [payload]. It cannot
* scrub [payload] itself: that is a JVM `ByteArray` the caller owns, as are
* any buffers that produced it, and the runtime may have copied it while
* compacting the heap. Treat it and every JVM copy as plaintext-equivalent
* for as long as they are reachable: keep them short-lived, never log them,
* and overwrite your own array once this call returns where that is
* feasible. Overwriting the array you hold does not reach any copy the
* runtime made of it, so this reduces exposure rather than eliminating it.
*
* @param encryptionKeyIndex `null` to let the SDK allocate the index
* (preferred); or an explicit non-negative per-document index
* (migration / tests only).
* @param version payload version byte. Which values are meaningful is
* decided by the wallet core; an unsupported one is rejected there and
* surfaced as a platform-wallet invalid-parameter error.
* @param payload already-serialized opaque plaintext; the SDK does not
* parse it.
* [mnemonicResolverHandle] is the host mnemonic-resolver handle
* ([org.dashfoundation.dashsdk.wallet.PlatformWalletManager.mnemonicResolverHandle]):
* required for external-signable wallets (the app's shape — the AES key
* derives on demand through the resolver), ignored for wallets with
* resident private keys.
*
* @return the confirmed document's canonical JSON (its 32-byte id is the
* base58 `$id` field).
*/
suspend fun createEncryptedDocument(
walletHandle: Long,
mnemonicResolverHandle: Long,
ownerId: ByteArray,
contractId: ByteArray,
documentType: String,
version: Int,
payload: ByteArray,
signerHandle: Long,
encryptionKeyIndex: Int? = null,
): String = gate.op {
require(ownerId.size == 32) { "ownerId must be 32 bytes" }
require(contractId.size == 32) { "contractId must be 32 bytes" }
require(encryptionKeyIndex == null || encryptionKeyIndex >= 0) {
"encryptionKeyIndex, when supplied, must be non-negative, got $encryptionKeyIndex"
}
mapNativeErrors {
TransactionsNative.documentCreateEncrypted(
walletHandle,
mnemonicResolverHandle,
ownerId,
contractId,
documentType,
// -1 is the JNI sentinel for "let Rust allocate the index".
encryptionKeyIndex ?: -1,
version,
payload,
signerHandle,
)
}
}

/**
* Fetch + DECRYPT every encrypted wallet-contract document owned by
* [ownerId] on [contractId]'s [documentType] updated at or after [sinceMs]
* (epoch-millis). Implements the read half of the legacy
* `BlockchainIdentity.getTxMetaData(since, key)` retirement: the SDK fetches
* the owner-scoped, since-timestamp documents and decrypts each with the
* identity's derived key. Documents that fail to decrypt, and documents
* carrying an unsupported wire version, are skipped Rust-side — a bad
* document never aborts the fetch.
*
* ### Decryption is not authentication
* The envelope is AES-256-CBC with PKCS7 and carries no integrity tag, so a
* successful decrypt does not mean the bytes are genuine. A wrong key or a
* modified ciphertext usually fails the unpad and is skipped, but PKCS7
* accepts a wrong plaintext often enough that an element can carry opaque
* garbage. Parse every `payload` strictly — CBOR for `version` 0, protobuf
* for 1 — and discard anything that does not parse, rather than trusting it
* because it appeared in the array.
*
* @return a JSON array; each element is `{ "id", "ownerId" (base58),
* "keyIndex", "encryptionKeyIndex", "version", "updatedAt" (number|null),
* "payload" (base64 of the decrypted opaque plaintext) }`. The caller
* parses each `payload` itself and MUST dispatch on `version`: `0` is a
* CBOR payload, `1` a protobuf `TxMetadataBatch`. Those are the only
* versions the legacy format defines; a document carrying anything else
* is skipped by the SDK and never reaches this array. Reconcile memo /
* taxCategory / exchangeRate / service / giftCard fields into the local
* store from the parsed payload.
*
* ### What is not scrubbed
* The SDK zeroizes the native decrypted-payload and JSON buffers it owns.
* It cannot scrub the returned `String`: that is a JVM object the runtime
* manages, as are every copy of it and every object parsed out of it, and
* the runtime may have copied it while compacting the heap. Treat it and
* everything derived from it as plaintext-equivalent for as long as they
* are reachable: parse promptly, never log them, and do not retain or
* persist them longer than required. Unlike a `ByteArray` there is no
* overwrite to attempt here at all, so short retention is the only control
* the caller has.
*
* [mnemonicResolverHandle] is the host mnemonic-resolver handle
* ([org.dashfoundation.dashsdk.wallet.PlatformWalletManager.mnemonicResolverHandle]):
* required for external-signable wallets (the app's shape — the AES key
* derives on demand through the resolver), ignored for wallets with
* resident private keys.
*/
suspend fun fetchEncryptedDocuments(
walletHandle: Long,
mnemonicResolverHandle: Long,
ownerId: ByteArray,
contractId: ByteArray,
documentType: String,
sinceMs: Long,
): String = gate.op {
require(ownerId.size == 32) { "ownerId must be 32 bytes" }
require(contractId.size == 32) { "contractId must be 32 bytes" }
require(sinceMs >= 0) { "sinceMs must be non-negative, got $sinceMs" }
mapNativeErrors {
TransactionsNative.documentFetchEncrypted(
walletHandle,
mnemonicResolverHandle,
ownerId,
contractId,
documentType,
sinceMs,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,92 @@ internal object TransactionsNative {
signerHandle: Long,
): String

/**
* Create + broadcast an ENCRYPTED wallet-contract document (the wire-
* compatible `txMetadata` shape) on [contractId]'s [documentType], owned by
* [ownerId], signed via [signerHandle]. Bridges
* the Rust-ABI composite
* `create_encrypted_document_with_deferred_payload`.
*
* The Rust side selects the identity's ENCRYPTION key id (the `keyIndex`
* field), derives the AES key from the wallet HD tree, and seals [payload]
* into the legacy `version ‖ IV ‖ AES-256-CBC` blob — decryptable by the
* legacy `org.dashj.platform` stack and vice versa.
*
* @param mnemonicResolverHandle the host mnemonic-resolver handle
* ([org.dashfoundation.dashsdk.wallet.PlatformWalletManager.mnemonicResolverHandle]);
* required (non-zero) for external-signable wallets — the app's shape —
* whose txMetadata AES key derives on demand through the resolver.
* Ignored for wallets with resident private keys.
* @param encryptionKeyIndex an explicit per-document index (migration /
* tests), OR `-1` to let the SDK allocate one from authoritative Platform
* state. Both forms enter one Rust operation. For `-1`, Rust settles the
* index before asking JNI to copy this array into native memory — the
* allocation query has no request timeout, so copying first would retain
* plaintext throughout an unbounded wait. Rust then takes ownership of
* the native copy and scrubs it as soon as the properties are sealed,
* before broadcast.
* Values `< -1` are rejected. `-1` rather than a boxed `Integer?` keeps
* this signature on primitives.
* @param version payload version byte. This layer narrows it to a byte and
* nothing more: which values are meaningful is decided by the wallet core,
* which rejects an unsupported one before anything is sealed.
* @param payload the already-serialized opaque plaintext (a protobuf
* `TxMetadataBatch`); the SDK does not parse it. The native copies made of
* it are zeroized, but this `ByteArray` and any JVM copies of it are
* plaintext-equivalent and cannot be scrubbed by the SDK — see
* [org.dashfoundation.dashsdk.documents.DocumentTransactions.createEncryptedDocument].
* @return the confirmed document's canonical JSON (its 32-byte id is the
* base58 `$id` field).
*/
external fun documentCreateEncrypted(
walletHandle: Long,
mnemonicResolverHandle: Long,
ownerId: ByteArray,
contractId: ByteArray,
documentType: String,
encryptionKeyIndex: Int,
version: Int,
payload: ByteArray,
signerHandle: Long,
): String

/**
* Fetch + DECRYPT every encrypted wallet-contract document owned by
* [ownerId] on [contractId]'s [documentType] updated at or after [sinceMs]
* (epoch-millis). Bridges `platform_wallet_fetch_encrypted_documents` — the
* wire-compatible read counterpart of the legacy `getTxMetaData(since, key)`.
*
* @param mnemonicResolverHandle the host mnemonic-resolver handle
* ([org.dashfoundation.dashsdk.wallet.PlatformWalletManager.mnemonicResolverHandle]);
* required (non-zero) for external-signable wallets — the app's shape —
* whose txMetadata AES key derives on demand through the resolver.
* Ignored for wallets with resident private keys.
* @return a JSON array; each element is `{ "id", "ownerId" (base58),
* "keyIndex", "encryptionKeyIndex", "version", "updatedAt" (number|null),
* "payload" (base64 of the decrypted opaque plaintext) }`. Documents that
* fail to decrypt, and documents carrying an unsupported wire version,
* are skipped Rust-side. A payload that IS returned is not authenticated:
* the envelope is AES-256-CBC with PKCS7 and no integrity tag, so a wrong
* key or modified ciphertext usually fails the unpad but can occasionally
* unpad cleanly and surface opaque garbage. Parse each payload strictly
* and discard what does not parse.
*
* SDK-owned Rust/C decrypted payload and JSON buffers are zeroized before
* deallocation. The returned host `String` is plaintext-equivalent; its
* runtime-managed storage, copies, and parsed-object copies cannot be
* reliably overwritten by the SDK. Parse it promptly, do not log it, and
* do not retain or persist it longer than required.
*/
external fun documentFetchEncrypted(
walletHandle: Long,
mnemonicResolverHandle: Long,
ownerId: ByteArray,
contractId: ByteArray,
documentType: String,
sinceMs: Long,
): String

/**
* Cast a masternode contested-resource vote and wait for the response.
* Bridges `dash_sdk_contested_resource_cast_vote` (Swift
Expand Down
Loading
Loading