Summary
If the InstantSend-lock wait for a freshly broadcast asset lock times out, the funding is left permanently unusable. The asset lock transaction is on chain, confirmed and ChainLocked, and the wallet keeps a tracked record of it — but that record stays at status = broadcast forever, and every UI path that could consume it refuses it as "not ready".
There is no recovery: restarting does not retry, and a fresh wallet import cannot reconstruct the record at all. Funds committed to that asset lock cannot be claimed by the application afterwards.
Observed on v1.0.0-weekly.20260721+81fddd0a1 (mainnet, GUI, standard BIP44 wallet).
What happens
Three shield-from-core operations earlier the same day completed normally. The IS-lock wait resolved quickly each time:
entered outpoint=…:0 timeout=Some(300s) → result_variant=VerifiedAssetLockConsumed (46s)
entered outpoint=…:0 timeout=Some(300s) → result_variant=VerifiedAssetLockConsumed ( 8s)
entered outpoint=…:0 timeout=Some(300s) → result_variant=VerifiedAssetLockConsumed ( 9s)
The fourth one did not resolve inside the window:
T+00:00 entered outpoint=…:0 timeout=Some(300s)
T+00:00 New wallet transaction detected txid=… context=…
T+00:00 Removed spent UTXO outpoint=… txid=… (inputs consumed, funds committed)
T+05:00 IS-lock did not propagate within 300s for shielded fund-from-asset-lock (tx …)
T+05:00 Transaction … not yet chain-locked, waiting for ChainLock
and nothing ever advanced past that point.
The on-chain state was fine the whole time. Queried independently: the asset lock is a type=8 / version=3 transaction, mined, txlock=true (so an InstantSend lock does exist), and subsequently ChainLocked. The OP_RETURN credit output is unspent, as expected for an asset lock. So the local wait failed while the network condition it was waiting for had actually been met.
Why it is unrecoverable
1. Restart does not retry. After restart the tracked record is still status = broadcast. ChainLocks continue to be processed and verified, but with no effect:
ChainLockProcessed(chainlock_height=…, accounts=0, finalized_txids=0)
ChainLockProcessed(chainlock_height=…, accounts=0, finalized_txids=0)
2. Block processing stops at that block and never resumes. core_sync_state.last_processed_height stays pinned at the height of the asset lock transaction while synced_height keeps advancing (30+ blocks ahead and growing). Manually rewinding last_processed_height by one block does not help — the filter pass reports found 0 matching blocks and skips it again.
3. Every consuming path rejects it. "Recover an unfinished funding" lists the tracked lock correctly (address, amount, Status: Sent to the network. Waiting for confirmation.), but selecting it and submitting yields:
This funding isn't ready to use yet. Wait for it to be confirmed on the Dash network, then try again.
The check reads the stale local status, not chain state. det-cli offers no alternative: every relevant command (identity-credits-topup, shielded-shield-from-core, …) is documented as "Creates an asset lock", i.e. it makes a new one rather than consuming an existing one.
4. A fresh import cannot rebuild the record. Importing the same recovery phrase into a clean data directory produces a healthy wallet that rescans and finds the same 13 transactions — including the asset lock transaction itself — but its asset_locks table stays empty. The tracked-lock record appears to be written only when the app itself creates the lock; it is never reconstructed from chain data. So the only record of that funding lives in the data directory of the instance that failed.
Two-instance comparison
This is the part I would emphasise, because it separates "corrupted local state" from a genuine code path problem. Two independent instances, same wallet, different data directories:
|
instance A (upgraded, had run the pre-1.0 migration) |
instance B (clean dir, fresh import of the same phrase) |
core_address_pool |
380 |
380 |
core_transactions / core_utxos |
13 / 13 |
13 / 13 |
filter matched |
0 |
1200 |
asset_locks rows |
4 (incl. the stuck one) |
0 |
last_processed_height |
pinned at the asset-lock block |
pinned at the same block |
Instance B is demonstrably healthy — its filters match, it rescanned from birth height, it found every transaction. It still stops at exactly the same block. So this is not migration damage or a corrupted store; both instances hit the same wall at the same transaction.
(Instance A additionally shows matched: 0, i.e. its filter matching is broken too — possibly a second, separate issue. But it is not the cause of the stall, since B stalls identically with healthy matching.)
Reproduction
- Shield from core wallet (or any flow that creates an asset lock), so the app enters the IS-lock wait.
- Arrange for the IS-lock not to be observed within the 300 s window — in my case this happened spontaneously on mainnet while three earlier locks the same day resolved in under a minute.
- Observe
IS-lock did not propagate within 300s, then not yet chain-locked, waiting for ChainLock.
- Wait for the transaction to be mined and ChainLocked (verify independently that
txlock is true and the block is ChainLocked).
- Restart the app. Try "Recover an unfinished funding".
Expected: the app re-checks chain state, sees the lock is confirmed and ChainLocked, and lets it be consumed.
Actual: status remains broadcast, block processing stays pinned at that block, and the funding is rejected as not ready — permanently.
Suggested directions
- Re-validate from chain state rather than trusting the stored
status. The readiness check for a tracked lock should consult confirmations / ChainLock at the time of use. The information needed was available and correct throughout; only the local record was stale.
- Make the IS-lock wait's timeout non-terminal. Falling back to "wait for ChainLock" is the right intent, but that fallback never completes. Once the tx is in a ChainLocked block, that should be sufficient to advance
broadcast → ChainLocked → consumable.
- Do not let one unresolved transaction pin
last_processed_height. A single pending item blocking all subsequent block processing turns a transient network hiccup into permanent loss of access.
- Consider reconstructing tracked asset locks during rescan. Since the transaction is recognisably
type=8 and belongs to the wallet, a fresh import could rebuild the record — which would make wallet re-import an actual recovery path. Today it is not.
Impact
Funds committed to the asset lock cannot be claimed through the application. They are not lost on chain — the credit output is intact and only the owner's keys can claim it, and asset locks have no expiry window (unlike asset unlocks) — but the app offers no route to it, and re-importing the wallet does not help. For a user without a second instance to compare against, the symptom presents as "the balance disappeared", which is alarming and hard to diagnose.
Related
Investigated and drafted with assistance from Claude (Anthropic). Log lines are quoted verbatim with identifiers elided; on-chain facts were verified independently of the application. Amounts, addresses, transaction ids and block heights are omitted deliberately.
Summary
If the InstantSend-lock wait for a freshly broadcast asset lock times out, the funding is left permanently unusable. The asset lock transaction is on chain, confirmed and ChainLocked, and the wallet keeps a tracked record of it — but that record stays at
status = broadcastforever, and every UI path that could consume it refuses it as "not ready".There is no recovery: restarting does not retry, and a fresh wallet import cannot reconstruct the record at all. Funds committed to that asset lock cannot be claimed by the application afterwards.
Observed on
v1.0.0-weekly.20260721+81fddd0a1(mainnet, GUI, standard BIP44 wallet).What happens
Three shield-from-core operations earlier the same day completed normally. The IS-lock wait resolved quickly each time:
The fourth one did not resolve inside the window:
and nothing ever advanced past that point.
The on-chain state was fine the whole time. Queried independently: the asset lock is a
type=8/version=3transaction, mined,txlock=true(so an InstantSend lock does exist), and subsequently ChainLocked. TheOP_RETURNcredit output is unspent, as expected for an asset lock. So the local wait failed while the network condition it was waiting for had actually been met.Why it is unrecoverable
1. Restart does not retry. After restart the tracked record is still
status = broadcast. ChainLocks continue to be processed and verified, but with no effect:2. Block processing stops at that block and never resumes.
core_sync_state.last_processed_heightstays pinned at the height of the asset lock transaction whilesynced_heightkeeps advancing (30+ blocks ahead and growing). Manually rewindinglast_processed_heightby one block does not help — the filter pass reportsfound 0 matching blocksand skips it again.3. Every consuming path rejects it. "Recover an unfinished funding" lists the tracked lock correctly (address, amount,
Status: Sent to the network. Waiting for confirmation.), but selecting it and submitting yields:The check reads the stale local
status, not chain state.det-clioffers no alternative: every relevant command (identity-credits-topup,shielded-shield-from-core, …) is documented as "Creates an asset lock", i.e. it makes a new one rather than consuming an existing one.4. A fresh import cannot rebuild the record. Importing the same recovery phrase into a clean data directory produces a healthy wallet that rescans and finds the same 13 transactions — including the asset lock transaction itself — but its
asset_lockstable stays empty. The tracked-lock record appears to be written only when the app itself creates the lock; it is never reconstructed from chain data. So the only record of that funding lives in the data directory of the instance that failed.Two-instance comparison
This is the part I would emphasise, because it separates "corrupted local state" from a genuine code path problem. Two independent instances, same wallet, different data directories:
core_address_poolcore_transactions/core_utxosmatchedasset_locksrowslast_processed_heightInstance B is demonstrably healthy — its filters match, it rescanned from birth height, it found every transaction. It still stops at exactly the same block. So this is not migration damage or a corrupted store; both instances hit the same wall at the same transaction.
(Instance A additionally shows
matched: 0, i.e. its filter matching is broken too — possibly a second, separate issue. But it is not the cause of the stall, since B stalls identically with healthy matching.)Reproduction
IS-lock did not propagate within 300s, thennot yet chain-locked, waiting for ChainLock.txlockis true and the block is ChainLocked).Expected: the app re-checks chain state, sees the lock is confirmed and ChainLocked, and lets it be consumed.
Actual:
statusremainsbroadcast, block processing stays pinned at that block, and the funding is rejected as not ready — permanently.Suggested directions
status. The readiness check for a tracked lock should consult confirmations / ChainLock at the time of use. The information needed was available and correct throughout; only the local record was stale.broadcast → ChainLocked → consumable.last_processed_height. A single pending item blocking all subsequent block processing turns a transient network hiccup into permanent loss of access.type=8and belongs to the wallet, a fresh import could rebuild the record — which would make wallet re-import an actual recovery path. Today it is not.Impact
Funds committed to the asset lock cannot be claimed through the application. They are not lost on chain — the credit output is intact and only the owner's keys can claim it, and asset locks have no expiry window (unlike asset unlocks) — but the app offers no route to it, and re-importing the wallet does not help. For a user without a second instance to compare against, the symptom presents as "the balance disappeared", which is alarming and hard to diagnose.
Related
Maxon Shield reads the DISPLAY-ONLY snapshot balance, not the coin selector's set — over-shoots and the asset lock always fails #929 —Maxreads the DISPLAY-ONLY snapshot balance; different root cause, same wallet, also involves asset-lock funding.Investigated and drafted with assistance from Claude (Anthropic). Log lines are quoted verbatim with identifiers elided; on-chain facts were verified independently of the application. Amounts, addresses, transaction ids and block heights are omitted deliberately.