Skip to content

fix(platform-wallet): register data contract for returned-proof verification on writes#4035

Merged
QuantumExplorer merged 2 commits into
feat/kotlin-sdk-and-example-appfrom
claude/mobile-proof-verify-unknown-contract
Jul 7, 2026
Merged

fix(platform-wallet): register data contract for returned-proof verification on writes#4035
QuantumExplorer merged 2 commits into
feat/kotlin-sdk-and-example-appfrom
claude/mobile-proof-verify-unknown-contract

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Jul 7, 2026

Copy link
Copy Markdown
Member

This PR has two parts: (1) the core fix — the shared Rust write path now registers the contract so returned-proof verification resolves it; and (2) an Android parity follow-up — the Kotlin app now preloads stored contracts into the trusted provider at SDK init, the way iOS already does.

Issue being fixed or feature implemented

On mobile (KotlinExampleApp / rs-unified-sdk-jni, and the same shared code on Swift), a document write broadcasts successfully and the document lands on-chain, but the app reports failure with:

Proof verification error: dash drive: proof: unknown contract in documents batch transition error: unknown contract with id HLY575cN… in document verification

The token write path hits the identical "unknown contract … in token verification".

Root cause: the returned-proof verifier resolves the target data contract only through the SDK's ContextProvider (Drive::verify_state_transition_was_executed_with_proofknown_contracts_provider_fnContextProvider::get_data_contract). The mobile FFI SDK uses TrustedHttpContextProvider, whose get_data_contract serves contracts only from its known_contracts map + built-in system contracts + optional fallback — no network fetch. The platform-wallet write path fetches the contract to build the document but never registers it there, so post-broadcast verification returns NoneProofError::UnknownContract. Reads are unaffected (DocumentQuery/Fetch embed the Arc<DataContract> and verify against that); DataContractCreate is unaffected (verifies straight from the proof).

iOS didn't surface this only because the SwiftExampleApp bulk-preloads all stored contracts into the trusted provider at SDK init (AppState.loadKnownContractsIntoSDKdash_sdk_add_known_contracts); the Kotlin app had no equivalent. So the bug was latent on both platforms — Android just wasn't papering over it.

What was done?

Part 1 — the fix (register at write time, all platforms):

  • Added a provided ContextProvider::register_data_contract(&self, Arc<DataContract>) (default no-op) in rs-context-provider, forwarded in both blanket impls (AsRef<dyn ContextProvider> and Mutex<T>) — load-bearing, since Sdk::context_provider() returns an Arc<dyn ContextProvider>.
  • Overrode it in TrustedHttpContextProvider (into known_contracts) and GrpcContextProvider (into its cache).
  • rs-platform-wallet network/document.rs: create_document_with_signer and the shared fetch_contract_arc_for_document_op (replace/delete/transfer/set-price/purchase) register the fetched contract before broadcast.

Part 2 — Android parity (preload at init, matches iOS):

  • JNI Java_…_QueriesNative_addKnownContracts bridges dash_sdk_add_known_contracts (marshals a byte[][] of versioned serializations + a comma-joined base58 id list; throws on FFI error).
  • Kotlin SDK: QueriesNative.addKnownContracts + Contracts.loadKnownContracts(List<Pair<String, ByteArray>>) (mirrors SDK.loadKnownContracts).
  • App: AppContainer.bootstrap preloads every DataContractEntity with a non-null binarySerialization after SDK init — best-effort, non-fatal.
  • ContractDownloader now fetches JSON + binary serialization in one round trip and persists binarySerialization (new rows + backfill), so the preload has bytes to load (it was always null before).

Token write paths hit the identical error and can reuse register_data_contract — left as a follow-up to keep this scoped to documents.

How Has This Been Tested?

  • cargo check on dash-context-provider, rs-sdk-trusted-context-provider, dash-sdk, platform-wallet, and rs-unified-sdk-jni — all clean; rustfmt --check clean; Kotlin :sdk: + :app:compileDebugKotlin BUILD SUCCESSFUL.
  • Part 1 device-verified (KotlinExampleApp, testnet, arm64 JNI rebuilt): re-drove DOC-02 — the app now shows "Document Created / Broadcast confirmed on-chain." instead of the error dialog; the confirmed doc (8f28eqGDPm4Fa6CHzXbtp7x1wovdYnVYag6n1BDzzqLA) is queryable via Browse Documents.
  • Part 2 device-verified: the new JNI symbol is exported in the rebuilt .so (llvm-nm); after downloading a contract, the app logs AppContainer: Loaded 1 known contracts into SDK's trusted provider at startup — no UnsatisfiedLinkError, no deserialize error (proves the byte[][] marshaling → versioned_deserialize → register path works end to end).

Breaking Changes

None. register_data_contract is a provided trait method with a default no-op (additive, backward-compatible for all existing ContextProvider implementors). Not consensus-breaking.

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

🤖 Generated with Claude Code

…ication on writes

Mobile document/token writes broadcast successfully and land on-chain, but
the app reported failure with "unknown contract with id <id> in document
verification" (and the token analog). The returned-proof verifier resolves
the target data contract only through the SDK's ContextProvider; the mobile
TrustedHttpContextProvider serves contracts solely from its known_contracts
map (no network fetch), and the platform-wallet write path fetched the
contract to build the document but never registered it there, so the
post-broadcast verification returned None -> ProofError::UnknownContract.

Add a provided ContextProvider::register_data_contract (default no-op),
forwarded in the AsRef and Mutex blanket impls (so a call through the
Arc<dyn ContextProvider> that Sdk::context_provider() returns reaches the
concrete provider) and overridden by TrustedHttpContextProvider (into
known_contracts) and GrpcContextProvider (into its cache). The platform-wallet
document create and the shared mutate-op contract fetch (replace/delete/
transfer/set-price/purchase) register the fetched contract before broadcast,
so the returned-proof verification can resolve it.

Reads were unaffected (they verify against the Arc<DataContract> embedded in
the query); contract-create was unaffected (verifies straight from the proof).

Device-verified on testnet: KotlinExampleApp DOC-02 now shows the "Broadcast
confirmed on-chain" success section instead of the error dialog, and the new
document is queryable via Browse Documents.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@thepastaclaw

thepastaclaw commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

✅ Review complete (commit a94b142)

@thepastaclaw thepastaclaw left a comment

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.

Code Review

The document-write fix matches the PR's stated implementation: platform-wallet registers the fetched contract before document broadcasts, and the SDK/provider forwarding path reaches the concrete cache-backed providers. I found no blocking correctness, consensus, dependency-direction, or security issue. The only in-scope review item is missing regression coverage for the load-bearing trait forwarding/cache registration path.

Source: Codex/gpt-5.5 reviewers for general, security-auditor, rust-quality, and ffi-engineer; Codex/gpt-5.5 verifier. Claude/opus reviewer lanes and the Claude verifier were attempted but failed due the extra-usage limit (resets Jul 10, 8am America/Chicago).

🟡 1 suggestion(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-context-provider/src/provider.rs`:
- [SUGGESTION] packages/rs-context-provider/src/provider.rs:112-113: Add regression coverage for provider registration forwarding
  This forwarding is load-bearing for the PR's fix. `IdentityWallet::register_contract_for_proof_verification` calls `register_data_contract` through the value returned by `Sdk::context_provider()`, which wraps the concrete provider behind the `ContextProvider` abstraction; without these forwarding impls, the call can compile while dispatching to the default no-op and the returned-proof verifier will again fail with an unknown contract. The existing trusted-provider test only checks the empty known-contract lookup path, so add a focused test that registers through the same wrapper/trait shape and then verifies `get_data_contract` returns the contract.

Comment on lines +112 to +113
fn register_data_contract(&self, contract: Arc<DataContract>) {
self.as_ref().register_data_contract(contract)

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: Add regression coverage for provider registration forwarding

This forwarding is load-bearing for the PR's fix. IdentityWallet::register_contract_for_proof_verification calls register_data_contract through the value returned by Sdk::context_provider(), which wraps the concrete provider behind the ContextProvider abstraction; without these forwarding impls, the call can compile while dispatching to the default no-op and the returned-proof verifier will again fail with an unknown contract. The existing trusted-provider test only checks the empty known-contract lookup path, so add a focused test that registers through the same wrapper/trait shape and then verifies get_data_contract returns the contract.

source: ['codex']

…t init

Brings Android to iOS parity. The SwiftExampleApp preloads every locally
stored data contract into the SDK's trusted context provider at SDK init
(AppState.loadKnownContractsIntoSDK -> SDK.loadKnownContracts ->
dash_sdk_add_known_contracts), so proof verification resolves those contracts
without a network fetch. The Kotlin app had no equivalent, so its trusted
provider only ever held system contracts.

- JNI: Java_..._QueriesNative_addKnownContracts bridges
  dash_sdk_add_known_contracts, marshaling a byte[][] of versioned contract
  serializations plus a comma-joined base58 id list; throws on FFI error.
- Kotlin SDK: QueriesNative.addKnownContracts external fun +
  Contracts.loadKnownContracts(List<Pair<String, ByteArray>>) wrapper
  (mirrors Swift SDK.loadKnownContracts).
- App: AppContainer.bootstrap preloads every DataContractEntity row that has a
  non-null binarySerialization after SDK init — best-effort and non-fatal, so
  a failure never blocks bootstrap (mirrors iOS "not critical for SDK op").
- ContractDownloader now fetches JSON + the binary serialization in one round
  trip (fetchWithSerialization) and persists binarySerialization for new rows
  and backfills pre-existing rows, so the preload has bytes to load; it was
  always null before, which would have made the preload a no-op.

Complements the returned-proof-verification fix on this branch (which
registers the contract at write time). This adds the startup bulk-preload
iOS relies on, so any provider-backed proof verification resolves stored
contracts without a fetch.

Device-verified: KotlinExampleApp logs "Loaded 1 known contracts into SDK's
trusted provider" at startup after downloading a contract; no
UnsatisfiedLinkError, no deserialize error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2010f763-e3ff-49ff-967e-71c6f206ba82

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/mobile-proof-verify-unknown-contract

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.

@thepastaclaw thepastaclaw left a comment

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.

Code Review

Source: reviewer lanes: codex gpt-5.5 general (ok), codex gpt-5.5 security-auditor (ok), codex gpt-5.5 rust-quality (ok), codex gpt-5.5 ffi-engineer (ok); failed lanes: claude opus general (failed_or_unparseable), claude opus security-auditor (failed_or_unparseable), claude opus rust-quality (failed_or_unparseable), claude opus ffi-engineer (failed_or_unparseable); verifier: codex gpt-5.5 (ok); specialists: security-auditor, rust-quality, ffi-engineer.

Verified head a94b142. The carried-forward prior finding is still valid because no test was added for the load-bearing ContextProvider forwarding path; the latest delta also introduces one in-scope Android preload lifecycle gap after SDK rebuilds. No CodeRabbit inline findings were provided, so there are no reactions.

Prior reconciliation:

  • prior-1: STILL_VALID — packages/rs-context-provider/src/provider.rs still forwards register_data_contract at lines 112-113, but searches across the Rust/Kotlin touched areas found no focused regression test that registers through the opaque Sdk::context_provider() wrapper shape and then verifies get_data_contract returns the registered contract.

Carried-forward prior findings:

  • [SUGGESTION] packages/rs-context-provider/src/provider.rs:112-113: Add regression coverage for provider registration forwarding
    The document-write fix relies on calls made through the opaque value returned by Sdk::context_provider() reaching the underlying provider override. If this blanket impl ever stops forwarding register_data_contract, the code still compiles but falls back to the trait's default no-op, so returned-proof verification can again fail with an unknown contract after a successful broadcast. Add a focused regression test that registers a contract through the same wrapper/trait-object shape and confirms get_data_contract resolves it.

New findings in the latest delta:

  • [SUGGESTION] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/di/AppContainer.kt:248: Reload known contracts after SDK rebuilds
    Known contracts are loaded only during bootstrap, immediately after the first initializeSdk call. AppState.switchNetwork rebuilds and publishes a fresh Sdk with a fresh trusted context provider, while the SDK-change observer in AppRoot only calls activateManager, so stored contracts are not registered into the new provider after a network switch or SDK reconfiguration. Wire the preload into the SDK rebuild path as well, before returned-proof verification can use the new provider.

Out-of-scope follow-up noted by verifier:

  • Apply returned-proof contract registration to token writes: Token external-signer helpers fetch the data contract and then broadcast through SDK token write methods without the document path's new provider registration. This is the same returned-proof dependency, but the reviewed PR explicitly targets document writes plus Android known-contract preload, so it should be tracked separately.

🟡 2 suggestion(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

Carried-forward prior finding:
- [SUGGESTION] packages/rs-context-provider/src/provider.rs:112-113: Add regression coverage for provider registration forwarding
  The document-write fix relies on calls made through the opaque value returned by Sdk::context_provider() reaching the underlying provider override. If this blanket impl ever stops forwarding register_data_contract, the code still compiles but falls back to the trait's default no-op, so returned-proof verification can again fail with an unknown contract after a successful broadcast. Add a focused regression test that registers a contract through the same wrapper/trait-object shape and confirms get_data_contract resolves it.

New latest-delta finding:
- [SUGGESTION] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/di/AppContainer.kt:248: Reload known contracts after SDK rebuilds
  Known contracts are loaded only during bootstrap, immediately after the first initializeSdk call. AppState.switchNetwork rebuilds and publishes a fresh Sdk with a fresh trusted context provider, while the SDK-change observer in AppRoot only calls activateManager, so stored contracts are not registered into the new provider after a network switch or SDK reconfiguration. Wire the preload into the SDK rebuild path as well, before returned-proof verification can use the new provider.

Sdk.enableLogging(Sdk.LogLevel.DEBUG)
appState.restorePreferences()
appState.initializeSdk()
loadKnownContractsIntoSdk()

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: Reload known contracts after SDK rebuilds

Known contracts are loaded only during bootstrap, immediately after the first initializeSdk call. AppState.switchNetwork rebuilds and publishes a fresh Sdk with a fresh trusted context provider, while the SDK-change observer in AppRoot only calls activateManager, so stored contracts are not registered into the new provider after a network switch or SDK reconfiguration. Wire the preload into the SDK rebuild path as well, before returned-proof verification can use the new provider.

source: ['codex']

@QuantumExplorer QuantumExplorer merged commit 78dd34b into feat/kotlin-sdk-and-example-app Jul 7, 2026
6 checks passed
@QuantumExplorer QuantumExplorer deleted the claude/mobile-proof-verify-unknown-contract branch July 7, 2026 09:22
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