Skip to content

swift-sdk: the DPP-layer ContractBounds cannot represent a contract-group bound the wallet layer reports #4853

Description

@romchornyi

Expected Behavior

A Swift SDK consumer that reads a key's contract bounds from ManagedPlatformWallet and
converts them to the DPP-layer IdentityPublicKey should be able to represent every bound the
wallet layer can report. Either the DPP-layer ContractBounds models a contract-group bound, or
the SDK offers a conversion whose lossiness is part of its signature, so a consumer cannot lose
the bound by accident.

Current Behavior

packages/swift-sdk carries two ContractBounds enums with different shapes.

ManagedPlatformWallet.ContractBounds
has three cases since #4800:

case singleContract(id: Data)
case singleContractDocumentType(id: Data, documentTypeName: String)
case contractGroup(id: Data)          // kind == 3 on the FFI side

The top-level ContractBounds
— the one IdentityPublicKey.contractBounds is typed as — still has two, and its Codable
BoundType enumerates the same two.

The SDK's own persistence seam already documents the consequence and chooses to drop the bound
(PersistentPublicKey.toIdentityPublicKey()):

A ContractGroup bound (kind 3) has no variant on the DPP-layer ContractBounds, and its id
is a group id, not a contract id, so projecting it as .singleContract would claim a bound the
key does not have. Report no bounds instead; the row keeps the group bound for the FFI restore
path, which does model it.

and the reverse conversion
(PersistentPublicKey.from(_:identityId:))
notes that it "only ever writes kinds 0, 1 and 2".

Inside the SDK this is deliberate and safe, because the group bound survives in the store for the
FFI restore path. For a consumer outside the SDK it is neither. A consumer that maps
ManagedPlatformWallet.ContractBoundsContractBounds gets a non-exhaustive switch with no
correct arm to add: .contractGroup has nowhere to go. Its choices are to drop the bound — which
is what the SDK does internally, but without the store to fall back on — or to refuse the value.

This is not hypothetical. It broke dashpay/dashwallet-ios develop: after the app's local SDK
pin moved onto current v4.2-dev,
PlatformDashConnectDataSource.parse()
stopped compiling:

PlatformDashConnectDataSource.swift:246:33: error: switch must be exhaustive
note: add missing case: '.contractGroup(id: let id)'

Dropping the bound is not an option there. That mapping feeds the check that decides which
approved app a scanned key-registration belongs to; a key whose bound is silently erased "names
no single app", and the app also rebuilds an IdentityPubkey from the same value to hand back to
updateIdentity(...), so a dropped bound would mean signing a weaker transition than the dApp
asked for. The app therefore has to refuse contract-group-bound keys outright.

Possible Solution

Root cause: #4800 extended the wallet-layer enum to carry FFI kind 3, but the DPP-layer enum it
is routinely converted into was left at two cases, and nothing in the SDK's public surface marks
the conversion as lossy.

Preferred fix — add the variant to the DPP-layer enum:

  • case contractGroup(id: Identifier) on ContractBounds, plus the matching BoundType case and
    Codable arms.
  • Update PersistentPublicKey.toIdentityPublicKey() / .from(_:identityId:) to carry kind 3
    instead of flattening it to "no bounds", so a SwiftData round-trip stops weakening the bound.
  • Audit description (CustomStringConvertible) and any other exhaustive switch over the enum.

This is source-breaking for anything switching over ContractBounds, so it wants the same
feat(sdk)! treatment #4800 and #4811 had.

Alternative, if the DPP-layer enum must stay pinned to what rs-dpp models: expose an explicit
converter (e.g. ManagedPlatformWallet.ContractBounds.asDPPBounds() throws -> ContractBounds)
whose signature makes the failure visible, and say in the doc comment that kind 3 has no DPP
representation. That keeps the enums as they are but stops each consumer from re-deriving the
same lossy decision on its own.

This needs a maintainer's call: whether the DPP-layer enum is meant to mirror rs-dpp exactly, or
to be the SDK's Swift-facing model of a key's bounds.

Steps to Reproduce (for bugs)

  1. Check out v4.2-dev at 72b58f6073 (any commit at or after fix(sdk): persist the contract bounds kind on Android and iOS #4800, 5568dfa443).
  2. In a Swift consumer of the SDK, exhaustively switch over a
    ManagedPlatformWallet.IdentityPubkey's contractBounds and return a DPP-layer
    ContractBounds from each arm — the shape PlatformDashConnectDataSource.parse() uses.
  3. Build. The switch is non-exhaustive, and .contractGroup has no arm that can be written.

Against the app: check out dashpay/dashwallet-ios develop at 65d6aa101, point the local
package at packages/swift-sdk on v4.2-dev at 72b58f6073, rebuild
DashSDKFFI.xcframework with ./build_ios.sh --target sim, and build the dashpay scheme.

Context

Blocks building dashpay/dashwallet-ios develop against current v4.2-dev. The app-side
workaround — refusing a contract-group-bound key registration — is in dashwallet-ios#1138, but
it is a refusal, not support: DashConnect cannot accept such a key until the SDK can represent
the bound end to end. The same trap is waiting for any other consumer that converts between the
two enums.

Related: #4800 (added the wallet-layer variant), #4791 / #4793 (contract groups), #4811 (key
limits, the other source-breaking SDK change in the same window).

Your Environment

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions