Skip to content

feat(dpp): unify JSON/Value conversion traits#3573

Merged
shumkov merged 215 commits into
v4.1-devfrom
feat/json-convertible-address-transitions
Jul 10, 2026
Merged

feat(dpp): unify JSON/Value conversion traits#3573
shumkov merged 215 commits into
v4.1-devfrom
feat/json-convertible-address-transitions

Conversation

@shumkov

@shumkov shumkov commented Apr 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

Unification of JSON/Value conversion across rs-dpp + wasm-dpp2: canonical JsonConvertible / ValueConvertible traits on every domain type, ~80 trait impls + ~200 round-trip tests, and a coordinated deprecation sweep that removed all 5 documented Critical bugs and most legacy non-canonical conversion mechanisms.

What landed (high-level)

  • Pass 1: canonical JsonConvertible / ValueConvertible impls on ~80 rs-dpp domain types.
  • Pass 2: ~200 dedicated json_convertible_tests / value_convertible_tests modules with full wire-shape assertions per type.
  • Phase D (steps 1–11): deprecation and deletion of non-canonical conversion mechanisms. Removed StateTransitionValueConvert/StateTransitionJsonConvert traits + 68 impl files, A6/A7/A8/A9 identity traits, A10/A11 document traits down to one helper each, AssetLockProof asymmetric methods, and the _versioned DataContract method family. Replaced with canonical + (for DataContract) from_*_validated(value, &pv) opt-in validation.
  • All 5 Critical findings resolved (see table below).
  • Upstream PRs landed — dashcore fix: Starcounter-Jack JSON-Patch Prototype Pollution vulnerability #708 + feat(drive): log number of refunded epochs #729 merged; this branch dropped the local outpoint_serde wrapper. The blstrs_plus PR is still pending (one ValidatorSet value-round-trip test remains #[ignore]).

Critical findings status

# Finding Resolution
Critical-1 is_human_readable HR/non-HR divergence Documented on both canonical traits in serialization_traits.rs with the divergence table + ContentDeserializer caveat + pointer to canonical dual-shape visitor examples.
Critical-2 Silent array→bytes coercion in From<JsonValue> for Value Heuristic removed (rs-platform-value/src/converter/serde_json.rs). Faithful conversion: JSON array → Value::Array. Pin tests added.
Critical-3 ExtendedDocument non-round-trippable Replaced broken manual serde with #[serde(tag = "$extendedFormatVersion")] derive; round-trip tests added.
Critical-4 DataContract serde impurity Platform-version coupling pinned in 3 regression tests; validation flipped to opt-in (Deserialize no longer hardcodes full_validation=true); KEEP-AS-EXCEPTION rationale documented at all 3 sites.
Critical-5 to_canonical_object sorts keys (assumed signature-load-bearing) Falsified — signing uses bincode (PlatformSignable derive). Methods had zero production callers; deleted along with A1/A2.

DataContract API — final shape

The deprecation sweep collapsed the _versioned method family into a clear split by validation policy:

  • No validation (the new default): canonical serde_json::from_value::<DataContract>(json) / platform_value::from_value::<DataContract>(v) / serde_json::to_value(&dc) / platform_value::to_value(&dc). Use for storage reads, internal round-trips.
  • Opt-in validation: DataContract::from_json_validated(json, &pv) / from_value_validated(value, &pv). Use on trust boundaries (SDK ingest, fixture loaders). No bool param — name implies always-validates.
  • Kept: to_validating_json(&pv) — different concept (produces JSON-Schema-compatible output with binary as u8 arrays).
  • Deleted entirely: to_*_versioned, into_value_versioned, from_*_versioned(_, full_validation, _).

Test results

  • rs-dpp: 3619/3619 lib tests pass, 0 failed, 7 ignored. Of the 7 ignored: 6 are pre-existing recursive_schema_validator ignores; 1 is ValidatorSet::value_round_trip_with_full_wire_shape (pending blstrs_plus upstream PR).
  • rs-platform-value: 1036/1036 lib tests pass.
  • rs-drive: 3040/3040 lib tests pass.
  • rs-sdk: 117/117 lib tests pass.
  • rs-sdk-ffi: 252/252 lib tests pass.
  • Workspace cargo check --tests clean across dpp / drive / drive-abci / wasm-dpp / wasm-dpp2 / dash-sdk / rs-sdk-ffi.

Architectural conventions established

  • Tag-shape rules: all versioned outer enums use #[serde(tag = "$formatVersion")]; all discriminated enums use a $-prefixed key ($type, $action, $transition); zero adjacent-tagged enums remain.
  • #[json_safe_fields] rollout: 25+ V0/V1 struct leaves carry the attribute. JS-safe large-integer protection at every serialization site.
  • Wire-shape test convention: json!{} / platform_value!{} literal assertions in every round-trip test (~85 tests on this convention).
  • Wasm-side adapter pattern: impl_wasm_conversions_inner! (45 sites in wasm-dpp2) for rs-dpp domain types using canonical traits; impl_wasm_conversions_serde! (20 sites) for wasm-only DTOs without rs-dpp counterparts — pattern documented and re-audited.

Cross-package audit (just before shipping)

  • wasm-sdk: 0 manual Serialize/Deserialize, 0 references to deleted legacy APIs, 38 impl_wasm_serde_conversions! applications. All DTOs follow canonical patterns.
  • wasm-dpp2: 3 manual serde impls (IdentifierWasm, PoolingWasm, PlatformAddressWasm) — all documented production-required adapters for lenient JS-facing parsing; rest of crate flows through canonical helpers.
  • rs-sdk / rs-sdk-ffi / swift-sdk: no breakage; no references to deleted APIs.

Consensus and stored-data compatibility

Audited the full diff vs v4.1-dev (4 independent review passes: bincode byte-compatibility, platform-value serde rewrite, rs-drive/rs-drive-abci hunks, validation-behavior tracing). Verdict: no consensus impact; all stored blockchain data remains readable.

  • Binary formats are byte-identical. PlatformSerialize dispatches to derived bincode::Encode, independent of serde — the serde impl swaps in this PR cannot change wire, signable, or stored bytes. No platform_serialize / platform_signable / bincode attribute changes anywhere; document/v0/serialize.rs and the document serialization traits are untouched; every type that swapped serde impls kept its Encode/Decode derives intact. The one touched #[bincode(with_serde)] consensus file (asset_lock_proof/mod.rs) only changed conversion-method bodies.
  • Consensus validation is unchanged. Nodes ingest state transitions exclusively via bincode (decode_raw_state_transitionsdeserialize_from_bytes); the JSON/Value paths reshaped here are client-only. The AddressWitness MAX_P2SH_SIGNATURES cap is still enforced at bincode decode (witness.rs), and ContestedIndexFieldMatch serde is never used in contract ingestion (index parsing is manual from Value maps).
  • Stored data stays compatible. Contracts/documents/identities decode through unchanged bincode/custom-binary paths. The rewritten platform_value serde serializer cannot reach storage: Value has native bincode derives, and no consensus type routes a Value through serde-bincode (workspace-wide sweep).
  • rs-drive / rs-drive-abci diffs are test-only; the single production change outside rs-dpp is wasm-drive-verify's group-action JS output, which runs after proof verification succeeds and cannot alter the accept/reject decision.

Breaking changes (JS-facing wire shape)

Consumer-visible JSON shape changes — downstream JS consumers must update. None of these affect consensus (see section above):

  • TokenPricingSchedule is now internally $type-tagged with JS-safe integers (was externally tagged with raw u64):
    • { "SinglePrice": N }{ "$type": "singlePrice", "price": N }
    • { "SetPrices": { "5": 50 } }{ "$type": "setPrices", "prices": { "5": 50 } }
    • price / amounts serialize as strings (toJSON) / BigInt (toObject) above 2^53.
    • Reaches TokenSetPriceResult, TokenSetPriceForDirectPurchaseTransition, TokenEvent::ChangePriceForDirectPurchase, and the two StateTransitionProofResult pricing-schedule variants.
  • verifyActionInfosInContract{Vec,Map} (wasm-drive-verify) now returns the canonical group-action shape (rewritten to use DPP's canonical serde instead of a hand-rolled representation):
    • { "type": "Mint", ... }{ "$kind": "tokenEvent", "$type": "mint", ... }
    • amounts: string → BigInt; fields renamed notepublicNote, frozenIdentityfrozenIdentifier, burnFromburnFromIdentifier, personalEncryptedNoteprivateEncryptedNote; encrypted-note object → [number, number, Uint8Array].
  • AddressWitness JSON: tag key renamed type$type (variants p2pkh / p2sh, field redeemScript). The MAX_P2SH_SIGNATURES cap is no longer pre-checked on the JSON/Value deserialize path — bincode decode (the path nodes use) still enforces it, so oversized witnesses are still rejected on-chain; client tooling just loses the early error.
  • TokenDistributionRecipient JSON now only accepts the canonical { "$type": ... } shape and explicitly rejects the pre-4.0.0-beta.4 shapes (bare "ContractOwner", { "Identity": "<base58>" }). No system contract uses this type; only off-chain fixtures/tooling with the old shape are affected.

Out of scope (separate work)

  • blstrs_plus BLS PublicKey dual-shape deserialize — pending upstream. One ValidatorSet value-round-trip test remains #[ignore] until it lands.
  • Pass 5 (delete the legacy wasm-dpp crate) — blocked on team decision.

Docs

  • docs/json-value-unification-plan.md — the live plan + status doc (regularly updated through the work).
  • docs/json-value-conversion-inventory.md — pre-pass-1 structural inventory.
  • docs/json-value-conversion-canonical-pattern.md — the canonical-trait usage pattern, kept up to date.

Test plan

  • CI green (currently 3 success / 12 skipped / 0 failed).
  • Reviewer runs cargo test -p dpp --features all_features_without_client --lib and sees 3619 pass / 0 fail.
  • Reviewer skims docs/json-value-unification-plan.md to confirm the architectural decisions (validation-opt-in, KEEP-AS-EXCEPTION for DataContract, tag-shape conventions) match team intent.
  • Spot-check a wasm-dpp2 wrapper migration (e.g., IdentityCreditWithdrawalTransitionWasm) round-trips correctly from the JS SDK perspective.

🤖 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 shumkov changed the base branch from v4.0-dev to v4.1-dev July 2, 2026 08:12
shumkov and others added 9 commits July 6, 2026 09:42
The bls_pubkey serde helper unconditionally imports crate::bls_signatures,
which only exists under the `bls-signatures` feature. Wasm builds that compile
rs-dpp without it (e.g. wasm-drive-verify) failed with E0433/E0432. Gate the
module behind the feature its wrapped type (dashcore::blsful::PublicKey) and its
only consumers (core_types validator/validator-set) already require.

Reproduced with a no-default-features check of the wasm-drive-verify feature set:
could not find `bls_signatures` before, compiles after.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop working/planning docs that don't belong in the shipped PR: the unification
plan, inventory snapshot, superseded enum-tagging spec, the next-PR wasm-dpp2
cleanup plan, and a stray v12 upgrade-boundary doc. Keep the canonical-pattern
reference and remove its now-dangling links, and drop the same dangling
doc-pointer comments in the DataContract serde module and platform-value
converter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…map_value

The JSON/Value unification removed ExtendedDocument.toObject() (its rs-dpp
delegate to_json_object_for_validation was deleted), breaking every
platform-test-suite caller with "toObject is not a function" across the
Document, contacts and dpns specs (9 tests). Reinstate it over the canonical
non-human-readable to_map_value() so identifiers and binary data — including
nested document properties — serialize as Uint8Array in one pass, without
walking the document type's identifier/binary paths.

platform-test-suite: 9 failing before, all passing after.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t comparisons

DataContractWasm.toObject now serializes the contract's native format via
canonical platform_value::to_value (previously force-downgraded through
PlatformVersion::first()), so a V1 contract emits createdAt/updatedAt and their
block-height/epoch fields. Comparing an unpublished fixture (no metadata) against
a published-then-fetched contract (platform-stamped metadata) therefore diverged.
Strip the contract-level metadata before the deep-equals, mirroring the Document
specs' timestamp handling.

platform-test-suite: 2 failing before, passing after.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…de path

to_binary_bytes / into_binary_bytes matched only Value::U8 array elements, while
their to_bytes_32 / to_identifier siblings accept any integer variant via
to_integer(). A binary (ByteArray) document property that round-trips through a
schemaless JSON layer — e.g. editing and replacing a cached document — arrives as
a plain JSON int array that decodes to Value::U64 elements, so the replace failed
at serialize with "not an array of bytes". Relax both array arms to
byte.to_integer(), matching the siblings; this repairs the round-trip for every
cached row regardless of its integer encoding.

Regression test would have caught this: the array-of-U64 case errored before the
fix ("not an array of bytes"), passes after.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The confirmed-document JSON handed to Swift used the legacy
to_json_with_identifiers_using_bytes (base58 ids but binary as u8-arrays, no
$formatVersion, unset system fields omitted), diverging from the DOC-01 list
query path (dash_sdk_document_search: canonical to_object + serde_json — base64
binary, $formatVersion present, unset fields as null) that Swift actually reads.
Route the create and replace/transfer/set-price/purchase confirm paths through a
single canonical helper (to_object + serde_json), so the persisted body is
byte-identical to what a later query returns. Drop the now-unused
DocumentJsonMethodsV0 / PlatformVersion imports; update Rust + Swift docs.

Adds a pin test asserting the confirmed JSON has the canonical shape (base58 id,
base64 binary, $formatVersion) and equals the query mechanism's output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After the JSON/Value unification and the wallet-ffi canonicalization, three
hand-rolled conversion methods have no remaining production callers:
- Document::to_json_with_identifiers_using_bytes (+ the whole DocumentJsonMethodsV0 trait)
- DataContract::to_validating_json
- ExtendedDocument::to_pretty_json

Delete them with their impls, dispatch, re-exports, and now-dead tests. Kept:
DataContractJsonConversionMethodsV0::from_json (intentional final API), the
value-level try_to_validating_json / try_into_validating_json JSON-Schema
validation primitives, and to_map_value / into_map_value.

Verified: cargo check -p dpp --all-features, the no-default-features wasm feature
set, -p platform-wallet-ffi and -p wasm-dpp2 all compile; dpp lib tests 3784 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…red serializer

Code review (security audit) found the earlier binary round-trip fix relaxed
platform-value to_binary_bytes/into_binary_bytes to accept arrays of wider
integers — but those helpers are also on the drive-abci block-serialization
(app-hash) path via Document::serialize_v0, and the block path does not
sanitize. Relaxing them was an ungated consensus rule change: a crafted
Value::Array byteArray transition would be rejected by old nodes and stored by
new ones, diverging the app hash (rolling-upgrade fork risk).

Revert the shared serializer to strict Value::U8 (restoring consensus behavior),
and instead normalize int-array -> bytes in the client-only
DocumentType::sanitize_value_mut, which the block/consensus path never calls.
This fixes the Swift seed->edit->replace round-trip for binary properties
without changing which state transitions are accepted.

Regression test would have caught this: sanitize leaves Array([U64,..]) untouched
before the fix, converts it to Value::Bytes after.

Also strengthen the wallet-ffi confirmed-JSON pin test — assert the exact base64
of the binary property and that unset system fields are present-as-null (the
concrete differences from the legacy shape), dropping a tautological assertion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Verified against head 603b10c. One new latest-delta blocking issue survives verification in the Swift token wrappers; the unresolved carried-forward issues are still concentrated in wasm TypeScript discriminator declarations and canonical JSON/Value conversion edge cases. CodeRabbit supplied no inline findings, and prior findings 1 and 6 are resolved because the referenced docs were removed.

Reviewed commit: 603b10c6

Source: reviewers: opus general/security-auditor/ffi-engineer failed (extra-usage quota, reset Jul 10 8am America/Chicago); gpt-5.5 general/security-auditor/ffi-engineer completed. Verifier: opus failed same quota; gpt-5.5 completed. Specialists: security-auditor, ffi-engineer.

Result: 4 blocking, 5 suggestion(s).

Prior Reconciliation

  • prior-1 FIXED: docs/v12-upgrade-boundary-risks.md was removed at this head.
  • prior-6 FIXED: docs/json-value-unification-plan.md was removed at this head.
  • prior-2, prior-3, prior-4, prior-5, prior-7, prior-8, prior-9, and prior-10 are STILL VALID and carried forward below.

New Findings In Latest Delta

blocking: Keep the token signer alive for the whole FFI callback

packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/Tokens/TokenActions.swift (lines 108-127)

This latest-delta wrapper captures only signerHandle for the actual platform_wallet_token_transfer call and uses _ = signer before the FFI call starts. KeychainSigner registers its Rust callback context with Unmanaged.passUnretained(self), so Rust later dereferences a non-owning Swift object through the signer vtable. A bare _ = signer is the last use of the object in the detached task and can be optimized/released before the synchronous Rust signing callback completes; nearby wallet wrappers now use withExtendedLifetime(signer) for this exact reason. tokenBurn has the same pattern at lines 170-192. Wrap the full marshalling and FFI call in withExtendedLifetime(signer) so short-lived signer arguments cannot be deallocated while Rust is signing.

Carried-Forward Prior Findings

blocking: AssetLockProof declarations still advertise `type` instead of `$type`

packages/wasm-dpp2/src/asset_lock_proof/proof.rs (lines 23-37)

Carried forward from prior-2. The TypeScript custom section says AssetLockProofObject and AssetLockProofJSON are discriminated by type, but the rs-dpp enum derives and deserializes with #[serde(tag = "$type")]. Because this wrapper now delegates through the canonical inner conversion path, JS payloads that satisfy the generated TypeScript shape fail at the wasm/Rust boundary with a missing $type, and runtime toObject()/toJSON() output does not satisfy the published declarations.

blocking: FeeStrategyStep declarations use `type` while deserialization requires `$type`

packages/wasm-dpp2/src/platform_address/fee_strategy.rs (lines 13-30)

Carried forward from prior-3. FeeStrategyStepObject and FeeStrategyStepJSON document { type, index }, and the wrapper comment repeats that shape. The wrapped AddressFundsFeeStrategyStep serializer emits $type, and its custom deserializer only reads $type, returning missing_field("$type") for the documented object. Wallet code following these declarations can build address-funded transitions that type-check in TypeScript but are rejected by the canonical wasm/Rust conversion.

blocking: Several wasm-dpp2 declarations still use unprefixed discriminators

packages/wasm-dpp2/src/voting/vote_poll.rs (lines 38-50)

Carried forward from prior-4, with the same root cause still present across companion wrappers. VotePollObject/VotePollJSON declare type, but rs-dpp serializes VotePoll with $type; ResourceVoteChoice and ContestedDocumentVotePollWinnerInfo declarations also use type while their manual serializers require $type; AddressWitness declarations use type while the enum is #[serde(tag = "$type")]; and GroupActionEvent/TokenEvent declarations use kind/type while runtime output is $kind/$type. The PR states that discriminated enums use $-prefixed keys, and these public TS sections now contradict the canonical conversion paths they expose, causing TS-valid payloads to fail deserialization and returned objects to violate the generated types.

suggestion: TokenPricingSchedule JSON can still emit unsafe u64 numbers

packages/rs-dpp/src/tokens/token_pricing_schedule.rs (lines 28-51)

Carried forward from prior-7. This PR adds canonical JsonConvertible/ValueConvertible impls for TokenPricingSchedule, but the enum still derives plain serde for SinglePrice(Credits) and SetPrices(BTreeMap<TokenAmount, Credits>). Both aliases are u64, and the new tests pin JSON numbers such as { "SinglePrice": 1234 } and { "SetPrices": { "5": 50 } }. Values above JavaScript's safe integer range can be rounded by JS consumers, which conflicts with the PR's stated JS-safe large-integer coverage and the explicit escape-hatch comment claiming developers handle this type safely.

suggestion: FFI data-contract JSON no longer uses the SDK protocol version

packages/rs-sdk-ffi/src/data_contract/queries/fetch_with_serialization.rs (lines 110-131)

Carried forward from prior-8. The FFI fetch path returns JSON to C/Swift by calling serde_json::to_value(&contract). DataContract serde intentionally resolves PlatformVersion::get_version_or_current_or_latest(None), but this boundary used to serialize with wrapper.sdk.version(). The JSON shape returned by this SDK API can now depend on process-global/current/latest protocol state rather than the network version used for the proof-verified fetch. Keep the general DataContract serde exception, but route this FFI API through an explicit SDK-versioned serialization path.

suggestion: Reject oversized BLS public-key sequences before allocating them

packages/rs-dpp/src/serialization/dashcore/bls_pubkey.rs (lines 151-157)

Carried forward from prior-5. visit_seq accepts a byte sequence, pushes every element into a Vec, and only then calls from_compressed_g1_bytes, which rejects anything other than 48 bytes. A hostile JSON/Value payload can force allocation and parsing proportional to an arbitrarily large sequence before the fixed-size public key length check runs. Since valid compressed-G1 input is exactly 48 bytes, reject immediately when the 49th byte is seen.

suggestion: Legacy update-transition constructor does not alias `identityContractNonce`

packages/wasm-dpp/src/data_contract/state_transition/data_contract_update_transition/mod.rs (lines 62-76)

Carried forward from prior-9. The legacy wasm constructor preserves lenient JS construction by defaulting $formatVersion, userFeeIncrease, signaturePublicKeyId, and signature, then calls strict DataContractUpdateTransition::from_object(raw). The strict V0 struct now expects the nonce as $identity-contract-nonce, but this compatibility shim does not map the older JS-facing identityContractNonce key. Existing callers using the legacy field name can no longer construct an update transition through this lenient path.

suggestion: Canonical conversion tests still miss changed enum variants

packages/rs-dpp/src/data_contract/document_type/property/array.rs (lines 715-841)

Carried forward from prior-10. The new canonical wire-shape tests for ArrayItemType cover Integer, Number, String, ByteArray, and Identifier but omit Boolean and Date, even though both variants participate in the same $type representation. The related TokenDistributionInfo tests in token_distribution_key.rs only pin PreProgrammed and omit Perpetual. Because this PR's stated goal is comprehensive canonical JSON/Value round-trip coverage, add literal JSON and Value assertions for these variants so discriminator and field-shape regressions are caught.

CodeRabbit

No CodeRabbit walkthrough/summary comment was found. Only emit coderabbit_reactions for findings with a concrete inline comment id.

Inline posting was not possible because gh pr diff for this PR returns PullRequest.diff too_large (300-file GitHub diff limit), so this exact-SHA review is posted as a body-only review.

shumkov and others added 8 commits July 8, 2026 16:53
The enum-tagging sweep moved rs-dpp's data-carrying enums to
#[serde(tag = "$type")] (and $kind for GroupActionEvent), but several wasm-dpp2
TypeScript custom-section declarations still advertised bare type/kind. JS
payloads satisfying the published .d.ts would fail at the wasm/Rust boundary
(missing $type), and toObject()/toJSON() output did not match the declarations.

Update the declarations (and the wrapper doc comments that repeat the shape) to
$type/$kind across AssetLockProof, AddressFundsFeeStrategyStep, VotePoll,
ResourceVoteChoice, ContestedDocumentVotePollWinnerInfo, AddressWitness,
TokenEvent, and GroupActionEvent, so the published types match the canonical
serde wire shape. Also fix two stale rs-dpp comments (VotePoll, TokenEvent) that
still described the superseded plain-type convention.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TokenPricingSchedule derived plain externally-tagged serde over SinglePrice(Credits)
and SetPrices(BTreeMap<TokenAmount, Credits>) — all u64 — so large values serialized
as raw JSON numbers (JS precision loss above 2^53), while its escape-hatch
impl JsonSafeFields {} marker asserted safety without enforcing it. The type leaks by
value into TokenSetPriceForDirectPurchaseTransitionV0, TokenEvent::ChangePriceForDirectPurchase,
and two StateTransitionProofResult variants, so the gap reached the state-transition wire.

Convert to an internally-$type-tagged Repr (#[serde(into/from)]) routing Credits through
json_safe_u64 and the map through json_safe_u64_u64_map (mirroring RewardDistributionMoment
and DistributionFunction::Stepwise). Fixing the type's serde propagates to all five sites.
Wire shape: {"SinglePrice":N} -> {"$type":"singlePrice","price":N}, large values as
strings. The bincode/consensus binary path is untouched.

New regression test pins a >2^53 value as a string; full dpp lib suite green (3786).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
visit_seq collected an arbitrarily long byte sequence into a Vec before the
fixed 48-byte compressed-G1 length check ran, letting a hostile JSON/Value
payload force allocation/parsing proportional to its size. Reject as soon as the
sequence exceeds COMPRESSED_G1_LEN. No functional change (over-length input
already errored, just after allocating); this bounds the allocation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…erpetual

The canonical JSON round-trip suites omitted these variants though they share the
$type wire representation. Add wire-shape assertions for ArrayItemType::Boolean
and ::Date, and a $type discriminator + round-trip pin for
TokenDistributionInfo::Perpetual, so discriminator/field-shape regressions are caught.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ersion

The JSON path used serde_json::to_value(&contract), whose DataContract serde
resolves the process-global current/latest platform version, so the returned JSON
shape could depend on process state rather than the network version used for the
proof-verified fetch. Serialize through try_into_platform_versioned(wrapper.sdk.version())
instead, matching the fetch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
verify_active_action_infos hand-rolled a ~440-line JS representation of
GroupActionEvent/TokenEvent (bare `type`, PascalCase discriminators, string
amounts) that diverged from the canonical DPP wire shape and duplicated the serde
logic. Replace it with the wasm-dpp2 to_object pattern: platform_value::to_value(&event)
fed to a serde_wasm_bindgen serializer configured for Uint8Array bytes and BigInt
u64, plus the same typed-map-key stringification (so TokenPricingSchedule::SetPrices'
u64 keys don't fail as JS object keys).

Output is now the canonical internally-tagged shape
({"$kind":"tokenEvent","$type":"mint",...}) with JS-safe BigInt amounts,
matching wasm-dpp2 — and ~312 fewer lines. Adds pure-Rust tests pinning the
$kind/$type shape and the map-key normalization.

Also fixes a stale doc comment on GroupActionEvent that described the superseded
plain-`type`/`kind` convention and a nonexistent `data` wrapper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two leftovers from the enum-tagging sweep found in code review: a wasm-dpp2 TS
doc comment claiming the rs-dpp enum uses `#[serde(tag = "type")]` (it uses
`$type`), and the TokenEvent Deserialize visitor's expecting() string naming a
`type` discriminator (the code reads `$type`). Comment/message only, no behavior
change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cargo fmt --all over the branch's own changed files (line-wrap collapses and
alphabetical module reordering) so cargo fmt --check --all is green. Confined to
files this PR already touches; no base v4.1-dev files reformatted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The latest push fixed the prior wasm-dpp2 discriminator declarations, TokenPricingSchedule JS-safe JSON shape, oversized BLS sequence handling, and the version-pinned fetch_with_serialization FFI path. Four in-scope suggestions remain: one new cumulative sibling FFI JSON regression, one carried-forward wasm-dpp compatibility regression, and two partial gaps in the PR's promised full JSON/Value wire-shape coverage. The carried-forward Swift signer lifetime issue is real but OUTDATED as a PR #3573 finding because TokenActions.swift is not changed by the cumulative PR diff.

Source: reviewers: opus general/security-auditor/ffi-engineer failed (extra-usage quota, reset Jul 10 8am America/Chicago); gpt-5.5 general/security-auditor/ffi-engineer completed. Verifier: opus failed same quota; gpt-5.5 completed. Specialists: security-auditor, ffi-engineer.

Prior reconciliation: prior-603-1 OUTDATED/out-of-scope for this PR head; prior-603-2, prior-603-3, prior-603-4, prior-603-5, prior-603-6, and prior-603-7 FIXED; prior-603-8 STILL VALID; prior-603-9 PARTIALLY STILL VALID.

🟡 4 suggestion(s)

Note: review_poster.py was invoked first, but GitHub refused the PR diff with PullRequest.diff too_large (>300 files), so these same verifier-approved findings are posted as a body-only review.

Verified active findings

suggestion: Serialize simple data-contract JSON at the SDK protocol version

packages/rs-sdk-ffi/src/data_contract/queries/fetch_json.rs (line 52-54)

This PR changed dash_sdk_data_contract_fetch_json from the explicit contract.to_json(wrapper.sdk.version()) path to plain serde_json::to_value(&contract). The manual DataContract serializer still calls PlatformVersion::get_version_or_current_or_latest(None), so this FFI endpoint now depends on the process-global/current version rather than the SDK version used for the proof-verified fetch. The latest delta fixed the same issue in dash_sdk_data_contract_fetch_with_serialization; route this simple JSON endpoint through DataContractInSerializationFormat with wrapper.sdk.version() as well so C/Swift callers do not see protocol-version-dependent JSON drift across upgrades.

suggestion: Preserve the legacy identityContractNonce constructor key

packages/wasm-dpp/src/data_contract/state_transition/data_contract_update_transition/mod.rs (line 62-76)

STILL VALID for prior-603-8. The PR replaced the legacy StateTransitionValueConvert::from_object(..., PlatformVersion::first()) path with strict ValueConvertible::from_object(raw). The old value-conversion implementation explicitly read identityContractNonce, while the new strict V0 serde shape requires $identity-contract-nonce. This constructor still preserves legacy leniency for $formatVersion, userFeeIncrease, signaturePublicKeyId, and signature, but it never aliases identityContractNonce before deserialization, so existing JS callers using the previous public nonce key fail at construction. Map identityContractNonce to $identity-contract-nonce when the canonical key is absent.

suggestion: Add ValueConvertible coverage for Boolean and Date array items

packages/rs-dpp/src/data_contract/document_type/property/array.rs (line 805-865)

STILL VALID in part for prior-603-9. The latest delta added JSON wire-shape assertions for ArrayItemType::Boolean and ArrayItemType::Date, but the ValueConvertible block still stops at Identifier. Because this PR explicitly promises comprehensive JSON/Value round-trip tests with literal wire-shape assertions, the non-human-readable Value shape for these two variants remains unpinned. Add matching value_round_trip_boolean_variant and value_round_trip_date_variant assertions.

suggestion: Pin the full Perpetual token-distribution Value shape

packages/rs-dpp/src/data_contract/associated_token/token_distribution_key.rs (line 391-407)

STILL VALID in part for prior-603-9. The new TokenDistributionInfo::Perpetual test only asserts the JSON $type discriminator and round-trips the value. It does not assert the full JSON wire shape and has no corresponding ValueConvertible assertion, so a regression in the nested moment or resolved-recipient Value representation would still pass. Add literal JSON and Value shape assertions for the Perpetual variant, matching the existing full-shape PreProgrammed tests.

Prior-finding reconciliation

  • AssetLockProof declarations still advertise type instead of $type — FIXED: AssetLockProofObject and AssetLockProofJSON now use $type for both variants, and the surrounding comment points to the rs-dpp $type serde tag.
  • FeeStrategyStep declarations use type while deserialization requires $type — FIXED: the public FeeStrategyStepObject and FeeStrategyStepJSON TypeScript declarations now use $type for deductFromInput and reduceOutput.
  • Several wasm-dpp2 declarations still use unprefixed discriminators — FIXED: the wasm-dpp2 voting, address witness, group action, and token event TypeScript surfaces now advertise $type or $kind consistently with runtime serialization.
  • TokenPricingSchedule JSON can still emit unsafe u64 numbers — FIXED: TokenPricingSchedule now serializes through an internally $type-tagged representation that applies json_safe_u64 and json_safe_u64_u64_map, with a regression test for values above Number.MAX_SAFE_INTEGER.
  • FFI data-contract JSON no longer uses the SDK protocol version — FIXED for the originally reported fetch_with_serialization API: it now converts to DataContractInSerializationFormat with wrapper.sdk.version() before JSON serialization. A separate sibling issue remains in fetch_json.rs and is reported above.
  • Reject oversized BLS public-key sequences before allocating them — FIXED: BlsPublicKeyVisitor::visit_seq now returns invalid_length as soon as the 49th byte is encountered, before growing beyond COMPRESSED_G1_LEN.
  • Keep the token signer alive for the whole FFI callback — OUTDATED as an in-scope PR #3573 finding at 0d69463: the issue is real in current source, but TokenActions.swift is not changed by the current cumulative PR diff from origin/v4.1-dev and was not changed in 603b10c..0d69463. Kept as out_of_scope follow-up context instead of a public carried-forward blocker.
  • Token action wrappers should pin KeychainSigner lifetime — OUTDATED as a PR #3573 finding: the issue is real in current source, but packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/Tokens/TokenActions.swift is not changed in the cumulative PR diff from the v4.1-dev merge base. The wrappers capture signerHandle and use _ = signer inside detached tasks even though KeychainSigner registers an unretained Swift context; nearby wallet code now uses withExtendedLifetime(signer) for this pattern.
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

- [SUGGESTION] In `packages/rs-sdk-ffi/src/data_contract/queries/fetch_json.rs`:52-54: Serialize simple data-contract JSON at the SDK protocol version
  This PR changed `dash_sdk_data_contract_fetch_json` from the explicit `contract.to_json(wrapper.sdk.version())` path to plain `serde_json::to_value(&contract)`. The manual `DataContract` serializer still calls `PlatformVersion::get_version_or_current_or_latest(None)`, so this FFI endpoint now depends on the process-global/current version rather than the SDK version used for the proof-verified fetch. The latest delta fixed the same issue in `dash_sdk_data_contract_fetch_with_serialization`; route this simple JSON endpoint through `DataContractInSerializationFormat` with `wrapper.sdk.version()` as well so C/Swift callers do not see protocol-version-dependent JSON drift across upgrades.
- [SUGGESTION] In `packages/wasm-dpp/src/data_contract/state_transition/data_contract_update_transition/mod.rs`:62-76: Preserve the legacy identityContractNonce constructor key
  STILL VALID for prior-603-8. The PR replaced the legacy `StateTransitionValueConvert::from_object(..., PlatformVersion::first())` path with strict `ValueConvertible::from_object(raw)`. The old value-conversion implementation explicitly read `identityContractNonce`, while the new strict V0 serde shape requires `$identity-contract-nonce`. This constructor still preserves legacy leniency for `$formatVersion`, `userFeeIncrease`, `signaturePublicKeyId`, and `signature`, but it never aliases `identityContractNonce` before deserialization, so existing JS callers using the previous public nonce key fail at construction. Map `identityContractNonce` to `$identity-contract-nonce` when the canonical key is absent.
- [SUGGESTION] In `packages/rs-dpp/src/data_contract/document_type/property/array.rs`:805-865: Add ValueConvertible coverage for Boolean and Date array items
  STILL VALID in part for prior-603-9. The latest delta added JSON wire-shape assertions for `ArrayItemType::Boolean` and `ArrayItemType::Date`, but the `ValueConvertible` block still stops at `Identifier`. Because this PR explicitly promises comprehensive JSON/Value round-trip tests with literal wire-shape assertions, the non-human-readable Value shape for these two variants remains unpinned. Add matching `value_round_trip_boolean_variant` and `value_round_trip_date_variant` assertions.
- [SUGGESTION] In `packages/rs-dpp/src/data_contract/associated_token/token_distribution_key.rs`:391-407: Pin the full Perpetual token-distribution Value shape
  STILL VALID in part for prior-603-9. The new `TokenDistributionInfo::Perpetual` test only asserts the JSON `$type` discriminator and round-trips the value. It does not assert the full JSON wire shape and has no corresponding `ValueConvertible` assertion, so a regression in the nested `moment` or resolved-recipient Value representation would still pass. Add literal JSON and Value shape assertions for the Perpetual variant, matching the existing full-shape PreProgrammed tests.

TokenPricingSchedule is now internally $type-tagged, so fromJSON/fromObject reject
the old externally-tagged { SinglePrice: N } with "missing field $type". Update the
three TokenSetPriceResult pricingSchedule tests to { $type: 'singlePrice', price: N }
and assert the new field names. This is the wasm-sdk consumer of the intentional
TokenPricingSchedule wire-shape change (22a2cf9).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Checked out and verified head 1703be8. The latest delta from 0d69463 only updates TokenSetPriceResult pricingSchedule test fixtures, but cumulative PR scope still leaves these verified issues: prior-0d-1 STILL VALID, prior-0d-2 STILL VALID, prior-0d-3 STILL VALID, and prior-0d-4 STILL VALID. I also verified one cumulative PR regression in register_contract where the comment and intended non-validating load path no longer match the actual serde behavior.

Source: claude-general: opus failed (extra-usage quota: resets Jul 10, 8am America/Chicago); claude-security-auditor: opus failed (extra-usage quota: resets Jul 10, 8am America/Chicago); claude-ffi-engineer: opus failed (extra-usage quota: resets Jul 10, 8am America/Chicago); codex-general: gpt-5.5 completed; codex-security-auditor: gpt-5.5 completed; codex-ffi-engineer: gpt-5.5 completed; verifier: gpt-5.5 completed.

Reviewed commit: 1703be8

🟡 5 suggestion(s)

Verified findings

suggestion: Use the explicit non-validating contract loader

packages/rs-scripts/src/bin/register_contract.rs (line 121)

This script still documents that fixture contracts are loaded without full validation because it overwrites ownerId/id before submitting the normal state transition, but the code now calls serde_json::from_value::. DataContract's Deserialize impl routes through try_from_platform_versioned with full_validation = true, so structurally decodable fixtures can be rejected before the script gets to replace the owner/id. Restore DataContract::from_json(json_value, false, platform_version), or deserialize DataContractInSerializationFormat and call the versioned conversion with full_validation = false.

suggestion: Serialize fetched contract JSON with the SDK protocol version

packages/rs-sdk-ffi/src/data_contract/queries/fetch_json.rs (line 52)

prior-0d-1 STILL VALID. This FFI endpoint fetches the contract through wrapper.sdk but serializes it with serde_json::to_value(&contract). DataContract's Serialize impl uses PlatformVersion::get_version_or_current_or_latest(None), so the returned JSON can follow the process-global/current version rather than the SDK network version used for the verified fetch. The sibling fetch_with_serialization path already pins wrapper.sdk.version(); this simple JSON endpoint should use the same DataContractInSerializationFormat conversion before serializing.

suggestion: Preserve the legacy identityContractNonce constructor key

packages/wasm-dpp/src/data_contract/state_transition/data_contract_update_transition/mod.rs (line 62)

prior-0d-2 STILL VALID. The wasm constructor still fills several omitted legacy fields before delegating to strict DataContractUpdateTransition::from_object(raw), but it does not translate the old JS-facing identityContractNonce key to the current $identity-contract-nonce serde key. Existing callers using the previous public constructor shape now fail construction before a Rust transition is produced. Add an alias/migration in this shim before calling from_object.

suggestion: Add ValueConvertible coverage for Boolean and Date array items

packages/rs-dpp/src/data_contract/document_type/property/array.rs (line 805)

prior-0d-3 STILL VALID. The file now pins JSON wire shapes for ArrayItemType::Boolean and ArrayItemType::Date, but the ValueConvertible tests still stop at Identifier. Because this PR is standardizing canonical JSON/Value wire-shape behavior, add Value assertions for Boolean and Date so the non-human-readable Value representation is locked down as well.

suggestion: Pin the full Perpetual token-distribution Value shape

packages/rs-dpp/src/data_contract/associated_token/token_distribution_key.rs (line 391)

prior-0d-4 STILL VALID. The Perpetual test checks only the JSON $type discriminator and then round-trips the value. Unlike the PreProgrammed tests, it does not assert the full JSON shape and has no ValueConvertible assertion for the nested moment and resolved-recipient representation. Add full-shape JSON and Value assertions so regressions in those nested wire forms cannot pass unnoticed.

_Note: Inline posting failed (command failed (1): python3 scripts/review_poster.py dashpay/platform 3573 1703be8 --dry-run
STDOUT:

STDERR:
Traceback (most recent call last):
File "/Users/claw/.openclaw/workspace/scripts/review_poster.py", line 904, in
result = post_review(
File ), so I posted the same verified findings as a top-level review body._

@shumkov shumkov left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed the review threads — per-comment status inline. bls_pubkey moved to serialization/dashcore; all data-carrying enums internally $type-tagged ($type everywhere, GroupActionEvent → $kind); validator_set BLS deserialize fixed via deserialize_any.

Comment thread packages/rs-dpp/src/serialization/dashcore/bls_pubkey.rs
Comment thread packages/rs-dpp/src/core_types/validator_set/mod.rs Outdated
@shumkov shumkov changed the title feat(dpp): unify JSON/Value conversion traits + comprehensive round-trip tests feat(dpp): unify JSON/Value conversion traits Jul 10, 2026
@shumkov shumkov merged commit f7d7c8d into v4.1-dev Jul 10, 2026
40 checks passed
@shumkov shumkov deleted the feat/json-convertible-address-transitions branch July 10, 2026 11:07
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Platform team Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants