Repository navigation
fix(ICP_Rosetta): DEFI-3050: bound the number of signing payloads in /construction/payloads - #12082
mbjorkqvist wants to merge 4 commits into
Conversation
…/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>
… payloads Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟢 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
|
✅ No security or compliance issues detected. Reviewed everything up to 7603fbd. Security OverviewDetected Code Changes
|
|
|
||
| /// Validate the number of (request, ingress expiry) pairs before any signing | ||
| /// payloads are built for them. | ||
| fn validate_request_ingress_expiry_pairs( |
There was a problem hiding this comment.
🤖 /construction/combine needs this bound too. It clones each update of the client-supplied unsigned transaction once per ingress expiry into the signed transaction.
There was a problem hiding this comment.
🤖 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())?; |
There was a problem hiding this comment.
🤖 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.
There was a problem hiding this comment.
🤖 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 |
There was a problem hiding this comment.
🤖 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.
- 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>
| unsigned_transaction.ingress_expiries.len(), | ||
| )?; | ||
| for (_, update) in &unsigned_transaction.updates { | ||
| validate_update_arg_size(update)?; |
There was a problem hiding this comment.
🤖 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.
There was a problem hiding this comment.
🤖 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>
There was a problem hiding this comment.
🔵 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 resolved since last review
Review effort: Balanced

Why
/construction/payloadsbuilds 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
/construction/payloadsrequests 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.🤖 Generated with Claude Code