feat(core): allow serialization of SyncResponse#2232
Conversation
Add `serde::Serialize`/`Deserialize` for `SyncResponse` so a connected device can serialize a sync response and hand it to an air-gapped device to apply independently. `SyncResponse` holds a `TxUpdate` and an `Option<CheckPoint>`: - `TxUpdate` gains a plain derive; its fields are flat collections so there is nothing self-referential to recurse over. - `CheckPoint` is a reference-counted linked list, so a derived impl would recurse through `prev`/`skip` once per checkpoint and overflow the stack on long chains -- the same hazard that forced the hand-written `Drop` (bitcoindevkit#1634). It is instead serialized iteratively as a flat sequence of `(height, data)` pairs and rebuilt with `CheckPoint::from_blocks`, which re-derives the `skip`/`index` topology deterministically. Tests cover round-tripping `CheckPoint`, `TxUpdate` and `SyncResponse`, and assert (de)serialization is not recursive by round-tripping a long chain on a deliberately small thread stack. https://claude.ai/code/session_014BXMFRQP8qoGBghJcWzakG
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #2232 +/- ##
==========================================
+ Coverage 77.69% 79.11% +1.42%
==========================================
Files 29 30 +1
Lines 5801 6047 +246
Branches 271 279 +8
==========================================
+ Hits 4507 4784 +277
+ Misses 1223 1187 -36
- Partials 71 76 +5
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| #[cfg_attr( | ||
| feature = "serde", | ||
| derive(serde::Serialize, serde::Deserialize), | ||
| serde(bound( | ||
| serialize = "A: serde::Serialize", | ||
| deserialize = "A: Ord + serde::Deserialize<'de>" | ||
| )) | ||
| )] | ||
| #[non_exhaustive] |
There was a problem hiding this comment.
nit: are we looking to have forward-compatibility in this deserialization process ? if that's the case, it's a good idea to add serde(default) for each field.
|
Quick (maybe naive) question... I thought an air-gapped device only signed transactions, why does it need the checkpoint/chain data too? What does it do with it on the offline side? |
|
Linking to our issue here because it's relevant: bitcoindevkit/bdk-ffi#970. I was considering adding a serialization layer just for bindings so that other clients could be built on top of us without requiring we ship them in the main "bindings" library, so as not to bloat it unecessarily (at the moment all clients must ship into one, big dependency). This makes it less attractive to build and test alternative clients with the bindings (Floresta for example). I am not commenting on the PR itself, but rather just giving a thumbs up and saying "this feature is useful!" with some extra context. Also, even if this only ends up in |
Description
Adds
serde::Serialize/serde::DeserializeforSyncResponse, enabling air-gapped workflows: a connected (online) device can sync, serialize theSyncResponse, and hand it to a disconnected (offline) device which deserializes and applies it independently. This is the goal of #1954, implemented without recursion.SyncResponseis made up of aTxUpdateand anOption<CheckPoint>, so both gain serde support:TxUpdate— a plain#[derive(Serialize, Deserialize)]. Its fields are flat collections, so there is nothing self-referential to recurse over. AnOrdbound is added onAfor deserialization so thatanchors: BTreeSet<(A, Txid)>can be rebuilt.CheckPoint— this is the interesting one. ACheckPointis a reference-counted linked list (prev+skippointers), so a derived implementation would recurse one stack frame per checkpoint and overflow the stack on long chains — the exact hazard that already forced a hand-writtenDrop(example_bitcoind_rpc_pollingsync command ends with stack overflow #1634). It is therefore implemented by hand:CheckPoint::iter()and emit a flat sequence of(height, data)pairs.CheckPoint::from_blocks(..), which re-derives theskip/indextopology deterministically.The
skip/indextopology is intentionally not serialized — it is reconstructed on load, keeping the encoding minimal and both directions iterative.Notes to the reviewers
CheckPoints is a bit odd since it is a linked list" concern raised in review: the linked list is flattened to a(height, data)sequence and rebuilt, so neither serialization nor deserialization recurses.checkpoint_serde_is_not_recursiveround-trips a 10k-checkpoint chain on a deliberately small (128 KiB) thread stack, mirroring the existingcheckpoint_drop_is_not_recursivetest — a recursive impl would overflow it.serde_jsonis added only as a dev-dependency ofbdk_core, for the round-trip tests.SyncResponseto match core: allow serialization of SyncResponse #1954.FullScanResponsecan follow the exact same pattern as a trivial follow-up if desired.Changelog notice
serde::Serializeandserde::Deserializeimplementations forSyncResponse,TxUpdate, andCheckPoint(under theserdefeature).CheckPointis (de)serialized iteratively as a flat list of(height, data)blocks to avoid recursing over its linked-list structure.Checklists
All Submissions:
New Features:
Generated by Claude Code