fix(platform-wallet): register data contract for returned-proof verification on writes#4035
Conversation
…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>
|
✅ Review complete (commit a94b142) |
There was a problem hiding this comment.
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.
| fn register_data_contract(&self, contract: Arc<DataContract>) { | ||
| self.as_ref().register_data_contract(contract) |
There was a problem hiding this comment.
🟡 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>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
🟡 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']
78dd34b
into
feat/kotlin-sdk-and-example-app
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:
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_proof→known_contracts_provider_fn→ContextProvider::get_data_contract). The mobile FFI SDK usesTrustedHttpContextProvider, whoseget_data_contractserves contracts only from itsknown_contractsmap + 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 returnsNone→ProofError::UnknownContract. Reads are unaffected (DocumentQuery/Fetchembed theArc<DataContract>and verify against that);DataContractCreateis 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.loadKnownContractsIntoSDK→dash_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):
ContextProvider::register_data_contract(&self, Arc<DataContract>)(default no-op) inrs-context-provider, forwarded in both blanket impls (AsRef<dyn ContextProvider>andMutex<T>) — load-bearing, sinceSdk::context_provider()returns anArc<dyn ContextProvider>.TrustedHttpContextProvider(intoknown_contracts) andGrpcContextProvider(into its cache).rs-platform-walletnetwork/document.rs:create_document_with_signerand the sharedfetch_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):
Java_…_QueriesNative_addKnownContractsbridgesdash_sdk_add_known_contracts(marshals abyte[][]of versioned serializations + a comma-joined base58 id list; throws on FFI error).QueriesNative.addKnownContracts+Contracts.loadKnownContracts(List<Pair<String, ByteArray>>)(mirrorsSDK.loadKnownContracts).AppContainer.bootstrappreloads everyDataContractEntitywith a non-nullbinarySerializationafter SDK init — best-effort, non-fatal.ContractDownloadernow fetches JSON + binary serialization in one round trip and persistsbinarySerialization(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 checkondash-context-provider,rs-sdk-trusted-context-provider,dash-sdk,platform-wallet, andrs-unified-sdk-jni— all clean;rustfmt --checkclean; Kotlin:sdk:+:app:compileDebugKotlinBUILD SUCCESSFUL.8f28eqGDPm4Fa6CHzXbtp7x1wovdYnVYag6n1BDzzqLA) is queryable via Browse Documents..so(llvm-nm); after downloading a contract, the app logsAppContainer: Loaded 1 known contracts into SDK's trusted providerat startup — noUnsatisfiedLinkError, no deserialize error (proves thebyte[][]marshaling →versioned_deserialize→ register path works end to end).Breaking Changes
None.
register_data_contractis a provided trait method with a default no-op (additive, backward-compatible for all existingContextProviderimplementors). Not consensus-breaking.Checklist:
For repository code-owners and collaborators only
🤖 Generated with Claude Code