Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 35 additions & 8 deletions core/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ type StateTransition struct {
msg *Message
gasRemaining uint64
initialGas uint64
gasSurcharge uint64
gasSurchargeReason tracing.GasChangeReason
state vm.StateDB
evm *vm.EVM
feeCharged bool
Expand All @@ -253,6 +255,17 @@ func NewStateTransition(evm *vm.EVM, msg *Message, gp *GasPool, feeCharged bool,
}
}

// WithGasSurcharge configures Execute to reserve non-refundable gas after preCheck
// and before intrinsic gas. Call it before Execute; ApplyMessage does not apply a
// surcharge. The reservation is included in UsedGas and fees and excluded from
// refund caps and the data floor. A shortfall returns ErrIntrinsicGas. reason is
// the tracer category emitted when the reservation is taken.
func (st *StateTransition) WithGasSurcharge(gas uint64, reason tracing.GasChangeReason) *StateTransition {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] The doc comment covers the accounting well but omits the usage contract a caller needs: it must be called before Execute(), it applies only to this top-level transition (ApplyMessage and nested calls are unaffected), and a gas limit below the surcharge surfaces as ErrIntrinsicGas. Worth stating, since the only caller lives outside this repo.

st.gasSurcharge = gas
st.gasSurchargeReason = reason
return st
}

// to returns the recipient of the message.
func (st *StateTransition) to() common.Address {
if st.msg == nil || st.msg.To == nil /* contract creation */ {
Expand Down Expand Up @@ -440,6 +453,15 @@ func (st *StateTransition) Execute() (*ExecutionResult, error) {
if err := st.preCheck(); err != nil {
return nil, err
}
if st.gasRemaining < st.gasSurcharge {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] Reusing ErrIntrinsicGas for a surcharge shortfall produces the self-contradictory message "intrinsic gas too low: have 999, want surcharge 1000", and makes surcharge failures indistinguishable from real intrinsic-gas failures for any errors.Is(err, ErrIntrinsicGas) consumer (txpool, RPC error mapping, metrics). A dedicated sentinel — e.g. ErrGasSurcharge in core/error.go, optionally wrapping ErrIntrinsicGas for backwards compatibility — would let callers tell the two apart.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] Reusing ErrIntrinsicGas for a surcharge shortfall makes the two failures indistinguishable to callers — RPC error mapping, txpool rejection reasons, and metrics all see "intrinsic gas too low" for a condition the sender cannot fix by looking at intrinsic cost. Consider a dedicated ErrGasSurcharge that wraps ErrIntrinsicGas (fmt.Errorf("%w: ...", ErrIntrinsicGas) already gives errors.Is compatibility, so a named sentinel costs nothing) so Sei's integration can report the actual cause.

return nil, fmt.Errorf("%w: have %d, want surcharge %d", ErrIntrinsicGas, st.gasRemaining, st.gasSurcharge)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] Reusing ErrIntrinsicGas conflates "gas limit below the node-injected surcharge" with a genuine intrinsic-gas shortfall — callers (txpool, RPC error mapping) can't tell them apart, and the message text (want surcharge N) is the only signal. Consider a distinct sentinel (e.g. ErrGasSurcharge) wrapping/alongside it. Note this is a consensus-level rejection of the whole transaction, not a failed execution, which is why distinguishing it matters for admission control.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Reusing ErrIntrinsicGas renders as intrinsic gas too low: have N, want surcharge M, which reads oddly and is indistinguishable from a genuine intrinsic-gas shortfall for callers matching on the sentinel. Reusing the class is a reasonable deliberate choice (it keeps the miner skipping the tx rather than treating the block as invalid), so if that's intentional a one-line comment saying so would help; otherwise a dedicated sentinel wrapping ErrIntrinsicGas would let callers tell them apart.

}
if st.gasSurcharge > 0 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] This emits OnGasChange unconditionally on the caller-supplied reason, including when the caller passes tracing.GasChangeIgnored. Elsewhere (core/vm/contract.go:133,145) GasChangeIgnored means "suppress the hook, the change is emitted manually." Consider honoring that convention here for consistency: if reason != tracing.GasChangeIgnored.

if t := st.evm.Config.Tracer; t != nil && t.OnGasChange != nil {
t.OnGasChange(st.gasRemaining, st.gasRemaining-st.gasSurcharge, st.gasSurchargeReason)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] The surcharge OnGasChange fires before the intrinsic-gas check (line 476) and the floor-data check (line 486), so a tracer observes the surcharge deduction for transactions that are then rejected and never execute. Upstream's GasChangeTxInitialBalance in initGas has the same property, so this is consistent — but if you want the hook to only reflect executed transactions, moving the emission after the two admission checks (keeping the gasRemaining subtraction where it is) would do it.

}
st.gasRemaining -= st.gasSurcharge
}

var (
msg = st.msg
Expand All @@ -462,8 +484,8 @@ func (st *StateTransition) Execute() (*ExecutionResult, error) {
if err != nil {
return nil, err
}
if msg.GasLimit < floorDataGas {
return nil, fmt.Errorf("%w: have %d, want %d", ErrFloorDataGas, msg.GasLimit, floorDataGas)
if st.gasRemaining < floorDataGas {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Worth a short comment noting why this diverges from upstream's msg.GasLimit < floorDataGas: initGas sets gasRemaining = msg.GasLimit, so with no surcharge this is identical, and with a surcharge it deliberately checks the post-surcharge execution budget. As written the line looks like an unexplained deviation and will draw a question (or a bad resolution) on every upstream rebase of this file.

return nil, fmt.Errorf("%w: have %d, want %d", ErrFloorDataGas, st.gasRemaining, floorDataGas)
}
}
if t := st.evm.Config.Tracer; t != nil && t.OnGasChange != nil {
Expand Down Expand Up @@ -538,9 +560,9 @@ func (st *StateTransition) Execute() (*ExecutionResult, error) {
st.gasRemaining += gasRefund
if rules.IsPrague {
// After EIP-7623: Data-heavy transactions pay the floor gas.
if st.gasUsed() < floorDataGas {
if st.executionGasUsed() < floorDataGas {
prev := st.gasRemaining
st.gasRemaining = st.initialGas - floorDataGas
st.gasRemaining = st.initialGas - st.gasSurcharge - floorDataGas
if t := st.evm.Config.Tracer; t != nil && t.OnGasChange != nil {
t.OnGasChange(prev, st.gasRemaining, tracing.GasChangeTxDataFloor)
}
Expand Down Expand Up @@ -647,11 +669,11 @@ func (st *StateTransition) applyAuthorization(auth *types.SetCodeAuthorization)
func (st *StateTransition) calcRefund() uint64 {
var refund uint64
if !st.evm.ChainConfig().IsLondon(st.evm.Context.BlockNumber) {
// Before EIP-3529: refunds were capped to gasUsed / 2
refund = st.gasUsed() / params.RefundQuotient
// Before EIP-3529: refunds were capped to execution gas / 2
refund = st.executionGasUsed() / params.RefundQuotient
} else {
// After EIP-3529: refunds are capped to gasUsed / 5
refund = st.gasUsed() / params.RefundQuotientEIP3529
// After EIP-3529: refunds are capped to execution gas / 5
refund = st.executionGasUsed() / params.RefundQuotientEIP3529
}
if refund > st.state.GetRefund() {
refund = st.state.GetRefund()
Expand All @@ -662,6 +684,11 @@ func (st *StateTransition) calcRefund() uint64 {
return refund
}

// executionGasUsed returns gas consumed by intrinsic cost and EVM execution, excluding the surcharge.
func (st *StateTransition) executionGasUsed() uint64 {
return st.gasUsed() - st.gasSurcharge

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] executionGasUsed underflows if it is ever reached before the surcharge has been deducted from gasRemaining. Both current call sites (refund cap, Prague floor) are post-deduction and safe — refunds are bounded by executionGasUsed/5, so the post-refund gasRemaining still cannot exceed initialGas - gasSurcharge — but a one-line comment stating that precondition would keep a future early-return from silently wrapping around.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] Worth a doc comment stating the invariant, since a violation wraps silently rather than panicking. The subtraction is safe today because calcRefund caps the refund at executionGasUsed()/quotient, which keeps gasRemaining <= initialGas - gasSurcharge at both call sites (line 562 and line 672/675) — but that reasoning is non-local and easy to break with a future refund change. Something like:

// executionGasUsed returns the gas consumed by intrinsic cost and EVM execution,
// excluding the non-refundable surcharge. Safe from underflow because gasRemaining
// never exceeds initialGas-gasSurcharge after the surcharge is reserved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] This subtraction is unguarded. It is safe today — Execute always deducts the surcharge before calcRefund or the Prague floor branch can run, and the check at line 456 guarantees gasRemaining >= gasSurcharge — but if a future path ever reaches calcRefund without the deduction, this wraps to ~2^64 and the EIP-3529 refund cap silently disappears, which is a consensus-visible failure with no error. Either clamp (if st.gasUsed() < st.gasSurcharge { return 0 }) or state the invariant in the comment so the dependency on ordering is explicit.

}

// returnGas returns ETH for remaining gas,
// exchanged at the original rate.
func (st *StateTransition) returnGas() {
Expand Down
Loading
Loading