-
Notifications
You must be signed in to change notification settings - Fork 56
fix(platform-wallet): settle already-consumed asset locks and survive an ambiguous resume broadcast #4337
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v4.2-dev
Are you sure you want to change the base?
fix(platform-wallet): settle already-consumed asset locks and survive an ambiguous resume broadcast #4337
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,7 +65,9 @@ use dash_sdk::platform::transition::put_identity::PutIdentity; | |
| use dash_sdk::platform::transition::put_settings::PutSettings; | ||
| use dash_sdk::platform::transition::top_up_identity::TopUpIdentity; | ||
|
|
||
| use crate::error::{is_instant_lock_proof_invalid, PlatformWalletError}; | ||
| use crate::error::{ | ||
| asset_lock_already_consumed_out_point, is_instant_lock_proof_invalid, PlatformWalletError, | ||
| }; | ||
| use crate::wallet::asset_lock::orchestration::{ | ||
| out_point_from_proof, submit_with_cl_height_retry, FundingResolution, ResolvedFunding, | ||
| }; | ||
|
|
@@ -261,7 +263,15 @@ impl IdentityWallet { | |
| .await | ||
| .map_err(PlatformWalletError::Sdk)? | ||
|
Comment on lines
263
to
264
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Blocking: Route ChainLock fallback rejections through consumed-lock settlement The outer submission error is classified with source: ['codex', 'coderabbit'] |
||
| } | ||
| Err(e) => return Err(PlatformWalletError::Sdk(e)), | ||
| // See the matching arm in `top_up_identity_with_funding`: a | ||
| // credit output Platform already spent is terminal, and this is | ||
| // the only place that can record it locally on the failure path. | ||
| Err(e) => { | ||
| if let Some(out_point) = asset_lock_already_consumed_out_point(&e) { | ||
| return Err(self.settle_already_consumed_lock(out_point).await); | ||
| } | ||
| return Err(PlatformWalletError::Sdk(e)); | ||
| } | ||
|
Comment on lines
+266
to
+274
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Handle already-consumed errors from both ChainLock retries. The IS-lock fallback branches propagate a failed ChainLock retry before the new generic error arms execute. Therefore, an already-consumed rejection after an IS-to-CL upgrade remains
📍 Affects 1 file
🤖 Prompt for AI Agents |
||
| }; | ||
|
|
||
| // Step 4 (best-effort): bookkeeping — add to local | ||
|
|
@@ -492,7 +502,18 @@ impl IdentityWallet { | |
| .await | ||
| .map_err(PlatformWalletError::Sdk)? | ||
| } | ||
| Err(e) => return Err(PlatformWalletError::Sdk(e)), | ||
| // Platform says this credit output was already spent — the | ||
| // top-up it would have paid for landed earlier. Record that | ||
| // locally (nothing else ever does: the success path is the only | ||
| // other caller of `consume_asset_lock`) so the lock leaves the | ||
| // resumable set instead of being retried against the same | ||
| // deterministic rejection forever. | ||
| Err(e) => { | ||
| if let Some(out_point) = asset_lock_already_consumed_out_point(&e) { | ||
| return Err(self.settle_already_consumed_lock(out_point).await); | ||
| } | ||
| return Err(PlatformWalletError::Sdk(e)); | ||
| } | ||
| }; | ||
|
|
||
| // Step 4 (best-effort): persist the new balance + clean up the | ||
|
|
@@ -550,6 +571,43 @@ impl IdentityWallet { | |
| } | ||
| } | ||
|
|
||
| impl IdentityWallet { | ||
| /// Record Platform's "already completely used" verdict for `out_point` | ||
| /// locally and return the typed error describing it. | ||
| /// | ||
| /// The credits this lock paid for exist on chain — an earlier attempt | ||
| /// succeeded and the client never learned. Marking it | ||
| /// [`Consumed`](crate::wallet::asset_lock::tracked::AssetLockStatus::Consumed) | ||
| /// takes it out of the resumable set, which is what stops a recovery | ||
| /// worker retrying it on every pass (and, for clients that block new | ||
| /// funding while a lock is unresolved, unblocks the next purchase). | ||
| /// | ||
| /// Returns [`AssetLockAlreadyConsumed`](PlatformWalletError::AssetLockAlreadyConsumed) | ||
| /// — the same typed error a resume of an already-consumed lock raises, so | ||
| /// callers need one terminal case, not a Platform error-string match. | ||
| /// A bookkeeping failure here can only be `WalletNotFound`; it is logged | ||
| /// rather than returned, because the verdict itself is what the caller | ||
| /// must act on. | ||
| async fn settle_already_consumed_lock( | ||
| &self, | ||
| out_point: dashcore::OutPoint, | ||
| ) -> PlatformWalletError { | ||
| tracing::info!( | ||
| outpoint = %out_point, | ||
| "Platform rejected the asset lock as already completely used — its \ | ||
| credits landed on an earlier attempt; marking the lock consumed" | ||
| ); | ||
| if let Err(e) = self.asset_locks.consume_asset_lock(&out_point).await { | ||
| tracing::warn!( | ||
| outpoint = %out_point, | ||
| error = %e, | ||
| "consume_asset_lock failed after Platform's already-used rejection" | ||
| ); | ||
| } | ||
| PlatformWalletError::AssetLockAlreadyConsumed(out_point) | ||
|
Comment on lines
+591
to
+607
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Blocking: Do not permanently settle a lock from an unauthenticated DAPI rejection
source: ['codex'] |
||
| } | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Helpers | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Suggestion: Add regression coverage for consumed-error classification and settlement
The two new tests cover only the
Builtrebroadcast behavior. There is no test proving thatasset_lock_already_consumed_out_pointextracts the correct outpoint from both supported SDK wrappers, rejects unrelated errors, or that the resulting settlement persistsConsumedand removes the lock from the resumable set. The adjacent address-nonce classifier already demonstrates the expected wrapper-by-wrapper test pattern. Add equivalent classifier tests and an end-to-end wallet-state assertion for the consumed settlement path.source: ['codex']