fix: Bound tx validation and add spawn blocking calls - #2488
Conversation
kkovaacs
left a comment
There was a problem hiding this comment.
Looks good to me % a small nit.
Mirko-von-Leipzig
left a comment
There was a problem hiding this comment.
Thank you; good findings. I initially thought it would be database write lock contention.
| (signed_block, bytes) | ||
| }) | ||
| .await | ||
| .unwrap_or_else(|e| std::panic::resume_unwind(e.into_panic())); |
There was a problem hiding this comment.
@kkovaacs a fairly common pattern - should we make this part of the spawn function itself?
There was a problem hiding this comment.
This is one of the points where I wish we had more powerful tracing. It would be sensible to always have decode, encode and handle instrumented by the trait itself.
However that is not helpful because our macros need introspection of the function body to work properly.
Nothing actionable; just a complaint into the void :)
There was a problem hiding this comment.
Perhaps for a different PR, but I think this exhibits my point that most of our RPC work is actually not async, but rather blocking.
Our database calls are actually sync under the hood; we're just twisting ourselves into pretzels jumping between sync and async multiple times.
07d2747 to
897f5dc
Compare
Summary
Under load, the validator's
SignBlockRPC was spiking from ~30ms to ~300ms, with the tracing added in #2487 showing the time spread uniformly across the request rather than concentrated in any one span — the signature of CPU starvation, not lock contention. Each submitted transaction runs proof verification and full VM re-execution on unbounded blocking threads, oversubscribing every core, while the Golden seal (secp256k1 ops) and the CPU-heavy parts of block signing ran directly on async workers.The two paths have asymmetric failure modes: a delayed block signature stalls the whole chain, while a delayed transaction validation only slows admission — the RPC submits every transaction to every validator before it may enter the mempool, so blocks are proposed exclusively from already-validated transactions and signing never waits on validation. Signing must therefore always have CPU headroom; validation is the correct place to apply backpressure.
Changes:
SubmitProvenTransactionnow acquires a permit from a semaphore sized toavailable_parallelism - 2(minimum 1) before validating, converting CPU oversubscription into visible queueing. The permit is acquired after the already-validated short-circuit, so duplicate submissions never wait. The wait is instrumented as anacquire_validation_permitspan.into_header_and_body(account/nullifier/note tree roots plus chain and transaction commitments — hashing proportional to block contents), and full-block serialization now run viaspawn_blocking_in_current_span, with panics propagated viaresume_unwind(same convention astx_validation).decodespan:SignBlock::decodenow timesProposedBlockdeserialization, closing the last un-instrumented sync segment of the request path.Verified against a local multi-validator run: the
rpcspan's self-time drops to ~0% (fully attributed), the offloaded work still reports undervalidate_block(the helper carries the span across the thread hop), and the uncontendedacquire_validation_permitadds no measurable latency. Behavior note: under sustained validation overload, submissions now queue (and eventually hit the request timeout) instead of silently degrading block signing — backpressure lands on submitters, which already handle it.Changelog