Skip to content

Serve the SSZ validation routes from the simulation role - #543

Open
0w3n-d wants to merge 1 commit into
od/builder-sim-blobs-step8from
od/builder-sim-servers-step9
Open

Serve the SSZ validation routes from the simulation role#543
0w3n-d wants to merge 1 commit into
od/builder-sim-blobs-step8from
od/builder-sim-servers-step9

Conversation

@0w3n-d

@0w3n-d 0w3n-d commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Issue: #527 (step 9 of 10)

Base branch: od/builder-sim-blobs-step8 (#536, step 8). Retarget as the stack
merges.

What this PR does

The simulation role now serves traffic. /validate and /validate_merged
accept the relay's SszValidationRequest and SszMergedValidationRequest
unchanged, decode the submission through helix-common's decoder, and answer:

  • 200 when the block validates;
  • 400 with the reason, which is what the relay classifies on;
  • 424 for a dehydrated submission, so the relay retries with full SSZ bytes
    rather than treating it as a validation failure;
  • 500 if a validation task panics.

Validation is synchronous CPU work, so it runs on a blocking thread under a
semaphore sized by max_concurrent_validations. The disallow list refreshes
from blacklist_endpoint every five minutes.

main builds the BlockValidator and spawns both, so the module no longer
carries allow(dead_code).

What this PR deliberately does not do

No JSON-RPC. Only the SSZ routes are served, so the relay must have
ssz_url set for this simulator. The JSON path would need a second conversion
from JsonValidationRequest's helix-types payload, and SSZ is the path the
relay actually prefers. rpc_addr is therefore removed from
SimulationConfig — an unused config field is worse than an absent one.
Serving JSON-RPC can be a follow-up if a deployment needs it.

The 424 path is untested. DehydratedBidSubmissionFulu has private fields
and no test constructor, so a dehydrated submission cannot be built from this
crate. The arm is two lines and mirrors the reth simulator; testing it needs a
test-only constructor in helix-types, which I left out of this step.

Things worth knowing

  • A 128 KiB blob overflows a debug build's stack. In debug the copies are
    not elided and tokio's 2 MiB worker stack overflows; release passes. The blob
    wire-format test therefore runs on a 32 MiB thread, and eblobs fills its
    vector in place rather than collecting through an iterator. Production runs
    release, so this is not a runtime concern, but a debug_assertions build of
    this role would need a larger stack.
  • A failed blacklist fetch means no filtering. The list starts empty, so if
    blacklist_endpoint is unreachable at boot, regional-filtering proposers get
    unfiltered blocks until a fetch succeeds. The reth simulator behaves the same
    way, so this is not a regression, but it is worth knowing operationally.

Tests

Written first and signed off before implementation. 9 new, 89 in the crate:

  • A valid submission is accepted; an underpaid one gives 400 with a reason the
    relay can read.
  • The merged route accepts a payment split across base_payment_tx_index and
    the last transaction.
  • The request's apply_blacklist decides the answer: the same block is rejected
    with it set and accepted without, which pins the flag's plumbing end to end.
  • A malformed body gives 400.
  • A real blobs bundle survives the round trip through the wire format.
  • The disallow refresh replaces stale entries and reports a new digest, and an
    unchanged list reports nothing.

Smoke-tested on the binary: it binds 0.0.0.0:8552, logs Validation server listening, and the refresh task runs on the first tick.

just fmt-check, just test and cargo clippy --all-features --no-deps -- -D warnings
are clean.

Reviewer checklist

  • CI (lint, unit-test) is green
  • Matches the linked issue/step
  • No unexplained scope creep or unrelated files touched

/validate and /validate_merged decode the relay's SSZ requests, run the
validator on a blocking thread under a semaphore, and answer 200 or 400
with the reason. A dehydrated submission answers 424 so the relay retries
with full bytes. The disallow list refreshes from the configured endpoint.

main now builds the validator and spawns both, so the module no longer
needs allow(dead_code).

Drops rpc_addr: no JSON-RPC is served, and an unused config field is a
trap. The relay reaches this role through its simulator ssz_url.

Step 9 of #527.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant