Skip to content

fix(sdk): persist the contract bounds kind on Android and iOS - #4800

Open
QuantumExplorer wants to merge 2 commits into
v4.2-devfrom
fix/contract-group-bounds-kind-mobile
Open

QuantumExplorer wants to merge 2 commits into
v4.2-devfrom
fix/contract-group-bounds-kind-mobile

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Sep 17, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

Follow-up to #4793, which has merged; this branch is rebased onto v4.2-dev and carries only the two mobile commits. Until it merges, v4.2-dev emits a bounds kind the Kotlin and Swift layers do not know. #4793 lets an AUTHENTICATION key carry contract bounds naming a contract group, which crosses the wallet FFI as contract_bounds_kind == 3 with the group id and no document type.

Neither mobile store can round-trip that. Both persist a key's bounds as an id plus an optional document type name and infer the kind on restore, so a group bound comes back as a single-contract bound to the group id on Android (the local signing preflight then refuses valid batches until the identity is refreshed) and unbounded on Swift. Consensus enforces the stored bounds either way; this is about the wallet's own view of the key.

Split out of #4793 so the Rust change and the mobile persistence can be reviewed separately.

What was done?

Android (kotlin-sdk)

  • public_keys gains a nullable contractBoundsKind column. Database version 11 to 12 with a manual MIGRATION_11_12, in the style of the earlier bumps; 12.json was generated by Room.
  • The persist callback records the kind the native row carries. Restore honours a stored kind and keeps the old inference only for legacy rows where it is null. A stored kind 2 without its document type demotes to 1, as the Rust loader does.
  • ContractBounds.ContractGroup in the SDK type, kind 3 in IdentityPubkeyCodec (an id and no document type), kind docs updated in the bridges.

iOS (swift-sdk)

  • PersistentPublicKey gains an optional contractBoundsKind; a nil kind marks a legacy row and keeps the old inference. The persist path stores the kind the FFI row carries, the restore path emits it (kind 3 with the id and no document type).
  • ManagedPlatformWallet.ContractBounds gains contractGroup(id:) with the pin and parse paths for kind 3. The example app's storage inspector shows the bounds variant.
  • Schema version cut, the part to look at. Following the rule written in DashModelContainer ("when the next property lands: freeze every model here into the version being retired, add a version, add a stage, commit a store written by this build"), the live models move to schema version 5, version 4 is frozen with scripts/freeze_schema_models.py (36 generated files, reproducible with --check), the plan gains a lightweight 4 to 5 stage, and dash-v5.store was written by this build.

Decision for reviewers: is schema version 4 shipped?

Version 4 arrived with #4589 on 2026-09-08 and has only been in the 4.2 dev tags. The migration tests treat it as shipped ("adding a version to the plan is shipping it"), which is why this PR cuts version 5. The same doc also allows changing the live version before it ships by rewriting its fixture on purpose. If dev builds do not count as shipped, this shrinks to: the column on the live version 4 model plus a regenerated dash-v4.store, with no version 5 and none of the 36 frozen files, at the cost that stores written by dev builds since 2026-09-08 fail to open and need a reset. Say which and I will change it.

How Has This Been Tested?

  • Kotlin: the SDK's full JVM unit suite (479 tests), including the kind 3 round trip through the persistence handler, kinds 0 to 2 unchanged, legacy inference, the codec bytes and the schema version and column. The 11 to 12 Room migration test compiles but is instrumented, so CI or a device has to run it.
  • Swift: swift test for the migration suite (14, including the new 4 to 5 migration and the fixture) and the new bounds suite (8); scripts/freeze_schema_models.py --check (73 frozen files match) with its script tests; an xcodebuild of SwiftExampleApp for the simulator with warnings as errors.
  • Before the rebase the tree of this branch was byte-identical to 287e8aebb7, the head of feat(platform)!: allow authentication keys bound to a contract group #4793 before the split, where every CI workflow passed, including the Kotlin SDK build, the Swift SDK build and tests, and the Swift frozen schema check.

Breaking Changes

None for consensus. Local stores migrate forward: Room 11 to 12, SwiftData schema 4 to 5 (lightweight).

Notes for reviewers

  • The Swift DPP-layer ContractBounds enum deliberately gets no group case: generated frozen schema copies switch over it exhaustively, so that projection reports no bounds for a group-bound row rather than a false single contract. The FFI restore path carries kind 3.
  • Neither example app can create a group-bound key yet; both only offer bounds for encryption and decryption keys. A group picker for authentication keys is a follow-up.

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

@coderabbitai

coderabbitai Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 59 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 1001c9b6-f050-4a99-b2a0-415a4452b2fb

📥 Commits

Reviewing files that changed from the base of the PR and between da14366 and 337cf7b.

📒 Files selected for processing (58)
  • packages/kotlin-sdk/sdk/schemas/org.dashfoundation.dashsdk.persistence.DashDatabase/12.json
  • packages/kotlin-sdk/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/persistence/DashDatabaseMigrationTest.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/NativePersistenceBridge.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/TransactionsNative.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/identity/IdentityPubkeyCodec.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/identity/IdentityUpdates.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/DashDatabase.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/PlatformWalletPersistenceHandler.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/entities/PublicKeyEntity.kt
  • packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/identity/RegistrationKeysTest.kt
  • packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/persistence/DashDatabaseTest.kt
  • packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/persistence/PlatformWalletPersistenceHandlerTest.kt
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentAccount.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentAssetLock.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentCoreAddress.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentDPNSName.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentDashpayContactProfile.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentDashpayContactRequest.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentDashpayIgnoredSender.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentDashpayPayment.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentDashpayProfile.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentDataContract.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentDocument.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentDocumentType.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentIdentity.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentIndex.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentInvitation.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentKeyword.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentMasternode.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentPendingInput.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentPlatformAddress.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentPlatformAddressesSyncState.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentProperty.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentPublicKey.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentShieldedActivity.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentShieldedNote.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentShieldedOutgoingNote.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentShieldedSyncState.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentShieldedViewingKey.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentToken.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentTokenBalance.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentTokenHistoryEvent.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentTrackedMasternode.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentTransaction.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentTxo.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentWallet.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+PersistentWalletManagerMetadata.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/FrozenSchemas/DashSchemaV4+TokenTypes.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentPublicKey.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/StorageRecordDetailViews.swift
  • packages/swift-sdk/SwiftTests/SwiftDashSDKTests/DashModelMigrationTests.swift
  • packages/swift-sdk/SwiftTests/SwiftDashSDKTests/Fixtures/SchemaStores/dash-v5.store
  • packages/swift-sdk/SwiftTests/SwiftDashSDKTests/IdentityKeyContractBoundsTests.swift
  • packages/swift-sdk/scripts/freeze_schema_models.py
  • packages/swift-sdk/scripts/test_freeze_schema_models.py

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 commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

⚠️ DEGRADED — Final review complete — no blockers (commit 337cf7b) · triage: normal · stand-in models (primary models out of quota)

Base automatically changed from feat/contract-group-key-bounds to v4.2-dev September 17, 2026 13:38
@github-actions github-actions Bot added this to the v4.2.0 milestone Sep 17, 2026
QuantumExplorer and others added 2 commits September 17, 2026 21:24
…nd-trip

Android stored a key's bounds as an id plus an optional document type name
and inferred the kind on restore, so a key bound to a contract group (kind 3)
came back as a single-contract bound to the group id and the local signing
preflight refused valid batches until the identity was refreshed.

- public_keys gains a nullable contractBoundsKind column (schema 12, manual
  MIGRATION_11_12 in the style of the earlier bumps). The persist callback
  records the kind the native row carries; restore honours it and keeps the
  old inference for legacy rows with a NULL kind.
- ContractBounds.ContractGroup in the SDK type, kind 3 in IdentityPubkeyCodec
  (an id and no document type), kind docs updated in the bridges and the JNI
  blob layout.
- Tests: kind 3 and every other kind round-trip through the handler, legacy
  rows still infer 0, 1 and 2, the codec bytes for kind 3, the schema version
  and column, and an 11 to 12 migration test (instrumented, compiled only).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d-trip

Swift stored a key's bounds as an id plus an optional document type name and
inferred the kind on restore, and the persist callback dropped kinds it did
not know. A key bound to a contract group (kind 3) therefore restored
unbounded after a restart.

- PersistentPublicKey gains an optional contractBoundsKind. The live models
  move to schema version 5: version 4 is frozen with the project's freeze
  script (36 generated files, reproducible with --check), the plan gains a
  lightweight 4 to 5 stage, and a version 5 store fixture written by this
  build joins the migration tests. A nil kind marks a legacy row and keeps
  the old inference.
- The persist path stores the kind the FFI row carries, including 3; the
  restore path emits the stored kind (kind 3 with the id and no document
  type, a kind 2 row without its name demoted to 1 as Rust does).
- ManagedPlatformWallet.ContractBounds gains contractGroup(id:), with the
  pin and parse paths for kind 3. The DPP-layer bounds enum is unchanged
  (generated frozen schemas switch over it exhaustively), so that projection
  reports no bounds for a group-bound row instead of a false single contract.
- The example app's storage inspector shows the bounds variant.
- Tests: kind 3 round trip, kinds 0 to 2 unchanged, legacy inference,
  unusable rows, pin and parse of kind 3, unknown kinds still rejected, and
  the version 4 to 5 migration.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@QuantumExplorer
QuantumExplorer force-pushed the fix/contract-group-bounds-kind-mobile branch from a5121a6 to 337cf7b Compare September 17, 2026 14:25
@github-actions

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

PR Hygiene

State: waiting-bots · commit 337cf7b055a837914dc773fea6d5e14395b9af5f

  • Bot review threads remain unresolved
  • Proceeded without coderabbitai: no review within the configured window

Self-review is an author attestation that you have read the diff:
/self-reviewed — covers everything pushed so far; post it again after a new push.

This report does not bypass CI or repository protection rules.

@github-actions

Copy link
Copy Markdown
Contributor

@coderabbitai review

No review for 337cf7b0 yet, so PR Hygiene is asking once. If nothing arrives, the requirement is dropped for this commit and the pull request is labelled bot-review-missed.

@github-actions

Copy link
Copy Markdown
Contributor

@thepastaclaw review

No review for 337cf7b0 yet, so PR Hygiene is asking once. If nothing arrives, the requirement is dropped for this commit and the pull request is labelled bot-review-missed.

@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.

⚠️ DEGRADED — Final validation — Phase 1 + Phase 2

⚠️ DEGRADED review. The primary review models were unavailable (gpt-6-astra unavailable: All credentials for model gpt-6-astra are cooling down (last error: usage_limit_reached: The usage limit has been reache), so this review ran on stand-in models: gpt-5.6-lunamuse-spark-1.3-contributor, gpt-5.6-solmuse-spark-1.3-contributor, gpt-5.6-terramuse-spark-1.3-contributor, gpt-6-astramuse-spark-1.3-contributor. Both review phases and the independent verifiers still ran, but on weaker models, with Phase 1 capped at high effort. Treat the verdict as provisional; a full-strength re-review will run on the next push once the primary models are back.

Mobile-only follow-up that correctly round-trips the FFI contract-bounds kind 3 (ContractGroup) through the Room 11→12 column and the SwiftData V4→V5 schema. Two in-scope suggestions remain: the Kotlin restore path coerces unknown/stale stored kinds to SingleContract while Swift degrades them to unbounded, and the Swift contractBounds setter comment no longer describes the kind column it now resets.

🟡 2 suggestion(s)

Review provenance

Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: architecture-layering); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: ffi-engineer); reviewer 3: muse-spark-1.3-contributor (agent: phase1-reviewer, role: security-auditor); reviewer 4: muse-spark-1.3-contributor (standing in for gpt-6-astra) (agent: phase2-reviewer, role: general); reviewer 5: muse-spark-1.3-contributor (standing in for gpt-6-astra) (agent: phase2-reviewer, role: architecture-layering); reviewer 6: muse-spark-1.3-contributor (standing in for gpt-6-astra) (agent: phase2-reviewer, role: security-auditor); final verifier: muse-spark-1.3-contributor (standing in for gpt-6-astra) (agent: astra-verifier, role: final-verifier)

  • Degraded mode: gpt-6-astra unavailable: All credentials for model gpt-6-astra are cooling down (last error: usage_limit_reached: The usage limit has been reache (detected by probe, since 2026-09-18T05:22:01Z); stand-ins gpt-5.6-lunamuse-spark-1.3-contributor, gpt-5.6-solmuse-spark-1.3-contributor, gpt-5.6-terramuse-spark-1.3-contributor, gpt-6-astramuse-spark-1.3-contributor; Phase 1 effort capped at high
  • Triage: normal by muse-spark-1.3-contributor (standing in for gpt-6-astra) (effort low) — Adds a nullable contractBoundsKind column with Room 11-to-12 and SwiftData 4-to-5 migrations plus codec round-trip handling, a contained wallet-persistence fix that does not change consensus, funds, crypto, or signing logic.
  • Phase 1 reviewers: muse-spark-1.3-contributor — architecture-layering (completed, effort high); agent phase1-reviewer, muse-spark-1.3-contributor — ffi-engineer (completed, effort high); agent phase1-reviewer, muse-spark-1.3-contributor — security-auditor (completed, effort high); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-3.8-flash-high (lane failed), glm-5.3-flash (zai below 15% reserve: 5h 99% left, weekly 14% left)
  • Fresh verifier: muse-spark-1.3-contributor (standing in for gpt-6-astra) — final-verifier; agent astra-verifier
  • Phase 2 reviewers: muse-spark-1.3-contributor (standing in for gpt-6-astra) — general (completed, effort high); agent phase2-reviewer, muse-spark-1.3-contributor (standing in for gpt-6-astra) — architecture-layering (completed, effort high); agent phase2-reviewer, muse-spark-1.3-contributor (standing in for gpt-6-astra) — security-auditor (completed, effort high); agent phase2-reviewer
🤖 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/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/PlatformWalletPersistenceHandler.kt`:
- [SUGGESTION] packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/PlatformWalletPersistenceHandler.kt:2868-2875: Unknown or zero stored bounds kind coerced to SingleContract instead of degrading to unbounded
  The restore branch only matches stored kinds 3 and 1 explicitly, then falls through to doc-type inference (`docType != null → 2 else → 1`). A row with a decodable id blob and a stored kind of 0 or any future/unknown kind therefore restores as kind 1 (or 2 when a doc-type string happens to be present) — asserting a SingleContract bound the key does not have. The Swift restore path in the same PR (`(1...3).contains(boundsKind)`, else kind 0) degrades those rows to unbounded, so the two platforms disagree on the same row, and the persist side stores the native kind verbatim, so a future kind would be stored then mis-restored on Android only. Branch on the stored kind first and degrade anything unrecognized to 0; also use isNullOrEmpty so an empty-string doc-type does not restore as kind 2 on Android while Swift demotes it to 1.

In `packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentPublicKey.swift`:
- [SUGGESTION] packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentPublicKey.swift:131-135: contractBounds setter comment omits the kind column it now resets
  The setter now also resets `contractBoundsKind` (0/1), but the comment still tells callers that writing `contractBoundsDocumentTypeName` after the setter is enough for a `.singleContractDocumentType` round-trip. Since `effectiveContractBoundsKind` prefers the stored kind, a doc-type written after this setter without also writing kind 2 restores as `.singleContract` and silently drops the qualifier. The only current caller overwrites the kind afterwards, so nothing is broken today, but the guidance is wrong for the next caller — update it in the same diff that changed the setter body.
Out-of-scope follow-up suggestions (1)

These are valid observations, but they are outside this PR's scope and should be handled in separate issues or author/maintainer-requested PRs rather than blocking this review.

  • Unknown future bounds kinds fail open in the local wallet view — Speculative hardening for a kind that does not exist yet; the actionable part (Kotlin degrading unknown kinds to 0 like Swift) is kept as an in-scope suggestion above, and the remainder asks for a separate fail-closed redesign tracked outside this PR.
    • Follow-up: Consider creating a separate issue or author/maintainer-requested PR for this.

Comment on lines 2868 to 2875
val boundsId = pk.contractBoundsData?.let { contractBoundsJsonToId(it) }
val (kind, id) = when {
boundsId == null -> 0.toByte() to ByteArray(0)
pk.contractBoundsKind == 3 -> 3.toByte() to boundsId
pk.contractBoundsKind == 1 -> 1.toByte() to boundsId
pk.contractBoundsDocumentTypeName != null -> 2.toByte() to boundsId
else -> 1.toByte() to boundsId
}

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: Unknown or zero stored bounds kind coerced to SingleContract instead of degrading to unbounded

The restore branch only matches stored kinds 3 and 1 explicitly, then falls through to doc-type inference (docType != null → 2 else → 1). A row with a decodable id blob and a stored kind of 0 or any future/unknown kind therefore restores as kind 1 (or 2 when a doc-type string happens to be present) — asserting a SingleContract bound the key does not have. The Swift restore path in the same PR ((1...3).contains(boundsKind), else kind 0) degrades those rows to unbounded, so the two platforms disagree on the same row, and the persist side stores the native kind verbatim, so a future kind would be stored then mis-restored on Android only. Branch on the stored kind first and degrade anything unrecognized to 0; also use isNullOrEmpty so an empty-string doc-type does not restore as kind 2 on Android while Swift demotes it to 1.

Suggested change
val boundsId = pk.contractBoundsData?.let { contractBoundsJsonToId(it) }
val (kind, id) = when {
boundsId == null -> 0.toByte() to ByteArray(0)
pk.contractBoundsKind == 3 -> 3.toByte() to boundsId
pk.contractBoundsKind == 1 -> 1.toByte() to boundsId
pk.contractBoundsDocumentTypeName != null -> 2.toByte() to boundsId
else -> 1.toByte() to boundsId
}
val boundsId = pk.contractBoundsData?.let { contractBoundsJsonToId(it) }
val storedKind = pk.contractBoundsKind
val (kind, id) = when {
boundsId == null -> 0.toByte() to ByteArray(0)
storedKind == 3 -> 3.toByte() to boundsId
storedKind == 1 -> 1.toByte() to boundsId
storedKind == 2 && !pk.contractBoundsDocumentTypeName.isNullOrEmpty() -> 2.toByte() to boundsId
storedKind == 2 -> 1.toByte() to boundsId
storedKind == 0 -> 0.toByte() to ByteArray(0)
storedKind == null && !pk.contractBoundsDocumentTypeName.isNullOrEmpty() -> 2.toByte() to boundsId
storedKind == null -> 1.toByte() to boundsId
else -> 0.toByte() to ByteArray(0)
}

source: muse-spark-1.3-contributor (phase1-reviewer: architecture-layering, ffi-engineer, security-auditor); muse-spark-1.3-contributor (phase2-reviewer: general, architecture-layering, security-auditor)

Comment on lines 132 to +135
// round-trip should write `contractBoundsDocumentTypeName`
// explicitly after this setter, or go through
// `PersistentPublicKey.from(IdentityPublicKey, identityId:)`
// which sets both columns atomically.
// which sets every column atomically.

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: contractBounds setter comment omits the kind column it now resets

The setter now also resets contractBoundsKind (0/1), but the comment still tells callers that writing contractBoundsDocumentTypeName after the setter is enough for a .singleContractDocumentType round-trip. Since effectiveContractBoundsKind prefers the stored kind, a doc-type written after this setter without also writing kind 2 restores as .singleContract and silently drops the qualifier. The only current caller overwrites the kind afterwards, so nothing is broken today, but the guidance is wrong for the next caller — update it in the same diff that changed the setter body.

Suggested change
// round-trip should write `contractBoundsDocumentTypeName`
// explicitly after this setter, or go through
// `PersistentPublicKey.from(IdentityPublicKey, identityId:)`
// which sets both columns atomically.
// which sets every column atomically.
// Callers that want the full `.singleContractDocumentType`
// round-trip must write `contractBoundsDocumentTypeName` AND
// `contractBoundsKind` (2) explicitly after this setter — the
// stored kind wins over inference on restore, so a doc-type
// without kind 2 comes back as `.singleContract` — or go

source: muse-spark-1.3-contributor (phase2-reviewer: general)

@github-actions github-actions Bot added the bot-review-missed A required review bot did not report in time; it was waived. label Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot-review-missed A required review bot did not report in time; it was waived.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants