Per-slot action plan, slot outcome history & builder testing knobs#134
Merged
Conversation
The reversed nil-guard in Dispatcher.Unsubscribe made every call an early-return, leaking all subscriptions. Track removal purely via slice membership (the dispatcher field stays immutable after Subscribe), which also makes double-unsubscribe race-free. Add dispatcher tests incl. blocking/non-blocking delivery and concurrent subscribe/unsubscribe/fire.
…lues Adds five mutable settings (default < CLI < UI resolution like all others): - slot-result-retention-epochs (100): per-slot plan+result history window - slot-artifact-retention-epochs (100): raw SSZ artifact window - slot-artifact-capture-enabled (true): toggle raw artifact capture - epbs-bid-value-override (0=off): absolute p2p bid base, alternative to the subsidy formula; allows underbidding the block value - builder-api-value-override (0=off): absolute total served bid value Retention values reject 0 via settings validation.
Sparse per-slot operation modes for bidding, builder-api serving and reveal. Categories are explicit instructions: absent = inherit the global baseline, disabled = suppress, custom = force-active with optional overrides (custom activates the flow even when the module is globally disabled; availability gates still win). PlanService.Freeze(slot) resolves an immutable FrozenPlan (raw plan + effective settings merged from the live global config + target-slot fork) that every decision point of the slot observes; edits to frozen or past slots fail with ErrSlotLocked (409 at the API layer). Bulk updates are atomic all-or-nothing with three-state category patches (absent/null/ object, strict decoding) and overflow-safe range targeting. Plans persist via the kv_store "slot_plans" namespace; past plans are pruned to slot-result-retention-epochs on epoch transitions, future plans never.
Dedicated table for raw per-slot SSZ artifacts (built payloads, signed bids, signed envelopes) — deliberately not a kv_store namespace: blobs are too large for the in-RAM memstore pattern, and pruning needs a SQL range delete on the integer slot column. Batch inserts run in one transaction; MAX(idx) lookup makes per-slot bid index allocation restart-safe. Disabled-database no-op semantics match the other repositories.
…ority Freeze(slot) now resolves the COMPLETE build decision into the frozen snapshot (ResolvedBuildSettings): global schedule (all/every_nth/next_n, start slot), plan force/suppress, forced-build marking and the effective build start time — plan > global > defaults, in one place. The service owns the next_n accounting (OnSlotBuilt; forced builds exempt), UpdateConfig counter resets, and schedule stats. Consumers poll frozen snapshots instead of interpreting raw plans themselves.
The SlotManager is gone: the plan service is the scheduling authority. On payload_attributes the service freezes the slot's plan and acts on frozen.Build — decision, skip reason, and build start time all come from the snapshot; successful builds report back via planSvc.OnSlotBuilt. Plan-involved skips fire the new BuildSkippedEvent (deduped per slot) so the slot results tracker can explain unbuilt slots. planSvc is a mandatory constructor dependency; no fallback paths.
The scheduler freezes each slot's plan on first evaluation and the snapshot alone decides whether and how the slot is bid on: frozen windows/interval/amounts, absolute bid-value base (allows underbidding), optional proposer-preferences-gate bypass, and per-slot enable in both directions (a plan can activate bidding while ePBS is globally disabled and vice versa — the service enabled flag is status reporting only). The scheduler holds no config anymore; Service.UpdateConfig and the cfg constructor param are gone. CreateAndSubmitBid now returns the constructed signed bid even when the gossip submission fails, and signs with the TARGET slot's fork (the current-fork lookup was wrong at the first Gloas slot). BidSubmissionEvent carries status, the signed bid and the highest competitor bid (new tracker query excluding our own builder index). All value math is overflow-clamped.
RevealService resolves reveal timing/suppression exclusively from the slot's frozen plan: plan-disabled slots fire a terminal skipped result (reason plan_disabled) and keep the dedupe entry so no later request can publish; custom reveal times bypass the in-slot deadline for late-reveal testing. Envelope construction is split from network publish so every RevealResult attempt carries the built envelope (failed publishes stay inspectable), and envelope signing uses the TARGET slot's fork instead of the current fork. The InclusionTracker no longer stores won blocks — it builds the WonBlock summary for PayloadIncludedEvent only; durable storage moves to the slot results tracker. WonBlock/WonBlocksNamespace/WonBlockCodec stay for the wire shape and the one-time namespace migration.
…rding Bid serving in both dialects (legacy getHeader, Gloas getExecutionPayloadBid) is decided exclusively by the slot's frozen plan, resolved at request time: suppressed slots 204, plan-activated slots serve even when the module is globally disabled (the enabled flag keeps gating only the non-slot-scoped endpoints). planSvc is a constructor dependency of NewServer and both dialect handlers. Frozen per-slot values drive the response: subsidy override, absolute total bid value (legacy path now composes wei in uint256 — also fixes a pre-existing subsidy*1e9 uint64 overflow), and a context-cancellable response delay capped at one slot. Requests beyond currentSlot+1 are rejected with 400 BEFORE freezing, so arbitrary clients cannot lock future plans against edits. A narrow SlotResultRecorder interface (wired later by the results tracker) records bid outcomes — served only after a successful response write, suppressed/failed/cancelled otherwise, with the exact signed object for artifact capture and polling dedupe — plus block submission outcomes on all legacy v1/v2 and Gloas submit paths.
The plan service starts right after the chain service (step 7b), with kv_store persistence and a flush-before-close defer, and is a mandatory constructor dependency of the payload builder, p2p bidder, reveal service and Builder API server. Settings changes route to planSvc.UpdateConfig for next_n counter resets; the p2p bidder no longer takes or receives config. The inclusion tracker's state-db hookup is gone (won-block storage moves to the slot results tracker).
…facts New package owning the generic slot history: one SlotResult per slot where ePBS or the Builder API was active, with attempt-level records (build lifecycle incl. waiting_attributes/no_attributes baselines from a catch-up-bounded slot clock, bid attempts with statuses and competitor context, block submissions, reveal attempts, inclusion) plus the frozen applied plan snapshotted at record creation. Records are immutable copy-on-write values; attempts cap at 256/kind with a dropped counter; update events coalesce per slot for the SSE bridge. The ArtifactStore captures the raw SSZ objects (built payload, every signed bid with restart-safe per-slot indices, the signed envelope at construction time) through a bounded in-memory buffer (newest 64 slots, works without a state-db) and an async batching writer into the slot_artifacts table — hot paths never wait on SQLite. Capture toggles via slot-artifact-capture-enabled. The tracker consumes the services' blocking subscriptions (Subscribe* methods gained a blocking flag), implements the builderapi SlotResultRecorder contract, serves the Bids Won view unchanged (slot-descending, offset/limit), migrates the legacy won_blocks namespace once (merge-safe, idempotent, crash-safe across its two batches), and prunes summaries and artifacts to their separate epoch windows.
The tracker starts after all event sources exist and before any producer service (step 12b) so its blocking subscriptions never miss an event, persists via the slot_results namespace (running the won_blocks migration), drains before the state-db closes, and registers as the Builder API's result recorder. The WebUI's Bids Won endpoint reads the tracker's included-slot view (wire shape unchanged); the plan service and tracker are threaded into the API handler for the upcoming plan and result endpoints.
PlanUpdate gains a "set" member of dotted paths applied after the category patches, so partial edits never require full category objects: "bid.bid_min_amount": 5000 updates one override (null clears it back to inherit), "reveal.mode" switches modes, "builder_api" with null/object keeps the whole-category semantics. Setting a field on an absent category creates it as custom; unknown paths/fields are rejected via the strict decoder; paths apply in lexicographic order.
New REST surface:
- GET/POST /api/buildoor/action-plan — range reads and atomic bulk plan
mutations (path-based partial updates supported); auth + audit; frozen
or past slots map to 409; the response carries the authoritative
normalized plans so clients never guess patch results
- GET /api/buildoor/slot-results — attempt-level outcome history per
slot range
- GET /api/buildoor/slot-results/{slot}/payload|envelope|bids|bids/{i} —
raw artifacts with beacon-API content negotiation (q-value-aware
Accept parsing: application/octet-stream = exact SSZ bytes, JSON =
{"version","data"} envelope decoded per fork, 406 otherwise,
Eth-Consensus-Version + Vary headers)
- POST /api/config/settings — generic path-based global settings updates
keyed by canonical registry keys ({"epbs.bid_subsidy": 1000}); any
subset in one call, atomic, unknown keys rejected
SSE: new action_plan_updated (PlanChangeEvent) and slot_result_updated
(coalesced SlotResult) events; chain_info now carries slots_per_epoch
for the epoch×slot timetable. Swagger regenerated.
New view rendering the per-slot action plan as a timetable (rows = epochs, future on top; columns = slots_per_epoch from chain_info; sticky epoch column, live current-slot highlight). Cells show B/A/R category chips (custom = filled, disabled = struck) plus a result status dot (inclusion > failure/suppressed reveal > published > bid/served > planned-but-nothing-happened > payload-only). Clicking a future slot opens the edit modal: per-category mode select (inherit/custom/disabled) with unit-labeled override fields; saves prefer fine-grained "set" paths so single-field edits never clobber sibling overrides; 409 freeze conflicts surface as warnings. Past slots show the frozen applied plan (raw + resolved, forced markers), the full attempt history, and artifact access — JSON links plus SSZ blob downloads via Accept negotiation. Shift-click range selection and a from/to form feed one atomic bulk update. The view rides the existing SSE connection through a new onStreamEvent fan-out in useEventStream (no second EventSource), patches in-range state on action_plan_updated/slot_result_updated, and refetches after reconnects via a connection-generation counter.
Core-component entries for pkg/action_plan (scheduling authority, freeze semantics, per-slot enable override contract) and pkg/slot_results (attempt-aware history, artifact store, won_blocks ownership move); the new settings/flags; the plan/results/artifact/settings endpoints and SSE events; slot_artifacts in the state-db section; updated startup sequence (7b/12b) matching run.go 1:1; testing recipes and 409/artifact-retention troubleshooting notes.
…eplay cache Slot-scoped stream events are kept in a server-side replay cache (5-slot window, 2000-entry cap) and prefilled into each new client's channel at registration, so the UI restores the slot graph and event log on connect instead of rendering an empty page. Registration and broadcasting serialize on one mutex, making replay + live events a single ordered, gapless stream. Events carry a monotonic seq (seeded from wall-clock micros to survive restarts); the frontend keeps a high-watermark and drops already-processed events on reconnect while still applying gap events. Lifecycle events are tagged with the current slot so they replay too; per-client state snapshots and the REST-backed plan/result invalidation events stay uncached.
SlotTimeline sized its window from the moment the page loaded (firstValidSlotRef = current slot), so the SSE replay burst populated slotStates but the graph still started with only the current slot. Seed the window start from the earliest replayed slot instead.
BidsWonView opened its own EventSource in an effect keyed on refetch, whose identity changed every render. Each refetch re-ran the effect, reconnected, and the new connection immediately re-received the replayed bid_won events, refetching again — an endless request loop. Subscribe to bid_won through the shared connection's onStreamEvent fan-out instead (stable, and deduped by the seq watermark on reconnects) and memoize useBidsWon's refetch.
…grid The backend marshals phase0.Slot as a JSON string, so the SSE handler's 'typeof slot !== number' guard silently dropped every slot_result_updated event — the grid only ever showed REST-fetched history (which worked by accident via object-key coercion). Normalize slots to numbers at every ingest point. Also give BidsWonView its own useEventStream call: pages mount exclusively, so on the Bids Won tab nothing else established the shared connection its bid_won subscription feeds from.
A reveal skipped by the action plan (plan_disabled) or a missed deadline (late) was drawn with the same 'reveal-sent' dot as a successful reveal, so a disabled reveal looked like it had published. Give skipped attempts their own muted/dashed marker, thread the skip reason through the reveal SSE event into the dot tooltip and event log, and add a 'Reveal Withheld' legend entry.
The inclusion tracker now derives a canonical verdict for every won slot from the chain itself: on each head event it walks the head's ancestry (cached by block root) to the block at the won slot — a different root means the win was reorged out (orphaned); otherwise the first canonical block after the won slot proves via its committed parent execution hash whether our payload became canonical or was missed. Verdicts are re-evaluated for a 16-slot window so reorgs flip them; every change fires a PayloadStatusEvent, recorded on the slot result as inclusion.payload_status (pending until the first follow-up; pre-Gloas wins are canonical immediately) and pushed over SSE. The Action Plan cell bottom row now renders two dots: left = bid outcome (included/orphaned/bidding/failed/built/idle), right = payload canonical verdict (canonical/missed/reorged/pending), shown only for won slots. Legend and slot detail modal updated accordingly.
Adds a modeless 'build' category to the per-slot action plan with a reorg_parent_payload flag. When set, payload_builder builds the slot's payload on the grandparent (n-2) execution payload instead of the immediate parent: the FCU head hash, the parent block number and the withdrawals are sourced from the PARENT slot's cached payload attributes (whose parent is n-2), while every other property — incl. the beacon parent root — stays from the current slot. The effective attributes are built once and stored on the Payload, so the bid derived from it advertises the same parent it built on. Falls back to a normal build (logged) when the parent slot's attributes are unavailable or already share our parent. This is a deliberate parent-payload reorg attempt — rejected by mainnet forkchoice, useful for exercising reveal/inclusion against a withheld parent. The build category carries no custom/disabled mode; the flag is resolved into frozen.Build.ReorgParentPayload and never forces or suppresses the build decision itself. UI: a 'Build' section in the slot edit modal (normal parent / reorg parent, tri-state in bulk), an amber 'P' plan chip on the grid, the flag surfaced on the frozen-plan detail, and a legend entry.
…I testing Adds a modeless 'transforms' plan category letting operators rewrite the JSON of the built payload, the bid message, or the envelope message with arbitrary jq expressions — for custom builder testing the tool is not aware of. Backed by a new pkg/jqtransform wrapping itchyny/gojq (pure Go, environment access disabled, single-output enforced, context-timeout). Semantics: - payload: rewrites the built execution payload before it feeds both the bid commitment and the reveal (Payload.BlockHash re-synced from the result), so both stay consistent. - bid / envelope: rewrite the MESSAGE just before signing, then RE-SIGN with the target-slot fork. Results are validly signed but customized, and a bid commitment can deliberately diverge from the revealed payload for adversarial testing. Round-trip is MarshalJSON -> gojq -> UnmarshalJSON into a fresh object with Version preset (jqtransform.ApplyTyped). Expressions are jq-validated at plan-update time (400 on bad jq); a runtime transform failure fails that construction loudly rather than signing the untransformed object. Live testing: POST /api/buildoor/action-plan/test-transform runs the exact production gojq against a captured artifact for the slot (bid/envelope reduced to .message), else the latest buffered artifact, else an illustrative template. The slot edit modal gains a Transforms section with per-target expression inputs and a debounced input/output preview; grid cells show a 'jq' chip and the frozen-plan detail lists applied expressions.
… JSON TestTransformResponse.Input/Output were json.RawMessage, so they serialized as raw JSON objects while the frontend typed them as strings and rendered them directly in <pre> — React error #31 (object as child) on Test click. Return pretty-printed JSON as strings so the wire type matches and the UI renders verbatim.
The window was pinned once at init and never advanced, so a page left open across epochs kept showing the same range. Derive the window from the current epoch while 'following' (default) so it moves as epochs pass; navigating (Older/Newer/jump) pins an explicit range and 'Now' resumes following. A 'live' badge and highlighted Now button show the follow state.
fireUpdate fired the first update in a slot's 1s window and silently dropped every later one, so a burst's FINAL state (e.g. included) never reached clients — the Action Plan grid sat on the gray baseline until a manual refetch. Add trailing-edge coalescing: remember the latest suppressed result and flush it when the window closes, so the final state is always delivered (leading + trailing). Timer-driven flush is mutex-guarded and skips a stopped tracker.
…p cooldown The displayed builder balance = authoritative snapshot balance + a local adjustment (top-ups add, revealed bids subtract) that bridges the gap until the epoch snapshot reflects the operation. The adjustment was only cleared when the snapshot balance VALUE changed, so an idle builder whose balance stopped changing kept an optimistic 50 ETH top-up credit pinned to the display indefinitely (showed ~51 ETH vs ~1 ETH real). Anchor each adjustment delta to its epoch and drop it once the authoritative snapshot advances past that epoch (PaymentTracker. ReconcileToEpoch), driven from the always-running inclusion tracker's per-epoch hook so it works with lifecycle disabled too. Removes the now-unused balance-change reset. Re-enabling reconciliation also un-freezes auto-top-up (the phantom credit had made it think the builder was funded); add an 8-epoch top-up cooldown so a slow-to-land builder deposit isn't duplicated each epoch.
parithosh
approved these changes
Jul 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Per-slot action plan, slot outcome history & builder testing knobs
Adds a per-slot action plan to buildoor so operators can script different
behaviour for individual slots during Gloas/ePBS fork testing, records the
outcome of every active slot (down to the exact SSZ objects), and surfaces
both in a new Action Plan WebUI tab. On top of that it adds targeted
testing knobs — parent-payload reorg builds and arbitrary jq transforms of
the payload/bid/envelope — plus a batch of WebUI robustness fixes.
Buildoor is a Gloas/ePBS development & testing tool, not a mainnet builder;
several features here (underbidding, parent reorg, arbitrary object rewriting)
are deliberately adversarial and are rejected by mainnet forkchoice.
Branch:
pk910/testing-capabilities· base:main(2c374b8)What you can do now
and reveal on individual slots — even against the global enable flags — and
override amounts, windows, response delays, reveal timing. Plan far ahead; a
slot's plan freezes ~1 slot before execution and becomes immutable.
gets an attempt-level result record (build lifecycle, every bid with competitor
context, block submissions, reveal attempts, inclusion + canonical verdict) with
the frozen plan snapshotted in, plus the exact SSZ artifacts (built payload,
every signed bid, the envelope — even when a reveal was withheld) served as SSZ
or JSON via
Acceptnegotiation.per cell (bid status / canonical payload verdict), click-to-edit modals, bulk /
range editing, artifact downloads, all updating live over the existing SSE
connection.
execution payload — a deliberate parent-payload reorg attempt.
message, or envelope message (bid/envelope re-signed) — for custom builder
behaviour the tool isn't aware of — with a live in-modal expression tester.
Architecture
Four new packages and an extended data model, wired as a single per-slot
authority feeding downstream consumers by frozen snapshots.
pkg/action_plan— the per-slot scheduling authorityThe
PlanServiceis the single owner of per-slot scheduling and settings(the old
SlotManageris gone; the schedule modes all/every_nth/next_n andnext_n accounting live here). Consumers receive it as a mandatory constructor
dependency and poll it — no module interprets raw plans or schedule config
itself, no nil-guards, no fallback paths.
bid/builder_api/revealare three-state:absent = inherit the global baseline,
disabled= suppress,custom=force-active (even when the module is globally disabled) with optional
overrides. Two further modeless categories:
build(reorg_parent_payload)and
transforms(jq expressions).Freeze(slot)resolves an immutableFrozenPlan— rawplan + effective settings merged from the live global config + the target-slot
fork + the complete build decision — the first time any decision point touches
the slot; every later caller gets the identical snapshot, so later config
changes never rewrite a partially executed slot. Edits to past or frozen slots
fail with
ErrSlotLocked(HTTP 409).ApplyUpdatesis all-or-nothing, supports slotlists + inclusive ranges, three-state category patches, and fine-grained
setpaths (
"bid.bid_min_amount": 5000) so consumers never send a full categoryobject for a partial edit. Committed changes fire
PlanChangeEvent.kv_storeslot_plansnamespace; past plans prune toretention, future plans never.
pkg/slot_results— generic per-slot outcome historySlotResultper active slot: build lifecycle (incl.waiting_attributes/no_attributesbaselines from a slot clock, so "plannedbut nothing happened" is visible), bid attempts (both transports), block
submissions, reveal attempts, inclusion + a reorg-aware canonical payload
verdict — plus the frozen
applied_plan. Copy-on-write records, attempt capswith a dropped counter, coalesced SSE updates. Consumes the producer services'
blocking subscriptions so history is loss-free.
ArtifactStore— raw SSZ artifacts per slot (payload / every signed bid /envelope) with a write-through 64-slot memory buffer plus an async batching
writer into a dedicated
slot_artifactsSQLite table (blobs are too large forthe in-RAM memstore pattern; pruning needs a SQL range delete). Hot paths never
wait on SQLite.
migrates the legacy
won_blockskv namespace once on startup (merge-safe,idempotent, crash-safe).
pkg/jqtransform— sandboxed jqWraps
itchyny/gojq(pure Go, no I/O): environment access disabled,single-output enforced, context-timeout.
ApplyTypedround-trips afork-agnostic object through
MarshalJSON → gojq → UnmarshalJSONinto a freshobject with
Versionpreset.pkg/db—slot_artifactstableNew goose migration (
00003_slot_artifacts.sql) + repository:(slot, kind, idx)primary key, batched insert, range-delete prune.Feature detail
Reorg-aware canonical payload verdicts
The inclusion tracker no longer does a one-shot follow-up check. Each won slot is
tracked for a 16-slot window; on every head event it walks the head's ancestry
(cached by block root) down to the won slot to derive a verdict —
canonical(the next canonical block builds on our payload),
missed(builds on an olderexecution block: payload withheld / late / voted empty), or
orphaned(the wonblock itself was reorged out). Reorgs flip verdicts; every change fires a
PayloadStatusEventrecorded oninclusion.payload_statusand pushed over SSE.Parent-payload reorg build (
build.reorg_parent_payload)Builds the slot's payload on the grandparent (n-2) execution payload: the FCU
head hash, the parent block number, and the withdrawals come from the parent
slot's cached payload attributes (whose parent is n-2), while every other
property — including the beacon parent root — stays from the current slot. The
effective attributes are built once and stored on the
Payload, so the builtpayload and the bid derived from it agree on the reorged parent. Falls back
to a normal build (logged) when the parent slot's attributes are unavailable.
jq transforms (
transforms.payload/.bid/.envelope)commitment and the reveal (
Payload.BlockHashre-synced from the result).re-sign with the target-slot fork — so results are validly signed but
customized, and a bid commitment can deliberately diverge from the revealed
payload.
transform failure fails that construction loudly rather than signing the
untransformed object.
POST /api/buildoor/action-plan/test-transformruns theexact production gojq against a captured artifact for the slot (bid/envelope
reduced to
.message), else the latest buffered artifact, else an illustrativetemplate — powering a debounced input/output preview in the edit modal.
SSE connect-time replay
Every slot-scoped event is kept in a 5-slot server-side replay cache and
prefilled into a new client's channel at registration (registration and
broadcasting serialize on one mutex → one ordered, gapless stream). Events carry
a monotonic
seq(seeded from wall-clock micros to survive restarts); thefrontend keeps a high-watermark and drops replayed duplicates on reconnect. The
UI now restores the slot graph and event log on connect instead of starting
empty.
Security fix — Builder API plan-freeze DoS guard
getHeader/getExecutionPayloadBidreject slots beyondcurrentSlot+1(400)before freezing, so a client cannot lock arbitrary future slot plans.
API surface
REST (
pkg/webui/handlers/api):GET /api/buildoor/action-plan?min_slot=&max_slot=— per-slot plans in range.POST /api/buildoor/action-plan— atomic bulk mutation (three-statecategories +
setpaths); past/frozen slots → 409 (auth + audit).POST /api/buildoor/action-plan/test-transform— evaluate a jq expressionagainst a sample object.
GET /api/buildoor/slot-results?min_slot=&max_slot=— attempt-level history.GET /api/buildoor/slot-results/{slot}/payload|envelope|bids|bids/{index}—raw SSZ artifacts with
Accept-based SSZ/JSON content negotiation.POST /api/config/settings— generic path-based global settings update.GET /api/buildoor/bids-won— now served from the slot results tracker.SSE (
/api/events):action_plan_updated,slot_result_updated, theconnect-time replay burst +
seq, plus the existing event set.Config:
--slot-result-retention-epochs(100),--slot-artifact-retention-epochs(100),
--slot-artifact-capture-enabled(true),--epbs-bid-value-override,--builder-api-value-override.Notable fixes bundled in
Dispatcher.Unsubscribenever removed subscriptions (reversed guard) —this leak also blocked loss-free blocking subscriptions; fixed with tests.
fork — wrong at the first Gloas slot; both fixed.
uint64overflow (subsidy * 1e9) — moved touint256.
not applying live
slot_result_updated(slot marshaled as a JSON string);withheld reveals rendered as successful; replayed slots not shown in the
timeline; transform test input/output rendered as objects (React Remove extra data modifier for the payload #31).
Data & migration notes
--state-db <path>is set. With it:settings,
kv_store(incl.slot_plans,slot_results), and theslot_artifactstable persist across restarts; the legacywon_blocksnamespace is migrated into
slot_resultsonce and deleted.00003_slot_artifacts.sql), applied automatically.Testing
action_plan(freeze truth table, bulk/path updates,build & transform categories),
slot_results(tracker, artifacts, migration),jqtransform(env-blocking, single-output, cancellation), the inclusiontracker's reorg verdicts, the bid/envelope transform round-trip + re-sign, the
SSE replay cache, and the transform test endpoint.
make build, fullgo test ./...,-raceon the touched packages,tsc --noEmit, and the production webpack build — all green.