Skip to content

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

Open
shumkov wants to merge 205 commits into
v4.1-devfrom
feat/json-convertible-address-transitions
Open

feat(dpp): unify JSON/Value conversion traits + comprehensive round-trip tests#3573
shumkov wants to merge 205 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.

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>

@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 at head a592d79. Prior finding reconciliation: #1 (runbook leak in docs/v12-upgrade-boundary-risks.md) STILL VALID — file at lines 119-135 still contains the self-labelled 'do not commit' section with live HPMN validator IP, ansible user, and operator SSH key path; BLOCKING. #2 (wasm-dpp2 TS type vs runtime $type/$kind) STILL VALID — proof.rs:18-38 still declares type discriminator and the doc-comment still says #[serde(tag = "type")], while rs-dpp asset_lock_proof/mod.rs:38,49 uses $type; BLOCKING. #3 (TokenPricingSchedule raw u64) STILL VALID — no json_safe_fields, tests pin raw numeric shape; SUGGESTION. #4 (TokenDistributionInfo unpinned variants) STILL VALID — only PreProgrammed pinned; SUGGESTION. #5 (ArrayItemType Boolean/Date unpinned) STILL VALID — only 5 of 7 variants pinned; SUGGESTION. New delta finding: the rewritten lenient constructor at wasm-dpp/.../data_contract_update_transition/mod.rs:52-79 defaults $formatVersion/userFeeIncrease/signaturePublicKeyId/signature but does not default or alias $identity-contract-nonce, which is a required serde field; two still-checked-in JS specs construct the transition without it.

🔴 2 blocking | 🟡 4 suggestion(s)

Note: review_poster dry-run could not map inline comments (pay/platform failed: could not find pull request diff: HTTP 406: Sorry, the diff exceeded the maximum number of files (300). Consider using 'List pull requests files' API or locally cloning the repository instead. (https://api.github.com/repos/dashpay/platform/pulls/3573)
PullRequest.diff too_large
), so I posted the same verified findings as a body-only review.

Verified active findings

blocking: Committed runbook self-labelled 'do not commit' discloses live HPMN validator IP, SSH user, and operator SSH key path

docs/v12-upgrade-boundary-risks.md (line 119-135)

STILL VALID at head a592d79. The section heading at line 119 reads ## Runbook (local only — contains infra details, do not commit), but the body publishes operator-sensitive infrastructure metadata the author themselves marked as private:

  • hp-masternode-8 = 68.67.122.8 is identified as a live HPMN testnet validator with ansible_user ubuntu and do NOT stop the drive container annotation.
  • 54.185.219.212 is identified as a disposable mainnet-synced node.
  • SSH access template is published verbatim: ssh -i ~/.ssh/evo-app-deploy.rsa ubuntu@<host> — exposing both the SSH username and the operator's private-key filename (evo-app-deploy.rsa).
  • Volume layout (docker volume ls | grep -i drive, tar strategy) gives an attacker a reliable map of the live drive container.

This content is entirely unrelated to the JSON/Value unification work this PR is about and the author's own heading explicitly classifies it as not-for-commit. Pairing operator workflow + key-file paths with named live infra is materially more useful for credential-phishing or social-engineering than each fact in isolation. Remove the runbook section before merge; keep operator runbooks in a private store. Consider rotating evo-app-deploy.rsa out of caution.

blocking: wasm-dpp2 TypeScript declarations advertise `type`/`kind` while rs-dpp runtime emits `$type`/`$kind`

packages/wasm-dpp2/src/asset_lock_proof/proof.rs (line 18-38)

STILL VALID at head a592d79. The #[wasm_bindgen(typescript_custom_section)] at proof.rs:23-37 declares AssetLockProofObject/AssetLockProofJSON as { type: "instant" | "chain" } & ... and the doc-comment at line 25 still says #[serde(tag = "type")]. The actual rs-dpp enum at packages/rs-dpp/src/identity/state_transition/asset_lock_proof/mod.rs:38,49 uses #[serde(tag = "$type", rename_all = "camelCase")], and the wasm-dpp2 JS unit specs updated earlier in this PR assert $type on the runtime objects.

The same mismatch persists across this PR's TS sections: packages/wasm-dpp2/src/voting/winner_info.rs:18-28, packages/wasm-dpp2/src/voting/vote_poll.rs:39,50, packages/wasm-dpp2/src/voting/resource_vote_choice.rs:19-29, packages/wasm-dpp2/src/shielded/address_witness.rs:17,25,39,47, packages/wasm-dpp2/src/platform_address/fee_strategy.rs:18-30, and packages/wasm-dpp2/src/group/token_event.rs — all corresponding to rs-dpp enums migrated to serde(tag = "$type") / serde(tag = "$kind") by 7f63b89.

Net effect for wasm-dpp2 consumers compiling against the published .d.ts:

  1. JSON.parse(json).type is undefined; the discriminator is at $type.
  2. Every TS discriminated-union narrowing case "instant": resolves to never.
  3. JS code constructing { type: "instant", ... } per the TS declaration is rejected by from_object/fromJSON strict deserialization.

Sync each typescript_custom_section literal and the Mirrors the rs-dpp serde shape doc-comment to the runtime tag ($type, and $kind for GroupAction). Also update CONVENTIONS.md / any Mirrors the rs-dpp serde shape (... "type" ...) text in matching JS doc-comments.

#[wasm_bindgen(typescript_custom_section)]
const TS_TYPES: &str = r#"
/**
 * AssetLockProof serialized as a plain object.
 *
 * Internally-tagged discriminated union — `$type` discriminates the variant and
 * the variant's fields sit alongside it. Mirrors the rs-dpp serde shape (which
 * uses `#[serde(tag = "$type")]` on the enum) and the convention used by
 * `AddressWitness` / `AddressFundsFeeStrategyStep`.
 */
export type AssetLockProofObject =
    | ({ $type: "instant" } & InstantAssetLockProofObject)
    | ({ $type: "chain" } & ChainAssetLockProofObject);

/**
 * AssetLockProof serialized as JSON.
 */
export type AssetLockProofJSON =
    | ({ $type: "instant" } & InstantAssetLockProofJSON)
    | ({ $type: "chain" } & ChainAssetLockProofJSON);
"#;
suggestion: Lenient constructor does not default or alias `$identity-contract-nonce`, breaking existing JS callers

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

Introduced by the latest delta. The rewritten constructor calls strict DataContractUpdateTransition::from_object(raw) after defaulting $formatVersion, userFeeIncrease, signaturePublicKeyId, and signature for legacy callers, but does not handle the identity_contract_nonce field — required at packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v0/mod.rs:33-38 with the serde rename "$identity-contract-nonce".

Two existing JS specs construct new DataContractUpdateTransition({...}) without that field at packages/wasm-dpp/test/integration/dataContract/stateTransition/DataContractUpdateTransition/validation/basic/validateDataContractUpdateTransitionBasicFactory.spec.js:47-52 and packages/wasm-dpp/test/unit/dataContract/stateTransition/DataContractUpdateTransition/validation/state/validateDataContractUpdateTransitionStateFactory.spec.js:40-43. Strict from_object will reject these with a missing-field error. The one spec updated in this delta (DataContractUpdateTransition.spec.js:24) had to add the canonical key explicitly, confirming the breakage.

Either default the nonce to 0 alongside the other ensure_* calls if the legacy constructor was previously permissive about its absence, or update/skip the two remaining specs to supply the canonical key. If a legacy camelCase alias (identityContractNonce) was ever supported by the pre-canonical lenient path, translate it to the canonical name as part of the ensure_* block.

suggestion: TokenPricingSchedule canonical JSON emits raw u64 — JS clients silently round Credits above 2^53−1

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

STILL VALID at head a592d79. TokenPricingSchedule (lines 28-45) derives plain Serialize/Deserialize with no #[json_safe_fields], and the canonical impls (lines 48, 51) are blanket impls with no Repr wrapper. The variants SinglePrice(Credits) and SetPrices(BTreeMap<TokenAmount, Credits>) both expose raw u64 on the JSON wire surface; the tests at lines 100-123 pin the raw numeric shape {"SinglePrice": 1234} / {"SetPrices": {"5": 50, "10": 80}}.

This PR establishes a stated #[json_safe_fields] convention applied to 25+ V0/V1 struct leaves to protect JS clients from silently rounding u64 values above 2^53−1. Token prices use the 100-billion-per-Dash credit scale (per CLAUDE.md), so a tier price above ~92,233 DASH (~9.2 × 10^15 duffs) drops precision when round-tripped through JsonConvertible::to_json and consumed by JSON.parse on the JS side. Because TokenPricingSchedule is referenced from TokenConfiguration distribution rules, this can manifest as off-by-rounding fees with no JS-side error.

Apply #[json_safe_fields] (or a Repr wrapper with json_safe_u64 on the inner Credits/TokenAmount fields) so JS receives the decimal-string form, matching the convention applied throughout this PR.

suggestion: TokenDistributionInfo round-trip tests only pin PreProgrammed; Perpetual and TokenDistributionTypeWithResolvedRecipient unpinned

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

STILL VALID at head a592d79. mod json_convertible_tests_token_distribution_info (lines 322-389) defines a single fixture TokenDistributionInfo::PreProgrammed(1_700_000_000_000, Identifier::new([0x42; 32])) and pins both JSON and Value wire shapes only for that variant. The Perpetual { moment, recipient } variant — where moment is RewardDistributionMoment (an internally-tagged enum migrated to $type in 51d3193) and recipient is TokenDistributionRecipient — is not pinned, and TokenDistributionTypeWithResolvedRecipient (the resolved-recipient wrapper with its own $type-tagged repr) is also missing canonical wire-shape pins.

The broader internally-tag everything with $type sweep is the consensus-critical change being landed here; the unpinned Perpetual variant is exactly where a future regression (missing tag = "$type" or a silent variant-flip on a nested enum) would land without a fixture there to catch it. Mirror the PreProgrammed test for at least one Perpetual case (e.g. BlockBasedTimeInterval) and one TokenDistributionTypeWithResolvedRecipient case.

suggestion: ArrayItemType canonical round-trip tests miss Boolean and Date variants

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

STILL VALID at head a592d79. mod json_convertible_tests (lines 712-842) pins JSON + Value wire shapes for only five of ArrayItemType's seven variants: Integer (716, 782), Number (729, 793), String (740, 804), ByteArray (757, 819), and Identifier (771, 833). The two unit variants Boolean and Date have no canonical wire-shape pin in either path.

This was the last enum landed in the internally tag everything with $type sweep (commit c341be6) and is the consensus-critical schema-definition surface for document contracts; a future serde-rename slip on Boolean ({"$type":"boolean"} vs {"$type":"bool"}) would silently change contract document schemas with no test failure.

Add two more tests symmetric to the existing pattern, one each for ArrayItemType::Boolean and ArrayItemType::Date, asserting the JSON and Value layers.

🤖 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.

- [BLOCKING] In `docs/v12-upgrade-boundary-risks.md`:119-135: Committed runbook self-labelled 'do not commit' discloses live HPMN validator IP, SSH user, and operator SSH key path
  STILL VALID at head a592d79bec. The section heading at line 119 reads `## Runbook (local only — contains infra details, do not commit)`, but the body publishes operator-sensitive infrastructure metadata the author themselves marked as private:

- `hp-masternode-8` = `68.67.122.8` is identified as a live HPMN testnet validator with ansible_user `ubuntu` and `do NOT stop the drive container` annotation.
- `54.185.219.212` is identified as a disposable mainnet-synced node.
- SSH access template is published verbatim: `ssh -i ~/.ssh/evo-app-deploy.rsa ubuntu@<host>` — exposing both the SSH username and the operator's private-key filename (`evo-app-deploy.rsa`).
- Volume layout (`docker volume ls | grep -i drive`, tar strategy) gives an attacker a reliable map of the live drive container.

This content is entirely unrelated to the JSON/Value unification work this PR is about and the author's own heading explicitly classifies it as not-for-commit. Pairing operator workflow + key-file paths with named live infra is materially more useful for credential-phishing or social-engineering than each fact in isolation. Remove the runbook section before merge; keep operator runbooks in a private store. Consider rotating `evo-app-deploy.rsa` out of caution.
- [BLOCKING] In `packages/wasm-dpp2/src/asset_lock_proof/proof.rs`:18-38: wasm-dpp2 TypeScript declarations advertise `type`/`kind` while rs-dpp runtime emits `$type`/`$kind`
  STILL VALID at head a592d79bec. The `#[wasm_bindgen(typescript_custom_section)]` at proof.rs:23-37 declares `AssetLockProofObject`/`AssetLockProofJSON` as `{ type: "instant" | "chain" } & ...` and the doc-comment at line 25 still says `#[serde(tag = "type")]`. The actual rs-dpp enum at packages/rs-dpp/src/identity/state_transition/asset_lock_proof/mod.rs:38,49 uses `#[serde(tag = "$type", rename_all = "camelCase")]`, and the wasm-dpp2 JS unit specs updated earlier in this PR assert `$type` on the runtime objects.

The same mismatch persists across this PR's TS sections: `packages/wasm-dpp2/src/voting/winner_info.rs:18-28`, `packages/wasm-dpp2/src/voting/vote_poll.rs:39,50`, `packages/wasm-dpp2/src/voting/resource_vote_choice.rs:19-29`, `packages/wasm-dpp2/src/shielded/address_witness.rs:17,25,39,47`, `packages/wasm-dpp2/src/platform_address/fee_strategy.rs:18-30`, and `packages/wasm-dpp2/src/group/token_event.rs` — all corresponding to rs-dpp enums migrated to `serde(tag = "$type")` / `serde(tag = "$kind")` by 7f63b89.

Net effect for `wasm-dpp2` consumers compiling against the published `.d.ts`:
1. `JSON.parse(json).type` is `undefined`; the discriminator is at `$type`.
2. Every TS discriminated-union narrowing `case "instant":` resolves to `never`.
3. JS code constructing `{ type: "instant", ... }` per the TS declaration is rejected by `from_object`/`fromJSON` strict deserialization.

Sync each `typescript_custom_section` literal and the `Mirrors the rs-dpp serde shape` doc-comment to the runtime tag (`$type`, and `$kind` for GroupAction). Also update CONVENTIONS.md / any `Mirrors the rs-dpp serde shape (... "type" ...)` text in matching JS doc-comments.
- [SUGGESTION] In `packages/wasm-dpp/src/data_contract/state_transition/data_contract_update_transition/mod.rs`:52-79: Lenient constructor does not default or alias `$identity-contract-nonce`, breaking existing JS callers
  Introduced by the latest delta. The rewritten constructor calls strict `DataContractUpdateTransition::from_object(raw)` after defaulting `$formatVersion`, `userFeeIncrease`, `signaturePublicKeyId`, and `signature` for legacy callers, but does not handle the `identity_contract_nonce` field — required at `packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v0/mod.rs:33-38` with the serde rename `"$identity-contract-nonce"`.

Two existing JS specs construct `new DataContractUpdateTransition({...})` without that field at `packages/wasm-dpp/test/integration/dataContract/stateTransition/DataContractUpdateTransition/validation/basic/validateDataContractUpdateTransitionBasicFactory.spec.js:47-52` and `packages/wasm-dpp/test/unit/dataContract/stateTransition/DataContractUpdateTransition/validation/state/validateDataContractUpdateTransitionStateFactory.spec.js:40-43`. Strict `from_object` will reject these with a missing-field error. The one spec updated in this delta (`DataContractUpdateTransition.spec.js:24`) had to add the canonical key explicitly, confirming the breakage.

Either default the nonce to `0` alongside the other ensure_* calls if the legacy constructor was previously permissive about its absence, or update/skip the two remaining specs to supply the canonical key. If a legacy camelCase alias (`identityContractNonce`) was ever supported by the pre-canonical lenient path, translate it to the canonical name as part of the ensure_* block.
- [SUGGESTION] In `packages/rs-dpp/src/tokens/token_pricing_schedule.rs`:28-51: TokenPricingSchedule canonical JSON emits raw u64 — JS clients silently round Credits above 2^53−1
  STILL VALID at head a592d79bec. `TokenPricingSchedule` (lines 28-45) derives plain `Serialize`/`Deserialize` with no `#[json_safe_fields]`, and the canonical impls (lines 48, 51) are blanket impls with no Repr wrapper. The variants `SinglePrice(Credits)` and `SetPrices(BTreeMap<TokenAmount, Credits>)` both expose raw `u64` on the JSON wire surface; the tests at lines 100-123 pin the raw numeric shape `{"SinglePrice": 1234}` / `{"SetPrices": {"5": 50, "10": 80}}`.

This PR establishes a stated `#[json_safe_fields]` convention applied to 25+ V0/V1 struct leaves to protect JS clients from silently rounding u64 values above 2^53−1. Token prices use the 100-billion-per-Dash credit scale (per CLAUDE.md), so a tier price above ~92,233 DASH (~9.2 × 10^15 duffs) drops precision when round-tripped through `JsonConvertible::to_json` and consumed by `JSON.parse` on the JS side. Because `TokenPricingSchedule` is referenced from `TokenConfiguration` distribution rules, this can manifest as off-by-rounding fees with no JS-side error.

Apply `#[json_safe_fields]` (or a Repr wrapper with `json_safe_u64` on the inner `Credits`/`TokenAmount` fields) so JS receives the decimal-string form, matching the convention applied throughout this PR.
- [SUGGESTION] In `packages/rs-dpp/src/data_contract/associated_token/token_distribution_key.rs`:322-389: TokenDistributionInfo round-trip tests only pin PreProgrammed; Perpetual and TokenDistributionTypeWithResolvedRecipient unpinned
  STILL VALID at head a592d79bec. `mod json_convertible_tests_token_distribution_info` (lines 322-389) defines a single fixture `TokenDistributionInfo::PreProgrammed(1_700_000_000_000, Identifier::new([0x42; 32]))` and pins both JSON and Value wire shapes only for that variant. The `Perpetual { moment, recipient }` variant — where `moment` is `RewardDistributionMoment` (an internally-tagged enum migrated to `$type` in 51d31930) and `recipient` is `TokenDistributionRecipient` — is not pinned, and `TokenDistributionTypeWithResolvedRecipient` (the resolved-recipient wrapper with its own `$type`-tagged repr) is also missing canonical wire-shape pins.

The broader `internally-tag everything with $type` sweep is the consensus-critical change being landed here; the unpinned `Perpetual` variant is exactly where a future regression (missing `tag = "$type"` or a silent variant-flip on a nested enum) would land without a fixture there to catch it. Mirror the `PreProgrammed` test for at least one Perpetual case (e.g. `BlockBasedTimeInterval`) and one `TokenDistributionTypeWithResolvedRecipient` case.
- [SUGGESTION] In `packages/rs-dpp/src/data_contract/document_type/property/array.rs`:706-842: ArrayItemType canonical round-trip tests miss Boolean and Date variants
  STILL VALID at head a592d79bec. `mod json_convertible_tests` (lines 712-842) pins JSON + Value wire shapes for only five of `ArrayItemType`'s seven variants: Integer (716, 782), Number (729, 793), String (740, 804), ByteArray (757, 819), and Identifier (771, 833). The two unit variants `Boolean` and `Date` have no canonical wire-shape pin in either path.

This was the last enum landed in the `internally tag everything with $type` sweep (commit c341be6cb7) and is the consensus-critical schema-definition surface for document contracts; a future serde-rename slip on Boolean (`{"$type":"boolean"}` vs `{"$type":"bool"}`) would silently change contract document schemas with no test failure.

Add two more tests symmetric to the existing pattern, one each for `ArrayItemType::Boolean` and `ArrayItemType::Date`, asserting the JSON and Value layers.

…ompiles

The `rs-dapi` Docker build (`cargo build -p rs-dapi`, which enables dpp with
only `state-transitions` — no serde/json/value-conversion) failed to compile
dpp with 37+ errors. `cargo check --workspace` masked it because the workspace
enables the union of all features.

Two root causes, both from the earlier trait-deletion refactor on this branch:

1. Orphaned `#[cfg]` attributes. Deleting the `mod json_conversion;` /
   `mod value_conversion;` declarations left their `#[cfg(feature =
   "json-conversion")]` / `#[cfg(feature = "value-conversion")]` attributes
   dangling onto the *following* module, so core state-transition modules
   (`methods`, `state_transition_like`, `version`, `proved`, `types`) became
   gated behind a conversion feature they have nothing to do with — while
   their importers (`v0_methods.rs`, etc.) referenced them unconditionally.
   Removed the orphaned attributes across the state_transition tree (they were
   ungated at the merge-base).

2. `ExtendedBlockInfoV0` derives serde unconditionally (the block module is
   always-serde by design), but its `signature: [u8; 96]` relied on the
   `json_safe_fields` macro — gated behind `json-conversion` — to serialize a
   >32-byte array. With json-conversion off, the field had no serde handling.
   Annotated it with the existing any-size helper `crate::serialization::
   serde_bytes` and made that helper available in every configuration (it only
   depends on the non-optional `serde` + `base64`), instead of re-introducing a
   bespoke serializer.

Red→green: `cargo build -p rs-dapi --locked` went from 37 compile errors to
clean; `cargo check --workspace --tests` still passes; drive / drive-abci
resolve dpp with json-conversion so their byte handling is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <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 at head ed30168. Latest delta is a small feature-gating repair: ungates crate::serialization::serde_bytes so it compiles in every feature configuration (now needed by always-serde ExtendedBlockInfoV0.signature: [u8; 96]) and removes erroneous #[cfg(feature = "json-conversion"|"value-conversion")] gates from state-transition submodules so rs-dapi (state-transitions-only) compiles. No new defects introduced. All six coordinator-prior-findings re-verified against current head: prior-1, prior-2, prior-3, prior-4, prior-5, prior-6 are STILL VALID and carried forward unchanged. The codex-security latest-delta finding on bounded byte deserialization is dropped as speculative pre-existing hardening unrelated to this PR's scope.

Reviewed commit: ed30168

🔴 2 blocking | 🟡 4 suggestion(s)

Verified findings

blocking: Committed runbook self-labelled 'do not commit' discloses live HPMN validator IP, SSH user, and operator key path

docs/v12-upgrade-boundary-risks.md (line 119)

Re-verified at head ed30168 — file unchanged by the latest delta. The section heading itself reads ## Runbook (local only — contains infra details, do not commit), but the body then publishes operator-sensitive metadata: live testnet HPMN validator hp-masternode-8 = 68.67.122.8 (explicitly identified as a producing validator: "do NOT stop the drive container"), a second host IP 54.185.219.212, SSH user ubuntu, the operator's SSH key path ~/.ssh/evo-app-deploy.rsa, and step-by-step ssh/scp extraction commands. dashpay/platform is a public repository — anyone reading this PR (or the git history once it lands) gets a confirmed live validator IP, the SSH account name, the exact key filename, and confirmation of the validator's Docker layout. This is meaningful reconnaissance against the operator and contradicts the author's own privacy marker. The runbook is also out of scope for the JSON/Value unification work. Remove (or move to local-only storage) before merge; consider history rewrite + key rotation if this commit was already pushed to a public ref.

blocking: wasm-dpp2 TypeScript declarations advertise `type` while rs-dpp runtime emits `$type`

packages/wasm-dpp2/src/asset_lock_proof/proof.rs (line 18)

Re-verified at head ed30168 — file unchanged by the latest delta. The typescript_custom_section declares AssetLockProofObject/AssetLockProofJSON as ({ type: "instant" | "chain" } & ...) and the doc-comment at line 25 still claims the rs-dpp enum uses #[serde(tag = "type")]. The canonical enum at packages/rs-dpp/src/identity/state_transition/asset_lock_proof/mod.rs:38,49 actually uses #[serde(tag = "$type", rename_all = "camelCase")] (this PR's deliberate Critical-2 fix). TS consumers constructing { type: "instant", … } will fail rs-dpp deserialization at the wasm boundary; consumers branching on obj.type will never match the runtime shape (which carries $type). Worse, the TS type permits objects with both type and $type present, allowing a UI/TS layer to think it's signing one variant while the runtime dispatches on $type and produces the other — exactly the cross-variant confusion the $type rename was meant to prevent. Same divergence pattern likely exists across other wasm-dpp2 typescript_custom_section blocks (winner_info, resource_vote_choice, vote_poll, fee_strategy, address_witness, token_event) — sweep with grep before merge.

#[wasm_bindgen(typescript_custom_section)]
const TS_TYPES: &str = r#"
/**
 * AssetLockProof serialized as a plain object.
 *
 * Internally-tagged discriminated union — `$type` discriminates the variant and
 * the variant's fields sit alongside it. Mirrors the rs-dpp serde shape (which
 * uses `#[serde(tag = "$type")]` on the enum) and the convention used by
 * `AddressWitness` / `AddressFundsFeeStrategyStep`.
 */
export type AssetLockProofObject =
    | ({ $type: "instant" } & InstantAssetLockProofObject)
    | ({ $type: "chain" } & ChainAssetLockProofObject);

/**
 * AssetLockProof serialized as JSON.
 */
export type AssetLockProofJSON =
    | ({ $type: "instant" } & InstantAssetLockProofJSON)
    | ({ $type: "chain" } & ChainAssetLockProofJSON);
"#;
suggestion: Lenient constructor does not default or alias `identityContractNonce`, breaking legacy JS callers

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

Re-verified at head ed30168 — file unchanged by the latest delta. The rewritten constructor defaults $formatVersion, userFeeIncrease, signaturePublicKeyId, and signature to keep legacy partial JS input passing the strict from_object(raw) canonical deserializer, but does NOT default or alias the canonical required field identityContractNonce (renamed from the legacy hyphenated $identity-contract-nonce form at packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v0/mod.rs:31-36). External JS consumers that historically passed identityContractNonce / identity_contract_nonce / $identity-contract-nonce, or that omitted the field entirely (intending to set it later via a setter), will now fail at construction with an opaque missing-field error. Because this is replay-protection state, do NOT silently default it to zero — instead, accept the legacy aliases and rename them to the canonical key before calling from_object, so legacy callers continue to work without weakening the nonce contract.

suggestion: TokenPricingSchedule canonical JSON emits raw u64 — JS clients silently round Credits above 2^53−1

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

Re-verified at head ed30168 — file unchanged by the latest delta. TokenPricingSchedule derives plain Serialize/Deserialize (no #[json_safe_fields] attribute) and the canonical impls at lines 48 and 51 are blanket impls with no SafeU64/Repr wrapper. The variants SinglePrice(Credits) and SetPrices(BTreeMap<TokenAmount, Credits>) carry u64 aliases that round-trip through serde_json::Value::Number(u64), which JavaScript silently rounds above Number.MAX_SAFE_INTEGER (2^53 − 1 ≈ 9.0e15). Token pricing carries Credits (1 DASH = 10^11 credits, so ~90 000 DASH exceeds the safe range) — this is exactly the JS-large-integer hazard the rest of this PR's #[json_safe_fields] rollout (25+ V0/V1 leaves) was designed to prevent. A wallet UI fetching a pricing schedule via the JS SDK can silently round the displayed price, leading to a user authorizing one price while the underlying transition charges another. Apply #[json_safe_fields] (or equivalent serde(with = "...") SafeU64 wrappers) consistent with the rest of the codebase.

suggestion: TokenDistributionInfo round-trip tests only pin PreProgrammed; Perpetual + TokenDistributionTypeWithResolvedRecipient unpinned

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

Re-verified at head ed30168 — test module unchanged by the latest delta. mod json_convertible_tests_token_distribution_info defines a single fixture TokenDistributionInfo::PreProgrammed(1_700_000_000_000, Identifier::new([0x42; 32])) and pins both JSON and Value wire shapes only for that variant. The Perpetual variant of TokenDistributionInfo and every variant of the sibling enum TokenDistributionTypeWithResolvedRecipient are unpinned. TokenDistributionInfo controls who receives newly distributed tokens and when, so a future serde/tag-shape regression on these variants is a token-routing risk that the current test matrix would not catch. Add literal json!{} / platform_value!{} assertions per variant following the established pattern at lines 340-388.

suggestion: ArrayItemType canonical round-trip tests miss Boolean and Date variants

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

Re-verified at head ed30168 — test module unchanged by the latest delta. mod json_convertible_tests (lines 712-842) pins JSON + Value wire shapes for only five of ArrayItemType's seven variants: Integer, Number, String, ByteArray, Identifier. The unit variants Boolean and Date remain unpinned. These shape a document property's array-element type and feed the schema validator; a silent wire-shape regression (e.g. booleanbool, datedateTime) would not be caught. Add four short tests mirroring the existing json_round_trip_integer_variant / value_round_trip_integer_variant patterns so the full 7-variant enum is locked in.

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

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

@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

Re-verified all six coordinator-supplied prior findings against head a496b0b — every one is STILL VALID and carried forward. The latest delta (ed30168..a496b0b) is dominated by a v3.1-dev merge of ~400 unrelated files; it did not touch any of the prior-flagged lines. Beyond carry-forwards, this verification also confirms a systematic type vs $type discriminator mismatch across multiple wasm-dpp2 TypeScript custom sections (the same root cause as prior-2 in proof.rs, now confirmed in fee_strategy.rs, vote_poll.rs, resource_vote_choice.rs, winner_info.rs, address_witness.rs, and the TokenEvent doc-comment) — rs-dpp custom Serialize impls emit $type everywhere but the public TS surface advertises type. Consolidated into one additional blocking finding to stay within the 10-finding budget. The Swift identityToHandle dangling-pointer issue raised by codex-ffi-engineer is real but lives in a pre-existing file untouched by this PR's own commits (introduced by upstream PRs pulled in via the v3.1-dev merge) and is out of scope for this PR's stated JSON/Value conversion goal.

Reviewed commit: a496b0b

🔴 3 blocking | 🟡 4 suggestion(s)

Verified findings

blocking: Committed runbook self-labelled 'do not commit' discloses live HPMN validator IP, SSH user, and operator key path

docs/v12-upgrade-boundary-risks.md (line 119)

Re-verified at HEAD a496b0b — file unchanged by the v3.1-dev merge delta. The section heading at L119 itself reads ## Runbook (local only — contains infra details, do not commit), yet the body (L121–135) publishes operator-sensitive metadata into a public-repo branch staged for merge: live testnet HPMN validator hp-masternode-8 = 68.67.122.8, secondary host 54.185.219.212, SSH user ubuntu, operator key path ~/.ssh/evo-app-deploy.rsa, and step-by-step ssh/scp extraction commands targeting the drive volume. The 'do not commit' caveat in the heading does not undo the disclosure once committed.

Remove this section from the PR (move to private/local-only storage). Because the branch was pushed to GitHub, content may already be indexed; consider history rewrite and rotation of the named SSH key, and review whether the named validator host needs hardening.

blocking: TypeScript declarations for AssetLockProof advertise `type` while rs-dpp runtime emits `$type`

packages/wasm-dpp2/src/asset_lock_proof/proof.rs (line 18)

Re-verified at HEAD a496b0b — file unchanged by the latest delta. The typescript_custom_section (L19–38) declares AssetLockProofObject / AssetLockProofJSON as unions discriminated by type, and the in-file doc-comment claims rs-dpp uses #[serde(tag = "type")]. The actual rs-dpp source at packages/rs-dpp/src/identity/state_transition/asset_lock_proof/mod.rs:38,49 is #[serde(tag = "$type", rename_all = "camelCase")] for both AssetLockProof Serialize and RawAssetLockProof Deserialize.

Consequences across the wasm boundary:

  • TS callers constructing { type: "instant", ... } produce objects the rs-dpp Deserialize rejects (or treats the type field as a non-tag map entry).
  • TS callers branching on proof.type never see the actual $type discriminator and silently take the wrong branch — chain-locked proofs could be treated as instant-locked.

Update both TS aliases and the doc-comment to use $type so the public TS surface matches the wire shape (consistent with AddressWitness / AddressFundsFeeStrategyStep conventions).

#[wasm_bindgen(typescript_custom_section)]
const TS_TYPES: &str = r#"
/**
 * AssetLockProof serialized as a plain object.
 *
 * Internally-tagged discriminated union — `$type` discriminates the variant and
 * the variant's fields sit alongside it. Mirrors the rs-dpp serde shape (which
 * uses `#[serde(tag = "$type")]` on the enum) and the convention used by
 * `AddressWitness` / `AddressFundsFeeStrategyStep`.
 */
export type AssetLockProofObject =
    | ({ $type: "instant" } & InstantAssetLockProofObject)
    | ({ $type: "chain" } & ChainAssetLockProofObject);

/**
 * AssetLockProof serialized as JSON.
 */
export type AssetLockProofJSON =
    | ({ $type: "instant" } & InstantAssetLockProofJSON)
    | ({ $type: "chain" } & ChainAssetLockProofJSON);
"#;
blocking: Systematic `type` vs `$type` discriminator mismatch across wasm-dpp2 TypeScript custom sections

packages/wasm-dpp2/src/platform_address/fee_strategy.rs (line 8)

The same root-cause defect that affects AssetLockProof exists in at least five additional wasm-dpp2 TypeScript surfaces. In every case the rs-dpp custom Serialize impl emits $type, but the exported TS declaration declares type:

  • packages/wasm-dpp2/src/platform_address/fee_strategy.rs:17-19,28-30 — TS declares { type: "deductFromInput"|"reduceOutput"; index: number }; rs-dpp at packages/rs-dpp/src/address_funds/fee_strategy/mod.rs:42,46,99 serializes/deserializes $type via a custom Visitor that errors with missing_field("$type").
  • packages/wasm-dpp2/src/voting/vote_poll.rs:38-44,49-55 — TS declares type: "contestedDocumentResourceVotePoll"; rs-dpp at packages/rs-dpp/src/voting/vote_polls/mod.rs:29 uses serde(tag = "$type", rename_all = "camelCase"). The neighboring doc-comment even contradicts the code by claiming the tag is plain type.
  • packages/wasm-dpp2/src/voting/resource_vote_choice.rs:18-21,26-29 — TS declares { type: "towardsIdentity"|"abstain"|"lock"; ... }; rs-dpp's custom Serialize at packages/rs-dpp/src/voting/vote_choices/resource_vote_choice/mod.rs:47,53,58,85,103 keys on $type.
  • packages/wasm-dpp2/src/voting/winner_info.rs:17-20,25-28 — TS declares { type: "noWinner"|"locked"|"wonByIdentity"; ... }; rs-dpp at packages/rs-dpp/src/voting/vote_info_storage/contested_document_vote_poll_winner_info/mod.rs:31,36,42,70,88 keys on $type (and its own internal tests assert "$type": "wonByIdentity").
  • packages/wasm-dpp2/src/shielded/address_witness.rs:16-49 — TS declares type: "p2pkh"|"p2sh"; rs-dpp at packages/rs-dpp/src/address_funds/witness.rs:32 is serde(tag = "$type").
  • packages/wasm-dpp2/src/group/token_event.rs:7-46 — TS interface is permissive ({ type: string; [field: string]: unknown }), but every example in the doc-comment uses { type: ... } while the rs-dpp custom Serialize at packages/rs-dpp/src/tokens/token_event.rs:194,202,210,217,... emits "$type". Typed callers following the doc-comment will build undeserializable events.

The failure mode is identical across all of these: JS callers construct or branch on obj.type while the runtime object only has $type. Either the wire shape is wrong and rs-dpp should emit type (consistent with the in-tree vote_poll doc-comment), or the TS declarations are wrong and should advertise $type (consistent with the AddressWitness/AddressFundsFeeStrategyStep convention the rest of this PR cites). Pick one and align all sites — leaving them divergent guarantees silent client/server disagreement across the wasm boundary on every one of these enums.

suggestion: Lenient constructor does not default or alias `identityContractNonce`, breaking legacy JS callers

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

Re-verified at HEAD a496b0b — file unchanged by the latest delta. The rewritten constructor (L62–75) builds a lenient pre-pass that ensures $formatVersion, userFeeIncrease, signaturePublicKeyId, and signature are present before delegating to strict DataContractUpdateTransition::from_object(raw). But identityContractNonce is canonical-required and is neither defaulted nor aliased from legacy spellings (e.g. $identity-contract-nonce / identity_contract_nonce). Existing JS callers that previously constructed a transition from minimal inputs and signed it afterwards will now hit from_object failure on construction.

Do NOT default to zero — zero is a valid nonce, and silently substituting it would let unsigned/wrong-nonce transitions through with replay/ordering risk. Either accept the legacy alias by remapping it to identityContractNonce before from_object, or document the migration explicitly in the PR's migration notes so consumers know to rename.

suggestion: TokenPricingSchedule canonical JSON emits raw u64 — JS clients silently round Credits above 2^53−1

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

Re-verified at HEAD a496b0b — file unchanged by the latest delta. TokenPricingSchedule derives plain Serialize/Deserialize (L28) with no #[json_safe_fields] attribute, and the JsonConvertible/ValueConvertible impls (L48, L51) are blanket impls with no SafeU64/Repr wrapper.

SinglePrice(Credits) and SetPrices(BTreeMap<TokenAmount, Credits>) both carry u64 aliases. When the canonical JSON is consumed by JS at the wasm boundary, any value above Number.MAX_SAFE_INTEGER (2^53 − 1 ≈ 9.007e15) will silently lose precision. Credit-denominated values for tokens can easily exceed that bound (1 Dash = 1e11 credits, so 100k Dash worth = 1e16 credits, past the safe range). A wallet quoting a price or signing a direct-purchase transition could end up agreeing to different economic terms than the contract author signed.

The PR established #[json_safe_fields] as the canonical convention for large-integer protection. This type should adopt it to be consistent with the 25+ V0/V1 leaves the PR description claims now carry it.

suggestion: TokenDistributionInfo round-trip tests only pin PreProgrammed; Perpetual + TokenDistributionTypeWithResolvedRecipient unpinned

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

Re-verified at HEAD a496b0b — test module unchanged. mod json_convertible_tests_token_distribution_info (L322–389) defines a single fixture: TokenDistributionInfo::PreProgrammed(...). Only that variant has JSON and Value wire-shape pinning. The Perpetual variant of TokenDistributionInfo and the related TokenDistributionTypeWithResolvedRecipient enum (also in this file) are unpinned.

This is the same wire-shape regression risk the PR sets out to prevent for all canonical types — a future refactor of either unpinned variant (rename, tag change, field reorder) can silently break the wire shape without any test failing. Add two more fixture-and-assert tests (one for Perpetual, one for the resolved-recipient enum) to match the convention established by ~85 tests elsewhere in the PR.

suggestion: ArrayItemType canonical round-trip tests miss Boolean and Date variants

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

Re-verified at HEAD a496b0b — test module unchanged. The ArrayItemType enum (L9–17) has seven variants: Integer, Number, String, ByteArray, Identifier, Boolean, Date. The json_convertible_tests module (L706–842) pins JSON + Value wire shapes for only the first five — Boolean and Date are uncovered.

Both missing variants have production callers that branch on type (Date even has special I64→U64 coercion at L168 in this same file), so any silent rename or tag-shape change is a real wire-shape regression risk. Add the four missing tests (json + value × Boolean + Date) to close the symmetry the rest of the module establishes.

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

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

serde has no built-in `Serialize`/`Deserialize` for `[u8; N]` when N > 32
(see serde-rs/serde#1937), and the `json_safe_fields` macro that would inject
the byte serializer only runs under `json-conversion`. `ExtendedBlockInfo`
derives serde unconditionally, so the `serde_bytes` annotation is written out
explicitly to keep it compiling when json-conversion is off (e.g. the
rs-dapi / state-transitions-only build).

Comment-only change; no behavior delta, no test needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <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.

Automated review for 955e5eac.

Body-only fallback: GitHub refused PR diff retrieval for this PR (>300 files), so inline mapping was impossible; posting the same verifier-approved findings as an exact-SHA body-only review.

Summary

Incremental delta a496b0b..955e5ea is comment-only and fixes none of the prior review findings. All seven prior findings are STILL VALID at current HEAD; two additional in-scope suggestions were verified in the cumulative PR surface.

Verified findings

1. blocking: Committed local runbook exposes live validator infrastructure details

docs/v12-upgrade-boundary-risks.md:119-135

Prior status: STILL VALID. This section is explicitly labeled local-only and 'do not commit', but it is checked into the PR and publishes live HPMN/testnet host information, a second host IP, SSH username, operator key path, and concrete ssh/scp dump commands. That is operator-sensitive reconnaissance material in a public repository branch and should not ship with the JSON/Value conversion work. Remove the runbook from the PR and treat the already-pushed host/key details as exposed.

2. blocking: AssetLockProof TypeScript declarations advertise `type` while rs-dpp uses `$type`

packages/wasm-dpp2/src/asset_lock_proof/proof.rs:18-38

Prior status: STILL VALID. The exported TypeScript custom section tells JS callers to construct and inspect AssetLockProof objects with a plain type discriminator, and the comment claims rs-dpp uses #[serde(tag = "type")]. The actual rs-dpp enum and raw deserializer use #[serde(tag = "$type", rename_all = "camelCase")], so callers following these typings will send objects the Rust side rejects and will branch on a field that runtime output does not contain. Align the TS declarations and comments with the executable $type wire shape.

#[wasm_bindgen(typescript_custom_section)]
const TS_TYPES: &str = r#"
/**
 * AssetLockProof serialized as a plain object.
 *
 * Internally-tagged discriminated union: `$type` discriminates the variant and
 * the variant's fields sit alongside it. Mirrors the rs-dpp serde shape (which
 * uses `#[serde(tag = "$type")]` on the enum) and the convention used by
 * `AddressWitness` / `AddressFundsFeeStrategyStep`.
 */
export type AssetLockProofObject =
    | ({ $type: "instant" } & InstantAssetLockProofObject)
    | ({ $type: "chain" } & ChainAssetLockProofObject);

/**
 * AssetLockProof serialized as JSON.
 */
export type AssetLockProofJSON =
    | ({ $type: "instant" } & InstantAssetLockProofJSON)
    | ({ $type: "chain" } & ChainAssetLockProofJSON);
"#;
3. blocking: Additional wasm-dpp2 TypeScript surfaces still use `type` instead of `$type`

packages/wasm-dpp2/src/platform_address/fee_strategy.rs:8-31

Prior status: STILL VALID. This same discriminator mismatch appears beyond AssetLockProof. FeeStrategyStepObject and FeeStrategyStepJSON declare { type: ... }, while AddressFundsFeeStrategyStep serializes $type and its deserializer errors on missing $type; the same stale plain-type pattern remains in VotePoll, ResourceVoteChoice, ContestedDocumentVotePollWinnerInfo, AddressWitness, and TokenEvent TypeScript custom sections or examples. These are public JS contracts for the new canonical conversion surface, so they must match the runtime objects emitted and accepted by rs-dpp.

4. suggestion: Lenient update-transition constructor no longer accepts legacy nonce field

packages/wasm-dpp/src/data_contract/state_transition/data_contract_update_transition/mod.rs:52-79

Prior status: STILL VALID. The constructor now pre-fills $formatVersion, userFeeIncrease, signaturePublicKeyId, and signature before strict from_object, but it does not map the previous JS constructor field identityContractNonce to the canonical serde field $identity-contract-nonce. The PR even changes the existing JS test fixture from identityContractNonce to $identity-contract-nonce, which means existing callers following the old public constructor shape will fail construction. Keep requiring a real nonce, but alias the legacy field or document the migration explicitly.

5. suggestion: TokenPricingSchedule JSON conversion emits u64 prices as unsafe JS numbers

packages/rs-dpp/src/tokens/token_pricing_schedule.rs:28-51

Prior status: STILL VALID. TokenPricingSchedule derives plain serde and its JsonConvertible/ValueConvertible impls do not use json_safe_fields or an equivalent wrapper, so SinglePrice(Credits) and SetPrices(BTreeMap<TokenAmount, Credits>) serialize credit and token amounts as raw JSON numbers. Those u64 values can exceed JavaScript's safe integer range, causing wasm/JS callers to round token prices or thresholds silently. Apply the same large-integer-safe JSON convention used elsewhere in this PR's conversion sweep.

6. suggestion: TokenDistributionInfo tests pin only the PreProgrammed wire shape

packages/rs-dpp/src/data_contract/associated_token/token_distribution_key.rs:322-389

Prior status: STILL VALID. The canonical conversion tests for TokenDistributionInfo use only a PreProgrammed fixture, leaving the Perpetual variant unpinned. The related TokenDistributionTypeWithResolvedRecipient enum also has JsonConvertible/ValueConvertible impls but no literal wire-shape tests. Add fixture assertions for the missing variants so future tag, field, or payload-shape changes fail in the test suite.

7. suggestion: ArrayItemType conversion tests miss Boolean and Date variants

packages/rs-dpp/src/data_contract/document_type/property/array.rs:706-842

Prior status: STILL VALID. ArrayItemType has seven variants, but the canonical JSON/Value tests only pin Integer, Number, String, ByteArray, and Identifier. Boolean and Date are still uncovered, even though Date has dedicated conversion behavior in this file. Add JSON and Value wire-shape assertions for both variants to complete the variant matrix introduced by this PR.

8. suggestion: C/Swift data-contract JSON no longer uses the SDK protocol version

packages/rs-sdk-ffi/src/data_contract/queries/fetch_with_serialization.rs:110-112

New finding. This PR changed the FFI JSON path from contract.to_json(wrapper.sdk.version()) to serde_json::to_value(&contract). DataContract's serde impl explicitly calls PlatformVersion::get_version_or_current_or_latest(None), so the JSON returned to C/Swift can now be serialized with process current/latest platform version instead of the version pinned on this SDK handle. The same regression is present in fetch_json.rs; build DataContractInSerializationFormat with wrapper.sdk.version() before serializing JSON across the FFI boundary.

9. suggestion: BLS public-key sequence deserialization allocates unbounded input before rejecting it

packages/rs-dpp/src/serialization/dashcore/bls_pubkey.rs:151-157

New finding. The new BLS public-key serde wrapper accepts byte sequences for JSON/Value round-trips, but visit_seq pushes every element into a Vec and only checks that the length is exactly 48 after the sequence ends. A malformed payload with a very large array in a public-key slot forces unnecessary allocation and parsing before the deserializer can reject it. Stop as soon as more than 48 bytes are seen.

    fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
        let mut bytes = Vec::with_capacity(COMPRESSED_G1_LEN);
        while let Some(b) = seq.next_element::<u8>()? {
            if bytes.len() >= COMPRESSED_G1_LEN {
                return Err(serde::de::Error::custom(format!(
                    "expected {COMPRESSED_G1_LEN} compressed-G1 bytes for public key, got more"
                )));
            }
            bytes.push(b);
        }
        from_compressed_g1_bytes(&bytes)
    }

Dropped / out-of-scope observations

  • JS→Rust boundary: 32-byte Uint8Array silently re-typed as Value::Identifier — Out of scope for this PR. The heuristic is pre-existing and was not introduced or newly relied on by the JSON/Value conversion changes under review.
  • JS→Rust boundary: f64 as u64 cast loses precision silently above 2^53 — Out of scope for this PR. The conversion behavior predates the reviewed changes and should be handled as a separate wasm input-boundary cleanup.
  • Pre-existing AssetLockProof variant getters can panic from JS — Out of scope for this PR. The panicking getters predate the conversion-surface changes and are not part of the current delta.
  • C-FFI string return-value ownership documentation should name the free routine — Dropped as routine adjacent documentation cleanup. It is not required for the JSON/Value conversion surface to work and does not affect this review action.
🤖 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.

- [BLOCKING] In `docs/v12-upgrade-boundary-risks.md`:119-135: Committed local runbook exposes live validator infrastructure details
  Prior status: STILL VALID. This section is explicitly labeled local-only and 'do not commit', but it is checked into the PR and publishes live HPMN/testnet host information, a second host IP, SSH username, operator key path, and concrete ssh/scp dump commands. That is operator-sensitive reconnaissance material in a public repository branch and should not ship with the JSON/Value conversion work. Remove the runbook from the PR and treat the already-pushed host/key details as exposed.
- [BLOCKING] In `packages/wasm-dpp2/src/asset_lock_proof/proof.rs`:18-38: AssetLockProof TypeScript declarations advertise `type` while rs-dpp uses `$type`
  Prior status: STILL VALID. The exported TypeScript custom section tells JS callers to construct and inspect AssetLockProof objects with a plain `type` discriminator, and the comment claims rs-dpp uses `#[serde(tag = "type")]`. The actual rs-dpp enum and raw deserializer use `#[serde(tag = "$type", rename_all = "camelCase")]`, so callers following these typings will send objects the Rust side rejects and will branch on a field that runtime output does not contain. Align the TS declarations and comments with the executable `$type` wire shape.
- [BLOCKING] In `packages/wasm-dpp2/src/platform_address/fee_strategy.rs`:8-31: Additional wasm-dpp2 TypeScript surfaces still use `type` instead of `$type`
  Prior status: STILL VALID. This same discriminator mismatch appears beyond AssetLockProof. `FeeStrategyStepObject` and `FeeStrategyStepJSON` declare `{ type: ... }`, while `AddressFundsFeeStrategyStep` serializes `$type` and its deserializer errors on missing `$type`; the same stale plain-`type` pattern remains in VotePoll, ResourceVoteChoice, ContestedDocumentVotePollWinnerInfo, AddressWitness, and TokenEvent TypeScript custom sections or examples. These are public JS contracts for the new canonical conversion surface, so they must match the runtime objects emitted and accepted by rs-dpp.
- [SUGGESTION] In `packages/wasm-dpp/src/data_contract/state_transition/data_contract_update_transition/mod.rs`:52-79: Lenient update-transition constructor no longer accepts legacy nonce field
  Prior status: STILL VALID. The constructor now pre-fills `$formatVersion`, `userFeeIncrease`, `signaturePublicKeyId`, and `signature` before strict `from_object`, but it does not map the previous JS constructor field `identityContractNonce` to the canonical serde field `$identity-contract-nonce`. The PR even changes the existing JS test fixture from `identityContractNonce` to `$identity-contract-nonce`, which means existing callers following the old public constructor shape will fail construction. Keep requiring a real nonce, but alias the legacy field or document the migration explicitly.
- [SUGGESTION] In `packages/rs-dpp/src/tokens/token_pricing_schedule.rs`:28-51: TokenPricingSchedule JSON conversion emits u64 prices as unsafe JS numbers
  Prior status: STILL VALID. `TokenPricingSchedule` derives plain serde and its JsonConvertible/ValueConvertible impls do not use `json_safe_fields` or an equivalent wrapper, so `SinglePrice(Credits)` and `SetPrices(BTreeMap<TokenAmount, Credits>)` serialize credit and token amounts as raw JSON numbers. Those u64 values can exceed JavaScript's safe integer range, causing wasm/JS callers to round token prices or thresholds silently. Apply the same large-integer-safe JSON convention used elsewhere in this PR's conversion sweep.
- [SUGGESTION] In `packages/rs-dpp/src/data_contract/associated_token/token_distribution_key.rs`:322-389: TokenDistributionInfo tests pin only the PreProgrammed wire shape
  Prior status: STILL VALID. The canonical conversion tests for TokenDistributionInfo use only a `PreProgrammed` fixture, leaving the `Perpetual` variant unpinned. The related `TokenDistributionTypeWithResolvedRecipient` enum also has JsonConvertible/ValueConvertible impls but no literal wire-shape tests. Add fixture assertions for the missing variants so future tag, field, or payload-shape changes fail in the test suite.
- [SUGGESTION] In `packages/rs-dpp/src/data_contract/document_type/property/array.rs`:706-842: ArrayItemType conversion tests miss Boolean and Date variants
  Prior status: STILL VALID. `ArrayItemType` has seven variants, but the canonical JSON/Value tests only pin Integer, Number, String, ByteArray, and Identifier. Boolean and Date are still uncovered, even though Date has dedicated conversion behavior in this file. Add JSON and Value wire-shape assertions for both variants to complete the variant matrix introduced by this PR.
- [SUGGESTION] In `packages/rs-sdk-ffi/src/data_contract/queries/fetch_with_serialization.rs`:110-112: C/Swift data-contract JSON no longer uses the SDK protocol version
  New finding. This PR changed the FFI JSON path from `contract.to_json(wrapper.sdk.version())` to `serde_json::to_value(&contract)`. `DataContract`'s serde impl explicitly calls `PlatformVersion::get_version_or_current_or_latest(None)`, so the JSON returned to C/Swift can now be serialized with process current/latest platform version instead of the version pinned on this SDK handle. The same regression is present in `fetch_json.rs`; build `DataContractInSerializationFormat` with `wrapper.sdk.version()` before serializing JSON across the FFI boundary.
- [SUGGESTION] In `packages/rs-dpp/src/serialization/dashcore/bls_pubkey.rs`:151-157: BLS public-key sequence deserialization allocates unbounded input before rejecting it
  New finding. The new BLS public-key serde wrapper accepts byte sequences for JSON/Value round-trips, but `visit_seq` pushes every element into a Vec and only checks that the length is exactly 48 after the sequence ends. A malformed payload with a very large array in a public-key slot forces unnecessary allocation and parsing before the deserializer can reject it. Stop as soon as more than 48 bytes are seen.

…m_json(bool)

This PR's mandate is to clean up the JSON/Value *representation*, not to change
validation behavior. Two earlier commits on the branch had quietly changed it:
`e9fad56439` flipped the canonical `DataContract` `Deserialize` from
`full_validation=true` to `false`, and `b40f17550b` replaced the
`from_json(value, full_validation, pv)` API with a `from_json_validated` +
bare-canonical split. Net effect: `serde_json::from_value::<DataContract>(...)`
silently stopped running schema validation — you could construct an invalid
`DataContract`. Reverting both to keep the PR behavior-neutral.

- Canonical `Deserialize` runs full schema validation again (`true`). A
  deserialized `DataContract` is a valid one; `from_str` / `from_value` /
  `#[derive(Deserialize)]` reject schema-invalid contracts as they did
  pre-branch.
- Restored the single boolean entry point on the conversion traits:
  `from_json(value, full_validation, pv)` / `from_value(value, full_validation,
  pv)`. `from_*_validated` is kept as a thin default-method alias for
  `from_*(_, true, _)`, so the ~15 trust-boundary call sites (consensus
  `data_contract_create`, factory, document ingest) are untouched — lowest risk.
- The explicit `false` opt-out (reconstruct already-trusted data without
  re-validating, e.g. storage reads) now routes through `from_*(_, false, _)`,
  which goes via `try_from_platform_versioned` and never the validating serde
  path. Consolidated the SDK `if full_validation { … } else { bare from_value }`
  branches (wasm-dpp2, wasm-dpp, json fixture loader) into the bool method so
  `full_validation=false` keeps skipping validation instead of silently
  validating once Deserialize is flipped back.
- Inverted the Critical-4 pin test to lock the restored behavior: canonical
  Deserialize REJECTS an invalid index, `from_json(_, false, _)` ACCEPTS it,
  `from_json_validated` REJECTS it.

cargo test -p dpp --lib: 3796 passed, 0 failed. cargo check --workspace --tests
and cargo check -p rs-dapi: clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <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

Automated review for 6e5d5683.

Body-only fallback: GitHub refused PR diff retrieval for this PR (>300 files), so inline mapping was impossible; posting the same verifier-approved findings as an exact-SHA body-only review.

Summary

Incremental review against head 6e5d568. Reconciled all 9 prior findings against current code: all 9 are STILL VALID and verified in place. The latest delta restores DataContract validate-by-default Deserialize (with explicit from_json(_, false, _) opt-out) and a behavior pin — that change is internally consistent and correct, so codex-general's new claim that it is a blocking bug is dropped as a false positive. No new in-scope findings emerged. Two blocking issues (committed live-infra runbook; wasm-dpp2 TS type vs runtime $type) gate this PR.

🔴 3 blocking | 🟡 6 suggestion(s)

Carried-forward prior findings

blocking: Committed local runbook exposes live validator infrastructure details

docs/v12-upgrade-boundary-risks.md:119-135

This runbook section is explicitly labeled local only — contains infra details, do not commit, yet it has been checked into the PR. It publishes a live testnet HPMN validator hostname and IP (hp-masternode-8 = 68.67.122.8), a second mainnet-synced host IP (54.185.219.212), the SSH user (ubuntu), the operator key path (~/.ssh/evo-app-deploy.rsa), and the concrete ssh/scp commands used to reach them. That is operator-sensitive reconnaissance material that should not ship in a public branch, and it is unrelated to the JSON/Value conversion work this PR is about. Remove the entire ## Runbook (local only ...) section before merge and treat the already-pushed host/key details as exposed (rotate the deploy key, audit access on the named hosts).

blocking: AssetLockProof TypeScript declarations advertise `type` while rs-dpp uses `$type`

packages/wasm-dpp2/src/asset_lock_proof/proof.rs:18-38

The exported TypeScript custom section tells JS callers to construct and inspect AssetLockProofObject / AssetLockProofJSON with a plain type discriminator, and the doc comment claims rs-dpp uses #[serde(tag = "type")]. The actual rs-dpp enum at packages/rs-dpp/src/identity/state_transition/asset_lock_proof/mod.rs:38,49 uses #[serde(tag = "$type", rename_all = "camelCase")]. Callers following these public typings will construct objects the Rust side rejects on from_object / fromJSON and will branch on a runtime field that does not exist. Align the TS declarations and surrounding comments with the executable $type wire shape.

#[wasm_bindgen(typescript_custom_section)]
const TS_TYPES: &str = r#"
/**
 * AssetLockProof serialized as a plain object.
 *
 * Internally-tagged discriminated union: `$type` discriminates the variant and
 * the variant's fields sit alongside it. Mirrors the rs-dpp serde shape (which
 * uses `#[serde(tag = "$type")]` on the enum) and the convention used by
 * `AddressWitness` / `AddressFundsFeeStrategyStep`.
 */
export type AssetLockProofObject =
    | ({ $type: "instant" } & InstantAssetLockProofObject)
    | ({ $type: "chain" } & ChainAssetLockProofObject);

/**
 * AssetLockProof serialized as JSON.
 */
export type AssetLockProofJSON =
    | ({ $type: "instant" } & InstantAssetLockProofJSON)
    | ({ $type: "chain" } & ChainAssetLockProofJSON);
"#;
blocking: Additional wasm-dpp2 TypeScript surfaces still use `type` instead of `$type`

packages/wasm-dpp2/src/platform_address/fee_strategy.rs:8-31

Same discriminator mismatch beyond AssetLockProof. FeeStrategyStepObject and FeeStrategyStepJSON declare { type: "deductFromInput" | "reduceOutput"; index }, but the runtime serde in packages/rs-dpp/src/address_funds/fee_strategy/mod.rs emits and requires $type (verified at lines 42, 46, 99, with round-trip tests at 131, 141, 148, 153, 206, 217, 237, 250). The same stale plain-type pattern remains in the wasm-dpp2 TS custom sections / examples for VotePoll, ResourceVoteChoice, ContestedDocumentVotePollWinnerInfo, AddressWitness, and TokenEvent. These are public JS contracts for the new canonical conversion surface — they must match the runtime objects rs-dpp emits and accepts. Sweep every wasm-dpp2 typescript_custom_section and example for type: "..." and rewrite to $type: "...".

#[wasm_bindgen(typescript_custom_section)]
const FEE_STRATEGY_STEP_TS_TYPES: &str = r#"
/**
 * Fee strategy step in Object form (output of a transition's `toObject()`).
 *
 * Discriminated by `$type`: "deductFromInput" reduces an input's contribution
 * by the fee, "reduceOutput" reduces an output's amount by the fee. The
 * `index` selects the input/output position.
 */
export type FeeStrategyStepObject =
    | { $type: "deductFromInput"; index: number }
    | { $type: "reduceOutput"; index: number };

/**
 * Fee strategy step in JSON form (output of a transition's `toJSON()`).
 */
export type FeeStrategyStepJSON =
    | { $type: "deductFromInput"; index: number }
    | { $type: "reduceOutput"; index: number };
"#;
suggestion: C/Swift data-contract JSON no longer pins the SDK protocol version

packages/rs-sdk-ffi/src/data_contract/queries/fetch_with_serialization.rs:110-131

This PR changed the FFI JSON path from contract.to_json(wrapper.sdk.version()) to serde_json::to_value(&contract) (line 111). DataContract's manual Serialize impl in packages/rs-dpp/src/data_contract/conversion/serde/mod.rs:48-60 resolves the version via PlatformVersion::get_version_or_current_or_latest(None), which reads process-global state and falls back to the latest known platform version. The JSON now crossing the C ABI to Swift is therefore governed by global state instead of the version pinned on the SDK handle the caller passed in (wrapper.sdk). In the same FFI result the bincode serialized_data is still produced through SDK-version-aware paths, so a divergence between json_string and serialized_data for the same contract is reachable. The same regression is in packages/rs-sdk-ffi/src/data_contract/queries/fetch_json.rs:54. Build DataContractInSerializationFormat with wrapper.sdk.version() and serialize that explicitly across the FFI boundary.

suggestion: Lenient update-transition constructor no longer accepts the legacy `identityContractNonce` field

packages/wasm-dpp/src/data_contract/state_transition/data_contract_update_transition/mod.rs:52-79

The constructor now pre-fills $formatVersion, userFeeIncrease, signaturePublicKeyId, and signature before strict DataContractUpdateTransition::from_object(raw) (lines 71-76), but it does not alias the previous JS public field identityContractNonce to the canonical serde field $identity-contract-nonce. This PR also rewrites the existing JS fixture from identityContractNonce to $identity-contract-nonce, so existing callers built against the old public constructor shape will fail with a missing-field error from the strict deserializer. Either alias the legacy key into the canonical key inside this Value::Map normalization step (keep requiring a real nonce — do not default it to zero), or document the field rename as a public migration in the PR.

suggestion: TokenPricingSchedule JSON emits u64 prices as unsafe JS numbers

packages/rs-dpp/src/tokens/token_pricing_schedule.rs:28-51

TokenPricingSchedule derives plain serde and its JsonConvertible / ValueConvertible impls (lines 48, 51) do not use json_safe_fields or an equivalent large-integer wrapper. As a result SinglePrice(Credits) and SetPrices(BTreeMap<TokenAmount, Credits>) serialize u64 credit and token amounts as raw JSON numbers. Values above Number.MAX_SAFE_INTEGER (2^53) will be silently rounded by JavaScript wasm/SDK callers that go through toJSON() — a financial-amount truncation in the same conversion sweep this PR introduces. Apply the safe-integer JSON convention used elsewhere in the PR to both variants.

suggestion: TokenDistributionInfo conversion tests pin only the PreProgrammed variant

packages/rs-dpp/src/data_contract/associated_token/token_distribution_key.rs:322-389

The canonical conversion tests for TokenDistributionInfo use only a PreProgrammed fixture (lines 336-388), leaving the Perpetual variant unpinned. The related TokenDistributionTypeWithResolvedRecipient enum also has JsonConvertible / ValueConvertible impls but no literal wire-shape tests. Wire-shape pinning is the central guarantee this PR introduces, so the variant matrix should be complete: add JSON and Value fixture assertions for Perpetual and for each variant of TokenDistributionTypeWithResolvedRecipient so a future tag, field, or payload-shape change fails loudly.

suggestion: ArrayItemType conversion tests miss Boolean and Date variants

packages/rs-dpp/src/data_contract/document_type/property/array.rs:706-842

ArrayItemType has seven variants but the new canonical conversion tests only pin Integer, Number, String, ByteArray, and Identifier. Boolean and Date are uncovered, even though Date has dedicated conversion behavior in this file. Add JSON and Value wire-shape assertions for both so the variant matrix introduced by this PR is complete.

suggestion: BLS public-key `visit_seq` collects unbounded input before rejecting it

packages/rs-dpp/src/serialization/dashcore/bls_pubkey.rs:151-157

The new BLS public-key serde wrapper accepts byte sequences for JSON/Value round-trips, but visit_seq pushes every element into a Vec and only checks the exact 48-byte (COMPRESSED_G1_LEN) length inside from_compressed_g1_bytes(&bytes) after the sequence ends. A malformed payload with a very large array in a public-key slot forces unnecessary allocation and parsing before the deserializer rejects it. This is currently reached only from trusted Dash Core validator data, but the with = "..." pattern is designed for reuse and the failure mode amplifies as soon as another type reuses this module on a less trusted ingest path. Cap the loop at COMPRESSED_G1_LEN + 1 and error on overflow so the structural promise ("48-byte key") is enforced inside the visitor.

    fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
        let mut bytes = Vec::with_capacity(COMPRESSED_G1_LEN);
        while let Some(b) = seq.next_element::<u8>()? {
            if bytes.len() >= COMPRESSED_G1_LEN {
                return Err(serde::de::Error::custom(format!(
                    "expected {COMPRESSED_G1_LEN} compressed-G1 bytes for public key, got more"
                )));
            }
            bytes.push(b);
        }
        from_compressed_g1_bytes(&bytes)
    }

Dropped / false-positive observations

  • DataContract serde deserialization validates despite the canonical no-validation contract (codex-general) — False positive. The latest commit (6e5d568 'fix(dpp)!: restore validate-by-default DataContract deserialize + from_json(bool)') is an intentional reversal — Deserialize validates by default and the explicit opt-out is from_json(_, false, _) / from_value(_, false, _). A pin test in packages/rs-dpp/src/data_contract/conversion/serde/mod.rs enforces the round-trip behavior. The PR description is stale relative to the shipped semantics, but the code is consistent with itself and with the new opt-out API. The claim that this breaks rs-scripts/src/bin/register_contract.rs is not load-bearing for the PR's stated goal, and the safer default for an SDK trust boundary is to validate. Independent security review (claude-security-auditor) reached the same conclusion.
  • wasm-dpp2 fromJSON/fromObject expose full_validation: bool to JS callers with no safer-default helper (claude-security-auditor) — Defense-in-depth ergonomic suggestion only, not a defect. SDK-side validation is not trusted by Drive in any case. Treating it as a hard blocker would require a parallel API redesign on the JS surface that is outside this PR's scope.
  • Pre-existing handle leak in dash_sdk_data_contract_fetch_with_serialization JSON error paths (claude-ffi-engineer) — Pre-existing on origin/v3.1-dev and not introduced or worsened by this PR. The PR only changed which JSON-conversion call sits inside the same error-return structure. Belongs in a separate FFI cleanup PR.
  • Legacy wasm-dpp toJSON/toObject switched from PlatformVersion::first() to global current (claude-ffi-engineer) — Behavioral change is acknowledged by the PR's scope (canonical serde routes via PlatformVersion::get_version_or_current_or_latest) and the legacy wasm-dpp crate is documented as slated for deletion. Not gating; if there is a concrete JS consumer hitting a V0-vs-V1 shape regression, a focused follow-up makes more sense than backing out the unification here.
  • Surplus address proof result omits JS-safe credit serialization in VerifiedAssetLockConsumedWithAddressInfos (codex-security-auditor) — Same root cause as the TokenPricingSchedule JS-safe-integer finding already kept (defense in depth across u64 financial fields). Folding it into one issue under the safe-integer sweep keeps the public review focused; tracking via the kept finding rather than as a separate suggestion.
  • Audit remaining serde callsites that previously assumed no-validation Deserialize (claude-security-auditor) — Workspace-wide audit follow-up unrelated to PR-introduced behavior — out of scope for this dispatcher and not concrete enough to track here.
🤖 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.

- [BLOCKING] In `docs/v12-upgrade-boundary-risks.md:119-135`: Committed local runbook exposes live validator infrastructure details
  This runbook section is explicitly labeled `local only — contains infra details, do not commit`, yet it has been checked into the PR. It publishes a live testnet HPMN validator hostname and IP (`hp-masternode-8` = `68.67.122.8`), a second mainnet-synced host IP (`54.185.219.212`), the SSH user (`ubuntu`), the operator key path (`~/.ssh/evo-app-deploy.rsa`), and the concrete `ssh`/`scp` commands used to reach them. That is operator-sensitive reconnaissance material that should not ship in a public branch, and it is unrelated to the JSON/Value conversion work this PR is about. Remove the entire `## Runbook (local only ...)` section before merge and treat the already-pushed host/key details as exposed (rotate the deploy key, audit access on the named hosts).
- [BLOCKING] In `packages/wasm-dpp2/src/asset_lock_proof/proof.rs:18-38`: AssetLockProof TypeScript declarations advertise `type` while rs-dpp uses `$type`
  The exported TypeScript custom section tells JS callers to construct and inspect `AssetLockProofObject` / `AssetLockProofJSON` with a plain `type` discriminator, and the doc comment claims rs-dpp uses `#[serde(tag = "type")]`. The actual rs-dpp enum at `packages/rs-dpp/src/identity/state_transition/asset_lock_proof/mod.rs:38,49` uses `#[serde(tag = "$type", rename_all = "camelCase")]`. Callers following these public typings will construct objects the Rust side rejects on `from_object` / `fromJSON` and will branch on a runtime field that does not exist. Align the TS declarations and surrounding comments with the executable `$type` wire shape.
- [BLOCKING] In `packages/wasm-dpp2/src/platform_address/fee_strategy.rs:8-31`: Additional wasm-dpp2 TypeScript surfaces still use `type` instead of `$type`
  Same discriminator mismatch beyond AssetLockProof. `FeeStrategyStepObject` and `FeeStrategyStepJSON` declare `{ type: "deductFromInput" | "reduceOutput"; index }`, but the runtime serde in `packages/rs-dpp/src/address_funds/fee_strategy/mod.rs` emits and requires `$type` (verified at lines 42, 46, 99, with round-trip tests at 131, 141, 148, 153, 206, 217, 237, 250). The same stale plain-`type` pattern remains in the wasm-dpp2 TS custom sections / examples for `VotePoll`, `ResourceVoteChoice`, `ContestedDocumentVotePollWinnerInfo`, `AddressWitness`, and `TokenEvent`. These are public JS contracts for the new canonical conversion surface — they must match the runtime objects rs-dpp emits and accepts. Sweep every `wasm-dpp2` typescript_custom_section and example for `type: "..."` and rewrite to `$type: "..."`.
- [SUGGESTION] In `packages/rs-sdk-ffi/src/data_contract/queries/fetch_with_serialization.rs:110-131`: C/Swift data-contract JSON no longer pins the SDK protocol version
  This PR changed the FFI JSON path from `contract.to_json(wrapper.sdk.version())` to `serde_json::to_value(&contract)` (line 111). `DataContract`'s manual `Serialize` impl in `packages/rs-dpp/src/data_contract/conversion/serde/mod.rs:48-60` resolves the version via `PlatformVersion::get_version_or_current_or_latest(None)`, which reads process-global state and falls back to the latest known platform version. The JSON now crossing the C ABI to Swift is therefore governed by global state instead of the version pinned on the SDK handle the caller passed in (`wrapper.sdk`). In the same FFI result the bincode `serialized_data` is still produced through SDK-version-aware paths, so a divergence between `json_string` and `serialized_data` for the same contract is reachable. The same regression is in `packages/rs-sdk-ffi/src/data_contract/queries/fetch_json.rs:54`. Build `DataContractInSerializationFormat` with `wrapper.sdk.version()` and serialize that explicitly across the FFI boundary.
- [SUGGESTION] In `packages/wasm-dpp/src/data_contract/state_transition/data_contract_update_transition/mod.rs:52-79`: Lenient update-transition constructor no longer accepts the legacy `identityContractNonce` field
  The constructor now pre-fills `$formatVersion`, `userFeeIncrease`, `signaturePublicKeyId`, and `signature` before strict `DataContractUpdateTransition::from_object(raw)` (lines 71-76), but it does not alias the previous JS public field `identityContractNonce` to the canonical serde field `$identity-contract-nonce`. This PR also rewrites the existing JS fixture from `identityContractNonce` to `$identity-contract-nonce`, so existing callers built against the old public constructor shape will fail with a missing-field error from the strict deserializer. Either alias the legacy key into the canonical key inside this `Value::Map` normalization step (keep requiring a real nonce — do not default it to zero), or document the field rename as a public migration in the PR.
- [SUGGESTION] In `packages/rs-dpp/src/tokens/token_pricing_schedule.rs:28-51`: TokenPricingSchedule JSON emits u64 prices as unsafe JS numbers
  `TokenPricingSchedule` derives plain serde and its `JsonConvertible` / `ValueConvertible` impls (lines 48, 51) do not use `json_safe_fields` or an equivalent large-integer wrapper. As a result `SinglePrice(Credits)` and `SetPrices(BTreeMap<TokenAmount, Credits>)` serialize u64 credit and token amounts as raw JSON numbers. Values above `Number.MAX_SAFE_INTEGER` (2^53) will be silently rounded by JavaScript wasm/SDK callers that go through `toJSON()` — a financial-amount truncation in the same conversion sweep this PR introduces. Apply the safe-integer JSON convention used elsewhere in the PR to both variants.
- [SUGGESTION] In `packages/rs-dpp/src/data_contract/associated_token/token_distribution_key.rs:322-389`: TokenDistributionInfo conversion tests pin only the PreProgrammed variant
  The canonical conversion tests for `TokenDistributionInfo` use only a `PreProgrammed` fixture (lines 336-388), leaving the `Perpetual` variant unpinned. The related `TokenDistributionTypeWithResolvedRecipient` enum also has `JsonConvertible` / `ValueConvertible` impls but no literal wire-shape tests. Wire-shape pinning is the central guarantee this PR introduces, so the variant matrix should be complete: add JSON and Value fixture assertions for `Perpetual` and for each variant of `TokenDistributionTypeWithResolvedRecipient` so a future tag, field, or payload-shape change fails loudly.
- [SUGGESTION] In `packages/rs-dpp/src/data_contract/document_type/property/array.rs:706-842`: ArrayItemType conversion tests miss Boolean and Date variants
  `ArrayItemType` has seven variants but the new canonical conversion tests only pin Integer, Number, String, ByteArray, and Identifier. Boolean and Date are uncovered, even though `Date` has dedicated conversion behavior in this file. Add JSON and Value wire-shape assertions for both so the variant matrix introduced by this PR is complete.
- [SUGGESTION] In `packages/rs-dpp/src/serialization/dashcore/bls_pubkey.rs:151-157`: BLS public-key `visit_seq` collects unbounded input before rejecting it
  The new BLS public-key serde wrapper accepts byte sequences for JSON/Value round-trips, but `visit_seq` pushes every element into a `Vec` and only checks the exact 48-byte (`COMPRESSED_G1_LEN`) length inside `from_compressed_g1_bytes(&bytes)` after the sequence ends. A malformed payload with a very large array in a public-key slot forces unnecessary allocation and parsing before the deserializer rejects it. This is currently reached only from trusted Dash Core validator data, but the `with = "..."` pattern is designed for reuse and the failure mode amplifies as soon as another type reuses this module on a less trusted ingest path. Cap the loop at `COMPRESSED_G1_LEN + 1` and error on overflow so the structural promise ("48-byte key") is enforced inside the visitor.

…serializes

Follow-up to the validate-by-default restore: these two drive test sites built
a DataContract via bare `platform_value::from_value::<DataContract>`, which now
routes through the validating serde `Deserialize`. The fixtures are valid (and
re-validated by `apply_contract` downstream), so there's no behavior change in
practice — but switch them to the explicit no-validation path
`DataContract::from_value(value, false, platform_version)` for zero ambiguity,
matching their original no-validation intent.

Co-Authored-By: Claude Opus 4.8 (1M context) <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 at head f574f8e. Latest delta from 6e5d568 is two rs-drive test-only call-site updates switching platform_value::from_value::<DataContract> to DataContract::from_value(_, false, platform_version) — no new findings, and no prior finding fixed. All 9 coordinator-prior-findings classified STILL VALID against the current tree (re-verified file/line evidence for the runbook leak, the wasm-dpp2 type vs rs-dpp $type discriminator gap on AssetLockProof and FeeStrategyStep, the FFI JSON path losing SDK-pinned version, the dropped identityContractNonce alias, the unsafe-integer leak in TokenPricingSchedule, the variant-coverage gaps in TokenDistributionInfo and ArrayItemType, and the unbounded visit_seq in the BLS pubkey wrapper). 9 in-scope findings carried forward; under the 10-finding budget.

Reviewed commit: f574f8e

🔴 3 blocking | 🟡 6 suggestion(s)

Verified findings

blocking: Committed local runbook exposes live validator infrastructure details

docs/v12-upgrade-boundary-risks.md (line 119)

Re-verified at head f574f8e. The section header explicitly says local only — contains infra details, do not commit, but it is checked into a public PR branch. It publishes a live testnet HPMN validator hostname/IP (hp-masternode-8 = 68.67.122.8), a second host IP (54.185.219.212), the SSH user (ubuntu), the operator deploy-key path (~/.ssh/evo-app-deploy.rsa), and the exact ssh/scp invocations to reach them. That is operator-sensitive reconnaissance material unrelated to the JSON/Value conversion work this PR is about. Remove the ## Runbook (local only ...) section before merge, and treat the already-pushed host/key details as exposed: rotate evo-app-deploy.rsa, audit recent SSH access on 68.67.122.8 and 54.185.219.212, and consider rewriting branch history to drop these lines.

blocking: AssetLockProof TS declarations advertise `type` while rs-dpp serializes `$type`

packages/wasm-dpp2/src/asset_lock_proof/proof.rs (line 18)

Re-verified at head f574f8e. The exported typescript_custom_section declares AssetLockProofObject / AssetLockProofJSON with a plain type discriminator (lines 28–37) and the doc comment claims rs-dpp uses #[serde(tag = "type")]. The actual rs-dpp enum at packages/rs-dpp/src/identity/state_transition/asset_lock_proof/mod.rs:38 and :49 is #[serde(tag = "$type", rename_all = "camelCase")]. JS callers following these public typings will construct objects rs-dpp rejects on from_object / fromJSON (missing $type) and will branch on a runtime field rs-dpp does not emit. Align the TS declarations and doc comment with the executable $type wire shape.

#[wasm_bindgen(typescript_custom_section)]
const TS_TYPES: &str = r#"
/**
 * AssetLockProof serialized as a plain object.
 *
 * Internally-tagged discriminated union — `$type` discriminates the variant and
 * the variant's fields sit alongside it. Mirrors the rs-dpp serde shape (which
 * uses `#[serde(tag = "$type")]` on the enum) and the convention used by
 * `AddressWitness` / `AddressFundsFeeStrategyStep`.
 */
export type AssetLockProofObject =
    | ({ $type: "instant" } & InstantAssetLockProofObject)
    | ({ $type: "chain" } & ChainAssetLockProofObject);

/**
 * AssetLockProof serialized as JSON.
 */
export type AssetLockProofJSON =
    | ({ $type: "instant" } & InstantAssetLockProofJSON)
    | ({ $type: "chain" } & ChainAssetLockProofJSON);
"#;
blocking: wasm-dpp2 TS surfaces still use `type` instead of `$type`

packages/wasm-dpp2/src/platform_address/fee_strategy.rs (line 8)

Re-verified at head f574f8e. FeeStrategyStepObject and FeeStrategyStepJSON declare { type: "deductFromInput" | "reduceOutput"; index }, but the runtime serde for AddressFundsFeeStrategyStep in packages/rs-dpp/src/address_funds/fee_strategy/mod.rs emits and requires $type (verified at lines 42, 46, 81, 99 with round-trip tests at 131, 141, 148, 153). The same stale plain-type pattern remains in wasm-dpp2 TS custom sections / examples for VotePoll, ResourceVoteChoice, ContestedDocumentVotePollWinnerInfo, AddressWitness, and TokenEvent. JS callers building values to these typings will fail strict deserialization with missing field $type, and inspection code branching on .type will see undefined. Sweep every wasm-dpp2 typescript_custom_section and example for type: "..." and rewrite to $type: "...".

#[wasm_bindgen(typescript_custom_section)]
const FEE_STRATEGY_STEP_TS_TYPES: &str = r#"
/**
 * Fee strategy step in Object form (output of a transition's `toObject()`).
 *
 * Discriminated by `$type`: "deductFromInput" reduces an input's contribution
 * by the fee, "reduceOutput" reduces an output's amount by the fee. The
 * `index` selects the input/output position.
 */
export type FeeStrategyStepObject =
    | { $type: "deductFromInput"; index: number }
    | { $type: "reduceOutput"; index: number };

/**
 * Fee strategy step in JSON form (output of a transition's `toJSON()`).
 *
 * Identical shape to `FeeStrategyStepObject` because the only payload is a
 * small `index` (u16) which serializes the same way in both binary and
 * human-readable formats.
 */
export type FeeStrategyStepJSON =
    | { $type: "deductFromInput"; index: number }
    | { $type: "reduceOutput"; index: number };
"#;
suggestion: C/Swift data-contract JSON no longer pins the SDK protocol version

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

Re-verified at head f574f8e (line 111 still serde_json::to_value(&contract)). DataContract's manual Serialize impl in packages/rs-dpp/src/data_contract/conversion/serde/mod.rs:48-60 resolves the version via PlatformVersion::get_version_or_current_or_latest(None), which reads process-global state and falls back to the latest known platform version. The JSON crossing the C ABI to Swift is therefore governed by global state instead of the version pinned on the SDK handle the caller passed in (wrapper.sdk). In the same FFI result the bincode serialized_data is still produced through SDK-version-aware paths, so a divergence between json_string and serialized_data for the same contract is reachable. The same regression is in packages/rs-sdk-ffi/src/data_contract/queries/fetch_json.rs:54. Build a DataContractInSerializationFormat with wrapper.sdk.version() and serialize that explicitly across the FFI boundary so the version is pinned to the caller's SDK handle.

suggestion: Lenient update-transition constructor no longer accepts legacy `identityContractNonce` field

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

Re-verified at head f574f8e. The constructor pre-fills $formatVersion, userFeeIncrease, signaturePublicKeyId, and signature before strict DataContractUpdateTransition::from_object(raw) (lines 71–76), but it does not alias the previously-public JS field identityContractNonce into the canonical serde key $identity-contract-nonce. This PR also rewrites the existing JS fixture from identityContractNonce to $identity-contract-nonce, so existing callers built against the old public constructor shape will fail with a missing-field error from the strict deserializer. Either alias the legacy key into the canonical key inside this Value::Map normalization step (keep requiring a real nonce — do not default it to zero), or document the field rename as a public migration in the PR.

suggestion: TokenPricingSchedule JSON emits u64 prices as unsafe JS numbers

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

Re-verified at head f574f8e. TokenPricingSchedule derives plain serde at line 28 and its JsonConvertible / ValueConvertible impls (lines 48, 51) do not use json_safe_fields or an equivalent large-integer wrapper. As a result SinglePrice(Credits) and SetPrices(BTreeMap<TokenAmount, Credits>) serialize u64 credit and token amounts as raw JSON numbers. Values above Number.MAX_SAFE_INTEGER (2^53) will be silently rounded by JavaScript wasm/SDK callers that go through toJSON() — a financial-amount truncation introduced by the same conversion sweep this PR ships. Credits is a Dash credit amount (1 Dash = 1e11 credits), so prices above ≈90,071 Dash overflow JS safe-integer range. Apply the safe-integer JSON convention used elsewhere in the PR to both variants.

suggestion: TokenDistributionInfo conversion tests pin only the PreProgrammed variant

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

Re-verified at head f574f8e (lines 336–388 still use only a PreProgrammed fixture). The canonical conversion tests for TokenDistributionInfo leave the Perpetual variant unpinned. The related TokenDistributionTypeWithResolvedRecipient enum also has JsonConvertible / ValueConvertible impls but no literal wire-shape tests. Wire-shape pinning is the central guarantee this PR introduces, so the variant matrix should be complete: add JSON and Value fixture assertions for Perpetual and for each variant of TokenDistributionTypeWithResolvedRecipient so a future tag, field, or payload-shape change fails loudly.

suggestion: ArrayItemType conversion tests miss Boolean and Date variants

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

Re-verified at head f574f8e (test module 706–842 still exercises only Integer, Number, String, ByteArray, Identifier). ArrayItemType has seven variants; Boolean and Date are uncovered even though Date has dedicated conversion behavior in this file (timestamp vs. ISO-string handling), making it the highest-risk variant for a silent wire-shape regression. Add JSON and Value wire-shape assertions for both variants so the variant matrix introduced by this PR is complete.

suggestion: BLS public-key `visit_seq` collects unbounded input before rejecting it

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

Re-verified at head f574f8e (visitor body unchanged). The new BLS public-key serde wrapper accepts byte sequences for JSON/Value round-trips, but visit_seq pushes every element into a Vec and only checks the exact 48-byte (COMPRESSED_G1_LEN) length inside from_compressed_g1_bytes(&bytes) after the sequence ends. A malformed payload with a very large array in a public-key slot forces unnecessary allocation and parsing before the deserializer rejects it. Currently reached only from trusted Dash Core validator data, but the with = "..." pattern is designed for reuse and the failure mode amplifies as soon as another type reuses this module on a less-trusted ingest path. Cap the loop at COMPRESSED_G1_LEN + 1 and error on overflow so the 48-byte structural promise is enforced inside the visitor.

    fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
        let mut bytes = Vec::with_capacity(COMPRESSED_G1_LEN);
        while let Some(b) = seq.next_element::<u8>()? {
            if bytes.len() == COMPRESSED_G1_LEN {
                return Err(serde::de::Error::invalid_length(
                    COMPRESSED_G1_LEN + 1,
                    &"exactly 48 bytes for a compressed G1 BLS public key",
                ));
            }
            bytes.push(b);
        }
        from_compressed_g1_bytes(&bytes)
    }

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

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

…ol) API

Follow-up: `from_json_validated` / `from_value_validated` were thin convenience
aliases for `from_*(_, true, _)`, kept only to avoid churning call sites. They're
redundant with the boolean entry point and the original pre-branch API had no
such variant, so remove them and migrate every caller to
`from_*(value, full_validation, pv)`:

- `from_*_validated(v, pv)` -> `from_*(v, true, pv)` across ~22 sites
  (rs-dpp cbor/factory/extended_document/document_create_transition/json fixture
  loader + ~14 consensus sites in rs-drive-abci data_contract_create/update).
  Behavior-identical (validated == full_validation true).
- Removed the two default-method aliases from the conversion traits.
- Refreshed the DataContract module/trait docs to describe the single
  `from_*(value, full_validation, pv)` entry point (canonical serde Deserialize
  validates; `false` is the explicit no-validation opt-out).

cargo check --workspace --tests clean; dpp lib 3796 passed / 0 failed;
drive-abci lib 2538 passed / 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <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

All 9 prior findings re-validated as STILL VALID against head 24ade51. The latest delta f574f8e..24ade51 is a pure refactor removing from_*_validated aliases on DataContract in favor of the from_*(value, bool, &pv) API; it does not touch any of the prior-flagged code paths. One new in-scope finding from codex-general: the unification plan doc still describes the now-deleted _validated helpers and the opposite (no-validation) serde default, contradicting the executable API at conversion/serde/mod.rs:74-79 and the (just-removed) helpers. Review action is REQUEST_CHANGES because prior-1 (committed local infra/SSH runbook) and prior-2/-3 (wasm-dpp2 TS type vs Rust $type discriminator mismatch) remain blocking.

Reviewed commit: 24ade51

🔴 3 blocking | 🟡 7 suggestion(s)

Verified findings

blocking: Committed local runbook exposes live validator infrastructure details

docs/v12-upgrade-boundary-risks.md (line 119)

This section is explicitly labeled local only — contains infra details, do not commit, but it is checked into the PR at 24ade51 unchanged. It publishes a live testnet HPMN validator hostname/IP (hp-masternode-8 = 68.67.122.8, ansible_user ubuntu), a second mainnet-synced host IP (54.185.219.212), the operator SSH key path (~/.ssh/evo-app-deploy.rsa), and concrete ssh/scp invocations. This is operator-sensitive reconnaissance material that is unrelated to the JSON/Value conversion work this PR is about. Remove the entire ## Runbook (local only ...) section before merge and treat the already-pushed host/key details as exposed: rotate evo-app-deploy.rsa and audit access on the named hosts.

blocking: AssetLockProof TypeScript declarations advertise `type` while rs-dpp uses `$type`

packages/wasm-dpp2/src/asset_lock_proof/proof.rs (line 18)

Verified at head: the typescript_custom_section declares AssetLockProofObject / AssetLockProofJSON with a plain type discriminator and the doc comment claims rs-dpp uses #[serde(tag = "type")]. The rs-dpp enum at packages/rs-dpp/src/identity/state_transition/asset_lock_proof/mod.rs:37-49 actually uses #[serde(tag = "$type", rename_all = "camelCase")] for both Serialize and RawAssetLockProof Deserialize. JS callers following these public typings will construct objects rs-dpp rejects on from_object / fromJSON (asset-lock proofs feed identity create / topup flows) and will read undefined when branching on obj.type after a Rust→JS round trip. Align the TS declarations and surrounding comments with the executable $type wire shape.

#[wasm_bindgen(typescript_custom_section)]
const TS_TYPES: &str = r#"
/**
 * AssetLockProof serialized as a plain object.
 *
 * Internally-tagged discriminated union — `$type` discriminates the variant and
 * the variant's fields sit alongside it. Mirrors the rs-dpp serde shape (which
 * uses `#[serde(tag = "$type")]` on the enum) and the convention used by
 * `AddressWitness` / `AddressFundsFeeStrategyStep`.
 */
export type AssetLockProofObject =
    | ({ $type: "instant" } & InstantAssetLockProofObject)
    | ({ $type: "chain" } & ChainAssetLockProofObject);

/**
 * AssetLockProof serialized as JSON.
 */
export type AssetLockProofJSON =
    | ({ $type: "instant" } & InstantAssetLockProofJSON)
    | ({ $type: "chain" } & ChainAssetLockProofJSON);
"#;
blocking: Additional wasm-dpp2 TS surfaces still use `type` instead of `$type`

packages/wasm-dpp2/src/platform_address/fee_strategy.rs (line 8)

Same discriminator mismatch beyond AssetLockProof. Verified at head: FeeStrategyStepObject / FeeStrategyStepJSON declare { type: "deductFromInput" | "reduceOutput"; index }, but the runtime serde in packages/rs-dpp/src/address_funds/fee_strategy/mod.rs emits $type (lines 42, 46) and the deserializer rejects payloads missing $type (line 99) with round-trip tests at 131/138/141 asserting the $type shape. The same stale plain-type pattern remains in wasm-dpp2 typescript_custom_section blocks for VotePoll, ResourceVoteChoice, ContestedDocumentVotePollWinnerInfo (voting/winner_info.rs), AddressWitness (shielded/address_witness.rs), and TokenEvent (group/token_event.rs). Fee-strategy steps drive which input/output absorbs the transaction fee; rejecting all steps because the discriminator key is wrong silently changes accounting at the JS boundary. Sweep every wasm-dpp2 typescript_custom_section and example for type: "..." and rewrite to $type: "...".

#[wasm_bindgen(typescript_custom_section)]
const FEE_STRATEGY_STEP_TS_TYPES: &str = r#"
/**
 * Fee strategy step in Object form (output of a transition's `toObject()`).
 *
 * Discriminated by `$type`: "deductFromInput" reduces an input's contribution
 * by the fee, "reduceOutput" reduces an output's amount by the fee. The
 * `index` selects the input/output position.
 */
export type FeeStrategyStepObject =
    | { $type: "deductFromInput"; index: number }
    | { $type: "reduceOutput"; index: number };

/**
 * Fee strategy step in JSON form (output of a transition's `toJSON()`).
 */
export type FeeStrategyStepJSON =
    | { $type: "deductFromInput"; index: number }
    | { $type: "reduceOutput"; index: number };
"#;
suggestion: C/Swift data-contract JSON no longer pins the SDK protocol version

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

Verified at head: this FFI path now calls serde_json::to_value(&contract) (line 111). DataContract's manual Serialize impl in packages/rs-dpp/src/data_contract/conversion/serde/mod.rs:48-60 resolves the platform version via PlatformVersion::get_version_or_current_or_latest(None) — process-global state with a fallback to the latest known platform version — rather than the version pinned on the SDK handle the caller passed in (wrapper.sdk). In the same FFI result the bincode serialized_data is still produced through SDK-version-aware paths, so a divergence between json_string and serialized_data for the same contract is reachable whenever the active global differs from wrapper.sdk.version(). The same regression is in packages/rs-sdk-ffi/src/data_contract/queries/fetch_json.rs:54. Build DataContractInSerializationFormat with wrapper.sdk.version() and serialize that explicitly across the FFI boundary.

suggestion: Lenient update-transition constructor no longer accepts legacy `identityContractNonce`

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

Verified at head: the constructor pre-fills $formatVersion, userFeeIncrease, signaturePublicKeyId, and signature before strict DataContractUpdateTransition::from_object(raw) (lines 71-76), but it does not alias the previous JS public field identityContractNonce to the canonical serde field $identity-contract-nonce. This PR also rewrites the existing JS fixture from identityContractNonce to $identity-contract-nonce, so existing callers built against the old constructor shape now fail with a strict missing-field error. The nonce is replay-protection material for data-contract updates, so this is a breaking JS API rename for wallets/services. Either alias the legacy key into the canonical key in this Value::Map normalization step (keep requiring a real nonce — do not default to zero), or document the field rename as a public migration in the PR notes.

suggestion: TokenPricingSchedule JSON emits u64 prices as unsafe JS numbers

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

Verified at head: TokenPricingSchedule derives plain Serialize/Deserialize (line 28) and its JsonConvertible / ValueConvertible impls (lines 48, 51) do not use json_safe_fields or any equivalent large-integer wrapper. SinglePrice(Credits) and SetPrices(BTreeMap<TokenAmount, Credits>) therefore serialize u64 credit and token amounts as raw JSON numbers. Values above Number.MAX_SAFE_INTEGER (2^53) are silently rounded by JavaScript SDK/wasm callers that go through any toJSON() path — a financial-amount truncation on a type that is specifically about authoritative token pricing, in the same conversion sweep this PR introduces. Apply the safe-integer JSON convention used elsewhere in the PR to both variants.

suggestion: TokenDistributionInfo conversion tests pin only the PreProgrammed variant

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

Verified at head: the canonical conversion tests for TokenDistributionInfo use only a PreProgrammed fixture (lines 336-388), leaving the Perpetual variant unpinned. The related TokenDistributionTypeWithResolvedRecipient enum also has JsonConvertible / ValueConvertible impls but no literal wire-shape tests. Wire-shape pinning is the central guarantee this PR introduces — the variant matrix should be complete. Add JSON and Value fixture assertions for Perpetual and for each variant of TokenDistributionTypeWithResolvedRecipient so a future tag, field, or payload-shape change fails loudly.

suggestion: ArrayItemType conversion tests miss Boolean and Date variants

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

Verified at head: ArrayItemType has seven variants but the new canonical conversion tests in json_convertible_tests / value_convertible_tests only pin Integer, Number, String, ByteArray, and Identifier (lines 721-841). Boolean and Date are uncovered in the canonical round-trip tests, even though Date has dedicated conversion behavior in this file (e.g. lines 168, 229, 279). Add JSON and Value wire-shape assertions for Boolean and Date so a regression in their encoding (e.g. ms-since-epoch as integer vs. string) is caught at unit-test time rather than at runtime in document validation.

suggestion: BLS public-key `visit_seq` collects unbounded input before rejecting it

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

Verified at head: visit_seq pushes every element from the input sequence into a Vec and only checks the exact 48-byte (COMPRESSED_G1_LEN) length inside from_compressed_g1_bytes(&bytes) after the sequence ends. A malformed JSON/Value payload with a very large array in a public-key slot forces unnecessary allocation and parsing before rejection. This is currently reached only from trusted Dash Core validator data, but the with = "..." pattern is designed for reuse, and the failure mode amplifies as soon as another type plugs this module into a less-trusted ingest path. Cap the loop at COMPRESSED_G1_LEN + 1 and error on overflow so the structural promise ("48-byte key") is enforced inside the visitor.

    fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
        let mut bytes = Vec::with_capacity(COMPRESSED_G1_LEN);
        while let Some(b) = seq.next_element::<u8>()? {
            if bytes.len() == COMPRESSED_G1_LEN {
                return Err(serde::de::Error::invalid_length(
                    bytes.len() + 1,
                    &"a 48-byte compressed BLS G1 public key",
                ));
            }
            bytes.push(b);
        }
        from_compressed_g1_bytes(&bytes)
    }
suggestion: Unification plan still documents the removed `_validated` API and the wrong serde default

docs/json-value-unification-plan.md (line 840)

Verified at head: this section still says canonical serde_json::from_value::<DataContract>(...) is the no-validation default (lines 840, 853), that the trust-boundary API is from_json_validated(json, &pv) / from_value_validated(value, &pv) with no bool param (line 841, 849), and that from_*_versioned(_, full_validation, _) was deleted entirely (line 843). The latest delta in this PR (commit 24ade51) removes the _validated helpers and routes everything through from_*(value, true, &pv), and the prior commit 6e5d568 restored validate-by-default in conversion/serde/mod.rs:74-79 (hardcoded true) with a comment explicitly pointing readers to from_json/from_value(_, false, _) for the no-validation path. The plan now contradicts the executable API in every direction (default behavior, helper name, bool param). Since the PR description asks reviewers to read this plan for architectural intent, update §10 to reflect the validate-by-default default and the single from_*(value, full_validation, &pv) surface.

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

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

…eger

`ContestedIndexFieldMatch`'s `PositiveIntegerMatch(u128)` carried a hand-rolled
`positive_integer_match_value` serde module inline in `document_type/index`,
which re-declared the `JS_MAX_SAFE_INTEGER` constant and duplicated the JS-safety
logic that already lives in `serialization/json/safe_integer.rs`.

Move it next to its siblings as `json_safe_u128_content` (reusing the shared
`JS_MAX_SAFE_INTEGER`) and reference it via
`#[serde(with = "crate::serialization::json_safe_u128_content")]`. Behavior is
unchanged — it stays distinct from `json_safe_u128` because it must never emit
`serialize_u128` (serde's `Content` buffer, used by internal `$type` tagging,
can't hold a 128-bit integer). Now it's discoverable for the next `$type` enum
that needs a u128 field, and the constant duplication is gone.

ContestedIndexFieldMatch round-trip tests: 37 passed, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <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

Incremental + cumulative review against d33f72f. The latest delta (24ade51..d33f72f) is a pure refactor that hoists a Content-safe u128 serde helper into rs-dpp's serialization::json::safe_integer module — no new defect introduced. All 10 coordinator-supplied prior findings re-verify as STILL VALID against this head; none are FIXED, OUTDATED, or INTENTIONALLY DEFERRED. Codex additionally surfaces 4 valid new wasm-dpp2 TypeScript-declaration discriminator mismatches (Voting, AddressWitness, TokenEvent, GroupActionEvent) of the same class as prior-2 / prior-3, consolidated into one new blocking finding to stay within the 10-item budget. Net: 4 blocking + 6 suggestion findings; review_action = REQUEST_CHANGES because in-scope blocking issues remain.

Reviewed commit: d33f72f

🔴 4 blocking | 🟡 6 suggestion(s)

Verified findings

blocking: Committed local runbook exposes live validator infrastructure

docs/v12-upgrade-boundary-risks.md (line 119)

STILL VALID at d33f72f. The section's own header is Runbook (local only — contains infra details, do not commit), yet it is checked in. It publishes a live testnet HPMN validator (hp-masternode-8 = 68.67.122.8, ansible_user ubuntu, explicitly flagged as a live validator), a second mainnet-synced host IP (54.185.219.212), the operator SSH key path (~/.ssh/evo-app-deploy.rsa), and concrete ssh -i ~/.ssh/evo-app-deploy.rsa ubuntu@<host> / scp invocations. Publishing a live validator hostname/IP along with the operator account name and the path of the operator's private key materially lowers attacker effort against that node and contradicts the file's own warning. Remove the section from the PR (or replace it with an instructions-only, host-redacted summary); the operational details belong in a private operations doc, not a public-repo docs/ tree.

blocking: AssetLockProof TypeScript declarations advertise `type` while rs-dpp uses `$type`

packages/wasm-dpp2/src/asset_lock_proof/proof.rs (line 18)

STILL VALID at d33f72f. The typescript_custom_section declares AssetLockProofObject / AssetLockProofJSON with a plain type discriminator, and the surrounding doc comment claims rs-dpp uses #[serde(tag = "type")]. Both are wrong: packages/rs-dpp/src/identity/state_transition/asset_lock_proof/mod.rs:38,49 use #[serde(tag = "$type", rename_all = "camelCase")]. JS callers built against these TS types will produce { type: "instant" | "chain", ... }, which rs-dpp's internally-tagged deserializer rejects with missing $type; conversely toObject() / toJSON() emit $type and so violate the declared TS shape. Update the typescript_custom_section and doc comment to $type.

#[wasm_bindgen(typescript_custom_section)]
const TS_TYPES: &str = r#"
/**
 * AssetLockProof serialized as a plain object.
 *
 * Internally-tagged discriminated union — `$type` discriminates the variant and
 * the variant's fields sit alongside it. Mirrors the rs-dpp serde shape (which
 * uses `#[serde(tag = "$type")]` on the enum) and the convention used by
 * `AddressWitness` / `AddressFundsFeeStrategyStep`.
 */
export type AssetLockProofObject =
    | ({ $type: "instant" } & InstantAssetLockProofObject)
    | ({ $type: "chain" } & ChainAssetLockProofObject);

/**
 * AssetLockProof serialized as JSON.
 */
export type AssetLockProofJSON =
    | ({ $type: "instant" } & InstantAssetLockProofJSON)
    | ({ $type: "chain" } & ChainAssetLockProofJSON);
"#;
blocking: FeeStrategyStep TypeScript declarations use `type` while rs-dpp emits `$type`

packages/wasm-dpp2/src/platform_address/fee_strategy.rs (line 8)

STILL VALID at d33f72f. FeeStrategyStepObject and FeeStrategyStepJSON declare { type: "deductFromInput" | "reduceOutput"; index }, and the doc comment at lines 38–40 reinforces the wrong shape. Runtime serde in packages/rs-dpp/src/address_funds/fee_strategy/mod.rs:42,46,81,99,115 writes $type and only accepts $type (returning missing_field("$type") otherwise). Because FeeStrategyStepWasm is #[serde(transparent)] over AddressFundsFeeStrategyStep, any object built against the declared TS contract fails deserialization on the rs-dpp side, and any object produced by toObject() / toJSON() carries $type, breaking JS consumers narrowing on .type. Align the TS surface (and doc comment) with $type.

#[wasm_bindgen(typescript_custom_section)]
const FEE_STRATEGY_STEP_TS_TYPES: &str = r#"
/**
 * Fee strategy step in Object form (output of a transition's `toObject()`).
 *
 * Discriminated by `$type`: "deductFromInput" reduces an input's contribution
 * by the fee, "reduceOutput" reduces an output's amount by the fee. The
 * `index` selects the input/output position.
 */
export type FeeStrategyStepObject =
    | { $type: "deductFromInput"; index: number }
    | { $type: "reduceOutput"; index: number };

/**
 * Fee strategy step in JSON form (output of a transition's `toJSON()`).
 *
 * Identical shape to `FeeStrategyStepObject` because the only payload is a
 * small `index` (u16) which serializes the same way in both binary and
 * human-readable formats.
 */
export type FeeStrategyStepJSON =
    | { $type: "deductFromInput"; index: number }
    | { $type: "reduceOutput"; index: number };
"#;
blocking: Additional wasm-dpp2 TS surfaces use `type`/`kind` while runtime emits `$type`/`$kind`

packages/wasm-dpp2/src/voting/vote_poll.rs (line 22)

Same root cause as the AssetLockProof and FeeStrategyStep findings — surfaced cumulatively across wasm-dpp2 by this PR's audit pass. Four additional TypeScript declaration blocks still advertise a plain discriminator key while the rs-dpp serializers/deserializers emit and require the $-prefixed key:

  • packages/wasm-dpp2/src/voting/vote_poll.rs:38,50 declares type: "contestedDocumentResourceVotePoll", but packages/rs-dpp/src/voting/vote_polls/mod.rs:29 is #[serde(tag = "$type", rename_all = "camelCase")].
  • packages/wasm-dpp2/src/shielded/address_witness.rs:17,25,39,47 declares type: "p2pkh" | "p2sh", but packages/rs-dpp/src/address_funds/witness.rs:32 is #[serde(tag = "$type")].
  • packages/wasm-dpp2/src/group/token_event.rs:33,42 declares type: string, but packages/rs-dpp/src/tokens/token_event.rs:194-271 emits every variant as serialize_entry("$type", ...) and :329,352 only accepts $type (the in-file comment at :67 confirms the convention).
  • packages/wasm-dpp2/src/group/action_event.rs:16,21 declares kind: "tokenEvent", but packages/rs-dpp/src/group/action_event.rs:25 is #[serde(tag = "$kind", rename_all = "camelCase")]. The doubly-tagged GroupActionEvent compounds the issue because its kind mismatch sits on top of the TokenEvent type mismatch.

All four are public TypeScript surfaces; consumers following them either fail deserialization on rs-dpp ingest or get values that don't match the declared shape. Update each typescript_custom_section to use $type (and $kind for GroupActionEvent), matching the convention already followed elsewhere in wasm-dpp2.

suggestion: BLS public-key `visit_seq` collects unbounded input before rejecting it

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

STILL VALID at d33f72f. visit_seq allocates with capacity 48 and then while let Some(b) = seq.next_element::<u8>()? { bytes.push(b); }, only enforcing the 48-byte requirement after the sequence ends inside from_compressed_g1_bytes. A hostile JSON or platform_value payload with a multi-megabyte byte sequence forces proportional allocation/parsing on every BLS-pubkey deserialize path (validator-set/state-transition ingest, wasm input, FFI JSON) before being rejected. Cap the loop at 48 elements and error out on the 49th — pure DoS hardening with no consensus impact.

    fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
        let mut bytes = Vec::with_capacity(COMPRESSED_G1_LEN);
        while let Some(b) = seq.next_element::<u8>()? {
            if bytes.len() == COMPRESSED_G1_LEN {
                return Err(serde::de::Error::invalid_length(
                    bytes.len() + 1,
                    &"a compressed G1 BLS public key of exactly 48 bytes",
                ));
            }
            bytes.push(b);
        }
        from_compressed_g1_bytes(&bytes)
    }
suggestion: Unification plan still documents the removed `_validated` API and the wrong serde default

docs/json-value-unification-plan.md (line 840)

STILL VALID at d33f72f. This section presents the final shape of the DataContract conversion API as: canonical serde_json::from_value::<DataContract>(...) = no validation (line 840), opt-in DataContract::from_json_validated(json, &pv) / from_value_validated(value, &pv) with no bool param (line 841), and 'Validation default flipped to no-validation' (line 853). All three claims are now false. Commit 6e5d5683d9 restored validate-by-default on Deserialize for DataContract, and 24ade51d34 dropped the _validated aliases in favor of from_json(value, full_validation: bool, &pv) / from_value(value, full_validation: bool, &pv) (verified at packages/rs-dpp/src/data_contract/conversion/json/mod.rs:11). The plan now misdocuments both the validation default and the public API surface in a load-bearing area — update it to match the as-shipped behavior so future readers/contributors don't follow the obsolete plan.

suggestion: TokenPricingSchedule JSON emits u64 prices as unsafe JS numbers

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

STILL VALID at d33f72f. TokenPricingSchedule derives plain Serialize/Deserialize, and its JsonConvertible / ValueConvertible impls are blanket no-ops. SinglePrice(Credits) and SetPrices(BTreeMap<TokenAmount, Credits>) both wrap u64, so any price above Number.MAX_SAFE_INTEGER round-trips through canonical JSON as a JS Number and silently loses precision in wasm-dpp2 consumers — exactly the JS-safety gap this PR closes elsewhere via json_safe_u64. Token pricing sits on the contract-author → end-user trust boundary; either apply json_safe_u64 via #[serde(with = ...)] on the variants/map values or document explicitly why this type is exempt and pin the expected wire shape in tests.

suggestion: C/Swift data-contract JSON no longer pins the SDK protocol version

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

STILL VALID at d33f72f. This FFI path calls serde_json::to_value(&contract). After the unification work DataContract's manual Serialize resolves the platform version internally via PlatformVersion::get_version_or_current_or_latest, so the JSON shape returned to C/Swift depends on process-global current/latest state rather than the SDK/network protocol version the caller is operating against. For a mobile SDK whose JSON output is expected to be reproducible across protocol-version rolls, this couples FFI output to an unrelated global. Route through an explicit version-pinned serializer (e.g. to_validating_json(&pv) or whichever versioned conversion the trait still exposes), using the SDK's configured platform version.

suggestion: Lenient update-transition constructor does not accept legacy `identityContractNonce`

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

STILL VALID at d33f72f. The constructor pre-fills $formatVersion, userFeeIncrease, signaturePublicKeyId, and signature and then calls strict DataContractUpdateTransition::from_object(raw). The V0 transition's identity-contract-nonce field is serde(rename = "$identity-contract-nonce") (packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v0/mod.rs:31-36), but the lenient constructor does not alias the previous JS public name identityContractNonce to that canonical key. Legacy JS clients constructing the transition with the old field name will silently land with the nonce missing/defaulted on the strict path — and the nonce is the replay-protection primitive. Either alias the legacy key before strict deserialization or detect-and-error so callers update.

suggestion: Canonical conversion tests miss enum variants (ArrayItemType: Boolean/Date; TokenDistributionInfo: Perpetual)

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

STILL VALID at d33f72f. Two adjacent canonical-conversion test modules don't pin every variant of their enum:

  • packages/rs-dpp/src/data_contract/document_type/property/array.rs:706-842 covers Integer, Number, String, ByteArray, Identifier — but ArrayItemType also has Boolean and Date (defined at lines 15-16). A wire-shape regression in those two variants (internal-tag rename, accidental adjacent tagging) would not be caught.
  • packages/rs-dpp/src/data_contract/associated_token/token_distribution_key.rs:322-389 only fixtures TokenDistributionInfo::PreProgrammed, leaving Perpetual and the TokenDistributionTypeWithResolvedRecipient branches unpinned for both JSON and Value round-trip / wire-shape assertions.

Add a fixture + round-trip + literal wire-shape assertion for each missing variant, following the {"$type": ...} pattern already used in these modules. This PR's stated goal is to pin canonical wire shapes; finishing the variant coverage is required for that pinning to hold.

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

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

@shumkov shumkov modified the milestones: v4.0.0, v4.1.0 Jul 2, 2026
@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants