Skip to content

fix(ICP_Rosetta): DEFI-3050: bound the number of signing payloads in /construction/payloads - #12082

Open
mbjorkqvist wants to merge 4 commits into
masterfrom
mathias/DEFI-3050-bound-construction-payloads
Open

mbjorkqvist wants to merge 4 commits into
masterfrom
mathias/DEFI-3050-bound-construction-payloads

Conversation

@mbjorkqvist

@mbjorkqvist mbjorkqvist commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Why

/construction/payloads builds two signing payloads for every combination of operation-derived request and ingress expiry. With the 24h ingress window (up to 720 expiries) and a request body full of operations, a single call could produce tens of millions of payloads, tying up a worker for minutes and using a large amount of memory.

What

  • Reject /construction/payloads requests whose number of requests times number of ingress expiries exceeds a fixed limit (equivalent to 100 requests over the full 24h window), with an invalid-request error returned before any payloads are built. Realistic transfer and neuron workflows stay well within the limit.
  • Add tests for the limit boundary, for a request with the maximum number of STAKE operations, and for a realistic multi-operation neuron workflow over a 24h window.
  • Add a CHANGELOG entry.

🤖 Generated with Claude Code

…/construction/payloads

Reject /construction/payloads requests where the number of requests
multiplied by the number of ingress expiries exceeds a fixed limit,
before any signing payloads are built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the fix label Nov 12, 2026
… payloads

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

No active review issues are reported for the change limiting signing-payload generation.

No open issues 🎉

What changed in this PR

This PR limits signing-payload generation in ICP Rosetta’s /construction/payloads endpoint to prevent oversized requests from consuming excessive processing time and memory.

Changes:

  • Reject requests exceeding 72,000 request–ingress-expiry pairs before building signing payloads, using overflow-safe validation.
  • Add boundary, overflow, oversized STAKE request, and realistic neuron-workflow tests.
  • Document the limit in the changelog.
File Description
rs/​rosetta-api/​icp/​src/​request_handler/​tests.rs Adds oversized-request rejection and realistic-workflow acceptance tests.
rs/​rosetta-api/​icp/​src/​request_handler/​construction_payloads.rs Enforces the pair limit and tests boundary and overflow handling.
rs/​rosetta-api/​icp/​CHANGELOG.md Records the new construction-payload limit.

Review effort: Balanced

@mbjorkqvist
mbjorkqvist marked this pull request as ready for review November 12, 2026 18:37
@mbjorkqvist
mbjorkqvist requested a review from a team as a code owner November 12, 2026 18:37
@github-actions github-actions Bot added the @defi label Nov 12, 2026
@zeropath-ai

zeropath-ai Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

✅ No security or compliance issues detected. Reviewed everything up to 7603fbd.

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► rs/rosetta-api/icp/src/request_handler/construction_combine.rs
    Validate signature/public key sizes and ingress expiry ordering in /construction/combine
► rs/rosetta-api/icp/src/request_handler/construction_payloads.rs
    Add max constants for ingress expiry pairs and update size; add validation functions for request/ingress pairs and update size
► rs/rosetta-api/icp/src/request_handler/tests.rs
    Add tests for ingestion window and signing payload limits and various request scenarios
► rs/rosetta-api/icp/CHANGELOG.md
    Document fixed limit improvements to ingress expiries and update sizes


/// Validate the number of (request, ingress expiry) pairs before any signing
/// payloads are built for them.
fn validate_request_ingress_expiry_pairs(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 /construction/combine needs this bound too. It clones each update of the client-supplied unsigned transaction once per ingress expiry into the signed transaction.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Agreed. /construction/combine now checks the number of (update, ingress expiry) pairs before building any envelopes (d4ea5a5). Copilot then pointed out that repeating an ingress expiry lets one signature pair be reused for every pair, so in 7603fbd combine also bounds the size of each client-supplied update and of each signature and public key, and requires strictly increasing ingress expiries.

now += interval;
}

validate_request_ingress_expiry_pairs(transactions.len(), ingress_expiries.len())?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 This bounds the number of (request, expiry) pairs but not the size of each request. FOLLOW.followees is an unbounded Vec<u64>, and add_payloads clones the update and hashes its whole argument twice per expiry. I measured it on this branch: one FOLLOW with 2.09M followees (4.18 MB of JSON, under the 4 MiB body limit) counts as 720 pairs and passes this check, but construction_payloads takes 15.3 s, synchronously on an actix worker. Governance rejects more than MAX_FOLLOWEES_PER_TOPIC (15) followees anyway, so rejecting longer lists when parsing FollowMetadata would close this cheaply. A cap on the encoded argument before the expiry loop, like the one #12095 adds for ICRC, would also cover fields added later.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Good catch, thanks for measuring. I capped the size of each canister update at 4 KiB in add_payloads, before the expiry loop, and in /construction/combine (d4ea5a5 for the argument, 7603fbd for the whole CBOR-encoded update, so nonce and the other fields are covered too). That covers every request type and any fields added later, and leaves the number of followees to governance. The largest updates Rosetta builds are manage_neuron calls of about 2.3 KB, mostly the Candid type table; a FOLLOW with 15 followees is 2,331 bytes. A new test checks that the largest requests are still accepted over the maximum ingress window, and another checks that an oversized FOLLOW is rejected.

}
}

/// A request with as many STAKE operations as fit in a request body and the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 nit: 30,000 STAKE operations (~170 B of JSON each) exceed the 4 MiB body limit, so this isn't "as many as fit". The test still covers rejection; maybe just reword.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Right, reworded in d4ea5a5.

- Limit the size of each canister call argument to 4 KiB before it is
  cloned for every ingress expiry in /construction/payloads.
- Apply the same limits to the client-supplied unsigned transaction in
  /construction/combine.
- Reword the doc comment of the oversized STAKE request test.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Critical memory-exhaustion vulnerabilities remain because unbounded nonce and signature sizes are repeatedly allocated across expiries despite the existing guards.

2 open issues

Review effort: Balanced

unsigned_transaction.ingress_expiries.len(),
)?;
for (_, update) in &unsigned_transaction.updates {
validate_update_arg_size(update)?;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Valid, thanks. Fixed in 7603fbd: the 4 KiB limit now applies to the whole CBOR-encoded update rather than only its argument, in both /construction/payloads and /construction/combine. should_reject_combine_exceeding_limits covers an oversized nonce and repeated expiries.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 The remaining worst case of 72,000 pairs × 4 KiB doesn't apply to /construction/combine. Since 7603fbd the ingress expiries must be strictly increasing, so every (update, expiry) pair needs its own two signature entries, which are looked up by a hash over the whole update. With at least ~400 bytes of JSON per signature entry, the 4 MiB body allows at most ~10k signatures, i.e. ~5k pairs, and combine stops at the first missing signature. That keeps the retained envelopes around 25 MB. /construction/payloads doesn't retain a copy of the update per pair, only the two signing payloads, so its cost is the 72,000-pair budget with at most 4 KiB hashed per pair.

…onstruction/combine

- Limit the CBOR-encoded size of each canister update to 4 KiB instead of
  only its argument, so every variable-length field is bounded.
- Limit the size of signatures and public keys in /construction/combine.
- Require strictly increasing ingress expiries in /construction/combine,
  so a signature cannot be reused for many envelopes.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The cap still permits 72,000 pairs with 4-KiB updates, potentially retaining hundreds of MiB before serialization, so confirming safety requires production memory and worker-latency profiling unavailable in this review.

1 open issue
1 resolved since last review

Review effort: Balanced

This branch has not been deployed

No deployments
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.

3 participants