Skip to content

Commit d78b940

Browse files
bfoss765claude
andcommitted
fix(platform-wallet-ffi)!: move ErrorShieldedInviteAlreadyClaimed 32 -> 37 and mirror it (#4204)
32 is allocated to `ErrorTransactionBuild` (#4247, also carried by #4256) in ERROR_CODE_REGISTRY.md (#4261). This variant took 32 without a registry row, so the two collide as a hard `E0081: discriminant value 32 assigned more than once` the moment both land — reproduced on a real integration merge, not hypothetical. 27-36 are all claimed (27 ErrorShutdownIncomplete via the merged #4268; 29 #4184; 31 #4183; 32/33 #4247/#4256; 34-36 the #4185 trio) and 28/30 are vacated-but-RESERVED, so 37 is the allocation frontier. The code was also unmirrored on BOTH hosts, which is the more dangerous half: Swift is exhaustive, so it surfaced as .errorUnknown and lost its identity; Kotlin fell through to Generic(32), and in any tree carrying #4185's ErrorReservationWalletMismatch = 32 it actively MISCLASSIFIED "shielded invite already claimed" as "reservation wallet mismatch". That matters on the claim-recovery path specifically — the error is raised from four sites in shielded/operations.rs, three inside the recovery function. Adds the typed Kotlin PlatformWallet.ShieldedInviteAlreadyClaimed (terminal, inherited isRetryable = false), the Swift enum case + init(ffi:) arm, a DashSdkErrorTest assertion pinning 37, and refreshes the stale Swift reservation comment the registry asked the next toucher to drop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b6992a5 commit d78b940

4 files changed

Lines changed: 66 additions & 6 deletions

File tree

packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/errors/DashSdkError.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,25 @@ sealed class DashSdkError(
239239
class NotFound(message: String, cause: Throwable? = null) :
240240
PlatformWallet(message, cause)
241241

242+
/**
243+
* `ErrorShieldedInviteAlreadyClaimed` (native code 37). A one-time-key
244+
* (shielded invitation) claim found the invitation note's nullifier
245+
* already spent on chain, and could NOT produce positive evidence that
246+
* this claim's Type-20 transition created an identity — the spend was
247+
* finalized to the creation-failure address, or another holder of the
248+
* same bearer one-time key won the race, or the id is not re-derivable.
249+
*
250+
* TERMINAL and NOT retryable (the inherited [isRetryable] `false`):
251+
* the note is consumed, so no retry can spend it again. Distinct from
252+
* [ShieldedCreateUnconfirmed], which means "executed, not yet
253+
* resolvable, hold the slot". No identity id is produced — this wallet
254+
* has no identity to hold a slot for, and claiming one would be the
255+
* false-ownership assertion this code exists to prevent. Hosts should
256+
* surface the invitation as spent rather than registering an identity.
257+
*/
258+
class ShieldedInviteAlreadyClaimed(message: String, cause: Throwable? = null) :
259+
PlatformWallet(message, cause)
260+
242261
/**
243262
* Any other `PlatformWalletFFIResultCode` without a dedicated type.
244263
* Carries the platform-wallet [nativeCode] (already de-offset) and
@@ -351,6 +370,10 @@ sealed class DashSdkError(
351370
// sniffing involved. (Codes 26-30 are reserved by sibling PRs
352371
// #4185 / #4184 — see PlatformWalletFFIResultCode.)
353372
31 -> PlatformWallet.SigningKeyUnavailable(message, cause)
373+
// ErrorShieldedInviteAlreadyClaimed. Allocated 37 (not 32, which
374+
// belongs to ErrorTransactionBuild — dashpay/platform#4247/#4256);
375+
// see packages/rs-platform-wallet-ffi/ERROR_CODE_REGISTRY.md.
376+
37 -> PlatformWallet.ShieldedInviteAlreadyClaimed(message, cause)
354377
else ->
355378
// @Deprecated fallback — see the code-6 arm; code 31 is the
356379
// real discriminator.

packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/errors/DashSdkErrorTest.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,19 @@ class DashSdkErrorTest {
9090
// The message must warn against retrying, like the broadcast sibling.
9191
assertTrue(spendUnconfirmed.message!!.contains("do NOT retry"))
9292

93+
// Code 37, NOT 32: 32 is ErrorTransactionBuild (dashpay/platform#4247,
94+
// #4256). This assertion is the mirror's guard against the collision —
95+
// if the Rust discriminant is ever moved back onto a claimed number,
96+
// the host silently reclassifies an already-claimed invite as some
97+
// other branch's error. See ERROR_CODE_REGISTRY.md (#4261).
98+
val inviteClaimed =
99+
DashSdkError.fromNative(DashSDKException(offset + 37, "nullifier already spent"))
100+
assertTrue(inviteClaimed is DashSdkError.PlatformWallet.ShieldedInviteAlreadyClaimed)
101+
assertFalse(
102+
"ShieldedInviteAlreadyClaimed is TERMINAL — the note is consumed",
103+
inviteClaimed.isRetryable,
104+
)
105+
93106
val broadcastUnconfirmed =
94107
DashSdkError.fromNative(DashSDKException(offset + 20, "ambiguous broadcast"))
95108
assertTrue(

packages/rs-platform-wallet-ffi/src/error.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,8 +214,15 @@ pub enum PlatformWalletFFIResultCode {
214214
/// this code exists to prevent. Hosts should surface the invitation as spent
215215
/// rather than registering any identity.
216216
///
217-
/// Code 32: 27-30 stay reserved for the in-flight branches noted above.
218-
ErrorShieldedInviteAlreadyClaimed = 32,
217+
/// Code 37 — the next free integer per the allocation frontier in
218+
/// `ERROR_CODE_REGISTRY.md` (dashpay/platform#4261). This variant briefly
219+
/// held 32, which is allocated to `ErrorTransactionBuild`
220+
/// (dashpay/platform#4247, also carried by #4256); the two collided as an
221+
/// `E0081` the moment both were merged. 27-36 are all claimed (27
222+
/// `ErrorShutdownIncomplete`, merged via #4268; 29 #4184; 31 #4183; 32/33
223+
/// #4247/#4256; 34-36 the #4185 deferred-token trio), and 28/30 are vacated
224+
/// but RESERVED, so 37 is the only correct allocation.
225+
ErrorShieldedInviteAlreadyClaimed = 37,
219226

220227
NotFound = 98, // Used exclusively for all the Option that are retuned as errors
221228
ErrorUnknown = 99,

packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletResult.swift

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,30 @@ public enum PlatformWalletResultCode: Int32, Sendable {
7070
/// released and a corrected transaction may be submitted again.
7171
case errorTransactionBroadcastRejected = 26
7272
// Raw value 26 above (errorTransactionBroadcastRejected) landed on
73-
// v4.1-dev. Raw values 27-28 remain reserved for the deferred-payment
74-
// reservation-token errors (dashpay/platform#4185, which must renumber off
75-
// 26) and 29/30 for the asset-lock funding errors
76-
// (dashpay/platform#4184) on sibling branches.
73+
// v4.1-dev. 27-36 are claimed by sibling branches and MUST NOT be reused
74+
// here: 27 errorShutdownIncomplete (dashpay/platform#4268, merged), 29 the
75+
// asset-lock funding shortfall (#4184), 31 below (#4183), 32/33
76+
// errorTransactionBuild / errorTransactionSigning (#4247/#4256), 34-36 the
77+
// deferred-payment reservation-token trio (#4185). 28 and 30 are vacated
78+
// but RESERVED. These raw values MUST match `PlatformWalletFFIResultCode`
79+
// in packages/rs-platform-wallet-ffi/src/error.rs — there is no
80+
// compile-time check across the ABI. See ERROR_CODE_REGISTRY.md (#4261).
7781
/// A state transition could not be signed because the signer has no
7882
/// usable private key for the requested public key — restored from the
7983
/// structured signer completion code (dashpay/platform#4060 finding 7).
8084
/// Route to key repair; not retryable as-is.
8185
case errorSigningKeyUnavailable = 31
86+
/// A one-time-key (shielded invitation) claim found the invitation note's
87+
/// nullifier already spent on chain, with no positive evidence that this
88+
/// claim created an identity. TERMINAL and NOT retryable — the note is
89+
/// consumed, so no retry can spend it again, and no identity id is
90+
/// produced. Surface the invitation as spent.
91+
///
92+
/// Raw value 37 is the allocation frontier from ERROR_CODE_REGISTRY.md
93+
/// (dashpay/platform#4261): 32 belongs to `errorTransactionBuild`
94+
/// (#4247/#4256), 34-36 to the #4185 deferred-token trio, and 28/30 are
95+
/// vacated-but-reserved.
96+
case errorShieldedInviteAlreadyClaimed = 37
8297
case notFound = 98
8398
case errorUnknown = 99
8499

@@ -140,6 +155,8 @@ public enum PlatformWalletResultCode: Int32, Sendable {
140155
self = .errorTransactionBroadcastRejected
141156
case PLATFORM_WALLET_FFI_RESULT_CODE_ERROR_SIGNING_KEY_UNAVAILABLE:
142157
self = .errorSigningKeyUnavailable
158+
case PLATFORM_WALLET_FFI_RESULT_CODE_ERROR_SHIELDED_INVITE_ALREADY_CLAIMED:
159+
self = .errorShieldedInviteAlreadyClaimed
143160
case PLATFORM_WALLET_FFI_RESULT_CODE_NOT_FOUND:
144161
self = .notFound
145162
case PLATFORM_WALLET_FFI_RESULT_CODE_ERROR_UNKNOWN:

0 commit comments

Comments
 (0)