Skip to content

refactor: add bounded vector deserialization#7439

Merged
PastaPastaPasta merged 1 commit into
dashpay:developfrom
thepastaclaw:serialize/limited-vector
Jul 10, 2026
Merged

refactor: add bounded vector deserialization#7439
PastaPastaPasta merged 1 commit into
dashpay:developfrom
thepastaclaw:serialize/limited-vector

Conversation

@thepastaclaw

@thepastaclaw thepastaclaw commented Jul 10, 2026

Copy link
Copy Markdown

Issue being fixed or feature implemented

Network messages often have protocol-specific vector limits below the generic serialization limit. Callers need to enforce those limits before vector allocation and element decoding, without changing the wire format.

What was done?

  • Factor the existing batched vector element decoder into a shared internal helper.
  • Add UnserializeVectorWithMaxSize for runtime bounds.
  • Add LIMITED_VECTOR / LimitedVectorFormatter for compile-time bounds in READWRITE declarations.
  • Keep serialization byte-for-byte compatible with ordinary vectors; only deserialization is bounded.
  • Compare CompactSize counts before narrowing or allocating, including counts at and above MAX_SIZE.

Stacked adopters

Each consumer remains a separate command-specific PR:

Reviewing #7439 first leaves each child PR with only its protocol-specific policy, punishment, and regression tests.

How Has This Been Tested?

  • src/test/test_dash --run_test=serialize_tests
  • Exact and over-limit boundaries, zero limits, custom element formatters, MAX_SIZE and MAX_SIZE + 1 declarations, 64-bit CompactSize counts, wire compatibility, and rejection before element decode are covered.

Breaking Changes

None. Existing vector serialization and deserialization behavior is unchanged.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation
  • I have assigned this pull request to a milestone

@kwvg kwvg 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.

Consider pr_7416 (especially f80422c 9cfe87c and 6b47508 split out for review, should be squashed in final iteration)

@thepastaclaw

Copy link
Copy Markdown
Author

Addressed in bf865ac. I ported the substance of f80422c and 6b47508 onto this branch: restored the normal CompactSize range check, documented the unrelated serialization-exception behavior, and updated the focused boundary tests. The changes are squashed into the existing single PR commit as requested; unrelated commits from pr_7416 were not brought over.

Validation: make -j15 -C src test/test_dash, ./src/test/test_dash --run_test=serialize_tests, the focused unserialize_vector_with_max_size case, and git diff --check all pass.

Introduce a runtime and a compile-time bounded reader for CompactSize-prefixed
vectors so downstream call sites can reject grossly oversized wire counts
before any element decode or allocation. Both primitives preserve the ordinary
std::vector wire format — the limit is a deserialization safety property.

- Factor the batched vector element-decode loop out of VectorFormatter::Unser
  into detail::UnserializeVectorContents<Formatter>, and reuse it from both
  new primitives so the DoS-resistant 5 MiB batching is not duplicated.

- UnserializeVectorWithMaxSize<Formatter>(stream, vec, max_size) reads the
  CompactSize prefix and rejects a count above the caller's max_size before
  any element decode, returning false without consuming an element. A count
  above the generic MAX_SIZE cap throws from ReadCompactSize as malformed;
  since a realistic max_size is far below MAX_SIZE (you reach
  MAX_PROTOCOL_MESSAGE_LENGTH long before MAX_SIZE), the caller's own limit is
  the gate that fires in practice. The helper may otherwise throw on an
  unrelated serialization failure.

- LimitedVectorFormatter<Limit, ElemFormatter=DefaultFormatter> is the
  READWRITE-friendly wrapper; a LIMITED_VECTOR(obj, n) macro mirrors the
  existing LIMITED_STRING convention. Ser delegates to VectorFormatter so
  the write side stays unbounded and interoperable with the standard reader.

Cover the primitives with focused unit tests: within/exact/over/zero-limit
boundaries, wire-format identity vs the ordinary vector encoding, that Ser
remains unbounded, and — via a CountingFormatter that tallies Unser calls —
that a MAX_SIZE wire count is rejected before any element decode occurs, while
counts above MAX_SIZE throw at the CompactSize level.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@thepastaclaw thepastaclaw force-pushed the serialize/limited-vector branch from bf865ac to af6a0fd Compare July 10, 2026 13:41
@thepastaclaw

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The serialization framework now shares batched vector allocation and element decoding through a helper. It adds runtime maximum-size decoding and a compile-time LimitedVectorFormatter, exposed through the LIMITED_VECTOR macro. Oversized vectors are rejected before element decoding, while serialization retains the standard vector wire format. Tests cover size boundaries, malformed prefixes, failure behavior, and compatibility with unbounded vector readers.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant LimitedVectorFormatter
  participant Stream
  participant UnserializeVectorContents
  participant ElemFormatter
  Caller->>LimitedVectorFormatter: unserialize bounded vector
  LimitedVectorFormatter->>Stream: read CompactSize element count
  Stream-->>LimitedVectorFormatter: element count
  alt count <= Limit
    LimitedVectorFormatter->>UnserializeVectorContents: decode vector elements
    UnserializeVectorContents->>ElemFormatter: unserialize each element
    ElemFormatter-->>UnserializeVectorContents: decoded element
    UnserializeVectorContents-->>LimitedVectorFormatter: populated vector
  else count > Limit
    LimitedVectorFormatter-->>Caller: throw length-limit failure
  end
Loading

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed It clearly summarizes the main change: adding bounded vector deserialization.
Description check ✅ Passed It directly describes the implemented bounded vector deserialization features.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@PastaPastaPasta PastaPastaPasta marked this pull request as ready for review July 10, 2026 15:23
@thepastaclaw

thepastaclaw commented Jul 10, 2026

Copy link
Copy Markdown
Author

✅ Review complete (commit af6a0fd)

@knst knst 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.

LGTM af6a0fd

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Code Review

Clean, well-scoped serialization refactor adding bounded-vector deserialization primitives with byte-identical wire format and thorough unit coverage. No blocking issues: the count is validated before any allocation, the Ser side stays unbounded for wire compatibility, and CompactSize range handling is correct. One legitimate but practically-inert nitpick about formatter construction-order reordering in the refactor.

💬 1 nitpick(s)

Source: reviewers opus (general, dash-core-commit-history) and gpt-5.5 (general, dash-core-commit-history); verifier opus.

Comment thread src/serialize.h
formatter.Unser(s, v.back());
}
}
detail::UnserializeVectorContents<Formatter>(s, v, ReadCompactSize(s));

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

💬 Nitpick: Refactor reorders Formatter construction relative to clear/count-read

The pre-refactor VectorFormatter::Unser default-constructed Formatter formatter; before calling v.clear() and ReadCompactSize(s). The shared helper now constructs the Formatter inside detail::UnserializeVectorContents, after the vector is cleared and the count has been consumed from the stream. For a Formatter whose default constructor has side effects or can throw, a construction failure would now leave the stream advanced past the count and the destination vector cleared — an observable difference in a change advertised as behavior-preserving. This is inert for every formatter in the tree today (all are stateless empty structs with trivial default constructors), so it is only worth noting for the refactor's stated invariant, not a defect. If you want to preserve the exact original ordering, construct the Formatter in VectorFormatter::Unser and pass it into the shared helper by reference.

source: ['codex']

@PastaPastaPasta PastaPastaPasta merged commit 9474fc5 into dashpay:develop Jul 10, 2026
47 checks passed
@UdjinM6 UdjinM6 added this to the 24 milestone Jul 10, 2026
PastaPastaPasta added a commit that referenced this pull request Jul 10, 2026
3cb73fd test: drop redundant spork_tests unit suite (PastaClaw)
44a8fe1 test: regression coverage for CSporkMessage signature-size bound (PastaClaw)
d2e600f fix: bound CSporkMessage signature vector allocation (PastaClaw)

Pull request description:

  Depends on #7439. Please review only the two SPORK-specific commits here.

  ## What changed

  - bound `CSporkMessage::vchSig` to `CPubKey::COMPACT_SIGNATURE_SIZE` before vector allocation using #7439s `LIMITED_VECTOR` formatter
  - punish peers that send malformed, truncated, or oversized SPORK messages
  - add unit coverage for the exact `CompactSize(MAX_SIZE)` trigger and a P2P disconnect regression test

  ## Why

  A malformed SPORK message could declare a large signature length in a small payload. Generic byte-vector deserialization would allocate its first 5,000,000-byte chunk before discovering the payload was truncated. The resulting exception was caught by the generic message-processing handler without penalizing the peer, allowing repeated attempts on the same connection.

  Valid SPORK signatures are 65-byte compact signatures on every network. The testnet distinction changes the signing preimage, not the signature encoding. Shorter well-formed signatures continue through the existing invalid-signature path and are punished there; the formatter prevents oversized declarations from allocating first.

  The reusable bounded-vector serialization primitives are introduced separately in #7439.

  ## Validation

  - `src/test/test_dash --run_test=spork_tests,serialize_tests`
  - `test/functional/feature_sporks.py`

  The exact unit and functional regressions were also run against the vulnerable implementation: the unit test observed the old EOF-after-allocation path, and the malformed P2P peer was not disconnected.

ACKs for top commit:
  PastaPastaPasta:
    utACK 3cb73fd
  kwvg:
    utACK 3cb73fd

Tree-SHA512: d547f589c3920baaacb121c618b812314901639c449bd562ea150de3a8743e3fbff10c1c92f2e192de776452d74ad7dc9be02622ddd29d9e5f7e43eacd966fd2
PastaPastaPasta added a commit that referenced this pull request Jul 10, 2026
a286e0e fix: bound quorum data response vectors (PastaClaw)

Pull request description:

  Depends on #7439. Please review only the final command-specific commit here.

  ## Issue being fixed or feature implemented

  Quorum-data responses carry verification-vector and encrypted-contribution vectors whose expected sizes are known from the requested quorum. This hardens QDATA processing by checking the serialized vector count before allocating, deserializing, or processing those vectors.

  This path is MNAuth/request-gated and is intentionally handled separately from unauthenticated public vector intake.

  ## What was done?

  - Read QDATA verification vectors with the requested quorum threshold as the maximum.
  - Read encrypted contributions with the number of valid quorum members as the maximum.
  - Require exact semantic counts and penalize mismatches before decrypt/aggregate work.
  - Extend functional coverage for undersized, oversized, and allocation-amplifying CompactSize declarations.

  The reusable bounded-vector serialization primitives are introduced separately in #7439.

  ## How Has This Been Tested?

  - `test/functional/p2p_quorum_data.py`

  ## Breaking Changes

  None.

  ## Checklist:
  - [x] I have performed a self-review of my own code
  - [x] I have commented my code, particularly in hard-to-understand areas
  - [x] I have added or updated relevant unit/integration/functional/e2e tests
  - [ ] I have made corresponding changes to the documentation
  - [ ] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)

ACKs for top commit:
  kwvg:
    Code near identical to past utACK (see [diff](https://github.com/dashpay/dash/compare/ecf49885bcbb40fd09bd81de95edf54d3996e2fe..a286e0e7eb4fd63eb65f05f5deefd5a2c4c43f55))

---

utACK a286e0e

Tree-SHA512: 1d217cff7096f0dbad736e98be91e6b4eac154ae7ca0512cb3200f448b3152485d89f3f13a5d36617d7e4122aac2ed521f79ccd6ca787c0e8fafb200cb3cf118
PastaPastaPasta added a commit that referenced this pull request Jul 10, 2026
8f0b813 fix(governance): bound vote signature deserialization (PastaClaw)

Pull request description:

  Uses the shared bounded-vector deserialization primitive merged in #7439.

  ## Motivation

  Governance vote signatures were deserialized through the generic byte-vector path. A peer could declare a very large signature length, causing allocation before the stream reported truncation. The outer message-processing catch did not score or disconnect the peer, allowing repeated malformed messages.

  ## Changes

  - bound network governance-vote signature reads to 96 bytes before allocation
  - require one of the two structurally valid encodings: 65-byte compact ECDSA or 96-byte BLS
  - score malformed or truncated governance vote messages with 100 misbehavior points
  - preserve disk, hash, and outbound serialization behavior
  - add focused unit coverage

  ## Testing

  - `./src/test/test_dash --run_test=governance_vote_wire_tests` (4/4 tests)
  - `./src/test/test_dash --run_test=serialize_tests` (10/10 tests)
  - `test/lint/lint-python.py`
  - `git diff --check upstream/develop...HEAD`

ACKs for top commit:
  knst:
    utACK 8f0b813

Tree-SHA512: 5cd804ffb410f47936615d230f5a5ebe4b2b4e1dd85455acb42df37fb65c0397f05c38704900a3ecf5d2729e157d03b298c4a7774b7764b3830cb57273a724cc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants