Skip to content

fix: classify a stale funding reservation as NotBroadcast on the atomic send path - #1548

Open
HashEngineering wants to merge 1 commit into
feat/kotlin-sdk-phase-1from
fix/stale-reservation-send-classification
Open

fix: classify a stale funding reservation as NotBroadcast on the atomic send path#1548
HashEngineering wants to merge 1 commit into
feat/kotlin-sdk-phase-1from
fix/stale-reservation-send-classification

Conversation

@HashEngineering

@HashEngineering HashEngineering commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

What

Adds a typed StaleReservationToken arm to classifyCoreSendFailure, so a stale funding reservation on the atomic send path classifies NotBroadcast instead of Ambiguous.

Why

dashpay/platform#4309 age-guards the finalized-transaction handle path (finalizebroadcastTransaction) against the same reservation bound the deferred BIP70 surface already used, and deliberately reuses native code 34 (ErrorStaleReservationToken) rather than minting a new one.

Until then, code 34 could only arrive from the deferred surface, so the arm lived only in classifyDeferredBroadcastFailure. After #4309 it also reaches:

  • SdkL1SendService.sendToAddressManagedPlatformWallet.sendToAddresses (the plain send)
  • CoreSendAllNativeManagedCoreWallet.broadcastTransaction (send-all / CoinJoin drain)

Neither is covered by the deferred classifier, so the error fell through classifyCoreSendFailure's message-prefix rules to classifyBroadcastFailure and came back Ambiguous.

That is wrong in the direction that costs the user most. The SDK contract guarantees this refusal happens before the broadcaster is touched, and the guard releases the still-owned reservation owner-guarded on the way out, so the inputs are immediately reselectable. Classifying it Ambiguous shows a payment that provably never left the device as "may be on the network" and blocks the safe dashj fallback — the exact failure mode the existing typed arms (CoreInsufficientFunds, SigningKeyUnavailable, TransactionBroadcastRejected) were added to prevent.

Changes

  • SdkL1SendService.kt — typed StaleReservationToken arm in classifyCoreSendFailure, placed with the other definitively-pre-network arms, plus KDoc explaining why both classifiers keep an arm (the deferred one stays ahead only to name the deferred surface in the reason string).
  • SdkL1SendServiceTest.ktclassify_staleReservationToken_isNotBroadcast_onTheAtomicSendPath, and code 34 added to the deferred-table parity list.

Testing

:wallet:testProdDebugUnitTest --tests SdkL1SendServiceTest71 tests, 0 failures.

Verified non-vacuous: with the production arm reverted, the new test fails (AssertionError) and it is the only failure. The existing deferred-path test still passes without it, confirming the two classifiers are independently covered.

The new test also asserts the ambiguous sibling TransactionBroadcastUnconfirmed (code 20) keeps its Ambiguous classification, so the pair cannot collapse into one rule.

Notes

  • Safe to land before #4309. Against the current AAR the arm is unreachable, so this is a no-op until the native guard ships.
  • A second gap from #4309 is not addressed here and needs a platform-side change first: PlatformWalletError::InputMidBroadcast (the new build refusal when a coin selection picks an input held by an in-flight dispatch) maps to ErrorUnknown (99) and so also classifies Ambiguous. Fixing it by message-prefix matching would be fragile — the message embeds an outpoint — so the right fix is claiming a real FFI code upstream and adding a typed arm here. Not yet raised upstream.

…ic send path

dashpay/platform#4309 age-guards the atomic finalize -> broadcast path
against the same reservation bound the deferred surface already used, and
reuses native code 34 (ErrorStaleReservationToken) for it. That puts the
error on classifyCoreSendFailure's paths for the first time -- the plain
send (sendToAddresses) and the drain (CoreSendAllNative) -- where there was
no arm for it.

Without an arm it fell through to classifyBroadcastFailure and came back
Ambiguous, so a refusal the SDK contract guarantees never reached the
broadcaster would surface as "may be on the network" and block the safe
dashj fallback. The guard also releases the still-owned reservation
owner-guarded on the way out, so the freed inputs are immediately
reselectable by a rebuild.

classifyDeferredBroadcastFailure keeps its own arm ahead of this one purely
to name the deferred surface in the reason string.

Test fails without the production arm and passes with it; the ambiguous
sibling (code 20) is asserted to keep its Ambiguous classification so the
pair cannot collapse into one rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1f46355f-6dc4-4689-8e23-f655586bf2e2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant