Commit 56fe09d
fix(hashes): make SerdeHash tolerant of ContentDeserializer's HR-quirk (#729)
* fix(hashes): make SerdeHash tolerant of ContentDeserializer's HR-quirk
This is the same kind of serde-tag incompatibility fixed for `OutPoint`
in #708, applied to the hash-newtype family (sha256, sha256d, hash160,
hash_x11, ripemd160, sha1, sha512 — affecting Txid, BlockHash, ProTxHash,
PubkeyHash, QuorumHash, and every other type generated by `hash_newtype!`
or `serde_impl!`).
`SerdeHash::deserialize` used two separate visitors — a string-only HR
visitor (`HexVisitor`) and a bytes-only non-HR visitor (`BytesVisitor`).
That works fine in isolation but breaks the moment a hash-bearing struct
is wrapped by an internally-tagged enum (`#[serde(tag = "...")]`),
`flatten`, or an untagged enum. Serde routes those through
`ContentDeserializer`, a format-agnostic intermediate buffer that always
reports `is_human_readable() == true` regardless of the upstream format.
A value originally written by a non-HR encoder is therefore replayed
into the HR branch as raw bytes, which the previous `HexVisitor::visit_str`
saw as "32 chars" instead of "64-char hex" and rejected with
`bad hex string length 32 (expected 64)`.
This was hit downstream in dashpay/platform when validators / validator
sets (which contain `ProTxHash`, `PubkeyHash`, `QuorumHash`) were
configured for the dpp `tag = "$formatVersion"` versioning convention.
## Fix
Rework `SerdeHash::deserialize` to use a single `AnyShapeVisitor` that
accepts every shape a hash can arrive in:
- `visit_str` / `visit_borrowed_str` — ASCII hex (canonical HR form).
- `visit_bytes` / `visit_borrowed_bytes` — disambiguated by length:
exactly `N` bytes → raw hash, exactly `2*N` bytes → UTF-8 hex.
Any other length is rejected.
- `visit_seq` — length-prefixed `u8` sequence (used by bincode and
other non-self-describing formats).
Use `deserialize_any` in the HR branch so the actual content shape —
not the reported HR flag — drives dispatch. Keep `deserialize_bytes`
in the non-HR branch since bincode is non-self-describing and does not
support `deserialize_any`.
## Trade-off
Raw JSON now also accepts the byte-form (`"\x11..."` UTF-8 bytes vs.
`"11..."` hex string) because `deserialize_any` in serde_json's
self-describing mode dispatches based on the JSON token. We disambiguate
strictly by length in `visit_bytes`, so anything that's neither N bytes
nor 2*N bytes still errors. This is consistent with the OutPoint fix
in #708 — accept any shape, validate by length.
## Implementation note: no_std / no alloc
`dashcore_hashes` does not enable `serde/alloc` (it has only `serde-std`
which transitively gates that), so `Visitor::visit_byte_buf` and
`visit_string` (defined behind serde's `alloc` feature) are unavailable.
The `visit_seq` path uses a stack array sized to fit the largest hash
(64 bytes — sha512) instead of a `Vec`, keeping the crate's no-alloc
posture.
## Tests
Two new regression tests in `dash/src/hash_types.rs`:
- `serde_round_trip_through_internally_tagged_enum` — wraps a `Txid`
in a `#[serde(tag = "type")]` enum, round-trips through
`serde_json::Value` (which forces buffering through
`ContentDeserializer`), and asserts the round-trip is identity. Also
verifies the canonical hex-string form still deserializes and that
bincode round-trip still succeeds via the byte/seq path.
- `serde_round_trip_through_internally_tagged_enum_pubkey_hash` —
same shape with `PubkeyHash` (20-byte hash) to exercise the
smaller-length disambiguation path.
`bincode` dev-dep updated to `features = ["serde"]` (same change as #708)
so the bincode regression assertion compiles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(test): rustfmt + correct comment about N being in bytes
* review: address PR feedback — real visit_seq test + debug_assert overflow
Two in-scope fixes from review:
1. The Txid round-trip test had an abandoned `raw_txid_bytes` literal
followed by `let _ = raw_txid_bytes; // documentation only` — leftover
exploration that misled readers into thinking the bytes were used.
Replace with a real assertion that constructs a `serde_json::Value::Array`
of u8 numbers, wraps it in a `#[serde(tag = "type")]` enum, and
round-trips through `serde_json::from_value`. This now actually
exercises the new `visit_seq` path through `ContentDeserializer` —
the security review noted that the prior test only hit `visit_str`,
leaving `visit_bytes`/`visit_seq` regression coverage thin.
2. The `MAX_HASH_BYTES = 64` overflow check in `visit_seq` was returning
a runtime error with a debug-prose string ("recompile with larger
MAX") that leaked an internal type name to user error logs. Convert
to `debug_assert!` — failure mode is now a test panic in debug builds
(caught at CI time when adding a wider hash type), zero overhead in
release. The condition is unreachable in any release build that
compiled at all, since adding a wider digest would require updating
`serde_impl!` invocations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 976fbe1 commit 56fe09d
3 files changed
Lines changed: 217 additions & 32 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
67 | 67 | | |
68 | 68 | | |
69 | 69 | | |
70 | | - | |
| 70 | + | |
71 | 71 | | |
72 | 72 | | |
73 | 73 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
389 | 389 | | |
390 | 390 | | |
391 | 391 | | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
26 | | - | |
27 | 26 | | |
28 | 27 | | |
29 | | - | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
30 | 52 | | |
31 | | - | |
| 53 | + | |
32 | 54 | | |
33 | 55 | | |
34 | 56 | | |
35 | 57 | | |
36 | 58 | | |
37 | | - | |
| 59 | + | |
38 | 60 | | |
39 | 61 | | |
40 | | - | |
| 62 | + | |
41 | 63 | | |
42 | 64 | | |
43 | 65 | | |
44 | | - | |
45 | | - | |
46 | | - | |
47 | | - | |
48 | | - | |
| 66 | + | |
49 | 67 | | |
50 | 68 | | |
51 | | - | |
| 69 | + | |
52 | 70 | | |
53 | 71 | | |
54 | 72 | | |
55 | 73 | | |
56 | 74 | | |
57 | | - | |
58 | | - | |
59 | | - | |
60 | | - | |
61 | | - | |
62 | | - | |
63 | | - | |
64 | | - | |
65 | | - | |
66 | | - | |
67 | 75 | | |
68 | | - | |
69 | | - | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
70 | 99 | | |
71 | 100 | | |
72 | | - | |
| 101 | + | |
73 | 102 | | |
74 | 103 | | |
75 | 104 | | |
76 | | - | |
77 | | - | |
78 | | - | |
79 | | - | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
80 | 140 | | |
81 | 141 | | |
82 | 142 | | |
| |||
90 | 150 | | |
91 | 151 | | |
92 | 152 | | |
93 | | - | |
| 153 | + | |
94 | 154 | | |
95 | 155 | | |
96 | 156 | | |
| |||
106 | 166 | | |
107 | 167 | | |
108 | 168 | | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
109 | 180 | | |
110 | 181 | | |
111 | | - | |
| 182 | + | |
112 | 183 | | |
113 | | - | |
| 184 | + | |
114 | 185 | | |
115 | 186 | | |
116 | 187 | | |
| |||
0 commit comments