Skip to content

feat(rs-dpp): unify JSON/Value conversion traits + comprehensive round-trip tests#3573

Draft
shumkov wants to merge 63 commits intov3.1-devfrom
feat/json-convertible-address-transitions
Draft

feat(rs-dpp): unify JSON/Value conversion traits + comprehensive round-trip tests#3573
shumkov wants to merge 63 commits intov3.1-devfrom
feat/json-convertible-address-transitions

Conversation

@shumkov
Copy link
Copy Markdown
Collaborator

@shumkov shumkov commented Apr 30, 2026

Summary

Unification work for the canonical JsonConvertible / ValueConvertible traits across packages/rs-dpp. Two passes:

Pass 1 — added trait impls to ~80 domain types (commit 9f23d675af). All types now expose the canonical conversion surface.

Pass 2 — added 197 round-trip tests using non-default fixtures + per-property assertions per the new test convention. Surfaced 3 platform_value bugs that were previously silent.

Test results

  • 3619 dpp lib tests pass, 18 ignored, 0 failed (no regressions vs. base branch).
  • 197 dedicated json_convertible_tests across ~95 types.
  • 12 of the 197 are #[ignore]d — each documents either a real bug found or a structural issue (BLS keys, untagged-enum ambiguity, known-broken serde).

Bugs surfaced (logged in docs/json-value-unification-plan.md §10b)

  1. OutPoint round-trip via platform_value::Value::Map fails — affects ChainAssetLockProof, AddressFundingFromAssetLockTransition, ShieldFromAssetLockTransition. JSON works.
  2. [u8; N] fixed-array fields with custom serializers fail platform_value round-trip ("Invalid symbol 17, offset 0") — affects ExtendedBlockInfo signature and all 5 shielded transitions' Orchard fields. JSON works.
  3. DataContract document_schemas lose sized integer types via JSON round-trip (U32(63)U64(63), I32(0)U64(0)) — Critical-1 manifestation; affects every state transition embedding a DataContract. Value round-trip works.

Convention added

Every J/V test follows docs/json-value-unification-plan.md §8:

  1. Round-trip via to_jsonfrom_json (and same for value).
  2. Non-default fixture with distinguishable values per field.
  3. Per-property assertions on the recovered struct — catches silent field drops, type narrowing, custom-PartialEq quirks.
  4. Tagged-enum $formatVersion preservation test where applicable.

Fixtures source priority: hand-built struct literals > random_* constructors > from_* factories > crate::tests::fixtures::* helpers > Default::default() only for unit-only enums.

Code improvements

  • Derived PartialEq on StateTransitionProofResult + StoredAssetLockInfo (was missing, blocked round-trip assert_eq).

Out of scope (genuinely residual — needs upstream work)

  • ValidatorSet — needs real BLS public key construction (crypto setup).
  • ExtendedDocument — Critical-3 known-broken serde (writes version, reads $version).
  • StateTransition umbrella — serde(untagged), deserialize ambiguity (logged #[ignore]).

Docs

  • docs/json-value-conversion-inventory.md — pre-pass-1 structural inventory.
  • docs/json-value-unification-plan.md — full plan with phases, bug log, lessons learned, fixture conventions.

Test plan

  • Reviewer runs cargo test -p dpp --features=json-conversion,value-conversion,serde-conversion and sees 3619 pass / 0 fail.
  • Reviewer skims docs/json-value-unification-plan.md §10b for the 3 surfaced bugs and decides priority for follow-up fix.
  • Reviewer confirms the test convention in §8 is appropriate for the project's testing style.

🤖 Generated with Claude Code

shumkov and others added 30 commits April 30, 2026 15:29
Adds JsonConvertible / ValueConvertible impls (canonical traits in
packages/rs-dpp/src/serialization/serialization_traits.rs) to the
domain types catalogued in docs/json-value-conversion-inventory.md.

This is the unification first pass — round-trip correctness, tagged-
enum tag preservation, and integer-precision tests are deferred to the
second pass per the plan. Some impls may produce broken JSON or fail
round-trip until pass 2 fixes them; that's expected.

Coverage:
- Symmetrize V-only and J-only types (15+1).
- Add J+V to types missing both: top priorities (DataContract,
  StateTransition, BatchTransition, Document, AssetLockProof,
  AddressCreditWithdrawalTransition, Pooling) plus 22 batch transitions
  and 19 leaf serde types.

Skipped: types without serde derives, lifetime-param refs, and the
wasm-dpp legacy crate per minimum-touch policy.

Approach: derive(JsonConvertible/ValueConvertible) where the type
already opts into the json_safe_fields macro ecosystem; empty manual
impl X {} (§6 escape hatch) elsewhere to bypass the JsonSafeFields
cascade. Both paths use the trait's default serde-delegating methods.

Adds planning docs:
- docs/json-value-conversion-inventory.md — structural inventory.
- docs/json-value-unification-plan.md — phased plan with critical
  findings and per-mechanism deprecation decisions.

cargo check -p dpp passes with --features=json-conversion,value-conversion,serde-conversion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates the unification plan with:
- Progress table tracking the 5 passes (1 done, 2 in progress).
- Phase B/C status updated: ~80 types now have canonical impls.
- Skip-list rationale for types we deliberately did NOT migrate
  (no serde derives, lifetime params, internal indirection).
- Section 11 "Lessons learned from pass 1" — the JsonSafeFields
  cascade, BTreeMap-of-enum-keys serde helpers, what shipped in the
  481 commits we pulled, test-fixture pattern, sandbox/sccache/gpg
  gotchas.
- Reference to pass-1 commit 9f23d67.

Companion doc gets a status banner pointing back to the plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ity)

Adding empty impl JsonConvertible/ValueConvertible for DataContract in
pass 1 collided with the existing DataContractJsonConversionMethodsV0::
to_json(&self, &PlatformVersion) at every call site that passes a
PlatformVersion — Rust E0034 (multiple applicable items in scope).

Per the unification plan §3.11 step 10, DataContract is KEEP-AS-EXCEPTION
(version-aware serde via DataContractInSerializationFormat). The proper
unification path renames the legacy methods to *_versioned first, then
the canonical traits can layer on. That's a follow-up.

For now, leave a comment in data_contract/mod.rs explaining the absence
and pointing readers at DataContractInSerializationFormat (which DOES
have the canonical traits) when they need a JSON shape.

cargo test -p dpp --features=json-conversion,value-conversion,serde-conversion
--lib json_convertible_tests now passes (10/10 — the 5 address-transition
round-trip + tag-preservation tests from pass 1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds json_round_trip + value_round_trip tests for 11 types covered
by the pass-1 unification commit (9f23d67). All 28 tests in the
new modules pass; no regressions in the existing 3432 dpp lib tests.

Types covered:
- Identity, IdentityV0, IdentityPublicKey
- AddressCreditWithdrawalTransition
- TokenContractInfo, TokenPaymentInfo
- Document
- Pooling
- GroupStateTransitionInfo

Types skipped with TODO (V0 inner lacks Default):
- AssetLockValue (AssetLockValueV0)
- GroupAction (GroupActionV0 has GroupActionEvent field with no Default)

Pass-2 work continues: more types to follow, then bug discovery
(StateTransition untagged, ExtendedDocument bug, Critical-1 / -2 / -4).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds round-trip tests for TokenEmergencyAction, GasFeesPaidBy, and
YesNoAbstainVoteChoice — all flat enums with derive(Default).
Also marks TokenMarketplaceRules and other types whose V0 lacks Default
with TODO(unification pass 2) comments — they need explicit fixtures.

34 json_convertible_tests pass, no regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…DistributionType (pass 2)

DocumentPatch has Default and J+V impls — round-trips cleanly.
TokenDistributionType has Default but the J+V impls are on its variants
(TokenDistributionTypeWithResolvedRecipient, TokenDistributionInfo),
neither of which has Default — left as TODO for explicit fixture.

36/36 json_convertible_tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…perty assertions

Per user direction, every J/V test must:
1. Use a NON-DEFAULT fixture (distinguishable values per field).
2. Round-trip via to_json/from_json (and to_object/from_object).
3. Assert each field of the recovered value individually — catches
   silent field drops, type narrowing, and PartialEq quirks that
   whole-struct equality can miss.

IdentityCreateFromAddressesTransition is the canonical example —
fixture has 6 non-default fields including a 2-entry inputs map
with both P2PKH+P2SH addresses, a populated public key, two
witness types, custom fee strategy, and non-zero user_fee_increase.
All three tests pass (json_round_trip, value_round_trip,
format_version_tag).

Plan §8 updated with the new mandatory convention and rationale.
Existing tests with Default fixtures are now legacy and will be
upgraded as we revisit each type in pass 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sferToAddresses tests

Apply the new mandatory convention (non-default fixture + per-property
assertions + round-trip) to two more address transitions. Both fixtures
use distinguishable values for every field (identity_id, recipient_addresses,
nonce, signature, fee strategy, witnesses, etc.) so the per-property
assertions actually exercise data preservation.

3/5 address transitions now on the new convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upgrade AddressFundingFromAssetLockTransition, AddressFundsTransferTransition,
and AddressCreditWithdrawalTransition tests to non-default fixture +
per-property assertions per the new convention.

Bug surfaced: AddressFundingFromAssetLockTransition.value_round_trip
fails — `OutPoint` inside `ChainAssetLockProof` cannot deserialize from
`platform_value::Value::Map` ("invalid type: map, expected an OutPoint").
JSON round-trip works fine. Marked the value test #[ignore] with the
reason and logged in plan §10b for pass-3 fix.

5/5 address transitions now on the new convention. 46 json_convertible_tests
pass, 3 ignored (1 OutPoint bug + 2 StateTransition untagged-enum known
failures).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…erty assertions

Replaces the legacy Identity::default() fixture with one that has:
- id: Identifier::new([0x42; 32])
- balance: 1_000_000
- revision: 7
- public_keys: BTreeMap with 2 distinct entries

Per-property assertions check id, balance, revision, and public_keys count.
Removes the duplicate empty-fixture test module that was leftover.

401 dpp lib tests pass (filtered to identity::identity).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… tests

Apply non-default fixture + per-property assertion convention to:
- IdentityPublicKey (8 distinguishable fields incl. disabled_at, contract_bounds)
- TokenContractInfo (contract_id + token_contract_position; note: untagged enum)
- Pooling (test all 3 variants — Never/IfAvailable/Standard)

48 json_convertible_tests pass, 3 ignored (1 OutPoint bug, 2 StateTransition).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces single-Default-fixture tests for unit enums with
each_variant() pattern that exercises all variants in turn. This is
the per-property-assertion equivalent for unit-only enums where each
discriminant is the only "field".

Upgrades:
- TokenEmergencyAction (Pause, Resume)
- GasFeesPaidBy (DocumentOwner, ContractOwner, PreferContractOwner)
- YesNoAbstainVoteChoice (YES, NO, ABSTAIN)

48 json_convertible_tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apply non-default fixture + per-property assertion convention to:
- GroupStateTransitionInfo (group_contract_position=5, action_id=[0x33;32], action_is_proposer=true)
- DocumentPatch (id=[0x77;32], 2 properties, revision=3, updated_at=1.7T)

48 json_convertible_tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…per-property

5-field fixture with all Option fields populated and gas_fees_paid_by
set to a non-default variant. Per-property assertion verifies each field
preserves through round-trip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…er-property

5-field fixture (owner_id, transitions, user_fee_increase,
signature_public_key_id, signature) with distinguishable values.
transitions vec is empty since DocumentTransition sub-types are tested
in their own modules. Per-property assertion verifies each field
preserves through round-trip.

49 json_convertible_tests pass, 3 ignored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rk list

Updates the plan with:
- Pass-2 status table — 17/~80 types upgraded, 1 bug surfaced.
- Explicit list of types still on Default fixtures or without tests.
- Cost estimate: ~10-15 hours of focused work to finish pass 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds basic round-trip + format version tag tests for:
- IdentityCreateTransition (json/value tests #[ignore]: V0::default()
  has structurally invalid asset_lock_proof — needs explicit fixture)
- IdentityTopUpTransition
- IdentityCreditTransferTransition
- MasternodeVoteTransition
- IdentityPublicKeyInCreation
- IdentityUpdateTransition
- IdentityCreditWithdrawalTransition

DataContractCreateTransition and DataContractUpdateTransition skipped:
their V0 inners lack Default — needs explicit fixtures (TODO).

68 json_convertible_tests pass, 5 ignored (3 prior + 2 new
IdentityCreateTransition pending real fixture).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds basic round-trip tests using Default fixture for:
- BlockInfo (struct with Default)
- Vote (manual Default impl)
- VotePoll (manual Default impl)
- ResourceVoteChoice (derived Default with #[default] variant)
- InstantAssetLockProof (manual Default impl)

Marks 6 types as TODO (no Default — needs explicit fixture):
- ContractBoundSpecification, ChainAssetLockProof,
- ExtendedBlockInfo, ExtendedEpochInfo, FinalizedEpochInfo,
- IdentityTokenInfo, TokenStatus.

78 json_convertible_tests pass, 5 ignored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces TODOs with hand-built fixtures for:
- IdentityTokenInfo (frozen=true)
- TokenStatus (paused=true)
- ExtendedEpochInfo (6 fields, distinguishable values)
- FinalizedEpochInfo (12 fields incl. block_proposers map)
- ExtendedBlockInfo (8 fields incl. signature [u8;96])

Bug surfaced: ExtendedBlockInfo value_round_trip fails on signature
field round-trip via platform_value::Value ("Invalid symbol 17"). JSON
works. Marked #[ignore] and logged in plan §10b.

87 conversion tests pass, 6 ignored (3 prior + 1 new bug + 2 needs-fixture).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AssetLockValue uses AssetLockValue::new() factory (V0 fields are
pub(super), can't be set directly).
ChainAssetLockProof uses OutPoint::from_str factory; value test
ignored due to known OutPoint round-trip bug.

90 conversion tests pass, 7 ignored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ourceVotePoll + ContestedDocumentVotePollWinnerInfo

102 conversion tests pass, 7 ignored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ansition

Both use fully-qualified trait syntax to disambiguate from legacy
StateTransitionValueConvert::to_object/to_json methods on the same
type — known E0034 ambiguity per plan §3.11.

106 conversion tests pass, 7 ignored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DocumentReplaceTransition, DocumentTransferTransition,
DocumentPurchaseTransition, DocumentUpdatePriceTransition — all use
fully-qualified trait syntax to disambiguate from legacy methods.

116 conversion tests pass, 7 ignored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nMint

122 conversion tests pass, 7 ignored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…troyFrozenFunds

128 tests pass, 7 ignored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…y/Claim/DirectPurchase/SetPrice)

136 conversion tests pass, 7 ignored. All 17 of 19 batch sub-transitions
now tested (only TokenConfigUpdate remaining — needs TokenConfigurationChangeItem fixture).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shumkov and others added 23 commits April 30, 2026 19:53
154 conversion tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
160 conversion tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… tests

164 conversion tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
167 conversion tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
177 conversion tests pass, 12 ignored (3 platform_value [u8;N] bugs + 2 OutPoint bugs + 7 needs-fixture/known-fragile).

All 5 shielded transitions now tested.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dAssetLockInfo

StateTransitionProofResult was missing PartialEq, blocking simple
round-trip assert_eq tests. Add PartialEq derive to it and to
StoredAssetLockInfo (one of its variants' inner type). All variants
now compose cleanly.

Test simplified back to assert_eq from variant-matching workaround.
3603 dpp lib tests pass, no regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ansition tests

Uses crate::tests::fixtures::get_data_contract_fixture (already gated
on feature = "fixtures-and-mocks", available in test mode) + the
existing TryFromPlatformVersioned<DataContract> factory to build the
DataContractInSerializationFormat. No new visibility changes needed.

Bug surfaced: JSON round-trip loses sized integer types in
document_schemas — U32(63) becomes U64(63), I32(0) becomes U64(0).
This is a Critical-1 manifestation (platform_value preserves sized
integers; serde_json::Value has only one Number type). Marked the
JSON tests #[ignore] with the reason; value round-trip and
$formatVersion tag tests pass cleanly.

Plan §10b updated with the new bug entry.

183 conversion tests pass, 14 ignored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…asset_lock_proof_fixture

Real proof + correctly-computed identity_id (via create_identifier())
makes round-trip identity. 3 ignored tests now pass.

188 conversion tests pass, 12 ignored (down from 14).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…fixture)

189 conversion tests pass, 12 ignored. All 22/22 batch sub-transitions tested.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pass 2 done. Outstanding items: ValidatorSet (BLS crypto), ExtendedDocument
(known-broken serde), StateTransition umbrella (untagged ambiguity).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 30, 2026

Important

Review skipped

Draft detected.

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

Run ID: e13f9bbf-da30-41b2-b5c2-780f5d2fc462

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/json-convertible-address-transitions

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 and usage tips.

@github-actions github-actions Bot added this to the v3.1.0 milestone Apr 30, 2026
shumkov and others added 5 commits May 1, 2026 15:15
…lize bug

Constructed ValidatorSet fixture using SecretKey<Bls12381G2Impl>::random().public_key()
(matches the existing pattern in v0/mod.rs:331).

New bug surfaced: BlsPublicKey<Bls12381G2Impl>::deserialize requires a
**borrowed** string (&str), but both serde_json::Value and
platform_value::Value produce owned strings when traversed. Round-trip
fails with "invalid type: string ..., expected a borrowed string" on
both paths.

Affects every V0 field of type BlsPublicKey<Bls12381G2Impl>:
- ValidatorV0.public_key
- ValidatorSetV0.threshold_public_key

Upstream fix lives in dashcore::blsful — Deserialize impl needs to
accept owned strings via visit_string in addition to visit_borrowed_str.

Both tests #[ignore]d with the bug detail; logged in plan §10b.

197 tests pass, 14 ignored (3 platform_value bugs documented).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…serde

Replaces the broken manual Serialize/Deserialize impl on ExtendedDocument
with a derived impl using `#[serde(tag = "$extendedFormatVersion")]`.

The previous manual impl wrote `version` on serialize, read `$version` on
deserialize, and required a `data_contract` field that serialize never
emitted — so `from_value(to_value(&doc))` always failed.

Why a separate `$extendedFormatVersion` key (not the inner Document's
`$formatVersion`): ExtendedDocument is an envelope wrapping Document plus
the full DataContract plus `$entropy`/`$metadata`/`$tokenPaymentInfo`. The
envelope can evolve independently of the inner Document — bumping
ExtendedDocument to V1 shouldn't force a Document V1 it doesn't otherwise
need. Two version dimensions, two keys.

Naive `tag = "$formatVersion"` was rejected: serde emits both the outer
enum tag AND the flattened inner Document tag at the same JSON level —
duplicate keys in one object → undefined JSON behavior, deserialize fails.

`value_round_trip` is `#[ignore]` pending Critical-1: Bytes32::deserialize
unconditionally requires a base64 string, but platform_value emits bytes
when is_human_readable=false. JSON path round-trips clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`Bytes32::deserialize` was branching on `is_human_readable` and only
accepting one input shape per branch — base64 string in HR mode, byte
array in non-HR mode. That breaks for any internally-tagged enum (e.g.
`#[serde(tag = "$version")]`) wrapping a struct with a Bytes32 field,
because serde's `ContentDeserializer` always reports
`is_human_readable: true` regardless of the parent deserializer.

Concretely: ExtendedDocument's new `#[serde(tag = "$extendedFormatVersion")]`
combined with `platform_value::to_value` (HR=false, emits bytes) made the
ContentDeserializer hand a byte array to a StringVisitor expecting a
base64 string → "invalid type: byte array" failure on the `$entropy` field.

Fix: both visitors now accept strings AND bytes. Mirrors the existing
pattern in `BinaryData` and `Identifier` (the comments in those files
document the same ContentDeserializer quirk). The `value_round_trip`
test on ExtendedDocument now passes; un-ignored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ged enums

The two helper modules used by `#[json_safe_fields]` to serialize `[u8;N]`
and `Vec<u8>` fields branched on `is_human_readable` to choose between a
base64-string path and a byte-buffer path. That broke for any internally-
tagged enum (e.g. `#[serde(tag = "$formatVersion")]`) wrapping a struct
with such a field, because serde's `ContentDeserializer` always reports
`is_human_readable: true` regardless of the parent deserializer.

Concretely: when the outer enum is tagged and the source goes through
`platform_value::to_value` (HR=false, emits `Value::Bytes`), the buffer
becomes `Content::ByteBuf`. Then the helper's HR branch called
`<String>::deserialize`, which dispatched to `visit_str` with the bytes
interpreted as UTF-8 — and base64-decoding that produced the
"Invalid symbol 17, offset 0" error on the very first byte.

Fix: a single visitor that accepts strings, bytes, byte_buf, and seq in
both branches; HR branch dispatches via `deserialize_any` so true HR
deserializers (serde_json) hit `visit_str` and ContentDeserializer-wrapped
bytes hit `visit_bytes` cleanly. Mirrors the dual-visitor pattern used by
`Bytes32`, `BinaryData`, and `Identifier` in `rs-platform-value`.

Bonus cleanup: `ExtendedBlockInfoV0::signature` had its own custom
`signature_serializer` module that did the same thing as serde_bytes but
with the same bug. Removed; the field now picks up `serde_bytes` via the
auto-injection.

Six previously-ignored tests now pass:
- `ExtendedBlockInfo::value_round_trip_with_per_property_assertions`
- `ShieldTransition::value_round_trip`
- `UnshieldTransition::value_round_trip`
- `ShieldedTransferTransition::value_round_trip`
- `ShieldFromAssetLockTransition::value_round_trip`
- `ShieldedWithdrawalTransition::value_round_trip`

dpp lib: 3627 passing (+6), 14 ignored (-6). platform-value: 1035 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…enums

`AssetLockProof` is `#[serde(tag = "type")]`, which routes deserialization
through serde's `ContentDeserializer`. That deserializer always reports
`is_human_readable: true` regardless of the upstream source. dashcore's
`OutPoint::deserialize` (and the `Txid` it contains, via `hash_newtype!`)
uses two completely disjoint visitors per `is_human_readable` branch:
HR-only `StringVisitor` for `"txid:vout"` strings, non-HR-only
`StructVisitor` for `{txid, vout}` maps. When platform_value serializes
the OutPoint as a struct (HR=false), the buffered Content::Map is then
replayed into the HR `StringVisitor` → `"invalid type: map, expected an
OutPoint"`. Same exact pattern surfaces one level deeper for `Txid`
("bad hex string length 32").

Local fix: `outpoint_serde` wrapper module on `ChainAssetLockProof::out_point`
with a unified visitor that accepts string + struct + seq, plus a
`TxidCompat` newtype handling the same bug for Txid. HR branch dispatches
via `deserialize_any` (handles true-HR + ContentDeserializer); non-HR
branch uses `deserialize_struct` / `deserialize_byte_buf` (bincode rejects
deserialize_any with `Serde(AnyNotSupported)`).

The wrapper carries a TODO marker pointing at the upstream dashcore fix.
The proper fix is to apply the unified-visitor pattern inside dashcore's
`serde_struct_human_string_impl!` macro itself, which would benefit every
`hash_newtype!`-generated type (OutPoint, Txid, BlockHash, …) in one
stroke. Once that lands and we bump dashcore, drop this wrapper and the
`serde(with = …)` annotation.

Two previously-ignored tests now pass:
- `ChainAssetLockProof::value_round_trip_with_per_property_assertions`
- `AddressFundingFromAssetLockTransition::value_round_trip_with_per_property_assertions`

dpp lib: 3629 passing (+2), 12 ignored (-2). platform-value: 1035 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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