Skip to content

fix(sdk): deserialization error due to outdated contract cache#3052

Open
shumkov wants to merge 1 commit intov3.1-devfrom
fix/sdk/outdated-data-contract-cache
Open

fix(sdk): deserialization error due to outdated contract cache#3052
shumkov wants to merge 1 commit intov3.1-devfrom
fix/sdk/outdated-data-contract-cache

Conversation

@shumkov
Copy link
Collaborator

@shumkov shumkov commented Feb 4, 2026

Issue being fixed or feature implemented

When a data contract is updated on the network, the SDK may hold a cached (old) version of the contract. Documents serialized with the new schema
fail to deserialize against the old cached contract, producing a CorruptedSerialization error that propagates to the caller with no recovery.

What was done?

  • Preserved typed ProtocolError in drive-proof-verifier instead of stringifying it, enabling pattern matching on specific error variants
    downstream.
  • Added retry-once logic to Document::fetch and Document::fetch_many: on CorruptedSerialization error, the SDK refetches the contract from the
    network, updates the context provider cache, and retries with the fresh contract.
  • Added update_data_contract method to the ContextProvider trait so the cache can be updated after a successful refetch.
  • Added clone_with_contract to DocumentQuery to rebuild the query with a fresh contract.
  • Added mock infrastructure for simulating proof errors in tests.

How Has This Been Tested?

  • Added 3 integration tests in packages/rs-sdk/tests/fetch/mock_document_contract_refresh.rs:
    • test_fetch_document_retries_on_stale_contract — verifies Document::fetch retries with a fresh contract
    • test_fetch_many_documents_retries_on_stale_contract — verifies Document::fetch_many retries with a fresh contract
    • test_fetch_document_does_not_retry_on_other_errors — verifies non-deserialization errors propagate without retry
  • Added 4 unit tests for is_document_deserialization_error() in packages/rs-sdk/src/platform/fetch.rs
  • All 115 existing dash-sdk tests pass
  • cargo clippy and cargo fmt clean on all affected crates

Breaking Changes

drive_proof_verifier::Error::ProtocolError changed from a struct variant { error: String } to a tuple variant (dpp::ProtocolError). Code
matching on this variant needs updating.

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

Summary by CodeRabbit

  • New Features

    • Document fetch now automatically retries with a refreshed contract if the cached contract becomes stale, improving reliability.
    • Added contract update capabilities across context providers to maintain fresh contract caches.
  • Bug Fixes

    • Improved protocol error handling and reporting for more accurate error diagnostics.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

This PR introduces a new update_data_contract method across context providers, refactors protocol error handling to preserve error objects instead of string conversions, and implements automatic contract refresh with retry logic for document fetches when stale contracts are detected during deserialization.

Changes

Cohort / File(s) Summary
Context Provider Interface
packages/rs-context-provider/src/provider.rs
Added update_data_contract method with default no-op implementation to trait, and forwarding implementations for AsRef<dyn ContextProvider> and Mutex<T> wrappers.
Context Provider Implementations
packages/rs-sdk-trusted-context-provider/src/provider.rs, packages/rs-sdk/src/mock/provider.rs, packages/wasm-sdk/src/context_provider.rs
Implemented update_data_contract method in TrustedHttpContextProvider, GrpcContextProvider, and WasmTrustedContext to cache/store incoming DataContracts.
Protocol Error Refactoring
packages/rs-drive-proof-verifier/src/error.rs
Changed ProtocolError variant from struct-like { error: String } to tuple-like (ProtocolError), updated From<> implementations and MapGroveDbError to route protocol errors correctly while preserving string representations for other drive errors.
Error Wrapping Updates
packages/rs-drive-proof-verifier/src/proof.rs, packages/rs-drive-proof-verifier/src/unproved.rs
Updated error construction patterns in proof parsing to use the new ProtocolError and ResponseDecodeError variants, replacing string-based conversions with proper error type handling.
Smart Contract Refresh Logic
packages/rs-sdk/src/platform/fetch.rs
Centralized fetch request execution, added refetch_contract_for_query helper to fetch fresh contracts from network and update cache, and is_document_deserialization_error detector. Document fetch now retries on deserialization errors after refreshing the contract.
Fetch Many Enhancements
packages/rs-sdk/src/platform/fetch_many.rs
Introduced fetch_many_request helper for unified request handling, updated fetch_many_with_metadata_and_proof signature to accept optional RequestSettings and return metadata/proof tuple, implemented two-step retry strategy with contract refresh for Documents.
Query Utilities
packages/rs-sdk/src/platform/documents/document_query.rs
Added clone_with_contract method to create DocumentQuery copies with substituted contracts while preserving filters and clauses.
Mock SDK Infrastructure
packages/rs-sdk/src/mock/sdk.rs
Extended MockDashPlatformSdk with proof_error_expectations tracking, expect_fetch_proof_error method to configure deserialization error simulations, and updated proof parsing to inject simulated errors for testing.
Test Coverage
packages/rs-sdk/tests/fetch/mock_document_contract_refresh.rs, packages/rs-sdk/tests/fetch/mod.rs
Added comprehensive test module for contract refresh flows: verifies Document::fetch and fetch_many retries on stale contract errors, validates error propagation without retry for non-deserialization errors, includes helpers for contract/document setup.

Sequence Diagram

sequenceDiagram
    participant Client as SDK Consumer
    participant SDK as Sdk
    participant Cache as ContextProvider<br/>(Cache)
    participant Network as Network/DAPI
    participant Parser as FromProof<br/>Parser

    Client->>SDK: fetch_document(query with old_contract)
    SDK->>Cache: get cached old_contract
    Cache-->>SDK: returns old_contract
    
    SDK->>Network: fetch with old_contract
    Network-->>Parser: send proof
    
    Parser->>Parser: attempt parse with old_contract
    Parser-->>SDK: ProtocolError(CorruptedSerialization)
    
    rect rgba(255, 100, 100, 0.5)
    Note over SDK: Detect deserialization error
    SDK->>Network: refetch_contract_for_query()
    Network-->>SDK: returns fresh_contract
    SDK->>Cache: update_data_contract(fresh_contract)
    Cache->>Cache: store in contract cache
    end
    
    rect rgba(100, 200, 100, 0.5)
    Note over SDK: Retry with fresh contract
    SDK->>SDK: clone_with_contract(fresh_contract)
    SDK->>Network: fetch_document(query with fresh_contract)
    Network-->>Parser: send proof
    Parser->>Parser: parse with fresh_contract
    Parser-->>SDK: Success(Document)
    end
    
    SDK-->>Client: Document
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A contract grew stale in the cache so deep,
But our clever SDK won't lose a wink of sleep!
It detects the corruption, fetches anew with care,
Retries with fresh data—proof verified fair! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main objective: fixing SDK deserialization errors caused by an outdated contract cache. It is concise, specific, and reflects the core problem being addressed.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/sdk/outdated-data-contract-cache

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 and usage tips.

@github-actions
Copy link

github-actions bot commented Feb 4, 2026

✅ gRPC Query Coverage Report

================================================================================
gRPC Query Coverage Report - NEW QUERIES ONLY
================================================================================

Total queries in proto: 53
Previously known queries: 47
New queries found: 6

================================================================================

New Query Implementation Status:
--------------------------------------------------------------------------------
✓ getAddressInfo                                /home/runner/work/platform/platform/packages/rs-sdk/src/platform/query.rs
✓ getAddressesBranchState                       /home/runner/work/platform/platform/packages/rs-sdk/src/platform/address_sync/mod.rs
✓ getAddressesInfos                             /home/runner/work/platform/platform/packages/rs-sdk/src/platform/fetch_many.rs
✓ getAddressesTrunkState                        /home/runner/work/platform/platform/packages/rs-sdk/src/platform/query.rs
✓ getRecentAddressBalanceChanges                /home/runner/work/platform/platform/packages/rs-sdk/src/platform/query.rs
✓ getRecentCompactedAddressBalanceChanges       /home/runner/work/platform/platform/packages/rs-sdk/src/platform/query.rs

================================================================================
Summary:
--------------------------------------------------------------------------------
New queries implemented: 6 (100.0%)
New queries missing: 0 (0.0%)

Total known queries: 53
  - Implemented: 50
  - Not implemented: 2
  - Excluded: 1

Not implemented queries:
  - getConsensusParams
  - getTokenPreProgrammedDistributions

@shumkov shumkov self-assigned this Feb 4, 2026
@shumkov shumkov moved this to In review / testing in Platform team Feb 4, 2026
@github-actions github-actions bot added this to the v3.1.0 milestone Feb 4, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/rs-sdk/tests/fetch/mock_document_contract_refresh.rs`:
- Around line 114-235: Rename the three tests so their function names start with
"should_" per guidelines: change test_fetch_document_retries_on_stale_contract
to should_fetch_document_retries_on_stale_contract,
test_fetch_many_documents_retries_on_stale_contract to
should_fetch_many_documents_retries_on_stale_contract, and
test_fetch_document_does_not_retry_on_other_errors to
should_fetch_document_does_not_retry_on_other_errors; update the async fn
identifiers accordingly wherever they are referenced (e.g., in this file) so the
test runner and imports continue to work.

Comment on lines +114 to +235
/// Test: Document::fetch retries once when the first attempt fails with a deserialization error.
///
/// Setup:
/// 1. Create old and new versions of a data contract
/// 2. Build a DocumentQuery using the OLD contract
/// 3. Mock the document query with old contract to return a CorruptedSerialization error
/// 4. Mock DataContract::fetch to return the NEW contract
/// 5. Mock the document query with new contract to return the expected document
/// 6. Verify that Document::fetch succeeds (retry worked)
#[tokio::test]
async fn test_fetch_document_retries_on_stale_contract() {
let mut sdk = Sdk::new_mock();

let (old_contract, new_contract, new_doc_type) = make_old_and_new_contracts();

let expected_document = new_doc_type
.random_document(None, sdk.version())
.expect("create document");
let document_id = expected_document.id();

// Build a query using the old contract (simulates stale cache)
let old_query = DocumentQuery::new(old_contract.clone(), "document_type_name")
.expect("create document query with old contract")
.with_document_id(&document_id);

// 1) Old query should fail with a deserialization error
sdk.mock()
.expect_fetch_proof_error::<Document, _>(old_query.clone())
.await
.expect("set error expectation");

// 2) DataContract::fetch should return the new contract (refetch)
sdk.mock()
.expect_fetch(new_contract.id(), Some(new_contract.clone()))
.await
.expect("set contract refetch expectation");

// 3) New query (with fresh contract) should succeed
let new_query = old_query.clone_with_contract(std::sync::Arc::new(new_contract));
sdk.mock()
.expect_fetch(new_query, Some(expected_document.clone()))
.await
.expect("set document fetch expectation with new contract");

// Execute: the retry logic should handle the error and succeed
let result = Document::fetch(&sdk, old_query)
.await
.expect("fetch should succeed after retry");

let fetched = result.expect("document should be returned");
assert_eq!(fetched.id(), expected_document.id());
}

/// Test: Document::fetch_many retries once when the first attempt fails with a deserialization error.
#[tokio::test]
async fn test_fetch_many_documents_retries_on_stale_contract() {
let mut sdk = Sdk::new_mock();

let (old_contract, new_contract, new_doc_type) = make_old_and_new_contracts();

let expected_document = new_doc_type
.random_document(None, sdk.version())
.expect("create document");

let expected_documents =
Documents::from([(expected_document.id(), Some(expected_document.clone()))]);

// Build a query using the old contract (simulates stale cache)
let old_query = DocumentQuery::new(old_contract.clone(), "document_type_name")
.expect("create document query with old contract");

// 1) Old query should fail with a deserialization error
// We use expect_fetch_proof_error with Document since DocumentQuery is the
// request type for both Fetch and FetchMany
sdk.mock()
.expect_fetch_proof_error::<Document, _>(old_query.clone())
.await
.expect("set error expectation");

// 2) DataContract::fetch should return the new contract (refetch)
sdk.mock()
.expect_fetch(new_contract.id(), Some(new_contract.clone()))
.await
.expect("set contract refetch expectation");

// 3) New query (with fresh contract) should succeed
let new_query = old_query.clone_with_contract(std::sync::Arc::new(new_contract));
sdk.mock()
.expect_fetch_many(new_query, Some(expected_documents.clone()))
.await
.expect("set document fetch_many expectation with new contract");

// Execute: the retry logic should handle the error and succeed
let result = Document::fetch_many(&sdk, old_query)
.await
.expect("fetch_many should succeed after retry");

assert_eq!(result.len(), 1);
let (doc_id, doc) = result.into_iter().next().unwrap();
assert_eq!(doc_id, expected_document.id());
assert!(doc.is_some());
}

/// Test: When the error is NOT a deserialization error, no retry happens and the error propagates.
#[tokio::test]
async fn test_fetch_document_does_not_retry_on_other_errors() {
let sdk = Sdk::new_mock();

let doc_type = mock_document_type();
let contract = mock_data_contract(Some(&doc_type));

// Build a query but don't set any expectations — the mock will have no matching
// response for execute, which should result in an error that is NOT a deserialization error.
let query = DocumentQuery::new(contract, doc_type.name()).expect("create document query");

let result = Document::fetch(&sdk, query).await;

// Should fail — non-deserialization errors propagate without retry
assert!(
result.is_err(),
"expected error when no mock expectations are set"
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Rename new tests to start with should_… for guideline compliance.

✏️ Suggested renames
-async fn test_fetch_document_retries_on_stale_contract() {
+async fn should_retry_document_fetch_on_stale_contract() {

-async fn test_fetch_many_documents_retries_on_stale_contract() {
+async fn should_retry_fetch_many_documents_on_stale_contract() {

-async fn test_fetch_document_does_not_retry_on_other_errors() {
+async fn should_not_retry_document_fetch_on_other_errors() {

As per coding guidelines: **/tests/**/*.{js,jsx,ts,tsx,rs}: Name tests descriptively, starting with 'should …'.

🤖 Prompt for AI Agents
In `@packages/rs-sdk/tests/fetch/mock_document_contract_refresh.rs` around lines
114 - 235, Rename the three tests so their function names start with "should_"
per guidelines: change test_fetch_document_retries_on_stale_contract to
should_fetch_document_retries_on_stale_contract,
test_fetch_many_documents_retries_on_stale_contract to
should_fetch_many_documents_retries_on_stale_contract, and
test_fetch_document_does_not_retry_on_other_errors to
should_fetch_document_does_not_retry_on_other_errors; update the async fn
identifiers accordingly wherever they are referenced (e.g., in this file) so the
test runner and imports continue to work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In review / testing

Development

Successfully merging this pull request may close these issues.

1 participant