feat(payload): add shared resource metering evaluator - #4673
Conversation
Define one versioned schedule and decision model for both payload builders, including per-dimension dry-run, fail-open arithmetic, and transaction-versus-block scope. Move provider and rejection-cache ownership into the payload crate while preserving builder-core re-exports.
🟡 Heimdall Review Status
|
| /// Independently budgeted resource dimensions. | ||
| pub dimensions: Vec<ResourceMeteringDimension>, | ||
| #[serde(skip)] | ||
| operation_index: HashMap<String, Vec<(usize, u64, u64)>>, |
There was a problem hiding this comment.
nit: operation_index uses HashMap (non-deterministic iteration order). This is fine here since evaluate() only does key lookups (not iteration), but worth noting that the Serialize derive on ResourceMeteringSchedule will skip this field (#[serde(skip)]), so serialization determinism is preserved. If a future change iterates operation_index in a consensus-sensitive path, it should switch to BTreeMap.
| ); | ||
| if decision.should_exclude() { | ||
| self.record_decision(tx_hash, &decision); | ||
| } | ||
| (simulated, decision) |
There was a problem hiding this comment.
Note: predict only records decisions where should_exclude() is true (enforced throttles). Dry-run throttle observations at the prediction stage are silently dropped — the metric/log won't fire until decide_executed runs after execution. This means if a caller uses predict to skip execution entirely (which it can't today since should_exclude() is false for dry-run), the observation would be lost.
This is fine given the current flow where CalculationFailed and dry-run Throttle both proceed to execution, but it's worth documenting explicitly that predict intentionally under-reports dry-run observations, since a future caller might expect the metrics to be recorded here regardless of the exclude decision.
Review SummaryPR: feat(payload): add shared resource metering evaluator This PR introduces a versioned resource-metering schedule and decision model ( Block-Production Safety AssessmentThis PR is block-production-sensitive (touches metering and payload assembly paths). After reviewing against the block production review guide:
No critical block-production findings. Minor Findings
What Looks Good
|
|
Caution This PR may regress performance. 1 benchmark(s) slower by more than 10% beyond the noise band: Benchmark results (advisory)Median time on the PR head versus the base branch, measured on the same host. Wall-clock, so a change is only flagged when it clears ±10% and the confidence intervals do not overlap. Only benchmarks past the ±10% threshold (plus new or dropped ones) are listed. This check never blocks a merge.
46 benchmark(s) within ±10% omitted. |
| } | ||
|
|
||
| /// Executed admission check. Records the final decision. | ||
| pub fn decide_executed( |
There was a problem hiding this comment.
A bit confused by the naming here, who calls it and what does it do exactly?
There was a problem hiding this comment.
Renamed to simulated_admission / executed_admission so they pair with simulated_sample and name the data source instead of a vague verb.
Payload builders call simulated_admission before EVM execution and skip when should_exclude() is true. The returned sample is passed to executed_admission after execution. Sequencer txs still go through unthrottled_usage / account_unthrottled.
| @@ -0,0 +1,1565 @@ | |||
| //! Versioned resource-metering schedules and their transaction-cost evaluator. | |||
There was a problem hiding this comment.
Is this file purely new code or is it copies / moved from somewhere existing?
There was a problem hiding this comment.
Mostly new. The evaluator, schedule JSON, samples, and decision types in this file did not exist before. What moved from builder-core in this PR is MeteringProvider (metering.rs) and RejectionCache (rejection_cache.rs). The old wall-clock execution-time throttle stays in builder-core until #4677; this file is not a copy of that.
…ed admission The old names hid that these are meterBundle vs post-state admission checks, not a forecast or a generic decision helper.
|
📊 14 benchmark(s) added — baselines recorded (deterministic instruction counts under Valgrind). Per-change deltas will appear on future PRs, once these land on the base branch. View run Benchmark details (14)
|
| .copied() | ||
| .unwrap_or_default() | ||
| .checked_add(transaction_cost) | ||
| .ok_or(ResourceThrottlingCheckError::ArithmeticOverflow)?; |
There was a problem hiding this comment.
The early ? return from checked_add overflow exits the loop before later dimensions are examined. If dimension N has a cumulative overflow but dimension N+1 has an enforced transaction-scope violation, the method returns ArithmeticOverflow (→ CalculationFailed → fail-open) instead of the permanent transaction-scope rejection.
This breaks the priority invariant documented in the method doc comment ("enforced transaction-scope, then enforced block-scope, then the first dry-run overrun"). In practice u128 cumulative overflow is near-impossible, but the early return silently downgrades a permanent eviction to a fail-open pass.
Consider accumulating the overflow like the other error classes instead of returning early:
| .ok_or(ResourceThrottlingCheckError::ArithmeticOverflow)?; | |
| .ok_or(ResourceThrottlingCheckError::ArithmeticOverflow); | |
| let used = match used { | |
| Ok(used) => used, | |
| Err(_) => continue, | |
| }; |
This way the loop continues and can still find an enforced transaction-scope violation in a later dimension.
Review SummaryWell-structured PR that introduces a shared resource-metering evaluator with versioned schedules and moves metering/rejection-cache ownership to the payload crate. The fail-open design throughout is the right approach for block-production safety. FindingsMinor (1 finding):
Block Production AssessmentThis PR is block-production-sensitive (metering, tx selection). The fail-open design is consistently applied:
No block-production halt/stall risks identified. Other Observations (no action needed)
|
…d_usage Name the two limit checks after the sample they use. Neither method accumulates block usage; callers apply that after include.
Review SummaryThis PR introduces a shared, versioned resource-metering schedule and evaluator for payload builders, with well-designed fail-open semantics throughout. The code moves metering provider and rejection cache ownership to the payload crate and adds comprehensive validation, evaluation, and throttling logic. Block Production SafetyThe PR is block-production-sensitive (touches metering and payload assembly). After tracing all error paths in Existing Inline Findings (from prior review)Three inline comments were previously posted and remain relevant:
Additional Observations (no new inline comments needed)
Overall the design is solid with thorough test coverage of edge cases (overflow, fail-open, dry-run vs enforce, priority ranking). The primary actionable item from prior reviews is the |
Summary
dryRun(omitted = enforce), fail-open arithmetic, and transaction-versus-block throttle scope.Stacked replacement for the shared evaluator slice of #4428. Later PRs in this stack add observations, native admission, Flashblocks admission, and wall-clock retirement.
Test plan
cargo test --lib -p base-execution-payload-buildertype=routine
risk=low
impact=sev5