You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
An asset_locks row's lifecycle_blob can hold a serialized AssetLockProof that encodes successfully but can never be decoded back — permanently and reproducibly. Every subsequent SqlitePersister::load_from_persistor() (wallet rehydration) call for a persister containing such a row fails, which in dash-evo-tool manifests as "chain sync never starts" on every app relaunch, blocking every Testnet-dependent feature.
Confirmed at platform commit 93b967f9c7ab0164b47fe825d2bae58b3974625c (current dash-evo-toolv1.0-dev pin). Not data corruption, not disk/lock contention — a genuine wire-format incompatibility, root-caused via a standalone repro binary against the pinned crate, not inferred from a user-facing message.
Root cause
AssetLockProof (packages/rs-dpp/src/identity/state_transition/asset_lock_proof/mod.rs:37-41) is deliberately an internally-tagged serde enum:
The type also derives bincode's own native Encode/Decode — but the variant fields are annotated #[bincode(with_serde)], meaning bincode's derive delegates to serde::Serialize/Deserialize for the inner content, not its own native codec.
rs-platform-wallet-storage's shared blob codec (packages/rs-platform-wallet-storage/src/sqlite/schema/blob.rs:85-117) is a one-size-fits-all wrapper used by every _blob column via a sealed PersistableBlob: Serialize trait:
bincode::serde's bridge deserializer does not implement deserialize_any (it can't — bincode has no self-describing length/type prefix to support arbitrary lookahead). An internally-tagged enum's Deserialize impl requires exactly that (buffer the whole map/struct to find the "type" discriminant before knowing which variant to build). Result: bincode::serde::encode_to_vec succeeds (serialization never needs lookahead), but bincode::serde::decode_from_slice fails deterministically with Serde(AnyNotSupported), surfaced as WalletStorageError::BincodeDecode.
A row containing an AssetLockProof can be written, but can never be read back, by design of the current codec pairing.
Full failure chain (dash-evo-tool side, for context)
PlatformWalletManager::new spawns a wallet-event adapter that retains an Arc<SqlitePersister>before the fallible load_from_persistor call resolves. When that load fails, the failed-construction path doesn't tear the adapter down, so the persister stays registered as "open" for the rest of that process. Any same-process retry then hits WalletStorageError::AlreadyOpen instead of the real decode error — a confusing secondary symptom that looks unrelated to the actual cause. A fresh process restart clears the guard and goes straight back to the real BincodeDecode failure.
Reproduction
A byte-identical platform-wallet.sqlite containing exactly one such row (an active testnet asset lock, wallet_id=38A9219B..., outpoint=2012A6E6..., status=is_locked, amount_duffs=2000000) is preserved and hashed; happy to share privately if useful — not attaching here since it's a real (if worthless, testnet) wallet artifact.
Minimal repro without the fixture: construct any AssetLockEntry whose lifecycle_blob embeds an AssetLockProof, persist it through rs-platform-wallet-storage's normal write path, then attempt to load it back through PlatformWalletManager::load_from_persistor() (or directly through blob::decode::<AssetLockEntry>). Decode fails with BincodeDecode { source: Serde(AnyNotSupported) } on every attempt.
Suggested fix direction (not prescriptive — flagging for whoever picks this up)
AssetLockProof already derives bincode's nativeEncode/Decode at the outer level (independent of the #[bincode(with_serde)]-annotated inner fields per the type's own doc comment: "Bincode Encode/Decode derives are independent of serde"). A fix likely needs one of:
A second blob-codec path for types that implement bincode's native Encode/Decode directly (bypassing the bincode::serde bridge entirely for those types), used for AssetLockEntry/AssetLockProof specifically.
Or a different self-describing serialization format for this one blob column that supports internally-tagged enums (note: blob.rs's own doc comment states "Schema evolution is gated by the refinery migration version — no per-blob revision tag," so introducing a second format needs to account for that).
Either way, please audit every other impl_persistable_blob! call site for the same latent issue — AssetLockProof is very unlikely to be the only internally-tagged (or otherwise deserialize_any-requiring) serde shape reachable through this shared codec, and this bug class will silently recur for any future type with the same shape. A migration/compat-read path should also be considered for any already-written incompatible rows in the wild (this is a live wallet-storage format, not just a test artifact).
Also worth filing (logging/UX, tracked separately in dash-evo-tool)
load_from_persistor's error is logged by the DET consumer with %error (Display) rather than ?error (Debug) at the call site that surfaces this to users, so the real typed variant was invisible in application logs throughout an entire QA campaign — only a dedicated forensic investigation recovered it. Not a platform-repo issue, noting for completeness since it made this bug much harder to diagnose than it should have been.
Summary
An
asset_locksrow'slifecycle_blobcan hold a serializedAssetLockProofthat encodes successfully but can never be decoded back — permanently and reproducibly. Every subsequentSqlitePersister::load_from_persistor()(wallet rehydration) call for a persister containing such a row fails, which indash-evo-toolmanifests as "chain sync never starts" on every app relaunch, blocking every Testnet-dependent feature.Confirmed at
platformcommit93b967f9c7ab0164b47fe825d2bae58b3974625c(currentdash-evo-toolv1.0-devpin). Not data corruption, not disk/lock contention — a genuine wire-format incompatibility, root-caused via a standalone repro binary against the pinned crate, not inferred from a user-facing message.Root cause
AssetLockProof(packages/rs-dpp/src/identity/state_transition/asset_lock_proof/mod.rs:37-41) is deliberately an internally-tagged serde enum:The type also derives bincode's own native
Encode/Decode— but the variant fields are annotated#[bincode(with_serde)], meaning bincode's derive delegates toserde::Serialize/Deserializefor the inner content, not its own native codec.rs-platform-wallet-storage's shared blob codec (packages/rs-platform-wallet-storage/src/sqlite/schema/blob.rs:85-117) is a one-size-fits-all wrapper used by every_blobcolumn via a sealedPersistableBlob: Serializetrait:bincode::serde's bridge deserializer does not implementdeserialize_any(it can't — bincode has no self-describing length/type prefix to support arbitrary lookahead). An internally-tagged enum'sDeserializeimpl requires exactly that (buffer the whole map/struct to find the"type"discriminant before knowing which variant to build). Result:bincode::serde::encode_to_vecsucceeds (serialization never needs lookahead), butbincode::serde::decode_from_slicefails deterministically withSerde(AnyNotSupported), surfaced asWalletStorageError::BincodeDecode.A row containing an
AssetLockProofcan be written, but can never be read back, by design of the current codec pairing.Full failure chain (dash-evo-tool side, for context)
Once one such row exists, rehydration fails on every subsequent process launch — this is not intermittent.
Secondary masking effect (worth fixing too, separately)
PlatformWalletManager::newspawns a wallet-event adapter that retains anArc<SqlitePersister>before the fallibleload_from_persistorcall resolves. When that load fails, the failed-construction path doesn't tear the adapter down, so the persister stays registered as "open" for the rest of that process. Any same-process retry then hitsWalletStorageError::AlreadyOpeninstead of the real decode error — a confusing secondary symptom that looks unrelated to the actual cause. A fresh process restart clears the guard and goes straight back to the realBincodeDecodefailure.Reproduction
A byte-identical
platform-wallet.sqlitecontaining exactly one such row (an active testnet asset lock,wallet_id=38A9219B...,outpoint=2012A6E6...,status=is_locked,amount_duffs=2000000) is preserved and hashed; happy to share privately if useful — not attaching here since it's a real (if worthless, testnet) wallet artifact.Minimal repro without the fixture: construct any
AssetLockEntrywhoselifecycle_blobembeds anAssetLockProof, persist it throughrs-platform-wallet-storage's normal write path, then attempt to load it back throughPlatformWalletManager::load_from_persistor()(or directly throughblob::decode::<AssetLockEntry>). Decode fails withBincodeDecode { source: Serde(AnyNotSupported) }on every attempt.Suggested fix direction (not prescriptive — flagging for whoever picks this up)
AssetLockProofalready derives bincode's nativeEncode/Decodeat the outer level (independent of the#[bincode(with_serde)]-annotated inner fields per the type's own doc comment: "Bincode Encode/Decode derives are independent of serde"). A fix likely needs one of:Encode/Decodedirectly (bypassing thebincode::serdebridge entirely for those types), used forAssetLockEntry/AssetLockProofspecifically.blob.rs's own doc comment states "Schema evolution is gated by the refinery migration version — no per-blob revision tag," so introducing a second format needs to account for that).Either way, please audit every other
impl_persistable_blob!call site for the same latent issue —AssetLockProofis very unlikely to be the only internally-tagged (or otherwisedeserialize_any-requiring) serde shape reachable through this shared codec, and this bug class will silently recur for any future type with the same shape. A migration/compat-read path should also be considered for any already-written incompatible rows in the wild (this is a live wallet-storage format, not just a test artifact).Also worth filing (logging/UX, tracked separately in dash-evo-tool)
load_from_persistor's error is logged by the DET consumer with%error(Display) rather than?error(Debug) at the call site that surfaces this to users, so the real typed variant was invisible in application logs throughout an entire QA campaign — only a dedicated forensic investigation recovered it. Not a platform-repo issue, noting for completeness since it made this bug much harder to diagnose than it should have been.Environment
platformcommit:93b967f9c7ab0164b47fe825d2bae58b3974625cdash-evo-tool, branchfix/tx-history(PR fix(rs-drive-abci): config file is broken #892), commit57195d54🤖 Co-authored by Claudius the Magnificent AI Agent