feat(dpp): unify JSON/Value conversion traits + comprehensive round-trip tests#3573
Draft
feat(dpp): unify JSON/Value conversion traits + comprehensive round-trip tests#3573
Conversation
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>
…IndexInformation)
…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>
…entity
Identity and PartialIdentity wasm wrappers had to_json / from_json /
to_object methods that went through generic serde via the wasm-side
helper (\`serialization::to_json\` etc.). These calls produce the same
wire shape as the canonical \`JsonConvertible\` / \`ValueConvertible\`
trait methods on dpp::Identity / dpp::PartialIdentity, but bypass the
trait — so any future custom impl on dpp wouldn't propagate to the
wasm boundary.
Switch to canonical traits where possible:
IdentityWasm:
- to_object: already used canonical \`self.0.to_object()\` (no change)
- to_json: \`serialization::to_json\` -> \`self.0.to_json()\` + json_to_js
- from_json: \`serialization::from_json\` -> \`Identity::from_json(json)\`
- from_object: KEPT — uses \`Identity::try_from_platform_versioned\`
because the wasm API dispatches on the \`platform_version\` arg, not
on the value's \`\$formatVersion\` tag (intentional SDK convention).
PartialIdentityWasm:
- to_object: \`platform_value::to_value\` -> \`self.0.to_object()\` (canonical)
- to_json: \`platform_value::to_value\` -> \`self.0.to_object()\` then
\`platform_value_to_json\`
- from_object / from_json: KEPT — manual field-by-field with
\`platform_version\` for inner IdentityPublicKey deserialization.
For IdentityPublicKey: the wasm \`to_object\` calls \`to_cleaned_object\`
(which strips disabledAt: None) — different semantic from canonical
to_object, so kept as-is to avoid changing the JS wire shape.
Disambiguation: PartialIdentity now imports \`ValueConvertible\`, which
collides with \`IdentityPublicKeyPlatformValueConversionMethodsV0::from_object\`
on IdentityPublicKey (both have a \`from_object\` method). Used the
fully-qualified \`<IdentityPublicKey as IdentityPublicKeyPlatformValueConversionMethodsV0>::from_object\`
form at the one collision site.
Test results: 1120 passing, 0 failing (unchanged).
cargo check -p wasm-dpp2: clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dpp traits
Two more callers of the wasm-side generic-serde helper that wrap a
versioned dpp type with the canonical traits available — switched to
delegate via JsonConvertible / ValueConvertible:
- \`DataContractWasm::config()\` getter (data_contract/model.rs:397):
was \`serialization::to_object(self.0.config())\` (generic serde).
Now \`config().to_object()\` -> platform_value -> JS. DataContractConfig
is a versioned enum with \`#[derive(JsonConvertible, ValueConvertible)]\`.
- \`TokenConfigurationLocalizationWasm::TryFrom<&JsValue>\` fallback path
(tokens/configuration/localization.rs:121): was
\`serialization::from_object(...)\`. Now goes through
\`platform_value_from_object\` then
\`TokenConfigurationLocalization::from_object\` (the canonical
ValueConvertible trait method).
After these and the prior commits in this branch, the only remaining
callers of wasm-dpp2's generic-serde helpers (\`serialization::to_object\`
/ \`from_object\`) are over leaf types that aren't versioned dpp
structures — \`BTreeMap<String, JsonSchema>\` (document_schemas) and
\`BTreeMap<String, Value>\` (document data). Those are fine to keep on
generic serde.
Manual audit summary across all wasm-dpp2 wrappers of dpp domain types:
✅ Already routed through dpp methods (no change):
IdentityPublicKeyWasm - to_cleaned_object / to_json_object /
from_object / from_json_object
DocumentWasm - to_map_value / from_platform_value /
Document::to_json / from_json_value
DataContractWasm - to_value / from_value / from_json /
from_bytes / to_bytes (all dpp methods)
✅ Migrated in this branch:
IdentityWasm - to_object / to_json / from_json now via
JsonConvertible / ValueConvertible (was
generic serde via wasm helper)
PartialIdentityWasm - to_object / to_json now via
ValueConvertible (was direct
platform_value::to_value)
BatchTransitionWasm - now via _inner! macro (delegates to
JsonConvertible / ValueConvertible)
GroupWasm - same
TokenConfigurationLocalizationWasm - same
PoolingWasm Deserialize - delegates to dpp::pooling_serde
❌ Cannot migrate (legitimate context-aware extensions):
IdentityWasm.from_object - wasm SDK convention: dispatch on
platform_version arg, not value's
\$formatVersion tag
PartialIdentityWasm.from_* - manual field-by-field deserialization
with platform_version for inner keys
IdentityPublicKeyWasm.{to,from}_*- already context-aware via dpp methods
DocumentWasm.from_* - composite wrapper (Document + metadata)
DataContractWasm.{to,from}_* - take \`platform_version\` + \`full_validation\`
❌ Cannot migrate (semantic divergence):
IdentityPublicKeyWasm.to_object - to_cleaned_object strips disabledAt:None
❌ Cannot migrate (JS interop adapters):
IdentifierWasm / PlatformAddressWasm Serialize/Deserialize -
visit_seq for Uint8Array, visit_map for {type,data} JS quirks
Test results: 1120 passing, 0 failing (unchanged).
cargo check -p wasm-dpp2: clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous Phase E note dismissed wasm-dpp2's manual Serialize/Deserialize impls on \`IdentifierWasm\` and \`PlatformAddressWasm\` as "JS-interop quirks." Tracing through actual production usage shows that's under-described — the adapters back the public TS \`IdentifierLike = Identifier | Uint8Array | string\` contract and the wasm-sdk's \`address_infos_to_js_map\` returning \`Map<hex_string, ...>\` keyed on \`PlatformAddressWasm::to_hex()\`. Both flows pass non-canonical strings (hex / bech32m) through the deserialize path in production. Replace the dismissive note with a side-by-side comparison table (canonical wire shapes dpp provides vs the lenient extras wasm adds), the production usage reasons (IdentifierLike API contract, hex Map keys, bech32m UI input), and a verdict that the current factoring is correct — pushing more into dpp would weaken the canonical contract or add dpp surface for one consumer. Includes "Don't re-litigate this" note so a future agent doesn't try the obvious-looking \`#[serde(transparent)]\` simplification. Also expanded the manual to_*/from_* methods audit table to enumerate every wrapper of an rs-dpp domain type (Identity, PartialIdentity, IdentityPublicKey, Document, DataContract) and exactly which dpp methods each delegates to — so the question "are we calling dpp identity methods?" is answered definitively in the doc. Final note rewrite: the "small follow-up" section that listed bytes_b64 cleanup as a future PR now records what actually shipped on this branch (bytes_b64 deletion + serde_bytes::option backport, plus the 4 + 2 + 2 wrapper migrations). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…page Phase F of the json-value unification: lock in what's been built so nobody re-introduces the parallel inherent-method pattern, and give contributors a single canonical reference to point to. ## docs/json-value-conversion-canonical-pattern.md A how-to-do-it-correctly-today reference (vs the long-form unification plan, which captures rationale + history). Covers: - The two canonical traits and their default impls - When to derive vs hand-roll the impls (decision table) - Tag-key conventions (\$formatVersion / \$baseFormatVersion / \$extendedFormatVersion / \$type / \$transition / \$action / kind / type) with the \$-prefix-iff-neighbor-\$-fields rule - Round-trip + per-property test template (non-default fixture, both JSON and platform_value path, wire-shape lock) - Escape hatches: tuple-variant enums needing internal tagging, field-level shaping helpers (\`serde_bytes\`, \`serde_bytes::option\`, \`serde_bytes_var\`, \`json_safe_u64\`, \`pooling_serde\`, \`json_safe_fields\` proc macro) - Critical-1 through Critical-5 awareness (is_human_readable divergence, JSON array→bytes coercion, ExtendedDocument Critical-3, DataContract impure serde, to_canonical_object signature dependence) - wasm-dpp2 wrapper patterns (\`_inner!\` for rs-dpp domain types, \`_serde!\` for wasm-only DTOs, manual context-aware methods) - Anti-patterns to avoid ## scripts/lint/check_no_new_inherent_conversions.sh + allowlist Lints \`packages/rs-dpp/src/**/*.rs\` for module-scope \`pub fn (to_json|from_json|to_object|from_object|into_object)\` methods. Compares against a snapshot allowlist of currently-tolerated exceptions (7 entries — context-aware methods like \`Document::to_json(&self, &PlatformVersion)\` and tuple-variant asset-lock-proof helpers). - Strips line numbers from grep output so the allowlist is stable across unrelated edits. - Emits paths relative to repo root so the allowlist is portable across machines / CI runners. - \`--update\` flag regenerates the allowlist (only when intentionally removing a tolerated exception). - Adds a step "No new inherent JSON/Value conversions" to .github/workflows/tests-rs-workspace.yml between formatting and clippy. Runs on macOS self-hosted alongside the existing lints. Verified: passes on clean tree, fails when a new violation is planted in a temp file inside packages/rs-dpp/src. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI lint and canonical-pattern reference doc both shipped in 5caa138. Update Phase F bullets in docs/json-value-unification-plan.md from ⬜ to ✅ with pointers to the lint script and the new reference doc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…on AssetLockProof variants
Phase D step 2 of the json-value unification plan: removing inherent
methods that are pure 1:1 delegations to canonical traits.
Both \`InstantAssetLockProof\` and \`ChainAssetLockProof\` had:
pub fn to_object(&self) -> Result<Value, ProtocolError> {
platform_value::to_value(self).map_err(ProtocolError::ValueError)
}
pub fn to_cleaned_object(&self) -> Result<Value, ProtocolError> {
self.to_object()
}
The first is byte-for-byte equivalent to the default
\`ValueConvertible::to_object\` impl. The second is just an alias for
the first (these types have nothing to "clean" — neither carries an
\`Option<T>\` field that would null out). Both types already derive
\`ValueConvertible\`, so deleting the inherent methods leaves callers
on the canonical default.
Updates:
- rs-dpp: delete the 4 inherent methods. Update internal tests to
bring \`ValueConvertible\` into scope. Drop the redundant
\`test_to_cleaned_object_succeeds\` test (was an exact alias of
\`test_to_object_succeeds\` after the inherent method aliasing).
- wasm-dpp (legacy, minimum-touch): patch the 2 call sites that used
\`self.0.to_cleaned_object()\` — add \`use dpp::serialization::
ValueConvertible\` and switch to \`self.0.to_object()\`. Identical
behavior. Per the plan's minimum-touch policy for wasm-dpp legacy.
Test results:
- rs-dpp: 3718 lib tests passing (was 3716 — 1 removed test,
feature-gating changes raised the visible count).
- wasm-dpp: cargo check clean.
- wasm-dpp2: 1120 / 0 unchanged.
Net: -23 lines in rs-dpp inherent surface, identical wire shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…_in_creation Phase D step 3 of the json-value unification plan: dead code removal. ## CBOR module — entirely commented-out, deleted Three files in \`identity/identity_public_key/conversion/cbor/\` and \`identity/identity_public_key/v0/conversion/cbor.rs\` consisted of nothing but commented-out lines (139 total, every line a \`// \`-prefix). The files defined \`IdentityPublicKeyCborConversionMethodsV0\` — nothing in the workspace references the trait or any of its methods, even when building with \`identity-cbor-conversion\` enabled. Just dead. - Deleted: \`identity/identity_public_key/conversion/cbor/\` directory (mod.rs + v0/mod.rs). - Deleted: \`identity/identity_public_key/v0/conversion/cbor.rs\`. - Removed the corresponding \`#[cfg(feature = "identity-cbor-conversion")] mod cbor;\` lines from both \`conversion/mod.rs\` files. The \`identity-cbor-conversion\` feature flag stays in Cargo.toml since it composes with downstream features; removing it is a breaking change for consumers who reference it. The flag now just brings in the \`cbor\` dep + enables \`value-conversion\` — pure carrier, no module gating. Cleanup of the feature itself can be a separate decision. ## public_key_in_creation/v0/mod.rs — 119 lines of commented code A 119-line block (lines 148-266) of commented-out methods on \`IdentityPublicKeyInCreationV0\` — \`from_object\`, \`from_raw_json_object\`, \`from_json_object\`, \`to_raw_object\`, \`to_raw_cleaned_object\`, \`to_raw_json_object\`, \`to_ecdsa_array\`, plus two \`#[cfg(feature = "state-transition-cbor-conversion")]\` methods (\`from_cbor_value\` / \`to_cbor_value\`). All replaced by either canonical traits (\`JsonConvertible\` / \`ValueConvertible\` derives on the type) or by the \`IdentityPublicKey\` canonical conversion methods that work via the \`From\` impl into \`IdentityPublicKey\`. The closing \`// }\` at line 266 is what gave it away — the block had been a method block on the impl, kept around as a hand-rolled reference for the canonical migration. Now stale. Verified: - cargo check -p dpp (default + identity-cbor-conversion features): clean - cargo test -p dpp --lib: 3718 passing, 0 failed - wasm-dpp2: 1120 / 0 unchanged Net: -260 lines of dead code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both steps shipped in earlier commits on this branch (\`30b43dc87b\` and \`bde42eb320\`). Update the plan doc to reflect what landed and capture two side notes: 1. The \`identity-cbor-conversion\` feature flag in rs-dpp's Cargo.toml is now a pure dep-carrier after the CBOR module deletion — no module-gating remains. Left in place to avoid a breaking change for downstream consumers. 2. The plan's earlier reference to commented blocks at \`asset_lock_proof/mod.rs:62-133\` was stale — that area was cleaned up in this branch's earlier asset-lock-proof tagged-enum work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase D step 4 of the json-value unification plan: replace explicit
\`value.remove("disabledAt")\` cleanup with
\`#[serde(skip_serializing_if = "Option::is_none")]\` on
\`IdentityPublicKeyV0::disabled_at\`. Eliminates the parallel
to_object/to_cleaned_object wire-shape divergence.
## Audit summary (per the plan's pre-merge requirement)
The "anything hashing serializations sees different bytes" risk is
ZERO for consensus paths. All hashing/signing/proof generation goes
through bincode (PlatformSerializable::serialize_to_bytes), which
is independent of serde-skip attributes:
- Identity::hash() — bincode
- Drive identity-key storage (replace_key_in_storage,
insert_key_to_storage) — bincode
- IdentityPublicKey::public_key_hash() — hashes only the raw \`data\`
bytes
- State transitions adding/updating keys use IdentityPublicKeyInCreation
which has no \`disabled_at\` field at all
- to_canonical_object / to_canonical_cleaned_object exist only on
state transitions, never on standalone IdentityPublicKey
## Wire-shape change
Visible only on the **serde-driven JSON / platform_value** path. For
the common case of a non-disabled key:
Before: { id, type, purpose, ..., data, disabledAt: null }
After: { id, type, purpose, ..., data }
\`disabledAt\` with a real timestamp (Some) emits unchanged. Round-trip
works because the field also has \`#[serde(default)]\` — deserializing
an object without a \`disabledAt\` key produces \`None\`.
## Cleanups enabled
- IdentityPublicKeyV0::to_cleaned_object → pure delegation to to_object
(was 8 lines with explicit remove("disabledAt"))
- IdentityV0::to_cleaned_object → pure delegation to to_object (was 9
lines iterating publicKeys array, calling
remove_optional_value_if_null)
- Both methods will be deletable in step 5 along with the
IdentityPlatformValueConversionMethodsV0 trait surface that wraps
them.
## Test fixture updates
- rs-dpp Identity round-trip tests (json + value paths) — drop the
\`disabledAt: null\` literal from the expected wire shape.
- wasm-dpp2 Identity.spec.ts (3 fixtures: expectedJSONOutput,
expectedJSONInput, expectedObject) — drop \`disabledAt: null\` /
\`disabledAt: undefined\` for the same reason.
- The existing \`tagged_raw_value()\` deserialization input fixture in
packages/rs-dpp/src/identity/conversion/platform_value/mod.rs
keeps \`disabledAt: Value::Null\` to verify legacy-shape input is
still accepted on deserialize.
Test results:
- rs-dpp: 3718 lib tests passing (2 wire-shape assertion updates).
- wasm-dpp2: 1120 / 0 passing (3 fixture updates).
- cargo check on wasm-dpp / wasm-dpp2 / wasm-sdk: clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Shipped in commit 7bed945. Captures the consensus audit findings inline so the next contributor knows the path was investigated and why bincode-based hashing/signing is unaffected by the serde-skip attribute. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase D step 5 of the json-value unification plan: now that
\`disabled_at\` carries \`#[serde(skip_serializing_if = "Option::is_none")]\`
(step 4), \`to_cleaned_object\` is byte-for-byte identical to
\`to_object\`. Delete the redundant trait surface.
## Deleted
- \`IdentityPlatformValueConversionMethodsV0\` (entire 1-method trait)
- file: \`identity/conversion/platform_value/v0/mod.rs\` (deleted)
- file: \`identity/v0/conversion/platform_value.rs\` (deleted, V0 impl)
- outer impl on \`Identity\` in \`identity/conversion/platform_value/mod.rs\`
- mod declaration in \`identity/v0/conversion/mod.rs\`
- The trait's only method (\`to_cleaned_object\`) had a default body
\`self.to_object()\` and the V0 impl was already pure delegation
after step 4. Canonical \`ValueConvertible::to_object\` covers the
same surface.
- \`to_cleaned_object\` method on
\`IdentityPublicKeyPlatformValueConversionMethodsV0\`
- removed from trait def (\`identity_public_key/conversion/platform_value/v0/mod.rs\`)
- removed from V0 impl (\`identity_public_key/v0/conversion/platform_value.rs\`)
- removed from outer \`IdentityPublicKey\` impl
(\`identity_public_key/conversion/platform_value/mod.rs\`)
- The trait itself stays (its \`from_object(value, &platform_version)\`
method has legitimate version-dispatch semantics that
\`ValueConvertible::from_object\` doesn't provide).
## Migrated callers
- \`IdentityV0::to_json\` / \`to_json_object\`: switched from
\`self.to_cleaned_object()\` to canonical
\`ValueConvertible::to_object\` (via the same trait import).
- \`IdentityPublicKeyV0::to_json\` / \`to_json_object\`: same.
- \`IdentityPublicKeyWasm::to_object\` (wasm-dpp2): switched from
\`self.0.to_cleaned_object()\` to fully-qualified
\`ValueConvertible::to_object(&self.0)\` (disambiguates between
canonical \`ValueConvertible\` and legacy \`*PlatformValueConversionMethodsV0\`,
both of which are in scope at the call site).
## Test fixtures updated
- \`identity_public_key/v0/conversion/platform_value.rs\`:
renamed \`to_cleaned_object_*\` tests → \`to_object_*\` (assertions
unchanged — same wire-shape behavior, just exercising the
canonical method now).
- \`identity_public_key/conversion/platform_value/mod.rs\`:
renamed \`to_cleaned_object_removes_disabled_at_when_none\` →
\`to_object_strips_disabled_at_when_none\`.
- \`identity/conversion/platform_value/mod.rs\`:
renamed \`identity_wrapper_to_cleaned_object_includes_format_version_tag\` →
\`identity_wrapper_to_object_includes_format_version_tag\`.
## Test results
- rs-dpp: 3713 lib tests passing (was 3718 — lost 5 tests: 2 deleted
with the v0/conversion/platform_value.rs file + 3 inline tests that
exercised the now-redundant \`to_cleaned_object\` method specifically).
- wasm-dpp2: 1120 / 0 unchanged.
- cargo check on rs-dpp / wasm-dpp / wasm-dpp2: clean.
Net: −2 trait files, −1 trait method, ~−110 lines of redundant code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures what shipped in commit 18034d6 (to_cleaned_object cleanup across Identity / IdentityPublicKey trait surfaces) and what's still deferred for a larger follow-up — the rest of the legacy trait method migration and the from_json_object binary-field replacement extraction. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…K trait Phase D step 5 (continued): \`IdentityPublicKeyPlatformValueConversionMethodsV0\` no longer carries \`to_object\` / \`into_object\` — both methods were byte-identical to canonical \`ValueConvertible::to_object\` / \`into_object\` (which the outer \`IdentityPublicKey\` enum already derives). The trait now exists solely as the wrapper for its version-aware \`from_object(value, &platform_version)\` method, which dispatches by \`identity_key_structure_version\` (distinct from canonical \`from_object\` that dispatches on the value's own \`\$formatVersion\` tag). ## Concrete changes - Trait def: dropped 2 method signatures, kept \`from_object\` with expanded doc explaining its raison d'etre. - V0 impl: dropped \`to_object\` / \`into_object\` bodies (each was a one-liner around \`platform_value::to_value\`). - Outer \`IdentityPublicKey\` impl: dropped 2 dispatch arms. - \`identity_public_key/v0/conversion/json.rs\`: \`to_json\` / \`to_json_object\` previously called \`self.to_object()\` from the legacy trait. Inlined \`platform_value::to_value(self)\` (the body was identical) since \`IdentityPublicKeyV0\` doesn't derive canonical \`ValueConvertible\` directly (only the outer enum does). ## Test fixture updates - Several inner-V0 tests called \`key.to_object()\` / \`.into_object()\`. Switched to direct \`platform_value::to_value\` (V0 is a leaf struct without the canonical trait). - Outer-wrapper tests in \`conversion/platform_value/mod.rs\` now import \`ValueConvertible\` for canonical \`to_object\` / \`into_object\` calls. \`from_object\` calls are explicitly fully-qualified through the legacy trait (\`<IdentityPublicKey as IdentityPublicKeyPlatformValueConversionMethodsV0>::from_object\`) because both canonical (no platform_version) and legacy (with platform_version) signatures coexist on the type. - Reworked \`to_object_delegates_to_v0_serde_shape\` (was wrong — outer's \`to_object\` includes \`\$formatVersion\` while inner V0's serde shape doesn't): the new test \`to_object_includes_format_version_tag\` asserts the outer wrapper surfaces \`\$formatVersion: "0"\` from canonical \`to_object\`. ## Test results - rs-dpp: 3712 lib tests passing (was 3713 — 1 less, dropped redundant delegation test). - wasm-dpp2: 1120 / 0 unchanged. - cargo check on rs-dpp / wasm-dpp / wasm-dpp2: clean. The remaining \`IdentityPublicKeyJsonConversionMethodsV0\` and \`IdentityJsonConversionMethodsV0\` traits stay intact — their \`to_json_object\` (validating-JSON shape) and \`from_json_object\` (binary-field replacement + version dispatch) methods provide distinct semantics from canonical \`JsonConvertible\`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…odsV0 entirely After Phase D step 5 trimmed the trait down to a single method (\`from_object(value, &platform_version)\`), examination shows the platform_version arg is dead scaffolding for V1+ that doesn't exist: - The V0 inner impl explicitly ignores it (\`_platform_version\`). - The outer dispatch only ever maps to V0 (the only structure version). - For V0 the result is byte-identical to canonical \`ValueConvertible::from_object(value)\` — both produce the same \`IdentityPublicKey\` because the outer enum is internally tagged with \`\$formatVersion\` and all V0 values carry that tag. Future-V1 migration scenarios — where "platform decides version" might diverge from "value decides version" — are speculative; if/when V1 ships, canonical's tag-driven dispatch is the correct semantic (deserialize the value as it claims to be). The legacy "override-the-tag-by-arg" form is unused everywhere it's currently called. ## Changes - Deleted \`identity_public_key/conversion/platform_value/v0/mod.rs\` (trait def, was a 1-method trait). - Deleted \`identity_public_key/v0/conversion/platform_value.rs\` (V0 impl + the \`TryFrom<&IdentityPublicKeyV0> for Value\` and \`TryFrom<Value> for IdentityPublicKeyV0\` helpers that lived alongside it — they were leftover scaffolding). - Outer impl on \`IdentityPublicKey\` removed; the file now contains only the test module exercising canonical \`ValueConvertible\` round trips. - Removed \`mod platform_value\` from \`identity_public_key/v0/conversion/mod.rs\`. ## Caller migration - wasm-dpp2 \`partial_identity.rs:387\`: switched from \`<IdentityPublicKey as IdentityPublicKeyPlatformValueConversionMethodsV0>::from_object(value, platform_version)\` to \`<IdentityPublicKey as ValueConvertible>::from_object(value)\`. - wasm-dpp2 \`public_key.rs:412\`: \`fromObject\` keeps the JS API signature \`(value, platformVersion)\` for SDK consistency, but routes through canonical \`ValueConvertible::from_object\`. The \`platform_version\` arg is now \`_platform_version\` — reserved for future use, not load-bearing. - rs-dpp \`identity_public_key/v0/conversion/json.rs\`: \`from_json_object\` switched from \`Self::from_object(value, platform_version)\` to \`platform_value::from_value(value)\` (the TryFrom impls that previously routed through serde lived in the deleted file). ## Test results - rs-dpp: 3704 lib tests passing (was 3712 — 8 fewer: dropped delegation tests that exercised the now-deleted trait surface). - wasm-dpp2: 1120 / 0 unchanged. - cargo check: clean across rs-dpp / wasm-dpp / wasm-dpp2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…bject Same dead-scaffolding pattern as the value-path cleanup (\`IdentityPublicKeyPlatformValueConversionMethodsV0::from_object\`): \`from_json_object\` took a \`&PlatformVersion\` arg, the V0 inner impl ignored it (\`_platform_version\`), and the outer dispatcher always mapped to V0 (the only structure version that exists). For V0 only, the result is byte-identical regardless of the arg. Trim the signature to \`from_json_object(raw_object) -> ProtocolError\`. The trait still keeps \`to_json_object\` (validating-JSON shape) and \`from_json_object\` (binary-field replacement) since both have real semantic content distinct from canonical \`JsonConvertible\`. Updated wasm-dpp2 callers (\`IdentityPublicKey.fromJSON\`, \`PartialIdentity.fromJSON\`) to drop the unused arg. The wasm wrapper JS API still accepts \`platformVersion\` for SDK consistency, prefixed \`_\` since it's reserved for future use. Test results: - rs-dpp: 3704 lib tests passing. - wasm-dpp2: 1120 / 0 unchanged. Also expanded the trait doc comment so its remaining purpose (validating-JSON shape support, complementary to canonical JsonConvertible) is unambiguous. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cy traits
The wasm-dpp2 \`IdentityPublicKey.toJSON\` / \`fromJSON\` JS API was
exposing a deviant wire shape: validating-JSON form with binary fields
as JSON arrays of u8 values. Every other rs-dpp type's canonical JSON
shape uses base64 strings — including the embedded public keys inside
\`IdentityWasm.toJSON\`. The two paths produced *different* shapes for
the same data depending on whether you serialized the wrapping
Identity or the standalone IdentityPublicKey.
Switch \`IdentityPublicKeyWasm.toJSON\` / \`fromJSON\` and
\`PartialIdentityWasm\`'s inner-key deserialization to canonical
\`JsonConvertible\`. Now: same shape everywhere, base64 strings for
binary, base58 strings for identifiers.
That eliminates the only production reason to keep the legacy JSON
conversion traits. Audit confirmed:
- \`IdentityJsonConversionMethodsV0\` (Identity, not IPK): zero
non-test callers anywhere in the workspace. Was already dead.
- \`IdentityPublicKeyJsonConversionMethodsV0\` (IPK): only callers
were the wasm-dpp2 sites we just migrated.
## Deletions
Six files removed:
- \`identity/conversion/json/v0/mod.rs\` (\`IdentityJsonConversionMethodsV0\` trait def)
- \`identity/conversion/json/mod.rs\` (outer wrapper, was \`pub use v0::*;\`)
- \`identity/v0/conversion/json.rs\` (V0 impl + tests, ~165 lines)
- \`identity_public_key/conversion/json/v0/mod.rs\` (\`IdentityPublicKeyJsonConversionMethodsV0\` trait def)
- \`identity_public_key/conversion/json/mod.rs\` (outer impl + tests, ~85 lines)
- \`identity_public_key/v0/conversion/json.rs\` (V0 impl + \`TryFrom<&str>\` + tests, ~170 lines)
Module declarations removed from:
- \`identity/conversion/mod.rs\` (\`pub mod json\`)
- \`identity/v0/conversion/mod.rs\` (\`pub mod json\`)
- \`identity_public_key/conversion/mod.rs\` (\`pub mod json\`)
- \`identity_public_key/v0/conversion/mod.rs\` (\`mod json\`)
## Test fixture updates
- \`wasm-dpp2/tests/unit/IdentityPublicKey.spec.ts\`:
\`toJSON\` test was asserting \`Array.from(json.data).deep.equal(...)\`
against byte-array form. Switched to
\`expect(json.data).to.equal('A2o5...=')\` for the canonical base64
string. The \`fromJSON\` test was already passing a base64 string
fixture (\`data: 'A2o5...'\`) — it kept working through the legacy
\`from_json_object\` because \`replace_at_paths\` accepts both
shapes; now it goes through canonical \`JsonConvertible::from_json\`
which expects base64 directly.
## Test results
- rs-dpp: 3686 lib tests passing (was 3704 — lost 18 from the deleted
trait test modules, all of which were exercising methods that no
longer exist).
- wasm-dpp2: 1120 / 0 passing (1 fixture updated, full green).
- cargo check on rs-dpp / wasm-dpp / wasm-dpp2: clean.
Net: −425 lines of legacy trait code, single canonical JSON wire shape
across the entire SDK surface for identity-family types.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…yInto impls
Phase D step 6 cleanup. The Critical-2 fix from earlier on this branch
already gave \`AssetLockProof\` a tagged-enum representation
(\`#[serde(tag = "type")]\`) and a manual \`Deserialize\` that routes
through \`RawAssetLockProof\` for the instant-lock raw-bytes shape.
What remained was three asymmetric helpers that produced *untagged*
\`platform_value::Value\` (dropping the variant tag entirely), so their
output couldn't round-trip through the manual \`Deserialize\`:
- \`AssetLockProof::to_raw_object\` (inherent method)
- \`TryInto<Value> for AssetLockProof\`
- \`TryInto<Value> for &AssetLockProof\`
All three had **zero production callers** workspace-wide (verified
across rs-dpp, rs-drive, rs-drive-abci, rs-sdk, wasm-dpp, wasm-dpp2,
wasm-sdk). The only callers were tests in the same module exercising
the dead helpers.
Deleted them. Use canonical \`ValueConvertible::to_object\` instead —
it produces the correctly-tagged shape (\`{type: "instant" | "chain",
...fields}\`) that round-trips through \`Deserialize\`.
The lingering \`TryFrom<&Value>\` / \`TryFrom<Value> for AssetLockProof\`
hacks (which accept legacy integer-tag and externally-tagged shapes)
are KEPT for now — they have 2 production callers in
state-transition value-conversion paths, and migrating them requires
auditing every upstream that produces a state transition's raw Value.
A TODO comment in the source already flagged this. Follow-up work.
Test fixture updates:
- \`chain_proof_to_raw_object\` test renamed to
\`chain_proof_to_object_canonical\`, switched to
\`ValueConvertible::to_object\`.
- \`chain_proof_value_round_trip\` rewritten to exercise the canonical
\`to_object\` -> \`from_object\` round trip with a wire-shape
assertion that the result has \`type: "chain"\` (not the legacy
integer tag form).
- \`try_into_value\` test module deleted (exercised the now-deleted
\`TryInto<Value>\` impls).
Test results:
- rs-dpp: 3684 lib tests passing (was 3686 — lost 2 tests that
exercised the deleted impls).
- wasm-dpp2: 1120 / 0 unchanged.
- cargo check: clean across rs-dpp, wasm-dpp, wasm-dpp2.
Net: ~50 lines of asymmetric dead code removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…from_value
Phase D step 6, completing what the in-source TODO comment promised:
// todo: replace with
// from_value(value.clone()).map_err(ProtocolError::ValueError)
The hack accepted two pre-Critical-2 wire shapes that no longer flow:
1. Integer-tagged: \`{type: 0|1, ...fields}\` (used \`AssetLockProofType\`
enum's u8 discriminant)
2. Externally-tagged: \`{Instant: {...fields}}\` / \`{Chain: {...}}\`
(variant name as map key)
After the Critical-2 fix earlier in this branch made AssetLockProof
internally tagged with \`#[serde(tag = "type")]\`, the canonical
serialization produces \`{type: "instant" | "chain", ...fields}\` —
incompatible with both legacy shapes. The hack was simultaneously
broken for canonical input (its \`get_optional_integer("type")\` call
errors when \`type\` is a string) AND unreachable for legacy input
(nothing in the workspace produces those shapes anymore).
Audit:
- Production callers of \`AssetLockProof::try_from(&Value)\` /
\`try_from(Value)\`: only 2 — \`IdentityTopUpTransitionV0::from_object\`
and \`IdentityCreateTransitionV0::from_object\`, both legacy
\`StateTransitionValueConvert\` (A1) trait methods.
- External callers of those legacy A1 \`from_object\` methods:
zero in rs-drive / rs-drive-abci / rs-sdk / wasm-dpp / wasm-dpp2 /
wasm-sdk for these specific transitions. (wasm-dpp uses A1 for
DataContract create/update only.)
- All 3684 rs-dpp lib tests still pass with the hack replaced —
confirming nothing was relying on the legacy-shape acceptance.
Replaced both \`TryFrom\` impls with one-line \`platform_value::from_value\`
calls. The \`Deserialize\` impl handles the routing through
\`RawAssetLockProof\` for the instant-lock raw-bytes shape.
Test results:
- rs-dpp: 3684 / 0
- wasm-dpp / wasm-dpp2 / wasm-sdk: cargo check clean
- wasm-dpp2: 1120 / 0
Net: −56 lines of dead-on-arrival code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…notes ## Step 5 — Identity family canonical migration ✅ DONE Updates the plan doc to reflect that ALL FOUR legacy identity-family conversion traits have been deleted entirely (across multiple commits on this branch): - \`IdentityPlatformValueConversionMethodsV0\` (1-method, redundant after step 4 \`skip_serializing_if\`) - \`IdentityPublicKeyPlatformValueConversionMethodsV0\` (4 methods, including the formerly-defended \`from_object(value, &platform_version)\` whose platform_version dispatch turned out to be dead scaffolding — V0 ignored the arg, only V0 exists, canonical tag-driven dispatch is byte-identical) - \`IdentityJsonConversionMethodsV0\` (3 methods, zero non-test callers) - \`IdentityPublicKeyJsonConversionMethodsV0\` (3 methods including validating-JSON shape — wasm-dpp2 IdentityPublicKey JS API switched to canonical base64 strings, matching every other rs-dpp type) Documents what shipped + what didn't (the validating-JSON byte-array shape was deliberately dropped — every other type's canonical JSON output uses base64 strings, the IdentityPublicKey deviation was an SDK API inconsistency). ## Step 6 — AssetLockProof tagged-enum + dead helpers ✅ DONE Documents the two-part work: the original Critical-2 fix (tagged-enum representation, manual Deserialize via RawAssetLockProof, canonical traits) plus the asymmetric-helper deletion (\`to_raw_object\`, \`TryInto<Value>\`, the \`TryFrom\` hack that accepted pre-Critical-2 legacy shapes which no longer flow anywhere). ## Step 8 — Document family ⬜ DEFERRED Captures the audit findings for the follow-up PR: production callers in rs-drive's document-update consensus path, the genuine semantic divergence of \`to_map_value\` (returns BTreeMap, not Value), \`from_json_value\` (manual ingest), and \`to_json_with_identifiers_using_bytes\` (validating-JSON shape). Lists the specific call sites and the sequence of steps a follow-up PR should take, including the hash-equivalence audit needed for rs-drive. No code changes — pure documentation update. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ferent methods Phase D step 8 slice A. The two legacy Document conversion traits had a mix of redundant methods (1:1 canonical equivalents) and methods with genuine semantic content. Trim the redundant ones, keep the rest with clearer documentation of why they exist. ## DocumentPlatformValueMethodsV0 (A11) Deleted from trait + V0 impl + outer impl + ExtendedDocumentV0 impl: - \`to_object\` — 1:1 equivalent of canonical \`ValueConvertible::to_object\`. - \`into_value\` — 1:1 equivalent of canonical \`ValueConvertible::into_object\`. Kept (with expanded doc comments explaining why): - \`to_map_value\` / \`into_map_value\` — return \`BTreeMap<String, Value>\`, used by ExtendedDocument and DocumentWasm to compose Document plus metadata fields. Canonical returns \`Value::Map(Vec<...>)\`, not the map directly. - \`from_platform_value(value, &platform_version)\` — **legacy-shape ingest**. Accepts un-tagged Document values (no \`\$formatVersion\`). Symmetric with \`from_json_value\` on the JSON side. Used to ingest DPNS / DashPay legacy JSON fixtures and older stored shapes that predate \`#[serde(tag = "\$formatVersion")]\`. Initially audited as "dead scaffolding for V1+" — actually load-bearing for legacy ingest. ExtendedDocument's \`from_trusted_platform_value\` / \`from_untrusted_platform_value\` and the \`json_should_generate_human_readable_binaries\` test rely on it. ## DocumentJsonMethodsV0 (A10) Deleted from trait + V0 impl + outer impl + ExtendedDocumentV0 impl: - \`to_json\` — 1:1 equivalent of canonical \`JsonConvertible::to_json\`. Its body was \`self.to_object()?.try_into()\` — same as canonical. Kept: - \`to_json_with_identifiers_using_bytes\` — validating-JSON wire shape (bs58 string identifiers + binary fields as JSON arrays of u8). Used by JSON Schema validators. - \`from_json_value<S, E>\` — generic over identifier deserialization type, accepts JSON without \`\$formatVersion\`. Legacy-shape ingest. ## Internal call-site updates - \`document/v0/cbor_conversion.rs\`: \`self.to_object()\` (deleted V0 trait method) -> inlined \`platform_value::to_value(self)\`. CBOR conversion goes through the same path. - \`document/v0/json_conversion.rs\` tests: \`doc.to_json(...)\` -> \`serde_json::to_value(&doc)\` (DocumentV0 doesn't derive JsonConvertible directly). - \`extended_document/v0/mod.rs\`: \`pub fn to_pretty_json\` inlined what legacy \`to_json\` body did (\`to_object()?.try_into()\`) — goes through platform_value as an intermediate, which is what the test fixture asserts (binary as base64, identifiers as bs58). Canonical \`JsonConvertible::to_json\` would go directly via serde_json (one-step), producing a subtly different shape due to Critical-1 (is_human_readable divergence). - \`extended_document/mod.rs\`: \`pub fn to_json\` (outer inherent, takes &PlatformVersion) routes through canonical \`JsonConvertible::to_json\` — platform_version arg kept for API compatibility but isn't load-bearing. ## Test results - rs-dpp: 3677 lib tests passing (was 3684 — lost 7 tests that exercised the deleted methods; round-trip coverage moved to canonical-trait round-trip tests where applicable). - wasm-dpp2: 1120 / 0 unchanged. - rs-drive: cargo check clean. Net: ~−170 lines of redundant code; clearer trait surface. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 8 slice A shipped in commit 678121a. Update the plan doc with: 1. What landed: deleted the 1:1 canonical-equivalent methods from both Document family legacy traits (\`to_object\`, \`into_value\` from A11; \`to_json\` from A10), kept the rest with expanded doc comments. 2. Audit course-correction on \`from_platform_value\`: my initial pass dismissed it as "dead scaffolding for V1+" — wrong. The method accepts un-tagged Document values that canonical \`ValueConvertible::from_object\` errors on (DPNS legacy fixtures, ExtendedDocument's \`from_trusted_platform_value\`). Reverted that part of the migration; kept the method as legacy-shape ingest (symmetric with \`from_json_value\`). 3. Slice B deferred work: wasm-dpp2 DocumentWasm.fromObject migration, rs-drive test-fixture migration, \`to_map_value\` API decision. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase D step 8 slice B finishes the Document-family canonical migration
by removing both legacy ingest methods and routing every caller through
canonical ValueConvertible / JsonConvertible.
Trait deletions (rs-dpp):
- DocumentPlatformValueMethodsV0::from_platform_value
- DocumentJsonMethodsV0::from_json_value<S, E>
Traits now hold only the methods with no canonical equivalent:
A11 -> to_map_value / into_map_value (BTreeMap<String,Value> shape)
A10 -> to_json_with_identifiers_using_bytes (validating-JSON shape)
Caller migrations:
- wasm-dpp2 DocumentWasm.toObject emits \$formatVersion: \"0\";
fromObject/fromJSON use canonical from_object / from_json.
- ExtendedDocumentV0 from_trusted_platform_value /
from_untrusted_platform_value insert \$formatVersion via the
new ensure_document_format_version helper, then delegate to
canonical Document::from_object. Public API unchanged.
- wasm-dpp legacy DocumentWasm.fromObject inserts \$formatVersion
inline before canonical (minimum-touch, JS surface unchanged).
- rs-drive 4 sites in drive/document/update/mod.rs and 7 sites in
tests/query_tests.rs migrated to local document_from_legacy_value
helpers using the same insert-tag-then-canonical pattern.
- Removed obsolete tests in document/v0/json_conversion.rs that
targeted the deleted from_json_value method; canonical round-trip
is covered in serialization_traits and v0/serialize.rs.
Verified:
cargo test -p dpp --features all_features_without_client --lib
-> 3670 passed, 0 failed, 8 ignored
cargo test -p drive --lib drive::document::update::tests
-> 39 passed
cargo check -p dpp -p drive -p wasm-dpp -p wasm-dpp2 --tests
clean (only pre-existing warnings)
Net for Phase D step 8 (slice A + B): legacy A10 / A11 traits reduced
from 6 methods to 3, with the 3 survivors documenting why they stay.
Phase D step 9 removes both legacy state-transition trait families
entirely. The audit reframed the work after three findings:
1. Signing is bincode (PlatformSignable -> signable_bytes()), not
JSON canonical. The to_canonical_object / to_canonical_cleaned_object
machinery on A1 was vestigial JS-DPP-era scaffolding with zero
production callers - only its own tautological tests called it.
2. Outer enums already had canonical JsonConvertible / ValueConvertible
derives from Phase C; A1 / A2 were running in parallel doing the
same work.
3. Cross-package use was tiny: 2 wasm-dpp legacy files used
to_cleaned_object. wasm-dpp2 had zero A1 / A2 callers.
What landed (4 slices in one commit, after the audit collapsed
"step 9 long pole" to a deletion):
Slice 1 + 3: deleted both traits and 68 impl files
- traits/state_transition_value_convert.rs
- traits/state_transition_json_convert.rs
- 34 value_conversion.rs + 34 json_conversion.rs (per transition x
{inner, outer} x {V0, V1})
- removed mod declarations from 34 parent mod.rs files
- removed re-exports from traits/mod.rs
Slice 2: migrated 2 wasm-dpp legacy callers to canonical
- DataContractCreateTransitionWasm / DataContractUpdateTransitionWasm
constructor + toObject use canonical ValueConvertible::to_object()
plus manual signature path stripping for skip_signature
- constructors use insert-$formatVersion-then-canonical pattern
matching the Document migration
- state_transition_facade.rs is dead (module not exposed); no fix
needed there
Slice 4 (subset): added #[json_safe_fields] to 5 V0 inners that lacked it
- AddressCreditWithdrawalTransitionV0
- AddressFundingFromAssetLockTransitionV0
- AddressFundsTransferTransitionV0
- IdentityCreateFromAddressesTransitionV0
- IdentityTopUpFromAddressesTransitionV0
Slice 4 (deferred): BatchTransitionV0 / V1 cannot get #[json_safe_fields]
yet - the attribute generates compile-time JsonSafeFields assertions
on field types and DocumentTransition / BatchedTransition (and their
sub-transitions) need their own JsonSafeFields impls first. Follow-up
for the BatchTransition family migration.
Test cleanup: subagent deleted 76 tautology tests across 13 test
modules that were exercising the removed methods. Canonical round-trip
on outer enums via json_convertible_tests / value_convertible_tests
covers correctness.
Verification:
cargo test -p dpp --features all_features_without_client --lib
-> 3594 passed, 0 failed, 8 ignored (was 3670; -76 tautology)
cargo check -p drive -p wasm-dpp -p wasm-dpp2 -p dash-sdk -p drive-abci --tests
clean (only pre-existing warnings)
Net: 219 lines added, 5051 lines deleted across 108 files.
…lpers Plan-doc refresh: - Updated progress table (now 2026-05-09): Phase D 1-9 done, steps 10-11 remain. Pass 4 (wasm-dpp2 _serde! migration) reframed: step 9 audit showed wasm-dpp2 had no actual A1/A2 blockers, so the remaining _serde! sites need re-survey to identify actual blockers. - Critical-5 (sorted-keys-for-signing) marked FALSIFIED — signing is bincode, the canonical-object machinery had zero production callers. - A1/A2 rows in §3.1 trait table struck out with deletion commit ref. - §3.5 catalogue: state_transition trait + impl files marked deleted. - §3.10 affected-type total: pre-Phase D ~90 non-canonical types collapsed to ~10-15 remaining (DataContract KEEP-AS-EXCEPTION, AddressWitness/ContestedIndexFieldMatch step 11, wasm-dpp legacy). - §3.11 step 7 (ExtendedDocument C1) marked DONE with commit ref. - Phase D summary section now lists step-by-step status. Code: deleted abstract_state_transition.rs - the state_transition_helpers module had become dead after step 9 removed all callers (the trait methods were the only consumers of to_object/to_cleaned_object/to_json helpers). Verified zero remaining users via grep, then removed the mod declaration and pub use re-export from state_transition/mod.rs. Verification: 3594/3594 dpp lib tests pass; workspace --tests check clean.
Step 9 follow-up: completes the JS-safe integer protection for the
BatchTransition tree, deferred at step 9 because it required a deeper
walk through the document/token transition sub-tree.
Attribute applied to V0 inners that were missing it (8 leaves):
- DocumentDeleteTransitionV0
- TokenFreezeTransitionV0
- TokenUnfreezeTransitionV0
- TokenDestroyFrozenFundsTransitionV0
- TokenClaimTransitionV0
- TokenEmergencyActionTransitionV0
- TokenConfigUpdateTransitionV0
- TokenSetPriceForDirectPurchaseTransitionV0
Plus BatchTransitionV0 / BatchTransitionV1 themselves (deferred at
step 9 with explanatory comments; comments removed, attribute applied).
Manual JsonSafeFields impls added in safe_fields.rs for 7 types where
the `#[json_safe_fields]` macro doesn't reach (variant-internal u64s
or wrapper enums with manual `impl JsonConvertible`):
- DocumentTransition, TokenTransition, BatchedTransition (wrapper
enums - their inner V0 leaves are all json_safe_fields-annotated,
so safe by induction)
- TokenEmergencyAction (unit-variant enum)
- TokenDistributionType (unit-variant enum)
- TokenPricingSchedule (escape-hatch pattern - tuple variants hold
Credits / BTreeMap<TokenAmount, Credits>; matches existing
TokenEvent pattern)
- TokenConfigurationChangeItem (escape-hatch - tuple variants hold
Option<TokenAmount> / Option<GroupContractPosition>)
Also: made DocumentTransition / TokenTransition enum re-exports public
in batch_transition/batched_transition/mod.rs (were `use`, now `pub use`)
so safe_fields.rs can name them via `crate::...::batched_transition::X`.
Verification:
cargo test -p dpp --features all_features_without_client --lib
-> 3594 passed, 0 failed, 8 ignored
cargo check -p drive -p wasm-dpp -p wasm-dpp2 -p dash-sdk -p drive-abci --tests
clean (only pre-existing warnings)
Plan / memory updated to mark step 9 follow-up done. Phase D steps
1-9 + follow-up are now complete; remaining: step 10 (DataContract
KEEP-AS-EXCEPTION rename pass) and step 11 (AddressWitness /
ContestedIndexFieldMatch manual-impl refactor).
…dexFieldMatch
Phase D step 11 — replace custom Serialize/Deserialize impls with serde
attributes, gated on round-trip + wire-shape parity tests.
AddressWitness (address_funds/witness.rs):
- Replaced ~115 lines of manual serde with `#[serde(tag = "type")]`
internal tagging.
- Explicit `rename = "p2pkh"` / `rename = "p2sh"` on variants
(camelCase rule is ambiguous for `P2pkh` / `P2sh`).
- `redeem_script` field gets explicit `rename = "redeemScript"`.
- Behavior change: `MAX_P2SH_SIGNATURES` no longer enforced on the
JSON/Value deserialize path. The bincode `Decode` impl still
enforces it (the load-bearing wire format). Documented in the
type's doc comment. JSON/Value is dev/SDK-facing; downstream
consumers must validate signature counts before re-serializing.
- Wire shape unchanged. The existing 4 round-trip tests in
`json_convertible_tests` (P2PKH / P2SH x JSON / Value) keep
passing — byte-for-byte parity confirmed.
ContestedIndexFieldMatch (data_contract/document_type/index/mod.rs):
- Replaced ~95 lines of manual serde with
`#[serde(rename_all = "snake_case")]` externally-tagged enum.
- LazyRegex gets `serde(from = "String", into = "String")` so it
round-trips as a bare string (the `regex: OnceLock<Regex>` field
is reconstructed lazily on use).
- Bug fix: previous custom Serialize emitted `{"Regex": ...}`
(PascalCase) while custom Deserialize expected `{"regex": ...}`
(snake_case) — the type was non-round-trippable through serde.
New impl is consistently snake_case in both directions.
- No production callers identified — production data-contract
loading uses the unrelated Value-walking `regexPattern` path.
- Added 4 round-trip + wire-shape parity tests:
`json_round_trip_contested_index_field_match_regex`
`json_round_trip_contested_index_field_match_positive_integer`
`value_round_trip_contested_index_field_match_regex`
`value_round_trip_contested_index_field_match_positive_integer`
First two assert exact JSON shape `{"regex": "..."}` /
`{"positive_integer_match": N}`.
Verification:
cargo test -p dpp --features all_features_without_client --lib
-> 3598 passed (was 3594; +4 new), 0 failed, 8 ignored
cargo check -p drive -p wasm-dpp -p wasm-dpp2 -p dash-sdk -p drive-abci --tests
clean (only pre-existing warnings)
Net: ~-210 lines, both types now go through pure serde derive. Phase D
steps 4-9 + 11 all complete; only step 10 (DataContract — KEEP-AS-EXCEPTION
rename pass) remains in the deprecation order.
Step 11 follow-up: my initial migration picked snake_case for the
ContestedIndexFieldMatch wire shape, justified as "preserve what the
existing Deserialize accepted." That was the wrong call:
1. The previous serde impl was already non-round-trippable (Serialize
emitted PascalCase, Deserialize expected snake_case), so there's no
real wire shape to preserve.
2. There are zero production callers — data-contract loading uses an
unrelated Value-walking `regexPattern` path.
3. The codebase convention is camelCase for JSON wire shapes
(`$formatVersion`, `redeemScript`, etc.). snake_case sticks out.
Changed `serde(rename_all = "snake_case")` -> `serde(rename_all =
"camelCase")` on the enum. Updated the parity test fixture from
`{"positive_integer_match": 42}` to `{"positiveIntegerMatch": 42}`.
The `Regex` variant is unchanged (single-word lowercase identical
between snake_case and camelCase).
Verified: 3598/3598 dpp lib tests pass.
…al-4
Phase D step 10 — three pieces landing together:
1. Critical-4 pinned in regression tests
(data_contract/conversion/serde/mod.rs::data_contract_serde_pins_critical_4):
- data_contract_round_trips_through_serde_json: serde_json round-trip
works at the active platform version.
- data_contract_serialize_matches_serialization_format_at_current_version:
DataContract::serialize is byte-equivalent to
DataContractInSerializationFormat::serialize at LATEST_PLATFORM_VERSION
— proves the manual impl is a thin format-routing wrapper, not a
custom shape.
- data_contract_deserialize_rejects_invalid_schema_via_full_validation:
DataContract::deserialize rejects a contract with an indices entry
referencing a nonexistent property — proves the hardcoded
full_validation=true runs on the JSON/Value/CBOR ingest path.
Module-level doc explains the impurity rationale (Critical-4 in the
unification plan) so future readers / refactors understand the
constraint.
2. Method rename pass to disambiguate from canonical traits:
DataContractJsonConversionMethodsV0::
from_json -> from_json_versioned
to_json -> to_json_versioned
to_validating_json (unchanged — no clash)
DataContractValueConversionMethodsV0::
from_value -> from_value_versioned
to_value -> to_value_versioned
into_value -> into_value_versioned
The `_versioned` suffix makes it visually obvious that these are NOT
canonical JsonConvertible::to_json (zero args) but the version-aware
path that takes &PlatformVersion and a full_validation flag. ~26 call
sites updated across rs-dpp / rs-drive / rs-drive-abci / wasm-dpp /
wasm-dpp2 / dash-sdk / rs-sdk-ffi.
3. KEEP-AS-EXCEPTION rationale documented at three sites:
- conversion/json/v0/mod.rs (trait def)
- conversion/value/v0/mod.rs (trait def)
- data_contract/mod.rs:112-120 (outer enum, already had a comment;
updated to reference the new method names)
The traits stay because DataContract is a versioned enum routed
through DataContractInSerializationFormat. Both the platform version
and full_validation flag are inputs to the conversion that canonical
`JsonConvertible` / `ValueConvertible` (with their parameter-free
signatures) cannot express. The rename UNBLOCKS adding canonical
traits in the future if we choose to (no longer ambiguous), but
doesn't do so now.
Drive-by: rs-sdk-ffi/src/document/queries/search.rs needed an explicit
`use ValueConvertible;` import — pre-existing E0599 surfaced during the
verification cargo check across the workspace, fixed inline since it
was a one-line block on getting the workspace clean.
Verification:
cargo test -p dpp --features all_features_without_client --lib
-> 3601 passed, 0 failed, 8 ignored (was 3598; +3 from new
Critical-4 pin tests)
cargo check -p dpp -p drive -p drive-abci -p wasm-dpp -p wasm-dpp2 -p dash-sdk -p rs-sdk-ffi --tests
clean (only pre-existing warnings)
Phase D unification plan steps 1-11 are now complete. The plan's
"deprecate non-canonical mechanisms" pass is done for all non-DataContract
types; DataContract intentionally retains its version-aware traits as
the documented exception.
Step 10 follow-up. Flips the hardcoded full_validation=true in the
manual `Deserialize` impl on DataContract to false, making schema
validation opt-in instead of a hidden default.
Why:
- Most production callsites load already-validated contracts from
storage and were paying validation cost twice.
- Hidden behavior in serde Deserialize doesn't match the rest of the
codebase's serde semantics (where Deserialize means "structurally
well-formed").
- Trust-but-verify boundaries (SDK ingest, JSON fixtures, gRPC
handlers) were already plumbing the `full_validation` flag
explicitly through `from_*_versioned` and don't depend on the
implicit canonical-Deserialize default.
Audit confirmed canonical `Deserialize` had zero production callers
depending on its implicit validation: only ExtendedDocument's nested
round-trip and the Critical-4 pin tests themselves exercised it.
Production callers using `from_*_versioned(_, true, _)` keep working
unchanged (they explicitly opt in); those using `from_*_versioned(_,
false, _)` keep working unchanged (they explicitly opt out); only the
canonical path semantics change, and they change to match the rest of
the codebase.
Critical-4 pin test renamed
`data_contract_deserialize_does_not_validate_by_default` and now
asserts both halves of the new contract:
- canonical `serde_json::from_value::<DataContract>(...)` ACCEPTS a
contract with an invalid `indices` schema entry (no validation).
- explicit `DataContract::from_json_versioned(_, true, _)` REJECTS
the same payload (opt-in validation runs).
Module-level doc on conversion/serde/mod.rs updated:
- "Validation policy: opt-in, not default" section explains the new
contract.
- Critical-4 framing updated to focus on the platform-version coupling
(which stays) rather than the hardcoded validation (which is gone).
Verification:
cargo test -p dpp --features all_features_without_client --lib
-> 3601 passed, 0 failed, 8 ignored (no count change)
cargo check -p drive -p drive-abci -p wasm-dpp -p wasm-dpp2 -p dash-sdk -p rs-sdk-ffi --tests
clean (only pre-existing warnings)
Plan + memory updated to reflect the four-piece Step 10 landing
(Critical-4 pin + rename + KEEP-AS-EXCEPTION docs + validation flip).
User-directed cleanup of step 10's earlier rename pass. The `_versioned`
naming was misleading — every conversion path uses the platform version,
including canonical (via `get_current()`). The actual semantic split is
**validation: yes / no**.
Final API:
WITHOUT validation (the cheap path):
serde_json::from_value::<DataContract>(...)
platform_value::from_value::<DataContract>(...)
serde_json::to_value(&dc)
platform_value::to_value(&dc)
WITH validation (opt-in, trust-boundary path):
DataContract::from_json_validated(json, &pv)
DataContract::from_value_validated(value, &pv)
Kept (different concept):
to_validating_json(&pv) — JSON Schema-compatible output with
binary fields as u8 arrays
Deleted entirely:
- DataContractJsonConversionMethodsV0::from_json_versioned (had a
bool full_validation that's now baked into the method name choice)
- DataContractJsonConversionMethodsV0::to_json_versioned (canonical
serde_json::to_value does the same via get_current())
- DataContractValueConversionMethodsV0::from_value_versioned
- DataContractValueConversionMethodsV0::to_value_versioned
- DataContractValueConversionMethodsV0::into_value_versioned
Caller migration (~30 sites across rs-dpp / rs-drive / rs-drive-abci /
wasm-dpp / wasm-dpp2 / dash-sdk / rs-sdk-ffi):
- from_*_versioned(value, true, &pv) -> from_*_validated(value, &pv)
- from_*_versioned(value, false, &pv) -> canonical serde/platform_value
from_value (caller's pv is
usually current; for the few
sites that need pv to control
variant dispatch, deserialize
via DataContractInSerializationFormat
then try_from_platform_versioned
— same internal shape the old
method had)
- from_*_versioned(value, dynamic_bool, &pv) -> if/else bridge between
validated and canonical
(preserves dynamism)
- to_*_versioned(&pv) / into_value_versioned(&pv) -> canonical to_value /
serde_json::to_value
(DataContract doesn't
implement canonical
JsonConvertible/
ValueConvertible directly
per its doc comment;
use the function form)
Notable judgment call: tests/json_document.rs test-fixture loader needs
caller-provided &pv to control which DataContract variant comes back
(history-replay scenarios). Bridge's else-branch deserializes through
DataContractInSerializationFormat (serde(tag = "$formatVersion") enum
dispatches by tag), then calls try_from_platform_versioned(format,
false, &mut vec![], pv) for caller-pv variant dispatch. This preserves
the exact internal shape the old from_json_versioned(value, false, pv)
had.
Verification:
cargo test -p dpp --features all_features_without_client --lib
-> 3601 passed, 0 failed, 8 ignored (no count change)
cargo check -p dpp -p drive -p drive-abci -p wasm-dpp -p wasm-dpp2 -p dash-sdk -p rs-sdk-ffi --tests
clean
Plan + memory updated. Phase D unification work complete; DataContract
no longer has the misleading `_versioned` ceremony — validation is the
real axis, and it's expressed in the type system via method name.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Unification work for the canonical
JsonConvertible/ValueConvertibletraits acrosspackages/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
json_convertible_testsacross ~95 types.#[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)OutPointround-trip viaplatform_value::Value::Mapfails — affectsChainAssetLockProof,AddressFundingFromAssetLockTransition,ShieldFromAssetLockTransition. JSON works.[u8; N]fixed-array fields with custom serializers fail platform_value round-trip ("Invalid symbol 17, offset 0") — affectsExtendedBlockInfosignature and all 5 shielded transitions' Orchard fields. JSON works.DataContractdocument_schemaslose 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:to_json→from_json(and same for value).$formatVersionpreservation 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
PartialEqonStateTransitionProofResult+StoredAssetLockInfo(was missing, blocked round-tripassert_eq).Out of scope (genuinely residual — needs upstream work)
ValidatorSet— needs real BLS public key construction (crypto setup).ExtendedDocument— Critical-3 known-broken serde (writesversion, reads$version).StateTransitionumbrella —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
cargo test -p dpp --features=json-conversion,value-conversion,serde-conversionand sees 3619 pass / 0 fail.docs/json-value-unification-plan.md§10b for the 3 surfaced bugs and decides priority for follow-up fix.🤖 Generated with Claude Code